void 0.20.0 → 0.20.2

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 (93) 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-CYVhSNFy.mjs → auth-cmd-gniL2fNt.mjs} +5 -4
  4. package/dist/auth-link-NZdjCmSc.mjs +28 -0
  5. package/dist/{build-cmd-CkVD1uOh.mjs → build-cmd-sI18tX_O.mjs} +3 -3
  6. package/dist/{cache-QcUfR-ff.mjs → cache-IHn5MwBC.mjs} +3 -3
  7. package/dist/{cancel-deploy-DsvWKFTe.mjs → cancel-deploy-C5qTOdLi.mjs} +3 -3
  8. package/dist/cf-access-DRsQRe6k.mjs +75 -0
  9. package/dist/cli/cli.mjs +364 -1883
  10. package/dist/cli/env-schema-probe.mjs +11 -2
  11. package/dist/{client-CyCHWSO_.mjs → client-dHfSJvAN.mjs} +336 -58
  12. package/dist/{cloudflare-auth-DRkGe7-s.mjs → cloudflare-auth-6M5llVPC.mjs} +53 -6
  13. package/dist/{cloudflare-cmd-D3ME2GAe.mjs → cloudflare-cmd-4RPGN3KB.mjs} +2 -2
  14. package/dist/{cloudflare-connect-BivMefMA.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-D9Yr-T5f.mjs → connect-Bfk31O_8.mjs} +6 -6
  18. package/dist/{create-project-DMV-csEm.mjs → create-project-Bk9Z0-Jg.mjs} +6 -5
  19. package/dist/{db-BxoUWL0F.mjs → db-BkRoptAt.mjs} +75 -48
  20. package/dist/{delete-iJOqxBz1.mjs → delete-DouASY9P.mjs} +3 -3
  21. package/dist/{deploy-CcDoeBIf.mjs → deploy-DTaWUS1S.mjs} +137 -128
  22. package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
  23. package/dist/{domain-gu_iaHmn.mjs → domain-1RhhOVrC.mjs} +4 -4
  24. package/dist/email-C-lGh51B.mjs +795 -0
  25. package/dist/{env-D4Emu-M_.mjs → env-DBKmK4vc.mjs} +1 -0
  26. package/dist/{env-DmU2To0C.mjs → env-DJHsPE7Z.mjs} +5 -5
  27. package/dist/{env-validation-ENpMy6Ez.mjs → env-validation-CF6KvTRf.mjs} +3 -1
  28. package/dist/{gen-BKw6qHIg.mjs → gen-B_wPnVTK.mjs} +3 -3
  29. package/dist/generate-RTK8_kK1.mjs +47 -0
  30. package/dist/{github-cmd-DJS5Ab-H.mjs → github-cmd-PW7ZnWTp.mjs} +3 -3
  31. package/dist/{headers-D8QfRX9Y.mjs → headers-BAHwgHdW.mjs} +1 -1
  32. package/dist/help-CwOX-zmI.mjs +2216 -0
  33. package/dist/{inbound-BJ70in1n.d.mts → inbound-CH5Mksyy.d.mts} +37 -42
  34. package/dist/{inbound-CNKxb3FY.mjs → inbound-aVHEUhKo.mjs} +143 -101
  35. package/dist/index.mjs +69 -30
  36. package/dist/{init-Dx5cgKqK.mjs → init-BWZ7q5Z4.mjs} +11 -11
  37. package/dist/{link-D_rm5sRH.mjs → link-Rmvu2Wl_.mjs} +4 -4
  38. package/dist/{list-M8XShti7.mjs → list-DEE2S6mY.mjs} +4 -4
  39. package/dist/{local-d1-BE8KBbMy.mjs → local-d1-D2I6Ox5F.mjs} +1 -1
  40. package/dist/{login-C8-UuLnp.mjs → login-Uvferzmm.mjs} +28 -10
  41. package/dist/{logs-DwFU7dMW.mjs → logs-27FenuiC.mjs} +4 -4
  42. package/dist/{mime-BJD7d_qL.mjs → mime-D5Nmdzf7.mjs} +23 -9
  43. package/dist/{node-BkaXcpAc.mjs → node-Ez5KW5rn.mjs} +3 -3
  44. package/dist/operator-auth-B3e08unv.mjs +52 -0
  45. package/dist/operator-client-LUZnlnYk.mjs +82 -0
  46. package/dist/{operator-cmd-DYWRbWUA.mjs → operator-cmd-CjOTmAYE.mjs} +35 -55
  47. package/dist/{output-tFQLLj26.mjs → output-B0cfNSx5.mjs} +316 -2
  48. package/dist/pages/index.mjs +2 -2
  49. package/dist/platform-auth-config-DrbQXXiW.mjs +368 -0
  50. package/dist/platform-auth-protection-Bhtvp0B_.mjs +219 -0
  51. package/dist/platform-auth-recovery-CeOKGVeJ.mjs +310 -0
  52. package/dist/{platform-cmd-BEOVv1PN.mjs → platform-cmd-BFhieCdV.mjs} +16 -6
  53. package/dist/{platform-domain-BszUfiS8.mjs → platform-domain-C74PULqV.mjs} +4 -4
  54. package/dist/{platform-lifecycle-CjSr6xf0.mjs → platform-lifecycle-BwAIgz-t.mjs} +1667 -191
  55. package/dist/{platform-management-W30iCaTL.mjs → platform-management-COogu_Se.mjs} +33 -7
  56. package/dist/{platform-recovery-pHHv4ZSg.mjs → platform-recovery-ewqLefp1.mjs} +6 -5
  57. package/dist/{prepare-CBetXvsN.mjs → prepare-CtDJjoOj.mjs} +2 -2
  58. package/dist/{prepare-BfJvFUtJ.mjs → prepare-blNRQvQl.mjs} +2 -2
  59. package/dist/prerender-render.d.mts +11 -0
  60. package/dist/prerender-render.mjs +111 -0
  61. package/dist/{project-cmd-DCpk1cDt.mjs → project-cmd-DmZK9Hxf.mjs} +32 -14
  62. package/dist/project-team-D8jOJMUJ.mjs +130 -0
  63. package/dist/project-token-DA34bf-C.mjs +75 -0
  64. package/dist/{provision-Blnstcm2.mjs → provision-CSJOjjQk.mjs} +2 -0
  65. package/dist/{requests-Dn8Vheh1.mjs → requests-CUExwGQQ.mjs} +3 -3
  66. package/dist/{rollback-CtlPXEBi.mjs → rollback-CDNGU1gr.mjs} +4 -4
  67. package/dist/{runner-mysql-CgRFl3s6.mjs → runner-mysql-7BPUNGmL.mjs} +1 -1
  68. package/dist/{runner-pg-DCkWPsWS.mjs → runner-pg-BkEza-dX.mjs} +1 -1
  69. package/dist/runtime/ai.mjs +3 -2
  70. package/dist/runtime/email/testing.d.mts +1 -1
  71. package/dist/runtime/email/testing.mjs +3 -3
  72. package/dist/runtime/email-protocol.d.mts +15 -0
  73. package/dist/runtime/email-protocol.mjs +70 -0
  74. package/dist/runtime/email.d.mts +2 -2
  75. package/dist/runtime/email.mjs +189 -96
  76. package/dist/runtime/remote/index.mjs +5 -3
  77. package/dist/{secret-BqTxGqki.mjs → secret-Bzzi2e9E.mjs} +5 -5
  78. package/dist/{skills-Q46GZMO-.mjs → skills-C0RvGjeE.mjs} +1 -1
  79. package/dist/sqlite-validation-BzKMWnO4.mjs +25 -0
  80. package/dist/{subcommand-prompt-WfySCQ7S.mjs → subcommand-prompt-Bmyn5Rlc.mjs} +1 -1
  81. package/dist/{validate-Bihr8WBi.mjs → validate-tBBN_dXH.mjs} +1 -0
  82. package/package.json +12 -7
  83. package/skills/void/SKILL.md +35 -4
  84. package/skills/void/docs/guide/deployment.md +2 -0
  85. package/skills/void/docs/guide/email.md +121 -98
  86. package/skills/void/docs/guide/platform-administration.md +214 -2
  87. package/skills/void/docs/guide/platform-development.md +93 -2
  88. package/skills/void/docs/guide/project-collaboration.md +94 -0
  89. package/skills/void/docs/guide/self-hosted-platform.md +184 -30
  90. package/skills/void/docs/reference/cli.md +294 -15
  91. package/dist/cf-access-AJ1ehiFR.mjs +0 -42
  92. package/dist/cf-access-DsSsZUPr.mjs +0 -67
  93. package/dist/email-r6DHJyAB.mjs +0 -262
