@secure-exec/browser 0.1.1-rc.2 → 0.2.0-rc.1
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/dist/driver.d.ts +17 -3
- package/dist/driver.js +21 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/os-filesystem.d.ts +47 -0
- package/dist/os-filesystem.js +384 -0
- package/dist/worker-adapter.d.ts +21 -0
- package/dist/worker-adapter.js +40 -0
- package/dist/worker.js +1 -1
- package/package.json +12 -2
package/dist/driver.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createCommandExecutorStub, createFsStub, createNetworkStub } from "@secure-exec/core";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Permissions, VirtualFileSystem } from "@secure-exec/core";
|
|
3
|
+
import type { NetworkAdapter, SystemDriver } from "@secure-exec/core";
|
|
3
4
|
export interface BrowserRuntimeSystemOptions {
|
|
4
5
|
filesystem: "opfs" | "memory";
|
|
5
6
|
networkEnabled: boolean;
|
|
@@ -23,16 +24,23 @@ export declare class OpfsFileSystem implements VirtualFileSystem {
|
|
|
23
24
|
}>>;
|
|
24
25
|
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
25
26
|
createDir(path: string): Promise<void>;
|
|
26
|
-
mkdir(path: string
|
|
27
|
+
mkdir(path: string, _options?: {
|
|
28
|
+
recursive?: boolean;
|
|
29
|
+
}): Promise<void>;
|
|
27
30
|
exists(path: string): Promise<boolean>;
|
|
28
31
|
stat(path: string): Promise<{
|
|
29
32
|
mode: number;
|
|
30
33
|
size: number;
|
|
31
34
|
isDirectory: boolean;
|
|
35
|
+
isSymbolicLink: boolean;
|
|
32
36
|
atimeMs: number;
|
|
33
37
|
mtimeMs: number;
|
|
34
38
|
ctimeMs: number;
|
|
35
39
|
birthtimeMs: number;
|
|
40
|
+
ino: number;
|
|
41
|
+
nlink: number;
|
|
42
|
+
uid: number;
|
|
43
|
+
gid: number;
|
|
36
44
|
}>;
|
|
37
45
|
removeFile(path: string): Promise<void>;
|
|
38
46
|
removeDir(path: string): Promise<void>;
|
|
@@ -43,17 +51,23 @@ export declare class OpfsFileSystem implements VirtualFileSystem {
|
|
|
43
51
|
mode: number;
|
|
44
52
|
size: number;
|
|
45
53
|
isDirectory: boolean;
|
|
46
|
-
isSymbolicLink
|
|
54
|
+
isSymbolicLink: boolean;
|
|
47
55
|
atimeMs: number;
|
|
48
56
|
mtimeMs: number;
|
|
49
57
|
ctimeMs: number;
|
|
50
58
|
birthtimeMs: number;
|
|
59
|
+
ino: number;
|
|
60
|
+
nlink: number;
|
|
61
|
+
uid: number;
|
|
62
|
+
gid: number;
|
|
51
63
|
}>;
|
|
52
64
|
link(_oldPath: string, _newPath: string): Promise<void>;
|
|
53
65
|
chmod(_path: string, _mode: number): Promise<void>;
|
|
54
66
|
chown(_path: string, _uid: number, _gid: number): Promise<void>;
|
|
55
67
|
utimes(_path: string, _atime: number, _mtime: number): Promise<void>;
|
|
56
68
|
truncate(path: string, length: number): Promise<void>;
|
|
69
|
+
realpath(path: string): Promise<string>;
|
|
70
|
+
pread(path: string, offset: number, length: number): Promise<Uint8Array>;
|
|
57
71
|
}
|
|
58
72
|
export interface BrowserDriverOptions {
|
|
59
73
|
filesystem?: "opfs" | "memory";
|
package/dist/driver.js
CHANGED
|
@@ -103,7 +103,7 @@ export class OpfsFileSystem {
|
|
|
103
103
|
await this.getDirHandle(parent, false);
|
|
104
104
|
await this.getDirHandle(normalized, true);
|
|
105
105
|
}
|
|
106
|
-
async mkdir(path) {
|
|
106
|
+
async mkdir(path, _options) {
|
|
107
107
|
const parts = splitPath(path);
|
|
108
108
|
let current = "";
|
|
109
109
|
for (const part of parts) {
|
|
@@ -134,10 +134,15 @@ export class OpfsFileSystem {
|
|
|
134
134
|
mode: S_IFREG | 0o644,
|
|
135
135
|
size: file.size,
|
|
136
136
|
isDirectory: false,
|
|
137
|
+
isSymbolicLink: false,
|
|
137
138
|
atimeMs: file.lastModified,
|
|
138
139
|
mtimeMs: file.lastModified,
|
|
139
140
|
ctimeMs: file.lastModified,
|
|
140
141
|
birthtimeMs: file.lastModified,
|
|
142
|
+
ino: 0,
|
|
143
|
+
nlink: 1,
|
|
144
|
+
uid: 0,
|
|
145
|
+
gid: 0,
|
|
141
146
|
};
|
|
142
147
|
}
|
|
143
148
|
catch {
|
|
@@ -149,10 +154,15 @@ export class OpfsFileSystem {
|
|
|
149
154
|
mode: S_IFDIR | 0o755,
|
|
150
155
|
size: 4096,
|
|
151
156
|
isDirectory: true,
|
|
157
|
+
isSymbolicLink: false,
|
|
152
158
|
atimeMs: now,
|
|
153
159
|
mtimeMs: now,
|
|
154
160
|
ctimeMs: now,
|
|
155
161
|
birthtimeMs: now,
|
|
162
|
+
ino: 0,
|
|
163
|
+
nlink: 2,
|
|
164
|
+
uid: 0,
|
|
165
|
+
gid: 0,
|
|
156
166
|
};
|
|
157
167
|
}
|
|
158
168
|
catch {
|
|
@@ -207,6 +217,16 @@ export class OpfsFileSystem {
|
|
|
207
217
|
await writable.truncate(length);
|
|
208
218
|
await writable.close();
|
|
209
219
|
}
|
|
220
|
+
async realpath(path) {
|
|
221
|
+
const normalized = normalizePath(path);
|
|
222
|
+
if (await this.exists(normalized))
|
|
223
|
+
return normalized;
|
|
224
|
+
throw new Error(`ENOENT: no such file or directory, realpath '${normalized}'`);
|
|
225
|
+
}
|
|
226
|
+
async pread(path, offset, length) {
|
|
227
|
+
const data = await this.readFile(path);
|
|
228
|
+
return data.slice(offset, offset + length);
|
|
229
|
+
}
|
|
210
230
|
}
|
|
211
231
|
/** Create an OPFS-backed filesystem, falling back to in-memory if OPFS is unavailable. */
|
|
212
232
|
export async function createOpfsFileSystem() {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,3 +3,6 @@ export type { BrowserDriverOptions, BrowserRuntimeSystemOptions, } from "./drive
|
|
|
3
3
|
export { createBrowserRuntimeDriverFactory, } from "./runtime-driver.js";
|
|
4
4
|
export type { BrowserRuntimeDriverFactoryOptions, } from "./runtime-driver.js";
|
|
5
5
|
export { createInMemoryFileSystem } from "@secure-exec/core";
|
|
6
|
+
export { InMemoryFileSystem } from "./os-filesystem.js";
|
|
7
|
+
export { BrowserWorkerAdapter } from "./worker-adapter.js";
|
|
8
|
+
export type { WorkerHandle } from "./worker-adapter.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createBrowserDriver, createBrowserNetworkAdapter, createOpfsFileSystem, } from "./driver.js";
|
|
2
2
|
export { createBrowserRuntimeDriverFactory, } from "./runtime-driver.js";
|
|
3
3
|
export { createInMemoryFileSystem } from "@secure-exec/core";
|
|
4
|
+
export { InMemoryFileSystem } from "./os-filesystem.js";
|
|
5
|
+
export { BrowserWorkerAdapter } from "./worker-adapter.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory filesystem for browser environments.
|
|
3
|
+
*
|
|
4
|
+
* Expanded from the original secure-exec InMemoryFileSystem with POSIX
|
|
5
|
+
* extensions (symlinks, hard links, chmod, chown, utimes, truncate)
|
|
6
|
+
* needed by the kernel VFS interface.
|
|
7
|
+
*/
|
|
8
|
+
import type { VirtualFileSystem, VirtualStat, VirtualDirEntry } from "@secure-exec/core";
|
|
9
|
+
export declare class InMemoryFileSystem implements VirtualFileSystem {
|
|
10
|
+
private entries;
|
|
11
|
+
constructor();
|
|
12
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
13
|
+
readTextFile(path: string): Promise<string>;
|
|
14
|
+
readDir(path: string): Promise<string[]>;
|
|
15
|
+
readDirWithTypes(path: string): Promise<VirtualDirEntry[]>;
|
|
16
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
17
|
+
createDir(path: string): Promise<void>;
|
|
18
|
+
mkdir(path: string, options?: {
|
|
19
|
+
recursive?: boolean;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
exists(path: string): Promise<boolean>;
|
|
22
|
+
stat(path: string): Promise<VirtualStat>;
|
|
23
|
+
removeFile(path: string): Promise<void>;
|
|
24
|
+
removeDir(path: string): Promise<void>;
|
|
25
|
+
realpath(path: string): Promise<string>;
|
|
26
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
27
|
+
symlink(target: string, linkPath: string): Promise<void>;
|
|
28
|
+
readlink(path: string): Promise<string>;
|
|
29
|
+
lstat(path: string): Promise<VirtualStat>;
|
|
30
|
+
link(oldPath: string, newPath: string): Promise<void>;
|
|
31
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
32
|
+
chown(path: string, uid: number, gid: number): Promise<void>;
|
|
33
|
+
utimes(path: string, atime: number, mtime: number): Promise<void>;
|
|
34
|
+
truncate(path: string, length: number): Promise<void>;
|
|
35
|
+
pread(path: string, offset: number, length: number): Promise<Uint8Array>;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve symlinks to get the final path. Returns the normalized path
|
|
38
|
+
* after following all symlinks.
|
|
39
|
+
*/
|
|
40
|
+
private resolvePath;
|
|
41
|
+
/** Resolve a path and return the entry (following symlinks). */
|
|
42
|
+
private resolveEntry;
|
|
43
|
+
private newDir;
|
|
44
|
+
private toStat;
|
|
45
|
+
private enoent;
|
|
46
|
+
}
|
|
47
|
+
export declare function createInMemoryFileSystem(): InMemoryFileSystem;
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory filesystem for browser environments.
|
|
3
|
+
*
|
|
4
|
+
* Expanded from the original secure-exec InMemoryFileSystem with POSIX
|
|
5
|
+
* extensions (symlinks, hard links, chmod, chown, utimes, truncate)
|
|
6
|
+
* needed by the kernel VFS interface.
|
|
7
|
+
*/
|
|
8
|
+
const S_IFREG = 0o100000;
|
|
9
|
+
const S_IFDIR = 0o040000;
|
|
10
|
+
const S_IFLNK = 0o120000;
|
|
11
|
+
const MAX_SYMLINK_DEPTH = 40;
|
|
12
|
+
function normalizePath(path) {
|
|
13
|
+
if (!path)
|
|
14
|
+
return "/";
|
|
15
|
+
let normalized = path.startsWith("/") ? path : `/${path}`;
|
|
16
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
17
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
18
|
+
normalized = normalized.slice(0, -1);
|
|
19
|
+
}
|
|
20
|
+
// Resolve . and ..
|
|
21
|
+
const parts = normalized.split("/");
|
|
22
|
+
const resolved = [];
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (part === "." || part === "")
|
|
25
|
+
continue;
|
|
26
|
+
if (part === "..") {
|
|
27
|
+
resolved.pop();
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
resolved.push(part);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return "/" + resolved.join("/") || "/";
|
|
34
|
+
}
|
|
35
|
+
function dirname(path) {
|
|
36
|
+
const parts = normalizePath(path).split("/").filter(Boolean);
|
|
37
|
+
if (parts.length <= 1)
|
|
38
|
+
return "/";
|
|
39
|
+
return "/" + parts.slice(0, -1).join("/");
|
|
40
|
+
}
|
|
41
|
+
let nextIno = 1;
|
|
42
|
+
export class InMemoryFileSystem {
|
|
43
|
+
entries = new Map();
|
|
44
|
+
constructor() {
|
|
45
|
+
// Root directory
|
|
46
|
+
this.entries.set("/", this.newDir());
|
|
47
|
+
}
|
|
48
|
+
// --- Core operations ---
|
|
49
|
+
async readFile(path) {
|
|
50
|
+
const entry = this.resolveEntry(path);
|
|
51
|
+
if (!entry || entry.type !== "file") {
|
|
52
|
+
throw this.enoent("open", path);
|
|
53
|
+
}
|
|
54
|
+
entry.atimeMs = Date.now();
|
|
55
|
+
return entry.data;
|
|
56
|
+
}
|
|
57
|
+
async readTextFile(path) {
|
|
58
|
+
const data = await this.readFile(path);
|
|
59
|
+
return new TextDecoder().decode(data);
|
|
60
|
+
}
|
|
61
|
+
async readDir(path) {
|
|
62
|
+
return (await this.readDirWithTypes(path)).map((e) => e.name);
|
|
63
|
+
}
|
|
64
|
+
async readDirWithTypes(path) {
|
|
65
|
+
const resolved = this.resolvePath(path);
|
|
66
|
+
const dir = this.entries.get(resolved);
|
|
67
|
+
if (!dir || dir.type !== "dir") {
|
|
68
|
+
throw this.enoent("scandir", path);
|
|
69
|
+
}
|
|
70
|
+
const prefix = resolved === "/" ? "/" : resolved + "/";
|
|
71
|
+
const names = new Map();
|
|
72
|
+
for (const [entryPath, entry] of this.entries) {
|
|
73
|
+
if (entryPath.startsWith(prefix)) {
|
|
74
|
+
const rest = entryPath.slice(prefix.length);
|
|
75
|
+
if (rest && !rest.includes("/")) {
|
|
76
|
+
names.set(rest, {
|
|
77
|
+
name: rest,
|
|
78
|
+
isDirectory: entry.type === "dir",
|
|
79
|
+
isSymbolicLink: entry.type === "symlink",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return Array.from(names.values());
|
|
85
|
+
}
|
|
86
|
+
async writeFile(path, content) {
|
|
87
|
+
const normalized = normalizePath(path);
|
|
88
|
+
// Ensure parent exists
|
|
89
|
+
await this.mkdir(dirname(normalized), { recursive: true });
|
|
90
|
+
const data = typeof content === "string"
|
|
91
|
+
? new TextEncoder().encode(content)
|
|
92
|
+
: content;
|
|
93
|
+
const existing = this.entries.get(normalized);
|
|
94
|
+
if (existing && existing.type === "file") {
|
|
95
|
+
existing.data = data;
|
|
96
|
+
existing.mtimeMs = Date.now();
|
|
97
|
+
existing.ctimeMs = Date.now();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
this.entries.set(normalized, {
|
|
102
|
+
type: "file",
|
|
103
|
+
data,
|
|
104
|
+
mode: S_IFREG | 0o644,
|
|
105
|
+
uid: 1000,
|
|
106
|
+
gid: 1000,
|
|
107
|
+
nlink: 1,
|
|
108
|
+
ino: nextIno++,
|
|
109
|
+
atimeMs: now,
|
|
110
|
+
mtimeMs: now,
|
|
111
|
+
ctimeMs: now,
|
|
112
|
+
birthtimeMs: now,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async createDir(path) {
|
|
116
|
+
const normalized = normalizePath(path);
|
|
117
|
+
const parent = dirname(normalized);
|
|
118
|
+
if (!this.entries.has(parent)) {
|
|
119
|
+
throw this.enoent("mkdir", path);
|
|
120
|
+
}
|
|
121
|
+
if (!this.entries.has(normalized)) {
|
|
122
|
+
this.entries.set(normalized, this.newDir());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async mkdir(path, options) {
|
|
126
|
+
const normalized = normalizePath(path);
|
|
127
|
+
if (options?.recursive !== false) {
|
|
128
|
+
// Recursive: create all missing parents
|
|
129
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
130
|
+
let current = "";
|
|
131
|
+
for (const part of parts) {
|
|
132
|
+
current += "/" + part;
|
|
133
|
+
if (!this.entries.has(current)) {
|
|
134
|
+
this.entries.set(current, this.newDir());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
await this.createDir(path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async exists(path) {
|
|
143
|
+
try {
|
|
144
|
+
const resolved = this.resolvePath(path);
|
|
145
|
+
return this.entries.has(resolved);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async stat(path) {
|
|
152
|
+
const entry = this.resolveEntry(path);
|
|
153
|
+
if (!entry)
|
|
154
|
+
throw this.enoent("stat", path);
|
|
155
|
+
return this.toStat(entry);
|
|
156
|
+
}
|
|
157
|
+
async removeFile(path) {
|
|
158
|
+
const resolved = this.resolvePath(path);
|
|
159
|
+
const entry = this.entries.get(resolved);
|
|
160
|
+
if (!entry || entry.type === "dir") {
|
|
161
|
+
throw this.enoent("unlink", path);
|
|
162
|
+
}
|
|
163
|
+
this.entries.delete(resolved);
|
|
164
|
+
}
|
|
165
|
+
async removeDir(path) {
|
|
166
|
+
const resolved = this.resolvePath(path);
|
|
167
|
+
if (resolved === "/") {
|
|
168
|
+
throw new Error("EPERM: operation not permitted, rmdir '/'");
|
|
169
|
+
}
|
|
170
|
+
const entry = this.entries.get(resolved);
|
|
171
|
+
if (!entry || entry.type !== "dir") {
|
|
172
|
+
throw this.enoent("rmdir", path);
|
|
173
|
+
}
|
|
174
|
+
// Check if empty
|
|
175
|
+
const prefix = resolved + "/";
|
|
176
|
+
for (const key of this.entries.keys()) {
|
|
177
|
+
if (key.startsWith(prefix)) {
|
|
178
|
+
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${path}'`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.entries.delete(resolved);
|
|
182
|
+
}
|
|
183
|
+
async realpath(path) {
|
|
184
|
+
return this.resolvePath(path);
|
|
185
|
+
}
|
|
186
|
+
async rename(oldPath, newPath) {
|
|
187
|
+
const oldResolved = this.resolvePath(oldPath);
|
|
188
|
+
const newNorm = normalizePath(newPath);
|
|
189
|
+
const entry = this.entries.get(oldResolved);
|
|
190
|
+
if (!entry)
|
|
191
|
+
throw this.enoent("rename", oldPath);
|
|
192
|
+
// Ensure parent of target exists
|
|
193
|
+
if (!this.entries.has(dirname(newNorm))) {
|
|
194
|
+
throw this.enoent("rename", newPath);
|
|
195
|
+
}
|
|
196
|
+
if (entry.type !== "dir") {
|
|
197
|
+
this.entries.set(newNorm, entry);
|
|
198
|
+
this.entries.delete(oldResolved);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Move directory and all children
|
|
202
|
+
const prefix = oldResolved + "/";
|
|
203
|
+
const toMove = [];
|
|
204
|
+
for (const [key, val] of this.entries) {
|
|
205
|
+
if (key === oldResolved || key.startsWith(prefix)) {
|
|
206
|
+
toMove.push([key, val]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const [key] of toMove) {
|
|
210
|
+
this.entries.delete(key);
|
|
211
|
+
}
|
|
212
|
+
for (const [key, val] of toMove) {
|
|
213
|
+
const newKey = key === oldResolved
|
|
214
|
+
? newNorm
|
|
215
|
+
: newNorm + key.slice(oldResolved.length);
|
|
216
|
+
this.entries.set(newKey, val);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// --- Symlinks ---
|
|
220
|
+
async symlink(target, linkPath) {
|
|
221
|
+
const normalized = normalizePath(linkPath);
|
|
222
|
+
if (this.entries.has(normalized)) {
|
|
223
|
+
throw new Error(`EEXIST: file already exists, symlink '${linkPath}'`);
|
|
224
|
+
}
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
this.entries.set(normalized, {
|
|
227
|
+
type: "symlink",
|
|
228
|
+
target,
|
|
229
|
+
mode: S_IFLNK | 0o777,
|
|
230
|
+
uid: 1000,
|
|
231
|
+
gid: 1000,
|
|
232
|
+
nlink: 1,
|
|
233
|
+
ino: nextIno++,
|
|
234
|
+
atimeMs: now,
|
|
235
|
+
mtimeMs: now,
|
|
236
|
+
ctimeMs: now,
|
|
237
|
+
birthtimeMs: now,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async readlink(path) {
|
|
241
|
+
const normalized = normalizePath(path);
|
|
242
|
+
const entry = this.entries.get(normalized);
|
|
243
|
+
if (!entry || entry.type !== "symlink") {
|
|
244
|
+
throw this.enoent("readlink", path);
|
|
245
|
+
}
|
|
246
|
+
return entry.target;
|
|
247
|
+
}
|
|
248
|
+
async lstat(path) {
|
|
249
|
+
const normalized = normalizePath(path);
|
|
250
|
+
const entry = this.entries.get(normalized);
|
|
251
|
+
if (!entry)
|
|
252
|
+
throw this.enoent("lstat", path);
|
|
253
|
+
return this.toStat(entry);
|
|
254
|
+
}
|
|
255
|
+
// --- Links ---
|
|
256
|
+
async link(oldPath, newPath) {
|
|
257
|
+
const entry = this.resolveEntry(oldPath);
|
|
258
|
+
if (!entry || entry.type !== "file") {
|
|
259
|
+
throw this.enoent("link", oldPath);
|
|
260
|
+
}
|
|
261
|
+
const newNorm = normalizePath(newPath);
|
|
262
|
+
if (this.entries.has(newNorm)) {
|
|
263
|
+
throw new Error(`EEXIST: file already exists, link '${newPath}'`);
|
|
264
|
+
}
|
|
265
|
+
entry.nlink++;
|
|
266
|
+
this.entries.set(newNorm, entry);
|
|
267
|
+
}
|
|
268
|
+
// --- Permissions & Metadata ---
|
|
269
|
+
async chmod(path, mode) {
|
|
270
|
+
const entry = this.resolveEntry(path);
|
|
271
|
+
if (!entry)
|
|
272
|
+
throw this.enoent("chmod", path);
|
|
273
|
+
// Preserve file type bits, update permission bits
|
|
274
|
+
entry.mode = (entry.mode & 0o170000) | (mode & 0o7777);
|
|
275
|
+
entry.ctimeMs = Date.now();
|
|
276
|
+
}
|
|
277
|
+
async chown(path, uid, gid) {
|
|
278
|
+
const entry = this.resolveEntry(path);
|
|
279
|
+
if (!entry)
|
|
280
|
+
throw this.enoent("chown", path);
|
|
281
|
+
entry.uid = uid;
|
|
282
|
+
entry.gid = gid;
|
|
283
|
+
entry.ctimeMs = Date.now();
|
|
284
|
+
}
|
|
285
|
+
async utimes(path, atime, mtime) {
|
|
286
|
+
const entry = this.resolveEntry(path);
|
|
287
|
+
if (!entry)
|
|
288
|
+
throw this.enoent("utimes", path);
|
|
289
|
+
entry.atimeMs = atime;
|
|
290
|
+
entry.mtimeMs = mtime;
|
|
291
|
+
entry.ctimeMs = Date.now();
|
|
292
|
+
}
|
|
293
|
+
async truncate(path, length) {
|
|
294
|
+
const entry = this.resolveEntry(path);
|
|
295
|
+
if (!entry || entry.type !== "file") {
|
|
296
|
+
throw this.enoent("truncate", path);
|
|
297
|
+
}
|
|
298
|
+
if (length < entry.data.length) {
|
|
299
|
+
entry.data = entry.data.slice(0, length);
|
|
300
|
+
}
|
|
301
|
+
else if (length > entry.data.length) {
|
|
302
|
+
const newData = new Uint8Array(length);
|
|
303
|
+
newData.set(entry.data);
|
|
304
|
+
entry.data = newData;
|
|
305
|
+
}
|
|
306
|
+
entry.mtimeMs = Date.now();
|
|
307
|
+
entry.ctimeMs = Date.now();
|
|
308
|
+
}
|
|
309
|
+
async pread(path, offset, length) {
|
|
310
|
+
const entry = this.resolveEntry(path);
|
|
311
|
+
if (!entry || entry.type !== "file") {
|
|
312
|
+
throw this.enoent("open", path);
|
|
313
|
+
}
|
|
314
|
+
entry.atimeMs = Date.now();
|
|
315
|
+
if (offset >= entry.data.length)
|
|
316
|
+
return new Uint8Array(0);
|
|
317
|
+
return entry.data.slice(offset, Math.min(offset + length, entry.data.length));
|
|
318
|
+
}
|
|
319
|
+
// --- Helpers ---
|
|
320
|
+
/**
|
|
321
|
+
* Resolve symlinks to get the final path. Returns the normalized path
|
|
322
|
+
* after following all symlinks.
|
|
323
|
+
*/
|
|
324
|
+
resolvePath(path, depth = 0) {
|
|
325
|
+
if (depth > MAX_SYMLINK_DEPTH) {
|
|
326
|
+
throw new Error(`ELOOP: too many levels of symbolic links, '${path}'`);
|
|
327
|
+
}
|
|
328
|
+
const normalized = normalizePath(path);
|
|
329
|
+
const entry = this.entries.get(normalized);
|
|
330
|
+
if (!entry)
|
|
331
|
+
return normalized;
|
|
332
|
+
if (entry.type === "symlink") {
|
|
333
|
+
const target = entry.target.startsWith("/")
|
|
334
|
+
? entry.target
|
|
335
|
+
: dirname(normalized) + "/" + entry.target;
|
|
336
|
+
return this.resolvePath(target, depth + 1);
|
|
337
|
+
}
|
|
338
|
+
return normalized;
|
|
339
|
+
}
|
|
340
|
+
/** Resolve a path and return the entry (following symlinks). */
|
|
341
|
+
resolveEntry(path) {
|
|
342
|
+
const resolved = this.resolvePath(path);
|
|
343
|
+
return this.entries.get(resolved);
|
|
344
|
+
}
|
|
345
|
+
newDir() {
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
return {
|
|
348
|
+
type: "dir",
|
|
349
|
+
mode: S_IFDIR | 0o755,
|
|
350
|
+
uid: 1000,
|
|
351
|
+
gid: 1000,
|
|
352
|
+
nlink: 2,
|
|
353
|
+
ino: nextIno++,
|
|
354
|
+
atimeMs: now,
|
|
355
|
+
mtimeMs: now,
|
|
356
|
+
ctimeMs: now,
|
|
357
|
+
birthtimeMs: now,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
toStat(entry) {
|
|
361
|
+
return {
|
|
362
|
+
mode: entry.mode,
|
|
363
|
+
size: entry.type === "file" ? entry.data.length : 4096,
|
|
364
|
+
isDirectory: entry.type === "dir",
|
|
365
|
+
isSymbolicLink: entry.type === "symlink",
|
|
366
|
+
atimeMs: entry.atimeMs,
|
|
367
|
+
mtimeMs: entry.mtimeMs,
|
|
368
|
+
ctimeMs: entry.ctimeMs,
|
|
369
|
+
birthtimeMs: entry.birthtimeMs,
|
|
370
|
+
ino: entry.ino,
|
|
371
|
+
nlink: entry.nlink,
|
|
372
|
+
uid: entry.uid,
|
|
373
|
+
gid: entry.gid,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
enoent(op, path) {
|
|
377
|
+
const err = new Error(`ENOENT: no such file or directory, ${op} '${path}'`);
|
|
378
|
+
err.code = "ENOENT";
|
|
379
|
+
return err;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
export function createInMemoryFileSystem() {
|
|
383
|
+
return new InMemoryFileSystem();
|
|
384
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser worker adapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the Web Worker API for spawning Workers.
|
|
5
|
+
* Requires COOP/COEP headers for SharedArrayBuffer support.
|
|
6
|
+
*/
|
|
7
|
+
export interface WorkerHandle {
|
|
8
|
+
postMessage(data: unknown, transferList?: Transferable[]): void;
|
|
9
|
+
onMessage(handler: (data: unknown) => void): void;
|
|
10
|
+
onError(handler: (err: Error) => void): void;
|
|
11
|
+
onExit(handler: (code: number) => void): void;
|
|
12
|
+
terminate(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare class BrowserWorkerAdapter {
|
|
15
|
+
/**
|
|
16
|
+
* Spawn a Web Worker for the given script URL.
|
|
17
|
+
*/
|
|
18
|
+
static create(scriptUrl: string | URL, options?: {
|
|
19
|
+
workerData?: unknown;
|
|
20
|
+
}): WorkerHandle;
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser worker adapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the Web Worker API for spawning Workers.
|
|
5
|
+
* Requires COOP/COEP headers for SharedArrayBuffer support.
|
|
6
|
+
*/
|
|
7
|
+
export class BrowserWorkerAdapter {
|
|
8
|
+
/**
|
|
9
|
+
* Spawn a Web Worker for the given script URL.
|
|
10
|
+
*/
|
|
11
|
+
static create(scriptUrl, options) {
|
|
12
|
+
const worker = new Worker(scriptUrl, { type: "module" });
|
|
13
|
+
// Send workerData as the initial message (Web Workers don't have
|
|
14
|
+
// a constructor option for this like Node's worker_threads)
|
|
15
|
+
if (options?.workerData !== undefined) {
|
|
16
|
+
worker.postMessage({
|
|
17
|
+
type: "init",
|
|
18
|
+
workerData: options.workerData,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
postMessage(data, transferList) {
|
|
23
|
+
worker.postMessage(data, transferList ?? []);
|
|
24
|
+
},
|
|
25
|
+
onMessage(handler) {
|
|
26
|
+
worker.addEventListener("message", (e) => handler(e.data));
|
|
27
|
+
},
|
|
28
|
+
onError(handler) {
|
|
29
|
+
worker.addEventListener("error", (e) => handler(new Error(e.message)));
|
|
30
|
+
},
|
|
31
|
+
onExit(_handler) {
|
|
32
|
+
// Web Workers don't have an exit event — the terminate()
|
|
33
|
+
// caller is responsible for cleanup
|
|
34
|
+
},
|
|
35
|
+
terminate() {
|
|
36
|
+
worker.terminate();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/worker.js
CHANGED
|
@@ -75,7 +75,7 @@ function revivePermissions(serialized) {
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* Wrap a sync function in the bridge calling convention (`applySync`) so
|
|
78
|
-
* bridge code can call it the same way it calls
|
|
78
|
+
* bridge code can call it the same way it calls bridge References.
|
|
79
79
|
*/
|
|
80
80
|
function makeApplySync(fn) {
|
|
81
81
|
const applySync = (_ctx, args) => fn(...args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@secure-exec/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-rc.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,11 +39,21 @@
|
|
|
39
39
|
"types": "./dist/permission-validation.d.ts",
|
|
40
40
|
"import": "./dist/permission-validation.js",
|
|
41
41
|
"default": "./dist/permission-validation.js"
|
|
42
|
+
},
|
|
43
|
+
"./internal/os-filesystem": {
|
|
44
|
+
"types": "./dist/os-filesystem.d.ts",
|
|
45
|
+
"import": "./dist/os-filesystem.js",
|
|
46
|
+
"default": "./dist/os-filesystem.js"
|
|
47
|
+
},
|
|
48
|
+
"./internal/worker-adapter": {
|
|
49
|
+
"types": "./dist/worker-adapter.d.ts",
|
|
50
|
+
"import": "./dist/worker-adapter.js",
|
|
51
|
+
"default": "./dist/worker-adapter.js"
|
|
42
52
|
}
|
|
43
53
|
},
|
|
44
54
|
"dependencies": {
|
|
45
55
|
"sucrase": "^3.35.0",
|
|
46
|
-
"@secure-exec/core": "0.
|
|
56
|
+
"@secure-exec/core": "0.2.0-rc.1"
|
|
47
57
|
},
|
|
48
58
|
"devDependencies": {
|
|
49
59
|
"@types/node": "^22.10.2",
|