actor-gate 0.1.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 (155) hide show
  1. package/package.json +25 -0
  2. package/src/config/base-config.d.ts +17 -0
  3. package/src/config/base-config.js +33 -0
  4. package/src/config/index.d.ts +5 -0
  5. package/src/config/index.js +5 -0
  6. package/src/config/nextjs-public-config.d.ts +46 -0
  7. package/src/config/nextjs-public-config.js +89 -0
  8. package/src/config/nextjs-server-config.d.ts +32 -0
  9. package/src/config/nextjs-server-config.js +10 -0
  10. package/src/config/react-client.d.ts +23 -0
  11. package/src/config/react-client.js +69 -0
  12. package/src/config/react-config.d.ts +18 -0
  13. package/src/config/react-config.js +38 -0
  14. package/src/core/adapters/access-token-revocation-adapter.d.ts +8 -0
  15. package/src/core/adapters/access-token-revocation-adapter.js +1 -0
  16. package/src/core/adapters/access-token-transport-adapter.d.ts +15 -0
  17. package/src/core/adapters/access-token-transport-adapter.js +1 -0
  18. package/src/core/adapters/authorization-code-adapter.d.ts +21 -0
  19. package/src/core/adapters/authorization-code-adapter.js +1 -0
  20. package/src/core/adapters/authorization-hooks.d.ts +13 -0
  21. package/src/core/adapters/authorization-hooks.js +1 -0
  22. package/src/core/adapters/index.d.ts +14 -0
  23. package/src/core/adapters/index.js +1 -0
  24. package/src/core/adapters/login-method-adapter.d.ts +7 -0
  25. package/src/core/adapters/login-method-adapter.js +1 -0
  26. package/src/core/adapters/oauth-client-adapter.d.ts +13 -0
  27. package/src/core/adapters/oauth-client-adapter.js +1 -0
  28. package/src/core/adapters/oauth-client-management-adapter.d.ts +23 -0
  29. package/src/core/adapters/oauth-client-management-adapter.js +1 -0
  30. package/src/core/adapters/oauth-grant-type.d.ts +1 -0
  31. package/src/core/adapters/oauth-grant-type.js +1 -0
  32. package/src/core/adapters/oauth-policy.d.ts +9 -0
  33. package/src/core/adapters/oauth-policy.js +1 -0
  34. package/src/core/adapters/observability-hooks.d.ts +31 -0
  35. package/src/core/adapters/observability-hooks.js +1 -0
  36. package/src/core/adapters/pending-auth-request-adapter.d.ts +18 -0
  37. package/src/core/adapters/pending-auth-request-adapter.js +1 -0
  38. package/src/core/adapters/refresh-token-adapter.d.ts +24 -0
  39. package/src/core/adapters/refresh-token-adapter.js +1 -0
  40. package/src/core/adapters/session-adapter.d.ts +14 -0
  41. package/src/core/adapters/session-adapter.js +1 -0
  42. package/src/core/adapters/token-adapter.d.ts +15 -0
  43. package/src/core/adapters/token-adapter.js +1 -0
  44. package/src/core/http/bearer-challenge.d.ts +6 -0
  45. package/src/core/http/bearer-challenge.js +16 -0
  46. package/src/core/ids/id-codec.d.ts +6 -0
  47. package/src/core/ids/id-codec.js +30 -0
  48. package/src/core/index.d.ts +9 -0
  49. package/src/core/index.js +7 -0
  50. package/src/core/oauth/pkce.d.ts +9 -0
  51. package/src/core/oauth/pkce.js +30 -0
  52. package/src/core/services/access-token-service.d.ts +42 -0
  53. package/src/core/services/access-token-service.js +304 -0
  54. package/src/core/services/auth-error.d.ts +14 -0
  55. package/src/core/services/auth-error.js +47 -0
  56. package/src/core/services/contracts.d.ts +23 -0
  57. package/src/core/services/contracts.js +1 -0
  58. package/src/core/services/direct-auth-service.d.ts +50 -0
  59. package/src/core/services/direct-auth-service.js +267 -0
  60. package/src/core/services/index.d.ts +7 -0
  61. package/src/core/services/index.js +5 -0
  62. package/src/core/services/mcp-auth-service.d.ts +39 -0
  63. package/src/core/services/mcp-auth-service.js +170 -0
  64. package/src/core/services/oauth-service.d.ts +91 -0
  65. package/src/core/services/oauth-service.js +571 -0
  66. package/src/core/services/observability.d.ts +22 -0
  67. package/src/core/services/observability.js +71 -0
  68. package/src/core/services/revocation-policy.d.ts +21 -0
  69. package/src/core/services/revocation-policy.js +51 -0
  70. package/src/core/sessions/client-session.d.ts +7 -0
  71. package/src/core/sessions/client-session.js +18 -0
  72. package/src/core/tokens/access-claims.d.ts +21 -0
  73. package/src/core/tokens/access-claims.js +128 -0
  74. package/src/core/tokens/id-claims.d.ts +20 -0
  75. package/src/core/tokens/id-claims.js +25 -0
  76. package/src/core/types/auth-contract.d.ts +33 -0
  77. package/src/core/types/auth-contract.js +1 -0
  78. package/src/express/index.d.ts +1 -0
  79. package/src/express/index.js +1 -0
  80. package/src/express/protected-route.d.ts +44 -0
  81. package/src/express/protected-route.js +119 -0
  82. package/src/index.d.ts +8 -0
  83. package/src/index.js +8 -0
  84. package/src/mcp/index.d.ts +1 -0
  85. package/src/mcp/index.js +1 -0
  86. package/src/mcp/json-rpc-auth.d.ts +5 -0
  87. package/src/mcp/json-rpc-auth.js +41 -0
  88. package/src/next/app/catch-all.d.ts +32 -0
  89. package/src/next/app/catch-all.js +82 -0
  90. package/src/next/app/cookies.d.ts +22 -0
  91. package/src/next/app/cookies.js +36 -0
  92. package/src/next/app/direct-auth-handlers.d.ts +55 -0
  93. package/src/next/app/direct-auth-handlers.js +419 -0
  94. package/src/next/app/index.d.ts +8 -0
  95. package/src/next/app/index.js +8 -0
  96. package/src/next/app/mcp-oauth-handlers.d.ts +74 -0
  97. package/src/next/app/mcp-oauth-handlers.js +365 -0
  98. package/src/next/app/protected-route.d.ts +27 -0
  99. package/src/next/app/protected-route.js +59 -0
  100. package/src/next/app/request.d.ts +12 -0
  101. package/src/next/app/request.js +30 -0
  102. package/src/next/app/response.d.ts +16 -0
  103. package/src/next/app/response.js +48 -0
  104. package/src/next/app/wrapper.d.ts +28 -0
  105. package/src/next/app/wrapper.js +78 -0
  106. package/src/next/index.d.ts +6 -0
  107. package/src/next/index.js +5 -0
  108. package/src/next/pages/catch-all.d.ts +19 -0
  109. package/src/next/pages/catch-all.js +60 -0
  110. package/src/next/pages/cookies.d.ts +41 -0
  111. package/src/next/pages/cookies.js +87 -0
  112. package/src/next/pages/direct-auth-handlers.d.ts +58 -0
  113. package/src/next/pages/direct-auth-handlers.js +425 -0
  114. package/src/next/pages/index.d.ts +8 -0
  115. package/src/next/pages/index.js +8 -0
  116. package/src/next/pages/mcp-oauth-handlers.d.ts +77 -0
  117. package/src/next/pages/mcp-oauth-handlers.js +341 -0
  118. package/src/next/pages/protected-route.d.ts +28 -0
  119. package/src/next/pages/protected-route.js +59 -0
  120. package/src/next/pages/request.d.ts +14 -0
  121. package/src/next/pages/request.js +66 -0
  122. package/src/next/pages/response.d.ts +28 -0
  123. package/src/next/pages/response.js +29 -0
  124. package/src/next/pages/wrapper.d.ts +29 -0
  125. package/src/next/pages/wrapper.js +74 -0
  126. package/src/next/rewrites.d.ts +12 -0
  127. package/src/next/rewrites.js +74 -0
  128. package/src/next/shared/auth-http.d.ts +24 -0
  129. package/src/next/shared/auth-http.js +42 -0
  130. package/src/next/shared/auth-routes.d.ts +17 -0
  131. package/src/next/shared/auth-routes.js +153 -0
  132. package/src/next/shared/direct-auth-utils.d.ts +71 -0
  133. package/src/next/shared/direct-auth-utils.js +275 -0
  134. package/src/next/shared/oauth-utils.d.ts +45 -0
  135. package/src/next/shared/oauth-utils.js +308 -0
  136. package/src/next/shared/well-known-utils.d.ts +46 -0
  137. package/src/next/shared/well-known-utils.js +108 -0
  138. package/src/testing/in-memory/in-memory-access-token-revocation-adapter.d.ts +2 -0
  139. package/src/testing/in-memory/in-memory-access-token-revocation-adapter.js +14 -0
  140. package/src/testing/in-memory/in-memory-authorization-code-adapter.d.ts +2 -0
  141. package/src/testing/in-memory/in-memory-authorization-code-adapter.js +36 -0
  142. package/src/testing/in-memory/in-memory-oauth-client-adapter.d.ts +14 -0
  143. package/src/testing/in-memory/in-memory-oauth-client-adapter.js +26 -0
  144. package/src/testing/in-memory/in-memory-pending-auth-request-adapter.d.ts +2 -0
  145. package/src/testing/in-memory/in-memory-pending-auth-request-adapter.js +43 -0
  146. package/src/testing/in-memory/in-memory-refresh-token-adapter.d.ts +2 -0
  147. package/src/testing/in-memory/in-memory-refresh-token-adapter.js +67 -0
  148. package/src/testing/in-memory/in-memory-session-adapter.d.ts +6 -0
  149. package/src/testing/in-memory/in-memory-session-adapter.js +43 -0
  150. package/src/testing/in-memory/index.d.ts +7 -0
  151. package/src/testing/in-memory/index.js +7 -0
  152. package/src/testing/in-memory/test-fixtures.d.ts +5 -0
  153. package/src/testing/in-memory/test-fixtures.js +18 -0
  154. package/src/testing/index.d.ts +2 -0
  155. package/src/testing/index.js +4 -0
