Skip to content
← Public packages

@kentcdodds/x

X API v2 helpers for tweets, search, legacy DMs, and encrypted X Chat via a Fly XDK sidecar.

src/adapt.ts

124 lines · 4.4 KB · TypeScript
import { packageStorage } from 'kody:runtime'
import {
	CHAT_PIN_SECRET,
	DEFAULT_OAUTH_INTEGRATION,
	DEFAULT_SIDECAR_URL,
	FLY_APP_NAME,
	SIDECAR_TOKEN_SECRET,
	SIDECAR_URL_VALUE,
} from './owner-config.ts'
import { resolveSidecarUrl, secretSetupUrls, sidecarHost } from './sidecar.ts'

/**
 * After a community fork, call this first.
 *
 * Returns every owner-specific setting and the exact swap steps so you can
 * point this copy at your own X account and Fly sidecar, then publish.
 *
 * @example
 * import adapt from 'kody:@you/x/adapt'
 * const guide = await adapt()
 */
export default async function adapt() {
	const stored = await packageStorage().get(SIDECAR_URL_VALUE)
	const storedUrl = typeof stored === 'string' ? stored.trim() : ''
	const resolvedUrl = await resolveSidecarUrl()
	const host = sidecarHost(resolvedUrl)
	const setup = secretSetupUrls(resolvedUrl)
	const usingListingDefault = !storedUrl

	return {
		purpose:
			'Swap this forked X package onto your own OAuth account and Fly XDK sidecar, then publish (and optionally community-publish) your copy.',
		doNot: [
			'Do not generate a new X Chat keypair. Unlock the existing identity with the PIN the user already types on x.com.',
			'Do not ask the user to paste the Chat PIN in chat. Send the prefilled secret URL.',
			'Do not send the PIN or sidecar token to api.x.com.',
			'Do not keep calling the listing owner’s Fly host after a fork.',
		],
		ownerSettings: {
			sidecarUrl: {
				storageName: SIDECAR_URL_VALUE,
				resolved: resolvedUrl,
				source: storedUrl ? 'package-storage' : 'owner-config-default',
				listingDefault: DEFAULT_SIDECAR_URL,
				storedValue: storedUrl || null,
				needsSwap: usingListingDefault,
			},
			flyApp: {
				listingDefault: FLY_APP_NAME,
				note: 'Deploy your own Fly app. Pass `app` to `./sidecar/source` so fly.toml matches.',
			},
			secrets: {
				xChatPin: CHAT_PIN_SECRET,
				xChatSidecarToken: SIDECAR_TOKEN_SECRET,
				setupUrls: setup,
				approveHostOnly: host,
			},
			oauth: {
				defaultIntegration: DEFAULT_OAUTH_INTEGRATION,
				connectUrl: `https://kody.codes/connect/oauth?provider=${DEFAULT_OAUTH_INTEGRATION}`,
				extraAccounts: 'Connect `x-<purpose>` and pass `account: \'<purpose>\'`.',
			},
		},
		steps: [
			{
				id: 'intent',
				title: 'Rewrite README ## Intent',
				detail:
					'Community forks must describe the forking user’s goal, not the listing author’s. Keep the section short.',
			},
			{
				id: 'oauth',
				title: 'Connect your X OAuth integration',
				detail: `Open https://kody.codes/connect/oauth?provider=${DEFAULT_OAUTH_INTEGRATION} (or \`x-<purpose>\` for extra accounts). Token secrets come from that connect flow — do not hard-code aliases.`,
			},
			{
				id: 'sidecar',
				title: 'Deploy your own Fly XDK sidecar',
				detail:
					'Import `./sidecar/source` with your Fly app name. Deploy those files. Set machine env SIDECAR_TOKEN to a new random bearer. Never reuse the listing owner’s app or token.',
				example: {
					export: './sidecar/source',
					params: { app: 'your-x-chat' },
					expectedUrl: 'https://your-x-chat.fly.dev',
				},
			},
			{
				id: 'sidecar-url',
				title: `Save ${SIDECAR_URL_VALUE} in this package's storage`,
				detail:
					'No code edit required. After publish, invoke `./migrate-from-values` with your Fly origin so get/send chat stop using the listing default.',
				example: {
					export: './migrate-from-values',
					params: { sidecarUrl: 'https://your-x-chat.fly.dev' },
				},
			},
			{
				id: 'secrets',
				title: 'Save Chat PIN and sidecar token',
				detail:
					'Use the prefilled URLs (never the bare secrets page). Approve only your Fly host on both secrets — not Juicebox, not api.x.com.',
				setupUrls: setup,
			},
			{
				id: 'smoke',
				title: 'Smoke-test on your copy',
				detail:
					'Invoke `./list-chat-conversations` (no sidecar). Then `./get-chat-conversation` on one thread. `./send-chat` stays dry-run unless the user explicitly confirms a send.',
			},
			{
				id: 'publish',
				title: 'Publish your package',
				detail:
					'Git lane: push, then `package_publish_external_push`. Tool-only: `repo_publish_session`. Confirm `private` is omitted or false if you want a community listing.',
			},
			{
				id: 'community',
				title: 'Publish the community listing',
				detail:
					'Call `community_publish` with this package id after the account publish succeeds. Share the user URL `/@username/x`, not `/community/{listingId}`.',
			},
		],
	}
}