@appforge-ci/core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent-readiness.d.ts +163 -0
  3. package/dist/agent-readiness.d.ts.map +1 -0
  4. package/dist/agent-readiness.js +179 -0
  5. package/dist/agent-readiness.js.map +1 -0
  6. package/dist/agent-readiness.test.d.ts +2 -0
  7. package/dist/agent-readiness.test.d.ts.map +1 -0
  8. package/dist/agent-readiness.test.js +177 -0
  9. package/dist/agent-readiness.test.js.map +1 -0
  10. package/dist/api-client.d.ts +529 -0
  11. package/dist/api-client.d.ts.map +1 -0
  12. package/dist/api-client.js +551 -0
  13. package/dist/api-client.js.map +1 -0
  14. package/dist/api-client.test.d.ts +2 -0
  15. package/dist/api-client.test.d.ts.map +1 -0
  16. package/dist/api-client.test.js +305 -0
  17. package/dist/api-client.test.js.map +1 -0
  18. package/dist/build-cost.test.d.ts +2 -0
  19. package/dist/build-cost.test.d.ts.map +1 -0
  20. package/dist/build-cost.test.js +21 -0
  21. package/dist/build-cost.test.js.map +1 -0
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +8 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ota-artifact.d.ts +41 -0
  27. package/dist/ota-artifact.d.ts.map +1 -0
  28. package/dist/ota-artifact.js +43 -0
  29. package/dist/ota-artifact.js.map +1 -0
  30. package/dist/ota-artifact.test.d.ts +2 -0
  31. package/dist/ota-artifact.test.d.ts.map +1 -0
  32. package/dist/ota-artifact.test.js +44 -0
  33. package/dist/ota-artifact.test.js.map +1 -0
  34. package/dist/ota-bundle-hash.d.ts +38 -0
  35. package/dist/ota-bundle-hash.d.ts.map +1 -0
  36. package/dist/ota-bundle-hash.js +40 -0
  37. package/dist/ota-bundle-hash.js.map +1 -0
  38. package/dist/ota-bundle-hash.test.d.ts +2 -0
  39. package/dist/ota-bundle-hash.test.d.ts.map +1 -0
  40. package/dist/ota-bundle-hash.test.js +28 -0
  41. package/dist/ota-bundle-hash.test.js.map +1 -0
  42. package/dist/ota-rollout.d.ts +38 -0
  43. package/dist/ota-rollout.d.ts.map +1 -0
  44. package/dist/ota-rollout.js +53 -0
  45. package/dist/ota-rollout.js.map +1 -0
  46. package/dist/ota-rollout.test.d.ts +2 -0
  47. package/dist/ota-rollout.test.d.ts.map +1 -0
  48. package/dist/ota-rollout.test.js +48 -0
  49. package/dist/ota-rollout.test.js.map +1 -0
  50. package/dist/schemas.d.ts +776 -0
  51. package/dist/schemas.d.ts.map +1 -0
  52. package/dist/schemas.js +538 -0
  53. package/dist/schemas.js.map +1 -0
  54. package/dist/token-packs.test.d.ts +2 -0
  55. package/dist/token-packs.test.d.ts.map +1 -0
  56. package/dist/token-packs.test.js +34 -0
  57. package/dist/token-packs.test.js.map +1 -0
  58. package/dist/types.d.ts +445 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +80 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +35 -0
