@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 +567 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +539 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
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;
|