@datacules/agent-identity-store-aws 0.11.0 → 0.11.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/LICENSE +109 -0
- package/dist/cjs/AwsCredentialStore.js +102 -0
- package/dist/cjs/AwsCredentialStore.js.map +1 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/AwsCredentialStore.js +98 -0
- package/dist/esm/AwsCredentialStore.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/AwsCredentialStore.d.ts +19 -0
- package/dist/types/AwsCredentialStore.d.ts.map +1 -0
- package/{src/index.ts → dist/types/index.d.ts} +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +31 -3
- package/src/AwsCredentialStore.ts +0 -124
- package/src/aws.test.ts +0 -196
package/LICENSE
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Datacules Agent Identity License — Version 1.0
|
|
2
|
+
Copyright (c) 2026 Datacules LLC. All rights reserved.
|
|
3
|
+
|
|
4
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
PREAMBLE
|
|
6
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
This software — Agent Identity & Auth Patterns — is developed and owned by
|
|
9
|
+
Datacules LLC. It is made available to the public as open-source software
|
|
10
|
+
under the permissive terms below.
|
|
11
|
+
|
|
12
|
+
Datacules LLC retains ownership and authorship of this software while
|
|
13
|
+
granting broad, royalty-free rights for anyone to use, copy, modify, and
|
|
14
|
+
distribute it — in commercial or non-commercial contexts — without requiring
|
|
15
|
+
that derivative works also become open source.
|
|
16
|
+
|
|
17
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
TERMS AND CONDITIONS
|
|
19
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
1. PERMISSION TO USE
|
|
22
|
+
|
|
23
|
+
Permission is hereby granted, free of charge, to any person or
|
|
24
|
+
organization obtaining a copy of this software and associated
|
|
25
|
+
documentation files (the "Software"), to use, copy, modify, merge,
|
|
26
|
+
publish, distribute, sublicense, and/or sell copies of the Software,
|
|
27
|
+
and to permit persons to whom the Software is furnished to do so,
|
|
28
|
+
subject to the conditions below.
|
|
29
|
+
|
|
30
|
+
2. ATTRIBUTION
|
|
31
|
+
|
|
32
|
+
a. Redistributions of source code must retain this copyright notice,
|
|
33
|
+
this list of conditions, and the disclaimer below.
|
|
34
|
+
|
|
35
|
+
b. Redistributions in binary form or as a product must reproduce this
|
|
36
|
+
copyright notice, this list of conditions, and the disclaimer in the
|
|
37
|
+
documentation and/or other materials provided with the distribution.
|
|
38
|
+
|
|
39
|
+
c. Neither the name "Datacules LLC" nor the names of its contributors
|
|
40
|
+
may be used to endorse or promote products derived from this Software
|
|
41
|
+
without prior written permission from Datacules LLC.
|
|
42
|
+
|
|
43
|
+
3. COMMERCIAL USE
|
|
44
|
+
|
|
45
|
+
Use of this Software in commercial products, SaaS platforms, internal
|
|
46
|
+
enterprise tools, or any revenue-generating context is explicitly
|
|
47
|
+
permitted without royalty, fee, or additional licensing agreement,
|
|
48
|
+
provided that the conditions in Section 2 (Attribution) are met.
|
|
49
|
+
|
|
50
|
+
4. NO COPYLEFT / NO VIRAL REQUIREMENT
|
|
51
|
+
|
|
52
|
+
This license does NOT require that derivative works, modifications,
|
|
53
|
+
or software that uses or embeds this Software be made open source.
|
|
54
|
+
You may incorporate this Software into proprietary or closed-source
|
|
55
|
+
products under your own license terms.
|
|
56
|
+
|
|
57
|
+
5. MODIFICATIONS
|
|
58
|
+
|
|
59
|
+
Modified versions of the Software may be distributed under the same
|
|
60
|
+
terms as this license or under any other permissive open-source
|
|
61
|
+
license (e.g. MIT, Apache 2.0, BSD), provided that:
|
|
62
|
+
|
|
63
|
+
a. The original copyright notice of Datacules LLC is preserved.
|
|
64
|
+
b. Modifications are clearly documented and distinguished from the
|
|
65
|
+
original work.
|
|
66
|
+
|
|
67
|
+
6. COMPATIBILITY
|
|
68
|
+
|
|
69
|
+
This license is compatible with other permissive open-source licenses
|
|
70
|
+
such as MIT, BSD 2-Clause, BSD 3-Clause, and Apache License 2.0. It
|
|
71
|
+
is also GPL-compatible — this Software may coexist with GPL-licensed
|
|
72
|
+
code, though this Software itself is not distributed under the GPL.
|
|
73
|
+
|
|
74
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
DISCLAIMER
|
|
76
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
THIS SOFTWARE IS PROVIDED BY DATACULES LLC AND CONTRIBUTORS "AS IS" AND
|
|
79
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
80
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
81
|
+
AND NON-INFRINGEMENT ARE DISCLAIMED.
|
|
82
|
+
|
|
83
|
+
IN NO EVENT SHALL DATACULES LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
84
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
85
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
86
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
87
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
88
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
89
|
+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
90
|
+
|
|
91
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
SUMMARY (non-binding)
|
|
93
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
✔ Use freely — commercial, proprietary, or open-source projects
|
|
96
|
+
✔ Modify and distribute with or without changes
|
|
97
|
+
✔ Sell products built on this Software
|
|
98
|
+
✔ No royalties or fees
|
|
99
|
+
✔ No requirement to open-source your own code
|
|
100
|
+
✔ Attribution to Datacules LLC required in source and binary distributions
|
|
101
|
+
✗ Do not use "Datacules LLC" to endorse derived products without permission
|
|
102
|
+
|
|
103
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
CONTACT
|
|
105
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
Datacules LLC
|
|
108
|
+
For licensing enquiries: legal@datacules.com
|
|
109
|
+
Product: https://github.com/hvrcharon1/agent-identity
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AwsCredentialStore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* AWS Secrets Manager + DynamoDB CredentialStore implementation.
|
|
6
|
+
*
|
|
7
|
+
* Secrets Manager holds the credential metadata JSON (id, kind, scope, status, ref).
|
|
8
|
+
* DynamoDB holds migration reservation locks (TTL-based, atomic conditional writes).
|
|
9
|
+
*
|
|
10
|
+
* Setup:
|
|
11
|
+
* 1. Store each Credential as a JSON string in Secrets Manager.
|
|
12
|
+
* Tag it with agent-identity-status: active|pending|revoked
|
|
13
|
+
* so listActive() can filter via the tag API.
|
|
14
|
+
* 2. Create a DynamoDB table named 'agent-identity-locks' with
|
|
15
|
+
* partition key 'ref' (String) and TTL attribute 'expiresAt' (Number).
|
|
16
|
+
* 3. Grant the IAM role: secretsmanager:GetSecretValue,
|
|
17
|
+
* secretsmanager:ListSecrets, dynamodb:PutItem, dynamodb:DeleteItem.
|
|
18
|
+
*/
|
|
19
|
+
const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
20
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
21
|
+
class AwsCredentialStore {
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
const config = options.region ? { region: options.region } : {};
|
|
24
|
+
this.sm = new client_secrets_manager_1.SecretsManagerClient(config);
|
|
25
|
+
this.dynamo = new client_dynamodb_1.DynamoDBClient(config);
|
|
26
|
+
this.locksTable = options.locksTable ?? 'agent-identity-locks';
|
|
27
|
+
}
|
|
28
|
+
async findByRef(ref) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await this.sm.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: ref }));
|
|
31
|
+
if (!res.SecretString)
|
|
32
|
+
return null;
|
|
33
|
+
const cred = JSON.parse(res.SecretString);
|
|
34
|
+
return cred.status === 'active' ? cred : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async listActive() {
|
|
41
|
+
const res = await this.sm.send(new client_secrets_manager_1.ListSecretsCommand({
|
|
42
|
+
Filters: [{ Key: 'tag-key', Values: ['agent-identity-status'] }],
|
|
43
|
+
}));
|
|
44
|
+
const results = [];
|
|
45
|
+
for (const s of res.SecretList ?? []) {
|
|
46
|
+
const tag = s.Tags?.find((t) => t.Key === 'agent-identity-status');
|
|
47
|
+
if (tag?.Value !== 'active')
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const cred = JSON.parse(s.Description ?? '{}');
|
|
51
|
+
results.push(cred);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// malformed description — skip
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
async listByKind(kind) {
|
|
60
|
+
const all = await this.listActive();
|
|
61
|
+
return all.filter((c) => c.kind === kind);
|
|
62
|
+
}
|
|
63
|
+
async reserve(ref, migrationId, ttlSeconds) {
|
|
64
|
+
const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
65
|
+
try {
|
|
66
|
+
await this.dynamo.send(new client_dynamodb_1.PutItemCommand({
|
|
67
|
+
TableName: this.locksTable,
|
|
68
|
+
Item: {
|
|
69
|
+
ref: { S: ref },
|
|
70
|
+
migrationId: { S: migrationId },
|
|
71
|
+
expiresAt: { N: String(expiresAt) },
|
|
72
|
+
},
|
|
73
|
+
// Succeed only if: no item exists, OR this migration already owns it, OR the TTL has expired
|
|
74
|
+
ConditionExpression: 'attribute_not_exists(#r) OR migrationId = :mid OR expiresAt < :now',
|
|
75
|
+
ExpressionAttributeNames: { '#r': 'ref' },
|
|
76
|
+
ExpressionAttributeValues: {
|
|
77
|
+
':mid': { S: migrationId },
|
|
78
|
+
':now': { N: String(Math.floor(Date.now() / 1000)) },
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false; // ConditionalCheckFailedException — already locked
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async release(ref, migrationId) {
|
|
88
|
+
try {
|
|
89
|
+
await this.dynamo.send(new client_dynamodb_1.DeleteItemCommand({
|
|
90
|
+
TableName: this.locksTable,
|
|
91
|
+
Key: { ref: { S: ref } },
|
|
92
|
+
ConditionExpression: 'migrationId = :mid',
|
|
93
|
+
ExpressionAttributeValues: { ':mid': { S: migrationId } },
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Idempotent — already released or never held
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.AwsCredentialStore = AwsCredentialStore;
|
|
102
|
+
//# sourceMappingURL=AwsCredentialStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AwsCredentialStore.js","sourceRoot":"","sources":["../../src/AwsCredentialStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AwsCredentialStore = void 0;
|
|
4
|
+
var AwsCredentialStore_1 = require("./AwsCredentialStore");
|
|
5
|
+
Object.defineProperty(exports, "AwsCredentialStore", { enumerable: true, get: function () { return AwsCredentialStore_1.AwsCredentialStore; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,2DAA0D;AAAjD,wHAAA,kBAAkB,OAAA"}
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { SecretsManagerClient, GetSecretValueCommand, ListSecretsCommand, } from '@aws-sdk/client-secrets-manager';
|
|
17
|
+
import { DynamoDBClient, PutItemCommand, DeleteItemCommand, } from '@aws-sdk/client-dynamodb';
|
|
18
|
+
export class AwsCredentialStore {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
const config = options.region ? { region: options.region } : {};
|
|
21
|
+
this.sm = new SecretsManagerClient(config);
|
|
22
|
+
this.dynamo = new DynamoDBClient(config);
|
|
23
|
+
this.locksTable = options.locksTable ?? 'agent-identity-locks';
|
|
24
|
+
}
|
|
25
|
+
async findByRef(ref) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await this.sm.send(new GetSecretValueCommand({ SecretId: ref }));
|
|
28
|
+
if (!res.SecretString)
|
|
29
|
+
return null;
|
|
30
|
+
const cred = JSON.parse(res.SecretString);
|
|
31
|
+
return cred.status === 'active' ? cred : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async listActive() {
|
|
38
|
+
const res = await this.sm.send(new ListSecretsCommand({
|
|
39
|
+
Filters: [{ Key: 'tag-key', Values: ['agent-identity-status'] }],
|
|
40
|
+
}));
|
|
41
|
+
const results = [];
|
|
42
|
+
for (const s of res.SecretList ?? []) {
|
|
43
|
+
const tag = s.Tags?.find((t) => t.Key === 'agent-identity-status');
|
|
44
|
+
if (tag?.Value !== 'active')
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
const cred = JSON.parse(s.Description ?? '{}');
|
|
48
|
+
results.push(cred);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// malformed description — skip
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
async listByKind(kind) {
|
|
57
|
+
const all = await this.listActive();
|
|
58
|
+
return all.filter((c) => c.kind === kind);
|
|
59
|
+
}
|
|
60
|
+
async reserve(ref, migrationId, ttlSeconds) {
|
|
61
|
+
const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
62
|
+
try {
|
|
63
|
+
await this.dynamo.send(new PutItemCommand({
|
|
64
|
+
TableName: this.locksTable,
|
|
65
|
+
Item: {
|
|
66
|
+
ref: { S: ref },
|
|
67
|
+
migrationId: { S: migrationId },
|
|
68
|
+
expiresAt: { N: String(expiresAt) },
|
|
69
|
+
},
|
|
70
|
+
// Succeed only if: no item exists, OR this migration already owns it, OR the TTL has expired
|
|
71
|
+
ConditionExpression: 'attribute_not_exists(#r) OR migrationId = :mid OR expiresAt < :now',
|
|
72
|
+
ExpressionAttributeNames: { '#r': 'ref' },
|
|
73
|
+
ExpressionAttributeValues: {
|
|
74
|
+
':mid': { S: migrationId },
|
|
75
|
+
':now': { N: String(Math.floor(Date.now() / 1000)) },
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false; // ConditionalCheckFailedException — already locked
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async release(ref, migrationId) {
|
|
85
|
+
try {
|
|
86
|
+
await this.dynamo.send(new DeleteItemCommand({
|
|
87
|
+
TableName: this.locksTable,
|
|
88
|
+
Key: { ref: { S: ref } },
|
|
89
|
+
ConditionExpression: 'migrationId = :mid',
|
|
90
|
+
ExpressionAttributeValues: { ':mid': { S: migrationId } },
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Idempotent — already released or never held
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=AwsCredentialStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AwsCredentialStore.js","sourceRoot":"","sources":["../../src/AwsCredentialStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Credential, CredentialKind, CredentialStore } from '@datacules/agent-identity';
|
|
2
|
+
export interface AwsCredentialStoreOptions {
|
|
3
|
+
/** AWS region (default: reads AWS_REGION env var) */
|
|
4
|
+
region?: string;
|
|
5
|
+
/** DynamoDB table name for migration locks (default: 'agent-identity-locks') */
|
|
6
|
+
locksTable?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class AwsCredentialStore implements CredentialStore {
|
|
9
|
+
private readonly sm;
|
|
10
|
+
private readonly dynamo;
|
|
11
|
+
private readonly locksTable;
|
|
12
|
+
constructor(options?: AwsCredentialStoreOptions);
|
|
13
|
+
findByRef(ref: string): Promise<Credential | null>;
|
|
14
|
+
listActive(): Promise<Credential[]>;
|
|
15
|
+
listByKind(kind: CredentialKind): Promise<Credential[]>;
|
|
16
|
+
reserve(ref: string, migrationId: string, ttlSeconds: number): Promise<boolean>;
|
|
17
|
+
release(ref: string, migrationId: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=AwsCredentialStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AwsCredentialStore.d.ts","sourceRoot":"","sources":["../../src/AwsCredentialStore.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE7F,MAAM,WAAW,yBAAyB;IACxC,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,kBAAmB,YAAW,eAAe;IACxD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,OAAO,GAAE,yBAA8B;IAO7C,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAWlD,UAAU,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAoBnC,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAKvD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2B/E,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAc/D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datacules/agent-identity-store-aws",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "AWS Secrets Manager + DynamoDB credential store for @datacules/agent-identity",
|
|
6
|
+
"author": "Datacules LLC",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/hvrcharon1/agent-identity.git",
|
|
11
|
+
"directory": "packages/stores/aws"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent-identity",
|
|
15
|
+
"aws",
|
|
16
|
+
"secrets-manager",
|
|
17
|
+
"dynamodb",
|
|
18
|
+
"credential-store",
|
|
19
|
+
"ai-agents",
|
|
20
|
+
"datacules"
|
|
21
|
+
],
|
|
6
22
|
"main": "./dist/cjs/index.js",
|
|
7
23
|
"module": "./dist/esm/index.js",
|
|
8
24
|
"types": "./dist/types/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/esm/index.js",
|
|
28
|
+
"require": "./dist/cjs/index.js",
|
|
29
|
+
"types": "./dist/types/index.d.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
9
37
|
"scripts": {
|
|
10
|
-
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json",
|
|
11
39
|
"type-check": "tsc --noEmit"
|
|
12
40
|
},
|
|
13
41
|
"dependencies": {
|
|
@@ -15,7 +43,7 @@
|
|
|
15
43
|
"@aws-sdk/client-dynamodb": "^3.600.0"
|
|
16
44
|
},
|
|
17
45
|
"peerDependencies": {
|
|
18
|
-
"@datacules/agent-identity": "^0.
|
|
46
|
+
"@datacules/agent-identity": "^0.11.1"
|
|
19
47
|
},
|
|
20
48
|
"devDependencies": {
|
|
21
49
|
"@datacules/agent-identity": "*",
|
|
@@ -1,124 +0,0 @@
|
|
|
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/aws.test.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
// Mock AWS SDK modules — constructors return objects with vi.fn() send methods.
|
|
4
|
-
// vi.mock() calls are hoisted before imports by Vitest.
|
|
5
|
-
vi.mock('@aws-sdk/client-secrets-manager', () => ({
|
|
6
|
-
SecretsManagerClient: vi.fn(() => ({ send: vi.fn() })),
|
|
7
|
-
GetSecretValueCommand: vi.fn((input: unknown) => input),
|
|
8
|
-
ListSecretsCommand: vi.fn((input: unknown) => input),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock('@aws-sdk/client-dynamodb', () => ({
|
|
12
|
-
DynamoDBClient: vi.fn(() => ({ send: vi.fn() })),
|
|
13
|
-
PutItemCommand: vi.fn((input: unknown) => input),
|
|
14
|
-
DeleteItemCommand: vi.fn((input: unknown) => input),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
import { AwsCredentialStore } from './index.js';
|
|
18
|
-
import type { Credential } from '@datacules/agent-identity';
|
|
19
|
-
|
|
20
|
-
const makeCred = (overrides: Partial<Credential> = {}): Credential => ({
|
|
21
|
-
id: 'cred-openai',
|
|
22
|
-
kind: 'fixed',
|
|
23
|
-
name: 'OpenAI API Key',
|
|
24
|
-
scope: 'global',
|
|
25
|
-
status: 'active',
|
|
26
|
-
provider: 'openai',
|
|
27
|
-
ref: 'openai-prod-slot',
|
|
28
|
-
...overrides,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('AwsCredentialStore', () => {
|
|
32
|
-
let store: AwsCredentialStore;
|
|
33
|
-
let smSend: ReturnType<typeof vi.fn>;
|
|
34
|
-
let dynamoSend: ReturnType<typeof vi.fn>;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
vi.clearAllMocks();
|
|
38
|
-
store = new AwsCredentialStore({ region: 'us-east-1', locksTable: 'test-locks' });
|
|
39
|
-
// Access mock send functions injected by the mocked constructors
|
|
40
|
-
smSend = (store as any).sm.send as ReturnType<typeof vi.fn>;
|
|
41
|
-
dynamoSend = (store as any).dynamo.send as ReturnType<typeof vi.fn>;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// ─── findByRef() ────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
describe('findByRef()', () => {
|
|
47
|
-
it('returns active credential when SM returns active SecretString', async () => {
|
|
48
|
-
const cred = makeCred();
|
|
49
|
-
smSend.mockResolvedValue({ SecretString: JSON.stringify(cred) });
|
|
50
|
-
const result = await store.findByRef('openai-prod-slot');
|
|
51
|
-
expect(result).toEqual(cred);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('returns null when credential status is not active', async () => {
|
|
55
|
-
const cred = makeCred({ status: 'pending' });
|
|
56
|
-
smSend.mockResolvedValue({ SecretString: JSON.stringify(cred) });
|
|
57
|
-
expect(await store.findByRef('openai-prod-slot')).toBeNull();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('returns null when SecretString is absent on the SM response', async () => {
|
|
61
|
-
smSend.mockResolvedValue({ SecretString: undefined });
|
|
62
|
-
expect(await store.findByRef('openai-prod-slot')).toBeNull();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('returns null without throwing when SM send throws', async () => {
|
|
66
|
-
smSend.mockRejectedValue(new Error('ResourceNotFoundException'));
|
|
67
|
-
expect(await store.findByRef('missing-ref')).toBeNull();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('sends GetSecretValueCommand with the correct SecretId', async () => {
|
|
71
|
-
const cred = makeCred();
|
|
72
|
-
smSend.mockResolvedValue({ SecretString: JSON.stringify(cred) });
|
|
73
|
-
await store.findByRef('my-secret-ref');
|
|
74
|
-
expect(smSend).toHaveBeenCalledWith(
|
|
75
|
-
expect.objectContaining({ SecretId: 'my-secret-ref' })
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// ─── listActive() ───────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
describe('listActive()', () => {
|
|
83
|
-
it('returns credentials where tag value is active, parsed from Description', async () => {
|
|
84
|
-
const cred = makeCred();
|
|
85
|
-
smSend.mockResolvedValue({
|
|
86
|
-
SecretList: [
|
|
87
|
-
{
|
|
88
|
-
Tags: [{ Key: 'agent-identity-status', Value: 'active' }],
|
|
89
|
-
Description: JSON.stringify(cred),
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
});
|
|
93
|
-
const results = await store.listActive();
|
|
94
|
-
expect(results).toHaveLength(1);
|
|
95
|
-
expect(results[0]).toEqual(cred);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('skips secrets where the agent-identity-status tag value is not active', async () => {
|
|
99
|
-
smSend.mockResolvedValue({
|
|
100
|
-
SecretList: [
|
|
101
|
-
{
|
|
102
|
-
Tags: [{ Key: 'agent-identity-status', Value: 'revoked' }],
|
|
103
|
-
Description: JSON.stringify(makeCred({ status: 'revoked' })),
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
});
|
|
107
|
-
expect(await store.listActive()).toHaveLength(0);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('returns empty array when SecretList is undefined', async () => {
|
|
111
|
-
smSend.mockResolvedValue({ SecretList: undefined });
|
|
112
|
-
expect(await store.listActive()).toEqual([]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('skips secrets with malformed Description JSON without throwing', async () => {
|
|
116
|
-
smSend.mockResolvedValue({
|
|
117
|
-
SecretList: [
|
|
118
|
-
{
|
|
119
|
-
Tags: [{ Key: 'agent-identity-status', Value: 'active' }],
|
|
120
|
-
Description: 'not-valid-json',
|
|
121
|
-
},
|
|
122
|
-
],
|
|
123
|
-
});
|
|
124
|
-
expect(await store.listActive()).toHaveLength(0);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ─── listByKind() ───────────────────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
describe('listByKind()', () => {
|
|
131
|
-
it('returns only credentials matching the requested kind', async () => {
|
|
132
|
-
const fixed = makeCred({ kind: 'fixed', id: 'cred-fixed' });
|
|
133
|
-
const delegated = makeCred({ kind: 'user-delegated', id: 'cred-user', ref: 'user-slot' });
|
|
134
|
-
smSend.mockResolvedValue({
|
|
135
|
-
SecretList: [
|
|
136
|
-
{ Tags: [{ Key: 'agent-identity-status', Value: 'active' }], Description: JSON.stringify(fixed) },
|
|
137
|
-
{ Tags: [{ Key: 'agent-identity-status', Value: 'active' }], Description: JSON.stringify(delegated) },
|
|
138
|
-
],
|
|
139
|
-
});
|
|
140
|
-
const result = await store.listByKind('fixed');
|
|
141
|
-
expect(result).toHaveLength(1);
|
|
142
|
-
expect(result[0].kind).toBe('fixed');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('returns empty array when no credentials match the requested kind', async () => {
|
|
146
|
-
const fixed = makeCred({ kind: 'fixed' });
|
|
147
|
-
smSend.mockResolvedValue({
|
|
148
|
-
SecretList: [
|
|
149
|
-
{ Tags: [{ Key: 'agent-identity-status', Value: 'active' }], Description: JSON.stringify(fixed) },
|
|
150
|
-
],
|
|
151
|
-
});
|
|
152
|
-
expect(await store.listByKind('user-delegated')).toHaveLength(0);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// ─── reserve() ──────────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
describe('reserve()', () => {
|
|
159
|
-
it('returns true when DynamoDB PutItem succeeds (no conflicting lock)', async () => {
|
|
160
|
-
dynamoSend.mockResolvedValue({});
|
|
161
|
-
expect(await store.reserve('cred-ref', 'migration-1', 300)).toBe(true);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('returns false when DynamoDB throws ConditionalCheckFailedException', async () => {
|
|
165
|
-
dynamoSend.mockRejectedValue(new Error('ConditionalCheckFailedException'));
|
|
166
|
-
expect(await store.reserve('cred-ref', 'migration-2', 300)).toBe(false);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('sends PutItemCommand to the configured locksTable name', async () => {
|
|
170
|
-
dynamoSend.mockResolvedValue({});
|
|
171
|
-
await store.reserve('my-ref', 'mig-id', 600);
|
|
172
|
-
expect(dynamoSend).toHaveBeenCalledWith(
|
|
173
|
-
expect.objectContaining({ TableName: 'test-locks' })
|
|
174
|
-
);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ─── release() ──────────────────────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
describe('release()', () => {
|
|
181
|
-
it('issues a DeleteItemCommand with the correct ref key', async () => {
|
|
182
|
-
dynamoSend.mockResolvedValue({});
|
|
183
|
-
await store.release('cred-ref', 'migration-1');
|
|
184
|
-
expect(dynamoSend).toHaveBeenCalledWith(
|
|
185
|
-
expect.objectContaining({
|
|
186
|
-
Key: { ref: { S: 'cred-ref' } },
|
|
187
|
-
})
|
|
188
|
-
);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('resolves without throwing when DeleteItem throws (idempotent release)', async () => {
|
|
192
|
-
dynamoSend.mockRejectedValue(new Error('ConditionalCheckFailedException'));
|
|
193
|
-
await expect(store.release('cred-ref', 'migration-x')).resolves.toBeUndefined();
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|