@secure-exec/core 0.2.0-rc.2 → 0.2.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.
Files changed (53) hide show
  1. package/dist/generated/isolate-runtime.d.ts +1 -1
  2. package/dist/generated/isolate-runtime.js +1 -1
  3. package/dist/index.d.ts +17 -4
  4. package/dist/index.js +10 -2
  5. package/dist/isolate-runtime/require-setup.js +145 -7
  6. package/dist/kernel/device-backend.d.ts +14 -0
  7. package/dist/kernel/device-backend.js +251 -0
  8. package/dist/kernel/device-layer.js +9 -0
  9. package/dist/kernel/index.d.ts +4 -4
  10. package/dist/kernel/index.js +3 -3
  11. package/dist/kernel/kernel.js +141 -119
  12. package/dist/kernel/mount-table.d.ts +75 -0
  13. package/dist/kernel/mount-table.js +353 -0
  14. package/dist/kernel/permissions.d.ts +9 -0
  15. package/dist/kernel/permissions.js +33 -1
  16. package/dist/kernel/proc-backend.d.ts +30 -0
  17. package/dist/kernel/proc-backend.js +428 -0
  18. package/dist/kernel/proc-layer.js +6 -0
  19. package/dist/kernel/process-table.d.ts +3 -1
  20. package/dist/kernel/process-table.js +23 -3
  21. package/dist/kernel/pty.d.ts +3 -2
  22. package/dist/kernel/pty.js +13 -2
  23. package/dist/kernel/types.d.ts +45 -4
  24. package/dist/kernel/types.js +9 -0
  25. package/dist/kernel/vfs.d.ts +30 -2
  26. package/dist/kernel/vfs.js +19 -2
  27. package/dist/shared/api-types.d.ts +6 -0
  28. package/dist/shared/console-formatter.js +8 -8
  29. package/dist/shared/in-memory-fs.d.ts +14 -62
  30. package/dist/shared/in-memory-fs.js +101 -636
  31. package/dist/shared/permissions.js +5 -0
  32. package/dist/test/block-store-conformance.d.ts +34 -0
  33. package/dist/test/block-store-conformance.js +251 -0
  34. package/dist/test/metadata-store-conformance.d.ts +37 -0
  35. package/dist/test/metadata-store-conformance.js +646 -0
  36. package/dist/test/vfs-conformance.d.ts +65 -0
  37. package/dist/test/vfs-conformance.js +842 -0
  38. package/dist/types.d.ts +1 -0
  39. package/dist/vfs/chunked-vfs.d.ts +66 -0
  40. package/dist/vfs/chunked-vfs.js +1290 -0
  41. package/dist/vfs/host-block-store.d.ts +19 -0
  42. package/dist/vfs/host-block-store.js +97 -0
  43. package/dist/vfs/memory-block-store.d.ts +16 -0
  44. package/dist/vfs/memory-block-store.js +45 -0
  45. package/dist/vfs/memory-metadata.d.ts +75 -0
  46. package/dist/vfs/memory-metadata.js +528 -0
  47. package/dist/vfs/sqlite-metadata.d.ts +91 -0
  48. package/dist/vfs/sqlite-metadata.js +582 -0
  49. package/dist/vfs/types.d.ts +210 -0
  50. package/dist/vfs/types.js +8 -0
  51. package/package.json +20 -1
  52. package/dist/kernel/inode-table.d.ts +0 -43
  53. package/dist/kernel/inode-table.js +0 -85
