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 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Fable 5 committed 2026-08-30 20:16:12 +01:00
1 parent 0ada812294
commit f37673cad7
4 files changed
+45 -35

No files matched your search

+3 -12
View File
@@ -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')
+3
View File
@@ -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
+18
View File
@@ -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-----
+21 -23
View File
@@ -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.