@sigx/lynx-updates 0.8.0 → 0.9.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/LICENSE +21 -21
- package/README.md +199 -199
- package/android/com/sigx/updates/UpdateDownloader.kt +154 -154
- package/android/com/sigx/updates/UpdateStore.kt +367 -367
- package/android/com/sigx/updates/UpdatesActivityHook.kt +25 -25
- package/android/com/sigx/updates/UpdatesBundleResolver.kt +18 -18
- package/android/com/sigx/updates/UpdatesEventBus.kt +54 -54
- package/android/com/sigx/updates/UpdatesLifecyclePublisher.kt +42 -42
- package/android/com/sigx/updates/UpdatesModule.kt +235 -235
- package/ios/UpdateDownloader.swift +152 -152
- package/ios/UpdateStore.swift +286 -286
- package/ios/UpdatesBundleResolver.swift +15 -15
- package/ios/UpdatesEventBus.swift +59 -59
- package/ios/UpdatesLifecyclePublisher.swift +48 -48
- package/ios/UpdatesModule.swift +178 -178
- package/package.json +3 -3
- package/signalx-module.json +35 -35
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025-2026 Andreas Ekdahl
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Andreas Ekdahl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
# @sigx/lynx-updates
|
|
2
|
-
|
|
3
|
-
Over-the-air (OTA) bundle updates for sigx-lynx. Ship JS-only releases to
|
|
4
|
-
installed apps without a store round-trip — with pluggable backends, every
|
|
5
|
-
update mode from fully-automatic to fully-manual, and crash-driven rollback.
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pnpm add @sigx/lynx-updates
|
|
9
|
-
sigx prebuild # links the native module + bakes the runtime fingerprint
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Quick start
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
// src/main.tsx
|
|
16
|
-
import { defineApp } from '@sigx/lynx';
|
|
17
|
-
import { Updates } from '@sigx/lynx-updates';
|
|
18
|
-
import App from './App';
|
|
19
|
-
|
|
20
|
-
Updates.configure({
|
|
21
|
-
provider: { url: 'https://cdn.example.com/myapp/production/manifest.json' },
|
|
22
|
-
mode: 'silent', // download now, apply on next launch
|
|
23
|
-
checkOn: ['launch', 'foreground'],
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
defineApp(<App />).mount(null);
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Publish an update:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
sigx build # produces dist/main.lynx.bundle
|
|
33
|
-
sigx updates:publish # writes updates-dist/production/{manifest.json, updates/<id>/...}
|
|
34
|
-
# upload updates-dist/production/ to any static host — done.
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Update modes
|
|
38
|
-
|
|
39
|
-
| Mode | Behavior |
|
|
40
|
-
|---|---|
|
|
41
|
-
| `'silent'` (default) | Auto check + download; the update applies on the next cold launch. |
|
|
42
|
-
| `'immediate'` | Auto check + download, then applies immediately via an in-place reload. |
|
|
43
|
-
| `'manual'` | Nothing automatic — drive `checkForUpdate()` / `download()` / `apply()` yourself. |
|
|
44
|
-
|
|
45
|
-
**Mandatory updates** (`mandatory: true` in the manifest, `--mandatory` on
|
|
46
|
-
publish) override every mode: `state.mandatory` becomes true (block the UI —
|
|
47
|
-
see `<UpdateGate>` in `@sigx/lynx-updates-ui`), and the update downloads and
|
|
48
|
-
applies automatically. Opt out with `honorMandatory: false`.
|
|
49
|
-
|
|
50
|
-
## Runtime-version compatibility
|
|
51
|
-
|
|
52
|
-
An OTA bundle can only run on a native binary that has the native modules it
|
|
53
|
-
expects. `sigx prebuild` computes a **runtime fingerprint** from the linked
|
|
54
|
-
native modules' source content, the Lynx SDK version and the scaffold
|
|
55
|
-
revision, and bakes it into the binary. `sigx updates:publish` stamps the
|
|
56
|
-
same fingerprint into the manifest, and the client refuses mismatches:
|
|
57
|
-
|
|
58
|
-
- Add/remove/update a native module package → new fingerprint → published
|
|
59
|
-
updates no longer match → ship a store release. The check surfaces this as
|
|
60
|
-
`{ type: 'incompatible' }` / the `incompatibleUpdate` event.
|
|
61
|
-
- JS-only changes (any lockstep release that doesn't touch native code) keep
|
|
62
|
-
the fingerprint stable — published updates stay valid.
|
|
63
|
-
- Prefer manual control? Pin it: `updates: { runtimeVersion: '1.0.0' }` in
|
|
64
|
-
`signalx.config.ts` (Expo-style — you own the compatibility guarantee).
|
|
65
|
-
|
|
66
|
-
After a store update, all downloaded OTA updates are dropped automatically
|
|
67
|
-
(the binary's fingerprint/versionCode no longer match the recorded state).
|
|
68
|
-
|
|
69
|
-
## Rollback safety
|
|
70
|
-
|
|
71
|
-
Updates commit in two phases. A downloaded update is *pending* until the app
|
|
72
|
-
signals a healthy boot via `markReady()` — called automatically just after
|
|
73
|
-
`Updates.configure()` (set `autoMarkReady: false` to gate on your own signal, e.g.
|
|
74
|
-
first screen rendered). If the app crashes before `markReady()` on
|
|
75
|
-
`rollback.maxFailedLaunches` consecutive launches (default 2), the native
|
|
76
|
-
side deletes the update and reverts to the previous bundle. Detect it:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
const { didRollBack } = await Updates.getCurrentlyRunning();
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## API
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
Updates.configure(config) // sync, idempotent — call before defineApp()
|
|
86
|
-
Updates.checkForUpdate() // → { type: 'update-available' | 'up-to-date' | 'incompatible', ... }
|
|
87
|
-
Updates.download(manifest?) // download + verify + stage for next launch
|
|
88
|
-
Updates.apply() // apply staged update NOW (in-place reload; only rejects)
|
|
89
|
-
Updates.markReady() // health signal — commits the pending update
|
|
90
|
-
Updates.getCurrentlyRunning() // { updateId, isEmbedded, isFirstLaunchAfterUpdate, didRollBack, ... }
|
|
91
|
-
Updates.clearUpdates() // back to the baked bundle on next launch
|
|
92
|
-
Updates.getState() / Updates.addListener(fn) / Updates.isAvailable()
|
|
93
|
-
useUpdates() // Computed<UpdatesState> for components
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
State machine: `idle → checking → up-to-date | available | incompatible`,
|
|
97
|
-
`available → downloading → ready → applying`; failures land in `error` and
|
|
98
|
-
every transition fires a typed `UpdatesEvent`.
|
|
99
|
-
|
|
100
|
-
## Self-hosted & authenticated backends
|
|
101
|
-
|
|
102
|
-
The built-in `StaticManifestProvider` works against more than a static CDN — you
|
|
103
|
-
don't need a custom backend just to point at a runtime-resolved host or attach a
|
|
104
|
-
short-lived token. Pass these on the provider shorthand (or to
|
|
105
|
-
`new StaticManifestProvider({ ... })`):
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
Updates.configure({
|
|
109
|
-
provider: {
|
|
110
|
-
// Host discovered after launch (sign-in, environment selection):
|
|
111
|
-
// a resolver runs before every check. Return a URL, or { url, headers }.
|
|
112
|
-
url: (ctx) => `${session.apiBase}/updates/${ctx.channel}/manifest.json`,
|
|
113
|
-
|
|
114
|
-
// Static headers, merged into BOTH the manifest fetch and the download.
|
|
115
|
-
headers: { Accept: 'application/json' },
|
|
116
|
-
|
|
117
|
-
// Per-request auth — inject (and refresh) a short-lived token. The
|
|
118
|
-
// returned headers are merged over `headers`. onBeforeCheck guards the
|
|
119
|
-
// manifest request; onBeforeDownload guards the bundle download.
|
|
120
|
-
onBeforeCheck: async (ctx) => ({ Authorization: `Bearer ${await auth.token()}` }),
|
|
121
|
-
onBeforeDownload: async (manifest, ctx) => ({ Authorization: `Bearer ${await auth.token()}` }),
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
- Relative `bundleUrl`s resolve against whatever URL the resolver returned for
|
|
127
|
-
that check, so per-environment hosts just work.
|
|
128
|
-
- The hooks run on **every** check/download — return a fresh token each time and
|
|
129
|
-
refresh inside the hook when it's near expiry.
|
|
130
|
-
|
|
131
|
-
**Replacing the provider at runtime.** `Updates.configure()` is idempotent: a
|
|
132
|
-
second call swaps the provider (and channel/mode) for subsequent checks without
|
|
133
|
-
re-running the launch check. Use it for a wholesale backend switch; for the
|
|
134
|
-
common "discover the host after sign-in" case prefer the `url` resolver above —
|
|
135
|
-
no re-wiring.
|
|
136
|
-
|
|
137
|
-
## Custom backends
|
|
138
|
-
|
|
139
|
-
The static-manifest provider is ~150 lines over `fetch`. Anything else —
|
|
140
|
-
signed manifests, staged rollout services, the Expo Updates protocol —
|
|
141
|
-
implements `UpdateProvider` in its own package, no core changes:
|
|
142
|
-
|
|
143
|
-
```ts
|
|
144
|
-
import type { UpdateProvider } from '@sigx/lynx-updates';
|
|
145
|
-
|
|
146
|
-
const myBackend: UpdateProvider = {
|
|
147
|
-
name: 'my-backend',
|
|
148
|
-
async checkForUpdate(ctx) {
|
|
149
|
-
// ctx: { platform, runtimeVersion, currentUpdateId, embeddedVersion, channel }
|
|
150
|
-
const res = await fetch(`https://updates.example.com/check`, { ... });
|
|
151
|
-
// normalize your protocol's answer to an UpdateManifest
|
|
152
|
-
return { type: 'update-available', manifest };
|
|
153
|
-
},
|
|
154
|
-
async resolveDownload(manifest) {
|
|
155
|
-
return { url: manifest.bundleUrl, sha256: manifest.sha256, headers: { Authorization: '…' } };
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
Updates.configure({ provider: myBackend });
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
The byte transfer always happens natively (streamed to disk with incremental
|
|
163
|
-
SHA-256 verification) — providers only decide *what* to download.
|
|
164
|
-
|
|
165
|
-
## Static manifest format
|
|
166
|
-
|
|
167
|
-
`sigx updates:publish` maintains this document; serve it from any static host:
|
|
168
|
-
|
|
169
|
-
```json
|
|
170
|
-
{
|
|
171
|
-
"schemaVersion": 1,
|
|
172
|
-
"updates": [{
|
|
173
|
-
"id": "a1b2c3d4e5f60718",
|
|
174
|
-
"version": "1.4.2",
|
|
175
|
-
"channel": "production",
|
|
176
|
-
"platforms": ["android"],
|
|
177
|
-
"runtimeVersion": "fp1-3aa01b2c44de9921",
|
|
178
|
-
"bundleUrl": "updates/a1b2c3d4e5f60718/main.lynx.bundle",
|
|
179
|
-
"sha256": "<64-hex>",
|
|
180
|
-
"mandatory": false,
|
|
181
|
-
"createdAt": "2026-06-12T10:00:00Z",
|
|
182
|
-
"metadata": { "releaseNotes": "Bug fixes." }
|
|
183
|
-
}]
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
One URL serves every channel/runtime version: old binaries keep matching
|
|
188
|
-
their entries while new binaries pick up new ones. `bundleUrl` may be
|
|
189
|
-
relative (resolved against the manifest URL).
|
|
190
|
-
|
|
191
|
-
## Notes
|
|
192
|
-
|
|
193
|
-
- **Dev builds**: when running from a dev server URL, OTA is inert (the dev
|
|
194
|
-
server owns the bundle). Baked-bundle debug runs DO consult the update
|
|
195
|
-
store, so rollback can be exercised locally.
|
|
196
|
-
- **Web**: no-ops gracefully — every API degrades like the other native
|
|
197
|
-
modules.
|
|
198
|
-
- Prebuilt UI (update prompt, blocking gate, progress, restart banner):
|
|
199
|
-
[`@sigx/lynx-updates-ui`](../lynx-updates-ui/README.md).
|
|
1
|
+
# @sigx/lynx-updates
|
|
2
|
+
|
|
3
|
+
Over-the-air (OTA) bundle updates for sigx-lynx. Ship JS-only releases to
|
|
4
|
+
installed apps without a store round-trip — with pluggable backends, every
|
|
5
|
+
update mode from fully-automatic to fully-manual, and crash-driven rollback.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @sigx/lynx-updates
|
|
9
|
+
sigx prebuild # links the native module + bakes the runtime fingerprint
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// src/main.tsx
|
|
16
|
+
import { defineApp } from '@sigx/lynx';
|
|
17
|
+
import { Updates } from '@sigx/lynx-updates';
|
|
18
|
+
import App from './App';
|
|
19
|
+
|
|
20
|
+
Updates.configure({
|
|
21
|
+
provider: { url: 'https://cdn.example.com/myapp/production/manifest.json' },
|
|
22
|
+
mode: 'silent', // download now, apply on next launch
|
|
23
|
+
checkOn: ['launch', 'foreground'],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
defineApp(<App />).mount(null);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Publish an update:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
sigx build # produces dist/main.lynx.bundle
|
|
33
|
+
sigx updates:publish # writes updates-dist/production/{manifest.json, updates/<id>/...}
|
|
34
|
+
# upload updates-dist/production/ to any static host — done.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Update modes
|
|
38
|
+
|
|
39
|
+
| Mode | Behavior |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `'silent'` (default) | Auto check + download; the update applies on the next cold launch. |
|
|
42
|
+
| `'immediate'` | Auto check + download, then applies immediately via an in-place reload. |
|
|
43
|
+
| `'manual'` | Nothing automatic — drive `checkForUpdate()` / `download()` / `apply()` yourself. |
|
|
44
|
+
|
|
45
|
+
**Mandatory updates** (`mandatory: true` in the manifest, `--mandatory` on
|
|
46
|
+
publish) override every mode: `state.mandatory` becomes true (block the UI —
|
|
47
|
+
see `<UpdateGate>` in `@sigx/lynx-updates-ui`), and the update downloads and
|
|
48
|
+
applies automatically. Opt out with `honorMandatory: false`.
|
|
49
|
+
|
|
50
|
+
## Runtime-version compatibility
|
|
51
|
+
|
|
52
|
+
An OTA bundle can only run on a native binary that has the native modules it
|
|
53
|
+
expects. `sigx prebuild` computes a **runtime fingerprint** from the linked
|
|
54
|
+
native modules' source content, the Lynx SDK version and the scaffold
|
|
55
|
+
revision, and bakes it into the binary. `sigx updates:publish` stamps the
|
|
56
|
+
same fingerprint into the manifest, and the client refuses mismatches:
|
|
57
|
+
|
|
58
|
+
- Add/remove/update a native module package → new fingerprint → published
|
|
59
|
+
updates no longer match → ship a store release. The check surfaces this as
|
|
60
|
+
`{ type: 'incompatible' }` / the `incompatibleUpdate` event.
|
|
61
|
+
- JS-only changes (any lockstep release that doesn't touch native code) keep
|
|
62
|
+
the fingerprint stable — published updates stay valid.
|
|
63
|
+
- Prefer manual control? Pin it: `updates: { runtimeVersion: '1.0.0' }` in
|
|
64
|
+
`signalx.config.ts` (Expo-style — you own the compatibility guarantee).
|
|
65
|
+
|
|
66
|
+
After a store update, all downloaded OTA updates are dropped automatically
|
|
67
|
+
(the binary's fingerprint/versionCode no longer match the recorded state).
|
|
68
|
+
|
|
69
|
+
## Rollback safety
|
|
70
|
+
|
|
71
|
+
Updates commit in two phases. A downloaded update is *pending* until the app
|
|
72
|
+
signals a healthy boot via `markReady()` — called automatically just after
|
|
73
|
+
`Updates.configure()` (set `autoMarkReady: false` to gate on your own signal, e.g.
|
|
74
|
+
first screen rendered). If the app crashes before `markReady()` on
|
|
75
|
+
`rollback.maxFailedLaunches` consecutive launches (default 2), the native
|
|
76
|
+
side deletes the update and reverts to the previous bundle. Detect it:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const { didRollBack } = await Updates.getCurrentlyRunning();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
Updates.configure(config) // sync, idempotent — call before defineApp()
|
|
86
|
+
Updates.checkForUpdate() // → { type: 'update-available' | 'up-to-date' | 'incompatible', ... }
|
|
87
|
+
Updates.download(manifest?) // download + verify + stage for next launch
|
|
88
|
+
Updates.apply() // apply staged update NOW (in-place reload; only rejects)
|
|
89
|
+
Updates.markReady() // health signal — commits the pending update
|
|
90
|
+
Updates.getCurrentlyRunning() // { updateId, isEmbedded, isFirstLaunchAfterUpdate, didRollBack, ... }
|
|
91
|
+
Updates.clearUpdates() // back to the baked bundle on next launch
|
|
92
|
+
Updates.getState() / Updates.addListener(fn) / Updates.isAvailable()
|
|
93
|
+
useUpdates() // Computed<UpdatesState> for components
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
State machine: `idle → checking → up-to-date | available | incompatible`,
|
|
97
|
+
`available → downloading → ready → applying`; failures land in `error` and
|
|
98
|
+
every transition fires a typed `UpdatesEvent`.
|
|
99
|
+
|
|
100
|
+
## Self-hosted & authenticated backends
|
|
101
|
+
|
|
102
|
+
The built-in `StaticManifestProvider` works against more than a static CDN — you
|
|
103
|
+
don't need a custom backend just to point at a runtime-resolved host or attach a
|
|
104
|
+
short-lived token. Pass these on the provider shorthand (or to
|
|
105
|
+
`new StaticManifestProvider({ ... })`):
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
Updates.configure({
|
|
109
|
+
provider: {
|
|
110
|
+
// Host discovered after launch (sign-in, environment selection):
|
|
111
|
+
// a resolver runs before every check. Return a URL, or { url, headers }.
|
|
112
|
+
url: (ctx) => `${session.apiBase}/updates/${ctx.channel}/manifest.json`,
|
|
113
|
+
|
|
114
|
+
// Static headers, merged into BOTH the manifest fetch and the download.
|
|
115
|
+
headers: { Accept: 'application/json' },
|
|
116
|
+
|
|
117
|
+
// Per-request auth — inject (and refresh) a short-lived token. The
|
|
118
|
+
// returned headers are merged over `headers`. onBeforeCheck guards the
|
|
119
|
+
// manifest request; onBeforeDownload guards the bundle download.
|
|
120
|
+
onBeforeCheck: async (ctx) => ({ Authorization: `Bearer ${await auth.token()}` }),
|
|
121
|
+
onBeforeDownload: async (manifest, ctx) => ({ Authorization: `Bearer ${await auth.token()}` }),
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- Relative `bundleUrl`s resolve against whatever URL the resolver returned for
|
|
127
|
+
that check, so per-environment hosts just work.
|
|
128
|
+
- The hooks run on **every** check/download — return a fresh token each time and
|
|
129
|
+
refresh inside the hook when it's near expiry.
|
|
130
|
+
|
|
131
|
+
**Replacing the provider at runtime.** `Updates.configure()` is idempotent: a
|
|
132
|
+
second call swaps the provider (and channel/mode) for subsequent checks without
|
|
133
|
+
re-running the launch check. Use it for a wholesale backend switch; for the
|
|
134
|
+
common "discover the host after sign-in" case prefer the `url` resolver above —
|
|
135
|
+
no re-wiring.
|
|
136
|
+
|
|
137
|
+
## Custom backends
|
|
138
|
+
|
|
139
|
+
The static-manifest provider is ~150 lines over `fetch`. Anything else —
|
|
140
|
+
signed manifests, staged rollout services, the Expo Updates protocol —
|
|
141
|
+
implements `UpdateProvider` in its own package, no core changes:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import type { UpdateProvider } from '@sigx/lynx-updates';
|
|
145
|
+
|
|
146
|
+
const myBackend: UpdateProvider = {
|
|
147
|
+
name: 'my-backend',
|
|
148
|
+
async checkForUpdate(ctx) {
|
|
149
|
+
// ctx: { platform, runtimeVersion, currentUpdateId, embeddedVersion, channel }
|
|
150
|
+
const res = await fetch(`https://updates.example.com/check`, { ... });
|
|
151
|
+
// normalize your protocol's answer to an UpdateManifest
|
|
152
|
+
return { type: 'update-available', manifest };
|
|
153
|
+
},
|
|
154
|
+
async resolveDownload(manifest) {
|
|
155
|
+
return { url: manifest.bundleUrl, sha256: manifest.sha256, headers: { Authorization: '…' } };
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
Updates.configure({ provider: myBackend });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The byte transfer always happens natively (streamed to disk with incremental
|
|
163
|
+
SHA-256 verification) — providers only decide *what* to download.
|
|
164
|
+
|
|
165
|
+
## Static manifest format
|
|
166
|
+
|
|
167
|
+
`sigx updates:publish` maintains this document; serve it from any static host:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"schemaVersion": 1,
|
|
172
|
+
"updates": [{
|
|
173
|
+
"id": "a1b2c3d4e5f60718",
|
|
174
|
+
"version": "1.4.2",
|
|
175
|
+
"channel": "production",
|
|
176
|
+
"platforms": ["android"],
|
|
177
|
+
"runtimeVersion": "fp1-3aa01b2c44de9921",
|
|
178
|
+
"bundleUrl": "updates/a1b2c3d4e5f60718/main.lynx.bundle",
|
|
179
|
+
"sha256": "<64-hex>",
|
|
180
|
+
"mandatory": false,
|
|
181
|
+
"createdAt": "2026-06-12T10:00:00Z",
|
|
182
|
+
"metadata": { "releaseNotes": "Bug fixes." }
|
|
183
|
+
}]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
One URL serves every channel/runtime version: old binaries keep matching
|
|
188
|
+
their entries while new binaries pick up new ones. `bundleUrl` may be
|
|
189
|
+
relative (resolved against the manifest URL).
|
|
190
|
+
|
|
191
|
+
## Notes
|
|
192
|
+
|
|
193
|
+
- **Dev builds**: when running from a dev server URL, OTA is inert (the dev
|
|
194
|
+
server owns the bundle). Baked-bundle debug runs DO consult the update
|
|
195
|
+
store, so rollback can be exercised locally.
|
|
196
|
+
- **Web**: no-ops gracefully — every API degrades like the other native
|
|
197
|
+
modules.
|
|
198
|
+
- Prebuilt UI (update prompt, blocking gate, progress, restart banner):
|
|
199
|
+
[`@sigx/lynx-updates-ui`](../lynx-updates-ui/README.md).
|