@@ -0,0 +1,210 @@
1
+ /**
2
+ * VFS storage layer interfaces.
3
+ *
4
+ * FsMetadataStore owns the filesystem tree (inodes, directory entries, symlinks,
5
+ * chunk mapping). FsBlockStore is a dumb key-value byte store for file content.
6
+ * ChunkedVFS composes the two into a VirtualFileSystem.
7
+ */
8
+ export type InodeType = "file" | "directory" | "symlink";
9
+ export interface CreateInodeAttrs {
10
+ type: InodeType;
11
+ mode: number;
12
+ uid: number;
13
+ gid: number;
14
+ /** Required for symlinks. */
15
+ symlinkTarget?: string;
16
+ }
17
+ export interface InodeMeta {
18
+ ino: number;
19
+ type: InodeType;
20
+ mode: number;
21
+ uid: number;
22
+ gid: number;
23
+ size: number;
24
+ nlink: number;
25
+ atimeMs: number;
26
+ mtimeMs: number;
27
+ ctimeMs: number;
28
+ birthtimeMs: number;
29
+ /**
30
+ * 'inline': content stored in inlineContent (small files).
31
+ * 'chunked': content stored as blocks in the block store.
32
+ */
33
+ storageMode: "inline" | "chunked";
34
+ /** Inline content for small files. Null if chunked. */
35
+ inlineContent: Uint8Array | null;
36
+ }
37
+ export interface DentryInfo {
38
+ name: string;
39
+ ino: number;
40
+ type: InodeType;
41
+ }
42
+ export interface DentryStatInfo extends DentryInfo {
43
+ stat: InodeMeta;
44
+ }
45
+ /**
46
+ * Owns the filesystem tree, inode metadata, and chunk mapping.
47
+ * No file content. All path resolution happens here.
48
+ *
49
+ * Implementations:
50
+ * - InMemoryMetadataStore: pure JS Map-based, for ephemeral VMs and tests.
51
+ * - SqliteMetadataStore: SQLite-backed, for persistent local and cloud storage.
52
+ */
53
+ export interface FsMetadataStore {
54
+ /**
55
+ * Execute a callback atomically. All metadata mutations within
56
+ * the callback either fully commit or fully roll back.
57
+ * SQLite: wraps in BEGIN/COMMIT.
58
+ * InMemory: just calls the callback (single-threaded JS, no rollback needed).
59
+ */
60
+ transaction<T>(fn: () => Promise<T>): Promise<T>;
61
+ /** Create a new inode. Returns the allocated inode number. */
62
+ createInode(attrs: CreateInodeAttrs): Promise<number>;
63
+ /** Get inode metadata by number. Returns null if not found. */
64
+ getInode(ino: number): Promise<InodeMeta | null>;
65
+ /** Update inode metadata fields (partial update). */
66
+ updateInode(ino: number, updates: Partial<InodeMeta>): Promise<void>;
67
+ /** Delete an inode and all associated data (chunk map, symlink target). */
68
+ deleteInode(ino: number): Promise<void>;
69
+ /** Look up a child name in a directory. Returns child ino or null. */
70
+ lookup(parentIno: number, name: string): Promise<number | null>;
71
+ /** Create a directory entry. Throws EEXIST if name already exists. */
72
+ createDentry(parentIno: number, name: string, childIno: number, type: InodeType): Promise<void>;
73
+ /** Remove a directory entry. Does NOT delete the child inode. */
74
+ removeDentry(parentIno: number, name: string): Promise<void>;
75
+ /** List all entries in a directory. */
76
+ listDir(parentIno: number): Promise<DentryInfo[]>;
77
+ /**
78
+ * List all entries with full inode metadata (avoids N+1).
79
+ * SQLite: single JOIN query. InMemory: iterate + Map lookup.
80
+ */
81
+ listDirWithStats(parentIno: number): Promise<DentryStatInfo[]>;
82
+ /**
83
+ * Move a directory entry. Atomic: removes from src parent,
84
+ * adds to dst parent. Handles same-parent rename.
85
+ */
86
+ renameDentry(srcParentIno: number, srcName: string, dstParentIno: number, dstName: string): Promise<void>;
87
+ /**
88
+ * Walk the dentry tree from root, following symlinks.
89
+ * Returns the resolved inode number.
90
+ * Throws ENOENT if any component does not exist.
91
+ * Throws ELOOP if symlink depth exceeds 40 (SYMLOOP_MAX).
92
+ */
93
+ resolvePath(path: string): Promise<number>;
94
+ /**
95
+ * Resolve all intermediate path components but NOT the final one.
96
+ * Returns the parent inode and the final component name.
97
+ * Used for lstat, readlink, unlink, and creating new entries.
98
+ * Throws ENOENT if any intermediate component does not exist.
99
+ */
100
+ resolveParentPath(path: string): Promise<{
101
+ parentIno: number;
102
+ name: string;
103
+ }>;
104
+ /** Get the symlink target for a symlink inode. */
105
+ readSymlink(ino: number): Promise<string>;
106
+ /** Get the block store key for a chunk. Null if not set (sparse hole). */
107
+ getChunkKey(ino: number, chunkIndex: number): Promise<string | null>;
108
+ /** Set the block store key for a chunk. Creates or updates. */
109
+ setChunkKey(ino: number, chunkIndex: number, key: string): Promise<void>;
110
+ /** Get all chunk keys for a file, ordered by chunk index. */
111
+ getAllChunkKeys(ino: number): Promise<{
112
+ chunkIndex: number;
113
+ key: string;
114
+ }[]>;
115
+ /** Delete all chunk mappings for an inode. Returns the deleted keys. */
116
+ deleteAllChunks(ino: number): Promise<string[]>;
117
+ /**
118
+ * Delete chunk mappings for indices >= startIndex.
119
+ * Returns the deleted keys. Used by truncate.
120
+ */
121
+ deleteChunksFrom(ino: number, startIndex: number): Promise<string[]>;
122
+ }
123
+ /**
124
+ * Dumb key-value byte store. Knows nothing about files, directories, or inodes.
125
+ *
126
+ * Error contracts:
127
+ * - read/readRange: throw KernelError("ENOENT") if key not found.
128
+ * - readRange beyond block size: return available bytes (short read).
129
+ * - write: overwrite if key exists.
130
+ * - delete/deleteMany: no-op for non-existent keys.
131
+ * - copy: throw KernelError("ENOENT") if source key not found.
132
+ */
133
+ export interface FsBlockStore {
134
+ /** Read an entire block. Throws if key not found. */
135
+ read(key: string): Promise<Uint8Array>;
136
+ /** Read a byte range within a block. Throws if key not found. */
137
+ readRange(key: string, offset: number, length: number): Promise<Uint8Array>;
138
+ /** Write a block (creates or overwrites). */
139
+ write(key: string, data: Uint8Array): Promise<void>;
140
+ /** Delete a block. No-op if key does not exist. */
141
+ delete(key: string): Promise<void>;
142
+ /** Delete multiple blocks. No-op for keys that don't exist. */
143
+ deleteMany(keys: string[]): Promise<void>;
144
+ /**
145
+ * Server-side copy. Optional.
146
+ * If not implemented, callers fall back to read + write.
147
+ */
148
+ copy?(srcKey: string, dstKey: string): Promise<void>;
149
+ }
150
+ export interface VersionMeta {
151
+ version: number;
152
+ size: number;
153
+ createdAt: number;
154
+ storageMode: "inline" | "chunked";
155
+ inlineContent: Uint8Array | null;
156
+ }
157
+ /**
158
+ * Optional versioning extension for FsMetadataStore.
159
+ *
160
+ * Implementations that support versioning (e.g., SqliteMetadataStore) can
161
+ * implement this interface to allow ChunkedVFS to snapshot, list, and
162
+ * restore file versions.
163
+ *
164
+ * Both SqliteMetadataStore and InMemoryMetadataStore implement versioning
165
+ * when the `versioning` option is enabled at construction time.
166
+ */
167
+ export interface FsMetadataStoreVersioning {
168
+ /** Snapshot current chunk map + size. Returns the version number. */
169
+ createVersion(ino: number): Promise<number>;
170
+ /** Get version info. Returns null if the version does not exist. */
171
+ getVersion(ino: number, version: number): Promise<VersionMeta | null>;
172
+ /** List versions, newest first. */
173
+ listVersions(ino: number): Promise<VersionMeta[]>;
174
+ /** Get chunk map for a specific version. */
175
+ getVersionChunkMap(ino: number, version: number): Promise<{
176
+ chunkIndex: number;
177
+ key: string;
178
+ }[]>;
179
+ /**
180
+ * Delete version records. Returns block keys that are no longer
181
+ * referenced by ANY version or the current chunk map.
182
+ */
183
+ deleteVersions(ino: number, versions: number[]): Promise<string[]>;
184
+ /** Restore current chunk map to match a version. */
185
+ restoreVersion(ino: number, version: number): Promise<void>;
186
+ }
187
+ export type RetentionPolicy =
188
+ /** Keep the N most recent versions. Delete the rest immediately. */
189
+ {
190
+ type: "count";
191
+ keep: number;
192
+ }
193
+ /** Keep versions newer than maxAgeMs. Delete older immediately. */
194
+ | {
195
+ type: "age";
196
+ maxAgeMs: number;
197
+ }
198
+ /**
199
+ * Mark old metadata as pruned but do NOT delete blocks.
200
+ * Used with block stores that have their own TTL/lifecycle
201
+ * (e.g., S3 lifecycle rules). The block store handles cleanup.
202
+ */
203
+ | {
204
+ type: "deferred";
205
+ };
206
+ export interface VersionInfo {
207
+ version: number;
208
+ size: number;
209
+ createdAt: number;
210
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * VFS storage layer interfaces.
3
+ *
4
+ * FsMetadataStore owns the filesystem tree (inodes, directory entries, symlinks,
5
+ * chunk mapping). FsBlockStore is a dumb key-value byte store for file content.
6
+ * ChunkedVFS composes the two into a VirtualFileSystem.
7
+ */
8
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secure-exec/core",
3
- "version": "0.2.0-rc.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.js",
@@ -79,14 +79,33 @@
79
79
  "types": "./dist/shared/*.d.ts",
80
80
  "import": "./dist/shared/*.js",
81
81
  "default": "./dist/shared/*.js"
82
+ },
83
+ "./test/vfs-conformance": {
84
+ "types": "./dist/test/vfs-conformance.d.ts",
85
+ "import": "./dist/test/vfs-conformance.js",
86
+ "default": "./dist/test/vfs-conformance.js"
87
+ },
88
+ "./test/block-store-conformance": {
89
+ "types": "./dist/test/block-store-conformance.d.ts",
90
+ "import": "./dist/test/block-store-conformance.js",
91
+ "default": "./dist/test/block-store-conformance.js"
92
+ },
93
+ "./test/metadata-store-conformance": {
94
+ "types": "./dist/test/metadata-store-conformance.d.ts",
95
+ "import": "./dist/test/metadata-store-conformance.js",
96
+ "default": "./dist/test/metadata-store-conformance.js"
82
97
  }
