@auth-craft/tenant-access-control-dynamodb 0.0.1 → 0.0.3
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 +553 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +531 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/dist/index.cjs
CHANGED
|
@@ -4,9 +4,528 @@ 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
|
+
};
|
|
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: USR#{userId}
|
|
107
|
+
* Example: USR#01HQZX8V9ABCDEFGHIJK
|
|
108
|
+
* Used by: DynamoDBTenantMemberItem (main table)
|
|
109
|
+
*/
|
|
110
|
+
TENANT_MEMBER_SK: (userId) => `${PkPrefix.USER}${userId}`,
|
|
111
|
+
// ============================================
|
|
112
|
+
// SESSION
|
|
113
|
+
// ============================================
|
|
114
|
+
/**
|
|
115
|
+
* Session PK (New session-centric design)
|
|
116
|
+
* Pattern: SES#{sessionId}
|
|
117
|
+
* Example: SES#01HQZX8V9ABCDEFGHIJK
|
|
118
|
+
*/
|
|
119
|
+
SESSION_PK: (sessionId) => `${PkPrefix.SESSION}${sessionId}`,
|
|
120
|
+
/**
|
|
121
|
+
* Session Metadata SK
|
|
122
|
+
* Pattern: MD
|
|
123
|
+
* Stores: userId, tokenId, tenantId, audience, permMask, roleIds,
|
|
124
|
+
* deviceLabel, country, city, createdAt, expiresAt, lastUsedAt,
|
|
125
|
+
* revokedAt, metadata, TTL
|
|
126
|
+
* Note: sessionId extracted from PK
|
|
127
|
+
*/
|
|
128
|
+
SESSION_METADATA_SK: () => SkPrefix.METADATA,
|
|
129
|
+
/**
|
|
130
|
+
* Session SK (alias for SESSION_METADATA_SK)
|
|
131
|
+
*/
|
|
132
|
+
SESSION_SK: () => SkPrefix.METADATA,
|
|
133
|
+
/**
|
|
134
|
+
* Provider SK
|
|
135
|
+
* Pattern: PRV#{provider}#{externalId}
|
|
136
|
+
* Used for:
|
|
137
|
+
* - GSI_UserProviders sort key (userProviderSk)
|
|
138
|
+
* - DynamoDBProviderRegistryItem PK
|
|
139
|
+
*/
|
|
140
|
+
PROVIDER_SK: (provider, externalId) => `${SkPrefix.PROVIDER}${provider}${DELIMITER}${externalId}`,
|
|
141
|
+
// ============================================
|
|
142
|
+
// PROVIDER (Provider-centric design)
|
|
143
|
+
// ============================================
|
|
144
|
+
/**
|
|
145
|
+
* Provider PK (Main table)
|
|
146
|
+
* Pattern: PRV#{provider}#{externalId}
|
|
147
|
+
* Example: PRV#google#123456789
|
|
148
|
+
* Used by: DynamoDBUserProviderItem (main table)
|
|
149
|
+
*/
|
|
150
|
+
PROVIDER_PK: (provider, externalId) => `${PkPrefix.PROVIDER}${provider}${DELIMITER}${externalId}`,
|
|
151
|
+
/**
|
|
152
|
+
* Provider Metadata SK
|
|
153
|
+
* Pattern: MD
|
|
154
|
+
* Stores: userId, system, externalUsername, externalEmail, externalPhone,
|
|
155
|
+
* externalName, externalAvatarUrl, accessToken, refreshToken,
|
|
156
|
+
* tokenExpiresAt, createdAt, metadata
|
|
157
|
+
* Note: id generated at mapper layer from PK (base64url), NOT stored
|
|
158
|
+
* Note: provider and externalId extracted from PK
|
|
159
|
+
*/
|
|
160
|
+
PROVIDER_METADATA_SK: () => SkPrefix.METADATA,
|
|
161
|
+
// ============================================
|
|
162
|
+
// CHALLENGE
|
|
163
|
+
// ============================================
|
|
164
|
+
/**
|
|
165
|
+
* Challenge PK
|
|
166
|
+
* Pattern: CHL#{challengeId}
|
|
167
|
+
* Example: CHL#abc123xyz
|
|
168
|
+
*/
|
|
169
|
+
CHALLENGE_PK: (challengeId) => `${PkPrefix.CHALLENGE}${challengeId}`,
|
|
170
|
+
/**
|
|
171
|
+
* Challenge Metadata SK
|
|
172
|
+
* Pattern: MD
|
|
173
|
+
* Stores: userId, tenantId, audience, purpose, status, phase, allowedMethods,
|
|
174
|
+
* method, codeHash, attempts, maxAttempts, metadata, flowKey,
|
|
175
|
+
* expiresAt, createdAt, TTL
|
|
176
|
+
* Note: challengeId extracted from PK
|
|
177
|
+
*/
|
|
178
|
+
CHALLENGE_SK: () => SkPrefix.METADATA,
|
|
179
|
+
// ============================================
|
|
180
|
+
// IDENTITY
|
|
181
|
+
// ============================================
|
|
182
|
+
/**
|
|
183
|
+
* Identity PK
|
|
184
|
+
* Pattern: IDT#{type}#{value}
|
|
185
|
+
* Examples:
|
|
186
|
+
* - IDT#email#user@example.com
|
|
187
|
+
* - IDT#phone#+84987654321
|
|
188
|
+
* - IDT#username#johndoe
|
|
189
|
+
*
|
|
190
|
+
* Purpose: Fast O(1) lookup by identity type and value
|
|
191
|
+
*/
|
|
192
|
+
IDENTITY_PK: (type, value) => {
|
|
193
|
+
return `${PkPrefix.IDENTITY}${type}#${value}`;
|
|
194
|
+
},
|
|
195
|
+
/**
|
|
196
|
+
* Identity Metadata SK
|
|
197
|
+
* Pattern: MD
|
|
198
|
+
* Stores: userId, loginEnabled, verifiedAt, guards, createdAt, updatedAt
|
|
199
|
+
* Note: type and value extracted from PK
|
|
200
|
+
*/
|
|
201
|
+
IDENTITY_SK: () => SkPrefix.METADATA,
|
|
202
|
+
// ============================================
|
|
203
|
+
// CREDENTIAL
|
|
204
|
+
// ============================================
|
|
205
|
+
/**
|
|
206
|
+
* Credential PK
|
|
207
|
+
* Pattern: CRD#{credentialId}
|
|
208
|
+
* Example: CRD#cred_01JCQR8XMZP2KGH7NWV8F9E3TS
|
|
209
|
+
*/
|
|
210
|
+
CREDENTIAL_PK: (credentialId) => `${PkPrefix.CREDENTIAL}${credentialId}`,
|
|
211
|
+
/**
|
|
212
|
+
* Credential Metadata SK
|
|
213
|
+
* Pattern: MD
|
|
214
|
+
* Stores: userId, value, metadata, isActive, createdAt, updatedAt, lastUsedAt
|
|
215
|
+
* Note: credentialId and type extracted from PK and GSI SK
|
|
216
|
+
*/
|
|
217
|
+
CREDENTIAL_SK: () => SkPrefix.METADATA,
|
|
218
|
+
/**
|
|
219
|
+
* Credential Type Prefix (for GSI query)
|
|
220
|
+
* Pattern: CRD#{type}#
|
|
221
|
+
* Used to query credentials by type with begins_with
|
|
222
|
+
*/
|
|
223
|
+
CREDENTIAL_TYPE_PREFIX: (type) => `${PkPrefix.CREDENTIAL}${type}#`,
|
|
224
|
+
/**
|
|
225
|
+
* Credential User GSI SK
|
|
226
|
+
* Pattern: CRD#{type}#{credentialId}
|
|
227
|
+
* Example: CRD#password#cred_01JCQR8XMZP2KGH7NWV8F9E3TS
|
|
228
|
+
* Allows querying credentials by user and filtering by type
|
|
229
|
+
*/
|
|
230
|
+
CREDENTIAL_USER_SK: (type, credentialId) => `${PkPrefix.CREDENTIAL}${type}#${credentialId}`,
|
|
231
|
+
// ============================================
|
|
232
|
+
// IDENTITY
|
|
233
|
+
// ============================================
|
|
234
|
+
/**
|
|
235
|
+
* User Index GSI PK (for Identity)
|
|
236
|
+
* Pattern: USR#{userId}
|
|
237
|
+
* Example: USR#01JCQR8XMZP2KGH7NWV8F9E3TS
|
|
238
|
+
*
|
|
239
|
+
* Purpose: Query all identities for a specific user
|
|
240
|
+
*/
|
|
241
|
+
USER_INDEX_PK: (userId) => `${PkPrefix.USER}${userId}`,
|
|
242
|
+
/**
|
|
243
|
+
* User Index GSI SK (for Identity)
|
|
244
|
+
* Pattern: IDT#{type}#{value}
|
|
245
|
+
* Examples:
|
|
246
|
+
* - IDT#email#user@example.com
|
|
247
|
+
* - IDT#phone#+84987654321
|
|
248
|
+
*
|
|
249
|
+
* Purpose: Sort identities by type and extract type/value without additional attributes
|
|
250
|
+
*/
|
|
251
|
+
USER_INDEX_SK: (type, value) => {
|
|
252
|
+
return `${PkPrefix.IDENTITY}${type}#${value}`;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
var KeyExtractor = {
|
|
256
|
+
// ============================================
|
|
257
|
+
// PK Extractors
|
|
258
|
+
// ============================================
|
|
259
|
+
/**
|
|
260
|
+
* Extract userId from User PK
|
|
261
|
+
* Pattern: USR#{userId} -> userId
|
|
262
|
+
*/
|
|
263
|
+
userId: (pk) => pk.slice(PkPrefix.USER.length),
|
|
264
|
+
/**
|
|
265
|
+
* Extract sessionId from Session PK
|
|
266
|
+
* Pattern: SES#{sessionId} -> sessionId
|
|
267
|
+
*/
|
|
268
|
+
sessionId: (pk) => pk.slice(PkPrefix.SESSION.length),
|
|
269
|
+
/**
|
|
270
|
+
* Extract provider and externalId from Provider PK
|
|
271
|
+
* Pattern: PRV#{provider}#{externalId} -> { provider, externalId }
|
|
272
|
+
* Example: PRV#google#123456789 -> { provider: 'google', externalId: '123456789' }
|
|
273
|
+
*/
|
|
274
|
+
providerPKInfo: (pk) => {
|
|
275
|
+
const remainder = pk.slice(PkPrefix.PROVIDER.length);
|
|
276
|
+
const delimiterIndex = remainder.indexOf("#");
|
|
277
|
+
if (delimiterIndex === -1) return null;
|
|
278
|
+
const provider = remainder.slice(0, delimiterIndex);
|
|
279
|
+
const externalId = remainder.slice(delimiterIndex + 1);
|
|
280
|
+
return { provider, externalId };
|
|
281
|
+
},
|
|
282
|
+
// ============================================
|
|
283
|
+
// SK Extractors
|
|
284
|
+
// ============================================
|
|
285
|
+
/**
|
|
286
|
+
* Extract provider and externalId from Provider SK
|
|
287
|
+
* Pattern: PRV#{provider}#{externalId} -> { provider, externalId }
|
|
288
|
+
*/
|
|
289
|
+
providerInfo: (sk) => {
|
|
290
|
+
const remainder = sk.slice(SkPrefix.PROVIDER.length);
|
|
291
|
+
const delimiterIndex = remainder.indexOf(DELIMITER);
|
|
292
|
+
if (delimiterIndex === -1) return null;
|
|
293
|
+
const provider = remainder.slice(0, delimiterIndex);
|
|
294
|
+
const externalId = remainder.slice(delimiterIndex + 1);
|
|
295
|
+
return { provider, externalId };
|
|
296
|
+
},
|
|
297
|
+
/**
|
|
298
|
+
* Extract method and optional id from MFA SK
|
|
299
|
+
* Pattern: MFA#{method} or MFA#{method}#{id} -> { method, id? }
|
|
300
|
+
* Example: MFA#totp -> { method: 'totp' }
|
|
301
|
+
* Example: MFA#webauthn#key_abc -> { method: 'webauthn', id: 'key_abc' }
|
|
302
|
+
*/
|
|
303
|
+
mfaSKInfo: (sk) => {
|
|
304
|
+
const remainder = sk.slice(SkPrefix.MFA.length);
|
|
305
|
+
const delimiterIndex = remainder.indexOf(DELIMITER);
|
|
306
|
+
if (delimiterIndex === -1) {
|
|
307
|
+
return { method: remainder };
|
|
308
|
+
}
|
|
309
|
+
const method = remainder.slice(0, delimiterIndex);
|
|
310
|
+
const id = remainder.slice(delimiterIndex + 1);
|
|
311
|
+
return { method, id };
|
|
312
|
+
},
|
|
313
|
+
// ============================================
|
|
314
|
+
// Tenant Extractors (SCHEMA_V4)
|
|
315
|
+
// ============================================
|
|
316
|
+
/**
|
|
317
|
+
* Extract tenantId from TenantMember PK
|
|
318
|
+
* Pattern: TNT#{tenantId} -> tenantId
|
|
319
|
+
* Example: TNT#org-abc -> org-abc
|
|
320
|
+
*/
|
|
321
|
+
tenantId: (pk) => pk.slice(PkPrefix.TENANT.length),
|
|
322
|
+
/**
|
|
323
|
+
* Extract userId from TenantMember SK
|
|
324
|
+
* Pattern: USR#{userId} -> userId
|
|
325
|
+
* Example: USR#01HQZX8V9ABCDEFGHIJK -> 01HQZX8V9ABCDEFGHIJK
|
|
326
|
+
*/
|
|
327
|
+
tenantMemberUserId: (sk) => sk.slice(PkPrefix.USER.length),
|
|
328
|
+
/**
|
|
329
|
+
* Extract challengeId from Challenge PK
|
|
330
|
+
* Pattern: CHL#{challengeId} -> challengeId
|
|
331
|
+
* Example: CHL#abc123xyz -> abc123xyz
|
|
332
|
+
*/
|
|
333
|
+
challengeIdFromPK: (pk) => pk.slice(PkPrefix.CHALLENGE.length),
|
|
334
|
+
/**
|
|
335
|
+
* Extract type and value from Identity PK
|
|
336
|
+
* Pattern: IDT#{type}#{value} -> { type, value }
|
|
337
|
+
* Example: IDT#email#user@example.com -> { type: 'email', value: 'user@example.com' }
|
|
338
|
+
*/
|
|
339
|
+
identityPKInfo: (pk) => {
|
|
340
|
+
const remainder = pk.slice(PkPrefix.IDENTITY.length);
|
|
341
|
+
const delimiterIndex = remainder.indexOf("#");
|
|
342
|
+
if (delimiterIndex === -1) return null;
|
|
343
|
+
const type = remainder.slice(0, delimiterIndex);
|
|
344
|
+
const value = remainder.slice(delimiterIndex + 1);
|
|
345
|
+
return { type, value };
|
|
346
|
+
},
|
|
347
|
+
/**
|
|
348
|
+
* Extract credentialId from Credential PK
|
|
349
|
+
* Pattern: CRD#{credentialId} -> credentialId
|
|
350
|
+
* Example: CRD#cred_01JCQR8XMZP2KGH7NWV8F9E3TS -> cred_01JCQR8XMZP2KGH7NWV8F9E3TS
|
|
351
|
+
*/
|
|
352
|
+
credentialId: (pk) => pk.slice(PkPrefix.CREDENTIAL.length),
|
|
353
|
+
/**
|
|
354
|
+
* Extract type from Credential User GSI SK
|
|
355
|
+
* Pattern: CRD#{type}#{credentialId} -> type
|
|
356
|
+
* Example: CRD#password#cred_abc -> password
|
|
357
|
+
*/
|
|
358
|
+
credentialTypeFromUserSK: (sk) => {
|
|
359
|
+
const remainder = sk.slice(PkPrefix.CREDENTIAL.length);
|
|
360
|
+
const delimiterIndex = remainder.indexOf("#");
|
|
361
|
+
if (delimiterIndex === -1) return "";
|
|
362
|
+
return remainder.slice(0, delimiterIndex);
|
|
363
|
+
},
|
|
364
|
+
/**
|
|
365
|
+
* Extract type and value from User Index SK (GSI)
|
|
366
|
+
* Pattern: IDT#{type}#{value} -> { type, value }
|
|
367
|
+
* Example: IDT#email#user@example.com -> { type: 'email', value: 'user@example.com' }
|
|
368
|
+
*/
|
|
369
|
+
userIndexSKInfo: (sk) => {
|
|
370
|
+
const remainder = sk.slice(PkPrefix.IDENTITY.length);
|
|
371
|
+
const delimiterIndex = remainder.indexOf("#");
|
|
372
|
+
if (delimiterIndex === -1) return null;
|
|
373
|
+
const type = remainder.slice(0, delimiterIndex);
|
|
374
|
+
const value = remainder.slice(delimiterIndex + 1);
|
|
375
|
+
return { type, value };
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
var GSIKeys = {
|
|
379
|
+
// ============================================
|
|
380
|
+
// GSI_ActiveSessions
|
|
381
|
+
// ============================================
|
|
382
|
+
/**
|
|
383
|
+
* Active Sessions GSI PK
|
|
384
|
+
* Pattern: USR#{userId} (user-partitioned for scalability)
|
|
385
|
+
* Used by: GSI_ActiveSessions
|
|
386
|
+
* NOTE: Changed from global 'ACTIVE' to user-partitioned to prevent hot partition
|
|
387
|
+
*/
|
|
388
|
+
ACTIVE_SESSION_PK: (userId) => `${PkPrefix.USER}${userId}`,
|
|
389
|
+
/**
|
|
390
|
+
* Active Sessions GSI SK
|
|
391
|
+
* Pattern: {createdAt}#{sessionId}
|
|
392
|
+
* Used by: GSI_ActiveSessions
|
|
393
|
+
*/
|
|
394
|
+
ACTIVE_SESSION_SK: (createdAt, sessionId) => `${createdAt}${DELIMITER}${sessionId}`,
|
|
395
|
+
// ============================================
|
|
396
|
+
// GSI_UserSessions
|
|
397
|
+
// ============================================
|
|
398
|
+
/**
|
|
399
|
+
* User Sessions GSI PK
|
|
400
|
+
* Pattern: USR#{userId}
|
|
401
|
+
* Used by: GSI_UserSessions
|
|
402
|
+
*/
|
|
403
|
+
USER_SESSION_PK: (userId) => `${PkPrefix.USER}${userId}`,
|
|
404
|
+
/**
|
|
405
|
+
* User Sessions GSI SK
|
|
406
|
+
* Pattern: {createdAt}#{sessionId}
|
|
407
|
+
* Used by: GSI_UserSessions
|
|
408
|
+
*/
|
|
409
|
+
USER_SESSION_SK: (createdAt, sessionId) => `${createdAt}${DELIMITER}${sessionId}`,
|
|
410
|
+
// ============================================
|
|
411
|
+
// GSI_UsersByStatus
|
|
412
|
+
// ============================================
|
|
413
|
+
/**
|
|
414
|
+
* Users By Status GSI PK
|
|
415
|
+
* Pattern: USR#ACTIVE or USR#INACTIVE (SCHEMA_V4)
|
|
416
|
+
* Used by: GSI_UsersByStatus
|
|
417
|
+
* Maps: active → ACTIVE, suspended/deleted → INACTIVE
|
|
418
|
+
*/
|
|
419
|
+
USER_STATUS_PK: (status) => {
|
|
420
|
+
const statusGroup = status === USER_STATUS.ACTIVE ? "ACTIVE" : "INACTIVE";
|
|
421
|
+
return `${EntityPrefix.USER}${DELIMITER}${statusGroup}`;
|
|
422
|
+
},
|
|
423
|
+
/**
|
|
424
|
+
* Users By Status GSI SK
|
|
425
|
+
* Pattern: {userId}
|
|
426
|
+
* Used by: GSI_UsersByStatus
|
|
427
|
+
* Simple userId pattern to avoid needing createdAt for syncAuthStatus
|
|
428
|
+
*/
|
|
429
|
+
USER_STATUS_SK: (userId) => userId,
|
|
430
|
+
// ============================================
|
|
431
|
+
// GSI_UserProviders
|
|
432
|
+
// ============================================
|
|
433
|
+
/**
|
|
434
|
+
* User Providers GSI PK
|
|
435
|
+
* Pattern: USR#{userId}
|
|
436
|
+
* Used by: GSI_UserProviders
|
|
437
|
+
*/
|
|
438
|
+
USER_PROVIDER_PK: (userId) => `${PkPrefix.USER}${userId}`,
|
|
439
|
+
/**
|
|
440
|
+
* User Providers GSI SK
|
|
441
|
+
* Pattern: PRV#{provider}#{externalId}
|
|
442
|
+
* Used by: GSI_UserProviders
|
|
443
|
+
*/
|
|
444
|
+
USER_PROVIDER_SK: (provider, externalId) => `${SkPrefix.PROVIDER}${provider}${DELIMITER}${externalId}`
|
|
445
|
+
};
|
|
446
|
+
var TableAttr = {
|
|
447
|
+
PK: "PK",
|
|
448
|
+
SK: "SK",
|
|
449
|
+
TTL: "TTL",
|
|
450
|
+
ACTIVE_SESSION_PK: "activeSessionPk",
|
|
451
|
+
// GSI partition key for active sessions
|
|
452
|
+
ACTIVE_SESSION_SK: "activeSessionSk",
|
|
453
|
+
// GSI sort key for active sessions (createdAt#sessionId)
|
|
454
|
+
USER_SESSION_PK: "userSessionPk",
|
|
455
|
+
// GSI partition key for user sessions (USR#{userId})
|
|
456
|
+
USER_SESSION_SK: "userSessionSk",
|
|
457
|
+
// GSI sort key for user sessions (createdAt#sessionId)
|
|
458
|
+
USER_STATUS_PK: "userStatusPk",
|
|
459
|
+
// GSI partition key for users by status
|
|
460
|
+
USER_STATUS_SK: "userStatusSk",
|
|
461
|
+
// GSI sort key for users by status (userId)
|
|
462
|
+
USER_PROVIDER_PK: "userProviderPk",
|
|
463
|
+
// GSI partition key for user providers (USR#{userId})
|
|
464
|
+
USER_PROVIDER_SK: "userProviderSk",
|
|
465
|
+
// GSI sort key for user providers (PRV#{provider}#{externalId})
|
|
466
|
+
// Challenge attributes
|
|
467
|
+
USER_ID: "userId",
|
|
468
|
+
TENANT_ID: "tenantId",
|
|
469
|
+
AUDIENCE: "audience",
|
|
470
|
+
PURPOSE: "purpose",
|
|
471
|
+
CHALLENGE_STATUS: "challengeStatus",
|
|
472
|
+
// Named challengeStatus to avoid DynamoDB reserved keyword 'status'
|
|
473
|
+
PHASE: "phase",
|
|
474
|
+
ALLOWED_METHODS: "allowedMethods",
|
|
475
|
+
METHOD: "method",
|
|
476
|
+
IDENTITY_TYPE: "identityType",
|
|
477
|
+
// For credential challenges
|
|
478
|
+
IDENTITY_VALUE: "identityValue",
|
|
479
|
+
// For credential challenges
|
|
480
|
+
CODE_HASH: "codeHash",
|
|
481
|
+
ATTEMPTS: "attempts",
|
|
482
|
+
MAX_ATTEMPTS: "maxAttempts",
|
|
483
|
+
METADATA: "metadata",
|
|
484
|
+
FLOW_KEY: "flowKey",
|
|
485
|
+
EXPIRES_AT: "expiresAt",
|
|
486
|
+
CREATED_AT: "createdAt",
|
|
487
|
+
// Session attributes
|
|
488
|
+
TOKEN_ID: "tokenId",
|
|
489
|
+
LAST_USED_AT: "lastUsedAt",
|
|
490
|
+
REVOKED_AT: "revokedAt",
|
|
491
|
+
PERM_MASK: "permMask",
|
|
492
|
+
ROLE_IDS: "roleIds",
|
|
493
|
+
DEVICE_LABEL: "deviceLabel",
|
|
494
|
+
COUNTRY: "country",
|
|
495
|
+
CITY: "city",
|
|
496
|
+
// Identity attributes
|
|
497
|
+
IDENTITY_USER_PK: "identityUserPk",
|
|
498
|
+
// GSI partition key for user identities (USR#{userId})
|
|
499
|
+
IDENTITY_USER_SK: "identityUserSk",
|
|
500
|
+
// GSI sort key for user identities (IDT#{type}#{value})
|
|
501
|
+
LOGIN_ENABLED: "loginEnabled",
|
|
502
|
+
VERIFIED_AT: "verifiedAt",
|
|
503
|
+
GUARDS: "guards",
|
|
504
|
+
UPDATED_AT: "updatedAt",
|
|
505
|
+
// Credential attributes
|
|
506
|
+
CREDENTIAL_USER_PK: "credentialUserPk",
|
|
507
|
+
// GSI partition key for user credentials (USR#{userId})
|
|
508
|
+
CREDENTIAL_USER_SK: "credentialUserSk",
|
|
509
|
+
// GSI sort key for user credentials (CRD#{type}#{credentialId})
|
|
510
|
+
CREDENTIAL_VALUE: "value"
|
|
511
|
+
// Credential value (password hash, etc.)
|
|
512
|
+
// Note: REVOKED_AT is shared with Session (defined above)
|
|
513
|
+
};
|
|
514
|
+
var GSIName = {
|
|
515
|
+
ACTIVE_SESSIONS: "GSI_ActiveSessions",
|
|
516
|
+
USER_SESSIONS: "GSI_UserSessions",
|
|
517
|
+
USERS_BY_STATUS: "GSI_UsersByStatus",
|
|
518
|
+
USER_PROVIDERS: "GSI_UserProviders",
|
|
519
|
+
USER_IDENTITIES: "GSI_UserIdentities",
|
|
520
|
+
// Query all identities for a user
|
|
521
|
+
USER_CREDENTIALS: "GSI_UserCredentials"
|
|
522
|
+
// Query all credentials for a user
|
|
523
|
+
};
|
|
524
|
+
function millisToDate(millis) {
|
|
525
|
+
return new Date(millis);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/dynamodb-tenant-member-repo.ts
|
|
10
529
|
var DynamoDBTenantMemberRepository = class {
|
|
11
530
|
constructor(client, tableName) {
|
|
12
531
|
this.client = client;
|
|
@@ -16,14 +535,14 @@ var DynamoDBTenantMemberRepository = class {
|
|
|
16
535
|
});
|
|
17
536
|
}
|
|
18
537
|
docClient;
|
|
19
|
-
async get(tenantId,
|
|
538
|
+
async get(tenantId, userId) {
|
|
20
539
|
try {
|
|
21
540
|
const response = await this.docClient.send(
|
|
22
541
|
new libDynamodb.GetCommand({
|
|
23
542
|
TableName: this.tableName,
|
|
24
543
|
Key: {
|
|
25
|
-
[
|
|
26
|
-
[
|
|
544
|
+
[TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
|
|
545
|
+
[TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(userId)
|
|
27
546
|
}
|
|
28
547
|
})
|
|
29
548
|
);
|
|
@@ -42,8 +561,8 @@ var DynamoDBTenantMemberRepository = class {
|
|
|
42
561
|
new libDynamodb.PutCommand({
|
|
43
562
|
TableName: this.tableName,
|
|
44
563
|
Item: {
|
|
45
|
-
[
|
|
46
|
-
[
|
|
564
|
+
[TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(data.tenantId),
|
|
565
|
+
[TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(data.userId),
|
|
47
566
|
userStatus: data.status,
|
|
48
567
|
roleIds: JSON.stringify(data.roleIds),
|
|
49
568
|
permMask: data.permMask ?? 0,
|
|
@@ -61,7 +580,7 @@ var DynamoDBTenantMemberRepository = class {
|
|
|
61
580
|
return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
|
|
62
581
|
}
|
|
63
582
|
}
|
|
64
|
-
async updatePermissions(tenantId,
|
|
583
|
+
async updatePermissions(tenantId, userId, updates) {
|
|
65
584
|
try {
|
|
66
585
|
const setClauses = ["#updatedAt = :updatedAt"];
|
|
67
586
|
const names = { "#updatedAt": "updatedAt" };
|
|
@@ -80,8 +599,8 @@ var DynamoDBTenantMemberRepository = class {
|
|
|
80
599
|
new libDynamodb.UpdateCommand({
|
|
81
600
|
TableName: this.tableName,
|
|
82
601
|
Key: {
|
|
83
|
-
[
|
|
84
|
-
[
|
|
602
|
+
[TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
|
|
603
|
+
[TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(userId)
|
|
85
604
|
},
|
|
86
605
|
UpdateExpression: `SET ${setClauses.join(", ")}`,
|
|
87
606
|
ExpressionAttributeNames: names,
|
|
@@ -97,14 +616,14 @@ var DynamoDBTenantMemberRepository = class {
|
|
|
97
616
|
return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
|
|
98
617
|
}
|
|
99
618
|
}
|
|
100
|
-
async updateStatus(tenantId,
|
|
619
|
+
async updateStatus(tenantId, userId, status) {
|
|
101
620
|
try {
|
|
102
621
|
await this.docClient.send(
|
|
103
622
|
new libDynamodb.UpdateCommand({
|
|
104
623
|
TableName: this.tableName,
|
|
105
624
|
Key: {
|
|
106
|
-
[
|
|
107
|
-
[
|
|
625
|
+
[TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
|
|
626
|
+
[TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(userId)
|
|
108
627
|
},
|
|
109
628
|
UpdateExpression: "SET #status = :status, #updatedAt = :updatedAt",
|
|
110
629
|
ExpressionAttributeNames: {
|
|
@@ -126,14 +645,14 @@ var DynamoDBTenantMemberRepository = class {
|
|
|
126
645
|
return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
|
|
127
646
|
}
|
|
128
647
|
}
|
|
129
|
-
async remove(tenantId,
|
|
648
|
+
async remove(tenantId, userId) {
|
|
130
649
|
try {
|
|
131
650
|
await this.docClient.send(
|
|
132
651
|
new libDynamodb.DeleteCommand({
|
|
133
652
|
TableName: this.tableName,
|
|
134
653
|
Key: {
|
|
135
|
-
[
|
|
136
|
-
[
|
|
654
|
+
[TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
|
|
655
|
+
[TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(userId)
|
|
137
656
|
}
|
|
138
657
|
})
|
|
139
658
|
);
|
|
@@ -143,17 +662,16 @@ var DynamoDBTenantMemberRepository = class {
|
|
|
143
662
|
}
|
|
144
663
|
}
|
|
145
664
|
mapItemToEntity(item) {
|
|
146
|
-
const tenantId =
|
|
147
|
-
const
|
|
665
|
+
const tenantId = KeyExtractor.tenantId(item[TableAttr.PK]);
|
|
666
|
+
const userId = KeyExtractor.tenantMemberUserId(item[TableAttr.SK]);
|
|
148
667
|
return {
|
|
149
668
|
tenantId,
|
|
150
|
-
|
|
151
|
-
userId: skInfo?.userId ?? "",
|
|
669
|
+
userId,
|
|
152
670
|
status: item["userStatus"],
|
|
153
671
|
roleIds: JSON.parse(item["roleIds"]),
|
|
154
672
|
permMask: item["permMask"] ?? 0,
|
|
155
|
-
createdAt:
|
|
156
|
-
updatedAt:
|
|
673
|
+
createdAt: millisToDate(item["createdAt"]),
|
|
674
|
+
updatedAt: millisToDate(item["updatedAt"])
|
|
157
675
|
};
|
|
158
676
|
}
|
|
159
677
|
};
|
|
@@ -180,22 +698,22 @@ var DynamoDBSessionRepository = class {
|
|
|
180
698
|
const response = await this.docClient.send(
|
|
181
699
|
new libDynamodb.QueryCommand({
|
|
182
700
|
TableName: this.tableName,
|
|
183
|
-
IndexName:
|
|
184
|
-
KeyConditionExpression: `${
|
|
701
|
+
IndexName: GSIName.ACTIVE_SESSIONS,
|
|
702
|
+
KeyConditionExpression: `${TableAttr.ACTIVE_SESSION_PK} = :activePk`,
|
|
185
703
|
ExpressionAttributeValues: {
|
|
186
|
-
":activePk":
|
|
704
|
+
":activePk": GSIKeys.ACTIVE_SESSION_PK(userId)
|
|
187
705
|
},
|
|
188
|
-
ProjectionExpression: `${
|
|
706
|
+
ProjectionExpression: `${TableAttr.PK}, ${TableAttr.TENANT_ID}, ${TableAttr.AUDIENCE}, ${TableAttr.USER_ID}`,
|
|
189
707
|
Limit: limit,
|
|
190
708
|
ExclusiveStartKey: exclusiveStartKey,
|
|
191
709
|
ScanIndexForward: false
|
|
192
710
|
})
|
|
193
711
|
);
|
|
194
712
|
const sessions = (response.Items ?? []).map((item) => ({
|
|
195
|
-
id:
|
|
196
|
-
userId: item[
|
|
197
|
-
tenantId: item[
|
|
198
|
-
audience: item[
|
|
713
|
+
id: KeyExtractor.sessionId(item[TableAttr.PK]),
|
|
714
|
+
userId: item[TableAttr.USER_ID],
|
|
715
|
+
tenantId: item[TableAttr.TENANT_ID],
|
|
716
|
+
audience: item[TableAttr.AUDIENCE]
|
|
199
717
|
}));
|
|
200
718
|
const hasNext = !!response.LastEvaluatedKey;
|
|
201
719
|
const cursor = hasNext ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString("base64url") : null;
|
|
@@ -226,19 +744,19 @@ var DynamoDBSessionRepository = class {
|
|
|
226
744
|
Update: {
|
|
227
745
|
TableName: this.tableName,
|
|
228
746
|
Key: {
|
|
229
|
-
[
|
|
230
|
-
[
|
|
747
|
+
[TableAttr.PK]: KeyPattern.SESSION_PK(sessionId),
|
|
748
|
+
[TableAttr.SK]: KeyPattern.SESSION_SK()
|
|
231
749
|
},
|
|
232
750
|
UpdateExpression: `SET #revokedAt = :revokedAt REMOVE #activePk, #activeSk`,
|
|
233
751
|
ExpressionAttributeNames: {
|
|
234
|
-
"#revokedAt":
|
|
235
|
-
"#activePk":
|
|
236
|
-
"#activeSk":
|
|
752
|
+
"#revokedAt": TableAttr.REVOKED_AT,
|
|
753
|
+
"#activePk": TableAttr.ACTIVE_SESSION_PK,
|
|
754
|
+
"#activeSk": TableAttr.ACTIVE_SESSION_SK
|
|
237
755
|
},
|
|
238
756
|
ExpressionAttributeValues: {
|
|
239
757
|
":revokedAt": revokedAt
|
|
240
758
|
},
|
|
241
|
-
ConditionExpression: `attribute_exists(${
|
|
759
|
+
ConditionExpression: `attribute_exists(${TableAttr.PK}) AND attribute_not_exists(#revokedAt)`
|
|
242
760
|
}
|
|
243
761
|
}));
|
|
244
762
|
try {
|