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
@@ -29,6 +29,24 @@ void platform auth status
29
29
 
30
30
  This shows your account, the session's expiry, and the features available on the platform. To end the session, run `void platform auth logout`.
31
31
 
32
+ If the platform is protected by Cloudflare Access, `void connect <url>` handles
33
+ the company sign-in before Void login. Interactive Access authentication uses a
34
+ locally installed `cloudflared` and saves its short-lived credential in your
35
+ system keychain for that platform origin. You do not need to copy browser cookies.
36
+
37
+ For CI, Access credentials do not replace Void deployment credentials. A project
38
+ owner creates the latter with `void project token create`; it is independently
39
+ revocable, expires within 90 days, and authorizes only that project's deploy
40
+ workflow. Human and operator tokens cannot be renewed by Access service proof.
41
+
42
+ Store Access credentials in origin-keyed `VOID_ACCESS_CREDENTIALS`. A deploy
43
+ that uses prerendering or remote bindings needs entries for both the exact API
44
+ and proxy HTTPS origins, even when both entries contain the same admitted
45
+ service-token pair. `VOID_ACCESS_ORIGIN` selects only one recipient and therefore
46
+ cannot cover both calls. Keep the JSON value in your secret manager, not in
47
+ application configuration. See [CI deployment setup](./self-hosted-platform.md#connect-and-deploy-an-app)
48
+ for the required shape.
49
+
32
50
  ## Choosing a Platform
33
51
 
34
52
  If you manage more than one platform, list your connections and choose a default:
@@ -46,9 +64,64 @@ void platform user list --connection <connection-id>
46
64
 
47
65
  Use an ID or URL from the connection list. Administrative commands use this selection even when you run them inside an app with a different deployment destination.
48
66
 
67
+ ## Recovering Administrator Login
68
+
69
+ If no administrator can use the configured identity provider, the installation
70
+ owner can recover an existing administrator with Cloudflare management access,
71
+ direct database access, and the original encrypted recovery credentials:
72
+
73
+ ```sh
74
+ void platform config auth recover company --installation <id> --file recovery.json
75
+ ```
76
+
77
+ The file identifies the existing account, for example
78
+ `{"administratorUserId":"existing-admin-id"}`. Recovery opens a real login test
79
+ and asks you to confirm the exact identity before restoring access. It does not
80
+ create a new administrator. To use a replacement provider, include its nonsecret
81
+ `configuration` with a new connection ID and supply the secret through
82
+ `--client-secret-env <name>`. To rotate an existing provider's secret, keep its
83
+ connection ID and identity configuration.
84
+
85
+ For `--yes`, also pin `expectedIdentity` with the exact `issuer` and `subject` in
86
+ the file. A successful browser login is still required. Use `--plan` to preview
87
+ the affected administrator and connection before starting recovery.
88
+
49
89
  ## Giving People Access
50
90
 
51
- A newly installed platform restricts signup to approved identities. To let a teammate join with GitHub, add their login to the allowlist:
91
+ Open **Settings** in the administrator UI, or run `void platform config auth`,
92
+ to add, test, enable, or disable login methods. Access protection and login
93
+ methods are separate settings. A provider test shows the authenticated account
94
+ before you explicitly link it or enable the configuration.
95
+
96
+ To change the company gate after installation, use the installing workstation
97
+ with its saved recovery credentials:
98
+
99
+ ```sh
100
+ void platform config auth protection show
101
+ void platform config auth protection enable --installation <id>
102
+ void platform config auth protection disable --installation <id>
103
+ ```
104
+
105
+ Enabling offers application creation or connection to an existing application.
106
+ It checks every API, proxy, and configured dashboard origin and requires a company
107
+ user sign-in. Changing protection ends all current human sessions; sign in again
108
+ afterwards. Before removing a gate used for company signup, select another
109
+ verified company rule or restricted signup. Removing protection retains the
110
+ Cloudflare applications for deliberate cleanup. Disabling an Access login method
111
+ does not remove the gate.
112
+
113
+ Users can add another enabled login to their existing account with
114
+ `void auth link <connection-id>` or **Account** in the optional
115
+ dashboard. Sign in again first if prompted, then authenticate with the additional
116
+ provider and confirm the identity shown. Matching email addresses alone do not
117
+ link accounts.
118
+
119
+ For company installations, choose **Company-approved users** under **Who can
120
+ join?** to create accounts automatically for users accepted by your configured
121
+ company rules. Individual invitations are not required. Invited/allowlisted
122
+ signup remains available when you need to approve people individually.
123
+
124
+ With invited/allowlisted signup selected, let a teammate join with GitHub by adding their login to the allowlist:
52
125
 
53
126
  ```sh
54
127
  void platform signup allow github teammate
@@ -65,6 +138,14 @@ void platform signup allow email '*@example.com'
65
138
 
66
139
  Email patterns apply across the platform's sign-in providers. Quote a domain pattern so your shell passes the `*` to Void.
67
140
 
141
+ For an OIDC login without a verified email, allow the identity by its login connection and stable provider subject instead:
142
+
143
+ ```sh
144
+ void platform signup allow identity company-sso 'Employee-42'
145
+ ```
146
+
147
+ Use the connection ID shown by `void platform config auth list` and the exact subject reported by your identity provider. The match is case-sensitive and does not infer an email address or link another account. Any domain or group restrictions configured for that login method still apply, and the account joins as an ordinary user. Remove the grant with `void platform signup disallow identity company-sso 'Employee-42'`.
148
+
68
149
  To invite someone else by email, use:
69
150
 
70
151
  ```sh
@@ -83,7 +164,7 @@ void platform invitation list
83
164
  Invitation history remains available after an involved account is removed. The stored actor ID
84
165
  remains visible when that account's login no longer exists.
85
166
 
86
- `void platform signup open` allows anyone to sign up. Use `void platform signup restrict` to require an allowlist match again. Removing an allowlist entry affects future signup; it does not suspend an existing account.
167
+ `void platform signup open` allows anyone to sign up. Use `void platform signup restrict` to require an allowlist match again. Disallowing an entry affects future signup; it does not suspend an existing account.
87
168
 
88
169
  ## Managing Users and Projects
89
170
 
@@ -104,6 +185,19 @@ void platform project show <project-id>
104
185
 
105
186
  Project details include resources, domains, and recent builds and deployments. The [command reference](../reference/cli.md#operator-commands) also covers suspending and restoring users, deleting projects, and removing accounts.
106
187
 
188
+ ### Transferring Project Ownership
189
+
190
+ Only an installation administrator can change a project's owner. The new owner must already have an account on this platform. Preview the transfer before applying it:
191
+
192
+ ```sh
193
+ void platform project owner <project-id> <new-owner-user-id> --plan
194
+ void platform project owner <project-id> <new-owner-user-id> --yes
195
+ ```
196
+
197
+ The preview shows both owners, their plans and suspension state, active work that blocks the transfer, and the changes to routing and usage counters. Wait for or cancel any active deploy, rollback, or build before retrying.
198
+
199
+ After the transfer, the former owner becomes a project administrator. Existing project-scoped CI deploy credentials are revoked; create replacements as the new owner. The new owner's plan and limits apply immediately. Usage before the transfer remains charged to the former owner; later usage is charged to the new owner. If an apply reports partial convergence, inspect the project and operator event log before repeating it.
200
+
107
201
  New users on a self-hosted installation start with the `custom` profile, which
108
202
  does not cap application requests, AI usage, deployment frequency, or retained
109
203
  Worker deployments. Named profiles such as `pro` apply the platform's quota and
@@ -121,6 +215,122 @@ before changing application traffic or deleting resources. If cleanup fails,
121
215
  access stays revoked and the error describes the partial result. A remaining
122
216
  administrator can inspect it and retry cleanup.
123
217
 
218
+ ## Managing Email
219
+
220
+ These commands apply to a platform that enables email. Every project can send from and receive at `<slug>+tag@<mail domain>` on the platform's shared mail domain; the [email guide](./email.md) describes what applications do with that.
221
+
222
+ ### Deciding who mail may reach
223
+
224
+ By default, mail from the shared sender reaches only the recipients each project has verified. For a team whose applications mainly mail colleagues, widen that once for the whole installation:
225
+
226
+ ```sh
227
+ void platform email policy
228
+ void platform email policy-set domains --domains example.com,corp.example.net
229
+ void platform email policy-set any
230
+ void platform email policy-set verified
231
+ ```
232
+
233
+ `domains` lets every project mail any address on the listed domains in addition to its verified recipients; `any` lifts the check. Cloudflare still refuses a recipient it has not verified until your mail domain is onboarded for Email Sending, which needs Workers Paid on the platform's account. Until then such sends come back as `UNVERIFIED_DESTINATION` for that recipient, whatever the policy says. Two things the policy never changes: sends from a project's own custom domain, and mail to any address on the platform's own mail domain — those stay verified-recipient-only, so no project reaches another project's inbox without its consent. A tightening applies to new send admissions as soon as it commits; previously admitted attempts may finish. The same setting is on the admin UI's **Email** page.
234
+
235
+ ### Project caps
236
+
237
+ Each project has 200 recipient submissions per UTC calendar month and 10 in a rolling 60-second window by default. An attempt stays charged once it starts, including a failed or uncertain outcome. Raise or lower a project's caps by id or slug:
238
+
239
+ ```sh
240
+ void platform email limit hr-portal --monthly 2000 --burst 30
241
+ ```
242
+
243
+ The project's page in the admin UI shows the current values and clears an override.
244
+
245
+ ### Recovering an interrupted send
246
+
247
+ If a sender stops after beginning a provider call, its unresolved attempt blocks
248
+ removal of the project or sender domain. Inspect the attempt ID and start time:
249
+
250
+ ```sh
251
+ void platform email attempts --project hr-portal
252
+ ```
253
+
254
+ First confirm that the original execution has ended through your Worker logs or
255
+ incident records. Once the attempt is at least 24 hours old, resolve it with an
256
+ audit reason that contains no recipient address or message content:
257
+
258
+ ```sh
259
+ void platform email attempt-resolve <attempt-id> --ended --reason "Original Worker execution ended; provider outcome could not be verified" --plan
260
+ void platform email attempt-resolve <attempt-id> --ended --reason "Original Worker execution ended; provider outcome could not be verified"
261
+ ```
262
+
263
+ This records the send as `outcome_unknown` and releases its cleanup fence. The
264
+ attempt remains charged and is never retried. If its 30-day delivery record has
265
+ expired, only its recipient-free fence is removed. Do not resolve an attempt
266
+ while its original execution may still be active.
267
+
268
+ Inspect a project's retained receipt and recipient outcomes with:
269
+
270
+ ```sh
271
+ void platform email logs hr-portal --page 1 --limit 50 --json
272
+ ```
273
+
274
+ Logs include operation IDs, recorded outcomes, provider references, and error codes.
275
+ After project deletion, use its project ID until the 30-day metadata retention
276
+ period expires. Message bodies, subjects, attachments, and credentials are not logged.
277
+
278
+ ### Registering email domains for projects
279
+
280
+ Your users may hold no Cloudflare account. Tell the platform that administrators register email domains, so `void email domain add` prints the command to ask you for instead of opening a token-creation page:
281
+
282
+ ```sh
283
+ void platform email settings-set --domains admin
284
+ ```
285
+
286
+ Then register a domain for a project. The zone must be in the platform's Cloudflare account; the platform's own credential does the setup, and no Cloudflare credential is stored per domain:
287
+
288
+ ```sh
289
+ void platform email domain-add mail.example.com --project hr-portal
290
+ void platform email domain-status mail.example.com
291
+ void platform email domain-rotate-secret mail.example.com
292
+ void platform email domains
293
+ ```
294
+
295
+ Name the exact mail domain: a subdomain such as `mail.example.com` when the apex already receives mail, otherwise the apex itself. `domain-status` reports inbound, outbound, and management readiness separately. Follow any required DNS or Cloudflare dashboard step, then use `domain-sync` to reconcile. Use `domain-rotate-secret` when you need to replace the zone ingress credential explicitly. Email Sending onboarding needs Workers Paid; after upgrading, `domain-sync` re-attempts it. For a zone in another Cloudflare account, pipe an API token for that account on standard input:
296
+
297
+ ```sh
298
+ void platform email domain-add mail.other.example --project hr-portal --token-stdin --yes < token.txt
299
+ ```
300
+
301
+ Project owners see administrator-registered domains in `void email domain list` and `status`; `status` names the `void platform email` command for any step they cannot take themselves, and `sync` and `remove` point them at `domain-sync` and `domain-remove`. A domain an owner registered before you switched to `admin` stays theirs to renew, sync and remove. The runtime token needs the email permissions listed in the [self-hosting guide](./self-hosted-platform.md#runtime-token-permissions) for this to work.
302
+
303
+ A permission refusal leaves setup blocked. After correcting it, run `domain-sync`
304
+ to resume that same setup operation; repeat `domain-rotate-secret` to resume a
305
+ blocked rotation without replacing its staged secret. If project deletion leaves
306
+ a blocked domain cleanup, correct the permission and run `domain-remove <domain>`
307
+ to finish the retained cleanup. If its token has expired or been revoked, provide
308
+ a replacement scoped to the same account and zone:
309
+
310
+ ```sh
311
+ void platform email domain-remove mail.other.example --token-stdin --yes < token.txt
312
+ ```
313
+
314
+ This recovery is available only after project deletion has begun and no other
315
+ project uses the connection. For a live project, renew its token through
316
+ `domain-add`. A timed-out Cloudflare
317
+ mutation stays stopped because replay may duplicate a provider write. After
318
+ confirming the original execution ended, wait 24 hours, inspect the exact
319
+ routing, Worker, secret, catch-all, or Sending resource named by the preview,
320
+ and use its recovery fence:
321
+
322
+ ```sh
323
+ void platform email operation-resolve <operation-id> --ended --outcome <applied|not-applied> --reason "Provider state verified" --plan
324
+ void platform email operation-resolve <operation-id> --ended --outcome <applied|not-applied> --reason "Provider state verified"
325
+ ```
326
+
327
+ Run recovery with the same platform version that created the persisted intent.
328
+ `applied` continues without replaying the write; `not-applied` permits that exact
329
+ step to retry. Secret rotation keeps its staged generation, and removal resumes
330
+ from the unresolved cleanup step. Age alone never authorizes a retry.
331
+
332
+ Register administrator domains only after a platform upgrade has completed, and remove them before rolling the platform back to a version that predates this feature: an earlier runtime cannot use the platform credential for them and does not distinguish administrator-registered domains from owner-registered ones.
333
+
124
334
  ## Previewing Changes
125
335
 
126
336
  Commands that change the platform show the affected objects before asking for confirmation. To inspect a change without applying it, add `--plan`:
@@ -210,3 +420,5 @@ unset VOID_OPERATOR_TOKEN
210
420
  Disable shell tracing for the exchange and do not write either token to logs or plaintext files. A full API login session expires after 30 days and can be revoked sooner; renew it through the normal authenticated login flow and update the protected CI secret. A job that runs longer than one hour must repeat the exchange while its full API session is still valid. An expired operator token cannot refresh itself, and Void does not issue permanent service tokens for administrator automation.
211
421
 
212
422
  `auth login --token-stdin` performs the same elevation and saves the one-hour operator token in the system keychain for interactive use. See [operator authentication](../reference/cli.md#operator-authentication) for the full command syntax.
423
+
424
+ An email zone has one connection and ingress Worker shared by its exact domain assignments. Removing one assignment preserves resources used by the others. The Email administration page can explicitly rotate that connection secret; ordinary synchronization does not rotate it. Setup and cleanup outcomes include operation IDs, and uncertain provider writes remain recorded until reconciled.
@@ -83,7 +83,18 @@ CF_ACCESS_APP_URL=https://platform.example.com
83
83
  SITE_DOMAIN=apps.example.com
84
84
  ```
85
85
 
86
- The helper refreshes an Access session before the dev server starts. A configured `CF_ACCESS_CLIENT_ID` and `CF_ACCESS_CLIENT_SECRET` pair can be used for automation instead. This is dashboard development configuration; Access credentials are separate from platform login credentials.
86
+ The helper refreshes an Access session before the dev server starts. Human
87
+ dashboard requests require the user's Access session as well as their Void login;
88
+ a service token does not represent that user. Service-token pairs are for scoped
89
+ machine operations. This is dashboard development configuration; Access
90
+ credentials are separate from platform login credentials.
91
+
92
+ For a deployed dashboard, bind its `API` service to your platform API, configure
93
+ `DASHBOARD_URL` on both the dashboard and API, and include that exact dashboard origin in the
94
+ platform's Access protection application. The dashboard passes the browser's
95
+ company identity to the API using that service binding. Its login page shows
96
+ the platform's currently enabled methods, and **Account** supports adding an
97
+ additional login identity.
87
98
 
88
99
  ## Testing Changes
89
100
 
@@ -144,6 +155,57 @@ void-dev platform upgrade <installation-id> \
144
155
 
145
156
  Then run it without `--plan` to apply the upgrade. Source-built runtimes go through the same artifact, database, ownership, and health checks as released runtimes. The CLI preserves a disabled installation's state and records the source revision in its installation checkpoint.
146
157
 
158
+ ## Optional GitHub Webhook Ingress for Access-Protected APIs
159
+
160
+ The core installer does not deploy the dashboard, GitHub App, build Containers,
161
+ or webhook ingress. If a source-built installation adds those optional services
162
+ and Cloudflare Access protects its API hostname, GitHub cannot deliver directly
163
+ to `/webhooks/github`: GitHub does not present your Access credentials. Do not add
164
+ an Everyone or bypass policy to the API application.
165
+
166
+ The API source package includes an optional, path-isolated Worker for this case.
167
+ It accepts only `POST /github`, validates GitHub's signature over the raw body,
168
+ and forwards one authenticated internal operation over an API service binding.
169
+ The API independently verifies both that internal proof and GitHub's signature
170
+ before running the normal webhook handler. Installations without perimeter
171
+ protection can continue using the API's direct `/webhooks/github` endpoint.
172
+
173
+ To deploy the optional ingress:
174
+
175
+ 1. Deploy the source API/build runtime containing the internal operation and its
176
+ build bindings, then finish the separate GitHub App/build-service
177
+ configuration. A core install or upgrade alone does not add managed builds.
178
+ Enable the platform's managed-build capability only when that infrastructure
179
+ is ready.
180
+ 2. Edit `platform/packages/api/wrangler.github-webhook-ingress.jsonc`. Give the
181
+ ingress a name unique to the installation and set its `API` service binding to
182
+ the exact installed API Worker name. Keep its public hostname separate from
183
+ every human/API hostname covered by Access.
184
+ 3. Deploy it from the repository root:
185
+
186
+ ```sh
187
+ vp run --filter @voidcloud/api deploy:github-webhook-ingress --env production
188
+ ```
189
+
190
+ Use `--env staging` for the staging entries in the same config.
191
+
192
+ 4. In the Cloudflare dashboard, add encrypted Worker secrets. Set the GitHub
193
+ App's existing `GITHUB_WEBHOOK_SECRET` on both the API and ingress Workers.
194
+ Generate a separate high-entropy value, such as `openssl rand -base64 32`,
195
+ and set it as `GITHUB_WEBHOOK_INGRESS_SECRET` on both Workers. Do not reuse a
196
+ platform management, JWT, Access, or GitHub webhook credential for that value.
197
+ 5. In the GitHub App settings, keep **Content type** set to `application/json`,
198
+ keep the same webhook secret, and change **Webhook URL** to the isolated
199
+ ingress URL ending in `/github`. Use GitHub's test delivery and confirm a 2xx
200
+ response before relying on push builds.
201
+
202
+ The ingress has no login, dashboard, project, operator, proxy, or arbitrary
203
+ forwarding route. It does not make the GitHub integration part of the core
204
+ installer, provision build executors, create a GitHub App, configure Cloudflare
205
+ Access, or manage either Worker's secrets. Its body limit is 25 MiB, based on
206
+ GitHub's [documented 25 MB webhook payload cap](https://docs.github.com/en/webhooks/webhook-events-and-payloads#payload-cap);
207
+ malformed or larger deliveries are rejected before event processing.
208
+
147
209
  ## Deploying Source Builds from CI {#source-build-ci}
148
210
 
149
211
  Build `@void/platform` from your checkout and pass its runtime directory to `install`, `upgrade`, `repair`, `enable`, or `rollback` with `--runtime`. Custom runtimes get the same integrity, migration, health, and rollback checks as packaged releases.
@@ -169,7 +231,7 @@ node packages/void/dist/cli/cli.mjs platform upgrade "$VOID_PLATFORM_INSTALLATIO
169
231
 
170
232
  Omit the installation selector only if the account has one discoverable installation. Run one deployment per installation at a time, and let it finish before starting the next. Cancelling during migrations or Worker rollout can leave an installation waiting for recovery.
171
233
 
172
- Keep the management token, database URL, JWT signing secret, and complete project-encryption keyring in a protected CI environment. Upgrades inherit deployed Worker secrets. The original values are needed when recreating a missing Worker; configuration credentials are also needed when explicitly rotating them.
234
+ Keep the management token, database URL, JWT signing secret, email signing secret for email-enabled installations, and complete project-encryption keyring in a protected CI environment. Upgrades inherit deployed Worker secrets. The original values are needed when recreating a missing Worker; configuration credentials are also needed when explicitly rotating them.
173
235
 
174
236
  :::
175
237
 
@@ -232,4 +294,33 @@ Publishing requires both SDK CI and platform CI, including the platform unit and
232
294
  API integration suites. Release tags also run the Windows SDK checks; a passing
233
295
  SDK-only build cannot publish a changed control plane.
234
296
 
297
+ ### Retrying a Release
298
+
299
+ To retry a failed release without moving an existing tag, add `+retry.N` to a
300
+ new Git tag, with `N` starting at `1`. Keep the package versions unchanged:
301
+
302
+ | Git tag | Package version | npm channel |
303
+ | ------------------------ | --------------- | ----------- |
304
+ | `v0.21.0` | `0.21.0` | `latest` |
305
+ | `v0.21.0+retry.1` | `0.21.0` | `latest` |
306
+ | `v0.21.0-beta.1+retry.2` | `0.21.0-beta.1` | `beta` |
307
+
308
+ For example, when the packages are at `0.21.0`, commit the release fix and tag
309
+ that commit:
310
+
311
+ ```sh
312
+ git tag -a 'v0.21.0+retry.1' -m 'Retry 0.21.0 publication.'
313
+ git push origin 'refs/tags/v0.21.0+retry.1'
314
+ ```
315
+
316
+ The retry suffix belongs only in the Git tag, not in `package.json`. A `-1`
317
+ suffix is a distinct prerelease version, not a retry. Tag and package versions
318
+ are checked before dependency installation and the full CI jobs; retries still
319
+ run the normal release checks.
320
+
321
+ Retries publish only package versions that are still missing from npm. They
322
+ cannot replace an already-published version. If an earlier attempt partially
323
+ published the release and you changed its package contents, bump the version
324
+ instead of combining different contents under the same version.
325
+
235
326
  For implementation history, use the design archive at `platform/meta/design-docs/README.md`. Its proposals explain earlier decisions; the source and current guides define the supported behavior.
@@ -0,0 +1,94 @@
1
+ ---
2
+ outline: deep
3
+ ---
4
+
5
+ # Project Collaboration
6
+
7
+ A project hosted on a Void platform can have one owner and additional readers, collaborators, and project administrators. Team access is a platform feature; it is not available for projects deployed directly to Cloudflare.
8
+
9
+ ## Roles
10
+
11
+ | Role | Access |
12
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
13
+ | Reader | View the project, usage, resources, deployments, builds, logs, and member roster. Cannot deploy. |
14
+ | Collaborator | Reader access plus deploy, rollback, cancellation, migrations, secrets, database configuration, and deploy prerequisites. |
15
+ | Project administrator | Collaborator access plus domains, email destinations, GitHub configuration, invitations, roles, and member removal. |
16
+ | Owner | Project administrator access plus project deletion. |
17
+
18
+ Only the owner can create, list, renew, or revoke the project's CI deploy
19
+ credentials. Those credentials run unattended deployments as the billing owner
20
+ and remain independent of a member's login session, so collaborators and
21
+ project administrators deploy with their own account instead of minting a
22
+ durable owner credential.
23
+
24
+ The owner's plan, limits, and suspension state govern the project. A member's
25
+ own plan does not change the project's available usage, and acting through a
26
+ collaborator does not bypass an owner suspension. A project administrator is
27
+ not an installation administrator and receives no platform-wide access.
28
+
29
+ ## Invite a Registered User
30
+
31
+ An owner or project administrator can invite someone by the email address registered to their existing account on the same platform:
32
+
33
+ ```sh
34
+ void project team invite teammate@example.com --role collaborator
35
+ ```
36
+
37
+ Pass `--project <slug>` to manage a project other than the linked project. An unknown email is rejected. A project invitation neither creates an account nor adds the address to a restricted-signup allowlist.
38
+
39
+ Invitations expire after seven days. If the platform cannot send invitation
40
+ email, the CLI prints the invitation ID and connection and acceptance commands
41
+ to share with the invited user. The invitation also appears when that user runs
42
+ `void project team pending` on the same platform.
43
+
44
+ List the roster and sent invitations with:
45
+
46
+ ```sh
47
+ void project team list
48
+ void project team invitations
49
+ ```
50
+
51
+ ## Respond to an Invitation
52
+
53
+ Connect to the platform from your invitation, then see invitations addressed to
54
+ your account and respond using the invitation ID:
55
+
56
+ ```sh
57
+ void connect https://api.example.com
58
+ void project team pending
59
+ void project team accept <invitation-id>
60
+ # or
61
+ void project team decline <invitation-id>
62
+ ```
63
+
64
+ An invitation is bound to the registered account. Forwarding its ID does not let another user accept it.
65
+
66
+ These three commands use the active platform connection selected by
67
+ `void connect`, even inside a directory linked to another project or deployed
68
+ directly to Cloudflare. `VOID_API_URL` takes precedence when set. If you use
69
+ `VOID_TOKEN`, set `VOID_API_URL` to the matching platform; a token without that
70
+ URL selects Void Cloud. Each command shows the platform it contacts.
71
+
72
+ Accepting an invitation grants access without linking your current directory.
73
+ Open the invited application's directory and run `void project link` if it is
74
+ not linked yet. If it is already linked to another project, use a separate
75
+ checkout without `.void/project.json`, connect to the invited platform there,
76
+ and run `void project link`.
77
+
78
+ If the invitation is missing, check the platform and signed-in account. To
79
+ switch accounts, set `VOID_API_URL` to the invitation's platform URL, unset
80
+ `VOID_TOKEN` if present, then run `void auth logout` and `void auth login` using
81
+ the invited email address. For an expired or revoked invitation, ask a project
82
+ administrator to invite you again.
83
+
84
+ ## Change or Remove Access
85
+
86
+ Owners and project administrators can change any non-owner member or revoke a pending invitation:
87
+
88
+ ```sh
89
+ void project team role <user-id> reader
90
+ void project team remove <user-id>
91
+ void project team revoke <invitation-id>
92
+ ```
93
+
94
+ A non-owner member can leave with `void project team leave`. The owner cannot leave; an installation administrator must [transfer ownership](./platform-administration.md#transferring-project-ownership) first.