void 0.20.1 → 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 (87) hide show
  1. package/README.md +5 -1
  2. package/dist/{auth-W9WII-mN.mjs → auth-DPl6kck4.mjs} +46 -26
  3. package/dist/{auth-cmd-CAH62yDU.mjs → auth-cmd-gniL2fNt.mjs} +5 -4
  4. package/dist/auth-link-NZdjCmSc.mjs +28 -0
  5. package/dist/{build-cmd-CJvZvPQO.mjs → build-cmd-sI18tX_O.mjs} +3 -3
  6. package/dist/{cache-BlNeQjuP.mjs → cache-IHn5MwBC.mjs} +3 -3
  7. package/dist/{cancel-deploy-CmlAZ9P6.mjs → cancel-deploy-C5qTOdLi.mjs} +3 -3
  8. package/dist/cf-access-DRsQRe6k.mjs +75 -0
  9. package/dist/cli/cli.mjs +308 -1958
  10. package/dist/cli/env-schema-probe.mjs +11 -2
  11. package/dist/{client-Clirrol3.mjs → client-dHfSJvAN.mjs} +300 -81
  12. package/dist/{cloudflare-auth-B1QtTO1b.mjs → cloudflare-auth-6M5llVPC.mjs} +2 -2
  13. package/dist/{cloudflare-cmd-B6_OZx2V.mjs → cloudflare-cmd-4RPGN3KB.mjs} +2 -2
  14. package/dist/{cloudflare-connect-j5D4hhrG.mjs → cloudflare-connect-t1UU5svD.mjs} +2 -2
  15. package/dist/{cloudflare-operations-CPTpRW6d.mjs → cloudflare-operations-BzWnlC1_.mjs} +1 -1
  16. package/dist/{config-BQFq7QvD.mjs → config-uNGuFsI2.mjs} +1 -1
  17. package/dist/{connect-C04Wdy_h.mjs → connect-Bfk31O_8.mjs} +6 -6
  18. package/dist/{create-project-ChGZ1DFd.mjs → create-project-Bk9Z0-Jg.mjs} +6 -5
  19. package/dist/{db-D2d_mUsB.mjs → db-BkRoptAt.mjs} +47 -30
  20. package/dist/{delete-D8GigDk8.mjs → delete-DouASY9P.mjs} +3 -3
  21. package/dist/{deploy-iXZ3F0N6.mjs → deploy-DTaWUS1S.mjs} +121 -110
  22. package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
  23. package/dist/{domain-B1VmoSr0.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-BcQzYgoG.mjs → env-DJHsPE7Z.mjs} +5 -5
  27. package/dist/{env-validation-ENpMy6Ez.mjs → env-validation-CF6KvTRf.mjs} +3 -1
  28. package/dist/{gen-DI2YwdBM.mjs → gen-B_wPnVTK.mjs} +2 -2
  29. package/dist/{github-cmd-xItS5Zwf.mjs → github-cmd-PW7ZnWTp.mjs} +3 -3
  30. package/dist/{headers-D8QfRX9Y.mjs → headers-BAHwgHdW.mjs} +1 -1
  31. package/dist/help-CwOX-zmI.mjs +2216 -0
  32. package/dist/{inbound-afAcWeQ9.d.mts → inbound-CH5Mksyy.d.mts} +34 -48
  33. package/dist/{inbound-2d0zi2yS.mjs → inbound-aVHEUhKo.mjs} +130 -100
  34. package/dist/index.mjs +54 -17
  35. package/dist/{init-BD-9THgn.mjs → init-BWZ7q5Z4.mjs} +11 -11
  36. package/dist/{link-RMdgjF1v.mjs → link-Rmvu2Wl_.mjs} +4 -4
  37. package/dist/{list-3F52R_yO.mjs → list-DEE2S6mY.mjs} +4 -4
  38. package/dist/{login-pV69H-ZO.mjs → login-Uvferzmm.mjs} +28 -10
  39. package/dist/{logs-DFHHD6wE.mjs → logs-27FenuiC.mjs} +4 -4
  40. package/dist/{mime-BJD7d_qL.mjs → mime-D5Nmdzf7.mjs} +23 -9
  41. package/dist/{node-Dk3H2jmU.mjs → node-Ez5KW5rn.mjs} +2 -2
  42. package/dist/operator-auth-B3e08unv.mjs +52 -0
  43. package/dist/operator-client-LUZnlnYk.mjs +82 -0
  44. package/dist/{operator-cmd-DYWRbWUA.mjs → operator-cmd-CjOTmAYE.mjs} +35 -55
  45. package/dist/{output-tFQLLj26.mjs → output-B0cfNSx5.mjs} +316 -2
  46. package/dist/pages/index.mjs +2 -2
  47. package/dist/platform-auth-config-DrbQXXiW.mjs +368 -0
  48. package/dist/platform-auth-protection-Bhtvp0B_.mjs +219 -0
  49. package/dist/platform-auth-recovery-CeOKGVeJ.mjs +310 -0
  50. package/dist/{platform-cmd-DxJ2FRwR.mjs → platform-cmd-BFhieCdV.mjs} +16 -6
  51. package/dist/{platform-domain-ChvbJkdy.mjs → platform-domain-C74PULqV.mjs} +4 -4
  52. package/dist/{platform-lifecycle-DN4MzJF_.mjs → platform-lifecycle-BwAIgz-t.mjs} +1667 -191
  53. package/dist/{platform-management-Db2PXw0B.mjs → platform-management-COogu_Se.mjs} +33 -7
  54. package/dist/{platform-recovery-C_YO-tIs.mjs → platform-recovery-ewqLefp1.mjs} +6 -5
  55. package/dist/{prepare-CBetXvsN.mjs → prepare-CtDJjoOj.mjs} +2 -2
  56. package/dist/{prepare-BfJvFUtJ.mjs → prepare-blNRQvQl.mjs} +2 -2
  57. package/dist/prerender-render.d.mts +11 -0
  58. package/dist/prerender-render.mjs +111 -0
  59. package/dist/{project-cmd-Mo0V9yKS.mjs → project-cmd-DmZK9Hxf.mjs} +32 -14
  60. package/dist/project-team-D8jOJMUJ.mjs +130 -0
  61. package/dist/project-token-DA34bf-C.mjs +75 -0
  62. package/dist/{provision-Blnstcm2.mjs → provision-CSJOjjQk.mjs} +2 -0
  63. package/dist/{requests-BcKOVpRg.mjs → requests-CUExwGQQ.mjs} +3 -3
  64. package/dist/{rollback-Bx85-0xh.mjs → rollback-CDNGU1gr.mjs} +4 -4
  65. package/dist/runtime/ai.mjs +3 -2
  66. package/dist/runtime/email/testing.d.mts +1 -1
  67. package/dist/runtime/email/testing.mjs +3 -3
  68. package/dist/runtime/email-protocol.d.mts +15 -0
  69. package/dist/runtime/email-protocol.mjs +70 -0
  70. package/dist/runtime/email.d.mts +2 -2
  71. package/dist/runtime/email.mjs +189 -96
  72. package/dist/runtime/remote/index.mjs +5 -3
  73. package/dist/{secret-ByhJ9AMl.mjs → secret-Bzzi2e9E.mjs} +5 -5
  74. package/dist/{skills-Q46GZMO-.mjs → skills-C0RvGjeE.mjs} +1 -1
  75. package/dist/{subcommand-prompt-WfySCQ7S.mjs → subcommand-prompt-Bmyn5Rlc.mjs} +1 -1
  76. package/package.json +12 -7
  77. package/skills/void/SKILL.md +35 -4
  78. package/skills/void/docs/guide/deployment.md +2 -0
  79. package/skills/void/docs/guide/email.md +102 -110
  80. package/skills/void/docs/guide/platform-administration.md +214 -2
  81. package/skills/void/docs/guide/platform-development.md +64 -2
  82. package/skills/void/docs/guide/project-collaboration.md +94 -0
  83. package/skills/void/docs/guide/self-hosted-platform.md +184 -30
  84. package/skills/void/docs/reference/cli.md +249 -21
  85. package/dist/cf-access-AJ1ehiFR.mjs +0 -42
  86. package/dist/cf-access-DsSsZUPr.mjs +0 -67
  87. package/dist/email-uKyQYUVY.mjs +0 -1016
