@absolutejs/absolute 0.20.0-beta.59 → 0.20.0-beta.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/cli/index.js +828 -254
- package/dist/index.js +54 -4
- package/dist/index.js.map +3 -3
- package/dist/mobile/index.js +656 -17
- package/dist/mobile/index.js.map +16 -9
- package/dist/mobile/remoteMacAgentEntry.js +204 -204
- package/dist/mobile/shellBootstrap.js +1 -0
- package/dist/mobile/shellUpdate.js +456 -0
- package/dist/src/mobile/capacitorBundle.d.ts +7 -0
- package/dist/src/mobile/config.d.ts +5 -0
- package/dist/src/mobile/index.d.ts +7 -0
- package/dist/src/mobile/mobileBundleInspection.d.ts +8 -0
- package/dist/src/mobile/mobileInspect.d.ts +6 -0
- package/dist/src/mobile/nativeUpdates.d.ts +4 -0
- package/dist/src/mobile/shellBootstrap.d.ts +1 -0
- package/dist/src/mobile/shellUpdate.d.ts +2 -0
- package/dist/src/mobile/transport.d.ts +6 -0
- package/dist/src/mobile/updateClient.d.ts +40 -0
- package/dist/src/mobile/updateProtocol.d.ts +51 -0
- package/dist/src/mobile/updatePublisher.d.ts +66 -0
- package/dist/src/mobile/updateRollout.d.ts +16 -0
- package/dist/src/mobile/updateRuntime.d.ts +27 -0
- package/dist/src/mobile/updateSigning.d.ts +20 -0
- package/dist/types/build.d.ts +9 -0
- package/package.json +8 -8
package/dist/mobile/index.js
CHANGED
|
@@ -167,7 +167,8 @@ var init_startupBanner = __esm(() => {
|
|
|
167
167
|
|
|
168
168
|
// src/mobile/config.ts
|
|
169
169
|
import { resolve as resolve6 } from "path";
|
|
170
|
-
|
|
170
|
+
import { createPublicKey } from "crypto";
|
|
171
|
+
var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
|
|
171
172
|
const root = resolve6(projectRoot);
|
|
172
173
|
const path = resolve6(root, value);
|
|
173
174
|
if (path !== root && !path.startsWith(`${root}/`)) {
|
|
@@ -240,7 +241,52 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
240
241
|
}
|
|
241
242
|
return value.match(/.{2}/g)?.join(":") ?? value;
|
|
242
243
|
}))
|
|
243
|
-
].sort(),
|
|
244
|
+
].sort(), normalizeUpdates = (config, productionOrigin) => {
|
|
245
|
+
if (!config.updates)
|
|
246
|
+
return;
|
|
247
|
+
const channel = requireText(config.updates.channel ?? "production", "mobile.updates.channel");
|
|
248
|
+
if (!UPDATE_NAME_PATTERN.test(channel))
|
|
249
|
+
throw new TypeError("mobile.updates.channel contains unsupported characters.");
|
|
250
|
+
const manifestUrl = new URL(config.updates.manifestUrl ?? `/__absolute/mobile/updates/${encodeURIComponent(channel)}/update.json`, `${productionOrigin}/`);
|
|
251
|
+
const loopback = manifestUrl.hostname === "localhost" || manifestUrl.hostname === "127.0.0.1" || manifestUrl.hostname === "[::1]";
|
|
252
|
+
if (manifestUrl.protocol !== "https:" && !loopback)
|
|
253
|
+
throw new TypeError("mobile.updates.manifestUrl must use HTTPS outside loopback development.");
|
|
254
|
+
if (manifestUrl.username || manifestUrl.password || manifestUrl.hash)
|
|
255
|
+
throw new TypeError("mobile.updates.manifestUrl cannot contain credentials or a fragment.");
|
|
256
|
+
const entries = Object.entries(config.updates.publicKeys).sort(([left], [right]) => left.localeCompare(right));
|
|
257
|
+
if (entries.length === 0)
|
|
258
|
+
throw new TypeError("mobile.updates.publicKeys must contain at least one key.");
|
|
259
|
+
const publicKeys = Object.fromEntries(entries.map(([keyId, key]) => {
|
|
260
|
+
if (!UPDATE_NAME_PATTERN.test(keyId))
|
|
261
|
+
throw new TypeError("mobile.updates.publicKeys contains an invalid key ID.");
|
|
262
|
+
const normalized = requireText(key, `mobile.updates.publicKeys.${keyId}`);
|
|
263
|
+
if (!UPDATE_PUBLIC_KEY_PATTERN.test(normalized))
|
|
264
|
+
throw new TypeError(`mobile.updates.publicKeys.${keyId} must be base64-encoded ECDSA P-256 SPKI DER.`);
|
|
265
|
+
let decoded;
|
|
266
|
+
try {
|
|
267
|
+
decoded = Buffer.from(normalized, "base64");
|
|
268
|
+
} catch {
|
|
269
|
+
throw new TypeError(`mobile.updates.publicKeys.${keyId} must be canonical base64.`);
|
|
270
|
+
}
|
|
271
|
+
let keyType;
|
|
272
|
+
let namedCurve;
|
|
273
|
+
try {
|
|
274
|
+
const publicKey = createPublicKey({
|
|
275
|
+
format: "der",
|
|
276
|
+
key: decoded,
|
|
277
|
+
type: "spki"
|
|
278
|
+
});
|
|
279
|
+
keyType = publicKey.asymmetricKeyType;
|
|
280
|
+
namedCurve = publicKey.asymmetricKeyDetails?.namedCurve;
|
|
281
|
+
} catch {
|
|
282
|
+
keyType = undefined;
|
|
283
|
+
}
|
|
284
|
+
if (decoded.toString("base64") !== normalized || keyType !== "ec" || namedCurve !== "prime256v1")
|
|
285
|
+
throw new TypeError(`mobile.updates.publicKeys.${keyId} is not an ECDSA P-256 SPKI public key.`);
|
|
286
|
+
return [keyId, normalized];
|
|
287
|
+
}));
|
|
288
|
+
return { channel, manifestUrl: manifestUrl.href, publicKeys };
|
|
289
|
+
}, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
|
|
244
290
|
if (segment === "*" && (index !== count - 1 || count === 1)) {
|
|
245
291
|
throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
|
|
246
292
|
}
|
|
@@ -298,6 +344,7 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
298
344
|
if (deepLinkScheme && !SCHEME_PATTERN.test(deepLinkScheme)) {
|
|
299
345
|
throw new TypeError("mobile.deepLinks.scheme is not a valid URL scheme.");
|
|
300
346
|
}
|
|
347
|
+
const updates = normalizeUpdates(config, productionOrigin);
|
|
301
348
|
return {
|
|
302
349
|
androidCertificateFingerprints: normalizeCertificateFingerprints(config.deepLinks?.android?.sha256CertificateFingerprints),
|
|
303
350
|
appId,
|
|
@@ -314,7 +361,8 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
314
361
|
nativeProjectDirectory: resolveProjectPath(projectRoot, config.nativeProject?.directory ?? (config.engine === "expo" ? ".absolutejs/mobile/expo" : "mobile"), "mobile.nativeProject.directory"),
|
|
315
362
|
platforms: normalizePlatforms(config.platforms),
|
|
316
363
|
productionOrigin,
|
|
317
|
-
pushAndroidGoogleServicesFile: resolveProjectPath(projectRoot, config.pushNotifications?.android?.googleServicesFile ?? "google-services.json", "mobile.pushNotifications.android.googleServicesFile")
|
|
364
|
+
pushAndroidGoogleServicesFile: resolveProjectPath(projectRoot, config.pushNotifications?.android?.googleServicesFile ?? "google-services.json", "mobile.pushNotifications.android.googleServicesFile"),
|
|
365
|
+
...updates ? { updates } : {}
|
|
318
366
|
};
|
|
319
367
|
};
|
|
320
368
|
var init_config = __esm(() => {
|
|
@@ -322,6 +370,8 @@ var init_config = __esm(() => {
|
|
|
322
370
|
SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*$/;
|
|
323
371
|
APPLE_APP_ID_PREFIX_PATTERN = /^[A-Z0-9]{10}$/;
|
|
324
372
|
CERTIFICATE_FINGERPRINT_PATTERN = /^[0-9A-F]{64}$/;
|
|
373
|
+
UPDATE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
374
|
+
UPDATE_PUBLIC_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u;
|
|
325
375
|
HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/;
|
|
326
376
|
EXPO_RESERVED_ROUTE_PREFIXES = new Set([
|
|
327
377
|
"_expo",
|
|
@@ -7497,6 +7547,12 @@ var shellExpoDevicesModule = () => {
|
|
|
7497
7547
|
return candidate;
|
|
7498
7548
|
throw new TypeError("AbsoluteJS Expo device bridge module is missing.");
|
|
7499
7549
|
};
|
|
7550
|
+
var shellUpdateModule = () => {
|
|
7551
|
+
const candidate = ["js", "ts"].map((extension) => join9(import.meta.dir, `shellUpdate.${extension}`)).find(existsSync2);
|
|
7552
|
+
if (candidate)
|
|
7553
|
+
return candidate;
|
|
7554
|
+
throw new TypeError("AbsoluteJS mobile update shell module is missing.");
|
|
7555
|
+
};
|
|
7500
7556
|
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
7501
7557
|
var contentSecurityPolicy = (productionOrigin) => {
|
|
7502
7558
|
const backend = new URL(productionOrigin);
|
|
@@ -7557,7 +7613,7 @@ var resolveProjectImport = async (projectRoot, specifier) => {
|
|
|
7557
7613
|
throw new TypeError(`${specifier} has an unsafe import entry.`);
|
|
7558
7614
|
return resolved;
|
|
7559
7615
|
};
|
|
7560
|
-
var buildShellBootstrap = async (staging, auth, sync, storagePrefix, engine, deviceCapabilities, projectRoot) => {
|
|
7616
|
+
var buildShellBootstrap = async (staging, auth, sync, storagePrefix, engine, updates, deviceCapabilities, projectRoot) => {
|
|
7561
7617
|
const capacitor = engine !== "expo";
|
|
7562
7618
|
const shellCapabilities = capacitor ? deviceCapabilities.capabilities : [];
|
|
7563
7619
|
const modulePath = shellBootstrapModule();
|
|
@@ -7565,12 +7621,13 @@ var buildShellBootstrap = async (staging, auth, sync, storagePrefix, engine, dev
|
|
|
7565
7621
|
const syncFactory = capacitor ? "installAbsoluteMobileShellSync" : "installAbsoluteExpoShellSync";
|
|
7566
7622
|
const authImport = auth ? `import { ${authFactory} } from ${JSON.stringify(capacitor ? shellAuthModule() : shellExpoAuthModule())};
|
|
7567
7623
|
` : "";
|
|
7568
|
-
const options = auth ? `{ createAuth: ${authFactory}${sync ? `, installSync: ${syncFactory}` : ""} }` : "";
|
|
7569
7624
|
const syncImport = sync ? `import { ${syncFactory} } from ${JSON.stringify(capacitor ? shellSyncModule() : shellExpoSyncModule())};
|
|
7570
7625
|
` : "";
|
|
7571
7626
|
const pushIndex = shellCapabilities.indexOf("pushNotifications");
|
|
7572
7627
|
const push = pushIndex !== -1;
|
|
7573
7628
|
const pushImport = push ? `import { createAbsoluteMobileShellPush } from ${JSON.stringify(shellPushModule())};
|
|
7629
|
+
` : "";
|
|
7630
|
+
const updateImport = updates ? `import { installAbsoluteMobileShellUpdates } from ${JSON.stringify(shellUpdateModule())};
|
|
7574
7631
|
` : "";
|
|
7575
7632
|
const capabilityImports = (await Promise.all(shellCapabilities.map(async (name, index) => {
|
|
7576
7633
|
const provider = deviceCapabilities.providers[name];
|
|
@@ -7587,15 +7644,21 @@ const absoluteMobilePushCapability = absoluteDeviceCapability${pushIndex}(absolu
|
|
|
7587
7644
|
const baseAdapterModule = capacitor ? await resolveProjectImport(projectRoot, "@absolutejs/devices-capacitor") : shellExpoDevicesModule();
|
|
7588
7645
|
const adapterImport = capacitor ? `import { installCapacitorDeviceAdapterIfNative } from ${JSON.stringify(baseAdapterModule)};` : `import { createAbsoluteExpoBridgeFetch, installAbsoluteExpoWebDeviceAdapter } from ${JSON.stringify(baseAdapterModule)};`;
|
|
7589
7646
|
const adapterInstall = capacitor ? `installCapacitorDeviceAdapterIfNative({ storagePrefix: ${JSON.stringify(storagePrefix)}${capabilityOptions ? `, ${capabilityOptions}` : ""} });` : `installAbsoluteExpoWebDeviceAdapter(${JSON.stringify(deviceCapabilities.capabilities)});`;
|
|
7590
|
-
|
|
7591
|
-
if (capacitor)
|
|
7592
|
-
shellOptions = options;
|
|
7647
|
+
const shellOptionProperties = [];
|
|
7593
7648
|
if (push) {
|
|
7594
|
-
|
|
7595
|
-
}
|
|
7649
|
+
shellOptionProperties.push("createAuth: (config, options) => createAbsoluteMobileShellAuth(config, options)", "beforeSignOut: absoluteMobilePush.beforeSignOut", "connectPush: (auth) => absoluteMobilePush.connect(auth, absoluteMobilePushCapability)");
|
|
7650
|
+
} else if (auth)
|
|
7651
|
+
shellOptionProperties.push(`createAuth: ${authFactory}`);
|
|
7652
|
+
else if (!capacitor)
|
|
7653
|
+
shellOptionProperties.push("createFetch: createAbsoluteExpoBridgeFetch");
|
|
7654
|
+
if (sync)
|
|
7655
|
+
shellOptionProperties.push(`installSync: ${syncFactory}`);
|
|
7656
|
+
if (updates)
|
|
7657
|
+
shellOptionProperties.push("installUpdates: installAbsoluteMobileShellUpdates");
|
|
7658
|
+
const shellOptions = `{ ${shellOptionProperties.join(", ")} }`;
|
|
7596
7659
|
await writeFile10(entryPath, `import { startAbsoluteMobileShell } from ${JSON.stringify(modulePath)};
|
|
7597
7660
|
${adapterImport}
|
|
7598
|
-
${authImport}${syncImport}${pushImport}${capabilityImports}
|
|
7661
|
+
${authImport}${syncImport}${pushImport}${updateImport}${capabilityImports}
|
|
7599
7662
|
${pushSetup}${adapterInstall}
|
|
7600
7663
|
void startAbsoluteMobileShell(${shellOptions});
|
|
7601
7664
|
`);
|
|
@@ -7738,10 +7801,12 @@ var materializeAbsoluteCapacitorWebBundle = async (options) => {
|
|
|
7738
7801
|
deviceCapabilities: options.deviceCapabilities.capabilities,
|
|
7739
7802
|
entry: options.config.entry,
|
|
7740
7803
|
format: ABSOLUTE_MOBILE_CLIENT_MANIFEST_FORMAT,
|
|
7804
|
+
nativeRuntime: options.runtimeFingerprint,
|
|
7741
7805
|
pages,
|
|
7742
7806
|
productionOrigin: options.config.productionOrigin,
|
|
7743
7807
|
routes: options.artifact.routes,
|
|
7744
7808
|
runtime: options.artifact.runtime,
|
|
7809
|
+
...options.config.updates ? { updates: options.config.updates } : {},
|
|
7745
7810
|
...options.sync ? {
|
|
7746
7811
|
sync: {
|
|
7747
7812
|
background: {
|
|
@@ -7761,7 +7826,7 @@ var materializeAbsoluteCapacitorWebBundle = async (options) => {
|
|
|
7761
7826
|
writeFile10(join9(staging, MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
|
|
7762
7827
|
`),
|
|
7763
7828
|
writeFile10(join9(staging, INDEX_FILE), indexHtml(options.config.appName, options.config.productionOrigin)),
|
|
7764
|
-
buildShellBootstrap(staging, options.auth !== undefined, options.auth !== undefined && options.sync === true, `absolutejs.${options.auth?.clientId ?? options.config.appId}.`, options.config.engine, options.deviceCapabilities, options.projectRoot)
|
|
7829
|
+
buildShellBootstrap(staging, options.auth !== undefined, options.auth !== undefined && options.sync === true, `absolutejs.${options.auth?.clientId ?? options.config.appId}.`, options.config.engine, options.config.engine === "capacitor" && options.config.updates !== undefined, options.deviceCapabilities, options.projectRoot)
|
|
7765
7830
|
]);
|
|
7766
7831
|
await installBundle(staging, destination);
|
|
7767
7832
|
return manifest;
|
|
@@ -9261,6 +9326,149 @@ var syncAbsoluteExpoWebAssets = async (config) => {
|
|
|
9261
9326
|
return { appBuild, assets: assets.length, bundleId, path: destination };
|
|
9262
9327
|
};
|
|
9263
9328
|
|
|
9329
|
+
// src/mobile/updateRuntime.ts
|
|
9330
|
+
import { createHash as createHash11 } from "crypto";
|
|
9331
|
+
|
|
9332
|
+
// src/mobile/updateProtocol.ts
|
|
9333
|
+
var ABSOLUTE_MOBILE_UPDATE_FORMAT = 1;
|
|
9334
|
+
var ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES = 32 * 1024 * 1024;
|
|
9335
|
+
var ABSOLUTE_MOBILE_UPDATE_MAX_FILES = 1e4;
|
|
9336
|
+
var ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES = 128 * 1024 * 1024;
|
|
9337
|
+
var ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM = "ecdsa-p256-sha256";
|
|
9338
|
+
var HASH_PATTERN = /^[a-f0-9]{64}$/u;
|
|
9339
|
+
var RELEASE_PATTERN = /^amu_[a-f0-9]{64}$/u;
|
|
9340
|
+
var KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
9341
|
+
var CHANNEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
9342
|
+
var isClassification = (value) => value === "bug-fix" || value === "content" || value === "security";
|
|
9343
|
+
var object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9344
|
+
var canonicalValue = (value) => {
|
|
9345
|
+
if (Array.isArray(value))
|
|
9346
|
+
return value.map(canonicalValue);
|
|
9347
|
+
if (!object3(value))
|
|
9348
|
+
return value;
|
|
9349
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
|
|
9350
|
+
};
|
|
9351
|
+
var absoluteMobileUpdateSigningPayload = (manifest) => new TextEncoder().encode(canonicalizeAbsoluteMobileUpdate(manifest));
|
|
9352
|
+
var canonicalizeAbsoluteMobileUpdate = (value) => JSON.stringify(canonicalValue(value));
|
|
9353
|
+
var requireText2 = (value, field2) => {
|
|
9354
|
+
if (typeof value !== "string" || value.length === 0)
|
|
9355
|
+
throw new TypeError(`${field2} must be a non-empty string.`);
|
|
9356
|
+
return value;
|
|
9357
|
+
};
|
|
9358
|
+
var normalizeAbsoluteMobileUpdatePath = (value) => {
|
|
9359
|
+
const path = requireText2(value, "Update file path").replaceAll("\\", "/");
|
|
9360
|
+
if (path.startsWith("/") || path.includes("\x00") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
|
|
9361
|
+
throw new TypeError("Update file paths must be normalized relative paths.");
|
|
9362
|
+
}
|
|
9363
|
+
return path;
|
|
9364
|
+
};
|
|
9365
|
+
var parseFile = (value) => {
|
|
9366
|
+
if (!object3(value))
|
|
9367
|
+
throw new TypeError("Update files must be objects.");
|
|
9368
|
+
const path = normalizeAbsoluteMobileUpdatePath(value.path);
|
|
9369
|
+
if (typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes < 0 || value.bytes > ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES) {
|
|
9370
|
+
throw new TypeError(`Update file ${path} has an invalid byte length.`);
|
|
9371
|
+
}
|
|
9372
|
+
if (typeof value.sha256 !== "string" || !HASH_PATTERN.test(value.sha256))
|
|
9373
|
+
throw new TypeError(`Update file ${path} has an invalid SHA-256 digest.`);
|
|
9374
|
+
return { bytes: value.bytes, path, sha256: value.sha256 };
|
|
9375
|
+
};
|
|
9376
|
+
var parseAbsoluteMobileUnsignedUpdateManifest = (value) => {
|
|
9377
|
+
if (!object3(value) || value.format !== ABSOLUTE_MOBILE_UPDATE_FORMAT)
|
|
9378
|
+
throw new TypeError("Invalid AbsoluteJS mobile update manifest.");
|
|
9379
|
+
const appId = requireText2(value.appId, "Update appId");
|
|
9380
|
+
const channel = requireText2(value.channel, "Update channel");
|
|
9381
|
+
if (!CHANNEL_PATTERN.test(channel))
|
|
9382
|
+
throw new TypeError("Update channel contains unsupported characters.");
|
|
9383
|
+
if (!isClassification(value.classification))
|
|
9384
|
+
throw new TypeError("Update classification is invalid.");
|
|
9385
|
+
const createdAt = requireText2(value.createdAt, "Update createdAt");
|
|
9386
|
+
if (!Number.isFinite(Date.parse(createdAt)) || new Date(createdAt).toISOString() !== createdAt)
|
|
9387
|
+
throw new TypeError("Update createdAt must be a canonical ISO timestamp.");
|
|
9388
|
+
if (!Array.isArray(value.files) || value.files.length === 0)
|
|
9389
|
+
throw new TypeError("An update must contain at least one file.");
|
|
9390
|
+
if (value.files.length > ABSOLUTE_MOBILE_UPDATE_MAX_FILES)
|
|
9391
|
+
throw new TypeError("Update contains too many files.");
|
|
9392
|
+
const files = value.files.map(parseFile);
|
|
9393
|
+
const sorted = [...files].sort((left, right) => left.path.localeCompare(right.path));
|
|
9394
|
+
if (files.some((file, index) => file.path !== sorted[index]?.path))
|
|
9395
|
+
throw new TypeError("Update files must be sorted by path.");
|
|
9396
|
+
if (new Set(files.map(({ path }) => path)).size !== files.length)
|
|
9397
|
+
throw new TypeError("Update file paths must be unique.");
|
|
9398
|
+
if (files.reduce((total, file) => total + file.bytes, 0) > ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES)
|
|
9399
|
+
throw new TypeError("Update exceeds the maximum uncompressed size.");
|
|
9400
|
+
if (typeof value.releaseId !== "string" || !RELEASE_PATTERN.test(value.releaseId))
|
|
9401
|
+
throw new TypeError("Update releaseId is invalid.");
|
|
9402
|
+
if (typeof value.runtimeFingerprint !== "string" || !HASH_PATTERN.test(value.runtimeFingerprint))
|
|
9403
|
+
throw new TypeError("Update runtime fingerprint is invalid.");
|
|
9404
|
+
if (value.withinSubmittedPurpose !== true)
|
|
9405
|
+
throw new TypeError("OTA updates must attest that they remain within the submitted app purpose.");
|
|
9406
|
+
return {
|
|
9407
|
+
appId,
|
|
9408
|
+
channel,
|
|
9409
|
+
classification: value.classification,
|
|
9410
|
+
createdAt,
|
|
9411
|
+
files,
|
|
9412
|
+
format: ABSOLUTE_MOBILE_UPDATE_FORMAT,
|
|
9413
|
+
releaseId: value.releaseId,
|
|
9414
|
+
runtimeFingerprint: value.runtimeFingerprint,
|
|
9415
|
+
withinSubmittedPurpose: true
|
|
9416
|
+
};
|
|
9417
|
+
};
|
|
9418
|
+
var parseAbsoluteMobileUpdateManifest = (value) => {
|
|
9419
|
+
if (!object3(value))
|
|
9420
|
+
throw new TypeError("Invalid AbsoluteJS mobile update manifest.");
|
|
9421
|
+
const { signature: signatureValue, ...unsignedValue } = value;
|
|
9422
|
+
const unsigned = parseAbsoluteMobileUnsignedUpdateManifest(unsignedValue);
|
|
9423
|
+
if (!object3(signatureValue))
|
|
9424
|
+
throw new TypeError("Update signature is missing.");
|
|
9425
|
+
if (signatureValue.algorithm !== ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM)
|
|
9426
|
+
throw new TypeError("Update signature algorithm is unsupported.");
|
|
9427
|
+
const keyId = requireText2(signatureValue.keyId, "Update signature keyId");
|
|
9428
|
+
if (!KEY_ID_PATTERN.test(keyId))
|
|
9429
|
+
throw new TypeError("Update signature keyId is invalid.");
|
|
9430
|
+
const signature = requireText2(signatureValue.value, "Update signature value");
|
|
9431
|
+
let signatureBytes;
|
|
9432
|
+
try {
|
|
9433
|
+
signatureBytes = Uint8Array.from(atob(signature), (character) => character.charCodeAt(0));
|
|
9434
|
+
} catch {
|
|
9435
|
+
signatureBytes = new Uint8Array;
|
|
9436
|
+
}
|
|
9437
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(signature) || signatureBytes.byteLength !== 64 || btoa(String.fromCharCode(...signatureBytes)) !== signature)
|
|
9438
|
+
throw new TypeError("Update signature is not canonical base64.");
|
|
9439
|
+
return {
|
|
9440
|
+
...unsigned,
|
|
9441
|
+
signature: {
|
|
9442
|
+
algorithm: ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM,
|
|
9443
|
+
keyId,
|
|
9444
|
+
value: signature
|
|
9445
|
+
}
|
|
9446
|
+
};
|
|
9447
|
+
};
|
|
9448
|
+
var unsignedAbsoluteMobileUpdate = (manifest) => {
|
|
9449
|
+
const { signature: _signature, ...unsigned } = manifest;
|
|
9450
|
+
return unsigned;
|
|
9451
|
+
};
|
|
9452
|
+
|
|
9453
|
+
// src/mobile/updateRuntime.ts
|
|
9454
|
+
var ABSOLUTE_MOBILE_SHELL_ABI = 1;
|
|
9455
|
+
var ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT = 1;
|
|
9456
|
+
var createAbsoluteMobileUpdateRuntimeDescriptor = (options) => ({
|
|
9457
|
+
appId: options.config.appId,
|
|
9458
|
+
auth: options.auth ?? null,
|
|
9459
|
+
deepLinks: {
|
|
9460
|
+
hosts: options.config.deepLinkHosts,
|
|
9461
|
+
...options.config.deepLinkScheme ? { scheme: options.config.deepLinkScheme } : {}
|
|
9462
|
+
},
|
|
9463
|
+
deviceCapabilities: options.deviceCapabilities,
|
|
9464
|
+
engine: options.config.engine,
|
|
9465
|
+
format: ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT,
|
|
9466
|
+
shellAbi: ABSOLUTE_MOBILE_SHELL_ABI,
|
|
9467
|
+
syncSchema: options.syncSchema ?? null,
|
|
9468
|
+
updates: options.config.updates ?? null
|
|
9469
|
+
});
|
|
9470
|
+
var fingerprintAbsoluteMobileUpdateRuntime = (descriptor) => createHash11("sha256").update(canonicalizeAbsoluteMobileUpdate(descriptor)).digest("hex");
|
|
9471
|
+
|
|
9264
9472
|
// src/mobile/buildPipeline.ts
|
|
9265
9473
|
var isElysiaApp = (value) => typeof value === "object" && value !== null && typeof Reflect.get(value, "compile") === "function" && Array.isArray(Reflect.get(value, "routes"));
|
|
9266
9474
|
var isStringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
@@ -9336,6 +9544,12 @@ var finalizeAbsoluteMobileCompatibilityBuild = async (options) => {
|
|
|
9336
9544
|
const sync = auth !== undefined && projectUsesAbsoluteSync(options.projectRoot);
|
|
9337
9545
|
const syncSchema = sync ? discoverAbsoluteSyncSchema(options.projectRoot) : undefined;
|
|
9338
9546
|
const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot, mobile.engine);
|
|
9547
|
+
const runtimeFingerprint = fingerprintAbsoluteMobileUpdateRuntime(createAbsoluteMobileUpdateRuntimeDescriptor({
|
|
9548
|
+
...auth ? { auth } : {},
|
|
9549
|
+
config: mobile,
|
|
9550
|
+
deviceCapabilities,
|
|
9551
|
+
...syncSchema ? { syncSchema } : {}
|
|
9552
|
+
}));
|
|
9339
9553
|
const usesPush = deviceCapabilities.capabilities.includes("pushNotifications");
|
|
9340
9554
|
if (usesPush && !auth)
|
|
9341
9555
|
throw new TypeError("Portable push notifications require @absolutejs/auth so provider tokens can be registered without exposing identity controls to page code.");
|
|
@@ -9364,6 +9578,7 @@ var finalizeAbsoluteMobileCompatibilityBuild = async (options) => {
|
|
|
9364
9578
|
config: mobile,
|
|
9365
9579
|
deviceCapabilities,
|
|
9366
9580
|
projectRoot: options.projectRoot,
|
|
9581
|
+
runtimeFingerprint,
|
|
9367
9582
|
...sync ? { sync: true } : {},
|
|
9368
9583
|
...syncSchema ? { syncSchema: { components: syncSchema.components } } : {}
|
|
9369
9584
|
});
|
|
@@ -11291,6 +11506,65 @@ var applyAbsoluteNativeDeviceCapabilities = async (projectRoot, config, platform
|
|
|
11291
11506
|
// src/mobile/index.ts
|
|
11292
11507
|
init_nativeAuth();
|
|
11293
11508
|
|
|
11509
|
+
// src/mobile/nativeUpdates.ts
|
|
11510
|
+
import { readFile as readFile19, rename as rename14, writeFile as writeFile16 } from "fs/promises";
|
|
11511
|
+
import { join as join21 } from "path";
|
|
11512
|
+
var START = "// absolutejs:mobile-updates:start";
|
|
11513
|
+
var END = "// absolutejs:mobile-updates:end";
|
|
11514
|
+
var writeChanged = async (path, source) => {
|
|
11515
|
+
const current = await readFile19(path, "utf8");
|
|
11516
|
+
if (current === source)
|
|
11517
|
+
return false;
|
|
11518
|
+
const temporary = `${path}.${crypto.randomUUID()}.tmp`;
|
|
11519
|
+
await writeFile16(temporary, source, { flag: "wx" });
|
|
11520
|
+
await rename14(temporary, path);
|
|
11521
|
+
return true;
|
|
11522
|
+
};
|
|
11523
|
+
var replaceRegion = (source, region) => {
|
|
11524
|
+
const start = source.indexOf(START);
|
|
11525
|
+
const end = source.indexOf(END);
|
|
11526
|
+
if (start < 0 !== end < 0 || end < start)
|
|
11527
|
+
throw new TypeError("AbsoluteJS mobile update markers are malformed.");
|
|
11528
|
+
if (start >= 0) {
|
|
11529
|
+
const from = source.lastIndexOf(`
|
|
11530
|
+
`, start) + 1;
|
|
11531
|
+
const newline = source.indexOf(`
|
|
11532
|
+
`, end + END.length);
|
|
11533
|
+
return `${source.slice(0, from)}${region}${source.slice(newline < 0 ? source.length : newline + 1)}`;
|
|
11534
|
+
}
|
|
11535
|
+
if (!region)
|
|
11536
|
+
return source;
|
|
11537
|
+
const launch = source.indexOf("didFinishLaunchingWithOptions");
|
|
11538
|
+
const brace = launch < 0 ? -1 : source.indexOf("{", launch);
|
|
11539
|
+
const insert = brace < 0 ? -1 : source.indexOf(`
|
|
11540
|
+
`, brace) + 1;
|
|
11541
|
+
if (insert <= 0)
|
|
11542
|
+
throw new TypeError("Could not find a safe iOS location for mobile update recovery.");
|
|
11543
|
+
return `${source.slice(0, insert)}${region}${source.slice(insert)}`;
|
|
11544
|
+
};
|
|
11545
|
+
var iosRecoveryRegion = ` ${START}
|
|
11546
|
+
// A confirmed Capacitor snapshot lives in Library/NoCloud and is not
|
|
11547
|
+
// restored during device migration. Clear only a dangling pointer so
|
|
11548
|
+
// Capacitor falls back to the store-signed embedded bundle.
|
|
11549
|
+
if let persisted = UserDefaults.standard.string(forKey: "serverBasePath"), !persisted.isEmpty,
|
|
11550
|
+
let library = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first {
|
|
11551
|
+
let snapshot = library
|
|
11552
|
+
.appendingPathComponent("NoCloud/ionic_built_snapshots", isDirectory: true)
|
|
11553
|
+
.appendingPathComponent(URL(fileURLWithPath: persisted).lastPathComponent, isDirectory: true)
|
|
11554
|
+
if !FileManager.default.fileExists(atPath: snapshot.path) {
|
|
11555
|
+
UserDefaults.standard.removeObject(forKey: "serverBasePath")
|
|
11556
|
+
}
|
|
11557
|
+
}
|
|
11558
|
+
${END}
|
|
11559
|
+
`;
|
|
11560
|
+
var applyAbsoluteNativeUpdates = async (config, platforms = config.platforms) => {
|
|
11561
|
+
if (config.engine !== "capacitor" || !platforms.includes("ios"))
|
|
11562
|
+
return { changed: false };
|
|
11563
|
+
const path = join21(config.nativeProjectDirectory, "ios/App/App/AppDelegate.swift");
|
|
11564
|
+
const source = await readFile19(path, "utf8");
|
|
11565
|
+
const updated = replaceRegion(source, config.updates ? iosRecoveryRegion : "");
|
|
11566
|
+
return { changed: await writeChanged(path, updated) };
|
|
11567
|
+
};
|
|
11294
11568
|
// src/mobile/releasePublisher.ts
|
|
11295
11569
|
import { access as access12 } from "fs/promises";
|
|
11296
11570
|
import { isAbsolute as isAbsolute6, relative as relative12, resolve as resolve15, sep as sep8 } from "path";
|
|
@@ -11468,8 +11742,8 @@ var propertyName = (property) => {
|
|
|
11468
11742
|
return property.name.text;
|
|
11469
11743
|
return;
|
|
11470
11744
|
};
|
|
11471
|
-
var objectPropertyExpression = (
|
|
11472
|
-
const property =
|
|
11745
|
+
var objectPropertyExpression = (object4, name) => {
|
|
11746
|
+
const property = object4.properties.find((candidate) => propertyName(candidate) === name);
|
|
11473
11747
|
if (property && ts2.isPropertyAssignment(property)) {
|
|
11474
11748
|
return property.initializer;
|
|
11475
11749
|
}
|
|
@@ -11665,8 +11939,8 @@ var spreadObject = (expression, checker, bindings) => {
|
|
|
11665
11939
|
return;
|
|
11666
11940
|
return callableObject(expression, checker);
|
|
11667
11941
|
};
|
|
11668
|
-
var objectAssetKey = (
|
|
11669
|
-
for (const property of [...
|
|
11942
|
+
var objectAssetKey = (object4, name, checker, bindings = new Map) => {
|
|
11943
|
+
for (const property of [...object4.properties].reverse()) {
|
|
11670
11944
|
if (propertyName(property) === name && ts2.isShorthandPropertyAssignment(property)) {
|
|
11671
11945
|
return assetKeyWithBindings(property.name, checker, bindings);
|
|
11672
11946
|
}
|
|
@@ -12380,6 +12654,344 @@ var installAbsoluteMobileUiPrimitives = () => {
|
|
|
12380
12654
|
}
|
|
12381
12655
|
};
|
|
12382
12656
|
};
|
|
12657
|
+
// src/mobile/updateClient.ts
|
|
12658
|
+
var exactManifestUrl = (value) => {
|
|
12659
|
+
const url = new URL(value);
|
|
12660
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1")
|
|
12661
|
+
throw new TypeError("Mobile update manifests require HTTPS outside loopback development.");
|
|
12662
|
+
if (url.username || url.password || url.hash)
|
|
12663
|
+
throw new TypeError("Mobile update manifest URLs cannot contain credentials or fragments.");
|
|
12664
|
+
return url;
|
|
12665
|
+
};
|
|
12666
|
+
var fileUrl = (manifestUrl, releaseId, path) => {
|
|
12667
|
+
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
12668
|
+
const base = new URL(`./${encodeURIComponent(releaseId)}/files/`, manifestUrl);
|
|
12669
|
+
const result = new URL(encodedPath, base);
|
|
12670
|
+
if (result.origin !== manifestUrl.origin || !result.pathname.startsWith(base.pathname))
|
|
12671
|
+
throw new TypeError("Mobile update asset escaped its signed release origin.");
|
|
12672
|
+
return result;
|
|
12673
|
+
};
|
|
12674
|
+
var readChunks = async (reader, maximum2, chunks = [], received = 0) => {
|
|
12675
|
+
const result = await reader.read();
|
|
12676
|
+
if (result.done)
|
|
12677
|
+
return { chunks, received };
|
|
12678
|
+
const total = received + result.value.byteLength;
|
|
12679
|
+
if (total > maximum2)
|
|
12680
|
+
throw new TypeError("Mobile update response exceeds its signed size.");
|
|
12681
|
+
chunks.push(result.value);
|
|
12682
|
+
return readChunks(reader, maximum2, chunks, total);
|
|
12683
|
+
};
|
|
12684
|
+
var readBounded = async (response, maximum2) => {
|
|
12685
|
+
const declared = Number(response.headers.get("content-length"));
|
|
12686
|
+
if (Number.isFinite(declared) && declared > maximum2)
|
|
12687
|
+
throw new TypeError("Mobile update response exceeds its signed size.");
|
|
12688
|
+
if (!response.body)
|
|
12689
|
+
return new Uint8Array;
|
|
12690
|
+
const reader = response.body.getReader();
|
|
12691
|
+
let result;
|
|
12692
|
+
try {
|
|
12693
|
+
result = await readChunks(reader, maximum2);
|
|
12694
|
+
} catch (error) {
|
|
12695
|
+
await reader.cancel().catch(() => {
|
|
12696
|
+
return;
|
|
12697
|
+
});
|
|
12698
|
+
throw error;
|
|
12699
|
+
}
|
|
12700
|
+
const contents = new Uint8Array(result.received);
|
|
12701
|
+
let offset = 0;
|
|
12702
|
+
for (const chunk of result.chunks) {
|
|
12703
|
+
contents.set(chunk, offset);
|
|
12704
|
+
offset += chunk.byteLength;
|
|
12705
|
+
}
|
|
12706
|
+
return contents;
|
|
12707
|
+
};
|
|
12708
|
+
var requestHeaders = (config) => ({
|
|
12709
|
+
"x-absolute-mobile-app": config.appId,
|
|
12710
|
+
"x-absolute-mobile-channel": config.channel,
|
|
12711
|
+
"x-absolute-mobile-installation": config.installationId,
|
|
12712
|
+
"x-absolute-mobile-release": config.currentReleaseId,
|
|
12713
|
+
"x-absolute-mobile-runtime": config.runtimeFingerprint
|
|
12714
|
+
});
|
|
12715
|
+
var requireCompatible = (manifest, config) => {
|
|
12716
|
+
if (manifest.appId !== config.appId)
|
|
12717
|
+
throw new TypeError("Mobile update belongs to another app.");
|
|
12718
|
+
if (manifest.channel !== config.channel)
|
|
12719
|
+
throw new TypeError("Mobile update belongs to another channel.");
|
|
12720
|
+
if (manifest.runtimeFingerprint !== config.runtimeFingerprint)
|
|
12721
|
+
throw new TypeError("Mobile update requires a different native runtime.");
|
|
12722
|
+
};
|
|
12723
|
+
var createAbsoluteMobileUpdateClient = (options) => {
|
|
12724
|
+
const manifestUrl = exactManifestUrl(options.config.manifestUrl);
|
|
12725
|
+
const request = options.fetch ?? globalThis.fetch;
|
|
12726
|
+
const downloadFiles = async (manifest, index = 0, received = 0) => {
|
|
12727
|
+
const file = manifest.files[index];
|
|
12728
|
+
if (!file)
|
|
12729
|
+
return received;
|
|
12730
|
+
const asset = await request(fileUrl(manifestUrl, manifest.releaseId, file.path), {
|
|
12731
|
+
cache: "no-store",
|
|
12732
|
+
credentials: "omit",
|
|
12733
|
+
redirect: "error",
|
|
12734
|
+
signal: AbortSignal.timeout(30000)
|
|
12735
|
+
});
|
|
12736
|
+
if (!asset.ok)
|
|
12737
|
+
throw new TypeError(`Mobile update asset ${file.path} failed with HTTP ${asset.status}.`);
|
|
12738
|
+
const contents = await readBounded(asset, file.bytes);
|
|
12739
|
+
const total = received + contents.byteLength;
|
|
12740
|
+
if (contents.byteLength !== file.bytes || total > ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES)
|
|
12741
|
+
throw new TypeError(`Mobile update asset ${file.path} has an invalid size.`);
|
|
12742
|
+
if (await options.verifier.digest(contents) !== file.sha256)
|
|
12743
|
+
throw new TypeError(`Mobile update asset ${file.path} failed integrity verification.`);
|
|
12744
|
+
await options.store.write(file, contents);
|
|
12745
|
+
return downloadFiles(manifest, index + 1, total);
|
|
12746
|
+
};
|
|
12747
|
+
const check = async (download = false) => {
|
|
12748
|
+
const response = await request(manifestUrl, {
|
|
12749
|
+
cache: "no-store",
|
|
12750
|
+
credentials: "omit",
|
|
12751
|
+
headers: requestHeaders(options.config),
|
|
12752
|
+
redirect: "error",
|
|
12753
|
+
signal: AbortSignal.timeout(15000)
|
|
12754
|
+
});
|
|
12755
|
+
if (response.status === 204 || response.status === 304)
|
|
12756
|
+
return { kind: "current" };
|
|
12757
|
+
if (!response.ok)
|
|
12758
|
+
throw new TypeError(`Mobile update check failed with HTTP ${response.status}.`);
|
|
12759
|
+
const manifestBytes = await readBounded(response, 1024 * 1024);
|
|
12760
|
+
let manifestValue;
|
|
12761
|
+
try {
|
|
12762
|
+
manifestValue = JSON.parse(new TextDecoder().decode(manifestBytes));
|
|
12763
|
+
} catch {
|
|
12764
|
+
throw new TypeError("Mobile update manifest is not valid JSON.");
|
|
12765
|
+
}
|
|
12766
|
+
const manifest = parseAbsoluteMobileUpdateManifest(manifestValue);
|
|
12767
|
+
requireCompatible(manifest, options.config);
|
|
12768
|
+
if (!await options.verifier.verify(manifest))
|
|
12769
|
+
throw new TypeError("Mobile update signature verification failed.");
|
|
12770
|
+
if (manifest.releaseId === options.config.currentReleaseId)
|
|
12771
|
+
return { kind: "current" };
|
|
12772
|
+
if (!download)
|
|
12773
|
+
return { kind: "update-available", manifest };
|
|
12774
|
+
await options.store.begin(manifest);
|
|
12775
|
+
try {
|
|
12776
|
+
await downloadFiles(manifest);
|
|
12777
|
+
await options.store.commit(manifest);
|
|
12778
|
+
} catch (error) {
|
|
12779
|
+
await options.store.abort(manifest.releaseId);
|
|
12780
|
+
throw error;
|
|
12781
|
+
}
|
|
12782
|
+
return { kind: "downloaded", manifest };
|
|
12783
|
+
};
|
|
12784
|
+
return {
|
|
12785
|
+
check,
|
|
12786
|
+
activate: (releaseId) => options.store.activate(releaseId),
|
|
12787
|
+
download: () => check(true)
|
|
12788
|
+
};
|
|
12789
|
+
};
|
|
12790
|
+
// src/mobile/updatePublisher.ts
|
|
12791
|
+
import { access as access13 } from "fs/promises";
|
|
12792
|
+
import { isAbsolute as isAbsolute7, relative as relative15, resolve as resolve18, sep as sep9 } from "path";
|
|
12793
|
+
import { pathToFileURL as pathToFileURL4 } from "url";
|
|
12794
|
+
|
|
12795
|
+
// src/mobile/updateSigning.ts
|
|
12796
|
+
import { createHash as createHash12, sign, verify } from "crypto";
|
|
12797
|
+
import {
|
|
12798
|
+
cp as cp3,
|
|
12799
|
+
mkdir as mkdir13,
|
|
12800
|
+
mkdtemp as mkdtemp8,
|
|
12801
|
+
readdir as readdir5,
|
|
12802
|
+
readFile as readFile20,
|
|
12803
|
+
rename as rename15,
|
|
12804
|
+
rm as rm11,
|
|
12805
|
+
stat as stat4,
|
|
12806
|
+
writeFile as writeFile17
|
|
12807
|
+
} from "fs/promises";
|
|
12808
|
+
import { dirname as dirname14, join as join22, relative as relative14, resolve as resolve17 } from "path";
|
|
12809
|
+
var UPDATE_MANIFEST_FILE = "update.json";
|
|
12810
|
+
var UPDATE_FILES_DIRECTORY = "files";
|
|
12811
|
+
var sha2562 = (value) => createHash12("sha256").update(value).digest("hex");
|
|
12812
|
+
var listFiles = async (root, directory = root) => {
|
|
12813
|
+
const entries = await readdir5(directory, { withFileTypes: true });
|
|
12814
|
+
const paths = await Promise.all(entries.map(async (entry) => {
|
|
12815
|
+
const path = join22(directory, entry.name);
|
|
12816
|
+
if (entry.isDirectory())
|
|
12817
|
+
return listFiles(root, path);
|
|
12818
|
+
if (!entry.isFile())
|
|
12819
|
+
throw new TypeError("Mobile updates cannot contain links or special files.");
|
|
12820
|
+
return [relative14(root, path).replaceAll("\\", "/")];
|
|
12821
|
+
}));
|
|
12822
|
+
return paths.flat().sort();
|
|
12823
|
+
};
|
|
12824
|
+
var inspectFiles = async (root, paths) => Promise.all(paths.map(async (path) => {
|
|
12825
|
+
const source = join22(root, path);
|
|
12826
|
+
const [metadata, contents] = await Promise.all([
|
|
12827
|
+
stat4(source),
|
|
12828
|
+
readFile20(source)
|
|
12829
|
+
]);
|
|
12830
|
+
return { bytes: metadata.size, path, sha256: sha2562(contents) };
|
|
12831
|
+
}));
|
|
12832
|
+
var releaseIdFor = (value) => `amu_${sha2562(canonicalizeAbsoluteMobileUpdate(value))}`;
|
|
12833
|
+
var buildAbsoluteMobileUpdate = async (options) => {
|
|
12834
|
+
const bundleDirectory = resolve17(options.bundleDirectory);
|
|
12835
|
+
const outputRoot = resolve17(options.outputDirectory);
|
|
12836
|
+
if (outputRoot === bundleDirectory || outputRoot.startsWith(`${bundleDirectory}/`))
|
|
12837
|
+
throw new TypeError("Mobile update output must be outside the embedded bundle.");
|
|
12838
|
+
const paths = await listFiles(bundleDirectory);
|
|
12839
|
+
const files = await inspectFiles(bundleDirectory, paths);
|
|
12840
|
+
const withoutId = {
|
|
12841
|
+
appId: options.appId,
|
|
12842
|
+
channel: options.channel,
|
|
12843
|
+
classification: options.classification,
|
|
12844
|
+
createdAt: (options.createdAt ?? new Date).toISOString(),
|
|
12845
|
+
files,
|
|
12846
|
+
format: ABSOLUTE_MOBILE_UPDATE_FORMAT,
|
|
12847
|
+
runtimeFingerprint: options.runtimeFingerprint,
|
|
12848
|
+
withinSubmittedPurpose: true
|
|
12849
|
+
};
|
|
12850
|
+
const unsigned = {
|
|
12851
|
+
...withoutId,
|
|
12852
|
+
releaseId: releaseIdFor(withoutId)
|
|
12853
|
+
};
|
|
12854
|
+
const signature = sign("sha256", absoluteMobileUpdateSigningPayload(unsigned), { dsaEncoding: "ieee-p1363", key: options.privateKey });
|
|
12855
|
+
const manifest = parseAbsoluteMobileUpdateManifest({
|
|
12856
|
+
...unsigned,
|
|
12857
|
+
signature: {
|
|
12858
|
+
algorithm: "ecdsa-p256-sha256",
|
|
12859
|
+
keyId: options.keyId,
|
|
12860
|
+
value: signature.toString("base64")
|
|
12861
|
+
}
|
|
12862
|
+
});
|
|
12863
|
+
await mkdir13(outputRoot, { recursive: true });
|
|
12864
|
+
const outputDirectory = join22(outputRoot, manifest.releaseId);
|
|
12865
|
+
const staging = await mkdtemp8(join22(outputRoot, ".stage-"));
|
|
12866
|
+
try {
|
|
12867
|
+
await cp3(bundleDirectory, join22(staging, UPDATE_FILES_DIRECTORY), {
|
|
12868
|
+
force: true,
|
|
12869
|
+
recursive: true
|
|
12870
|
+
});
|
|
12871
|
+
await writeFile17(join22(staging, UPDATE_MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
|
|
12872
|
+
`);
|
|
12873
|
+
await rename15(staging, outputDirectory);
|
|
12874
|
+
} catch (error) {
|
|
12875
|
+
await rm11(staging, { force: true, recursive: true });
|
|
12876
|
+
if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "EEXIST")
|
|
12877
|
+
throw new TypeError(`Mobile update ${manifest.releaseId} already exists.`, { cause: error });
|
|
12878
|
+
throw error;
|
|
12879
|
+
}
|
|
12880
|
+
return {
|
|
12881
|
+
manifest,
|
|
12882
|
+
manifestPath: join22(outputDirectory, UPDATE_MANIFEST_FILE),
|
|
12883
|
+
outputDirectory
|
|
12884
|
+
};
|
|
12885
|
+
};
|
|
12886
|
+
var copyAbsoluteMobileUpdateFile = async (updateDirectory, path, destination) => {
|
|
12887
|
+
const source = resolve17(updateDirectory, UPDATE_FILES_DIRECTORY, path);
|
|
12888
|
+
const root = resolve17(updateDirectory, UPDATE_FILES_DIRECTORY);
|
|
12889
|
+
if (!source.startsWith(`${root}/`))
|
|
12890
|
+
throw new TypeError("Update file escaped its release.");
|
|
12891
|
+
await mkdir13(dirname14(destination), { recursive: true });
|
|
12892
|
+
await cp3(source, destination, { force: true });
|
|
12893
|
+
};
|
|
12894
|
+
var readAbsoluteMobileUpdate = async (directory) => parseAbsoluteMobileUpdateManifest(JSON.parse(await readFile20(join22(resolve17(directory), UPDATE_MANIFEST_FILE), "utf8")));
|
|
12895
|
+
var verifyAbsoluteMobileUpdateSignature = (manifestValue, publicKey) => {
|
|
12896
|
+
const manifest = parseAbsoluteMobileUpdateManifest(manifestValue);
|
|
12897
|
+
const valid = verify("sha256", absoluteMobileUpdateSigningPayload(unsignedAbsoluteMobileUpdate(manifest)), { dsaEncoding: "ieee-p1363", key: publicKey }, Buffer.from(manifest.signature.value, "base64"));
|
|
12898
|
+
if (!valid)
|
|
12899
|
+
throw new TypeError("Mobile update signature verification failed.");
|
|
12900
|
+
return manifest;
|
|
12901
|
+
};
|
|
12902
|
+
|
|
12903
|
+
// src/mobile/updatePublisher.ts
|
|
12904
|
+
var object4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12905
|
+
var isPublisher2 = (value) => object4(value) && typeof value.publishUpdate === "function" && typeof value.promoteUpdate === "function" && typeof value.rollbackUpdate === "function";
|
|
12906
|
+
var projectPath2 = (projectRoot, requested, label) => {
|
|
12907
|
+
const root = resolve18(projectRoot);
|
|
12908
|
+
const path = resolve18(root, requested);
|
|
12909
|
+
const projectRelative = relative15(root, path);
|
|
12910
|
+
if (projectRelative === ".." || projectRelative.startsWith(`..${sep9}`) || isAbsolute7(projectRelative))
|
|
12911
|
+
throw new TypeError(`${label} must remain inside the project.`);
|
|
12912
|
+
return path;
|
|
12913
|
+
};
|
|
12914
|
+
var loadAbsoluteMobileUpdatePublisher = async (projectRoot, requestedModulePath) => {
|
|
12915
|
+
const modulePath = projectPath2(projectRoot, requestedModulePath, "mobile update registry");
|
|
12916
|
+
await access13(modulePath).catch(() => {
|
|
12917
|
+
throw new TypeError(`Mobile update registry does not exist: ${modulePath}`);
|
|
12918
|
+
});
|
|
12919
|
+
const loaded = await import(pathToFileURL4(modulePath).href);
|
|
12920
|
+
const publisher = object4(loaded) ? loaded.default ?? loaded.registry : undefined;
|
|
12921
|
+
if (!isPublisher2(publisher))
|
|
12922
|
+
throw new TypeError("Mobile update registry must implement publishUpdate, promoteUpdate, and rollbackUpdate.");
|
|
12923
|
+
return publisher;
|
|
12924
|
+
};
|
|
12925
|
+
var promoteAbsoluteMobileUpdate = async (options) => {
|
|
12926
|
+
const result = await options.publisher.promoteUpdate({
|
|
12927
|
+
appId: options.appId,
|
|
12928
|
+
channel: options.channel,
|
|
12929
|
+
releaseId: options.releaseId,
|
|
12930
|
+
rollout: options.rollout,
|
|
12931
|
+
signal: options.signal
|
|
12932
|
+
});
|
|
12933
|
+
if (result.appId !== options.appId || result.channel !== options.channel || result.releaseId !== options.releaseId || result.rollout !== options.rollout || result.stage !== "promoted")
|
|
12934
|
+
throw new TypeError("Mobile update registry returned a different promotion identity.");
|
|
12935
|
+
return result;
|
|
12936
|
+
};
|
|
12937
|
+
var publishAbsoluteMobileUpdate = async (options) => {
|
|
12938
|
+
const releaseDirectory = projectPath2(options.projectRoot, options.releaseDirectory, "mobile update release directory");
|
|
12939
|
+
const manifest = await readAbsoluteMobileUpdate(releaseDirectory);
|
|
12940
|
+
const result = await options.publisher.publishUpdate({
|
|
12941
|
+
manifest,
|
|
12942
|
+
releaseDirectory,
|
|
12943
|
+
rollout: options.rollout,
|
|
12944
|
+
signal: options.signal
|
|
12945
|
+
});
|
|
12946
|
+
if (result.appId !== manifest.appId || result.channel !== manifest.channel || result.releaseId !== manifest.releaseId || result.rollout !== options.rollout || result.stage !== "published" || typeof result.reused !== "boolean")
|
|
12947
|
+
throw new TypeError("Mobile update registry returned a different publication identity.");
|
|
12948
|
+
return result;
|
|
12949
|
+
};
|
|
12950
|
+
var rollbackAbsoluteMobileUpdate = async (options) => {
|
|
12951
|
+
const result = await options.publisher.rollbackUpdate({
|
|
12952
|
+
appId: options.appId,
|
|
12953
|
+
channel: options.channel,
|
|
12954
|
+
...options.releaseId ? { releaseId: options.releaseId } : {},
|
|
12955
|
+
signal: options.signal
|
|
12956
|
+
});
|
|
12957
|
+
if (result.appId !== options.appId || result.channel !== options.channel || result.releaseId !== options.releaseId || result.stage !== "rolled-back")
|
|
12958
|
+
throw new TypeError("Mobile update registry returned a different rollback identity.");
|
|
12959
|
+
return result;
|
|
12960
|
+
};
|
|
12961
|
+
// src/mobile/updateRollout.ts
|
|
12962
|
+
import { createHash as createHash13 } from "crypto";
|
|
12963
|
+
var UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u;
|
|
12964
|
+
var requireHeader = (headers, name) => {
|
|
12965
|
+
const value = headers.get(name);
|
|
12966
|
+
if (!value)
|
|
12967
|
+
throw new TypeError(`Mobile update request is missing ${name}.`);
|
|
12968
|
+
return value;
|
|
12969
|
+
};
|
|
12970
|
+
var isAbsoluteMobileUpdateRolloutMember = (options) => {
|
|
12971
|
+
if (!Number.isFinite(options.rollout) || options.rollout < 0 || options.rollout > 1)
|
|
12972
|
+
throw new TypeError("Mobile update rollout must be between 0 and 1.");
|
|
12973
|
+
if (options.rollout === 0)
|
|
12974
|
+
return false;
|
|
12975
|
+
if (options.rollout === 1)
|
|
12976
|
+
return true;
|
|
12977
|
+
const digest = createHash13("sha256").update(`${options.appId}\x00${options.channel}\x00${options.releaseId}\x00${options.installationId}`).digest();
|
|
12978
|
+
const bucket = digest.readUInt32BE(0) / 4294967296;
|
|
12979
|
+
return bucket < options.rollout;
|
|
12980
|
+
};
|
|
12981
|
+
var parseAbsoluteMobileUpdateRequest = (request) => {
|
|
12982
|
+
const identity = {
|
|
12983
|
+
appId: requireHeader(request.headers, "x-absolute-mobile-app"),
|
|
12984
|
+
channel: requireHeader(request.headers, "x-absolute-mobile-channel"),
|
|
12985
|
+
currentReleaseId: requireHeader(request.headers, "x-absolute-mobile-release"),
|
|
12986
|
+
installationId: requireHeader(request.headers, "x-absolute-mobile-installation"),
|
|
12987
|
+
runtimeFingerprint: requireHeader(request.headers, "x-absolute-mobile-runtime")
|
|
12988
|
+
};
|
|
12989
|
+
if (!UUID_PATTERN.test(identity.installationId))
|
|
12990
|
+
throw new TypeError("Mobile update installation identity is invalid.");
|
|
12991
|
+
if (!/^[a-f0-9]{64}$/u.test(identity.runtimeFingerprint))
|
|
12992
|
+
throw new TypeError("Mobile update runtime identity is invalid.");
|
|
12993
|
+
return identity;
|
|
12994
|
+
};
|
|
12383
12995
|
export {
|
|
12384
12996
|
ABSOLUTE_ANDROID_RELEASE_FORMAT,
|
|
12385
12997
|
ABSOLUTE_AUTH_PACKAGE,
|
|
@@ -12403,8 +13015,15 @@ export {
|
|
|
12403
13015
|
ABSOLUTE_MOBILE_PREVIEW_PATH,
|
|
12404
13016
|
ABSOLUTE_MOBILE_RETAINED_GENERATIONS,
|
|
12405
13017
|
ABSOLUTE_MOBILE_ROUTE_DETAIL,
|
|
13018
|
+
ABSOLUTE_MOBILE_SHELL_ABI,
|
|
12406
13019
|
ABSOLUTE_MOBILE_SHELL_DEVICE_CAPABILITIES,
|
|
12407
13020
|
ABSOLUTE_MOBILE_TRANSFORM_PROTOCOL,
|
|
13021
|
+
ABSOLUTE_MOBILE_UPDATE_FORMAT,
|
|
13022
|
+
ABSOLUTE_MOBILE_UPDATE_MAX_FILES,
|
|
13023
|
+
ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES,
|
|
13024
|
+
ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES,
|
|
13025
|
+
ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT,
|
|
13026
|
+
ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM,
|
|
12408
13027
|
ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV,
|
|
12409
13028
|
ABSOLUTE_NATIVE_AUTH_SCOPES,
|
|
12410
13029
|
ABSOLUTE_NATIVE_ROUTE_DATA_MEDIA_TYPE,
|
|
@@ -12419,6 +13038,7 @@ export {
|
|
|
12419
13038
|
absoluteExpoExecutable,
|
|
12420
13039
|
absoluteIosDeviceAcceptanceCommands,
|
|
12421
13040
|
absoluteMobilePreviewDocument,
|
|
13041
|
+
absoluteMobileUpdateSigningPayload,
|
|
12422
13042
|
absoluteRemoteMacSshBase,
|
|
12423
13043
|
absoluteRemoteProjectSyncCommands,
|
|
12424
13044
|
absoluteRemoteReleaseInputSyncCommands,
|
|
@@ -12428,16 +13048,20 @@ export {
|
|
|
12428
13048
|
activateAbsoluteMobilePage,
|
|
12429
13049
|
applyAbsoluteNativeDeepLinks,
|
|
12430
13050
|
applyAbsoluteNativeDeviceCapabilities,
|
|
13051
|
+
applyAbsoluteNativeUpdates,
|
|
12431
13052
|
assertAbsoluteDeviceCapabilityPackages,
|
|
12432
13053
|
buildAbsoluteAndroidRelease,
|
|
12433
13054
|
buildAbsoluteIosRelease,
|
|
12434
13055
|
buildAbsoluteMobileCompatibilityRelease,
|
|
13056
|
+
buildAbsoluteMobileUpdate,
|
|
12435
13057
|
buildAbsoluteRemoteIosRelease,
|
|
13058
|
+
canonicalizeAbsoluteMobileUpdate,
|
|
12436
13059
|
captureAbsoluteMobileRouteGraph,
|
|
12437
13060
|
captureAbsoluteRemoteMacCommand,
|
|
12438
13061
|
carryForwardAbsoluteMobileCompatibilityReleases,
|
|
12439
13062
|
cleanAbsoluteRemoteMacWorkspace,
|
|
12440
13063
|
closeAbsoluteMobileSheet,
|
|
13064
|
+
copyAbsoluteMobileUpdateFile,
|
|
12441
13065
|
createAbsoluteExpoBridgeError,
|
|
12442
13066
|
createAbsoluteExpoBridgeResponse,
|
|
12443
13067
|
createAbsoluteIosNativeWatcher,
|
|
@@ -12454,6 +13078,8 @@ export {
|
|
|
12454
13078
|
createAbsoluteMobilePageRequest,
|
|
12455
13079
|
createAbsoluteMobilePreviewPlugin,
|
|
12456
13080
|
createAbsoluteMobileRouteMetadataPlugin,
|
|
13081
|
+
createAbsoluteMobileUpdateClient,
|
|
13082
|
+
createAbsoluteMobileUpdateRuntimeDescriptor,
|
|
12457
13083
|
createAbsoluteMobileUpgradeResponse,
|
|
12458
13084
|
createAbsoluteRemoteExpoIosDevProject,
|
|
12459
13085
|
createAbsoluteRemoteIosDevProject,
|
|
@@ -12466,6 +13092,7 @@ export {
|
|
|
12466
13092
|
finalizeAbsoluteMobilePage,
|
|
12467
13093
|
fingerprintAbsoluteIosDevProject,
|
|
12468
13094
|
fingerprintAbsoluteIosNativeProject,
|
|
13095
|
+
fingerprintAbsoluteMobileUpdateRuntime,
|
|
12469
13096
|
getAbsoluteMobileSyncRemediation,
|
|
12470
13097
|
getAbsoluteRemoteMacProfile,
|
|
12471
13098
|
getCurrentAbsoluteMobileProducerContext,
|
|
@@ -12483,9 +13110,11 @@ export {
|
|
|
12483
13110
|
installAbsoluteMobileUiPrimitives,
|
|
12484
13111
|
installAbsoluteRemoteMacAgent,
|
|
12485
13112
|
isAbsoluteIosNativeRootInput,
|
|
13113
|
+
isAbsoluteMobileUpdateRolloutMember,
|
|
12486
13114
|
listAbsoluteRemoteMacProfiles,
|
|
12487
13115
|
loadAbsoluteDeviceCapabilityProviders,
|
|
12488
13116
|
loadAbsoluteMobileMaterializedBundle,
|
|
13117
|
+
loadAbsoluteMobileUpdatePublisher,
|
|
12489
13118
|
loadAbsoluteNativeReleasePublisher,
|
|
12490
13119
|
matchesAbsoluteMobileRoutePattern,
|
|
12491
13120
|
materializeAbsoluteCapacitorWebBundle,
|
|
@@ -12497,6 +13126,7 @@ export {
|
|
|
12497
13126
|
normalizeAbsoluteIosDeviceHost,
|
|
12498
13127
|
normalizeAbsoluteIosDeviceIdentifier,
|
|
12499
13128
|
normalizeAbsoluteMobileConfig,
|
|
13129
|
+
normalizeAbsoluteMobileUpdatePath,
|
|
12500
13130
|
openAbsoluteMobileSheet,
|
|
12501
13131
|
pairAbsoluteRemoteMac,
|
|
12502
13132
|
parseAbsoluteAndroidInstalledApp,
|
|
@@ -12507,6 +13137,9 @@ export {
|
|
|
12507
13137
|
parseAbsoluteMobileCompatibilityArtifact,
|
|
12508
13138
|
parseAbsoluteMobilePageEnvelope,
|
|
12509
13139
|
parseAbsoluteMobilePageRequest,
|
|
13140
|
+
parseAbsoluteMobileUnsignedUpdateManifest,
|
|
13141
|
+
parseAbsoluteMobileUpdateManifest,
|
|
13142
|
+
parseAbsoluteMobileUpdateRequest,
|
|
12510
13143
|
parseIosDeviceTypes,
|
|
12511
13144
|
parseIosRuntimes,
|
|
12512
13145
|
parseIosSimulators,
|
|
@@ -12517,10 +13150,13 @@ export {
|
|
|
12517
13150
|
projectImportsAbsoluteDeviceCapability,
|
|
12518
13151
|
projectUsesAbsoluteAuth,
|
|
12519
13152
|
projectUsesAbsoluteSync,
|
|
13153
|
+
promoteAbsoluteMobileUpdate,
|
|
12520
13154
|
publishAbsoluteAndroidRelease,
|
|
12521
13155
|
publishAbsoluteIosRelease,
|
|
13156
|
+
publishAbsoluteMobileUpdate,
|
|
12522
13157
|
readAbsoluteMobileLinkIntent,
|
|
12523
13158
|
readAbsoluteMobileMaterializedReleases,
|
|
13159
|
+
readAbsoluteMobileUpdate,
|
|
12524
13160
|
redactAbsoluteIosLog,
|
|
12525
13161
|
removeAbsoluteRemoteMacProfile,
|
|
12526
13162
|
repairAbsoluteIosDevSession,
|
|
@@ -12534,6 +13170,7 @@ export {
|
|
|
12534
13170
|
resolveAbsoluteMobileNavigation,
|
|
12535
13171
|
resolveAbsoluteMobileRoute,
|
|
12536
13172
|
retainAbsoluteMobileCompatibilityArtifacts,
|
|
13173
|
+
rollbackAbsoluteMobileUpdate,
|
|
12537
13174
|
runAbsoluteAndroidUpgradeConformance,
|
|
12538
13175
|
runWithAbsoluteMobileProducer,
|
|
12539
13176
|
serializeAbsoluteMobileAuthEnvironment,
|
|
@@ -12547,15 +13184,17 @@ export {
|
|
|
12547
13184
|
syncAbsoluteRemoteMacProject,
|
|
12548
13185
|
syncAbsoluteRemoteMacReleaseInputs,
|
|
12549
13186
|
testAbsoluteIosPhysicalDevice,
|
|
13187
|
+
unsignedAbsoluteMobileUpdate,
|
|
12550
13188
|
validateAbsoluteRemoteMacProfileName,
|
|
12551
13189
|
validateAbsoluteSshDestination,
|
|
12552
13190
|
verifyAbsoluteMobileAssociationFiles,
|
|
12553
13191
|
verifyAbsoluteMobileCompatibilityProducer,
|
|
13192
|
+
verifyAbsoluteMobileUpdateSignature,
|
|
12554
13193
|
waitForAbsoluteIosHmrLog,
|
|
12555
13194
|
writeAbsoluteCapacitorConfig,
|
|
12556
13195
|
writeAbsoluteExpoProject,
|
|
12557
13196
|
writeAbsoluteMobileGithubWorkflow
|
|
12558
13197
|
};
|
|
12559
13198
|
|
|
12560
|
-
//# debugId=
|
|
13199
|
+
//# debugId=FA0645035026C26B64756E2164756E21
|
|
12561
13200
|
//# sourceMappingURL=index.js.map
|