83
98
  },
84
99
  "devDependencies": {
100
+ "@types/better-sqlite3": "^7.6.13",
85
101
  "@types/node": "^22.10.2",
86
102
  "@xterm/headless": "^6.0.0",
87
103
  "typescript": "^5.7.2",
88
104
  "vitest": "^2.1.8"
89
105
  },
106
+ "dependencies": {
107
+ "better-sqlite3": "^12.8.0"
108
+ },
90
109
  "scripts": {
91
110
  "check-types:src": "tsc --noEmit",
92
111
  "check-types:isolate-runtime": "tsc -p tsconfig.isolate-runtime.json --noEmit",
@@ -1,43 +0,0 @@
1
- /**
2
- * Inode table with refcounting and deferred unlink.
3
- *
4
- * Provides a POSIX-style inode layer: hard link counts (nlink),
5
- * open FD reference counting (openRefCount), and deferred deletion
6
- * when nlink reaches 0 but FDs are still open.
7
- */
8
- export interface Inode {
9
- readonly ino: number;
10
- nlink: number;
11
- openRefCount: number;
12
- mode: number;
13
- uid: number;
14
- gid: number;
15
- size: number;
16
- atime: Date;
17
- mtime: Date;
18
- ctime: Date;
19
- birthtime: Date;
20
- }
21
- export declare class InodeTable {
22
- private inodes;
23
- private nextIno;
24
- /** Allocate a new inode with the given mode, uid, gid. Returns the inode. */
25
- allocate(mode: number, uid: number, gid: number): Inode;
26
- /** Look up an inode by number. */
27
- get(ino: number): Inode | null;
28
- /** Increment hard link count (new directory entry pointing to this inode). */
29
- incrementLinks(ino: number): void;
30
- /** Decrement hard link count (directory entry removed). */
31
- decrementLinks(ino: number): void;
32
- /** Increment open FD reference count. */
33
- incrementOpenRefs(ino: number): void;
34
- /** Decrement open FD reference count. */
35
- decrementOpenRefs(ino: number): void;
36
- /** True when nlink=0 AND openRefCount=0 — inode data can be freed. */
37
- shouldDelete(ino: number): boolean;
38
- /** Remove the inode from the table. Called after shouldDelete returns true. */
39
- delete(ino: number): void;
40
- /** Number of inodes in the table. */
41
- get size(): number;
42
- private requireInode;
43
- }
@@ -1,85 +0,0 @@
1
- /**
2
- * Inode table with refcounting and deferred unlink.
3
- *
4
- * Provides a POSIX-style inode layer: hard link counts (nlink),
5
- * open FD reference counting (openRefCount), and deferred deletion
6
- * when nlink reaches 0 but FDs are still open.
7
- */
8
- import { KernelError } from "./types.js";
9
- export class InodeTable {
10
- inodes = new Map();
11
- nextIno = 1;
12
- /** Allocate a new inode with the given mode, uid, gid. Returns the inode. */
13
- allocate(mode, uid, gid) {
14
- const now = new Date();
15
- const inode = {
16
- ino: this.nextIno++,
17
- nlink: 1,
18
- openRefCount: 0,
19
- mode,
20
- uid,
21
- gid,
22
- size: 0,
23
- atime: now,
24
- mtime: now,
25
- ctime: now,
26
- birthtime: now,
27
- };
28
- this.inodes.set(inode.ino, inode);
29
- return inode;
30
- }
31
- /** Look up an inode by number. */
32
- get(ino) {
33
- return this.inodes.get(ino) ?? null;
34
- }
35
- /** Increment hard link count (new directory entry pointing to this inode). */
36
- incrementLinks(ino) {
37
- const inode = this.requireInode(ino);
38
- inode.nlink++;
39
- inode.ctime = new Date();
40
- }
41
- /** Decrement hard link count (directory entry removed). */
42
- decrementLinks(ino) {
43
- const inode = this.requireInode(ino);
44
- if (inode.nlink <= 0) {
45
- throw new KernelError("EINVAL", `inode ${ino} nlink already 0`);
46
- }
47
- inode.nlink--;
48
- inode.ctime = new Date();
49
- }
50
- /** Increment open FD reference count. */
51
- incrementOpenRefs(ino) {
52
- const inode = this.requireInode(ino);
53
- inode.openRefCount++;
54
- }
55
- /** Decrement open FD reference count. */
56
- decrementOpenRefs(ino) {
57
- const inode = this.requireInode(ino);
58
- if (inode.openRefCount <= 0) {
59
- throw new KernelError("EINVAL", `inode ${ino} openRefCount already 0`);
60
- }
61
- inode.openRefCount--;
62
- }
63
- /** True when nlink=0 AND openRefCount=0 — inode data can be freed. */
64
- shouldDelete(ino) {
65
- const inode = this.inodes.get(ino);
66
- if (!inode)
67
- return false;
68
- return inode.nlink === 0 && inode.openRefCount === 0;
69
- }
70
- /** Remove the inode from the table. Called after shouldDelete returns true. */
71
- delete(ino) {
72
- this.inodes.delete(ino);
73
- }
74
- /** Number of inodes in the table. */
75
- get size() {
76
- return this.inodes.size;
77
- }
78
- requireInode(ino) {
79
- const inode = this.inodes.get(ino);
80
- if (!inode) {
81
- throw new KernelError("ENOENT", `inode ${ino} not found`);
82
- }
83
- return inode;
84
- }
85
- }