@absolutejs/absolute 0.20.0-beta.84 → 0.20.0-beta.86

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.
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  var __require = import.meta.require;
3
3
 
4
- // .angular-partial-tmp-wrP0vo/src/core/streamingSlotRegistrar.ts
4
+ // .angular-partial-tmp-sMuxmM/src/core/streamingSlotRegistrar.ts
5
5
  var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
6
6
  var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
7
7
  var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  var __require = import.meta.require;
3
3
 
4
- // .angular-partial-tmp-wrP0vo/src/core/streamingSlotRegistrar.ts
4
+ // .angular-partial-tmp-sMuxmM/src/core/streamingSlotRegistrar.ts
5
5
  var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
6
6
  var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
7
7
  var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
@@ -48,7 +48,7 @@ var warnMissingStreamingSlotCollector = (primitiveName) => {
48
48
  getWarningController()?.maybeWarn(primitiveName);
49
49
  };
50
50
 
51
- // .angular-partial-tmp-wrP0vo/src/core/streamingSlotRegistry.ts
51
+ // .angular-partial-tmp-sMuxmM/src/core/streamingSlotRegistry.ts
52
52
  var STREAMING_SLOT_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotAsyncLocalStorage");
53
53
  var isObjectRecord2 = (value) => Boolean(value) && typeof value === "object";
54
54
  var isAsyncLocalStorage = (value) => isObjectRecord2(value) && ("getStore" in value) && typeof value.getStore === "function" && ("run" in value) && typeof value.run === "function";
