@azlib/identity 0.2.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 (69) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +387 -0
  3. package/dist/errors-BGMwaW5s.d.mts +481 -0
  4. package/dist/errors-BGMwaW5s.d.mts.map +1 -0
  5. package/dist/errors-Bcjx9o6g.cjs +111 -0
  6. package/dist/errors-C2xZAatu.d.cts +481 -0
  7. package/dist/errors-C2xZAatu.d.cts.map +1 -0
  8. package/dist/errors-CEmnZxIn.mjs +66 -0
  9. package/dist/errors-CEmnZxIn.mjs.map +1 -0
  10. package/dist/express.cjs +405 -0
  11. package/dist/express.d.cts +87 -0
  12. package/dist/express.d.cts.map +1 -0
  13. package/dist/express.d.mts +87 -0
  14. package/dist/express.d.mts.map +1 -0
  15. package/dist/express.mjs +401 -0
  16. package/dist/express.mjs.map +1 -0
  17. package/dist/identity-4eP45YIP.cjs +461 -0
  18. package/dist/identity-Bz9RDOvT.mjs +410 -0
  19. package/dist/identity-Bz9RDOvT.mjs.map +1 -0
  20. package/dist/identity-router-DBL20UWT.d.mts +115 -0
  21. package/dist/identity-router-DBL20UWT.d.mts.map +1 -0
  22. package/dist/identity-router-Dib30Waj.d.cts +115 -0
  23. package/dist/identity-router-Dib30Waj.d.cts.map +1 -0
  24. package/dist/identity-service-B9zrvE9z.d.mts +128 -0
  25. package/dist/identity-service-B9zrvE9z.d.mts.map +1 -0
  26. package/dist/identity-service-CLzKx8Z7.d.cts +128 -0
  27. package/dist/identity-service-CLzKx8Z7.d.cts.map +1 -0
  28. package/dist/identity-store-BRRahxcS.d.cts +272 -0
  29. package/dist/identity-store-BRRahxcS.d.cts.map +1 -0
  30. package/dist/identity-store-BRRahxcS.d.mts +272 -0
  31. package/dist/identity-store-BRRahxcS.d.mts.map +1 -0
  32. package/dist/index-CpufYgyn.d.cts +30 -0
  33. package/dist/index-CpufYgyn.d.cts.map +1 -0
  34. package/dist/index-CpufYgyn.d.mts +30 -0
  35. package/dist/index-CpufYgyn.d.mts.map +1 -0
  36. package/dist/index.cjs +18 -0
  37. package/dist/index.d.cts +4 -0
  38. package/dist/index.d.mts +4 -0
  39. package/dist/index.mjs +3 -0
  40. package/dist/logger-Be1wDzBC.cjs +48 -0
  41. package/dist/logger-CcCHJVVe.mjs +33 -0
  42. package/dist/logger-CcCHJVVe.mjs.map +1 -0
  43. package/dist/nestjs.cjs +516 -0
  44. package/dist/nestjs.d.cts +102 -0
  45. package/dist/nestjs.d.cts.map +1 -0
  46. package/dist/nestjs.d.mts +102 -0
  47. package/dist/nestjs.d.mts.map +1 -0
  48. package/dist/nestjs.mjs +500 -0
  49. package/dist/nestjs.mjs.map +1 -0
  50. package/dist/node.cjs +946 -0
  51. package/dist/node.d.cts +260 -0
  52. package/dist/node.d.cts.map +1 -0
  53. package/dist/node.d.mts +260 -0
  54. package/dist/node.d.mts.map +1 -0
  55. package/dist/node.mjs +890 -0
  56. package/dist/node.mjs.map +1 -0
  57. package/dist/test-utils.cjs +169 -0
  58. package/dist/test-utils.d.cts +12 -0
  59. package/dist/test-utils.d.cts.map +1 -0
  60. package/dist/test-utils.d.mts +12 -0
  61. package/dist/test-utils.d.mts.map +1 -0
  62. package/dist/test-utils.mjs +170 -0
  63. package/dist/test-utils.mjs.map +1 -0
  64. package/package.json +92 -0
  65. package/schema/model.ts +100 -0
  66. package/schema/mysql.sql +102 -0
  67. package/schema/postgres.sql +92 -0
  68. package/schema/prisma.schema +122 -0
  69. package/schema/sqlite.sql +92 -0
