alpha-gate 0.1.0

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 (140) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +138 -0
  4. package/bin/alpha-gate.mjs +75 -0
  5. package/deploy/backup.sh +45 -0
  6. package/deploy/deploy.sh +21 -0
  7. package/deploy/dev.sh +56 -0
  8. package/deploy/lib/statedir.sh +11 -0
  9. package/deploy/teardown.sh +17 -0
  10. package/docs/ONBOARDING.md +16 -0
  11. package/docs/PRINCIPLES.md +195 -0
  12. package/docs/README.md +54 -0
  13. package/docs/UPLOADING.md +14 -0
  14. package/docs/integrate/activation.md +56 -0
  15. package/docs/integrate/sparkle-go.md +106 -0
  16. package/docs/integrate/sparkle-swift.md +63 -0
  17. package/docs/maintain/backup.md +52 -0
  18. package/docs/maintain/migrate-account.md +95 -0
  19. package/docs/maintain/teardown.md +46 -0
  20. package/docs/maintain/troubleshooting.md +102 -0
  21. package/docs/maintain/updating.md +92 -0
  22. package/docs/operate/add-users.md +55 -0
  23. package/docs/operate/channels.md +54 -0
  24. package/docs/operate/email.md +51 -0
  25. package/docs/operate/monitoring.md +56 -0
  26. package/docs/operate/publish.md +90 -0
  27. package/docs/operate/remove-users.md +47 -0
  28. package/docs/setup/cloudflare-account.md +41 -0
  29. package/docs/setup/deploy.md +84 -0
  30. package/docs/setup/install.md +65 -0
  31. package/migrations/0001_clients.sql +12 -0
  32. package/migrations/0002_builds_streams.sql +30 -0
  33. package/migrations/0003_access_log.sql +14 -0
  34. package/migrations/0004_meta.sql +5 -0
  35. package/migrations/0005_admin_audit.sql +14 -0
  36. package/migrations/0006_build_dmg.sql +6 -0
  37. package/migrations/0007_access_requests.sql +12 -0
  38. package/migrations/0008_build_rollback_target.sql +7 -0
  39. package/migrations/0009_hidden.sql +6 -0
  40. package/migrations/0010_purged_at.sql +5 -0
  41. package/package.json +65 -0
  42. package/publish.sh +302 -0
  43. package/release.json +7 -0
  44. package/src/auth/access-jwt.ts +210 -0
  45. package/src/auth/token-gate.ts +27 -0
  46. package/src/core/appcast.ts +107 -0
  47. package/src/core/audit-chain.ts +145 -0
  48. package/src/core/invite-template.ts +127 -0
  49. package/src/core/no-build.ts +160 -0
  50. package/src/core/resolver.ts +60 -0
  51. package/src/core/tokens.ts +51 -0
  52. package/src/core/types.ts +76 -0
  53. package/src/core/validation.ts +60 -0
  54. package/src/core/verdict.ts +195 -0
  55. package/src/core/version.ts +113 -0
  56. package/src/cron.ts +37 -0
  57. package/src/db/access-log.ts +168 -0
  58. package/src/db/access-requests.ts +90 -0
  59. package/src/db/admin-audit.ts +101 -0
  60. package/src/db/builds.ts +169 -0
  61. package/src/db/client.ts +80 -0
  62. package/src/db/clients.ts +103 -0
  63. package/src/db/meta.ts +30 -0
  64. package/src/db/streams.ts +85 -0
  65. package/src/deploy/cli.ts +181 -0
  66. package/src/deploy/commands/deploy.ts +392 -0
  67. package/src/deploy/commands/dev.ts +190 -0
  68. package/src/deploy/commands/teardown.ts +159 -0
  69. package/src/deploy/core/args.ts +205 -0
  70. package/src/deploy/core/colors.ts +49 -0
  71. package/src/deploy/core/config.ts +65 -0
  72. package/src/deploy/core/parse.ts +51 -0
  73. package/src/deploy/core/paths.ts +27 -0
  74. package/src/deploy/core/plan.ts +138 -0
  75. package/src/deploy/core/result.ts +13 -0
  76. package/src/deploy/core/state.ts +64 -0
  77. package/src/deploy/core/table.ts +49 -0
  78. package/src/deploy/core/types.ts +39 -0
  79. package/src/deploy/core/ui.ts +107 -0
  80. package/src/deploy/seams/clock.ts +10 -0
  81. package/src/deploy/seams/files.ts +52 -0
  82. package/src/deploy/seams/io.ts +56 -0
  83. package/src/deploy/seams/wrangler.ts +100 -0
  84. package/src/deploy/ui-preview.ts +112 -0
  85. package/src/deps.ts +41 -0
  86. package/src/dev/admin-entry.ts +104 -0
  87. package/src/env.ts +47 -0
  88. package/src/lib/clock.ts +17 -0
  89. package/src/lib/hosts.ts +27 -0
  90. package/src/lib/text.ts +6 -0
  91. package/src/r2/builds-bucket.ts +50 -0
  92. package/src/r2/keys.ts +27 -0
  93. package/src/routes/admin/admin-context.ts +9 -0
  94. package/src/routes/admin/audit-fields.ts +22 -0
  95. package/src/routes/admin/branding.tsx +161 -0
  96. package/src/routes/admin/builds.tsx +388 -0
  97. package/src/routes/admin/clients.tsx +396 -0
  98. package/src/routes/admin/confirm.tsx +80 -0
  99. package/src/routes/admin/flash.ts +72 -0
  100. package/src/routes/admin/form.ts +43 -0
  101. package/src/routes/admin/index.ts +146 -0
  102. package/src/routes/admin/invite.ts +57 -0
  103. package/src/routes/admin/middleware.ts +52 -0
  104. package/src/routes/admin/negotiate.ts +13 -0
  105. package/src/routes/admin/pending.tsx +73 -0
  106. package/src/routes/admin/read-model.ts +518 -0
  107. package/src/routes/admin/streams.tsx +182 -0
  108. package/src/routes/admin/theme.ts +34 -0
  109. package/src/routes/admin/upload.tsx +320 -0
  110. package/src/routes/admin/views.tsx +293 -0
  111. package/src/routes/app/access.tsx +38 -0
  112. package/src/routes/app/app-context.ts +8 -0
  113. package/src/routes/app/appcast.ts +68 -0
  114. package/src/routes/app/assets.ts +25 -0
  115. package/src/routes/app/download.ts +45 -0
  116. package/src/routes/app/get.tsx +35 -0
  117. package/src/routes/app/index.ts +31 -0
  118. package/src/routes/app/resolve.ts +16 -0
  119. package/src/services/anchor.ts +54 -0
  120. package/src/services/audit.ts +19 -0
  121. package/src/services/branding.ts +51 -0
  122. package/src/services/email-cloudflare.ts +80 -0
  123. package/src/services/email.ts +68 -0
  124. package/src/services/self-update.ts +62 -0
  125. package/src/views/access-page.tsx +39 -0
  126. package/src/views/admin/ci-page.tsx +76 -0
  127. package/src/views/admin/combobox.tsx +191 -0
  128. package/src/views/admin/forms.tsx +37 -0
  129. package/src/views/admin/layout.tsx +496 -0
  130. package/src/views/admin/manage-pages.tsx +223 -0
  131. package/src/views/admin/manage.tsx +1120 -0
  132. package/src/views/admin/plist-extract.ts +233 -0
  133. package/src/views/admin/read-pages.tsx +1098 -0
  134. package/src/views/admin/setup-page.tsx +108 -0
  135. package/src/views/admin/table-enhance.ts +176 -0
  136. package/src/views/admin/ui.tsx +159 -0
  137. package/src/views/get-page.tsx +39 -0
  138. package/src/views/layout.tsx +57 -0
  139. package/src/worker.ts +23 -0
  140. package/tsconfig.json +35 -0
