@capuchoo/core 0.2.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -22,8 +22,20 @@ declare const UpdateMessage: {
22
22
  readonly NATIVE_UPDATE_REQUIRED: "native_update_required";
23
23
  /** A newer artefact is available. */
24
24
  readonly UPDATE_AVAILABLE: "update_available";
25
+ /**
26
+ * A newer native binary is available, but not mandatory.
27
+ *
28
+ * Distinct from UPDATE_AVAILABLE because the top-level `url` is deliberately
29
+ * absent: that field is the Capacitor plugin's OTA contract, and it
30
+ * auto-downloads whatever is there and unzips it. An APK in `url` made the
31
+ * plugin download 45 MB and fail, hiding the real update behind a download
32
+ * error. The binary is in `native_update` instead.
33
+ */
34
+ readonly NATIVE_UPDATE_AVAILABLE: "native_update_available";
25
35
  /** The device already runs the newest artefact for its channel. */
26
36
  readonly NO_UPDATE: "No update available";
37
+ /** No application carries the requesting bundle identifier. */
38
+ readonly APP_NOT_FOUND: "App not found";
27
39
  /** The channel name does not exist for this application. */
28
40
  readonly CHANNEL_NOT_FOUND: "Channel not found";
29
41
  /**
@@ -31,8 +43,39 @@ declare const UpdateMessage: {
31
43
  * staging build asking a production channel, for example.
32
44
  */
33
45
  readonly ENVIRONMENT_MISMATCH: "Environment mismatch";
46
+ /**
47
+ * The channel exists but points at no bundle, and PLATFORM_MISMATCH means it
48
+ * points at one built for another platform.
49
+ *
50
+ * Neither is actionable by the device, and both used to return a bare
51
+ * `{ config: {} }` - the same response as "you are up to date". Three
52
+ * different situations were indistinguishable on the wire, which is why an
53
+ * iOS device asking an Android-only channel produced silence rather than a
54
+ * diagnosis. Clients still take no action; the names exist so the answer to
55
+ * "why did nothing happen" is in the response.
56
+ */
57
+ readonly NO_BUNDLE: "No bundle assigned";
58
+ readonly PLATFORM_MISMATCH: "Platform mismatch";
34
59
  };
35
60
  type UpdateMessageValue = (typeof UpdateMessage)[keyof typeof UpdateMessage];
61
+ /**
62
+ * How the plugin classifies a response that carries no downloadable bundle.
63
+ *
64
+ * Read from `@capgo/capacitor-updater@7.50.2`, which is the authority here:
65
+ * `CapacitorUpdaterPlugin.normalizedUpdateResponseKind` (android, line 4333)
66
+ * maps anything that is not one of these three to `"failed"`, and the check
67
+ * path at line 4515 enters this branch whenever the response has *either* an
68
+ * `error` or a `kind` key.
69
+ *
70
+ * Two consequences the backend must respect, both of which it violated:
71
+ *
72
+ * 1. A response that carries an update must NOT set `kind`, or the plugin
73
+ * classifies it instead of downloading it.
74
+ * 2. A response that carries no update MUST set `kind`, or it is reported as a
75
+ * failed update check - which is where the app's "the update could not be
76
+ * downloaded" came from on a device that was simply up to date.
77
+ */
78
+ type UpdateResponseKind = "up_to_date" | "blocked" | "failed";
36
79
  /** What the device tells the server about itself. */
