@azlib/identity 0.2.1 → 0.3.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 (70) hide show
  1. package/README.md +119 -54
  2. package/dist/cookie-header-DptIxRmW.mjs +76 -0
  3. package/dist/cookie-header-DptIxRmW.mjs.map +1 -0
  4. package/dist/cookie-header-Dqd5k5BU.cjs +115 -0
  5. package/dist/cookie-header-LFWHKQ04.d.cts +86 -0
  6. package/dist/cookie-header-LFWHKQ04.d.cts.map +1 -0
  7. package/dist/cookie-header-LFWHKQ04.d.mts +86 -0
  8. package/dist/cookie-header-LFWHKQ04.d.mts.map +1 -0
  9. package/dist/{errors-CBbiRO2n.d.cts → errors-0Zh7EG06.d.cts} +4 -2
  10. package/dist/errors-0Zh7EG06.d.cts.map +1 -0
  11. package/dist/{errors-_OcodtdV.d.mts → errors-DVDEUk9-.d.mts} +4 -2
  12. package/dist/errors-DVDEUk9-.d.mts.map +1 -0
  13. package/dist/express.d.cts +3 -3
  14. package/dist/express.d.cts.map +1 -1
  15. package/dist/express.d.mts +3 -3
  16. package/dist/express.d.mts.map +1 -1
  17. package/dist/express.mjs.map +1 -1
  18. package/dist/{identity-Bz9RDOvT.mjs → identity-D0qyca_c.mjs} +37 -3
  19. package/dist/identity-D0qyca_c.mjs.map +1 -0
  20. package/dist/{identity-4eP45YIP.cjs → identity-Yz1BVkrM.cjs} +46 -0
  21. package/dist/{identity-router-D1xJ5H7y.d.cts → identity-router-BiratpJR.d.mts} +3 -3
  22. package/dist/identity-router-BiratpJR.d.mts.map +1 -0
  23. package/dist/{identity-router-tj3-teBI.d.mts → identity-router-D4oqYI60.d.cts} +3 -3
  24. package/dist/identity-router-D4oqYI60.d.cts.map +1 -0
  25. package/dist/{identity-service---OTann1.d.cts → identity-service-DE0Ffguz.d.cts} +3 -3
  26. package/dist/{identity-service---OTann1.d.cts.map → identity-service-DE0Ffguz.d.cts.map} +1 -1
  27. package/dist/{identity-service-WpOKb0MK.d.mts → identity-service-JtsoA6lv.d.mts} +3 -3
  28. package/dist/{identity-service-WpOKb0MK.d.mts.map → identity-service-JtsoA6lv.d.mts.map} +1 -1
  29. package/dist/identity-store-BRRahxcS.d.cts.map +1 -1
  30. package/dist/identity-store-BRRahxcS.d.mts.map +1 -1
  31. package/dist/index-CsYqHPvK.d.cts +54 -0
  32. package/dist/index-CsYqHPvK.d.cts.map +1 -0
  33. package/dist/index-LhSdoWEP.d.mts +54 -0
  34. package/dist/index-LhSdoWEP.d.mts.map +1 -0
  35. package/dist/index.cjs +11 -1
  36. package/dist/index.d.cts +4 -3
  37. package/dist/index.d.mts +4 -3
  38. package/dist/index.mjs +3 -2
  39. package/dist/logger-CcCHJVVe.mjs.map +1 -1
  40. package/dist/nestjs.d.cts +3 -3
  41. package/dist/nestjs.d.cts.map +1 -1
  42. package/dist/nestjs.d.mts +3 -3
  43. package/dist/nestjs.d.mts.map +1 -1
  44. package/dist/nestjs.mjs.map +1 -1
  45. package/dist/next.cjs +285 -0
  46. package/dist/next.d.cts +58 -0
  47. package/dist/next.d.cts.map +1 -0
  48. package/dist/next.d.mts +58 -0
  49. package/dist/next.d.mts.map +1 -0
  50. package/dist/next.mjs +274 -0
  51. package/dist/next.mjs.map +1 -0
  52. package/dist/node.cjs +18 -19
  53. package/dist/node.d.cts +5 -4
  54. package/dist/node.d.cts.map +1 -1
  55. package/dist/node.d.mts +5 -4
  56. package/dist/node.d.mts.map +1 -1
  57. package/dist/node.mjs +10 -20
  58. package/dist/node.mjs.map +1 -1
  59. package/dist/test-utils.mjs.map +1 -1
  60. package/package.json +7 -7
  61. package/schema/model.ts +110 -22
  62. package/dist/errors-CBbiRO2n.d.cts.map +0 -1
  63. package/dist/errors-_OcodtdV.d.mts.map +0 -1
  64. package/dist/identity-Bz9RDOvT.mjs.map +0 -1
  65. package/dist/identity-router-D1xJ5H7y.d.cts.map +0 -1
  66. package/dist/identity-router-tj3-teBI.d.mts.map +0 -1
  67. package/dist/index-CpufYgyn.d.cts +0 -30
  68. package/dist/index-CpufYgyn.d.cts.map +0 -1
  69. package/dist/index-CpufYgyn.d.mts +0 -30
  70. package/dist/index-CpufYgyn.d.mts.map +0 -1
@@ -0,0 +1,86 @@
1
+ //#region core/cookie-header.d.ts
2
+ /**
3
+ * Cookie names and Cookie-header parsing for identity sessions.
4
+ *
5
+ * Edge-safe: no Node APIs. Use {@link hasIdentityCookies} in Next.js `proxy.ts` /
6
+ * middleware to gate routes without hitting the database.
7
+ */
8
+ /** Default HttpOnly cookie that holds the short-lived access JWT. */
9
+ declare const DEFAULT_ACCESS_COOKIE = "azlib_at";
10
+ /** Default HttpOnly cookie that holds the rotating refresh token. */
11
+ declare const DEFAULT_REFRESH_COOKIE = "azlib_rt";
12
+ /** Cookie attribute set shared by access and refresh cookies. */
13
+ interface IdentityCookieAttributes {
14
+ /** Mark cookies as `HttpOnly`. Default: `true`. */
15
+ httpOnly?: boolean;
16
+ /**
17
+ * Mark cookies as `Secure`. Defaults to `true` when `NODE_ENV` is `"production"`,
18
+ * `false` otherwise.
19
+ */
20
+ secure?: boolean;
21
+ /** `SameSite` policy. Default: `"lax"`. */
22
+ sameSite?: "strict" | "lax" | "none";
23
+ /** Cookie path. Default: `"/"`. */
24
+ path?: string;
25
+ /** Cookie domain. Omit to use the current host. */
26
+ domain?: string;
27
+ }
28
+ /** Names and attributes for the access + refresh cookie pair. */
29
+ interface IdentityCookieOptions extends IdentityCookieAttributes {
30
+ /** Access-token cookie name. Default: {@link DEFAULT_ACCESS_COOKIE}. */
31
+ accessCookie?: string;
32
+ /** Refresh-token cookie name. Default: {@link DEFAULT_REFRESH_COOKIE}. */
33
+ refreshCookie?: string;
34
+ }
35
+ /** Resolved cookie names. */
36
+ interface IdentityCookieNames {
37
+ access: string;
38
+ refresh: string;
39
+ }
40
+ /**
41
+ * A Next.js `cookies().get` / Request-cookies compatible reader.
42
+ */
43
+ interface CookieReader {
44
+ get(name: string): {
45
+ value: string;
46
+ } | undefined;
47
+ }
48
+ /**
49
+ * A Next.js `cookies().set` / `cookies().delete` compatible writer.
50
+ * `maxAge` is in **seconds** (Web / Next.js convention, not Express milliseconds).
51
+ */
52
+ interface CookieWriter {
53
+ set(name: string, value: string, options?: {
54
+ httpOnly?: boolean;
55
+ secure?: boolean;
56
+ sameSite?: "strict" | "lax" | "none";
57
+ path?: string;
58
+ domain?: string;
59
+ maxAge?: number;
60
+ expires?: Date;
61
+ }): unknown;
62
+ delete(name: string, options?: {
63
+ path?: string;
64
+ domain?: string;
65
+ }): unknown;
66
+ }
67
+ type CookieStore = CookieReader & CookieWriter;
68
+ /** Anything {@link hasIdentityCookies} / {@link readCookieValue} can inspect. */
69
+ type CookieSource = CookieReader | Headers | string | null | undefined;
70
+ declare function resolveCookieNames(options?: IdentityCookieOptions): IdentityCookieNames;
71
+ declare function resolveCookieAttributes(options?: IdentityCookieOptions): Required<Pick<IdentityCookieAttributes, "httpOnly" | "sameSite" | "path">> & IdentityCookieAttributes;
72
+ /**
73
+ * Parses a raw `Cookie` header into a name → value map.
74
+ * Values are `decodeURIComponent`'d when possible.
75
+ */
76
+ declare function parseCookieHeader(header: string | null | undefined): Map<string, string>;
77
+ /** Reads a single cookie value from a header string, `Headers`, or cookie reader. */
78
+ declare function readCookieValue(source: CookieSource, name: string): string | undefined;
79
+ /**
80
+ * Returns true when the request carries an identity access and/or refresh cookie.
81
+ * Intended for Edge/proxy gates: presence only — do not treat this as authentication.
82
+ */
83
+ declare function hasIdentityCookies(source: CookieSource, options?: IdentityCookieOptions): boolean;
84
+ //#endregion
85
+ export { DEFAULT_ACCESS_COOKIE as a, IdentityCookieNames as c, parseCookieHeader as d, readCookieValue as f, CookieWriter as i, IdentityCookieOptions as l, resolveCookieNames as m, CookieSource as n, DEFAULT_REFRESH_COOKIE as o, resolveCookieAttributes as p, CookieStore as r, IdentityCookieAttributes as s, CookieReader as t, hasIdentityCookies as u };
86
+ //# sourceMappingURL=cookie-header-LFWHKQ04.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie-header-LFWHKQ04.d.mts","names":[],"sources":["../core/cookie-header.ts"],"mappings":";;;;;;;;cAQa;;cAGA;;UAGI;;EAEf;;;;;EAKA;;EAEA;;EAEA;;EAEA;;;UAIe,8BAA8B;;EAE7C;;EAEA;;;UAIe;EACf;EACA;;;;;UAMe;EACf,IAAI;IAAiB;;;;;;;UAON;EACf,IACE,cACA,eACA;IACE;IACA;IACA;IACA;IACA;IACA;IACA,UAAU;;EAGd,OAAO,cAAc;IAAY;IAAe;;;KAGtC,cAAc,eAAe;;KAG7B,eACR,eACA;iBAKY,mBACd,UAAS,wBACR;iBAOa,wBACd,UAAS,wBACR,SAAS,KAAK,+DACf;;;;;iBAcc,kBACd,oCACC;;iBA8Ba,gBACd,QAAQ,cACR;;;;;iBAqBc,mBACd,QAAQ,cACR,UAAS"}
@@ -237,6 +237,8 @@ interface IdentityConfig {
237
237
  generateId: () => string;
238
238
  onEvent: ((event: IdentityEvent) => void | Promise<void>) | undefined;
239
239
  }
