From f37673cad70e5de7671c6f81f1e8cd7dfb5e07d9 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Sun, 30 Aug 2026 20:16:12 +0100 Subject: [PATCH] feat(mobile): finalize XPREM OTA config with real app id and certificate Server registration is complete: app id 9a66f3c2-fda7-416f-87e2-6db833b32e9d, real code-signing certificate (public half), staging/production channels mapped server-side. Drops the app-id placeholder guard (cert guard stays) and re-includes certificate.pem past the root *.pem ignore rule, which had been silently excluding it. Co-Authored-By: Claude Fable 5 --- apps/mobile/app.config.js | 15 +++-------- apps/mobile/certs/.gitignore | 3 +++ apps/mobile/certs/certificate.pem | 18 +++++++++++++ docs/mobile-ota.md | 44 +++++++++++++++---------------- 4 files changed, 45 insertions(+), 35 deletions(-) create mode 100644 apps/mobile/certs/certificate.pem diff --git a/apps/mobile/app.config.js b/apps/mobile/app.config.js index 3bb6bae..761a5b2 100644 --- a/apps/mobile/app.config.js +++ b/apps/mobile/app.config.js @@ -23,11 +23,10 @@ const OTA_SERVER_URL = 'https://ota.apps.jbrunton.co.uk'; const OTA_MANIFEST_URL = `${OTA_SERVER_URL}/manifest`; /** - * XPREM application UUID, shown in the XPREM dashboard under App Info. - * PLACEHOLDER until the update server deployment reports the real value. + * XPREM application UUID for Sauci, shown in the XPREM dashboard under App + * Info. Not a secret; overridable for pointing a build at another app. */ -const OTA_APP_ID_PLACEHOLDER = 'REPLACE_WITH_XPREM_APP_ID'; -const OTA_APP_ID = process.env.XPREM_APP_ID || OTA_APP_ID_PLACEHOLDER; +const OTA_APP_ID = process.env.XPREM_APP_ID || '9a66f3c2-fda7-416f-87e2-6db833b32e9d'; /** Public code-signing certificate downloaded from the XPREM dashboard. */ const CODE_SIGNING_CERTIFICATE_PATH = './certs/certificate.pem'; @@ -50,14 +49,6 @@ const releaseChannel = process.env.RELEASE_CHANNEL || 'development'; const codeSigningDisabled = Boolean(process.env.DISABLE_CODE_SIGNING); function assertUpdateConfigIsReal() { - if (OTA_APP_ID === OTA_APP_ID_PLACEHOLDER) { - throw new Error( - `RELEASE_CHANNEL=${releaseChannel} requires a real XPREM app id. ` + - 'Set XPREM_APP_ID, or replace the placeholder in apps/mobile/app.config.js. ' + - 'See docs/mobile-ota.md.', - ); - } - const certificatePath = path.join(__dirname, CODE_SIGNING_CERTIFICATE_PATH); const certificate = fs.existsSync(certificatePath) ? fs.readFileSync(certificatePath, 'utf8') diff --git a/apps/mobile/certs/.gitignore b/apps/mobile/certs/.gitignore index 9c4f928..f69292b 100644 --- a/apps/mobile/certs/.gitignore +++ b/apps/mobile/certs/.gitignore @@ -1,3 +1,6 @@ # The update-signing private key belongs to the XPREM server, never to the app. private-key.pem *.key +# The public certificate must be committed despite the root *.pem rule: the +# expo-updates plugin embeds it into every build. +!certificate.pem diff --git a/apps/mobile/certs/certificate.pem b/apps/mobile/certs/certificate.pem new file mode 100644 index 0000000..dc93cd9 --- /dev/null +++ b/apps/mobile/certs/certificate.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIICzTCCAbWgAwIBAgIJAPrPjxnUOGisMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV +BAMTBVNhdWNpMB4XDTI2MDgzMDE4MTAzMFoXDTM2MDgzMDE4MTAzMFowEDEOMAwG +A1UEAxMFU2F1Y2kwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWiUac +8i9bIbNZjv8iunVr6kqlgb9vea98JHz/A83biyszg0+GpmLYsr4zV2F7A6RitiIx +SOML+wVedyD0F2quWccBGXinvMAds0TxgIOjQfP+8+VJ/duDjveHvn+OFW4s7XxW +SY7TK1ufDcPWo/f5XzfqtBveug49zv3/F3MwfmMUbEY/zvH7nayPGJNuSyf3wISn +JIt0BsHNhWykRXCzjaF2fy3nAebOyezbtbapCjUlxPENVtBCzBQ7C9kaQXCBw5mV +hxhJlOxHWXpHJgDLHpev8A/CWQRh67tsevjKi6R6SDlf0R0xSbDJTElx8goWOOQW +VO9aDqI+oH0KpWfnAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E +DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAyUc20bIRCxoBTJg84mPl +2nFDWSaAfzgo0K0b6x2T1suuq4LAOUgzBqJISrQAALd0besK1qUZSAMhjQu1wPZT +MHuWRMxQHfOkyHMYUfOoPMS4bE+4K0WjzfoVt3y1A+oxhcIAQUUGyNgNmebfEN07 +JlX0B8UI8aIMjyfpsIDgZx1piHKwittGy/V4MPOLbmT2BBwS9BELcmWE4CwWbfeo +JM4lXY0hk3M/GTstRYaUKiwBWEU+z1PdAmWAgrtxpVnXN+oaToWh0WbVkbF1K6r5 +3oBGFTL7kT+Wogg9GK04XoAREw0jZJ/k77oJx68lGVBNiB+Ue2KqX0H/0cf2CslY +Ug== +-----END CERTIFICATE----- diff --git a/docs/mobile-ota.md b/docs/mobile-ota.md index 9123085..2c41ec1 100644 --- a/docs/mobile-ota.md +++ b/docs/mobile-ota.md @@ -12,8 +12,10 @@ already installed build. ## Status -The client is wired but not yet live. Two values from the server deployment are -still outstanding; see [Pending server values](#pending-server-values). +The client is wired with the real server values (app id +`9a66f3c2-fda7-416f-87e2-6db833b32e9d` and the Sauci code-signing certificate, +SHA-256 fingerprint beginning `DF:C9:17:A9`). What remains is the native +prebuild and a store build; see [Server registration](#server-registration). ## How it works @@ -96,9 +98,10 @@ branch surfing, it would strip the header from every poll for the life of the install. `RELEASE_CHANNEL` is set per profile in `apps/mobile/eas.json`. The config throws -a descriptive error whenever `RELEASE_CHANNEL` is set but the app id or the -certificate is still a placeholder, so a misconfigured build fails at config -resolution rather than shipping a client that can never validate a manifest. +a descriptive error whenever `RELEASE_CHANNEL` is set but the committed +certificate is missing or is the placeholder, so a misconfigured build fails at +config resolution rather than shipping a client that can never validate a +manifest. ### Local development @@ -145,8 +148,8 @@ the request headers, the code-signing metadata, and the certificate contents int these files. Those values embed the real app id and certificate, so the native projects are deliberately left untouched until both land. -Once the pending values below are in place, regenerate and commit the native -projects as a reviewed source change: +With the real app id and certificate now committed, regenerate and commit the +native projects as a reviewed source change: ```bash cd apps/mobile @@ -222,24 +225,19 @@ A bad update that prevents the app from starting cannot be fixed over the air on devices that never complete a launch. Keep `fallbackToCacheTimeout` short and treat a startup-path change as store-release risk, not OTA risk. -## Pending server values +## Server registration -The client wiring is complete apart from two values owned by the XPREM server -deployment. Both are non-secret. +The Sauci app is registered on the XPREM server; the deployment itself is +documented in the homelab repo (`unraid/docs/xprem-ota.md`). -| Value | Where it goes | How to obtain it | -| --- | --- | --- | -| XPREM app id (UUID) | `OTA_APP_ID_PLACEHOLDER` in `apps/mobile/app.config.js`, or the `XPREM_APP_ID` env var | XPREM dashboard, App Info page for the Sauci app | -| Code-signing certificate (PEM) | Replace the whole of `apps/mobile/certs/certificate.pem` | XPREM dashboard App Info download, or `npx eoas generate-certs` | +| Value | State | +| --- | --- | +| App id | `9a66f3c2-fda7-416f-87e2-6db833b32e9d`, hardcoded in `apps/mobile/app.config.js` (non-secret) | +| Code-signing certificate | Committed at `apps/mobile/certs/certificate.pem` (public half; CN=Sauci, valid to 2036, SHA-256 fingerprint `DF:C9:17:A9:85:02:DD:EB:86:06:2B:64:8D:73:D2:8C:A2:AA:29:F0:AC:62:A8:76:62:A1:6B:A2:42:B0:3D:71`) | +| Signing private key | Never leaves the server: keys mode `database`, sealed in the server's Postgres under its master key | +| Publish token | `XPREM_SAUCI_API_KEY` in the homelab repo `.env`; export as `EOO_TOKEN` when publishing | +| Channels | `staging` and `production` created server-side, mapped 1:1 to branches of the same names. `development` intentionally has no server channel — dev clients run with `DISABLE_CODE_SIGNING=1` against the packager | -Also required, outside this repository: - -- An `EOO_TOKEN` publish token per environment, stored as a secret. -- `development`, `staging`, and `production` channels created in the dashboard - and mapped to branches of the same names. -- The `private-key.pem` counterpart of the certificate installed in the server key - store. - -After both values land: replace them, run the prebuild described in +Remaining before updates flow: run the prebuild described in [Native projects](#native-projects), commit the native diff, and cut a new store build. Only builds produced after that point can receive updates.