@@ -0,0 +1,18 @@
1
+ export interface PendingAuthRequestAdapter<TPendingAuthRequest> {
2
+ create(input: {
3
+ requestId: string;
4
+ payload: TPendingAuthRequest;
5
+ expiresAtUnix: number;
6
+ }): Promise<void>;
7
+ find(requestId: string): Promise<{
8
+ payload: TPendingAuthRequest;
9
+ expiresAtUnix: number;
10
+ consumedAtUnix?: number | null;
11
+ } | null>;
12
+ consume(requestId: string, nowUnix: number): Promise<{
13
+ payload: TPendingAuthRequest;
14
+ expiresAtUnix: number;
15
+ consumedAtUnix?: number | null;
16
+ } | null>;
17
+ delete(requestId: string): Promise<void>;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ export interface RefreshTokenAdapter<TSessionId> {
2
+ create(input: {
3
+ sessionId: TSessionId;
4
+ tokenHash: string;
5
+ expiresAtUnix: number;
6
+ familyId: string;
7
+ parentTokenHash?: string;
8
+ }): Promise<void>;
9
+ find(tokenHash: string): Promise<{
10
+ sessionId: TSessionId;
11
+ familyId: string;
12
+ expiresAtUnix: number;
13
+ revokedAtUnix?: number | null;
14
+ } | null>;
15
+ consume(tokenHash: string, nowUnix: number): Promise<{
16
+ sessionId: TSessionId;
17
+ familyId: string;
18
+ expiresAtUnix: number;
19
+ consumedAtUnix?: number | null;
20
+ revokedAtUnix?: number | null;
21
+ } | null>;
22
+ revoke(tokenHash: string, nowUnix: number): Promise<void>;
23
+ revokeFamily(familyId: string, nowUnix: number): Promise<void>;
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { AuthActor, SessionRecord } from '../types/auth-contract';
2
+ export interface SessionAdapter<TSessionId, TUserId, TActor extends AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>> {
3
+ create(input: {
4
+ userId: TUserId;
5
+ actor: TActor;
6
+ clientId?: string;
7
+ userAgent?: string;
8
+ ipAddress?: string;
9
+ ttlSeconds: number;
10
+ serverSessionData?: TServerSessionData;
11
+ }): Promise<SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>>;
12
+ findById(sessionId: TSessionId): Promise<SessionRecord<TSessionId, TUserId, TActor, TServerSessionData> | null>;
13
+ revoke(sessionId: TSessionId, nowUnix: number): Promise<void>;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { AccessClaims, AuthActor } from '../types/auth-contract';
2
+ export interface TokenAdapter<TActor extends AuthActor = AuthActor, TExtClaims extends Record<string, unknown> = Record<string, never>> {
3
+ signAccessToken(input: {
4
+ sub: string;
5
+ uid: string;
6
+ actor: TActor;
7
+ cid?: string;
8
+ ver: number;
9
+ jti: string;
10
+ audience: string;
11
+ expiresInSeconds: number;
12
+ ext?: TExtClaims;
13
+ }): string;
14
+ verifyAccessToken(token: string): AccessClaims<TActor, TExtClaims>;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export type BearerChallengeOptions = {
2
+ error?: string;
3
+ resourceMetadataUrl?: string;
4
+ scope?: string;
5
+ };
6
+ export declare function buildBearerChallengeHeader(options?: BearerChallengeOptions): string;
@@ -0,0 +1,16 @@
1
+ function escapeQuotedAttribute(value) {
2
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
3
+ }
4
+ function appendQuotedAttribute(attributes, key, value) {
5
+ if (!value) {
6
+ return;
7
+ }
8
+ attributes.push(`${key}="${escapeQuotedAttribute(value)}"`);
9
+ }
10
+ export function buildBearerChallengeHeader(options = {}) {
11
+ const headerParts = ['Bearer'];
12
+ appendQuotedAttribute(headerParts, 'scope', options.scope);
13
+ appendQuotedAttribute(headerParts, 'resource_metadata', options.resourceMetadataUrl);
14
+ appendQuotedAttribute(headerParts, 'error', options.error);
15
+ return headerParts.join(', ');
16
+ }
@@ -0,0 +1,6 @@
1
+ export interface IdCodec<T> {
2
+ toClaim(value: T): string;
3
+ fromClaim(value: string): T;
4
+ }
5
+ export declare const stringIdCodec: IdCodec<string>;
6
+ export declare const numberIdCodec: IdCodec<number>;
@@ -0,0 +1,30 @@
1
+ function isCanonicalPositiveIntegerClaim(value) {
2
+ return /^[1-9]\d*$/u.test(value);
3
+ }
4
+ function assertSafePositiveInteger(value, operation) {
5
+ if (!Number.isSafeInteger(value) || value <= 0) {
6
+ throw new Error(`${operation} expects a positive safe integer.`);
7
+ }
8
+ }
9
+ export const stringIdCodec = {
10
+ toClaim(value) {
11
+ return value;
12
+ },
13
+ fromClaim(value) {
14
+ return value;
15
+ },
16
+ };
17
+ export const numberIdCodec = {
18
+ toClaim(value) {
19
+ assertSafePositiveInteger(value, 'numberIdCodec.toClaim');
20
+ return String(value);
21
+ },
22
+ fromClaim(value) {
23
+ if (!isCanonicalPositiveIntegerClaim(value)) {
24
+ throw new Error('numberIdCodec.fromClaim expects a positive integer claim.');
25
+ }
26
+ const parsed = Number(value);
27
+ assertSafePositiveInteger(parsed, 'numberIdCodec.fromClaim');
28
+ return parsed;
29
+ },
30
+ };
@@ -0,0 +1,9 @@
1
+ export { buildBearerChallengeHeader, type BearerChallengeOptions, } from './http/bearer-challenge';
2
+ export { base64UrlEncode, createS256CodeChallenge, verifyPkce, type PkceCodeMethod, type VerifyPkceParams, } from './oauth/pkce';
3
+ export { numberIdCodec, stringIdCodec, type IdCodec } from './ids/id-codec';
4
+ export { decodeSubjectClaims, encodeSubjectClaims, type AuthContext, type SubjectClaims, } from './tokens/id-claims';
5
+ export * from './services/index';
6
+ export { DEFAULT_TOKEN_CLAIMS_VERSION, buildAccessClaims, parseAccessClaims, type BuildAccessClaimsInput, } from './tokens/access-claims';
7
+ export { buildClientSession } from './sessions/client-session';
8
+ export type { AccessClaims, AuthActor, ClientSession, SessionRecord, UserActor, } from './types/auth-contract';
9
+ export type * from './adapters/index';
@@ -0,0 +1,7 @@
1
+ export { buildBearerChallengeHeader, } from './http/bearer-challenge';
2
+ export { base64UrlEncode, createS256CodeChallenge, verifyPkce, } from './oauth/pkce';
3
+ export { numberIdCodec, stringIdCodec } from './ids/id-codec';
4
+ export { decodeSubjectClaims, encodeSubjectClaims, } from './tokens/id-claims';
5
+ export * from './services/index';
6
+ export { DEFAULT_TOKEN_CLAIMS_VERSION, buildAccessClaims, parseAccessClaims, } from './tokens/access-claims';
7
+ export { buildClientSession } from './sessions/client-session';
@@ -0,0 +1,9 @@
1
+ export type PkceCodeMethod = 'plain' | 'S256';
2
+ export type VerifyPkceParams = {
3
+ codeVerifier: string;
4
+ codeChallenge: string;
5
+ codeMethod: string;
6
+ };
7
+ export declare function base64UrlEncode(value: Uint8Array): string;
8
+ export declare function createS256CodeChallenge(codeVerifier: string): string;
9
+ export declare function verifyPkce(params: VerifyPkceParams): boolean;
@@ -0,0 +1,30 @@
1
+ import { createHash, timingSafeEqual } from 'node:crypto';
2
+ export function base64UrlEncode(value) {
3
+ return Buffer.from(value)
4
+ .toString('base64')
5
+ .replace(/\+/g, '-')
6
+ .replace(/\//g, '_')
7
+ .replace(/=+$/u, '');
8
+ }
9
+ export function createS256CodeChallenge(codeVerifier) {
10
+ const digest = createHash('sha256').update(codeVerifier, 'utf8').digest();
11
+ return base64UrlEncode(digest);
12
+ }
13
+ function constantTimeEqual(left, right) {
14
+ const leftBuffer = Buffer.from(left, 'utf8');
15
+ const rightBuffer = Buffer.from(right, 'utf8');
16
+ if (leftBuffer.length !== rightBuffer.length) {
17
+ return false;
18
+ }
19
+ return timingSafeEqual(leftBuffer, rightBuffer);
20
+ }
21
+ export function verifyPkce(params) {
22
+ if (params.codeMethod === 'plain') {
23
+ return constantTimeEqual(params.codeVerifier, params.codeChallenge);
24
+ }
25
+ if (params.codeMethod === 'S256') {
26
+ const computedChallenge = createS256CodeChallenge(params.codeVerifier);
27
+ return constantTimeEqual(computedChallenge, params.codeChallenge);
28
+ }
29
+ return false;
30
+ }
@@ -0,0 +1,42 @@
1
+ import type { AccessTokenRevocationAdapter } from '../adapters/access-token-revocation-adapter';
2
+ import type { ObservabilityConfig } from '../adapters/observability-hooks';
3
+ import type { SessionAdapter } from '../adapters/session-adapter';
4
+ import type { TokenAdapter } from '../adapters/token-adapter';
5
+ import type { IdCodec } from '../ids/id-codec';
6
+ import type { AuthActor, SessionRecord } from '../types/auth-contract';
7
+ import type { IssuedAccessToken, ValidatedAccessTokenResult } from './contracts';
8
+ import { type RevocationGuarantee, type SessionMode } from './revocation-policy';
9
+ export type AccessTokenService<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
10
+ issueAccessToken(input: {
11
+ session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
12
+ requestId?: string;
13
+ }): Promise<IssuedAccessToken<TSessionId, TUserId, TActor, TExtClaims, TClientSessionData>>;
14
+ validateAccessToken(input: {
15
+ accessToken: string;
16
+ requestId?: string;
17
+ expectedAudience?: string;
18
+ allowedActors?: readonly TActor[] | ReadonlySet<TActor>;
19
+ }): Promise<ValidatedAccessTokenResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>>;
20
+ };
21
+ export type CreateAccessTokenServiceInput<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
22
+ tokenAdapter: TokenAdapter<TActor, TExtClaims>;
23
+ sessionIdCodec: IdCodec<TSessionId>;
24
+ userIdCodec: IdCodec<TUserId>;
25
+ sessionAdapter?: SessionAdapter<TSessionId, TUserId, TActor, TServerSessionData>;
26
+ accessTokenRevocationAdapter?: AccessTokenRevocationAdapter;
27
+ sessionMode?: SessionMode;
28
+ revocationGuarantee?: RevocationGuarantee;
29
+ boundedRevocationCacheSeconds?: number;
30
+ audience: string;
31
+ accessTokenTtlSeconds: number;
32
+ tokenClaimsVersion?: number;
33
+ nowUnix?: () => number;
34
+ buildExtClaims?: (input: {
35
+ session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
36
+ }) => TExtClaims;
37
+ buildClientSessionData?: (input: {
38
+ session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
39
+ }) => TClientSessionData;
40
+ observability?: ObservabilityConfig<TActor>;
41
+ };
42
+ export declare function createAccessTokenService<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>>(input: CreateAccessTokenServiceInput<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>): AccessTokenService<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
@@ -0,0 +1,304 @@
1
+ import { buildClientSession } from '../sessions/client-session';
2
+ import { decodeSubjectClaims } from '../tokens/id-claims';
3
+ import { buildAccessClaims, parseAccessClaims } from '../tokens/access-claims';
4
+ import { AuthServiceError, isAuthServiceError } from './auth-error';
5
+ import { createRevocationDecisionEngine, shouldValidateSession, } from './revocation-policy';
6
+ import { emitAuditEvent, emitMetric, reportServiceError, } from './observability';
7
+ function assertPositiveSafeInteger(value, fieldName) {
8
+ if (!Number.isSafeInteger(value) || value <= 0) {
9
+ throw new AuthServiceError({
10
+ code: 'invalid_request',
11
+ message: `${fieldName} must be a positive safe integer.`,
12
+ });
13
+ }
14
+ }
15
+ function normalizeAllowedActors(actors) {
16
+ if (actors === undefined) {
17
+ return null;
18
+ }
19
+ if (actors instanceof Set) {
20
+ return actors;
21
+ }
22
+ return new Set(actors);
23
+ }
24
+ export function createAccessTokenService(input) {
25
+ if (input.audience.length === 0) {
26
+ throw new AuthServiceError({
27
+ code: 'invalid_request',
28
+ message: 'audience must be a non-empty string.',
29
+ });
30
+ }
31
+ assertPositiveSafeInteger(input.accessTokenTtlSeconds, 'accessTokenTtlSeconds');
32
+ const sessionMode = input.sessionMode ?? 'hybrid';
33
+ const revocationGuarantee = input.revocationGuarantee ?? 'strict';
34
+ const boundedRevocationCacheSeconds = input.boundedRevocationCacheSeconds ?? 30;
35
+ if (sessionMode === 'stateful' && !input.sessionAdapter) {
36
+ throw new AuthServiceError({
37
+ code: 'invalid_request',
38
+ message: 'sessionAdapter is required when sessionMode is stateful.',
39
+ });
40
+ }
41
+ if (revocationGuarantee === 'bounded') {
42
+ assertPositiveSafeInteger(boundedRevocationCacheSeconds, 'boundedRevocationCacheSeconds');
43
+ }
44
+ const nowUnix = input.nowUnix ?? (() => Math.floor(Date.now() / 1000));
45
+ const revocationDecisionEngine = createRevocationDecisionEngine();
46
+ return {
47
+ async issueAccessToken({ session, requestId }) {
48
+ const issuedAtUnix = nowUnix();
49
+ try {
50
+ const claims = buildAccessClaims({
51
+ session,
52
+ sessionIdCodec: input.sessionIdCodec,
53
+ userIdCodec: input.userIdCodec,
54
+ audience: input.audience,
55
+ accessTokenTtlSeconds: input.accessTokenTtlSeconds,
56
+ ...(input.tokenClaimsVersion === undefined
57
+ ? {}
58
+ : { tokenClaimsVersion: input.tokenClaimsVersion }),
59
+ nowUnix: issuedAtUnix,
60
+ ...(input.buildExtClaims === undefined
61
+ ? {}
62
+ : { buildExtClaims: input.buildExtClaims }),
63
+ });
64
+ const accessToken = input.tokenAdapter.signAccessToken({
65
+ sub: claims.sub,
66
+ uid: claims.uid,
67
+ actor: claims.actor,
68
+ ...(claims.cid === undefined ? {} : { cid: claims.cid }),
69
+ ver: claims.ver,
70
+ jti: claims.jti,
71
+ audience: claims.aud,
72
+ expiresInSeconds: claims.exp - claims.iat,
73
+ ...(claims.ext === undefined ? {} : { ext: claims.ext }),
74
+ });
75
+ const clientSession = buildClientSession({
76
+ session,
77
+ ...(input.buildClientSessionData === undefined
78
+ ? {}
79
+ : { buildClientSessionData: input.buildClientSessionData }),
80
+ });
81
+ await emitAuditEvent({
82
+ observability: input.observability,
83
+ requestId,
84
+ event: {
85
+ name: 'access_token_issued',
86
+ atUnix: issuedAtUnix,
87
+ actor: claims.actor,
88
+ userId: claims.uid,
89
+ ...(claims.cid === undefined ? {} : { clientId: claims.cid }),
90
+ sessionId: claims.sub,
91
+ jti: claims.jti,
92
+ },
93
+ });
94
+ await emitMetric({
95
+ observability: input.observability,
96
+ requestId,
97
+ metric: {
98
+ name: 'auth.access_token.issued',
99
+ value: 1,
100
+ tags: { actor: String(claims.actor) },
101
+ },
102
+ });
103
+ return {
104
+ accessToken,
105
+ claims,
106
+ clientSession,
107
+ };
108
+ }
109
+ catch (error) {
110
+ if (isAuthServiceError(error)) {
111
+ throw error;
112
+ }
113
+ await reportServiceError({
114
+ observability: input.observability,
115
+ where: 'access-token-service.issueAccessToken',
116
+ error,
117
+ requestId,
118
+ });
119
+ throw new AuthServiceError({
120
+ code: 'system_error',
121
+ message: 'Failed to issue access token.',
122
+ cause: error,
123
+ });
124
+ }
125
+ },
126
+ async validateAccessToken({ accessToken, requestId, expectedAudience, allowedActors, }) {
127
+ const now = nowUnix();
128
+ let parsedClaims;
129
+ const reject = async (error) => {
130
+ await emitAuditEvent({
131
+ observability: input.observability,
132
+ requestId,
133
+ event: {
134
+ name: 'access_token_rejected',
135
+ atUnix: now,
136
+ ...(parsedClaims === undefined
137
+ ? {}
138
+ : {
139
+ actor: parsedClaims.actor,
140
+ userId: parsedClaims.uid,
141
+ clientId: parsedClaims.cid,
142
+ sessionId: parsedClaims.sub,
143
+ jti: parsedClaims.jti,
144
+ }),
145
+ reason: error.code,
146
+ },
147
+ });
148
+ await emitMetric({
149
+ observability: input.observability,
150
+ requestId,
151
+ metric: {
152
+ name: 'auth.access_token.rejected',
153
+ value: 1,
154
+ tags: { code: error.code },
155
+ },
156
+ });
157
+ throw error;
158
+ };
159
+ try {
160
+ try {
161
+ parsedClaims = parseAccessClaims({
162
+ claims: input.tokenAdapter.verifyAccessToken(accessToken),
163
+ ...(input.tokenClaimsVersion === undefined
164
+ ? {}
165
+ : { tokenClaimsVersion: input.tokenClaimsVersion }),
166
+ });
167
+ }
168
+ catch (error) {
169
+ return reject(new AuthServiceError({
170
+ code: 'invalid_token',
171
+ message: 'Access token verification failed.',
172
+ cause: error,
173
+ }));
174
+ }
175
+ const audience = expectedAudience ?? input.audience;
176
+ if (parsedClaims.aud !== audience) {
177
+ return reject(new AuthServiceError({
178
+ code: 'invalid_token',
179
+ message: 'Access token audience mismatch.',
180
+ }));
181
+ }
182
+ if (parsedClaims.exp <= now) {
183
+ return reject(new AuthServiceError({
184
+ code: 'token_expired',
185
+ message: 'Access token has expired.',
186
+ }));
187
+ }
188
+ const allowedActorSet = normalizeAllowedActors(allowedActors);
189
+ if (allowedActorSet && !allowedActorSet.has(parsedClaims.actor)) {
190
+ return reject(new AuthServiceError({
191
+ code: 'forbidden',
192
+ message: 'Actor is not allowed for this resource.',
193
+ }));
194
+ }
195
+ if (await revocationDecisionEngine.isAccessTokenRevoked({
196
+ jti: parsedClaims.jti,
197
+ nowUnix: now,
198
+ revocationGuarantee,
199
+ boundedRevocationCacheSeconds,
200
+ ...(input.accessTokenRevocationAdapter === undefined
201
+ ? {}
202
+ : {
203
+ accessTokenRevocationAdapter: input.accessTokenRevocationAdapter,
204
+ }),
205
+ })) {
206
+ return reject(new AuthServiceError({
207
+ code: 'token_revoked',
208
+ message: 'Access token was revoked.',
209
+ }));
210
+ }
211
+ let authContext;
212
+ try {
213
+ authContext = decodeSubjectClaims({
214
+ claims: parsedClaims,
215
+ sessionIdCodec: input.sessionIdCodec,
216
+ userIdCodec: input.userIdCodec,
217
+ });
218
+ }
219
+ catch (error) {
220
+ return reject(new AuthServiceError({
221
+ code: 'invalid_token',
222
+ message: 'Access token subject claims are invalid.',
223
+ cause: error,
224
+ }));
225
+ }
226
+ let session;
227
+ let clientSession;
228
+ if (shouldValidateSession({
229
+ sessionMode,
230
+ hasSessionAdapter: input.sessionAdapter !== undefined,
231
+ })) {
232
+ const foundSession = await input.sessionAdapter.findById(authContext.sessionId);
233
+ if (!foundSession) {
234
+ return reject(new AuthServiceError({
235
+ code: 'session_not_found',
236
+ message: 'Session was not found.',
237
+ }));
238
+ }
239
+ session = foundSession;
240
+ if (session.userId !== authContext.userId) {
241
+ return reject(new AuthServiceError({
242
+ code: 'unauthorized',
243
+ message: 'Session user does not match token user.',
244
+ }));
245
+ }
246
+ if (session.actor !== parsedClaims.actor) {
247
+ return reject(new AuthServiceError({
248
+ code: 'unauthorized',
249
+ message: 'Session actor does not match token actor.',
250
+ }));
251
+ }
252
+ if (session.clientId !== parsedClaims.cid) {
253
+ return reject(new AuthServiceError({
254
+ code: 'unauthorized',
255
+ message: 'Session client does not match token client.',
256
+ }));
257
+ }
258
+ if (session.revokedAt !== undefined &&
259
+ session.revokedAt !== null &&
260
+ session.revokedAt <= now) {
261
+ return reject(new AuthServiceError({
262
+ code: 'session_revoked',
263
+ message: 'Session was revoked.',
264
+ }));
265
+ }
266
+ if (session.expiresAt <= now) {
267
+ return reject(new AuthServiceError({
268
+ code: 'session_expired',
269
+ message: 'Session has expired.',
270
+ }));
271
+ }
272
+ clientSession = buildClientSession({
273
+ session,
274
+ ...(input.buildClientSessionData === undefined
275
+ ? {}
276
+ : { buildClientSessionData: input.buildClientSessionData }),
277
+ });
278
+ }
279
+ return {
280
+ claims: parsedClaims,
281
+ authContext,
282
+ ...(session === undefined ? {} : { session }),
283
+ ...(clientSession === undefined ? {} : { clientSession }),
284
+ };
285
+ }
286
+ catch (error) {
287
+ if (isAuthServiceError(error)) {
288
+ throw error;
289
+ }
290
+ await reportServiceError({
291
+ observability: input.observability,
292
+ where: 'access-token-service.validateAccessToken',
293
+ error,
294
+ requestId,
295
+ });
296
+ throw new AuthServiceError({
297
+ code: 'system_error',
298
+ message: 'Failed to validate access token.',
299
+ cause: error,
300
+ });
301
+ }
302
+ },
303
+ };
304
+ }
@@ -0,0 +1,14 @@
1
+ export type AuthServiceErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'invalid_token' | 'token_expired' | 'token_revoked' | 'session_not_found' | 'session_revoked' | 'session_expired' | 'unauthorized' | 'forbidden' | 'system_error';
2
+ export declare class AuthServiceError extends Error {
3
+ readonly code: AuthServiceErrorCode;
4
+ readonly details?: Record<string, unknown>;
5
+ readonly cause?: unknown;
6
+ constructor(input: {
7
+ code: AuthServiceErrorCode;
8
+ message: string;
9
+ details?: Record<string, unknown>;
10
+ cause?: unknown;
11
+ });
12
+ }
13
+ export declare function isAuthServiceError(error: unknown): error is AuthServiceError;
14
+ export declare function authErrorToHttpStatus(code: AuthServiceErrorCode): number;
@@ -0,0 +1,47 @@
1
+ export class AuthServiceError extends Error {
2
+ code;
3
+ details;
4
+ cause;
5
+ constructor(input) {
6
+ super(input.message);
7
+ this.name = 'AuthServiceError';
8
+ this.code = input.code;
9
+ if (input.details !== undefined) {
10
+ this.details = input.details;
11
+ }
12
+ if (input.cause !== undefined) {
13
+ this.cause = input.cause;
14
+ }
15
+ }
16
+ }
17
+ export function isAuthServiceError(error) {
18
+ return error instanceof AuthServiceError;
19
+ }
20
+ export function authErrorToHttpStatus(code) {
21
+ switch (code) {
22
+ case 'invalid_request':
23
+ return 400;
24
+ case 'invalid_client':
25
+ return 401;
26
+ case 'invalid_grant':
27
+ return 400;
28
+ case 'invalid_token':
29
+ return 401;
30
+ case 'token_expired':
31
+ return 401;
32
+ case 'token_revoked':
33
+ return 401;
34
+ case 'session_not_found':
35
+ return 401;
36
+ case 'session_revoked':
37
+ return 401;
38
+ case 'session_expired':
39
+ return 401;
40
+ case 'unauthorized':
41
+ return 401;
42
+ case 'forbidden':
43
+ return 403;
44
+ case 'system_error':
45
+ return 500;
46
+ }
47
+ }
@@ -0,0 +1,23 @@
1
+ import type { AuthContext } from '../tokens/id-claims';
2
+ import type { AccessClaims, AuthActor, ClientSession, SessionRecord } from '../types/auth-contract';
3
+ export type RefreshTokenMode = 'reuse' | 'rotate' | 'rotate_with_reuse_detection';
4
+ export type RefreshTokenReuseRevokeScope = 'token' | 'family' | 'session';
5
+ export type IssuedAccessToken<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TExtClaims extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>> = {
6
+ accessToken: string;
7
+ claims: AccessClaims<TActor, TExtClaims>;
8
+ clientSession?: ClientSession<TSessionId, TUserId, TActor, TClientSessionData>;
9
+ };
10
+ export type ValidatedAccessTokenResult<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
11
+ claims: AccessClaims<TActor, TExtClaims>;
12
+ authContext: AuthContext<TSessionId, TUserId>;
13
+ session?: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
14
+ clientSession?: ClientSession<TSessionId, TUserId, TActor, TClientSessionData>;
15
+ };
16
+ export type OAuthPendingRequest = {
17
+ clientId: string;
18
+ redirectUri: string;
19
+ codeChallenge: string;
20
+ codeMethod: 'S256' | 'plain';
21
+ scopes: string[];
22
+ state?: string;
23
+ };
@@ -0,0 +1 @@
1
+ export {};