@runlot/api 0.1.0-rc.22 → 0.1.0-rc.221
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/dist/index.d.ts +1553 -125
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1969 -166
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +30331 -3514
- package/dist/schema.d.ts.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export const API_VERSION = "v1";
|
|
2
|
-
/**
|
|
2
|
+
/** The shared query for browsing. Empty values are not carried — the server reads "empty means default". */
|
|
3
|
+
/** base64url of the UTF-8 JSON, no padding — the `draft` parameter's encoding. */
|
|
4
|
+
export function base64urlJson(v) {
|
|
5
|
+
const bytes = new TextEncoder().encode(JSON.stringify(v));
|
|
6
|
+
let bin = "";
|
|
7
|
+
for (const b of bytes)
|
|
8
|
+
bin += String.fromCharCode(b);
|
|
9
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
10
|
+
}
|
|
3
11
|
function gitQuery(ref, path) {
|
|
4
12
|
const q = new URLSearchParams();
|
|
5
13
|
if (ref !== "")
|
|
@@ -8,7 +16,70 @@ function gitQuery(ref, path) {
|
|
|
8
16
|
q.set("path", path);
|
|
9
17
|
return q.size > 0 ? `?${q.toString()}` : "";
|
|
10
18
|
}
|
|
11
|
-
|
|
19
|
+
function obsProfileQS(q = {}) {
|
|
20
|
+
const p = new URLSearchParams();
|
|
21
|
+
for (const [k, v] of Object.entries(q)) {
|
|
22
|
+
if (v !== undefined && v !== "")
|
|
23
|
+
p.set(k, String(v));
|
|
24
|
+
}
|
|
25
|
+
const s = p.toString();
|
|
26
|
+
return s ? `?${s}` : "";
|
|
27
|
+
}
|
|
28
|
+
export const API_SCOPES = [
|
|
29
|
+
"project:read",
|
|
30
|
+
"deploy",
|
|
31
|
+
"db:read",
|
|
32
|
+
"db:write",
|
|
33
|
+
"repo:read",
|
|
34
|
+
"repo:write",
|
|
35
|
+
"org:admin",
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Fine-grained token permissions (docs/authorization.md §3.13): one level per
|
|
39
|
+
* category. A token holds these or the bundles in API_SCOPES, not both.
|
|
40
|
+
*/
|
|
41
|
+
export const FINE_PERMISSIONS = [
|
|
42
|
+
"metadata:read",
|
|
43
|
+
"deployments:read", "deployments:write", "projects:write",
|
|
44
|
+
"secrets:read", "secrets:write",
|
|
45
|
+
"settings:read", "settings:write",
|
|
46
|
+
"logs:read",
|
|
47
|
+
"data:read", "data:write",
|
|
48
|
+
"database:read", "database:write", "database:admin",
|
|
49
|
+
"errors:read", "errors:write", "errors:pii", "errors:admin",
|
|
50
|
+
"observability:read", "observability:pii", "observability:admin",
|
|
51
|
+
"app_auth:read", "app_auth:write",
|
|
52
|
+
"email:read", "email:write",
|
|
53
|
+
"integrations:read", "integrations:write",
|
|
54
|
+
"repository:read", "repository:write",
|
|
55
|
+
"org:read", "org:write",
|
|
56
|
+
];
|
|
57
|
+
export const BUILTIN_AUTH_PROVIDERS = ["github", "google"];
|
|
58
|
+
export const RESERVED_AUTH_PROVIDER_IDS = ["github", "google", "runlot"];
|
|
59
|
+
/** The id pattern for an OIDC provider (`^[a-z][a-z0-9-]{1,30}$`). */
|
|
60
|
+
export const AUTH_PROVIDER_ID = /^[a-z][a-z0-9-]{1,30}$/;
|
|
61
|
+
export const AUTH_RUNLOT_METHODS = ["password", "google", "github", "passkey"];
|
|
62
|
+
/**
|
|
63
|
+
* A purchase or renewal answers with the domain once it is paid and done, or with
|
|
64
|
+
* the charge while payment is still on its way (a checkout to pay in, or a charge
|
|
65
|
+
* settling). The charge is the one with a `chargeId`.
|
|
66
|
+
*/
|
|
67
|
+
export function isDomainCharge(v) {
|
|
68
|
+
return "chargeId" in v;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A status the edge in front of cp-public can produce on its own: 502–504, and
|
|
72
|
+
* Cloudflare's 520–527 (524 is its 100 s origin timeout). With no JSON body these
|
|
73
|
+
* are the edge's words, not the server's, and are raised as `edge_<status>`.
|
|
74
|
+
*/
|
|
75
|
+
export function isEdgeStatus(status) {
|
|
76
|
+
return (status >= 502 && status <= 504) || (status >= 520 && status <= 527);
|
|
77
|
+
}
|
|
78
|
+
/** True for the `edge_<status>` ApiError `isEdgeStatus` produces. */
|
|
79
|
+
export function isEdgeError(err) {
|
|
80
|
+
return err instanceof ApiError && err.code.startsWith("edge_");
|
|
81
|
+
}
|
|
82
|
+
/** The exception that carries the server's error envelope unchanged. */
|
|
12
83
|
export class ApiError extends Error {
|
|
13
84
|
status;
|
|
14
85
|
code;
|
|
@@ -23,13 +94,29 @@ export class ApiError extends Error {
|
|
|
23
94
|
}
|
|
24
95
|
}
|
|
25
96
|
/**
|
|
26
|
-
* cp-public
|
|
97
|
+
* The cp-public client.
|
|
27
98
|
*
|
|
28
|
-
* CLI
|
|
29
|
-
*
|
|
99
|
+
* The CLI and the dashboard use the same one. If each called fetch itself, their
|
|
100
|
+
* error handling would drift a little, and that difference shows up during an
|
|
101
|
+
* incident.
|
|
102
|
+
*/
|
|
103
|
+
/**
|
|
104
|
+
* A database's routes live under the org and take the database's name
|
|
105
|
+
* (docs/org-database.md §5.1). The database named after a project is that
|
|
106
|
+
* project's `env.db`, so callers that hold a project name pass it unchanged.
|
|
30
107
|
*/
|
|
31
108
|
function dbPath(org, name) {
|
|
32
|
-
return `/v1/orgs/${encodeURIComponent(org)}/
|
|
109
|
+
return `/v1/orgs/${encodeURIComponent(org)}/databases/${encodeURIComponent(name)}`;
|
|
110
|
+
}
|
|
111
|
+
function previewQuery(opts) {
|
|
112
|
+
const q = new URLSearchParams({ preview: opts.preview });
|
|
113
|
+
// Empty values are not carried: the server reads "absent" as its default, and
|
|
114
|
+
// `?db=` would be neither absent nor a member of the enum.
|
|
115
|
+
if (opts.db)
|
|
116
|
+
q.set("db", opts.db);
|
|
117
|
+
if (opts.from)
|
|
118
|
+
q.set("from", opts.from);
|
|
119
|
+
return q.toString();
|
|
33
120
|
}
|
|
34
121
|
export class Client {
|
|
35
122
|
opts;
|
|
@@ -42,7 +129,25 @@ export class Client {
|
|
|
42
129
|
h["Authorization"] = `Bearer ${this.opts.token}`;
|
|
43
130
|
return h;
|
|
44
131
|
}
|
|
45
|
-
|
|
132
|
+
/** Runs one attempt again under `opts.retry`, for a safe method only. */
|
|
133
|
+
async retrying(method, once) {
|
|
134
|
+
const r = this.opts.retry;
|
|
135
|
+
const safe = method === "GET" || method === "HEAD";
|
|
136
|
+
for (let attempt = 0;; attempt += 1) {
|
|
137
|
+
try {
|
|
138
|
+
return await once();
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (r === undefined || !safe || attempt >= r.attempts || !(isEdgeError(err) || r.transport(err)))
|
|
142
|
+
throw err;
|
|
143
|
+
await new Promise((done) => setTimeout(done, r.backoffMs * 2 ** attempt));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
request(method, path, init) {
|
|
148
|
+
return this.retrying(method, () => this.requestOnce(method, path, init));
|
|
149
|
+
}
|
|
150
|
+
async requestOnce(method, path, init) {
|
|
46
151
|
const res = await fetch(new URL(path, this.opts.baseUrl), {
|
|
47
152
|
...init,
|
|
48
153
|
method,
|
|
@@ -54,8 +159,21 @@ export class Client {
|
|
|
54
159
|
body = (await res.json());
|
|
55
160
|
}
|
|
56
161
|
catch {
|
|
57
|
-
//
|
|
58
|
-
//
|
|
162
|
+
// An error response that is not JSON (a 502 made by a proxy, for instance)
|
|
163
|
+
// is raised as the same exception — so the caller does not have to handle
|
|
164
|
+
// two shapes of failure.
|
|
165
|
+
}
|
|
166
|
+
if (body === undefined && isEdgeStatus(res.status)) {
|
|
167
|
+
// The edge replaced the 5xx body with its own HTML. The code and reason
|
|
168
|
+
// the server wrote are not here — so say that, rather than "unknown". On
|
|
169
|
+
// 2026-09-10 a registrar rejection came out this way as
|
|
170
|
+
// "unknown: POST … → 502".
|
|
171
|
+
body = {
|
|
172
|
+
error: {
|
|
173
|
+
code: "edge_" + res.status,
|
|
174
|
+
message: "The server returned an error (" + res.status + "), and the error details could not be read. Try again shortly.",
|
|
175
|
+
},
|
|
176
|
+
};
|
|
59
177
|
}
|
|
60
178
|
throw new ApiError(res.status, body, `${method} ${path} → ${res.status}`);
|
|
61
179
|
}
|
|
@@ -63,83 +181,1195 @@ export class Client {
|
|
|
63
181
|
return undefined;
|
|
64
182
|
return (await res.json());
|
|
65
183
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
184
|
+
/** A body that is not JSON (the raw mail). Error handling is the same as request. */
|
|
185
|
+
text(method, path) {
|
|
186
|
+
return this.retrying(method, () => this.textOnce(method, path));
|
|
187
|
+
}
|
|
188
|
+
async textOnce(method, path) {
|
|
189
|
+
const res = await fetch(new URL(path, this.opts.baseUrl), { method, headers: this.headers() });
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
let body;
|
|
192
|
+
try {
|
|
193
|
+
body = (await res.json());
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// The same reason as in request
|
|
197
|
+
}
|
|
198
|
+
throw new ApiError(res.status, body, `${method} ${path} → ${res.status}`);
|
|
199
|
+
}
|
|
200
|
+
return await res.text();
|
|
201
|
+
}
|
|
202
|
+
json(method, path, body) {
|
|
203
|
+
if (body === undefined)
|
|
204
|
+
return this.request(method, path);
|
|
205
|
+
return this.request(method, path, {
|
|
206
|
+
body: JSON.stringify(body),
|
|
207
|
+
headers: { "Content-Type": "application/json" },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
devLogin(subject) {
|
|
211
|
+
return this.json("POST", "/v1/auth/dev/login", { subject });
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Takes the sign-up and sends the confirmation mail. Neither a session nor a
|
|
215
|
+
* password is created here — the password is chosen in `verifyEmail` by
|
|
216
|
+
* whoever opens the mailbox.
|
|
217
|
+
*/
|
|
218
|
+
passwordSignUp(email, name) {
|
|
219
|
+
return this.json("POST", "/v1/auth/password/sign-up", {
|
|
220
|
+
email,
|
|
221
|
+
...(name === undefined ? {} : { name }),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
passwordSignIn(email, password) {
|
|
225
|
+
return this.json("POST", "/v1/auth/password/sign-in", { email, password });
|
|
226
|
+
}
|
|
227
|
+
/** The same accepted response comes back whether or not the email exists. */
|
|
228
|
+
passwordForgot(email) {
|
|
229
|
+
return this.json("POST", "/v1/auth/password/forgot", { email });
|
|
230
|
+
}
|
|
231
|
+
passwordReset(token, password) {
|
|
232
|
+
return this.json("POST", "/v1/auth/password/reset", { token, password });
|
|
233
|
+
}
|
|
234
|
+
/** Uses the confirmation link and sets the account's first password. */
|
|
235
|
+
verifyEmail(token, password) {
|
|
236
|
+
return this.json("POST", "/v1/auth/email/verify", { token, password });
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Finishes a sign-in that came back `{mfa:"totp"}`.
|
|
240
|
+
*
|
|
241
|
+
* Callable without a token: `pending` is the credential, and it is not a
|
|
242
|
+
* session — no other endpoint accepts it.
|
|
243
|
+
*/
|
|
244
|
+
completeTotpLogin(pending, factor) {
|
|
245
|
+
return this.json("POST", "/v1/auth/totp", { pending, ...factor });
|
|
246
|
+
}
|
|
247
|
+
/** This account's two-factor state, including which orgs require it. */
|
|
248
|
+
getTotp() {
|
|
249
|
+
return this.json("GET", "/v1/me/totp");
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Draws a secret to scan. It does **not** turn the factor on — `confirmTotp`
|
|
253
|
+
* does, after the app has proved it can produce a code.
|
|
254
|
+
*/
|
|
255
|
+
enrollTotp() {
|
|
256
|
+
return this.json("POST", "/v1/me/totp/enroll");
|
|
257
|
+
}
|
|
258
|
+
/** Turns the factor on and hands back the ten recovery codes. They are shown once. */
|
|
259
|
+
confirmTotp(code) {
|
|
260
|
+
return this.json("POST", "/v1/me/totp/confirm", { code });
|
|
261
|
+
}
|
|
262
|
+
/** Turns it off. A code or a recovery code is required — the session alone is not. */
|
|
263
|
+
disableTotp(factor) {
|
|
264
|
+
return this.json("DELETE", "/v1/me/totp", factor);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Answers "How did you hear about Runlot?" once. An empty answer dismisses it;
|
|
268
|
+
* a second call changes nothing (the first answer stands).
|
|
269
|
+
*/
|
|
270
|
+
answerHeardFrom(answer) {
|
|
271
|
+
return this.json("PUT", "/v1/me/heard-from", { answer });
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Gives or withdraws consent to advertising email. `granted: true` must come
|
|
275
|
+
* only from a box the person ticked (opt-in, docs/marketing-consent.md).
|
|
276
|
+
*/
|
|
277
|
+
setMarketingConsent(granted, source) {
|
|
278
|
+
return this.json("PUT", "/v1/me/marketing-consent", { granted, source });
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Withdraws advertising-email consent with the token from a mail's unsubscribe link.
|
|
282
|
+
* No session: withdrawal must not ask for a login (docs/marketing-consent.md).
|
|
283
|
+
*/
|
|
284
|
+
unsubscribeMarketing(token) {
|
|
285
|
+
return this.json("POST", "/v1/marketing/unsubscribe", { token });
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The GitHub OAuth start URL. It is a 302 the browser follows, so it goes into
|
|
289
|
+
* `window.location` rather than being called with fetch. `redirect` has to be
|
|
290
|
+
* on the server's allow-list.
|
|
291
|
+
*/
|
|
292
|
+
githubStartUrl(redirect) {
|
|
293
|
+
const u = new URL("/v1/auth/github/start", this.opts.baseUrl);
|
|
294
|
+
u.searchParams.set("redirect", redirect);
|
|
295
|
+
return u.toString();
|
|
296
|
+
}
|
|
297
|
+
/** The Google OAuth start URL. Uses the same dashboard callback shape as GitHub. */
|
|
298
|
+
googleStartUrl(redirect) {
|
|
299
|
+
const u = new URL("/v1/auth/google/start", this.opts.baseUrl);
|
|
300
|
+
u.searchParams.set("redirect", redirect);
|
|
301
|
+
return u.toString();
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The generic OIDC start URL (docs/onprem-delivery.md §7.7). Same 302-and-fragment
|
|
305
|
+
* shape as the other two; which provider is behind it is the installation's choice.
|
|
306
|
+
*/
|
|
307
|
+
oidcStartUrl(redirect) {
|
|
308
|
+
const u = new URL("/v1/auth/oidc/start", this.opts.baseUrl);
|
|
309
|
+
u.searchParams.set("redirect", redirect);
|
|
310
|
+
return u.toString();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Does this deployment federate an identity provider?
|
|
314
|
+
*
|
|
315
|
+
* **There is no endpoint that answers "which logins are on"** — one existed and was
|
|
316
|
+
* deleted, because its only reader was the login screen and it put a round trip in
|
|
317
|
+
* front of every login button (cp/internal/public/public.go). This is not that
|
|
318
|
+
* endpoint coming back: cp-public **does not register** the two OIDC routes when no
|
|
319
|
+
* issuer is configured, so the mux's own 404 is the answer, and the probe is a HEAD
|
|
320
|
+
* that no handler runs.
|
|
321
|
+
*
|
|
322
|
+
* Any other status means the route is there — a 400 (this probe carries no
|
|
323
|
+
* `redirect`) or a 502 (the provider's discovery document could not be read) both say
|
|
324
|
+
* the handler exists, and a button that leads to a readable error beats no button.
|
|
325
|
+
* A network failure answers false: on SaaS, where the answer is false anyway, a dead
|
|
326
|
+
* API address must not draw a login that cannot work.
|
|
327
|
+
*/
|
|
328
|
+
async oidcAvailable() {
|
|
329
|
+
try {
|
|
330
|
+
const res = await fetch(new URL("/v1/auth/oidc/start", this.opts.baseUrl), { method: "HEAD" });
|
|
331
|
+
return res.status !== 404;
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* This installation's licence (docs/onprem-delivery.md §6.5). Readable by any
|
|
339
|
+
* signed-in user: the footer it feeds is on every page for everybody.
|
|
340
|
+
*
|
|
341
|
+
* On SaaS it answers `onPrem: false` rather than 404, so the caller has one shape to
|
|
342
|
+
* read and the surfaces draw nothing on the flag.
|
|
343
|
+
*/
|
|
344
|
+
license() {
|
|
345
|
+
return this.json("GET", "/v1/license");
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The names of this deployment (app domain, docs address). Callable without auth.
|
|
349
|
+
*
|
|
350
|
+
* This exists so the dashboard does not hard-code a domain into the screen
|
|
351
|
+
* (docs/environments.md §3.2) — hard-coded, the dev screen tells people a
|
|
352
|
+
* production address, and because that address opens, nobody ever says it is
|
|
353
|
+
* wrong.
|
|
354
|
+
*/
|
|
355
|
+
siteConfig() {
|
|
356
|
+
return this.json("GET", "/v1/config");
|
|
357
|
+
}
|
|
358
|
+
me() {
|
|
359
|
+
return this.json("GET", "/v1/me");
|
|
360
|
+
}
|
|
361
|
+
listOrgs() {
|
|
362
|
+
return this.json("GET", "/v1/orgs");
|
|
363
|
+
}
|
|
364
|
+
/** Changes the display name only (admin). The slug is the name that becomes an address, so it is not on this surface. */
|
|
365
|
+
/**
|
|
366
|
+
* `requireTotp` is left out when it is not given, on purpose: the server reads
|
|
367
|
+
* an absent field as "leave it alone". Sending `false` by default would mean a
|
|
368
|
+
* rename silently switched the org's second-factor requirement off.
|
|
369
|
+
*/
|
|
370
|
+
updateOrg(org, name, requireTotp) {
|
|
371
|
+
return this.json("PATCH", `/v1/orgs/${encodeURIComponent(org)}`, {
|
|
372
|
+
name,
|
|
373
|
+
...(requireTotp === undefined ? {} : { requireTotp }),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Deletes the organization (admin). **This cannot be undone.**
|
|
378
|
+
*
|
|
379
|
+
* `confirm` has to be the organization slug exactly — the confirmation lives on
|
|
380
|
+
* the server because three places call this surface (the screen, the CLI, and
|
|
381
|
+
* scripts). If a live project, domain, repository, or DNS zone is left, it is a
|
|
382
|
+
* 409 `org_not_empty` and `details.holdings` says what is left — **projects
|
|
383
|
+
* already deleted are not counted.**
|
|
384
|
+
*
|
|
385
|
+
* The `purgeAfter` in the answer is when the rows actually disappear. By the
|
|
386
|
+
* moment it returns the organization is already gone from every surface; what
|
|
387
|
+
* remains is retention until that time.
|
|
388
|
+
*/
|
|
389
|
+
deleteOrg(org, confirm) {
|
|
390
|
+
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}`, { confirm });
|
|
391
|
+
}
|
|
392
|
+
createOrg(slug, name) {
|
|
393
|
+
return this.json("POST", "/v1/orgs", { slug, name });
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* The organization's recent notifications (docs/limits.md §3.4).
|
|
397
|
+
*
|
|
398
|
+
* A row with no `sentAt` is either still queued or has no email address to
|
|
399
|
+
* reach — and for the second, this list is the only channel, so the screen
|
|
400
|
+
* draws both states the same way.
|
|
401
|
+
*/
|
|
402
|
+
listNotifications(org, limit) {
|
|
403
|
+
const q = limit === undefined ? "" : `?limit=${limit}`;
|
|
404
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/notifications${q}`);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The plan and billing state (docs/billing.md). If `enabled` is false, this
|
|
408
|
+
* deployment has no billing — the screen shows only a contact address.
|
|
409
|
+
*/
|
|
410
|
+
getBilling(org) {
|
|
411
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/billing`);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* A Pro checkout session. Send the browser to the `url` that comes back. Even
|
|
415
|
+
* once payment finishes, the thing that changes the plan is the webhook, so
|
|
416
|
+
* read `getBilling` again after the browser returns.
|
|
417
|
+
*/
|
|
418
|
+
createBillingCheckout(org, returnTo) {
|
|
419
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/checkout`, { returnTo });
|
|
420
|
+
}
|
|
421
|
+
/** A one-time `url` for the customer portal (payment method, receipts, cancellation). */
|
|
422
|
+
createBillingPortal(org, returnTo) {
|
|
423
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/portal`, { returnTo });
|
|
424
|
+
}
|
|
425
|
+
/** The customer session token for the payment-method embed (1 hour). An org with no payment history gets a 404. */
|
|
426
|
+
createBillingSession(org) {
|
|
427
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/session`, {});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* The org's saved cards, read live from Polar (admin). `customer: false` means the org has
|
|
431
|
+
* no Polar billing account yet — the first checkout creates it. Nothing here is stored by runlot.
|
|
432
|
+
*/
|
|
433
|
+
getBillingPaymentMethods(org) {
|
|
434
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/billing/payment-methods`);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* A one-time `url` for Polar's hosted page where a card is added (admin). The browser comes back
|
|
438
|
+
* to `returnTo`. An org with no Polar billing account gets one made first.
|
|
439
|
+
*/
|
|
440
|
+
addBillingPaymentMethod(org, returnTo) {
|
|
441
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/payment-methods/add`, { returnTo });
|
|
442
|
+
}
|
|
443
|
+
/** Makes a saved card Polar's default — the card automatic charges use (admin). */
|
|
444
|
+
setBillingDefaultPaymentMethod(org, id) {
|
|
445
|
+
return this.json("PUT", `/v1/orgs/${encodeURIComponent(org)}/billing/payment-methods/default`, { id });
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Removes a saved card (admin). Pro keeps a valid card (409 `replacement_required`), and so does
|
|
449
|
+
* auto top-up while it is on (409 `autotopup_needs_card`). Removing the default promotes another
|
|
450
|
+
* valid card first (`promotedDefault`).
|
|
451
|
+
*/
|
|
452
|
+
removeBillingPaymentMethod(org, id) {
|
|
453
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/payment-methods/remove`, { id });
|
|
454
|
+
}
|
|
455
|
+
/** Schedules cancellation at the end of the period (`cancel: true`), or undoes it. There is no immediate revocation. */
|
|
456
|
+
cancelBilling(org, body) {
|
|
457
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/cancel`, body);
|
|
458
|
+
}
|
|
459
|
+
/** The org's spending limit, this month's overage against it and the lock (docs/customer-limits.md §3). */
|
|
460
|
+
getSpendingLimit(org) {
|
|
461
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/billing/spending-limit`);
|
|
462
|
+
}
|
|
463
|
+
/** Sets the spending limit in cents (admin). A value below this month's overage locks the org at once. */
|
|
464
|
+
setSpendingLimit(org, cents) {
|
|
465
|
+
return this.json("PUT", `/v1/orgs/${encodeURIComponent(org)}/billing/spending-limit`, { cents });
|
|
466
|
+
}
|
|
467
|
+
/** A project's app memory, per-request CPU and spending limits (docs/customer-limits.md §4, §12). */
|
|
468
|
+
getProjectLimits(org, name) {
|
|
469
|
+
return this.json("GET", `${this.projectPath(org, name)}/limits`);
|
|
470
|
+
}
|
|
471
|
+
/** A field left out is unchanged; null puts it back on the plan's value, or removes the project's spending limit (admin). */
|
|
472
|
+
setProjectLimits(org, name, body) {
|
|
473
|
+
return this.json("PUT", `${this.projectPath(org, name)}/limits`, body);
|
|
474
|
+
}
|
|
475
|
+
listProjects(org) {
|
|
476
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/projects`);
|
|
477
|
+
}
|
|
478
|
+
createProject(org, name) {
|
|
479
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/projects`, { name });
|
|
480
|
+
}
|
|
481
|
+
getProject(org, name) {
|
|
482
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}`);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Renames the project (admin). **The default address moves with it and there is
|
|
486
|
+
* no alias** — which is why the response carries the new address. Unlike the
|
|
487
|
+
* org slug, this name can change because it is the inner label of the org
|
|
488
|
+
* wildcard certificate.
|
|
489
|
+
*/
|
|
490
|
+
updateProject(org, name, next) {
|
|
491
|
+
return this.json("PATCH", this.projectPath(org, name), { name: next });
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Starts tearing the project down (admin). **This cannot be undone and leaves
|
|
495
|
+
* no recovery generation.** `confirm` has to be the project name exactly.
|
|
496
|
+
*/
|
|
497
|
+
deleteProject(org, name, confirm) {
|
|
498
|
+
// A project's own database stays in the org (`databaseKept` on the answer).
|
|
499
|
+
return this.json("DELETE", this.projectPath(org, name), { confirm });
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Disables a project (admin): it stops serving and taking new backups, and
|
|
503
|
+
* keeps everything it has. A project disabled for a year is deleted for good.
|
|
504
|
+
*/
|
|
505
|
+
disableProject(org, name) {
|
|
506
|
+
return this.json("POST", `${this.projectPath(org, name)}/disable`);
|
|
507
|
+
}
|
|
508
|
+
/** Enables a disabled project (admin). 409 `project_limit` when there is no room for it. */
|
|
509
|
+
enableProject(org, name) {
|
|
510
|
+
return this.json("POST", `${this.projectPath(org, name)}/enable`);
|
|
511
|
+
}
|
|
512
|
+
/** Watches the teardown operation to its end even after the project is gone. */
|
|
513
|
+
getOrgOperation(org, opId) {
|
|
514
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/operations/${encodeURIComponent(opId)}`);
|
|
515
|
+
}
|
|
516
|
+
/** Everything the project's first screen takes in one call. Usage calls `getUsage` alongside it. */
|
|
517
|
+
getProjectOverview(org, name) {
|
|
518
|
+
return this.json("GET", `${this.projectPath(org, name)}/overview`);
|
|
519
|
+
}
|
|
520
|
+
listDeployments(org, name) {
|
|
521
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/deployments`);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* One deployment, with `migrations` when the control plane applied its
|
|
525
|
+
* `migrations/` — which today means a preview deployed with `db=branch`
|
|
526
|
+
* (docs/studio.md §6.6 step 2).
|
|
527
|
+
*
|
|
528
|
+
* A preview's deployment is read under **the parent project's name**, the name
|
|
529
|
+
* the caller deployed through: the preview is a project of its own, but the
|
|
530
|
+
* caller never learns its name.
|
|
531
|
+
*/
|
|
532
|
+
getDeployment(org, name, deploymentId) {
|
|
533
|
+
return this.json("GET", `${this.projectPath(org, name)}/deployments/${encodeURIComponent(deploymentId)}`);
|
|
534
|
+
}
|
|
535
|
+
/** Completed-front HTTP outcomes only; native crash-free health is unknown. */
|
|
536
|
+
getReleaseHealth(org, name) {
|
|
537
|
+
return this.json("GET", `${this.projectPath(org, name)}/error-releases`);
|
|
538
|
+
}
|
|
539
|
+
/** Historical first transition per issue; never substitutes a latest occurrence. */
|
|
540
|
+
listReleaseIssues(org, name, version, opts) {
|
|
541
|
+
const q = new URLSearchParams({ kind: opts.kind, windowStart: opts.windowStart, windowEnd: opts.windowEnd });
|
|
542
|
+
if (opts.cursor !== undefined)
|
|
543
|
+
q.set("cursor", opts.cursor);
|
|
544
|
+
return this.json("GET", `${this.projectPath(org, name)}/error-releases/${encodeURIComponent(String(version))}/issues?${q}`);
|
|
545
|
+
}
|
|
546
|
+
/** Runlot Errors: the issue stream (docs/customer-error-tracking.md). */
|
|
547
|
+
listErrorIssues(org, name, opts = {}) {
|
|
548
|
+
const q = new URLSearchParams();
|
|
549
|
+
if (opts.status)
|
|
550
|
+
q.set("status", opts.status);
|
|
551
|
+
if (opts.query)
|
|
552
|
+
q.set("query", opts.query);
|
|
553
|
+
if (opts.cursor)
|
|
554
|
+
q.set("cursor", opts.cursor);
|
|
555
|
+
if (opts.limit !== undefined)
|
|
556
|
+
q.set("limit", String(opts.limit));
|
|
557
|
+
const qs = q.size > 0 ? `?${q}` : "";
|
|
558
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors${qs}`);
|
|
559
|
+
}
|
|
560
|
+
getErrorIssue(org, name, issueId) {
|
|
561
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}`);
|
|
562
|
+
}
|
|
563
|
+
updateErrorIssue(org, name, issueId, update) {
|
|
564
|
+
return this.json("PATCH", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}`, update);
|
|
565
|
+
}
|
|
566
|
+
/** Source context, suspects and owners for the selected stored occurrence, or latest when omitted. */
|
|
567
|
+
async getErrorInsights(org, name, issueId, opts = {}) {
|
|
568
|
+
const eventId = opts.eventId;
|
|
569
|
+
const q = new URLSearchParams();
|
|
570
|
+
if (eventId !== undefined)
|
|
571
|
+
q.set("eventId", eventId);
|
|
572
|
+
const qs = q.size > 0 ? `?${q}` : "";
|
|
573
|
+
const out = await this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/insights${qs}`);
|
|
574
|
+
if (eventId !== undefined && (eventId === "" || out.eventId !== eventId)) {
|
|
575
|
+
throw new Error("Code insights did not identify the requested occurrence; this server may not support event selection.");
|
|
576
|
+
}
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
listErrorEvents(org, name, issueId, opts = {}) {
|
|
580
|
+
const q = new URLSearchParams();
|
|
581
|
+
if (opts.cursor)
|
|
582
|
+
q.set("cursor", opts.cursor);
|
|
583
|
+
if (opts.limit !== undefined)
|
|
584
|
+
q.set("limit", String(opts.limit));
|
|
585
|
+
const qs = q.size > 0 ? `?${q}` : "";
|
|
586
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/events${qs}`);
|
|
587
|
+
}
|
|
588
|
+
getErrorEvent(org, name, issueId, eventId) {
|
|
589
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/events/${encodeURIComponent(eventId)}`);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Other events of the project joined to this one by front's request ID or a
|
|
593
|
+
* browser-minted trace ID (§12). Correlation only: issues are never merged.
|
|
594
|
+
*/
|
|
595
|
+
getRelatedErrorEvents(org, name, issueId, eventId) {
|
|
596
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/events/${encodeURIComponent(eventId)}/related`);
|
|
597
|
+
}
|
|
598
|
+
/** An issue's hourly history from the ClickHouse store; `available: false` where there is none. */
|
|
599
|
+
getErrorHistory(org, name, issueId, days) {
|
|
600
|
+
const qs = days !== undefined ? `?days=${encodeURIComponent(String(days))}` : "";
|
|
601
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/history${qs}`);
|
|
602
|
+
}
|
|
603
|
+
/** Assign an issue to an org member, or clear it with null. */
|
|
604
|
+
assignErrorIssue(org, name, issueId, userId) {
|
|
605
|
+
return this.json("PUT", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/assignee`, { userId });
|
|
606
|
+
}
|
|
607
|
+
listErrorComments(org, name, issueId) {
|
|
608
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/comments`);
|
|
609
|
+
}
|
|
610
|
+
/** Plain text, at most 4000 characters; the author is the signed-in user. */
|
|
611
|
+
addErrorComment(org, name, issueId, body) {
|
|
612
|
+
return this.json("POST", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/comments`, { body });
|
|
613
|
+
}
|
|
614
|
+
/** Merge up to ten issues of the same origin into `targetIssueId`. */
|
|
615
|
+
mergeErrorIssues(org, name, targetIssueId, sourceIssueIds) {
|
|
616
|
+
return this.json("POST", `${this.projectPath(org, name)}/errors/${encodeURIComponent(targetIssueId)}/merge`, {
|
|
617
|
+
sourceIssueIds,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/** Take a merged source back out of the issue it was merged into. */
|
|
621
|
+
unmergeErrorIssue(org, name, issueId) {
|
|
622
|
+
return this.json("POST", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/unmerge`);
|
|
623
|
+
}
|
|
624
|
+
/** Shared stack hints: frame overlap, never a merge or a causal claim. */
|
|
625
|
+
similarErrorIssues(org, name, issueId) {
|
|
626
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/similar`);
|
|
627
|
+
}
|
|
628
|
+
getErrorFingerprintRules(org, name) {
|
|
629
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors-fingerprint-rules`);
|
|
630
|
+
}
|
|
631
|
+
putErrorFingerprintRules(org, name, rules) {
|
|
632
|
+
return this.json("PUT", `${this.projectPath(org, name)}/errors-fingerprint-rules`, { rules });
|
|
633
|
+
}
|
|
634
|
+
getErrorAlerts(org, name) { return this.json("GET", `${this.projectPath(org, name)}/error-alerts`); }
|
|
635
|
+
putErrorAlertRule(org, name, id, rule) { return this.json("PUT", `${this.projectPath(org, name)}/error-alerts/rules/${encodeURIComponent(id)}`, rule); }
|
|
636
|
+
deleteErrorAlertRule(org, name, id) { return this.json("DELETE", `${this.projectPath(org, name)}/error-alerts/rules/${encodeURIComponent(id)}`); }
|
|
637
|
+
putErrorAlertDestination(org, name, id, input) { return this.json("PUT", `${this.projectPath(org, name)}/error-alerts/destinations/${encodeURIComponent(id)}`, input); }
|
|
638
|
+
deleteErrorAlertDestination(org, name, id) { return this.json("DELETE", `${this.projectPath(org, name)}/error-alerts/destinations/${encodeURIComponent(id)}`); }
|
|
639
|
+
getErrorAlertMail(org, name) { return this.json("GET", `${this.projectPath(org, name)}/error-alerts/mail`); }
|
|
640
|
+
putErrorAlertMail(org, name, preference) { return this.json("PUT", `${this.projectPath(org, name)}/error-alerts/mail`, preference); }
|
|
641
|
+
listErrorAlertDeliveries(org, name) { return this.json("GET", `${this.projectPath(org, name)}/error-alerts/deliveries`); }
|
|
642
|
+
// Monitors, downtimes and SLOs (docs/customer-observability.md §17.8, §17.9).
|
|
643
|
+
// Destinations are the error-alert destinations above, shared by both.
|
|
644
|
+
listMonitors(org, name) { return this.json("GET", `${this.projectPath(org, name)}/observability/monitors`); }
|
|
645
|
+
exportMonitors(org, name) { return this.json("GET", `${this.projectPath(org, name)}/observability/monitors/export`); }
|
|
646
|
+
getMonitor(org, name, id) { return this.json("GET", `${this.projectPath(org, name)}/observability/monitors/${encodeURIComponent(id)}`); }
|
|
647
|
+
putMonitor(org, name, id, monitor) { return this.json("PUT", `${this.projectPath(org, name)}/observability/monitors/${encodeURIComponent(id)}`, monitor); }
|
|
648
|
+
deleteMonitor(org, name, id) { return this.json("DELETE", `${this.projectPath(org, name)}/observability/monitors/${encodeURIComponent(id)}`); }
|
|
649
|
+
getMonitorHistory(org, name, id, limit) {
|
|
650
|
+
const q = limit ? `?limit=${encodeURIComponent(String(limit))}` : "";
|
|
651
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/monitors/${encodeURIComponent(id)}/history${q}`);
|
|
652
|
+
}
|
|
653
|
+
testMonitor(org, name, id) { return this.json("POST", `${this.projectPath(org, name)}/observability/monitors/${encodeURIComponent(id)}/test`); }
|
|
654
|
+
listDowntimes(org, name) { return this.json("GET", `${this.projectPath(org, name)}/observability/downtimes`); }
|
|
655
|
+
putDowntime(org, name, id, downtime) { return this.json("PUT", `${this.projectPath(org, name)}/observability/downtimes/${encodeURIComponent(id)}`, downtime); }
|
|
656
|
+
deleteDowntime(org, name, id) { return this.json("DELETE", `${this.projectPath(org, name)}/observability/downtimes/${encodeURIComponent(id)}`); }
|
|
657
|
+
listSLOs(org, name) { return this.json("GET", `${this.projectPath(org, name)}/observability/slos`); }
|
|
658
|
+
getSLO(org, name, id) { return this.json("GET", `${this.projectPath(org, name)}/observability/slos/${encodeURIComponent(id)}`); }
|
|
659
|
+
putSLO(org, name, id, slo) { return this.json("PUT", `${this.projectPath(org, name)}/observability/slos/${encodeURIComponent(id)}`, slo); }
|
|
660
|
+
deleteSLO(org, name, id) { return this.json("DELETE", `${this.projectPath(org, name)}/observability/slos/${encodeURIComponent(id)}`); }
|
|
661
|
+
/**
|
|
662
|
+
* Observability access and governance (customer-observability.md §12, §16, §17.15):
|
|
663
|
+
* the caller's grants, per-project role overrides, the audit trail, the data page,
|
|
664
|
+
* caps, export destinations and rehydration.
|
|
665
|
+
*/
|
|
666
|
+
getObservabilityPermissions(org, name) {
|
|
667
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/permissions`);
|
|
668
|
+
}
|
|
669
|
+
listObservabilityRoles(org, name) {
|
|
670
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/roles`);
|
|
671
|
+
}
|
|
672
|
+
putObservabilityRole(org, name, userId, grants) {
|
|
673
|
+
return this.json("PUT", `${this.projectPath(org, name)}/observability/roles/${encodeURIComponent(userId)}`, { grants });
|
|
674
|
+
}
|
|
675
|
+
deleteObservabilityRole(org, name, userId) {
|
|
676
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/observability/roles/${encodeURIComponent(userId)}`);
|
|
677
|
+
}
|
|
678
|
+
listObservabilityAudit(org, name, opts = {}) {
|
|
679
|
+
const q = new URLSearchParams();
|
|
680
|
+
if (opts.limit !== undefined)
|
|
681
|
+
q.set("limit", String(opts.limit));
|
|
682
|
+
if (opts.before !== undefined)
|
|
683
|
+
q.set("before", opts.before);
|
|
684
|
+
const qs = q.size > 0 ? `?${q.toString()}` : "";
|
|
685
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/audit${qs}`);
|
|
686
|
+
}
|
|
687
|
+
getObservabilityData(org, name) {
|
|
688
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/data`);
|
|
689
|
+
}
|
|
690
|
+
getObservabilityCaps(org, name) {
|
|
691
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/caps`);
|
|
692
|
+
}
|
|
693
|
+
/** Named meters only: a number, -1 (no cap, Pro) or null (the plan's included amount). */
|
|
694
|
+
putObservabilityCaps(org, name, caps) {
|
|
695
|
+
return this.json("PUT", `${this.projectPath(org, name)}/observability/caps`, caps);
|
|
696
|
+
}
|
|
697
|
+
listObservabilityExports(org, name) {
|
|
698
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/exports`);
|
|
699
|
+
}
|
|
700
|
+
getObservabilityExport(org, name, exportId) {
|
|
701
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/exports/${encodeURIComponent(exportId)}`);
|
|
702
|
+
}
|
|
703
|
+
createObservabilityExport(org, name, input) {
|
|
704
|
+
return this.json("POST", `${this.projectPath(org, name)}/observability/exports`, input);
|
|
705
|
+
}
|
|
706
|
+
updateObservabilityExport(org, name, exportId, input) {
|
|
707
|
+
return this.json("PATCH", `${this.projectPath(org, name)}/observability/exports/${encodeURIComponent(exportId)}`, input);
|
|
708
|
+
}
|
|
709
|
+
deleteObservabilityExport(org, name, exportId) {
|
|
710
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/observability/exports/${encodeURIComponent(exportId)}`);
|
|
711
|
+
}
|
|
712
|
+
listObservabilityRehydrations(org, name) {
|
|
713
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/rehydrations`);
|
|
714
|
+
}
|
|
715
|
+
getObservabilityRehydration(org, name, jobId) {
|
|
716
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/rehydrations/${encodeURIComponent(jobId)}`);
|
|
717
|
+
}
|
|
718
|
+
/** Loads an archived range back for seven days; the answer is the queued job. */
|
|
719
|
+
createObservabilityRehydration(org, name, input) {
|
|
720
|
+
return this.json("POST", `${this.projectPath(org, name)}/observability/rehydrations`, input);
|
|
721
|
+
}
|
|
722
|
+
/** `runlot bug-report` (customer-observability.md §14): the CP snapshots what the references name. */
|
|
723
|
+
createBugReport(org, input) {
|
|
724
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/bug-reports`, input);
|
|
725
|
+
}
|
|
726
|
+
listBugReports(org) {
|
|
727
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/bug-reports`);
|
|
728
|
+
}
|
|
729
|
+
getBugReport(org, reportId, opts = {}) {
|
|
730
|
+
const qs = opts.snapshot === false ? "?snapshot=0" : "";
|
|
731
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/bug-reports/${encodeURIComponent(reportId)}${qs}`);
|
|
732
|
+
}
|
|
733
|
+
deleteBugReport(org, reportId) {
|
|
734
|
+
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}/bug-reports/${encodeURIComponent(reportId)}`);
|
|
735
|
+
}
|
|
736
|
+
/** Synthetic HTTP checks of the project's own addresses (customer-observability.md §17.11). */
|
|
737
|
+
listUptimeChecks(org, name) {
|
|
738
|
+
return this.json("GET", `${this.projectPath(org, name)}/uptime-checks`);
|
|
739
|
+
}
|
|
740
|
+
putUptimeCheck(org, name, id, input) {
|
|
741
|
+
return this.json("PUT", `${this.projectPath(org, name)}/uptime-checks/${encodeURIComponent(id)}`, input);
|
|
742
|
+
}
|
|
743
|
+
deleteUptimeCheck(org, name, id) {
|
|
744
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/uptime-checks/${encodeURIComponent(id)}`);
|
|
745
|
+
}
|
|
746
|
+
listUptimeRuns(org, name, id, limit) {
|
|
747
|
+
const qs = limit !== undefined ? `?limit=${encodeURIComponent(String(limit))}` : "";
|
|
748
|
+
return this.json("GET", `${this.projectPath(org, name)}/uptime-checks/${encodeURIComponent(id)}/runs${qs}`);
|
|
749
|
+
}
|
|
750
|
+
/** Dashboards (customer-observability.md §17.7). A built-in's id is `builtin:<key>`. */
|
|
751
|
+
listDashboards(org, name) {
|
|
752
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/dashboards`);
|
|
753
|
+
}
|
|
754
|
+
createDashboard(org, name, input) {
|
|
755
|
+
return this.json("POST", `${this.projectPath(org, name)}/observability/dashboards`, input);
|
|
756
|
+
}
|
|
757
|
+
getDashboard(org, name, id) {
|
|
758
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/dashboards/${encodeURIComponent(id)}`);
|
|
759
|
+
}
|
|
760
|
+
putDashboard(org, name, id, input) {
|
|
761
|
+
return this.json("PUT", `${this.projectPath(org, name)}/observability/dashboards/${encodeURIComponent(id)}`, input);
|
|
762
|
+
}
|
|
763
|
+
deleteDashboard(org, name, id) {
|
|
764
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/observability/dashboards/${encodeURIComponent(id)}`);
|
|
765
|
+
}
|
|
766
|
+
/** The `.runlot/dashboards/<key>.json` form of a dashboard. */
|
|
767
|
+
exportDashboard(org, name, id) {
|
|
768
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/dashboards/${encodeURIComponent(id)}/export`);
|
|
769
|
+
}
|
|
770
|
+
/** Make (or rotate) the public link; the token is shown once. */
|
|
771
|
+
shareDashboard(org, name, id, isPublic = true) {
|
|
772
|
+
return this.json("POST", `${this.projectPath(org, name)}/observability/dashboards/${encodeURIComponent(id)}/share`, { public: isPublic });
|
|
773
|
+
}
|
|
774
|
+
unshareDashboard(org, name, id) {
|
|
775
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/observability/dashboards/${encodeURIComponent(id)}/share`);
|
|
776
|
+
}
|
|
777
|
+
getDashboardWidgetData(org, name, id, widgetId, req = {}) {
|
|
778
|
+
return this.json("POST", `${this.projectPath(org, name)}/observability/dashboards/${encodeURIComponent(id)}/widgets/${encodeURIComponent(widgetId)}/data`, req);
|
|
779
|
+
}
|
|
780
|
+
getDashboardSettings(org, name) {
|
|
781
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/dashboard-settings`);
|
|
782
|
+
}
|
|
783
|
+
putDashboardSettings(org, name, settings) {
|
|
784
|
+
return this.json("PUT", `${this.projectPath(org, name)}/observability/dashboard-settings`, settings);
|
|
785
|
+
}
|
|
786
|
+
/** Deploy and OOM markers for every time series. */
|
|
787
|
+
getObservabilityMarkers(org, name, range = {}) {
|
|
788
|
+
const q = new URLSearchParams();
|
|
789
|
+
if (range.from)
|
|
790
|
+
q.set("from", range.from);
|
|
791
|
+
if (range.to)
|
|
792
|
+
q.set("to", range.to);
|
|
793
|
+
const qs = q.toString();
|
|
794
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/markers${qs ? `?${qs}` : ""}`);
|
|
795
|
+
}
|
|
796
|
+
/** Watchdog insights (§17.10). Free: deploy regressions only. */
|
|
797
|
+
listInsights(org, name, opts = {}) {
|
|
798
|
+
const q = new URLSearchParams();
|
|
799
|
+
if (opts.state)
|
|
800
|
+
q.set("state", opts.state);
|
|
801
|
+
if (opts.limit !== undefined)
|
|
802
|
+
q.set("limit", String(opts.limit));
|
|
803
|
+
const qs = q.toString();
|
|
804
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/insights${qs ? `?${qs}` : ""}`);
|
|
805
|
+
}
|
|
806
|
+
/** A project's capacity risks and every check (docs/capacity-risk-alerts.md §5.3). Every plan. */
|
|
807
|
+
getProjectHealth(org, name) {
|
|
808
|
+
return this.json("GET", `${this.projectPath(org, name)}/health`);
|
|
809
|
+
}
|
|
810
|
+
/** Every project's capacity status and the organization's budget check. */
|
|
811
|
+
getOrgHealth(org) {
|
|
812
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/health`);
|
|
813
|
+
}
|
|
814
|
+
resolveInsight(org, name, id) {
|
|
815
|
+
return this.json("POST", `${this.projectPath(org, name)}/observability/insights/${encodeURIComponent(id)}/resolve`);
|
|
816
|
+
}
|
|
817
|
+
/** Synthetic checks (§17.11): the uptime checks, including multi-step API checks. */
|
|
818
|
+
listSynthetics(org, name) {
|
|
819
|
+
return this.json("GET", `${this.projectPath(org, name)}/synthetics`);
|
|
820
|
+
}
|
|
821
|
+
putSynthetic(org, name, id, input) {
|
|
822
|
+
return this.json("PUT", `${this.projectPath(org, name)}/synthetics/${encodeURIComponent(id)}`, input);
|
|
823
|
+
}
|
|
824
|
+
deleteSynthetic(org, name, id) {
|
|
825
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/synthetics/${encodeURIComponent(id)}`);
|
|
826
|
+
}
|
|
827
|
+
listSyntheticRuns(org, name, id, limit) {
|
|
828
|
+
const qs = limit !== undefined ? `?limit=${encodeURIComponent(String(limit))}` : "";
|
|
829
|
+
return this.json("GET", `${this.projectPath(org, name)}/synthetics/${encodeURIComponent(id)}/runs${qs}`);
|
|
830
|
+
}
|
|
831
|
+
/** A public dashboard link. Needs no credential. */
|
|
832
|
+
getSharedDashboard(token) {
|
|
833
|
+
return this.json("GET", `/v1/shared/dashboards/${encodeURIComponent(token)}`);
|
|
834
|
+
}
|
|
835
|
+
getSharedDashboardWidgetData(token, widgetId, req = {}) {
|
|
836
|
+
return this.json("POST", `/v1/shared/dashboards/${encodeURIComponent(token)}/widgets/${encodeURIComponent(widgetId)}/data`, req);
|
|
837
|
+
}
|
|
838
|
+
// Customer observability, APM read side (docs/customer-observability.md §8, §17.2–§17.4,
|
|
839
|
+
// §17.6). Range options are RFC 3339 times; `step` is seconds or a duration ("5m").
|
|
840
|
+
obsApmPath(org, name, sub, q) {
|
|
841
|
+
const qs = new URLSearchParams();
|
|
842
|
+
for (const [k, v] of Object.entries(q ?? {}))
|
|
843
|
+
if (v !== undefined && v !== "")
|
|
844
|
+
qs.set(k, String(v));
|
|
845
|
+
const tail = qs.size > 0 ? `?${qs}` : "";
|
|
846
|
+
return `${this.projectPath(org, name)}/observability/${sub}${tail}`;
|
|
847
|
+
}
|
|
848
|
+
getObsMetricsOverview(org, name, opts = {}) {
|
|
849
|
+
return this.json("GET", this.obsApmPath(org, name, "metrics/overview", opts));
|
|
850
|
+
}
|
|
851
|
+
getObsMetricsMemory(org, name, opts = {}) {
|
|
852
|
+
return this.json("GET", this.obsApmPath(org, name, "metrics/memory", opts));
|
|
853
|
+
}
|
|
854
|
+
getObsMetricsInfrastructure(org, name, opts = {}) {
|
|
855
|
+
return this.json("GET", this.obsApmPath(org, name, "metrics/infrastructure", opts));
|
|
856
|
+
}
|
|
857
|
+
getObsMetricsCustom(org, name, opts) {
|
|
858
|
+
return this.json("GET", this.obsApmPath(org, name, "metrics/custom", opts));
|
|
859
|
+
}
|
|
860
|
+
getObsMetricsCustomNames(org, name) {
|
|
861
|
+
return this.json("GET", this.obsApmPath(org, name, "metrics/custom/names"));
|
|
862
|
+
}
|
|
863
|
+
listObsRoutes(org, name) {
|
|
864
|
+
return this.json("GET", this.obsApmPath(org, name, "routes"));
|
|
865
|
+
}
|
|
866
|
+
putObsRoute(org, name, input) {
|
|
867
|
+
return this.json("PUT", this.obsApmPath(org, name, "routes"), input);
|
|
868
|
+
}
|
|
869
|
+
getObsAPMRoutes(org, name, opts = {}) {
|
|
870
|
+
return this.json("GET", this.obsApmPath(org, name, "apm/routes", opts));
|
|
871
|
+
}
|
|
872
|
+
getObsAPMBreakdown(org, name, route, opts = {}) {
|
|
873
|
+
return this.json("GET", this.obsApmPath(org, name, `apm/routes/${encodeURIComponent(route)}/breakdown`, opts));
|
|
874
|
+
}
|
|
875
|
+
getObsAPMServiceMap(org, name, opts = {}) {
|
|
876
|
+
return this.json("GET", this.obsApmPath(org, name, "apm/service-map", opts));
|
|
877
|
+
}
|
|
878
|
+
getObsAPMCompare(org, name, a, b, opts = {}) {
|
|
879
|
+
return this.json("GET", this.obsApmPath(org, name, "apm/compare", { ...opts, a, b }));
|
|
880
|
+
}
|
|
881
|
+
listObsAPMTraces(org, name, opts = {}) {
|
|
882
|
+
return this.json("GET", this.obsApmPath(org, name, "apm/traces", opts));
|
|
883
|
+
}
|
|
884
|
+
getObsAPMTrace(org, name, traceId) {
|
|
885
|
+
return this.json("GET", this.obsApmPath(org, name, `apm/traces/${encodeURIComponent(traceId)}`));
|
|
886
|
+
}
|
|
887
|
+
listObsTraceFilters(org, name) {
|
|
888
|
+
return this.json("GET", this.obsApmPath(org, name, "trace-filters"));
|
|
889
|
+
}
|
|
890
|
+
createObsTraceFilter(org, name, input) {
|
|
891
|
+
return this.json("POST", this.obsApmPath(org, name, "trace-filters"), input);
|
|
892
|
+
}
|
|
893
|
+
updateObsTraceFilter(org, name, id, input) {
|
|
894
|
+
return this.json("PUT", this.obsApmPath(org, name, `trace-filters/${encodeURIComponent(id)}`), input);
|
|
895
|
+
}
|
|
896
|
+
deleteObsTraceFilter(org, name, id) {
|
|
897
|
+
return this.json("DELETE", this.obsApmPath(org, name, `trace-filters/${encodeURIComponent(id)}`));
|
|
898
|
+
}
|
|
899
|
+
listObsDBQueries(org, name, opts = {}) {
|
|
900
|
+
return this.json("GET", this.obsApmPath(org, name, "database/queries", opts));
|
|
901
|
+
}
|
|
902
|
+
getObsDBQuery(org, name, fingerprint, opts = {}) {
|
|
903
|
+
return this.json("GET", this.obsApmPath(org, name, `database/queries/${encodeURIComponent(fingerprint)}`, opts));
|
|
904
|
+
}
|
|
905
|
+
getObsDBHealth(org, name) {
|
|
906
|
+
return this.json("GET", this.obsApmPath(org, name, "database/health"));
|
|
907
|
+
}
|
|
908
|
+
/** The continuous profiler's switch (customer-observability.md §17.13). */
|
|
909
|
+
getObsProfilerSettings(org, name) {
|
|
910
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/profiler`);
|
|
911
|
+
}
|
|
912
|
+
/** On, off, or null for the default. Applies from the next deployment or restart. Admin. */
|
|
913
|
+
putObsProfilerSettings(org, name, enabled) {
|
|
914
|
+
return this.json("PUT", `${this.projectPath(org, name)}/observability/profiler`, { enabled });
|
|
915
|
+
}
|
|
916
|
+
/** The logs/traces sampling override; a null field is the manifest's. */
|
|
917
|
+
getObsSampling(org, name) {
|
|
918
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/sampling`);
|
|
919
|
+
}
|
|
920
|
+
/** Replaces the override (null = the manifest's). Admin. */
|
|
921
|
+
putObsSampling(org, name, body) {
|
|
922
|
+
return this.json("PUT", `${this.projectPath(org, name)}/observability/sampling`, body);
|
|
923
|
+
}
|
|
924
|
+
listObsProfiles(org, name, q = {}) {
|
|
925
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/profiles${obsProfileQS({ ...q })}`);
|
|
926
|
+
}
|
|
927
|
+
/** The windows within a minute of `ts` — the profile of a slow trace's minute. */
|
|
928
|
+
nearObsProfiles(org, name, ts) {
|
|
929
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/profiles/near${obsProfileQS({ ts })}`);
|
|
930
|
+
}
|
|
931
|
+
getObsFlamegraph(org, name, q = {}) {
|
|
932
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/profiles/flamegraph${obsProfileQS({ ...q })}`);
|
|
933
|
+
}
|
|
934
|
+
getObsProfileTop(org, name, q = {}) {
|
|
935
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/profiles/top${obsProfileQS({ ...q })}`);
|
|
936
|
+
}
|
|
937
|
+
/** Self-time share per function, version a against version b; `route` compares only the samples attributed to it. */
|
|
938
|
+
compareObsProfiles(org, name, a, b, q = {}) {
|
|
939
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/profiles/compare${obsProfileQS({ a, b, ...q })}`);
|
|
940
|
+
}
|
|
941
|
+
/** RUM: page views, sessions and p75 vitals over time (Pro). */
|
|
942
|
+
getObsRUMOverview(org, name, q = {}) {
|
|
943
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/rum/overview${obsProfileQS({ ...q })}`);
|
|
944
|
+
}
|
|
945
|
+
/** RUM by page template, with vitals, long tasks and resource/fetch timings (Pro). */
|
|
946
|
+
listObsRUMPages(org, name, q = {}) {
|
|
947
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/rum/pages${obsProfileQS({ ...q })}`);
|
|
948
|
+
}
|
|
949
|
+
/** RUM by device class (Pro). */
|
|
950
|
+
listObsRUMDevices(org, name, q = {}) {
|
|
951
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/rum/devices${obsProfileQS({ ...q })}`);
|
|
952
|
+
}
|
|
953
|
+
/** RUM page views, newest (or slowest LCP) first (Pro). */
|
|
954
|
+
listObsRUMViews(org, name, q = {}) {
|
|
955
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/rum/views${obsProfileQS({ ...q })}`);
|
|
956
|
+
}
|
|
957
|
+
/** One page view and the server requests it caused (Pro). */
|
|
958
|
+
getObsRUMView(org, name, viewId) {
|
|
959
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/rum/views/${encodeURIComponent(viewId)}`);
|
|
960
|
+
}
|
|
961
|
+
/** Starts one heap snapshot (Pro, admin; pauses the app while taken). Poll the list. */
|
|
962
|
+
createObsHeapSnapshot(org, name) {
|
|
963
|
+
return this.json("POST", `${this.projectPath(org, name)}/observability/heap-snapshots`, {});
|
|
964
|
+
}
|
|
965
|
+
listObsHeapSnapshots(org, name) {
|
|
966
|
+
return this.json("GET", `${this.projectPath(org, name)}/observability/heap-snapshots`);
|
|
967
|
+
}
|
|
968
|
+
/** The download URL of a ready snapshot (a `.heapsnapshot` file). */
|
|
969
|
+
obsHeapSnapshotURL(org, name, snapshotId) {
|
|
970
|
+
return new URL(`${this.projectPath(org, name)}/observability/heap-snapshots/${encodeURIComponent(snapshotId)}`, this.opts.baseUrl).toString();
|
|
971
|
+
}
|
|
972
|
+
/** A ready snapshot's text (Chrome DevTools heap snapshot JSON). */
|
|
973
|
+
downloadObsHeapSnapshot(org, name, snapshotId) {
|
|
974
|
+
return this.text("GET", `${this.projectPath(org, name)}/observability/heap-snapshots/${encodeURIComponent(snapshotId)}`);
|
|
975
|
+
}
|
|
976
|
+
getErrorOwnership(org, name) {
|
|
977
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors-ownership`);
|
|
978
|
+
}
|
|
979
|
+
createErrorOwnershipPin(org, name, input) {
|
|
980
|
+
return this.json("POST", `${this.projectPath(org, name)}/errors-ownership/pins`, input);
|
|
981
|
+
}
|
|
982
|
+
revokeErrorOwnershipPin(org, name, deploymentId, selector) {
|
|
983
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/errors-ownership/pins/${encodeURIComponent(deploymentId)}/${encodeURIComponent(selector)}`);
|
|
984
|
+
}
|
|
985
|
+
getErrorCapturePolicy(org, name) {
|
|
986
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors-capture-policy`);
|
|
987
|
+
}
|
|
988
|
+
putErrorCapturePolicy(org, name, policy, acknowledgePII = false) {
|
|
989
|
+
return this.json("PUT", `${this.projectPath(org, name)}/errors-capture-policy`, { policy, acknowledgePII });
|
|
990
|
+
}
|
|
991
|
+
// ---- Observability (docs/customer-observability.md §7, §17.5) ----
|
|
992
|
+
obsPath(org, name) {
|
|
993
|
+
return `${this.projectPath(org, name)}/observability`;
|
|
994
|
+
}
|
|
995
|
+
getObservabilityCapabilities(org, name) {
|
|
996
|
+
return this.json("GET", `${this.obsPath(org, name)}/capabilities`);
|
|
997
|
+
}
|
|
998
|
+
/** Runs a query; `{ available: false }` where there is no event store. */
|
|
999
|
+
queryObservability(org, name, query) {
|
|
1000
|
+
return this.json("POST", `${this.obsPath(org, name)}/query`, query);
|
|
1001
|
+
}
|
|
1002
|
+
listObservabilityKeys(org, name, opts = {}) {
|
|
1003
|
+
return this.json("GET", `${this.obsPath(org, name)}/keys${obsQS(opts)}`);
|
|
1004
|
+
}
|
|
1005
|
+
listObservabilityValues(org, name, key, opts = {}) {
|
|
1006
|
+
return this.json("GET", `${this.obsPath(org, name)}/values${obsQS({ ...opts, key })}`);
|
|
1007
|
+
}
|
|
1008
|
+
listObservabilityPatterns(org, name, opts = {}) {
|
|
1009
|
+
return this.json("GET", `${this.obsPath(org, name)}/patterns${obsQS(opts)}`);
|
|
1010
|
+
}
|
|
1011
|
+
getObservabilityRequest(org, name, requestId) {
|
|
1012
|
+
return this.json("GET", `${this.obsPath(org, name)}/requests/${encodeURIComponent(requestId)}`);
|
|
1013
|
+
}
|
|
1014
|
+
listSavedQueries(org, name) {
|
|
1015
|
+
return this.json("GET", `${this.obsPath(org, name)}/saved-queries`);
|
|
1016
|
+
}
|
|
1017
|
+
getSavedQuery(org, name, queryId) {
|
|
1018
|
+
return this.json("GET", `${this.obsPath(org, name)}/saved-queries/${encodeURIComponent(queryId)}`);
|
|
1019
|
+
}
|
|
1020
|
+
createSavedQuery(org, name, input) {
|
|
1021
|
+
return this.json("POST", `${this.obsPath(org, name)}/saved-queries`, input);
|
|
1022
|
+
}
|
|
1023
|
+
updateSavedQuery(org, name, queryId, input) {
|
|
1024
|
+
return this.json("PUT", `${this.obsPath(org, name)}/saved-queries/${encodeURIComponent(queryId)}`, input);
|
|
1025
|
+
}
|
|
1026
|
+
deleteSavedQuery(org, name, queryId) {
|
|
1027
|
+
return this.json("DELETE", `${this.obsPath(org, name)}/saved-queries/${encodeURIComponent(queryId)}`);
|
|
1028
|
+
}
|
|
1029
|
+
starSavedQuery(org, name, queryId) {
|
|
1030
|
+
return this.json("PUT", `${this.obsPath(org, name)}/saved-queries/${encodeURIComponent(queryId)}/star`);
|
|
1031
|
+
}
|
|
1032
|
+
unstarSavedQuery(org, name, queryId) {
|
|
1033
|
+
return this.json("DELETE", `${this.obsPath(org, name)}/saved-queries/${encodeURIComponent(queryId)}/star`);
|
|
1034
|
+
}
|
|
1035
|
+
getObservabilityPipeline(org, name) {
|
|
1036
|
+
return this.json("GET", `${this.obsPath(org, name)}/pipeline`);
|
|
1037
|
+
}
|
|
1038
|
+
putObservabilityPipeline(org, name, processors) {
|
|
1039
|
+
return this.json("PUT", `${this.obsPath(org, name)}/pipeline`, { processors });
|
|
1040
|
+
}
|
|
1041
|
+
getObservabilityScanner(org, name) {
|
|
1042
|
+
return this.json("GET", `${this.obsPath(org, name)}/scanner`);
|
|
1043
|
+
}
|
|
1044
|
+
putObservabilityScanner(org, name, scanner) {
|
|
1045
|
+
return this.json("PUT", `${this.obsPath(org, name)}/scanner`, scanner);
|
|
1046
|
+
}
|
|
1047
|
+
/** Runs a scanner (the project's, or `scanner`) over a sample; nothing is stored. */
|
|
1048
|
+
testObservabilityScanner(org, name, text, scanner) {
|
|
1049
|
+
return this.json("POST", `${this.obsPath(org, name)}/scanner/test`, scanner ? { text, scanner } : { text });
|
|
1050
|
+
}
|
|
1051
|
+
/** The whole ingest configuration, shaped as `.runlot/logs.json`. */
|
|
1052
|
+
exportObservabilityLogsConfig(org, name) {
|
|
1053
|
+
return this.json("GET", `${this.obsPath(org, name)}/logs-config`);
|
|
1054
|
+
}
|
|
1055
|
+
listObservabilityLogMetrics(org, name) {
|
|
1056
|
+
return this.json("GET", `${this.obsPath(org, name)}/log-metrics`);
|
|
1057
|
+
}
|
|
1058
|
+
putObservabilityLogMetric(org, name, metric, def) {
|
|
1059
|
+
return this.json("PUT", `${this.obsPath(org, name)}/log-metrics/${encodeURIComponent(metric)}`, def);
|
|
1060
|
+
}
|
|
1061
|
+
deleteObservabilityLogMetric(org, name, metric) {
|
|
1062
|
+
return this.json("DELETE", `${this.obsPath(org, name)}/log-metrics/${encodeURIComponent(metric)}`);
|
|
1063
|
+
}
|
|
1064
|
+
getErrorEventPII(org, name, issueId, eventId) {
|
|
1065
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/events/${encodeURIComponent(eventId)}?includePII=true`);
|
|
1066
|
+
}
|
|
1067
|
+
getErrorSettings(org, name) {
|
|
1068
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors-settings`);
|
|
1069
|
+
}
|
|
1070
|
+
putErrorSettings(org, name,
|
|
1071
|
+
/** Omitted writable fields preserve their values; ownership assignment defaults off. */
|
|
1072
|
+
settings) {
|
|
1073
|
+
return this.json("PUT", `${this.projectPath(org, name)}/errors-settings`, settings);
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* One AI diagnosis of an issue, billed to the org's AI credit. There is no
|
|
1077
|
+
* prompt to pass: the control plane builds it from the issue's stored event.
|
|
1078
|
+
* The same `idempotencyKey` again returns the stored answer without a second
|
|
1079
|
+
* charge, so a caller retrying after a timeout must reuse its key.
|
|
1080
|
+
*/
|
|
1081
|
+
diagnoseErrorIssue(org, name, issueId, body) {
|
|
1082
|
+
return this.json("POST", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/diagnoses`, body);
|
|
1083
|
+
}
|
|
1084
|
+
listErrorDiagnoses(org, name, issueId) {
|
|
1085
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/diagnoses`);
|
|
1086
|
+
}
|
|
1087
|
+
fixPath(org, name, issueId) {
|
|
1088
|
+
return `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/fix-proposals`;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Ask a model to propose a fix for an issue. Billed to the org's AI credit and
|
|
1092
|
+
* writes nothing: the repository, base commit and files are chosen by the control
|
|
1093
|
+
* plane. Reuse `idempotencyKey` on a retry so it is not charged twice.
|
|
1094
|
+
*/
|
|
1095
|
+
proposeErrorFix(org, name, issueId, body) {
|
|
1096
|
+
return this.json("POST", this.fixPath(org, name, issueId), body);
|
|
1097
|
+
}
|
|
1098
|
+
listErrorFixProposals(org, name, issueId) {
|
|
1099
|
+
return this.json("GET", this.fixPath(org, name, issueId));
|
|
1100
|
+
}
|
|
1101
|
+
getErrorFixProposal(org, name, issueId, proposalId) {
|
|
1102
|
+
return this.json("GET", `${this.fixPath(org, name, issueId)}/${encodeURIComponent(proposalId)}`);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Write a ready proposal as one commit on its own `runlot/autofix/` branch. It
|
|
1106
|
+
* builds, deploys and merges nothing. `proposalSha` must be the proposal's exactly.
|
|
1107
|
+
*/
|
|
1108
|
+
applyErrorFix(org, name, issueId, proposalId, proposalSha) {
|
|
1109
|
+
return this.json("POST", `${this.fixPath(org, name, issueId)}/${encodeURIComponent(proposalId)}/apply`, {
|
|
1110
|
+
proposalSha,
|
|
1111
|
+
acknowledgeUnreviewedCode: true,
|
|
72
1112
|
});
|
|
73
1113
|
}
|
|
74
|
-
|
|
75
|
-
return this.
|
|
1114
|
+
lookupErrorFixProposal(org, name, issueId, key) {
|
|
1115
|
+
return this.request("GET", `${this.fixPath(org, name, issueId)}/lookup`, { headers: { "Idempotency-Key": key } });
|
|
1116
|
+
}
|
|
1117
|
+
lookupErrorFixPreview(org, name, issueId, proposalId, key) {
|
|
1118
|
+
return this.request("GET", `${this.fixPath(org, name, issueId)}/${encodeURIComponent(proposalId)}/previews/lookup`, { headers: { "Idempotency-Key": key } });
|
|
1119
|
+
}
|
|
1120
|
+
lookupErrorFixVerification(org, name, issueId, proposalId, jobId, key) {
|
|
1121
|
+
return this.request("GET", `${this.fixPath(org, name, issueId)}/${encodeURIComponent(proposalId)}/previews/${encodeURIComponent(jobId)}/verify/lookup`, { headers: { "Idempotency-Key": key } });
|
|
1122
|
+
}
|
|
1123
|
+
approveErrorFixPreview(org, name, issueId, proposalId, body) {
|
|
1124
|
+
return this.json("POST", `${this.fixPath(org, name, issueId)}/${encodeURIComponent(proposalId)}/previews`, body);
|
|
1125
|
+
}
|
|
1126
|
+
getErrorFixPreview(org, name, issueId, proposalId, jobId) {
|
|
1127
|
+
return this.json("GET", `${this.fixPath(org, name, issueId)}/${encodeURIComponent(proposalId)}/previews/${encodeURIComponent(jobId)}`);
|
|
1128
|
+
}
|
|
1129
|
+
approveErrorFixPreviewVerification(org, name, issueId, proposalId, jobId, body) {
|
|
1130
|
+
return this.json("POST", `${this.fixPath(org, name, issueId)}/${encodeURIComponent(proposalId)}/previews/${encodeURIComponent(jobId)}/verify`, body);
|
|
1131
|
+
}
|
|
1132
|
+
/** Erase captured issue data, after explicit review. Admin; unmerge first. */
|
|
1133
|
+
deleteErrorIssue(org, name, issueId) {
|
|
1134
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}`);
|
|
1135
|
+
}
|
|
1136
|
+
/** Erase an exact application user ID; anonymous aggregates remain. Admin. */
|
|
1137
|
+
eraseErrorUser(org, name, userId) {
|
|
1138
|
+
return this.json("POST", `${this.projectPath(org, name)}/errors-erasure`, { userId });
|
|
1139
|
+
}
|
|
1140
|
+
/** Untrusted browser histograms and technical-fuse counters, not billed usage. Viewer. */
|
|
1141
|
+
getBrowserTelemetry(org, name) {
|
|
1142
|
+
return this.json("GET", `${this.projectPath(org, name)}/browser-telemetry`);
|
|
1143
|
+
}
|
|
1144
|
+
/** Unverified feedback whose exact browser error anchor belongs to this issue. Never includes email. */
|
|
1145
|
+
listBrowserFeedback(org, name, issueId) {
|
|
1146
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/feedback`);
|
|
1147
|
+
}
|
|
1148
|
+
/** Explicit sensitive read. Admin + errors:pii; optional unverified email only while feedback-email retention is on. */
|
|
1149
|
+
listBrowserFeedbackWithPII(org, name, issueId) {
|
|
1150
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/feedback?includePII=true`);
|
|
1151
|
+
}
|
|
1152
|
+
/** The masked structural replay switch, cap and this month's count. Viewer. */
|
|
1153
|
+
getErrorReplaySettings(org, name) {
|
|
1154
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors-replay-settings`);
|
|
76
1155
|
}
|
|
77
1156
|
/**
|
|
78
|
-
*
|
|
79
|
-
* `
|
|
1157
|
+
* Both fields are required: `monthlyCap` null is the plan's amount, `0` stops collection,
|
|
1158
|
+
* `-1` removes the cap (Pro). Admin.
|
|
80
1159
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
u.searchParams.set("redirect", redirect);
|
|
84
|
-
return u.toString();
|
|
1160
|
+
putErrorReplaySettings(org, name, settings) {
|
|
1161
|
+
return this.json("PUT", `${this.projectPath(org, name)}/errors-replay-settings`, settings);
|
|
85
1162
|
}
|
|
86
|
-
|
|
87
|
-
|
|
1163
|
+
/** An issue's complete masked structural replays. Admin, until a granular PII scope exists. */
|
|
1164
|
+
listErrorReplays(org, name, issueId) {
|
|
1165
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/replays`);
|
|
88
1166
|
}
|
|
89
|
-
|
|
90
|
-
|
|
1167
|
+
/** One replay and its re-validated batches, for the sandboxed player. Admin. */
|
|
1168
|
+
getErrorReplay(org, name, issueId, replayId) {
|
|
1169
|
+
return this.json("GET", `${this.projectPath(org, name)}/errors/${encodeURIComponent(issueId)}/replays/${encodeURIComponent(replayId)}`);
|
|
91
1170
|
}
|
|
92
|
-
|
|
93
|
-
|
|
1171
|
+
/**
|
|
1172
|
+
* Scheduled runs: what the live deployment declares, when each fires next, and
|
|
1173
|
+
* the recent history (docs/cron.md §8). `limit` bounds the history rows only —
|
|
1174
|
+
* the schedules and the per-schedule last run are never paged.
|
|
1175
|
+
*/
|
|
1176
|
+
getProjectCron(org, name, limit) {
|
|
1177
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
1178
|
+
return this.json("GET", `${this.projectPath(org, name)}/cron${qs}`);
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Queues: what the live deployment declares, how deep each one is, and the
|
|
1182
|
+
* recent batch history (docs/queues.md §6). The depth is read from the
|
|
1183
|
+
* project's own database; `depthError` on the answer says when it could not be.
|
|
1184
|
+
*/
|
|
1185
|
+
getProjectQueues(org, name, limit) {
|
|
1186
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
1187
|
+
return this.json("GET", `${this.projectPath(org, name)}/queues${qs}`);
|
|
94
1188
|
}
|
|
95
1189
|
/**
|
|
96
|
-
*
|
|
1190
|
+
* Durable Objects: how many this project has placed, against the two caps
|
|
1191
|
+
* (docs/durable-objects.md).
|
|
97
1192
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
1193
|
+
* **It answers how much, never which.** There is no actor id in the response
|
|
1194
|
+
* and none is coming — those name individual objects, and a list of them would
|
|
1195
|
+
* be an object enumeration wearing a quota's name. `nodes` is keyed by node id
|
|
1196
|
+
* because `nodeLimit` is a separate bound that can refuse a placement while
|
|
1197
|
+
* the project-wide cap still has room.
|
|
100
1198
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/notifications${q}`);
|
|
1199
|
+
getProjectDurableObjects(org, name) {
|
|
1200
|
+
return this.json("GET", `${this.projectPath(org, name)}/durable-objects`);
|
|
104
1201
|
}
|
|
105
|
-
|
|
106
|
-
return this.
|
|
1202
|
+
queuePath(org, name, queue) {
|
|
1203
|
+
return `${this.projectPath(org, name)}/queues/${encodeURIComponent(queue)}`;
|
|
107
1204
|
}
|
|
108
|
-
|
|
109
|
-
|
|
1205
|
+
listQueueRuns(org, name, queue, limit) {
|
|
1206
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
1207
|
+
return this.json("GET", `${this.queuePath(org, name, queue)}/runs${qs}`);
|
|
110
1208
|
}
|
|
111
|
-
|
|
112
|
-
|
|
1209
|
+
/** The head of the queue without leasing it — looking cannot delay a delivery. */
|
|
1210
|
+
peekQueue(org, name, queue, limit) {
|
|
1211
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
1212
|
+
return this.json("GET", `${this.queuePath(org, name, queue)}/messages${qs}`);
|
|
113
1213
|
}
|
|
114
|
-
|
|
115
|
-
|
|
1214
|
+
/** Irreversible. The confirmation is the caller's — the server holds none. */
|
|
1215
|
+
purgeQueue(org, name, queue) {
|
|
1216
|
+
return this.json("DELETE", `${this.queuePath(org, name, queue)}/messages`);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Moves a dead-letter queue's messages back. `to` is required only when the
|
|
1220
|
+
* dead-letter queue belongs to more than one consumer, or to none.
|
|
1221
|
+
*/
|
|
1222
|
+
redriveQueue(org, name, queue, to) {
|
|
1223
|
+
return this.json("POST", `${this.queuePath(org, name, queue)}/redrive`, to === undefined ? {} : { to });
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The vector store: every namespace with its count and width, and the plan's
|
|
1227
|
+
* bounds (docs/vectors.md §4). Read from the project's own database;
|
|
1228
|
+
* `readError` on the answer says when it could not be.
|
|
1229
|
+
*/
|
|
1230
|
+
getProjectVectors(org, name) {
|
|
1231
|
+
return this.json("GET", `${this.projectPath(org, name)}/vectors`);
|
|
116
1232
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
1233
|
+
/** Irreversible. The confirmation is the caller's — the server holds none. */
|
|
1234
|
+
deleteVectorNamespace(org, name, namespace) {
|
|
1235
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/vectors/${encodeURIComponent(namespace)}`);
|
|
1236
|
+
}
|
|
1237
|
+
/** The body is the bundle. One digest covers all of the code and the assets. */
|
|
1238
|
+
/**
|
|
1239
|
+
* Uploads the bundle. `headers` carries the provenance headers
|
|
1240
|
+
* (`X-Runlot-Git-*`, openapi createDeployment) — the body is the bundle
|
|
1241
|
+
* itself, so the metadata can only travel in headers.
|
|
1242
|
+
*/
|
|
1243
|
+
deploy(org, name, bundle, headers = {}) {
|
|
119
1244
|
return this.request("POST", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/deployments`, {
|
|
120
1245
|
body: bundle,
|
|
1246
|
+
headers: { ...headers, "Content-Type": "application/gzip" },
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* The same upload, aimed at a preview (docs/preview.md). **It does not move
|
|
1251
|
+
* production's live pointer** — that is the whole difference, and it is why
|
|
1252
|
+
* this is a named method rather than an optional argument on `deploy`: a
|
|
1253
|
+
* caller that forgets the argument deploys to production.
|
|
1254
|
+
*
|
|
1255
|
+
* `db` picks what `env.db` points at (`branch` by default, `shared` at
|
|
1256
|
+
* production's own database, `none` for no database), and `from` pours the
|
|
1257
|
+
* branch from a named generation instead of the newest one.
|
|
1258
|
+
*/
|
|
1259
|
+
deployPreview(org, name, bundle,
|
|
1260
|
+
// `| undefined` spelled out: under exactOptionalPropertyTypes a caller that
|
|
1261
|
+
// forwards an unset flag is passing the property, not omitting it.
|
|
1262
|
+
opts, headers = {}) {
|
|
1263
|
+
return this.request("POST", `${this.projectPath(org, name)}/deployments?${previewQuery(opts)}`, {
|
|
1264
|
+
body: bundle,
|
|
1265
|
+
headers: { ...headers, "Content-Type": "application/gzip" },
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Opens an incremental upload (docs/incremental-deploy-upload.md §1.1). `files` is
|
|
1270
|
+
* the bundle's entry list in tar order; the answer names the hashes to upload.
|
|
1271
|
+
* An older CP answers 404 here — the caller falls back to `deploy`.
|
|
1272
|
+
*/
|
|
1273
|
+
createDeploymentUpload(org, name, files, preview) {
|
|
1274
|
+
let path = `${this.projectPath(org, name)}/deployments/uploads`;
|
|
1275
|
+
if (preview)
|
|
1276
|
+
path += `?${previewQuery(preview)}`;
|
|
1277
|
+
return this.json("POST", path, { files });
|
|
1278
|
+
}
|
|
1279
|
+
/** One batch: a gzip tar whose entries are named by their content's sha256 (§1.2). */
|
|
1280
|
+
uploadDeploymentBlobs(org, name, uploadId, batch) {
|
|
1281
|
+
return this.request("POST", `${this.projectPath(org, name)}/deployments/uploads/${encodeURIComponent(uploadId)}/blobs`, {
|
|
1282
|
+
body: batch,
|
|
121
1283
|
headers: { "Content-Type": "application/gzip" },
|
|
122
1284
|
});
|
|
123
1285
|
}
|
|
124
|
-
/**
|
|
1286
|
+
/**
|
|
1287
|
+
* Assembles and deploys the session (§1.3). The answer is `createDeployment`'s
|
|
1288
|
+
* (with `preview` for a preview session). Idempotent per session, so a retry
|
|
1289
|
+
* after an edge timeout returns the same deployment.
|
|
1290
|
+
*/
|
|
1291
|
+
commitDeploymentUpload(org, name, uploadId, headers = {}, staticChecks) {
|
|
1292
|
+
// The body is optional: a CP from before the checks reads none, and the deploy is the same.
|
|
1293
|
+
return this.request("POST", `${this.projectPath(org, name)}/deployments/uploads/${encodeURIComponent(uploadId)}/commit`, {
|
|
1294
|
+
headers: staticChecks !== undefined ? { ...headers, "Content-Type": "application/json" } : headers,
|
|
1295
|
+
...(staticChecks !== undefined ? { body: JSON.stringify({ staticChecks }) } : {}),
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
/** The parent's live previews (viewer). A deleted preview is not listed. */
|
|
1299
|
+
listPreviews(org, name) {
|
|
1300
|
+
return this.json("GET", `${this.projectPath(org, name)}/previews`);
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Deletes a preview and its branch (member).
|
|
1304
|
+
*
|
|
1305
|
+
* No confirmation string, unlike `deleteProject`: a branch is a copy taken at a
|
|
1306
|
+
* moment and nothing in it is the only copy of anything.
|
|
1307
|
+
*/
|
|
1308
|
+
deletePreview(org, name, preview) {
|
|
1309
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/previews/${encodeURIComponent(preview)}`);
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Makes the preview's artifact the parent's live deployment (member).
|
|
1313
|
+
*
|
|
1314
|
+
* **The code, never the data** (docs/preview.md §7). The branch is not merged
|
|
1315
|
+
* and production's database is not replaced. The preview stays alive.
|
|
1316
|
+
*/
|
|
1317
|
+
promotePreview(org, name, preview) {
|
|
1318
|
+
return this.json("POST", `${this.projectPath(org, name)}/previews/${encodeURIComponent(preview)}/promote`);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* The project's share link — `<label>.share.<app-domain>`, the one address
|
|
1322
|
+
* Studio hands to somebody who is not a developer (docs/studio.md §6.9).
|
|
1323
|
+
*
|
|
1324
|
+
* 404 when the project has never been shared: a link is minted by
|
|
1325
|
+
* `putShareLink`, never as a side effect of reading.
|
|
1326
|
+
*/
|
|
1327
|
+
getShareLink(org, name) {
|
|
1328
|
+
return this.json("GET", `${this.projectPath(org, name)}/share`);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Points the share link at a preview, minting the label if there is none.
|
|
1332
|
+
*
|
|
1333
|
+
* Idempotent: the label is kept across shares, so somebody holding the link
|
|
1334
|
+
* keeps working. `rotate` mints a new label and retires the old name in the
|
|
1335
|
+
* same request ("링크 재발급"), so the project never has two live links.
|
|
1336
|
+
*/
|
|
1337
|
+
putShareLink(org, name, opts = {}) {
|
|
1338
|
+
return this.json("PUT", `${this.projectPath(org, name)}/share`, {
|
|
1339
|
+
...(opts.preview === undefined ? {} : { preview: opts.preview }),
|
|
1340
|
+
...(opts.rotate === undefined ? {} : { rotate: opts.rotate }),
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
/** Stops the link answering and forgets the label. The preview keeps running. */
|
|
1344
|
+
deleteShareLink(org, name) {
|
|
1345
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/share`);
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Creates a database named `name` in the org (member). When `name` is a project
|
|
1349
|
+
* without a database, this is that project's own `env.db`. Idempotent for a
|
|
1350
|
+
* project's own database: the password is not rolled and the same connection
|
|
1351
|
+
* details come back.
|
|
1352
|
+
*/
|
|
125
1353
|
createDatabase(org, name) {
|
|
126
|
-
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/
|
|
1354
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/databases`, { name });
|
|
127
1355
|
}
|
|
1356
|
+
/** One database: size, state and the projects that bind it. 404 when there is none. */
|
|
128
1357
|
getDatabase(org, name) {
|
|
129
|
-
return this.json("GET",
|
|
1358
|
+
return this.json("GET", dbPath(org, name));
|
|
130
1359
|
}
|
|
131
1360
|
/**
|
|
132
|
-
*
|
|
133
|
-
* viewer
|
|
1361
|
+
* The connection credentials as well. The route is split from `getDatabase`
|
|
1362
|
+
* because of roles — a viewer may know whether there is a database, but not
|
|
1363
|
+
* the password (proto/openapi.yaml).
|
|
134
1364
|
*/
|
|
135
1365
|
getDatabaseConnect(org, name) {
|
|
136
|
-
return this.json("GET",
|
|
1366
|
+
return this.json("GET", `${dbPath(org, name)}/connect`);
|
|
137
1367
|
}
|
|
138
|
-
/**
|
|
1368
|
+
/** The endpoint only — no credentials. A viewer gets it too. Used by `runlot db forward`. */
|
|
139
1369
|
getDatabaseEndpoint(org, name) {
|
|
140
1370
|
return this.json("GET", `${dbPath(org, name)}/endpoint`);
|
|
141
1371
|
}
|
|
142
|
-
/**
|
|
1372
|
+
/** Issues a short-lived machine credential. The password is carried in this response and nowhere else. */
|
|
143
1373
|
createDatabaseToken(org, name, ttlSeconds) {
|
|
144
1374
|
return this.json("POST", `${dbPath(org, name)}/tokens`, ttlSeconds === undefined ? {} : { ttlSeconds });
|
|
145
1375
|
}
|
|
@@ -150,59 +1380,123 @@ export class Client {
|
|
|
150
1380
|
return this.json("DELETE", `${dbPath(org, name)}/tokens/${encodeURIComponent(user)}`);
|
|
151
1381
|
}
|
|
152
1382
|
/**
|
|
153
|
-
*
|
|
1383
|
+
* Makes one diagnostic image.
|
|
154
1384
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* (proto/openapi.yaml).
|
|
1385
|
+
* **Not idempotent** — every call makes another image. And it is slow: the
|
|
1386
|
+
* response comes after the dump finishes, and that project's actor is held
|
|
1387
|
+
* for the whole of it (proto/openapi.yaml).
|
|
158
1388
|
*/
|
|
159
1389
|
exportDatabase(org, name) {
|
|
160
|
-
return this.json("POST",
|
|
1390
|
+
return this.json("POST", `${dbPath(org, name)}/export`);
|
|
161
1391
|
}
|
|
162
1392
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
1393
|
+
* Every generation a restore can choose, newest first. `lastCheckedAt` is the
|
|
1394
|
+
* heartbeat of the no-change skip — it is what tells "no new generations"
|
|
1395
|
+
* apart from "the backup is dead".
|
|
165
1396
|
*/
|
|
166
1397
|
listGenerations(org, name) {
|
|
167
1398
|
return this.json("GET", `${this.dbPath(org, name)}/generations`);
|
|
168
1399
|
}
|
|
169
1400
|
/**
|
|
170
|
-
*
|
|
1401
|
+
* Runs one statement in a database (the dashboard console).
|
|
1402
|
+
*
|
|
1403
|
+
* `readOnly` is **omitted, not sent as false**, when the caller wants the safe
|
|
1404
|
+
* mode: the server treats an absent field as read-only, so leaving it out is the
|
|
1405
|
+
* one spelling that cannot be misread. Sending `false` is the explicit ask for
|
|
1406
|
+
* write mode, and only then does the server run outside a transaction.
|
|
1407
|
+
*
|
|
1408
|
+
* SQL failures arrive as an `ApiError` whose body carries `details.sqlstate` —
|
|
1409
|
+
* PostgreSQL's own code, which is what the error panel shows.
|
|
1410
|
+
*/
|
|
1411
|
+
executeSQL(org, name, sql, opts = {}) {
|
|
1412
|
+
const body = { sql };
|
|
1413
|
+
if (opts.readOnly === false)
|
|
1414
|
+
body.readOnly = false;
|
|
1415
|
+
if (opts.params !== undefined && opts.params.length > 0)
|
|
1416
|
+
body.params = opts.params;
|
|
1417
|
+
return this.json("POST", `${this.dbPath(org, name)}/execute`, body);
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Rolls back to a generation. **Writes made since then are lost.**
|
|
171
1421
|
*
|
|
172
|
-
*
|
|
173
|
-
* `getDatabaseOperation
|
|
174
|
-
*
|
|
1422
|
+
* It ends at 202 — what comes back is an operation id, and progress is read
|
|
1423
|
+
* through `getDatabaseOperation`. The server does not resolve `latest`: the
|
|
1424
|
+
* `(epoch, seq)` chosen from the list is carried as-is (proto/openapi.yaml).
|
|
175
1425
|
*/
|
|
176
1426
|
restoreDatabase(org, name, epoch, seq) {
|
|
177
1427
|
return this.json("POST", `${this.dbPath(org, name)}/restore`, { epoch, seq });
|
|
178
1428
|
}
|
|
179
1429
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
1430
|
+
* Rolls back to a restore point **inside** a backup (pg-feather docs/37):
|
|
1431
|
+
* `txnId` is that commit, `at` is the last commit at or before that time. One
|
|
1432
|
+
* backup carries the commits in between in order, so the place you can roll
|
|
1433
|
+
* back to is not tied to a backup boundary. Everything else is as in
|
|
1434
|
+
* restoreDatabase — 202, an operation id, and one safety backup before the
|
|
1435
|
+
* rollback.
|
|
1436
|
+
*/
|
|
1437
|
+
restoreDatabaseTo(org, name, target) {
|
|
1438
|
+
return this.json("POST", `${this.dbPath(org, name)}/restore`, target);
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Removes a database (admin). `confirm` has to match the database name
|
|
1442
|
+
* **exactly**; if it does not, the server refuses with 400 `confirm_mismatch`.
|
|
1443
|
+
* A bound database answers 409 `database_bound` unless `unbindAll` is set.
|
|
1444
|
+
*/
|
|
1445
|
+
deleteDatabase(org, name, confirm, opts = {}) {
|
|
1446
|
+
return this.json("POST", `${this.dbPath(org, name)}/delete`, opts.unbindAll === true ? { confirm, unbindAll: true } : { confirm });
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Removes one project's binding to a database (admin), for a project nobody will
|
|
1450
|
+
* redeploy — a deploy otherwise reconciles bindings from runlot.json. 204.
|
|
182
1451
|
*/
|
|
183
|
-
|
|
184
|
-
return this.json("
|
|
1452
|
+
unbindDatabase(org, database, project) {
|
|
1453
|
+
return this.json("DELETE", `${this.dbPath(org, database)}/bindings/${encodeURIComponent(project)}`);
|
|
185
1454
|
}
|
|
186
|
-
/**
|
|
1455
|
+
/** Every database in the org with the projects that bind it. Viewer and above. */
|
|
1456
|
+
listOrgDatabases(org) {
|
|
1457
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/databases`);
|
|
1458
|
+
}
|
|
1459
|
+
/** The progress of one operation. The operation id is not a credential; it lives under the database path. */
|
|
187
1460
|
getDatabaseOperation(org, name, opId) {
|
|
188
1461
|
return this.json("GET", `${this.dbPath(org, name)}/operations/${encodeURIComponent(opId)}`);
|
|
189
1462
|
}
|
|
190
|
-
/**
|
|
1463
|
+
/** Aborts an operation that has not yet reached a phase it cannot come back from. */
|
|
191
1464
|
abortDatabaseOperation(org, name, opId) {
|
|
192
1465
|
return this.json("POST", `${this.dbPath(org, name)}/operations/${encodeURIComponent(opId)}/abort`);
|
|
193
1466
|
}
|
|
194
1467
|
// ---- Phase 6 (docs/phase6.md) ----
|
|
195
1468
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
1469
|
+
* A live tail of the logs. It is **a stream, not JSON**, so it does not go
|
|
1470
|
+
* through `request` — what comes back is an async iterable of line-by-line
|
|
1471
|
+
* `LogEvent`, and aborting `signal` closes the connection. An error response
|
|
1472
|
+
* (4xx/5xx) is the same `ApiError` as in every other method.
|
|
199
1473
|
*/
|
|
200
1474
|
async *tailLogs(org, name, opts = {}) {
|
|
1475
|
+
yield* this.tailStream(org, name, opts, undefined);
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* The live tail as `LogRecord`s — the stored event's shape (`--format json`).
|
|
1479
|
+
* Pings and the end event still arrive as `LogEvent`s.
|
|
1480
|
+
*/
|
|
1481
|
+
async *tailLogRecords(org, name, opts = {}) {
|
|
1482
|
+
yield* this.tailStream(org, name, opts, "json");
|
|
1483
|
+
}
|
|
1484
|
+
async *tailStream(org, name, opts, format) {
|
|
201
1485
|
const q = new URLSearchParams();
|
|
202
1486
|
if (opts.tail !== undefined)
|
|
203
1487
|
q.set("tail", String(opts.tail));
|
|
204
1488
|
if (opts.follow !== undefined)
|
|
205
1489
|
q.set("follow", String(opts.follow));
|
|
1490
|
+
if (opts.level)
|
|
1491
|
+
q.set("level", opts.level);
|
|
1492
|
+
if (opts.source)
|
|
1493
|
+
q.set("source", opts.source);
|
|
1494
|
+
if (opts.q)
|
|
1495
|
+
q.set("q", opts.q);
|
|
1496
|
+
if (opts.requestId)
|
|
1497
|
+
q.set("requestId", opts.requestId);
|
|
1498
|
+
if (format)
|
|
1499
|
+
q.set("format", format);
|
|
206
1500
|
const path = `${this.projectPath(org, name)}/logs${q.size ? `?${q}` : ""}`;
|
|
207
1501
|
const res = await fetch(new URL(path, this.opts.baseUrl), {
|
|
208
1502
|
method: "GET",
|
|
@@ -215,7 +1509,7 @@ export class Client {
|
|
|
215
1509
|
body = (await res.json());
|
|
216
1510
|
}
|
|
217
1511
|
catch {
|
|
218
|
-
//
|
|
1512
|
+
// The same reason as in request above.
|
|
219
1513
|
}
|
|
220
1514
|
throw new ApiError(res.status, body, `GET ${path} → ${res.status}`);
|
|
221
1515
|
}
|
|
@@ -243,14 +1537,35 @@ export class Client {
|
|
|
243
1537
|
yield JSON.parse(rest);
|
|
244
1538
|
}
|
|
245
1539
|
finally {
|
|
246
|
-
//
|
|
1540
|
+
// If the consumer ends early (break), the server's follow has to end too.
|
|
247
1541
|
await reader.cancel().catch(() => { });
|
|
248
1542
|
}
|
|
249
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* The stored lane (0062). It is a plain JSON page, not a stream: the history
|
|
1546
|
+
* does not reach a node, so it answers for a project that is cold, parked, or
|
|
1547
|
+
* has moved since the lines were written — every one of which is a 409 on
|
|
1548
|
+
* `tailLogs`.
|
|
1549
|
+
*
|
|
1550
|
+
* `q` is a plain substring, matched case-insensitively. It is not a regular
|
|
1551
|
+
* expression — `%` and `_` match themselves.
|
|
1552
|
+
*
|
|
1553
|
+
* Paging is keyset: pass the `cursor` from a page back to get the next, older
|
|
1554
|
+
* one. A page with no `cursor` is the end of what is kept.
|
|
1555
|
+
*/
|
|
1556
|
+
getLogHistory(org, name, opts = {}) {
|
|
1557
|
+
const params = new URLSearchParams();
|
|
1558
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
1559
|
+
if (v !== undefined && v !== "")
|
|
1560
|
+
params.set(k, String(v));
|
|
1561
|
+
}
|
|
1562
|
+
const qs = params.size ? `?${params}` : "";
|
|
1563
|
+
return this.json("GET", `${this.projectPath(org, name)}/logs/history${qs}`);
|
|
1564
|
+
}
|
|
250
1565
|
listSecrets(org, name) {
|
|
251
1566
|
return this.json("GET", `${this.projectPath(org, name)}/secrets`);
|
|
252
1567
|
}
|
|
253
|
-
/**
|
|
1568
|
+
/** The value travels in this call only. It is in no response at all. */
|
|
254
1569
|
setSecret(org, name, secret, value) {
|
|
255
1570
|
return this.json("PUT", `${this.projectPath(org, name)}/secrets/${encodeURIComponent(secret)}`, { value });
|
|
256
1571
|
}
|
|
@@ -261,111 +1576,438 @@ export class Client {
|
|
|
261
1576
|
const q = hours === undefined ? "" : `?hours=${hours}`;
|
|
262
1577
|
return this.json("GET", `${this.projectPath(org, name)}/usage${q}`);
|
|
263
1578
|
}
|
|
264
|
-
// ----
|
|
265
|
-
/**
|
|
1579
|
+
// ---- Object storage (docs/storage.md §8) ----
|
|
1580
|
+
/** Grant, prefix, quota, usage. `usedBytes` lags by the node's reporting interval. */
|
|
266
1581
|
getStorage(org, name) {
|
|
267
1582
|
return this.json("GET", `${this.projectPath(org, name)}/storage`);
|
|
268
1583
|
}
|
|
269
1584
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
1585
|
+
* Grants storage. It is idempotent, so pressing twice leaves the prefix as it
|
|
1586
|
+
* was. Taking neither the prefix nor the quota as an argument is the point —
|
|
1587
|
+
* the prefix is the isolation boundary and the quota is set by the plan
|
|
1588
|
+
* (proto/openapi.yaml).
|
|
273
1589
|
*/
|
|
274
1590
|
grantStorage(org, name) {
|
|
275
1591
|
return this.json("POST", `${this.projectPath(org, name)}/storage`);
|
|
276
1592
|
}
|
|
277
|
-
/**
|
|
1593
|
+
/** Removes the grant. **The objects are not deleted** (docs/storage.md §7). */
|
|
278
1594
|
revokeStorage(org, name) {
|
|
279
1595
|
return this.json("DELETE", `${this.projectPath(org, name)}/storage`);
|
|
280
1596
|
}
|
|
281
|
-
// ---- 최종 사용자 인증 (docs/auth.md §9) ----
|
|
282
1597
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
1598
|
+
* The key prefixes this project serves unsigned (docs/storage.md §4.7).
|
|
1599
|
+
*
|
|
1600
|
+
* A prefix is stored normalized — `assets` becomes `assets/`, and `/` becomes
|
|
1601
|
+
* the empty string, which means every key. The value that comes back is the
|
|
1602
|
+
* value that governs, so a screen must render the response rather than what
|
|
1603
|
+
* the user typed.
|
|
1604
|
+
*/
|
|
1605
|
+
listStoragePublic(org, name) {
|
|
1606
|
+
return this.json("GET", `${this.projectPath(org, name)}/storage/public`);
|
|
1607
|
+
}
|
|
1608
|
+
/** Publishes a prefix. Idempotent. All three verbs answer with the whole list. */
|
|
1609
|
+
addStoragePublic(org, name, prefix) {
|
|
1610
|
+
return this.json("POST", `${this.projectPath(org, name)}/storage/public`, { prefix });
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Unpublishes a prefix. Idempotent.
|
|
1614
|
+
*
|
|
1615
|
+
* **The world keeps reading for up to a minute.** The object front caches the
|
|
1616
|
+
* list, so this call is not a revocation that takes effect at the instant it
|
|
1617
|
+
* returns — a screen that says "no longer public" the moment this resolves is
|
|
1618
|
+
* telling a small lie.
|
|
1619
|
+
*/
|
|
1620
|
+
removeStoragePublic(org, name, prefix) {
|
|
1621
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/storage/public`, { prefix });
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* One page of objects grouped by `/` (`runlot storage ls`). `prefixes` are the
|
|
1625
|
+
* folders one level down; feed `cursor` back unchanged, its absence is the end.
|
|
1626
|
+
*/
|
|
1627
|
+
listStorageObjects(org, name, opts = {}) {
|
|
1628
|
+
const q = new URLSearchParams();
|
|
1629
|
+
if (opts.prefix)
|
|
1630
|
+
q.set("prefix", opts.prefix);
|
|
1631
|
+
if (opts.cursor)
|
|
1632
|
+
q.set("cursor", opts.cursor);
|
|
1633
|
+
if (opts.limit)
|
|
1634
|
+
q.set("limit", String(opts.limit));
|
|
1635
|
+
const qs = q.toString();
|
|
1636
|
+
return this.json("GET", `${this.projectPath(org, name)}/storage/objects${qs ? "?" + qs : ""}`);
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* A five-minute signed GET for one object (`runlot storage download`). The key's
|
|
1640
|
+
* `/` segments stay in the path; each segment is encoded on its own.
|
|
1641
|
+
*/
|
|
1642
|
+
signStorageObject(org, name, key) {
|
|
1643
|
+
const path = key.split("/").map(encodeURIComponent).join("/");
|
|
1644
|
+
return this.json("GET", `${this.projectPath(org, name)}/storage/objects/${path}`);
|
|
1645
|
+
}
|
|
1646
|
+
// ---- Email (docs/email.md §5) ----
|
|
1647
|
+
/** Grant, address, sender, limits, recent counts. The address comes back as a preview even without a grant. */
|
|
1648
|
+
getEmail(org, name) {
|
|
1649
|
+
return this.json("GET", `${this.projectPath(org, name)}/email`);
|
|
1650
|
+
}
|
|
1651
|
+
/** Turns it on. **Idempotent** — we choose the address. The deploy reads `"email": true` and calls this. */
|
|
1652
|
+
grantEmail(org, name) {
|
|
1653
|
+
return this.json("POST", `${this.projectPath(org, name)}/email`);
|
|
1654
|
+
}
|
|
1655
|
+
/** Turns it off. Mail arriving at that address now gets a 550. */
|
|
1656
|
+
revokeEmail(org, name) {
|
|
1657
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/email`);
|
|
1658
|
+
}
|
|
1659
|
+
// ---- Custom sending domains (docs/email.md §2.4) ----
|
|
1660
|
+
/** The project's own sending domains, each with the records to publish. */
|
|
1661
|
+
listEmailDomains(org, name) {
|
|
1662
|
+
return this.json("GET", `${this.projectPath(org, name)}/email/domains`);
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Creates the provider identity and answers with the records to publish.
|
|
1666
|
+
* `publish` writes them into the zone when we host one covering the domain.
|
|
1667
|
+
*/
|
|
1668
|
+
addEmailDomain(org, name, domain, publish = false) {
|
|
1669
|
+
return this.json("POST", `${this.projectPath(org, name)}/email/domains`, { domain, publish });
|
|
1670
|
+
}
|
|
1671
|
+
/** Re-checks now. The background loop does the same once a minute while pending. */
|
|
1672
|
+
verifyEmailDomain(org, name, domain) {
|
|
1673
|
+
return this.json("POST", `${this.projectPath(org, name)}/email/domains/${encodeURIComponent(domain)}/verify`);
|
|
1674
|
+
}
|
|
1675
|
+
/** Deletes the identity and the row. Sending from it is refused from the next convergence. */
|
|
1676
|
+
removeEmailDomain(org, name, domain) {
|
|
1677
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/email/domains/${encodeURIComponent(domain)}`);
|
|
1678
|
+
}
|
|
1679
|
+
// ---- AI (docs/ai.md §6) ----
|
|
1680
|
+
/**
|
|
1681
|
+
* The model catalogue. **Being logged in is enough to see it** — what you can
|
|
1682
|
+
* buy is not an organization's secret, and this list is the price table.
|
|
1683
|
+
*
|
|
1684
|
+
* Prices are integer **micro-cents** per million tokens (1 µ¢ = 1e-8 USD).
|
|
1685
|
+
* Not letting the server pick the decimal place is the point — a small call
|
|
1686
|
+
* rounded to cents becomes 0.
|
|
1687
|
+
*/
|
|
1688
|
+
listAIModels() {
|
|
1689
|
+
return this.json("GET", "/v1/ai/models");
|
|
1690
|
+
}
|
|
1691
|
+
/** Grant, limits, the org balance, 24 h usage. The limits come back as a preview even without a grant. */
|
|
1692
|
+
getAI(org, name) {
|
|
1693
|
+
return this.json("GET", `${this.projectPath(org, name)}/ai`);
|
|
1694
|
+
}
|
|
1695
|
+
/** Turns it on. **Idempotent.** The deploy reads `"ai": true` and calls this. */
|
|
1696
|
+
grantAI(org, name) {
|
|
1697
|
+
return this.json("POST", `${this.projectPath(org, name)}/ai`);
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Lends the org's own Claude/Codex subscriptions to this project's
|
|
1701
|
+
* `anthropic/*` and `openai/*` calls (docs/studio-byos-ai.md §3.1).
|
|
1702
|
+
*
|
|
1703
|
+
* **Omitting a field leaves it as it was**, so turning lending on does not
|
|
1704
|
+
* clear a fallback that was already set. Admin only: the subscription that
|
|
1705
|
+
* carries the vendor-terms risk belongs to a person, not to the org.
|
|
1706
|
+
*/
|
|
1707
|
+
patchAI(org, name, body) {
|
|
1708
|
+
return this.json("PATCH", `${this.projectPath(org, name)}/ai`, body);
|
|
1709
|
+
}
|
|
1710
|
+
/** Turns it off. **The ledger and the balance are untouched** — the balance belongs to the org and the grant belongs to the project. */
|
|
1711
|
+
revokeAI(org, name) {
|
|
1712
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/ai`);
|
|
1713
|
+
}
|
|
1714
|
+
/** The org's credit balance and its automatic top-up settings. The balance is in micro-cents and can be negative. */
|
|
1715
|
+
getAICredit(org) {
|
|
1716
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/ai/credit`);
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* One page of this project's AI calls, newest first (docs/ai-observability.md §6.1).
|
|
1720
|
+
* `before` is the previous page's opaque `nextBefore`.
|
|
1721
|
+
*/
|
|
1722
|
+
listAICalls(org, name, opts = {}) {
|
|
1723
|
+
const q = new URLSearchParams();
|
|
1724
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
1725
|
+
if (v !== undefined && v !== "")
|
|
1726
|
+
q.set(k, String(v));
|
|
1727
|
+
}
|
|
1728
|
+
const qs = q.toString();
|
|
1729
|
+
return this.json("GET", `${this.projectPath(org, name)}/ai/calls${qs ? "?" + qs : ""}`);
|
|
1730
|
+
}
|
|
1731
|
+
/** One AI call; the input and output ride only for a reader with observability:pii. */
|
|
1732
|
+
getAICall(org, name, callId) {
|
|
1733
|
+
return this.json("GET", `${this.projectPath(org, name)}/ai/calls/${encodeURIComponent(callId)}`);
|
|
1734
|
+
}
|
|
1735
|
+
/** AI calls over time, by model, and the reasons lending did not serve (§6.3). */
|
|
1736
|
+
getAISummary(org, name, opts = {}) {
|
|
1737
|
+
const q = new URLSearchParams();
|
|
1738
|
+
if (opts.from)
|
|
1739
|
+
q.set("from", opts.from);
|
|
1740
|
+
if (opts.to)
|
|
1741
|
+
q.set("to", opts.to);
|
|
1742
|
+
const qs = q.toString();
|
|
1743
|
+
return this.json("GET", `${this.projectPath(org, name)}/ai/summary${qs ? "?" + qs : ""}`);
|
|
1744
|
+
}
|
|
1745
|
+
/** AI calls grouped by the app's `metadata.sessionId` (§6.4). */
|
|
1746
|
+
listAISessions(org, name, opts = {}) {
|
|
1747
|
+
const q = new URLSearchParams();
|
|
1748
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
1749
|
+
if (v !== undefined && v !== "")
|
|
1750
|
+
q.set(k, String(v));
|
|
1751
|
+
}
|
|
1752
|
+
const qs = q.toString();
|
|
1753
|
+
return this.json("GET", `${this.projectPath(org, name)}/ai/sessions${qs ? "?" + qs : ""}`);
|
|
1754
|
+
}
|
|
1755
|
+
/** The org members' desktops that can lend to this project right now (§6.5). */
|
|
1756
|
+
getAILenders(org, name) {
|
|
1757
|
+
return this.json("GET", `${this.projectPath(org, name)}/ai/lenders`);
|
|
1758
|
+
}
|
|
1759
|
+
/** Whether this project records prompts and answers (§5). */
|
|
1760
|
+
getAICapture(org, name) {
|
|
1761
|
+
return this.json("GET", `${this.projectPath(org, name)}/ai/capture`);
|
|
1762
|
+
}
|
|
1763
|
+
/** Turns prompt capture on or off. observability:admin; audited. */
|
|
1764
|
+
setAICapture(org, name, captureBodies) {
|
|
1765
|
+
return this.json("PUT", `${this.projectPath(org, name)}/ai/capture`, { captureBodies });
|
|
1766
|
+
}
|
|
1767
|
+
/** One page of the org ledger. The cursor is `ledger_id` — cutting on time splits the same millisecond. */
|
|
1768
|
+
listAILedger(org, opts = {}) {
|
|
1769
|
+
const q = new URLSearchParams();
|
|
1770
|
+
if (opts.limit)
|
|
1771
|
+
q.set("limit", String(opts.limit));
|
|
1772
|
+
if (opts.before)
|
|
1773
|
+
q.set("before", String(opts.before));
|
|
1774
|
+
if (opts.kind)
|
|
1775
|
+
q.set("kind", opts.kind);
|
|
1776
|
+
if (opts.since)
|
|
1777
|
+
q.set("since", opts.since);
|
|
1778
|
+
const qs = q.toString();
|
|
1779
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/ai/ledger${qs ? "?" + qs : ""}`);
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* The credit top-up checkout. **A browser coming back is not evidence** — the
|
|
1783
|
+
* one thing that raises the balance is the `order.paid` webhook, and the
|
|
1784
|
+
* screen polls the balance after the success event.
|
|
1785
|
+
*/
|
|
1786
|
+
createAICreditCheckout(org, microCents, returnTo) {
|
|
1787
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/ai/credit/checkout`, { microCents, returnTo });
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Turns automatic top-up on or off. **Turning it on is explicit consent**
|
|
1791
|
+
* (docs/ai.md §3.5) — the screen shows the charging terms verbatim and calls
|
|
1792
|
+
* this only after the box is checked.
|
|
1793
|
+
*/
|
|
1794
|
+
setAIAutoTopup(org, body) {
|
|
1795
|
+
return this.json("PUT", `/v1/orgs/${encodeURIComponent(org)}/ai/autotopup`, body);
|
|
1796
|
+
}
|
|
1797
|
+
/** One page of the mail log (docs/email.md §6.1). Newest first, cursor-paged. */
|
|
1798
|
+
listMailLog(org, name, opts = {}) {
|
|
1799
|
+
const q = new URLSearchParams();
|
|
1800
|
+
if (opts.direction)
|
|
1801
|
+
q.set("direction", opts.direction);
|
|
1802
|
+
if (opts.cursor)
|
|
1803
|
+
q.set("cursor", opts.cursor);
|
|
1804
|
+
if (opts.limit)
|
|
1805
|
+
q.set("limit", String(opts.limit));
|
|
1806
|
+
const qs = q.toString();
|
|
1807
|
+
return this.json("GET", `${this.projectPath(org, name)}/email/log${qs ? "?" + qs : ""}`);
|
|
1808
|
+
}
|
|
1809
|
+
/** The metadata of one message. */
|
|
1810
|
+
getMailLog(org, name, mailId) {
|
|
1811
|
+
return this.json("GET", `${this.projectPath(org, name)}/email/log/${encodeURIComponent(mailId)}`);
|
|
1812
|
+
}
|
|
1813
|
+
/** The URL of the raw message — the screen uses it as a download link (.eml). */
|
|
1814
|
+
mailLogRawURL(org, name, mailId) {
|
|
1815
|
+
return new URL(`${this.projectPath(org, name)}/email/log/${encodeURIComponent(mailId)}/raw`, this.opts.baseUrl).toString();
|
|
1816
|
+
}
|
|
1817
|
+
/** The raw text. What the detail view draws the headers and body from. */
|
|
1818
|
+
getMailLogRaw(org, name, mailId) {
|
|
1819
|
+
return this.text("GET", `${this.projectPath(org, name)}/email/log/${encodeURIComponent(mailId)}/raw`);
|
|
1820
|
+
}
|
|
1821
|
+
// ---- End-user authentication (docs/auth.md §9) ----
|
|
1822
|
+
/**
|
|
1823
|
+
* Grant, settings, providers, hosts. **The signed-up users do not come** —
|
|
1824
|
+
* that table is inside the project DB (docs/auth.md §6) and only the worker's
|
|
1825
|
+
* `env.auth` sees it.
|
|
285
1826
|
*
|
|
286
|
-
* `providers`
|
|
287
|
-
*
|
|
1827
|
+
* `providers` carries the client id only. The client secret is sealed and
|
|
1828
|
+
* appears in no response.
|
|
288
1829
|
*/
|
|
289
1830
|
getAuth(org, name) {
|
|
290
1831
|
return this.json("GET", `${this.projectPath(org, name)}/auth`);
|
|
291
1832
|
}
|
|
292
1833
|
/**
|
|
293
|
-
*
|
|
1834
|
+
* Turns auth on (member). Idempotent.
|
|
294
1835
|
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
1836
|
+
* **With no database it is 409 `database_required`** — the user table lives
|
|
1837
|
+
* inside the project DB, so standing up the grant alone leaves no table the
|
|
1838
|
+
* moment the worker starts. The caller catches that code and points at the
|
|
1839
|
+
* `"database": true` declaration — the deploy never sees this code, because
|
|
1840
|
+
* its ordering rules it out.
|
|
298
1841
|
*/
|
|
299
1842
|
grantAuth(org, name) {
|
|
300
1843
|
return this.json("POST", `${this.projectPath(org, name)}/auth`);
|
|
301
1844
|
}
|
|
302
1845
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
1846
|
+
* The app's end users, newest first (`runlot auth users`). Read-only, and read
|
|
1847
|
+
* from the project's own database — a parked or unreachable one answers 502/504.
|
|
1848
|
+
* Feed `cursor` back unchanged; its absence is the end.
|
|
1849
|
+
*/
|
|
1850
|
+
listAppUsers(org, name, opts = {}) {
|
|
1851
|
+
const q = new URLSearchParams();
|
|
1852
|
+
if (opts.search)
|
|
1853
|
+
q.set("search", opts.search);
|
|
1854
|
+
if (opts.cursor)
|
|
1855
|
+
q.set("cursor", opts.cursor);
|
|
1856
|
+
if (opts.limit)
|
|
1857
|
+
q.set("limit", String(opts.limit));
|
|
1858
|
+
const qs = q.toString();
|
|
1859
|
+
return this.json("GET", `${this.projectPath(org, name)}/auth/users${qs ? "?" + qs : ""}`);
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* One `env.ai.run` on the development lane: catalog models, the free plan's caps,
|
|
1863
|
+
* the org's credit, recorded as `lane = dev`. One JSON answer, no streaming.
|
|
1864
|
+
*/
|
|
1865
|
+
devAIRun(org, name, body) {
|
|
1866
|
+
return this.json("POST", `${this.projectPath(org, name)}/ai/dev/run`, body);
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Changes the settings (member). **A partial update** — only the fields given
|
|
1870
|
+
* change. Sending the whole thing on one switch means that when two people
|
|
1871
|
+
* touch the same card, the later save overwrites the earlier one. With no
|
|
1872
|
+
* grant it is 404 `not_granted`.
|
|
306
1873
|
*/
|
|
307
1874
|
updateAuthSettings(org, name, settings) {
|
|
308
1875
|
return this.json("PATCH", `${this.projectPath(org, name)}/auth`, { settings });
|
|
309
1876
|
}
|
|
310
1877
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
1878
|
+
* Turns auth off (admin). **The signed-up users are not deleted** — the
|
|
1879
|
+
* `runlot_auth` tables stay in the project DB, and turning it back on lets the
|
|
1880
|
+
* same people log in unchanged.
|
|
313
1881
|
*/
|
|
314
1882
|
revokeAuth(org, name) {
|
|
315
1883
|
return this.json("DELETE", `${this.projectPath(org, name)}/auth`);
|
|
316
1884
|
}
|
|
317
1885
|
/**
|
|
318
|
-
*
|
|
1886
|
+
* Registers or replaces a social provider (admin). It is the same PUT, so it
|
|
1887
|
+
* is idempotent.
|
|
319
1888
|
*
|
|
320
|
-
* **`clientSecret`
|
|
321
|
-
* `https://<host>/__runlot/auth/callback/<provider>`
|
|
322
|
-
*
|
|
1889
|
+
* **`clientSecret` cannot be read back.** The callback URL is
|
|
1890
|
+
* `https://<host>/__runlot/auth/callback/<provider>` for each host, and the
|
|
1891
|
+
* list of hosts is in the response's `hosts`.
|
|
1892
|
+
*
|
|
1893
|
+
* `issuer` is the base URL of your own GitHub Enterprise Server installation,
|
|
1894
|
+
* and only `github` takes one. **This PUT is the whole row** — the same reason
|
|
1895
|
+
* `clientSecret` has to be sent every time — so an empty string clears it and
|
|
1896
|
+
* puts that provider back on github.com. The key is left out entirely when the
|
|
1897
|
+
* argument is omitted, so a caller that does not know about issuers sends the
|
|
1898
|
+
* body it always sent.
|
|
1899
|
+
*/
|
|
1900
|
+
setAuthProvider(org, name, provider, clientId, clientSecret, issuer) {
|
|
1901
|
+
return this.json("PUT", `${this.projectPath(org, name)}/auth/providers/${encodeURIComponent(provider)}`, { clientId, clientSecret, ...(issuer === undefined ? {} : { issuer }) });
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Turns on the Runlot provider (admin): "Continue with Runlot", with email,
|
|
1905
|
+
* Google and GitHub at Runlot behind it and nothing to register. 409
|
|
1906
|
+
* `runlot_unavailable` when this deployment has no sign-in broker.
|
|
1907
|
+
*/
|
|
1908
|
+
setRunlotAuthProvider(org, name) {
|
|
1909
|
+
return this.json("PUT", `${this.projectPath(org, name)}/auth/providers/runlot`, { kind: "runlot" });
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Registers or replaces a generic OIDC provider under a project-chosen id
|
|
1913
|
+
* (admin). The CP reads `<issuer>/.well-known/openid-configuration` when
|
|
1914
|
+
* saving; a failure is 400 `oidc_discovery_failed` quoting the provider.
|
|
1915
|
+
*/
|
|
1916
|
+
setOidcAuthProvider(org, name, id, body) {
|
|
1917
|
+
return this.json("PUT", `${this.projectPath(org, name)}/auth/providers/${encodeURIComponent(id)}`, { kind: "oidc", ...body });
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* The statements that create the `runlot_auth` schema (member). 404 when
|
|
1921
|
+
* login is not turned on for the project.
|
|
1922
|
+
*/
|
|
1923
|
+
getAuthSchema(org, name) {
|
|
1924
|
+
return this.json("GET", `${this.projectPath(org, name)}/auth/schema`);
|
|
1925
|
+
}
|
|
1926
|
+
/** The project's sign-in branding with defaults filled (docs/custom-sign-in.md §12.3). */
|
|
1927
|
+
getAuthBranding(org, name) {
|
|
1928
|
+
return this.getAuthBrandingAnswer(org, name).then((a) => a.brand);
|
|
1929
|
+
}
|
|
1930
|
+
/** The GET answer whole: the filled brand plus the methods the chooser would draw now. */
|
|
1931
|
+
getAuthBrandingAnswer(org, name) {
|
|
1932
|
+
return this.json("GET", `${this.projectPath(org, name)}/auth/branding`);
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Replaces the whole branding object (member). Validated server-side; a bad
|
|
1936
|
+
* field is 400 `invalid_branding` with `fields: [{path, message}]`.
|
|
1937
|
+
*/
|
|
1938
|
+
putAuthBranding(org, name, branding) {
|
|
1939
|
+
return this.json("PUT", `${this.projectPath(org, name)}/auth/branding`, branding)
|
|
1940
|
+
.then((a) => a.brand);
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* The URL of the rendered chooser with (draft) branding (§12.4). A page, not
|
|
1944
|
+
* JSON: the dashboard puts it in an iframe and the CLI opens it in a browser.
|
|
1945
|
+
* `draft` travels as base64url JSON.
|
|
323
1946
|
*/
|
|
324
|
-
|
|
325
|
-
|
|
1947
|
+
authBrandingPreviewUrl(org, name, opts = {}) {
|
|
1948
|
+
const u = new URL(`${this.projectPath(org, name)}/auth/branding/preview`, this.opts.baseUrl);
|
|
1949
|
+
if (opts.lang)
|
|
1950
|
+
u.searchParams.set("lang", opts.lang);
|
|
1951
|
+
if (opts.theme)
|
|
1952
|
+
u.searchParams.set("theme", opts.theme);
|
|
1953
|
+
if (opts.state)
|
|
1954
|
+
u.searchParams.set("state", opts.state);
|
|
1955
|
+
if (opts.draft)
|
|
1956
|
+
u.searchParams.set("draft", base64urlJson(opts.draft));
|
|
1957
|
+
return u.toString();
|
|
1958
|
+
}
|
|
1959
|
+
/** Sign-in funnel counters for the last `days` days (§9.4). No per-person data. */
|
|
1960
|
+
getAuthFunnel(org, name, days = 30) {
|
|
1961
|
+
return this.json("GET", `${this.projectPath(org, name)}/auth/funnel?days=${encodeURIComponent(String(days))}`);
|
|
326
1962
|
}
|
|
327
1963
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
1964
|
+
* Removes a provider (admin). Someone who signed up only through that provider
|
|
1965
|
+
* loses their way in, but the user row stays — register it again and they come
|
|
1966
|
+
* back to the same account.
|
|
330
1967
|
*/
|
|
331
1968
|
deleteAuthProvider(org, name, provider) {
|
|
332
1969
|
return this.json("DELETE", `${this.projectPath(org, name)}/auth/providers/${encodeURIComponent(provider)}`);
|
|
333
1970
|
}
|
|
334
|
-
// ----
|
|
1971
|
+
// ---- Deployment access control (docs/access.md §7) ----
|
|
335
1972
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
1973
|
+
* Who can reach this deployment. **No values** — both the password and the
|
|
1974
|
+
* bypass secret live only as hashes, and what comes back here is the fact that
|
|
1975
|
+
* there is one: `hasPassword` and `hasBypass`.
|
|
338
1976
|
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
1977
|
+
* A project that has never set a policy is `mode: "public"` with no
|
|
1978
|
+
* `updatedAt`. The screen tells those two apart for the reason in §7 — a
|
|
1979
|
+
* sentence about the state instead of an empty state.
|
|
341
1980
|
*/
|
|
342
1981
|
getAccess(org, name) {
|
|
343
1982
|
return this.json("GET", `${this.projectPath(org, name)}/access`);
|
|
344
1983
|
}
|
|
345
1984
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
1985
|
+
* Changes the policy (admin). Without a `password`, **the existing password
|
|
1986
|
+
* stays** — if a save that only changes the mode wiped the password, coming
|
|
1987
|
+
* back to that mode would mean setting it again.
|
|
348
1988
|
*/
|
|
349
1989
|
setAccess(org, name, mode, password) {
|
|
350
1990
|
return this.json("PUT", `${this.projectPath(org, name)}/access`, { mode, password });
|
|
351
1991
|
}
|
|
352
1992
|
/**
|
|
353
|
-
*
|
|
354
|
-
*
|
|
1993
|
+
* Makes a bypass secret for automation (admin). **The value is in this answer
|
|
1994
|
+
* and nowhere else** — the screen shows it once on arrival and can never fetch
|
|
1995
|
+
* it again. Calling again kills the old value.
|
|
355
1996
|
*/
|
|
356
1997
|
newAccessBypass(org, name) {
|
|
357
1998
|
return this.json("POST", `${this.projectPath(org, name)}/access/bypass`);
|
|
358
1999
|
}
|
|
359
|
-
/**
|
|
2000
|
+
/** Removes the bypass (admin). Removing one that does not exist also succeeds. */
|
|
360
2001
|
revokeAccessBypass(org, name) {
|
|
361
2002
|
return this.json("DELETE", `${this.projectPath(org, name)}/access/bypass`);
|
|
362
2003
|
}
|
|
363
2004
|
/**
|
|
364
|
-
*
|
|
2005
|
+
* Gets a one-time address back into a protected deployment (docs/access.md §3.4-3).
|
|
365
2006
|
*
|
|
366
|
-
*
|
|
367
|
-
* (
|
|
368
|
-
*
|
|
2007
|
+
* It is **not** under the project path: all the calling screen knows is the
|
|
2008
|
+
* hostname (the original request went there), and the server derives the org
|
|
2009
|
+
* and project names from it. Doing `location.replace` with the `redirect` that
|
|
2010
|
+
* comes back is the caller's job.
|
|
369
2011
|
*/
|
|
370
2012
|
authorizeAccess(to, next) {
|
|
371
2013
|
return this.json("POST", "/v1/access/authorize", { to, next });
|
|
@@ -373,44 +2015,155 @@ export class Client {
|
|
|
373
2015
|
listMembers(org) {
|
|
374
2016
|
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/members`);
|
|
375
2017
|
}
|
|
376
|
-
/**
|
|
2018
|
+
/** This is not an invite — the user must already have logged in once (404 `user_not_found`). */
|
|
377
2019
|
addMember(org, provider, subject, role) {
|
|
378
2020
|
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/members`, { provider, subject, role });
|
|
379
2021
|
}
|
|
2022
|
+
// ---- Project authorization (docs/authorization.md §3) ----
|
|
2023
|
+
getProjectPermissions(org, name) {
|
|
2024
|
+
return this.json("GET", `${this.projectPath(org, name)}/permissions`);
|
|
2025
|
+
}
|
|
2026
|
+
listProjectMembers(org, name) {
|
|
2027
|
+
return this.json("GET", `${this.projectPath(org, name)}/members`);
|
|
2028
|
+
}
|
|
2029
|
+
putProjectMember(org, name, userId, body) {
|
|
2030
|
+
return this.json("PUT", `${this.projectPath(org, name)}/members/${encodeURIComponent(userId)}`, body);
|
|
2031
|
+
}
|
|
2032
|
+
deleteProjectMember(org, name, userId) {
|
|
2033
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/members/${encodeURIComponent(userId)}`);
|
|
2034
|
+
}
|
|
2035
|
+
putProjectTeam(org, name, team, body) {
|
|
2036
|
+
return this.json("PUT", `${this.projectPath(org, name)}/teams/${encodeURIComponent(team)}`, body);
|
|
2037
|
+
}
|
|
2038
|
+
deleteProjectTeam(org, name, team) {
|
|
2039
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/teams/${encodeURIComponent(team)}`);
|
|
2040
|
+
}
|
|
2041
|
+
patchProjectAccessSettings(org, name, body) {
|
|
2042
|
+
return this.json("PATCH", `${this.projectPath(org, name)}/access-settings`, body);
|
|
2043
|
+
}
|
|
2044
|
+
putProjectOwner(org, name, userId) {
|
|
2045
|
+
return this.json("PUT", `${this.projectPath(org, name)}/owner`, { userId });
|
|
2046
|
+
}
|
|
2047
|
+
listAccessRequests(org, name) {
|
|
2048
|
+
return this.json("GET", `${this.projectPath(org, name)}/access-requests`);
|
|
2049
|
+
}
|
|
2050
|
+
createAccessRequest(org, name, body) {
|
|
2051
|
+
return this.json("POST", `${this.projectPath(org, name)}/access-requests`, body);
|
|
2052
|
+
}
|
|
2053
|
+
grantAccessRequest(org, name, id, body = {}) {
|
|
2054
|
+
return this.json("POST", `${this.projectPath(org, name)}/access-requests/${encodeURIComponent(id)}/grant`, body);
|
|
2055
|
+
}
|
|
2056
|
+
denyAccessRequest(org, name, id, reason) {
|
|
2057
|
+
return this.json("POST", `${this.projectPath(org, name)}/access-requests/${encodeURIComponent(id)}/deny`, reason ? { reason } : {});
|
|
2058
|
+
}
|
|
2059
|
+
listTeams(org) {
|
|
2060
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/teams`);
|
|
2061
|
+
}
|
|
2062
|
+
createTeam(org, slug, name) {
|
|
2063
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/teams`, { slug, name });
|
|
2064
|
+
}
|
|
2065
|
+
deleteTeam(org, slug) {
|
|
2066
|
+
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}`);
|
|
2067
|
+
}
|
|
2068
|
+
addTeamMember(org, slug, userId) {
|
|
2069
|
+
return this.json("PUT", `/v1/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/members/${encodeURIComponent(userId)}`);
|
|
2070
|
+
}
|
|
2071
|
+
removeTeamMember(org, slug, userId) {
|
|
2072
|
+
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/members/${encodeURIComponent(userId)}`);
|
|
2073
|
+
}
|
|
2074
|
+
getGovernance(org) {
|
|
2075
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/governance`);
|
|
2076
|
+
}
|
|
2077
|
+
putGovernance(org, body) {
|
|
2078
|
+
return this.json("PUT", `/v1/orgs/${encodeURIComponent(org)}/governance`, body);
|
|
2079
|
+
}
|
|
2080
|
+
/** The organization's audit log (admin, docs/b2b-governance.md G1). */
|
|
2081
|
+
listOrgAudit(org, q = {}) {
|
|
2082
|
+
const p = new URLSearchParams();
|
|
2083
|
+
for (const [k, v] of Object.entries(q))
|
|
2084
|
+
if (v !== undefined && v !== "")
|
|
2085
|
+
p.set(k, String(v));
|
|
2086
|
+
const qs = p.toString();
|
|
2087
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/audit${qs ? `?${qs}` : ""}`);
|
|
2088
|
+
}
|
|
2089
|
+
listApprovals(org) {
|
|
2090
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/approvals`);
|
|
2091
|
+
}
|
|
2092
|
+
approveRequest(org, id, comment) {
|
|
2093
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/approvals/${encodeURIComponent(id)}/approve`, comment ? { comment } : {});
|
|
2094
|
+
}
|
|
2095
|
+
rejectRequest(org, id, comment) {
|
|
2096
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/approvals/${encodeURIComponent(id)}/reject`, comment ? { comment } : {});
|
|
2097
|
+
}
|
|
380
2098
|
setMemberRole(org, userId, role) {
|
|
381
2099
|
return this.json("PUT", `/v1/orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(userId)}`, { role });
|
|
382
2100
|
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Removing someone else is admin; **removing yourself is leaving**, which
|
|
2103
|
+
* anyone can do (docs/members.md). The last admin can do neither
|
|
2104
|
+
* (409 `last_admin`).
|
|
2105
|
+
*/
|
|
383
2106
|
removeMember(org, userId) {
|
|
384
2107
|
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(userId)}`);
|
|
385
2108
|
}
|
|
2109
|
+
// --- Invites (docs/members.md) ---
|
|
2110
|
+
/** Pending and expired invites (admin). Accepted and revoked ones are history, so they do not come. */
|
|
2111
|
+
listInvites(org) {
|
|
2112
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/invites`);
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Invites by email (admin). **Calling again with the same address is a
|
|
2116
|
+
* resend** — the same row is updated rather than a new one created, and the
|
|
2117
|
+
* server gives 200 instead of 201.
|
|
2118
|
+
*/
|
|
2119
|
+
createInvite(org, email, role) {
|
|
2120
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/invites`, { email, role });
|
|
2121
|
+
}
|
|
2122
|
+
/** Revokes an invite (admin). Mail that has not gone out yet does not go out. */
|
|
2123
|
+
revokeInvite(org, inviteId) {
|
|
2124
|
+
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}/invites/${encodeURIComponent(inviteId)}`);
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* A preview of the invite. **No session needed** — whoever got the link has
|
|
2128
|
+
* not logged in yet, and they need to know what they are accepting before
|
|
2129
|
+
* they do.
|
|
2130
|
+
*/
|
|
2131
|
+
getInvite(token) {
|
|
2132
|
+
return this.json("GET", `/v1/invites/${encodeURIComponent(token)}`);
|
|
2133
|
+
}
|
|
2134
|
+
/** Accepts (login required). If they are already a member the role does not change. */
|
|
2135
|
+
acceptInvite(token) {
|
|
2136
|
+
return this.json("POST", `/v1/invites/${encodeURIComponent(token)}/accept`);
|
|
2137
|
+
}
|
|
386
2138
|
// --- domains: D1 hostnames methods ---
|
|
387
|
-
/**
|
|
2139
|
+
/** The default hostname and the custom domains come together (viewer). */
|
|
388
2140
|
listHostnames(org, name) {
|
|
389
2141
|
return this.json("GET", `${this.projectPath(org, name)}/hostnames`);
|
|
390
2142
|
}
|
|
391
2143
|
/**
|
|
392
|
-
*
|
|
2144
|
+
* Attaches a custom domain (member).
|
|
393
2145
|
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
2146
|
+
* Idempotent — sending the same name again returns the existing row
|
|
2147
|
+
* unchanged. The response's `verify` is the CNAME and TXT the customer has to
|
|
2148
|
+
* add.
|
|
396
2149
|
*/
|
|
397
|
-
addHostname(org, name, hostname) {
|
|
398
|
-
return this.json("POST", `${this.projectPath(org, name)}/hostnames`, { hostname });
|
|
2150
|
+
addHostname(org, name, hostname, role) {
|
|
2151
|
+
return this.json("POST", `${this.projectPath(org, name)}/hostnames`, role === undefined ? { hostname } : { hostname, role });
|
|
399
2152
|
}
|
|
400
|
-
/**
|
|
2153
|
+
/** Runs the same check the background verifier makes, right now (member). */
|
|
401
2154
|
verifyHostname(org, name, host) {
|
|
402
2155
|
return this.json("POST", `${this.projectPath(org, name)}/hostnames/${encodeURIComponent(host)}/verify`);
|
|
403
2156
|
}
|
|
404
|
-
/**
|
|
2157
|
+
/** Detaches a custom domain (member). The default hostname gives a 400. */
|
|
405
2158
|
deleteHostname(org, name, host) {
|
|
406
2159
|
return this.json("DELETE", `${this.projectPath(org, name)}/hostnames/${encodeURIComponent(host)}`);
|
|
407
2160
|
}
|
|
408
2161
|
// --- domains: D2 dns methods ---
|
|
409
|
-
/**
|
|
2162
|
+
/** The organization's zones. No `rrsets` — those ride only on a single-zone lookup. */
|
|
410
2163
|
listZones(org) {
|
|
411
2164
|
return this.json("GET", this.dnsPath(org));
|
|
412
2165
|
}
|
|
413
|
-
/**
|
|
2166
|
+
/** Idempotent. A zone that already exists comes back unchanged. */
|
|
414
2167
|
createZone(org, name) {
|
|
415
2168
|
return this.json("POST", this.dnsPath(org), { name });
|
|
416
2169
|
}
|
|
@@ -421,8 +2174,9 @@ export class Client {
|
|
|
421
2174
|
return this.json("DELETE", `${this.dnsPath(org)}/${encodeURIComponent(zone)}`);
|
|
422
2175
|
}
|
|
423
2176
|
/**
|
|
424
|
-
*
|
|
425
|
-
*
|
|
2177
|
+
* Replaces one rrset **whole**. `records` is the complete new set for that
|
|
2178
|
+
* (name, type), so adding a single value means sending the current ones with
|
|
2179
|
+
* it.
|
|
426
2180
|
*/
|
|
427
2181
|
putRRset(org, zone, name, type, ttl, records) {
|
|
428
2182
|
return this.json("PUT", this.rrsetPath(org, zone, name, type), { ttl, records });
|
|
@@ -438,39 +2192,49 @@ export class Client {
|
|
|
438
2192
|
setZoneDS(org, zone, ds) {
|
|
439
2193
|
return this.json("PUT", `${this.dnsPath(org)}/${encodeURIComponent(zone)}/dnssec/ds`, { ds });
|
|
440
2194
|
}
|
|
441
|
-
/**
|
|
2195
|
+
/** The shared prefix of the DNS surface. */
|
|
442
2196
|
dnsPath(org) {
|
|
443
2197
|
return `/v1/orgs/${encodeURIComponent(org)}/dns/zones`;
|
|
444
2198
|
}
|
|
445
|
-
/** `@`
|
|
2199
|
+
/** Both `@` and `*` arrive as a name. Keeps the same string out of four places. */
|
|
446
2200
|
rrsetPath(org, zone, name, type) {
|
|
447
2201
|
return (`${this.dnsPath(org)}/${encodeURIComponent(zone)}` +
|
|
448
2202
|
`/rrsets/${encodeURIComponent(name)}/${encodeURIComponent(type)}`);
|
|
449
2203
|
}
|
|
450
2204
|
// --- domains: D3 domains methods ---
|
|
451
2205
|
/**
|
|
452
|
-
*
|
|
2206
|
+
* Looks a name up (member or above).
|
|
453
2207
|
*
|
|
454
|
-
* `priceCents`
|
|
455
|
-
*
|
|
456
|
-
*
|
|
2208
|
+
* `priceCents` is the first year of registration; `renewalCents` is one year
|
|
2209
|
+
* of renewal. They are separate because a first-year discount is common —
|
|
2210
|
+
* folded into one, the screen cannot say "buy it for $1, then $15 next year".
|
|
457
2211
|
*/
|
|
458
2212
|
checkDomains(org, names) {
|
|
459
2213
|
return this.json("POST", `${this.domainsPath(org)}/check`, { names });
|
|
460
2214
|
}
|
|
461
2215
|
/**
|
|
462
|
-
*
|
|
2216
|
+
* Buys a domain (admin — this spends money).
|
|
463
2217
|
*
|
|
464
|
-
* **
|
|
465
|
-
*
|
|
2218
|
+
* **Payment comes first** (docs/domains.md §5.8). With a saved payment method
|
|
2219
|
+
* the answer is the registered domain; otherwise a `DomainCharge` whose
|
|
2220
|
+
* `checkoutUrl` is where to pay — follow it with `getDomainCharge`. Paying also
|
|
2221
|
+
* agrees to the automatic yearly renewal charge.
|
|
466
2222
|
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
2223
|
+
* **The ICANN verification mail goes to `contact.email`.** The screen has to
|
|
2224
|
+
* say so before the purchase (docs/domains.md §5.5).
|
|
469
2225
|
*/
|
|
470
2226
|
purchaseDomain(org, body) {
|
|
471
2227
|
return this.json("POST", this.domainsPath(org), body);
|
|
472
2228
|
}
|
|
473
|
-
/**
|
|
2229
|
+
/** One domain payment — how a `DomainCharge` from purchase or renewal is followed. */
|
|
2230
|
+
getDomainCharge(org, chargeId) {
|
|
2231
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/domain-charges/${encodeURIComponent(chargeId)}`);
|
|
2232
|
+
}
|
|
2233
|
+
/** Domain payments, newest first. */
|
|
2234
|
+
listDomainCharges(org) {
|
|
2235
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/domain-charges`);
|
|
2236
|
+
}
|
|
2237
|
+
/** Imports a domain you already own (admin). Creates the zone only; the registrar is not called. */
|
|
474
2238
|
importDomain(org, name) {
|
|
475
2239
|
return this.json("POST", `${this.domainsPath(org)}/import`, { name });
|
|
476
2240
|
}
|
|
@@ -480,15 +2244,13 @@ export class Client {
|
|
|
480
2244
|
getDomain(org, name) {
|
|
481
2245
|
return this.json("GET", this.domainPath(org, name));
|
|
482
2246
|
}
|
|
483
|
-
|
|
484
|
-
|
|
2247
|
+
/** Renews now, paid the same way as a purchase. */
|
|
2248
|
+
renewDomain(org, name, years, pay = {}) {
|
|
2249
|
+
return this.json("POST", `${this.domainPath(org, name)}/renew`, { years, ...pay });
|
|
485
2250
|
}
|
|
486
2251
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* 등록기관 쪽 자동 갱신은 그대로 켜져 있어요 — 도메인은 하루 늦으면 잃는
|
|
490
|
-
* 자산이라 우리가 먼저 갱신하고 뒤에 청구해요. 화면은 이 사실을 함께
|
|
491
|
-
* 말해야 해요.
|
|
2252
|
+
* Turns automatic renewal on or off. On: one year is charged 30 days before
|
|
2253
|
+
* expiry. Off: nothing is charged and the domain lapses at expiry.
|
|
492
2254
|
*/
|
|
493
2255
|
setDomainAutoRenew(org, name, enabled) {
|
|
494
2256
|
return this.json("POST", `${this.domainPath(org, name)}/auto-renew`, { enabled });
|
|
@@ -496,37 +2258,37 @@ export class Client {
|
|
|
496
2258
|
setDomainLock(org, name, locked) {
|
|
497
2259
|
return this.json("POST", `${this.domainPath(org, name)}/lock`, { locked });
|
|
498
2260
|
}
|
|
499
|
-
/**
|
|
2261
|
+
/** The transfer auth code. The audit records that one was issued and **not the code itself**. */
|
|
500
2262
|
domainAuthCode(org, name) {
|
|
501
2263
|
return this.json("POST", `${this.domainPath(org, name)}/auth-code`);
|
|
502
2264
|
}
|
|
503
|
-
/**
|
|
2265
|
+
/** Re-reads from the registrar right now (member). */
|
|
504
2266
|
syncDomain(org, name) {
|
|
505
2267
|
return this.json("POST", `${this.domainPath(org, name)}/sync`);
|
|
506
2268
|
}
|
|
507
|
-
/**
|
|
2269
|
+
/** The shared prefix of the domain surface. Keeps the same string out of ten places. */
|
|
508
2270
|
domainsPath(org) {
|
|
509
2271
|
return `/v1/orgs/${encodeURIComponent(org)}/domains`;
|
|
510
2272
|
}
|
|
511
2273
|
domainPath(org, name) {
|
|
512
2274
|
return `${this.domainsPath(org)}/${encodeURIComponent(name)}`;
|
|
513
2275
|
}
|
|
514
|
-
/**
|
|
2276
|
+
/** The shared prefix of the project surface. */
|
|
515
2277
|
projectPath(org, name) {
|
|
516
2278
|
return `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}`;
|
|
517
2279
|
}
|
|
518
|
-
/**
|
|
2280
|
+
/** The shared prefix of the database surface. Keeps the same string out of five places. */
|
|
519
2281
|
dbPath(org, name) {
|
|
520
|
-
return
|
|
2282
|
+
return dbPath(org, name);
|
|
521
2283
|
}
|
|
522
|
-
// ----
|
|
2284
|
+
// ---- Self-hosted Git (docs/git-hosting.md §12.1, §19) ----
|
|
523
2285
|
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
2286
|
+
// **git does the repository work and we handle only the metadata.** That is
|
|
2287
|
+
// why there is nothing like `pushRepo` here — an API that wraps git either
|
|
2288
|
+
// reimplements every one of git's flags or boxes the user in.
|
|
527
2289
|
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
2290
|
+
// A repository **belongs to a project**. That is why every method starts with
|
|
2291
|
+
// (org, project) — the same shape as the database and the secrets.
|
|
530
2292
|
listRepos(org, project) {
|
|
531
2293
|
return this.json("GET", this.repoPath(org, project));
|
|
532
2294
|
}
|
|
@@ -542,10 +2304,43 @@ export class Client {
|
|
|
542
2304
|
deleteRepo(org, project, name) {
|
|
543
2305
|
return this.json("DELETE", `${this.repoPath(org, project)}/${encodeURIComponent(name)}`);
|
|
544
2306
|
}
|
|
545
|
-
/** ref
|
|
2307
|
+
/** The ref history. The lead for recovering a commit lost to a force-push is here. */
|
|
546
2308
|
listRefUpdates(org, project, name) {
|
|
547
2309
|
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/ref-updates`);
|
|
548
2310
|
}
|
|
2311
|
+
// Push-to-deploy (docs/code/push-to-deploy). The hook hangs off the
|
|
2312
|
+
// repository and the builds off the **project**, because a build outlives the
|
|
2313
|
+
// repository it came from.
|
|
2314
|
+
getDeployHook(org, project, name) {
|
|
2315
|
+
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/deploy-hook`);
|
|
2316
|
+
}
|
|
2317
|
+
setDeployHook(org, project, name, body) {
|
|
2318
|
+
return this.json("PUT", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/deploy-hook`, body);
|
|
2319
|
+
}
|
|
2320
|
+
deleteDeployHook(org, project, name) {
|
|
2321
|
+
return this.json("DELETE", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/deploy-hook`);
|
|
2322
|
+
}
|
|
2323
|
+
/** Build a commit without pushing. Omit `commitSha` for the hooked branch's tip. */
|
|
2324
|
+
deployRepoNow(org, project, name, commitSha) {
|
|
2325
|
+
return this.json("POST", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/deploy`, commitSha === undefined ? {} : { commitSha });
|
|
2326
|
+
}
|
|
2327
|
+
listBuilds(org, project, limit) {
|
|
2328
|
+
const q = limit === undefined ? "" : `?limit=${limit}`;
|
|
2329
|
+
return this.json("GET", `${this.projectPath(org, project)}/builds${q}`);
|
|
2330
|
+
}
|
|
2331
|
+
getBuild(org, project, buildId) {
|
|
2332
|
+
return this.json("GET", `${this.projectPath(org, project)}/builds/${encodeURIComponent(buildId)}`);
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* A build's output. It comes back as **text**, not JSON — see the endpoint's
|
|
2336
|
+
* description. `json()` would try to parse it and fail on the first line.
|
|
2337
|
+
*/
|
|
2338
|
+
getBuildLog(org, project, buildId) {
|
|
2339
|
+
return this.text("GET", `${this.projectPath(org, project)}/builds/${encodeURIComponent(buildId)}/log`);
|
|
2340
|
+
}
|
|
2341
|
+
cancelBuild(org, project, buildId) {
|
|
2342
|
+
return this.json("POST", `${this.projectPath(org, project)}/builds/${encodeURIComponent(buildId)}/cancel`);
|
|
2343
|
+
}
|
|
549
2344
|
listSSHKeys() {
|
|
550
2345
|
return this.json("GET", "/v1/me/ssh-keys");
|
|
551
2346
|
}
|
|
@@ -558,14 +2353,14 @@ export class Client {
|
|
|
558
2353
|
listAccessTokens() {
|
|
559
2354
|
return this.json("GET", "/v1/me/tokens");
|
|
560
2355
|
}
|
|
561
|
-
/**
|
|
2356
|
+
/** The plaintext is carried in this response **once** and never again. */
|
|
562
2357
|
createAccessToken(body) {
|
|
563
2358
|
return this.json("POST", "/v1/me/tokens", body);
|
|
564
2359
|
}
|
|
565
2360
|
revokeAccessToken(tokenId) {
|
|
566
2361
|
return this.json("DELETE", `/v1/me/tokens/${encodeURIComponent(tokenId)}`);
|
|
567
2362
|
}
|
|
568
|
-
// ----
|
|
2363
|
+
// ---- Browsing (docs/git-hosting.md §12.2) ----
|
|
569
2364
|
getRepoRefs(org, project, name) {
|
|
570
2365
|
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/refs`);
|
|
571
2366
|
}
|
|
@@ -584,7 +2379,7 @@ export class Client {
|
|
|
584
2379
|
const qs = q.size > 0 ? `?${q.toString()}` : "";
|
|
585
2380
|
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/commits${qs}`);
|
|
586
2381
|
}
|
|
587
|
-
/**
|
|
2382
|
+
/** The shared prefix of the repository surface. It sits under the project (§19). */
|
|
588
2383
|
repoPath(org, project) {
|
|
589
2384
|
return `${this.projectPath(org, project)}/repos`;
|
|
590
2385
|
}
|
|
@@ -592,4 +2387,12 @@ export class Client {
|
|
|
592
2387
|
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/rollback`, { version });
|
|
593
2388
|
}
|
|
594
2389
|
}
|
|
2390
|
+
/** A query string from the defined, non-empty options. */
|
|
2391
|
+
function obsQS(opts) {
|
|
2392
|
+
const q = new URLSearchParams();
|
|
2393
|
+
for (const [k, v] of Object.entries(opts))
|
|
2394
|
+
if (v)
|
|
2395
|
+
q.set(k, v);
|
|
2396
|
+
return q.size ? `?${q}` : "";
|
|
2397
|
+
}
|
|
595
2398
|
//# sourceMappingURL=index.js.map
|