@@ -0,0 +1,305 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ApiClient, ApiError, apiErrorMessage } from "./api-client.js";
4
+ /**
5
+ * `ApiClient.request` is the shared HTTP wrapper every single call the
6
+ * dashboard, CLI, and admin CLI make to the control-plane API goes
7
+ * through — auth header injection, JSON parsing (including the
8
+ * empty-body case), routing to the admin Worker vs. the customer-facing
9
+ * one, and wrapping a non-2xx response into `ApiError`. None of it had
10
+ * ever been exercised, despite being the single most foundational piece
11
+ * of shared code in `packages/core`. `fetchImpl` is a small, additive,
12
+ * optional test seam on `ApiClientOptions` (same "defaults to the real
13
+ * thing" shape `followBuildLogs`/`waitForCommandResolution` already
14
+ * established) — every real caller omits it and gets the real global
15
+ * `fetch`, unchanged.
16
+ */
17
+ function fakeFetch(handler) {
18
+ return (async (url, init) => {
19
+ const result = handler(url, init);
20
+ const text = "text" in result ? result.text : JSON.stringify(result.body);
21
+ return {
22
+ ok: result.status >= 200 && result.status < 300,
23
+ status: result.status,
24
+ text: async () => text,
25
+ };
26
+ });
27
+ }
28
+ test("request sends a bearer token when one is configured, and none at all when it isn't", async () => {
29
+ let capturedAuth;
30
+ const withToken = new ApiClient({
31
+ baseUrl: "https://api.test",
32
+ token: "afk_secret",
33
+ fetchImpl: fakeFetch((_, init) => {
34
+ capturedAuth = init.headers.authorization;
35
+ return { status: 200, body: { user: { id: "u1" }, orgs: [] } };
36
+ }),
37
+ });
38
+ await withToken.me();
39
+ assert.equal(capturedAuth, "Bearer afk_secret");
40
+ let sawAuthHeader = true;
41
+ const withoutToken = new ApiClient({
42
+ baseUrl: "https://api.test",
43
+ fetchImpl: fakeFetch((_, init) => {
44
+ sawAuthHeader = "authorization" in init.headers;
45
+ return { status: 200, body: { packs: [] } };
46
+ }),
47
+ });
48
+ await withoutToken.listTokenPacks();
49
+ assert.equal(sawAuthHeader, false);
50
+ });
51
+ test("request JSON-serializes a request body, and sends none at all for a bodyless call", async () => {
52
+ let capturedBody;
53
+ const client = new ApiClient({
54
+ baseUrl: "https://api.test",
55
+ fetchImpl: fakeFetch((_, init) => {
56
+ capturedBody = init.body;
57
+ return { status: 201, body: { token: "t", user: {}, org: {} } };
58
+ }),
59
+ });
60
+ await client.signup("a@example.com", "password1234", undefined, "Acme");
61
+ assert.equal(capturedBody, JSON.stringify({ email: "a@example.com", password: "password1234", name: undefined, orgName: "Acme" }));
62
+ let bodylessCaptured = "not yet set";
63
+ const bodylessClient = new ApiClient({
64
+ baseUrl: "https://api.test",
65
+ fetchImpl: fakeFetch((_, init) => {
66
+ bodylessCaptured = init.body;
67
+ return { status: 200, body: { user: {}, orgs: [] } };
68
+ }),
69
+ });
70
+ await bodylessClient.me();
71
+ assert.equal(bodylessCaptured, undefined);
72
+ });
73
+ test("request throws ApiError with the real status and parsed body on a non-2xx response", async () => {
74
+ const client = new ApiClient({
75
+ baseUrl: "https://api.test",
76
+ fetchImpl: fakeFetch(() => ({ status: 402, body: { error: "insufficient tokens" } })),
77
+ });
78
+ await assert.rejects(() => client.triggerBuild({ appId: "app-1", workflow: "default" }), (err) => {
79
+ assert.ok(err instanceof ApiError);
80
+ assert.equal(err.status, 402);
81
+ assert.deepEqual(err.body, { error: "insufficient tokens" });
82
+ return true;
83
+ });
84
+ });
85
+ // Regression pin: a real deployment sits behind Cloudflare's edge, which
86
+ // can return a plain-text/HTML error page (a gateway timeout, a WAF
87
+ // block, a rate-limit page) instead of the JSON apps/api's/apps/admin-
88
+ // api's own jsonErrorHandler always produces from inside the Worker
89
+ // itself. Before this fix, `JSON.parse`-ing that unconditionally crashed
90
+ // every caller with a raw, uncatchable SyntaxError instead of the
91
+ // ApiError they already know how to catch.
92
+ test("request wraps a non-JSON error body in ApiError instead of crashing on JSON.parse", async () => {
93
+ const client = new ApiClient({
94
+ baseUrl: "https://api.test",
95
+ fetchImpl: fakeFetch(() => ({ status: 524, text: "<html>Gateway Timeout</html>" })),
96
+ });
97
+ await assert.rejects(() => client.triggerBuild({ appId: "app-1", workflow: "default" }), (err) => {
98
+ assert.ok(err instanceof ApiError);
99
+ assert.equal(err.status, 524);
100
+ assert.equal(err.body, "<html>Gateway Timeout</html>");
101
+ return true;
102
+ });
103
+ });
104
+ // Same edge-error scenario, but on an ok-looking status — rarer, but a
105
+ // caller still deserves a clear thrown error over an uncatchable
106
+ // SyntaxError with no status or context at all.
107
+ test("request throws a clear error (not a raw SyntaxError) on a 2xx response with a non-JSON body", async () => {
108
+ const client = new ApiClient({
109
+ baseUrl: "https://api.test",
110
+ fetchImpl: fakeFetch(() => ({ status: 200, text: "<html>ok?</html>" })),
111
+ });
112
+ await assert.rejects(() => client.me(), (err) => {
113
+ assert.ok(err instanceof Error);
114
+ assert.ok(!(err instanceof SyntaxError), "must not leak a raw JSON.parse SyntaxError to the caller");
115
+ assert.match(err.message, /expected a JSON response/);
116
+ return true;
117
+ });
118
+ });
119
+ test("request treats an empty response body as null rather than throwing on JSON.parse", async () => {
120
+ const client = new ApiClient({
121
+ baseUrl: "https://api.test",
122
+ fetchImpl: fakeFetch(() => ({ status: 200, text: "" })),
123
+ });
124
+ const result = await client.deleteAgent("org-1", "agent-1");
125
+ assert.equal(result, null);
126
+ });
127
+ /**
128
+ * Regression test for this cycle's fix: `getOtaManifest` is typed
129
+ * `Promise<OtaManifest | null>`, matching the real server's behavior —
130
+ * `apps/api/src/routes/ota.ts` answers "already up to date"/"no active
131
+ * release"/"not in this install's staged rollout" with a genuine HTTP
132
+ * 204 and a truly empty body (`c.body(null, 204)`), the overwhelmingly
133
+ * common response for a polling OTA-check endpoint. Before this fix,
134
+ * `request()`'s empty-body fallback resolved to `undefined`, not `null`
135
+ * — silently breaking that declared type contract. Any caller written
136
+ * against the type, doing the natural `if (manifest === null) return;`,
137
+ * would instead fall through and crash on `undefined.bundleUrl` on
138
+ * every single "nothing to do" response. `fakeFetch`'s `text: ""` shape
139
+ * (not `body: null`, which serializes to the *string* `"null"` and
140
+ * parses back fine either way) is what actually reproduces a real 204's
141
+ * empty body.
142
+ */
143
+ test("getOtaManifest resolves to null (not undefined) on a real empty-body 204, matching its declared type", async () => {
144
+ const client = new ApiClient({
145
+ baseUrl: "https://api.test",
146
+ fetchImpl: fakeFetch(() => ({ status: 204, text: "" })),
147
+ });
148
+ const manifest = await client.getOtaManifest("app-1", "production", "key-1");
149
+ assert.equal(manifest, null);
150
+ });
151
+ test("admin-Worker methods hit adminApiUrl when configured, and fall back to baseUrl otherwise", async () => {
152
+ let capturedUrl;
153
+ const withAdminUrl = new ApiClient({
154
+ baseUrl: "https://api.test",
155
+ adminApiUrl: "https://admin.test",
156
+ fetchImpl: fakeFetch((url) => {
157
+ capturedUrl = url;
158
+ return { status: 200, body: { agents: [] } };
159
+ }),
160
+ });
161
+ await withAdminUrl.listAgents("org-1");
162
+ assert.equal(capturedUrl, "https://admin.test/orgs/org-1/agents");
163
+ let fallbackUrl;
164
+ const withoutAdminUrl = new ApiClient({
165
+ baseUrl: "https://api.test",
166
+ fetchImpl: fakeFetch((url) => {
167
+ fallbackUrl = url;
168
+ return { status: 200, body: { agents: [] } };
169
+ }),
170
+ });
171
+ await withoutAdminUrl.listAgents("org-1");
172
+ assert.equal(fallbackUrl, "https://api.test/orgs/org-1/agents");
173
+ });
174
+ // Regression pin: `verifyEmail`/`resendVerification` (this cycle's fix)
175
+ // used to not exist on `ApiClient` at all — the dashboard's own
176
+ // verification link had no method to call, so the token from a real
177
+ // signup verification email had no client-side way to ever be redeemed.
178
+ // This just pins the request shape (method, path, body); the dashboard
179
+ // page that calls it is covered separately in apps/dashboard.
180
+ test("verifyEmail POSTs the token to /auth/verify-email; resendVerification POSTs no body to /auth/resend-verification", async () => {
181
+ let captured;
182
+ const client = new ApiClient({
183
+ baseUrl: "https://api.test",
184
+ fetchImpl: fakeFetch((url, init) => {
185
+ captured = { url, method: init.method, body: init.body };
186
+ return { status: 200, body: { ok: true } };
187
+ }),
188
+ });
189
+ await client.verifyEmail("tok_abc123");
190
+ assert.deepEqual(captured, {
191
+ url: "https://api.test/auth/verify-email",
192
+ method: "POST",
193
+ body: JSON.stringify({ token: "tok_abc123" }),
194
+ });
195
+ await client.resendVerification();
196
+ assert.deepEqual(captured, {
197
+ url: "https://api.test/auth/resend-verification",
198
+ method: "POST",
199
+ body: undefined,
200
+ });
201
+ });
202
+ // Regression pin: `changePassword` used to not exist on `ApiClient` at
203
+ // all — there was no password-rotation route anywhere in the product
204
+ // (found by a fresh Explore-subagent survey). Pins the request shape
205
+ // only; the dashboard panel that calls it is covered separately.
206
+ test("changePassword POSTs currentPassword/newPassword to /auth/change-password", async () => {
207
+ let captured;
208
+ const client = new ApiClient({
209
+ baseUrl: "https://api.test",
210
+ fetchImpl: fakeFetch((url, init) => {
211
+ captured = { url, method: init.method, body: init.body };
212
+ return { status: 200, body: { ok: true } };
213
+ }),
214
+ });
215
+ await client.changePassword("old-pw", "a-brand-new-password");
216
+ assert.deepEqual(captured, {
217
+ url: "https://api.test/auth/change-password",
218
+ method: "POST",
219
+ body: JSON.stringify({ currentPassword: "old-pw", newPassword: "a-brand-new-password" }),
220
+ });
221
+ });
222
+ test("getOtaManifest URL-encodes a channel name containing '/', matching the other OTA methods", async () => {
223
+ let capturedUrl;
224
+ const client = new ApiClient({
225
+ baseUrl: "https://api.test",
226
+ fetchImpl: fakeFetch((url) => {
227
+ capturedUrl = url;
228
+ return { status: 200, body: null };
229
+ }),
230
+ });
231
+ await client.getOtaManifest("app-1", "beta/ios", "key-1");
232
+ assert.ok(capturedUrl?.includes("/ota/beta%2Fios/manifest"), capturedUrl);
233
+ assert.ok(!capturedUrl?.includes("/ota/beta/ios/manifest"), capturedUrl);
234
+ });
235
+ // `AbortSignal.timeout`'s own internal timer is unref'd (confirmed
236
+ // directly: a bare `node -e` script with nothing else scheduled exits
237
+ // before the timer ever fires) — harmless in a real long-running process
238
+ // (the dashboard's own event loop, a CLI command already doing other
239
+ // work), but a test whose only pending work is the awaited call has
240
+ // nothing else keeping it alive. A trivial ref'd interval for the
241
+ // duration of the call fixes that — same helper shape `packages/agent`'s
242
+ // own `api.test.ts`/`self-update.test.ts` already established for the
243
+ // identical issue there.
244
+ function withRefKeepAlive(fn) {
245
+ const keepAlive = setInterval(() => { }, 1000);
246
+ return fn().finally(() => clearInterval(keepAlive));
247
+ }
248
+ /**
249
+ * The actual bug this cycle's fix closes: before it, `request()`'s one
250
+ * `fetch` call — the single method every dashboard page, CLI command, and
251
+ * admin-cli command funnels every network call through — had no timeout
252
+ * at all. A connection that stalls after opening (a network partition, a
253
+ * proxy holding the connection open without sending further bytes, a hung
254
+ * Worker instance) left the returned promise pending forever: a CLI
255
+ * command would hang the terminal indefinitely with no error printed, and
256
+ * a dashboard page's data-loading call would leave its loading state
257
+ * stuck with no way to recover short of a full refresh. The mock fetch
258
+ * mimics what a real `fetch` does once aborted — reject once the signal
259
+ * fires — to isolate "does request() actually wire the signal through"
260
+ * from "does AbortSignal.timeout itself work" (a language feature, not
261
+ * this codebase's to test). Passes a tiny `requestTimeoutMs` via the
262
+ * constructor's test seam rather than waiting out the real
263
+ * `DEFAULT_REQUEST_TIMEOUT_MS`.
264
+ */
265
+ test("request times out instead of hanging forever when the connection stalls", async () => {
266
+ await withRefKeepAlive(() => {
267
+ const client = new ApiClient({
268
+ baseUrl: "https://api.test",
269
+ requestTimeoutMs: 20,
270
+ fetchImpl: (async (_url, init) => {
271
+ return new Promise((_resolve, reject) => {
272
+ init?.signal?.addEventListener("abort", () => reject(new Error("The operation was aborted")));
273
+ });
274
+ }),
275
+ });
276
+ return assert.rejects(() => client.me());
277
+ });
278
+ });
279
+ /**
280
+ * `apiErrorMessage` — the shared "what should the user see" extraction
281
+ * pulled out after the identical bug (an `ApiError`'s raw
282
+ * `API error ${status}: {...}` wire string shown instead of the
283
+ * server's own `body.error` text) was found and fixed independently in
284
+ * `Login.tsx`/`Signup.tsx`/`Device.tsx`/`VerifyEmail.tsx` and
285
+ * `VncViewer.tsx`'s own `vncViewerErrorMessage`. See ROADMAP.md's
286
+ * "shared apiErrorMessage helper" entry for the ~30 remaining call
287
+ * sites this is meant to eventually replace.
288
+ */
289
+ test("apiErrorMessage extracts the server's body.error for an ApiError that has one", () => {
290
+ const err = new ApiError(409, { error: "app has an active build — wait for it to finish or cancel it first" });
291
+ assert.equal(apiErrorMessage(err, "fallback"), "app has an active build — wait for it to finish or cancel it first");
292
+ });
293
+ test("apiErrorMessage falls back to the ApiError's own message when body.error is missing or not a string", () => {
294
+ assert.equal(apiErrorMessage(new ApiError(500, {}), "fallback"), new ApiError(500, {}).message);
295
+ assert.equal(apiErrorMessage(new ApiError(500, { error: 42 }), "fallback"), new ApiError(500, { error: 42 }).message);
296
+ assert.equal(apiErrorMessage(new ApiError(500, null), "fallback"), new ApiError(500, null).message);
297
+ });
298
+ test("apiErrorMessage uses a plain Error's own message, never the ApiError-only body.error path", () => {
299
+ assert.equal(apiErrorMessage(new Error("network down"), "fallback"), "network down");
300
+ });
301
+ test("apiErrorMessage uses the caller's fallback for a non-Error throw", () => {
302
+ assert.equal(apiErrorMessage("some string throw", "fallback"), "fallback");
303
+ assert.equal(apiErrorMessage(undefined, "fallback"), "fallback");
304
+ });
305
+ //# sourceMappingURL=api-client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.test.js","sourceRoot":"","sources":["../src/api-client.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,SAAS,SAAS,CAChB,OAAiH;IAEjH,OAAO,CAAC,KAAK,EAAE,GAAW,EAAE,IAAiB,EAAE,EAAE;QAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1E,OAAO;YACL,EAAE,EAAE,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC,MAAM,GAAG,GAAG;YAC/C,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI;SACX,CAAC;IAChB,CAAC,CAA4B,CAAC;AAChC,CAAC;AAED,IAAI,CAAC,oFAAoF,EAAE,KAAK,IAAI,EAAE;IACpG,IAAI,YAAgC,CAAC;IACrC,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC;QAC9B,OAAO,EAAE,kBAAkB;QAC3B,KAAK,EAAE,YAAY;QACnB,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE;YAC/B,YAAY,GAAI,IAAI,CAAC,OAAkC,CAAC,aAAa,CAAC;YACtE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC;QACjE,CAAC,CAAC;KACH,CAAC,CAAC;IACH,MAAM,SAAS,CAAC,EAAE,EAAE,CAAC;IACrB,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC;IAEhD,IAAI,aAAa,GAAG,IAAI,CAAC;IACzB,MAAM,YAAY,GAAG,IAAI,SAAS,CAAC;QACjC,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE;YAC/B,aAAa,GAAG,eAAe,IAAK,IAAI,CAAC,OAAkC,CAAC;YAC5E,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;QAC9C,CAAC,CAAC;KACH,CAAC,CAAC;IACH,MAAM,YAAY,CAAC,cAAc,EAAE,CAAC;IACpC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;AACrC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mFAAmF,EAAE,KAAK,IAAI,EAAE;IACnG,IAAI,YAAqB,CAAC;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE;YAC/B,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC;YACzB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC;QAClE,CAAC,CAAC;KACH,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IACxE,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAEnI,IAAI,gBAAgB,GAAY,aAAa,CAAC;IAC9C,MAAM,cAAc,GAAG,IAAI,SAAS,CAAC;QACnC,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE;YAC/B,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC;YAC7B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC;QACvD,CAAC,CAAC;KACH,CAAC,CAAC;IACH,MAAM,cAAc,CAAC,EAAE,EAAE,CAAC;IAC1B,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;AAC5C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oFAAoF,EAAE,KAAK,IAAI,EAAE;IACpG,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,CAAC,CAAC;KACtF,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAClE,CAAC,GAAY,EAAE,EAAE;QACf,MAAM,CAAC,EAAE,CAAC,GAAG,YAAY,QAAQ,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC9B,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;QAC7D,OAAO,IAAI,CAAC;IACd,CAAC,CACF,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,yEAAyE;AACzE,oEAAoE;AACpE,uEAAuE;AACvE,oEAAoE;AACpE,yEAAyE;AACzE,kEAAkE;AAClE,2CAA2C;AAC3C,IAAI,CAAC,mFAAmF,EAAE,KAAK,IAAI,EAAE;IACnG,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,8BAA8B,EAAE,CAAC,CAAC;KACpF,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAClE,CAAC,GAAY,EAAE,EAAE;QACf,MAAM,CAAC,EAAE,CAAC,GAAG,YAAY,QAAQ,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,8BAA8B,CAAC,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC,CACF,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,uEAAuE;AACvE,iEAAiE;AACjE,gDAAgD;AAChD,IAAI,CAAC,6FAA6F,EAAE,KAAK,IAAI,EAAE;IAC7G,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,CAAC;KACxE,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EACjB,CAAC,GAAY,EAAE,EAAE;QACf,MAAM,CAAC,EAAE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,YAAY,WAAW,CAAC,EAAE,0DAA0D,CAAC,CAAC;QACrG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,0BAA0B,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC,CACF,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;IAClG,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;KACxD,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC5D,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,IAAI,CAAC,sGAAsG,EAAE,KAAK,IAAI,EAAE;IACtH,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;KACxD,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;IAC7E,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0FAA0F,EAAE,KAAK,IAAI,EAAE;IAC1G,IAAI,WAA+B,CAAC;IACpC,MAAM,YAAY,GAAG,IAAI,SAAS,CAAC;QACjC,OAAO,EAAE,kBAAkB;QAC3B,WAAW,EAAE,oBAAoB;QACjC,SAAS,EAAE,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE;YAC3B,WAAW,GAAG,GAAG,CAAC;YAClB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;QAC/C,CAAC,CAAC;KACH,CAAC,CAAC;IACH,MAAM,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,sCAAsC,CAAC,CAAC;IAElE,IAAI,WAA+B,CAAC;IACpC,MAAM,eAAe,GAAG,IAAI,SAAS,CAAC;QACpC,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE;YAC3B,WAAW,GAAG,GAAG,CAAC;YAClB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;QAC/C,CAAC,CAAC;KACH,CAAC,CAAC;IACH,MAAM,eAAe,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,oCAAoC,CAAC,CAAC;AAClE,CAAC,CAAC,CAAC;AAEH,wEAAwE;AACxE,gEAAgE;AAChE,oEAAoE;AACpE,wEAAwE;AACxE,uEAAuE;AACvE,8DAA8D;AAC9D,IAAI,CAAC,kHAAkH,EAAE,KAAK,IAAI,EAAE;IAClI,IAAI,QAAoE,CAAC;IACzE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;YACjC,QAAQ,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAgB,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;YACnE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;QAC7C,CAAC,CAAC;KACH,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IACvC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE;QACzB,GAAG,EAAE,oCAAoC;QACzC,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;KAC9C,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,kBAAkB,EAAE,CAAC;IAClC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE;QACzB,GAAG,EAAE,2CAA2C;QAChD,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,SAAS;KAChB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,uEAAuE;AACvE,qEAAqE;AACrE,qEAAqE;AACrE,iEAAiE;AACjE,IAAI,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;IAC3F,IAAI,QAAoE,CAAC;IACzE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;YACjC,QAAQ,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAgB,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;YACnE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;QAC7C,CAAC,CAAC;KACH,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,sBAAsB,CAAC,CAAC;IAC9D,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE;QACzB,GAAG,EAAE,uCAAuC;QAC5C,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,eAAe,EAAE,QAAQ,EAAE,WAAW,EAAE,sBAAsB,EAAE,CAAC;KACzF,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0FAA0F,EAAE,KAAK,IAAI,EAAE;IAC1G,IAAI,WAA+B,CAAC;IACpC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE;YAC3B,WAAW,GAAG,GAAG,CAAC;YAClB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACrC,CAAC,CAAC;KACH,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IAC1D,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,0BAA0B,CAAC,EAAE,WAAW,CAAC,CAAC;IAC1E,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAAE,WAAW,CAAC,CAAC;AAC3E,CAAC,CAAC,CAAC;AAEH,mEAAmE;AACnE,sEAAsE;AACtE,yEAAyE;AACzE,qEAAqE;AACrE,oEAAoE;AACpE,kEAAkE;AAClE,yEAAyE;AACzE,sEAAsE;AACtE,yBAAyB;AACzB,SAAS,gBAAgB,CAAI,EAAoB;IAC/C,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,GAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IAC9C,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,IAAI,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;IACzF,MAAM,gBAAgB,CAAC,GAAG,EAAE;QAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;YAC3B,OAAO,EAAE,kBAAkB;YAC3B,gBAAgB,EAAE,EAAE;YACpB,SAAS,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,IAAkB,EAAE,EAAE;gBACrD,OAAO,IAAI,OAAO,CAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE;oBAChD,IAAI,EAAE,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC;gBAChG,CAAC,CAAC,CAAC;YACL,CAAC,CAA4B;SAC9B,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH;;;;;;;;;GASG;AACH,IAAI,CAAC,+EAA+E,EAAE,GAAG,EAAE;IACzF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,oEAAoE,EAAE,CAAC,CAAC;IAC/G,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,oEAAoE,CAAC,CAAC;AACvH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qGAAqG,EAAE,GAAG,EAAE;IAC/G,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,IAAI,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IAChG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,QAAQ,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,IAAI,QAAQ,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IACtH,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,UAAU,CAAC,EAAE,IAAI,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;AACtG,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2FAA2F,EAAE,GAAG,EAAE;IACrG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,EAAE,cAAc,CAAC,CAAC;AACvF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,kEAAkE,EAAE,GAAG,EAAE;IAC5E,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,mBAAmB,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;IAC3E,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;AACnE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=build-cost.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-cost.test.d.ts","sourceRoot":"","sources":["../src/build-cost.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { getBuildCostTokens, BUILD_COST_TOKENS, BUILD_COST_TOKENS_BY_PLATFORM } from "./types.js";
4
+ test("getBuildCostTokens returns the flat default for every known platform (no pricing change bundled)", () => {
5
+ for (const platform of ["ios", "android", "flutter", "react-native"]) {
6
+ assert.equal(getBuildCostTokens(platform), BUILD_COST_TOKENS);
7
+ }
8
+ });
9
+ test("getBuildCostTokens converts the DB's underscore storage format (react_native)", () => {
10
+ assert.equal(getBuildCostTokens("react_native"), BUILD_COST_TOKENS_BY_PLATFORM["react-native"]);
11
+ });
12
+ test("getBuildCostTokens falls back to the flat default for an unrecognized platform instead of throwing", () => {
13
+ assert.equal(getBuildCostTokens("some-future-platform"), BUILD_COST_TOKENS);
14
+ });
15
+ test("BUILD_COST_TOKENS_BY_PLATFORM has an entry for every Platform", () => {
16
+ const platforms = ["ios", "android", "flutter", "react-native"];
17
+ for (const platform of platforms) {
18
+ assert.equal(typeof BUILD_COST_TOKENS_BY_PLATFORM[platform], "number");
19
+ }
20
+ });
21
+ //# sourceMappingURL=build-cost.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-cost.test.js","sourceRoot":"","sources":["../src/build-cost.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC;AAElG,IAAI,CAAC,kGAAkG,EAAE,GAAG,EAAE;IAC5G,KAAK,MAAM,QAAQ,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,CAAU,EAAE,CAAC;QAC9E,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAChE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+EAA+E,EAAE,GAAG,EAAE;IACzF,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,cAAc,CAAC,EAAE,6BAA6B,CAAC,cAAc,CAAC,CAAC,CAAC;AAClG,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oGAAoG,EAAE,GAAG,EAAE;IAC9G,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,sBAAsB,CAAC,EAAE,iBAAiB,CAAC,CAAC;AAC9E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+DAA+D,EAAE,GAAG,EAAE;IACzE,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,CAAU,CAAC;IACzE,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzE,CAAC;AACH,CAAC,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./schemas.js";
3
+ export * from "./api-client.js";
4
+ export * from "./agent-readiness.js";
5
+ export * from "./ota-rollout.js";
6
+ export * from "./ota-bundle-hash.js";
7
+ export * from "./ota-artifact.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./schemas.js";
3
+ export * from "./api-client.js";
4
+ export * from "./agent-readiness.js";
5
+ export * from "./ota-rollout.js";
6
+ export * from "./ota-bundle-hash.js";
7
+ export * from "./ota-artifact.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Picks which of a build's artifacts is "the OTA bundle" — shared by
3
+ * `apps/api/src/routes/ota.ts` (dashboard "promote build to OTA
4
+ * release" action) and `apps/admin-api/src/routes/agents.ts`
5
+ * (`POST /builds/:buildId/complete`'s auto-promote path). Both used to
6
+ * inline this as a single flat `Array.prototype.find` predicate:
7
+ *
8
+ * ```js
9
+ * artifacts.find(
10
+ * (a) => a.fileName.endsWith(".ota.zip") || a.fileName.includes("ota") || a.fileName.endsWith(".bundle"),
11
+ * ) ?? artifacts[0]
12
+ * ```
13
+ *
14
+ * That was a real bug, not just duplicated logic: `find`'s three OR'd
15
+ * clauses have no priority between them — it returns whichever array
16
+ * element satisfies *any one* of the three first, so a build's very
17
+ * first artifact whose filename merely *contains* the substring "ota"
18
+ * (`quota-report.json`, `rotation-test.log`, `annotation-summary.xml`)
19
+ * would win over a real `app-release.ota.zip` listed later, purely
20
+ * because artifacts are read back in upload order
21
+ * (`packages/db/src/builds.ts`'s `listArtifacts`, `ORDER BY createdAt
22
+ * ASC`), which mirrors the order a workflow's own `artifacts:` glob
23
+ * patterns are listed in `appforge.yaml` — entirely ordinary config,
24
+ * e.g. a test-report glob listed before the release-bundle glob. The
25
+ * consequence: the wrong file gets promoted as the OTA release,
26
+ * `GET /apps/:appId/ota/:channel/manifest` hands it out to every
27
+ * client on that channel, and a device attempting to apply a JSON/log
28
+ * file as an app update breaks the update flow (or crashes the app, if
29
+ * it's loaded as a JS bundle) — with admin-api's auto-publish path
30
+ * (any workflow with `ota: {...}` that succeeds) doing this with no
31
+ * human review at all.
32
+ *
33
+ * Fixed by ranking, not flat-OR'ing: a precise match
34
+ * (`.ota.zip`/`.bundle` suffix) always wins over the loose substring
35
+ * heuristic, which only ever gets a chance if nothing precise matched
36
+ * anywhere in the list — never mid-scan against a single element.
37
+ */
38
+ export declare function pickOtaArtifact<T extends {
39
+ fileName: string;
40
+ }>(artifacts: T[]): T | undefined;
41
+ //# sourceMappingURL=ota-artifact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-artifact.d.ts","sourceRoot":"","sources":["../src/ota-artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,EAAE,SAAS,EAAE,CAAC,EAAE,GAAG,CAAC,GAAG,SAAS,CAM7F"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Picks which of a build's artifacts is "the OTA bundle" — shared by
3
+ * `apps/api/src/routes/ota.ts` (dashboard "promote build to OTA
4
+ * release" action) and `apps/admin-api/src/routes/agents.ts`
5
+ * (`POST /builds/:buildId/complete`'s auto-promote path). Both used to
6
+ * inline this as a single flat `Array.prototype.find` predicate:
7
+ *
8
+ * ```js
9
+ * artifacts.find(
10
+ * (a) => a.fileName.endsWith(".ota.zip") || a.fileName.includes("ota") || a.fileName.endsWith(".bundle"),
11
+ * ) ?? artifacts[0]
12
+ * ```
13
+ *
14
+ * That was a real bug, not just duplicated logic: `find`'s three OR'd
15
+ * clauses have no priority between them — it returns whichever array
16
+ * element satisfies *any one* of the three first, so a build's very
17
+ * first artifact whose filename merely *contains* the substring "ota"
18
+ * (`quota-report.json`, `rotation-test.log`, `annotation-summary.xml`)
19
+ * would win over a real `app-release.ota.zip` listed later, purely
20
+ * because artifacts are read back in upload order
21
+ * (`packages/db/src/builds.ts`'s `listArtifacts`, `ORDER BY createdAt
22
+ * ASC`), which mirrors the order a workflow's own `artifacts:` glob
23
+ * patterns are listed in `appforge.yaml` — entirely ordinary config,
24
+ * e.g. a test-report glob listed before the release-bundle glob. The
25
+ * consequence: the wrong file gets promoted as the OTA release,
26
+ * `GET /apps/:appId/ota/:channel/manifest` hands it out to every
27
+ * client on that channel, and a device attempting to apply a JSON/log
28
+ * file as an app update breaks the update flow (or crashes the app, if
29
+ * it's loaded as a JS bundle) — with admin-api's auto-publish path
30
+ * (any workflow with `ota: {...}` that succeeds) doing this with no
31
+ * human review at all.
32
+ *
33
+ * Fixed by ranking, not flat-OR'ing: a precise match
34
+ * (`.ota.zip`/`.bundle` suffix) always wins over the loose substring
35
+ * heuristic, which only ever gets a chance if nothing precise matched
36
+ * anywhere in the list — never mid-scan against a single element.
37
+ */
38
+ export function pickOtaArtifact(artifacts) {
39
+ return (artifacts.find((a) => a.fileName.endsWith(".ota.zip") || a.fileName.endsWith(".bundle")) ??
40
+ artifacts.find((a) => a.fileName.includes("ota")) ??
41
+ artifacts[0]);
42
+ }
43
+ //# sourceMappingURL=ota-artifact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-artifact.js","sourceRoot":"","sources":["../src/ota-artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,eAAe,CAAiC,SAAc;IAC5E,OAAO,CACL,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACxF,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACjD,SAAS,CAAC,CAAC,CAAC,CACb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ota-artifact.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-artifact.test.d.ts","sourceRoot":"","sources":["../src/ota-artifact.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { pickOtaArtifact } from "./ota-artifact.js";
4
+ test("pickOtaArtifact picks a precise .ota.zip suffix match over an earlier, unrelated artifact", () => {
5
+ const artifacts = [
6
+ { fileName: "app-release.apk" },
7
+ { fileName: "index.android.ota.zip" },
8
+ ];
9
+ assert.equal(pickOtaArtifact(artifacts), artifacts[1]);
10
+ });
11
+ test("pickOtaArtifact picks a precise .bundle suffix match over an earlier, unrelated artifact", () => {
12
+ const artifacts = [{ fileName: "app-release.apk" }, { fileName: "index.android.bundle" }];
13
+ assert.equal(pickOtaArtifact(artifacts), artifacts[1]);
14
+ });
15
+ test("pickOtaArtifact regression: a loose substring match listed FIRST no longer beats a precise match listed later", () => {
16
+ // This is the exact bug: a flat `Array.find` with three OR'd clauses
17
+ // has no priority between them, so it returns whichever element
18
+ // satisfies ANY clause first. "quota-report.json" merely *contains*
19
+ // "ota" — it isn't a real OTA bundle — but under the old code it was
20
+ // returned outright since it's first in the array and .find never
21
+ // looked further once it matched.
22
+ const artifacts = [
23
+ { fileName: "quota-report.json" },
24
+ { fileName: "app-release.ota.zip" },
25
+ ];
26
+ assert.equal(pickOtaArtifact(artifacts), artifacts[1]);
27
+ // The old, buggy predicate for comparison — proves this is the exact
28
+ // regression being fixed, not a hypothetical.
29
+ const oldPick = (arts) => arts.find((a) => a.fileName.endsWith(".ota.zip") || a.fileName.includes("ota") || a.fileName.endsWith(".bundle")) ??
30
+ arts[0];
31
+ assert.equal(oldPick(artifacts), artifacts[0]);
32
+ });
33
+ test("pickOtaArtifact falls back to the loose substring match only when nothing precise matches anywhere", () => {
34
+ const artifacts = [{ fileName: "app-release.apk" }, { fileName: "rotation-test.log" }];
35
+ assert.equal(pickOtaArtifact(artifacts), artifacts[1]);
36
+ });
37
+ test("pickOtaArtifact falls back to artifacts[0] when nothing matches at all", () => {
38
+ const artifacts = [{ fileName: "app-release.apk" }, { fileName: "build.log" }];
39
+ assert.equal(pickOtaArtifact(artifacts), artifacts[0]);
40
+ });
41
+ test("pickOtaArtifact returns undefined for an empty artifacts array", () => {
42
+ assert.equal(pickOtaArtifact([]), undefined);
43
+ });
44
+ //# sourceMappingURL=ota-artifact.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-artifact.test.js","sourceRoot":"","sources":["../src/ota-artifact.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,IAAI,CAAC,2FAA2F,EAAE,GAAG,EAAE;IACrG,MAAM,SAAS,GAAG;QAChB,EAAE,QAAQ,EAAE,iBAAiB,EAAE;QAC/B,EAAE,QAAQ,EAAE,uBAAuB,EAAE;KACtC,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0FAA0F,EAAE,GAAG,EAAE;IACpG,MAAM,SAAS,GAAG,CAAC,EAAE,QAAQ,EAAE,iBAAiB,EAAE,EAAE,EAAE,QAAQ,EAAE,sBAAsB,EAAE,CAAC,CAAC;IAC1F,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+GAA+G,EAAE,GAAG,EAAE;IACzH,qEAAqE;IACrE,gEAAgE;IAChE,oEAAoE;IACpE,qEAAqE;IACrE,kEAAkE;IAClE,kCAAkC;IAClC,MAAM,SAAS,GAAG;QAChB,EAAE,QAAQ,EAAE,mBAAmB,EAAE;QACjC,EAAE,QAAQ,EAAE,qBAAqB,EAAE;KACpC,CAAC;IAEF,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvD,qEAAqE;IACrE,8CAA8C;IAC9C,MAAM,OAAO,GAAG,CAAC,IAAsB,EAAE,EAAE,CACzC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACjH,IAAI,CAAC,CAAC,CAAC,CAAC;IACV,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oGAAoG,EAAE,GAAG,EAAE;IAC9G,MAAM,SAAS,GAAG,CAAC,EAAE,QAAQ,EAAE,iBAAiB,EAAE,EAAE,EAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAC,CAAC;IACvF,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wEAAwE,EAAE,GAAG,EAAE;IAClF,MAAM,SAAS,GAAG,CAAC,EAAE,QAAQ,EAAE,iBAAiB,EAAE,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;IAC/E,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;IAC1E,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Derives an OTA release's `bundleHash` from the promoted build's R2
3
+ * storage key (`builds/${buildId}/${fileName}`, see
4
+ * `apps/admin-api/src/lib/storage.ts` / `apps/api/src/lib/storage.ts`'s
5
+ * `artifactKey`). Both `apps/api/src/routes/ota.ts` (dashboard "promote
6
+ * build to OTA release" action) and `apps/admin-api/src/routes/
7
+ * agents.ts` (`POST /builds/:buildId/complete`'s auto-promote path,
8
+ * `packages/agent`'s only actual caller since it never sends its own
9
+ * `input.ota.bundleHash` — see `packages/agent/src/lib/builder.ts`)
10
+ * used to inline this as `storageKey.replace(/[^a-zA-Z0-9]/g,
11
+ * "").slice(-32)`.
12
+ *
13
+ * That `.slice(-32)` was a collision bug, not a stylistic choice:
14
+ * `buildId` is always exactly 32 alphanumeric characters (a
15
+ * dash-stripped `crypto.randomUUID()`, see `packages/db/src/id.ts`), so
16
+ * for any artifact `fileName` with 32 or more alphanumeric characters
17
+ * (ordinary for a descriptive OTA bundle name, e.g.
18
+ * "react-native-ota-bundle-production.zip"), keeping only the *last* 32
19
+ * characters of the stripped key drops the build-specific `buildId`
20
+ * entirely and keeps only a fixed suffix of `fileName` — making
21
+ * `bundleHash` an app-wide *constant* across every future release for
22
+ * that app. The manifest endpoint's `currentHash ===
23
+ * activeRelease.bundleHash` check (`ota.ts`'s `GET /apps/:appId/ota/
24
+ * :channel/manifest`) then permanently returns `204 No Content`
25
+ * ("already up to date") for that app, silently breaking OTA delivery
26
+ * the moment a device picks up one release with that hash.
27
+ *
28
+ * The fix: don't truncate at all. `bundleHash`'s DB column
29
+ * (`migrations/0012_ota_updates.sql`) is unconstrained `TEXT`, and
30
+ * every consumer either compares it for equality (`ota.ts`'s manifest
31
+ * check, `packages/agent`) or truncates it further purely for display
32
+ * (`AppDetail.tsx`, `packages/cli/src/commands/ota.ts`'s
33
+ * `formatReleaseLine`) — nothing depends on a fixed 32-character
34
+ * length, so keeping the whole stripped key preserves full
35
+ * build-specific entropy with no downstream breakage.
36
+ */
37
+ export declare function deriveOtaBundleHash(storageKey: string): string;
38
+ //# sourceMappingURL=ota-bundle-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-bundle-hash.d.ts","sourceRoot":"","sources":["../src/ota-bundle-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Derives an OTA release's `bundleHash` from the promoted build's R2
3
+ * storage key (`builds/${buildId}/${fileName}`, see
4
+ * `apps/admin-api/src/lib/storage.ts` / `apps/api/src/lib/storage.ts`'s
5
+ * `artifactKey`). Both `apps/api/src/routes/ota.ts` (dashboard "promote
6
+ * build to OTA release" action) and `apps/admin-api/src/routes/
7
+ * agents.ts` (`POST /builds/:buildId/complete`'s auto-promote path,
8
+ * `packages/agent`'s only actual caller since it never sends its own
9
+ * `input.ota.bundleHash` — see `packages/agent/src/lib/builder.ts`)
10
+ * used to inline this as `storageKey.replace(/[^a-zA-Z0-9]/g,
11
+ * "").slice(-32)`.
12
+ *
13
+ * That `.slice(-32)` was a collision bug, not a stylistic choice:
14
+ * `buildId` is always exactly 32 alphanumeric characters (a
15
+ * dash-stripped `crypto.randomUUID()`, see `packages/db/src/id.ts`), so
16
+ * for any artifact `fileName` with 32 or more alphanumeric characters
17
+ * (ordinary for a descriptive OTA bundle name, e.g.
18
+ * "react-native-ota-bundle-production.zip"), keeping only the *last* 32
19
+ * characters of the stripped key drops the build-specific `buildId`
20
+ * entirely and keeps only a fixed suffix of `fileName` — making
21
+ * `bundleHash` an app-wide *constant* across every future release for
22
+ * that app. The manifest endpoint's `currentHash ===
23
+ * activeRelease.bundleHash` check (`ota.ts`'s `GET /apps/:appId/ota/
24
+ * :channel/manifest`) then permanently returns `204 No Content`
25
+ * ("already up to date") for that app, silently breaking OTA delivery
26
+ * the moment a device picks up one release with that hash.
27
+ *
28
+ * The fix: don't truncate at all. `bundleHash`'s DB column
29
+ * (`migrations/0012_ota_updates.sql`) is unconstrained `TEXT`, and
30
+ * every consumer either compares it for equality (`ota.ts`'s manifest
31
+ * check, `packages/agent`) or truncates it further purely for display
32
+ * (`AppDetail.tsx`, `packages/cli/src/commands/ota.ts`'s
33
+ * `formatReleaseLine`) — nothing depends on a fixed 32-character
34
+ * length, so keeping the whole stripped key preserves full
35
+ * build-specific entropy with no downstream breakage.
36
+ */
37
+ export function deriveOtaBundleHash(storageKey) {
38
+ return storageKey.replace(/[^a-zA-Z0-9]/g, "");
39
+ }
40
+ //# sourceMappingURL=ota-bundle-hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-bundle-hash.js","sourceRoot":"","sources":["../src/ota-bundle-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAkB;IACpD,OAAO,UAAU,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;AACjD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ota-bundle-hash.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-bundle-hash.test.d.ts","sourceRoot":"","sources":["../src/ota-bundle-hash.test.ts"],"names":[],"mappings":""}