@oomol-lab/open-flow 0.1.0-beta.75 → 0.1.0-beta.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/browser/{ContentIcon-FCpKiwjp.js → ContentIcon-VZQO8VZU.js} +9 -6
- package/dist/browser/{FlowCanvas-dzRBnlUT.js → FlowCanvas-BNGDMaWr.js} +75 -63
- package/dist/browser/{createResourceDialog-BeYDdUl-.js → createResourceDialog-BmtakfVg.js} +63 -57
- package/dist/browser/{dialog-ZtpsFbl4.js → dialog-BcFBfRB1.js} +126 -32
- package/dist/browser/{exportSvg-DqXw1yf-.js → exportSvg-CG8hEUNk.js} +2 -2
- package/dist/browser/{flowChanges-b_5UobFj.js → flowChanges-BGo_4Ypo.js} +6063 -11285
- package/dist/browser/{flowWorkspace-ItojZbO4.js → flowWorkspace-BFHGev7s.js} +1065 -2570
- package/dist/browser/preview.css +626 -653
- package/dist/browser/preview.js +1907 -9663
- package/dist/browser/theme.css +117 -263
- package/dist/browser/{tooltip-wZJRB9oP.js → tooltip-BAWSUexx.js} +2 -2
- package/dist/browser/{typeScriptSession-CSRwWmZm.js → typeScriptSession-CRoezJPN.js} +1 -1
- package/dist/browser/ui.css +464 -463
- package/dist/browser/ui.js +4 -3
- package/dist/browser/workbench.css +9891 -9897
- package/dist/browser/workbench.d.ts +6 -2
- package/dist/browser/workbench.js +838 -499
- package/dist/browser/{workbenchSelect-M441Lz0d.js → workbenchSelect-CjK-xdm3.js} +2 -2
- package/dist/command/index.js +48 -50
- package/dist/common/authoring.d.ts +5 -5
- package/dist/common/authoringExamples.d.ts +3 -2
- package/dist/common/authoringSchema.d.ts +26 -4
- package/dist/common/connector-proxy.d.ts +1 -1
- package/dist/common/control-api-conformance.js +352 -144
- package/dist/common/control-api-errors.d.ts +12 -0
- package/dist/common/control-api.d.ts +38 -4
- package/dist/common/control-api.js +373 -162
- package/dist/common/control-requests.d.ts +22 -3
- package/dist/common/control-requests.js +268 -28
- package/dist/common/cron-trigger.d.ts +2 -1
- package/dist/common/decision.d.ts +4 -4
- package/dist/common/decision.js +5 -2
- package/dist/common/engine-contract.d.ts +6 -1
- package/dist/common/event-source-definition.d.ts +1 -1
- package/dist/common/event-source-view.d.ts +22 -0
- package/dist/common/event-source.d.ts +1 -1
- package/dist/common/event-source.js +24 -65
- package/dist/common/event-sources.d.ts +0 -21
- package/dist/common/flow-authoring.js +90 -20
- package/dist/common/flow-change-schema.d.ts +9 -1
- package/dist/common/flow-change.d.ts +8 -202
- package/dist/common/flow-change.js +96 -24
- package/dist/common/flow-encoding.js +89 -21
- package/dist/common/flow-input-value.d.ts +1 -1
- package/dist/common/flow-model.d.ts +121 -0
- package/dist/common/flow-notifications.d.ts +1 -1
- package/dist/common/flow-retry.d.ts +2 -4
- package/dist/common/flow-semantics.js +161 -80
- package/dist/common/flow-state.d.ts +123 -0
- package/dist/common/flow-state.js +5852 -0
- package/dist/common/flow-workflowReferences.d.ts +8 -0
- package/dist/common/flowInspection.d.ts +1 -1
- package/dist/common/http-hosts.d.ts +6 -0
- package/dist/common/http-request.d.ts +13 -5
- package/dist/common/http-request.js +23 -5
- package/dist/common/integration-trigger.d.ts +2 -1
- package/dist/common/mcp.d.ts +96 -3
- package/dist/common/mcp.js +355 -142
- package/dist/common/openapi.d.ts +4 -4
- package/dist/common/openapi.js +4 -1
- package/dist/common/poll-trigger.d.ts +2 -1
- package/dist/common/run-events.js +1 -0
- package/dist/common/runtime-contract.d.ts +37 -1
- package/dist/common/runtime-contract.js +717 -72
- package/dist/common/runtime-nodejs-compat.d.ts +44 -0
- package/dist/common/runtime-nodejs-compat.js +5870 -0
- package/dist/common/scheduler-checkpoint.d.ts +1 -1
- package/dist/common/scheduler-node-execution.d.ts +2 -1
- package/dist/common/scheduler.d.ts +2 -2
- package/dist/common/scheduler.js +76 -6
- package/dist/common/state-conformance.d.ts +16 -0
- package/dist/common/tags.d.ts +4 -0
- package/dist/common/trigger-catalog.d.ts +1 -7
- package/dist/common/trigger-config-options.d.ts +1 -1
- package/dist/common/trigger-config.d.ts +1 -1
- package/dist/common/trigger-contract.d.ts +2 -1
- package/dist/common/trigger-localization.d.ts +8 -2
- package/dist/common/trigger-model.d.ts +101 -0
- package/dist/common/webhook-response.d.ts +1 -1
- package/dist/common/webhook-trigger.d.ts +3 -1
- package/dist/common/workflow.d.ts +3 -12
- package/dist/common/workflow.js +25 -23
- package/dist/index.d.ts +52 -14
- package/package.json +9 -1
- package/src/base/common/json.ts +1 -1
- package/src/connector/common/proxy.ts +1 -1
- package/src/control/common/api.ts +90 -16
- package/src/control/common/authoring.ts +6 -1
- package/src/control/common/authoringExamples.ts +8 -0
- package/src/control/common/authoringSchema.ts +26 -13
- package/src/control/common/authoringTools.ts +1 -1
- package/src/control/common/conformance.ts +1 -1
- package/src/control/common/connectorDecoders.ts +1 -1
- package/src/control/common/errors.ts +6 -0
- package/src/control/common/eventSources.ts +2 -29
- package/src/control/common/flowDecoders.ts +1 -0
- package/src/control/common/mcp.ts +16 -0
- package/src/control/common/requests.ts +10 -2
- package/src/control/common/tags.ts +20 -0
- package/src/control/common/triggerCatalog.ts +1 -8
- package/src/decision/common/decision.ts +15 -7
- package/src/event-source/common/definition.ts +1 -1
- package/src/event-source/common/index.ts +1 -1
- package/src/event-source/common/view.ts +20 -0
- package/src/execution/common/checkpoint.ts +1 -1
- package/src/execution/common/events.ts +3 -0
- package/src/execution/common/node-execution.ts +4 -1
- package/src/execution/common/scheduler.ts +3 -1
- package/src/flow/common/change.ts +60 -193
- package/src/flow/common/changeSchema.ts +39 -4
- package/src/flow/common/connectionUsage.ts +1 -1
- package/src/flow/common/errorHandling.ts +1 -1
- package/src/flow/common/inputValue.ts +1 -1
- package/src/flow/common/model.ts +130 -0
- package/src/flow/common/modules.ts +1 -1
- package/src/flow/common/portChanges.ts +1 -1
- package/src/flow/common/retry.ts +2 -4
- package/src/flow/common/semantics.ts +31 -4
- package/src/flow/common/webhookResponse.ts +1 -1
- package/src/flow/common/workflowReferences.ts +31 -0
- package/src/http/common/authBindings.ts +6 -9
- package/src/http/common/hosts.ts +13 -0
- package/src/http/common/httpRequest.ts +30 -15
- package/src/openapi/common/openapi.ts +9 -5
- package/src/{execution → runtime}/common/engineContract.ts +23 -15
- package/src/runtime/common/nodejsCompat.ts +275 -0
- package/src/runtime/common/nodejsGuest.generated.json +47 -0
- package/src/runtime/common/runtime.ts +1254 -0
- package/src/state/common/conformance.ts +86 -0
- package/src/state/common/state.ts +169 -0
- package/src/trigger/common/config.ts +1 -1
- package/src/trigger/common/configOptions.ts +1 -1
- package/src/trigger/common/contract.ts +2 -1
- package/src/trigger/common/cron.ts +2 -1
- package/src/trigger/common/integration.ts +2 -1
- package/src/trigger/common/model.ts +87 -0
- package/src/trigger/common/poll.ts +2 -1
- package/src/trigger/common/webhook.ts +3 -1
- package/src/trigger/providers/fieldDescriptions.ts +1 -1
- package/src/trigger/providers/localization.ts +9 -2
- package/src/trigger/providers/remote.ts +1 -1
- package/src/types/index.ts +55 -15
- package/src/workflow/common/workflow.ts +3 -44
- package/src/execution/common/runtime.ts +0 -563
- /package/src/{control → connector}/common/providerIconSprite.ts +0 -0
|
@@ -22,22 +22,25 @@ function dequal(foo, bar) {
|
|
|
22
22
|
return foo !== foo && bar !== bar;
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
|
-
//#region src/
|
|
26
|
-
|
|
25
|
+
//#region src/runtime/common/engineContract.ts
|
|
26
|
+
/** v10 adds persistent Flow State to the published v9 Node compatibility and fetch contract. */
|
|
27
|
+
var currentEngineContract = "open-flow-engine/v10";
|
|
28
|
+
/** The published v8 Node compatibility contract, retained for existing Publications and Runs. */
|
|
27
29
|
var nodejsEngineContract = "open-flow-engine/v8/nodejs-compat-v1";
|
|
28
|
-
var
|
|
30
|
+
var platformSource = (contract) => `export const engineContract = ${JSON.stringify(contract)}
|
|
31
|
+
export function identity(value) { return value }
|
|
32
|
+
`;
|
|
33
|
+
var v8Contract = {
|
|
29
34
|
workflowTools: true,
|
|
30
35
|
httpRequests: true,
|
|
31
36
|
waitAutoContinue: true,
|
|
32
37
|
webhookResponses: true,
|
|
33
38
|
platformExports: /* @__PURE__ */ new Set(["engineContract", "identity"]),
|
|
34
39
|
platformModule: "open-flow:platform",
|
|
35
|
-
platformSource:
|
|
36
|
-
export function identity(value) { return value }
|
|
37
|
-
`
|
|
40
|
+
platformSource: platformSource("open-flow-engine/v8")
|
|
38
41
|
};
|
|
39
42
|
var nodejsContract = {
|
|
40
|
-
...
|
|
43
|
+
...v8Contract,
|
|
41
44
|
builtinModules: new Set([
|
|
42
45
|
"assert",
|
|
43
46
|
"assert/strict",
|
|
@@ -64,32 +67,94 @@ var nodejsContract = {
|
|
|
64
67
|
"util",
|
|
65
68
|
"zlib"
|
|
66
69
|
].flatMap((name) => [name, `node:${name}`])),
|
|
67
|
-
platformSource:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
platformSource: platformSource(nodejsEngineContract)
|
|
71
|
+
};
|
|
72
|
+
var v9Contract = {
|
|
73
|
+
...nodejsContract,
|
|
74
|
+
codeFetch: true,
|
|
75
|
+
platformSource: platformSource("open-flow-engine/v9")
|
|
76
|
+
};
|
|
77
|
+
var currentContract = {
|
|
78
|
+
...v9Contract,
|
|
79
|
+
flowState: true,
|
|
80
|
+
platformSource: platformSource(currentEngineContract)
|
|
70
81
|
};
|
|
71
82
|
function findEngineContract(contract) {
|
|
72
|
-
if (contract == "open-flow-engine/
|
|
83
|
+
if (contract == "open-flow-engine/v10") return currentContract;
|
|
84
|
+
if (contract == "open-flow-engine/v9") return v9Contract;
|
|
85
|
+
if (contract == "open-flow-engine/v8") return v8Contract;
|
|
73
86
|
if (contract == "open-flow-engine/v8/nodejs-compat-v1") return nodejsContract;
|
|
74
87
|
if (contract == "open-flow-engine/v7" || contract == "open-flow-engine/v7/nodejs-compat-v1") return {
|
|
75
|
-
...contract.endsWith("/nodejs-compat-v1") ? nodejsContract :
|
|
88
|
+
...contract.endsWith("/nodejs-compat-v1") ? nodejsContract : v8Contract,
|
|
76
89
|
workflowTools: false,
|
|
77
90
|
httpRequests: false,
|
|
78
|
-
platformSource:
|
|
91
|
+
platformSource: platformSource(contract)
|
|
79
92
|
};
|
|
80
93
|
if (contract == "open-flow-engine/v6" || contract == "open-flow-engine/v6/nodejs-compat-v1") return {
|
|
81
|
-
...contract == "open-flow-engine/v6" ?
|
|
94
|
+
...contract == "open-flow-engine/v6" ? v8Contract : nodejsContract,
|
|
82
95
|
workflowTools: false,
|
|
83
96
|
httpRequests: false,
|
|
84
97
|
waitAutoContinue: false,
|
|
85
98
|
webhookResponses: false,
|
|
86
|
-
platformSource:
|
|
87
|
-
export function identity(value) { return value }
|
|
88
|
-
`
|
|
99
|
+
platformSource: platformSource(contract)
|
|
89
100
|
};
|
|
90
101
|
}
|
|
91
102
|
//#endregion
|
|
92
|
-
//#region src/
|
|
103
|
+
//#region src/runtime/common/runtime.ts
|
|
104
|
+
/** Serialized into the guest; keep validation and helpers self-contained. */
|
|
105
|
+
function createState(invoke) {
|
|
106
|
+
const invalid = (message) => Object.assign(new TypeError(message), { code: "capability.invalid" });
|
|
107
|
+
const keyOf = (key) => {
|
|
108
|
+
if (typeof key !== "string" || key.length === 0 || new TextEncoder().encode(key).length > 256) throw invalid("State keys require 1 to 256 UTF-8 bytes.");
|
|
109
|
+
return key;
|
|
110
|
+
};
|
|
111
|
+
const valueOf = (value) => {
|
|
112
|
+
if (value === void 0) throw invalid("State values must be JSON.");
|
|
113
|
+
try {
|
|
114
|
+
return snapshotRuntimeResult({ value }).value;
|
|
115
|
+
} catch {
|
|
116
|
+
throw invalid("State values must be JSON.");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const idsOf = (ids) => {
|
|
120
|
+
if (!Array.isArray(ids) || !Array.from(ids).every((id) => typeof id === "string" || typeof id === "number" && Number.isFinite(id))) throw invalid("Identities must be strings or finite numbers.");
|
|
121
|
+
return ids;
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
state: Object.freeze({
|
|
125
|
+
async get(key) {
|
|
126
|
+
return (await invoke("state.get", { key: keyOf(key) })).body.value;
|
|
127
|
+
},
|
|
128
|
+
async set(key, value) {
|
|
129
|
+
await invoke("state.set", {
|
|
130
|
+
key: keyOf(key),
|
|
131
|
+
value: valueOf(value)
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
async insert(key, value) {
|
|
135
|
+
if (value === null) throw invalid("State insert does not accept null.");
|
|
136
|
+
return (await invoke("state.insert", {
|
|
137
|
+
key: keyOf(key),
|
|
138
|
+
value: valueOf(value)
|
|
139
|
+
})).body.inserted;
|
|
140
|
+
},
|
|
141
|
+
async delete(key) {
|
|
142
|
+
await invoke("state.delete", { key: keyOf(key) });
|
|
143
|
+
},
|
|
144
|
+
async list() {
|
|
145
|
+
return (await invoke("state.list", {})).body.keys;
|
|
146
|
+
}
|
|
147
|
+
}),
|
|
148
|
+
dedupe: Object.freeze({
|
|
149
|
+
async insert(ids) {
|
|
150
|
+
return (await invoke("dedupe.insert", { ids: idsOf(ids) })).body.inserted;
|
|
151
|
+
},
|
|
152
|
+
async has(ids) {
|
|
153
|
+
return (await invoke("dedupe.has", { ids: idsOf(ids) })).body.present;
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
};
|
|
157
|
+
}
|
|
93
158
|
/** Snapshot a Task result before JSON transport can discard or coerce invalid values. */
|
|
94
159
|
function snapshotRuntimeResult(value) {
|
|
95
160
|
const parents = /* @__PURE__ */ new Set();
|
|
@@ -105,18 +170,18 @@ function snapshotRuntimeResult(value) {
|
|
|
105
170
|
};
|
|
106
171
|
return value === void 0 ? void 0 : copy(value, 0);
|
|
107
172
|
}
|
|
108
|
-
var nodejsRuntimeConformanceCases = [
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
173
|
+
var nodejsRuntimeConformanceCases = [
|
|
174
|
+
{
|
|
175
|
+
name: "supports Node builtin imports and asynchronous virtual file operations",
|
|
176
|
+
async verify(harness) {
|
|
177
|
+
equal(await harness.invoke({
|
|
178
|
+
capability: async () => ({
|
|
179
|
+
body: null,
|
|
180
|
+
status: 403
|
|
181
|
+
}),
|
|
182
|
+
input: null,
|
|
183
|
+
invocationId: "nodejs-files",
|
|
184
|
+
program: program(harness, `import assert from 'node:assert/strict';
|
|
120
185
|
import { Buffer } from 'node:buffer'; import path from 'node:path'; import fs from 'node:fs/promises';
|
|
121
186
|
export default async () => {
|
|
122
187
|
const file = path.join('/tmp', 'conformance.txt');
|
|
@@ -125,26 +190,24 @@ export default async () => {
|
|
|
125
190
|
assert.equal(text, '你好');
|
|
126
191
|
await fs.unlink(file);
|
|
127
192
|
return { text, bytes: Buffer.byteLength(text) };
|
|
128
|
-
}`)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
program: {
|
|
147
|
-
...program(harness, `import { setTimeout } from 'node:timers/promises';
|
|
193
|
+
}`)
|
|
194
|
+
}), {
|
|
195
|
+
text: "你好",
|
|
196
|
+
bytes: 6
|
|
197
|
+
}, "Node virtual files");
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "preserves abort reasons and cancels Promise timers",
|
|
202
|
+
async verify(harness) {
|
|
203
|
+
equal(await harness.invoke({
|
|
204
|
+
capability: async () => ({
|
|
205
|
+
body: null,
|
|
206
|
+
status: 403
|
|
207
|
+
}),
|
|
208
|
+
input: null,
|
|
209
|
+
invocationId: "nodejs-abort",
|
|
210
|
+
program: program(harness, `import { setTimeout } from 'node:timers/promises';
|
|
148
211
|
export default async () => {
|
|
149
212
|
const controller = new AbortController(); const reason = { message: 'stop' };
|
|
150
213
|
const combined = AbortSignal.any([controller.signal]);
|
|
@@ -154,15 +217,216 @@ export default async () => {
|
|
|
154
217
|
try { await pending; return false; } catch (error) {
|
|
155
218
|
return { sameReason: combined.reason === reason && thrown === reason, name: error.name };
|
|
156
219
|
}
|
|
157
|
-
}`)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
220
|
+
}`)
|
|
221
|
+
}), {
|
|
222
|
+
sameReason: true,
|
|
223
|
+
name: "AbortError"
|
|
224
|
+
}, "Node cancellation");
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
nodejsCase("resolves prefixed and bare builtin names to the same module and shares globals with their modules", `import { Buffer } from 'node:buffer'; import bare from 'buffer'; import processModule from 'node:process';
|
|
228
|
+
import { URL as ImportedURL, URLSearchParams as ImportedParams } from 'node:url'; import { ReadableStream as ImportedStream } from 'node:stream/web';
|
|
229
|
+
export default () => ({ buffer: Buffer === bare.Buffer && Buffer === globalThis.Buffer, process: process === processModule,
|
|
230
|
+
url: URL === ImportedURL && URLSearchParams === ImportedParams, stream: ReadableStream === ImportedStream,
|
|
231
|
+
cwd: process.cwd(), env: Object.keys(process.env) })`, {
|
|
232
|
+
buffer: true,
|
|
233
|
+
process: true,
|
|
234
|
+
url: true,
|
|
235
|
+
stream: true,
|
|
236
|
+
cwd: "/tmp",
|
|
237
|
+
env: []
|
|
238
|
+
}),
|
|
239
|
+
nodejsCase("supports events, string decoding, paths, query strings, URLs and promisify", `import { EventEmitter } from 'node:events'; import { StringDecoder } from 'node:string_decoder'; import { Buffer } from 'node:buffer';
|
|
240
|
+
import path from 'node:path'; import qs from 'node:querystring'; import { promisify } from 'node:util';
|
|
241
|
+
export default async () => {
|
|
242
|
+
const emitter = new EventEmitter(); let count = 0; emitter.once('value', (x) => (count += x)); emitter.emit('value', 7); emitter.emit('value', 9);
|
|
243
|
+
const decoder = new StringDecoder('utf8'); const bytes = Buffer.from('你好');
|
|
244
|
+
const incremental = new TextDecoder(); const partial = incremental.decode(new Uint8Array([0xe4, 0xbd]), { stream: true });
|
|
245
|
+
const url = new URL('../文件?a=1', 'https://example.com/a/b'); url.searchParams.append('b', 'x y');
|
|
246
|
+
return { count, text: decoder.write(bytes.subarray(0, 2)) + decoder.end(bytes.subarray(2)), partial, rest: incremental.decode(new Uint8Array([0xa0])),
|
|
247
|
+
path: path.join('/tmp', 'a', '..', 'b'), query: qs.parse('a=1&a=2').a, url: url.href, canParse: URL.canParse('not a url'),
|
|
248
|
+
value: await promisify((callback) => callback(null, 42))() };
|
|
249
|
+
}`, {
|
|
250
|
+
count: 7,
|
|
251
|
+
text: "你好",
|
|
252
|
+
partial: "",
|
|
253
|
+
rest: "你",
|
|
254
|
+
path: "/tmp/b",
|
|
255
|
+
query: ["1", "2"],
|
|
256
|
+
url: "https://example.com/%E6%96%87%E4%BB%B6?a=1&b=x+y",
|
|
257
|
+
canParse: false,
|
|
258
|
+
value: 42
|
|
259
|
+
}),
|
|
260
|
+
nodejsCase("encodes Buffers and computes hashes, HMACs, gzip and random values", `import { Buffer } from 'node:buffer'; import { createHash, createHmac, randomBytes } from 'node:crypto'; import { gzipSync, gunzipSync, deflateRawSync, inflateRawSync } from 'node:zlib';
|
|
261
|
+
export default () => ({ hex: Buffer.from('你好').toString('hex'), base64: Buffer.from('abc').toString('base64'), latin1: Buffer.from([0xe9]).toString('latin1'),
|
|
262
|
+
utf16: Buffer.from('a', 'utf16le').length, hash: createHash('sha256').update('a').update('bc').digest('hex'),
|
|
263
|
+
md5: createHash('md5').update('abc').digest('base64'), hmac: createHmac('sha256', 'key').update('message').digest('hex'),
|
|
264
|
+
gzip: gunzipSync(gzipSync(Buffer.from('你好'))).toString(), raw: inflateRawSync(deflateRawSync('raw')).toString(),
|
|
265
|
+
random: randomBytes(12).length, uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(crypto.randomUUID()),
|
|
266
|
+
values: crypto.getRandomValues(new Uint8Array(4)).length })`, {
|
|
267
|
+
hex: "e4bda0e5a5bd",
|
|
268
|
+
base64: "YWJj",
|
|
269
|
+
latin1: "é",
|
|
270
|
+
utf16: 2,
|
|
271
|
+
hash: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
|
272
|
+
md5: "kAFQmDzST7DWlj99KOF/cg==",
|
|
273
|
+
hmac: "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a",
|
|
274
|
+
gzip: "你好",
|
|
275
|
+
raw: "raw",
|
|
276
|
+
random: 12,
|
|
277
|
+
uuid: true,
|
|
278
|
+
values: 4
|
|
279
|
+
}),
|
|
280
|
+
nodejsCase("provides an invocation-local filesystem with Node error codes", `import fs from 'node:fs'; import fsp from 'node:fs/promises';
|
|
281
|
+
export default async () => {
|
|
282
|
+
await fsp.mkdir('/tmp/nested', { recursive: true });
|
|
283
|
+
fs.writeFileSync('/tmp/nested/data', '你好');
|
|
284
|
+
const handle = await fsp.open('/tmp/nested/data', 'r'); const data = await handle.readFile('utf8'); await handle.close();
|
|
285
|
+
await fsp.rename('/tmp/nested/data', '/tmp/nested/moved');
|
|
286
|
+
let missing; try { fs.readFileSync('/etc/passwd'); } catch (error) { missing = error.code; }
|
|
287
|
+
return { data, size: fs.statSync('/tmp/nested/moved').size, entries: fs.readdirSync('/tmp/nested'), missing };
|
|
288
|
+
}`, {
|
|
289
|
+
data: "你好",
|
|
290
|
+
size: 6,
|
|
291
|
+
entries: ["moved"],
|
|
292
|
+
missing: "ENOENT"
|
|
293
|
+
}),
|
|
294
|
+
nodejsCase("runs timers, Node streams, pipelines and Web Streams", `import { setTimeout as delay } from 'node:timers/promises'; import { Readable, Writable } from 'node:stream'; import { pipeline } from 'node:stream/promises';
|
|
295
|
+
export default async () => {
|
|
296
|
+
let count = 0; const timer = setInterval(() => { count++; clearInterval(timer) }, 1); const removed = setTimeout(() => { count += 100 }, 1); clearTimeout(removed);
|
|
297
|
+
await delay(10);
|
|
298
|
+
const data = []; for await (const chunk of Readable.from(['a', 'b'])) data.push(chunk);
|
|
299
|
+
let piped = ''; await pipeline(Readable.from(['x', 'y']), new Writable({ write(chunk, _encoding, callback) { piped += chunk.toString(); callback() } }));
|
|
300
|
+
const reader = new ReadableStream({ start(controller) { controller.enqueue('web'); controller.close() } }).getReader();
|
|
301
|
+
return { count, data, piped, web: (await reader.read()).value };
|
|
302
|
+
}`, {
|
|
303
|
+
count: 1,
|
|
304
|
+
data: ["a", "b"],
|
|
305
|
+
piped: "xy",
|
|
306
|
+
web: "web"
|
|
307
|
+
}),
|
|
308
|
+
nodejsCase("supports AbortSignal timeout and pre-aborted signals", `import { setTimeout as delay } from 'node:timers/promises';
|
|
309
|
+
export default async () => { const timeout = AbortSignal.timeout(1); await delay(10); return { timeout: timeout.reason.name, aborted: AbortSignal.abort().aborted } }`, {
|
|
310
|
+
timeout: "TimeoutError",
|
|
311
|
+
aborted: true
|
|
312
|
+
}),
|
|
313
|
+
nodejsCase("encodes and decodes text larger than one builtin transfer and creates many decoders", `export default () => {
|
|
314
|
+
const text = '你😀a'.repeat(400000); const bytes = new TextEncoder().encode(text);
|
|
315
|
+
const decoder = new TextDecoder(); let streamed = '';
|
|
316
|
+
for (let offset = 0; offset < bytes.length; offset += 1000003) streamed += decoder.decode(bytes.subarray(offset, offset + 1000003), { stream: true });
|
|
317
|
+
streamed += decoder.decode();
|
|
318
|
+
let decoded = 0; for (let index = 0; index < 300; index++) decoded += new TextDecoder().decode(new Uint8Array([0x61])).length;
|
|
319
|
+
return { bytes: bytes.length, whole: new TextDecoder().decode(bytes) === text, streamed: streamed === text, decoded };
|
|
320
|
+
}`, {
|
|
321
|
+
bytes: 32e5,
|
|
322
|
+
whole: true,
|
|
323
|
+
streamed: true,
|
|
324
|
+
decoded: 300
|
|
325
|
+
}),
|
|
326
|
+
{
|
|
327
|
+
name: "isolates files, globals and builtin module state between Task invocations",
|
|
328
|
+
async verify(harness) {
|
|
329
|
+
const invoke = async (invocationId, source) => await harness.invoke({
|
|
330
|
+
capability: async () => ({
|
|
331
|
+
body: null,
|
|
332
|
+
status: 403
|
|
333
|
+
}),
|
|
334
|
+
input: null,
|
|
335
|
+
invocationId,
|
|
336
|
+
program: program(harness, source)
|
|
337
|
+
});
|
|
338
|
+
await invoke("nodejs-isolation-write", `import fs from 'node:fs'; import crypto from 'node:crypto';
|
|
339
|
+
export default () => { fs.writeFileSync('/tmp/marker', 'x'); URL.prototype.taskMarker = true; crypto.constants.taskMarker = true; globalThis.taskMarker = true }`);
|
|
340
|
+
equal(await invoke("nodejs-isolation-read", `import fs from 'node:fs'; import crypto from 'node:crypto';
|
|
341
|
+
export default () => ({ file: fs.existsSync('/tmp/marker'), url: 'taskMarker' in URL.prototype, module: 'taskMarker' in crypto.constants, global: 'taskMarker' in globalThis })`), {
|
|
342
|
+
file: false,
|
|
343
|
+
url: false,
|
|
344
|
+
module: false,
|
|
345
|
+
global: false
|
|
346
|
+
}, "Task isolation");
|
|
347
|
+
}
|
|
164
348
|
}
|
|
165
|
-
|
|
349
|
+
];
|
|
350
|
+
/** Reference Artifact Host for the guest chunk protocol; deployments own storage, identity and limits. */
|
|
351
|
+
function artifactHost() {
|
|
352
|
+
const chunkBytes = 524288;
|
|
353
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
354
|
+
const artifacts = /* @__PURE__ */ new Map();
|
|
355
|
+
const kinds = [];
|
|
356
|
+
let nextUpload = 0;
|
|
357
|
+
const capability = async (call) => {
|
|
358
|
+
kinds.push(call.kind);
|
|
359
|
+
const payload = call.payload;
|
|
360
|
+
if (call.kind == "artifact.write") {
|
|
361
|
+
const upload = typeof payload.upload == "string" ? payload.upload : `u${++nextUpload}`;
|
|
362
|
+
uploads.set(upload, [...uploads.get(upload) ?? [], decodeBase64(payload.data)]);
|
|
363
|
+
return {
|
|
364
|
+
body: { upload },
|
|
365
|
+
status: 200
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (call.kind == "artifact.put") {
|
|
369
|
+
const parts = [...typeof payload.upload == "string" ? uploads.get(payload.upload) ?? [] : [], decodeBase64(payload.data)];
|
|
370
|
+
const bytes = new Uint8Array(parts.reduce((size, part) => size + part.length, 0));
|
|
371
|
+
parts.reduce((offset, part) => (bytes.set(part, offset), offset + part.length), 0);
|
|
372
|
+
const id = `a${artifacts.size + 1}`;
|
|
373
|
+
artifacts.set(id, bytes);
|
|
374
|
+
return {
|
|
375
|
+
body: {
|
|
376
|
+
kind: "artifact",
|
|
377
|
+
id,
|
|
378
|
+
name: payload.name,
|
|
379
|
+
size: bytes.length,
|
|
380
|
+
digest: artifactDigest(id),
|
|
381
|
+
...payload.mediaType == null ? {} : { mediaType: payload.mediaType }
|
|
382
|
+
},
|
|
383
|
+
status: 200
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (call.kind == "artifact.read") {
|
|
387
|
+
const bytes = artifacts.get(String(payload.id));
|
|
388
|
+
const offset = Number(payload.offset);
|
|
389
|
+
return {
|
|
390
|
+
body: {
|
|
391
|
+
data: encodeBase64(bytes.subarray(offset, offset + chunkBytes)),
|
|
392
|
+
size: bytes.length
|
|
393
|
+
},
|
|
394
|
+
status: 200
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
throw new Error(`Unexpected Capability ${call.kind}.`);
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
capability,
|
|
401
|
+
digest: artifactDigest,
|
|
402
|
+
kinds: () => kinds
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function decodeBase64(text) {
|
|
406
|
+
return Uint8Array.from(atob(String(text)), (character) => character.charCodeAt(0));
|
|
407
|
+
}
|
|
408
|
+
function encodeBase64(bytes) {
|
|
409
|
+
return btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""));
|
|
410
|
+
}
|
|
411
|
+
function artifactDigest(id) {
|
|
412
|
+
return `sha256:${id.slice(1).padStart(64, "0")}`;
|
|
413
|
+
}
|
|
414
|
+
function nodejsCase(name, source, expected) {
|
|
415
|
+
return {
|
|
416
|
+
name,
|
|
417
|
+
async verify(harness) {
|
|
418
|
+
equal(await harness.invoke({
|
|
419
|
+
capability: async () => ({
|
|
420
|
+
body: null,
|
|
421
|
+
status: 403
|
|
422
|
+
}),
|
|
423
|
+
input: null,
|
|
424
|
+
invocationId: `nodejs-${name}`,
|
|
425
|
+
program: program(harness, source)
|
|
426
|
+
}), expected, name);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
166
430
|
function program(harness, source, modules = {}) {
|
|
167
431
|
return {
|
|
168
432
|
engineContract: currentEngineContract,
|
|
@@ -190,6 +454,59 @@ async function rejects(operation, pattern, message) {
|
|
|
190
454
|
throw new Error(`${message}: operation unexpectedly succeeded.`);
|
|
191
455
|
}
|
|
192
456
|
var runtimeConformanceCases = [
|
|
457
|
+
{
|
|
458
|
+
name: "cancels detached State work when its Task finishes",
|
|
459
|
+
async verify(harness) {
|
|
460
|
+
let aborted = false;
|
|
461
|
+
equal(await harness.invoke({
|
|
462
|
+
input: null,
|
|
463
|
+
invocationId: "state-detached",
|
|
464
|
+
capability: async ({ signal }) => await new Promise((resolve) => {
|
|
465
|
+
signal.addEventListener("abort", () => {
|
|
466
|
+
aborted = true;
|
|
467
|
+
resolve({
|
|
468
|
+
status: 200,
|
|
469
|
+
body: null
|
|
470
|
+
});
|
|
471
|
+
}, { once: true });
|
|
472
|
+
}),
|
|
473
|
+
program: program(harness, `export default async (_, context) => {
|
|
474
|
+
void context.state.set('detached', 1);
|
|
475
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
476
|
+
return null;
|
|
477
|
+
}`)
|
|
478
|
+
}), null, "Detached State Task result");
|
|
479
|
+
equal(aborted, true, "Detached State cancellation");
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: "rejects non-JSON State values and invalid identities before Host dispatch",
|
|
484
|
+
async verify(harness) {
|
|
485
|
+
let calls = 0;
|
|
486
|
+
equal(await harness.invoke({
|
|
487
|
+
input: null,
|
|
488
|
+
invocationId: "state-invalid",
|
|
489
|
+
capability: async () => {
|
|
490
|
+
calls += 1;
|
|
491
|
+
return {
|
|
492
|
+
status: 200,
|
|
493
|
+
body: null
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
program: program(harness, `export default async (_, context) => {
|
|
497
|
+
const cyclic = {}; cyclic.self = cyclic;
|
|
498
|
+
const invalid = [undefined, NaN, Infinity, new Date(), { x: undefined }, cyclic, [undefined], { x: () => 1 }];
|
|
499
|
+
const errors = [];
|
|
500
|
+
for (const value of invalid) { try { await context.state.set('key', value); errors.push('accepted'); } catch (e) { errors.push(e.code); } }
|
|
501
|
+
for (const call of [() => context.state.get(''), () => context.state.get('x'.repeat(257)), () => context.state.insert('x', null), () => context.dedupe.insert([NaN]), () => context.dedupe.has([null]), () => context.dedupe.insert(new Array(1))]) {
|
|
502
|
+
try { await call(); errors.push('accepted'); } catch (e) { errors.push(e.code); }
|
|
503
|
+
}
|
|
504
|
+
return errors;
|
|
505
|
+
}`)
|
|
506
|
+
}), Array.from({ length: 14 }, () => "capability.invalid"), "State boundary validation");
|
|
507
|
+
equal(calls, 0, "State invalid dispatch");
|
|
508
|
+
}
|
|
509
|
+
},
|
|
193
510
|
{
|
|
194
511
|
name: "rejects Task results that JSON transport would discard or coerce",
|
|
195
512
|
async verify(harness) {
|
|
@@ -359,8 +676,8 @@ export default (input) => ({ engineContract, value: identity(double(input.value)
|
|
|
359
676
|
}),
|
|
360
677
|
input: null,
|
|
361
678
|
invocationId: "closed-linker",
|
|
362
|
-
program: program(harness, `import
|
|
363
|
-
export default () =>
|
|
679
|
+
program: program(harness, `import { execSync } from 'node:child_process'
|
|
680
|
+
export default () => execSync('cat /etc/passwd').toString()`)
|
|
364
681
|
}), /invalid|unsupported|cannot import|not part of/i, "Closed linker");
|
|
365
682
|
}
|
|
366
683
|
},
|
|
@@ -369,6 +686,11 @@ export default () => fs.readFileSync('/etc/passwd', 'utf8')`)
|
|
|
369
686
|
async verify(harness) {
|
|
370
687
|
const calls = [];
|
|
371
688
|
equal(await harness.invoke({
|
|
689
|
+
capabilities: [{
|
|
690
|
+
kind: "connector",
|
|
691
|
+
action: "example.read",
|
|
692
|
+
connections: []
|
|
693
|
+
}],
|
|
372
694
|
capability: async (call) => {
|
|
373
695
|
calls.push(call);
|
|
374
696
|
return {
|
|
@@ -381,16 +703,13 @@ export default () => fs.readFileSync('/etc/passwd', 'utf8')`)
|
|
|
381
703
|
},
|
|
382
704
|
input: { issue: 42 },
|
|
383
705
|
invocationId: "async-capability",
|
|
384
|
-
program: program(harness, `export default async (input,
|
|
706
|
+
program: program(harness, `export default async (input, context) => context.actions.example.read(input)`)
|
|
385
707
|
}), {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
status: 200
|
|
708
|
+
accepted: true,
|
|
709
|
+
echoed: {
|
|
710
|
+
action: "example.read",
|
|
711
|
+
input: { issue: 42 }
|
|
712
|
+
}
|
|
394
713
|
}, "Capability result");
|
|
395
714
|
equal(calls.map(({ invocationId, kind, payload }) => ({
|
|
396
715
|
invocationId,
|
|
@@ -398,14 +717,150 @@ export default () => fs.readFileSync('/etc/passwd', 'utf8')`)
|
|
|
398
717
|
payload
|
|
399
718
|
})), [{
|
|
400
719
|
invocationId: "async-capability",
|
|
401
|
-
kind: "
|
|
720
|
+
kind: "connector",
|
|
402
721
|
payload: {
|
|
403
|
-
action: "read",
|
|
722
|
+
action: "example.read",
|
|
404
723
|
input: { issue: 42 }
|
|
405
724
|
}
|
|
406
725
|
}], "Capability request");
|
|
407
726
|
}
|
|
408
727
|
},
|
|
728
|
+
{
|
|
729
|
+
name: "stores and reads Artifacts through bounded chunks",
|
|
730
|
+
async verify(harness) {
|
|
731
|
+
const host = artifactHost();
|
|
732
|
+
const value = await harness.invoke({
|
|
733
|
+
capability: host.capability,
|
|
734
|
+
input: null,
|
|
735
|
+
invocationId: "artifact-roundtrip",
|
|
736
|
+
program: program(harness, `export default async (_input, context) => {
|
|
737
|
+
const large = new Uint8Array(1200 * 1024).map((_, index) => index % 251)
|
|
738
|
+
const text = await context.artifact.put('你好', { name: 'greeting.txt', mediaType: 'text/plain' })
|
|
739
|
+
const binary = await context.artifact.put(large, { name: 'data.bin' })
|
|
740
|
+
const empty = await context.artifact.put(new Uint8Array(0), { name: 'empty' })
|
|
741
|
+
const read = await context.artifact.open(binary)
|
|
742
|
+
return { text, binary, empty, greeting: new TextDecoder().decode(await context.artifact.open(text)),
|
|
743
|
+
same: read.length == large.length && read.every((byte, index) => byte == large[index]), emptyLength: (await context.artifact.open(empty)).length }
|
|
744
|
+
}`)
|
|
745
|
+
});
|
|
746
|
+
const ref = (id, name, size, mediaType) => ({
|
|
747
|
+
kind: "artifact",
|
|
748
|
+
id,
|
|
749
|
+
name,
|
|
750
|
+
size,
|
|
751
|
+
digest: host.digest(id),
|
|
752
|
+
...mediaType == null ? {} : { mediaType }
|
|
753
|
+
});
|
|
754
|
+
equal(value, {
|
|
755
|
+
text: ref("a1", "greeting.txt", 6, "text/plain"),
|
|
756
|
+
binary: ref("a2", "data.bin", 1228800),
|
|
757
|
+
empty: ref("a3", "empty", 0),
|
|
758
|
+
greeting: "你好",
|
|
759
|
+
same: true,
|
|
760
|
+
emptyLength: 0
|
|
761
|
+
}, "Artifact roundtrip");
|
|
762
|
+
equal(host.kinds(), [
|
|
763
|
+
"artifact.put",
|
|
764
|
+
"artifact.write",
|
|
765
|
+
"artifact.write",
|
|
766
|
+
"artifact.put",
|
|
767
|
+
"artifact.put",
|
|
768
|
+
"artifact.read",
|
|
769
|
+
"artifact.read",
|
|
770
|
+
"artifact.read",
|
|
771
|
+
"artifact.read",
|
|
772
|
+
"artifact.read"
|
|
773
|
+
], "Artifact chunks");
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "rejects invalid Artifact arguments before Host dispatch",
|
|
778
|
+
async verify(harness) {
|
|
779
|
+
const host = artifactHost();
|
|
780
|
+
equal(await harness.invoke({
|
|
781
|
+
capability: host.capability,
|
|
782
|
+
input: null,
|
|
783
|
+
invocationId: "artifact-invalid",
|
|
784
|
+
program: program(harness, `export default async (_input, context) => {
|
|
785
|
+
const codes = []
|
|
786
|
+
for (const call of [() => context.artifact.put({}, { name: 'x' }), () => context.artifact.put('x', {}), () => context.artifact.put('x', { name: 'x', mediaType: 1 }), () => context.artifact.open({ id: 'a1' })]) {
|
|
787
|
+
try { await call() } catch (error) { codes.push(error.code) }
|
|
788
|
+
}
|
|
789
|
+
return codes
|
|
790
|
+
}`)
|
|
791
|
+
}), Array.from({ length: 4 }, () => "capability.invalid"), "Artifact validation");
|
|
792
|
+
equal(host.kinds(), [], "Artifact dispatch");
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: "calls declared HTTP hosts through context.fetch",
|
|
797
|
+
async verify(harness) {
|
|
798
|
+
const calls = [];
|
|
799
|
+
equal(await harness.invoke({
|
|
800
|
+
capability: async (call) => {
|
|
801
|
+
calls.push({
|
|
802
|
+
kind: call.kind,
|
|
803
|
+
payload: call.payload
|
|
804
|
+
});
|
|
805
|
+
return {
|
|
806
|
+
status: 200,
|
|
807
|
+
body: {
|
|
808
|
+
status: 201,
|
|
809
|
+
statusText: "Created",
|
|
810
|
+
url: "https://api.example.com/items?q=1",
|
|
811
|
+
headers: [
|
|
812
|
+
["content-type", "application/json"],
|
|
813
|
+
["x-tag", "a"],
|
|
814
|
+
["x-tag", "b"]
|
|
815
|
+
],
|
|
816
|
+
body: btoa("{\"id\":7}")
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
},
|
|
820
|
+
input: null,
|
|
821
|
+
invocationId: "code-fetch",
|
|
822
|
+
program: program(harness, `export default async (_input, context) => {
|
|
823
|
+
const response = await context.fetch(new URL('https://api.example.com/items?q=1'), { method: 'post', headers: [['X-Trace', '1']], body: '{"name":"a"}' })
|
|
824
|
+
const json = await response.json()
|
|
825
|
+
let reused; try { await response.text() } catch (error) { reused = error instanceof TypeError }
|
|
826
|
+
const codes = []
|
|
827
|
+
for (const call of [() => context.fetch('https://api.example.com', { method: 'TRACE' }), () => context.fetch('https://api.example.com', { body: 'x' }),
|
|
828
|
+
() => context.fetch('https://api.example.com', { method: 'POST', body: {} }), () => context.fetch('https://api.example.com', { headers: { 'bad header': 'x' } }),
|
|
829
|
+
() => context.fetch('https://api.example.com', { method: 'PUT', body: new Uint8Array(1024 * 1024 + 1) })]) {
|
|
830
|
+
try { await call() } catch (error) { codes.push(error.code) }
|
|
831
|
+
}
|
|
832
|
+
return { status: response.status, ok: response.ok, statusText: response.statusText, url: response.url, redirected: response.redirected, json, reused,
|
|
833
|
+
tag: response.headers.get('X-Tag'), has: response.headers.has('content-type'), names: [...response.headers.keys()], codes, egress: typeof context.egress }
|
|
834
|
+
}`)
|
|
835
|
+
}), {
|
|
836
|
+
status: 201,
|
|
837
|
+
ok: true,
|
|
838
|
+
statusText: "Created",
|
|
839
|
+
url: "https://api.example.com/items?q=1",
|
|
840
|
+
redirected: false,
|
|
841
|
+
json: { id: 7 },
|
|
842
|
+
reused: true,
|
|
843
|
+
tag: "a, b",
|
|
844
|
+
has: true,
|
|
845
|
+
names: [
|
|
846
|
+
"content-type",
|
|
847
|
+
"x-tag",
|
|
848
|
+
"x-tag"
|
|
849
|
+
],
|
|
850
|
+
codes: Array.from({ length: 5 }, () => "capability.invalid"),
|
|
851
|
+
egress: "undefined"
|
|
852
|
+
}, "Fetch response");
|
|
853
|
+
equal(calls, [{
|
|
854
|
+
kind: "http.fetch",
|
|
855
|
+
payload: {
|
|
856
|
+
url: "https://api.example.com/items?q=1",
|
|
857
|
+
method: "POST",
|
|
858
|
+
headers: [["x-trace", "1"], ["content-type", "text/plain;charset=UTF-8"]],
|
|
859
|
+
body: btoa("{\"name\":\"a\"}")
|
|
860
|
+
}
|
|
861
|
+
}], "Fetch request");
|
|
862
|
+
}
|
|
863
|
+
},
|
|
409
864
|
{
|
|
410
865
|
name: "returns final outputs without exposing intermediate output capability",
|
|
411
866
|
async verify(harness) {
|
|
@@ -555,6 +1010,196 @@ function resolveAction(declarations, payload) {
|
|
|
555
1010
|
...connectionId == null ? {} : { connectionId }
|
|
556
1011
|
};
|
|
557
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Serialized into the sandbox before the Capability helpers that use it; keep it self-contained. Encodes in guest
|
|
1015
|
+
* JavaScript, because Node compatibility `atob`/`btoa` route through bounded Host primitives.
|
|
1016
|
+
*/
|
|
1017
|
+
function createBase64() {
|
|
1018
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1019
|
+
const values = (/* @__PURE__ */ new Uint8Array(128)).fill(255);
|
|
1020
|
+
for (let index = 0; index < 64; index++) values[alphabet.charCodeAt(index)] = index;
|
|
1021
|
+
return Object.freeze({
|
|
1022
|
+
encode(bytes) {
|
|
1023
|
+
const parts = [];
|
|
1024
|
+
for (let start = 0; start < bytes.length; start += 24576) {
|
|
1025
|
+
let text = "";
|
|
1026
|
+
for (let index = start; index < Math.min(start + 24576, bytes.length); index += 3) {
|
|
1027
|
+
const value = bytes[index] << 16 | (bytes[index + 1] ?? 0) << 8 | (bytes[index + 2] ?? 0);
|
|
1028
|
+
text += alphabet[value >> 18] + alphabet[value >> 12 & 63] + (index + 1 < bytes.length ? alphabet[value >> 6 & 63] : "=") + (index + 2 < bytes.length ? alphabet[value & 63] : "=");
|
|
1029
|
+
}
|
|
1030
|
+
parts.push(text);
|
|
1031
|
+
}
|
|
1032
|
+
return parts.join("");
|
|
1033
|
+
},
|
|
1034
|
+
decode(text) {
|
|
1035
|
+
if (typeof text != "string" || text.length % 4 != 0) throw new TypeError("Base64 content is invalid.");
|
|
1036
|
+
const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
|
|
1037
|
+
const bytes = new Uint8Array(text.length / 4 * 3 - padding);
|
|
1038
|
+
for (let index = 0, offset = 0; index < text.length; index += 4) {
|
|
1039
|
+
const digits = [
|
|
1040
|
+
0,
|
|
1041
|
+
1,
|
|
1042
|
+
2,
|
|
1043
|
+
3
|
|
1044
|
+
].map((position) => {
|
|
1045
|
+
const code = text.charCodeAt(index + position);
|
|
1046
|
+
if (code == 61 && index + position >= text.length - padding) return 0;
|
|
1047
|
+
const value = code < 128 ? values[code] : 255;
|
|
1048
|
+
if (value == 255) throw new TypeError("Base64 content is invalid.");
|
|
1049
|
+
return value;
|
|
1050
|
+
});
|
|
1051
|
+
const value = digits[0] << 18 | digits[1] << 12 | digits[2] << 6 | digits[3];
|
|
1052
|
+
if (offset < bytes.length) bytes[offset++] = value >> 16;
|
|
1053
|
+
if (offset < bytes.length) bytes[offset++] = value >> 8 & 255;
|
|
1054
|
+
if (offset < bytes.length) bytes[offset++] = value & 255;
|
|
1055
|
+
}
|
|
1056
|
+
return bytes;
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Serialized into the sandbox; keep it self-contained. Transfers bytes as base64 chunks through
|
|
1062
|
+
* `artifact.write`, `artifact.put` and `artifact.read` so each Capability message stays bounded.
|
|
1063
|
+
*/
|
|
1064
|
+
function createArtifacts(invoke, { decode, encode } = createBase64()) {
|
|
1065
|
+
const chunkBytes = 524288;
|
|
1066
|
+
const invalid = (message) => Object.assign(new TypeError(message), { code: "capability.invalid" });
|
|
1067
|
+
return Object.freeze({
|
|
1068
|
+
async put(data, options) {
|
|
1069
|
+
const settings = options;
|
|
1070
|
+
if (settings == null || typeof settings != "object" || typeof settings.name != "string" || settings.name.length == 0) throw invalid("Artifact name is required.");
|
|
1071
|
+
if (settings.mediaType !== void 0 && typeof settings.mediaType != "string") throw invalid("Artifact mediaType must be a string.");
|
|
1072
|
+
const bytes = typeof data == "string" ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : void 0;
|
|
1073
|
+
if (bytes == null) throw invalid("Artifact data must be a string or Uint8Array.");
|
|
1074
|
+
const tail = bytes.length == 0 ? 0 : Math.floor((bytes.length - 1) / chunkBytes) * chunkBytes;
|
|
1075
|
+
let upload;
|
|
1076
|
+
for (let offset = 0; offset < tail; offset += chunkBytes) upload = (await invoke("artifact.write", {
|
|
1077
|
+
...upload === void 0 ? {} : { upload },
|
|
1078
|
+
data: encode(bytes.subarray(offset, offset + chunkBytes))
|
|
1079
|
+
})).body?.upload;
|
|
1080
|
+
return (await invoke("artifact.put", {
|
|
1081
|
+
...upload === void 0 ? {} : { upload },
|
|
1082
|
+
name: settings.name,
|
|
1083
|
+
...settings.mediaType === void 0 ? {} : { mediaType: settings.mediaType },
|
|
1084
|
+
data: encode(bytes.subarray(tail))
|
|
1085
|
+
})).body;
|
|
1086
|
+
},
|
|
1087
|
+
async open(ref) {
|
|
1088
|
+
const artifact = ref;
|
|
1089
|
+
if (artifact == null || typeof artifact != "object" || artifact.kind != "artifact" || typeof artifact.id != "string") throw invalid("An ArtifactRef is required.");
|
|
1090
|
+
const parts = [];
|
|
1091
|
+
let offset = 0;
|
|
1092
|
+
let size = 0;
|
|
1093
|
+
do {
|
|
1094
|
+
const body = (await invoke("artifact.read", {
|
|
1095
|
+
id: artifact.id,
|
|
1096
|
+
offset
|
|
1097
|
+
})).body;
|
|
1098
|
+
const part = decode(body?.data);
|
|
1099
|
+
if (typeof body?.size != "number" || !Number.isSafeInteger(body.size) || part.length == 0 && offset < body.size) throw new TypeError("Artifact content is invalid.");
|
|
1100
|
+
size = body.size;
|
|
1101
|
+
parts.push(part);
|
|
1102
|
+
offset += part.length;
|
|
1103
|
+
} while (offset < size);
|
|
1104
|
+
const bytes = new Uint8Array(offset);
|
|
1105
|
+
let position = 0;
|
|
1106
|
+
for (const part of parts) {
|
|
1107
|
+
bytes.set(part, position);
|
|
1108
|
+
position += part.length;
|
|
1109
|
+
}
|
|
1110
|
+
return bytes;
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Serialized into the sandbox; keep it self-contained. `context.fetch` sends one `http.fetch` Capability call per request.
|
|
1116
|
+
* The Host authorizes the URL against the node's `http` Capability, does not follow redirects and adds no credentials.
|
|
1117
|
+
* Request and response bodies are limited to 1 MiB; the response is a read-only subset of the Fetch `Response`.
|
|
1118
|
+
*/
|
|
1119
|
+
function createFetch(invoke, { decode, encode } = createBase64()) {
|
|
1120
|
+
const maximumBytes = 1048576;
|
|
1121
|
+
const methods = /* @__PURE__ */ new Set([
|
|
1122
|
+
"GET",
|
|
1123
|
+
"HEAD",
|
|
1124
|
+
"POST",
|
|
1125
|
+
"PUT",
|
|
1126
|
+
"PATCH",
|
|
1127
|
+
"DELETE",
|
|
1128
|
+
"OPTIONS"
|
|
1129
|
+
]);
|
|
1130
|
+
const invalid = (message) => Object.assign(new TypeError(message), { code: "capability.invalid" });
|
|
1131
|
+
const headerList = (value) => {
|
|
1132
|
+
if (value == null) return [];
|
|
1133
|
+
const pairs = typeof value[Symbol.iterator] == "function" ? Array.from(value, (pair) => {
|
|
1134
|
+
if (!Array.isArray(pair) || pair.length != 2) throw invalid("Headers must be name/value pairs.");
|
|
1135
|
+
return pair;
|
|
1136
|
+
}) : typeof value == "object" ? Object.entries(value) : void 0;
|
|
1137
|
+
if (pairs == null) throw invalid("Headers must be an object or name/value pairs.");
|
|
1138
|
+
return pairs.map(([name, text]) => {
|
|
1139
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(String(name))) throw invalid(`Invalid header name "${String(name)}".`);
|
|
1140
|
+
return [String(name).toLowerCase(), String(text)];
|
|
1141
|
+
});
|
|
1142
|
+
};
|
|
1143
|
+
return async function fetch(input, init = {}) {
|
|
1144
|
+
const options = init ?? {};
|
|
1145
|
+
if (typeof options != "object") throw invalid("Fetch options must be an object.");
|
|
1146
|
+
const url = String(input instanceof Object && "href" in input ? input.href : input);
|
|
1147
|
+
const method = String(options.method ?? "GET").toUpperCase();
|
|
1148
|
+
if (!methods.has(method)) throw invalid(`Unsupported HTTP method "${method}".`);
|
|
1149
|
+
const headers = headerList(options.headers);
|
|
1150
|
+
const body = options.body == null ? void 0 : typeof options.body == "string" ? new TextEncoder().encode(options.body) : options.body instanceof Uint8Array ? options.body : options.body instanceof ArrayBuffer ? new Uint8Array(options.body) : void 0;
|
|
1151
|
+
if (options.body != null && body == null) throw invalid("Fetch body must be a string, Uint8Array or ArrayBuffer.");
|
|
1152
|
+
if (body != null && (method == "GET" || method == "HEAD")) throw invalid(`A ${method} request cannot have a body.`);
|
|
1153
|
+
if (body != null && body.length > maximumBytes) throw invalid("Fetch request body exceeds 1 MiB.");
|
|
1154
|
+
if (typeof options.body == "string" && !headers.some(([name]) => name == "content-type")) headers.push(["content-type", "text/plain;charset=UTF-8"]);
|
|
1155
|
+
const signal = options.signal;
|
|
1156
|
+
if (signal?.aborted) throw signal.reason;
|
|
1157
|
+
const request = invoke("http.fetch", {
|
|
1158
|
+
url,
|
|
1159
|
+
method,
|
|
1160
|
+
headers,
|
|
1161
|
+
...body == null ? {} : { body: encode(body) }
|
|
1162
|
+
});
|
|
1163
|
+
const result = (signal?.addEventListener == null ? await request : await Promise.race([request, new Promise((_, reject) => signal.addEventListener("abort", () => reject(signal.reason), { once: true }))])).body;
|
|
1164
|
+
if (result == null || typeof result.status != "number" || typeof result.url != "string" || typeof result.body != "string" || !Array.isArray(result.headers) || result.headers.some((pair) => !Array.isArray(pair) || typeof pair[0] != "string" || typeof pair[1] != "string")) throw new TypeError("The HTTP response is invalid.");
|
|
1165
|
+
const status = result.status;
|
|
1166
|
+
const entries = result.headers;
|
|
1167
|
+
const content = decode(result.body);
|
|
1168
|
+
let used = false;
|
|
1169
|
+
const consume = () => {
|
|
1170
|
+
if (used) throw new TypeError("Response body has already been used.");
|
|
1171
|
+
used = true;
|
|
1172
|
+
return content.slice();
|
|
1173
|
+
};
|
|
1174
|
+
const responseHeaders = Object.freeze({
|
|
1175
|
+
get: (name) => {
|
|
1176
|
+
const values = entries.filter(([key]) => key == String(name).toLowerCase()).map(([, value]) => value);
|
|
1177
|
+
return values.length == 0 ? null : values.join(", ");
|
|
1178
|
+
},
|
|
1179
|
+
has: (name) => entries.some(([key]) => key == String(name).toLowerCase()),
|
|
1180
|
+
entries: () => entries.map(([name, value]) => [name, value])[Symbol.iterator](),
|
|
1181
|
+
keys: () => entries.map(([name]) => name)[Symbol.iterator](),
|
|
1182
|
+
values: () => entries.map(([, value]) => value)[Symbol.iterator](),
|
|
1183
|
+
forEach: (callback) => entries.forEach(([name, value]) => callback(value, name)),
|
|
1184
|
+
[Symbol.iterator]: () => entries.map(([name, value]) => [name, value])[Symbol.iterator]()
|
|
1185
|
+
});
|
|
1186
|
+
return Object.freeze({
|
|
1187
|
+
status,
|
|
1188
|
+
statusText: typeof result.statusText == "string" ? result.statusText : "",
|
|
1189
|
+
ok: status >= 200 && status < 300,
|
|
1190
|
+
redirected: false,
|
|
1191
|
+
url: result.url,
|
|
1192
|
+
headers: responseHeaders,
|
|
1193
|
+
get bodyUsed() {
|
|
1194
|
+
return used;
|
|
1195
|
+
},
|
|
1196
|
+
bytes: async () => consume(),
|
|
1197
|
+
arrayBuffer: async () => consume().buffer,
|
|
1198
|
+
text: async () => new TextDecoder().decode(consume()),
|
|
1199
|
+
json: async () => JSON.parse(new TextDecoder().decode(consume()))
|
|
1200
|
+
});
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
558
1203
|
/** Serialized into the sandbox alongside the strict JSON snapshot helper. */
|
|
559
1204
|
function createWorkflows(invoke) {
|
|
560
1205
|
return Object.freeze({ async call(name, input = {}) {
|
|
@@ -566,4 +1211,4 @@ function createWorkflows(invoke) {
|
|
|
566
1211
|
} });
|
|
567
1212
|
}
|
|
568
1213
|
//#endregion
|
|
569
|
-
export { createActions, createWorkflows, currentEngineContract, findEngineContract, nodejsEngineContract, nodejsRuntimeConformanceCases, resolveAction, runtimeConformanceCases, snapshotRuntimeResult };
|
|
1214
|
+
export { createActions, createArtifacts, createBase64, createFetch, createState, createWorkflows, currentEngineContract, findEngineContract, nodejsEngineContract, nodejsRuntimeConformanceCases, resolveAction, runtimeConformanceCases, snapshotRuntimeResult };
|