@datacules/agent-identity-store-aws 0.2.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/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@datacules/agent-identity-store-aws",
3
+ "version": "0.2.1",
4
+ "private": false,
5
+ "description": "AWS Secrets Manager + DynamoDB credential store for @datacules/agent-identity",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "type-check": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@aws-sdk/client-secrets-manager": "^3.600.0",
15
+ "@aws-sdk/client-dynamodb": "^3.600.0"
16
+ },
17
+ "peerDependencies": {
18
+ "@datacules/agent-identity": "^0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@datacules/agent-identity": "*",
22
+ "typescript": "^5"
23
+ }
24
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * AWS Secrets Manager + DynamoDB CredentialStore implementation.
3
+ *
4
+ * Secrets Manager holds the credential metadata JSON (id, kind, scope, status, ref).
5
+ * DynamoDB holds migration reservation locks (TTL-based, atomic conditional writes).
6
+ *
7
+ * Setup:
8
+ * 1. Store each Credential as a JSON string in Secrets Manager.
9
+ * Tag it with agent-identity-status: active|pending|revoked
10
+ * so listActive() can filter via the tag API.
11
+ * 2. Create a DynamoDB table named 'agent-identity-locks' with
12
+ * partition key 'ref' (String) and TTL attribute 'expiresAt' (Number).
13
+ * 3. Grant the IAM role: secretsmanager:GetSecretValue,
14
+ * secretsmanager:ListSecrets, dynamodb:PutItem, dynamodb:DeleteItem.
15
+ */
16
+ import {
17
+ SecretsManagerClient,
18
+ GetSecretValueCommand,
19
+ ListSecretsCommand,
20
+ } from '@aws-sdk/client-secrets-manager';
21
+ import {
22
+ DynamoDBClient,
23
+ PutItemCommand,
24
+ DeleteItemCommand,
25
+ } from '@aws-sdk/client-dynamodb';
26
+ import type { Credential, CredentialKind, CredentialStore } from '@datacules/agent-identity';
27
+
28
+ export interface AwsCredentialStoreOptions {
29
+ /** AWS region (default: reads AWS_REGION env var) */
30
+ region?: string;
31
+ /** DynamoDB table name for migration locks (default: 'agent-identity-locks') */
32
+ locksTable?: string;
33
+ }
34
+
35
+ export class AwsCredentialStore implements CredentialStore {
36
+ private readonly sm: SecretsManagerClient;
37
+ private readonly dynamo: DynamoDBClient;
38
+ private readonly locksTable: string;
39
+
40
+ constructor(options: AwsCredentialStoreOptions = {}) {
41
+ const config = options.region ? { region: options.region } : {};
42
+ this.sm = new SecretsManagerClient(config);
43
+ this.dynamo = new DynamoDBClient(config);
44
+ this.locksTable = options.locksTable ?? 'agent-identity-locks';
45
+ }
46
+
47
+ async findByRef(ref: string): Promise<Credential | null> {
48
+ try {
49
+ const res = await this.sm.send(new GetSecretValueCommand({ SecretId: ref }));
50
+ if (!res.SecretString) return null;
51
+ const cred: Credential = JSON.parse(res.SecretString);
52
+ return cred.status === 'active' ? cred : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ async listActive(): Promise<Credential[]> {
59
+ const res = await this.sm.send(
60
+ new ListSecretsCommand({
61
+ Filters: [{ Key: 'tag-key', Values: ['agent-identity-status'] }],
62
+ })
63
+ );
64
+ const results: Credential[] = [];
65
+ for (const s of res.SecretList ?? []) {
66
+ const tag = s.Tags?.find((t) => t.Key === 'agent-identity-status');
67
+ if (tag?.Value !== 'active') continue;
68
+ try {
69
+ const cred: Credential = JSON.parse(s.Description ?? '{}');
70
+ results.push(cred);
71
+ } catch {
72
+ // malformed description — skip
73
+ }
74
+ }
75
+ return results;
76
+ }
77
+
78
+ async listByKind(kind: CredentialKind): Promise<Credential[]> {
79
+ const all = await this.listActive();
80
+ return all.filter((c) => c.kind === kind);
81
+ }
82
+
83
+ async reserve(ref: string, migrationId: string, ttlSeconds: number): Promise<boolean> {
84
+ const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
85
+ try {
86
+ await this.dynamo.send(
87
+ new PutItemCommand({
88
+ TableName: this.locksTable,
89
+ Item: {
90
+ ref: { S: ref },
91
+ migrationId: { S: migrationId },
92
+ expiresAt: { N: String(expiresAt) },
93
+ },
94
+ // Succeed only if: no item exists, OR this migration already owns it, OR the TTL has expired
95
+ ConditionExpression:
96
+ 'attribute_not_exists(#r) OR migrationId = :mid OR expiresAt < :now',
97
+ ExpressionAttributeNames: { '#r': 'ref' },
98
+ ExpressionAttributeValues: {
99
+ ':mid': { S: migrationId },
100
+ ':now': { N: String(Math.floor(Date.now() / 1000)) },
101
+ },
102
+ })
103
+ );
104
+ return true;
105
+ } catch {
106
+ return false; // ConditionalCheckFailedException — already locked
107
+ }
108
+ }
109
+
110
+ async release(ref: string, migrationId: string): Promise<void> {
111
+ try {
112
+ await this.dynamo.send(
113
+ new DeleteItemCommand({
114
+ TableName: this.locksTable,
115
+ Key: { ref: { S: ref } },
116
+ ConditionExpression: 'migrationId = :mid',
117
+ ExpressionAttributeValues: { ':mid': { S: migrationId } },
118
+ })
119
+ );
120
+ } catch {
121
+ // Idempotent — already released or never held
122
+ }
123
+ }
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { AwsCredentialStore } from './AwsCredentialStore';
2
+ export type { AwsCredentialStoreOptions } from './AwsCredentialStore';