@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.
- package/LICENSE +21 -0
- package/dist/agent-readiness.d.ts +163 -0
- package/dist/agent-readiness.d.ts.map +1 -0
- package/dist/agent-readiness.js +179 -0
- package/dist/agent-readiness.js.map +1 -0
- package/dist/agent-readiness.test.d.ts +2 -0
- package/dist/agent-readiness.test.d.ts.map +1 -0
- package/dist/agent-readiness.test.js +177 -0
- package/dist/agent-readiness.test.js.map +1 -0
- package/dist/api-client.d.ts +529 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +551 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api-client.test.d.ts +2 -0
- package/dist/api-client.test.d.ts.map +1 -0
- package/dist/api-client.test.js +305 -0
- package/dist/api-client.test.js.map +1 -0
- package/dist/build-cost.test.d.ts +2 -0
- package/dist/build-cost.test.d.ts.map +1 -0
- package/dist/build-cost.test.js +21 -0
- package/dist/build-cost.test.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/ota-artifact.d.ts +41 -0
- package/dist/ota-artifact.d.ts.map +1 -0
- package/dist/ota-artifact.js +43 -0
- package/dist/ota-artifact.js.map +1 -0
- package/dist/ota-artifact.test.d.ts +2 -0
- package/dist/ota-artifact.test.d.ts.map +1 -0
- package/dist/ota-artifact.test.js +44 -0
- package/dist/ota-artifact.test.js.map +1 -0
- package/dist/ota-bundle-hash.d.ts +38 -0
- package/dist/ota-bundle-hash.d.ts.map +1 -0
- package/dist/ota-bundle-hash.js +40 -0
- package/dist/ota-bundle-hash.js.map +1 -0
- package/dist/ota-bundle-hash.test.d.ts +2 -0
- package/dist/ota-bundle-hash.test.d.ts.map +1 -0
- package/dist/ota-bundle-hash.test.js +28 -0
- package/dist/ota-bundle-hash.test.js.map +1 -0
- package/dist/ota-rollout.d.ts +38 -0
- package/dist/ota-rollout.d.ts.map +1 -0
- package/dist/ota-rollout.js +53 -0
- package/dist/ota-rollout.js.map +1 -0
- package/dist/ota-rollout.test.d.ts +2 -0
- package/dist/ota-rollout.test.d.ts.map +1 -0
- package/dist/ota-rollout.test.js +48 -0
- package/dist/ota-rollout.test.js.map +1 -0
- package/dist/schemas.d.ts +776 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +538 -0
- package/dist/schemas.js.map +1 -0
- package/dist/token-packs.test.d.ts +2 -0
- package/dist/token-packs.test.d.ts.map +1 -0
- package/dist/token-packs.test.js +34 -0
- package/dist/token-packs.test.js.map +1 -0
- package/dist/types.d.ts +445 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +80 -0
- package/dist/types.js.map +1 -0
- 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 @@
|
|
|
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"}
|