void 0.20.1 → 0.20.3

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.
Files changed (109) hide show
  1. package/README.md +5 -1
  2. package/dist/{auth-W9WII-mN.mjs → auth-DPl6kck4.mjs} +46 -26
  3. package/dist/{auth-cmd-CAH62yDU.mjs → auth-cmd-CzwquNiP.mjs} +5 -4
  4. package/dist/auth-link-ElDTgF7j.mjs +28 -0
  5. package/dist/{build-cmd-CJvZvPQO.mjs → build-cmd-BpJe6boP.mjs} +3 -3
  6. package/dist/{cache-BlNeQjuP.mjs → cache-D98YTqeE.mjs} +3 -3
  7. package/dist/{cancel-deploy-CmlAZ9P6.mjs → cancel-deploy-CPbEQLMG.mjs} +3 -3
  8. package/dist/cf-access-DRsQRe6k.mjs +75 -0
  9. package/dist/cli/cli.mjs +309 -1958
  10. package/dist/cli/env-schema-probe.mjs +11 -2
  11. package/dist/client-BQBrZoCX.mjs +989 -0
  12. package/dist/{cloudflare-auth-B1QtTO1b.mjs → cloudflare-auth-6M5llVPC.mjs} +2 -2
  13. package/dist/{cloudflare-cmd-B6_OZx2V.mjs → cloudflare-cmd-4RPGN3KB.mjs} +2 -2
  14. package/dist/{cloudflare-connect-j5D4hhrG.mjs → cloudflare-connect-t1UU5svD.mjs} +2 -2
  15. package/dist/{cloudflare-operations-CPTpRW6d.mjs → cloudflare-operations-BzWnlC1_.mjs} +1 -1
  16. package/dist/{config-BQFq7QvD.mjs → config-uNGuFsI2.mjs} +1 -1
  17. package/dist/{connect-C04Wdy_h.mjs → connect-WCQZ_u3m.mjs} +6 -6
  18. package/dist/{create-project-ChGZ1DFd.mjs → create-project-D0oXA090.mjs} +7 -7
  19. package/dist/{db-D2d_mUsB.mjs → db-DJ-9qs3S.mjs} +47 -30
  20. package/dist/{delete-D8GigDk8.mjs → delete-BZ4-WaGm.mjs} +3 -3
  21. package/dist/{deploy-iXZ3F0N6.mjs → deploy-BAhhcg5q.mjs} +135 -117
  22. package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
  23. package/dist/{domain-B1VmoSr0.mjs → domain-BxAyhxXN.mjs} +4 -4
  24. package/dist/email-Bj7Cvdwp.mjs +795 -0
  25. package/dist/{env-D4Emu-M_.mjs → env-DBKmK4vc.mjs} +1 -0
  26. package/dist/{env-BcQzYgoG.mjs → env-DP_EErve.mjs} +5 -5
  27. package/dist/{env-validation-ENpMy6Ez.mjs → env-validation-CF6KvTRf.mjs} +3 -1
  28. package/dist/{gen-DI2YwdBM.mjs → gen-B_wPnVTK.mjs} +2 -2
  29. package/dist/{github-cmd-xItS5Zwf.mjs → github-cmd-C-z_xRrQ.mjs} +15 -21
  30. package/dist/{headers-D8QfRX9Y.mjs → headers-BAHwgHdW.mjs} +1 -1
  31. package/dist/help-CwOX-zmI.mjs +2216 -0
  32. package/dist/{inbound-afAcWeQ9.d.mts → inbound-CH5Mksyy.d.mts} +34 -48
  33. package/dist/{inbound-2d0zi2yS.mjs → inbound-aVHEUhKo.mjs} +130 -100
  34. package/dist/index.mjs +54 -17
  35. package/dist/{init-BD-9THgn.mjs → init-BGktCXgA.mjs} +11 -11
  36. package/dist/{link-RMdgjF1v.mjs → link-D2kbqhWb.mjs} +4 -4
  37. package/dist/{list-3F52R_yO.mjs → list-CvkK_G7k.mjs} +4 -4
  38. package/dist/{login-pV69H-ZO.mjs → login-WIjNc77c.mjs} +28 -10
  39. package/dist/{logs-DFHHD6wE.mjs → logs-dLUFCapG.mjs} +4 -4
  40. package/dist/{mime-BJD7d_qL.mjs → mime-D5Nmdzf7.mjs} +23 -9
  41. package/dist/{node-Dk3H2jmU.mjs → node-Ez5KW5rn.mjs} +2 -2
  42. package/dist/operator-auth-B3e08unv.mjs +52 -0
  43. package/dist/operator-client-LUZnlnYk.mjs +82 -0
  44. package/dist/{operator-cmd-DYWRbWUA.mjs → operator-cmd-CKJ7xIRs.mjs} +35 -55
  45. package/dist/{output-tFQLLj26.mjs → output-B0cfNSx5.mjs} +316 -2
  46. package/dist/pages/index.mjs +2 -2
  47. package/dist/platform-auth-config-CdVWRRJr.mjs +368 -0
  48. package/dist/platform-auth-protection-Drl0qhrn.mjs +219 -0
  49. package/dist/platform-auth-recovery-CmKEWpDo.mjs +310 -0
  50. package/dist/{platform-cmd-DxJ2FRwR.mjs → platform-cmd-5q_k56xS.mjs} +16 -6
  51. package/dist/{platform-domain-ChvbJkdy.mjs → platform-domain-4GiDlcqx.mjs} +4 -4
  52. package/dist/{platform-lifecycle-DN4MzJF_.mjs → platform-lifecycle-R9xAxvtG.mjs} +1716 -204
  53. package/dist/{platform-management-Db2PXw0B.mjs → platform-management-BfWsXHEW.mjs} +35 -7
  54. package/dist/{platform-recovery-C_YO-tIs.mjs → platform-recovery-CbK-I1FB.mjs} +6 -5
  55. package/dist/{prepare-CBetXvsN.mjs → prepare-CtDJjoOj.mjs} +2 -2
  56. package/dist/{prepare-BfJvFUtJ.mjs → prepare-blNRQvQl.mjs} +2 -2
  57. package/dist/prerender-render.d.mts +11 -0
  58. package/dist/prerender-render.mjs +111 -0
  59. package/dist/{project-cmd-Mo0V9yKS.mjs → project-cmd-CTdmnzvc.mjs} +32 -14
  60. package/dist/project-team-CGxsQe3_.mjs +132 -0
  61. package/dist/project-token-Cirx7uwZ.mjs +75 -0
  62. package/dist/{provision-Blnstcm2.mjs → provision-CSJOjjQk.mjs} +2 -0
  63. package/dist/{requests-BcKOVpRg.mjs → requests-4Nq59hOr.mjs} +3 -3
  64. package/dist/{rollback-Bx85-0xh.mjs → rollback-Dr7u0Ljx.mjs} +4 -4
  65. package/dist/runtime/ai.mjs +3 -2
  66. package/dist/runtime/email/testing.d.mts +1 -1
  67. package/dist/runtime/email/testing.mjs +3 -3
  68. package/dist/runtime/email-protocol.d.mts +15 -0
  69. package/dist/runtime/email-protocol.mjs +70 -0
  70. package/dist/runtime/email.d.mts +2 -2
  71. package/dist/runtime/email.mjs +189 -96
  72. package/dist/runtime/remote/index.mjs +54 -11
  73. package/dist/runtime/sandbox.d.mts +4 -56
  74. package/dist/runtime/sandbox.mjs +81 -220
  75. package/dist/{secret-ByhJ9AMl.mjs → secret-AcPi-FoA.mjs} +5 -5
  76. package/dist/{skills-Q46GZMO-.mjs → skills-C0RvGjeE.mjs} +1 -1
  77. package/dist/{subcommand-prompt-WfySCQ7S.mjs → subcommand-prompt-Bmyn5Rlc.mjs} +1 -1
  78. package/package.json +12 -7
  79. package/skills/void/SKILL.md +35 -4
  80. package/skills/void/docs/guide/deployment.md +16 -14
  81. package/skills/void/docs/guide/email.md +114 -120
  82. package/skills/void/docs/guide/platform/administration/access.md +163 -0
  83. package/skills/void/docs/guide/platform/administration/email.md +121 -0
  84. package/skills/void/docs/guide/platform/administration/operations.md +97 -0
  85. package/skills/void/docs/guide/platform/administration/projects.md +54 -0
  86. package/skills/void/docs/guide/platform/development/local.md +119 -0
  87. package/skills/void/docs/guide/platform/development/runtime.md +124 -0
  88. package/skills/void/docs/guide/platform/development/schema-ci.md +95 -0
  89. package/skills/void/docs/guide/platform/installation/ci.md +55 -0
  90. package/skills/void/docs/guide/platform/installation/credentials.md +80 -0
  91. package/skills/void/docs/guide/platform/installation/domains.md +68 -0
  92. package/skills/void/docs/guide/platform/installation/first-deployment.md +82 -0
  93. package/skills/void/docs/guide/platform/installation/maintenance.md +137 -0
  94. package/skills/void/docs/guide/platform/installation/prerequisites.md +86 -0
  95. package/skills/void/docs/guide/platform/installation/setup.md +169 -0
  96. package/skills/void/docs/guide/platform/installation/uninstall.md +54 -0
  97. package/skills/void/docs/guide/platform-administration.md +6 -202
  98. package/skills/void/docs/guide/platform-development.md +5 -254
  99. package/skills/void/docs/guide/project-collaboration.md +94 -0
  100. package/skills/void/docs/guide/sandboxes.md +9 -24
  101. package/skills/void/docs/guide/self-hosted-platform.md +13 -542
  102. package/skills/void/docs/reference/api.md +34 -34
  103. package/skills/void/docs/reference/cli.md +276 -31
  104. package/skills/void/docs/reference/config.md +1 -1
  105. package/skills/void/docs/reference/resource-inference.md +10 -10
  106. package/dist/cf-access-AJ1ehiFR.mjs +0 -42
  107. package/dist/cf-access-DsSsZUPr.mjs +0 -67
  108. package/dist/client-Clirrol3.mjs +0 -705
  109. package/dist/email-uKyQYUVY.mjs +0 -1016
