@auth-craft/tenant-access-control-dynamodb 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4,9 +4,547 @@ var clientDynamodb = require('@aws-sdk/client-dynamodb');
4
4
  var libDynamodb = require('@aws-sdk/lib-dynamodb');
5
5
  var tsMicroResult = require('ts-micro-result');
6
6
  var tenantAccessControl = require('@auth-craft/tenant-access-control');
7
- var databasePluginDynamodb = require('@auth-craft/database-plugin-dynamodb');
8
7
 
9
8
  // src/index.ts
9
+
10
+ // ../auth-core/dist/chunk-66N4CPWZ.mjs
11
+ var USER_STATUS = {
12
+ ACTIVE: "active"};
13
+
14
+ // ../database-plugin-dynamodb/dist/index.mjs
15
+ var EntityPrefix = {
16
+ USER: "USR"
17
+ };
18
+ var PkPrefix = {
19
+ USER: `${EntityPrefix.USER}#`,
20
+ SESSION: "SES#",
21
+ PROVIDER: "PRV#",
22
+ CHALLENGE: "CHL#",
23
+ IDENTITY: "IDT#",
24
+ // Identity (email/phone/username)
25
+ CREDENTIAL: "CRD#",
26
+ // Credential (password, passkey, etc.)
27
+ // Note: MFA credentials use PK: USR#{userId} (stored under user, not separate partition)
28
+ TENANT: "TNT#",
29
+ // TenantMember (SCHEMA_V4 multi-tenant)
30
+ AUDIENCE: "AUD#"
31
+ // Audience prefix for TenantMember SK
32
+ };
33
+ var SkPrefix = {
34
+ METADATA: "MD",
35
+ // Metadata suffix for entities
36
+ AUTH: "AUTH",
37
+ // UserAuth entity (SCHEMA_V4)
38
+ PROFILE: "PROF",
39
+ // UserProfile entity (SCHEMA_V4)
40
+ PROVIDER: "PRV#",
41
+ MFA: "MFA#"
42
+ // UserMFAAuthenticator entity (SCHEMA_V4)
43
+ };
44
+ var DELIMITER = "#";
45
+ var KeyPattern = {
46
+ // ============================================
47
+ // USER
48
+ // ============================================
49
+ /**
50
+ * User PK
51
+ * Pattern: USR#{userId}
52
+ */
53
+ USER_PK: (userId) => `${PkPrefix.USER}${userId}`,
54
+ /**
55
+ * User Auth SK (SCHEMA_V4)
56
+ * Pattern: AUTH
57
+ * Stores: userStatus, mfaRequired, mfaMethods[], createdAt, updatedAt
58
+ *
59
+ * Removed in SCHEMA_V4:
60
+ * - email, phone fields → moved to Identity entity
61
+ * - passwordHash → moved to Credential entity
62
+ * - lockedUntil, failedLoginAttempts → moved to Identity.guards (per-method)
63
+ * - MFA credentials (mfaSecret, mfaBackupCodes) → moved to UserMFAAuthenticator items
64
+ * - Profile fields (givenName, familyName, avatarUrl, locale, metadata) → moved to PROF item
65
+ * - tokenVersion → removed (use Session.revokedAt instead)
66
+ * - permissions → removed from UserAuth (use TenantMember.permMask or Session.permMask)
67
+ */
68
+ USER_AUTH_SK: () => SkPrefix.AUTH,
69
+ /**
70
+ * User Profile SK (SCHEMA_V4)
71
+ * Pattern: PROF
72
+ * Stores: userStatus, deletedAt, givenName, familyName, avatarUrl,
73
+ * locale, metadata, createdAt, updatedAt, statusPk, statusSk
74
+ *
75
+ * Removed in SCHEMA_V4:
76
+ * - email, phone fields → moved to Identity entity
77
+ */
78
+ USER_PROFILE_SK: () => SkPrefix.PROFILE,
79
+ // ============================================
80
+ // USER MFA AUTHENTICATOR (SCHEMA_V4)
81
+ // ============================================
82
+ /**
83
+ * UserMFAAuthenticator SK (single-instance methods like TOTP)
84
+ * Pattern: MFA#{method}
85
+ * Example: MFA#totp
86
+ * Used by: Single-instance MFA methods that don't need ID
87
+ */
88
+ USER_MFA_SK: (method) => `${SkPrefix.MFA}${method}`,
89
+ /**
90
+ * UserMFAAuthenticator SK (multi-instance methods like WebAuthn)
91
+ * Pattern: MFA#{method}#{id}
92
+ * Example: MFA#webauthn#key_abc123, MFA#passkey#pk_xyz789
93
+ * Used by: Multi-instance MFA methods (security keys, passkeys)
94
+ */
95
+ USER_MFA_WITH_ID_SK: (method, id) => `${SkPrefix.MFA}${method}${DELIMITER}${id}`,
96
+ // ============================================
97
+ // TENANT MEMBER (SCHEMA_V4)
98
+ // ============================================
99
+ /**
100
+ * Tenant Member PK
101
+ * Pattern: TNT#{tenantId}
102
+ * Example: TNT#org-abc
103
+ * Used by: DynamoDBTenantMemberItem (main table)
104
+ */
105
+ TENANT_MEMBER_PK: (tenantId) => `${PkPrefix.TENANT}${tenantId}`,
106
+ /**
107
+ * Tenant Member SK
108
+ * Pattern: AUD#{audience}#USR#{userId}
109
+ * Example: AUD#admin#USR#01HQZX8V9ABCDEFGHIJK
110
+ * Used by: DynamoDBTenantMemberItem (main table)
111
+ */
112
+ TENANT_MEMBER_SK: (audience, userId) => `${PkPrefix.AUDIENCE}${audience}${DELIMITER}${PkPrefix.USER}${userId}`,
113
+ /**
114
+ * Tenant Member SK Prefix (for querying by audience)
115
+ * Pattern: AUD#{audience}#
116
+ * Example: AUD#admin#
117
+ * Used for: Query all members in a specific audience
118
+ */
119
+ TENANT_MEMBER_SK_PREFIX: (audience) => `${PkPrefix.AUDIENCE}${audience}${DELIMITER}`,
120
+ // ============================================
121
+ // SESSION
122
+ // ============================================
123
+ /**
124
+ * Session PK (New session-centric design)
125
+ * Pattern: SES#{sessionId}
126
+ * Example: SES#01HQZX8V9ABCDEFGHIJK
127
+ */
128
+ SESSION_PK: (sessionId) => `${PkPrefix.SESSION}${sessionId}`,
129
+ /**
130
+ * Session Metadata SK
131
+ * Pattern: MD
132
+ * Stores: userId, tokenId, tenantId, audience, permMask, roleIds,
133
+ * deviceLabel, country, city, createdAt, expiresAt, lastUsedAt,
134
+ * revokedAt, metadata, TTL
135
+ * Note: sessionId extracted from PK
136
+ */
137
+ SESSION_METADATA_SK: () => SkPrefix.METADATA,
138
+ /**
139
+ * Session SK (alias for SESSION_METADATA_SK)
140
+ */
141
+ SESSION_SK: () => SkPrefix.METADATA,
142
+ /**
143
+ * Provider SK
144
+ * Pattern: PRV#{provider}#{externalId}
145
+ * Used for:
146
+ * - GSI_UserProviders sort key (userProviderSk)
147
+ * - DynamoDBProviderRegistryItem PK
148
+ */
149
+ PROVIDER_SK: (provider, externalId) => `${SkPrefix.PROVIDER}${provider}${DELIMITER}${externalId}`,
150
+ // ============================================
151
+ // PROVIDER (Provider-centric design)
152
+ // ============================================
153
+ /**
154
+ * Provider PK (Main table)
155
+ * Pattern: PRV#{provider}#{externalId}
156
+ * Example: PRV#google#123456789
157
+ * Used by: DynamoDBUserProviderItem (main table)
158
+ */
159
+ PROVIDER_PK: (provider, externalId) => `${PkPrefix.PROVIDER}${provider}${DELIMITER}${externalId}`,
160
+ /**
161
+ * Provider Metadata SK
162
+ * Pattern: MD
163
+ * Stores: userId, system, externalUsername, externalEmail, externalPhone,
164
+ * externalName, externalAvatarUrl, accessToken, refreshToken,
165
+ * tokenExpiresAt, createdAt, metadata
166
+ * Note: id generated at mapper layer from PK (base64url), NOT stored
167
+ * Note: provider and externalId extracted from PK
168
+ */
169
+ PROVIDER_METADATA_SK: () => SkPrefix.METADATA,
170
+ // ============================================
171
+ // CHALLENGE
172
+ // ============================================
173
+ /**
174
+ * Challenge PK
175
+ * Pattern: CHL#{challengeId}
176
+ * Example: CHL#abc123xyz
177
+ */
178
+ CHALLENGE_PK: (challengeId) => `${PkPrefix.CHALLENGE}${challengeId}`,
179
+ /**
180
+ * Challenge Metadata SK
181
+ * Pattern: MD
182
+ * Stores: userId, tenantId, audience, purpose, status, phase, allowedMethods,
183
+ * method, codeHash, attempts, maxAttempts, metadata, flowKey,
184
+ * expiresAt, createdAt, TTL
185
+ * Note: challengeId extracted from PK
186
+ */
187
+ CHALLENGE_SK: () => SkPrefix.METADATA,
188
+ // ============================================
189
+ // IDENTITY
190
+ // ============================================
191
+ /**
192
+ * Identity PK
193
+ * Pattern: IDT#{type}#{value}
194
+ * Examples:
195
+ * - IDT#email#user@example.com
196
+ * - IDT#phone#+84987654321
197
+ * - IDT#username#johndoe
198
+ *
199
+ * Purpose: Fast O(1) lookup by identity type and value
200
+ */
201
+ IDENTITY_PK: (type, value) => {
202
+ return `${PkPrefix.IDENTITY}${type}#${value}`;
203
+ },
204
+ /**
205
+ * Identity Metadata SK
206
+ * Pattern: MD
207
+ * Stores: userId, loginEnabled, verifiedAt, guards, createdAt, updatedAt
208
+ * Note: type and value extracted from PK
209
+ */
210
+ IDENTITY_SK: () => SkPrefix.METADATA,
211
+ // ============================================
212
+ // CREDENTIAL
213
+ // ============================================
214
+ /**
215
+ * Credential PK
216
+ * Pattern: CRD#{credentialId}
217
+ * Example: CRD#cred_01JCQR8XMZP2KGH7NWV8F9E3TS
218
+ */
219
+ CREDENTIAL_PK: (credentialId) => `${PkPrefix.CREDENTIAL}${credentialId}`,
220
+ /**
221
+ * Credential Metadata SK
222
+ * Pattern: MD
223
+ * Stores: userId, value, metadata, isActive, createdAt, updatedAt, lastUsedAt
224
+ * Note: credentialId and type extracted from PK and GSI SK
225
+ */
226
+ CREDENTIAL_SK: () => SkPrefix.METADATA,
227
+ /**
228
+ * Credential Type Prefix (for GSI query)
229
+ * Pattern: CRD#{type}#
230
+ * Used to query credentials by type with begins_with
231
+ */
232
+ CREDENTIAL_TYPE_PREFIX: (type) => `${PkPrefix.CREDENTIAL}${type}#`,
233
+ /**
234
+ * Credential User GSI SK
235
+ * Pattern: CRD#{type}#{credentialId}
236
+ * Example: CRD#password#cred_01JCQR8XMZP2KGH7NWV8F9E3TS
237
+ * Allows querying credentials by user and filtering by type
238
+ */
239
+ CREDENTIAL_USER_SK: (type, credentialId) => `${PkPrefix.CREDENTIAL}${type}#${credentialId}`,
240
+ // ============================================
241
+ // IDENTITY
242
+ // ============================================
243
+ /**
244
+ * User Index GSI PK (for Identity)
245
+ * Pattern: USR#{userId}
246
+ * Example: USR#01JCQR8XMZP2KGH7NWV8F9E3TS
247
+ *
248
+ * Purpose: Query all identities for a specific user
249
+ */
250
+ USER_INDEX_PK: (userId) => `${PkPrefix.USER}${userId}`,
251
+ /**
252
+ * User Index GSI SK (for Identity)
253
+ * Pattern: IDT#{type}#{value}
254
+ * Examples:
255
+ * - IDT#email#user@example.com
256
+ * - IDT#phone#+84987654321
257
+ *
258
+ * Purpose: Sort identities by type and extract type/value without additional attributes
259
+ */
260
+ USER_INDEX_SK: (type, value) => {
261
+ return `${PkPrefix.IDENTITY}${type}#${value}`;
262
+ }
263
+ };
264
+ var KeyExtractor = {
265
+ // ============================================
266
+ // PK Extractors
267
+ // ============================================
268
+ /**
269
+ * Extract userId from User PK
270
+ * Pattern: USR#{userId} -> userId
271
+ */
272
+ userId: (pk) => pk.slice(PkPrefix.USER.length),
273
+ /**
274
+ * Extract sessionId from Session PK
275
+ * Pattern: SES#{sessionId} -> sessionId
276
+ */
277
+ sessionId: (pk) => pk.slice(PkPrefix.SESSION.length),
278
+ /**
279
+ * Extract provider and externalId from Provider PK
280
+ * Pattern: PRV#{provider}#{externalId} -> { provider, externalId }
281
+ * Example: PRV#google#123456789 -> { provider: 'google', externalId: '123456789' }
282
+ */
283
+ providerPKInfo: (pk) => {
284
+ const remainder = pk.slice(PkPrefix.PROVIDER.length);
285
+ const delimiterIndex = remainder.indexOf("#");
286
+ if (delimiterIndex === -1) return null;
287
+ const provider = remainder.slice(0, delimiterIndex);
288
+ const externalId = remainder.slice(delimiterIndex + 1);
289
+ return { provider, externalId };
290
+ },
291
+ // ============================================
292
+ // SK Extractors
293
+ // ============================================
294
+ /**
295
+ * Extract provider and externalId from Provider SK
296
+ * Pattern: PRV#{provider}#{externalId} -> { provider, externalId }
297
+ */
298
+ providerInfo: (sk) => {
299
+ const remainder = sk.slice(SkPrefix.PROVIDER.length);
300
+ const delimiterIndex = remainder.indexOf(DELIMITER);
301
+ if (delimiterIndex === -1) return null;
302
+ const provider = remainder.slice(0, delimiterIndex);
303
+ const externalId = remainder.slice(delimiterIndex + 1);
304
+ return { provider, externalId };
305
+ },
306
+ /**
307
+ * Extract method and optional id from MFA SK
308
+ * Pattern: MFA#{method} or MFA#{method}#{id} -> { method, id? }
309
+ * Example: MFA#totp -> { method: 'totp' }
310
+ * Example: MFA#webauthn#key_abc -> { method: 'webauthn', id: 'key_abc' }
311
+ */
312
+ mfaSKInfo: (sk) => {
313
+ const remainder = sk.slice(SkPrefix.MFA.length);
314
+ const delimiterIndex = remainder.indexOf(DELIMITER);
315
+ if (delimiterIndex === -1) {
316
+ return { method: remainder };
317
+ }
318
+ const method = remainder.slice(0, delimiterIndex);
319
+ const id = remainder.slice(delimiterIndex + 1);
320
+ return { method, id };
321
+ },
322
+ // ============================================
323
+ // Tenant Extractors (SCHEMA_V4)
324
+ // ============================================
325
+ /**
326
+ * Extract tenantId from TenantMember PK
327
+ * Pattern: TNT#{tenantId} -> tenantId
328
+ * Example: TNT#org-abc -> org-abc
329
+ */
330
+ tenantId: (pk) => pk.slice(PkPrefix.TENANT.length),
331
+ /**
332
+ * Extract audience and userId from TenantMember SK
333
+ * Pattern: AUD#{audience}#USR#{userId} -> { audience, userId }
334
+ * Example: AUD#admin#USR#01HQZX8V9ABCDEFGHIJK -> { audience: 'admin', userId: '01HQZX8V9ABCDEFGHIJK' }
335
+ */
336
+ tenantMemberSKInfo: (sk) => {
337
+ const audPrefix = PkPrefix.AUDIENCE;
338
+ const usrPrefix = PkPrefix.USER;
339
+ if (!sk.startsWith(audPrefix)) return null;
340
+ const remainder = sk.slice(audPrefix.length);
341
+ const usrIndex = remainder.indexOf(DELIMITER + usrPrefix);
342
+ if (usrIndex === -1) return null;
343
+ const audience = remainder.slice(0, usrIndex);
344
+ const userId = remainder.slice(usrIndex + 1 + usrPrefix.length);
345
+ return { audience, userId };
346
+ },
347
+ /**
348
+ * Extract challengeId from Challenge PK
349
+ * Pattern: CHL#{challengeId} -> challengeId
350
+ * Example: CHL#abc123xyz -> abc123xyz
351
+ */
352
+ challengeIdFromPK: (pk) => pk.slice(PkPrefix.CHALLENGE.length),
353
+ /**
354
+ * Extract type and value from Identity PK
355
+ * Pattern: IDT#{type}#{value} -> { type, value }
356
+ * Example: IDT#email#user@example.com -> { type: 'email', value: 'user@example.com' }
357
+ */
358
+ identityPKInfo: (pk) => {
359
+ const remainder = pk.slice(PkPrefix.IDENTITY.length);
360
+ const delimiterIndex = remainder.indexOf("#");
361
+ if (delimiterIndex === -1) return null;
362
+ const type = remainder.slice(0, delimiterIndex);
363
+ const value = remainder.slice(delimiterIndex + 1);
364
+ return { type, value };
365
+ },
366
+ /**
367
+ * Extract credentialId from Credential PK
368
+ * Pattern: CRD#{credentialId} -> credentialId
369
+ * Example: CRD#cred_01JCQR8XMZP2KGH7NWV8F9E3TS -> cred_01JCQR8XMZP2KGH7NWV8F9E3TS
370
+ */
371
+ credentialId: (pk) => pk.slice(PkPrefix.CREDENTIAL.length),
372
+ /**
373
+ * Extract type from Credential User GSI SK
374
+ * Pattern: CRD#{type}#{credentialId} -> type
375
+ * Example: CRD#password#cred_abc -> password
376
+ */
377
+ credentialTypeFromUserSK: (sk) => {
378
+ const remainder = sk.slice(PkPrefix.CREDENTIAL.length);
379
+ const delimiterIndex = remainder.indexOf("#");
380
+ if (delimiterIndex === -1) return "";
381
+ return remainder.slice(0, delimiterIndex);
382
+ },
383
+ /**
384
+ * Extract type and value from User Index SK (GSI)
385
+ * Pattern: IDT#{type}#{value} -> { type, value }
386
+ * Example: IDT#email#user@example.com -> { type: 'email', value: 'user@example.com' }
387
+ */
388
+ userIndexSKInfo: (sk) => {
389
+ const remainder = sk.slice(PkPrefix.IDENTITY.length);
390
+ const delimiterIndex = remainder.indexOf("#");
391
+ if (delimiterIndex === -1) return null;
392
+ const type = remainder.slice(0, delimiterIndex);
393
+ const value = remainder.slice(delimiterIndex + 1);
394
+ return { type, value };
395
+ }
396
+ };
397
+ var GSIKeys = {
398
+ // ============================================
399
+ // GSI_ActiveSessions
400
+ // ============================================
401
+ /**
402
+ * Active Sessions GSI PK
403
+ * Pattern: USR#{userId} (user-partitioned for scalability)
404
+ * Used by: GSI_ActiveSessions
405
+ * NOTE: Changed from global 'ACTIVE' to user-partitioned to prevent hot partition
406
+ */
407
+ ACTIVE_SESSION_PK: (userId) => `${PkPrefix.USER}${userId}`,
408
+ /**
409
+ * Active Sessions GSI SK
410
+ * Pattern: {createdAt}#{sessionId}
411
+ * Used by: GSI_ActiveSessions
412
+ */
413
+ ACTIVE_SESSION_SK: (createdAt, sessionId) => `${createdAt}${DELIMITER}${sessionId}`,
414
+ // ============================================
415
+ // GSI_UserSessions
416
+ // ============================================
417
+ /**
418
+ * User Sessions GSI PK
419
+ * Pattern: USR#{userId}
420
+ * Used by: GSI_UserSessions
421
+ */
422
+ USER_SESSION_PK: (userId) => `${PkPrefix.USER}${userId}`,
423
+ /**
424
+ * User Sessions GSI SK
425
+ * Pattern: {createdAt}#{sessionId}
426
+ * Used by: GSI_UserSessions
427
+ */
428
+ USER_SESSION_SK: (createdAt, sessionId) => `${createdAt}${DELIMITER}${sessionId}`,
429
+ // ============================================
430
+ // GSI_UsersByStatus
431
+ // ============================================
432
+ /**
433
+ * Users By Status GSI PK
434
+ * Pattern: USR#ACTIVE or USR#INACTIVE (SCHEMA_V4)
435
+ * Used by: GSI_UsersByStatus
436
+ * Maps: active → ACTIVE, suspended/deleted → INACTIVE
437
+ */
438
+ USER_STATUS_PK: (status) => {
439
+ const statusGroup = status === USER_STATUS.ACTIVE ? "ACTIVE" : "INACTIVE";
440
+ return `${EntityPrefix.USER}${DELIMITER}${statusGroup}`;
441
+ },
442
+ /**
443
+ * Users By Status GSI SK
444
+ * Pattern: {userId}
445
+ * Used by: GSI_UsersByStatus
446
+ * Simple userId pattern to avoid needing createdAt for syncAuthStatus
447
+ */
448
+ USER_STATUS_SK: (userId) => userId,
449
+ // ============================================
450
+ // GSI_UserProviders
451
+ // ============================================
452
+ /**
453
+ * User Providers GSI PK
454
+ * Pattern: USR#{userId}
455
+ * Used by: GSI_UserProviders
456
+ */
457
+ USER_PROVIDER_PK: (userId) => `${PkPrefix.USER}${userId}`,
458
+ /**
459
+ * User Providers GSI SK
460
+ * Pattern: PRV#{provider}#{externalId}
461
+ * Used by: GSI_UserProviders
462
+ */
463
+ USER_PROVIDER_SK: (provider, externalId) => `${SkPrefix.PROVIDER}${provider}${DELIMITER}${externalId}`
464
+ };
465
+ var TableAttr = {
466
+ PK: "PK",
467
+ SK: "SK",
468
+ TTL: "TTL",
469
+ ACTIVE_SESSION_PK: "activeSessionPk",
470
+ // GSI partition key for active sessions
471
+ ACTIVE_SESSION_SK: "activeSessionSk",
472
+ // GSI sort key for active sessions (createdAt#sessionId)
473
+ USER_SESSION_PK: "userSessionPk",
474
+ // GSI partition key for user sessions (USR#{userId})
475
+ USER_SESSION_SK: "userSessionSk",
476
+ // GSI sort key for user sessions (createdAt#sessionId)
477
+ USER_STATUS_PK: "userStatusPk",
478
+ // GSI partition key for users by status
479
+ USER_STATUS_SK: "userStatusSk",
480
+ // GSI sort key for users by status (userId)
481
+ USER_PROVIDER_PK: "userProviderPk",
482
+ // GSI partition key for user providers (USR#{userId})
483
+ USER_PROVIDER_SK: "userProviderSk",
484
+ // GSI sort key for user providers (PRV#{provider}#{externalId})
485
+ // Challenge attributes
486
+ USER_ID: "userId",
487
+ TENANT_ID: "tenantId",
488
+ AUDIENCE: "audience",
489
+ PURPOSE: "purpose",
490
+ CHALLENGE_STATUS: "challengeStatus",
491
+ // Named challengeStatus to avoid DynamoDB reserved keyword 'status'
492
+ PHASE: "phase",
493
+ ALLOWED_METHODS: "allowedMethods",
494
+ METHOD: "method",
495
+ IDENTITY_TYPE: "identityType",
496
+ // For credential challenges
497
+ IDENTITY_VALUE: "identityValue",
498
+ // For credential challenges
499
+ CODE_HASH: "codeHash",
500
+ ATTEMPTS: "attempts",
501
+ MAX_ATTEMPTS: "maxAttempts",
502
+ METADATA: "metadata",
503
+ FLOW_KEY: "flowKey",
504
+ EXPIRES_AT: "expiresAt",
505
+ CREATED_AT: "createdAt",
506
+ // Session attributes
507
+ TOKEN_ID: "tokenId",
508
+ LAST_USED_AT: "lastUsedAt",
509
+ REVOKED_AT: "revokedAt",
510
+ PERM_MASK: "permMask",
511
+ ROLE_IDS: "roleIds",
512
+ DEVICE_LABEL: "deviceLabel",
513
+ COUNTRY: "country",
514
+ CITY: "city",
515
+ // Identity attributes
516
+ IDENTITY_USER_PK: "identityUserPk",
517
+ // GSI partition key for user identities (USR#{userId})
518
+ IDENTITY_USER_SK: "identityUserSk",
519
+ // GSI sort key for user identities (IDT#{type}#{value})
520
+ LOGIN_ENABLED: "loginEnabled",
521
+ VERIFIED_AT: "verifiedAt",
522
+ GUARDS: "guards",
523
+ UPDATED_AT: "updatedAt",
524
+ // Credential attributes
525
+ CREDENTIAL_USER_PK: "credentialUserPk",
526
+ // GSI partition key for user credentials (USR#{userId})
527
+ CREDENTIAL_USER_SK: "credentialUserSk",
528
+ // GSI sort key for user credentials (CRD#{type}#{credentialId})
529
+ CREDENTIAL_VALUE: "value"
530
+ // Credential value (password hash, etc.)
531
+ // Note: REVOKED_AT is shared with Session (defined above)
532
+ };
533
+ var GSIName = {
534
+ ACTIVE_SESSIONS: "GSI_ActiveSessions",
535
+ USER_SESSIONS: "GSI_UserSessions",
536
+ USERS_BY_STATUS: "GSI_UsersByStatus",
537
+ USER_PROVIDERS: "GSI_UserProviders",
538
+ USER_IDENTITIES: "GSI_UserIdentities",
539
+ // Query all identities for a user
540
+ USER_CREDENTIALS: "GSI_UserCredentials"
541
+ // Query all credentials for a user
542
+ };
543
+ function millisToDate(millis) {
544
+ return new Date(millis);
545
+ }
546
+
547
+ // src/dynamodb-tenant-member-repo.ts
10
548
  var DynamoDBTenantMemberRepository = class {
11
549
  constructor(client, tableName) {
12
550
  this.client = client;
@@ -22,8 +560,8 @@ var DynamoDBTenantMemberRepository = class {
22
560
  new libDynamodb.GetCommand({
23
561
  TableName: this.tableName,
24
562
  Key: {
25
- [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(tenantId),
26
- [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(audience, userId)
563
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
564
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId)
27
565
  }
28
566
  })
29
567
  );
@@ -42,8 +580,8 @@ var DynamoDBTenantMemberRepository = class {
42
580
  new libDynamodb.PutCommand({
43
581
  TableName: this.tableName,
44
582
  Item: {
45
- [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(data.tenantId),
46
- [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(data.audience, data.userId),
583
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(data.tenantId),
584
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(data.audience, data.userId),
47
585
  userStatus: data.status,
48
586
  roleIds: JSON.stringify(data.roleIds),
49
587
  permMask: data.permMask ?? 0,
@@ -80,8 +618,8 @@ var DynamoDBTenantMemberRepository = class {
80
618
  new libDynamodb.UpdateCommand({
81
619
  TableName: this.tableName,
82
620
  Key: {
83
- [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(tenantId),
84
- [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(audience, userId)
621
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
622
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId)
85
623
  },
86
624
  UpdateExpression: `SET ${setClauses.join(", ")}`,
87
625
  ExpressionAttributeNames: names,
@@ -103,8 +641,8 @@ var DynamoDBTenantMemberRepository = class {
103
641
  new libDynamodb.UpdateCommand({
104
642
  TableName: this.tableName,
105
643
  Key: {
106
- [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(tenantId),
107
- [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(audience, userId)
644
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
645
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId)
108
646
  },
109
647
  UpdateExpression: "SET #status = :status, #updatedAt = :updatedAt",
110
648
  ExpressionAttributeNames: {
@@ -132,8 +670,8 @@ var DynamoDBTenantMemberRepository = class {
132
670
  new libDynamodb.DeleteCommand({
133
671
  TableName: this.tableName,
134
672
  Key: {
135
- [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(tenantId),
136
- [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(audience, userId)
673
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
674
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId)
137
675
  }
138
676
  })
139
677
  );
@@ -143,8 +681,8 @@ var DynamoDBTenantMemberRepository = class {
143
681
  }
144
682
  }
145
683
  mapItemToEntity(item) {
146
- const tenantId = databasePluginDynamodb.KeyExtractor.tenantId(item[databasePluginDynamodb.TableAttr.PK]);
147
- const skInfo = databasePluginDynamodb.KeyExtractor.tenantMemberSKInfo(item[databasePluginDynamodb.TableAttr.SK]);
684
+ const tenantId = KeyExtractor.tenantId(item[TableAttr.PK]);
685
+ const skInfo = KeyExtractor.tenantMemberSKInfo(item[TableAttr.SK]);
148
686
  return {
149
687
  tenantId,
150
688
  audience: skInfo?.audience ?? "",
@@ -152,8 +690,8 @@ var DynamoDBTenantMemberRepository = class {
152
690
  status: item["userStatus"],
153
691
  roleIds: JSON.parse(item["roleIds"]),
154
692
  permMask: item["permMask"] ?? 0,
155
- createdAt: databasePluginDynamodb.millisToDate(item["createdAt"]),
156
- updatedAt: databasePluginDynamodb.millisToDate(item["updatedAt"])
693
+ createdAt: millisToDate(item["createdAt"]),
694
+ updatedAt: millisToDate(item["updatedAt"])
157
695
  };
158
696
  }
159
697
  };
@@ -180,22 +718,22 @@ var DynamoDBSessionRepository = class {
180
718
  const response = await this.docClient.send(
181
719
  new libDynamodb.QueryCommand({
182
720
  TableName: this.tableName,
183
- IndexName: databasePluginDynamodb.GSIName.ACTIVE_SESSIONS,
184
- KeyConditionExpression: `${databasePluginDynamodb.TableAttr.ACTIVE_SESSION_PK} = :activePk`,
721
+ IndexName: GSIName.ACTIVE_SESSIONS,
722
+ KeyConditionExpression: `${TableAttr.ACTIVE_SESSION_PK} = :activePk`,
185
723
  ExpressionAttributeValues: {
186
- ":activePk": databasePluginDynamodb.GSIKeys.ACTIVE_SESSION_PK(userId)
724
+ ":activePk": GSIKeys.ACTIVE_SESSION_PK(userId)
187
725
  },
188
- ProjectionExpression: `${databasePluginDynamodb.TableAttr.PK}, ${databasePluginDynamodb.TableAttr.TENANT_ID}, ${databasePluginDynamodb.TableAttr.AUDIENCE}, ${databasePluginDynamodb.TableAttr.USER_ID}`,
726
+ ProjectionExpression: `${TableAttr.PK}, ${TableAttr.TENANT_ID}, ${TableAttr.AUDIENCE}, ${TableAttr.USER_ID}`,
189
727
  Limit: limit,
190
728
  ExclusiveStartKey: exclusiveStartKey,
191
729
  ScanIndexForward: false
192
730
  })
193
731
  );
194
732
  const sessions = (response.Items ?? []).map((item) => ({
195
- id: databasePluginDynamodb.KeyExtractor.sessionId(item[databasePluginDynamodb.TableAttr.PK]),
196
- userId: item[databasePluginDynamodb.TableAttr.USER_ID],
197
- tenantId: item[databasePluginDynamodb.TableAttr.TENANT_ID],
198
- audience: item[databasePluginDynamodb.TableAttr.AUDIENCE]
733
+ id: KeyExtractor.sessionId(item[TableAttr.PK]),
734
+ userId: item[TableAttr.USER_ID],
735
+ tenantId: item[TableAttr.TENANT_ID],
736
+ audience: item[TableAttr.AUDIENCE]
199
737
  }));
200
738
  const hasNext = !!response.LastEvaluatedKey;
201
739
  const cursor = hasNext ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString("base64url") : null;
@@ -226,19 +764,19 @@ var DynamoDBSessionRepository = class {
226
764
  Update: {
227
765
  TableName: this.tableName,
228
766
  Key: {
229
- [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.SESSION_PK(sessionId),
230
- [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.SESSION_SK()
767
+ [TableAttr.PK]: KeyPattern.SESSION_PK(sessionId),
768
+ [TableAttr.SK]: KeyPattern.SESSION_SK()
231
769
  },
232
770
  UpdateExpression: `SET #revokedAt = :revokedAt REMOVE #activePk, #activeSk`,
233
771
  ExpressionAttributeNames: {
234
- "#revokedAt": databasePluginDynamodb.TableAttr.REVOKED_AT,
235
- "#activePk": databasePluginDynamodb.TableAttr.ACTIVE_SESSION_PK,
236
- "#activeSk": databasePluginDynamodb.TableAttr.ACTIVE_SESSION_SK
772
+ "#revokedAt": TableAttr.REVOKED_AT,
773
+ "#activePk": TableAttr.ACTIVE_SESSION_PK,
774
+ "#activeSk": TableAttr.ACTIVE_SESSION_SK
237
775
  },
238
776
  ExpressionAttributeValues: {
239
777
  ":revokedAt": revokedAt
240
778
  },
241
- ConditionExpression: `attribute_exists(${databasePluginDynamodb.TableAttr.PK}) AND attribute_not_exists(#revokedAt)`
779
+ ConditionExpression: `attribute_exists(${TableAttr.PK}) AND attribute_not_exists(#revokedAt)`
242
780
  }
243
781
  }));
244
782
  try {