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