@r4security/sdk 0.0.2
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/README.md +95 -0
- package/lib/index.cjs +1093 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +69 -0
- package/lib/index.d.ts +69 -0
- package/lib/index.js +1058 -0
- package/lib/index.js.map +1 -0
- package/package.json +44 -0
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
R4: () => R4,
|
|
34
|
+
default: () => src_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
38
|
+
|
|
39
|
+
// src/client.ts
|
|
40
|
+
var R4Client = class {
|
|
41
|
+
apiKey;
|
|
42
|
+
baseUrl;
|
|
43
|
+
constructor(apiKey, baseUrl) {
|
|
44
|
+
this.apiKey = apiKey;
|
|
45
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
46
|
+
}
|
|
47
|
+
buildHeaders() {
|
|
48
|
+
return {
|
|
49
|
+
"X-API-Key": this.apiKey,
|
|
50
|
+
"Content-Type": "application/json"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async request(path4, init) {
|
|
54
|
+
const response = await fetch(`${this.baseUrl}${path4}`, {
|
|
55
|
+
...init,
|
|
56
|
+
headers: {
|
|
57
|
+
...this.buildHeaders(),
|
|
58
|
+
...init.headers ?? {}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
63
|
+
const errorMessage = typeof errorBody.error?.message === "string" ? errorBody.error.message : `HTTP ${response.status}: ${response.statusText}`;
|
|
64
|
+
throw new Error(`R4 API Error: ${errorMessage}`);
|
|
65
|
+
}
|
|
66
|
+
if (response.status === 204) {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
return response.json();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Registers or re-confirms the agent runtime's local RSA public key.
|
|
73
|
+
*/
|
|
74
|
+
async registerAgentPublicKey(body) {
|
|
75
|
+
return this.request("/api/v1/machine/vault/public-key", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
body: JSON.stringify(body)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Lists all accessible non-hidden vaults. When `projectId` is provided, the
|
|
82
|
+
* backend additionally filters to vaults associated with that project.
|
|
83
|
+
*/
|
|
84
|
+
async listVaults(projectId) {
|
|
85
|
+
const search = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
|
86
|
+
return this.request(`/api/v1/machine/vault${search}`, {
|
|
87
|
+
method: "GET"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Retrieves the active wrapped DEK for the authenticated agent on a vault.
|
|
92
|
+
*/
|
|
93
|
+
async getAgentWrappedKey(vaultId) {
|
|
94
|
+
return this.request(
|
|
95
|
+
`/api/v1/machine/vault/${encodeURIComponent(vaultId)}/wrapped-key`,
|
|
96
|
+
{ method: "GET" }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Retrieves the trusted user-key directory for a vault so the runtime can
|
|
101
|
+
* verify wrapped-DEK signatures locally.
|
|
102
|
+
*/
|
|
103
|
+
async getVaultUserKeyDirectory(vaultId, params) {
|
|
104
|
+
const searchParams = new URLSearchParams();
|
|
105
|
+
if (params?.knownTransparencyVersion !== void 0) {
|
|
106
|
+
searchParams.set("knownTransparencyVersion", String(params.knownTransparencyVersion));
|
|
107
|
+
}
|
|
108
|
+
if (params?.knownTransparencyHash) {
|
|
109
|
+
searchParams.set("knownTransparencyHash", params.knownTransparencyHash);
|
|
110
|
+
}
|
|
111
|
+
const search = searchParams.size > 0 ? `?${searchParams.toString()}` : "";
|
|
112
|
+
return this.request(
|
|
113
|
+
`/api/v1/machine/vault/${encodeURIComponent(vaultId)}/public-keys${search}`,
|
|
114
|
+
{ method: "GET" }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Lists all items in a vault with lightweight metadata.
|
|
119
|
+
*/
|
|
120
|
+
async listVaultItems(vaultId) {
|
|
121
|
+
return this.request(
|
|
122
|
+
`/api/v1/machine/vault/${encodeURIComponent(vaultId)}/items`,
|
|
123
|
+
{ method: "GET" }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Retrieves the full field payloads for a vault item.
|
|
128
|
+
*/
|
|
129
|
+
async getVaultItemDetail(vaultId, itemId) {
|
|
130
|
+
return this.request(
|
|
131
|
+
`/api/v1/machine/vault/${encodeURIComponent(vaultId)}/items/${encodeURIComponent(itemId)}`,
|
|
132
|
+
{ method: "GET" }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/crypto.ts
|
|
138
|
+
var import_node_crypto = __toESM(require("crypto"), 1);
|
|
139
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
140
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
141
|
+
|
|
142
|
+
// src/transparency.ts
|
|
143
|
+
var TRANSPARENCY_WITNESS_PAYLOAD_PREFIX = "r4-transparency-witness-v1";
|
|
144
|
+
var DEFAULT_TRANSPARENCY_WITNESS_URL = "https://transparency.r4.dev";
|
|
145
|
+
var TRANSPARENCY_WITNESS_ROOT_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
146
|
+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA18JhILFiS/BOWR9laubW
|
|
147
|
+
g2vepQy26BXAlnrscZZVQUzBBaCM4hWobpt3Nh77vxP0gqVAJXP1hVhPPwxGQnOF
|
|
148
|
+
4Qg/RK4iEETjMdmh3KMqFX9MeE9tP4cTOGtsgWsedNpu6TvMT+2vu+0ltmr7p4Xv
|
|
149
|
+
H0ID48Q8JLeNksc/RekrsfzQ9DVtXFS7z1FF2VQgzamdJsW9hGMiM7Q+0iXei7PW
|
|
150
|
+
3PsLd1aNtqJ3lIj3t12qFiJiYyKF0hEq0//Abgb9SgDv/WOlRG1Ianf1/fnP2jer
|
|
151
|
+
ZYiZSylXqQdun0Db2d0+FDm/znV2AGAmBEXm6qnCogEHu77LoLyCyJOlB9WNtRwh
|
|
152
|
+
KnbzTmE2Mw/43jxvCcR7pE5kik/tdeMvqGFZfg3ozUG9eM0q0TURH6g9b9J4sBnR
|
|
153
|
+
dxz2PbF4cl/AeL4ANPmLz3kUQaDA6wR0veVk5jV+Uqr55TYz/zEbY1rtJbmnc53Q
|
|
154
|
+
ihPS6xtSiexrqnOgqm/AVbiRhxjPqfg3/VJM3zR5Blnu02AqVR9kCT0WkyEWRz5X
|
|
155
|
+
6HU8DEocJIPz8UwBMKQ7rnjMPv/Fjpuav/EIad5vOdfxCZkjyTYoQg8vLUyfXvgD
|
|
156
|
+
mBWFgKIN8GTRyM+LjZIgznjN58dZ8ZvsGd14oKnH7WgAh9FVh8ri7gNmsdJeRTn/
|
|
157
|
+
2zDkTlx+FQxAxqFaYV7qCvcCAwEAAQ==
|
|
158
|
+
-----END PUBLIC KEY-----`;
|
|
159
|
+
var buildOrgUserKeyDirectoryWitnessPayload = (orgId, head) => [
|
|
160
|
+
TRANSPARENCY_WITNESS_PAYLOAD_PREFIX,
|
|
161
|
+
"org-user-key-directory",
|
|
162
|
+
orgId,
|
|
163
|
+
String(head.version),
|
|
164
|
+
head.hash
|
|
165
|
+
].join(":");
|
|
166
|
+
var buildOrgUserKeyDirectoryWitnessPath = (orgId) => `v1/orgs/${orgId}/user-key-directory-head.json`;
|
|
167
|
+
var buildTransparencyWitnessUrl = (baseUrl, path4) => `${baseUrl.replace(/\/+$/, "")}/${path4.replace(/^\/+/, "")}`;
|
|
168
|
+
var shouldUseDefaultTransparencyWitness = (apiBaseUrl) => /(^https?:\/\/)?([^.]+\.)?r4\.dev(?::\d+)?(\/|$)/i.test(apiBaseUrl.trim());
|
|
169
|
+
|
|
170
|
+
// src/crypto.ts
|
|
171
|
+
var RSA_OAEP_CONFIG = {
|
|
172
|
+
padding: import_node_crypto.default.constants.RSA_PKCS1_OAEP_PADDING,
|
|
173
|
+
oaepHash: "sha256"
|
|
174
|
+
};
|
|
175
|
+
var RSA_PSS_SIGN_CONFIG = {
|
|
176
|
+
padding: import_node_crypto.default.constants.RSA_PKCS1_PSS_PADDING,
|
|
177
|
+
saltLength: 32
|
|
178
|
+
};
|
|
179
|
+
var USER_KEY_ROTATION_PREFIX = "r4-user-key-rotation-v1";
|
|
180
|
+
var USER_KEY_DIRECTORY_CHECKPOINT_PREFIX = "r4-user-key-directory-checkpoint-v1";
|
|
181
|
+
var USER_KEY_DIRECTORY_TRANSPARENCY_ENTRY_PREFIX = "r4-user-key-directory-transparency-entry-v1";
|
|
182
|
+
var WRAPPED_DEK_SIGNATURE_PREFIX = "r4-wrapped-dek-signature-v1";
|
|
183
|
+
var VAULT_SUMMARY_CHECKPOINT_PREFIX = "r4-vault-summary-checkpoint-v1";
|
|
184
|
+
var VAULT_ITEM_DETAIL_CHECKPOINT_PREFIX = "r4-vault-item-detail-checkpoint-v1";
|
|
185
|
+
function pemToDer(pem, beginLabel, endLabel) {
|
|
186
|
+
const derBase64 = pem.replace(beginLabel, "").replace(endLabel, "").replace(/\s/g, "");
|
|
187
|
+
return Buffer.from(derBase64, "base64");
|
|
188
|
+
}
|
|
189
|
+
function getWrappedDekFingerprint(wrappedDek) {
|
|
190
|
+
return import_node_crypto.default.createHash("sha256").update(Buffer.from(wrappedDek, "base64")).digest("hex");
|
|
191
|
+
}
|
|
192
|
+
function getCheckpointFingerprint(prefix, canonicalJson) {
|
|
193
|
+
return `${prefix}:${import_node_crypto.default.createHash("sha256").update(canonicalJson, "utf8").digest("hex")}`;
|
|
194
|
+
}
|
|
195
|
+
function loadPrivateKey(privateKeyPath) {
|
|
196
|
+
return import_node_fs.default.readFileSync(import_node_path.default.resolve(privateKeyPath), "utf8").trim();
|
|
197
|
+
}
|
|
198
|
+
function derivePublicKey(privateKeyPem) {
|
|
199
|
+
return import_node_crypto.default.createPublicKey(privateKeyPem).export({
|
|
200
|
+
type: "spki",
|
|
201
|
+
format: "pem"
|
|
202
|
+
}).toString();
|
|
203
|
+
}
|
|
204
|
+
function getPublicKeyFingerprint(publicKeyPem) {
|
|
205
|
+
const derBytes = pemToDer(publicKeyPem, "-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----");
|
|
206
|
+
return import_node_crypto.default.createHash("sha256").update(derBytes).digest("hex");
|
|
207
|
+
}
|
|
208
|
+
function buildUserKeyRotationPayload(previousUserKeyPairId, newPublicKeyFingerprint) {
|
|
209
|
+
return `${USER_KEY_ROTATION_PREFIX}:${previousUserKeyPairId}:${newPublicKeyFingerprint}`;
|
|
210
|
+
}
|
|
211
|
+
function verifyUserKeyRotation(previousUserKeyPairId, newPublicKeyPem, rotationSignature, previousPublicKeyPem) {
|
|
212
|
+
const payload = buildUserKeyRotationPayload(
|
|
213
|
+
previousUserKeyPairId,
|
|
214
|
+
getPublicKeyFingerprint(newPublicKeyPem)
|
|
215
|
+
);
|
|
216
|
+
try {
|
|
217
|
+
return import_node_crypto.default.verify(
|
|
218
|
+
"sha256",
|
|
219
|
+
Buffer.from(payload, "utf8"),
|
|
220
|
+
{
|
|
221
|
+
key: previousPublicKeyPem,
|
|
222
|
+
...RSA_PSS_SIGN_CONFIG
|
|
223
|
+
},
|
|
224
|
+
Buffer.from(rotationSignature, "base64")
|
|
225
|
+
);
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function verifyTransparencyWitnessPayload(payload, signature, publicKeyPem) {
|
|
231
|
+
try {
|
|
232
|
+
return import_node_crypto.default.verify(
|
|
233
|
+
"sha256",
|
|
234
|
+
Buffer.from(payload, "utf8"),
|
|
235
|
+
{
|
|
236
|
+
key: publicKeyPem,
|
|
237
|
+
...RSA_PSS_SIGN_CONFIG
|
|
238
|
+
},
|
|
239
|
+
Buffer.from(signature, "base64")
|
|
240
|
+
);
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function verifyOrgUserKeyDirectoryWitnessArtifact(artifact, publicKeyPem) {
|
|
246
|
+
return verifyTransparencyWitnessPayload(
|
|
247
|
+
buildOrgUserKeyDirectoryWitnessPayload(artifact.orgId, artifact.head),
|
|
248
|
+
artifact.signature,
|
|
249
|
+
publicKeyPem
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
function normalizeUserKeyDirectoryCheckpoint(checkpoint) {
|
|
253
|
+
return {
|
|
254
|
+
orgId: checkpoint.orgId,
|
|
255
|
+
version: checkpoint.version,
|
|
256
|
+
entries: [...checkpoint.entries].map((entry) => ({
|
|
257
|
+
userKeyPairId: entry.userKeyPairId,
|
|
258
|
+
orgUserId: entry.orgUserId,
|
|
259
|
+
fingerprint: entry.fingerprint,
|
|
260
|
+
previousUserKeyPairId: entry.previousUserKeyPairId ?? null,
|
|
261
|
+
rotationSignature: entry.rotationSignature ?? null
|
|
262
|
+
})).sort((left, right) => left.userKeyPairId.localeCompare(right.userKeyPairId))
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function canonicalizeUserKeyDirectoryCheckpoint(checkpoint) {
|
|
266
|
+
return JSON.stringify(normalizeUserKeyDirectoryCheckpoint(checkpoint));
|
|
267
|
+
}
|
|
268
|
+
function buildUserKeyDirectoryCheckpointPayload(checkpoint) {
|
|
269
|
+
return getCheckpointFingerprint(
|
|
270
|
+
USER_KEY_DIRECTORY_CHECKPOINT_PREFIX,
|
|
271
|
+
canonicalizeUserKeyDirectoryCheckpoint(checkpoint)
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
function verifyUserKeyDirectoryCheckpoint(checkpoint, signature, publicKeyPem) {
|
|
275
|
+
try {
|
|
276
|
+
return import_node_crypto.default.verify(
|
|
277
|
+
"sha256",
|
|
278
|
+
Buffer.from(buildUserKeyDirectoryCheckpointPayload(checkpoint), "utf8"),
|
|
279
|
+
{
|
|
280
|
+
key: publicKeyPem,
|
|
281
|
+
...RSA_PSS_SIGN_CONFIG
|
|
282
|
+
},
|
|
283
|
+
Buffer.from(signature, "base64")
|
|
284
|
+
);
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function buildUserKeyDirectoryTransparencyEntryHash(entry) {
|
|
290
|
+
return getCheckpointFingerprint(
|
|
291
|
+
USER_KEY_DIRECTORY_TRANSPARENCY_ENTRY_PREFIX,
|
|
292
|
+
JSON.stringify({
|
|
293
|
+
orgId: entry.orgId,
|
|
294
|
+
version: entry.version,
|
|
295
|
+
directoryCheckpointPayload: entry.directoryCheckpointPayload,
|
|
296
|
+
signerUserKeyPairId: entry.signerUserKeyPairId,
|
|
297
|
+
signerOrgUserId: entry.signerOrgUserId,
|
|
298
|
+
signerFingerprint: entry.signerFingerprint,
|
|
299
|
+
signature: entry.signature,
|
|
300
|
+
previousEntryHash: entry.previousEntryHash ?? null
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
function buildUserKeyDirectoryTransparencyEntry(params) {
|
|
305
|
+
const entryWithoutHash = {
|
|
306
|
+
orgId: params.checkpoint.orgId,
|
|
307
|
+
version: params.checkpoint.version,
|
|
308
|
+
directoryCheckpointPayload: buildUserKeyDirectoryCheckpointPayload(params.checkpoint),
|
|
309
|
+
signerUserKeyPairId: params.signerUserKeyPairId,
|
|
310
|
+
signerOrgUserId: params.signerOrgUserId,
|
|
311
|
+
signerFingerprint: getPublicKeyFingerprint(params.signerPublicKey),
|
|
312
|
+
signature: params.signature,
|
|
313
|
+
previousEntryHash: params.previousEntryHash ?? null
|
|
314
|
+
};
|
|
315
|
+
return {
|
|
316
|
+
...entryWithoutHash,
|
|
317
|
+
entryHash: buildUserKeyDirectoryTransparencyEntryHash(entryWithoutHash)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function verifyUserKeyDirectoryTransparencyProof(params) {
|
|
321
|
+
if (params.proof.head.version !== params.currentEntry.version || params.proof.head.hash !== params.currentEntry.entryHash) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (!params.previousHead) {
|
|
325
|
+
if (params.proof.entries.length === 0) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
for (let index = 0; index < params.proof.entries.length; index++) {
|
|
329
|
+
const entry = params.proof.entries[index];
|
|
330
|
+
if (!entry) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
const expectedHash = buildUserKeyDirectoryTransparencyEntryHash({
|
|
334
|
+
orgId: entry.orgId,
|
|
335
|
+
version: entry.version,
|
|
336
|
+
directoryCheckpointPayload: entry.directoryCheckpointPayload,
|
|
337
|
+
signerUserKeyPairId: entry.signerUserKeyPairId,
|
|
338
|
+
signerOrgUserId: entry.signerOrgUserId,
|
|
339
|
+
signerFingerprint: entry.signerFingerprint,
|
|
340
|
+
signature: entry.signature,
|
|
341
|
+
previousEntryHash: entry.previousEntryHash
|
|
342
|
+
});
|
|
343
|
+
if (expectedHash !== entry.entryHash) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
if (index === 0) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const previousEntry = params.proof.entries[index - 1];
|
|
350
|
+
if (!previousEntry) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
if (entry.previousEntryHash !== previousEntry.entryHash || entry.version !== previousEntry.version + 1) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const lastEntry = params.proof.entries[params.proof.entries.length - 1];
|
|
358
|
+
return lastEntry?.entryHash === params.currentEntry.entryHash && lastEntry.version === params.currentEntry.version;
|
|
359
|
+
}
|
|
360
|
+
if (params.currentEntry.version < params.previousHead.version) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
if (params.currentEntry.version === params.previousHead.version) {
|
|
364
|
+
return params.currentEntry.entryHash === params.previousHead.hash && params.proof.entries.length === 0;
|
|
365
|
+
}
|
|
366
|
+
if (params.proof.entries.length === 0) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
let previousVersion = params.previousHead.version;
|
|
370
|
+
let previousHash = params.previousHead.hash;
|
|
371
|
+
for (const entry of params.proof.entries) {
|
|
372
|
+
const expectedHash = buildUserKeyDirectoryTransparencyEntryHash({
|
|
373
|
+
orgId: entry.orgId,
|
|
374
|
+
version: entry.version,
|
|
375
|
+
directoryCheckpointPayload: entry.directoryCheckpointPayload,
|
|
376
|
+
signerUserKeyPairId: entry.signerUserKeyPairId,
|
|
377
|
+
signerOrgUserId: entry.signerOrgUserId,
|
|
378
|
+
signerFingerprint: entry.signerFingerprint,
|
|
379
|
+
signature: entry.signature,
|
|
380
|
+
previousEntryHash: entry.previousEntryHash
|
|
381
|
+
});
|
|
382
|
+
if (expectedHash !== entry.entryHash || entry.previousEntryHash !== previousHash || entry.version !== previousVersion + 1) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
previousVersion = entry.version;
|
|
386
|
+
previousHash = entry.entryHash;
|
|
387
|
+
}
|
|
388
|
+
return previousHash === params.currentEntry.entryHash && previousVersion === params.currentEntry.version;
|
|
389
|
+
}
|
|
390
|
+
function buildWrappedDekSignaturePayload(vaultId, recipientKeyId, signerUserKeyPairId, dekVersion, wrappedDek) {
|
|
391
|
+
return [
|
|
392
|
+
WRAPPED_DEK_SIGNATURE_PREFIX,
|
|
393
|
+
vaultId,
|
|
394
|
+
recipientKeyId,
|
|
395
|
+
signerUserKeyPairId,
|
|
396
|
+
String(dekVersion),
|
|
397
|
+
getWrappedDekFingerprint(wrappedDek)
|
|
398
|
+
].join(":");
|
|
399
|
+
}
|
|
400
|
+
function verifyWrappedDekSignature(vaultId, recipientKeyId, signerUserKeyPairId, dekVersion, wrappedDek, wrappedDekSignature, signerPublicKeyPem) {
|
|
401
|
+
const payload = buildWrappedDekSignaturePayload(
|
|
402
|
+
vaultId,
|
|
403
|
+
recipientKeyId,
|
|
404
|
+
signerUserKeyPairId,
|
|
405
|
+
dekVersion,
|
|
406
|
+
wrappedDek
|
|
407
|
+
);
|
|
408
|
+
try {
|
|
409
|
+
return import_node_crypto.default.verify(
|
|
410
|
+
"sha256",
|
|
411
|
+
Buffer.from(payload, "utf8"),
|
|
412
|
+
{
|
|
413
|
+
key: signerPublicKeyPem,
|
|
414
|
+
...RSA_PSS_SIGN_CONFIG
|
|
415
|
+
},
|
|
416
|
+
Buffer.from(wrappedDekSignature, "base64")
|
|
417
|
+
);
|
|
418
|
+
} catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function normalizeVaultSummaryCheckpoint(checkpoint) {
|
|
423
|
+
return {
|
|
424
|
+
vaultId: checkpoint.vaultId,
|
|
425
|
+
version: checkpoint.version,
|
|
426
|
+
name: checkpoint.name,
|
|
427
|
+
dataClassification: checkpoint.dataClassification ?? null,
|
|
428
|
+
currentDekVersion: checkpoint.currentDekVersion ?? null,
|
|
429
|
+
items: [...checkpoint.items].map((item) => ({
|
|
430
|
+
id: item.id,
|
|
431
|
+
name: item.name,
|
|
432
|
+
type: item.type ?? null,
|
|
433
|
+
websites: [...item.websites],
|
|
434
|
+
groupId: item.groupId ?? null
|
|
435
|
+
})).sort((left, right) => left.id.localeCompare(right.id)),
|
|
436
|
+
groups: [...checkpoint.groups].map((group) => ({
|
|
437
|
+
id: group.id,
|
|
438
|
+
name: group.name,
|
|
439
|
+
parentId: group.parentId ?? null
|
|
440
|
+
})).sort((left, right) => left.id.localeCompare(right.id))
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function canonicalizeVaultSummaryCheckpoint(checkpoint) {
|
|
444
|
+
return JSON.stringify(normalizeVaultSummaryCheckpoint(checkpoint));
|
|
445
|
+
}
|
|
446
|
+
function buildVaultSummaryCheckpointPayload(checkpoint) {
|
|
447
|
+
return getCheckpointFingerprint(
|
|
448
|
+
VAULT_SUMMARY_CHECKPOINT_PREFIX,
|
|
449
|
+
canonicalizeVaultSummaryCheckpoint(checkpoint)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
function verifyVaultSummaryCheckpoint(checkpoint, signature, publicKeyPem) {
|
|
453
|
+
try {
|
|
454
|
+
return import_node_crypto.default.verify(
|
|
455
|
+
"sha256",
|
|
456
|
+
Buffer.from(buildVaultSummaryCheckpointPayload(checkpoint), "utf8"),
|
|
457
|
+
{
|
|
458
|
+
key: publicKeyPem,
|
|
459
|
+
...RSA_PSS_SIGN_CONFIG
|
|
460
|
+
},
|
|
461
|
+
Buffer.from(signature, "base64")
|
|
462
|
+
);
|
|
463
|
+
} catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function normalizeVaultItemDetailCheckpoint(checkpoint) {
|
|
468
|
+
return {
|
|
469
|
+
vaultItemId: checkpoint.vaultItemId,
|
|
470
|
+
vaultId: checkpoint.vaultId,
|
|
471
|
+
version: checkpoint.version,
|
|
472
|
+
name: checkpoint.name,
|
|
473
|
+
type: checkpoint.type ?? null,
|
|
474
|
+
websites: [...checkpoint.websites],
|
|
475
|
+
groupId: checkpoint.groupId ?? null,
|
|
476
|
+
fields: [...checkpoint.fields].map((field) => ({
|
|
477
|
+
id: field.id,
|
|
478
|
+
name: field.name,
|
|
479
|
+
type: field.type,
|
|
480
|
+
order: field.order,
|
|
481
|
+
fieldInstanceIds: [...field.fieldInstanceIds].sort(),
|
|
482
|
+
assetIds: [...field.assetIds].sort()
|
|
483
|
+
})).sort((left, right) => left.order - right.order || left.id.localeCompare(right.id))
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function canonicalizeVaultItemDetailCheckpoint(checkpoint) {
|
|
487
|
+
return JSON.stringify(normalizeVaultItemDetailCheckpoint(checkpoint));
|
|
488
|
+
}
|
|
489
|
+
function buildVaultItemDetailCheckpointPayload(checkpoint) {
|
|
490
|
+
return getCheckpointFingerprint(
|
|
491
|
+
VAULT_ITEM_DETAIL_CHECKPOINT_PREFIX,
|
|
492
|
+
canonicalizeVaultItemDetailCheckpoint(checkpoint)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
function verifyVaultItemDetailCheckpoint(checkpoint, signature, publicKeyPem) {
|
|
496
|
+
try {
|
|
497
|
+
return import_node_crypto.default.verify(
|
|
498
|
+
"sha256",
|
|
499
|
+
Buffer.from(buildVaultItemDetailCheckpointPayload(checkpoint), "utf8"),
|
|
500
|
+
{
|
|
501
|
+
key: publicKeyPem,
|
|
502
|
+
...RSA_PSS_SIGN_CONFIG
|
|
503
|
+
},
|
|
504
|
+
Buffer.from(signature, "base64")
|
|
505
|
+
);
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function isVaultEnvelope(value) {
|
|
511
|
+
if (!value.startsWith("{")) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const parsed = JSON.parse(value);
|
|
516
|
+
return parsed.v === 3;
|
|
517
|
+
} catch {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function decryptWithVaultDEK(encryptedValue, dek) {
|
|
522
|
+
if (!isVaultEnvelope(encryptedValue)) {
|
|
523
|
+
throw new Error("Invalid encrypted value: expected v3 vault envelope format");
|
|
524
|
+
}
|
|
525
|
+
const envelope = JSON.parse(encryptedValue);
|
|
526
|
+
const decipher = import_node_crypto.default.createDecipheriv("aes-256-gcm", dek, Buffer.from(envelope.iv, "base64"));
|
|
527
|
+
decipher.setAuthTag(Buffer.from(envelope.t, "base64"));
|
|
528
|
+
const decrypted = Buffer.concat([
|
|
529
|
+
decipher.update(Buffer.from(envelope.d, "base64")),
|
|
530
|
+
decipher.final()
|
|
531
|
+
]);
|
|
532
|
+
return decrypted.toString("utf8");
|
|
533
|
+
}
|
|
534
|
+
function decryptStoredFieldValue(value, dek) {
|
|
535
|
+
return isVaultEnvelope(value) ? decryptWithVaultDEK(value, dek) : value;
|
|
536
|
+
}
|
|
537
|
+
function unwrapDEKWithPrivateKey(wrappedDek, privateKeyPem) {
|
|
538
|
+
return import_node_crypto.default.privateDecrypt(
|
|
539
|
+
{ key: privateKeyPem, ...RSA_OAEP_CONFIG },
|
|
540
|
+
Buffer.from(wrappedDek, "base64")
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/trust-store.ts
|
|
545
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
546
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
547
|
+
function loadTrustStore(trustStorePath) {
|
|
548
|
+
try {
|
|
549
|
+
const raw = import_node_fs2.default.readFileSync(trustStorePath, "utf8");
|
|
550
|
+
const parsed = JSON.parse(raw);
|
|
551
|
+
return {
|
|
552
|
+
version: 1,
|
|
553
|
+
userKeyPins: parsed.userKeyPins ?? {},
|
|
554
|
+
checkpointVersionPins: parsed.checkpointVersionPins ?? {},
|
|
555
|
+
transparencyHeadPins: parsed.transparencyHeadPins ?? {}
|
|
556
|
+
};
|
|
557
|
+
} catch {
|
|
558
|
+
return {
|
|
559
|
+
version: 1,
|
|
560
|
+
userKeyPins: {},
|
|
561
|
+
checkpointVersionPins: {},
|
|
562
|
+
transparencyHeadPins: {}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function saveTrustStore(trustStorePath, store) {
|
|
567
|
+
import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(trustStorePath), { recursive: true });
|
|
568
|
+
import_node_fs2.default.writeFileSync(trustStorePath, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
569
|
+
}
|
|
570
|
+
function getPinStorageKey(orgId, orgUserId) {
|
|
571
|
+
return `${orgId}:${orgUserId}`;
|
|
572
|
+
}
|
|
573
|
+
function getDirectoryPinStorageKey(orgId) {
|
|
574
|
+
return `org:${orgId}`;
|
|
575
|
+
}
|
|
576
|
+
async function fetchWitnessArtifact(pathName) {
|
|
577
|
+
const response = await fetch(
|
|
578
|
+
buildTransparencyWitnessUrl(DEFAULT_TRANSPARENCY_WITNESS_URL, pathName),
|
|
579
|
+
{
|
|
580
|
+
cache: "no-store"
|
|
581
|
+
}
|
|
582
|
+
);
|
|
583
|
+
if (!response.ok) {
|
|
584
|
+
throw new Error(`Failed to fetch public transparency witness artifact (${response.status}).`);
|
|
585
|
+
}
|
|
586
|
+
return response.json();
|
|
587
|
+
}
|
|
588
|
+
function getSinglePinnedTransparencyHead(trustStorePath) {
|
|
589
|
+
const store = loadTrustStore(trustStorePath);
|
|
590
|
+
const heads = Object.values(store.transparencyHeadPins);
|
|
591
|
+
return heads.length === 1 ? heads[0] : null;
|
|
592
|
+
}
|
|
593
|
+
async function getPublicOrgWitnessHead(apiBaseUrl, orgId) {
|
|
594
|
+
if (!shouldUseDefaultTransparencyWitness(apiBaseUrl)) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const artifact = await fetchWitnessArtifact(
|
|
598
|
+
buildOrgUserKeyDirectoryWitnessPath(orgId)
|
|
599
|
+
);
|
|
600
|
+
if (artifact.kind !== "org-user-key-directory" || artifact.orgId !== orgId || !verifyOrgUserKeyDirectoryWitnessArtifact(
|
|
601
|
+
artifact,
|
|
602
|
+
TRANSPARENCY_WITNESS_ROOT_PUBLIC_KEY_PEM
|
|
603
|
+
)) {
|
|
604
|
+
throw new Error(`Public transparency witness verification failed for org ${orgId}.`);
|
|
605
|
+
}
|
|
606
|
+
return artifact.head;
|
|
607
|
+
}
|
|
608
|
+
async function pinVaultUserPublicKeys(trustStorePath, orgId, publicKeys) {
|
|
609
|
+
const store = loadTrustStore(trustStorePath);
|
|
610
|
+
let changed = false;
|
|
611
|
+
for (const key of publicKeys) {
|
|
612
|
+
const storageKey = getPinStorageKey(orgId, key.orgUserId);
|
|
613
|
+
const computedFingerprint = getPublicKeyFingerprint(key.publicKey);
|
|
614
|
+
if (computedFingerprint !== key.fingerprint) {
|
|
615
|
+
throw new Error(`Server returned a mismatched fingerprint for user ${key.orgUserId}.`);
|
|
616
|
+
}
|
|
617
|
+
const existing = store.userKeyPins[storageKey];
|
|
618
|
+
const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
619
|
+
if (!existing) {
|
|
620
|
+
store.userKeyPins[storageKey] = {
|
|
621
|
+
keyPairId: key.userKeyPairId,
|
|
622
|
+
fingerprint: key.fingerprint,
|
|
623
|
+
publicKey: key.publicKey,
|
|
624
|
+
pinnedAt: verifiedAt,
|
|
625
|
+
verifiedAt
|
|
626
|
+
};
|
|
627
|
+
changed = true;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (existing.keyPairId === key.userKeyPairId) {
|
|
631
|
+
if (existing.fingerprint !== key.fingerprint || existing.publicKey !== key.publicKey) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`Pinned public key ${key.userKeyPairId} changed unexpectedly for user ${key.orgUserId}.`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (existing.verifiedAt !== verifiedAt) {
|
|
637
|
+
store.userKeyPins[storageKey] = {
|
|
638
|
+
...existing,
|
|
639
|
+
verifiedAt
|
|
640
|
+
};
|
|
641
|
+
changed = true;
|
|
642
|
+
}
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (!key.previousUserKeyPairId || key.previousUserKeyPairId !== existing.keyPairId || !key.rotationSignature) {
|
|
646
|
+
throw new Error(`Public key rotation for user ${key.orgUserId} is missing a trusted continuity proof.`);
|
|
647
|
+
}
|
|
648
|
+
const rotationVerified = verifyUserKeyRotation(
|
|
649
|
+
existing.keyPairId,
|
|
650
|
+
key.publicKey,
|
|
651
|
+
key.rotationSignature,
|
|
652
|
+
existing.publicKey
|
|
653
|
+
);
|
|
654
|
+
if (!rotationVerified) {
|
|
655
|
+
throw new Error(`Public key rotation for user ${key.orgUserId} failed signature verification.`);
|
|
656
|
+
}
|
|
657
|
+
store.userKeyPins[storageKey] = {
|
|
658
|
+
keyPairId: key.userKeyPairId,
|
|
659
|
+
fingerprint: key.fingerprint,
|
|
660
|
+
publicKey: key.publicKey,
|
|
661
|
+
pinnedAt: existing.pinnedAt,
|
|
662
|
+
verifiedAt
|
|
663
|
+
};
|
|
664
|
+
changed = true;
|
|
665
|
+
}
|
|
666
|
+
if (changed) {
|
|
667
|
+
saveTrustStore(trustStorePath, store);
|
|
668
|
+
}
|
|
669
|
+
return publicKeys;
|
|
670
|
+
}
|
|
671
|
+
async function verifySignedUserKeyDirectory(trustStorePath, directory, anchorHead) {
|
|
672
|
+
if (!directory.directoryCheckpoint) {
|
|
673
|
+
if (directory.publicKeys.length > 0) {
|
|
674
|
+
throw new Error("Server omitted the user-key directory checkpoint for a non-empty vault signer directory.");
|
|
675
|
+
}
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
const { directoryCheckpoint } = directory;
|
|
679
|
+
const orgId = directoryCheckpoint.checkpoint.orgId;
|
|
680
|
+
if (!directoryCheckpoint.signerOrgUserId || !directoryCheckpoint.signerPublicKey) {
|
|
681
|
+
throw new Error("Server returned an incomplete user-key directory signer payload.");
|
|
682
|
+
}
|
|
683
|
+
const signerFingerprint = getPublicKeyFingerprint(directoryCheckpoint.signerPublicKey);
|
|
684
|
+
const signerEntry = directoryCheckpoint.checkpoint.entries.find(
|
|
685
|
+
(entry) => entry.userKeyPairId === directoryCheckpoint.signerUserKeyPairId && entry.orgUserId === directoryCheckpoint.signerOrgUserId
|
|
686
|
+
);
|
|
687
|
+
const signerKey = {
|
|
688
|
+
userKeyPairId: directoryCheckpoint.signerUserKeyPairId,
|
|
689
|
+
orgUserId: directoryCheckpoint.signerOrgUserId,
|
|
690
|
+
publicKey: directoryCheckpoint.signerPublicKey,
|
|
691
|
+
fingerprint: signerFingerprint,
|
|
692
|
+
previousUserKeyPairId: signerEntry?.previousUserKeyPairId ?? null,
|
|
693
|
+
rotationSignature: signerEntry?.rotationSignature ?? null
|
|
694
|
+
};
|
|
695
|
+
const storeBeforeVerification = loadTrustStore(trustStorePath);
|
|
696
|
+
try {
|
|
697
|
+
await pinVaultUserPublicKeys(trustStorePath, orgId, [signerKey]);
|
|
698
|
+
const verified = verifyUserKeyDirectoryCheckpoint(
|
|
699
|
+
directoryCheckpoint.checkpoint,
|
|
700
|
+
directoryCheckpoint.signature,
|
|
701
|
+
directoryCheckpoint.signerPublicKey
|
|
702
|
+
);
|
|
703
|
+
if (!verified) {
|
|
704
|
+
throw new Error(`User-key directory signature verification failed for org ${orgId}.`);
|
|
705
|
+
}
|
|
706
|
+
if (!directory.transparency) {
|
|
707
|
+
throw new Error(`Server omitted the user-key directory transparency proof for org ${orgId}.`);
|
|
708
|
+
}
|
|
709
|
+
const store = loadTrustStore(trustStorePath);
|
|
710
|
+
const pinnedTransparencyHead = store.transparencyHeadPins[getDirectoryPinStorageKey(orgId)] ?? null;
|
|
711
|
+
const trustedPreviousHead = anchorHead ?? pinnedTransparencyHead;
|
|
712
|
+
const legacyPinnedVersion = store.checkpointVersionPins[getDirectoryPinStorageKey(orgId)] ?? null;
|
|
713
|
+
if (!trustedPreviousHead && legacyPinnedVersion !== null && directory.transparency.head.version < legacyPinnedVersion) {
|
|
714
|
+
throw new Error(`User-key transparency head rolled back unexpectedly for org ${orgId}.`);
|
|
715
|
+
}
|
|
716
|
+
if (!trustedPreviousHead && directory.transparency.entries.length === 0) {
|
|
717
|
+
throw new Error(`Server omitted the current transparency entry for org ${orgId}.`);
|
|
718
|
+
}
|
|
719
|
+
if (directory.transparency.entries.length > 0) {
|
|
720
|
+
const currentProofEntry = directory.transparency.entries[directory.transparency.entries.length - 1];
|
|
721
|
+
if (!currentProofEntry) {
|
|
722
|
+
throw new Error(`Server returned an empty transparency proof for org ${orgId}.`);
|
|
723
|
+
}
|
|
724
|
+
const expectedCurrentEntry = buildUserKeyDirectoryTransparencyEntry({
|
|
725
|
+
checkpoint: directoryCheckpoint.checkpoint,
|
|
726
|
+
signerUserKeyPairId: directoryCheckpoint.signerUserKeyPairId,
|
|
727
|
+
signerOrgUserId: directoryCheckpoint.signerOrgUserId,
|
|
728
|
+
signerPublicKey: directoryCheckpoint.signerPublicKey,
|
|
729
|
+
signature: directoryCheckpoint.signature,
|
|
730
|
+
previousEntryHash: currentProofEntry.previousEntryHash ?? null
|
|
731
|
+
});
|
|
732
|
+
if (expectedCurrentEntry.entryHash !== currentProofEntry.entryHash) {
|
|
733
|
+
throw new Error(`User-key transparency entry does not match the signed directory for org ${orgId}.`);
|
|
734
|
+
}
|
|
735
|
+
if (anchorHead && directory.transparency.head.version === anchorHead.version) {
|
|
736
|
+
if (directory.transparency.head.hash !== anchorHead.hash) {
|
|
737
|
+
throw new Error(`Public transparency witness head fork detected for org ${orgId}.`);
|
|
738
|
+
}
|
|
739
|
+
if (currentProofEntry.entryHash !== anchorHead.hash || expectedCurrentEntry.entryHash !== anchorHead.hash) {
|
|
740
|
+
throw new Error(`User-key transparency witness anchor mismatch for org ${orgId}.`);
|
|
741
|
+
}
|
|
742
|
+
} else if (!verifyUserKeyDirectoryTransparencyProof({
|
|
743
|
+
currentEntry: expectedCurrentEntry,
|
|
744
|
+
proof: directory.transparency,
|
|
745
|
+
previousHead: trustedPreviousHead
|
|
746
|
+
})) {
|
|
747
|
+
throw new Error(`User-key transparency proof verification failed for org ${orgId}.`);
|
|
748
|
+
}
|
|
749
|
+
} else if (anchorHead && directory.transparency.head.version === anchorHead.version) {
|
|
750
|
+
throw new Error(`Server omitted the current transparency entry required to verify org ${orgId} against the public witness.`);
|
|
751
|
+
} else if (!trustedPreviousHead || trustedPreviousHead.version !== directory.transparency.head.version || trustedPreviousHead.hash !== directory.transparency.head.hash) {
|
|
752
|
+
throw new Error(`Server returned an incomplete user-key transparency proof for org ${orgId}.`);
|
|
753
|
+
}
|
|
754
|
+
assertAndPinTransparencyHead(trustStorePath, orgId, directory.transparency.head);
|
|
755
|
+
const checkpointEntries = new Map(
|
|
756
|
+
directoryCheckpoint.checkpoint.entries.map((entry) => [entry.userKeyPairId, entry])
|
|
757
|
+
);
|
|
758
|
+
for (const key of directory.publicKeys) {
|
|
759
|
+
const entry = checkpointEntries.get(key.userKeyPairId);
|
|
760
|
+
if (!entry) {
|
|
761
|
+
throw new Error(`User key ${key.userKeyPairId} is missing from the signed org directory.`);
|
|
762
|
+
}
|
|
763
|
+
if (entry.orgUserId !== key.orgUserId || entry.fingerprint !== key.fingerprint || entry.previousUserKeyPairId !== key.previousUserKeyPairId || entry.rotationSignature !== key.rotationSignature) {
|
|
764
|
+
throw new Error(`User key ${key.userKeyPairId} does not match the signed org directory.`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return orgId;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
saveTrustStore(trustStorePath, storeBeforeVerification);
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function verifyAndPinVaultUserPublicKeys(trustStorePath, directory, anchorHead) {
|
|
774
|
+
const orgId = await verifySignedUserKeyDirectory(trustStorePath, directory, anchorHead);
|
|
775
|
+
if (!orgId) {
|
|
776
|
+
return directory.publicKeys;
|
|
777
|
+
}
|
|
778
|
+
return pinVaultUserPublicKeys(trustStorePath, orgId, directory.publicKeys);
|
|
779
|
+
}
|
|
780
|
+
function assertAndPinCheckpointVersion(trustStorePath, storageKey, version) {
|
|
781
|
+
const store = loadTrustStore(trustStorePath);
|
|
782
|
+
const pinnedVersion = store.checkpointVersionPins[storageKey];
|
|
783
|
+
if (pinnedVersion !== void 0 && version < pinnedVersion) {
|
|
784
|
+
throw new Error(`Checkpoint version rolled back unexpectedly for ${storageKey}.`);
|
|
785
|
+
}
|
|
786
|
+
if (pinnedVersion === void 0 || version > pinnedVersion) {
|
|
787
|
+
store.checkpointVersionPins[storageKey] = version;
|
|
788
|
+
saveTrustStore(trustStorePath, store);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function assertAndPinTransparencyHead(trustStorePath, orgId, head) {
|
|
792
|
+
const store = loadTrustStore(trustStorePath);
|
|
793
|
+
const storageKey = getDirectoryPinStorageKey(orgId);
|
|
794
|
+
const pinnedHead = store.transparencyHeadPins[storageKey];
|
|
795
|
+
if (pinnedHead) {
|
|
796
|
+
if (head.version < pinnedHead.version) {
|
|
797
|
+
throw new Error(`User-key transparency head rolled back unexpectedly for org ${orgId}.`);
|
|
798
|
+
}
|
|
799
|
+
if (head.version === pinnedHead.version && head.hash !== pinnedHead.hash) {
|
|
800
|
+
throw new Error(`User-key transparency head fork detected for org ${orgId}.`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (!pinnedHead || head.version > pinnedHead.version) {
|
|
804
|
+
store.transparencyHeadPins[storageKey] = head;
|
|
805
|
+
saveTrustStore(trustStorePath, store);
|
|
806
|
+
}
|
|
807
|
+
assertAndPinCheckpointVersion(trustStorePath, storageKey, head.version);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/index.ts
|
|
811
|
+
var R4_DEFAULT_API_BASE_URL = "https://r4.dev";
|
|
812
|
+
var R4_DEV_API_BASE_URL = "https://dev.r4.dev";
|
|
813
|
+
function toScreamingSnakeCase(input) {
|
|
814
|
+
return input.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").toUpperCase();
|
|
815
|
+
}
|
|
816
|
+
function resolveTrustStorePath(config) {
|
|
817
|
+
if (config.trustStorePath) {
|
|
818
|
+
return import_node_path3.default.resolve(config.trustStorePath);
|
|
819
|
+
}
|
|
820
|
+
if (config.privateKeyPath) {
|
|
821
|
+
return `${import_node_path3.default.resolve(config.privateKeyPath)}.trust.json`;
|
|
822
|
+
}
|
|
823
|
+
return import_node_path3.default.resolve(process.cwd(), ".r4-trust-store.json");
|
|
824
|
+
}
|
|
825
|
+
function resolveApiBaseUrl(config) {
|
|
826
|
+
if (config.baseUrl) {
|
|
827
|
+
return config.baseUrl;
|
|
828
|
+
}
|
|
829
|
+
return config.dev ? R4_DEV_API_BASE_URL : R4_DEFAULT_API_BASE_URL;
|
|
830
|
+
}
|
|
831
|
+
function buildVaultSummaryCheckpointFromListResponse(response, version) {
|
|
832
|
+
return {
|
|
833
|
+
vaultId: response.vaultId,
|
|
834
|
+
version,
|
|
835
|
+
name: response.vaultName,
|
|
836
|
+
dataClassification: response.dataClassification ?? null,
|
|
837
|
+
currentDekVersion: response.currentDekVersion ?? null,
|
|
838
|
+
items: response.items.map((item) => ({
|
|
839
|
+
id: item.id,
|
|
840
|
+
name: item.name,
|
|
841
|
+
type: item.type ?? null,
|
|
842
|
+
websites: item.websites ?? [],
|
|
843
|
+
groupId: item.groupId ?? null
|
|
844
|
+
})),
|
|
845
|
+
groups: response.vaultItemGroups.map((group) => ({
|
|
846
|
+
id: group.id,
|
|
847
|
+
name: group.name,
|
|
848
|
+
parentId: group.parentId ?? null
|
|
849
|
+
}))
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function buildVaultItemDetailCheckpointFromResponse(item, version) {
|
|
853
|
+
return {
|
|
854
|
+
vaultItemId: item.id,
|
|
855
|
+
vaultId: item.vaultId,
|
|
856
|
+
version,
|
|
857
|
+
name: item.name,
|
|
858
|
+
type: item.type ?? null,
|
|
859
|
+
websites: item.websites ?? [],
|
|
860
|
+
groupId: item.groupId ?? null,
|
|
861
|
+
fields: item.fields.map((field, index) => ({
|
|
862
|
+
id: field.id,
|
|
863
|
+
name: field.name,
|
|
864
|
+
type: field.type,
|
|
865
|
+
order: field.order ?? index,
|
|
866
|
+
fieldInstanceIds: field.fieldInstanceIds ?? [],
|
|
867
|
+
assetIds: field.assetIds ?? []
|
|
868
|
+
}))
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
var R4 = class _R4 {
|
|
872
|
+
client;
|
|
873
|
+
baseUrl;
|
|
874
|
+
projectId;
|
|
875
|
+
privateKeyPem;
|
|
876
|
+
publicKeyPem;
|
|
877
|
+
trustStorePath;
|
|
878
|
+
_env = null;
|
|
879
|
+
constructor(config) {
|
|
880
|
+
if (!config.apiKey) {
|
|
881
|
+
throw new Error("R4 SDK: apiKey is required");
|
|
882
|
+
}
|
|
883
|
+
if (!config.privateKey && !config.privateKeyPath) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
"R4 SDK: privateKey or privateKeyPath is required for zero-trust local decryption."
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const baseUrl = resolveApiBaseUrl(config);
|
|
889
|
+
this.baseUrl = baseUrl;
|
|
890
|
+
this.client = new R4Client(config.apiKey, baseUrl);
|
|
891
|
+
this.projectId = config.projectId;
|
|
892
|
+
this.privateKeyPem = config.privateKey ?? loadPrivateKey(config.privateKeyPath);
|
|
893
|
+
this.publicKeyPem = derivePublicKey(this.privateKeyPem);
|
|
894
|
+
this.trustStorePath = resolveTrustStorePath(config);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Static factory method that creates and initializes an R4 instance.
|
|
898
|
+
*/
|
|
899
|
+
static async create(config) {
|
|
900
|
+
const instance = new _R4(config);
|
|
901
|
+
await instance.init();
|
|
902
|
+
return instance;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Initializes the SDK by registering the agent public key (idempotent) and
|
|
906
|
+
* decrypting all accessible vault values locally into a flat env map.
|
|
907
|
+
*/
|
|
908
|
+
async init() {
|
|
909
|
+
this._env = await this.fetchEnv();
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Returns the locally decrypted env map.
|
|
913
|
+
*/
|
|
914
|
+
get env() {
|
|
915
|
+
if (!this._env) {
|
|
916
|
+
throw new Error(
|
|
917
|
+
"R4 SDK: env is not initialized. Call await r4.init() first, or use R4.create() for automatic initialization."
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
return this._env;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Re-fetches and locally re-decrypts the current vault view.
|
|
924
|
+
*/
|
|
925
|
+
async refresh() {
|
|
926
|
+
this._env = await this.fetchEnv();
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Registers the local agent public key, loads all accessible vaults, verifies
|
|
930
|
+
* wrapped-DEK signatures against pinned signer keys, unwraps each vault DEK,
|
|
931
|
+
* and builds a flat SCREAMING_SNAKE_CASE env map from decrypted field values.
|
|
932
|
+
*/
|
|
933
|
+
async fetchEnv() {
|
|
934
|
+
try {
|
|
935
|
+
await this.client.registerAgentPublicKey({
|
|
936
|
+
publicKey: this.publicKeyPem
|
|
937
|
+
});
|
|
938
|
+
} catch (error) {
|
|
939
|
+
throw new Error(
|
|
940
|
+
`R4 SDK: failed to register the local agent public key. The zero-trust SDK requires an AGENT-scoped API key and a matching local private key. ${error instanceof Error ? error.message : String(error)}`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const { vaults } = await this.client.listVaults(this.projectId);
|
|
944
|
+
const envEntries = await Promise.all(vaults.map((vault) => this.fetchVaultEnv(vault.id)));
|
|
945
|
+
return Object.assign({}, ...envEntries);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Fetches a single vault's wrapped DEK, verifies it against the pinned signer
|
|
949
|
+
* directory, unwraps the DEK locally, then decrypts every field value in that
|
|
950
|
+
* vault item-by-item.
|
|
951
|
+
*/
|
|
952
|
+
async fetchVaultEnv(vaultId) {
|
|
953
|
+
const pinnedTransparencyHead = getSinglePinnedTransparencyHead(this.trustStorePath);
|
|
954
|
+
const [wrappedKey, itemsResponse, initialPublicKeyDirectory] = await Promise.all([
|
|
955
|
+
this.client.getAgentWrappedKey(vaultId),
|
|
956
|
+
this.client.listVaultItems(vaultId),
|
|
957
|
+
this.client.getVaultUserKeyDirectory(
|
|
958
|
+
vaultId,
|
|
959
|
+
pinnedTransparencyHead ? {
|
|
960
|
+
knownTransparencyVersion: pinnedTransparencyHead.version,
|
|
961
|
+
knownTransparencyHash: pinnedTransparencyHead.hash
|
|
962
|
+
} : void 0
|
|
963
|
+
)
|
|
964
|
+
]);
|
|
965
|
+
let publicKeyDirectory = initialPublicKeyDirectory;
|
|
966
|
+
let witnessAnchorHead = null;
|
|
967
|
+
if (!pinnedTransparencyHead) {
|
|
968
|
+
const orgId = initialPublicKeyDirectory.directoryCheckpoint?.checkpoint.orgId ?? null;
|
|
969
|
+
if (orgId && initialPublicKeyDirectory.directoryCheckpoint && initialPublicKeyDirectory.transparency) {
|
|
970
|
+
witnessAnchorHead = await getPublicOrgWitnessHead(this.baseUrl, orgId);
|
|
971
|
+
if (witnessAnchorHead) {
|
|
972
|
+
if (initialPublicKeyDirectory.transparency.head.version < witnessAnchorHead.version) {
|
|
973
|
+
throw new Error(`R4 SDK: public transparency witness head is ahead of the server response for org ${orgId}.`);
|
|
974
|
+
}
|
|
975
|
+
if (initialPublicKeyDirectory.transparency.head.version === witnessAnchorHead.version) {
|
|
976
|
+
if (initialPublicKeyDirectory.transparency.head.hash !== witnessAnchorHead.hash) {
|
|
977
|
+
throw new Error(`R4 SDK: public transparency witness head fork detected for org ${orgId}.`);
|
|
978
|
+
}
|
|
979
|
+
} else {
|
|
980
|
+
publicKeyDirectory = await this.client.getVaultUserKeyDirectory(vaultId, {
|
|
981
|
+
knownTransparencyVersion: witnessAnchorHead.version,
|
|
982
|
+
knownTransparencyHash: witnessAnchorHead.hash
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const trustedPublicKeys = await verifyAndPinVaultUserPublicKeys(
|
|
989
|
+
this.trustStorePath,
|
|
990
|
+
publicKeyDirectory,
|
|
991
|
+
witnessAnchorHead
|
|
992
|
+
);
|
|
993
|
+
const signerKey = trustedPublicKeys.find(
|
|
994
|
+
(publicKey) => publicKey.userKeyPairId === wrappedKey.signerUserKeyPairId
|
|
995
|
+
);
|
|
996
|
+
if (!signerKey) {
|
|
997
|
+
throw new Error(
|
|
998
|
+
`R4 SDK: wrapped DEK for vault ${vaultId} was signed by unknown user key ${wrappedKey.signerUserKeyPairId}.`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
const signatureVerified = verifyWrappedDekSignature(
|
|
1002
|
+
vaultId,
|
|
1003
|
+
wrappedKey.encryptionKeyId,
|
|
1004
|
+
wrappedKey.signerUserKeyPairId,
|
|
1005
|
+
wrappedKey.dekVersion,
|
|
1006
|
+
wrappedKey.wrappedDek,
|
|
1007
|
+
wrappedKey.wrappedDekSignature,
|
|
1008
|
+
signerKey.publicKey
|
|
1009
|
+
);
|
|
1010
|
+
if (!signatureVerified) {
|
|
1011
|
+
throw new Error(`R4 SDK: wrapped DEK signature verification failed for vault ${vaultId}.`);
|
|
1012
|
+
}
|
|
1013
|
+
const dek = unwrapDEKWithPrivateKey(wrappedKey.wrappedDek, this.privateKeyPem);
|
|
1014
|
+
if (!itemsResponse.summaryCheckpoint) {
|
|
1015
|
+
throw new Error(`R4 SDK: vault ${vaultId} is missing a signed summary checkpoint.`);
|
|
1016
|
+
}
|
|
1017
|
+
const summarySignerKey = trustedPublicKeys.find(
|
|
1018
|
+
(publicKey) => publicKey.userKeyPairId === itemsResponse.summaryCheckpoint.signerUserKeyPairId
|
|
1019
|
+
);
|
|
1020
|
+
if (!summarySignerKey) {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
`R4 SDK: vault ${vaultId} summary checkpoint was signed by unknown user key ${itemsResponse.summaryCheckpoint.signerUserKeyPairId}.`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const expectedSummaryCheckpoint = buildVaultSummaryCheckpointFromListResponse(
|
|
1026
|
+
itemsResponse,
|
|
1027
|
+
itemsResponse.summaryCheckpoint.checkpoint.version
|
|
1028
|
+
);
|
|
1029
|
+
const summaryVerified = verifyVaultSummaryCheckpoint(
|
|
1030
|
+
expectedSummaryCheckpoint,
|
|
1031
|
+
itemsResponse.summaryCheckpoint.signature,
|
|
1032
|
+
summarySignerKey.publicKey
|
|
1033
|
+
);
|
|
1034
|
+
if (!summaryVerified) {
|
|
1035
|
+
throw new Error(`R4 SDK: vault summary checkpoint verification failed for vault ${vaultId}.`);
|
|
1036
|
+
}
|
|
1037
|
+
assertAndPinCheckpointVersion(
|
|
1038
|
+
this.trustStorePath,
|
|
1039
|
+
`summary:${vaultId}`,
|
|
1040
|
+
expectedSummaryCheckpoint.version
|
|
1041
|
+
);
|
|
1042
|
+
const itemDetails = await Promise.all(
|
|
1043
|
+
itemsResponse.items.map((item) => this.client.getVaultItemDetail(vaultId, item.id))
|
|
1044
|
+
);
|
|
1045
|
+
const env = {};
|
|
1046
|
+
for (const item of itemDetails) {
|
|
1047
|
+
if (!item.detailCheckpoint) {
|
|
1048
|
+
throw new Error(`R4 SDK: vault item ${item.id} is missing a signed detail checkpoint.`);
|
|
1049
|
+
}
|
|
1050
|
+
const detailSignerKey = trustedPublicKeys.find(
|
|
1051
|
+
(publicKey) => publicKey.userKeyPairId === item.detailCheckpoint.signerUserKeyPairId
|
|
1052
|
+
);
|
|
1053
|
+
if (!detailSignerKey) {
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
`R4 SDK: vault item ${item.id} checkpoint was signed by unknown user key ${item.detailCheckpoint.signerUserKeyPairId}.`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
const expectedDetailCheckpoint = buildVaultItemDetailCheckpointFromResponse(
|
|
1059
|
+
item,
|
|
1060
|
+
item.detailCheckpoint.checkpoint.version
|
|
1061
|
+
);
|
|
1062
|
+
const detailVerified = verifyVaultItemDetailCheckpoint(
|
|
1063
|
+
expectedDetailCheckpoint,
|
|
1064
|
+
item.detailCheckpoint.signature,
|
|
1065
|
+
detailSignerKey.publicKey
|
|
1066
|
+
);
|
|
1067
|
+
if (!detailVerified) {
|
|
1068
|
+
throw new Error(`R4 SDK: vault item checkpoint verification failed for item ${item.id}.`);
|
|
1069
|
+
}
|
|
1070
|
+
assertAndPinCheckpointVersion(
|
|
1071
|
+
this.trustStorePath,
|
|
1072
|
+
`detail:${item.id}`,
|
|
1073
|
+
expectedDetailCheckpoint.version
|
|
1074
|
+
);
|
|
1075
|
+
for (const field of item.fields) {
|
|
1076
|
+
if (field.value === null) {
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
env[toScreamingSnakeCase(`${item.name}_${field.name}`)] = decryptStoredFieldValue(
|
|
1080
|
+
field.value,
|
|
1081
|
+
dek
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return env;
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
var src_default = R4;
|
|
1089
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1090
|
+
0 && (module.exports = {
|
|
1091
|
+
R4
|
|
1092
|
+
});
|
|
1093
|
+
//# sourceMappingURL=index.cjs.map
|