@hot-updater/protocol 0.0.0-stage → 1.0.0-rc.20
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 +33 -0
- package/dist/index.cjs +463 -0
- package/dist/index.d.cts +451 -0
- package/dist/index.d.mts +451 -0
- package/dist/index.mjs +418 -0
- package/package.json +48 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Sungyu Kang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
ADDITIONAL DISCLAIMER
|
|
26
|
+
|
|
27
|
+
The authors of this software shall not be held responsible for any legal issues,
|
|
28
|
+
regulatory violations, policy violations, or disputes arising from the use,
|
|
29
|
+
distribution, or integration of this software.
|
|
30
|
+
|
|
31
|
+
Users are solely responsible for ensuring compliance with all applicable laws,
|
|
32
|
+
regulations, and third-party policies (including but not limited to app store
|
|
33
|
+
policies).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/bundleArtifacts.ts
|
|
3
|
+
const stripBundleArtifactMetadata = (metadata) => metadata;
|
|
4
|
+
const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
|
|
5
|
+
const getManifestFileHash = (bundle) => bundle.manifestFileHash;
|
|
6
|
+
const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
|
|
7
|
+
const isBundlePatchArtifact = (value) => {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
9
|
+
const candidate = value;
|
|
10
|
+
return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
|
|
11
|
+
};
|
|
12
|
+
const readBundlePatchArray = (patches) => {
|
|
13
|
+
if (!Array.isArray(patches)) return [];
|
|
14
|
+
return patches.filter(isBundlePatchArtifact);
|
|
15
|
+
};
|
|
16
|
+
const getBundlePatches = (bundle) => {
|
|
17
|
+
const patches = readBundlePatchArray(bundle.patches);
|
|
18
|
+
const seenBaseBundleIds = /* @__PURE__ */ new Set();
|
|
19
|
+
return patches.filter((patch) => {
|
|
20
|
+
if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
|
|
21
|
+
seenBaseBundleIds.add(patch.baseBundleId);
|
|
22
|
+
return true;
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const getBundlePatch = (bundle, baseBundleId) => {
|
|
26
|
+
return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
|
|
27
|
+
};
|
|
28
|
+
const getPrimaryPatch = (bundle) => {
|
|
29
|
+
return getBundlePatches(bundle)[0] ?? null;
|
|
30
|
+
};
|
|
31
|
+
const getPatchBaseBundleId = (bundle) => getPrimaryPatch(bundle)?.baseBundleId ?? null;
|
|
32
|
+
const getPatchBaseFileHash = (bundle) => getPrimaryPatch(bundle)?.baseFileHash ?? null;
|
|
33
|
+
const getPatchFileHash = (bundle) => getPrimaryPatch(bundle)?.patchFileHash ?? null;
|
|
34
|
+
const getPatchStorageUri = (bundle) => getPrimaryPatch(bundle)?.patchStorageUri ?? null;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/clientPlugin.ts
|
|
37
|
+
/** Declares a client plugin; built-in and third-party plugins use the same contract. */
|
|
38
|
+
const defineClientPlugin = (plugin) => plugin;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/rollout.ts
|
|
41
|
+
const NUMERIC_COHORT_SIZE = 1e3;
|
|
42
|
+
const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
|
|
43
|
+
const MAX_COHORT_LENGTH = 64;
|
|
44
|
+
const INVALID_COHORT_ERROR_MESSAGE = `Invalid cohort. Use 1-1000 or a lowercase slug without spaces, up to 64 characters.`;
|
|
45
|
+
const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
|
|
46
|
+
function parseNumericCohortValue(cohort) {
|
|
47
|
+
if (!/^\d+$/.test(cohort)) return null;
|
|
48
|
+
const parsed = Number.parseInt(cohort, 10);
|
|
49
|
+
if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
function positiveMod(value, modulus) {
|
|
53
|
+
return (value % modulus + modulus) % modulus;
|
|
54
|
+
}
|
|
55
|
+
function hashString(value) {
|
|
56
|
+
let hash = 0;
|
|
57
|
+
for (let i = 0; i < value.length; i++) {
|
|
58
|
+
const char = value.charCodeAt(i);
|
|
59
|
+
hash = (hash << 5) - hash + char;
|
|
60
|
+
hash |= 0;
|
|
61
|
+
}
|
|
62
|
+
return hash;
|
|
63
|
+
}
|
|
64
|
+
function gcd(a, b) {
|
|
65
|
+
let x = Math.abs(a);
|
|
66
|
+
let y = Math.abs(b);
|
|
67
|
+
while (y !== 0) {
|
|
68
|
+
const next = x % y;
|
|
69
|
+
x = y;
|
|
70
|
+
y = next;
|
|
71
|
+
}
|
|
72
|
+
return x;
|
|
73
|
+
}
|
|
74
|
+
function modularInverse(value, modulus) {
|
|
75
|
+
let t = 0;
|
|
76
|
+
let newT = 1;
|
|
77
|
+
let r = modulus;
|
|
78
|
+
let newR = positiveMod(value, modulus);
|
|
79
|
+
while (newR !== 0) {
|
|
80
|
+
const quotient = Math.floor(r / newR);
|
|
81
|
+
[t, newT] = [newT, t - quotient * newT];
|
|
82
|
+
[r, newR] = [newR, r - quotient * newR];
|
|
83
|
+
}
|
|
84
|
+
if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
|
|
85
|
+
return positiveMod(t, modulus);
|
|
86
|
+
}
|
|
87
|
+
function getRolloutShuffleParameters(bundleId) {
|
|
88
|
+
let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
|
|
89
|
+
if (multiplier === 0) multiplier = 1;
|
|
90
|
+
while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
|
|
91
|
+
multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
|
|
92
|
+
if (multiplier === 0) multiplier = 1;
|
|
93
|
+
}
|
|
94
|
+
const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
|
|
95
|
+
return {
|
|
96
|
+
multiplier,
|
|
97
|
+
offset,
|
|
98
|
+
inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function normalizeRolloutCohortCount(rolloutCohortCount) {
|
|
102
|
+
if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
|
|
103
|
+
if (rolloutCohortCount <= 0) return 0;
|
|
104
|
+
if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
|
|
105
|
+
return Math.floor(rolloutCohortCount);
|
|
106
|
+
}
|
|
107
|
+
function normalizeCohortValue(cohort) {
|
|
108
|
+
const normalized = cohort.trim().toLowerCase();
|
|
109
|
+
const numericCohort = parseNumericCohortValue(normalized);
|
|
110
|
+
if (numericCohort !== null) return String(numericCohort);
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
function getNumericCohortValue(cohort) {
|
|
114
|
+
return parseNumericCohortValue(normalizeCohortValue(cohort));
|
|
115
|
+
}
|
|
116
|
+
function isNumericCohort(cohort) {
|
|
117
|
+
return getNumericCohortValue(cohort) !== null;
|
|
118
|
+
}
|
|
119
|
+
function isCustomCohort(cohort) {
|
|
120
|
+
const normalized = normalizeCohortValue(cohort);
|
|
121
|
+
return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
|
|
122
|
+
}
|
|
123
|
+
function isValidCohort(cohort) {
|
|
124
|
+
const normalized = normalizeCohortValue(cohort);
|
|
125
|
+
return isNumericCohort(normalized) || isCustomCohort(normalized);
|
|
126
|
+
}
|
|
127
|
+
function getDefaultNumericCohort(identifier) {
|
|
128
|
+
const cohortValue = positiveMod(hashString(identifier), NUMERIC_COHORT_SIZE) + 1;
|
|
129
|
+
return String(cohortValue);
|
|
130
|
+
}
|
|
131
|
+
function getNumericCohortRolloutPosition(bundleId, cohortValue) {
|
|
132
|
+
if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
|
|
133
|
+
const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
|
|
134
|
+
return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
|
|
135
|
+
}
|
|
136
|
+
function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
|
|
137
|
+
const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
|
|
138
|
+
if (normalizedRolloutCount <= 0) return [];
|
|
139
|
+
if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
|
|
140
|
+
const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
|
|
141
|
+
return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
|
|
142
|
+
}
|
|
143
|
+
function isCohortEligibleForUpdate(bundleId, cohort, rolloutCohortCount, targetCohorts) {
|
|
144
|
+
const normalizedCohort = cohort === null || cohort === void 0 ? void 0 : normalizeCohortValue(cohort);
|
|
145
|
+
const normalizedTargetCohorts = targetCohorts?.map((targetCohort) => normalizeCohortValue(targetCohort)) ?? [];
|
|
146
|
+
if (normalizedCohort !== void 0 && normalizedTargetCohorts.includes(normalizedCohort)) return true;
|
|
147
|
+
const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
|
|
148
|
+
if (normalizedRolloutCount <= 0) return false;
|
|
149
|
+
if (normalizedCohort === void 0) return normalizedRolloutCount >= NUMERIC_COHORT_SIZE;
|
|
150
|
+
const numericCohort = getNumericCohortValue(normalizedCohort);
|
|
151
|
+
if (numericCohort === null) return false;
|
|
152
|
+
if (normalizedRolloutCount >= 1e3) return true;
|
|
153
|
+
return getNumericCohortRolloutPosition(bundleId, numericCohort) < normalizedRolloutCount;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/releaseCatalog.ts
|
|
157
|
+
const RELEASE_CATALOG_SCHEMA_VERSION = 1;
|
|
158
|
+
const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
|
|
159
|
+
const MAX_CRASHED_BUNDLES = 10;
|
|
160
|
+
const MAX_TARGET_COHORTS_PER_RELEASE = 100;
|
|
161
|
+
const MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE = 512;
|
|
162
|
+
const MAX_COMPILED_CATALOG_BYTES = 262144;
|
|
163
|
+
const hashSelectionContext = (value) => {
|
|
164
|
+
let first = 2166136261;
|
|
165
|
+
let second = 2654435769;
|
|
166
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
167
|
+
const code = value.charCodeAt(index);
|
|
168
|
+
first = Math.imul(first ^ code, 16777619);
|
|
169
|
+
second = Math.imul(second ^ code, 2246822507);
|
|
170
|
+
}
|
|
171
|
+
return `v1:${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0).toString(16).padStart(8, "0")}`;
|
|
172
|
+
};
|
|
173
|
+
function createReleaseSelectionContextHash(input) {
|
|
174
|
+
const crashedBundleIds = [...new Set(input.crashedBundleIds)].sort().slice(0, 10);
|
|
175
|
+
const canonical = JSON.stringify({
|
|
176
|
+
activeBundleId: input.activeBundleId ?? null,
|
|
177
|
+
activeReleaseId: input.activeReleaseId ?? null,
|
|
178
|
+
cohort: normalizeCohortValue(input.cohort ?? ""),
|
|
179
|
+
crashedBundleIds,
|
|
180
|
+
minimumReleaseId: input.minimumReleaseId,
|
|
181
|
+
selectorSchemaVersion: 1,
|
|
182
|
+
strategy: input.strategy,
|
|
183
|
+
strategyValue: input.strategyValue
|
|
184
|
+
});
|
|
185
|
+
return hashSelectionContext(canonical);
|
|
186
|
+
}
|
|
187
|
+
function isReleaseEligibleForCohort(release, cohort) {
|
|
188
|
+
if (isCohortEligibleForUpdate(release.releaseId, cohort, release.rolloutCohortCount, release.targetCohorts)) return true;
|
|
189
|
+
return cohort !== null && cohort !== void 0 && isCustomCohort(normalizeCohortValue(cohort)) && normalizeRolloutCohortCount(release.rolloutCohortCount) === 1e3;
|
|
190
|
+
}
|
|
191
|
+
function selectDesiredRelease(catalog, input) {
|
|
192
|
+
const crashedBundleIds = new Set(input.crashedBundleIds);
|
|
193
|
+
const activeReleaseId = input.activeReleaseId ?? null;
|
|
194
|
+
const hasActiveBundle = input.currentBundleId !== input.builtInBundleId;
|
|
195
|
+
const isSafeBundleRelease = (release) => release.kind === "BUNDLE" && release.bundleId !== null && !crashedBundleIds.has(release.bundleId) && release.bundleId >= input.minimumReleaseId;
|
|
196
|
+
const desiredBundle = (release, status) => ({
|
|
197
|
+
bundleId: release.bundleId,
|
|
198
|
+
kind: "BUNDLE",
|
|
199
|
+
release,
|
|
200
|
+
releaseId: release.releaseId,
|
|
201
|
+
status
|
|
202
|
+
});
|
|
203
|
+
for (const release of catalog.releases) {
|
|
204
|
+
if (!isReleaseEligibleForCohort(release, input.cohort)) continue;
|
|
205
|
+
if (activeReleaseId !== null && release.releaseId <= activeReleaseId || activeReleaseId === null && hasActiveBundle && (release.bundleId === null || release.bundleId <= input.currentBundleId)) continue;
|
|
206
|
+
if (isSafeBundleRelease(release)) return desiredBundle(release, "UPDATE");
|
|
207
|
+
if (release.bundleId !== null) continue;
|
|
208
|
+
return {
|
|
209
|
+
bundleId: input.builtInBundleId,
|
|
210
|
+
kind: "EMBEDDED",
|
|
211
|
+
release,
|
|
212
|
+
releaseId: release.releaseId,
|
|
213
|
+
status: "ROLLBACK"
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (activeReleaseId !== null || hasActiveBundle) {
|
|
217
|
+
const rollbackReleases = catalog.rollbackReleases ?? catalog.releases;
|
|
218
|
+
const current = rollbackReleases.find((release) => release.releaseId === activeReleaseId || activeReleaseId === null && release.bundleId === input.currentBundleId);
|
|
219
|
+
if (current !== void 0 && isSafeBundleRelease(current) && isReleaseEligibleForCohort(current, input.cohort)) return desiredBundle(current, "UPDATE");
|
|
220
|
+
const rollback = rollbackReleases.find((release) => isSafeBundleRelease(release) && (activeReleaseId !== null ? release.releaseId < activeReleaseId : release.bundleId < input.currentBundleId));
|
|
221
|
+
if (rollback !== void 0 && isSafeBundleRelease(rollback)) return desiredBundle(rollback, "ROLLBACK");
|
|
222
|
+
if (input.currentBundleId <= input.minimumReleaseId) return null;
|
|
223
|
+
}
|
|
224
|
+
if (catalog.fallbackPolicy !== "BUILTIN_IF_ACTIVE_INELIGIBLE") return null;
|
|
225
|
+
return {
|
|
226
|
+
bundleId: input.builtInBundleId,
|
|
227
|
+
kind: "BUILTIN",
|
|
228
|
+
release: null,
|
|
229
|
+
releaseId: null,
|
|
230
|
+
status: "ROLLBACK"
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function assessCatalogAcceptance(incoming, highestSeen) {
|
|
234
|
+
if (highestSeen === null) return {
|
|
235
|
+
accepted: true,
|
|
236
|
+
shouldAdvanceHighWater: true
|
|
237
|
+
};
|
|
238
|
+
if (incoming.generation < highestSeen.generation) return {
|
|
239
|
+
accepted: false,
|
|
240
|
+
reason: "STALE_GENERATION"
|
|
241
|
+
};
|
|
242
|
+
if (incoming.generation === highestSeen.generation && incoming.catalogHash !== highestSeen.catalogHash) return {
|
|
243
|
+
accepted: false,
|
|
244
|
+
reason: "GENERATION_HASH_MISMATCH"
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
accepted: true,
|
|
248
|
+
shouldAdvanceHighWater: incoming.generation > highestSeen.generation
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function authorizeReleaseTransition(input) {
|
|
252
|
+
const { active, desired, explicitScopeSwitch } = input;
|
|
253
|
+
if (active === null || active.scopeKey === null || active.generation === null) return {
|
|
254
|
+
authorized: true,
|
|
255
|
+
reason: "FIRST_AUTHENTICATED_SELECTION"
|
|
256
|
+
};
|
|
257
|
+
if (desired.catalogId !== active.catalogId || desired.scopeKey !== active.scopeKey) {
|
|
258
|
+
if (!explicitScopeSwitch) return {
|
|
259
|
+
authorized: false,
|
|
260
|
+
reason: "UNSOLICITED_SCOPE"
|
|
261
|
+
};
|
|
262
|
+
if (desired.releaseId === null || desired.kind === "BUILTIN") return {
|
|
263
|
+
authorized: false,
|
|
264
|
+
reason: "EMPTY_TARGET_SCOPE"
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
authorized: true,
|
|
268
|
+
reason: "EXPLICIT_SCOPE_SWITCH"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (desired.generation === null || desired.generation < active.generation) return {
|
|
272
|
+
authorized: false,
|
|
273
|
+
reason: "STALE_GENERATION"
|
|
274
|
+
};
|
|
275
|
+
if (desired.generation > active.generation) return {
|
|
276
|
+
authorized: true,
|
|
277
|
+
reason: "NEWER_POLICY"
|
|
278
|
+
};
|
|
279
|
+
if (desired.selectionContextHash !== active.selectionContextHash) return {
|
|
280
|
+
authorized: true,
|
|
281
|
+
reason: "CONTEXT_RESELECTION"
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
authorized: false,
|
|
285
|
+
reason: "BACKWARD_NOT_AUTHORIZED"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/releaseCatalogScope.ts
|
|
290
|
+
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
291
|
+
const MAX_CATALOG_SEGMENT_LENGTH = 255;
|
|
292
|
+
function assertCatalogSegment(value, name) {
|
|
293
|
+
if (value.length === 0 || value.length > MAX_CATALOG_SEGMENT_LENGTH || !/^[A-Za-z0-9._~-]+$/.test(value)) throw new Error(`${name} must be 1-${MAX_CATALOG_SEGMENT_LENGTH} URL-safe ASCII characters`);
|
|
294
|
+
}
|
|
295
|
+
function encodeUtf8(value) {
|
|
296
|
+
const bytes = [];
|
|
297
|
+
for (const codePointText of value) {
|
|
298
|
+
const codePoint = codePointText.codePointAt(0);
|
|
299
|
+
if (codePoint <= 127) bytes.push(codePoint);
|
|
300
|
+
else if (codePoint <= 2047) bytes.push(192 | codePoint >> 6, 128 | codePoint & 63);
|
|
301
|
+
else if (codePoint <= 65535) bytes.push(224 | codePoint >> 12, 128 | codePoint >> 6 & 63, 128 | codePoint & 63);
|
|
302
|
+
else bytes.push(240 | codePoint >> 18, 128 | codePoint >> 12 & 63, 128 | codePoint >> 6 & 63, 128 | codePoint & 63);
|
|
303
|
+
}
|
|
304
|
+
return bytes;
|
|
305
|
+
}
|
|
306
|
+
function decodeUtf8(bytes) {
|
|
307
|
+
let result = "";
|
|
308
|
+
for (let index = 0; index < bytes.length;) {
|
|
309
|
+
const first = bytes[index++];
|
|
310
|
+
let codePoint;
|
|
311
|
+
let continuationCount;
|
|
312
|
+
if (first <= 127) {
|
|
313
|
+
codePoint = first;
|
|
314
|
+
continuationCount = 0;
|
|
315
|
+
} else if ((first & 224) === 192) {
|
|
316
|
+
codePoint = first & 31;
|
|
317
|
+
continuationCount = 1;
|
|
318
|
+
} else if ((first & 240) === 224) {
|
|
319
|
+
codePoint = first & 15;
|
|
320
|
+
continuationCount = 2;
|
|
321
|
+
} else if ((first & 248) === 240) {
|
|
322
|
+
codePoint = first & 7;
|
|
323
|
+
continuationCount = 3;
|
|
324
|
+
} else throw new Error("Invalid UTF-8 channel key");
|
|
325
|
+
for (let offset = 0; offset < continuationCount; offset += 1) {
|
|
326
|
+
const continuation = bytes[index++];
|
|
327
|
+
if (continuation === void 0 || (continuation & 192) !== 128) throw new Error("Invalid UTF-8 channel key");
|
|
328
|
+
codePoint = codePoint << 6 | continuation & 63;
|
|
329
|
+
}
|
|
330
|
+
if (codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343 || continuationCount === 1 && codePoint < 128 || continuationCount === 2 && codePoint < 2048 || continuationCount === 3 && codePoint < 65536) throw new Error("Invalid UTF-8 channel key");
|
|
331
|
+
result += String.fromCodePoint(codePoint);
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
function base64UrlEncode(bytes) {
|
|
336
|
+
let result = "";
|
|
337
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
338
|
+
const first = bytes[index];
|
|
339
|
+
const second = bytes[index + 1];
|
|
340
|
+
const third = bytes[index + 2];
|
|
341
|
+
const value = first << 16 | (second ?? 0) << 8 | (third ?? 0);
|
|
342
|
+
result += BASE64URL_ALPHABET[value >> 18 & 63];
|
|
343
|
+
result += BASE64URL_ALPHABET[value >> 12 & 63];
|
|
344
|
+
if (second !== void 0) result += BASE64URL_ALPHABET[value >> 6 & 63];
|
|
345
|
+
if (third !== void 0) result += BASE64URL_ALPHABET[value & 63];
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
function base64UrlDecode(value) {
|
|
350
|
+
if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) throw new Error("Invalid channel key");
|
|
351
|
+
const bytes = [];
|
|
352
|
+
for (let index = 0; index < value.length; index += 4) {
|
|
353
|
+
const chunk = value.slice(index, index + 4);
|
|
354
|
+
const encoded = [...chunk].map((character) => BASE64URL_ALPHABET.indexOf(character));
|
|
355
|
+
if (encoded.some((part) => part < 0)) throw new Error("Invalid channel key");
|
|
356
|
+
const bits = encoded[0] << 18 | encoded[1] << 12 | (encoded[2] ?? 0) << 6 | (encoded[3] ?? 0);
|
|
357
|
+
bytes.push(bits >> 16 & 255);
|
|
358
|
+
if (chunk.length >= 3) bytes.push(bits >> 8 & 255);
|
|
359
|
+
if (chunk.length === 4) bytes.push(bits & 255);
|
|
360
|
+
}
|
|
361
|
+
return bytes;
|
|
362
|
+
}
|
|
363
|
+
function normalizeChannelName(channel) {
|
|
364
|
+
return channel.trim().normalize("NFC");
|
|
365
|
+
}
|
|
366
|
+
function assertCanonicalChannelName(channel) {
|
|
367
|
+
const normalized = normalizeChannelName(channel);
|
|
368
|
+
if (normalized.length === 0 || normalized !== channel) throw new Error("Channel name must be non-empty, trimmed, and NFC-normalized");
|
|
369
|
+
let codePointCount = 0;
|
|
370
|
+
for (const _codePoint of normalized) {
|
|
371
|
+
codePointCount += 1;
|
|
372
|
+
if (codePointCount > 255) throw new Error("Channel name must not exceed 255 characters");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function encodeChannelKey(channel) {
|
|
376
|
+
assertCanonicalChannelName(channel);
|
|
377
|
+
return base64UrlEncode(encodeUtf8(channel));
|
|
378
|
+
}
|
|
379
|
+
function decodeChannelKey(channelKey) {
|
|
380
|
+
const channel = decodeUtf8(base64UrlDecode(channelKey));
|
|
381
|
+
assertCanonicalChannelName(channel);
|
|
382
|
+
if (encodeChannelKey(channel) !== channelKey) throw new Error("Channel key is not canonically encoded");
|
|
383
|
+
return channel;
|
|
384
|
+
}
|
|
385
|
+
function createReleaseCatalogScopeKey(input) {
|
|
386
|
+
decodeChannelKey(input.channelKey);
|
|
387
|
+
if (input.strategy === "APP_VERSION") return `v1:app-version:${input.platform}:${input.channelKey}`;
|
|
388
|
+
assertCatalogSegment(input.fingerprintHash, "Fingerprint hash");
|
|
389
|
+
return `v1:fingerprint:${input.platform}:${input.channelKey}:${input.fingerprintHash}`;
|
|
390
|
+
}
|
|
391
|
+
function parseReleaseCatalogScopeKey(scopeKey) {
|
|
392
|
+
const segments = scopeKey.split(":");
|
|
393
|
+
const [version, strategy, platform, channelKey] = segments;
|
|
394
|
+
if (version !== "v1" || platform !== "ios" && platform !== "android" || channelKey === void 0) throw new Error("Invalid release catalog scope key");
|
|
395
|
+
const input = strategy === "app-version" && segments.length === 4 ? {
|
|
396
|
+
channelKey,
|
|
397
|
+
platform,
|
|
398
|
+
strategy: "APP_VERSION"
|
|
399
|
+
} : strategy === "fingerprint" && segments.length === 5 && segments[4] !== void 0 ? {
|
|
400
|
+
channelKey,
|
|
401
|
+
fingerprintHash: segments[4],
|
|
402
|
+
platform,
|
|
403
|
+
strategy: "FINGERPRINT"
|
|
404
|
+
} : (() => {
|
|
405
|
+
throw new Error("Invalid release catalog scope key");
|
|
406
|
+
})();
|
|
407
|
+
if (createReleaseCatalogScopeKey(input) !== scopeKey) throw new Error("Release catalog scope key is not canonical");
|
|
408
|
+
return input;
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/types.ts
|
|
412
|
+
const ARTIFACT_PROTOCOL_VERSION = 1;
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/uuid.ts
|
|
415
|
+
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
416
|
+
const UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
417
|
+
const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(value);
|
|
418
|
+
//#endregion
|
|
419
|
+
exports.ARTIFACT_PROTOCOL_VERSION = ARTIFACT_PROTOCOL_VERSION;
|
|
420
|
+
exports.DEFAULT_ROLLOUT_COHORT_COUNT = DEFAULT_ROLLOUT_COHORT_COUNT;
|
|
421
|
+
exports.INVALID_COHORT_ERROR_MESSAGE = INVALID_COHORT_ERROR_MESSAGE;
|
|
422
|
+
exports.MAX_COHORT_LENGTH = MAX_COHORT_LENGTH;
|
|
423
|
+
exports.MAX_COMPILED_CATALOG_BYTES = MAX_COMPILED_CATALOG_BYTES;
|
|
424
|
+
exports.MAX_CRASHED_BUNDLES = MAX_CRASHED_BUNDLES;
|
|
425
|
+
exports.MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE = MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE;
|
|
426
|
+
exports.MAX_TARGET_COHORTS_PER_RELEASE = MAX_TARGET_COHORTS_PER_RELEASE;
|
|
427
|
+
exports.NIL_UUID = NIL_UUID;
|
|
428
|
+
exports.NUMERIC_COHORT_SIZE = NUMERIC_COHORT_SIZE;
|
|
429
|
+
exports.RELEASE_CATALOG_FALLBACK_POLICY = RELEASE_CATALOG_FALLBACK_POLICY;
|
|
430
|
+
exports.RELEASE_CATALOG_SCHEMA_VERSION = RELEASE_CATALOG_SCHEMA_VERSION;
|
|
431
|
+
exports.assertCanonicalChannelName = assertCanonicalChannelName;
|
|
432
|
+
exports.assessCatalogAcceptance = assessCatalogAcceptance;
|
|
433
|
+
exports.authorizeReleaseTransition = authorizeReleaseTransition;
|
|
434
|
+
exports.createReleaseCatalogScopeKey = createReleaseCatalogScopeKey;
|
|
435
|
+
exports.createReleaseSelectionContextHash = createReleaseSelectionContextHash;
|
|
436
|
+
exports.decodeChannelKey = decodeChannelKey;
|
|
437
|
+
exports.defineClientPlugin = defineClientPlugin;
|
|
438
|
+
exports.encodeChannelKey = encodeChannelKey;
|
|
439
|
+
exports.getAssetBaseStorageUri = getAssetBaseStorageUri;
|
|
440
|
+
exports.getBundlePatch = getBundlePatch;
|
|
441
|
+
exports.getBundlePatches = getBundlePatches;
|
|
442
|
+
exports.getDefaultNumericCohort = getDefaultNumericCohort;
|
|
443
|
+
exports.getManifestFileHash = getManifestFileHash;
|
|
444
|
+
exports.getManifestStorageUri = getManifestStorageUri;
|
|
445
|
+
exports.getNumericCohortRolloutPosition = getNumericCohortRolloutPosition;
|
|
446
|
+
exports.getNumericCohortValue = getNumericCohortValue;
|
|
447
|
+
exports.getPatchBaseBundleId = getPatchBaseBundleId;
|
|
448
|
+
exports.getPatchBaseFileHash = getPatchBaseFileHash;
|
|
449
|
+
exports.getPatchFileHash = getPatchFileHash;
|
|
450
|
+
exports.getPatchStorageUri = getPatchStorageUri;
|
|
451
|
+
exports.getRolledOutNumericCohorts = getRolledOutNumericCohorts;
|
|
452
|
+
exports.isCohortEligibleForUpdate = isCohortEligibleForUpdate;
|
|
453
|
+
exports.isCustomCohort = isCustomCohort;
|
|
454
|
+
exports.isNumericCohort = isNumericCohort;
|
|
455
|
+
exports.isReleaseEligibleForCohort = isReleaseEligibleForCohort;
|
|
456
|
+
exports.isUUIDv7 = isUUIDv7;
|
|
457
|
+
exports.isValidCohort = isValidCohort;
|
|
458
|
+
exports.normalizeChannelName = normalizeChannelName;
|
|
459
|
+
exports.normalizeCohortValue = normalizeCohortValue;
|
|
460
|
+
exports.normalizeRolloutCohortCount = normalizeRolloutCohortCount;
|
|
461
|
+
exports.parseReleaseCatalogScopeKey = parseReleaseCatalogScopeKey;
|
|
462
|
+
exports.selectDesiredRelease = selectDesiredRelease;
|
|
463
|
+
exports.stripBundleArtifactMetadata = stripBundleArtifactMetadata;
|