@@ -41,11 +41,12 @@ Use this page as a command reference. If you are setting up a project for the fi
41
41
  | `void build logs` | Stream, tail, or download build logs |
42
42
  | `void email status` | Show email readiness on your own Cloudflare account (`--platform cloudflare`) |
43
43
  | `void email setup` | Set email up on your own Cloudflare account, without deploying |
44
- | `void email usage` | Show monthly email send/receive counts and quota |
44
+ | `void email usage` | Show monthly recipient attempts, inbound receipts, and quota |
45
45
  | `void email logs` | Show recent email delivery activity |
46
46
  | `void email destinations` | List verified recipient addresses |
47
47
  | `void email allow <address>` | Add a recipient and send a verification email |
48
48
  | `void email disallow <address>` | Remove a recipient from the allowlist |
49
+ | `void email domain` | Send and receive at your own domain on a Cloudflare zone |
49
50
  | `void init` | Setup wizard for new or existing projects |
50
51
 
51
52
  ## Binary Invocation
@@ -165,7 +166,7 @@ In a non-interactive shell, supply a URL or explicit target. Cloudflare requires
165
166
 
166
167
  ### `void auth login`
167
168
 
168
- OAuth login. You choose GitHub or Google at the prompt, and the token is saved in the operating-system keychain, scoped to the platform origin. Login fails closed when no keychain is available instead of writing the token to a plaintext file; headless environments use `VOID_TOKEN` from their secret manager.
169
+ Browser login through one of the platform's currently enabled methods. The token is saved in the operating-system keychain, scoped to the platform origin. Login fails closed when no keychain is available instead of writing the token to a plaintext file; headless environments use `VOID_TOKEN` from their secret manager.
169
170
 
170
171
  Set `VOID_API_URL` alongside `VOID_TOKEN` to identify the platform that issued it.
171
172
  A token without an API URL is only used for Void Cloud's production API; a saved
@@ -174,6 +175,13 @@ saved login instead, unset `VOID_TOKEN`.
174
175
 
175
176
  This is optional if you already completed auth during `void connect` or the interactive `void init` flow.
176
177
 
178
+ ### `void auth link [connection-id]`
179
+
180
+ Link another enabled login method to your current account. Sign in again if your
181
+ session is no longer recent, complete the additional provider's browser login,
182
+ and confirm the displayed identity. With no connection ID, choose an enabled
183
+ method interactively. The optional dashboard exposes the same flow in **Account**.
184
+
177
185
  ### `void auth logout`
178
186
 
179
187
  Removes saved credentials.
@@ -184,7 +192,11 @@ Prints your current login.
184
192
 
185
193
  ### `void auth token`
186
194
 
187
- Copies your auth token to the system clipboard. Useful for setting up CI secrets.
195
+ Copies your human auth token to the system clipboard. It is intended for
196
+ interactive troubleshooting and remains subject to login-method revocation. Do
197
+ not combine it with a Cloudflare Access service token for CI; machine Access
198
+ proof cannot turn a human Void token into an automation identity. Create a
199
+ project-scoped credential with `void project token create` instead.
188
200
 
189
201
  ## Cloudflare authentication
190
202
 
@@ -216,7 +228,80 @@ Link current directory to an existing hosted Void project by slug, or select int
216
228
 
217
229
  ### `void project list`
218
230
 
