knitting 0.1.56 → 0.1.61
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/BROWSER.md +134 -0
- package/README.md +347 -46
- package/knitting.browser.d.ts +6 -0
- package/knitting.browser.js +1 -0
- package/knitting.d.ts +3 -2
- package/knitting.js +3 -3
- package/map.md +21 -9
- package/package.json +17 -2
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-compiled-worker.ts +276 -0
- package/scripts/compiled-worker/porffor.d.ts +5 -0
- package/scripts/compiled-worker/runtime.ts +529 -0
- package/scripts/compiled-worker/task-shim.ts +32 -0
- package/src/api.d.ts +24 -1
- package/src/api.js +311 -27
- package/src/common/module-url.d.ts +1 -0
- package/src/common/module-url.js +30 -2
- package/src/common/node-compat.d.ts +2 -0
- package/src/common/path-canonical.js +20 -6
- package/src/common/runtime.d.ts +3 -1
- package/src/common/runtime.js +25 -4
- package/src/common/task-source.d.ts +2 -0
- package/src/common/task-source.js +17 -1
- package/src/common/worker-runtime.js +16 -7
- package/src/connections/buffer-reference-native.js +25 -1
- package/src/connections/buffer-reference.d.ts +23 -2
- package/src/connections/buffer-reference.js +143 -29
- package/src/connections/bun.d.ts +1 -1
- package/src/connections/bun.js +25 -14
- package/src/connections/deno.d.ts +3 -1
- package/src/connections/deno.js +35 -14
- package/src/connections/external-array-buffer.d.ts +2 -0
- package/src/connections/external-array-buffer.js +33 -0
- package/src/connections/node-addons.d.ts +11 -0
- package/src/connections/node-addons.js +49 -1
- package/src/connections/node-ffi-api.d.ts +19 -0
- package/src/connections/node-ffi-api.js +28 -0
- package/src/connections/node-ffi.d.ts +27 -0
- package/src/connections/node-ffi.js +262 -0
- package/src/connections/node.d.ts +1 -1
- package/src/connections/node.js +20 -20
- package/src/connections/package-assets.js +25 -15
- package/src/connections/posix.d.ts +42 -0
- package/src/connections/posix.js +101 -0
- package/src/connections/process-shared-buffer.d.ts +1 -0
- package/src/connections/process-shared-buffer.js +11 -45
- package/src/connections/types.d.ts +4 -0
- package/src/connections/windows.d.ts +19 -10
- package/src/connections/windows.js +87 -66
- package/src/error.d.ts +7 -6
- package/src/error.js +8 -7
- package/src/memory/lock.d.ts +112 -77
- package/src/memory/lock.js +314 -87
- package/src/memory/payloadCodec.d.ts +11 -2
- package/src/memory/payloadCodec.js +129 -37
- package/src/memory/regionRegistry.d.ts +1 -3
- package/src/memory/regionRegistry.js +16 -14
- package/src/permission/compatibility.d.ts +28 -0
- package/src/permission/compatibility.js +239 -0
- package/src/permission/index.d.ts +2 -0
- package/src/permission/index.js +1 -0
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +59 -15
- package/src/runtime/compiled-artifact.d.ts +21 -0
- package/src/runtime/compiled-artifact.js +260 -0
- package/src/runtime/compiled-builder.d.ts +8 -0
- package/src/runtime/compiled-builder.js +42 -0
- package/src/runtime/compiled-worker.d.ts +27 -0
- package/src/runtime/compiled-worker.js +562 -0
- package/src/runtime/dispatcher.js +9 -0
- package/src/runtime/inline-executor.js +27 -15
- package/src/runtime/pool.d.ts +121 -5
- package/src/runtime/pool.js +263 -49
- package/src/runtime/process-worker.d.ts +3 -13
- package/src/runtime/process-worker.js +97 -101
- package/src/runtime/tx-queue.d.ts +18 -2
- package/src/runtime/tx-queue.js +56 -16
- package/src/runtime/worker-common.d.ts +13 -0
- package/src/runtime/worker-common.js +102 -0
- package/src/types.d.ts +89 -9
- package/src/worker/composable-runners.js +3 -2
- package/src/worker/loop.d.ts +1 -0
- package/src/worker/loop.js +52 -17
- package/src/worker/rx-queue.d.ts +11 -1
- package/src/worker/rx-queue.js +4 -1
- package/src/worker/task-loader.d.ts +5 -4
- package/src/worker/task-loader.js +13 -7
- package/src/worker/timers.d.ts +8 -1
- package/src/worker/timers.js +28 -23
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
2
|
+
const hasExplicit = (value, key) => value !== undefined && hasOwn(value, key);
|
|
3
|
+
const hasNonEmptyArray = (value, key) => Array.isArray(value?.[key]) && value[key].length > 0;
|
|
4
|
+
const explicitProtocolObject = (permission) => permission != null && typeof permission === "object"
|
|
5
|
+
? permission
|
|
6
|
+
: undefined;
|
|
7
|
+
const explicitNestedObject = (input, key) => {
|
|
8
|
+
const value = input?.[key];
|
|
9
|
+
return value != null && typeof value === "object"
|
|
10
|
+
? value
|
|
11
|
+
: undefined;
|
|
12
|
+
};
|
|
13
|
+
const targetLabel = (target) => {
|
|
14
|
+
if (target.runtime !== "node") {
|
|
15
|
+
return `${target.runtime[0].toUpperCase()}${target.runtime.slice(1)}`;
|
|
16
|
+
}
|
|
17
|
+
return target.nodeMajor === undefined
|
|
18
|
+
? "Node (version unknown)"
|
|
19
|
+
: `Node ${target.nodeMajor}`;
|
|
20
|
+
};
|
|
21
|
+
const nodeTargetUsesFfiTransport = (target) => target.runtime === "node" &&
|
|
22
|
+
target.nodeMajor !== undefined &&
|
|
23
|
+
target.nodeMajor >= 26 &&
|
|
24
|
+
target.nodeMajor % 2 === 0;
|
|
25
|
+
const permissionCheck = (permission, level, reason) => ({
|
|
26
|
+
permission,
|
|
27
|
+
level,
|
|
28
|
+
reason,
|
|
29
|
+
});
|
|
30
|
+
const classifyRestriction = ({ target, permission, scoped, processNativeTransport = false, }) => {
|
|
31
|
+
const label = targetLabel(target);
|
|
32
|
+
const exact = (reason) => permissionCheck(permission, "exact", reason);
|
|
33
|
+
const coarse = (reason) => permissionCheck(permission, "coarse", reason);
|
|
34
|
+
const unsupported = (reason) => permissionCheck(permission, "unsupported", reason);
|
|
35
|
+
if (target.runtime === "bun") {
|
|
36
|
+
return unsupported("Bun process workers do not expose a matching runtime permission control.");
|
|
37
|
+
}
|
|
38
|
+
if (target.runtime === "deno") {
|
|
39
|
+
if (permission === "workers") {
|
|
40
|
+
return unsupported("Knitting does not currently configure a Deno worker-spawn restriction.");
|
|
41
|
+
}
|
|
42
|
+
if (permission === "wasi") {
|
|
43
|
+
return unsupported("Deno does not expose a matching Knitting WASI process permission.");
|
|
44
|
+
}
|
|
45
|
+
if (processNativeTransport &&
|
|
46
|
+
(permission === "ffi" || permission === "denyFfi")) {
|
|
47
|
+
return unsupported("Deno process workers require --allow-ffi for Knitting's shared-memory transport.");
|
|
48
|
+
}
|
|
49
|
+
return exact("Deno can represent this restriction with scoped runtime flags.");
|
|
50
|
+
}
|
|
51
|
+
if (permission === "read" || permission === "write") {
|
|
52
|
+
return exact(`${label} supports scoped filesystem allow rules.`);
|
|
53
|
+
}
|
|
54
|
+
if (permission === "run") {
|
|
55
|
+
return scoped
|
|
56
|
+
? coarse(`${label} can only allow or deny all child processes, not an executable allow-list.`)
|
|
57
|
+
: exact(`${label} can deny all child processes.`);
|
|
58
|
+
}
|
|
59
|
+
if (permission === "workers") {
|
|
60
|
+
return exact(`${label} can deny worker creation.`);
|
|
61
|
+
}
|
|
62
|
+
if (permission === "wasi") {
|
|
63
|
+
return exact(`${label} can deny WASI.`);
|
|
64
|
+
}
|
|
65
|
+
if (permission === "net") {
|
|
66
|
+
if (target.nodeMajor === undefined) {
|
|
67
|
+
return unsupported("The Node version is unknown, so Knitting cannot prove that network denial is available.");
|
|
68
|
+
}
|
|
69
|
+
if (target.nodeMajor < 25) {
|
|
70
|
+
return unsupported(`${label} has no network permission control.`);
|
|
71
|
+
}
|
|
72
|
+
return scoped
|
|
73
|
+
? coarse(`${label} --allow-net is all-or-nothing and cannot enforce a host allow-list.`)
|
|
74
|
+
: exact(`${label} denies network access when --allow-net is omitted.`);
|
|
75
|
+
}
|
|
76
|
+
if (permission === "node.allowAddons") {
|
|
77
|
+
if (nodeTargetUsesFfiTransport(target)) {
|
|
78
|
+
return exact(`${label} uses node:ffi rather than addons for Knitting's process transport.`);
|
|
79
|
+
}
|
|
80
|
+
return unsupported(`${label} process workers require addon permission for Knitting's shared-memory transport.`);
|
|
81
|
+
}
|
|
82
|
+
if (permission === "node.allowFfi") {
|
|
83
|
+
if (target.nodeMajor !== undefined &&
|
|
84
|
+
!nodeTargetUsesFfiTransport(target)) {
|
|
85
|
+
return exact(`${label} does not use node:ffi for Knitting's process transport.`);
|
|
86
|
+
}
|
|
87
|
+
return unsupported(`${label} process workers require node:ffi permission for Knitting's shared-memory transport.`);
|
|
88
|
+
}
|
|
89
|
+
if (processNativeTransport &&
|
|
90
|
+
(permission === "ffi" || permission === "denyFfi")) {
|
|
91
|
+
return unsupported(`${label} process workers require native-code permission for Knitting's shared-memory transport.`);
|
|
92
|
+
}
|
|
93
|
+
const unsupportedReasons = {
|
|
94
|
+
denyRead: `${label} has no filesystem deny-list control.`,
|
|
95
|
+
denyWrite: `${label} has no filesystem deny-list control.`,
|
|
96
|
+
denyNet: `${label} has no network deny-list control.`,
|
|
97
|
+
allowImport: `${label} has no import-host allow-list control.`,
|
|
98
|
+
env: `${label} has no environment-variable permission control.`,
|
|
99
|
+
denyEnv: `${label} has no environment-variable deny-list control.`,
|
|
100
|
+
denyRun: `${label} has no child-process deny-list control.`,
|
|
101
|
+
sys: `${label} has no system-information permission control.`,
|
|
102
|
+
denySys: `${label} has no system-information deny-list control.`,
|
|
103
|
+
};
|
|
104
|
+
return unsupported(unsupportedReasons[permission] ??
|
|
105
|
+
`${label} cannot represent this restriction.`);
|
|
106
|
+
};
|
|
107
|
+
export const classifyProcessPermissionCompatibility = ({ permission, resolved, target, }) => {
|
|
108
|
+
if (resolved?.enabled !== true ||
|
|
109
|
+
resolved.unsafe === true ||
|
|
110
|
+
permission === "unsafe" ||
|
|
111
|
+
permission === "off") {
|
|
112
|
+
return { target, checks: [] };
|
|
113
|
+
}
|
|
114
|
+
const input = explicitProtocolObject(permission);
|
|
115
|
+
const envInput = explicitNestedObject(input, "env");
|
|
116
|
+
const nodeInput = explicitNestedObject(input, "node");
|
|
117
|
+
const denoInput = explicitNestedObject(input, "deno");
|
|
118
|
+
const checks = [];
|
|
119
|
+
const add = (permissionName, explicit, options = {}) => {
|
|
120
|
+
checks.push({
|
|
121
|
+
...classifyRestriction({
|
|
122
|
+
target,
|
|
123
|
+
permission: permissionName,
|
|
124
|
+
...options,
|
|
125
|
+
}),
|
|
126
|
+
explicit,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
if (!resolved.readAll) {
|
|
130
|
+
add("read", hasExplicit(input, "read"), {
|
|
131
|
+
scoped: resolved.read.length > 0,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (!resolved.writeAll) {
|
|
135
|
+
add("write", hasExplicit(input, "write"), {
|
|
136
|
+
scoped: resolved.write.length > 0,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (resolved.denyRead.length > 0) {
|
|
140
|
+
add("denyRead", hasNonEmptyArray(input, "denyRead"));
|
|
141
|
+
}
|
|
142
|
+
if (resolved.denyWrite.length > 0) {
|
|
143
|
+
add("denyWrite", hasNonEmptyArray(input, "denyWrite"));
|
|
144
|
+
}
|
|
145
|
+
if (!resolved.netAll) {
|
|
146
|
+
add("net", hasExplicit(input, "net"), {
|
|
147
|
+
scoped: resolved.net.length > 0,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (resolved.denyNet.length > 0) {
|
|
151
|
+
add("denyNet", hasNonEmptyArray(input, "denyNet"));
|
|
152
|
+
}
|
|
153
|
+
if (!resolved.allowImportAll) {
|
|
154
|
+
add("allowImport", hasExplicit(input, "allowImport"), { scoped: resolved.allowImport.length > 0 });
|
|
155
|
+
}
|
|
156
|
+
if (!resolved.env.allowAll) {
|
|
157
|
+
add("env", hasExplicit(envInput, "allow"), {
|
|
158
|
+
scoped: resolved.env.allow.length > 0,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (resolved.env.deny.length > 0) {
|
|
162
|
+
add("denyEnv", hasNonEmptyArray(envInput, "deny"));
|
|
163
|
+
}
|
|
164
|
+
if (!resolved.runAll) {
|
|
165
|
+
const explicitRun = hasExplicit(input, "run") ||
|
|
166
|
+
(target.runtime === "node" &&
|
|
167
|
+
hasExplicit(nodeInput, "allowChildProcess")) ||
|
|
168
|
+
(target.runtime === "deno" &&
|
|
169
|
+
hasExplicit(denoInput, "allowRun"));
|
|
170
|
+
add("run", explicitRun, { scoped: resolved.run.length > 0 });
|
|
171
|
+
}
|
|
172
|
+
if (resolved.denyRun.length > 0) {
|
|
173
|
+
add("denyRun", hasNonEmptyArray(input, "denyRun"));
|
|
174
|
+
}
|
|
175
|
+
if (!resolved.workers) {
|
|
176
|
+
const explicitWorkers = hasExplicit(input, "workers") ||
|
|
177
|
+
(target.runtime === "node" &&
|
|
178
|
+
hasExplicit(nodeInput, "allowWorker"));
|
|
179
|
+
add("workers", explicitWorkers);
|
|
180
|
+
}
|
|
181
|
+
if (!resolved.ffiAll) {
|
|
182
|
+
add("ffi", hasExplicit(input, "ffi"), {
|
|
183
|
+
scoped: resolved.ffi.length > 0,
|
|
184
|
+
processNativeTransport: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (resolved.denyFfi.length > 0) {
|
|
188
|
+
add("denyFfi", hasNonEmptyArray(input, "denyFfi"), {
|
|
189
|
+
processNativeTransport: !resolved.ffiAll,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (!resolved.sysAll) {
|
|
193
|
+
add("sys", hasExplicit(input, "sys"), {
|
|
194
|
+
scoped: resolved.sys.length > 0,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (resolved.denySys.length > 0) {
|
|
198
|
+
add("denySys", hasNonEmptyArray(input, "denySys"));
|
|
199
|
+
}
|
|
200
|
+
if (!resolved.wasi) {
|
|
201
|
+
const explicitWasi = hasExplicit(input, "wasi") ||
|
|
202
|
+
(target.runtime === "node" &&
|
|
203
|
+
hasExplicit(nodeInput, "allowWasi"));
|
|
204
|
+
add("wasi", explicitWasi);
|
|
205
|
+
}
|
|
206
|
+
if (target.runtime === "node" && nodeInput !== undefined) {
|
|
207
|
+
if (hasOwn(nodeInput, "allowAddons") && nodeInput.allowAddons === false) {
|
|
208
|
+
add("node.allowAddons", true);
|
|
209
|
+
}
|
|
210
|
+
if (hasOwn(nodeInput, "allowFfi") && nodeInput.allowFfi === false) {
|
|
211
|
+
add("node.allowFfi", true);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { target, checks };
|
|
215
|
+
};
|
|
216
|
+
const warningKeys = new Set();
|
|
217
|
+
export const enforceProcessPermissionCompatibility = (report) => {
|
|
218
|
+
const incompatible = report.checks.filter((check) => check.level !== "exact");
|
|
219
|
+
const explicit = incompatible.filter((check) => check.explicit);
|
|
220
|
+
const label = targetLabel(report.target);
|
|
221
|
+
if (explicit.length > 0) {
|
|
222
|
+
const details = explicit
|
|
223
|
+
.map((check) => `permission.${check.permission} is ${check.level}: ${check.reason}`)
|
|
224
|
+
.join(" ");
|
|
225
|
+
throw new Error(`knitting: ${label} process workers cannot enforce the requested permission policy. ` +
|
|
226
|
+
`${details} Choose a runtime that can represent the restriction, add an OS sandbox, ` +
|
|
227
|
+
`or remove the restriction only if broader access is acceptable.`);
|
|
228
|
+
}
|
|
229
|
+
if (incompatible.length === 0)
|
|
230
|
+
return;
|
|
231
|
+
const names = [...new Set(incompatible.map((check) => check.permission))];
|
|
232
|
+
const warningKey = label;
|
|
233
|
+
if (warningKeys.has(warningKey))
|
|
234
|
+
return;
|
|
235
|
+
warningKeys.add(warningKey);
|
|
236
|
+
console.warn(`knitting: ${label} process workers cannot enforce every implicit strict ` +
|
|
237
|
+
`permission default (${names.join(", ")}). Explicit unsupported ` +
|
|
238
|
+
`restrictions fail closed; use an OS sandbox for hostile task code.`);
|
|
239
|
+
};
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
export { classifyProcessPermissionCompatibility, enforceProcessPermissionCompatibility, } from "./compatibility.js";
|
|
1
2
|
export { resolvePermissionProtocol, toRuntimePermissionFlags, } from "./protocol.js";
|
|
3
|
+
export type { PermissionCompatibilityCheck, PermissionCompatibilityLevel, ProcessPermissionCompatibilityReport, ProcessPermissionRuntime, ProcessPermissionTarget, } from "./compatibility.js";
|
|
2
4
|
export type { PermissionPath, PermissionMode, PermissionLegacyMode, SysApiName, NodePermissionSettings, DenoPermissionSettings, PermissionEnvironment, PermissionProtocol, PermissionProtocolInput, ResolvedPermissionProtocol, } from "./protocol.js";
|
package/src/permission/index.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isAbsolute as pathIsAbsolute, relative as pathRelative, resolve as pathResolve, } from "node:path";
|
|
3
|
-
import { fileURLToPath as fileURLToPathCompat } from "node:url";
|
|
4
|
-
import { RUNTIME } from "../common/runtime.js";
|
|
1
|
+
import { IS_BROWSER, RUNTIME } from "../common/runtime.js";
|
|
5
2
|
import { toCanonicalPath as toSharedCanonicalPath } from "../common/path-canonical.js";
|
|
6
|
-
import { getNodeProcess } from "../common/node-compat.js";
|
|
3
|
+
import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
|
|
4
|
+
const requireNodeModule = (specifier) => {
|
|
5
|
+
const module = getNodeBuiltinModule(specifier);
|
|
6
|
+
if (module === undefined) {
|
|
7
|
+
throw new Error(`${specifier} is not available in this runtime`);
|
|
8
|
+
}
|
|
9
|
+
return module;
|
|
10
|
+
};
|
|
11
|
+
const nodeFs = () => requireNodeModule("node:fs");
|
|
12
|
+
const nodePath = () => requireNodeModule("node:path");
|
|
13
|
+
const nodeUrl = () => requireNodeModule("node:url");
|
|
14
|
+
const existsSyncCompat = (path) => nodeFs().existsSync(path);
|
|
15
|
+
const pathIsAbsolute = (path) => nodePath().isAbsolute(path);
|
|
16
|
+
const pathRelative = (from, to) => nodePath().relative(from, to);
|
|
17
|
+
const pathResolve = (...segments) => nodePath().resolve(...segments);
|
|
18
|
+
const fileURLToPathCompat = (url) => nodeUrl().fileURLToPath(url);
|
|
7
19
|
const DEFAULT_ENV_FILE = ".env";
|
|
8
20
|
const DEFAULT_DENO_LOCK_FILE = "deno.lock";
|
|
9
21
|
const NODE_MODULES_DIR = "node_modules";
|
|
@@ -209,7 +221,10 @@ const toEnvFiles = (input, cwd, home) => {
|
|
|
209
221
|
: [DEFAULT_ENV_FILE];
|
|
210
222
|
return toUniquePathList(values, cwd, home);
|
|
211
223
|
};
|
|
212
|
-
const rawRealpathSync =
|
|
224
|
+
const rawRealpathSync = (candidate) => {
|
|
225
|
+
const { realpathSync } = nodeFs();
|
|
226
|
+
return (realpathSync.native ?? realpathSync)(candidate);
|
|
227
|
+
};
|
|
213
228
|
const toCanonicalPath = (candidate) => {
|
|
214
229
|
return toSharedCanonicalPath(candidate, {
|
|
215
230
|
existsSync: existsSyncCompat,
|
|
@@ -276,26 +291,45 @@ const resolveNodePermissionActivationFlag = () => {
|
|
|
276
291
|
}
|
|
277
292
|
return "--permission";
|
|
278
293
|
};
|
|
279
|
-
const
|
|
294
|
+
const nodeSupportsNetworkPermission = () => {
|
|
295
|
+
try {
|
|
296
|
+
const raw = getNodeProcess()?.versions?.node;
|
|
297
|
+
const major = Number.parseInt(String(raw).split(".", 1)[0] ?? "", 10);
|
|
298
|
+
return Number.isInteger(major) && major >= 25;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const toNodeFlags = ({ read, readAll, write, writeAll, netAll, envFiles, node, }) => {
|
|
280
305
|
const modelFlags = [];
|
|
281
306
|
if (readAll) {
|
|
282
307
|
modelFlags.push("--allow-fs-read=*");
|
|
283
308
|
}
|
|
284
|
-
else
|
|
285
|
-
|
|
309
|
+
else {
|
|
310
|
+
for (const path of read) {
|
|
311
|
+
modelFlags.push(`--allow-fs-read=${path}`);
|
|
312
|
+
}
|
|
286
313
|
}
|
|
287
314
|
if (writeAll) {
|
|
288
315
|
modelFlags.push("--allow-fs-write=*");
|
|
289
316
|
}
|
|
290
|
-
else
|
|
291
|
-
|
|
317
|
+
else {
|
|
318
|
+
for (const path of write) {
|
|
319
|
+
modelFlags.push(`--allow-fs-write=${path}`);
|
|
320
|
+
}
|
|
292
321
|
}
|
|
293
322
|
if (node.allowWorker)
|
|
294
323
|
modelFlags.push("--allow-worker");
|
|
295
324
|
if (node.allowChildProcess)
|
|
296
325
|
modelFlags.push("--allow-child-process");
|
|
326
|
+
if (netAll && nodeSupportsNetworkPermission()) {
|
|
327
|
+
modelFlags.push("--allow-net");
|
|
328
|
+
}
|
|
297
329
|
if (node.allowAddons)
|
|
298
330
|
modelFlags.push("--allow-addons");
|
|
331
|
+
if (node.allowFfi)
|
|
332
|
+
modelFlags.push("--allow-ffi");
|
|
299
333
|
if (node.allowWasi)
|
|
300
334
|
modelFlags.push("--allow-wasi");
|
|
301
335
|
const flags = [];
|
|
@@ -402,6 +436,10 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
|
|
|
402
436
|
const input = normalizeProtocolInput(permission);
|
|
403
437
|
if (!input)
|
|
404
438
|
return undefined;
|
|
439
|
+
// Browsers have no filesystem, process, or runtime flags to police, and the
|
|
440
|
+
// strict default would otherwise reach for `node:path` and throw.
|
|
441
|
+
if (IS_BROWSER)
|
|
442
|
+
return undefined;
|
|
405
443
|
const rawMode = input.mode;
|
|
406
444
|
const mode = (rawMode === "unsafe" || rawMode === "off")
|
|
407
445
|
? "unsafe"
|
|
@@ -458,6 +496,7 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
|
|
|
458
496
|
allowWorker: true,
|
|
459
497
|
allowChildProcess: true,
|
|
460
498
|
allowAddons: true,
|
|
499
|
+
allowFfi: true,
|
|
461
500
|
allowWasi: true,
|
|
462
501
|
flags: [],
|
|
463
502
|
},
|
|
@@ -534,9 +573,7 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
|
|
|
534
573
|
const workers = hasOwn(input, "workers")
|
|
535
574
|
? input.workers === true
|
|
536
575
|
: input.node?.allowWorker === true;
|
|
537
|
-
const ffiSource = hasOwn(input, "ffi")
|
|
538
|
-
? input.ffi
|
|
539
|
-
: (input.node?.allowAddons === true ? true : false);
|
|
576
|
+
const ffiSource = hasOwn(input, "ffi") ? input.ffi : false;
|
|
540
577
|
const ffiAll = ffiSource === true;
|
|
541
578
|
const ffi = ffiAll ? [] : toUniquePathList(Array.isArray(ffiSource) ? ffiSource : undefined, cwd, home);
|
|
542
579
|
const denyFfi = toUniquePathList(input.denyFfi, cwd, home);
|
|
@@ -552,7 +589,13 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
|
|
|
552
589
|
const nodeSettings = {
|
|
553
590
|
allowWorker: workers,
|
|
554
591
|
allowChildProcess: runAll || run.length > 0,
|
|
555
|
-
|
|
592
|
+
// Top-level `ffi` is the explicit cross-runtime native-code capability.
|
|
593
|
+
// Runtime-specific overrides stay independent so allowing addons does not
|
|
594
|
+
// silently grant unrestricted node:ffi access.
|
|
595
|
+
allowAddons: input.node?.allowAddons === true ||
|
|
596
|
+
ffiAll ||
|
|
597
|
+
ffi.length > 0,
|
|
598
|
+
allowFfi: input.node?.allowFfi === true || ffiAll || ffi.length > 0,
|
|
556
599
|
allowWasi: wasi,
|
|
557
600
|
};
|
|
558
601
|
const denoSettings = {
|
|
@@ -604,6 +647,7 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
|
|
|
604
647
|
readAll,
|
|
605
648
|
write: resolvedWrite,
|
|
606
649
|
writeAll,
|
|
650
|
+
netAll,
|
|
607
651
|
envFiles,
|
|
608
652
|
node: nodeSettings,
|
|
609
653
|
}),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CompiledWorkerCheck, CompiledWorkerOptions } from "../types.js";
|
|
2
|
+
export declare const COMPILED_WORKER_FORMAT = "knitting-compiled-worker";
|
|
3
|
+
export declare const COMPILED_WORKER_FORMAT_VERSION = 1;
|
|
4
|
+
export declare const COMPILED_WORKER_PROTOCOL = "knitting-number-v1";
|
|
5
|
+
export declare const COMPILED_WORKER_JSON_PROTOCOL = "knitting-json-v1";
|
|
6
|
+
export declare const COMPILED_WORKER_EXTENSION = ".knt";
|
|
7
|
+
export type CompiledWorkerTask = {
|
|
8
|
+
index: number;
|
|
9
|
+
exportName: string;
|
|
10
|
+
};
|
|
11
|
+
export type ArtifactInspection = CompiledWorkerCheck & {
|
|
12
|
+
artifactHref: string;
|
|
13
|
+
manifestHref: string;
|
|
14
|
+
taskEntries?: CompiledWorkerTask[];
|
|
15
|
+
};
|
|
16
|
+
export declare const defaultCompiledWorkerArtifact: (source: string | URL) => string;
|
|
17
|
+
export declare const inspectCompiledWorkerArtifact: ({ source, options, requiredTasks, }: {
|
|
18
|
+
source: string | URL;
|
|
19
|
+
options?: CompiledWorkerOptions;
|
|
20
|
+
requiredTasks?: readonly string[];
|
|
21
|
+
}) => ArtifactInspection;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
|
|
2
|
+
import { toModuleUrl } from "../common/module-url.js";
|
|
3
|
+
export const COMPILED_WORKER_FORMAT = "knitting-compiled-worker";
|
|
4
|
+
export const COMPILED_WORKER_FORMAT_VERSION = 1;
|
|
5
|
+
export const COMPILED_WORKER_PROTOCOL = "knitting-number-v1";
|
|
6
|
+
export const COMPILED_WORKER_JSON_PROTOCOL = "knitting-json-v1";
|
|
7
|
+
export const COMPILED_WORKER_EXTENSION = ".knt";
|
|
8
|
+
const deno = globalThis.Deno;
|
|
9
|
+
const normalizeArch = (arch) => {
|
|
10
|
+
if (arch === "x86_64")
|
|
11
|
+
return "x64";
|
|
12
|
+
if (arch === "aarch64")
|
|
13
|
+
return "arm64";
|
|
14
|
+
return arch ?? "unknown";
|
|
15
|
+
};
|
|
16
|
+
const runtimeTarget = () => {
|
|
17
|
+
const process = getNodeProcess();
|
|
18
|
+
if (process !== undefined) {
|
|
19
|
+
return {
|
|
20
|
+
platform: process.platform ?? "unknown",
|
|
21
|
+
arch: normalizeArch(process.arch),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
platform: deno?.build?.os ?? "unknown",
|
|
26
|
+
arch: normalizeArch(deno?.build?.arch),
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const normalizeSourceHref = (source) => source instanceof URL ? source.href : toModuleUrl(source);
|
|
30
|
+
const resolveHref = (value, base) => {
|
|
31
|
+
try {
|
|
32
|
+
return new URL(value).href;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return new URL(value, base).href;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export const defaultCompiledWorkerArtifact = (source) => {
|
|
39
|
+
const url = new URL(normalizeSourceHref(source));
|
|
40
|
+
url.search = "";
|
|
41
|
+
url.hash = "";
|
|
42
|
+
url.pathname = url.pathname.replace(/\.[^./]+$/, "") +
|
|
43
|
+
COMPILED_WORKER_EXTENSION;
|
|
44
|
+
return url.href;
|
|
45
|
+
};
|
|
46
|
+
const artifactHrefs = (source, options = {}) => {
|
|
47
|
+
const sourceHref = normalizeSourceHref(source);
|
|
48
|
+
const artifactHref = options.artifact === undefined
|
|
49
|
+
? defaultCompiledWorkerArtifact(sourceHref)
|
|
50
|
+
: resolveHref(options.artifact, sourceHref);
|
|
51
|
+
const manifestHref = options.manifest === undefined
|
|
52
|
+
? artifactHref + ".json"
|
|
53
|
+
: resolveHref(options.manifest, artifactHref);
|
|
54
|
+
return { artifactHref, manifestHref };
|
|
55
|
+
};
|
|
56
|
+
const filePath = (href) => {
|
|
57
|
+
const converter = getNodeBuiltinModule("node:url")?.fileURLToPath;
|
|
58
|
+
if (converter !== undefined)
|
|
59
|
+
return converter(href);
|
|
60
|
+
const url = new URL(href);
|
|
61
|
+
if (url.protocol !== "file:") {
|
|
62
|
+
throw new Error("Compiled worker artifact must use file:, got " + url.protocol);
|
|
63
|
+
}
|
|
64
|
+
return decodeURIComponent(url.pathname);
|
|
65
|
+
};
|
|
66
|
+
const readTextFile = (path) => {
|
|
67
|
+
const fs = getNodeBuiltinModule("node:fs");
|
|
68
|
+
if (fs !== undefined)
|
|
69
|
+
return fs.readFileSync(path, "utf8");
|
|
70
|
+
if (deno?.readTextFileSync !== undefined)
|
|
71
|
+
return deno.readTextFileSync(path);
|
|
72
|
+
throw new Error("Synchronous filesystem access is unavailable");
|
|
73
|
+
};
|
|
74
|
+
const inspectExecutable = (path) => {
|
|
75
|
+
try {
|
|
76
|
+
const fs = getNodeBuiltinModule("node:fs");
|
|
77
|
+
if (fs !== undefined) {
|
|
78
|
+
const stat = fs.statSync(path);
|
|
79
|
+
if (!stat.isFile())
|
|
80
|
+
return "artifact is not a file";
|
|
81
|
+
if (runtimeTarget().platform !== "win32" && (stat.mode & 0o111) === 0) {
|
|
82
|
+
return "artifact is not executable";
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (deno?.statSync !== undefined) {
|
|
87
|
+
const stat = deno.statSync(path);
|
|
88
|
+
if (!stat.isFile)
|
|
89
|
+
return "artifact is not a file";
|
|
90
|
+
if (stat.mode !== null && (stat.mode & 0o111) === 0) {
|
|
91
|
+
return "artifact is not executable";
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return "filesystem metadata is unavailable";
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = String(error?.message ?? error);
|
|
99
|
+
return "artifact is unavailable: " + message;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const compilerLabel = (compiler) => {
|
|
103
|
+
if (typeof compiler === "string" && compiler.length > 0)
|
|
104
|
+
return compiler;
|
|
105
|
+
if (compiler !== null && typeof compiler === "object" &&
|
|
106
|
+
typeof compiler.name === "string" &&
|
|
107
|
+
compiler.name.length > 0 &&
|
|
108
|
+
typeof compiler.version === "string") {
|
|
109
|
+
const value = compiler;
|
|
110
|
+
return (value.name + " " + value.version).trim();
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
};
|
|
114
|
+
export const inspectCompiledWorkerArtifact = ({ source, options, requiredTasks, }) => {
|
|
115
|
+
const sourceHref = normalizeSourceHref(source);
|
|
116
|
+
const { artifactHref, manifestHref } = artifactHrefs(sourceHref, options);
|
|
117
|
+
let artifact;
|
|
118
|
+
let manifest;
|
|
119
|
+
try {
|
|
120
|
+
artifact = filePath(artifactHref);
|
|
121
|
+
manifest = filePath(manifestHref);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const message = String(error?.message ?? error);
|
|
125
|
+
return {
|
|
126
|
+
compiled: false,
|
|
127
|
+
artifact: artifactHref,
|
|
128
|
+
manifest: manifestHref,
|
|
129
|
+
artifactHref,
|
|
130
|
+
manifestHref,
|
|
131
|
+
reason: message,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const base = {
|
|
135
|
+
compiled: false,
|
|
136
|
+
artifact,
|
|
137
|
+
manifest,
|
|
138
|
+
artifactHref,
|
|
139
|
+
manifestHref,
|
|
140
|
+
};
|
|
141
|
+
const executableProblem = inspectExecutable(artifact);
|
|
142
|
+
if (executableProblem !== undefined) {
|
|
143
|
+
return { ...base, reason: executableProblem };
|
|
144
|
+
}
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(readTextFile(manifest));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const message = String(error?.message ?? error);
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
reason: "manifest is unavailable or invalid: " + message,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
157
|
+
return { ...base, reason: "manifest root must be an object" };
|
|
158
|
+
}
|
|
159
|
+
const workerManifest = parsed;
|
|
160
|
+
const compiler = compilerLabel(workerManifest.compiler);
|
|
161
|
+
const details = {
|
|
162
|
+
compiler,
|
|
163
|
+
protocol: typeof workerManifest.protocol === "string"
|
|
164
|
+
? workerManifest.protocol
|
|
165
|
+
: undefined,
|
|
166
|
+
};
|
|
167
|
+
if (workerManifest.format !== COMPILED_WORKER_FORMAT ||
|
|
168
|
+
workerManifest.version !== COMPILED_WORKER_FORMAT_VERSION) {
|
|
169
|
+
return { ...base, ...details, reason: "unsupported manifest format" };
|
|
170
|
+
}
|
|
171
|
+
if (workerManifest.protocol !== COMPILED_WORKER_PROTOCOL &&
|
|
172
|
+
workerManifest.protocol !== COMPILED_WORKER_JSON_PROTOCOL) {
|
|
173
|
+
return { ...base, ...details, reason: "unsupported worker protocol" };
|
|
174
|
+
}
|
|
175
|
+
if (compiler === undefined) {
|
|
176
|
+
return { ...base, ...details, reason: "manifest compiler is invalid" };
|
|
177
|
+
}
|
|
178
|
+
const target = runtimeTarget();
|
|
179
|
+
if (workerManifest.target?.platform !== target.platform ||
|
|
180
|
+
normalizeArch(workerManifest.target?.arch) !== target.arch) {
|
|
181
|
+
return {
|
|
182
|
+
...base,
|
|
183
|
+
...details,
|
|
184
|
+
reason: "artifact target " +
|
|
185
|
+
(workerManifest.target?.platform ?? "unknown") + "/" +
|
|
186
|
+
(workerManifest.target?.arch ?? "unknown") + " does not match " +
|
|
187
|
+
target.platform + "/" + target.arch,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (!Array.isArray(workerManifest.tasks) || workerManifest.tasks.length === 0) {
|
|
191
|
+
return { ...base, ...details, reason: "manifest has no tasks" };
|
|
192
|
+
}
|
|
193
|
+
const indices = new Set();
|
|
194
|
+
const taskNames = new Set();
|
|
195
|
+
for (const task of workerManifest.tasks) {
|
|
196
|
+
if (!Number.isSafeInteger(task?.index) || task.index < 0 ||
|
|
197
|
+
task.index > 0x7fff_ffff ||
|
|
198
|
+
typeof task.exportName !== "string" || task.exportName.length === 0 ||
|
|
199
|
+
indices.has(task.index) || taskNames.has(task.exportName)) {
|
|
200
|
+
return { ...base, ...details, reason: "manifest task table is invalid" };
|
|
201
|
+
}
|
|
202
|
+
indices.add(task.index);
|
|
203
|
+
taskNames.add(task.exportName);
|
|
204
|
+
}
|
|
205
|
+
if (typeof workerManifest.source !== "string" ||
|
|
206
|
+
workerManifest.source.length === 0) {
|
|
207
|
+
return { ...base, ...details, reason: "manifest source is invalid" };
|
|
208
|
+
}
|
|
209
|
+
let manifestSource;
|
|
210
|
+
try {
|
|
211
|
+
manifestSource = resolveHref(workerManifest.source.replace(/\\/g, "/"), artifactHref);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return { ...base, ...details, reason: "manifest source is invalid" };
|
|
215
|
+
}
|
|
216
|
+
if (manifestSource !== sourceHref) {
|
|
217
|
+
return {
|
|
218
|
+
...base,
|
|
219
|
+
...details,
|
|
220
|
+
tasks: [...taskNames],
|
|
221
|
+
reason: "manifest was built for a different task module",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (Number.isFinite(workerManifest.sourceMtimeMs)) {
|
|
225
|
+
try {
|
|
226
|
+
const sourcePath = filePath(sourceHref);
|
|
227
|
+
const fs = getNodeBuiltinModule("node:fs");
|
|
228
|
+
const currentMtime = fs?.statSync(sourcePath).mtimeMs ??
|
|
229
|
+
deno?.statSync?.(sourcePath).mtime?.getTime();
|
|
230
|
+
if (currentMtime !== undefined &&
|
|
231
|
+
currentMtime > workerManifest.sourceMtimeMs + 0.5) {
|
|
232
|
+
return {
|
|
233
|
+
...base,
|
|
234
|
+
...details,
|
|
235
|
+
tasks: [...taskNames],
|
|
236
|
+
reason: "task module changed after the artifact was built",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return { ...base, ...details, reason: "task module is unavailable" };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const missing = requiredTasks?.filter((name) => !taskNames.has(name)) ?? [];
|
|
245
|
+
if (missing.length > 0) {
|
|
246
|
+
return {
|
|
247
|
+
...base,
|
|
248
|
+
...details,
|
|
249
|
+
tasks: [...taskNames],
|
|
250
|
+
reason: "compiled artifact is missing tasks: " + missing.join(", "),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
...base,
|
|
255
|
+
...details,
|
|
256
|
+
compiled: true,
|
|
257
|
+
tasks: [...taskNames],
|
|
258
|
+
taskEntries: workerManifest.tasks,
|
|
259
|
+
};
|
|
260
|
+
};
|