@promigence/sdk 0.1.0-beta.12
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/NOTICE +14 -0
- package/README.md +54 -0
- package/dist/blake3.d.ts +10 -0
- package/dist/blake3.js +111 -0
- package/dist/bundle.d.ts +96 -0
- package/dist/bundle.js +227 -0
- package/dist/ca-guard.d.ts +1 -0
- package/dist/ca-guard.js +166 -0
- package/dist/caller.d.ts +73 -0
- package/dist/caller.js +47 -0
- package/dist/cjs/blake3.d.ts +10 -0
- package/dist/cjs/blake3.js +116 -0
- package/dist/cjs/bundle.d.ts +96 -0
- package/dist/cjs/bundle.js +242 -0
- package/dist/cjs/ca-guard.d.ts +1 -0
- package/dist/cjs/ca-guard.js +174 -0
- package/dist/cjs/caller.d.ts +73 -0
- package/dist/cjs/caller.js +57 -0
- package/dist/cjs/client.d.ts +3015 -0
- package/dist/cjs/client.js +2617 -0
- package/dist/cjs/command-errors.d.ts +41 -0
- package/dist/cjs/command-errors.js +43 -0
- package/dist/cjs/context.d.ts +119 -0
- package/dist/cjs/context.js +598 -0
- package/dist/cjs/core.d.ts +285 -0
- package/dist/cjs/core.js +290 -0
- package/dist/cjs/credstore.d.ts +90 -0
- package/dist/cjs/credstore.js +219 -0
- package/dist/cjs/dispatcher.d.ts +58 -0
- package/dist/cjs/dispatcher.js +91 -0
- package/dist/cjs/github.d.ts +158 -0
- package/dist/cjs/github.js +84 -0
- package/dist/cjs/gitlab.d.ts +104 -0
- package/dist/cjs/gitlab.js +24 -0
- package/dist/cjs/handles.d.ts +82 -0
- package/dist/cjs/handles.js +139 -0
- package/dist/cjs/hint.d.ts +1 -0
- package/dist/cjs/hint.js +10 -0
- package/dist/cjs/http2.d.ts +206 -0
- package/dist/cjs/http2.js +1932 -0
- package/dist/cjs/index.d.ts +34 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/keyed.gen.d.ts +1 -0
- package/dist/cjs/keyed.gen.js +10 -0
- package/dist/cjs/models.gen.d.ts +6162 -0
- package/dist/cjs/models.gen.js +2 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/proxy.d.ts +40 -0
- package/dist/cjs/proxy.js +257 -0
- package/dist/cjs/pty.d.ts +171 -0
- package/dist/cjs/pty.js +504 -0
- package/dist/cjs/sandbox.d.ts +537 -0
- package/dist/cjs/sandbox.js +736 -0
- package/dist/cjs/ssh.d.ts +25 -0
- package/dist/cjs/ssh.js +117 -0
- package/dist/cjs/trace.d.ts +33 -0
- package/dist/cjs/trace.js +88 -0
- package/dist/cjs/transport-worker.d.ts +1 -0
- package/dist/cjs/transport-worker.js +468 -0
- package/dist/cjs/warm.d.ts +1 -0
- package/dist/cjs/warm.js +43 -0
- package/dist/client.d.ts +3015 -0
- package/dist/client.js +2583 -0
- package/dist/command-errors.d.ts +41 -0
- package/dist/command-errors.js +36 -0
- package/dist/context.d.ts +119 -0
- package/dist/context.js +588 -0
- package/dist/core.d.ts +285 -0
- package/dist/core.js +266 -0
- package/dist/credstore.d.ts +90 -0
- package/dist/credstore.js +206 -0
- package/dist/dispatcher.d.ts +58 -0
- package/dist/dispatcher.js +86 -0
- package/dist/github.d.ts +158 -0
- package/dist/github.js +78 -0
- package/dist/gitlab.d.ts +104 -0
- package/dist/gitlab.js +20 -0
- package/dist/handles.d.ts +82 -0
- package/dist/handles.js +135 -0
- package/dist/hint.d.ts +1 -0
- package/dist/hint.js +7 -0
- package/dist/http2.d.ts +206 -0
- package/dist/http2.js +1921 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +15 -0
- package/dist/keyed.gen.d.ts +1 -0
- package/dist/keyed.gen.js +7 -0
- package/dist/models.gen.d.ts +6162 -0
- package/dist/models.gen.js +1 -0
- package/dist/proxy.d.ts +40 -0
- package/dist/proxy.js +249 -0
- package/dist/pty.d.ts +171 -0
- package/dist/pty.js +496 -0
- package/dist/sandbox.d.ts +537 -0
- package/dist/sandbox.js +725 -0
- package/dist/ssh.d.ts +25 -0
- package/dist/ssh.js +114 -0
- package/dist/trace.d.ts +33 -0
- package/dist/trace.js +80 -0
- package/dist/transport-worker.d.ts +1 -0
- package/dist/transport-worker.js +466 -0
- package/dist/warm.d.ts +1 -0
- package/dist/warm.js +40 -0
- package/package.json +54 -0
package/dist/gitlab.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitLab connection (`/v1/integrations/gitlab/*`): the answers' types, and the project path
|
|
3
|
+
* `snapshots.create({ repo: "gitlab:group/project" })` takes.
|
|
4
|
+
*
|
|
5
|
+
* A person authorizes the platform's GitLab application once, read-only (`promigence gitlab
|
|
6
|
+
* connect`), and an owner of the organization confirms it in the browser; after that a snapshot is
|
|
7
|
+
* built from a private project by its path, with no credential ever passing through the client.
|
|
8
|
+
*/
|
|
9
|
+
/** `POST /integrations/gitlab/connect`: open `authorize_url` in a browser. */
|
|
10
|
+
export interface GitlabConnect {
|
|
11
|
+
authorize_url: string;
|
|
12
|
+
/** RFC 3339: the link works for 15 minutes. */
|
|
13
|
+
expires_at: string;
|
|
14
|
+
gitlab_url: string;
|
|
15
|
+
/** Read-only: `read_api`, `read_repository`. */
|
|
16
|
+
scopes: string[];
|
|
17
|
+
}
|
|
18
|
+
/** One GitLab user whose read-only authorization is connected. */
|
|
19
|
+
export interface GitlabConnection {
|
|
20
|
+
connection_id: string;
|
|
21
|
+
username: string;
|
|
22
|
+
user_id: number;
|
|
23
|
+
gitlab_url: string;
|
|
24
|
+
connected_at: string;
|
|
25
|
+
/** The authorization stopped working on GitLab: connect again. */
|
|
26
|
+
broken: boolean;
|
|
27
|
+
broken_reason?: string;
|
|
28
|
+
/** Optional: a project webhook (push events) for instant rebuilds of tracked branches. */
|
|
29
|
+
webhook_url: string;
|
|
30
|
+
webhook_token: string;
|
|
31
|
+
}
|
|
32
|
+
/** An authorization waiting for an owner of the organization to confirm it. */
|
|
33
|
+
export interface GitlabPending {
|
|
34
|
+
connection_id: string;
|
|
35
|
+
username: string;
|
|
36
|
+
authorized_at: string;
|
|
37
|
+
expires_at: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
/** A snapshot that follows a GitLab branch: each new commit builds it again (each build billed per second). */
|
|
41
|
+
export interface GitlabTrack {
|
|
42
|
+
track_id: string;
|
|
43
|
+
repo: string;
|
|
44
|
+
branch: string;
|
|
45
|
+
alias: string;
|
|
46
|
+
head_sha?: string;
|
|
47
|
+
built_sha?: string;
|
|
48
|
+
snapshot_id?: string;
|
|
49
|
+
build_id?: string;
|
|
50
|
+
last_error?: string;
|
|
51
|
+
created_at: string;
|
|
52
|
+
}
|
|
53
|
+
/** `GET /integrations/gitlab`. `configured: false` on a deployment with no GitLab application. */
|
|
54
|
+
export interface GitlabStatus {
|
|
55
|
+
configured: boolean;
|
|
56
|
+
gitlab_url?: string;
|
|
57
|
+
message?: string;
|
|
58
|
+
connections: GitlabConnection[];
|
|
59
|
+
pending: GitlabPending[];
|
|
60
|
+
tracks: GitlabTrack[];
|
|
61
|
+
}
|
|
62
|
+
/** `DELETE /integrations/gitlab/connections/:connection_id`. */
|
|
63
|
+
export interface GitlabDisconnected {
|
|
64
|
+
connection_id: string;
|
|
65
|
+
username: string;
|
|
66
|
+
disconnected: boolean;
|
|
67
|
+
/** Our token was revoked on GitLab. */
|
|
68
|
+
revoked: boolean;
|
|
69
|
+
tracks_removed: number;
|
|
70
|
+
message: string;
|
|
71
|
+
}
|
|
72
|
+
/** One GitLab project a connection can read. */
|
|
73
|
+
export interface GitlabRepo {
|
|
74
|
+
/** `group/sub/project`: what `snapshots.create({ repo: "gitlab:<path>" })` takes. */
|
|
75
|
+
path: string;
|
|
76
|
+
project_id: number;
|
|
77
|
+
private: boolean;
|
|
78
|
+
default_branch: string;
|
|
79
|
+
archived: boolean;
|
|
80
|
+
connection_id: string;
|
|
81
|
+
last_activity_at?: string;
|
|
82
|
+
}
|
|
83
|
+
/** `GET /integrations/gitlab/repos`: one page of one connection's projects. */
|
|
84
|
+
export interface GitlabRepos {
|
|
85
|
+
repos: GitlabRepo[];
|
|
86
|
+
page: number;
|
|
87
|
+
per_page: number;
|
|
88
|
+
has_more: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** `DELETE /integrations/gitlab/tracks/:track_id`. */
|
|
91
|
+
export interface GitlabUntracked {
|
|
92
|
+
track_id: string;
|
|
93
|
+
repo: string;
|
|
94
|
+
branch: string;
|
|
95
|
+
alias: string;
|
|
96
|
+
untracked: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** The prefix that names a connected GitLab project in `repo`. */
|
|
99
|
+
export declare const GITLAB_REPO_PREFIX = "gitlab:";
|
|
100
|
+
/**
|
|
101
|
+
* `gitlab:group/sub/project` as `group/sub/project`; `undefined` for anything that is not one (2 to
|
|
102
|
+
* 20 path segments of letters, digits, `.` `_` `-`). The same rule as the API's.
|
|
103
|
+
*/
|
|
104
|
+
export declare function parseGitlabRepo(repo: string | undefined): string | undefined;
|
package/dist/gitlab.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const GITLAB_REPO_PREFIX = "gitlab:";
|
|
2
|
+
const SEGMENT_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,99}$/;
|
|
3
|
+
export function parseGitlabRepo(repo) {
|
|
4
|
+
if (typeof repo !== "string")
|
|
5
|
+
return undefined;
|
|
6
|
+
let r = repo.trim();
|
|
7
|
+
if (!r.startsWith(GITLAB_REPO_PREFIX))
|
|
8
|
+
return undefined;
|
|
9
|
+
r = r.slice(GITLAB_REPO_PREFIX.length);
|
|
10
|
+
if (r.endsWith(".git"))
|
|
11
|
+
r = r.slice(0, -4);
|
|
12
|
+
if (!r || r.length > 255)
|
|
13
|
+
return undefined;
|
|
14
|
+
const segs = r.split("/");
|
|
15
|
+
if (segs.length < 2 || segs.length > 20)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (!segs.every((s) => SEGMENT_RE.test(s) && s !== ".." && !s.endsWith(".atom")))
|
|
18
|
+
return undefined;
|
|
19
|
+
return r;
|
|
20
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CommandHandle` — the handle `commands.run(cmd, { background: true })` returns.
|
|
3
|
+
*
|
|
4
|
+
* Output is read the way the API serves it: by byte range, polled. There is no second streaming
|
|
5
|
+
* transport for background execs, and a poll at 100–500 ms is below the resolution anyone reads a
|
|
6
|
+
* dev server's log at. `wait()` parks on the API's `wait_ms` long-poll between reads, so a
|
|
7
|
+
* process that ends is noticed within one round trip, not one poll interval.
|
|
8
|
+
*
|
|
9
|
+
* Stated once: `pid` is the API's `exec_id` (a string), not a process number —
|
|
10
|
+
* it is what `commands.kill(pid)` and `commands.connect(pid)` take.
|
|
11
|
+
*/
|
|
12
|
+
import { type CommandResult } from "./core.js";
|
|
13
|
+
import { inspect, type InspectOptions } from "node:util";
|
|
14
|
+
import type { BackgroundExecRecord, PromigenceClient } from "./client.js";
|
|
15
|
+
export interface CommandHandleOpts {
|
|
16
|
+
onStdout?: (data: string) => void;
|
|
17
|
+
onStderr?: (data: string) => void;
|
|
18
|
+
/** Poll interval while only callbacks are attached (default 250 ms). */
|
|
19
|
+
pollMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* The bound the command was started with, in ms: `wait()` then throws `CommandTimeoutError`
|
|
22
|
+
* for a command killed there rather than a bare `CommandExitError(137)`.
|
|
23
|
+
*/
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare class CommandHandle {
|
|
27
|
+
#private;
|
|
28
|
+
/** The `exec_id`. */
|
|
29
|
+
readonly pid: string;
|
|
30
|
+
readonly sandboxId: string;
|
|
31
|
+
/** Everything read so far, decoded as UTF-8. */
|
|
32
|
+
stdout: string;
|
|
33
|
+
stderr: string;
|
|
34
|
+
private readonly opts;
|
|
35
|
+
private readonly offset;
|
|
36
|
+
private readonly decoder;
|
|
37
|
+
private record;
|
|
38
|
+
private pumping;
|
|
39
|
+
private stopped;
|
|
40
|
+
private draining;
|
|
41
|
+
constructor(client: PromigenceClient, record: BackgroundExecRecord, opts?: CommandHandleOpts);
|
|
42
|
+
/** The last status the API reported. */
|
|
43
|
+
get state(): BackgroundExecRecord["state"];
|
|
44
|
+
get exitCode(): number | undefined;
|
|
45
|
+
get error(): string | undefined;
|
|
46
|
+
/** Started with `stdin: true`, so `sendStdin` / `closeStdin` reach the process. */
|
|
47
|
+
get stdinOpen(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Write to the process's standard input. Text is sent as UTF-8; bytes are sent as they are.
|
|
50
|
+
*
|
|
51
|
+
* Only for a command started with `{ stdin: true }` — a child's stdin cannot be opened after
|
|
52
|
+
* it has been spawned, and this throws (`no_stdin`) rather than dropping the bytes.
|
|
53
|
+
*/
|
|
54
|
+
sendStdin(data: string | Uint8Array): Promise<number>;
|
|
55
|
+
/**
|
|
56
|
+
* Close stdin (EOF). A process reading to end-of-input — `sort`, `cat`, a REPL — does nothing
|
|
57
|
+
* until this arrives, so it is the other half of `sendStdin` and not an optional tidy-up.
|
|
58
|
+
*/
|
|
59
|
+
closeStdin(): Promise<void>;
|
|
60
|
+
/** Read every byte that is buffered now, on both streams. Serialised: one drain at a time. */
|
|
61
|
+
private drain;
|
|
62
|
+
private pump;
|
|
63
|
+
/**
|
|
64
|
+
* Wait for the process to end and return its result. A non-zero exit throws
|
|
65
|
+
* `CommandExitError` (it carries `stdout`/`stderr`/`exitCode`); a command killed at the
|
|
66
|
+
* `timeoutMs` it was started with throws `CommandTimeoutError`, a `CommandExitError` that says so.
|
|
67
|
+
*/
|
|
68
|
+
wait(): Promise<CommandResult>;
|
|
69
|
+
/** SIGKILL. `true` if the process was running and the signal was delivered. */
|
|
70
|
+
kill(): Promise<boolean>;
|
|
71
|
+
/** Stop polling for output. The process keeps running; `commands.connect(pid)` re-attaches. */
|
|
72
|
+
disconnect(): Promise<void>;
|
|
73
|
+
/** What `JSON.stringify` shows: ids and status. The client (and its key) is never part of it. */
|
|
74
|
+
toJSON(): {
|
|
75
|
+
pid: string;
|
|
76
|
+
sandboxId: string;
|
|
77
|
+
state: BackgroundExecRecord["state"];
|
|
78
|
+
exitCode: number | undefined;
|
|
79
|
+
};
|
|
80
|
+
/** What `console.log` / `util.inspect` show: the same as `toJSON`. */
|
|
81
|
+
[inspect.custom](_depth: number, opts: InspectOptions): string;
|
|
82
|
+
}
|
package/dist/handles.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { CommandExitError, EnvFailedError } from "./core.js";
|
|
2
|
+
import { inspect } from "node:util";
|
|
3
|
+
import { CommandTimeoutError, exitTimedOut } from "./command-errors.js";
|
|
4
|
+
const HANDLE_WAIT_MS = 30_000;
|
|
5
|
+
const HANDLE_POLL_MS = 250;
|
|
6
|
+
export class CommandHandle {
|
|
7
|
+
pid;
|
|
8
|
+
sandboxId;
|
|
9
|
+
stdout = "";
|
|
10
|
+
stderr = "";
|
|
11
|
+
#client;
|
|
12
|
+
opts;
|
|
13
|
+
offset = { stdout: 0, stderr: 0 };
|
|
14
|
+
decoder = { stdout: new TextDecoder(), stderr: new TextDecoder() };
|
|
15
|
+
record;
|
|
16
|
+
pumping = false;
|
|
17
|
+
stopped = false;
|
|
18
|
+
draining;
|
|
19
|
+
constructor(client, record, opts = {}) {
|
|
20
|
+
this.#client = client;
|
|
21
|
+
this.record = record;
|
|
22
|
+
this.pid = record.exec_id;
|
|
23
|
+
this.sandboxId = record.sandbox_id;
|
|
24
|
+
this.opts = opts;
|
|
25
|
+
if (opts.onStdout || opts.onStderr)
|
|
26
|
+
void this.pump();
|
|
27
|
+
}
|
|
28
|
+
get state() { return this.record.state; }
|
|
29
|
+
get exitCode() {
|
|
30
|
+
const e = this.record.exit;
|
|
31
|
+
if (!e)
|
|
32
|
+
return undefined;
|
|
33
|
+
return e.exit_code ?? (e.signal ? 128 + e.signal : -1);
|
|
34
|
+
}
|
|
35
|
+
get error() { return this.record.error?.msg; }
|
|
36
|
+
get stdinOpen() { return Boolean(this.record.stdin_open); }
|
|
37
|
+
async sendStdin(data) {
|
|
38
|
+
const r = await this.#client.sandboxes.execs.stdin(this.sandboxId, this.pid, { data });
|
|
39
|
+
return r.written;
|
|
40
|
+
}
|
|
41
|
+
async closeStdin() {
|
|
42
|
+
await this.#client.sandboxes.execs.stdin(this.sandboxId, this.pid, { eof: true });
|
|
43
|
+
}
|
|
44
|
+
drain() {
|
|
45
|
+
const run = async () => {
|
|
46
|
+
for (const s of ["stdout", "stderr"]) {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const r = await this.#client.sandboxes.execs.read(this.sandboxId, this.pid, s, { offset: this.offset[s] });
|
|
49
|
+
this.record = r.record;
|
|
50
|
+
if (r.data.length === 0)
|
|
51
|
+
break;
|
|
52
|
+
this.offset[s] += r.data.length;
|
|
53
|
+
const text = this.decoder[s].decode(r.data, { stream: true });
|
|
54
|
+
this[s] += text;
|
|
55
|
+
if (text)
|
|
56
|
+
(s === "stdout" ? this.opts.onStdout : this.opts.onStderr)?.(text);
|
|
57
|
+
if (r.eof)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const next = (this.draining ?? Promise.resolve()).then(run, run);
|
|
63
|
+
this.draining = next.catch(() => { });
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
async pump() {
|
|
67
|
+
if (this.pumping)
|
|
68
|
+
return;
|
|
69
|
+
this.pumping = true;
|
|
70
|
+
try {
|
|
71
|
+
while (!this.stopped) {
|
|
72
|
+
await this.drain();
|
|
73
|
+
if (this.record.state !== "running") {
|
|
74
|
+
await this.drain();
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
await new Promise((r) => { setTimeout(r, this.opts.pollMs ?? 250); });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
this.pumping = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async wait() {
|
|
87
|
+
for (;;) {
|
|
88
|
+
await this.drain();
|
|
89
|
+
if (this.record.state !== "running")
|
|
90
|
+
break;
|
|
91
|
+
this.record = await this.#client.sandboxes.execs.get(this.sandboxId, this.pid, { waitMs: HANDLE_WAIT_MS });
|
|
92
|
+
if (this.record.state === "running" && !((this.record.waited_ms ?? 0) >= HANDLE_WAIT_MS)) {
|
|
93
|
+
await new Promise((r) => { setTimeout(r, HANDLE_POLL_MS); });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
await this.drain();
|
|
97
|
+
for (const s of ["stdout", "stderr"]) {
|
|
98
|
+
const tail = this.decoder[s].decode();
|
|
99
|
+
if (tail) {
|
|
100
|
+
this[s] += tail;
|
|
101
|
+
(s === "stdout" ? this.opts.onStdout : this.opts.onStderr)?.(tail);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (this.record.state === "failed") {
|
|
105
|
+
const e = this.record.error;
|
|
106
|
+
throw new EnvFailedError(e?.code ?? "sandbox_unresponsive", `background exec ${this.pid} failed: ${e?.msg ?? "no exit record"}`, "Not your command's failure. Check the sandbox is still ready (`promigence run report <run_id>`) and start it again.");
|
|
107
|
+
}
|
|
108
|
+
const result = {
|
|
109
|
+
exitCode: this.exitCode ?? -1,
|
|
110
|
+
stdout: this.stdout,
|
|
111
|
+
stderr: this.stderr,
|
|
112
|
+
truncated: Boolean(this.record.truncated),
|
|
113
|
+
execId: this.pid,
|
|
114
|
+
};
|
|
115
|
+
if (result.exitCode !== 0) {
|
|
116
|
+
if (exitTimedOut(this.record.exit, this.opts.timeoutMs))
|
|
117
|
+
throw new CommandTimeoutError(result, this.opts.timeoutMs);
|
|
118
|
+
throw new CommandExitError(result);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
async kill() {
|
|
123
|
+
const r = await this.#client.sandboxes.execs.signal(this.sandboxId, this.pid, "SIGKILL");
|
|
124
|
+
return r.delivered;
|
|
125
|
+
}
|
|
126
|
+
async disconnect() {
|
|
127
|
+
this.stopped = true;
|
|
128
|
+
}
|
|
129
|
+
toJSON() {
|
|
130
|
+
return { pid: this.pid, sandboxId: this.sandboxId, state: this.state, exitCode: this.exitCode };
|
|
131
|
+
}
|
|
132
|
+
[inspect.custom](_depth, opts) {
|
|
133
|
+
return `CommandHandle ${inspect(this.toJSON(), opts)}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
package/dist/hint.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function hintArg(value: string | number): string;
|
package/dist/hint.js
ADDED
package/dist/http2.d.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP/2 transport for `PromigenceClient.request()` on Node.js.
|
|
3
|
+
*
|
|
4
|
+
* Why: a process that opens many sandboxes at once sends many API requests at once. Over the
|
|
5
|
+
* global `fetch` each concurrent request gets its own HTTP/1.1 connection, so a burst of 100 pays
|
|
6
|
+
* for 100 TCP + TLS handshakes before the first byte of work. One HTTP/2 connection carries all
|
|
7
|
+
* of them as streams: one handshake, then every request goes out immediately.
|
|
8
|
+
*
|
|
9
|
+
* When it is used — all of these, otherwise the request goes to `fetch` exactly as before:
|
|
10
|
+
* - the runtime is Node.js (not Bun, Deno, a browser or an edge worker);
|
|
11
|
+
* - the caller did not inject `opts.fetch`, did not pass `http2: false`, did not set
|
|
12
|
+
* `PROMIGENCE_HTTP2=0`, did not replace `globalThis.fetch` or install a custom undici
|
|
13
|
+
* dispatcher (a proxy, a mock), and has no proxy variable in the environment;
|
|
14
|
+
* - the URL is `https:` and the server picks `h2` during the TLS handshake (ALPN);
|
|
15
|
+
* - the request is a control call: JSON or no body, JSON answer. File transfers and the
|
|
16
|
+
* streamed exec stay on `fetch`.
|
|
17
|
+
*
|
|
18
|
+
* Connections. One pool per origin, shared by every client in the process. Connecting is lazy
|
|
19
|
+
* and shared: 100 first requests wait on one connect, not 100. The first connect to an origin
|
|
20
|
+
* opens one session per address the API's name resolves to (up to FIRST_SESSIONS, of the address
|
|
21
|
+
* family Node tries first), all started together before any request is sent, and the requests
|
|
22
|
+
* waiting for it are spread over them (`Opening`); a name with one address gets one session, as
|
|
23
|
+
* before. More (up to MAX_SESSIONS) open only when every open session is at the server's
|
|
24
|
+
* SETTINGS_MAX_CONCURRENT_STREAMS. `PromigenceClient.preconnect()` starts the first connect
|
|
25
|
+
* before the first request instead, sending nothing; so does the SDK itself once it has loaded in
|
|
26
|
+
* a process with an API key in its environment (`connectAtLoad`; `PROMIGENCE_PRECONNECT=0` turns
|
|
27
|
+
* that off). A session is `unref()`ed whenever it has no request in flight, so a script can exit,
|
|
28
|
+
* and closed after IDLE_CLOSE_MS without one.
|
|
29
|
+
*
|
|
30
|
+
* Cost per request. At 100 requests in flight the client's own work is serial and every request
|
|
31
|
+
* queues behind the others', so the hot path allocates as little as it can: the answer is an `H2Response` (status, the raw header map, the bytes — a real `Headers`
|
|
32
|
+
* only if someone asks for it), the bound is one timer instead of an `AbortSignal` and its
|
|
33
|
+
* listeners, and a request that finds a ready session takes it without a promise in between.
|
|
34
|
+
*
|
|
35
|
+
* Failure rules. The worst case must be "behaves like fetch", never "a request is lost or runs
|
|
36
|
+
* twice":
|
|
37
|
+
* - the server does not speak h2 (ALPN picked http/1.1 or nothing), refuses HTTP/2 for the
|
|
38
|
+
* request (HTTP_1_1_REQUIRED, 421), or the TLS handshake itself fails: nothing was sent, so
|
|
39
|
+
* this request goes to `fetch`, and so does every later one to that origin;
|
|
40
|
+
* - the TCP connect fails or times out: this request goes to `fetch`; h2 is retried for the
|
|
41
|
+
* origin after RETRY_AFTER_MS, and given up after three such failures in a row;
|
|
42
|
+
* - the server provably did not process the request (REFUSED_STREAM, a stream above a GOAWAY's
|
|
43
|
+
* last stream id, a session that closed before the request was handed to it): it is sent
|
|
44
|
+
* again on a fresh session, any method;
|
|
45
|
+
* - the connection died after the request may have reached the server and before any response
|
|
46
|
+
* arrived: sent again ONLY when replaying is harmless (GET, DELETE, or a request with an
|
|
47
|
+
* `Idempotency-Key`), once; otherwise the error surfaces exactly as a `fetch` failure does;
|
|
48
|
+
* - a response has started: never sent again.
|
|
49
|
+
* - sessions that keep dying with requests on them, or streams that keep failing on a live
|
|
50
|
+
* session, turn h2 off for the origin for the rest of the process.
|
|
51
|
+
*/
|
|
52
|
+
import type { LookupAddress } from "node:dns";
|
|
53
|
+
import * as http2 from "node:http2";
|
|
54
|
+
import { type TraceStamp } from "./trace.js";
|
|
55
|
+
type FetchLike = typeof globalThis.fetch;
|
|
56
|
+
/** What `request()` hands the transport. */
|
|
57
|
+
export interface Http2Init {
|
|
58
|
+
method: string;
|
|
59
|
+
headers: Record<string, string>;
|
|
60
|
+
/** JSON text. Byte bodies never come here. */
|
|
61
|
+
body?: string;
|
|
62
|
+
/**
|
|
63
|
+
* The request's bound, in ms from the call. On h2 it is the transport's own timer: a stream
|
|
64
|
+
* waits exactly this long, never less, and there is no socket-level timeout underneath. On
|
|
65
|
+
* fallback `fetch` gets `AbortSignal.timeout` of whatever is left of it.
|
|
66
|
+
*/
|
|
67
|
+
timeoutMs: number;
|
|
68
|
+
/** The caller's own signal, when it gave one. */
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
/** Read by `fetch` on fallback only. */
|
|
71
|
+
dispatcher?: object;
|
|
72
|
+
/** Phase stamps when `PROMIGENCE_SDK_TRACE=1`. */
|
|
73
|
+
trace?: TraceStamp;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Decide, once per client, whether HTTP/2 may be tried at all. `opt` is `PromigenceClientOpts.http2`;
|
|
77
|
+
* the caller has already ruled out an injected `fetch`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function http2Allowed(opt: boolean | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* What an h2 exchange answers with: the subset of `Response` that `request()` reads, without a
|
|
82
|
+
* `Response`, a `Headers` or a `ReadableStream` behind it. `headers` builds the real `Headers`
|
|
83
|
+
* on first use, so a caller's `onHeaders` sees exactly what it always saw.
|
|
84
|
+
*/
|
|
85
|
+
export declare class H2Response {
|
|
86
|
+
readonly status: number;
|
|
87
|
+
/** node:http2's header map: lower-case names, `set-cookie` as an array. */
|
|
88
|
+
private readonly raw;
|
|
89
|
+
/** Already content-decoded; `null` for a HEAD / 204 / 205 / 304 answer, as `fetch` has it. */
|
|
90
|
+
private readonly bytes;
|
|
91
|
+
readonly ok: boolean;
|
|
92
|
+
private built;
|
|
93
|
+
constructor(status: number,
|
|
94
|
+
/** node:http2's header map: lower-case names, `set-cookie` as an array. */
|
|
95
|
+
raw: http2.IncomingHttpHeaders,
|
|
96
|
+
/** Already content-decoded; `null` for a HEAD / 204 / 205 / 304 answer, as `fetch` has it. */
|
|
97
|
+
bytes: Buffer | null);
|
|
98
|
+
/**
|
|
99
|
+
* True when this answers a resend of a request that may already have been processed (a
|
|
100
|
+
* replay-safe request whose connection died after it was sent): a DELETE answered 404 here was
|
|
101
|
+
* most likely done by the attempt whose answer was lost.
|
|
102
|
+
*/
|
|
103
|
+
replayed: boolean;
|
|
104
|
+
/** `Headers.get`: the value, joined with ", " when the header was sent more than once. */
|
|
105
|
+
header(name: string): string | null;
|
|
106
|
+
/** `text()` without the promise around it: the body has already arrived. */
|
|
107
|
+
textNow(): string;
|
|
108
|
+
get headers(): Headers;
|
|
109
|
+
/** The body as text, decoded the way `Response.text()` decodes it (UTF-8, BOM dropped). */
|
|
110
|
+
text(): Promise<string>;
|
|
111
|
+
}
|
|
112
|
+
/** What a request needs of its URL: the origin of its pool, and its `:authority` and `:path`. */
|
|
113
|
+
interface Target {
|
|
114
|
+
origin: string;
|
|
115
|
+
host: string;
|
|
116
|
+
path: string;
|
|
117
|
+
}
|
|
118
|
+
/** The scheme and authority of a URL that parsed as `https:`, and what the URL parser made of them. */
|
|
119
|
+
interface Prefix {
|
|
120
|
+
/** `https://` and the authority, exactly as written: up to the `/` that starts the path. */
|
|
121
|
+
text: string;
|
|
122
|
+
origin: string;
|
|
123
|
+
host: string;
|
|
124
|
+
}
|
|
125
|
+
export declare class Http2Transport {
|
|
126
|
+
private readonly fallback;
|
|
127
|
+
/** `fallback` is the `fetch` this client would have used without HTTP/2. */
|
|
128
|
+
constructor(fallback: FetchLike);
|
|
129
|
+
/**
|
|
130
|
+
* Start the connection `url`'s requests would use, without sending anything. The same checks as
|
|
131
|
+
* a request: an `https:` URL, no custom dispatcher, HTTP/2 not turned off for the origin. Never
|
|
132
|
+
* throws; the first request connects as before if this could not.
|
|
133
|
+
*/
|
|
134
|
+
preconnect(url: string): void;
|
|
135
|
+
/** The request, sent with `fetch` instead: bounded by what is left of its budget. */
|
|
136
|
+
private viaFetch;
|
|
137
|
+
/**
|
|
138
|
+
* Send one control request. Resolves or rejects the way `fallback(url, init)` would: an answer
|
|
139
|
+
* (`H2Response` over h2, `Response` on fallback), the timeout's or the signal's reason on abort,
|
|
140
|
+
* `TypeError("fetch failed")` on a network failure. `replayable`: sending it twice is harmless
|
|
141
|
+
* (see the module comment).
|
|
142
|
+
*/
|
|
143
|
+
fetch(url: string, init: Http2Init, replayable: boolean): Promise<Response | H2Response>;
|
|
144
|
+
}
|
|
145
|
+
export declare function warmTransport(): void;
|
|
146
|
+
/** Test and diagnostics view of the process-wide pools. Not part of the public API. */
|
|
147
|
+
export declare const __http2Internals: {
|
|
148
|
+
pools(): Array<{
|
|
149
|
+
origin: string;
|
|
150
|
+
sessions: number;
|
|
151
|
+
opened: number;
|
|
152
|
+
warmed: number;
|
|
153
|
+
off: boolean;
|
|
154
|
+
suspended: boolean;
|
|
155
|
+
offReason: string;
|
|
156
|
+
active: number;
|
|
157
|
+
split: number[];
|
|
158
|
+
connecting: boolean;
|
|
159
|
+
parked: boolean;
|
|
160
|
+
}>;
|
|
161
|
+
/** What a request to `url` sends as `:authority` and `:path`, read with `known` as a transport does. */
|
|
162
|
+
targetOf(url: string, known?: {
|
|
163
|
+
text: string;
|
|
164
|
+
origin: string;
|
|
165
|
+
host: string;
|
|
166
|
+
}): {
|
|
167
|
+
target: Target | undefined;
|
|
168
|
+
prefix: Prefix | undefined;
|
|
169
|
+
};
|
|
170
|
+
/** The schemes and authorities requests have parsed, newest first. */
|
|
171
|
+
prefixes(): string[];
|
|
172
|
+
/** Close every session and forget every origin's state. */
|
|
173
|
+
reset(): void;
|
|
174
|
+
/** A pool for `origin` of its own, outside the process-wide map, for unit tests of its logic. */
|
|
175
|
+
pool(origin: string): unknown;
|
|
176
|
+
/** The transport thread, for tests and diagnostics. */
|
|
177
|
+
thread(): {
|
|
178
|
+
state: string;
|
|
179
|
+
sessions: number;
|
|
180
|
+
limit: number;
|
|
181
|
+
answered: number;
|
|
182
|
+
everReady: boolean;
|
|
183
|
+
closedReason: string;
|
|
184
|
+
} | undefined;
|
|
185
|
+
/** The transport thread's number of connections and spacing between new requests on one, for an environment (`threadKnobs`). */
|
|
186
|
+
threadKnobs(env: NodeJS.ProcessEnv): {
|
|
187
|
+
sessions: number;
|
|
188
|
+
gapUs: number;
|
|
189
|
+
pace: string;
|
|
190
|
+
};
|
|
191
|
+
/** Tests: make the transport thread fail as a crash would. */
|
|
192
|
+
crashThreadForTest(): void;
|
|
193
|
+
/** The early lookup, for tests: start one with a platform, DNS servers and hosts file of the test's own. */
|
|
194
|
+
early: {
|
|
195
|
+
start(host: string): boolean;
|
|
196
|
+
/** Its state and answer, read without waiting. */
|
|
197
|
+
state(host: string): {
|
|
198
|
+
state: string;
|
|
199
|
+
answer?: LookupAddress[];
|
|
200
|
+
fresh: boolean;
|
|
201
|
+
} | undefined;
|
|
202
|
+
hosts(): string[];
|
|
203
|
+
reset(): void;
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
export {};
|