@@ -0,0 +1,461 @@
1
+ const require_errors = require("./errors-Bcjx9o6g.cjs");
2
+ const require_logger = require("./logger-Be1wDzBC.cjs");
3
+ //#region core/config.ts
4
+ const MIN_SECRET_LENGTH = 32;
5
+ const defaultGenerateId = () => globalThis.crypto.randomUUID();
6
+ const defaultNow = () => /* @__PURE__ */ new Date();
7
+ /**
8
+ * Validates and applies defaults to consumer-provided configuration.
9
+ * Throws {@link IdentityConfigError} when required values are missing or invalid.
10
+ */
11
+ function resolveIdentityConfig(input) {
12
+ if (!input.accessTokenSecret || input.accessTokenSecret.length < MIN_SECRET_LENGTH) throw new require_errors.IdentityConfigError(`accessTokenSecret must be at least ${MIN_SECRET_LENGTH} characters.`);
13
+ const accessTokenTtlSeconds = input.accessTokenTtlSeconds ?? 900;
14
+ const refreshTokenTtlSeconds = input.refreshTokenTtlSeconds ?? 3600 * 24 * 14;
15
+ const passwordScryptCost = input.passwordScryptCost ?? 16384;
16
+ if (accessTokenTtlSeconds <= 0) throw new require_errors.IdentityConfigError("accessTokenTtlSeconds must be positive.");
17
+ if (refreshTokenTtlSeconds <= accessTokenTtlSeconds) throw new require_errors.IdentityConfigError("refreshTokenTtlSeconds must be greater than accessTokenTtlSeconds.");
18
+ if ((passwordScryptCost & passwordScryptCost - 1) !== 0) throw new require_errors.IdentityConfigError("passwordScryptCost must be a power of two.");
19
+ return {
20
+ accessTokenSecret: input.accessTokenSecret,
21
+ accessTokenTtlSeconds,
22
+ refreshTokenTtlSeconds,
23
+ issuer: input.issuer ?? "azlib-identity",
24
+ audience: input.audience,
25
+ passwordScryptCost,
26
+ lockout: {
27
+ maxFailedAttempts: input.lockout?.maxFailedAttempts ?? 10,
28
+ durationSeconds: input.lockout?.durationSeconds ?? 900
29
+ },
30
+ now: input.overrides?.now ?? defaultNow,
31
+ generateId: input.overrides?.generateId ?? defaultGenerateId,
32
+ onEvent: input.overrides?.onEvent,
33
+ notifications: input.notifications,
34
+ logger: require_logger.resolveLogger(input.logger)
35
+ };
36
+ }
37
+ //#endregion
38
+ //#region core/authorization.ts
39
+ const hasPermission = (principal, permission) => principal.permissions.includes(permission);
40
+ /**
41
+ * Evaluates a requirement using deny-by-default semantics:
42
+ * 1. If a `permission` is required and the principal lacks it, deny.
43
+ * 2. If a `policy` is provided, it must return `true` to allow.
44
+ * 3. If neither is provided, deny (nothing explicitly granted access).
45
+ */
46
+ async function evaluateAuthorization(requirement, context) {
47
+ const { permission, policy } = requirement;
48
+ if (permission !== void 0 && !hasPermission(context.principal, permission)) return {
49
+ allowed: false,
50
+ reason: "missing-permission"
51
+ };
52
+ if (policy) {
53
+ if (await policy(context) !== true) return {
54
+ allowed: false,
55
+ reason: "policy-denied"
56
+ };
57
+ return {
58
+ allowed: true,
59
+ reason: "policy-allowed"
60
+ };
61
+ }
62
+ if (permission !== void 0) return {
63
+ allowed: true,
64
+ reason: "permission-granted"
65
+ };
66
+ return {
67
+ allowed: false,
68
+ reason: "no-grant"
69
+ };
70
+ }
71
+ /** Convenience boolean form of {@link evaluateAuthorization}. */
72
+ async function isAuthorized(requirement, context) {
73
+ return (await evaluateAuthorization(requirement, context)).allowed;
74
+ }
75
+ //#endregion
76
+ //#region core/session-state.ts
77
+ /**
78
+ * Builds the runtime principal for a user by reading their roles and permissions from
79
+ * the store. Returns the shape attached to authenticated requests.
80
+ */
81
+ async function hydratePrincipal(store, user) {
82
+ const [roles, permissions] = await Promise.all([store.listRolesForUser(user.userId), store.listPermissionsForUser(user.userId)]);
83
+ return {
84
+ userId: user.userId,
85
+ email: user.email,
86
+ displayName: user.displayName,
87
+ status: user.status,
88
+ emailVerified: user.emailVerifiedAt !== null,
89
+ roles: roles.map((role) => role.name),
90
+ permissions: [...permissions]
91
+ };
92
+ }
93
+ /**
94
+ * Issues a fresh token pair for a user and persists a new refresh session
95
+ * (storing only the hash of the refresh token).
96
+ */
97
+ async function issueSession(deps, user) {
98
+ const { config, store, tokenService } = deps;
99
+ const now = config.now();
100
+ const principal = await hydratePrincipal(store, user);
101
+ const access = await tokenService.issueAccessToken(user.userId, user.authVersion);
102
+ const sessionId = config.generateId();
103
+ const refresh = tokenService.createRefreshToken(sessionId);
104
+ const refreshExpiresAt = new Date(now.getTime() + config.refreshTokenTtlSeconds * 1e3);
105
+ await store.createSession({
106
+ sessionId,
107
+ userId: user.userId,
108
+ refreshTokenHash: refresh.hash,
109
+ createdAt: now,
110
+ expiresAt: refreshExpiresAt
111
+ });
112
+ return {
113
+ user: principal,
114
+ tokens: {
115
+ accessToken: access.token,
116
+ refreshToken: refresh.token,
117
+ accessTokenExpiresAt: access.expiresAt,
118
+ refreshTokenExpiresAt: refreshExpiresAt
119
+ }
120
+ };
121
+ }
122
+ //#endregion
123
+ //#region core/oauth/oauth-service.ts
124
+ /** Thrown when an OAuth operation targets an unregistered provider name. */
125
+ var OAuthProviderNotFoundError = class extends Error {
126
+ constructor(providerName) {
127
+ super(`No OAuth provider registered with name "${providerName}".`);
128
+ this.name = "OAuthProviderNotFoundError";
129
+ }
130
+ };
131
+ /** Thrown when the callback state does not match the expected state (CSRF guard). */
132
+ var OAuthStateMismatchError = class extends Error {
133
+ constructor() {
134
+ super("OAuth state mismatch. The callback may have been replayed or tampered with.");
135
+ this.name = "OAuthStateMismatchError";
136
+ }
137
+ };
138
+ /**
139
+ * Creates the OAuth2 / OIDC service. Wire into the identity service by passing configured
140
+ * provider instances (e.g. `createGoogleOAuthProvider(...)`) to the `oauth.providers` list
141
+ * in {@link IdentityConfigInput}.
142
+ */
143
+ function createOAuthService(deps) {
144
+ const { providers, config, store, sessionDeps, audit } = deps;
145
+ const providerMap = /* @__PURE__ */ new Map();
146
+ for (const p of providers) providerMap.set(p.name, p);
147
+ function getProvider(name) {
148
+ const p = providerMap.get(name);
149
+ if (!p) throw new OAuthProviderNotFoundError(name);
150
+ return p;
151
+ }
152
+ return {
153
+ get providers() {
154
+ return [...providerMap.keys()];
155
+ },
156
+ buildAuthorizationUrl(providerName, redirectUri, scopes) {
157
+ const provider = getProvider(providerName);
158
+ const state = config.generateId();
159
+ return {
160
+ url: provider.buildAuthorizationUrl({
161
+ redirectUri,
162
+ state,
163
+ scopes
164
+ }),
165
+ state
166
+ };
167
+ },
168
+ async handleCallback(providerName, params) {
169
+ if (params.state !== params.expectedState) throw new OAuthStateMismatchError();
170
+ const provider = getProvider(providerName);
171
+ const tokens = await provider.exchangeCode({
172
+ code: params.code,
173
+ redirectUri: params.redirectUri
174
+ });
175
+ const userInfo = await provider.fetchUserInfo(tokens);
176
+ const now = config.now();
177
+ if (store.findUserByOAuthId) {
178
+ const linkedUser = await store.findUserByOAuthId(providerName, userInfo.providerUserId);
179
+ if (linkedUser) {
180
+ await audit.record("user.login.succeeded", linkedUser.userId, {
181
+ email: linkedUser.email,
182
+ provider: providerName
183
+ });
184
+ return issueSession(sessionDeps, linkedUser);
185
+ }
186
+ }
187
+ if (userInfo.email) {
188
+ const existingUser = await store.findUserByEmail(userInfo.email);
189
+ if (existingUser) {
190
+ await linkOAuthAccount(store, existingUser.userId, providerName, userInfo, now);
191
+ await audit.record("user.oauth.linked", existingUser.userId, {
192
+ provider: providerName,
193
+ providerUserId: userInfo.providerUserId
194
+ });
195
+ await audit.record("user.login.succeeded", existingUser.userId, {
196
+ email: existingUser.email,
197
+ provider: providerName
198
+ });
199
+ return issueSession(sessionDeps, existingUser);
200
+ }
201
+ }
202
+ const newUser = await store.createUser({
203
+ userId: config.generateId(),
204
+ email: userInfo.email ?? `oauth:${providerName}:${userInfo.providerUserId}`,
205
+ displayName: userInfo.displayName,
206
+ status: "active",
207
+ authVersion: 0,
208
+ createdAt: now,
209
+ updatedAt: now
210
+ });
211
+ await linkOAuthAccount(store, newUser.userId, providerName, userInfo, now);
212
+ await audit.record("user.registered", newUser.userId, {
213
+ provider: providerName,
214
+ providerUserId: userInfo.providerUserId
215
+ });
216
+ await audit.record("user.oauth.linked", newUser.userId, {
217
+ provider: providerName,
218
+ providerUserId: userInfo.providerUserId
219
+ });
220
+ await audit.record("user.login.succeeded", newUser.userId, {
221
+ email: newUser.email,
222
+ provider: providerName
223
+ });
224
+ return issueSession(sessionDeps, newUser);
225
+ }
226
+ };
227
+ }
228
+ async function linkOAuthAccount(store, userId, provider, userInfo, now) {
229
+ if (!store.createOAuthLink) return;
230
+ const link = {
231
+ userId,
232
+ provider,
233
+ providerUserId: userInfo.providerUserId,
234
+ email: userInfo.email,
235
+ displayName: userInfo.displayName,
236
+ linkedAt: now
237
+ };
238
+ await store.createOAuthLink(link);
239
+ }
240
+ //#endregion
241
+ //#region schema/model.ts
242
+ const identitySchemaModel = { tables: [
243
+ {
244
+ name: "identity_users",
245
+ description: "Core user accounts.",
246
+ columns: [
247
+ {
248
+ name: "user_id",
249
+ description: "Primary key.",
250
+ nullable: false
251
+ },
252
+ {
253
+ name: "email",
254
+ description: "Unique, case-insensitive login email.",
255
+ nullable: false
256
+ },
257
+ {
258
+ name: "display_name",
259
+ description: "Optional display name.",
260
+ nullable: true
261
+ },
262
+ {
263
+ name: "status",
264
+ description: "active | disabled | locked.",
265
+ nullable: false
266
+ },
267
+ {
268
+ name: "email_verified_at",
269
+ description: "When email was verified.",
270
+ nullable: true
271
+ },
272
+ {
273
+ name: "auth_version",
274
+ description: "Token invalidation counter.",
275
+ nullable: false
276
+ },
277
+ {
278
+ name: "created_at",
279
+ description: "Creation timestamp.",
280
+ nullable: false
281
+ },
282
+ {
283
+ name: "updated_at",
284
+ description: "Last update timestamp.",
285
+ nullable: false
286
+ }
287
+ ]
288
+ },
289
+ {
290
+ name: "identity_credentials",
291
+ description: "Password hashes, one per user.",
292
+ columns: [
293
+ {
294
+ name: "user_id",
295
+ description: "FK to identity_users.",
296
+ nullable: false
297
+ },
298
+ {
299
+ name: "password_hash",
300
+ description: "Algorithm-tagged hash.",
301
+ nullable: false
302
+ },
303
+ {
304
+ name: "updated_at",
305
+ description: "Last update timestamp.",
306
+ nullable: false
307
+ }
308
+ ]
309
+ },
310
+ {
311
+ name: "identity_sessions",
312
+ description: "Server-side refresh sessions with hashed tokens.",
313
+ columns: [
314
+ {
315
+ name: "session_id",
316
+ description: "Primary key.",
317
+ nullable: false
318
+ },
319
+ {
320
+ name: "user_id",
321
+ description: "FK to identity_users.",
322
+ nullable: false
323
+ },
324
+ {
325
+ name: "refresh_token_hash",
326
+ description: "Hash of the active refresh token.",
327
+ nullable: false
328
+ },
329
+ {
330
+ name: "created_at",
331
+ description: "Creation timestamp.",
332
+ nullable: false
333
+ },
334
+ {
335
+ name: "expires_at",
336
+ description: "Expiry timestamp.",
337
+ nullable: false
338
+ },
339
+ {
340
+ name: "revoked_at",
341
+ description: "Set when rotated or revoked.",
342
+ nullable: true
343
+ }
344
+ ]
345
+ },
346
+ {
347
+ name: "identity_roles",
348
+ description: "Named roles.",
349
+ columns: [
350
+ {
351
+ name: "role_id",
352
+ description: "Primary key.",
353
+ nullable: false
354
+ },
355
+ {
356
+ name: "name",
357
+ description: "Unique role name.",
358
+ nullable: false
359
+ },
360
+ {
361
+ name: "description",
362
+ description: "Optional description.",
363
+ nullable: true
364
+ }
365
+ ]
366
+ },
367
+ {
368
+ name: "identity_user_roles",
369
+ description: "User-to-role assignments.",
370
+ columns: [{
371
+ name: "user_id",
372
+ description: "FK to identity_users.",
373
+ nullable: false
374
+ }, {
375
+ name: "role_id",
376
+ description: "FK to identity_roles.",
377
+ nullable: false
378
+ }]
379
+ },
380
+ {
381
+ name: "identity_role_permissions",
382
+ description: "Permissions granted to roles.",
383
+ columns: [{
384
+ name: "role_id",
385
+ description: "FK to identity_roles.",
386
+ nullable: false
387
+ }, {
388
+ name: "permission",
389
+ description: "Permission string.",
390
+ nullable: false
391
+ }]
392
+ },
393
+ {
394
+ name: "identity_user_permissions",
395
+ description: "Permissions granted directly to users.",
396
+ columns: [{
397
+ name: "user_id",
398
+ description: "FK to identity_users.",
399
+ nullable: false
400
+ }, {
401
+ name: "permission",
402
+ description: "Permission string.",
403
+ nullable: false
404
+ }]
405
+ }
406
+ ] };
407
+ //#endregion
408
+ Object.defineProperty(exports, "OAuthProviderNotFoundError", {
409
+ enumerable: true,
410
+ get: function() {
411
+ return OAuthProviderNotFoundError;
412
+ }
413
+ });
414
+ Object.defineProperty(exports, "OAuthStateMismatchError", {
415
+ enumerable: true,
416
+ get: function() {
417
+ return OAuthStateMismatchError;
418
+ }
419
+ });
420
+ Object.defineProperty(exports, "createOAuthService", {
421
+ enumerable: true,
422
+ get: function() {
423
+ return createOAuthService;
424
+ }
425
+ });
426
+ Object.defineProperty(exports, "evaluateAuthorization", {
427
+ enumerable: true,
428
+ get: function() {
429
+ return evaluateAuthorization;
430
+ }
431
+ });
432
+ Object.defineProperty(exports, "hydratePrincipal", {
433
+ enumerable: true,
434
+ get: function() {
435
+ return hydratePrincipal;
436
+ }
437
+ });
438
+ Object.defineProperty(exports, "identitySchemaModel", {
439
+ enumerable: true,
440
+ get: function() {
441
+ return identitySchemaModel;
442
+ }
443
+ });
444
+ Object.defineProperty(exports, "isAuthorized", {
445
+ enumerable: true,
446
+ get: function() {
447
+ return isAuthorized;
448
+ }
449
+ });
450
+ Object.defineProperty(exports, "issueSession", {
451
+ enumerable: true,
452
+ get: function() {
453
+ return issueSession;
454
+ }
455
+ });
456
+ Object.defineProperty(exports, "resolveIdentityConfig", {
457
+ enumerable: true,
458
+ get: function() {
459
+ return resolveIdentityConfig;
460
+ }
461
+ });