@codemation/managed-auth 0.1.0 → 0.1.1

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.
@@ -1,19 +1,19 @@
1
1
 
2
- > @codemation/managed-auth@0.1.0 build C:\Users\ChrisBlokland\projects\codemation\framework\packages\managed-auth
2
+ > @codemation/managed-auth@0.1.1 build /home/runner/work/codemation/codemation/packages/managed-auth
3
3
  > tsdown
4
4
 
5
- tsdown v0.15.12 powered by rolldown v1.0.0-beta.45
6
- Using tsdown config: C:\Users\ChrisBlokland\projects\codemation\framework\packages\managed-auth\tsdown.config.ts
7
- entry: src\index.ts
8
- tsconfig: tsconfig.json
9
- Build start
10
- [CJS] dist\index.cjs 5.49 kB │ gzip: 2.15 kB
11
- [CJS] dist\index.cjs.map 8.47 kB │ gzip: 2.96 kB
12
- [CJS] 2 files, total: 13.96 kB
13
- [ESM] dist\index.js 4.45 kB │ gzip: 1.72 kB
14
- [ESM] dist\index.js.map 8.46 kB │ gzip: 2.96 kB
15
- [ESM] dist\index.d.ts 3.27 kB │ gzip: 1.34 kB
16
- [ESM] 3 files, total: 16.17 kB
17
- [CJS] dist\index.d.cts 3.27 kB │ gzip: 1.34 kB
18
- [CJS] 1 files, total: 3.27 kB
19
- Build complete in 3258ms
5
+ ℹ tsdown v0.15.12 powered by rolldown v1.0.0-beta.45
6
+ ℹ Using tsdown config: /home/runner/work/codemation/codemation/packages/managed-auth/tsdown.config.ts
7
+ ℹ entry: src/index.ts
8
+ ℹ tsconfig: tsconfig.json
9
+ ℹ Build start
10
+ ℹ [CJS] dist/index.cjs 4.82 kB │ gzip: 1.80 kB
11
+ ℹ [CJS] dist/index.cjs.map 7.51 kB │ gzip: 2.51 kB
12
+ ℹ [CJS] 2 files, total: 12.33 kB
13
+ ℹ [ESM] dist/index.js 3.78 kB │ gzip: 1.37 kB
14
+ ℹ [ESM] dist/index.js.map 7.50 kB │ gzip: 2.50 kB
15
+ ℹ [ESM] dist/index.d.ts 1.80 kB │ gzip: 0.66 kB
16
+ ℹ [ESM] 3 files, total: 13.08 kB
17
+ ℹ [CJS] dist/index.d.cts 1.80 kB │ gzip: 0.66 kB
18
+ ℹ [CJS] 1 files, total: 1.80 kB
19
+ ✔ Build complete in 4994ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @codemation/managed-auth
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#247](https://github.com/MadeRelevant/codemation/pull/247) [`bfdd759`](https://github.com/MadeRelevant/codemation/commit/bfdd7590b4903676b223c2f302b9bcd0f4a4583c) Thanks [@cblokland90](https://github.com/cblokland90)! - Remove all human-written comments from TypeScript source files and add `codemation/no-comments` ESLint rule to enforce self-describing code going forward.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -27,15 +27,6 @@ jose = __toESM(jose);
27
27
  //#region src/JwksCache.ts
28
28
  const DEFAULT_TTL_MS = 900 * 1e3;
29
29
  const DEFAULT_JITTER_MS = 60 * 1e3;
