void 0.20.2 → 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 (68) hide show
  1. package/dist/{auth-cmd-gniL2fNt.mjs → auth-cmd-CzwquNiP.mjs} +3 -3
  2. package/dist/{auth-link-NZdjCmSc.mjs → auth-link-ElDTgF7j.mjs} +3 -3
  3. package/dist/{build-cmd-sI18tX_O.mjs → build-cmd-BpJe6boP.mjs} +1 -1
  4. package/dist/{cache-IHn5MwBC.mjs → cache-D98YTqeE.mjs} +1 -1
  5. package/dist/{cancel-deploy-C5qTOdLi.mjs → cancel-deploy-CPbEQLMG.mjs} +1 -1
  6. package/dist/cli/cli.mjs +27 -26
  7. package/dist/{client-dHfSJvAN.mjs → client-BQBrZoCX.mjs} +143 -78
  8. package/dist/{connect-Bfk31O_8.mjs → connect-WCQZ_u3m.mjs} +3 -3
  9. package/dist/{create-project-Bk9Z0-Jg.mjs → create-project-D0oXA090.mjs} +2 -3
  10. package/dist/{db-BkRoptAt.mjs → db-DJ-9qs3S.mjs} +2 -2
  11. package/dist/{delete-DouASY9P.mjs → delete-BZ4-WaGm.mjs} +1 -1
  12. package/dist/{deploy-DTaWUS1S.mjs → deploy-BAhhcg5q.mjs} +17 -10
  13. package/dist/{domain-1RhhOVrC.mjs → domain-BxAyhxXN.mjs} +1 -1
  14. package/dist/{email-C-lGh51B.mjs → email-Bj7Cvdwp.mjs} +2 -2
  15. package/dist/{env-DJHsPE7Z.mjs → env-DP_EErve.mjs} +1 -1
  16. package/dist/{github-cmd-PW7ZnWTp.mjs → github-cmd-C-z_xRrQ.mjs} +13 -19
  17. package/dist/index.mjs +4 -4
  18. package/dist/{init-BWZ7q5Z4.mjs → init-BGktCXgA.mjs} +4 -4
  19. package/dist/{link-Rmvu2Wl_.mjs → link-D2kbqhWb.mjs} +2 -2
  20. package/dist/{list-DEE2S6mY.mjs → list-CvkK_G7k.mjs} +1 -1
  21. package/dist/{login-Uvferzmm.mjs → login-WIjNc77c.mjs} +2 -2
  22. package/dist/{logs-27FenuiC.mjs → logs-dLUFCapG.mjs} +1 -1
  23. package/dist/{operator-cmd-CjOTmAYE.mjs → operator-cmd-CKJ7xIRs.mjs} +2 -2
  24. package/dist/{platform-auth-config-DrbQXXiW.mjs → platform-auth-config-CdVWRRJr.mjs} +2 -2
  25. package/dist/{platform-auth-protection-Bhtvp0B_.mjs → platform-auth-protection-Drl0qhrn.mjs} +2 -2
  26. package/dist/{platform-auth-recovery-CeOKGVeJ.mjs → platform-auth-recovery-CmKEWpDo.mjs} +2 -2
  27. package/dist/{platform-cmd-BFhieCdV.mjs → platform-cmd-5q_k56xS.mjs} +3 -3
  28. package/dist/{platform-domain-C74PULqV.mjs → platform-domain-4GiDlcqx.mjs} +2 -2
  29. package/dist/{platform-lifecycle-BwAIgz-t.mjs → platform-lifecycle-R9xAxvtG.mjs} +61 -25
  30. package/dist/{platform-management-COogu_Se.mjs → platform-management-BfWsXHEW.mjs} +5 -3
  31. package/dist/{platform-recovery-ewqLefp1.mjs → platform-recovery-CbK-I1FB.mjs} +2 -2
  32. package/dist/{project-cmd-DmZK9Hxf.mjs → project-cmd-CTdmnzvc.mjs} +12 -12
  33. package/dist/{project-team-D8jOJMUJ.mjs → project-team-CGxsQe3_.mjs} +4 -2
  34. package/dist/{project-token-DA34bf-C.mjs → project-token-Cirx7uwZ.mjs} +1 -1
  35. package/dist/{requests-CUExwGQQ.mjs → requests-4Nq59hOr.mjs} +1 -1
  36. package/dist/{rollback-CDNGU1gr.mjs → rollback-Dr7u0Ljx.mjs} +1 -1
  37. package/dist/runtime/remote/index.mjs +49 -8
  38. package/dist/runtime/sandbox.d.mts +4 -56
  39. package/dist/runtime/sandbox.mjs +81 -220
  40. package/dist/{secret-Bzzi2e9E.mjs → secret-AcPi-FoA.mjs} +1 -1
  41. package/package.json +7 -7
  42. package/skills/void/SKILL.md +2 -2
  43. package/skills/void/docs/guide/deployment.md +14 -14
  44. package/skills/void/docs/guide/email.md +12 -10
  45. package/skills/void/docs/guide/platform/administration/access.md +163 -0
  46. package/skills/void/docs/guide/platform/administration/email.md +121 -0
  47. package/skills/void/docs/guide/platform/administration/operations.md +97 -0
  48. package/skills/void/docs/guide/platform/administration/projects.md +54 -0
  49. package/skills/void/docs/guide/platform/development/local.md +119 -0
  50. package/skills/void/docs/guide/platform/development/runtime.md +124 -0
  51. package/skills/void/docs/guide/platform/development/schema-ci.md +95 -0
  52. package/skills/void/docs/guide/platform/installation/ci.md +55 -0
  53. package/skills/void/docs/guide/platform/installation/credentials.md +80 -0
  54. package/skills/void/docs/guide/platform/installation/domains.md +68 -0
  55. package/skills/void/docs/guide/platform/installation/first-deployment.md +82 -0
  56. package/skills/void/docs/guide/platform/installation/maintenance.md +137 -0
  57. package/skills/void/docs/guide/platform/installation/prerequisites.md +86 -0
  58. package/skills/void/docs/guide/platform/installation/setup.md +169 -0
  59. package/skills/void/docs/guide/platform/installation/uninstall.md +54 -0
  60. package/skills/void/docs/guide/platform-administration.md +6 -414
  61. package/skills/void/docs/guide/platform-development.md +5 -316
  62. package/skills/void/docs/guide/project-collaboration.md +1 -1
  63. package/skills/void/docs/guide/sandboxes.md +9 -24
  64. package/skills/void/docs/guide/self-hosted-platform.md +11 -694
  65. package/skills/void/docs/reference/api.md +34 -34
  66. package/skills/void/docs/reference/cli.md +28 -11
  67. package/skills/void/docs/reference/config.md +1 -1
  68. package/skills/void/docs/reference/resource-inference.md +10 -10
