@absolutejs/absolute 0.20.0-beta.80 → 0.20.0-beta.82

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.
@@ -597,10 +597,14 @@ var init_startupBanner = __esm(() => {
597
597
  });
598
598
 
599
599
  // src/mobile/config.ts
600
+ var exports_config = {};
601
+ __export(exports_config, {
602
+ normalizeAbsoluteMobileConfig: () => normalizeAbsoluteMobileConfig
603
+ });
600
604
  import { readFileSync as readFileSync2 } from "fs";
601
605
  import { resolve as resolve6 } from "path";
602
606
  import { createHash as createHash8, createPublicKey, X509Certificate as X509Certificate2 } from "crypto";
603
- var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
607
+ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
604
608
  const root = resolve6(projectRoot);
605
609
  const path = resolve6(root, value);
606
610
  if (path !== root && !path.startsWith(`${root}/`)) {
@@ -785,6 +789,48 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
785
789
  publicKeys,
786
790
  ...expoCodeSigning ? { expoCodeSigning } : {}
787
791
  };
792
+ }, normalizeUpdateServer = (config, productionOrigin, projectRoot, updates) => {
793
+ if (!updates || !config.updates)
794
+ return;
795
+ const registryModule = requireText(config.updates.server?.registry ?? "mobile.update.ts", "mobile.updates.server.registry");
796
+ resolveProjectPath(projectRoot, registryModule, "mobile.updates.server.registry");
797
+ const expoPrivateKeyEnv = requireText(config.updates.server?.expoPrivateKeyEnv ?? "ABSOLUTE_EXPO_UPDATE_PRIVATE_KEY", "mobile.updates.server.expoPrivateKeyEnv");
798
+ if (!ENVIRONMENT_NAME_PATTERN.test(expoPrivateKeyEnv))
799
+ throw new TypeError("mobile.updates.server.expoPrivateKeyEnv must be a valid environment variable name.");
800
+ const autoMount = config.updates.server?.autoMount ?? true;
801
+ if (autoMount) {
802
+ const manifest = new URL(updates.manifestUrl);
803
+ if (manifest.origin !== productionOrigin)
804
+ throw new TypeError("mobile.updates.manifestUrl must use mobile.server.productionOrigin while mobile.updates.server.autoMount is enabled.");
805
+ if (!manifest.pathname.endsWith("/update.json"))
806
+ throw new TypeError("mobile.updates.manifestUrl must end in /update.json while mobile.updates.server.autoMount is enabled.");
807
+ }
808
+ const expoCodeSigningKeys = {};
809
+ if (updates.expoCodeSigning)
810
+ expoCodeSigningKeys[updates.expoCodeSigning.keyId] = {
811
+ certificatePem: updates.expoCodeSigning.certificatePem,
812
+ privateKeyEnv: expoPrivateKeyEnv
813
+ };
814
+ for (const [keyId, key] of Object.entries(config.updates.server?.expoCodeSigningKeys ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
815
+ if (!updates.expoCodeSigning)
816
+ throw new TypeError("mobile.updates.server.expoCodeSigningKeys requires the Expo engine and expoCodeSigning.");
817
+ if (!UPDATE_NAME_PATTERN.test(keyId))
818
+ throw new TypeError("mobile.updates.server.expoCodeSigningKeys contains an invalid key ID.");
819
+ if (expoCodeSigningKeys[keyId])
820
+ throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} duplicates the active Expo key.`);
821
+ const privateKeyEnv = requireText(key.privateKeyEnv, `mobile.updates.server.expoCodeSigningKeys.${keyId}.privateKeyEnv`);
822
+ if (!ENVIRONMENT_NAME_PATTERN.test(privateKeyEnv))
823
+ throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId}.privateKeyEnv must be a valid environment variable name.`);
824
+ const certificatePath = resolveProjectPath(projectRoot, requireText(key.certificatePath, `mobile.updates.server.expoCodeSigningKeys.${keyId}.certificatePath`), `mobile.updates.server.expoCodeSigningKeys.${keyId}.certificatePath`);
825
+ const { certificate, certificatePem } = readExpoCodeSigningCertificate(certificatePath);
826
+ if (certificate.publicKey.asymmetricKeyType !== "rsa" || certificate.issuer !== certificate.subject || !certificate.verify(certificate.publicKey))
827
+ throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} certificate must be a self-signed RSA root.`);
828
+ const now = Date.now();
829
+ if (now < Date.parse(certificate.validFrom) || now > Date.parse(certificate.validTo))
830
+ throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} certificate is not currently valid.`);
831
+ expoCodeSigningKeys[keyId] = { certificatePem, privateKeyEnv };
832
+ }
833
+ return { autoMount, expoCodeSigningKeys, registryModule };
788
834
  }, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
789
835
  if (segment === "*" && (index !== count - 1 || count === 1)) {
790
836
  throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
@@ -844,6 +890,7 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
844
890
  throw new TypeError("mobile.deepLinks.scheme is not a valid URL scheme.");
845
891
  }
846
892
  const updates = normalizeUpdates(config, productionOrigin, projectRoot);
893
+ const updateServer = normalizeUpdateServer(config, productionOrigin, projectRoot, updates);
847
894
  const observability = normalizeObservability(config, productionOrigin);
848
895
  return {
849
896
  androidCertificateFingerprints: normalizeCertificateFingerprints(config.deepLinks?.android?.sha256CertificateFingerprints),
@@ -863,7 +910,8 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
863
910
  platforms: normalizePlatforms(config.platforms),
864
911
  productionOrigin,
865
912
  pushAndroidGoogleServicesFile: resolveProjectPath(projectRoot, config.pushNotifications?.android?.googleServicesFile ?? "google-services.json", "mobile.pushNotifications.android.googleServicesFile"),
866
- ...updates ? { updates } : {}
913
+ ...updates ? { updates } : {},
914
+ ...updateServer ? { updateServer } : {}
867
915
  };
868
916
  };
869
917
  var init_config = __esm(() => {
@@ -873,6 +921,7 @@ var init_config = __esm(() => {
873
921
  CERTIFICATE_FINGERPRINT_PATTERN = /^[0-9A-F]{64}$/;
874
922
  UPDATE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
875
923
  UPDATE_PUBLIC_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u;
924
+ ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
876
925
  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])?))*$/;
