@quicktane-sdk/client 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/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/index.cjs +357 -0
- package/dist/index.d.cts +138 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +320 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QuickTane
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# QuickTane Node SDK
|
|
2
|
+
|
|
3
|
+
Run code in isolated, ephemeral cloud sandboxes ([QuickTane](https://quicktane.com)) — one API call. TypeScript-native, zero runtime dependencies (uses the built-in `fetch`).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @quicktane-sdk/client
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Requires Node 18+.
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { QuickTane } from "@quicktane-sdk/client";
|
|
15
|
+
|
|
16
|
+
const qt = new QuickTane("sk_live_..."); // or set QUICKTANE_API_KEY
|
|
17
|
+
|
|
18
|
+
const run = await qt.runAndWait("print('hello from the sandbox')", "python");
|
|
19
|
+
|
|
20
|
+
console.log(run.status); // "completed"
|
|
21
|
+
console.log(run.output); // "hello from the sandbox\n"
|
|
22
|
+
console.log(run.exitCode); // 0
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Supported languages: `python`, `node`.
|
|
26
|
+
|
|
27
|
+
## Fire-and-forget + webhooks
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const run = await qt.run("console.log(2 + 2)", "node");
|
|
31
|
+
console.log(run.runId, run.status); // 42 "running"
|
|
32
|
+
|
|
33
|
+
const finished = await qt.getRun(run.runId);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Register a webhook endpoint in your dashboard, then verify deliveries with the **raw** body:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { verifySignature } from "@quicktane-sdk/client";
|
|
40
|
+
|
|
41
|
+
if (!verifySignature(rawBody, req.headers["x-quicktane-signature"], signingSecret)) {
|
|
42
|
+
return res.status(400).end();
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Interactive sessions
|
|
47
|
+
|
|
48
|
+
Run many commands in one live sandbox — file and process state persist between calls:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const sbx = await qt.createSandbox("python"); // waits until ready
|
|
52
|
+
|
|
53
|
+
await sbx.files.write("app.py", "print('hello from a session')");
|
|
54
|
+
const result = await sbx.execCommand("python app.py");
|
|
55
|
+
console.log(result.stdout, result.ok); // "hello from a session\n" true
|
|
56
|
+
|
|
57
|
+
// state persists across execs:
|
|
58
|
+
await sbx.execCommand("pip install requests");
|
|
59
|
+
await sbx.exec("import requests; print(requests.__version__)");
|
|
60
|
+
|
|
61
|
+
await sbx.kill();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
new QuickTane(apiKey, {
|
|
68
|
+
baseUrl, // override the API base (e.g. "http://localhost:8000" for local dev)
|
|
69
|
+
timeout, // HTTP request timeout in ms (default 30000)
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`apiKey` falls back to `QUICKTANE_API_KEY`; `baseUrl` to `QUICKTANE_BASE_URL`.
|
|
74
|
+
|
|
75
|
+
## Errors
|
|
76
|
+
|
|
77
|
+
All errors extend `QuickTaneError`: `AuthenticationError` (401/403), `NotFoundError` (404),
|
|
78
|
+
`ValidationError` (422), `RateLimitError` (429), `APIError` (other). Each carries
|
|
79
|
+
`.statusCode` and `.body`.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpm install
|
|
85
|
+
pnpm test # vitest
|
|
86
|
+
pnpm build # tsup → dist (ESM + CJS + types)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
MIT licensed.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
APIError: () => APIError,
|
|
24
|
+
AuthenticationError: () => AuthenticationError,
|
|
25
|
+
Files: () => Files,
|
|
26
|
+
NotFoundError: () => NotFoundError,
|
|
27
|
+
QuickTane: () => QuickTane,
|
|
28
|
+
QuickTaneError: () => QuickTaneError,
|
|
29
|
+
RateLimitError: () => RateLimitError,
|
|
30
|
+
Run: () => Run,
|
|
31
|
+
Sandbox: () => Sandbox,
|
|
32
|
+
ValidationError: () => ValidationError,
|
|
33
|
+
verifySignature: () => verifySignature
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/errors.ts
|
|
38
|
+
var QuickTaneError = class extends Error {
|
|
39
|
+
statusCode;
|
|
40
|
+
body;
|
|
41
|
+
constructor(message, statusCode, body) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = new.target.name;
|
|
44
|
+
this.statusCode = statusCode;
|
|
45
|
+
this.body = body;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var AuthenticationError = class extends QuickTaneError {
|
|
49
|
+
};
|
|
50
|
+
var NotFoundError = class extends QuickTaneError {
|
|
51
|
+
};
|
|
52
|
+
var ValidationError = class extends QuickTaneError {
|
|
53
|
+
};
|
|
54
|
+
var RateLimitError = class extends QuickTaneError {
|
|
55
|
+
};
|
|
56
|
+
var APIError = class extends QuickTaneError {
|
|
57
|
+
};
|
|
58
|
+
var STATUS_MAP = {
|
|
59
|
+
401: AuthenticationError,
|
|
60
|
+
403: AuthenticationError,
|
|
61
|
+
404: NotFoundError,
|
|
62
|
+
422: ValidationError,
|
|
63
|
+
429: RateLimitError
|
|
64
|
+
};
|
|
65
|
+
async function raiseForStatus(response) {
|
|
66
|
+
if (response.status < 400) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let body = null;
|
|
70
|
+
let message = response.statusText || "Request failed";
|
|
71
|
+
try {
|
|
72
|
+
body = await response.clone().json();
|
|
73
|
+
message = body?.message ?? firstValidationError(body) ?? message;
|
|
74
|
+
} catch {
|
|
75
|
+
try {
|
|
76
|
+
body = await response.clone().text();
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const ErrorClass = STATUS_MAP[response.status] ?? APIError;
|
|
81
|
+
throw new ErrorClass(message, response.status, body);
|
|
82
|
+
}
|
|
83
|
+
function firstValidationError(body) {
|
|
84
|
+
const errors = body?.errors;
|
|
85
|
+
if (errors && typeof errors === "object") {
|
|
86
|
+
for (const messages of Object.values(errors)) {
|
|
87
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
88
|
+
return String(messages[0]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/models.ts
|
|
96
|
+
var TERMINAL = /* @__PURE__ */ new Set([
|
|
97
|
+
"completed",
|
|
98
|
+
"failed",
|
|
99
|
+
"timeout",
|
|
100
|
+
"error",
|
|
101
|
+
"killed"
|
|
102
|
+
]);
|
|
103
|
+
var Run = class _Run {
|
|
104
|
+
constructor(runId, status, language, exitCode, durationMs, output, createdAt) {
|
|
105
|
+
this.runId = runId;
|
|
106
|
+
this.status = status;
|
|
107
|
+
this.language = language;
|
|
108
|
+
this.exitCode = exitCode;
|
|
109
|
+
this.durationMs = durationMs;
|
|
110
|
+
this.output = output;
|
|
111
|
+
this.createdAt = createdAt;
|
|
112
|
+
}
|
|
113
|
+
runId;
|
|
114
|
+
status;
|
|
115
|
+
language;
|
|
116
|
+
exitCode;
|
|
117
|
+
durationMs;
|
|
118
|
+
output;
|
|
119
|
+
createdAt;
|
|
120
|
+
/** True once the run has finished (in any terminal state). */
|
|
121
|
+
get isTerminal() {
|
|
122
|
+
return TERMINAL.has(this.status);
|
|
123
|
+
}
|
|
124
|
+
/** True when the run completed with a zero exit code. */
|
|
125
|
+
get succeeded() {
|
|
126
|
+
return this.status === "completed" && (this.exitCode === 0 || this.exitCode === null);
|
|
127
|
+
}
|
|
128
|
+
static fromJSON(data) {
|
|
129
|
+
return new _Run(
|
|
130
|
+
data.run_id,
|
|
131
|
+
data.status,
|
|
132
|
+
data.language,
|
|
133
|
+
data.exit_code ?? null,
|
|
134
|
+
data.duration_ms ?? null,
|
|
135
|
+
data.output ?? null,
|
|
136
|
+
data.created_at ?? null
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/sandbox.ts
|
|
142
|
+
var TERMINAL2 = /* @__PURE__ */ new Set(["stopped", "error"]);
|
|
143
|
+
function sleep(ms) {
|
|
144
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
145
|
+
}
|
|
146
|
+
var Files = class {
|
|
147
|
+
constructor(sandbox) {
|
|
148
|
+
this.sandbox = sandbox;
|
|
149
|
+
}
|
|
150
|
+
sandbox;
|
|
151
|
+
async write(path, content) {
|
|
152
|
+
await this.sandbox.request("POST", "/files", { path, content });
|
|
153
|
+
}
|
|
154
|
+
async read(path) {
|
|
155
|
+
const data = await this.sandbox.request("GET", "/files", { path });
|
|
156
|
+
return data.content ?? "";
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var Sandbox = class {
|
|
160
|
+
constructor(client, sessionId, language, status) {
|
|
161
|
+
this.client = client;
|
|
162
|
+
this.sessionId = sessionId;
|
|
163
|
+
this.language = language;
|
|
164
|
+
this.status = status;
|
|
165
|
+
this.files = new Files(this);
|
|
166
|
+
}
|
|
167
|
+
client;
|
|
168
|
+
sessionId;
|
|
169
|
+
language;
|
|
170
|
+
status;
|
|
171
|
+
files;
|
|
172
|
+
/** Run code (in the session language, or `options.language`) in the sandbox. */
|
|
173
|
+
async exec(code, options = {}) {
|
|
174
|
+
return this.run({
|
|
175
|
+
language: options.language ?? this.language,
|
|
176
|
+
code,
|
|
177
|
+
timeout: options.timeout ?? 30
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/** Run a shell command in the sandbox. */
|
|
181
|
+
async execCommand(command, options = {}) {
|
|
182
|
+
return this.run({ command, timeout: options.timeout ?? 30 });
|
|
183
|
+
}
|
|
184
|
+
/** Stop the session and free its pod. */
|
|
185
|
+
async kill() {
|
|
186
|
+
await this.request("DELETE", "");
|
|
187
|
+
this.status = "stopped";
|
|
188
|
+
}
|
|
189
|
+
async run(payload) {
|
|
190
|
+
const data = await this.request("POST", "/exec", payload);
|
|
191
|
+
const exitCode = data.exit_code ?? null;
|
|
192
|
+
const timedOut = Boolean(data.timed_out);
|
|
193
|
+
return {
|
|
194
|
+
stdout: data.stdout ?? "",
|
|
195
|
+
stderr: data.stderr ?? "",
|
|
196
|
+
exitCode,
|
|
197
|
+
timedOut,
|
|
198
|
+
ok: exitCode === 0 && !timedOut
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** @internal */
|
|
202
|
+
async waitReady(pollInterval, maxWait) {
|
|
203
|
+
const start = Date.now();
|
|
204
|
+
while (this.status !== "ready" && !TERMINAL2.has(this.status)) {
|
|
205
|
+
if (Date.now() - start >= maxWait) {
|
|
206
|
+
throw new Error(`Session ${this.sessionId} was not ready within ${maxWait}ms`);
|
|
207
|
+
}
|
|
208
|
+
await sleep(pollInterval);
|
|
209
|
+
const data = await this.client.json("GET", `/sessions/${this.sessionId}`);
|
|
210
|
+
this.status = data.status;
|
|
211
|
+
}
|
|
212
|
+
if (this.status !== "ready") {
|
|
213
|
+
throw new Error(`Session ${this.sessionId} entered status "${this.status}"`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** @internal */
|
|
217
|
+
request(method, path, data) {
|
|
218
|
+
return this.client.json(method, `/sessions/${this.sessionId}${path}`, data);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/client.ts
|
|
223
|
+
var DEFAULT_BASE_URL = "https://api.quicktane.com";
|
|
224
|
+
var VERSION = "0.1.0";
|
|
225
|
+
var QuickTane = class {
|
|
226
|
+
apiKey;
|
|
227
|
+
baseUrl;
|
|
228
|
+
timeoutMs;
|
|
229
|
+
fetchImpl;
|
|
230
|
+
constructor(apiKey, options = {}) {
|
|
231
|
+
const key = apiKey ?? options.apiKey ?? process.env.QUICKTANE_API_KEY;
|
|
232
|
+
if (!key) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
"A QuickTane API key is required. Pass it in or set QUICKTANE_API_KEY."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
this.apiKey = key;
|
|
238
|
+
this.baseUrl = (options.baseUrl ?? process.env.QUICKTANE_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
239
|
+
this.timeoutMs = options.timeout ?? 3e4;
|
|
240
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
241
|
+
if (!this.fetchImpl) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
"Global fetch is not available. Upgrade to Node 18+ or pass a `fetch` implementation."
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Queue a sandbox run and return immediately (status is usually `running`). */
|
|
248
|
+
async run(code, language = "python", options = {}) {
|
|
249
|
+
const response = await this.request("POST", "/run", {
|
|
250
|
+
language,
|
|
251
|
+
code,
|
|
252
|
+
timeout: options.timeout ?? 30
|
|
253
|
+
});
|
|
254
|
+
return Run.fromJSON(await response.json());
|
|
255
|
+
}
|
|
256
|
+
/** Fetch the current state of a run. */
|
|
257
|
+
async getRun(runId) {
|
|
258
|
+
const response = await this.request("GET", `/runs/${runId}`);
|
|
259
|
+
return Run.fromJSON(await response.json());
|
|
260
|
+
}
|
|
261
|
+
/** Queue a run and poll until it reaches a terminal state, then return it. */
|
|
262
|
+
async runAndWait(code, language = "python", options = {}) {
|
|
263
|
+
const pollInterval = options.pollInterval ?? 1e3;
|
|
264
|
+
const maxWait = options.maxWait ?? 3e5;
|
|
265
|
+
let run = await this.run(code, language, { timeout: options.timeout });
|
|
266
|
+
const start = Date.now();
|
|
267
|
+
while (!run.isTerminal) {
|
|
268
|
+
if (Date.now() - start >= maxWait) {
|
|
269
|
+
throw new Error(`Run ${run.runId} did not finish within ${maxWait}ms`);
|
|
270
|
+
}
|
|
271
|
+
await sleep2(pollInterval);
|
|
272
|
+
run = await this.getRun(run.runId);
|
|
273
|
+
}
|
|
274
|
+
return run;
|
|
275
|
+
}
|
|
276
|
+
/** Start a live interactive session and (by default) wait until it is ready. */
|
|
277
|
+
async createSandbox(language = "python", options = {}) {
|
|
278
|
+
const data = await this.json("POST", "/sessions", { language });
|
|
279
|
+
const sandbox = new Sandbox(this, data.session_id, data.language, data.status);
|
|
280
|
+
if (options.wait ?? true) {
|
|
281
|
+
await sandbox.waitReady(options.pollInterval ?? 1e3, options.maxWait ?? 12e4);
|
|
282
|
+
}
|
|
283
|
+
return sandbox;
|
|
284
|
+
}
|
|
285
|
+
/** Reconnect to an existing session by id. */
|
|
286
|
+
async getSandbox(sessionId) {
|
|
287
|
+
const data = await this.json("GET", `/sessions/${sessionId}`);
|
|
288
|
+
return new Sandbox(this, data.session_id, data.language, data.status);
|
|
289
|
+
}
|
|
290
|
+
/** @internal — JSON request/response helper used by sessions. */
|
|
291
|
+
async json(method, path, data) {
|
|
292
|
+
let url = path;
|
|
293
|
+
let body;
|
|
294
|
+
if (method === "GET") {
|
|
295
|
+
if (data && Object.keys(data).length > 0) {
|
|
296
|
+
url = `${path}?${new URLSearchParams(data).toString()}`;
|
|
297
|
+
}
|
|
298
|
+
} else if (data !== void 0) {
|
|
299
|
+
body = data;
|
|
300
|
+
}
|
|
301
|
+
const response = await this.request(method, url, body);
|
|
302
|
+
const text = await response.text();
|
|
303
|
+
return text ? JSON.parse(text) : {};
|
|
304
|
+
}
|
|
305
|
+
async request(method, path, body) {
|
|
306
|
+
const controller = new AbortController();
|
|
307
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
308
|
+
let response;
|
|
309
|
+
try {
|
|
310
|
+
response = await this.fetchImpl(`${this.baseUrl}/v1${path}`, {
|
|
311
|
+
method,
|
|
312
|
+
headers: {
|
|
313
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
314
|
+
Accept: "application/json",
|
|
315
|
+
"User-Agent": `quicktane-node/${VERSION}`,
|
|
316
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
317
|
+
},
|
|
318
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
319
|
+
signal: controller.signal
|
|
320
|
+
});
|
|
321
|
+
} finally {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
}
|
|
324
|
+
await raiseForStatus(response);
|
|
325
|
+
return response;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
function sleep2(ms) {
|
|
329
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/webhooks.ts
|
|
333
|
+
var import_node_crypto = require("crypto");
|
|
334
|
+
function verifySignature(payload, signatureHeader, secret) {
|
|
335
|
+
if (!signatureHeader) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const body = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
339
|
+
const expected = "sha256=" + (0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex");
|
|
340
|
+
const a = Buffer.from(expected);
|
|
341
|
+
const b = Buffer.from(signatureHeader);
|
|
342
|
+
return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
|
|
343
|
+
}
|
|
344
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
345
|
+
0 && (module.exports = {
|
|
346
|
+
APIError,
|
|
347
|
+
AuthenticationError,
|
|
348
|
+
Files,
|
|
349
|
+
NotFoundError,
|
|
350
|
+
QuickTane,
|
|
351
|
+
QuickTaneError,
|
|
352
|
+
RateLimitError,
|
|
353
|
+
Run,
|
|
354
|
+
Sandbox,
|
|
355
|
+
ValidationError,
|
|
356
|
+
verifySignature
|
|
357
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
type RunStatus = "pending" | "running" | "completed" | "failed" | "timeout" | "error" | "killed";
|
|
2
|
+
/** A sandbox execution. */
|
|
3
|
+
declare class Run {
|
|
4
|
+
readonly runId: number;
|
|
5
|
+
readonly status: string;
|
|
6
|
+
readonly language: string;
|
|
7
|
+
readonly exitCode: number | null;
|
|
8
|
+
readonly durationMs: number | null;
|
|
9
|
+
readonly output: string | null;
|
|
10
|
+
readonly createdAt: string | null;
|
|
11
|
+
constructor(runId: number, status: string, language: string, exitCode: number | null, durationMs: number | null, output: string | null, createdAt: string | null);
|
|
12
|
+
/** True once the run has finished (in any terminal state). */
|
|
13
|
+
get isTerminal(): boolean;
|
|
14
|
+
/** True when the run completed with a zero exit code. */
|
|
15
|
+
get succeeded(): boolean;
|
|
16
|
+
static fromJSON(data: Record<string, unknown>): Run;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ExecResult {
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
exitCode: number | null;
|
|
23
|
+
timedOut: boolean;
|
|
24
|
+
/** True when the command completed with a zero exit code. */
|
|
25
|
+
ok: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface ExecOptions {
|
|
28
|
+
language?: string;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Filesystem access for a session workspace. */
|
|
32
|
+
declare class Files {
|
|
33
|
+
private readonly sandbox;
|
|
34
|
+
constructor(sandbox: Sandbox);
|
|
35
|
+
write(path: string, content: string): Promise<void>;
|
|
36
|
+
read(path: string): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
/** A live interactive session — run many commands in one persistent environment. */
|
|
39
|
+
declare class Sandbox {
|
|
40
|
+
private readonly client;
|
|
41
|
+
readonly sessionId: number;
|
|
42
|
+
readonly language: string;
|
|
43
|
+
status: string;
|
|
44
|
+
readonly files: Files;
|
|
45
|
+
constructor(client: QuickTane, sessionId: number, language: string, status: string);
|
|
46
|
+
/** Run code (in the session language, or `options.language`) in the sandbox. */
|
|
47
|
+
exec(code: string, options?: ExecOptions): Promise<ExecResult>;
|
|
48
|
+
/** Run a shell command in the sandbox. */
|
|
49
|
+
execCommand(command: string, options?: {
|
|
50
|
+
timeout?: number;
|
|
51
|
+
}): Promise<ExecResult>;
|
|
52
|
+
/** Stop the session and free its pod. */
|
|
53
|
+
kill(): Promise<void>;
|
|
54
|
+
private run;
|
|
55
|
+
/** @internal */
|
|
56
|
+
waitReady(pollInterval: number, maxWait: number): Promise<void>;
|
|
57
|
+
/** @internal */
|
|
58
|
+
request(method: string, path: string, data?: Record<string, unknown>): Promise<Record<string, any>>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface QuickTaneOptions {
|
|
62
|
+
apiKey?: string;
|
|
63
|
+
baseUrl?: string;
|
|
64
|
+
/** HTTP request timeout in milliseconds (default 30000). */
|
|
65
|
+
timeout?: number;
|
|
66
|
+
/** Custom fetch implementation (defaults to the global `fetch`). */
|
|
67
|
+
fetch?: typeof fetch;
|
|
68
|
+
}
|
|
69
|
+
interface RunOptions {
|
|
70
|
+
/** Sandbox time limit in seconds (default 30). */
|
|
71
|
+
timeout?: number;
|
|
72
|
+
}
|
|
73
|
+
interface RunAndWaitOptions extends RunOptions {
|
|
74
|
+
/** Milliseconds between status polls (default 1000). */
|
|
75
|
+
pollInterval?: number;
|
|
76
|
+
/** Give up after this many milliseconds (default 300000). */
|
|
77
|
+
maxWait?: number;
|
|
78
|
+
}
|
|
79
|
+
declare class QuickTane {
|
|
80
|
+
private readonly apiKey;
|
|
81
|
+
private readonly baseUrl;
|
|
82
|
+
private readonly timeoutMs;
|
|
83
|
+
private readonly fetchImpl;
|
|
84
|
+
constructor(apiKey?: string, options?: QuickTaneOptions);
|
|
85
|
+
/** Queue a sandbox run and return immediately (status is usually `running`). */
|
|
86
|
+
run(code: string, language?: string, options?: RunOptions): Promise<Run>;
|
|
87
|
+
/** Fetch the current state of a run. */
|
|
88
|
+
getRun(runId: number | string): Promise<Run>;
|
|
89
|
+
/** Queue a run and poll until it reaches a terminal state, then return it. */
|
|
90
|
+
runAndWait(code: string, language?: string, options?: RunAndWaitOptions): Promise<Run>;
|
|
91
|
+
/** Start a live interactive session and (by default) wait until it is ready. */
|
|
92
|
+
createSandbox(language?: string, options?: {
|
|
93
|
+
wait?: boolean;
|
|
94
|
+
pollInterval?: number;
|
|
95
|
+
maxWait?: number;
|
|
96
|
+
}): Promise<Sandbox>;
|
|
97
|
+
/** Reconnect to an existing session by id. */
|
|
98
|
+
getSandbox(sessionId: number): Promise<Sandbox>;
|
|
99
|
+
/** @internal — JSON request/response helper used by sessions. */
|
|
100
|
+
json(method: string, path: string, data?: Record<string, unknown>): Promise<Record<string, any>>;
|
|
101
|
+
private request;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
declare class QuickTaneError extends Error {
|
|
105
|
+
statusCode?: number;
|
|
106
|
+
body?: unknown;
|
|
107
|
+
constructor(message: string, statusCode?: number, body?: unknown);
|
|
108
|
+
}
|
|
109
|
+
/** Invalid, missing, or suspended API key (401 / 403). */
|
|
110
|
+
declare class AuthenticationError extends QuickTaneError {
|
|
111
|
+
}
|
|
112
|
+
/** The requested run does not exist (404). */
|
|
113
|
+
declare class NotFoundError extends QuickTaneError {
|
|
114
|
+
}
|
|
115
|
+
/** The request was rejected (422). */
|
|
116
|
+
declare class ValidationError extends QuickTaneError {
|
|
117
|
+
}
|
|
118
|
+
/** Rate limit or plan concurrency limit exceeded (429). */
|
|
119
|
+
declare class RateLimitError extends QuickTaneError {
|
|
120
|
+
}
|
|
121
|
+
/** Any other non-2xx response. */
|
|
122
|
+
declare class APIError extends QuickTaneError {
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Verify a QuickTane webhook signature.
|
|
127
|
+
*
|
|
128
|
+
* Compute the HMAC-SHA256 of the raw request body with your endpoint's signing
|
|
129
|
+
* secret and compare it, in constant time, to the `X-QuickTane-Signature`
|
|
130
|
+
* header (`sha256=<hex>`).
|
|
131
|
+
*
|
|
132
|
+
* @param payload the RAW request body (string or Buffer).
|
|
133
|
+
* @param signatureHeader the `X-QuickTane-Signature` header value.
|
|
134
|
+
* @param secret your endpoint's signing secret (`whsec_...`).
|
|
135
|
+
*/
|
|
136
|
+
declare function verifySignature(payload: string | Buffer, signatureHeader: string, secret: string): boolean;
|
|
137
|
+
|
|
138
|
+
export { APIError, AuthenticationError, type ExecOptions, type ExecResult, Files, NotFoundError, QuickTane, QuickTaneError, type QuickTaneOptions, RateLimitError, Run, type RunAndWaitOptions, type RunOptions, type RunStatus, Sandbox, ValidationError, verifySignature };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
type RunStatus = "pending" | "running" | "completed" | "failed" | "timeout" | "error" | "killed";
|
|
2
|
+
/** A sandbox execution. */
|
|
3
|
+
declare class Run {
|
|
4
|
+
readonly runId: number;
|
|
5
|
+
readonly status: string;
|
|
6
|
+
readonly language: string;
|
|
7
|
+
readonly exitCode: number | null;
|
|
8
|
+
readonly durationMs: number | null;
|
|
9
|
+
readonly output: string | null;
|
|
10
|
+
readonly createdAt: string | null;
|
|
11
|
+
constructor(runId: number, status: string, language: string, exitCode: number | null, durationMs: number | null, output: string | null, createdAt: string | null);
|
|
12
|
+
/** True once the run has finished (in any terminal state). */
|
|
13
|
+
get isTerminal(): boolean;
|
|
14
|
+
/** True when the run completed with a zero exit code. */
|
|
15
|
+
get succeeded(): boolean;
|
|
16
|
+
static fromJSON(data: Record<string, unknown>): Run;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ExecResult {
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
exitCode: number | null;
|
|
23
|
+
timedOut: boolean;
|
|
24
|
+
/** True when the command completed with a zero exit code. */
|
|
25
|
+
ok: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface ExecOptions {
|
|
28
|
+
language?: string;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Filesystem access for a session workspace. */
|
|
32
|
+
declare class Files {
|
|
33
|
+
private readonly sandbox;
|
|
34
|
+
constructor(sandbox: Sandbox);
|
|
35
|
+
write(path: string, content: string): Promise<void>;
|
|
36
|
+
read(path: string): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
/** A live interactive session — run many commands in one persistent environment. */
|
|
39
|
+
declare class Sandbox {
|
|
40
|
+
private readonly client;
|
|
41
|
+
readonly sessionId: number;
|
|
42
|
+
readonly language: string;
|
|
43
|
+
status: string;
|
|
44
|
+
readonly files: Files;
|
|
45
|
+
constructor(client: QuickTane, sessionId: number, language: string, status: string);
|
|
46
|
+
/** Run code (in the session language, or `options.language`) in the sandbox. */
|
|
47
|
+
exec(code: string, options?: ExecOptions): Promise<ExecResult>;
|
|
48
|
+
/** Run a shell command in the sandbox. */
|
|
49
|
+
execCommand(command: string, options?: {
|
|
50
|
+
timeout?: number;
|
|
51
|
+
}): Promise<ExecResult>;
|
|
52
|
+
/** Stop the session and free its pod. */
|
|
53
|
+
kill(): Promise<void>;
|
|
54
|
+
private run;
|
|
55
|
+
/** @internal */
|
|
56
|
+
waitReady(pollInterval: number, maxWait: number): Promise<void>;
|
|
57
|
+
/** @internal */
|
|
58
|
+
request(method: string, path: string, data?: Record<string, unknown>): Promise<Record<string, any>>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface QuickTaneOptions {
|
|
62
|
+
apiKey?: string;
|
|
63
|
+
baseUrl?: string;
|
|
64
|
+
/** HTTP request timeout in milliseconds (default 30000). */
|
|
65
|
+
timeout?: number;
|
|
66
|
+
/** Custom fetch implementation (defaults to the global `fetch`). */
|
|
67
|
+
fetch?: typeof fetch;
|
|
68
|
+
}
|
|
69
|
+
interface RunOptions {
|
|
70
|
+
/** Sandbox time limit in seconds (default 30). */
|
|
71
|
+
timeout?: number;
|
|
72
|
+
}
|
|
73
|
+
interface RunAndWaitOptions extends RunOptions {
|
|
74
|
+
/** Milliseconds between status polls (default 1000). */
|
|
75
|
+
pollInterval?: number;
|
|
76
|
+
/** Give up after this many milliseconds (default 300000). */
|
|
77
|
+
maxWait?: number;
|
|
78
|
+
}
|
|
79
|
+
declare class QuickTane {
|
|
80
|
+
private readonly apiKey;
|
|
81
|
+
private readonly baseUrl;
|
|
82
|
+
private readonly timeoutMs;
|
|
83
|
+
private readonly fetchImpl;
|
|
84
|
+
constructor(apiKey?: string, options?: QuickTaneOptions);
|
|
85
|
+
/** Queue a sandbox run and return immediately (status is usually `running`). */
|
|
86
|
+
run(code: string, language?: string, options?: RunOptions): Promise<Run>;
|
|
87
|
+
/** Fetch the current state of a run. */
|
|
88
|
+
getRun(runId: number | string): Promise<Run>;
|
|
89
|
+
/** Queue a run and poll until it reaches a terminal state, then return it. */
|
|
90
|
+
runAndWait(code: string, language?: string, options?: RunAndWaitOptions): Promise<Run>;
|
|
91
|
+
/** Start a live interactive session and (by default) wait until it is ready. */
|
|
92
|
+
createSandbox(language?: string, options?: {
|
|
93
|
+
wait?: boolean;
|
|
94
|
+
pollInterval?: number;
|
|
95
|
+
maxWait?: number;
|
|
96
|
+
}): Promise<Sandbox>;
|
|
97
|
+
/** Reconnect to an existing session by id. */
|
|
98
|
+
getSandbox(sessionId: number): Promise<Sandbox>;
|
|
99
|
+
/** @internal — JSON request/response helper used by sessions. */
|
|
100
|
+
json(method: string, path: string, data?: Record<string, unknown>): Promise<Record<string, any>>;
|
|
101
|
+
private request;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
declare class QuickTaneError extends Error {
|
|
105
|
+
statusCode?: number;
|
|
106
|
+
body?: unknown;
|
|
107
|
+
constructor(message: string, statusCode?: number, body?: unknown);
|
|
108
|
+
}
|
|
109
|
+
/** Invalid, missing, or suspended API key (401 / 403). */
|
|
110
|
+
declare class AuthenticationError extends QuickTaneError {
|
|
111
|
+
}
|
|
112
|
+
/** The requested run does not exist (404). */
|
|
113
|
+
declare class NotFoundError extends QuickTaneError {
|
|
114
|
+
}
|
|
115
|
+
/** The request was rejected (422). */
|
|
116
|
+
declare class ValidationError extends QuickTaneError {
|
|
117
|
+
}
|
|
118
|
+
/** Rate limit or plan concurrency limit exceeded (429). */
|
|
119
|
+
declare class RateLimitError extends QuickTaneError {
|
|
120
|
+
}
|
|
121
|
+
/** Any other non-2xx response. */
|
|
122
|
+
declare class APIError extends QuickTaneError {
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Verify a QuickTane webhook signature.
|
|
127
|
+
*
|
|
128
|
+
* Compute the HMAC-SHA256 of the raw request body with your endpoint's signing
|
|
129
|
+
* secret and compare it, in constant time, to the `X-QuickTane-Signature`
|
|
130
|
+
* header (`sha256=<hex>`).
|
|
131
|
+
*
|
|
132
|
+
* @param payload the RAW request body (string or Buffer).
|
|
133
|
+
* @param signatureHeader the `X-QuickTane-Signature` header value.
|
|
134
|
+
* @param secret your endpoint's signing secret (`whsec_...`).
|
|
135
|
+
*/
|
|
136
|
+
declare function verifySignature(payload: string | Buffer, signatureHeader: string, secret: string): boolean;
|
|
137
|
+
|
|
138
|
+
export { APIError, AuthenticationError, type ExecOptions, type ExecResult, Files, NotFoundError, QuickTane, QuickTaneError, type QuickTaneOptions, RateLimitError, Run, type RunAndWaitOptions, type RunOptions, type RunStatus, Sandbox, ValidationError, verifySignature };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var QuickTaneError = class extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, statusCode, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
this.statusCode = statusCode;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var AuthenticationError = class extends QuickTaneError {
|
|
13
|
+
};
|
|
14
|
+
var NotFoundError = class extends QuickTaneError {
|
|
15
|
+
};
|
|
16
|
+
var ValidationError = class extends QuickTaneError {
|
|
17
|
+
};
|
|
18
|
+
var RateLimitError = class extends QuickTaneError {
|
|
19
|
+
};
|
|
20
|
+
var APIError = class extends QuickTaneError {
|
|
21
|
+
};
|
|
22
|
+
var STATUS_MAP = {
|
|
23
|
+
401: AuthenticationError,
|
|
24
|
+
403: AuthenticationError,
|
|
25
|
+
404: NotFoundError,
|
|
26
|
+
422: ValidationError,
|
|
27
|
+
429: RateLimitError
|
|
28
|
+
};
|
|
29
|
+
async function raiseForStatus(response) {
|
|
30
|
+
if (response.status < 400) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let body = null;
|
|
34
|
+
let message = response.statusText || "Request failed";
|
|
35
|
+
try {
|
|
36
|
+
body = await response.clone().json();
|
|
37
|
+
message = body?.message ?? firstValidationError(body) ?? message;
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
body = await response.clone().text();
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const ErrorClass = STATUS_MAP[response.status] ?? APIError;
|
|
45
|
+
throw new ErrorClass(message, response.status, body);
|
|
46
|
+
}
|
|
47
|
+
function firstValidationError(body) {
|
|
48
|
+
const errors = body?.errors;
|
|
49
|
+
if (errors && typeof errors === "object") {
|
|
50
|
+
for (const messages of Object.values(errors)) {
|
|
51
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
52
|
+
return String(messages[0]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/models.ts
|
|
60
|
+
var TERMINAL = /* @__PURE__ */ new Set([
|
|
61
|
+
"completed",
|
|
62
|
+
"failed",
|
|
63
|
+
"timeout",
|
|
64
|
+
"error",
|
|
65
|
+
"killed"
|
|
66
|
+
]);
|
|
67
|
+
var Run = class _Run {
|
|
68
|
+
constructor(runId, status, language, exitCode, durationMs, output, createdAt) {
|
|
69
|
+
this.runId = runId;
|
|
70
|
+
this.status = status;
|
|
71
|
+
this.language = language;
|
|
72
|
+
this.exitCode = exitCode;
|
|
73
|
+
this.durationMs = durationMs;
|
|
74
|
+
this.output = output;
|
|
75
|
+
this.createdAt = createdAt;
|
|
76
|
+
}
|
|
77
|
+
runId;
|
|
78
|
+
status;
|
|
79
|
+
language;
|
|
80
|
+
exitCode;
|
|
81
|
+
durationMs;
|
|
82
|
+
output;
|
|
83
|
+
createdAt;
|
|
84
|
+
/** True once the run has finished (in any terminal state). */
|
|
85
|
+
get isTerminal() {
|
|
86
|
+
return TERMINAL.has(this.status);
|
|
87
|
+
}
|
|
88
|
+
/** True when the run completed with a zero exit code. */
|
|
89
|
+
get succeeded() {
|
|
90
|
+
return this.status === "completed" && (this.exitCode === 0 || this.exitCode === null);
|
|
91
|
+
}
|
|
92
|
+
static fromJSON(data) {
|
|
93
|
+
return new _Run(
|
|
94
|
+
data.run_id,
|
|
95
|
+
data.status,
|
|
96
|
+
data.language,
|
|
97
|
+
data.exit_code ?? null,
|
|
98
|
+
data.duration_ms ?? null,
|
|
99
|
+
data.output ?? null,
|
|
100
|
+
data.created_at ?? null
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/sandbox.ts
|
|
106
|
+
var TERMINAL2 = /* @__PURE__ */ new Set(["stopped", "error"]);
|
|
107
|
+
function sleep(ms) {
|
|
108
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
}
|
|
110
|
+
var Files = class {
|
|
111
|
+
constructor(sandbox) {
|
|
112
|
+
this.sandbox = sandbox;
|
|
113
|
+
}
|
|
114
|
+
sandbox;
|
|
115
|
+
async write(path, content) {
|
|
116
|
+
await this.sandbox.request("POST", "/files", { path, content });
|
|
117
|
+
}
|
|
118
|
+
async read(path) {
|
|
119
|
+
const data = await this.sandbox.request("GET", "/files", { path });
|
|
120
|
+
return data.content ?? "";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var Sandbox = class {
|
|
124
|
+
constructor(client, sessionId, language, status) {
|
|
125
|
+
this.client = client;
|
|
126
|
+
this.sessionId = sessionId;
|
|
127
|
+
this.language = language;
|
|
128
|
+
this.status = status;
|
|
129
|
+
this.files = new Files(this);
|
|
130
|
+
}
|
|
131
|
+
client;
|
|
132
|
+
sessionId;
|
|
133
|
+
language;
|
|
134
|
+
status;
|
|
135
|
+
files;
|
|
136
|
+
/** Run code (in the session language, or `options.language`) in the sandbox. */
|
|
137
|
+
async exec(code, options = {}) {
|
|
138
|
+
return this.run({
|
|
139
|
+
language: options.language ?? this.language,
|
|
140
|
+
code,
|
|
141
|
+
timeout: options.timeout ?? 30
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Run a shell command in the sandbox. */
|
|
145
|
+
async execCommand(command, options = {}) {
|
|
146
|
+
return this.run({ command, timeout: options.timeout ?? 30 });
|
|
147
|
+
}
|
|
148
|
+
/** Stop the session and free its pod. */
|
|
149
|
+
async kill() {
|
|
150
|
+
await this.request("DELETE", "");
|
|
151
|
+
this.status = "stopped";
|
|
152
|
+
}
|
|
153
|
+
async run(payload) {
|
|
154
|
+
const data = await this.request("POST", "/exec", payload);
|
|
155
|
+
const exitCode = data.exit_code ?? null;
|
|
156
|
+
const timedOut = Boolean(data.timed_out);
|
|
157
|
+
return {
|
|
158
|
+
stdout: data.stdout ?? "",
|
|
159
|
+
stderr: data.stderr ?? "",
|
|
160
|
+
exitCode,
|
|
161
|
+
timedOut,
|
|
162
|
+
ok: exitCode === 0 && !timedOut
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** @internal */
|
|
166
|
+
async waitReady(pollInterval, maxWait) {
|
|
167
|
+
const start = Date.now();
|
|
168
|
+
while (this.status !== "ready" && !TERMINAL2.has(this.status)) {
|
|
169
|
+
if (Date.now() - start >= maxWait) {
|
|
170
|
+
throw new Error(`Session ${this.sessionId} was not ready within ${maxWait}ms`);
|
|
171
|
+
}
|
|
172
|
+
await sleep(pollInterval);
|
|
173
|
+
const data = await this.client.json("GET", `/sessions/${this.sessionId}`);
|
|
174
|
+
this.status = data.status;
|
|
175
|
+
}
|
|
176
|
+
if (this.status !== "ready") {
|
|
177
|
+
throw new Error(`Session ${this.sessionId} entered status "${this.status}"`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** @internal */
|
|
181
|
+
request(method, path, data) {
|
|
182
|
+
return this.client.json(method, `/sessions/${this.sessionId}${path}`, data);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// src/client.ts
|
|
187
|
+
var DEFAULT_BASE_URL = "https://api.quicktane.com";
|
|
188
|
+
var VERSION = "0.1.0";
|
|
189
|
+
var QuickTane = class {
|
|
190
|
+
apiKey;
|
|
191
|
+
baseUrl;
|
|
192
|
+
timeoutMs;
|
|
193
|
+
fetchImpl;
|
|
194
|
+
constructor(apiKey, options = {}) {
|
|
195
|
+
const key = apiKey ?? options.apiKey ?? process.env.QUICKTANE_API_KEY;
|
|
196
|
+
if (!key) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"A QuickTane API key is required. Pass it in or set QUICKTANE_API_KEY."
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
this.apiKey = key;
|
|
202
|
+
this.baseUrl = (options.baseUrl ?? process.env.QUICKTANE_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
203
|
+
this.timeoutMs = options.timeout ?? 3e4;
|
|
204
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
205
|
+
if (!this.fetchImpl) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"Global fetch is not available. Upgrade to Node 18+ or pass a `fetch` implementation."
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Queue a sandbox run and return immediately (status is usually `running`). */
|
|
212
|
+
async run(code, language = "python", options = {}) {
|
|
213
|
+
const response = await this.request("POST", "/run", {
|
|
214
|
+
language,
|
|
215
|
+
code,
|
|
216
|
+
timeout: options.timeout ?? 30
|
|
217
|
+
});
|
|
218
|
+
return Run.fromJSON(await response.json());
|
|
219
|
+
}
|
|
220
|
+
/** Fetch the current state of a run. */
|
|
221
|
+
async getRun(runId) {
|
|
222
|
+
const response = await this.request("GET", `/runs/${runId}`);
|
|
223
|
+
return Run.fromJSON(await response.json());
|
|
224
|
+
}
|
|
225
|
+
/** Queue a run and poll until it reaches a terminal state, then return it. */
|
|
226
|
+
async runAndWait(code, language = "python", options = {}) {
|
|
227
|
+
const pollInterval = options.pollInterval ?? 1e3;
|
|
228
|
+
const maxWait = options.maxWait ?? 3e5;
|
|
229
|
+
let run = await this.run(code, language, { timeout: options.timeout });
|
|
230
|
+
const start = Date.now();
|
|
231
|
+
while (!run.isTerminal) {
|
|
232
|
+
if (Date.now() - start >= maxWait) {
|
|
233
|
+
throw new Error(`Run ${run.runId} did not finish within ${maxWait}ms`);
|
|
234
|
+
}
|
|
235
|
+
await sleep2(pollInterval);
|
|
236
|
+
run = await this.getRun(run.runId);
|
|
237
|
+
}
|
|
238
|
+
return run;
|
|
239
|
+
}
|
|
240
|
+
/** Start a live interactive session and (by default) wait until it is ready. */
|
|
241
|
+
async createSandbox(language = "python", options = {}) {
|
|
242
|
+
const data = await this.json("POST", "/sessions", { language });
|
|
243
|
+
const sandbox = new Sandbox(this, data.session_id, data.language, data.status);
|
|
244
|
+
if (options.wait ?? true) {
|
|
245
|
+
await sandbox.waitReady(options.pollInterval ?? 1e3, options.maxWait ?? 12e4);
|
|
246
|
+
}
|
|
247
|
+
return sandbox;
|
|
248
|
+
}
|
|
249
|
+
/** Reconnect to an existing session by id. */
|
|
250
|
+
async getSandbox(sessionId) {
|
|
251
|
+
const data = await this.json("GET", `/sessions/${sessionId}`);
|
|
252
|
+
return new Sandbox(this, data.session_id, data.language, data.status);
|
|
253
|
+
}
|
|
254
|
+
/** @internal — JSON request/response helper used by sessions. */
|
|
255
|
+
async json(method, path, data) {
|
|
256
|
+
let url = path;
|
|
257
|
+
let body;
|
|
258
|
+
if (method === "GET") {
|
|
259
|
+
if (data && Object.keys(data).length > 0) {
|
|
260
|
+
url = `${path}?${new URLSearchParams(data).toString()}`;
|
|
261
|
+
}
|
|
262
|
+
} else if (data !== void 0) {
|
|
263
|
+
body = data;
|
|
264
|
+
}
|
|
265
|
+
const response = await this.request(method, url, body);
|
|
266
|
+
const text = await response.text();
|
|
267
|
+
return text ? JSON.parse(text) : {};
|
|
268
|
+
}
|
|
269
|
+
async request(method, path, body) {
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
272
|
+
let response;
|
|
273
|
+
try {
|
|
274
|
+
response = await this.fetchImpl(`${this.baseUrl}/v1${path}`, {
|
|
275
|
+
method,
|
|
276
|
+
headers: {
|
|
277
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
278
|
+
Accept: "application/json",
|
|
279
|
+
"User-Agent": `quicktane-node/${VERSION}`,
|
|
280
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
281
|
+
},
|
|
282
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
283
|
+
signal: controller.signal
|
|
284
|
+
});
|
|
285
|
+
} finally {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
}
|
|
288
|
+
await raiseForStatus(response);
|
|
289
|
+
return response;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
function sleep2(ms) {
|
|
293
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/webhooks.ts
|
|
297
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
298
|
+
function verifySignature(payload, signatureHeader, secret) {
|
|
299
|
+
if (!signatureHeader) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const body = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
303
|
+
const expected = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
|
|
304
|
+
const a = Buffer.from(expected);
|
|
305
|
+
const b = Buffer.from(signatureHeader);
|
|
306
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
307
|
+
}
|
|
308
|
+
export {
|
|
309
|
+
APIError,
|
|
310
|
+
AuthenticationError,
|
|
311
|
+
Files,
|
|
312
|
+
NotFoundError,
|
|
313
|
+
QuickTane,
|
|
314
|
+
QuickTaneError,
|
|
315
|
+
RateLimitError,
|
|
316
|
+
Run,
|
|
317
|
+
Sandbox,
|
|
318
|
+
ValidationError,
|
|
319
|
+
verifySignature
|
|
320
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quicktane-sdk/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node/TypeScript SDK for QuickTane — run code in isolated, ephemeral cloud sandboxes.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist"],
|
|
18
|
+
"engines": { "node": ">=18" },
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["sandbox", "code-execution", "gvisor", "ai-agents", "code-interpreter"],
|
|
25
|
+
"homepage": "https://quicktane.com/docs",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/quicktane/SDK-NodeJS.git"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": { "access": "public" },
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20",
|
|
33
|
+
"tsup": "^8",
|
|
34
|
+
"typescript": "^5",
|
|
35
|
+
"vitest": "^2"
|
|
36
|
+
}
|
|
37
|
+
}
|