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.
Files changed (68) hide show
  1. package/dist/ai.cjs +608 -59
  2. package/dist/ai.d.cts +217 -89
  3. package/dist/ai.d.ts +217 -89
  4. package/dist/ai.js +601 -59
  5. package/dist/cli.cjs +142 -94
  6. package/dist/cli.d.cts +11 -27
  7. package/dist/cli.d.ts +11 -27
  8. package/dist/cli.js +143 -95
  9. package/dist/files.d.cts +1 -1
  10. package/dist/files.d.ts +1 -1
  11. package/dist/platform.cjs +1 -13
  12. package/dist/platform.d.cts +1 -26
  13. package/dist/platform.d.ts +1 -26
  14. package/dist/platform.js +1 -10
  15. package/dist/schema.d.cts +1 -1
  16. package/dist/schema.d.ts +1 -1
  17. package/dist/state.d.cts +1 -1
  18. package/dist/state.d.ts +1 -1
  19. package/dist/{ui-Bg11tvlc.d.ts → ui-CHc4xEs_.d.ts} +7 -7
  20. package/dist/ui.d.ts +1 -1
  21. package/dist/window.d.ts +1 -1
  22. package/package.json +2 -1
  23. package/templates/image-generation/app/index.tsx +3 -1
  24. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  25. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  26. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  27. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  28. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  29. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  30. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  31. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  32. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  33. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  34. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  35. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  36. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  37. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  38. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  39. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  40. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  41. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  42. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  43. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  44. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  45. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  46. package/templates/native/ios/Bleam/AppDelegate.swift +0 -97
  47. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -121
  48. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  49. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  50. package/templates/native/ios/GenerationService/main.swift +174 -48
  51. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  52. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  53. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
  54. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  55. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -175
  56. package/templates/text-generation/app/index.tsx +93 -0
  57. package/templates/text-generation/app.config.ts +5 -0
  58. package/templates/updates/README.md +116 -0
  59. package/templates/updates/src/index.ts +292 -0
  60. package/templates/updates/src/schema.ts +230 -0
  61. package/templates/updates/tsconfig.json +12 -0
  62. package/templates/updates/wrangler.jsonc +19 -0
  63. package/templates/native/ios/PlatformHelper/Info.plist +0 -29
  64. package/templates/native/ios/PlatformHelper/main.swift +0 -335
  65. /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
  66. /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
  67. /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
  68. /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +0 -0
@@ -0,0 +1,5 @@
1
+ import { appConfig } from 'bleam'
2
+
3
+ export default appConfig({
4
+ name: 'Bleam Basic',
5
+ })
@@ -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,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "WebWorker"],
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["src"]
12
+ }
@@ -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>