877
926
  EXPO_RESERVED_ROUTE_PREFIXES = new Set([
878
927
  "_expo",
@@ -19902,6 +19951,919 @@ var init_web_vitals_attribution = __esm(() => {
19902
19951
  z = [800, 1800];
19903
19952
  });
19904
19953
 
19954
+ // node_modules/@absolutejs/deploy/dist/mobileUpdate.js
19955
+ var exports_mobileUpdate = {};
19956
+ __export(exports_mobileUpdate, {
19957
+ MOBILE_UPDATE_REGISTRY_FORMAT: () => MOBILE_UPDATE_REGISTRY_FORMAT,
19958
+ MobileUpdateRegistryError: () => MobileUpdateRegistryError,
19959
+ createMobileUpdateHandler: () => createMobileUpdateHandler,
19960
+ createMobileUpdateRegistry: () => createMobileUpdateRegistry,
19961
+ parseMobileUpdateManifest: () => parseMobileUpdateManifest
19962
+ });
19963
+ import {
19964
+ createHash as createHash15,
19965
+ createPrivateKey,
19966
+ createPublicKey as createPublicKey2,
19967
+ sign as sign2,
19968
+ verify as verify2,
19969
+ X509Certificate as X509Certificate3
19970
+ } from "crypto";
19971
+ import { readFile as readFile23, stat as stat5 } from "fs/promises";
19972
+ import path from "path";
19973
+ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", MobileUpdateRegistryError, object6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field2) => {
19974
+ if (typeof value !== "string" || value.length === 0)
19975
+ throw new MobileUpdateRegistryError(`Mobile update ${field2} is invalid`);
19976
+ return value;
19977
+ }, iso = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value, safePath2 = (value) => {
19978
+ const file = text2(value, "file path").replaceAll("\\", "/");
19979
+ if (file.startsWith("/") || file.split("/").some((segment) => !segment || segment === "." || segment === ".."))
19980
+ throw new MobileUpdateRegistryError("Mobile update file path is invalid");
19981
+ return file;
19982
+ }, expoAsset = (value) => {
19983
+ if (!object6(value))
19984
+ throw new MobileUpdateRegistryError("Expo update asset is invalid");
19985
+ const assetPath = safePath2(value.path);
19986
+ if (value.extension !== undefined && (typeof value.extension !== "string" || !/^[A-Za-z0-9]+$/.test(value.extension)))
19987
+ throw new MobileUpdateRegistryError("Expo update asset extension is invalid");
19988
+ return {
19989
+ ...typeof value.extension === "string" ? { extension: value.extension } : {},
19990
+ path: assetPath
19991
+ };
19992
+ }, parseExpoUpdateDescriptor = (bytes) => {
19993
+ let value;
19994
+ try {
19995
+ value = JSON.parse(new TextDecoder().decode(bytes));
19996
+ } catch {
19997
+ throw new MobileUpdateRegistryError("Expo update descriptor is invalid");
19998
+ }
19999
+ if (!object6(value) || value.engine !== "expo" || value.format !== 1 || !object6(value.expoConfig) || !object6(value.platforms) || typeof value.runtimeVersion !== "string" || !HASH.test(value.runtimeVersion))
20000
+ throw new MobileUpdateRegistryError("Expo update descriptor is invalid");
20001
+ const platforms = {};
20002
+ for (const name of ["android", "ios"]) {
20003
+ const candidate = value.platforms[name];
20004
+ if (candidate === undefined)
20005
+ continue;
20006
+ if (!object6(candidate) || !Array.isArray(candidate.assets) || !object6(candidate.launchAsset))
20007
+ throw new MobileUpdateRegistryError("Expo update platform descriptor is invalid");
20008
+ platforms[name] = {
20009
+ assets: candidate.assets.map(expoAsset),
20010
+ launchAsset: expoAsset(candidate.launchAsset)
20011
+ };
20012
+ }
20013
+ if (Object.keys(platforms).length === 0)
20014
+ throw new MobileUpdateRegistryError("Expo update descriptor has no native platforms");
20015
+ return {
20016
+ engine: "expo",
20017
+ expoConfig: value.expoConfig,
20018
+ format: 1,
20019
+ platforms,
20020
+ runtimeVersion: value.runtimeVersion
20021
+ };
20022
+ }, parseMobileUpdateManifest = (value) => {
20023
+ if (!object6(value) || value.format !== 1)
20024
+ throw new MobileUpdateRegistryError("Mobile update manifest is invalid");
20025
+ const appId = text2(value.appId, "appId");
20026
+ const channel = text2(value.channel, "channel");
20027
+ const releaseId = text2(value.releaseId, "releaseId");
20028
+ const runtimeFingerprint = text2(value.runtimeFingerprint, "runtime");
20029
+ if (!APP_ID.test(appId) || !NAME.test(channel) || !RELEASE.test(releaseId))
20030
+ throw new MobileUpdateRegistryError("Mobile update identity is invalid");
20031
+ if (!HASH.test(runtimeFingerprint) || !iso(value.createdAt))
20032
+ throw new MobileUpdateRegistryError("Mobile update runtime or timestamp is invalid");
20033
+ if (value.classification !== "bug-fix" && value.classification !== "content" && value.classification !== "security")
20034
+ throw new MobileUpdateRegistryError("Mobile update classification is invalid");
20035
+ if (value.withinSubmittedPurpose !== true)
20036
+ throw new MobileUpdateRegistryError("Mobile update policy attestation is missing");
20037
+ if (!Array.isArray(value.files) || value.files.length === 0)
20038
+ throw new MobileUpdateRegistryError("Mobile update file inventory is invalid");
20039
+ const files = value.files.map((candidate) => {
20040
+ if (!object6(candidate))
20041
+ throw new MobileUpdateRegistryError("Mobile update file is invalid");
20042
+ const filePath = safePath2(candidate.path);
20043
+ if (!Number.isSafeInteger(candidate.bytes) || Number(candidate.bytes) < 0 || Number(candidate.bytes) > MAX_FILE_BYTES || typeof candidate.sha256 !== "string" || !HASH.test(candidate.sha256))
20044
+ throw new MobileUpdateRegistryError(`Mobile update file ${filePath} is invalid`);
20045
+ return {
20046
+ bytes: Number(candidate.bytes),
20047
+ path: filePath,
20048
+ sha256: candidate.sha256
20049
+ };
20050
+ });
20051
+ if (files.reduce((total, file) => total + file.bytes, 0) > MAX_TOTAL_BYTES || new Set(files.map((file) => file.path)).size !== files.length || files.some((file, index) => index > 0 ? file.path.localeCompare(files[index - 1]?.path ?? "") <= 0 : false))
20052
+ throw new MobileUpdateRegistryError("Mobile update file inventory is invalid");
20053
+ const signatureKeyId = object6(value.signature) ? text2(value.signature.keyId, "signature key") : "";
20054
+ const signatureValue = object6(value.signature) ? text2(value.signature.value, "signature") : "";
20055
+ if (!object6(value.signature) || value.signature.algorithm !== "ecdsa-p256-sha256" || !NAME.test(signatureKeyId) || !/^[A-Za-z0-9+/]+={0,2}$/.test(signatureValue) || Buffer.from(signatureValue, "base64").byteLength !== 64 || Buffer.from(signatureValue, "base64").toString("base64") !== signatureValue)
20056
+ throw new MobileUpdateRegistryError("Mobile update signature is invalid");
20057
+ return {
20058
+ appId,
20059
+ channel,
20060
+ classification: value.classification,
20061
+ createdAt: value.createdAt,
20062
+ files,
20063
+ format: 1,
20064
+ releaseId,
20065
+ runtimeFingerprint,
20066
+ signature: {
20067
+ algorithm: "ecdsa-p256-sha256",
20068
+ keyId: signatureKeyId,
20069
+ value: signatureValue
20070
+ },
20071
+ withinSubmittedPurpose: true
20072
+ };
20073
+ }, canonicalValue2 = (value) => {
20074
+ if (Array.isArray(value))
20075
+ return value.map(canonicalValue2);
20076
+ if (!object6(value))
20077
+ return value;
20078
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue2(value[key])]));
20079
+ }, verifyManifestSignature = (manifest, publicKeys) => {
20080
+ const encoded = publicKeys[manifest.signature.keyId];
20081
+ if (!encoded)
20082
+ throw new MobileUpdateRegistryError("Mobile update signing key is not trusted");
20083
+ let publicKey;
20084
+ try {
20085
+ const der = Buffer.from(encoded, "base64");
20086
+ if (der.toString("base64") !== encoded)
20087
+ throw new Error("invalid base64");
20088
+ publicKey = createPublicKey2({ format: "der", key: der, type: "spki" });
20089
+ } catch {
20090
+ throw new MobileUpdateRegistryError("Mobile update trusted public key is invalid");
20091
+ }
20092
+ if (publicKey.asymmetricKeyType !== "ec" || publicKey.asymmetricKeyDetails?.namedCurve !== "prime256v1")
20093
+ throw new MobileUpdateRegistryError("Mobile update trusted public key must use ECDSA P-256");
20094
+ const { signature: _signature, ...unsigned } = manifest;
20095
+ if (!verify2("sha256", new TextEncoder().encode(JSON.stringify(canonicalValue2(unsigned))), { dsaEncoding: "ieee-p1363", key: publicKey }, Buffer.from(manifest.signature.value, "base64")))
20096
+ throw new MobileUpdateRegistryError("Mobile update signature verification failed");
20097
+ }, parseChannel = (value) => {
20098
+ if (!object6(value) || value.format !== MOBILE_UPDATE_REGISTRY_FORMAT)
20099
+ throw new MobileUpdateRegistryError("Mobile update channel is invalid");
20100
+ const appId = text2(value.appId, "channel appId");
20101
+ const channel = text2(value.channel, "channel");
20102
+ if (!APP_ID.test(appId) || !NAME.test(channel) || !iso(value.promotedAt) || typeof value.rollout !== "number" || value.rollout < 0 || value.rollout > 1 || value.releaseId !== undefined && (typeof value.releaseId !== "string" || !RELEASE.test(value.releaseId)) || value.fallbackReleaseId !== undefined && (typeof value.fallbackReleaseId !== "string" || !RELEASE.test(value.fallbackReleaseId)) || value.activationId !== undefined && (typeof value.activationId !== "string" || !HASH.test(value.activationId)) || value.activatedAt !== undefined && !iso(value.activatedAt) || value.activationId === undefined !== (value.activatedAt === undefined))
20103
+ throw new MobileUpdateRegistryError("Mobile update channel is invalid");
20104
+ return {
20105
+ ...value.activationId ? { activationId: value.activationId } : {},
20106
+ ...value.activatedAt ? { activatedAt: value.activatedAt } : {},
20107
+ appId,
20108
+ channel,
20109
+ ...value.fallbackReleaseId ? { fallbackReleaseId: value.fallbackReleaseId } : {},
20110
+ format: MOBILE_UPDATE_REGISTRY_FORMAT,
20111
+ promotedAt: value.promotedAt,
20112
+ ...value.releaseId ? { releaseId: value.releaseId } : {},
20113
+ rollout: value.rollout
20114
+ };
20115
+ }, normalizedPrefix = (value) => {
20116
+ const prefix = value.replace(/^\/+|\/+$/g, "");
20117
+ if (!prefix || prefix.split("/").some((segment) => segment === "." || segment === ".."))
20118
+ throw new MobileUpdateRegistryError("Mobile update prefix is invalid");
20119
+ return prefix;
20120
+ }, appHash = (appId) => createHash15("sha256").update(appId).digest("hex"), digest = (bytes) => createHash15("sha256").update(bytes).digest("hex"), json = (value) => new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
20121
+ `), decode2 = (value) => JSON.parse(new TextDecoder().decode(value)), fileDigest = async (file) => {
20122
+ const hasher = new Bun.CryptoHasher("sha256");
20123
+ for await (const chunk of file.stream())
20124
+ hasher.update(chunk);
20125
+ return hasher.digest("hex");
20126
+ }, rolloutMember = (input) => {
20127
+ if (input.rollout === 0)
20128
+ return false;
20129
+ if (input.rollout === 1)
20130
+ return true;
20131
+ const value = createHash15("sha256").update(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${input.installationId}`).digest().readUInt32BE(0);
20132
+ return value / 4294967296 < input.rollout;
20133
+ }, createMobileUpdateRegistry = (options) => {
20134
+ const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
20135
+ const clock = options.clock ?? (() => new Date);
20136
+ const root = (appId) => `${prefix}/${appHash(appId)}`;
20137
+ const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
20138
+ const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
20139
+ const fileKey = (manifest, file) => `${releaseRoot(manifest)}/files/${file.path}`;
20140
+ const channelKey = (appId, channel) => {
20141
+ if (!APP_ID.test(appId) || !NAME.test(channel))
20142
+ throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
20143
+ return `${root(appId)}/channels/${channel}.json`;
20144
+ };
20145
+ const readManifest = async (appId, releaseId) => {
20146
+ const key = manifestKey({ appId, releaseId });
20147
+ const bytes = await options.store.get(key);
20148
+ if (!bytes)
20149
+ return null;
20150
+ const head = await options.store.head(key);
20151
+ if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
20152
+ throw new MobileUpdateRegistryError("Stored mobile update manifest integrity failed");
20153
+ const manifest = parseMobileUpdateManifest(decode2(bytes));
20154
+ verifyManifestSignature(manifest, options.publicKeys);
20155
+ if (manifest.appId !== appId || manifest.releaseId !== releaseId)
20156
+ throw new MobileUpdateRegistryError("Stored mobile update identity changed");
20157
+ return { key, manifest };
20158
+ };
20159
+ const readChannel = async (appId, channel) => {
20160
+ const bytes = await options.store.get(channelKey(appId, channel));
20161
+ if (!bytes)
20162
+ return null;
20163
+ const value = parseChannel(decode2(bytes));
20164
+ if (value.appId !== appId || value.channel !== channel)
20165
+ throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
20166
+ return value;
20167
+ };
20168
+ const writeChannel = async (input, signal) => {
20169
+ const value = {
20170
+ ...input,
20171
+ format: MOBILE_UPDATE_REGISTRY_FORMAT,
20172
+ promotedAt: clock().toISOString()
20173
+ };
20174
+ const bytes = json(value);
20175
+ await options.store.put(channelKey(value.appId, value.channel), bytes, {
20176
+ cacheControl: "no-cache",
20177
+ contentType: "application/json",
20178
+ maxBytes: bytes.byteLength,
20179
+ metadata: {
20180
+ channel: value.channel,
20181
+ ...value.releaseId ? { releaseId: value.releaseId } : {},
20182
+ sha256: digest(bytes)
20183
+ },
20184
+ signal
20185
+ });
20186
+ return value;
20187
+ };
20188
+ const promoteUpdate = async (input) => {
20189
+ input.signal?.throwIfAborted();
20190
+ if (input.rollout <= 0 || input.rollout > 1)
20191
+ throw new MobileUpdateRegistryError("Mobile update rollout is invalid");
20192
+ const release = await readManifest(input.appId, input.releaseId);
20193
+ if (!release || release.manifest.channel !== input.channel)
20194
+ throw new MobileUpdateRegistryError("Mobile update was not published to this channel");
20195
+ const existing = await readChannel(input.appId, input.channel);
20196
+ await writeChannel({
20197
+ appId: input.appId,
20198
+ channel: input.channel,
20199
+ ...existing?.releaseId && existing.releaseId !== input.releaseId ? { fallbackReleaseId: existing.releaseId } : existing?.fallbackReleaseId ? { fallbackReleaseId: existing.fallbackReleaseId } : {},
20200
+ releaseId: input.releaseId,
20201
+ rollout: input.rollout
20202
+ }, input.signal);
20203
+ return {
20204
+ appId: input.appId,
20205
+ channel: input.channel,
20206
+ releaseId: input.releaseId,
20207
+ rollout: input.rollout,
20208
+ stage: "promoted"
20209
+ };
20210
+ };
20211
+ const resolveUpdateState = async (input) => {
20212
+ const channel = await readChannel(input.appId, input.channel);
20213
+ if (!channel?.releaseId)
20214
+ return { status: "empty" };
20215
+ const selected = rolloutMember({
20216
+ appId: input.appId,
20217
+ channel: input.channel,
20218
+ installationId: input.installationId,
20219
+ releaseId: channel.releaseId,
20220
+ rollout: channel.rollout
20221
+ }) ? channel.releaseId : channel.fallbackReleaseId;
20222
+ if (!selected)
20223
+ return { status: "empty" };
20224
+ const release = await readManifest(input.appId, selected);
20225
+ if (!release || release.manifest.runtimeFingerprint !== input.runtimeFingerprint)
20226
+ return { status: "incompatible" };
20227
+ return {
20228
+ ...selected === channel.releaseId && channel.activationId && channel.activatedAt ? {
20229
+ activationId: channel.activationId,
20230
+ activatedAt: channel.activatedAt
20231
+ } : {},
20232
+ manifest: release.manifest,
20233
+ manifestKey: release.key,
20234
+ status: "selected"
20235
+ };
20236
+ };
20237
+ return {
20238
+ publishUpdate: async (input) => {
20239
+ input.signal?.throwIfAborted();
20240
+ const manifest = parseMobileUpdateManifest(input.manifest);
20241
+ verifyManifestSignature(manifest, options.publicKeys);
20242
+ const localRoot = path.resolve(input.releaseDirectory);
20243
+ const localManifest = parseMobileUpdateManifest(JSON.parse(await readFile23(path.join(localRoot, "update.json"), "utf8")));
20244
+ if (JSON.stringify(localManifest) !== JSON.stringify(manifest))
20245
+ throw new MobileUpdateRegistryError("Local mobile update manifest changed");
20246
+ const existing = await readManifest(manifest.appId, manifest.releaseId);
20247
+ let reused = existing !== null;
20248
+ if (existing && JSON.stringify(existing.manifest) !== JSON.stringify(manifest))
20249
+ throw new MobileUpdateRegistryError("Published mobile update is immutable");
20250
+ if (!existing) {
20251
+ for (const file of manifest.files) {
20252
+ const local = path.join(localRoot, "files", file.path);
20253
+ const metadata = await stat5(local).catch(() => null);
20254
+ if (!metadata?.isFile() || metadata.size !== file.bytes)
20255
+ throw new MobileUpdateRegistryError(`Mobile update file ${file.path} size changed`);
20256
+ if (await fileDigest(Bun.file(local)) !== file.sha256)
20257
+ throw new MobileUpdateRegistryError(`Mobile update file ${file.path} integrity failed`);
20258
+ const key = fileKey(manifest, file);
20259
+ const stored = await options.store.head(key);
20260
+ if (!stored) {
20261
+ await options.store.put(key, Bun.file(local).stream(), {
20262
+ cacheControl: "public, max-age=31536000, immutable",
20263
+ contentType: "application/octet-stream",
20264
+ maxBytes: file.bytes,
20265
+ metadata: { releaseid: manifest.releaseId, sha256: file.sha256 },
20266
+ signal: input.signal
20267
+ });
20268
+ } else if (stored.size !== file.bytes || stored.metadata?.sha256 !== file.sha256 || (stored.metadata?.releaseid ?? stored.metadata?.releaseId) !== manifest.releaseId)
20269
+ throw new MobileUpdateRegistryError("Stored mobile update file identity changed");
20270
+ }
20271
+ const bytes = json(manifest);
20272
+ await options.store.put(manifestKey(manifest), bytes, {
20273
+ cacheControl: "public, max-age=31536000, immutable",
20274
+ contentType: "application/json",
20275
+ maxBytes: bytes.byteLength,
20276
+ metadata: { releaseid: manifest.releaseId, sha256: digest(bytes) },
20277
+ signal: input.signal
20278
+ });
20279
+ if (!await readManifest(manifest.appId, manifest.releaseId))
20280
+ throw new MobileUpdateRegistryError("Mobile update publication verification failed");
20281
+ reused = false;
20282
+ }
20283
+ await promoteUpdate({
20284
+ appId: manifest.appId,
20285
+ channel: manifest.channel,
20286
+ releaseId: manifest.releaseId,
20287
+ rollout: input.rollout,
20288
+ signal: input.signal
20289
+ });
20290
+ return {
20291
+ appId: manifest.appId,
20292
+ channel: manifest.channel,
20293
+ releaseId: manifest.releaseId,
20294
+ reused,
20295
+ rollout: input.rollout,
20296
+ stage: "published"
20297
+ };
20298
+ },
20299
+ promoteUpdate,
20300
+ rollbackUpdate: async (input) => {
20301
+ input.signal?.throwIfAborted();
20302
+ const existing = await readChannel(input.appId, input.channel);
20303
+ if (!existing)
20304
+ throw new MobileUpdateRegistryError("Mobile update channel does not exist");
20305
+ if (input.releaseId) {
20306
+ const release = await readManifest(input.appId, input.releaseId);
20307
+ if (!release || release.manifest.channel !== input.channel)
20308
+ throw new MobileUpdateRegistryError("Mobile rollback release was not published");
20309
+ }
20310
+ const activatedAt = clock().toISOString();
20311
+ await writeChannel({
20312
+ ...input.releaseId ? {
20313
+ activationId: digest(new TextEncoder().encode(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${activatedAt}`)),
20314
+ activatedAt
20315
+ } : {},
20316
+ appId: input.appId,
20317
+ channel: input.channel,
20318
+ ...existing.releaseId ? { fallbackReleaseId: existing.releaseId } : {},
20319
+ ...input.releaseId ? { releaseId: input.releaseId } : {},
20320
+ rollout: input.releaseId ? 1 : 0
20321
+ }, input.signal);
20322
+ return {
20323
+ appId: input.appId,
20324
+ channel: input.channel,
20325
+ ...input.releaseId ? { releaseId: input.releaseId } : {},
20326
+ stage: "rolled-back"
20327
+ };
20328
+ },
20329
+ resolveUpdate: async (input) => {
20330
+ const resolution = await resolveUpdateState(input);
20331
+ return resolution.status === "selected" ? { manifest: resolution.manifest, manifestKey: resolution.manifestKey } : null;
20332
+ },
20333
+ resolveUpdateState,
20334
+ readUpdateFile: async (input) => {
20335
+ const release = await readManifest(input.appId, input.releaseId);
20336
+ if (!release)
20337
+ return null;
20338
+ const requested = safePath2(input.path);
20339
+ const file = release.manifest.files.find((candidate) => candidate.path === requested);
20340
+ if (!file)
20341
+ return null;
20342
+ const key = fileKey(release.manifest, file);
20343
+ const [bytes, head] = await Promise.all([
20344
+ options.store.get(key),
20345
+ options.store.head(key)
20346
+ ]);
20347
+ if (!bytes || !head)
20348
+ return null;
20349
+ if (bytes.byteLength !== file.bytes || head.size !== file.bytes || head.metadata?.sha256 !== file.sha256 || (head.metadata?.releaseid ?? head.metadata?.releaseId) !== release.manifest.releaseId || digest(bytes) !== file.sha256)
20350
+ throw new MobileUpdateRegistryError("Stored mobile update file integrity failed");
20351
+ return { bytes, file };
20352
+ }
20353
+ };
20354
+ }, expoUpdateId = (releaseId) => {
20355
+ const hash = RELEASE.test(releaseId) ? releaseId.slice("amu_".length, "amu_".length + 32) : createHash15("sha256").update(releaseId).digest("hex").slice(0, 32);
20356
+ return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20)}`;
20357
+ }, expoContentType = (extension2, launch) => {
20358
+ if (launch)
20359
+ return "application/javascript";
20360
+ const normalized = extension2?.toLowerCase();
20361
+ if (normalized === "png")
20362
+ return "image/png";
20363
+ if (normalized === "jpg" || normalized === "jpeg")
20364
+ return "image/jpeg";
20365
+ if (normalized === "webp")
20366
+ return "image/webp";
20367
+ if (normalized === "gif")
20368
+ return "image/gif";
20369
+ if (normalized === "svg")
20370
+ return "image/svg+xml";
20371
+ if (normalized === "json")
20372
+ return "application/json";
20373
+ if (normalized === "ttf")
20374
+ return "font/ttf";
20375
+ if (normalized === "otf")
20376
+ return "font/otf";
20377
+ if (normalized === "woff")
20378
+ return "font/woff";
20379
+ if (normalized === "woff2")
20380
+ return "font/woff2";
20381
+ return "application/octet-stream";
20382
+ }, encodeUpdatePath = (value) => value.split("/").map(encodeURIComponent).join("/"), expoProtocolHeaders, resolveExpoCodeSigning = (value) => {
20383
+ if (!value)
20384
+ return;
20385
+ const entries = Object.entries(value.keys);
20386
+ if (entries.length === 0)
20387
+ throw new MobileUpdateRegistryError("Expo code signing requires at least one key");
20388
+ return new Map(entries.map(([keyId, material]) => {
20389
+ if (!NAME.test(keyId))
20390
+ throw new MobileUpdateRegistryError("Expo code-signing key ID is invalid");
20391
+ let certificate;
20392
+ let privateKey;
20393
+ try {
20394
+ certificate = new X509Certificate3(material.certificate);
20395
+ privateKey = createPrivateKey(material.privateKey);
20396
+ } catch (error) {
20397
+ throw new MobileUpdateRegistryError("Expo code-signing certificate or private key is invalid", { cause: error });
20398
+ }
20399
+ if (certificate.publicKey.asymmetricKeyType !== "rsa" || privateKey.asymmetricKeyType !== "rsa" || certificate.issuer !== certificate.subject || !certificate.verify(certificate.publicKey))
20400
+ throw new MobileUpdateRegistryError("Expo code signing requires a self-signed RSA root and private key");
20401
+ const certificatePublicKey = certificate.publicKey.export({
20402
+ format: "der",
20403
+ type: "spki"
20404
+ });
20405
+ const privatePublicKey = createPublicKey2(privateKey).export({
20406
+ format: "der",
20407
+ type: "spki"
20408
+ });
20409
+ if (!certificatePublicKey.equals(privatePublicKey))
20410
+ throw new MobileUpdateRegistryError("Expo code-signing private key does not match its certificate");
20411
+ const now = Date.now();
20412
+ if (now < Date.parse(certificate.validFrom) || now > Date.parse(certificate.validTo))
20413
+ throw new MobileUpdateRegistryError("Expo code-signing certificate is not currently valid");
20414
+ return [keyId, { keyId, privateKey }];
20415
+ }));
20416
+ }, expoSignatureExpectation = (value) => {
20417
+ const fields = new Map;
20418
+ for (const raw of value.split(",")) {
20419
+ const match = /^\s*([a-z][a-z0-9_-]*)(?:=(\?1|\?0|"[^"\\]*"|[a-z0-9._-]+))?\s*$/u.exec(raw);
20420
+ if (!match || fields.has(match[1]))
20421
+ throw new MobileUpdateRegistryError("Expo code-signing expectation is invalid");
20422
+ const encoded = match[2];
20423
+ fields.set(match[1], encoded === undefined || encoded === "?1" ? true : encoded.startsWith('"') ? encoded.slice(1, -1) : encoded);
20424
+ }
20425
+ return fields;
20426
+ }, expoExtraParam = (request, key) => {
20427
+ const value = request.headers.get("expo-extra-params");
20428
+ if (!value)
20429
+ return null;
20430
+ for (const raw of value.split(",")) {
20431
+ const match = /^\s*([a-z][a-z0-9_.*-]*)=(?:"((?:[^"\\]|\\["\\])*)"|([^\s,]+))\s*$/u.exec(raw);
20432
+ if (!match || match[1] !== key)
20433
+ continue;
20434
+ return match[2] !== undefined ? match[2].replace(/\\(["\\])/gu, "$1") : match[3] ?? null;
20435
+ }
20436
+ return null;
20437
+ }, requestedExpoSignature = (request, codeSigning) => {
20438
+ const expectation = request.headers.get("expo-expect-signature");
20439
+ if (!expectation)
20440
+ return;
20441
+ if (!codeSigning)
20442
+ throw new MobileUpdateRegistryError("Expo end-to-end code signing is not configured");
20443
+ const fields = expoSignatureExpectation(expectation);
20444
+ const keyId = fields.get("keyid");
20445
+ const algorithm = fields.get("alg");
20446
+ const signatureRequested = fields.get("sig") === true;
20447
+ if (!signatureRequested || keyId !== undefined && typeof keyId !== "string" || algorithm !== undefined && algorithm !== EXPO_CODE_SIGNING_ALGORITHM)
20448
+ throw new MobileUpdateRegistryError("Requested Expo code-signing parameters are unsupported");
20449
+ const selected = typeof keyId === "string" ? codeSigning.get(keyId) : codeSigning.size === 1 ? codeSigning.values().next().value : undefined;
20450
+ if (!selected)
20451
+ throw new MobileUpdateRegistryError("Requested Expo code-signing key is unavailable");
20452
+ return selected;
20453
+ }, expoSignatureHeader = (body, codeSigning) => {
20454
+ const signature = sign2("RSA-SHA256", Buffer.from(body), codeSigning.privateKey);
20455
+ return `sig="${signature.toString("base64")}", keyid="${codeSigning.keyId}", alg="${EXPO_CODE_SIGNING_ALGORITHM}"`;
20456
+ }, expoRollbackResponse = (request, codeSigning) => {
20457
+ const current = request.headers.get("expo-current-update-id");
20458
+ const embedded = request.headers.get("expo-embedded-update-id");
20459
+ if (!current || current === embedded)
20460
+ return new Response(null, { status: 204 });
20461
+ const boundary = `absolutejs-${crypto.randomUUID()}`;
20462
+ const directive = JSON.stringify({
20463
+ parameters: { commitTime: new Date().toISOString() },
20464
+ type: "rollBackToEmbedded"
20465
+ });
20466
+ const body = [
20467
+ `--${boundary}`,
20468
+ 'content-disposition: form-data; name="directive"',
20469
+ "content-type: application/json; charset=utf-8",
20470
+ ...codeSigning ? [`expo-signature: ${expoSignatureHeader(directive, codeSigning)}`] : [],
20471
+ "",
20472
+ directive,
20473
+ `--${boundary}--`,
20474
+ ""
20475
+ ].join(`\r
20476
+ `);
20477
+ return new Response(body, {
20478
+ headers: {
20479
+ ...expoProtocolHeaders,
20480
+ "content-type": `multipart/mixed; boundary=${boundary}`
20481
+ }
20482
+ });
20483
+ }, createMobileUpdateHandler = (options) => {
20484
+ const route = (options.route ?? `/__absolute/mobile/updates/${options.channel}`).replace(/^\/+|\/+$/g, "");
20485
+ const allowedOrigins = new Set(options.allowedOrigins ?? ["capacitor://localhost", "https://localhost"]);
20486
+ const expoCodeSigning = resolveExpoCodeSigning(options.expoCodeSigning);
20487
+ return async (request) => {
20488
+ const origin = request.headers.get("origin");
20489
+ const cors = origin && allowedOrigins.has(origin) ? { "access-control-allow-origin": origin, vary: "Origin" } : {};
20490
+ if (request.method === "OPTIONS") {
20491
+ if (!origin || !allowedOrigins.has(origin))
20492
+ return new Response(null, { status: 403 });
20493
+ return new Response(null, {
20494
+ headers: {
20495
+ ...cors,
20496
+ "access-control-allow-headers": "x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
20497
+ "access-control-allow-methods": "GET,OPTIONS",
20498
+ "access-control-max-age": "600"
20499
+ },
20500
+ status: 204
20501
+ });
20502
+ }
20503
+ if (request.method !== "GET")
20504
+ return new Response(null, { status: 405 });
20505
+ const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
20506
+ const relative17 = pathname.startsWith(`${route}/`) ? pathname.slice(route.length + 1) : "";
20507
+ if (relative17 === "update.json") {
20508
+ const expoProtocolVersion = request.headers.get("expo-protocol-version");
20509
+ const expoProtocol = expoProtocolVersion !== null;
20510
+ if (expoProtocol && expoProtocolVersion !== "1")
20511
+ return Response.json({
20512
+ error: `Unsupported Expo Updates protocol version: ${expoProtocolVersion}`
20513
+ }, { status: 406 });
20514
+ const appId = request.headers.get("x-absolute-mobile-app");
20515
+ const channel = request.headers.get("x-absolute-mobile-channel");
20516
+ const installationId = expoProtocol ? expoExtraParam(request, "absolute-installation") ?? request.headers.get("x-absolute-mobile-installation") : request.headers.get("x-absolute-mobile-installation");
20517
+ const runtimeFingerprint = expoProtocol ? request.headers.get("expo-runtime-version") : request.headers.get("x-absolute-mobile-runtime");
20518
+ if (appId !== options.appId || channel !== options.channel || !installationId || !runtimeFingerprint)
20519
+ return new Response(null, { status: 400 });
20520
+ const resolution = options.registry.resolveUpdateState ? await options.registry.resolveUpdateState({
20521
+ appId,
20522
+ channel,
20523
+ installationId,
20524
+ runtimeFingerprint
20525
+ }) : undefined;
20526
+ const selected = resolution ? resolution.status === "selected" ? resolution : null : await options.registry.resolveUpdate({
20527
+ appId,
20528
+ channel,
20529
+ installationId,
20530
+ runtimeFingerprint
20531
+ });
20532
+ if (expoProtocol) {
20533
+ let requestedCodeSigning;
20534
+ try {
20535
+ requestedCodeSigning = requestedExpoSignature(request, expoCodeSigning);
20536
+ } catch (error) {
20537
+ return Response.json({
20538
+ error: error instanceof Error ? error.message : "Expo code-signing negotiation failed"
20539
+ }, { status: expoCodeSigning ? 406 : 400 });
20540
+ }
20541
+ if (!selected) {
20542
+ if (resolution?.status === "incompatible")
20543
+ return new Response(null, { status: 204 });
20544
+ return expoRollbackResponse(request, requestedCodeSigning);
20545
+ }
20546
+ const platform5 = request.headers.get("expo-platform");
20547
+ if (platform5 !== "android" && platform5 !== "ios")
20548
+ return new Response(null, { status: 400 });
20549
+ const updateId = expoUpdateId((resolution?.status === "selected" ? resolution.activationId : undefined) ?? selected.manifest.releaseId);
20550
+ if (request.headers.get("expo-current-update-id") === updateId)
20551
+ return new Response(null, { status: 204 });
20552
+ const descriptorFile = await options.registry.readUpdateFile({
20553
+ appId,
20554
+ path: EXPO_DESCRIPTOR,
20555
+ releaseId: selected.manifest.releaseId
20556
+ });
20557
+ if (!descriptorFile)
20558
+ return new Response(null, { status: 409 });
20559
+ const descriptor = parseExpoUpdateDescriptor(descriptorFile.bytes);
20560
+ const platformUpdate = descriptor.platforms[platform5];
20561
+ if (!platformUpdate || descriptor.runtimeVersion !== runtimeFingerprint)
20562
+ return new Response(null, { status: 204 });
20563
+ const origin2 = new URL(request.url).origin;
20564
+ const releaseRoute = `${origin2}/${route}/${selected.manifest.releaseId}/files`;
20565
+ const protocolAsset = (asset2, launch = false) => {
20566
+ const file2 = selected.manifest.files.find((candidate) => candidate.path === asset2.path);
20567
+ if (!file2)
20568
+ throw new MobileUpdateRegistryError(`Expo update references missing signed asset ${asset2.path}`);
20569
+ return {
20570
+ contentType: expoContentType(asset2.extension, launch),
20571
+ ...asset2.extension ? { fileExtension: `.${asset2.extension}` } : {},
20572
+ key: file2.sha256,
20573
+ hash: Buffer.from(file2.sha256, "hex").toString("base64url"),
20574
+ url: `${releaseRoute}/${encodeUpdatePath(asset2.path)}`
20575
+ };
20576
+ };
20577
+ const manifest = JSON.stringify({
20578
+ assets: platformUpdate.assets.map((asset2) => protocolAsset(asset2)),
20579
+ createdAt: (resolution?.status === "selected" ? resolution.activatedAt : undefined) ?? selected.manifest.createdAt,
20580
+ extra: {
20581
+ absolutejs: {
20582
+ channel: selected.manifest.channel,
20583
+ releaseId: selected.manifest.releaseId
20584
+ },
20585
+ expoClient: descriptor.expoConfig
20586
+ },
20587
+ id: updateId,
20588
+ launchAsset: protocolAsset(platformUpdate.launchAsset, true),
20589
+ metadata: {
20590
+ channel: selected.manifest.channel,
20591
+ releaseId: selected.manifest.releaseId
20592
+ },
20593
+ runtimeVersion: descriptor.runtimeVersion
20594
+ });
20595
+ return new Response(manifest, {
20596
+ headers: {
20597
+ ...expoProtocolHeaders,
20598
+ ...requestedCodeSigning ? {
20599
+ "expo-signature": expoSignatureHeader(manifest, requestedCodeSigning)
20600
+ } : {}
20601
+ }
20602
+ });
20603
+ }
20604
+ if (!selected)
20605
+ return new Response(null, { status: 204 });
20606
+ return Response.json(selected.manifest, {
20607
+ headers: {
20608
+ ...cors,
20609
+ "cache-control": "no-store",
20610
+ etag: `"${selected.manifest.releaseId}"`
20611
+ }
20612
+ });
20613
+ }
20614
+ const match = /^(amu_[a-f0-9]{64})\/files\/(.+)$/.exec(relative17);
20615
+ if (!match?.[1] || !match[2])
20616
+ return new Response(null, { status: 404 });
20617
+ const file = await options.registry.readUpdateFile({
20618
+ appId: options.appId,
20619
+ path: decodeURIComponent(match[2]),
20620
+ releaseId: match[1]
20621
+ });
20622
+ if (!file)
20623
+ return new Response(null, { status: 404 });
20624
+ return new Response(new Blob([new Uint8Array(file.bytes).buffer]), {
20625
+ headers: {
20626
+ ...cors,
20627
+ "cache-control": "public, max-age=31536000, immutable",
20628
+ "content-length": String(file.file.bytes),
20629
+ "content-type": expoContentType(file.file.path.includes(".") ? file.file.path.slice(file.file.path.lastIndexOf(".") + 1) : undefined, false),
20630
+ etag: `"${file.file.sha256}"`
20631
+ }
20632
+ });
20633
+ };
20634
+ };
20635
+ var init_mobileUpdate = __esm(() => {
20636
+ MAX_FILE_BYTES = 32 * 1024 * 1024;
20637
+ MAX_TOTAL_BYTES = 128 * 1024 * 1024;
20638
+ HASH = /^[a-f0-9]{64}$/;
20639
+ RELEASE = /^amu_[a-f0-9]{64}$/;
20640
+ APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
20641
+ NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
20642
+ MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
20643
+ };
20644
+ expoProtocolHeaders = {
20645
+ "cache-control": "private, max-age=0",
20646
+ "content-type": "application/expo+json",
20647
+ "expo-protocol-version": "1",
20648
+ "expo-sfv-version": "0"
20649
+ };
20650
+ });
20651
+
20652
+ // src/mobile/updateServer.ts
20653
+ var exports_updateServer = {};
20654
+ __export(exports_updateServer, {
20655
+ ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT: () => ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT,
20656
+ DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE: () => DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE,
20657
+ createAbsoluteMobileUpdateServerPlugin: () => createAbsoluteMobileUpdateServerPlugin,
20658
+ inspectAbsoluteMobileUpdateServer: () => inspectAbsoluteMobileUpdateServer,
20659
+ loadAbsoluteMobileUpdateServerModule: () => loadAbsoluteMobileUpdateServerModule,
20660
+ renderAbsoluteMobileUpdateRegistry: () => renderAbsoluteMobileUpdateRegistry,
20661
+ writeAbsoluteMobileUpdateRegistry: () => writeAbsoluteMobileUpdateRegistry
20662
+ });
20663
+ import {
20664
+ createPrivateKey as createPrivateKey2,
20665
+ createPublicKey as createPublicKey3,
20666
+ X509Certificate as X509Certificate4
20667
+ } from "crypto";
20668
+ import { access as access16, mkdir as mkdir18 } from "fs/promises";
20669
+ import { dirname as dirname19, isAbsolute as isAbsolute9, relative as relative17, resolve as resolve20, sep as sep11 } from "path";
20670
+ import { pathToFileURL as pathToFileURL5 } from "url";
20671
+ import { Elysia as Elysia4 } from "elysia";
20672
+ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE = "mobile.update.ts", object7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), projectPath3 = (projectRoot, requested) => {
20673
+ const root = resolve20(projectRoot);
20674
+ const path2 = resolve20(root, requested);
20675
+ const projectRelative = relative17(root, path2);
20676
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep11}`) || isAbsolute9(projectRelative))
20677
+ throw new TypeError("mobile.updates.server.registry must remain inside the project.");
20678
+ return path2;
20679
+ }, isRegistry4 = (value) => object7(value) && [
20680
+ "publishUpdate",
20681
+ "promoteUpdate",
20682
+ "rollbackUpdate",
20683
+ "resolveUpdate",
20684
+ "readUpdateFile"
20685
+ ].every((method) => typeof value[method] === "function"), serverMetadata = (value) => {
20686
+ if (!object7(value))
20687
+ throw new TypeError("Mobile update registry must export valid absoluteMobileUpdateServer metadata. Run `absolute mobile update provision`.");
20688
+ const { format, provider, storage } = value;
20689
+ if (format !== ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT || storage !== "local" && storage !== "durable" || typeof provider !== "string" || !provider)
20690
+ throw new TypeError("Mobile update registry must export valid absoluteMobileUpdateServer metadata. Run `absolute mobile update provision`.");
20691
+ const metadata = {
20692
+ format: ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT,
20693
+ provider,
20694
+ storage
20695
+ };
20696
+ return metadata;
20697
+ }, loadAbsoluteMobileUpdateServerModule = async (projectRoot, requestedModulePath = DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE) => {
20698
+ const modulePath = projectPath3(projectRoot, requestedModulePath);
20699
+ await access16(modulePath).catch(() => {
20700
+ throw new TypeError(`Mobile update server registry does not exist: ${modulePath}. Run \`absolute mobile update provision\`.`);
20701
+ });
20702
+ const loaded = await import(pathToFileURL5(modulePath).href);
20703
+ if (!object7(loaded))
20704
+ throw new TypeError("Mobile update registry module has no exports.");
20705
+ const registry4 = loaded.default ?? loaded.registry;
20706
+ if (!isRegistry4(registry4))
20707
+ throw new TypeError("Mobile update registry must implement publication, promotion, rollback, resolution, and file reads.");
20708
+ const metadata = serverMetadata(loaded.absoluteMobileUpdateServer);
20709
+ const verifier = loaded.verifyAbsoluteMobileUpdateServer;
20710
+ if (metadata.storage === "durable" && typeof verifier !== "function")
20711
+ throw new TypeError("Mobile update registries marked durable must export verifyAbsoluteMobileUpdateServer(). Re-run `absolute mobile update provision --storage s3 --force` or provide an active durability check.");
20712
+ return {
20713
+ metadata,
20714
+ registry: registry4,
20715
+ ...typeof verifier === "function" ? {
20716
+ verifyDurability: async () => {
20717
+ await verifier();
20718
+ }
20719
+ } : {}
20720
+ };
20721
+ }, verifyDurableModule = async (module) => {
20722
+ if (module.metadata.storage !== "durable")
20723
+ throw new TypeError("Mobile production updates require durable object storage. Re-run `absolute mobile update provision --storage s3 --force` or configure a durable adapter.");
20724
+ try {
20725
+ await module.verifyDurability?.();
20726
+ } catch (error) {
20727
+ throw new TypeError(`Durable mobile update storage verification failed for ${module.metadata.provider}. Check the bucket, endpoint, credentials, and read/write/delete permissions.`, { cause: error });
20728
+ }
20729
+ }, expoSigningOptions = (config) => {
20730
+ if (!config.updates?.expoCodeSigning)
20731
+ return;
20732
+ const entries = Object.entries(config.updateServer?.expoCodeSigningKeys ?? {});
20733
+ const keys = Object.fromEntries(entries.map(([keyId, key]) => {
20734
+ const privateKey = process.env[key.privateKeyEnv];
20735
+ if (!privateKey)
20736
+ throw new TypeError(`Expo update serving requires ${key.privateKeyEnv} on the trusted server.`);
20737
+ try {
20738
+ const certificate = new X509Certificate4(key.certificatePem);
20739
+ const expected = certificate.publicKey.export({
20740
+ format: "der",
20741
+ type: "spki"
20742
+ });
20743
+ const actual = createPublicKey3(createPrivateKey2(privateKey)).export({
20744
+ format: "der",
20745
+ type: "spki"
20746
+ });
20747
+ if (!expected.equals(actual))
20748
+ throw new Error("key mismatch");
20749
+ } catch (error) {
20750
+ throw new TypeError(`${key.privateKeyEnv} must contain the RSA private key matching Expo update key ${keyId}.`, { cause: error });
20751
+ }
20752
+ return [keyId, { certificate: key.certificatePem, privateKey }];
20753
+ }));
20754
+ return { keys };
20755
+ }, createAbsoluteMobileUpdateServerPlugin = async (config, projectRoot, options = {}) => {
20756
+ const { updates, updateServer: server } = config;
20757
+ if (!updates || !server?.autoMount)
20758
+ return new Elysia4({ name: "absolutejs-mobile-updates-disabled" });
20759
+ const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
20760
+ if (options.production)
20761
+ await verifyDurableModule(module);
20762
+ const manifest = new URL(updates.manifestUrl);
20763
+ if (!manifest.pathname.endsWith("/update.json"))
20764
+ throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
20765
+ const route = manifest.pathname.slice(0, -"/update.json".length);
20766
+ const { createMobileUpdateHandler: createMobileUpdateHandler2 } = await Promise.resolve().then(() => (init_mobileUpdate(), exports_mobileUpdate));
20767
+ const handler = createMobileUpdateHandler2({
20768
+ appId: config.appId,
20769
+ channel: updates.channel,
20770
+ ...config.engine === "expo" ? { expoCodeSigning: expoSigningOptions(config) } : {},
20771
+ registry: module.registry,
20772
+ route
20773
+ });
20774
+ return new Elysia4({ name: "absolutejs-mobile-updates" }).all(`${route}/*`, ({ request }) => handler(request));
20775
+ }, inspectAbsoluteMobileUpdateServer = async (config, projectRoot) => {
20776
+ if (!config.updates)
20777
+ return;
20778
+ const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
20779
+ await verifyDurableModule(module);
20780
+ if (config.engine === "expo")
20781
+ expoSigningOptions(config);
20782
+ return module.metadata;
20783
+ }, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistry = (options) => {
20784
+ const metadata = `export const absoluteMobileUpdateServer = {
20785
+ format: 1,
20786
+ provider: '${options.storage}',
20787
+ storage: '${options.storage === "local" ? "local" : "durable"}'
20788
+ } as const;`;
20789
+ if (options.storage === "local")
20790
+ return `import { fileURLToPath } from 'node:url';
20791
+ import { localBlobStore } from '@absolutejs/blob/local';
20792
+ import { createMobileUpdateRegistry } from '@absolutejs/deploy/mobile-update';
20793
+
20794
+ ${metadata}
20795
+
20796
+ const store = localBlobStore({
20797
+ root: process.env.ABSOLUTE_MOBILE_UPDATE_LOCAL_ROOT ??
20798
+ fileURLToPath(new URL('./.absolutejs/mobile/update-registry/', import.meta.url))
20799
+ });
20800
+
20801
+ export default createMobileUpdateRegistry({
20802
+ publicKeys: ${publicKeysSource(options.publicKeys)},
20803
+ store
20804
+ });
20805
+ `;
20806
+ return `import { randomUUID } from 'node:crypto';
20807
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
20808
+ import { awsS3BlobStore } from '@absolutejs/blob/aws-s3';
20809
+ import { createMobileUpdateRegistry } from '@absolutejs/deploy/mobile-update';
20810
+
20811
+ ${metadata}
20812
+
20813
+ const required = (name: string) => {
20814
+ const value = process.env[name];
20815
+ if (!value) throw new Error(\`Missing \${name}\`);
20816
+ return value;
20817
+ };
20818
+
20819
+ const bucket = required('ABSOLUTE_MOBILE_UPDATE_S3_BUCKET');
20820
+ const client = new S3Client({
20821
+ region: process.env.ABSOLUTE_MOBILE_UPDATE_S3_REGION ?? 'auto',
20822
+ forcePathStyle: process.env.ABSOLUTE_MOBILE_UPDATE_S3_FORCE_PATH_STYLE === '1',
20823
+ ...(process.env.ABSOLUTE_MOBILE_UPDATE_S3_ENDPOINT
20824
+ ? { endpoint: process.env.ABSOLUTE_MOBILE_UPDATE_S3_ENDPOINT }
20825
+ : {})
20826
+ });
20827
+ const store = awsS3BlobStore({ bucket, client });
20828
+
20829
+ export const verifyAbsoluteMobileUpdateServer = async () => {
20830
+ const key = \`absolutejs/mobile-updates/_health/\${randomUUID()}\`;
20831
+ const expected = randomUUID();
20832
+ let stored = false;
20833
+ try {
20834
+ await client.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: expected }));
20835
+ stored = true;
20836
+ const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
20837
+ if ((await response.Body?.transformToString()) !== expected)
20838
+ throw new Error('Durability probe read did not match its write.');
20839
+ } finally {
20840
+ if (stored) await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
20841
+ }
20842
+ };
20843
+
20844
+ export default createMobileUpdateRegistry({
20845
+ publicKeys: ${publicKeysSource(options.publicKeys)},
20846
+ store
20847
+ });
20848
+ `;
20849
+ }, writeAbsoluteMobileUpdateRegistry = async (options) => {
20850
+ const path2 = projectPath3(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
20851
+ if (!options.force) {
20852
+ await access16(path2).then(() => {
20853
+ throw new TypeError(`Mobile update registry already exists: ${path2}. Pass --force to replace it.`);
20854
+ }, () => {
20855
+ return;
20856
+ });
20857
+ }
20858
+ await mkdir18(dirname19(path2), { recursive: true });
20859
+ await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
20860
+ publicKeys: options.publicKeys,
20861
+ storage: options.storage
20862
+ }));
20863
+ return path2;
20864
+ };
20865
+ var init_updateServer = () => {};
20866
+
19905
20867
  // src/mobile/index.ts
19906
20868
  init_artifactStore();
19907
20869
 
@@ -37789,6 +38751,9 @@ var parseAbsoluteMobileUpdateRequest = (request) => {
37789
38751
  throw new TypeError("Mobile update runtime identity is invalid.");
37790
38752
  return identity;
37791
38753
  };
38754
+
38755
+ // src/mobile/index.ts
38756
+ init_updateServer();
37792
38757
  export {
37793
38758
  ABSOLUTE_ANDROID_RELEASE_FORMAT,
37794
38759
  ABSOLUTE_AUTH_PACKAGE,
@@ -37822,6 +38787,7 @@ export {
37822
38787
  ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES,
37823
38788
  ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES,
37824
38789
  ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT,
38790
+ ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT,
37825
38791
  ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM,
37826
38792
  ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV,
37827
38793
  ABSOLUTE_NATIVE_AUTH_SCOPES,
@@ -37834,6 +38800,7 @@ export {
37834
38800
  ANDROID_ASSOCIATION_PATH,
37835
38801
  APPLE_ASSOCIATION_PATH,
37836
38802
  AbsoluteMobilePageProtocolError,
38803
+ DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE,
37837
38804
  MOBILE_PAGE_REQUEST_HEADERS,
37838
38805
  absoluteDeviceNativeRequirements,
37839
38806
  absoluteExpoExecutable,
@@ -37886,6 +38853,7 @@ export {
37886
38853
  createAbsoluteMobileRouteMetadataPlugin,
37887
38854
  createAbsoluteMobileUpdateClient,
37888
38855
  createAbsoluteMobileUpdateRuntimeDescriptor,
38856
+ createAbsoluteMobileUpdateServerPlugin,
37889
38857
  createAbsoluteMobileUpgradeResponse,
37890
38858
  createAbsoluteRemoteExpoIosDevProject,
37891
38859
  createAbsoluteRemoteIosDevProject,
@@ -37910,6 +38878,7 @@ export {
37910
38878
  hashAbsoluteMobilePropsSchema,
37911
38879
  inspectAbsoluteAndroidInstalledApp,
37912
38880
  inspectAbsoluteMobileRouteMetadata,
38881
+ inspectAbsoluteMobileUpdateServer,
37913
38882
  inspectAbsoluteRemoteMac,
37914
38883
  inspectAbsoluteRemoteMacLanHost,
37915
38884
  inspectAbsoluteRemoteMacWorkspace,
@@ -37928,6 +38897,7 @@ export {
37928
38897
  loadAbsoluteDeviceCapabilityProviders,
37929
38898
  loadAbsoluteMobileMaterializedBundle,
37930
38899
  loadAbsoluteMobileUpdatePublisher,
38900
+ loadAbsoluteMobileUpdateServerModule,
37931
38901
  loadAbsoluteNativeReleasePublisher,
37932
38902
  matchesAbsoluteMobileRoutePattern,
37933
38903
  materializeAbsoluteCapacitorWebBundle,
@@ -37974,6 +38944,7 @@ export {
37974
38944
  readAbsoluteMobileUpdate,
37975
38945
  redactAbsoluteIosLog,
37976
38946
  removeAbsoluteRemoteMacProfile,
38947
+ renderAbsoluteMobileUpdateRegistry,
37977
38948
  repairAbsoluteIosDevSession,
37978
38949
  requestAbsoluteMobileBack,
37979
38950
  requireAbsoluteIosReleaseMetadata,
@@ -38009,8 +38980,9 @@ export {
38009
38980
  waitForAbsoluteIosHmrLog,
38010
38981
  writeAbsoluteCapacitorConfig,
38011
38982
  writeAbsoluteExpoProject,
38012
- writeAbsoluteMobileGithubWorkflow
38983
+ writeAbsoluteMobileGithubWorkflow,
38984
+ writeAbsoluteMobileUpdateRegistry
38013
38985
  };
38014
38986
 
38015
- //# debugId=7510C0F4F58DE47064756E2164756E21
38987
+ //# debugId=A519D38D860E371564756E2164756E21
38016
38988
  //# sourceMappingURL=index.js.map