@supernovae-st/nika 0.71.0 → 0.118.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +562 -30
- package/dist/bin/nika.js +407 -0
- package/dist/index.cjs +2889 -0
- package/dist/index.d.cts +593 -0
- package/dist/index.d.ts +593 -0
- package/dist/index.js +2838 -0
- package/docs/architecture.md +85 -0
- package/docs/http-api.md +87 -0
- package/docs/migrating-to-0.116.md +147 -0
- package/docs/testing.md +156 -0
- package/openapi.json +1 -0
- package/package.json +76 -24
- package/bin.js +0 -49
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2889 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
Nika: () => Nika,
|
|
34
|
+
NikaCompatibilityError: () => NikaCompatibilityError,
|
|
35
|
+
NikaConfigurationError: () => NikaConfigurationError,
|
|
36
|
+
NikaEngineUnavailable: () => NikaEngineUnavailable,
|
|
37
|
+
NikaError: () => NikaError,
|
|
38
|
+
NikaEventBufferOverflowError: () => NikaEventBufferOverflowError,
|
|
39
|
+
NikaObservationInterrupted: () => NikaObservationInterrupted,
|
|
40
|
+
NikaOperationError: () => NikaOperationError,
|
|
41
|
+
NikaProtocolError: () => NikaProtocolError,
|
|
42
|
+
NikaRunOwnershipError: () => NikaRunOwnershipError,
|
|
43
|
+
NikaTransportError: () => NikaTransportError,
|
|
44
|
+
isNikaRunSealedEvent: () => isNikaRunSealedEvent,
|
|
45
|
+
isNikaRunSettledEvent: () => isNikaRunSettledEvent,
|
|
46
|
+
isNikaTerminalEvent: () => isNikaTerminalEvent
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(index_exports);
|
|
49
|
+
|
|
50
|
+
// src/errors.ts
|
|
51
|
+
var NikaError = class extends Error {
|
|
52
|
+
constructor(message, options) {
|
|
53
|
+
super(message, options);
|
|
54
|
+
this.name = "NikaError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var NikaConfigurationError = class extends NikaError {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "NikaConfigurationError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var NikaTransportError = class extends NikaError {
|
|
64
|
+
transport;
|
|
65
|
+
constructor(transport, message, options) {
|
|
66
|
+
super(message, options);
|
|
67
|
+
this.name = "NikaTransportError";
|
|
68
|
+
this.transport = transport;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var NikaCompatibilityError = class extends NikaError {
|
|
72
|
+
capability;
|
|
73
|
+
transport;
|
|
74
|
+
constructor(capability, transport, message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "NikaCompatibilityError";
|
|
77
|
+
this.capability = capability;
|
|
78
|
+
this.transport = transport;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var NikaProtocolError = class extends NikaTransportError {
|
|
82
|
+
constructor(transport, message, options) {
|
|
83
|
+
super(transport, message, options);
|
|
84
|
+
this.name = "NikaProtocolError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var NikaObservationInterrupted = class extends NikaTransportError {
|
|
88
|
+
runId;
|
|
89
|
+
lastSequence;
|
|
90
|
+
attempts;
|
|
91
|
+
constructor(transport, runId, lastSequence, attempts) {
|
|
92
|
+
super(
|
|
93
|
+
transport,
|
|
94
|
+
`HTTP observation interrupted after ${attempts} retries; run ${runId} last acknowledged sequence ${lastSequence}`
|
|
95
|
+
);
|
|
96
|
+
this.name = "NikaObservationInterrupted";
|
|
97
|
+
this.runId = runId;
|
|
98
|
+
this.lastSequence = lastSequence;
|
|
99
|
+
this.attempts = attempts;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var NikaOperationError = class extends NikaError {
|
|
103
|
+
operation;
|
|
104
|
+
code;
|
|
105
|
+
transport;
|
|
106
|
+
status;
|
|
107
|
+
findings;
|
|
108
|
+
currentRevision;
|
|
109
|
+
machineCode;
|
|
110
|
+
constructor(operation, transport, code, message, details) {
|
|
111
|
+
super(message);
|
|
112
|
+
this.name = "NikaOperationError";
|
|
113
|
+
this.operation = operation;
|
|
114
|
+
this.code = code;
|
|
115
|
+
this.transport = transport;
|
|
116
|
+
this.status = details.status;
|
|
117
|
+
this.findings = details.findings;
|
|
118
|
+
this.currentRevision = details.currentRevision;
|
|
119
|
+
this.machineCode = details.machineCode;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var NikaEventBufferOverflowError = class extends NikaError {
|
|
123
|
+
runId;
|
|
124
|
+
limit;
|
|
125
|
+
constructor(runId, limit) {
|
|
126
|
+
super(`Event subscriber for run ${runId} exceeded its ${limit}-event buffer`);
|
|
127
|
+
this.name = "NikaEventBufferOverflowError";
|
|
128
|
+
this.runId = runId;
|
|
129
|
+
this.limit = limit;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var NikaRunOwnershipError = class extends NikaError {
|
|
133
|
+
constructor() {
|
|
134
|
+
super("The NikaRun was not created by this Nika client");
|
|
135
|
+
this.name = "NikaRunOwnershipError";
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// src/lib/http-transport.ts
|
|
140
|
+
var import_node_crypto2 = require("crypto");
|
|
141
|
+
|
|
142
|
+
// src/lib/binary/error.ts
|
|
143
|
+
var NikaEngineUnavailable = class extends NikaError {
|
|
144
|
+
code = "NIKA_ENGINE_UNAVAILABLE";
|
|
145
|
+
platform;
|
|
146
|
+
arch;
|
|
147
|
+
packageName;
|
|
148
|
+
constructor(platform, arch, packageName) {
|
|
149
|
+
const target = `${platform}-${arch}`;
|
|
150
|
+
const detail = packageName ? `the optional payload ${packageName} is not installed (npm leaves it UNMET OPTIONAL when the registry has no version matching this client)` : "there is no packaged payload for this host";
|
|
151
|
+
super(
|
|
152
|
+
`Nika engine unavailable for ${target}: ${detail}. A nika found on PATH is deliberately not used. Set NIKA_BIN=/absolute/path/to/nika, pass config.bin, or install the matching payload package.`
|
|
153
|
+
);
|
|
154
|
+
this.name = "NikaEngineUnavailable";
|
|
155
|
+
this.platform = platform;
|
|
156
|
+
this.arch = arch;
|
|
157
|
+
this.packageName = packageName;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/lib/binary/packages.ts
|
|
162
|
+
var NATIVE_PACKAGES = {
|
|
163
|
+
"darwin-arm64": "@supernovae-st/nika-darwin-arm64",
|
|
164
|
+
"darwin-x64": "@supernovae-st/nika-darwin-x64",
|
|
165
|
+
"linux-x64": "@supernovae-st/nika-linux-x64",
|
|
166
|
+
"linux-arm64": "@supernovae-st/nika-linux-arm64"
|
|
167
|
+
};
|
|
168
|
+
function packageForHost(platform, arch, glibc) {
|
|
169
|
+
if (platform === "linux" && !glibc) return void 0;
|
|
170
|
+
return NATIVE_PACKAGES[`${platform}-${arch}`];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/lib/binary/resolve.ts
|
|
174
|
+
var import_node_module = require("module");
|
|
175
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
176
|
+
var import_meta = {};
|
|
177
|
+
var requireFromBundle = (0, import_node_module.createRequire)(
|
|
178
|
+
typeof import_meta.url === "string" ? import_meta.url : __filename
|
|
179
|
+
);
|
|
180
|
+
function resolveNikaEngine(configuredBin, host = {}) {
|
|
181
|
+
if (configuredBin !== void 0) {
|
|
182
|
+
return { bin: absoluteEnginePath(configuredBin, "bin") };
|
|
183
|
+
}
|
|
184
|
+
const envBin = (host.env ?? process.env).NIKA_BIN;
|
|
185
|
+
if (envBin !== void 0) {
|
|
186
|
+
return { bin: absoluteEnginePath(envBin, "NIKA_BIN") };
|
|
187
|
+
}
|
|
188
|
+
const platform = host.platform ?? process.platform;
|
|
189
|
+
const arch = host.arch ?? process.arch;
|
|
190
|
+
const packageName = packageForHost(
|
|
191
|
+
platform,
|
|
192
|
+
arch,
|
|
193
|
+
host.glibc ?? runtimeIsGlibc(platform)
|
|
194
|
+
);
|
|
195
|
+
if (!packageName) throw new NikaEngineUnavailable(platform, arch);
|
|
196
|
+
try {
|
|
197
|
+
const packageJson = (host.resolvePackageJson ?? requireFromBundle.resolve)(
|
|
198
|
+
`${packageName}/package.json`
|
|
199
|
+
);
|
|
200
|
+
const packageRoot = import_node_path.default.dirname(packageJson);
|
|
201
|
+
return {
|
|
202
|
+
bin: import_node_path.default.join(packageRoot, "bin", "nika"),
|
|
203
|
+
packageName,
|
|
204
|
+
packageRoot
|
|
205
|
+
};
|
|
206
|
+
} catch (cause) {
|
|
207
|
+
if (isMissingModule(cause)) {
|
|
208
|
+
throw new NikaEngineUnavailable(platform, arch, packageName);
|
|
209
|
+
}
|
|
210
|
+
throw cause;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function absoluteEnginePath(value, source) {
|
|
214
|
+
if (value.length === 0) {
|
|
215
|
+
throw new NikaConfigurationError(`${source} must be a non-empty string`);
|
|
216
|
+
}
|
|
217
|
+
if (!import_node_path.default.isAbsolute(value)) {
|
|
218
|
+
throw new NikaConfigurationError(
|
|
219
|
+
`${source} must be an absolute path to a nika engine (got "${value}"); a bare name or a relative path would be resolved through PATH or the working directory, which this client refuses`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function runtimeIsGlibc(platform) {
|
|
225
|
+
if (platform !== "linux") return true;
|
|
226
|
+
const report = process.report?.getReport();
|
|
227
|
+
return typeof report?.header?.glibcVersionRuntime === "string";
|
|
228
|
+
}
|
|
229
|
+
function isMissingModule(cause) {
|
|
230
|
+
return cause instanceof Error && "code" in cause && cause.code === "MODULE_NOT_FOUND";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/lib/binary/verify.ts
|
|
234
|
+
var import_node_crypto = require("crypto");
|
|
235
|
+
var import_node_fs = require("fs");
|
|
236
|
+
var import_promises = require("fs/promises");
|
|
237
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
238
|
+
|
|
239
|
+
// src/lib/engine-capture.ts
|
|
240
|
+
var import_node_child_process = require("child_process");
|
|
241
|
+
function captureEngine(bin, args, options) {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
if (options.signal?.aborted) {
|
|
244
|
+
reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const child = (0, import_node_child_process.spawn)(bin, args, {
|
|
248
|
+
cwd: options.cwd,
|
|
249
|
+
shell: false,
|
|
250
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
251
|
+
});
|
|
252
|
+
let stdout = "";
|
|
253
|
+
let stderr = "";
|
|
254
|
+
let overflow = false;
|
|
255
|
+
let spawnError;
|
|
256
|
+
const append = (stream, chunk) => {
|
|
257
|
+
if (overflow) return;
|
|
258
|
+
if (stream === "stdout") stdout += chunk;
|
|
259
|
+
else stderr += chunk;
|
|
260
|
+
if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
|
|
261
|
+
overflow = true;
|
|
262
|
+
child.kill("SIGTERM");
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
child.stdout.setEncoding("utf8");
|
|
266
|
+
child.stderr.setEncoding("utf8");
|
|
267
|
+
child.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
268
|
+
child.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
269
|
+
const abort = () => child.kill("SIGTERM");
|
|
270
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
271
|
+
child.once("error", (cause) => {
|
|
272
|
+
spawnError = cause;
|
|
273
|
+
});
|
|
274
|
+
child.once("close", (code) => {
|
|
275
|
+
options.signal?.removeEventListener("abort", abort);
|
|
276
|
+
if (options.signal?.aborted) {
|
|
277
|
+
reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
|
|
278
|
+
} else if (spawnError) {
|
|
279
|
+
reject(new NikaTransportError(
|
|
280
|
+
options.transport,
|
|
281
|
+
`Cannot spawn ${bin} for ${options.label}: ${spawnError.message}`,
|
|
282
|
+
{ cause: spawnError }
|
|
283
|
+
));
|
|
284
|
+
} else if (overflow) {
|
|
285
|
+
reject(new NikaProtocolError(
|
|
286
|
+
options.transport,
|
|
287
|
+
`${options.label} exceeded ${options.bufferBytes} bytes`
|
|
288
|
+
));
|
|
289
|
+
} else {
|
|
290
|
+
resolve({ exitCode: code ?? 3, stdout, stderr });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/lib/settlement.ts
|
|
297
|
+
var TALLY_FIELDS = [
|
|
298
|
+
"total",
|
|
299
|
+
"ok",
|
|
300
|
+
"failed",
|
|
301
|
+
"recovered",
|
|
302
|
+
"skipped",
|
|
303
|
+
"cancelled",
|
|
304
|
+
"never_started"
|
|
305
|
+
];
|
|
306
|
+
function readSettlement(value, transport, expectedStatus) {
|
|
307
|
+
const fault = (detail) => {
|
|
308
|
+
throw new NikaProtocolError(transport, `Engine settlement was malformed: ${detail}`);
|
|
309
|
+
};
|
|
310
|
+
const settlement = record(value) ?? fault("not an object");
|
|
311
|
+
for (const field of ["status", "cause"]) {
|
|
312
|
+
if (settlement[field] !== void 0 && typeof settlement[field] !== "string") {
|
|
313
|
+
fault(`${field} was not a string`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (settlement.status !== void 0 && expectedStatus !== void 0 && settlement.status !== expectedStatus) {
|
|
317
|
+
fault(`status ${String(settlement.status)} contradicts the record's ${expectedStatus}`);
|
|
318
|
+
}
|
|
319
|
+
count("elapsed_ms", settlement.elapsed_ms, fault);
|
|
320
|
+
if (settlement.tasks !== void 0) {
|
|
321
|
+
const tasks = record(settlement.tasks) ?? fault("tasks was not an object");
|
|
322
|
+
for (const field of TALLY_FIELDS) count(`tasks.${field}`, tasks[field], fault);
|
|
323
|
+
}
|
|
324
|
+
if (settlement.spend !== void 0) {
|
|
325
|
+
const spend = record(settlement.spend) ?? fault("spend was not an object");
|
|
326
|
+
if (spend.qualifier !== void 0 && typeof spend.qualifier !== "string") {
|
|
327
|
+
fault("spend.qualifier was not a string");
|
|
328
|
+
}
|
|
329
|
+
if (spend.pricing_as_of !== void 0 && spend.pricing_as_of !== null && typeof spend.pricing_as_of !== "string") {
|
|
330
|
+
fault("spend.pricing_as_of was not a string");
|
|
331
|
+
}
|
|
332
|
+
count("spend.priced_calls", spend.priced_calls, fault);
|
|
333
|
+
count("spend.unpriced_calls", spend.unpriced_calls, fault);
|
|
334
|
+
if (spend.total_cost_usd !== void 0 && spend.total_cost_usd !== null) {
|
|
335
|
+
amount("spend.total_cost_usd", spend.total_cost_usd, fault);
|
|
336
|
+
}
|
|
337
|
+
if (spend.by_source !== void 0) {
|
|
338
|
+
const bySource = record(spend.by_source) ?? fault("spend.by_source was not an object");
|
|
339
|
+
for (const [source, cost] of Object.entries(bySource)) {
|
|
340
|
+
amount(`spend.by_source.${source}`, cost, fault);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (settlement.error !== void 0) {
|
|
345
|
+
const error = record(settlement.error) ?? fault("error was not an object");
|
|
346
|
+
if (typeof error.code !== "string" || typeof error.message !== "string") {
|
|
347
|
+
fault("error omitted its code or message");
|
|
348
|
+
}
|
|
349
|
+
if (error.task !== void 0 && typeof error.task !== "string") {
|
|
350
|
+
fault("error.task was not a string");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return settlement;
|
|
354
|
+
}
|
|
355
|
+
function record(value) {
|
|
356
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
357
|
+
}
|
|
358
|
+
function count(name, value, fault) {
|
|
359
|
+
if (value === void 0) return;
|
|
360
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
361
|
+
fault(`${name} was not a non-negative integer`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function amount(name, value, fault) {
|
|
365
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
366
|
+
fault(`${name} was not a non-negative number`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/lib/machine.ts
|
|
371
|
+
function machineObject(value) {
|
|
372
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
373
|
+
}
|
|
374
|
+
function parseMachineObject(text, transport) {
|
|
375
|
+
let value;
|
|
376
|
+
try {
|
|
377
|
+
value = JSON.parse(text);
|
|
378
|
+
} catch (cause) {
|
|
379
|
+
throw new NikaProtocolError(transport, "Engine machine output was not valid JSON", {
|
|
380
|
+
cause: cause instanceof Error ? cause : void 0
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const object = machineObject(value);
|
|
384
|
+
if (!object) {
|
|
385
|
+
throw new NikaProtocolError(transport, "Engine machine output was not a JSON object");
|
|
386
|
+
}
|
|
387
|
+
return object;
|
|
388
|
+
}
|
|
389
|
+
function eventStatus(event) {
|
|
390
|
+
if (typeof event?.status === "string") return event.status;
|
|
391
|
+
const fields = Array.isArray(event?.fields) ? event.fields : [];
|
|
392
|
+
for (const field of fields) {
|
|
393
|
+
const row = machineObject(field);
|
|
394
|
+
if (row?.key === "status" && typeof row.value === "string") return row.value;
|
|
395
|
+
}
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
398
|
+
function eventSettlement(event, transport = "http") {
|
|
399
|
+
if (!event) return void 0;
|
|
400
|
+
const record2 = event;
|
|
401
|
+
if (record2.settlement !== void 0) {
|
|
402
|
+
return readSettlement(record2.settlement, transport, eventStatus(event));
|
|
403
|
+
}
|
|
404
|
+
if (["cause", "elapsed_ms", "tasks", "spend"].every((key) => record2[key] === void 0)) return void 0;
|
|
405
|
+
const fields = ["status", "cause", "elapsed_ms", "tasks", "spend", "error"];
|
|
406
|
+
return readSettlement(Object.fromEntries(fields.filter((key) => record2[key] !== void 0).map((key) => [key, record2[key]])), transport, eventStatus(event));
|
|
407
|
+
}
|
|
408
|
+
function eventReceipt(event) {
|
|
409
|
+
const direct = machineObject(event?.receipt);
|
|
410
|
+
if (direct) return Object.freeze(direct);
|
|
411
|
+
const fields = Array.isArray(event?.fields) ? event.fields : [];
|
|
412
|
+
for (const field of fields) {
|
|
413
|
+
const row = machineObject(field);
|
|
414
|
+
if (row?.key === "receipt") {
|
|
415
|
+
const receipt = machineObject(row.value);
|
|
416
|
+
if (receipt) return Object.freeze(receipt);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return void 0;
|
|
420
|
+
}
|
|
421
|
+
function eventOutputs(event) {
|
|
422
|
+
const direct = machineObject(event?.outputs);
|
|
423
|
+
if (direct) return direct;
|
|
424
|
+
const fields = Array.isArray(event?.fields) ? event.fields : [];
|
|
425
|
+
for (const field of fields) {
|
|
426
|
+
const row = machineObject(field);
|
|
427
|
+
if (row?.key === "outputs") return machineObject(row.value);
|
|
428
|
+
}
|
|
429
|
+
return void 0;
|
|
430
|
+
}
|
|
431
|
+
function eventError(event) {
|
|
432
|
+
const settled = machineObject(machineObject(event?.settlement)?.error);
|
|
433
|
+
if (settled) return settled;
|
|
434
|
+
const direct = machineObject(event?.error);
|
|
435
|
+
if (direct) return direct;
|
|
436
|
+
if (typeof event?.code === "string" || typeof event?.message === "string") {
|
|
437
|
+
return {
|
|
438
|
+
...typeof event.code === "string" ? { code: event.code } : {},
|
|
439
|
+
...typeof event.message === "string" ? { message: event.message } : {}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return fieldsError(event);
|
|
443
|
+
}
|
|
444
|
+
function fieldsError(event) {
|
|
445
|
+
if (event?.kind !== "task_failed") return void 0;
|
|
446
|
+
const fields = Array.isArray(event.fields) ? event.fields : [];
|
|
447
|
+
let detail;
|
|
448
|
+
let task;
|
|
449
|
+
for (const field of fields) {
|
|
450
|
+
const row = machineObject(field);
|
|
451
|
+
if (row?.key === "detail" && typeof row.value === "string") detail = row.value;
|
|
452
|
+
if (row?.key === "task" && typeof row.value === "string") task = row.value;
|
|
453
|
+
}
|
|
454
|
+
if (detail === void 0) return void 0;
|
|
455
|
+
const match = detail.trim().match(/^(NIKA-[A-Z0-9-]+)\s*·?\s*([\s\S]*)$/);
|
|
456
|
+
const code = match?.[1];
|
|
457
|
+
const message = (match ? match[2] : detail).trim();
|
|
458
|
+
return {
|
|
459
|
+
...code ? { code } : {},
|
|
460
|
+
...message.length > 0 ? { message } : {},
|
|
461
|
+
...task ? { task } : {}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function receiptTraceLocator(receipt) {
|
|
465
|
+
const value = receipt.trace_path;
|
|
466
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/lib/engine-identity.ts
|
|
470
|
+
var MACHINE_PROTOCOL_VERSION = 1;
|
|
471
|
+
var REQUIRED_CAPABILITIES = ["check", "executionSnapshot", "eventStream"];
|
|
472
|
+
var COMPATIBILITY_CLOCKS = [
|
|
473
|
+
"machineProtocolVersion",
|
|
474
|
+
"snapshotFormatVersion",
|
|
475
|
+
"checkReportVersion",
|
|
476
|
+
"eventFormatVersion"
|
|
477
|
+
];
|
|
478
|
+
function compatibleEngineIdentity(value, transport, peer) {
|
|
479
|
+
const identity = machineObject(value);
|
|
480
|
+
if (!identity) throw incompatible(transport, "Engine identity is not an object");
|
|
481
|
+
if (typeof identity.engineVersion !== "string" || identity.engineVersion.length === 0) {
|
|
482
|
+
throw incompatible(transport, "Engine identity is missing engineVersion");
|
|
483
|
+
}
|
|
484
|
+
for (const clock of COMPATIBILITY_CLOCKS) {
|
|
485
|
+
if (!Number.isSafeInteger(identity[clock])) {
|
|
486
|
+
throw incompatible(transport, `Engine identity is missing ${clock}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (identity.machineProtocolVersion !== MACHINE_PROTOCOL_VERSION) {
|
|
490
|
+
throw incompatible(
|
|
491
|
+
transport,
|
|
492
|
+
`Engine machine protocol ${String(identity.machineProtocolVersion)} is incompatible with SDK protocol ${MACHINE_PROTOCOL_VERSION}`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (!Array.isArray(identity.supportedCapabilities) || !identity.supportedCapabilities.every((item) => typeof item === "string")) {
|
|
496
|
+
throw incompatible(transport, "Engine identity is missing supportedCapabilities");
|
|
497
|
+
}
|
|
498
|
+
for (const capability of REQUIRED_CAPABILITIES) {
|
|
499
|
+
if (!identity.supportedCapabilities.includes(capability)) {
|
|
500
|
+
throw incompatible(transport, `Engine identity does not support ${capability}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const checked = identity;
|
|
504
|
+
if (peer) {
|
|
505
|
+
for (const clock of COMPATIBILITY_CLOCKS) {
|
|
506
|
+
if (checked[clock] !== peer[clock]) {
|
|
507
|
+
throw incompatible(
|
|
508
|
+
transport,
|
|
509
|
+
`Remote ${clock} ${checked[clock]} is incompatible with local ${clock} ${peer[clock]}`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return checked;
|
|
515
|
+
}
|
|
516
|
+
function incompatible(transport, message) {
|
|
517
|
+
return new NikaCompatibilityError("engineIdentity", transport, message);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/lib/binary/verify.ts
|
|
521
|
+
var IDENTITY_BUFFER_BYTES = 16 * 1024;
|
|
522
|
+
async function verifyNikaEngine(engine) {
|
|
523
|
+
const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
|
|
524
|
+
const identity = await probeIdentity(engine.bin);
|
|
525
|
+
if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
|
|
526
|
+
throw incompatible2(
|
|
527
|
+
`Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return identity;
|
|
531
|
+
}
|
|
532
|
+
async function verifyManagedPayload(engine) {
|
|
533
|
+
const packageRoot = engine.packageRoot;
|
|
534
|
+
if (!packageRoot || !engine.packageName) {
|
|
535
|
+
throw incompatible2("Managed engine resolution is missing package identity");
|
|
536
|
+
}
|
|
537
|
+
const manifest = await readJson(import_node_path2.default.join(packageRoot, "package.json"), "payload manifest");
|
|
538
|
+
if (manifest.name !== engine.packageName || typeof manifest.version !== "string") {
|
|
539
|
+
throw incompatible2(`Invalid manifest for ${engine.packageName}`);
|
|
540
|
+
}
|
|
541
|
+
const integrity = await readJson(
|
|
542
|
+
import_node_path2.default.join(packageRoot, "INTEGRITY.json"),
|
|
543
|
+
"payload integrity metadata"
|
|
544
|
+
);
|
|
545
|
+
if (integrity.algorithm !== "sha256" || integrity.executable?.file !== "bin/nika" || !isSha256(integrity.executable?.sha256)) {
|
|
546
|
+
throw incompatible2(`Invalid INTEGRITY.json for ${engine.packageName}`);
|
|
547
|
+
}
|
|
548
|
+
let metadata;
|
|
549
|
+
try {
|
|
550
|
+
metadata = await (0, import_promises.stat)(engine.bin);
|
|
551
|
+
} catch (cause) {
|
|
552
|
+
throw incompatible2(`Cannot read packaged engine ${engine.bin}`, cause);
|
|
553
|
+
}
|
|
554
|
+
if (!metadata.isFile()) {
|
|
555
|
+
throw incompatible2(`Packaged engine is not a file: ${engine.bin}`);
|
|
556
|
+
}
|
|
557
|
+
const actual = await sha256(engine.bin);
|
|
558
|
+
if (actual !== integrity.executable.sha256) {
|
|
559
|
+
throw incompatible2(`Packaged engine checksum mismatch for ${engine.packageName}`);
|
|
560
|
+
}
|
|
561
|
+
return manifest.version;
|
|
562
|
+
}
|
|
563
|
+
async function probeIdentity(bin) {
|
|
564
|
+
const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
|
|
565
|
+
const captured = await captureEngine(bin, ["--sdk-identity"], {
|
|
566
|
+
bufferBytes: IDENTITY_BUFFER_BYTES,
|
|
567
|
+
transport: "native-process",
|
|
568
|
+
label: "Engine identity probe"
|
|
569
|
+
}).catch((cause) => {
|
|
570
|
+
throw incompatible2(
|
|
571
|
+
`Engine identity probe of ${bin} failed`,
|
|
572
|
+
cause
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
if (captured.exitCode !== 0) {
|
|
576
|
+
throw incompatible2(
|
|
577
|
+
`Engine identity probe of ${bin} exited with code ${captured.exitCode}; ${notAnEngine}`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
let value;
|
|
581
|
+
try {
|
|
582
|
+
value = JSON.parse(captured.stdout.trim());
|
|
583
|
+
} catch (cause) {
|
|
584
|
+
throw incompatible2(
|
|
585
|
+
`Engine identity probe of ${bin} did not emit one JSON object; ${notAnEngine}`,
|
|
586
|
+
cause
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (captured.stderr.trim()) {
|
|
590
|
+
throw incompatible2(`Engine identity probe of ${bin} wrote unexpected diagnostics`);
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
return compatibleEngineIdentity(value, "native-process");
|
|
594
|
+
} catch (cause) {
|
|
595
|
+
if (cause instanceof NikaCompatibilityError) throw cause;
|
|
596
|
+
throw incompatible2("Engine identity probe was incompatible", cause);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async function readJson(file, label) {
|
|
600
|
+
try {
|
|
601
|
+
const parsed = JSON.parse(await (0, import_promises.readFile)(file, "utf8"));
|
|
602
|
+
if (!isRecord(parsed)) throw new TypeError(`${label} is not an object`);
|
|
603
|
+
return parsed;
|
|
604
|
+
} catch (cause) {
|
|
605
|
+
throw incompatible2(`Cannot verify ${label} at ${file}`, cause);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function sha256(file) {
|
|
609
|
+
return new Promise((resolve, reject) => {
|
|
610
|
+
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
611
|
+
const stream = (0, import_node_fs.createReadStream)(file);
|
|
612
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
613
|
+
stream.once("error", reject);
|
|
614
|
+
stream.once("end", () => resolve(hash.digest("hex")));
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
function isRecord(value) {
|
|
618
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
619
|
+
}
|
|
620
|
+
function isSha256(value) {
|
|
621
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
622
|
+
}
|
|
623
|
+
function incompatible2(message, cause) {
|
|
624
|
+
return new NikaCompatibilityError(
|
|
625
|
+
"engineIdentity",
|
|
626
|
+
"native-process",
|
|
627
|
+
cause instanceof Error ? `${message}: ${cause.message}` : message
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/lib/sse/parser.ts
|
|
632
|
+
var SseParseError = class extends Error {
|
|
633
|
+
constructor(message, options) {
|
|
634
|
+
super(message, options);
|
|
635
|
+
this.name = "SseParseError";
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
var SseParser = class {
|
|
639
|
+
constructor(limits) {
|
|
640
|
+
this.limits = limits;
|
|
641
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
642
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
643
|
+
throw new RangeError(`${name} must be a positive integer`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
pending = new Uint8Array(0);
|
|
648
|
+
pendingBytes = 0;
|
|
649
|
+
frameBytes = 0;
|
|
650
|
+
dataLines = [];
|
|
651
|
+
id;
|
|
652
|
+
retry;
|
|
653
|
+
firstLine = true;
|
|
654
|
+
push(chunk) {
|
|
655
|
+
const frames = [];
|
|
656
|
+
let offset = 0;
|
|
657
|
+
while (offset < chunk.length) {
|
|
658
|
+
const newline = chunk.indexOf(10, offset);
|
|
659
|
+
if (newline < 0) {
|
|
660
|
+
this.append(chunk.subarray(offset));
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
this.append(chunk.subarray(offset, newline));
|
|
664
|
+
const frame = this.processPendingLine();
|
|
665
|
+
if (frame) frames.push(frame);
|
|
666
|
+
offset = newline + 1;
|
|
667
|
+
}
|
|
668
|
+
return frames;
|
|
669
|
+
}
|
|
670
|
+
finish() {
|
|
671
|
+
if (this.pendingBytes > 0) this.processPendingLine(false);
|
|
672
|
+
if (this.dataLines.length > 0) {
|
|
673
|
+
throw new SseParseError("SSE stream ended with an incomplete data frame");
|
|
674
|
+
}
|
|
675
|
+
this.resetFrame();
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
append(bytes) {
|
|
679
|
+
const length = this.pendingBytes + bytes.length;
|
|
680
|
+
if (length > this.limits.maxBufferBytes) {
|
|
681
|
+
throw new SseParseError(`SSE buffer exceeded ${this.limits.maxBufferBytes} bytes`);
|
|
682
|
+
}
|
|
683
|
+
if (length > this.limits.maxLineBytes) {
|
|
684
|
+
throw new SseParseError(`SSE line exceeded ${this.limits.maxLineBytes} bytes`);
|
|
685
|
+
}
|
|
686
|
+
if (bytes.length === 0) return;
|
|
687
|
+
this.ensurePendingCapacity(length);
|
|
688
|
+
this.pending.set(bytes, this.pendingBytes);
|
|
689
|
+
this.pendingBytes = length;
|
|
690
|
+
}
|
|
691
|
+
ensurePendingCapacity(length) {
|
|
692
|
+
if (this.pending.byteLength >= length) return;
|
|
693
|
+
const ceiling = Math.min(this.limits.maxBufferBytes, this.limits.maxLineBytes);
|
|
694
|
+
const capacity = Math.min(
|
|
695
|
+
ceiling,
|
|
696
|
+
Math.max(length, Math.max(1, this.pending.byteLength) * 2)
|
|
697
|
+
);
|
|
698
|
+
const pending = new Uint8Array(capacity);
|
|
699
|
+
pending.set(this.pending.subarray(0, this.pendingBytes));
|
|
700
|
+
this.pending = pending;
|
|
701
|
+
}
|
|
702
|
+
processPendingLine(terminated = true) {
|
|
703
|
+
const rawLength = this.pendingBytes + (terminated ? 1 : 0);
|
|
704
|
+
if (this.frameBytes + rawLength > this.limits.maxFrameBytes) {
|
|
705
|
+
throw new SseParseError(`SSE frame exceeded ${this.limits.maxFrameBytes} bytes`);
|
|
706
|
+
}
|
|
707
|
+
this.frameBytes += rawLength;
|
|
708
|
+
let bytes = this.pending.subarray(0, this.pendingBytes);
|
|
709
|
+
this.pendingBytes = 0;
|
|
710
|
+
if (bytes.at(-1) === 13) bytes = bytes.subarray(0, -1);
|
|
711
|
+
let line;
|
|
712
|
+
try {
|
|
713
|
+
line = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
714
|
+
} catch (cause) {
|
|
715
|
+
throw new SseParseError("SSE line was not valid UTF-8", {
|
|
716
|
+
cause: cause instanceof Error ? cause : void 0
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
if (this.firstLine) {
|
|
720
|
+
this.firstLine = false;
|
|
721
|
+
if (line.startsWith("\uFEFF")) line = line.slice(1);
|
|
722
|
+
}
|
|
723
|
+
if (line.length === 0) return this.dispatch();
|
|
724
|
+
if (line.startsWith(":")) return void 0;
|
|
725
|
+
const colon = line.indexOf(":");
|
|
726
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
727
|
+
let value = colon < 0 ? "" : line.slice(colon + 1);
|
|
728
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
729
|
+
switch (field) {
|
|
730
|
+
case "data":
|
|
731
|
+
this.dataLines.push(value);
|
|
732
|
+
break;
|
|
733
|
+
case "id":
|
|
734
|
+
if (!value.includes("\0")) this.id = value;
|
|
735
|
+
break;
|
|
736
|
+
case "retry":
|
|
737
|
+
if (/^\d+$/.test(value)) {
|
|
738
|
+
const parsed = Number(value);
|
|
739
|
+
if (Number.isSafeInteger(parsed)) this.retry = parsed;
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
default:
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
return void 0;
|
|
746
|
+
}
|
|
747
|
+
dispatch() {
|
|
748
|
+
const data = this.dataLines.join("\n");
|
|
749
|
+
const frame = {
|
|
750
|
+
...data.length > 0 ? { data } : {},
|
|
751
|
+
...this.id !== void 0 ? { id: this.id } : {},
|
|
752
|
+
...this.retry !== void 0 ? { retry: this.retry } : {}
|
|
753
|
+
};
|
|
754
|
+
this.resetFrame();
|
|
755
|
+
return Object.keys(frame).length > 0 ? frame : void 0;
|
|
756
|
+
}
|
|
757
|
+
resetFrame() {
|
|
758
|
+
this.frameBytes = 0;
|
|
759
|
+
this.dataLines = [];
|
|
760
|
+
this.id = void 0;
|
|
761
|
+
this.retry = void 0;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
async function* decodeSse(stream, limits, signal) {
|
|
765
|
+
const reader = stream.getReader();
|
|
766
|
+
const parser = new SseParser(limits);
|
|
767
|
+
const abort = () => {
|
|
768
|
+
void reader.cancel().catch(() => {
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
if (signal?.aborted) abort();
|
|
772
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
773
|
+
try {
|
|
774
|
+
while (true) {
|
|
775
|
+
const { done, value } = await reader.read();
|
|
776
|
+
if (done) break;
|
|
777
|
+
for (const frame of parser.push(value)) yield frame;
|
|
778
|
+
}
|
|
779
|
+
for (const frame of parser.finish()) yield frame;
|
|
780
|
+
} finally {
|
|
781
|
+
signal?.removeEventListener("abort", abort);
|
|
782
|
+
await reader.cancel().catch(() => {
|
|
783
|
+
});
|
|
784
|
+
reader.releaseLock();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/lib/http-transport.ts
|
|
789
|
+
var JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
790
|
+
"queued",
|
|
791
|
+
"running",
|
|
792
|
+
"interrupted",
|
|
793
|
+
"paused",
|
|
794
|
+
"succeeded",
|
|
795
|
+
"failed",
|
|
796
|
+
"cancelled"
|
|
797
|
+
]);
|
|
798
|
+
var MAX_OBSERVATION_RETRIES = 5;
|
|
799
|
+
var POSITIVE_TRACE_VERDICTS = /* @__PURE__ */ new Set(["VERIFIED", "OK", "SEALED", "ANCHORED", "REPLAYED"]);
|
|
800
|
+
var RETRY_BASE_MILLISECONDS = 100;
|
|
801
|
+
var RETRY_MIN_MILLISECONDS = 25;
|
|
802
|
+
var RETRY_MAX_MILLISECONDS = 5e3;
|
|
803
|
+
var WORKFLOW_REFUSAL_STATUSES = /* @__PURE__ */ new Set([404, 422]);
|
|
804
|
+
var HttpTransport = class {
|
|
805
|
+
constructor(options) {
|
|
806
|
+
this.options = options;
|
|
807
|
+
}
|
|
808
|
+
kind = "http";
|
|
809
|
+
serverIdentity;
|
|
810
|
+
localIdentity;
|
|
811
|
+
resolvedEngine;
|
|
812
|
+
remoteIdentity;
|
|
813
|
+
async check(workflow, options) {
|
|
814
|
+
if (options.model !== void 0 || options.nativeStrict === true) {
|
|
815
|
+
throw this.gap(
|
|
816
|
+
"checkOptions",
|
|
817
|
+
"nika serve admission has no request envelope for model or nativeStrict overrides"
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (isContainedWorkflowName(workflow)) return this.checkByName(workflow, options.signal);
|
|
821
|
+
const captured = await this.captureSnapshot(workflow, options.signal, true);
|
|
822
|
+
if (captured.bytes === void 0) return captured.report;
|
|
823
|
+
const snapshot = captured.identity;
|
|
824
|
+
if (!snapshot) {
|
|
825
|
+
throw this.gap("executionSnapshot", "Local engine omitted execution snapshot identity");
|
|
826
|
+
}
|
|
827
|
+
const acknowledged = snapshotAck(await this.json("/v1/check", {
|
|
828
|
+
method: "POST",
|
|
829
|
+
headers: { "Content-Type": "application/json" },
|
|
830
|
+
body: captured.bytes,
|
|
831
|
+
signal: options.signal
|
|
832
|
+
}, true, [200], "check"));
|
|
833
|
+
if (!acknowledged || acknowledged.snapshot_digest !== snapshot.digest || acknowledged.root !== snapshot.root || acknowledged.units !== snapshot.units) {
|
|
834
|
+
throw new NikaProtocolError(this.kind, "Check admission did not acknowledge the snapshot");
|
|
835
|
+
}
|
|
836
|
+
return captured.report;
|
|
837
|
+
}
|
|
838
|
+
async startRun(workflow, options) {
|
|
839
|
+
if (options.vars && Object.keys(options.vars).length > 0 || options.model !== void 0 || options.maxCostUsd !== void 0) {
|
|
840
|
+
throw this.gap(
|
|
841
|
+
"runOptions",
|
|
842
|
+
"nika serve admission has no request envelope for vars, model, or maxCostUsd"
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const idempotencyKey = options.idempotencyKey ?? (0, import_node_crypto2.randomUUID)();
|
|
846
|
+
if (Buffer.byteLength(idempotencyKey) < 1 || Buffer.byteLength(idempotencyKey) > 255) {
|
|
847
|
+
throw new NikaTransportError(this.kind, "Idempotency-Key must be 1-255 bytes");
|
|
848
|
+
}
|
|
849
|
+
if (isContainedWorkflowName(workflow)) {
|
|
850
|
+
await this.ensureServerIdentity();
|
|
851
|
+
const job2 = await this.admitJob(JSON.stringify({ workflow }), idempotencyKey);
|
|
852
|
+
return this.httpRun(job2.id, 0, job2);
|
|
853
|
+
}
|
|
854
|
+
const captured = await this.captureSnapshot(workflow);
|
|
855
|
+
if (captured.bytes === void 0) {
|
|
856
|
+
throw this.gap("executionSnapshot", "Local workflow check was not clean; run was not admitted");
|
|
857
|
+
}
|
|
858
|
+
if (!captured.identity) {
|
|
859
|
+
throw this.gap("executionSnapshot", "Local engine omitted execution snapshot identity");
|
|
860
|
+
}
|
|
861
|
+
const job = await this.admitJob(captured.bytes, idempotencyKey);
|
|
862
|
+
return this.httpRun(job.id, 0, job, captured.identity.digest);
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* The by-name form of `POST /v1/check` (ADR-131): the resident captures
|
|
866
|
+
* the world of a workflow its registry lists and answers the compact
|
|
867
|
+
* acknowledgement; nothing is hashed or spawned here. A refusal that
|
|
868
|
+
* judges the workflow is a red result, as it is on the native transport:
|
|
869
|
+
* 404 (the name is not served) and 422 (the capture or admission refused
|
|
870
|
+
* it). Authorization, deadline, envelope and availability refusals still
|
|
871
|
+
* throw, because they judge the request, not the workflow.
|
|
872
|
+
*/
|
|
873
|
+
async checkByName(workflow, signal) {
|
|
874
|
+
await this.ensureServerIdentity();
|
|
875
|
+
const outcome = await this.jsonOutcome("/v1/check", {
|
|
876
|
+
method: "POST",
|
|
877
|
+
headers: { "Content-Type": "application/json" },
|
|
878
|
+
body: JSON.stringify({ workflow }),
|
|
879
|
+
signal
|
|
880
|
+
}, true, [200], "check", true);
|
|
881
|
+
if ("refusal" in outcome) {
|
|
882
|
+
if (!WORKFLOW_REFUSAL_STATUSES.has(outcome.status)) throw this.refused("/v1/check", outcome);
|
|
883
|
+
return { clean: false, error: outcome.refusal };
|
|
884
|
+
}
|
|
885
|
+
const acknowledged = snapshotAck(outcome.object);
|
|
886
|
+
if (!acknowledged) {
|
|
887
|
+
throw new NikaProtocolError(this.kind, "Check admission did not acknowledge the workflow");
|
|
888
|
+
}
|
|
889
|
+
return { clean: true, ...acknowledged };
|
|
890
|
+
}
|
|
891
|
+
async admitJob(body, idempotencyKey) {
|
|
892
|
+
const admitted = await this.json("/v1/jobs", {
|
|
893
|
+
method: "POST",
|
|
894
|
+
headers: {
|
|
895
|
+
"Content-Type": "application/json",
|
|
896
|
+
"Idempotency-Key": idempotencyKey
|
|
897
|
+
},
|
|
898
|
+
body
|
|
899
|
+
}, true, [200, 202], "run");
|
|
900
|
+
const id = typeof admitted.id === "string" ? admitted.id : void 0;
|
|
901
|
+
if (!id) {
|
|
902
|
+
throw new NikaProtocolError(this.kind, "Job admission response omitted its id");
|
|
903
|
+
}
|
|
904
|
+
return durableJob(admitted, id, this.kind);
|
|
905
|
+
}
|
|
906
|
+
async attachRun(id, options) {
|
|
907
|
+
await this.ensureServerIdentity();
|
|
908
|
+
const object = await this.json(`/v1/jobs/${encodeURIComponent(id)}`, {
|
|
909
|
+
method: "GET",
|
|
910
|
+
headers: { Accept: "application/json" }
|
|
911
|
+
}, true, [200], "attachRun");
|
|
912
|
+
const durable = durableJob(object, id, this.kind);
|
|
913
|
+
return this.httpRun(id, options.lastEventId ?? 0, durable);
|
|
914
|
+
}
|
|
915
|
+
async listWorkflows() {
|
|
916
|
+
await this.ensureServerIdentity();
|
|
917
|
+
const object = await this.json("/v1/workflows", {
|
|
918
|
+
method: "GET",
|
|
919
|
+
headers: { Accept: "application/json" }
|
|
920
|
+
}, true, [200], "listWorkflows");
|
|
921
|
+
if (Object.keys(object).some((key) => key !== "workflows") || !Array.isArray(object.workflows) || object.workflows.some((name) => !isContainedWorkflowName(name)) || new Set(object.workflows).size !== object.workflows.length) {
|
|
922
|
+
throw new NikaProtocolError(this.kind, "Workflow catalog response was malformed");
|
|
923
|
+
}
|
|
924
|
+
return Object.freeze([...object.workflows]);
|
|
925
|
+
}
|
|
926
|
+
async workflow(name) {
|
|
927
|
+
await this.ensureServerIdentity();
|
|
928
|
+
const object = await this.json(`/v1/workflows/${workflowPath(name)}`, {
|
|
929
|
+
method: "GET",
|
|
930
|
+
headers: { Accept: "application/json" }
|
|
931
|
+
}, true, [200], "workflow");
|
|
932
|
+
if (Object.keys(object).some((key) => key !== "workflow") || !isContainedWorkflowName(object.workflow) || object.workflow !== name) {
|
|
933
|
+
throw new NikaProtocolError(this.kind, "Workflow metadata response was malformed");
|
|
934
|
+
}
|
|
935
|
+
return object;
|
|
936
|
+
}
|
|
937
|
+
async schedule(workflow, options) {
|
|
938
|
+
await this.ensureScheduleCapability();
|
|
939
|
+
const path3 = `/v1/schedules/${encodeURIComponent(options.id)}`;
|
|
940
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
941
|
+
if (options.revision === void 0) {
|
|
942
|
+
headers.set("If-None-Match", "*");
|
|
943
|
+
} else {
|
|
944
|
+
headers.set("If-Match", `"${options.revision}"`);
|
|
945
|
+
}
|
|
946
|
+
const object = await this.operationJson("schedule", path3, {
|
|
947
|
+
method: "PUT",
|
|
948
|
+
headers,
|
|
949
|
+
body: JSON.stringify(scheduleBody(workflow, options))
|
|
950
|
+
});
|
|
951
|
+
if (object.applied !== true || typeof object.changed !== "boolean" || !scheduleStatusProjection(object.status)) {
|
|
952
|
+
throw new NikaProtocolError(this.kind, "Schedule apply response was not an acknowledgement");
|
|
953
|
+
}
|
|
954
|
+
return object;
|
|
955
|
+
}
|
|
956
|
+
async scheduleStatus(id) {
|
|
957
|
+
await this.ensureScheduleCapability();
|
|
958
|
+
const path3 = `/v1/schedules/${encodeURIComponent(id)}`;
|
|
959
|
+
const object = await this.operationJson("scheduleStatus", path3, {
|
|
960
|
+
method: "GET",
|
|
961
|
+
headers: { Accept: "application/json" }
|
|
962
|
+
});
|
|
963
|
+
if (!scheduleStatusProjection(object)) {
|
|
964
|
+
throw new NikaProtocolError(this.kind, "Schedule status response omitted machine fields");
|
|
965
|
+
}
|
|
966
|
+
return object;
|
|
967
|
+
}
|
|
968
|
+
async traceVerify(receipt, options) {
|
|
969
|
+
await this.ensureServerIdentity();
|
|
970
|
+
const jobId2 = receipt.job_id;
|
|
971
|
+
if (typeof jobId2 !== "string" || jobId2.length === 0) {
|
|
972
|
+
throw this.gap("traceVerify", "The engine receipt does not carry a remote job_id");
|
|
973
|
+
}
|
|
974
|
+
const object = await this.json(
|
|
975
|
+
`/v1/jobs/${encodeURIComponent(jobId2)}/trace/verify`,
|
|
976
|
+
{ method: "GET", signal: options.signal },
|
|
977
|
+
true,
|
|
978
|
+
[200],
|
|
979
|
+
"traceVerify"
|
|
980
|
+
);
|
|
981
|
+
if (typeof object.verdict !== "string" || object.reason !== void 0 && typeof object.reason !== "string" || object.trace_id !== void 0 && typeof object.trace_id !== "string") {
|
|
982
|
+
throw new NikaProtocolError(this.kind, "Trace verification verdict was malformed");
|
|
983
|
+
}
|
|
984
|
+
const traceBound = typeof object.trace_id === "string" && object.trace_id === receipt.trace_id;
|
|
985
|
+
return {
|
|
986
|
+
...object,
|
|
987
|
+
verified: POSITIVE_TRACE_VERDICTS.has(object.verdict.toUpperCase()) && traceBound
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
httpRun(id, initialSequence = 0, attachedState, expectedSnapshotDigest) {
|
|
991
|
+
const controller = new AbortController();
|
|
992
|
+
let terminalObserved = false;
|
|
993
|
+
let resolveDone;
|
|
994
|
+
let rejectDone;
|
|
995
|
+
const done = new Promise((resolve, reject) => {
|
|
996
|
+
resolveDone = resolve;
|
|
997
|
+
rejectDone = reject;
|
|
998
|
+
});
|
|
999
|
+
done.catch(() => {
|
|
1000
|
+
});
|
|
1001
|
+
const settle = (source) => {
|
|
1002
|
+
const event = "sequence" in source ? source : void 0;
|
|
1003
|
+
const durable = event ? void 0 : source;
|
|
1004
|
+
const receipt = event ? eventReceipt(event) : durable?.receipt;
|
|
1005
|
+
if (durable?.execution_id !== void 0 && attachedState?.execution_id !== void 0 && durable.execution_id !== attachedState.execution_id) {
|
|
1006
|
+
throw new NikaProtocolError(this.kind, "Durable execution identity changed after attach");
|
|
1007
|
+
}
|
|
1008
|
+
if (durable?.trace_id !== void 0 && attachedState?.trace_id !== void 0 && durable.trace_id !== attachedState.trace_id) {
|
|
1009
|
+
throw new NikaProtocolError(this.kind, "Durable trace identity changed after attach");
|
|
1010
|
+
}
|
|
1011
|
+
const knownExecutionId = attachedState?.execution_id ?? durable?.execution_id;
|
|
1012
|
+
const knownTraceId = attachedState?.trace_id ?? durable?.trace_id;
|
|
1013
|
+
if (receipt) {
|
|
1014
|
+
assertReceiptIdentity(
|
|
1015
|
+
receipt,
|
|
1016
|
+
id,
|
|
1017
|
+
this.kind,
|
|
1018
|
+
knownExecutionId,
|
|
1019
|
+
knownTraceId,
|
|
1020
|
+
expectedSnapshotDigest
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const executionId = knownExecutionId ?? (typeof receipt?.execution_id === "string" ? receipt.execution_id : void 0);
|
|
1024
|
+
const traceId = knownTraceId ?? (typeof receipt?.trace_id === "string" ? receipt.trace_id : void 0);
|
|
1025
|
+
terminalObserved = true;
|
|
1026
|
+
const outputs = event ? eventOutputs(event) : durable?.outputs;
|
|
1027
|
+
const error = event ? eventError(event) : durable?.settlement?.error ?? durable?.error;
|
|
1028
|
+
const settlement = event ? eventSettlement(event, this.kind) : durable?.settlement;
|
|
1029
|
+
resolveDone({
|
|
1030
|
+
id,
|
|
1031
|
+
status: source.status,
|
|
1032
|
+
transport: this.kind,
|
|
1033
|
+
...executionId ? { execution_id: executionId } : {},
|
|
1034
|
+
...traceId ? { trace_id: traceId } : {},
|
|
1035
|
+
...outputs ? { outputs } : {},
|
|
1036
|
+
...receipt ? { receipt } : {},
|
|
1037
|
+
...error ? { error } : {},
|
|
1038
|
+
...settlement ? { settlement } : {}
|
|
1039
|
+
});
|
|
1040
|
+
};
|
|
1041
|
+
if (attachedState && isObservationEnded(attachedState)) settle(attachedState);
|
|
1042
|
+
const events = {
|
|
1043
|
+
[Symbol.asyncIterator]: async function* () {
|
|
1044
|
+
try {
|
|
1045
|
+
for await (const event of this.observeJob(
|
|
1046
|
+
id,
|
|
1047
|
+
controller.signal,
|
|
1048
|
+
settle,
|
|
1049
|
+
initialSequence
|
|
1050
|
+
)) {
|
|
1051
|
+
yield event;
|
|
1052
|
+
}
|
|
1053
|
+
} catch (cause) {
|
|
1054
|
+
const error = cause instanceof Error ? cause : new NikaTransportError(this.kind, String(cause));
|
|
1055
|
+
if (terminalObserved && controller.signal.aborted) return;
|
|
1056
|
+
if (!terminalObserved) rejectDone(error);
|
|
1057
|
+
throw error;
|
|
1058
|
+
}
|
|
1059
|
+
}.bind(this)
|
|
1060
|
+
};
|
|
1061
|
+
return {
|
|
1062
|
+
id,
|
|
1063
|
+
events,
|
|
1064
|
+
done,
|
|
1065
|
+
status: async () => {
|
|
1066
|
+
const object = await this.json(`/v1/jobs/${encodeURIComponent(id)}/status`, {
|
|
1067
|
+
method: "GET",
|
|
1068
|
+
headers: { Accept: "application/json" }
|
|
1069
|
+
}, true, [200], "status");
|
|
1070
|
+
if (typeof object.status !== "string" || !JOB_STATUSES.has(object.status)) {
|
|
1071
|
+
throw new NikaProtocolError(this.kind, "Job status response was malformed");
|
|
1072
|
+
}
|
|
1073
|
+
return object.status;
|
|
1074
|
+
},
|
|
1075
|
+
cancel: async () => {
|
|
1076
|
+
const { object, status } = await this.jsonWithStatus(
|
|
1077
|
+
`/v1/jobs/${encodeURIComponent(id)}/cancel`,
|
|
1078
|
+
{ method: "POST" },
|
|
1079
|
+
true,
|
|
1080
|
+
[200, 202],
|
|
1081
|
+
"cancel"
|
|
1082
|
+
);
|
|
1083
|
+
const durable = durableJob(object, id, this.kind);
|
|
1084
|
+
if (status === 202) {
|
|
1085
|
+
if (isExecutionTerminal(durable.status)) {
|
|
1086
|
+
throw new NikaProtocolError(this.kind, "Pending cancellation returned a terminal job");
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
runId: id,
|
|
1090
|
+
accepted: true,
|
|
1091
|
+
status: "cancellation_requested",
|
|
1092
|
+
transport: this.kind
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
if (durable.status !== "paused" && !isExecutionTerminal(durable.status)) {
|
|
1096
|
+
throw new NikaProtocolError(this.kind, "Cancellation did not return an ended observation");
|
|
1097
|
+
}
|
|
1098
|
+
settle(durable);
|
|
1099
|
+
const accepted = durable.status === "cancelled";
|
|
1100
|
+
return {
|
|
1101
|
+
runId: id,
|
|
1102
|
+
accepted,
|
|
1103
|
+
status: accepted ? "cancelled" : "already_settled",
|
|
1104
|
+
transport: this.kind
|
|
1105
|
+
};
|
|
1106
|
+
},
|
|
1107
|
+
cleanup: async () => {
|
|
1108
|
+
controller.abort();
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
async *observeJob(id, signal, settle, initialSequence = 0) {
|
|
1113
|
+
const state = {
|
|
1114
|
+
lastSequence: initialSequence,
|
|
1115
|
+
attempt: 0,
|
|
1116
|
+
terminalObserved: false
|
|
1117
|
+
};
|
|
1118
|
+
const jobPath = `/v1/jobs/${encodeURIComponent(id)}`;
|
|
1119
|
+
const eventsPath = `/v1/jobs/${encodeURIComponent(id)}/events`;
|
|
1120
|
+
const settlement = { jobPath, id, settle };
|
|
1121
|
+
while (!state.terminalObserved) {
|
|
1122
|
+
const headers = new Headers({ Accept: "text/event-stream" });
|
|
1123
|
+
if (state.lastSequence > 0) headers.set("Last-Event-ID", String(state.lastSequence));
|
|
1124
|
+
let response;
|
|
1125
|
+
try {
|
|
1126
|
+
response = await this.observationRequest(eventsPath, { headers, signal });
|
|
1127
|
+
} catch {
|
|
1128
|
+
await this.retryObservation(state, signal, {
|
|
1129
|
+
retry: true
|
|
1130
|
+
}, settlement);
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (response.status === 404) {
|
|
1134
|
+
await discardResponse(response);
|
|
1135
|
+
const durable2 = await this.inspectDurableJob(jobPath, id, signal);
|
|
1136
|
+
if (isRetryObservation(durable2)) {
|
|
1137
|
+
await this.retryObservation(state, signal, durable2, settlement);
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
if (isObservationEnded(durable2)) {
|
|
1141
|
+
state.terminalObserved = true;
|
|
1142
|
+
settle(durable2);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
await this.retryObservation(state, signal, { retry: true }, settlement);
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (isRetryableStatus(response.status)) {
|
|
1149
|
+
const retryAfterMilliseconds = retryAfter(response.headers.get("Retry-After"));
|
|
1150
|
+
await discardResponse(response);
|
|
1151
|
+
await this.retryObservation(state, signal, {
|
|
1152
|
+
retry: true,
|
|
1153
|
+
...retryAfterMilliseconds !== void 0 ? { retryAfterMilliseconds } : {}
|
|
1154
|
+
}, settlement);
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
this.assertObservationResponse(response, eventsPath, "text/event-stream");
|
|
1158
|
+
const body = response.body;
|
|
1159
|
+
if (!body) {
|
|
1160
|
+
throw new NikaProtocolError(this.kind, "SSE event response omitted its body");
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
const limits = this.sseLimits();
|
|
1164
|
+
for await (const frame of decodeSse(body, limits, signal)) {
|
|
1165
|
+
if (frame.retry !== void 0) {
|
|
1166
|
+
state.serverRetryMilliseconds = boundedDelay(frame.retry);
|
|
1167
|
+
}
|
|
1168
|
+
if (frame.data === void 0) continue;
|
|
1169
|
+
const event = this.nikaEvent(id, frame.id, frame.data);
|
|
1170
|
+
const sequence = event.sequence;
|
|
1171
|
+
if (sequence === state.lastSequence) {
|
|
1172
|
+
if (frame.data === state.lastData) continue;
|
|
1173
|
+
throw new NikaProtocolError(
|
|
1174
|
+
this.kind,
|
|
1175
|
+
`SSE sequence ${sequence} replayed with different data`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
if (sequence !== state.lastSequence + 1) {
|
|
1179
|
+
const relation = sequence > state.lastSequence ? "gap" : "out-of-order replay";
|
|
1180
|
+
throw new NikaProtocolError(
|
|
1181
|
+
this.kind,
|
|
1182
|
+
`SSE ${relation}: expected sequence ${state.lastSequence + 1}, received ${sequence}`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
state.lastSequence = sequence;
|
|
1186
|
+
state.lastData = frame.data;
|
|
1187
|
+
state.attempt = 0;
|
|
1188
|
+
if (isObservationEnded(event)) {
|
|
1189
|
+
state.terminalObserved = true;
|
|
1190
|
+
settle(event);
|
|
1191
|
+
}
|
|
1192
|
+
yield event;
|
|
1193
|
+
if (state.terminalObserved) return;
|
|
1194
|
+
}
|
|
1195
|
+
} catch (cause) {
|
|
1196
|
+
if (cause instanceof SseParseError) {
|
|
1197
|
+
throw new NikaProtocolError(this.kind, cause.message, { cause });
|
|
1198
|
+
}
|
|
1199
|
+
if (cause instanceof NikaProtocolError) throw cause;
|
|
1200
|
+
}
|
|
1201
|
+
const durable = await this.inspectDurableJob(jobPath, id, signal);
|
|
1202
|
+
if (isRetryObservation(durable)) {
|
|
1203
|
+
await this.retryObservation(state, signal, durable, settlement);
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (isObservationEnded(durable)) {
|
|
1207
|
+
state.terminalObserved = true;
|
|
1208
|
+
settle(durable);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
await this.retryObservation(state, signal, { retry: true }, settlement);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
nikaEvent(jobId2, id, data) {
|
|
1215
|
+
if (!id || !/^[1-9]\d*$/.test(id)) {
|
|
1216
|
+
throw new NikaProtocolError(
|
|
1217
|
+
this.kind,
|
|
1218
|
+
"SSE event id must be a canonical positive decimal sequence"
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const sequence = Number(id);
|
|
1222
|
+
if (!Number.isSafeInteger(sequence)) {
|
|
1223
|
+
throw new NikaProtocolError(this.kind, "SSE event id exceeded the safe integer range");
|
|
1224
|
+
}
|
|
1225
|
+
let value;
|
|
1226
|
+
try {
|
|
1227
|
+
value = JSON.parse(data);
|
|
1228
|
+
} catch (cause) {
|
|
1229
|
+
throw new NikaProtocolError(this.kind, "SSE data was not valid JSON", {
|
|
1230
|
+
cause: cause instanceof Error ? cause : void 0
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
const event = machineObject(value);
|
|
1234
|
+
if (!event) throw new NikaProtocolError(this.kind, "SSE data was not an object");
|
|
1235
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1236
|
+
"sequence",
|
|
1237
|
+
"kind",
|
|
1238
|
+
"status",
|
|
1239
|
+
"code",
|
|
1240
|
+
"message",
|
|
1241
|
+
"outputs",
|
|
1242
|
+
"receipt",
|
|
1243
|
+
"settlement"
|
|
1244
|
+
]);
|
|
1245
|
+
if (Object.keys(event).some((key) => !allowed.has(key))) {
|
|
1246
|
+
throw new NikaProtocolError(this.kind, "SSE data contained fields outside the public projection");
|
|
1247
|
+
}
|
|
1248
|
+
if (event.sequence !== sequence) {
|
|
1249
|
+
throw new NikaProtocolError(
|
|
1250
|
+
this.kind,
|
|
1251
|
+
`SSE id ${id} did not equal JSON data.sequence`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
if (!Object.hasOwn(event, "kind") || event.kind !== null && typeof event.kind !== "string") {
|
|
1255
|
+
throw new NikaProtocolError(this.kind, "SSE data.kind was not a string or null");
|
|
1256
|
+
}
|
|
1257
|
+
if (!Object.hasOwn(event, "status") || event.status !== null && (typeof event.status !== "string" || !JOB_STATUSES.has(event.status))) {
|
|
1258
|
+
throw new NikaProtocolError(this.kind, "SSE data.status was not a known job status or null");
|
|
1259
|
+
}
|
|
1260
|
+
for (const field of ["code", "message"]) {
|
|
1261
|
+
if (event[field] !== void 0 && typeof event[field] !== "string") {
|
|
1262
|
+
throw new NikaProtocolError(this.kind, `SSE data.${field} was not a string`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (event.outputs !== void 0 && !machineObject(event.outputs)) {
|
|
1266
|
+
throw new NikaProtocolError(this.kind, "SSE data.outputs was not an object");
|
|
1267
|
+
}
|
|
1268
|
+
if (event.settlement !== void 0) {
|
|
1269
|
+
readSettlement(
|
|
1270
|
+
event.settlement,
|
|
1271
|
+
this.kind,
|
|
1272
|
+
typeof event.status === "string" ? event.status : void 0
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
if (event.receipt !== void 0) {
|
|
1276
|
+
const receipt = machineObject(event.receipt);
|
|
1277
|
+
if (!receipt) {
|
|
1278
|
+
throw new NikaProtocolError(this.kind, "SSE data.receipt was not an object");
|
|
1279
|
+
}
|
|
1280
|
+
assertReceiptIdentity(receipt, jobId2, this.kind);
|
|
1281
|
+
}
|
|
1282
|
+
return event;
|
|
1283
|
+
}
|
|
1284
|
+
async inspectDurableJob(path3, expectedId, signal) {
|
|
1285
|
+
let response;
|
|
1286
|
+
try {
|
|
1287
|
+
response = await this.observationRequest(path3, { headers: { Accept: "application/json" }, signal });
|
|
1288
|
+
} catch {
|
|
1289
|
+
return { retry: true };
|
|
1290
|
+
}
|
|
1291
|
+
if (response.status === 404) {
|
|
1292
|
+
await discardResponse(response);
|
|
1293
|
+
throw new NikaTransportError(this.kind, `HTTP 404 for ${path3}; job was durably absent`);
|
|
1294
|
+
}
|
|
1295
|
+
if (isRetryableStatus(response.status)) {
|
|
1296
|
+
const retryAfterMilliseconds = retryAfter(response.headers.get("Retry-After"));
|
|
1297
|
+
await discardResponse(response);
|
|
1298
|
+
return {
|
|
1299
|
+
retry: true,
|
|
1300
|
+
...retryAfterMilliseconds !== void 0 ? { retryAfterMilliseconds } : {}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
this.assertObservationResponse(response, path3, "application/json");
|
|
1304
|
+
try {
|
|
1305
|
+
const object = await this.readObservationObject(response, path3);
|
|
1306
|
+
return durableJob(object, expectedId, this.kind);
|
|
1307
|
+
} catch (cause) {
|
|
1308
|
+
if (cause instanceof NikaTransportError && !(cause instanceof NikaProtocolError)) {
|
|
1309
|
+
return { retry: true };
|
|
1310
|
+
}
|
|
1311
|
+
throw cause;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
assertObservationResponse(response, path3, contentType) {
|
|
1315
|
+
if (response.status === 204) {
|
|
1316
|
+
void discardResponse(response);
|
|
1317
|
+
throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${path3}`);
|
|
1318
|
+
}
|
|
1319
|
+
if (response.status === 401 || response.status === 403) {
|
|
1320
|
+
void discardResponse(response);
|
|
1321
|
+
throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${path3}`);
|
|
1322
|
+
}
|
|
1323
|
+
if (response.status !== 200) {
|
|
1324
|
+
void discardResponse(response);
|
|
1325
|
+
throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${path3}`);
|
|
1326
|
+
}
|
|
1327
|
+
const actual = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1328
|
+
if (actual !== contentType) {
|
|
1329
|
+
void discardResponse(response);
|
|
1330
|
+
throw new NikaProtocolError(
|
|
1331
|
+
this.kind,
|
|
1332
|
+
`HTTP ${path3} returned an invalid content-type`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async readObservationObject(response, path3, signal) {
|
|
1337
|
+
if (!response.body) {
|
|
1338
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path3} omitted its JSON body`);
|
|
1339
|
+
}
|
|
1340
|
+
const reader = response.body.getReader();
|
|
1341
|
+
const chunks = [];
|
|
1342
|
+
let bytes = 0;
|
|
1343
|
+
let rejectBoundary;
|
|
1344
|
+
const boundary = new Promise((_resolve, reject) => {
|
|
1345
|
+
rejectBoundary = reject;
|
|
1346
|
+
});
|
|
1347
|
+
const abort = () => {
|
|
1348
|
+
rejectBoundary(new NikaTransportError(this.kind, `HTTP ${path3} response body was aborted`));
|
|
1349
|
+
void reader.cancel().catch(() => {
|
|
1350
|
+
});
|
|
1351
|
+
};
|
|
1352
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1353
|
+
const timer = setTimeout(() => {
|
|
1354
|
+
rejectBoundary(new NikaTransportError(
|
|
1355
|
+
this.kind,
|
|
1356
|
+
`HTTP response body timed out after ${this.options.requestTimeout}ms`
|
|
1357
|
+
));
|
|
1358
|
+
void reader.cancel().catch(() => {
|
|
1359
|
+
});
|
|
1360
|
+
}, this.options.requestTimeout);
|
|
1361
|
+
try {
|
|
1362
|
+
if (signal?.aborted) abort();
|
|
1363
|
+
while (true) {
|
|
1364
|
+
const { done, value: value2 } = await Promise.race([reader.read(), boundary]);
|
|
1365
|
+
if (done) break;
|
|
1366
|
+
bytes += value2.byteLength;
|
|
1367
|
+
if (bytes > this.options.machineBufferBytes) {
|
|
1368
|
+
throw new NikaProtocolError(
|
|
1369
|
+
this.kind,
|
|
1370
|
+
`HTTP ${path3} JSON exceeded ${this.options.machineBufferBytes} bytes`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
chunks.push(value2);
|
|
1374
|
+
}
|
|
1375
|
+
} catch (cause) {
|
|
1376
|
+
if (cause instanceof NikaProtocolError) throw cause;
|
|
1377
|
+
throw new NikaTransportError(this.kind, `HTTP ${path3} response body reset`);
|
|
1378
|
+
} finally {
|
|
1379
|
+
clearTimeout(timer);
|
|
1380
|
+
signal?.removeEventListener("abort", abort);
|
|
1381
|
+
await reader.cancel().catch(() => {
|
|
1382
|
+
});
|
|
1383
|
+
reader.releaseLock();
|
|
1384
|
+
}
|
|
1385
|
+
const body = new Uint8Array(bytes);
|
|
1386
|
+
let offset = 0;
|
|
1387
|
+
for (const chunk of chunks) {
|
|
1388
|
+
body.set(chunk, offset);
|
|
1389
|
+
offset += chunk.byteLength;
|
|
1390
|
+
}
|
|
1391
|
+
let text;
|
|
1392
|
+
try {
|
|
1393
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
1394
|
+
} catch (cause) {
|
|
1395
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path3} JSON was not valid UTF-8`, {
|
|
1396
|
+
cause: cause instanceof Error ? cause : void 0
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
let value;
|
|
1400
|
+
try {
|
|
1401
|
+
value = JSON.parse(text);
|
|
1402
|
+
} catch (cause) {
|
|
1403
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path3} did not return valid JSON`, {
|
|
1404
|
+
cause: cause instanceof Error ? cause : void 0
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
const object = machineObject(value);
|
|
1408
|
+
if (!object) throw new NikaProtocolError(this.kind, `HTTP ${path3} did not return an object`);
|
|
1409
|
+
return object;
|
|
1410
|
+
}
|
|
1411
|
+
async observationRequest(path3, init, withTimeout = true) {
|
|
1412
|
+
const controller = withTimeout ? new AbortController() : void 0;
|
|
1413
|
+
const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
|
|
1414
|
+
const callerSignal = init.signal;
|
|
1415
|
+
const abort = () => controller?.abort(callerSignal?.reason);
|
|
1416
|
+
callerSignal?.addEventListener("abort", abort, { once: true });
|
|
1417
|
+
const headers = new Headers(init.headers);
|
|
1418
|
+
headers.set("Authorization", `Bearer ${this.options.token}`);
|
|
1419
|
+
try {
|
|
1420
|
+
return await this.options.fetch(`${this.options.url}${path3}`, {
|
|
1421
|
+
...init,
|
|
1422
|
+
headers,
|
|
1423
|
+
signal: controller?.signal ?? callerSignal
|
|
1424
|
+
});
|
|
1425
|
+
} catch {
|
|
1426
|
+
if (controller?.signal.aborted && !callerSignal?.aborted) {
|
|
1427
|
+
throw new NikaTransportError(
|
|
1428
|
+
this.kind,
|
|
1429
|
+
`HTTP observation timed out after ${this.options.requestTimeout}ms`
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
throw new NikaTransportError(this.kind, "HTTP observation transport failed");
|
|
1433
|
+
} finally {
|
|
1434
|
+
if (timer) clearTimeout(timer);
|
|
1435
|
+
callerSignal?.removeEventListener("abort", abort);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
async retryObservation(state, signal, retry, settlement) {
|
|
1439
|
+
if (state.attempt >= MAX_OBSERVATION_RETRIES) {
|
|
1440
|
+
const durable = await this.inspectDurableJob(
|
|
1441
|
+
settlement.jobPath,
|
|
1442
|
+
settlement.id,
|
|
1443
|
+
signal
|
|
1444
|
+
);
|
|
1445
|
+
if (!isRetryObservation(durable) && isObservationEnded(durable)) {
|
|
1446
|
+
state.terminalObserved = true;
|
|
1447
|
+
settlement.settle(durable);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
throw new NikaObservationInterrupted(
|
|
1451
|
+
this.kind,
|
|
1452
|
+
settlement.id,
|
|
1453
|
+
state.lastSequence,
|
|
1454
|
+
state.attempt
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
state.attempt += 1;
|
|
1458
|
+
const exponential = Math.min(
|
|
1459
|
+
RETRY_BASE_MILLISECONDS * 2 ** (state.attempt - 1),
|
|
1460
|
+
RETRY_MAX_MILLISECONDS
|
|
1461
|
+
);
|
|
1462
|
+
const milliseconds = retry.retryAfterMilliseconds ?? state.serverRetryMilliseconds ?? exponential;
|
|
1463
|
+
await (this.options.retryDelay ?? abortableDelay)(boundedDelay(milliseconds), signal);
|
|
1464
|
+
}
|
|
1465
|
+
sseLimits() {
|
|
1466
|
+
return {
|
|
1467
|
+
maxLineBytes: this.options.machineBufferBytes,
|
|
1468
|
+
maxFrameBytes: this.options.machineBufferBytes,
|
|
1469
|
+
maxBufferBytes: this.options.machineBufferBytes
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
gap(capability, message) {
|
|
1473
|
+
return new NikaCompatibilityError(capability, this.kind, message);
|
|
1474
|
+
}
|
|
1475
|
+
ensureServerIdentity() {
|
|
1476
|
+
this.serverIdentity ??= this.probeServerIdentity();
|
|
1477
|
+
return this.serverIdentity;
|
|
1478
|
+
}
|
|
1479
|
+
async probeServerIdentity() {
|
|
1480
|
+
const health = await this.json("/health", { method: "GET" }, false);
|
|
1481
|
+
if (health.status !== "ok" || health.service !== "nika-serve") {
|
|
1482
|
+
throw new NikaCompatibilityError(
|
|
1483
|
+
"engineIdentity",
|
|
1484
|
+
this.kind,
|
|
1485
|
+
"GET /health did not identify a live nika-serve engine"
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
this.remoteIdentity = compatibleEngineIdentity(health, this.kind);
|
|
1489
|
+
return this.remoteIdentity;
|
|
1490
|
+
}
|
|
1491
|
+
ensureLocalEngine() {
|
|
1492
|
+
this.localIdentity ??= this.verifyLocalEngine();
|
|
1493
|
+
return this.localIdentity;
|
|
1494
|
+
}
|
|
1495
|
+
localEngine() {
|
|
1496
|
+
this.resolvedEngine ??= this.options.resolveEngine();
|
|
1497
|
+
return this.resolvedEngine;
|
|
1498
|
+
}
|
|
1499
|
+
async verifyLocalEngine() {
|
|
1500
|
+
const local = await verifyNikaEngine(this.localEngine());
|
|
1501
|
+
const remote = await this.ensureServerIdentity();
|
|
1502
|
+
compatibleEngineIdentity(remote, this.kind, local);
|
|
1503
|
+
return local;
|
|
1504
|
+
}
|
|
1505
|
+
async ensureScheduleCapability() {
|
|
1506
|
+
await this.ensureServerIdentity();
|
|
1507
|
+
if (!this.remoteIdentity?.supportedCapabilities.includes("schedule")) {
|
|
1508
|
+
throw this.gap(
|
|
1509
|
+
"schedule",
|
|
1510
|
+
"The connected nika serve process did not advertise resident schedule authority"
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async captureSnapshot(workflow, signal, teach = false) {
|
|
1515
|
+
const identity = await this.ensureLocalEngine();
|
|
1516
|
+
const captured = await captureEngine(
|
|
1517
|
+
this.localEngine().bin,
|
|
1518
|
+
["check", workflow, "--json", "--sdk-snapshot"],
|
|
1519
|
+
{
|
|
1520
|
+
cwd: this.options.cwd,
|
|
1521
|
+
signal,
|
|
1522
|
+
bufferBytes: this.options.machineBufferBytes,
|
|
1523
|
+
transport: this.kind,
|
|
1524
|
+
label: "SDK snapshot capture"
|
|
1525
|
+
}
|
|
1526
|
+
);
|
|
1527
|
+
let outer;
|
|
1528
|
+
try {
|
|
1529
|
+
outer = JSON.parse(captured.stdout.trim());
|
|
1530
|
+
} catch (cause) {
|
|
1531
|
+
throw new NikaCompatibilityError(
|
|
1532
|
+
"executionSnapshot",
|
|
1533
|
+
this.kind,
|
|
1534
|
+
`Local engine did not emit the required snapshot report (exit ${captured.exitCode})`
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const report = { ...outer, exitCode: captured.exitCode };
|
|
1538
|
+
delete report.execution_snapshot;
|
|
1539
|
+
if (captured.exitCode !== 0 || outer.clean !== true) {
|
|
1540
|
+
report.clean = false;
|
|
1541
|
+
if (teach && !Array.isArray(outer.findings)) {
|
|
1542
|
+
return { report: await this.teachingReport(workflow, report, signal) };
|
|
1543
|
+
}
|
|
1544
|
+
return { report };
|
|
1545
|
+
}
|
|
1546
|
+
compatibleEngineIdentity(outer, this.kind, identity);
|
|
1547
|
+
if (outer.engineVersion !== identity.engineVersion || outer.report_version !== identity.checkReportVersion) {
|
|
1548
|
+
throw this.gap(
|
|
1549
|
+
"executionSnapshot",
|
|
1550
|
+
"Local snapshot report identity changed after the engine probe"
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
if (captured.stderr.trim()) {
|
|
1554
|
+
throw this.gap("executionSnapshot", "Local snapshot capture wrote unexpected diagnostics");
|
|
1555
|
+
}
|
|
1556
|
+
const bytes = outer.execution_snapshot;
|
|
1557
|
+
if (typeof bytes !== "string" || bytes.length === 0) {
|
|
1558
|
+
throw this.gap("executionSnapshot", "Local engine omitted execution_snapshot bytes");
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
report,
|
|
1562
|
+
bytes,
|
|
1563
|
+
identity: snapshotIdentity(bytes, this.kind)
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* A red workflow has no exportable snapshot, so the capture returns one
|
|
1568
|
+
* error line and no findings. Ask the same local engine for its plain
|
|
1569
|
+
* teaching report, so `findings[]` reads the same on both transports. The
|
|
1570
|
+
* refused capture line is preserved as `snapshot_error` rather than
|
|
1571
|
+
* overwriting whatever the engine put in `error`. No bytes leave the
|
|
1572
|
+
* machine on this path: the caller still sees `clean: false`.
|
|
1573
|
+
*/
|
|
1574
|
+
async teachingReport(workflow, refused, signal) {
|
|
1575
|
+
const captured = await captureEngine(
|
|
1576
|
+
this.localEngine().bin,
|
|
1577
|
+
["check", workflow, "--json"],
|
|
1578
|
+
{
|
|
1579
|
+
cwd: this.options.cwd,
|
|
1580
|
+
signal,
|
|
1581
|
+
bufferBytes: this.options.machineBufferBytes,
|
|
1582
|
+
transport: this.kind,
|
|
1583
|
+
label: "SDK teaching check"
|
|
1584
|
+
}
|
|
1585
|
+
);
|
|
1586
|
+
let parsed;
|
|
1587
|
+
try {
|
|
1588
|
+
parsed = JSON.parse(captured.stdout.trim());
|
|
1589
|
+
} catch {
|
|
1590
|
+
return refused;
|
|
1591
|
+
}
|
|
1592
|
+
const plain = machineObject(parsed);
|
|
1593
|
+
if (!plain) return refused;
|
|
1594
|
+
const report = {
|
|
1595
|
+
...plain,
|
|
1596
|
+
clean: false,
|
|
1597
|
+
exitCode: captured.exitCode,
|
|
1598
|
+
...refused.error === void 0 ? {} : { snapshot_error: refused.error }
|
|
1599
|
+
};
|
|
1600
|
+
delete report.execution_snapshot;
|
|
1601
|
+
return report;
|
|
1602
|
+
}
|
|
1603
|
+
async json(path3, init, authenticated = true, acceptedStatuses = [200], operation) {
|
|
1604
|
+
const { object } = await this.jsonWithStatus(
|
|
1605
|
+
path3,
|
|
1606
|
+
init,
|
|
1607
|
+
authenticated,
|
|
1608
|
+
acceptedStatuses,
|
|
1609
|
+
operation
|
|
1610
|
+
);
|
|
1611
|
+
return object;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* The JSON object an accepted status carries, with that status, for the
|
|
1615
|
+
* routes whose accepted statuses mean different things (cancel: 200 is a
|
|
1616
|
+
* settled job, 202 an accepted request).
|
|
1617
|
+
*/
|
|
1618
|
+
async jsonWithStatus(path3, init, authenticated, acceptedStatuses, operation) {
|
|
1619
|
+
const outcome = await this.jsonOutcome(path3, init, authenticated, acceptedStatuses, operation);
|
|
1620
|
+
if ("refusal" in outcome) throw this.refused(path3, outcome);
|
|
1621
|
+
return outcome;
|
|
1622
|
+
}
|
|
1623
|
+
async jsonOutcome(path3, init, authenticated, acceptedStatuses, operation, strictRefusal = false) {
|
|
1624
|
+
const response = await this.fetchResponse(path3, init, true, authenticated, false);
|
|
1625
|
+
if (!acceptedStatuses.includes(response.status)) {
|
|
1626
|
+
if (response.ok) {
|
|
1627
|
+
await discardResponse(response);
|
|
1628
|
+
throw new NikaProtocolError(
|
|
1629
|
+
this.kind,
|
|
1630
|
+
`HTTP ${path3} returned non-contract status ${response.status}`
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
const refusal = operation === void 0 ? void 0 : await this.readRefusal(response, path3, strictRefusal);
|
|
1634
|
+
if (operation !== void 0 && refusal) {
|
|
1635
|
+
return { operation, status: response.status, refusal };
|
|
1636
|
+
}
|
|
1637
|
+
await discardResponse(response);
|
|
1638
|
+
throw new NikaTransportError(
|
|
1639
|
+
this.kind,
|
|
1640
|
+
`HTTP ${response.status} for ${path3}: [REDACTED]`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1644
|
+
if (contentType !== "application/json") {
|
|
1645
|
+
await discardResponse(response);
|
|
1646
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path3} returned an invalid content-type`);
|
|
1647
|
+
}
|
|
1648
|
+
return {
|
|
1649
|
+
object: await this.readObservationObject(response, path3, init.signal ?? void 0),
|
|
1650
|
+
status: response.status
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
refused(path3, outcome) {
|
|
1654
|
+
const { code, message } = outcome.refusal;
|
|
1655
|
+
return new NikaOperationError(
|
|
1656
|
+
outcome.operation,
|
|
1657
|
+
this.kind,
|
|
1658
|
+
code,
|
|
1659
|
+
`HTTP ${outcome.status} for ${path3}: ${code}` + (message ? ` (${message})` : ""),
|
|
1660
|
+
{ status: outcome.status, machineCode: code }
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Read a non-2xx body only when the server typed it as an engine refusal.
|
|
1665
|
+
* Anything else (plain text, oversized, malformed, a code that is not a
|
|
1666
|
+
* plain identifier, a code that carries the bearer token) is discarded and
|
|
1667
|
+
* the caller falls back to the redacted transport error.
|
|
1668
|
+
*/
|
|
1669
|
+
async readRefusal(response, path3, strict = false) {
|
|
1670
|
+
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1671
|
+
if (contentType !== "application/json") {
|
|
1672
|
+
await discardResponse(response);
|
|
1673
|
+
return void 0;
|
|
1674
|
+
}
|
|
1675
|
+
let object;
|
|
1676
|
+
try {
|
|
1677
|
+
object = await this.readObservationObject(response, path3);
|
|
1678
|
+
} catch {
|
|
1679
|
+
return void 0;
|
|
1680
|
+
}
|
|
1681
|
+
const error = machineObject(object.error);
|
|
1682
|
+
if (strict && (Object.keys(object).some((key) => key !== "error") || !error || Object.keys(error).some((key) => key !== "code" && key !== "message") || typeof error.message !== "string")) return void 0;
|
|
1683
|
+
const code = error?.code;
|
|
1684
|
+
if (typeof code !== "string" || !/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/.test(code) || code.includes(this.options.token)) {
|
|
1685
|
+
return void 0;
|
|
1686
|
+
}
|
|
1687
|
+
const message = typeof error?.message === "string" && error.message.length > 0 ? this.redact(error.message) : void 0;
|
|
1688
|
+
return { code, ...strict ? { message: this.redact(error?.message) } : message ? { message } : {} };
|
|
1689
|
+
}
|
|
1690
|
+
/** Engine messages are already path-free; a reflected bearer token never survives. */
|
|
1691
|
+
redact(text) {
|
|
1692
|
+
const clean = text.replace(/[\u0000-\u001f\u007f]+/g, " ").split(this.options.token).join("[REDACTED]");
|
|
1693
|
+
return clean.length > 240 ? `${clean.slice(0, 240)}...` : clean;
|
|
1694
|
+
}
|
|
1695
|
+
async operationJson(operation, path3, init) {
|
|
1696
|
+
const response = await this.fetchResponse(path3, init, true, true, false);
|
|
1697
|
+
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1698
|
+
if (contentType !== "application/json") {
|
|
1699
|
+
await discardResponse(response);
|
|
1700
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path3} returned an invalid content-type`);
|
|
1701
|
+
}
|
|
1702
|
+
const object = await this.readObservationObject(response, path3);
|
|
1703
|
+
if (!response.ok) throw this.operationFailure(operation, response.status, object);
|
|
1704
|
+
return object;
|
|
1705
|
+
}
|
|
1706
|
+
operationFailure(operation, status, object) {
|
|
1707
|
+
if (status === 412 && operation === "schedule") {
|
|
1708
|
+
const machine2 = machineObject(object.error);
|
|
1709
|
+
const currentRevision = machine2?.currentRevision;
|
|
1710
|
+
return new NikaOperationError(
|
|
1711
|
+
operation,
|
|
1712
|
+
this.kind,
|
|
1713
|
+
"schedule_conflict",
|
|
1714
|
+
typeof machine2?.message === "string" ? machine2.message : "Schedule revision conflict",
|
|
1715
|
+
{
|
|
1716
|
+
status,
|
|
1717
|
+
machineCode: typeof machine2?.code === "string" ? machine2.code : void 0,
|
|
1718
|
+
...typeof currentRevision === "string" || currentRevision === null ? { currentRevision } : {}
|
|
1719
|
+
}
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
if (Array.isArray(object.findings)) {
|
|
1723
|
+
const findings = operationFindings(object.findings);
|
|
1724
|
+
if (!findings) {
|
|
1725
|
+
throw new NikaProtocolError(this.kind, "Schedule refusal findings were malformed");
|
|
1726
|
+
}
|
|
1727
|
+
return new NikaOperationError(
|
|
1728
|
+
operation,
|
|
1729
|
+
this.kind,
|
|
1730
|
+
"schedule_refused",
|
|
1731
|
+
"Schedule was refused by the resident authority",
|
|
1732
|
+
{ status, findings }
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
const machine = machineObject(object.error);
|
|
1736
|
+
if (typeof machine?.code !== "string" || typeof machine.message !== "string") {
|
|
1737
|
+
throw new NikaProtocolError(this.kind, `HTTP ${pathForOperation(operation)} error was malformed`);
|
|
1738
|
+
}
|
|
1739
|
+
return new NikaOperationError(
|
|
1740
|
+
operation,
|
|
1741
|
+
this.kind,
|
|
1742
|
+
machine.code,
|
|
1743
|
+
machine.message,
|
|
1744
|
+
{ status, machineCode: machine.code }
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
async fetchResponse(path3, init, withTimeout, authenticated = true, requireOk = true) {
|
|
1748
|
+
const controller = withTimeout ? new AbortController() : void 0;
|
|
1749
|
+
const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
|
|
1750
|
+
const callerSignal = init.signal;
|
|
1751
|
+
const abort = () => controller?.abort(callerSignal?.reason);
|
|
1752
|
+
callerSignal?.addEventListener("abort", abort, { once: true });
|
|
1753
|
+
const headers = new Headers(init.headers);
|
|
1754
|
+
if (authenticated) headers.set("Authorization", `Bearer ${this.options.token}`);
|
|
1755
|
+
try {
|
|
1756
|
+
const response = await this.options.fetch(`${this.options.url}${path3}`, {
|
|
1757
|
+
...init,
|
|
1758
|
+
headers,
|
|
1759
|
+
signal: controller?.signal ?? callerSignal
|
|
1760
|
+
});
|
|
1761
|
+
if (requireOk && !response.ok) {
|
|
1762
|
+
await discardResponse(response);
|
|
1763
|
+
throw new NikaTransportError(
|
|
1764
|
+
this.kind,
|
|
1765
|
+
`HTTP ${response.status} for ${path3}: [REDACTED]`
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
return response;
|
|
1769
|
+
} catch (cause) {
|
|
1770
|
+
if (cause instanceof NikaTransportError) throw cause;
|
|
1771
|
+
if (controller?.signal.aborted && !callerSignal?.aborted) {
|
|
1772
|
+
throw new NikaTransportError(
|
|
1773
|
+
this.kind,
|
|
1774
|
+
`HTTP request timed out after ${this.options.requestTimeout}ms`,
|
|
1775
|
+
{ cause: cause instanceof Error ? cause : void 0 }
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
throw new NikaTransportError(this.kind, "HTTP transport failed", {
|
|
1779
|
+
cause: cause instanceof Error ? cause : void 0
|
|
1780
|
+
});
|
|
1781
|
+
} finally {
|
|
1782
|
+
if (timer) clearTimeout(timer);
|
|
1783
|
+
callerSignal?.removeEventListener("abort", abort);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
function workflowPath(name) {
|
|
1788
|
+
const segments = name.split("/");
|
|
1789
|
+
if (name.startsWith("/") || name.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
1790
|
+
throw new TypeError("workflow name must be a contained slash-separated path");
|
|
1791
|
+
}
|
|
1792
|
+
return segments.map(encodeURIComponent).join("/");
|
|
1793
|
+
}
|
|
1794
|
+
function isContainedWorkflowName(value) {
|
|
1795
|
+
if (typeof value !== "string" || !value.endsWith(".nika.yaml")) return false;
|
|
1796
|
+
try {
|
|
1797
|
+
workflowPath(value);
|
|
1798
|
+
return true;
|
|
1799
|
+
} catch {
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function scheduleBody(workflow, options) {
|
|
1804
|
+
return {
|
|
1805
|
+
workflow,
|
|
1806
|
+
when: options.when,
|
|
1807
|
+
maxCostUsd: options.maxCostUsd,
|
|
1808
|
+
missed: options.missed,
|
|
1809
|
+
...options.maxLatenessSeconds !== void 0 ? { maxLatenessSeconds: options.maxLatenessSeconds } : {},
|
|
1810
|
+
...options.overlap !== void 0 ? { overlap: options.overlap } : {},
|
|
1811
|
+
...options.afterSkip !== void 0 ? { afterSkip: options.afterSkip } : {},
|
|
1812
|
+
...options.jitter !== void 0 ? { jitter: options.jitter } : {},
|
|
1813
|
+
...options.tolerance !== void 0 ? { tolerance: options.tolerance } : {},
|
|
1814
|
+
...options.active !== void 0 ? { active: options.active } : {},
|
|
1815
|
+
...options.pauseReason !== void 0 ? { pauseReason: options.pauseReason } : {},
|
|
1816
|
+
...options.pauseUntil !== void 0 ? { pauseUntil: options.pauseUntil } : {}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
function scheduleStatusProjection(value) {
|
|
1820
|
+
const status = machineObject(value);
|
|
1821
|
+
const definition = machineObject(status?.definition);
|
|
1822
|
+
const when = machineObject(definition?.when);
|
|
1823
|
+
const due = machineObject(status?.due);
|
|
1824
|
+
const finding = machineObject(status?.finding);
|
|
1825
|
+
return !!status && !!definition && typeof definition.id === "string" && typeof definition.workflow === "string" && !!when && typeof when.kind === "string" && typeof definition.maxCostUsd === "number" && typeof definition.missed === "string" && (definition.maxLatenessSeconds === null || typeof definition.maxLatenessSeconds === "number") && typeof definition.overlap === "string" && typeof definition.afterSkip === "string" && (definition.jitter === null || typeof definition.jitter === "string") && (definition.tolerance === null || typeof definition.tolerance === "string") && typeof definition.active === "boolean" && (definition.pauseReason === null || typeof definition.pauseReason === "string") && (definition.pauseUntil === null || typeof definition.pauseUntil === "string") && typeof status.origin === "string" && typeof status.revision === "string" && typeof status.active === "boolean" && (status.pause === null || !!machineObject(status.pause)) && Array.isArray(status.next) && status.next.every(scheduleSlotProjection) && (status.earliestWakeHint === null || typeof status.earliestWakeHint === "string") && (status.lastDecision === null || !!machineObject(status.lastDecision)) && (typeof due?.kind === "string" && status.finding === void 0 || status.due === void 0 && scheduleFindingProjection(finding));
|
|
1826
|
+
}
|
|
1827
|
+
function scheduleSlotProjection(value) {
|
|
1828
|
+
const slot = machineObject(value);
|
|
1829
|
+
return typeof slot?.slotId === "string" && typeof slot.scheduledFor === "string" && (slot.requestedCivil === null || typeof slot.requestedCivil === "string") && typeof slot.shift === "string";
|
|
1830
|
+
}
|
|
1831
|
+
function scheduleFindingProjection(value) {
|
|
1832
|
+
const finding = machineObject(value);
|
|
1833
|
+
return typeof finding?.code === "string" && typeof finding.detail === "string";
|
|
1834
|
+
}
|
|
1835
|
+
function operationFindings(values) {
|
|
1836
|
+
const findings = [];
|
|
1837
|
+
for (const value of values) {
|
|
1838
|
+
const finding = machineObject(value);
|
|
1839
|
+
if (typeof finding?.code !== "string" || typeof finding.detail !== "string") {
|
|
1840
|
+
return void 0;
|
|
1841
|
+
}
|
|
1842
|
+
findings.push(finding);
|
|
1843
|
+
}
|
|
1844
|
+
return findings;
|
|
1845
|
+
}
|
|
1846
|
+
function pathForOperation(operation) {
|
|
1847
|
+
if (operation === "schedule") return "schedule apply";
|
|
1848
|
+
if (operation === "scheduleStatus") return "schedule status";
|
|
1849
|
+
return operation;
|
|
1850
|
+
}
|
|
1851
|
+
function isObservationEnded(source) {
|
|
1852
|
+
return isExecutionTerminal(source.status) || source.status === "paused" && machineObject(source.settlement)?.status === "paused";
|
|
1853
|
+
}
|
|
1854
|
+
function isExecutionTerminal(status) {
|
|
1855
|
+
return status === "succeeded" || status === "failed" || status === "interrupted" || status === "cancelled";
|
|
1856
|
+
}
|
|
1857
|
+
function isRetryableStatus(status) {
|
|
1858
|
+
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
1859
|
+
}
|
|
1860
|
+
function retryAfter(value) {
|
|
1861
|
+
if (!value) return void 0;
|
|
1862
|
+
const trimmed = value.trim();
|
|
1863
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1864
|
+
const seconds = Number(trimmed);
|
|
1865
|
+
return Number.isSafeInteger(seconds) ? boundedDelay(seconds * 1e3) : void 0;
|
|
1866
|
+
}
|
|
1867
|
+
const date = Date.parse(trimmed);
|
|
1868
|
+
if (!Number.isFinite(date)) return void 0;
|
|
1869
|
+
return boundedDelay(Math.max(0, date - Date.now()));
|
|
1870
|
+
}
|
|
1871
|
+
function boundedDelay(milliseconds) {
|
|
1872
|
+
return Math.max(
|
|
1873
|
+
RETRY_MIN_MILLISECONDS,
|
|
1874
|
+
Math.min(Math.floor(milliseconds), RETRY_MAX_MILLISECONDS)
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
function abortableDelay(milliseconds, signal) {
|
|
1878
|
+
if (signal.aborted) return Promise.reject(signal.reason ?? new Error("Observation aborted"));
|
|
1879
|
+
return new Promise((resolve, reject) => {
|
|
1880
|
+
const timer = setTimeout(done, milliseconds);
|
|
1881
|
+
function done() {
|
|
1882
|
+
signal.removeEventListener("abort", abort);
|
|
1883
|
+
resolve();
|
|
1884
|
+
}
|
|
1885
|
+
function abort() {
|
|
1886
|
+
clearTimeout(timer);
|
|
1887
|
+
signal.removeEventListener("abort", abort);
|
|
1888
|
+
reject(signal.reason ?? new Error("Observation aborted"));
|
|
1889
|
+
}
|
|
1890
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
async function discardResponse(response) {
|
|
1894
|
+
await response.body?.cancel().catch(() => {
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
function isRetryObservation(value) {
|
|
1898
|
+
return "retry" in value;
|
|
1899
|
+
}
|
|
1900
|
+
function durableJob(value, expectedId, transport) {
|
|
1901
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1902
|
+
"id",
|
|
1903
|
+
"status",
|
|
1904
|
+
"execution_id",
|
|
1905
|
+
"trace_id",
|
|
1906
|
+
"outputs",
|
|
1907
|
+
"receipt",
|
|
1908
|
+
"error",
|
|
1909
|
+
"settlement"
|
|
1910
|
+
]);
|
|
1911
|
+
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
1912
|
+
throw new NikaProtocolError(transport, "Durable job response contained unknown fields");
|
|
1913
|
+
}
|
|
1914
|
+
if (value.id !== expectedId || typeof value.status !== "string" || !JOB_STATUSES.has(value.status)) {
|
|
1915
|
+
throw new NikaProtocolError(transport, "Durable job response had an invalid id or status");
|
|
1916
|
+
}
|
|
1917
|
+
for (const field of ["execution_id", "trace_id"]) {
|
|
1918
|
+
if (value[field] !== void 0 && typeof value[field] !== "string") {
|
|
1919
|
+
throw new NikaProtocolError(transport, `Durable job response ${field} was not a string`);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
let error;
|
|
1923
|
+
if (value.error !== void 0) {
|
|
1924
|
+
const object = machineObject(value.error);
|
|
1925
|
+
if (!object || Object.keys(object).some((key) => key !== "code" && key !== "message") || typeof object.code !== "string" || typeof object.message !== "string") {
|
|
1926
|
+
throw new NikaProtocolError(transport, "Durable job response error was malformed");
|
|
1927
|
+
}
|
|
1928
|
+
error = { code: object.code, message: object.message };
|
|
1929
|
+
}
|
|
1930
|
+
const outputs = value.outputs === void 0 ? void 0 : machineObject(value.outputs);
|
|
1931
|
+
const receipt = value.receipt === void 0 ? void 0 : machineObject(value.receipt);
|
|
1932
|
+
const settlement = value.settlement === void 0 ? void 0 : readSettlement(value.settlement, transport, value.status);
|
|
1933
|
+
if (value.outputs !== void 0 && !outputs) {
|
|
1934
|
+
throw new NikaProtocolError(transport, "Durable job response outputs were malformed");
|
|
1935
|
+
}
|
|
1936
|
+
if (value.receipt !== void 0 && !receipt) {
|
|
1937
|
+
throw new NikaProtocolError(transport, "Durable job response receipt was malformed");
|
|
1938
|
+
}
|
|
1939
|
+
if (receipt) {
|
|
1940
|
+
assertReceiptIdentity(
|
|
1941
|
+
receipt,
|
|
1942
|
+
expectedId,
|
|
1943
|
+
transport,
|
|
1944
|
+
typeof value.execution_id === "string" ? value.execution_id : void 0,
|
|
1945
|
+
typeof value.trace_id === "string" ? value.trace_id : void 0
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
return {
|
|
1949
|
+
id: value.id,
|
|
1950
|
+
status: value.status,
|
|
1951
|
+
...typeof value.execution_id === "string" ? { execution_id: value.execution_id } : {},
|
|
1952
|
+
...typeof value.trace_id === "string" ? { trace_id: value.trace_id } : {},
|
|
1953
|
+
...outputs ? { outputs } : {},
|
|
1954
|
+
...receipt ? { receipt: Object.freeze(receipt) } : {},
|
|
1955
|
+
...error ? { error } : {},
|
|
1956
|
+
...settlement ? { settlement } : {}
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
function assertReceiptIdentity(receipt, expectedJobId, transport, expectedExecutionId, expectedTraceId, expectedSnapshotDigest) {
|
|
1960
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1961
|
+
"job_id",
|
|
1962
|
+
"execution_id",
|
|
1963
|
+
"trace_id",
|
|
1964
|
+
"snapshot_digest",
|
|
1965
|
+
"origin",
|
|
1966
|
+
"chain_head"
|
|
1967
|
+
]);
|
|
1968
|
+
if (Object.keys(receipt).some((key) => !allowed.has(key))) {
|
|
1969
|
+
throw new NikaProtocolError(transport, "Receipt contained unknown fields");
|
|
1970
|
+
}
|
|
1971
|
+
if (receipt.job_id !== expectedJobId) {
|
|
1972
|
+
throw new NikaProtocolError(transport, "Receipt job identity did not match its run");
|
|
1973
|
+
}
|
|
1974
|
+
if (typeof receipt.execution_id !== "string" || receipt.execution_id.length === 0) {
|
|
1975
|
+
throw new NikaProtocolError(transport, "Receipt omitted its execution identity");
|
|
1976
|
+
}
|
|
1977
|
+
if (typeof receipt.trace_id !== "string" || receipt.trace_id.length === 0) {
|
|
1978
|
+
throw new NikaProtocolError(transport, "Receipt omitted its trace identity");
|
|
1979
|
+
}
|
|
1980
|
+
if (typeof receipt.snapshot_digest !== "string" || !/^[0-9a-f]{64}$/.test(receipt.snapshot_digest)) {
|
|
1981
|
+
throw new NikaProtocolError(transport, "Receipt omitted its canonical snapshot digest");
|
|
1982
|
+
}
|
|
1983
|
+
if (receipt.chain_head !== void 0 && (typeof receipt.chain_head !== "string" || receipt.chain_head.length === 0)) {
|
|
1984
|
+
throw new NikaProtocolError(transport, "Receipt chain head was malformed");
|
|
1985
|
+
}
|
|
1986
|
+
if (receipt.origin !== void 0) validateReceiptOrigin(receipt.origin, transport);
|
|
1987
|
+
if (expectedExecutionId !== void 0 && receipt.execution_id !== expectedExecutionId) {
|
|
1988
|
+
throw new NikaProtocolError(transport, "Receipt execution identity did not match its job");
|
|
1989
|
+
}
|
|
1990
|
+
if (expectedTraceId !== void 0 && receipt.trace_id !== expectedTraceId) {
|
|
1991
|
+
throw new NikaProtocolError(transport, "Receipt trace identity did not match its job");
|
|
1992
|
+
}
|
|
1993
|
+
if (expectedSnapshotDigest !== void 0 && receipt.snapshot_digest !== expectedSnapshotDigest) {
|
|
1994
|
+
throw new NikaProtocolError(transport, "Receipt snapshot digest did not match its admission");
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function snapshotAck(value) {
|
|
1998
|
+
const allowed = /* @__PURE__ */ new Set(["status", "snapshot_digest", "root", "units"]);
|
|
1999
|
+
if (Object.keys(value).some((key) => !allowed.has(key)) || value.status !== "accepted" || typeof value.snapshot_digest !== "string" || !/^[0-9a-f]{64}$/.test(value.snapshot_digest) || typeof value.root !== "string" || value.root.length === 0 || typeof value.units !== "number" || !Number.isSafeInteger(value.units) || value.units < 1) {
|
|
2000
|
+
return void 0;
|
|
2001
|
+
}
|
|
2002
|
+
return {
|
|
2003
|
+
status: "accepted",
|
|
2004
|
+
snapshot_digest: value.snapshot_digest,
|
|
2005
|
+
root: value.root,
|
|
2006
|
+
units: value.units
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
function snapshotIdentity(bytes, transport) {
|
|
2010
|
+
let value;
|
|
2011
|
+
try {
|
|
2012
|
+
value = JSON.parse(bytes);
|
|
2013
|
+
} catch (cause) {
|
|
2014
|
+
throw new NikaCompatibilityError(
|
|
2015
|
+
"executionSnapshot",
|
|
2016
|
+
transport,
|
|
2017
|
+
"Local engine emitted malformed execution_snapshot bytes"
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
const snapshot = machineObject(value);
|
|
2021
|
+
if (!snapshot || typeof snapshot.digest !== "string" || !/^[0-9a-f]{64}$/.test(snapshot.digest) || typeof snapshot.root !== "string" || snapshot.root.length === 0 || !Array.isArray(snapshot.units) || snapshot.units.length < 1) {
|
|
2022
|
+
throw new NikaCompatibilityError(
|
|
2023
|
+
"executionSnapshot",
|
|
2024
|
+
transport,
|
|
2025
|
+
"Local engine emitted an invalid execution_snapshot identity"
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
return { digest: snapshot.digest, root: snapshot.root, units: snapshot.units.length };
|
|
2029
|
+
}
|
|
2030
|
+
function validateReceiptOrigin(value, transport) {
|
|
2031
|
+
const origin = machineObject(value);
|
|
2032
|
+
if (!origin || typeof origin.kind !== "string") {
|
|
2033
|
+
throw new NikaProtocolError(transport, "Receipt origin was malformed");
|
|
2034
|
+
}
|
|
2035
|
+
if (origin.kind === "manual") {
|
|
2036
|
+
if (Object.keys(origin).some((key) => key !== "kind")) {
|
|
2037
|
+
throw new NikaProtocolError(transport, "Manual receipt origin contained unknown fields");
|
|
2038
|
+
}
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
const required = [
|
|
2042
|
+
"kind",
|
|
2043
|
+
"schedule_origin",
|
|
2044
|
+
"schedule_id",
|
|
2045
|
+
"schedule_revision",
|
|
2046
|
+
"slot_id",
|
|
2047
|
+
"decision",
|
|
2048
|
+
"scheduled_for",
|
|
2049
|
+
"fired_at",
|
|
2050
|
+
"arm_generation"
|
|
2051
|
+
];
|
|
2052
|
+
if (origin.kind !== "schedule" || Object.keys(origin).some((key) => !required.includes(key)) || required.some((key) => !Object.hasOwn(origin, key)) || !["project", "api"].includes(String(origin.schedule_origin)) || !["scheduled", "catch_up"].includes(String(origin.decision)) || required.slice(2).some((key) => typeof origin[key] !== "string" || origin[key].length === 0) || Buffer.byteLength(String(origin.schedule_id)) > 255 || !/^sha256:[0-9a-f]{64}$/.test(String(origin.schedule_revision)) || !/^[0-9a-f]{64}$/.test(String(origin.slot_id)) || !/^[0-9a-f]{64}$/.test(String(origin.arm_generation)) || !isCanonicalTimestamp(String(origin.scheduled_for)) || !isCanonicalTimestamp(String(origin.fired_at))) {
|
|
2053
|
+
throw new NikaProtocolError(transport, "Scheduled receipt origin was malformed");
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
function isCanonicalTimestamp(value) {
|
|
2057
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
|
|
2058
|
+
if (!match) return false;
|
|
2059
|
+
const [
|
|
2060
|
+
,
|
|
2061
|
+
yearText,
|
|
2062
|
+
monthText,
|
|
2063
|
+
dayText,
|
|
2064
|
+
hourText,
|
|
2065
|
+
minuteText,
|
|
2066
|
+
secondText,
|
|
2067
|
+
offsetSign,
|
|
2068
|
+
offsetHourText,
|
|
2069
|
+
offsetMinuteText
|
|
2070
|
+
] = match;
|
|
2071
|
+
const year = Number(yearText);
|
|
2072
|
+
const month = Number(monthText);
|
|
2073
|
+
const day = Number(dayText);
|
|
2074
|
+
const hour = Number(hourText);
|
|
2075
|
+
const minute = Number(minuteText);
|
|
2076
|
+
const second = Number(secondText);
|
|
2077
|
+
const offsetHour = offsetSign ? Number(offsetHourText) : 0;
|
|
2078
|
+
const offsetMinute = offsetSign ? Number(offsetMinuteText) : 0;
|
|
2079
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
2080
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
2081
|
+
return year > 0 && month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1] && hour <= 23 && minute <= 59 && second <= 59 && offsetHour <= 23 && offsetMinute <= 59 && Number.isFinite(Date.parse(value));
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/lib/native-process-transport.ts
|
|
2085
|
+
var import_node_child_process2 = require("child_process");
|
|
2086
|
+
var import_node_crypto3 = require("crypto");
|
|
2087
|
+
var NativeProcessTransport = class {
|
|
2088
|
+
constructor(options) {
|
|
2089
|
+
this.options = options;
|
|
2090
|
+
}
|
|
2091
|
+
kind = "native-process";
|
|
2092
|
+
ready;
|
|
2093
|
+
async check(workflow, options) {
|
|
2094
|
+
await this.ensureReady();
|
|
2095
|
+
const args = ["check", workflow, "--json"];
|
|
2096
|
+
if (options.model) args.push("--model", options.model);
|
|
2097
|
+
if (options.nativeStrict) args.push("--native-strict");
|
|
2098
|
+
const captured = await this.capture(args, options.signal);
|
|
2099
|
+
const report = reportObject(captured.stdout) ?? reportObject(stripEnginePrefix(captured.stderr));
|
|
2100
|
+
if (!report) {
|
|
2101
|
+
const excerpt = diagnosticExcerpt(captured.stderr);
|
|
2102
|
+
throw new NikaCompatibilityError(
|
|
2103
|
+
"check",
|
|
2104
|
+
this.kind,
|
|
2105
|
+
`This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (excerpt ? `: ${excerpt}` : "")
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
return { ...report, exitCode: captured.exitCode };
|
|
2109
|
+
}
|
|
2110
|
+
async startRun(workflow, options) {
|
|
2111
|
+
await this.ensureReady();
|
|
2112
|
+
if (options.idempotencyKey !== void 0) {
|
|
2113
|
+
throw new NikaCompatibilityError(
|
|
2114
|
+
"idempotencyKey",
|
|
2115
|
+
this.kind,
|
|
2116
|
+
"idempotencyKey is an HTTP admission option"
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
const args = ["run", workflow, "--json", ...runFlags(options)];
|
|
2120
|
+
const child = (0, import_node_child_process2.spawn)(this.options.engine.bin, args, {
|
|
2121
|
+
cwd: this.options.cwd,
|
|
2122
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2123
|
+
shell: false
|
|
2124
|
+
});
|
|
2125
|
+
return this.processRun((0, import_node_crypto3.randomUUID)(), child);
|
|
2126
|
+
}
|
|
2127
|
+
async attachRun(_id, _options) {
|
|
2128
|
+
throw new NikaCompatibilityError(
|
|
2129
|
+
"attachRun",
|
|
2130
|
+
this.kind,
|
|
2131
|
+
"Only a resident nika serve authority owns durable jobs across client processes"
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
async listWorkflows() {
|
|
2135
|
+
throw new NikaCompatibilityError(
|
|
2136
|
+
"workflowCatalog",
|
|
2137
|
+
this.kind,
|
|
2138
|
+
"The contained workflow catalog belongs to a resident nika serve authority"
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
async workflow(_name) {
|
|
2142
|
+
throw new NikaCompatibilityError(
|
|
2143
|
+
"workflowCatalog",
|
|
2144
|
+
this.kind,
|
|
2145
|
+
"The contained workflow catalog belongs to a resident nika serve authority"
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
async schedule(_workflow, _options) {
|
|
2149
|
+
throw new NikaCompatibilityError(
|
|
2150
|
+
"schedule",
|
|
2151
|
+
this.kind,
|
|
2152
|
+
"A direct native process is not a proven resident schedule authority; connect to nika serve"
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
async scheduleStatus(_id) {
|
|
2156
|
+
throw new NikaCompatibilityError(
|
|
2157
|
+
"schedule",
|
|
2158
|
+
this.kind,
|
|
2159
|
+
"A direct native process is not a proven resident schedule authority; connect to nika serve"
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
async traceVerify(receipt, options) {
|
|
2163
|
+
await this.ensureReady();
|
|
2164
|
+
const locator = receiptTraceLocator(receipt);
|
|
2165
|
+
if (!locator) {
|
|
2166
|
+
throw new NikaCompatibilityError(
|
|
2167
|
+
"traceVerify",
|
|
2168
|
+
this.kind,
|
|
2169
|
+
"The engine receipt does not carry a native trace locator"
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
const captured = await this.capture(["trace", "verify", locator, "--plain"], options.signal);
|
|
2173
|
+
if (captured.exitCode !== 0) {
|
|
2174
|
+
return {
|
|
2175
|
+
verified: false,
|
|
2176
|
+
verdict: "invalid",
|
|
2177
|
+
exitCode: captured.exitCode,
|
|
2178
|
+
output: captured.stdout + captured.stderr
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
const evidence = await this.capture(
|
|
2182
|
+
["trace", "evidence", locator, "--json", "--plain"],
|
|
2183
|
+
options.signal
|
|
2184
|
+
);
|
|
2185
|
+
let manifest;
|
|
2186
|
+
try {
|
|
2187
|
+
manifest = parseMachineObject(evidence.stdout.trim(), this.kind);
|
|
2188
|
+
} catch (cause) {
|
|
2189
|
+
throw new NikaCompatibilityError(
|
|
2190
|
+
"traceVerify",
|
|
2191
|
+
this.kind,
|
|
2192
|
+
`This engine did not emit the required evidence manifest (exit ${evidence.exitCode})`
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
const mismatch = receiptMismatch(receipt, manifest);
|
|
2196
|
+
return {
|
|
2197
|
+
verified: mismatch === void 0,
|
|
2198
|
+
// The same vocabulary the HTTP verdict speaks, so a caller reads one
|
|
2199
|
+
// field on both transports instead of inferring it from exitCode.
|
|
2200
|
+
verdict: mismatch === void 0 ? "verified" : "invalid",
|
|
2201
|
+
...mismatch === void 0 ? {} : { reason: "receipt_mismatch" },
|
|
2202
|
+
exitCode: mismatch === void 0 ? 0 : 2,
|
|
2203
|
+
output: captured.stdout + captured.stderr + (mismatch === void 0 ? "" : `
|
|
2204
|
+
RECEIPT MISMATCH: ${mismatch}
|
|
2205
|
+
`)
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
processRun(id, child) {
|
|
2209
|
+
let settled = false;
|
|
2210
|
+
let lastEvent;
|
|
2211
|
+
let receipt;
|
|
2212
|
+
let outputs;
|
|
2213
|
+
let machineError;
|
|
2214
|
+
let settlement;
|
|
2215
|
+
let streamError;
|
|
2216
|
+
let refusal;
|
|
2217
|
+
let resolveDrained;
|
|
2218
|
+
const drained = new Promise((resolve) => {
|
|
2219
|
+
resolveDrained = resolve;
|
|
2220
|
+
});
|
|
2221
|
+
let stderr = "";
|
|
2222
|
+
child.stderr.setEncoding("utf8");
|
|
2223
|
+
child.stderr.on("data", (chunk) => {
|
|
2224
|
+
stderr = boundedAppend(stderr, chunk, this.options.machineBufferBytes);
|
|
2225
|
+
});
|
|
2226
|
+
const closed = new Promise((resolve, reject) => {
|
|
2227
|
+
child.once("error", (cause) => {
|
|
2228
|
+
streamError = new NikaTransportError(
|
|
2229
|
+
this.kind,
|
|
2230
|
+
`Cannot spawn ${this.options.engine.bin}: ${cause.message}`,
|
|
2231
|
+
{ cause }
|
|
2232
|
+
);
|
|
2233
|
+
reject(streamError);
|
|
2234
|
+
});
|
|
2235
|
+
child.once("close", (code, signal) => {
|
|
2236
|
+
settled = true;
|
|
2237
|
+
if (code === null && signal && !streamError) resolve(130);
|
|
2238
|
+
else resolve(code ?? 3);
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
closed.catch(() => {
|
|
2242
|
+
});
|
|
2243
|
+
const kind = this.kind;
|
|
2244
|
+
const machineBufferBytes = this.options.machineBufferBytes;
|
|
2245
|
+
const lineEvent = (line) => {
|
|
2246
|
+
const refused = engineRefusal(line);
|
|
2247
|
+
if (refused) {
|
|
2248
|
+
refusal ??= refused;
|
|
2249
|
+
return void 0;
|
|
2250
|
+
}
|
|
2251
|
+
return machineLine(line, kind);
|
|
2252
|
+
};
|
|
2253
|
+
const events = {
|
|
2254
|
+
[Symbol.asyncIterator]: async function* () {
|
|
2255
|
+
let buffer = "";
|
|
2256
|
+
child.stdout.setEncoding("utf8");
|
|
2257
|
+
try {
|
|
2258
|
+
for await (const chunk of child.stdout) {
|
|
2259
|
+
buffer += String(chunk);
|
|
2260
|
+
let newline;
|
|
2261
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
2262
|
+
const line = buffer.slice(0, newline).trim();
|
|
2263
|
+
buffer = buffer.slice(newline + 1);
|
|
2264
|
+
if (!line) continue;
|
|
2265
|
+
const event = lineEvent(line);
|
|
2266
|
+
if (!event) continue;
|
|
2267
|
+
lastEvent = event;
|
|
2268
|
+
receipt = eventReceipt(event) ?? receipt;
|
|
2269
|
+
outputs = eventOutputs(event) ?? outputs;
|
|
2270
|
+
const currentSettlement = eventSettlement(event, kind);
|
|
2271
|
+
machineError = currentSettlement ? eventError(event) : eventError(event) ?? machineError;
|
|
2272
|
+
settlement = currentSettlement ?? settlement;
|
|
2273
|
+
yield event;
|
|
2274
|
+
}
|
|
2275
|
+
if (Buffer.byteLength(buffer) > machineBufferBytes) {
|
|
2276
|
+
throw new NikaProtocolError(
|
|
2277
|
+
kind,
|
|
2278
|
+
`Native machine line exceeded ${machineBufferBytes} bytes`
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
const tail = buffer.trim();
|
|
2283
|
+
if (tail) {
|
|
2284
|
+
const event = lineEvent(tail);
|
|
2285
|
+
if (event) {
|
|
2286
|
+
lastEvent = event;
|
|
2287
|
+
receipt = eventReceipt(event) ?? receipt;
|
|
2288
|
+
outputs = eventOutputs(event) ?? outputs;
|
|
2289
|
+
const currentSettlement = eventSettlement(event, kind);
|
|
2290
|
+
machineError = currentSettlement ? eventError(event) : eventError(event) ?? machineError;
|
|
2291
|
+
settlement = currentSettlement ?? settlement;
|
|
2292
|
+
yield event;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
} catch (cause) {
|
|
2296
|
+
streamError = cause instanceof Error ? cause : new Error(String(cause));
|
|
2297
|
+
if (!settled) child.kill("SIGTERM");
|
|
2298
|
+
throw streamError;
|
|
2299
|
+
} finally {
|
|
2300
|
+
resolveDrained();
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
const done = Promise.all([closed, drained]).then(([exitCode]) => {
|
|
2305
|
+
if (streamError) throw streamError;
|
|
2306
|
+
if (refusal) {
|
|
2307
|
+
throw new NikaOperationError(
|
|
2308
|
+
"run",
|
|
2309
|
+
this.kind,
|
|
2310
|
+
refusal.code,
|
|
2311
|
+
refusal.line,
|
|
2312
|
+
{ status: exitCode }
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
const status = eventStatus(lastEvent) ?? statusForExit(exitCode, lastEvent?.kind);
|
|
2316
|
+
return {
|
|
2317
|
+
id,
|
|
2318
|
+
status,
|
|
2319
|
+
transport: this.kind,
|
|
2320
|
+
exitCode,
|
|
2321
|
+
...outputs ? { outputs } : {},
|
|
2322
|
+
...receipt ? { receipt } : {},
|
|
2323
|
+
...machineError ? { error: machineError } : {},
|
|
2324
|
+
...settlement ? { settlement } : {},
|
|
2325
|
+
...stderr ? { diagnostics: stderr } : {}
|
|
2326
|
+
};
|
|
2327
|
+
});
|
|
2328
|
+
done.catch(() => {
|
|
2329
|
+
});
|
|
2330
|
+
let cancelPromise;
|
|
2331
|
+
return {
|
|
2332
|
+
id,
|
|
2333
|
+
events,
|
|
2334
|
+
done,
|
|
2335
|
+
status: async () => {
|
|
2336
|
+
throw new NikaCompatibilityError(
|
|
2337
|
+
"runStatus",
|
|
2338
|
+
this.kind,
|
|
2339
|
+
"A direct native process has no independent durable status authority; await run.done"
|
|
2340
|
+
);
|
|
2341
|
+
},
|
|
2342
|
+
cancel: () => {
|
|
2343
|
+
cancelPromise ??= Promise.resolve(
|
|
2344
|
+
settled ? {
|
|
2345
|
+
runId: id,
|
|
2346
|
+
accepted: false,
|
|
2347
|
+
status: "already_settled",
|
|
2348
|
+
transport: this.kind
|
|
2349
|
+
} : {
|
|
2350
|
+
runId: id,
|
|
2351
|
+
accepted: child.kill("SIGTERM"),
|
|
2352
|
+
status: "cancellation_requested",
|
|
2353
|
+
transport: this.kind
|
|
2354
|
+
}
|
|
2355
|
+
);
|
|
2356
|
+
return cancelPromise;
|
|
2357
|
+
},
|
|
2358
|
+
cleanup: async () => {
|
|
2359
|
+
if (!settled) child.kill("SIGTERM");
|
|
2360
|
+
await closed.catch(() => {
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
capture(args, signal) {
|
|
2366
|
+
return new Promise((resolve, reject) => {
|
|
2367
|
+
if (signal?.aborted) {
|
|
2368
|
+
reject(new NikaTransportError(this.kind, "Operation aborted by caller"));
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
const child = (0, import_node_child_process2.spawn)(this.options.engine.bin, args, {
|
|
2372
|
+
cwd: this.options.cwd,
|
|
2373
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2374
|
+
shell: false
|
|
2375
|
+
});
|
|
2376
|
+
let stdout = "";
|
|
2377
|
+
let stderr = "";
|
|
2378
|
+
child.stdout.setEncoding("utf8");
|
|
2379
|
+
child.stderr.setEncoding("utf8");
|
|
2380
|
+
child.stdout.on("data", (chunk) => {
|
|
2381
|
+
stdout = boundedAppend(stdout, chunk, this.options.machineBufferBytes);
|
|
2382
|
+
});
|
|
2383
|
+
child.stderr.on("data", (chunk) => {
|
|
2384
|
+
stderr = boundedAppend(stderr, chunk, this.options.machineBufferBytes);
|
|
2385
|
+
});
|
|
2386
|
+
const abort = () => child.kill("SIGTERM");
|
|
2387
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2388
|
+
child.once("error", (cause) => {
|
|
2389
|
+
signal?.removeEventListener("abort", abort);
|
|
2390
|
+
reject(new NikaTransportError(
|
|
2391
|
+
this.kind,
|
|
2392
|
+
`Cannot spawn ${this.options.engine.bin}: ${cause.message}`,
|
|
2393
|
+
{ cause }
|
|
2394
|
+
));
|
|
2395
|
+
});
|
|
2396
|
+
child.once("close", (code) => {
|
|
2397
|
+
signal?.removeEventListener("abort", abort);
|
|
2398
|
+
if (signal?.aborted) {
|
|
2399
|
+
reject(new NikaTransportError(this.kind, "Operation aborted by caller"));
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
resolve({ exitCode: code ?? 3, stdout, stderr });
|
|
2403
|
+
});
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
ensureReady() {
|
|
2407
|
+
this.ready ??= verifyNikaEngine(this.options.engine).then(() => void 0);
|
|
2408
|
+
return this.ready;
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
var ENGINE_PREFIX = /^nika:\s*/;
|
|
2412
|
+
var REFUSAL_CODE = /^(NIKA-[A-Z0-9-]+)\b/;
|
|
2413
|
+
var DIAGNOSTIC_EXCERPT_LIMIT = 240;
|
|
2414
|
+
function reportObject(text) {
|
|
2415
|
+
const trimmed = text.trim();
|
|
2416
|
+
if (!trimmed) return void 0;
|
|
2417
|
+
let value;
|
|
2418
|
+
try {
|
|
2419
|
+
value = JSON.parse(trimmed);
|
|
2420
|
+
} catch {
|
|
2421
|
+
return void 0;
|
|
2422
|
+
}
|
|
2423
|
+
return machineObject(value);
|
|
2424
|
+
}
|
|
2425
|
+
function stripEnginePrefix(text) {
|
|
2426
|
+
return text.trim().replace(ENGINE_PREFIX, "");
|
|
2427
|
+
}
|
|
2428
|
+
function diagnosticExcerpt(text, limit = DIAGNOSTIC_EXCERPT_LIMIT) {
|
|
2429
|
+
const single = stripEnginePrefix(text).replace(/\s+/g, " ").trim();
|
|
2430
|
+
if (!single) return void 0;
|
|
2431
|
+
return single.length > limit ? `${single.slice(0, limit)}\u2026` : single;
|
|
2432
|
+
}
|
|
2433
|
+
function engineRefusal(line) {
|
|
2434
|
+
const code = REFUSAL_CODE.exec(line)?.[1];
|
|
2435
|
+
return code ? { code, line } : void 0;
|
|
2436
|
+
}
|
|
2437
|
+
function machineLine(line, kind) {
|
|
2438
|
+
try {
|
|
2439
|
+
return parseMachineObject(line, kind);
|
|
2440
|
+
} catch (cause) {
|
|
2441
|
+
const verdict = cause instanceof Error ? cause.message : "Engine machine output was unreadable";
|
|
2442
|
+
const excerpt = diagnosticExcerpt(line);
|
|
2443
|
+
throw new NikaProtocolError(
|
|
2444
|
+
kind,
|
|
2445
|
+
excerpt ? `${verdict}: ${excerpt}` : verdict,
|
|
2446
|
+
{ cause: cause instanceof Error ? cause : void 0 }
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
function runFlags(options) {
|
|
2451
|
+
const flags = [];
|
|
2452
|
+
for (const [key, value] of Object.entries(options.vars ?? {})) {
|
|
2453
|
+
flags.push("--var", `${key}=${String(value)}`);
|
|
2454
|
+
}
|
|
2455
|
+
if (options.model) flags.push("--model", options.model);
|
|
2456
|
+
if (options.maxCostUsd !== void 0) flags.push("--max-cost-usd", String(options.maxCostUsd));
|
|
2457
|
+
return flags;
|
|
2458
|
+
}
|
|
2459
|
+
function statusForExit(exitCode, kind) {
|
|
2460
|
+
if (kind === "workflow_completed") return "succeeded";
|
|
2461
|
+
if (kind === "workflow_failed") return "failed";
|
|
2462
|
+
if (kind === "workflow_paused") return "paused";
|
|
2463
|
+
if (kind === "workflow_cancelled") return "cancelled";
|
|
2464
|
+
switch (exitCode) {
|
|
2465
|
+
case 0:
|
|
2466
|
+
return "succeeded";
|
|
2467
|
+
case 4:
|
|
2468
|
+
return "paused";
|
|
2469
|
+
case 130:
|
|
2470
|
+
return "interrupted";
|
|
2471
|
+
default:
|
|
2472
|
+
return "failed";
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function boundedAppend(current, chunk, limit) {
|
|
2476
|
+
const combined = current + chunk;
|
|
2477
|
+
if (Buffer.byteLength(combined) <= limit) return combined;
|
|
2478
|
+
return Buffer.from(combined).subarray(-limit).toString("utf8");
|
|
2479
|
+
}
|
|
2480
|
+
function receiptMismatch(receipt, manifest) {
|
|
2481
|
+
const trace = machineObject(manifest.trace);
|
|
2482
|
+
const seal = machineObject(manifest.seal);
|
|
2483
|
+
const covers = machineObject(seal?.covers);
|
|
2484
|
+
const binding = machineObject(covers?.sdk_receipt);
|
|
2485
|
+
if (trace?.chain !== "intact" || seal?.present !== true || seal.verifies !== true || seal.covers_chain !== true || !binding) return "the evidence manifest has no verified signed SDK receipt binding";
|
|
2486
|
+
const claims = ["receipt_format", "execution_id", "trace_id", "snapshot_digest"];
|
|
2487
|
+
for (const claim of claims) {
|
|
2488
|
+
if (receipt[claim] !== binding[claim]) return `${claim} is not bound to this trace`;
|
|
2489
|
+
}
|
|
2490
|
+
if (receipt.chain_head !== trace.head) return "chain_head is not this journal head";
|
|
2491
|
+
if (receipt.chain_len !== trace.events) return "chain_len is not this journal length";
|
|
2492
|
+
if (receipt.sealed !== true) return "sealed is not true for this verified seal";
|
|
2493
|
+
return void 0;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// src/lib/run-session.ts
|
|
2497
|
+
var RunSession = class {
|
|
2498
|
+
constructor(source, eventBufferSize) {
|
|
2499
|
+
this.source = source;
|
|
2500
|
+
this.eventBufferSize = eventBufferSize;
|
|
2501
|
+
this.done = new Promise((resolve, reject) => {
|
|
2502
|
+
this.resolveDone = resolve;
|
|
2503
|
+
this.rejectDone = reject;
|
|
2504
|
+
});
|
|
2505
|
+
this.done.catch(() => {
|
|
2506
|
+
});
|
|
2507
|
+
this.run = Object.freeze({ id: source.id, done: this.done });
|
|
2508
|
+
void this.observeSettlement();
|
|
2509
|
+
void this.pump();
|
|
2510
|
+
}
|
|
2511
|
+
run;
|
|
2512
|
+
history = [];
|
|
2513
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
2514
|
+
done;
|
|
2515
|
+
resolveDone;
|
|
2516
|
+
rejectDone;
|
|
2517
|
+
terminal = false;
|
|
2518
|
+
cancelPromise;
|
|
2519
|
+
historyOverflowed = false;
|
|
2520
|
+
events(options = {}) {
|
|
2521
|
+
const requested = options.bufferSize ?? this.eventBufferSize;
|
|
2522
|
+
if (!Number.isInteger(requested) || requested < 1 || requested > this.eventBufferSize) {
|
|
2523
|
+
throw new RangeError(
|
|
2524
|
+
`events bufferSize must be an integer from 1 to ${this.eventBufferSize}`
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
if (this.historyOverflowed || this.history.length > requested) {
|
|
2528
|
+
throw new NikaEventBufferOverflowError(this.source.id, requested);
|
|
2529
|
+
}
|
|
2530
|
+
const subscription = new EventSubscription(
|
|
2531
|
+
this.source.id,
|
|
2532
|
+
requested,
|
|
2533
|
+
this.history.slice(-requested),
|
|
2534
|
+
options.signal,
|
|
2535
|
+
() => this.subscribers.delete(subscription)
|
|
2536
|
+
);
|
|
2537
|
+
if (this.terminal || options.signal?.aborted) subscription.close();
|
|
2538
|
+
else this.subscribers.add(subscription);
|
|
2539
|
+
return subscription;
|
|
2540
|
+
}
|
|
2541
|
+
cancel() {
|
|
2542
|
+
this.cancelPromise ??= this.requestCancel().catch((error) => {
|
|
2543
|
+
this.cancelPromise = void 0;
|
|
2544
|
+
throw error;
|
|
2545
|
+
});
|
|
2546
|
+
return this.cancelPromise;
|
|
2547
|
+
}
|
|
2548
|
+
status() {
|
|
2549
|
+
return this.source.status();
|
|
2550
|
+
}
|
|
2551
|
+
async requestCancel() {
|
|
2552
|
+
const cancellation = await this.source.cancel();
|
|
2553
|
+
if (cancellation.status === "cancellation_requested") return cancellation;
|
|
2554
|
+
const result = await this.source.done;
|
|
2555
|
+
if (!this.terminal && this.subscribers.size === 0) {
|
|
2556
|
+
this.terminal = true;
|
|
2557
|
+
this.resolveDone(result);
|
|
2558
|
+
for (const subscriber of [...this.subscribers]) subscriber.close();
|
|
2559
|
+
this.subscribers.clear();
|
|
2560
|
+
await this.source.cleanup().catch(() => {
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
return cancellation;
|
|
2564
|
+
}
|
|
2565
|
+
async observeSettlement() {
|
|
2566
|
+
try {
|
|
2567
|
+
this.resolveDone(await this.source.done);
|
|
2568
|
+
} catch (cause) {
|
|
2569
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2570
|
+
this.rejectDone(error);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
async pump() {
|
|
2574
|
+
try {
|
|
2575
|
+
for await (const event of this.source.events) {
|
|
2576
|
+
this.history.push(event);
|
|
2577
|
+
if (this.history.length > this.eventBufferSize) {
|
|
2578
|
+
this.history.shift();
|
|
2579
|
+
this.historyOverflowed = true;
|
|
2580
|
+
}
|
|
2581
|
+
for (const subscriber of [...this.subscribers]) subscriber.push(event);
|
|
2582
|
+
}
|
|
2583
|
+
this.resolveDone(await this.source.done);
|
|
2584
|
+
} catch (cause) {
|
|
2585
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2586
|
+
this.rejectDone(error);
|
|
2587
|
+
for (const subscriber of [...this.subscribers]) subscriber.fail(error);
|
|
2588
|
+
} finally {
|
|
2589
|
+
this.terminal = true;
|
|
2590
|
+
for (const subscriber of [...this.subscribers]) subscriber.close();
|
|
2591
|
+
this.subscribers.clear();
|
|
2592
|
+
await this.source.cleanup().catch(() => {
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
var EventSubscription = class {
|
|
2598
|
+
constructor(runId, limit, initial, signal, detach) {
|
|
2599
|
+
this.runId = runId;
|
|
2600
|
+
this.limit = limit;
|
|
2601
|
+
this.detach = detach;
|
|
2602
|
+
this.queue = [...initial];
|
|
2603
|
+
this.signal = signal;
|
|
2604
|
+
this.abort = () => this.close();
|
|
2605
|
+
if (signal?.aborted) this.closed = true;
|
|
2606
|
+
else signal?.addEventListener("abort", this.abort, { once: true });
|
|
2607
|
+
}
|
|
2608
|
+
queue;
|
|
2609
|
+
waiter;
|
|
2610
|
+
closed = false;
|
|
2611
|
+
error;
|
|
2612
|
+
abort;
|
|
2613
|
+
signal;
|
|
2614
|
+
[Symbol.asyncIterator]() {
|
|
2615
|
+
return this;
|
|
2616
|
+
}
|
|
2617
|
+
next() {
|
|
2618
|
+
const event = this.queue.shift();
|
|
2619
|
+
if (event) return Promise.resolve({ value: event, done: false });
|
|
2620
|
+
if (this.error) return Promise.reject(this.error);
|
|
2621
|
+
if (this.closed) return Promise.resolve({ value: void 0, done: true });
|
|
2622
|
+
if (this.waiter) {
|
|
2623
|
+
return Promise.reject(new Error("Concurrent next() calls are not supported"));
|
|
2624
|
+
}
|
|
2625
|
+
return new Promise((resolve, reject) => {
|
|
2626
|
+
this.waiter = { resolve, reject };
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
return() {
|
|
2630
|
+
this.close();
|
|
2631
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
2632
|
+
}
|
|
2633
|
+
push(event) {
|
|
2634
|
+
if (this.closed || this.error) return;
|
|
2635
|
+
if (this.waiter) {
|
|
2636
|
+
const waiter = this.waiter;
|
|
2637
|
+
this.waiter = void 0;
|
|
2638
|
+
waiter.resolve({ value: event, done: false });
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
if (this.queue.length >= this.limit) {
|
|
2642
|
+
this.fail(new NikaEventBufferOverflowError(this.runId, this.limit));
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
this.queue.push(event);
|
|
2646
|
+
}
|
|
2647
|
+
close() {
|
|
2648
|
+
if (this.closed) return;
|
|
2649
|
+
this.closed = true;
|
|
2650
|
+
this.signal?.removeEventListener("abort", this.abort);
|
|
2651
|
+
this.detach();
|
|
2652
|
+
if (this.waiter) {
|
|
2653
|
+
const waiter = this.waiter;
|
|
2654
|
+
this.waiter = void 0;
|
|
2655
|
+
waiter.resolve({ value: void 0, done: true });
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
fail(error) {
|
|
2659
|
+
if (this.closed || this.error) return;
|
|
2660
|
+
this.error = error;
|
|
2661
|
+
this.queue.length = 0;
|
|
2662
|
+
this.signal?.removeEventListener("abort", this.abort);
|
|
2663
|
+
this.detach();
|
|
2664
|
+
if (this.waiter) {
|
|
2665
|
+
const waiter = this.waiter;
|
|
2666
|
+
this.waiter = void 0;
|
|
2667
|
+
waiter.reject(error);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
};
|
|
2671
|
+
|
|
2672
|
+
// src/events.ts
|
|
2673
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
2674
|
+
"succeeded",
|
|
2675
|
+
"failed",
|
|
2676
|
+
"interrupted",
|
|
2677
|
+
"cancelled"
|
|
2678
|
+
]);
|
|
2679
|
+
function isNikaRunSettledEvent(event) {
|
|
2680
|
+
return event.kind === "run_settled" || event.kind === "execution.settled";
|
|
2681
|
+
}
|
|
2682
|
+
function isNikaTerminalEvent(event) {
|
|
2683
|
+
return typeof event.status === "string" && TERMINAL_STATUSES.has(event.status);
|
|
2684
|
+
}
|
|
2685
|
+
function isNikaRunSealedEvent(event) {
|
|
2686
|
+
return event.kind === "run_sealed";
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// src/index.ts
|
|
2690
|
+
var DEFAULT_EVENT_BUFFER_SIZE = 256;
|
|
2691
|
+
var DEFAULT_MACHINE_BUFFER_BYTES = 64 * 1024;
|
|
2692
|
+
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
2693
|
+
var Nika = class {
|
|
2694
|
+
transportKind;
|
|
2695
|
+
transport;
|
|
2696
|
+
sessions = /* @__PURE__ */ new WeakMap();
|
|
2697
|
+
eventBufferSize;
|
|
2698
|
+
constructor(config = {}) {
|
|
2699
|
+
this.eventBufferSize = positiveInteger(
|
|
2700
|
+
config.eventBufferSize ?? DEFAULT_EVENT_BUFFER_SIZE,
|
|
2701
|
+
"eventBufferSize"
|
|
2702
|
+
);
|
|
2703
|
+
const machineBufferBytes = positiveInteger(
|
|
2704
|
+
config.machineBufferBytes ?? DEFAULT_MACHINE_BUFFER_BYTES,
|
|
2705
|
+
"machineBufferBytes"
|
|
2706
|
+
);
|
|
2707
|
+
if (config.url !== void 0) {
|
|
2708
|
+
if (typeof config.token !== "string" || config.token.length === 0) {
|
|
2709
|
+
throw new NikaConfigurationError("A non-empty token is required with a Nika URL");
|
|
2710
|
+
}
|
|
2711
|
+
const url = checkedUrl(config.url, config.allowInsecureHttp === true);
|
|
2712
|
+
this.transport = new HttpTransport({
|
|
2713
|
+
url,
|
|
2714
|
+
token: checkedToken(config.token),
|
|
2715
|
+
fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
|
|
2716
|
+
requestTimeout: positiveInteger(
|
|
2717
|
+
config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT,
|
|
2718
|
+
"requestTimeout"
|
|
2719
|
+
),
|
|
2720
|
+
machineBufferBytes,
|
|
2721
|
+
// Lazy: HTTP observation needs no local engine. Resolution (and its
|
|
2722
|
+
// typed NikaEngineUnavailable refusal) is deferred to caller-owned
|
|
2723
|
+
// source capture in check/run.
|
|
2724
|
+
resolveEngine: () => resolveNikaEngine(config.bin),
|
|
2725
|
+
cwd: config.cwd
|
|
2726
|
+
});
|
|
2727
|
+
} else {
|
|
2728
|
+
const remoteOnly = config;
|
|
2729
|
+
if (remoteOnly.token !== void 0 || remoteOnly.allowInsecureHttp !== void 0 || remoteOnly.requestTimeout !== void 0 || remoteOnly.fetch !== void 0) {
|
|
2730
|
+
throw new NikaConfigurationError("token, allowInsecureHttp, requestTimeout, and fetch require url");
|
|
2731
|
+
}
|
|
2732
|
+
const engine = resolveNikaEngine(config.bin);
|
|
2733
|
+
this.transport = new NativeProcessTransport({
|
|
2734
|
+
engine,
|
|
2735
|
+
cwd: config.cwd,
|
|
2736
|
+
machineBufferBytes
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
this.transportKind = this.transport.kind;
|
|
2740
|
+
}
|
|
2741
|
+
check(workflow, options = {}) {
|
|
2742
|
+
return this.transport.check(workflowName(workflow), options);
|
|
2743
|
+
}
|
|
2744
|
+
/**
|
|
2745
|
+
* `Outputs` is the caller's projection of the engine-emitted outputs map;
|
|
2746
|
+
* the SDK transports outputs without validating their shape.
|
|
2747
|
+
*/
|
|
2748
|
+
async run(workflow, options = {}) {
|
|
2749
|
+
const source = await this.transport.startRun(workflowName(workflow), options);
|
|
2750
|
+
const session = new RunSession(source, this.eventBufferSize);
|
|
2751
|
+
this.sessions.set(session.run, session);
|
|
2752
|
+
return session.run;
|
|
2753
|
+
}
|
|
2754
|
+
/** Reattach this client process to an already-admitted durable HTTP job. */
|
|
2755
|
+
async attachRun(id, options = {}) {
|
|
2756
|
+
const lastEventId = options.lastEventId ?? 0;
|
|
2757
|
+
if (!Number.isSafeInteger(lastEventId) || lastEventId < 0) {
|
|
2758
|
+
throw new RangeError("lastEventId must be a non-negative safe integer");
|
|
2759
|
+
}
|
|
2760
|
+
const source = await this.transport.attachRun(jobId(id), { lastEventId });
|
|
2761
|
+
const session = new RunSession(source, this.eventBufferSize);
|
|
2762
|
+
this.sessions.set(session.run, session);
|
|
2763
|
+
return session.run;
|
|
2764
|
+
}
|
|
2765
|
+
/** List contained workflow names from a resident HTTP authority. */
|
|
2766
|
+
listWorkflows() {
|
|
2767
|
+
return this.transport.listWorkflows();
|
|
2768
|
+
}
|
|
2769
|
+
/** Read path-free metadata for one contained workflow. */
|
|
2770
|
+
workflow(name) {
|
|
2771
|
+
return this.transport.workflow(workflowName(name));
|
|
2772
|
+
}
|
|
2773
|
+
events(run, options = {}) {
|
|
2774
|
+
return this.session(run).events(options);
|
|
2775
|
+
}
|
|
2776
|
+
cancel(run) {
|
|
2777
|
+
return this.session(run).cancel();
|
|
2778
|
+
}
|
|
2779
|
+
/** Read the current durable status without waiting for terminal settlement. */
|
|
2780
|
+
status(run) {
|
|
2781
|
+
return this.session(run).status();
|
|
2782
|
+
}
|
|
2783
|
+
schedule(workflow, options) {
|
|
2784
|
+
scheduleId(options?.id);
|
|
2785
|
+
return this.transport.schedule(workflowName(workflow), options);
|
|
2786
|
+
}
|
|
2787
|
+
scheduleStatus(id) {
|
|
2788
|
+
return this.transport.scheduleStatus(scheduleId(id));
|
|
2789
|
+
}
|
|
2790
|
+
traceVerify(receipt, options = {}) {
|
|
2791
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
2792
|
+
throw new TypeError("traceVerify() accepts an engine-issued NikaReceipt object only");
|
|
2793
|
+
}
|
|
2794
|
+
return this.transport.traceVerify(receipt, options);
|
|
2795
|
+
}
|
|
2796
|
+
session(run) {
|
|
2797
|
+
const session = this.sessions.get(run);
|
|
2798
|
+
if (!session) throw new NikaRunOwnershipError();
|
|
2799
|
+
return session;
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
function checkedUrl(value, allowInsecureHttp) {
|
|
2803
|
+
let url;
|
|
2804
|
+
try {
|
|
2805
|
+
url = new URL(value);
|
|
2806
|
+
} catch {
|
|
2807
|
+
throw new NikaConfigurationError(`Invalid Nika URL: ${value}`);
|
|
2808
|
+
}
|
|
2809
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
2810
|
+
throw new NikaConfigurationError("Nika URL must use https: or http:");
|
|
2811
|
+
}
|
|
2812
|
+
if (url.protocol === "http:") {
|
|
2813
|
+
if (!allowInsecureHttp) {
|
|
2814
|
+
throw new NikaConfigurationError(
|
|
2815
|
+
"Plain HTTP requires allowInsecureHttp: true; prefer HTTPS for remote engines"
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2818
|
+
if (!isLoopbackHost(url.hostname)) {
|
|
2819
|
+
throw new NikaConfigurationError(
|
|
2820
|
+
`Plain HTTP is limited to loopback hosts (localhost, 127.0.0.0/8, [::1]); use HTTPS for ${url.hostname}`
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
2825
|
+
throw new NikaConfigurationError("Nika URL cannot contain credentials, a query, or a fragment");
|
|
2826
|
+
}
|
|
2827
|
+
return url.toString().replace(/\/$/, "");
|
|
2828
|
+
}
|
|
2829
|
+
var LOOPBACK_IPV4 = /^127(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
2830
|
+
var IPV4_MAPPED_IPV6 = /^::ffff:([0-9a-f]{1,4}):[0-9a-f]{1,4}$/;
|
|
2831
|
+
function isLoopbackHost(hostname) {
|
|
2832
|
+
const host = hostname.toLowerCase();
|
|
2833
|
+
if (host === "localhost") return true;
|
|
2834
|
+
if (LOOPBACK_IPV4.test(host)) return true;
|
|
2835
|
+
const literal = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
2836
|
+
if (literal === "::1") return true;
|
|
2837
|
+
const mapped = literal.match(IPV4_MAPPED_IPV6);
|
|
2838
|
+
return mapped !== null && Number.parseInt(mapped[1], 16) >>> 8 === 127;
|
|
2839
|
+
}
|
|
2840
|
+
function checkedToken(value) {
|
|
2841
|
+
const bytes = Buffer.byteLength(value);
|
|
2842
|
+
if (bytes < 32 || bytes > 512 || !/^[\x21-\x7e]+$/.test(value)) {
|
|
2843
|
+
throw new NikaConfigurationError(
|
|
2844
|
+
"Nika bearer token must be 32-512 visible ASCII bytes without whitespace"
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
return value;
|
|
2848
|
+
}
|
|
2849
|
+
function positiveInteger(value, name) {
|
|
2850
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
2851
|
+
throw new NikaConfigurationError(`${name} must be a positive integer`);
|
|
2852
|
+
}
|
|
2853
|
+
return value;
|
|
2854
|
+
}
|
|
2855
|
+
function workflowName(value) {
|
|
2856
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2857
|
+
throw new TypeError("workflow must be a non-empty string");
|
|
2858
|
+
}
|
|
2859
|
+
return value;
|
|
2860
|
+
}
|
|
2861
|
+
function scheduleId(value) {
|
|
2862
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2863
|
+
throw new TypeError("schedule id must be a non-empty string");
|
|
2864
|
+
}
|
|
2865
|
+
return value;
|
|
2866
|
+
}
|
|
2867
|
+
function jobId(value) {
|
|
2868
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2869
|
+
throw new TypeError("job id must be a non-empty string");
|
|
2870
|
+
}
|
|
2871
|
+
return value;
|
|
2872
|
+
}
|
|
2873
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2874
|
+
0 && (module.exports = {
|
|
2875
|
+
Nika,
|
|
2876
|
+
NikaCompatibilityError,
|
|
2877
|
+
NikaConfigurationError,
|
|
2878
|
+
NikaEngineUnavailable,
|
|
2879
|
+
NikaError,
|
|
2880
|
+
NikaEventBufferOverflowError,
|
|
2881
|
+
NikaObservationInterrupted,
|
|
2882
|
+
NikaOperationError,
|
|
2883
|
+
NikaProtocolError,
|
|
2884
|
+
NikaRunOwnershipError,
|
|
2885
|
+
NikaTransportError,
|
|
2886
|
+
isNikaRunSealedEvent,
|
|
2887
|
+
isNikaRunSettledEvent,
|
|
2888
|
+
isNikaTerminalEvent
|
|
2889
|
+
});
|