@@ -41,7 +41,7 @@ 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 |
@@ -166,7 +166,7 @@ In a non-interactive shell, supply a URL or explicit target. Cloudflare requires
166
166
 
167
167
  ### `void auth login`
168
168
 
169
- 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.
170
170
 
171
171
  Set `VOID_API_URL` alongside `VOID_TOKEN` to identify the platform that issued it.
172
172
  A token without an API URL is only used for Void Cloud's production API; a saved
@@ -175,6 +175,13 @@ saved login instead, unset `VOID_TOKEN`.
175
175
 
176
176
  This is optional if you already completed auth during `void connect` or the interactive `void init` flow.
177
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
+
178
185
  ### `void auth logout`
179
186
 
180
187
  Removes saved credentials.
@@ -185,7 +192,11 @@ Prints your current login.
185
192
 
186
193
  ### `void auth token`
187
194
 
188
- 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.
189
200
 
190
201
  ## Cloudflare authentication
191
202
 
@@ -217,7 +228,80 @@ Link current directory to an existing hosted Void project by slug, or select int
217
228
 
218
229
  ### `void project list`
219
230
 
220
- 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.
221
305
 
222
306
  ### `void project logs`
223
307
 
@@ -362,13 +446,78 @@ Void does not automatically retry changes. If a request loses its connection or
362
446
  Sign in, inspect your session, or sign out:
