@runlot/api 0.1.0-rc.14 → 0.1.0-rc.140
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 +710 -112
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +884 -116
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +12143 -2747
- package/dist/schema.d.ts.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
function gitQuery(ref, path) {
|
|
4
|
+
const q = new URLSearchParams();
|
|
5
|
+
if (ref !== "")
|
|
6
|
+
q.set("ref", ref);
|
|
7
|
+
if (path !== "")
|
|
8
|
+
q.set("path", path);
|
|
9
|
+
return q.size > 0 ? `?${q.toString()}` : "";
|
|
10
|
+
}
|
|
11
|
+
export const API_SCOPES = [
|
|
12
|
+
"project:read",
|
|
13
|
+
"deploy",
|
|
14
|
+
"db:read",
|
|
15
|
+
"db:write",
|
|
16
|
+
"repo:read",
|
|
17
|
+
"repo:write",
|
|
18
|
+
"org:admin",
|
|
19
|
+
];
|
|
20
|
+
/** The exception that carries the server's error envelope unchanged. */
|
|
3
21
|
export class ApiError extends Error {
|
|
4
22
|
status;
|
|
5
23
|
code;
|
|
@@ -14,10 +32,11 @@ export class ApiError extends Error {
|
|
|
14
32
|
}
|
|
15
33
|
}
|
|
16
34
|
/**
|
|
17
|
-
* cp-public
|
|
35
|
+
* The cp-public client.
|
|
18
36
|
*
|
|
19
|
-
* CLI
|
|
20
|
-
*
|
|
37
|
+
* The CLI and the dashboard use the same one. If each called fetch itself, their
|
|
38
|
+
* error handling would drift a little, and that difference shows up during an
|
|
39
|
+
* incident.
|
|
21
40
|
*/
|
|
22
41
|
function dbPath(org, name) {
|
|
23
42
|
return `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/database`;
|
|
@@ -45,8 +64,21 @@ export class Client {
|
|
|
45
64
|
body = (await res.json());
|
|
46
65
|
}
|
|
47
66
|
catch {
|
|
48
|
-
//
|
|
49
|
-
//
|
|
67
|
+
// An error response that is not JSON (a 502 made by a proxy, for instance)
|
|
68
|
+
// is raised as the same exception — so the caller does not have to handle
|
|
69
|
+
// two shapes of failure.
|
|
70
|
+
}
|
|
71
|
+
if (body === undefined && res.status >= 502 && res.status <= 504) {
|
|
72
|
+
// The edge replaced the 5xx body with its own HTML. The code and reason
|
|
73
|
+
// the server wrote are not here — so say that, rather than "unknown". On
|
|
74
|
+
// 2026-09-10 a registrar rejection came out this way as
|
|
75
|
+
// "unknown: POST … → 502".
|
|
76
|
+
body = {
|
|
77
|
+
error: {
|
|
78
|
+
code: "edge_" + res.status,
|
|
79
|
+
message: "The server returned an error (" + res.status + "), and the error details could not be read. Try again shortly.",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
50
82
|
}
|
|
51
83
|
throw new ApiError(res.status, body, `${method} ${path} → ${res.status}`);
|
|
52
84
|
}
|
|
@@ -54,6 +86,21 @@ export class Client {
|
|
|
54
86
|
return undefined;
|
|
55
87
|
return (await res.json());
|
|
56
88
|
}
|
|
89
|
+
/** A body that is not JSON (the raw mail). Error handling is the same as request. */
|
|
90
|
+
async text(method, path) {
|
|
91
|
+
const res = await fetch(new URL(path, this.opts.baseUrl), { method, headers: this.headers() });
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
let body;
|
|
94
|
+
try {
|
|
95
|
+
body = (await res.json());
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// The same reason as in request
|
|
99
|
+
}
|
|
100
|
+
throw new ApiError(res.status, body, `${method} ${path} → ${res.status}`);
|
|
101
|
+
}
|
|
102
|
+
return await res.text();
|
|
103
|
+
}
|
|
57
104
|
json(method, path, body) {
|
|
58
105
|
if (body === undefined)
|
|
59
106
|
return this.request(method, path);
|
|
@@ -65,38 +112,205 @@ export class Client {
|
|
|
65
112
|
devLogin(subject) {
|
|
66
113
|
return this.json("POST", "/v1/auth/dev/login", { subject });
|
|
67
114
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Takes the sign-up and sends the confirmation mail. Neither a session nor a
|
|
117
|
+
* password is created here — the password is chosen in `verifyEmail` by
|
|
118
|
+
* whoever opens the mailbox.
|
|
119
|
+
*/
|
|
120
|
+
passwordSignUp(email, name) {
|
|
121
|
+
return this.json("POST", "/v1/auth/password/sign-up", {
|
|
122
|
+
email,
|
|
123
|
+
...(name === undefined ? {} : { name }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
passwordSignIn(email, password) {
|
|
127
|
+
return this.json("POST", "/v1/auth/password/sign-in", { email, password });
|
|
128
|
+
}
|
|
129
|
+
/** The same accepted response comes back whether or not the email exists. */
|
|
130
|
+
passwordForgot(email) {
|
|
131
|
+
return this.json("POST", "/v1/auth/password/forgot", { email });
|
|
132
|
+
}
|
|
133
|
+
passwordReset(token, password) {
|
|
134
|
+
return this.json("POST", "/v1/auth/password/reset", { token, password });
|
|
135
|
+
}
|
|
136
|
+
/** Uses the confirmation link and sets the account's first password. */
|
|
137
|
+
verifyEmail(token, password) {
|
|
138
|
+
return this.json("POST", "/v1/auth/email/verify", { token, password });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Finishes a sign-in that came back `{mfa:"totp"}`.
|
|
142
|
+
*
|
|
143
|
+
* Callable without a token: `pending` is the credential, and it is not a
|
|
144
|
+
* session — no other endpoint accepts it.
|
|
145
|
+
*/
|
|
146
|
+
completeTotpLogin(pending, factor) {
|
|
147
|
+
return this.json("POST", "/v1/auth/totp", { pending, ...factor });
|
|
148
|
+
}
|
|
149
|
+
/** This account's two-factor state, including which orgs require it. */
|
|
150
|
+
getTotp() {
|
|
151
|
+
return this.json("GET", "/v1/me/totp");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Draws a secret to scan. It does **not** turn the factor on — `confirmTotp`
|
|
155
|
+
* does, after the app has proved it can produce a code.
|
|
156
|
+
*/
|
|
157
|
+
enrollTotp() {
|
|
158
|
+
return this.json("POST", "/v1/me/totp/enroll");
|
|
159
|
+
}
|
|
160
|
+
/** Turns the factor on and hands back the ten recovery codes. They are shown once. */
|
|
161
|
+
confirmTotp(code) {
|
|
162
|
+
return this.json("POST", "/v1/me/totp/confirm", { code });
|
|
163
|
+
}
|
|
164
|
+
/** Turns it off. A code or a recovery code is required — the session alone is not. */
|
|
165
|
+
disableTotp(factor) {
|
|
166
|
+
return this.json("DELETE", "/v1/me/totp", factor);
|
|
71
167
|
}
|
|
72
168
|
/**
|
|
73
|
-
* GitHub OAuth
|
|
74
|
-
* `window.location`
|
|
169
|
+
* The GitHub OAuth start URL. It is a 302 the browser follows, so it goes into
|
|
170
|
+
* `window.location` rather than being called with fetch. `redirect` has to be
|
|
171
|
+
* on the server's allow-list.
|
|
75
172
|
*/
|
|
76
173
|
githubStartUrl(redirect) {
|
|
77
174
|
const u = new URL("/v1/auth/github/start", this.opts.baseUrl);
|
|
78
175
|
u.searchParams.set("redirect", redirect);
|
|
79
176
|
return u.toString();
|
|
80
177
|
}
|
|
178
|
+
/** The Google OAuth start URL. Uses the same dashboard callback shape as GitHub. */
|
|
179
|
+
googleStartUrl(redirect) {
|
|
180
|
+
const u = new URL("/v1/auth/google/start", this.opts.baseUrl);
|
|
181
|
+
u.searchParams.set("redirect", redirect);
|
|
182
|
+
return u.toString();
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The generic OIDC start URL (docs/onprem-delivery.md §7.7). Same 302-and-fragment
|
|
186
|
+
* shape as the other two; which provider is behind it is the installation's choice.
|
|
187
|
+
*/
|
|
188
|
+
oidcStartUrl(redirect) {
|
|
189
|
+
const u = new URL("/v1/auth/oidc/start", this.opts.baseUrl);
|
|
190
|
+
u.searchParams.set("redirect", redirect);
|
|
191
|
+
return u.toString();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Does this deployment federate an identity provider?
|
|
195
|
+
*
|
|
196
|
+
* **There is no endpoint that answers "which logins are on"** — one existed and was
|
|
197
|
+
* deleted, because its only reader was the login screen and it put a round trip in
|
|
198
|
+
* front of every login button (cp/internal/public/public.go). This is not that
|
|
199
|
+
* endpoint coming back: cp-public **does not register** the two OIDC routes when no
|
|
200
|
+
* issuer is configured, so the mux's own 404 is the answer, and the probe is a HEAD
|
|
201
|
+
* that no handler runs.
|
|
202
|
+
*
|
|
203
|
+
* Any other status means the route is there — a 400 (this probe carries no
|
|
204
|
+
* `redirect`) or a 502 (the provider's discovery document could not be read) both say
|
|
205
|
+
* the handler exists, and a button that leads to a readable error beats no button.
|
|
206
|
+
* A network failure answers false: on SaaS, where the answer is false anyway, a dead
|
|
207
|
+
* API address must not draw a login that cannot work.
|
|
208
|
+
*/
|
|
209
|
+
async oidcAvailable() {
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch(new URL("/v1/auth/oidc/start", this.opts.baseUrl), { method: "HEAD" });
|
|
212
|
+
return res.status !== 404;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* This installation's licence (docs/onprem-delivery.md §6.5). Readable by any
|
|
220
|
+
* signed-in user: the footer it feeds is on every page for everybody.
|
|
221
|
+
*
|
|
222
|
+
* On SaaS it answers `onPrem: false` rather than 404, so the caller has one shape to
|
|
223
|
+
* read and the surfaces draw nothing on the flag.
|
|
224
|
+
*/
|
|
225
|
+
license() {
|
|
226
|
+
return this.json("GET", "/v1/license");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The names of this deployment (app domain, docs address). Callable without auth.
|
|
230
|
+
*
|
|
231
|
+
* This exists so the dashboard does not hard-code a domain into the screen
|
|
232
|
+
* (docs/environments.md §3.2) — hard-coded, the dev screen tells people a
|
|
233
|
+
* production address, and because that address opens, nobody ever says it is
|
|
234
|
+
* wrong.
|
|
235
|
+
*/
|
|
236
|
+
siteConfig() {
|
|
237
|
+
return this.json("GET", "/v1/config");
|
|
238
|
+
}
|
|
81
239
|
me() {
|
|
82
240
|
return this.json("GET", "/v1/me");
|
|
83
241
|
}
|
|
84
242
|
listOrgs() {
|
|
85
243
|
return this.json("GET", "/v1/orgs");
|
|
86
244
|
}
|
|
245
|
+
/** Changes the display name only (admin). The slug is the name that becomes an address, so it is not on this surface. */
|
|
246
|
+
/**
|
|
247
|
+
* `requireTotp` is left out when it is not given, on purpose: the server reads
|
|
248
|
+
* an absent field as "leave it alone". Sending `false` by default would mean a
|
|
249
|
+
* rename silently switched the org's second-factor requirement off.
|
|
250
|
+
*/
|
|
251
|
+
updateOrg(org, name, requireTotp) {
|
|
252
|
+
return this.json("PATCH", `/v1/orgs/${encodeURIComponent(org)}`, {
|
|
253
|
+
name,
|
|
254
|
+
...(requireTotp === undefined ? {} : { requireTotp }),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Deletes the organization (admin). **This cannot be undone.**
|
|
259
|
+
*
|
|
260
|
+
* `confirm` has to be the organization slug exactly — the confirmation lives on
|
|
261
|
+
* the server because three places call this surface (the screen, the CLI, and
|
|
262
|
+
* scripts). If a live project, domain, repository, or DNS zone is left, it is a
|
|
263
|
+
* 409 `org_not_empty` and `details.holdings` says what is left — **projects
|
|
264
|
+
* already deleted are not counted.**
|
|
265
|
+
*
|
|
266
|
+
* The `purgeAfter` in the answer is when the rows actually disappear. By the
|
|
267
|
+
* moment it returns the organization is already gone from every surface; what
|
|
268
|
+
* remains is retention until that time.
|
|
269
|
+
*/
|
|
270
|
+
deleteOrg(org, confirm) {
|
|
271
|
+
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}`, { confirm });
|
|
272
|
+
}
|
|
87
273
|
createOrg(slug, name) {
|
|
88
274
|
return this.json("POST", "/v1/orgs", { slug, name });
|
|
89
275
|
}
|
|
90
276
|
/**
|
|
91
|
-
*
|
|
277
|
+
* The organization's recent notifications (docs/limits.md §3.4).
|
|
92
278
|
*
|
|
93
|
-
* `sentAt`
|
|
94
|
-
*
|
|
279
|
+
* A row with no `sentAt` is either still queued or has no email address to
|
|
280
|
+
* reach — and for the second, this list is the only channel, so the screen
|
|
281
|
+
* draws both states the same way.
|
|
95
282
|
*/
|
|
96
283
|
listNotifications(org, limit) {
|
|
97
284
|
const q = limit === undefined ? "" : `?limit=${limit}`;
|
|
98
285
|
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/notifications${q}`);
|
|
99
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* The plan and billing state (docs/billing.md). If `enabled` is false, this
|
|
289
|
+
* deployment has no billing — the screen shows only a contact address.
|
|
290
|
+
*/
|
|
291
|
+
getBilling(org) {
|
|
292
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/billing`);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* A Pro checkout session. Send the browser to the `url` that comes back. Even
|
|
296
|
+
* once payment finishes, the thing that changes the plan is the webhook, so
|
|
297
|
+
* read `getBilling` again after the browser returns.
|
|
298
|
+
*/
|
|
299
|
+
createBillingCheckout(org, returnTo) {
|
|
300
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/checkout`, { returnTo });
|
|
301
|
+
}
|
|
302
|
+
/** A one-time `url` for the customer portal (payment method, receipts, cancellation). */
|
|
303
|
+
createBillingPortal(org, returnTo) {
|
|
304
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/portal`, { returnTo });
|
|
305
|
+
}
|
|
306
|
+
/** The customer session token for the payment-method embed (1 hour). An org with no payment history gets a 404. */
|
|
307
|
+
createBillingSession(org) {
|
|
308
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/session`, {});
|
|
309
|
+
}
|
|
310
|
+
/** Schedules cancellation at the end of the period (`cancel: true`), or undoes it. There is no immediate revocation. */
|
|
311
|
+
cancelBilling(org, body) {
|
|
312
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/billing/cancel`, body);
|
|
313
|
+
}
|
|
100
314
|
listProjects(org) {
|
|
101
315
|
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/projects`);
|
|
102
316
|
}
|
|
@@ -106,17 +320,210 @@ export class Client {
|
|
|
106
320
|
getProject(org, name) {
|
|
107
321
|
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}`);
|
|
108
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Renames the project (admin). **The default address moves with it and there is
|
|
325
|
+
* no alias** — which is why the response carries the new address. Unlike the
|
|
326
|
+
* org slug, this name can change because it is the inner label of the org
|
|
327
|
+
* wildcard certificate.
|
|
328
|
+
*/
|
|
329
|
+
updateProject(org, name, next) {
|
|
330
|
+
return this.json("PATCH", this.projectPath(org, name), { name: next });
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Starts tearing the project down (admin). **This cannot be undone and leaves
|
|
334
|
+
* no recovery generation.** `confirm` has to be the project name exactly.
|
|
335
|
+
*/
|
|
336
|
+
deleteProject(org, name, confirm) {
|
|
337
|
+
return this.json("DELETE", this.projectPath(org, name), { confirm });
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Disables a project (admin): it stops serving and taking new backups, and
|
|
341
|
+
* keeps everything it has. A project disabled for a year is deleted for good.
|
|
342
|
+
*/
|
|
343
|
+
disableProject(org, name) {
|
|
344
|
+
return this.json("POST", `${this.projectPath(org, name)}/disable`);
|
|
345
|
+
}
|
|
346
|
+
/** Enables a disabled project (admin). 409 `project_limit` when there is no room for it. */
|
|
347
|
+
enableProject(org, name) {
|
|
348
|
+
return this.json("POST", `${this.projectPath(org, name)}/enable`);
|
|
349
|
+
}
|
|
350
|
+
/** Watches the teardown operation to its end even after the project is gone. */
|
|
351
|
+
getOrgOperation(org, opId) {
|
|
352
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/operations/${encodeURIComponent(opId)}`);
|
|
353
|
+
}
|
|
354
|
+
/** Everything the project's first screen takes in one call. Usage calls `getUsage` alongside it. */
|
|
355
|
+
getProjectOverview(org, name) {
|
|
356
|
+
return this.json("GET", `${this.projectPath(org, name)}/overview`);
|
|
357
|
+
}
|
|
109
358
|
listDeployments(org, name) {
|
|
110
359
|
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/deployments`);
|
|
111
360
|
}
|
|
112
|
-
/**
|
|
113
|
-
|
|
361
|
+
/**
|
|
362
|
+
* One deployment, with `migrations` when the control plane applied its
|
|
363
|
+
* `migrations/` — which today means a preview deployed with `db=branch`
|
|
364
|
+
* (docs/studio.md §6.6 step 2).
|
|
365
|
+
*
|
|
366
|
+
* A preview's deployment is read under **the parent project's name**, the name
|
|
367
|
+
* the caller deployed through: the preview is a project of its own, but the
|
|
368
|
+
* caller never learns its name.
|
|
369
|
+
*/
|
|
370
|
+
getDeployment(org, name, deploymentId) {
|
|
371
|
+
return this.json("GET", `${this.projectPath(org, name)}/deployments/${encodeURIComponent(deploymentId)}`);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Scheduled runs: what the live deployment declares, when each fires next, and
|
|
375
|
+
* the recent history (docs/cron.md §8). `limit` bounds the history rows only —
|
|
376
|
+
* the schedules and the per-schedule last run are never paged.
|
|
377
|
+
*/
|
|
378
|
+
getProjectCron(org, name, limit) {
|
|
379
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
380
|
+
return this.json("GET", `${this.projectPath(org, name)}/cron${qs}`);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Queues: what the live deployment declares, how deep each one is, and the
|
|
384
|
+
* recent batch history (docs/queues.md §6). The depth is read from the
|
|
385
|
+
* project's own database; `depthError` on the answer says when it could not be.
|
|
386
|
+
*/
|
|
387
|
+
getProjectQueues(org, name, limit) {
|
|
388
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
389
|
+
return this.json("GET", `${this.projectPath(org, name)}/queues${qs}`);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Durable Objects: how many this project has placed, against the two caps
|
|
393
|
+
* (docs/durable-objects.md).
|
|
394
|
+
*
|
|
395
|
+
* **It answers how much, never which.** There is no actor id in the response
|
|
396
|
+
* and none is coming — those name individual objects, and a list of them would
|
|
397
|
+
* be an object enumeration wearing a quota's name. `nodes` is keyed by node id
|
|
398
|
+
* because `nodeLimit` is a separate bound that can refuse a placement while
|
|
399
|
+
* the project-wide cap still has room.
|
|
400
|
+
*/
|
|
401
|
+
getProjectDurableObjects(org, name) {
|
|
402
|
+
return this.json("GET", `${this.projectPath(org, name)}/durable-objects`);
|
|
403
|
+
}
|
|
404
|
+
queuePath(org, name, queue) {
|
|
405
|
+
return `${this.projectPath(org, name)}/queues/${encodeURIComponent(queue)}`;
|
|
406
|
+
}
|
|
407
|
+
listQueueRuns(org, name, queue, limit) {
|
|
408
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
409
|
+
return this.json("GET", `${this.queuePath(org, name, queue)}/runs${qs}`);
|
|
410
|
+
}
|
|
411
|
+
/** The head of the queue without leasing it — looking cannot delay a delivery. */
|
|
412
|
+
peekQueue(org, name, queue, limit) {
|
|
413
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
414
|
+
return this.json("GET", `${this.queuePath(org, name, queue)}/messages${qs}`);
|
|
415
|
+
}
|
|
416
|
+
/** Irreversible. The confirmation is the caller's — the server holds none. */
|
|
417
|
+
purgeQueue(org, name, queue) {
|
|
418
|
+
return this.json("DELETE", `${this.queuePath(org, name, queue)}/messages`);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Moves a dead-letter queue's messages back. `to` is required only when the
|
|
422
|
+
* dead-letter queue belongs to more than one consumer, or to none.
|
|
423
|
+
*/
|
|
424
|
+
redriveQueue(org, name, queue, to) {
|
|
425
|
+
return this.json("POST", `${this.queuePath(org, name, queue)}/redrive`, to === undefined ? {} : { to });
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The vector store: every namespace with its count and width, and the plan's
|
|
429
|
+
* bounds (docs/vectors.md §4). Read from the project's own database;
|
|
430
|
+
* `readError` on the answer says when it could not be.
|
|
431
|
+
*/
|
|
432
|
+
getProjectVectors(org, name) {
|
|
433
|
+
return this.json("GET", `${this.projectPath(org, name)}/vectors`);
|
|
434
|
+
}
|
|
435
|
+
/** Irreversible. The confirmation is the caller's — the server holds none. */
|
|
436
|
+
deleteVectorNamespace(org, name, namespace) {
|
|
437
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/vectors/${encodeURIComponent(namespace)}`);
|
|
438
|
+
}
|
|
439
|
+
/** The body is the bundle. One digest covers all of the code and the assets. */
|
|
440
|
+
/**
|
|
441
|
+
* Uploads the bundle. `headers` carries the provenance headers
|
|
442
|
+
* (`X-Runlot-Git-*`, openapi createDeployment) — the body is the bundle
|
|
443
|
+
* itself, so the metadata can only travel in headers.
|
|
444
|
+
*/
|
|
445
|
+
deploy(org, name, bundle, headers = {}) {
|
|
114
446
|
return this.request("POST", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/deployments`, {
|
|
115
447
|
body: bundle,
|
|
116
|
-
headers: { "Content-Type": "application/gzip" },
|
|
448
|
+
headers: { ...headers, "Content-Type": "application/gzip" },
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The same upload, aimed at a preview (docs/preview.md). **It does not move
|
|
453
|
+
* production's live pointer** — that is the whole difference, and it is why
|
|
454
|
+
* this is a named method rather than an optional argument on `deploy`: a
|
|
455
|
+
* caller that forgets the argument deploys to production.
|
|
456
|
+
*
|
|
457
|
+
* `db` picks what `env.db` points at (`branch` by default, `shared` at
|
|
458
|
+
* production's own database, `none` for no database), and `from` pours the
|
|
459
|
+
* branch from a named generation instead of the newest one.
|
|
460
|
+
*/
|
|
461
|
+
deployPreview(org, name, bundle,
|
|
462
|
+
// `| undefined` spelled out: under exactOptionalPropertyTypes a caller that
|
|
463
|
+
// forwards an unset flag is passing the property, not omitting it.
|
|
464
|
+
opts, headers = {}) {
|
|
465
|
+
const q = new URLSearchParams({ preview: opts.preview });
|
|
466
|
+
// Empty values are not carried: the server reads "absent" as its default, and
|
|
467
|
+
// `?db=` would be neither absent nor a member of the enum.
|
|
468
|
+
if (opts.db)
|
|
469
|
+
q.set("db", opts.db);
|
|
470
|
+
if (opts.from)
|
|
471
|
+
q.set("from", opts.from);
|
|
472
|
+
return this.request("POST", `${this.projectPath(org, name)}/deployments?${q.toString()}`, {
|
|
473
|
+
body: bundle,
|
|
474
|
+
headers: { ...headers, "Content-Type": "application/gzip" },
|
|
117
475
|
});
|
|
118
476
|
}
|
|
119
|
-
/**
|
|
477
|
+
/** The parent's live previews (viewer). A deleted preview is not listed. */
|
|
478
|
+
listPreviews(org, name) {
|
|
479
|
+
return this.json("GET", `${this.projectPath(org, name)}/previews`);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Deletes a preview and its branch (member).
|
|
483
|
+
*
|
|
484
|
+
* No confirmation string, unlike `deleteProject`: a branch is a copy taken at a
|
|
485
|
+
* moment and nothing in it is the only copy of anything.
|
|
486
|
+
*/
|
|
487
|
+
deletePreview(org, name, preview) {
|
|
488
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/previews/${encodeURIComponent(preview)}`);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Makes the preview's artifact the parent's live deployment (member).
|
|
492
|
+
*
|
|
493
|
+
* **The code, never the data** (docs/preview.md §7). The branch is not merged
|
|
494
|
+
* and production's database is not replaced. The preview stays alive.
|
|
495
|
+
*/
|
|
496
|
+
promotePreview(org, name, preview) {
|
|
497
|
+
return this.json("POST", `${this.projectPath(org, name)}/previews/${encodeURIComponent(preview)}/promote`);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* The project's share link — `<label>.share.<app-domain>`, the one address
|
|
501
|
+
* Studio hands to somebody who is not a developer (docs/studio.md §6.9).
|
|
502
|
+
*
|
|
503
|
+
* 404 when the project has never been shared: a link is minted by
|
|
504
|
+
* `putShareLink`, never as a side effect of reading.
|
|
505
|
+
*/
|
|
506
|
+
getShareLink(org, name) {
|
|
507
|
+
return this.json("GET", `${this.projectPath(org, name)}/share`);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Points the share link at a preview, minting the label if there is none.
|
|
511
|
+
*
|
|
512
|
+
* Idempotent: the label is kept across shares, so somebody holding the link
|
|
513
|
+
* keeps working. `rotate` mints a new label and retires the old name in the
|
|
514
|
+
* same request ("링크 재발급"), so the project never has two live links.
|
|
515
|
+
*/
|
|
516
|
+
putShareLink(org, name, opts = {}) {
|
|
517
|
+
return this.json("PUT", `${this.projectPath(org, name)}/share`, {
|
|
518
|
+
...(opts.preview === undefined ? {} : { preview: opts.preview }),
|
|
519
|
+
...(opts.rotate === undefined ? {} : { rotate: opts.rotate }),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/** Stops the link answering and forgets the label. The preview keeps running. */
|
|
523
|
+
deleteShareLink(org, name) {
|
|
524
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/share`);
|
|
525
|
+
}
|
|
526
|
+
/** Idempotent. Called twice, the password is not rolled and the same connection details come back. */
|
|
120
527
|
createDatabase(org, name) {
|
|
121
528
|
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/database`);
|
|
122
529
|
}
|
|
@@ -124,17 +531,18 @@ export class Client {
|
|
|
124
531
|
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/database`);
|
|
125
532
|
}
|
|
126
533
|
/**
|
|
127
|
-
*
|
|
128
|
-
* viewer
|
|
534
|
+
* The connection credentials as well. The route is split from `getDatabase`
|
|
535
|
+
* because of roles — a viewer may know whether there is a database, but not
|
|
536
|
+
* the password (proto/openapi.yaml).
|
|
129
537
|
*/
|
|
130
538
|
getDatabaseConnect(org, name) {
|
|
131
539
|
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/database/connect`);
|
|
132
540
|
}
|
|
133
|
-
/**
|
|
541
|
+
/** The endpoint only — no credentials. A viewer gets it too. Used by `runlot port-forward`. */
|
|
134
542
|
getDatabaseEndpoint(org, name) {
|
|
135
543
|
return this.json("GET", `${dbPath(org, name)}/endpoint`);
|
|
136
544
|
}
|
|
137
|
-
/**
|
|
545
|
+
/** Issues a short-lived machine credential. The password is carried in this response and nowhere else. */
|
|
138
546
|
createDatabaseToken(org, name, ttlSeconds) {
|
|
139
547
|
return this.json("POST", `${dbPath(org, name)}/tokens`, ttlSeconds === undefined ? {} : { ttlSeconds });
|
|
140
548
|
}
|
|
@@ -145,52 +553,84 @@ export class Client {
|
|
|
145
553
|
return this.json("DELETE", `${dbPath(org, name)}/tokens/${encodeURIComponent(user)}`);
|
|
146
554
|
}
|
|
147
555
|
/**
|
|
148
|
-
*
|
|
556
|
+
* Makes one diagnostic image.
|
|
149
557
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* (proto/openapi.yaml).
|
|
558
|
+
* **Not idempotent** — every call makes another image. And it is slow: the
|
|
559
|
+
* response comes after the dump finishes, and that project's actor is held
|
|
560
|
+
* for the whole of it (proto/openapi.yaml).
|
|
153
561
|
*/
|
|
154
562
|
exportDatabase(org, name) {
|
|
155
563
|
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/database/export`);
|
|
156
564
|
}
|
|
157
565
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
566
|
+
* Every generation a restore can choose, newest first. `lastCheckedAt` is the
|
|
567
|
+
* heartbeat of the no-change skip — it is what tells "no new generations"
|
|
568
|
+
* apart from "the backup is dead".
|
|
160
569
|
*/
|
|
161
570
|
listGenerations(org, name) {
|
|
162
571
|
return this.json("GET", `${this.dbPath(org, name)}/generations`);
|
|
163
572
|
}
|
|
164
573
|
/**
|
|
165
|
-
*
|
|
574
|
+
* Runs one statement in the project's database (the dashboard console).
|
|
575
|
+
*
|
|
576
|
+
* `readOnly` is **omitted, not sent as false**, when the caller wants the safe
|
|
577
|
+
* mode: the server treats an absent field as read-only, so leaving it out is the
|
|
578
|
+
* one spelling that cannot be misread. Sending `false` is the explicit ask for
|
|
579
|
+
* write mode, and only then does the server run outside a transaction.
|
|
580
|
+
*
|
|
581
|
+
* SQL failures arrive as an `ApiError` whose body carries `details.sqlstate` —
|
|
582
|
+
* PostgreSQL's own code, which is what the error panel shows.
|
|
583
|
+
*/
|
|
584
|
+
executeSQL(org, name, sql, opts = {}) {
|
|
585
|
+
const body = { sql };
|
|
586
|
+
if (opts.readOnly === false)
|
|
587
|
+
body.readOnly = false;
|
|
588
|
+
if (opts.params !== undefined && opts.params.length > 0)
|
|
589
|
+
body.params = opts.params;
|
|
590
|
+
return this.json("POST", `${this.dbPath(org, name)}/execute`, body);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Rolls back to a generation. **Writes made since then are lost.**
|
|
166
594
|
*
|
|
167
|
-
*
|
|
168
|
-
* `getDatabaseOperation
|
|
169
|
-
*
|
|
595
|
+
* It ends at 202 — what comes back is an operation id, and progress is read
|
|
596
|
+
* through `getDatabaseOperation`. The server does not resolve `latest`: the
|
|
597
|
+
* `(epoch, seq)` chosen from the list is carried as-is (proto/openapi.yaml).
|
|
170
598
|
*/
|
|
171
599
|
restoreDatabase(org, name, epoch, seq) {
|
|
172
600
|
return this.json("POST", `${this.dbPath(org, name)}/restore`, { epoch, seq });
|
|
173
601
|
}
|
|
174
602
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
603
|
+
* Rolls back to a restore point **inside** a backup (pg-feather docs/37):
|
|
604
|
+
* `txnId` is that commit, `at` is the last commit at or before that time. One
|
|
605
|
+
* backup carries the commits in between in order, so the place you can roll
|
|
606
|
+
* back to is not tied to a backup boundary. Everything else is as in
|
|
607
|
+
* restoreDatabase — 202, an operation id, and one safety backup before the
|
|
608
|
+
* rollback.
|
|
609
|
+
*/
|
|
610
|
+
restoreDatabaseTo(org, name, target) {
|
|
611
|
+
return this.json("POST", `${this.dbPath(org, name)}/restore`, target);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Removes the database. `confirm` has to match the project name **exactly**;
|
|
615
|
+
* if it does not, the server refuses with 400 `confirm_mismatch`.
|
|
177
616
|
*/
|
|
178
617
|
deleteDatabase(org, name, confirm) {
|
|
179
618
|
return this.json("POST", `${this.dbPath(org, name)}/delete`, { confirm });
|
|
180
619
|
}
|
|
181
|
-
/**
|
|
620
|
+
/** The progress of one operation. The operation id is not a credential; it lives under the project path. */
|
|
182
621
|
getDatabaseOperation(org, name, opId) {
|
|
183
622
|
return this.json("GET", `${this.dbPath(org, name)}/operations/${encodeURIComponent(opId)}`);
|
|
184
623
|
}
|
|
185
|
-
/**
|
|
624
|
+
/** Aborts an operation that has not yet reached a phase it cannot come back from. */
|
|
186
625
|
abortDatabaseOperation(org, name, opId) {
|
|
187
626
|
return this.json("POST", `${this.dbPath(org, name)}/operations/${encodeURIComponent(opId)}/abort`);
|
|
188
627
|
}
|
|
189
628
|
// ---- Phase 6 (docs/phase6.md) ----
|
|
190
629
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
630
|
+
* A live tail of the logs. It is **a stream, not JSON**, so it does not go
|
|
631
|
+
* through `request` — what comes back is an async iterable of line-by-line
|
|
632
|
+
* `LogEvent`, and aborting `signal` closes the connection. An error response
|
|
633
|
+
* (4xx/5xx) is the same `ApiError` as in every other method.
|
|
194
634
|
*/
|
|
195
635
|
async *tailLogs(org, name, opts = {}) {
|
|
196
636
|
const q = new URLSearchParams();
|
|
@@ -210,7 +650,7 @@ export class Client {
|
|
|
210
650
|
body = (await res.json());
|
|
211
651
|
}
|
|
212
652
|
catch {
|
|
213
|
-
//
|
|
653
|
+
// The same reason as in request above.
|
|
214
654
|
}
|
|
215
655
|
throw new ApiError(res.status, body, `GET ${path} → ${res.status}`);
|
|
216
656
|
}
|
|
@@ -238,14 +678,35 @@ export class Client {
|
|
|
238
678
|
yield JSON.parse(rest);
|
|
239
679
|
}
|
|
240
680
|
finally {
|
|
241
|
-
//
|
|
681
|
+
// If the consumer ends early (break), the server's follow has to end too.
|
|
242
682
|
await reader.cancel().catch(() => { });
|
|
243
683
|
}
|
|
244
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* The stored lane (0062). It is a plain JSON page, not a stream: the history
|
|
687
|
+
* does not reach a node, so it answers for a project that is cold, parked, or
|
|
688
|
+
* has moved since the lines were written — every one of which is a 409 on
|
|
689
|
+
* `tailLogs`.
|
|
690
|
+
*
|
|
691
|
+
* `q` is a plain substring, matched case-insensitively. It is not a regular
|
|
692
|
+
* expression — `%` and `_` match themselves.
|
|
693
|
+
*
|
|
694
|
+
* Paging is keyset: pass the `cursor` from a page back to get the next, older
|
|
695
|
+
* one. A page with no `cursor` is the end of what is kept.
|
|
696
|
+
*/
|
|
697
|
+
getLogHistory(org, name, opts = {}) {
|
|
698
|
+
const params = new URLSearchParams();
|
|
699
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
700
|
+
if (v !== undefined && v !== "")
|
|
701
|
+
params.set(k, String(v));
|
|
702
|
+
}
|
|
703
|
+
const qs = params.size ? `?${params}` : "";
|
|
704
|
+
return this.json("GET", `${this.projectPath(org, name)}/logs/history${qs}`);
|
|
705
|
+
}
|
|
245
706
|
listSecrets(org, name) {
|
|
246
707
|
return this.json("GET", `${this.projectPath(org, name)}/secrets`);
|
|
247
708
|
}
|
|
248
|
-
/**
|
|
709
|
+
/** The value travels in this call only. It is in no response at all. */
|
|
249
710
|
setSecret(org, name, secret, value) {
|
|
250
711
|
return this.json("PUT", `${this.projectPath(org, name)}/secrets/${encodeURIComponent(secret)}`, { value });
|
|
251
712
|
}
|
|
@@ -256,111 +717,279 @@ export class Client {
|
|
|
256
717
|
const q = hours === undefined ? "" : `?hours=${hours}`;
|
|
257
718
|
return this.json("GET", `${this.projectPath(org, name)}/usage${q}`);
|
|
258
719
|
}
|
|
259
|
-
// ----
|
|
260
|
-
/**
|
|
720
|
+
// ---- Object storage (docs/storage.md §8) ----
|
|
721
|
+
/** Grant, prefix, quota, usage. `usedBytes` lags by the node's reporting interval. */
|
|
261
722
|
getStorage(org, name) {
|
|
262
723
|
return this.json("GET", `${this.projectPath(org, name)}/storage`);
|
|
263
724
|
}
|
|
264
725
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
726
|
+
* Grants storage. It is idempotent, so pressing twice leaves the prefix as it
|
|
727
|
+
* was. Taking neither the prefix nor the quota as an argument is the point —
|
|
728
|
+
* the prefix is the isolation boundary and the quota is set by the plan
|
|
729
|
+
* (proto/openapi.yaml).
|
|
268
730
|
*/
|
|
269
731
|
grantStorage(org, name) {
|
|
270
732
|
return this.json("POST", `${this.projectPath(org, name)}/storage`);
|
|
271
733
|
}
|
|
272
|
-
/**
|
|
734
|
+
/** Removes the grant. **The objects are not deleted** (docs/storage.md §7). */
|
|
273
735
|
revokeStorage(org, name) {
|
|
274
736
|
return this.json("DELETE", `${this.projectPath(org, name)}/storage`);
|
|
275
737
|
}
|
|
276
|
-
// ---- 최종 사용자 인증 (docs/auth.md §9) ----
|
|
277
738
|
/**
|
|
278
|
-
*
|
|
279
|
-
* DB 안이고 (docs/auth.md §6) 워커의 `env.auth` 만 본다.
|
|
739
|
+
* The key prefixes this project serves unsigned (docs/storage.md §4.7).
|
|
280
740
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
741
|
+
* A prefix is stored normalized — `assets` becomes `assets/`, and `/` becomes
|
|
742
|
+
* the empty string, which means every key. The value that comes back is the
|
|
743
|
+
* value that governs, so a screen must render the response rather than what
|
|
744
|
+
* the user typed.
|
|
745
|
+
*/
|
|
746
|
+
listStoragePublic(org, name) {
|
|
747
|
+
return this.json("GET", `${this.projectPath(org, name)}/storage/public`);
|
|
748
|
+
}
|
|
749
|
+
/** Publishes a prefix. Idempotent. All three verbs answer with the whole list. */
|
|
750
|
+
addStoragePublic(org, name, prefix) {
|
|
751
|
+
return this.json("POST", `${this.projectPath(org, name)}/storage/public`, { prefix });
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Unpublishes a prefix. Idempotent.
|
|
755
|
+
*
|
|
756
|
+
* **The world keeps reading for up to a minute.** The object front caches the
|
|
757
|
+
* list, so this call is not a revocation that takes effect at the instant it
|
|
758
|
+
* returns — a screen that says "no longer public" the moment this resolves is
|
|
759
|
+
* telling a small lie.
|
|
760
|
+
*/
|
|
761
|
+
removeStoragePublic(org, name, prefix) {
|
|
762
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/storage/public`, { prefix });
|
|
763
|
+
}
|
|
764
|
+
// ---- Email (docs/email.md §5) ----
|
|
765
|
+
/** Grant, address, sender, limits, recent counts. The address comes back as a preview even without a grant. */
|
|
766
|
+
getEmail(org, name) {
|
|
767
|
+
return this.json("GET", `${this.projectPath(org, name)}/email`);
|
|
768
|
+
}
|
|
769
|
+
/** Turns it on. **Idempotent** — we choose the address. The deploy reads `"email": true` and calls this. */
|
|
770
|
+
grantEmail(org, name) {
|
|
771
|
+
return this.json("POST", `${this.projectPath(org, name)}/email`);
|
|
772
|
+
}
|
|
773
|
+
/** Turns it off. Mail arriving at that address now gets a 550. */
|
|
774
|
+
revokeEmail(org, name) {
|
|
775
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/email`);
|
|
776
|
+
}
|
|
777
|
+
// ---- Custom sending domains (docs/email.md §2.4) ----
|
|
778
|
+
/** The project's own sending domains, each with the records to publish. */
|
|
779
|
+
listEmailDomains(org, name) {
|
|
780
|
+
return this.json("GET", `${this.projectPath(org, name)}/email/domains`);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Creates the provider identity and answers with the records to publish.
|
|
784
|
+
* `publish` writes them into the zone when we host one covering the domain.
|
|
785
|
+
*/
|
|
786
|
+
addEmailDomain(org, name, domain, publish = false) {
|
|
787
|
+
return this.json("POST", `${this.projectPath(org, name)}/email/domains`, { domain, publish });
|
|
788
|
+
}
|
|
789
|
+
/** Re-checks now. The background loop does the same once a minute while pending. */
|
|
790
|
+
verifyEmailDomain(org, name, domain) {
|
|
791
|
+
return this.json("POST", `${this.projectPath(org, name)}/email/domains/${encodeURIComponent(domain)}/verify`);
|
|
792
|
+
}
|
|
793
|
+
/** Deletes the identity and the row. Sending from it is refused from the next convergence. */
|
|
794
|
+
removeEmailDomain(org, name, domain) {
|
|
795
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/email/domains/${encodeURIComponent(domain)}`);
|
|
796
|
+
}
|
|
797
|
+
// ---- AI (docs/ai.md §6) ----
|
|
798
|
+
/**
|
|
799
|
+
* The model catalogue. **Being logged in is enough to see it** — what you can
|
|
800
|
+
* buy is not an organization's secret, and this list is the price table.
|
|
801
|
+
*
|
|
802
|
+
* Prices are integer **micro-cents** per million tokens (1 µ¢ = 1e-8 USD).
|
|
803
|
+
* Not letting the server pick the decimal place is the point — a small call
|
|
804
|
+
* rounded to cents becomes 0.
|
|
805
|
+
*/
|
|
806
|
+
listAIModels() {
|
|
807
|
+
return this.json("GET", "/v1/ai/models");
|
|
808
|
+
}
|
|
809
|
+
/** Grant, limits, the org balance, 24 h usage. The limits come back as a preview even without a grant. */
|
|
810
|
+
getAI(org, name) {
|
|
811
|
+
return this.json("GET", `${this.projectPath(org, name)}/ai`);
|
|
812
|
+
}
|
|
813
|
+
/** Turns it on. **Idempotent.** The deploy reads `"ai": true` and calls this. */
|
|
814
|
+
grantAI(org, name) {
|
|
815
|
+
return this.json("POST", `${this.projectPath(org, name)}/ai`);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Lends the org's own Claude/Codex subscriptions to this project's
|
|
819
|
+
* `anthropic/*` and `openai/*` calls (docs/studio-byos-ai.md §3.1).
|
|
820
|
+
*
|
|
821
|
+
* **Omitting a field leaves it as it was**, so turning lending on does not
|
|
822
|
+
* clear a fallback that was already set. Admin only: the subscription that
|
|
823
|
+
* carries the vendor-terms risk belongs to a person, not to the org.
|
|
824
|
+
*/
|
|
825
|
+
patchAI(org, name, body) {
|
|
826
|
+
return this.json("PATCH", `${this.projectPath(org, name)}/ai`, body);
|
|
827
|
+
}
|
|
828
|
+
/** Turns it off. **The ledger and the balance are untouched** — the balance belongs to the org and the grant belongs to the project. */
|
|
829
|
+
revokeAI(org, name) {
|
|
830
|
+
return this.json("DELETE", `${this.projectPath(org, name)}/ai`);
|
|
831
|
+
}
|
|
832
|
+
/** The org's credit balance and its automatic top-up settings. The balance is in micro-cents and can be negative. */
|
|
833
|
+
getAICredit(org) {
|
|
834
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/ai/credit`);
|
|
835
|
+
}
|
|
836
|
+
/** One page of the org ledger. The cursor is `ledger_id` — cutting on time splits the same millisecond. */
|
|
837
|
+
listAILedger(org, opts = {}) {
|
|
838
|
+
const q = new URLSearchParams();
|
|
839
|
+
if (opts.limit)
|
|
840
|
+
q.set("limit", String(opts.limit));
|
|
841
|
+
if (opts.before)
|
|
842
|
+
q.set("before", String(opts.before));
|
|
843
|
+
const qs = q.toString();
|
|
844
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/ai/ledger${qs ? "?" + qs : ""}`);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* The credit top-up checkout. **A browser coming back is not evidence** — the
|
|
848
|
+
* one thing that raises the balance is the `order.paid` webhook, and the
|
|
849
|
+
* screen polls the balance after the success event.
|
|
850
|
+
*/
|
|
851
|
+
createAICreditCheckout(org, microCents, returnTo) {
|
|
852
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/ai/credit/checkout`, { microCents, returnTo });
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Turns automatic top-up on or off. **Turning it on is explicit consent**
|
|
856
|
+
* (docs/ai.md §3.5) — the screen shows the charging terms verbatim and calls
|
|
857
|
+
* this only after the box is checked.
|
|
858
|
+
*/
|
|
859
|
+
setAIAutoTopup(org, body) {
|
|
860
|
+
return this.json("PUT", `/v1/orgs/${encodeURIComponent(org)}/ai/autotopup`, body);
|
|
861
|
+
}
|
|
862
|
+
/** One page of the mail log (docs/email.md §6.1). Newest first, cursor-paged. */
|
|
863
|
+
listMailLog(org, name, opts = {}) {
|
|
864
|
+
const q = new URLSearchParams();
|
|
865
|
+
if (opts.direction)
|
|
866
|
+
q.set("direction", opts.direction);
|
|
867
|
+
if (opts.cursor)
|
|
868
|
+
q.set("cursor", opts.cursor);
|
|
869
|
+
if (opts.limit)
|
|
870
|
+
q.set("limit", String(opts.limit));
|
|
871
|
+
const qs = q.toString();
|
|
872
|
+
return this.json("GET", `${this.projectPath(org, name)}/email/log${qs ? "?" + qs : ""}`);
|
|
873
|
+
}
|
|
874
|
+
/** The metadata of one message. */
|
|
875
|
+
getMailLog(org, name, mailId) {
|
|
876
|
+
return this.json("GET", `${this.projectPath(org, name)}/email/log/${encodeURIComponent(mailId)}`);
|
|
877
|
+
}
|
|
878
|
+
/** The URL of the raw message — the screen uses it as a download link (.eml). */
|
|
879
|
+
mailLogRawURL(org, name, mailId) {
|
|
880
|
+
return new URL(`${this.projectPath(org, name)}/email/log/${encodeURIComponent(mailId)}/raw`, this.opts.baseUrl).toString();
|
|
881
|
+
}
|
|
882
|
+
/** The raw text. What the detail view draws the headers and body from. */
|
|
883
|
+
getMailLogRaw(org, name, mailId) {
|
|
884
|
+
return this.text("GET", `${this.projectPath(org, name)}/email/log/${encodeURIComponent(mailId)}/raw`);
|
|
885
|
+
}
|
|
886
|
+
// ---- End-user authentication (docs/auth.md §9) ----
|
|
887
|
+
/**
|
|
888
|
+
* Grant, settings, providers, hosts. **The signed-up users do not come** —
|
|
889
|
+
* that table is inside the project DB (docs/auth.md §6) and only the worker's
|
|
890
|
+
* `env.auth` sees it.
|
|
891
|
+
*
|
|
892
|
+
* `providers` carries the client id only. The client secret is sealed and
|
|
893
|
+
* appears in no response.
|
|
283
894
|
*/
|
|
284
895
|
getAuth(org, name) {
|
|
285
896
|
return this.json("GET", `${this.projectPath(org, name)}/auth`);
|
|
286
897
|
}
|
|
287
898
|
/**
|
|
288
|
-
*
|
|
899
|
+
* Turns auth on (member). Idempotent.
|
|
289
900
|
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
901
|
+
* **With no database it is 409 `database_required`** — the user table lives
|
|
902
|
+
* inside the project DB, so standing up the grant alone leaves no table the
|
|
903
|
+
* moment the worker starts. The caller catches that code and points at the
|
|
904
|
+
* `"database": true` declaration — the deploy never sees this code, because
|
|
905
|
+
* its ordering rules it out.
|
|
293
906
|
*/
|
|
294
907
|
grantAuth(org, name) {
|
|
295
908
|
return this.json("POST", `${this.projectPath(org, name)}/auth`);
|
|
296
909
|
}
|
|
297
910
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
911
|
+
* Changes the settings (member). **A partial update** — only the fields given
|
|
912
|
+
* change. Sending the whole thing on one switch means that when two people
|
|
913
|
+
* touch the same card, the later save overwrites the earlier one. With no
|
|
914
|
+
* grant it is 404 `not_granted`.
|
|
301
915
|
*/
|
|
302
916
|
updateAuthSettings(org, name, settings) {
|
|
303
917
|
return this.json("PATCH", `${this.projectPath(org, name)}/auth`, { settings });
|
|
304
918
|
}
|
|
305
919
|
/**
|
|
306
|
-
*
|
|
307
|
-
*
|
|
920
|
+
* Turns auth off (admin). **The signed-up users are not deleted** — the
|
|
921
|
+
* `runlot_auth` tables stay in the project DB, and turning it back on lets the
|
|
922
|
+
* same people log in unchanged.
|
|
308
923
|
*/
|
|
309
924
|
revokeAuth(org, name) {
|
|
310
925
|
return this.json("DELETE", `${this.projectPath(org, name)}/auth`);
|
|
311
926
|
}
|
|
312
927
|
/**
|
|
313
|
-
*
|
|
928
|
+
* Registers or replaces a social provider (admin). It is the same PUT, so it
|
|
929
|
+
* is idempotent.
|
|
314
930
|
*
|
|
315
|
-
* **`clientSecret`
|
|
316
|
-
* `https://<host>/__runlot/auth/callback/<provider>`
|
|
317
|
-
*
|
|
931
|
+
* **`clientSecret` cannot be read back.** The callback URL is
|
|
932
|
+
* `https://<host>/__runlot/auth/callback/<provider>` for each host, and the
|
|
933
|
+
* list of hosts is in the response's `hosts`.
|
|
934
|
+
*
|
|
935
|
+
* `issuer` is the base URL of your own GitHub Enterprise Server installation,
|
|
936
|
+
* and only `github` takes one. **This PUT is the whole row** — the same reason
|
|
937
|
+
* `clientSecret` has to be sent every time — so an empty string clears it and
|
|
938
|
+
* puts that provider back on github.com. The key is left out entirely when the
|
|
939
|
+
* argument is omitted, so a caller that does not know about issuers sends the
|
|
940
|
+
* body it always sent.
|
|
318
941
|
*/
|
|
319
|
-
setAuthProvider(org, name, provider, clientId, clientSecret) {
|
|
320
|
-
return this.json("PUT", `${this.projectPath(org, name)}/auth/providers/${encodeURIComponent(provider)}`, { clientId, clientSecret });
|
|
942
|
+
setAuthProvider(org, name, provider, clientId, clientSecret, issuer) {
|
|
943
|
+
return this.json("PUT", `${this.projectPath(org, name)}/auth/providers/${encodeURIComponent(provider)}`, { clientId, clientSecret, ...(issuer === undefined ? {} : { issuer }) });
|
|
321
944
|
}
|
|
322
945
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
946
|
+
* Removes a provider (admin). Someone who signed up only through that provider
|
|
947
|
+
* loses their way in, but the user row stays — register it again and they come
|
|
948
|
+
* back to the same account.
|
|
325
949
|
*/
|
|
326
950
|
deleteAuthProvider(org, name, provider) {
|
|
327
951
|
return this.json("DELETE", `${this.projectPath(org, name)}/auth/providers/${encodeURIComponent(provider)}`);
|
|
328
952
|
}
|
|
329
|
-
// ----
|
|
953
|
+
// ---- Deployment access control (docs/access.md §7) ----
|
|
330
954
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
955
|
+
* Who can reach this deployment. **No values** — both the password and the
|
|
956
|
+
* bypass secret live only as hashes, and what comes back here is the fact that
|
|
957
|
+
* there is one: `hasPassword` and `hasBypass`.
|
|
333
958
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
959
|
+
* A project that has never set a policy is `mode: "public"` with no
|
|
960
|
+
* `updatedAt`. The screen tells those two apart for the reason in §7 — a
|
|
961
|
+
* sentence about the state instead of an empty state.
|
|
336
962
|
*/
|
|
337
963
|
getAccess(org, name) {
|
|
338
964
|
return this.json("GET", `${this.projectPath(org, name)}/access`);
|
|
339
965
|
}
|
|
340
966
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
967
|
+
* Changes the policy (admin). Without a `password`, **the existing password
|
|
968
|
+
* stays** — if a save that only changes the mode wiped the password, coming
|
|
969
|
+
* back to that mode would mean setting it again.
|
|
343
970
|
*/
|
|
344
971
|
setAccess(org, name, mode, password) {
|
|
345
972
|
return this.json("PUT", `${this.projectPath(org, name)}/access`, { mode, password });
|
|
346
973
|
}
|
|
347
974
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
975
|
+
* Makes a bypass secret for automation (admin). **The value is in this answer
|
|
976
|
+
* and nowhere else** — the screen shows it once on arrival and can never fetch
|
|
977
|
+
* it again. Calling again kills the old value.
|
|
350
978
|
*/
|
|
351
979
|
newAccessBypass(org, name) {
|
|
352
980
|
return this.json("POST", `${this.projectPath(org, name)}/access/bypass`);
|
|
353
981
|
}
|
|
354
|
-
/**
|
|
982
|
+
/** Removes the bypass (admin). Removing one that does not exist also succeeds. */
|
|
355
983
|
revokeAccessBypass(org, name) {
|
|
356
984
|
return this.json("DELETE", `${this.projectPath(org, name)}/access/bypass`);
|
|
357
985
|
}
|
|
358
986
|
/**
|
|
359
|
-
*
|
|
987
|
+
* Gets a one-time address back into a protected deployment (docs/access.md §3.4-3).
|
|
360
988
|
*
|
|
361
|
-
*
|
|
362
|
-
* (
|
|
363
|
-
*
|
|
989
|
+
* It is **not** under the project path: all the calling screen knows is the
|
|
990
|
+
* hostname (the original request went there), and the server derives the org
|
|
991
|
+
* and project names from it. Doing `location.replace` with the `redirect` that
|
|
992
|
+
* comes back is the caller's job.
|
|
364
993
|
*/
|
|
365
994
|
authorizeAccess(to, next) {
|
|
366
995
|
return this.json("POST", "/v1/access/authorize", { to, next });
|
|
@@ -368,44 +997,79 @@ export class Client {
|
|
|
368
997
|
listMembers(org) {
|
|
369
998
|
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/members`);
|
|
370
999
|
}
|
|
371
|
-
/**
|
|
1000
|
+
/** This is not an invite — the user must already have logged in once (404 `user_not_found`). */
|
|
372
1001
|
addMember(org, provider, subject, role) {
|
|
373
1002
|
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/members`, { provider, subject, role });
|
|
374
1003
|
}
|
|
375
1004
|
setMemberRole(org, userId, role) {
|
|
376
1005
|
return this.json("PUT", `/v1/orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(userId)}`, { role });
|
|
377
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Removing someone else is admin; **removing yourself is leaving**, which
|
|
1009
|
+
* anyone can do (docs/members.md). The last admin can do neither
|
|
1010
|
+
* (409 `last_admin`).
|
|
1011
|
+
*/
|
|
378
1012
|
removeMember(org, userId) {
|
|
379
1013
|
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(userId)}`);
|
|
380
1014
|
}
|
|
1015
|
+
// --- Invites (docs/members.md) ---
|
|
1016
|
+
/** Pending and expired invites (admin). Accepted and revoked ones are history, so they do not come. */
|
|
1017
|
+
listInvites(org) {
|
|
1018
|
+
return this.json("GET", `/v1/orgs/${encodeURIComponent(org)}/invites`);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Invites by email (admin). **Calling again with the same address is a
|
|
1022
|
+
* resend** — the same row is updated rather than a new one created, and the
|
|
1023
|
+
* server gives 200 instead of 201.
|
|
1024
|
+
*/
|
|
1025
|
+
createInvite(org, email, role) {
|
|
1026
|
+
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/invites`, { email, role });
|
|
1027
|
+
}
|
|
1028
|
+
/** Revokes an invite (admin). Mail that has not gone out yet does not go out. */
|
|
1029
|
+
revokeInvite(org, inviteId) {
|
|
1030
|
+
return this.json("DELETE", `/v1/orgs/${encodeURIComponent(org)}/invites/${encodeURIComponent(inviteId)}`);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* A preview of the invite. **No session needed** — whoever got the link has
|
|
1034
|
+
* not logged in yet, and they need to know what they are accepting before
|
|
1035
|
+
* they do.
|
|
1036
|
+
*/
|
|
1037
|
+
getInvite(token) {
|
|
1038
|
+
return this.json("GET", `/v1/invites/${encodeURIComponent(token)}`);
|
|
1039
|
+
}
|
|
1040
|
+
/** Accepts (login required). If they are already a member the role does not change. */
|
|
1041
|
+
acceptInvite(token) {
|
|
1042
|
+
return this.json("POST", `/v1/invites/${encodeURIComponent(token)}/accept`);
|
|
1043
|
+
}
|
|
381
1044
|
// --- domains: D1 hostnames methods ---
|
|
382
|
-
/**
|
|
1045
|
+
/** The default hostname and the custom domains come together (viewer). */
|
|
383
1046
|
listHostnames(org, name) {
|
|
384
1047
|
return this.json("GET", `${this.projectPath(org, name)}/hostnames`);
|
|
385
1048
|
}
|
|
386
1049
|
/**
|
|
387
|
-
*
|
|
1050
|
+
* Attaches a custom domain (member).
|
|
388
1051
|
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
1052
|
+
* Idempotent — sending the same name again returns the existing row
|
|
1053
|
+
* unchanged. The response's `verify` is the CNAME and TXT the customer has to
|
|
1054
|
+
* add.
|
|
391
1055
|
*/
|
|
392
1056
|
addHostname(org, name, hostname) {
|
|
393
1057
|
return this.json("POST", `${this.projectPath(org, name)}/hostnames`, { hostname });
|
|
394
1058
|
}
|
|
395
|
-
/**
|
|
1059
|
+
/** Runs the same check the background verifier makes, right now (member). */
|
|
396
1060
|
verifyHostname(org, name, host) {
|
|
397
1061
|
return this.json("POST", `${this.projectPath(org, name)}/hostnames/${encodeURIComponent(host)}/verify`);
|
|
398
1062
|
}
|
|
399
|
-
/**
|
|
1063
|
+
/** Detaches a custom domain (member). The default hostname gives a 400. */
|
|
400
1064
|
deleteHostname(org, name, host) {
|
|
401
1065
|
return this.json("DELETE", `${this.projectPath(org, name)}/hostnames/${encodeURIComponent(host)}`);
|
|
402
1066
|
}
|
|
403
1067
|
// --- domains: D2 dns methods ---
|
|
404
|
-
/**
|
|
1068
|
+
/** The organization's zones. No `rrsets` — those ride only on a single-zone lookup. */
|
|
405
1069
|
listZones(org) {
|
|
406
1070
|
return this.json("GET", this.dnsPath(org));
|
|
407
1071
|
}
|
|
408
|
-
/**
|
|
1072
|
+
/** Idempotent. A zone that already exists comes back unchanged. */
|
|
409
1073
|
createZone(org, name) {
|
|
410
1074
|
return this.json("POST", this.dnsPath(org), { name });
|
|
411
1075
|
}
|
|
@@ -416,8 +1080,9 @@ export class Client {
|
|
|
416
1080
|
return this.json("DELETE", `${this.dnsPath(org)}/${encodeURIComponent(zone)}`);
|
|
417
1081
|
}
|
|
418
1082
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
1083
|
+
* Replaces one rrset **whole**. `records` is the complete new set for that
|
|
1084
|
+
* (name, type), so adding a single value means sending the current ones with
|
|
1085
|
+
* it.
|
|
421
1086
|
*/
|
|
422
1087
|
putRRset(org, zone, name, type, ttl, records) {
|
|
423
1088
|
return this.json("PUT", this.rrsetPath(org, zone, name, type), { ttl, records });
|
|
@@ -433,39 +1098,40 @@ export class Client {
|
|
|
433
1098
|
setZoneDS(org, zone, ds) {
|
|
434
1099
|
return this.json("PUT", `${this.dnsPath(org)}/${encodeURIComponent(zone)}/dnssec/ds`, { ds });
|
|
435
1100
|
}
|
|
436
|
-
/**
|
|
1101
|
+
/** The shared prefix of the DNS surface. */
|
|
437
1102
|
dnsPath(org) {
|
|
438
1103
|
return `/v1/orgs/${encodeURIComponent(org)}/dns/zones`;
|
|
439
1104
|
}
|
|
440
|
-
/** `@`
|
|
1105
|
+
/** Both `@` and `*` arrive as a name. Keeps the same string out of four places. */
|
|
441
1106
|
rrsetPath(org, zone, name, type) {
|
|
442
1107
|
return (`${this.dnsPath(org)}/${encodeURIComponent(zone)}` +
|
|
443
1108
|
`/rrsets/${encodeURIComponent(name)}/${encodeURIComponent(type)}`);
|
|
444
1109
|
}
|
|
445
1110
|
// --- domains: D3 domains methods ---
|
|
446
1111
|
/**
|
|
447
|
-
*
|
|
1112
|
+
* Looks a name up (member or above).
|
|
448
1113
|
*
|
|
449
|
-
* `priceCents`
|
|
450
|
-
*
|
|
451
|
-
*
|
|
1114
|
+
* `priceCents` is the first year of registration; `renewalCents` is one year
|
|
1115
|
+
* of renewal. They are separate because a first-year discount is common —
|
|
1116
|
+
* folded into one, the screen cannot say "buy it for $1, then $15 next year".
|
|
452
1117
|
*/
|
|
453
1118
|
checkDomains(org, names) {
|
|
454
1119
|
return this.json("POST", `${this.domainsPath(org)}/check`, { names });
|
|
455
1120
|
}
|
|
456
1121
|
/**
|
|
457
|
-
*
|
|
1122
|
+
* Buys a domain (admin — this spends money).
|
|
458
1123
|
*
|
|
459
|
-
* **ICANN
|
|
460
|
-
*
|
|
1124
|
+
* **The ICANN verification mail goes to `contact.email`.** The screen has to
|
|
1125
|
+
* say so before the purchase (docs/domains.md §5.5).
|
|
461
1126
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
1127
|
+
* Sending the same request again is safe — a finished domain comes back
|
|
1128
|
+
* unchanged, and a purchase that stopped part way resumes from where it
|
|
1129
|
+
* stopped.
|
|
464
1130
|
*/
|
|
465
1131
|
purchaseDomain(org, body) {
|
|
466
1132
|
return this.json("POST", this.domainsPath(org), body);
|
|
467
1133
|
}
|
|
468
|
-
/**
|
|
1134
|
+
/** Imports a domain you already own (admin). Creates the zone only; the registrar is not called. */
|
|
469
1135
|
importDomain(org, name) {
|
|
470
1136
|
return this.json("POST", `${this.domainsPath(org)}/import`, { name });
|
|
471
1137
|
}
|
|
@@ -479,11 +1145,11 @@ export class Client {
|
|
|
479
1145
|
return this.json("POST", `${this.domainPath(org, name)}/renew`, { years });
|
|
480
1146
|
}
|
|
481
1147
|
/**
|
|
482
|
-
*
|
|
1148
|
+
* Changes only the **intent** to auto-renew.
|
|
483
1149
|
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
1150
|
+
* Auto-renew at the registrar stays on — a domain is an asset you lose if you
|
|
1151
|
+
* are a day late, so we renew first and bill afterwards. The screen has to say
|
|
1152
|
+
* this alongside.
|
|
487
1153
|
*/
|
|
488
1154
|
setDomainAutoRenew(org, name, enabled) {
|
|
489
1155
|
return this.json("POST", `${this.domainPath(org, name)}/auto-renew`, { enabled });
|
|
@@ -491,29 +1157,131 @@ export class Client {
|
|
|
491
1157
|
setDomainLock(org, name, locked) {
|
|
492
1158
|
return this.json("POST", `${this.domainPath(org, name)}/lock`, { locked });
|
|
493
1159
|
}
|
|
494
|
-
/**
|
|
1160
|
+
/** The transfer auth code. The audit records that one was issued and **not the code itself**. */
|
|
495
1161
|
domainAuthCode(org, name) {
|
|
496
1162
|
return this.json("POST", `${this.domainPath(org, name)}/auth-code`);
|
|
497
1163
|
}
|
|
498
|
-
/**
|
|
1164
|
+
/** Re-reads from the registrar right now (member). */
|
|
499
1165
|
syncDomain(org, name) {
|
|
500
1166
|
return this.json("POST", `${this.domainPath(org, name)}/sync`);
|
|
501
1167
|
}
|
|
502
|
-
/**
|
|
1168
|
+
/** The shared prefix of the domain surface. Keeps the same string out of ten places. */
|
|
503
1169
|
domainsPath(org) {
|
|
504
1170
|
return `/v1/orgs/${encodeURIComponent(org)}/domains`;
|
|
505
1171
|
}
|
|
506
1172
|
domainPath(org, name) {
|
|
507
1173
|
return `${this.domainsPath(org)}/${encodeURIComponent(name)}`;
|
|
508
1174
|
}
|
|
509
|
-
/**
|
|
1175
|
+
/** The shared prefix of the project surface. */
|
|
510
1176
|
projectPath(org, name) {
|
|
511
1177
|
return `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}`;
|
|
512
1178
|
}
|
|
513
|
-
/**
|
|
1179
|
+
/** The shared prefix of the database surface. Keeps the same string out of five places. */
|
|
514
1180
|
dbPath(org, name) {
|
|
515
1181
|
return `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/database`;
|
|
516
1182
|
}
|
|
1183
|
+
// ---- Self-hosted Git (docs/git-hosting.md §12.1, §19) ----
|
|
1184
|
+
//
|
|
1185
|
+
// **git does the repository work and we handle only the metadata.** That is
|
|
1186
|
+
// why there is nothing like `pushRepo` here — an API that wraps git either
|
|
1187
|
+
// reimplements every one of git's flags or boxes the user in.
|
|
1188
|
+
//
|
|
1189
|
+
// A repository **belongs to a project**. That is why every method starts with
|
|
1190
|
+
// (org, project) — the same shape as the database and the secrets.
|
|
1191
|
+
listRepos(org, project) {
|
|
1192
|
+
return this.json("GET", this.repoPath(org, project));
|
|
1193
|
+
}
|
|
1194
|
+
createRepo(org, project, body) {
|
|
1195
|
+
return this.json("POST", this.repoPath(org, project), body);
|
|
1196
|
+
}
|
|
1197
|
+
getRepo(org, project, name) {
|
|
1198
|
+
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}`);
|
|
1199
|
+
}
|
|
1200
|
+
updateRepo(org, project, name, body) {
|
|
1201
|
+
return this.json("PATCH", `${this.repoPath(org, project)}/${encodeURIComponent(name)}`, body);
|
|
1202
|
+
}
|
|
1203
|
+
deleteRepo(org, project, name) {
|
|
1204
|
+
return this.json("DELETE", `${this.repoPath(org, project)}/${encodeURIComponent(name)}`);
|
|
1205
|
+
}
|
|
1206
|
+
/** The ref history. The lead for recovering a commit lost to a force-push is here. */
|
|
1207
|
+
listRefUpdates(org, project, name) {
|
|
1208
|
+
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/ref-updates`);
|
|
1209
|
+
}
|
|
1210
|
+
// Push-to-deploy (docs/code/push-to-deploy). The hook hangs off the
|
|
1211
|
+
// repository and the builds off the **project**, because a build outlives the
|
|
1212
|
+
// repository it came from.
|
|
1213
|
+
getDeployHook(org, project, name) {
|
|
1214
|
+
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/deploy-hook`);
|
|
1215
|
+
}
|
|
1216
|
+
setDeployHook(org, project, name, body) {
|
|
1217
|
+
return this.json("PUT", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/deploy-hook`, body);
|
|
1218
|
+
}
|
|
1219
|
+
deleteDeployHook(org, project, name) {
|
|
1220
|
+
return this.json("DELETE", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/deploy-hook`);
|
|
1221
|
+
}
|
|
1222
|
+
/** Build a commit without pushing. Omit `commitSha` for the hooked branch's tip. */
|
|
1223
|
+
deployRepoNow(org, project, name, commitSha) {
|
|
1224
|
+
return this.json("POST", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/deploy`, commitSha === undefined ? {} : { commitSha });
|
|
1225
|
+
}
|
|
1226
|
+
listBuilds(org, project, limit) {
|
|
1227
|
+
const q = limit === undefined ? "" : `?limit=${limit}`;
|
|
1228
|
+
return this.json("GET", `${this.projectPath(org, project)}/builds${q}`);
|
|
1229
|
+
}
|
|
1230
|
+
getBuild(org, project, buildId) {
|
|
1231
|
+
return this.json("GET", `${this.projectPath(org, project)}/builds/${encodeURIComponent(buildId)}`);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* A build's output. It comes back as **text**, not JSON — see the endpoint's
|
|
1235
|
+
* description. `json()` would try to parse it and fail on the first line.
|
|
1236
|
+
*/
|
|
1237
|
+
getBuildLog(org, project, buildId) {
|
|
1238
|
+
return this.text("GET", `${this.projectPath(org, project)}/builds/${encodeURIComponent(buildId)}/log`);
|
|
1239
|
+
}
|
|
1240
|
+
cancelBuild(org, project, buildId) {
|
|
1241
|
+
return this.json("POST", `${this.projectPath(org, project)}/builds/${encodeURIComponent(buildId)}/cancel`);
|
|
1242
|
+
}
|
|
1243
|
+
listSSHKeys() {
|
|
1244
|
+
return this.json("GET", "/v1/me/ssh-keys");
|
|
1245
|
+
}
|
|
1246
|
+
addSSHKey(publicKey, name) {
|
|
1247
|
+
return this.json("POST", "/v1/me/ssh-keys", { publicKey, name });
|
|
1248
|
+
}
|
|
1249
|
+
deleteSSHKey(keyId) {
|
|
1250
|
+
return this.json("DELETE", `/v1/me/ssh-keys/${encodeURIComponent(keyId)}`);
|
|
1251
|
+
}
|
|
1252
|
+
listAccessTokens() {
|
|
1253
|
+
return this.json("GET", "/v1/me/tokens");
|
|
1254
|
+
}
|
|
1255
|
+
/** The plaintext is carried in this response **once** and never again. */
|
|
1256
|
+
createAccessToken(body) {
|
|
1257
|
+
return this.json("POST", "/v1/me/tokens", body);
|
|
1258
|
+
}
|
|
1259
|
+
revokeAccessToken(tokenId) {
|
|
1260
|
+
return this.json("DELETE", `/v1/me/tokens/${encodeURIComponent(tokenId)}`);
|
|
1261
|
+
}
|
|
1262
|
+
// ---- Browsing (docs/git-hosting.md §12.2) ----
|
|
1263
|
+
getRepoRefs(org, project, name) {
|
|
1264
|
+
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/refs`);
|
|
1265
|
+
}
|
|
1266
|
+
getRepoTree(org, project, name, ref = "", path = "") {
|
|
1267
|
+
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/tree${gitQuery(ref, path)}`);
|
|
1268
|
+
}
|
|
1269
|
+
getRepoBlob(org, project, name, ref = "", path = "") {
|
|
1270
|
+
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/blob${gitQuery(ref, path)}`);
|
|
1271
|
+
}
|
|
1272
|
+
getRepoCommits(org, project, name, ref = "", limit) {
|
|
1273
|
+
const q = new URLSearchParams();
|
|
1274
|
+
if (ref !== "")
|
|
1275
|
+
q.set("ref", ref);
|
|
1276
|
+
if (limit !== undefined)
|
|
1277
|
+
q.set("limit", String(limit));
|
|
1278
|
+
const qs = q.size > 0 ? `?${q.toString()}` : "";
|
|
1279
|
+
return this.json("GET", `${this.repoPath(org, project)}/${encodeURIComponent(name)}/commits${qs}`);
|
|
1280
|
+
}
|
|
1281
|
+
/** The shared prefix of the repository surface. It sits under the project (§19). */
|
|
1282
|
+
repoPath(org, project) {
|
|
1283
|
+
return `${this.projectPath(org, project)}/repos`;
|
|
1284
|
+
}
|
|
517
1285
|
rollback(org, name, version) {
|
|
518
1286
|
return this.json("POST", `/v1/orgs/${encodeURIComponent(org)}/projects/${encodeURIComponent(name)}/rollback`, { version });
|
|
519
1287
|
}
|