/** * Cloudflare-target smoke gate: builds the CF bundle, boots it under a real * workerd (`wrangler dev`) against the in-memory fake Slack + fake provider * backend, and drives the full Slack-native first-run story — programmatic app * creation, confidential bot OAuth, signed Events proof, first-Owner OIDC — * then signed Slack events end-to-end. Asserts the parts that only * workerd can prove: * * 1. a deploy-minted setup capability creates no person or browser session, * while exact installer OIDC creates the first Owner and session last, * 2. the DO-backed config store seeds and serves /admin/api/agents, * 3. the app boots healthy with no Slack installation and Events fail closed, * 4. app creation, bot install, issued scopes, directory/channel probes, * separately signed Events proof, and OIDC all use the exact fake realm, * 4. a signed synthetic app_mention verifies against the STORED signing * secret, is admitted, and the turn delivers a final to (fake) Slack * through the in-process dispatch + waitUntil path, * 5. an identical redelivery is deduped by the DO claim store, * 6. a workerd RESTART (same --persist-to) still dedupes the original * event, still verifies with the stored secret, and still admits an * implicit thread reply — Durable Object state survives the process, * 7. a tampered signature is rejected (the stored secret is really used). * * No secrets, no external traffic: every outbound URL points at 127.0.0.1. * Exit 0 on success, 1 with diagnostics on failure. The default/core run * builds and structurally validates Sandbox before rebuilding and booting core; * SMOKE_SKIP_BUILD=1 is available only to an explicit Sandbox-profile run that * reuses an existing dist-cf artifact for iteration speed. */ import { spawn, spawnSync } from 'node:child_process'; import { createHash, generateKeyPairSync } from 'node:crypto'; import { existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import vm from 'node:vm'; import { hasScheduledComposition } from './worker-artifact.mjs'; import { readBuildIdentity } from './lib/build-identity.mjs'; import { REPO_ROOT, SIGNING_SECRET, EVENTS_PATH, assertNodeVersion, getFreePort, loadFake, loadTsModule, postSignedEvent, delay, } from './lib/offline-harness.mjs'; import { runDrainCheck } from './lib/cf-drain-check.mjs'; import { cloudflareSmokeTelemetryDevVars, withCloudflareSmokeTelemetryBindings, } from './lib/cf-smoke-telemetry.mjs'; import { classifyCloudflareDeploymentProfile, resolveCloudflareDeploymentProfile, } from './cloudflare-deployment-profile.mjs'; import { mintSetupCapability, SETUP_CAPABILITY_DIGEST_BINDING, SETUP_CAPABILITY_ISSUED_AT_BINDING, } from '../src/auth/setup-capability.mjs'; const WRANGLER_BIN = join(REPO_ROOT, 'node_modules', '.bin', 'wrangler'); const CF_OUTPUT_DIR = join(REPO_ROOT, 'dist-cf'); export const CF_WRANGLER_CONFIG = join(CF_OUTPUT_DIR, 'chickpea', 'wrangler.json'); const CF_SMOKE_WRANGLER_CONFIG = join(CF_OUTPUT_DIR, 'chickpea', 'wrangler.smoke.json'); export const CF_AI_SMOKE_CONFIG = join(CF_OUTPUT_DIR, 'chickpea', 'wrangler.ai-smoke.json'); export const PERSIST_DIR = mkdtempSync(join(tmpdir(), 'chickpea-cf-smoke-')); const AUTH_SECRET = '9d'.repeat(32); const PUBLIC_ORIGIN = 'https://chickpea-smoke.invalid'; export const WORKSPACE = 'T0SMOKE'; export const APP_ID = 'A0SMOKE'; export const BOT_USER_ID = 'UBOTSMOKE'; export const OWNER_USER_ID = 'UOWNERSMOKE'; export const ONBOARDING_DM_CHANNEL = 'DSMOKEONBOARDING'; export const OAUTH_CLIENT_ID = '123456.789012'; export const OAUTH_CLIENT_SECRET = 'fake-confidential-client-secret'; export const OAUTH_CONFIGURATION_TOKEN = 'xoxe.fake-workerd-configuration-token'; export const OAUTH_BOT_TOKEN = 'xoxb-fake-workerd-bot-token'; const RECOVERY_TOKEN = '5c'.repeat(32); export const CHANNEL = 'C0SMOKE'; const AI_CHANNEL = 'C0SMOKEAI'; const MENTION_TS = '1782770400.000100'; const MEMORY_TS = '1782770450.000100'; const AI_MENTION_TS = '1782770100.000100'; export const PORT = process.env.SMOKE_WRANGLER_PORT ? Number(process.env.SMOKE_WRANGLER_PORT) : await getFreePort(); const AI_SMOKE_SERVICE = 'chickpea-ai-smoke-stub'; const AI_SMOKE_REPLY = 'workers-ai-binding-smoke::gateway-disabled'; const AI_SMOKE_RPC_FLAG = 'enable_abortsignal_rpc'; const USAGE_PRE_DELIVERY_BUDGET_MS = 100; const AGENT_BINDINGS = [ ['FLUE_CHICKPEA_SLACK_V2_AGENT', 'FlueChickpeaSlackV2Agent'], ['FLUE_CHICKPEA_ROUTINE_INTENT_V2_AGENT', 'FlueChickpeaRoutineIntentV2Agent'], ['FLUE_CHICKPEA_ROUTINE_EXECUTION_V2_AGENT', 'FlueChickpeaRoutineExecutionV2Agent'], ['FLUE_CHICKPEA_TEST_TURN_V1_AGENT', 'FlueChickpeaTestTurnV1Agent'], ]; const BETA_FLUE_CLASSES = [ 'FlueRegistry', 'FlueSlackThreadAgent', 'FlueRoutineIntentAgent', 'FlueRoutineWorkflow', ]; const RETIRED_AUTH_CLASSES = ['AuthGuard']; // Slow-turn case: a distinct channel + thread whose provider reply is held, so // the events ack must return before the turn and the DO alarm relay delivers it. const SLOW_CHANNEL = 'C0SMOKESLOW'; const SLOW_MENTION_TS = '1782771000.000100'; const GEMINI_MENTION_TS = '1782771200.000100'; // Local workerd does not cancel waitUntil at ~30s (checked 2026-09-24: a 40s // waitUntil completed under `wrangler dev`), so a hold past 30s proves nothing // here. The hold only has to exceed the 10s fast-ack bound so an inline turn // would fail that check. Overridable for iteration; keep it above 10s. const SLOW_TURN_DELAY_MS = Number(process.env.SMOKE_SLOW_TURN_DELAY_MS ?? 12_000); // Which turn executor this run proves: the default (runner, variable unset) // or the SLACK_TAG_TURN_EXECUTOR=alarm emergency fallback. if (![undefined, '', 'runner', 'alarm'].includes(process.env.SMOKE_TURN_EXECUTOR)) { throw new Error('SMOKE_TURN_EXECUTOR must be runner (the default) or alarm.'); } const SMOKE_TURN_EXECUTOR = process.env.SMOKE_TURN_EXECUTOR === 'alarm' ? 'alarm' : 'runner'; if (!(SLOW_TURN_DELAY_MS > 10_000)) { throw new Error('SMOKE_SLOW_TURN_DELAY_MS must exceed the 10000 ms fast-ack bound.'); } export const failures = []; let adminCookie = ''; export let defaultAgentMention = ''; export function setDefaultAgentMention(mention) { defaultAgentMention = mention; } function check(ok, label, detail = '') { const status = ok ? 'ok ' : 'FAIL'; console.log(` [${status}] ${label}${detail ? ` — ${detail}` : ''}`); if (!ok) failures.push(label); } function sameArray(actual, expected) { return ( Array.isArray(actual) && actual.length === expected.length && actual.every((value, index) => value === expected[index]) ); } export function buildCloudflareTarget(profile, { reuseExisting = false } = {}) { if (reuseExisting && process.env.SMOKE_SKIP_BUILD === '1' && existsSync(CF_WRANGLER_CONFIG)) { console.log(`• SMOKE_SKIP_BUILD=1 — reusing existing ${profile} dist-cf build`); return; } console.log(`• building ${profile} Cloudflare target (Vite → dist-cf)…`); const result = spawnSync('npm', ['run', 'flue:build:cf'], { cwd: REPO_ROOT, env: { ...process.env, CHICKPEA_DEPLOY_PROFILE: profile }, stdio: 'inherit', }); if (result.status !== 0) { throw new Error(`${profile} flue:build:cf failed (exit ${result.status})`); } } function verifyBuildArtifacts(expectedProfile = resolveCloudflareDeploymentProfile()) { const config = JSON.parse(readFileSync(CF_WRANGLER_CONFIG, 'utf8')); let actualProfile; try { actualProfile = classifyCloudflareDeploymentProfile(config); } catch (error) { check(false, `built wrangler.json has an exact ${expectedProfile} deployment profile`, String(error)); } check( actualProfile === expectedProfile, `built wrangler.json matches the requested ${expectedProfile} deployment profile`, `generated ${actualProfile ?? 'invalid'}`, ); const artifactRoot = join(CF_OUTPUT_DIR, 'chickpea'); const bundle = readdirSync(artifactRoot, { recursive: true }) .filter((entry) => typeof entry === 'string' && entry.endsWith('.js')) .sort() .map((entry) => readFileSync(join(artifactRoot, entry), 'utf8')) .join('\n'); const doBindings = config.durable_objects?.bindings ?? []; check( doBindings.some((b) => b.name === 'TAG_STATE' && b.class_name === 'TagStateStore'), 'built wrangler.json carries the TAG_STATE binding', ); check( doBindings.some((b) => b.name === 'SLACK_THREAD_RUNNER' && b.class_name === 'SlackThreadRunner'), 'built wrangler.json carries the SLACK_THREAD_RUNNER binding', ); for (const [name, className] of AGENT_BINDINGS) { check( doBindings.some((binding) => binding.name === name && binding.class_name === className), `built wrangler.json carries ${name}/${className}`, ); } if (expectedProfile === 'sandbox') { check( doBindings.some((b) => b.name === 'SANDBOX' && b.class_name === 'Sandbox'), 'Sandbox build carries the reviewed Sandbox DO binding', ); } else { check( !doBindings.some((b) => b.name === 'SANDBOX' || b.class_name === 'Sandbox'), 'core build carries no Sandbox DO binding', ); check((config.containers ?? []).length === 0, 'core build declares no Container application'); } const authDb = (config.d1_databases ?? []).find((binding) => binding.binding === 'AUTH_DB'); check( Boolean(authDb) && String(authDb.migrations_dir ?? '').endsWith('migrations/better-auth'), 'built wrangler.json carries AUTH_DB reviewed migrations', ); const migrations = config.migrations ?? []; const tags = migrations.map((migration) => migration.tag); check( sameArray(tags, ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7', 'v8', 'v9', 'v10', 'v11', 'v12']), 'built wrangler.json migrations use the exact append-only v1 through v12 chain', tags.join(','), ); const sandboxMigration = migrations.find((migration) => migration.tag === 'v3'); check( sameArray(sandboxMigration?.new_sqlite_classes ?? [], ['Sandbox']), 'built wrangler.json preserves the v3 Sandbox class migration in every deployment profile', ); const reset = migrations.find((migration) => migration.tag === 'v6'); check( sameArray( [...(reset?.deleted_classes ?? [])].sort(), [...BETA_FLUE_CLASSES].sort(), ), 'v6 deletes exactly the four beta Flue classes', ); const authGuardMigration = migrations.find((migration) => migration.tag === 'v7'); check( sameArray(authGuardMigration?.new_sqlite_classes ?? [], RETIRED_AUTH_CLASSES), 'v7 preserves the AuthGuard SQLite class history', ); const authGuardRetirement = migrations.find((migration) => migration.tag === 'v8'); check( sameArray(authGuardRetirement?.deleted_classes ?? [], RETIRED_AUTH_CLASSES), 'v8 retires exactly the AuthGuard class', ); const gatewaySessionMigration = migrations.find((migration) => migration.tag === 'v9'); check( sameArray(gatewaySessionMigration?.new_sqlite_classes ?? [], ['SlackGatewaySession']), 'v9 adds exactly the outbound shared-app gateway session class', ); const codingWorkerMigration = migrations.find((migration) => migration.tag === 'v10'); check( sameArray(codingWorkerMigration?.new_sqlite_classes ?? [], ['FlueChickpeaCodingWorkerV1Agent']), 'v10 adds exactly the coding worker Agent class', ); const threadRunnerMigration = migrations.find((migration) => migration.tag === 'v11'); check( sameArray(threadRunnerMigration?.new_sqlite_classes ?? [], ['SlackThreadRunner']) && (threadRunnerMigration?.deleted_classes ?? []).length === 0, 'v11 adds exactly the per-thread Slack runner class', ); const testTurnMigration = migrations.find((migration) => migration.tag === 'v12'); check( sameArray(testTurnMigration?.new_sqlite_classes ?? [], ['FlueChickpeaTestTurnV1Agent']) && (testTurnMigration?.deleted_classes ?? []).length === 0, 'v12 adds exactly the test run Agent class', ); if (expectedProfile === 'sandbox') { const sandboxContainer = (config.containers ?? []).find( (container) => container.class_name === 'Sandbox', ); check( sandboxContainer?.instance_type === 'standard-1' && sandboxContainer?.max_instances === 25, 'Sandbox build keeps the bounded multi-thread capacity', sandboxContainer ? `${sandboxContainer.instance_type} / ${sandboxContainer.max_instances} max instances` : 'missing', ); } const redirect = join(REPO_ROOT, '.wrangler', 'deploy', 'config.json'); const redirectBody = existsSync(redirect) ? readFileSync(redirect, 'utf8') : ''; check(redirectBody.includes('dist-cf'), '.wrangler/deploy/config.json points into dist-cf'); check( existsSync(join(REPO_ROOT, 'src', 'db.node.ts')) && !existsSync(join(REPO_ROOT, 'src', 'db.ts')), 'Node-only persistence stays outside Cloudflare auto-discovery', ); check(config.ai?.binding === 'AI', 'built wrangler.json carries the production AI binding'); check( config.version_metadata?.binding === 'CF_VERSION_METADATA', 'built wrangler.json carries the Worker version metadata binding', ); check( sameArray(config.triggers?.crons ?? [], ['* * * * *']), 'built wrangler.json carries exactly one heartbeat Cron Trigger', ); check( !Object.hasOwn(config.vars ?? {}, 'TAG_OPENAI_SUBSCRIPTION_ENABLED'), 'built artifact exposes no OpenAI Subscription preview gate', ); check( Object.keys(config.vars ?? {}).sort().join(',') === 'CHICKPEA_APP_VERSION,CHICKPEA_SOURCE_COMMIT' && config.vars.CHICKPEA_APP_VERSION === readBuildIdentity(REPO_ROOT).version && config.vars.CHICKPEA_SOURCE_COMMIT === (readBuildIdentity(REPO_ROOT).sourceCommit ?? ''), 'built artifact carries exact source identity and no customer-editable runtime defaults', ); check( config.observability?.traces?.enabled === true, 'built artifact enables Workers Traces for metadata-only Flue spans', ); check( hasScheduledComposition(readFileSync(join(artifactRoot, config.main), 'utf8')) && bundle.includes('chickpea-slack-v2') && bundle.includes('chickpea-routine-intent-v2') && bundle.includes('chickpea-routine-execution-v2') && bundle.includes('chickpea.response-metadata') && bundle.includes('@flue/runtime/cloudflare-tracing') && bundle.includes('FLUE_PRIVATE_SANDBOX_COMMAND_V1'), 'built Worker composes the heartbeat, fresh agents, and content-free tracing', ); check( bundle.includes('routine_schedule_actions') && bundle.includes('manage_scheduled_work') && bundle.includes('slackScheduleActionInvoke') && bundle.includes('claimDueScheduleActions') && bundle.includes('schedule_action_payload_unavailable'), 'built Worker carries the durable schedule-action ledger, tool, RPC, and alarm recovery path', ); } export function writeSmokeWranglerConfigs(setup) { const productionConfig = JSON.parse(readFileSync(CF_WRANGLER_CONFIG, 'utf8')); // The production AI binding accepts the turn's AbortSignal directly. Our // local stand-in crosses a service-RPC boundary, so opt only the disposable // smoke workers into serializing that otherwise-identical argument. const smokeCompatibilityFlags = [ ...new Set([...(productionConfig.compatibility_flags ?? []), AI_SMOKE_RPC_FLAG]), ]; const smokeConfig = { ...productionConfig, d1_databases: (productionConfig.d1_databases ?? []).map((database) => database.binding === 'AUTH_DB' ? { ...database, database_id: '00000000-0000-0000-0000-000000000001' } : database), vars: withCloudflareSmokeTelemetryBindings({ ...(productionConfig.vars ?? {}), [SETUP_CAPABILITY_DIGEST_BINDING]: setup.digest, [SETUP_CAPABILITY_ISSUED_AT_BINDING]: String(setup.issuedAt), // Exercise exactly one internal workspace/channel through the enforced // ledger lane while the ordinary and slow channels remain legacy. SLACK_TAG_LEDGER_CANARY_CHANNELS: `${WORKSPACE}/${AI_CHANNEL}`, // Journeys run through per-thread SlackThreadRunner objects, the // default executor. SMOKE_TURN_EXECUTOR=alarm (verify:cf-smoke:alarm) // sets the emergency fallback so the state store's alarm path stays // covered. ...(SMOKE_TURN_EXECUTOR === 'alarm' ? { SLACK_TAG_TURN_EXECUTOR: 'alarm' } : {}), }), dev: { ...(productionConfig.dev ?? {}), enable_containers: false }, compatibility_flags: smokeCompatibilityFlags, services: [ ...(productionConfig.services ?? []).filter((service) => service.binding !== 'AI'), { binding: 'AI', service: AI_SMOKE_SERVICE }, ], }; // A Sandbox-profile structure smoke never calls the coding tier, and local // Wrangler may invoke Docker despite dev.enable_containers=false. Core has // no declaration; for Sandbox only, omit it from this disposable copy after // the exact production shape was asserted above. delete smokeConfig.containers; delete smokeConfig.ai; writeFileSync(CF_SMOKE_WRANGLER_CONFIG, `${JSON.stringify(smokeConfig, null, 2)}\n`); writeFileSync( CF_AI_SMOKE_CONFIG, `${JSON.stringify( { name: AI_SMOKE_SERVICE, main: '../../scripts/fixtures/cloudflare-ai-binding-smoke.mjs', compatibility_date: productionConfig.compatibility_date, compatibility_flags: smokeCompatibilityFlags, vars: { AI_SMOKE_REPLY }, }, null, 2, )}\n`, ); } export function writeDevVars(fakeUrl) { // wrangler dev reads .dev.vars from the directory of the config file it was // given. dist-cf is disposable build output, so writing here never touches a // developer's real .dev.vars in the repo root. // // Deliberately no Slack app/bot credentials here: the smoke runs the real // app-creation and OAuth journey. Only the independent deployment keyring // and local provider/API endpoints are target configuration. writeFileSync( join(CF_OUTPUT_DIR, 'chickpea', '.dev.vars'), [ `CHICKPEA_AUTH_SECRET=${AUTH_SECRET}`, `CHICKPEA_RECOVERY_TOKEN=${RECOVERY_TOKEN}`, 'CHICKPEA_CREDENTIAL_KEY_CURRENT_ID=key_v1', `CHICKPEA_CREDENTIAL_KEY_KEY_V1=${Buffer.alloc(32, 7).toString('base64url')}`, `SLACK_TAG_PUBLIC_URL=${PUBLIC_ORIGIN}`, `SLACK_API_URL=${fakeUrl}/api/`, `LOCAL_STUB_URL=${fakeUrl}/v1`, 'SLACK_TAG_MODEL=local-stub/smoke-model', `ANTHROPIC_API_URL=${fakeUrl}`, `OPENAI_API_URL=${fakeUrl}/openai/v1`, `OPENROUTER_API_URL=${fakeUrl}/openrouter`, `GEMINI_API_URL=${fakeUrl}/gemini/v1beta`, `GEMINI_BASE_URL=${fakeUrl}/gemini/v1beta`, ...cloudflareSmokeTelemetryDevVars(), '', ].join('\n'), ); } export function applyLocalAuthMigrations() { const result = spawnSync( WRANGLER_BIN, [ 'd1', 'migrations', 'apply', 'AUTH_DB', '--local', '--persist-to', PERSIST_DIR, '--config', CF_SMOKE_WRANGLER_CONFIG, ], { cwd: REPO_ROOT, env: { ...process.env, CI: '1' }, stdio: 'inherit' }, ); if (result.status !== 0) { throw new Error(`local AUTH_DB migrations failed (exit ${result.status})`); } } export function spawnWranglerDev() { const child = spawn( WRANGLER_BIN, [ 'dev', '--config', CF_SMOKE_WRANGLER_CONFIG, '--config', CF_AI_SMOKE_CONFIG, '--port', String(PORT), // OUTSIDE dist-cf on purpose: a rebuild wipes the build output, and local // DO state must survive it (and the restart half of this smoke). '--persist-to', PERSIST_DIR, // This gate verifies the production container declaration above but // intentionally runs read-only model turns that never acquire one. // Avoid making an unrelated local Docker daemon a prerequisite. '--enable-containers=false', ], { cwd: REPO_ROOT, env: { ...process.env, CI: '1' }, stdio: ['ignore', 'pipe', 'pipe'] }, ); let output = ''; child.stdout.on('data', (chunk) => (output += chunk)); child.stderr.on('data', (chunk) => (output += chunk)); return { child, getOutput: () => output }; } export function stopWrangler(handle) { return new Promise((resolve) => { const { child } = handle; if (child.exitCode !== null || child.signalCode !== null) { resolve(); return; } const settle = setTimeout(() => { child.kill('SIGKILL'); resolve(); }, 5000); child.once('exit', () => { clearTimeout(settle); resolve(); }); child.kill('SIGTERM'); }); } export async function adminFetch(baseUrl, path, init = {}) { const method = String(init.method ?? 'GET').toUpperCase(); const mutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method); const response = await fetch(`${baseUrl}${path}`, { ...init, headers: { ...(adminCookie ? { cookie: adminCookie } : {}), 'content-type': 'application/json', ...(mutation ? { origin: PUBLIC_ORIGIN, 'sec-fetch-site': 'same-origin' } : {}), ...(init.headers ?? {}), }, }); const text = await response.text(); let body; try { body = text ? JSON.parse(text) : undefined; } catch { body = text; } return { status: response.status, body }; } async function adminPageHtml(baseUrl, path = '/admin') { const response = await fetch(`${baseUrl}${path}`, { headers: adminCookie ? { cookie: adminCookie } : {}, }); return response.text(); } function formHeaders() { return { origin: PUBLIC_ORIGIN, 'sec-fetch-site': 'same-origin', 'content-type': 'application/x-www-form-urlencoded', }; } async function postForm(baseUrl, path, fields, cookie = '') { return fetch(`${baseUrl}${path}`, { method: 'POST', redirect: 'manual', headers: { ...formHeaders(), ...(cookie ? { cookie } : {}) }, body: new URLSearchParams(fields), }); } function responseCookie(response, prefix) { const values = typeof response.headers.getSetCookie === 'function' ? response.headers.getSetCookie() : [response.headers.get('set-cookie') ?? '']; for (const value of values) { for (const candidate of value.split(/,(?=\s*[^;,]+=)/)) { const cookie = candidate.trim().split(';', 1)[0] ?? ''; if (cookie.startsWith(prefix)) return cookie; } } return ''; } function slackAuthorizationUrlFromHandoff(html) { const encoded = /data-slack-authorization-link href="([^"]+)"/.exec(html)?.[1]; return new URL(encoded ? encoded.replaceAll('&', '&') : 'about:blank'); } export async function completeSlackNativeSetup(baseUrl, eventsUrl, setup, backend) { const setupPage = await fetch(`${baseUrl}/admin/setup`); const setupHtml = await setupPage.text(); check( setupPage.status === 200 && setupHtml.includes('Add Chickpea to Slack') && !setupHtml.includes('Resume private setup') && setupHtml.includes('/admin/setup/client.js') && // The customer-owned fallback's configuration-token field is a // type="password" input; the legacy password sign-in it guards against // used a named field, not an input type. !/name="password"|ownerEmail|sign up/i.test(setupHtml) && !setupHtml.includes(setup.capability), 'fresh setup GET renders the real Slack stage without password or capability echo', `HTTP ${setupPage.status}`, ); // The connect surface is public and answers before Slack setup finishes, so // a person can hand their coding agent one line on a fresh install. It // renders the deployment's own public origin: SLACK_TAG_PUBLIC_URL is pinned // in .dev.vars, so the guide carries PUBLIC_ORIGIN, not the loopback address // the smoke happens to reach the Worker on. const connectGuide = await fetch(`${baseUrl}/connect.md`); const connectGuideBody = await connectGuide.text(); const connectPage = await fetch(`${baseUrl}/connect`); const connectPageBody = await connectPage.text(); const connectConditions = { guide: connectGuide.status === 200, guideType: (connectGuide.headers.get('content-type') ?? '').startsWith('text/markdown'), guideMcpUrl: connectGuideBody.includes(`${PUBLIC_ORIGIN}/mcp`), page: connectPage.status === 200, pagePrompt: connectPageBody.includes( `Connect my coding agent to my Chickpea using ${PUBLIC_ORIGIN}/connect.md`, ), }; check( Object.values(connectConditions).every(Boolean), 'public connect surface serves the coding-agent guide and its copy prompt', `guide HTTP ${connectGuide.status}; page HTTP ${connectPage.status}; failed: ${ Object.entries(connectConditions).filter(([, ok]) => !ok).map(([name]) => name).join(',') || 'none' }`, ); const opened = await postForm(baseUrl, '/admin/setup', { action: 'open', capability: setup.capability, destination: '/admin/channels', }); const openedHtml = await opened.text(); check( opened.status === 200 && openedHtml.includes('data-primary-action="gateway-install"') && openedHtml.includes('Add to Slack') && openedHtml.includes('Use your own Slack app instead') && openedHtml.includes('Generate an App Configuration token in Slack') && openedHtml.includes('href="/admin/setup/manual"'), 'deploy capability leads with Add to Slack and keeps both customer-owned fallbacks', `HTTP ${opened.status}`, ); const manualOpened = await postForm(baseUrl, '/admin/setup/manual', { action: 'open', capability: setup.capability, destination: '/admin/onboarding', }); const manualHtml = await manualOpened.text(); const manualAsset = await fetch(`${baseUrl}/admin/assets/onboarding/create-workspace.webp`); const manualAssetBytes = new Uint8Array(await manualAsset.arrayBuffer()); const manualConditions = { page: manualOpened.status === 200, create: manualHtml.includes('Create Chickpea'), finish: manualHtml.includes('Finish creating Chickpea'), events: manualHtml.includes('verify the Events URL after Slack installation'), credentials: manualHtml.includes('Add app credentials'), capabilityHidden: !manualHtml.includes(setup.capability), asset: manualAsset.status === 200, assetType: manualAsset.headers.get('content-type') === 'image/webp', assetRiff: new TextDecoder().decode(manualAssetBytes.slice(0, 4)) === 'RIFF', assetSize: manualAssetBytes.byteLength > 10_000, }; check( Object.values(manualConditions).every(Boolean), 'separate manual journey restores its historical screens and public screenshot assets', `page HTTP ${manualOpened.status}; asset HTTP ${manualAsset.status}; failed: ${ Object.entries(manualConditions).filter(([, ok]) => !ok).map(([name]) => name).join(',') || 'none' }`, ); const created = await postForm(baseUrl, '/admin/setup', { action: 'create', capability: setup.capability, destination: '/admin/channels', configurationToken: OAUTH_CONFIGURATION_TOKEN, }); const createdHtml = await created.text(); check( created.status === 200 && createdHtml.includes('Continue to Slack') && !createdHtml.includes(OAUTH_CONFIGURATION_TOKEN) && !createdHtml.includes(OAUTH_CLIENT_SECRET), 'programmatic app creation records write-only app credentials and advances setup', `HTTP ${created.status}`, ); const iconUploads = backend.callsOfMethod('apps.icon.set'); check( iconUploads.length === 1 && iconUploads[0].ok === true && iconUploads[0].body.app_id === APP_ID && iconUploads[0].body.file_type === 'image/png' && iconUploads[0].body.file_sha256 === createHash('sha256') .update(readFileSync(join(REPO_ROOT, 'assets', 'bot-avatar.png'))).digest('hex'), 'the created app gets the Chickpea logo read through the Worker ASSETS binding', `${iconUploads.length} apps.icon.set call(s)`, ); const providerCallsBeforeInstall = backend.providerCalls().length; const preInstall = await postSignedEvent(eventsUrl, mentionEvent('Ev_SMOKE_PRE_INSTALL', { baseApp: true })); await backend.quiesce(); check( preInstall.status === 200 && backend.providerCalls().length === providerCallsBeforeInstall && backend.finals().length === 0, 'app-stage signing acknowledges Slack but rejects Agent work before bot OAuth', `HTTP ${preInstall.status}`, ); const installStart = await postForm(baseUrl, '/auth/slack/install/start', { capability: setup.capability, destination: '/admin/channels', }); const installAuthorization = slackAuthorizationUrlFromHandoff(await installStart.text()); const installCookie = responseCookie(installStart, '__Secure-chickpea_slack_install='); check( installStart.status === 200 && installAuthorization.origin === 'https://slack.com' && Boolean(installAuthorization.searchParams.get('state')) && Boolean(installCookie), 'bot OAuth start uses a narrow browser cookie and Slack authorization state', `HTTP ${installStart.status}`, ); const installState = installAuthorization.searchParams.get('state') ?? ''; const installCallback = await fetch( `${baseUrl}/auth/slack/install/callback?state=${encodeURIComponent(installState)}&code=bot-smoke-code`, { redirect: 'manual', headers: { cookie: installCookie } }, ); check( installCallback.status === 303 && installCallback.headers.get('location')?.includes('slack_install=waiting_events'), 'confidential bot exchange validates app/team/bot/scopes/directory/channel before waiting on Events', `HTTP ${installCallback.status} location=${installCallback.headers.get('location')}`, ); const challenge = await postSignedEvent(eventsUrl, { type: 'url_verification', challenge: 'cf-smoke-oauth-proof', api_app_id: APP_ID, team_id: WORKSPACE, }); check( challenge.status === 200 && challenge.body?.challenge === 'cf-smoke-oauth-proof', 'a separately signed URL verification promotes only the exact OAuth candidate', `HTTP ${challenge.status}`, ); let finalized; for (let attempt = 0; attempt < 8; attempt += 1) { await delay(500); finalized = await postForm(baseUrl, '/auth/slack/install/finalize', { capability: setup.capability, destination: '/admin/channels', }); if (finalized.headers.get('location')?.includes('slack_install=bot_installed')) break; } check( finalized?.status === 303 && finalized.headers.get('location')?.includes('slack_install=bot_installed'), 'exact signed Events proof finalizes the encrypted bot credential revision', `HTTP ${String(finalized?.status)} location=${String(finalized?.headers.get('location'))}`, ); const ownerStart = await postForm(baseUrl, '/auth/slack/oidc/start', { purpose: 'first_owner', capability: setup.capability, destination: '/admin/channels', }); const ownerAuthorization = slackAuthorizationUrlFromHandoff(await ownerStart.text()); const ownerCookie = responseCookie(ownerStart, '__Secure-chickpea_slack_oidc='); const state = ownerAuthorization.searchParams.get('state') ?? ''; const nonce = ownerAuthorization.searchParams.get('nonce') ?? ''; check( ownerStart.status === 200 && ownerAuthorization.origin === 'https://slack.com' && ownerAuthorization.searchParams.get('team') === WORKSPACE && Boolean(ownerCookie) && state.length >= 32 && nonce.length >= 32, 'first-Owner OIDC is exact-team and independently state/nonce/browser bound', `HTTP ${ownerStart.status}`, ); const ownerCallback = await fetch( `${baseUrl}/auth/slack/oidc/callback?state=${encodeURIComponent(state)}&code=${encodeURIComponent(`oidc.${nonce}`)}`, { headers: { cookie: ownerCookie } }, ); const ownerCallbackBody = await ownerCallback.text(); adminCookie = responseCookie(ownerCallback, 'better-auth.session_token=') || responseCookie(ownerCallback, '__Secure-better-auth.session_token='); const callbackCookieNames = (typeof ownerCallback.headers.getSetCookie === 'function' ? ownerCallback.headers.getSetCookie() : [ownerCallback.headers.get('set-cookie') ?? '']) .map((value) => value.trim().split('=', 1)[0]) .filter(Boolean) .join(','); check( ownerCallback.status === 200 && Boolean(adminCookie) && ownerCallbackBody.includes('data-slack-auth-surface="owner-complete"'), 'exact installer OIDC creates the first Owner and only then issues a Better Auth session', `HTTP ${ownerCallback.status} cookies=${callbackCookieNames || 'none'} session=${Boolean(adminCookie)} ownerPage=${ownerCallbackBody.includes('data-slack-auth-surface="owner-complete"')}`, ); const unauthenticated = await fetch(`${baseUrl}/admin/api/agents`); check( unauthenticated.status === 401, 'Admin APIs still fail closed without the Slack-established browser session', `HTTP ${unauthenticated.status}`, ); } /** * Same-app Slack credential recovery through the built Worker, after the * app's signing secret was regenerated in Slack. URL repair * (apps.manifest.export/update with a configuration token) and the bot token * exchange (oauth.v2.access, refused like Slack for a redirect the app did not * register) each leave the Worker through its global fetch, which workerd * refuses to call as a method of another object. Slack's URL check, signed * with the new secret, is the Events proof finalize promotes with. Runs last: * the recovered bundle replaces the one the earlier checks used. */ async function exerciseSlackRecovery(baseUrl, backend, eventsUrl) { const regeneratedSecret = 'regenerated-workerd-signing-secret'; const wireStart = backend.wireLog.length; const slackCalls = (prefix) => backend.wireLog.slice(wireStart) .filter((entry) => entry.method.startsWith(prefix)).map((entry) => `${entry.method}:${entry.ok}`); const begun = await postForm(baseUrl, '/admin/recovery', { action: 'begin', recoveryToken: RECOVERY_TOKEN }); const cookie = [ responseCookie(begun, '__Secure-chickpea_slack_recovery_browser='), responseCookie(begun, '__Secure-chickpea_slack_recovery='), ].filter(Boolean).join('; '); check( begun.status === 200 && (await begun.text()).includes('name="action" value="stage"') && cookie.split('; ').length === 2, 'the deployment recovery token opens one browser-bound repair session', `HTTP ${begun.status}`, ); const staged = await postForm(baseUrl, '/admin/recovery', { action: 'stage', appId: APP_ID, teamId: WORKSPACE, clientId: OAUTH_CLIENT_ID, clientSecret: OAUTH_CLIENT_SECRET, signingSecret: regeneratedSecret, configurationToken: OAUTH_CONFIGURATION_TOKEN, }, cookie); const authorization = slackAuthorizationUrlFromHandoff(await staged.text()); const repair = slackCalls('apps.manifest.'); check( staged.status === 200 && authorization.origin === 'https://slack.com' && sameArray(repair, ['apps.manifest.export:true', 'apps.manifest.update:true', 'apps.manifest.export:true']), 'recovery URL repair reaches Slack through the Worker fetch', `HTTP ${staged.status}; ${repair.join(',') || 'no manifest calls'}`, ); // Slack sends the browser back to the redirect recovery asked for. const redirect = new URL(authorization.searchParams.get('redirect_uri') ?? '', baseUrl); const callback = await fetch( `${baseUrl}${redirect.pathname}?state=${ encodeURIComponent(authorization.searchParams.get('state') ?? '')}&code=recovery-smoke-code`, { headers: { cookie } }, ); const exchanges = slackCalls('oauth.v2.access'); check( callback.status === 200 && (await callback.text()).includes('name="action" value="finalize"') && sameArray(exchanges, ['oauth.v2.access:true']), 'recovery bot token exchange reaches Slack through the Worker fetch for a redirect the app registered', `HTTP ${callback.status}; ${exchanges.join(',') || 'no token exchange'}`, ); const forged = await postSignedEvent(eventsUrl, { type: 'url_verification', challenge: 'cf-smoke-forged-proof', }, { signingSecret: 'forged-workerd-signing-secret' }); const proof = await postSignedEvent(eventsUrl, { type: 'url_verification', challenge: 'cf-smoke-recovery-proof', }, { signingSecret: regeneratedSecret }); const finalized = await postForm(baseUrl, '/admin/recovery', { action: 'finalize' }, cookie); const finalizedHtml = await finalized.text(); check( forged.status === 401 && proof.status === 200 && proof.body?.challenge === 'cf-smoke-recovery-proof' && finalized.status === 200 && finalizedHtml.includes('Slack connection repaired'), 'Slack\'s URL check signed with the regenerated secret answers (a forged one does not), and finalize promotes the repair', `forged HTTP ${forged.status}; proof HTTP ${proof.status}; finalize HTTP ${finalized.status}`, ); const [recovered, superseded] = [ await postSignedEvent(eventsUrl, { type: 'url_verification', challenge: 'cf-smoke-after-recovery' }, { signingSecret: regeneratedSecret }), await postSignedEvent(eventsUrl, { type: 'url_verification', challenge: 'cf-smoke-after-recovery' }), ]; check( recovered.status === 200 && recovered.body?.challenge === 'cf-smoke-after-recovery' && superseded.status === 401, 'after recovery Slack deliveries verify with the regenerated secret, not the replaced one', `regenerated HTTP ${recovered.status}; replaced HTTP ${superseded.status}`, ); } async function completeSlackLogin(baseUrl) { const start = await postForm(baseUrl, '/auth/slack/oidc/start', { purpose: 'login', destination: '/admin', }); const authorization = slackAuthorizationUrlFromHandoff(await start.text()); const oidcCookie = responseCookie(start, '__Secure-chickpea_slack_oidc='); const state = authorization.searchParams.get('state') ?? ''; const nonce = authorization.searchParams.get('nonce') ?? ''; if (start.status !== 200 || !oidcCookie || state.length < 32 || nonce.length < 32) { throw new Error(`fresh Slack login did not start (HTTP ${start.status})`); } const callback = await fetch( `${baseUrl}/auth/slack/oidc/callback?state=${encodeURIComponent(state)}&code=${encodeURIComponent(`oidc.${nonce}`)}`, { redirect: 'manual', headers: { cookie: oidcCookie } }, ); const session = responseCookie(callback, 'better-auth.session_token=') || responseCookie(callback, '__Secure-better-auth.session_token='); if (callback.status !== 303 || callback.headers.get('location') !== '/admin' || !session) { throw new Error(`fresh Slack login did not issue a session (HTTP ${callback.status})`); } return session; } async function renderAdminWithWorkerdState(baseUrl, path = '/admin') { const html = await adminPageHtml(baseUrl, path); const scriptSrc = html.match(/