219
- List all hosted projects (slug, mode, URL). For a saved Cloudflare target, this displays the current Worker's versions instead because there is no Void project registry.
231
+ List all accessible hosted projects (slug, role, type, URL). Shared projects are included and the role column distinguishes them from projects you own. For a saved Cloudflare target, this displays the current Worker's versions instead because there is no Void project registry.
232
+
233
+ ### `void project team`
234
+
235
+ Manage access to a project hosted on a Void platform:
236
+
237
+ ```sh
238
+ void project team list [--project <slug>]
239
+ void project team invite <email> --role <reader|collaborator|admin> [--project <slug>]
240
+ void project team invitations [--project <slug>]
241
+ void project team role <user-id> <reader|collaborator|admin> [--project <slug>]
242
+ void project team remove <user-id> [--project <slug>]
243
+ void project team revoke <invitation-id> [--project <slug>]
244
+ void project team leave [--project <slug>]
245
+ void project team pending
246
+ void project team accept <invitation-id>
247
+ void project team decline <invitation-id>
248
+ ```
249
+
250
+ Project-scoped commands use `--project`, then `VOID_PROJECT`, then the linked project. Invitations can target only an email address already registered to a user on that platform; they do not create accounts or grant signup access. Only the invited account can accept or decline its invitation.
251
+
252
+ Readers can view the project but cannot deploy. Collaborators can deploy and manage deploy prerequisites. Project administrators can additionally manage domains, email destinations, GitHub configuration, and the project team. The owner alone can delete the project. See [Project Collaboration](../guide/project-collaboration.md) for the full role boundaries.
253
+
254
+ Project-scoped team management commands are not available for projects deployed
255
+ directly to Cloudflare. The account-scoped `pending`, `accept`, and `decline`
256
+ commands use the active connection selected by `void connect <url>`, regardless
257
+ of the current project's link or deploy target. `VOID_API_URL` takes precedence;
258
+ an unscoped `VOID_TOKEN` selects Void Cloud. These commands display their
259
+ platform, and accepting an invitation leaves directory links intact. To link
260
+ the invited application, run `void project link` in an unlinked checkout of
261
+ that application.
262
+
263
+ ### `void project token <create|list|renew|revoke>`
264
+
265
+ Manage revocable `aud: deploy` credentials for one Void platform project. The
266
+ credential can only call the endpoints used by `void deploy`; it cannot access
267
+ operator, account, secret-writing, project-deletion, or other projects' routes.
268
+
269
+ ```sh
270
+ void project token create --name github-actions --expires-in 30
271
+ void project token list
272
+ void project token renew <credential-id> --expires-in 30
273
+ void project token revoke <credential-id>
274
+ ```
275
+
276
+ Pass `--project <name>` outside a linked project. Expiry is bounded to 1–90
277
+ days. Create and renew display the bearer value once; replace the stored secret
278
+ immediately after renewal because the previous credential is revoked in the
279
+ same operation. Project deletion and owner suspension also stop its use. Login
280
+ method disablement does not revoke these independent deploy credentials.
281
+
282
+ Store the printed `VOID_TOKEN` and `VOID_API_URL` in the CI secret manager. The
283
+ Access pair passes the perimeter; the scoped Void credential authorizes only
284
+ this project's deploy workflow. When prerendering or remote proxy bindings are
285
+ used on an Access-protected platform, store `VOID_ACCESS_CREDENTIALS` as an
286
+ origin-keyed JSON secret containing both exact HTTPS origins, even if both use
287
+ the same service-token pair:
288
+
289
+ ```json
290
+ {
291
+ "https://void-company-api.example.workers.dev": {
292
+ "CF_ACCESS_CLIENT_ID": "<service-token client ID>",
293
+ "CF_ACCESS_CLIENT_SECRET": "<service-token client secret>"
294
+ },
295
+ "https://void-company-proxy.example.workers.dev": {
296
+ "CF_ACCESS_CLIENT_ID": "<service-token client ID>",
297
+ "CF_ACCESS_CLIENT_SECRET": "<service-token client secret>"
298
+ }
299
+ }
300
+ ```
301
+
302
+ `VOID_ACCESS_ORIGIN` scopes a pair to one origin, so selecting only the API
303
+ origin is insufficient for a workflow that calls the proxy. Use distinct pairs
304
+ in the two entries when the Access policies require them.
220
305
 
221
306
  ### `void project logs`
222
307
 
@@ -361,13 +446,78 @@ Void does not automatically retry changes. If a request loses its connection or
361
446
  Sign in, inspect your session, or sign out:
362
447
 
363
448
  ```sh
364
- void platform auth login [--provider github|google] [--token-stdin]
449
+ void platform auth login [--provider <connection-id>] [--token-stdin]
365
450
  void platform auth status
366
451
  void platform auth logout
367
452
  void platform auth token [--token-stdin]
368
453
  ```
369
454
 