@@ -4,14 +4,7 @@ import { Sandbox, getSandbox as getSandbox$1 } from "@cloudflare/sandbox";
4
4
  //#region src/runtime/sandbox.ts
5
5
  const DEFAULT_SANDBOX_ID = "default";
6
6
  const DEFAULT_SANDBOX_BINDING = "SANDBOX";
7
- const SANDBOX_USAGE_QUEUE_BINDING = "USAGE_EVENTS_QUEUE";
8
- const SANDBOX_GATE_BINDING = "__VOID_PROXY";
9
- const HEARTBEAT_MS = 3e5;
10
- /**
11
- * Thrown by `getSandbox()` when the platform gate denies a new acquire because
12
- * the user has hit a concurrency or runtime budget cap. Surface this to the
13
- * caller so they can render a friendly error; do not retry transparently.
14
- */
7
+ const SANDBOX_SERVICE_BINDING = "__VOID_SANDBOX";
15
8
  var SandboxLimitError = class extends Error {
16
9
  name = "SandboxLimitError";
17
10
  reason;
@@ -20,7 +13,6 @@ var SandboxLimitError = class extends Error {
20
13
  this.reason = reason;
21
14
  }
22
15
  };
23
- /** Managed platform Sandbox is unavailable or its activation gate cannot be verified. */
24
16
  var SandboxUnavailableError = class extends Error {
25
17
  name = "SandboxUnavailableError";
26
18
  constructor(message = "Managed Sandbox is unavailable on this Void platform.") {
@@ -31,233 +23,102 @@ let sandboxBindingName = DEFAULT_SANDBOX_BINDING;
31
23
  function __setSandboxBindingName(bindingName) {
32
24
  sandboxBindingName = bindingName;
33
25
  }
34
- const STORAGE_KEY_START_TS = "__voidStartTs";
35
- const STORAGE_KEY_LAST_FLUSH_TS = "__voidLastFlushTs";
36
- function getUsageContext() {
37
- const runtimeEnv = getRawRuntimeEnv();
38
- return {
39
- userId: runtimeEnv.__VOID_USER_ID,
40
- projectId: runtimeEnv.__PROJECT_ID,
41
- slug: runtimeEnv.__VOID_PROJECT_SLUG
42
- };
43
- }
44
- function getSandboxInstanceId(ctx) {
45
- return ctx.id.toString();
46
- }
47
- /**
48
- * Flush the accumulated runtime delta to the usage queue. No-op when the
49
- * usage-queue binding is not bound (e.g. local dev without the platform
50
- * proxy). Errors are swallowed and logged — usage emission must never crash
51
- * the sandbox lifecycle.
52
- */
53
- async function emitRuntimeSeconds(ctx, sandboxRuntimeSeconds) {
54
- if (sandboxRuntimeSeconds <= 0) return;
55
- const queue = getRawRuntimeEnv()[SANDBOX_USAGE_QUEUE_BINDING];
56
- if (!queue) return;
57
- const event = {
58
- requests: 0,
59
- responseBytes: 0,
60
- sandboxRuntimeSeconds,
61
- sandboxInstanceId: getSandboxInstanceId(ctx),
62
- ...getUsageContext()
63
- };
64
- try {
65
- await queue.send(event);
66
- } catch (error) {
67
- console.error("[void/sandbox] failed to emit usage event:", error);
68
- }
69
- }
70
- function isManagedPlatformSandbox(runtimeEnv) {
71
- return [
72
- SANDBOX_GATE_BINDING,
73
- "__PROJECT_ID",
74
- "__VOID_PROXY_TOKEN",
75
- "__VOID_USER_ID",
76
- "__VOID_PROJECT_SLUG"
77
- ].some((binding) => runtimeEnv[binding] !== void 0);
26
+ function rethrowSandboxError(error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ for (const reason of ["concurrency", "runtime_budget"]) if (message.includes(`VOID_SANDBOX_${reason}`)) throw new SandboxLimitError(reason);
29
+ if (message.includes("VOID_SANDBOX_unavailable")) throw new SandboxUnavailableError();
30
+ throw error;
78
31
  }
79
- function resolveGateIdentity(runtimeEnv = getRawRuntimeEnv()) {
80
- const gate = runtimeEnv[SANDBOX_GATE_BINDING];
81
- if (!gate) return null;
82
- const projectId = runtimeEnv.__PROJECT_ID;
83
- const aiToken = runtimeEnv.__VOID_PROXY_TOKEN;
84
- if (!projectId || !aiToken) return null;
85
- return {
86
- gate,
87
- projectId,
88
- aiToken
32
+ function managedClient(capability) {
33
+ const call = async (target, operation, args) => {
34
+ try {
35
+ return decodeManagedResult(await target.invoke(operation, prepareManagedArgs(operation, args)));
36
+ } catch (error) {
37
+ rethrowSandboxError(error);
38
+ }
89
39
  };
90
- }
91
- async function acquireManagedSandbox(sandboxId, runtimeEnv = getRawRuntimeEnv()) {
92
- if (!isManagedPlatformSandbox(runtimeEnv)) return;
93
- const identity = resolveGateIdentity(runtimeEnv);
94
- if (!identity) throw new SandboxUnavailableError("Managed Sandbox activation could not verify the platform identity bindings.");
95
- let response;
96
- try {
97
- response = await identity.gate.fetch("https://gate/sandbox/acquire", {
98
- method: "POST",
99
- headers: { "content-type": "application/json" },
100
- body: JSON.stringify({
101
- projectId: identity.projectId,
102
- aiToken: identity.aiToken,
103
- sandboxId
104
- })
105
- });
106
- } catch {
107
- throw new SandboxUnavailableError("Managed Sandbox activation could not reach the platform gate.");
40
+ return new Proxy(Object.create(null), { get(_target, property) {
41
+ if (typeof property !== "string" || property === "then") return;
42
+ if (property === "tunnels") return new Proxy(Object.create(null), { get(_tunnels, method) {
43
+ if (typeof method !== "string" || method === "then") return;
44
+ return (...args) => call(capability, "tunnels", [method, ...args]);
45
+ } });
46
+ return (...args) => call(capability, property, args);
47
+ } });
48
+ function decodeManagedResult(value) {
49
+ if (Array.isArray(value)) return value.map(decodeManagedResult);
50
+ if (!value || typeof value !== "object") return value;
51
+ const record = value;
52
+ if (record.__voidSandboxType === "process") {
53
+ const { capability: processCapability, __voidSandboxType: _type, ...snapshot } = value;
54
+ return Object.assign(snapshot, {
55
+ kill: (...args) => call(processCapability, "kill", args),
56
+ getStatus: (...args) => call(processCapability, "getStatus", args),
57
+ getLogs: (...args) => call(processCapability, "getLogs", args),
58
+ waitForLog: (...args) => call(processCapability, "waitForLog", args),
59
+ waitForPort: (...args) => call(processCapability, "waitForPort", args),
60
+ waitForExit: (...args) => call(processCapability, "waitForExit", args)
61
+ });
62
+ }
63
+ if (record.__voidSandboxType === "session") {
64
+ const session = value;
65
+ return new Proxy({ id: session.id }, { get(target, property) {
66
+ if (property === "id") return target.id;
67
+ if (typeof property !== "string" || property === "then") return;
68
+ return (...args) => call(session.capability, property, args);
69
+ } });
70
+ }
71
+ return value;
108
72
  }
109
- if (response.status === 409) {
110
- const body = await response.json().catch(() => ({}));
111
- throw new SandboxLimitError(body.reason === "concurrency" || body.reason === "runtime_budget" ? body.reason : "concurrency");
73
+ function prepareManagedArgs(operation, args) {
74
+ if (operation !== "startProcess") return args;
75
+ const [command, options, ...rest] = args;
76
+ if (!options || typeof options !== "object" || Array.isArray(options)) return args;
77
+ const onStart = options.onStart;
78
+ if (typeof onStart !== "function") return args;
79
+ return [
80
+ command,
81
+ {
82
+ ...options,
83
+ onStart: (process) => onStart(decodeManagedResult(process))
84
+ },
85
+ ...rest
86
+ ];
112
87
  }
113
- if (!response.ok) throw new SandboxUnavailableError(`Managed Sandbox activation was rejected by the platform gate (HTTP ${response.status}).`);
114
- }
115
- /**
116
- * Release notification to the gate so it can drop the active lease counter.
117
- * Returns the in-flight fetch as a Promise (errors swallowed) so callers can
118
- * route it through `ctx.waitUntil()` and keep it alive past lifecycle
119
- * teardown. Failure is silent — the gate maintains its own TTL fallback.
120
- */
121
- function notifyGateRelease(ctx) {
122
- const identity = resolveGateIdentity();
123
- if (!identity) return Promise.resolve();
124
- const sandboxId = getSandboxInstanceId(ctx);
125
- return identity.gate.fetch("https://gate/sandbox/release", {
126
- method: "POST",
127
- headers: { "content-type": "application/json" },
128
- body: JSON.stringify({
129
- projectId: identity.projectId,
130
- aiToken: identity.aiToken,
131
- sandboxId
132
- })
133
- }).then(() => void 0).catch(() => {});
134
88
  }
135
89
  /**
136
- * Heartbeat-driven keepalive ping for the gate's concurrency slot. The DO's
137
- * `STALE_MS` (1h) sweep would otherwise reap slots for long-running sandbox
138
- * sessions whose `acquiredAt` ages past the threshold between acquire and
139
- * release, letting new acquires exceed `sandboxMaxConcurrentInstances`. The
140
- * 5 min heartbeat sits well inside the sweep window, so each successful
141
- * refresh resets the stale clock. Returns the in-flight fetch as a Promise
142
- * (errors swallowed) so callers can route it through `ctx.waitUntil()` and
143
- * keep the alarm path itself off the critical path — a failed refresh just
144
- * means the slot ages out naturally, which matches the pre-existing failure
145
- * mode if the gate goes down entirely.
146
- */
147
- function notifyGateRefresh(ctx) {
148
- const identity = resolveGateIdentity();
149
- if (!identity) return Promise.resolve();
150
- const sandboxId = getSandboxInstanceId(ctx);
151
- return identity.gate.fetch("https://gate/sandbox/refresh", {
152
- method: "POST",
153
- headers: { "content-type": "application/json" },
154
- body: JSON.stringify({
155
- projectId: identity.projectId,
156
- aiToken: identity.aiToken,
157
- sandboxId
158
- })
159
- }).then(() => void 0).catch(() => {});
160
- }
161
- /**
162
- * Sandbox variant that participates in Void platform metering: it records
163
- * container runtime seconds into DO storage, emits periodic heartbeats to
164
- * the usage queue, and notifies the gate when the container stops.
165
- *
166
- * Generated code from `void inference` wires user `Sandbox` subclasses to
167
- * extend this class on platform deploys so per-tenant accounting works
168
- * without user opt-in. On local dev (no usage-queue / gate bindings), all
169
- * overrides degrade to no-ops and the sandbox behaves like the upstream
170
- * `Sandbox` class.
90
+ * Retained for generated native/local Worker entries. Managed deployments host
91
+ * the platform-owned Sandbox class in a separate Worker and never use this DO.
171
92
  */
172
93
  var VoidPlatformSandbox = class extends Sandbox {
173
94
  async onStart() {
174
- await acquireManagedSandbox(getSandboxInstanceId(this.ctx), this.env);
175
- const now = Date.now();
176
- const stored = await this.ctx.storage.get([STORAGE_KEY_START_TS, STORAGE_KEY_LAST_FLUSH_TS]);
177
- const orphanedAnchor = stored.get(STORAGE_KEY_LAST_FLUSH_TS) ?? stored.get(STORAGE_KEY_START_TS);
178
- if (orphanedAnchor !== void 0 && orphanedAnchor < now) {
179
- const sandboxRuntimeSeconds = (now - orphanedAnchor) / 1e3;
180
- await emitRuntimeSeconds(this.ctx, sandboxRuntimeSeconds);
181
- await this.ctx.storage.delete([STORAGE_KEY_START_TS, STORAGE_KEY_LAST_FLUSH_TS]);
182
- }
183
- try {
184
- await super.onStart();
185
- } catch (err) {
186
- this.ctx.waitUntil(notifyGateRelease(this.ctx));
187
- throw err;
188
- }
189
- const startedAt = Date.now();
190
- await this.ctx.storage.put({
191
- [STORAGE_KEY_START_TS]: startedAt,
192
- [STORAGE_KEY_LAST_FLUSH_TS]: startedAt
193
- });
194
- await this.ctx.storage.setAlarm(startedAt + HEARTBEAT_MS);
195
- }
196
- async alarm(alarmProps) {
197
- await super.alarm(alarmProps);
198
- const lastFlush = await this.ctx.storage.get(STORAGE_KEY_LAST_FLUSH_TS);
199
- if (typeof lastFlush !== "number") return;
200
- const now = Date.now();
201
- const sandboxRuntimeSeconds = Math.max(0, (now - lastFlush) / 1e3);
202
- await emitRuntimeSeconds(this.ctx, sandboxRuntimeSeconds);
203
- this.ctx.waitUntil(notifyGateRefresh(this.ctx));
204
- await Promise.all([this.ctx.storage.put(STORAGE_KEY_LAST_FLUSH_TS, now), this.ctx.storage.setAlarm(now + HEARTBEAT_MS)]);
205
- }
206
- async onStop() {
207
- await this.flushFinal();
208
- this.ctx.waitUntil(notifyGateRelease(this.ctx));
209
- await super.onStop();
210
- }
211
- async onActivityExpired() {
212
- await this.flushFinal();
213
- this.ctx.waitUntil(notifyGateRelease(this.ctx));
214
- await super.onActivityExpired();
215
- }
216
- async flushFinal() {
217
- const lastFlush = await this.ctx.storage.get(STORAGE_KEY_LAST_FLUSH_TS);
218
- if (typeof lastFlush === "number") {
219
- const sandboxRuntimeSeconds = Math.max(0, (Date.now() - lastFlush) / 1e3);
220
- await emitRuntimeSeconds(this.ctx, sandboxRuntimeSeconds);
221
- }
222
- await this.ctx.storage.delete(STORAGE_KEY_START_TS);
223
- await this.ctx.storage.delete(STORAGE_KEY_LAST_FLUSH_TS);
95
+ const env = this.env;
96
+ if (env.__PROJECT_ID !== void 0 || env.__VOID_PROXY !== void 0) throw new SandboxUnavailableError("Managed Sandboxes require the platform Sandbox controller. Redeploy this application.");
97
+ await super.onStart();
224
98
  }
225
99
  };
226
- /**
227
- * Resolve the platform-provided sandbox stub for the given id, optionally
228
- * coordinating with the platform gate to enforce per-user concurrency and
229
- * runtime budgets.
230
- *
231
- * When the gate binding (`__VOID_PROXY` by default) is present AND the
232
- * platform's `__PROJECT_ID` + `__VOID_PROXY_TOKEN` identity vars are
233
- * available, we POST to `https://gate/sandbox/acquire` with the HMAC-signed
234
- * body the shared proxy auth middleware expects. A `409` response means the
235
- * user has exceeded a budget — we throw `SandboxLimitError` so the caller
236
- * can render a clear message. Any other non-OK status (or a thrown transport
237
- * error from the service binding) fails closed before returning a usable
238
- * managed stub.
239
- *
240
- * When all reserved platform identity bindings are absent, this is a native
241
- * Cloudflare or local deployment and returns the upstream stub directly.
242
- *
243
- * The stub is constructed BEFORE calling the gate so we can pass its opaque,
244
- * namespace-specific Durable Object id (`stub.id.toString()`) as `sandboxId`.
245
- * The same id is available as `ctx.id.toString()` inside lifecycle hooks, so
246
- * acquire, heartbeat, and release use one globally unique instance identity
247
- * even when separate projects or deployments both request the name `default`.
248
- * Constructing the stub does NOT start a container — that only happens on
249
- * the first method call against the stub.
250
- */
100
+ /** Resolve an isolated Sandbox on the current deployment. Starts on first use. */
251
101
  async function getSandbox(id = DEFAULT_SANDBOX_ID, options) {
252
102
  const { binding = sandboxBindingName, ...sandboxOptions } = options ?? {};
253
- const namespace = requireRuntimeBinding(binding);
254
- const stub = getSandbox$1(namespace, id, {
103
+ const runtimeEnv = getRawRuntimeEnv();
104
+ const service = runtimeEnv[SANDBOX_SERVICE_BINDING];
105
+ if (service) {
106
+ if (binding !== sandboxBindingName) throw new SandboxUnavailableError(`Managed Sandbox binding ${binding} is unavailable.`);
107
+ try {
108
+ return managedClient(await service.getSandbox(id, {
109
+ normalizeId: true,
110
+ ...sandboxOptions
111
+ }));
112
+ } catch (error) {
113
+ rethrowSandboxError(error);
114
+ }
115
+ }
116
+ if (runtimeEnv.__PROJECT_ID !== void 0 || runtimeEnv.__VOID_PROXY !== void 0) throw new SandboxUnavailableError("This deployment has no managed Sandbox controller. Redeploy this application.");
117
+ return getSandbox$1(requireRuntimeBinding(binding), id, {
255
118
  normalizeId: true,
256
119
  ...sandboxOptions
257
120
  });
258
- await acquireManagedSandbox(stub.id.toString());
259
- return stub;
260
121
  }
261
122
  const sandbox = { get: getSandbox };
262
123
  //#endregion
263
- export { DEFAULT_SANDBOX_BINDING, DEFAULT_SANDBOX_ID, HEARTBEAT_MS, SANDBOX_GATE_BINDING, SANDBOX_USAGE_QUEUE_BINDING, Sandbox, SandboxLimitError, SandboxUnavailableError, VoidPlatformSandbox, __setSandboxBindingName, getSandbox, sandbox };
124
+ export { DEFAULT_SANDBOX_BINDING, DEFAULT_SANDBOX_ID, SANDBOX_SERVICE_BINDING, Sandbox, SandboxLimitError, SandboxUnavailableError, VoidPlatformSandbox, __setSandboxBindingName, getSandbox, sandbox };
@@ -1,7 +1,7 @@
1
1
  import { A as outro, C as cancel, D as log, E as intro, I as isCancel, j as password, k as note, r as cliTitle, w as confirm } from "./output-B0cfNSx5.mjs";
2
2
  import { S as readProjectConfig, l as getToken, x as readDeploymentPlatform } from "./auth-DPl6kck4.mjs";
3
3
  import { m as validateSecretValues } from "./env-validation-CF6KvTRf.mjs";
4
- import { n as PlatformClient } from "./client-dHfSJvAN.mjs";
4
+ import { n as PlatformClient } from "./client-BQBrZoCX.mjs";
5
5
  import { r as resolveProjectBySlug, t as getRequestedProjectSlug } from "./resolve-project--Vxawf7z.mjs";
6
6
  import { readFileSync } from "node:fs";
7
7
  //#region src/cli/secret.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "void",
3
- "version": "0.20.2",
3
+ "version": "0.20.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -318,11 +318,11 @@
318
318
  "@cloudflare/vite-plugin": "1.54.7",
319
319
  "@cloudflare/workers-types": "^4.20260702.1",
320
320
  "@napi-rs/keyring": "2.0.0",
321
- "@void/deploy-cloudflare": "0.20.2",
322
- "@void/deploy-core": "0.20.2",
323
- "@void/edge": "0.20.2",
324
- "@void/isr": "0.20.2",
325
- "@void/platform": "0.20.2",
321
+ "@void/deploy-cloudflare": "0.20.3",
322
+ "@void/deploy-core": "0.20.3",
323
+ "@void/edge": "0.20.3",
324
+ "@void/isr": "0.20.3",
325
+ "@void/platform": "0.20.3",
326
326
  "better-auth": "^1.7.3",
327
327
  "better-sqlite3": "^13.0.3",
328
328
  "blake3-jit": "^1.1.0",
@@ -368,7 +368,7 @@
368
368
  "zod": "^4.6.1"
369
369
  },
370
370
  "peerDependencies": {
371
- "@void/md": "0.20.2",
371
+ "@void/md": "0.20.3",
372
372
  "arktype": ">=2.0.0",
373
373
  "valibot": ">=1.0.0-beta.7",
374
374
  "vite": "^8.0.0",
@@ -23,7 +23,7 @@ read permission; `void platform install --plan` verifies coverage. Use the
23
23
  self-hosted platform guide for certificate setup rather than enabling a paid
24
24
  product without an explicit request.
25
25
 
26
- For first-time platform setup, follow `docs/guide/self-hosted-platform.md` in order and introduce credentials when the user reaches that step. Recommend a domain; offer explicit `void platform install --workers-dev` for testing before one is ready. PostgreSQL, credentials for the selected login methods, runtime/R2 credentials, and saved signing/encryption keys are required. GitHub is the default and is optional. `--auth-config` accepts nonsecret provider configuration with environment secret references; `--plan` does not read those secrets. Configurable installations finish first-administrator setup using a one-time code and browser identity confirmation. Domain-free installation skips zone/DNS/certificate operations and can use browser login. `void platform domain set <domain>` later performs resumable DNS/HTTPS setup while preserving existing test URLs and the API origin. The command checks the deployed runtime token's cache-purge permission for the new zone; it does not ask users to retrieve that token. Prefer interactive prompts for secrets and keep CI variables in the CI workflow. Void creates the platform infrastructure and database tables.
26
+ For first-time platform setup, follow the installation pages linked from `docs/guide/self-hosted-platform.md` in order and introduce credentials when the user reaches that step. Recommend a domain; offer explicit `void platform install --workers-dev` for testing before one is ready. PostgreSQL, credentials for the selected login methods, runtime/R2 credentials, and saved signing/encryption keys are required. GitHub is the default and is optional. `--auth-config` accepts nonsecret provider configuration with environment secret references; `--plan` does not read those secrets. Configurable installations finish first-administrator setup using a one-time code and browser identity confirmation. Domain-free installation skips zone/DNS/certificate operations and can use browser login. `void platform domain set <domain>` later performs resumable DNS/HTTPS setup while preserving existing test URLs and the API origin. The command checks the deployed runtime token's cache-purge permission for the new zone; it does not ask users to retrieve that token. Prefer interactive prompts for secrets and keep CI variables in the CI workflow. Void creates the platform infrastructure and database tables.
27
27
 
28
28
  For an existing Cloudflare site, run `void deploy` with its root `wrangler.jsonc` or `wrangler.json`. An unlinked project gets a prompt to link and deploy using the existing Worker and resources. Accepting verifies and saves the destination, then continues deployment. Keep new application resources, migrations, auth setup, and secret changes separate from this first handoff; ISR has its own explicit cache choice. The active Worker Version must be the latest upload so inherited secrets have an unambiguous source. Failed builds retain the link for retry. Read `docs/integrations/cloudflare.md` for the supported configuration and rollback behavior.
29
29
 
@@ -91,7 +91,7 @@ Cloudflare browser login does not grant AI Gateway access. A platform installati
91
91
 
92
92
  Use `--runtime <directory>` on platform install, upgrade, repair, enable, or rollback when deploying a locally built `@void/platform` runtime. The directory must contain the generated integrity manifest, Worker artifacts, and migration tree. Build it with `vp run --filter @void/platform build`; Git source revisions are detected automatically, with `-dirty` for uncommitted changes. `VOID_PLATFORM_SOURCE_REVISION` is an optional override. Do not treat this as a safety bypass: custom and packaged runtimes follow the same verification and rollback path.
93
93
 
94
- For self-hosted platform installation, require a dedicated empty PostgreSQL database. Interactive lifecycle commands use Cloudflare browser OAuth with keyring storage. Wrangler OAuth has Zone Read, so a plan that creates a zone or DNS record requires a scoped `CLOUDFLARE_API_TOKEN` with Account → Zone → Edit and/or Zone → DNS → Edit; finish Workers onboarding and choose a workers.dev subdomain for a fresh Cloudflare account. The installed platform uses a separate `VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN`. Its core permissions include Hyperdrive: Write at account scope and Cache Purge: Purge on the application zone; SSL and Certificates: Edit is needed only when a custom runtime enables custom project domains. The installer preflights account and Hyperdrive access and safely verifies Cache Purge for an existing zone with a unique nonexistent URL. Fresh installs require unique per-installation values from the operator's secret manager: `VOID_PLATFORM_JWT_SECRET` with at least 32 random bytes and `VOID_PLATFORM_PROJECT_SECRET_KEY` as canonical base64 for 32 random bytes. Never let a local checkpoint or ephemeral CI runner be their only custodian. Enabling email generates a separate signing key in encrypted recovery state; supply `VOID_PLATFORM_EMAIL_SIGNING_SECRET` from the secret manager for headless installs without persistent recovery files. The installer transactionally claims the database for one installation, pins that URL after a successful claim, never removes the claim during uninstall, and uses it for a cross-host lifecycle lock plus a secret-free authoritative lifecycle manifest. Local recovery checkpoints are AES-256-GCM encrypted with an OS-keychain key; never place platform secrets in plaintext files. After discovery on a new machine, provide `VOID_PLATFORM_DATABASE_URL`. For email-enabled installations without encrypted recovery state, also restore the original `VOID_PLATFORM_EMAIL_SIGNING_SECRET`; recreating only the email gateway needs this key, not JWT or runtime credentials. Normal upgrades preserve other deployed Worker secrets; restore the original runtime, GitHub, R2, JWT, and project-encryption values when recreating the API or proxy as documented. Managed platform Sandbox is paused for removal: preview `void platform system sandbox-drain --plan`, follow each returned `nextCursor` with `--cursor '<value>'` to inspect bounded pages, then apply with `--yes` and rerun until the DB-backed response reports `complete: true`; never delete an unverified app by name. The installer trusts only DB-backed sandbox-drain probe protocol v1. Before it accepts an empty inventory, the database admission barrier makes older deployment inserts finish and become visible or rejects them after the protocol floor is armed. Lifecycle redeploys preserve unmanaged Worker routes and custom domains in both the new trigger state and rollback snapshot. They stop before migrations or uploads when live Hyperdrive origin metadata differs from the pinned database. Platform upgrade SQL is forward-only: packaged hashes and the live Drizzle prefix must match, pending migrations require an exact source-version/schema rollback edge, and the old version remains authoritative until target health succeeds. Use `void platform rollback --runtime <earlier>` only when the installed runtime declares the exact earlier version/schema compatible; pass `--from-runtime` for the exact current custom artifact. Rollback preserves the forward database schema and cannot lower a database-required safety protocol. Safe uninstall retains Worker routes, custom domains, and R2 for explicit manual cleanup because Cloudflare cannot condition their deletion on an immutable generation.
94
+ For self-hosted platform installation, require a dedicated empty PostgreSQL database. Interactive lifecycle commands use Cloudflare browser OAuth with keyring storage. Wrangler OAuth has Zone Read, so a plan that creates a zone or DNS record requires a scoped `CLOUDFLARE_API_TOKEN` with Account → Zone → Edit and/or Zone → DNS → Edit; finish Workers onboarding and choose a workers.dev subdomain for a fresh Cloudflare account. The installed platform uses a separate `VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN`. Its core permissions include Hyperdrive: Write at account scope and Cache Purge: Purge on the application zone; SSL and Certificates: Edit is needed only when a custom runtime enables custom project domains. Managed Sandbox apps additionally require Workers Paid plus Account → Containers → Edit and Account → Cloudchamber → Edit on that runtime token; these are checked on the first Sandbox application deploy, not during platform install or upgrade. The installer preflights account and Hyperdrive access and safely verifies Cache Purge for an existing zone with a unique nonexistent URL. Fresh installs require unique per-installation values from the operator's secret manager: `VOID_PLATFORM_JWT_SECRET` with at least 32 random bytes and `VOID_PLATFORM_PROJECT_SECRET_KEY` as canonical base64 for 32 random bytes. Never let a local checkpoint or ephemeral CI runner be their only custodian. Enabling email generates a separate signing key in encrypted recovery state; supply `VOID_PLATFORM_EMAIL_SIGNING_SECRET` from the secret manager for headless installs without persistent recovery files. The installer transactionally claims the database for one installation, pins that URL after a successful claim, never removes the claim during uninstall, and uses it for a cross-host lifecycle lock plus a secret-free authoritative lifecycle manifest. Local recovery checkpoints are AES-256-GCM encrypted with an OS-keychain key; never place platform secrets in plaintext files. After discovery on a new machine, provide `VOID_PLATFORM_DATABASE_URL`. For email-enabled installations without encrypted recovery state, also restore the original `VOID_PLATFORM_EMAIL_SIGNING_SECRET`; recreating only the email gateway needs this key, not JWT or runtime credentials. Normal upgrades preserve other deployed Worker secrets; restore the original runtime, GitHub, R2, JWT, and project-encryption values when recreating the API or proxy as documented. Upgrades enable managed Sandboxes automatically. An upgrade from the tenant-owned legacy may still require `void platform system sandbox-drain`: preview with `--plan`, follow each `nextCursor`, then apply with `--yes` until the DB-backed response reports `complete: true`; never delete an unverified app by name. The installer trusts only DB-backed sandbox-drain probe protocol v1. Before it accepts an empty inventory, the database admission barrier makes older deployment inserts finish and become visible or rejects them after the protocol floor is armed. Lifecycle redeploys preserve unmanaged Worker routes and custom domains in both the new trigger state and rollback snapshot. They stop before migrations or uploads when live Hyperdrive origin metadata differs from the pinned database. Platform upgrade SQL is forward-only: packaged hashes and the live Drizzle prefix must match, pending migrations require an exact source-version/schema rollback edge, and the old version remains authoritative until target health succeeds. Use `void platform rollback --runtime <earlier>` only when the installed runtime declares the exact earlier version/schema compatible; pass `--from-runtime` for the exact current custom artifact. Rollback preserves the forward database schema and cannot lower a database-required safety protocol. Safe uninstall retains Worker routes, custom domains, and R2 for explicit manual cleanup because Cloudflare cannot condition their deletion on an immutable generation.
95
95
 
96
96
  For self-hosted recovery, `VOID_PLATFORM_PROJECT_SECRET_KEY` restores an original `v1` project-secret keyring. After rotation, restore every retained version with `VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON` and its active entry with `VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION`. These inputs restore a missing API Worker and never replace the live keyring of an existing Worker. Inject them from a secret manager without logging them or writing plaintext files.
97
97
 
@@ -14,23 +14,23 @@ Direct Cloudflare deployment runs an application in your account. A self-hosted
14
14
  Void platform provides that deployment service to a team using the operator's
15
15
  account. Both use the same application APIs, with the following differences:
16
16
 
17
- | Feature | Direct Cloudflare | Core self-hosted platform |
18
- | ------------------------------------------------ | -------------------------------- | ----------------------------------------- |
19
- | Static sites, SPAs, and native Pages SSR | Supported | Supported |
20
- | Routing rules, WebSockets, queues, cron, and ISR | Supported | Supported |
21
- | D1, KV, R2, and external SQL through Hyperdrive | Supported | Supported |
22
- | Workers AI and provider requests | Your account and gateway | Installation gateway and proxy |
23
- | Runtime logs | Live Cloudflare tail | Retained platform logs |
24
- | Application rollback | Worker versions | Retained platform deployments |
25
- | Typed Durable State | Supported | Unavailable |
26
- | Sandboxes | Requires Workers Paid and Docker | Unavailable in beta |
27
- | Custom application domains | Supported | Unavailable in the core installation |
28
- | Generated GitHub deployment workflow | Supported | Use your CI with a scoped developer token |
29
- | User dashboard and managed GitHub builds | Not required | Not included in a standard installation |
17
+ | Feature | Direct Cloudflare | Core self-hosted platform |
18
+ | ------------------------------------------------ | -------------------------------- | --------------------------------------------- |
19
+ | Static sites, SPAs, and native Pages SSR | Supported | Supported |
20
+ | Routing rules, WebSockets, queues, cron, and ISR | Supported | Supported |
21
+ | D1, KV, R2, and external SQL through Hyperdrive | Supported | Supported |
22
+ | Workers AI and provider requests | Your account and gateway | Installation gateway and proxy |
23
+ | Runtime logs | Live Cloudflare tail | Retained platform logs |
24
+ | Application rollback | Worker versions | Retained platform deployments |
25
+ | Typed Durable State | Supported | Unavailable |
26
+ | Sandboxes | Requires Workers Paid and Docker | Requires Workers Paid on the platform account |
27
+ | Custom application domains | Supported | Unavailable in the core installation |
28
+ | Generated GitHub deployment workflow | Supported | Use your CI with a scoped developer token |
29
+ | User dashboard and managed GitHub builds | Not required | Not included in a standard installation |
30
30
 
31
31
  Ordinary native applications remain compatible with Workers Free within its
32
32
  quotas. Installing a team platform requires Workers for Platforms and the
33
- [documented infrastructure](./self-hosted-platform.md#cloudflare-footprint).
33
+ [documented infrastructure](./platform/installation/domains.md#cloudflare-footprint).
34
34
  Application rollback never reverses database migrations.
35
35
 
36
36
  Routing-rule parity applies to native Void applications and static deployments.
@@ -10,7 +10,7 @@ Send transactional email from your app via [Cloudflare's `send_email` binding](h
10
10
  import { sendEmail } from 'void/email';
11
11
 
12
12
  const result = await sendEmail({
13
- from: 'Acme <acme+noreply@mail.void.cloud>',
13
+ from: 'Acme <acme+noreply@mail.example.com>', // use your project's sender address
14
14
  to: 'user@example.com',
15
15
  subject: 'Welcome',
16
16
  text: 'Thanks for signing up!',
@@ -36,9 +36,13 @@ hits first, because a recipient you have not verified yet fails per-recipient.
36
36
 
37
37
  ## Setup
38
38
 
39
- Zero config on the Void platform (`void deploy`). Every Void project ships with:
39
+ On a platform with email enabled, your app needs no email configuration before
40
+ `void deploy`. Ask your administrator for the platform's shared mail domain. A
41
+ self-hosted administrator [enables email during installation or upgrade](/guide/platform/installation/credentials#runtime-token-permissions); installations without it do not offer platform email. Void Cloud uses `mail.void.cloud`.
40
42
 
41
- - **Sender** — `<your-slug>+noreply@mail.void.cloud`. Used as the default `from` if you omit it. The platform owns the zone with Email Routing + DKIM + SPF + DMARC set up; you do nothing. Project slugs are capped at 56 characters so this local part fits RFC 5321's 64 octets; a project created before the cap with a longer slug must pass `from` explicitly.
43
+ Each project on an email-enabled platform has:
44
+
45
+ - **Sender** — `<your-slug>+noreply@<mail-domain>`. Used as the default `from` if you omit it. The platform administrator configures the mail zone and its Email Routing, DKIM, SPF, and DMARC records. Project slugs are capped at 56 characters so this local part fits RFC 5321's 64 octets; a project created before the cap with a longer slug must pass `from` explicitly.
42
46
  - **No worker binding to add** — outbound mail is sent by the Void proxy, which holds the
43
47
  platform `send_email` binding. Your worker never gets one, so there is nothing to configure.
44
48
  - **Your own address as a recipient** — the email on your Void account is registered as a recipient when the project is created. It is verified at once when Cloudflare already holds it verified for the platform (you clicked its link for an earlier project of yours); otherwise Cloudflare mails it a verification link, and until you click that link and run `void email destinations` — the listing is what records the click — a send to yourself comes back `ok: false` with a per-recipient `UNVERIFIED_DESTINATION` in `result.deliveries`.
@@ -61,7 +65,7 @@ Cloudflare emails the recipient with a verification link. Once they click it and
61
65
  void email destinations
62
66
  ```
63
67
 
64
- The project owner's email (the GitHub address you signed up with) is added automatically when the project is created, so it skips `void email allow` — not the verification. See [Setup](#setup) for when it is verified at once and when there is a link to click.
68
+ The project owner's account email is added automatically when the project is created on an email-enabled platform, so it skips `void email allow` — not the verification. See [Setup](#setup) for when it is verified at once and when there is a link to click.
65
69
 
66
70
  ::: warning When this is the right fit
67
71
  The shared sender is great for: ops alerts to the team, notifications to the project owner, reply-by-email flows on top of inbound, internal/app-internal mail.
@@ -71,7 +75,7 @@ For SaaS sending to arbitrary end-users (every signup gets a welcome email), the
71
75
 
72
76
  ## Your own domain on the platform
73
77
 
74
- The shared sender lives on the platform's `mail.void.cloud` zone. To send — and receive — at a domain you own, register its Cloudflare zone with the project:
78
+ The shared sender uses the platform's configured mail domain. To send — and receive — at a domain you own, register its Cloudflare zone with the project:
75
79
 
76
80
  ```sh
77
81
  void email domain add acme.com
@@ -120,7 +124,7 @@ At most 50 recipients across `to`, `cc` and `bcc` per call. Each address is chec
120
124
 
121
125
  `Address` accepts either a string (`"hello@acme.dev"` or `"Name <hello@acme.dev>"`) or an object (`{ email, name? }`). Display names with non-ASCII characters are RFC 2047 encoded automatically.
122
126
 
123
- On the platform the sender is pinned to your project. `from` must be your project's own platform address — `<project-slug>@mail.void.cloud` or `<project-slug>+<tag>@mail.void.cloud`, optionally with a display name (`Acme <acme+noreply@mail.void.cloud>`) — or any address on a domain registered with `void email domain add` (see [Your own domain on the platform](#your-own-domain-on-the-platform)). Anything else is rejected with `INVALID_FROM`. Omit `from` and Void fills in `<project-slug>+noreply@mail.void.cloud` for you.
127
+ On the platform the sender is pinned to your project. `from` must be your project's own platform address — `<project-slug>@<mail-domain>` or `<project-slug>+<tag>@<mail-domain>`, optionally with a display name — or any address on a domain registered with `void email domain add` (see [Your own domain on the platform](#your-own-domain-on-the-platform)). For example, if your platform's mail domain is `mail.example.com`, you can use `Acme <acme+noreply@mail.example.com>`. Anything else is rejected with `INVALID_FROM`. Omit `from` and Void fills in `<project-slug>+noreply@<mail-domain>` for you.
124
128
 
125
129
  On your own Cloudflare account, `from` defaults to `email.from` from `void.json` and must be on a domain your account can send from; Cloudflare rejects any other sender and `sendEmail` reports it as `INVALID_FROM`.
126
130
 
@@ -128,7 +132,6 @@ On your own Cloudflare account, `from` defaults to `email.from` from `void.json`
128
132
 
129
133
  ```ts
130
134
  await sendEmail({
131
- from: 'Acme <acme+noreply@mail.void.cloud>',
132
135
  to: 'user@example.com',
133
136
  subject: 'Your receipt',
134
137
  text: 'Receipt attached.',
@@ -146,7 +149,6 @@ For inline images (e.g. logos referenced from HTML), set `disposition: 'inline'`
146
149
 
147
150
  ```ts
148
151
  await sendEmail({
149
- from: 'Acme <acme+noreply@mail.void.cloud>',
150
152
  to: 'user@example.com',
151
153
  subject: 'Hello',
152
154
  html: '<img src="cid:logo" alt="Acme">',
@@ -241,7 +243,7 @@ The dev inbox is **not** available under a Class B or C framework — SvelteKit,
241
243
 
242
244
  ```ts
243
245
  const result = await sendEmail({
244
- from: 'acme+noreply@mail.void.cloud',
246
+ from: 'acme+noreply@mail.example.com', // replace with your project sender
245
247
  to: 'verified@acme.dev',
246
248
  subject: 'Skips the dev inbox',
247
249
  text: 'Not captured — and not delivered either.',
@@ -264,7 +266,7 @@ describe('signup flow', () => {
264
266
  const inbox = createEmailTestHarness();
265
267
 
266
268
  await sendEmail({
267
- from: 'acme+noreply@mail.void.cloud',
269
+ from: 'acme+noreply@mail.example.com', // use your project sender
268
270
  to: 'user@example.com',
269
271
  subject: 'Welcome',
270
272
  text: 'Hi!',