37
80
  interface UpdateCheckRequest {
38
81
  /** Bundle identifier of the running build, e.g. `com.ayb.lowmaro.staging`. */
@@ -85,6 +128,12 @@ interface NativeUpdatePayload {
85
128
  release_notes?: string;
86
129
  required?: boolean;
87
130
  platform?: Platform;
131
+ /**
132
+ * Size in bytes, so a client can warn before spending someone's mobile data
133
+ * on 45 MB. The column is `file_size_bytes`; the two were never mapped, so
134
+ * this was declared here and never once populated until `nativePayload`
135
+ * translated it.
136
+ */
88
137
  file_size?: number;
89
138
  }
90
139
  /**
@@ -95,8 +144,25 @@ interface NativeUpdatePayload {
95
144
  interface UpdateCheckResponse {
96
145
  message?: string;
97
146
  error?: string;
147
+ /**
148
+ * Classification for a response that carries no bundle. Absent - and it must
149
+ * be absent - when one is offered. See `UpdateResponseKind`.
150
+ */
151
+ kind?: UpdateResponseKind;
98
152
  /** OTA bundle fields. */
99
153
  version_name?: string;
154
+ /**
155
+ * The same value as `version_name`, under the name the Capacitor plugin
156
+ * reads.
157
+ *
158
+ * `CapacitorUpdaterPlugin` line 4551 calls `jsRes.getString("version")`
159
+ * unconditionally once a response is not classified, and a missing key throws
160
+ * a JSONException that is caught as "error in update check". The backend sent
161
+ * only `version_name`, so every background check the plugin made - on every
162
+ * response, including a perfectly good bundle - ended as a failed update. Our
163
+ * own runtime never noticed because it reads the response itself.
164
+ */
165
+ version?: string;
100
166
  url?: string;
101
167
  checksum?: string;
102
168
  sessionKey?: string;
@@ -119,6 +185,14 @@ interface ResolvedUpdate {
119
185
  platform?: Platform;
120
186
  checksum?: string;
121
187
  sessionKey?: string;
188
+ /**
189
+ * Size in bytes of a native binary, when the server published one.
190
+ *
191
+ * The runtime verifies a cached download against it before reusing the file:
192
+ * a connection dropped mid-download leaves a partial APK at the right path,
193
+ * and installing that fails with "There was a problem parsing the package".
194
+ */
195
+ fileSize?: number;
122
196
  /** Set once the OTA plugin has downloaded the bundle. */
123
197
  bundleId?: string;
124
198
  }
@@ -136,19 +210,60 @@ declare function resolveUpdate(response: UpdateCheckResponse | null | undefined)
136
210
  * Callers should surface these instead of showing "you are up to date".
137
211
  */
138
212
  declare function isBlockingResponse(response: UpdateCheckResponse): boolean;
139
- /** Analytics events posted to `POST {endpoint}/api/native-updates/log`. */
140
- type UpdateEvent = "check" | "download" | "download_complete" | "install" | "cancel" | "error";
213
+ /**
214
+ * Analytics events posted to `POST {endpoint}/api/native-updates/log`.
215
+ *
216
+ * A list rather than a bare union because `native_update_logs.event` carries a
217
+ * CHECK constraint, and the two had drifted: the column allowed `check`,
218
+ * `download`, `install`, `fail` and `skip` while this declared `check`,
219
+ * `download`, `download_complete`, `install`, `cancel` and `error`. Three of
220
+ * the six were rejected by the database, so a device reporting
221
+ * `download_complete` - which is what one does after every native download -
222
+ * got a 500. `native-update-events.test.ts` reads the migration and fails if
223
+ * this list ever moves ahead of it again.
224
+ */
225
+ declare const UPDATE_EVENTS: readonly ["check", "download", "download_complete", "install", "cancel", "error"];
226
+ type UpdateEvent = (typeof UPDATE_EVENTS)[number];
141
227
  interface UpdateEventPayload {
142
228
  event: UpdateEvent;
143
229
  platform: Platform;
230
+ /**
231
+ * Bundle identifier of the running build.
232
+ *
233
+ * `native_update_logs.app_id` is NOT NULL and the server cannot resolve a row
234
+ * without it, so it rejects a payload that omits this with a 400. This field
235
+ * was missing from the contract and from the app runtime, so **every** native
236
+ * download, install and error event was rejected - and because the runtime
237
+ * catches the failure and warns, nothing ever surfaced. It was found by
238
+ * reading the WebView console on a device mid-install.
239
+ */
240
+ app_id: string;
144
241
  device_id: string;
145
242
  current_version_code: number;
146
243
  new_version?: string;
147
244
  new_version_code?: number;
148
245
  channel: string;
149
246
  environment: string;
247
+ /** Failure detail for an `error` event. Older servers read `error_message`. */
150
248
  error?: string;
151
249
  }
250
+ /** Field names a payload must carry for the server to record it. */
251
+ declare const UPDATE_EVENT_REQUIRED: readonly ["event", "platform", "app_id"];
252
+ /**
253
+ * Validates an incoming update event, naming everything that is missing.
254
+ *
255
+ * Pure, and shared with the server, so "what the client sends" and "what the
256
+ * server accepts" cannot drift the way they did here: the client sent `error`
257
+ * and the server read `error_message`, so even a payload that got past
258
+ * validation lost its failure detail.
259
+ */
260
+ declare function parseUpdateEvent(body: Record<string, unknown>): {
261
+ ok: true;
262
+ event: UpdateEventPayload;
263
+ } | {
264
+ ok: false;
265
+ missing: string[];
266
+ };
152
267
  //#endregion
153
268
  //#region src/channel-environment.d.ts
154
269
  /**
@@ -176,6 +291,140 @@ declare function hasEnvironmentMismatch(name: string, environment: EnvironmentSe
176
291
  /** The warning to show for a mismatch, or `null` when there is nothing to warn about. */
177
292
  declare function environmentMismatchWarning(name: string, environment: EnvironmentSelection): string | null;
178
293
  //#endregion
294
+ //#region src/update-decision.d.ts
295
+ /** The build a device is running, as it reports itself. */
296
+ interface DeviceState {
297
+ /** Bundle identifier of the binary, which carries its environment suffix. */
298
+ appId: string;
299
+ platform: Platform;
300
+ /** Native build number. 0 when the device did not report one. */
301
+ versionCode: number;
302
+ /** Applied OTA bundle version, or `"builtin"` when none has landed. */
303
+ versionName: string;
304
+ }
305
+ interface ChannelState {
306
+ name: string;
307
+ environment: Environment;
308
+ }
309
+ /**
310
+ * A native binary row.
311
+ *
312
+ * `file_size_bytes` is the column name; the wire field is `file_size`. They
313
+ * were never mapped, so the contract's `file_size` has never once been
314
+ * populated - `renderUpdateResponse` is where that is now translated.
315
+ */
316
+ interface NativeRelease {
317
+ version_name: string;
318
+ version_code: number;
319
+ download_url: string;
320
+ platform: Platform;
321
+ required?: boolean | null;
322
+ release_notes?: string | null;
323
+ file_size_bytes?: number | null;
324
+ }
325
+ /** An OTA bundle row. `url` is already resolved to something downloadable. */
326
+ interface OtaRelease {
327
+ version_name: string;
328
+ url: string;
329
+ platform: Platform;
330
+ checksum?: string | null;
331
+ session_key?: string | null;
332
+ /** Native build number this bundle needs; below it, it must not be served. */
333
+ min_update_version?: string | number | null;
334
+ required?: boolean | null;
335
+ release_notes?: string | null;
336
+ }
337
+ /** Everything the server looked up. Facts only - no decisions. */
338
+ interface UpdateFacts {
339
+ device: DeviceState;
340
+ /** null when no app carries the requested bundle identifier. */
341
+ app: {
342
+ id: string;
343
+ } | null;
344
+ /** null when the app has no channel by the requested name. */
345
+ channel: ChannelState | null;
346
+ /** The native binary the channel points at, if any. */
347
+ native: NativeRelease | null;
348
+ /** The OTA bundle the channel points at, if any. */
349
+ ota: OtaRelease | null;
350
+ }
351
+ /**
352
+ * The closed set of outcomes.
353
+ *
354
+ * Every one is named, including the three that used to share a bare
355
+ * `{ config: {} }` response: a channel with no bundle, a bundle built for
356
+ * another platform, and a device already up to date were indistinguishable on
357
+ * the wire, so "the update did nothing" had no diagnosis.
358
+ */
359
+ type UpdateDecision = {
360
+ kind: "app-not-found";
361
+ } | {
362
+ kind: "channel-not-found";
363
+ } | {
364
+ kind: "environment-mismatch";
365
+ expected: Environment;
366
+ channel: ChannelState;
367
+ } | {
368
+ kind: "native";
369
+ release: NativeRelease;
370
+ } | {
371
+ kind: "native-required";
372
+ minVersionCode: number;
373
+ installedVersionCode: number;
374
+ } | {
375
+ kind: "ota";
376
+ release: OtaRelease;
377
+ } | {
378
+ kind: "no-bundle";
379
+ } | {
380
+ kind: "platform-mismatch";
381
+ bundlePlatform: Platform;
382
+ devicePlatform: Platform;
383
+ } | {
384
+ kind: "up-to-date";
385
+ version: string;
386
+ };
387
+ /**
388
+ * Decides what to serve.
389
+ *
390
+ * The order is load-bearing. Native comes before OTA because the server can
391
+ * have both, and applying a bundle to a binary too old to run it leaves the
392
+ * device broken with no way back. The environment check comes before either, so
393
+ * a staging build can never be handed a production bundle by asking for the
394
+ * wrong channel.
395
+ */
396
+ declare function decideUpdate(facts: UpdateFacts): UpdateDecision;
397
+ /**
398
+ * The wire fields of a native binary, and only those.
399
+ *
400
+ * The previous implementation spread the database row, so every device on earth
401
+ * received the internal `id`, `app_id`, `uploaded_by` and row timestamps.
402
+ */
403
+ declare function nativePayload(release: NativeRelease): NativeUpdatePayload;
404
+ interface RenderContext {
405
+ /** Remote configuration for the channel's environment. */
406
+ config: Record<string, unknown>;
407
+ /**
408
+ * The binary satisfying a blocked bundle's `min_update_version`, when one was
409
+ * found. Only consulted for a `native-required` decision, and null when the
410
+ * publisher gated a bundle behind a build they never uploaded.
411
+ */
412
+ gate?: NativeRelease | null;
413
+ }
414
+ /**
415
+ * Turns a decision into the response the plugin reads.
416
+ *
417
+ * The one rule that must never be broken here: a native binary is offered only
418
+ * through `native_update`, never the top-level `url`. That field is the
419
+ * Capacitor plugin's OTA contract - it downloads whatever is there and unzips
420
+ * it as a web bundle. An APK in it made the plugin fetch 45 MB, fail to unzip
421
+ * it, and report "the update could not be downloaded" while a perfectly
422
+ * installable update sat unread in `native_update`.
423
+ */
424
+ declare function renderUpdateResponse(decision: UpdateDecision, context: RenderContext): UpdateCheckResponse;
425
+ /** One line naming the branch that fired, for the server log. */
426
+ declare function describeDecision(decision: UpdateDecision): string;
427
+ //#endregion
179
428
  //#region src/project-config.d.ts
180
429
  /**
181
430
  * `.capuchoo/project.json` - the file that makes an application deployable.
@@ -426,5 +675,5 @@ declare function canPublishTo(profile: UserProfile, cloudAppId: string): boolean
426
675
  declare const APP_CREATOR_ROLES: ReadonlySet<string>;
427
676
  declare function canCreateApps(organization: CloudOrganization): boolean;
428
677
  //#endregion
429
- export { APP_CREATOR_ROLES, type BuildConfig, type BumpType, type CloudApp, type CloudChannel, type CloudOrganization, type CloudRelease, type CloudUser, type CredentialScope, ENVIRONMENTS, type Environment, type EnvironmentSelection, type FlavourConfig, INITIAL_VERSION_CODES, type NativeUpdatePayload, PROJECT_CONFIG_VERSION, type Platform, type ProjectConfig, type ResolvedProjectConfig, type ResolvedUpdate, type SemanticVersion, type UpdateCheckRequest, type UpdateCheckResponse, type UpdateEvent, type UpdateEventPayload, type UpdateKind, UpdateMessage, type UpdateMessageValue, type UserProfile, type VersionCodes, bumpVersion, canCreateApps, canPublishTo, compareVersions, defaultFlavour, describeEnvironmentMismatch, environmentFromAppId, environmentMismatchWarning, formatVersion, hasEnvironmentMismatch, isBlockingResponse, isEnvironmentAllowed, isValidBundleId, nextVersionCode, normaliseProjectConfig, parseVersion, resolveUpdate, suggestEnvironment, validateProjectConfig, versionEnv };
678
+ export { APP_CREATOR_ROLES, type BuildConfig, type BumpType, type ChannelState, type CloudApp, type CloudChannel, type CloudOrganization, type CloudRelease, type CloudUser, type CredentialScope, type DeviceState, ENVIRONMENTS, type Environment, type EnvironmentSelection, type FlavourConfig, INITIAL_VERSION_CODES, type NativeRelease, type NativeUpdatePayload, type OtaRelease, PROJECT_CONFIG_VERSION, type Platform, type ProjectConfig, type RenderContext, type ResolvedProjectConfig, type ResolvedUpdate, type SemanticVersion, UPDATE_EVENTS, UPDATE_EVENT_REQUIRED, type UpdateCheckRequest, type UpdateCheckResponse, type UpdateDecision, type UpdateEvent, type UpdateEventPayload, type UpdateFacts, type UpdateKind, UpdateMessage, type UpdateMessageValue, type UpdateResponseKind, type UserProfile, type VersionCodes, bumpVersion, canCreateApps, canPublishTo, compareVersions, decideUpdate, defaultFlavour, describeDecision, describeEnvironmentMismatch, environmentFromAppId, environmentMismatchWarning, formatVersion, hasEnvironmentMismatch, isBlockingResponse, isEnvironmentAllowed, isValidBundleId, nativePayload, nextVersionCode, normaliseProjectConfig, parseUpdateEvent, parseVersion, renderUpdateResponse, resolveUpdate, suggestEnvironment, validateProjectConfig, versionEnv };
430
679
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/update-contract.ts","../src/channel-environment.ts","../src/project-config.ts","../src/version.ts","../src/cloud.ts"],"mappings":";;;;;;;;;;;;KAYY;;KAGA;;;;;cAMC;;;;;;;;;;;;;;;KAgBD,6BAA6B,4BAA4B;;UAGpD;;EAEf;EACA,UAAU;;EAEV;EACA;;;;;;;EAOA;;EAEA;;;;;;EAMA;;EAEA;EACA;;;;;;;EAOA;EACA;;;;;;EAMA;EACA;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;;;;;;;UAQe;EACf;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;;EAGA,gBAAgB;;EAGhB,SAAS;;KAGC;;UAGK;EACf,MAAM;EACN;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;EACA;;EAEA;;;;;;;;;;iBAWc,cACd,UAAU,yCACT;;;;;iBAsCa,mBAAmB,UAAU;;KAQjC;UAQK;EACf,OAAO;EACP,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;;;;KCnMU,uBAAuB;;;;;;;;iBAenB,mBAAmB,eAAe;;iBAYlC,uBAAuB,cAAc,aAAa;;iBAQlD,2BACd,cACA,aAAa;;;;;;;;;;;;;;;;;cCnCF;;UAGI;;;;;;;EAOf;;EAEA;;EAEA;;EAEA;;UAGe;;;;;EAKf;;EAEA;;UAGe;;EAEf;;EAGA;;EAEA;EACA;EACA;;EAGA;EACA;EACA;;;;;EAMA;EAEA,WAAW,QAAQ,OAAO,aAAa;EACvC,QAAQ;;EAGR;;EAGA;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,UAAU,OAAO,aAAa;EAC9B,OAAO;EACP;;cAGW,uBAAuB;;;;;iBAMpB,eAAe,aAAa,cAAc;iBAS1C,uBAAuB,QAAQ,gBAAgB;;iBAuC/C,sBAAsB,QAAQ,QAAQ;iBAiBtC,gBAAgB;;;;;;;;iBAWhB,qBAAqB,gBAAgB;;;;;;;;;;;;;;iBAoBrC,qBAAqB,eAAe,oBAAoB;;iBAOxD,4BACd,eACA,oBAAoB,aACpB;;;;;;;;;;;;;KClMU;UAEK;EACf;EACA;EACA;EACA;EACA;;iBAMc,aAAa,gBAAgB;iBAa7B,cAAc,SAAS;;;;;iBAWvB,YAAY,eAAe,MAAM;;;;;;;iBA8BjC,gBAAgB,WAAW;KAsB/B,eAAe,OAAO;cAErB,uBAAuB;;;;;;;iBAYpB,gBACd,OAAO,QAAQ,kCACf,aAAa,cACZ;;;;;;;;;;;iBAea,WAAW,iBAAiB;;;;;;;;UClI3B;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;;;;;EAKA,aAAa;EACb;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;;;;UAKe;EACf;;EAEA;;UAGe;EACf,MAAM;EACN,eAAe;EACf,MAAM,MAAM;IAAa;;;EAEzB,aAAa;;;;;;;;;iBAUC,aAAa,SAAS,aAAa;;cAMtC,mBAAmB;iBAEhB,cAAc,cAAc"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/update-contract.ts","../src/channel-environment.ts","../src/update-decision.ts","../src/project-config.ts","../src/version.ts","../src/cloud.ts"],"mappings":";;;;;;;;;;;;KAYY;;KAGA;;;;;cAMC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyCD,6BAA6B,4BAA4B;;;;;;;;;;;;;;;;;;KAmBzD;;UAGK;;EAEf;EACA,UAAU;;EAEV;EACA;;;;;;;EAOA;;EAEA;;;;;;EAMA;;EAEA;EACA;;;;;;;EAOA;EACA;;;;;;EAMA;EACA;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA,WAAW;;;;;;;EAOX;;;;;;;UAQe;EACf;EACA;;;;;EAMA,OAAO;;EAGP;;;;;;;;;;;;EAYA;EACA;EACA;EACA;EACA;EACA;;EAGA,gBAAgB;;EAGhB,SAAS;;KAGC;;UAGK;EACf,MAAM;EACN;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;EACA;;;;;;;;EAQA;;EAEA;;;;;;;;;;iBAWc,cACd,UAAU,yCACT;;;;;iBAuCa,mBAAmB,UAAU;;;;;;;;;;;;;cAmBhC;KASD,sBAAsB;UAEjB;EACf,OAAO;EACP,UAAU;;;;;;;;;;;EAWV;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;;cAIW;;;;;;;;;iBAUG,iBACd,MAAM;EACH;EAAU,OAAO;;EAAyB;EAAW;;;;;;;;;;;;;;;KC1T9C,uBAAuB;;;;;;;;iBAenB,mBAAmB,eAAe;;iBAYlC,uBAAuB,cAAc,aAAa;;iBAQlD,2BACd,cACA,aAAa;;;;UChBE;;EAEf;EACA,UAAU;;EAEV;;EAEA;;UAGe;EACf;EACA,aAAa;;;;;;;;;UAUE;EACf;EACA;EACA;EACA,UAAU;EACV;EACA;EACA;;;UAIe;EACf;EACA;EACA,UAAU;EACV;EACA;;EAEA;EACA;EACA;;;UAIe;EACf,QAAQ;;EAER;IAAO;;;EAEP,SAAS;;EAET,QAAQ;;EAER,KAAK;;;;;;;;;;KAWK;EACN;;EACA;;EACA;EAA8B,UAAU;EAAa,SAAS;;EAC9D;EAAgB,SAAS;;EACzB;EAAyB;EAAwB;;EACjD;EAAa,SAAS;;EACtB;;EACA;EAA2B,gBAAgB;EAAU,gBAAgB;;EACrE;EAAoB;;;;;;;;;;;iBAmBV,aAAa,OAAO,cAAc;;;;;;;iBAoDlC,cAAc,SAAS,gBAAgB;UAYtC;;EAEf,QAAQ;;;;;;EAMR,OAAO;;;;;;;;;;;;iBAaO,qBACd,UAAU,gBACV,SAAS,gBACR;;iBAyFa,iBAAiB,UAAU;;;;;;;;;;;;;;;;;cClS9B;;UAGI;;;;;;;EAOf;;EAEA;;EAEA;;EAEA;;UAGe;;;;;EAKf;;EAEA;;UAGe;;EAEf;;EAGA;;EAEA;EACA;EACA;;EAGA;EACA;EACA;;;;;EAMA;EAEA,WAAW,QAAQ,OAAO,aAAa;EACvC,QAAQ;;EAGR;;EAGA;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,UAAU,OAAO,aAAa;EAC9B,OAAO;EACP;;cAGW,uBAAuB;;;;;iBAMpB,eAAe,aAAa,cAAc;iBAS1C,uBAAuB,QAAQ,gBAAgB;;iBAuC/C,sBAAsB,QAAQ,QAAQ;iBAiBtC,gBAAgB;;;;;;;;iBAWhB,qBAAqB,gBAAgB;;;;;;;;;;;;;;iBAoBrC,qBAAqB,eAAe,oBAAoB;;iBAOxD,4BACd,eACA,oBAAoB,aACpB;;;;;;;;;;;;;KClMU;UAEK;EACf;EACA;EACA;EACA;EACA;;iBAMc,aAAa,gBAAgB;iBAa7B,cAAc,SAAS;;;;;iBAWvB,YAAY,eAAe,MAAM;;;;;;;iBA8BjC,gBAAgB,WAAW;KAsB/B,eAAe,OAAO;cAErB,uBAAuB;;;;;;;iBAYpB,gBACd,OAAO,QAAQ,kCACf,aAAa,cACZ;;;;;;;;;;;iBAea,WAAW,iBAAiB;;;;;;;;UClI3B;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;;;;;EAKA,aAAa;EACb;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;;;;UAKe;EACf;;EAEA;;UAGe;EACf,MAAM;EACN,eAAe;EACf,MAAM,MAAM;IAAa;;;EAEzB,aAAa;;;;;;;;;iBAUC,aAAa,SAAS,aAAa;;cAMtC,mBAAmB;iBAEhB,cAAc,cAAc"}
package/dist/index.js CHANGED
@@ -40,15 +40,40 @@ const UpdateMessage = {
40
40
  NATIVE_UPDATE_REQUIRED: "native_update_required",
41
41
  /** A newer artefact is available. */
42
42
  UPDATE_AVAILABLE: "update_available",
43
+ /**
44
+ * A newer native binary is available, but not mandatory.
45
+ *
46
+ * Distinct from UPDATE_AVAILABLE because the top-level `url` is deliberately
47
+ * absent: that field is the Capacitor plugin's OTA contract, and it
48
+ * auto-downloads whatever is there and unzips it. An APK in `url` made the
49
+ * plugin download 45 MB and fail, hiding the real update behind a download
50
+ * error. The binary is in `native_update` instead.
51
+ */
52
+ NATIVE_UPDATE_AVAILABLE: "native_update_available",
43
53
  /** The device already runs the newest artefact for its channel. */
44
54
  NO_UPDATE: "No update available",
55
+ /** No application carries the requesting bundle identifier. */
56
+ APP_NOT_FOUND: "App not found",
45
57
  /** The channel name does not exist for this application. */
46
58
  CHANNEL_NOT_FOUND: "Channel not found",
47
59
  /**
48
60
  * The requesting app id does not belong to the channel's environment - a
49
61
  * staging build asking a production channel, for example.
50
62
  */
51
- ENVIRONMENT_MISMATCH: "Environment mismatch"
63
+ ENVIRONMENT_MISMATCH: "Environment mismatch",
64
+ /**
65
+ * The channel exists but points at no bundle, and PLATFORM_MISMATCH means it
66
+ * points at one built for another platform.
67
+ *
68
+ * Neither is actionable by the device, and both used to return a bare
69
+ * `{ config: {} }` - the same response as "you are up to date". Three
70
+ * different situations were indistinguishable on the wire, which is why an
71
+ * iOS device asking an Android-only channel produced silence rather than a
72
+ * diagnosis. Clients still take no action; the names exist so the answer to
73
+ * "why did nothing happen" is in the response.
74
+ */
75
+ NO_BUNDLE: "No bundle assigned",
76
+ PLATFORM_MISMATCH: "Platform mismatch"
52
77
  };
