← Public packages
@kentcdodds/x
X API v2 helpers for tweets, search, legacy DMs, and encrypted X Chat via a Fly XDK sidecar.
src/worker-source.ts
69 lines · 7.9 KB · TypeScript/**
* RETIRED (2026-09-26): X Activity CRC shim Worker. Platform
* `challenge: x-activity-crc` on the minted `activity-event` webhook
* replaces this. Kept as reference only — do not redeploy for go-live.
*/
/**
* Return deployable Cloudflare Worker source that answers X Activity CRC /
* signature checks anonymously and forwards verified events into Kody
* `kody:@kentcdodds/kodykoala-activity/handle-activity-event` via minted webhook.
*
* Files only — does not deploy. Same pattern as
* `@kentcdodds/youtube-livestream-vod-manager/worker-source`.
*
* @returns Worker files (`src/worker.js`, `wrangler.toml`) and required secrets.
*
* @example
* import getWorkerSource from 'kody:@kentcdodds/x/worker-source'
* const bundle = getWorkerSource()
* // => { name: 'x-activity-proxy', files: { 'src/worker.js': '...' }, env: {...} }
*/
const WORKER_JS = "function json(body, status = 200) {\n\treturn new Response(JSON.stringify(body, null, 2), {\n\t\tstatus,\n\t\theaders: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },\n\t})\n}\n\nfunction requiredEnv(env, name) {\n\tconst value = env[name]\n\tif (!value) throw new Error(name + ' is required')\n\treturn value\n}\n\nfunction toBase64(buf) {\n\treturn btoa(String.fromCharCode(...new Uint8Array(buf)))\n}\n\nfunction toHex(buf) {\n\treturn [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nasync function hmacSha256(secret, message) {\n\tconst enc = new TextEncoder()\n\tconst key = await crypto.subtle.importKey(\n\t\t'raw',\n\t\tenc.encode(secret),\n\t\t{ name: 'HMAC', hash: 'SHA-256' },\n\t\tfalse,\n\t\t['sign'],\n\t)\n\treturn crypto.subtle.sign('HMAC', key, enc.encode(message))\n}\n\nasync function crcResponseToken(crcToken, consumerSecret) {\n\treturn 'sha256=' + toBase64(await hmacSha256(consumerSecret, crcToken))\n}\n\nfunction safeEqual(a, b) {\n\tif (a.length !== b.length) return false\n\tlet x = 0\n\tfor (let i = 0; i < a.length; i++) x |= a.charCodeAt(i) ^ b.charCodeAt(i)\n\treturn x === 0\n}\n\nasync function verifyWebhookSignature(body, signatureHeader, consumerSecret) {\n\tconst header = (signatureHeader || '').trim()\n\tif (!header) return { ok: false, reason: 'missing-signature' }\n\tconst digest = await hmacSha256(consumerSecret, body)\n\tconst b64 = 'sha256=' + toBase64(digest)\n\tconst hex = 'sha256=' + toHex(digest)\n\tconst normalized = header.toLowerCase().startsWith('sha256=') ? header : 'sha256=' + header\n\tconst ok = safeEqual(normalized, b64) || safeEqual(normalized.toLowerCase(), hex.toLowerCase())\n\treturn { ok, reason: ok ? 'matched' : 'mismatch' }\n}\n\nfunction pickSignatureHeader(headers) {\n\treturn (\n\t\theaders.get('x-twitter-webhooks-signature-oauth2') ||\n\t\theaders.get('x-twitter-webhooks-signature') ||\n\t\tnull\n\t)\n}\n\nfunction normalizePath(pathname) {\n\tif (pathname.length > 1 && pathname.endsWith('/')) return pathname.slice(0, -1)\n\treturn pathname || '/'\n}\n\nasync function handleCrc(request, env) {\n\tconst url = new URL(request.url)\n\tconst crcToken = url.searchParams.get('crc_token') || url.searchParams.get('CRC_TOKEN')\n\tif (!crcToken) {\n\t\treturn json({\n\t\t\tok: true,\n\t\t\tservice: 'x-activity-proxy',\n\t\t\thint: 'X Activity CRC + event proxy for @kodykoala → Pam',\n\t\t\thasClientSecret: Boolean(env.X_CLIENT_SECRET),\n\t\t\thasKodyWebhook: Boolean(env.KODY_WEBHOOK_URL),\n\t\t})\n\t}\n\tif (!env.X_CLIENT_SECRET) {\n\t\treturn json(\n\t\t\t{\n\t\t\t\tok: false,\n\t\t\t\terror: 'x-client-secret-unset',\n\t\t\t\thint: 'Set Worker secret X_CLIENT_SECRET from Kody xClientSecret',\n\t\t\t},\n\t\t\t503,\n\t\t)\n\t}\n\tconst response_token = await crcResponseToken(crcToken, env.X_CLIENT_SECRET)\n\treturn json({ response_token })\n}\n\nasync function forwardToKody(env, payload) {\n\tconst webhookUrl = env.KODY_WEBHOOK_URL\n\tif (!webhookUrl) {\n\t\treturn { forwarded: false, reason: 'kody-webhook-url-unset' }\n\t}\n\tconst response = await fetch(webhookUrl, {\n\t\tmethod: 'POST',\n\t\theaders: { 'content-type': 'application/json' },\n\t\tbody: JSON.stringify({\n\t\t\tparams: { payload, dryRun: false },\n\t\t\tsource: 'x-activity-proxy',\n\t\t\ttopic: 'x.activity.event',\n\t\t}),\n\t})\n\tconst text = await response.text()\n\tlet body = null\n\ttry {\n\t\tbody = text ? JSON.parse(text) : null\n\t} catch {\n\t\tbody = { raw: text.slice(0, 1000) }\n\t}\n\treturn {\n\t\tforwarded: true,\n\t\tstatus: response.status,\n\t\tok: response.ok || response.status === 202,\n\t\tbody,\n\t}\n}\n\nasync function handleEvent(request, env) {\n\tconst bodyText = await request.text()\n\tif (!env.X_CLIENT_SECRET) {\n\t\treturn json({ ok: false, error: 'x-client-secret-unset' }, 503)\n\t}\n\tconst signature = await verifyWebhookSignature(\n\t\tbodyText,\n\t\tpickSignatureHeader(request.headers),\n\t\tenv.X_CLIENT_SECRET,\n\t)\n\tif (!signature.ok) {\n\t\treturn json({ ok: false, signature }, 401)\n\t}\n\n\tlet payload = null\n\ttry {\n\t\tpayload = bodyText ? JSON.parse(bodyText) : null\n\t} catch {\n\t\treturn json({ ok: false, error: 'invalid-json' }, 400)\n\t}\n\n\t// Always 200 after verify so X does not retry-storm; forward errors are recorded.\n\tlet forward\n\ttry {\n\t\tforward = await forwardToKody(env, payload)\n\t} catch (error) {\n\t\tforward = {\n\t\t\tforwarded: false,\n\t\t\treason: 'forward-threw',\n\t\t\terror: error instanceof Error ? error.message.slice(0, 300) : String(error).slice(0, 300),\n\t\t}\n\t}\n\treturn json({\n\t\tok: true,\n\t\treceivedAt: new Date().toISOString(),\n\t\tsignature,\n\t\tforward,\n\t})\n}\n\nexport default {\n\tasync fetch(request, env) {\n\t\tconst url = new URL(request.url)\n\t\tconst path = normalizePath(url.pathname)\n\t\tif (request.method === 'GET' && path === '/health') {\n\t\t\treturn json({ ok: true, service: 'x-activity-proxy' })\n\t\t}\n\t\tif (request.method === 'GET' && (path === '/' || path === '/activity')) {\n\t\t\treturn await handleCrc(request, env)\n\t\t}\n\t\tif (request.method === 'POST' && (path === '/' || path === '/activity')) {\n\t\t\treturn await handleEvent(request, env)\n\t\t}\n\t\treturn json({ ok: false, error: 'not-found' }, 404)\n\t},\n}\n"
const WRANGLER_TOML = 'name = "x-activity-proxy"\nmain = "src/worker.js"\ncompatibility_date = "2026-06-25"\n'
const README = "# X Activity Proxy Worker\n\nRETIRED Worker shim. Prefer @kentcdodds/kodykoala-activity minted webhook.\n\n## Required secrets\n\n- `X_CLIENT_SECRET`: X app consumer secret (`xClientSecret`) for CRC + HMAC.\n- `KODY_WEBHOOK_URL`: minted Kody inbound webhook URL for `activity-event`\n (package settings reveal; never paste into chat).\n\n## Routes\n\n- `GET /health` — liveness\n- `GET /activity?crc_token=` — X CRC challenge\n- `POST /activity` — signed Activity events → Kody `handle-activity-event`\n\nRegister with X:\n\n```ts\nawait registerActivityWebhook({\n url: 'https://x-activity-proxy.kentcdodds.workers.dev/activity',\n confirm: true,\n})\n```\n"
/**
* Return deployable Cloudflare Worker source for the X Activity public proxy.
* @returns Worker files and required secret names.
* @example
* import getWorkerSource from 'kody:@kentcdodds/x/worker-source'
* const bundle = getWorkerSource()
*/
export function getWorkerSource() {
return {
name: 'x-activity-proxy',
publicUrl: 'https://x-activity-proxy.kentcdodds.workers.dev/activity',
files: {
'package.json': JSON.stringify(
{
name: 'x-activity-proxy',
private: true,
type: 'module',
scripts: { deploy: 'wrangler deploy' },
devDependencies: { wrangler: 'latest' },
},
null,
2,
),
'wrangler.toml': WRANGLER_TOML,
'README.md': README,
'src/worker.js': WORKER_JS,
},
env: {
requiredSecrets: ['X_CLIENT_SECRET', 'KODY_WEBHOOK_URL'],
vars: ['CALLBACK_URL'],
},
}
}
/**
* Return deployable Cloudflare Worker source for the X Activity public proxy.
* @returns Worker files and required secret names.
* @example
* import getWorkerSource from 'kody:@kentcdodds/x/worker-source'
* const bundle = getWorkerSource()
*/
export default getWorkerSource