@nimbus-sh/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/_shared/error-text.d.ts +15 -0
- package/dist/_shared/error-text.d.ts.map +1 -0
- package/dist/_shared/error-text.js +21 -0
- package/dist/_shared/exports-resolver.d.ts +19 -7
- package/dist/_shared/exports-resolver.d.ts.map +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/retry.d.ts.map +1 -1
- package/dist/_shared/retry.js +3 -1
- package/dist/_shared/tarball-stream.d.ts +93 -0
- package/dist/_shared/tarball-stream.d.ts.map +1 -0
- package/dist/_shared/tarball-stream.js +235 -0
- package/dist/_shared/tarball.d.ts +17 -0
- package/dist/_shared/tarball.d.ts.map +1 -0
- package/dist/_shared/tarball.js +39 -0
- package/dist/_shared/w7-frame.js +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +30 -16
- package/dist/observability/oom-classify.d.ts +1 -1
- package/dist/observability/oom-classify.d.ts.map +1 -1
- package/dist/observability/oom-classify.js +7 -0
- package/dist/observability/oom-discriminator.d.ts +1 -1
- package/dist/runtime/clang-runner.d.ts +38 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -0
- package/dist/runtime/clang-runner.js +866 -0
- package/dist/runtime/esbuild-service.d.ts +4 -3
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +9 -4
- package/dist/runtime/facet-host.d.ts +12 -0
- package/dist/runtime/facet-host.d.ts.map +1 -1
- package/dist/runtime/installed-runtimes.d.ts +4 -3
- package/dist/runtime/installed-runtimes.d.ts.map +1 -1
- package/dist/runtime/local-facet-host.d.ts.map +1 -1
- package/dist/runtime/local-facet-host.js +29 -9
- package/dist/runtime/long-running-handle.d.ts +3 -3
- package/dist/runtime/long-running-handle.d.ts.map +1 -1
- package/dist/runtime/os-contracts.d.ts +7 -5
- package/dist/runtime/os-contracts.d.ts.map +1 -1
- package/dist/runtime/port-registry.d.ts +28 -0
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +79 -9
- package/dist/runtime/process-logs.d.ts +9 -1
- package/dist/runtime/process-logs.d.ts.map +1 -1
- package/dist/runtime/project-detect.d.ts +6 -1
- package/dist/runtime/project-detect.d.ts.map +1 -1
- package/dist/runtime/require-resolver.d.ts.map +1 -1
- package/dist/runtime/require-resolver.js +3 -4
- package/dist/runtime/ruby-gems.d.ts +30 -0
- package/dist/runtime/ruby-gems.d.ts.map +1 -0
- package/dist/runtime/ruby-gems.js +636 -0
- package/dist/runtime/ruby-runner.d.ts +127 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -0
- package/dist/runtime/ruby-runner.js +1357 -0
- package/dist/runtime/runtime-registry.d.ts +4 -3
- package/dist/runtime/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/runtime-registry.js +2 -1
- package/dist/runtime/session-process-supervisor.d.ts +15 -0
- package/dist/runtime/session-process-supervisor.d.ts.map +1 -1
- package/dist/runtime/session-process-supervisor.js +30 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
- package/dist/runtime/virtual-socket-kernel.js +10 -5
- package/dist/runtime/wasm-runner.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.js +22 -14
- package/dist/shell/features.d.ts.map +1 -1
- package/dist/shell/features.js +5 -4
- package/dist/shell/unix-commands.d.ts +14 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +115 -69
- package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
- package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
- package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/index.js +3 -2
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
- package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/process.js +6 -3
- package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
- package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/Shell.js +86 -75
- package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
- package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/lexer.js +40 -0
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +8 -2
- package/dist/vfs/sqlite-vfs.d.ts +61 -1
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +199 -35
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +9 -2
- package/package.json +4 -2
- package/src/_shared/error-text.ts +21 -0
- package/src/_shared/exports-resolver.ts +24 -4
- package/src/_shared/retry.ts +4 -2
- package/src/_shared/tarball-stream.ts +263 -0
- package/src/_shared/tarball.ts +46 -0
- package/src/_shared/w7-frame.ts +2 -1
- package/src/constants.ts +30 -16
- package/src/observability/oom-classify.ts +7 -0
- package/src/observability/oom-discriminator.ts +1 -1
- package/src/runtime/clang-runner.ts +924 -0
- package/src/runtime/esbuild-service.ts +16 -11
- package/src/runtime/facet-host.ts +12 -0
- package/src/runtime/installed-runtimes.ts +4 -3
- package/src/runtime/local-facet-host.ts +28 -8
- package/src/runtime/long-running-handle.ts +3 -3
- package/src/runtime/os-contracts.ts +7 -5
- package/src/runtime/port-registry.ts +97 -9
- package/src/runtime/process-logs.ts +12 -3
- package/src/runtime/project-detect.ts +7 -1
- package/src/runtime/require-resolver.ts +7 -7
- package/src/runtime/ruby-gems.ts +682 -0
- package/src/runtime/ruby-runner.ts +1484 -0
- package/src/runtime/runtime-registry.ts +9 -7
- package/src/runtime/session-process-supervisor.ts +27 -0
- package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
- package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
- package/src/runtime/virtual-socket-kernel.ts +10 -5
- package/src/runtime/wasm-runner.ts +55 -29
- package/src/shell/features.ts +5 -4
- package/src/shell/unix-commands.ts +273 -128
- package/src/substrate/lifo/commands/net/ip.ts +15 -15
- package/src/substrate/lifo/commands/system/ps.ts +2 -1
- package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
- package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
- package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
- package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
- package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
- package/src/substrate/lifo/node-compat/index.ts +3 -2
- package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
- package/src/substrate/lifo/node-compat/process.ts +5 -2
- package/src/substrate/lifo/shell/Shell.ts +92 -70
- package/src/substrate/lifo/shell/lexer.ts +38 -0
- package/src/vfs/seed-project.ts +10 -3
- package/src/vfs/sqlite-vfs.ts +209 -36
- package/src/workspace/nimbus-workspace.ts +10 -1
package/dist/vfs/sqlite-vfs.js
CHANGED
|
@@ -51,11 +51,22 @@ import { WeightedCreditPool, } from '../_shared/weighted-credit-pool.js';
|
|
|
51
51
|
import { LEGACY_SYMLINK_REGISTRY_PATH } from './symlink-registry.js';
|
|
52
52
|
import { CRED_KERNEL, } from '../runtime/os-contracts.js';
|
|
53
53
|
const CONTENT_ID_ALLOCATION_ATTEMPTS = 8;
|
|
54
|
+
/**
|
|
55
|
+
* Live view of the global object. `process` is not in the Workers lib, so its
|
|
56
|
+
* shape is declared here rather than assumed present.
|
|
57
|
+
*/
|
|
58
|
+
const nodeHost = globalThis;
|
|
59
|
+
/** Single gate for the W2.5b install-pipeline diagnostics below. */
|
|
60
|
+
function installPipelineDiagEnabled() {
|
|
61
|
+
return nodeHost.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1';
|
|
62
|
+
}
|
|
54
63
|
const INODE_ROWS_PER_SQL_EXEC = 9;
|
|
55
64
|
const CHUNK_ROWS_PER_SQL_EXEC = 33;
|
|
56
65
|
const CONTENT_IDS_PER_SQL_EXEC = 50;
|
|
57
66
|
const TRANSACTION_DURATION_SAMPLE_COUNT = 128;
|
|
58
67
|
const CONTENT_SCHEMA_MIGRATION = 'content_generations_v1';
|
|
68
|
+
/** Storage key of the shared scratch tree. `normalizeVfsPath` drops the slash. */
|
|
69
|
+
const TMP_ROOT = 'tmp';
|
|
59
70
|
export const VFS_APPEND_RECEIPT_LIMIT = 2048;
|
|
60
71
|
const INODE_KIND_FILE = 0;
|
|
61
72
|
const INODE_KIND_DIRECTORY = 1;
|
|
@@ -357,6 +368,9 @@ export class SqliteVFS {
|
|
|
357
368
|
activeStagingContentIds = new Set();
|
|
358
369
|
/** True only while durable GC work or a known abandoned staging row exists. */
|
|
359
370
|
maintenancePending = false;
|
|
371
|
+
// Keyset cursor for the orphan scan: each maintenance call reads one bounded
|
|
372
|
+
// page of distinct content ids past this bound, wrapping to '' at the end.
|
|
373
|
+
orphanScanCursor = '';
|
|
360
374
|
// Stage 2 transaction/phase telemetry. Scalar writes stay cheap; the
|
|
361
375
|
// percentile is computed from the fixed ring only when diagnostics read it.
|
|
362
376
|
_activeTransaction = null;
|
|
@@ -939,6 +953,85 @@ export class SqliteVFS {
|
|
|
939
953
|
return null;
|
|
940
954
|
return this.blobToUint8Array(rows[0].data);
|
|
941
955
|
}
|
|
956
|
+
/**
|
|
957
|
+
* Principals whose `/tmp` is private, keyed by uid, valued by the storage
|
|
958
|
+
* root their `/tmp` resolves to.
|
|
959
|
+
*
|
|
960
|
+
* `/tmp` keeps its path in every view and only the bytes behind it differ,
|
|
961
|
+
* so nothing has to be told which principal it is. The credential is the
|
|
962
|
+
* only per-process state visible where paths are RESOLVED — there is no pid
|
|
963
|
+
* and no cwd down here — so it is what the private view is keyed on.
|
|
964
|
+
*
|
|
965
|
+
* Resolution rather than a mount, because a mount diverges the two planes:
|
|
966
|
+
* the shell writing `/tmp/a` and the file API writing `/tmp/b` landed in
|
|
967
|
+
* different trees under the same name. `resolvePath` already takes `cred`,
|
|
968
|
+
* and every plane goes through it.
|
|
969
|
+
*
|
|
970
|
+
* Registration is also what makes a principal CONFINED for `chmod`. The two
|
|
971
|
+
* properties travel together because they answer one question: is this
|
|
972
|
+
* principal a guest in this filesystem. Nothing here applies to an
|
|
973
|
+
* unregistered credential, so the ordinary session user is untouched.
|
|
974
|
+
*/
|
|
975
|
+
confinedTmpRoots = new Map();
|
|
976
|
+
/**
|
|
977
|
+
* Confine a principal. `tmpRoot` is a storage key, not a logical path — the
|
|
978
|
+
* caller owns creating and chowning it, because a per-principal `chown` is
|
|
979
|
+
* uid-0 only and a guest cannot provision its own.
|
|
980
|
+
*/
|
|
981
|
+
confinePrincipal(uid, tmpRoot) {
|
|
982
|
+
const root = normalizeVfsPath(tmpRoot);
|
|
983
|
+
if (root === '')
|
|
984
|
+
throw vfsError('EINVAL', 'a private /tmp root cannot be the filesystem root');
|
|
985
|
+
this.confinedTmpRoots.set(uid, root);
|
|
986
|
+
}
|
|
987
|
+
/** Drop a confinement. A principal's `/tmp` dies with it; its home does not. */
|
|
988
|
+
releasePrincipal(uid) {
|
|
989
|
+
this.confinedTmpRoots.delete(uid);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Logical path -> storage key, for one credential.
|
|
993
|
+
*
|
|
994
|
+
* Everything under `/tmp` belongs to the caller's own private root, which is
|
|
995
|
+
* what makes the same path mean different bytes per principal. Idempotent: a
|
|
996
|
+
* key already inside that root is returned untouched, so the several methods
|
|
997
|
+
* that derive a key before handing it on cannot stack the rewrite.
|
|
998
|
+
*/
|
|
999
|
+
storageKey(path, cred) {
|
|
1000
|
+
const key = normalizeVfsPath(path);
|
|
1001
|
+
const root = this.confinedTmpRoots.get(cred.uid);
|
|
1002
|
+
if (root === undefined)
|
|
1003
|
+
return key;
|
|
1004
|
+
if (key === root || key.startsWith(`${root}/`))
|
|
1005
|
+
return key;
|
|
1006
|
+
if (key === TMP_ROOT)
|
|
1007
|
+
return root;
|
|
1008
|
+
if (!key.startsWith(`${TMP_ROOT}/`))
|
|
1009
|
+
return key;
|
|
1010
|
+
return `${root}/${key.slice(TMP_ROOT.length + 1)}`;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Storage key -> the name this credential knows it by, or `null` when it has
|
|
1014
|
+
* none. The inverse of {@link storageKey}, for the one surface that reports
|
|
1015
|
+
* paths it was not asked about: {@link list}.
|
|
1016
|
+
*
|
|
1017
|
+
* A confined caller has no name for the shared scratch tree — `/tmp` is its
|
|
1018
|
+
* own root — nor for another principal's, so both answer `null` and are
|
|
1019
|
+
* omitted. An unconfined caller sees storage as it is, which is what the
|
|
1020
|
+
* kernel and the session user need.
|
|
1021
|
+
*/
|
|
1022
|
+
logicalPath(key, cred) {
|
|
1023
|
+
const root = this.confinedTmpRoots.get(cred.uid);
|
|
1024
|
+
if (root === undefined)
|
|
1025
|
+
return key;
|
|
1026
|
+
if (key === root)
|
|
1027
|
+
return TMP_ROOT;
|
|
1028
|
+
if (key.startsWith(`${root}/`))
|
|
1029
|
+
return `${TMP_ROOT}/${key.slice(root.length + 1)}`;
|
|
1030
|
+
// The SHARED scratch root has no name here — this caller's `/tmp` is its
|
|
1031
|
+
// own root, which already supplied that entry. Returning it too would
|
|
1032
|
+
// enumerate one name twice, for two different directories.
|
|
1033
|
+
return key === TMP_ROOT || key.startsWith(`${TMP_ROOT}/`) ? null : key;
|
|
1034
|
+
}
|
|
942
1035
|
// ── Filesystem operations ─────────────────────────────────────────────
|
|
943
1036
|
as(cred) {
|
|
944
1037
|
const bound = Object.freeze({
|
|
@@ -983,7 +1076,7 @@ export class SqliteVFS {
|
|
|
983
1076
|
writeBatch: (payload) => this.writeBatch(payload, bound),
|
|
984
1077
|
writeStream: (stream, options) => this.writeStream(stream, options, bound),
|
|
985
1078
|
mkdirBatch: (paths) => this.mkdirBatch(paths, bound),
|
|
986
|
-
revision: (path) => this.revision(path),
|
|
1079
|
+
revision: (path) => this.revision(path, bound),
|
|
987
1080
|
epoch: this._epoch,
|
|
988
1081
|
};
|
|
989
1082
|
}
|
|
@@ -1007,7 +1100,7 @@ export class SqliteVFS {
|
|
|
1007
1100
|
return (granted & requested) === requested;
|
|
1008
1101
|
}
|
|
1009
1102
|
resolvePath(path, cred, followLeaf, allowMissing) {
|
|
1010
|
-
let current =
|
|
1103
|
+
let current = this.storageKey(path, cred);
|
|
1011
1104
|
const seen = new Set();
|
|
1012
1105
|
for (let hops = 0; hops <= 40; hops++) {
|
|
1013
1106
|
const parts = current.split('/').filter(Boolean);
|
|
@@ -1028,8 +1121,13 @@ export class SqliteVFS {
|
|
|
1028
1121
|
seen.add(prefix);
|
|
1029
1122
|
const target = dec.decode(this.readInodeBytes(prefix, inode));
|
|
1030
1123
|
const suffix = parts.slice(index + 1).join('/');
|
|
1124
|
+
// An ABSOLUTE target is a logical path the same way the caller's
|
|
1125
|
+
// was, so it goes through the same rewrite: otherwise a symlink to
|
|
1126
|
+
// /tmp/y stored inside a private tree would read the shared one,
|
|
1127
|
+
// which is the one way out of the confinement. A RELATIVE target
|
|
1128
|
+
// resolves against a key that is already private, so it stays there.
|
|
1031
1129
|
const resolvedTarget = target.startsWith('/')
|
|
1032
|
-
?
|
|
1130
|
+
? this.storageKey(target, cred)
|
|
1033
1131
|
: normalizeVfsPath(`${this.parentPath(prefix)}/${target}`);
|
|
1034
1132
|
current = suffix ? normalizeVfsPath(`${resolvedTarget}/${suffix}`) : resolvedTarget;
|
|
1035
1133
|
restarted = true;
|
|
@@ -1068,6 +1166,29 @@ export class SqliteVFS {
|
|
|
1068
1166
|
if (resolved.inode?.kind !== 'directory')
|
|
1069
1167
|
throw vfsError('ENOTDIR', parent);
|
|
1070
1168
|
}
|
|
1169
|
+
/**
|
|
1170
|
+
* POSIX sticky-bit restriction on a shared directory.
|
|
1171
|
+
*
|
|
1172
|
+
* Write permission on a directory is normally enough to remove or rename
|
|
1173
|
+
* anything inside it, which is why `/tmp` is `1777` and not `0777`: the
|
|
1174
|
+
* sticky bit narrows that to the entry's owner, the directory's owner, and
|
|
1175
|
+
* root. Nothing enforced it here, so a world-writable shared directory gave
|
|
1176
|
+
* every principal the ability to delete every other principal's files —
|
|
1177
|
+
* the mode said one thing and the filesystem did another.
|
|
1178
|
+
*/
|
|
1179
|
+
checkStickyParentMutation(path, inode, cred) {
|
|
1180
|
+
if (cred.uid === 0)
|
|
1181
|
+
return;
|
|
1182
|
+
const parent = this.parentPath(normalizeVfsPath(path));
|
|
1183
|
+
if (parent === '')
|
|
1184
|
+
return;
|
|
1185
|
+
const parentInode = this.inodes.get(parent);
|
|
1186
|
+
if (!parentInode || (parentInode.mode & 0o1000) === 0)
|
|
1187
|
+
return;
|
|
1188
|
+
if (cred.uid !== parentInode.uid && cred.uid !== inode.uid) {
|
|
1189
|
+
throw vfsError('EPERM', normalizeVfsPath(path));
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1071
1192
|
/**
|
|
1072
1193
|
* Shared resolver for the boolean probes (exists/isDirectory/isFile/
|
|
1073
1194
|
* isSymlink). Resolution-structure failures — a missing or non-directory
|
|
@@ -1105,10 +1226,12 @@ export class SqliteVFS {
|
|
|
1105
1226
|
* under it changed in this DO lifetime). `revision('')` equals the
|
|
1106
1227
|
* global clock by construction (every mutation stamps all ancestors).
|
|
1107
1228
|
*/
|
|
1108
|
-
revision(path) {
|
|
1229
|
+
revision(path, cred) {
|
|
1109
1230
|
if (path === undefined)
|
|
1110
1231
|
return this._revision;
|
|
1111
|
-
|
|
1232
|
+
// A credentialed caller asks about its own view; a confined one asking
|
|
1233
|
+
// after /tmp/x means its own file, so the counter must be that file's.
|
|
1234
|
+
const p = cred === undefined ? normalizeVfsPath(path) : this.storageKey(path, cred);
|
|
1112
1235
|
if (p === '')
|
|
1113
1236
|
return this._revision;
|
|
1114
1237
|
return this._pathRevisions.get(p) ?? 0;
|
|
@@ -1288,7 +1411,7 @@ export class SqliteVFS {
|
|
|
1288
1411
|
}
|
|
1289
1412
|
}
|
|
1290
1413
|
mkdir(path, options, cred) {
|
|
1291
|
-
const normalized =
|
|
1414
|
+
const normalized = this.storageKey(path, cred);
|
|
1292
1415
|
this.assertMutationsAllowed([normalized]);
|
|
1293
1416
|
if (this.exists(normalized, cred))
|
|
1294
1417
|
return;
|
|
@@ -1389,7 +1512,7 @@ export class SqliteVFS {
|
|
|
1389
1512
|
}
|
|
1390
1513
|
symlink(target, path, cred) {
|
|
1391
1514
|
this.assertMutationsAllowed([path]);
|
|
1392
|
-
const normalized =
|
|
1515
|
+
const normalized = this.storageKey(path, cred);
|
|
1393
1516
|
const prior = this.checkAccess(normalized, 0, cred, { followLeaf: false, allowMissingLeaf: true });
|
|
1394
1517
|
if (prior.inode)
|
|
1395
1518
|
throw vfsError('EEXIST', normalized);
|
|
@@ -2124,6 +2247,22 @@ export class SqliteVFS {
|
|
|
2124
2247
|
throw vfsError('ENOENT', path);
|
|
2125
2248
|
if (cred.uid !== 0 && cred.uid !== inode.uid)
|
|
2126
2249
|
throw vfsError('EPERM', resolved.path);
|
|
2250
|
+
// A confined principal owns its own triad and nothing else. Refusing chmod
|
|
2251
|
+
// outright would be simpler and wrong: execution is gated on the x bit, so
|
|
2252
|
+
// a guest that writes build.sh and cannot chmod it cannot run it. What
|
|
2253
|
+
// must not happen is WIDENING past its own principal, so the group, other
|
|
2254
|
+
// and setuid/setgid/sticky bits keep the value they were provisioned with
|
|
2255
|
+
// while the owner triad moves freely. `u+x` works; `+x` and `777` do not.
|
|
2256
|
+
//
|
|
2257
|
+
// Refused, never clamped. Quietly narrowing a mutation to the part that
|
|
2258
|
+
// was allowed reports success for something other than what was asked, so
|
|
2259
|
+
// the refusal names the spelling that works instead.
|
|
2260
|
+
if (cred.uid !== 0 && this.confinedTmpRoots.has(cred.uid)) {
|
|
2261
|
+
const beyondOwner = (mode & 0o7777) & ~0o700;
|
|
2262
|
+
if (beyondOwner !== ((inode.mode & 0o7777) & ~0o700)) {
|
|
2263
|
+
throw vfsError('EPERM', `${resolved.path}: mode change would grant permission outside your own principal; use u+x`);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2127
2266
|
this.assertMutationsAllowed([inode.path]);
|
|
2128
2267
|
const full = inodeTypeBits(inode.kind) | (mode & 0o7777);
|
|
2129
2268
|
inode.mode = full;
|
|
@@ -2183,7 +2322,7 @@ export class SqliteVFS {
|
|
|
2183
2322
|
// Before the walk, deliberately — see VfsListPage.
|
|
2184
2323
|
const epoch = this._epoch;
|
|
2185
2324
|
const rev = this._revision;
|
|
2186
|
-
const from = after === null || after === undefined ? '' :
|
|
2325
|
+
const from = after === null || after === undefined ? '' : this.storageKey(after, cred);
|
|
2187
2326
|
// this.inodes is insertion-ordered, not path-ordered, so the sort is what
|
|
2188
2327
|
// makes `after` a resume key at all. Paid once per page against a map the
|
|
2189
2328
|
// DO already holds whole.
|
|
@@ -2210,8 +2349,16 @@ export class SqliteVFS {
|
|
|
2210
2349
|
catch {
|
|
2211
2350
|
continue;
|
|
2212
2351
|
}
|
|
2352
|
+
// Reported in the caller's OWN path space. Enumerating raw storage keys
|
|
2353
|
+
// would name a private root the caller cannot address and does not know
|
|
2354
|
+
// it has, and a caller feeding such a path back would be asking about
|
|
2355
|
+
// someone else's tree. `null` means the path has no name for this
|
|
2356
|
+
// caller, which is the same OMIT the access check above performs.
|
|
2357
|
+
const logical = this.logicalPath(path, cred);
|
|
2358
|
+
if (logical === null)
|
|
2359
|
+
continue;
|
|
2213
2360
|
entries.push({
|
|
2214
|
-
path,
|
|
2361
|
+
path: logical,
|
|
2215
2362
|
kind: inode.kind,
|
|
2216
2363
|
size: inode.size,
|
|
2217
2364
|
rev: this._pathRevisions.get(path) ?? 0,
|
|
@@ -2220,7 +2367,7 @@ export class SqliteVFS {
|
|
|
2220
2367
|
return { epoch, rev, entries, next };
|
|
2221
2368
|
}
|
|
2222
2369
|
readdir(path, cred) {
|
|
2223
|
-
const np =
|
|
2370
|
+
const np = this.storageKey(path, cred);
|
|
2224
2371
|
const resolved = np ? this.checkAccess(np, 0o4, cred) : { path: '', inode: undefined };
|
|
2225
2372
|
const inode = resolved.inode;
|
|
2226
2373
|
if (inode && inode.kind !== 'directory')
|
|
@@ -2229,7 +2376,7 @@ export class SqliteVFS {
|
|
|
2229
2376
|
if (!kids) {
|
|
2230
2377
|
// W2.5b diagnostic: empty children-set for a directory we expected
|
|
2231
2378
|
// to be populated.
|
|
2232
|
-
if (
|
|
2379
|
+
if (installPipelineDiagEnabled()) {
|
|
2233
2380
|
// eslint-disable-next-line no-console
|
|
2234
2381
|
console.warn('[sqlite-vfs/W2.5b] readdir miss path=' + np +
|
|
2235
2382
|
' kidsUndefined=true inodeExists=' + this.inodes.has(np));
|
|
@@ -2248,7 +2395,7 @@ export class SqliteVFS {
|
|
|
2248
2395
|
// fewer (some entries' inodes are missing from this.inodes), log it.
|
|
2249
2396
|
// This distinguishes (a) "children index broken" from (b) "inodes
|
|
2250
2397
|
// map lost entries".
|
|
2251
|
-
if (
|
|
2398
|
+
if (installPipelineDiagEnabled() &&
|
|
2252
2399
|
kids.size !== results.length) {
|
|
2253
2400
|
// eslint-disable-next-line no-console
|
|
2254
2401
|
console.warn('[sqlite-vfs/W2.5b] readdir size mismatch path=' + np +
|
|
@@ -2273,23 +2420,25 @@ export class SqliteVFS {
|
|
|
2273
2420
|
if (!inode)
|
|
2274
2421
|
throw vfsError('ENOENT', path);
|
|
2275
2422
|
this.checkParentAccess(resolved.path, cred);
|
|
2423
|
+
this.checkStickyParentMutation(resolved.path, inode, cred);
|
|
2276
2424
|
if (inode.isDir)
|
|
2277
2425
|
throw vfsError('EISDIR', resolved.path);
|
|
2278
2426
|
this.writeBatch({ inodes: [], chunks: [], deletePaths: [resolved.path] }, cred);
|
|
2279
2427
|
}
|
|
2280
2428
|
rmdir(path, cred) {
|
|
2281
2429
|
this.assertMutationsAllowed([path]);
|
|
2282
|
-
const np =
|
|
2430
|
+
const np = this.storageKey(path, cred);
|
|
2283
2431
|
const resolved = this.checkAccess(np, 0, cred, { followLeaf: false });
|
|
2284
2432
|
this.checkParentAccess(resolved.path, cred);
|
|
2433
|
+
const inode = this.inodes.get(np);
|
|
2434
|
+
if (!inode)
|
|
2435
|
+
throw vfsError('ENOENT', path);
|
|
2436
|
+
this.checkStickyParentMutation(resolved.path, inode, cred);
|
|
2285
2437
|
// Check if empty using children index (O(1) instead of O(N))
|
|
2286
2438
|
const kids = this.children.get(np);
|
|
2287
2439
|
if (kids && kids.size > 0) {
|
|
2288
2440
|
throw vfsError('ENOTEMPTY', path);
|
|
2289
2441
|
}
|
|
2290
|
-
const inode = this.inodes.get(np);
|
|
2291
|
-
if (!inode)
|
|
2292
|
-
throw vfsError('ENOENT', path);
|
|
2293
2442
|
if (!inode.isDir)
|
|
2294
2443
|
throw vfsError('ENOTDIR', path);
|
|
2295
2444
|
this.writeBatch({ inodes: [], chunks: [], deletePaths: [np] }, cred);
|
|
@@ -2316,6 +2465,7 @@ export class SqliteVFS {
|
|
|
2316
2465
|
if (!resolved.inode)
|
|
2317
2466
|
throw vfsError('ENOENT', normalizeVfsPath(path));
|
|
2318
2467
|
this.checkParentAccess(resolved.path, cred);
|
|
2468
|
+
this.checkStickyParentMutation(resolved.path, resolved.inode, cred);
|
|
2319
2469
|
const removable = this.collectSubtreeInodes([resolved.path]);
|
|
2320
2470
|
// The directories the recursive walk used to enumerate are exactly the
|
|
2321
2471
|
// directory inodes of the subtree, and enumerating one needed read
|
|
@@ -2367,6 +2517,7 @@ export class SqliteVFS {
|
|
|
2367
2517
|
const inode = this.inodes.get(oldPath);
|
|
2368
2518
|
if (!inode)
|
|
2369
2519
|
throw new Error("ENOENT: " + oldPath);
|
|
2520
|
+
this.checkStickyParentMutation(oldPath, inode, cred);
|
|
2370
2521
|
// W-3 (WASI filesystem WASI): if newPath already exists, unlink it first so the
|
|
2371
2522
|
// SQL UPDATE doesn't conflict on inodes.path uniqueness. POSIX rename(2)
|
|
2372
2523
|
// overwrites the destination atomically; clang's atomic-write pattern
|
|
@@ -2376,6 +2527,7 @@ export class SqliteVFS {
|
|
|
2376
2527
|
// (the latter is rare but POSIX permits it for empty dirs).
|
|
2377
2528
|
const destInode = this.inodes.get(newPath);
|
|
2378
2529
|
if (destInode) {
|
|
2530
|
+
this.checkStickyParentMutation(newPath, destInode, cred);
|
|
2379
2531
|
if (destInode.isDir) {
|
|
2380
2532
|
// POSIX semantics: rename onto a non-empty dir is an error
|
|
2381
2533
|
// (ENOTEMPTY); rename onto an empty dir is allowed. We surface
|
|
@@ -2590,14 +2742,14 @@ export class SqliteVFS {
|
|
|
2590
2742
|
}
|
|
2591
2743
|
// ── Batch write (npm install fast path) ───────────────────────────────
|
|
2592
2744
|
normalizeBatchInode(entry, cred) {
|
|
2593
|
-
const path =
|
|
2745
|
+
const path = this.storageKey(entry.path, cred);
|
|
2594
2746
|
const prior = this.inodes.get(path);
|
|
2595
2747
|
const newUid = cred.uid === 0 ? (entry.uid ?? 1000) : cred.uid;
|
|
2596
2748
|
const newGid = cred.uid === 0 ? (entry.gid ?? 1000) : cred.gid;
|
|
2597
2749
|
return {
|
|
2598
2750
|
...entry,
|
|
2599
2751
|
path,
|
|
2600
|
-
parentPath:
|
|
2752
|
+
parentPath: this.storageKey(entry.parentPath, cred),
|
|
2601
2753
|
mode: prior
|
|
2602
2754
|
? prior.mode
|
|
2603
2755
|
: inodeKind(entry) === 'symlink'
|
|
@@ -2632,7 +2784,7 @@ export class SqliteVFS {
|
|
|
2632
2784
|
}
|
|
2633
2785
|
};
|
|
2634
2786
|
for (const path of payload.deletePaths ?? []) {
|
|
2635
|
-
const normalized =
|
|
2787
|
+
const normalized = this.storageKey(path, cred);
|
|
2636
2788
|
const existing = this.checkAccess(normalized, 0, cred, {
|
|
2637
2789
|
followLeaf: false,
|
|
2638
2790
|
allowMissingLeaf: true,
|
|
@@ -2648,14 +2800,14 @@ export class SqliteVFS {
|
|
|
2648
2800
|
checkParent(entry.path);
|
|
2649
2801
|
}
|
|
2650
2802
|
for (const chunk of payload.chunks) {
|
|
2651
|
-
const path =
|
|
2803
|
+
const path = this.storageKey(chunk.path, cred);
|
|
2652
2804
|
if (!pending.has(path))
|
|
2653
2805
|
this.checkAccess(path, 0o2, cred, { followLeaf: false });
|
|
2654
2806
|
}
|
|
2655
2807
|
return {
|
|
2656
2808
|
...payload,
|
|
2657
2809
|
inodes,
|
|
2658
|
-
deletePaths: payload.deletePaths?.map(
|
|
2810
|
+
deletePaths: payload.deletePaths?.map((path) => this.storageKey(path, cred)),
|
|
2659
2811
|
};
|
|
2660
2812
|
}
|
|
2661
2813
|
/**
|
|
@@ -3307,18 +3459,30 @@ export class SqliteVFS {
|
|
|
3307
3459
|
let orphanScanComplete = true;
|
|
3308
3460
|
let stagingScanComplete = true;
|
|
3309
3461
|
if (transactions < maximum) {
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3462
|
+
// The page is selected before the orphan predicates so a scan that finds
|
|
3463
|
+
// nothing still reads only one page, not the whole covering index.
|
|
3464
|
+
const pageRows = [...this.sql.exec(`SELECT page.content_id AS content_id,
|
|
3465
|
+
(NOT EXISTS (
|
|
3466
|
+
SELECT 1 FROM content_lifecycle AS lifecycle
|
|
3467
|
+
WHERE lifecycle.content_id = page.content_id
|
|
3468
|
+
)
|
|
3469
|
+
AND ${sqlNoInodeContentReference('page.content_id')}) AS orphaned
|
|
3470
|
+
FROM (
|
|
3471
|
+
SELECT chunks.content_id
|
|
3472
|
+
FROM file_chunks AS chunks
|
|
3473
|
+
WHERE chunks.content_id > ?
|
|
3474
|
+
GROUP BY chunks.content_id
|
|
3475
|
+
ORDER BY chunks.content_id
|
|
3476
|
+
LIMIT ?
|
|
3477
|
+
) AS page
|
|
3478
|
+
ORDER BY page.content_id`, this.orphanScanCursor, CONTENT_IDS_PER_SQL_EXEC)];
|
|
3479
|
+
orphanScanComplete = pageRows.length < CONTENT_IDS_PER_SQL_EXEC;
|
|
3480
|
+
this.orphanScanCursor = orphanScanComplete
|
|
3481
|
+
? ''
|
|
3482
|
+
: String(pageRows[pageRows.length - 1].content_id);
|
|
3483
|
+
const contentIds = pageRows
|
|
3484
|
+
.filter((row) => Number(row.orphaned) === 1)
|
|
3485
|
+
.map((row) => String(row.content_id));
|
|
3322
3486
|
if (contentIds.length > 0) {
|
|
3323
3487
|
const candidates = contentIds
|
|
3324
3488
|
.map((_, index) => index === 0 ? 'SELECT ? AS content_id' : 'SELECT ?')
|
|
@@ -3480,7 +3644,7 @@ export class SqliteVFS {
|
|
|
3480
3644
|
/** Best-effort process.memoryUsage().heapUsed; 0 in DO contexts. */
|
|
3481
3645
|
_safeHeapUsed() {
|
|
3482
3646
|
try {
|
|
3483
|
-
const mu =
|
|
3647
|
+
const mu = nodeHost.process?.memoryUsage?.();
|
|
3484
3648
|
return Number(mu?.heapUsed) || 0;
|
|
3485
3649
|
}
|
|
3486
3650
|
catch {
|
|
@@ -3658,7 +3822,7 @@ export class SqliteVFS {
|
|
|
3658
3822
|
// `_addToChildrenIndex` uses Set.add so repeated calls are idempotent;
|
|
3659
3823
|
// gating it on `prior === undefined` was the bug. Counters remain
|
|
3660
3824
|
// gated correctly so they don't double-count.
|
|
3661
|
-
const __diag = (
|
|
3825
|
+
const __diag = installPipelineDiagEnabled();
|
|
3662
3826
|
const replacedPaths = new Set();
|
|
3663
3827
|
for (const entry of plan.inodes) {
|
|
3664
3828
|
const prior = this.inodes.get(entry.path);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nimbus-workspace.d.ts","sourceRoot":"","sources":["../../src/workspace/nimbus-workspace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAC3D,OAAO,EAAE,KAAK,EAAE,MAAM,kCAAkC,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAE7E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAI9E,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAC/F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,SAAS,EAAqB,MAAM,sBAAsB,CAAC;AAMpE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAG/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAK1D,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAKxF,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,eAAe,CAAC;IACxC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,kDAAkD;IAClD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IACzC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC9C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,eAAe;IA4BxB,OAAO,CAAC,QAAQ,CAAC,GAAG;IA3BtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAErC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAE/C,OAAO;WAoBM,MAAM,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC;IAwD9E,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;IAInE;;;;;;OAMG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;;;;;OAMG;IACH,KAAK,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAK3D;;;;;;;OAOG;IACH,OAAO,IAAI,IAAI;CAKhB;
|
|
1
|
+
{"version":3,"file":"nimbus-workspace.d.ts","sourceRoot":"","sources":["../../src/workspace/nimbus-workspace.ts"],"names":[],"mappings":"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"}
|
|
@@ -220,9 +220,11 @@ function defaultEnv() {
|
|
|
220
220
|
* them.
|
|
221
221
|
*/
|
|
222
222
|
async function registerWasmRuntimes(deps) {
|
|
223
|
-
const [{ makeBashRunnerFactory }, { makeCPythonRunnerFactory }, { wasmRunnerSpec }, { buildRuntimeHandler },] = await Promise.all([
|
|
223
|
+
const [{ makeBashRunnerFactory }, { makeCPythonRunnerFactory }, { makeRubyRunnerFactory }, { makeClangRunnerFactory }, { wasmRunnerSpec }, { buildRuntimeHandler },] = await Promise.all([
|
|
224
224
|
import('../runtime/bash-runner.js'),
|
|
225
225
|
import('../runtime/cpython-runner.js'),
|
|
226
|
+
import('../runtime/ruby-runner.js'),
|
|
227
|
+
import('../runtime/clang-runner.js'),
|
|
226
228
|
import('../runtime/wasm-runner.js'),
|
|
227
229
|
import('../runtime/runtime-registry.js'),
|
|
228
230
|
]);
|
|
@@ -251,8 +253,13 @@ async function registerWasmRuntimes(deps) {
|
|
|
251
253
|
'bash-runner': makeBashRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
|
|
252
254
|
// No `startResident`: a workspace owns no actor that could outlive the
|
|
253
255
|
// call, so a program that keeps serving is refused by name rather than
|
|
254
|
-
// run as a one-shot that dies with it.
|
|
256
|
+
// run as a one-shot that dies with it. Same for ruby, where a script is
|
|
257
|
+
// the shape that may bind a port.
|
|
255
258
|
'cpython-runner': makeCPythonRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
|
|
259
|
+
'ruby-runner': makeRubyRunnerFactory({
|
|
260
|
+
facets: deps.facets, vfs: deps.vfs, registry: deps.registry,
|
|
261
|
+
}),
|
|
262
|
+
'clang-runner': makeClangRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
|
|
256
263
|
};
|
|
257
264
|
rehydrateInstalledRuntimesView(deps.vfs.as(CRED_KERNEL), deps.registry, deps.home, (key) => runners[key]);
|
|
258
265
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-sh/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Backend-agnostic half of Nimbus \u2014 the durable filesystem, the shell, and the OS/process contracts, over a host-supplied SQL port.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"dist",
|
|
48
|
-
"src"
|
|
48
|
+
"src",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
49
51
|
],
|
|
50
52
|
"scripts": {
|
|
51
53
|
"build": "tsc -p tsconfig.json --noCheck --noEmit false --declaration true --declarationMap true --outDir dist --rootDir src",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* error-text.ts — the text a caught value carries.
|
|
3
|
+
*
|
|
4
|
+
* A `catch` binding is `unknown`: the throw site may have thrown an Error, a
|
|
5
|
+
* string, or a plain object with a `message`, and the shell and runtime paths
|
|
6
|
+
* report all three the same way. This is that report, in one place, so a
|
|
7
|
+
* command's diagnostics do not depend on which of those a facet threw.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The message a thrown value names, or the value itself when it names none.
|
|
12
|
+
* Empty and other falsy messages fall through to the value, so an
|
|
13
|
+
* `new Error('')` still reports as `Error` rather than as nothing.
|
|
14
|
+
*/
|
|
15
|
+
export function errorText(error: unknown): string {
|
|
16
|
+
if (typeof error === 'object' && error !== null && 'message' in error) {
|
|
17
|
+
const { message } = error;
|
|
18
|
+
if (message) return String(message);
|
|
19
|
+
}
|
|
20
|
+
return String(error);
|
|
21
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 1. Install-time supervisor (TS) — `src/npm-resolver.ts` re-exports the
|
|
8
8
|
* typed functions for tree resolution.
|
|
9
9
|
*
|
|
10
|
-
* 2.
|
|
10
|
+
* 2. LoaderPool isolates (JS string) — `src/loaders/pre-bundle-preamble.ts`
|
|
11
11
|
* embeds `getExportsResolverJS()` as part of the pool preamble so the
|
|
12
12
|
* pre-bundle facet uses identical resolution semantics to the supervisor.
|
|
13
13
|
*
|
|
@@ -37,6 +37,26 @@ export const DEFAULT_ESM_CONDITIONS = ['import', 'module', 'browser', 'default']
|
|
|
37
37
|
/** Default conditions for CJS runtime resolution (user-shell node). */
|
|
38
38
|
export const DEFAULT_CJS_CONDITIONS = ['require', 'node', 'default'];
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* A `package.json#exports` / `#imports` value: a target path, an ordered list
|
|
42
|
+
* of fallbacks to try in turn, or a map keyed by subpath (`"./client"`) or by
|
|
43
|
+
* condition (`"import"`) whose values are the same shape again. `null` is the
|
|
44
|
+
* spec's "this subpath is not exported" marker, and blocks fallback.
|
|
45
|
+
*/
|
|
46
|
+
export type ExportsField =
|
|
47
|
+
| string
|
|
48
|
+
| null
|
|
49
|
+
| ExportsField[]
|
|
50
|
+
| { [key: string]: ExportsField };
|
|
51
|
+
|
|
52
|
+
/** The package.json fields entry-point resolution reads. */
|
|
53
|
+
export interface ResolvablePackageJson {
|
|
54
|
+
exports?: ExportsField;
|
|
55
|
+
imports?: ExportsField;
|
|
56
|
+
main?: string;
|
|
57
|
+
module?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
/**
|
|
41
61
|
* Resolve `package.json#exports` (or `#imports`) per Node spec.
|
|
42
62
|
*
|
|
@@ -46,7 +66,7 @@ export const DEFAULT_CJS_CONDITIONS = ['require', 'node', 'default'];
|
|
|
46
66
|
* @returns Relative path target string, or null if not found / forbidden
|
|
47
67
|
*/
|
|
48
68
|
export function resolveExports(
|
|
49
|
-
exportsField:
|
|
69
|
+
exportsField: ExportsField | undefined,
|
|
50
70
|
subpath: string = '.',
|
|
51
71
|
conditions: string[] = DEFAULT_ESM_CONDITIONS,
|
|
52
72
|
): string | null {
|
|
@@ -123,7 +143,7 @@ export function resolveExports(
|
|
|
123
143
|
* array (Node spec).
|
|
124
144
|
*/
|
|
125
145
|
function resolveConditionValue(
|
|
126
|
-
target:
|
|
146
|
+
target: ExportsField | undefined,
|
|
127
147
|
conditions: string[],
|
|
128
148
|
): string | null {
|
|
129
149
|
if (target === null || target === undefined) return null;
|
|
@@ -162,7 +182,7 @@ function resolveConditionValue(
|
|
|
162
182
|
* itself (caller probes filesystem with extension-list).
|
|
163
183
|
*/
|
|
164
184
|
export function resolvePackageEntry(
|
|
165
|
-
pkg:
|
|
185
|
+
pkg: ResolvablePackageJson,
|
|
166
186
|
subpath: string = '.',
|
|
167
187
|
conditions: string[] = DEFAULT_ESM_CONDITIONS,
|
|
168
188
|
): string | null {
|
package/src/_shared/retry.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { disposeRpcResource } from './rpc-dispose.js';
|
|
27
|
+
import { errorText } from './error-text.js';
|
|
27
28
|
|
|
28
29
|
/** Default retry count AFTER the first attempt (3 = up to 4 total attempts). */
|
|
29
30
|
export const DEFAULT_RETRIES = 3;
|
|
@@ -179,7 +180,7 @@ export async function retryableFetch(
|
|
|
179
180
|
const delayMs = jittered(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
|
|
180
181
|
opts?.onRetry?.(attempt + 1, totalRetries, delayMs, `HTTP ${resp.status}`);
|
|
181
182
|
await new Promise<void>((r) => setTimeout(r, delayMs));
|
|
182
|
-
} catch (e
|
|
183
|
+
} catch (e) {
|
|
183
184
|
if (timer) clearTimeout(timer);
|
|
184
185
|
// Network-level failure (fetch rejection, timeout via AbortError,
|
|
185
186
|
// DNS, connection reset). Retry the same way as a 5xx.
|
|
@@ -189,7 +190,8 @@ export async function retryableFetch(
|
|
|
189
190
|
throw e;
|
|
190
191
|
}
|
|
191
192
|
const delayMs = jittered(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
|
|
192
|
-
const
|
|
193
|
+
const isAbort = typeof e === 'object' && e !== null && 'name' in e && e.name === 'AbortError';
|
|
194
|
+
const reason = isAbort ? 'timeout' : errorText(e);
|
|
193
195
|
opts?.onRetry?.(attempt + 1, totalRetries, delayMs, reason);
|
|
194
196
|
await new Promise<void>((r) => setTimeout(r, delayMs));
|
|
195
197
|
}
|