@every-app/sdk 0.0.3 → 0.0.5

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.
@@ -0,0 +1,416 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { SignJWT, generateKeyPair, exportJWK } from "jose";
3
+
4
+ // Mock the cloudflare:workers module
5
+ vi.mock("cloudflare:workers", () => ({
6
+ env: {
7
+ GATEWAY_URL: "https://gateway.example.com",
8
+ EVERY_APP_GATEWAY: null,
9
+ },
10
+ }));
11
+
12
+ // Mock @tanstack/react-start/server
13
+ vi.mock("@tanstack/react-start/server", () => ({
14
+ getRequest: vi.fn(),
15
+ }));
16
+
17
+ import { authenticateRequest } from "./authenticateRequest";
18
+ import type { AuthConfig } from "./types";
19
+
20
+ describe("authenticateRequest", () => {
21
+ let keyPair: Awaited<ReturnType<typeof generateKeyPair>>;
22
+ let jwks: { keys: object[] };
23
+
24
+ const authConfig: AuthConfig = {
25
+ issuer: "https://gateway.example.com",
26
+ audience: "test-app",
27
+ };
28
+
29
+ beforeEach(async () => {
30
+ // Generate a fresh key pair for each test
31
+ keyPair = await generateKeyPair("RS256");
32
+ const publicJwk = await exportJWK(keyPair.publicKey);
33
+
34
+ jwks = {
35
+ keys: [
36
+ {
37
+ ...publicJwk,
38
+ kid: "test-key-1",
39
+ use: "sig",
40
+ alg: "RS256",
41
+ },
42
+ ],
43
+ };
44
+
45
+ // Mock global fetch for JWKS endpoint
46
+ global.fetch = vi.fn().mockImplementation(async (url: string) => {
47
+ if (url.includes("/api/embedded/jwks")) {
48
+ return new Response(JSON.stringify(jwks), {
49
+ status: 200,
50
+ headers: { "Content-Type": "application/json" },
51
+ });
52
+ }
53
+ return new Response("Not Found", { status: 404 });
54
+ });
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.restoreAllMocks();
59
+ });
60
+
61
+ async function createValidToken(overrides: Record<string, unknown> = {}) {
62
+ const jwt = await new SignJWT({
63
+ email: "user@example.com",
64
+ appId: "test-app",
65
+ permissions: ["read", "write"],
66
+ ...overrides,
67
+ })
68
+ .setProtectedHeader({ alg: "RS256" })
69
+ .setSubject("user-123")
70
+ .setIssuer(authConfig.issuer)
71
+ .setAudience(authConfig.audience)
72
+ .setExpirationTime("1h")
73
+ .setIssuedAt()
74
+ .sign(keyPair.privateKey);
75
+
76
+ return jwt;
77
+ }
78
+
79
+ function createRequest(authHeader?: string): Request {
80
+ const headers = new Headers();
81
+ if (authHeader) {
82
+ headers.set("authorization", authHeader);
83
+ }
84
+ return new Request("https://app.example.com/api/test", { headers });
85
+ }
86
+
87
+ describe("missing or invalid authorization header", () => {
88
+ it("returns null when no authorization header is present", async () => {
89
+ const request = createRequest();
90
+ const result = await authenticateRequest(authConfig, request);
91
+ expect(result).toBeNull();
92
+ });
93
+
94
+ it("returns null for empty authorization header", async () => {
95
+ const request = createRequest("");
96
+ const result = await authenticateRequest(authConfig, request);
97
+ expect(result).toBeNull();
98
+ });
99
+
100
+ it("returns null for non-Bearer authorization", async () => {
101
+ const request = createRequest("Basic dXNlcjpwYXNz");
102
+ const result = await authenticateRequest(authConfig, request);
103
+ expect(result).toBeNull();
104
+ });
105
+
106
+ it("returns null for malformed Bearer token (no space)", async () => {
107
+ const request = createRequest("BearereyJhbGciOiJSUzI1NiJ9");
108
+ const result = await authenticateRequest(authConfig, request);
109
+ expect(result).toBeNull();
110
+ });
111
+
112
+ it("returns null for lowercase bearer prefix", async () => {
113
+ const token = await createValidToken();
114
+ const request = createRequest(`bearer ${token}`);
115
+ const result = await authenticateRequest(authConfig, request);
116
+ expect(result).toBeNull();
117
+ });
118
+ });
119
+
120
+ describe("valid token verification", () => {
121
+ it("returns payload for valid token with correct issuer and audience", async () => {
122
+ const token = await createValidToken();
123
+ const request = createRequest(`Bearer ${token}`);
124
+
125
+ const result = await authenticateRequest(authConfig, request);
126
+
127
+ expect(result).not.toBeNull();
128
+ expect(result!.sub).toBe("user-123");
129
+ expect(result!.email).toBe("user@example.com");
130
+ expect(result!.appId).toBe("test-app");
131
+ expect(result!.permissions).toEqual(["read", "write"]);
132
+ expect(result!.iss).toBe(authConfig.issuer);
133
+ expect(result!.aud).toBe(authConfig.audience);
134
+ });
135
+
136
+ it("includes iat and exp claims in returned payload", async () => {
137
+ const token = await createValidToken();
138
+ const request = createRequest(`Bearer ${token}`);
139
+
140
+ const result = await authenticateRequest(authConfig, request);
141
+
142
+ expect(result).not.toBeNull();
143
+ expect(typeof result!.iat).toBe("number");
144
+ expect(typeof result!.exp).toBe("number");
145
+ expect(result!.exp).toBeGreaterThan(result!.iat);
146
+ });
147
+ });
148
+
149
+ describe("token expiration", () => {
150
+ it("returns null for expired token", async () => {
151
+ const jwt = await new SignJWT({ email: "user@example.com" })
152
+ .setProtectedHeader({ alg: "RS256" })
153
+ .setSubject("user-123")
154
+ .setIssuer(authConfig.issuer)
155
+ .setAudience(authConfig.audience)
156
+ .setExpirationTime("-1h") // Expired 1 hour ago
157
+ .setIssuedAt(Math.floor(Date.now() / 1000) - 7200) // Issued 2 hours ago
158
+ .sign(keyPair.privateKey);
159
+
160
+ const request = createRequest(`Bearer ${jwt}`);
161
+ const result = await authenticateRequest(authConfig, request);
162
+
163
+ expect(result).toBeNull();
164
+ });
165
+ });
166
+
167
+ describe("issuer validation", () => {
168
+ it("returns null for token with wrong issuer", async () => {
169
+ const jwt = await new SignJWT({ email: "user@example.com" })
170
+ .setProtectedHeader({ alg: "RS256" })
171
+ .setSubject("user-123")
172
+ .setIssuer("https://malicious.example.com") // Wrong issuer
173
+ .setAudience(authConfig.audience)
174
+ .setExpirationTime("1h")
175
+ .setIssuedAt()
176
+ .sign(keyPair.privateKey);
177
+
178
+ const request = createRequest(`Bearer ${jwt}`);
179
+ const result = await authenticateRequest(authConfig, request);
180
+
181
+ expect(result).toBeNull();
182
+ });
183
+
184
+ it("returns null for token with missing issuer", async () => {
185
+ const jwt = await new SignJWT({ email: "user@example.com" })
186
+ .setProtectedHeader({ alg: "RS256" })
187
+ .setSubject("user-123")
188
+ // No issuer set
189
+ .setAudience(authConfig.audience)
190
+ .setExpirationTime("1h")
191
+ .setIssuedAt()
192
+ .sign(keyPair.privateKey);
193
+
194
+ const request = createRequest(`Bearer ${jwt}`);
195
+ const result = await authenticateRequest(authConfig, request);
196
+
197
+ expect(result).toBeNull();
198
+ });
199
+ });
200
+
201
+ describe("audience validation", () => {
202
+ it("returns null for token with wrong audience", async () => {
203
+ const jwt = await new SignJWT({ email: "user@example.com" })
204
+ .setProtectedHeader({ alg: "RS256" })
205
+ .setSubject("user-123")
206
+ .setIssuer(authConfig.issuer)
207
+ .setAudience("wrong-app") // Wrong audience
208
+ .setExpirationTime("1h")
209
+ .setIssuedAt()
210
+ .sign(keyPair.privateKey);
211
+
212
+ const request = createRequest(`Bearer ${jwt}`);
213
+ const result = await authenticateRequest(authConfig, request);
214
+
215
+ expect(result).toBeNull();
216
+ });
217
+
218
+ it("returns null for token with missing audience", async () => {
219
+ const jwt = await new SignJWT({ email: "user@example.com" })
220
+ .setProtectedHeader({ alg: "RS256" })
221
+ .setSubject("user-123")
222
+ .setIssuer(authConfig.issuer)
223
+ // No audience set
224
+ .setExpirationTime("1h")
225
+ .setIssuedAt()
226
+ .sign(keyPair.privateKey);
227
+
228
+ const request = createRequest(`Bearer ${jwt}`);
229
+ const result = await authenticateRequest(authConfig, request);
230
+
231
+ expect(result).toBeNull();
232
+ });
233
+ });
234
+
235
+ describe("signature validation", () => {
236
+ it("returns null for token signed with different key", async () => {
237
+ // Generate a different key pair
238
+ const differentKeyPair = await generateKeyPair("RS256");
239
+
240
+ const jwt = await new SignJWT({ email: "user@example.com" })
241
+ .setProtectedHeader({ alg: "RS256" })
242
+ .setSubject("user-123")
243
+ .setIssuer(authConfig.issuer)
244
+ .setAudience(authConfig.audience)
245
+ .setExpirationTime("1h")
246
+ .setIssuedAt()
247
+ .sign(differentKeyPair.privateKey); // Different key!
248
+
249
+ const request = createRequest(`Bearer ${jwt}`);
250
+ const result = await authenticateRequest(authConfig, request);
251
+
252
+ expect(result).toBeNull();
253
+ });
254
+
255
+ it("returns null for tampered token payload", async () => {
256
+ const token = await createValidToken();
257
+ // Tamper with the payload by modifying the base64
258
+ const [header, _payload, signature] = token.split(".");
259
+ const tamperedPayload = btoa(
260
+ JSON.stringify({ sub: "attacker-id", email: "attacker@evil.com" }),
261
+ )
262
+ .replace(/=/g, "")
263
+ .replace(/\+/g, "-")
264
+ .replace(/\//g, "_");
265
+ const tamperedToken = `${header}.${tamperedPayload}.${signature}`;
266
+
267
+ const request = createRequest(`Bearer ${tamperedToken}`);
268
+ const result = await authenticateRequest(authConfig, request);
269
+
270
+ expect(result).toBeNull();
271
+ });
272
+
273
+ it("returns null for malformed JWT structure", async () => {
274
+ const malformedTokens = [
275
+ "not.a.valid.jwt.structure",
276
+ "only-one-part",
277
+ "two.parts",
278
+ "",
279
+ "header.payload.", // Missing signature
280
+ ".payload.signature", // Missing header
281
+ ];
282
+
283
+ for (const token of malformedTokens) {
284
+ const request = createRequest(`Bearer ${token}`);
285
+ const result = await authenticateRequest(authConfig, request);
286
+ expect(result).toBeNull();
287
+ }
288
+ });
289
+ });
290
+
291
+ describe("algorithm restrictions", () => {
292
+ it("returns null for HS256-signed token against RSA JWKS", async () => {
293
+ // Create HS256 token using a symmetric secret
294
+ const secret = new TextEncoder().encode("super-secret-key");
295
+ const jwt = await new SignJWT({ email: "user@example.com" })
296
+ .setProtectedHeader({ alg: "HS256" })
297
+ .setSubject("user-123")
298
+ .setIssuer(authConfig.issuer)
299
+ .setAudience(authConfig.audience)
300
+ .setExpirationTime("1h")
301
+ .setIssuedAt()
302
+ .sign(secret);
303
+
304
+ const request = createRequest(`Bearer ${jwt}`);
305
+ const result = await authenticateRequest(authConfig, request);
306
+
307
+ expect(result).toBeNull();
308
+ });
309
+
310
+ it("returns null for unsecured 'none' algorithm token", async () => {
311
+ // Craft a JWT with alg: none and no signature
312
+ const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" }))
313
+ .toString("base64")
314
+ .replace(/=/g, "")
315
+ .replace(/\+/g, "-")
316
+ .replace(/\//g, "_");
317
+
318
+ const payload = Buffer.from(
319
+ JSON.stringify({
320
+ sub: "user-123",
321
+ iss: authConfig.issuer,
322
+ aud: authConfig.audience,
323
+ iat: Math.floor(Date.now() / 1000),
324
+ exp: Math.floor(Date.now() / 1000) + 3600,
325
+ email: "user@example.com",
326
+ }),
327
+ )
328
+ .toString("base64")
329
+ .replace(/=/g, "")
330
+ .replace(/\+/g, "-")
331
+ .replace(/\//g, "_");
332
+
333
+ // Note the trailing dot for empty signature
334
+ const unsecuredToken = `${header}.${payload}.`;
335
+
336
+ const request = createRequest(`Bearer ${unsecuredToken}`);
337
+ const result = await authenticateRequest(authConfig, request);
338
+
339
+ expect(result).toBeNull();
340
+ });
341
+ });
342
+
343
+ describe("JWKS fetch failures", () => {
344
+ it("returns null when JWKS endpoint returns 404", async () => {
345
+ global.fetch = vi
346
+ .fn()
347
+ .mockResolvedValue(new Response("Not Found", { status: 404 }));
348
+
349
+ const token = await createValidToken();
350
+ const request = createRequest(`Bearer ${token}`);
351
+ const result = await authenticateRequest(authConfig, request);
352
+
353
+ expect(result).toBeNull();
354
+ });
355
+
356
+ it("returns null when JWKS endpoint returns 500", async () => {
357
+ global.fetch = vi
358
+ .fn()
359
+ .mockResolvedValue(
360
+ new Response("Internal Server Error", { status: 500 }),
361
+ );
362
+
363
+ const token = await createValidToken();
364
+ const request = createRequest(`Bearer ${token}`);
365
+ const result = await authenticateRequest(authConfig, request);
366
+
367
+ expect(result).toBeNull();
368
+ });
369
+
370
+ it("returns null when JWKS fetch throws network error", async () => {
371
+ global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
372
+
373
+ const token = await createValidToken();
374
+ const request = createRequest(`Bearer ${token}`);
375
+ const result = await authenticateRequest(authConfig, request);
376
+
377
+ expect(result).toBeNull();
378
+ });
379
+
380
+ it("returns null when JWKS returns invalid JSON", async () => {
381
+ global.fetch = vi.fn().mockResolvedValue(
382
+ new Response("not valid json", {
383
+ status: 200,
384
+ headers: { "Content-Type": "application/json" },
385
+ }),
386
+ );
387
+
388
+ const token = await createValidToken();
389
+ const request = createRequest(`Bearer ${token}`);
390
+ const result = await authenticateRequest(authConfig, request);
391
+
392
+ expect(result).toBeNull();
393
+ });
394
+
395
+ it("returns null when JWKS has no matching key", async () => {
396
+ // Return JWKS with a different key
397
+ const differentKeyPair = await generateKeyPair("RS256");
398
+ const differentJwk = await exportJWK(differentKeyPair.publicKey);
399
+
400
+ global.fetch = vi.fn().mockResolvedValue(
401
+ new Response(
402
+ JSON.stringify({
403
+ keys: [{ ...differentJwk, kid: "different-key", alg: "RS256" }],
404
+ }),
405
+ { status: 200, headers: { "Content-Type": "application/json" } },
406
+ ),
407
+ );
408
+
409
+ const token = await createValidToken();
410
+ const request = createRequest(`Bearer ${token}`);
411
+ const result = await authenticateRequest(authConfig, request);
412
+
413
+ expect(result).toBeNull();
414
+ });
415
+ });
416
+ });
@@ -41,7 +41,6 @@ export async function authenticateRequest(
41
41
  try {
42
42
  const session = await verifySessionToken(token, authConfig);
43
43
  return session;
44
- // TODO Is there a way to handle this more gracefully?
45
44
  } catch (error) {
46
45
  console.error(
47
46
  JSON.stringify({
@@ -49,7 +48,8 @@ export async function authenticateRequest(
49
48
  error: error instanceof Error ? error.message : String(error),
50
49
  stack: error instanceof Error ? error.stack : undefined,
51
50
  errorType: error instanceof Error ? error.constructor.name : "Unknown",
52
- authConfig,
51
+ issuer: authConfig.issuer,
52
+ audience: authConfig.audience,
53
53
  }),
54
54
  );
55
55
  return null;
@@ -70,9 +70,7 @@ async function verifySessionToken(
70
70
  throw new Error("Audience must be provided for token verification");
71
71
  }
72
72
 
73
- // TODO Maybe we don't even need this if we just store the jwks as an env when we deploy
74
- // But, the limitation of these services not being able to talk to each other will be frustrating.
75
- // I wonder if there is a better abstraction to wrap this dynamic fetching and link all the services together.
73
+ // Fetch JWKS - use service binding in production, direct fetch in development
76
74
  const jwksResponse =
77
75
  import.meta.env.PROD && env.EVERY_APP_GATEWAY
78
76
  ? await env.EVERY_APP_GATEWAY.fetch("http://localhost/api/embedded/jwks")
@@ -90,13 +88,20 @@ async function verifySessionToken(
90
88
  const options: JWTVerifyOptions = {
91
89
  issuer,
92
90
  audience,
91
+ algorithms: ["RS256"],
93
92
  };
94
93
 
95
94
  const { payload } = await jwtVerify(token, localJWKS, options);
96
95
  return payload as SessionTokenPayload;
97
96
  }
98
97
 
99
- function extractBearerToken(authHeader: string | null): string | null {
98
+ /**
99
+ * Extracts the bearer token from an Authorization header.
100
+ *
101
+ * @param authHeader - The Authorization header value (e.g., "Bearer eyJ...")
102
+ * @returns The token string if valid, null otherwise
103
+ */
104
+ export function extractBearerToken(authHeader: string | null): string | null {
100
105
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
101
106
  return null;
102
107
  }
@@ -3,6 +3,12 @@ import path from "path";
3
3
  import { execSync } from "child_process";
4
4
  import { parse } from "jsonc-parser";
5
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
+
6
12
  export function getLocalD1Url() {
7
13
  const basePath = path.resolve(".wrangler");
8
14
 
@@ -22,9 +28,7 @@ export function getLocalD1Url() {
22
28
  return null;
23
29
  }
24
30
 
25
- const dbFile = fs
26
- .readdirSync(basePath, { encoding: "utf-8", recursive: true })
27
- .find((f) => f.endsWith(".sqlite"));
31
+ let dbFile = findSqliteFile(basePath);
28
32
 
29
33
  if (!dbFile) {
30
34
  // Read wrangler.jsonc to get the database name
@@ -47,20 +51,14 @@ export function getLocalD1Url() {
47
51
  );
48
52
 
49
53
  // Try to find the db file again after initialization
50
- const dbFileAfterInit = fs
51
- .readdirSync(basePath, { encoding: "utf-8", recursive: true })
52
- .find((f) => f.endsWith(".sqlite"));
54
+ dbFile = findSqliteFile(basePath);
53
55
 
54
- if (!dbFileAfterInit) {
56
+ if (!dbFile) {
55
57
  throw new Error(
56
58
  `Failed to initialize local D1 database. The sqlite file was not created.`,
57
59
  );
58
60
  }
59
-
60
- const url = path.resolve(basePath, dbFileAfterInit);
61
- return url;
62
61
  }
63
62
 
64
- const url = path.resolve(basePath, dbFile);
65
- return url;
63
+ return path.resolve(basePath, dbFile);
66
64
  }
@@ -1,8 +1,4 @@
1
1
  export interface AuthConfig {
2
- jwksUrl: string;
3
2
  issuer: string;
4
3
  audience: string;
5
- autoDiscoverJwks?: boolean;
6
- debug?: boolean;
7
- onError?: (error: Error, req: unknown) => void;
8
4
  }