30
- /**
31
- * Fetches and caches JWKS from a configured URL.
32
- *
33
- * On a `kid` cache miss the cache is refreshed once before failing —
34
- * this handles key rotation without requiring a restart.
35
- *
36
- * Refresh-window jitter avoids thundering-herd stampedes when multiple
37
- * workers share the same process lifetime.
38
- */
39
30
  var JwksCache = class {
40
31
  cache = null;
41
32
  constructor(config, fetch, clock) {
@@ -43,7 +34,6 @@ var JwksCache = class {
43
34
  this.fetch = fetch;
44
35
  this.clock = clock;
45
36
  }
46
- /** Returns the cached key for `kid`, fetching if needed. Refreshes once on kid miss. */
47
37
  async getKey(kid) {
48
38
  let entry = await this.getValidEntry();
49
39
  const cached = entry.keys.get(kid);
@@ -80,14 +70,6 @@ var JwksCache = class {
80
70
 
81
71
  //#endregion
82
72
  //#region src/ManagedJwtVerifier.ts
83
- /**
84
- * Validates CP-signed EdDSA JWT bearers.
85
- *
86
- * Checks: signature, `iss` matches expected, `aud` matches expected workspaceId,
87
- * `exp`, `nbf`. Returns a `VerifiedManagedPrincipal` or a structured `JwtVerificationFailure`.
88
- *
89
- * Hono-independent — no web framework coupling.
90
- */
91
73
  var ManagedJwtVerifier = class {
92
74
  constructor(config, jwksCache) {
93
75
  this.config = config;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["config: JwksCacheConfig","fetch: FetchFn","clock: Clock","entry: CacheEntry","config: ManagedJwtVerifierConfig","jwksCache: JwksCache","kid: string | undefined","joseErrors"],"sources":["../src/JwksCache.ts","../src/ManagedJwtVerifier.ts"],"sourcesContent":["import { importJWK } from \"jose\";\nimport type { Clock, FetchFn, JwksCacheConfig } from \"./types\";\n\nconst DEFAULT_TTL_MS = 15 * 60 * 1000; // 15 minutes\nconst DEFAULT_JITTER_MS = 60 * 1000; // 60 seconds\n\ntype JoseKey = Awaited<ReturnType<typeof importJWK>>;\n\ninterface CacheEntry {\n keys: Map<string, JoseKey>;\n expiresAt: number;\n}\n\ninterface JwkKey {\n kid?: string;\n kty?: string;\n crv?: string;\n x?: string;\n use?: string;\n alg?: string;\n}\n\ninterface JwksResponse {\n keys: JwkKey[];\n}\n\n/**\n * Fetches and caches JWKS from a configured URL.\n *\n * On a `kid` cache miss the cache is refreshed once before failing —\n * this handles key rotation without requiring a restart.\n *\n * Refresh-window jitter avoids thundering-herd stampedes when multiple\n * workers share the same process lifetime.\n */\nexport class JwksCache {\n private cache: CacheEntry | null = null;\n\n constructor(\n private readonly config: JwksCacheConfig,\n private readonly fetch: FetchFn,\n private readonly clock: Clock,\n ) {}\n\n /** Returns the cached key for `kid`, fetching if needed. Refreshes once on kid miss. */\n async getKey(kid: string): Promise<JoseKey | null> {\n let entry = await this.getValidEntry();\n const cached = entry.keys.get(kid);\n if (cached !== undefined) {\n return cached;\n }\n\n // kid miss — refresh once to handle key rotation\n entry = await this.refresh();\n return entry.keys.get(kid) ?? null;\n }\n\n private async getValidEntry(): Promise<CacheEntry> {\n if (this.cache !== null && this.clock.now() < this.cache.expiresAt) {\n return this.cache;\n }\n return this.refresh();\n }\n\n private async refresh(): Promise<CacheEntry> {\n const response = await this.fetch(this.config.jwksUrl);\n if (!response.ok) {\n throw new Error(`JWKS fetch failed for ${this.config.jwksUrl}`);\n }\n const body = (await response.json()) as JwksResponse;\n const keys = new Map<string, JoseKey>();\n\n for (const jwk of body.keys ?? []) {\n if (!jwk.kid) continue;\n try {\n const key = await importJWK(jwk as Parameters<typeof importJWK>[0]);\n keys.set(jwk.kid, key);\n } catch {\n // Skip malformed individual keys — don't fail the whole cache refresh\n }\n }\n\n const ttl = this.config.ttlMs ?? DEFAULT_TTL_MS;\n const jitter = Math.random() * (this.config.jitterMs ?? DEFAULT_JITTER_MS);\n const entry: CacheEntry = {\n keys,\n expiresAt: this.clock.now() + ttl + jitter,\n };\n this.cache = entry;\n return entry;\n }\n}\n","import { decodeProtectedHeader, jwtVerify, errors as joseErrors } from \"jose\";\nimport type { JwtVerificationFailure, ManagedJwtVerifierConfig, VerifiedManagedPrincipal } from \"./types\";\nimport type { JwksCache } from \"./JwksCache\";\n\ntype VerifyResult = VerifiedManagedPrincipal | JwtVerificationFailure;\n\n/**\n * Validates CP-signed EdDSA JWT bearers.\n *\n * Checks: signature, `iss` matches expected, `aud` matches expected workspaceId,\n * `exp`, `nbf`. Returns a `VerifiedManagedPrincipal` or a structured `JwtVerificationFailure`.\n *\n * Hono-independent — no web framework coupling.\n */\nexport class ManagedJwtVerifier {\n constructor(\n private readonly config: ManagedJwtVerifierConfig,\n private readonly jwksCache: JwksCache,\n ) {}\n\n async verify(token: string): Promise<VerifyResult> {\n // Decode header to get kid before verifying signature\n let kid: string | undefined;\n try {\n const header = decodeProtectedHeader(token);\n kid = typeof header.kid === \"string\" ? header.kid : undefined;\n } catch {\n return { failure: \"malformed\", message: \"Unable to decode JWT header.\" };\n }\n\n if (!kid) {\n return { failure: \"missing-kid\", message: \"JWT header is missing the `kid` field.\" };\n }\n\n const key = await this.jwksCache.getKey(kid);\n if (key === null) {\n return { failure: \"unknown-kid\", message: `No key found for kid \"${kid}\" after JWKS refresh.` };\n }\n\n try {\n const { payload } = await jwtVerify(token, key, {\n issuer: this.config.expectedIssuer,\n audience: this.config.expectedAudience,\n algorithms: [\"EdDSA\"],\n });\n\n const sub = payload.sub;\n const aud = payload.aud;\n const workspaceId = Array.isArray(aud) ? aud[0] : aud;\n\n if (!sub || typeof sub !== \"string\") {\n return { failure: \"malformed\", message: \"JWT is missing the `sub` claim.\" };\n }\n if (!workspaceId || typeof workspaceId !== \"string\") {\n return { failure: \"wrong-aud\", message: \"JWT `aud` claim is absent or not a string.\" };\n }\n\n return { userId: sub, workspaceId };\n } catch (error) {\n if (error instanceof joseErrors.JWTExpired) {\n return { failure: \"expired\", message: \"JWT has expired.\" };\n }\n if (error instanceof joseErrors.JWTClaimValidationFailed) {\n const claim = error.claim;\n if (claim === \"iss\") {\n return { failure: \"wrong-iss\", message: \"JWT `iss` claim does not match expected issuer.\" };\n }\n if (claim === \"aud\") {\n return { failure: \"wrong-aud\", message: \"JWT `aud` claim does not match expected audience.\" };\n }\n if (claim === \"nbf\") {\n return { failure: \"not-yet-valid\", message: \"JWT is not yet valid (nbf check failed).\" };\n }\n return { failure: \"malformed\", message: `JWT claim validation failed: ${error.message}` };\n }\n if (error instanceof joseErrors.JWSSignatureVerificationFailed || error instanceof joseErrors.JWSInvalid) {\n return { failure: \"bad-signature\", message: \"JWT signature verification failed.\" };\n }\n return { failure: \"malformed\", message: \"JWT verification failed.\" };\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["config: JwksCacheConfig","fetch: FetchFn","clock: Clock","entry: CacheEntry","config: ManagedJwtVerifierConfig","jwksCache: JwksCache","kid: string | undefined","joseErrors"],"sources":["../src/JwksCache.ts","../src/ManagedJwtVerifier.ts"],"sourcesContent":["import { importJWK } from \"jose\";\nimport type { Clock, FetchFn, JwksCacheConfig } from \"./types\";\n\nconst DEFAULT_TTL_MS = 15 * 60 * 1000;\nconst DEFAULT_JITTER_MS = 60 * 1000;\n\ntype JoseKey = Awaited<ReturnType<typeof importJWK>>;\n\ninterface CacheEntry {\n keys: Map<string, JoseKey>;\n expiresAt: number;\n}\n\ninterface JwkKey {\n kid?: string;\n kty?: string;\n crv?: string;\n x?: string;\n use?: string;\n alg?: string;\n}\n\ninterface JwksResponse {\n keys: JwkKey[];\n}\n\nexport class JwksCache {\n private cache: CacheEntry | null = null;\n\n constructor(\n private readonly config: JwksCacheConfig,\n private readonly fetch: FetchFn,\n private readonly clock: Clock,\n ) {}\n\n async getKey(kid: string): Promise<JoseKey | null> {\n let entry = await this.getValidEntry();\n const cached = entry.keys.get(kid);\n if (cached !== undefined) {\n return cached;\n }\n\n entry = await this.refresh();\n return entry.keys.get(kid) ?? null;\n }\n\n private async getValidEntry(): Promise<CacheEntry> {\n if (this.cache !== null && this.clock.now() < this.cache.expiresAt) {\n return this.cache;\n }\n return this.refresh();\n }\n\n private async refresh(): Promise<CacheEntry> {\n const response = await this.fetch(this.config.jwksUrl);\n if (!response.ok) {\n throw new Error(`JWKS fetch failed for ${this.config.jwksUrl}`);\n }\n const body = (await response.json()) as JwksResponse;\n const keys = new Map<string, JoseKey>();\n\n for (const jwk of body.keys ?? []) {\n if (!jwk.kid) continue;\n try {\n const key = await importJWK(jwk as Parameters<typeof importJWK>[0]);\n keys.set(jwk.kid, key);\n } catch {}\n }\n\n const ttl = this.config.ttlMs ?? DEFAULT_TTL_MS;\n const jitter = Math.random() * (this.config.jitterMs ?? DEFAULT_JITTER_MS);\n const entry: CacheEntry = {\n keys,\n expiresAt: this.clock.now() + ttl + jitter,\n };\n this.cache = entry;\n return entry;\n }\n}\n","import { decodeProtectedHeader, jwtVerify, errors as joseErrors } from \"jose\";\nimport type { JwtVerificationFailure, ManagedJwtVerifierConfig, VerifiedManagedPrincipal } from \"./types\";\nimport type { JwksCache } from \"./JwksCache\";\n\ntype VerifyResult = VerifiedManagedPrincipal | JwtVerificationFailure;\n\nexport class ManagedJwtVerifier {\n constructor(\n private readonly config: ManagedJwtVerifierConfig,\n private readonly jwksCache: JwksCache,\n ) {}\n\n async verify(token: string): Promise<VerifyResult> {\n let kid: string | undefined;\n try {\n const header = decodeProtectedHeader(token);\n kid = typeof header.kid === \"string\" ? header.kid : undefined;\n } catch {\n return { failure: \"malformed\", message: \"Unable to decode JWT header.\" };\n }\n\n if (!kid) {\n return { failure: \"missing-kid\", message: \"JWT header is missing the `kid` field.\" };\n }\n\n const key = await this.jwksCache.getKey(kid);\n if (key === null) {\n return { failure: \"unknown-kid\", message: `No key found for kid \"${kid}\" after JWKS refresh.` };\n }\n\n try {\n const { payload } = await jwtVerify(token, key, {\n issuer: this.config.expectedIssuer,\n audience: this.config.expectedAudience,\n algorithms: [\"EdDSA\"],\n });\n\n const sub = payload.sub;\n const aud = payload.aud;\n const workspaceId = Array.isArray(aud) ? aud[0] : aud;\n\n if (!sub || typeof sub !== \"string\") {\n return { failure: \"malformed\", message: \"JWT is missing the `sub` claim.\" };\n }\n if (!workspaceId || typeof workspaceId !== \"string\") {\n return { failure: \"wrong-aud\", message: \"JWT `aud` claim is absent or not a string.\" };\n }\n\n return { userId: sub, workspaceId };\n } catch (error) {\n if (error instanceof joseErrors.JWTExpired) {\n return { failure: \"expired\", message: \"JWT has expired.\" };\n }\n if (error instanceof joseErrors.JWTClaimValidationFailed) {\n const claim = error.claim;\n if (claim === \"iss\") {\n return { failure: \"wrong-iss\", message: \"JWT `iss` claim does not match expected issuer.\" };\n }\n if (claim === \"aud\") {\n return { failure: \"wrong-aud\", message: \"JWT `aud` claim does not match expected audience.\" };\n }\n if (claim === \"nbf\") {\n return { failure: \"not-yet-valid\", message: \"JWT is not yet valid (nbf check failed).\" };\n }\n return { failure: \"malformed\", message: `JWT claim validation failed: ${error.message}` };\n }\n if (error instanceof joseErrors.JWSSignatureVerificationFailed || error instanceof joseErrors.JWSInvalid) {\n return { failure: \"bad-signature\", message: \"JWT signature verification failed.\" };\n }\n return { failure: \"malformed\", message: \"JWT verification failed.\" };\n }\n }\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,64 +1,41 @@
1
1
  import { importJWK } from "jose";
2
2
 
3
3
  //#region src/types.d.ts
4
-
5
- /**
6
- * A successfully verified CP-signed JWT principal.
7
- * `userId` maps to the JWT `sub` claim; `workspaceId` maps to `aud`.
8
- */
9
4
  interface VerifiedManagedPrincipal {
10
5
  readonly userId: string;
11
6
  readonly workspaceId: string;
12
7
  }
13
8
  type JwtVerificationFailureReason = "missing-kid" | "unknown-kid" | "bad-signature" | "wrong-iss" | "wrong-aud" | "expired" | "not-yet-valid" | "malformed";
14
- /** Structured failure returned instead of throwing, so callers can map to HTTP status codes cleanly. */
15
9
  interface JwtVerificationFailure {
16
10
  readonly failure: JwtVerificationFailureReason;
17
11
  readonly message: string;
18
12
  }
19
13
  interface JwksCacheConfig {
20
- /** URL to the JWKS endpoint (e.g. https://cp.example.com/.well-known/jwks.json). */
21
14
  readonly jwksUrl: string;
22
- /** TTL in milliseconds before the cache is considered stale. Defaults to 15 minutes. */
23
15
  readonly ttlMs?: number;
24
- /** Jitter window in milliseconds applied to TTL to avoid stampedes. Defaults to 60 seconds. */
25
16
  readonly jitterMs?: number;
26
17
  }
27
18
  interface ManagedJwtVerifierConfig {
28
- /** Expected value of the JWT `iss` claim — must exactly match. */
29
19
  readonly expectedIssuer: string;
30
- /** Expected `aud` claim — must exactly match the workspace ID. */
31
20
  readonly expectedAudience: string;
32
21
  readonly jwksCache: JwksCacheConfig;
33
22
  }
34
- /** Minimal fetch-compatible function type for injection / testing. */
35
23
  type FetchFn = (url: string) => Promise<{
36
24
  ok: boolean;
37
25
  json(): Promise<unknown>;
38
26
  }>;
39
- /** Minimal clock interface for injection / testing. */
40
27
  interface Clock {
41
28
  now(): number;
42
29
  }
43
30
  //#endregion
44
31
  //#region src/JwksCache.d.ts
45
32
  type JoseKey = Awaited<ReturnType<typeof importJWK>>;
46
- /**
47
- * Fetches and caches JWKS from a configured URL.
48
- *
49
- * On a `kid` cache miss the cache is refreshed once before failing —
50
- * this handles key rotation without requiring a restart.
51
- *
52
- * Refresh-window jitter avoids thundering-herd stampedes when multiple
53
- * workers share the same process lifetime.
54
- */
55
33
  declare class JwksCache {
56
34
  private readonly config;
57
35
  private readonly fetch;
58
36
  private readonly clock;
59
37
  private cache;
60
38
  constructor(config: JwksCacheConfig, fetch: FetchFn, clock: Clock);
61
- /** Returns the cached key for `kid`, fetching if needed. Refreshes once on kid miss. */
62
39
  getKey(kid: string): Promise<JoseKey | null>;
63
40
  private getValidEntry;
64
41
  private refresh;
@@ -66,14 +43,6 @@ declare class JwksCache {
66
43
  //#endregion
67
44
  //#region src/ManagedJwtVerifier.d.ts
68
45
  type VerifyResult = VerifiedManagedPrincipal | JwtVerificationFailure;
69
- /**
70
- * Validates CP-signed EdDSA JWT bearers.
71
- *
72
- * Checks: signature, `iss` matches expected, `aud` matches expected workspaceId,
73
- * `exp`, `nbf`. Returns a `VerifiedManagedPrincipal` or a structured `JwtVerificationFailure`.
74
- *
75
- * Hono-independent — no web framework coupling.
76
- */
77
46
  declare class ManagedJwtVerifier {
78
47
  private readonly config;
79
48
  private readonly jwksCache;
package/dist/index.d.ts CHANGED
@@ -1,64 +1,41 @@
1
1
  import { importJWK } from "jose";
2
2
 
3
3
  //#region src/types.d.ts
4
-
5
- /**
6
- * A successfully verified CP-signed JWT principal.
7
- * `userId` maps to the JWT `sub` claim; `workspaceId` maps to `aud`.
8
- */
9
4
  interface VerifiedManagedPrincipal {
10
5
  readonly userId: string;
11
6
  readonly workspaceId: string;
12
7
  }
13
8
  type JwtVerificationFailureReason = "missing-kid" | "unknown-kid" | "bad-signature" | "wrong-iss" | "wrong-aud" | "expired" | "not-yet-valid" | "malformed";
14
- /** Structured failure returned instead of throwing, so callers can map to HTTP status codes cleanly. */
15
9
  interface JwtVerificationFailure {
16
10
  readonly failure: JwtVerificationFailureReason;
17
11
  readonly message: string;
18
12
  }
19
13
  interface JwksCacheConfig {
20
- /** URL to the JWKS endpoint (e.g. https://cp.example.com/.well-known/jwks.json). */
21
14
  readonly jwksUrl: string;
22
- /** TTL in milliseconds before the cache is considered stale. Defaults to 15 minutes. */
23
15
  readonly ttlMs?: number;
24
- /** Jitter window in milliseconds applied to TTL to avoid stampedes. Defaults to 60 seconds. */
25
16
  readonly jitterMs?: number;
26
17
  }
27
18
  interface ManagedJwtVerifierConfig {
28
- /** Expected value of the JWT `iss` claim — must exactly match. */
29
19
  readonly expectedIssuer: string;
30
- /** Expected `aud` claim — must exactly match the workspace ID. */
31
20
  readonly expectedAudience: string;
32
21
  readonly jwksCache: JwksCacheConfig;
33
22
  }
34
- /** Minimal fetch-compatible function type for injection / testing. */
35
23
  type FetchFn = (url: string) => Promise<{
36
24
  ok: boolean;
37
25
  json(): Promise<unknown>;
38
26
  }>;
39
- /** Minimal clock interface for injection / testing. */
40
27
  interface Clock {
41
28
  now(): number;
42
29
  }
43
30
  //#endregion
44
31
  //#region src/JwksCache.d.ts
45
32
  type JoseKey = Awaited<ReturnType<typeof importJWK>>;
46
- /**
47
- * Fetches and caches JWKS from a configured URL.
48
- *
49
- * On a `kid` cache miss the cache is refreshed once before failing —
50
- * this handles key rotation without requiring a restart.
51
- *
52
- * Refresh-window jitter avoids thundering-herd stampedes when multiple
53
- * workers share the same process lifetime.
54
- */
55
33
  declare class JwksCache {
56
34
  private readonly config;
57
35
  private readonly fetch;
58
36
  private readonly clock;
59
37
  private cache;
60
38
  constructor(config: JwksCacheConfig, fetch: FetchFn, clock: Clock);
61
- /** Returns the cached key for `kid`, fetching if needed. Refreshes once on kid miss. */
62
39
  getKey(kid: string): Promise<JoseKey | null>;
63
40
  private getValidEntry;
64
41
  private refresh;
@@ -66,14 +43,6 @@ declare class JwksCache {
66
43
  //#endregion
67
44
  //#region src/ManagedJwtVerifier.d.ts
68
45
  type VerifyResult = VerifiedManagedPrincipal | JwtVerificationFailure;
69
- /**
70
- * Validates CP-signed EdDSA JWT bearers.
71
- *
72
- * Checks: signature, `iss` matches expected, `aud` matches expected workspaceId,
73
- * `exp`, `nbf`. Returns a `VerifiedManagedPrincipal` or a structured `JwtVerificationFailure`.
74
- *
75
- * Hono-independent — no web framework coupling.
76
- */
77
46
  declare class ManagedJwtVerifier {
78
47
  private readonly config;
79
48
  private readonly jwksCache;
package/dist/index.js CHANGED
@@ -3,15 +3,6 @@ import { decodeProtectedHeader, errors, importJWK, jwtVerify } from "jose";
3
3
  //#region src/JwksCache.ts
4
4
  const DEFAULT_TTL_MS = 900 * 1e3;
5
5
  const DEFAULT_JITTER_MS = 60 * 1e3;
6
- /**
7
- * Fetches and caches JWKS from a configured URL.
8
- *
9
- * On a `kid` cache miss the cache is refreshed once before failing —
10
- * this handles key rotation without requiring a restart.
11
- *
12
- * Refresh-window jitter avoids thundering-herd stampedes when multiple
13
- * workers share the same process lifetime.
14
- */
15
6
  var JwksCache = class {
16
7
  cache = null;
17
8
  constructor(config, fetch, clock) {
@@ -19,7 +10,6 @@ var JwksCache = class {
19
10
  this.fetch = fetch;
20
11
  this.clock = clock;
21
12
  }
22
- /** Returns the cached key for `kid`, fetching if needed. Refreshes once on kid miss. */
23
13
  async getKey(kid) {
24
14
  let entry = await this.getValidEntry();
25
15
  const cached = entry.keys.get(kid);
@@ -56,14 +46,6 @@ var JwksCache = class {
56
46
 
57
47
  //#endregion
58
48
  //#region src/ManagedJwtVerifier.ts
59
- /**
60
- * Validates CP-signed EdDSA JWT bearers.
61
- *
62
- * Checks: signature, `iss` matches expected, `aud` matches expected workspaceId,
63
- * `exp`, `nbf`. Returns a `VerifiedManagedPrincipal` or a structured `JwtVerificationFailure`.
64
- *
65
- * Hono-independent — no web framework coupling.
66
- */
67
49
  var ManagedJwtVerifier = class {
68
50
  constructor(config, jwksCache) {
69
51
  this.config = config;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["config: JwksCacheConfig","fetch: FetchFn","clock: Clock","entry: CacheEntry","config: ManagedJwtVerifierConfig","jwksCache: JwksCache","kid: string | undefined","joseErrors"],"sources":["../src/JwksCache.ts","../src/ManagedJwtVerifier.ts"],"sourcesContent":["import { importJWK } from \"jose\";\nimport type { Clock, FetchFn, JwksCacheConfig } from \"./types\";\n\nconst DEFAULT_TTL_MS = 15 * 60 * 1000; // 15 minutes\nconst DEFAULT_JITTER_MS = 60 * 1000; // 60 seconds\n\ntype JoseKey = Awaited<ReturnType<typeof importJWK>>;\n\ninterface CacheEntry {\n keys: Map<string, JoseKey>;\n expiresAt: number;\n}\n\ninterface JwkKey {\n kid?: string;\n kty?: string;\n crv?: string;\n x?: string;\n use?: string;\n alg?: string;\n}\n\ninterface JwksResponse {\n keys: JwkKey[];\n}\n\n/**\n * Fetches and caches JWKS from a configured URL.\n *\n * On a `kid` cache miss the cache is refreshed once before failing —\n * this handles key rotation without requiring a restart.\n *\n * Refresh-window jitter avoids thundering-herd stampedes when multiple\n * workers share the same process lifetime.\n */\nexport class JwksCache {\n private cache: CacheEntry | null = null;\n\n constructor(\n private readonly config: JwksCacheConfig,\n private readonly fetch: FetchFn,\n private readonly clock: Clock,\n ) {}\n\n /** Returns the cached key for `kid`, fetching if needed. Refreshes once on kid miss. */\n async getKey(kid: string): Promise<JoseKey | null> {\n let entry = await this.getValidEntry();\n const cached = entry.keys.get(kid);\n if (cached !== undefined) {\n return cached;\n }\n\n // kid miss — refresh once to handle key rotation\n entry = await this.refresh();\n return entry.keys.get(kid) ?? null;\n }\n\n private async getValidEntry(): Promise<CacheEntry> {\n if (this.cache !== null && this.clock.now() < this.cache.expiresAt) {\n return this.cache;\n }\n return this.refresh();\n }\n\n private async refresh(): Promise<CacheEntry> {\n const response = await this.fetch(this.config.jwksUrl);\n if (!response.ok) {\n throw new Error(`JWKS fetch failed for ${this.config.jwksUrl}`);\n }\n const body = (await response.json()) as JwksResponse;\n const keys = new Map<string, JoseKey>();\n\n for (const jwk of body.keys ?? []) {\n if (!jwk.kid) continue;\n try {\n const key = await importJWK(jwk as Parameters<typeof importJWK>[0]);\n keys.set(jwk.kid, key);\n } catch {\n // Skip malformed individual keys — don't fail the whole cache refresh\n }\n }\n\n const ttl = this.config.ttlMs ?? DEFAULT_TTL_MS;\n const jitter = Math.random() * (this.config.jitterMs ?? DEFAULT_JITTER_MS);\n const entry: CacheEntry = {\n keys,\n expiresAt: this.clock.now() + ttl + jitter,\n };\n this.cache = entry;\n return entry;\n }\n}\n","import { decodeProtectedHeader, jwtVerify, errors as joseErrors } from \"jose\";\nimport type { JwtVerificationFailure, ManagedJwtVerifierConfig, VerifiedManagedPrincipal } from \"./types\";\nimport type { JwksCache } from \"./JwksCache\";\n\ntype VerifyResult = VerifiedManagedPrincipal | JwtVerificationFailure;\n\n/**\n * Validates CP-signed EdDSA JWT bearers.\n *\n * Checks: signature, `iss` matches expected, `aud` matches expected workspaceId,\n * `exp`, `nbf`. Returns a `VerifiedManagedPrincipal` or a structured `JwtVerificationFailure`.\n *\n * Hono-independent — no web framework coupling.\n */\nexport class ManagedJwtVerifier {\n constructor(\n private readonly config: ManagedJwtVerifierConfig,\n private readonly jwksCache: JwksCache,\n ) {}\n\n async verify(token: string): Promise<VerifyResult> {\n // Decode header to get kid before verifying signature\n let kid: string | undefined;\n try {\n const header = decodeProtectedHeader(token);\n kid = typeof header.kid === \"string\" ? header.kid : undefined;\n } catch {\n return { failure: \"malformed\", message: \"Unable to decode JWT header.\" };\n }\n\n if (!kid) {\n return { failure: \"missing-kid\", message: \"JWT header is missing the `kid` field.\" };\n }\n\n const key = await this.jwksCache.getKey(kid);\n if (key === null) {\n return { failure: \"unknown-kid\", message: `No key found for kid \"${kid}\" after JWKS refresh.` };\n }\n\n try {\n const { payload } = await jwtVerify(token, key, {\n issuer: this.config.expectedIssuer,\n audience: this.config.expectedAudience,\n algorithms: [\"EdDSA\"],\n });\n\n const sub = payload.sub;\n const aud = payload.aud;\n const workspaceId = Array.isArray(aud) ? aud[0] : aud;\n\n if (!sub || typeof sub !== \"string\") {\n return { failure: \"malformed\", message: \"JWT is missing the `sub` claim.\" };\n }\n if (!workspaceId || typeof workspaceId !== \"string\") {\n return { failure: \"wrong-aud\", message: \"JWT `aud` claim is absent or not a string.\" };\n }\n\n return { userId: sub, workspaceId };\n } catch (error) {\n if (error instanceof joseErrors.JWTExpired) {\n return { failure: \"expired\", message: \"JWT has expired.\" };\n }\n if (error instanceof joseErrors.JWTClaimValidationFailed) {\n const claim = error.claim;\n if (claim === \"iss\") {\n return { failure: \"wrong-iss\", message: \"JWT `iss` claim does not match expected issuer.\" };\n }\n if (claim === \"aud\") {\n return { failure: \"wrong-aud\", message: \"JWT `aud` claim does not match expected audience.\" };\n }\n if (claim === \"nbf\") {\n return { failure: \"not-yet-valid\", message: \"JWT is not yet valid (nbf check failed).\" };\n }\n return { failure: \"malformed\", message: `JWT claim validation failed: ${error.message}` };\n }\n if (error instanceof joseErrors.JWSSignatureVerificationFailed || error instanceof joseErrors.JWSInvalid) {\n return { failure: \"bad-signature\", message: \"JWT signature verification failed.\" };\n }\n return { failure: \"malformed\", message: \"JWT verification failed.\" };\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["config: JwksCacheConfig","fetch: FetchFn","clock: Clock","entry: CacheEntry","config: ManagedJwtVerifierConfig","jwksCache: JwksCache","kid: string | undefined","joseErrors"],"sources":["../src/JwksCache.ts","../src/ManagedJwtVerifier.ts"],"sourcesContent":["import { importJWK } from \"jose\";\nimport type { Clock, FetchFn, JwksCacheConfig } from \"./types\";\n\nconst DEFAULT_TTL_MS = 15 * 60 * 1000;\nconst DEFAULT_JITTER_MS = 60 * 1000;\n\ntype JoseKey = Awaited<ReturnType<typeof importJWK>>;\n\ninterface CacheEntry {\n keys: Map<string, JoseKey>;\n expiresAt: number;\n}\n\ninterface JwkKey {\n kid?: string;\n kty?: string;\n crv?: string;\n x?: string;\n use?: string;\n alg?: string;\n}\n\ninterface JwksResponse {\n keys: JwkKey[];\n}\n\nexport class JwksCache {\n private cache: CacheEntry | null = null;\n\n constructor(\n private readonly config: JwksCacheConfig,\n private readonly fetch: FetchFn,\n private readonly clock: Clock,\n ) {}\n\n async getKey(kid: string): Promise<JoseKey | null> {\n let entry = await this.getValidEntry();\n const cached = entry.keys.get(kid);\n if (cached !== undefined) {\n return cached;\n }\n\n entry = await this.refresh();\n return entry.keys.get(kid) ?? null;\n }\n\n private async getValidEntry(): Promise<CacheEntry> {\n if (this.cache !== null && this.clock.now() < this.cache.expiresAt) {\n return this.cache;\n }\n return this.refresh();\n }\n\n private async refresh(): Promise<CacheEntry> {\n const response = await this.fetch(this.config.jwksUrl);\n if (!response.ok) {\n throw new Error(`JWKS fetch failed for ${this.config.jwksUrl}`);\n }\n const body = (await response.json()) as JwksResponse;\n const keys = new Map<string, JoseKey>();\n\n for (const jwk of body.keys ?? []) {\n if (!jwk.kid) continue;\n try {\n const key = await importJWK(jwk as Parameters<typeof importJWK>[0]);\n keys.set(jwk.kid, key);\n } catch {}\n }\n\n const ttl = this.config.ttlMs ?? DEFAULT_TTL_MS;\n const jitter = Math.random() * (this.config.jitterMs ?? DEFAULT_JITTER_MS);\n const entry: CacheEntry = {\n keys,\n expiresAt: this.clock.now() + ttl + jitter,\n };\n this.cache = entry;\n return entry;\n }\n}\n","import { decodeProtectedHeader, jwtVerify, errors as joseErrors } from \"jose\";\nimport type { JwtVerificationFailure, ManagedJwtVerifierConfig, VerifiedManagedPrincipal } from \"./types\";\nimport type { JwksCache } from \"./JwksCache\";\n\ntype VerifyResult = VerifiedManagedPrincipal | JwtVerificationFailure;\n\nexport class ManagedJwtVerifier {\n constructor(\n private readonly config: ManagedJwtVerifierConfig,\n private readonly jwksCache: JwksCache,\n ) {}\n\n async verify(token: string): Promise<VerifyResult> {\n let kid: string | undefined;\n try {\n const header = decodeProtectedHeader(token);\n kid = typeof header.kid === \"string\" ? header.kid : undefined;\n } catch {\n return { failure: \"malformed\", message: \"Unable to decode JWT header.\" };\n }\n\n if (!kid) {\n return { failure: \"missing-kid\", message: \"JWT header is missing the `kid` field.\" };\n }\n\n const key = await this.jwksCache.getKey(kid);\n if (key === null) {\n return { failure: \"unknown-kid\", message: `No key found for kid \"${kid}\" after JWKS refresh.` };\n }\n\n try {\n const { payload } = await jwtVerify(token, key, {\n issuer: this.config.expectedIssuer,\n audience: this.config.expectedAudience,\n algorithms: [\"EdDSA\"],\n });\n\n const sub = payload.sub;\n const aud = payload.aud;\n const workspaceId = Array.isArray(aud) ? aud[0] : aud;\n\n if (!sub || typeof sub !== \"string\") {\n return { failure: \"malformed\", message: \"JWT is missing the `sub` claim.\" };\n }\n if (!workspaceId || typeof workspaceId !== \"string\") {\n return { failure: \"wrong-aud\", message: \"JWT `aud` claim is absent or not a string.\" };\n }\n\n return { userId: sub, workspaceId };\n } catch (error) {\n if (error instanceof joseErrors.JWTExpired) {\n return { failure: \"expired\", message: \"JWT has expired.\" };\n }\n if (error instanceof joseErrors.JWTClaimValidationFailed) {\n const claim = error.claim;\n if (claim === \"iss\") {\n return { failure: \"wrong-iss\", message: \"JWT `iss` claim does not match expected issuer.\" };\n }\n if (claim === \"aud\") {\n return { failure: \"wrong-aud\", message: \"JWT `aud` claim does not match expected audience.\" };\n }\n if (claim === \"nbf\") {\n return { failure: \"not-yet-valid\", message: \"JWT is not yet valid (nbf check failed).\" };\n }\n return { failure: \"malformed\", message: `JWT claim validation failed: ${error.message}` };\n }\n if (error instanceof joseErrors.JWSSignatureVerificationFailed || error instanceof joseErrors.JWSInvalid) {\n return { failure: \"bad-signature\", message: \"JWT signature verification failed.\" };\n }\n return { failure: \"malformed\", message: \"JWT verification failed.\" };\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/managed-auth",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/JwksCache.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { importJWK } from "jose";
2
2
  import type { Clock, FetchFn, JwksCacheConfig } from "./types";
3
3
 
4
- const DEFAULT_TTL_MS = 15 * 60 * 1000; // 15 minutes
5
- const DEFAULT_JITTER_MS = 60 * 1000; // 60 seconds
4
+ const DEFAULT_TTL_MS = 15 * 60 * 1000;
5
+ const DEFAULT_JITTER_MS = 60 * 1000;
6
6
 
7
7
  type JoseKey = Awaited<ReturnType<typeof importJWK>>;
8
8
 
@@ -24,15 +24,6 @@ interface JwksResponse {
24
24
  keys: JwkKey[];
25
25
  }
26
26
 
27
- /**
28
- * Fetches and caches JWKS from a configured URL.
29
- *
30
- * On a `kid` cache miss the cache is refreshed once before failing —
31
- * this handles key rotation without requiring a restart.
32
- *
33
- * Refresh-window jitter avoids thundering-herd stampedes when multiple
34
- * workers share the same process lifetime.
35
- */
36
27
  export class JwksCache {
37
28
  private cache: CacheEntry | null = null;
38
29
 
@@ -42,7 +33,6 @@ export class JwksCache {
42
33
  private readonly clock: Clock,
43
34
  ) {}
44
35
 
45
- /** Returns the cached key for `kid`, fetching if needed. Refreshes once on kid miss. */
46
36
  async getKey(kid: string): Promise<JoseKey | null> {
47
37
  let entry = await this.getValidEntry();
48
38
  const cached = entry.keys.get(kid);
@@ -50,7 +40,6 @@ export class JwksCache {
50
40
  return cached;
51
41
  }
52
42
 
53
- // kid miss — refresh once to handle key rotation
54
43
  entry = await this.refresh();
55
44
  return entry.keys.get(kid) ?? null;
56
45
  }
@@ -75,9 +64,7 @@ export class JwksCache {
75
64
  try {
76
65
  const key = await importJWK(jwk as Parameters<typeof importJWK>[0]);
77
66
  keys.set(jwk.kid, key);
78
- } catch {
79
- // Skip malformed individual keys — don't fail the whole cache refresh
80
- }
67
+ } catch {}
81
68
  }
82
69
 
83
70
  const ttl = this.config.ttlMs ?? DEFAULT_TTL_MS;
@@ -4,14 +4,6 @@ import type { JwksCache } from "./JwksCache";
4
4
 
5
5
  type VerifyResult = VerifiedManagedPrincipal | JwtVerificationFailure;
6
6
 
7
- /**
8
- * Validates CP-signed EdDSA JWT bearers.
9
- *
10
- * Checks: signature, `iss` matches expected, `aud` matches expected workspaceId,
11
- * `exp`, `nbf`. Returns a `VerifiedManagedPrincipal` or a structured `JwtVerificationFailure`.
12
- *
13
- * Hono-independent — no web framework coupling.
14
- */
15
7
  export class ManagedJwtVerifier {
16
8
  constructor(
17
9
  private readonly config: ManagedJwtVerifierConfig,
@@ -19,7 +11,6 @@ export class ManagedJwtVerifier {
19
11
  ) {}
20
12
 
21
13
  async verify(token: string): Promise<VerifyResult> {
22
- // Decode header to get kid before verifying signature
23
14
  let kid: string | undefined;
24
15
  try {
25
16
  const header = decodeProtectedHeader(token);
package/src/types.ts CHANGED
@@ -1,7 +1,3 @@
1
- /**
2
- * A successfully verified CP-signed JWT principal.
3
- * `userId` maps to the JWT `sub` claim; `workspaceId` maps to `aud`.
4
- */
5
1
  export interface VerifiedManagedPrincipal {
6
2
  readonly userId: string;
7
3
  readonly workspaceId: string;
@@ -17,33 +13,25 @@ export type JwtVerificationFailureReason =
17
13
  | "not-yet-valid"
18
14
  | "malformed";
19
15
 
20
- /** Structured failure returned instead of throwing, so callers can map to HTTP status codes cleanly. */
21
16
  export interface JwtVerificationFailure {
22
17
  readonly failure: JwtVerificationFailureReason;
23
18
  readonly message: string;
24
19
  }
25
20
 
26
21
  export interface JwksCacheConfig {
27
- /** URL to the JWKS endpoint (e.g. https://cp.example.com/.well-known/jwks.json). */
28
22
  readonly jwksUrl: string;
29
- /** TTL in milliseconds before the cache is considered stale. Defaults to 15 minutes. */
30
23
  readonly ttlMs?: number;
31
- /** Jitter window in milliseconds applied to TTL to avoid stampedes. Defaults to 60 seconds. */
32
24
  readonly jitterMs?: number;
33
25
  }
34
26
 
35
27
  export interface ManagedJwtVerifierConfig {
36
- /** Expected value of the JWT `iss` claim — must exactly match. */
37
28
  readonly expectedIssuer: string;
38
- /** Expected `aud` claim — must exactly match the workspace ID. */
39
29
  readonly expectedAudience: string;
40
30
  readonly jwksCache: JwksCacheConfig;
41
31
  }
42
32
 
43
- /** Minimal fetch-compatible function type for injection / testing. */
44
33
  export type FetchFn = (url: string) => Promise<{ ok: boolean; json(): Promise<unknown> }>;
45
34
 
46
- /** Minimal clock interface for injection / testing. */
47
35
  export interface Clock {
48
36
  now(): number;
49
37
  }
@@ -62,17 +62,16 @@ describe("JwksCache", () => {
62
62
  (clock as unknown as { _now: number })._now = ((clock as unknown as { _now: number })._now ?? 0) + ms;
63
63
  };
64
64
 
65
- // Use a simple advancing clock
66
65
  let now = 0;
67
66
  const advancingClock: Clock = { now: () => now };
68
67
 
69
68
  const cache = new JwksCache({ jwksUrl: "/jwks", ttlMs: 60_000, jitterMs: 0 }, fetch, advancingClock);
70
69
 
71
70
  await cache.getKey("key-1");
72
- now += 30_000; // advance 30s — still within TTL
71
+ now += 30_000;
73
72
  await cache.getKey("key-1");
74
73
 
75
- expect(callCount).toBe(1); // no second fetch
74
+ expect(callCount).toBe(1);
76
75
  });
77
76
 
78
77
  it("re-fetches after TTL expires", async () => {
@@ -88,7 +87,7 @@ describe("JwksCache", () => {
88
87
  const cache = new JwksCache({ jwksUrl: "/jwks", ttlMs: 60_000, jitterMs: 0 }, fetch, clock);
89
88
 
90
89
  await cache.getKey("key-1");
91
- now += 61_000; // past TTL
90
+ now += 61_000;
92
91
  await cache.getKey("key-1");
93
92
 
94
93
  expect(callCount).toBe(2);
@@ -96,7 +95,6 @@ describe("JwksCache", () => {
96
95
 
97
96
  it("refreshes once on kid miss and returns the new key", async () => {
98
97
  let callCount = 0;
99
- // First fetch has only key-1; second has key-1 and key-2
100
98
  const fetch: FetchFn = async () => {
101
99
  callCount++;
102
100
  const keys = callCount === 1 ? [edKey1] : [edKey1, edKey2];
@@ -105,11 +103,9 @@ describe("JwksCache", () => {
105
103
  const clock: Clock = { now: () => 0 };
106
104
  const cache = new JwksCache({ jwksUrl: "/jwks", ttlMs: 60_000, jitterMs: 0 }, fetch, clock);
107
105
 
108
- // Populate cache with just key-1
109
106
  await cache.getKey("key-1");
110
107
  expect(callCount).toBe(1);
111
108
 
112
- // Ask for key-2 — cache miss triggers one refresh
113
109
  const key = await cache.getKey("key-2");
114
110
  expect(key).not.toBeNull();
115
111
  expect(callCount).toBe(2);
@@ -43,10 +43,6 @@ function makeFrozenClock(): Clock {
43
43
  return { now: () => 0 };
44
44
  }
45
45
 
46
- // Deterministic timestamps: use fixed calendar dates rather than Date.now().
47
- // exp far in the future (2099) — always valid by the real clock.
48
- // Expired exp: a date in the past (2000).
49
- // Future nbf: a date in the distant future (2099).
50
46
  const EXP_FUTURE_UNIX = Math.floor(new Date("2099-12-31T00:00:00Z").getTime() / 1000);
51
47
  const EXP_PAST_UNIX = Math.floor(new Date("2000-01-01T00:00:00Z").getTime() / 1000);
52
48
  const NBF_PAST_UNIX = Math.floor(new Date("2000-01-01T00:00:00Z").getTime() / 1000);
@@ -111,7 +107,6 @@ describe("ManagedJwtVerifier", () => {
111
107
  const verifier = makeVerifier([kp1.jwk]);
112
108
  const token = await makeToken(kp1.privateKey, "key-1");
113
109
  const parts = token.split(".");
114
- // Tamper the payload
115
110
  const tampered = `${parts[0]}.${parts[1]}X.${parts[2]}`;
116
111
  const result = await verifier.verify(tampered);
117
112
  expect("failure" in result).toBe(true);
@@ -162,9 +157,8 @@ describe("ManagedJwtVerifier", () => {
162
157
 
163
158
  it("fails with missing-kid when header has no kid field", async () => {
164
159
  const verifier = makeVerifier([kp1.jwk]);
165
- // Build a token without kid in header using deterministic timestamps
166
160
  const token = await new SignJWT({ sub: "user-1" })
167
- .setProtectedHeader({ alg: "EdDSA" }) // no kid
161
+ .setProtectedHeader({ alg: "EdDSA" })
168
162
  .setIssuer(ISSUER)
169
163
  .setAudience(WORKSPACE_ID)
170
164
  .setExpirationTime(EXP_FUTURE_UNIX)
@@ -179,7 +173,6 @@ describe("ManagedJwtVerifier", () => {
179
173
 
180
174
  it("fails with unknown-kid after refresh when kid is not in JWKS", async () => {
181
175
  const verifier = makeVerifier([kp1.jwk]);
182
- // Token signed with key-2 but JWKS only has key-1 (even after refresh)
183
176
  const token = await makeToken(kp2.privateKey, "key-2");
184
177
  const result = await verifier.verify(token);
185
178
  expect("failure" in result).toBe(true);
@@ -189,7 +182,6 @@ describe("ManagedJwtVerifier", () => {
189
182
  });
190
183
 
191
184
  it("accepts a new-key token after CP rotates keys (kid appears on refresh)", async () => {
192
- // First JWKS response has only key-1; second response (on kid miss) adds key-2
193
185
  let callCount = 0;
194
186
  const fetch: FetchFn = async () => {
195
187
  callCount++;
@@ -202,10 +194,8 @@ describe("ManagedJwtVerifier", () => {
202
194
  cache,
203
195
  );
204
196
 
205
- // Warm the cache with key-1 only
206
197
  await cache.getKey("key-1");
207
198
 
208
- // Now verify a token using key-2 — should trigger refresh and succeed
209
199
  const token = await makeToken(kp2.privateKey, "key-2");
210
200
  const result = await verifier.verify(token);
211
201
  expect("failure" in result).toBe(false);
@@ -1,4 +0,0 @@
1
-
2
- > @codemation/managed-auth@0.0.1 lint C:\Users\ChrisBlokland\projects\codemation\framework\packages\managed-auth
3
- > eslint .
4
-
@@ -1,4 +0,0 @@
1
-
2
- > @codemation/managed-auth@0.0.1 typecheck C:\Users\ChrisBlokland\projects\codemation\framework\packages\managed-auth
3
- > tsc -p tsconfig.json --noEmit
4
-