diff --git a/cloudflare_workers/api/wrangler.jsonc b/cloudflare_workers/api/wrangler.jsonc index ca56e8aaff..c2ad038bba 100644 --- a/cloudflare_workers/api/wrangler.jsonc +++ b/cloudflare_workers/api/wrangler.jsonc @@ -21,6 +21,9 @@ "ai": { "binding": "AI" }, + "version_metadata": { + "binding": "CF_VERSION_METADATA" + }, "vars": { "ENV_NAME": "capgo_api-prod" }, @@ -77,6 +80,9 @@ "ai": { "binding": "AI" }, + "version_metadata": { + "binding": "CF_VERSION_METADATA" + }, "vars": { "ENV_NAME": "capgo_api-preprod" }, @@ -132,6 +138,9 @@ "ai": { "binding": "AI" }, + "version_metadata": { + "binding": "CF_VERSION_METADATA" + }, "vars": { "ENV_NAME": "capgo_api-alpha" }, @@ -187,6 +196,9 @@ "ai": { "binding": "AI" }, + "version_metadata": { + "binding": "CF_VERSION_METADATA" + }, "vars": { "ENV_NAME": "capgo_api-local" } diff --git a/supabase/functions/_backend/utils/cloudflare.ts b/supabase/functions/_backend/utils/cloudflare.ts index d73a584233..92b7c32565 100644 --- a/supabase/functions/_backend/utils/cloudflare.ts +++ b/supabase/functions/_backend/utils/cloudflare.ts @@ -1,4 +1,4 @@ -import type { AnalyticsEngineDataPoint, D1Database, Hyperdrive } from '@cloudflare/workers-types' +import type { AnalyticsEngineDataPoint, D1Database, Hyperdrive, WorkerVersionMetadata } from '@cloudflare/workers-types' import type { Context } from 'hono' import type { DeviceComparable } from './deviceComparison.ts' import type { Database } from './supabase.types.ts' @@ -55,6 +55,7 @@ export type Bindings = { ATTACHMENT_UPLOAD_HANDLER: DurableObjectNamespace ATTACHMENT_BUCKET: R2Bucket AI?: AiBinding + CF_VERSION_METADATA?: WorkerVersionMetadata } const TRACK_DEVICE_USAGE_CACHE_PATH = '/.track-device-usage-cache' diff --git a/supabase/functions/_backend/utils/hono.ts b/supabase/functions/_backend/utils/hono.ts index ed6a5384de..b0e6cb5d0d 100644 --- a/supabase/functions/_backend/utils/hono.ts +++ b/supabase/functions/_backend/utils/hono.ts @@ -211,6 +211,21 @@ export const middlewareAPISecret = honoFactory.createMiddleware(async (c, next) export const BRES = { status: 'ok' } +export function buildWorkerSourceHeaders(functionName: string, envName: string, versionMetadata?: Partial) { + const headers: Record = { + 'X-Worker-Source': `${envName || functionName}-${CapgoVersion}`, + } + + if (versionMetadata?.id) + headers['X-Worker-Version-Id'] = versionMetadata.id + if (versionMetadata?.tag) + headers['X-Worker-Version-Tag'] = versionMetadata.tag + if (versionMetadata?.timestamp) + headers['X-Worker-Version-Timestamp'] = versionMetadata.timestamp + + return headers +} + export function createHono(functionName: string, _version: string) { let appGlobal if (getRuntimeKey() === 'deno') { @@ -221,8 +236,9 @@ export function createHono(functionName: string, _version: string) { } appGlobal.use('*', (c, next): Promise => { // ADD HEADER TO IDENTIFY WORKER SOURCE - const name = `${getEnv(c, 'ENV_NAME') || functionName}-${CapgoVersion}` - c.header('X-Worker-Source', name) + const headers = buildWorkerSourceHeaders(functionName, getEnv(c, 'ENV_NAME'), c.env?.CF_VERSION_METADATA) + for (const [header, value] of Object.entries(headers)) + c.header(header, value) return next() }) diff --git a/tests/worker-source-header.unit.test.ts b/tests/worker-source-header.unit.test.ts new file mode 100644 index 0000000000..a75fd45ba8 --- /dev/null +++ b/tests/worker-source-header.unit.test.ts @@ -0,0 +1,45 @@ +import { afterAll, describe, expect, it, vi } from 'vitest' +import { createHono } from '../supabase/functions/_backend/utils/hono.ts' +import { version } from '../supabase/functions/_backend/utils/version.ts' + +describe('worker source headers', () => { + afterAll(() => { + vi.unstubAllEnvs() + }) + + it.concurrent('exposes the Cloudflare Worker deployment version when metadata is bound', async () => { + vi.stubEnv('ENV_NAME', 'capgo_api-prod') + const app = createHono('api', version) + app.get('/ok', c => c.json({ status: 'ok' })) + + const response = await app.fetch( + new Request('http://localhost/ok'), + { + CF_VERSION_METADATA: { + id: '02af90ed-1d5a-474c-9afd-aa2eb41a14ac', + tag: 'deploy-prod', + timestamp: '2026-05-11T13:58:16.307Z', + }, + }, + ) + + expect(response.headers.get('x-worker-source')).toBe(`capgo_api-prod-${version}`) + expect(response.headers.get('x-worker-version-id')).toBe('02af90ed-1d5a-474c-9afd-aa2eb41a14ac') + expect(response.headers.get('x-worker-version-tag')).toBe('deploy-prod') + expect(response.headers.get('x-worker-version-timestamp')).toBe('2026-05-11T13:58:16.307Z') + }) + + it.concurrent('keeps the existing source header when version metadata is not available', async () => { + vi.stubEnv('ENV_NAME', 'capgo_api-prod') + const app = createHono('api', version) + app.get('/ok', c => c.json({ status: 'ok' })) + + const response = await app.fetch( + new Request('http://localhost/ok'), + {}, + ) + + expect(response.headers.get('x-worker-source')).toBe(`capgo_api-prod-${version}`) + expect(response.headers.get('x-worker-version-id')).toBeNull() + }) +})