@runlot/api 0.1.0-rc.21 → 0.1.0-rc.211

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