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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/bin/alpha-gate.mjs +75 -0
- package/deploy/backup.sh +45 -0
- package/deploy/deploy.sh +21 -0
- package/deploy/dev.sh +56 -0
- package/deploy/lib/statedir.sh +11 -0
- package/deploy/teardown.sh +17 -0
- package/docs/ONBOARDING.md +16 -0
- package/docs/PRINCIPLES.md +195 -0
- package/docs/README.md +54 -0
- package/docs/UPLOADING.md +14 -0
- package/docs/integrate/activation.md +56 -0
- package/docs/integrate/sparkle-go.md +106 -0
- package/docs/integrate/sparkle-swift.md +63 -0
- package/docs/maintain/backup.md +52 -0
- package/docs/maintain/migrate-account.md +95 -0
- package/docs/maintain/teardown.md +46 -0
- package/docs/maintain/troubleshooting.md +102 -0
- package/docs/maintain/updating.md +92 -0
- package/docs/operate/add-users.md +55 -0
- package/docs/operate/channels.md +54 -0
- package/docs/operate/email.md +51 -0
- package/docs/operate/monitoring.md +56 -0
- package/docs/operate/publish.md +90 -0
- package/docs/operate/remove-users.md +47 -0
- package/docs/setup/cloudflare-account.md +41 -0
- package/docs/setup/deploy.md +84 -0
- package/docs/setup/install.md +65 -0
- package/migrations/0001_clients.sql +12 -0
- package/migrations/0002_builds_streams.sql +30 -0
- package/migrations/0003_access_log.sql +14 -0
- package/migrations/0004_meta.sql +5 -0
- package/migrations/0005_admin_audit.sql +14 -0
- package/migrations/0006_build_dmg.sql +6 -0
- package/migrations/0007_access_requests.sql +12 -0
- package/migrations/0008_build_rollback_target.sql +7 -0
- package/migrations/0009_hidden.sql +6 -0
- package/migrations/0010_purged_at.sql +5 -0
- package/package.json +65 -0
- package/publish.sh +302 -0
- package/release.json +7 -0
- package/src/auth/access-jwt.ts +210 -0
- package/src/auth/token-gate.ts +27 -0
- package/src/core/appcast.ts +107 -0
- package/src/core/audit-chain.ts +145 -0
- package/src/core/invite-template.ts +127 -0
- package/src/core/no-build.ts +160 -0
- package/src/core/resolver.ts +60 -0
- package/src/core/tokens.ts +51 -0
- package/src/core/types.ts +76 -0
- package/src/core/validation.ts +60 -0
- package/src/core/verdict.ts +195 -0
- package/src/core/version.ts +113 -0
- package/src/cron.ts +37 -0
- package/src/db/access-log.ts +168 -0
- package/src/db/access-requests.ts +90 -0
- package/src/db/admin-audit.ts +101 -0
- package/src/db/builds.ts +169 -0
- package/src/db/client.ts +80 -0
- package/src/db/clients.ts +103 -0
- package/src/db/meta.ts +30 -0
- package/src/db/streams.ts +85 -0
- package/src/deploy/cli.ts +181 -0
- package/src/deploy/commands/deploy.ts +392 -0
- package/src/deploy/commands/dev.ts +190 -0
- package/src/deploy/commands/teardown.ts +159 -0
- package/src/deploy/core/args.ts +205 -0
- package/src/deploy/core/colors.ts +49 -0
- package/src/deploy/core/config.ts +65 -0
- package/src/deploy/core/parse.ts +51 -0
- package/src/deploy/core/paths.ts +27 -0
- package/src/deploy/core/plan.ts +138 -0
- package/src/deploy/core/result.ts +13 -0
- package/src/deploy/core/state.ts +64 -0
- package/src/deploy/core/table.ts +49 -0
- package/src/deploy/core/types.ts +39 -0
- package/src/deploy/core/ui.ts +107 -0
- package/src/deploy/seams/clock.ts +10 -0
- package/src/deploy/seams/files.ts +52 -0
- package/src/deploy/seams/io.ts +56 -0
- package/src/deploy/seams/wrangler.ts +100 -0
- package/src/deploy/ui-preview.ts +112 -0
- package/src/deps.ts +41 -0
- package/src/dev/admin-entry.ts +104 -0
- package/src/env.ts +47 -0
- package/src/lib/clock.ts +17 -0
- package/src/lib/hosts.ts +27 -0
- package/src/lib/text.ts +6 -0
- package/src/r2/builds-bucket.ts +50 -0
- package/src/r2/keys.ts +27 -0
- package/src/routes/admin/admin-context.ts +9 -0
- package/src/routes/admin/audit-fields.ts +22 -0
- package/src/routes/admin/branding.tsx +161 -0
- package/src/routes/admin/builds.tsx +388 -0
- package/src/routes/admin/clients.tsx +396 -0
- package/src/routes/admin/confirm.tsx +80 -0
- package/src/routes/admin/flash.ts +72 -0
- package/src/routes/admin/form.ts +43 -0
- package/src/routes/admin/index.ts +146 -0
- package/src/routes/admin/invite.ts +57 -0
- package/src/routes/admin/middleware.ts +52 -0
- package/src/routes/admin/negotiate.ts +13 -0
- package/src/routes/admin/pending.tsx +73 -0
- package/src/routes/admin/read-model.ts +518 -0
- package/src/routes/admin/streams.tsx +182 -0
- package/src/routes/admin/theme.ts +34 -0
- package/src/routes/admin/upload.tsx +320 -0
- package/src/routes/admin/views.tsx +293 -0
- package/src/routes/app/access.tsx +38 -0
- package/src/routes/app/app-context.ts +8 -0
- package/src/routes/app/appcast.ts +68 -0
- package/src/routes/app/assets.ts +25 -0
- package/src/routes/app/download.ts +45 -0
- package/src/routes/app/get.tsx +35 -0
- package/src/routes/app/index.ts +31 -0
- package/src/routes/app/resolve.ts +16 -0
- package/src/services/anchor.ts +54 -0
- package/src/services/audit.ts +19 -0
- package/src/services/branding.ts +51 -0
- package/src/services/email-cloudflare.ts +80 -0
- package/src/services/email.ts +68 -0
- package/src/services/self-update.ts +62 -0
- package/src/views/access-page.tsx +39 -0
- package/src/views/admin/ci-page.tsx +76 -0
- package/src/views/admin/combobox.tsx +191 -0
- package/src/views/admin/forms.tsx +37 -0
- package/src/views/admin/layout.tsx +496 -0
- package/src/views/admin/manage-pages.tsx +223 -0
- package/src/views/admin/manage.tsx +1120 -0
- package/src/views/admin/plist-extract.ts +233 -0
- package/src/views/admin/read-pages.tsx +1098 -0
- package/src/views/admin/setup-page.tsx +108 -0
- package/src/views/admin/table-enhance.ts +176 -0
- package/src/views/admin/ui.tsx +159 -0
- package/src/views/get-page.tsx +39 -0
- package/src/views/layout.tsx +57 -0
- package/src/worker.ts +23 -0
- package/tsconfig.json +35 -0
package/docs/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Alpha Gate documentation
|
|
2
|
+
|
|
3
|
+
Alpha Gate is a self-hosted distribution gate for notarized macOS apps updated via Sparkle: private,
|
|
4
|
+
per-tester download links and update feeds, served from your own Cloudflare account. These pages are
|
|
5
|
+
task-oriented; the architecture and its invariants live in [PRINCIPLES](PRINCIPLES.md).
|
|
6
|
+
|
|
7
|
+
The journey, in order:
|
|
8
|
+
|
|
9
|
+
## Set up
|
|
10
|
+
|
|
11
|
+
| Page | Covers |
|
|
12
|
+
|---|---|
|
|
13
|
+
| [Install](setup/install.md) | The CLI from npm or a clone; running both Workers locally, no account needed |
|
|
14
|
+
| [Prepare a Cloudflare account](setup/cloudflare-account.md) | wrangler login, the Zero Trust bootstrap, what you can skip |
|
|
15
|
+
| [Deploy](setup/deploy.md) | The first deploy, locking the admin behind Access, verifying, updating in place, multiple instances |
|
|
16
|
+
|
|
17
|
+
## Integrate your app
|
|
18
|
+
|
|
19
|
+
| Page | Covers |
|
|
20
|
+
|---|---|
|
|
21
|
+
| [Sparkle in Swift](integrate/sparkle-swift.md) | The EdDSA key, Info.plist, the runtime per-user feed URL; official Sparkle references |
|
|
22
|
+
| [Sparkle in Go](integrate/sparkle-go.md) | Pure-Go via [go-sparkle](https://github.com/alex-ivanov/go-sparkle), or framework bindings — and where they differ |
|
|
23
|
+
| [Activation](integrate/activation.md) | The token, the activate deep link, why it is out-of-band, both sides of the wiring |
|
|
24
|
+
|
|
25
|
+
## Operate
|
|
26
|
+
|
|
27
|
+
| Page | Covers |
|
|
28
|
+
|---|---|
|
|
29
|
+
| [Add users](operate/add-users.md) | Inviting, the invite page, access requests, what the user sees |
|
|
30
|
+
| [Remove users](operate/remove-users.md) | Revoke, reactivate, reissue, hide — and which one to reach for |
|
|
31
|
+
| [Channels](operate/channels.md) | Grouping who gets what: linking builds, assigning users, pinning, the stranding guard |
|
|
32
|
+
| [Publish](operate/publish.md) | The one-command publish, the browser upload, CI, rollback, end-to-end verification |
|
|
33
|
+
| [Monitoring](operate/monitoring.md) | Reading the Overview, the Next-check column, Activity, and the audit chain |
|
|
34
|
+
| [Email](operate/email.md) | Copy-paste invites by default; Cloudflare email as the optional paid add-on |
|
|
35
|
+
|
|
36
|
+
## Maintain
|
|
37
|
+
|
|
38
|
+
| Page | Covers |
|
|
39
|
+
|---|---|
|
|
40
|
+
| [Backup](maintain/backup.md) | What is irreplaceable, the one-command dump, restoring |
|
|
41
|
+
| [Migrate to a new account](maintain/migrate-account.md) | The planned cutover a hostname change requires |
|
|
42
|
+
| [Updating the tool](maintain/updating.md) | The release banner, updating from npm or a clone, breaking releases |
|
|
43
|
+
| [Teardown](maintain/teardown.md) | Removing an instance, and the two steps that stay manual |
|
|
44
|
+
| [Troubleshooting](maintain/troubleshooting.md) | Symptoms → causes → fixes, across deploy, publishing, and the feed |
|
|
45
|
+
|
|
46
|
+
## Reference
|
|
47
|
+
|
|
48
|
+
- [PRINCIPLES](PRINCIPLES.md) — the architecture, the hard constraints, and the invariants that must
|
|
49
|
+
keep holding. Read it before changing core behavior.
|
|
50
|
+
- [CONTRIBUTING](../CONTRIBUTING.md) — the developer guide: conventions, testing, adding a feature.
|
|
51
|
+
|
|
52
|
+
New here? The shortest useful path is [Install](setup/install.md) →
|
|
53
|
+
[Deploy](setup/deploy.md) → [Sparkle in Swift](integrate/sparkle-swift.md) →
|
|
54
|
+
[Add users](operate/add-users.md) → [Publish](operate/publish.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Uploading builds
|
|
2
|
+
|
|
3
|
+
This guide was split into task pages. Start at the [documentation index](README.md).
|
|
4
|
+
|
|
5
|
+
Where its sections went:
|
|
6
|
+
|
|
7
|
+
| Was | Now |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Wire Sparkle into your app | [integrate/sparkle-swift.md](integrate/sparkle-swift.md) (Go apps: [integrate/sparkle-go.md](integrate/sparkle-go.md)) |
|
|
10
|
+
| Activation, the token, the deep link | [integrate/activation.md](integrate/activation.md) |
|
|
11
|
+
| Create a channel | [operate/channels.md](operate/channels.md) |
|
|
12
|
+
| Invite a user, getting the link to users | [operate/add-users.md](operate/add-users.md) |
|
|
13
|
+
| Publish (one command, browser, CI), verify, rollback | [operate/publish.md](operate/publish.md) |
|
|
14
|
+
| Troubleshooting | [maintain/troubleshooting.md](maintain/troubleshooting.md) |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Activation
|
|
2
|
+
|
|
3
|
+
How the per-user token reaches your app, and what to implement on the app side and the admin side.
|
|
4
|
+
|
|
5
|
+
## Why the token is out-of-band
|
|
6
|
+
|
|
7
|
+
The build is generic and signed once. **The token is never embedded in the binary** — embedding it would break the notarization seal and force a signing pass per user. Instead, every user installs the same artifact, and the token, the user's only credential, reaches the app at runtime through a deep link or a paste. The token never sits in Sparkle's static configuration either: `SUFeedURL` stays unset, and your app holds the token and builds the per-user feed URL from it at check time (see [Sparkle in Swift](sparkle-swift.md)). (see [Sparkle in Swift](sparkle-swift.md)).
|
|
8
|
+
|
|
9
|
+
## What the token is
|
|
10
|
+
|
|
11
|
+
A token is 32 characters of Crockford base32 (digits and letters, excluding I, L, O, U), carrying 160 bits of entropy. One token exists per user, and it gates the download, the appcast, and the `/get` page alike. Lookup is forgiving: case, whitespace, hyphens, and the confusable characters O (read as 0) and I/L (read as 1) are normalized, so a hand-typed paste still matches.
|
|
12
|
+
|
|
13
|
+
The token travels in URLs (`/get?token=`, `/appcast?token=`, `/download?token=`), which means it appears in Cloudflare's own request logs. That is an accepted trade-off for a private alpha. Two mitigations apply: an unknown token is indistinguishable from a revoked one (the same generic 404 on `/get` and `/download`, the same renewal notice on `/appcast`), so a response never confirms that a token exists, and the `/get` page sets `Referrer-Policy: no-referrer`, so the token never leaks through a Referer header.
|
|
14
|
+
|
|
15
|
+
## The activate URL scheme
|
|
16
|
+
|
|
17
|
+
The user's `/get` page shows two buttons, Download and Activate. Activate links to `<scheme>://activate?token=<token>`. The scheme comes from the **Activate URL scheme** field in admin Settings and must equal the scheme your app registers in its `Info.plist` under `CFBundleURLTypes → CFBundleURLSchemes`. If the two differ, clicking Activate opens nothing on the user's machine, because macOS finds no handler for the URL.
|
|
18
|
+
|
|
19
|
+
## App side
|
|
20
|
+
|
|
21
|
+
First launch has no token, so show an activate prompt with two ways in:
|
|
22
|
+
|
|
23
|
+
- **Deep link.** Handle `<scheme>://activate?token=<token>`: store the token in the Keychain, then start the updater.
|
|
24
|
+
- **Paste.** The `/get` page prints the raw key under the buttons ("Access key — paste it in the app if Activate doesn't open it"), so keep a text field that accepts it. You do not need to clean the input; the server normalizes case, whitespace, hyphens, and the O/I/L confusables.
|
|
25
|
+
|
|
26
|
+
An illustrative handler (AppKit; lives in your app):
|
|
27
|
+
|
|
28
|
+
```swift
|
|
29
|
+
func application(_ application: NSApplication, open urls: [URL]) {
|
|
30
|
+
for url in urls where url.host == "activate" {
|
|
31
|
+
guard let token = URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
|
32
|
+
.queryItems?.first(where: { $0.name == "token" })?.value
|
|
33
|
+
else { continue }
|
|
34
|
+
Keychain.token = token // your Keychain wrapper
|
|
35
|
+
startUpdateChecks() // safe now: a feed URL exists
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Do not start update checks before a token exists; with no feed URL, Sparkle errors. The delegate that turns the stored token into the per-user feed URL is covered in [Sparkle in Swift](sparkle-swift.md) and [Sparkle in a Go app](sparkle-go.md).
|
|
41
|
+
|
|
42
|
+
## Lifecycle: revoke, reactivate, reissue
|
|
43
|
+
|
|
44
|
+
Revoking a user cuts access at once: `/download` returns 404, and the next update check receives an informational renewal notice instead of a build. The notice is a Sparkle item with no enclosure, so it can never be installed. It shows the title and message configured in Settings, displays as version "Access renewal", and links to the public `/access` page where the user can request access again. An active user is never shown this notice.
|
|
45
|
+
|
|
46
|
+
Because the token was never in the binary, the installed app recovers **without a reinstall**:
|
|
47
|
+
|
|
48
|
+
- **Reactivate** restores access on the existing link and token; updates resume on the app's next check.
|
|
49
|
+
- **Reissue** replaces the link and invalidates the token the installed app holds. The old link stops working; the user opens the new `/get` link and activates again, by deep link or paste. The old link stops working; the user opens the new `/get` link and activates again, by deep link or paste.
|
|
50
|
+
|
|
51
|
+
## Admin side: where the scheme is set
|
|
52
|
+
|
|
53
|
+
- **Guided first deploy.** On a fresh database, `./deploy/deploy.sh` prompts for the Activate URL scheme (default `myapp`), along with the app name, blurb, and accent colour.
|
|
54
|
+
- **Settings.** The admin Settings page has the *Activate URL scheme* field under *App activation*, next to the Sparkle public key. The `/get` page reads the scheme on every request, so a correction applies to every existing invite link immediately.
|
|
55
|
+
|
|
56
|
+
Next: [Add users](../operate/add-users.md)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Sparkle in a Go app
|
|
2
|
+
|
|
3
|
+
How to wire a Go macOS app to Alpha Gate. There are two routes: a pure-Go client that speaks the
|
|
4
|
+
Sparkle wire format ([alex-ivanov/go-sparkle](https://github.com/alex-ivanov/go-sparkle) — no
|
|
5
|
+
framework, works in any Go app), and cgo bindings around the real `Sparkle.framework`
|
|
6
|
+
([abemedia/go-sparkle](https://github.com/abemedia/go-sparkle), which needs a Cocoa run loop). The
|
|
7
|
+
server side is identical either way.
|
|
8
|
+
|
|
9
|
+
## Route 1: pure Go (alex-ivanov/go-sparkle)
|
|
10
|
+
|
|
11
|
+
A stdlib-only re-implementation of the Sparkle client: appcast parse, ed25519 verify, download,
|
|
12
|
+
and the macOS `.app` swap + relaunch. Signatures and keys are byte-identical with Sparkle's own
|
|
13
|
+
tooling, and token-gated feeds are built in — which is exactly what an Alpha Gate feed is.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
go get github.com/alex-ivanov/go-sparkle
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Configure it with the feed URL template; the `<TOKEN>` and `<CFBundleVersion>` placeholders do
|
|
20
|
+
what the Swift page's delegate methods do:
|
|
21
|
+
|
|
22
|
+
```go
|
|
23
|
+
up := sparkle.New(sparkle.Config{
|
|
24
|
+
FeedURL: "https://<your-app-host>/appcast?token=<TOKEN>&installed=<CFBundleVersion>",
|
|
25
|
+
PublicEDKey: "<base64 ed25519 public key>", // the same key as SUPublicEDKey
|
|
26
|
+
InstalledVersion: 42, // your build number
|
|
27
|
+
OSVersion: "14.4", // filters sparkle:minimumSystemVersion
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
rel, err := up.Check(ctx, token) // nil when up to date
|
|
31
|
+
if rel != nil {
|
|
32
|
+
path, err := up.Download(ctx, rel, token) // length + ed25519 verified before returning
|
|
33
|
+
app, err := sparkle.Apply(path) // swap the .app in place + relaunch
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The token is percent-encoded into the query (which is where Alpha Gate reads it) and also sent as
|
|
38
|
+
an `Authorization: Bearer` header, which Alpha Gate ignores — only the query parameter authenticates. `Apply` handles both `.zip` and `.dmg` artifacts, so whatever
|
|
39
|
+
you [publish](../operate/publish.md) installs unchanged.
|
|
40
|
+
|
|
41
|
+
Differences from framework Sparkle worth knowing with an Alpha Gate feed:
|
|
42
|
+
|
|
43
|
+
- **Revocation is quiet.** Framework Sparkle shows revoked users the renewal notice
|
|
44
|
+
([activation](activation.md)); the pure-Go client filters that notice item out (it has no
|
|
45
|
+
enclosure), so `Check` returns nil and a revoked app reads as up to date forever. No error is
|
|
46
|
+
raised and no download is attempted. If you want a visible "renew access" moment, detect it
|
|
47
|
+
yourself: treat the enclosure-less notice item in the fetched appcast as the signal (an active
|
|
48
|
+
token never receives one), or probe `/get?token=` and treat its 404 as revoked.
|
|
49
|
+
- **`--critical` has no effect** on this client — critical updates are on its not-implemented
|
|
50
|
+
list, so a critical build installs like any other.
|
|
51
|
+
|
|
52
|
+
### A pure-Go release pipeline
|
|
53
|
+
|
|
54
|
+
The library's `cmd/sign_update` is a drop-in replacement for Sparkle's `sign_update` — same flags,
|
|
55
|
+
same key format, same output, and on macOS it reads the login Keychain exactly where Sparkle's
|
|
56
|
+
`generate_keys` stores the key. `publish.sh` accepts it directly:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
go install github.com/alex-ivanov/go-sparkle/cmd/sign_update@latest
|
|
60
|
+
./publish.sh MyApp.zip --channel beta --sign-update "$(command -v sign_update)"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
You can also mint the keypair without installing Sparkle at all: `sparkle keygen` (from
|
|
64
|
+
`cmd/sparkle`) prints the base64 public key to embed and writes the private key file — keys are
|
|
65
|
+
interchangeable with Sparkle's. The [backup warning](../maintain/backup.md) applies to that key
|
|
66
|
+
file exactly as it does to the Keychain copy.
|
|
67
|
+
|
|
68
|
+
## Route 2: framework bindings (abemedia/go-sparkle)
|
|
69
|
+
|
|
70
|
+
If your Go app already runs a Cocoa UI (webview, Qt, Wails), you can drive the real
|
|
71
|
+
`Sparkle.framework` through [abemedia/go-sparkle](https://github.com/abemedia/go-sparkle) —
|
|
72
|
+
cgo bindings around Sparkle 2.7.0 via the legacy `SUUpdater` class. You get Sparkle's own UI and
|
|
73
|
+
the renewal-notice display, at the cost of shipping the framework:
|
|
74
|
+
|
|
75
|
+
1. Place `Sparkle.framework` (from the [official releases](https://github.com/sparkle-project/Sparkle/releases))
|
|
76
|
+
in `YourApp.app/Contents/Frameworks/`.
|
|
77
|
+
2. Build with `CGO_LDFLAGS='-Wl,-rpath,@loader_path/../Frameworks' go build .`
|
|
78
|
+
|
|
79
|
+
There is no delegate support, so compose the full feed URL in Go and set it on **every launch**,
|
|
80
|
+
before any check — Sparkle persists the URL to user defaults, and re-setting it overwrites a stale
|
|
81
|
+
token or build number:
|
|
82
|
+
|
|
83
|
+
```go
|
|
84
|
+
import sparkle "github.com/abemedia/go-sparkle"
|
|
85
|
+
|
|
86
|
+
sparkle.SetFeedURL("https://<your-app-host>/appcast?token=" + token + "&installed=" + build)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Info.plist and the deep link (both routes)
|
|
90
|
+
|
|
91
|
+
A Go `.app` bundle's `Info.plist` is yours to author (tools like
|
|
92
|
+
[appify](https://github.com/machinebox/appify) scaffold it). The keys match the
|
|
93
|
+
[Swift page's table](sparkle-swift.md): an integer `CFBundleVersion`, your activate scheme in
|
|
94
|
+
`CFBundleURLSchemes`, no `SUFeedURL`. `SUPublicEDKey` matters to route 2; route 1 takes the key in
|
|
95
|
+
its `Config` instead.
|
|
96
|
+
|
|
97
|
+
macOS delivers `myapp://activate?token=…` as an Apple Event (`kAEGetURL`), not in `os.Args`, so a
|
|
98
|
+
Go app needs a small Objective-C shim via cgo: register the handler early
|
|
99
|
+
(`applicationWillFinishLaunching`), pass the URL to an `//export`-ed Go function, store the token,
|
|
100
|
+
then configure the updater. A worked example of the pattern:
|
|
101
|
+
[Handling macOS URL schemes with Go](https://blakewilliams.me/posts/handling-macos-url-schemes-with-go);
|
|
102
|
+
Wails v3 ships a [ready-made example](https://pkg.go.dev/github.com/wailsapp/wails/v3/examples/single-instance-url-scheme).
|
|
103
|
+
Keep a paste field as the fallback — the `/get` page shows the raw key
|
|
104
|
+
(see [activation](activation.md)).
|
|
105
|
+
|
|
106
|
+
Next: [the token and activation flow](activation.md)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Sparkle 2 in a Swift app
|
|
2
|
+
|
|
3
|
+
How to wire Sparkle 2 into a native macOS app so its updates flow through your Alpha Gate instance.
|
|
4
|
+
|
|
5
|
+
## What differs from stock Sparkle
|
|
6
|
+
|
|
7
|
+
One thing: the update feed is per-user. Each user's token selects their channel, pin, and build, so there is no single appcast URL to bake into the app. You leave `SUFeedURL` unset and supply the feed URL at runtime through the updater delegate (below). Everything else (installing the framework, the updater controller, the check-for-updates menu item) is standard Sparkle 2; follow the [official setup guide](https://sparkle-project.org/documentation/) and come back here for the feed.
|
|
8
|
+
|
|
9
|
+
Keep two hosts straight. The app's feed points at the App Worker: the `app_url` in your deploy state (`.deploy/<slug>.state.json` in a clone, `~/.alpha-gate/<slug>.state.json` from npm). Publishing talks to the admin Worker, a different hostname.
|
|
10
|
+
|
|
11
|
+
The token is never embedded in the binary — embedding it would break the notarization seal. The build is generic and signed once; the token reaches the app out of band on first launch (see [Activation](activation.md)).
|
|
12
|
+
|
|
13
|
+
The admin's **App setup** page shows this whole section personalized for your instance, with your activate scheme and saved public key filled in.
|
|
14
|
+
|
|
15
|
+
## Generate the EdDSA key (once)
|
|
16
|
+
|
|
17
|
+
Use `generate_keys` from Sparkle 2's `bin/` directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
./bin/generate_keys # stores the PRIVATE key in your login Keychain; prints the PUBLIC key
|
|
21
|
+
./bin/generate_keys -x sparkle_private.pem # export the private key (for backup, or CI secrets)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The private key is what `sign_update` uses at publish time. The Worker never holds it; it only stores the signature string each build produces.
|
|
25
|
+
|
|
26
|
+
**The private key is unrecoverable — back it up now.** It lives only in your login Keychain. If you lose it (laptop dies, Keychain wiped), every already-installed app rejects all future updates: Sparkle verifies each download against the public key baked into the app, and you cannot produce a matching signature without the private key. Run `./bin/generate_keys -x sparkle_private.pem` and store that file somewhere safe and off the laptop, such as a password manager or an encrypted backup. This is the single most important thing to back up in the whole system.
|
|
27
|
+
|
|
28
|
+
## Info.plist keys
|
|
29
|
+
|
|
30
|
+
| Key | Value |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `SUPublicEDKey` | the public key `generate_keys` printed; Sparkle verifies every download against it |
|
|
33
|
+
| `SUFeedURL` | leave unset — the feed is per-user, supplied at runtime |
|
|
34
|
+
| `CFBundleURLTypes` → `CFBundleURLSchemes` | your activate scheme, e.g. `myapp`; must equal the *Activate URL scheme* in admin Settings |
|
|
35
|
+
| `SURequireSignedFeed` | off / unset — incompatible with per-user feeds (a signed feed would put the signing key on the edge); the per-archive EdDSA still blocks tampering |
|
|
36
|
+
| `CFBundleVersion` | a positive integer that increases every release (Sparkle's compare key); see below |
|
|
37
|
+
| `CFBundleShortVersionString` | the human version (e.g. `1.4.0`) shown in the update dialog |
|
|
38
|
+
|
|
39
|
+
**`CFBundleVersion` must be a positive integer that increases every release.** It becomes `build_number` (Sparkle's `sparkle:version`), which Sparkle compares numerically and the server requires to be an integer. `CFBundleShortVersionString` can be anything: `1.4.0`, a git describe. If your build sets `CFBundleVersion` to something non-integer (e.g. `0.0.3` or a hash), publishing rejects it — fix the build, or override per-publish with `--build-number <n>`.
|
|
40
|
+
|
|
41
|
+
## Supply the feed URL at runtime
|
|
42
|
+
|
|
43
|
+
Implement the updater delegate so each check carries the token and the installed build:
|
|
44
|
+
|
|
45
|
+
```swift
|
|
46
|
+
func feedURLString(for updater: SPUUpdater) -> String? {
|
|
47
|
+
guard let token = Keychain.token else { return nil } // no token yet → the check will fail; gate checks on activation (see below)
|
|
48
|
+
return "https://<APP_WORKER_HOST>/appcast?token=\(token)"
|
|
49
|
+
}
|
|
50
|
+
func feedParameters(for updater: SPUUpdater, sendingSystemProfile: Bool) -> [[String: String]] {
|
|
51
|
+
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "0"
|
|
52
|
+
return [["key": "installed", "value": build]] // powers the admin's "current version" + stranded detection
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`<APP_WORKER_HOST>` is the `app_url` host from your deploy state. `Keychain.token` is illustrative — read the token from wherever your activation flow stored it. **Do not start update checks before a token exists**: with no feed, Sparkle errors. The guard does not skip the check — it only keeps a tokenless request from ever reaching the server; hold off enabling automatic checks (or calling checkForUpdates) until activation has stored the token.
|
|
57
|
+
|
|
58
|
+
## References
|
|
59
|
+
|
|
60
|
+
- Sparkle basic setup (framework, updater controller): <https://sparkle-project.org/documentation/>
|
|
61
|
+
- Keys and signing (`generate_keys`, `sign_update`): <https://sparkle-project.org/documentation/publishing/>
|
|
62
|
+
|
|
63
|
+
Next: [Activation](activation.md)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Backup and restore
|
|
2
|
+
|
|
3
|
+
This page covers what to back up, how the backup command works, and how to restore a dump into a fresh instance.
|
|
4
|
+
|
|
5
|
+
## What matters
|
|
6
|
+
|
|
7
|
+
The irreplaceable state lives in D1: users and their tokens, build metadata, channels, logs, and the audit chain. R2 holds the archive bytes, which you re-create by publishing the same artifacts again, the branding images, which you re-upload on the Settings page, and the daily audit-chain anchors, which are append-only tamper evidence that a fresh instance starts over. So a backup is a D1 dump; everything else in R2 is either re-creatable or restarts clean.
|
|
8
|
+
|
|
9
|
+
Two things live only in your login Keychain, in neither D1 nor the local deploy state:
|
|
10
|
+
|
|
11
|
+
- The Sparkle private key. **Export it now** — run `generate_keys -x sparkle_private.pem` from Sparkle 2's `bin/` directory and store the file off the laptop, in a password manager or an encrypted backup. If you lose the key, every already-installed app rejects all future updates: Sparkle verifies each download against the public key baked into the app, and no matching signature can be produced without the private key. No dump restores this.
|
|
12
|
+
- The Access service token, which `publish.sh` stores on first use. This one is recreatable: make a new one in Cloudflare Zero Trust (Access → Service Auth), include it in the Service Auth policy on the admin Access application, and re-enter it by passing `--reset-token` on your next publish.
|
|
13
|
+
|
|
14
|
+
## The backup command
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
./deploy/backup.sh --instance <slug> # from a clone
|
|
18
|
+
npx alpha-gate backup --instance <slug> # from npm
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Both forms export the remote D1 database to `<slug>-<timestamp>.sql` in the state directory — `.deploy/` in a clone, `~/.alpha-gate` from npm. Pass `--out <dir>` to write somewhere else:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
./deploy/backup.sh --instance <slug> --out ~/secure-backups
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run it before a risky change, or on a schedule with cron or launchd. The dump path is the last line the command prints on stdout, so a scheduled job can capture it with `tail -n 1`. This is the same export `teardown.sh` takes before destroying an instance.
|
|
28
|
+
|
|
29
|
+
**The dump contains your testers' live access tokens.** Anyone who reads it can download your builds as any tester. Keep it off the laptop, in private storage, and prune old copies.
|
|
30
|
+
|
|
31
|
+
## Restore
|
|
32
|
+
|
|
33
|
+
Restore in this order: create the empty database, import the dump into it, then deploy. Importing
|
|
34
|
+
into an already-deployed instance fails — the deploy has applied migrations, and the dump replays
|
|
35
|
+
`d1_migrations` and every table, so the import hits conflicts and rolls back.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx wrangler d1 create alpha-gate-<slug> # the empty database
|
|
39
|
+
npx wrangler d1 execute alpha-gate-<slug> --remote --file <slug>-<timestamp>.sql
|
|
40
|
+
./deploy/deploy.sh --instance <slug> # deploy on top
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The deploy finds the restored database, sees its migrations already recorded, and skips first-init
|
|
44
|
+
prompts because your settings came back with the dump. Keep the `--remote` flag on the import:
|
|
45
|
+
without it, wrangler applies the dump to a local development copy and the deployed instance is
|
|
46
|
+
unchanged. Users keep their existing tokens and `/get` links after the restore while the hostname stays the same. Restoring under a different instance slug or account changes the hostname, which breaks every link you already sent and every installed app's feed URL — that move is a planned cutover, not a restore (see [Migrate to another account](migrate-account.md)).
|
|
47
|
+
|
|
48
|
+
## Losing the local state directory
|
|
49
|
+
|
|
50
|
+
The state directory (`.deploy/` or `~/.alpha-gate`, overridden by `$ALPHA_GATE_HOME`) is not a backup target. Re-running `deploy.sh` regenerates it: configs, resolved ids, URLs, and the Access wiring are re-derived from the live resources. The one exception is email — **those flags are remembered only in the state directory**, so after losing it pass `--email-provider` and `--email-from` once more. A bare re-run without them quietly reverts invites to copy-paste links.
|
|
51
|
+
|
|
52
|
+
Next: [Migrate to another account](migrate-account.md)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Migrate an instance to a different Cloudflare account
|
|
2
|
+
|
|
3
|
+
How to move a running instance to another Cloudflare account without stranding the apps your users already installed.
|
|
4
|
+
|
|
5
|
+
## Why this is more than backup and restore
|
|
6
|
+
|
|
7
|
+
The Workers run on `*.workers.dev`, and that hostname embeds the account: the app Worker serves at `https://alpha-gate-<instance>.<account>.workers.dev`. A new account means a new `<account>` label, so the app hostname changes. Every installed app has the old host baked into the feed URL its code builds (see [Sparkle integration](../integrate/sparkle-swift.md)) — it checks the old host and knows nothing about the new one.
|
|
8
|
+
|
|
9
|
+
The database restores cleanly ([backup](backup.md) covers the dump), but restoring alone does not move your users. Until each app installs a build whose feed URL points at the new host, it depends on the old instance. Tear down the old instance before that update has landed and **installed apps stop updating**; the only recovery is asking every user to reinstall by hand. So treat the move as a planned cutover, not a restore.
|
|
10
|
+
|
|
11
|
+
## The order that works
|
|
12
|
+
|
|
13
|
+
Nothing below interrupts the old instance until the last step. Users keep updating throughout. (The `npx alpha-gate <cmd>` form takes the same flags as the `./deploy/*.sh` wrappers shown here.)
|
|
14
|
+
|
|
15
|
+
1. **Keep the old instance running** and take a fresh dump:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
./deploy/backup.sh --instance <slug> # → .deploy/<slug>-<timestamp>.sql
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The dump contains your testers' live access tokens — handle it like a secret.
|
|
22
|
+
|
|
23
|
+
2. **Log wrangler into the new account, create the database there, and restore into it while it
|
|
24
|
+
is still empty** (importing after a deploy fails on migration conflicts — see
|
|
25
|
+
[backup](backup.md)):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx wrangler login # the NEW account
|
|
29
|
+
npx wrangler d1 create alpha-gate-<slug>
|
|
30
|
+
npx wrangler d1 execute alpha-gate-<slug> --remote --file .deploy/<slug>-<timestamp>.sql
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The same slug is fine: a different account means a different `<account>` label, so the
|
|
34
|
+
hostnames cannot collide. Users, tokens, channels, build metadata, logs, and the audit chain
|
|
35
|
+
all carry over. Archive bytes do not — R2 is not in the dump, so the restored build rows have
|
|
36
|
+
nothing downloadable behind them yet. Step 7 fixes that.
|
|
37
|
+
|
|
38
|
+
3. **Deploy on top of the restored database:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
./deploy/deploy.sh --instance <slug>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The deploy finds the database, sees its migrations already recorded, and skips the first-init
|
|
45
|
+
prompts — your app name, activate scheme, and branding came back with the dump.
|
|
46
|
+
|
|
47
|
+
4. **Redo the two manual Access steps** for the new account: enable Cloudflare Access on the new admin hostname and allowlist your email, exactly as in [deploy](../setup/deploy.md). Both values change, because Zero Trust teams are per account. Feed them back:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
./deploy/deploy.sh --instance <slug> \
|
|
51
|
+
--access-team-domain <team>.cloudflareaccess.com --access-aud <AUD>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
5. **Create a service token in the new account's Zero Trust** (with a Service Auth policy on the new admin Access application, as in [publish](../operate/publish.md)). Your Keychain still holds the old account's token under this slug, so on the first publish to the new instance pass `--reset-token`; `publish.sh` prompts for the new Client ID and Secret and stores them.
|
|
55
|
+
|
|
56
|
+
6. **Check email carried over, if you used it.** The flags ride in the slug's local state, so the step-3 deploy reuses them and prints `(reusing email: …)`. If that line is missing (a different machine, a lost state directory), pass the flags once — this can ride on the same run as step 4:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
./deploy/deploy.sh --instance <slug> --email-provider cloudflare --email-from alpha@<sending-domain>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
7. Ship the cutover release. Rebuild your app so its feed URL points at the new app host (`https://alpha-gate-<slug>.<new-account>.workers.dev`), give it a higher `CFBundleVersion`, and publish the artifact to both instances:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# to the OLD instance — installed apps still check there
|
|
66
|
+
./publish.sh MyApp.dmg --channel <name> \
|
|
67
|
+
--admin-url https://alpha-gate-<slug>-admin.<old-account>.workers.dev
|
|
68
|
+
|
|
69
|
+
# to the NEW instance — gives the restored data a downloadable top build
|
|
70
|
+
./publish.sh MyApp.dmg --channel <name> --instance <slug> --reset-token
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Local state for the slug now points at the new account, so the old instance must be addressed with `--admin-url`; that publish prompts for the old account's service token, which still exists in the old Zero Trust. Every app that installs this build starts checking the new host, where its restored token is already valid.
|
|
74
|
+
|
|
75
|
+
8. **Watch the new instance's Activity page** until your users appear there. Each updated app's next check shows up as a `check` entry carrying its installed build (see [monitoring](../operate/monitoring.md)). Users who have not updated yet are still on the old instance — leave it running until the checks you care about have moved.
|
|
76
|
+
|
|
77
|
+
9. **Tear down the old instance.** Teardown acts on whichever account wrangler is logged into, and you are currently logged into the new one — log back into the old account first:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx wrangler login # the OLD account
|
|
81
|
+
./deploy/teardown.sh --instance <slug> --dry-run # confirm it targets the old resources
|
|
82
|
+
./deploy/teardown.sh --instance <slug>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It archives the old D1 first — one more backup. [Teardown](teardown.md) covers the closing checklist (empty the R2 bucket, remove the old Access application). Teardown also removes the slug's local state files, which by now describe the NEW instance — take a copy of the state directory first, and log wrangler back into the new account when you are done.
|
|
86
|
+
|
|
87
|
+
## Next time: a custom domain on the app Worker
|
|
88
|
+
|
|
89
|
+
Both Workers serve on whatever hostname the request arrives on; the one `workers.dev` assumption is the invite-link derivation (`src/lib/hosts.ts`), so `/get` links keep using the `workers.dev` app host even after you attach a domain. If you attach a domain you control to the app Worker (a Cloudflare dashboard action; the account needs a zone for that domain) and bake that hostname into your app's feed URL from day one, the host in installed apps never changes. A future account move is then deploy, restore, redo Access, and re-point the domain at the new account's Worker — steps 7 and 8 disappear. Do it before the first build ships with a `workers.dev` feed URL.
|
|
90
|
+
|
|
91
|
+
## Invite links
|
|
92
|
+
|
|
93
|
+
Every `/get?token=` link you already sent embeds the old host, so those links die with the old instance. The tokens themselves survive the restore unchanged: activated apps are unaffected, because the token rides in the feed URL the app builds, not in the link. Only users who have not yet redeemed an invite need a fresh link — open each user's page on the new admin, which shows the current `/get?token=` link, and re-send it (see [add users](../operate/add-users.md)). The links the admin shows are derived from the `workers.dev` naming contract (`src/lib/hosts.ts`), so they point at the new host as soon as the new admin is up.
|
|
94
|
+
|
|
95
|
+
Next: [Teardown](teardown.md)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Teardown
|
|
2
|
+
|
|
3
|
+
How to remove an Alpha Gate instance completely — both Workers, the database, the local state, and the two steps you finish by hand.
|
|
4
|
+
|
|
5
|
+
## The command
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
./deploy/teardown.sh --instance <slug> # confirm, archive the database, destroy
|
|
9
|
+
./deploy/teardown.sh --instance <slug> --dry-run # rehearse: print the plan, touch nothing
|
|
10
|
+
./deploy/teardown.sh --instance <slug> --no-archive # destroy without the database backup
|
|
11
|
+
./deploy/teardown.sh --instance <slug> --yes # skip the confirmation prompt (CI)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
From npm the same flags apply: `npx alpha-gate teardown --instance <slug>`. `--archive-dir <dir>` writes the database archive somewhere other than the state directory.
|
|
15
|
+
|
|
16
|
+
You need wrangler authentication (`npx wrangler login`, once per machine). The command prints the full destructive plan before touching anything, then asks you to **type the instance name** to confirm. Any other input aborts with nothing deleted. A non-interactive run without `--yes` refuses instead of hanging.
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
The steps run in this order:
|
|
21
|
+
|
|
22
|
+
1. Archives the database first, while it still exists, to `<state-dir>/<slug>-<timestamp>.sql`. The state directory is `.deploy/` in a git clone, `~/.alpha-gate` for the npm install. This is the same dump [backup](backup.md) produces and **it contains your testers' live access tokens** — move it to private storage or shred it. If the export fails, teardown stops and nothing is destroyed; fix the error, or re-run with `--no-archive` to destroy without a backup.
|
|
23
|
+
2. Deletes the app Worker (`alpha-gate-<slug>`) and the admin Worker (`alpha-gate-<slug>-admin`). Workers that are already gone are tolerated, so re-running after a partial teardown works — pass `--no-archive` on the re-run if the database was already deleted, since the archive step has nothing left to export.
|
|
24
|
+
3. Deletes the R2 bucket, but only if it is already empty. A non-empty bucket is left in place and reported.
|
|
25
|
+
4. Deletes the D1 database.
|
|
26
|
+
5. Removes the local configs from the state directory: `<slug>.app.toml`, `<slug>.admin.toml`, `<slug>.state.json`.
|
|
27
|
+
|
|
28
|
+
A `--dry-run` prints the same plan and step lines but runs no wrangler commands and deletes no files.
|
|
29
|
+
|
|
30
|
+
## What stays manual
|
|
31
|
+
|
|
32
|
+
Two things pure wrangler cannot do, printed as the closing checklist:
|
|
33
|
+
|
|
34
|
+
- Empty and delete a non-empty R2 bucket: dashboard → R2 → the bucket → delete. This line appears only when the bucket survived; an empty bucket was already deleted for you.
|
|
35
|
+
- **Remove the Cloudflare Access application** for `alpha-gate-<slug>-admin`: Zero Trust → Access → Applications. Until you do, a dead application lingers in your Zero Trust account.
|
|
36
|
+
|
|
37
|
+
Teardown also does not touch the Access service token. publish.sh had you create one the first time you published to this instance (a CI runner may hold another); remove them under Zero Trust → Access → Service Auth., remove it under Zero Trust → Access → Service Auth.
|
|
38
|
+
|
|
39
|
+
## What survives on your Mac
|
|
40
|
+
|
|
41
|
+
Teardown never touches the Keychain. Two secrets stay in your login Keychain:
|
|
42
|
+
|
|
43
|
+
- The **Sparkle private key**. Its public half (`SUPublicEDKey`) is baked into every build you shipped, and if the key is lost, **every already-installed app rejects all updates** — even from a fresh instance later. Keep it unless you are retiring the app for good. See [Sparkle integration](../integrate/sparkle-swift.md).
|
|
44
|
+
- The Access service token that `publish.sh` stored, keyed by instance. It is a credential for the Access application you are deleting, so it becomes useless once that application is gone; a future instance takes a new one via `./publish.sh <file> --reset-token`.
|
|
45
|
+
|
|
46
|
+
Delete them from your login Keychain by hand if you want them gone: the service token is two items under the service `alpha-gate-access` (`<slug>-client-id` and `<slug>-client-secret`); the Sparkle key is the item `generate_keys` created (search for "sparkle").
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Symptoms, their causes, and their fixes, grouped by area.
|
|
4
|
+
|
|
5
|
+
## Deploy and access
|
|
6
|
+
|
|
7
|
+
### The admin returns 403 after it worked before
|
|
8
|
+
|
|
9
|
+
The Access secrets stored on the Worker are usually stale. The Worker verifies the Access JWT itself and fails closed, so wrong or unset secrets reject every admin request.
|
|
10
|
+
|
|
11
|
+
- **You renamed your Zero Trust team.** The team domain changed to `<new-team>.cloudflareaccess.com`; the AUD usually stays. Re-run deploy with the new domain:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
./deploy/deploy.sh --instance <slug> --access-team-domain <new-team>.cloudflareaccess.com
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **You deleted and recreated the Access application.** The AUD changed. Re-run with the new one:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
./deploy/deploy.sh --instance <slug> --access-aud <new-aud>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Where to find both values is covered in [Deploy](../setup/deploy.md).
|
|
24
|
+
|
|
25
|
+
### `deploy.sh` stops at preflight
|
|
26
|
+
|
|
27
|
+
Preflight is read-only and prints what failed with a `→` fix line under it. The two common causes: Node is older than 20, or wrangler is not authenticated — run `npx wrangler login`, or set `CLOUDFLARE_API_TOKEN` in CI. Fix what the line says and re-run; deploy is idempotent.
|
|
28
|
+
|
|
29
|
+
### Opening the admin origin redirects to `/admin`
|
|
30
|
+
|
|
31
|
+
Expected. `GET /` on the admin Worker answers 302 to `/admin`; the back office lives under that path. Nothing to fix.
|
|
32
|
+
|
|
33
|
+
## Publishing
|
|
34
|
+
|
|
35
|
+
### "build number … is not a positive integer"
|
|
36
|
+
|
|
37
|
+
The app's `CFBundleVersion` is not an integer (for example `0.0.3` or a git hash). It becomes `build_number`, which Sparkle compares numerically, so the server requires an integer. Fix the build so `CFBundleVersion` is a monotonic integer, or override for this publish:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
./publish.sh MyApp.dmg --build-number <n>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `publish.sh` reads the wrong or old version
|
|
44
|
+
|
|
45
|
+
The script reads the app inside the artifact and prints what it read. Two causes:
|
|
46
|
+
|
|
47
|
+
- The app inside the DMG is a symlink. The script refuses to follow it — it would read your installed copy instead. Build the DMG with a real copy of the app.
|
|
48
|
+
- The DMG was built from a stale app. Rebuild the DMG.
|
|
49
|
+
|
|
50
|
+
### "Build number N already exists"
|
|
51
|
+
|
|
52
|
+
`build_number` is unique and permanent — it must increase every release anyway. Publish with a higher number.
|
|
53
|
+
|
|
54
|
+
### HTTP 302, or "Access rejected the service token"
|
|
55
|
+
|
|
56
|
+
Access redirected the request to its login page instead of admitting the service token. An email one-time-PIN policy alone does not admit service tokens.
|
|
57
|
+
|
|
58
|
+
- Add a second policy on the admin Access application with action **Service Auth** that includes your token.
|
|
59
|
+
- If the stored credentials are wrong, re-enter them with `./publish.sh <artifact> --reset-token`.
|
|
60
|
+
- Confirm the token lives in the same Cloudflare account as the Access application.
|
|
61
|
+
|
|
62
|
+
The service-token setup is covered in [Publish](../operate/publish.md).
|
|
63
|
+
|
|
64
|
+
### `hdiutil: Resource busy`
|
|
65
|
+
|
|
66
|
+
Normally handled — `publish.sh` mounts the DMG at a random mount point. If you mounted the DMG manually, eject it first.
|
|
67
|
+
|
|
68
|
+
## The feed and testers
|
|
69
|
+
|
|
70
|
+
### A user gets no updates
|
|
71
|
+
|
|
72
|
+
Check these in order:
|
|
73
|
+
|
|
74
|
+
- **No channel anywhere.** A user with no channel receives nothing, and a build linked to no channel is served to no one through channel resolution (a pin can still serve it). Attach both to a channel — see [Channels](../operate/channels.md).
|
|
75
|
+
- **Pinned below what they run.** Sparkle cannot downgrade — an item below the installed version is never offered. A pin below the installed build never lands — the pin overrides the channel, and Sparkle ignores the lower item. Roll forward: republish that code under a higher build number and re-pin, or unpin so channel resolution resumes.
|
|
76
|
+
- **Revoked token.** The feed serves an informational "Access renewal" notice instead of an update. Once you reissue and the user re-activates, updates resume without a reinstall.
|
|
77
|
+
|
|
78
|
+
### `/get` returns 404
|
|
79
|
+
|
|
80
|
+
An unknown or revoked token gets An unknown, malformed, or revoked token gets the identical generic 404.. **This is by design** — the response reveals nothing about whether a token exists. Compare the token against the link on that user's page in the admin; a revoked user needs a reissued link.
|
|
81
|
+
|
|
82
|
+
## Local dev
|
|
83
|
+
|
|
84
|
+
### Local state is stuck (migrations fail, stale data)
|
|
85
|
+
|
|
86
|
+
Wipe the local D1/R2 state and start clean:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
./deploy/dev.sh --reset
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When local migrations fail, the CLI prints this hint itself: retry with `--reset` — a stuck local state can block them.
|
|
93
|
+
|
|
94
|
+
### "port 8787 is already in use"
|
|
95
|
+
|
|
96
|
+
An orphaned `workerd` from a prior Ctrl-C'd run usually still holds the port; wrangler does not report the collision itself, so the CLI checks before starting. Free the port or pick another:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pkill -f workerd
|
|
100
|
+
# or
|
|
101
|
+
./deploy/dev.sh --port <n>
|
|
102
|
+
```
|