@datacules/agent-identity-store-azure 0.10.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
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,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AzureKeyVaultCredentialStore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Azure Key Vault + Azure Table Storage CredentialStore implementation.
|
|
6
|
+
*
|
|
7
|
+
* Key Vault holds each credential as a secret whose name is the credential ref.
|
|
8
|
+
* The secret value is the JSON-serialised Credential object.
|
|
9
|
+
* The secret's "content-type" tag carries the status (active | pending | revoked)
|
|
10
|
+
* so listActive() can skip inactive secrets without fetching their values.
|
|
11
|
+
*
|
|
12
|
+
* Table Storage holds migration reservation locks.
|
|
13
|
+
* Table name: agent-identity-locks
|
|
14
|
+
* Partition key: "lock" (constant — all locks in one partition for simplicity)
|
|
15
|
+
* Row key: the credential ref being locked
|
|
16
|
+
* Columns: migrationId (string), expiresAt (number — Unix epoch seconds)
|
|
17
|
+
*
|
|
18
|
+
* Azure setup:
|
|
19
|
+
* 1. Create an Azure Key Vault and store each Credential as a JSON secret.
|
|
20
|
+
* Set the secret's ContentType to "active", "pending", or "revoked".
|
|
21
|
+
* 2. Create a Storage Account and a Table named "agentidentitylocks".
|
|
22
|
+
* (Table names may not contain hyphens — use "agentidentitylocks".)
|
|
23
|
+
* 3. Grant the running identity:
|
|
24
|
+
* Key Vault Secrets User (read secrets)
|
|
25
|
+
* Key Vault Secrets Officer (only needed for write operations)
|
|
26
|
+
* Storage Table Data Contributor (read + write locks table)
|
|
27
|
+
* 4. Set AZURE_KEYVAULT_URL and AZURE_TABLES_ENDPOINT environment variables,
|
|
28
|
+
* or pass them as constructor options.
|
|
29
|
+
* DefaultAzureCredential resolves auth automatically from:
|
|
30
|
+
* - Managed Identity (production)
|
|
31
|
+
* - AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET env vars
|
|
32
|
+
* - Azure CLI (local development)
|
|
33
|
+
* - Workload Identity (AKS)
|
|
34
|
+
*/
|
|
35
|
+
const keyvault_secrets_1 = require("@azure/keyvault-secrets");
|
|
36
|
+
const data_tables_1 = require("@azure/data-tables");
|
|
37
|
+
const identity_1 = require("@azure/identity");
|
|
38
|
+
class AzureKeyVaultCredentialStore {
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
const vaultUrl = options.keyVaultUrl ?? process.env['AZURE_KEYVAULT_URL'] ?? '';
|
|
41
|
+
if (!vaultUrl) {
|
|
42
|
+
throw new Error('AzureKeyVaultCredentialStore: keyVaultUrl is required. ' +
|
|
43
|
+
'Pass it as an option or set AZURE_KEYVAULT_URL.');
|
|
44
|
+
}
|
|
45
|
+
const tablesEndpoint = options.tablesEndpoint ?? process.env['AZURE_TABLES_ENDPOINT'] ?? '';
|
|
46
|
+
if (!tablesEndpoint) {
|
|
47
|
+
throw new Error('AzureKeyVaultCredentialStore: tablesEndpoint is required. ' +
|
|
48
|
+
'Pass it as an option or set AZURE_TABLES_ENDPOINT.');
|
|
49
|
+
}
|
|
50
|
+
const locksTable = options.locksTable ?? 'agentidentitylocks';
|
|
51
|
+
const credential = new identity_1.DefaultAzureCredential();
|
|
52
|
+
this.secrets = new keyvault_secrets_1.SecretClient(vaultUrl, credential);
|
|
53
|
+
this.table = new data_tables_1.TableClient(tablesEndpoint, locksTable, credential);
|
|
54
|
+
}
|
|
55
|
+
// ─── CredentialStore: reads ──────────────────────────────────────────────
|
|
56
|
+
async findByRef(ref) {
|
|
57
|
+
try {
|
|
58
|
+
const secret = await this.secrets.getSecret(ref);
|
|
59
|
+
if (!secret.value)
|
|
60
|
+
return null;
|
|
61
|
+
// contentType carries status; skip non-active secrets without parsing JSON
|
|
62
|
+
if (secret.properties.contentType !== 'active')
|
|
63
|
+
return null;
|
|
64
|
+
const cred = JSON.parse(secret.value);
|
|
65
|
+
return cred.status === 'active' ? cred : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async listActive() {
|
|
72
|
+
const results = [];
|
|
73
|
+
try {
|
|
74
|
+
for await (const secretProperties of this.secrets.listPropertiesOfSecrets()) {
|
|
75
|
+
// Only fetch value for secrets tagged as active
|
|
76
|
+
if (secretProperties.contentType !== 'active')
|
|
77
|
+
continue;
|
|
78
|
+
if (!secretProperties.enabled)
|
|
79
|
+
continue;
|
|
80
|
+
const name = secretProperties.name;
|
|
81
|
+
if (!name)
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
const secret = await this.secrets.getSecret(name);
|
|
85
|
+
if (!secret.value)
|
|
86
|
+
continue;
|
|
87
|
+
const cred = JSON.parse(secret.value);
|
|
88
|
+
if (cred.status === 'active')
|
|
89
|
+
results.push(cred);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// malformed secret — skip
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Key Vault unreachable — return empty rather than throw
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
async listByKind(kind) {
|
|
102
|
+
const all = await this.listActive();
|
|
103
|
+
return all.filter((c) => c.kind === kind);
|
|
104
|
+
}
|
|
105
|
+
// ─── CredentialStore: migration locks ────────────────────────────────────
|
|
106
|
+
async reserve(ref, migrationId, ttlSeconds) {
|
|
107
|
+
const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
108
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
109
|
+
// Check for an existing unexpired lock held by a different migration
|
|
110
|
+
try {
|
|
111
|
+
const existing = await this.table.getEntity('lock', ref);
|
|
112
|
+
if (existing.migrationId !== migrationId &&
|
|
113
|
+
existing.expiresAt > nowSeconds) {
|
|
114
|
+
return false; // locked by another migration and not yet expired
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Entity does not exist — proceed to create
|
|
119
|
+
}
|
|
120
|
+
// Upsert the lock (merge semantics — overwrites existing row if present)
|
|
121
|
+
try {
|
|
122
|
+
await this.table.upsertEntity({
|
|
123
|
+
partitionKey: 'lock',
|
|
124
|
+
rowKey: ref,
|
|
125
|
+
migrationId,
|
|
126
|
+
expiresAt,
|
|
127
|
+
}, 'Replace');
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async release(ref, migrationId) {
|
|
135
|
+
try {
|
|
136
|
+
const existing = await this.table.getEntity('lock', ref);
|
|
137
|
+
// Only delete if this migration owns the lock
|
|
138
|
+
if (existing.migrationId !== migrationId)
|
|
139
|
+
return;
|
|
140
|
+
await this.table.deleteEntity('lock', ref);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Already released or never held — idempotent
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.AzureKeyVaultCredentialStore = AzureKeyVaultCredentialStore;
|
|
148
|
+
//# sourceMappingURL=AzureKeyVaultCredentialStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AzureKeyVaultCredentialStore.js","sourceRoot":"","sources":["../../src/AzureKeyVaultCredentialStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AzureKeyVaultCredentialStore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Azure Key Vault + Azure Table Storage CredentialStore implementation.
|
|
6
|
+
*
|
|
7
|
+
* Key Vault holds each credential as a secret whose name is the credential ref.
|
|
8
|
+
* The secret value is the JSON-serialised Credential object.
|
|
9
|
+
* The secret's "content-type" tag carries the status (active | pending | revoked)
|
|
10
|
+
* so listActive() can skip inactive secrets without fetching their values.
|
|
11
|
+
*
|
|
12
|
+
* Table Storage holds migration reservation locks.
|
|
13
|
+
* Table name: agent-identity-locks
|
|
14
|
+
* Partition key: "lock" (constant — all locks in one partition for simplicity)
|
|
15
|
+
* Row key: the credential ref being locked
|
|
16
|
+
* Columns: migrationId (string), expiresAt (number — Unix epoch seconds)
|
|
17
|
+
*
|
|
18
|
+
* Azure setup:
|
|
19
|
+
* 1. Create an Azure Key Vault and store each Credential as a JSON secret.
|
|
20
|
+
* Set the secret's ContentType to "active", "pending", or "revoked".
|
|
21
|
+
* 2. Create a Storage Account and a Table named "agentidentitylocks".
|
|
22
|
+
* (Table names may not contain hyphens — use "agentidentitylocks".)
|
|
23
|
+
* 3. Grant the running identity:
|
|
24
|
+
* Key Vault Secrets User (read secrets)
|
|
25
|
+
* Key Vault Secrets Officer (only needed for write operations)
|
|
26
|
+
* Storage Table Data Contributor (read + write locks table)
|
|
27
|
+
* 4. Set AZURE_KEYVAULT_URL and AZURE_TABLES_ENDPOINT environment variables,
|
|
28
|
+
* or pass them as constructor options.
|
|
29
|
+
* DefaultAzureCredential resolves auth automatically from:
|
|
30
|
+
* - Managed Identity (production)
|
|
31
|
+
* - AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET env vars
|
|
32
|
+
* - Azure CLI (local development)
|
|
33
|
+
* - Workload Identity (AKS)
|
|
34
|
+
*/
|
|
35
|
+
const keyvault_secrets_1 = require("@azure/keyvault-secrets");
|
|
36
|
+
const data_tables_1 = require("@azure/data-tables");
|
|
37
|
+
const identity_1 = require("@azure/identity");
|
|
38
|
+
class AzureKeyVaultCredentialStore {
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
const vaultUrl = options.keyVaultUrl ?? process.env['AZURE_KEYVAULT_URL'] ?? '';
|
|
41
|
+
if (!vaultUrl) {
|
|
42
|
+
throw new Error('AzureKeyVaultCredentialStore: keyVaultUrl is required. ' +
|
|
43
|
+
'Pass it as an option or set AZURE_KEYVAULT_URL.');
|
|
44
|
+
}
|
|
45
|
+
const tablesEndpoint = options.tablesEndpoint ?? process.env['AZURE_TABLES_ENDPOINT'] ?? '';
|
|
46
|
+
if (!tablesEndpoint) {
|
|
47
|
+
throw new Error('AzureKeyVaultCredentialStore: tablesEndpoint is required. ' +
|
|
48
|
+
'Pass it as an option or set AZURE_TABLES_ENDPOINT.');
|
|
49
|
+
}
|
|
50
|
+
const locksTable = options.locksTable ?? 'agentidentitylocks';
|
|
51
|
+
const credential = new identity_1.DefaultAzureCredential();
|
|
52
|
+
this.secrets = new keyvault_secrets_1.SecretClient(vaultUrl, credential);
|
|
53
|
+
this.table = new data_tables_1.TableClient(tablesEndpoint, locksTable, credential);
|
|
54
|
+
}
|
|
55
|
+
// ─── CredentialStore: reads ──────────────────────────────────────────────
|
|
56
|
+
async findByRef(ref) {
|
|
57
|
+
try {
|
|
58
|
+
const secret = await this.secrets.getSecret(ref);
|
|
59
|
+
if (!secret.value)
|
|
60
|
+
return null;
|
|
61
|
+
// contentType carries status; skip non-active secrets without parsing JSON
|
|
62
|
+
if (secret.properties.contentType !== 'active')
|
|
63
|
+
return null;
|
|
64
|
+
const cred = JSON.parse(secret.value);
|
|
65
|
+
return cred.status === 'active' ? cred : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async listActive() {
|
|
72
|
+
const results = [];
|
|
73
|
+
try {
|
|
74
|
+
for await (const secretProperties of this.secrets.listPropertiesOfSecrets()) {
|
|
75
|
+
// Only fetch value for secrets tagged as active
|
|
76
|
+
if (secretProperties.contentType !== 'active')
|
|
77
|
+
continue;
|
|
78
|
+
if (!secretProperties.enabled)
|
|
79
|
+
continue;
|
|
80
|
+
const name = secretProperties.name;
|
|
81
|
+
if (!name)
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
const secret = await this.secrets.getSecret(name);
|
|
85
|
+
if (!secret.value)
|
|
86
|
+
continue;
|
|
87
|
+
const cred = JSON.parse(secret.value);
|
|
88
|
+
if (cred.status === 'active')
|
|
89
|
+
results.push(cred);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// malformed secret — skip
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Key Vault unreachable — return empty rather than throw
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
async listByKind(kind) {
|
|
102
|
+
const all = await this.listActive();
|
|
103
|
+
return all.filter((c) => c.kind === kind);
|
|
104
|
+
}
|
|
105
|
+
// ─── CredentialStore: migration locks ────────────────────────────────────
|
|
106
|
+
async reserve(ref, migrationId, ttlSeconds) {
|
|
107
|
+
const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
108
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
109
|
+
// Check for an existing unexpired lock held by a different migration
|
|
110
|
+
try {
|
|
111
|
+
const existing = await this.table.getEntity('lock', ref);
|
|
112
|
+
if (existing.migrationId !== migrationId &&
|
|
113
|
+
existing.expiresAt > nowSeconds) {
|
|
114
|
+
return false; // locked by another migration and not yet expired
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Entity does not exist — proceed to create
|
|
119
|
+
}
|
|
120
|
+
// Upsert the lock (merge semantics — overwrites existing row if present)
|
|
121
|
+
try {
|
|
122
|
+
await this.table.upsertEntity({
|
|
123
|
+
partitionKey: 'lock',
|
|
124
|
+
rowKey: ref,
|
|
125
|
+
migrationId,
|
|
126
|
+
expiresAt,
|
|
127
|
+
}, 'Replace');
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async release(ref, migrationId) {
|
|
135
|
+
try {
|
|
136
|
+
const existing = await this.table.getEntity('lock', ref);
|
|
137
|
+
// Only delete if this migration owns the lock
|
|
138
|
+
if (existing.migrationId !== migrationId)
|
|
139
|
+
return;
|
|
140
|
+
await this.table.deleteEntity('lock', ref);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Already released or never held — idempotent
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.AzureKeyVaultCredentialStore = AzureKeyVaultCredentialStore;
|
|
148
|
+
//# sourceMappingURL=AzureKeyVaultCredentialStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AzureKeyVaultCredentialStore.js","sourceRoot":"","sources":["../../src/AzureKeyVaultCredentialStore.ts"],"names":[],"mappings":"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"}
|