@opengeni/vaults 3.1.2 → 3.2.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 +14 -0
- package/dist/{chunk-7S23LHTK.js → chunk-BD43JBPC.js} +9 -2
- package/dist/chunk-BD43JBPC.js.map +1 -0
- package/dist/{chunk-VKPGG2FW.js → chunk-BEKN77JO.js} +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +736 -2
- package/dist/index.js.map +1 -1
- package/dist/provider-capabilities.d.ts +4 -0
- package/dist/provider-pages.d.ts +56 -0
- package/dist/provider.d.ts +59 -3
- package/dist/remote-provider.d.ts +117 -0
- package/dist/testing/fake-provider.js +2 -2
- package/dist/testing/index.js +2 -2
- package/package.json +2 -2
- package/src/index.ts +3 -0
- package/src/provider-capabilities.ts +64 -0
- package/src/provider-pages.ts +206 -0
- package/src/provider.ts +84 -2
- package/src/remote-provider.ts +784 -0
- package/dist/chunk-7S23LHTK.js.map +0 -1
- /package/dist/{chunk-VKPGG2FW.js.map → chunk-BEKN77JO.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -5,7 +5,727 @@ import {
|
|
|
5
5
|
createProviderCallBudget,
|
|
6
6
|
createVaultProviderRegistry,
|
|
7
7
|
isVaultProviderError
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-BD43JBPC.js";
|
|
9
|
+
|
|
10
|
+
// src/provider-pages.ts
|
|
11
|
+
import { VAULT_LIMITS } from "@opengeni/contracts";
|
|
12
|
+
var PROVIDER_PAGE_SIZE = 100;
|
|
13
|
+
var PROVIDER_MAX_PAGES = 100;
|
|
14
|
+
function incomplete() {
|
|
15
|
+
throw new VaultProviderError(
|
|
16
|
+
"provider_inventory_incomplete",
|
|
17
|
+
"The provider inventory could not be read completely. The previous inventory was kept."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
async function traverseProviderPages(input) {
|
|
21
|
+
if (!Number.isSafeInteger(input.maxItems) || input.maxItems < 1 || input.maxItems > 1e4)
|
|
22
|
+
incomplete();
|
|
23
|
+
const items = [];
|
|
24
|
+
const ids = /* @__PURE__ */ new Set();
|
|
25
|
+
const cursors = /* @__PURE__ */ new Set();
|
|
26
|
+
let cursor;
|
|
27
|
+
let snapshot;
|
|
28
|
+
for (let count = 0; count < PROVIDER_MAX_PAGES; count++) {
|
|
29
|
+
input.signal?.throwIfAborted();
|
|
30
|
+
const limit = Math.min(PROVIDER_PAGE_SIZE, input.maxItems - items.length);
|
|
31
|
+
if (limit < 1) incomplete();
|
|
32
|
+
const page = await input.page({
|
|
33
|
+
limit,
|
|
34
|
+
...cursor !== void 0 ? { cursor } : {},
|
|
35
|
+
...snapshot != null ? { snapshot } : {}
|
|
36
|
+
});
|
|
37
|
+
input.signal?.throwIfAborted();
|
|
38
|
+
if (!page || !Array.isArray(page.items) || page.items.length > limit || page.snapshot !== null && (typeof page.snapshot !== "string" || page.snapshot.length === 0 || page.snapshot.length > 1024) || page.nextCursor !== null && (typeof page.nextCursor !== "string" || page.nextCursor.length === 0 || page.nextCursor.length > 1024))
|
|
39
|
+
incomplete();
|
|
40
|
+
if (snapshot !== void 0 && page.snapshot !== snapshot) incomplete();
|
|
41
|
+
snapshot = page.snapshot;
|
|
42
|
+
if (page.freshness && page.freshness.state !== "current") incomplete();
|
|
43
|
+
for (const item of page.items) {
|
|
44
|
+
const id = input.identity(item);
|
|
45
|
+
if (typeof id !== "string" || id.length === 0 || id.length > 300 || ids.has(id)) incomplete();
|
|
46
|
+
ids.add(id);
|
|
47
|
+
items.push(item);
|
|
48
|
+
}
|
|
49
|
+
if (page.nextCursor === null) return { items, consistent: count === 0 || snapshot !== null };
|
|
50
|
+
if (items.length >= input.maxItems || cursors.has(page.nextCursor)) incomplete();
|
|
51
|
+
cursors.add(page.nextCursor);
|
|
52
|
+
cursor = page.nextCursor;
|
|
53
|
+
}
|
|
54
|
+
return incomplete();
|
|
55
|
+
}
|
|
56
|
+
async function collectProviderPages(input) {
|
|
57
|
+
return (await traverseProviderPages(input)).items;
|
|
58
|
+
}
|
|
59
|
+
function validateLegacyInventory(items, identity, max) {
|
|
60
|
+
if (!Array.isArray(items) || items.length > max) incomplete();
|
|
61
|
+
const ids = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const id = identity(item);
|
|
64
|
+
if (typeof id !== "string" || !id || id.length > 300 || ids.has(id)) incomplete();
|
|
65
|
+
ids.add(id);
|
|
66
|
+
}
|
|
67
|
+
return items;
|
|
68
|
+
}
|
|
69
|
+
async function readProviderVaultInventory(provider, ctx) {
|
|
70
|
+
const identity = (vault) => vault.externalVaultId;
|
|
71
|
+
if (provider.listVaultPage) {
|
|
72
|
+
return traverseProviderPages({
|
|
73
|
+
page: async (page) => {
|
|
74
|
+
const result = await provider.listVaultPage(ctx, page);
|
|
75
|
+
if (result.freshness) ctx.onFreshness?.(result.freshness);
|
|
76
|
+
return result;
|
|
77
|
+
},
|
|
78
|
+
identity,
|
|
79
|
+
maxItems: VAULT_LIMITS.vaultsPerOwnerScope,
|
|
80
|
+
...ctx.signal ? { signal: ctx.signal } : {}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
ctx.signal?.throwIfAborted();
|
|
84
|
+
const items = await provider.listVaults(ctx);
|
|
85
|
+
ctx.signal?.throwIfAborted();
|
|
86
|
+
return {
|
|
87
|
+
items: validateLegacyInventory(items, identity, VAULT_LIMITS.vaultsPerOwnerScope),
|
|
88
|
+
consistent: true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function listProviderVaults(provider, ctx) {
|
|
92
|
+
return (await readProviderVaultInventory(provider, ctx)).items;
|
|
93
|
+
}
|
|
94
|
+
async function readProviderItemInventory(provider, ctx, externalVaultId) {
|
|
95
|
+
const identity = (item) => item.externalItemId;
|
|
96
|
+
if (provider.listItemPage) {
|
|
97
|
+
return traverseProviderPages({
|
|
98
|
+
page: async (page) => {
|
|
99
|
+
const result = await provider.listItemPage(ctx, externalVaultId, page);
|
|
100
|
+
if (result.freshness) ctx.onFreshness?.(result.freshness);
|
|
101
|
+
return result;
|
|
102
|
+
},
|
|
103
|
+
identity,
|
|
104
|
+
maxItems: VAULT_LIMITS.itemsPerVault,
|
|
105
|
+
...ctx.signal ? { signal: ctx.signal } : {}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
ctx.signal?.throwIfAborted();
|
|
109
|
+
const items = await provider.listItems(ctx, externalVaultId);
|
|
110
|
+
ctx.signal?.throwIfAborted();
|
|
111
|
+
return {
|
|
112
|
+
items: validateLegacyInventory(items, identity, VAULT_LIMITS.itemsPerVault),
|
|
113
|
+
consistent: true
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function listProviderItems(provider, ctx, externalVaultId) {
|
|
117
|
+
return (await readProviderItemInventory(provider, ctx, externalVaultId)).items;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/provider-capabilities.ts
|
|
121
|
+
function providerOperations(capabilities, vault) {
|
|
122
|
+
const base = capabilities.operations ?? {
|
|
123
|
+
createItemTypes: capabilities.write ? ["login", "api_key", "note"] : [],
|
|
124
|
+
updateItem: capabilities.write,
|
|
125
|
+
removeField: capabilities.write,
|
|
126
|
+
archiveItem: capabilities.write,
|
|
127
|
+
archiveKind: capabilities.write ? "archive" : "none",
|
|
128
|
+
deleteItem: false,
|
|
129
|
+
restoreItem: false,
|
|
130
|
+
readFiles: capabilities.files,
|
|
131
|
+
writeFiles: false,
|
|
132
|
+
createTotp: capabilities.write && capabilities.totp,
|
|
133
|
+
updateTotp: capabilities.write && capabilities.totp,
|
|
134
|
+
createVault: capabilities.createVault,
|
|
135
|
+
restoreFieldHistory: false
|
|
136
|
+
};
|
|
137
|
+
const override = vault?.operations;
|
|
138
|
+
const operations = {
|
|
139
|
+
createItemTypes: base.createItemTypes.filter(
|
|
140
|
+
(type) => type !== "passkey" && (!override || override.createItemTypes.includes(type))
|
|
141
|
+
),
|
|
142
|
+
updateItem: base.updateItem && (override?.updateItem ?? true),
|
|
143
|
+
removeField: base.removeField && (override?.removeField ?? true),
|
|
144
|
+
archiveItem: base.archiveItem && (override?.archiveItem ?? true),
|
|
145
|
+
archiveKind: base.archiveKind,
|
|
146
|
+
deleteItem: false,
|
|
147
|
+
restoreItem: base.restoreItem && (override?.restoreItem ?? true),
|
|
148
|
+
readFiles: base.readFiles && (override?.readFiles ?? true),
|
|
149
|
+
writeFiles: base.writeFiles && (override?.writeFiles ?? true),
|
|
150
|
+
createTotp: base.createTotp && (override?.createTotp ?? true),
|
|
151
|
+
updateTotp: base.updateTotp && (override?.updateTotp ?? true),
|
|
152
|
+
createVault: base.createVault && (override?.createVault ?? true),
|
|
153
|
+
restoreFieldHistory: base.restoreFieldHistory && (override?.restoreFieldHistory ?? true)
|
|
154
|
+
};
|
|
155
|
+
if (override && override.archiveKind !== base.archiveKind) operations.archiveItem = false;
|
|
156
|
+
if (operations.archiveKind === "none") operations.archiveItem = false;
|
|
157
|
+
if (!operations.archiveItem) operations.archiveKind = "none";
|
|
158
|
+
operations.removeField &&= operations.updateItem;
|
|
159
|
+
operations.updateTotp &&= operations.updateItem;
|
|
160
|
+
operations.createTotp &&= operations.createItemTypes.length > 0;
|
|
161
|
+
if (vault?.writeAccess !== "read_only" && capabilities.write) return operations;
|
|
162
|
+
return {
|
|
163
|
+
...operations,
|
|
164
|
+
createItemTypes: [],
|
|
165
|
+
updateItem: false,
|
|
166
|
+
removeField: false,
|
|
167
|
+
archiveItem: false,
|
|
168
|
+
archiveKind: "none",
|
|
169
|
+
restoreItem: false,
|
|
170
|
+
writeFiles: false,
|
|
171
|
+
createTotp: false,
|
|
172
|
+
updateTotp: false,
|
|
173
|
+
createVault: false,
|
|
174
|
+
restoreFieldHistory: false
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/remote-provider.ts
|
|
179
|
+
import {
|
|
180
|
+
VAULT_LIMITS as VAULT_LIMITS2,
|
|
181
|
+
VAULT_PROVIDER_WIRE_LIMITS,
|
|
182
|
+
VAULT_PROVIDER_WIRE_RESPONSE_SIGNATURE_HEADER,
|
|
183
|
+
VAULT_PROVIDER_WIRE_VERSION,
|
|
184
|
+
VaultProviderWireRequest,
|
|
185
|
+
parseTotpInput,
|
|
186
|
+
parseVaultProviderWireResponse,
|
|
187
|
+
vaultProviderWireResponseKey,
|
|
188
|
+
vaultProviderWireUtf8Bytes
|
|
189
|
+
} from "@opengeni/contracts";
|
|
190
|
+
import {
|
|
191
|
+
OPENGENI_SIGNATURE_HEADER,
|
|
192
|
+
signOpenGeniPayload,
|
|
193
|
+
verifyOpenGeniSignature
|
|
194
|
+
} from "@opengeni/contracts/workspace-integration-wire";
|
|
195
|
+
var RemoteVaultTransportError = class extends Error {
|
|
196
|
+
failure;
|
|
197
|
+
requestSent;
|
|
198
|
+
constructor(failure, requestSent) {
|
|
199
|
+
super(`remote vault transport failed: ${failure}`);
|
|
200
|
+
this.name = "RemoteVaultTransportError";
|
|
201
|
+
this.failure = failure;
|
|
202
|
+
this.requestSent = requestSent;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
function createFetchRemoteVaultTransport(fetchImpl) {
|
|
206
|
+
return {
|
|
207
|
+
async post(request) {
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
let failure = null;
|
|
210
|
+
const timer = setTimeout(() => {
|
|
211
|
+
failure = "timeout";
|
|
212
|
+
controller.abort();
|
|
213
|
+
}, request.timeoutMs);
|
|
214
|
+
const onAbort = () => {
|
|
215
|
+
failure ??= "aborted";
|
|
216
|
+
controller.abort();
|
|
217
|
+
};
|
|
218
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
219
|
+
if (request.signal?.aborted) onAbort();
|
|
220
|
+
try {
|
|
221
|
+
let response;
|
|
222
|
+
try {
|
|
223
|
+
response = await fetchImpl(request.url, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: { ...request.headers },
|
|
226
|
+
body: request.body,
|
|
227
|
+
redirect: "manual",
|
|
228
|
+
signal: controller.signal
|
|
229
|
+
});
|
|
230
|
+
} catch {
|
|
231
|
+
throw new RemoteVaultTransportError(failure ?? "network", true);
|
|
232
|
+
}
|
|
233
|
+
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
|
|
234
|
+
await response.body?.cancel().catch(() => {
|
|
235
|
+
});
|
|
236
|
+
throw new RemoteVaultTransportError("redirect", true);
|
|
237
|
+
}
|
|
238
|
+
const chunks = [];
|
|
239
|
+
let total = 0;
|
|
240
|
+
const reader = response.body?.getReader();
|
|
241
|
+
if (reader) {
|
|
242
|
+
try {
|
|
243
|
+
for (; ; ) {
|
|
244
|
+
const { done, value } = await reader.read();
|
|
245
|
+
if (done) break;
|
|
246
|
+
total += value.byteLength;
|
|
247
|
+
if (total > request.maxResponseBytes) {
|
|
248
|
+
await reader.cancel().catch(() => {
|
|
249
|
+
});
|
|
250
|
+
throw new RemoteVaultTransportError("too_large", true);
|
|
251
|
+
}
|
|
252
|
+
chunks.push(value);
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof RemoteVaultTransportError) throw error;
|
|
256
|
+
throw new RemoteVaultTransportError(failure ?? "network", true);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const bytes = new Uint8Array(total);
|
|
260
|
+
let offset = 0;
|
|
261
|
+
for (const chunk of chunks) {
|
|
262
|
+
bytes.set(chunk, offset);
|
|
263
|
+
offset += chunk.byteLength;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
status: response.status,
|
|
267
|
+
headers: response.headers,
|
|
268
|
+
body: new TextDecoder("utf-8", { fatal: false }).decode(bytes)
|
|
269
|
+
};
|
|
270
|
+
} finally {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
var REMOTE_VAULT_READ_ONLY_OPERATIONS = Object.freeze({
|
|
278
|
+
createItemTypes: [],
|
|
279
|
+
updateItem: false,
|
|
280
|
+
removeField: false,
|
|
281
|
+
archiveItem: false,
|
|
282
|
+
archiveKind: "none",
|
|
283
|
+
deleteItem: false,
|
|
284
|
+
restoreItem: false,
|
|
285
|
+
readFiles: false,
|
|
286
|
+
writeFiles: false,
|
|
287
|
+
createTotp: false,
|
|
288
|
+
updateTotp: false,
|
|
289
|
+
createVault: false,
|
|
290
|
+
restoreFieldHistory: false
|
|
291
|
+
});
|
|
292
|
+
function remoteVaultOperations(operations) {
|
|
293
|
+
const createItemTypes = operations.createItemTypes.filter(
|
|
294
|
+
(type) => type !== "passkey" && type !== "file"
|
|
295
|
+
);
|
|
296
|
+
const archiveItem = operations.archiveItem && operations.archiveKind !== "none";
|
|
297
|
+
return {
|
|
298
|
+
createItemTypes,
|
|
299
|
+
updateItem: operations.updateItem,
|
|
300
|
+
removeField: operations.removeField && operations.updateItem,
|
|
301
|
+
archiveItem,
|
|
302
|
+
archiveKind: archiveItem ? operations.archiveKind : "none",
|
|
303
|
+
deleteItem: false,
|
|
304
|
+
restoreItem: false,
|
|
305
|
+
readFiles: operations.readFiles,
|
|
306
|
+
writeFiles: false,
|
|
307
|
+
createTotp: operations.createTotp && createItemTypes.length > 0,
|
|
308
|
+
updateTotp: operations.updateTotp && operations.updateItem,
|
|
309
|
+
createVault: false,
|
|
310
|
+
restoreFieldHistory: false
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
var SAFE_MESSAGES = {
|
|
314
|
+
provider_auth_invalid: "The remote vault rejected Opengeni's credentials.",
|
|
315
|
+
provider_auth_expired: "The remote vault needs to be reconnected.",
|
|
316
|
+
provider_rate_limited: "The remote vault is rate limiting requests. Try again later.",
|
|
317
|
+
provider_unavailable: "The remote vault is unavailable.",
|
|
318
|
+
provider_conflict: "The item changed in the remote vault. Reload it and try again.",
|
|
319
|
+
provider_not_found: "The remote vault item or field was not found.",
|
|
320
|
+
provider_permission_denied: "The remote vault does not allow this operation.",
|
|
321
|
+
provider_locked: "The remote vault is locked. Unlock it where it runs, then refresh.",
|
|
322
|
+
provider_inventory_incomplete: "The remote vault inventory could not be read completely. The previous inventory was kept.",
|
|
323
|
+
provider_outcome_unknown: "The remote vault did not confirm the change. It may or may not have been saved; check the vault before trying again."
|
|
324
|
+
};
|
|
325
|
+
function fail(code, retryAfterMs) {
|
|
326
|
+
throw new VaultProviderError(code, SAFE_MESSAGES[code], retryAfterMs);
|
|
327
|
+
}
|
|
328
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
329
|
+
var CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
330
|
+
function validEndpoint(endpoint, allowInsecureHttp) {
|
|
331
|
+
let url;
|
|
332
|
+
try {
|
|
333
|
+
url = new URL(endpoint);
|
|
334
|
+
} catch {
|
|
335
|
+
return fail("provider_unavailable");
|
|
336
|
+
}
|
|
337
|
+
const schemeOk = url.protocol === "https:" || allowInsecureHttp && url.protocol === "http:";
|
|
338
|
+
if (!schemeOk || url.username || url.password || url.hash || endpoint.length > 2048) {
|
|
339
|
+
fail("provider_unavailable");
|
|
340
|
+
}
|
|
341
|
+
return url.toString();
|
|
342
|
+
}
|
|
343
|
+
function decodeBase64(base64) {
|
|
344
|
+
const binary = atob(base64);
|
|
345
|
+
const bytes = new Uint8Array(binary.length);
|
|
346
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
347
|
+
return bytes;
|
|
348
|
+
}
|
|
349
|
+
function retryAfterHeader(headers) {
|
|
350
|
+
const value = headers.get("retry-after");
|
|
351
|
+
if (!value || !/^\d{1,6}$/.test(value.trim())) return void 0;
|
|
352
|
+
return Math.min(Number(value.trim()) * 1e3, 864e5);
|
|
353
|
+
}
|
|
354
|
+
function toProviderItem(item) {
|
|
355
|
+
return {
|
|
356
|
+
externalItemId: item.externalItemId,
|
|
357
|
+
name: item.name,
|
|
358
|
+
type: item.type,
|
|
359
|
+
websites: [...item.websites],
|
|
360
|
+
fields: item.fields.map((field) => ({
|
|
361
|
+
externalFieldId: field.externalFieldId,
|
|
362
|
+
name: field.name,
|
|
363
|
+
kind: field.kind
|
|
364
|
+
})),
|
|
365
|
+
version: item.version,
|
|
366
|
+
updatedAt: item.updatedAt
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function toProviderVault(vault) {
|
|
370
|
+
return {
|
|
371
|
+
externalVaultId: vault.externalVaultId,
|
|
372
|
+
name: vault.name,
|
|
373
|
+
contentVersion: vault.contentVersion,
|
|
374
|
+
itemCount: vault.itemCount,
|
|
375
|
+
writeAccess: vault.writeAccess,
|
|
376
|
+
...vault.operations ? { operations: remoteVaultOperations(vault.operations) } : {}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function createRemoteVaultProvider(options) {
|
|
380
|
+
if (options.provider !== "external" && options.provider !== "bitwarden") {
|
|
381
|
+
fail("provider_unavailable");
|
|
382
|
+
}
|
|
383
|
+
const endpoint = validEndpoint(options.endpoint, options.allowInsecureHttp === true);
|
|
384
|
+
if (typeof options.signingSecret !== "string" || options.signingSecret.length < 32 || options.signingSecret.length > 4096) {
|
|
385
|
+
fail("provider_auth_invalid");
|
|
386
|
+
}
|
|
387
|
+
if (options.connectionId !== void 0 && !UUID.test(options.connectionId)) {
|
|
388
|
+
fail("provider_unavailable");
|
|
389
|
+
}
|
|
390
|
+
const timeoutMs = Math.min(
|
|
391
|
+
Math.max(Math.trunc(options.timeoutMs ?? VAULT_PROVIDER_WIRE_LIMITS.defaultTimeoutMs), 1e3),
|
|
392
|
+
VAULT_PROVIDER_WIRE_LIMITS.maxTimeoutMs
|
|
393
|
+
);
|
|
394
|
+
const now = options.now ?? (() => Date.now());
|
|
395
|
+
const newRequestId = options.newRequestId ?? (() => crypto.randomUUID());
|
|
396
|
+
const responseKey = vaultProviderWireResponseKey(options.signingSecret);
|
|
397
|
+
let described = null;
|
|
398
|
+
function currentOperations() {
|
|
399
|
+
if (described) return described.operations;
|
|
400
|
+
if (options.operations) return remoteVaultOperations(options.operations);
|
|
401
|
+
return REMOTE_VAULT_READ_ONLY_OPERATIONS;
|
|
402
|
+
}
|
|
403
|
+
function connectionIdFor(ctx) {
|
|
404
|
+
const fromCtx = ctx.connectionId;
|
|
405
|
+
if (fromCtx && options.connectionId && fromCtx !== options.connectionId) {
|
|
406
|
+
fail("provider_permission_denied");
|
|
407
|
+
}
|
|
408
|
+
const id = fromCtx ?? options.connectionId;
|
|
409
|
+
if (!id || !UUID.test(id)) fail("provider_unavailable");
|
|
410
|
+
return id.toLowerCase();
|
|
411
|
+
}
|
|
412
|
+
function freshnessFrom(wire) {
|
|
413
|
+
if (!wire) return null;
|
|
414
|
+
return {
|
|
415
|
+
state: wire.state,
|
|
416
|
+
lastCheckedAt: new Date(now()).toISOString(),
|
|
417
|
+
sourceSyncedAt: wire.sourceSyncedAt,
|
|
418
|
+
source: wire.source,
|
|
419
|
+
errorCode: wire.errorCode,
|
|
420
|
+
retryAt: wire.retryAt
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async function call(ctx, budget, operation) {
|
|
424
|
+
const mutation = operation.op === "write_item" || operation.op === "archive_item";
|
|
425
|
+
if (!ctx.authority) fail("provider_permission_denied");
|
|
426
|
+
const connectionId = connectionIdFor(ctx);
|
|
427
|
+
ctx.signal?.throwIfAborted();
|
|
428
|
+
ctx.budget.take(budget);
|
|
429
|
+
const requestId = newRequestId();
|
|
430
|
+
const { accountId, workspaceId, subjectId, connectionWorkspaceId } = ctx.authority;
|
|
431
|
+
const parsed = VaultProviderWireRequest.safeParse({
|
|
432
|
+
version: VAULT_PROVIDER_WIRE_VERSION,
|
|
433
|
+
type: "request",
|
|
434
|
+
requestId,
|
|
435
|
+
sentAt: new Date(now()).toISOString(),
|
|
436
|
+
provider: options.provider,
|
|
437
|
+
connectionId,
|
|
438
|
+
authority: {
|
|
439
|
+
accountId,
|
|
440
|
+
workspaceId,
|
|
441
|
+
subjectId,
|
|
442
|
+
connectionWorkspaceId,
|
|
443
|
+
...ctx.authority.sessionId ? { sessionId: ctx.authority.sessionId } : {},
|
|
444
|
+
...ctx.authority.turnId ? { turnId: ctx.authority.turnId } : {},
|
|
445
|
+
...ctx.authority.attemptId ? { attemptId: ctx.authority.attemptId } : {},
|
|
446
|
+
...ctx.authority.siteId ? { siteId: ctx.authority.siteId } : {}
|
|
447
|
+
},
|
|
448
|
+
operation
|
|
449
|
+
});
|
|
450
|
+
if (!parsed.success) fail("provider_unavailable");
|
|
451
|
+
const body = JSON.stringify(parsed.data);
|
|
452
|
+
if (vaultProviderWireUtf8Bytes(body) > VAULT_PROVIDER_WIRE_LIMITS.maxRequestBytes) {
|
|
453
|
+
fail("provider_unavailable");
|
|
454
|
+
}
|
|
455
|
+
const signature = await signOpenGeniPayload(
|
|
456
|
+
options.signingSecret,
|
|
457
|
+
body,
|
|
458
|
+
Math.floor(now() / 1e3)
|
|
459
|
+
);
|
|
460
|
+
const unverified = (code, retryAfterMs) => mutation ? fail("provider_outcome_unknown") : fail(code, retryAfterMs);
|
|
461
|
+
let response;
|
|
462
|
+
try {
|
|
463
|
+
response = await options.transport.post({
|
|
464
|
+
url: endpoint,
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: {
|
|
467
|
+
"content-type": "application/json",
|
|
468
|
+
accept: "application/json",
|
|
469
|
+
[OPENGENI_SIGNATURE_HEADER]: signature
|
|
470
|
+
},
|
|
471
|
+
body,
|
|
472
|
+
timeoutMs,
|
|
473
|
+
maxResponseBytes: VAULT_PROVIDER_WIRE_LIMITS.maxResponseBytes,
|
|
474
|
+
...ctx.signal ? { signal: ctx.signal } : {}
|
|
475
|
+
});
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error instanceof RemoteVaultTransportError && !error.requestSent) {
|
|
478
|
+
fail("provider_unavailable");
|
|
479
|
+
}
|
|
480
|
+
if (mutation) fail("provider_outcome_unknown");
|
|
481
|
+
ctx.signal?.throwIfAborted();
|
|
482
|
+
return fail("provider_unavailable");
|
|
483
|
+
}
|
|
484
|
+
if (typeof response?.body !== "string" || vaultProviderWireUtf8Bytes(response.body) > VAULT_PROVIDER_WIRE_LIMITS.maxResponseBytes) {
|
|
485
|
+
return unverified("provider_unavailable");
|
|
486
|
+
}
|
|
487
|
+
if (response.status !== 200) {
|
|
488
|
+
if (response.status === 401 || response.status === 403) {
|
|
489
|
+
return unverified("provider_auth_invalid");
|
|
490
|
+
}
|
|
491
|
+
if (response.status === 429) {
|
|
492
|
+
return unverified("provider_rate_limited", retryAfterHeader(response.headers));
|
|
493
|
+
}
|
|
494
|
+
return unverified("provider_unavailable");
|
|
495
|
+
}
|
|
496
|
+
const verified = await verifyOpenGeniSignature({
|
|
497
|
+
secret: responseKey,
|
|
498
|
+
body: response.body,
|
|
499
|
+
signature: response.headers.get(VAULT_PROVIDER_WIRE_RESPONSE_SIGNATURE_HEADER),
|
|
500
|
+
toleranceSeconds: VAULT_PROVIDER_WIRE_LIMITS.signatureToleranceSeconds,
|
|
501
|
+
nowSeconds: Math.floor(now() / 1e3)
|
|
502
|
+
});
|
|
503
|
+
if (!verified) return unverified("provider_unavailable");
|
|
504
|
+
let json;
|
|
505
|
+
try {
|
|
506
|
+
json = JSON.parse(response.body);
|
|
507
|
+
} catch {
|
|
508
|
+
return unverified("provider_unavailable");
|
|
509
|
+
}
|
|
510
|
+
const envelope = parseVaultProviderWireResponse(json, { requestId, op: operation.op });
|
|
511
|
+
if (!envelope) return unverified("provider_unavailable");
|
|
512
|
+
const freshness = freshnessFrom(envelope.freshness);
|
|
513
|
+
if (!envelope.ok) {
|
|
514
|
+
if (freshness) ctx.onFreshness?.(freshness);
|
|
515
|
+
fail(envelope.error.code, envelope.error.retryAfterMs ?? void 0);
|
|
516
|
+
}
|
|
517
|
+
return { result: envelope.result, freshness };
|
|
518
|
+
}
|
|
519
|
+
function readContext(ctx) {
|
|
520
|
+
const epoch = ctx.cacheEpoch;
|
|
521
|
+
const cacheEpoch = typeof epoch === "string" && epoch.length > 0 && epoch.length <= VAULT_PROVIDER_WIRE_LIMITS.cacheEpochMaxChars && !CONTROL.test(epoch) ? epoch : null;
|
|
522
|
+
return { forceRefresh: ctx.forceRefresh === true, cacheEpoch };
|
|
523
|
+
}
|
|
524
|
+
function wirePage(page) {
|
|
525
|
+
const limit = Math.trunc(page.limit);
|
|
526
|
+
if (!Number.isSafeInteger(limit) || limit < 1) fail("provider_inventory_incomplete");
|
|
527
|
+
return {
|
|
528
|
+
limit: Math.min(limit, VAULT_PROVIDER_WIRE_LIMITS.pageSizeMax),
|
|
529
|
+
...page.cursor !== void 0 ? { cursor: page.cursor } : {},
|
|
530
|
+
...page.snapshot !== void 0 ? { snapshot: page.snapshot } : {}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function idempotencyKey(ctx) {
|
|
534
|
+
const key = ctx.idempotencyKey;
|
|
535
|
+
if (typeof key === "string" && key.length > 0 && key.length <= VAULT_PROVIDER_WIRE_LIMITS.idempotencyKeyMaxChars && !CONTROL.test(key)) {
|
|
536
|
+
return key;
|
|
537
|
+
}
|
|
538
|
+
return crypto.randomUUID();
|
|
539
|
+
}
|
|
540
|
+
const provider = {
|
|
541
|
+
id: options.provider,
|
|
542
|
+
capabilities() {
|
|
543
|
+
const operations = currentOperations();
|
|
544
|
+
return {
|
|
545
|
+
write: operations.createItemTypes.length > 0 || operations.updateItem || operations.archiveItem,
|
|
546
|
+
createVault: false,
|
|
547
|
+
totp: described?.capabilities.totp ?? (operations.createTotp || operations.updateTotp),
|
|
548
|
+
files: operations.readFiles,
|
|
549
|
+
groupGrants: false,
|
|
550
|
+
passkeys: false,
|
|
551
|
+
operations
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
async validate(ctx) {
|
|
555
|
+
const { result, freshness } = await call(ctx, "admin", { op: "describe" });
|
|
556
|
+
if (freshness) ctx.onFreshness?.(freshness);
|
|
557
|
+
const validation = {
|
|
558
|
+
accountId: result.accountId,
|
|
559
|
+
accountName: result.accountName,
|
|
560
|
+
operations: remoteVaultOperations(result.operations),
|
|
561
|
+
capabilities: { ...result.capabilities }
|
|
562
|
+
};
|
|
563
|
+
validation.operations.readFiles &&= validation.capabilities.files;
|
|
564
|
+
if (!validation.capabilities.totp) {
|
|
565
|
+
validation.operations.createTotp = false;
|
|
566
|
+
validation.operations.updateTotp = false;
|
|
567
|
+
}
|
|
568
|
+
described = validation;
|
|
569
|
+
return {
|
|
570
|
+
...validation,
|
|
571
|
+
operations: { ...validation.operations },
|
|
572
|
+
capabilities: { ...validation.capabilities }
|
|
573
|
+
};
|
|
574
|
+
},
|
|
575
|
+
async listVaultPage(ctx, page) {
|
|
576
|
+
const request = wirePage(page);
|
|
577
|
+
const { result, freshness } = await call(ctx, "list", {
|
|
578
|
+
op: "list_vaults",
|
|
579
|
+
page: request,
|
|
580
|
+
...readContext(ctx)
|
|
581
|
+
});
|
|
582
|
+
if (result.items.length > request.limit) fail("provider_inventory_incomplete");
|
|
583
|
+
return {
|
|
584
|
+
items: result.items.map(toProviderVault),
|
|
585
|
+
nextCursor: result.nextCursor,
|
|
586
|
+
snapshot: result.snapshot,
|
|
587
|
+
...freshness ? { freshness } : {}
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
async listItemPage(ctx, externalVaultId, page) {
|
|
591
|
+
const request = wirePage(page);
|
|
592
|
+
const { result, freshness } = await call(ctx, "list", {
|
|
593
|
+
op: "list_items",
|
|
594
|
+
externalVaultId,
|
|
595
|
+
page: request,
|
|
596
|
+
...readContext(ctx)
|
|
597
|
+
});
|
|
598
|
+
if (result.items.length > request.limit) fail("provider_inventory_incomplete");
|
|
599
|
+
return {
|
|
600
|
+
items: result.items.map(toProviderItem),
|
|
601
|
+
nextCursor: result.nextCursor,
|
|
602
|
+
snapshot: result.snapshot,
|
|
603
|
+
...freshness ? { freshness } : {}
|
|
604
|
+
};
|
|
605
|
+
},
|
|
606
|
+
// Compatibility: a complete bounded traversal or a refusal, never a prefix.
|
|
607
|
+
async listVaults(ctx) {
|
|
608
|
+
return collectProviderPages({
|
|
609
|
+
page: async (page) => {
|
|
610
|
+
const result = await provider.listVaultPage(ctx, page);
|
|
611
|
+
if (result.freshness) ctx.onFreshness?.(result.freshness);
|
|
612
|
+
return result;
|
|
613
|
+
},
|
|
614
|
+
identity: (vault) => vault.externalVaultId,
|
|
615
|
+
maxItems: VAULT_LIMITS2.vaultsPerOwnerScope,
|
|
616
|
+
...ctx.signal ? { signal: ctx.signal } : {}
|
|
617
|
+
});
|
|
618
|
+
},
|
|
619
|
+
async listItems(ctx, externalVaultId) {
|
|
620
|
+
return collectProviderPages({
|
|
621
|
+
page: async (page) => {
|
|
622
|
+
const result = await provider.listItemPage(ctx, externalVaultId, page);
|
|
623
|
+
if (result.freshness) ctx.onFreshness?.(result.freshness);
|
|
624
|
+
return result;
|
|
625
|
+
},
|
|
626
|
+
identity: (item) => item.externalItemId,
|
|
627
|
+
maxItems: VAULT_LIMITS2.itemsPerVault,
|
|
628
|
+
...ctx.signal ? { signal: ctx.signal } : {}
|
|
629
|
+
});
|
|
630
|
+
},
|
|
631
|
+
async readFields(ctx, externalVaultId, externalItemId, externalFieldIds) {
|
|
632
|
+
if (externalFieldIds.length === 0) return [];
|
|
633
|
+
if (externalFieldIds.length > VAULT_PROVIDER_WIRE_LIMITS.readFieldsMax || new Set(externalFieldIds).size !== externalFieldIds.length) {
|
|
634
|
+
fail("provider_not_found");
|
|
635
|
+
}
|
|
636
|
+
const { result, freshness } = await call(ctx, "read", {
|
|
637
|
+
op: "read_fields",
|
|
638
|
+
externalVaultId,
|
|
639
|
+
externalItemId,
|
|
640
|
+
externalFieldIds: [...externalFieldIds],
|
|
641
|
+
...readContext(ctx)
|
|
642
|
+
});
|
|
643
|
+
if (freshness) ctx.onFreshness?.(freshness);
|
|
644
|
+
const byId = new Map(result.fields.map((field) => [field.externalFieldId, field]));
|
|
645
|
+
if (result.fields.length !== externalFieldIds.length || externalFieldIds.some((id) => !byId.has(id))) {
|
|
646
|
+
fail("provider_unavailable");
|
|
647
|
+
}
|
|
648
|
+
const values = [];
|
|
649
|
+
for (const externalFieldId of externalFieldIds) {
|
|
650
|
+
const wire = byId.get(externalFieldId).value;
|
|
651
|
+
let material;
|
|
652
|
+
if (wire.type === "text") {
|
|
653
|
+
material = { type: "text", value: wire.value };
|
|
654
|
+
} else if (wire.type === "totp") {
|
|
655
|
+
try {
|
|
656
|
+
material = { type: "totp", params: parseTotpInput(wire.value) };
|
|
657
|
+
} catch {
|
|
658
|
+
return fail("provider_unavailable");
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
let bytes;
|
|
662
|
+
try {
|
|
663
|
+
bytes = decodeBase64(wire.base64);
|
|
664
|
+
} catch {
|
|
665
|
+
return fail("provider_unavailable");
|
|
666
|
+
}
|
|
667
|
+
if (bytes.byteLength > VAULT_LIMITS2.fileValueMaxBytes) fail("provider_unavailable");
|
|
668
|
+
material = { type: "bytes", value: bytes };
|
|
669
|
+
}
|
|
670
|
+
values.push({ externalFieldId, material });
|
|
671
|
+
}
|
|
672
|
+
return values;
|
|
673
|
+
},
|
|
674
|
+
async writeItem(ctx, externalVaultId, item) {
|
|
675
|
+
const operations = currentOperations();
|
|
676
|
+
const update = item.externalItemId !== void 0;
|
|
677
|
+
const type = item.type;
|
|
678
|
+
if (type === "passkey") return fail("provider_permission_denied");
|
|
679
|
+
if (update ? !operations.updateItem : !operations.createItemTypes.includes(item.type)) {
|
|
680
|
+
fail("provider_permission_denied");
|
|
681
|
+
}
|
|
682
|
+
if (item.fields.some((field) => field.kind === "file" && field.value !== void 0) || update && !operations.removeField && item.fields.some((field) => field.remove) || item.fields.some((field) => field.kind === "totp" && field.value !== void 0) && !(update ? operations.updateTotp : operations.createTotp)) {
|
|
683
|
+
fail("provider_permission_denied");
|
|
684
|
+
}
|
|
685
|
+
const fields = item.fields.map((field) => ({
|
|
686
|
+
...field.externalFieldId !== void 0 ? { externalFieldId: field.externalFieldId } : {},
|
|
687
|
+
name: field.name,
|
|
688
|
+
kind: field.kind,
|
|
689
|
+
...field.value !== void 0 ? { value: field.value } : {},
|
|
690
|
+
...field.remove ? { remove: true } : {}
|
|
691
|
+
}));
|
|
692
|
+
const { result, freshness } = await call(ctx, "write", {
|
|
693
|
+
op: "write_item",
|
|
694
|
+
externalVaultId,
|
|
695
|
+
item: {
|
|
696
|
+
...update ? { externalItemId: item.externalItemId } : {},
|
|
697
|
+
...item.expectedVersion !== void 0 ? { expectedVersion: item.expectedVersion } : {},
|
|
698
|
+
name: item.name,
|
|
699
|
+
type,
|
|
700
|
+
websites: [...item.websites],
|
|
701
|
+
fields
|
|
702
|
+
},
|
|
703
|
+
idempotencyKey: idempotencyKey(ctx)
|
|
704
|
+
});
|
|
705
|
+
if (freshness) ctx.onFreshness?.(freshness);
|
|
706
|
+
if (update && result.item.externalItemId !== item.externalItemId) {
|
|
707
|
+
fail("provider_outcome_unknown");
|
|
708
|
+
}
|
|
709
|
+
return toProviderItem(result.item);
|
|
710
|
+
},
|
|
711
|
+
async archiveItem(ctx, externalVaultId, externalItemId) {
|
|
712
|
+
if (!currentOperations().archiveItem) fail("provider_permission_denied");
|
|
713
|
+
const { freshness } = await call(ctx, "write", {
|
|
714
|
+
op: "archive_item",
|
|
715
|
+
externalVaultId,
|
|
716
|
+
externalItemId,
|
|
717
|
+
idempotencyKey: idempotencyKey(ctx)
|
|
718
|
+
});
|
|
719
|
+
if (freshness) ctx.onFreshness?.(freshness);
|
|
720
|
+
},
|
|
721
|
+
async refresh(ctx) {
|
|
722
|
+
const { freshness } = await call(ctx, "admin", { op: "refresh" });
|
|
723
|
+
if (freshness) ctx.onFreshness?.(freshness);
|
|
724
|
+
return freshness;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
return provider;
|
|
728
|
+
}
|
|
9
729
|
|
|
10
730
|
// src/splice/index.ts
|
|
11
731
|
import { SecretReferenceError as SecretReferenceError3 } from "@opengeni/contracts";
|
|
@@ -890,13 +1610,27 @@ function dialectFromShell(shell, os) {
|
|
|
890
1610
|
}
|
|
891
1611
|
export {
|
|
892
1612
|
EMPTY_VAULT_PROVIDER_REGISTRY,
|
|
1613
|
+
PROVIDER_MAX_PAGES,
|
|
1614
|
+
PROVIDER_PAGE_SIZE,
|
|
1615
|
+
REMOTE_VAULT_READ_ONLY_OPERATIONS,
|
|
1616
|
+
RemoteVaultTransportError,
|
|
893
1617
|
SECRET_EXEC_ENV_PREFIX,
|
|
894
1618
|
VAULT_PROVIDER_ERROR_CODES,
|
|
895
1619
|
VaultProviderError,
|
|
1620
|
+
collectProviderPages,
|
|
1621
|
+
createFetchRemoteVaultTransport,
|
|
896
1622
|
createProviderCallBudget,
|
|
1623
|
+
createRemoteVaultProvider,
|
|
897
1624
|
createVaultProviderRegistry,
|
|
898
1625
|
dialectFromShell,
|
|
899
1626
|
isVaultProviderError,
|
|
900
|
-
|
|
1627
|
+
listProviderItems,
|
|
1628
|
+
listProviderVaults,
|
|
1629
|
+
providerOperations,
|
|
1630
|
+
readProviderItemInventory,
|
|
1631
|
+
readProviderVaultInventory,
|
|
1632
|
+
remoteVaultOperations,
|
|
1633
|
+
spliceShellCommand,
|
|
1634
|
+
traverseProviderPages
|
|
901
1635
|
};
|
|
902
1636
|
//# sourceMappingURL=index.js.map
|