@peac/jwks-cache 0.9.18

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.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @peac/jwks-cache
2
+
3
+ Edge-safe JWKS fetch and cache with SSRF protection
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @peac/jwks-cache
9
+ ```
10
+
11
+ ## Documentation
12
+
13
+ See [peacprotocol.org](https://peacprotocol.org) for full documentation.
14
+
15
+ ## License
16
+
17
+ Apache-2.0
18
+
19
+ ---
20
+
21
+ PEAC Protocol is an open source project stewarded by Originary and community contributors.
22
+
23
+ [Originary](https://www.originary.xyz) | [Docs](https://peacprotocol.org) | [GitHub](https://github.com/peacprotocol/peac)
@@ -0,0 +1,37 @@
1
+ /**
2
+ * In-memory cache implementation.
3
+ */
4
+ import type { CacheBackend, CacheEntry } from './types.js';
5
+ /**
6
+ * Simple in-memory cache with TTL support.
7
+ */
8
+ export declare class InMemoryCache implements CacheBackend {
9
+ private readonly cache;
10
+ get(key: string): Promise<CacheEntry | null>;
11
+ set(key: string, value: CacheEntry): Promise<void>;
12
+ delete(key: string): Promise<void>;
13
+ /**
14
+ * Clear all entries.
15
+ */
16
+ clear(): void;
17
+ /**
18
+ * Get current cache size.
19
+ */
20
+ get size(): number;
21
+ }
22
+ /**
23
+ * Build cache key for a specific key ID.
24
+ */
25
+ export declare function buildCacheKey(issuerOrigin: string, kid: string): string;
26
+ /**
27
+ * Build cache key for JWKS set.
28
+ */
29
+ export declare function buildJwksCacheKey(issuerOrigin: string): string;
30
+ /**
31
+ * Parse Cache-Control header for max-age.
32
+ *
33
+ * @param cacheControl - Cache-Control header value
34
+ * @returns max-age in seconds or null if not found
35
+ */
36
+ export declare function parseCacheControlMaxAge(cacheControl: string | null): number | null;
37
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3D;;GAEG;AACH,qBAAa,aAAc,YAAW,YAAY;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IAEjD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAiB5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAWlF"}
package/dist/cache.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * In-memory cache implementation.
3
+ */
4
+ /**
5
+ * Simple in-memory cache with TTL support.
6
+ */
7
+ export class InMemoryCache {
8
+ cache = new Map();
9
+ async get(key) {
10
+ const entry = this.cache.get(key);
11
+ if (!entry) {
12
+ return null;
13
+ }
14
+ // Check expiration
15
+ const now = Math.floor(Date.now() / 1000);
16
+ if (now >= entry.expiresAt) {
17
+ this.cache.delete(key);
18
+ return null;
19
+ }
20
+ return entry;
21
+ }
22
+ async set(key, value) {
23
+ this.cache.set(key, value);
24
+ }
25
+ async delete(key) {
26
+ this.cache.delete(key);
27
+ }
28
+ /**
29
+ * Clear all entries.
30
+ */
31
+ clear() {
32
+ this.cache.clear();
33
+ }
34
+ /**
35
+ * Get current cache size.
36
+ */
37
+ get size() {
38
+ return this.cache.size;
39
+ }
40
+ }
41
+ /**
42
+ * Build cache key for a specific key ID.
43
+ */
44
+ export function buildCacheKey(issuerOrigin, kid) {
45
+ return `${issuerOrigin}:${kid}`;
46
+ }
47
+ /**
48
+ * Build cache key for JWKS set.
49
+ */
50
+ export function buildJwksCacheKey(issuerOrigin) {
51
+ return `${issuerOrigin}:__jwks__`;
52
+ }
53
+ /**
54
+ * Parse Cache-Control header for max-age.
55
+ *
56
+ * @param cacheControl - Cache-Control header value
57
+ * @returns max-age in seconds or null if not found
58
+ */
59
+ export function parseCacheControlMaxAge(cacheControl) {
60
+ if (!cacheControl) {
61
+ return null;
62
+ }
63
+ const match = cacheControl.match(/max-age=(\d+)/);
64
+ if (!match) {
65
+ return null;
66
+ }
67
+ return parseInt(match[1], 10);
68
+ }
69
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;GAEG;AACH,MAAM,OAAO,aAAa;IACP,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAEvD,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAElC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,CAAC;QACd,CAAC;QAED,mBAAmB;QACnB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,IAAI,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAiB;QACtC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED;;OAEG;IACH,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;CACF;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,YAAoB,EAAE,GAAW;IAC7D,OAAO,GAAG,YAAY,IAAI,GAAG,EAAE,CAAC;AAClC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB;IACpD,OAAO,GAAG,YAAY,WAAW,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,YAA2B;IACjE,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * JWKS Cache error codes per execution pack specification.
3
+ */
4
+ export declare const ErrorCodes: {
5
+ /** Network error fetching JWKS */
6
+ readonly JWKS_FETCH_FAILED: "E_JWKS_FETCH_FAILED";
7
+ /** Fetch timeout */
8
+ readonly JWKS_TIMEOUT: "E_JWKS_TIMEOUT";
9
+ /** Invalid JSON or structure */
10
+ readonly JWKS_INVALID: "E_JWKS_INVALID";
11
+ /** Response > 1MB */
12
+ readonly JWKS_TOO_LARGE: "E_JWKS_TOO_LARGE";
13
+ /** keys.length > 100 */
14
+ readonly JWKS_TOO_MANY_KEYS: "E_JWKS_TOO_MANY_KEYS";
15
+ /** Private IP or metadata URL blocked */
16
+ readonly SSRF_BLOCKED: "E_SSRF_BLOCKED";
17
+ /** Requested kid not in JWKS */
18
+ readonly KEY_NOT_FOUND: "E_KEY_NOT_FOUND";
19
+ /** All discovery paths failed */
20
+ readonly ALL_PATHS_FAILED: "E_ALL_PATHS_FAILED";
21
+ };
22
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
23
+ /**
24
+ * HTTP status codes for each error.
25
+ */
26
+ export declare const ErrorHttpStatus: Record<ErrorCode, number>;
27
+ /**
28
+ * JWKS error with code and HTTP status.
29
+ */
30
+ export declare class JwksError extends Error {
31
+ readonly code: ErrorCode;
32
+ readonly httpStatus: number;
33
+ constructor(code: ErrorCode, message: string);
34
+ }
35
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,UAAU;IACrB,kCAAkC;;IAElC,oBAAoB;;IAEpB,gCAAgC;;IAEhC,qBAAqB;;IAErB,wBAAwB;;IAExB,yCAAyC;;IAEzC,gCAAgC;;IAEhC,iCAAiC;;CAEzB,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CASrD,CAAC;AAEF;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM;CAM7C"}
package/dist/errors.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * JWKS Cache error codes per execution pack specification.
3
+ */
4
+ export const ErrorCodes = {
5
+ /** Network error fetching JWKS */
6
+ JWKS_FETCH_FAILED: 'E_JWKS_FETCH_FAILED',
7
+ /** Fetch timeout */
8
+ JWKS_TIMEOUT: 'E_JWKS_TIMEOUT',
9
+ /** Invalid JSON or structure */
10
+ JWKS_INVALID: 'E_JWKS_INVALID',
11
+ /** Response > 1MB */
12
+ JWKS_TOO_LARGE: 'E_JWKS_TOO_LARGE',
13
+ /** keys.length > 100 */
14
+ JWKS_TOO_MANY_KEYS: 'E_JWKS_TOO_MANY_KEYS',
15
+ /** Private IP or metadata URL blocked */
16
+ SSRF_BLOCKED: 'E_SSRF_BLOCKED',
17
+ /** Requested kid not in JWKS */
18
+ KEY_NOT_FOUND: 'E_KEY_NOT_FOUND',
19
+ /** All discovery paths failed */
20
+ ALL_PATHS_FAILED: 'E_ALL_PATHS_FAILED',
21
+ };
22
+ /**
23
+ * HTTP status codes for each error.
24
+ */
25
+ export const ErrorHttpStatus = {
26
+ [ErrorCodes.JWKS_FETCH_FAILED]: 502,
27
+ [ErrorCodes.JWKS_TIMEOUT]: 504,
28
+ [ErrorCodes.JWKS_INVALID]: 502,
29
+ [ErrorCodes.JWKS_TOO_LARGE]: 502,
30
+ [ErrorCodes.JWKS_TOO_MANY_KEYS]: 502,
31
+ [ErrorCodes.SSRF_BLOCKED]: 403,
32
+ [ErrorCodes.KEY_NOT_FOUND]: 401,
33
+ [ErrorCodes.ALL_PATHS_FAILED]: 502,
34
+ };
35
+ /**
36
+ * JWKS error with code and HTTP status.
37
+ */
38
+ export class JwksError extends Error {
39
+ code;
40
+ httpStatus;
41
+ constructor(code, message) {
42
+ super(message);
43
+ this.name = 'JwksError';
44
+ this.code = code;
45
+ this.httpStatus = ErrorHttpStatus[code];
46
+ }
47
+ }
48
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,kCAAkC;IAClC,iBAAiB,EAAE,qBAAqB;IACxC,oBAAoB;IACpB,YAAY,EAAE,gBAAgB;IAC9B,gCAAgC;IAChC,YAAY,EAAE,gBAAgB;IAC9B,qBAAqB;IACrB,cAAc,EAAE,kBAAkB;IAClC,wBAAwB;IACxB,kBAAkB,EAAE,sBAAsB;IAC1C,yCAAyC;IACzC,YAAY,EAAE,gBAAgB;IAC9B,gCAAgC;IAChC,aAAa,EAAE,iBAAiB;IAChC,iCAAiC;IACjC,gBAAgB,EAAE,oBAAoB;CAC9B,CAAC;AAIX;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAA8B;IACxD,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,GAAG;IACnC,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,GAAG;IAC9B,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,GAAG;IAC9B,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,GAAG;IAChC,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,GAAG;IACpC,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,GAAG;IAC9B,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,GAAG;IAC/B,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,GAAG;CACnC,CAAC;AAEF;;GAEG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IACzB,IAAI,CAAY;IAChB,UAAU,CAAS;IAE5B,YAAY,IAAe,EAAE,OAAe;QAC1C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @peac/jwks-cache
3
+ *
4
+ * Edge-safe JWKS fetch and cache with SSRF protection.
5
+ */
6
+ export type { JWK, JWKS, CacheBackend, CacheEntry, ResolverOptions, ResolvedKey, JwksKeyResolver, } from './types.js';
7
+ export { InMemoryCache, buildCacheKey, buildJwksCacheKey, parseCacheControlMaxAge, } from './cache.js';
8
+ export { validateUrl, isMetadataIp } from './security.js';
9
+ export { createResolver, resolveKey, importJwkAsEd25519, createJwkVerifier } from './resolver.js';
10
+ export { ErrorCodes, ErrorHttpStatus, JwksError } from './errors.js';
11
+ export type { ErrorCode } from './errors.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,YAAY,EACV,GAAG,EACH,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,eAAe,EACf,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAG1D,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlG,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACrE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @peac/jwks-cache
3
+ *
4
+ * Edge-safe JWKS fetch and cache with SSRF protection.
5
+ */
6
+ // Cache
7
+ export { InMemoryCache, buildCacheKey, buildJwksCacheKey, parseCacheControlMaxAge, } from './cache.js';
8
+ // Security
9
+ export { validateUrl, isMetadataIp } from './security.js';
10
+ // Resolver
11
+ export { createResolver, resolveKey, importJwkAsEd25519, createJwkVerifier } from './resolver.js';
12
+ // Errors
13
+ export { ErrorCodes, ErrorHttpStatus, JwksError } from './errors.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,QAAQ;AACR,OAAO,EACL,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB,WAAW;AACX,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE1D,WAAW;AACX,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElG,SAAS;AACT,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * JWKS resolver with multi-path discovery and caching.
3
+ */
4
+ import type { SignatureVerifier } from '@peac/http-signatures';
5
+ import type { JWK, ResolverOptions, ResolvedKey, JwksKeyResolver } from './types.js';
6
+ /**
7
+ * Create a JWKS key resolver.
8
+ *
9
+ * @param options - Resolver options
10
+ * @returns Key resolver function
11
+ */
12
+ export declare function createResolver(options?: ResolverOptions): JwksKeyResolver;
13
+ /**
14
+ * Resolve a key by issuer and key ID.
15
+ *
16
+ * Discovery order (per TAP spec):
17
+ * 1. /.well-known/jwks
18
+ * 2. /keys?keyID=<kid>
19
+ * 3. /.well-known/jwks.json (fallback)
20
+ */
21
+ export declare function resolveKey(issuer: string, keyid: string, options: Required<Pick<ResolverOptions, 'cache' | 'defaultTtlSeconds' | 'maxTtlSeconds' | 'minTtlSeconds' | 'timeoutMs' | 'maxResponseBytes' | 'maxKeys' | 'allowLocalhost'>> & Pick<ResolverOptions, 'isAllowedHost'>): Promise<ResolvedKey | null>;
22
+ /**
23
+ * Import a JWK as Ed25519 public key.
24
+ *
25
+ * Returns an opaque key object (runtime-neutral).
26
+ * Use createJwkVerifier() for a complete SignatureVerifier.
27
+ */
28
+ export declare function importJwkAsEd25519(jwk: JWK): Promise<unknown>;
29
+ /**
30
+ * Create a SignatureVerifier from a JWK.
31
+ *
32
+ * Convenience function for TAP integration.
33
+ *
34
+ * @param jwk - Ed25519 JWK
35
+ * @returns SignatureVerifier function
36
+ */
37
+ export declare function createJwkVerifier(jwk: JWK): Promise<SignatureVerifier>;
38
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EACV,GAAG,EAGH,eAAe,EACf,WAAW,EACX,eAAe,EAChB,MAAM,YAAY,CAAC;AAYpB;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,eAAoB,GAAG,eAAe,CAgC7E;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,CACf,IAAI,CACF,eAAe,EACb,OAAO,GACP,mBAAmB,GACnB,eAAe,GACf,eAAe,GACf,WAAW,GACX,kBAAkB,GAClB,SAAS,GACT,gBAAgB,CACnB,CACF,GACC,IAAI,CAAC,eAAe,EAAE,eAAe,CAAC,GACvC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAgI7B;AAkHD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAYnE;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAa5E"}
@@ -0,0 +1,266 @@
1
+ /**
2
+ * JWKS resolver with multi-path discovery and caching.
3
+ */
4
+ import { ErrorCodes, JwksError } from './errors.js';
5
+ import { validateUrl } from './security.js';
6
+ import { InMemoryCache, buildCacheKey, parseCacheControlMaxAge } from './cache.js';
7
+ const DEFAULT_TTL_SECONDS = 3600; // 1 hour
8
+ const MAX_TTL_SECONDS = 86400; // 24 hours
9
+ const MIN_TTL_SECONDS = 60;
10
+ const DEFAULT_TIMEOUT_MS = 5000;
11
+ const DEFAULT_MAX_RESPONSE_BYTES = 1024 * 1024; // 1MB
12
+ const DEFAULT_MAX_KEYS = 100;
13
+ /**
14
+ * Create a JWKS key resolver.
15
+ *
16
+ * @param options - Resolver options
17
+ * @returns Key resolver function
18
+ */
19
+ export function createResolver(options = {}) {
20
+ const cache = options.cache ?? new InMemoryCache();
21
+ const { defaultTtlSeconds = DEFAULT_TTL_SECONDS, maxTtlSeconds = MAX_TTL_SECONDS, minTtlSeconds = MIN_TTL_SECONDS, timeoutMs = DEFAULT_TIMEOUT_MS, maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, maxKeys = DEFAULT_MAX_KEYS, isAllowedHost, allowLocalhost = false, } = options;
22
+ return async (issuer, keyid) => {
23
+ const resolvedKey = await resolveKey(issuer, keyid, {
24
+ cache,
25
+ defaultTtlSeconds,
26
+ maxTtlSeconds,
27
+ minTtlSeconds,
28
+ timeoutMs,
29
+ maxResponseBytes,
30
+ maxKeys,
31
+ isAllowedHost,
32
+ allowLocalhost,
33
+ });
34
+ if (!resolvedKey) {
35
+ return null;
36
+ }
37
+ return createJwkVerifier(resolvedKey.jwk);
38
+ };
39
+ }
40
+ /**
41
+ * Resolve a key by issuer and key ID.
42
+ *
43
+ * Discovery order (per TAP spec):
44
+ * 1. /.well-known/jwks
45
+ * 2. /keys?keyID=<kid>
46
+ * 3. /.well-known/jwks.json (fallback)
47
+ */
48
+ export async function resolveKey(issuer, keyid, options) {
49
+ const { cache, isAllowedHost, allowLocalhost } = options;
50
+ // Normalize issuer to origin
51
+ const issuerOrigin = new URL(issuer).origin;
52
+ const cacheKey = buildCacheKey(issuerOrigin, keyid);
53
+ // Check cache first
54
+ const cached = await cache.get(cacheKey);
55
+ if (cached) {
56
+ return {
57
+ jwk: cached.jwk,
58
+ source: '/.well-known/jwks',
59
+ cached: true,
60
+ };
61
+ }
62
+ // Discovery paths in order
63
+ const paths = [
64
+ {
65
+ url: `${issuerOrigin}/.well-known/jwks`,
66
+ source: '/.well-known/jwks',
67
+ isSingleKey: false,
68
+ },
69
+ {
70
+ url: `${issuerOrigin}/keys?keyID=${encodeURIComponent(keyid)}`,
71
+ source: '/keys',
72
+ isSingleKey: true,
73
+ },
74
+ {
75
+ url: `${issuerOrigin}/.well-known/jwks.json`,
76
+ source: '/.well-known/jwks.json',
77
+ isSingleKey: false,
78
+ },
79
+ ];
80
+ const errors = [];
81
+ for (const path of paths) {
82
+ try {
83
+ // Validate URL for SSRF
84
+ validateUrl(path.url, { isAllowedHost, allowLocalhost });
85
+ const result = await fetchWithTimeout(path.url, options.timeoutMs);
86
+ // Check response size
87
+ const contentLength = result.headers.get('content-length');
88
+ if (contentLength && parseInt(contentLength, 10) > options.maxResponseBytes) {
89
+ throw new JwksError(ErrorCodes.JWKS_TOO_LARGE, `Response too large: ${contentLength} bytes`);
90
+ }
91
+ // Parse response
92
+ const text = await result.text();
93
+ if (text.length > options.maxResponseBytes) {
94
+ throw new JwksError(ErrorCodes.JWKS_TOO_LARGE, `Response too large: ${text.length} bytes`);
95
+ }
96
+ let data;
97
+ try {
98
+ data = JSON.parse(text);
99
+ }
100
+ catch {
101
+ throw new JwksError(ErrorCodes.JWKS_INVALID, 'Invalid JSON response');
102
+ }
103
+ // Extract JWK
104
+ let jwk = null;
105
+ if (path.isSingleKey) {
106
+ // Single key endpoint returns JWK directly
107
+ jwk = validateJwk(data);
108
+ }
109
+ else {
110
+ // JWKS endpoint returns key set
111
+ const jwks = validateJwks(data, options.maxKeys);
112
+ jwk = findKey(jwks, keyid);
113
+ }
114
+ if (!jwk) {
115
+ continue; // Try next path
116
+ }
117
+ // Calculate TTL
118
+ const cacheControlMaxAge = parseCacheControlMaxAge(result.headers.get('cache-control'));
119
+ const ttl = calculateTtl(cacheControlMaxAge, options.defaultTtlSeconds, options.minTtlSeconds, options.maxTtlSeconds);
120
+ // Cache the key
121
+ const now = Math.floor(Date.now() / 1000);
122
+ await cache.set(cacheKey, {
123
+ jwk,
124
+ expiresAt: now + ttl,
125
+ etag: result.headers.get('etag') ?? undefined,
126
+ });
127
+ return {
128
+ jwk,
129
+ source: path.source,
130
+ cached: false,
131
+ };
132
+ }
133
+ catch (error) {
134
+ errors.push(error);
135
+ // Continue to next path
136
+ }
137
+ }
138
+ // All paths failed
139
+ if (errors.length > 0) {
140
+ const lastError = errors[errors.length - 1];
141
+ if (lastError instanceof JwksError) {
142
+ throw lastError;
143
+ }
144
+ throw new JwksError(ErrorCodes.ALL_PATHS_FAILED, `All discovery paths failed for ${issuerOrigin}`);
145
+ }
146
+ return null;
147
+ }
148
+ /**
149
+ * Fetch with timeout.
150
+ */
151
+ async function fetchWithTimeout(url, timeoutMs) {
152
+ const controller = new AbortController();
153
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
154
+ try {
155
+ const response = await fetch(url, {
156
+ signal: controller.signal,
157
+ redirect: 'error', // No redirect following (fail-closed)
158
+ headers: {
159
+ Accept: 'application/json',
160
+ },
161
+ });
162
+ if (!response.ok) {
163
+ throw new JwksError(ErrorCodes.JWKS_FETCH_FAILED, `HTTP ${response.status}: ${response.statusText}`);
164
+ }
165
+ return response;
166
+ }
167
+ catch (error) {
168
+ if (error.name === 'AbortError') {
169
+ throw new JwksError(ErrorCodes.JWKS_TIMEOUT, `Fetch timeout after ${timeoutMs}ms`);
170
+ }
171
+ if (error instanceof JwksError) {
172
+ throw error;
173
+ }
174
+ throw new JwksError(ErrorCodes.JWKS_FETCH_FAILED, `Fetch failed: ${error.message}`);
175
+ }
176
+ finally {
177
+ clearTimeout(timeout);
178
+ }
179
+ }
180
+ /**
181
+ * Validate and extract JWK from response data.
182
+ */
183
+ function validateJwk(data) {
184
+ if (!data || typeof data !== 'object') {
185
+ return null;
186
+ }
187
+ const jwk = data;
188
+ if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string') {
189
+ return null;
190
+ }
191
+ return {
192
+ kty: jwk.kty,
193
+ crv: jwk.crv,
194
+ x: jwk.x,
195
+ kid: typeof jwk.kid === 'string' ? jwk.kid : undefined,
196
+ use: typeof jwk.use === 'string' ? jwk.use : undefined,
197
+ alg: typeof jwk.alg === 'string' ? jwk.alg : undefined,
198
+ };
199
+ }
200
+ /**
201
+ * Validate JWKS structure.
202
+ */
203
+ function validateJwks(data, maxKeys) {
204
+ if (!data || typeof data !== 'object') {
205
+ throw new JwksError(ErrorCodes.JWKS_INVALID, 'Invalid JWKS structure');
206
+ }
207
+ const jwks = data;
208
+ if (!Array.isArray(jwks.keys)) {
209
+ throw new JwksError(ErrorCodes.JWKS_INVALID, 'JWKS must have keys array');
210
+ }
211
+ if (jwks.keys.length > maxKeys) {
212
+ throw new JwksError(ErrorCodes.JWKS_TOO_MANY_KEYS, `Too many keys: ${jwks.keys.length} > ${maxKeys}`);
213
+ }
214
+ return { keys: jwks.keys };
215
+ }
216
+ /**
217
+ * Find key by ID in JWKS.
218
+ */
219
+ function findKey(jwks, keyid) {
220
+ for (const key of jwks.keys) {
221
+ if (key.kid === keyid) {
222
+ return validateJwk(key);
223
+ }
224
+ }
225
+ return null;
226
+ }
227
+ /**
228
+ * Calculate TTL from Cache-Control and options.
229
+ */
230
+ function calculateTtl(cacheControlMaxAge, defaultTtl, minTtl, maxTtl) {
231
+ let ttl = cacheControlMaxAge ?? defaultTtl;
232
+ ttl = Math.max(minTtl, ttl);
233
+ ttl = Math.min(maxTtl, ttl);
234
+ return ttl;
235
+ }
236
+ /**
237
+ * Import a JWK as Ed25519 public key.
238
+ *
239
+ * Returns an opaque key object (runtime-neutral).
240
+ * Use createJwkVerifier() for a complete SignatureVerifier.
241
+ */
242
+ export async function importJwkAsEd25519(jwk) {
243
+ return globalThis.crypto.subtle.importKey('jwk', {
244
+ kty: jwk.kty,
245
+ crv: jwk.crv,
246
+ x: jwk.x,
247
+ }, { name: 'Ed25519' }, false, ['verify']);
248
+ }
249
+ /**
250
+ * Create a SignatureVerifier from a JWK.
251
+ *
252
+ * Convenience function for TAP integration.
253
+ *
254
+ * @param jwk - Ed25519 JWK
255
+ * @returns SignatureVerifier function
256
+ */
257
+ export async function createJwkVerifier(jwk) {
258
+ const key = await importJwkAsEd25519(jwk);
259
+ return async (data, signature) => {
260
+ // Create proper ArrayBuffer views to satisfy TypeScript
261
+ const sigBuffer = new Uint8Array(signature).buffer;
262
+ const dataBuffer = new Uint8Array(data).buffer;
263
+ return globalThis.crypto.subtle.verify('Ed25519', key, sigBuffer, dataBuffer);
264
+ };
265
+ }
266
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * SSRF protection for JWKS fetching.
3
+ *
4
+ * Edge runtimes cannot reliably block private IPs via DNS resolution.
5
+ * This module implements what IS possible at the edge.
6
+ */
7
+ /**
8
+ * Validate URL for SSRF protection.
9
+ *
10
+ * @param url - URL to validate
11
+ * @param options - Validation options
12
+ * @throws JwksError if URL is blocked
13
+ */
14
+ export declare function validateUrl(url: string, options?: {
15
+ allowLocalhost?: boolean;
16
+ isAllowedHost?: (host: string) => boolean;
17
+ }): void;
18
+ /**
19
+ * Check if hostname is a metadata IP.
20
+ */
21
+ export declare function isMetadataIp(hostname: string): boolean;
22
+ //# sourceMappingURL=security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IACP,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACtC,GACL,IAAI,CAmCN;AAsCD;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAYtD"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * SSRF protection for JWKS fetching.
3
+ *
4
+ * Edge runtimes cannot reliably block private IPs via DNS resolution.
5
+ * This module implements what IS possible at the edge.
6
+ */
7
+ import { ErrorCodes, JwksError } from './errors.js';
8
+ /**
9
+ * Validate URL for SSRF protection.
10
+ *
11
+ * @param url - URL to validate
12
+ * @param options - Validation options
13
+ * @throws JwksError if URL is blocked
14
+ */
15
+ export function validateUrl(url, options = {}) {
16
+ let parsed;
17
+ try {
18
+ parsed = new URL(url);
19
+ }
20
+ catch {
21
+ throw new JwksError(ErrorCodes.SSRF_BLOCKED, `Invalid URL: ${url}`);
22
+ }
23
+ // HTTPS required (except localhost in dev)
24
+ if (parsed.protocol !== 'https:') {
25
+ if (parsed.protocol === 'http:' && options.allowLocalhost && isLocalhostHost(parsed.hostname)) {
26
+ // Allow http://localhost in dev mode
27
+ }
28
+ else {
29
+ throw new JwksError(ErrorCodes.SSRF_BLOCKED, `HTTPS required, got ${parsed.protocol}`);
30
+ }
31
+ }
32
+ // Block localhost variants (unless explicitly allowed)
33
+ if (isLocalhostHost(parsed.hostname) && !options.allowLocalhost) {
34
+ throw new JwksError(ErrorCodes.SSRF_BLOCKED, `Localhost blocked: ${parsed.hostname}`);
35
+ }
36
+ // Block literal IP addresses in URL
37
+ if (isLiteralIp(parsed.hostname)) {
38
+ throw new JwksError(ErrorCodes.SSRF_BLOCKED, `Literal IP addresses blocked: ${parsed.hostname}`);
39
+ }
40
+ // Check enterprise allowlist if provided
41
+ if (options.isAllowedHost && !options.isAllowedHost(parsed.hostname)) {
42
+ throw new JwksError(ErrorCodes.SSRF_BLOCKED, `Host not in allowlist: ${parsed.hostname}`);
43
+ }
44
+ }
45
+ /**
46
+ * Check if hostname is localhost variant.
47
+ */
48
+ function isLocalhostHost(hostname) {
49
+ const lower = hostname.toLowerCase();
50
+ return (lower === 'localhost' ||
51
+ lower === '127.0.0.1' ||
52
+ lower === '::1' ||
53
+ lower === '[::1]' ||
54
+ lower.endsWith('.localhost'));
55
+ }
56
+ /**
57
+ * Check if hostname is a literal IP address.
58
+ */
59
+ function isLiteralIp(hostname) {
60
+ // IPv4 pattern
61
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
62
+ return true;
63
+ }
64
+ // IPv6 pattern (with or without brackets)
65
+ if (hostname.startsWith('[') && hostname.endsWith(']')) {
66
+ return true;
67
+ }
68
+ // Colon indicates IPv6 (no brackets)
69
+ if (hostname.includes(':')) {
70
+ return true;
71
+ }
72
+ return false;
73
+ }
74
+ /**
75
+ * Check if hostname is a metadata IP.
76
+ */
77
+ export function isMetadataIp(hostname) {
78
+ // AWS/GCP/Azure metadata service
79
+ if (hostname === '169.254.169.254') {
80
+ return true;
81
+ }
82
+ // Link-local range (169.254.x.x)
83
+ if (hostname.startsWith('169.254.')) {
84
+ return true;
85
+ }
86
+ return false;
87
+ }
88
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.js","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,GAAW,EACX,UAGI,EAAE;IAEN,IAAI,MAAW,CAAC;IAEhB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,SAAS,CAAC,UAAU,CAAC,YAAY,EAAE,gBAAgB,GAAG,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,2CAA2C;IAC3C,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,CAAC,cAAc,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9F,qCAAqC;QACvC,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,SAAS,CAAC,UAAU,CAAC,YAAY,EAAE,uBAAuB,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACzF,CAAC;IACH,CAAC;IAED,uDAAuD;IACvD,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CAAC,UAAU,CAAC,YAAY,EAAE,sBAAsB,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IACxF,CAAC;IAED,oCAAoC;IACpC,IAAI,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CACjB,UAAU,CAAC,YAAY,EACvB,iCAAiC,MAAM,CAAC,QAAQ,EAAE,CACnD,CAAC;IACJ,CAAC;IAED,yCAAyC;IACzC,IAAI,OAAO,CAAC,aAAa,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,SAAS,CAAC,UAAU,CAAC,YAAY,EAAE,0BAA0B,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC5F,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,QAAgB;IACvC,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,OAAO,CACL,KAAK,KAAK,WAAW;QACrB,KAAK,KAAK,WAAW;QACrB,KAAK,KAAK,KAAK;QACf,KAAK,KAAK,OAAO;QACjB,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,CAC7B,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,WAAW,CAAC,QAAgB;IACnC,eAAe;IACf,IAAI,yBAAyB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,0CAA0C;IAC1C,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,qCAAqC;IACrC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,iCAAiC;IACjC,IAAI,QAAQ,KAAK,iBAAiB,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,iCAAiC;IACjC,IAAI,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @peac/jwks-cache - Edge-safe JWKS types
3
+ */
4
+ import type { SignatureVerifier } from '@peac/http-signatures';
5
+ /**
6
+ * JSON Web Key (JWK) structure for Ed25519 public keys.
7
+ */
8
+ export interface JWK {
9
+ /** Key type - must be "OKP" for Ed25519 */
10
+ kty: string;
11
+ /** Curve - must be "Ed25519" */
12
+ crv: string;
13
+ /** Public key (base64url) */
14
+ x: string;
15
+ /** Key ID (optional) */
16
+ kid?: string;
17
+ /** Key use (optional, e.g., "sig") */
18
+ use?: string;
19
+ /** Algorithm (optional, e.g., "EdDSA") */
20
+ alg?: string;
21
+ }
22
+ /**
23
+ * JSON Web Key Set (JWKS) structure.
24
+ */
25
+ export interface JWKS {
26
+ keys: JWK[];
27
+ }
28
+ /**
29
+ * Cache backend interface for pluggable storage.
30
+ */
31
+ export interface CacheBackend {
32
+ /**
33
+ * Get cached value.
34
+ * @param key - Cache key
35
+ * @returns Cached value or null if not found/expired
36
+ */
37
+ get(key: string): Promise<CacheEntry | null>;
38
+ /**
39
+ * Set cached value with TTL.
40
+ * @param key - Cache key
41
+ * @param value - Value to cache
42
+ */
43
+ set(key: string, value: CacheEntry): Promise<void>;
44
+ /**
45
+ * Delete cached value.
46
+ * @param key - Cache key
47
+ */
48
+ delete(key: string): Promise<void>;
49
+ }
50
+ /**
51
+ * Cache entry with metadata.
52
+ */
53
+ export interface CacheEntry {
54
+ /** Cached JWK */
55
+ jwk: JWK;
56
+ /** Cache expiration timestamp (Unix seconds) */
57
+ expiresAt: number;
58
+ /** ETag for conditional refresh (optional) */
59
+ etag?: string;
60
+ }
61
+ /**
62
+ * JWKS resolver options.
63
+ */
64
+ export interface ResolverOptions {
65
+ /** Cache backend (defaults to in-memory) */
66
+ cache?: CacheBackend;
67
+ /** Default TTL in seconds (defaults to 3600) */
68
+ defaultTtlSeconds?: number;
69
+ /** Max TTL in seconds (defaults to 86400) */
70
+ maxTtlSeconds?: number;
71
+ /** Min TTL in seconds (defaults to 60) */
72
+ minTtlSeconds?: number;
73
+ /** Fetch timeout in ms (defaults to 5000) */
74
+ timeoutMs?: number;
75
+ /** Max response size in bytes (defaults to 1MB) */
76
+ maxResponseBytes?: number;
77
+ /** Max keys in JWKS (defaults to 100) */
78
+ maxKeys?: number;
79
+ /** Optional allowlist callback for enterprise deployments */
80
+ isAllowedHost?: (host: string) => boolean;
81
+ /** Allow localhost in dev mode (defaults to false) */
82
+ allowLocalhost?: boolean;
83
+ }
84
+ /**
85
+ * Resolved key result.
86
+ */
87
+ export interface ResolvedKey {
88
+ /** The JWK */
89
+ jwk: JWK;
90
+ /** Which path resolved the key */
91
+ source: '/.well-known/jwks' | '/.well-known/jwks.json' | '/keys';
92
+ /** Whether result was from cache */
93
+ cached: boolean;
94
+ }
95
+ /**
96
+ * Key resolver function type for TAP integration.
97
+ * Returns a SignatureVerifier or null if key not found.
98
+ */
99
+ export type JwksKeyResolver = (issuer: string, keyid: string) => Promise<SignatureVerifier | null>;
100
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D;;GAEG;AACH,MAAM,WAAW,GAAG;IAClB,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,GAAG,EAAE,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAE7C;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnD;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,iBAAiB;IACjB,GAAG,EAAE,GAAG,CAAC;IACT,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,sDAAsD;IACtD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,cAAc;IACd,GAAG,EAAE,GAAG,CAAC;IACT,kCAAkC;IAClC,MAAM,EAAE,mBAAmB,GAAG,wBAAwB,GAAG,OAAO,CAAC;IACjE,oCAAoC;IACpC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @peac/jwks-cache - Edge-safe JWKS types
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@peac/jwks-cache",
3
+ "version": "0.9.18",
4
+ "description": "Edge-safe JWKS fetch and cache with SSRF protection",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest"
21
+ },
22
+ "keywords": [
23
+ "jwks",
24
+ "cache",
25
+ "ed25519",
26
+ "ssrf",
27
+ "edge",
28
+ "peac"
29
+ ],
30
+ "author": "PEAC Protocol Contributors",
31
+ "license": "Apache-2.0",
32
+ "bugs": {
33
+ "url": "https://github.com/peacprotocol/peac/issues"
34
+ },
35
+ "homepage": "https://peacprotocol.org",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/peacprotocol/peac.git",
39
+ "directory": "packages/jwks-cache"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@peac/http-signatures": "workspace:*"
46
+ },
47
+ "devDependencies": {
48
+ "typescript": "^5.3.0",
49
+ "vitest": "^2.0.0"
50
+ }
51
+ }