@datacules/agent-identity-store-azure 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/AzureKeyVaultCredentialStore.js +148 -0
- package/dist/cjs/AzureKeyVaultCredentialStore.js.map +1 -0
- package/dist/cjs/azure.test.js +196 -0
- package/dist/cjs/azure.test.js.map +1 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/AzureKeyVaultCredentialStore.js +148 -0
- package/dist/esm/AzureKeyVaultCredentialStore.js.map +1 -0
- package/dist/esm/azure.test.js +196 -0
- package/dist/esm/azure.test.js.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/AzureKeyVaultCredentialStore.d.ts +31 -0
- package/dist/types/AzureKeyVaultCredentialStore.d.ts.map +1 -0
- package/dist/types/azure.test.d.ts +2 -0
- package/dist/types/azure.test.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 +19 -3
- package/src/AzureKeyVaultCredentialStore.ts +0 -184
- package/src/azure.test.ts +0 -247
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
// Mock Azure SDK modules — constructors return objects with vi.fn() methods.
|
|
5
|
+
// vi.mock() calls are hoisted before imports by Vitest.
|
|
6
|
+
vitest_1.vi.mock('@azure/identity', () => ({
|
|
7
|
+
DefaultAzureCredential: vitest_1.vi.fn(() => ({})),
|
|
8
|
+
}));
|
|
9
|
+
vitest_1.vi.mock('@azure/keyvault-secrets', () => ({
|
|
10
|
+
SecretClient: vitest_1.vi.fn(() => ({
|
|
11
|
+
getSecret: vitest_1.vi.fn(),
|
|
12
|
+
listPropertiesOfSecrets: vitest_1.vi.fn(),
|
|
13
|
+
})),
|
|
14
|
+
}));
|
|
15
|
+
vitest_1.vi.mock('@azure/data-tables', () => ({
|
|
16
|
+
TableClient: vitest_1.vi.fn(() => ({
|
|
17
|
+
getEntity: vitest_1.vi.fn(),
|
|
18
|
+
upsertEntity: vitest_1.vi.fn(),
|
|
19
|
+
deleteEntity: vitest_1.vi.fn(),
|
|
20
|
+
})),
|
|
21
|
+
odata: vitest_1.vi.fn((s) => s),
|
|
22
|
+
}));
|
|
23
|
+
const index_js_1 = require("./index.js");
|
|
24
|
+
const makeCred = (overrides = {}) => ({
|
|
25
|
+
id: 'cred-openai',
|
|
26
|
+
kind: 'fixed',
|
|
27
|
+
name: 'OpenAI Key',
|
|
28
|
+
scope: 'global',
|
|
29
|
+
status: 'active',
|
|
30
|
+
provider: 'openai',
|
|
31
|
+
ref: 'openai-prod-slot',
|
|
32
|
+
...overrides,
|
|
33
|
+
});
|
|
34
|
+
// Minimal secret response shape returned by SecretClient.getSecret()
|
|
35
|
+
const makeSecretResponse = (cred) => ({
|
|
36
|
+
value: JSON.stringify(cred),
|
|
37
|
+
properties: { contentType: cred.status },
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.describe)('AzureKeyVaultCredentialStore', () => {
|
|
40
|
+
let store;
|
|
41
|
+
let secretsMock;
|
|
42
|
+
let tableMock;
|
|
43
|
+
(0, vitest_1.beforeEach)(() => {
|
|
44
|
+
vitest_1.vi.clearAllMocks();
|
|
45
|
+
store = new index_js_1.AzureKeyVaultCredentialStore({
|
|
46
|
+
keyVaultUrl: 'https://test.vault.azure.net',
|
|
47
|
+
tablesEndpoint: 'https://test.table.core.windows.net',
|
|
48
|
+
});
|
|
49
|
+
// Access mock clients injected by the mocked SDK constructors
|
|
50
|
+
secretsMock = store.secrets;
|
|
51
|
+
tableMock = store.table;
|
|
52
|
+
});
|
|
53
|
+
// ─── constructor ────────────────────────────────────────────────────────────
|
|
54
|
+
(0, vitest_1.describe)('constructor', () => {
|
|
55
|
+
(0, vitest_1.it)('throws when keyVaultUrl is missing from both options and environment', () => {
|
|
56
|
+
delete process.env['AZURE_KEYVAULT_URL'];
|
|
57
|
+
(0, vitest_1.expect)(() => new index_js_1.AzureKeyVaultCredentialStore({
|
|
58
|
+
tablesEndpoint: 'https://test.table.core.windows.net',
|
|
59
|
+
})).toThrow('keyVaultUrl is required');
|
|
60
|
+
});
|
|
61
|
+
(0, vitest_1.it)('throws when tablesEndpoint is missing from both options and environment', () => {
|
|
62
|
+
delete process.env['AZURE_TABLES_ENDPOINT'];
|
|
63
|
+
(0, vitest_1.expect)(() => new index_js_1.AzureKeyVaultCredentialStore({
|
|
64
|
+
keyVaultUrl: 'https://test.vault.azure.net',
|
|
65
|
+
})).toThrow('tablesEndpoint is required');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
// ─── findByRef() ────────────────────────────────────────────────────────────
|
|
69
|
+
(0, vitest_1.describe)('findByRef()', () => {
|
|
70
|
+
(0, vitest_1.it)('returns active credential when Key Vault returns a secret with contentType=active', async () => {
|
|
71
|
+
const cred = makeCred();
|
|
72
|
+
secretsMock.getSecret.mockResolvedValue(makeSecretResponse(cred));
|
|
73
|
+
(0, vitest_1.expect)(await store.findByRef('openai-prod-slot')).toEqual(cred);
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('returns null when contentType is not active (does not parse the value)', async () => {
|
|
76
|
+
const cred = makeCred({ status: 'pending' });
|
|
77
|
+
secretsMock.getSecret.mockResolvedValue({
|
|
78
|
+
value: JSON.stringify(cred),
|
|
79
|
+
properties: { contentType: 'pending' },
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.expect)(await store.findByRef('openai-prod-slot')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
(0, vitest_1.it)('returns null when secret value is undefined', async () => {
|
|
84
|
+
secretsMock.getSecret.mockResolvedValue({
|
|
85
|
+
value: undefined,
|
|
86
|
+
properties: { contentType: 'active' },
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.expect)(await store.findByRef('openai-prod-slot')).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
(0, vitest_1.it)('returns null without throwing when getSecret throws', async () => {
|
|
91
|
+
secretsMock.getSecret.mockRejectedValue(new Error('SecretNotFound'));
|
|
92
|
+
(0, vitest_1.expect)(await store.findByRef('missing-ref')).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
// ─── listActive() ───────────────────────────────────────────────────────────
|
|
96
|
+
(0, vitest_1.describe)('listActive()', () => {
|
|
97
|
+
(0, vitest_1.it)('returns active credentials iterated from listPropertiesOfSecrets and fetched via getSecret', async () => {
|
|
98
|
+
const cred = makeCred();
|
|
99
|
+
secretsMock.listPropertiesOfSecrets.mockReturnValue((async function* () {
|
|
100
|
+
yield { contentType: 'active', enabled: true, name: 'openai-prod-slot' };
|
|
101
|
+
})());
|
|
102
|
+
secretsMock.getSecret.mockResolvedValue(makeSecretResponse(cred));
|
|
103
|
+
const results = await store.listActive();
|
|
104
|
+
(0, vitest_1.expect)(results).toHaveLength(1);
|
|
105
|
+
(0, vitest_1.expect)(results[0]).toEqual(cred);
|
|
106
|
+
});
|
|
107
|
+
(0, vitest_1.it)('skips secrets with contentType !== active without calling getSecret', async () => {
|
|
108
|
+
secretsMock.listPropertiesOfSecrets.mockReturnValue((async function* () {
|
|
109
|
+
yield { contentType: 'pending', enabled: true, name: 'pending-slot' };
|
|
110
|
+
})());
|
|
111
|
+
(0, vitest_1.expect)(await store.listActive()).toHaveLength(0);
|
|
112
|
+
(0, vitest_1.expect)(secretsMock.getSecret).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)('skips secrets with enabled=false', async () => {
|
|
115
|
+
secretsMock.listPropertiesOfSecrets.mockReturnValue((async function* () {
|
|
116
|
+
yield { contentType: 'active', enabled: false, name: 'disabled-slot' };
|
|
117
|
+
})());
|
|
118
|
+
(0, vitest_1.expect)(await store.listActive()).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
(0, vitest_1.it)('returns empty array when listPropertiesOfSecrets throws (Key Vault unreachable)', async () => {
|
|
121
|
+
secretsMock.listPropertiesOfSecrets.mockReturnValue(
|
|
122
|
+
// eslint-disable-next-line require-yield
|
|
123
|
+
(async function* () {
|
|
124
|
+
throw new Error('KeyVaultUnavailable');
|
|
125
|
+
})());
|
|
126
|
+
(0, vitest_1.expect)(await store.listActive()).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// ─── listByKind() ───────────────────────────────────────────────────────────
|
|
130
|
+
(0, vitest_1.describe)('listByKind()', () => {
|
|
131
|
+
(0, vitest_1.it)('returns only credentials matching the requested kind', async () => {
|
|
132
|
+
const fixed = makeCred({ kind: 'fixed', id: 'cred-fixed', ref: 'fixed-slot' });
|
|
133
|
+
const delegated = makeCred({ kind: 'user-delegated', id: 'cred-user', ref: 'user-slot' });
|
|
134
|
+
secretsMock.listPropertiesOfSecrets.mockReturnValue((async function* () {
|
|
135
|
+
yield { contentType: 'active', enabled: true, name: 'fixed-slot' };
|
|
136
|
+
yield { contentType: 'active', enabled: true, name: 'user-slot' };
|
|
137
|
+
})());
|
|
138
|
+
secretsMock.getSecret
|
|
139
|
+
.mockResolvedValueOnce(makeSecretResponse(fixed))
|
|
140
|
+
.mockResolvedValueOnce(makeSecretResponse(delegated));
|
|
141
|
+
const result = await store.listByKind('fixed');
|
|
142
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
143
|
+
(0, vitest_1.expect)(result[0].kind).toBe('fixed');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// ─── reserve() ──────────────────────────────────────────────────────────────
|
|
147
|
+
(0, vitest_1.describe)('reserve()', () => {
|
|
148
|
+
(0, vitest_1.it)('returns true when no existing lock is found (getEntity throws) and upsert succeeds', async () => {
|
|
149
|
+
tableMock.getEntity.mockRejectedValue(new Error('EntityNotFound'));
|
|
150
|
+
tableMock.upsertEntity.mockResolvedValue({});
|
|
151
|
+
(0, vitest_1.expect)(await store.reserve('cred-ref', 'mig-1', 300)).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
(0, vitest_1.it)('returns false when a different migration holds an unexpired lock', async () => {
|
|
154
|
+
const futureExpiry = Math.floor(Date.now() / 1000) + 600;
|
|
155
|
+
tableMock.getEntity.mockResolvedValue({
|
|
156
|
+
migrationId: 'other-migration',
|
|
157
|
+
expiresAt: futureExpiry,
|
|
158
|
+
});
|
|
159
|
+
(0, vitest_1.expect)(await store.reserve('cred-ref', 'mig-2', 300)).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
(0, vitest_1.it)('returns true when the same migration re-acquires its own lock', async () => {
|
|
162
|
+
const futureExpiry = Math.floor(Date.now() / 1000) + 600;
|
|
163
|
+
tableMock.getEntity.mockResolvedValue({
|
|
164
|
+
migrationId: 'mig-1',
|
|
165
|
+
expiresAt: futureExpiry,
|
|
166
|
+
});
|
|
167
|
+
tableMock.upsertEntity.mockResolvedValue({});
|
|
168
|
+
(0, vitest_1.expect)(await store.reserve('cred-ref', 'mig-1', 300)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
// ─── release() ──────────────────────────────────────────────────────────────
|
|
172
|
+
(0, vitest_1.describe)('release()', () => {
|
|
173
|
+
(0, vitest_1.it)('deletes the entity when migrationId matches the stored lock owner', async () => {
|
|
174
|
+
tableMock.getEntity.mockResolvedValue({
|
|
175
|
+
migrationId: 'mig-1',
|
|
176
|
+
expiresAt: Math.floor(Date.now() / 1000) + 600,
|
|
177
|
+
});
|
|
178
|
+
tableMock.deleteEntity.mockResolvedValue({});
|
|
179
|
+
await store.release('cred-ref', 'mig-1');
|
|
180
|
+
(0, vitest_1.expect)(tableMock.deleteEntity).toHaveBeenCalledWith('lock', 'cred-ref');
|
|
181
|
+
});
|
|
182
|
+
(0, vitest_1.it)('does not call deleteEntity when migrationId does not match the stored lock', async () => {
|
|
183
|
+
tableMock.getEntity.mockResolvedValue({
|
|
184
|
+
migrationId: 'other-mig',
|
|
185
|
+
expiresAt: Math.floor(Date.now() / 1000) + 600,
|
|
186
|
+
});
|
|
187
|
+
await store.release('cred-ref', 'mig-1');
|
|
188
|
+
(0, vitest_1.expect)(tableMock.deleteEntity).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
(0, vitest_1.it)('resolves without throwing when getEntity throws (lock already released)', async () => {
|
|
191
|
+
tableMock.getEntity.mockRejectedValue(new Error('EntityNotFound'));
|
|
192
|
+
await (0, vitest_1.expect)(store.release('cred-ref', 'mig-1')).resolves.toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=azure.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure.test.js","sourceRoot":"","sources":["../../src/azure.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AzureKeyVaultCredentialStore = void 0;
|
|
4
|
+
var AzureKeyVaultCredentialStore_1 = require("./AzureKeyVaultCredentialStore");
|
|
5
|
+
Object.defineProperty(exports, "AzureKeyVaultCredentialStore", { enumerable: true, get: function () { return AzureKeyVaultCredentialStore_1.AzureKeyVaultCredentialStore; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Credential, CredentialKind, CredentialStore } from '@datacules/agent-identity';
|
|
2
|
+
export interface AzureKeyVaultCredentialStoreOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Full URL of the Azure Key Vault, e.g. https://my-vault.vault.azure.net
|
|
5
|
+
* Falls back to AZURE_KEYVAULT_URL environment variable.
|
|
6
|
+
*/
|
|
7
|
+
keyVaultUrl?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Full URL of the Azure Table Storage endpoint,
|
|
10
|
+
* e.g. https://myaccount.table.core.windows.net
|
|
11
|
+
* Falls back to AZURE_TABLES_ENDPOINT environment variable.
|
|
12
|
+
*/
|
|
13
|
+
tablesEndpoint?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Name of the Table Storage table used for migration locks.
|
|
16
|
+
* Default: 'agentidentitylocks'
|
|
17
|
+
* Note: Azure Table names may not contain hyphens.
|
|
18
|
+
*/
|
|
19
|
+
locksTable?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class AzureKeyVaultCredentialStore implements CredentialStore {
|
|
22
|
+
private readonly secrets;
|
|
23
|
+
private readonly table;
|
|
24
|
+
constructor(options?: AzureKeyVaultCredentialStoreOptions);
|
|
25
|
+
findByRef(ref: string): Promise<Credential | null>;
|
|
26
|
+
listActive(): Promise<Credential[]>;
|
|
27
|
+
listByKind(kind: CredentialKind): Promise<Credential[]>;
|
|
28
|
+
reserve(ref: string, migrationId: string, ttlSeconds: number): Promise<boolean>;
|
|
29
|
+
release(ref: string, migrationId: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=AzureKeyVaultCredentialStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AzureKeyVaultCredentialStore.d.ts","sourceRoot":"","sources":["../../src/AzureKeyVaultCredentialStore.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE7F,MAAM,WAAW,mCAAmC;IAClD;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AASD,qBAAa,4BAA6B,YAAW,eAAe;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;gBAExB,OAAO,GAAE,mCAAwC;IA4BvD,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAalD,UAAU,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAwBnC,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAOvD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkC/E,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAU/D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure.test.d.ts","sourceRoot":"","sources":["../../src/azure.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,YAAY,EAAE,mCAAmC,EAAE,MAAM,gCAAgC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datacules/agent-identity-store-azure",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Azure Key Vault + Table Storage 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/azure"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent-identity",
|
|
15
|
+
"azure",
|
|
16
|
+
"key-vault",
|
|
17
|
+
"credential-store",
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"datacules"
|
|
20
|
+
],
|
|
6
21
|
"main": "./dist/cjs/index.js",
|
|
7
22
|
"module": "./dist/esm/index.js",
|
|
8
23
|
"types": "./dist/types/index.d.ts",
|
|
@@ -23,7 +38,7 @@
|
|
|
23
38
|
"@azure/identity": "^4.3.0"
|
|
24
39
|
},
|
|
25
40
|
"peerDependencies": {
|
|
26
|
-
"@datacules/agent-identity": "^0.
|
|
41
|
+
"@datacules/agent-identity": "^0.11.1"
|
|
27
42
|
},
|
|
28
43
|
"devDependencies": {
|
|
29
44
|
"@datacules/agent-identity": "*",
|
|
@@ -31,6 +46,7 @@
|
|
|
31
46
|
},
|
|
32
47
|
"files": [
|
|
33
48
|
"dist",
|
|
34
|
-
"
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
35
51
|
]
|
|
36
52
|
}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Azure Key Vault + Azure Table Storage CredentialStore implementation.
|
|
3
|
-
*
|
|
4
|
-
* Key Vault holds each credential as a secret whose name is the credential ref.
|
|
5
|
-
* The secret value is the JSON-serialised Credential object.
|
|
6
|
-
* The secret's "content-type" tag carries the status (active | pending | revoked)
|
|
7
|
-
* so listActive() can skip inactive secrets without fetching their values.
|
|
8
|
-
*
|
|
9
|
-
* Table Storage holds migration reservation locks.
|
|
10
|
-
* Table name: agent-identity-locks
|
|
11
|
-
* Partition key: "lock" (constant — all locks in one partition for simplicity)
|
|
12
|
-
* Row key: the credential ref being locked
|
|
13
|
-
* Columns: migrationId (string), expiresAt (number — Unix epoch seconds)
|
|
14
|
-
*
|
|
15
|
-
* Azure setup:
|
|
16
|
-
* 1. Create an Azure Key Vault and store each Credential as a JSON secret.
|
|
17
|
-
* Set the secret's ContentType to "active", "pending", or "revoked".
|
|
18
|
-
* 2. Create a Storage Account and a Table named "agentidentitylocks".
|
|
19
|
-
* (Table names may not contain hyphens — use "agentidentitylocks".)
|
|
20
|
-
* 3. Grant the running identity:
|
|
21
|
-
* Key Vault Secrets User (read secrets)
|
|
22
|
-
* Key Vault Secrets Officer (only needed for write operations)
|
|
23
|
-
* Storage Table Data Contributor (read + write locks table)
|
|
24
|
-
* 4. Set AZURE_KEYVAULT_URL and AZURE_TABLES_ENDPOINT environment variables,
|
|
25
|
-
* or pass them as constructor options.
|
|
26
|
-
* DefaultAzureCredential resolves auth automatically from:
|
|
27
|
-
* - Managed Identity (production)
|
|
28
|
-
* - AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET env vars
|
|
29
|
-
* - Azure CLI (local development)
|
|
30
|
-
* - Workload Identity (AKS)
|
|
31
|
-
*/
|
|
32
|
-
import { SecretClient } from '@azure/keyvault-secrets';
|
|
33
|
-
import { TableClient, odata } from '@azure/data-tables';
|
|
34
|
-
import { DefaultAzureCredential } from '@azure/identity';
|
|
35
|
-
import type { Credential, CredentialKind, CredentialStore } from '@datacules/agent-identity';
|
|
36
|
-
|
|
37
|
-
export interface AzureKeyVaultCredentialStoreOptions {
|
|
38
|
-
/**
|
|
39
|
-
* Full URL of the Azure Key Vault, e.g. https://my-vault.vault.azure.net
|
|
40
|
-
* Falls back to AZURE_KEYVAULT_URL environment variable.
|
|
41
|
-
*/
|
|
42
|
-
keyVaultUrl?: string;
|
|
43
|
-
/**
|
|
44
|
-
* Full URL of the Azure Table Storage endpoint,
|
|
45
|
-
* e.g. https://myaccount.table.core.windows.net
|
|
46
|
-
* Falls back to AZURE_TABLES_ENDPOINT environment variable.
|
|
47
|
-
*/
|
|
48
|
-
tablesEndpoint?: string;
|
|
49
|
-
/**
|
|
50
|
-
* Name of the Table Storage table used for migration locks.
|
|
51
|
-
* Default: 'agentidentitylocks'
|
|
52
|
-
* Note: Azure Table names may not contain hyphens.
|
|
53
|
-
*/
|
|
54
|
-
locksTable?: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface LockEntity {
|
|
58
|
-
partitionKey: string;
|
|
59
|
-
rowKey: string;
|
|
60
|
-
migrationId: string;
|
|
61
|
-
expiresAt: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class AzureKeyVaultCredentialStore implements CredentialStore {
|
|
65
|
-
private readonly secrets: SecretClient;
|
|
66
|
-
private readonly table: TableClient;
|
|
67
|
-
|
|
68
|
-
constructor(options: AzureKeyVaultCredentialStoreOptions = {}) {
|
|
69
|
-
const vaultUrl =
|
|
70
|
-
options.keyVaultUrl ?? process.env['AZURE_KEYVAULT_URL'] ?? '';
|
|
71
|
-
if (!vaultUrl) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
'AzureKeyVaultCredentialStore: keyVaultUrl is required. ' +
|
|
74
|
-
'Pass it as an option or set AZURE_KEYVAULT_URL.'
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const tablesEndpoint =
|
|
79
|
-
options.tablesEndpoint ?? process.env['AZURE_TABLES_ENDPOINT'] ?? '';
|
|
80
|
-
if (!tablesEndpoint) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
'AzureKeyVaultCredentialStore: tablesEndpoint is required. ' +
|
|
83
|
-
'Pass it as an option or set AZURE_TABLES_ENDPOINT.'
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const locksTable = options.locksTable ?? 'agentidentitylocks';
|
|
88
|
-
const credential = new DefaultAzureCredential();
|
|
89
|
-
|
|
90
|
-
this.secrets = new SecretClient(vaultUrl, credential);
|
|
91
|
-
this.table = new TableClient(tablesEndpoint, locksTable, credential);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ─── CredentialStore: reads ──────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
async findByRef(ref: string): Promise<Credential | null> {
|
|
97
|
-
try {
|
|
98
|
-
const secret = await this.secrets.getSecret(ref);
|
|
99
|
-
if (!secret.value) return null;
|
|
100
|
-
// contentType carries status; skip non-active secrets without parsing JSON
|
|
101
|
-
if (secret.properties.contentType !== 'active') return null;
|
|
102
|
-
const cred: Credential = JSON.parse(secret.value);
|
|
103
|
-
return cred.status === 'active' ? cred : null;
|
|
104
|
-
} catch {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async listActive(): Promise<Credential[]> {
|
|
110
|
-
const results: Credential[] = [];
|
|
111
|
-
try {
|
|
112
|
-
for await (const secretProperties of this.secrets.listPropertiesOfSecrets()) {
|
|
113
|
-
// Only fetch value for secrets tagged as active
|
|
114
|
-
if (secretProperties.contentType !== 'active') continue;
|
|
115
|
-
if (!secretProperties.enabled) continue;
|
|
116
|
-
const name = secretProperties.name;
|
|
117
|
-
if (!name) continue;
|
|
118
|
-
try {
|
|
119
|
-
const secret = await this.secrets.getSecret(name);
|
|
120
|
-
if (!secret.value) continue;
|
|
121
|
-
const cred: Credential = JSON.parse(secret.value);
|
|
122
|
-
if (cred.status === 'active') results.push(cred);
|
|
123
|
-
} catch {
|
|
124
|
-
// malformed secret — skip
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// Key Vault unreachable — return empty rather than throw
|
|
129
|
-
}
|
|
130
|
-
return results;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async listByKind(kind: CredentialKind): Promise<Credential[]> {
|
|
134
|
-
const all = await this.listActive();
|
|
135
|
-
return all.filter((c) => c.kind === kind);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ─── CredentialStore: migration locks ────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
async reserve(ref: string, migrationId: string, ttlSeconds: number): Promise<boolean> {
|
|
141
|
-
const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
142
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
143
|
-
|
|
144
|
-
// Check for an existing unexpired lock held by a different migration
|
|
145
|
-
try {
|
|
146
|
-
const existing = await this.table.getEntity<LockEntity>('lock', ref);
|
|
147
|
-
if (
|
|
148
|
-
existing.migrationId !== migrationId &&
|
|
149
|
-
existing.expiresAt > nowSeconds
|
|
150
|
-
) {
|
|
151
|
-
return false; // locked by another migration and not yet expired
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
// Entity does not exist — proceed to create
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Upsert the lock (merge semantics — overwrites existing row if present)
|
|
158
|
-
try {
|
|
159
|
-
await this.table.upsertEntity<LockEntity>(
|
|
160
|
-
{
|
|
161
|
-
partitionKey: 'lock',
|
|
162
|
-
rowKey: ref,
|
|
163
|
-
migrationId,
|
|
164
|
-
expiresAt,
|
|
165
|
-
},
|
|
166
|
-
'Replace'
|
|
167
|
-
);
|
|
168
|
-
return true;
|
|
169
|
-
} catch {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async release(ref: string, migrationId: string): Promise<void> {
|
|
175
|
-
try {
|
|
176
|
-
const existing = await this.table.getEntity<LockEntity>('lock', ref);
|
|
177
|
-
// Only delete if this migration owns the lock
|
|
178
|
-
if (existing.migrationId !== migrationId) return;
|
|
179
|
-
await this.table.deleteEntity('lock', ref);
|
|
180
|
-
} catch {
|
|
181
|
-
// Already released or never held — idempotent
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|