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