diff --git a/package-lock.json b/package-lock.json index bb15dea..c0a1d09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -203,9 +203,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -223,9 +220,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -243,9 +237,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -263,9 +254,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2006,6 +1994,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -2023,6 +2012,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -2040,6 +2030,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -2057,6 +2048,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -2074,6 +2066,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -2091,6 +2084,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -2108,6 +2102,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2125,6 +2120,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2142,6 +2138,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2159,6 +2156,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2176,6 +2174,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2193,6 +2192,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2210,6 +2210,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2227,6 +2228,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2244,6 +2246,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2261,6 +2264,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2278,6 +2282,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2295,6 +2300,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2312,6 +2318,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2329,6 +2336,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2346,6 +2354,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2380,6 +2389,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -2397,6 +2407,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2414,6 +2425,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2431,6 +2443,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2542,6 +2555,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2564,6 +2578,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2586,6 +2601,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2602,6 +2618,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2618,9 +2635,7 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2637,9 +2652,7 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2656,9 +2669,7 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2675,9 +2686,7 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2694,9 +2703,7 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2713,9 +2720,7 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2732,9 +2737,7 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2751,9 +2754,7 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2770,9 +2771,7 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2795,9 +2794,7 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2820,9 +2817,7 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2845,9 +2840,7 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2870,9 +2863,7 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2895,9 +2886,7 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2920,9 +2909,7 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2945,9 +2932,7 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2970,6 +2955,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -2989,6 +2975,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3008,6 +2995,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3027,6 +3015,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3406,9 +3395,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3425,9 +3411,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3444,9 +3427,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3463,9 +3443,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5338,9 +5315,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5358,9 +5332,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5378,9 +5349,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5398,9 +5366,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5418,9 +5383,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5438,9 +5400,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6384,9 +6343,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6403,9 +6359,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6422,9 +6375,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6441,9 +6391,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10668,9 +10615,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10691,9 +10635,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10714,9 +10655,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10737,9 +10675,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/router/index.ts b/router/index.ts index e6e85fd..aabdc97 100644 --- a/router/index.ts +++ b/router/index.ts @@ -14,6 +14,10 @@ interface Env { fetch(request: Request): Promise; }; WEBHOOK_WORKER_ORIGIN?: string; + RELAYFILE_CLOUD_WORKER?: { + fetch(request: Request): Promise; + }; + RELAYFILE_CLOUD_ORIGIN?: string; } function hasRecorderEnv(env: Env): env is Env & RecorderEnv { @@ -43,6 +47,20 @@ const WEBHOOK_ORIGIN_FLAG_KEY = "WEBHOOK_ORIGIN"; // the actual destination of the forward. const WEBHOOK_WORKER_FORWARDED_HEADER = "x-cloud-webhook-worker-forwarded"; const WEBHOOK_WORKER_FORWARDED_VALUE = "webhook-worker"; +const WEBHOOK_ORIGIN_WORKER = "worker"; +const WEBHOOK_ORIGIN_RELAYFILE_CLOUD = "relayfile-cloud"; +const RELAYFILE_CLOUD_FORWARDED_HEADER = "x-cloud-webhook-relayfile-cloud-forwarded"; +const RELAYFILE_CLOUD_FORWARDED_VALUE = "relayfile-cloud"; +// Relayfile-cloud's Nango webhook handler path (strips the /cloud prefix). +const RELAYFILE_CLOUD_NANGO_WEBHOOK_PATH = "/v1/nango/webhook"; +// Lifecycle events must always reach cloud-web so handleAuthEvent can run. +const RELAYFILE_CLOUD_NANGO_LIFECYCLE_TYPES = new Set(["auth", "connection.created"]); +const RELAYFILE_CLOUD_NANGO_INGEST_TYPES = new Set(["forward", "sync", "webhook"]); +const RELAYFILE_CLOUD_NANGO_PROVIDER_CONFIG_KEYS = new Set([ + "github-relay", "github-sage", "github-app", "github-app-oauth", + "linear-relay", "linear-sage", "notion-relay", "notion-sage", + "slack-relay", "slack-sage", "slack-sage-preview", +]); let loggedPhase5aLambdaEliminated = false; // Exact paths the webhook worker handles. Other sub-paths under @@ -230,6 +248,10 @@ async function shouldUseCloudWebWorker( return false; } + if (await shouldUseRelayfileCloudWebhook(pathname, request, env)) { + return false; + } + return true; } @@ -259,7 +281,88 @@ export async function shouldUseNangoWebhookWorkerRoute( } const configured = await readWebhookOriginFlag(env); - return configured?.trim().toLowerCase() === "worker"; + return configured?.trim().toLowerCase() === WEBHOOK_ORIGIN_WORKER; +} + +export async function shouldUseNangoRelayfileCloudRoute( + pathname: string, + env: Env, +): Promise { + if (!isNangoWebhookWorkerPath(pathname)) { + return false; + } + + const configured = await readWebhookOriginFlag(env); + return configured?.trim().toLowerCase() === WEBHOOK_ORIGIN_RELAYFILE_CLOUD; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function readString(record: Record, key: string): string | null { + const value = record[key]; + return typeof value === "string" && value.trim() ? value.trim() : null; +} + +function readNangoWebhookType(record: Record): string | null { + return ( + readString(record, "type") ?? + readString(record, "eventType") ?? + readString(record, "event_type") + )?.toLowerCase() ?? null; +} + +function readNangoProviderConfigKey(record: Record): string | null { + return ( + readString(record, "providerConfigKey") ?? + readString(record, "provider_config_key") ?? + readString(record, "from") ?? + readString(record, "provider") + )?.toLowerCase() ?? null; +} + +async function isRelayfileCloudNangoIngestRequest(request: Request): Promise { + let body: unknown; + try { + body = await request.clone().json(); + } catch { + return false; + } + if (!isRecord(body)) return false; + const type = readNangoWebhookType(body); + if (!type || RELAYFILE_CLOUD_NANGO_LIFECYCLE_TYPES.has(type)) return false; + if (!RELAYFILE_CLOUD_NANGO_INGEST_TYPES.has(type)) return false; + const providerConfigKey = readNangoProviderConfigKey(body); + return providerConfigKey + ? RELAYFILE_CLOUD_NANGO_PROVIDER_CONFIG_KEYS.has(providerConfigKey) + : false; +} + +function isWebhookWorkerForward(request: Request): boolean { + return ( + request.headers.get(WEBHOOK_WORKER_FORWARDED_HEADER) === WEBHOOK_WORKER_FORWARDED_VALUE + ); +} + +function isRelayfileCloudForward(request: Request): boolean { + return ( + request.headers.get(RELAYFILE_CLOUD_FORWARDED_HEADER) === RELAYFILE_CLOUD_FORWARDED_VALUE + ); +} + +async function shouldUseRelayfileCloudWebhook( + pathname: string, + request: Request, + env: Env, +): Promise { + if (isWebhookWorkerForward(request) || isRelayfileCloudForward(request)) { + return false; + } + if (!(await shouldUseNangoRelayfileCloudRoute(pathname, env))) { + return false; + } + return isRelayfileCloudNangoIngestRequest(request); } async function shouldUseWebhookWorker( @@ -273,7 +376,7 @@ async function shouldUseWebhookWorker( // Without this header check, the router catches that forward and redirects // it back to webhook-worker, which re-enqueues, ad infinitum (or until // `maxRetries`). - if (request.headers.get(WEBHOOK_WORKER_FORWARDED_HEADER) === WEBHOOK_WORKER_FORWARDED_VALUE) { + if (isWebhookWorkerForward(request)) { return false; } return shouldUseNangoWebhookWorkerRoute(pathname, env); @@ -305,6 +408,35 @@ function buildWebhookWorkerRequest( return new Request(targetUrl.toString(), init); } +function buildRelayfileCloudWebhookRequest( + request: Request, + requestUrl: URL, + cloudOrigin?: string, +): Request { + const targetUrl = new URL(requestUrl.toString()); + targetUrl.pathname = RELAYFILE_CLOUD_NANGO_WEBHOOK_PATH; + + if (cloudOrigin) { + const originUrl = new URL(cloudOrigin); + targetUrl.protocol = originUrl.protocol; + targetUrl.hostname = originUrl.hostname; + targetUrl.port = originUrl.port; + } + + const headers = new Headers(request.headers); + headers.set(RELAYFILE_CLOUD_FORWARDED_HEADER, RELAYFILE_CLOUD_FORWARDED_VALUE); + + const init: RequestInit & { duplex?: "half" } = { + method: request.method, + headers, + body: request.body, + redirect: "manual", + duplex: "half", + }; + + return new Request(targetUrl.toString(), init); +} + export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); @@ -382,6 +514,33 @@ export default { } } + if (await shouldUseRelayfileCloudWebhook(url.pathname, request, env)) { + if (env.RELAYFILE_CLOUD_WORKER) { + const workerResponse = await env.RELAYFILE_CLOUD_WORKER.fetch( + buildRelayfileCloudWebhookRequest(request, url), + ); + if (recorderRequestClone && recorderEnv) { + ctx.waitUntil( + maybeRecord(recorderRequestClone, workerResponse.clone(), recorderEnv, ctx), + ); + } + return workerResponse; + } + + const cloudOrigin = env.RELAYFILE_CLOUD_ORIGIN?.trim(); + if (cloudOrigin) { + const originResponse = await globalThis.fetch( + buildRelayfileCloudWebhookRequest(request, url, cloudOrigin), + ); + if (recorderRequestClone && recorderEnv) { + ctx.waitUntil( + maybeRecord(recorderRequestClone, originResponse.clone(), recorderEnv, ctx), + ); + } + return originResponse; + } + } + const requestHost = request.headers.get("Host") || url.hostname; const originUrl = new URL(getOrigin(url.hostname, url.pathname, env)); const mountPrefix = getMountPrefix(url.hostname, url.pathname); diff --git a/router/test/webhook-routing.test.ts b/router/test/webhook-routing.test.ts index 8daa494..950f2d1 100644 --- a/router/test/webhook-routing.test.ts +++ b/router/test/webhook-routing.test.ts @@ -32,6 +32,7 @@ function buildEnv(opts: { webhookOriginFlag?: string; cloudWebWorker?: WorkerBinding; webhookWorker?: WorkerBinding; + relayfileCloudWorker?: WorkerBinding; }) { const routerConfig = new MemoryKV(); if (opts.webhookOriginFlag !== undefined) { @@ -42,6 +43,7 @@ function buildEnv(opts: { ROUTER_CONFIG: asKV(routerConfig), CLOUD_WEB_WORKER: opts.cloudWebWorker, WEBHOOK_WORKER: opts.webhookWorker, + RELAYFILE_CLOUD_WORKER: opts.relayfileCloudWorker, } as unknown as Parameters[1]; } @@ -132,6 +134,111 @@ describe("router webhook routing", () => { expect(cloudWebWorker.fetch).not.toHaveBeenCalled(); }); + it("routes /api/v1/webhooks/nango to relayfile-cloud when WEBHOOK_ORIGIN=relayfile-cloud and body is allowlisted ingest", async () => { + const cloudWebWorker = makeBinding(); + const webhookWorker = makeBinding(); + const relayfileCloudWorker = makeBinding(); + const env = buildEnv({ + webhookOriginFlag: "relayfile-cloud", + cloudWebWorker, + webhookWorker, + relayfileCloudWorker, + }); + + const request = new Request( + "https://origin.agentrelay.cloud/cloud/api/v1/webhooks/nango", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "forward", providerConfigKey: "github-relay" }), + }, + ); + + await worker.fetch(request, env, buildCtx()); + + expect(relayfileCloudWorker.fetch).toHaveBeenCalledOnce(); + expect(cloudWebWorker.fetch).not.toHaveBeenCalled(); + expect(webhookWorker.fetch).not.toHaveBeenCalled(); + }); + + it("routes auth event to cloud-web even when WEBHOOK_ORIGIN=relayfile-cloud (lifecycle events must stay in cloud)", async () => { + const cloudWebWorker = makeBinding(); + const webhookWorker = makeBinding(); + const relayfileCloudWorker = makeBinding(); + const env = buildEnv({ + webhookOriginFlag: "relayfile-cloud", + cloudWebWorker, + webhookWorker, + relayfileCloudWorker, + }); + + const request = new Request( + "https://origin.agentrelay.cloud/cloud/api/v1/webhooks/nango", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "auth", providerConfigKey: "github-relay" }), + }, + ); + + await worker.fetch(request, env, buildCtx()); + + expect(cloudWebWorker.fetch).toHaveBeenCalledOnce(); + expect(relayfileCloudWorker.fetch).not.toHaveBeenCalled(); + expect(webhookWorker.fetch).not.toHaveBeenCalled(); + }); + + it("routes non-allowlisted provider to cloud-web even when WEBHOOK_ORIGIN=relayfile-cloud", async () => { + const cloudWebWorker = makeBinding(); + const relayfileCloudWorker = makeBinding(); + const env = buildEnv({ + webhookOriginFlag: "relayfile-cloud", + cloudWebWorker, + relayfileCloudWorker, + }); + + const request = new Request( + "https://origin.agentrelay.cloud/cloud/api/v1/webhooks/nango", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "forward", providerConfigKey: "unknown-provider" }), + }, + ); + + await worker.fetch(request, env, buildCtx()); + + expect(cloudWebWorker.fetch).toHaveBeenCalledOnce(); + expect(relayfileCloudWorker.fetch).not.toHaveBeenCalled(); + }); + + it("bypasses relayfile-cloud when request carries x-cloud-webhook-relayfile-cloud-forwarded header (loop break)", async () => { + const cloudWebWorker = makeBinding(); + const relayfileCloudWorker = makeBinding(); + const env = buildEnv({ + webhookOriginFlag: "relayfile-cloud", + cloudWebWorker, + relayfileCloudWorker, + }); + + const request = new Request( + "https://origin.agentrelay.cloud/cloud/api/v1/webhooks/nango", + { + method: "POST", + headers: { + "content-type": "application/json", + "x-cloud-webhook-relayfile-cloud-forwarded": "relayfile-cloud", + }, + body: JSON.stringify({ type: "forward", providerConfigKey: "github-relay" }), + }, + ); + + await worker.fetch(request, env, buildCtx()); + + expect(cloudWebWorker.fetch).toHaveBeenCalledOnce(); + expect(relayfileCloudWorker.fetch).not.toHaveBeenCalled(); + }); + it("routes to cloud-web when WEBHOOK_ORIGIN flag is unset (regression: still works without flag)", async () => { const cloudWebWorker = makeBinding(); const webhookWorker = makeBinding(); diff --git a/router/vitest.config.ts b/router/vitest.config.ts new file mode 100644 index 0000000..8b5840a --- /dev/null +++ b/router/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + }, +}); diff --git a/router/wrangler.jsonc b/router/wrangler.jsonc index 753ff32..26f9ae1 100644 --- a/router/wrangler.jsonc +++ b/router/wrangler.jsonc @@ -15,7 +15,10 @@ // binding — SST names the webhook Worker with a random hash suffix, so it // isn't bindable by a stable script name.) "services": [ - { "binding": "CLOUD_WEB_WORKER", "service": "cloud-web-worker" } + { "binding": "CLOUD_WEB_WORKER", "service": "cloud-web-worker" }, + // relayfile-cloud worker — routes allowlisted Nango ingest webhooks when + // WEBHOOK_ORIGIN flag is "relayfile-cloud". Dormant until flag is set. + { "binding": "RELAYFILE_CLOUD_WORKER", "service": "relayfile-cloud" } ], // ROUTER_CONFIG: holds the WEBHOOK_ORIGIN ops flag + recorder config. @@ -41,7 +44,10 @@ // is cloud-owned and the script name carries a random hash suffix, so it // would change if cloud recreates that Worker — re-copy it from the live // router's WEBHOOK_WORKER_ORIGIN if webhook routing ever 5xxs. - "WEBHOOK_WORKER_ORIGIN": "https://incoming-webhooks.agentrelay.com" + "WEBHOOK_WORKER_ORIGIN": "https://incoming-webhooks.agentrelay.com", + // Fallback URL when RELAYFILE_CLOUD_WORKER service binding is absent. + // Dormant: only used when WEBHOOK_ORIGIN flag is "relayfile-cloud". + "RELAYFILE_CLOUD_ORIGIN": "https://file.agentrelay.com" }, // The apex front door. Cloud's prod AgentRelayRouter must have released the