@superclerk/sdk 0.0.0-stage → 0.1.1
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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +269 -2
- package/api.md +310 -0
- package/core/api-promise.d.ts +27 -0
- package/core/api-promise.js +52 -0
- package/core/client.d.ts +72 -0
- package/core/client.js +226 -0
- package/core/errors.d.ts +62 -0
- package/core/errors.js +72 -0
- package/core/pagination.d.ts +29 -0
- package/core/pagination.js +50 -0
- package/core/timing.d.ts +20 -0
- package/core/timing.js +54 -0
- package/generated/client.d.ts +31 -0
- package/generated/client.js +34 -0
- package/generated/resources.d.ts +1321 -0
- package/generated/resources.js +1958 -0
- package/generated/tools.d.ts +23983 -0
- package/generated/tools.js +2 -0
- package/generated/types.d.ts +8877 -0
- package/generated/types.js +2 -0
- package/generated/version.d.ts +4 -0
- package/generated/version.js +5 -0
- package/generated/webhooks.d.ts +4 -0
- package/generated/webhooks.js +2 -0
- package/index.d.ts +14 -0
- package/index.js +5 -0
- package/lib/connect.d.ts +12 -0
- package/lib/connect.js +14 -0
- package/lib/connections.d.ts +17 -0
- package/lib/connections.js +28 -0
- package/lib/social.d.ts +32 -0
- package/lib/social.js +27 -0
- package/lib/tools.d.ts +33 -0
- package/lib/tools.js +31 -0
- package/lib/webhooks.d.ts +38 -0
- package/lib/webhooks.js +65 -0
- package/package.json +34 -4
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { APIConnectionError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* What every method returns: a promise of the parsed answer that also hands over the raw response. The request starts
|
|
4
|
+
* when the method is called, like fetch; the body is parsed once, when the promise is first awaited.
|
|
5
|
+
*/
|
|
6
|
+
export class APIPromise extends Promise {
|
|
7
|
+
response;
|
|
8
|
+
parse;
|
|
9
|
+
#parsed;
|
|
10
|
+
constructor(response, parse) {
|
|
11
|
+
// The executor's value is never read: then, catch and finally all go through the parsed answer.
|
|
12
|
+
super((resolve) => resolve(undefined));
|
|
13
|
+
this.response = response;
|
|
14
|
+
this.parse = parse;
|
|
15
|
+
}
|
|
16
|
+
/** The raw response, its body unread. Await the promise or call withResponse instead to read the body. */
|
|
17
|
+
asResponse() {
|
|
18
|
+
return this.response;
|
|
19
|
+
}
|
|
20
|
+
/** The parsed answer with the response it came in and the request's ID. */
|
|
21
|
+
async withResponse() {
|
|
22
|
+
const [response, data] = await Promise.all([this.response, this.parsed()]);
|
|
23
|
+
return { data, response, requestID: response.headers.get("x-superclerk-request-id") };
|
|
24
|
+
}
|
|
25
|
+
parsed() {
|
|
26
|
+
this.#parsed ??= this.response.then(this.parse);
|
|
27
|
+
return this.#parsed;
|
|
28
|
+
}
|
|
29
|
+
then(onfulfilled, onrejected) {
|
|
30
|
+
return this.parsed().then(onfulfilled, onrejected);
|
|
31
|
+
}
|
|
32
|
+
catch(onrejected) {
|
|
33
|
+
return this.parsed().catch(onrejected);
|
|
34
|
+
}
|
|
35
|
+
finally(onfinally) {
|
|
36
|
+
return this.parsed().finally(onfinally);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reads the body of an answer the SDK parses. Your abort rejects with its reason; any other failure means the answer
|
|
41
|
+
* never arrived whole.
|
|
42
|
+
*/
|
|
43
|
+
export async function readBody(read, signal) {
|
|
44
|
+
try {
|
|
45
|
+
return await read;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (signal?.aborted)
|
|
49
|
+
throw signal.reason;
|
|
50
|
+
throw new APIConnectionError("connection_failed", "The answer couldn't be read whole", { cause: error });
|
|
51
|
+
}
|
|
52
|
+
}
|
package/core/client.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { APIPromise } from "./api-promise.js";
|
|
2
|
+
import { PagePromise, type PageBody } from "./pagination.js";
|
|
3
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
/** One API operation, as a generated method describes it. */
|
|
5
|
+
export interface Operation {
|
|
6
|
+
method: HttpMethod;
|
|
7
|
+
/** The path with its parameters already in place, such as `/v1/contacts/cct_123`. */
|
|
8
|
+
path: string;
|
|
9
|
+
query?: Record<string, unknown>;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
/** The operation takes an Idempotency-Key: the SDK sends one, and every retry of the call repeats it. */
|
|
12
|
+
idempotent?: boolean;
|
|
13
|
+
/** A 2xx answer's body: JSON, or a response handed back as it came, such as a provider's through the proxy. */
|
|
14
|
+
response: "json" | "raw";
|
|
15
|
+
}
|
|
16
|
+
export interface ClientOptions {
|
|
17
|
+
/** Defaults to the `SUPERCLERK_API_KEY` environment variable. */
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
/** Defaults to the `SUPERCLERK_BASE_URL` environment variable, then `https://api.superclerk.com`. */
|
|
20
|
+
baseURL?: string;
|
|
21
|
+
/** A client workspace of the key's agency to act in, as `workspaces.list` returns it. */
|
|
22
|
+
workspace?: string;
|
|
23
|
+
/** How long one attempt may wait for its answer to start, in milliseconds. Defaults to 60 seconds. */
|
|
24
|
+
timeout?: number;
|
|
25
|
+
/** How many times a failed attempt is retried when retrying is safe. Defaults to 2. */
|
|
26
|
+
maxRetries?: number;
|
|
27
|
+
/** The secret `webhooks.unwrap` verifies with. Defaults to the `SUPERCLERK_WEBHOOK_SECRET` environment variable. */
|
|
28
|
+
webhookSecret?: string;
|
|
29
|
+
/** The fetch every request goes through. Defaults to the runtime's own. */
|
|
30
|
+
fetch?: (input: Request) => Promise<Response>;
|
|
31
|
+
/** Headers sent with every request. */
|
|
32
|
+
defaultHeaders?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
export interface RequestOptions {
|
|
35
|
+
/** A UUIDv4 to send as Idempotency-Key, for retrying a call across processes. The SDK makes one when it's left out. */
|
|
36
|
+
idempotencyKey?: string;
|
|
37
|
+
/** The client workspace this request acts in, instead of the client's. */
|
|
38
|
+
workspace?: string;
|
|
39
|
+
timeout?: number;
|
|
40
|
+
maxRetries?: number;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
export declare class Core {
|
|
45
|
+
#private;
|
|
46
|
+
readonly baseURL: string;
|
|
47
|
+
readonly workspace: string | undefined;
|
|
48
|
+
readonly timeout: number;
|
|
49
|
+
readonly maxRetries: number;
|
|
50
|
+
readonly webhookSecret: string | undefined;
|
|
51
|
+
/** What this client resolved, its environment included, so a copy keeps its key and address. */
|
|
52
|
+
protected readonly clientOptions: ClientOptions;
|
|
53
|
+
constructor(options?: ClientOptions);
|
|
54
|
+
/** Sends an operation and answers its parsed body. */
|
|
55
|
+
request<T>(operation: Operation, options?: RequestOptions): APIPromise<T>;
|
|
56
|
+
/** Sends a list operation; await it for the first page, or iterate it for every item across pages. */
|
|
57
|
+
list<Body extends PageBody>(operation: Operation, options?: RequestOptions): PagePromise<Body>;
|
|
58
|
+
/**
|
|
59
|
+
* Sends an operation, retrying what is safe to retry, and answers its response. A non-2xx answer throws APIError,
|
|
60
|
+
* except a raw operation's answer from the provider itself, which is handed back whatever its status.
|
|
61
|
+
*/
|
|
62
|
+
send(operation: Operation, options?: RequestOptions): Promise<Response>;
|
|
63
|
+
private log;
|
|
64
|
+
private headers;
|
|
65
|
+
}
|
|
66
|
+
/** The base of every resource: it calls the API through the client it belongs to. */
|
|
67
|
+
export declare class APIResource {
|
|
68
|
+
protected readonly _client: Core;
|
|
69
|
+
constructor(_client: Core);
|
|
70
|
+
}
|
|
71
|
+
/** A path parameter, encoded to stay one segment. */
|
|
72
|
+
export declare function pathParam(value: string): string;
|
package/core/client.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { API_VERSION, DEFAULT_BASE_URL, SDK_VERSION } from "../generated/version.js";
|
|
2
|
+
import { APIPromise, readBody } from "./api-promise.js";
|
|
3
|
+
import { APIConnectionError, APIError, SuperclerkError } from "./errors.js";
|
|
4
|
+
import { PagePromise } from "./pagination.js";
|
|
5
|
+
import { answeredByProvider, retryAfterMs, sleep } from "./timing.js";
|
|
6
|
+
// An attempt the API refused before doing any work is safe to repeat; a timeout or 5xx is safe only for a call that
|
|
7
|
+
// repeats harmlessly: a read, a PUT or DELETE, or a write the server deduplicates by its Idempotency-Key.
|
|
8
|
+
const REFUSED = new Set([429, 503]);
|
|
9
|
+
const UNFINISHED = new Set([408, 500, 502, 504]);
|
|
10
|
+
export class Core {
|
|
11
|
+
baseURL;
|
|
12
|
+
workspace;
|
|
13
|
+
timeout;
|
|
14
|
+
maxRetries;
|
|
15
|
+
webhookSecret;
|
|
16
|
+
/** What this client resolved, its environment included, so a copy keeps its key and address. */
|
|
17
|
+
clientOptions;
|
|
18
|
+
#apiKey;
|
|
19
|
+
#fetch;
|
|
20
|
+
#defaultHeaders;
|
|
21
|
+
#userAgent = userAgent();
|
|
22
|
+
// SUPERCLERK_LOG=debug writes each attempt and retry to stderr, for you or the agent debugging an integration.
|
|
23
|
+
#debug = readEnv("SUPERCLERK_LOG") === "debug";
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
const apiKey = options.apiKey ?? readEnv("SUPERCLERK_API_KEY");
|
|
26
|
+
if (!apiKey)
|
|
27
|
+
throw new SuperclerkError("missing_api_key", "Pass apiKey, or set the SUPERCLERK_API_KEY environment variable. To create a key: https://docs.superclerk.com/authentication");
|
|
28
|
+
this.#apiKey = apiKey;
|
|
29
|
+
this.baseURL = (options.baseURL ?? readEnv("SUPERCLERK_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
30
|
+
this.workspace = options.workspace;
|
|
31
|
+
this.timeout = options.timeout ?? 60_000;
|
|
32
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
33
|
+
this.webhookSecret = options.webhookSecret ?? readEnv("SUPERCLERK_WEBHOOK_SECRET");
|
|
34
|
+
this.#fetch = options.fetch ?? ((request) => fetch(request));
|
|
35
|
+
this.#defaultHeaders = options.defaultHeaders ?? {};
|
|
36
|
+
this.clientOptions = { ...options, apiKey, baseURL: this.baseURL, webhookSecret: this.webhookSecret };
|
|
37
|
+
}
|
|
38
|
+
/** Sends an operation and answers its parsed body. */
|
|
39
|
+
request(operation, options = {}) {
|
|
40
|
+
return new APIPromise(this.send(operation, options), async (response) => operation.response === "raw" ? response : readBody(response.json(), options.signal));
|
|
41
|
+
}
|
|
42
|
+
/** Sends a list operation; await it for the first page, or iterate it for every item across pages. */
|
|
43
|
+
list(operation, options = {}) {
|
|
44
|
+
return new PagePromise(this, operation, options);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Sends an operation, retrying what is safe to retry, and answers its response. A non-2xx answer throws APIError,
|
|
48
|
+
* except a raw operation's answer from the provider itself, which is handed back whatever its status.
|
|
49
|
+
*/
|
|
50
|
+
async send(operation, options = {}) {
|
|
51
|
+
const headers = this.headers(operation, options);
|
|
52
|
+
const body = encodeBody(operation.body, headers);
|
|
53
|
+
// Serialized once, so every attempt sends the same bytes under the same key, a multipart boundary included.
|
|
54
|
+
const prepared = new Request(`${this.baseURL}${operation.path}${query(operation.query)}`, {
|
|
55
|
+
method: operation.method,
|
|
56
|
+
headers,
|
|
57
|
+
body,
|
|
58
|
+
});
|
|
59
|
+
const init = {
|
|
60
|
+
method: prepared.method,
|
|
61
|
+
headers: prepared.headers,
|
|
62
|
+
body: prepared.body === null ? undefined : await prepared.arrayBuffer(),
|
|
63
|
+
};
|
|
64
|
+
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
65
|
+
for (let retries = 0;; retries++) {
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
response = await this.attempt(prepared.url, init, options);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!(error instanceof APIConnectionError) || retries >= maxRetries || !repeatable(operation))
|
|
72
|
+
throw error;
|
|
73
|
+
const wait = backoffMs(retries);
|
|
74
|
+
this.log(`retrying in ${Math.round(wait)} ms after ${error.code}`);
|
|
75
|
+
await sleep(wait, options.signal);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (response.ok || (operation.response === "raw" && answeredByProvider(response)))
|
|
79
|
+
return response;
|
|
80
|
+
if (retries >= maxRetries || !retryable(operation, response)) {
|
|
81
|
+
throw APIError.from(response, await readBody(response.text(), options.signal));
|
|
82
|
+
}
|
|
83
|
+
await response.body?.cancel();
|
|
84
|
+
const wait = retryAfterMs(response) ?? backoffMs(retries);
|
|
85
|
+
this.log(`retrying in ${Math.round(wait)} ms after ${response.status}`);
|
|
86
|
+
await sleep(wait, options.signal);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @internal One attempt at a request through the client's fetch, with your signal, and the timeout running until
|
|
91
|
+
* the answer's headers arrive. No answer throws APIConnectionError; your abort rejects with its reason.
|
|
92
|
+
*/
|
|
93
|
+
async attempt(url, init, options) {
|
|
94
|
+
const timeout = options.timeout ?? this.timeout;
|
|
95
|
+
const timer = new AbortController();
|
|
96
|
+
const stop = setTimeout(() => timer.abort(), timeout);
|
|
97
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timer.signal]) : timer.signal;
|
|
98
|
+
// Host and path only: a signed upload URL's query is a credential.
|
|
99
|
+
const { host, pathname } = new URL(url);
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
try {
|
|
102
|
+
const response = await this.#fetch(new Request(url, { ...init, signal }));
|
|
103
|
+
// An upload URL's storage answers too, without a Superclerk request ID.
|
|
104
|
+
const requestID = response.headers.get("x-superclerk-request-id");
|
|
105
|
+
this.log(`${init.method} ${host}${pathname} → ${response.status} in ${Date.now() - startedAt} ms${requestID ? ` (${requestID})` : ""}`);
|
|
106
|
+
return response;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
this.log(`${init.method} ${host}${pathname} → no answer after ${Date.now() - startedAt} ms`);
|
|
110
|
+
if (options.signal?.aborted)
|
|
111
|
+
throw options.signal.reason;
|
|
112
|
+
if (timer.signal.aborted) {
|
|
113
|
+
throw new APIConnectionError("timeout", `No answer started within ${timeout} ms`, { cause: error });
|
|
114
|
+
}
|
|
115
|
+
throw new APIConnectionError("connection_failed", `${host} could not be reached`, { cause: error });
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(stop);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
log(message) {
|
|
122
|
+
if (this.#debug)
|
|
123
|
+
console.error(`[superclerk] ${message}`);
|
|
124
|
+
}
|
|
125
|
+
headers(operation, options) {
|
|
126
|
+
const headers = new Headers(this.#defaultHeaders);
|
|
127
|
+
headers.set("Authorization", `Bearer ${this.#apiKey}`);
|
|
128
|
+
headers.set("Accept", "application/json");
|
|
129
|
+
headers.set("Superclerk-Version", API_VERSION);
|
|
130
|
+
headers.set("User-Agent", this.#userAgent);
|
|
131
|
+
const workspace = options.workspace ?? this.workspace;
|
|
132
|
+
if (workspace !== undefined)
|
|
133
|
+
headers.set("Superclerk-Workspace", workspace);
|
|
134
|
+
if (operation.idempotent)
|
|
135
|
+
headers.set("Idempotency-Key", options.idempotencyKey ?? crypto.randomUUID());
|
|
136
|
+
for (const [name, value] of Object.entries(options.headers ?? {}))
|
|
137
|
+
headers.set(name, value);
|
|
138
|
+
return headers;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** The base of every resource: it calls the API through the client it belongs to. */
|
|
142
|
+
export class APIResource {
|
|
143
|
+
_client;
|
|
144
|
+
constructor(_client) {
|
|
145
|
+
this._client = _client;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** A path parameter, encoded to stay one segment. */
|
|
149
|
+
export function pathParam(value) {
|
|
150
|
+
return encodeURIComponent(value);
|
|
151
|
+
}
|
|
152
|
+
// An answer that names its run is the run's recorded ending, or the provider's answer: repeating the request would
|
|
153
|
+
// replay it, not redo it.
|
|
154
|
+
function retryable(operation, response) {
|
|
155
|
+
if (response.headers.has("x-superclerk-run-id"))
|
|
156
|
+
return false;
|
|
157
|
+
if (REFUSED.has(response.status))
|
|
158
|
+
return true;
|
|
159
|
+
return UNFINISHED.has(response.status) && repeatable(operation);
|
|
160
|
+
}
|
|
161
|
+
function repeatable(operation) {
|
|
162
|
+
return (operation.idempotent || operation.method === "GET" || operation.method === "PUT" || operation.method === "DELETE");
|
|
163
|
+
}
|
|
164
|
+
function backoffMs(retries) {
|
|
165
|
+
// 0.5 s doubling to 8 s, less up to a quarter at random, so clients that failed together retry apart.
|
|
166
|
+
return Math.min(500 * 2 ** retries, 8_000) * (1 - Math.random() * 0.25);
|
|
167
|
+
}
|
|
168
|
+
// A body fetch already sends goes as it is, such as a proxied form or file; anything else is JSON.
|
|
169
|
+
function encodeBody(body, headers) {
|
|
170
|
+
if (body === undefined ||
|
|
171
|
+
typeof body === "string" ||
|
|
172
|
+
body instanceof Blob ||
|
|
173
|
+
body instanceof ArrayBuffer ||
|
|
174
|
+
ArrayBuffer.isView(body) ||
|
|
175
|
+
body instanceof FormData ||
|
|
176
|
+
body instanceof URLSearchParams) {
|
|
177
|
+
return body;
|
|
178
|
+
}
|
|
179
|
+
headers.set("Content-Type", "application/json");
|
|
180
|
+
return JSON.stringify(body);
|
|
181
|
+
}
|
|
182
|
+
// Arrays repeat their key, and an object's entries become keys of their own (OpenAPI's exploded form style).
|
|
183
|
+
function query(values = {}) {
|
|
184
|
+
const params = new URLSearchParams();
|
|
185
|
+
const add = (key, value) => {
|
|
186
|
+
if (Array.isArray(value))
|
|
187
|
+
return value.forEach((item) => add(key, item));
|
|
188
|
+
if (value !== null && typeof value === "object")
|
|
189
|
+
return Object.entries(value).forEach((entry) => add(...entry));
|
|
190
|
+
if (value !== undefined && value !== null)
|
|
191
|
+
params.append(key, String(value));
|
|
192
|
+
};
|
|
193
|
+
Object.entries(values).forEach((entry) => add(...entry));
|
|
194
|
+
return params.size > 0 ? `?${params}` : "";
|
|
195
|
+
}
|
|
196
|
+
// Node and Bun have a process; Workers and browsers don't, and the SDK names none of their types.
|
|
197
|
+
const runtimeProcess = globalThis.process;
|
|
198
|
+
function readEnv(name) {
|
|
199
|
+
const value = runtimeProcess?.env?.[name];
|
|
200
|
+
return value === "" ? undefined : value;
|
|
201
|
+
}
|
|
202
|
+
// The variable each coding agent sets in the processes it runs, as Stripe's SDK reads them.
|
|
203
|
+
const CODING_AGENTS = [
|
|
204
|
+
["CLAUDECODE", "claude_code"],
|
|
205
|
+
["CURSOR_AGENT", "cursor"],
|
|
206
|
+
["CODEX_SANDBOX", "codex"],
|
|
207
|
+
["CODEX_THREAD_ID", "codex"],
|
|
208
|
+
["GEMINI_CLI", "gemini_cli"],
|
|
209
|
+
["CLINE_ACTIVE", "cline"],
|
|
210
|
+
["OPENCODE", "opencode"],
|
|
211
|
+
];
|
|
212
|
+
/** `superclerk-js/0.1.0 node/22.12.0 darwin-arm64`, ending `agent/claude_code` when a coding agent runs the process. */
|
|
213
|
+
function userAgent() {
|
|
214
|
+
const versions = runtimeProcess?.versions ?? {};
|
|
215
|
+
// Bun and Deno report a Node version too, so they're asked first.
|
|
216
|
+
const runtime = versions.bun
|
|
217
|
+
? `bun/${versions.bun}`
|
|
218
|
+
: versions.deno
|
|
219
|
+
? `deno/${versions.deno}`
|
|
220
|
+
: versions.node
|
|
221
|
+
? `node/${versions.node}`
|
|
222
|
+
: "runtime/unknown";
|
|
223
|
+
const platform = runtimeProcess?.platform && `${runtimeProcess.platform}-${runtimeProcess.arch}`;
|
|
224
|
+
const agent = CODING_AGENTS.find(([variable]) => readEnv(variable))?.[1];
|
|
225
|
+
return [`superclerk-js/${SDK_VERSION}`, runtime, platform, agent && `agent/${agent}`].filter(Boolean).join(" ");
|
|
226
|
+
}
|
package/core/errors.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything the SDK throws. Branch on `code`, which names the failure the same way the API does (`not_found`,
|
|
3
|
+
* `revision_conflict`, `rate_limited`, …); `instanceof` against a subclass tells only where it came from.
|
|
4
|
+
*/
|
|
5
|
+
export declare class SuperclerkError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
constructor(code: string, message: string, options?: {
|
|
9
|
+
cause?: unknown;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** One field the request got wrong, by its path in the request. */
|
|
13
|
+
export interface FieldError {
|
|
14
|
+
path: string[];
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
/** The API answered with a status that isn't 2xx. */
|
|
18
|
+
export declare class APIError extends SuperclerkError {
|
|
19
|
+
/** The answer's body: the error object, or its text when it wasn't JSON. */
|
|
20
|
+
readonly body: unknown;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly status: number;
|
|
23
|
+
readonly headers: Headers;
|
|
24
|
+
/** The request's ID, for support: the `x-superclerk-request-id` header. */
|
|
25
|
+
readonly requestID: string | null;
|
|
26
|
+
readonly fields: FieldError[] | undefined;
|
|
27
|
+
private constructor();
|
|
28
|
+
/** The error an answer makes of its status and body: the API's error object, or the text when it isn't JSON. */
|
|
29
|
+
static from(response: Response, text: string): APIError;
|
|
30
|
+
}
|
|
31
|
+
/** No whole answer came: the network failed, or no answer started within the attempt's timeout. */
|
|
32
|
+
export declare class APIConnectionError extends SuperclerkError {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
constructor(code: "connection_failed" | "timeout", message: string, options?: {
|
|
35
|
+
cause?: unknown;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** A webhook's signature didn't verify, so its body must not be trusted. */
|
|
39
|
+
export declare class WebhookVerificationError extends SuperclerkError {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
|
43
|
+
/** A tool run that `tools.run` started ended failed or cancelled. `code` is the run's `error.kind`. */
|
|
44
|
+
export declare class ToolRunError extends SuperclerkError {
|
|
45
|
+
readonly run: {
|
|
46
|
+
id: string;
|
|
47
|
+
status: string;
|
|
48
|
+
error: {
|
|
49
|
+
kind: string;
|
|
50
|
+
message?: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
readonly name: string;
|
|
54
|
+
constructor(run: {
|
|
55
|
+
id: string;
|
|
56
|
+
status: string;
|
|
57
|
+
error: {
|
|
58
|
+
kind: string;
|
|
59
|
+
message?: string;
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
package/core/errors.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything the SDK throws. Branch on `code`, which names the failure the same way the API does (`not_found`,
|
|
3
|
+
* `revision_conflict`, `rate_limited`, …); `instanceof` against a subclass tells only where it came from.
|
|
4
|
+
*/
|
|
5
|
+
export class SuperclerkError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
name = "SuperclerkError";
|
|
8
|
+
constructor(code, message, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** The API answered with a status that isn't 2xx. */
|
|
14
|
+
export class APIError extends SuperclerkError {
|
|
15
|
+
body;
|
|
16
|
+
name = "APIError";
|
|
17
|
+
status;
|
|
18
|
+
headers;
|
|
19
|
+
/** The request's ID, for support: the `x-superclerk-request-id` header. */
|
|
20
|
+
requestID;
|
|
21
|
+
fields;
|
|
22
|
+
constructor(response,
|
|
23
|
+
/** The answer's body: the error object, or its text when it wasn't JSON. */
|
|
24
|
+
body) {
|
|
25
|
+
const error = body?.error;
|
|
26
|
+
// An answer that isn't the API's, such as a proxy's HTML page, still says what happened by its status.
|
|
27
|
+
const message = error?.message ?? `${response.status} ${response.statusText}`.trim();
|
|
28
|
+
// The wrong fields go in the message too: an uncaught error prints its message, but a field's path only as [Array].
|
|
29
|
+
const wrong = error?.fields?.map((field) => (field.path.length ? `${field.path.join(".")}: ` : "") + field.message);
|
|
30
|
+
super(error?.code ?? error?.kind ?? `http_${response.status}`, wrong?.length ? `${message} (${wrong.join("; ")})` : message);
|
|
31
|
+
this.body = body;
|
|
32
|
+
this.status = response.status;
|
|
33
|
+
this.headers = response.headers;
|
|
34
|
+
this.requestID = response.headers.get("x-superclerk-request-id");
|
|
35
|
+
this.fields = error?.fields;
|
|
36
|
+
}
|
|
37
|
+
/** The error an answer makes of its status and body: the API's error object, or the text when it isn't JSON. */
|
|
38
|
+
static from(response, text) {
|
|
39
|
+
return new APIError(response, parseJSON(text) ?? text);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** No whole answer came: the network failed, or no answer started within the attempt's timeout. */
|
|
43
|
+
export class APIConnectionError extends SuperclerkError {
|
|
44
|
+
name = "APIConnectionError";
|
|
45
|
+
constructor(code, message, options) {
|
|
46
|
+
super(code, message, options);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** A webhook's signature didn't verify, so its body must not be trusted. */
|
|
50
|
+
export class WebhookVerificationError extends SuperclerkError {
|
|
51
|
+
name = "WebhookVerificationError";
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super("invalid_webhook_signature", message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** A tool run that `tools.run` started ended failed or cancelled. `code` is the run's `error.kind`. */
|
|
57
|
+
export class ToolRunError extends SuperclerkError {
|
|
58
|
+
run;
|
|
59
|
+
name = "ToolRunError";
|
|
60
|
+
constructor(run) {
|
|
61
|
+
super(run.error.kind, run.error.message ?? `Tool run ${run.id} ended ${run.status}`);
|
|
62
|
+
this.run = run;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function parseJSON(text) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(text);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { APIPromise } from "./api-promise.js";
|
|
2
|
+
import type { Core, Operation, RequestOptions } from "./client.js";
|
|
3
|
+
/** A list's answer: its items, where the next page starts, and any fields beside them. */
|
|
4
|
+
export type PageBody = {
|
|
5
|
+
items: unknown[];
|
|
6
|
+
nextCursor: string | null;
|
|
7
|
+
};
|
|
8
|
+
/** One page of a list. Iterate it for its items and every later page's. */
|
|
9
|
+
export declare class Page<Body extends PageBody> implements AsyncIterable<Body["items"][number]> {
|
|
10
|
+
private readonly client;
|
|
11
|
+
private readonly operation;
|
|
12
|
+
private readonly options;
|
|
13
|
+
/** The page's whole answer, with any fields beside its items, such as an opportunity list's `stageCounts`. */
|
|
14
|
+
readonly body: Body;
|
|
15
|
+
constructor(client: Core, operation: Operation, options: RequestOptions,
|
|
16
|
+
/** The page's whole answer, with any fields beside its items, such as an opportunity list's `stageCounts`. */
|
|
17
|
+
body: Body);
|
|
18
|
+
get items(): Body["items"][number][];
|
|
19
|
+
/** Where the next page starts; null on the last page. */
|
|
20
|
+
get nextCursor(): string | null;
|
|
21
|
+
hasNextPage(): boolean;
|
|
22
|
+
getNextPage(): PagePromise<Body>;
|
|
23
|
+
[Symbol.asyncIterator](): AsyncGenerator<Body["items"][number]>;
|
|
24
|
+
}
|
|
25
|
+
/** A list call: await it for the first page, or iterate it for every item across pages. */
|
|
26
|
+
export declare class PagePromise<Body extends PageBody> extends APIPromise<Page<Body>> implements AsyncIterable<Body["items"][number]> {
|
|
27
|
+
constructor(client: Core, operation: Operation, options: RequestOptions);
|
|
28
|
+
[Symbol.asyncIterator](): AsyncGenerator<Body["items"][number]>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { APIPromise, readBody } from "./api-promise.js";
|
|
2
|
+
import { SuperclerkError } from "./errors.js";
|
|
3
|
+
/** One page of a list. Iterate it for its items and every later page's. */
|
|
4
|
+
export class Page {
|
|
5
|
+
client;
|
|
6
|
+
operation;
|
|
7
|
+
options;
|
|
8
|
+
body;
|
|
9
|
+
constructor(client, operation, options,
|
|
10
|
+
/** The page's whole answer, with any fields beside its items, such as an opportunity list's `stageCounts`. */
|
|
11
|
+
body) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
this.operation = operation;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.body = body;
|
|
16
|
+
}
|
|
17
|
+
get items() {
|
|
18
|
+
return this.body.items;
|
|
19
|
+
}
|
|
20
|
+
/** Where the next page starts; null on the last page. */
|
|
21
|
+
get nextCursor() {
|
|
22
|
+
return this.body.nextCursor;
|
|
23
|
+
}
|
|
24
|
+
hasNextPage() {
|
|
25
|
+
return this.body.nextCursor !== null;
|
|
26
|
+
}
|
|
27
|
+
getNextPage() {
|
|
28
|
+
if (this.body.nextCursor === null)
|
|
29
|
+
throw new SuperclerkError("no_next_page", "This is the last page");
|
|
30
|
+
return this.client.list({ ...this.operation, query: { ...this.operation.query, cursor: this.body.nextCursor } }, this.options);
|
|
31
|
+
}
|
|
32
|
+
async *[Symbol.asyncIterator]() {
|
|
33
|
+
let page = this;
|
|
34
|
+
for (;;) {
|
|
35
|
+
yield* page.items;
|
|
36
|
+
if (!page.hasNextPage())
|
|
37
|
+
return;
|
|
38
|
+
page = await page.getNextPage();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** A list call: await it for the first page, or iterate it for every item across pages. */
|
|
43
|
+
export class PagePromise extends APIPromise {
|
|
44
|
+
constructor(client, operation, options) {
|
|
45
|
+
super(client.send(operation, options), async (response) => new Page(client, operation, options, await readBody(response.json(), options.signal)));
|
|
46
|
+
}
|
|
47
|
+
async *[Symbol.asyncIterator]() {
|
|
48
|
+
yield* await this;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/core/timing.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { APIPromise } from "./api-promise.js";
|
|
2
|
+
import type { Core, RequestOptions } from "./client.js";
|
|
3
|
+
/** Waits, unless the signal aborts first. */
|
|
4
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
5
|
+
/** How long a response asks to be waited for: `Retry-After` in seconds or as a date, at most a minute. */
|
|
6
|
+
export declare function retryAfterMs(response: Response): number | undefined;
|
|
7
|
+
/** Whether a proxy answer is the provider's own response, fresh or stored, rather than one of Superclerk's. */
|
|
8
|
+
export declare function answeredByProvider(response: Response): boolean;
|
|
9
|
+
/** Whether an answer says the work goes on: Superclerk's 202, not a provider's own through the proxy. */
|
|
10
|
+
export declare function isPending(response: Response): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Asks the API to hold a read until the work settles, for as long as one attempt may last, less five seconds for the
|
|
13
|
+
* answer to come back. The API holds at most 30 seconds whatever it's asked, and your own `Prefer` header wins.
|
|
14
|
+
*/
|
|
15
|
+
export declare function holding(client: Core, options: RequestOptions): RequestOptions;
|
|
16
|
+
/**
|
|
17
|
+
* Reads something that settles later, such as a tool run or a connect link, until it does. The API holds each read,
|
|
18
|
+
* and an answer that says the work goes on is let go unread. Aborting the signal stops the waiting, not the work.
|
|
19
|
+
*/
|
|
20
|
+
export declare function settle<T>(client: Core, read: (options: RequestOptions) => APIPromise<T>, options: RequestOptions): Promise<T>;
|
package/core/timing.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Waits, unless the signal aborts first. */
|
|
2
|
+
export function sleep(ms, signal) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
signal?.throwIfAborted();
|
|
5
|
+
const stop = () => {
|
|
6
|
+
clearTimeout(timer);
|
|
7
|
+
reject(signal.reason);
|
|
8
|
+
};
|
|
9
|
+
const timer = setTimeout(() => {
|
|
10
|
+
signal?.removeEventListener("abort", stop);
|
|
11
|
+
resolve();
|
|
12
|
+
}, ms);
|
|
13
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** How long a response asks to be waited for: `Retry-After` in seconds or as a date, at most a minute. */
|
|
17
|
+
export function retryAfterMs(response) {
|
|
18
|
+
const header = response.headers.get("retry-after") ?? "";
|
|
19
|
+
const ms = /^\d+$/.test(header) ? Number(header) * 1_000 : Date.parse(header) - Date.now();
|
|
20
|
+
// NaN, a missing or unreadable header, is never at least 0.
|
|
21
|
+
return ms >= 0 ? Math.min(ms, 60_000) : undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Whether a proxy answer is the provider's own response, fresh or stored, rather than one of Superclerk's. */
|
|
24
|
+
export function answeredByProvider(response) {
|
|
25
|
+
const source = response.headers.get("x-superclerk-response-source");
|
|
26
|
+
return source === "upstream" || source === "replay";
|
|
27
|
+
}
|
|
28
|
+
/** Whether an answer says the work goes on: Superclerk's 202, not a provider's own through the proxy. */
|
|
29
|
+
export function isPending(response) {
|
|
30
|
+
return response.status === 202 && !answeredByProvider(response);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Asks the API to hold a read until the work settles, for as long as one attempt may last, less five seconds for the
|
|
34
|
+
* answer to come back. The API holds at most 30 seconds whatever it's asked, and your own `Prefer` header wins.
|
|
35
|
+
*/
|
|
36
|
+
export function holding(client, options) {
|
|
37
|
+
const seconds = Math.max(Math.floor((options.timeout ?? client.timeout) / 1_000) - 5, 0);
|
|
38
|
+
return { ...options, headers: { Prefer: `wait=${seconds}`, ...options.headers } };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads something that settles later, such as a tool run or a connect link, until it does. The API holds each read,
|
|
42
|
+
* and an answer that says the work goes on is let go unread. Aborting the signal stops the waiting, not the work.
|
|
43
|
+
*/
|
|
44
|
+
export async function settle(client, read, options) {
|
|
45
|
+
const held = holding(client, options);
|
|
46
|
+
for (;;) {
|
|
47
|
+
const answer = read(held);
|
|
48
|
+
const response = await answer.asResponse();
|
|
49
|
+
if (!isPending(response))
|
|
50
|
+
return answer;
|
|
51
|
+
await response.body?.cancel();
|
|
52
|
+
await sleep(retryAfterMs(response) ?? 1_000, options.signal);
|
|
53
|
+
}
|
|
54
|
+
}
|