@@ -0,0 +1,989 @@
1
+ import { n as __exportAll } from "./rolldown-runtime-rQ84J-ij.mjs";
2
+ import { o as join } from "./dist-BrsS7cai.mjs";
3
+ import { B as ensureCloudflaredToken, U as isCloudflareAccessChallenge, V as ensurePlatformAccessToken, X as normalizePlatformApiUrl, p as getPackageDir } from "./output-B0cfNSx5.mjs";
4
+ import { t as cfAccessHeaders } from "./cf-access-DRsQRe6k.mjs";
5
+ import { o as getApiUrl } from "./auth-DPl6kck4.mjs";
6
+ import { readFileSync } from "node:fs";
7
+ import { stripVTControlCharacters } from "node:util";
8
+ //#region src/cli/platform-errors.ts
9
+ function parsePlatformErrorBody(body) {
10
+ try {
11
+ const parsed = JSON.parse(body);
12
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
13
+ const result = {};
14
+ for (const key of [
15
+ "error",
16
+ "message",
17
+ "deployment",
18
+ "minVersion",
19
+ "clientVersion"
20
+ ]) {
21
+ const value = parsed[key];
22
+ if (typeof value === "string") result[key] = value;
23
+ }
24
+ return result;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function plainExplanation(value) {
30
+ const text = value ? stripVTControlCharacters(value).replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, " ").trim() : void 0;
31
+ return text && !text.startsWith("{") && !text.startsWith("[") && !/<[^>]+>/.test(text) ? text : void 0;
32
+ }
33
+ function platformErrorMessage(payload) {
34
+ const message = plainExplanation(payload?.message);
35
+ return message && !/^(forbidden|unauthorized|access denied|permission denied)[.!]?$/i.test(message) ? message : void 0;
36
+ }
37
+ function unexplainedRejection(status) {
38
+ if (status === 401) return "Authentication was rejected. Check the credentials for the selected platform and any access gateway in front of it.";
39
+ if (status === 403) return "Access was denied by the platform or its access gateway. Check the selected platform and ask its administrator to confirm your access.";
40
+ return "The platform did not provide an explanation. Retry the operation; if it still fails, contact the platform administrator.";
41
+ }
42
+ function permissionMessage(permission) {
43
+ if (permission === "owner") return "Only the project owner can perform this action. Ask the owner to run it using their platform login.";
44
+ if (permission === "read") return "This action requires project membership (reader or higher). Ask a project admin or owner for access.";
45
+ if (permission === "settings" || permission === "members") return "This action requires admin or owner access. Ask a project admin or owner to confirm your access.";
46
+ if (permission === "configure" || permission === "deploy") return "This action requires collaborator, admin, or owner access. Ask a project admin or owner to confirm your access.";
47
+ return "Ask the platform administrator to check your access to this action.";
48
+ }
49
+ const rejectionMessages = {
50
+ "invalid or expired token": "The platform reported an invalid or expired token. Sign in again with `void connect <platform-url>`, or replace `VOID_TOKEN` if you supplied it.",
51
+ "missing authorization header": "A platform login or token is required. Sign in with `void connect <platform-url>`, or supply a valid `VOID_TOKEN` for that platform.",
52
+ "session revoked; sign in again": "Your platform session was revoked. Sign in again with `void connect <platform-url>`, or replace `VOID_TOKEN` if you supplied it.",
53
+ "user not found": "This account is no longer available. Contact the platform administrator or sign in with an active account on this platform.",
54
+ "deploy credential revoked or expired": "This project deploy credential was revoked or expired. Ask the project owner to issue a replacement and update the credential used by your automation.",
55
+ "deploy token build is no longer active": "This build is no longer active. Start a new build to obtain a valid deployment credential.",
56
+ github_identity_required: "Link your GitHub account with `void github link`, then retry this operation.",
57
+ connect_authorization_required: "This GitHub repository requires authorization. Run `void github connect` in an interactive terminal to authorize access, then retry.",
58
+ "invalid connect grant": "GitHub authorization failed or expired. Run `void github connect` again.",
59
+ "installation access revoked": "Access to this GitHub installation was revoked. Ask its administrator to restore access, then run `void github join`.",
60
+ "account suspended": "This account is suspended. Contact the platform administrator to restore access.",
61
+ "project owner suspended": "The project owner is suspended. Ask the owner to contact the platform administrator.",
62
+ "project is being deleted": "This project is being deleted. Wait for deletion to finish before creating a replacement.",
63
+ "registered user not found": "No registered account matches that email address. Ask the recipient to sign in to this platform first, then invite their registered email.",
64
+ "the project owner cannot be invited or changed as a member": "The project owner cannot be invited or changed through team membership commands. Choose another member.",
65
+ "the project owner cannot leave the project": "The project owner cannot leave. Ask a platform administrator to transfer ownership first.",
66
+ "user is already a project member": "This user is already a project member. Use `void project team list` to find their user ID, then `void project team role` to change their role.",
67
+ "project member not found": "This user is no longer a project member. Run `void project team list` to check current members.",
68
+ "project has too many pending invitations": "This project has reached its pending invitation limit. Run `void project team invitations` and revoke invitations that are no longer needed."
69
+ };
70
+ function formatPlatformApiError(action, status, body, permission) {
71
+ const parsed = parsePlatformErrorBody(body);
72
+ const message = platformErrorMessage(parsed);
73
+ if (status === 426 && parsed?.error === "cli_outdated" && message) return message;
74
+ if (!parsed) {
75
+ if (status === 401 || status === 403) {
76
+ const plain = plainExplanation(body);
77
+ return `${action} (HTTP ${status}): ${plain && !/^(forbidden|unauthorized|access denied|permission denied)[.!]?$/i.test(plain) ? plain.replace(/\s+/g, " ").slice(0, 240) : unexplainedRejection(status)}`;
78
+ }
79
+ return `${action}: ${status} ${body}`;
80
+ }
81
+ return `${action} (HTTP ${status}): ${(message ?? (status === 403 && parsed.error === "forbidden" ? `Access denied. ${permissionMessage(permission)}` : (parsed.error && Object.hasOwn(rejectionMessages, parsed.error) ? rejectionMessages[parsed.error] : void 0) ?? (status === 401 && ["unauthorized", "missing token"].includes(parsed.error ?? "") ? rejectionMessages["invalid or expired token"] : plainExplanation(parsed.error)))) || unexplainedRejection(status)}`;
82
+ }
83
+ //#endregion
84
+ //#region src/cli/client.ts
85
+ var client_exports = /* @__PURE__ */ __exportAll({
86
+ PlatformApiError: () => PlatformApiError,
87
+ PlatformClient: () => PlatformClient,
88
+ isCliOutdatedError: () => isCliOutdatedError,
89
+ isExpiredTokenError: () => isExpiredTokenError,
90
+ readCliVersion: () => readCliVersion
91
+ });
92
+ /**
93
+ * Current CLI version. Sent as `X-Void-CLI-Version` on every platform
94
+ * request so the platform's CLI-version gate can refuse stale CLIs
95
+ * cleanly (HTTP 426 + `cli_outdated`) instead of letting a wire
96
+ * mismatch surface as an opaque parse error.
97
+ *
98
+ * Resolved once per process. Returns `null` when package.json can't be
99
+ * read or has no `version` string — the ctor then OMITS the header
100
+ * entirely. We deliberately do NOT send a placeholder string like
101
+ * "unknown": the platform treats present-but-unparseable headers as a
102
+ * stale-CLI signal and would 426 the request, bricking the CLI on any
103
+ * packaging error. Missing-header is the documented pass-through path.
104
+ */
105
+ function readCliVersion() {
106
+ try {
107
+ const pkgPath = join(getPackageDir(), "package.json");
108
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
109
+ return typeof pkg.version === "string" ? pkg.version : null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ const VOID_CLI_VERSION = readCliVersion();
115
+ /**
116
+ * Detects the `cli_outdated` 426 response shape emitted by the platform's
117
+ * `cliVersionGate` middleware. When present, callers should surface the
118
+ * server's `message` field verbatim — it already includes upgrade
119
+ * instructions — rather than the default `<action>: <status> <body>`
120
+ * which would dump raw JSON at the user.
121
+ */
122
+ function isCliOutdatedError(err) {
123
+ if (!(err instanceof PlatformApiError)) return false;
124
+ if (err.status !== 426) return false;
125
+ return parsePlatformErrorBody(err.body)?.error === "cli_outdated";
126
+ }
127
+ var PlatformApiError = class extends Error {
128
+ status;
129
+ body;
130
+ code;
131
+ constructor(action, status, body, permission) {
132
+ const parsed = parsePlatformErrorBody(body);
133
+ super(formatPlatformApiError(action, status, body, permission));
134
+ this.name = "PlatformApiError";
135
+ this.status = status;
136
+ this.body = body;
137
+ this.code = parsed?.error ?? parsed?.message;
138
+ }
139
+ };
140
+ async function throwPlatformApiError(action, res, permission) {
141
+ throw new PlatformApiError(action, res.status, await res.text(), isCloudflareAccessChallenge(res) ? void 0 : permission);
142
+ }
143
+ function isExpiredTokenError(error) {
144
+ if (error instanceof PlatformApiError) return error.status === 401 && error.code === "invalid or expired token";
145
+ return false;
146
+ }
147
+ function isPreflightRejection(error) {
148
+ return error instanceof PlatformApiError && ([401, 403].includes(error.status) && !!parsePlatformErrorBody(error.body)?.error || isCliOutdatedError(error));
149
+ }
150
+ /**
151
+ * Retry contract for the deploy POST's transport phase.
152
+ *
153
+ * - Only `fetch` rejections (`TypeError` — no HTTP response) are retried.
154
+ * A Response of any status means the server answered; surface it
155
+ * verbatim rather than replay a request it may have accepted.
156
+ * - A rejected fetch does NOT prove the request never arrived — the
157
+ * connection may have dropped after delivery. Arbitration is left to
158
+ * the server's precheck, the only atomic (project-locked) verdict: if
159
+ * an earlier attempt or a concurrent deploy is still in flight, the
160
+ * replay is rejected with 409 and surfaced as a deploy conflict
161
+ * naming the deployment id. A client-side GET-then-decide lookup can
162
+ * never be authoritative (the row can transition between lookup and
163
+ * replay), so no dedupe query is made — the residual case is the lost
164
+ * attempt completing within seconds and being redeployed identically.
165
+ */
166
+ const DEPLOY_POST_MAX_RETRIES = 3;
167
+ const DEPLOY_POST_BACKOFF_MS = [
168
+ 1e3,
169
+ 5e3,
170
+ 1e4
171
+ ];
172
+ /**
173
+ * HTTP client for the Void platform API.
174
+ */
175
+ var PlatformClient = class {
176
+ baseUrl;
177
+ authHeaders;
178
+ accessHeaders;
179
+ cliLog;
180
+ apiUrl;
181
+ constructor(token, options) {
182
+ if (typeof options === "string") this.baseUrl = normalizePlatformApiUrl(options);
183
+ else {
184
+ this.baseUrl = normalizePlatformApiUrl(options?.apiUrl ?? getApiUrl(options?.root));
185
+ this.cliLog = options?.cliLog;
186
+ }
187
+ this.apiUrl = this.baseUrl;
188
+ ensureCloudflaredToken(this.baseUrl);
189
+ this.accessHeaders = cfAccessHeaders(process.env, this.baseUrl);
190
+ this.authHeaders = {
191
+ Authorization: `Bearer ${token}`,
192
+ ...VOID_CLI_VERSION !== null && { "X-Void-CLI-Version": VOID_CLI_VERSION }
193
+ };
194
+ }
195
+ async request(url, options = {}) {
196
+ if (new URL(url).origin !== this.baseUrl) throw new Error("Platform credentials cannot be sent to another origin.");
197
+ if (!this.accessHeaders["CF-Access-Client-Id"]) ensureCloudflaredToken(this.baseUrl);
198
+ const send = () => {
199
+ const currentAccessHeaders = cfAccessHeaders(process.env, this.baseUrl);
200
+ const accessHeaders = this.accessHeaders["CF-Access-Client-Id"] ? this.accessHeaders : Object.keys(currentAccessHeaders).length > 0 ? currentAccessHeaders : this.accessHeaders;
201
+ return {
202
+ accessHeaders,
203
+ response: fetch(url, {
204
+ ...options,
205
+ headers: {
206
+ ...options.headers,
207
+ ...accessHeaders
208
+ },
209
+ redirect: "manual"
210
+ })
211
+ };
212
+ };
213
+ const first = send();
214
+ const response = await first.response;
215
+ if (!isCloudflareAccessChallenge(response)) return response;
216
+ if (first.accessHeaders["CF-Access-Client-Id"]) return response;
217
+ await response.body?.cancel();
218
+ ensurePlatformAccessToken(this.baseUrl, true, false, first.accessHeaders["cf-access-token"]);
219
+ return send().response;
220
+ }
221
+ async startIdentityLink(connectionId) {
222
+ const response = await this.request(this.baseUrl + "/auth/link", {
223
+ method: "POST",
224
+ headers: {
225
+ ...this.authHeaders,
226
+ "Content-Type": "application/json"
227
+ },
228
+ body: JSON.stringify({ connectionId })
229
+ });
230
+ if (!response.ok) await throwPlatformApiError("Failed to link login method", response);
231
+ const result = await response.json();
232
+ const url = new URL(result.url);
233
+ if (url.origin !== this.baseUrl || url.pathname !== "/auth/link" || url.username || url.password) throw new Error("auth: Invalid account-link URL from platform.");
234
+ return result;
235
+ }
236
+ async whoami() {
237
+ const res = await this.request(`${this.baseUrl}/me`, { headers: { ...this.authHeaders } });
238
+ if (!res.ok) await throwPlatformApiError("Failed to fetch user info", res);
239
+ return res.json();
240
+ }
241
+ async listProjects() {
242
+ const res = await this.request(`${this.baseUrl}/projects`, { headers: { ...this.authHeaders } });
243
+ if (!res.ok) await throwPlatformApiError("Failed to list projects", res);
244
+ return res.json();
245
+ }
246
+ async listProjectTokens(projectId) {
247
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/tokens`, { headers: { ...this.authHeaders } });
248
+ if (!res.ok) await throwPlatformApiError("Failed to list project tokens", res, "owner");
249
+ return res.json();
250
+ }
251
+ async createProjectToken(projectId, input) {
252
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/tokens`, {
253
+ method: "POST",
254
+ headers: {
255
+ ...this.authHeaders,
256
+ "Content-Type": "application/json"
257
+ },
258
+ body: JSON.stringify(input)
259
+ });
260
+ if (!res.ok) await throwPlatformApiError("Failed to create project token", res, "owner");
261
+ return res.json();
262
+ }
263
+ async listProjectMembers(projectId) {
264
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/members`, { headers: { ...this.authHeaders } });
265
+ if (!res.ok) await throwPlatformApiError("Failed to list project members", res, "read");
266
+ return (await res.json()).members;
267
+ }
268
+ async listProjectInvitations(projectId) {
269
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/invitations`, { headers: { ...this.authHeaders } });
270
+ if (!res.ok) await throwPlatformApiError("Failed to list project invitations", res, "members");
271
+ return (await res.json()).invitations;
272
+ }
273
+ async inviteProjectMember(projectId, email, role) {
274
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/invitations`, {
275
+ method: "POST",
276
+ headers: {
277
+ "Content-Type": "application/json",
278
+ ...this.authHeaders
279
+ },
280
+ body: JSON.stringify({
281
+ email,
282
+ role
283
+ })
284
+ });
285
+ if (!res.ok) await throwPlatformApiError("Failed to invite project member", res, "members");
286
+ return res.json();
287
+ }
288
+ async renewProjectToken(projectId, credentialId, expiresInDays) {
289
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/tokens/${encodeURIComponent(credentialId)}/renew`, {
290
+ method: "POST",
291
+ headers: {
292
+ ...this.authHeaders,
293
+ "Content-Type": "application/json"
294
+ },
295
+ body: JSON.stringify(expiresInDays ? { expiresInDays } : {})
296
+ });
297
+ if (!res.ok) await throwPlatformApiError("Failed to renew project token", res, "owner");
298
+ return res.json();
299
+ }
300
+ async updateProjectMemberRole(projectId, userId, role) {
301
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/members/${encodeURIComponent(userId)}`, {
302
+ method: "PATCH",
303
+ headers: {
304
+ "Content-Type": "application/json",
305
+ ...this.authHeaders
306
+ },
307
+ body: JSON.stringify({ role })
308
+ });
309
+ if (!res.ok) await throwPlatformApiError("Failed to change project member role", res, "members");
310
+ return res.json();
311
+ }
312
+ async revokeProjectToken(projectId, credentialId) {
313
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/tokens/${encodeURIComponent(credentialId)}`, {
314
+ method: "DELETE",
315
+ headers: { ...this.authHeaders }
316
+ });
317
+ if (!res.ok) await throwPlatformApiError("Failed to revoke project token", res, "owner");
318
+ return res.json();
319
+ }
320
+ async removeProjectMember(projectId, userId) {
321
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/members/${encodeURIComponent(userId)}`, {
322
+ method: "DELETE",
323
+ headers: { ...this.authHeaders }
324
+ });
325
+ if (!res.ok) await throwPlatformApiError("Failed to remove project member", res, "members");
326
+ }
327
+ async revokeProjectInvitation(projectId, invitationId) {
328
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/invitations/${encodeURIComponent(invitationId)}`, {
329
+ method: "DELETE",
330
+ headers: { ...this.authHeaders }
331
+ });
332
+ if (!res.ok) await throwPlatformApiError("Failed to revoke project invitation", res, "members");
333
+ }
334
+ async leaveProject(projectId) {
335
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/leave`, {
336
+ method: "POST",
337
+ headers: { ...this.authHeaders }
338
+ });
339
+ if (!res.ok) await throwPlatformApiError("Failed to leave project", res);
340
+ }
341
+ async listPendingProjectInvitations() {
342
+ const res = await this.request(`${this.baseUrl}/me/project-invitations`, { headers: { ...this.authHeaders } });
343
+ if (!res.ok) await throwPlatformApiError("Failed to list pending project invitations", res);
344
+ return (await res.json()).invitations;
345
+ }
346
+ async respondToProjectInvitation(invitationId, action) {
347
+ const res = await this.request(`${this.baseUrl}/project-invitations/${encodeURIComponent(invitationId)}/${action}`, {
348
+ method: "POST",
349
+ headers: { ...this.authHeaders }
350
+ });
351
+ if (!res.ok) await throwPlatformApiError(`Failed to ${action} project invitation`, res);
352
+ }
353
+ async listDeployments(projectId) {
354
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/deployments`, { headers: { ...this.authHeaders } });
355
+ if (!res.ok) await throwPlatformApiError("Failed to list deployments", res, "read");
356
+ return res.json();
357
+ }
358
+ async rollback(projectId, deploymentId, options) {
359
+ const body = { deploymentId };
360
+ if (options?.allowDestructive && options.allowDestructive.length > 0) body.allowDestructive = options.allowDestructive;
361
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/rollback`, {
362
+ method: "POST",
363
+ headers: {
364
+ "Content-Type": "application/json",
365
+ ...this.authHeaders
366
+ },
367
+ body: JSON.stringify(body)
368
+ });
369
+ if (!res.ok) await throwPlatformApiError("Rollback failed", res, "deploy");
370
+ return res.json();
371
+ }
372
+ async cancelDeployment(projectId, deploymentId) {
373
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/deployments/${deploymentId}/cancel`, {
374
+ method: "POST",
375
+ headers: { ...this.authHeaders }
376
+ });
377
+ if (!res.ok) await throwPlatformApiError("Cancel deployment failed", res, "deploy");
378
+ return res.json();
379
+ }
380
+ async createProject(slug) {
381
+ const res = await this.request(`${this.baseUrl}/projects`, {
382
+ method: "POST",
383
+ headers: {
384
+ "Content-Type": "application/json",
385
+ ...this.authHeaders
386
+ },
387
+ body: JSON.stringify({ slug })
388
+ });
389
+ if (!res.ok) await throwPlatformApiError("Failed to create project", res);
390
+ return res.json();
391
+ }
392
+ async preflight(projectId, assetManifest, hasWorker) {
393
+ const total = Object.keys(assetManifest).length;
394
+ try {
395
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/deploy/preflight`, {
396
+ method: "POST",
397
+ headers: {
398
+ "Content-Type": "application/json",
399
+ ...this.authHeaders
400
+ },
401
+ body: JSON.stringify({
402
+ assetManifest,
403
+ ...hasWorker && { hasWorker }
404
+ })
405
+ });
406
+ if (!res.ok) {
407
+ const error = new PlatformApiError("Preflight failed", res.status, await res.text(), isCloudflareAccessChallenge(res) ? void 0 : "deploy");
408
+ if (isPreflightRejection(error)) throw error;
409
+ return {
410
+ needed: Object.keys(assetManifest),
411
+ total,
412
+ skipped: 0
413
+ };
414
+ }
415
+ return res.json();
416
+ } catch (error) {
417
+ if (isPreflightRejection(error)) throw error;
418
+ return {
419
+ needed: Object.keys(assetManifest),
420
+ total,
421
+ skipped: 0
422
+ };
423
+ }
424
+ }
425
+ /**
426
+ * Request presigned R2 PUT URLs for a batch of assets. Each entry carries
427
+ * both `blake3` (storage key) and `md5` (wire-level integrity primitive)
428
+ * so the platform can sign a PUT URL that R2 enforces against the bytes
429
+ * the CLI is about to upload. See design doc 0068 for the full protocol.
430
+ */
431
+ async requestUploadUrls(projectId, assets) {
432
+ const url = `${this.baseUrl}/projects/${projectId}/deploy/upload-urls`;
433
+ this.cliLog?.info("upload_urls_post", {
434
+ url,
435
+ assetCount: assets.length
436
+ });
437
+ const res = await this.request(url, {
438
+ method: "POST",
439
+ headers: {
440
+ "Content-Type": "application/json",
441
+ ...this.authHeaders
442
+ },
443
+ body: JSON.stringify({ assets })
444
+ });
445
+ const cfRay = res.headers.get("cf-ray");
446
+ const cfCacheStatus = res.headers.get("cf-cache-status");
447
+ this.cliLog?.info("upload_urls_response", {
448
+ status: res.status,
449
+ cfRay,
450
+ cfCacheStatus
451
+ });
452
+ if (!res.ok) await throwPlatformApiError("Failed to request upload URLs", res, "deploy");
453
+ return (await res.json()).urls;
454
+ }
455
+ /**
456
+ * Manifest-only (direct-to-R2) deploy. POSTs a JSON body containing the
457
+ * deploy manifest, asset manifest (no asset bytes), and any worker module
458
+ * bytes (base64-encoded). Streams an NDJSON response that the caller can
459
+ * apply with the existing `applyDeployEvent` machinery.
460
+ */
461
+ async *finalizeDeploy(projectId, payload) {
462
+ const url = `${this.baseUrl}/projects/${projectId}/deploy`;
463
+ yield* this.streamDeployRequest(url, projectId, {
464
+ method: "POST",
465
+ headers: {
466
+ "Content-Type": "application/json",
467
+ ...this.authHeaders
468
+ },
469
+ body: JSON.stringify(payload)
470
+ });
471
+ }
472
+ /**
473
+ * POST + NDJSON-stream-reader used by `finalizeDeploy()`. Captures
474
+ * `cf-ray` + `cf-cache-status` for postmortem correlation.
475
+ */
476
+ async *streamDeployRequest(url, projectId, init) {
477
+ const cliLog = this.cliLog;
478
+ cliLog?.info("deploy_post", { url });
479
+ let res;
480
+ let attempt = 0;
481
+ while (true) try {
482
+ res = await this.request(url, init);
483
+ break;
484
+ } catch (err) {
485
+ if (!(err instanceof TypeError) || attempt >= DEPLOY_POST_MAX_RETRIES) {
486
+ cliLog?.error("deploy_fetch_rejected", err, {
487
+ url,
488
+ attempts: attempt + 1
489
+ });
490
+ throw err;
491
+ }
492
+ cliLog?.warn("deploy_fetch_retry", {
493
+ url,
494
+ attempt: attempt + 1,
495
+ delayMs: DEPLOY_POST_BACKOFF_MS[attempt],
496
+ error: err instanceof Error ? err.message : String(err)
497
+ });
498
+ await sleep(DEPLOY_POST_BACKOFF_MS[attempt]);
499
+ attempt++;
500
+ }
501
+ const cfRay = res.headers.get("cf-ray");
502
+ const cfCacheStatus = res.headers.get("cf-cache-status");
503
+ cliLog?.info("deploy_response", {
504
+ status: res.status,
505
+ cfRay,
506
+ cfCacheStatus,
507
+ contentType: res.headers.get("content-type")
508
+ });
509
+ if (!res.ok) {
510
+ const body = await res.text();
511
+ if (res.status === 409) {
512
+ const parsed = parsePlatformErrorBody(body);
513
+ if (parsed?.deployment) {
514
+ cliLog?.warn("deploy_conflict", {
515
+ url,
516
+ deploymentId: parsed.deployment,
517
+ error: parsed.error
518
+ });
519
+ yield {
520
+ event: "error",
521
+ step: "finalize",
522
+ message: `${platformErrorMessage(parsed) ?? platformErrorMessage({ message: parsed.error }) ?? "deploy in progress"}: deployment ${parsed.deployment} — it may be an earlier attempt of this deploy or a concurrent one; inspect with \`void project status\` before redeploying`
523
+ };
524
+ return;
525
+ }
526
+ }
527
+ throw new PlatformApiError("Deploy failed", res.status, body, isCloudflareAccessChallenge(res) ? void 0 : "deploy");
528
+ }
529
+ const reader = res.body.getReader();
530
+ const decoder = new TextDecoder();
531
+ let buffer = "";
532
+ let bytesRead = 0;
533
+ let firstByteAt = null;
534
+ const startedAt = Date.now();
535
+ let eventCount = 0;
536
+ let lastEventName = "<none>";
537
+ try {
538
+ while (true) {
539
+ const { done, value } = await reader.read();
540
+ if (done) {
541
+ cliLog?.info("deploy_stream_eof", {
542
+ bytesRead,
543
+ eventCount,
544
+ lastEvent: lastEventName
545
+ });
546
+ break;
547
+ }
548
+ if (firstByteAt === null) {
549
+ firstByteAt = Date.now() - startedAt;
550
+ cliLog?.info("deploy_stream_ttfb", { ttfbMs: firstByteAt });
551
+ }
552
+ bytesRead += value.byteLength;
553
+ buffer += decoder.decode(value, { stream: true });
554
+ let newlineIdx;
555
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
556
+ const line = buffer.slice(0, newlineIdx);
557
+ buffer = buffer.slice(newlineIdx + 1);
558
+ if (line.trim()) {
559
+ const event = JSON.parse(line);
560
+ eventCount++;
561
+ lastEventName = event.event ?? "<no-event-field>";
562
+ cliLog?.info("deploy_event", {
563
+ eventCount,
564
+ event: lastEventName,
565
+ raw: line.slice(0, 200)
566
+ });
567
+ yield event;
568
+ }
569
+ }
570
+ }
571
+ } catch (err) {
572
+ cliLog?.error("deploy_stream_error", err, {
573
+ bytesRead,
574
+ eventCount,
575
+ lastEvent: lastEventName
576
+ });
577
+ throw err;
578
+ }
579
+ }
580
+ async listSecrets(projectId) {
581
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/secrets`, { headers: { ...this.authHeaders } });
582
+ if (!res.ok) await throwPlatformApiError("Failed to list secrets", res, "configure");
583
+ return res.json();
584
+ }
585
+ async setSecret(projectId, name, value) {
586
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/secrets/${encodeURIComponent(name)}`, {
587
+ method: "PUT",
588
+ headers: {
589
+ "Content-Type": "application/json",
590
+ ...this.authHeaders
591
+ },
592
+ body: JSON.stringify({ value })
593
+ });
594
+ if (!res.ok) await throwPlatformApiError("Failed to set secret", res, "configure");
595
+ return res.json();
596
+ }
597
+ async deleteProject(projectId) {
598
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}`, {
599
+ method: "DELETE",
600
+ headers: { ...this.authHeaders }
601
+ });
602
+ if (!res.ok) await throwPlatformApiError("Failed to delete project", res, "owner");
603
+ return res.json();
604
+ }
605
+ async getMigrationStatus(projectId, dialect) {
606
+ const url = new URL(`${this.baseUrl}/projects/${projectId}/migrations`);
607
+ if (dialect) url.searchParams.set("dialect", dialect);
608
+ const res = await this.request(url, { headers: { ...this.authHeaders } });
609
+ if (!res.ok) await throwPlatformApiError("Failed to get migration status", res, "read");
610
+ return res.json();
611
+ }
612
+ async applyRemoteMigrations(projectId, migrations) {
613
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/migrations/apply`, {
614
+ method: "POST",
615
+ headers: {
616
+ "Content-Type": "application/json",
617
+ ...this.authHeaders
618
+ },
619
+ body: JSON.stringify({ migrations })
620
+ });
621
+ if (!res.ok) await throwPlatformApiError("Failed to apply remote migrations", res, "configure");
622
+ return res.json();
623
+ }
624
+ async renameMigrations(projectId, renames) {
625
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/migrations/rename`, {
626
+ method: "POST",
627
+ headers: {
628
+ "Content-Type": "application/json",
629
+ ...this.authHeaders
630
+ },
631
+ body: JSON.stringify({ renames })
632
+ });
633
+ if (!res.ok) await throwPlatformApiError("Failed to rename remote migrations", res, "configure");
634
+ return res.json();
635
+ }
636
+ async addDomain(projectId, hostname) {
637
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/domains`, {
638
+ method: "POST",
639
+ headers: {
640
+ "Content-Type": "application/json",
641
+ ...this.authHeaders
642
+ },
643
+ body: JSON.stringify({ hostname })
644
+ });
645
+ if (!res.ok) await throwPlatformApiError("Failed to add domain", res, "settings");
646
+ return res.json();
647
+ }
648
+ async listDomains(projectId) {
649
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/domains`, { headers: { ...this.authHeaders } });
650
+ if (!res.ok) await throwPlatformApiError("Failed to list domains", res, "read");
651
+ return res.json();
652
+ }
653
+ async getDomainStatus(projectId, domainId) {
654
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/domains/${domainId}`, { headers: { ...this.authHeaders } });
655
+ if (!res.ok) await throwPlatformApiError("Failed to get domain status", res, "read");
656
+ return res.json();
657
+ }
658
+ async removeDomain(projectId, domainId) {
659
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/domains/${domainId}`, {
660
+ method: "DELETE",
661
+ headers: { ...this.authHeaders }
662
+ });
663
+ if (!res.ok) await throwPlatformApiError("Failed to remove domain", res, "settings");
664
+ return res.json();
665
+ }
666
+ async deleteSecret(projectId, name) {
667
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/secrets/${encodeURIComponent(name)}`, {
668
+ method: "DELETE",
669
+ headers: { ...this.authHeaders }
670
+ });
671
+ if (!res.ok) await throwPlatformApiError("Failed to delete secret", res, "configure");
672
+ return res.json();
673
+ }
674
+ async getResources(projectId) {
675
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/resources`, { headers: { ...this.authHeaders } });
676
+ if (!res.ok) await throwPlatformApiError("Failed to get resources", res, "read");
677
+ return res.json();
678
+ }
679
+ async createPrerenderToken(projectId) {
680
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/prerender-token`, {
681
+ method: "POST",
682
+ headers: { ...this.authHeaders }
683
+ });
684
+ if (!res.ok) await throwPlatformApiError("Failed to create prerender token", res, "deploy");
685
+ return (await res.json()).token;
686
+ }
687
+ async provisionDatabase(projectId, connectionString) {
688
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/database`, {
689
+ method: "POST",
690
+ headers: {
691
+ "Content-Type": "application/json",
692
+ ...this.authHeaders
693
+ },
694
+ body: JSON.stringify({ connectionString })
695
+ });
696
+ if (!res.ok) await throwPlatformApiError("Failed to provision database", res, "configure");
697
+ return res.json();
698
+ }
699
+ async setDatabaseUrl(projectId, connectionString) {
700
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/database-url`, {
701
+ method: "PUT",
702
+ headers: {
703
+ "Content-Type": "application/json",
704
+ ...this.authHeaders
705
+ },
706
+ body: JSON.stringify({ connectionString })
707
+ });
708
+ if (!res.ok) await throwPlatformApiError("Failed to update database URL", res, "configure");
709
+ return res.json();
710
+ }
711
+ async getDatabaseUrl(projectId) {
712
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/database-url`, { headers: { ...this.authHeaders } });
713
+ if (!res.ok) await throwPlatformApiError("Failed to fetch database URL", res, "owner");
714
+ return res.json();
715
+ }
716
+ async purgeCache(projectId) {
717
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/revalidate`, {
718
+ method: "POST",
719
+ headers: {
720
+ "Content-Type": "application/json",
721
+ ...this.authHeaders
722
+ },
723
+ body: JSON.stringify({ all: true })
724
+ });
725
+ if (!res.ok) await throwPlatformApiError("Cache purge failed", res, "deploy");
726
+ return res.json();
727
+ }
728
+ async getLogs(projectId, since, limit = 200, deploymentId, level) {
729
+ const params = new URLSearchParams({
730
+ since,
731
+ limit: String(limit)
732
+ });
733
+ if (deploymentId) params.set("deploymentId", deploymentId);
734
+ if (level && level !== "all") params.set("level", level);
735
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/logs?${params}`, { headers: { ...this.authHeaders } });
736
+ if (!res.ok) await throwPlatformApiError("Failed to fetch logs", res, "read");
737
+ return (await res.json()).data;
738
+ }
739
+ async getRequests(projectId, since, limit = 200, status) {
740
+ const params = new URLSearchParams({
741
+ since,
742
+ limit: String(limit)
743
+ });
744
+ if (status && status !== "all") params.set("status", status);
745
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/requests?${params}`, { headers: { ...this.authHeaders } });
746
+ if (!res.ok) await throwPlatformApiError("Failed to fetch requests", res, "read");
747
+ return (await res.json()).data;
748
+ }
749
+ async listBuilds(projectId, limit = 20) {
750
+ const params = new URLSearchParams({ limit: String(limit) });
751
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/builds?${params}`, { headers: { ...this.authHeaders } });
752
+ if (!res.ok) await throwPlatformApiError("Failed to list builds", res, "read");
753
+ return (await res.json()).builds;
754
+ }
755
+ async getBuildLogs(projectId, buildId, since = 0) {
756
+ const params = new URLSearchParams({ since: String(since) });
757
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/builds/${buildId}/logs?${params}`, { headers: { ...this.authHeaders } });
758
+ if (!res.ok) await throwPlatformApiError("Failed to fetch build logs", res, "read");
759
+ return res.json();
760
+ }
761
+ async getGithubInstallUrl() {
762
+ const res = await this.request(`${this.baseUrl}/github/install-url`, { headers: { ...this.authHeaders } });
763
+ if (!res.ok) await throwPlatformApiError("Failed to get GitHub install URL", res);
764
+ return res.json();
765
+ }
766
+ async listGithubInstallations() {
767
+ const res = await this.request(`${this.baseUrl}/github/installations`, { headers: { ...this.authHeaders } });
768
+ if (!res.ok) await throwPlatformApiError("Failed to list GitHub installations", res);
769
+ return res.json();
770
+ }
771
+ async connectGithub(projectId, body) {
772
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/github`, {
773
+ method: "POST",
774
+ headers: {
775
+ "Content-Type": "application/json",
776
+ ...this.authHeaders
777
+ },
778
+ body: JSON.stringify(body)
779
+ });
780
+ if (!res.ok) await throwPlatformApiError("Failed to connect GitHub repository", res, "settings");
781
+ return res.json();
782
+ }
783
+ /**
784
+ * Start the per-repo connect-authorization round-trip used by a non-owner org
785
+ * member (Task 7 platform contract). The callback redirects back to `redirect`
786
+ * with either `?connect_grant=<signed>` (proof of per-repo access) or
787
+ * `?error=repo_not_accessible`. Mirrors {@link getGithubLinkUrl} but carries
788
+ * the installation + repo the grant is scoped to.
789
+ */
790
+ async getGithubConnectAuthorizeUrl(redirect, codeChallenge, installationId, repoFullName) {
791
+ const params = new URLSearchParams({
792
+ redirect,
793
+ code_challenge: codeChallenge,
794
+ code_challenge_method: "S256",
795
+ installationId,
796
+ repoFullName
797
+ });
798
+ const res = await this.request(`${this.baseUrl}/github/connect-authorize-url?${params}`, { headers: { ...this.authHeaders } });
799
+ if (!res.ok) await throwPlatformApiError("Failed to start GitHub connect authorization", res);
800
+ return res.json();
801
+ }
802
+ async getGithubConnection(projectId) {
803
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/github`, { headers: { ...this.authHeaders } });
804
+ if (!res.ok) await throwPlatformApiError("Failed to get GitHub connection", res, "read");
805
+ return res.json();
806
+ }
807
+ async updateGithubConnection(projectId, body) {
808
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/github`, {
809
+ method: "PATCH",
810
+ headers: {
811
+ "Content-Type": "application/json",
812
+ ...this.authHeaders
813
+ },
814
+ body: JSON.stringify(body)
815
+ });
816
+ if (!res.ok) await throwPlatformApiError("Failed to update GitHub connection", res, "settings");
817
+ return res.json();
818
+ }
819
+ async deleteGithubConnection(projectId) {
820
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/github`, {
821
+ method: "DELETE",
822
+ headers: { ...this.authHeaders }
823
+ });
824
+ if (!res.ok) await throwPlatformApiError("Failed to disconnect GitHub repository", res, "settings");
825
+ return res.json();
826
+ }
827
+ async getGithubLinkUrl(redirect, codeChallenge) {
828
+ const params = new URLSearchParams({
829
+ redirect,
830
+ code_challenge: codeChallenge,
831
+ code_challenge_method: "S256"
832
+ });
833
+ const res = await this.request(`${this.baseUrl}/github/link-url?${params}`, { headers: { ...this.authHeaders } });
834
+ if (!res.ok) await throwPlatformApiError("Failed to start GitHub link", res);
835
+ return res.json();
836
+ }
837
+ async completeGithubLink(linkCode, codeVerifier) {
838
+ const res = await this.request(`${this.baseUrl}/github/link/complete`, {
839
+ method: "POST",
840
+ headers: {
841
+ "Content-Type": "application/json",
842
+ ...this.authHeaders
843
+ },
844
+ body: JSON.stringify({
845
+ link_code: linkCode,
846
+ code_verifier: codeVerifier
847
+ })
848
+ });
849
+ if (!res.ok) await throwPlatformApiError("Failed to complete GitHub link", res);
850
+ return res.json();
851
+ }
852
+ async getGithubJoinUrl(redirect, codeChallenge) {
853
+ const params = new URLSearchParams({
854
+ redirect,
855
+ code_challenge: codeChallenge,
856
+ code_challenge_method: "S256"
857
+ });
858
+ const res = await this.request(`${this.baseUrl}/github/join-url?${params}`, { headers: { ...this.authHeaders } });
859
+ if (!res.ok) await throwPlatformApiError("Failed to start GitHub join", res);
860
+ return res.json();
861
+ }
862
+ async completeGithubJoin(joinCode, codeVerifier) {
863
+ const res = await this.request(`${this.baseUrl}/github/join/complete`, {
864
+ method: "POST",
865
+ headers: {
866
+ "Content-Type": "application/json",
867
+ ...this.authHeaders
868
+ },
869
+ body: JSON.stringify({
870
+ join_code: joinCode,
871
+ code_verifier: codeVerifier
872
+ })
873
+ });
874
+ if (!res.ok) await throwPlatformApiError("Failed to complete GitHub join", res);
875
+ return res.json();
876
+ }
877
+ async getEmailUsage(projectId) {
878
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/usage`, { headers: { ...this.authHeaders } });
879
+ if (!res.ok) await throwPlatformApiError("Failed to fetch email usage", res, "read");
880
+ return res.json();
881
+ }
882
+ async getEmailLogs(projectId, limit) {
883
+ const url = new URL(`${this.baseUrl}/projects/${projectId}/email/logs`);
884
+ if (limit !== void 0) url.searchParams.set("limit", String(limit));
885
+ const res = await this.request(url, { headers: { ...this.authHeaders } });
886
+ if (!res.ok) await throwPlatformApiError("Failed to fetch email logs", res, "read");
887
+ return res.json();
888
+ }
889
+ async getEmailOperation(projectId, operationId) {
890
+ const res = await this.request(`${this.baseUrl}/projects/${encodeURIComponent(projectId)}/email/operations/${encodeURIComponent(operationId)}`, { headers: { ...this.authHeaders } });
891
+ if (!res.ok) await throwPlatformApiError("Failed to fetch email operation", res, "read");
892
+ return res.json();
893
+ }
894
+ async listEmailDestinations(projectId) {
895
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/destinations`, { headers: { ...this.authHeaders } });
896
+ if (!res.ok) await throwPlatformApiError("Failed to list email destinations", res, "read");
897
+ return res.json();
898
+ }
899
+ async addEmailDestination(projectId, address) {
900
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/destinations`, {
901
+ method: "POST",
902
+ headers: {
903
+ "Content-Type": "application/json",
904
+ ...this.authHeaders
905
+ },
906
+ body: JSON.stringify({ address })
907
+ });
908
+ if (!res.ok) await throwPlatformApiError("Failed to add email destination", res, "settings");
909
+ return {
910
+ ...await res.json(),
911
+ created: res.status === 201
912
+ };
913
+ }
914
+ async removeEmailDestination(projectId, destinationId) {
915
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/destinations/${destinationId}`, {
916
+ method: "DELETE",
917
+ headers: { ...this.authHeaders }
918
+ });
919
+ if (!res.ok) await throwPlatformApiError("Failed to remove email destination", res, "settings");
920
+ return res.json();
921
+ }
922
+ /** Installation email settings the CLI reads before starting a credential flow. */
923
+ async getEmailSettings(projectId) {
924
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/settings`, { headers: { ...this.authHeaders } });
925
+ if (!res.ok) await throwPlatformApiError("Failed to read email settings", res, "read");
926
+ return res.json();
927
+ }
928
+ async listEmailDomains(projectId) {
929
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/domains`, { headers: { ...this.authHeaders } });
930
+ if (!res.ok) await throwPlatformApiError("Failed to list email domains", res, "read");
931
+ return res.json();
932
+ }
933
+ /** Submit a scoped token once; later operations address the domain without it. */
934
+ async addEmailDomain(projectId, input) {
935
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/domains`, {
936
+ method: "POST",
937
+ headers: {
938
+ "Content-Type": "application/json",
939
+ ...this.authHeaders
940
+ },
941
+ body: JSON.stringify({
942
+ domain: input.domain,
943
+ ...input.apiToken !== void 0 ? { apiToken: input.apiToken } : {},
944
+ ...input.attemptId !== void 0 ? { attemptId: input.attemptId } : {}
945
+ })
946
+ });
947
+ if (!res.ok) await throwPlatformApiError("Failed to add email domain", res, "settings");
948
+ return res.json();
949
+ }
950
+ async getEmailDomain(projectId, domain) {
951
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/domains/${encodeURIComponent(domain)}`, { headers: { ...this.authHeaders } });
952
+ if (!res.ok) await throwPlatformApiError("Failed to fetch email domain status", res, "read");
953
+ return res.json();
954
+ }
955
+ /** Rotates the shared zone connection secret. */
956
+ async rotateEmailDomainSecret(projectId, domain, attemptId) {
957
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/domains/${encodeURIComponent(domain)}/rotate-secret`, {
958
+ method: "POST",
959
+ headers: {
960
+ "Content-Type": "application/json",
961
+ ...this.authHeaders
962
+ },
963
+ body: JSON.stringify({ attemptId })
964
+ });
965
+ if (!res.ok) await throwPlatformApiError("Failed to rotate email connection secret", res, "settings");
966
+ return res.json();
967
+ }
968
+ async syncEmailDomain(projectId, domain) {
969
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/domains/${encodeURIComponent(domain)}/sync`, {
970
+ method: "POST",
971
+ headers: { ...this.authHeaders }
972
+ });
973
+ if (!res.ok) await throwPlatformApiError("Failed to sync email domain", res, "settings");
974
+ return res.json();
975
+ }
976
+ async removeEmailDomain(projectId, domain) {
977
+ const res = await this.request(`${this.baseUrl}/projects/${projectId}/email/domains/${encodeURIComponent(domain)}`, {
978
+ method: "DELETE",
979
+ headers: { ...this.authHeaders }
980
+ });
981
+ if (!res.ok) await throwPlatformApiError("Failed to remove email domain", res, "settings");
982
+ return res.json();
983
+ }
984
+ };
985
+ function sleep(ms) {
986
+ return new Promise((resolve) => setTimeout(resolve, ms));
987
+ }
988
+ //#endregion
989
+ export { isExpiredTokenError as a, isCliOutdatedError as i, PlatformClient as n, parsePlatformErrorBody as o, client_exports as r, platformErrorMessage as s, PlatformApiError as t };