@gobing-ai/ts-runtime 0.4.5 → 0.4.7
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 +62 -6
- package/dist/db-errors.d.ts +13 -0
- package/dist/db-errors.d.ts.map +1 -1
- package/dist/db-errors.js +16 -0
- package/dist/extension/extension-loader.d.ts +12 -0
- package/dist/extension/extension-loader.d.ts.map +1 -1
- package/dist/extension/extension-loader.js +21 -0
- package/dist/extension/extension-path.d.ts +7 -1
- package/dist/extension/extension-path.d.ts.map +1 -1
- package/dist/extension/extension-path.js +7 -1
- package/dist/file-system-node.d.ts.map +1 -1
- package/dist/file-system-node.js +20 -1
- package/dist/file-system.d.ts +24 -0
- package/dist/file-system.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/path.d.ts +2 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +18 -0
- package/dist/runtime-node-bun.d.ts.map +1 -1
- package/dist/runtime-node-bun.js +17 -8
- package/package.json +11 -3
- package/src/db-errors.ts +20 -0
- package/src/extension/extension-loader.ts +34 -0
- package/src/extension/extension-path.ts +7 -1
- package/src/file-system-node.ts +22 -0
- package/src/file-system.ts +25 -0
- package/src/index.ts +1 -1
- package/src/path.ts +21 -0
- package/src/runtime-node-bun.ts +18 -10
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ and Cloudflare Workers through a factory pattern that auto-detects the runtime.
|
|
|
17
17
|
| Runtime factory | `RuntimeFactory` → `loadRuntimeFactory()` | `nodeBunFactory` | `cloudflareWorkersFactory` |
|
|
18
18
|
| File system | `FileSystem` | `createNodeFileSystem()` (sync `node:fs`) | `createCfFileSystem()` (stub) |
|
|
19
19
|
| Process execution | `ProcessExecutor` (class) | `run()` via execa, `runStreaming()` via `Bun.spawn` | throws |
|
|
20
|
-
| SQL database | `createDbAdapter(config)` → `DbAdapter` | Bun SQLite via `@gobing-ai/ts-db` | throws `D1NotConfiguredError` (D1 round pending) |
|
|
20
|
+
| SQL database | `createDbAdapter(config)` → `DbAdapter` | Bun SQLite via `@gobing-ai/ts-db` (optional peer) | throws `D1NotConfiguredError` (D1 round pending) |
|
|
21
21
|
| Configuration | `Config` (Zod schema) | YAML + env vars | CONFIG_YAML blob + env vars |
|
|
22
22
|
| Context | `RuntimeContext` | service locator | service locator |
|
|
23
23
|
| Path utilities | `SEP`, `basenamePath`, `dirnamePath`, `joinPath`, `resolvePath`, `relativePath`, … | runtime-portable (zero `node:*`) | runtime-portable (zero `node:*`) |
|
|
@@ -63,6 +63,8 @@ classDiagram
|
|
|
63
63
|
+stat(path) FileStat~|~null~|~Promise~FileStat|~
|
|
64
64
|
+resolve(...segments) string
|
|
65
65
|
+getProjectRoot() string
|
|
66
|
+
+readFileStream(path) AsyncIterable<string>▀optional (ADR-021)
|
|
67
|
+
+realPath(path) string▀optional (ADR-022)
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
class createNodeFileSystem {
|
|
@@ -72,6 +74,8 @@ classDiagram
|
|
|
72
74
|
+ensureDir(path) void
|
|
73
75
|
+stat(path) FileStat | null
|
|
74
76
|
+getProjectRoot() string
|
|
77
|
+
+readFileStream(path) AsyncIterable<string>
|
|
78
|
+
+realPath(path) string
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
class createCfFileSystem {
|
|
@@ -346,6 +350,24 @@ assertRelativeExtensionPath('/etc/evil.ts'); // throws: must be relative
|
|
|
346
350
|
assertRelativeExtensionPath('../escape.ts'); // throws: must not contain ".."
|
|
347
351
|
```
|
|
348
352
|
|
|
353
|
+
The string-level guard does not resolve symlinks — a symlink inside `baseDir` that points outside passes
|
|
354
|
+
the check. For symlink-safe confinement (ADR-022), supply a `realPath` canonicalizer to `LoadExtensionsOptions`:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { loadExtensionModules } from '@gobing-ai/ts-runtime/extension';
|
|
358
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
359
|
+
|
|
360
|
+
const fs = createNodeFileSystem('/abs/path');
|
|
361
|
+
await loadExtensionModules(refs, {
|
|
362
|
+
allowExtensions: true,
|
|
363
|
+
moduleLoader: (p) => import(p),
|
|
364
|
+
realPath: fs.realPath, // enables symlink confinement check
|
|
365
|
+
}, register);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
When `realPath` is provided, the loader canonicalizes both the resolved path and `baseDir`, rejecting
|
|
369
|
+
symlinks that escape the declaring directory. When absent (e.g. CF Workers), the check is skipped.
|
|
370
|
+
|
|
349
371
|
### 7. File system abstraction
|
|
350
372
|
|
|
351
373
|
Use `createNodeFileSystem()` for a real `node:fs`-backed filesystem (Node/Bun) or
|
|
@@ -366,6 +388,26 @@ cffs.getProjectRoot(); // '/bundle'
|
|
|
366
388
|
cffs.readFile('/x'); // throws: "use D1, KV, or R2"
|
|
367
389
|
```
|
|
368
390
|
|
|
391
|
+
Optional streaming and symlink-safe operations:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
// Stream large files line-by-line (ADR-021) — Node/Bun only.
|
|
395
|
+
// The JSONL importer uses this to avoid buffering multi-GB files.
|
|
396
|
+
if (fs.readFileStream) {
|
|
397
|
+
for await (const line of fs.readFileStream('data/huge.jsonl')) {
|
|
398
|
+
// process one line at a time
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Canonical path resolution for symlink-safe extension confinement (ADR-022).
|
|
403
|
+
// The extension loader uses this to verify that a symlink hasn't escaped baseDir.
|
|
404
|
+
if (fs.realPath) {
|
|
405
|
+
const real = fs.realPath('/base/extensions/linked.ts');
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Both are optional — CF Workers stubs omit them. The JSONL importer falls back to `readFile` + split when `readFileStream` is absent; the extension loader skips the symlink check when `realPath` is absent.
|
|
410
|
+
|
|
369
411
|
The old `getFs()` / `setFileSystem` global swap and `SyncFileSystem` are marked `@deprecated` —
|
|
370
412
|
use `createNodeFileSystem()` or `ctx.require('fileSystem')` instead.
|
|
371
413
|
|
|
@@ -392,11 +434,25 @@ On Cloudflare Workers, `capabilities.hasSqlDatabase` is `false` and `createDbAda
|
|
|
392
434
|
`D1NotConfiguredError` — the method exists on the interface so consumer code is forward-compatible and
|
|
393
435
|
needs no change when the D1 round ships.
|
|
394
436
|
|
|
395
|
-
**Dependency note:** `@gobing-ai/ts-db` is
|
|
396
|
-
|
|
397
|
-
`
|
|
398
|
-
`
|
|
399
|
-
`
|
|
437
|
+
**Dependency note (optional peer):** `@gobing-ai/ts-db` is declared as an **optional
|
|
438
|
+
`peerDependency`** of `ts-runtime` (ADR-012 addendum), not a regular dependency — a regular entry
|
|
439
|
+
would create a manifest cycle (`ts-db` depends on `ts-runtime`) and force-install `ts-db` (plus its
|
|
440
|
+
`drizzle-orm` peer) on every consumer, including Workers bundles and apps that never touch SQL.
|
|
441
|
+
The factory interface uses a structural `RuntimeDbAdapter` type (defined locally) that a ts-db
|
|
442
|
+
`DbAdapter` satisfies via structural subtyping; `nodeBunFactory.createDbAdapter` loads `ts-db` via a
|
|
443
|
+
literal dynamic `import()` at runtime.
|
|
444
|
+
|
|
445
|
+
**Who must install it:** only consumers that call `nodeBunFactory.createDbAdapter` on Node/Bun —
|
|
446
|
+
install `@gobing-ai/ts-db` yourself (`bun add @gobing-ai/ts-db`). Workers consumers do not need it
|
|
447
|
+
(`capabilities.hasSqlDatabase` is `false`; the method throws `D1NotConfiguredError`).
|
|
448
|
+
|
|
449
|
+
**Bundling (`Bun --compile` / esbuild / Vite):** the literal specifier keeps `ts-db` bundler-visible,
|
|
450
|
+
so `Bun --compile` can fold it into a standalone binary. If you bundle for a different runtime, mark
|
|
451
|
+
`@gobing-ai/ts-db` `external` to preserve the dynamic import.
|
|
452
|
+
|
|
453
|
+
**Failure mode:** if `@gobing-ai/ts-db` is absent or exports no `createDbAdapter` (incompatible
|
|
454
|
+
version), `createDbAdapter` throws a typed `DbModuleNotInstalledError` (with the underlying resolution
|
|
455
|
+
error chained as `cause`) instead of a raw `MODULE_NOT_FOUND`.
|
|
400
456
|
|
|
401
457
|
### 9. Graceful disposal
|
|
402
458
|
|
package/dist/db-errors.d.ts
CHANGED
|
@@ -11,4 +11,17 @@
|
|
|
11
11
|
export declare class D1NotConfiguredError extends Error {
|
|
12
12
|
constructor(message?: string);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Thrown by {@link RuntimeFactory.createDbAdapter} on `node-bun` when the
|
|
16
|
+
* optional peer `@gobing-ai/ts-db` is not installed.
|
|
17
|
+
*
|
|
18
|
+
* `ts-db` is an **optional peerDependency** of `ts-runtime` (ADR-012 addendum):
|
|
19
|
+
* it cannot be a regular dependency because `ts-db` depends on `ts-runtime`
|
|
20
|
+
* (manifest cycle). Consumers who call `nodeBunFactory.createDbAdapter` must
|
|
21
|
+
* install `@gobing-ai/ts-db` themselves. This error surfaces a missing module
|
|
22
|
+
* as an actionable, typed failure instead of a raw `MODULE_NOT_FOUND`.
|
|
23
|
+
*/
|
|
24
|
+
export declare class DbModuleNotInstalledError extends Error {
|
|
25
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
26
|
+
}
|
|
14
27
|
//# sourceMappingURL=db-errors.d.ts.map
|
package/dist/db-errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db-errors.d.ts","sourceRoot":"","sources":["../src/db-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBAC/B,OAAO,SAAiE;CAIvF"}
|
|
1
|
+
{"version":3,"file":"db-errors.d.ts","sourceRoot":"","sources":["../src/db-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBAC/B,OAAO,SAAiE;CAIvF;AAED;;;;;;;;;GASG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBAE5C,OAAO,SAAoO,EAC3O,OAAO,CAAC,EAAE,YAAY;CAK7B"}
|
package/dist/db-errors.js
CHANGED
|
@@ -14,3 +14,19 @@ export class D1NotConfiguredError extends Error {
|
|
|
14
14
|
this.name = 'D1NotConfiguredError';
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown by {@link RuntimeFactory.createDbAdapter} on `node-bun` when the
|
|
19
|
+
* optional peer `@gobing-ai/ts-db` is not installed.
|
|
20
|
+
*
|
|
21
|
+
* `ts-db` is an **optional peerDependency** of `ts-runtime` (ADR-012 addendum):
|
|
22
|
+
* it cannot be a regular dependency because `ts-db` depends on `ts-runtime`
|
|
23
|
+
* (manifest cycle). Consumers who call `nodeBunFactory.createDbAdapter` must
|
|
24
|
+
* install `@gobing-ai/ts-db` themselves. This error surfaces a missing module
|
|
25
|
+
* as an actionable, typed failure instead of a raw `MODULE_NOT_FOUND`.
|
|
26
|
+
*/
|
|
27
|
+
export class DbModuleNotInstalledError extends Error {
|
|
28
|
+
constructor(message = '@gobing-ai/ts-db is not installed. It is an optional peer of @gobing-ai/ts-runtime, required only for createDbAdapter on node-bun. Install it (`bun add @gobing-ai/ts-db`) or, when bundling, mark `@gobing-ai/ts-db` external.', options) {
|
|
29
|
+
super(message, options);
|
|
30
|
+
this.name = 'DbModuleNotInstalledError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -37,6 +37,18 @@ export interface LoadExtensionsOptions {
|
|
|
37
37
|
* means the shared core has no ambient code-loading capability of its own.
|
|
38
38
|
*/
|
|
39
39
|
readonly moduleLoader: (absPath: string) => Promise<Record<string, unknown>>;
|
|
40
|
+
/**
|
|
41
|
+
* Optional canonical-path resolver for symlink-safe confinement (ADR-022).
|
|
42
|
+
*
|
|
43
|
+
* When provided, the loader resolves the **real** path of the extension module
|
|
44
|
+
* (following symlinks) and re-checks that it remains within `baseDir`'s real
|
|
45
|
+
* root. This closes the symlink-escape vector: an authored path like
|
|
46
|
+
* `extensions/legit.ts` can pass the string-level `..` guard but still point
|
|
47
|
+
* outside `baseDir` via a symlink. Callers that serve extension modules from a
|
|
48
|
+
* real filesystem (e.g. `createNodeFileSystem().realPath`) should supply this.
|
|
49
|
+
* Stub filesystems without symlinks (e.g. CF Workers) may omit it.
|
|
50
|
+
*/
|
|
51
|
+
readonly realPath?: (absPath: string) => string;
|
|
40
52
|
}
|
|
41
53
|
/** A validated extension module export: an object carrying at least a string `name`. */
|
|
42
54
|
export interface LoadedExtension {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-loader.d.ts","sourceRoot":"","sources":["../../src/extension/extension-loader.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY,CAAC,cAAc,SAAS,MAAM,GAAG,MAAM;IAChE,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC/B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IACtD;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"extension-loader.d.ts","sourceRoot":"","sources":["../../src/extension/extension-loader.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY,CAAC,cAAc,SAAS,MAAM,GAAG,MAAM;IAChE,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC/B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IACtD;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7E;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CACnD;AAED,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CAAC,cAAc,SAAS,MAAM,EACpE,IAAI,EAAE,SAAS,YAAY,CAAC,cAAc,CAAC,EAAE,EAC7C,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,EAAE,CAAC,GAAG,EAAE,YAAY,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAClG,OAAO,CAAC,IAAI,CAAC,CAsDf"}
|
|
@@ -35,6 +35,27 @@ export async function loadExtensionModules(refs, options, register) {
|
|
|
35
35
|
// imports a caller-supplied absolute path it did not resolve itself.
|
|
36
36
|
assertRelativeExtensionPath(ref.path, { sourceName: ref.sourceName });
|
|
37
37
|
const absPath = resolve(ref.baseDir, ref.path);
|
|
38
|
+
// ADR-022: when realPath is available, canonicalize and re-check confinement
|
|
39
|
+
// to close the symlink-escape vector — an authored path with no ".." can still
|
|
40
|
+
// resolve outside baseDir via a symlink.
|
|
41
|
+
if (options.realPath) {
|
|
42
|
+
let realAbs;
|
|
43
|
+
let realBase;
|
|
44
|
+
try {
|
|
45
|
+
realAbs = options.realPath(absPath);
|
|
46
|
+
realBase = options.realPath(ref.baseDir);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// Canonicalizers surface raw filesystem errors (e.g. ENOENT for a
|
|
50
|
+
// missing module) — rewrap with the declaring ref so the failing
|
|
51
|
+
// extension is identifiable from the message alone.
|
|
52
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
53
|
+
throw new Error(`"${ref.sourceName}" extension "${ref.path}" cannot be canonicalized: ${reason}`);
|
|
54
|
+
}
|
|
55
|
+
if (realAbs !== realBase && !realAbs.startsWith(`${realBase}/`) && !realAbs.startsWith(`${realBase}\\`)) {
|
|
56
|
+
throw new Error(`"${ref.sourceName}" extension "${ref.path}" resolves outside baseDir via symlink (real: "${realAbs}", base: "${realBase}") — refusing to load`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
38
59
|
const moduleExports = await options.moduleLoader(absPath);
|
|
39
60
|
const candidate = moduleExports.default ?? moduleExports.extension;
|
|
40
61
|
if (candidate === null ||
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Assert that an extension module path is relative and does not escape its
|
|
3
|
-
* declaring directory.
|
|
3
|
+
* declaring directory via string-level traversal.
|
|
4
4
|
*
|
|
5
5
|
* Extension declarations are data, and a path that is absolute or escapes via `..`
|
|
6
6
|
* is a trust-boundary violation even when extension loading is explicitly allowed.
|
|
7
7
|
* This is a standalone validator (not a schema refinement) so the loader can enforce
|
|
8
8
|
* it at load time, independent of any engine's config schema — defense in depth.
|
|
9
9
|
*
|
|
10
|
+
* This guard is string-level only: it rejects `..` segments and absolute paths in
|
|
11
|
+
* the declaration but does NOT resolve symlinks. A symlink inside `baseDir` that
|
|
12
|
+
* points outside it passes this check. For symlink-safe confinement, supply a
|
|
13
|
+
* `realPath` canonicalizer to `LoadExtensionsOptions` (ADR-022); the loader
|
|
14
|
+
* performs the filesystem-level check when `realPath` is provided.
|
|
15
|
+
*
|
|
10
16
|
* @throws When the path is absolute or contains a `..` traversal segment.
|
|
11
17
|
*/
|
|
12
18
|
export declare function assertRelativeExtensionPath(path: string, options?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-path.d.ts","sourceRoot":"","sources":["../../src/extension/extension-path.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"extension-path.d.ts","sourceRoot":"","sources":["../../src/extension/extension-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,CAQrG"}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Assert that an extension module path is relative and does not escape its
|
|
3
|
-
* declaring directory.
|
|
3
|
+
* declaring directory via string-level traversal.
|
|
4
4
|
*
|
|
5
5
|
* Extension declarations are data, and a path that is absolute or escapes via `..`
|
|
6
6
|
* is a trust-boundary violation even when extension loading is explicitly allowed.
|
|
7
7
|
* This is a standalone validator (not a schema refinement) so the loader can enforce
|
|
8
8
|
* it at load time, independent of any engine's config schema — defense in depth.
|
|
9
9
|
*
|
|
10
|
+
* This guard is string-level only: it rejects `..` segments and absolute paths in
|
|
11
|
+
* the declaration but does NOT resolve symlinks. A symlink inside `baseDir` that
|
|
12
|
+
* points outside it passes this check. For symlink-safe confinement, supply a
|
|
13
|
+
* `realPath` canonicalizer to `LoadExtensionsOptions` (ADR-022); the loader
|
|
14
|
+
* performs the filesystem-level check when `realPath` is provided.
|
|
15
|
+
*
|
|
10
16
|
* @throws When the path is absolute or contains a `..` traversal segment.
|
|
11
17
|
*/
|
|
12
18
|
export function assertRelativeExtensionPath(path, options = {}) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-system-node.d.ts","sourceRoot":"","sources":["../src/file-system-node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"file-system-node.d.ts","sourceRoot":"","sources":["../src/file-system-node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAmBH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,CA+E9D;AAWD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAaxD"}
|
package/dist/file-system-node.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `node:fs` fully, so this works on both runtimes without a Bun-specific
|
|
10
10
|
* variant.
|
|
11
11
|
*/
|
|
12
|
-
import { appendFileSync, cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
12
|
+
import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
13
13
|
import { dirname, resolve as resolvePath } from 'node:path';
|
|
14
14
|
/**
|
|
15
15
|
* Create a {@link FileSystem} backed by `node:fs`.
|
|
@@ -23,6 +23,24 @@ export function createNodeFileSystem(root) {
|
|
|
23
23
|
resolve: (...segments) => resolvePath(projectRoot, ...segments),
|
|
24
24
|
exists: (path) => existsSync(path),
|
|
25
25
|
readFile: (path) => readFileSync(path, 'utf-8'),
|
|
26
|
+
readFileStream: async function* (path) {
|
|
27
|
+
// WHY: large JSONL history files (100MB+) must not be loaded into memory
|
|
28
|
+
// all at once. createReadStream + manual line splitting lets the importer
|
|
29
|
+
// process records incrementally with constant memory.
|
|
30
|
+
const stream = createReadStream(path, { encoding: 'utf-8' });
|
|
31
|
+
let buffer = '';
|
|
32
|
+
for await (const chunk of stream) {
|
|
33
|
+
buffer += chunk;
|
|
34
|
+
const lines = buffer.split(/\r?\n/);
|
|
35
|
+
// Keep the last (possibly partial) line in the buffer.
|
|
36
|
+
buffer = lines.pop() ?? '';
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
yield line;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (buffer.length > 0)
|
|
42
|
+
yield buffer;
|
|
43
|
+
},
|
|
26
44
|
writeFile: (path, content) => {
|
|
27
45
|
ensureParentDir(path);
|
|
28
46
|
writeFileSync(path, content, 'utf-8');
|
|
@@ -62,6 +80,7 @@ export function createNodeFileSystem(root) {
|
|
|
62
80
|
return null;
|
|
63
81
|
}
|
|
64
82
|
},
|
|
83
|
+
realPath: (path) => realpathSync(path),
|
|
65
84
|
};
|
|
66
85
|
}
|
|
67
86
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
package/dist/file-system.d.ts
CHANGED
|
@@ -20,6 +20,18 @@ export interface FileSystem {
|
|
|
20
20
|
exists(path: string): boolean | Promise<boolean>;
|
|
21
21
|
/** Read file contents as UTF-8 string. Throws if not found. */
|
|
22
22
|
readFile(path: string): string | Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Read file contents as a stream of UTF-8 text lines.
|
|
25
|
+
*
|
|
26
|
+
* Returns an async iterable yielding one line per iteration (without
|
|
27
|
+
* trailing newline). This is the streaming counterpart of `readFile`
|
|
28
|
+
* for large files that should not be loaded into memory all at once.
|
|
29
|
+
*
|
|
30
|
+
* Optional: Cloudflare Workers and other stubs may omit this method.
|
|
31
|
+
* Callers must check for its presence before use and fall back to
|
|
32
|
+
* `readFile` + `split` when unavailable.
|
|
33
|
+
*/
|
|
34
|
+
readFileStream?(path: string): AsyncIterable<string>;
|
|
23
35
|
/** Write file contents, creating parent directories as needed. */
|
|
24
36
|
writeFile(path: string, content: string): void | Promise<void>;
|
|
25
37
|
/** Append content to a file, creating it if it doesn't exist. */
|
|
@@ -41,6 +53,18 @@ export interface FileSystem {
|
|
|
41
53
|
write(chunk: string): void;
|
|
42
54
|
end(): void;
|
|
43
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the canonical (symlink-free) absolute path.
|
|
58
|
+
*
|
|
59
|
+
* WHY: used by the extension loader (ADR-022) to verify that an extension
|
|
60
|
+
* module's real path — after following symlinks — still falls within its
|
|
61
|
+
* declaring `baseDir`. Without this, a symlink can bypass the string-level
|
|
62
|
+
* `..` guard and load code from outside the intended directory.
|
|
63
|
+
*
|
|
64
|
+
* Optional: stubs without a real filesystem (e.g. CF Workers) may omit this.
|
|
65
|
+
* When absent, the extension loader skips the symlink confinement check.
|
|
66
|
+
*/
|
|
67
|
+
realPath?(path: string): string;
|
|
44
68
|
/** Resolve path segments relative to the project root. */
|
|
45
69
|
resolve(...segments: string[]): string;
|
|
46
70
|
/** Get the project root directory path. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-system.d.ts","sourceRoot":"","sources":["../src/file-system.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,MAAM,WAAW,QAAQ;IACrB,MAAM,IAAI,OAAO,CAAC;IAClB,WAAW,IAAI,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACvB,mCAAmC;IACnC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEjD,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"file-system.d.ts","sourceRoot":"","sources":["../src/file-system.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,MAAM,WAAW,QAAQ;IACrB,MAAM,IAAI,OAAO,CAAC;IAClB,WAAW,IAAI,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACvB,mCAAmC;IACnC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEjD,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;;;;;;OAUG;IACH,cAAc,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAErD,kEAAkE;IAClE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/D,iEAAiE;IACjE,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE,kFAAkF;IAClF,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9C,2DAA2D;IAC3D,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEpD,8CAA8C;IAC9C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C,6CAA6C;IAC7C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExD,4CAA4C;IAC5C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,6EAA6E;IAC7E,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAE/D,6FAA6F;IAC7F,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,GAAG,IAAI,IAAI,CAAA;KAAE,CAAC;IAE7E;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAEhC,0DAA0D;IAC1D,OAAO,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAEvC,2CAA2C;IAC3C,cAAc,IAAI,MAAM,CAAC;CAC5B"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './config';
|
|
2
2
|
export * from './context';
|
|
3
3
|
export { createRuntimeContextFromFactory } from './context';
|
|
4
|
-
export { D1NotConfiguredError } from './db-errors';
|
|
4
|
+
export { D1NotConfiguredError, DbModuleNotInstalledError } from './db-errors';
|
|
5
5
|
export type { FileStat, FileSystem } from './file-system';
|
|
6
6
|
export { createCfFileSystem } from './file-system-cf';
|
|
7
7
|
export { createNodeFileSystem, findProjectRoot } from './file-system-node';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,+BAA+B,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,+BAA+B,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC9E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EACH,eAAe,EACf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,OAAO,EACP,aAAa,GAChB,MAAM,MAAM,CAAC;AACd,cAAc,QAAQ,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACjG,YAAY,EACR,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,UAAU,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC1E,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AAIxB,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAExG;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,sBAAsB,CAAC,CAAC;AAE3G;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,qBAAqB,CAAC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './config.js';
|
|
2
2
|
export * from './context.js';
|
|
3
3
|
export { createRuntimeContextFromFactory } from './context.js';
|
|
4
|
-
export { D1NotConfiguredError } from './db-errors.js';
|
|
4
|
+
export { D1NotConfiguredError, DbModuleNotInstalledError } from './db-errors.js';
|
|
5
5
|
export { createCfFileSystem } from './file-system-cf.js';
|
|
6
6
|
export { createNodeFileSystem, findProjectRoot } from './file-system-node.js';
|
|
7
7
|
export { atomicWriteFile, atomicWriteJson, createLogStream, ensureDirForFile, readJsonFile, walkDir, writeJsonFile, } from './fs.js';
|
package/dist/path.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare function isAbsolutePath(path: string): boolean;
|
|
|
8
8
|
export declare function dirnamePath(path: string): string;
|
|
9
9
|
/** Return the last segment of a path. Optionally strip a trailing extension. */
|
|
10
10
|
export declare function basenamePath(p: string, ext?: string): string;
|
|
11
|
+
/** Convert a file:// URL into the portable path format used by this package. */
|
|
12
|
+
export declare function fileUrlToPath(url: string): string;
|
|
11
13
|
/** Compute a platform-independent relative path from `from` to `to`. Both paths should be absolute. */
|
|
12
14
|
export declare function relativePath(from: string, to: string): string;
|
|
13
15
|
/** Joins path segments with `/`, normalizing separators and collapsing redundant slashes. */
|
package/dist/path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAIA,kGAAkG;AAClG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,oFAAoF;AACpF,eAAO,MAAM,GAAG,EAAE,MACgF,CAAC;AAEnG,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAmBD,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWhD;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAkB7D;AAED,6FAA6F;AAC7F,wBAAgB,QAAQ,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAMtD;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAqBzD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAIA,kGAAkG;AAClG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,oFAAoF;AACpF,eAAO,MAAM,GAAG,EAAE,MACgF,CAAC;AAEnG,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAmBD,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWhD;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAkBjD;AAED,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAkB7D;AAED,6FAA6F;AAC7F,wBAAgB,QAAQ,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAMtD;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAqBzD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
package/dist/path.js
CHANGED
|
@@ -54,6 +54,24 @@ export function basenamePath(p, ext) {
|
|
|
54
54
|
}
|
|
55
55
|
return base;
|
|
56
56
|
}
|
|
57
|
+
/** Convert a file:// URL into the portable path format used by this package. */
|
|
58
|
+
export function fileUrlToPath(url) {
|
|
59
|
+
const parsed = new URL(url);
|
|
60
|
+
if (parsed.protocol !== 'file:') {
|
|
61
|
+
throw new Error(`Expected file URL, got "${parsed.protocol}"`);
|
|
62
|
+
}
|
|
63
|
+
// Encoded separators would decode into extra path segments (e.g. "%2F..%2F"
|
|
64
|
+
// becoming "/../"), silently changing path semantics — reject like node:url does.
|
|
65
|
+
if (/%2f|%5c/i.test(parsed.pathname)) {
|
|
66
|
+
throw new Error('File URL path must not include encoded "/" or "\\" characters');
|
|
67
|
+
}
|
|
68
|
+
const pathname = decodeURIComponent(parsed.pathname);
|
|
69
|
+
const localPath = /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
|
|
70
|
+
if (parsed.hostname.length > 0 && parsed.hostname !== 'localhost') {
|
|
71
|
+
return normalizeSeparators(`//${parsed.hostname}${localPath}`);
|
|
72
|
+
}
|
|
73
|
+
return normalizeSeparators(localPath);
|
|
74
|
+
}
|
|
57
75
|
/** Compute a platform-independent relative path from `from` to `to`. Both paths should be absolute. */
|
|
58
76
|
export function relativePath(from, to) {
|
|
59
77
|
const fromParsed = pathParts(resolvePath(from));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-node-bun.d.ts","sourceRoot":"","sources":["../src/runtime-node-bun.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime-node-bun.d.ts","sourceRoot":"","sources":["../src/runtime-node-bun.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAUxD,oEAAoE;AACpE,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAqC5B,CAAC"}
|
package/dist/runtime-node-bun.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parse as parseYaml } from 'yaml';
|
|
2
2
|
import { buildConfigFromObject, getProcessEnv } from './config.js';
|
|
3
|
+
import { DbModuleNotInstalledError } from './db-errors.js';
|
|
3
4
|
import { createNodeFileSystem } from './file-system-node.js';
|
|
4
5
|
import { ProcessExecutor } from './process-executor.js';
|
|
5
6
|
// Lazy re-initialisable singleton for test isolation.
|
|
@@ -30,14 +31,22 @@ export const nodeBunFactory = {
|
|
|
30
31
|
return loadNodeConfig(options);
|
|
31
32
|
},
|
|
32
33
|
async createDbAdapter(config) {
|
|
33
|
-
// Dynamic import via
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
// Dynamic import via variable specifier — ts-db depends on ts-runtime,
|
|
35
|
+
// so a static (or literal-specifier dynamic) import forces tsc to resolve
|
|
36
|
+
// the module at build time, raising TS2307 when no prebuilt db dist/
|
|
37
|
+
// exists (CI/clean checkout). A variable specifier is opaque to tsc.
|
|
38
|
+
// ts-db is an optional peerDependency (ADR-012 addendum, task 0040).
|
|
39
|
+
const tsDbSpec = '@gobing-ai/ts-db';
|
|
40
|
+
let mod;
|
|
41
|
+
try {
|
|
42
|
+
mod = await import(tsDbSpec);
|
|
43
|
+
}
|
|
44
|
+
catch (cause) {
|
|
45
|
+
throw new DbModuleNotInstalledError(undefined, { cause });
|
|
46
|
+
}
|
|
47
|
+
if (typeof mod.createDbAdapter !== 'function') {
|
|
48
|
+
throw new DbModuleNotInstalledError('@gobing-ai/ts-db is installed but does not export createDbAdapter — the installed version may be incompatible or partial.');
|
|
49
|
+
}
|
|
41
50
|
return mod.createDbAdapter({ driver: 'bun-sqlite', url: config.url });
|
|
42
51
|
},
|
|
43
52
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-runtime",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "@gobing-ai/ts-runtime — Runtime abstractions for Bun, Node, and Cloudflare Workers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -58,15 +58,23 @@
|
|
|
58
58
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-runtime-v<version> && git push --tags' && exit 1"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
61
|
+
"@gobing-ai/ts-utils": "^0.4.7",
|
|
62
62
|
"execa": "^9.5.0",
|
|
63
63
|
"yaml": "^2.7.0",
|
|
64
64
|
"zod": "^4.1.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
67
|
+
"@gobing-ai/ts-db": "^0.4.7",
|
|
68
68
|
"@types/bun": "1.3.14"
|
|
69
69
|
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"@gobing-ai/ts-db": "^0.4.7"
|
|
72
|
+
},
|
|
73
|
+
"peerDependenciesMeta": {
|
|
74
|
+
"@gobing-ai/ts-db": {
|
|
75
|
+
"optional": true
|
|
76
|
+
}
|
|
77
|
+
},
|
|
70
78
|
"publishConfig": {
|
|
71
79
|
"access": "public"
|
|
72
80
|
}
|
package/src/db-errors.ts
CHANGED
|
@@ -14,3 +14,23 @@ export class D1NotConfiguredError extends Error {
|
|
|
14
14
|
this.name = 'D1NotConfiguredError';
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Thrown by {@link RuntimeFactory.createDbAdapter} on `node-bun` when the
|
|
20
|
+
* optional peer `@gobing-ai/ts-db` is not installed.
|
|
21
|
+
*
|
|
22
|
+
* `ts-db` is an **optional peerDependency** of `ts-runtime` (ADR-012 addendum):
|
|
23
|
+
* it cannot be a regular dependency because `ts-db` depends on `ts-runtime`
|
|
24
|
+
* (manifest cycle). Consumers who call `nodeBunFactory.createDbAdapter` must
|
|
25
|
+
* install `@gobing-ai/ts-db` themselves. This error surfaces a missing module
|
|
26
|
+
* as an actionable, typed failure instead of a raw `MODULE_NOT_FOUND`.
|
|
27
|
+
*/
|
|
28
|
+
export class DbModuleNotInstalledError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
message = '@gobing-ai/ts-db is not installed. It is an optional peer of @gobing-ai/ts-runtime, required only for createDbAdapter on node-bun. Install it (`bun add @gobing-ai/ts-db`) or, when bundling, mark `@gobing-ai/ts-db` external.',
|
|
31
|
+
options?: ErrorOptions,
|
|
32
|
+
) {
|
|
33
|
+
super(message, options);
|
|
34
|
+
this.name = 'DbModuleNotInstalledError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -39,6 +39,18 @@ export interface LoadExtensionsOptions {
|
|
|
39
39
|
* means the shared core has no ambient code-loading capability of its own.
|
|
40
40
|
*/
|
|
41
41
|
readonly moduleLoader: (absPath: string) => Promise<Record<string, unknown>>;
|
|
42
|
+
/**
|
|
43
|
+
* Optional canonical-path resolver for symlink-safe confinement (ADR-022).
|
|
44
|
+
*
|
|
45
|
+
* When provided, the loader resolves the **real** path of the extension module
|
|
46
|
+
* (following symlinks) and re-checks that it remains within `baseDir`'s real
|
|
47
|
+
* root. This closes the symlink-escape vector: an authored path like
|
|
48
|
+
* `extensions/legit.ts` can pass the string-level `..` guard but still point
|
|
49
|
+
* outside `baseDir` via a symlink. Callers that serve extension modules from a
|
|
50
|
+
* real filesystem (e.g. `createNodeFileSystem().realPath`) should supply this.
|
|
51
|
+
* Stub filesystems without symlinks (e.g. CF Workers) may omit it.
|
|
52
|
+
*/
|
|
53
|
+
readonly realPath?: (absPath: string) => string;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
/** A validated extension module export: an object carrying at least a string `name`. */
|
|
@@ -91,6 +103,28 @@ export async function loadExtensionModules<TExtensionKind extends string>(
|
|
|
91
103
|
// imports a caller-supplied absolute path it did not resolve itself.
|
|
92
104
|
assertRelativeExtensionPath(ref.path, { sourceName: ref.sourceName });
|
|
93
105
|
const absPath = resolve(ref.baseDir, ref.path);
|
|
106
|
+
// ADR-022: when realPath is available, canonicalize and re-check confinement
|
|
107
|
+
// to close the symlink-escape vector — an authored path with no ".." can still
|
|
108
|
+
// resolve outside baseDir via a symlink.
|
|
109
|
+
if (options.realPath) {
|
|
110
|
+
let realAbs: string;
|
|
111
|
+
let realBase: string;
|
|
112
|
+
try {
|
|
113
|
+
realAbs = options.realPath(absPath);
|
|
114
|
+
realBase = options.realPath(ref.baseDir);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// Canonicalizers surface raw filesystem errors (e.g. ENOENT for a
|
|
117
|
+
// missing module) — rewrap with the declaring ref so the failing
|
|
118
|
+
// extension is identifiable from the message alone.
|
|
119
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
120
|
+
throw new Error(`"${ref.sourceName}" extension "${ref.path}" cannot be canonicalized: ${reason}`);
|
|
121
|
+
}
|
|
122
|
+
if (realAbs !== realBase && !realAbs.startsWith(`${realBase}/`) && !realAbs.startsWith(`${realBase}\\`)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`"${ref.sourceName}" extension "${ref.path}" resolves outside baseDir via symlink (real: "${realAbs}", base: "${realBase}") — refusing to load`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
94
128
|
const moduleExports = await options.moduleLoader(absPath);
|
|
95
129
|
const candidate = moduleExports.default ?? moduleExports.extension;
|
|
96
130
|
if (
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Assert that an extension module path is relative and does not escape its
|
|
3
|
-
* declaring directory.
|
|
3
|
+
* declaring directory via string-level traversal.
|
|
4
4
|
*
|
|
5
5
|
* Extension declarations are data, and a path that is absolute or escapes via `..`
|
|
6
6
|
* is a trust-boundary violation even when extension loading is explicitly allowed.
|
|
7
7
|
* This is a standalone validator (not a schema refinement) so the loader can enforce
|
|
8
8
|
* it at load time, independent of any engine's config schema — defense in depth.
|
|
9
9
|
*
|
|
10
|
+
* This guard is string-level only: it rejects `..` segments and absolute paths in
|
|
11
|
+
* the declaration but does NOT resolve symlinks. A symlink inside `baseDir` that
|
|
12
|
+
* points outside it passes this check. For symlink-safe confinement, supply a
|
|
13
|
+
* `realPath` canonicalizer to `LoadExtensionsOptions` (ADR-022); the loader
|
|
14
|
+
* performs the filesystem-level check when `realPath` is provided.
|
|
15
|
+
*
|
|
10
16
|
* @throws When the path is absolute or contains a `..` traversal segment.
|
|
11
17
|
*/
|
|
12
18
|
export function assertRelativeExtensionPath(path: string, options: { sourceName?: string } = {}): void {
|
package/src/file-system-node.ts
CHANGED
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
import {
|
|
14
14
|
appendFileSync,
|
|
15
15
|
cpSync,
|
|
16
|
+
createReadStream,
|
|
16
17
|
createWriteStream,
|
|
17
18
|
existsSync,
|
|
18
19
|
mkdirSync,
|
|
19
20
|
readdirSync,
|
|
20
21
|
readFileSync,
|
|
22
|
+
realpathSync,
|
|
21
23
|
renameSync,
|
|
22
24
|
rmSync,
|
|
23
25
|
statSync,
|
|
@@ -44,6 +46,24 @@ export function createNodeFileSystem(root?: string): FileSystem {
|
|
|
44
46
|
|
|
45
47
|
readFile: (path: string) => readFileSync(path, 'utf-8'),
|
|
46
48
|
|
|
49
|
+
readFileStream: async function* (path: string): AsyncIterable<string> {
|
|
50
|
+
// WHY: large JSONL history files (100MB+) must not be loaded into memory
|
|
51
|
+
// all at once. createReadStream + manual line splitting lets the importer
|
|
52
|
+
// process records incrementally with constant memory.
|
|
53
|
+
const stream = createReadStream(path, { encoding: 'utf-8' });
|
|
54
|
+
let buffer = '';
|
|
55
|
+
for await (const chunk of stream) {
|
|
56
|
+
buffer += chunk;
|
|
57
|
+
const lines = buffer.split(/\r?\n/);
|
|
58
|
+
// Keep the last (possibly partial) line in the buffer.
|
|
59
|
+
buffer = lines.pop() ?? '';
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
yield line;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (buffer.length > 0) yield buffer;
|
|
65
|
+
},
|
|
66
|
+
|
|
47
67
|
writeFile: (path: string, content: string) => {
|
|
48
68
|
ensureParentDir(path);
|
|
49
69
|
writeFileSync(path, content, 'utf-8');
|
|
@@ -90,6 +110,8 @@ export function createNodeFileSystem(root?: string): FileSystem {
|
|
|
90
110
|
return null;
|
|
91
111
|
}
|
|
92
112
|
},
|
|
113
|
+
|
|
114
|
+
realPath: (path: string) => realpathSync(path),
|
|
93
115
|
};
|
|
94
116
|
}
|
|
95
117
|
|
package/src/file-system.ts
CHANGED
|
@@ -22,6 +22,18 @@ export interface FileSystem {
|
|
|
22
22
|
|
|
23
23
|
/** Read file contents as UTF-8 string. Throws if not found. */
|
|
24
24
|
readFile(path: string): string | Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Read file contents as a stream of UTF-8 text lines.
|
|
27
|
+
*
|
|
28
|
+
* Returns an async iterable yielding one line per iteration (without
|
|
29
|
+
* trailing newline). This is the streaming counterpart of `readFile`
|
|
30
|
+
* for large files that should not be loaded into memory all at once.
|
|
31
|
+
*
|
|
32
|
+
* Optional: Cloudflare Workers and other stubs may omit this method.
|
|
33
|
+
* Callers must check for its presence before use and fall back to
|
|
34
|
+
* `readFile` + `split` when unavailable.
|
|
35
|
+
*/
|
|
36
|
+
readFileStream?(path: string): AsyncIterable<string>;
|
|
25
37
|
|
|
26
38
|
/** Write file contents, creating parent directories as needed. */
|
|
27
39
|
writeFile(path: string, content: string): void | Promise<void>;
|
|
@@ -49,6 +61,19 @@ export interface FileSystem {
|
|
|
49
61
|
/** Create a writable stream for append-only output (Node/Bun only). Throws on CF Workers. */
|
|
50
62
|
createWriteStream(path: string): { write(chunk: string): void; end(): void };
|
|
51
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the canonical (symlink-free) absolute path.
|
|
66
|
+
*
|
|
67
|
+
* WHY: used by the extension loader (ADR-022) to verify that an extension
|
|
68
|
+
* module's real path — after following symlinks — still falls within its
|
|
69
|
+
* declaring `baseDir`. Without this, a symlink can bypass the string-level
|
|
70
|
+
* `..` guard and load code from outside the intended directory.
|
|
71
|
+
*
|
|
72
|
+
* Optional: stubs without a real filesystem (e.g. CF Workers) may omit this.
|
|
73
|
+
* When absent, the extension loader skips the symlink confinement check.
|
|
74
|
+
*/
|
|
75
|
+
realPath?(path: string): string;
|
|
76
|
+
|
|
52
77
|
/** Resolve path segments relative to the project root. */
|
|
53
78
|
resolve(...segments: string[]): string;
|
|
54
79
|
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './config';
|
|
2
2
|
export * from './context';
|
|
3
3
|
export { createRuntimeContextFromFactory } from './context';
|
|
4
|
-
export { D1NotConfiguredError } from './db-errors';
|
|
4
|
+
export { D1NotConfiguredError, DbModuleNotInstalledError } from './db-errors';
|
|
5
5
|
export type { FileStat, FileSystem } from './file-system';
|
|
6
6
|
export { createCfFileSystem } from './file-system-cf';
|
|
7
7
|
export { createNodeFileSystem, findProjectRoot } from './file-system-node';
|
package/src/path.ts
CHANGED
|
@@ -58,6 +58,27 @@ export function basenamePath(p: string, ext?: string): string {
|
|
|
58
58
|
return base;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Convert a file:// URL into the portable path format used by this package. */
|
|
62
|
+
export function fileUrlToPath(url: string): string {
|
|
63
|
+
const parsed = new URL(url);
|
|
64
|
+
if (parsed.protocol !== 'file:') {
|
|
65
|
+
throw new Error(`Expected file URL, got "${parsed.protocol}"`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Encoded separators would decode into extra path segments (e.g. "%2F..%2F"
|
|
69
|
+
// becoming "/../"), silently changing path semantics — reject like node:url does.
|
|
70
|
+
if (/%2f|%5c/i.test(parsed.pathname)) {
|
|
71
|
+
throw new Error('File URL path must not include encoded "/" or "\\" characters');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pathname = decodeURIComponent(parsed.pathname);
|
|
75
|
+
const localPath = /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
|
|
76
|
+
if (parsed.hostname.length > 0 && parsed.hostname !== 'localhost') {
|
|
77
|
+
return normalizeSeparators(`//${parsed.hostname}${localPath}`);
|
|
78
|
+
}
|
|
79
|
+
return normalizeSeparators(localPath);
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
/** Compute a platform-independent relative path from `from` to `to`. Both paths should be absolute. */
|
|
62
83
|
export function relativePath(from: string, to: string): string {
|
|
63
84
|
const fromParsed = pathParts(resolvePath(from));
|
package/src/runtime-node-bun.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parse as parseYaml } from 'yaml';
|
|
2
2
|
import type { Config } from './config';
|
|
3
3
|
import { buildConfigFromObject, getProcessEnv } from './config';
|
|
4
|
+
import { DbModuleNotInstalledError } from './db-errors';
|
|
4
5
|
import type { FileSystem } from './file-system';
|
|
5
6
|
import { createNodeFileSystem } from './file-system-node';
|
|
6
7
|
import { ProcessExecutor, type ProcessExecutorConfig } from './process-executor';
|
|
@@ -40,16 +41,23 @@ export const nodeBunFactory: RuntimeFactory = {
|
|
|
40
41
|
},
|
|
41
42
|
|
|
42
43
|
async createDbAdapter(config: DatabaseConfig): Promise<RuntimeDbAdapter> {
|
|
43
|
-
// Dynamic import via
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
44
|
+
// Dynamic import via variable specifier — ts-db depends on ts-runtime,
|
|
45
|
+
// so a static (or literal-specifier dynamic) import forces tsc to resolve
|
|
46
|
+
// the module at build time, raising TS2307 when no prebuilt db dist/
|
|
47
|
+
// exists (CI/clean checkout). A variable specifier is opaque to tsc.
|
|
48
|
+
// ts-db is an optional peerDependency (ADR-012 addendum, task 0040).
|
|
49
|
+
const tsDbSpec = '@gobing-ai/ts-db';
|
|
50
|
+
let mod: { createDbAdapter: (config: DatabaseConfig) => Promise<RuntimeDbAdapter> };
|
|
51
|
+
try {
|
|
52
|
+
mod = await import(tsDbSpec);
|
|
53
|
+
} catch (cause) {
|
|
54
|
+
throw new DbModuleNotInstalledError(undefined, { cause });
|
|
55
|
+
}
|
|
56
|
+
if (typeof mod.createDbAdapter !== 'function') {
|
|
57
|
+
throw new DbModuleNotInstalledError(
|
|
58
|
+
'@gobing-ai/ts-db is installed but does not export createDbAdapter — the installed version may be incompatible or partial.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
53
61
|
return mod.createDbAdapter({ driver: 'bun-sqlite', url: config.url });
|
|
54
62
|
},
|
|
55
63
|
};
|