@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,28 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { deriveOtaBundleHash } from "./ota-bundle-hash.js";
4
+ test("deriveOtaBundleHash strips non-alphanumeric characters", () => {
5
+ assert.equal(deriveOtaBundleHash("builds/abc123/app.zip"), "buildsabc123appzip");
6
+ });
7
+ test("deriveOtaBundleHash regression: two different buildIds with the same long fileName no longer collide", () => {
8
+ // Both buildIds are 32 alphanumeric chars, matching crypto.randomUUID()
9
+ // with dashes stripped (packages/db/src/id.ts). The fileName alone is
10
+ // 34 alphanumeric characters — long enough that the old
11
+ // `.replace(...).slice(-32)` derivation dropped the buildId entirely
12
+ // and returned an identical hash for both builds.
13
+ const fileName = "react-native-ota-bundle-production.zip";
14
+ const buildIdA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
15
+ const buildIdB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
16
+ const hashA = deriveOtaBundleHash(`builds/${buildIdA}/${fileName}`);
17
+ const hashB = deriveOtaBundleHash(`builds/${buildIdB}/${fileName}`);
18
+ assert.notEqual(hashA, hashB);
19
+ // The old, buggy derivation for comparison — proves this is the exact
20
+ // collision being fixed, not a hypothetical.
21
+ const oldDerive = (storageKey) => storageKey.replace(/[^a-zA-Z0-9]/g, "").slice(-32);
22
+ assert.equal(oldDerive(`builds/${buildIdA}/${fileName}`), oldDerive(`builds/${buildIdB}/${fileName}`));
23
+ });
24
+ test("deriveOtaBundleHash is deterministic for the same storageKey", () => {
25
+ const storageKey = "builds/deadbeefdeadbeefdeadbeefdeadbeef/app-release.aab";
26
+ assert.equal(deriveOtaBundleHash(storageKey), deriveOtaBundleHash(storageKey));
27
+ });
28
+ //# sourceMappingURL=ota-bundle-hash.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-bundle-hash.test.js","sourceRoot":"","sources":["../src/ota-bundle-hash.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,IAAI,CAAC,wDAAwD,EAAE,GAAG,EAAE;IAClE,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,uBAAuB,CAAC,EAAE,oBAAoB,CAAC,CAAC;AACnF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sGAAsG,EAAE,GAAG,EAAE;IAChH,wEAAwE;IACxE,sEAAsE;IACtE,wDAAwD;IACxD,qEAAqE;IACrE,kDAAkD;IAClD,MAAM,QAAQ,GAAG,wCAAwC,CAAC;IAC1D,MAAM,QAAQ,GAAG,kCAAkC,CAAC;IACpD,MAAM,QAAQ,GAAG,kCAAkC,CAAC;IAEpD,MAAM,KAAK,GAAG,mBAAmB,CAAC,UAAU,QAAQ,IAAI,QAAQ,EAAE,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,mBAAmB,CAAC,UAAU,QAAQ,IAAI,QAAQ,EAAE,CAAC,CAAC;IAEpE,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAE9B,sEAAsE;IACtE,6CAA6C;IAC7C,MAAM,SAAS,GAAG,CAAC,UAAkB,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7F,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,UAAU,QAAQ,IAAI,QAAQ,EAAE,CAAC,EAAE,SAAS,CAAC,UAAU,QAAQ,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC;AACzG,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8DAA8D,EAAE,GAAG,EAAE;IACxE,MAAM,UAAU,GAAG,yDAAyD,CAAC;IAC7E,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAC,CAAC;AACjF,CAAC,CAAC,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Deterministic, non-cryptographic staged-rollout gate for OTA manifest
3
+ * serving (`apps/api/src/routes/ota.ts`'s `GET /apps/:appId/ota/:channel/
4
+ * manifest`). `OtaRelease.rollout` (0-100) has existed since
5
+ * `migrations/0012_ota_updates.sql`, but the manifest endpoint has always
6
+ * served the active release to every caller regardless of it — this is
7
+ * that missing enforcement, slice (6) of #86's rough plan (see
8
+ * `ROADMAP.md`).
9
+ *
10
+ * A plain, fast, synchronous hash (FNV-1a) rather than WebCrypto's
11
+ * `crypto.subtle.digest` (async, and this codebase already reserves
12
+ * WebCrypto for actual security boundaries — see `apps/api/src/lib/
13
+ * auth.ts`): staged rollout only staggers *exposure* to a release, it
14
+ * isn't a security control, and a client could trivially compute its own
15
+ * bucket either way, so there's nothing here worth paying an async
16
+ * round-trip for.
17
+ */
18
+ /**
19
+ * Whether a given installation falls inside a release's rollout
20
+ * percentage. Hashes `releaseId` together with `installationId` (not
21
+ * `installationId` alone) so one installation's bucket is independent
22
+ * from one release to the next — an installation excluded from release
23
+ * A's 10% rollout isn't guaranteed excluded from release B's, and vice
24
+ * versa — while staying *stable* across repeat polls of the *same*
25
+ * release, so a device doesn't flip in and out of an in-progress
26
+ * rollout between manifest checks.
27
+ *
28
+ * `rolloutPercent` is clamped at the boundaries rather than hashed
29
+ * through: 100 (the default for every release created before this
30
+ * existed, and the common case) always returns true and 0 always
31
+ * returns false, with no dependence on the hash at all — a release
32
+ * meant to reach everyone, or no one, isn't left to chance at the
33
+ * boundary the way `bucket < rolloutPercent` alone could be (bucket 0
34
+ * would otherwise need `rolloutPercent` to be *strictly* greater than
35
+ * zero to ever be included).
36
+ */
37
+ export declare function isInStagedRollout(installationId: string, releaseId: string, rolloutPercent: number): boolean;
38
+ //# sourceMappingURL=ota-rollout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-rollout.d.ts","sourceRoot":"","sources":["../src/ota-rollout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAWH;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAK5G"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Deterministic, non-cryptographic staged-rollout gate for OTA manifest
3
+ * serving (`apps/api/src/routes/ota.ts`'s `GET /apps/:appId/ota/:channel/
4
+ * manifest`). `OtaRelease.rollout` (0-100) has existed since
5
+ * `migrations/0012_ota_updates.sql`, but the manifest endpoint has always
6
+ * served the active release to every caller regardless of it — this is
7
+ * that missing enforcement, slice (6) of #86's rough plan (see
8
+ * `ROADMAP.md`).
9
+ *
10
+ * A plain, fast, synchronous hash (FNV-1a) rather than WebCrypto's
11
+ * `crypto.subtle.digest` (async, and this codebase already reserves
12
+ * WebCrypto for actual security boundaries — see `apps/api/src/lib/
13
+ * auth.ts`): staged rollout only staggers *exposure* to a release, it
14
+ * isn't a security control, and a client could trivially compute its own
15
+ * bucket either way, so there's nothing here worth paying an async
16
+ * round-trip for.
17
+ */
18
+ function fnv1a(input) {
19
+ let hash = 0x811c9dc5;
20
+ for (let i = 0; i < input.length; i++) {
21
+ hash ^= input.charCodeAt(i);
22
+ hash = Math.imul(hash, 0x01000193);
23
+ }
24
+ return hash >>> 0;
25
+ }
26
+ /**
27
+ * Whether a given installation falls inside a release's rollout
28
+ * percentage. Hashes `releaseId` together with `installationId` (not
29
+ * `installationId` alone) so one installation's bucket is independent
30
+ * from one release to the next — an installation excluded from release
31
+ * A's 10% rollout isn't guaranteed excluded from release B's, and vice
32
+ * versa — while staying *stable* across repeat polls of the *same*
33
+ * release, so a device doesn't flip in and out of an in-progress
34
+ * rollout between manifest checks.
35
+ *
36
+ * `rolloutPercent` is clamped at the boundaries rather than hashed
37
+ * through: 100 (the default for every release created before this
38
+ * existed, and the common case) always returns true and 0 always
39
+ * returns false, with no dependence on the hash at all — a release
40
+ * meant to reach everyone, or no one, isn't left to chance at the
41
+ * boundary the way `bucket < rolloutPercent` alone could be (bucket 0
42
+ * would otherwise need `rolloutPercent` to be *strictly* greater than
43
+ * zero to ever be included).
44
+ */
45
+ export function isInStagedRollout(installationId, releaseId, rolloutPercent) {
46
+ if (rolloutPercent >= 100)
47
+ return true;
48
+ if (rolloutPercent <= 0)
49
+ return false;
50
+ const bucket = fnv1a(`${releaseId}:${installationId}`) % 100;
51
+ return bucket < rolloutPercent;
52
+ }
53
+ //# sourceMappingURL=ota-rollout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-rollout.js","sourceRoot":"","sources":["../src/ota-rollout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,SAAS,KAAK,CAAC,KAAa;IAC1B,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,iBAAiB,CAAC,cAAsB,EAAE,SAAiB,EAAE,cAAsB;IACjG,IAAI,cAAc,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,cAAc,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,SAAS,IAAI,cAAc,EAAE,CAAC,GAAG,GAAG,CAAC;IAC7D,OAAO,MAAM,GAAG,cAAc,CAAC;AACjC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ota-rollout.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-rollout.test.d.ts","sourceRoot":"","sources":["../src/ota-rollout.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { isInStagedRollout } from "./ota-rollout.js";
4
+ test("rollout of 100 always returns true, regardless of installation/release", () => {
5
+ for (const installationId of ["a", "b", "c", "device-1234"]) {
6
+ assert.equal(isInStagedRollout(installationId, "rel-1", 100), true);
7
+ }
8
+ });
9
+ test("rollout of 0 always returns false, regardless of installation/release", () => {
10
+ for (const installationId of ["a", "b", "c", "device-1234"]) {
11
+ assert.equal(isInStagedRollout(installationId, "rel-1", 0), false);
12
+ }
13
+ });
14
+ test("is deterministic — the same installation/release/rollout always decides the same way", () => {
15
+ const first = isInStagedRollout("device-abc", "rel-1", 50);
16
+ for (let i = 0; i < 20; i++) {
17
+ assert.equal(isInStagedRollout("device-abc", "rel-1", 50), first);
18
+ }
19
+ });
20
+ test("the same installation can bucket differently across two different releases", () => {
21
+ // Not guaranteed for every installation/rollout pair, but true often
22
+ // enough across a spread of installations that this would fail if
23
+ // releaseId weren't actually mixed into the hash at all (i.e. if
24
+ // bucketing depended on installationId alone).
25
+ let sawDifference = false;
26
+ for (let i = 0; i < 50; i++) {
27
+ const installationId = `device-${i}`;
28
+ if (isInStagedRollout(installationId, "rel-1", 50) !== isInStagedRollout(installationId, "rel-2", 50)) {
29
+ sawDifference = true;
30
+ break;
31
+ }
32
+ }
33
+ assert.ok(sawDifference, "expected at least one installation to bucket differently between two releases");
34
+ });
35
+ test("a mid-range rollout roughly splits a spread of installations both ways, not all-or-nothing", () => {
36
+ let included = 0;
37
+ const total = 500;
38
+ for (let i = 0; i < total; i++) {
39
+ if (isInStagedRollout(`device-${i}`, "rel-1", 30))
40
+ included++;
41
+ }
42
+ // Not a strict statistical claim — just guards against the two ways
43
+ // this could silently degenerate to "always true" or "always false"
44
+ // (e.g. a hash that doesn't vary, or a comparison operator flipped).
45
+ assert.ok(included > 0, "expected at least some installations to be included at 30% rollout");
46
+ assert.ok(included < total, "expected at least some installations to be excluded at 30% rollout");
47
+ });
48
+ //# sourceMappingURL=ota-rollout.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota-rollout.test.js","sourceRoot":"","sources":["../src/ota-rollout.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,IAAI,CAAC,wEAAwE,EAAE,GAAG,EAAE;IAClF,KAAK,MAAM,cAAc,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,CAAC;QAC5D,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;IACtE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,uEAAuE,EAAE,GAAG,EAAE;IACjF,KAAK,MAAM,cAAc,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,CAAC;QAC5D,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,cAAc,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACrE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sFAAsF,EAAE,GAAG,EAAE;IAChG,MAAM,KAAK,GAAG,iBAAiB,CAAC,YAAY,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;IAC3D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,YAAY,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;IACpE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,4EAA4E,EAAE,GAAG,EAAE;IACtF,qEAAqE;IACrE,kEAAkE;IAClE,iEAAiE;IACjE,+CAA+C;IAC/C,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,cAAc,GAAG,UAAU,CAAC,EAAE,CAAC;QACrC,IAAI,iBAAiB,CAAC,cAAc,EAAE,OAAO,EAAE,EAAE,CAAC,KAAK,iBAAiB,CAAC,cAAc,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC;YACtG,aAAa,GAAG,IAAI,CAAC;YACrB,MAAM;QACR,CAAC;IACH,CAAC;IACD,MAAM,CAAC,EAAE,CAAC,aAAa,EAAE,+EAA+E,CAAC,CAAC;AAC5G,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,4FAA4F,EAAE,GAAG,EAAE;IACtG,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,MAAM,KAAK,GAAG,GAAG,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,IAAI,iBAAiB,CAAC,UAAU,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC;YAAE,QAAQ,EAAE,CAAC;IAChE,CAAC;IACD,oEAAoE;IACpE,oEAAoE;IACpE,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,QAAQ,GAAG,CAAC,EAAE,oEAAoE,CAAC,CAAC;IAC9F,MAAM,CAAC,EAAE,CAAC,QAAQ,GAAG,KAAK,EAAE,oEAAoE,CAAC,CAAC;AACpG,CAAC,CAAC,CAAC"}