370
- Browser login defaults to GitHub; Google is available when enabled on the platform. Login saves a one-hour administrator session in your system keychain. Logout revokes that session and removes its local credential.
455
+ With no `--provider`, browser login offers the platform's enabled login methods.
456
+
457
+ #### Authentication configuration
458
+
459
+ `void platform config auth` opens interactive configuration. These commands use
460
+ your administrator session from `void platform auth login`:
461
+
462
+ ```sh
463
+ void platform config auth list
464
+ void platform config auth show company
465
+ void platform config auth add google
466
+ void platform config auth add oidc --id company
467
+ void platform config auth add cloudflare-access --id access
468
+ void platform config auth configure company
469
+ void platform config auth test company
470
+ void platform config auth link company
471
+ void platform config auth enable company
472
+ void platform config auth disable github
473
+ void platform config auth admission
474
+ void platform config auth protection show
475
+ void platform config auth protection enable --installation <id>
476
+ void platform config auth protection disable --installation <id>
477
+ void platform config auth recover company --installation <id> --file recovery.json
478
+ ```
479
+
480
+ When configuring an existing installation for the first time, run
481
+ `void platform config auth initialize`, then sign in again. Its current login
482
+ methods and signup policy are preserved.
483
+
484
+ Adding or editing a method saves a pending configuration. Enabling it verifies the
485
+ login in your browser before applying it. Linking the verified identity to your
486
+ account is a separate, explicit action. Before disabling a method, verify a linked
487
+ alternative; the last method cannot be disabled. Disabling revokes human sessions
488
+ created through that method, including operator sessions. Scoped deployment tokens
489
+ remain valid; a human login token used as `VOID_TOKEN` is still revoked.
490
+
491
+ Commands accept `--connection <registered-id-or-url>` and `--json`. Changes accept
492
+ `--plan` or `--yes`. For scripted configuration, use `--file <path>` for the
493
+ nonsecret fields and `--client-secret-env <name>` for the environment variable
494
+ containing the secret. Omit the secret when editing to retain its saved value.
495
+ For `enable` or `link` in scripts, supply `--test-id <id>` from a completed test.
496
+ `test --json` returns a browser URL, test ID, and expiry without waiting for completion.
497
+
498
+ `admission` chooses invited/allowlisted, company-approved, or public signup.
499
+ Company-approved signup creates ordinary accounts automatically when a user
500
+ passes a configured company rule. Select an enabled, company-restricted OIDC or
501
+ Google Workspace method, or an enabled Cloudflare Access gate. In scripts,
502
+ `admission --file <path> --yes` reads a policy such as
503
+ `{"mode":"company","connections":["company"],"access":false}`.
504
+
505
+ Browser login offers the platform's enabled methods; `--provider <connection-id>` selects one. Login saves a one-hour administrator session in your system keychain. Logout revokes that session and removes its local credential.
506
+
507
+ `protection enable` creates or connects Cloudflare Access applications independently
508
+ of login methods. Its `--file` accepts the `cloudflareAccess` object described in
509
+ [installation setup](../guide/self-hosted-platform.md#configure-github-oauth).
510
+ Protection changes require installation ownership, the saved recovery credentials,
511
+ and a human administrator session. They revoke current human sessions. Before
512
+ removing protection, change any signup rule that depends on that gate. Cloudflare
513
+ applications are retained for deliberate cleanup.
514
+
515
+ `recover <connection-id>` restores an existing administrator when normal login is
516
+ unavailable. Its file contains `administratorUserId`, optional nonsecret provider
517
+ `configuration`, and an `expectedIdentity` object with exact `issuer` and `subject`
518
+ when using `--yes`. Recovery requires Cloudflare management/database authority,
519
+ original recovery keys, and a successful browser provider test. Use
520
+ `--client-secret-env <name>` for new or rotated credentials.
371
521
 
372
522
  `auth token` prints your current operator token. With `--token-stdin`, it exchanges a full administrator API login session from standard input for a new operator token. `auth login --token-stdin` saves the exchanged token to the keychain instead of printing it.
373
523
 
@@ -406,10 +556,13 @@ List projects across the platform, filter them by owner, or inspect one project'
406
556
  void platform project list [--user <user-id>] [--search <text>] [--page <n>] [--limit <n>]
407
557
  void platform project show <id>
408
558
  void platform project delete <id>
559
+ void platform project owner <project-id> <user-id>
409
560
  ```
410
561
 
411
562
  Search matches a project's slug, ID, or owner's login. `show` includes resources, domains, the latest 10 deployments, and the latest 20 builds. `delete` removes the project and its resources.
412
563
 
564
+ `owner` transfers a project to another registered user. Preview it with `--plan`; apply it interactively or with `--yes`. The preview reports active-work blockers and the account plan that will apply. The former owner becomes a project administrator, existing project-scoped CI deploy credentials are revoked, and usage already incurred remains with the former owner.
565
+
413
566
  #### Deployments {#operator-deployments}
414
567
 
415
568
  Find a deployment, inspect its manifest, or request cancellation:
@@ -455,14 +608,24 @@ void platform signup open
455
608
  void platform signup restrict
456
609
  ```
457
610
 
458
- Add and remove entries by their type and pattern:
611
+ Add and remove GitHub or email entries by their type and pattern:
459
612
 
460
613
  ```sh
461
614
  void platform signup allow <github|email> <pattern> [--note <text>]
615
+ void platform signup disallow <github|email> <pattern>
462
616
  void platform signup remove <github|email> <pattern>
463
617
  ```
464
618
 
465
- GitHub entries match a login. Email entries match an address or a domain pattern such as `*@example.com`, across sign-in providers. Quote wildcard patterns in your shell. With restrictions enabled and an empty allowlist, nobody new can sign up.
619
+ `remove` remains available as an alias for existing scripts. GitHub entries match a login. Email entries match an address or a domain pattern such as `*@example.com`, across sign-in providers. Quote wildcard patterns in your shell.
620
+
621
+ For an OIDC identity that has no verified email, grant access using its configured connection ID and stable provider subject:
622
+
623
+ ```sh
624
+ void platform signup allow identity <connection-id> <subject> [--note <text>]
625
+ void platform signup disallow identity <connection-id> <subject>
626
+ ```
627
+
628
+ Connection IDs are shown by `void platform config auth list`. Identity subjects match exactly and case-sensitively; wildcards, email inference, and account linking are not applied. The login method's domain or group restrictions must still pass, and a newly admitted account has the ordinary user role. With restrictions enabled and an empty allowlist, nobody new can sign up.
466
629
 
467
630
  #### Invitations {#operator-invitations}
468
631
 
@@ -476,6 +639,61 @@ void platform invitation revoke <id>
476
639
 
477
640
  Send accepts up to 100 comma-separated addresses. Invitations grant signup access even if email delivery is unavailable or fails; delivery is reported separately. Revoking a pending invitation removes its exact email grant. A broader domain entry can still allow that person to sign up.
478
641
 
642
+ #### Email {#operator-email}
643
+
644
+ Decide who mail from the shared sender may reach, who registers email domains, and a project's outbound caps:
645
+
646
+ ```sh
647
+ void platform email policy
648
+ void platform email policy-set <verified|domains|any> [--domains <domain[,domain...]>]
649
+ void platform email settings
650
+ void platform email settings-set --domains <self-serve|admin>
651
+ void platform email limit <project-id|slug> [--monthly <n>] [--burst <n>]
652
+ void platform email logs <project-id|slug> [--page <n>] [--limit <n>] [--json]
653
+ void platform email attempts [--project <id|slug>] [--page <n>] [--limit <n>]
654
+ void platform email attempt-resolve <attempt-id> --ended --reason <text>
655
+ void platform email operation-resolve <operation-id> --ended --outcome <applied|not-applied> --reason <text>
656
+ ```
657
+
658
+ `policy` decides which recipients a project's `<slug>+tag@<mail domain>` sender reaches: `verified` (the default) means only that project's verified destinations; `domains` adds every address on the listed domains; `any` lifts the check. Neither widens delivery to addresses on the platform's own mail domain: those stay verified-destination-only, so no project reaches another project's inbox without its consent. Cloudflare still refuses a destination it has not verified until the platform mail domain is onboarded for Email Sending, so under `domains` or `any` such refusals arrive as per-recipient `UNVERIFIED_DESTINATION` results. Custom-domain sends are not affected.
659
+
660
+ `settings-set --domains admin` tells `void email domain add` to print the administrator's command instead of starting token setup. `limit` overrides the project's monthly and rolling 60-second caps (defaults 200 and 10); a project page in the admin UI shows and clears them.
661
+
662
+ `logs` inspects retained receipt and recipient outcomes, including operation IDs, provider references, error codes, and policy versions. Pages contain at most 100 records, newest first; use `--json` for all fields. After project deletion, use its project ID to inspect metadata until the 30-day retention period expires. Message content and credentials are never included.
663
+
664
+ `attempts` lists interrupted provider calls and their earliest resolution time. Once
665
+ the original Worker execution has ended and the attempt is at least 24 hours old,
666
+ `attempt-resolve` records `outcome_unknown`, retains its quota charge, and releases
667
+ the project/domain cleanup fence. `--ended` is your attestation that the call is no
668
+ longer active; `--reason` is stored in the operator audit log. Keep recipient
669
+ addresses and message content out of the reason. The send is never retried. Use
670
+ `--plan` to preview and `--yes` to apply without a prompt.
671
+
672
+ `operation-resolve` recovers a Cloudflare routing, Worker, secret, catch-all, or
673
+ Sending mutation whose outcome remains unknown. After the original execution
674
+ has ended and the operation is at least 24 hours old, inspect the exact resource
675
+ named by the preview and attest whether its write was `applied` or `not-applied`.
676
+ Applied writes continue at the next step; not-applied writes retry the same
677
+ persisted intent. The running platform version must match that intent, so restore
678
+ the matching version before recovering an operation created by older code. The
679
+ preview pins the step, attempt, connection and route generations, resource
680
+ identity, and digest used by the apply request. Time alone never retries a write.
681
+
682
+ Register and maintain email domains for projects whose owners hold no Cloudflare credential:
683
+
684
+ ```sh
685
+ void platform email domains [--project <id|slug>]
686
+ void platform email domain-add <domain> --project <id|slug> [--token-stdin]
687
+ void platform email domain-status <domain>
688
+ void platform email domain-sync <domain>
689
+ void platform email domain-rotate-secret <domain>
690
+ void platform email domain-remove <domain> [--token-stdin]
691
+ ```
692
+
693
+ `domain-add` uses the platform's Cloudflare credential for zones in its account. For another account, pipe a scoped Cloudflare API token on standard input with `--token-stdin --yes`. Name the exact mail domain (`mail.example.com`, or the apex when it receives no mail yet). `domain-status` shows inbound, outbound, and credential-management readiness with the latest operation. A blocked operation resumes through `domain-sync`; a blocked rotation resumes through `domain-rotate-secret`, preserving already confirmed steps and its staged credential. An uncertain operation remains stopped until read-back proves the result or an administrator uses `operation-resolve`. Domains an administrator adds show `managed_by: admin`; their owners can list and inspect them but use these commands for `sync`, `domain-rotate-secret`, and `remove`.
694
+
695
+ If project deletion leaves cleanup blocked by an expired or revoked Cloudflare token, use `domain-remove <domain> --token-stdin --yes` with a replacement scoped to the same account and zone. This resumes the retained cleanup only when no other project uses the connection. For a live project, renew its token through `domain-add` instead.
696
+
479
697
  #### System {#operator-system}
480
698
 
481
699
  Inspect activity, check service health, or review administrative changes:
@@ -516,6 +734,7 @@ void platform install [options] [--yes]
516
734
  | `--name <slug>` | Installation name used in `void-<name>-<role>` resource names; choose an unused name |
517
735
  | `--display-name <name>` | Human-readable platform name |
518
736
  | `--account <id>` | Cloudflare account id |
737
+ | `--auth-config <path>` | Login methods, signup policy, and environment references for provider secrets |
519
738
  | `--application-domain <domain>` | Base domain for deployed apps |
520
739
  | `--workers-dev` | Explicit testing mode; add an application domain later |
521
740
  | `--zone <domain>` | Cloudflare zone containing the application domain |
@@ -532,7 +751,9 @@ Read-only plans, workers.dev installations with the default API hostname, and su
532
751
 
533
752
  The installed platform needs a separate runtime token to provision resources for apps. The interactive installer prompts for it and the other setup values. For non-interactive installs, inject the variables listed in [Install from CI](../guide/self-hosted-platform.md#install-from-ci).
534
753
 
535
- `--plan` prints the actual resource names, GitHub callback, and direct setup links without opening credential pages or saving a draft; Cloudflare browser login still opens if needed. New platform resources use `void-<name>-<role>` names without random suffixes. Existing installations keep their recorded names, and unowned name conflicts stop installation without overwriting resources. After you confirm an interactive install, Void opens each missing credential's setup page and shows a short permission/checklist fallback. The runtime-token link preselects all required account permissions, including Workers Tail, Hyperdrive, and AI Gateway when needed; domain installations must also select the indicated zone. Supplied credentials skip browser opening. Setup drafts pin Worker names and the GitHub callback and save partial credentials encrypted locally. Interactive installs list unfinished installations, including interrupted provisioning, or offer a new install. Entering an existing unfinished name asks to resume it; declining returns to name entry. Starting new leaves previous setup, credentials, and resources untouched. Completed platforms are not offered for resumption. `--resume` skips the choice and is required for non-interactive recovery.
754
+ To enable email during install or upgrade, set both `VOID_EMAIL_SENDER_DOMAIN` and `VOID_EMAIL_SHARED_ZONE_ID`. Void records the pair for later upgrades; supplying only one is an error.
755
+
756
+ `--plan` prints the actual resource names, selected login methods, login callback, and direct setup links without opening credential pages or saving a draft; Cloudflare browser login still opens if needed. New platform resources use `void-<name>-<role>` names without random suffixes. Existing installations keep their recorded names, and unowned name conflicts stop installation without overwriting resources. After you confirm an interactive install, Void opens each missing credential's setup page and shows a short permission/checklist fallback. The runtime-token link preselects all required account permissions, including Workers Tail, Hyperdrive, and AI Gateway when needed; domain installations must also select the indicated zone. Supplied credentials skip browser opening. Setup drafts pin Worker names and the login callback and save partial credentials encrypted locally. Interactive installs list unfinished installations, including interrupted provisioning, or offer a new install. Entering an existing unfinished name asks to resume it; declining returns to name entry. Starting new leaves previous setup, credentials, and resources untouched. Completed platforms are not offered for resumption. `--resume` skips the choice and is required for non-interactive recovery.
536
757
 
537
758
  Use an empty PostgreSQL database dedicated to the installation. You can correct a failed initial connection, but after the database is claimed or Hyperdrive is provisioned, commands reject a different URL.
538
759
 
@@ -574,7 +795,7 @@ void platform uninstall [id] [--plan] [--purge-data] [--keep-zone] [--yes]
574
795
 
575
796
  Omit `id` when only one installation is configured, or choose from the interactive picker. Non-interactive commands need an ID when several installations exist. Commands that make changes also require `--yes`; `--plan` only previews changes.
576
797
 
577
- After discovery on another machine, set `VOID_PLATFORM_DATABASE_URL`. Normal upgrades preserve deployed Worker secrets. Restore the original runtime, GitHub, R2, JWT, and project-encryption values only if repair needs to recreate a missing API or proxy Worker.
798
+ After discovery on another machine, set `VOID_PLATFORM_DATABASE_URL`. An upgrade that preserves every deployed Worker also preserves its secrets. For an email-enabled installation without its encrypted recovery file, restore `VOID_PLATFORM_EMAIL_SIGNING_SECRET`; recreating only the email gateway needs that key and does not need the Cloudflare runtime token or JWT signing key. Recreating the API or proxy also requires the email key when email is enabled, in addition to their normal secrets. Recreating the API requires its original runtime-token, GitHub, R2, JWT, and project-encryption values; recreating the proxy requires the runtime token and JWT signing key.
578
799
 
579
800
  | Command | Behavior |
580
801
  | ----------- | --------------------------------------------------------------------------------------------- |
@@ -598,7 +819,7 @@ Platform migrations only move forward. Void checks compatibility before updating
598
819
 
599
820
  An upgrade completes after the new Workers pass health checks. If rollout fails, Void attempts to restore the previous Workers. Retrying does not repeat completed migrations.
600
821
 
601
- `platform rollback` restores a compatible earlier runtime without reversing database migrations. Pass its files with `--runtime`. If the installed version is a custom build, also supply that version with `--from-runtime`. Void refuses rollbacks that are incompatible with the current database. A later `upgrade` can move forward again.
822
+ `platform rollback` restores a compatible earlier runtime without reversing database migrations. Pass its files with `--runtime`. If the installed version is a custom build, also supply that version with `--from-runtime`. Void refuses targets that are incompatible with the current database or predate installed authentication, sandbox-drain, or ownership-aware usage protocols. A later `upgrade` can move forward again.
602
823
 
603
824
  Uninstall verifies remote ownership before removing anything. Data resources are retained unless you pass `--purge-data`. Workers, R2, AI Gateway, DNS records, routes, custom domains, adopted resources, external PostgreSQL, and zones are always retained for manual review.
604
825
 
@@ -724,7 +945,7 @@ Provisioning reuses known resource IDs and writes newly resolved IDs into `wrang
724
945
 
725
946
  Existing remote secrets are preserved. Void also preserves or creates `BETTER_AUTH_SECRET` for auth apps.
726
947
 
727
- **Email.** When the app uses email (`sendEmail()` or `email/` handlers) and `void.json` has `email.from`, the deploy reads the state of that address's zone before the build — session scopes, zone, MX records, Email Routing, subaddressing, routing rules, Email Sending, and what `wrangler.jsonc` holds — prints a checklist of what it would change in your account, and asks once (default Yes). On Yes it enables what is missing, writes `send_email: [{ "name": "SEND_EMAIL" }]`, the `__VOID_EMAIL_FROM` var and the `addresses` array into `wrangler.jsonc`, and lets wrangler create the routing rules when the activated version's triggers are synchronized; the deploy ends with the address map. A deploy with nothing left to set up asks nothing. Without `email.from` the deploy prints `add "email": { "from": "you@mail.acme.com" } to void.json` and continues without email. Non-interactive runs (CI, or stdin/stdout not a terminal) never prompt: they print the checklist plus `Run void email setup --platform cloudflare once locally, commit wrangler.jsonc, then redeploy` and deploy without email (or with the setup `wrangler.jsonc` already carries, when the binding is committed; a committed `addresses` array whose routing is off is removed first, since wrangler's plan on it would fail after the upload) — unless `--require-email` is passed, which fails instead. A deploy whose account rows all read ready reconciles the two config rows — `addresses` against the current derivation and `vars.__VOID_EMAIL_FROM` against `email.from` — with a plain file write and no prompt. See [Your own Cloudflare account](../guide/email.md#your-own-cloudflare-account) for the whole flow, including the subdomain-vs-apex rule and what stays manual.
948
+ **Email.** When the app uses email (`sendEmail()` or `email/` handlers) and `void.json` has `email.from`, the deploy reads the state of that address's zone before the build — session scopes, zone, MX records, Email Routing, subaddressing, routing rules, Email Sending, and what `wrangler.jsonc` holds — prints a checklist of what it would change in your account, and asks once (default Yes). On Yes it enables what is missing, writes `send_email: [{ "name": "SEND_EMAIL" }]`, the `__VOID_EMAIL_FROM` var and the `addresses` array into `wrangler.jsonc`, and lets wrangler create the routing rules when the activated version's triggers are synchronized; the deploy ends with the address map. A deploy with nothing left to set up asks nothing. Without `email.from` the deploy prints `add "email": { "from": "you@mail.acme.com" } to void.json` and continues without email. Non-interactive runs (CI, or stdin/stdout not a terminal) never prompt: they print the checklist plus `Run void email setup --platform cloudflare once locally, commit wrangler.jsonc, then redeploy` and deploy without email (or with the setup `wrangler.jsonc` already carries, when the binding is committed) — unless `--require-email` is passed, which fails instead. If setup has committed the exact subdomain `addresses` plan but the deploy's resolver still sees no MX records, Void preserves the plan and stops before build or upload until DNS can be verified. A deploy whose account rows all read ready reconciles the two config rows — `addresses` against the current derivation and `vars.__VOID_EMAIL_FROM` against `email.from` — with a plain file write and no prompt. See [Your own Cloudflare account](../guide/email.md#your-own-cloudflare-account) for the whole flow, including the subdomain-vs-apex rule and what stays manual.
728
949
 
729
950
  See the [Cloudflare guide](../integrations/cloudflare.md#deploy-to-your-own-cloudflare-account) for the complete deployment sequence, first-deploy exceptions, secret precedence, and recovery behavior.
730
951
 
@@ -742,6 +963,8 @@ Generate SQL migration files from schema changes.
742
963
 
743
964
  The command compares your current `db/schema.ts` or `db/schema/` modules against the last generated Drizzle snapshot and writes new migration artifacts under `db/migrations/`. When Void-managed auth is enabled, it also resolves the Better Auth schema in production mode and includes those tables automatically, including configured renames and plugin tables. This works for auth-only apps without an application schema. Review and commit the generated files before deploying.
744
965
 
966
+ For SQLite, Void checks that the migration history applies to a fresh database. If generation fails this check, the previous SQL, snapshots, and journal are restored. If an existing migration fails, repair that unapplied migration first: rerunning generation compares snapshots and does not repair existing SQL. This check does not verify that a migration preserves existing data; review table rebuilds and foreign-key actions carefully.
967
+
745
968
  ### `void db status`
746
969
 
747
970
  Show migration status. Displays which migrations are applied or pending locally, then uses the saved deployment target for remote status: the hosted API for Void projects, the pinned D1 database and its configured migration table for direct Cloudflare SQLite projects, or the shell `DATABASE_URL` for direct Cloudflare PostgreSQL/MySQL projects. If the remote credential or service is unavailable, local status is still shown.
@@ -1283,7 +1506,7 @@ Project resolution for email commands follows the same order as deploy (`--proje
1283
1506
  void email usage [--project <name>]
1284
1507
  ```
1285
1508
 
1286
- Show the current month's outbound and inbound counts, the monthly outbound limit, and how much of it is left. A suspended project is flagged in the output.
1509
+ Show the current month's recipient attempts and inbound receipts, the monthly attempt limit, and how much of it is left. Reserved submissions count toward the limit; started attempts remain charged even if delivery fails or its outcome is unknown. A suspended project is flagged in the output.
1287
1510
 
1288
1511
  ### `void email logs`
1289
1512
 
@@ -1291,7 +1514,7 @@ Show the current month's outbound and inbound counts, the monthly outbound limit
1291
1514
  void email logs [--limit <n>] [--project <name>]
1292
1515
  ```
1293
1516
 
1294
- Show recent email activity — timestamp, direction, sender, recipient, status, and subject. `--limit` takes a positive integer. Email activity logs are not available yet on the platform; the command says so. Console output from your email handler appears in `void project logs`, like any other invocation of your worker.
1517
+ Show recent email operation metadata retained for 30 days: timestamp, direction, operation ID, recipient, and state. `--limit` accepts 1–100. Subjects, bodies, and attachments are not stored. Provider acceptance does not confirm mailbox delivery; inspect unknown outcomes before retrying.
1295
1518
 
1296
1519
  ### `void email destinations`
1297
1520
 
@@ -1317,7 +1540,63 @@ The project owner's email is added automatically when the project is created, so
1317
1540
  void email disallow <address> [--project <name>]
1318
1541
  ```
1319
1542
 
1320
- Remove one recipient from the project's destination list. Sends to that address are refused within about a minute: the platform updates the project's allowlist as part of the command, and the proxy re-reads it every 60 seconds. No deploy is involved.
1543
+ Remove one recipient from the project's destination list. New sends to that address are refused immediately; previously admitted attempts may finish. No deploy is involved.
1544
+
1545
+ ### `void email domain`
1546
+
1547
+ ```
1548
+ void email domain <add|status|list|sync|rotate-secret|remove> [<domain>] [--project <name>]
1549
+ ```
1550
+
1551
+ Send and receive at your own domain on a Cloudflare zone you own, registered to the project. Void platform only — on your own Cloudflare account the mail domain comes from `email.from` instead (see `void email setup`). The walkthrough is [Your own domain on the platform](../guide/email.md#your-own-domain-on-the-platform).
1552
+
1553
+ #### `void email domain add`
1554
+
1555
+ ```
1556
+ void email domain add <domain> [--subdomain <label|host>] [--project <name>]
1557
+ ```
1558
+
1559
+ Register an exact domain with the project using a scoped Cloudflare API token. The CLI opens a token template, accepts a masked paste or newly copied token, and asks you to confirm account and zone restrictions. Credentials are encrypted for the zone connection. The CLI proposes `mail.<domain>` when the apex already has MX records; `--subdomain` overrides that proposal. Several domains can share a zone connection, but each domain belongs to one project. Setup returns an operation ID so an interrupted request can be checked without restarting the operation.
1560
+
1561
+ #### `void email domain status`
1562
+
1563
+ ```
1564
+ void email domain status <domain> [--project <name>]
1565
+ ```
1566
+
1567
+ Show the recorded inbound, outbound, and management readiness, observation times, and latest operation. Use `sync` to reconcile setup and refresh readiness.
1568
+
1569
+ #### `void email domain list`
1570
+
1571
+ ```
1572
+ void email domain list [--project <name>]
1573
+ ```
1574
+
1575
+ List the project's registered email domains and readiness.
1576
+
1577
+ #### `void email domain sync`
1578
+
1579
+ ```
1580
+ void email domain sync <domain> [--project <name>]
1581
+ ```
1582
+
1583
+ Reconcile the domain connection and refresh readiness. Sync does not rotate its secret. A blocked or uncertain operation exits unsuccessfully and names the operation to inspect.
1584
+
1585
+ #### `void email domain rotate-secret`
1586
+
1587
+ ```sh
1588
+ void email domain rotate-secret <domain> [--project <name>]
1589
+ ```
1590
+
1591
+ Rotate the ingress secret for the zone connection shared by this domain and its siblings. Inbound must be ready; run `void email domain sync <domain>` first if setup is incomplete. The platform accepts the staged secret before updating the Worker and promotes it only after verifying the deployed generation. An uncertain update stays recorded for reconciliation.
1592
+
1593
+ #### `void email domain remove`
1594
+
1595
+ ```
1596
+ void email domain remove <domain> [--project <name>]
1597
+ ```
1598
+
1599
+ Disable the domain assignment and record cleanup. Zone resources used by another domain remain available. Unfinished or uncertain cleanup remains recorded until it can be reconciled safely.
1321
1600
 
1322
1601
  ### `void email status`
1323
1602
 
@@ -1,42 +0,0 @@
1
- //#region src/shared/cf-access.ts
2
- /**
3
- * Resolve Cloudflare Access headers from an env-like object.
4
- *
5
- * Priority:
6
- * 1. Service token pair (`CF_ACCESS_CLIENT_ID` + `CF_ACCESS_CLIENT_SECRET`) — CI path
7
- * 2. User JWT (`CF_ACCESS_TOKEN`) — dev path, typically populated via `cloudflared access token`
8
- * 3. No headers — prod, or unprotected hostnames
9
- */
10
- function isVoidAccessGatedUrl(targetUrl) {
11
- try {
12
- const url = new URL(targetUrl);
13
- return url.protocol === "https:" && (url.hostname.endsWith(".staging.void.cloud") || url.hostname.endsWith(".staging.void.app"));
14
- } catch {
15
- return false;
16
- }
17
- }
18
- function accessHeaders(env) {
19
- const id = env.CF_ACCESS_CLIENT_ID;
20
- const secret = env.CF_ACCESS_CLIENT_SECRET;
21
- if (typeof id === "string" && typeof secret === "string" && id.length > 0 && secret.length > 0) return {
22
- "CF-Access-Client-Id": id,
23
- "CF-Access-Client-Secret": secret
24
- };
25
- const token = env.CF_ACCESS_TOKEN;
26
- if (typeof token === "string" && token.length > 0) return { "cf-access-token": token };
27
- return {};
28
- }
29
- /** Attach shared Void Access credentials only to known hosted staging origins. */
30
- function cfAccessHeaders(env, targetUrl) {
31
- return isVoidAccessGatedUrl(targetUrl) ? accessHeaders(env) : {};
32
- }
33
- /**
34
- * Capture credentials for a Cloudflare Worker readiness request whose target
35
- * is resolved and constrained by the direct-deploy adapter rather than a
36
- * user-connected Void platform URL.
37
- */
38
- function trustedCloudflareWorkerAccessHeaders(env) {
39
- return accessHeaders(env);
40
- }
41
- //#endregion
42
- export { isVoidAccessGatedUrl as n, trustedCloudflareWorkerAccessHeaders as r, cfAccessHeaders as t };
@@ -1,67 +0,0 @@
1
- import { n as isVoidAccessGatedUrl } from "./cf-access-AJ1ehiFR.mjs";
2
- import { spawnSync } from "node:child_process";
3
- //#region src/cli/cf-access.ts
4
- const probedHosts = /* @__PURE__ */ new Set();
5
- /**
6
- * If the CLI is about to talk to a Cloudflare-Access-gated staging host,
7
- * probe the locally-installed `cloudflared` binary for a cached user JWT
8
- * and export it as `CF_ACCESS_TOKEN`. On interactive shells with no cached
9
- * token, open a browser login flow to populate the cache, then re-read it.
10
- *
11
- * Cached per-host so that an early non-gated call (e.g. prod) does not
12
- * permanently disable the probe for a later gated call (e.g. staging) in
13
- * the same process. Noop when:
14
- * - a service-token pair is already set (CI path)
15
- * - `CF_ACCESS_TOKEN` is already set (manual override or prior probe)
16
- * - the target host is not gated
17
- * - `cloudflared` is not installed
18
- */
19
- function ensureCloudflaredToken(apiUrl) {
20
- if (process.env.CF_ACCESS_CLIENT_ID && process.env.CF_ACCESS_CLIENT_SECRET) return;
21
- if (process.env.CF_ACCESS_TOKEN) return;
22
- if (!isVoidAccessGatedUrl(apiUrl)) return;
23
- const host = new URL(apiUrl).hostname;
24
- if (probedHosts.has(host)) return;
25
- probedHosts.add(host);
26
- let token = runCloudflared([
27
- "access",
28
- "token",
29
- `-app=${apiUrl}`
30
- ]);
31
- if (!token && process.stdin.isTTY && process.stdout.isTTY) {
32
- runCloudflared([
33
- "access",
34
- "login",
35
- apiUrl
36
- ], {
37
- inherit: true,
38
- timeoutMs: 12e4
39
- });
40
- token = runCloudflared([
41
- "access",
42
- "token",
43
- `-app=${apiUrl}`
44
- ]);
45
- }
46
- if (token) process.env.CF_ACCESS_TOKEN = token;
47
- }
48
- function runCloudflared(args, opts) {
49
- try {
50
- const result = spawnSync("cloudflared", args, {
51
- encoding: "utf-8",
52
- stdio: opts?.inherit ? "inherit" : [
53
- "ignore",
54
- "pipe",
55
- "pipe"
56
- ],
57
- timeout: opts?.timeoutMs ?? 5e3
58
- });
59
- if (result.error || result.status !== 0) return null;
60
- const out = (result.stdout ?? "").trim();
61
- return out.length > 0 ? out : null;
62
- } catch {
63
- return null;
64
- }
65
- }
66
- //#endregion
67
- export { ensureCloudflaredToken as t };