@nylorun/runtime 0.6.0-beta → 0.8.0-beta
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/CHANGELOG.md +48 -0
- package/README.md +9 -5
- package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
- package/dist/adapters/sandbox/microsandbox.js +238 -0
- package/dist/adapters/sandbox/virtual.d.ts +4 -0
- package/dist/adapters/sandbox/virtual.js +125 -0
- package/dist/configuration.d.ts +1 -1
- package/dist/configuration.js +1 -1
- package/dist/core/executors.d.ts +32 -0
- package/dist/core/executors.js +60 -0
- package/dist/core/main.js +7 -12
- package/dist/core/runtime.d.ts +41 -1
- package/dist/core/runtime.js +594 -53
- package/dist/core/store.d.ts +13 -0
- package/dist/core/store.js +95 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mcp/connect.d.ts +31 -0
- package/dist/mcp/connect.js +190 -0
- package/dist/mcp/pool.d.ts +45 -0
- package/dist/mcp/pool.js +143 -0
- package/dist/mcp/snapshot.d.ts +25 -0
- package/dist/mcp/snapshot.js +10 -0
- package/dist/mcp/stdio.d.ts +15 -0
- package/dist/mcp/stdio.js +80 -0
- package/dist/model/catalog.d.ts +12 -0
- package/dist/model/catalog.js +29 -0
- package/dist/model/models.d.ts +3 -1
- package/dist/model/models.js +4 -2
- package/dist/model/pi-model.d.ts +9 -3
- package/dist/model/pi-model.js +47 -11
- package/dist/model/settings.d.ts +0 -2
- package/dist/model/settings.js +0 -47
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/sandbox/manager.d.ts +61 -0
- package/dist/sandbox/manager.js +239 -0
- package/dist/sandbox/policy.d.ts +13 -0
- package/dist/sandbox/policy.js +67 -0
- package/dist/sandbox/select.d.ts +35 -0
- package/dist/sandbox/select.js +76 -0
- package/dist/sandbox/tools.d.ts +25 -0
- package/dist/sandbox/tools.js +165 -0
- package/dist/sandbox/types.d.ts +66 -0
- package/dist/vault/crypto.d.ts +12 -0
- package/dist/vault/crypto.js +80 -0
- package/dist/vault/error.d.ts +4 -0
- package/dist/vault/error.js +7 -0
- package/dist/vault/kek.d.ts +6 -0
- package/dist/vault/kek.js +26 -0
- package/dist/vault/service.d.ts +99 -0
- package/dist/vault/service.js +825 -0
- package/dist/vault/url.d.ts +3 -0
- package/dist/vault/url.js +27 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +12 -6
- package/dist/config.d.ts +0 -20
- package/dist/model/defaults.d.ts +0 -17
- package/dist/model/defaults.js +0 -21
- package/dist/model/http-model.d.ts +0 -12
- package/dist/model/http-model.js +0 -299
- package/dist/node/local-sessions.d.ts +0 -5
- package/dist/node/local-sessions.js +0 -174
- package/dist/server/ag-ui.d.ts +0 -8
- package/dist/server/ag-ui.js +0 -76
- package/dist/server/delivery.d.ts +0 -24
- package/dist/server/delivery.js +0 -107
- package/dist/server/digests.d.ts +0 -13
- package/dist/server/digests.js +0 -39
- package/dist/server/host.d.ts +0 -70
- package/dist/server/host.js +0 -647
- package/dist/session/api.d.ts +0 -10
- package/dist/session/api.js +0 -15
- package/dist/session/default.d.ts +0 -5
- package/dist/session/default.js +0 -30
- package/dist/session/handle.d.ts +0 -27
- package/dist/session/handle.js +0 -199
- package/dist/session/index.d.ts +0 -2
- package/dist/session/index.js +0 -2
- package/dist/sessions/host.d.ts +0 -39
- package/dist/sessions/host.js +0 -359
- package/dist/sessions/store.d.ts +0 -41
- package/dist/sessions/store.js +0 -33
- /package/dist/{config.js → sandbox/types.js} +0 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { canonical } from "../core/store.js";
|
|
3
|
+
import { decryptSecret, encryptSecret, VaultCryptoError, } from "./crypto.js";
|
|
4
|
+
import { VaultError } from "./error.js";
|
|
5
|
+
import { hostModelCatalog } from "../model/catalog.js";
|
|
6
|
+
import { normalizeVaultUrl } from "./url.js";
|
|
7
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
8
|
+
const HOST_VAULT_ID = "host";
|
|
9
|
+
const HOST_MODEL_ID = "host-model";
|
|
10
|
+
export class VaultService {
|
|
11
|
+
db;
|
|
12
|
+
tx;
|
|
13
|
+
kek;
|
|
14
|
+
fetchImpl;
|
|
15
|
+
constructor(db, tx, kek, fetchImpl) {
|
|
16
|
+
this.db = db;
|
|
17
|
+
this.tx = tx;
|
|
18
|
+
this.kek = kek;
|
|
19
|
+
this.fetchImpl = fetchImpl;
|
|
20
|
+
}
|
|
21
|
+
createVault(body) {
|
|
22
|
+
return this.tx(() => this.replay(`create-vault:${body.idempotencyKey}`, body, () => {
|
|
23
|
+
const now = new Date().toISOString();
|
|
24
|
+
const id = randomUUID();
|
|
25
|
+
this.db
|
|
26
|
+
.prepare(`INSERT INTO vaults(id,name,owner_user_id,metadata_json,created_at) VALUES(?,?,?,?,?)`)
|
|
27
|
+
.run(id, body.name, body.ownerUserId, body.metadata ? JSON.stringify(body.metadata) : null, now);
|
|
28
|
+
const info = this.vaultInfo(id);
|
|
29
|
+
this.audit({
|
|
30
|
+
actor: "application",
|
|
31
|
+
action: "create",
|
|
32
|
+
vaultId: id,
|
|
33
|
+
outcome: "created",
|
|
34
|
+
});
|
|
35
|
+
return info;
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
listVaults(ownerUserId) {
|
|
39
|
+
const rows = this.db
|
|
40
|
+
.prepare(`SELECT id FROM vaults WHERE owner_user_id=? AND scope='user' ORDER BY created_at, id`)
|
|
41
|
+
.all(ownerUserId);
|
|
42
|
+
return rows.map((row) => this.vaultInfo(row.id));
|
|
43
|
+
}
|
|
44
|
+
getVault(id) {
|
|
45
|
+
return this.vaultInfo(id);
|
|
46
|
+
}
|
|
47
|
+
deleteVault(id) {
|
|
48
|
+
return this.tx(() => {
|
|
49
|
+
this.vaultInfo(id);
|
|
50
|
+
const credentials = this.db
|
|
51
|
+
.prepare(`SELECT id FROM vault_credentials WHERE vault_id=?`)
|
|
52
|
+
.all(id);
|
|
53
|
+
for (const credential of credentials) {
|
|
54
|
+
this.audit({
|
|
55
|
+
actor: "application",
|
|
56
|
+
action: "delete",
|
|
57
|
+
vaultId: id,
|
|
58
|
+
credentialId: credential.id,
|
|
59
|
+
outcome: "deleted",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
this.db.prepare(`DELETE FROM vault_credentials WHERE vault_id=?`).run(id);
|
|
63
|
+
this.db.prepare(`DELETE FROM vaults WHERE id=?`).run(id);
|
|
64
|
+
this.audit({
|
|
65
|
+
actor: "application",
|
|
66
|
+
action: "delete",
|
|
67
|
+
vaultId: id,
|
|
68
|
+
outcome: "deleted",
|
|
69
|
+
});
|
|
70
|
+
return { id };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
createCredential(vaultId, body) {
|
|
74
|
+
return this.tx(() => this.replay(`create-credential:${vaultId}:${body.idempotencyKey}`, body, () => {
|
|
75
|
+
this.vaultInfo(vaultId);
|
|
76
|
+
const id = randomUUID();
|
|
77
|
+
const url = normalizeVaultUrl(body.auth.url);
|
|
78
|
+
const payload = payloadFromCreate(body);
|
|
79
|
+
this.insertCredential({
|
|
80
|
+
id,
|
|
81
|
+
vaultId,
|
|
82
|
+
name: body.name,
|
|
83
|
+
type: body.auth.type,
|
|
84
|
+
url,
|
|
85
|
+
expiresAt: expiresFromCreate(body),
|
|
86
|
+
payload,
|
|
87
|
+
rotatedAt: null,
|
|
88
|
+
});
|
|
89
|
+
this.audit({
|
|
90
|
+
actor: "application",
|
|
91
|
+
action: "create",
|
|
92
|
+
vaultId,
|
|
93
|
+
credentialId: id,
|
|
94
|
+
outcome: "created",
|
|
95
|
+
});
|
|
96
|
+
return this.credentialInfo(vaultId, id);
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
listCredentials(vaultId) {
|
|
100
|
+
this.vaultInfo(vaultId);
|
|
101
|
+
const rows = this.db
|
|
102
|
+
.prepare(`SELECT id FROM vault_credentials WHERE vault_id=? ORDER BY created_at, id`)
|
|
103
|
+
.all(vaultId);
|
|
104
|
+
return rows.map((row) => this.credentialInfo(vaultId, row.id));
|
|
105
|
+
}
|
|
106
|
+
getCredential(vaultId, id) {
|
|
107
|
+
return this.credentialInfo(vaultId, id);
|
|
108
|
+
}
|
|
109
|
+
rotateCredential(vaultId, id, body) {
|
|
110
|
+
return this.tx(() => this.replay(`rotate:${vaultId}:${id}:${body.idempotencyKey}`, body, () => {
|
|
111
|
+
const row = this.credentialRow(vaultId, id);
|
|
112
|
+
if (row.type !== body.auth.type)
|
|
113
|
+
throw new VaultError(409, "Credential type cannot change");
|
|
114
|
+
const current = this.readPayload(row);
|
|
115
|
+
const next = { ...current };
|
|
116
|
+
let expiresAt = row.expires_at;
|
|
117
|
+
if (body.auth.type === "bearer")
|
|
118
|
+
next.token = body.auth.token;
|
|
119
|
+
else {
|
|
120
|
+
next.accessToken = body.auth.accessToken;
|
|
121
|
+
expiresAt =
|
|
122
|
+
body.auth.expiresAt === undefined || body.auth.expiresAt === null
|
|
123
|
+
? null
|
|
124
|
+
: requireTimestamp(body.auth.expiresAt);
|
|
125
|
+
}
|
|
126
|
+
this.writePayload(row, next, expiresAt, new Date().toISOString());
|
|
127
|
+
this.audit({
|
|
128
|
+
actor: "application",
|
|
129
|
+
action: "rotate",
|
|
130
|
+
vaultId,
|
|
131
|
+
credentialId: id,
|
|
132
|
+
outcome: "rotated",
|
|
133
|
+
});
|
|
134
|
+
return this.credentialInfo(vaultId, id);
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
deleteCredential(vaultId, id) {
|
|
138
|
+
return this.tx(() => {
|
|
139
|
+
this.credentialRow(vaultId, id);
|
|
140
|
+
this.db
|
|
141
|
+
.prepare(`DELETE FROM vault_credentials WHERE id=? AND vault_id=?`)
|
|
142
|
+
.run(id, vaultId);
|
|
143
|
+
this.audit({
|
|
144
|
+
actor: "application",
|
|
145
|
+
action: "delete",
|
|
146
|
+
vaultId,
|
|
147
|
+
credentialId: id,
|
|
148
|
+
outcome: "deleted",
|
|
149
|
+
});
|
|
150
|
+
return { id };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
assertAttachment(ownerUserId, vaultIds, selections) {
|
|
154
|
+
if (new Set(vaultIds).size !== vaultIds.length)
|
|
155
|
+
throw new VaultError(400, "Duplicate vault id");
|
|
156
|
+
if (new Set(selections.map((item) => item.serverName)).size !== selections.length)
|
|
157
|
+
throw new VaultError(400, "Duplicate credential selection");
|
|
158
|
+
for (const id of vaultIds) {
|
|
159
|
+
const vault = this.vaultRow(id);
|
|
160
|
+
if (!vault)
|
|
161
|
+
throw new VaultError(404, "Vault not found");
|
|
162
|
+
if (vault.scope === "host")
|
|
163
|
+
throw new VaultError(400, "Host vault cannot be attached to a session");
|
|
164
|
+
if (vault.owner_user_id !== ownerUserId)
|
|
165
|
+
throw new VaultError(403, "Vault belongs to another user");
|
|
166
|
+
}
|
|
167
|
+
for (const selection of selections) {
|
|
168
|
+
const row = this.db
|
|
169
|
+
.prepare(`SELECT vault_id FROM vault_credentials WHERE id=?`)
|
|
170
|
+
.get(selection.credentialId);
|
|
171
|
+
if (!row || !vaultIds.includes(row.vault_id))
|
|
172
|
+
throw new VaultError(400, "Credential is not in an attached vault");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
recordAttachment(sessionId, vaultIds) {
|
|
176
|
+
this.audit({
|
|
177
|
+
actor: "application",
|
|
178
|
+
action: "attach",
|
|
179
|
+
sessionId,
|
|
180
|
+
target: vaultIds.join(","),
|
|
181
|
+
outcome: "attached",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async authorize(input) {
|
|
185
|
+
const url = normalizeVaultUrl(input.url);
|
|
186
|
+
const missing = input.vaultIds.filter((id) => !this.vaultRow(id));
|
|
187
|
+
if (missing.length > 0) {
|
|
188
|
+
this.audit({
|
|
189
|
+
actor: "host",
|
|
190
|
+
action: "use",
|
|
191
|
+
sessionId: input.sessionId,
|
|
192
|
+
target: url,
|
|
193
|
+
outcome: "refused",
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
status: "refused",
|
|
197
|
+
url,
|
|
198
|
+
credentialIds: [],
|
|
199
|
+
reason: "vault_missing",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const matches = input.vaultIds
|
|
203
|
+
.flatMap((vaultId) => this.rowsForVault(vaultId))
|
|
204
|
+
.filter((row) => JSON.parse(row.binding_json).url === url)
|
|
205
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
206
|
+
const selection = input.serverName
|
|
207
|
+
? input.credentialSelections.find((item) => item.serverName === input.serverName)
|
|
208
|
+
: undefined;
|
|
209
|
+
if (matches.length === 0)
|
|
210
|
+
return { status: "unauthenticated", url, headers: {} };
|
|
211
|
+
const chosen = choose(matches, selection);
|
|
212
|
+
if (chosen.kind === "refused") {
|
|
213
|
+
this.audit({
|
|
214
|
+
actor: "host",
|
|
215
|
+
action: "use",
|
|
216
|
+
sessionId: input.sessionId,
|
|
217
|
+
target: url,
|
|
218
|
+
outcome: "refused",
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
status: "refused",
|
|
222
|
+
url,
|
|
223
|
+
credentialIds: chosen.credentialIds,
|
|
224
|
+
reason: chosen.reason,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
let row = chosen.row;
|
|
228
|
+
let payload;
|
|
229
|
+
try {
|
|
230
|
+
payload = this.readPayload(row);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (!(error instanceof VaultCryptoError))
|
|
234
|
+
throw error;
|
|
235
|
+
this.audit({
|
|
236
|
+
actor: "host",
|
|
237
|
+
action: "use",
|
|
238
|
+
vaultId: row.vault_id,
|
|
239
|
+
credentialId: row.id,
|
|
240
|
+
sessionId: input.sessionId,
|
|
241
|
+
target: url,
|
|
242
|
+
outcome: "refused",
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
status: "refused",
|
|
246
|
+
url,
|
|
247
|
+
credentialIds: [row.id],
|
|
248
|
+
reason: "unreadable",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (row.type === "oauth" && dueForRefresh(row.expires_at)) {
|
|
252
|
+
const refreshed = await this.refresh(row, payload, input.sessionId, url);
|
|
253
|
+
if (refreshed.status === "refused")
|
|
254
|
+
return refreshed;
|
|
255
|
+
row = this.credentialRow(row.vault_id, row.id);
|
|
256
|
+
payload = refreshed.payload;
|
|
257
|
+
}
|
|
258
|
+
const token = row.type === "bearer" ? payload.token : payload.accessToken;
|
|
259
|
+
if (!token) {
|
|
260
|
+
return {
|
|
261
|
+
status: "refused",
|
|
262
|
+
url,
|
|
263
|
+
credentialIds: [row.id],
|
|
264
|
+
reason: "unreadable",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
this.audit({
|
|
268
|
+
actor: "host",
|
|
269
|
+
action: "use",
|
|
270
|
+
vaultId: row.vault_id,
|
|
271
|
+
credentialId: row.id,
|
|
272
|
+
sessionId: input.sessionId,
|
|
273
|
+
target: url,
|
|
274
|
+
outcome: "approved",
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
status: "authorized",
|
|
278
|
+
url,
|
|
279
|
+
headers: { authorization: `Bearer ${token}` },
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
getHostModel() {
|
|
283
|
+
const row = this.activeHostCredentialRow();
|
|
284
|
+
if (!row)
|
|
285
|
+
return { configured: false };
|
|
286
|
+
const binding = modelBinding(row);
|
|
287
|
+
return {
|
|
288
|
+
configured: true,
|
|
289
|
+
provider: binding.provider,
|
|
290
|
+
model: binding.model,
|
|
291
|
+
authType: binding.authType,
|
|
292
|
+
...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
listHostProviders() {
|
|
296
|
+
const active = this.activeProviderId();
|
|
297
|
+
const catalog = new Map(hostModelCatalog().providers.map((provider) => [provider.id, provider.name]));
|
|
298
|
+
const providers = this.hostModelRows().map((row) => {
|
|
299
|
+
const binding = modelBinding(row);
|
|
300
|
+
return {
|
|
301
|
+
id: binding.provider,
|
|
302
|
+
name: catalog.get(binding.provider) ??
|
|
303
|
+
(binding.provider === "custom"
|
|
304
|
+
? "Custom OpenAI-compatible"
|
|
305
|
+
: binding.provider),
|
|
306
|
+
model: binding.model,
|
|
307
|
+
authType: binding.authType,
|
|
308
|
+
...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
|
|
309
|
+
lastUpdated: row.rotated_at ?? row.created_at,
|
|
310
|
+
active: binding.provider === active,
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
providers.sort((left, right) => left.name.localeCompare(right.name));
|
|
314
|
+
return { providers };
|
|
315
|
+
}
|
|
316
|
+
putHostModel(body) {
|
|
317
|
+
return this.tx(() => this.replay(`host-model:${body.idempotencyKey}`, body, () => {
|
|
318
|
+
this.validateHostModel(body);
|
|
319
|
+
this.ensureHostVault();
|
|
320
|
+
const credentialId = hostModelCredentialId(body.provider);
|
|
321
|
+
this.deleteHostProviderRows(body.provider);
|
|
322
|
+
const payload = body.auth.type === "api_key"
|
|
323
|
+
? {
|
|
324
|
+
apiKey: body.auth.key,
|
|
325
|
+
...(body.auth.env ? { env: body.auth.env } : {}),
|
|
326
|
+
}
|
|
327
|
+
: { oauth: body.auth };
|
|
328
|
+
this.insertHostCredential({
|
|
329
|
+
id: credentialId,
|
|
330
|
+
provider: body.provider,
|
|
331
|
+
model: body.model,
|
|
332
|
+
baseUrl: body.baseUrl,
|
|
333
|
+
authType: body.auth.type,
|
|
334
|
+
payload,
|
|
335
|
+
});
|
|
336
|
+
this.setActiveProvider(body.provider);
|
|
337
|
+
this.audit({
|
|
338
|
+
actor: "application",
|
|
339
|
+
action: "rotate",
|
|
340
|
+
vaultId: HOST_VAULT_ID,
|
|
341
|
+
credentialId,
|
|
342
|
+
outcome: "rotated",
|
|
343
|
+
});
|
|
344
|
+
return this.getHostModel();
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
selectHostModel(body) {
|
|
348
|
+
return this.tx(() => this.replay(`host-model-select:${body.idempotencyKey}`, body, () => {
|
|
349
|
+
this.validateHostModel(body);
|
|
350
|
+
const row = this.hostCredentialRowFor(body.provider);
|
|
351
|
+
if (!row)
|
|
352
|
+
throw new VaultError(404, "Model provider is not configured");
|
|
353
|
+
const binding = modelBinding(row);
|
|
354
|
+
const payload = this.readHostPayload(row, binding);
|
|
355
|
+
const credentialId = hostModelCredentialId(body.provider);
|
|
356
|
+
this.deleteHostProviderRows(body.provider);
|
|
357
|
+
this.insertHostCredential({
|
|
358
|
+
id: credentialId,
|
|
359
|
+
provider: body.provider,
|
|
360
|
+
model: body.model,
|
|
361
|
+
baseUrl: body.baseUrl,
|
|
362
|
+
authType: binding.authType,
|
|
363
|
+
payload,
|
|
364
|
+
});
|
|
365
|
+
this.setActiveProvider(body.provider);
|
|
366
|
+
this.audit({
|
|
367
|
+
actor: "application",
|
|
368
|
+
action: "rotate",
|
|
369
|
+
vaultId: HOST_VAULT_ID,
|
|
370
|
+
credentialId,
|
|
371
|
+
outcome: "rotated",
|
|
372
|
+
});
|
|
373
|
+
return this.getHostModel();
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
readHostModel() {
|
|
377
|
+
const row = this.activeHostCredentialRow();
|
|
378
|
+
if (!row)
|
|
379
|
+
return undefined;
|
|
380
|
+
const binding = modelBinding(row);
|
|
381
|
+
const payload = this.readHostPayload(row, binding);
|
|
382
|
+
const credential = binding.authType === "oauth"
|
|
383
|
+
? { type: "oauth", ...payload.oauth }
|
|
384
|
+
: {
|
|
385
|
+
type: "api_key",
|
|
386
|
+
key: payload.apiKey,
|
|
387
|
+
...(payload.env ? { env: payload.env } : {}),
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
provider: binding.provider,
|
|
391
|
+
model: binding.model,
|
|
392
|
+
...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
|
|
393
|
+
authType: binding.authType,
|
|
394
|
+
credential,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
updateHostCredential(credential) {
|
|
398
|
+
this.tx(() => {
|
|
399
|
+
const row = this.activeHostCredentialRow();
|
|
400
|
+
if (!row)
|
|
401
|
+
throw new VaultError(404, "Model provider is not configured");
|
|
402
|
+
const binding = modelBinding(row);
|
|
403
|
+
const payload = credential.type === "oauth"
|
|
404
|
+
? { oauth: credential }
|
|
405
|
+
: {
|
|
406
|
+
apiKey: credential.key,
|
|
407
|
+
...(credential.env ? { env: credential.env } : {}),
|
|
408
|
+
};
|
|
409
|
+
const aad = hostModelAad(binding.provider, binding.model);
|
|
410
|
+
const sealed = encryptSecret(this.kek(), aad, Buffer.from(JSON.stringify(payload), "utf8"));
|
|
411
|
+
this.db
|
|
412
|
+
.prepare(`UPDATE vault_credentials
|
|
413
|
+
SET type='model', binding_json=?, kek_id=?, nonce=?, ciphertext=?, wrapped_dek=?, rotated_at=?
|
|
414
|
+
WHERE id=?`)
|
|
415
|
+
.run(JSON.stringify({ ...binding, authType: credential.type }), sealed.kekId, sealed.nonce, sealed.ciphertext, sealed.wrappedDek, new Date().toISOString(), row.id);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
reject(route) {
|
|
419
|
+
this.audit({
|
|
420
|
+
actor: "executor",
|
|
421
|
+
action: "reject",
|
|
422
|
+
target: route,
|
|
423
|
+
outcome: "rejected",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
async refresh(row, payload, sessionId, url) {
|
|
427
|
+
const failRefresh = () => {
|
|
428
|
+
this.audit({
|
|
429
|
+
actor: "host",
|
|
430
|
+
action: "refresh",
|
|
431
|
+
vaultId: row.vault_id,
|
|
432
|
+
credentialId: row.id,
|
|
433
|
+
sessionId,
|
|
434
|
+
target: payload.tokenEndpoint,
|
|
435
|
+
outcome: "refresh_failed",
|
|
436
|
+
});
|
|
437
|
+
return {
|
|
438
|
+
status: "refused",
|
|
439
|
+
url,
|
|
440
|
+
credentialIds: [row.id],
|
|
441
|
+
reason: "refresh_failed",
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
if (!payload.refreshToken || !payload.tokenEndpoint || !payload.clientId)
|
|
445
|
+
return failRefresh();
|
|
446
|
+
try {
|
|
447
|
+
const body = new URLSearchParams({
|
|
448
|
+
grant_type: "refresh_token",
|
|
449
|
+
refresh_token: payload.refreshToken,
|
|
450
|
+
client_id: payload.clientId,
|
|
451
|
+
});
|
|
452
|
+
const headers = {
|
|
453
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
454
|
+
};
|
|
455
|
+
if (payload.tokenEndpointAuth === "client_secret_post") {
|
|
456
|
+
if (!payload.clientSecret)
|
|
457
|
+
return failRefresh();
|
|
458
|
+
body.set("client_secret", payload.clientSecret);
|
|
459
|
+
}
|
|
460
|
+
if (payload.tokenEndpointAuth === "client_secret_basic") {
|
|
461
|
+
if (!payload.clientSecret)
|
|
462
|
+
return failRefresh();
|
|
463
|
+
const encoded = Buffer.from(`${encodeURIComponent(payload.clientId)}:${encodeURIComponent(payload.clientSecret)}`).toString("base64");
|
|
464
|
+
headers.authorization = `Basic ${encoded}`;
|
|
465
|
+
}
|
|
466
|
+
const response = await this.fetchImpl(payload.tokenEndpoint, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers,
|
|
469
|
+
body: body.toString(),
|
|
470
|
+
redirect: "error",
|
|
471
|
+
});
|
|
472
|
+
if (!response.ok)
|
|
473
|
+
return failRefresh();
|
|
474
|
+
const json = (await response.json());
|
|
475
|
+
if (typeof json.access_token !== "string" || json.access_token.length === 0)
|
|
476
|
+
return failRefresh();
|
|
477
|
+
const next = {
|
|
478
|
+
...payload,
|
|
479
|
+
accessToken: json.access_token,
|
|
480
|
+
refreshToken: typeof json.refresh_token === "string"
|
|
481
|
+
? json.refresh_token
|
|
482
|
+
: payload.refreshToken,
|
|
483
|
+
};
|
|
484
|
+
const expiresAt = typeof json.expires_in === "number" && Number.isFinite(json.expires_in)
|
|
485
|
+
? new Date(Date.now() + json.expires_in * 1000).toISOString()
|
|
486
|
+
: null;
|
|
487
|
+
this.tx(() => {
|
|
488
|
+
const current = this.credentialRow(row.vault_id, row.id);
|
|
489
|
+
this.writePayload(current, next, expiresAt, new Date().toISOString());
|
|
490
|
+
this.audit({
|
|
491
|
+
actor: "host",
|
|
492
|
+
action: "refresh",
|
|
493
|
+
vaultId: row.vault_id,
|
|
494
|
+
credentialId: row.id,
|
|
495
|
+
sessionId,
|
|
496
|
+
target: payload.tokenEndpoint,
|
|
497
|
+
outcome: "approved",
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
return { status: "ok", payload: next };
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return failRefresh();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
replay(key, body, create) {
|
|
507
|
+
const hash = createHash("sha256")
|
|
508
|
+
.update(requestHash(body))
|
|
509
|
+
.digest("hex");
|
|
510
|
+
const existing = this.db
|
|
511
|
+
.prepare(`SELECT body_hash, response FROM vault_idempotency WHERE id=?`)
|
|
512
|
+
.get(key);
|
|
513
|
+
if (existing) {
|
|
514
|
+
if (existing.body_hash !== hash)
|
|
515
|
+
throw new VaultError(409, "Idempotency key already binds another request");
|
|
516
|
+
return JSON.parse(existing.response);
|
|
517
|
+
}
|
|
518
|
+
const created = create();
|
|
519
|
+
this.db
|
|
520
|
+
.prepare(`INSERT INTO vault_idempotency(id, body_hash, response) VALUES(?,?,?)`)
|
|
521
|
+
.run(key, hash, JSON.stringify(created));
|
|
522
|
+
return created;
|
|
523
|
+
}
|
|
524
|
+
insertCredential(input) {
|
|
525
|
+
const aad = credentialAad(input.vaultId, input.id, input.type, input.url);
|
|
526
|
+
const sealed = encryptSecret(this.kek(), aad, Buffer.from(JSON.stringify(input.payload), "utf8"));
|
|
527
|
+
this.db
|
|
528
|
+
.prepare(`INSERT INTO vault_credentials(
|
|
529
|
+
id, vault_id, name, type, binding_json, expires_at, created_at, rotated_at,
|
|
530
|
+
kek_id, nonce, ciphertext, wrapped_dek
|
|
531
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`)
|
|
532
|
+
.run(input.id, input.vaultId, input.name, input.type, JSON.stringify({ url: input.url }), input.expiresAt, new Date().toISOString(), input.rotatedAt, sealed.kekId, sealed.nonce, sealed.ciphertext, sealed.wrappedDek);
|
|
533
|
+
}
|
|
534
|
+
writePayload(row, payload, expiresAt, rotatedAt) {
|
|
535
|
+
const url = JSON.parse(row.binding_json).url;
|
|
536
|
+
const aad = credentialAad(row.vault_id, row.id, row.type, url);
|
|
537
|
+
const sealed = encryptSecret(this.kek(), aad, Buffer.from(JSON.stringify(payload), "utf8"));
|
|
538
|
+
this.db
|
|
539
|
+
.prepare(`UPDATE vault_credentials
|
|
540
|
+
SET expires_at=?, rotated_at=?, kek_id=?, nonce=?, ciphertext=?, wrapped_dek=?
|
|
541
|
+
WHERE id=?`)
|
|
542
|
+
.run(expiresAt, rotatedAt, sealed.kekId, sealed.nonce, sealed.ciphertext, sealed.wrappedDek, row.id);
|
|
543
|
+
}
|
|
544
|
+
readPayload(row) {
|
|
545
|
+
const url = JSON.parse(row.binding_json).url;
|
|
546
|
+
const aad = credentialAad(row.vault_id, row.id, row.type, url);
|
|
547
|
+
const plaintext = decryptSecret(this.kek(), aad, Buffer.from(row.nonce), Buffer.from(row.ciphertext), Buffer.from(row.wrapped_dek), row.kek_id);
|
|
548
|
+
try {
|
|
549
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
plaintext.fill(0);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
vaultInfo(id) {
|
|
556
|
+
const row = this.vaultRow(id);
|
|
557
|
+
if (!row || row.scope === "host")
|
|
558
|
+
throw new VaultError(404, "Vault not found");
|
|
559
|
+
return {
|
|
560
|
+
id: row.id,
|
|
561
|
+
name: row.name,
|
|
562
|
+
ownerUserId: row.owner_user_id,
|
|
563
|
+
...(row.metadata_json
|
|
564
|
+
? { metadata: JSON.parse(row.metadata_json) }
|
|
565
|
+
: {}),
|
|
566
|
+
createdAt: row.created_at,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
vaultRow(id) {
|
|
570
|
+
return this.db
|
|
571
|
+
.prepare(`SELECT id, name, owner_user_id, metadata_json, created_at, scope FROM vaults WHERE id=?`)
|
|
572
|
+
.get(id);
|
|
573
|
+
}
|
|
574
|
+
ensureHostVault() {
|
|
575
|
+
const existing = this.vaultRow(HOST_VAULT_ID);
|
|
576
|
+
if (existing?.scope === "host")
|
|
577
|
+
return;
|
|
578
|
+
if (existing)
|
|
579
|
+
throw new VaultError(409, "Host vault id is already used");
|
|
580
|
+
this.db
|
|
581
|
+
.prepare(`INSERT INTO vaults(id,name,owner_user_id,metadata_json,created_at,scope) VALUES(?,?,?,?,?,?)`)
|
|
582
|
+
.run(HOST_VAULT_ID, "Host", "host", null, new Date().toISOString(), "host");
|
|
583
|
+
}
|
|
584
|
+
validateHostModel(body) {
|
|
585
|
+
if (body.provider === "custom") {
|
|
586
|
+
if (!body.baseUrl)
|
|
587
|
+
throw new VaultError(400, "A base URL is required for a custom provider.");
|
|
588
|
+
let url;
|
|
589
|
+
try {
|
|
590
|
+
url = new URL(body.baseUrl);
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
throw new VaultError(400, "Base URL must be an HTTP(S) URL.");
|
|
594
|
+
}
|
|
595
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
596
|
+
throw new VaultError(400, "Base URL must be an HTTP(S) URL.");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (body.baseUrl)
|
|
600
|
+
throw new VaultError(400, "A base URL is only valid for a custom provider.");
|
|
601
|
+
const provider = hostModelCatalog().providers.find((item) => item.id === body.provider);
|
|
602
|
+
if (!provider)
|
|
603
|
+
throw new VaultError(400, "Unknown model provider.");
|
|
604
|
+
if (!provider.models.some((item) => item.id === body.model))
|
|
605
|
+
throw new VaultError(400, "Unknown model.");
|
|
606
|
+
}
|
|
607
|
+
insertHostCredential(input) {
|
|
608
|
+
const aad = hostModelAad(input.provider, input.model);
|
|
609
|
+
const sealed = encryptSecret(this.kek(), aad, Buffer.from(JSON.stringify(input.payload), "utf8"));
|
|
610
|
+
this.db
|
|
611
|
+
.prepare(`INSERT INTO vault_credentials(
|
|
612
|
+
id, vault_id, name, type, binding_json, expires_at, created_at, rotated_at,
|
|
613
|
+
kek_id, nonce, ciphertext, wrapped_dek
|
|
614
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`)
|
|
615
|
+
.run(input.id, HOST_VAULT_ID, input.provider, "model", JSON.stringify({
|
|
616
|
+
provider: input.provider,
|
|
617
|
+
model: input.model,
|
|
618
|
+
...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
|
|
619
|
+
authType: input.authType,
|
|
620
|
+
}), null, new Date().toISOString(), null, sealed.kekId, sealed.nonce, sealed.ciphertext, sealed.wrappedDek);
|
|
621
|
+
}
|
|
622
|
+
readHostPayload(row, binding) {
|
|
623
|
+
const plaintext = decryptSecret(this.kek(), hostModelAad(binding.provider, binding.model), Buffer.from(row.nonce), Buffer.from(row.ciphertext), Buffer.from(row.wrapped_dek), row.kek_id);
|
|
624
|
+
try {
|
|
625
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
626
|
+
}
|
|
627
|
+
finally {
|
|
628
|
+
plaintext.fill(0);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
hostModelRows() {
|
|
632
|
+
return this.db
|
|
633
|
+
.prepare(`SELECT id, vault_id, name, type, binding_json, expires_at, created_at, rotated_at,
|
|
634
|
+
kek_id, nonce, ciphertext, wrapped_dek
|
|
635
|
+
FROM vault_credentials WHERE vault_id=? AND type='model'
|
|
636
|
+
ORDER BY created_at, id`)
|
|
637
|
+
.all(HOST_VAULT_ID).filter((row) => {
|
|
638
|
+
try {
|
|
639
|
+
modelBinding(row);
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
hostCredentialRowFor(provider) {
|
|
648
|
+
const preferred = hostModelCredentialId(provider);
|
|
649
|
+
const rows = this.hostModelRows();
|
|
650
|
+
return (rows.find((row) => row.id === preferred) ??
|
|
651
|
+
rows.find((row) => modelBinding(row).provider === provider));
|
|
652
|
+
}
|
|
653
|
+
activeHostCredentialRow() {
|
|
654
|
+
const active = this.activeProviderId();
|
|
655
|
+
if (!active)
|
|
656
|
+
return undefined;
|
|
657
|
+
return this.hostCredentialRowFor(active);
|
|
658
|
+
}
|
|
659
|
+
activeProviderId() {
|
|
660
|
+
const vault = this.vaultRow(HOST_VAULT_ID);
|
|
661
|
+
if (vault?.metadata_json) {
|
|
662
|
+
try {
|
|
663
|
+
const metadata = JSON.parse(vault.metadata_json);
|
|
664
|
+
if (typeof metadata.activeProvider === "string" &&
|
|
665
|
+
metadata.activeProvider &&
|
|
666
|
+
this.hostCredentialRowFor(metadata.activeProvider))
|
|
667
|
+
return metadata.activeProvider;
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
/* Fall through to the only configured provider. */
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const rows = this.hostModelRows();
|
|
674
|
+
if (rows.length === 0)
|
|
675
|
+
return undefined;
|
|
676
|
+
if (rows.length === 1)
|
|
677
|
+
return modelBinding(rows[0]).provider;
|
|
678
|
+
const legacy = rows.find((row) => row.id === HOST_MODEL_ID);
|
|
679
|
+
if (legacy)
|
|
680
|
+
return modelBinding(legacy).provider;
|
|
681
|
+
return modelBinding(rows[0]).provider;
|
|
682
|
+
}
|
|
683
|
+
setActiveProvider(provider) {
|
|
684
|
+
this.ensureHostVault();
|
|
685
|
+
this.db
|
|
686
|
+
.prepare(`UPDATE vaults SET metadata_json=? WHERE id=?`)
|
|
687
|
+
.run(JSON.stringify({ activeProvider: provider }), HOST_VAULT_ID);
|
|
688
|
+
}
|
|
689
|
+
deleteHostProviderRows(provider) {
|
|
690
|
+
const ids = this.hostModelRows()
|
|
691
|
+
.filter((row) => modelBinding(row).provider === provider)
|
|
692
|
+
.map((row) => row.id);
|
|
693
|
+
ids.push(hostModelCredentialId(provider));
|
|
694
|
+
for (const id of new Set(ids))
|
|
695
|
+
this.db
|
|
696
|
+
.prepare(`DELETE FROM vault_credentials WHERE id=? AND vault_id=?`)
|
|
697
|
+
.run(id, HOST_VAULT_ID);
|
|
698
|
+
}
|
|
699
|
+
credentialInfo(vaultId, id) {
|
|
700
|
+
const row = this.credentialRow(vaultId, id);
|
|
701
|
+
return {
|
|
702
|
+
id: row.id,
|
|
703
|
+
vaultId: row.vault_id,
|
|
704
|
+
name: row.name,
|
|
705
|
+
type: row.type,
|
|
706
|
+
binding: JSON.parse(row.binding_json),
|
|
707
|
+
...(row.expires_at ? { expiresAt: row.expires_at } : {}),
|
|
708
|
+
createdAt: row.created_at,
|
|
709
|
+
...(row.rotated_at ? { rotatedAt: row.rotated_at } : {}),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
credentialRow(vaultId, id) {
|
|
713
|
+
const row = this.db
|
|
714
|
+
.prepare(`SELECT id, vault_id, name, type, binding_json, expires_at, created_at, rotated_at,
|
|
715
|
+
kek_id, nonce, ciphertext, wrapped_dek
|
|
716
|
+
FROM vault_credentials WHERE id=? AND vault_id=?`)
|
|
717
|
+
.get(id, vaultId);
|
|
718
|
+
if (!row || row.type === "model")
|
|
719
|
+
throw new VaultError(404, "Credential not found");
|
|
720
|
+
return row;
|
|
721
|
+
}
|
|
722
|
+
rowsForVault(vaultId) {
|
|
723
|
+
return this.db
|
|
724
|
+
.prepare(`SELECT id, vault_id, name, type, binding_json, expires_at, created_at, rotated_at,
|
|
725
|
+
kek_id, nonce, ciphertext, wrapped_dek
|
|
726
|
+
FROM vault_credentials WHERE vault_id=?`)
|
|
727
|
+
.all(vaultId);
|
|
728
|
+
}
|
|
729
|
+
audit(entry) {
|
|
730
|
+
this.db
|
|
731
|
+
.prepare(`INSERT INTO vault_audit(id, at, actor, action, vault_id, credential_id, session_id, target, outcome)
|
|
732
|
+
VALUES(?,?,?,?,?,?,?,?,?)`)
|
|
733
|
+
.run(randomUUID(), new Date().toISOString(), entry.actor, entry.action, entry.vaultId ?? null, entry.credentialId ?? null, entry.sessionId ?? null, entry.target ?? null, entry.outcome);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function choose(matches, selection) {
|
|
737
|
+
const ids = matches.map((row) => row.id);
|
|
738
|
+
if (matches.length === 1) {
|
|
739
|
+
const only = matches[0];
|
|
740
|
+
if (selection && selection.credentialId !== only.id)
|
|
741
|
+
return {
|
|
742
|
+
kind: "refused",
|
|
743
|
+
credentialIds: ids,
|
|
744
|
+
reason: "selection_mismatch",
|
|
745
|
+
};
|
|
746
|
+
return { kind: "use", row: only };
|
|
747
|
+
}
|
|
748
|
+
if (!selection)
|
|
749
|
+
return { kind: "refused", credentialIds: ids, reason: "ambiguous" };
|
|
750
|
+
const chosen = matches.find((row) => row.id === selection.credentialId);
|
|
751
|
+
if (!chosen)
|
|
752
|
+
return {
|
|
753
|
+
kind: "refused",
|
|
754
|
+
credentialIds: ids,
|
|
755
|
+
reason: "selection_mismatch",
|
|
756
|
+
};
|
|
757
|
+
return { kind: "use", row: chosen };
|
|
758
|
+
}
|
|
759
|
+
function dueForRefresh(expiresAt) {
|
|
760
|
+
if (!expiresAt)
|
|
761
|
+
return false;
|
|
762
|
+
return Date.parse(expiresAt) - Date.now() <= REFRESH_SKEW_MS;
|
|
763
|
+
}
|
|
764
|
+
function payloadFromCreate(body) {
|
|
765
|
+
if (body.auth.type === "bearer")
|
|
766
|
+
return { token: body.auth.token };
|
|
767
|
+
const refresh = body.auth.refresh;
|
|
768
|
+
const tokenEndpoint = refresh
|
|
769
|
+
? normalizeVaultUrl(refresh.tokenEndpoint, { httpsOnly: true })
|
|
770
|
+
: undefined;
|
|
771
|
+
if (body.auth.expiresAt)
|
|
772
|
+
requireTimestamp(body.auth.expiresAt);
|
|
773
|
+
return {
|
|
774
|
+
accessToken: body.auth.accessToken,
|
|
775
|
+
...(refresh
|
|
776
|
+
? {
|
|
777
|
+
refreshToken: refresh.refreshToken,
|
|
778
|
+
tokenEndpoint,
|
|
779
|
+
clientId: refresh.clientId,
|
|
780
|
+
tokenEndpointAuth: refresh.tokenEndpointAuth.type,
|
|
781
|
+
...(refresh.tokenEndpointAuth.type === "none"
|
|
782
|
+
? {}
|
|
783
|
+
: { clientSecret: refresh.tokenEndpointAuth.clientSecret }),
|
|
784
|
+
}
|
|
785
|
+
: {}),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function expiresFromCreate(body) {
|
|
789
|
+
if (body.auth.type !== "oauth" || !body.auth.expiresAt)
|
|
790
|
+
return null;
|
|
791
|
+
return requireTimestamp(body.auth.expiresAt);
|
|
792
|
+
}
|
|
793
|
+
function requireTimestamp(value) {
|
|
794
|
+
if (Number.isNaN(Date.parse(value)))
|
|
795
|
+
throw new VaultError(400, "Credential expiry is invalid");
|
|
796
|
+
return new Date(Date.parse(value)).toISOString();
|
|
797
|
+
}
|
|
798
|
+
function modelBinding(row) {
|
|
799
|
+
const binding = JSON.parse(row.binding_json);
|
|
800
|
+
if (!binding.provider || !binding.model || !binding.authType)
|
|
801
|
+
throw new VaultError(500, "Host model credential is unreadable");
|
|
802
|
+
return binding;
|
|
803
|
+
}
|
|
804
|
+
function hostModelCredentialId(provider) {
|
|
805
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(provider))
|
|
806
|
+
throw new VaultError(400, "Invalid provider id");
|
|
807
|
+
return `${HOST_MODEL_ID}:${provider}`;
|
|
808
|
+
}
|
|
809
|
+
function hostModelAad(provider, model) {
|
|
810
|
+
return Buffer.from(canonical({
|
|
811
|
+
scope: "host",
|
|
812
|
+
vaultId: HOST_VAULT_ID,
|
|
813
|
+
credentialId: HOST_MODEL_ID,
|
|
814
|
+
type: "model",
|
|
815
|
+
provider,
|
|
816
|
+
model,
|
|
817
|
+
}), "utf8");
|
|
818
|
+
}
|
|
819
|
+
function credentialAad(vaultId, credentialId, type, url) {
|
|
820
|
+
return Buffer.from(canonical({ vaultId, credentialId, type, url }), "utf8");
|
|
821
|
+
}
|
|
822
|
+
function requestHash(body) {
|
|
823
|
+
const { requestId: _requestId, ...rest } = body;
|
|
824
|
+
return canonical(rest);
|
|
825
|
+
}
|