@absolutejs/absolute 0.20.0-beta.85 → 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-UibQSn/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-UibQSn/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-UibQSn/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) => {
@@ -29272,7 +29513,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29272
29513
  const origin = request.headers.get("origin");
29273
29514
  const cors = origin && allowedOrigins.has(origin) ? {
29274
29515
  "access-control-allow-origin": origin,
29275
- "access-control-expose-headers": "content-range,etag",
29516
+ "access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
29276
29517
  vary: "Origin"
29277
29518
  } : {};
29278
29519
  if (request.method === "OPTIONS") {
@@ -29281,17 +29522,58 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29281
29522
  return new Response(null, {
29282
29523
  headers: {
29283
29524
  ...cors,
29284
- "access-control-allow-headers": "if-range,range,x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
29285
- "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",
29286
29527
  "access-control-max-age": "600"
29287
29528
  },
29288
29529
  status: 204
29289
29530
  });
29290
29531
  }
29291
- if (request.method !== "GET")
29292
- return new Response(null, { status: 405 });
29293
29532
  const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
29294
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 });
29295
29577
  if (relative19 === "update.json") {
29296
29578
  const expoProtocolVersion = request.headers.get("expo-protocol-version");
29297
29579
  const expoProtocol = expoProtocolVersion !== null;
@@ -29317,6 +29599,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29317
29599
  installationId,
29318
29600
  runtimeFingerprint
29319
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;
29320
29609
  if (expoProtocol) {
29321
29610
  let requestedCodeSigning;
29322
29611
  try {
@@ -29368,6 +29657,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29368
29657
  extra: {
29369
29658
  absolutejs: {
29370
29659
  channel: selected.manifest.channel,
29660
+ ...healthToken ? { healthToken } : {},
29371
29661
  releaseId: selected.manifest.releaseId
29372
29662
  },
29373
29663
  expoClient: descriptor.expoConfig
@@ -29395,7 +29685,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
29395
29685
  headers: {
29396
29686
  ...cors,
29397
29687
  "cache-control": "no-store",
29398
- etag: `"${selected.manifest.releaseId}"`
29688
+ etag: `"${selected.manifest.releaseId}"`,
29689
+ ...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
29399
29690
  }
29400
29691
  });
29401
29692
  }
@@ -29458,6 +29749,14 @@ var init_mobileUpdate = __esm(() => {
29458
29749
  RELEASE = /^amu_[a-f0-9]{64}$/;
29459
29750
  APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
29460
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"]);
29461
29760
  MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
29462
29761
  };
29463
29762
  expoProtocolHeaders = {
@@ -29545,6 +29844,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
29545
29844
  } catch (error) {
29546
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 });
29547
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`.");
29548
29852
  }, expoSigningOptions = (config) => {
29549
29853
  if (!config.updates?.expoCodeSigning)
29550
29854
  return;
@@ -29576,8 +29880,10 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
29576
29880
  if (!updates || !server?.autoMount)
29577
29881
  return new Elysia5({ name: "absolutejs-mobile-updates-disabled" });
29578
29882
  const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
29579
- if (options.production)
29883
+ if (options.production) {
29580
29884
  await verifyDurableModule(module);
29885
+ verifyHealthModule(config, module);
29886
+ }
29581
29887
  const manifest = new URL(updates.manifestUrl);
29582
29888
  if (!manifest.pathname.endsWith("/update.json"))
29583
29889
  throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
@@ -29596,10 +29902,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
29596
29902
  return;
29597
29903
  const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
29598
29904
  await verifyDurableModule(module);
29905
+ verifyHealthModule(config, module);
29599
29906
  if (config.engine === "expo")
29600
29907
  expoSigningOptions(config);
29601
29908
  return module.metadata;
29602
- }, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistry = (options) => {
29909
+ }, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
29603
29910
  const metadata = `export const absoluteMobileUpdateServer = {
29604
29911
  format: 1,
29605
29912
  provider: '${options.storage}',
@@ -29665,6 +29972,19 @@ export default createMobileUpdateRegistry({
29665
29972
  store
29666
29973
  });
29667
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}`);
29668
29988
  }, writeAbsoluteMobileUpdateRegistry = async (options) => {
29669
29989
  const path2 = projectPath(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
29670
29990
  if (!options.force) {
@@ -29676,6 +29996,7 @@ export default createMobileUpdateRegistry({
29676
29996
  }
29677
29997
  await mkdir13(dirname31(path2), { recursive: true });
29678
29998
  await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
29999
+ ...options.health ? { health: options.health } : {},
29679
30000
  publicKeys: options.publicKeys,
29680
30001
  storage: options.storage
29681
30002
  }));
@@ -37278,5 +37599,5 @@ export {
37278
37599
  devBuild
37279
37600
  };
37280
37601
 
37281
- //# debugId=51C2CE65BFD2E24164756E2164756E21
37602
+ //# debugId=C5E73CF77BAB352E64756E2164756E21
37282
37603
  //# sourceMappingURL=build.js.map