@@ -0,0 +1,92 @@
1
+ # Updating Alpha Gate
2
+
3
+ How to keep the tool itself up to date on your deployed instances.
4
+
5
+ ## How you learn about a release
6
+
7
+ The deployed admin Worker runs a daily cron (12:00 UTC) that polls the npm registry for this
8
+ package's latest version (`registry.npmjs.org/<name>/latest`) and compares it against its baked-in
9
+ `TOOL_VERSION` — the version of the package that deployed it. When a newer version exists:
10
+
11
+ - the admin dashboard shows an attention item: *Alpha Gate \<version> is available*, with a
12
+ release notes link;
13
+ - the Settings page's **Self-update** row shows a `<version> available` tag; when the instance is
14
+ current it reads *up to date* with a last-checked time instead. A fresh deploy shows
15
+ *not checked yet* until the cron first fires, within 24 hours.
16
+
17
+ If email is configured, the instance also sends one notice per new version to the `--email-from` address; route that mailbox to yourself to receive it.
18
+
19
+ **The Worker only notifies — it never deploys itself.** Self-deployment would require a privileged
20
+ Cloudflare API token on the edge, which the design forbids. Updating is always an operator-run
21
+ `deploy`, so migrations happen with a human in the loop.
22
+
23
+ ## Update from npm
24
+
25
+ ```bash
26
+ npx alpha-gate@latest deploy --instance <slug>
27
+ ```
28
+
29
+ `@latest` fetches the newest published version; the rest is an ordinary re-deploy. To pin a
30
+ specific version, name it: `npx alpha-gate@0.1.0 deploy --instance <slug>`.
31
+
32
+ ## Update from a clone
33
+
34
+ ```bash
35
+ git pull
36
+ ./deploy/deploy.sh --instance <slug>
37
+ ```
38
+
39
+ Re-running `deploy.sh` updates the instance in place. The clone runs whatever `main` you pulled,
40
+ which may be ahead of the npm release.
41
+
42
+ ## What a re-deploy preserves
43
+
44
+ A re-deploy is idempotent. It reuses the existing D1 database and R2 bucket, applies any pending
45
+ migrations, and redeploys both Workers. Preserved across updates:
46
+
47
+ - all data: users and their tokens, builds, channels, logs, the audit chain;
48
+ - your email settings and Access secrets — they are remembered in the deploy state directory of the channel you deployed with (`.deploy/` in a clone, `~/.alpha-gate` for npm), so a bare re-run through that same channel keeps them. An update through the other channel finds no remembered flags and quietly reverts invites to copy-paste; pass `--email-provider`/`--email-from` (or `--access-team-domain`/`--access-aud`) again when you switch channels or want to change them. Pass
49
+ `--email-provider`/`--email-from` or `--access-team-domain`/`--access-aud` again only to change
50
+ them.
51
+
52
+ Nothing about a routine update requires a backup, but taking one first is cheap — see
53
+ [Backup](backup.md).
54
+
55
+ ## Breaking releases
56
+
57
+ When an upgrade needs manual steps, the release is marked breaking. The dashboard banner adds
58
+ *Includes breaking changes.* and the Settings tag reads `<version> available (breaking)`.
59
+
60
+ **When the banner says breaking, read the release notes before you run deploy.** The banner's
61
+ *Release notes* link points at the changelog; it lists the manual steps. Take a
62
+ [backup](backup.md) before a breaking update.
63
+
64
+ ## Forks
65
+
66
+ A fork should not track upstream's npm package. Point the update check at your own manifest by
67
+ setting `UPDATE_MANIFEST_URL` when you deploy — the URL is baked into the Worker at deploy time and
68
+ is read from the environment on every deploy run, so set it each time (or export it in your shell):
69
+
70
+ ```bash
71
+ UPDATE_MANIFEST_URL=https://registry.npmjs.org/<your-package>/latest \
72
+ ./deploy/deploy.sh --instance <slug>
73
+ ```
74
+
75
+ The URL can be your own package's `/latest` endpoint, or any static JSON in the shape of the
76
+ repo's `release.json`:
77
+
78
+ ```json
79
+ {
80
+ "latest": "0.1.0",
81
+ "min_supported": "0.1.0",
82
+ "notes_url": "https://<your-host>/CHANGELOG.md",
83
+ "breaking": false
84
+ }
85
+ ```
86
+
87
+ Keep `latest` in sync with your `package.json` version on every release; set `breaking: true` when
88
+ an upgrade needs manual steps. Until the manifest URL resolves, the check fails quietly and the
89
+ banner stays silent. Settings still shows a last-checked time — the timestamp records that the
90
+ check ran, not that it found anything.
91
+
92
+ Next: [Teardown](teardown.md)
@@ -0,0 +1,55 @@
1
+ # Adding users
2
+
3
+ How to create a user in the back office and get their private download link to them.
4
+
5
+ ## Add a user
6
+
7
+ Open the **Users** page. The *Add user* form takes three fields:
8
+
9
+ - Email — required.
10
+ - Label — optional, a short note shown next to the email in the user list (e.g. "design partner").
11
+ - Channel — optional, the channel the user is assigned to on creation. The default is "— none —".
12
+
13
+ **A user with no channel receives no updates until you assign one.** The form warns about this, and the Overview page lists such users as routed nowhere. Leave the channel empty only if you mean to.
14
+
15
+ Press *Create invite*.
16
+
17
+ ## The invite page
18
+
19
+ Creating the user lands you on a page titled "Invite ready". It shows the private link and, when the invite was not emailed, a ready-to-send message:
20
+
21
+ - *Private link* — the user's `https://<app-host>/get?token=<token>` URL, with a one-click *Copy link* button.
22
+ - *Message to send* — your invite template filled in with the link, with a *Copy message* button. Edit the template under Settings in the *Invite email template* fieldset; it supports `{app_name}`, `{get_url}`, and `{token}`, and feeds both the copy-paste message and real email.
23
+
24
+ The link is durable while the token is active: the user revisits it to re-download or re-activate. You can retrieve it any time from the *Invite link* section of the user's page — viewing it never changes the token. Replacing the link is a separate, explicit action (*Reissue* on the user's page).
25
+
26
+ On a deployed workers.dev instance the link carries the public host. On local dev or a custom domain the admin cannot derive the public host, and On a deployed workers.dev instance the link carries the public host. On local dev or a custom domain the admin cannot derive the public host, so the link carries the admin host instead — a user cannot open that one. The *Invite link* section of the user's page warns when this is the case; the invite page shows no warning, so check the host before sending. — a user cannot open that one.
27
+
28
+ ## What the user sees
29
+
30
+ The link opens the branded download page (`/get`) on the public host. It carries three things, in order:
31
+
32
+ 1. A *Download* button for the build their channel currently serves.
33
+ 2. An *Activate* button — a deep link of the form `<scheme>://activate?token=…` that hands the token to the installed app. The scheme comes from the *Activate URL scheme* setting and must match the URL scheme your app registers.
34
+ 3. The access key — the raw token, with the instruction to paste it in the app if Activate doesn't open it.
35
+
36
+ Below that, the page lists the steps: download and install the app, launch it, click Activate (or paste the key) to connect updates. Wiring the app side of this is covered in [Activation](../integrate/activation.md).
37
+
38
+ ## Access requests
39
+
40
+ The public host also serves a request page at `/access`: an email field and a *Request access* button. The page is public — any visitor can submit an email address; nothing is granted automatically.
41
+
42
+ Submissions appear under **Requests** in the admin nav — a count chip shows when any are waiting, and the Overview lists them under *Needs attention*. Each request offers two actions:
43
+
44
+ - *Invite* creates the user and shows the same invite page as *Add user*. If the email belongs to an existing user, Invite issues a fresh token — the old link and any activated install stop working until re-activated — and reactivates them if they were revoked. There is no confirmation step, unlike Reissue on the user's page.
45
+ - *Dismiss* clears the request without creating a user.
46
+
47
+ A user created from a request has no channel — open their page and assign one. Duplicate requests from the same email are grouped ("asked N times"); the requester sees a confirmation page but receives no email, so repeats are normal. Inviting or dismissing resolves every request from that email at once.
48
+
49
+ ## Sending the invite
50
+
51
+ Copy-paste is the default and needs no setup: copy the link or the filled message from the invite page and send it however you already talk to the user — chat, email, anything..
52
+
53
+ With email delivery configured, adding a user or inviting a request sends the invite automatically, and the invite page confirms it was emailed — the link shown is the same one they received. A failed send never blocks user creation; the page shows the failure reason and falls back to the copy-paste link. See [Email](email.md) to set it up.
54
+
55
+ Next: [Publish a build](publish.md)
@@ -0,0 +1,54 @@
1
+ # Channels
2
+
3
+ Release channels group users and builds: this page covers creating a channel, linking builds, assigning users, pinning a user to one build, and the guard against leaving any user with no servable build.
4
+
5
+ ## The model
6
+
7
+ A channel serves its **highest available linked build** to every user assigned to it. Link order does not matter: the linked build with the largest build number that is not withdrawn is what the channel offers.
8
+
9
+ Users and builds can be on several channels at once. A user on more than one channel is offered the highest available build across all of them — the user page's Channels section says so next to the list.
10
+
11
+ Two states serve nothing. A build linked to no channel is offered to no one — unless a user is pinned to it, since a pin ignores channels. A user assigned to no channel (and not pinned) gets an empty feed, and their app reports up to date; the back office surfaces both: a "no channel" warning on the user, and "In no channel — offered to no one" on the build.
12
+
13
+ ## Create a channel
14
+
15
+ On the Channels page, the Add channel section takes a name (`stable` and `beta` are typical) and the Create channel button creates it. Names are unique; a duplicate is rejected with a clear error.
16
+
17
+ The Channels list shows what each channel is serving, with a **"serving nothing"** warning when no available build is linked.
18
+
19
+ ## Link builds
20
+
21
+ Open the channel. The "Builds in this channel" section lists linked builds, each with an Unlink button. Below it, the "Builds to link" picker is a type-to-filter multi-select (type a build number or version); Link builds links every selected one.
22
+
23
+ Linking is **additive** — it can only raise what the channel serves, so it never asks for confirmation. You can also put a build into a channel at publish time, with `--channel <name>` or the Upload page's Channel field; see [Publishing](publish.md).
24
+
25
+ ## Assign users
26
+
27
+ On the same page, the "Users in this channel" section lists assigned users with their next-check verdict, each with an Unassign button. The "Users to assign" picker (type an email) is also multi-select; Assign users adds every selected one. Assigning is additive and needs no confirmation.
28
+
29
+ You can also pick the first channel when creating a user — the Add user form has a Channel field. See [Add users](add-users.md).
30
+
31
+ ## Pinning
32
+
33
+ A pin holds one user to one exact build. It lives on the user's page, in the Pin section: pick a build in the "Build to pin" picker and press Pin to build. **A pin overrides channels entirely** — while pinned, the user's channels are ignored, and Unpin restores normal channel flow.
34
+
35
+ Sparkle cannot downgrade, which shapes two cases:
36
+
37
+ - A pin below the build the user runs does not take effect through the updater. The feed still offers the pinned item; Sparkle discards anything lower than the installed version and reports up to date. A fresh install through the user's download link does serve the pinned build.
38
+ - A pin to a withdrawn build serves nothing: the user gets an empty feed and does not fall back to their channels. The user page flags this ("pin serves nothing" on the next-check verdict, with "pinned build is withdrawn — unpin or re-pin") and the Pin section keeps the Unpin action.
39
+
40
+ ## The stranding guard
41
+
42
+ Any change that could leave a user with no servable build (withdrawing a build, unlinking it from a channel, unassigning a user, pinning, unpinning) is confirmed, not blocked. When a user would be stranded, you get a confirmation page first. It names the action in plain words and **lists exactly which users** would be left with no available build; their apps will report up to date and receive nothing until a higher build reaches them. Confirm to proceed, or Cancel to return to the page you acted from.
43
+
44
+ The remedy for a stranded user is one of: publish a higher build into their channel, assign a different channel, or adjust the pin.
45
+
46
+ ## Delete a channel
47
+
48
+ The Delete channel action sits in the danger zone at the bottom of the channel's page and is **always confirmed**, even when nobody is stranded. Deleting unassigns every user and unlinks every build; the users and builds themselves are kept. When the deletion would strand anyone, the confirmation page includes that list.
49
+
50
+ ## Rollback
51
+
52
+ Moving everyone off a bad build is not a channel operation. Sparkle cannot downgrade, so it is a roll-forward done at publish time: rebuild the previous good code with a higher build number, publish it into the affected channel, then withdraw the bad build. See [Rollback](publish.md#rollback).
53
+
54
+ Next: [Publishing](publish.md)
@@ -0,0 +1,51 @@
1
+ # Email
2
+
3
+ How invites reach your users: copy-paste links by default, automated email through Cloudflare Email Service as the paid option.
4
+
5
+ ## The default: copy-paste
6
+
7
+ Out of the box there is nothing to set up and **nothing leaves the Worker**. When you [add a user](add-users.md) or invite a request, the back office shows an invite page with two blocks, each with a one-click copy button:
8
+
9
+ - **Private link** — the user's `/get` URL, with a Copy link button. The link is durable while the token is active; the user can revisit it.
10
+ - **Message to send** — your invite template filled in for this user, with a Copy message button. Paste it into whatever you already use to reach them.
11
+
12
+ The message body comes from the "Invite email template" on the Settings page; the same template feeds automated email once you enable it. In this mode the Email row on Settings reads "copy-paste links (no email sent)".
13
+
14
+ ## What automated email requires
15
+
16
+ Automated delivery uses Cloudflare Email Service through a `send_email` binding on the admin Worker. It needs two things:
17
+
18
+ 1. The **Workers Paid** plan.
19
+ 2. A real, onboarded sending domain: in the Cloudflare dashboard, under Email → Email Routing, you add and verify SPF/DKIM DNS records for a zone you control.
20
+
21
+ A `*.workers.dev` hostname cannot be the sending domain — you do not control its DNS. An account with no domain of its own stays on copy-paste. The sending domain does not need to serve the Workers — the instance can stay on `*.workers.dev`; the domain only needs to be onboarded for email in the same account.
22
+
23
+ ## Enable it
24
+
25
+ Re-run deploy with the email flags:
26
+
27
+ ```bash
28
+ ./deploy/deploy.sh --instance <slug> --email-provider cloudflare --email-from alpha@<your-sending-domain>
29
+ # from npm:
30
+ npx alpha-gate deploy --instance <slug> --email-provider cloudflare --email-from alpha@<your-sending-domain>
31
+ ```
32
+
33
+ `--email-from` is required when `--email-provider` is `cloudflare`. The deploy adds the `EMAIL` send_email binding to the admin Worker and sets the From address. The flags are remembered: a later bare re-run (`--instance <slug>` alone) keeps them, so you pass them again only when the address or provider changes. One asymmetry: there is currently no flag that turns email back off — `--email-provider none` reads as "not passed" and the remembered provider wins..
34
+
35
+ After the deploy, reload Settings. The Email row should read "sending via Cloudflare" with your From address.
36
+
37
+ ## Test it
38
+
39
+ Once email is active, Settings shows a "Test email delivery" section. Enter a recipient (it defaults to you) and press **Send test email**. It sends one email immediately and shows the exact result, so you can debug delivery without creating a user. If the send fails, run `wrangler tail` on the admin Worker to see the full provider error.
40
+
41
+ ## When a send fails
42
+
43
+ A failed or misconfigured send **never blocks creating the user**. The user is written first; delivery is attempted after. If the email does not send, the invite page says so, names the reason, and still shows the link and message for you to send manually — the copy-paste path is always available.
44
+
45
+ Misconfiguration behaves the same way. If the provider is set to Cloudflare but the `EMAIL` binding or the From address is missing, invites fall back to copy-paste instead of erroring. Settings flags this state as "misconfigured — falling back to copy-paste" and lists exactly what is missing.
46
+
47
+ ## The state-directory caveat
48
+
49
+ The email flags live only in the local deploy state directory (`.deploy/` in a clone, `~/.alpha-gate` from npm), not in the database. If you lose that directory, a bare re-run of deploy regenerates everything else but quietly reverts invites to copy-paste. Pass `--email-provider cloudflare --email-from <address>` once more to restore delivery. The Email row on Settings is where you would notice the reversion.
50
+
51
+ Next: [Backup](../maintain/backup.md)
@@ -0,0 +1,56 @@
1
+ # Monitoring
2
+
3
+ This page explains how to read the back office: which build each user gets, what their apps have done, and whether the recorded history is intact.
4
+
5
+ ## The Overview
6
+
7
+ The **Serving now** map draws one row per channel: the channel, the build it serves (its highest available linked build), and the audience. The audience cell summarizes the users on that channel — `all up to date`, or counts like `2 will update · 1 pinned`, or `1 faulted` when something is wrong. A channel with users but no available build shows `serving nothing — no build linked` and its users get an `empty feed`. Below the channels sits the **off the map** row: users routed nowhere (no channel, no pin) whose tokens work but whose checks resolve to nothing. **The map is computed from the same data and rules the public Worker resolves with, refined by each user's last-reported installed build — so what you see here is the effective outcome of their next check.**
8
+
9
+ **Needs attention** lists every current fault as a cause in prose plus exactly one remedy link:
10
+
11
+ - a stranded user (installed above everything their channels offer) → `Roll forward →`
12
+ - a pin that serves nothing (withdrawn, or below the installed build) → `Review pin →`
13
+ - a channel serving nothing to its users → `Link a build →`
14
+ - users with no channel → `Assign a channel →` (or `Review users →` for several)
15
+ - access requests waiting → `Review requests →`
16
+ - an audit chain mismatch → `Inspect audit →`
17
+ - a newer Alpha Gate release → `Details →` (update and re-deploy the instance)
18
+
19
+ When nothing is wrong the section reads "Nothing needs attention — every active user is served."
20
+
21
+ **Recent** merges tester activity and your own actions into one feed, newest first: lines like "mira@studio.dev checked — on **#1499**" sit next to "you withdrew build 1500". Under the feed, the audit chain status line reports `audit chain intact · N entries` (or a mismatch warning); it is the same judgment the Audit page shows.
22
+
23
+ The page header carries the last-publish line: the newest build, the channels it went to (or "— in no channel"), and when. If that line does not match what you meant to ship, start there.
24
+
25
+ ## The Users list
26
+
27
+ Each row answers the question you would otherwise have to reconstruct: what does this user's next update check do. The **Next check** column states it with its cause:
28
+
29
+ | Next check shows | Meaning |
30
+ | --- | --- |
31
+ | gets `#1500 · v1.3.0` | Offered this build. A `pinned` tag means the pin decides; `critical` means Sparkle treats it as required. |
32
+ | up to date · `#1500` | The served build is the one they already run. |
33
+ | — not served while revoked | Revoked; checks receive a renewal notice until you reactivate. |
34
+ | `no channel` | Assigned to no channel — resolves to nothing. |
35
+ | `no build` | Their channels carry no available build. |
36
+ | `pin serves nothing` | The pinned build was withdrawn; unpin or re-pin. |
37
+ | `pin below installed` | The pin sits under their installed build; Sparkle cannot downgrade. |
38
+ | `stranded` | Installed above everything their channels offer; roll forward. |
39
+
40
+ **Installed** is the build number the user's app last reported. **Last seen** is their most recent event of any kind. Filter the list by status, channel, `needs attention`, `pinned`, or `show hidden`.
41
+
42
+ ## Activity
43
+
44
+ The Activity page lists every check, download, and update: when, which user, which event, which build. Filter by user (any part of the email), event, or build number; the page shows the latest 100 events, so narrow with the filters when you need older ones. Entries older than 90 days are pruned daily. **The Downloads and Updates counts on the Builds page are computed from this log, so they cover the last 90 days — they are not all-time totals.**
45
+
46
+ ## Audit
47
+
48
+ The Audit page lists every admin action: when, the actor, the action slug (for example `client.revoke`), the target, the request IP, and the Cloudflare Ray ID. Targets are emails and build numbers, not database row ids, so rows stay readable on their own. Filter by actor or action; the page shows the latest 200 actions.
49
+
50
+ Each row is hash-chained to the one before it, and a daily job anchors the chain head to an append-only object in R2 (and emails it when email is configured). The status line above the table reports the live verdict: `chain intact · N entries · anchored`, `not yet anchored` before the first anchor has run, or `CHAIN MISMATCH — the log diverged from its last anchor`. **A mismatch means rows were edited, removed, or rebuilt.** Compare the log against the anchored copies in R2 and your Cloudflare account audit logs, Compare the log against the anchored copies in R2, the emailed anchors, and your Cloudflare account audit logs. The admin interface cannot rewrite any of them; overwriting the R2 copies would take Cloudflare account access, and even that cannot reach the emailed anchors or the account audit logs..
51
+
52
+ ## Activity vs Audit
53
+
54
+ Activity records what your testers' apps did; Audit records what you did.
55
+
56
+ Next: [Troubleshooting](../maintain/troubleshooting.md)
@@ -0,0 +1,90 @@
1
+ # Publish builds
2
+
3
+ Ship a build with one command, from the browser, or from CI, and roll back a bad release.
4
+
5
+ ## The one command
6
+
7
+ ```bash
8
+ ./publish.sh MyApp.dmg --channel beta # from a clone
9
+ npx alpha-gate publish MyApp.dmg --channel beta # from npm, same flags
10
+ ```
11
+
12
+ The artifact is a `.dmg` or a signed `.app` `.zip`. Before you run it:
13
+
14
+ - The artifact is already built, code-signed with Developer ID, notarized, and stapled. `publish.sh` does not build or notarize.
15
+ - The app inside has an integer `CFBundleVersion` that increases every release. The server rejects non-integers; override a bad value with `--build-number <n>`.
16
+ - Sparkle's `sign_update` is findable. The script auto-discovers it under Xcode's DerivedData; otherwise pass `--sign-update <path>`.
17
+
18
+ What it does, in order:
19
+
20
+ 1. Reads `CFBundleShortVersionString`, `CFBundleVersion`, and `LSMinimumSystemVersion` from the app inside the artifact (mounting a DMG, or reading the plist out of a zip) and prints what it read.
21
+ 2. Pre-checks the build number and channel against the running instance, so a duplicate build number or a mistyped channel name fails in a second instead of after a multi-minute upload.
22
+ 3. Signs the artifact with `sign_update` to produce the Sparkle EdDSA signature.
23
+ 4. Uploads. Over 90 MB it switches to the register path on its own: it puts the bytes into R2 with your own `wrangler` auth, then registers the metadata. No extra token.
24
+
25
+ When exactly one instance is deployed, the script targets it automatically; otherwise pass `--instance <slug>` or `--admin-url <url>`. Channels are named on the command line (`--channel beta`, never a database id); the channel must already exist (see [Channels](channels.md)). **One signed DMG serves both first-install and updates** — the `/get` download and the Sparkle enclosure are the same artifact, signed once.
26
+
27
+ ## The service token, the first time
28
+
29
+ The admin Worker sits behind Cloudflare Access, so `publish.sh` authenticates with an Access service token. On the first publish to a real instance the script prints the steps and prompts for the credentials:
30
+
31
+ 1. Cloudflare Zero Trust → Access → Service Auth → "Create service token".
32
+ 2. On the admin Access application, add a policy (Action: Service Auth) allowing it.
33
+
34
+ It stores the Client ID and Secret in your login Keychain, keyed by instance; every later run reads them without prompting. Pass `--reset-token` to forget the stored pair and enter a new one. Publishing to a `localhost` dev admin needs no token.
35
+
36
+ ## Useful flags
37
+
38
+ | Flag | What it does |
39
+ |---|---|
40
+ | `--channel <name>` | link the build to a channel by name |
41
+ | `--instance <slug>` | pick the instance when more than one is deployed |
42
+ | `--admin-url <url>` | target an admin Worker directly (CI, or no local deploy state) |
43
+ | `--sign-update <path>` | where `sign_update` lives (or set `$SIGN_UPDATE`); set `ED_SIGNATURE=<sig>` to skip signing entirely |
44
+ | `--build-number <n>` | override the `CFBundleVersion` read from the app |
45
+ | `--short-version <s>` | override the `CFBundleShortVersionString` read from the app |
46
+ | `--min-os <version>` | override the `LSMinimumSystemVersion` read from the app |
47
+ | `--critical` | mark the build as a critical update |
48
+ | `--reset-token` | forget the stored service token and prompt again |
49
+ | `--dry-run` | read and validate the artifact, print what would be published, upload nothing |
50
+
51
+ ## Browser upload
52
+
53
+ The admin Upload page publishes without the script, up to the 90 MB upload ceiling; a larger archive takes the register path, which only the one command handles for you. Picking an archive autofills the version, build number, and minimum macOS from the app's `Info.plist`; the fields stay editable. **Autofill works only for a signed `.app` `.zip`** — a `.dmg` or `.tar` can't be read in the browser, so type those fields yourself.
54
+
55
+ The page has two modes: New release and Roll back. Roll back shows the current highest build number and enforces it as a floor: a build number at or below it is rejected.
56
+
57
+ ## CI
58
+
59
+ The same script runs headless on a macOS runner. Put the service token in the environment and pass `--admin-url`:
60
+
61
+ ```bash
62
+ export CF_ACCESS_CLIENT_ID=<client-id>
63
+ export CF_ACCESS_CLIENT_SECRET=<client-secret>
64
+ ./publish.sh dist/MyApp.dmg \
65
+ --admin-url https://alpha-gate-<slug>-admin.<account>.workers.dev --channel beta
66
+ ```
67
+
68
+ A runner without a readable app bundle (a bare zip) passes `--build-number` and `--short-version` and sets `ED_SIGNATURE` from its own `sign_update` step. `.github/workflows/publish.yml` in the repository is a template: the publish step is complete, the build step is a deliberate placeholder — replace it with your app's build, sign, and notarize, then enable the tag trigger.
69
+
70
+ ## Rollback
71
+
72
+ Sparkle cannot downgrade: an update below the installed version is never offered, so you cannot re-serve the old build as-is. Withdrawing a bad version is a roll-forward:
73
+
74
+ 1. Rebuild the previous good code with a **higher** `build_number` (keep the old `short_version`). Publish it into the affected channel — the Upload page's Roll back mode shows the current highest build as the floor and rejects anything at or below it.
75
+ 2. Open the bad build from the Builds list and withdraw it; the Withdraw action is in the danger zone on the build's page. Because the higher build exists, no one is stranded — installed apps move to the roll-forward build on their next check, and the serving map on the Overview page shows the switch immediately.
76
+
77
+ ## Verify end to end
78
+
79
+ 1. The build appears on the admin Builds page, with download and update counts.
80
+ 2. Invite yourself (see [Add users](add-users.md)), open your `/get?token=` link, install, activate, and let the app check for updates.
81
+ 3. The Activity log shows a `check` carrying your installed build, then a `download` or `update`. Activity older than 90 days is pruned, so the counts on builds cover the last 90 days, not all time.
82
+ 4. Ask the feed directly:
83
+
84
+ ```bash
85
+ curl "https://<app-host>/appcast?token=<TOKEN>" — leave `installed` off (or pass your real installed build number): a made-up value is recorded as that user's installed build in the admin until the app's next real check.
86
+ ```
87
+
88
+ The response should list an `<item>` for your build.
89
+
90
+ Next: [Monitoring](monitoring.md)
@@ -0,0 +1,47 @@
1
+ # Removing users
2
+
3
+ How to take access away, and how to give it back, using the actions on a user's page.
4
+
5
+ All four actions live in the **Access** section at the bottom of the user's page (Users → click the email). Every one records an audit entry.
6
+
7
+ ## Revoke
8
+
9
+ **Revoke access…** cuts the user off. What actually happens:
10
+
11
+ - Their private download link stops working immediately. Every download routes through the gate, so there is no cached or pre-signed URL that keeps serving.
12
+ - Their installed app stops receiving updates. On the next update check, Sparkle shows a reactivation notice instead of an update — an informational item, never an error. You can customize its title and message under Settings → "Access notice (revoked or unknown tokens)".
13
+
14
+ Revoke is always confirmed before it runs, and it is **reversible**: the token is kept, not deleted. The user's page shows a `revoked` tag and warns that the invite link is dead until you reactivate.
15
+
16
+ Revoking an already-revoked user does nothing and says so.
17
+
18
+ ## Reactivate
19
+
20
+ **Reactivate** is the inverse of revoke. The stored token becomes valid again, so the same invite link starts working and the installed app resumes receiving updates on its next check. Nothing needs to be re-sent — the link the user already has is the one that revives.
21
+
22
+ ## Reissue
23
+
24
+ **Reissue link…** rotates the token. Use it when a link leaked or was lost.
25
+
26
+ - The old link stops working immediately.
27
+ - The installed app's session dies with it — the app asks the user to re-activate.
28
+ - A new invite link is minted. It must reach the user: it is emailed when email delivery is configured, otherwise the back office shows the link and message for you to copy and send.
29
+
30
+ Reissue is always confirmed, because it kills a working setup even when nothing was wrong with it.
31
+
32
+ For a revoked user the button reads **Reactivate with a fresh link…** and does both at once: it restores access and mints a new link, while the old link and any installed app token stay dead. (Reissue alone on a revoked user would mint a link that does not work — the flow prevents that.)
33
+
34
+ ## Hide
35
+
36
+ **Hide from list** declutters the Users list. It does not touch access: a hidden user keeps downloading and updating exactly as before. The user's page shows a quiet `hidden` tag.
37
+
38
+ The Users list has a "show hidden" checkbox that brings hidden users back into view; when some are hidden, the list also links to them ("N hidden not shown — show them"). **Unhide from list** reverses it.
39
+
40
+ ## Which one when
41
+
42
+ - Pausing someone temporarily → **Revoke**.
43
+ - They came back → **Reactivate** (same link keeps working).
44
+ - Their link leaked or was lost → **Reissue**.
45
+ - They left for good → **Revoke**, then **Hide** to keep the list clean.
46
+
47
+ Next: [Channels](channels.md)
@@ -0,0 +1,41 @@
1
+ # Prepare a Cloudflare account
2
+
3
+ What to set up on Cloudflare before your first deploy, and what you can skip.
4
+
5
+ ## The free tier is enough
6
+
7
+ Alpha Gate runs on Workers, D1, and R2, all covered by the free tier. There is **nothing to pre-create**: `deploy.sh` provisions the D1 database and R2 bucket itself. Sign up at cloudflare.com and move on.
8
+
9
+ ## Authenticate wrangler once
10
+
11
+ The deploy CLI drives your account through wrangler. Grant it access from the machine you deploy from:
12
+
13
+ ```bash
14
+ npx wrangler login # opens a browser; grants this machine access to your account
15
+ ```
16
+
17
+ For CI, set `CLOUDFLARE_API_TOKEN` instead of an interactive login.
18
+
19
+ ## Use a dedicated account (recommended)
20
+
21
+ Whoever can open this account's Cloudflare dashboard can read the D1 database and R2 bucket
22
+ directly, and the database rows include every tester's access token — the secret behind their
23
+ `/get` link and update feed. **A dashboard user could therefore impersonate any tester.** Testers
24
+ themselves never touch the database; the tokens are generated server-side when you add a tester, and
25
+ a tester's app only talks to the public Worker. If other people share your Cloudflare account, put
26
+ Alpha Gate in its own account so dashboard access does not equal tester access.
27
+
28
+ ## Set up Zero Trust now
29
+
30
+ After deploy, you lock the admin behind Cloudflare Access, which lives in Cloudflare Zero Trust. The first time you open Zero Trust, Cloudflare makes you pick a **team name** and add a **payment method** before you can create Access applications — even on the free Zero Trust plan. You are not charged on the free plan, You are not charged on the free plan, whose 50-user limit counts only Zero Trust logins such as your admin email, not testers..
31
+
32
+ Do this once now, at dash.cloudflare.com → Zero Trust, so the Access step after deploy does not stall on account setup.
33
+
34
+ ## What you do not need
35
+
36
+ - No custom domain and no DNS. Everything runs on `*.workers.dev`.
37
+ - No API token for deployment; `wrangler login` covers it (CI is the exception, above).
38
+
39
+ One caveat for later: sending invite email automatically requires a real sending domain you control — a `*.workers.dev` hostname cannot be one. Copy-paste invite links work without it. See [Email](../operate/email.md).
40
+
41
+ Next: [Deploy](deploy.md)
@@ -0,0 +1,84 @@
1
+ # Deploy
2
+
3
+ This page covers the first deploy of an instance, locking the admin behind Cloudflare Access, verifying the result, updating in place, and running more than one instance.
4
+
5
+ ## Run the deploy
6
+
7
+ Both distribution forms take the same flags:
8
+
9
+ ```bash
10
+ npx alpha-gate deploy --instance myalpha # from npm; state in ~/.alpha-gate
11
+ ./deploy/deploy.sh --instance myalpha # from a clone; state in .deploy/
12
+ ```
13
+
14
+ The `--instance` slug (lowercase letters, digits, hyphens) namespaces every resource. To rehearse without touching your account, add `--dry-run` — wrangler is mocked and nothing is created.
15
+
16
+ A run proceeds in four stages: a read-only preflight (Node ≥ 20, wrangler auth; any failure prints a `→` fix line), an inspect pass that reports which resources already exist, a confirm prompt ("Apply these changes?") showing the exact commands, then apply: create the D1 database and R2 bucket, apply migrations, and deploy both Workers. The command is idempotent; re-running updates in place.
17
+
18
+ The run ends by printing two URLs; until Access is wired it also shows the dashboard checklist covering the Access step below.:
19
+
20
+ - the app URL (`https://alpha-gate-myalpha.<account>.workers.dev`) — the public host your users and the Sparkle feed use;
21
+ - the admin URL (`https://alpha-gate-myalpha-admin.<account>.workers.dev`) — the back office.
22
+
23
+ ### Guided first init
24
+
25
+ On the first deploy of an instance, any branding value you do not pass as a flag is prompted (interactive runs only; `--yes` and `--dry-run` skip the prompts); press Enter to accept the default; press Enter to accept the default.
26
+
27
+ - App name (`--app-name`).
28
+ - Activate URL scheme (`--activate-scheme`) — **must match a `CFBundleURLSchemes` entry in your macOS app's Info.plist**, because the download page builds the activation deep link from it.
29
+ - Short blurb (`--blurb`) and accent colour (`--accent`), both optional branding.
30
+
31
+ These seed the instance so `/get` is correct immediately. After first init the admin Settings page owns these values; later deploys do not prompt for them.
32
+
33
+ ## Lock the admin behind Cloudflare Access
34
+
35
+ **The admin URL rejects every request until you complete this step** — the Worker fails closed while the Access secrets are unset. Enabling Access is what makes the admin usable. It is the one manual dashboard action; there is no wrangler command for enabling Access. It is the one manual dashboard action; there is no wrangler command for enabling Access.
36
+
37
+ The first time you use Zero Trust, Cloudflare makes you pick a team name and, even on the free Zero Trust plan, add a payment method before you can create Access applications. Cloudflare makes you pick a team name and, even on the free Zero Trust plan, add a payment method before you can create Access applications. You are not charged on the free plan, which covers up to 50 seats — only the admin emails you allowlist into Access count against it, never your testers (they authenticate with tokens, not Access).
38
+
39
+ 1. Dashboard → Workers & Pages → the `alpha-gate-myalpha-admin` Worker → Settings → Domains & Routes → enable Cloudflare Access. Cloudflare creates a self-hosted Access application for the admin hostname.
40
+ 2. Edit that application's policy: action Allow, include Emails → your email, identity method One-time PIN. Cloudflare emails a login code; no external identity provider is needed.
41
+ 3. Feed two values back to deploy, which sets them as secrets and redeploys for you:
42
+ - Team domain — Zero Trust → Settings, e.g. `yourteam.cloudflareaccess.com` (the bare domain, no `https://`).
43
+ - AUD tag — Access → Applications → your app → Overview → Application Audience (AUD).
44
+
45
+ ```bash
46
+ ./deploy/deploy.sh --instance myalpha \
47
+ --access-team-domain yourteam.cloudflareaccess.com --access-aud <AUD>
48
+ ```
49
+
50
+ An interactive first deploy offers this at the end of the run: it prints the dashboard steps, waits, then probes the admin URL to detect the team domain from the login redirect and prompts for the AUD — no re-run needed.
51
+
52
+ The Worker verifies the Access JWT itself and fails closed: while these secrets are unset, every admin request is rejected.
53
+
54
+ ## Verify
55
+
56
+ - Open the admin URL and log in with the one-time PIN. The root path redirects to `/admin`, the Overview page; the serving map shows each channel and what it offers (empty until you publish).
57
+ - Open the app URL's `/get` without a token. It returns a generic 404. That is by design — no token, nothing leaked.
58
+ - Open Settings. The "This instance" panel shows the Instance slug, the Email status, and your Access team and Access AUD, confirming the secrets landed.
59
+
60
+ ## Update in place
61
+
62
+ From npm:
63
+
64
+ ```bash
65
+ npx alpha-gate@latest deploy --instance myalpha
66
+ ```
67
+
68
+ From a clone: `git pull`, then re-run `./deploy/deploy.sh --instance myalpha`.
69
+
70
+ Either way the existing D1 database and R2 bucket are reused, pending migrations are applied, and both Workers are redeployed. Users, their tokens, builds, channels, and logs are preserved. **Email and Access settings are remembered** — a bare re-run keeps them; pass the flags again only to change them.
71
+
72
+ A daily cron polls the npm registry for a newer Alpha Gate release. When one exists, the Overview shows a banner ("Alpha Gate <version> is available") and Settings shows it in the Self-update row, along with when the last check ran. A fork can point `$UPDATE_MANIFEST_URL` at its own package or a static `release.json` at deploy time.
73
+
74
+ ## Multiple instances
75
+
76
+ Re-run with another slug:
77
+
78
+ ```bash
79
+ ./deploy/deploy.sh --instance staging
80
+ ```
81
+
82
+ Every resource is independent per instance: the D1 database, the R2 bucket, both Workers, the Access application, and the local state under `.deploy/<slug>.*` (or `~/.alpha-gate` for the npm form). Repeat the Access step for each instance.
83
+
84
+ Next: [Sparkle integration (Swift)](../integrate/sparkle-swift.md)