@datacules/agent-identity-store-vault 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/VaultCredentialStore.js +96 -0
- package/dist/cjs/VaultCredentialStore.js.map +1 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/VaultCredentialStore.js +92 -0
- package/dist/esm/VaultCredentialStore.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/VaultCredentialStore.d.ts +44 -0
- package/dist/types/VaultCredentialStore.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/VaultCredentialStore.ts +0 -133
- package/src/vault.test.ts +0 -173
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,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VaultCredentialStore = void 0;
|
|
4
|
+
class VaultCredentialStore {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.address = options.address.replace(/\/$/, '');
|
|
7
|
+
this.token = options.token;
|
|
8
|
+
this.mountPath = options.mountPath ?? 'secret';
|
|
9
|
+
this.prefix = options.prefix ?? 'agent-identity';
|
|
10
|
+
}
|
|
11
|
+
get headers() {
|
|
12
|
+
return { 'X-Vault-Token': this.token, 'Content-Type': 'application/json' };
|
|
13
|
+
}
|
|
14
|
+
credPath(ref) {
|
|
15
|
+
return `${this.address}/v1/${this.mountPath}/data/${this.prefix}/${ref}`;
|
|
16
|
+
}
|
|
17
|
+
lockPath(ref) {
|
|
18
|
+
return `${this.address}/v1/${this.mountPath}/data/${this.prefix}/_locks/${ref}`;
|
|
19
|
+
}
|
|
20
|
+
async findByRef(ref) {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(this.credPath(ref), { headers: this.headers });
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
return null;
|
|
25
|
+
const body = (await res.json());
|
|
26
|
+
const cred = body.data?.data;
|
|
27
|
+
return cred?.status === 'active' ? cred : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async listActive() {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${this.address}/v1/${this.mountPath}/metadata/${this.prefix}?list=true`, { headers: this.headers });
|
|
36
|
+
if (!res.ok)
|
|
37
|
+
return [];
|
|
38
|
+
const body = await res.json();
|
|
39
|
+
const keys = body.data?.keys ?? [];
|
|
40
|
+
const creds = await Promise.all(keys.map((k) => this.findByRef(k)));
|
|
41
|
+
return creds.filter((c) => c !== null);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async listByKind(kind) {
|
|
48
|
+
const all = await this.listActive();
|
|
49
|
+
return all.filter((c) => c.kind === kind);
|
|
50
|
+
}
|
|
51
|
+
async reserve(ref, migrationId, ttlSeconds) {
|
|
52
|
+
// Read existing lock
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(this.lockPath(ref), { headers: this.headers });
|
|
55
|
+
if (res.ok) {
|
|
56
|
+
const body = (await res.json());
|
|
57
|
+
const lock = body.data?.data;
|
|
58
|
+
if (lock?.migrationId !== migrationId && lock?.expiresAt > Date.now() / 1000) {
|
|
59
|
+
return false; // held by another migration
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { /* no existing lock */ }
|
|
64
|
+
// Write lock
|
|
65
|
+
const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(this.lockPath(ref), {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: this.headers,
|
|
70
|
+
body: JSON.stringify({ data: { migrationId, expiresAt } }),
|
|
71
|
+
});
|
|
72
|
+
return res.ok;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async release(ref, migrationId) {
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(this.lockPath(ref), { headers: this.headers });
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
return;
|
|
83
|
+
const body = (await res.json());
|
|
84
|
+
const lock = body.data?.data;
|
|
85
|
+
if (lock?.migrationId !== migrationId)
|
|
86
|
+
return;
|
|
87
|
+
await fetch(`${this.address}/v1/${this.mountPath}/data/${this.prefix}/_locks/${ref}`, {
|
|
88
|
+
method: 'DELETE',
|
|
89
|
+
headers: this.headers,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch { /* idempotent */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.VaultCredentialStore = VaultCredentialStore;
|
|
96
|
+
//# sourceMappingURL=VaultCredentialStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VaultCredentialStore.js","sourceRoot":"","sources":["../../src/VaultCredentialStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VaultCredentialStore = void 0;
|
|
4
|
+
var VaultCredentialStore_1 = require("./VaultCredentialStore");
|
|
5
|
+
Object.defineProperty(exports, "VaultCredentialStore", { enumerable: true, get: function () { return VaultCredentialStore_1.VaultCredentialStore; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export class VaultCredentialStore {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
this.address = options.address.replace(/\/$/, '');
|
|
4
|
+
this.token = options.token;
|
|
5
|
+
this.mountPath = options.mountPath ?? 'secret';
|
|
6
|
+
this.prefix = options.prefix ?? 'agent-identity';
|
|
7
|
+
}
|
|
8
|
+
get headers() {
|
|
9
|
+
return { 'X-Vault-Token': this.token, 'Content-Type': 'application/json' };
|
|
10
|
+
}
|
|
11
|
+
credPath(ref) {
|
|
12
|
+
return `${this.address}/v1/${this.mountPath}/data/${this.prefix}/${ref}`;
|
|
13
|
+
}
|
|
14
|
+
lockPath(ref) {
|
|
15
|
+
return `${this.address}/v1/${this.mountPath}/data/${this.prefix}/_locks/${ref}`;
|
|
16
|
+
}
|
|
17
|
+
async findByRef(ref) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(this.credPath(ref), { headers: this.headers });
|
|
20
|
+
if (!res.ok)
|
|
21
|
+
return null;
|
|
22
|
+
const body = (await res.json());
|
|
23
|
+
const cred = body.data?.data;
|
|
24
|
+
return cred?.status === 'active' ? cred : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async listActive() {
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${this.address}/v1/${this.mountPath}/metadata/${this.prefix}?list=true`, { headers: this.headers });
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
return [];
|
|
35
|
+
const body = await res.json();
|
|
36
|
+
const keys = body.data?.keys ?? [];
|
|
37
|
+
const creds = await Promise.all(keys.map((k) => this.findByRef(k)));
|
|
38
|
+
return creds.filter((c) => c !== null);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async listByKind(kind) {
|
|
45
|
+
const all = await this.listActive();
|
|
46
|
+
return all.filter((c) => c.kind === kind);
|
|
47
|
+
}
|
|
48
|
+
async reserve(ref, migrationId, ttlSeconds) {
|
|
49
|
+
// Read existing lock
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(this.lockPath(ref), { headers: this.headers });
|
|
52
|
+
if (res.ok) {
|
|
53
|
+
const body = (await res.json());
|
|
54
|
+
const lock = body.data?.data;
|
|
55
|
+
if (lock?.migrationId !== migrationId && lock?.expiresAt > Date.now() / 1000) {
|
|
56
|
+
return false; // held by another migration
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { /* no existing lock */ }
|
|
61
|
+
// Write lock
|
|
62
|
+
const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(this.lockPath(ref), {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: this.headers,
|
|
67
|
+
body: JSON.stringify({ data: { migrationId, expiresAt } }),
|
|
68
|
+
});
|
|
69
|
+
return res.ok;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async release(ref, migrationId) {
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch(this.lockPath(ref), { headers: this.headers });
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
return;
|
|
80
|
+
const body = (await res.json());
|
|
81
|
+
const lock = body.data?.data;
|
|
82
|
+
if (lock?.migrationId !== migrationId)
|
|
83
|
+
return;
|
|
84
|
+
await fetch(`${this.address}/v1/${this.mountPath}/data/${this.prefix}/_locks/${ref}`, {
|
|
85
|
+
method: 'DELETE',
|
|
86
|
+
headers: this.headers,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch { /* idempotent */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=VaultCredentialStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VaultCredentialStore.js","sourceRoot":"","sources":["../../src/VaultCredentialStore.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,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HashiCorp Vault KV v2 CredentialStore implementation.
|
|
3
|
+
*
|
|
4
|
+
* Each credential is stored as a JSON object under:
|
|
5
|
+
* <mountPath>/data/<ref>
|
|
6
|
+
*
|
|
7
|
+
* Example write:
|
|
8
|
+
* vault kv put secret/agent-identity/linear-service-account-slot \
|
|
9
|
+
* id=cred-linear kind=fixed scope='All projects' status=active ref=linear-service-account-slot
|
|
10
|
+
*
|
|
11
|
+
* Vault reservation uses a separate KV path for migration locks:
|
|
12
|
+
* <mountPath>/data/_locks/<ref>
|
|
13
|
+
*
|
|
14
|
+
* Required Vault policy:
|
|
15
|
+
* path "secret/data/agent-identity/*" { capabilities = ["read", "list"] }
|
|
16
|
+
* path "secret/data/agent-identity/_locks/*" { capabilities = ["create", "update", "delete", "read"] }
|
|
17
|
+
*/
|
|
18
|
+
import type { Credential, CredentialKind, CredentialStore } from '@datacules/agent-identity';
|
|
19
|
+
export interface VaultCredentialStoreOptions {
|
|
20
|
+
/** Vault server address e.g. https://vault.example.com */
|
|
21
|
+
address: string;
|
|
22
|
+
/** Vault token or AppRole token */
|
|
23
|
+
token: string;
|
|
24
|
+
/** KV v2 mount path (default: 'secret') */
|
|
25
|
+
mountPath?: string;
|
|
26
|
+
/** Path prefix under mountPath (default: 'agent-identity') */
|
|
27
|
+
prefix?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare class VaultCredentialStore implements CredentialStore {
|
|
30
|
+
private readonly address;
|
|
31
|
+
private readonly token;
|
|
32
|
+
private readonly mountPath;
|
|
33
|
+
private readonly prefix;
|
|
34
|
+
constructor(options: VaultCredentialStoreOptions);
|
|
35
|
+
private get headers();
|
|
36
|
+
private credPath;
|
|
37
|
+
private lockPath;
|
|
38
|
+
findByRef(ref: string): Promise<Credential | null>;
|
|
39
|
+
listActive(): Promise<Credential[]>;
|
|
40
|
+
listByKind(kind: CredentialKind): Promise<Credential[]>;
|
|
41
|
+
reserve(ref: string, migrationId: string, ttlSeconds: number): Promise<boolean>;
|
|
42
|
+
release(ref: string, migrationId: string): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=VaultCredentialStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VaultCredentialStore.d.ts","sourceRoot":"","sources":["../../src/VaultCredentialStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE7F,MAAM,WAAW,2BAA2B;IAC1C,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD,qBAAa,oBAAqB,YAAW,eAAe;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,OAAO,EAAE,2BAA2B;IAOhD,OAAO,KAAK,OAAO,GAElB;IAED,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,QAAQ;IAIV,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAYlD,UAAU,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAgBnC,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;CAa/D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datacules/agent-identity-store-vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "HashiCorp Vault KV v2 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/vault"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent-identity",
|
|
15
|
+
"hashicorp",
|
|
16
|
+
"vault",
|
|
17
|
+
"credential-store",
|
|
18
|
+
"secrets",
|
|
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
|
"peerDependencies": {
|
|
14
|
-
"@datacules/agent-identity": "^0.
|
|
42
|
+
"@datacules/agent-identity": "^0.11.1"
|
|
15
43
|
},
|
|
16
44
|
"devDependencies": {
|
|
17
45
|
"@datacules/agent-identity": "*",
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HashiCorp Vault KV v2 CredentialStore implementation.
|
|
3
|
-
*
|
|
4
|
-
* Each credential is stored as a JSON object under:
|
|
5
|
-
* <mountPath>/data/<ref>
|
|
6
|
-
*
|
|
7
|
-
* Example write:
|
|
8
|
-
* vault kv put secret/agent-identity/linear-service-account-slot \
|
|
9
|
-
* id=cred-linear kind=fixed scope='All projects' status=active ref=linear-service-account-slot
|
|
10
|
-
*
|
|
11
|
-
* Vault reservation uses a separate KV path for migration locks:
|
|
12
|
-
* <mountPath>/data/_locks/<ref>
|
|
13
|
-
*
|
|
14
|
-
* Required Vault policy:
|
|
15
|
-
* path "secret/data/agent-identity/*" { capabilities = ["read", "list"] }
|
|
16
|
-
* path "secret/data/agent-identity/_locks/*" { capabilities = ["create", "update", "delete", "read"] }
|
|
17
|
-
*/
|
|
18
|
-
import type { Credential, CredentialKind, CredentialStore } from '@datacules/agent-identity';
|
|
19
|
-
|
|
20
|
-
export interface VaultCredentialStoreOptions {
|
|
21
|
-
/** Vault server address e.g. https://vault.example.com */
|
|
22
|
-
address: string;
|
|
23
|
-
/** Vault token or AppRole token */
|
|
24
|
-
token: string;
|
|
25
|
-
/** KV v2 mount path (default: 'secret') */
|
|
26
|
-
mountPath?: string;
|
|
27
|
-
/** Path prefix under mountPath (default: 'agent-identity') */
|
|
28
|
-
prefix?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface VaultKVResponse {
|
|
32
|
-
data: { data: Record<string, unknown> };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class VaultCredentialStore implements CredentialStore {
|
|
36
|
-
private readonly address: string;
|
|
37
|
-
private readonly token: string;
|
|
38
|
-
private readonly mountPath: string;
|
|
39
|
-
private readonly prefix: string;
|
|
40
|
-
|
|
41
|
-
constructor(options: VaultCredentialStoreOptions) {
|
|
42
|
-
this.address = options.address.replace(/\/$/, '');
|
|
43
|
-
this.token = options.token;
|
|
44
|
-
this.mountPath = options.mountPath ?? 'secret';
|
|
45
|
-
this.prefix = options.prefix ?? 'agent-identity';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private get headers(): Record<string, string> {
|
|
49
|
-
return { 'X-Vault-Token': this.token, 'Content-Type': 'application/json' };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private credPath(ref: string): string {
|
|
53
|
-
return `${this.address}/v1/${this.mountPath}/data/${this.prefix}/${ref}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private lockPath(ref: string): string {
|
|
57
|
-
return `${this.address}/v1/${this.mountPath}/data/${this.prefix}/_locks/${ref}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async findByRef(ref: string): Promise<Credential | null> {
|
|
61
|
-
try {
|
|
62
|
-
const res = await fetch(this.credPath(ref), { headers: this.headers });
|
|
63
|
-
if (!res.ok) return null;
|
|
64
|
-
const body = (await res.json()) as VaultKVResponse;
|
|
65
|
-
const cred = body.data?.data as unknown as Credential;
|
|
66
|
-
return cred?.status === 'active' ? cred : null;
|
|
67
|
-
} catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async listActive(): Promise<Credential[]> {
|
|
73
|
-
try {
|
|
74
|
-
const res = await fetch(
|
|
75
|
-
`${this.address}/v1/${this.mountPath}/metadata/${this.prefix}?list=true`,
|
|
76
|
-
{ headers: this.headers }
|
|
77
|
-
);
|
|
78
|
-
if (!res.ok) return [];
|
|
79
|
-
const body = await res.json() as { data: { keys: string[] } };
|
|
80
|
-
const keys = body.data?.keys ?? [];
|
|
81
|
-
const creds = await Promise.all(keys.map((k: string) => this.findByRef(k)));
|
|
82
|
-
return creds.filter((c): c is Credential => c !== null);
|
|
83
|
-
} catch {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async listByKind(kind: CredentialKind): Promise<Credential[]> {
|
|
89
|
-
const all = await this.listActive();
|
|
90
|
-
return all.filter((c) => c.kind === kind);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async reserve(ref: string, migrationId: string, ttlSeconds: number): Promise<boolean> {
|
|
94
|
-
// Read existing lock
|
|
95
|
-
try {
|
|
96
|
-
const res = await fetch(this.lockPath(ref), { headers: this.headers });
|
|
97
|
-
if (res.ok) {
|
|
98
|
-
const body = (await res.json()) as VaultKVResponse;
|
|
99
|
-
const lock = body.data?.data as unknown as { migrationId: string; expiresAt: number };
|
|
100
|
-
if (lock?.migrationId !== migrationId && lock?.expiresAt > Date.now() / 1000) {
|
|
101
|
-
return false; // held by another migration
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} catch { /* no existing lock */ }
|
|
105
|
-
|
|
106
|
-
// Write lock
|
|
107
|
-
const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
108
|
-
try {
|
|
109
|
-
const res = await fetch(this.lockPath(ref), {
|
|
110
|
-
method: 'POST',
|
|
111
|
-
headers: this.headers,
|
|
112
|
-
body: JSON.stringify({ data: { migrationId, expiresAt } }),
|
|
113
|
-
});
|
|
114
|
-
return res.ok;
|
|
115
|
-
} catch {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async release(ref: string, migrationId: string): Promise<void> {
|
|
121
|
-
try {
|
|
122
|
-
const res = await fetch(this.lockPath(ref), { headers: this.headers });
|
|
123
|
-
if (!res.ok) return;
|
|
124
|
-
const body = (await res.json()) as VaultKVResponse;
|
|
125
|
-
const lock = body.data?.data as unknown as { migrationId: string };
|
|
126
|
-
if (lock?.migrationId !== migrationId) return;
|
|
127
|
-
await fetch(`${this.address}/v1/${this.mountPath}/data/${this.prefix}/_locks/${ref}`, {
|
|
128
|
-
method: 'DELETE',
|
|
129
|
-
headers: this.headers,
|
|
130
|
-
});
|
|
131
|
-
} catch { /* idempotent */ }
|
|
132
|
-
}
|
|
133
|
-
}
|
package/src/vault.test.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { VaultCredentialStore } from './index';
|
|
3
|
-
|
|
4
|
-
// All HTTP calls are mocked via vi.stubGlobal('fetch', ...).
|
|
5
|
-
// No live HashiCorp Vault instance is required to run these tests.
|
|
6
|
-
const mockFetch = vi.fn();
|
|
7
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
8
|
-
|
|
9
|
-
const VAULT_ADDR = 'http://vault:8200';
|
|
10
|
-
const TOKEN = 'root-token';
|
|
11
|
-
|
|
12
|
-
const CRED = {
|
|
13
|
-
id: 'cred-linear',
|
|
14
|
-
kind: 'fixed' as const,
|
|
15
|
-
name: 'Linear Service Account',
|
|
16
|
-
scope: 'read:all',
|
|
17
|
-
status: 'active' as const,
|
|
18
|
-
ref: 'linear-service-account-slot',
|
|
19
|
-
provider: 'openai' as const,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function makeStore() {
|
|
23
|
-
return new VaultCredentialStore({ address: VAULT_ADDR, token: TOKEN });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function jsonOk(data: unknown): Response {
|
|
27
|
-
return { ok: true, json: async () => data } as unknown as Response;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const CRED_VAULT_RESPONSE = { data: { data: CRED } };
|
|
31
|
-
|
|
32
|
-
describe('VaultCredentialStore', () => {
|
|
33
|
-
beforeEach(() => vi.clearAllMocks());
|
|
34
|
-
|
|
35
|
-
// ── findByRef() ────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
describe('findByRef()', () => {
|
|
38
|
-
it('returns the active credential on a 200 Vault KV v2 response', async () => {
|
|
39
|
-
mockFetch.mockResolvedValueOnce(jsonOk(CRED_VAULT_RESPONSE));
|
|
40
|
-
const result = await makeStore().findByRef(CRED.ref);
|
|
41
|
-
expect(result).toMatchObject({ id: 'cred-linear', status: 'active' });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('sends the X-Vault-Token header with the configured token', async () => {
|
|
45
|
-
mockFetch.mockResolvedValueOnce(jsonOk(CRED_VAULT_RESPONSE));
|
|
46
|
-
await makeStore().findByRef(CRED.ref);
|
|
47
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
48
|
-
`${VAULT_ADDR}/v1/secret/data/agent-identity/${CRED.ref}`,
|
|
49
|
-
expect.objectContaining({
|
|
50
|
-
headers: expect.objectContaining({ 'X-Vault-Token': TOKEN }),
|
|
51
|
-
})
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('returns null when the credential status is not active', async () => {
|
|
56
|
-
mockFetch.mockResolvedValueOnce(
|
|
57
|
-
jsonOk({ data: { data: { ...CRED, status: 'revoked' } } })
|
|
58
|
-
);
|
|
59
|
-
expect(await makeStore().findByRef(CRED.ref)).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('returns null on a non-ok Vault response (e.g. 404)', async () => {
|
|
63
|
-
mockFetch.mockResolvedValueOnce({ ok: false } as Response);
|
|
64
|
-
expect(await makeStore().findByRef('unknown-ref')).toBeNull();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('returns null and does not throw when fetch throws a network error', async () => {
|
|
68
|
-
mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));
|
|
69
|
-
await expect(makeStore().findByRef(CRED.ref)).resolves.toBeNull();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// ── listActive() ──────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
describe('listActive()', () => {
|
|
76
|
-
it('returns all active credentials via metadata LIST then individual GETs', async () => {
|
|
77
|
-
// First call: metadata list
|
|
78
|
-
mockFetch.mockResolvedValueOnce(
|
|
79
|
-
jsonOk({ data: { keys: [CRED.ref] } })
|
|
80
|
-
);
|
|
81
|
-
// Second call: individual credential GET
|
|
82
|
-
mockFetch.mockResolvedValueOnce(jsonOk(CRED_VAULT_RESPONSE));
|
|
83
|
-
const result = await makeStore().listActive();
|
|
84
|
-
expect(result).toHaveLength(1);
|
|
85
|
-
expect(result[0]).toMatchObject({ id: 'cred-linear', status: 'active' });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('returns an empty array when the metadata list response is not ok', async () => {
|
|
89
|
-
mockFetch.mockResolvedValueOnce({ ok: false } as Response);
|
|
90
|
-
expect(await makeStore().listActive()).toEqual([]);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('returns an empty array when fetch throws on the metadata call', async () => {
|
|
94
|
-
mockFetch.mockRejectedValueOnce(new Error('Vault unreachable'));
|
|
95
|
-
expect(await makeStore().listActive()).toEqual([]);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// ── listByKind() ──────────────────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
describe('listByKind()', () => {
|
|
102
|
-
it('returns only credentials matching the requested kind', async () => {
|
|
103
|
-
mockFetch.mockResolvedValueOnce(jsonOk({ data: { keys: [CRED.ref] } }));
|
|
104
|
-
mockFetch.mockResolvedValueOnce(jsonOk(CRED_VAULT_RESPONSE)); // kind: 'fixed'
|
|
105
|
-
const result = await makeStore().listByKind('fixed');
|
|
106
|
-
expect(result).toHaveLength(1);
|
|
107
|
-
expect(result[0].kind).toBe('fixed');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('returns an empty array when no credentials match the requested kind', async () => {
|
|
111
|
-
mockFetch.mockResolvedValueOnce(jsonOk({ data: { keys: [CRED.ref] } }));
|
|
112
|
-
mockFetch.mockResolvedValueOnce(jsonOk(CRED_VAULT_RESPONSE)); // kind: 'fixed'
|
|
113
|
-
// Asking for 'user-delegated' — the fixed credential should not appear
|
|
114
|
-
const result = await makeStore().listByKind('user-delegated');
|
|
115
|
-
expect(result).toHaveLength(0);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ── reserve() ─────────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
describe('reserve()', () => {
|
|
122
|
-
it('returns true and writes the lock when no prior lock exists (read → 404)', async () => {
|
|
123
|
-
mockFetch.mockResolvedValueOnce({ ok: false } as Response); // read → not found
|
|
124
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response); // write → success
|
|
125
|
-
expect(await makeStore().reserve(CRED.ref, 'mig-1', 300)).toBe(true);
|
|
126
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('returns false when the lock is held by a different migration within TTL', async () => {
|
|
130
|
-
const expiresAt = Math.floor(Date.now() / 1000) + 9999;
|
|
131
|
-
mockFetch.mockResolvedValueOnce(
|
|
132
|
-
jsonOk({ data: { data: { migrationId: 'other-mig', expiresAt } } })
|
|
133
|
-
);
|
|
134
|
-
expect(await makeStore().reserve(CRED.ref, 'mig-1', 300)).toBe(false);
|
|
135
|
-
expect(mockFetch).toHaveBeenCalledOnce(); // only the read — no write attempted
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('returns true when the same migration re-acquires its own active lock', async () => {
|
|
139
|
-
const expiresAt = Math.floor(Date.now() / 1000) + 9999;
|
|
140
|
-
mockFetch.mockResolvedValueOnce(
|
|
141
|
-
jsonOk({ data: { data: { migrationId: 'mig-1', expiresAt } } })
|
|
142
|
-
);
|
|
143
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response);
|
|
144
|
-
expect(await makeStore().reserve(CRED.ref, 'mig-1', 300)).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// ── release() ─────────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
describe('release()', () => {
|
|
151
|
-
it('issues a DELETE request when the migrationId matches the stored lock', async () => {
|
|
152
|
-
mockFetch.mockResolvedValueOnce(
|
|
153
|
-
jsonOk({ data: { data: { migrationId: 'mig-1' } } })
|
|
154
|
-
);
|
|
155
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response); // DELETE
|
|
156
|
-
await makeStore().release(CRED.ref, 'mig-1');
|
|
157
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('makes only one fetch (the read) when the migrationId does not match', async () => {
|
|
161
|
-
mockFetch.mockResolvedValueOnce(
|
|
162
|
-
jsonOk({ data: { data: { migrationId: 'other-mig' } } })
|
|
163
|
-
);
|
|
164
|
-
await makeStore().release(CRED.ref, 'mig-1');
|
|
165
|
-
expect(mockFetch).toHaveBeenCalledOnce();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('resolves without throwing when the lock is already gone (fetch throws)', async () => {
|
|
169
|
-
mockFetch.mockRejectedValueOnce(new Error('404 Not Found'));
|
|
170
|
-
await expect(makeStore().release(CRED.ref, 'mig-1')).resolves.not.toThrow();
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
});
|