ffp-sql-sandbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTEGRATION.md +37 -0
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/docker-executor.d.ts +6 -0
- package/dist/docker-executor.d.ts.map +1 -0
- package/dist/docker-executor.js +192 -0
- package/dist/docker-executor.js.map +1 -0
- package/dist/docker-types.d.ts +37 -0
- package/dist/docker-types.d.ts.map +1 -0
- package/dist/docker-types.js +50 -0
- package/dist/docker-types.js.map +1 -0
- package/dist/execute-sql.d.ts +9 -0
- package/dist/execute-sql.d.ts.map +1 -0
- package/dist/execute-sql.js +89 -0
- package/dist/execute-sql.js.map +1 -0
- package/dist/host-allowlist.d.ts +2 -0
- package/dist/host-allowlist.d.ts.map +1 -0
- package/dist/host-allowlist.js +9 -0
- package/dist/host-allowlist.js.map +1 -0
- package/dist/image.d.ts +16 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +19 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/limits.d.ts +11 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +25 -0
- package/dist/limits.js.map +1 -0
- package/dist/password-tar.d.ts +6 -0
- package/dist/password-tar.d.ts.map +1 -0
- package/dist/password-tar.js +29 -0
- package/dist/password-tar.js.map +1 -0
- package/dist/rewrite-host.d.ts +6 -0
- package/dist/rewrite-host.d.ts.map +1 -0
- package/dist/rewrite-host.js +16 -0
- package/dist/rewrite-host.js.map +1 -0
- package/dist/stream-consumer.d.ts +31 -0
- package/dist/stream-consumer.d.ts.map +1 -0
- package/dist/stream-consumer.js +139 -0
- package/dist/stream-consumer.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-sql.d.ts +8 -0
- package/dist/validate-sql.d.ts.map +1 -0
- package/dist/validate-sql.js +30 -0
- package/dist/validate-sql.js.map +1 -0
- package/package.json +45 -0
- package/sandbox/Dockerfile +13 -0
- package/sandbox/events.d.ts +12 -0
- package/sandbox/events.js +14 -0
- package/sandbox/execute.js +235 -0
- package/sandbox/package.json +10 -0
- package/sandbox/secrets.d.ts +5 -0
- package/sandbox/secrets.js +16 -0
- package/sandbox/session-setup.d.ts +4 -0
- package/sandbox/session-setup.js +20 -0
- package/sandbox/stream-limit.d.ts +16 -0
- package/sandbox/stream-limit.js +35 -0
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default runner image. Digest-pinned in code; do not use a floating tag.
|
|
3
|
+
*
|
|
4
|
+
* This pin is the v1 identity of `sandbox/Dockerfile`. It is **not** a
|
|
5
|
+
* GHCR-pullable digest until the runner is published. `executeSql` without
|
|
6
|
+
* `image` returns `IMAGE_UNAVAILABLE` if Docker cannot find it. After the
|
|
7
|
+
* first GHCR publish, replace this with `docker buildx imagetools inspect`.
|
|
8
|
+
* Custom `image` overrides are untrusted.
|
|
9
|
+
*
|
|
10
|
+
* Content identity (sha256 of the runner files at this freeze):
|
|
11
|
+
* sandbox/Dockerfile + execute.js + stream-limit.js + session-setup.js +
|
|
12
|
+
* secrets.js + events.js + package.json.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_SANDBOX_IMAGE = "ghcr.io/ffp-tech-lab/ffp-sql-sandbox-runner@sha256:861dd5eb53f5c3eb468b73b6068385f0688910fc10ba68610d20bb3de9f78952";
|
|
15
|
+
export declare function isDigestPinnedImage(image: string): boolean;
|
|
16
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,wHACqF,CAAC;AAExH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D"}
|
package/dist/image.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const DIGEST_PINNED = /^.+@sha256:[a-f0-9]{64}$/i;
|
|
2
|
+
/**
|
|
3
|
+
* Default runner image. Digest-pinned in code; do not use a floating tag.
|
|
4
|
+
*
|
|
5
|
+
* This pin is the v1 identity of `sandbox/Dockerfile`. It is **not** a
|
|
6
|
+
* GHCR-pullable digest until the runner is published. `executeSql` without
|
|
7
|
+
* `image` returns `IMAGE_UNAVAILABLE` if Docker cannot find it. After the
|
|
8
|
+
* first GHCR publish, replace this with `docker buildx imagetools inspect`.
|
|
9
|
+
* Custom `image` overrides are untrusted.
|
|
10
|
+
*
|
|
11
|
+
* Content identity (sha256 of the runner files at this freeze):
|
|
12
|
+
* sandbox/Dockerfile + execute.js + stream-limit.js + session-setup.js +
|
|
13
|
+
* secrets.js + events.js + package.json.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_SANDBOX_IMAGE = 'ghcr.io/ffp-tech-lab/ffp-sql-sandbox-runner@sha256:861dd5eb53f5c3eb468b73b6068385f0688910fc10ba68610d20bb3de9f78952';
|
|
16
|
+
export function isDigestPinnedImage(image) {
|
|
17
|
+
return DIGEST_PINNED.test(image.trim());
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAAA,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAChC,qHAAqH,CAAC;AAExH,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,OAAO,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AAC1C,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExecuteSqlInput, ExecuteSqlResult } from './types.js';
|
|
2
|
+
export { validateSql } from './validate-sql.js';
|
|
3
|
+
export { DEFAULT_SANDBOX_IMAGE } from './image.js';
|
|
4
|
+
export { DEFAULT_SANDBOX_LIMITS } from './types.js';
|
|
5
|
+
export type { ExecuteSqlFailure, ExecuteSqlInput, ExecuteSqlResult, ExecuteSqlSuccess, QueryResultData, SandboxConnection, SandboxLimits, SqlDialect, ValidateSqlFailure, ValidateSqlResult, ValidateSqlSuccess, } from './types.js';
|
|
6
|
+
/** Public executeSql: Docker client injection is not part of the supported API. */
|
|
7
|
+
export declare function executeSql(input: ExecuteSqlInput): Promise<ExecuteSqlResult>;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEpE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAEpB,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAE5E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { executeSql as executeSqlWithDeps } from './execute-sql.js';
|
|
2
|
+
export { validateSql } from './validate-sql.js';
|
|
3
|
+
export { DEFAULT_SANDBOX_IMAGE } from './image.js';
|
|
4
|
+
export { DEFAULT_SANDBOX_LIMITS } from './types.js';
|
|
5
|
+
/** Public executeSql: Docker client injection is not part of the supported API. */
|
|
6
|
+
export function executeSql(input) {
|
|
7
|
+
return executeSqlWithDeps(input);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGpE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAepD,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC/C,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC;AACnC,CAAC"}
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SandboxLimits } from './types.js';
|
|
2
|
+
export type LimitsError = {
|
|
3
|
+
ok: false;
|
|
4
|
+
error: string;
|
|
5
|
+
};
|
|
6
|
+
export type LimitsOk = {
|
|
7
|
+
ok: true;
|
|
8
|
+
limits: SandboxLimits;
|
|
9
|
+
};
|
|
10
|
+
export declare function mergeLimits(partial?: Partial<SandboxLimits>): LimitsOk | LimitsError;
|
|
11
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,CAAC;AAU3D,wBAAgB,WAAW,CACzB,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAC/B,QAAQ,GAAG,WAAW,CAiBxB"}
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEFAULT_SANDBOX_LIMITS, } from './types.js';
|
|
2
|
+
const LIMIT_KEYS = [
|
|
3
|
+
'timeoutMs',
|
|
4
|
+
'memoryMb',
|
|
5
|
+
'nanoCpus',
|
|
6
|
+
'maxRows',
|
|
7
|
+
'maxBytes',
|
|
8
|
+
];
|
|
9
|
+
export function mergeLimits(partial) {
|
|
10
|
+
const limits = {
|
|
11
|
+
...DEFAULT_SANDBOX_LIMITS,
|
|
12
|
+
...partial,
|
|
13
|
+
};
|
|
14
|
+
for (const key of LIMIT_KEYS) {
|
|
15
|
+
const value = limits[key];
|
|
16
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
error: `${key} must be a positive integer`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { ok: true, limits };
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,GAEvB,MAAM,YAAY,CAAC;AAKpB,MAAM,UAAU,GAAG;IACjB,WAAW;IACX,UAAU;IACV,UAAU;IACV,SAAS;IACT,UAAU;CACF,CAAC;AAEX,MAAM,UAAU,WAAW,CACzB,OAAgC;IAEhC,MAAM,MAAM,GAAkB;QAC5B,GAAG,sBAAsB;QACzB,GAAG,OAAO;KACX,CAAC;IAEF,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAC3C,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,GAAG,GAAG,6BAA6B;aAC3C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a ustar archive containing a single file `db_password`.
|
|
3
|
+
* Used to drop the DB password onto the container tmpfs (never Env).
|
|
4
|
+
*/
|
|
5
|
+
export declare function createPasswordTar(password: string, uid?: number): Buffer;
|
|
6
|
+
//# sourceMappingURL=password-tar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password-tar.d.ts","sourceRoot":"","sources":["../src/password-tar.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAO,GAAG,MAAM,CAuBtE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a ustar archive containing a single file `db_password`.
|
|
3
|
+
* Used to drop the DB password onto the container tmpfs (never Env).
|
|
4
|
+
*/
|
|
5
|
+
export function createPasswordTar(password, uid = 1000) {
|
|
6
|
+
const content = Buffer.from(password, 'utf8');
|
|
7
|
+
const header = Buffer.alloc(512);
|
|
8
|
+
header.write('db_password', 0, 100, 'utf8');
|
|
9
|
+
header.write('0000400\0', 100, 8, 'utf8');
|
|
10
|
+
header.write(octal(uid, 7) + '\0', 108, 8, 'utf8');
|
|
11
|
+
header.write(octal(uid, 7) + '\0', 116, 8, 'utf8');
|
|
12
|
+
header.write(octal(content.length, 11) + '\0', 124, 12, 'utf8');
|
|
13
|
+
header.write(octal(0, 11) + '\0', 136, 12, 'utf8');
|
|
14
|
+
header.write(' ', 148, 8, 'utf8');
|
|
15
|
+
header.write('0', 156, 1, 'utf8');
|
|
16
|
+
header.write('ustar\0', 257, 6, 'utf8');
|
|
17
|
+
header.write('00', 263, 2, 'utf8');
|
|
18
|
+
let checksum = 0;
|
|
19
|
+
for (let i = 0; i < header.length; i += 1) {
|
|
20
|
+
checksum += header[i] ?? 0;
|
|
21
|
+
}
|
|
22
|
+
header.write(octal(checksum, 6) + '\0 ', 148, 8, 'utf8');
|
|
23
|
+
const pad = content.length % 512 === 0 ? 0 : 512 - (content.length % 512);
|
|
24
|
+
return Buffer.concat([header, content, Buffer.alloc(pad), Buffer.alloc(1024)]);
|
|
25
|
+
}
|
|
26
|
+
function octal(value, width) {
|
|
27
|
+
return value.toString(8).padStart(width, '0');
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=password-tar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password-tar.js","sourceRoot":"","sources":["../src/password-tar.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,GAAG,GAAG,IAAI;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IAC5C,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1C,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IACnD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IACnD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IAChE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IACnD,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IAClC,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IACxC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IAEnC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IAEzD,MAAM,GAAG,GACP,OAAO,CAAC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;IAChE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,SAAS,KAAK,CAAC,KAAa,EAAE,KAAa;IACzC,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite-host.d.ts","sourceRoot":"","sources":["../src/rewrite-host.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGxD"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const LOOPBACK_HOSTS = new Set([
|
|
2
|
+
'localhost',
|
|
3
|
+
'127.0.0.1',
|
|
4
|
+
'::1',
|
|
5
|
+
'[::1]',
|
|
6
|
+
'0.0.0.0',
|
|
7
|
+
]);
|
|
8
|
+
/**
|
|
9
|
+
* Private container DNS rewrite. Not part of the public API.
|
|
10
|
+
* Allowlisting happens on the caller-supplied host before this is applied.
|
|
11
|
+
*/
|
|
12
|
+
export function rewriteLoopbackHost(host) {
|
|
13
|
+
const normalized = host.trim().toLowerCase();
|
|
14
|
+
return LOOPBACK_HOSTS.has(normalized) ? 'host.docker.internal' : host.trim();
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=rewrite-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite-host.js","sourceRoot":"","sources":["../src/rewrite-host.ts"],"names":[],"mappings":"AAAA,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,WAAW;IACX,WAAW;IACX,KAAK;IACL,OAAO;IACP,SAAS;CACV,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC7C,OAAO,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type RunnerEvent = {
|
|
2
|
+
type: 'meta';
|
|
3
|
+
columns: string[];
|
|
4
|
+
} | {
|
|
5
|
+
type: 'row';
|
|
6
|
+
values: unknown[];
|
|
7
|
+
} | {
|
|
8
|
+
type: 'end';
|
|
9
|
+
truncated?: boolean;
|
|
10
|
+
reason?: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'error';
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
export type ConsumeLimits = {
|
|
16
|
+
maxRows: number;
|
|
17
|
+
maxBytes: number;
|
|
18
|
+
abort: () => void;
|
|
19
|
+
};
|
|
20
|
+
export type ConsumeResult = {
|
|
21
|
+
columns: string[];
|
|
22
|
+
rows: unknown[][];
|
|
23
|
+
truncated: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Incrementally parse runner NDJSON from a live stream.
|
|
28
|
+
* Limits are applied as chunks arrive — not after buffering the full result.
|
|
29
|
+
*/
|
|
30
|
+
export declare function consumeNdjsonResult(stream: AsyncIterable<Buffer | string>, limits: ConsumeLimits): Promise<ConsumeResult>;
|
|
31
|
+
//# sourceMappingURL=stream-consumer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-consumer.d.ts","sourceRoot":"","sources":["../src/stream-consumer.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErC,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,EACtC,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,aAAa,CAAC,CAsFxB"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incrementally parse runner NDJSON from a live stream.
|
|
3
|
+
* Limits are applied as chunks arrive — not after buffering the full result.
|
|
4
|
+
*/
|
|
5
|
+
export async function consumeNdjsonResult(stream, limits) {
|
|
6
|
+
const rows = [];
|
|
7
|
+
let columns = [];
|
|
8
|
+
let truncated = false;
|
|
9
|
+
let error;
|
|
10
|
+
let bytes = 0;
|
|
11
|
+
let lineBuf = '';
|
|
12
|
+
let aborted = false;
|
|
13
|
+
const abortOnce = () => {
|
|
14
|
+
if (aborted) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
aborted = true;
|
|
18
|
+
truncated = true;
|
|
19
|
+
limits.abort();
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
for await (const chunk of stream) {
|
|
23
|
+
if (aborted) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
27
|
+
bytes += buf.byteLength;
|
|
28
|
+
if (bytes > limits.maxBytes) {
|
|
29
|
+
abortOnce();
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
lineBuf += buf.toString('utf8');
|
|
33
|
+
let newline = lineBuf.indexOf('\n');
|
|
34
|
+
while (newline !== -1) {
|
|
35
|
+
const line = lineBuf.slice(0, newline).trim();
|
|
36
|
+
lineBuf = lineBuf.slice(newline + 1);
|
|
37
|
+
if (line.length > 0) {
|
|
38
|
+
const event = parseRunnerEvent(line);
|
|
39
|
+
if (event) {
|
|
40
|
+
switch (event.type) {
|
|
41
|
+
case 'meta':
|
|
42
|
+
columns = event.columns;
|
|
43
|
+
break;
|
|
44
|
+
case 'row':
|
|
45
|
+
if (rows.length >= limits.maxRows) {
|
|
46
|
+
abortOnce();
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
rows.push(event.values);
|
|
50
|
+
if (rows.length >= limits.maxRows) {
|
|
51
|
+
abortOnce();
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case 'end':
|
|
55
|
+
if (event.truncated) {
|
|
56
|
+
truncated = true;
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
case 'error':
|
|
60
|
+
error = event.error;
|
|
61
|
+
break;
|
|
62
|
+
default: {
|
|
63
|
+
const _never = event;
|
|
64
|
+
void _never;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (aborted) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
newline = lineBuf.indexOf('\n');
|
|
74
|
+
}
|
|
75
|
+
if (aborted) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Stream destroy / abort is expected when limits fire.
|
|
82
|
+
}
|
|
83
|
+
const result = { columns, rows, truncated };
|
|
84
|
+
if (error !== undefined) {
|
|
85
|
+
result.error = error;
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
function parseRunnerEvent(line) {
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(line);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (parsed === null || typeof parsed !== 'object' || !('type' in parsed)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const record = parsed;
|
|
101
|
+
switch (record.type) {
|
|
102
|
+
case 'meta': {
|
|
103
|
+
const columns = parsed.columns;
|
|
104
|
+
if (!Array.isArray(columns) || !columns.every((c) => typeof c === 'string')) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return { type: 'meta', columns };
|
|
108
|
+
}
|
|
109
|
+
case 'row': {
|
|
110
|
+
const values = parsed.values;
|
|
111
|
+
if (!Array.isArray(values)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return { type: 'row', values };
|
|
115
|
+
}
|
|
116
|
+
case 'end': {
|
|
117
|
+
const truncated = parsed.truncated;
|
|
118
|
+
const reason = parsed.reason;
|
|
119
|
+
const event = { type: 'end' };
|
|
120
|
+
if (typeof truncated === 'boolean') {
|
|
121
|
+
event.truncated = truncated;
|
|
122
|
+
}
|
|
123
|
+
if (typeof reason === 'string') {
|
|
124
|
+
event.reason = reason;
|
|
125
|
+
}
|
|
126
|
+
return event;
|
|
127
|
+
}
|
|
128
|
+
case 'error': {
|
|
129
|
+
const error = parsed.error;
|
|
130
|
+
if (typeof error !== 'string') {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return { type: 'error', error };
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=stream-consumer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-consumer.js","sourceRoot":"","sources":["../src/stream-consumer.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type SqlDialect = 'postgres' | 'mysql';
|
|
2
|
+
export type SandboxLimits = {
|
|
3
|
+
timeoutMs: number;
|
|
4
|
+
memoryMb: number;
|
|
5
|
+
nanoCpus: number;
|
|
6
|
+
maxRows: number;
|
|
7
|
+
maxBytes: number;
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_SANDBOX_LIMITS: SandboxLimits;
|
|
10
|
+
export type SandboxConnection = {
|
|
11
|
+
type: SqlDialect;
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
user: string;
|
|
15
|
+
password: string;
|
|
16
|
+
database: string;
|
|
17
|
+
};
|
|
18
|
+
export type ValidateSqlSuccess = {
|
|
19
|
+
ok: true;
|
|
20
|
+
};
|
|
21
|
+
export type ValidateSqlFailure = {
|
|
22
|
+
ok: false;
|
|
23
|
+
code: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
};
|
|
26
|
+
export type ValidateSqlResult = ValidateSqlSuccess | ValidateSqlFailure;
|
|
27
|
+
export type ExecuteSqlInput = {
|
|
28
|
+
sql: string;
|
|
29
|
+
connection: SandboxConnection;
|
|
30
|
+
hostAllowlist: string[];
|
|
31
|
+
limits?: Partial<SandboxLimits>;
|
|
32
|
+
/** Custom runner image. Treated as untrusted; the default image is digest-pinned. */
|
|
33
|
+
image?: string;
|
|
34
|
+
};
|
|
35
|
+
export type QueryResultData = {
|
|
36
|
+
columns: string[];
|
|
37
|
+
rows: unknown[][];
|
|
38
|
+
truncated?: boolean;
|
|
39
|
+
};
|
|
40
|
+
export type ExecuteSqlSuccess = {
|
|
41
|
+
ok: true;
|
|
42
|
+
data: QueryResultData;
|
|
43
|
+
};
|
|
44
|
+
export type ExecuteSqlFailure = {
|
|
45
|
+
ok: false;
|
|
46
|
+
code: string;
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
export type ExecuteSqlResult = ExecuteSqlSuccess | ExecuteSqlFailure;
|
|
50
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;AAE9C,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,aAMpC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC;AAC9C,MAAM,MAAM,kBAAkB,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAC7E,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAExE,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,iBAAiB,CAAC;IAC9B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChC,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC;AACpE,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3E,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,iBAAiB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAUA,MAAM,CAAC,MAAM,sBAAsB,GAAkB;IACnD,SAAS,EAAE,MAAM;IACjB,QAAQ,EAAE,GAAG;IACb,QAAQ,EAAE,WAAW;IACrB,OAAO,EAAE,KAAK;IACd,QAAQ,EAAE,SAAS;CACpB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-sql.d.ts","sourceRoot":"","sources":["../src/validate-sql.ts"],"names":[],"mappings":"AAUA,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,GACV;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA4B5D"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const ALLOWED_PREFIX = /^\s*(SELECT|WITH|SHOW|DESCRIBE|EXPLAIN)\b/i;
|
|
2
|
+
const FORBIDDEN_KEYWORD = /\b(DROP|DELETE|UPDATE|INSERT|ALTER|TRUNCATE|CREATE|GRANT|REVOKE|EXEC|EXECUTE|xp_)\b/i;
|
|
3
|
+
const MULTI_STATEMENT = /;\s*\S/;
|
|
4
|
+
const PREFIX_REASON = 'Only SELECT / WITH / SHOW / DESCRIBE / EXPLAIN statements are allowed';
|
|
5
|
+
export function validateSql(sql) {
|
|
6
|
+
const trimmed = sql.trim();
|
|
7
|
+
if (!ALLOWED_PREFIX.test(trimmed)) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
code: 'NOT_READ_ONLY_PREFIX',
|
|
11
|
+
reason: PREFIX_REASON,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (FORBIDDEN_KEYWORD.test(trimmed)) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
code: 'FORBIDDEN_KEYWORD',
|
|
18
|
+
reason: 'Forbidden SQL keyword detected',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (MULTI_STATEMENT.test(trimmed)) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
code: 'MULTI_STATEMENT',
|
|
25
|
+
reason: 'Multi-statement SQL is not allowed',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { ok: true };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=validate-sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-sql.js","sourceRoot":"","sources":["../src/validate-sql.ts"],"names":[],"mappings":"AAAA,MAAM,cAAc,GAAG,4CAA4C,CAAC;AAEpE,MAAM,iBAAiB,GACrB,sFAAsF,CAAC;AAEzF,MAAM,eAAe,GAAG,QAAQ,CAAC;AAEjC,MAAM,aAAa,GACjB,uEAAuE,CAAC;AAE1E,MAAM,UAAU,WAAW,CACzB,GAAW;IAEX,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAClC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,sBAAsB;YAC5B,MAAM,EAAE,aAAa;SACtB,CAAC;IACJ,CAAC;IAED,IAAI,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACpC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,mBAAmB;YACzB,MAAM,EAAE,gCAAgC;SACzC,CAAC;IACJ,CAAC;IAED,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAClC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,iBAAiB;YACvB,MAAM,EAAE,oCAAoC;SAC7C,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ffp-sql-sandbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read-only SQL sandbox primitives: fail-fast validateSql + Docker executeSql with hard limits",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"sandbox",
|
|
18
|
+
"README.md",
|
|
19
|
+
"INTEGRATION.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"packageManager": "pnpm@10.33.3",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/FFP-Tech-Lab/ffp-sql-sandbox.git"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.build.json",
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"test": "tsx --test test/**/*.test.ts",
|
|
34
|
+
"prepack": "pnpm build"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"dockerode": "^4.0.7"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/dockerode": "^3.3.44",
|
|
41
|
+
"@types/node": "^22.18.0",
|
|
42
|
+
"tsx": "^4.20.5",
|
|
43
|
+
"typescript": "^5.9.2"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Digest-pinned Node 22 slim (linux index digest from Docker Hub, 2026-08-25).
|
|
2
|
+
FROM node:22-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5
|
|
3
|
+
|
|
4
|
+
WORKDIR /sandbox
|
|
5
|
+
|
|
6
|
+
COPY package.json ./
|
|
7
|
+
RUN npm install --omit=dev && npm cache clean --force
|
|
8
|
+
|
|
9
|
+
COPY execute.js stream-limit.js session-setup.js secrets.js events.js ./
|
|
10
|
+
|
|
11
|
+
USER node
|
|
12
|
+
|
|
13
|
+
ENTRYPOINT ["node", "execute.js"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function encodeRunnerEvent(event: {
|
|
2
|
+
type: string;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}): string;
|
|
5
|
+
export function writeEvent(
|
|
6
|
+
event: { type: string; [key: string]: unknown },
|
|
7
|
+
stream?: NodeJS.WritableStream,
|
|
8
|
+
): void;
|
|
9
|
+
export function writeRunnerError(
|
|
10
|
+
err: unknown,
|
|
11
|
+
stream?: NodeJS.WritableStream,
|
|
12
|
+
): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { stdout } from 'node:process';
|
|
2
|
+
|
|
3
|
+
export function encodeRunnerEvent(event) {
|
|
4
|
+
return JSON.stringify(event) + '\n';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function writeEvent(event, stream = stdout) {
|
|
8
|
+
stream.write(encodeRunnerEvent(event));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function writeRunnerError(err, stream = stdout) {
|
|
12
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13
|
+
writeEvent({ type: 'error', error: message }, stream);
|
|
14
|
+
}
|