@tenuo/core 0.2.4-beta.0 → 0.3.0-beta.0
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 +49 -5
- package/dist/api.d.ts +416 -17
- package/dist/api.js.map +1 -1
- package/dist/client.d.ts +59 -2
- package/dist/client.js +700 -70
- package/dist/client.js.map +1 -1
- package/dist/connect.d.ts +53 -0
- package/dist/connect.js +177 -0
- package/dist/connect.js.map +1 -0
- package/dist/constraints.d.ts +54 -3
- package/dist/constraints.js +154 -6
- package/dist/constraints.js.map +1 -1
- package/dist/errors.d.ts +8 -3
- package/dist/errors.js +10 -1
- package/dist/errors.js.map +1 -1
- package/dist/generated/tenuo_wasm.d.ts +136 -7
- package/dist/generated/tenuo_wasm.js +398 -9
- package/dist/generated/tenuo_wasm_bg.wasm +0 -0
- package/dist/generated/tenuo_wasm_bg.wasm.d.ts +40 -22
- package/dist/identity.d.ts +25 -0
- package/dist/identity.js +61 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +18 -3
- package/dist/mcp.js +67 -59
- package/dist/mcp.js.map +1 -1
- package/dist/nonce.d.ts +6 -1
- package/dist/nonce.js +12 -1
- package/dist/nonce.js.map +1 -1
- package/dist/receipts.d.ts +27 -0
- package/dist/receipts.js +128 -0
- package/dist/receipts.js.map +1 -0
- package/dist/runtime.d.ts +51 -0
- package/dist/runtime.js +102 -0
- package/dist/runtime.js.map +1 -0
- package/dist/session.d.ts +9 -2
- package/dist/session.js +56 -0
- package/dist/session.js.map +1 -1
- package/dist/wasm.d.ts +207 -5
- package/dist/wasm.js +62 -4
- package/dist/wasm.js.map +1 -1
- package/package.json +3 -2
- package/dist/api.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/constraints.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/mcp.d.ts.map +0 -1
- package/dist/nonce.d.ts.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/wasm.d.ts.map +0 -1
package/dist/client.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import {
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { parseConnectToken } from "./connect.js";
|
|
4
|
+
import { generateIdentity, identityFromKey } from "./identity.js";
|
|
5
|
+
import { createMcp, presentCall, verifyPresented } from "./mcp.js";
|
|
6
|
+
import { collectReceipt, emitIsolatedReceipt, inheritSessionCollector } from "./receipts.js";
|
|
7
|
+
import { createRuntime } from "./runtime.js";
|
|
3
8
|
import { AuthorizationDeniedError, ApprovalRequiredError, TenuoConfigurationError, TenuoError } from "./errors.js";
|
|
4
9
|
import { Session, isSession, nativeSession } from "./session.js";
|
|
5
|
-
import { createDevContext, createVerifierContext, importSessionFromChain, importSessionFromWire, loadWasm, } from "./wasm.js";
|
|
10
|
+
import { createDevContext, createIssuerContext, createVerifierContext, importSessionFromChain, importSessionFromWire, loadWasm, protocolLimits, publicKeyHexFromHolderKey, } from "./wasm.js";
|
|
6
11
|
const currentSession = new AsyncLocalStorage();
|
|
7
12
|
const toolPolicies = new WeakMap();
|
|
8
13
|
const wrappedInners = new WeakSet();
|
|
14
|
+
const CLEARANCE_NAMES = new Set(["untrusted", "external", "partner", "internal", "privileged", "system"]);
|
|
9
15
|
function nodeEnv() {
|
|
10
16
|
if (typeof process === "undefined") {
|
|
11
17
|
return undefined;
|
|
@@ -45,18 +51,21 @@ function normalizeHex(value) {
|
|
|
45
51
|
}
|
|
46
52
|
return hex;
|
|
47
53
|
}
|
|
48
|
-
|
|
54
|
+
function envValue(name, what) {
|
|
49
55
|
if (name.length === 0) {
|
|
50
|
-
throw new TenuoConfigurationError(
|
|
56
|
+
throw new TenuoConfigurationError(`${what} requires an environment variable name`);
|
|
51
57
|
}
|
|
52
58
|
if (typeof process === "undefined") {
|
|
53
|
-
throw new TenuoConfigurationError(`Environment variable ${name} is not available. Tenuo fails closed without a
|
|
59
|
+
throw new TenuoConfigurationError(`Environment variable ${name} is not available. Tenuo fails closed without a ${what}.`);
|
|
54
60
|
}
|
|
55
61
|
const value = process.env[name];
|
|
56
62
|
if (value === undefined || value.length === 0) {
|
|
57
|
-
throw new TenuoConfigurationError(`Environment variable ${name} is not set or empty. Tenuo fails closed without a
|
|
63
|
+
throw new TenuoConfigurationError(`Environment variable ${name} is not set or empty. Tenuo fails closed without a ${what}.`);
|
|
58
64
|
}
|
|
59
|
-
return
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
export function publicKeyFromEnv(name) {
|
|
68
|
+
return { kind: "public-key", source: "env", hex: normalizeHex(envValue(name, "trusted root")) };
|
|
60
69
|
}
|
|
61
70
|
export function publicKeyFromHex(hex) {
|
|
62
71
|
return { kind: "public-key", source: "hex", hex: normalizeHex(hex) };
|
|
@@ -65,13 +74,12 @@ export function publicKeyFromBytes(bytes) {
|
|
|
65
74
|
if (bytes.length !== 32) {
|
|
66
75
|
throw new TenuoConfigurationError("publicKeyFromBytes() requires a 32-byte public key");
|
|
67
76
|
}
|
|
68
|
-
|
|
69
|
-
return { kind: "public-key", source: "bytes", hex };
|
|
77
|
+
return { kind: "public-key", source: "bytes", hex: bytesToHex(bytes) };
|
|
70
78
|
}
|
|
71
|
-
function normalizeSecretHex(value) {
|
|
79
|
+
function normalizeSecretHex(value, what) {
|
|
72
80
|
const hex = value.trim().toLowerCase().replace(/^0x/, "");
|
|
73
81
|
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
74
|
-
throw new TenuoConfigurationError(
|
|
82
|
+
throw new TenuoConfigurationError(`${what} must be a 32-byte hex secret (64 hex characters)`);
|
|
75
83
|
}
|
|
76
84
|
return hex;
|
|
77
85
|
}
|
|
@@ -82,21 +90,90 @@ function hexToBytes(hex) {
|
|
|
82
90
|
}
|
|
83
91
|
return bytes;
|
|
84
92
|
}
|
|
93
|
+
function bytesToHex(bytes) {
|
|
94
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
95
|
+
}
|
|
85
96
|
export function holderKeyFromEnv(name) {
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
return hexToBytes(normalizeSecretHex(envValue(name, "holder key"), "Holder key"));
|
|
98
|
+
}
|
|
99
|
+
export function holderKeyFromHex(hex) {
|
|
100
|
+
return hexToBytes(normalizeSecretHex(hex, "Holder key"));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A fresh 32-byte Ed25519 secret. One per agent, kept in that agent's
|
|
104
|
+
* process; only its public key (see `publicKeyFromHolderKey`) travels.
|
|
105
|
+
*/
|
|
106
|
+
export function generateHolderKey() {
|
|
107
|
+
return new Uint8Array(randomBytes(32));
|
|
108
|
+
}
|
|
109
|
+
/** Same shape as a holder key. Named separately so a control plane's key reads as one. */
|
|
110
|
+
export function generateIssuerKey() {
|
|
111
|
+
return generateHolderKey();
|
|
112
|
+
}
|
|
113
|
+
/** The public half of a holder or issuer secret. */
|
|
114
|
+
export function publicKeyFromHolderKey(holderKey) {
|
|
115
|
+
if (!(holderKey instanceof Uint8Array) || holderKey.length !== 32) {
|
|
116
|
+
throw new TenuoConfigurationError("publicKeyFromHolderKey() requires a 32-byte holder key");
|
|
88
117
|
}
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
loadWasm();
|
|
119
|
+
return { kind: "public-key", source: "bytes", hex: publicKeyHexFromHolderKey(holderKey) };
|
|
120
|
+
}
|
|
121
|
+
const MAX_WASM_U32 = 0xffff_ffff;
|
|
122
|
+
function requireUint32(value, name, minimum = 0) {
|
|
123
|
+
if (!Number.isInteger(value) || value < minimum || value > MAX_WASM_U32) {
|
|
124
|
+
throw new TenuoConfigurationError(`${name} must be an integer between ${minimum} and ${MAX_WASM_U32}`);
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
/** Stable control-plane key from the environment. This process becomes an issuer. */
|
|
129
|
+
export function issuerKeyFromEnv(name) {
|
|
130
|
+
return { kind: "issuer-key", secret: hexToBytes(normalizeSecretHex(envValue(name, "issuer key"), "Issuer key")) };
|
|
131
|
+
}
|
|
132
|
+
export function issuerKeyFromHex(hex) {
|
|
133
|
+
return { kind: "issuer-key", secret: hexToBytes(normalizeSecretHex(hex, "Issuer key")) };
|
|
134
|
+
}
|
|
135
|
+
export function issuerKeyFromBytes(secret) {
|
|
136
|
+
if (!(secret instanceof Uint8Array) || secret.length !== 32) {
|
|
137
|
+
throw new TenuoConfigurationError("issuerKeyFromBytes() requires a 32-byte secret");
|
|
95
138
|
}
|
|
96
|
-
return
|
|
139
|
+
return { kind: "issuer-key", secret: new Uint8Array(secret) };
|
|
97
140
|
}
|
|
98
|
-
|
|
99
|
-
|
|
141
|
+
function requireDepth(value, name) {
|
|
142
|
+
requireUint32(value, name);
|
|
143
|
+
const maximum = protocolLimits().max_delegation_depth;
|
|
144
|
+
if (value > maximum) {
|
|
145
|
+
throw new TenuoConfigurationError(`${name} ${value} exceeds the protocol maximum of ${maximum}`);
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
function requireTtl(value, name, minimum = 0) {
|
|
150
|
+
requireUint32(value, name, minimum);
|
|
151
|
+
const maximum = protocolLimits().max_warrant_ttl_seconds;
|
|
152
|
+
if (value > maximum) {
|
|
153
|
+
throw new TenuoConfigurationError(`${name} ${value} exceeds the protocol maximum of ${maximum}`);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
function requirePublicKey(value, name) {
|
|
158
|
+
if (value === null ||
|
|
159
|
+
typeof value !== "object" ||
|
|
160
|
+
value.kind !== "public-key" ||
|
|
161
|
+
typeof value.hex !== "string") {
|
|
162
|
+
throw new TenuoConfigurationError(`${name} must be a public key handle (createTenuo.publicKeyFromHolderKey / publicKeyFromHex / publicKeyFromEnv)`);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function clearanceJson(value, name) {
|
|
167
|
+
if (typeof value === "number") {
|
|
168
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
169
|
+
throw new TenuoConfigurationError(`${name} must be an integer 0-255 or a level name`);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === "string" && CLEARANCE_NAMES.has(value)) {
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
throw new TenuoConfigurationError(`${name} must be one of ${[...CLEARANCE_NAMES].join(", ")} or an integer 0-255`);
|
|
100
177
|
}
|
|
101
178
|
function rootHexes(options) {
|
|
102
179
|
const handles = [];
|
|
@@ -131,19 +208,48 @@ class TenuoClient {
|
|
|
131
208
|
context;
|
|
132
209
|
canMint;
|
|
133
210
|
mcp;
|
|
211
|
+
decide;
|
|
134
212
|
constructor(options) {
|
|
135
213
|
if (options.root?.kind === "dev-root") {
|
|
136
|
-
this.context = createDevContext();
|
|
214
|
+
this.context = createDevContext(options.receiptSigner);
|
|
215
|
+
this.canMint = true;
|
|
216
|
+
}
|
|
217
|
+
else if (options.root?.kind === "issuer-key") {
|
|
218
|
+
if (!(options.root.secret instanceof Uint8Array) || options.root.secret.length !== 32) {
|
|
219
|
+
throw new TenuoConfigurationError("root: issuer key must be a 32-byte secret");
|
|
220
|
+
}
|
|
221
|
+
this.context = createIssuerContext(options.root.secret, rootHexes(options), options.receiptSigner);
|
|
137
222
|
this.canMint = true;
|
|
138
223
|
}
|
|
139
224
|
else {
|
|
140
|
-
this.context = createVerifierContext(rootHexes(options));
|
|
225
|
+
this.context = createVerifierContext(rootHexes(options), undefined, options.receiptSigner);
|
|
141
226
|
this.canMint = false;
|
|
142
227
|
}
|
|
143
228
|
if (options.revocationList !== undefined) {
|
|
144
229
|
this.context.loadRevocationList(normalizeWireBytes(options.revocationList));
|
|
145
230
|
}
|
|
146
|
-
this.
|
|
231
|
+
this.decide = (decision, tool, native, args) => {
|
|
232
|
+
this.applyDecision(decision, tool, native, args);
|
|
233
|
+
};
|
|
234
|
+
this.mcp = createMcp(this.context, this.decide, this);
|
|
235
|
+
}
|
|
236
|
+
applyDecision(decision, tool, native, args) {
|
|
237
|
+
if (decision.outcome === "allow") {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (decision.outcome === "approval_required") {
|
|
241
|
+
let request;
|
|
242
|
+
if (native !== undefined && args !== undefined) {
|
|
243
|
+
try {
|
|
244
|
+
request = approvalRequestFromWasm(this.context.approvalRequest(native, tool, args));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
request = undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
throw new ApprovalRequiredError(decision.tool ?? tool, decision.required ?? 1, decision.received ?? 0, decision.message, request);
|
|
251
|
+
}
|
|
252
|
+
throw new AuthorizationDeniedError(decision.code ?? "TENUO_TOOL_NOT_AUTHORIZED", explainDeny(decision.message ?? "Authorization denied", decision.field), decision.field);
|
|
147
253
|
}
|
|
148
254
|
tool(inner, policy) {
|
|
149
255
|
if (toolPolicies.has(inner) || wrappedInners.has(inner)) {
|
|
@@ -153,15 +259,14 @@ class TenuoClient {
|
|
|
153
259
|
const original = inner.execute;
|
|
154
260
|
const execute = async (args, callOptions) => {
|
|
155
261
|
const session = resolveSession(callOptions);
|
|
156
|
-
const
|
|
157
|
-
|
|
262
|
+
const native = nativeSession(session);
|
|
263
|
+
const decision = this.context.authorize(native, capability, args, approvalsFrom(callOptions), policy.allow, requestIdFrom(callOptions));
|
|
264
|
+
emitReceipt(callOptions, decision.receipt, session, this);
|
|
158
265
|
if (decision.outcome === "allow") {
|
|
159
266
|
return original(plainArgs(decision.args), forwardExecuteOptions(callOptions));
|
|
160
267
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
throw new AuthorizationDeniedError(decision.code ?? "TENUO_TOOL_NOT_AUTHORIZED", explainDeny(decision.message ?? "Authorization denied", decision.field), decision.field);
|
|
268
|
+
this.applyDecision(decision, capability, native, plainArgs(args));
|
|
269
|
+
throw new TenuoConfigurationError("unreachable: decision was neither allow nor deny");
|
|
165
270
|
};
|
|
166
271
|
const wrapped = Object.assign(Object.create(Object.getPrototypeOf(inner)), inner, {
|
|
167
272
|
execute,
|
|
@@ -172,23 +277,95 @@ class TenuoClient {
|
|
|
172
277
|
}
|
|
173
278
|
session(input) {
|
|
174
279
|
const allow = collectSessionAllow(input);
|
|
175
|
-
|
|
280
|
+
const kind = input.kind ?? "execution";
|
|
281
|
+
if (kind !== "execution" && kind !== "issuer") {
|
|
282
|
+
throw new TenuoConfigurationError('session().kind must be "execution" or "issuer"');
|
|
283
|
+
}
|
|
284
|
+
if (Object.keys(allow).length === 0 && kind === "execution") {
|
|
176
285
|
throw new TenuoConfigurationError("tenuo.session() requires tools from tenuo.tool() or at least one capability in allow");
|
|
177
286
|
}
|
|
178
287
|
if (!this.canMint) {
|
|
179
|
-
throw new TenuoConfigurationError("session() mints a warrant and needs a local issuer. Use createTenuo({ root: createTenuo.devRoot() }).");
|
|
288
|
+
throw new TenuoConfigurationError("session() mints a warrant and needs a local issuer. Use createTenuo({ root: createTenuo.devRoot() }) or an issuer key.");
|
|
289
|
+
}
|
|
290
|
+
const options = { kind, allow };
|
|
291
|
+
if (input.ttlSeconds !== undefined) {
|
|
292
|
+
options.ttlSeconds = requireTtl(input.ttlSeconds, "session().ttlSeconds");
|
|
293
|
+
}
|
|
294
|
+
if (input.holder !== undefined) {
|
|
295
|
+
options.holder = requirePublicKey(input.holder, "session().holder").hex;
|
|
296
|
+
}
|
|
297
|
+
if (input.maxDepth !== undefined) {
|
|
298
|
+
options.maxDepth = requireDepth(input.maxDepth, "session().maxDepth");
|
|
299
|
+
}
|
|
300
|
+
if (input.clearance !== undefined) {
|
|
301
|
+
options.clearance = clearanceJson(input.clearance, "session().clearance");
|
|
302
|
+
}
|
|
303
|
+
if (input.sessionId !== undefined) {
|
|
304
|
+
options.sessionId = requireLabel(input.sessionId, "session().sessionId");
|
|
305
|
+
}
|
|
306
|
+
if (input.agentId !== undefined) {
|
|
307
|
+
options.agentId = requireLabel(input.agentId, "session().agentId");
|
|
180
308
|
}
|
|
181
|
-
const ttl = input.ttlSeconds ?? 0;
|
|
182
309
|
if (input.requireApproval !== undefined) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
310
|
+
options.requireApproval = requireApprovalJson(input.requireApproval);
|
|
311
|
+
}
|
|
312
|
+
if (input.issuableTools !== undefined) {
|
|
313
|
+
options.issuableTools = [...input.issuableTools];
|
|
314
|
+
}
|
|
315
|
+
if (input.constraintBounds !== undefined) {
|
|
316
|
+
options.constraintBounds = input.constraintBounds;
|
|
317
|
+
}
|
|
318
|
+
if (input.maxIssueDepth !== undefined) {
|
|
319
|
+
options.maxIssueDepth = requireDepth(input.maxIssueDepth, "session().maxIssueDepth");
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
return new Session(this.context.mintExtended(options));
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
issue(issuer, input) {
|
|
329
|
+
if (!isSession(issuer)) {
|
|
330
|
+
throw new TenuoConfigurationError("issue() requires a Tenuo Session, not a plain object");
|
|
331
|
+
}
|
|
332
|
+
if (input === null || typeof input !== "object" || input.allow === undefined || Object.keys(input.allow).length === 0) {
|
|
333
|
+
throw new TenuoConfigurationError("issue() requires at least one capability in allow");
|
|
334
|
+
}
|
|
335
|
+
const options = {
|
|
336
|
+
allow: input.allow,
|
|
337
|
+
holder: requirePublicKey(input.holder, "issue().holder").hex,
|
|
338
|
+
};
|
|
339
|
+
if (input.ttlSeconds !== undefined) {
|
|
340
|
+
options.ttlSeconds = requireTtl(input.ttlSeconds, "issue().ttlSeconds", 1);
|
|
341
|
+
}
|
|
342
|
+
if (input.maxDepth !== undefined) {
|
|
343
|
+
options.maxDepth = requireDepth(input.maxDepth, "issue().maxDepth");
|
|
344
|
+
}
|
|
345
|
+
if (input.clearance !== undefined) {
|
|
346
|
+
options.clearance = clearanceJson(input.clearance, "issue().clearance");
|
|
347
|
+
}
|
|
348
|
+
if (input.sessionId !== undefined) {
|
|
349
|
+
options.sessionId = requireLabel(input.sessionId, "issue().sessionId");
|
|
350
|
+
}
|
|
351
|
+
if (input.agentId !== undefined) {
|
|
352
|
+
options.agentId = requireLabel(input.agentId, "issue().agentId");
|
|
353
|
+
}
|
|
354
|
+
if (input.requireApproval !== undefined) {
|
|
355
|
+
options.requireApproval = requireApprovalJson(input.requireApproval);
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
return new Session(this.context.issue(nativeSession(issuer), options));
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
throw chainError(error);
|
|
189
362
|
}
|
|
190
|
-
|
|
191
|
-
|
|
363
|
+
}
|
|
364
|
+
issuerPublicKey() {
|
|
365
|
+
if (!this.canMint) {
|
|
366
|
+
throw new TenuoConfigurationError("issuerPublicKey() needs a local issuer. This context verifies against trustedRoots and does not mint.");
|
|
367
|
+
}
|
|
368
|
+
return { kind: "public-key", source: "bytes", hex: this.context.issuerPublicKey() };
|
|
192
369
|
}
|
|
193
370
|
sessionFromWire(input) {
|
|
194
371
|
if (!(input.holderKey instanceof Uint8Array) || input.holderKey.length !== 32) {
|
|
@@ -216,22 +393,128 @@ class TenuoClient {
|
|
|
216
393
|
}
|
|
217
394
|
return currentSession.run(session, fn);
|
|
218
395
|
}
|
|
219
|
-
narrow(session, allow) {
|
|
396
|
+
narrow(session, allow, options) {
|
|
220
397
|
if (Object.keys(allow).length === 0) {
|
|
221
398
|
throw new TenuoConfigurationError("tenuo.narrow() requires a non-empty allow policy");
|
|
222
399
|
}
|
|
223
400
|
if (!isSession(session)) {
|
|
224
401
|
throw new TenuoConfigurationError("narrow() requires a Tenuo Session, not a plain object");
|
|
225
402
|
}
|
|
403
|
+
const native = options === undefined ? undefined : narrowOptionsJson(options);
|
|
226
404
|
try {
|
|
227
|
-
|
|
405
|
+
const child = new Session(this.context.narrow(nativeSession(session), allow, native));
|
|
406
|
+
inheritSessionCollector(session, child);
|
|
407
|
+
return child;
|
|
228
408
|
}
|
|
229
409
|
catch (error) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
410
|
+
throw chainError(error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
explain(session, tool, args) {
|
|
414
|
+
if (!isSession(session)) {
|
|
415
|
+
throw new TenuoConfigurationError("explain() requires a Tenuo Session, not a plain object");
|
|
416
|
+
}
|
|
417
|
+
let raw;
|
|
418
|
+
try {
|
|
419
|
+
raw = this.context.explain(nativeSession(session), tool, args);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
423
|
+
}
|
|
424
|
+
const out = {
|
|
425
|
+
tool: raw.tool,
|
|
426
|
+
kind: raw.kind,
|
|
427
|
+
outcome: raw.outcome,
|
|
428
|
+
toolGranted: raw.tool_granted,
|
|
429
|
+
fields: raw.fields.map((f) => {
|
|
430
|
+
const field = {
|
|
431
|
+
field: f.field,
|
|
432
|
+
kind: f.kind,
|
|
433
|
+
constraint: f.constraint,
|
|
434
|
+
satisfied: f.satisfied,
|
|
435
|
+
};
|
|
436
|
+
if (f.value !== undefined) {
|
|
437
|
+
field.value = f.value;
|
|
438
|
+
}
|
|
439
|
+
if (f.reason !== undefined) {
|
|
440
|
+
field.reason = f.reason;
|
|
441
|
+
}
|
|
442
|
+
return field;
|
|
443
|
+
}),
|
|
444
|
+
unknownFields: [...raw.unknown_fields],
|
|
445
|
+
missingFields: [...raw.missing_fields],
|
|
446
|
+
expired: raw.expired,
|
|
447
|
+
expiresAt: raw.expires_at,
|
|
448
|
+
chainValid: raw.chain_valid,
|
|
449
|
+
};
|
|
450
|
+
if (raw.code !== undefined) {
|
|
451
|
+
out.code = raw.code;
|
|
452
|
+
}
|
|
453
|
+
if (raw.field !== undefined) {
|
|
454
|
+
out.field = raw.field;
|
|
455
|
+
}
|
|
456
|
+
if (raw.message !== undefined) {
|
|
457
|
+
out.message = raw.message;
|
|
458
|
+
}
|
|
459
|
+
if (raw.chain_error !== undefined) {
|
|
460
|
+
out.chainError = raw.chain_error;
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
approvalRequest(session, tool, args) {
|
|
465
|
+
if (!isSession(session)) {
|
|
466
|
+
throw new TenuoConfigurationError("approvalRequest() requires a Tenuo Session, not a plain object");
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
return approvalRequestFromWasm(this.context.approvalRequest(nativeSession(session), tool, args));
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
attestApprovalRequest(session, tool, args) {
|
|
476
|
+
if (!isSession(session)) {
|
|
477
|
+
throw new TenuoConfigurationError("attestApprovalRequest() requires a Tenuo Session, not a plain object");
|
|
478
|
+
}
|
|
479
|
+
let raw;
|
|
480
|
+
try {
|
|
481
|
+
raw = this.context.approvalContextAttestation(nativeSession(session), tool, args);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
version: raw.version,
|
|
488
|
+
canonicalization: raw.canonicalization,
|
|
489
|
+
warrantId: raw.warrant_id,
|
|
490
|
+
tool: raw.tool,
|
|
491
|
+
requestHash: raw.request_hash,
|
|
492
|
+
holderKeyHex: raw.holder_key_hex,
|
|
493
|
+
argsCanonicalCborB64: raw.args_canonical_cbor_b64,
|
|
494
|
+
signerKeyHex: raw.signer_key_hex,
|
|
495
|
+
signatureB64: raw.signature_b64,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
present(session, tool, args, options) {
|
|
499
|
+
if (!isSession(session)) {
|
|
500
|
+
throw new TenuoConfigurationError("present() requires a Tenuo Session, not a plain object");
|
|
501
|
+
}
|
|
502
|
+
return presentCall(this.context, this.decide, session, tool, args, options, "present()", this)
|
|
503
|
+
.presented;
|
|
504
|
+
}
|
|
505
|
+
verify(presented, tool, args, options) {
|
|
506
|
+
return verifyPresented(this.context, this.decide, presented, tool, args, options, "verify()", this);
|
|
507
|
+
}
|
|
508
|
+
revocationList(input) {
|
|
509
|
+
if (!this.canMint) {
|
|
510
|
+
throw new TenuoConfigurationError("revocationList() needs a local issuer key. Use createTenuo.signRevocationList() with an explicit secret, or an issuer-key root.");
|
|
511
|
+
}
|
|
512
|
+
const { ids, version } = revocationInput(input);
|
|
513
|
+
try {
|
|
514
|
+
return this.context.signRevocationListVersioned(ids, version);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
235
518
|
}
|
|
236
519
|
}
|
|
237
520
|
revoke(list) {
|
|
@@ -241,6 +524,54 @@ class TenuoClient {
|
|
|
241
524
|
loadWasm();
|
|
242
525
|
}
|
|
243
526
|
}
|
|
527
|
+
function requireLabel(value, name) {
|
|
528
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
529
|
+
throw new TenuoConfigurationError(`${name} must be a non-empty string`);
|
|
530
|
+
}
|
|
531
|
+
return value;
|
|
532
|
+
}
|
|
533
|
+
function errorMessage(error) {
|
|
534
|
+
return error instanceof Error ? error.message : String(error);
|
|
535
|
+
}
|
|
536
|
+
/** Map a prefixed core message from narrow()/issue() to the matching error class. */
|
|
537
|
+
function chainError(error) {
|
|
538
|
+
if (error instanceof TenuoError) {
|
|
539
|
+
return error;
|
|
540
|
+
}
|
|
541
|
+
const message = explainChainFailure(errorMessage(error));
|
|
542
|
+
for (const code of ["TENUO_CHAIN_INVALID", "TENUO_DEPTH_EXCEEDED", "TENUO_WARRANT_EXPIRED"]) {
|
|
543
|
+
if (message.startsWith(code)) {
|
|
544
|
+
return new AuthorizationDeniedError(code, message);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return new TenuoConfigurationError(message);
|
|
548
|
+
}
|
|
549
|
+
function explainChainFailure(message) {
|
|
550
|
+
const tool = /tool '([^']+)' not in parent's tools/.exec(message)?.[1];
|
|
551
|
+
if (tool !== undefined) {
|
|
552
|
+
return `${message}. narrow() cannot add '${tool}': remove it or delegate from a parent that grants it.`;
|
|
553
|
+
}
|
|
554
|
+
if (message.includes("parent's allowed set") ||
|
|
555
|
+
message.includes("range expanded") ||
|
|
556
|
+
message.includes("would expand permissions") ||
|
|
557
|
+
message.includes("clearance cannot increase")) {
|
|
558
|
+
return `${message}. narrow() only accepts a child policy that is equal to or stricter than its parent.`;
|
|
559
|
+
}
|
|
560
|
+
return message;
|
|
561
|
+
}
|
|
562
|
+
function revocationInput(input) {
|
|
563
|
+
if (input === null || typeof input !== "object" || !Array.isArray(input.revoke)) {
|
|
564
|
+
throw new TenuoConfigurationError("revocation list input must be { revoke: string[], version? }");
|
|
565
|
+
}
|
|
566
|
+
const ids = input.revoke.filter((id) => typeof id === "string" && id.length > 0);
|
|
567
|
+
if (ids.length === 0 || ids.length !== input.revoke.length) {
|
|
568
|
+
throw new TenuoConfigurationError("revoke must be a non-empty array of warrant ids");
|
|
569
|
+
}
|
|
570
|
+
if (input.version !== undefined && (!Number.isInteger(input.version) || input.version < 1)) {
|
|
571
|
+
throw new TenuoConfigurationError("revocation list version must be a positive integer");
|
|
572
|
+
}
|
|
573
|
+
return { ids, version: input.version };
|
|
574
|
+
}
|
|
244
575
|
function plainArgs(value) {
|
|
245
576
|
if (value instanceof Map) {
|
|
246
577
|
return Object.fromEntries(value);
|
|
@@ -251,33 +582,105 @@ function plainArgs(value) {
|
|
|
251
582
|
return {};
|
|
252
583
|
}
|
|
253
584
|
function requireApprovalJson(require) {
|
|
254
|
-
if (require ===
|
|
255
|
-
|
|
585
|
+
if (require === null || typeof require !== "object") {
|
|
586
|
+
throw new TenuoConfigurationError("requireApproval must be an object");
|
|
587
|
+
}
|
|
588
|
+
if (!Array.isArray(require.approvers) || require.approvers.length === 0) {
|
|
589
|
+
throw new TenuoConfigurationError("requireApproval.approvers must not be empty");
|
|
590
|
+
}
|
|
591
|
+
if (!Number.isInteger(require.min) || require.min < 1) {
|
|
592
|
+
throw new TenuoConfigurationError("requireApproval.min must be at least 1");
|
|
256
593
|
}
|
|
257
594
|
const payload = {
|
|
258
|
-
approvers: require.approvers.map((approver) => approver.hex),
|
|
595
|
+
approvers: require.approvers.map((approver, i) => requirePublicKey(approver, `requireApproval.approvers[${i}]`).hex),
|
|
259
596
|
min: require.min,
|
|
260
597
|
};
|
|
261
598
|
if (require.tools !== undefined) {
|
|
262
599
|
payload.tools = [...require.tools];
|
|
263
600
|
}
|
|
601
|
+
if (require.gates !== undefined) {
|
|
602
|
+
const gates = {};
|
|
603
|
+
for (const [tool, gate] of Object.entries(require.gates)) {
|
|
604
|
+
const out = {};
|
|
605
|
+
if (gate.message !== undefined) {
|
|
606
|
+
out.message = gate.message;
|
|
607
|
+
}
|
|
608
|
+
if (gate.args !== undefined) {
|
|
609
|
+
out.args = { ...gate.args };
|
|
610
|
+
}
|
|
611
|
+
gates[tool] = out;
|
|
612
|
+
}
|
|
613
|
+
payload.gates = gates;
|
|
614
|
+
}
|
|
264
615
|
return payload;
|
|
265
616
|
}
|
|
266
|
-
function
|
|
267
|
-
if (
|
|
268
|
-
|
|
617
|
+
function narrowOptionsJson(options) {
|
|
618
|
+
if (options === null || typeof options !== "object") {
|
|
619
|
+
throw new TenuoConfigurationError("narrow() options must be an object");
|
|
620
|
+
}
|
|
621
|
+
const known = ["holder", "ttlSeconds", "terminal", "maxDepth", "clearance", "agentId", "addApprovers", "minApprovals"];
|
|
622
|
+
for (const key of Object.keys(options)) {
|
|
623
|
+
if (!known.includes(key)) {
|
|
624
|
+
throw new TenuoConfigurationError(`narrow() options: unknown key '${key}' (expected ${known.join(", ")})`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const out = {};
|
|
628
|
+
if (options.holder !== undefined) {
|
|
629
|
+
out.holder = requirePublicKey(options.holder, "narrow().holder").hex;
|
|
630
|
+
}
|
|
631
|
+
if (options.ttlSeconds !== undefined) {
|
|
632
|
+
out.ttlSeconds = requireTtl(options.ttlSeconds, "narrow().ttlSeconds", 1);
|
|
633
|
+
}
|
|
634
|
+
if (options.terminal !== undefined) {
|
|
635
|
+
if (typeof options.terminal !== "boolean") {
|
|
636
|
+
throw new TenuoConfigurationError("narrow().terminal must be a boolean");
|
|
637
|
+
}
|
|
638
|
+
out.terminal = options.terminal;
|
|
639
|
+
}
|
|
640
|
+
if (options.maxDepth !== undefined) {
|
|
641
|
+
out.maxDepth = requireDepth(options.maxDepth, "narrow().maxDepth");
|
|
642
|
+
}
|
|
643
|
+
if (out.terminal === true && out.maxDepth !== undefined) {
|
|
644
|
+
throw new TenuoConfigurationError("narrow() options: pass terminal or maxDepth, not both");
|
|
269
645
|
}
|
|
270
|
-
if (
|
|
271
|
-
|
|
646
|
+
if (options.clearance !== undefined) {
|
|
647
|
+
out.clearance = clearanceJson(options.clearance, "narrow().clearance");
|
|
272
648
|
}
|
|
273
|
-
|
|
649
|
+
if (options.agentId !== undefined) {
|
|
650
|
+
out.agentId = requireLabel(options.agentId, "narrow().agentId");
|
|
651
|
+
}
|
|
652
|
+
if (options.addApprovers !== undefined) {
|
|
653
|
+
out.addApprovers = options.addApprovers.map((a, i) => requirePublicKey(a, `narrow().addApprovers[${i}]`).hex);
|
|
654
|
+
}
|
|
655
|
+
if (options.minApprovals !== undefined) {
|
|
656
|
+
if (!Number.isInteger(options.minApprovals) || options.minApprovals < 1) {
|
|
657
|
+
throw new TenuoConfigurationError("narrow().minApprovals must be at least 1");
|
|
658
|
+
}
|
|
659
|
+
out.minApprovals = options.minApprovals;
|
|
660
|
+
}
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
function approvalRequestFromWasm(raw) {
|
|
664
|
+
return {
|
|
665
|
+
requestId: raw.request_id,
|
|
666
|
+
warrantId: raw.warrant_id,
|
|
667
|
+
tool: raw.tool,
|
|
668
|
+
args: { ...raw.args },
|
|
669
|
+
requestHash: raw.request_hash,
|
|
670
|
+
holderPublicKey: raw.holder_public_key,
|
|
671
|
+
requiredApprovers: [...raw.required_approvers],
|
|
672
|
+
minApprovals: raw.min_approvals,
|
|
673
|
+
warrantExpiresAt: raw.warrant_expires_at,
|
|
674
|
+
createdAt: raw.created_at,
|
|
675
|
+
message: raw.message,
|
|
676
|
+
};
|
|
274
677
|
}
|
|
275
678
|
function explainDeny(message, field) {
|
|
276
679
|
if (!message.includes("unknown field not allowed")) {
|
|
277
680
|
return message;
|
|
278
681
|
}
|
|
279
682
|
const named = field !== undefined && field.length > 0 ? `'${field}'` : "this argument";
|
|
280
|
-
return `${message}. Zero-trust: name ${named} in allow (
|
|
683
|
+
return `${message}. Zero-trust: name ${named} in allow (for example ${named}: pattern("*")), or remove it from the call; constrained policies reject omitted fields.`;
|
|
281
684
|
}
|
|
282
685
|
function forwardExecuteOptions(callOptions) {
|
|
283
686
|
if (callOptions === null || typeof callOptions !== "object") {
|
|
@@ -296,7 +699,7 @@ function forwardExecuteOptions(callOptions) {
|
|
|
296
699
|
return Object.keys(rest).length === 0 ? undefined : rest;
|
|
297
700
|
}
|
|
298
701
|
function importWireError(error) {
|
|
299
|
-
const message =
|
|
702
|
+
const message = errorMessage(error);
|
|
300
703
|
if (message.startsWith("TENUO_CHAIN_INVALID") || message.includes("invalid warrant")) {
|
|
301
704
|
return new TenuoError("TENUO_CHAIN_INVALID", message);
|
|
302
705
|
}
|
|
@@ -306,15 +709,21 @@ function importWireError(error) {
|
|
|
306
709
|
if (message.startsWith("TENUO_UNTRUSTED_ROOT")) {
|
|
307
710
|
return new TenuoError("TENUO_UNTRUSTED_ROOT", message);
|
|
308
711
|
}
|
|
712
|
+
if (message.startsWith("TENUO_INVALID_POP")) {
|
|
713
|
+
// The warrant is fine; the key is not the one it was issued to.
|
|
714
|
+
const separator = /[.!?]$/.test(message.trim()) ? "" : ".";
|
|
715
|
+
return new AuthorizationDeniedError("TENUO_INVALID_POP", `${message}${separator} If this followed narrow(), set { holder: receiverPublicKey } for the agent that imports the child.`);
|
|
716
|
+
}
|
|
309
717
|
return new TenuoConfigurationError(message);
|
|
310
718
|
}
|
|
311
719
|
function normalizeWireBytes(value) {
|
|
312
720
|
if (typeof value === "string") {
|
|
313
721
|
return value;
|
|
314
722
|
}
|
|
315
|
-
return
|
|
723
|
+
return bytesToHex(value);
|
|
316
724
|
}
|
|
317
|
-
function emitReceipt(callOptions, receipt) {
|
|
725
|
+
function emitReceipt(callOptions, receipt, session, host) {
|
|
726
|
+
collectReceipt(receipt, session, host);
|
|
318
727
|
if (receipt === undefined || callOptions === null || typeof callOptions !== "object") {
|
|
319
728
|
return;
|
|
320
729
|
}
|
|
@@ -325,15 +734,7 @@ function emitReceipt(callOptions, receipt) {
|
|
|
325
734
|
if (typeof onReceipt !== "function") {
|
|
326
735
|
return;
|
|
327
736
|
}
|
|
328
|
-
|
|
329
|
-
const result = onReceipt(receipt);
|
|
330
|
-
if (result !== null && typeof result === "object" && "then" in result && typeof result.then === "function") {
|
|
331
|
-
void Promise.resolve(result).catch(() => undefined);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
// Receipt hooks must not deny or fail the tool.
|
|
336
|
-
}
|
|
737
|
+
emitIsolatedReceipt(onReceipt, receipt);
|
|
337
738
|
}
|
|
338
739
|
function requestIdFrom(callOptions) {
|
|
339
740
|
if (callOptions !== null && typeof callOptions === "object" && "requestId" in callOptions) {
|
|
@@ -396,6 +797,217 @@ function sortedAllow(allow) {
|
|
|
396
797
|
}
|
|
397
798
|
return out;
|
|
398
799
|
}
|
|
800
|
+
// ---------------------------------------------------------------------------
|
|
801
|
+
// Static helpers: approvals, revocation, receipts, control-plane wire shapes
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
function requestHashOf(request) {
|
|
804
|
+
const hash = typeof request === "string" ? request : request.requestHash;
|
|
805
|
+
const hex = hash.trim().toLowerCase();
|
|
806
|
+
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
807
|
+
throw new TenuoConfigurationError("signApproval() needs an ApprovalRequest or a 64-hex request hash");
|
|
808
|
+
}
|
|
809
|
+
return hex;
|
|
810
|
+
}
|
|
811
|
+
/** Sign an approval as an approver. The approver never needs the warrant or the holder key. */
|
|
812
|
+
export function signApproval(request, approverSecret, options) {
|
|
813
|
+
if (!(approverSecret instanceof Uint8Array) || approverSecret.length !== 32) {
|
|
814
|
+
throw new TenuoConfigurationError("signApproval() requires a 32-byte approver secret");
|
|
815
|
+
}
|
|
816
|
+
if (options === null || typeof options !== "object" || typeof options.externalId !== "string" || options.externalId.trim().length === 0) {
|
|
817
|
+
throw new TenuoConfigurationError("signApproval() requires options.externalId: who approved");
|
|
818
|
+
}
|
|
819
|
+
if (options.ttlSeconds !== undefined && (!Number.isInteger(options.ttlSeconds) || options.ttlSeconds < 1)) {
|
|
820
|
+
throw new TenuoConfigurationError("signApproval().ttlSeconds must be a positive integer");
|
|
821
|
+
}
|
|
822
|
+
const warrantExpiresAt = options.warrantExpiresAt ?? (typeof request === "object" ? request.warrantExpiresAt : undefined);
|
|
823
|
+
try {
|
|
824
|
+
return loadWasm().sdkSignApprovalForRequest(requestHashOf(request), approverSecret, options.externalId, options.ttlSeconds, warrantExpiresAt);
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
/** Decode an approval envelope. Not authorization: the call path still decides. */
|
|
831
|
+
export function inspectApproval(envelope) {
|
|
832
|
+
const text = typeof envelope === "string" ? envelope : Buffer.from(envelope).toString("base64");
|
|
833
|
+
let raw;
|
|
834
|
+
try {
|
|
835
|
+
raw = loadWasm().sdkInspectApproval(text);
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
839
|
+
}
|
|
840
|
+
const out = {
|
|
841
|
+
approverPublicKey: raw.approver_public_key,
|
|
842
|
+
requestHash: raw.request_hash,
|
|
843
|
+
externalId: raw.external_id,
|
|
844
|
+
approvedAt: raw.approved_at,
|
|
845
|
+
expiresAt: raw.expires_at,
|
|
846
|
+
expired: raw.expired,
|
|
847
|
+
signatureValid: raw.signature_valid,
|
|
848
|
+
};
|
|
849
|
+
if (raw.error !== undefined) {
|
|
850
|
+
out.error = raw.error;
|
|
851
|
+
}
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
854
|
+
/** Sign a revocation list with an explicit issuer secret. */
|
|
855
|
+
export function signRevocationList(input, issuerSecret) {
|
|
856
|
+
if (!(issuerSecret instanceof Uint8Array) || issuerSecret.length !== 32) {
|
|
857
|
+
throw new TenuoConfigurationError("signRevocationList() requires a 32-byte issuer secret");
|
|
858
|
+
}
|
|
859
|
+
const { ids, version } = revocationInput(input);
|
|
860
|
+
try {
|
|
861
|
+
return loadWasm().sdkSignRevocationListVersioned(ids, version, issuerSecret);
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
export function inspectRevocationList(wire) {
|
|
868
|
+
let raw;
|
|
869
|
+
try {
|
|
870
|
+
raw = loadWasm().sdkInspectRevocationList(normalizeWireBytes(wire));
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
version: raw.version,
|
|
877
|
+
issuedAt: raw.issued_at,
|
|
878
|
+
issuerPublicKey: raw.issuer_public_key,
|
|
879
|
+
revokedIds: [...raw.revoked_ids],
|
|
880
|
+
signatureValid: raw.signature_valid,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/** Signature authenticity of a receipt. Not authorization. */
|
|
884
|
+
export function verifyReceipt(wire) {
|
|
885
|
+
let raw;
|
|
886
|
+
try {
|
|
887
|
+
raw = loadWasm().sdkVerifyReceipt(normalizeWireBytes(wire));
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
891
|
+
}
|
|
892
|
+
const out = {
|
|
893
|
+
authentic: true,
|
|
894
|
+
signerKey: raw.signer_key,
|
|
895
|
+
outcome: raw.outcome,
|
|
896
|
+
action: raw.action,
|
|
897
|
+
requestId: raw.request_id,
|
|
898
|
+
};
|
|
899
|
+
if (raw.decision_code !== undefined)
|
|
900
|
+
out.decisionCode = raw.decision_code;
|
|
901
|
+
if (raw.srl_version !== undefined)
|
|
902
|
+
out.srlVersion = raw.srl_version;
|
|
903
|
+
if (raw.srl_hash !== undefined)
|
|
904
|
+
out.srlHash = raw.srl_hash;
|
|
905
|
+
if (raw.request_hash !== undefined)
|
|
906
|
+
out.requestHash = raw.request_hash;
|
|
907
|
+
if (raw.policy_definition_hash !== undefined)
|
|
908
|
+
out.policyDefinitionHash = raw.policy_definition_hash;
|
|
909
|
+
if (raw.prev_receipt_hash !== undefined)
|
|
910
|
+
out.prevReceiptHash = raw.prev_receipt_hash;
|
|
911
|
+
if (raw.trusted_roots_hash !== undefined)
|
|
912
|
+
out.trustedRootsHash = raw.trusted_roots_hash;
|
|
913
|
+
return out;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Root-anchored receipt check: the embedded warrant chain against `roots`
|
|
917
|
+
* at the receipt's own decision instant. Needs no trust in the signer.
|
|
918
|
+
*/
|
|
919
|
+
export function verifyReceiptChain(wire, roots) {
|
|
920
|
+
if (!Array.isArray(roots) || roots.length === 0) {
|
|
921
|
+
throw new TenuoConfigurationError("verifyReceiptChain() requires at least one trusted root");
|
|
922
|
+
}
|
|
923
|
+
let raw;
|
|
924
|
+
try {
|
|
925
|
+
raw = loadWasm().sdkVerifyReceiptChain(normalizeWireBytes(wire), roots.map((r, i) => requirePublicKey(r, `roots[${i}]`).hex));
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
928
|
+
throw new TenuoConfigurationError(errorMessage(error));
|
|
929
|
+
}
|
|
930
|
+
const out = {
|
|
931
|
+
signerKey: raw.signer_key,
|
|
932
|
+
outcome: raw.outcome,
|
|
933
|
+
timestamp: raw.timestamp,
|
|
934
|
+
chainValid: raw.chain_valid,
|
|
935
|
+
};
|
|
936
|
+
if (raw.decision_code !== undefined)
|
|
937
|
+
out.decisionCode = raw.decision_code;
|
|
938
|
+
if (raw.chain_error !== undefined)
|
|
939
|
+
out.chainError = raw.chain_error;
|
|
940
|
+
if (raw.corroborates_denial !== undefined)
|
|
941
|
+
out.corroboratesDenial = raw.corroborates_denial;
|
|
942
|
+
if (raw.root_issuer !== undefined)
|
|
943
|
+
out.rootIssuer = raw.root_issuer;
|
|
944
|
+
if (raw.leaf_holder !== undefined)
|
|
945
|
+
out.leafHolder = raw.leaf_holder;
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* The v1 JSON body a control plane's approval API accepts. Same shape the
|
|
950
|
+
* Python SDK produces, so one approval service serves both.
|
|
951
|
+
*/
|
|
952
|
+
export function controlPlaneApprovalRequestV1(request, options) {
|
|
953
|
+
if (request === null || typeof request !== "object" || typeof request.requestHash !== "string") {
|
|
954
|
+
throw new TenuoConfigurationError("controlPlaneApprovalRequestV1() needs an ApprovalRequest from tenuo.approvalRequest()");
|
|
955
|
+
}
|
|
956
|
+
const body = {
|
|
957
|
+
schema_version: 1,
|
|
958
|
+
request_id_hex: request.requestId,
|
|
959
|
+
warrant_id: request.warrantId,
|
|
960
|
+
tool: request.tool,
|
|
961
|
+
arguments: { ...request.args },
|
|
962
|
+
request_hash_hex: request.requestHash,
|
|
963
|
+
holder_public_key_hex: request.holderPublicKey,
|
|
964
|
+
required_approver_keys_hex: [...request.requiredApprovers],
|
|
965
|
+
min_approvals: Math.max(1, request.minApprovals),
|
|
966
|
+
warrant_expires_at_unix: request.warrantExpiresAt,
|
|
967
|
+
created_at_unix: request.createdAt,
|
|
968
|
+
};
|
|
969
|
+
if (options?.attestation !== undefined) {
|
|
970
|
+
const a = options.attestation;
|
|
971
|
+
body.attestation = {
|
|
972
|
+
version: a.version,
|
|
973
|
+
canonicalization: a.canonicalization,
|
|
974
|
+
warrant_id: a.warrantId,
|
|
975
|
+
tool: a.tool,
|
|
976
|
+
request_hash: a.requestHash,
|
|
977
|
+
holder_key_hex: a.holderKeyHex,
|
|
978
|
+
args_canonical_cbor_b64: a.argsCanonicalCborB64,
|
|
979
|
+
signer_key_hex: a.signerKeyHex,
|
|
980
|
+
signature_b64: a.signatureB64,
|
|
981
|
+
// Python readers look for these two names.
|
|
982
|
+
signer_key: a.signerKeyHex,
|
|
983
|
+
signature: a.signatureB64,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
if (options?.temporal !== undefined && Object.keys(options.temporal).length > 0) {
|
|
987
|
+
body.temporal = { ...options.temporal };
|
|
988
|
+
}
|
|
989
|
+
if (request.message.length > 0) {
|
|
990
|
+
body.message = request.message;
|
|
991
|
+
}
|
|
992
|
+
return body;
|
|
993
|
+
}
|
|
994
|
+
/** Approval envelopes from a v1 response, ready for `{ approvals }` on the call. Empty unless status is a success. */
|
|
995
|
+
export function signedApprovalsFromResponseV1(response) {
|
|
996
|
+
if (response === null || typeof response !== "object" || typeof response.status !== "string") {
|
|
997
|
+
throw new TenuoConfigurationError("approval response must be an object with a status");
|
|
998
|
+
}
|
|
999
|
+
if (response.status !== "approved") {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
const list = response.signed_approvals_b64;
|
|
1003
|
+
if (list === undefined || list === null) {
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
if (!Array.isArray(list) || list.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
1007
|
+
throw new TenuoConfigurationError("signed_approvals_b64 must be an array of base64 strings");
|
|
1008
|
+
}
|
|
1009
|
+
return [...list];
|
|
1010
|
+
}
|
|
399
1011
|
function createTenuoImpl(options = {}) {
|
|
400
1012
|
if (!hasTrustAnchor(options)) {
|
|
401
1013
|
throw new TenuoConfigurationError("createTenuo() requires trustedRoots or root: createTenuo.devRoot(). An empty trust set is not a configuration.");
|
|
@@ -413,5 +1025,23 @@ export const createTenuo = Object.assign(createTenuoImpl, {
|
|
|
413
1025
|
publicKeyFromBytes,
|
|
414
1026
|
holderKeyFromEnv,
|
|
415
1027
|
holderKeyFromHex,
|
|
1028
|
+
generateHolderKey,
|
|
1029
|
+
generateIssuerKey,
|
|
1030
|
+
publicKeyFromHolderKey,
|
|
1031
|
+
issuerKeyFromEnv,
|
|
1032
|
+
issuerKeyFromHex,
|
|
1033
|
+
issuerKeyFromBytes,
|
|
1034
|
+
signApproval,
|
|
1035
|
+
inspectApproval,
|
|
1036
|
+
signRevocationList,
|
|
1037
|
+
inspectRevocationList,
|
|
1038
|
+
verifyReceipt,
|
|
1039
|
+
verifyReceiptChain,
|
|
1040
|
+
controlPlaneApprovalRequestV1,
|
|
1041
|
+
signedApprovalsFromResponseV1,
|
|
1042
|
+
parseConnectToken,
|
|
1043
|
+
identity: identityFromKey,
|
|
1044
|
+
generateIdentity,
|
|
1045
|
+
runtime: (options) => createRuntime(createTenuoImpl, options),
|
|
416
1046
|
});
|
|
417
1047
|
//# sourceMappingURL=client.js.map
|