withruntime 0.2.2 → 0.3.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/README.md +68 -4
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +1262 -0
- package/dist/client.d.ts +118 -0
- package/dist/client.js +160 -0
- package/dist/credentials.d.ts +44 -0
- package/dist/credentials.js +180 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.js +106 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +25 -0
- package/dist/login.d.ts +119 -0
- package/dist/login.js +367 -0
- package/dist/mcp.d.ts +15 -0
- package/dist/mcp.js +221 -0
- package/dist/page.d.ts +13 -0
- package/dist/page.js +39 -0
- package/dist/products/desktop.d.ts +103 -0
- package/dist/products/desktop.js +38 -0
- package/dist/products/images.d.ts +121 -0
- package/dist/products/images.js +64 -0
- package/dist/products/index.d.ts +29 -0
- package/dist/products/index.js +36 -0
- package/dist/products/interpreter.d.ts +88 -0
- package/dist/products/interpreter.js +81 -0
- package/dist/products/network.d.ts +47 -0
- package/dist/products/network.js +19 -0
- package/dist/products/previews.d.ts +51 -0
- package/dist/products/previews.js +30 -0
- package/dist/products/referrals.d.ts +36 -0
- package/dist/products/referrals.js +8 -0
- package/dist/products/volumes.d.ts +47 -0
- package/dist/products/volumes.js +27 -0
- package/dist/sandbox.d.ts +192 -0
- package/dist/sandbox.js +628 -0
- package/dist/snapshots.d.ts +50 -0
- package/dist/snapshots.js +55 -0
- package/dist/tar.d.ts +3 -0
- package/dist/tar.js +116 -0
- package/dist/transport.d.ts +65 -0
- package/dist/transport.js +261 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.js +1 -0
- package/package.json +53 -8
- package/cli.js +0 -7
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Page } from "./page.js";
|
|
2
|
+
import { type ClientExtensions } from "./products/index.js";
|
|
3
|
+
import { Sandbox } from "./sandbox.js";
|
|
4
|
+
import { Snapshots } from "./snapshots.js";
|
|
5
|
+
import { Transport, type RequestOptions } from "./transport.js";
|
|
6
|
+
import type { CreateSandbox, FeedbackKind, SandboxInfo, Usage } from "./types.js";
|
|
7
|
+
export type RuntimeOptions = {
|
|
8
|
+
/** Default: the RUNTIME_API_KEY environment variable. */
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
/** Default: RUNTIME_API_URL, then https://api.withruntime.com. */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
/** Per call, retries included. Default 5 minutes. */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
/** Retries of transport failures, 429, 502, 503 and 504. Default 4. */
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
/** Calls in flight at once; more wait their turn and reuse connections. Default 32. */
|
|
18
|
+
maxConnections?: number;
|
|
19
|
+
};
|
|
20
|
+
/** One client for every Runtime Cloud product. Create it once and reuse it:
|
|
21
|
+
* it keeps its connections open. */
|
|
22
|
+
export interface Runtime extends ClientExtensions {
|
|
23
|
+
}
|
|
24
|
+
export declare class Runtime {
|
|
25
|
+
readonly transport: Transport;
|
|
26
|
+
readonly sandboxes: Sandboxes;
|
|
27
|
+
readonly snapshots: Snapshots;
|
|
28
|
+
readonly feedback: FeedbackApi;
|
|
29
|
+
readonly support: SupportApi;
|
|
30
|
+
constructor(options?: RuntimeOptions);
|
|
31
|
+
/** Who this key is: organization, agent and credential. */
|
|
32
|
+
me(options?: RequestOptions): Promise<{
|
|
33
|
+
orgId: string;
|
|
34
|
+
principalId: string;
|
|
35
|
+
credentialId: string | null;
|
|
36
|
+
apiVersion: string;
|
|
37
|
+
}>;
|
|
38
|
+
/** Balance, holds, trial time and per-resource charges, in integer microdollars. */
|
|
39
|
+
/** Credit, holds, trial time and per-resource charges. */
|
|
40
|
+
usage(options?: RequestOptions): Promise<Usage>;
|
|
41
|
+
}
|
|
42
|
+
export declare class Sandboxes {
|
|
43
|
+
private readonly t;
|
|
44
|
+
constructor(t: Transport);
|
|
45
|
+
/** Creates a sandbox and waits until it is running. Every field is optional:
|
|
46
|
+
* with none you get the free trial while it lasts, the default region and a
|
|
47
|
+
* 2 vCPU / 4 GiB machine from a warm template. `wait: false` returns at once. */
|
|
48
|
+
create(input?: CreateSandbox & {
|
|
49
|
+
wait?: boolean;
|
|
50
|
+
}, options?: RequestOptions): Promise<Sandbox>;
|
|
51
|
+
get(id: string, options?: RequestOptions): Promise<Sandbox>;
|
|
52
|
+
/** Live sandboxes, oldest first. Await for a page, or `for await` over all. */
|
|
53
|
+
list(filter?: {
|
|
54
|
+
state?: SandboxInfo["state"][];
|
|
55
|
+
includeStopped?: boolean;
|
|
56
|
+
/** Every label must match. */
|
|
57
|
+
labels?: Record<string, string>;
|
|
58
|
+
name?: string;
|
|
59
|
+
limit?: number;
|
|
60
|
+
}): Promise<Page<Sandbox>>;
|
|
61
|
+
}
|
|
62
|
+
export declare class FeedbackApi {
|
|
63
|
+
private readonly t;
|
|
64
|
+
constructor(t: Transport);
|
|
65
|
+
/** Tell the Runtime team something. Please do, generously: bugs, missing
|
|
66
|
+
* features, what another provider does better, what blocks a migration. */
|
|
67
|
+
submit(input: {
|
|
68
|
+
kind: FeedbackKind;
|
|
69
|
+
summary: string;
|
|
70
|
+
detail?: string;
|
|
71
|
+
competitor?: string;
|
|
72
|
+
resourceId?: string;
|
|
73
|
+
requestId?: string;
|
|
74
|
+
context?: Record<string, unknown>;
|
|
75
|
+
}, options?: RequestOptions): Promise<{
|
|
76
|
+
id: string;
|
|
77
|
+
duplicate: boolean;
|
|
78
|
+
}>;
|
|
79
|
+
list(options?: {
|
|
80
|
+
limit?: number;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
data: Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
kind: FeedbackKind;
|
|
85
|
+
summary: string;
|
|
86
|
+
status: string;
|
|
87
|
+
item: unknown;
|
|
88
|
+
}>;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
export type SupportReply = {
|
|
92
|
+
conversationId: string;
|
|
93
|
+
status: "answered" | "working" | "escalated" | "waiting_customer" | "capped";
|
|
94
|
+
reply?: string;
|
|
95
|
+
pendingActions?: Array<{
|
|
96
|
+
id: string;
|
|
97
|
+
action: string;
|
|
98
|
+
resourceId?: string;
|
|
99
|
+
summary: string;
|
|
100
|
+
inputHash: string;
|
|
101
|
+
expiresAt: string;
|
|
102
|
+
}>;
|
|
103
|
+
};
|
|
104
|
+
export declare class SupportApi {
|
|
105
|
+
private readonly t;
|
|
106
|
+
constructor(t: Transport);
|
|
107
|
+
/** Ask Runtime support. If status is "working", read() it again in a minute. */
|
|
108
|
+
message(input: {
|
|
109
|
+
message?: string;
|
|
110
|
+
conversationId?: string;
|
|
111
|
+
approveActionId?: string;
|
|
112
|
+
approveInputHash?: string;
|
|
113
|
+
denyActionId?: string;
|
|
114
|
+
}): Promise<SupportReply>;
|
|
115
|
+
read(conversationId: string): Promise<SupportReply>;
|
|
116
|
+
}
|
|
117
|
+
/** The client behind the static helpers, from the environment. */
|
|
118
|
+
export declare function defaultClient(): Runtime;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
2
|
+
import { clientFactories } from "./products/index.js";
|
|
3
|
+
import { Sandbox, sandboxPage } from "./sandbox.js";
|
|
4
|
+
import { Snapshots } from "./snapshots.js";
|
|
5
|
+
import { Transport, missingKey } from "./transport.js";
|
|
6
|
+
/** The key `runtime login` saved for this machine, found on first use. */
|
|
7
|
+
const savedKey = async () => {
|
|
8
|
+
if (typeof process === "undefined" || !process.versions?.node)
|
|
9
|
+
throw missingKey();
|
|
10
|
+
const { resolveCredential } = await import("./credentials.js");
|
|
11
|
+
return resolveCredential(process.env);
|
|
12
|
+
};
|
|
13
|
+
const env = (name) => typeof process === "undefined"
|
|
14
|
+
? undefined
|
|
15
|
+
: process.env[name];
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
17
|
+
export class Runtime {
|
|
18
|
+
transport;
|
|
19
|
+
sandboxes;
|
|
20
|
+
snapshots;
|
|
21
|
+
feedback;
|
|
22
|
+
support;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.transport = new Transport({
|
|
25
|
+
// A key given, then RUNTIME_API_KEY, then (in Node and Bun) the key
|
|
26
|
+
// `runtime login` saved for this machine, read on the first call.
|
|
27
|
+
apiKey: options.apiKey ?? env("RUNTIME_API_KEY") ?? savedKey,
|
|
28
|
+
baseUrl: options.baseUrl ?? env("RUNTIME_API_URL"),
|
|
29
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
30
|
+
...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
|
|
31
|
+
...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
|
|
32
|
+
...(options.maxConnections ? { maxConnections: options.maxConnections } : {}),
|
|
33
|
+
});
|
|
34
|
+
this.sandboxes = new Sandboxes(this.transport);
|
|
35
|
+
this.snapshots = new Snapshots(this.transport);
|
|
36
|
+
this.feedback = new FeedbackApi(this.transport);
|
|
37
|
+
this.support = new SupportApi(this.transport);
|
|
38
|
+
for (const [name, make] of clientFactories())
|
|
39
|
+
Object.defineProperty(this, name, { value: make(this.transport), enumerable: true });
|
|
40
|
+
}
|
|
41
|
+
/** Who this key is: organization, agent and credential. */
|
|
42
|
+
me(options = {}) {
|
|
43
|
+
return this.transport.json({
|
|
44
|
+
method: "GET",
|
|
45
|
+
path: "/v1/me",
|
|
46
|
+
...options,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** Balance, holds, trial time and per-resource charges, in integer microdollars. */
|
|
50
|
+
/** Credit, holds, trial time and per-resource charges. */
|
|
51
|
+
usage(options = {}) {
|
|
52
|
+
return this.transport.json({
|
|
53
|
+
method: "GET",
|
|
54
|
+
path: "/v1/usage",
|
|
55
|
+
...options,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export class Sandboxes {
|
|
60
|
+
t;
|
|
61
|
+
constructor(t) {
|
|
62
|
+
this.t = t;
|
|
63
|
+
}
|
|
64
|
+
/** Creates a sandbox and waits until it is running. Every field is optional:
|
|
65
|
+
* with none you get the free trial while it lasts, the default region and a
|
|
66
|
+
* 2 vCPU / 4 GiB machine from a warm template. `wait: false` returns at once. */
|
|
67
|
+
async create(input = {}, options = {}) {
|
|
68
|
+
const { wait, ...body } = input;
|
|
69
|
+
const info = await this.t.json({
|
|
70
|
+
method: "POST",
|
|
71
|
+
path: "/v1/sandboxes",
|
|
72
|
+
body,
|
|
73
|
+
wait: wait === false ? 0 : 60,
|
|
74
|
+
...options,
|
|
75
|
+
});
|
|
76
|
+
const sandbox = new Sandbox(this.t, info);
|
|
77
|
+
if (wait !== false && info.state !== "running") {
|
|
78
|
+
await sandbox.waitFor("running", { timeoutSeconds: 60 });
|
|
79
|
+
if (sandbox.state !== "running")
|
|
80
|
+
throw new RuntimeError({
|
|
81
|
+
message: `Sandbox ${info.id} is ${sandbox.state}, not running.`,
|
|
82
|
+
code: "start_failed",
|
|
83
|
+
status: 0,
|
|
84
|
+
hint: "Read it with runtime.sandboxes.get(id); stopReason says why.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return sandbox;
|
|
88
|
+
}
|
|
89
|
+
async get(id, options = {}) {
|
|
90
|
+
return new Sandbox(this.t, await this.t.json({
|
|
91
|
+
method: "GET",
|
|
92
|
+
path: `/v1/sandboxes/${encodeURIComponent(id)}`,
|
|
93
|
+
...options,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
/** Live sandboxes, oldest first. Await for a page, or `for await` over all. */
|
|
97
|
+
async list(filter = {}) {
|
|
98
|
+
const query = {
|
|
99
|
+
...(filter.state ? { state: filter.state } : {}),
|
|
100
|
+
...(filter.includeStopped ? { includeStopped: true } : {}),
|
|
101
|
+
...(filter.labels
|
|
102
|
+
? { label: Object.entries(filter.labels).map(([k, v]) => `${k}:${v}`) }
|
|
103
|
+
: {}),
|
|
104
|
+
...(filter.name ? { name: filter.name } : {}),
|
|
105
|
+
...(filter.limit ? { limit: filter.limit } : {}),
|
|
106
|
+
};
|
|
107
|
+
return sandboxPage(this.t, await this.t.json({ method: "GET", path: "/v1/sandboxes", query }), query);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export class FeedbackApi {
|
|
111
|
+
t;
|
|
112
|
+
constructor(t) {
|
|
113
|
+
this.t = t;
|
|
114
|
+
}
|
|
115
|
+
/** Tell the Runtime team something. Please do, generously: bugs, missing
|
|
116
|
+
* features, what another provider does better, what blocks a migration. */
|
|
117
|
+
submit(input, options = {}) {
|
|
118
|
+
return this.t.json({
|
|
119
|
+
method: "POST",
|
|
120
|
+
path: "/v1/feedback",
|
|
121
|
+
body: input,
|
|
122
|
+
...options,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
list(options = {}) {
|
|
126
|
+
return this.t.json({
|
|
127
|
+
method: "GET",
|
|
128
|
+
path: "/v1/feedback",
|
|
129
|
+
query: { limit: options.limit },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export class SupportApi {
|
|
134
|
+
t;
|
|
135
|
+
constructor(t) {
|
|
136
|
+
this.t = t;
|
|
137
|
+
}
|
|
138
|
+
/** Ask Runtime support. If status is "working", read() it again in a minute. */
|
|
139
|
+
message(input) {
|
|
140
|
+
return this.t.json({
|
|
141
|
+
method: "POST",
|
|
142
|
+
path: "/v1/support/messages",
|
|
143
|
+
body: input,
|
|
144
|
+
retry: false,
|
|
145
|
+
timeoutMs: 120_000,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
read(conversationId) {
|
|
149
|
+
return this.t.json({
|
|
150
|
+
method: "GET",
|
|
151
|
+
path: `/v1/support/conversations/${encodeURIComponent(conversationId)}`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let shared;
|
|
156
|
+
/** The client behind the static helpers, from the environment. */
|
|
157
|
+
export function defaultClient() {
|
|
158
|
+
shared ??= new Runtime();
|
|
159
|
+
return shared;
|
|
160
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { missingKey } from "./transport.js";
|
|
2
|
+
export type SavedConnection = {
|
|
3
|
+
version: 1;
|
|
4
|
+
apiOrigin: string;
|
|
5
|
+
authOrigin: string;
|
|
6
|
+
key: string;
|
|
7
|
+
connectionId: string;
|
|
8
|
+
orgId: string;
|
|
9
|
+
agentName: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function connectionOrigins(env: NodeJS.ProcessEnv): {
|
|
12
|
+
api: string;
|
|
13
|
+
auth: string;
|
|
14
|
+
};
|
|
15
|
+
/** Private user storage, outside project files. Nothing reading this module
|
|
16
|
+
* returns a secret to CLI output. Store names bind credentials to both origins. */
|
|
17
|
+
export declare function connectionStore(env: NodeJS.ProcessEnv, directory?: string): {
|
|
18
|
+
read(): Promise<SavedConnection | null>;
|
|
19
|
+
save(connection: SavedConnection): Promise<void>;
|
|
20
|
+
remove(): Promise<void>;
|
|
21
|
+
/** A browser approval that was started and not yet answered, so a command
|
|
22
|
+
* that was stopped while it waited picks up the same link and code rather
|
|
23
|
+
* than asking the person to approve a new one. Unreadable or stale ones
|
|
24
|
+
* read as none. */
|
|
25
|
+
readPending(): Promise<PendingConnection | null>;
|
|
26
|
+
savePending(pending: PendingConnection): Promise<void>;
|
|
27
|
+
removePending(): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
/** What `readPending` keeps: the private half of the key pair the approved
|
|
30
|
+
* key is encrypted to, and the request it answers. */
|
|
31
|
+
export type PendingConnection = {
|
|
32
|
+
version: 1;
|
|
33
|
+
apiOrigin: string;
|
|
34
|
+
authOrigin: string;
|
|
35
|
+
privateKey: string;
|
|
36
|
+
deviceCode: string;
|
|
37
|
+
url: string;
|
|
38
|
+
userCode: string;
|
|
39
|
+
agentName: string;
|
|
40
|
+
expiresAt: number;
|
|
41
|
+
};
|
|
42
|
+
export declare function resolveCredential(env: NodeJS.ProcessEnv): Promise<string>;
|
|
43
|
+
/** No key in the environment and none saved on this machine. */
|
|
44
|
+
export declare const notConnected: typeof missingKey;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { isAbsolute, join } from "node:path";
|
|
6
|
+
import { apiOrigin } from "./index.js";
|
|
7
|
+
import { missingKey } from "./transport.js";
|
|
8
|
+
export function connectionOrigins(env) {
|
|
9
|
+
return {
|
|
10
|
+
api: apiOrigin(env.RUNTIME_API_URL ?? "https://api.withruntime.com"),
|
|
11
|
+
auth: apiOrigin(env.RUNTIME_AUTH_URL ?? "https://withruntime.com"),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function defaultDirectory(env) {
|
|
15
|
+
const root = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
16
|
+
if (!isAbsolute(root))
|
|
17
|
+
throw new Error("XDG_CONFIG_HOME must be an absolute path.");
|
|
18
|
+
return join(root, "runtime-cloud");
|
|
19
|
+
}
|
|
20
|
+
/** Private user storage, outside project files. Nothing reading this module
|
|
21
|
+
* returns a secret to CLI output. Store names bind credentials to both origins. */
|
|
22
|
+
export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
23
|
+
const origins = connectionOrigins(env);
|
|
24
|
+
const name = createHash("sha256").update(`${origins.auth}\n${origins.api}`).digest("hex");
|
|
25
|
+
const file = join(directory, `${name}.json`);
|
|
26
|
+
async function privateDirectory(create) {
|
|
27
|
+
if (create)
|
|
28
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
29
|
+
try {
|
|
30
|
+
const info = await lstat(directory);
|
|
31
|
+
if (!info.isDirectory() ||
|
|
32
|
+
info.isSymbolicLink() ||
|
|
33
|
+
(process.platform !== "win32" &&
|
|
34
|
+
((info.mode & 0o077) !== 0 || info.uid !== process.getuid?.())))
|
|
35
|
+
throw new Error("Runtime's credential directory must be private to your user.");
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (!create && error.code === "ENOENT")
|
|
39
|
+
return false;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/** One private file in the private directory: its text, or null. */
|
|
45
|
+
async function readPrivate(path, limit, label) {
|
|
46
|
+
if (!(await privateDirectory(false)))
|
|
47
|
+
return null;
|
|
48
|
+
let handle;
|
|
49
|
+
try {
|
|
50
|
+
handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code === "ENOENT")
|
|
54
|
+
return null;
|
|
55
|
+
throw new Error(`Could not safely open Runtime's ${label}.`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const info = await handle.stat();
|
|
59
|
+
if (!info.isFile() ||
|
|
60
|
+
info.size > limit ||
|
|
61
|
+
(process.platform !== "win32" &&
|
|
62
|
+
((info.mode & 0o077) !== 0 || info.uid !== process.getuid?.())))
|
|
63
|
+
throw new Error(`Runtime's ${label} must be private to your user.`);
|
|
64
|
+
return await handle.readFile("utf8");
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await handle.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function writePrivate(path, text) {
|
|
71
|
+
await privateDirectory(true);
|
|
72
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
73
|
+
try {
|
|
74
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
75
|
+
try {
|
|
76
|
+
await handle.writeFile(text);
|
|
77
|
+
await handle.sync();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await handle.close();
|
|
81
|
+
}
|
|
82
|
+
await rename(temporary, path);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await unlink(temporary).catch(() => undefined);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function removePrivate(path) {
|
|
89
|
+
if (await privateDirectory(false))
|
|
90
|
+
await unlink(path).catch((error) => {
|
|
91
|
+
if (error.code !== "ENOENT")
|
|
92
|
+
throw error;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const pendingFile = join(directory, `${name}.pending.json`);
|
|
96
|
+
return {
|
|
97
|
+
async read() {
|
|
98
|
+
const text = await readPrivate(file, 4096, "saved connection");
|
|
99
|
+
if (text === null)
|
|
100
|
+
return null;
|
|
101
|
+
let saved;
|
|
102
|
+
try {
|
|
103
|
+
saved = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new Error("Runtime's saved connection is invalid. Connect again.");
|
|
107
|
+
}
|
|
108
|
+
if (saved.version !== 1 ||
|
|
109
|
+
saved.apiOrigin !== origins.api ||
|
|
110
|
+
saved.authOrigin !== origins.auth ||
|
|
111
|
+
typeof saved.key !== "string" ||
|
|
112
|
+
!/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) ||
|
|
113
|
+
typeof saved.connectionId !== "string" ||
|
|
114
|
+
typeof saved.orgId !== "string" ||
|
|
115
|
+
typeof saved.agentName !== "string")
|
|
116
|
+
throw new Error("Runtime's saved connection is invalid. Connect again.");
|
|
117
|
+
return saved;
|
|
118
|
+
},
|
|
119
|
+
async save(connection) {
|
|
120
|
+
if (connection.apiOrigin !== origins.api || connection.authOrigin !== origins.auth)
|
|
121
|
+
throw new Error("Connection origins do not match.");
|
|
122
|
+
await writePrivate(file, JSON.stringify(connection));
|
|
123
|
+
},
|
|
124
|
+
async remove() {
|
|
125
|
+
await removePrivate(file);
|
|
126
|
+
},
|
|
127
|
+
/** A browser approval that was started and not yet answered, so a command
|
|
128
|
+
* that was stopped while it waited picks up the same link and code rather
|
|
129
|
+
* than asking the person to approve a new one. Unreadable or stale ones
|
|
130
|
+
* read as none. */
|
|
131
|
+
async readPending() {
|
|
132
|
+
let text;
|
|
133
|
+
try {
|
|
134
|
+
text = await readPrivate(pendingFile, 8192, "pending connection");
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (text === null)
|
|
140
|
+
return null;
|
|
141
|
+
try {
|
|
142
|
+
const pending = JSON.parse(text);
|
|
143
|
+
if (pending.version !== 1 ||
|
|
144
|
+
pending.apiOrigin !== origins.api ||
|
|
145
|
+
pending.authOrigin !== origins.auth ||
|
|
146
|
+
typeof pending.privateKey !== "string" ||
|
|
147
|
+
typeof pending.deviceCode !== "string" ||
|
|
148
|
+
typeof pending.url !== "string" ||
|
|
149
|
+
typeof pending.userCode !== "string" ||
|
|
150
|
+
typeof pending.agentName !== "string" ||
|
|
151
|
+
typeof pending.expiresAt !== "number" ||
|
|
152
|
+
pending.expiresAt <= Date.now())
|
|
153
|
+
return null;
|
|
154
|
+
return pending;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
async savePending(pending) {
|
|
161
|
+
await writePrivate(pendingFile, JSON.stringify(pending));
|
|
162
|
+
},
|
|
163
|
+
async removePending() {
|
|
164
|
+
await removePrivate(pendingFile);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export async function resolveCredential(env) {
|
|
169
|
+
if (env.RUNTIME_API_KEY) {
|
|
170
|
+
if (/\s/.test(env.RUNTIME_API_KEY))
|
|
171
|
+
throw new Error("RUNTIME_API_KEY is invalid.");
|
|
172
|
+
return env.RUNTIME_API_KEY;
|
|
173
|
+
}
|
|
174
|
+
const saved = await connectionStore(env).read();
|
|
175
|
+
if (!saved)
|
|
176
|
+
throw notConnected();
|
|
177
|
+
return saved.key;
|
|
178
|
+
}
|
|
179
|
+
/** No key in the environment and none saved on this machine. */
|
|
180
|
+
export const notConnected = missingKey;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Every failure the SDK raises. `code` is stable and machine-readable,
|
|
2
|
+
* `hint` says what to do next, and `requestId` is what to quote in a report
|
|
3
|
+
* (runtime.feedback.submit, `runtime feedback`, or support). */
|
|
4
|
+
export declare class RuntimeError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly requestId?: string;
|
|
8
|
+
readonly hint?: string;
|
|
9
|
+
readonly details?: Record<string, unknown>;
|
|
10
|
+
/** The key the SDK sent. A retry with it never repeats the effect. */
|
|
11
|
+
readonly idempotencyKey?: string;
|
|
12
|
+
readonly retryAfterMs?: number;
|
|
13
|
+
constructor(init: {
|
|
14
|
+
message: string;
|
|
15
|
+
code: string;
|
|
16
|
+
status: number;
|
|
17
|
+
requestId?: string;
|
|
18
|
+
hint?: string;
|
|
19
|
+
details?: Record<string, unknown>;
|
|
20
|
+
idempotencyKey?: string;
|
|
21
|
+
retryAfterMs?: number;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
});
|
|
24
|
+
/** Retrying this exact call (the SDK keeps the key) is safe and may work. */
|
|
25
|
+
get retryable(): boolean;
|
|
26
|
+
toString(): string;
|
|
27
|
+
}
|
|
28
|
+
export declare class AuthenticationError extends RuntimeError {
|
|
29
|
+
}
|
|
30
|
+
export declare class PermissionDeniedError extends RuntimeError {
|
|
31
|
+
}
|
|
32
|
+
export declare class NotFoundError extends RuntimeError {
|
|
33
|
+
}
|
|
34
|
+
export declare class ConflictError extends RuntimeError {
|
|
35
|
+
}
|
|
36
|
+
export declare class InvalidRequestError extends RuntimeError {
|
|
37
|
+
}
|
|
38
|
+
export declare class RateLimitError extends RuntimeError {
|
|
39
|
+
}
|
|
40
|
+
export declare class ServiceUnavailableError extends RuntimeError {
|
|
41
|
+
}
|
|
42
|
+
/** No answer arrived. A write may or may not have happened; retrying with the
|
|
43
|
+
* same idempotencyKey (the SDK does, automatically) settles it safely. */
|
|
44
|
+
export declare class ConnectionError extends RuntimeError {
|
|
45
|
+
}
|
|
46
|
+
/** Raised by exec({ check: true }) when a command exits non-zero. */
|
|
47
|
+
export declare class CommandError extends RuntimeError {
|
|
48
|
+
readonly exitCode: number | null;
|
|
49
|
+
readonly stdout: string;
|
|
50
|
+
readonly stderr: string;
|
|
51
|
+
constructor(result: {
|
|
52
|
+
exitCode: number | null;
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
timedOut?: boolean;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export declare function errorFor(status: number, body: unknown, idempotencyKey?: string): RuntimeError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Every failure the SDK raises. `code` is stable and machine-readable,
|
|
2
|
+
* `hint` says what to do next, and `requestId` is what to quote in a report
|
|
3
|
+
* (runtime.feedback.submit, `runtime feedback`, or support). */
|
|
4
|
+
export class RuntimeError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
requestId;
|
|
8
|
+
hint;
|
|
9
|
+
details;
|
|
10
|
+
/** The key the SDK sent. A retry with it never repeats the effect. */
|
|
11
|
+
idempotencyKey;
|
|
12
|
+
retryAfterMs;
|
|
13
|
+
constructor(init) {
|
|
14
|
+
super(init.message, init.cause === undefined ? undefined : { cause: init.cause });
|
|
15
|
+
this.name = new.target.name;
|
|
16
|
+
this.code = init.code;
|
|
17
|
+
this.status = init.status;
|
|
18
|
+
if (init.requestId !== undefined)
|
|
19
|
+
this.requestId = init.requestId;
|
|
20
|
+
if (init.hint !== undefined)
|
|
21
|
+
this.hint = init.hint;
|
|
22
|
+
if (init.details !== undefined)
|
|
23
|
+
this.details = init.details;
|
|
24
|
+
if (init.idempotencyKey !== undefined)
|
|
25
|
+
this.idempotencyKey = init.idempotencyKey;
|
|
26
|
+
if (init.retryAfterMs !== undefined)
|
|
27
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
28
|
+
}
|
|
29
|
+
/** Retrying this exact call (the SDK keeps the key) is safe and may work. */
|
|
30
|
+
get retryable() {
|
|
31
|
+
return (this.status === 0 ||
|
|
32
|
+
this.status === 429 ||
|
|
33
|
+
this.status === 502 ||
|
|
34
|
+
this.status === 503 ||
|
|
35
|
+
this.status === 504);
|
|
36
|
+
}
|
|
37
|
+
toString() {
|
|
38
|
+
return `${this.name} [${this.code}${this.status ? ` ${this.status}` : ""}]: ${this.message}${this.hint ? `\nHint: ${this.hint}` : ""}${this.requestId ? `\nRequest: ${this.requestId}` : ""}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class AuthenticationError extends RuntimeError {
|
|
42
|
+
}
|
|
43
|
+
export class PermissionDeniedError extends RuntimeError {
|
|
44
|
+
}
|
|
45
|
+
export class NotFoundError extends RuntimeError {
|
|
46
|
+
}
|
|
47
|
+
export class ConflictError extends RuntimeError {
|
|
48
|
+
}
|
|
49
|
+
export class InvalidRequestError extends RuntimeError {
|
|
50
|
+
}
|
|
51
|
+
export class RateLimitError extends RuntimeError {
|
|
52
|
+
}
|
|
53
|
+
export class ServiceUnavailableError extends RuntimeError {
|
|
54
|
+
}
|
|
55
|
+
/** No answer arrived. A write may or may not have happened; retrying with the
|
|
56
|
+
* same idempotencyKey (the SDK does, automatically) settles it safely. */
|
|
57
|
+
export class ConnectionError extends RuntimeError {
|
|
58
|
+
}
|
|
59
|
+
/** Raised by exec({ check: true }) when a command exits non-zero. */
|
|
60
|
+
export class CommandError extends RuntimeError {
|
|
61
|
+
exitCode;
|
|
62
|
+
stdout;
|
|
63
|
+
stderr;
|
|
64
|
+
constructor(result) {
|
|
65
|
+
super({
|
|
66
|
+
message: result.timedOut
|
|
67
|
+
? "Command timed out."
|
|
68
|
+
: `Command exited with ${result.exitCode}.${result.stderr ? ` ${result.stderr.trim().slice(-500)}` : ""}`,
|
|
69
|
+
code: result.timedOut ? "command_timeout" : "command_failed",
|
|
70
|
+
status: 0,
|
|
71
|
+
});
|
|
72
|
+
this.exitCode = result.exitCode;
|
|
73
|
+
this.stdout = result.stdout;
|
|
74
|
+
this.stderr = result.stderr;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function errorFor(status, body, idempotencyKey) {
|
|
78
|
+
const error = body?.error ?? {};
|
|
79
|
+
const init = {
|
|
80
|
+
message: typeof error.message === "string" ? error.message : `Runtime request failed (${status}).`,
|
|
81
|
+
code: typeof error.code === "string" ? error.code : "request_failed",
|
|
82
|
+
status,
|
|
83
|
+
...(typeof error.requestId === "string" ? { requestId: error.requestId } : {}),
|
|
84
|
+
...(typeof error.hint === "string" ? { hint: error.hint } : {}),
|
|
85
|
+
...(error.details && typeof error.details === "object"
|
|
86
|
+
? { details: error.details }
|
|
87
|
+
: {}),
|
|
88
|
+
...(typeof error.retryAfterMs === "number" ? { retryAfterMs: error.retryAfterMs } : {}),
|
|
89
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
90
|
+
};
|
|
91
|
+
if (status === 401)
|
|
92
|
+
return new AuthenticationError(init);
|
|
93
|
+
if (status === 403)
|
|
94
|
+
return new PermissionDeniedError(init);
|
|
95
|
+
if (status === 404)
|
|
96
|
+
return new NotFoundError(init);
|
|
97
|
+
if (status === 409)
|
|
98
|
+
return new ConflictError(init);
|
|
99
|
+
if (status === 400 || status === 422 || status === 413)
|
|
100
|
+
return new InvalidRequestError(init);
|
|
101
|
+
if (status === 429)
|
|
102
|
+
return new RateLimitError(init);
|
|
103
|
+
if (status >= 500)
|
|
104
|
+
return new ServiceUnavailableError(init);
|
|
105
|
+
return new RuntimeError(init);
|
|
106
|
+
}
|