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

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/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @auth-craft/tenant-access-control-dynamodb
2
+
3
+ > ⚠️ **Experimental / Internal Use**
4
+ >
5
+ > This package is published for convenience only.
6
+ >
7
+ > - No stability guarantee
8
+ > - Breaking changes may happen at any time
9
+ > - No documentation
10
+ > - No support
11
+ >
12
+ > Use at your own risk.
13
+
14
+ ---
15
+
16
+ DynamoDB implementation for `@auth-craft/tenant-access-control`.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @auth-craft/tenant-access-control @auth-craft/tenant-access-control-dynamodb
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```typescript
27
+ import { createTenantAccessSDK } from '@auth-craft/tenant-access-control';
28
+ import { createTenantAccessDynamoDBPlugin } from '@auth-craft/tenant-access-control-dynamodb';
29
+
30
+ // Create DynamoDB plugin
31
+ const plugin = createTenantAccessDynamoDBPlugin({
32
+ tableName: 'your-auth-table',
33
+ });
34
+
35
+ // Create SDK with DynamoDB implementation
36
+ const sdk = createTenantAccessSDK(plugin);
37
+
38
+ // Use SDK
39
+ const member = await sdk.getMember({ ... });
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ ### `createTenantAccessDynamoDBPlugin(config)`
45
+
46
+ **Parameters:**
47
+
48
+ ```typescript
49
+ interface TenantAccessDynamoDBConfig {
50
+ // DynamoDB table name (required)
51
+ tableName: string;
52
+
53
+ // Optional: Provide your own DynamoDB client
54
+ // If not provided, a new client will be created
55
+ client?: DynamoDBClient;
56
+ }
57
+ ```
58
+
59
+ ### With Custom Client
60
+
61
+ ```typescript
62
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
63
+
64
+ const client = new DynamoDBClient({
65
+ region: 'ap-southeast-1',
66
+ credentials: {
67
+ accessKeyId: '...',
68
+ secretAccessKey: '...',
69
+ },
70
+ });
71
+
72
+ const plugin = createTenantAccessDynamoDBPlugin({
73
+ tableName: 'auth-table',
74
+ client,
75
+ });
76
+ ```
77
+
78
+ ### With Local DynamoDB
79
+
80
+ ```typescript
81
+ const client = new DynamoDBClient({
82
+ endpoint: 'http://localhost:8000',
83
+ region: 'local',
84
+ credentials: {
85
+ accessKeyId: 'local',
86
+ secretAccessKey: 'local',
87
+ },
88
+ });
89
+
90
+ const plugin = createTenantAccessDynamoDBPlugin({
91
+ tableName: 'auth-table',
92
+ client,
93
+ });
94
+ ```
95
+
96
+ ## Table Schema
97
+
98
+ This package uses the same DynamoDB table schema as `@auth-craft/database-plugin-dynamodb`.
99
+
100
+ ### TenantMember Item
101
+
102
+ | Attribute | Value |
103
+ |-----------|-------|
104
+ | PK | `TNT#{tenantId}` |
105
+ | SK | `AUD#{audience}#USR#{userId}` |
106
+ | userStatus | `invited` \| `active` \| `suspended` \| `removed` |
107
+ | roleIds | JSON stringified array |
108
+ | permMask | number |
109
+ | createdAt | Unix milliseconds |
110
+ | updatedAt | Unix milliseconds |
111
+
112
+ ### Session Item
113
+
114
+ Uses existing session items with:
115
+ - `tenantId` - Tenant identifier
116
+ - `audience` - Target audience
117
+ - GSI_ActiveSessions for querying active sessions
118
+
119
+ ## Advanced Usage
120
+
121
+ ### Direct Repository Access
122
+
123
+ ```typescript
124
+ import {
125
+ DynamoDBTenantMemberRepository,
126
+ DynamoDBSessionRepository,
127
+ } from '@auth-craft/tenant-access-control-dynamodb';
128
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
129
+
130
+ const client = new DynamoDBClient({});
131
+
132
+ const tenantMemberRepo = new DynamoDBTenantMemberRepository(client, 'auth-table');
133
+ const sessionRepo = new DynamoDBSessionRepository(client, 'auth-table');
134
+
135
+ // Use repositories directly
136
+ const member = await tenantMemberRepo.get(tenantId, audience, userId);
137
+ ```
138
+
139
+ ## Dependencies
140
+
141
+ - `@auth-craft/tenant-access-control` - SDK and interfaces
142
+ - `@auth-craft/database-plugin-dynamodb` - Key patterns and utilities
143
+ - `@aws-sdk/client-dynamodb` - AWS SDK DynamoDB client
144
+ - `@aws-sdk/lib-dynamodb` - AWS SDK DynamoDB Document client
145
+
146
+ ## License
147
+
148
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,272 @@
1
+ 'use strict';
2
+
3
+ var clientDynamodb = require('@aws-sdk/client-dynamodb');
4
+ var libDynamodb = require('@aws-sdk/lib-dynamodb');
5
+ var tsMicroResult = require('ts-micro-result');
6
+ var tenantAccessControl = require('@auth-craft/tenant-access-control');
7
+ var databasePluginDynamodb = require('@auth-craft/database-plugin-dynamodb');
8
+
9
+ // src/index.ts
10
+ var DynamoDBTenantMemberRepository = class {
11
+ constructor(client, tableName) {
12
+ this.client = client;
13
+ this.tableName = tableName;
14
+ this.docClient = libDynamodb.DynamoDBDocumentClient.from(client, {
15
+ marshallOptions: { removeUndefinedValues: true }
16
+ });
17
+ }
18
+ docClient;
19
+ async get(tenantId, audience, userId) {
20
+ try {
21
+ const response = await this.docClient.send(
22
+ new libDynamodb.GetCommand({
23
+ TableName: this.tableName,
24
+ Key: {
25
+ [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(tenantId),
26
+ [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(audience, userId)
27
+ }
28
+ })
29
+ );
30
+ if (!response.Item) {
31
+ return tsMicroResult.ok(null);
32
+ }
33
+ return tsMicroResult.ok(this.mapItemToEntity(response.Item));
34
+ } catch {
35
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
36
+ }
37
+ }
38
+ async create(data) {
39
+ try {
40
+ const now = Date.now();
41
+ await this.docClient.send(
42
+ new libDynamodb.PutCommand({
43
+ TableName: this.tableName,
44
+ Item: {
45
+ [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(data.tenantId),
46
+ [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(data.audience, data.userId),
47
+ userStatus: data.status,
48
+ roleIds: JSON.stringify(data.roleIds),
49
+ permMask: data.permMask ?? 0,
50
+ createdAt: now,
51
+ updatedAt: now
52
+ },
53
+ ConditionExpression: "attribute_not_exists(PK)"
54
+ })
55
+ );
56
+ return tsMicroResult.ok();
57
+ } catch (error) {
58
+ if (error.name === "ConditionalCheckFailedException") {
59
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.MEMBER_ALREADY_EXISTS());
60
+ }
61
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
62
+ }
63
+ }
64
+ async updatePermissions(tenantId, audience, userId, updates) {
65
+ try {
66
+ const setClauses = ["#updatedAt = :updatedAt"];
67
+ const names = { "#updatedAt": "updatedAt" };
68
+ const values = { ":updatedAt": Date.now() };
69
+ if (updates.roleIds !== void 0) {
70
+ setClauses.push("#roleIds = :roleIds");
71
+ names["#roleIds"] = "roleIds";
72
+ values[":roleIds"] = JSON.stringify(updates.roleIds);
73
+ }
74
+ if (updates.permMask !== void 0) {
75
+ setClauses.push("#permMask = :permMask");
76
+ names["#permMask"] = "permMask";
77
+ values[":permMask"] = updates.permMask;
78
+ }
79
+ await this.docClient.send(
80
+ new libDynamodb.UpdateCommand({
81
+ TableName: this.tableName,
82
+ Key: {
83
+ [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(tenantId),
84
+ [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(audience, userId)
85
+ },
86
+ UpdateExpression: `SET ${setClauses.join(", ")}`,
87
+ ExpressionAttributeNames: names,
88
+ ExpressionAttributeValues: values,
89
+ ConditionExpression: "attribute_exists(PK)"
90
+ })
91
+ );
92
+ return tsMicroResult.ok();
93
+ } catch (error) {
94
+ if (error.name === "ConditionalCheckFailedException") {
95
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.MEMBER_NOT_FOUND());
96
+ }
97
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
98
+ }
99
+ }
100
+ async updateStatus(tenantId, audience, userId, status) {
101
+ try {
102
+ await this.docClient.send(
103
+ new libDynamodb.UpdateCommand({
104
+ TableName: this.tableName,
105
+ Key: {
106
+ [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(tenantId),
107
+ [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(audience, userId)
108
+ },
109
+ UpdateExpression: "SET #status = :status, #updatedAt = :updatedAt",
110
+ ExpressionAttributeNames: {
111
+ "#status": "userStatus",
112
+ "#updatedAt": "updatedAt"
113
+ },
114
+ ExpressionAttributeValues: {
115
+ ":status": status,
116
+ ":updatedAt": Date.now()
117
+ },
118
+ ConditionExpression: "attribute_exists(PK)"
119
+ })
120
+ );
121
+ return tsMicroResult.ok();
122
+ } catch (error) {
123
+ if (error.name === "ConditionalCheckFailedException") {
124
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.MEMBER_NOT_FOUND());
125
+ }
126
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
127
+ }
128
+ }
129
+ async remove(tenantId, audience, userId) {
130
+ try {
131
+ await this.docClient.send(
132
+ new libDynamodb.DeleteCommand({
133
+ TableName: this.tableName,
134
+ Key: {
135
+ [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_PK(tenantId),
136
+ [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.TENANT_MEMBER_SK(audience, userId)
137
+ }
138
+ })
139
+ );
140
+ return tsMicroResult.ok();
141
+ } catch {
142
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
143
+ }
144
+ }
145
+ mapItemToEntity(item) {
146
+ const tenantId = databasePluginDynamodb.KeyExtractor.tenantId(item[databasePluginDynamodb.TableAttr.PK]);
147
+ const skInfo = databasePluginDynamodb.KeyExtractor.tenantMemberSKInfo(item[databasePluginDynamodb.TableAttr.SK]);
148
+ return {
149
+ tenantId,
150
+ audience: skInfo?.audience ?? "",
151
+ userId: skInfo?.userId ?? "",
152
+ status: item["userStatus"],
153
+ roleIds: JSON.parse(item["roleIds"]),
154
+ permMask: item["permMask"] ?? 0,
155
+ createdAt: databasePluginDynamodb.millisToDate(item["createdAt"]),
156
+ updatedAt: databasePluginDynamodb.millisToDate(item["updatedAt"])
157
+ };
158
+ }
159
+ };
160
+ var DynamoDBSessionRepository = class {
161
+ constructor(client, tableName) {
162
+ this.client = client;
163
+ this.tableName = tableName;
164
+ this.docClient = libDynamodb.DynamoDBDocumentClient.from(client);
165
+ }
166
+ docClient;
167
+ async findActiveByUserId(userId, options) {
168
+ try {
169
+ const limit = Math.min(options?.limit ?? 50, 100);
170
+ let exclusiveStartKey;
171
+ if (options?.cursor) {
172
+ try {
173
+ exclusiveStartKey = JSON.parse(
174
+ Buffer.from(options.cursor, "base64url").toString("utf-8")
175
+ );
176
+ } catch {
177
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.INVALID_CURSOR());
178
+ }
179
+ }
180
+ const response = await this.docClient.send(
181
+ new libDynamodb.QueryCommand({
182
+ TableName: this.tableName,
183
+ IndexName: databasePluginDynamodb.GSIName.ACTIVE_SESSIONS,
184
+ KeyConditionExpression: `${databasePluginDynamodb.TableAttr.ACTIVE_SESSION_PK} = :activePk`,
185
+ ExpressionAttributeValues: {
186
+ ":activePk": databasePluginDynamodb.GSIKeys.ACTIVE_SESSION_PK(userId)
187
+ },
188
+ ProjectionExpression: `${databasePluginDynamodb.TableAttr.PK}, ${databasePluginDynamodb.TableAttr.TENANT_ID}, ${databasePluginDynamodb.TableAttr.AUDIENCE}, ${databasePluginDynamodb.TableAttr.USER_ID}`,
189
+ Limit: limit,
190
+ ExclusiveStartKey: exclusiveStartKey,
191
+ ScanIndexForward: false
192
+ })
193
+ );
194
+ const sessions = (response.Items ?? []).map((item) => ({
195
+ id: databasePluginDynamodb.KeyExtractor.sessionId(item[databasePluginDynamodb.TableAttr.PK]),
196
+ userId: item[databasePluginDynamodb.TableAttr.USER_ID],
197
+ tenantId: item[databasePluginDynamodb.TableAttr.TENANT_ID],
198
+ audience: item[databasePluginDynamodb.TableAttr.AUDIENCE]
199
+ }));
200
+ const hasNext = !!response.LastEvaluatedKey;
201
+ const cursor = hasNext ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString("base64url") : null;
202
+ const pagination = {
203
+ type: "cursor",
204
+ limit,
205
+ count: sessions.length,
206
+ hasNext,
207
+ hasPrev: !!options?.cursor,
208
+ cursor
209
+ };
210
+ return tsMicroResult.ok(sessions, { pagination });
211
+ } catch {
212
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
213
+ }
214
+ }
215
+ async revokeBatch(sessionIds) {
216
+ if (sessionIds.length === 0) {
217
+ return tsMicroResult.ok(0);
218
+ }
219
+ try {
220
+ const revokedAt = Date.now();
221
+ let count = 0;
222
+ const batchSize = 25;
223
+ for (let i = 0; i < sessionIds.length; i += batchSize) {
224
+ const batch = sessionIds.slice(i, i + batchSize);
225
+ const transactItems = batch.map((sessionId) => ({
226
+ Update: {
227
+ TableName: this.tableName,
228
+ Key: {
229
+ [databasePluginDynamodb.TableAttr.PK]: databasePluginDynamodb.KeyPattern.SESSION_PK(sessionId),
230
+ [databasePluginDynamodb.TableAttr.SK]: databasePluginDynamodb.KeyPattern.SESSION_SK()
231
+ },
232
+ UpdateExpression: `SET #revokedAt = :revokedAt REMOVE #activePk, #activeSk`,
233
+ ExpressionAttributeNames: {
234
+ "#revokedAt": databasePluginDynamodb.TableAttr.REVOKED_AT,
235
+ "#activePk": databasePluginDynamodb.TableAttr.ACTIVE_SESSION_PK,
236
+ "#activeSk": databasePluginDynamodb.TableAttr.ACTIVE_SESSION_SK
237
+ },
238
+ ExpressionAttributeValues: {
239
+ ":revokedAt": revokedAt
240
+ },
241
+ ConditionExpression: `attribute_exists(${databasePluginDynamodb.TableAttr.PK}) AND attribute_not_exists(#revokedAt)`
242
+ }
243
+ }));
244
+ try {
245
+ await this.docClient.send(
246
+ new libDynamodb.TransactWriteCommand({ TransactItems: transactItems })
247
+ );
248
+ count += batch.length;
249
+ } catch {
250
+ }
251
+ }
252
+ return tsMicroResult.ok(count);
253
+ } catch {
254
+ return tsMicroResult.err(tenantAccessControl.tenantAccessErrors.DATABASE_ERROR());
255
+ }
256
+ }
257
+ };
258
+
259
+ // src/index.ts
260
+ function createTenantAccessDynamoDBPlugin(config) {
261
+ const client = config.client ?? new clientDynamodb.DynamoDBClient({});
262
+ return {
263
+ tenantMemberRepository: new DynamoDBTenantMemberRepository(client, config.tableName),
264
+ sessionRepository: new DynamoDBSessionRepository(client, config.tableName)
265
+ };
266
+ }
267
+
268
+ exports.DynamoDBSessionRepository = DynamoDBSessionRepository;
269
+ exports.DynamoDBTenantMemberRepository = DynamoDBTenantMemberRepository;
270
+ exports.createTenantAccessDynamoDBPlugin = createTenantAccessDynamoDBPlugin;
271
+ //# sourceMappingURL=index.cjs.map
272
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dynamodb-tenant-member-repo.ts","../src/dynamodb-session-repo.ts","../src/index.ts"],"names":["DynamoDBDocumentClient","GetCommand","TableAttr","KeyPattern","ok","err","tenantAccessErrors","PutCommand","UpdateCommand","DeleteCommand","KeyExtractor","millisToDate","QueryCommand","GSIName","GSIKeys","TransactWriteCommand","DynamoDBClient"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * DynamoDB TenantMember Repository Implementation\r\n */\r\n\r\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\r\nimport {\r\n DynamoDBDocumentClient,\r\n GetCommand,\r\n PutCommand,\r\n UpdateCommand,\r\n DeleteCommand,\r\n} from '@aws-sdk/lib-dynamodb';\r\nimport { ok, err, type Result } from 'ts-micro-result';\r\nimport type {\r\n TenantMemberRepository,\r\n TenantMember,\r\n TenantMemberStatus,\r\n CreateTenantMemberData,\r\n UpdateTenantMemberPermissionsData,\r\n TenantId,\r\n UserId,\r\n} from '@auth-craft/tenant-access-control';\r\nimport { tenantAccessErrors } from '@auth-craft/tenant-access-control';\r\nimport {\r\n KeyPattern,\r\n KeyExtractor,\r\n TableAttr,\r\n millisToDate,\r\n} from '@auth-craft/database-plugin-dynamodb';\r\n\r\nexport class DynamoDBTenantMemberRepository implements TenantMemberRepository {\r\n private docClient: DynamoDBDocumentClient;\r\n\r\n constructor(\r\n private client: DynamoDBClient,\r\n private tableName: string\r\n ) {\r\n this.docClient = DynamoDBDocumentClient.from(client, {\r\n marshallOptions: { removeUndefinedValues: true },\r\n });\r\n }\r\n\r\n async get(\r\n tenantId: TenantId,\r\n audience: string,\r\n userId: UserId\r\n ): Promise<Result<TenantMember | null>> {\r\n try {\r\n const response = await this.docClient.send(\r\n new GetCommand({\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId),\r\n },\r\n })\r\n );\r\n\r\n if (!response.Item) {\r\n return ok(null);\r\n }\r\n\r\n return ok(this.mapItemToEntity(response.Item));\r\n } catch {\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n async create(data: CreateTenantMemberData): Promise<Result<void>> {\r\n try {\r\n const now = Date.now();\r\n\r\n await this.docClient.send(\r\n new PutCommand({\r\n TableName: this.tableName,\r\n Item: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(data.tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(data.audience, data.userId),\r\n userStatus: data.status,\r\n roleIds: JSON.stringify(data.roleIds),\r\n permMask: data.permMask ?? 0,\r\n createdAt: now,\r\n updatedAt: now,\r\n },\r\n ConditionExpression: 'attribute_not_exists(PK)',\r\n })\r\n );\r\n\r\n return ok();\r\n } catch (error: unknown) {\r\n if ((error as { name?: string }).name === 'ConditionalCheckFailedException') {\r\n return err(tenantAccessErrors.MEMBER_ALREADY_EXISTS());\r\n }\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n async updatePermissions(\r\n tenantId: TenantId,\r\n audience: string,\r\n userId: UserId,\r\n updates: UpdateTenantMemberPermissionsData\r\n ): Promise<Result<void>> {\r\n try {\r\n const setClauses: string[] = ['#updatedAt = :updatedAt'];\r\n const names: Record<string, string> = { '#updatedAt': 'updatedAt' };\r\n const values: Record<string, unknown> = { ':updatedAt': Date.now() };\r\n\r\n if (updates.roleIds !== undefined) {\r\n setClauses.push('#roleIds = :roleIds');\r\n names['#roleIds'] = 'roleIds';\r\n values[':roleIds'] = JSON.stringify(updates.roleIds);\r\n }\r\n\r\n if (updates.permMask !== undefined) {\r\n setClauses.push('#permMask = :permMask');\r\n names['#permMask'] = 'permMask';\r\n values[':permMask'] = updates.permMask;\r\n }\r\n\r\n await this.docClient.send(\r\n new UpdateCommand({\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId),\r\n },\r\n UpdateExpression: `SET ${setClauses.join(', ')}`,\r\n ExpressionAttributeNames: names,\r\n ExpressionAttributeValues: values,\r\n ConditionExpression: 'attribute_exists(PK)',\r\n })\r\n );\r\n\r\n return ok();\r\n } catch (error: unknown) {\r\n if ((error as { name?: string }).name === 'ConditionalCheckFailedException') {\r\n return err(tenantAccessErrors.MEMBER_NOT_FOUND());\r\n }\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n async updateStatus(\r\n tenantId: TenantId,\r\n audience: string,\r\n userId: UserId,\r\n status: TenantMemberStatus\r\n ): Promise<Result<void>> {\r\n try {\r\n await this.docClient.send(\r\n new UpdateCommand({\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId),\r\n },\r\n UpdateExpression: 'SET #status = :status, #updatedAt = :updatedAt',\r\n ExpressionAttributeNames: {\r\n '#status': 'userStatus',\r\n '#updatedAt': 'updatedAt',\r\n },\r\n ExpressionAttributeValues: {\r\n ':status': status,\r\n ':updatedAt': Date.now(),\r\n },\r\n ConditionExpression: 'attribute_exists(PK)',\r\n })\r\n );\r\n\r\n return ok();\r\n } catch (error: unknown) {\r\n if ((error as { name?: string }).name === 'ConditionalCheckFailedException') {\r\n return err(tenantAccessErrors.MEMBER_NOT_FOUND());\r\n }\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n async remove(tenantId: TenantId, audience: string, userId: UserId): Promise<Result<void>> {\r\n try {\r\n await this.docClient.send(\r\n new DeleteCommand({\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId),\r\n },\r\n })\r\n );\r\n\r\n return ok();\r\n } catch {\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n private mapItemToEntity(item: Record<string, unknown>): TenantMember {\r\n const tenantId = KeyExtractor.tenantId(item[TableAttr.PK] as string) as TenantId;\r\n const skInfo = KeyExtractor.tenantMemberSKInfo(item[TableAttr.SK] as string);\r\n\r\n return {\r\n tenantId,\r\n audience: skInfo?.audience ?? '',\r\n userId: (skInfo?.userId ?? '') as UserId,\r\n status: item['userStatus'] as TenantMemberStatus,\r\n roleIds: JSON.parse(item['roleIds'] as string),\r\n permMask: (item['permMask'] as number) ?? 0,\r\n createdAt: millisToDate(item['createdAt'] as number),\r\n updatedAt: millisToDate(item['updatedAt'] as number),\r\n };\r\n }\r\n}\r\n","/**\r\n * DynamoDB Session Repository Implementation (Limited)\r\n *\r\n * Only implements methods needed for tenant access control:\r\n * - findActiveByUserId: Query active sessions\r\n * - revokeBatch: Batch revoke sessions\r\n */\r\n\r\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\r\nimport {\r\n DynamoDBDocumentClient,\r\n QueryCommand,\r\n TransactWriteCommand,\r\n} from '@aws-sdk/lib-dynamodb';\r\nimport { ok, err, type Result, type PaginatedResult, type CursorPagination } from 'ts-micro-result';\r\nimport type {\r\n SessionRepository,\r\n SessionInfo,\r\n SessionId,\r\n UserId,\r\n TenantId,\r\n} from '@auth-craft/tenant-access-control';\r\nimport { tenantAccessErrors } from '@auth-craft/tenant-access-control';\r\nimport {\r\n KeyPattern,\r\n KeyExtractor,\r\n TableAttr,\r\n GSIName,\r\n GSIKeys,\r\n} from '@auth-craft/database-plugin-dynamodb';\r\n\r\nexport class DynamoDBSessionRepository implements SessionRepository {\r\n private docClient: DynamoDBDocumentClient;\r\n\r\n constructor(\r\n private client: DynamoDBClient,\r\n private tableName: string\r\n ) {\r\n this.docClient = DynamoDBDocumentClient.from(client);\r\n }\r\n\r\n async findActiveByUserId(\r\n userId: UserId,\r\n options?: { limit?: number; cursor?: string }\r\n ): Promise<PaginatedResult<SessionInfo>> {\r\n try {\r\n const limit = Math.min(options?.limit ?? 50, 100);\r\n let exclusiveStartKey: Record<string, unknown> | undefined;\r\n\r\n if (options?.cursor) {\r\n try {\r\n exclusiveStartKey = JSON.parse(\r\n Buffer.from(options.cursor, 'base64url').toString('utf-8')\r\n );\r\n } catch {\r\n return err(tenantAccessErrors.INVALID_CURSOR()) as PaginatedResult<SessionInfo>;\r\n }\r\n }\r\n\r\n const response = await this.docClient.send(\r\n new QueryCommand({\r\n TableName: this.tableName,\r\n IndexName: GSIName.ACTIVE_SESSIONS,\r\n KeyConditionExpression: `${TableAttr.ACTIVE_SESSION_PK} = :activePk`,\r\n ExpressionAttributeValues: {\r\n ':activePk': GSIKeys.ACTIVE_SESSION_PK(userId),\r\n },\r\n ProjectionExpression: `${TableAttr.PK}, ${TableAttr.TENANT_ID}, ${TableAttr.AUDIENCE}, ${TableAttr.USER_ID}`,\r\n Limit: limit,\r\n ExclusiveStartKey: exclusiveStartKey,\r\n ScanIndexForward: false,\r\n })\r\n );\r\n\r\n const sessions: SessionInfo[] = (response.Items ?? []).map((item) => ({\r\n id: KeyExtractor.sessionId(item[TableAttr.PK] as string) as SessionId,\r\n userId: item[TableAttr.USER_ID] as UserId,\r\n tenantId: item[TableAttr.TENANT_ID] as TenantId | undefined,\r\n audience: item[TableAttr.AUDIENCE] as string,\r\n }));\r\n\r\n const hasNext = !!response.LastEvaluatedKey;\r\n const cursor = hasNext\r\n ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString('base64url')\r\n : null;\r\n\r\n const pagination: CursorPagination = {\r\n type: 'cursor',\r\n limit,\r\n count: sessions.length,\r\n hasNext,\r\n hasPrev: !!options?.cursor,\r\n cursor,\r\n };\r\n\r\n return ok(sessions, { pagination }) as PaginatedResult<SessionInfo>;\r\n } catch {\r\n return err(tenantAccessErrors.DATABASE_ERROR()) as PaginatedResult<SessionInfo>;\r\n }\r\n }\r\n\r\n async revokeBatch(sessionIds: SessionId[]): Promise<Result<number>> {\r\n if (sessionIds.length === 0) {\r\n return ok(0);\r\n }\r\n\r\n try {\r\n const revokedAt = Date.now();\r\n let count = 0;\r\n const batchSize = 25;\r\n\r\n for (let i = 0; i < sessionIds.length; i += batchSize) {\r\n const batch = sessionIds.slice(i, i + batchSize);\r\n\r\n const transactItems = batch.map((sessionId) => ({\r\n Update: {\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.SESSION_PK(sessionId),\r\n [TableAttr.SK]: KeyPattern.SESSION_SK(),\r\n },\r\n UpdateExpression: `SET #revokedAt = :revokedAt REMOVE #activePk, #activeSk`,\r\n ExpressionAttributeNames: {\r\n '#revokedAt': TableAttr.REVOKED_AT,\r\n '#activePk': TableAttr.ACTIVE_SESSION_PK,\r\n '#activeSk': TableAttr.ACTIVE_SESSION_SK,\r\n },\r\n ExpressionAttributeValues: {\r\n ':revokedAt': revokedAt,\r\n },\r\n ConditionExpression: `attribute_exists(${TableAttr.PK}) AND attribute_not_exists(#revokedAt)`,\r\n },\r\n }));\r\n\r\n try {\r\n await this.docClient.send(\r\n new TransactWriteCommand({ TransactItems: transactItems })\r\n );\r\n count += batch.length;\r\n } catch {\r\n // Some sessions may already be revoked, continue with remaining batches\r\n }\r\n }\r\n\r\n return ok(count);\r\n } catch {\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n}\r\n","/**\r\n * @auth-craft/tenant-access-control-dynamodb\r\n *\r\n * DynamoDB implementation for tenant-access-control.\r\n *\r\n * @example\r\n * ```typescript\r\n * import { createTenantAccessSDK } from '@auth-craft/tenant-access-control';\r\n * import { createTenantAccessDynamoDBPlugin } from '@auth-craft/tenant-access-control-dynamodb';\r\n *\r\n * // Create DynamoDB plugin\r\n * const plugin = createTenantAccessDynamoDBPlugin({\r\n * tableName: 'auth-table',\r\n * });\r\n *\r\n * // Create SDK with DynamoDB implementation\r\n * const sdk = createTenantAccessSDK(plugin);\r\n * ```\r\n */\r\n\r\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\r\nimport type { TenantAccessControlDeps } from '@auth-craft/tenant-access-control';\r\nimport { DynamoDBTenantMemberRepository } from './dynamodb-tenant-member-repo';\r\nimport { DynamoDBSessionRepository } from './dynamodb-session-repo';\r\nimport type { TenantAccessDynamoDBConfig } from './config';\r\n\r\n/**\r\n * Create DynamoDB plugin for Tenant Access Control\r\n *\r\n * @param config - DynamoDB configuration\r\n * @returns TenantAccessControlDeps - Dependencies for createTenantAccessSDK\r\n */\r\nexport function createTenantAccessDynamoDBPlugin(\r\n config: TenantAccessDynamoDBConfig\r\n): TenantAccessControlDeps {\r\n const client = config.client ?? new DynamoDBClient({});\r\n\r\n return {\r\n tenantMemberRepository: new DynamoDBTenantMemberRepository(client, config.tableName),\r\n sessionRepository: new DynamoDBSessionRepository(client, config.tableName),\r\n };\r\n}\r\n\r\n// Export config type\r\nexport type { TenantAccessDynamoDBConfig } from './config';\r\n\r\n// Export repositories for advanced use cases\r\nexport { DynamoDBTenantMemberRepository } from './dynamodb-tenant-member-repo';\r\nexport { DynamoDBSessionRepository } from './dynamodb-session-repo';\r\n"]}
@@ -0,0 +1,86 @@
1
+ import { TenantMemberRepository, TenantId, UserId, TenantMember, CreateTenantMemberData, UpdateTenantMemberPermissionsData, TenantMemberStatus, SessionRepository, SessionInfo, SessionId, TenantAccessControlDeps } from '@auth-craft/tenant-access-control';
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { Result, PaginatedResult } from 'ts-micro-result';
4
+
5
+ /**
6
+ * Configuration for Tenant Access Control DynamoDB Plugin
7
+ */
8
+
9
+ interface TenantAccessDynamoDBConfig {
10
+ /**
11
+ * DynamoDB table name
12
+ */
13
+ tableName: string;
14
+ /**
15
+ * Optional DynamoDB client instance
16
+ * If not provided, a new client will be created
17
+ */
18
+ client?: DynamoDBClient;
19
+ }
20
+
21
+ /**
22
+ * DynamoDB TenantMember Repository Implementation
23
+ */
24
+
25
+ declare class DynamoDBTenantMemberRepository implements TenantMemberRepository {
26
+ private client;
27
+ private tableName;
28
+ private docClient;
29
+ constructor(client: DynamoDBClient, tableName: string);
30
+ get(tenantId: TenantId, audience: string, userId: UserId): Promise<Result<TenantMember | null>>;
31
+ create(data: CreateTenantMemberData): Promise<Result<void>>;
32
+ updatePermissions(tenantId: TenantId, audience: string, userId: UserId, updates: UpdateTenantMemberPermissionsData): Promise<Result<void>>;
33
+ updateStatus(tenantId: TenantId, audience: string, userId: UserId, status: TenantMemberStatus): Promise<Result<void>>;
34
+ remove(tenantId: TenantId, audience: string, userId: UserId): Promise<Result<void>>;
35
+ private mapItemToEntity;
36
+ }
37
+
38
+ /**
39
+ * DynamoDB Session Repository Implementation (Limited)
40
+ *
41
+ * Only implements methods needed for tenant access control:
42
+ * - findActiveByUserId: Query active sessions
43
+ * - revokeBatch: Batch revoke sessions
44
+ */
45
+
46
+ declare class DynamoDBSessionRepository implements SessionRepository {
47
+ private client;
48
+ private tableName;
49
+ private docClient;
50
+ constructor(client: DynamoDBClient, tableName: string);
51
+ findActiveByUserId(userId: UserId, options?: {
52
+ limit?: number;
53
+ cursor?: string;
54
+ }): Promise<PaginatedResult<SessionInfo>>;
55
+ revokeBatch(sessionIds: SessionId[]): Promise<Result<number>>;
56
+ }
57
+
58
+ /**
59
+ * @auth-craft/tenant-access-control-dynamodb
60
+ *
61
+ * DynamoDB implementation for tenant-access-control.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * import { createTenantAccessSDK } from '@auth-craft/tenant-access-control';
66
+ * import { createTenantAccessDynamoDBPlugin } from '@auth-craft/tenant-access-control-dynamodb';
67
+ *
68
+ * // Create DynamoDB plugin
69
+ * const plugin = createTenantAccessDynamoDBPlugin({
70
+ * tableName: 'auth-table',
71
+ * });
72
+ *
73
+ * // Create SDK with DynamoDB implementation
74
+ * const sdk = createTenantAccessSDK(plugin);
75
+ * ```
76
+ */
77
+
78
+ /**
79
+ * Create DynamoDB plugin for Tenant Access Control
80
+ *
81
+ * @param config - DynamoDB configuration
82
+ * @returns TenantAccessControlDeps - Dependencies for createTenantAccessSDK
83
+ */
84
+ declare function createTenantAccessDynamoDBPlugin(config: TenantAccessDynamoDBConfig): TenantAccessControlDeps;
85
+
86
+ export { DynamoDBSessionRepository, DynamoDBTenantMemberRepository, type TenantAccessDynamoDBConfig, createTenantAccessDynamoDBPlugin };
@@ -0,0 +1,86 @@
1
+ import { TenantMemberRepository, TenantId, UserId, TenantMember, CreateTenantMemberData, UpdateTenantMemberPermissionsData, TenantMemberStatus, SessionRepository, SessionInfo, SessionId, TenantAccessControlDeps } from '@auth-craft/tenant-access-control';
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { Result, PaginatedResult } from 'ts-micro-result';
4
+
5
+ /**
6
+ * Configuration for Tenant Access Control DynamoDB Plugin
7
+ */
8
+
9
+ interface TenantAccessDynamoDBConfig {
10
+ /**
11
+ * DynamoDB table name
12
+ */
13
+ tableName: string;
14
+ /**
15
+ * Optional DynamoDB client instance
16
+ * If not provided, a new client will be created
17
+ */
18
+ client?: DynamoDBClient;
19
+ }
20
+
21
+ /**
22
+ * DynamoDB TenantMember Repository Implementation
23
+ */
24
+
25
+ declare class DynamoDBTenantMemberRepository implements TenantMemberRepository {
26
+ private client;
27
+ private tableName;
28
+ private docClient;
29
+ constructor(client: DynamoDBClient, tableName: string);
30
+ get(tenantId: TenantId, audience: string, userId: UserId): Promise<Result<TenantMember | null>>;
31
+ create(data: CreateTenantMemberData): Promise<Result<void>>;
32
+ updatePermissions(tenantId: TenantId, audience: string, userId: UserId, updates: UpdateTenantMemberPermissionsData): Promise<Result<void>>;
33
+ updateStatus(tenantId: TenantId, audience: string, userId: UserId, status: TenantMemberStatus): Promise<Result<void>>;
34
+ remove(tenantId: TenantId, audience: string, userId: UserId): Promise<Result<void>>;
35
+ private mapItemToEntity;
36
+ }
37
+
38
+ /**
39
+ * DynamoDB Session Repository Implementation (Limited)
40
+ *
41
+ * Only implements methods needed for tenant access control:
42
+ * - findActiveByUserId: Query active sessions
43
+ * - revokeBatch: Batch revoke sessions
44
+ */
45
+
46
+ declare class DynamoDBSessionRepository implements SessionRepository {
47
+ private client;
48
+ private tableName;
49
+ private docClient;
50
+ constructor(client: DynamoDBClient, tableName: string);
51
+ findActiveByUserId(userId: UserId, options?: {
52
+ limit?: number;
53
+ cursor?: string;
54
+ }): Promise<PaginatedResult<SessionInfo>>;
55
+ revokeBatch(sessionIds: SessionId[]): Promise<Result<number>>;
56
+ }
57
+
58
+ /**
59
+ * @auth-craft/tenant-access-control-dynamodb
60
+ *
61
+ * DynamoDB implementation for tenant-access-control.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * import { createTenantAccessSDK } from '@auth-craft/tenant-access-control';
66
+ * import { createTenantAccessDynamoDBPlugin } from '@auth-craft/tenant-access-control-dynamodb';
67
+ *
68
+ * // Create DynamoDB plugin
69
+ * const plugin = createTenantAccessDynamoDBPlugin({
70
+ * tableName: 'auth-table',
71
+ * });
72
+ *
73
+ * // Create SDK with DynamoDB implementation
74
+ * const sdk = createTenantAccessSDK(plugin);
75
+ * ```
76
+ */
77
+
78
+ /**
79
+ * Create DynamoDB plugin for Tenant Access Control
80
+ *
81
+ * @param config - DynamoDB configuration
82
+ * @returns TenantAccessControlDeps - Dependencies for createTenantAccessSDK
83
+ */
84
+ declare function createTenantAccessDynamoDBPlugin(config: TenantAccessDynamoDBConfig): TenantAccessControlDeps;
85
+
86
+ export { DynamoDBSessionRepository, DynamoDBTenantMemberRepository, type TenantAccessDynamoDBConfig, createTenantAccessDynamoDBPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,268 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import { DynamoDBDocumentClient, GetCommand, PutCommand, UpdateCommand, DeleteCommand, QueryCommand, TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
3
+ import { ok, err } from 'ts-micro-result';
4
+ import { tenantAccessErrors } from '@auth-craft/tenant-access-control';
5
+ import { TableAttr, KeyPattern, KeyExtractor, millisToDate, GSIKeys, GSIName } from '@auth-craft/database-plugin-dynamodb';
6
+
7
+ // src/index.ts
8
+ var DynamoDBTenantMemberRepository = class {
9
+ constructor(client, tableName) {
10
+ this.client = client;
11
+ this.tableName = tableName;
12
+ this.docClient = DynamoDBDocumentClient.from(client, {
13
+ marshallOptions: { removeUndefinedValues: true }
14
+ });
15
+ }
16
+ docClient;
17
+ async get(tenantId, audience, userId) {
18
+ try {
19
+ const response = await this.docClient.send(
20
+ new GetCommand({
21
+ TableName: this.tableName,
22
+ Key: {
23
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
24
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId)
25
+ }
26
+ })
27
+ );
28
+ if (!response.Item) {
29
+ return ok(null);
30
+ }
31
+ return ok(this.mapItemToEntity(response.Item));
32
+ } catch {
33
+ return err(tenantAccessErrors.DATABASE_ERROR());
34
+ }
35
+ }
36
+ async create(data) {
37
+ try {
38
+ const now = Date.now();
39
+ await this.docClient.send(
40
+ new PutCommand({
41
+ TableName: this.tableName,
42
+ Item: {
43
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(data.tenantId),
44
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(data.audience, data.userId),
45
+ userStatus: data.status,
46
+ roleIds: JSON.stringify(data.roleIds),
47
+ permMask: data.permMask ?? 0,
48
+ createdAt: now,
49
+ updatedAt: now
50
+ },
51
+ ConditionExpression: "attribute_not_exists(PK)"
52
+ })
53
+ );
54
+ return ok();
55
+ } catch (error) {
56
+ if (error.name === "ConditionalCheckFailedException") {
57
+ return err(tenantAccessErrors.MEMBER_ALREADY_EXISTS());
58
+ }
59
+ return err(tenantAccessErrors.DATABASE_ERROR());
60
+ }
61
+ }
62
+ async updatePermissions(tenantId, audience, userId, updates) {
63
+ try {
64
+ const setClauses = ["#updatedAt = :updatedAt"];
65
+ const names = { "#updatedAt": "updatedAt" };
66
+ const values = { ":updatedAt": Date.now() };
67
+ if (updates.roleIds !== void 0) {
68
+ setClauses.push("#roleIds = :roleIds");
69
+ names["#roleIds"] = "roleIds";
70
+ values[":roleIds"] = JSON.stringify(updates.roleIds);
71
+ }
72
+ if (updates.permMask !== void 0) {
73
+ setClauses.push("#permMask = :permMask");
74
+ names["#permMask"] = "permMask";
75
+ values[":permMask"] = updates.permMask;
76
+ }
77
+ await this.docClient.send(
78
+ new UpdateCommand({
79
+ TableName: this.tableName,
80
+ Key: {
81
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
82
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId)
83
+ },
84
+ UpdateExpression: `SET ${setClauses.join(", ")}`,
85
+ ExpressionAttributeNames: names,
86
+ ExpressionAttributeValues: values,
87
+ ConditionExpression: "attribute_exists(PK)"
88
+ })
89
+ );
90
+ return ok();
91
+ } catch (error) {
92
+ if (error.name === "ConditionalCheckFailedException") {
93
+ return err(tenantAccessErrors.MEMBER_NOT_FOUND());
94
+ }
95
+ return err(tenantAccessErrors.DATABASE_ERROR());
96
+ }
97
+ }
98
+ async updateStatus(tenantId, audience, userId, status) {
99
+ try {
100
+ await this.docClient.send(
101
+ new UpdateCommand({
102
+ TableName: this.tableName,
103
+ Key: {
104
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
105
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId)
106
+ },
107
+ UpdateExpression: "SET #status = :status, #updatedAt = :updatedAt",
108
+ ExpressionAttributeNames: {
109
+ "#status": "userStatus",
110
+ "#updatedAt": "updatedAt"
111
+ },
112
+ ExpressionAttributeValues: {
113
+ ":status": status,
114
+ ":updatedAt": Date.now()
115
+ },
116
+ ConditionExpression: "attribute_exists(PK)"
117
+ })
118
+ );
119
+ return ok();
120
+ } catch (error) {
121
+ if (error.name === "ConditionalCheckFailedException") {
122
+ return err(tenantAccessErrors.MEMBER_NOT_FOUND());
123
+ }
124
+ return err(tenantAccessErrors.DATABASE_ERROR());
125
+ }
126
+ }
127
+ async remove(tenantId, audience, userId) {
128
+ try {
129
+ await this.docClient.send(
130
+ new DeleteCommand({
131
+ TableName: this.tableName,
132
+ Key: {
133
+ [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),
134
+ [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId)
135
+ }
136
+ })
137
+ );
138
+ return ok();
139
+ } catch {
140
+ return err(tenantAccessErrors.DATABASE_ERROR());
141
+ }
142
+ }
143
+ mapItemToEntity(item) {
144
+ const tenantId = KeyExtractor.tenantId(item[TableAttr.PK]);
145
+ const skInfo = KeyExtractor.tenantMemberSKInfo(item[TableAttr.SK]);
146
+ return {
147
+ tenantId,
148
+ audience: skInfo?.audience ?? "",
149
+ userId: skInfo?.userId ?? "",
150
+ status: item["userStatus"],
151
+ roleIds: JSON.parse(item["roleIds"]),
152
+ permMask: item["permMask"] ?? 0,
153
+ createdAt: millisToDate(item["createdAt"]),
154
+ updatedAt: millisToDate(item["updatedAt"])
155
+ };
156
+ }
157
+ };
158
+ var DynamoDBSessionRepository = class {
159
+ constructor(client, tableName) {
160
+ this.client = client;
161
+ this.tableName = tableName;
162
+ this.docClient = DynamoDBDocumentClient.from(client);
163
+ }
164
+ docClient;
165
+ async findActiveByUserId(userId, options) {
166
+ try {
167
+ const limit = Math.min(options?.limit ?? 50, 100);
168
+ let exclusiveStartKey;
169
+ if (options?.cursor) {
170
+ try {
171
+ exclusiveStartKey = JSON.parse(
172
+ Buffer.from(options.cursor, "base64url").toString("utf-8")
173
+ );
174
+ } catch {
175
+ return err(tenantAccessErrors.INVALID_CURSOR());
176
+ }
177
+ }
178
+ const response = await this.docClient.send(
179
+ new QueryCommand({
180
+ TableName: this.tableName,
181
+ IndexName: GSIName.ACTIVE_SESSIONS,
182
+ KeyConditionExpression: `${TableAttr.ACTIVE_SESSION_PK} = :activePk`,
183
+ ExpressionAttributeValues: {
184
+ ":activePk": GSIKeys.ACTIVE_SESSION_PK(userId)
185
+ },
186
+ ProjectionExpression: `${TableAttr.PK}, ${TableAttr.TENANT_ID}, ${TableAttr.AUDIENCE}, ${TableAttr.USER_ID}`,
187
+ Limit: limit,
188
+ ExclusiveStartKey: exclusiveStartKey,
189
+ ScanIndexForward: false
190
+ })
191
+ );
192
+ const sessions = (response.Items ?? []).map((item) => ({
193
+ id: KeyExtractor.sessionId(item[TableAttr.PK]),
194
+ userId: item[TableAttr.USER_ID],
195
+ tenantId: item[TableAttr.TENANT_ID],
196
+ audience: item[TableAttr.AUDIENCE]
197
+ }));
198
+ const hasNext = !!response.LastEvaluatedKey;
199
+ const cursor = hasNext ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString("base64url") : null;
200
+ const pagination = {
201
+ type: "cursor",
202
+ limit,
203
+ count: sessions.length,
204
+ hasNext,
205
+ hasPrev: !!options?.cursor,
206
+ cursor
207
+ };
208
+ return ok(sessions, { pagination });
209
+ } catch {
210
+ return err(tenantAccessErrors.DATABASE_ERROR());
211
+ }
212
+ }
213
+ async revokeBatch(sessionIds) {
214
+ if (sessionIds.length === 0) {
215
+ return ok(0);
216
+ }
217
+ try {
218
+ const revokedAt = Date.now();
219
+ let count = 0;
220
+ const batchSize = 25;
221
+ for (let i = 0; i < sessionIds.length; i += batchSize) {
222
+ const batch = sessionIds.slice(i, i + batchSize);
223
+ const transactItems = batch.map((sessionId) => ({
224
+ Update: {
225
+ TableName: this.tableName,
226
+ Key: {
227
+ [TableAttr.PK]: KeyPattern.SESSION_PK(sessionId),
228
+ [TableAttr.SK]: KeyPattern.SESSION_SK()
229
+ },
230
+ UpdateExpression: `SET #revokedAt = :revokedAt REMOVE #activePk, #activeSk`,
231
+ ExpressionAttributeNames: {
232
+ "#revokedAt": TableAttr.REVOKED_AT,
233
+ "#activePk": TableAttr.ACTIVE_SESSION_PK,
234
+ "#activeSk": TableAttr.ACTIVE_SESSION_SK
235
+ },
236
+ ExpressionAttributeValues: {
237
+ ":revokedAt": revokedAt
238
+ },
239
+ ConditionExpression: `attribute_exists(${TableAttr.PK}) AND attribute_not_exists(#revokedAt)`
240
+ }
241
+ }));
242
+ try {
243
+ await this.docClient.send(
244
+ new TransactWriteCommand({ TransactItems: transactItems })
245
+ );
246
+ count += batch.length;
247
+ } catch {
248
+ }
249
+ }
250
+ return ok(count);
251
+ } catch {
252
+ return err(tenantAccessErrors.DATABASE_ERROR());
253
+ }
254
+ }
255
+ };
256
+
257
+ // src/index.ts
258
+ function createTenantAccessDynamoDBPlugin(config) {
259
+ const client = config.client ?? new DynamoDBClient({});
260
+ return {
261
+ tenantMemberRepository: new DynamoDBTenantMemberRepository(client, config.tableName),
262
+ sessionRepository: new DynamoDBSessionRepository(client, config.tableName)
263
+ };
264
+ }
265
+
266
+ export { DynamoDBSessionRepository, DynamoDBTenantMemberRepository, createTenantAccessDynamoDBPlugin };
267
+ //# sourceMappingURL=index.js.map
268
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dynamodb-tenant-member-repo.ts","../src/dynamodb-session-repo.ts","../src/index.ts"],"names":["DynamoDBDocumentClient","err","tenantAccessErrors","TableAttr","KeyExtractor","ok","KeyPattern","DynamoDBClient"],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * DynamoDB TenantMember Repository Implementation\r\n */\r\n\r\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\r\nimport {\r\n DynamoDBDocumentClient,\r\n GetCommand,\r\n PutCommand,\r\n UpdateCommand,\r\n DeleteCommand,\r\n} from '@aws-sdk/lib-dynamodb';\r\nimport { ok, err, type Result } from 'ts-micro-result';\r\nimport type {\r\n TenantMemberRepository,\r\n TenantMember,\r\n TenantMemberStatus,\r\n CreateTenantMemberData,\r\n UpdateTenantMemberPermissionsData,\r\n TenantId,\r\n UserId,\r\n} from '@auth-craft/tenant-access-control';\r\nimport { tenantAccessErrors } from '@auth-craft/tenant-access-control';\r\nimport {\r\n KeyPattern,\r\n KeyExtractor,\r\n TableAttr,\r\n millisToDate,\r\n} from '@auth-craft/database-plugin-dynamodb';\r\n\r\nexport class DynamoDBTenantMemberRepository implements TenantMemberRepository {\r\n private docClient: DynamoDBDocumentClient;\r\n\r\n constructor(\r\n private client: DynamoDBClient,\r\n private tableName: string\r\n ) {\r\n this.docClient = DynamoDBDocumentClient.from(client, {\r\n marshallOptions: { removeUndefinedValues: true },\r\n });\r\n }\r\n\r\n async get(\r\n tenantId: TenantId,\r\n audience: string,\r\n userId: UserId\r\n ): Promise<Result<TenantMember | null>> {\r\n try {\r\n const response = await this.docClient.send(\r\n new GetCommand({\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId),\r\n },\r\n })\r\n );\r\n\r\n if (!response.Item) {\r\n return ok(null);\r\n }\r\n\r\n return ok(this.mapItemToEntity(response.Item));\r\n } catch {\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n async create(data: CreateTenantMemberData): Promise<Result<void>> {\r\n try {\r\n const now = Date.now();\r\n\r\n await this.docClient.send(\r\n new PutCommand({\r\n TableName: this.tableName,\r\n Item: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(data.tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(data.audience, data.userId),\r\n userStatus: data.status,\r\n roleIds: JSON.stringify(data.roleIds),\r\n permMask: data.permMask ?? 0,\r\n createdAt: now,\r\n updatedAt: now,\r\n },\r\n ConditionExpression: 'attribute_not_exists(PK)',\r\n })\r\n );\r\n\r\n return ok();\r\n } catch (error: unknown) {\r\n if ((error as { name?: string }).name === 'ConditionalCheckFailedException') {\r\n return err(tenantAccessErrors.MEMBER_ALREADY_EXISTS());\r\n }\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n async updatePermissions(\r\n tenantId: TenantId,\r\n audience: string,\r\n userId: UserId,\r\n updates: UpdateTenantMemberPermissionsData\r\n ): Promise<Result<void>> {\r\n try {\r\n const setClauses: string[] = ['#updatedAt = :updatedAt'];\r\n const names: Record<string, string> = { '#updatedAt': 'updatedAt' };\r\n const values: Record<string, unknown> = { ':updatedAt': Date.now() };\r\n\r\n if (updates.roleIds !== undefined) {\r\n setClauses.push('#roleIds = :roleIds');\r\n names['#roleIds'] = 'roleIds';\r\n values[':roleIds'] = JSON.stringify(updates.roleIds);\r\n }\r\n\r\n if (updates.permMask !== undefined) {\r\n setClauses.push('#permMask = :permMask');\r\n names['#permMask'] = 'permMask';\r\n values[':permMask'] = updates.permMask;\r\n }\r\n\r\n await this.docClient.send(\r\n new UpdateCommand({\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId),\r\n },\r\n UpdateExpression: `SET ${setClauses.join(', ')}`,\r\n ExpressionAttributeNames: names,\r\n ExpressionAttributeValues: values,\r\n ConditionExpression: 'attribute_exists(PK)',\r\n })\r\n );\r\n\r\n return ok();\r\n } catch (error: unknown) {\r\n if ((error as { name?: string }).name === 'ConditionalCheckFailedException') {\r\n return err(tenantAccessErrors.MEMBER_NOT_FOUND());\r\n }\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n async updateStatus(\r\n tenantId: TenantId,\r\n audience: string,\r\n userId: UserId,\r\n status: TenantMemberStatus\r\n ): Promise<Result<void>> {\r\n try {\r\n await this.docClient.send(\r\n new UpdateCommand({\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId),\r\n },\r\n UpdateExpression: 'SET #status = :status, #updatedAt = :updatedAt',\r\n ExpressionAttributeNames: {\r\n '#status': 'userStatus',\r\n '#updatedAt': 'updatedAt',\r\n },\r\n ExpressionAttributeValues: {\r\n ':status': status,\r\n ':updatedAt': Date.now(),\r\n },\r\n ConditionExpression: 'attribute_exists(PK)',\r\n })\r\n );\r\n\r\n return ok();\r\n } catch (error: unknown) {\r\n if ((error as { name?: string }).name === 'ConditionalCheckFailedException') {\r\n return err(tenantAccessErrors.MEMBER_NOT_FOUND());\r\n }\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n async remove(tenantId: TenantId, audience: string, userId: UserId): Promise<Result<void>> {\r\n try {\r\n await this.docClient.send(\r\n new DeleteCommand({\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.TENANT_MEMBER_PK(tenantId),\r\n [TableAttr.SK]: KeyPattern.TENANT_MEMBER_SK(audience, userId),\r\n },\r\n })\r\n );\r\n\r\n return ok();\r\n } catch {\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n\r\n private mapItemToEntity(item: Record<string, unknown>): TenantMember {\r\n const tenantId = KeyExtractor.tenantId(item[TableAttr.PK] as string) as TenantId;\r\n const skInfo = KeyExtractor.tenantMemberSKInfo(item[TableAttr.SK] as string);\r\n\r\n return {\r\n tenantId,\r\n audience: skInfo?.audience ?? '',\r\n userId: (skInfo?.userId ?? '') as UserId,\r\n status: item['userStatus'] as TenantMemberStatus,\r\n roleIds: JSON.parse(item['roleIds'] as string),\r\n permMask: (item['permMask'] as number) ?? 0,\r\n createdAt: millisToDate(item['createdAt'] as number),\r\n updatedAt: millisToDate(item['updatedAt'] as number),\r\n };\r\n }\r\n}\r\n","/**\r\n * DynamoDB Session Repository Implementation (Limited)\r\n *\r\n * Only implements methods needed for tenant access control:\r\n * - findActiveByUserId: Query active sessions\r\n * - revokeBatch: Batch revoke sessions\r\n */\r\n\r\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\r\nimport {\r\n DynamoDBDocumentClient,\r\n QueryCommand,\r\n TransactWriteCommand,\r\n} from '@aws-sdk/lib-dynamodb';\r\nimport { ok, err, type Result, type PaginatedResult, type CursorPagination } from 'ts-micro-result';\r\nimport type {\r\n SessionRepository,\r\n SessionInfo,\r\n SessionId,\r\n UserId,\r\n TenantId,\r\n} from '@auth-craft/tenant-access-control';\r\nimport { tenantAccessErrors } from '@auth-craft/tenant-access-control';\r\nimport {\r\n KeyPattern,\r\n KeyExtractor,\r\n TableAttr,\r\n GSIName,\r\n GSIKeys,\r\n} from '@auth-craft/database-plugin-dynamodb';\r\n\r\nexport class DynamoDBSessionRepository implements SessionRepository {\r\n private docClient: DynamoDBDocumentClient;\r\n\r\n constructor(\r\n private client: DynamoDBClient,\r\n private tableName: string\r\n ) {\r\n this.docClient = DynamoDBDocumentClient.from(client);\r\n }\r\n\r\n async findActiveByUserId(\r\n userId: UserId,\r\n options?: { limit?: number; cursor?: string }\r\n ): Promise<PaginatedResult<SessionInfo>> {\r\n try {\r\n const limit = Math.min(options?.limit ?? 50, 100);\r\n let exclusiveStartKey: Record<string, unknown> | undefined;\r\n\r\n if (options?.cursor) {\r\n try {\r\n exclusiveStartKey = JSON.parse(\r\n Buffer.from(options.cursor, 'base64url').toString('utf-8')\r\n );\r\n } catch {\r\n return err(tenantAccessErrors.INVALID_CURSOR()) as PaginatedResult<SessionInfo>;\r\n }\r\n }\r\n\r\n const response = await this.docClient.send(\r\n new QueryCommand({\r\n TableName: this.tableName,\r\n IndexName: GSIName.ACTIVE_SESSIONS,\r\n KeyConditionExpression: `${TableAttr.ACTIVE_SESSION_PK} = :activePk`,\r\n ExpressionAttributeValues: {\r\n ':activePk': GSIKeys.ACTIVE_SESSION_PK(userId),\r\n },\r\n ProjectionExpression: `${TableAttr.PK}, ${TableAttr.TENANT_ID}, ${TableAttr.AUDIENCE}, ${TableAttr.USER_ID}`,\r\n Limit: limit,\r\n ExclusiveStartKey: exclusiveStartKey,\r\n ScanIndexForward: false,\r\n })\r\n );\r\n\r\n const sessions: SessionInfo[] = (response.Items ?? []).map((item) => ({\r\n id: KeyExtractor.sessionId(item[TableAttr.PK] as string) as SessionId,\r\n userId: item[TableAttr.USER_ID] as UserId,\r\n tenantId: item[TableAttr.TENANT_ID] as TenantId | undefined,\r\n audience: item[TableAttr.AUDIENCE] as string,\r\n }));\r\n\r\n const hasNext = !!response.LastEvaluatedKey;\r\n const cursor = hasNext\r\n ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString('base64url')\r\n : null;\r\n\r\n const pagination: CursorPagination = {\r\n type: 'cursor',\r\n limit,\r\n count: sessions.length,\r\n hasNext,\r\n hasPrev: !!options?.cursor,\r\n cursor,\r\n };\r\n\r\n return ok(sessions, { pagination }) as PaginatedResult<SessionInfo>;\r\n } catch {\r\n return err(tenantAccessErrors.DATABASE_ERROR()) as PaginatedResult<SessionInfo>;\r\n }\r\n }\r\n\r\n async revokeBatch(sessionIds: SessionId[]): Promise<Result<number>> {\r\n if (sessionIds.length === 0) {\r\n return ok(0);\r\n }\r\n\r\n try {\r\n const revokedAt = Date.now();\r\n let count = 0;\r\n const batchSize = 25;\r\n\r\n for (let i = 0; i < sessionIds.length; i += batchSize) {\r\n const batch = sessionIds.slice(i, i + batchSize);\r\n\r\n const transactItems = batch.map((sessionId) => ({\r\n Update: {\r\n TableName: this.tableName,\r\n Key: {\r\n [TableAttr.PK]: KeyPattern.SESSION_PK(sessionId),\r\n [TableAttr.SK]: KeyPattern.SESSION_SK(),\r\n },\r\n UpdateExpression: `SET #revokedAt = :revokedAt REMOVE #activePk, #activeSk`,\r\n ExpressionAttributeNames: {\r\n '#revokedAt': TableAttr.REVOKED_AT,\r\n '#activePk': TableAttr.ACTIVE_SESSION_PK,\r\n '#activeSk': TableAttr.ACTIVE_SESSION_SK,\r\n },\r\n ExpressionAttributeValues: {\r\n ':revokedAt': revokedAt,\r\n },\r\n ConditionExpression: `attribute_exists(${TableAttr.PK}) AND attribute_not_exists(#revokedAt)`,\r\n },\r\n }));\r\n\r\n try {\r\n await this.docClient.send(\r\n new TransactWriteCommand({ TransactItems: transactItems })\r\n );\r\n count += batch.length;\r\n } catch {\r\n // Some sessions may already be revoked, continue with remaining batches\r\n }\r\n }\r\n\r\n return ok(count);\r\n } catch {\r\n return err(tenantAccessErrors.DATABASE_ERROR());\r\n }\r\n }\r\n}\r\n","/**\r\n * @auth-craft/tenant-access-control-dynamodb\r\n *\r\n * DynamoDB implementation for tenant-access-control.\r\n *\r\n * @example\r\n * ```typescript\r\n * import { createTenantAccessSDK } from '@auth-craft/tenant-access-control';\r\n * import { createTenantAccessDynamoDBPlugin } from '@auth-craft/tenant-access-control-dynamodb';\r\n *\r\n * // Create DynamoDB plugin\r\n * const plugin = createTenantAccessDynamoDBPlugin({\r\n * tableName: 'auth-table',\r\n * });\r\n *\r\n * // Create SDK with DynamoDB implementation\r\n * const sdk = createTenantAccessSDK(plugin);\r\n * ```\r\n */\r\n\r\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\r\nimport type { TenantAccessControlDeps } from '@auth-craft/tenant-access-control';\r\nimport { DynamoDBTenantMemberRepository } from './dynamodb-tenant-member-repo';\r\nimport { DynamoDBSessionRepository } from './dynamodb-session-repo';\r\nimport type { TenantAccessDynamoDBConfig } from './config';\r\n\r\n/**\r\n * Create DynamoDB plugin for Tenant Access Control\r\n *\r\n * @param config - DynamoDB configuration\r\n * @returns TenantAccessControlDeps - Dependencies for createTenantAccessSDK\r\n */\r\nexport function createTenantAccessDynamoDBPlugin(\r\n config: TenantAccessDynamoDBConfig\r\n): TenantAccessControlDeps {\r\n const client = config.client ?? new DynamoDBClient({});\r\n\r\n return {\r\n tenantMemberRepository: new DynamoDBTenantMemberRepository(client, config.tableName),\r\n sessionRepository: new DynamoDBSessionRepository(client, config.tableName),\r\n };\r\n}\r\n\r\n// Export config type\r\nexport type { TenantAccessDynamoDBConfig } from './config';\r\n\r\n// Export repositories for advanced use cases\r\nexport { DynamoDBTenantMemberRepository } from './dynamodb-tenant-member-repo';\r\nexport { DynamoDBSessionRepository } from './dynamodb-session-repo';\r\n"]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@auth-craft/tenant-access-control-dynamodb",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "description": "DynamoDB implementation for tenant-access-control",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "dev": "tsup --watch",
23
+ "type-check": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "@auth-craft/tenant-access-control": "workspace:*",
27
+ "@auth-craft/database-plugin-dynamodb": "workspace:*",
28
+ "@aws-sdk/client-dynamodb": "^3.600.0",
29
+ "@aws-sdk/lib-dynamodb": "^3.600.0",
30
+ "ts-micro-result": "^2.2.0"
31
+ },
32
+ "devDependencies": {
33
+ "@auth-craft/tsup-config": "workspace:*",
34
+ "@types/node": "^22.10.0",
35
+ "tsup": "^8.3.5",
36
+ "typescript": "^5.7.2"
37
+ }
38
+ }