package/dist/build.js CHANGED
@@ -27625,7 +27625,7 @@ __export(exports_config, {
27625
27625
  import { readFileSync as readFileSync35 } from "fs";
27626
27626
  import { resolve as resolve44 } from "path";
27627
27627
  import { createHash as createHash5, createPublicKey, X509Certificate } from "crypto";
27628
- 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, field) => {
27628
+ 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, DEFAULT_UPDATE_HEALTH_FAILURE_RATE = 0.2, DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS = 20, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
27629
27629
  const root = resolve44(projectRoot);
27630
27630
  const path = resolve44(root, value);
27631
27631
  if (path !== root && !path.startsWith(`${root}/`)) {
@@ -27819,6 +27819,20 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
27819
27819
  if (!ENVIRONMENT_NAME_PATTERN.test(expoPrivateKeyEnv))
27820
27820
  throw new TypeError("mobile.updates.server.expoPrivateKeyEnv must be a valid environment variable name.");
27821
27821
  const autoMount = config.updates.server?.autoMount ?? true;
27822
+ const configuredHealth = config.updates.server?.health;
27823
+ let health;
27824
+ if (configuredHealth !== false) {
27825
+ const failureRate = configuredHealth?.failureRate ?? DEFAULT_UPDATE_HEALTH_FAILURE_RATE;
27826
+ const minimumReports = configuredHealth?.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
27827
+ const secretEnv = requireText(configuredHealth?.secretEnv ?? "ABSOLUTE_MOBILE_UPDATE_HEALTH_SECRET", "mobile.updates.server.health.secretEnv");
27828
+ if (!Number.isFinite(failureRate) || failureRate <= 0 || failureRate > 1)
27829
+ throw new TypeError("mobile.updates.server.health.failureRate must be greater than 0 and at most 1.");
27830
+ if (!Number.isSafeInteger(minimumReports) || minimumReports < 1)
27831
+ throw new TypeError("mobile.updates.server.health.minimumReports must be a positive integer.");
27832
+ if (!ENVIRONMENT_NAME_PATTERN.test(secretEnv))
27833
+ throw new TypeError("mobile.updates.server.health.secretEnv must be a valid environment variable name.");
27834
+ health = { failureRate, minimumReports, secretEnv };
27835
+ }
27822
27836
  if (autoMount) {
27823
27837
  const manifest = new URL(updates.manifestUrl);
27824
27838
  if (manifest.origin !== productionOrigin)
@@ -27851,7 +27865,12 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
27851
27865
  throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} certificate is not currently valid.`);
27852
27866
  expoCodeSigningKeys[keyId] = { certificatePem, privateKeyEnv };
27853
27867
  }
27854
- return { autoMount, expoCodeSigningKeys, registryModule };
27868
+ return {
27869
+ autoMount,
27870
+ expoCodeSigningKeys,
27871
+ ...health ? { health } : {},
27872
+ registryModule
27873
+ };
27855
27874
  }, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
27856
27875
  if (segment === "*" && (index !== count - 1 || count === 1)) {
27857
27876
  throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
@@ -28472,15 +28491,17 @@ __export(exports_mobileUpdate, {
28472
28491
  });
28473
28492
  import {
28474
28493
  createHash as createHash6,
28494
+ createHmac,
28475
28495
  createPrivateKey,
28476
28496
  createPublicKey as createPublicKey2,
28477
28497
  sign,
28498
+ timingSafeEqual,
28478
28499
  verify,
28479
28500
  X509Certificate as X509Certificate2
28480
28501
  } from "crypto";
28481
28502
  import { readFile as readFile9, stat as stat3 } from "fs/promises";
28482
28503
  import path from "path";
28483
- var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
28504
+ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", HEALTH_TOKEN_VERSION = 1, HEALTH_KINDS, FAILURE_HEALTH_KINDS, MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
28484
28505
  if (typeof value !== "string" || value.length === 0)
28485
28506
  throw new MobileUpdateRegistryError(`Mobile update ${field} is invalid`);
28486
28507
  return value;
@@ -28640,15 +28661,43 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
28640
28661
  return true;
28641
28662
  const value = createHash6("sha256").update(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${input.installationId}`).digest().readUInt32BE(0);
28642
28663
  return value / 4294967296 < input.rollout;
28664
+ }, base64Url = (value) => Buffer.from(value).toString("base64url"), healthPromotionId = (channel) => digest(new TextEncoder().encode(`${channel.appId}\x00${channel.channel}\x00${channel.releaseId ?? "embedded"}\x00${channel.promotedAt}`)), finiteMetric = (value, field) => {
28665
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
28666
+ throw new MobileUpdateRegistryError(`Mobile update health ${field} is invalid`);
28667
+ return value;
28668
+ }, parseHealthTransfer = (value) => {
28669
+ if (value === undefined)
28670
+ return;
28671
+ if (!object3(value))
28672
+ throw new MobileUpdateRegistryError("Mobile update health transfer is invalid");
28673
+ return {
28674
+ avoidedBytes: finiteMetric(value.avoidedBytes, "avoidedBytes"),
28675
+ downloadedBytes: finiteMetric(value.downloadedBytes, "downloadedBytes"),
28676
+ durationMs: finiteMetric(value.durationMs, "durationMs"),
28677
+ resumedBytes: finiteMetric(value.resumedBytes, "resumedBytes"),
28678
+ reusedBytes: finiteMetric(value.reusedBytes, "reusedBytes"),
28679
+ throughputBytesPerSecond: finiteMetric(value.throughputBytesPerSecond, "throughputBytesPerSecond")
28680
+ };
28643
28681
  }, createMobileUpdateRegistry = (options) => {
28644
28682
  const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
28645
28683
  const clock = options.clock ?? (() => new Date);
28684
+ const health = options.health;
28685
+ if (health && health.secret.length < 32)
28686
+ throw new MobileUpdateRegistryError("Mobile update health secret must contain at least 32 characters");
28687
+ if (health && !options.store.list)
28688
+ throw new MobileUpdateRegistryError("Mobile update health requires storage lifecycle listing");
28689
+ const minimumReports = health?.autoPause?.minimumReports ?? 20;
28690
+ const failureThreshold = health?.autoPause?.failureRate ?? 0.2;
28691
+ if (health && (!Number.isSafeInteger(minimumReports) || minimumReports < 1 || failureThreshold <= 0 || failureThreshold > 1))
28692
+ throw new MobileUpdateRegistryError("Mobile update health auto-pause policy is invalid");
28646
28693
  const root = (appId) => `${prefix}/${appHash(appId)}`;
28647
28694
  const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
28648
28695
  const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
28649
28696
  const fileKey = (manifest, file5) => `${releaseRoot(manifest)}/files/${file5.path}`;
28650
28697
  const contentBlobKey = (appId, sha256) => `${root(appId)}/blobs/${sha256}`;
28651
28698
  const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
28699
+ const healthRoot = (appId, promotionId, releaseId) => `${root(appId)}/health/${promotionId}/${releaseId}`;
28700
+ const pauseKey = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/paused.json`;
28652
28701
  const channelKey = (appId, channel) => {
28653
28702
  if (!APP_ID.test(appId) || !NAME.test(channel))
28654
28703
  throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
@@ -28677,6 +28726,38 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
28677
28726
  throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
28678
28727
  return value;
28679
28728
  };
28729
+ const signHealthToken = (payload) => {
28730
+ if (!health)
28731
+ return null;
28732
+ const encoded = base64Url(JSON.stringify(payload));
28733
+ const signature = createHmac("sha256", health.secret).update(encoded).digest("base64url");
28734
+ return `${encoded}.${signature}`;
28735
+ };
28736
+ const verifyHealthToken = (token) => {
28737
+ if (!health)
28738
+ throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
28739
+ const [encoded, provided, extra] = token.split(".");
28740
+ if (!encoded || !provided || extra)
28741
+ throw new MobileUpdateRegistryError("Mobile update health token is invalid");
28742
+ const expected = createHmac("sha256", health.secret).update(encoded).digest();
28743
+ let actual;
28744
+ try {
28745
+ actual = Buffer.from(provided, "base64url");
28746
+ } catch {
28747
+ actual = Buffer.alloc(0);
28748
+ }
28749
+ if (actual.byteLength !== expected.byteLength || !timingSafeEqual(actual, expected))
28750
+ throw new MobileUpdateRegistryError("Mobile update health token is invalid");
28751
+ let value;
28752
+ try {
28753
+ value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
28754
+ } catch {
28755
+ throw new MobileUpdateRegistryError("Mobile update health token is invalid");
28756
+ }
28757
+ if (!object3(value) || value.format !== HEALTH_TOKEN_VERSION || typeof value.appId !== "string" || typeof value.channel !== "string" || typeof value.installationId !== "string" || typeof value.promotionId !== "string" || typeof value.releaseId !== "string" || typeof value.runtimeFingerprint !== "string")
28758
+ throw new MobileUpdateRegistryError("Mobile update health token is invalid");
28759
+ return value;
28760
+ };
28680
28761
  const assertNotMarked = async (appId, releaseId) => {
28681
28762
  if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
28682
28763
  throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
@@ -28731,13 +28812,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
28731
28812
  const channel = await readChannel(input.appId, input.channel);
28732
28813
  if (!channel?.releaseId)
28733
28814
  return { status: "empty" };
28734
- const selected = rolloutMember({
28815
+ let selected = rolloutMember({
28735
28816
  appId: input.appId,
28736
28817
  channel: input.channel,
28737
28818
  installationId: input.installationId,
28738
28819
  releaseId: channel.releaseId,
28739
28820
  rollout: channel.rollout
28740
28821
  }) ? channel.releaseId : channel.fallbackReleaseId;
28822
+ if (health && selected === channel.releaseId && await options.store.head(pauseKey(input.appId, healthPromotionId(channel), channel.releaseId)))
28823
+ selected = channel.fallbackReleaseId;
28741
28824
  if (!selected)
28742
28825
  return { status: "empty" };
28743
28826
  const release = await readManifest2(input.appId, selected);
@@ -28753,6 +28836,163 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
28753
28836
  status: "selected"
28754
28837
  };
28755
28838
  };
28839
+ const issueUpdateHealthToken = async (input) => {
28840
+ if (!health)
28841
+ return null;
28842
+ const channel = await readChannel(input.appId, input.channel);
28843
+ if (!channel?.releaseId || channel.releaseId !== input.releaseId)
28844
+ return null;
28845
+ const resolution = await resolveUpdateState(input);
28846
+ if (resolution.status !== "selected" || resolution.manifest.releaseId !== input.releaseId)
28847
+ return null;
28848
+ return signHealthToken({
28849
+ appId: input.appId,
28850
+ channel: input.channel,
28851
+ format: HEALTH_TOKEN_VERSION,
28852
+ installationId: input.installationId,
28853
+ promotionId: healthPromotionId(channel),
28854
+ releaseId: input.releaseId,
28855
+ runtimeFingerprint: input.runtimeFingerprint
28856
+ });
28857
+ };
28858
+ const inspectUpdateHealth = async (input) => {
28859
+ if (!health)
28860
+ throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
28861
+ const channel = await readChannel(input.appId, input.channel);
28862
+ const releaseId = input.releaseId ?? channel?.releaseId;
28863
+ if (!channel || !releaseId || channel.releaseId !== releaseId)
28864
+ return null;
28865
+ const promotionId = healthPromotionId(channel);
28866
+ const prefix2 = `${healthRoot(input.appId, promotionId, releaseId)}/events/`;
28867
+ const objects = [];
28868
+ const cursors = new Set;
28869
+ let cursor;
28870
+ do {
28871
+ const page = await options.store.list({
28872
+ ...cursor ? { cursor } : {},
28873
+ prefix: prefix2
28874
+ });
28875
+ objects.push(...page.objects);
28876
+ if (!page.truncated)
28877
+ break;
28878
+ if (!page.cursor || cursors.has(page.cursor))
28879
+ throw new MobileUpdateRegistryError("Mobile update health storage returned an invalid cursor");
28880
+ cursors.add(page.cursor);
28881
+ cursor = page.cursor;
28882
+ } while (true);
28883
+ const installations = new Set;
28884
+ const byKind = new Map([...HEALTH_KINDS].map((kind) => [
28885
+ kind,
28886
+ new Set
28887
+ ]));
28888
+ const transfer = {
28889
+ avoidedBytes: 0,
28890
+ downloadedBytes: 0,
28891
+ durationMs: 0,
28892
+ resumedBytes: 0,
28893
+ reusedBytes: 0,
28894
+ throughputBytesPerSecond: 0
28895
+ };
28896
+ for (const item of objects) {
28897
+ const bytes = await options.store.get(item.key);
28898
+ if (!bytes)
28899
+ continue;
28900
+ const head = await options.store.head(item.key);
28901
+ if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
28902
+ throw new MobileUpdateRegistryError("Stored mobile update health evidence integrity failed");
28903
+ const value = decode(bytes);
28904
+ if (!object3(value) || typeof value.installationHash !== "string" || !HEALTH_KINDS.has(String(value.kind)))
28905
+ throw new MobileUpdateRegistryError("Stored mobile update health evidence is invalid");
28906
+ const kind = value.kind;
28907
+ installations.add(value.installationHash);
28908
+ byKind.get(kind).add(value.installationHash);
28909
+ if (kind === "downloaded" && object3(value.transfer)) {
28910
+ const parsed = parseHealthTransfer(value.transfer);
28911
+ for (const key of Object.keys(transfer))
28912
+ transfer[key] += parsed[key];
28913
+ }
28914
+ }
28915
+ const failures = new Set([
28916
+ ...byKind.get("quarantined"),
28917
+ ...byKind.get("rolled-back")
28918
+ ]);
28919
+ const terminals = new Set([...byKind.get("activated"), ...failures]);
28920
+ const failureRate = terminals.size === 0 ? 0 : failures.size / terminals.size;
28921
+ return {
28922
+ activated: byKind.get("activated").size,
28923
+ appId: input.appId,
28924
+ channel: input.channel,
28925
+ downloaded: byKind.get("downloaded").size,
28926
+ downloadFailed: byKind.get("download-failed").size,
28927
+ failureRate,
28928
+ failures: failures.size,
28929
+ paused: Boolean(await options.store.head(pauseKey(input.appId, promotionId, releaseId))),
28930
+ promotionId,
28931
+ quarantined: byKind.get("quarantined").size,
28932
+ releaseId,
28933
+ reportedInstallations: installations.size,
28934
+ rolledBack: byKind.get("rolled-back").size,
28935
+ rollout: channel.rollout,
28936
+ terminalReports: terminals.size,
28937
+ transfer
28938
+ };
28939
+ };
28940
+ const recordUpdateHealth = async (input) => {
28941
+ const payload = verifyHealthToken(input.token);
28942
+ if (payload.appId !== input.appId || payload.channel !== input.channel || payload.installationId !== input.installationId || payload.releaseId !== input.releaseId || payload.runtimeFingerprint !== input.runtimeFingerprint || !HEALTH_KINDS.has(input.kind) || input.reason !== undefined && input.reason !== "boot-interrupted" && input.reason !== "boot-timeout")
28943
+ throw new MobileUpdateRegistryError("Mobile update health evidence does not match its token");
28944
+ const release = await readManifest2(input.appId, input.releaseId);
28945
+ if (!release || release.manifest.runtimeFingerprint !== input.runtimeFingerprint)
28946
+ throw new MobileUpdateRegistryError("Mobile update health release is invalid");
28947
+ const activeChannel = await readChannel(input.appId, input.channel);
28948
+ if (!activeChannel || activeChannel.releaseId !== input.releaseId || healthPromotionId(activeChannel) !== payload.promotionId)
28949
+ throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
28950
+ const transfer = parseHealthTransfer(input.transfer);
28951
+ const installationHash = createHmac("sha256", health.secret).update(input.installationId).digest("hex");
28952
+ const evidence = {
28953
+ format: 1,
28954
+ installationHash,
28955
+ kind: input.kind,
28956
+ observedAt: clock().toISOString(),
28957
+ ...input.reason ? { reason: input.reason } : {},
28958
+ ...transfer ? { transfer } : {}
28959
+ };
28960
+ const bytes = json(evidence);
28961
+ await options.store.put(`${healthRoot(input.appId, payload.promotionId, input.releaseId)}/events/${installationHash}/${input.kind}.json`, bytes, {
28962
+ cacheControl: "no-store",
28963
+ contentType: "application/json",
28964
+ maxBytes: bytes.byteLength,
28965
+ metadata: { kind: input.kind, sha256: digest(bytes) }
28966
+ });
28967
+ let report = await inspectUpdateHealth({
28968
+ appId: input.appId,
28969
+ channel: input.channel,
28970
+ releaseId: input.releaseId
28971
+ });
28972
+ if (!report)
28973
+ throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
28974
+ if (FAILURE_HEALTH_KINDS.has(input.kind) && report.terminalReports >= minimumReports && report.failureRate >= failureThreshold && !report.paused) {
28975
+ const marker = json({
28976
+ appId: input.appId,
28977
+ channel: input.channel,
28978
+ failureRate: report.failureRate,
28979
+ failures: report.failures,
28980
+ format: 1,
28981
+ pausedAt: clock().toISOString(),
28982
+ promotionId: payload.promotionId,
28983
+ releaseId: input.releaseId,
28984
+ reports: report.terminalReports
28985
+ });
28986
+ await options.store.put(pauseKey(input.appId, payload.promotionId, input.releaseId), marker, {
28987
+ cacheControl: "no-store",
28988
+ contentType: "application/json",
28989
+ maxBytes: marker.byteLength,
28990
+ metadata: { releaseid: input.releaseId, sha256: digest(marker) }
28991
+ });
28992
+ report = { ...report, paused: true };
28993
+ }
28994
+ return report;
28995
+ };
28756
28996
  const retentionValues = (input) => {
28757
28997
  if (!APP_ID.test(input.appId))
28758
28998
  throw new MobileUpdateRegistryError("Mobile update appId is invalid");
@@ -28995,6 +29235,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
28995
29235
  return result;
28996
29236
  };
28997
29237
  return {
29238
+ ...health ? { inspectUpdateHealth, issueUpdateHealthToken, recordUpdateHealth } : {},
28998
29239
  inspectUpdateStorage: async (input) => (await inventory(input)).report,
28999
29240
  pruneUpdates,
29000
29241
  publishUpdate: async (input) => {
@@ -29270,24 +29511,69 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29270
29511
  const expoCodeSigning = resolveExpoCodeSigning(options.expoCodeSigning);
29271
29512
  return async (request) => {
29272
29513
  const origin = request.headers.get("origin");
29273
- const cors = origin && allowedOrigins.has(origin) ? { "access-control-allow-origin": origin, vary: "Origin" } : {};
29514
+ const cors = origin && allowedOrigins.has(origin) ? {
29515
+ "access-control-allow-origin": origin,
29516
+ "access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
29517
+ vary: "Origin"
29518
+ } : {};
29274
29519
  if (request.method === "OPTIONS") {
29275
29520
  if (!origin || !allowedOrigins.has(origin))
29276
29521
  return new Response(null, { status: 403 });
29277
29522
  return new Response(null, {
29278
29523
  headers: {
29279
29524
  ...cors,
29280
- "access-control-allow-headers": "x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
29281
- "access-control-allow-methods": "GET,OPTIONS",
29525
+ "access-control-allow-headers": "content-type,if-range,range,x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-health-token,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
29526
+ "access-control-allow-methods": "GET,POST,OPTIONS",
29282
29527
  "access-control-max-age": "600"
29283
29528
  },
29284
29529
  status: 204
29285
29530
  });
29286
29531
  }
29287
- if (request.method !== "GET")
29288
- return new Response(null, { status: 405 });
29289
29532
  const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
29290
29533
  const relative19 = pathname.startsWith(`${route}/`) ? pathname.slice(route.length + 1) : "";
29534
+ if (request.method === "POST" && relative19 === "health") {
29535
+ if (!options.registry.recordUpdateHealth)
29536
+ return new Response(null, { status: 404 });
29537
+ const appId = request.headers.get("x-absolute-mobile-app");
29538
+ const channel = request.headers.get("x-absolute-mobile-channel");
29539
+ const installationId = request.headers.get("x-absolute-mobile-installation");
29540
+ const runtimeFingerprint = request.headers.get("x-absolute-mobile-runtime");
29541
+ const token = request.headers.get("x-absolute-mobile-health-token");
29542
+ const declared = Number(request.headers.get("content-length"));
29543
+ if (appId !== options.appId || channel !== options.channel || !installationId || !runtimeFingerprint || !token || Number.isFinite(declared) && declared > 4096)
29544
+ return new Response(null, { status: 400 });
29545
+ const bodyBytes = new Uint8Array(await request.arrayBuffer());
29546
+ if (bodyBytes.byteLength > 4096)
29547
+ return new Response(null, { status: 413 });
29548
+ let body;
29549
+ try {
29550
+ body = JSON.parse(new TextDecoder().decode(bodyBytes));
29551
+ } catch {
29552
+ return new Response(null, { status: 400 });
29553
+ }
29554
+ if (!object3(body) || typeof body.releaseId !== "string" || typeof body.kind !== "string")
29555
+ return new Response(null, { status: 400 });
29556
+ try {
29557
+ const report = await options.registry.recordUpdateHealth({
29558
+ appId,
29559
+ channel,
29560
+ installationId,
29561
+ kind: body.kind,
29562
+ ...body.reason === "boot-interrupted" || body.reason === "boot-timeout" ? { reason: body.reason } : {},
29563
+ releaseId: body.releaseId,
29564
+ runtimeFingerprint,
29565
+ token,
29566
+ ...object3(body.transfer) ? { transfer: body.transfer } : {}
29567
+ });
29568
+ return Response.json({ paused: report.paused }, { headers: { ...cors, "cache-control": "no-store" }, status: 202 });
29569
+ } catch (error) {
29570
+ if (error instanceof MobileUpdateRegistryError)
29571
+ return new Response(null, { status: 403 });
29572
+ throw error;
29573
+ }
29574
+ }
29575
+ if (request.method !== "GET")
29576
+ return new Response(null, { status: 405 });
29291
29577
  if (relative19 === "update.json") {
29292
29578
  const expoProtocolVersion = request.headers.get("expo-protocol-version");
29293
29579
  const expoProtocol = expoProtocolVersion !== null;
@@ -29313,6 +29599,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29313
29599
  installationId,
29314
29600
  runtimeFingerprint
29315
29601
  });
29602
+ const healthToken = selected && options.registry.issueUpdateHealthToken ? await options.registry.issueUpdateHealthToken({
29603
+ appId,
29604
+ channel,
29605
+ installationId,
29606
+ releaseId: selected.manifest.releaseId,
29607
+ runtimeFingerprint
29608
+ }) : null;
29316
29609
  if (expoProtocol) {
29317
29610
  let requestedCodeSigning;
29318
29611
  try {
@@ -29364,6 +29657,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29364
29657
  extra: {
29365
29658
  absolutejs: {
29366
29659
  channel: selected.manifest.channel,
29660
+ ...healthToken ? { healthToken } : {},
29367
29661
  releaseId: selected.manifest.releaseId
29368
29662
  },
29369
29663
  expoClient: descriptor.expoConfig
@@ -29391,7 +29685,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29391
29685
  headers: {
29392
29686
  ...cors,
29393
29687
  "cache-control": "no-store",
29394
- etag: `"${selected.manifest.releaseId}"`
29688
+ etag: `"${selected.manifest.releaseId}"`,
29689
+ ...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
29395
29690
  }
29396
29691
  });
29397
29692
  }
@@ -29405,14 +29700,42 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29405
29700
  });
29406
29701
  if (!file5)
29407
29702
  return new Response(null, { status: 404 });
29408
- return new Response(new Blob([new Uint8Array(file5.bytes).buffer]), {
29703
+ const etag = `"${file5.file.sha256}"`;
29704
+ const range = request.headers.get("range");
29705
+ const useRange = range !== null && (!request.headers.has("if-range") || request.headers.get("if-range") === etag);
29706
+ let contents = file5.bytes;
29707
+ let status = 200;
29708
+ let contentRange;
29709
+ if (useRange) {
29710
+ const parsed = /^bytes=(\d+)-(\d*)$/.exec(range);
29711
+ const start = parsed?.[1] === undefined ? NaN : Number(parsed[1]);
29712
+ const requestedEnd = parsed?.[2] === undefined || parsed[2] === "" ? file5.bytes.byteLength - 1 : Number(parsed[2]);
29713
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(requestedEnd) || start < 0 || start >= file5.bytes.byteLength || requestedEnd < start)
29714
+ return new Response(null, {
29715
+ headers: {
29716
+ ...cors,
29717
+ "accept-ranges": "bytes",
29718
+ "content-range": `bytes */${file5.bytes.byteLength}`,
29719
+ etag
29720
+ },
29721
+ status: 416
29722
+ });
29723
+ const end = Math.min(requestedEnd, file5.bytes.byteLength - 1);
29724
+ contents = file5.bytes.slice(start, end + 1);
29725
+ status = 206;
29726
+ contentRange = `bytes ${start}-${end}/${file5.bytes.byteLength}`;
29727
+ }
29728
+ return new Response(new Blob([new Uint8Array(contents).buffer]), {
29409
29729
  headers: {
29410
29730
  ...cors,
29731
+ "accept-ranges": "bytes",
29411
29732
  "cache-control": "public, max-age=31536000, immutable",
29412
- "content-length": String(file5.file.bytes),
29733
+ "content-length": String(contents.byteLength),
29734
+ ...contentRange ? { "content-range": contentRange } : {},
29413
29735
  "content-type": expoContentType(file5.file.path.includes(".") ? file5.file.path.slice(file5.file.path.lastIndexOf(".") + 1) : undefined, false),
29414
- etag: `"${file5.file.sha256}"`
29415
- }
29736
+ etag
29737
+ },
29738
+ status
29416
29739
  });
29417
29740
  };
29418
29741
  };
@@ -29426,6 +29749,14 @@ var init_mobileUpdate = __esm(() => {
29426
29749
  RELEASE = /^amu_[a-f0-9]{64}$/;
29427
29750
  APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
29428
29751
  NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
29752
+ HEALTH_KINDS = new Set([
29753
+ "activated",
29754
+ "downloaded",
29755
+ "download-failed",
29756
+ "quarantined",
29757
+ "rolled-back"
29758
+ ]);
29759
+ FAILURE_HEALTH_KINDS = new Set(["quarantined", "rolled-back"]);
29429
29760
  MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
29430
29761
  };
29431
29762
  expoProtocolHeaders = {
@@ -29513,6 +29844,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
29513
29844
  } catch (error) {
29514
29845
  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 });
29515
29846
  }
29847
+ }, verifyHealthModule = (config, module) => {
29848
+ if (!config.updateServer?.health)
29849
+ return;
29850
+ if (typeof module.registry.inspectUpdateHealth !== "function" || typeof module.registry.issueUpdateHealthToken !== "function" || typeof module.registry.recordUpdateHealth !== "function")
29851
+ throw new TypeError("Mobile update fleet health is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
29516
29852
  }, expoSigningOptions = (config) => {
29517
29853
  if (!config.updates?.expoCodeSigning)
29518
29854
  return;
@@ -29544,8 +29880,10 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
29544
29880
  if (!updates || !server?.autoMount)
29545
29881
  return new Elysia5({ name: "absolutejs-mobile-updates-disabled" });
29546
29882
  const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
29547
- if (options.production)
29883
+ if (options.production) {
29548
29884
  await verifyDurableModule(module);
29885
+ verifyHealthModule(config, module);
29886
+ }
29549
29887
  const manifest = new URL(updates.manifestUrl);
29550
29888
  if (!manifest.pathname.endsWith("/update.json"))
29551
29889
  throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
@@ -29564,10 +29902,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
29564
29902
  return;
29565
29903
  const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
29566
29904
  await verifyDurableModule(module);
29905
+ verifyHealthModule(config, module);
29567
29906
  if (config.engine === "expo")
29568
29907
  expoSigningOptions(config);
29569
29908
  return module.metadata;
29570
- }, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistry = (options) => {
29909
+ }, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
29571
29910
  const metadata = `export const absoluteMobileUpdateServer = {
29572
29911
  format: 1,
29573
29912
  provider: '${options.storage}',
@@ -29633,6 +29972,19 @@ export default createMobileUpdateRegistry({
29633
29972
  store
29634
29973
  });
29635
29974
  `;
29975
+ }, renderAbsoluteMobileUpdateRegistry = (options) => {
29976
+ const source = renderAbsoluteMobileUpdateRegistryBase(options);
29977
+ if (!options.health)
29978
+ return source;
29979
+ const secret = options.storage === "local" ? `process.env.${options.health.secretEnv} ?? 'absolutejs-local-health-secret-not-for-production'` : `required('${options.health.secretEnv}')`;
29980
+ const health = ` health: {
29981
+ autoPause: { failureRate: ${options.health.failureRate}, minimumReports: ${options.health.minimumReports} },
29982
+ secret: ${secret}
29983
+ },
29984
+ `;
29985
+ return source.replace(`export default createMobileUpdateRegistry({
29986
+ `, `export default createMobileUpdateRegistry({
29987
+ ${health}`);
29636
29988
  }, writeAbsoluteMobileUpdateRegistry = async (options) => {
29637
29989
  const path2 = projectPath(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
29638
29990
  if (!options.force) {
@@ -29644,6 +29996,7 @@ export default createMobileUpdateRegistry({
29644
29996
  }
29645
29997
  await mkdir13(dirname31(path2), { recursive: true });
29646
29998
  await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
29999
+ ...options.health ? { health: options.health } : {},
29647
30000
  publicKeys: options.publicKeys,
29648
30001
  storage: options.storage
29649
30002
  }));
@@ -37246,5 +37599,5 @@ export {
37246
37599
  devBuild
37247
37600
  };
37248
37601
 
37249
- //# debugId=5BE2EBFD6FC5145B64756E2164756E21
37602
+ //# debugId=C5E73CF77BAB352E64756E2164756E21
37250
37603
  //# sourceMappingURL=build.js.map