bleam 0.0.10 → 0.0.11
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/dist/ai.cjs +608 -59
- package/dist/ai.d.cts +217 -89
- package/dist/ai.d.ts +217 -89
- package/dist/ai.js +601 -59
- package/dist/cli.cjs +3 -183
- package/dist/cli.d.cts +2 -46
- package/dist/cli.d.ts +2 -46
- package/dist/cli.js +3 -183
- package/dist/files.d.cts +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/platform.cjs +1 -13
- package/dist/platform.d.cts +1 -28
- package/dist/platform.d.ts +1 -28
- package/dist/platform.js +1 -10
- package/dist/schema.d.cts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/state.d.cts +1 -1
- package/dist/state.d.ts +1 -1
- package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
- package/dist/{ui-Dd7SXdbg.d.cts → ui-CHc4xEs_.d.ts} +7 -7
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.cts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +1 -1
- package/templates/image-generation/app/index.tsx +3 -1
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
- package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -122
- package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
- package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
- package/templates/native/ios/GenerationService/main.swift +174 -48
- package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
- package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
- package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
- package/templates/text-generation/app/index.tsx +93 -0
- package/templates/text-generation/app.config.ts +5 -0
- package/templates/updates/README.md +43 -199
- package/templates/updates/src/index.ts +5 -98
- package/templates/updates/src/schema.ts +18 -173
- package/templates/native/ios/PlatformHelper/main.swift +0 -726
- /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
- /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
- /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
- /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { bonsai, prepareTextModel, stream } from 'bleam/ai'
|
|
2
|
+
import { sx } from 'bleam/styles'
|
|
3
|
+
import { Button } from 'bleam/ui'
|
|
4
|
+
import { Window } from 'bleam/window'
|
|
5
|
+
import { useRef, useState } from 'react'
|
|
6
|
+
import { Text, TextInput, View } from 'react-native'
|
|
7
|
+
|
|
8
|
+
const appName = 'Bleam Basic'
|
|
9
|
+
const model = bonsai('ternary-bonsai-4b')
|
|
10
|
+
|
|
11
|
+
export default function App() {
|
|
12
|
+
const [prompt, setPrompt] = useState(
|
|
13
|
+
'Rewrite this note so it is clear and concise: We should probably meet sometime next week to discuss the launch.',
|
|
14
|
+
)
|
|
15
|
+
const [answer, setAnswer] = useState('Runs privately on this Mac.')
|
|
16
|
+
const [status, setStatus] = useState('Ready')
|
|
17
|
+
const [isRunning, setIsRunning] = useState(false)
|
|
18
|
+
const controller = useRef<AbortController>(null)
|
|
19
|
+
|
|
20
|
+
async function run() {
|
|
21
|
+
if (controller.current) return
|
|
22
|
+
const nextController = new AbortController()
|
|
23
|
+
controller.current = nextController
|
|
24
|
+
setIsRunning(true)
|
|
25
|
+
setAnswer('')
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await prepareTextModel(model, {
|
|
29
|
+
signal: nextController.signal,
|
|
30
|
+
onProgress(progress) {
|
|
31
|
+
setStatus(
|
|
32
|
+
`Preparing model ${Math.round(progress.modelFraction * 100)}%`,
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
setStatus('Generating')
|
|
37
|
+
for await (const text of stream({
|
|
38
|
+
model,
|
|
39
|
+
system: 'You are a concise private writing assistant.',
|
|
40
|
+
prompt,
|
|
41
|
+
signal: nextController.signal,
|
|
42
|
+
})) {
|
|
43
|
+
setAnswer(text)
|
|
44
|
+
}
|
|
45
|
+
setStatus('Complete')
|
|
46
|
+
} catch (error) {
|
|
47
|
+
setStatus(error instanceof Error ? error.message : String(error))
|
|
48
|
+
} finally {
|
|
49
|
+
if (controller.current === nextController) controller.current = null
|
|
50
|
+
setIsRunning(false)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<View style={sx('flex-1', 'p-8', 'gap-6')}>
|
|
56
|
+
<Window title={appName} />
|
|
57
|
+
<Text style={sx('text-3xl', 'font-semibold')}>
|
|
58
|
+
Private Writing Assistant
|
|
59
|
+
</Text>
|
|
60
|
+
<Text style={sx('text-secondary-label')}>
|
|
61
|
+
Ternary Bonsai runs locally after its first model download.
|
|
62
|
+
</Text>
|
|
63
|
+
<TextInput
|
|
64
|
+
multiline
|
|
65
|
+
value={prompt}
|
|
66
|
+
onChangeText={setPrompt}
|
|
67
|
+
style={sx(
|
|
68
|
+
'min-h-32',
|
|
69
|
+
'rounded-xl',
|
|
70
|
+
'border',
|
|
71
|
+
'border-separator',
|
|
72
|
+
'p-4',
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
<View style={sx('flex-row', 'gap-3')}>
|
|
76
|
+
<Button
|
|
77
|
+
title="Generate"
|
|
78
|
+
loading={isRunning}
|
|
79
|
+
disabled={isRunning}
|
|
80
|
+
onPress={() => void run()}
|
|
81
|
+
/>
|
|
82
|
+
<Button
|
|
83
|
+
title="Cancel"
|
|
84
|
+
variant="secondary"
|
|
85
|
+
disabled={!isRunning}
|
|
86
|
+
onPress={() => controller.current?.abort()}
|
|
87
|
+
/>
|
|
88
|
+
</View>
|
|
89
|
+
<Text style={sx('text-secondary-label')}>{status}</Text>
|
|
90
|
+
<Text style={sx('text-lg', 'text-label')}>{answer}</Text>
|
|
91
|
+
</View>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -1,152 +1,75 @@
|
|
|
1
|
-
# Bleam Update Service
|
|
1
|
+
# Bleam OTA Update Service
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
and
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
It has no deployment command, no Cloudflare account ID, no remote binding, no
|
|
8
|
-
publisher endpoint, and no authentication. Running it cannot create or mutate
|
|
9
|
-
remote Cloudflare resources.
|
|
3
|
+
This local-only Worker publishes and resolves OTA releases from an R2 simulation
|
|
4
|
+
and serves their artifacts through opaque Bleam download URLs. It has no
|
|
5
|
+
deployment command, Cloudflare account ID, remote binding, authentication, or
|
|
6
|
+
remote publisher endpoint. Running it cannot mutate remote resources.
|
|
10
7
|
|
|
11
8
|
## Local Development
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
owning dev session exits. If a healthy Worker is already running, it is reused
|
|
17
|
-
and left running.
|
|
18
|
-
|
|
19
|
-
Local R2 state and Worker logs persist globally at:
|
|
10
|
+
`bleam dev` normally starts the packaged Worker at `http://127.0.0.1:8787`,
|
|
11
|
+
waits for `/v1/health`, and stops it with the owning dev session. Local state and
|
|
12
|
+
logs persist at:
|
|
20
13
|
|
|
21
|
-
```
|
|
14
|
+
```text
|
|
22
15
|
~/Library/Caches/bleam/updates/state
|
|
23
16
|
~/Library/Caches/bleam/updates/worker.log
|
|
24
17
|
```
|
|
25
18
|
|
|
26
|
-
Use Wrangler directly only while developing
|
|
27
|
-
|
|
19
|
+
Use Wrangler directly only while developing this service. Wrangler 4.110.0 or
|
|
20
|
+
newer is required:
|
|
28
21
|
|
|
29
22
|
```sh
|
|
30
23
|
wrangler dev --config services/updates/wrangler.jsonc --local \
|
|
31
24
|
--persist-to services/updates/.wrangler/state
|
|
32
25
|
```
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
and is ignored by Git. Seed repository-local artifacts with Wrangler, always
|
|
36
|
-
passing both `--local` and the same `--persist-to` directory:
|
|
37
|
-
|
|
38
|
-
```sh
|
|
39
|
-
wrangler r2 object put bleam-updates-local/releases/com.example.notes/ota/0.0.10.json \
|
|
40
|
-
--file release.json \
|
|
41
|
-
--local \
|
|
42
|
-
--persist-to services/updates/.wrangler/state \
|
|
43
|
-
--config services/updates/wrangler.jsonc
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Run the end-to-end local proof with:
|
|
27
|
+
Run the seeded R2 resolver and download smoke test with:
|
|
47
28
|
|
|
48
29
|
```sh
|
|
49
30
|
sh services/updates/scripts/smoke.sh
|
|
50
31
|
```
|
|
51
32
|
|
|
52
|
-
|
|
53
|
-
temporary local R2 state directory, starts Wrangler on `127.0.0.1:8791`, and
|
|
54
|
-
verifies both resolver routes, opaque artifact downloads, response hashes and
|
|
55
|
-
sizes, and a missing-release `404`.
|
|
56
|
-
|
|
57
|
-
Run the real CLI publication proof with:
|
|
33
|
+
Run the real `bleam publish ota` publication and download loop with:
|
|
58
34
|
|
|
59
35
|
```sh
|
|
60
36
|
sh services/updates/scripts/ota-e2e.sh
|
|
61
37
|
```
|
|
62
38
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Publish a signed full-app platform archive from a configured project with:
|
|
68
|
-
|
|
69
|
-
```sh
|
|
70
|
-
bleam publish platform --service-url http://127.0.0.1:8787
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
This runs the standalone Release build, packages the signed app as
|
|
74
|
-
`bleam-macos-app-zip-v1`, validates the declared bytes and SHA-256 through the
|
|
75
|
-
local service, and activates the stable `macos-arm64` resolver record.
|
|
76
|
-
|
|
77
|
-
With a configured Apple development team, run the full standalone lifecycle:
|
|
39
|
+
With a configured Apple development team, the installed-app harness validates
|
|
40
|
+
next-launch OTA activation, successful startup confirmation, rejection of a
|
|
41
|
+
hash-valid broken bundle, and fallback to the embedded bundle:
|
|
78
42
|
|
|
79
43
|
```sh
|
|
80
44
|
sh services/updates/scripts/ota-installed-e2e.sh
|
|
81
45
|
```
|
|
82
46
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
the app's per-user helper remains registered with launchd after the app quits.
|
|
87
|
-
Finally, it publishes the signed app as a platform release, verifies and caches
|
|
88
|
-
it through the dedicated helper, applies Update and Relaunch, checks the rollback
|
|
89
|
-
backup and replacement receipt, validates the installed app and helper
|
|
90
|
-
signatures, and confirms a moved copy cannot self-update.
|
|
91
|
-
|
|
92
|
-
Before a release, run the same lifecycle with clean-state preflight and cleanup
|
|
93
|
-
assertions:
|
|
94
|
-
|
|
95
|
-
```sh
|
|
96
|
-
sh services/updates/scripts/platform-release-readiness.sh
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
This refuses existing state for the isolated test identity and verifies the app,
|
|
100
|
-
preferences, caches, helper copy, LaunchAgent, launchd registration, and update
|
|
101
|
-
receipts are absent again after the lifecycle. A release still requires this
|
|
102
|
-
check on a separate clean Mac; the local harness cannot substitute for fresh
|
|
103
|
-
hardware and user-account validation.
|
|
47
|
+
The installed-app harness builds and launches an isolated signed app. It is not
|
|
48
|
+
part of the normal service test suite and should only be run when native testing
|
|
49
|
+
is intended.
|
|
104
50
|
|
|
105
|
-
##
|
|
51
|
+
## API
|
|
106
52
|
|
|
107
53
|
```text
|
|
54
|
+
GET /v1/health
|
|
108
55
|
GET /v1/apps/:bundleIdentifier/ota/:runtimeVersion
|
|
109
|
-
GET /v1/apps/:bundleIdentifier/platform/macos-arm64
|
|
110
56
|
GET /v1/downloads/:downloadId
|
|
111
57
|
POST /v1/local/releases/ota
|
|
112
|
-
POST /v1/local/releases/platform
|
|
113
58
|
```
|
|
114
59
|
|
|
115
|
-
The POST
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
resolver record.
|
|
119
|
-
|
|
120
|
-
`bundleIdentifier` is the Bleam app identity. A release manifest always binds
|
|
121
|
-
it to the Apple `teamIdentifier`; the Worker validates both from the stored
|
|
122
|
-
record before it responds.
|
|
60
|
+
The POST route accepts only `localhost` requests. It validates the manifest,
|
|
61
|
+
artifact byte size, and SHA-256 before writing the immutable artifact and
|
|
62
|
+
activating its runtime-specific resolver record.
|
|
123
63
|
|
|
124
|
-
Resolver responses
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
caching. The R2 object key is present only in the internal download record.
|
|
64
|
+
Resolver responses use `Cache-Control: no-store`. Opaque download IDs are
|
|
65
|
+
immutable, so artifact responses use one-year immutable caching. R2 object keys
|
|
66
|
+
remain private in internal download records.
|
|
128
67
|
|
|
129
68
|
## Manifest Contract
|
|
130
69
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
replacing the installed `.app`.
|
|
135
|
-
- A **platform update** replaces the complete signed `.app`, including the
|
|
136
|
-
Bleam runtime, helper, native APIs, and native dependencies.
|
|
137
|
-
- The **runtime version** identifies the native environment required by an OTA
|
|
138
|
-
release. Compatibility is an exact string match.
|
|
139
|
-
- A **release manifest** describes one immutable artifact. A **resolver record**
|
|
140
|
-
selects the currently active release for an app, channel, and target.
|
|
141
|
-
- A release is **staged** after its artifact has been downloaded, verified, and
|
|
142
|
-
extracted. It becomes active on the next cold launch.
|
|
143
|
-
|
|
144
|
-
Both release forms use `schemaVersion: 1`, `channel: "stable"`, a unique
|
|
145
|
-
`releaseId`, `bundleIdentifier`, `teamIdentifier`, `publishedAt`, and an
|
|
146
|
-
artifact containing its format, opaque `downloadId`, exact byte size, lowercase
|
|
147
|
-
SHA-256 digest, and the resolver-provided download URL.
|
|
148
|
-
|
|
149
|
-
OTA releases add `bundleVersion` and an exact `runtimeVersion`:
|
|
70
|
+
An OTA release replaces JavaScript and ordinary application assets without
|
|
71
|
+
replacing the installed app. Its `runtimeVersion` is the exact native runtime it
|
|
72
|
+
requires.
|
|
150
73
|
|
|
151
74
|
```json
|
|
152
75
|
{
|
|
@@ -157,7 +80,7 @@ OTA releases add `bundleVersion` and an exact `runtimeVersion`:
|
|
|
157
80
|
"channel": "stable",
|
|
158
81
|
"releaseId": "ota_release_01",
|
|
159
82
|
"bundleVersion": "1.2.0+4",
|
|
160
|
-
"runtimeVersion": "0.0.
|
|
83
|
+
"runtimeVersion": "0.0.11",
|
|
161
84
|
"artifact": {
|
|
162
85
|
"format": "bleam-ota-zip-v1",
|
|
163
86
|
"downloadId": "ota_release_01",
|
|
@@ -169,104 +92,25 @@ OTA releases add `bundleVersion` and an exact `runtimeVersion`:
|
|
|
169
92
|
}
|
|
170
93
|
```
|
|
171
94
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```json
|
|
175
|
-
{
|
|
176
|
-
"schemaVersion": 1,
|
|
177
|
-
"kind": "platform",
|
|
178
|
-
"bundleIdentifier": "com.example.notes",
|
|
179
|
-
"teamIdentifier": "ABCDE12345",
|
|
180
|
-
"channel": "stable",
|
|
181
|
-
"releaseId": "platform_release_01",
|
|
182
|
-
"appVersion": "1.3.0",
|
|
183
|
-
"buildNumber": "5",
|
|
184
|
-
"bleamPlatformVersion": "0.0.10",
|
|
185
|
-
"platformFingerprint": "82a09a48ea84e66a65615e30f0f62b7c10f2d5137f4eafa67ca44d90bd9c8823",
|
|
186
|
-
"redeploy": false,
|
|
187
|
-
"target": {
|
|
188
|
-
"os": "macos",
|
|
189
|
-
"architecture": "arm64",
|
|
190
|
-
"installLocation": "user-applications",
|
|
191
|
-
"minimumMacOSVersion": "13.4"
|
|
192
|
-
},
|
|
193
|
-
"artifact": {
|
|
194
|
-
"format": "bleam-macos-app-zip-v1",
|
|
195
|
-
"downloadId": "platform_release_01",
|
|
196
|
-
"url": "http://127.0.0.1:8787/v1/downloads/platform_release_01",
|
|
197
|
-
"sizeBytes": 182743921,
|
|
198
|
-
"sha256": "ab851b776c2656b3725a31882ca8a9bf0e64ae508703a32cd4f8400eb1bdc6ec"
|
|
199
|
-
},
|
|
200
|
-
"publishedAt": "2026-07-12T16:00:00Z"
|
|
201
|
-
}
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Platform Feed And Archive
|
|
205
|
-
|
|
206
|
-
The initial platform feed is the stable, Apple-silicon resolver at
|
|
207
|
-
`GET /v1/apps/:bundleIdentifier/platform/macos-arm64`. It returns only releases
|
|
208
|
-
targeting `macos`, `arm64`, and the `user-applications` install location. The
|
|
209
|
-
helper must reject a release whose app version is not semantic, whose build
|
|
210
|
-
number is not decimal, or whose minimum macOS version exceeds the current OS.
|
|
211
|
-
|
|
212
|
-
`platformFingerprint` is a deterministic SHA-256 over Bleam runtime identity
|
|
213
|
-
and supported signed bundle inputs. It excludes developer TypeScript and
|
|
214
|
-
ordinary OTA assets. Local publication rejects an unchanged fingerprint unless
|
|
215
|
-
the manifest records an explicit `redeploy: true`; redeployment does not bypass
|
|
216
|
-
artifact or installed-app verification. Legacy manifests without a fingerprint
|
|
217
|
-
remain readable only so their next normal publication can establish one.
|
|
218
|
-
|
|
219
|
-
`bleam-macos-app-zip-v1` is a ZIP containing exactly one top-level `app/`
|
|
220
|
-
directory and exactly one complete `.app` beneath it:
|
|
221
|
-
|
|
222
|
-
```text
|
|
223
|
-
app/<display name>.app/
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
The archive must not contain another top-level entry, multiple app bundles,
|
|
227
|
-
absolute paths, or entries that escape `app/` through `..` or symlink traversal.
|
|
228
|
-
The helper extracts into the app-specific cache, never directly into
|
|
229
|
-
`~/Applications`, and treats the extracted bundle as untrusted until checkpoint
|
|
230
|
-
verification of SHA-256, bundle ID, team ID, app/build versions, architecture,
|
|
231
|
-
and code signature succeeds. The release ID and artifact download ID are
|
|
232
|
-
immutable; changing app bytes requires a new release and download ID.
|
|
233
|
-
|
|
234
|
-
The R2 layout is internal:
|
|
95
|
+
The stored manifest omits only the resolver-provided artifact URL. The internal
|
|
96
|
+
R2 layout is:
|
|
235
97
|
|
|
236
98
|
```text
|
|
237
99
|
releases/<bundleIdentifier>/ota/<runtimeVersion>.json
|
|
238
|
-
releases/<bundleIdentifier>/platform/macos-arm64.json
|
|
239
100
|
downloads/<downloadId>.json
|
|
240
|
-
artifacts/<
|
|
101
|
+
artifacts/ota/<downloadId>.zip
|
|
241
102
|
```
|
|
242
103
|
|
|
243
104
|
## Verification And Recovery
|
|
244
105
|
|
|
245
|
-
Publication
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
Both release types bind the artifact to a bundle identifier and Apple team
|
|
249
|
-
identifier. OTA releases additionally require the app's exact runtime version.
|
|
106
|
+
Publication verifies the declared artifact size and SHA-256 before activating a
|
|
107
|
+
resolver record. The app repeats both checks after download and requires exact
|
|
108
|
+
bundle identifier, Apple team identifier, and runtime version compatibility.
|
|
250
109
|
|
|
251
110
|
OTA bundles are cached under
|
|
252
|
-
`~/Library/Caches/Bleam/OTA/<bundleIdentifier>/<releaseId>/`.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
bridge runtime or root content appearing on Fabric. If the app exits or the
|
|
259
|
-
bundle fails before confirmation, the next launch rejects that release, clears
|
|
260
|
-
its active and attempted state, and loads the embedded `main.jsbundle`. A
|
|
261
|
-
rejected release is not selected again while it remains the resolver's active
|
|
262
|
-
release.
|
|
263
|
-
|
|
264
|
-
Platform artifacts are downloaded and applied by the per-app helper. It verifies
|
|
265
|
-
the artifact hash, archive layout, bundle and team identity, app and build
|
|
266
|
-
versions, Bleam runtime and platform fingerprint, arm64 architecture, code
|
|
267
|
-
signature, and minimum macOS version before replacement. Hosted authentication,
|
|
268
|
-
ownership, entitlement, and deployment remain separate follow-up work.
|
|
269
|
-
|
|
270
|
-
Standalone builds configured with `BLEAM_UPDATE_SERVICE_URL` check the matching
|
|
271
|
-
OTA resolver after React starts. A verified release is cached for the next cold
|
|
272
|
-
launch and follows the activation and recovery behavior above.
|
|
111
|
+
`~/Library/Caches/Bleam/OTA/<bundleIdentifier>/<releaseId>/`. A verified release
|
|
112
|
+
is selected only on a later cold launch and marked attempted before React Native
|
|
113
|
+
starts. After startup confirmation, the attempted state is cleared. If startup
|
|
114
|
+
fails first, the following launch rejects the release, clears its active state,
|
|
115
|
+
and loads the embedded bundle. A rejected release is not selected again while
|
|
116
|
+
it remains active in the resolver.
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
compareRuntimeVersions,
|
|
3
2
|
type DownloadRecord,
|
|
4
3
|
isBundleIdentifier,
|
|
5
4
|
isDownloadId,
|
|
6
5
|
isRuntimeVersion,
|
|
7
6
|
parseDownloadRecord,
|
|
8
|
-
parsePublishedRelease,
|
|
9
7
|
parseStoredRelease,
|
|
10
8
|
resolveRelease,
|
|
11
9
|
type StoredRelease,
|
|
@@ -89,11 +87,7 @@ async function sha256(bytes: Uint8Array) {
|
|
|
89
87
|
).join('')
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
async function publishLocalRelease(
|
|
93
|
-
request: Request,
|
|
94
|
-
bucket: UpdatesBucket,
|
|
95
|
-
expectedKind: StoredRelease['kind'],
|
|
96
|
-
) {
|
|
90
|
+
async function publishLocalRelease(request: Request, bucket: UpdatesBucket) {
|
|
97
91
|
if (!isLocalRequest(request)) return notFound(request.method)
|
|
98
92
|
|
|
99
93
|
let input: Record<string, unknown>
|
|
@@ -108,10 +102,7 @@ async function publishLocalRelease(
|
|
|
108
102
|
}
|
|
109
103
|
|
|
110
104
|
try {
|
|
111
|
-
const release =
|
|
112
|
-
if (release.kind !== expectedKind) {
|
|
113
|
-
return error('invalid_request', 400, request.method)
|
|
114
|
-
}
|
|
105
|
+
const release = parseStoredRelease(input.release)
|
|
115
106
|
const artifact = base64Bytes(input.artifactBase64)
|
|
116
107
|
if (artifact.byteLength !== release.artifact.sizeBytes) {
|
|
117
108
|
return error('invalid_request', 400, request.method)
|
|
@@ -120,37 +111,9 @@ async function publishLocalRelease(
|
|
|
120
111
|
return error('invalid_request', 400, request.method)
|
|
121
112
|
}
|
|
122
113
|
|
|
123
|
-
const releaseKey =
|
|
124
|
-
release.kind === 'ota'
|
|
125
|
-
? `releases/${release.bundleIdentifier}/ota/${release.runtimeVersion}.json`
|
|
126
|
-
: `releases/${release.bundleIdentifier}/platform/macos-arm64.json`
|
|
127
|
-
if (release.kind === 'platform') {
|
|
128
|
-
const currentValue = await readStoredJson(bucket, releaseKey)
|
|
129
|
-
const current = currentValue
|
|
130
|
-
? parseStoredRelease(currentValue)
|
|
131
|
-
: undefined
|
|
132
|
-
const unchanged =
|
|
133
|
-
current?.kind === 'platform' &&
|
|
134
|
-
current.platformFingerprint !== undefined &&
|
|
135
|
-
current.platformFingerprint === release.platformFingerprint
|
|
136
|
-
if (
|
|
137
|
-
current?.kind === 'platform' &&
|
|
138
|
-
compareRuntimeVersions(
|
|
139
|
-
release.bleamPlatformVersion,
|
|
140
|
-
current.bleamPlatformVersion,
|
|
141
|
-
) < 0
|
|
142
|
-
) {
|
|
143
|
-
return error('platform_downgrade', 409, request.method)
|
|
144
|
-
}
|
|
145
|
-
if (unchanged && !release.redeploy) {
|
|
146
|
-
return error('platform_unchanged', 409, request.method)
|
|
147
|
-
}
|
|
148
|
-
if (!unchanged && release.redeploy) {
|
|
149
|
-
return error('redeploy_not_required', 400, request.method)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
114
|
+
const releaseKey = `releases/${release.bundleIdentifier}/ota/${release.runtimeVersion}.json`
|
|
152
115
|
|
|
153
|
-
const artifactKey = `artifacts/${release.
|
|
116
|
+
const artifactKey = `artifacts/ota/${release.artifact.downloadId}.zip`
|
|
154
117
|
const download: DownloadRecord = {
|
|
155
118
|
schemaVersion: 1,
|
|
156
119
|
downloadId: release.artifact.downloadId,
|
|
@@ -246,38 +209,6 @@ async function resolveOta(
|
|
|
246
209
|
}
|
|
247
210
|
}
|
|
248
211
|
|
|
249
|
-
async function resolvePlatform(
|
|
250
|
-
bucket: UpdatesBucket,
|
|
251
|
-
request: Request,
|
|
252
|
-
bundleIdentifier: string,
|
|
253
|
-
target: string,
|
|
254
|
-
method: string,
|
|
255
|
-
) {
|
|
256
|
-
if (!isBundleIdentifier(bundleIdentifier) || target !== 'macos-arm64') {
|
|
257
|
-
return notFound(method)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const stored = await readStoredJson(
|
|
261
|
-
bucket,
|
|
262
|
-
`releases/${bundleIdentifier}/platform/${target}.json`,
|
|
263
|
-
)
|
|
264
|
-
if (!stored) return notFound(method)
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
const release = parseStoredRelease(stored)
|
|
268
|
-
if (
|
|
269
|
-
release.kind !== 'platform' ||
|
|
270
|
-
release.bundleIdentifier !== bundleIdentifier ||
|
|
271
|
-
`${release.target.os}-${release.target.architecture}` !== target
|
|
272
|
-
) {
|
|
273
|
-
return unavailable(method)
|
|
274
|
-
}
|
|
275
|
-
return releaseResponse(release, request, method)
|
|
276
|
-
} catch {
|
|
277
|
-
return unavailable(method)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
212
|
async function downloadArtifact(
|
|
282
213
|
bucket: UpdatesBucket,
|
|
283
214
|
downloadId: string,
|
|
@@ -322,13 +253,7 @@ export default {
|
|
|
322
253
|
request.method === 'POST' &&
|
|
323
254
|
url.pathname === '/v1/local/releases/ota'
|
|
324
255
|
) {
|
|
325
|
-
return publishLocalRelease(request, env.UPDATES
|
|
326
|
-
}
|
|
327
|
-
if (
|
|
328
|
-
request.method === 'POST' &&
|
|
329
|
-
url.pathname === '/v1/local/releases/platform'
|
|
330
|
-
) {
|
|
331
|
-
return publishLocalRelease(request, env.UPDATES, 'platform')
|
|
256
|
+
return publishLocalRelease(request, env.UPDATES)
|
|
332
257
|
}
|
|
333
258
|
|
|
334
259
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
@@ -356,24 +281,6 @@ export default {
|
|
|
356
281
|
)
|
|
357
282
|
}
|
|
358
283
|
|
|
359
|
-
if (
|
|
360
|
-
path.length === 5 &&
|
|
361
|
-
path[0] === 'v1' &&
|
|
362
|
-
path[1] === 'apps' &&
|
|
363
|
-
path[3] === 'platform'
|
|
364
|
-
) {
|
|
365
|
-
const bundleIdentifier = decodedSegment(path[2])
|
|
366
|
-
const target = decodedSegment(path[4])
|
|
367
|
-
if (!bundleIdentifier || !target) return notFound(method)
|
|
368
|
-
return resolvePlatform(
|
|
369
|
-
env.UPDATES,
|
|
370
|
-
request,
|
|
371
|
-
bundleIdentifier,
|
|
372
|
-
target,
|
|
373
|
-
method,
|
|
374
|
-
)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
284
|
if (path.length === 3 && path[0] === 'v1' && path[1] === 'downloads') {
|
|
378
285
|
const downloadId = decodedSegment(path[2])
|
|
379
286
|
if (!downloadId) return notFound(method)
|