363
447
 
364
448
  ```sh
365
- void platform auth login [--provider github|google] [--token-stdin]
449
+ void platform auth login [--provider <connection-id>] [--token-stdin]
366
450
  void platform auth status
367
451
  void platform auth logout
368
452
  void platform auth token [--token-stdin]
369
453
  ```
370
454
 
371
- 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.
372
521
 
373
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.
374
523
 
@@ -407,10 +556,13 @@ List projects across the platform, filter them by owner, or inspect one project'
407
556
  void platform project list [--user <user-id>] [--search <text>] [--page <n>] [--limit <n>]
408
557
  void platform project show <id>
409
558
  void platform project delete <id>
559
+ void platform project owner <project-id> <user-id>
410
560
  ```
411
561
 
412
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.
413
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
+
414
566
  #### Deployments {#operator-deployments}
415
567
 
416
568
  Find a deployment, inspect its manifest, or request cancellation:
@@ -456,14 +608,24 @@ void platform signup open
456
608
  void platform signup restrict
457
609
  ```
458
610
 
459
- Add and remove entries by their type and pattern:
611
+ Add and remove GitHub or email entries by their type and pattern:
460
612
 
461
613
  ```sh
462
614
  void platform signup allow <github|email> <pattern> [--note <text>]
615
+ void platform signup disallow <github|email> <pattern>
463
616
  void platform signup remove <github|email> <pattern>
464
617
  ```
465
618
 
466
- 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.
467
629
 
468
630
  #### Invitations {#operator-invitations}
469
631
 
@@ -477,6 +639,61 @@ void platform invitation revoke <id>
477
639
 
478
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.
479
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
+
480
697
  #### System {#operator-system}
481
698
 
482
699
  Inspect activity, check service health, or review administrative changes:
@@ -517,6 +734,7 @@ void platform install [options] [--yes]
517
734
  | `--name <slug>` | Installation name used in `void-<name>-<role>` resource names; choose an unused name |
518
735
  | `--display-name <name>` | Human-readable platform name |
519
736
  | `--account <id>` | Cloudflare account id |
737
+ | `--auth-config <path>` | Login methods, signup policy, and environment references for provider secrets |
520
738
  | `--application-domain <domain>` | Base domain for deployed apps |
521
739
  | `--workers-dev` | Explicit testing mode; add an application domain later |
522
740
  | `--zone <domain>` | Cloudflare zone containing the application domain |
@@ -533,7 +751,9 @@ Read-only plans, workers.dev installations with the default API hostname, and su
533
751
 
534
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).
535
753
 
536
- `--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.
537
757
 
538
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.
539
759
 
@@ -575,7 +795,7 @@ void platform uninstall [id] [--plan] [--purge-data] [--keep-zone] [--yes]
575
795
 
576
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.
577
797
 
578
- 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.
579
799
 
580
800
  | Command | Behavior |
581
801
  | ----------- | --------------------------------------------------------------------------------------------- |
@@ -599,7 +819,7 @@ Platform migrations only move forward. Void checks compatibility before updating
599
819
 
600
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.
601
821
 
602
- `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.
603
823
 
604
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.
605
825
 
@@ -725,7 +945,7 @@ Provisioning reuses known resource IDs and writes newly resolved IDs into `wrang
725
945
 
726
946
  Existing remote secrets are preserved. Void also preserves or creates `BETTER_AUTH_SECRET` for auth apps.
727
947
 
728
- **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.
729
949
 
730
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.
731
951
 
@@ -1286,7 +1506,7 @@ Project resolution for email commands follows the same order as deploy (`--proje
1286
1506
  void email usage [--project <name>]