240
+ /** Default JWT `iss` claim used when `IdentityConfigInput.issuer` is omitted. */
241
+ declare const DEFAULT_ISSUER = "azlib-identity";
240
242
  /**
241
243
  * Validates and applies defaults to consumer-provided configuration.
242
244
  * Throws {@link IdentityConfigError} when required values are missing or invalid.
@@ -476,5 +478,5 @@ declare class AccountUnavailableError extends IdentityError {
476
478
  constructor(message?: string);
477
479
  }
478
480
  //#endregion
479
- export { resolveIdentityConfig as A, AuthorizationDecision as B, createTokenService as C, IdentityConfigInput as D, IdentityConfig as E, IdentityLogger as F, PolicyRule as H, consoleLogger as I, noopLogger as L, NotificationService as M, PasswordResetParams as N, IdentityOverrides as O, TwoFactorSmsParams as P, resolveLogger as R, TokenService as S, createAuditLogger as T, evaluateAuthorization as U, AuthorizationRequirement as V, isAuthorized as W, ExchangeCodeParams as _, IdentityError as a, OAuthUserInfo as b, UnauthenticatedError as c, OAuthProviderNotFoundError as d, OAuthService as f, BuildAuthUrlParams as g, createOAuthService as h, IdentityConfigError as i, EmailVerificationParams as j, LockoutConfig as k, OAuthAuthorizationUrl as l, OAuthStateMismatchError as m, EmailAlreadyRegisteredError as n, InvalidCredentialsError as o, OAuthServiceDeps as p, ForbiddenError as r, InvalidTokenError as s, AccountUnavailableError as t, OAuthCallbackParams as u, OAuthProvider as v, AuditLogger as w, SessionDeps as x, OAuthTokens as y, AuthorizationContext as z };
480
- //# sourceMappingURL=errors-CBbiRO2n.d.cts.map
481
+ export { LockoutConfig as A, AuthorizationContext as B, createTokenService as C, IdentityConfig as D, DEFAULT_ISSUER as E, TwoFactorSmsParams as F, isAuthorized as G, AuthorizationRequirement as H, IdentityLogger as I, consoleLogger as L, EmailVerificationParams as M, NotificationService as N, IdentityConfigInput as O, PasswordResetParams as P, noopLogger as R, TokenService as S, createAuditLogger as T, PolicyRule as U, AuthorizationDecision as V, evaluateAuthorization as W, ExchangeCodeParams as _, IdentityError as a, OAuthUserInfo as b, UnauthenticatedError as c, OAuthProviderNotFoundError as d, OAuthService as f, BuildAuthUrlParams as g, createOAuthService as h, IdentityConfigError as i, resolveIdentityConfig as j, IdentityOverrides as k, OAuthAuthorizationUrl as l, OAuthStateMismatchError as m, EmailAlreadyRegisteredError as n, InvalidCredentialsError as o, OAuthServiceDeps as p, ForbiddenError as r, InvalidTokenError as s, AccountUnavailableError as t, OAuthCallbackParams as u, OAuthProvider as v, AuditLogger as w, SessionDeps as x, OAuthTokens as y, resolveLogger as z };
482
+ //# sourceMappingURL=errors-0Zh7EG06.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-0Zh7EG06.d.cts","names":[],"sources":["../core/authorization.ts","../core/logger.ts","../core/notification.ts","../core/config.ts","../core/audit.ts","../core/token-service.ts","../core/session-state.ts","../core/oauth/oauth-provider.ts","../core/oauth/oauth-service.ts","../core/errors.ts"],"mappings":"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"}
@@ -237,6 +237,8 @@ interface IdentityConfig {
237
237
  generateId: () => string;
238
238
  onEvent: ((event: IdentityEvent) => void | Promise<void>) | undefined;
239
239
  }
240
+ /** Default JWT `iss` claim used when `IdentityConfigInput.issuer` is omitted. */
241
+ declare const DEFAULT_ISSUER = "azlib-identity";
240
242
  /**
241
243
  * Validates and applies defaults to consumer-provided configuration.
242
244
  * Throws {@link IdentityConfigError} when required values are missing or invalid.
@@ -476,5 +478,5 @@ declare class AccountUnavailableError extends IdentityError {
476
478
  constructor(message?: string);
477
479
  }
478
480
  //#endregion
479
- export { resolveIdentityConfig as A, AuthorizationDecision as B, createTokenService as C, IdentityConfigInput as D, IdentityConfig as E, IdentityLogger as F, PolicyRule as H, consoleLogger as I, noopLogger as L, NotificationService as M, PasswordResetParams as N, IdentityOverrides as O, TwoFactorSmsParams as P, resolveLogger as R, TokenService as S, createAuditLogger as T, evaluateAuthorization as U, AuthorizationRequirement as V, isAuthorized as W, ExchangeCodeParams as _, IdentityError as a, OAuthUserInfo as b, UnauthenticatedError as c, OAuthProviderNotFoundError as d, OAuthService as f, BuildAuthUrlParams as g, createOAuthService as h, IdentityConfigError as i, EmailVerificationParams as j, LockoutConfig as k, OAuthAuthorizationUrl as l, OAuthStateMismatchError as m, EmailAlreadyRegisteredError as n, InvalidCredentialsError as o, OAuthServiceDeps as p, ForbiddenError as r, InvalidTokenError as s, AccountUnavailableError as t, OAuthCallbackParams as u, OAuthProvider as v, AuditLogger as w, SessionDeps as x, OAuthTokens as y, AuthorizationContext as z };
480
- //# sourceMappingURL=errors-_OcodtdV.d.mts.map
481
+ export { LockoutConfig as A, AuthorizationContext as B, createTokenService as C, IdentityConfig as D, DEFAULT_ISSUER as E, TwoFactorSmsParams as F, isAuthorized as G, AuthorizationRequirement as H, IdentityLogger as I, consoleLogger as L, EmailVerificationParams as M, NotificationService as N, IdentityConfigInput as O, PasswordResetParams as P, noopLogger as R, TokenService as S, createAuditLogger as T, PolicyRule as U, AuthorizationDecision as V, evaluateAuthorization as W, ExchangeCodeParams as _, IdentityError as a, OAuthUserInfo as b, UnauthenticatedError as c, OAuthProviderNotFoundError as d, OAuthService as f, BuildAuthUrlParams as g, createOAuthService as h, IdentityConfigError as i, resolveIdentityConfig as j, IdentityOverrides as k, OAuthAuthorizationUrl as l, OAuthStateMismatchError as m, EmailAlreadyRegisteredError as n, InvalidCredentialsError as o, OAuthServiceDeps as p, ForbiddenError as r, InvalidTokenError as s, AccountUnavailableError as t, OAuthCallbackParams as u, OAuthProvider as v, AuditLogger as w, SessionDeps as x, OAuthTokens as y, resolveLogger as z };
482
+ //# sourceMappingURL=errors-DVDEUk9-.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-DVDEUk9-.d.mts","names":[],"sources":["../core/authorization.ts","../core/logger.ts","../core/notification.ts","../core/config.ts","../core/audit.ts","../core/token-service.ts","../core/session-state.ts","../core/oauth/oauth-provider.ts","../core/oauth/oauth-service.ts","../core/errors.ts"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { c as AuthResult, d as AuthenticatedIdentity } from "./identity-store-BRRahxcS.cjs";
2
- import { V as AuthorizationRequirement, f as OAuthService } from "./errors-CBbiRO2n.cjs";
3
- import { t as IdentityService } from "./identity-service---OTann1.cjs";
4
- import { i as identityErrorHandler, n as IdentityRouterOptions, r as createIdentityRouter, t as CookieRefreshOptions } from "./identity-router-D1xJ5H7y.cjs";
2
+ import { H as AuthorizationRequirement, f as OAuthService } from "./errors-0Zh7EG06.cjs";
3
+ import { t as IdentityService } from "./identity-service-DE0Ffguz.cjs";
4
+ import { i as identityErrorHandler, n as IdentityRouterOptions, r as createIdentityRouter, t as CookieRefreshOptions } from "./identity-router-D4oqYI60.cjs";
5
5
  import { NextFunction, Request, RequestHandler, Response } from "express";
6
6
  //#region core/express/middleware.d.ts
7
7
  declare global {
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.cts","names":[],"sources":["../core/express/middleware.ts","../core/express/oauth-routes.ts"],"mappings":";;;;;;QAOQ;YAEI;cACE;;MAER,WAAW;;;;;KAML,eAAe,cAAc,KAAK,YAAY,YAAY,QAAQ;;;;;;iBAc9D,YAAY,SAAS,kBAAkB;;;;;iBAmBvC,qBAAqB,qBACnC,SAAS,iBACT,aAAa,yBAAyB,YACtC,eAAe,eAAe,aAC7B;;iBAwBa,kBACd,SAAS,iBACT,qBACC;;;;;;UC1Ec;;EAEf;;;;;EAKA,SAAS,KAAK,0BAA0B;;;;;EAKxC,SAAS,KAAK,SAAS,KAAK,UAAU,uBAAuB;;;;;EAK7D,UAAU,KAAK,SAAS,KAAK,UAAU,QAAV,oBAAyD;;;;;;;;;;;;;;;;iBAiBxE,eACd,cAAc,cACd,sBACA,SAAS,KAAK,iDACb;;;;;;;;;;;;;;;iBA0Ba,cACd,cAAc,cACd,sBACA,SAAS,oBACR"}
1
+ {"version":3,"file":"express.d.cts","names":[],"sources":["../core/express/middleware.ts","../core/express/oauth-routes.ts"],"mappings":";;;;;;QAOQ;YAEI;cACE;;MAER,WAAW;;;;;KAML,eAAe,cACzB,KAAK,YACF,YAAY,QAAQ;;;;;;iBAcT,YAAY,SAAS,kBAAkB;;;;;iBAuBvC,qBAAqB,qBACnC,SAAS,iBACT,aAAa,yBAAyB,YACtC,eAAe,eAAe,aAC7B;;iBA4Ba,kBACd,SAAS,iBACT,qBACC;;;;;;UCpFc;;EAEf;;;;;EAKA,SAAS,KAAK,0BAA0B;;;;;EAKxC,SAAS,KAAK,SAAS,KAAK,UAAU,uBAAuB;;;;;EAK7D,UACE,KAAK,SACL,KAAK,UACL,QADK,oBAEG;;;;;;;;;;;;;;;;iBAiBI,eACd,cAAc,cACd,sBACA,SAAS,KAAK,iDACb;;;;;;;;;;;;;;;iBAiCa,cACd,cAAc,cACd,sBACA,SAAS,oBACR"}
@@ -1,7 +1,7 @@
1
1
  import { c as AuthResult, d as AuthenticatedIdentity } from "./identity-store-BRRahxcS.mjs";
2
- import { V as AuthorizationRequirement, f as OAuthService } from "./errors-_OcodtdV.mjs";
3
- import { t as IdentityService } from "./identity-service-WpOKb0MK.mjs";
4
- import { i as identityErrorHandler, n as IdentityRouterOptions, r as createIdentityRouter, t as CookieRefreshOptions } from "./identity-router-tj3-teBI.mjs";
2
+ import { H as AuthorizationRequirement, f as OAuthService } from "./errors-DVDEUk9-.mjs";
3
+ import { t as IdentityService } from "./identity-service-JtsoA6lv.mjs";
4
+ import { i as identityErrorHandler, n as IdentityRouterOptions, r as createIdentityRouter, t as CookieRefreshOptions } from "./identity-router-BiratpJR.mjs";
5
5
  import { NextFunction, Request, RequestHandler, Response } from "express";
6
6
  //#region core/express/middleware.d.ts
7
7
  declare global {
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.mts","names":[],"sources":["../core/express/middleware.ts","../core/express/oauth-routes.ts"],"mappings":";;;;;;QAOQ;YAEI;cACE;;MAER,WAAW;;;;;KAML,eAAe,cAAc,KAAK,YAAY,YAAY,QAAQ;;;;;;iBAc9D,YAAY,SAAS,kBAAkB;;;;;iBAmBvC,qBAAqB,qBACnC,SAAS,iBACT,aAAa,yBAAyB,YACtC,eAAe,eAAe,aAC7B;;iBAwBa,kBACd,SAAS,iBACT,qBACC;;;;;;UC1Ec;;EAEf;;;;;EAKA,SAAS,KAAK,0BAA0B;;;;;EAKxC,SAAS,KAAK,SAAS,KAAK,UAAU,uBAAuB;;;;;EAK7D,UAAU,KAAK,SAAS,KAAK,UAAU,QAAV,oBAAyD;;;;;;;;;;;;;;;;iBAiBxE,eACd,cAAc,cACd,sBACA,SAAS,KAAK,iDACb;;;;;;;;;;;;;;;iBA0Ba,cACd,cAAc,cACd,sBACA,SAAS,oBACR"}
1
+ {"version":3,"file":"express.d.mts","names":[],"sources":["../core/express/middleware.ts","../core/express/oauth-routes.ts"],"mappings":";;;;;;QAOQ;YAEI;cACE;;MAER,WAAW;;;;;KAML,eAAe,cACzB,KAAK,YACF,YAAY,QAAQ;;;;;;iBAcT,YAAY,SAAS,kBAAkB;;;;;iBAuBvC,qBAAqB,qBACnC,SAAS,iBACT,aAAa,yBAAyB,YACtC,eAAe,eAAe,aAC7B;;iBA4Ba,kBACd,SAAS,iBACT,qBACC;;;;;;UCpFc;;EAEf;;;;;EAKA,SAAS,KAAK,0BAA0B;;;;;EAKxC,SAAS,KAAK,SAAS,KAAK,UAAU,uBAAuB;;;;;EAK7D,UACE,KAAK,SACL,KAAK,UACL,QADK,oBAEG;;;;;;;;;;;;;;;;iBAiBI,eACd,cAAc,cACd,sBACA,SAAS,KAAK,iDACb;;;;;;;;;;;;;;;iBAiCa,cACd,cAAc,cACd,sBACA,SAAS,oBACR"}
@@ -1 +1 @@
1
- {"version":3,"file":"express.mjs","names":[],"sources":["../core/express/middleware.ts","../core/express/oauth-routes.ts","../core/express/identity-router.ts"],"sourcesContent":["import type { NextFunction, Request, RequestHandler, Response } from \"express\";\n\nimport type { AuthorizationRequirement } from \"../authorization\";\nimport { ForbiddenError, IdentityError, UnauthenticatedError } from \"../errors\";\nimport type { IdentityService } from \"../identity-service\";\nimport type { AuthenticatedIdentity } from \"../types\";\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** The authenticated principal, set by {@link requireAuth}. */\n identity?: AuthenticatedIdentity;\n }\n }\n}\n\n/** Loads the resource a policy will be evaluated against. */\nexport type ResourceLoader<TResource> = (req: Request) => TResource | Promise<TResource>;\n\nconst extractBearerToken = (req: Request): string | null => {\n const header = req.headers.authorization;\n if (!header || !header.startsWith(\"Bearer \")) return null;\n const token = header.slice(\"Bearer \".length).trim();\n return token.length > 0 ? token : null;\n};\n\n/**\n * Express middleware that authenticates the request using a Bearer access token and\n * attaches the hydrated principal to `req.identity`. Forwards identity errors to the\n * error-handling middleware.\n */\nexport function requireAuth(service: IdentityService): RequestHandler {\n return async (req: Request, _res: Response, next: NextFunction): Promise<void> => {\n try {\n const token = extractBearerToken(req);\n if (!token) {\n throw new UnauthenticatedError();\n }\n req.identity = await service.authenticate(token);\n next();\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Express middleware enforcing an authorization requirement (deny-by-default). Must run\n * after {@link requireAuth}. Optionally loads a resource for ownership policies.\n */\nexport function requireAuthorization<TResource = unknown>(\n service: IdentityService,\n requirement: AuthorizationRequirement<TResource>,\n loadResource?: ResourceLoader<TResource>,\n): RequestHandler {\n return async (req: Request, _res: Response, next: NextFunction): Promise<void> => {\n try {\n const principal = req.identity;\n if (!principal) {\n throw new UnauthenticatedError();\n }\n const resource = loadResource ? await loadResource(req) : undefined;\n const decision = await service.authorize(requirement, {\n principal,\n action: requirement.permission ?? \"custom\",\n resource,\n });\n if (!decision.allowed) {\n throw new ForbiddenError();\n }\n next();\n } catch (error) {\n next(error);\n }\n };\n}\n\n/** Convenience: require a single permission. */\nexport function requirePermission(\n service: IdentityService,\n permission: string,\n): RequestHandler {\n return requireAuthorization(service, { permission });\n}\n\n/**\n * Optional Express error handler that serializes {@link IdentityError} instances to JSON.\n * Mount after your routes. Non-identity errors are forwarded unchanged.\n */\nexport function identityErrorHandler() {\n return (err: unknown, _req: Request, res: Response, next: NextFunction): void => {\n if (err instanceof IdentityError) {\n res.status(err.statusCode).json({ error: { code: err.code, message: err.message } });\n return;\n }\n next(err);\n };\n}\n","import type { NextFunction, Request, RequestHandler, Response } from \"express\";\n\nimport { IdentityError } from \"../errors\";\nimport type { OAuthService } from \"../oauth/oauth-service\";\n\n/**\n * Options for {@link oauthAuthorize} and {@link oauthCallback}.\n */\nexport interface OAuthRouteOptions {\n /** The full redirect URI registered with the provider (must match exactly). */\n redirectUri: string;\n /**\n * Retrieves the stored CSRF state value from the current request context (e.g. from a\n * signed cookie or server session). Return `null` if no state has been stored yet.\n */\n getState(req: Request): string | null | Promise<string | null>;\n /**\n * Persists the generated CSRF state value before redirecting the user to the provider.\n * Use a signed cookie or server session.\n */\n setState(req: Request, res: Response, state: string): void | Promise<void>;\n /**\n * Called on a successful OAuth callback with the auth result. Typically sets a session\n * cookie and redirects to the app.\n */\n onSuccess(req: Request, res: Response, result: import(\"../types\").AuthResult): void | Promise<void>;\n}\n\n/**\n * Returns an Express route handler that redirects the user to the OAuth provider's\n * authorization page. Generates and persists a CSRF state value via `options.setState`.\n *\n * @example\n * ```ts\n * app.get(\"/auth/google\", oauthAuthorize(identity.oauth!, \"google\", {\n * redirectUri: `${BASE_URL}/auth/google/callback`,\n * getState: (req) => req.session?.oauthState ?? null,\n * setState: (req, _res, state) => { req.session!.oauthState = state; },\n * onSuccess: (_req, res, result) => res.json(result),\n * }));\n * ```\n */\nexport function oauthAuthorize(\n oauthService: OAuthService,\n providerName: string,\n options: Pick<OAuthRouteOptions, \"redirectUri\" | \"setState\">,\n): RequestHandler {\n return async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { url, state } = oauthService.buildAuthorizationUrl(providerName, options.redirectUri);\n await options.setState(req, res, state);\n res.redirect(url);\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Returns an Express route handler that handles the OAuth provider callback. Verifies\n * state, exchanges the code, and calls `options.onSuccess` with the auth result.\n *\n * @example\n * ```ts\n * app.get(\"/auth/google/callback\", oauthCallback(identity.oauth!, \"google\", {\n * redirectUri: `${BASE_URL}/auth/google/callback`,\n * getState: (req) => req.session?.oauthState ?? null,\n * setState: (req, _res, state) => { req.session!.oauthState = state; },\n * onSuccess: (_req, res, result) => res.json(result),\n * }));\n * ```\n */\nexport function oauthCallback(\n oauthService: OAuthService,\n providerName: string,\n options: OAuthRouteOptions,\n): RequestHandler {\n return async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const code = typeof req.query[\"code\"] === \"string\" ? req.query[\"code\"] : null;\n const state = typeof req.query[\"state\"] === \"string\" ? req.query[\"state\"] : null;\n const error = typeof req.query[\"error\"] === \"string\" ? req.query[\"error\"] : null;\n\n if (error) {\n const errorDescription =\n typeof req.query[\"error_description\"] === \"string\"\n ? req.query[\"error_description\"]\n : error;\n throw new IdentityError(\"identity/oauth-error\", `Provider error: ${errorDescription}`, 400);\n }\n\n if (!code || !state) {\n throw new IdentityError(\n \"identity/oauth-invalid-callback\",\n \"Missing code or state in OAuth callback.\",\n 400,\n );\n }\n\n const expectedState = await options.getState(req);\n if (!expectedState) {\n throw new IdentityError(\n \"identity/oauth-state-missing\",\n \"No OAuth state found in session. The request may have expired.\",\n 400,\n );\n }\n\n const result = await oauthService.handleCallback(providerName, {\n code,\n state,\n expectedState,\n redirectUri: options.redirectUri,\n });\n\n await options.onSuccess(req, res, result);\n } catch (error) {\n next(error);\n }\n };\n}\n","import { Router, type NextFunction, type Request, type Response } from \"express\";\n\nimport { IdentityError, UnauthenticatedError } from \"../errors\";\nimport type { IdentityService } from \"../identity-service\";\nimport type { IdentityLogger } from \"../logger\";\nimport { resolveLogger } from \"../logger\";\nimport type { AuthResult, AuthTokens, LoginResult } from \"../types\";\nimport { requireAuth } from \"./middleware\";\nimport { oauthAuthorize, oauthCallback } from \"./oauth-routes\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Options for controlling the refresh-token cookie. */\nexport interface CookieRefreshOptions {\n /** Cookie name. Default: `\"azlib_rt\"`. */\n name?: string;\n /** Mark the cookie as `HttpOnly`. Default: `true`. */\n httpOnly?: boolean;\n /**\n * Mark the cookie as `Secure`. Defaults to `true` when `NODE_ENV` is `\"production\"`,\n * `false` otherwise.\n */\n secure?: boolean;\n /** `SameSite` policy. Default: `\"lax\"`. */\n sameSite?: \"strict\" | \"lax\" | \"none\";\n /** Cookie path. Default: `\"/\"`. */\n path?: string;\n /** Cookie domain. Omit to use the current host. */\n domain?: string;\n}\n\n/**\n * Options for {@link createIdentityRouter}.\n */\nexport interface IdentityRouterOptions {\n /**\n * Controls how the refresh token is transported between server and client.\n *\n * - **`\"body\"`** (default) — the refresh token is included in the `tokens` object of\n * every successful `register`/`login`/`refresh` response. The client must store it\n * and send it back via the JSON body on `/refresh` and `/logout`.\n *\n * - **`{ cookie: CookieRefreshOptions }`** — the refresh token is sent as an\n * `HttpOnly` cookie. `/refresh` and `/logout` read it automatically; the response\n * body only includes the access token.\n */\n refreshToken?: \"body\" | { cookie: CookieRefreshOptions };\n\n /**\n * Base URL under which the OAuth callback routes are hosted.\n *\n * Example: `\"https://api.example.com/auth\"`.\n *\n * The callback URI for a provider becomes `{oauthBaseUrl}/{providerName}/callback`.\n * Required when the identity service has OAuth providers configured.\n */\n oauthBaseUrl?: string;\n\n /**\n * URL path prefix prepended to all identity routes.\n *\n * Default: `\"account\"`. Routes are served at `/{prefix}/login`, `/{prefix}/register`, etc.\n *\n * Mount the returned router at the application root:\n * ```ts\n * app.use(createIdentityRouter(service, { prefix: \"auth\" }));\n * // → POST /auth/login, POST /auth/register, …\n * ```\n *\n * Set to `\"\"` to omit the prefix and mount routes directly at the router's mount point.\n */\n prefix?: string;\n\n /**\n * Logger for request/response and error diagnostics.\n *\n * - Omit — defaults to a `console`-based logger with an `[identity]` prefix.\n * - Supply your own `IdentityLogger` — route output to Winston, Pino, etc.\n * - Pass `false` — disable all logging from this router.\n *\n * @example\n * ```ts\n * import pino from \"pino\";\n * app.use(createIdentityRouter(service, { logger: pino() }));\n * ```\n */\n logger?: IdentityLogger | false;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst OAUTH_STATE_COOKIE = \"azlib_oauth_state\";\n\nfunction resolveCookieName(opts: CookieRefreshOptions): string {\n return opts.name ?? \"azlib_rt\";\n}\n\n/** Parses cookies from the raw `Cookie` header without a dependency on `cookie-parser`. */\nfunction readCookie(req: Request, name: string): string | undefined {\n const raw = req.headers.cookie ?? \"\";\n for (const part of raw.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim();\n if (key === name) {\n try {\n return decodeURIComponent(part.slice(eq + 1).trim());\n } catch {\n return part.slice(eq + 1).trim();\n }\n }\n }\n return undefined;\n}\n\n/**\n * Writes the refresh token into a `Set-Cookie` header and returns a body-safe tokens\n * object that omits the refresh token.\n */\nfunction setCookieAndStripRefreshToken(\n res: Response,\n tokens: AuthTokens,\n opts: CookieRefreshOptions,\n): Omit<AuthTokens, \"refreshToken\" | \"refreshTokenExpiresAt\"> {\n const maxAge = tokens.refreshTokenExpiresAt.getTime() - Date.now();\n res.cookie(resolveCookieName(opts), tokens.refreshToken, {\n httpOnly: opts.httpOnly ?? true,\n secure: opts.secure ?? process.env.NODE_ENV === \"production\",\n sameSite: opts.sameSite ?? \"lax\",\n path: opts.path ?? \"/\",\n domain: opts.domain,\n maxAge,\n });\n const { refreshToken: _rt, refreshTokenExpiresAt: _exp, ...bodyTokens } = tokens;\n return bodyTokens;\n}\n\n/**\n * Sends the authentication result as JSON. In cookie mode the refresh token is stored\n * in a `Set-Cookie` header and excluded from the body.\n *\n * When the result is an MFA challenge (`{ kind: \"mfa_required\" }`), it is forwarded as-is\n * with HTTP 200 so the client knows to complete the TOTP step.\n */\nfunction sendLoginResult(\n res: Response,\n result: LoginResult,\n cookieOpts: CookieRefreshOptions | undefined,\n): void {\n if (\"mfaToken\" in result) {\n res.json(result);\n return;\n }\n sendAuthResult(res, result, cookieOpts);\n}\n\nfunction sendAuthResult(\n res: Response,\n result: AuthResult,\n cookieOpts: CookieRefreshOptions | undefined,\n): void {\n if (cookieOpts) {\n const tokens = setCookieAndStripRefreshToken(res, result.tokens, cookieOpts);\n res.json({ user: result.user, tokens });\n } else {\n res.json(result);\n }\n}\n\n/**\n * Reads the refresh token from the request. In cookie mode it is read from the cookie;\n * in body mode it is expected in `req.body.refreshToken`.\n */\nfunction readRefreshToken(req: Request, cookieOpts: CookieRefreshOptions | undefined): string | undefined {\n if (cookieOpts) {\n return readCookie(req, resolveCookieName(cookieOpts));\n }\n const body = req.body as Record<string, unknown> | undefined;\n const token = body?.[\"refreshToken\"];\n return typeof token === \"string\" ? token : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Router factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a pre-wired Express `Router` with all standard identity endpoints.\n *\n * Mount it once on your application:\n * ```ts\n * import express from \"express\";\n * import { createIdentityService } from \"@azlib/identity/node\";\n * import { createIdentityRouter, identityErrorHandler } from \"@azlib/identity/express\";\n *\n * const service = createIdentityService(config, store);\n * const app = express();\n *\n * app.use(express.json());\n * // Routes are served at /account/login, /account/register, etc. (default prefix)\n * app.use(createIdentityRouter(service));\n * // Or use a custom prefix:\n * app.use(createIdentityRouter(service, { prefix: \"auth\" }));\n * app.use(identityErrorHandler()); // optional convenience error handler\n * ```\n *\n * Pre-wired routes:\n *\n * | Method | Path | Description |\n * |--------|------|-------------|\n * | POST | `/register` | Create a new account |\n * | POST | `/login` | Email + password login |\n * | POST | `/refresh` | Rotate the refresh token |\n * | POST | `/logout` | Revoke the current session |\n * | GET | `/me` | Return the authenticated principal |\n * | GET | `/:provider` | Start an OAuth 2.0 authorisation flow *(optional)* |\n * | GET | `/:provider/callback` | Handle an OAuth 2.0 callback *(optional)* |\n */\nexport function createIdentityRouter(\n service: IdentityService,\n options: IdentityRouterOptions = {},\n): Router {\n const prefix = options.prefix ?? \"account\";\n const log = resolveLogger(options.logger);\n\n const router = Router();\n const cookieOpts: CookieRefreshOptions | undefined =\n typeof options.refreshToken === \"object\" && \"cookie\" in options.refreshToken\n ? options.refreshToken.cookie\n : undefined;\n\n // Request / response logging middleware\n router.use((_req: Request, res: Response, next: NextFunction): void => {\n const start = Date.now();\n res.on(\"finish\", () => {\n const ms = Date.now() - start;\n log.info(`${_req.method} ${_req.originalUrl} ${res.statusCode}`, { durationMs: ms });\n });\n next();\n });\n\n // POST /register\n router.post(\"/register\", async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const result = await service.register(req.body as { email: string; password: string; displayName?: string });\n sendAuthResult(res, result, cookieOpts);\n } catch (err) {\n next(err);\n }\n });\n\n // POST /login\n router.post(\"/login\", async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const result = await service.login(req.body as { email: string; password: string });\n sendLoginResult(res, result, cookieOpts);\n } catch (err) {\n next(err);\n }\n });\n\n // POST /refresh\n router.post(\"/refresh\", async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const token = readRefreshToken(req, cookieOpts);\n if (!token) {\n throw new UnauthenticatedError(\"Refresh token is missing.\");\n }\n const result = await service.refresh(token);\n sendAuthResult(res, result, cookieOpts);\n } catch (err) {\n next(err);\n }\n });\n\n // POST /logout\n router.post(\"/logout\", async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const token = readRefreshToken(req, cookieOpts);\n if (token) {\n await service.logout(token);\n }\n if (cookieOpts) {\n res.clearCookie(resolveCookieName(cookieOpts), { path: cookieOpts.path ?? \"/\" });\n }\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n });\n\n // GET /me — requires a valid Bearer access token\n router.get(\"/me\", requireAuth(service), (_req: Request, res: Response): void => {\n res.json(_req.identity);\n });\n\n // ---------------------------------------------------------------------------\n // Email verification\n // ---------------------------------------------------------------------------\n\n // POST /request-email-verification — re-send verification link (requires auth)\n router.post(\n \"/request-email-verification\",\n requireAuth(service),\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n await service.requestEmailVerification(req.identity!.userId);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /verify-email — consume the token from the verification link\n router.post(\"/verify-email\", async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { token } = req.body as { token?: string };\n if (!token) {\n throw new UnauthenticatedError(\"token is required.\");\n }\n await service.verifyEmail(token);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n });\n\n // ---------------------------------------------------------------------------\n // Password reset\n // ---------------------------------------------------------------------------\n\n // POST /request-password-reset — send reset link by email\n router.post(\n \"/request-password-reset\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { email } = req.body as { email?: string };\n if (!email) {\n throw new UnauthenticatedError(\"email is required.\");\n }\n await service.requestPasswordReset(email);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /reset-password — set new password with token\n router.post(\"/reset-password\", async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { token, newPassword } = req.body as { token?: string; newPassword?: string };\n if (!token || !newPassword) {\n throw new UnauthenticatedError(\"token and newPassword are required.\");\n }\n await service.resetPassword(token, newPassword);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n });\n\n // ---------------------------------------------------------------------------\n // Two-factor authentication (2FA / TOTP)\n // ---------------------------------------------------------------------------\n\n // POST /mfa/verify — complete an MFA challenge after login\n router.post(\"/mfa/verify\", async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { mfaToken, code } = req.body as { mfaToken?: string; code?: string };\n if (!mfaToken || !code) {\n throw new UnauthenticatedError(\"mfaToken and code are required.\");\n }\n const result = await service.verifyMfaChallenge(mfaToken, code);\n sendAuthResult(res, result, cookieOpts);\n } catch (err) {\n next(err);\n }\n });\n\n // POST /2fa/setup — begin 2FA setup (requires auth)\n router.post(\n \"/2fa/setup\",\n requireAuth(service),\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const result = await service.setup2FA(req.identity!.userId);\n res.json(result);\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /2fa/enable — confirm and enable 2FA (requires auth)\n router.post(\n \"/2fa/enable\",\n requireAuth(service),\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { code } = req.body as { code?: string };\n if (!code) {\n throw new UnauthenticatedError(\"code is required.\");\n }\n await service.enable2FA(req.identity!.userId, code);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /2fa/disable — disable 2FA (requires auth)\n router.post(\n \"/2fa/disable\",\n requireAuth(service),\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { code } = req.body as { code?: string };\n if (!code) {\n throw new UnauthenticatedError(\"code is required.\");\n }\n await service.disable2FA(req.identity!.userId, code);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // ---------------------------------------------------------------------------\n // Account management (admin)\n // ---------------------------------------------------------------------------\n\n // POST /admin/lock/:userId — lock an account\n router.post(\n \"/admin/lock/:userId\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n await service.lockAccount(req.params[\"userId\"] as string);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /admin/unlock/:userId — unlock an account\n router.post(\n \"/admin/unlock/:userId\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n await service.unlockAccount(req.params[\"userId\"] as string);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /admin/disable/:userId — disable an account\n router.post(\n \"/admin/disable/:userId\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n await service.disableAccount(req.params[\"userId\"] as string);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // OAuth routes — only when providers are configured on the service\n if (service.oauth) {\n const oauthService = service.oauth;\n const baseUrl = options.oauthBaseUrl ?? \"\";\n\n const stateCookieOptions = {\n httpOnly: true,\n sameSite: \"lax\" as const,\n secure: process.env.NODE_ENV === \"production\",\n maxAge: 10 * 60 * 1000, // 10 minutes\n };\n\n for (const providerName of oauthService.providers) {\n const redirectUri = `${baseUrl}/${providerName}/callback`;\n\n router.get(\n `/${providerName}`,\n oauthAuthorize(oauthService, providerName, {\n redirectUri,\n setState: (_req, oauthRes, state) => {\n oauthRes.cookie(OAUTH_STATE_COOKIE, state, stateCookieOptions);\n },\n }),\n );\n\n router.get(\n `/${providerName}/callback`,\n oauthCallback(oauthService, providerName, {\n redirectUri,\n getState: (oauthReq) => readCookie(oauthReq, OAUTH_STATE_COOKIE) ?? null,\n setState: (_oauthReq, oauthRes, _state) => {\n oauthRes.clearCookie(OAUTH_STATE_COOKIE);\n },\n onSuccess: (_oauthReq, oauthRes, result) => {\n sendAuthResult(oauthRes, result, cookieOpts);\n },\n }),\n );\n }\n }\n\n // Mount the inner router at the configured prefix.\n const outer = Router();\n if (prefix) {\n outer.use(`/${prefix}`, router);\n } else {\n outer.use(router);\n }\n return outer;\n}\n\n// ---------------------------------------------------------------------------\n// Convenience error handler\n// ---------------------------------------------------------------------------\n\n/**\n * Factory that creates an Express error-handling middleware which converts\n * {@link IdentityError} instances to structured JSON responses.\n * Mount it after the identity router:\n *\n * ```ts\n * app.use(createIdentityRouter(service, { prefix: \"auth\" }));\n * app.use(identityErrorHandler()); // default console logger\n * app.use(identityErrorHandler({ logger: false })); // silent\n * ```\n *\n * @param options.logger - Logger for recording serialised error details.\n * Pass `false` to disable. Defaults to the console logger.\n */\nexport function identityErrorHandler(\n options: { logger?: IdentityLogger | false } = {},\n): (err: unknown, req: Request, res: Response, next: NextFunction) => void {\n const log = resolveLogger(options.logger);\n return (err: unknown, _req: Request, res: Response, next: NextFunction): void => {\n if (err instanceof IdentityError) {\n log.debug(\"identity error\", { code: err.code, status: err.statusCode, message: err.message });\n res.status(err.statusCode).json({ code: err.code, message: err.message });\n return;\n }\n log.error(\"unhandled error in identity router\", { error: String(err) });\n next(err);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"express.mjs","names":[],"sources":["../core/express/middleware.ts","../core/express/oauth-routes.ts","../core/express/identity-router.ts"],"sourcesContent":["import type { NextFunction, Request, RequestHandler, Response } from \"express\";\n\nimport type { AuthorizationRequirement } from \"../authorization\";\nimport { ForbiddenError, IdentityError, UnauthenticatedError } from \"../errors\";\nimport type { IdentityService } from \"../identity-service\";\nimport type { AuthenticatedIdentity } from \"../types\";\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** The authenticated principal, set by {@link requireAuth}. */\n identity?: AuthenticatedIdentity;\n }\n }\n}\n\n/** Loads the resource a policy will be evaluated against. */\nexport type ResourceLoader<TResource> = (\n req: Request,\n) => TResource | Promise<TResource>;\n\nconst extractBearerToken = (req: Request): string | null => {\n const header = req.headers.authorization;\n if (!header || !header.startsWith(\"Bearer \")) return null;\n const token = header.slice(\"Bearer \".length).trim();\n return token.length > 0 ? token : null;\n};\n\n/**\n * Express middleware that authenticates the request using a Bearer access token and\n * attaches the hydrated principal to `req.identity`. Forwards identity errors to the\n * error-handling middleware.\n */\nexport function requireAuth(service: IdentityService): RequestHandler {\n return async (\n req: Request,\n _res: Response,\n next: NextFunction,\n ): Promise<void> => {\n try {\n const token = extractBearerToken(req);\n if (!token) {\n throw new UnauthenticatedError();\n }\n req.identity = await service.authenticate(token);\n next();\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Express middleware enforcing an authorization requirement (deny-by-default). Must run\n * after {@link requireAuth}. Optionally loads a resource for ownership policies.\n */\nexport function requireAuthorization<TResource = unknown>(\n service: IdentityService,\n requirement: AuthorizationRequirement<TResource>,\n loadResource?: ResourceLoader<TResource>,\n): RequestHandler {\n return async (\n req: Request,\n _res: Response,\n next: NextFunction,\n ): Promise<void> => {\n try {\n const principal = req.identity;\n if (!principal) {\n throw new UnauthenticatedError();\n }\n const resource = loadResource ? await loadResource(req) : undefined;\n const decision = await service.authorize(requirement, {\n principal,\n action: requirement.permission ?? \"custom\",\n resource,\n });\n if (!decision.allowed) {\n throw new ForbiddenError();\n }\n next();\n } catch (error) {\n next(error);\n }\n };\n}\n\n/** Convenience: require a single permission. */\nexport function requirePermission(\n service: IdentityService,\n permission: string,\n): RequestHandler {\n return requireAuthorization(service, { permission });\n}\n\n/**\n * Optional Express error handler that serializes {@link IdentityError} instances to JSON.\n * Mount after your routes. Non-identity errors are forwarded unchanged.\n */\nexport function identityErrorHandler() {\n return (\n err: unknown,\n _req: Request,\n res: Response,\n next: NextFunction,\n ): void => {\n if (err instanceof IdentityError) {\n res\n .status(err.statusCode)\n .json({ error: { code: err.code, message: err.message } });\n return;\n }\n next(err);\n };\n}\n","import type { NextFunction, Request, RequestHandler, Response } from \"express\";\n\nimport { IdentityError } from \"../errors\";\nimport type { OAuthService } from \"../oauth/oauth-service\";\n\n/**\n * Options for {@link oauthAuthorize} and {@link oauthCallback}.\n */\nexport interface OAuthRouteOptions {\n /** The full redirect URI registered with the provider (must match exactly). */\n redirectUri: string;\n /**\n * Retrieves the stored CSRF state value from the current request context (e.g. from a\n * signed cookie or server session). Return `null` if no state has been stored yet.\n */\n getState(req: Request): string | null | Promise<string | null>;\n /**\n * Persists the generated CSRF state value before redirecting the user to the provider.\n * Use a signed cookie or server session.\n */\n setState(req: Request, res: Response, state: string): void | Promise<void>;\n /**\n * Called on a successful OAuth callback with the auth result. Typically sets a session\n * cookie and redirects to the app.\n */\n onSuccess(\n req: Request,\n res: Response,\n result: import(\"../types\").AuthResult,\n ): void | Promise<void>;\n}\n\n/**\n * Returns an Express route handler that redirects the user to the OAuth provider's\n * authorization page. Generates and persists a CSRF state value via `options.setState`.\n *\n * @example\n * ```ts\n * app.get(\"/auth/google\", oauthAuthorize(identity.oauth!, \"google\", {\n * redirectUri: `${BASE_URL}/auth/google/callback`,\n * getState: (req) => req.session?.oauthState ?? null,\n * setState: (req, _res, state) => { req.session!.oauthState = state; },\n * onSuccess: (_req, res, result) => res.json(result),\n * }));\n * ```\n */\nexport function oauthAuthorize(\n oauthService: OAuthService,\n providerName: string,\n options: Pick<OAuthRouteOptions, \"redirectUri\" | \"setState\">,\n): RequestHandler {\n return async (\n req: Request,\n res: Response,\n next: NextFunction,\n ): Promise<void> => {\n try {\n const { url, state } = oauthService.buildAuthorizationUrl(\n providerName,\n options.redirectUri,\n );\n await options.setState(req, res, state);\n res.redirect(url);\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Returns an Express route handler that handles the OAuth provider callback. Verifies\n * state, exchanges the code, and calls `options.onSuccess` with the auth result.\n *\n * @example\n * ```ts\n * app.get(\"/auth/google/callback\", oauthCallback(identity.oauth!, \"google\", {\n * redirectUri: `${BASE_URL}/auth/google/callback`,\n * getState: (req) => req.session?.oauthState ?? null,\n * setState: (req, _res, state) => { req.session!.oauthState = state; },\n * onSuccess: (_req, res, result) => res.json(result),\n * }));\n * ```\n */\nexport function oauthCallback(\n oauthService: OAuthService,\n providerName: string,\n options: OAuthRouteOptions,\n): RequestHandler {\n return async (\n req: Request,\n res: Response,\n next: NextFunction,\n ): Promise<void> => {\n try {\n const code =\n typeof req.query[\"code\"] === \"string\" ? req.query[\"code\"] : null;\n const state =\n typeof req.query[\"state\"] === \"string\" ? req.query[\"state\"] : null;\n const error =\n typeof req.query[\"error\"] === \"string\" ? req.query[\"error\"] : null;\n\n if (error) {\n const errorDescription =\n typeof req.query[\"error_description\"] === \"string\"\n ? req.query[\"error_description\"]\n : error;\n throw new IdentityError(\n \"identity/oauth-error\",\n `Provider error: ${errorDescription}`,\n 400,\n );\n }\n\n if (!code || !state) {\n throw new IdentityError(\n \"identity/oauth-invalid-callback\",\n \"Missing code or state in OAuth callback.\",\n 400,\n );\n }\n\n const expectedState = await options.getState(req);\n if (!expectedState) {\n throw new IdentityError(\n \"identity/oauth-state-missing\",\n \"No OAuth state found in session. The request may have expired.\",\n 400,\n );\n }\n\n const result = await oauthService.handleCallback(providerName, {\n code,\n state,\n expectedState,\n redirectUri: options.redirectUri,\n });\n\n await options.onSuccess(req, res, result);\n } catch (error) {\n next(error);\n }\n };\n}\n","import {\n Router,\n type NextFunction,\n type Request,\n type Response,\n} from \"express\";\n\nimport { IdentityError, UnauthenticatedError } from \"../errors\";\nimport type { IdentityService } from \"../identity-service\";\nimport type { IdentityLogger } from \"../logger\";\nimport { resolveLogger } from \"../logger\";\nimport type { AuthResult, AuthTokens, LoginResult } from \"../types\";\nimport { requireAuth } from \"./middleware\";\nimport { oauthAuthorize, oauthCallback } from \"./oauth-routes\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Options for controlling the refresh-token cookie. */\nexport interface CookieRefreshOptions {\n /** Cookie name. Default: `\"azlib_rt\"`. */\n name?: string;\n /** Mark the cookie as `HttpOnly`. Default: `true`. */\n httpOnly?: boolean;\n /**\n * Mark the cookie as `Secure`. Defaults to `true` when `NODE_ENV` is `\"production\"`,\n * `false` otherwise.\n */\n secure?: boolean;\n /** `SameSite` policy. Default: `\"lax\"`. */\n sameSite?: \"strict\" | \"lax\" | \"none\";\n /** Cookie path. Default: `\"/\"`. */\n path?: string;\n /** Cookie domain. Omit to use the current host. */\n domain?: string;\n}\n\n/**\n * Options for {@link createIdentityRouter}.\n */\nexport interface IdentityRouterOptions {\n /**\n * Controls how the refresh token is transported between server and client.\n *\n * - **`\"body\"`** (default) — the refresh token is included in the `tokens` object of\n * every successful `register`/`login`/`refresh` response. The client must store it\n * and send it back via the JSON body on `/refresh` and `/logout`.\n *\n * - **`{ cookie: CookieRefreshOptions }`** — the refresh token is sent as an\n * `HttpOnly` cookie. `/refresh` and `/logout` read it automatically; the response\n * body only includes the access token.\n */\n refreshToken?: \"body\" | { cookie: CookieRefreshOptions };\n\n /**\n * Base URL under which the OAuth callback routes are hosted.\n *\n * Example: `\"https://api.example.com/auth\"`.\n *\n * The callback URI for a provider becomes `{oauthBaseUrl}/{providerName}/callback`.\n * Required when the identity service has OAuth providers configured.\n */\n oauthBaseUrl?: string;\n\n /**\n * URL path prefix prepended to all identity routes.\n *\n * Default: `\"account\"`. Routes are served at `/{prefix}/login`, `/{prefix}/register`, etc.\n *\n * Mount the returned router at the application root:\n * ```ts\n * app.use(createIdentityRouter(service, { prefix: \"auth\" }));\n * // → POST /auth/login, POST /auth/register, …\n * ```\n *\n * Set to `\"\"` to omit the prefix and mount routes directly at the router's mount point.\n */\n prefix?: string;\n\n /**\n * Logger for request/response and error diagnostics.\n *\n * - Omit — defaults to a `console`-based logger with an `[identity]` prefix.\n * - Supply your own `IdentityLogger` — route output to Winston, Pino, etc.\n * - Pass `false` — disable all logging from this router.\n *\n * @example\n * ```ts\n * import pino from \"pino\";\n * app.use(createIdentityRouter(service, { logger: pino() }));\n * ```\n */\n logger?: IdentityLogger | false;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst OAUTH_STATE_COOKIE = \"azlib_oauth_state\";\n\nfunction resolveCookieName(opts: CookieRefreshOptions): string {\n return opts.name ?? \"azlib_rt\";\n}\n\n/** Parses cookies from the raw `Cookie` header without a dependency on `cookie-parser`. */\nfunction readCookie(req: Request, name: string): string | undefined {\n const raw = req.headers.cookie ?? \"\";\n for (const part of raw.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim();\n if (key === name) {\n try {\n return decodeURIComponent(part.slice(eq + 1).trim());\n } catch {\n return part.slice(eq + 1).trim();\n }\n }\n }\n return undefined;\n}\n\n/**\n * Writes the refresh token into a `Set-Cookie` header and returns a body-safe tokens\n * object that omits the refresh token.\n */\nfunction setCookieAndStripRefreshToken(\n res: Response,\n tokens: AuthTokens,\n opts: CookieRefreshOptions,\n): Omit<AuthTokens, \"refreshToken\" | \"refreshTokenExpiresAt\"> {\n const maxAge = tokens.refreshTokenExpiresAt.getTime() - Date.now();\n res.cookie(resolveCookieName(opts), tokens.refreshToken, {\n httpOnly: opts.httpOnly ?? true,\n secure: opts.secure ?? process.env.NODE_ENV === \"production\",\n sameSite: opts.sameSite ?? \"lax\",\n path: opts.path ?? \"/\",\n domain: opts.domain,\n maxAge,\n });\n const {\n refreshToken: _rt,\n refreshTokenExpiresAt: _exp,\n ...bodyTokens\n } = tokens;\n return bodyTokens;\n}\n\n/**\n * Sends the authentication result as JSON. In cookie mode the refresh token is stored\n * in a `Set-Cookie` header and excluded from the body.\n *\n * When the result is an MFA challenge (`{ kind: \"mfa_required\" }`), it is forwarded as-is\n * with HTTP 200 so the client knows to complete the TOTP step.\n */\nfunction sendLoginResult(\n res: Response,\n result: LoginResult,\n cookieOpts: CookieRefreshOptions | undefined,\n): void {\n if (\"mfaToken\" in result) {\n res.json(result);\n return;\n }\n sendAuthResult(res, result, cookieOpts);\n}\n\nfunction sendAuthResult(\n res: Response,\n result: AuthResult,\n cookieOpts: CookieRefreshOptions | undefined,\n): void {\n if (cookieOpts) {\n const tokens = setCookieAndStripRefreshToken(\n res,\n result.tokens,\n cookieOpts,\n );\n res.json({ user: result.user, tokens });\n } else {\n res.json(result);\n }\n}\n\n/**\n * Reads the refresh token from the request. In cookie mode it is read from the cookie;\n * in body mode it is expected in `req.body.refreshToken`.\n */\nfunction readRefreshToken(\n req: Request,\n cookieOpts: CookieRefreshOptions | undefined,\n): string | undefined {\n if (cookieOpts) {\n return readCookie(req, resolveCookieName(cookieOpts));\n }\n const body = req.body as Record<string, unknown> | undefined;\n const token = body?.[\"refreshToken\"];\n return typeof token === \"string\" ? token : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Router factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a pre-wired Express `Router` with all standard identity endpoints.\n *\n * Mount it once on your application:\n * ```ts\n * import express from \"express\";\n * import { createIdentityService } from \"@azlib/identity/node\";\n * import { createIdentityRouter, identityErrorHandler } from \"@azlib/identity/express\";\n *\n * const service = createIdentityService(config, store);\n * const app = express();\n *\n * app.use(express.json());\n * // Routes are served at /account/login, /account/register, etc. (default prefix)\n * app.use(createIdentityRouter(service));\n * // Or use a custom prefix:\n * app.use(createIdentityRouter(service, { prefix: \"auth\" }));\n * app.use(identityErrorHandler()); // optional convenience error handler\n * ```\n *\n * Pre-wired routes:\n *\n * | Method | Path | Description |\n * |--------|------|-------------|\n * | POST | `/register` | Create a new account |\n * | POST | `/login` | Email + password login |\n * | POST | `/refresh` | Rotate the refresh token |\n * | POST | `/logout` | Revoke the current session |\n * | GET | `/me` | Return the authenticated principal |\n * | GET | `/:provider` | Start an OAuth 2.0 authorisation flow *(optional)* |\n * | GET | `/:provider/callback` | Handle an OAuth 2.0 callback *(optional)* |\n */\nexport function createIdentityRouter(\n service: IdentityService,\n options: IdentityRouterOptions = {},\n): Router {\n const prefix = options.prefix ?? \"account\";\n const log = resolveLogger(options.logger);\n\n const router = Router();\n const cookieOpts: CookieRefreshOptions | undefined =\n typeof options.refreshToken === \"object\" && \"cookie\" in options.refreshToken\n ? options.refreshToken.cookie\n : undefined;\n\n // Request / response logging middleware\n router.use((_req: Request, res: Response, next: NextFunction): void => {\n const start = Date.now();\n res.on(\"finish\", () => {\n const ms = Date.now() - start;\n log.info(`${_req.method} ${_req.originalUrl} ${res.statusCode}`, {\n durationMs: ms,\n });\n });\n next();\n });\n\n // POST /register\n router.post(\n \"/register\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const result = await service.register(\n req.body as { email: string; password: string; displayName?: string },\n );\n sendAuthResult(res, result, cookieOpts);\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /login\n router.post(\n \"/login\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const result = await service.login(\n req.body as { email: string; password: string },\n );\n sendLoginResult(res, result, cookieOpts);\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /refresh\n router.post(\n \"/refresh\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const token = readRefreshToken(req, cookieOpts);\n if (!token) {\n throw new UnauthenticatedError(\"Refresh token is missing.\");\n }\n const result = await service.refresh(token);\n sendAuthResult(res, result, cookieOpts);\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /logout\n router.post(\n \"/logout\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const token = readRefreshToken(req, cookieOpts);\n if (token) {\n await service.logout(token);\n }\n if (cookieOpts) {\n res.clearCookie(resolveCookieName(cookieOpts), {\n path: cookieOpts.path ?? \"/\",\n });\n }\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // GET /me — requires a valid Bearer access token\n router.get(\n \"/me\",\n requireAuth(service),\n (_req: Request, res: Response): void => {\n res.json(_req.identity);\n },\n );\n\n // ---------------------------------------------------------------------------\n // Email verification\n // ---------------------------------------------------------------------------\n\n // POST /request-email-verification — re-send verification link (requires auth)\n router.post(\n \"/request-email-verification\",\n requireAuth(service),\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n await service.requestEmailVerification(req.identity!.userId);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /verify-email — consume the token from the verification link\n router.post(\n \"/verify-email\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { token } = req.body as { token?: string };\n if (!token) {\n throw new UnauthenticatedError(\"token is required.\");\n }\n await service.verifyEmail(token);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // ---------------------------------------------------------------------------\n // Password reset\n // ---------------------------------------------------------------------------\n\n // POST /request-password-reset — send reset link by email\n router.post(\n \"/request-password-reset\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { email } = req.body as { email?: string };\n if (!email) {\n throw new UnauthenticatedError(\"email is required.\");\n }\n await service.requestPasswordReset(email);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /reset-password — set new password with token\n router.post(\n \"/reset-password\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { token, newPassword } = req.body as {\n token?: string;\n newPassword?: string;\n };\n if (!token || !newPassword) {\n throw new UnauthenticatedError(\"token and newPassword are required.\");\n }\n await service.resetPassword(token, newPassword);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // ---------------------------------------------------------------------------\n // Two-factor authentication (2FA / TOTP)\n // ---------------------------------------------------------------------------\n\n // POST /mfa/verify — complete an MFA challenge after login\n router.post(\n \"/mfa/verify\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { mfaToken, code } = req.body as {\n mfaToken?: string;\n code?: string;\n };\n if (!mfaToken || !code) {\n throw new UnauthenticatedError(\"mfaToken and code are required.\");\n }\n const result = await service.verifyMfaChallenge(mfaToken, code);\n sendAuthResult(res, result, cookieOpts);\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /2fa/setup — begin 2FA setup (requires auth)\n router.post(\n \"/2fa/setup\",\n requireAuth(service),\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const result = await service.setup2FA(req.identity!.userId);\n res.json(result);\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /2fa/enable — confirm and enable 2FA (requires auth)\n router.post(\n \"/2fa/enable\",\n requireAuth(service),\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { code } = req.body as { code?: string };\n if (!code) {\n throw new UnauthenticatedError(\"code is required.\");\n }\n await service.enable2FA(req.identity!.userId, code);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /2fa/disable — disable 2FA (requires auth)\n router.post(\n \"/2fa/disable\",\n requireAuth(service),\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n const { code } = req.body as { code?: string };\n if (!code) {\n throw new UnauthenticatedError(\"code is required.\");\n }\n await service.disable2FA(req.identity!.userId, code);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // ---------------------------------------------------------------------------\n // Account management (admin)\n // ---------------------------------------------------------------------------\n\n // POST /admin/lock/:userId — lock an account\n router.post(\n \"/admin/lock/:userId\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n await service.lockAccount(req.params[\"userId\"] as string);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /admin/unlock/:userId — unlock an account\n router.post(\n \"/admin/unlock/:userId\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n await service.unlockAccount(req.params[\"userId\"] as string);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // POST /admin/disable/:userId — disable an account\n router.post(\n \"/admin/disable/:userId\",\n async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n try {\n await service.disableAccount(req.params[\"userId\"] as string);\n res.status(204).end();\n } catch (err) {\n next(err);\n }\n },\n );\n\n // OAuth routes — only when providers are configured on the service\n if (service.oauth) {\n const oauthService = service.oauth;\n const baseUrl = options.oauthBaseUrl ?? \"\";\n\n const stateCookieOptions = {\n httpOnly: true,\n sameSite: \"lax\" as const,\n secure: process.env.NODE_ENV === \"production\",\n maxAge: 10 * 60 * 1000, // 10 minutes\n };\n\n for (const providerName of oauthService.providers) {\n const redirectUri = `${baseUrl}/${providerName}/callback`;\n\n router.get(\n `/${providerName}`,\n oauthAuthorize(oauthService, providerName, {\n redirectUri,\n setState: (_req, oauthRes, state) => {\n oauthRes.cookie(OAUTH_STATE_COOKIE, state, stateCookieOptions);\n },\n }),\n );\n\n router.get(\n `/${providerName}/callback`,\n oauthCallback(oauthService, providerName, {\n redirectUri,\n getState: (oauthReq) =>\n readCookie(oauthReq, OAUTH_STATE_COOKIE) ?? null,\n setState: (_oauthReq, oauthRes, _state) => {\n oauthRes.clearCookie(OAUTH_STATE_COOKIE);\n },\n onSuccess: (_oauthReq, oauthRes, result) => {\n sendAuthResult(oauthRes, result, cookieOpts);\n },\n }),\n );\n }\n }\n\n // Mount the inner router at the configured prefix.\n const outer = Router();\n if (prefix) {\n outer.use(`/${prefix}`, router);\n } else {\n outer.use(router);\n }\n return outer;\n}\n\n// ---------------------------------------------------------------------------\n// Convenience error handler\n// ---------------------------------------------------------------------------\n\n/**\n * Factory that creates an Express error-handling middleware which converts\n * {@link IdentityError} instances to structured JSON responses.\n * Mount it after the identity router:\n *\n * ```ts\n * app.use(createIdentityRouter(service, { prefix: \"auth\" }));\n * app.use(identityErrorHandler()); // default console logger\n * app.use(identityErrorHandler({ logger: false })); // silent\n * ```\n *\n * @param options.logger - Logger for recording serialised error details.\n * Pass `false` to disable. Defaults to the console logger.\n */\nexport function identityErrorHandler(\n options: { logger?: IdentityLogger | false } = {},\n): (err: unknown, req: Request, res: Response, next: NextFunction) => void {\n const log = resolveLogger(options.logger);\n return (\n err: unknown,\n _req: Request,\n res: Response,\n next: NextFunction,\n ): void => {\n if (err instanceof IdentityError) {\n log.debug(\"identity error\", {\n code: err.code,\n status: err.statusCode,\n message: err.message,\n });\n res.status(err.statusCode).json({ code: err.code, message: err.message });\n return;\n }\n log.error(\"unhandled error in identity router\", { error: String(err) });\n next(err);\n };\n}\n"],"mappings":"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"}
@@ -1,7 +1,10 @@
1
- import { i as IdentityConfigError } from "./errors-CEmnZxIn.mjs";
1
+ import { i as IdentityConfigError, s as InvalidTokenError } from "./errors-CEmnZxIn.mjs";
2
2
  import { r as resolveLogger } from "./logger-CcCHJVVe.mjs";
3
+ import { jwtVerify } from "jose";
3
4
  //#region core/config.ts
4
5
  const MIN_SECRET_LENGTH = 32;
6
+ /** Default JWT `iss` claim used when `IdentityConfigInput.issuer` is omitted. */
7
+ const DEFAULT_ISSUER = "azlib-identity";
5
8
  const defaultGenerateId = () => globalThis.crypto.randomUUID();
6
9
  const defaultNow = () => /* @__PURE__ */ new Date();
7
10
  /**
@@ -238,6 +241,37 @@ async function linkOAuthAccount(store, userId, provider, userInfo, now) {
238
241
  await store.createOAuthLink(link);
239
242
  }
240
243
  //#endregion
244
+ //#region core/access-token.ts
245
+ /**
246
+ * Verifies an HS256 access token and returns its claims.
247
+ *
248
+ * This helper is framework-agnostic and safe to run on the Edge (no `node:crypto`).
249
+ * Hydrating the principal still requires {@link IdentityService.authenticate} in Node.
250
+ *
251
+ * Throws {@link InvalidTokenError} when the token is missing, malformed, expired,
252
+ * signed with the wrong secret, or missing required claims.
253
+ */
254
+ async function verifyAccessToken(token, options) {
255
+ if (typeof token !== "string" || token.trim().length === 0) throw new InvalidTokenError();
256
+ try {
257
+ const secret = new TextEncoder().encode(options.secret);
258
+ const { payload } = await jwtVerify(token.trim(), secret, {
259
+ issuer: options.issuer ?? "azlib-identity",
260
+ audience: options.audience
261
+ });
262
+ if (typeof payload.sub !== "string" || payload.sub.length === 0 || typeof payload.authVersion !== "number" || !Number.isFinite(payload.authVersion)) throw new InvalidTokenError();
263
+ return {
264
+ sub: payload.sub,
265
+ authVersion: payload.authVersion,
266
+ iat: payload.iat ?? 0,
267
+ exp: payload.exp ?? 0
268
+ };
269
+ } catch (error) {
270
+ if (error instanceof InvalidTokenError) throw error;
271
+ throw new InvalidTokenError();
272
+ }
273
+ }
274
+ //#endregion
241
275
  //#region schema/model.ts
242
276
  const identitySchemaModel = { tables: [
243
277
  {
@@ -405,6 +439,6 @@ const identitySchemaModel = { tables: [
405
439
  }
406
440
  ] };
407
441
  //#endregion
408
- export { hydratePrincipal as a, isAuthorized as c, createOAuthService as i, resolveIdentityConfig as l, OAuthProviderNotFoundError as n, issueSession as o, OAuthStateMismatchError as r, evaluateAuthorization as s, identitySchemaModel as t };
442
+ export { createOAuthService as a, evaluateAuthorization as c, resolveIdentityConfig as d, OAuthStateMismatchError as i, isAuthorized as l, verifyAccessToken as n, hydratePrincipal as o, OAuthProviderNotFoundError as r, issueSession as s, identitySchemaModel as t, DEFAULT_ISSUER as u };
409
443
 
410
- //# sourceMappingURL=identity-Bz9RDOvT.mjs.map
444
+ //# sourceMappingURL=identity-D0qyca_c.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-D0qyca_c.mjs","names":[],"sources":["../core/config.ts","../core/authorization.ts","../core/session-state.ts","../core/oauth/oauth-service.ts","../core/access-token.ts","../schema/model.ts"],"sourcesContent":["import { IdentityConfigError } from \"./errors\";\nimport type { IdentityLogger } from \"./logger\";\nimport { resolveLogger } from \"./logger\";\nimport type { NotificationService } from \"./notification\";\nimport type { IdentityEvent } from \"./types\";\n\n/**\n * Pluggable override points for advanced consumers. All are optional; sensible Node\n * defaults are supplied by the runtime entry (`@azlib/identity/node`).\n */\nexport interface IdentityOverrides {\n /** Returns the current time. Override in tests for deterministic clocks. */\n now?: () => Date;\n /** Generates a unique id (user ids, session ids). Defaults to `crypto.randomUUID`. */\n generateId?: () => string;\n /**\n * Lifecycle hook invoked for every audit event (registration, login, refresh, etc.).\n * Use it to forward events to your own logging/analytics pipeline. It must never throw\n * for normal operation; failures are swallowed so auditing cannot break auth flows.\n */\n onEvent?: (event: IdentityEvent) => void | Promise<void>;\n}\n\n/** Account lockout policy applied after repeated failed logins. */\nexport interface LockoutConfig {\n /**\n * How many consecutive failed login attempts are allowed before the account is\n * temporarily locked. Set to 0 to disable lockout. Default 10.\n */\n maxFailedAttempts: number;\n /**\n * How long (in seconds) a locked account is blocked before automatic unlock.\n * Default 900 (15 minutes). Set to 0 for permanent lock (manual unlock required).\n */\n durationSeconds: number;\n}\n\n/** Raw configuration accepted from consumers. */\nexport interface IdentityConfigInput {\n /**\n * Secret used to sign and verify access tokens (HMAC). Must be at least 32 characters.\n * Provide via environment, never hardcode.\n */\n accessTokenSecret: string;\n /** Access-token lifetime in seconds. Default 900 (15 minutes). */\n accessTokenTtlSeconds?: number;\n /** Refresh-token lifetime in seconds. Default 1209600 (14 days). */\n refreshTokenTtlSeconds?: number;\n /** Token issuer (`iss`). Default `azlib-identity`. */\n issuer?: string;\n /** Token audience (`aud`). Optional. */\n audience?: string;\n /** scrypt cost parameter `N`. Default 16384. */\n passwordScryptCost?: number;\n /**\n * Account lockout policy. Omit or set `maxFailedAttempts: 0` to disable. Default: 10\n * attempts, 15-minute lockout.\n */\n lockout?: Partial<LockoutConfig>;\n /**\n * Pluggable notification service for email verification, password reset, and SMS 2FA.\n * Omit if you do not need these features.\n */\n notifications?: NotificationService;\n /**\n * Logger used throughout the package. Pass your own `IdentityLogger` to route\n * diagnostic output to Winston, Pino, or any other provider. Pass `false` to silence\n * all logging. Defaults to a console-based logger when omitted.\n */\n logger?: IdentityLogger | false;\n /** Override points for clock and id generation. */\n overrides?: IdentityOverrides;\n}\n\n/** Fully resolved, validated configuration used internally. */\nexport interface IdentityConfig {\n accessTokenSecret: string;\n accessTokenTtlSeconds: number;\n refreshTokenTtlSeconds: number;\n issuer: string;\n audience: string | undefined;\n passwordScryptCost: number;\n lockout: LockoutConfig;\n notifications: NotificationService | undefined;\n /** Resolved logger; never `false` — `false` becomes {@link noopLogger}. */\n logger: IdentityLogger;\n now: () => Date;\n generateId: () => string;\n onEvent: ((event: IdentityEvent) => void | Promise<void>) | undefined;\n}\n\nconst MIN_SECRET_LENGTH = 32;\n\n/** Default JWT `iss` claim used when `IdentityConfigInput.issuer` is omitted. */\nexport const DEFAULT_ISSUER = \"azlib-identity\";\n\nconst defaultGenerateId = (): string => globalThis.crypto.randomUUID();\nconst defaultNow = (): Date => new Date();\n\n/**\n * Validates and applies defaults to consumer-provided configuration.\n * Throws {@link IdentityConfigError} when required values are missing or invalid.\n */\nexport function resolveIdentityConfig(\n input: IdentityConfigInput,\n): IdentityConfig {\n if (\n !input.accessTokenSecret ||\n input.accessTokenSecret.length < MIN_SECRET_LENGTH\n ) {\n throw new IdentityConfigError(\n `accessTokenSecret must be at least ${MIN_SECRET_LENGTH} characters.`,\n );\n }\n\n const accessTokenTtlSeconds = input.accessTokenTtlSeconds ?? 900;\n const refreshTokenTtlSeconds =\n input.refreshTokenTtlSeconds ?? 60 * 60 * 24 * 14;\n const passwordScryptCost = input.passwordScryptCost ?? 16384;\n\n if (accessTokenTtlSeconds <= 0) {\n throw new IdentityConfigError(\"accessTokenTtlSeconds must be positive.\");\n }\n if (refreshTokenTtlSeconds <= accessTokenTtlSeconds) {\n throw new IdentityConfigError(\n \"refreshTokenTtlSeconds must be greater than accessTokenTtlSeconds.\",\n );\n }\n if ((passwordScryptCost & (passwordScryptCost - 1)) !== 0) {\n throw new IdentityConfigError(\"passwordScryptCost must be a power of two.\");\n }\n\n return {\n accessTokenSecret: input.accessTokenSecret,\n accessTokenTtlSeconds,\n refreshTokenTtlSeconds,\n issuer: input.issuer ?? DEFAULT_ISSUER,\n audience: input.audience,\n passwordScryptCost,\n lockout: {\n maxFailedAttempts: input.lockout?.maxFailedAttempts ?? 10,\n durationSeconds: input.lockout?.durationSeconds ?? 900,\n },\n now: input.overrides?.now ?? defaultNow,\n generateId: input.overrides?.generateId ?? defaultGenerateId,\n onEvent: input.overrides?.onEvent,\n notifications: input.notifications,\n logger: resolveLogger(input.logger),\n };\n}\n","import type { AuthenticatedIdentity, Permission } from \"./types\";\n\n/**\n * Context passed to authorization checks. `resource` is an opaque consumer-supplied\n * object (e.g. a loaded document) used by ownership/relationship policies.\n */\nexport interface AuthorizationContext<TResource = unknown> {\n principal: AuthenticatedIdentity;\n /** The action being attempted, typically a permission string. */\n action: string;\n /** The target resource, if any. */\n resource?: TResource;\n}\n\n/**\n * A policy rule returns:\n * - `true` to allow,\n * - `false`/`undefined` to abstain (deny-by-default unless another rule allows),\n * It must never throw for normal \"denied\" outcomes.\n */\nexport type PolicyRule<TResource = unknown> = (\n context: AuthorizationContext<TResource>,\n) => boolean | undefined | Promise<boolean | undefined>;\n\n/** A named requirement combining a required permission and optional ownership policy. */\nexport interface AuthorizationRequirement<TResource = unknown> {\n /** Permission the principal must hold. Omit to rely solely on policies. */\n permission?: Permission;\n /** Optional ownership/relationship rule evaluated against the resource. */\n policy?: PolicyRule<TResource>;\n}\n\n/** Result of an authorization decision. */\nexport interface AuthorizationDecision {\n allowed: boolean;\n /** Non-sensitive reason for diagnostics/audit. */\n reason: string;\n}\n\nconst hasPermission = (\n principal: AuthenticatedIdentity,\n permission: Permission,\n): boolean => principal.permissions.includes(permission);\n\n/**\n * Evaluates a requirement using deny-by-default semantics:\n * 1. If a `permission` is required and the principal lacks it, deny.\n * 2. If a `policy` is provided, it must return `true` to allow.\n * 3. If neither is provided, deny (nothing explicitly granted access).\n */\nexport async function evaluateAuthorization<TResource = unknown>(\n requirement: AuthorizationRequirement<TResource>,\n context: AuthorizationContext<TResource>,\n): Promise<AuthorizationDecision> {\n const { permission, policy } = requirement;\n\n if (\n permission !== undefined &&\n !hasPermission(context.principal, permission)\n ) {\n return { allowed: false, reason: \"missing-permission\" };\n }\n\n if (policy) {\n const result = await policy(context);\n if (result !== true) {\n return { allowed: false, reason: \"policy-denied\" };\n }\n return { allowed: true, reason: \"policy-allowed\" };\n }\n\n if (permission !== undefined) {\n return { allowed: true, reason: \"permission-granted\" };\n }\n\n return { allowed: false, reason: \"no-grant\" };\n}\n\n/** Convenience boolean form of {@link evaluateAuthorization}. */\nexport async function isAuthorized<TResource = unknown>(\n requirement: AuthorizationRequirement<TResource>,\n context: AuthorizationContext<TResource>,\n): Promise<boolean> {\n return (await evaluateAuthorization(requirement, context)).allowed;\n}\n","import type { IdentityConfig } from \"./config\";\nimport type { IdentityStore } from \"./identity-store\";\nimport type { TokenService } from \"./token-service\";\nimport type { AuthResult, AuthenticatedIdentity, IdentityUser } from \"./types\";\n\n/** Dependencies shared by the register/login/refresh flows. */\nexport interface SessionDeps {\n config: IdentityConfig;\n store: IdentityStore;\n tokenService: TokenService;\n}\n\n/**\n * Builds the runtime principal for a user by reading their roles and permissions from\n * the store. Returns the shape attached to authenticated requests.\n */\nexport async function hydratePrincipal(\n store: IdentityStore,\n user: IdentityUser,\n): Promise<AuthenticatedIdentity> {\n const [roles, permissions] = await Promise.all([\n store.listRolesForUser(user.userId),\n store.listPermissionsForUser(user.userId),\n ]);\n\n return {\n userId: user.userId,\n email: user.email,\n displayName: user.displayName,\n status: user.status,\n emailVerified: user.emailVerifiedAt !== null,\n roles: roles.map((role) => role.name),\n permissions: [...permissions],\n };\n}\n\n/**\n * Issues a fresh token pair for a user and persists a new refresh session\n * (storing only the hash of the refresh token).\n */\nexport async function issueSession(\n deps: SessionDeps,\n user: IdentityUser,\n): Promise<AuthResult> {\n const { config, store, tokenService } = deps;\n const now = config.now();\n\n const principal = await hydratePrincipal(store, user);\n const access = await tokenService.issueAccessToken(\n user.userId,\n user.authVersion,\n );\n const sessionId = config.generateId();\n const refresh = tokenService.createRefreshToken(sessionId);\n\n const refreshExpiresAt = new Date(\n now.getTime() + config.refreshTokenTtlSeconds * 1000,\n );\n await store.createSession({\n sessionId,\n userId: user.userId,\n refreshTokenHash: refresh.hash,\n createdAt: now,\n expiresAt: refreshExpiresAt,\n });\n\n return {\n user: principal,\n tokens: {\n accessToken: access.token,\n refreshToken: refresh.token,\n accessTokenExpiresAt: access.expiresAt,\n refreshTokenExpiresAt: refreshExpiresAt,\n },\n };\n}\n","import type { AuditLogger } from \"../audit\";\nimport type { IdentityConfig } from \"../config\";\nimport type { IdentityStore } from \"../identity-store\";\nimport { issueSession, type SessionDeps } from \"../session-state\";\nimport type { AuthResult, OAuthLinkedAccount } from \"../types\";\nimport type { OAuthProvider } from \"./oauth-provider\";\n\n/** Result of {@link OAuthService.buildAuthorizationUrl}. */\nexport interface OAuthAuthorizationUrl {\n /** The full provider authorization URL to redirect the user to. */\n url: string;\n /**\n * An opaque CSRF-prevention state value. Store this in a signed cookie or server\n * session and pass it as `expectedState` when calling {@link OAuthService.handleCallback}.\n */\n state: string;\n}\n\n/** Parameters for {@link OAuthService.handleCallback}. */\nexport interface OAuthCallbackParams {\n /** The authorization code received from the provider callback query string. */\n code: string;\n /** The `state` value received from the provider callback query string. */\n state: string;\n /**\n * The expected state value previously returned by {@link OAuthService.buildAuthorizationUrl}.\n * The callback will be rejected when they do not match (CSRF protection).\n */\n expectedState: string;\n /** The same redirect URI that was used in the authorization request. */\n redirectUri: string;\n /** Optional scopes to override for this callback's code exchange. */\n scopes?: string[];\n}\n\n/** The OAuth2 / OIDC surface of the identity service. */\nexport interface OAuthService {\n /** Returns the registered provider names, e.g. `[\"google\", \"microsoft\"]`. */\n readonly providers: readonly string[];\n\n /**\n * Builds the authorization URL for the given provider. Redirect the user's browser to\n * the returned `url`, then store `state` for later verification.\n */\n buildAuthorizationUrl(\n providerName: string,\n redirectUri: string,\n scopes?: string[],\n ): OAuthAuthorizationUrl;\n\n /**\n * Handles the provider callback after user consent. Verifies state, exchanges the code\n * for tokens, resolves or provisions a local user, and returns a full auth result.\n *\n * Throws {@link OAuthProviderNotFoundError} for unknown providers.\n * Throws a generic {@link IdentityError} when state mismatches (CSRF guard).\n */\n handleCallback(\n providerName: string,\n params: OAuthCallbackParams,\n ): Promise<AuthResult>;\n}\n\n/** Thrown when an OAuth operation targets an unregistered provider name. */\nexport class OAuthProviderNotFoundError extends Error {\n constructor(providerName: string) {\n super(`No OAuth provider registered with name \"${providerName}\".`);\n this.name = \"OAuthProviderNotFoundError\";\n }\n}\n\n/** Thrown when the callback state does not match the expected state (CSRF guard). */\nexport class OAuthStateMismatchError extends Error {\n constructor() {\n super(\n \"OAuth state mismatch. The callback may have been replayed or tampered with.\",\n );\n this.name = \"OAuthStateMismatchError\";\n }\n}\n\n/** Dependencies for {@link createOAuthService}. */\nexport interface OAuthServiceDeps {\n providers: readonly OAuthProvider[];\n config: IdentityConfig;\n store: IdentityStore;\n sessionDeps: SessionDeps;\n audit: AuditLogger;\n}\n\n/**\n * Creates the OAuth2 / OIDC service. Wire into the identity service by passing configured\n * provider instances (e.g. `createGoogleOAuthProvider(...)`) to the `oauth.providers` list\n * in {@link IdentityConfigInput}.\n */\nexport function createOAuthService(deps: OAuthServiceDeps): OAuthService {\n const { providers, config, store, sessionDeps, audit } = deps;\n\n const providerMap = new Map<string, OAuthProvider>();\n for (const p of providers) {\n providerMap.set(p.name, p);\n }\n\n function getProvider(name: string): OAuthProvider {\n const p = providerMap.get(name);\n if (!p) throw new OAuthProviderNotFoundError(name);\n return p;\n }\n\n return {\n get providers() {\n return [...providerMap.keys()];\n },\n\n buildAuthorizationUrl(providerName, redirectUri, scopes) {\n const provider = getProvider(providerName);\n const state = config.generateId();\n const url = provider.buildAuthorizationUrl({\n redirectUri,\n state,\n scopes,\n });\n return { url, state };\n },\n\n async handleCallback(providerName, params) {\n // CSRF guard — must be the first check.\n if (params.state !== params.expectedState) {\n throw new OAuthStateMismatchError();\n }\n\n const provider = getProvider(providerName);\n\n // Exchange the authorization code for provider tokens.\n const tokens = await provider.exchangeCode({\n code: params.code,\n redirectUri: params.redirectUri,\n });\n\n // Fetch normalized user info from the provider.\n const userInfo = await provider.fetchUserInfo(tokens);\n\n const now = config.now();\n\n // 1. Look up by OAuth link (returning user).\n if (store.findUserByOAuthId) {\n const linkedUser = await store.findUserByOAuthId(\n providerName,\n userInfo.providerUserId,\n );\n if (linkedUser) {\n await audit.record(\"user.login.succeeded\", linkedUser.userId, {\n email: linkedUser.email,\n provider: providerName,\n });\n return issueSession(sessionDeps, linkedUser);\n }\n }\n\n // 2. Look up by email (existing local account — link the provider).\n if (userInfo.email) {\n const existingUser = await store.findUserByEmail(userInfo.email);\n if (existingUser) {\n await linkOAuthAccount(\n store,\n existingUser.userId,\n providerName,\n userInfo,\n now,\n );\n await audit.record(\"user.oauth.linked\", existingUser.userId, {\n provider: providerName,\n providerUserId: userInfo.providerUserId,\n });\n await audit.record(\"user.login.succeeded\", existingUser.userId, {\n email: existingUser.email,\n provider: providerName,\n });\n return issueSession(sessionDeps, existingUser);\n }\n }\n\n // 3. Provision a new local user and link the provider.\n const newUser = await store.createUser({\n userId: config.generateId(),\n email:\n userInfo.email ?? `oauth:${providerName}:${userInfo.providerUserId}`,\n displayName: userInfo.displayName,\n status: \"active\",\n authVersion: 0,\n createdAt: now,\n updatedAt: now,\n });\n await linkOAuthAccount(\n store,\n newUser.userId,\n providerName,\n userInfo,\n now,\n );\n await audit.record(\"user.registered\", newUser.userId, {\n provider: providerName,\n providerUserId: userInfo.providerUserId,\n });\n await audit.record(\"user.oauth.linked\", newUser.userId, {\n provider: providerName,\n providerUserId: userInfo.providerUserId,\n });\n await audit.record(\"user.login.succeeded\", newUser.userId, {\n email: newUser.email,\n provider: providerName,\n });\n return issueSession(sessionDeps, newUser);\n },\n };\n}\n\nasync function linkOAuthAccount(\n store: IdentityStore,\n userId: string,\n provider: string,\n userInfo: {\n providerUserId: string;\n email: string | null;\n displayName: string | null;\n },\n now: Date,\n): Promise<void> {\n if (!store.createOAuthLink) return; // Store does not support OAuth links — skip silently.\n\n const link: OAuthLinkedAccount = {\n userId,\n provider,\n providerUserId: userInfo.providerUserId,\n email: userInfo.email,\n displayName: userInfo.displayName,\n linkedAt: now,\n };\n await store.createOAuthLink(link);\n}\n","import { jwtVerify } from \"jose\";\n\nimport { DEFAULT_ISSUER } from \"./config\";\nimport { InvalidTokenError } from \"./errors\";\nimport type { AccessTokenClaims } from \"./types\";\n\n/**\n * Options for {@link verifyAccessToken}. Edge-safe: depends only on `jose`.\n */\nexport interface VerifyAccessTokenOptions {\n /** HMAC secret used to sign access tokens. Must match `accessTokenSecret`. */\n secret: string;\n /** Expected JWT issuer. Default {@link DEFAULT_ISSUER}. */\n issuer?: string;\n /** Expected JWT audience. Omit when tokens are minted without `aud`. */\n audience?: string;\n}\n\n/**\n * Verifies an HS256 access token and returns its claims.\n *\n * This helper is framework-agnostic and safe to run on the Edge (no `node:crypto`).\n * Hydrating the principal still requires {@link IdentityService.authenticate} in Node.\n *\n * Throws {@link InvalidTokenError} when the token is missing, malformed, expired,\n * signed with the wrong secret, or missing required claims.\n */\nexport async function verifyAccessToken(\n token: string,\n options: VerifyAccessTokenOptions,\n): Promise<AccessTokenClaims> {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new InvalidTokenError();\n }\n\n try {\n const secret = new TextEncoder().encode(options.secret);\n const { payload } = await jwtVerify(token.trim(), secret, {\n issuer: options.issuer ?? DEFAULT_ISSUER,\n audience: options.audience,\n });\n if (\n typeof payload.sub !== \"string\" ||\n payload.sub.length === 0 ||\n typeof payload.authVersion !== \"number\" ||\n !Number.isFinite(payload.authVersion)\n ) {\n throw new InvalidTokenError();\n }\n return {\n sub: payload.sub,\n authVersion: payload.authVersion,\n iat: payload.iat ?? 0,\n exp: payload.exp ?? 0,\n };\n } catch (error) {\n if (error instanceof InvalidTokenError) throw error;\n throw new InvalidTokenError();\n }\n}\n","/**\n * Logical description of the relational schema `@azlib/identity` expects.\n *\n * Concrete DDL is shipped alongside this module:\n * - `@azlib/identity/schema/postgres.sql`\n * - `@azlib/identity/schema/mysql.sql`\n * - `@azlib/identity/schema/sqlite.sql`\n * - `@azlib/identity/schema/prisma.schema`\n *\n * This object is documentation-as-data so consumers and tooling can introspect the\n * expected tables without parsing SQL.\n */\nexport interface SchemaColumn {\n name: string;\n description: string;\n nullable: boolean;\n}\n\nexport interface SchemaTable {\n name: string;\n description: string;\n columns: readonly SchemaColumn[];\n}\n\nexport interface SchemaModel {\n tables: readonly SchemaTable[];\n}\n\nexport const identitySchemaModel: SchemaModel = {\n tables: [\n {\n name: \"identity_users\",\n description: \"Core user accounts.\",\n columns: [\n { name: \"user_id\", description: \"Primary key.\", nullable: false },\n {\n name: \"email\",\n description: \"Unique, case-insensitive login email.\",\n nullable: false,\n },\n {\n name: \"display_name\",\n description: \"Optional display name.\",\n nullable: true,\n },\n {\n name: \"status\",\n description: \"active | disabled | locked.\",\n nullable: false,\n },\n {\n name: \"email_verified_at\",\n description: \"When email was verified.\",\n nullable: true,\n },\n {\n name: \"auth_version\",\n description: \"Token invalidation counter.\",\n nullable: false,\n },\n {\n name: \"created_at\",\n description: \"Creation timestamp.\",\n nullable: false,\n },\n {\n name: \"updated_at\",\n description: \"Last update timestamp.\",\n nullable: false,\n },\n ],\n },\n {\n name: \"identity_credentials\",\n description: \"Password hashes, one per user.\",\n columns: [\n {\n name: \"user_id\",\n description: \"FK to identity_users.\",\n nullable: false,\n },\n {\n name: \"password_hash\",\n description: \"Algorithm-tagged hash.\",\n nullable: false,\n },\n {\n name: \"updated_at\",\n description: \"Last update timestamp.\",\n nullable: false,\n },\n ],\n },\n {\n name: \"identity_sessions\",\n description: \"Server-side refresh sessions with hashed tokens.\",\n columns: [\n { name: \"session_id\", description: \"Primary key.\", nullable: false },\n {\n name: \"user_id\",\n description: \"FK to identity_users.\",\n nullable: false,\n },\n {\n name: \"refresh_token_hash\",\n description: \"Hash of the active refresh token.\",\n nullable: false,\n },\n {\n name: \"created_at\",\n description: \"Creation timestamp.\",\n nullable: false,\n },\n {\n name: \"expires_at\",\n description: \"Expiry timestamp.\",\n nullable: false,\n },\n {\n name: \"revoked_at\",\n description: \"Set when rotated or revoked.\",\n nullable: true,\n },\n ],\n },\n {\n name: \"identity_roles\",\n description: \"Named roles.\",\n columns: [\n { name: \"role_id\", description: \"Primary key.\", nullable: false },\n { name: \"name\", description: \"Unique role name.\", nullable: false },\n {\n name: \"description\",\n description: \"Optional description.\",\n nullable: true,\n },\n ],\n },\n {\n name: \"identity_user_roles\",\n description: \"User-to-role assignments.\",\n columns: [\n {\n name: \"user_id\",\n description: \"FK to identity_users.\",\n nullable: false,\n },\n {\n name: \"role_id\",\n description: \"FK to identity_roles.\",\n nullable: false,\n },\n ],\n },\n {\n name: \"identity_role_permissions\",\n description: \"Permissions granted to roles.\",\n columns: [\n {\n name: \"role_id\",\n description: \"FK to identity_roles.\",\n nullable: false,\n },\n {\n name: \"permission\",\n description: \"Permission string.\",\n nullable: false,\n },\n ],\n },\n {\n name: \"identity_user_permissions\",\n description: \"Permissions granted directly to users.\",\n columns: [\n {\n name: \"user_id\",\n description: \"FK to identity_users.\",\n nullable: false,\n },\n {\n name: \"permission\",\n description: \"Permission string.\",\n nullable: false,\n },\n ],\n },\n ],\n};\n"],"mappings":"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AM;GAChE;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;EACF;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,SAAS;GACP;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;EACF;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,SAAS;GACP;IAAE,MAAM;IAAc,aAAa;IAAgB,UAAU;GAAM;GACnE;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;GACA;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;EACF;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,SAAS;GACP;IAAE,MAAM;IAAW,aAAa;IAAgB,UAAU;GAAM;GAChE;IAAE,MAAM;IAAQ,aAAa;IAAqB,UAAU;GAAM;GAClE;IACE,MAAM;IACN,aAAa;IACb,UAAU;GACZ;EACF;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,SAAS,CACP;GACE,MAAM;GACN,aAAa;GACb,UAAU;EACZ,GACA;GACE,MAAM;GACN,aAAa;GACb,UAAU;EACZ,CACF;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,SAAS,CACP;GACE,MAAM;GACN,aAAa;GACb,UAAU;EACZ,GACA;GACE,MAAM;GACN,aAAa;GACb,UAAU;EACZ,CACF;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,SAAS,CACP;GACE,MAAM;GACN,aAAa;GACb,UAAU;EACZ,GACA;GACE,MAAM;GACN,aAAa;GACb,UAAU;EACZ,CACF;CACF;AACF,EACF"}
@@ -1,7 +1,10 @@
1
1
  const require_errors = require("./errors-Bcjx9o6g.cjs");
2
2
  const require_logger = require("./logger-Be1wDzBC.cjs");
3
+ let jose = require("jose");
3
4
  //#region core/config.ts
4
5
  const MIN_SECRET_LENGTH = 32;
6
+ /** Default JWT `iss` claim used when `IdentityConfigInput.issuer` is omitted. */
7
+ const DEFAULT_ISSUER = "azlib-identity";
5
8
  const defaultGenerateId = () => globalThis.crypto.randomUUID();
6
9
  const defaultNow = () => /* @__PURE__ */ new Date();
7
10
  /**
@@ -238,6 +241,37 @@ async function linkOAuthAccount(store, userId, provider, userInfo, now) {
238
241
  await store.createOAuthLink(link);
239
242
  }
240
243
  //#endregion
244
+ //#region core/access-token.ts
245
+ /**
246
+ * Verifies an HS256 access token and returns its claims.
247
+ *
248
+ * This helper is framework-agnostic and safe to run on the Edge (no `node:crypto`).
249
+ * Hydrating the principal still requires {@link IdentityService.authenticate} in Node.
250
+ *
251
+ * Throws {@link InvalidTokenError} when the token is missing, malformed, expired,
252
+ * signed with the wrong secret, or missing required claims.
253
+ */
254
+ async function verifyAccessToken(token, options) {
255
+ if (typeof token !== "string" || token.trim().length === 0) throw new require_errors.InvalidTokenError();
256
+ try {
257
+ const secret = new TextEncoder().encode(options.secret);
258
+ const { payload } = await (0, jose.jwtVerify)(token.trim(), secret, {
259
+ issuer: options.issuer ?? "azlib-identity",
260
+ audience: options.audience
261
+ });
262
+ if (typeof payload.sub !== "string" || payload.sub.length === 0 || typeof payload.authVersion !== "number" || !Number.isFinite(payload.authVersion)) throw new require_errors.InvalidTokenError();
263
+ return {
264
+ sub: payload.sub,
265
+ authVersion: payload.authVersion,
266
+ iat: payload.iat ?? 0,
267
+ exp: payload.exp ?? 0
268
+ };
269
+ } catch (error) {
270
+ if (error instanceof require_errors.InvalidTokenError) throw error;
271
+ throw new require_errors.InvalidTokenError();
272
+ }
273
+ }
274
+ //#endregion
241
275
  //#region schema/model.ts
242
276
  const identitySchemaModel = { tables: [
243
277
  {
@@ -405,6 +439,12 @@ const identitySchemaModel = { tables: [
405
439
  }
406
440
  ] };
407
441
  //#endregion
442
+ Object.defineProperty(exports, "DEFAULT_ISSUER", {
443
+ enumerable: true,
444
+ get: function() {
445
+ return DEFAULT_ISSUER;
446
+ }
447
+ });
408
448
  Object.defineProperty(exports, "OAuthProviderNotFoundError", {
409
449
  enumerable: true,
410
450
  get: function() {
@@ -459,3 +499,9 @@ Object.defineProperty(exports, "resolveIdentityConfig", {
459
499
  return resolveIdentityConfig;
460
500
  }
461
501
  });
502
+ Object.defineProperty(exports, "verifyAccessToken", {
503
+ enumerable: true,
504
+ get: function() {
505
+ return verifyAccessToken;
506
+ }
507
+ });
@@ -1,5 +1,5 @@
1
- import { F as IdentityLogger } from "./errors-CBbiRO2n.cjs";
2
- import { t as IdentityService } from "./identity-service---OTann1.cjs";
1
+ import { I as IdentityLogger } from "./errors-DVDEUk9-.mjs";
2
+ import { t as IdentityService } from "./identity-service-JtsoA6lv.mjs";
3
3
  import { NextFunction, Request, Response, Router } from "express";
4
4
  //#region core/express/identity-router.d.ts
5
5
  /** Options for controlling the refresh-token cookie. */
@@ -128,4 +128,4 @@ declare function identityErrorHandler(options?: {
128
128
  }): (err: unknown, req: Request, res: Response, next: NextFunction) => void;
129
129
  //#endregion
130
130
  export { identityErrorHandler as i, IdentityRouterOptions as n, createIdentityRouter as r, CookieRefreshOptions as t };
131
- //# sourceMappingURL=identity-router-D1xJ5H7y.d.cts.map
131
+ //# sourceMappingURL=identity-router-BiratpJR.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-router-BiratpJR.d.mts","names":[],"sources":["../core/express/identity-router.ts"],"mappings":";;;;;UAoBiB;;EAEf;;EAEA;;;;;EAKA;;EAEA;;EAEA;;EAEA;;;;;UAMe;;;;;;;;;;;;EAYf;IAA0B,QAAQ;;;;;;;;;;EAUlC;;;;;;;;;;;;;;EAeA;;;;;;;;;;;;;;EAeA,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiJK,qBACd,SAAS,iBACT,UAAS,wBACR;;;;;;;;;;;;;;;iBA0Wa,qBACd;EAAW,SAAS;KAClB,cAAc,KAAK,SAAS,KAAK,UAAU,MAAM"}
@@ -1,5 +1,5 @@
1
- import { F as IdentityLogger } from "./errors-_OcodtdV.mjs";
2
- import { t as IdentityService } from "./identity-service-WpOKb0MK.mjs";
1
+ import { I as IdentityLogger } from "./errors-0Zh7EG06.cjs";
2
+ import { t as IdentityService } from "./identity-service-DE0Ffguz.cjs";
3
3
  import { NextFunction, Request, Response, Router } from "express";
4
4
  //#region core/express/identity-router.d.ts
5
5
  /** Options for controlling the refresh-token cookie. */
@@ -128,4 +128,4 @@ declare function identityErrorHandler(options?: {
128
128
  }): (err: unknown, req: Request, res: Response, next: NextFunction) => void;
129
129
  //#endregion
130
130
  export { identityErrorHandler as i, IdentityRouterOptions as n, createIdentityRouter as r, CookieRefreshOptions as t };
131
- //# sourceMappingURL=identity-router-tj3-teBI.d.mts.map
131
+ //# sourceMappingURL=identity-router-D4oqYI60.d.cts.map