@every-app/sdk 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/cloudflare/server/gateway.js +5 -0
  2. package/dist/shared/bypassGatewayLocalOnly.d.ts +2 -1
  3. package/dist/shared/bypassGatewayLocalOnly.d.ts.map +1 -1
  4. package/dist/shared/bypassGatewayLocalOnly.js +6 -2
  5. package/dist/tanstack/server/authenticateRequest.d.ts +2 -0
  6. package/dist/tanstack/server/authenticateRequest.d.ts.map +1 -1
  7. package/dist/tanstack/server/authenticateRequest.js +81 -4
  8. package/package.json +3 -3
  9. package/src/cloudflare/getLocalD1Url.ts +0 -64
  10. package/src/cloudflare/index.ts +0 -1
  11. package/src/cloudflare/lazyInit.ts +0 -67
  12. package/src/cloudflare/server/gateway.test.ts +0 -215
  13. package/src/cloudflare/server/gateway.ts +0 -106
  14. package/src/cloudflare/server/index.ts +0 -2
  15. package/src/core/authenticatedFetch.ts +0 -54
  16. package/src/core/index.ts +0 -12
  17. package/src/core/sessionManager.test.ts +0 -939
  18. package/src/core/sessionManager.ts +0 -492
  19. package/src/env.d.ts +0 -13
  20. package/src/shared/bypassGatewayLocalOnly.ts +0 -55
  21. package/src/shared/parseMessagePayload.ts +0 -22
  22. package/src/tanstack/EmbeddedAppProvider.tsx +0 -96
  23. package/src/tanstack/GatewayRequiredError.tsx +0 -150
  24. package/src/tanstack/_internal/useEveryAppSession.test.ts +0 -40
  25. package/src/tanstack/_internal/useEveryAppSession.tsx +0 -74
  26. package/src/tanstack/index.ts +0 -3
  27. package/src/tanstack/server/authConfig.ts +0 -19
  28. package/src/tanstack/server/authenticateRequest.test.ts +0 -447
  29. package/src/tanstack/server/authenticateRequest.ts +0 -137
  30. package/src/tanstack/server/index.ts +0 -3
  31. package/src/tanstack/server/types.ts +0 -4
  32. package/src/tanstack/useEveryAppRouter.tsx +0 -83
  33. package/src/tanstack/useSessionTokenClientMiddleware.ts +0 -43
@@ -32,6 +32,11 @@ export async function fetchGateway({ env, url, init, }) {
32
32
  return fetch(authenticatedRequest);
33
33
  }
