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

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.
@@ -19970,7 +19970,7 @@ import {
19970
19970
  } from "crypto";
19971
19971
  import { readFile as readFile23, stat as stat5 } from "fs/promises";
19972
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) => {
19973
+ 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, object6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field2) => {
19974
19974
  if (typeof value !== "string" || value.length === 0)
19975
19975
  throw new MobileUpdateRegistryError(`Mobile update ${field2} is invalid`);
19976
19976
  return value;
@@ -20137,6 +20137,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20137
20137
  const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
20138
20138
  const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
20139
20139
  const fileKey = (manifest, file) => `${releaseRoot(manifest)}/files/${file.path}`;
20140
+ const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
20140
20141
  const channelKey = (appId, channel) => {
20141
20142
  if (!APP_ID.test(appId) || !NAME.test(channel))
20142
20143
  throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
@@ -20165,6 +20166,12 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20165
20166
  throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
20166
20167
  return value;
20167
20168
  };
20169
+ const assertNotMarked = async (appId, releaseId) => {
20170
+ if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
20171
+ throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
20172
+ if (await options.store.head(tombstoneKey(appId, releaseId)))
20173
+ throw new MobileUpdateRegistryError("Mobile update release is marked for collection. Increase retention and apply garbage collection to restore it before promotion");
20174
+ };
20168
20175
  const writeChannel = async (input, signal) => {
20169
20176
  const value = {
20170
20177
  ...input,
@@ -20189,6 +20196,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20189
20196
  input.signal?.throwIfAborted();
20190
20197
  if (input.rollout <= 0 || input.rollout > 1)
20191
20198
  throw new MobileUpdateRegistryError("Mobile update rollout is invalid");
20199
+ await assertNotMarked(input.appId, input.releaseId);
20192
20200
  const release = await readManifest(input.appId, input.releaseId);
20193
20201
  if (!release || release.manifest.channel !== input.channel)
20194
20202
  throw new MobileUpdateRegistryError("Mobile update was not published to this channel");
@@ -20234,11 +20242,221 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20234
20242
  status: "selected"
20235
20243
  };
20236
20244
  };
20245
+ const retentionValues = (input) => {
20246
+ if (!APP_ID.test(input.appId))
20247
+ throw new MobileUpdateRegistryError("Mobile update appId is invalid");
20248
+ const retainRecent = input.retainRecent ?? DEFAULT_RETAIN_RECENT;
20249
+ const minAgeMs = input.minAgeMs ?? DEFAULT_MIN_AGE_MS;
20250
+ if (!Number.isSafeInteger(retainRecent) || retainRecent < 0)
20251
+ throw new MobileUpdateRegistryError("Mobile update retained release count is invalid");
20252
+ if (!Number.isSafeInteger(minAgeMs) || minAgeMs < 0)
20253
+ throw new MobileUpdateRegistryError("Mobile update minimum release age is invalid");
20254
+ return { minAgeMs, retainRecent };
20255
+ };
20256
+ const listObjects = async (appId, signal) => {
20257
+ const list = options.store.list;
20258
+ if (!list)
20259
+ throw new MobileUpdateRegistryError("Mobile update storage does not support lifecycle listing");
20260
+ const objects = [];
20261
+ const cursors = new Set;
20262
+ let cursor;
20263
+ do {
20264
+ signal?.throwIfAborted();
20265
+ const page = await list({
20266
+ ...cursor ? { cursor } : {},
20267
+ prefix: `${root(appId)}/`
20268
+ });
20269
+ objects.push(...page.objects);
20270
+ if (!page.truncated)
20271
+ break;
20272
+ if (!page.cursor || cursors.has(page.cursor))
20273
+ throw new MobileUpdateRegistryError("Mobile update storage returned an invalid lifecycle cursor");
20274
+ cursors.add(page.cursor);
20275
+ cursor = page.cursor;
20276
+ } while (true);
20277
+ return objects;
20278
+ };
20279
+ const inventory = async (input) => {
20280
+ const { minAgeMs, retainRecent } = retentionValues(input);
20281
+ const objects = await listObjects(input.appId, input.signal);
20282
+ const appRoot = `${root(input.appId)}/`;
20283
+ const releasePrefix = `${appRoot}releases/`;
20284
+ const channelPrefix = `${appRoot}channels/`;
20285
+ const markerPrefix = `${appRoot}gc/`;
20286
+ const objectKeysByRelease = new Map;
20287
+ const objectBytesByRelease = new Map;
20288
+ for (const blobObject of objects) {
20289
+ if (!blobObject.key.startsWith(releasePrefix))
20290
+ continue;
20291
+ const suffix = blobObject.key.slice(releasePrefix.length);
20292
+ const releaseId = suffix.slice(0, suffix.indexOf("/"));
20293
+ if (!RELEASE.test(releaseId))
20294
+ continue;
20295
+ objectKeysByRelease.set(releaseId, [
20296
+ ...objectKeysByRelease.get(releaseId) ?? [],
20297
+ blobObject.key
20298
+ ]);
20299
+ objectBytesByRelease.set(releaseId, (objectBytesByRelease.get(releaseId) ?? 0) + blobObject.size);
20300
+ }
20301
+ const channels = [];
20302
+ for (const blobObject of objects) {
20303
+ if (!blobObject.key.startsWith(channelPrefix) || !blobObject.key.endsWith(".json"))
20304
+ continue;
20305
+ const bytes = await options.store.get(blobObject.key);
20306
+ if (!bytes)
20307
+ throw new MobileUpdateRegistryError("Mobile update channel disappeared during lifecycle inspection");
20308
+ const channel = parseChannel(decode2(bytes));
20309
+ if (channel.appId !== input.appId)
20310
+ throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
20311
+ channels.push(channel);
20312
+ }
20313
+ const markedAt = new Map;
20314
+ for (const blobObject of objects) {
20315
+ if (!blobObject.key.startsWith(markerPrefix) || !blobObject.key.endsWith(".json"))
20316
+ continue;
20317
+ const releaseId = blobObject.key.slice(markerPrefix.length, -".json".length);
20318
+ if (!RELEASE.test(releaseId))
20319
+ continue;
20320
+ const bytes = await options.store.get(blobObject.key);
20321
+ if (!bytes)
20322
+ continue;
20323
+ const marker = decode2(bytes);
20324
+ if (!object6(marker) || marker.format !== 1 || marker.appId !== input.appId || marker.releaseId !== releaseId || !iso(marker.markedAt))
20325
+ throw new MobileUpdateRegistryError("Mobile update collection marker is invalid");
20326
+ markedAt.set(releaseId, marker.markedAt);
20327
+ }
20328
+ const active = new Set(channels.flatMap((channel) => channel.releaseId ? [channel.releaseId] : []));
20329
+ const fallback = new Set(channels.flatMap((channel) => channel.fallbackReleaseId ? [channel.fallbackReleaseId] : []));
20330
+ const manifests = [];
20331
+ for (const releaseId of objectKeysByRelease.keys()) {
20332
+ const release = await readManifest(input.appId, releaseId);
20333
+ if (release)
20334
+ manifests.push(release.manifest);
20335
+ }
20336
+ const recent = new Set;
20337
+ const manifestsByChannel = new Map;
20338
+ for (const manifest of manifests)
20339
+ manifestsByChannel.set(manifest.channel, [
20340
+ ...manifestsByChannel.get(manifest.channel) ?? [],
20341
+ manifest
20342
+ ]);
20343
+ for (const releases2 of manifestsByChannel.values())
20344
+ for (const manifest of releases2.toSorted((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, retainRecent))
20345
+ recent.add(manifest.releaseId);
20346
+ const now = clock().getTime();
20347
+ const releases = manifests.map((manifest) => {
20348
+ const protectedBy = [];
20349
+ if (active.has(manifest.releaseId))
20350
+ protectedBy.push("active");
20351
+ if (fallback.has(manifest.releaseId))
20352
+ protectedBy.push("fallback");
20353
+ if (recent.has(manifest.releaseId))
20354
+ protectedBy.push("recent");
20355
+ if (now - Date.parse(manifest.createdAt) < minAgeMs)
20356
+ protectedBy.push("age");
20357
+ return {
20358
+ bytes: objectBytesByRelease.get(manifest.releaseId) ?? 0,
20359
+ channel: manifest.channel,
20360
+ createdAt: manifest.createdAt,
20361
+ ...markedAt.has(manifest.releaseId) ? { markedAt: markedAt.get(manifest.releaseId) } : {},
20362
+ objectCount: objectKeysByRelease.get(manifest.releaseId)?.length ?? 0,
20363
+ protectedBy,
20364
+ releaseId: manifest.releaseId
20365
+ };
20366
+ }).toSorted((left, right) => right.createdAt.localeCompare(left.createdAt));
20367
+ const releaseBytes = releases.reduce((total, release) => total + release.bytes, 0);
20368
+ const totalBytes = objects.reduce((total, object22) => total + object22.size, 0);
20369
+ return {
20370
+ objectKeysByRelease,
20371
+ report: {
20372
+ appId: input.appId,
20373
+ channelCount: channels.length,
20374
+ reclaimableBytes: releases.filter((release) => release.protectedBy.length === 0).reduce((total, release) => total + release.bytes, 0),
20375
+ releaseBytes,
20376
+ releaseCount: releases.length,
20377
+ releases,
20378
+ totalBytes,
20379
+ totalObjectCount: objects.length,
20380
+ untrackedBytes: totalBytes - releaseBytes
20381
+ }
20382
+ };
20383
+ };
20384
+ const pruneUpdates = async (input) => {
20385
+ const initial = await inventory(input);
20386
+ const result = {
20387
+ ...initial.report,
20388
+ dryRun: input.apply !== true,
20389
+ marked: [],
20390
+ reclaimedBytes: 0,
20391
+ restored: [],
20392
+ swept: []
20393
+ };
20394
+ if (input.apply !== true)
20395
+ return result;
20396
+ const remove = options.store.delete;
20397
+ if (!remove)
20398
+ throw new MobileUpdateRegistryError("Mobile update storage does not support lifecycle deletion");
20399
+ const gracePeriodMs = input.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
20400
+ if (!Number.isSafeInteger(gracePeriodMs) || gracePeriodMs < 0)
20401
+ throw new MobileUpdateRegistryError("Mobile update collection grace period is invalid");
20402
+ const now = clock();
20403
+ for (const release of initial.report.releases) {
20404
+ input.signal?.throwIfAborted();
20405
+ const marker = tombstoneKey(input.appId, release.releaseId);
20406
+ if (release.protectedBy.length > 0) {
20407
+ if (release.markedAt) {
20408
+ await remove(marker);
20409
+ result.restored.push(release.releaseId);
20410
+ }
20411
+ continue;
20412
+ }
20413
+ if (!release.markedAt) {
20414
+ const bytes = json({
20415
+ appId: input.appId,
20416
+ format: 1,
20417
+ markedAt: now.toISOString(),
20418
+ releaseId: release.releaseId
20419
+ });
20420
+ await options.store.put(marker, bytes, {
20421
+ cacheControl: "no-cache",
20422
+ contentType: "application/json",
20423
+ maxBytes: bytes.byteLength,
20424
+ metadata: { releaseid: release.releaseId, sha256: digest(bytes) },
20425
+ signal: input.signal
20426
+ });
20427
+ result.marked.push(release.releaseId);
20428
+ }
20429
+ }
20430
+ const current = await inventory(input);
20431
+ for (const release of current.report.releases) {
20432
+ input.signal?.throwIfAborted();
20433
+ if (release.protectedBy.length > 0 || !release.markedAt || now.getTime() - Date.parse(release.markedAt) < gracePeriodMs || result.marked.includes(release.releaseId))
20434
+ continue;
20435
+ const checked = await inventory(input);
20436
+ const candidate = checked.report.releases.find((value) => value.releaseId === release.releaseId);
20437
+ if (!candidate || candidate.protectedBy.length > 0 || !candidate.markedAt)
20438
+ continue;
20439
+ const keys = checked.objectKeysByRelease.get(release.releaseId) ?? [];
20440
+ for (const key of keys.toSorted((left, right) => {
20441
+ const leftManifest = left.endsWith("/update.json");
20442
+ const rightManifest = right.endsWith("/update.json");
20443
+ return Number(leftManifest) - Number(rightManifest);
20444
+ }))
20445
+ await remove(key);
20446
+ await remove(tombstoneKey(input.appId, release.releaseId));
20447
+ result.reclaimedBytes += candidate.bytes;
20448
+ result.swept.push(release.releaseId);
20449
+ }
20450
+ return result;
20451
+ };
20237
20452
  return {
20453
+ inspectUpdateStorage: async (input) => (await inventory(input)).report,
20454
+ pruneUpdates,
20238
20455
  publishUpdate: async (input) => {
20239
20456
  input.signal?.throwIfAborted();
20240
20457
  const manifest = parseMobileUpdateManifest(input.manifest);
20241
20458
  verifyManifestSignature(manifest, options.publicKeys);
20459
+ await assertNotMarked(manifest.appId, manifest.releaseId);
20242
20460
  const localRoot = path.resolve(input.releaseDirectory);
20243
20461
  const localManifest = parseMobileUpdateManifest(JSON.parse(await readFile23(path.join(localRoot, "update.json"), "utf8")));
20244
20462
  if (JSON.stringify(localManifest) !== JSON.stringify(manifest))
@@ -20303,6 +20521,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20303
20521
  if (!existing)
20304
20522
  throw new MobileUpdateRegistryError("Mobile update channel does not exist");
20305
20523
  if (input.releaseId) {
20524
+ await assertNotMarked(input.appId, input.releaseId);
20306
20525
  const release = await readManifest(input.appId, input.releaseId);
20307
20526
  if (!release || release.manifest.channel !== input.channel)
20308
20527
  throw new MobileUpdateRegistryError("Mobile rollback release was not published");
@@ -20635,6 +20854,9 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20635
20854
  var init_mobileUpdate = __esm(() => {
20636
20855
  MAX_FILE_BYTES = 32 * 1024 * 1024;
20637
20856
  MAX_TOTAL_BYTES = 128 * 1024 * 1024;
20857
+ DAY_MS = 24 * 60 * 60 * 1000;
20858
+ DEFAULT_MIN_AGE_MS = 30 * DAY_MS;
20859
+ DEFAULT_GRACE_PERIOD_MS = 7 * DAY_MS;
20638
20860
  HASH = /^[a-f0-9]{64}$/;
20639
20861
  RELEASE = /^amu_[a-f0-9]{64}$/;
20640
20862
  APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
@@ -38660,6 +38882,45 @@ var verifyAbsoluteMobileUpdateSignature = (manifestValue, publicKey) => {
38660
38882
  };
38661
38883
 
38662
38884
  // src/mobile/updatePublisher.ts
38885
+ var lifecycleMethod = (publisher, name) => {
38886
+ const method = publisher[name];
38887
+ if (typeof method !== "function")
38888
+ throw new TypeError(`Mobile update registry does not support ${name}. Re-run \`absolute mobile update provision --force\` after upgrading @absolutejs/deploy.`);
38889
+ return method;
38890
+ };
38891
+ var validateStorageIdentity = (result, appId) => {
38892
+ if (!object5(result) || result.appId !== appId || !Array.isArray(result.releases) || ![
38893
+ result.channelCount,
38894
+ result.reclaimableBytes,
38895
+ result.releaseBytes,
38896
+ result.releaseCount,
38897
+ result.totalBytes,
38898
+ result.totalObjectCount,
38899
+ result.untrackedBytes
38900
+ ].every((value) => Number.isSafeInteger(value) && value >= 0))
38901
+ throw new TypeError("Mobile update registry returned an invalid storage report.");
38902
+ return result;
38903
+ };
38904
+ var inspectAbsoluteMobileUpdateStorage = async (options) => validateStorageIdentity(await lifecycleMethod(options.publisher, "inspectUpdateStorage")({
38905
+ appId: options.appId,
38906
+ ...options.minAgeMs === undefined ? {} : { minAgeMs: options.minAgeMs },
38907
+ ...options.retainRecent === undefined ? {} : { retainRecent: options.retainRecent },
38908
+ ...options.signal ? { signal: options.signal } : {}
38909
+ }), options.appId);
38910
+ var pruneAbsoluteMobileUpdates = async (options) => {
38911
+ const result = await lifecycleMethod(options.publisher, "pruneUpdates")({
38912
+ appId: options.appId,
38913
+ ...options.apply === undefined ? {} : { apply: options.apply },
38914
+ ...options.gracePeriodMs === undefined ? {} : { gracePeriodMs: options.gracePeriodMs },
38915
+ ...options.minAgeMs === undefined ? {} : { minAgeMs: options.minAgeMs },
38916
+ ...options.retainRecent === undefined ? {} : { retainRecent: options.retainRecent },
38917
+ ...options.signal ? { signal: options.signal } : {}
38918
+ });
38919
+ validateStorageIdentity(result, options.appId);
38920
+ if (!Array.isArray(result.marked) || !Array.isArray(result.restored) || !Array.isArray(result.swept) || typeof result.dryRun !== "boolean" || !Number.isSafeInteger(result.reclaimedBytes) || result.reclaimedBytes < 0)
38921
+ throw new TypeError("Mobile update registry returned an invalid collection report.");
38922
+ return result;
38923
+ };
38663
38924
  var object5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
38664
38925
  var isPublisher2 = (value) => object5(value) && typeof value.publishUpdate === "function" && typeof value.promoteUpdate === "function" && typeof value.rollbackUpdate === "function";
38665
38926
  var projectPath2 = (projectRoot, requested, label) => {
@@ -38879,6 +39140,7 @@ export {
38879
39140
  inspectAbsoluteAndroidInstalledApp,
38880
39141
  inspectAbsoluteMobileRouteMetadata,
38881
39142
  inspectAbsoluteMobileUpdateServer,
39143
+ inspectAbsoluteMobileUpdateStorage,
38882
39144
  inspectAbsoluteRemoteMac,
38883
39145
  inspectAbsoluteRemoteMacLanHost,
38884
39146
  inspectAbsoluteRemoteMacWorkspace,
@@ -38936,6 +39198,7 @@ export {
38936
39198
  projectUsesAbsoluteAuth,
38937
39199
  projectUsesAbsoluteSync,
38938
39200
  promoteAbsoluteMobileUpdate,
39201
+ pruneAbsoluteMobileUpdates,
38939
39202
  publishAbsoluteAndroidRelease,
38940
39203
  publishAbsoluteIosRelease,
38941
39204
  publishAbsoluteMobileUpdate,
@@ -38984,5 +39247,5 @@ export {
38984
39247
  writeAbsoluteMobileUpdateRegistry
38985
39248
  };
38986
39249
 
38987
- //# debugId=A519D38D860E371564756E2164756E21
39250
+ //# debugId=FB53026A710DD13464756E2164756E21
38988
39251
  //# sourceMappingURL=index.js.map