1287
1507
  ```
1288
1508
 
1289
- 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.
1290
1510
 
1291
1511
  ### `void email logs`
1292
1512
 
@@ -1294,7 +1514,7 @@ Show the current month's outbound and inbound counts, the monthly outbound limit
1294
1514
  void email logs [--limit <n>] [--project <name>]
1295
1515
  ```
1296
1516
 
1297
- 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.
1298
1518
 
1299
1519
  ### `void email destinations`
1300
1520
 
@@ -1320,12 +1540,12 @@ The project owner's email is added automatically when the project is created, so
1320
1540
  void email disallow <address> [--project <name>]
1321
1541
  ```
1322
1542
 
1323
- 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.
1324
1544
 
1325
1545
  ### `void email domain`
1326
1546
 
1327
1547
  ```
1328
- void email domain <add|status|list|sync|remove> [<domain>] [--project <name>]
1548
+ void email domain <add|status|list|sync|rotate-secret|remove> [<domain>] [--project <name>]
1329
1549
  ```
1330
1550
 
1331
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).
@@ -1336,7 +1556,7 @@ Send and receive at your own domain on a Cloudflare zone you own, registered to
1336
1556
  void email domain add <domain> [--subdomain <label|host>] [--project <name>]
1337
1557
  ```
1338
1558
 
1339
- Register the domain with the project. One credential is required, granted two ways: an OAuth grant from Cloudflare's hosted consent page (the page names Wrangler — Void borrows its OAuth client), or, as the fallback Enter switches to at any point, a scoped API token created from a three-click template link and picked up from the clipboard or a masked paste. The credential is POSTed once and stored on the platform, encrypted for the project — nothing is kept locally. Needs an interactive terminal. The CLI proposes `mail.<domain>` when the apex already carries MX records and allows the apex only when it carries none; `--subdomain` overrides the proposal. One live email domain per zone and one project per domain are enforced — a conflict is refused with a 409. Re-running `add` on a `failed`, `token_revoked`, or `token_expired` row replaces the credential and retries; the routing rules stay.
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.
1340
1560
 
1341
1561
  #### `void email domain status`
1342
1562
 
@@ -1344,7 +1564,7 @@ Register the domain with the project. One credential is required, granted two wa
1344
1564
  void email domain status <domain> [--project <name>]
1345
1565
  ```
1346
1566
 
1347
- Show one registered domain. The platform re-probes the stored credential and the relay worker on every call, so this doubles as the drift report. A `pending` row whose only remaining step is the dashboard's subdomain form (printed by `add`) self-clears to `active` once public MX on the domain names Cloudflare — which makes this command the poll for that one human step.
1567
+ Show the recorded inbound, outbound, and management readiness, observation times, and latest operation. Use `sync` to reconcile setup and refresh readiness.
1348
1568
 
1349
1569
  #### `void email domain list`
1350
1570
 
@@ -1352,7 +1572,7 @@ Show one registered domain. The platform re-probes the stored credential and the
1352
1572
  void email domain list [--project <name>]
1353
1573
  ```
1354
1574
 
1355
- List the project's registered email domains with their status and mode.
1575
+ List the project's registered email domains and readiness.
1356
1576
 
1357
1577
  #### `void email domain sync`
1358
1578
 
@@ -1360,7 +1580,15 @@ List the project's registered email domains with their status and mode.
1360
1580
  void email domain sync <domain> [--project <name>]
1361
1581
  ```
1362
1582
 
1363
- Redeploy the domain's relay worker at the current version and rotate its secret — the fix when `status` reports the relay missing or drifted.
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.
1364
1592
 
1365
1593
  #### `void email domain remove`
1366
1594
 
@@ -1368,7 +1596,7 @@ Redeploy the domain's relay worker at the current version and rotate its secret
1368
1596
  void email domain remove <domain> [--project <name>]
1369
1597
  ```
1370
1598
 
1371
- Delete the registration: the platform row, the stored credential, and the relay secret. Cloudflare-side cleanup is best-effort; any step that fails is named (`failed_steps`) so you can finish it in the Cloudflare dashboard.
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.
1372
1600
 
1373
1601
  ### `void email status`
1374
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 };