@secure-exec/browser 0.0.0-agentos-dylib-base.edaa4a4
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 +6 -0
- package/dist/child-process-bridge.d.ts +25 -0
- package/dist/child-process-bridge.js +50 -0
- package/dist/converged-base64.d.ts +2 -0
- package/dist/converged-base64.js +41 -0
- package/dist/converged-dgram-bridge.d.ts +11 -0
- package/dist/converged-dgram-bridge.js +147 -0
- package/dist/converged-driver-setup.d.ts +22 -0
- package/dist/converged-driver-setup.js +72 -0
- package/dist/converged-execution-host-bridge.d.ts +7 -0
- package/dist/converged-execution-host-bridge.js +85 -0
- package/dist/converged-executor-session.d.ts +60 -0
- package/dist/converged-executor-session.js +127 -0
- package/dist/converged-fs-bridge.d.ts +42 -0
- package/dist/converged-fs-bridge.js +245 -0
- package/dist/converged-module-servicer.d.ts +8 -0
- package/dist/converged-module-servicer.js +79 -0
- package/dist/converged-net-bridge.d.ts +28 -0
- package/dist/converged-net-bridge.js +155 -0
- package/dist/converged-permissions.d.ts +9 -0
- package/dist/converged-permissions.js +46 -0
- package/dist/converged-sync-bridge-handler.d.ts +47 -0
- package/dist/converged-sync-bridge-handler.js +140 -0
- package/dist/converged-sync-bridge-router.d.ts +33 -0
- package/dist/converged-sync-bridge-router.js +41 -0
- package/dist/driver.d.ts +91 -0
- package/dist/driver.js +386 -0
- package/dist/encoding.d.ts +4 -0
- package/dist/encoding.js +102 -0
- package/dist/generated/util-polyfill.d.ts +1 -0
- package/dist/generated/util-polyfill.js +2 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/kernel-backed-filesystem.d.ts +33 -0
- package/dist/kernel-backed-filesystem.js +205 -0
- package/dist/os-filesystem.d.ts +47 -0
- package/dist/os-filesystem.js +409 -0
- package/dist/permission-validation.d.ts +15 -0
- package/dist/permission-validation.js +62 -0
- package/dist/root-filesystem-from-vfs.d.ts +13 -0
- package/dist/root-filesystem-from-vfs.js +95 -0
- package/dist/runtime-driver.d.ts +66 -0
- package/dist/runtime-driver.js +611 -0
- package/dist/runtime.d.ts +248 -0
- package/dist/runtime.js +2296 -0
- package/dist/sidecar-wasm-module.d.ts +62 -0
- package/dist/sidecar-wasm-module.js +28 -0
- package/dist/sidecar-worker-protocol.d.ts +14 -0
- package/dist/sidecar-worker-protocol.js +9 -0
- package/dist/sidecar-worker.d.ts +19 -0
- package/dist/sidecar-worker.js +63 -0
- package/dist/signals.d.ts +13 -0
- package/dist/signals.js +89 -0
- package/dist/sync-bridge.d.ts +50 -0
- package/dist/sync-bridge.js +93 -0
- package/dist/wasi-polyfill.d.ts +1 -0
- package/dist/wasi-polyfill.js +2154 -0
- package/dist/worker-adapter.d.ts +21 -0
- package/dist/worker-adapter.js +41 -0
- package/dist/worker-protocol.d.ts +104 -0
- package/dist/worker-protocol.js +1 -0
- package/dist/worker-sidecar-client.d.ts +71 -0
- package/dist/worker-sidecar-client.js +152 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +2125 -0
- package/package.json +111 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Converged sync-bridge handler.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the legacy `runtime-driver.ts` `handleSyncBridgeOperation` (which
|
|
4
|
+
// serviced guest syscalls against an in-process TypeScript kernel) with one
|
|
5
|
+
// that routes every guest syscall to the wasm sidecar (`crates/sidecar-browser`)
|
|
6
|
+
// over the wire protocol. The kernel becomes the single enforcement point for
|
|
7
|
+
// both native and browser; the legacy fail-open TS executor is retired once this
|
|
8
|
+
// is the live path.
|
|
9
|
+
//
|
|
10
|
+
// The handler is synchronous: it encodes a wire frame, calls the injected
|
|
11
|
+
// synchronous `pushFrame` (the wasm `BrowserSidecarWasm.pushFrame`), decodes the
|
|
12
|
+
// response frame, and returns the sync-bridge response the worker writes back to
|
|
13
|
+
// the SAB. It is unit-tested with a fake synchronous `pushFrame`, no wasm needed.
|
|
14
|
+
import { decodeProtocolFramePayload, encodeProtocolFramePayload, HostProtocolFrameFactory, } from "@secure-exec/core/protocol-frames";
|
|
15
|
+
import { convergedFilesystemRequestPayload, convergedFilesystemSyncResponse, isSingleCallFilesystemOperation, wireStatToDirEntry, } from "./converged-fs-bridge.js";
|
|
16
|
+
import { convergedNetRequestPayload, convergedNetSyncResponse, isConvergedNetBridgeOperation, } from "./converged-net-bridge.js";
|
|
17
|
+
import { convergedDgramInlineResponse, convergedDgramRequestPayload, convergedDgramSyncResponse, dgramOperationUsesKernel, isConvergedDgramBridgeOperation, } from "./converged-dgram-bridge.js";
|
|
18
|
+
import { SYNC_BRIDGE_KIND_JSON } from "./sync-bridge.js";
|
|
19
|
+
/**
|
|
20
|
+
* Production transport: encodes a request frame, calls the synchronous wasm
|
|
21
|
+
* `pushFrame`, and decodes the response frame.
|
|
22
|
+
*/
|
|
23
|
+
export class PushFrameSidecarTransport {
|
|
24
|
+
frames = new HostProtocolFrameFactory();
|
|
25
|
+
pushFrame;
|
|
26
|
+
ownership;
|
|
27
|
+
codec;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.pushFrame = options.pushFrame;
|
|
30
|
+
this.ownership = options.ownership;
|
|
31
|
+
this.codec = options.codec ?? "bare";
|
|
32
|
+
}
|
|
33
|
+
sendRequest(payload) {
|
|
34
|
+
const frame = this.frames.createRequestFrame({
|
|
35
|
+
ownership: this.ownership,
|
|
36
|
+
payload,
|
|
37
|
+
});
|
|
38
|
+
const responseBytes = this.pushFrame(encodeProtocolFramePayload(frame, this.codec));
|
|
39
|
+
const decoded = decodeProtocolFramePayload(responseBytes, this.codec);
|
|
40
|
+
if (decoded.frame_type !== "response") {
|
|
41
|
+
throw new Error(`converged sync bridge expected a response frame, got ${decoded.frame_type}`);
|
|
42
|
+
}
|
|
43
|
+
if (decoded.payload.type === "rejected") {
|
|
44
|
+
const message = decoded.payload.message;
|
|
45
|
+
const error = new Error(message);
|
|
46
|
+
// Kernel failures carry a leading POSIX errno ("EACCES: ...") in the
|
|
47
|
+
// message; surface that as the guest-visible error code (Node/POSIX
|
|
48
|
+
// semantics) rather than the generic wire rejection code.
|
|
49
|
+
const errno = /^(E[A-Z0-9_]+):/.exec(message);
|
|
50
|
+
error.code = errno
|
|
51
|
+
? errno[1]
|
|
52
|
+
: decoded.payload.code;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
return decoded.payload;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class ConvergedSyncBridgeHandler {
|
|
59
|
+
transport;
|
|
60
|
+
executionId;
|
|
61
|
+
constructor(options) {
|
|
62
|
+
this.transport = options.transport;
|
|
63
|
+
this.executionId = options.executionId;
|
|
64
|
+
}
|
|
65
|
+
/** True if this handler services `operation` against the wasm sidecar. */
|
|
66
|
+
handles(operation) {
|
|
67
|
+
return (operation === "fs.readDir" ||
|
|
68
|
+
isSingleCallFilesystemOperation(operation) ||
|
|
69
|
+
isConvergedNetBridgeOperation(operation) ||
|
|
70
|
+
isConvergedDgramBridgeOperation(operation));
|
|
71
|
+
}
|
|
72
|
+
handle(operation, args) {
|
|
73
|
+
if (operation === "fs.readDir") {
|
|
74
|
+
return this.readDir(String(args[0]));
|
|
75
|
+
}
|
|
76
|
+
if (isSingleCallFilesystemOperation(operation)) {
|
|
77
|
+
const result = this.callFilesystem(convergedFilesystemRequestPayload(operation, args));
|
|
78
|
+
return convergedFilesystemSyncResponse(operation, result);
|
|
79
|
+
}
|
|
80
|
+
if (isConvergedNetBridgeOperation(operation)) {
|
|
81
|
+
const result = this.callKernel(convergedNetRequestPayload(operation, args, this.executionId));
|
|
82
|
+
return convergedNetSyncResponse(result);
|
|
83
|
+
}
|
|
84
|
+
if (isConvergedDgramBridgeOperation(operation)) {
|
|
85
|
+
if (!dgramOperationUsesKernel(operation)) {
|
|
86
|
+
return convergedDgramInlineResponse(operation);
|
|
87
|
+
}
|
|
88
|
+
const result = this.callKernel(convergedDgramRequestPayload(operation, args, this.executionId));
|
|
89
|
+
return convergedDgramSyncResponse(operation, decodeKernelJson(result));
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`converged sync bridge handler does not service ${operation}`);
|
|
92
|
+
}
|
|
93
|
+
readDir(path) {
|
|
94
|
+
// The wire `read_dir` returns names only; recover Dirent types with a
|
|
95
|
+
// per-entry `lstat` so `readdir(withFileTypes)` stays faithful.
|
|
96
|
+
const listing = this.callFilesystem({
|
|
97
|
+
type: "guest_filesystem_call",
|
|
98
|
+
operation: "read_dir",
|
|
99
|
+
path,
|
|
100
|
+
});
|
|
101
|
+
const entries = (listing.entries ?? []).map((name) => {
|
|
102
|
+
const child = this.callFilesystem({
|
|
103
|
+
type: "guest_filesystem_call",
|
|
104
|
+
operation: "lstat",
|
|
105
|
+
path: joinPath(path, name),
|
|
106
|
+
});
|
|
107
|
+
if (!child.stat) {
|
|
108
|
+
throw new Error(`lstat for ${name} returned no stat`);
|
|
109
|
+
}
|
|
110
|
+
return wireStatToDirEntry(name, child.stat);
|
|
111
|
+
});
|
|
112
|
+
return { kind: SYNC_BRIDGE_KIND_JSON, value: entries };
|
|
113
|
+
}
|
|
114
|
+
callFilesystem(payload) {
|
|
115
|
+
const response = this.transport.sendRequest(payload);
|
|
116
|
+
if (response.type !== "guest_filesystem_result") {
|
|
117
|
+
throw new Error(`expected guest_filesystem_result, got ${response.type}`);
|
|
118
|
+
}
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
callKernel(payload) {
|
|
122
|
+
const response = this.transport.sendRequest(payload);
|
|
123
|
+
if (response.type !== "guest_kernel_result") {
|
|
124
|
+
throw new Error(`expected guest_kernel_result, got ${response.type}`);
|
|
125
|
+
}
|
|
126
|
+
return response;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function joinPath(parent, child) {
|
|
130
|
+
if (parent.endsWith("/")) {
|
|
131
|
+
return `${parent}${child}`;
|
|
132
|
+
}
|
|
133
|
+
return `${parent}/${child}`;
|
|
134
|
+
}
|
|
135
|
+
function decodeKernelJson(result) {
|
|
136
|
+
if (result.payload.byteLength === 0) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return JSON.parse(new TextDecoder().decode(new Uint8Array(result.payload)));
|
|
140
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ConvergedSyncResponse } from "./converged-fs-bridge.js";
|
|
2
|
+
import type { ConvergedSyncBridgeHandler } from "./converged-sync-bridge-handler.js";
|
|
3
|
+
/** Services a sync-bridge operation not yet handled by the converged path. */
|
|
4
|
+
export type LegacySyncBridgeServicer = (operation: string, args: readonly unknown[]) => Promise<ConvergedSyncResponse>;
|
|
5
|
+
/**
|
|
6
|
+
* An async converged servicer (e.g. module resolution) that routes its
|
|
7
|
+
* operations to the kernel but cannot be synchronous because it walks the
|
|
8
|
+
* filesystem. Tried after the sync handler, before the legacy fallback.
|
|
9
|
+
*/
|
|
10
|
+
export interface AsyncConvergedServicer {
|
|
11
|
+
handles(operation: string): boolean;
|
|
12
|
+
handle(operation: string, args: readonly unknown[]): Promise<ConvergedSyncResponse>;
|
|
13
|
+
}
|
|
14
|
+
export interface ConvergedSyncBridgeRouterOptions {
|
|
15
|
+
handler: ConvergedSyncBridgeHandler;
|
|
16
|
+
legacy: LegacySyncBridgeServicer;
|
|
17
|
+
asyncServicers?: readonly AsyncConvergedServicer[];
|
|
18
|
+
}
|
|
19
|
+
export declare class ConvergedSyncBridgeRouter {
|
|
20
|
+
private readonly handler;
|
|
21
|
+
private readonly legacy;
|
|
22
|
+
private readonly asyncServicers;
|
|
23
|
+
constructor(options: ConvergedSyncBridgeRouterOptions);
|
|
24
|
+
/**
|
|
25
|
+
* Route one sync-bridge operation: sync converged handler (fs/net/dns) first,
|
|
26
|
+
* then any async converged servicers (module resolution), then the legacy
|
|
27
|
+
* fallback for families not yet converged. Returns a promise either way so
|
|
28
|
+
* callers have a single await point.
|
|
29
|
+
*/
|
|
30
|
+
route(operation: string, args: readonly unknown[]): Promise<ConvergedSyncResponse>;
|
|
31
|
+
/** True once every sync-bridge operation routes to a converged servicer. */
|
|
32
|
+
static isFullyConverged(handler: ConvergedSyncBridgeHandler, operations: readonly string[], asyncServicers?: readonly AsyncConvergedServicer[]): boolean;
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Converged sync-bridge router.
|
|
2
|
+
//
|
|
3
|
+
// The migration seam between the legacy in-process TS-kernel sync-bridge
|
|
4
|
+
// servicer (`runtime-driver.ts` `handleSyncBridgeOperation`) and the converged
|
|
5
|
+
// wasm-kernel handler (`ConvergedSyncBridgeHandler`). Operations the converged
|
|
6
|
+
// handler services (fs.* / net.* / dns.*) go to the wasm sidecar; everything not
|
|
7
|
+
// yet converged (module.* / child_process.* / dgram.* / process.signal_state)
|
|
8
|
+
// falls back to the legacy servicer. As each family is converged the fallback
|
|
9
|
+
// shrinks; when it is empty the legacy servicer is deleted (spec slice 5).
|
|
10
|
+
export class ConvergedSyncBridgeRouter {
|
|
11
|
+
handler;
|
|
12
|
+
legacy;
|
|
13
|
+
asyncServicers;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.handler = options.handler;
|
|
16
|
+
this.legacy = options.legacy;
|
|
17
|
+
this.asyncServicers = options.asyncServicers ?? [];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Route one sync-bridge operation: sync converged handler (fs/net/dns) first,
|
|
21
|
+
* then any async converged servicers (module resolution), then the legacy
|
|
22
|
+
* fallback for families not yet converged. Returns a promise either way so
|
|
23
|
+
* callers have a single await point.
|
|
24
|
+
*/
|
|
25
|
+
async route(operation, args) {
|
|
26
|
+
if (this.handler.handles(operation)) {
|
|
27
|
+
return this.handler.handle(operation, args);
|
|
28
|
+
}
|
|
29
|
+
for (const servicer of this.asyncServicers) {
|
|
30
|
+
if (servicer.handles(operation)) {
|
|
31
|
+
return servicer.handle(operation, args);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return this.legacy(operation, args);
|
|
35
|
+
}
|
|
36
|
+
/** True once every sync-bridge operation routes to a converged servicer. */
|
|
37
|
+
static isFullyConverged(handler, operations, asyncServicers = []) {
|
|
38
|
+
return operations.every((operation) => handler.handles(operation) ||
|
|
39
|
+
asyncServicers.some((servicer) => servicer.handles(operation)));
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { NetworkAdapter, Permissions, SystemDriver, VirtualFileSystem } from "./runtime.js";
|
|
2
|
+
import { createCommandExecutorStub, createFsStub, createNetworkStub } from "./runtime.js";
|
|
3
|
+
export interface BrowserRuntimeSystemOptions {
|
|
4
|
+
filesystem: "opfs" | "memory";
|
|
5
|
+
networkEnabled: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* VFS backed by the Origin Private File System (OPFS) API. Falls back to
|
|
9
|
+
* InMemoryFileSystem when OPFS is unavailable. Rename is not supported
|
|
10
|
+
* (throws ENOSYS) since OPFS doesn't provide atomic rename.
|
|
11
|
+
*/
|
|
12
|
+
export declare class OpfsFileSystem implements VirtualFileSystem {
|
|
13
|
+
private rootPromise;
|
|
14
|
+
constructor();
|
|
15
|
+
private getDirHandle;
|
|
16
|
+
private getFileHandle;
|
|
17
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
18
|
+
readTextFile(path: string): Promise<string>;
|
|
19
|
+
readDir(path: string): Promise<string[]>;
|
|
20
|
+
readDirWithTypes(path: string): Promise<Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
isDirectory: boolean;
|
|
23
|
+
}>>;
|
|
24
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
25
|
+
createDir(path: string): Promise<void>;
|
|
26
|
+
mkdir(path: string, _options?: {
|
|
27
|
+
recursive?: boolean;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
exists(path: string): Promise<boolean>;
|
|
30
|
+
stat(path: string): Promise<{
|
|
31
|
+
mode: number;
|
|
32
|
+
size: number;
|
|
33
|
+
blocks: number;
|
|
34
|
+
dev: number;
|
|
35
|
+
rdev: number;
|
|
36
|
+
isDirectory: boolean;
|
|
37
|
+
isSymbolicLink: boolean;
|
|
38
|
+
atimeMs: number;
|
|
39
|
+
mtimeMs: number;
|
|
40
|
+
ctimeMs: number;
|
|
41
|
+
birthtimeMs: number;
|
|
42
|
+
ino: number;
|
|
43
|
+
nlink: number;
|
|
44
|
+
uid: number;
|
|
45
|
+
gid: number;
|
|
46
|
+
}>;
|
|
47
|
+
removeFile(path: string): Promise<void>;
|
|
48
|
+
removeDir(path: string): Promise<void>;
|
|
49
|
+
rename(_oldPath: string, _newPath: string): Promise<void>;
|
|
50
|
+
symlink(_target: string, _linkPath: string): Promise<void>;
|
|
51
|
+
readlink(_path: string): Promise<string>;
|
|
52
|
+
lstat(path: string): Promise<{
|
|
53
|
+
mode: number;
|
|
54
|
+
size: number;
|
|
55
|
+
blocks: number;
|
|
56
|
+
dev: number;
|
|
57
|
+
rdev: number;
|
|
58
|
+
isDirectory: boolean;
|
|
59
|
+
isSymbolicLink: boolean;
|
|
60
|
+
atimeMs: number;
|
|
61
|
+
mtimeMs: number;
|
|
62
|
+
ctimeMs: number;
|
|
63
|
+
birthtimeMs: number;
|
|
64
|
+
ino: number;
|
|
65
|
+
nlink: number;
|
|
66
|
+
uid: number;
|
|
67
|
+
gid: number;
|
|
68
|
+
}>;
|
|
69
|
+
link(_oldPath: string, _newPath: string): Promise<void>;
|
|
70
|
+
chmod(_path: string, _mode: number): Promise<void>;
|
|
71
|
+
chown(_path: string, _uid: number, _gid: number): Promise<void>;
|
|
72
|
+
utimes(_path: string, _atime: number, _mtime: number): Promise<void>;
|
|
73
|
+
truncate(path: string, length: number): Promise<void>;
|
|
74
|
+
realpath(path: string): Promise<string>;
|
|
75
|
+
pread(path: string, offset: number, length: number): Promise<Uint8Array>;
|
|
76
|
+
pwrite(path: string, offset: number, data: Uint8Array): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
export interface BrowserDriverOptions {
|
|
79
|
+
filesystem?: "opfs" | "memory";
|
|
80
|
+
permissions?: Permissions;
|
|
81
|
+
useDefaultNetwork?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** Create an OPFS-backed filesystem, falling back to in-memory if OPFS is unavailable. */
|
|
84
|
+
export declare function createOpfsFileSystem(): Promise<VirtualFileSystem>;
|
|
85
|
+
/** Network adapter that delegates to the browser's native `fetch`. DNS and http2 are unsupported. */
|
|
86
|
+
export declare function createBrowserNetworkAdapter(): NetworkAdapter;
|
|
87
|
+
/** Recover runtime-driver options from a browser SystemDriver instance. */
|
|
88
|
+
export declare function getBrowserSystemDriverOptions(systemDriver: SystemDriver): BrowserRuntimeSystemOptions;
|
|
89
|
+
/** Assemble a browser-side SystemDriver with permission-wrapped adapters. */
|
|
90
|
+
export declare function createBrowserDriver(options?: BrowserDriverOptions): Promise<SystemDriver>;
|
|
91
|
+
export { createCommandExecutorStub, createFsStub, createNetworkStub };
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { bytesToBase64 } from "./encoding.js";
|
|
2
|
+
import { createCommandExecutorStub, createEnosysError, createFsStub, createInMemoryFileSystem, createNetworkStub, wrapFileSystem, wrapNetworkAdapter, } from "./runtime.js";
|
|
3
|
+
const S_IFREG = 0o100000;
|
|
4
|
+
const S_IFDIR = 0o040000;
|
|
5
|
+
const BROWSER_SYSTEM_DRIVER_OPTIONS = Symbol.for("secure-exec.browserSystemDriverOptions");
|
|
6
|
+
const LOOPBACK_DNS_NAMES = new Set([
|
|
7
|
+
"localhost",
|
|
8
|
+
"ip4-localhost",
|
|
9
|
+
"ip4-loopback",
|
|
10
|
+
]);
|
|
11
|
+
const LOOPBACK_IPV6_DNS_NAMES = new Set(["ip6-localhost", "ip6-loopback"]);
|
|
12
|
+
function isIpv4Literal(hostname) {
|
|
13
|
+
const parts = hostname.split(".");
|
|
14
|
+
return (parts.length === 4 &&
|
|
15
|
+
parts.every((part) => {
|
|
16
|
+
if (!/^\d+$/.test(part))
|
|
17
|
+
return false;
|
|
18
|
+
const value = Number(part);
|
|
19
|
+
return value >= 0 && value <= 255 && String(value) === part;
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
function isIpv6Literal(hostname) {
|
|
23
|
+
return hostname.includes(":") && /^[0-9a-fA-F:.]+$/.test(hostname);
|
|
24
|
+
}
|
|
25
|
+
function browserLocalDnsLookup(hostname) {
|
|
26
|
+
const normalized = hostname.trim().toLowerCase();
|
|
27
|
+
if (LOOPBACK_DNS_NAMES.has(normalized)) {
|
|
28
|
+
return { address: "127.0.0.1", family: 4 };
|
|
29
|
+
}
|
|
30
|
+
if (LOOPBACK_IPV6_DNS_NAMES.has(normalized)) {
|
|
31
|
+
return { address: "::1", family: 6 };
|
|
32
|
+
}
|
|
33
|
+
if (isIpv4Literal(normalized)) {
|
|
34
|
+
return { address: normalized, family: 4 };
|
|
35
|
+
}
|
|
36
|
+
if (isIpv6Literal(normalized)) {
|
|
37
|
+
return { address: normalized, family: 6 };
|
|
38
|
+
}
|
|
39
|
+
return { error: "DNS not supported in browser", code: "ENOSYS" };
|
|
40
|
+
}
|
|
41
|
+
function normalizePath(path) {
|
|
42
|
+
if (!path)
|
|
43
|
+
return "/";
|
|
44
|
+
let normalized = path.startsWith("/") ? path : `/${path}`;
|
|
45
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
46
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
47
|
+
normalized = normalized.slice(0, -1);
|
|
48
|
+
}
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
function splitPath(path) {
|
|
52
|
+
const normalized = normalizePath(path);
|
|
53
|
+
return normalized === "/" ? [] : normalized.slice(1).split("/");
|
|
54
|
+
}
|
|
55
|
+
function dirname(path) {
|
|
56
|
+
const parts = splitPath(path);
|
|
57
|
+
if (parts.length <= 1)
|
|
58
|
+
return "/";
|
|
59
|
+
return `/${parts.slice(0, -1).join("/")}`;
|
|
60
|
+
}
|
|
61
|
+
async function getRootHandle() {
|
|
62
|
+
if (!("storage" in navigator) || !("getDirectory" in navigator.storage)) {
|
|
63
|
+
throw createEnosysError("opfs");
|
|
64
|
+
}
|
|
65
|
+
return navigator.storage.getDirectory();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* VFS backed by the Origin Private File System (OPFS) API. Falls back to
|
|
69
|
+
* InMemoryFileSystem when OPFS is unavailable. Rename is not supported
|
|
70
|
+
* (throws ENOSYS) since OPFS doesn't provide atomic rename.
|
|
71
|
+
*/
|
|
72
|
+
export class OpfsFileSystem {
|
|
73
|
+
rootPromise;
|
|
74
|
+
constructor() {
|
|
75
|
+
this.rootPromise = getRootHandle();
|
|
76
|
+
}
|
|
77
|
+
async getDirHandle(path, create = false) {
|
|
78
|
+
const root = await this.rootPromise;
|
|
79
|
+
const parts = splitPath(path);
|
|
80
|
+
let current = root;
|
|
81
|
+
for (const part of parts) {
|
|
82
|
+
current = await current.getDirectoryHandle(part, { create });
|
|
83
|
+
}
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
async getFileHandle(path, create = false) {
|
|
87
|
+
const normalized = normalizePath(path);
|
|
88
|
+
const parent = dirname(normalized);
|
|
89
|
+
const name = normalized.split("/").pop() || "";
|
|
90
|
+
const dir = await this.getDirHandle(parent, create);
|
|
91
|
+
return dir.getFileHandle(name, { create });
|
|
92
|
+
}
|
|
93
|
+
async readFile(path) {
|
|
94
|
+
const handle = await this.getFileHandle(path);
|
|
95
|
+
const file = await handle.getFile();
|
|
96
|
+
const buffer = await file.arrayBuffer();
|
|
97
|
+
return new Uint8Array(buffer);
|
|
98
|
+
}
|
|
99
|
+
async readTextFile(path) {
|
|
100
|
+
const handle = await this.getFileHandle(path);
|
|
101
|
+
const file = await handle.getFile();
|
|
102
|
+
return file.text();
|
|
103
|
+
}
|
|
104
|
+
async readDir(path) {
|
|
105
|
+
const dir = await this.getDirHandle(path);
|
|
106
|
+
const entries = [];
|
|
107
|
+
for await (const [name] of dir.entries()) {
|
|
108
|
+
entries.push(name);
|
|
109
|
+
}
|
|
110
|
+
return entries;
|
|
111
|
+
}
|
|
112
|
+
async readDirWithTypes(path) {
|
|
113
|
+
const dir = await this.getDirHandle(path);
|
|
114
|
+
const entries = [];
|
|
115
|
+
for await (const [name, handle] of dir.entries()) {
|
|
116
|
+
entries.push({
|
|
117
|
+
name,
|
|
118
|
+
isDirectory: handle.kind === "directory",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
async writeFile(path, content) {
|
|
124
|
+
const normalized = normalizePath(path);
|
|
125
|
+
await this.mkdir(dirname(normalized));
|
|
126
|
+
const handle = await this.getFileHandle(normalized, true);
|
|
127
|
+
const writable = await handle.createWritable();
|
|
128
|
+
if (typeof content === "string") {
|
|
129
|
+
await writable.write(content);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
await writable.write(content);
|
|
133
|
+
}
|
|
134
|
+
await writable.close();
|
|
135
|
+
}
|
|
136
|
+
async createDir(path) {
|
|
137
|
+
const normalized = normalizePath(path);
|
|
138
|
+
const parent = dirname(normalized);
|
|
139
|
+
await this.getDirHandle(parent, false);
|
|
140
|
+
await this.getDirHandle(normalized, true);
|
|
141
|
+
}
|
|
142
|
+
async mkdir(path, _options) {
|
|
143
|
+
const parts = splitPath(path);
|
|
144
|
+
let current = "";
|
|
145
|
+
for (const part of parts) {
|
|
146
|
+
current += `/${part}`;
|
|
147
|
+
await this.getDirHandle(current, true);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async exists(path) {
|
|
151
|
+
try {
|
|
152
|
+
await this.getFileHandle(path);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
try {
|
|
157
|
+
await this.getDirHandle(path);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async stat(path) {
|
|
166
|
+
try {
|
|
167
|
+
const handle = await this.getFileHandle(path);
|
|
168
|
+
const file = await handle.getFile();
|
|
169
|
+
return {
|
|
170
|
+
mode: S_IFREG | 0o644,
|
|
171
|
+
size: file.size,
|
|
172
|
+
blocks: file.size === 0 ? 0 : Math.ceil(file.size / 512),
|
|
173
|
+
dev: 1,
|
|
174
|
+
rdev: 0,
|
|
175
|
+
isDirectory: false,
|
|
176
|
+
isSymbolicLink: false,
|
|
177
|
+
atimeMs: file.lastModified,
|
|
178
|
+
mtimeMs: file.lastModified,
|
|
179
|
+
ctimeMs: file.lastModified,
|
|
180
|
+
birthtimeMs: file.lastModified,
|
|
181
|
+
ino: 0,
|
|
182
|
+
nlink: 1,
|
|
183
|
+
uid: 0,
|
|
184
|
+
gid: 0,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
const normalized = normalizePath(path);
|
|
189
|
+
try {
|
|
190
|
+
await this.getDirHandle(normalized);
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
return {
|
|
193
|
+
mode: S_IFDIR | 0o755,
|
|
194
|
+
size: 4096,
|
|
195
|
+
blocks: 8,
|
|
196
|
+
dev: 1,
|
|
197
|
+
rdev: 0,
|
|
198
|
+
isDirectory: true,
|
|
199
|
+
isSymbolicLink: false,
|
|
200
|
+
atimeMs: now,
|
|
201
|
+
mtimeMs: now,
|
|
202
|
+
ctimeMs: now,
|
|
203
|
+
birthtimeMs: now,
|
|
204
|
+
ino: 0,
|
|
205
|
+
nlink: 2,
|
|
206
|
+
uid: 0,
|
|
207
|
+
gid: 0,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async removeFile(path) {
|
|
216
|
+
const normalized = normalizePath(path);
|
|
217
|
+
const parent = dirname(normalized);
|
|
218
|
+
const name = normalized.split("/").pop() || "";
|
|
219
|
+
const dir = await this.getDirHandle(parent);
|
|
220
|
+
await dir.removeEntry(name);
|
|
221
|
+
}
|
|
222
|
+
async removeDir(path) {
|
|
223
|
+
const normalized = normalizePath(path);
|
|
224
|
+
if (normalized === "/") {
|
|
225
|
+
throw new Error("EPERM: operation not permitted, rmdir '/'");
|
|
226
|
+
}
|
|
227
|
+
const parent = dirname(normalized);
|
|
228
|
+
const name = normalized.split("/").pop() || "";
|
|
229
|
+
const dir = await this.getDirHandle(parent);
|
|
230
|
+
await dir.removeEntry(name);
|
|
231
|
+
}
|
|
232
|
+
async rename(_oldPath, _newPath) {
|
|
233
|
+
throw createEnosysError("rename");
|
|
234
|
+
}
|
|
235
|
+
async symlink(_target, _linkPath) {
|
|
236
|
+
throw createEnosysError("symlink");
|
|
237
|
+
}
|
|
238
|
+
async readlink(_path) {
|
|
239
|
+
throw createEnosysError("readlink");
|
|
240
|
+
}
|
|
241
|
+
async lstat(path) {
|
|
242
|
+
return this.stat(path);
|
|
243
|
+
}
|
|
244
|
+
async link(_oldPath, _newPath) {
|
|
245
|
+
throw createEnosysError("link");
|
|
246
|
+
}
|
|
247
|
+
async chmod(_path, _mode) {
|
|
248
|
+
// No-op: OPFS does not support POSIX permissions
|
|
249
|
+
}
|
|
250
|
+
async chown(_path, _uid, _gid) {
|
|
251
|
+
// No-op: OPFS does not support POSIX ownership
|
|
252
|
+
}
|
|
253
|
+
async utimes(_path, _atime, _mtime) {
|
|
254
|
+
// No-op: OPFS does not support timestamp manipulation
|
|
255
|
+
}
|
|
256
|
+
async truncate(path, length) {
|
|
257
|
+
const handle = await this.getFileHandle(path);
|
|
258
|
+
const writable = await handle.createWritable({ keepExistingData: true });
|
|
259
|
+
await writable.truncate(length);
|
|
260
|
+
await writable.close();
|
|
261
|
+
}
|
|
262
|
+
async realpath(path) {
|
|
263
|
+
const normalized = normalizePath(path);
|
|
264
|
+
if (await this.exists(normalized))
|
|
265
|
+
return normalized;
|
|
266
|
+
throw new Error(`ENOENT: no such file or directory, realpath '${normalized}'`);
|
|
267
|
+
}
|
|
268
|
+
async pread(path, offset, length) {
|
|
269
|
+
const data = await this.readFile(path);
|
|
270
|
+
return data.slice(offset, offset + length);
|
|
271
|
+
}
|
|
272
|
+
async pwrite(path, offset, data) {
|
|
273
|
+
const content = await this.readFile(path);
|
|
274
|
+
const endPos = offset + data.length;
|
|
275
|
+
const newContent = new Uint8Array(Math.max(content.length, endPos));
|
|
276
|
+
newContent.set(content);
|
|
277
|
+
newContent.set(data, offset);
|
|
278
|
+
await this.writeFile(path, newContent);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Create an OPFS-backed filesystem, falling back to in-memory if OPFS is unavailable. */
|
|
282
|
+
export async function createOpfsFileSystem() {
|
|
283
|
+
if (!("storage" in navigator) ||
|
|
284
|
+
typeof navigator.storage.getDirectory !== "function") {
|
|
285
|
+
return createInMemoryFileSystem();
|
|
286
|
+
}
|
|
287
|
+
return new OpfsFileSystem();
|
|
288
|
+
}
|
|
289
|
+
/** Network adapter that delegates to the browser's native `fetch`. DNS and http2 are unsupported. */
|
|
290
|
+
export function createBrowserNetworkAdapter() {
|
|
291
|
+
return {
|
|
292
|
+
async fetch(url, options) {
|
|
293
|
+
const response = await fetch(url, {
|
|
294
|
+
method: options?.method || "GET",
|
|
295
|
+
headers: options?.headers,
|
|
296
|
+
body: options?.body,
|
|
297
|
+
});
|
|
298
|
+
const headers = {};
|
|
299
|
+
response.headers.forEach((v, k) => {
|
|
300
|
+
headers[k] = v;
|
|
301
|
+
});
|
|
302
|
+
const contentType = response.headers.get("content-type") || "";
|
|
303
|
+
const isBinary = contentType.includes("octet-stream") ||
|
|
304
|
+
contentType.includes("gzip") ||
|
|
305
|
+
url.endsWith(".tgz");
|
|
306
|
+
let body;
|
|
307
|
+
if (isBinary) {
|
|
308
|
+
const buffer = await response.arrayBuffer();
|
|
309
|
+
body = bytesToBase64(new Uint8Array(buffer));
|
|
310
|
+
headers["x-body-encoding"] = "base64";
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
body = await response.text();
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
ok: response.ok,
|
|
317
|
+
status: response.status,
|
|
318
|
+
statusText: response.statusText,
|
|
319
|
+
headers,
|
|
320
|
+
body,
|
|
321
|
+
url: response.url,
|
|
322
|
+
redirected: response.redirected,
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
async dnsLookup(hostname) {
|
|
326
|
+
return browserLocalDnsLookup(hostname);
|
|
327
|
+
},
|
|
328
|
+
async httpRequest(url, options) {
|
|
329
|
+
const response = await fetch(url, {
|
|
330
|
+
method: options?.method || "GET",
|
|
331
|
+
headers: options?.headers,
|
|
332
|
+
body: options?.body,
|
|
333
|
+
});
|
|
334
|
+
const headers = {};
|
|
335
|
+
response.headers.forEach((v, k) => {
|
|
336
|
+
headers[k] = v;
|
|
337
|
+
});
|
|
338
|
+
const body = await response.text();
|
|
339
|
+
return {
|
|
340
|
+
status: response.status,
|
|
341
|
+
statusText: response.statusText,
|
|
342
|
+
headers,
|
|
343
|
+
body,
|
|
344
|
+
url: response.url,
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** Recover runtime-driver options from a browser SystemDriver instance. */
|
|
350
|
+
export function getBrowserSystemDriverOptions(systemDriver) {
|
|
351
|
+
const options = systemDriver[BROWSER_SYSTEM_DRIVER_OPTIONS];
|
|
352
|
+
if (options) {
|
|
353
|
+
return options;
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
filesystem: "opfs",
|
|
357
|
+
networkEnabled: Boolean(systemDriver.network),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** Assemble a browser-side SystemDriver with permission-wrapped adapters. */
|
|
361
|
+
export async function createBrowserDriver(options = {}) {
|
|
362
|
+
const permissions = options.permissions;
|
|
363
|
+
const filesystemMode = options.filesystem ?? "opfs";
|
|
364
|
+
const filesystem = filesystemMode === "memory"
|
|
365
|
+
? createInMemoryFileSystem()
|
|
366
|
+
: await createOpfsFileSystem();
|
|
367
|
+
const networkAdapter = options.useDefaultNetwork
|
|
368
|
+
? wrapNetworkAdapter(createBrowserNetworkAdapter(), permissions)
|
|
369
|
+
: undefined;
|
|
370
|
+
const systemDriver = {
|
|
371
|
+
filesystem: wrapFileSystem(filesystem, permissions),
|
|
372
|
+
network: networkAdapter,
|
|
373
|
+
commandExecutor: createCommandExecutorStub(),
|
|
374
|
+
permissions,
|
|
375
|
+
runtime: {
|
|
376
|
+
process: {},
|
|
377
|
+
os: {},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
systemDriver[BROWSER_SYSTEM_DRIVER_OPTIONS] = {
|
|
381
|
+
filesystem: filesystemMode,
|
|
382
|
+
networkEnabled: Boolean(networkAdapter),
|
|
383
|
+
};
|
|
384
|
+
return systemDriver;
|
|
385
|
+
}
|
|
386
|
+
export { createCommandExecutorStub, createFsStub, createNetworkStub };
|