53
78
  /**
54
79
  * Narrows a raw response into an update to act on, or `null` when there is
@@ -68,7 +93,8 @@ function resolveUpdate(response) {
68
93
  downloadUrl: native.download_url,
69
94
  releaseNotes: native.release_notes,
70
95
  required: response.message === UpdateMessage.NATIVE_UPDATE_REQUIRED || (native.required ?? false),
71
- platform: native.platform
96
+ platform: native.platform,
97
+ ...typeof native.file_size === "number" ? { fileSize: native.file_size } : {}
72
98
  };
73
99
  if (response.url && response.version_name) return {
74
100
  kind: "ota",
@@ -88,6 +114,63 @@ function resolveUpdate(response) {
88
114
  function isBlockingResponse(response) {
89
115
  return response.message === UpdateMessage.CHANNEL_NOT_FOUND || response.message === UpdateMessage.ENVIRONMENT_MISMATCH;
90
116
  }
117
+ /**
118
+ * Analytics events posted to `POST {endpoint}/api/native-updates/log`.
119
+ *
120
+ * A list rather than a bare union because `native_update_logs.event` carries a
121
+ * CHECK constraint, and the two had drifted: the column allowed `check`,
122
+ * `download`, `install`, `fail` and `skip` while this declared `check`,
123
+ * `download`, `download_complete`, `install`, `cancel` and `error`. Three of
124
+ * the six were rejected by the database, so a device reporting
125
+ * `download_complete` - which is what one does after every native download -
126
+ * got a 500. `native-update-events.test.ts` reads the migration and fails if
127
+ * this list ever moves ahead of it again.
128
+ */
129
+ const UPDATE_EVENTS = [
130
+ "check",
131
+ "download",
132
+ "download_complete",
133
+ "install",
134
+ "cancel",
135
+ "error"
136
+ ];
137
+ /** Field names a payload must carry for the server to record it. */
138
+ const UPDATE_EVENT_REQUIRED = [
139
+ "event",
140
+ "platform",
141
+ "app_id"
142
+ ];
143
+ /**
144
+ * Validates an incoming update event, naming everything that is missing.
145
+ *
146
+ * Pure, and shared with the server, so "what the client sends" and "what the
147
+ * server accepts" cannot drift the way they did here: the client sent `error`
148
+ * and the server read `error_message`, so even a payload that got past
149
+ * validation lost its failure detail.
150
+ */
151
+ function parseUpdateEvent(body) {
152
+ const appId = body.app_id ?? body.appId;
153
+ const missing = UPDATE_EVENT_REQUIRED.filter((field) => field === "app_id" ? !appId : !body[field]);
154
+ if (missing.length > 0) return {
155
+ ok: false,
156
+ missing
157
+ };
158
+ return {
159
+ ok: true,
160
+ event: {
161
+ event: body.event,
162
+ platform: body.platform,
163
+ app_id: appId,
164
+ device_id: body.device_id ?? "",
165
+ current_version_code: Number(body.current_version_code ?? 0),
166
+ new_version: body.new_version,
167
+ new_version_code: body.new_version_code === void 0 ? void 0 : Number(body.new_version_code),
168
+ channel: body.channel ?? "",
169
+ environment: body.environment ?? "",
170
+ error: body.error ?? body.error_message
171
+ }
172
+ };
173
+ }
91
174
  //#endregion
92
175
  //#region src/project-config.ts
93
176
  /**
@@ -306,6 +389,190 @@ function versionEnv(version, versionCode) {
306
389
  };
307
390
  }
308
391
  //#endregion
392
+ //#region src/update-decision.ts
393
+ /**
394
+ * What a device should install, decided from what the server found.
395
+ *
396
+ * This is the rule that governs every install of every app, and until this file
397
+ * existed it lived as a two-hundred-line branch inside `updateService`,
398
+ * interleaved with five Supabase round trips. It had no tests - not because it
399
+ * was unimportant but because it could not be called without a database. So the
400
+ * only harness available was a physical phone, and every defect in it was found
401
+ * that way: a native binary served in the OTA `url` field, `required` dropped
402
+ * in transit, release notes stored and never sent, a native release the channel
403
+ * never pointed at.
404
+ *
405
+ * Those are one bug, five times: an unexecutable specification. So the decision
406
+ * is separated from the fetching here. `decideUpdate` is pure and total - it
407
+ * takes facts and returns one of a closed set of outcomes - and
408
+ * `renderUpdateResponse` is the only place a wire response is shaped. Both run
409
+ * in microseconds against a table of cases, which is where this class of defect
410
+ * has to be caught, because a phone in someone's hand is not a test suite.
411
+ *
412
+ * The backend had also reimplemented three rules this package already exports:
413
+ * semantic version comparison, the environment isolation check, and the message
414
+ * strings. Copies drift; these do not.
415
+ */
416
+ /** `min_update_version` as a number; absent, empty and unparseable all mean ungated. */
417
+ function minimumNativeVersion(ota) {
418
+ const raw = ota.min_update_version;
419
+ if (raw === null || raw === void 0 || raw === "") return 0;
420
+ const parsed = typeof raw === "number" ? raw : Number.parseInt(raw, 10);
421
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
422
+ }
423
+ /**
424
+ * Decides what to serve.
425
+ *
426
+ * The order is load-bearing. Native comes before OTA because the server can
427
+ * have both, and applying a bundle to a binary too old to run it leaves the
428
+ * device broken with no way back. The environment check comes before either, so
429
+ * a staging build can never be handed a production bundle by asking for the
430
+ * wrong channel.
431
+ */
432
+ function decideUpdate(facts) {
433
+ const { device, app, channel, native, ota } = facts;
434
+ if (!app) return { kind: "app-not-found" };
435
+ if (!channel) return { kind: "channel-not-found" };
436
+ if (!isEnvironmentAllowed(device.appId, channel.environment)) return {
437
+ kind: "environment-mismatch",
438
+ expected: channel.environment,
439
+ channel
440
+ };
441
+ if (native && native.platform === device.platform && native.version_code > device.versionCode) return {
442
+ kind: "native",
443
+ release: native
444
+ };
445
+ if (!ota) return { kind: "no-bundle" };
446
+ if (ota.platform !== device.platform) return {
447
+ kind: "platform-mismatch",
448
+ bundlePlatform: ota.platform,
449
+ devicePlatform: device.platform
450
+ };
451
+ if (compareVersions(ota.version_name, device.versionName) <= 0) return {
452
+ kind: "up-to-date",
453
+ version: device.versionName
454
+ };
455
+ const minimum = minimumNativeVersion(ota);
456
+ if (minimum > 0 && device.versionCode < minimum) return {
457
+ kind: "native-required",
458
+ minVersionCode: minimum,
459
+ installedVersionCode: device.versionCode
460
+ };
461
+ return {
462
+ kind: "ota",
463
+ release: ota
464
+ };
465
+ }
466
+ /**
467
+ * The wire fields of a native binary, and only those.
468
+ *
469
+ * The previous implementation spread the database row, so every device on earth
470
+ * received the internal `id`, `app_id`, `uploaded_by` and row timestamps.
471
+ */
472
+ function nativePayload(release) {
473
+ return {
474
+ version_name: release.version_name,
475
+ version_code: release.version_code,
476
+ download_url: release.download_url,
477
+ platform: release.platform,
478
+ required: release.required ?? false,
479
+ ...release.release_notes ? { release_notes: release.release_notes } : {},
480
+ ...typeof release.file_size_bytes === "number" ? { file_size: release.file_size_bytes } : {}
481
+ };
482
+ }
483
+ /**
484
+ * Turns a decision into the response the plugin reads.
485
+ *
486
+ * The one rule that must never be broken here: a native binary is offered only
487
+ * through `native_update`, never the top-level `url`. That field is the
488
+ * Capacitor plugin's OTA contract - it downloads whatever is there and unzips
489
+ * it as a web bundle. An APK in it made the plugin fetch 45 MB, fail to unzip
490
+ * it, and report "the update could not be downloaded" while a perfectly
491
+ * installable update sat unread in `native_update`.
492
+ */
493
+ function renderUpdateResponse(decision, context) {
494
+ const { config } = context;
495
+ switch (decision.kind) {
496
+ case "app-not-found": return {
497
+ message: UpdateMessage.APP_NOT_FOUND,
498
+ kind: "blocked"
499
+ };
500
+ case "channel-not-found": return {
501
+ message: UpdateMessage.CHANNEL_NOT_FOUND,
502
+ kind: "blocked"
503
+ };
504
+ case "environment-mismatch": return {
505
+ message: UpdateMessage.ENVIRONMENT_MISMATCH,
506
+ kind: "blocked",
507
+ config
508
+ };
509
+ case "native": {
510
+ const payload = nativePayload(decision.release);
511
+ return {
512
+ message: UpdateMessage.NATIVE_UPDATE_AVAILABLE,
513
+ kind: "blocked",
514
+ version_name: payload.version_name,
515
+ version: payload.version_name,
516
+ required: payload.required ?? false,
517
+ ...payload.release_notes ? { release_notes: payload.release_notes } : {},
518
+ native_update: payload,
519
+ config
520
+ };
521
+ }
522
+ case "native-required": return {
523
+ message: UpdateMessage.NATIVE_UPDATE_REQUIRED,
524
+ kind: "blocked",
525
+ error: `Native version ${decision.minVersionCode} required. You have ${decision.installedVersionCode}.`,
526
+ ...context.gate ? { version: context.gate.version_name } : {},
527
+ native_update: context.gate ? nativePayload(context.gate) : null,
528
+ config
529
+ };
530
+ case "ota": {
531
+ const { release } = decision;
532
+ return {
533
+ version_name: release.version_name,
534
+ version: release.version_name,
535
+ url: release.url,
536
+ ...release.checksum ? { checksum: release.checksum } : {},
537
+ ...release.session_key ? { sessionKey: release.session_key } : {},
538
+ required: release.required ?? false,
539
+ ...release.release_notes ? { release_notes: release.release_notes } : {},
540
+ config
541
+ };
542
+ }
543
+ case "no-bundle": return {
544
+ message: UpdateMessage.NO_BUNDLE,
545
+ kind: "up_to_date",
546
+ config
547
+ };
548
+ case "platform-mismatch": return {
549
+ message: UpdateMessage.PLATFORM_MISMATCH,
550
+ kind: "up_to_date",
551
+ config
552
+ };
553
+ case "up-to-date": return {
554
+ message: UpdateMessage.NO_UPDATE,
555
+ kind: "up_to_date",
556
+ version: decision.version,
557
+ config
558
+ };
559
+ }
560
+ }
561
+ /** One line naming the branch that fired, for the server log. */
562
+ function describeDecision(decision) {
563
+ switch (decision.kind) {
564
+ case "app-not-found": return "no app carries this bundle identifier";
565
+ case "channel-not-found": return "the app has no channel by that name";
566
+ case "environment-mismatch": return `a ${decision.expected} channel refused this build`;
567
+ case "native": return `native ${decision.release.version_name} (code ${decision.release.version_code})`;
568
+ case "native-required": return `bundle gated behind native ${decision.minVersionCode}, device has ${decision.installedVersionCode}`;
569
+ case "ota": return `bundle ${decision.release.version_name}`;
570
+ case "no-bundle": return "the channel points at no bundle";
571
+ case "platform-mismatch": return `the bundle is ${decision.bundlePlatform}, the device is ${decision.devicePlatform}`;
572
+ case "up-to-date": return `already on ${decision.version}`;
573
+ }
574
+ }
575
+ //#endregion
309
576
  //#region src/cloud.ts
