bleam 0.0.9 → 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 +142 -94
- package/dist/cli.d.cts +11 -27
- package/dist/cli.d.ts +11 -27
- package/dist/cli.js +143 -95
- 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 -26
- package/dist/platform.d.ts +1 -26
- 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-CHc4xEs_.d.ts} +7 -7
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +2 -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 -97
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -121
- 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 -175
- package/templates/text-generation/app/index.tsx +93 -0
- package/templates/text-generation/app.config.ts +5 -0
- package/templates/updates/README.md +116 -0
- package/templates/updates/src/index.ts +292 -0
- package/templates/updates/src/schema.ts +230 -0
- package/templates/updates/tsconfig.json +12 -0
- package/templates/updates/wrangler.jsonc +19 -0
- package/templates/native/ios/PlatformHelper/Info.plist +0 -29
- package/templates/native/ios/PlatformHelper/main.swift +0 -335
- /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,116 @@
|
|
|
1
|
+
# Bleam OTA Update Service
|
|
2
|
+
|
|
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.
|
|
7
|
+
|
|
8
|
+
## Local Development
|
|
9
|
+
|
|
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:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
~/Library/Caches/bleam/updates/state
|
|
16
|
+
~/Library/Caches/bleam/updates/worker.log
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Use Wrangler directly only while developing this service. Wrangler 4.110.0 or
|
|
20
|
+
newer is required:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
wrangler dev --config services/updates/wrangler.jsonc --local \
|
|
24
|
+
--persist-to services/updates/.wrangler/state
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run the seeded R2 resolver and download smoke test with:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
sh services/updates/scripts/smoke.sh
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run the real `bleam publish ota` publication and download loop with:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
sh services/updates/scripts/ota-e2e.sh
|
|
37
|
+
```
|
|
38
|
+
|
|
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:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
sh services/updates/scripts/ota-installed-e2e.sh
|
|
45
|
+
```
|
|
46
|
+
|
|
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.
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
GET /v1/health
|
|
55
|
+
GET /v1/apps/:bundleIdentifier/ota/:runtimeVersion
|
|
56
|
+
GET /v1/downloads/:downloadId
|
|
57
|
+
POST /v1/local/releases/ota
|
|
58
|
+
```
|
|
59
|
+
|
|
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.
|
|
63
|
+
|
|
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.
|
|
67
|
+
|
|
68
|
+
## Manifest Contract
|
|
69
|
+
|
|
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.
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"schemaVersion": 1,
|
|
77
|
+
"kind": "ota",
|
|
78
|
+
"bundleIdentifier": "com.example.notes",
|
|
79
|
+
"teamIdentifier": "ABCDE12345",
|
|
80
|
+
"channel": "stable",
|
|
81
|
+
"releaseId": "ota_release_01",
|
|
82
|
+
"bundleVersion": "1.2.0+4",
|
|
83
|
+
"runtimeVersion": "0.0.11",
|
|
84
|
+
"artifact": {
|
|
85
|
+
"format": "bleam-ota-zip-v1",
|
|
86
|
+
"downloadId": "ota_release_01",
|
|
87
|
+
"url": "http://127.0.0.1:8787/v1/downloads/ota_release_01",
|
|
88
|
+
"sizeBytes": 1843921,
|
|
89
|
+
"sha256": "b6d25f9f844da5e5d810501774e5949760a5a45f32e4d8f449905c61d6a8d58d"
|
|
90
|
+
},
|
|
91
|
+
"publishedAt": "2026-07-12T16:00:00Z"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The stored manifest omits only the resolver-provided artifact URL. The internal
|
|
96
|
+
R2 layout is:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
releases/<bundleIdentifier>/ota/<runtimeVersion>.json
|
|
100
|
+
downloads/<downloadId>.json
|
|
101
|
+
artifacts/ota/<downloadId>.zip
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Verification And Recovery
|
|
105
|
+
|
|
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.
|
|
109
|
+
|
|
110
|
+
OTA bundles are cached under
|
|
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.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DownloadRecord,
|
|
3
|
+
isBundleIdentifier,
|
|
4
|
+
isDownloadId,
|
|
5
|
+
isRuntimeVersion,
|
|
6
|
+
parseDownloadRecord,
|
|
7
|
+
parseStoredRelease,
|
|
8
|
+
resolveRelease,
|
|
9
|
+
type StoredRelease,
|
|
10
|
+
} from './schema'
|
|
11
|
+
|
|
12
|
+
interface StoredObject {
|
|
13
|
+
body: ReadableStream<Uint8Array>
|
|
14
|
+
httpEtag: string
|
|
15
|
+
size: number
|
|
16
|
+
text(): Promise<string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UpdatesBucket {
|
|
20
|
+
get(key: string): Promise<StoredObject | null>
|
|
21
|
+
put(
|
|
22
|
+
key: string,
|
|
23
|
+
value: string | ArrayBuffer,
|
|
24
|
+
options?: { httpMetadata?: { contentType: string } },
|
|
25
|
+
): Promise<unknown>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Env {
|
|
29
|
+
UPDATES: UpdatesBucket
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const jsonHeaders = {
|
|
33
|
+
'content-type': 'application/json; charset=utf-8',
|
|
34
|
+
'cache-control': 'no-store',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function json(value: unknown, status = 200, method = 'GET') {
|
|
38
|
+
return new Response(method === 'HEAD' ? null : JSON.stringify(value), {
|
|
39
|
+
status,
|
|
40
|
+
headers: jsonHeaders,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function error(code: string, status: number, method: string) {
|
|
45
|
+
return json({ error: { code } }, status, method)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function notFound(method: string) {
|
|
49
|
+
return error('not_found', 404, method)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function unavailable(method: string) {
|
|
53
|
+
return error('invalid_release', 500, method)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function methodNotAllowed() {
|
|
57
|
+
return new Response(null, {
|
|
58
|
+
status: 405,
|
|
59
|
+
headers: { allow: 'GET, HEAD' },
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isLocalRequest(request: Request) {
|
|
64
|
+
const hostname = new URL(request.url).hostname
|
|
65
|
+
return hostname === '127.0.0.1' || hostname === 'localhost'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function base64Bytes(value: string) {
|
|
69
|
+
const binary = atob(value)
|
|
70
|
+
const bytes = new Uint8Array(binary.length)
|
|
71
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
72
|
+
bytes[index] = binary.charCodeAt(index)
|
|
73
|
+
}
|
|
74
|
+
return bytes
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function sha256(bytes: Uint8Array) {
|
|
78
|
+
const digest = await crypto.subtle.digest(
|
|
79
|
+
'SHA-256',
|
|
80
|
+
bytes.buffer.slice(
|
|
81
|
+
bytes.byteOffset,
|
|
82
|
+
bytes.byteOffset + bytes.byteLength,
|
|
83
|
+
) as ArrayBuffer,
|
|
84
|
+
)
|
|
85
|
+
return Array.from(new Uint8Array(digest), (byte) =>
|
|
86
|
+
byte.toString(16).padStart(2, '0'),
|
|
87
|
+
).join('')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function publishLocalRelease(request: Request, bucket: UpdatesBucket) {
|
|
91
|
+
if (!isLocalRequest(request)) return notFound(request.method)
|
|
92
|
+
|
|
93
|
+
let input: Record<string, unknown>
|
|
94
|
+
try {
|
|
95
|
+
input = (await request.json()) as Record<string, unknown>
|
|
96
|
+
} catch {
|
|
97
|
+
return error('invalid_request', 400, request.method)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof input.artifactBase64 !== 'string') {
|
|
101
|
+
return error('invalid_request', 400, request.method)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const release = parseStoredRelease(input.release)
|
|
106
|
+
const artifact = base64Bytes(input.artifactBase64)
|
|
107
|
+
if (artifact.byteLength !== release.artifact.sizeBytes) {
|
|
108
|
+
return error('invalid_request', 400, request.method)
|
|
109
|
+
}
|
|
110
|
+
if ((await sha256(artifact)) !== release.artifact.sha256) {
|
|
111
|
+
return error('invalid_request', 400, request.method)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const releaseKey = `releases/${release.bundleIdentifier}/ota/${release.runtimeVersion}.json`
|
|
115
|
+
|
|
116
|
+
const artifactKey = `artifacts/ota/${release.artifact.downloadId}.zip`
|
|
117
|
+
const download: DownloadRecord = {
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
downloadId: release.artifact.downloadId,
|
|
120
|
+
artifactKey,
|
|
121
|
+
contentType: 'application/zip',
|
|
122
|
+
}
|
|
123
|
+
await bucket.put(artifactKey, artifact.buffer, {
|
|
124
|
+
httpMetadata: { contentType: 'application/zip' },
|
|
125
|
+
})
|
|
126
|
+
await bucket.put(
|
|
127
|
+
`downloads/${download.downloadId}.json`,
|
|
128
|
+
JSON.stringify(download),
|
|
129
|
+
{ httpMetadata: { contentType: 'application/json' } },
|
|
130
|
+
)
|
|
131
|
+
await bucket.put(releaseKey, JSON.stringify(release), {
|
|
132
|
+
httpMetadata: { contentType: 'application/json' },
|
|
133
|
+
})
|
|
134
|
+
return json(
|
|
135
|
+
resolveRelease(
|
|
136
|
+
release,
|
|
137
|
+
new URL(`/v1/downloads/${download.downloadId}`, request.url).toString(),
|
|
138
|
+
),
|
|
139
|
+
201,
|
|
140
|
+
)
|
|
141
|
+
} catch {
|
|
142
|
+
return error('invalid_request', 400, request.method)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function decodedSegment(value: string): string | undefined {
|
|
147
|
+
try {
|
|
148
|
+
return decodeURIComponent(value)
|
|
149
|
+
} catch {
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readStoredJson(bucket: UpdatesBucket, key: string) {
|
|
155
|
+
const object = await bucket.get(key)
|
|
156
|
+
if (!object) return undefined
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(await object.text()) as unknown
|
|
160
|
+
} catch {
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function releaseResponse(
|
|
166
|
+
release: StoredRelease,
|
|
167
|
+
request: Request,
|
|
168
|
+
method: string,
|
|
169
|
+
) {
|
|
170
|
+
const artifactUrl = new URL(
|
|
171
|
+
`/v1/downloads/${release.artifact.downloadId}`,
|
|
172
|
+
request.url,
|
|
173
|
+
).toString()
|
|
174
|
+
return json(resolveRelease(release, artifactUrl), 200, method)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function resolveOta(
|
|
178
|
+
bucket: UpdatesBucket,
|
|
179
|
+
request: Request,
|
|
180
|
+
bundleIdentifier: string,
|
|
181
|
+
runtimeVersion: string,
|
|
182
|
+
method: string,
|
|
183
|
+
) {
|
|
184
|
+
if (
|
|
185
|
+
!isBundleIdentifier(bundleIdentifier) ||
|
|
186
|
+
!isRuntimeVersion(runtimeVersion)
|
|
187
|
+
) {
|
|
188
|
+
return notFound(method)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stored = await readStoredJson(
|
|
192
|
+
bucket,
|
|
193
|
+
`releases/${bundleIdentifier}/ota/${runtimeVersion}.json`,
|
|
194
|
+
)
|
|
195
|
+
if (!stored) return notFound(method)
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const release = parseStoredRelease(stored)
|
|
199
|
+
if (
|
|
200
|
+
release.kind !== 'ota' ||
|
|
201
|
+
release.bundleIdentifier !== bundleIdentifier ||
|
|
202
|
+
release.runtimeVersion !== runtimeVersion
|
|
203
|
+
) {
|
|
204
|
+
return unavailable(method)
|
|
205
|
+
}
|
|
206
|
+
return releaseResponse(release, request, method)
|
|
207
|
+
} catch {
|
|
208
|
+
return unavailable(method)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function downloadArtifact(
|
|
213
|
+
bucket: UpdatesBucket,
|
|
214
|
+
downloadId: string,
|
|
215
|
+
method: string,
|
|
216
|
+
) {
|
|
217
|
+
if (!isDownloadId(downloadId)) return notFound(method)
|
|
218
|
+
|
|
219
|
+
const stored = await readStoredJson(bucket, `downloads/${downloadId}.json`)
|
|
220
|
+
if (!stored) return notFound(method)
|
|
221
|
+
|
|
222
|
+
let download: DownloadRecord
|
|
223
|
+
try {
|
|
224
|
+
download = parseDownloadRecord(stored)
|
|
225
|
+
} catch {
|
|
226
|
+
return unavailable(method)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (download.downloadId !== downloadId) return unavailable(method)
|
|
230
|
+
|
|
231
|
+
const artifact = await bucket.get(download.artifactKey)
|
|
232
|
+
if (!artifact) return notFound(method)
|
|
233
|
+
|
|
234
|
+
const headers = new Headers({
|
|
235
|
+
'content-type': download.contentType,
|
|
236
|
+
'cache-control': 'public, max-age=31536000, immutable',
|
|
237
|
+
etag: artifact.httpEtag,
|
|
238
|
+
'content-length': String(artifact.size),
|
|
239
|
+
})
|
|
240
|
+
return new Response(method === 'HEAD' ? null : artifact.body, {
|
|
241
|
+
status: 200,
|
|
242
|
+
headers,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export default {
|
|
247
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
248
|
+
const url = new URL(request.url)
|
|
249
|
+
if (request.method === 'GET' && url.pathname === '/v1/health') {
|
|
250
|
+
return json({ healthy: true })
|
|
251
|
+
}
|
|
252
|
+
if (
|
|
253
|
+
request.method === 'POST' &&
|
|
254
|
+
url.pathname === '/v1/local/releases/ota'
|
|
255
|
+
) {
|
|
256
|
+
return publishLocalRelease(request, env.UPDATES)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
260
|
+
return methodNotAllowed()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const path = url.pathname.split('/').filter(Boolean)
|
|
264
|
+
const method = request.method
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
path.length === 5 &&
|
|
268
|
+
path[0] === 'v1' &&
|
|
269
|
+
path[1] === 'apps' &&
|
|
270
|
+
path[3] === 'ota'
|
|
271
|
+
) {
|
|
272
|
+
const bundleIdentifier = decodedSegment(path[2])
|
|
273
|
+
const runtimeVersion = decodedSegment(path[4])
|
|
274
|
+
if (!bundleIdentifier || !runtimeVersion) return notFound(method)
|
|
275
|
+
return resolveOta(
|
|
276
|
+
env.UPDATES,
|
|
277
|
+
request,
|
|
278
|
+
bundleIdentifier,
|
|
279
|
+
runtimeVersion,
|
|
280
|
+
method,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (path.length === 3 && path[0] === 'v1' && path[1] === 'downloads') {
|
|
285
|
+
const downloadId = decodedSegment(path[2])
|
|
286
|
+
if (!downloadId) return notFound(method)
|
|
287
|
+
return downloadArtifact(env.UPDATES, downloadId, method)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return notFound(method)
|
|
291
|
+
},
|
|
292
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
export type ArtifactFormat = 'bleam-ota-zip-v1'
|
|
2
|
+
|
|
3
|
+
export interface StoredArtifact {
|
|
4
|
+
format: ArtifactFormat
|
|
5
|
+
downloadId: string
|
|
6
|
+
sizeBytes: number
|
|
7
|
+
sha256: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ResolvedArtifact extends StoredArtifact {
|
|
11
|
+
url: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OtaRelease {
|
|
15
|
+
schemaVersion: 1
|
|
16
|
+
kind: 'ota'
|
|
17
|
+
bundleIdentifier: string
|
|
18
|
+
teamIdentifier: string
|
|
19
|
+
channel: 'stable'
|
|
20
|
+
releaseId: string
|
|
21
|
+
bundleVersion: string
|
|
22
|
+
runtimeVersion: string
|
|
23
|
+
artifact: StoredArtifact & { format: 'bleam-ota-zip-v1' }
|
|
24
|
+
publishedAt: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type StoredRelease = OtaRelease
|
|
28
|
+
|
|
29
|
+
export type ResolvedRelease = Omit<OtaRelease, 'artifact'> & {
|
|
30
|
+
artifact: ResolvedArtifact
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DownloadRecord {
|
|
34
|
+
schemaVersion: 1
|
|
35
|
+
downloadId: string
|
|
36
|
+
artifactKey: string
|
|
37
|
+
contentType: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ManifestValidationError extends Error {
|
|
41
|
+
constructor(message: string) {
|
|
42
|
+
super(message)
|
|
43
|
+
this.name = 'ManifestValidationError'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function invalid(field: string, detail: string): never {
|
|
48
|
+
throw new ManifestValidationError(`${field} ${detail}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function record(value: unknown, field: string): Record<string, unknown> {
|
|
52
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
53
|
+
return invalid(field, 'must be an object')
|
|
54
|
+
}
|
|
55
|
+
return value as Record<string, unknown>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function string(value: unknown, field: string): string {
|
|
59
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
60
|
+
return invalid(field, 'must be a non-empty string')
|
|
61
|
+
}
|
|
62
|
+
return value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function exactString(value: unknown, field: string, expected: string): string {
|
|
66
|
+
const result = string(value, field)
|
|
67
|
+
if (result !== expected) {
|
|
68
|
+
return invalid(field, `must be ${JSON.stringify(expected)}`)
|
|
69
|
+
}
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function positiveInteger(value: unknown, field: string): number {
|
|
74
|
+
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
|
|
75
|
+
return invalid(field, 'must be a positive integer')
|
|
76
|
+
}
|
|
77
|
+
return value as number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function version(value: unknown, field: string): string {
|
|
81
|
+
const result = string(value, field)
|
|
82
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(result)) {
|
|
83
|
+
return invalid(field, 'must be a semantic version')
|
|
84
|
+
}
|
|
85
|
+
return result
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function publishedAt(value: unknown): string {
|
|
89
|
+
const result = string(value, 'publishedAt')
|
|
90
|
+
if (
|
|
91
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(result) ||
|
|
92
|
+
Number.isNaN(Date.parse(result))
|
|
93
|
+
) {
|
|
94
|
+
return invalid('publishedAt', 'must be an RFC 3339 UTC timestamp')
|
|
95
|
+
}
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isBundleIdentifier(value: string): boolean {
|
|
100
|
+
return /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*$/.test(value)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isRuntimeVersion(value: string): boolean {
|
|
104
|
+
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isDownloadId(value: string): boolean {
|
|
108
|
+
return /^[a-z][a-z0-9_-]{2,127}$/.test(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function releaseId(value: unknown): string {
|
|
112
|
+
const result = string(value, 'releaseId')
|
|
113
|
+
if (!isDownloadId(result)) {
|
|
114
|
+
return invalid(
|
|
115
|
+
'releaseId',
|
|
116
|
+
'must use lowercase letters, numbers, underscores, or hyphens',
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return result
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function bundleIdentifier(value: unknown): string {
|
|
123
|
+
const result = string(value, 'bundleIdentifier')
|
|
124
|
+
if (!isBundleIdentifier(result)) {
|
|
125
|
+
return invalid('bundleIdentifier', 'must be a bundle identifier')
|
|
126
|
+
}
|
|
127
|
+
return result
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function teamIdentifier(value: unknown): string {
|
|
131
|
+
const result = string(value, 'teamIdentifier')
|
|
132
|
+
if (!/^[A-Z0-9]{10}$/.test(result)) {
|
|
133
|
+
return invalid('teamIdentifier', 'must be a 10-character Apple team ID')
|
|
134
|
+
}
|
|
135
|
+
return result
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function artifact(value: unknown): StoredArtifact {
|
|
139
|
+
const input = record(value, 'artifact')
|
|
140
|
+
exactString(input.format, 'artifact.format', 'bleam-ota-zip-v1')
|
|
141
|
+
|
|
142
|
+
const downloadId = string(input.downloadId, 'artifact.downloadId')
|
|
143
|
+
if (!isDownloadId(downloadId)) {
|
|
144
|
+
return invalid(
|
|
145
|
+
'artifact.downloadId',
|
|
146
|
+
'must use lowercase letters, numbers, underscores, or hyphens',
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sha256 = string(input.sha256, 'artifact.sha256')
|
|
151
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)) {
|
|
152
|
+
return invalid('artifact.sha256', 'must be a lowercase SHA-256 hex digest')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
format: 'bleam-ota-zip-v1',
|
|
157
|
+
downloadId,
|
|
158
|
+
sizeBytes: positiveInteger(input.sizeBytes, 'artifact.sizeBytes'),
|
|
159
|
+
sha256,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function parseStoredRelease(value: unknown): StoredRelease {
|
|
164
|
+
const input = record(value, 'manifest')
|
|
165
|
+
if (input.schemaVersion !== 1) {
|
|
166
|
+
return invalid('schemaVersion', 'must be 1')
|
|
167
|
+
}
|
|
168
|
+
exactString(input.kind, 'kind', 'ota')
|
|
169
|
+
exactString(input.channel, 'channel', 'stable')
|
|
170
|
+
return {
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
kind: 'ota',
|
|
173
|
+
bundleIdentifier: bundleIdentifier(input.bundleIdentifier),
|
|
174
|
+
teamIdentifier: teamIdentifier(input.teamIdentifier),
|
|
175
|
+
channel: 'stable',
|
|
176
|
+
releaseId: releaseId(input.releaseId),
|
|
177
|
+
bundleVersion: string(input.bundleVersion, 'bundleVersion'),
|
|
178
|
+
runtimeVersion: version(input.runtimeVersion, 'runtimeVersion'),
|
|
179
|
+
artifact: artifact(input.artifact),
|
|
180
|
+
publishedAt: publishedAt(input.publishedAt),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function parseDownloadRecord(value: unknown): DownloadRecord {
|
|
185
|
+
const input = record(value, 'download record')
|
|
186
|
+
if (input.schemaVersion !== 1) {
|
|
187
|
+
return invalid('schemaVersion', 'must be 1')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const downloadId = string(input.downloadId, 'downloadId')
|
|
191
|
+
if (!isDownloadId(downloadId)) {
|
|
192
|
+
return invalid(
|
|
193
|
+
'downloadId',
|
|
194
|
+
'must use lowercase letters, numbers, underscores, or hyphens',
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const artifactKey = string(input.artifactKey, 'artifactKey')
|
|
199
|
+
if (
|
|
200
|
+
!/^artifacts\/[A-Za-z0-9._/-]+$/.test(artifactKey) ||
|
|
201
|
+
artifactKey.split('/').includes('..')
|
|
202
|
+
) {
|
|
203
|
+
return invalid('artifactKey', 'must be a private artifacts path')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const contentType = string(input.contentType, 'contentType')
|
|
207
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(contentType)) {
|
|
208
|
+
return invalid('contentType', 'must be a MIME type')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
schemaVersion: 1,
|
|
213
|
+
downloadId,
|
|
214
|
+
artifactKey,
|
|
215
|
+
contentType,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function resolveRelease(
|
|
220
|
+
release: StoredRelease,
|
|
221
|
+
artifactUrl: string,
|
|
222
|
+
): ResolvedRelease {
|
|
223
|
+
return {
|
|
224
|
+
...release,
|
|
225
|
+
artifact: {
|
|
226
|
+
...release.artifact,
|
|
227
|
+
url: artifactUrl,
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bleam-updates-local",
|
|
3
|
+
"main": "src/index.ts",
|
|
4
|
+
"compatibility_date": "2026-07-13",
|
|
5
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
6
|
+
"workers_dev": false,
|
|
7
|
+
"send_metrics": false,
|
|
8
|
+
"dev": {
|
|
9
|
+
"ip": "127.0.0.1",
|
|
10
|
+
"port": 8787,
|
|
11
|
+
"local_protocol": "http"
|
|
12
|
+
},
|
|
13
|
+
"r2_buckets": [
|
|
14
|
+
{
|
|
15
|
+
"binding": "UPDATES",
|
|
16
|
+
"bucket_name": "bleam-updates-local"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
-
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
7
|
-
<key>CFBundleExecutable</key>
|
|
8
|
-
<string>$(EXECUTABLE_NAME)</string>
|
|
9
|
-
<key>CFBundleIdentifier</key>
|
|
10
|
-
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
11
|
-
<key>CFBundleInfoDictionaryVersion</key>
|
|
12
|
-
<string>6.0</string>
|
|
13
|
-
<key>CFBundleName</key>
|
|
14
|
-
<string>$(PRODUCT_NAME)</string>
|
|
15
|
-
<key>CFBundlePackageType</key>
|
|
16
|
-
<string>XPC!</string>
|
|
17
|
-
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>$(MARKETING_VERSION)</string>
|
|
19
|
-
<key>CFBundleVersion</key>
|
|
20
|
-
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
21
|
-
<key>BleamPlatformVersion</key>
|
|
22
|
-
<string>0.0.9</string>
|
|
23
|
-
<key>XPCService</key>
|
|
24
|
-
<dict>
|
|
25
|
-
<key>ServiceType</key>
|
|
26
|
-
<string>Application</string>
|
|
27
|
-
</dict>
|
|
28
|
-
</dict>
|
|
29
|
-
</plist>
|