@rayspec/auth-core 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +105 -0
  2. package/dist/api-key.d.ts +28 -0
  3. package/dist/api-key.d.ts.map +1 -0
  4. package/dist/api-key.js +56 -0
  5. package/dist/api-key.js.map +1 -0
  6. package/dist/audit.d.ts +26 -0
  7. package/dist/audit.d.ts.map +1 -0
  8. package/dist/audit.js +11 -0
  9. package/dist/audit.js.map +1 -0
  10. package/dist/authz.d.ts +66 -0
  11. package/dist/authz.d.ts.map +1 -0
  12. package/dist/authz.js +112 -0
  13. package/dist/authz.js.map +1 -0
  14. package/dist/config.d.ts +23 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +42 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/deferrals.d.ts +33 -0
  19. package/dist/deferrals.d.ts.map +1 -0
  20. package/dist/deferrals.js +38 -0
  21. package/dist/deferrals.js.map +1 -0
  22. package/dist/dto.d.ts +337 -0
  23. package/dist/dto.d.ts.map +1 -0
  24. package/dist/dto.js +221 -0
  25. package/dist/dto.js.map +1 -0
  26. package/dist/email.d.ts +24 -0
  27. package/dist/email.d.ts.map +1 -0
  28. package/dist/email.js +46 -0
  29. package/dist/email.js.map +1 -0
  30. package/dist/errors.d.ts +87 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +104 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/index.d.ts +14 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +14 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/invite.d.ts +18 -0
  39. package/dist/invite.d.ts.map +1 -0
  40. package/dist/invite.js +33 -0
  41. package/dist/invite.js.map +1 -0
  42. package/dist/password.d.ts +17 -0
  43. package/dist/password.d.ts.map +1 -0
  44. package/dist/password.js +46 -0
  45. package/dist/password.js.map +1 -0
  46. package/dist/rate-limit.d.ts +93 -0
  47. package/dist/rate-limit.d.ts.map +1 -0
  48. package/dist/rate-limit.js +156 -0
  49. package/dist/rate-limit.js.map +1 -0
  50. package/dist/session.d.ts +18 -0
  51. package/dist/session.d.ts.map +1 -0
  52. package/dist/session.js +41 -0
  53. package/dist/session.js.map +1 -0
  54. package/dist/tokens.d.ts +117 -0
  55. package/dist/tokens.d.ts.map +1 -0
  56. package/dist/tokens.js +150 -0
  57. package/dist/tokens.js.map +1 -0
  58. package/package.json +33 -0