310
577
  /**
311
578
  * Whether this credential can publish to an app.
@@ -324,6 +591,6 @@ function canCreateApps(organization) {
324
591
  return APP_CREATOR_ROLES.has(organization.role);
325
592
  }
326
593
  //#endregion
327
- export { APP_CREATOR_ROLES, ENVIRONMENTS, INITIAL_VERSION_CODES, PROJECT_CONFIG_VERSION, UpdateMessage, bumpVersion, canCreateApps, canPublishTo, compareVersions, defaultFlavour, describeEnvironmentMismatch, environmentFromAppId, environmentMismatchWarning, formatVersion, hasEnvironmentMismatch, isBlockingResponse, isEnvironmentAllowed, isValidBundleId, nextVersionCode, normaliseProjectConfig, parseVersion, resolveUpdate, suggestEnvironment, validateProjectConfig, versionEnv };
594
+ export { APP_CREATOR_ROLES, ENVIRONMENTS, INITIAL_VERSION_CODES, PROJECT_CONFIG_VERSION, UPDATE_EVENTS, UPDATE_EVENT_REQUIRED, UpdateMessage, bumpVersion, canCreateApps, canPublishTo, compareVersions, decideUpdate, defaultFlavour, describeDecision, describeEnvironmentMismatch, environmentFromAppId, environmentMismatchWarning, formatVersion, hasEnvironmentMismatch, isBlockingResponse, isEnvironmentAllowed, isValidBundleId, nativePayload, nextVersionCode, normaliseProjectConfig, parseUpdateEvent, parseVersion, renderUpdateResponse, resolveUpdate, suggestEnvironment, validateProjectConfig, versionEnv };
328
595
 
329
596
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/channel-environment.ts","../src/update-contract.ts","../src/project-config.ts","../src/version.ts","../src/cloud.ts"],"sourcesContent":["import type { Environment } from \"./update-contract.js\";\n\n/**\n * A channel's `environment` decides which `.env` flavour the CLI builds and which\n * bundles the backend serves to it. The channel's *name* is only an identifier.\n *\n * Nothing links the two, so a channel named `prod` left on the `staging`\n * environment silently serves staging bundles to production devices - which is\n * how all three of Lowmaro's channels ended up on staging. These helpers make\n * the mismatch visible; they never correct it silently, because prod apps\n * legitimately point at a staging channel for beta testing.\n */\n\n/** Empty means \"not chosen yet\". A channel must not default into an environment. */\nexport type EnvironmentSelection = Environment | \"\";\n\nconst PATTERNS: Array<[Environment, RegExp]> = [\n [\"prod\", /^(prod|production|live|release|stable|main|master)$/],\n [\"staging\", /^(staging|stage|beta|uat|qa|test|preprod|pre-prod)$/],\n [\"dev\", /^(dev|develop|development|debug|local|alpha)$/],\n];\n\n/**\n * The environment a channel name implies, or `null` when the name says nothing.\n *\n * Matching is deliberately whole-name: a channel called `prod-eu` could belong\n * to either, and guessing at substrings would put a warning on names it cannot\n * reason about.\n */\nexport function suggestEnvironment(name: string): Environment | null {\n const normalized = name.trim().toLowerCase();\n if (!normalized) return null;\n\n for (const [environment, pattern] of PATTERNS) {\n if (pattern.test(normalized)) return environment;\n }\n\n return null;\n}\n\n/** True when the name implies one environment and a different one is selected. */\nexport function hasEnvironmentMismatch(name: string, environment: EnvironmentSelection): boolean {\n if (!environment) return false;\n\n const suggested = suggestEnvironment(name);\n return suggested !== null && suggested !== environment;\n}\n\n/** The warning to show for a mismatch, or `null` when there is nothing to warn about. */\nexport function environmentMismatchWarning(\n name: string,\n environment: EnvironmentSelection,\n): string | null {\n if (!hasEnvironmentMismatch(name, environment)) return null;\n\n const label = name.trim();\n return (\n `A channel named \"${label}\" is set to the ${environment} environment. ` +\n `Devices on it will receive ${environment} bundles, built from .env.${environment}. ` +\n `Set the environment to ${suggestEnvironment(label)} unless that is deliberate.`\n );\n}\n","/**\n * The wire contract for `POST {endpoint}/api/update`.\n *\n * This file is the single definition shared by the backend that produces the\n * response, the app runtime that consumes it, and the CLI that publishes the\n * artefacts it points at. Before this package existed the three had drifted:\n * the app template asked a second endpoint (`GET /api/native-updates/check`)\n * with an `{ available, update }` envelope the backend never returns on the\n * primary path, and sent a hard-coded `version_name: \"builtin\"` so the server\n * always compared against 0.0.0.\n */\n\nexport type Platform = \"android\" | \"ios\" | \"web\";\n\n/** Deployment environments. A channel is bound to exactly one of these. */\nexport type Environment = \"dev\" | \"staging\" | \"prod\";\n\n/**\n * Messages the backend puts in `message`. Both sides must agree on the exact\n * strings, so they live here rather than being retyped at each call site.\n */\nexport const UpdateMessage = {\n /** A newer native binary is assigned to the channel and must be installed. */\n NATIVE_UPDATE_REQUIRED: \"native_update_required\",\n /** A newer artefact is available. */\n UPDATE_AVAILABLE: \"update_available\",\n /** The device already runs the newest artefact for its channel. */\n NO_UPDATE: \"No update available\",\n /** The channel name does not exist for this application. */\n CHANNEL_NOT_FOUND: \"Channel not found\",\n /**\n * The requesting app id does not belong to the channel's environment - a\n * staging build asking a production channel, for example.\n */\n ENVIRONMENT_MISMATCH: \"Environment mismatch\",\n} as const;\n\nexport type UpdateMessageValue = (typeof UpdateMessage)[keyof typeof UpdateMessage];\n\n/** What the device tells the server about itself. */\nexport interface UpdateCheckRequest {\n /** Bundle identifier of the running build, e.g. `com.ayb.lowmaro.staging`. */\n appId: string;\n platform: Platform;\n /** Channel to consult. Falls back to `defaultChannel` server-side. */\n channel?: string;\n defaultChannel?: string;\n /**\n * Native build number as a string. The server compares this against\n * `native_updates.version_code` and against an OTA bundle's\n * `min_update_version`, so an omitted or wrong value silently disables\n * native-update gating.\n */\n versionCode?: string;\n /** Historical alias for `versionCode`; the server accepts either. */\n versionBuild?: string;\n /**\n * Semantic version of the *currently applied web bundle*, or `\"builtin\"`\n * when the app still runs the bundle shipped inside the binary. Sending a\n * constant here defeats version comparison entirely.\n */\n version_name?: string;\n /** Stable per-install identifier, used for channel overrides and stats. */\n deviceId?: string;\n isProd?: boolean;\n /**\n * Device facts the server stores but does not decide with. All optional: an\n * app that cannot determine one should omit it rather than send a placeholder,\n * because the server writes only the keys it receives and a placeholder would\n * overwrite a better value recorded earlier.\n */\n versionOs?: string;\n pluginVersion?: string;\n /**\n * Bundle version compiled into the binary. `version_name` is the *applied*\n * OTA bundle and is absent until one lands, so the two together are what say\n * whether a device has ever taken an update.\n */\n versionBuiltin?: string;\n isEmulator?: boolean;\n /** Caller-supplied label for this install, shown in the dashboard. */\n customId?: string;\n}\n\n/** A native binary (APK/IPA) the device should install. */\nexport interface NativeUpdatePayload {\n version_name: string;\n version_code: number;\n download_url: string;\n release_notes?: string;\n required?: boolean;\n platform?: Platform;\n file_size?: number;\n}\n\n/**\n * The response. Every field is optional because the server returns a partial\n * object per outcome rather than a discriminated union - `resolveUpdate` below\n * narrows it into something a caller can branch on safely.\n */\nexport interface UpdateCheckResponse {\n message?: string;\n error?: string;\n\n /** OTA bundle fields. */\n version_name?: string;\n url?: string;\n checksum?: string;\n sessionKey?: string;\n release_notes?: string;\n required?: boolean;\n\n /** Present when a native binary supersedes, or blocks, the OTA bundle. */\n native_update?: NativeUpdatePayload | null;\n\n /** Remote configuration resolved for the channel's environment. */\n config?: Record<string, string>;\n}\n\nexport type UpdateKind = \"native\" | \"ota\";\n\n/** A resolved, actionable update. */\nexport interface ResolvedUpdate {\n kind: UpdateKind;\n version: string;\n versionCode?: number;\n downloadUrl?: string;\n releaseNotes?: string;\n required: boolean;\n platform?: Platform;\n checksum?: string;\n sessionKey?: string;\n /** Set once the OTA plugin has downloaded the bundle. */\n bundleId?: string;\n}\n\n/**\n * Narrows a raw response into an update to act on, or `null` when there is\n * nothing to do.\n *\n * Native wins over OTA. The server can return both - a required native binary\n * alongside the OTA bundle that needs it - and installing the bundle first\n * would leave the device on a binary too old to run it.\n */\nexport function resolveUpdate(\n response: UpdateCheckResponse | null | undefined,\n): ResolvedUpdate | null {\n if (!response) return null;\n\n const native = response.native_update;\n if (native?.download_url) {\n return {\n kind: \"native\",\n version: native.version_name,\n versionCode: native.version_code,\n downloadUrl: native.download_url,\n releaseNotes: native.release_notes,\n // A native update is mandatory when the server says the OTA bundle\n // cannot run without it, whatever the record's own flag says.\n required:\n response.message === UpdateMessage.NATIVE_UPDATE_REQUIRED || (native.required ?? false),\n platform: native.platform,\n };\n }\n\n if (response.url && response.version_name) {\n return {\n kind: \"ota\",\n version: response.version_name,\n downloadUrl: response.url,\n releaseNotes: response.release_notes,\n required: response.required ?? false,\n checksum: response.checksum,\n sessionKey: response.sessionKey,\n };\n }\n\n return null;\n}\n\n/**\n * True when the response reports a condition the user cannot fix by updating.\n * Callers should surface these instead of showing \"you are up to date\".\n */\nexport function isBlockingResponse(response: UpdateCheckResponse): boolean {\n return (\n response.message === UpdateMessage.CHANNEL_NOT_FOUND ||\n response.message === UpdateMessage.ENVIRONMENT_MISMATCH\n );\n}\n\n/** Analytics events posted to `POST {endpoint}/api/native-updates/log`. */\nexport type UpdateEvent =\n | \"check\"\n | \"download\"\n | \"download_complete\"\n | \"install\"\n | \"cancel\"\n | \"error\";\n\nexport interface UpdateEventPayload {\n event: UpdateEvent;\n platform: Platform;\n device_id: string;\n current_version_code: number;\n new_version?: string;\n new_version_code?: number;\n channel: string;\n environment: string;\n error?: string;\n}\n","import type { Environment } from \"./update-contract.js\";\n\n/**\n * `.capuchoo/project.json` - the file that makes an application deployable.\n *\n * Version 1 held only the cloud identifiers, so the CLI had to *guess* how to\n * build: it shelled out to `pnpm run assets:<env>`, `pnpm build:<env>`,\n * `pnpm trapeze:<env>` and `pnpm exec cap sync`. Any application that named\n * its scripts differently, used npm, or had not installed Trapeze simply\n * failed halfway through a deploy.\n *\n * Version 2 describes the *inputs* instead - where each flavour's env file,\n * Trapeze config and icon sources live - and lets the CLI own the execution.\n * Every field has a default, so a v1 file keeps working: `normaliseProjectConfig`\n * fills in the conventional layout both existing apps already use.\n */\nexport const PROJECT_CONFIG_VERSION = 2;\n\n/** One build flavour, keyed by the environment its channels are bound to. */\nexport interface FlavourConfig {\n /**\n * Env file supplying `VITE_APP_ID`, `VITE_APP_NAME`, `VITE_UPDATE_CHANNEL`\n * and friends. The CLI reads it and passes the values to the build and to\n * the native configuration step as environment variables - it does not\n * rewrite the file, so a deploy leaves the working tree clean.\n */\n envFile: string;\n /** Trapeze config applied to the native projects. Optional. */\n trapezeConfig?: string;\n /** Directory holding `icon.png` / `splash.png` for icon generation. */\n assetPath?: string;\n /** Vite `--mode`. Defaults to the flavour name. */\n mode?: string;\n}\n\nexport interface BuildConfig {\n /**\n * Overrides the web build. Leave unset and the CLI runs the project's own\n * Vite build through the workspace toolchain it detects.\n */\n command?: string;\n /** Where to run the build from, relative to the app. For monorepo roots. */\n cwd?: string;\n}\n\nexport interface ProjectConfig {\n /** Absent on v1 files. */\n version?: number;\n\n /** Bundle identifier of the production flavour. */\n appId: string;\n /** Primary key of the application in Capuchoo. */\n cloudAppId: string;\n appName: string;\n createdAt: string;\n\n /** Vite output directory, and what Capacitor copies into the native app. */\n webDir?: string;\n androidDir?: string;\n iosDir?: string;\n\n /**\n * Monotonic native build numbers per environment. Written by the CLI, and\n * the one file a deploy is expected to modify.\n */\n versionCodeFile?: string;\n\n flavours?: Partial<Record<Environment, FlavourConfig>>;\n build?: BuildConfig;\n\n /** Optional GitHub Pages mirror for generated web assets. */\n ghPagesRepo?: string;\n\n /** @deprecated v1 fields, folded into `build` by `normaliseProjectConfig`. */\n monorepoRoot?: string;\n /** @deprecated v1 field. */\n packageName?: string;\n}\n\n/** A `ProjectConfig` with every optional resolved. */\nexport interface ResolvedProjectConfig {\n version: number;\n appId: string;\n cloudAppId: string;\n appName: string;\n createdAt: string;\n webDir: string;\n androidDir: string;\n iosDir: string;\n versionCodeFile: string;\n flavours: Record<Environment, FlavourConfig>;\n build: BuildConfig;\n ghPagesRepo?: string;\n}\n\nexport const ENVIRONMENTS: readonly Environment[] = [\"dev\", \"staging\", \"prod\"];\n\n/**\n * The layout both existing applications already use. Used as the default so a\n * v1 `project.json` needs no migration to keep deploying.\n */\nexport function defaultFlavour(environment: Environment): FlavourConfig {\n return {\n envFile: `build/${environment}/.env.${environment}`,\n trapezeConfig: `build/${environment}/trapeze.${environment}.yaml`,\n assetPath: `build/${environment}/assets`,\n mode: environment,\n };\n}\n\nexport function normaliseProjectConfig(config: ProjectConfig): ResolvedProjectConfig {\n const flavours = {} as Record<Environment, FlavourConfig>;\n for (const environment of ENVIRONMENTS) {\n const defaults = defaultFlavour(environment);\n const declared = config.flavours?.[environment];\n flavours[environment] = {\n envFile: declared?.envFile ?? defaults.envFile,\n trapezeConfig: declared?.trapezeConfig ?? defaults.trapezeConfig,\n assetPath: declared?.assetPath ?? defaults.assetPath,\n mode: declared?.mode ?? defaults.mode,\n };\n }\n\n // v1 expressed monorepo builds as `monorepoRoot` + `packageName`, which the\n // CLI turned into `pnpm exec vp run <pkg>#build:<env>`. Carry that forward as\n // an explicit build command so the behaviour is visible rather than implied.\n const build: BuildConfig = { ...config.build };\n if (!build.cwd && config.monorepoRoot) build.cwd = config.monorepoRoot;\n if (!build.command && config.packageName) {\n build.command = `vp run ${config.packageName}#build`;\n }\n\n return {\n version: config.version ?? 1,\n appId: config.appId,\n cloudAppId: config.cloudAppId,\n appName: config.appName,\n createdAt: config.createdAt,\n webDir: config.webDir ?? \"dist\",\n androidDir: config.androidDir ?? \"android\",\n iosDir: config.iosDir ?? \"ios\",\n versionCodeFile: config.versionCodeFile ?? \"version-code.json\",\n flavours,\n build,\n ghPagesRepo: config.ghPagesRepo,\n };\n}\n\n/** Fields a `project.json` must carry for a deploy to be possible. */\nexport function validateProjectConfig(config: Partial<ProjectConfig> | null | undefined): string[] {\n if (!config) return [\"project.json is missing or empty\"];\n\n const problems: string[] = [];\n if (!config.appId) problems.push(\"appId is required\");\n if (!config.cloudAppId) problems.push(\"cloudAppId is required\");\n if (!config.appName) problems.push(\"appName is required\");\n\n if (config.appId && !isValidBundleId(config.appId)) {\n problems.push(`appId \"${config.appId}\" is not a valid bundle identifier`);\n }\n\n return problems;\n}\n\nconst BUNDLE_ID = /^[a-z][a-z\\d_]*(\\.[a-z][a-z\\d_]*)+$/;\n\nexport function isValidBundleId(value: string): boolean {\n return BUNDLE_ID.test(value);\n}\n\n/**\n * Derives the environment a bundle identifier belongs to.\n *\n * The backend enforces the same rule server-side: a `.staging` build may only\n * be served staging channels. Mirroring it here lets the CLI refuse a\n * mismatched deploy before it uploads several megabytes.\n */\nexport function environmentFromAppId(appId: string): Environment {\n const id = appId.toLowerCase();\n if (id.endsWith(\".staging\")) return \"staging\";\n if (id.endsWith(\".dev\") || id.endsWith(\".debug\")) return \"dev\";\n return \"prod\";\n}\n\n/**\n * Whether a build may be served a channel bound to `channelEnvironment`.\n *\n * This mirrors the server's isolation check exactly, including its one\n * deliberate exception: a production build is allowed on a staging channel, so\n * a release candidate can be beta-tested by real installs without shipping a\n * separate bundle identifier.\n *\n * The rule lives here rather than being restated at each call site because the\n * CLI had reimplemented it as a plain equality check - which is *stricter* than\n * the server and rejected the exact beta-testing setup Lowmaro uses, where all\n * three channels are bound to staging and the app id carries no suffix.\n */\nexport function isEnvironmentAllowed(appId: string, channelEnvironment: Environment): boolean {\n const expected = environmentFromAppId(appId);\n if (expected === channelEnvironment) return true;\n return expected === \"prod\" && channelEnvironment === \"staging\";\n}\n\n/** Explains a rejected pairing, or null when it is allowed. */\nexport function describeEnvironmentMismatch(\n appId: string,\n channelEnvironment: Environment,\n channelName: string,\n): string | null {\n if (isEnvironmentAllowed(appId, channelEnvironment)) return null;\n\n const expected = environmentFromAppId(appId);\n return (\n `Channel \"${channelName}\" serves the ${channelEnvironment} environment, but ` +\n `the build's VITE_APP_ID is \"${appId}\", which is a ${expected} bundle id. ` +\n \"The server rejects this pairing, so the upload would be wasted.\"\n );\n}\n","import type { Environment } from \"./update-contract.js\";\n\n/**\n * Version arithmetic, kept free of any filesystem or child-process access so\n * both the CLI and the tests can use it directly.\n *\n * The CLI used to shell out to `npm version <type> --no-git-tag-version` for\n * this. In a workspace that is actively wrong: npm resolves the *nearest*\n * package.json, so running it from a monorepo root bumped the root package\n * instead of the app, and it mixed npm into a pnpm/Vite+ project for a job\n * that is three lines of string handling.\n */\n\nexport type BumpType = \"major\" | \"minor\" | \"patch\";\n\nexport interface SemanticVersion {\n major: number;\n minor: number;\n patch: number;\n prerelease?: string;\n build?: string;\n}\n\nconst SEMVER =\n /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([\\dA-Za-z-]+(?:\\.[\\dA-Za-z-]+)*))?(?:\\+([\\dA-Za-z-]+(?:\\.[\\dA-Za-z-]+)*))?$/;\n\nexport function parseVersion(value: string): SemanticVersion | null {\n const match = SEMVER.exec(value.trim());\n if (!match) return null;\n\n return {\n major: Number(match[1]),\n minor: Number(match[2]),\n patch: Number(match[3]),\n prerelease: match[4],\n build: match[5],\n };\n}\n\nexport function formatVersion(version: SemanticVersion): string {\n let out = `${version.major}.${version.minor}.${version.patch}`;\n if (version.prerelease) out += `-${version.prerelease}`;\n if (version.build) out += `+${version.build}`;\n return out;\n}\n\n/**\n * Bumps a version string. Prerelease and build metadata are dropped, matching\n * `npm version` semantics for a plain major/minor/patch bump.\n */\nexport function bumpVersion(value: string, type: BumpType): string {\n const parsed = parseVersion(value);\n if (!parsed) {\n throw new Error(`\"${value}\" is not a semantic version, so it cannot be bumped`);\n }\n\n switch (type) {\n case \"major\":\n return formatVersion({ major: parsed.major + 1, minor: 0, patch: 0 });\n case \"minor\":\n return formatVersion({\n major: parsed.major,\n minor: parsed.minor + 1,\n patch: 0,\n });\n case \"patch\":\n return formatVersion({\n major: parsed.major,\n minor: parsed.minor,\n patch: parsed.patch + 1,\n });\n }\n}\n\n/**\n * Compares two semantic versions. Returns a negative number when `a` is older.\n *\n * A missing or unparseable version sorts oldest, which is what the app needs:\n * the sentinel `\"builtin\"` must always look older than any published bundle.\n */\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n\n if (!left && !right) return 0;\n if (!left) return -1;\n if (!right) return 1;\n\n if (left.major !== right.major) return left.major - right.major;\n if (left.minor !== right.minor) return left.minor - right.minor;\n if (left.patch !== right.patch) return left.patch - right.patch;\n\n // 1.0.0-beta precedes 1.0.0.\n if (left.prerelease && !right.prerelease) return -1;\n if (!left.prerelease && right.prerelease) return 1;\n if (left.prerelease && right.prerelease) {\n return left.prerelease < right.prerelease ? -1 : left.prerelease > right.prerelease ? 1 : 0;\n }\n\n return 0;\n}\n\nexport type VersionCodes = Record<Environment, number>;\n\nexport const INITIAL_VERSION_CODES: VersionCodes = {\n dev: 1,\n staging: 1,\n prod: 1,\n};\n\n/**\n * Native build numbers must increase monotonically per environment: Android\n * refuses to install an APK whose versionCode is not greater than the\n * installed one, and the backend uses the same number to decide whether a\n * native update supersedes an OTA bundle.\n */\nexport function nextVersionCode(\n codes: Partial<VersionCodes> | null | undefined,\n environment: Environment,\n): VersionCodes {\n const current: VersionCodes = { ...INITIAL_VERSION_CODES, ...codes };\n return { ...current, [environment]: (current[environment] ?? 0) + 1 };\n}\n\n/**\n * Build-time variables injected into the web build and the native\n * configuration step.\n *\n * These used to be written *into* the committed `build/<env>/.env.<env>` file\n * by every deploy, which dirtied the working tree and made two concurrent\n * deploys race over one file. They are environment variables now: Trapeze\n * reads its `vars:` block from the process environment, and Vite reads\n * `VITE_*` the same way.\n */\nexport function versionEnv(version: string, versionCode: number) {\n return {\n VITE_APP_VERSION: version,\n VERSION_CODE: String(versionCode),\n BUILD_NUMBER: String(versionCode),\n };\n}\n","import type { Environment, Platform } from \"./update-contract.js\";\n\n/** Shapes returned by the authenticated `/api/*` endpoints. */\n\nexport interface CloudOrganization {\n id: string;\n name: string;\n slug: string;\n role: \"owner\" | \"admin\" | \"member\";\n}\n\nexport interface CloudApp {\n id: string;\n name: string;\n app_id: string;\n platform: string;\n organization_id: string;\n created_at: string;\n icon_url?: string;\n}\n\nexport interface CloudChannel {\n id: string;\n name: string;\n app_id: string;\n /**\n * Which build flavour this channel serves. The CLI derives the whole build\n * from it, so a channel without an environment cannot be deployed to.\n */\n environment: Environment;\n public: boolean;\n created_at: string;\n current_version_id?: string | null;\n current_native_version_id?: string | null;\n}\n\nexport interface CloudRelease {\n id: string;\n version_name: string;\n platform: Platform;\n channel?: string;\n active: boolean;\n required: boolean;\n release_notes?: string;\n created_at: string;\n}\n\nexport interface CloudUser {\n id: string;\n email: string;\n role?: string;\n}\n\n/** Response of `GET /api/auth/me`. */\n/** What the credential in use is, and what it is allowed to touch. */\nexport interface CredentialScope {\n type: \"api_key\" | \"session\";\n /** Cloud id of the only app this key may publish to, or null for all of them. */\n app_id: string | null;\n}\n\nexport interface UserProfile {\n user: CloudUser;\n organizations: CloudOrganization[];\n apps: Array<CloudApp & { role: string }>;\n /** Absent from older backends, so treat undefined as \"unknown\", not \"unscoped\". */\n credential?: CredentialScope;\n}\n\n/**\n * Whether this credential can publish to an app.\n *\n * An app-scoped key can still list every app the account owns, so a scope\n * mismatch is invisible until an upload returns 403 - after a full build. Both\n * `init` and `doctor` check this up front.\n */\nexport function canPublishTo(profile: UserProfile, cloudAppId: string): boolean {\n const scope = profile.credential?.app_id;\n return !scope || scope === cloudAppId;\n}\n\n/** Roles allowed to create an application inside an organization. */\nexport const APP_CREATOR_ROLES: ReadonlySet<string> = new Set([\"owner\", \"admin\"]);\n\nexport function canCreateApps(organization: CloudOrganization): boolean {\n return APP_CREATOR_ROLES.has(organization.role);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/channel-environment.ts","../src/update-contract.ts","../src/project-config.ts","../src/version.ts","../src/update-decision.ts","../src/cloud.ts"],"sourcesContent":["import type { Environment } from \"./update-contract.js\";\n\n/**\n * A channel's `environment` decides which `.env` flavour the CLI builds and which\n * bundles the backend serves to it. The channel's *name* is only an identifier.\n *\n * Nothing links the two, so a channel named `prod` left on the `staging`\n * environment silently serves staging bundles to production devices - which is\n * how all three of Lowmaro's channels ended up on staging. These helpers make\n * the mismatch visible; they never correct it silently, because prod apps\n * legitimately point at a staging channel for beta testing.\n */\n\n/** Empty means \"not chosen yet\". A channel must not default into an environment. */\nexport type EnvironmentSelection = Environment | \"\";\n\nconst PATTERNS: Array<[Environment, RegExp]> = [\n [\"prod\", /^(prod|production|live|release|stable|main|master)$/],\n [\"staging\", /^(staging|stage|beta|uat|qa|test|preprod|pre-prod)$/],\n [\"dev\", /^(dev|develop|development|debug|local|alpha)$/],\n];\n\n/**\n * The environment a channel name implies, or `null` when the name says nothing.\n *\n * Matching is deliberately whole-name: a channel called `prod-eu` could belong\n * to either, and guessing at substrings would put a warning on names it cannot\n * reason about.\n */\nexport function suggestEnvironment(name: string): Environment | null {\n const normalized = name.trim().toLowerCase();\n if (!normalized) return null;\n\n for (const [environment, pattern] of PATTERNS) {\n if (pattern.test(normalized)) return environment;\n }\n\n return null;\n}\n\n/** True when the name implies one environment and a different one is selected. */\nexport function hasEnvironmentMismatch(name: string, environment: EnvironmentSelection): boolean {\n if (!environment) return false;\n\n const suggested = suggestEnvironment(name);\n return suggested !== null && suggested !== environment;\n}\n\n/** The warning to show for a mismatch, or `null` when there is nothing to warn about. */\nexport function environmentMismatchWarning(\n name: string,\n environment: EnvironmentSelection,\n): string | null {\n if (!hasEnvironmentMismatch(name, environment)) return null;\n\n const label = name.trim();\n return (\n `A channel named \"${label}\" is set to the ${environment} environment. ` +\n `Devices on it will receive ${environment} bundles, built from .env.${environment}. ` +\n `Set the environment to ${suggestEnvironment(label)} unless that is deliberate.`\n );\n}\n","/**\n * The wire contract for `POST {endpoint}/api/update`.\n *\n * This file is the single definition shared by the backend that produces the\n * response, the app runtime that consumes it, and the CLI that publishes the\n * artefacts it points at. Before this package existed the three had drifted:\n * the app template asked a second endpoint (`GET /api/native-updates/check`)\n * with an `{ available, update }` envelope the backend never returns on the\n * primary path, and sent a hard-coded `version_name: \"builtin\"` so the server\n * always compared against 0.0.0.\n */\n\nexport type Platform = \"android\" | \"ios\" | \"web\";\n\n/** Deployment environments. A channel is bound to exactly one of these. */\nexport type Environment = \"dev\" | \"staging\" | \"prod\";\n\n/**\n * Messages the backend puts in `message`. Both sides must agree on the exact\n * strings, so they live here rather than being retyped at each call site.\n */\nexport const UpdateMessage = {\n /** A newer native binary is assigned to the channel and must be installed. */\n NATIVE_UPDATE_REQUIRED: \"native_update_required\",\n /** A newer artefact is available. */\n UPDATE_AVAILABLE: \"update_available\",\n /**\n * A newer native binary is available, but not mandatory.\n *\n * Distinct from UPDATE_AVAILABLE because the top-level `url` is deliberately\n * absent: that field is the Capacitor plugin's OTA contract, and it\n * auto-downloads whatever is there and unzips it. An APK in `url` made the\n * plugin download 45 MB and fail, hiding the real update behind a download\n * error. The binary is in `native_update` instead.\n */\n NATIVE_UPDATE_AVAILABLE: \"native_update_available\",\n /** The device already runs the newest artefact for its channel. */\n NO_UPDATE: \"No update available\",\n /** No application carries the requesting bundle identifier. */\n APP_NOT_FOUND: \"App not found\",\n /** The channel name does not exist for this application. */\n CHANNEL_NOT_FOUND: \"Channel not found\",\n /**\n * The requesting app id does not belong to the channel's environment - a\n * staging build asking a production channel, for example.\n */\n ENVIRONMENT_MISMATCH: \"Environment mismatch\",\n /**\n * The channel exists but points at no bundle, and PLATFORM_MISMATCH means it\n * points at one built for another platform.\n *\n * Neither is actionable by the device, and both used to return a bare\n * `{ config: {} }` - the same response as \"you are up to date\". Three\n * different situations were indistinguishable on the wire, which is why an\n * iOS device asking an Android-only channel produced silence rather than a\n * diagnosis. Clients still take no action; the names exist so the answer to\n * \"why did nothing happen\" is in the response.\n */\n NO_BUNDLE: \"No bundle assigned\",\n PLATFORM_MISMATCH: \"Platform mismatch\",\n} as const;\n\nexport type UpdateMessageValue = (typeof UpdateMessage)[keyof typeof UpdateMessage];\n\n/**\n * How the plugin classifies a response that carries no downloadable bundle.\n *\n * Read from `@capgo/capacitor-updater@7.50.2`, which is the authority here:\n * `CapacitorUpdaterPlugin.normalizedUpdateResponseKind` (android, line 4333)\n * maps anything that is not one of these three to `\"failed\"`, and the check\n * path at line 4515 enters this branch whenever the response has *either* an\n * `error` or a `kind` key.\n *\n * Two consequences the backend must respect, both of which it violated:\n *\n * 1. A response that carries an update must NOT set `kind`, or the plugin\n * classifies it instead of downloading it.\n * 2. A response that carries no update MUST set `kind`, or it is reported as a\n * failed update check - which is where the app's \"the update could not be\n * downloaded\" came from on a device that was simply up to date.\n */\nexport type UpdateResponseKind = \"up_to_date\" | \"blocked\" | \"failed\";\n\n/** What the device tells the server about itself. */\nexport interface UpdateCheckRequest {\n /** Bundle identifier of the running build, e.g. `com.ayb.lowmaro.staging`. */\n appId: string;\n platform: Platform;\n /** Channel to consult. Falls back to `defaultChannel` server-side. */\n channel?: string;\n defaultChannel?: string;\n /**\n * Native build number as a string. The server compares this against\n * `native_updates.version_code` and against an OTA bundle's\n * `min_update_version`, so an omitted or wrong value silently disables\n * native-update gating.\n */\n versionCode?: string;\n /** Historical alias for `versionCode`; the server accepts either. */\n versionBuild?: string;\n /**\n * Semantic version of the *currently applied web bundle*, or `\"builtin\"`\n * when the app still runs the bundle shipped inside the binary. Sending a\n * constant here defeats version comparison entirely.\n */\n version_name?: string;\n /** Stable per-install identifier, used for channel overrides and stats. */\n deviceId?: string;\n isProd?: boolean;\n /**\n * Device facts the server stores but does not decide with. All optional: an\n * app that cannot determine one should omit it rather than send a placeholder,\n * because the server writes only the keys it receives and a placeholder would\n * overwrite a better value recorded earlier.\n */\n versionOs?: string;\n pluginVersion?: string;\n /**\n * Bundle version compiled into the binary. `version_name` is the *applied*\n * OTA bundle and is absent until one lands, so the two together are what say\n * whether a device has ever taken an update.\n */\n versionBuiltin?: string;\n isEmulator?: boolean;\n /** Caller-supplied label for this install, shown in the dashboard. */\n customId?: string;\n}\n\n/** A native binary (APK/IPA) the device should install. */\nexport interface NativeUpdatePayload {\n version_name: string;\n version_code: number;\n download_url: string;\n release_notes?: string;\n required?: boolean;\n platform?: Platform;\n /**\n * Size in bytes, so a client can warn before spending someone's mobile data\n * on 45 MB. The column is `file_size_bytes`; the two were never mapped, so\n * this was declared here and never once populated until `nativePayload`\n * translated it.\n */\n file_size?: number;\n}\n\n/**\n * The response. Every field is optional because the server returns a partial\n * object per outcome rather than a discriminated union - `resolveUpdate` below\n * narrows it into something a caller can branch on safely.\n */\nexport interface UpdateCheckResponse {\n message?: string;\n error?: string;\n\n /**\n * Classification for a response that carries no bundle. Absent - and it must\n * be absent - when one is offered. See `UpdateResponseKind`.\n */\n kind?: UpdateResponseKind;\n\n /** OTA bundle fields. */\n version_name?: string;\n /**\n * The same value as `version_name`, under the name the Capacitor plugin\n * reads.\n *\n * `CapacitorUpdaterPlugin` line 4551 calls `jsRes.getString(\"version\")`\n * unconditionally once a response is not classified, and a missing key throws\n * a JSONException that is caught as \"error in update check\". The backend sent\n * only `version_name`, so every background check the plugin made - on every\n * response, including a perfectly good bundle - ended as a failed update. Our\n * own runtime never noticed because it reads the response itself.\n */\n version?: string;\n url?: string;\n checksum?: string;\n sessionKey?: string;\n release_notes?: string;\n required?: boolean;\n\n /** Present when a native binary supersedes, or blocks, the OTA bundle. */\n native_update?: NativeUpdatePayload | null;\n\n /** Remote configuration resolved for the channel's environment. */\n config?: Record<string, string>;\n}\n\nexport type UpdateKind = \"native\" | \"ota\";\n\n/** A resolved, actionable update. */\nexport interface ResolvedUpdate {\n kind: UpdateKind;\n version: string;\n versionCode?: number;\n downloadUrl?: string;\n releaseNotes?: string;\n required: boolean;\n platform?: Platform;\n checksum?: string;\n sessionKey?: string;\n /**\n * Size in bytes of a native binary, when the server published one.\n *\n * The runtime verifies a cached download against it before reusing the file:\n * a connection dropped mid-download leaves a partial APK at the right path,\n * and installing that fails with \"There was a problem parsing the package\".\n */\n fileSize?: number;\n /** Set once the OTA plugin has downloaded the bundle. */\n bundleId?: string;\n}\n\n/**\n * Narrows a raw response into an update to act on, or `null` when there is\n * nothing to do.\n *\n * Native wins over OTA. The server can return both - a required native binary\n * alongside the OTA bundle that needs it - and installing the bundle first\n * would leave the device on a binary too old to run it.\n */\nexport function resolveUpdate(\n response: UpdateCheckResponse | null | undefined,\n): ResolvedUpdate | null {\n if (!response) return null;\n\n const native = response.native_update;\n if (native?.download_url) {\n return {\n kind: \"native\",\n version: native.version_name,\n versionCode: native.version_code,\n downloadUrl: native.download_url,\n releaseNotes: native.release_notes,\n // A native update is mandatory when the server says the OTA bundle\n // cannot run without it, whatever the record's own flag says.\n required:\n response.message === UpdateMessage.NATIVE_UPDATE_REQUIRED || (native.required ?? false),\n platform: native.platform,\n ...(typeof native.file_size === \"number\" ? { fileSize: native.file_size } : {}),\n };\n }\n\n if (response.url && response.version_name) {\n return {\n kind: \"ota\",\n version: response.version_name,\n downloadUrl: response.url,\n releaseNotes: response.release_notes,\n required: response.required ?? false,\n checksum: response.checksum,\n sessionKey: response.sessionKey,\n };\n }\n\n return null;\n}\n\n/**\n * True when the response reports a condition the user cannot fix by updating.\n * Callers should surface these instead of showing \"you are up to date\".\n */\nexport function isBlockingResponse(response: UpdateCheckResponse): boolean {\n return (\n response.message === UpdateMessage.CHANNEL_NOT_FOUND ||\n response.message === UpdateMessage.ENVIRONMENT_MISMATCH\n );\n}\n\n/**\n * Analytics events posted to `POST {endpoint}/api/native-updates/log`.\n *\n * A list rather than a bare union because `native_update_logs.event` carries a\n * CHECK constraint, and the two had drifted: the column allowed `check`,\n * `download`, `install`, `fail` and `skip` while this declared `check`,\n * `download`, `download_complete`, `install`, `cancel` and `error`. Three of\n * the six were rejected by the database, so a device reporting\n * `download_complete` - which is what one does after every native download -\n * got a 500. `native-update-events.test.ts` reads the migration and fails if\n * this list ever moves ahead of it again.\n */\nexport const UPDATE_EVENTS = [\n \"check\",\n \"download\",\n \"download_complete\",\n \"install\",\n \"cancel\",\n \"error\",\n] as const;\n\nexport type UpdateEvent = (typeof UPDATE_EVENTS)[number];\n\nexport interface UpdateEventPayload {\n event: UpdateEvent;\n platform: Platform;\n /**\n * Bundle identifier of the running build.\n *\n * `native_update_logs.app_id` is NOT NULL and the server cannot resolve a row\n * without it, so it rejects a payload that omits this with a 400. This field\n * was missing from the contract and from the app runtime, so **every** native\n * download, install and error event was rejected - and because the runtime\n * catches the failure and warns, nothing ever surfaced. It was found by\n * reading the WebView console on a device mid-install.\n */\n app_id: string;\n device_id: string;\n current_version_code: number;\n new_version?: string;\n new_version_code?: number;\n channel: string;\n environment: string;\n /** Failure detail for an `error` event. Older servers read `error_message`. */\n error?: string;\n}\n\n/** Field names a payload must carry for the server to record it. */\nexport const UPDATE_EVENT_REQUIRED = [\"event\", \"platform\", \"app_id\"] as const;\n\n/**\n * Validates an incoming update event, naming everything that is missing.\n *\n * Pure, and shared with the server, so \"what the client sends\" and \"what the\n * server accepts\" cannot drift the way they did here: the client sent `error`\n * and the server read `error_message`, so even a payload that got past\n * validation lost its failure detail.\n */\nexport function parseUpdateEvent(\n body: Record<string, unknown>,\n): { ok: true; event: UpdateEventPayload } | { ok: false; missing: string[] } {\n // Accepted under either name, so an app built against an older contract still\n // records rather than having its events silently dropped.\n const appId = (body.app_id ?? body.appId) as string | undefined;\n\n const missing: string[] = UPDATE_EVENT_REQUIRED.filter((field) =>\n field === \"app_id\" ? !appId : !body[field],\n );\n\n if (missing.length > 0) return { ok: false, missing };\n\n return {\n ok: true,\n event: {\n event: body.event as UpdateEvent,\n platform: body.platform as Platform,\n app_id: appId as string,\n device_id: (body.device_id ?? \"\") as string,\n current_version_code: Number(body.current_version_code ?? 0),\n new_version: body.new_version as string | undefined,\n new_version_code:\n body.new_version_code === undefined ? undefined : Number(body.new_version_code),\n channel: (body.channel ?? \"\") as string,\n environment: (body.environment ?? \"\") as string,\n error: (body.error ?? body.error_message) as string | undefined,\n },\n };\n}\n","import type { Environment } from \"./update-contract.js\";\n\n/**\n * `.capuchoo/project.json` - the file that makes an application deployable.\n *\n * Version 1 held only the cloud identifiers, so the CLI had to *guess* how to\n * build: it shelled out to `pnpm run assets:<env>`, `pnpm build:<env>`,\n * `pnpm trapeze:<env>` and `pnpm exec cap sync`. Any application that named\n * its scripts differently, used npm, or had not installed Trapeze simply\n * failed halfway through a deploy.\n *\n * Version 2 describes the *inputs* instead - where each flavour's env file,\n * Trapeze config and icon sources live - and lets the CLI own the execution.\n * Every field has a default, so a v1 file keeps working: `normaliseProjectConfig`\n * fills in the conventional layout both existing apps already use.\n */\nexport const PROJECT_CONFIG_VERSION = 2;\n\n/** One build flavour, keyed by the environment its channels are bound to. */\nexport interface FlavourConfig {\n /**\n * Env file supplying `VITE_APP_ID`, `VITE_APP_NAME`, `VITE_UPDATE_CHANNEL`\n * and friends. The CLI reads it and passes the values to the build and to\n * the native configuration step as environment variables - it does not\n * rewrite the file, so a deploy leaves the working tree clean.\n */\n envFile: string;\n /** Trapeze config applied to the native projects. Optional. */\n trapezeConfig?: string;\n /** Directory holding `icon.png` / `splash.png` for icon generation. */\n assetPath?: string;\n /** Vite `--mode`. Defaults to the flavour name. */\n mode?: string;\n}\n\nexport interface BuildConfig {\n /**\n * Overrides the web build. Leave unset and the CLI runs the project's own\n * Vite build through the workspace toolchain it detects.\n */\n command?: string;\n /** Where to run the build from, relative to the app. For monorepo roots. */\n cwd?: string;\n}\n\nexport interface ProjectConfig {\n /** Absent on v1 files. */\n version?: number;\n\n /** Bundle identifier of the production flavour. */\n appId: string;\n /** Primary key of the application in Capuchoo. */\n cloudAppId: string;\n appName: string;\n createdAt: string;\n\n /** Vite output directory, and what Capacitor copies into the native app. */\n webDir?: string;\n androidDir?: string;\n iosDir?: string;\n\n /**\n * Monotonic native build numbers per environment. Written by the CLI, and\n * the one file a deploy is expected to modify.\n */\n versionCodeFile?: string;\n\n flavours?: Partial<Record<Environment, FlavourConfig>>;\n build?: BuildConfig;\n\n /** Optional GitHub Pages mirror for generated web assets. */\n ghPagesRepo?: string;\n\n /** @deprecated v1 fields, folded into `build` by `normaliseProjectConfig`. */\n monorepoRoot?: string;\n /** @deprecated v1 field. */\n packageName?: string;\n}\n\n/** A `ProjectConfig` with every optional resolved. */\nexport interface ResolvedProjectConfig {\n version: number;\n appId: string;\n cloudAppId: string;\n appName: string;\n createdAt: string;\n webDir: string;\n androidDir: string;\n iosDir: string;\n versionCodeFile: string;\n flavours: Record<Environment, FlavourConfig>;\n build: BuildConfig;\n ghPagesRepo?: string;\n}\n\nexport const ENVIRONMENTS: readonly Environment[] = [\"dev\", \"staging\", \"prod\"];\n\n/**\n * The layout both existing applications already use. Used as the default so a\n * v1 `project.json` needs no migration to keep deploying.\n */\nexport function defaultFlavour(environment: Environment): FlavourConfig {\n return {\n envFile: `build/${environment}/.env.${environment}`,\n trapezeConfig: `build/${environment}/trapeze.${environment}.yaml`,\n assetPath: `build/${environment}/assets`,\n mode: environment,\n };\n}\n\nexport function normaliseProjectConfig(config: ProjectConfig): ResolvedProjectConfig {\n const flavours = {} as Record<Environment, FlavourConfig>;\n for (const environment of ENVIRONMENTS) {\n const defaults = defaultFlavour(environment);\n const declared = config.flavours?.[environment];\n flavours[environment] = {\n envFile: declared?.envFile ?? defaults.envFile,\n trapezeConfig: declared?.trapezeConfig ?? defaults.trapezeConfig,\n assetPath: declared?.assetPath ?? defaults.assetPath,\n mode: declared?.mode ?? defaults.mode,\n };\n }\n\n // v1 expressed monorepo builds as `monorepoRoot` + `packageName`, which the\n // CLI turned into `pnpm exec vp run <pkg>#build:<env>`. Carry that forward as\n // an explicit build command so the behaviour is visible rather than implied.\n const build: BuildConfig = { ...config.build };\n if (!build.cwd && config.monorepoRoot) build.cwd = config.monorepoRoot;\n if (!build.command && config.packageName) {\n build.command = `vp run ${config.packageName}#build`;\n }\n\n return {\n version: config.version ?? 1,\n appId: config.appId,\n cloudAppId: config.cloudAppId,\n appName: config.appName,\n createdAt: config.createdAt,\n webDir: config.webDir ?? \"dist\",\n androidDir: config.androidDir ?? \"android\",\n iosDir: config.iosDir ?? \"ios\",\n versionCodeFile: config.versionCodeFile ?? \"version-code.json\",\n flavours,\n build,\n ghPagesRepo: config.ghPagesRepo,\n };\n}\n\n/** Fields a `project.json` must carry for a deploy to be possible. */\nexport function validateProjectConfig(config: Partial<ProjectConfig> | null | undefined): string[] {\n if (!config) return [\"project.json is missing or empty\"];\n\n const problems: string[] = [];\n if (!config.appId) problems.push(\"appId is required\");\n if (!config.cloudAppId) problems.push(\"cloudAppId is required\");\n if (!config.appName) problems.push(\"appName is required\");\n\n if (config.appId && !isValidBundleId(config.appId)) {\n problems.push(`appId \"${config.appId}\" is not a valid bundle identifier`);\n }\n\n return problems;\n}\n\nconst BUNDLE_ID = /^[a-z][a-z\\d_]*(\\.[a-z][a-z\\d_]*)+$/;\n\nexport function isValidBundleId(value: string): boolean {\n return BUNDLE_ID.test(value);\n}\n\n/**\n * Derives the environment a bundle identifier belongs to.\n *\n * The backend enforces the same rule server-side: a `.staging` build may only\n * be served staging channels. Mirroring it here lets the CLI refuse a\n * mismatched deploy before it uploads several megabytes.\n */\nexport function environmentFromAppId(appId: string): Environment {\n const id = appId.toLowerCase();\n if (id.endsWith(\".staging\")) return \"staging\";\n if (id.endsWith(\".dev\") || id.endsWith(\".debug\")) return \"dev\";\n return \"prod\";\n}\n\n/**\n * Whether a build may be served a channel bound to `channelEnvironment`.\n *\n * This mirrors the server's isolation check exactly, including its one\n * deliberate exception: a production build is allowed on a staging channel, so\n * a release candidate can be beta-tested by real installs without shipping a\n * separate bundle identifier.\n *\n * The rule lives here rather than being restated at each call site because the\n * CLI had reimplemented it as a plain equality check - which is *stricter* than\n * the server and rejected the exact beta-testing setup Lowmaro uses, where all\n * three channels are bound to staging and the app id carries no suffix.\n */\nexport function isEnvironmentAllowed(appId: string, channelEnvironment: Environment): boolean {\n const expected = environmentFromAppId(appId);\n if (expected === channelEnvironment) return true;\n return expected === \"prod\" && channelEnvironment === \"staging\";\n}\n\n/** Explains a rejected pairing, or null when it is allowed. */\nexport function describeEnvironmentMismatch(\n appId: string,\n channelEnvironment: Environment,\n channelName: string,\n): string | null {\n if (isEnvironmentAllowed(appId, channelEnvironment)) return null;\n\n const expected = environmentFromAppId(appId);\n return (\n `Channel \"${channelName}\" serves the ${channelEnvironment} environment, but ` +\n `the build's VITE_APP_ID is \"${appId}\", which is a ${expected} bundle id. ` +\n \"The server rejects this pairing, so the upload would be wasted.\"\n );\n}\n","import type { Environment } from \"./update-contract.js\";\n\n/**\n * Version arithmetic, kept free of any filesystem or child-process access so\n * both the CLI and the tests can use it directly.\n *\n * The CLI used to shell out to `npm version <type> --no-git-tag-version` for\n * this. In a workspace that is actively wrong: npm resolves the *nearest*\n * package.json, so running it from a monorepo root bumped the root package\n * instead of the app, and it mixed npm into a pnpm/Vite+ project for a job\n * that is three lines of string handling.\n */\n\nexport type BumpType = \"major\" | \"minor\" | \"patch\";\n\nexport interface SemanticVersion {\n major: number;\n minor: number;\n patch: number;\n prerelease?: string;\n build?: string;\n}\n\nconst SEMVER =\n /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([\\dA-Za-z-]+(?:\\.[\\dA-Za-z-]+)*))?(?:\\+([\\dA-Za-z-]+(?:\\.[\\dA-Za-z-]+)*))?$/;\n\nexport function parseVersion(value: string): SemanticVersion | null {\n const match = SEMVER.exec(value.trim());\n if (!match) return null;\n\n return {\n major: Number(match[1]),\n minor: Number(match[2]),\n patch: Number(match[3]),\n prerelease: match[4],\n build: match[5],\n };\n}\n\nexport function formatVersion(version: SemanticVersion): string {\n let out = `${version.major}.${version.minor}.${version.patch}`;\n if (version.prerelease) out += `-${version.prerelease}`;\n if (version.build) out += `+${version.build}`;\n return out;\n}\n\n/**\n * Bumps a version string. Prerelease and build metadata are dropped, matching\n * `npm version` semantics for a plain major/minor/patch bump.\n */\nexport function bumpVersion(value: string, type: BumpType): string {\n const parsed = parseVersion(value);\n if (!parsed) {\n throw new Error(`\"${value}\" is not a semantic version, so it cannot be bumped`);\n }\n\n switch (type) {\n case \"major\":\n return formatVersion({ major: parsed.major + 1, minor: 0, patch: 0 });\n case \"minor\":\n return formatVersion({\n major: parsed.major,\n minor: parsed.minor + 1,\n patch: 0,\n });\n case \"patch\":\n return formatVersion({\n major: parsed.major,\n minor: parsed.minor,\n patch: parsed.patch + 1,\n });\n }\n}\n\n/**\n * Compares two semantic versions. Returns a negative number when `a` is older.\n *\n * A missing or unparseable version sorts oldest, which is what the app needs:\n * the sentinel `\"builtin\"` must always look older than any published bundle.\n */\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n\n if (!left && !right) return 0;\n if (!left) return -1;\n if (!right) return 1;\n\n if (left.major !== right.major) return left.major - right.major;\n if (left.minor !== right.minor) return left.minor - right.minor;\n if (left.patch !== right.patch) return left.patch - right.patch;\n\n // 1.0.0-beta precedes 1.0.0.\n if (left.prerelease && !right.prerelease) return -1;\n if (!left.prerelease && right.prerelease) return 1;\n if (left.prerelease && right.prerelease) {\n return left.prerelease < right.prerelease ? -1 : left.prerelease > right.prerelease ? 1 : 0;\n }\n\n return 0;\n}\n\nexport type VersionCodes = Record<Environment, number>;\n\nexport const INITIAL_VERSION_CODES: VersionCodes = {\n dev: 1,\n staging: 1,\n prod: 1,\n};\n\n/**\n * Native build numbers must increase monotonically per environment: Android\n * refuses to install an APK whose versionCode is not greater than the\n * installed one, and the backend uses the same number to decide whether a\n * native update supersedes an OTA bundle.\n */\nexport function nextVersionCode(\n codes: Partial<VersionCodes> | null | undefined,\n environment: Environment,\n): VersionCodes {\n const current: VersionCodes = { ...INITIAL_VERSION_CODES, ...codes };\n return { ...current, [environment]: (current[environment] ?? 0) + 1 };\n}\n\n/**\n * Build-time variables injected into the web build and the native\n * configuration step.\n *\n * These used to be written *into* the committed `build/<env>/.env.<env>` file\n * by every deploy, which dirtied the working tree and made two concurrent\n * deploys race over one file. They are environment variables now: Trapeze\n * reads its `vars:` block from the process environment, and Vite reads\n * `VITE_*` the same way.\n */\nexport function versionEnv(version: string, versionCode: number) {\n return {\n VITE_APP_VERSION: version,\n VERSION_CODE: String(versionCode),\n BUILD_NUMBER: String(versionCode),\n };\n}\n","/**\n * What a device should install, decided from what the server found.\n *\n * This is the rule that governs every install of every app, and until this file\n * existed it lived as a two-hundred-line branch inside `updateService`,\n * interleaved with five Supabase round trips. It had no tests - not because it\n * was unimportant but because it could not be called without a database. So the\n * only harness available was a physical phone, and every defect in it was found\n * that way: a native binary served in the OTA `url` field, `required` dropped\n * in transit, release notes stored and never sent, a native release the channel\n * never pointed at.\n *\n * Those are one bug, five times: an unexecutable specification. So the decision\n * is separated from the fetching here. `decideUpdate` is pure and total - it\n * takes facts and returns one of a closed set of outcomes - and\n * `renderUpdateResponse` is the only place a wire response is shaped. Both run\n * in microseconds against a table of cases, which is where this class of defect\n * has to be caught, because a phone in someone's hand is not a test suite.\n *\n * The backend had also reimplemented three rules this package already exports:\n * semantic version comparison, the environment isolation check, and the message\n * strings. Copies drift; these do not.\n */\n\nimport { isEnvironmentAllowed } from \"./project-config.js\";\nimport {\n UpdateMessage,\n type Environment,\n type NativeUpdatePayload,\n type Platform,\n type UpdateCheckResponse,\n} from \"./update-contract.js\";\nimport { compareVersions } from \"./version.js\";\n\n/** The build a device is running, as it reports itself. */\nexport interface DeviceState {\n /** Bundle identifier of the binary, which carries its environment suffix. */\n appId: string;\n platform: Platform;\n /** Native build number. 0 when the device did not report one. */\n versionCode: number;\n /** Applied OTA bundle version, or `\"builtin\"` when none has landed. */\n versionName: string;\n}\n\nexport interface ChannelState {\n name: string;\n environment: Environment;\n}\n\n/**\n * A native binary row.\n *\n * `file_size_bytes` is the column name; the wire field is `file_size`. They\n * were never mapped, so the contract's `file_size` has never once been\n * populated - `renderUpdateResponse` is where that is now translated.\n */\nexport interface NativeRelease {\n version_name: string;\n version_code: number;\n download_url: string;\n platform: Platform;\n required?: boolean | null;\n release_notes?: string | null;\n file_size_bytes?: number | null;\n}\n\n/** An OTA bundle row. `url` is already resolved to something downloadable. */\nexport interface OtaRelease {\n version_name: string;\n url: string;\n platform: Platform;\n checksum?: string | null;\n session_key?: string | null;\n /** Native build number this bundle needs; below it, it must not be served. */\n min_update_version?: string | number | null;\n required?: boolean | null;\n release_notes?: string | null;\n}\n\n/** Everything the server looked up. Facts only - no decisions. */\nexport interface UpdateFacts {\n device: DeviceState;\n /** null when no app carries the requested bundle identifier. */\n app: { id: string } | null;\n /** null when the app has no channel by the requested name. */\n channel: ChannelState | null;\n /** The native binary the channel points at, if any. */\n native: NativeRelease | null;\n /** The OTA bundle the channel points at, if any. */\n ota: OtaRelease | null;\n}\n\n/**\n * The closed set of outcomes.\n *\n * Every one is named, including the three that used to share a bare\n * `{ config: {} }` response: a channel with no bundle, a bundle built for\n * another platform, and a device already up to date were indistinguishable on\n * the wire, so \"the update did nothing\" had no diagnosis.\n */\nexport type UpdateDecision =\n | { kind: \"app-not-found\" }\n | { kind: \"channel-not-found\" }\n | { kind: \"environment-mismatch\"; expected: Environment; channel: ChannelState }\n | { kind: \"native\"; release: NativeRelease }\n | { kind: \"native-required\"; minVersionCode: number; installedVersionCode: number }\n | { kind: \"ota\"; release: OtaRelease }\n | { kind: \"no-bundle\" }\n | { kind: \"platform-mismatch\"; bundlePlatform: Platform; devicePlatform: Platform }\n | { kind: \"up-to-date\"; version: string };\n\n/** `min_update_version` as a number; absent, empty and unparseable all mean ungated. */\nfunction minimumNativeVersion(ota: OtaRelease): number {\n const raw = ota.min_update_version;\n if (raw === null || raw === undefined || raw === \"\") return 0;\n const parsed = typeof raw === \"number\" ? raw : Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;\n}\n\n/**\n * Decides what to serve.\n *\n * The order is load-bearing. Native comes before OTA because the server can\n * have both, and applying a bundle to a binary too old to run it leaves the\n * device broken with no way back. The environment check comes before either, so\n * a staging build can never be handed a production bundle by asking for the\n * wrong channel.\n */\nexport function decideUpdate(facts: UpdateFacts): UpdateDecision {\n const { device, app, channel, native, ota } = facts;\n\n if (!app) return { kind: \"app-not-found\" };\n if (!channel) return { kind: \"channel-not-found\" };\n\n if (!isEnvironmentAllowed(device.appId, channel.environment)) {\n return { kind: \"environment-mismatch\", expected: channel.environment, channel };\n }\n\n // A native binary assigned to the channel and newer than the installed one\n // supersedes anything OTA. Platform is checked here rather than at the query\n // so an iOS device is never offered an APK.\n if (native && native.platform === device.platform && native.version_code > device.versionCode) {\n return { kind: \"native\", release: native };\n }\n\n if (!ota) return { kind: \"no-bundle\" };\n\n if (ota.platform !== device.platform) {\n return {\n kind: \"platform-mismatch\",\n bundlePlatform: ota.platform,\n devicePlatform: device.platform,\n };\n }\n\n // `\"builtin\"` is not a semantic version, and compareVersions sorts anything\n // unparseable oldest - which is exactly right: a device that has never taken\n // an update is behind every published bundle.\n if (compareVersions(ota.version_name, device.versionName) <= 0) {\n return { kind: \"up-to-date\", version: device.versionName };\n }\n\n const minimum = minimumNativeVersion(ota);\n if (minimum > 0 && device.versionCode < minimum) {\n return {\n kind: \"native-required\",\n minVersionCode: minimum,\n installedVersionCode: device.versionCode,\n };\n }\n\n return { kind: \"ota\", release: ota };\n}\n\n/**\n * The wire fields of a native binary, and only those.\n *\n * The previous implementation spread the database row, so every device on earth\n * received the internal `id`, `app_id`, `uploaded_by` and row timestamps.\n */\nexport function nativePayload(release: NativeRelease): NativeUpdatePayload {\n return {\n version_name: release.version_name,\n version_code: release.version_code,\n download_url: release.download_url,\n platform: release.platform,\n required: release.required ?? false,\n ...(release.release_notes ? { release_notes: release.release_notes } : {}),\n ...(typeof release.file_size_bytes === \"number\" ? { file_size: release.file_size_bytes } : {}),\n };\n}\n\nexport interface RenderContext {\n /** Remote configuration for the channel's environment. */\n config: Record<string, unknown>;\n /**\n * The binary satisfying a blocked bundle's `min_update_version`, when one was\n * found. Only consulted for a `native-required` decision, and null when the\n * publisher gated a bundle behind a build they never uploaded.\n */\n gate?: NativeRelease | null;\n}\n\n/**\n * Turns a decision into the response the plugin reads.\n *\n * The one rule that must never be broken here: a native binary is offered only\n * through `native_update`, never the top-level `url`. That field is the\n * Capacitor plugin's OTA contract - it downloads whatever is there and unzips\n * it as a web bundle. An APK in it made the plugin fetch 45 MB, fail to unzip\n * it, and report \"the update could not be downloaded\" while a perfectly\n * installable update sat unread in `native_update`.\n */\nexport function renderUpdateResponse(\n decision: UpdateDecision,\n context: RenderContext,\n): UpdateCheckResponse {\n const { config } = context;\n\n switch (decision.kind) {\n // Neither carries config: there is no app, or no channel to resolve one for.\n // Both are misconfiguration rather than breakage, so they are \"blocked\" -\n // the plugin logs those at info and does not raise a failed update.\n case \"app-not-found\":\n return { message: UpdateMessage.APP_NOT_FOUND, kind: \"blocked\" };\n\n case \"channel-not-found\":\n return { message: UpdateMessage.CHANNEL_NOT_FOUND, kind: \"blocked\" };\n\n case \"environment-mismatch\":\n return { message: UpdateMessage.ENVIRONMENT_MISMATCH, kind: \"blocked\", config };\n\n case \"native\": {\n const payload = nativePayload(decision.release);\n return {\n message: UpdateMessage.NATIVE_UPDATE_AVAILABLE,\n // An update exists, but not one the plugin can download and unzip. Left\n // unclassified it would fall through to the bundle path, find no `url`,\n // and be reported as a failed update check.\n kind: \"blocked\",\n // Mirrored at the top level so a client that only reads the flat shape\n // still learns the version and whether it may be postponed.\n version_name: payload.version_name,\n version: payload.version_name,\n required: payload.required ?? false,\n ...(payload.release_notes ? { release_notes: payload.release_notes } : {}),\n native_update: payload,\n config,\n };\n }\n\n case \"native-required\":\n return {\n message: UpdateMessage.NATIVE_UPDATE_REQUIRED,\n // A bundle exists and the device may not have it yet - blocked, not\n // failed. Without this the plugin normalised the missing kind to\n // \"failed\" and raised downloadFailed on every check.\n kind: \"blocked\",\n error:\n `Native version ${decision.minVersionCode} required. ` +\n `You have ${decision.installedVersionCode}.`,\n ...(context.gate ? { version: context.gate.version_name } : {}),\n native_update: context.gate ? nativePayload(context.gate) : null,\n config,\n };\n\n case \"ota\": {\n const { release } = decision;\n return {\n version_name: release.version_name,\n // The name the plugin reads. Deliberately no `kind` here: the plugin\n // treats the mere presence of that key as \"this response carries no\n // bundle\" and never downloads.\n version: release.version_name,\n url: release.url,\n ...(release.checksum ? { checksum: release.checksum } : {}),\n ...(release.session_key ? { sessionKey: release.session_key } : {}),\n // Both were stored and then dropped in transit: a release marked\n // required arrived as optional, so a client offered \"Later\" on an\n // update nobody may postpone.\n required: release.required ?? false,\n ...(release.release_notes ? { release_notes: release.release_notes } : {}),\n config,\n };\n }\n\n // Nothing to serve and nothing wrong. \"up_to_date\" is the only non-error\n // classification the plugin has; our own `message` keeps the distinction.\n case \"no-bundle\":\n return { message: UpdateMessage.NO_BUNDLE, kind: \"up_to_date\", config };\n\n case \"platform-mismatch\":\n return { message: UpdateMessage.PLATFORM_MISMATCH, kind: \"up_to_date\", config };\n\n case \"up-to-date\":\n return {\n message: UpdateMessage.NO_UPDATE,\n kind: \"up_to_date\",\n version: decision.version,\n config,\n };\n }\n}\n\n/** One line naming the branch that fired, for the server log. */\nexport function describeDecision(decision: UpdateDecision): string {\n switch (decision.kind) {\n case \"app-not-found\":\n return \"no app carries this bundle identifier\";\n case \"channel-not-found\":\n return \"the app has no channel by that name\";\n case \"environment-mismatch\":\n return `a ${decision.expected} channel refused this build`;\n case \"native\":\n return `native ${decision.release.version_name} (code ${decision.release.version_code})`;\n case \"native-required\":\n return (\n `bundle gated behind native ${decision.minVersionCode}, ` +\n `device has ${decision.installedVersionCode}`\n );\n case \"ota\":\n return `bundle ${decision.release.version_name}`;\n case \"no-bundle\":\n return \"the channel points at no bundle\";\n case \"platform-mismatch\":\n return `the bundle is ${decision.bundlePlatform}, the device is ${decision.devicePlatform}`;\n case \"up-to-date\":\n return `already on ${decision.version}`;\n }\n}\n","import type { Environment, Platform } from \"./update-contract.js\";\n\n/** Shapes returned by the authenticated `/api/*` endpoints. */\n\nexport interface CloudOrganization {\n id: string;\n name: string;\n slug: string;\n role: \"owner\" | \"admin\" | \"member\";\n}\n\nexport interface CloudApp {\n id: string;\n name: string;\n app_id: string;\n platform: string;\n organization_id: string;\n created_at: string;\n icon_url?: string;\n}\n\nexport interface CloudChannel {\n id: string;\n name: string;\n app_id: string;\n /**\n * Which build flavour this channel serves. The CLI derives the whole build\n * from it, so a channel without an environment cannot be deployed to.\n */\n environment: Environment;\n public: boolean;\n created_at: string;\n current_version_id?: string | null;\n current_native_version_id?: string | null;\n}\n\nexport interface CloudRelease {\n id: string;\n version_name: string;\n platform: Platform;\n channel?: string;\n active: boolean;\n required: boolean;\n release_notes?: string;\n created_at: string;\n}\n\nexport interface CloudUser {\n id: string;\n email: string;\n role?: string;\n}\n\n/** Response of `GET /api/auth/me`. */\n/** What the credential in use is, and what it is allowed to touch. */\nexport interface CredentialScope {\n type: \"api_key\" | \"session\";\n /** Cloud id of the only app this key may publish to, or null for all of them. */\n app_id: string | null;\n}\n\nexport interface UserProfile {\n user: CloudUser;\n organizations: CloudOrganization[];\n apps: Array<CloudApp & { role: string }>;\n /** Absent from older backends, so treat undefined as \"unknown\", not \"unscoped\". */\n credential?: CredentialScope;\n}\n\n/**\n * Whether this credential can publish to an app.\n *\n * An app-scoped key can still list every app the account owns, so a scope\n * mismatch is invisible until an upload returns 403 - after a full build. Both\n * `init` and `doctor` check this up front.\n */\nexport function canPublishTo(profile: UserProfile, cloudAppId: string): boolean {\n const scope = profile.credential?.app_id;\n return !scope || scope === cloudAppId;\n}\n\n/** Roles allowed to create an application inside an organization. */\nexport const APP_CREATOR_ROLES: ReadonlySet<string> = new Set([\"owner\", \"admin\"]);\n\nexport function canCreateApps(organization: CloudOrganization): boolean {\n return APP_CREATOR_ROLES.has(organization.role);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capuchoo/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared contract between the Capucho CLI, backend, dashboard and app runtime",
5
5
  "keywords": [
6
6
  "capacitor",