34
34
  function applyAppTokenAuth(request, env) {
35
+ const gatewayOrigin = new URL(getGatewayUrl(env)).origin;
36
+ const requestOrigin = new URL(request.url).origin;
37
+ if (requestOrigin !== gatewayOrigin) {
38
+ throw new Error(`Refusing to send gateway token to non-gateway origin: ${requestOrigin}`);
39
+ }
35
40
  const appToken = getGatewayAppApiToken(env);
36
41
  if (!appToken) {
37
42
  throw new Error("GATEWAY_APP_API_TOKEN is required. Run `npx everyapp app deploy` to provision one.");
@@ -3,7 +3,8 @@ export declare const BYPASS_GATEWAY_LOCAL_ONLY_USER_ID = "demo-local-user";
3
3
  export declare const BYPASS_GATEWAY_LOCAL_ONLY_EMAIL = "demo-local-user@local";
4
4
  export declare function isBypassGatewayLocalOnlyClient(): boolean;
5
5
  export declare function isBypassGatewayLocalOnlyServer(): boolean;
6
- export declare function createBypassGatewayLocalOnlySessionPayload(audience: string): {
6
+ export declare function createBypassGatewayLocalOnlySessionPayload(audience: string, orgId: string): {
7
+ orgId: string;
7
8
  sub: string;
8
9
  email: string;
9
10
  iss: string;
@@ -1 +1 @@
1
- {"version":3,"file":"bypassGatewayLocalOnly.d.ts","sourceRoot":"","sources":["../../src/shared/bypassGatewayLocalOnly.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,8BAA8B,CAAC;AAC3E,eAAO,MAAM,iCAAiC,oBAAoB,CAAC;AACnE,eAAO,MAAM,+BAA+B,0BAA0B,CAAC;AAEvE,wBAAgB,8BAA8B,IAAI,OAAO,CAYxD;AAED,wBAAgB,8BAA8B,IAAI,OAAO,CAsBxD;AAED,wBAAgB,0CAA0C,CAAC,QAAQ,EAAE,MAAM;;;;;;;EAY1E"}
1
+ {"version":3,"file":"bypassGatewayLocalOnly.d.ts","sourceRoot":"","sources":["../../src/shared/bypassGatewayLocalOnly.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,8BAA8B,CAAC;AAC3E,eAAO,MAAM,iCAAiC,oBAAoB,CAAC;AACnE,eAAO,MAAM,+BAA+B,0BAA0B,CAAC;AAEvE,wBAAgB,8BAA8B,IAAI,OAAO,CAYxD;AAED,wBAAgB,8BAA8B,IAAI,OAAO,CAsBxD;AAED,wBAAgB,0CAA0C,CACxD,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM;;;;;;;;EAkBd"}
@@ -27,10 +27,10 @@ export function isBypassGatewayLocalOnlyServer() {
27
27
  }
28
28
  return false;
29
29
  }
30
- export function createBypassGatewayLocalOnlySessionPayload(audience) {
30
+ export function createBypassGatewayLocalOnlySessionPayload(audience, orgId) {
31
31
  const issuedAt = Math.floor(Date.now() / 1000);
32
32
  const expiresAt = issuedAt + 60 * 60;
33
- return {
33
+ const payload = {
34
34
  sub: BYPASS_GATEWAY_LOCAL_ONLY_USER_ID,
35
35
  email: BYPASS_GATEWAY_LOCAL_ONLY_EMAIL,
36
36
  iss: "local",
@@ -38,4 +38,8 @@ export function createBypassGatewayLocalOnlySessionPayload(audience) {
38
38
  iat: issuedAt,
39
39
  exp: expiresAt,
40
40
  };
41
+ return {
42
+ ...payload,
43
+ orgId,
44
+ };
41
45
  }
@@ -16,6 +16,8 @@ interface SessionTokenPayload {
16
16
  iat: number;
17
17
  /** User email - used for user provisioning in apps */
18
18
  email?: string;
19
+ /** Organization ID used for org-bound runtime enforcement */
20
+ orgId?: string;
19
21
  }
20
22
  export declare function authenticateRequest(authConfig: AuthConfig, providedRequest?: Request): Promise<SessionTokenPayload | null>;
21
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"authenticateRequest.d.ts","sourceRoot":"","sources":["../../../src/tanstack/server/authenticateRequest.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ7C;;;GAGG;AACH,UAAU,mBAAmB;IAC3B,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,UAAU,EACtB,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA8CrC;AAyCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK3E"}
1
+ {"version":3,"file":"authenticateRequest.d.ts","sourceRoot":"","sources":["../../../src/tanstack/server/authenticateRequest.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ7C;;;GAGG;AACH,UAAU,mBAAmB;IAC3B,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAYD,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,UAAU,EACtB,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA+FrC;AA8CD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK3E"}
@@ -5,9 +5,10 @@ import { BYPASS_GATEWAY_LOCAL_ONLY_TOKEN, createBypassGatewayLocalOnlySessionPay
5
5
  export async function authenticateRequest(authConfig, providedRequest) {
6
6
  const request = providedRequest || getRequest();
7
7
  const authHeader = request.headers.get("authorization");
8
- const bypassGatewayLocalOnlyEnv = env.BYPASS_GATEWAY_LOCAL_ONLY;
8
+ const expectedOrganizationId = getOptionalEnvValue("EVERY_APP_ORG_ID");
9
+ const appTenancyMode = getAppTenancyMode();
9
10
  const isBypassGatewayLocalOnly = import.meta.env.PROD !== true &&
10
- (bypassGatewayLocalOnlyEnv === "true" ||
11
+ (getOptionalEnvValue("BYPASS_GATEWAY_LOCAL_ONLY") === "true" ||
11
12
  isBypassGatewayLocalOnlyServer() === true);
12
13
  if (!authHeader) {
13
14
  return null;
@@ -20,24 +21,62 @@ export async function authenticateRequest(authConfig, providedRequest) {
20
21
  if (token !== BYPASS_GATEWAY_LOCAL_ONLY_TOKEN) {
21
22
  return null;
22
23
  }
23
- return createBypassGatewayLocalOnlySessionPayload(authConfig.audience);
24
+ if (!expectedOrganizationId) {
25
+ console.error(JSON.stringify({
26
+ message: "BYPASS_GATEWAY_LOCAL_ONLY requires EVERY_APP_ORG_ID to be set.",
27
+ audience: authConfig.audience,
28
+ appTenancyMode,
29
+ }));
30
+ return null;
31
+ }
32
+ const session = createBypassGatewayLocalOnlySessionPayload(authConfig.audience, expectedOrganizationId);
33
+ if (!validateOrganizationBinding({
34
+ session,
35
+ source: "bypass",
36
+ audience: authConfig.audience,
37
+ expectedOrganizationId,
38
+ appTenancyMode,
39
+ })) {
40
+ return null;
41
+ }
42
+ return session;
24
43
  }
25
44
  try {
26
45
  const session = await verifySessionToken(token, authConfig);
46
+ if (!validateOrganizationBinding({
47
+ session,
48
+ source: "session",
49
+ audience: authConfig.audience,
50
+ expectedOrganizationId,
51
+ appTenancyMode,
52
+ })) {
53
+ return null;
54
+ }
27
55
  return session;
28
56
  }
29
57
  catch (error) {
58
+ const isProd = import.meta.env.PROD === true;
30
59
  console.error(JSON.stringify({
31
60
  message: "Error verifying session token",
32
61
  error: error instanceof Error ? error.message : String(error),
33
- stack: error instanceof Error ? error.stack : undefined,
62
+ stack: isProd === true
63
+ ? undefined
64
+ : error instanceof Error
65
+ ? error.stack
66
+ : undefined,
34
67
  errorType: error instanceof Error ? error.constructor.name : "Unknown",
35
68
  issuer: authConfig.issuer,
36
69
  audience: authConfig.audience,
70
+ expectedOrganizationId,
71
+ appTenancyMode,
37
72
  }));
38
73
  return null;
39
74
  }
40
75
  }
76
+ function getAppTenancyMode() {
77
+ const mode = getOptionalEnvValue("APP_TENANCY_MODE")?.toLowerCase();
78
+ return mode === "multi" ? "multi" : "single";
79
+ }
41
80
  async function verifySessionToken(token, config) {
42
81
  const { issuer, audience } = config;
43
82
  if (!issuer) {
@@ -75,3 +114,41 @@ export function extractBearerToken(authHeader) {
75
114
  }
76
115
  return authHeader.substring(7);
77
116
  }
117
+ function getOptionalEnvValue(key) {
118
+ const value = env[key];
119
+ const trimmed = value?.trim();
120
+ return trimmed || null;
121
+ }
122
+ function validateOrganizationBinding({ session, source, audience, expectedOrganizationId, appTenancyMode, }) {
123
+ if (!session.orgId) {
124
+ console.error(JSON.stringify({
125
+ message: source === "bypass"
126
+ ? "Bypass mode requires organization claim"
127
+ : "Session token is missing orgId. SDK v0.2.0 requires org-bound session tokens. Redeploy the app with EVERY_APP_ORG_ID configured and request a fresh session token.",
128
+ audience,
129
+ appTenancyMode,
130
+ }));
131
+ return false;
132
+ }
133
+ if (appTenancyMode === "single" && !expectedOrganizationId) {
134
+ console.error(JSON.stringify({
135
+ message: "EVERY_APP_ORG_ID is required when APP_TENANCY_MODE=single. Configure EVERY_APP_ORG_ID in worker secrets (for example via `every app deploy`) and retry.",
136
+ audience,
137
+ appTenancyMode,
138
+ }));
139
+ return false;
140
+ }
141
+ if (appTenancyMode === "single" && session.orgId !== expectedOrganizationId) {
142
+ console.error(JSON.stringify({
143
+ message: source === "bypass"
144
+ ? "Bypass mode organization mismatch"
145
+ : "Session token organization mismatch",
146
+ tokenOrgId: session.orgId,
147
+ expectedOrganizationId,
148
+ audience,
149
+ appTenancyMode,
150
+ }));
151
+ return false;
152
+ }
153
+ return true;
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@every-app/sdk",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -27,11 +27,11 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
- "src"
30
+ "README.md"
31
31
  ],
32
32
  "scripts": {
33
- "postinstall": "tsc -p tsconfig.build.json",
34
33
  "build": "tsc -p tsconfig.build.json",
34
+ "prepack": "npm run build",
35
35
  "build:prepublish": "pnpm install --ignore-scripts && npm run types:check && npm run build",
36
36
  "prepublishOnly": "npm run build:prepublish",
37
37
  "types:check": "tsc --noEmit",
@@ -1,64 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { execSync } from "child_process";
4
- import { parse } from "jsonc-parser";
5
-
6
- function findSqliteFile(basePath: string): string | undefined {
7
- return fs
8
- .readdirSync(basePath, { encoding: "utf-8", recursive: true })
9
- .find((f) => f.endsWith(".sqlite"));
10
- }
11
-
12
- export function getLocalD1Url() {
13
- const basePath = path.resolve(".wrangler");
14
-
15
- // Check if .wrangler directory exists
16
- if (!fs.existsSync(basePath)) {
17
- console.error(
18
- "================================================================================",
19
- );
20
- console.error("WARNING: .wrangler directory not found");
21
- console.error("This is expected in CI/non-development environments.");
22
- console.error(
23
- "The local D1 database is only available after running 'wrangler dev' which you can trigger by running 'npm run dev'.",
24
- );
25
- console.error(
26
- "================================================================================",
27
- );
28
- return null;
29
- }
30
-
31
- let dbFile = findSqliteFile(basePath);
32
-
33
- if (!dbFile) {
34
- // Read wrangler.jsonc to get the database name
35
- const wranglerConfigPath = path.resolve("wrangler.jsonc");
36
- const wranglerConfig = parse(fs.readFileSync(wranglerConfigPath, "utf-8"));
37
-
38
- const databaseName = wranglerConfig.d1_databases?.[0]?.database_name;
39
-
40
- if (!databaseName) {
41
- throw new Error(
42
- "Could not find database_name in wrangler.jsonc d1_databases configuration",
43
- );
44
- }
45
-
46
- // Execute the command to initialize the local database
47
- console.log(`Initializing local D1 database: ${databaseName}...`);
48
- execSync(
49
- `npx wrangler d1 execute ${databaseName} --local --command "SELECT 1;"`,
50
- { stdio: "pipe" },
51
- );
52
-
53
- // Try to find the db file again after initialization
54
- dbFile = findSqliteFile(basePath);
55
-
56
- if (!dbFile) {
57
- throw new Error(
58
- `Failed to initialize local D1 database. The sqlite file was not created.`,
59
- );
60
- }
61
- }
62
-
63
- return path.resolve(basePath, dbFile);
64
- }
@@ -1 +0,0 @@
1
- export { lazyInitForWorkers } from "./lazyInit.js";
@@ -1,67 +0,0 @@
1
- /**
2
- * Wraps a factory function in a Proxy to defer initialization until first access.
3
- * This prevents async operations (Like creating Tanstack DB Collections) from running in Cloudflare Workers' global scope.
4
- *
5
- * @param factory - A function that creates and returns the resource.
6
- * Must be a callback to defer execution; passing the value directly
7
- * would evaluate it at module load time, triggering the Cloudflare error.
8
- * @returns A Proxy that lazily initializes the resource on first property access
9
- *
10
- * @example
11
- * ```ts
12
- * export const myCollection = lazyInitForWorkers(() =>
13
- * createCollection(queryCollectionOptions({
14
- * queryKey: ["myData"],
15
- * queryFn: async () => fetchData(),
16
- * // ... other options
17
- * }))
18
- * );
19
- * ```
20
- */
21
- export function lazyInitForWorkers<T extends object>(factory: () => T): T {
22
- // Closure: This variable is captured by getInstance() and the Proxy traps below.
23
- // It remains in memory as long as the returned Proxy is referenced, enabling singleton behavior.
24
- let instance: T | null = null;
25
-
26
- function getInstance() {
27
- if (!instance) {
28
- instance = factory();
29
- }
30
- return instance;
31
- }
32
-
33
- return new Proxy({} as T, {
34
- get(_, prop) {
35
- const inst = getInstance();
36
- const value = inst[prop as keyof T];
37
- // Bind methods to the instance to preserve `this` context
38
- return typeof value === "function" ? value.bind(inst) : value;
39
- },
40
- set(_, prop, value) {
41
- const inst = getInstance();
42
- (inst as any)[prop] = value;
43
- return true;
44
- },
45
- deleteProperty(_, prop) {
46
- const inst = getInstance();
47
- delete (inst as any)[prop];
48
- return true;
49
- },
50
- has(_, prop) {
51
- const inst = getInstance();
52
- return prop in inst;
53
- },
54
- ownKeys(_) {
55
- const inst = getInstance();
56
- return Reflect.ownKeys(inst);
57
- },
58
- getOwnPropertyDescriptor(_, prop) {
59
- const inst = getInstance();
60
- return Reflect.getOwnPropertyDescriptor(inst, prop);
61
- },
62
- getPrototypeOf(_) {
63
- const inst = getInstance();
64
- return Reflect.getPrototypeOf(inst);
65
- },
66
- });
67
- }
@@ -1,215 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { fetchGateway, getGatewayUrl } from "./gateway";
3
-
4
- type TestFetcher = {
5
- fetch: ReturnType<typeof vi.fn>;
6
- };
7
-
8
- type TestEnv = {
9
- GATEWAY_URL?: string;
10
- EVERY_APP_GATEWAY?: TestFetcher;
11
- GATEWAY_APP_API_TOKEN?: string;
12
- APP_TOKEN?: string;
13
- };
14
-
15
- describe("gateway server helpers", () => {
16
- beforeEach(() => {
17
- vi.restoreAllMocks();
18
- });
19
-
20
- afterEach(() => {
21
- vi.restoreAllMocks();
22
- });
23
-
24
- it("returns gateway URL and throws when missing", () => {
25
- expect(getGatewayUrl({ GATEWAY_URL: "https://gateway.example.com" })).toBe(
26
- "https://gateway.example.com",
27
- );
28
-
29
- expect(() => getGatewayUrl({} as TestEnv)).toThrow(
30
- "GATEWAY_URL is required",
31
- );
32
- });
33
-
34
- it("fetches via HTTP when service binding is unavailable", async () => {
35
- const fetchMock = vi
36
- .spyOn(globalThis, "fetch")
37
- .mockResolvedValue(new Response("ok", { status: 200 }));
38
-
39
- const env: TestEnv = {
40
- GATEWAY_URL: "https://gateway.example.com",
41
- GATEWAY_APP_API_TOKEN: "eat_test_token",
42
- };
43
-
44
- await fetchGateway({
45
- env,
46
- url: "/api/ai/openai/v1/responses?stream=true",
47
- init: { method: "POST" },
48
- });
49
-
50
- expect(fetchMock).toHaveBeenCalledTimes(1);
51
- const [requestArg] = fetchMock.mock.calls[0];
52
- const request = requestArg as Request;
53
-
54
- expect(request.url).toBe(
55
- "https://gateway.example.com/api/ai/openai/v1/responses?stream=true",
56
- );
57
- });
58
-
59
- it("fetches via service binding when available in production", async () => {
60
- // Service binding is only used when import.meta.env.PROD is true
61
- const originalProd = import.meta.env.PROD;
62
- import.meta.env.PROD = true;
63
-
64
- try {
65
- const bindingFetch = vi
66
- .fn()
67
- .mockResolvedValue(new Response("ok", { status: 200 }));
68
-
69
- const env: TestEnv = {
70
- GATEWAY_URL: "https://gateway.example.com",
71
- EVERY_APP_GATEWAY: { fetch: bindingFetch },
72
- GATEWAY_APP_API_TOKEN: "eat_test_token",
73
- };
74
-
75
- await fetchGateway({
76
- env,
77
- url: "/api/ai/openai/v1/chat/completions",
78
- init: { method: "POST" },
79
- });
80
-
81
- expect(bindingFetch).toHaveBeenCalledTimes(1);
82
- const [requestArg] = bindingFetch.mock.calls[0];
83
- const request = requestArg as Request;
84
- expect(request.url).toBe(
85
- "http://localhost/api/ai/openai/v1/chat/completions",
86
- );
87
- } finally {
88
- import.meta.env.PROD = originalProd;
89
- }
90
- });
91
-
92
- it("skips service binding in development and uses HTTP fetch", async () => {
93
- const fetchMock = vi
94
- .spyOn(globalThis, "fetch")
95
- .mockResolvedValue(new Response("ok", { status: 200 }));
96
-
97
- const bindingFetch = vi.fn();
98
-
99
- const env: TestEnv = {
100
- GATEWAY_URL: "https://gateway.example.com",
101
- EVERY_APP_GATEWAY: { fetch: bindingFetch },
102
- GATEWAY_APP_API_TOKEN: "eat_test_token",
103
- };
104
-
105
- await fetchGateway({
106
- env,
107
- url: "/api/ai/openai/v1/chat/completions",
108
- init: { method: "POST" },
109
- });
110
-
111
- // In dev, should use HTTP fetch, not service binding
112
- expect(bindingFetch).not.toHaveBeenCalled();
113
- expect(fetchMock).toHaveBeenCalledTimes(1);
114
- });
115
-
116
- it("always injects app token and strips Authorization header", async () => {
117
- const fetchMock = vi
118
- .spyOn(globalThis, "fetch")
119
- .mockResolvedValue(new Response("ok", { status: 200 }));
120
-
121
- const env: TestEnv = {
122
- GATEWAY_URL: "https://gateway.example.com",
123
- GATEWAY_APP_API_TOKEN: "eat_my_token",
124
- };
125
-
126
- await fetchGateway({
127
- env,
128
- url: "/api/ai/openai/v1/responses",
129
- init: {
130
- method: "POST",
131
- headers: {
132
- authorization: "Bearer some-sdk-dummy-value",
133
- },
134
- },
135
- });
136
-
137
- expect(fetchMock).toHaveBeenCalledTimes(1);
138
- const [requestArg] = fetchMock.mock.calls[0];
139
- const request = requestArg as Request;
140
-
141
- expect(request.headers.get("x-every-app-token")).toBe("eat_my_token");
142
- expect(request.headers.get("authorization")).toBeNull();
143
- });
144
-
145
- it("throws when app token is missing", async () => {
146
- const env: TestEnv = {
147
- GATEWAY_URL: "https://gateway.example.com",
148
- };
149
-
150
- await expect(
151
- fetchGateway({
152
- env,
153
- url: "/api/ai/openai/v1/responses",
154
- }),
155
- ).rejects.toThrow("GATEWAY_APP_API_TOKEN is required");
156
- });
157
-
158
- it("supports legacy APP_TOKEN env variable", async () => {
159
- const fetchMock = vi
160
- .spyOn(globalThis, "fetch")
161
- .mockResolvedValue(new Response("ok", { status: 200 }));
162
-
163
- const env: TestEnv = {
164
- GATEWAY_URL: "https://gateway.example.com",
165
- APP_TOKEN: "eat_legacy_token",
166
- };
167
-
168
- await fetchGateway({
169
- env,
170
- url: "/api/ai/openai/v1/responses",
171
- init: { method: "POST" },
172
- });
173
-
174
- expect(fetchMock).toHaveBeenCalledTimes(1);
175
- const [requestArg] = fetchMock.mock.calls[0];
176
- const request = requestArg as Request;
177
-
178
- expect(request.headers.get("x-every-app-token")).toBe("eat_legacy_token");
179
- expect(request.headers.get("authorization")).toBeNull();
180
- });
181
-
182
- it("supports Request input and preserves method/body", async () => {
183
- const fetchMock = vi
184
- .spyOn(globalThis, "fetch")
185
- .mockResolvedValue(new Response("ok", { status: 200 }));
186
-
187
- const env: TestEnv = {
188
- GATEWAY_URL: "https://gateway.example.com",
189
- GATEWAY_APP_API_TOKEN: "eat_test_token",
190
- };
191
-
192
- const sourceRequest = new Request(
193
- "https://gateway.example.com/api/ai/openai/v1/responses",
194
- {
195
- method: "POST",
196
- body: JSON.stringify({ model: "gpt-5.2" }),
197
- headers: { "content-type": "application/json" },
198
- },
199
- );
200
-
201
- await fetchGateway({
202
- env,
203
- url: sourceRequest,
204
- });
205
-
206
- expect(fetchMock).toHaveBeenCalledTimes(1);
207
- const [requestArg] = fetchMock.mock.calls[0];
208
- const request = requestArg as Request;
209
- expect(request.url).toBe(
210
- "https://gateway.example.com/api/ai/openai/v1/responses",
211
- );
212
- expect(request.method).toBe("POST");
213
- expect(request.headers.get("content-type")).toBe("application/json");
214
- });
215
- });
@@ -1,106 +0,0 @@
1
- const SERVICE_BINDING_ORIGIN = "http://localhost";
2
- const APP_TOKEN_HEADER = "x-every-app-token";
3
-
4
- interface GatewayFetcher {
5
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
6
- }
7
-
8
- interface GatewayEnv {
9
- GATEWAY_URL?: string;
10
- EVERY_APP_GATEWAY?: GatewayFetcher;
11
- GATEWAY_APP_API_TOKEN?: string;
12
- APP_TOKEN?: string;
13
- }
14
-
15
- interface FetchGatewayOptions {
16
- env: GatewayEnv;
17
- /** The URL, path, or Request to send to the gateway. Typically the full URL
18
- * passed by a provider SDK's custom `fetch` override. */
19
- url: string | URL | Request;
20
- /** Standard `RequestInit` (method, headers, body, etc.) from the provider SDK. */
21
- init?: RequestInit;
22
- }
23
-
24
- export function getGatewayUrl(env: GatewayEnv): string {
25
- const gatewayUrl = env.GATEWAY_URL?.trim();
26
- if (!gatewayUrl) {
27
- throw new Error("GATEWAY_URL is required");
28
- }
29
- return gatewayUrl;
30
- }
31
-
32
- /**
33
- * Fetch from the gateway proxy, authenticating with the app token.
34
- *
35
- * - Strips any existing `Authorization` header (the gateway only accepts
36
- * app token auth via `x-every-app-token`).
37
- * - Requires `GATEWAY_APP_API_TOKEN` (or legacy `APP_TOKEN`) in the env.
38
- * - Routes via service binding in production, falls back to HTTP in dev.
39
- */
40
- export async function fetchGateway({
41
- env,
42
- url,
43
- init,
44
- }: FetchGatewayOptions): Promise<Response> {
45
- const gatewayBaseUrl = getGatewayUrl(env);
46
- const resolvedRequest = toRequest(url, init, gatewayBaseUrl);
47
- const authenticatedRequest = applyAppTokenAuth(resolvedRequest, env);
48
-
49
- // Use service binding in production for zero-latency internal routing.
50
- // In local dev wrangler still exposes the binding object, but the target
51
- // service usually isn't running locally, so we skip it and use HTTP fetch.
52
- if (import.meta.env.PROD && env.EVERY_APP_GATEWAY) {
53
- const url = new URL(authenticatedRequest.url);
54
- const bindingUrl = `${SERVICE_BINDING_ORIGIN}${url.pathname}${url.search}`;
55
- const bindingRequest = new Request(bindingUrl, authenticatedRequest);
56
- return env.EVERY_APP_GATEWAY.fetch(bindingRequest);
57
- }
58
-
59
- // HTTP fetch – used in local dev, or as a fallback when no binding exists
60
- return fetch(authenticatedRequest);
61
- }
62
-
63
- function applyAppTokenAuth(request: Request, env: GatewayEnv): Request {
64
- const appToken = getGatewayAppApiToken(env);
65
- if (!appToken) {
66
- throw new Error(
67
- "GATEWAY_APP_API_TOKEN is required. Run `npx everyapp app deploy` to provision one.",
68
- );
69
- }
70
-
71
- const headers = new Headers(request.headers);
72
- headers.delete("authorization");
73
- headers.set(APP_TOKEN_HEADER, appToken);
74
- return new Request(request, { headers });
75
- }
76
-
77
- function getGatewayAppApiToken(env: GatewayEnv): string | null {
78
- const configuredToken = env.GATEWAY_APP_API_TOKEN?.trim();
79
- if (configuredToken) {
80
- return configuredToken;
81
- }
82
-
83
- const legacyToken = env.APP_TOKEN?.trim();
84
- return legacyToken || null;
85
- }
86
-
87
- function toRequest(
88
- input: RequestInfo | URL,
89
- init?: RequestInit,
90
- baseUrl?: string,
91
- ): Request {
92
- if (input instanceof Request) {
93
- return init ? new Request(input, init) : input;
94
- }
95
-
96
- if (input instanceof URL) {
97
- return new Request(input.toString(), init);
98
- }
99
-
100
- if (typeof input === "string" && baseUrl && !/^https?:\/\//i.test(input)) {
101
- const normalizedPath = input.startsWith("/") ? input : `/${input}`;
102
- return new Request(new URL(normalizedPath, baseUrl).toString(), init);
103
- }
104
-
105
- return new Request(input, init);
106
- }
@@ -1,2 +0,0 @@
1
- export { getLocalD1Url } from "../getLocalD1Url.js";
2
- export { fetchGateway, getGatewayUrl } from "./gateway.js";