@@ -0,0 +1,18 @@
1
+ /** A freshly minted opaque session secret + the hash to persist. */
2
+ export interface MintedSessionSecret {
3
+ /** The opaque secret placed in the refresh cookie. Shown to the client ONCE per rotation. */
4
+ secret: string;
5
+ /** The HMAC hash to store in sessions.token_hash. */
6
+ tokenHash: string;
7
+ }
8
+ /** HMAC-SHA256 the opaque session secret with the boot-required pepper (hex digest). */
9
+ export declare function hashSessionSecret(secret: string, pepper?: string): string;
10
+ /** Mint a new opaque session secret (256-bit base64url) + its stored hash. */
11
+ export declare function mintSessionSecret(pepper?: string): MintedSessionSecret;
12
+ /** Constant-time compare a presented secret's hash against a stored hash. */
13
+ export declare function sessionSecretMatches(storedHash: string, presentedSecret: string, pepper?: string): boolean;
14
+ /** A fresh server-minted family id (refresh family). */
15
+ export declare function newFamilyId(): string;
16
+ /** A fresh server-minted JWT id (jti) for an access token. */
17
+ export declare function newJti(): string;
18
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAeA,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,6FAA6F;IAC7F,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,MAA0B,GAAG,MAAM,CAE5F;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,MAA0B,GAAG,mBAAmB,CAGzF;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM,EACvB,MAAM,GAAE,MAA0B,GACjC,OAAO,CAMT;AAED,wDAAwD;AACxD,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,8DAA8D;AAC9D,wBAAgB,MAAM,IAAI,MAAM,CAE/B"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Opaque server-session secret primitives (DB-free).
3
+ *
4
+ * The session cookie carries an OPAQUE secret minted server-side; only its HASH is stored
5
+ * (sessions.token_hash). The session id is server-minted too (no client-proposed id → no
6
+ * fixation). `family_id` binds a refresh family for reuse-detection + targeted revoke.
7
+ *
8
+ * Hashing: the opaque secret is a ≥256-bit CSPRNG value (high entropy), so a fast keyed HMAC
9
+ * (SHA-256, keyed by the api-key pepper) is the right primitive — NOT argon2id (reserved for
10
+ * low-entropy passwords). Verification is by exact hash equality on the indexed unique column
11
+ * (constant-time compare where a secret is presented).
12
+ */
13
+ import { createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
14
+ import { getApiKeyPepper } from './api-key.js';
15
+ /** HMAC-SHA256 the opaque session secret with the boot-required pepper (hex digest). */
16
+ export function hashSessionSecret(secret, pepper = getApiKeyPepper()) {
17
+ return createHmac('sha256', pepper).update(secret).digest('hex');
18
+ }
19
+ /** Mint a new opaque session secret (256-bit base64url) + its stored hash. */
20
+ export function mintSessionSecret(pepper = getApiKeyPepper()) {
21
+ const secret = randomBytes(32).toString('base64url'); // 256-bit
22
+ return { secret, tokenHash: hashSessionSecret(secret, pepper) };
23
+ }
24
+ /** Constant-time compare a presented secret's hash against a stored hash. */
25
+ export function sessionSecretMatches(storedHash, presentedSecret, pepper = getApiKeyPepper()) {
26
+ const computed = hashSessionSecret(presentedSecret, pepper);
27
+ const a = Buffer.from(computed, 'utf8');
28
+ const b = Buffer.from(storedHash, 'utf8');
29
+ if (a.length !== b.length)
30
+ return false;
31
+ return timingSafeEqual(a, b);
32
+ }
33
+ /** A fresh server-minted family id (refresh family). */
34
+ export function newFamilyId() {
35
+ return randomUUID();
36
+ }
37
+ /** A fresh server-minted JWT id (jti) for an access token. */
38
+ export function newJti() {
39
+ return randomUUID();
40
+ }
41
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAU/C,wFAAwF;AACxF,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,SAAiB,eAAe,EAAE;IAClF,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,SAAiB,eAAe,EAAE;IAClE,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU;IAChE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;AAClE,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,oBAAoB,CAClC,UAAkB,EAClB,eAAuB,EACvB,SAAiB,eAAe,EAAE;IAElC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC5D,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,WAAW;IACzB,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,MAAM;IACpB,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Access-token module — short-lived RFC-9068 JWTs (jose), DB-free + unit-testable.
3
+ *
4
+ * Opaque server sessions + SHORT JWT access tokens. The JWT is the stateless, aud+scope-
5
+ * bound credential a resource server can verify WITHOUT a DB hit; the opaque session (sessions
6
+ * table) is the single revocation point. Tokens are NEVER persisted.
7
+ *
8
+ * Verified doc-first (2026-06-22) against jose 6.2.3 (everything re-exported from the main entry):
9
+ * - new SignJWT(claims).setProtectedHeader({alg, kid, typ}).setIssuer/Audience/IssuedAt/
10
+ * ExpirationTime/Jti(...).sign(privateKey)
11
+ * - jwtVerify(token, keyOrJWKS, { issuer, audience, clockTolerance, typ })
12
+ * - importPKCS8(pem, alg) / generateKeyPair(alg) / exportJWK(publicKey) / createLocalJWKSet
13
+ *
14
+ * CLAIMS: iss, sub (userId), aud, scope (space-delimited), org_id, mship_role,
15
+ * jti, iat, exp. exp ~8 min; verify clockTolerance 30s. `typ` is `at+jwt` (RFC 9068).
16
+ *
17
+ * KEY ROTATION (documented): each signer has a `kid` (the public-key JWK thumbprint). To rotate,
18
+ * publish a NEW kid in the JWKS, sign new tokens under it, and KEEP the old public key in the
19
+ * JWKS for verification until every token minted under it has expired (≥ TTL + clockTolerance).
20
+ * Then retire the old key. A token's `kid` header selects its verification key, so old + new
21
+ * coexist during the overlap window and rotation is never an auth outage.
22
+ */
23
+ import { importPKCS8 } from 'jose';
24
+ /** Issuer — the RaySpec auth control plane. */
25
+ export declare const TOKEN_ISSUER = "rayspec-auth";
26
+ /** The control-plane API audience (this server). The agent runtime uses a DIFFERENT aud. */
27
+ export declare const AUDIENCE_API = "rayspec-api";
28
+ /** The agent-runtime resource-server audience (forward-compatible; not minted by default). */
29
+ export declare const AUDIENCE_AGENT_RUNTIME = "rayspec-agent-runtime";
30
+ /** Access-token TTL (~8 min) and verify clock tolerance (~30s). */
31
+ export declare const ACCESS_TOKEN_TTL_SECONDS: number;
32
+ export declare const CLOCK_TOLERANCE_SECONDS = 30;
33
+ /** RFC 9068 access-token media type — set in the JWT `typ` header and checked on verify. */
34
+ export declare const ACCESS_TOKEN_TYP = "at+jwt";
35
+ /** Supported signing algorithms (RS256 primary, EdDSA accepted). */
36
+ export type SigningAlg = 'RS256' | 'EdDSA';
37
+ /**
38
+ * The private-key type jose hands back from importPKCS8 / generateKeyPair. Derived from the API
39
+ * rather than naming the WebCrypto `CryptoKey` global (our tsconfig lib is ES2023, no DOM types),
40
+ * so the signer is typed without pulling in the DOM lib.
41
+ */
42
+ type JosePrivateKey = Awaited<ReturnType<typeof importPKCS8>>;
43
+ /** The claims a RaySpec access token carries (beyond the registered iss/aud/sub/iat/exp/jti). */
44
+ export interface AccessTokenClaims {
45
+ /** Subject — the global user id. */
46
+ userId: string;
47
+ /** The active org (tenant) this token is scoped to (may be undefined before an org is chosen). */
48
+ orgId?: string;
49
+ /** The caller's membership role in `orgId` at mint time (read-staleness bounded; never trusted
50
+ * for sensitive writes — those re-check live, see authz.ts). */
51
+ mshipRole?: string;
52
+ /** OAuth-style scopes granted to this token. */
53
+ scopes?: string[];
54
+ /** Audience (defaults to the control-plane API). */
55
+ audience?: string;
56
+ }
57
+ /** A verified token's normalized claims (what callers read after verifyAccessToken). */
58
+ export interface VerifiedAccessToken {
59
+ userId: string;
60
+ orgId?: string;
61
+ mshipRole?: string;
62
+ scopes: string[];
63
+ jti?: string;
64
+ audience: string;
65
+ }
66
+ /**
67
+ * A token signer bound to ONE key (one kid). Construct via `createSigner` (from a PEM) or
68
+ * `createSignerFromKeyPair` (tests). Holds the private key for signing + the public JWK for the
69
+ * JWKS endpoint. The verifier resolves a token's `kid` against a key set (see JwksProvider).
70
+ */
71
+ export declare class TokenSigner {
72
+ readonly kid: string;
73
+ readonly alg: SigningAlg;
74
+ private readonly privateKey;
75
+ private readonly publicJwk;
76
+ /** The access-token TTL (seconds) this signer mints with. Default ACCESS_TOKEN_TTL_SECONDS (480). */
77
+ private readonly ttlSeconds;
78
+ constructor(kid: string, alg: SigningAlg, privateKey: JosePrivateKey, publicJwk: Record<string, unknown>, ttlSeconds?: number);
79
+ /** Build a signer from a private + public CryptoKey pair (used by tests + createSigner). */
80
+ static fromKeys(privateKey: JosePrivateKey, publicKey: JosePrivateKey, alg: SigningAlg, ttlSeconds?: number): Promise<TokenSigner>;
81
+ /** The public JWK (with kid/use/alg) to publish in the JWKS endpoint. */
82
+ publicKeyJwk(): Record<string, unknown>;
83
+ /**
84
+ * The configured access-token TTL in seconds (default ACCESS_TOKEN_TTL_SECONDS). Callers (the auth
85
+ * routes) report THIS in the response `expiresIn` so it can never drift from the minted token's `exp`.
86
+ */
87
+ get accessTokenTtlSeconds(): number;
88
+ /** Mint a signed access token. exp = now + the signer's configured ttlSeconds (default ACCESS_TOKEN_TTL_SECONDS). */
89
+ mint(claims: AccessTokenClaims, jti: string): Promise<string>;
90
+ }
91
+ /**
92
+ * Build a signer from a PKCS#8 PEM private key + its algorithm. The PUBLIC key for the JWKS is
93
+ * derived from the private key via jose's import (RS256/EdDSA carry the public component).
94
+ * RS256 PKCS#8 is the documented default (RAYSPEC_JWT_SIGNING_KEY).
95
+ */
96
+ export declare function createSigner(pkcs8Pem: string, alg?: SigningAlg, ttlSeconds?: number): Promise<TokenSigner>;
97
+ /**
98
+ * The JWKS view + the verifier. Holds one or more public JWKs (the active signer plus any
99
+ * not-yet-retired rotated keys) and verifies a token's signature against the matching kid, its
100
+ * issuer, audience, exp (with clock tolerance), and the `at+jwt` type.
101
+ */
102
+ export declare class JwksProvider {
103
+ private readonly jwks;
104
+ constructor(publicJwks: Record<string, unknown>[]);
105
+ /** The public JWKS document served at GET /v1/oauth/jwks. */
106
+ toJwks(): {
107
+ keys: Record<string, unknown>[];
108
+ };
109
+ /**
110
+ * Verify an access token. Checks signature (by kid → key), issuer, audience, exp (±30s), and
111
+ * the RFC-9068 `typ`. Throws on any failure (caller maps to 401). `expectedAudience` defaults
112
+ * to the control-plane API audience — a token minted for a different aud is rejected here.
113
+ */
114
+ verify(token: string, expectedAudience?: string): Promise<VerifiedAccessToken>;
115
+ }
116
+ export {};
117
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
package/dist/tokens.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Access-token module — short-lived RFC-9068 JWTs (jose), DB-free + unit-testable.
3
+ *
4
+ * Opaque server sessions + SHORT JWT access tokens. The JWT is the stateless, aud+scope-
5
+ * bound credential a resource server can verify WITHOUT a DB hit; the opaque session (sessions
6
+ * table) is the single revocation point. Tokens are NEVER persisted.
7
+ *
8
+ * Verified doc-first (2026-06-22) against jose 6.2.3 (everything re-exported from the main entry):
9
+ * - new SignJWT(claims).setProtectedHeader({alg, kid, typ}).setIssuer/Audience/IssuedAt/
10
+ * ExpirationTime/Jti(...).sign(privateKey)
11
+ * - jwtVerify(token, keyOrJWKS, { issuer, audience, clockTolerance, typ })
12
+ * - importPKCS8(pem, alg) / generateKeyPair(alg) / exportJWK(publicKey) / createLocalJWKSet
13
+ *
14
+ * CLAIMS: iss, sub (userId), aud, scope (space-delimited), org_id, mship_role,
15
+ * jti, iat, exp. exp ~8 min; verify clockTolerance 30s. `typ` is `at+jwt` (RFC 9068).
16
+ *
17
+ * KEY ROTATION (documented): each signer has a `kid` (the public-key JWK thumbprint). To rotate,
18
+ * publish a NEW kid in the JWKS, sign new tokens under it, and KEEP the old public key in the
19
+ * JWKS for verification until every token minted under it has expired (≥ TTL + clockTolerance).
20
+ * Then retire the old key. A token's `kid` header selects its verification key, so old + new
21
+ * coexist during the overlap window and rotation is never an auth outage.
22
+ */
23
+ import { calculateJwkThumbprint, createLocalJWKSet, exportJWK, importPKCS8, jwtVerify, SignJWT, } from 'jose';
24
+ /** Issuer — the RaySpec auth control plane. */
25
+ export const TOKEN_ISSUER = 'rayspec-auth';
26
+ /** The control-plane API audience (this server). The agent runtime uses a DIFFERENT aud. */
27
+ export const AUDIENCE_API = 'rayspec-api';
28
+ /** The agent-runtime resource-server audience (forward-compatible; not minted by default). */
29
+ export const AUDIENCE_AGENT_RUNTIME = 'rayspec-agent-runtime';
30
+ /** Access-token TTL (~8 min) and verify clock tolerance (~30s). */
31
+ export const ACCESS_TOKEN_TTL_SECONDS = 8 * 60;
32
+ export const CLOCK_TOLERANCE_SECONDS = 30;
33
+ /** RFC 9068 access-token media type — set in the JWT `typ` header and checked on verify. */
34
+ export const ACCESS_TOKEN_TYP = 'at+jwt';
35
+ /**
36
+ * A token signer bound to ONE key (one kid). Construct via `createSigner` (from a PEM) or
37
+ * `createSignerFromKeyPair` (tests). Holds the private key for signing + the public JWK for the
38
+ * JWKS endpoint. The verifier resolves a token's `kid` against a key set (see JwksProvider).
39
+ */
40
+ export class TokenSigner {
41
+ kid;
42
+ alg;
43
+ privateKey;
44
+ publicJwk;
45
+ /** The access-token TTL (seconds) this signer mints with. Default ACCESS_TOKEN_TTL_SECONDS (480). */
46
+ ttlSeconds;
47
+ constructor(kid, alg, privateKey, publicJwk, ttlSeconds = ACCESS_TOKEN_TTL_SECONDS) {
48
+ this.kid = kid;
49
+ this.alg = alg;
50
+ this.privateKey = privateKey;
51
+ this.publicJwk = publicJwk;
52
+ this.ttlSeconds = ttlSeconds;
53
+ }
54
+ /** Build a signer from a private + public CryptoKey pair (used by tests + createSigner). */
55
+ static async fromKeys(privateKey, publicKey, alg, ttlSeconds = ACCESS_TOKEN_TTL_SECONDS) {
56
+ const publicJwk = (await exportJWK(publicKey));
57
+ const kid = await calculateJwkThumbprint(publicJwk);
58
+ publicJwk.kid = kid;
59
+ publicJwk.use = 'sig';
60
+ publicJwk.alg = alg;
61
+ return new TokenSigner(kid, alg, privateKey, publicJwk, ttlSeconds);
62
+ }
63
+ /** The public JWK (with kid/use/alg) to publish in the JWKS endpoint. */
64
+ publicKeyJwk() {
65
+ return { ...this.publicJwk };
66
+ }
67
+ /**
68
+ * The configured access-token TTL in seconds (default ACCESS_TOKEN_TTL_SECONDS). Callers (the auth
69
+ * routes) report THIS in the response `expiresIn` so it can never drift from the minted token's `exp`.
70
+ */
71
+ get accessTokenTtlSeconds() {
72
+ return this.ttlSeconds;
73
+ }
74
+ /** Mint a signed access token. exp = now + the signer's configured ttlSeconds (default ACCESS_TOKEN_TTL_SECONDS). */
75
+ async mint(claims, jti) {
76
+ const payload = {};
77
+ if (claims.orgId !== undefined)
78
+ payload.org_id = claims.orgId;
79
+ if (claims.mshipRole !== undefined)
80
+ payload.mship_role = claims.mshipRole;
81
+ payload.scope = (claims.scopes ?? []).join(' ');
82
+ return new SignJWT(payload)
83
+ .setProtectedHeader({ alg: this.alg, kid: this.kid, typ: ACCESS_TOKEN_TYP })
84
+ .setIssuer(TOKEN_ISSUER)
85
+ .setSubject(claims.userId)
86
+ .setAudience(claims.audience ?? AUDIENCE_API)
87
+ .setIssuedAt()
88
+ .setExpirationTime(`${this.ttlSeconds}s`)
89
+ .setJti(jti)
90
+ .sign(this.privateKey);
91
+ }
92
+ }
93
+ /**
94
+ * Build a signer from a PKCS#8 PEM private key + its algorithm. The PUBLIC key for the JWKS is
95
+ * derived from the private key via jose's import (RS256/EdDSA carry the public component).
96
+ * RS256 PKCS#8 is the documented default (RAYSPEC_JWT_SIGNING_KEY).
97
+ */
98
+ export async function createSigner(pkcs8Pem, alg = 'RS256', ttlSeconds = ACCESS_TOKEN_TTL_SECONDS) {
99
+ const privateKey = await importPKCS8(pkcs8Pem, alg, { extractable: true });
100
+ // Derive the public JWK from the private key: export the private JWK, strip the private fields.
101
+ const privJwk = (await exportJWK(privateKey));
102
+ const pubJwk = { ...privJwk };
103
+ // Remove private-key components so the JWKS never exposes private material.
104
+ for (const k of ['d', 'p', 'q', 'dp', 'dq', 'qi'])
105
+ delete pubJwk[k];
106
+ const kid = await calculateJwkThumbprint(pubJwk);
107
+ pubJwk.kid = kid;
108
+ pubJwk.use = 'sig';
109
+ pubJwk.alg = alg;
110
+ return new TokenSigner(kid, alg, privateKey, pubJwk, ttlSeconds);
111
+ }
112
+ /**
113
+ * The JWKS view + the verifier. Holds one or more public JWKs (the active signer plus any
114
+ * not-yet-retired rotated keys) and verifies a token's signature against the matching kid, its
115
+ * issuer, audience, exp (with clock tolerance), and the `at+jwt` type.
116
+ */
117
+ export class JwksProvider {
118
+ jwks;
119
+ constructor(publicJwks) {
120
+ this.jwks = { keys: publicJwks };
121
+ }
122
+ /** The public JWKS document served at GET /v1/oauth/jwks. */
123
+ toJwks() {
124
+ return { keys: this.jwks.keys.map((k) => ({ ...k })) };
125
+ }
126
+ /**
127
+ * Verify an access token. Checks signature (by kid → key), issuer, audience, exp (±30s), and
128
+ * the RFC-9068 `typ`. Throws on any failure (caller maps to 401). `expectedAudience` defaults
129
+ * to the control-plane API audience — a token minted for a different aud is rejected here.
130
+ */
131
+ async verify(token, expectedAudience = AUDIENCE_API) {
132
+ const jwkSet = createLocalJWKSet(this.jwks);
133
+ const { payload } = await jwtVerify(token, jwkSet, {
134
+ issuer: TOKEN_ISSUER,
135
+ audience: expectedAudience,
136
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
137
+ typ: ACCESS_TOKEN_TYP,
138
+ });
139
+ const scopeClaim = typeof payload.scope === 'string' ? payload.scope : '';
140
+ return {
141
+ userId: typeof payload.sub === 'string' ? payload.sub : '',
142
+ orgId: typeof payload.org_id === 'string' ? payload.org_id : undefined,
143
+ mshipRole: typeof payload.mship_role === 'string' ? payload.mship_role : undefined,
144
+ scopes: scopeClaim.length > 0 ? scopeClaim.split(' ') : [],
145
+ jti: typeof payload.jti === 'string' ? payload.jti : undefined,
146
+ audience: expectedAudience,
147
+ };
148
+ }
149
+ }
150
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@rayspec/auth-core",
3
+ "version": "1.5.0",
4
+ "private": false,
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "license": "FSL-1.1-ALv2",
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "dependencies": {
19
+ "argon2": "0.44.0",
20
+ "jose": "6.2.3",
21
+ "zod": "4.4.3"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "22.12.0",
25
+ "typescript": "5.9.3",
26
+ "vitest": "4.1.9"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -b",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit",
31
+ "test": "vitest run"
32
+ }
33
+ }