@le-space/ui 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/ui",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Floating Aleph sponsor relay UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,8 +36,8 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@le-space/browser": "0.4.2",
40
- "@le-space/core": "0.4.2",
39
+ "@le-space/browser": "0.4.3",
40
+ "@le-space/core": "0.4.3",
41
41
  "viem": "^2.38.5"
42
42
  },
43
43
  "peerDependencies": {
package/react/index.d.ts CHANGED
@@ -256,6 +256,8 @@ interface CompactBootstrapRegistrationRecord {
256
256
  registrationId?: string;
257
257
  multiaddrs: string[];
258
258
  browserMultiaddrs?: string[];
259
+ ownerAddress?: string;
260
+ publisherAddress?: string;
259
261
  updatedAt: number;
260
262
  } | null;
261
263
  }
@@ -320,6 +322,7 @@ declare class SponsorRelayController {
320
322
  private patch;
321
323
  private emitProgress;
322
324
  private configureRelayBootstrapRegistration;
325
+ private cleanupStaleVmBootstrapConfigHistory;
323
326
  private syncLatestDeploymentProgress;
324
327
  private canSkipRuntimeRefresh;
325
328
  private noteRuntimeRefreshResult;
package/react/index.js CHANGED
@@ -28,7 +28,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
28
28
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
29
29
 
30
30
  // src/shared/package-version.ts
31
- var UI_PACKAGE_VERSION = "0.4.2";
31
+ var UI_PACKAGE_VERSION = "0.4.3";
32
32
 
33
33
  // src/shared/format.ts
34
34
  function shortHash(value, head = 8, tail = 6) {
@@ -1741,14 +1741,15 @@ function describeRuntimeAvailability(runtime) {
1741
1741
  const mappedPorts2 = runtime.mappedPorts ?? {};
1742
1742
  const hostIpv4 = runtime.hostIpv4 ?? null;
1743
1743
  const ipv6 = runtime.ipv6 ?? null;
1744
+ const executionGuestIpv6 = execution?.networking?.ipv6_ip ?? null;
1744
1745
  const proxyUrl = runtime.proxyUrl ?? null;
1745
1746
  const schedulerSource = runtime.allocation?.source ?? null;
1746
1747
  const webAccessActive = runtime.webAccess?.active ?? null;
1747
1748
  const mappedPortCount = Object.keys(mappedPorts2).length;
1748
- if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && !isLikelyGlobalIpv6(ipv6)) {
1749
+ if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && executionGuestIpv6 && !isLikelyGlobalIpv6(executionGuestIpv6)) {
1749
1750
  return {
1750
1751
  state: "execution-invalid-public-ipv6",
1751
- reason: `The CRN exposed runtime networking, but the guest IPv6${ipv6 ? ` (${ipv6})` : ""} is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1752
+ reason: `The CRN exposed runtime networking, but the guest IPv6 (${executionGuestIpv6}) is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1752
1753
  schedulerSource,
1753
1754
  executionSeen: Boolean(execution),
1754
1755
  webAccessActive,
@@ -2698,6 +2699,7 @@ async function publishRelayBootstrapRegistration(args) {
2698
2699
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
2699
2700
  var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
2700
2701
  var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
2702
+ var VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS = 6 * 60 * 60 * 1e3;
2701
2703
  function parseJsonObject(value) {
2702
2704
  try {
2703
2705
  const parsed = JSON.parse(value);
@@ -2706,6 +2708,44 @@ function parseJsonObject(value) {
2706
2708
  return null;
2707
2709
  }
2708
2710
  }
2711
+ function parseTimestampMs(value) {
2712
+ if (typeof value === "number" && Number.isFinite(value)) {
2713
+ return value > 1e12 ? value : value * 1e3;
2714
+ }
2715
+ if (typeof value === "string" && value.trim()) {
2716
+ const numeric = Number(value);
2717
+ if (Number.isFinite(numeric)) {
2718
+ return numeric > 1e12 ? numeric : numeric * 1e3;
2719
+ }
2720
+ const parsed = Date.parse(value);
2721
+ return Number.isFinite(parsed) ? parsed : null;
2722
+ }
2723
+ return null;
2724
+ }
2725
+ function normalizeVmBootstrapConfigRecord(deploymentTokenKey, value, options = {}) {
2726
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2727
+ const candidate = value;
2728
+ const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
2729
+ const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
2730
+ const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
2731
+ const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
2732
+ const createdAt = typeof candidate.createdAt === "string" ? candidate.createdAt.trim() : "";
2733
+ const expiresAt = typeof candidate.expiresAt === "string" ? candidate.expiresAt.trim() : "";
2734
+ if (!deploymentTokenKey.trim() || !deploymentToken || deploymentToken !== deploymentTokenKey.trim() || !profile || !ownerAddress || !instanceItemHash || !createdAt || !expiresAt) {
2735
+ return null;
2736
+ }
2737
+ const nowMs = Math.max(0, Number(options.nowMs ?? Date.now()));
2738
+ const maxRecordAgeMs = Math.max(
2739
+ 6e4,
2740
+ Number(options.maxRecordAgeMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
2741
+ );
2742
+ const createdAtMs = parseTimestampMs(createdAt);
2743
+ const expiresAtMs = parseTimestampMs(expiresAt);
2744
+ if (createdAtMs == null || expiresAtMs == null) return null;
2745
+ if (expiresAtMs <= nowMs) return null;
2746
+ if (createdAtMs < nowMs - maxRecordAgeMs) return null;
2747
+ return candidate;
2748
+ }
2709
2749
  async function fetchVmBootstrapConfigAggregate(address, options) {
2710
2750
  const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2711
2751
  requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
@@ -2719,16 +2759,21 @@ async function fetchVmBootstrapConfigAggregate(address, options) {
2719
2759
  if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return {};
2720
2760
  return aggregate;
2721
2761
  }
2722
- function normalizeVmBootstrapConfigAggregate(aggregate) {
2762
+ function normalizeVmBootstrapConfigAggregate(aggregate, options = {}) {
2723
2763
  if (!aggregate || typeof aggregate !== "object") return {};
2724
2764
  return Object.fromEntries(
2725
- Object.entries(aggregate).filter(
2726
- (entry) => Boolean(entry[1] && typeof entry[1] === "object" && !Array.isArray(entry[1]))
2727
- )
2765
+ Object.entries(aggregate).map(([deploymentToken, record]) => [
2766
+ deploymentToken,
2767
+ normalizeVmBootstrapConfigRecord(deploymentToken, record, options)
2768
+ ]).filter((entry) => entry[1] != null)
2728
2769
  );
2729
2770
  }
2730
2771
  function createVmBootstrapConfigAggregateContent(args) {
2731
- const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate);
2772
+ const nowMs = Math.max(0, Number(args.now ?? Date.now() / 1e3) * 1e3);
2773
+ const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate, {
2774
+ nowMs,
2775
+ maxRecordAgeMs: args.maxRecordAgeMs
2776
+ });
2732
2777
  content[args.record.deploymentToken] = args.record;
2733
2778
  return {
2734
2779
  address: args.sender,
@@ -2773,6 +2818,55 @@ async function publishVmBootstrapConfigAggregate(args) {
2773
2818
  aggregateStatus
2774
2819
  };
2775
2820
  }
2821
+ function normalizeAggregateMessageHashRecord(value) {
2822
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2823
+ const candidate = value;
2824
+ const itemHash = typeof candidate.item_hash === "string" ? candidate.item_hash.trim() : "";
2825
+ if (!itemHash) return null;
2826
+ const content = candidate.content && typeof candidate.content === "object" && !Array.isArray(candidate.content) ? candidate.content : typeof candidate.item_content === "string" ? parseJsonObject(candidate.item_content) : null;
2827
+ if (!content || content.key !== VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY) return null;
2828
+ const timeMs = parseTimestampMs(candidate.time);
2829
+ if (timeMs == null) return null;
2830
+ return { itemHash, timeMs };
2831
+ }
2832
+ async function listStaleVmBootstrapConfigAggregateMessageHashes(args) {
2833
+ const address = args.address.trim();
2834
+ if (!address) return [];
2835
+ const currentAggregateItemHash = args.currentAggregateItemHash?.trim() ?? "";
2836
+ const olderThanMs = Math.max(
2837
+ 6e4,
2838
+ Number(args.olderThanMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
2839
+ );
2840
+ const cutoffMs = Math.max(0, Number(args.nowMs ?? Date.now()) - olderThanMs);
2841
+ const pagination = Math.max(1, Number(args.pagination ?? 100));
2842
+ const maxPages = Math.max(1, Number(args.maxPages ?? 10));
2843
+ const hashes = /* @__PURE__ */ new Set();
2844
+ for (let page = 1; page <= maxPages; page += 1) {
2845
+ const requestUrl = new URL("/api/v0/messages.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2846
+ requestUrl.searchParams.set("msgType", "AGGREGATE");
2847
+ requestUrl.searchParams.set("addresses", address);
2848
+ requestUrl.searchParams.set("message_statuses", "processed,pending,rejected,removing");
2849
+ requestUrl.searchParams.set("pagination", String(pagination));
2850
+ requestUrl.searchParams.set("page", String(page));
2851
+ requestUrl.searchParams.set("sortOrder", "-1");
2852
+ const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
2853
+ if (!response.ok) {
2854
+ throw new Error(`VM bootstrap config aggregate history request failed: ${response.status}`);
2855
+ }
2856
+ const payload = await response.json();
2857
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
2858
+ if (messages.length === 0) break;
2859
+ for (const message of messages) {
2860
+ const normalized = normalizeAggregateMessageHashRecord(message);
2861
+ if (!normalized) continue;
2862
+ if (normalized.itemHash === currentAggregateItemHash) continue;
2863
+ if (normalized.timeMs > cutoffMs) continue;
2864
+ hashes.add(normalized.itemHash);
2865
+ }
2866
+ if (messages.length < pagination) break;
2867
+ }
2868
+ return [...hashes];
2869
+ }
2776
2870
  async function publishVmBootstrapConfig(args) {
2777
2871
  const aggregate = await fetchVmBootstrapConfigAggregate(args.sender, {
2778
2872
  apiHost: args.apiHost,
@@ -2781,7 +2875,8 @@ async function publishVmBootstrapConfig(args) {
2781
2875
  const content = createVmBootstrapConfigAggregateContent({
2782
2876
  sender: args.sender,
2783
2877
  record: args.record,
2784
- existingAggregate: aggregate
2878
+ existingAggregate: aggregate,
2879
+ maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
2785
2880
  });
2786
2881
  const result = await publishVmBootstrapConfigAggregate({
2787
2882
  sender: args.sender,
@@ -2803,7 +2898,9 @@ async function deleteVmBootstrapConfig(args) {
2803
2898
  apiHost: args.apiHost,
2804
2899
  fetch: args.fetch
2805
2900
  });
2806
- const content = normalizeVmBootstrapConfigAggregate(aggregate);
2901
+ const content = normalizeVmBootstrapConfigAggregate(aggregate, {
2902
+ maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
2903
+ });
2807
2904
  delete content[args.deploymentToken];
2808
2905
  const publishResult = await publishVmBootstrapConfigAggregate({
2809
2906
  sender: args.sender,
@@ -3167,6 +3264,22 @@ function relayBootstrapInstanceItemHash(entry) {
3167
3264
  }
3168
3265
  return registrationIdInstanceItemHash(entry?.content?.registrationId);
3169
3266
  }
3267
+ function relayBootstrapMatchesCurrentWallet(entry, normalizedWalletAddress, activeInstanceHashes) {
3268
+ const instanceItemHash = relayBootstrapInstanceItemHash(entry);
3269
+ if (instanceItemHash && activeInstanceHashes.has(instanceItemHash)) {
3270
+ return true;
3271
+ }
3272
+ const candidateAddresses = [
3273
+ entry?.address,
3274
+ entry?.content?.ownerAddress,
3275
+ entry?.content?.authorization?.payload?.ownerAddress,
3276
+ entry?.content?.publisherAddress,
3277
+ entry?.content?.authorization?.payload?.publisherAddress
3278
+ ];
3279
+ return candidateAddresses.some(
3280
+ (value) => typeof value === "string" && value.trim().toLowerCase() === normalizedWalletAddress
3281
+ );
3282
+ }
3170
3283
  async function inspectInstanceRuntime(args) {
3171
3284
  const details = {
3172
3285
  messageStatus: String(
@@ -3790,6 +3903,7 @@ var SponsorRelayController = class {
3790
3903
  }
3791
3904
  }
3792
3905
  if (profile === "uc-go-peer" && args.deploymentToken) {
3906
+ let cleanupAggregateItemHash = null;
3793
3907
  await deleteVmBootstrapConfig({
3794
3908
  sender: this.state.wallet.address,
3795
3909
  deploymentToken: args.deploymentToken,
@@ -3798,6 +3912,14 @@ var SponsorRelayController = class {
3798
3912
  fetch: asJsonFetch,
3799
3913
  apiHost: this.client.apiHost,
3800
3914
  sync: true
3915
+ }).then((cleanupResult) => {
3916
+ cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
3917
+ this.trace("deploy:bootstrap-config-cleanup-complete", {
3918
+ itemHash: args.itemHash,
3919
+ deploymentToken: args.deploymentToken,
3920
+ aggregateItemHash: cleanupResult.aggregateItemHash,
3921
+ aggregateStatus: cleanupResult.aggregateStatus
3922
+ });
3801
3923
  }).catch((error) => {
3802
3924
  this.trace("deploy:bootstrap-config-cleanup-error", {
3803
3925
  itemHash: args.itemHash,
@@ -3805,8 +3927,75 @@ var SponsorRelayController = class {
3805
3927
  error: error instanceof Error ? error.message : String(error)
3806
3928
  });
3807
3929
  });
3930
+ await this.cleanupStaleVmBootstrapConfigHistory({
3931
+ itemHash: args.itemHash,
3932
+ currentAggregateItemHash: cleanupAggregateItemHash,
3933
+ stage: "publishing-bootstrap",
3934
+ progress: 99
3935
+ });
3808
3936
  }
3809
3937
  }
3938
+ async cleanupStaleVmBootstrapConfigHistory(args) {
3939
+ if (!this.state.wallet.address || !args.currentAggregateItemHash) {
3940
+ return;
3941
+ }
3942
+ const staleHashes = await listStaleVmBootstrapConfigAggregateMessageHashes({
3943
+ address: this.state.wallet.address,
3944
+ currentAggregateItemHash: args.currentAggregateItemHash,
3945
+ olderThanMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS,
3946
+ fetch: asJsonFetch,
3947
+ apiHost: this.client.apiHost
3948
+ }).catch((error) => {
3949
+ this.trace("deploy:bootstrap-config-history-scan-error", {
3950
+ itemHash: args.itemHash,
3951
+ currentAggregateItemHash: args.currentAggregateItemHash,
3952
+ error: error instanceof Error ? error.message : String(error)
3953
+ });
3954
+ return [];
3955
+ });
3956
+ if (staleHashes.length === 0) {
3957
+ return;
3958
+ }
3959
+ const staleCount = staleHashes.length;
3960
+ this.emitProgress({
3961
+ stage: args.stage,
3962
+ label: "Cleaning up stale handoff history",
3963
+ progress: args.progress,
3964
+ status: "warning",
3965
+ itemHash: args.itemHash,
3966
+ detail: `Forgetting ${staleCount} superseded Aleph handoff aggregate message${staleCount === 1 ? "" : "s"} older than 6 hours. MetaMask may request another cleanup signature.`
3967
+ });
3968
+ this.trace("deploy:bootstrap-config-history-cleanup-start", {
3969
+ itemHash: args.itemHash,
3970
+ currentAggregateItemHash: args.currentAggregateItemHash,
3971
+ staleHashes
3972
+ });
3973
+ await forgetAlephMessages({
3974
+ sender: this.state.wallet.address,
3975
+ hashes: staleHashes,
3976
+ reason: "Prune stale vm-bootstrap-config aggregate history older than 6 hours",
3977
+ signer: personalSign,
3978
+ hasher: sha256Hex,
3979
+ fetch: asJsonFetch,
3980
+ apiHost: this.client.apiHost,
3981
+ sync: true
3982
+ }).then((cleanupResult) => {
3983
+ this.trace("deploy:bootstrap-config-history-cleanup-complete", {
3984
+ itemHash: args.itemHash,
3985
+ currentAggregateItemHash: args.currentAggregateItemHash,
3986
+ staleHashes,
3987
+ forgetItemHash: cleanupResult.itemHash,
3988
+ forgetStatus: cleanupResult.status
3989
+ });
3990
+ }).catch((error) => {
3991
+ this.trace("deploy:bootstrap-config-history-cleanup-error", {
3992
+ itemHash: args.itemHash,
3993
+ currentAggregateItemHash: args.currentAggregateItemHash,
3994
+ staleHashes,
3995
+ error: error instanceof Error ? error.message : String(error)
3996
+ });
3997
+ });
3998
+ }
3810
3999
  syncLatestDeploymentProgress(instances) {
3811
4000
  const itemHash = this.state.lastDeploymentHash;
3812
4001
  if (!itemHash) {
@@ -4182,7 +4371,11 @@ var SponsorRelayController = class {
4182
4371
  instances.map((entry) => entry.instance.item_hash)
4183
4372
  );
4184
4373
  bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
4185
- (entry) => entry.address?.toLowerCase() === normalizedWalletAddress
4374
+ (entry) => relayBootstrapMatchesCurrentWallet(
4375
+ entry,
4376
+ normalizedWalletAddress,
4377
+ activeInstanceHashes
4378
+ )
4186
4379
  ).map((entry) => {
4187
4380
  const instanceItemHash = relayBootstrapInstanceItemHash(entry);
4188
4381
  const messageHash = entry.itemHash ?? entry.hash ?? null;
@@ -4201,6 +4394,8 @@ var SponsorRelayController = class {
4201
4394
  registrationId: entry.content.registrationId,
4202
4395
  multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
4203
4396
  browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
4397
+ ownerAddress: typeof entry.content.ownerAddress === "string" ? entry.content.ownerAddress : void 0,
4398
+ publisherAddress: typeof entry.content.publisherAddress === "string" ? entry.content.publisherAddress : void 0,
4204
4399
  updatedAt: entry.content.updatedAt
4205
4400
  } : null
4206
4401
  };
@@ -4572,6 +4767,7 @@ var SponsorRelayController = class {
4572
4767
  detail: failedDueToInvalidIpv6 ? `${attemptLabel} exposed runtime networking that cannot support proxy-backed HTTPS. Cleaning up this attempt before retrying another CRN.` : `${attemptLabel} failed: ${lastError.message} Cleaning up this attempt before retrying another CRN.`
4573
4768
  });
4574
4769
  if (attemptDeploymentToken) {
4770
+ let cleanupAggregateItemHash = null;
4575
4771
  this.emitProgress({
4576
4772
  stage: "deployment-confirmed",
4577
4773
  label: "Cleaning up failed attempt",
@@ -4593,6 +4789,7 @@ var SponsorRelayController = class {
4593
4789
  apiHost: this.client.apiHost,
4594
4790
  sync: true
4595
4791
  }).then((cleanupResult) => {
4792
+ cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
4596
4793
  this.trace("deploy:bootstrap-config-cleanup-complete", {
4597
4794
  itemHash: attemptItemHash,
4598
4795
  deploymentToken: attemptDeploymentToken,
@@ -4606,6 +4803,12 @@ var SponsorRelayController = class {
4606
4803
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4607
4804
  });
4608
4805
  });
4806
+ await this.cleanupStaleVmBootstrapConfigHistory({
4807
+ itemHash: attemptItemHash,
4808
+ currentAggregateItemHash: cleanupAggregateItemHash,
4809
+ stage: "deployment-confirmed",
4810
+ progress: 18
4811
+ });
4609
4812
  }
4610
4813
  this.emitProgress({
4611
4814
  stage: "deployment-confirmed",
package/shared/index.d.ts CHANGED
@@ -254,6 +254,8 @@ interface CompactBootstrapRegistrationRecord {
254
254
  registrationId?: string;
255
255
  multiaddrs: string[];
256
256
  browserMultiaddrs?: string[];
257
+ ownerAddress?: string;
258
+ publisherAddress?: string;
257
259
  updatedAt: number;
258
260
  } | null;
259
261
  }
@@ -309,7 +311,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
309
311
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
310
312
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
311
313
 
312
- declare const UI_PACKAGE_VERSION = "0.4.2";
314
+ declare const UI_PACKAGE_VERSION = "0.4.3";
313
315
 
314
316
  interface RootfsRequiredPortForward {
315
317
  port: number;
@@ -368,6 +370,7 @@ declare class SponsorRelayController {
368
370
  private patch;
369
371
  private emitProgress;
370
372
  private configureRelayBootstrapRegistration;
373
+ private cleanupStaleVmBootstrapConfigHistory;
371
374
  private syncLatestDeploymentProgress;
372
375
  private canSkipRuntimeRefresh;
373
376
  private noteRuntimeRefreshResult;
package/shared/index.js CHANGED
@@ -27,7 +27,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
27
27
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
28
28
 
29
29
  // src/shared/package-version.ts
30
- var UI_PACKAGE_VERSION = "0.4.2";
30
+ var UI_PACKAGE_VERSION = "0.4.3";
31
31
 
32
32
  // src/shared/format.ts
33
33
  function shortHash(value, head = 8, tail = 6) {
@@ -1740,14 +1740,15 @@ function describeRuntimeAvailability(runtime) {
1740
1740
  const mappedPorts2 = runtime.mappedPorts ?? {};
1741
1741
  const hostIpv4 = runtime.hostIpv4 ?? null;
1742
1742
  const ipv6 = runtime.ipv6 ?? null;
1743
+ const executionGuestIpv6 = execution?.networking?.ipv6_ip ?? null;
1743
1744
  const proxyUrl = runtime.proxyUrl ?? null;
1744
1745
  const schedulerSource = runtime.allocation?.source ?? null;
1745
1746
  const webAccessActive = runtime.webAccess?.active ?? null;
1746
1747
  const mappedPortCount = Object.keys(mappedPorts2).length;
1747
- if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && !isLikelyGlobalIpv6(ipv6)) {
1748
+ if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && executionGuestIpv6 && !isLikelyGlobalIpv6(executionGuestIpv6)) {
1748
1749
  return {
1749
1750
  state: "execution-invalid-public-ipv6",
1750
- reason: `The CRN exposed runtime networking, but the guest IPv6${ipv6 ? ` (${ipv6})` : ""} is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1751
+ reason: `The CRN exposed runtime networking, but the guest IPv6 (${executionGuestIpv6}) is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1751
1752
  schedulerSource,
1752
1753
  executionSeen: Boolean(execution),
1753
1754
  webAccessActive,
@@ -2697,6 +2698,7 @@ async function publishRelayBootstrapRegistration(args) {
2697
2698
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
2698
2699
  var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
2699
2700
  var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
2701
+ var VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS = 6 * 60 * 60 * 1e3;
2700
2702
  function parseJsonObject(value) {
2701
2703
  try {
2702
2704
  const parsed = JSON.parse(value);
@@ -2705,6 +2707,44 @@ function parseJsonObject(value) {
2705
2707
  return null;
2706
2708
  }
2707
2709
  }
2710
+ function parseTimestampMs(value) {
2711
+ if (typeof value === "number" && Number.isFinite(value)) {
2712
+ return value > 1e12 ? value : value * 1e3;
2713
+ }
2714
+ if (typeof value === "string" && value.trim()) {
2715
+ const numeric = Number(value);
2716
+ if (Number.isFinite(numeric)) {
2717
+ return numeric > 1e12 ? numeric : numeric * 1e3;
2718
+ }
2719
+ const parsed = Date.parse(value);
2720
+ return Number.isFinite(parsed) ? parsed : null;
2721
+ }
2722
+ return null;
2723
+ }
2724
+ function normalizeVmBootstrapConfigRecord(deploymentTokenKey, value, options = {}) {
2725
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2726
+ const candidate = value;
2727
+ const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
2728
+ const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
2729
+ const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
2730
+ const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
2731
+ const createdAt = typeof candidate.createdAt === "string" ? candidate.createdAt.trim() : "";
2732
+ const expiresAt = typeof candidate.expiresAt === "string" ? candidate.expiresAt.trim() : "";
2733
+ if (!deploymentTokenKey.trim() || !deploymentToken || deploymentToken !== deploymentTokenKey.trim() || !profile || !ownerAddress || !instanceItemHash || !createdAt || !expiresAt) {
2734
+ return null;
2735
+ }
2736
+ const nowMs = Math.max(0, Number(options.nowMs ?? Date.now()));
2737
+ const maxRecordAgeMs = Math.max(
2738
+ 6e4,
2739
+ Number(options.maxRecordAgeMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
2740
+ );
2741
+ const createdAtMs = parseTimestampMs(createdAt);
2742
+ const expiresAtMs = parseTimestampMs(expiresAt);
2743
+ if (createdAtMs == null || expiresAtMs == null) return null;
2744
+ if (expiresAtMs <= nowMs) return null;
2745
+ if (createdAtMs < nowMs - maxRecordAgeMs) return null;
2746
+ return candidate;
2747
+ }
2708
2748
  async function fetchVmBootstrapConfigAggregate(address, options) {
2709
2749
  const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2710
2750
  requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
@@ -2718,16 +2758,21 @@ async function fetchVmBootstrapConfigAggregate(address, options) {
2718
2758
  if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return {};
2719
2759
  return aggregate;
2720
2760
  }
2721
- function normalizeVmBootstrapConfigAggregate(aggregate) {
2761
+ function normalizeVmBootstrapConfigAggregate(aggregate, options = {}) {
2722
2762
  if (!aggregate || typeof aggregate !== "object") return {};
2723
2763
  return Object.fromEntries(
2724
- Object.entries(aggregate).filter(
2725
- (entry) => Boolean(entry[1] && typeof entry[1] === "object" && !Array.isArray(entry[1]))
2726
- )
2764
+ Object.entries(aggregate).map(([deploymentToken, record]) => [
2765
+ deploymentToken,
2766
+ normalizeVmBootstrapConfigRecord(deploymentToken, record, options)
2767
+ ]).filter((entry) => entry[1] != null)
2727
2768
  );
2728
2769
  }
2729
2770
  function createVmBootstrapConfigAggregateContent(args) {
2730
- const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate);
2771
+ const nowMs = Math.max(0, Number(args.now ?? Date.now() / 1e3) * 1e3);
2772
+ const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate, {
2773
+ nowMs,
2774
+ maxRecordAgeMs: args.maxRecordAgeMs
2775
+ });
2731
2776
  content[args.record.deploymentToken] = args.record;
2732
2777
  return {
2733
2778
  address: args.sender,
@@ -2772,6 +2817,55 @@ async function publishVmBootstrapConfigAggregate(args) {
2772
2817
  aggregateStatus
2773
2818
  };
2774
2819
  }
2820
+ function normalizeAggregateMessageHashRecord(value) {
2821
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2822
+ const candidate = value;
2823
+ const itemHash = typeof candidate.item_hash === "string" ? candidate.item_hash.trim() : "";
2824
+ if (!itemHash) return null;
2825
+ const content = candidate.content && typeof candidate.content === "object" && !Array.isArray(candidate.content) ? candidate.content : typeof candidate.item_content === "string" ? parseJsonObject(candidate.item_content) : null;
2826
+ if (!content || content.key !== VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY) return null;
2827
+ const timeMs = parseTimestampMs(candidate.time);
2828
+ if (timeMs == null) return null;
2829
+ return { itemHash, timeMs };
2830
+ }
2831
+ async function listStaleVmBootstrapConfigAggregateMessageHashes(args) {
2832
+ const address = args.address.trim();
2833
+ if (!address) return [];
2834
+ const currentAggregateItemHash = args.currentAggregateItemHash?.trim() ?? "";
2835
+ const olderThanMs = Math.max(
2836
+ 6e4,
2837
+ Number(args.olderThanMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
2838
+ );
2839
+ const cutoffMs = Math.max(0, Number(args.nowMs ?? Date.now()) - olderThanMs);
2840
+ const pagination = Math.max(1, Number(args.pagination ?? 100));
2841
+ const maxPages = Math.max(1, Number(args.maxPages ?? 10));
2842
+ const hashes = /* @__PURE__ */ new Set();
2843
+ for (let page = 1; page <= maxPages; page += 1) {
2844
+ const requestUrl = new URL("/api/v0/messages.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2845
+ requestUrl.searchParams.set("msgType", "AGGREGATE");
2846
+ requestUrl.searchParams.set("addresses", address);
2847
+ requestUrl.searchParams.set("message_statuses", "processed,pending,rejected,removing");
2848
+ requestUrl.searchParams.set("pagination", String(pagination));
2849
+ requestUrl.searchParams.set("page", String(page));
2850
+ requestUrl.searchParams.set("sortOrder", "-1");
2851
+ const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
2852
+ if (!response.ok) {
2853
+ throw new Error(`VM bootstrap config aggregate history request failed: ${response.status}`);
2854
+ }
2855
+ const payload = await response.json();
2856
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
2857
+ if (messages.length === 0) break;
2858
+ for (const message of messages) {
2859
+ const normalized = normalizeAggregateMessageHashRecord(message);
2860
+ if (!normalized) continue;
2861
+ if (normalized.itemHash === currentAggregateItemHash) continue;
2862
+ if (normalized.timeMs > cutoffMs) continue;
2863
+ hashes.add(normalized.itemHash);
2864
+ }
2865
+ if (messages.length < pagination) break;
2866
+ }
2867
+ return [...hashes];
2868
+ }
2775
2869
  async function publishVmBootstrapConfig(args) {
2776
2870
  const aggregate = await fetchVmBootstrapConfigAggregate(args.sender, {
2777
2871
  apiHost: args.apiHost,
@@ -2780,7 +2874,8 @@ async function publishVmBootstrapConfig(args) {
2780
2874
  const content = createVmBootstrapConfigAggregateContent({
2781
2875
  sender: args.sender,
2782
2876
  record: args.record,
2783
- existingAggregate: aggregate
2877
+ existingAggregate: aggregate,
2878
+ maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
2784
2879
  });
2785
2880
  const result = await publishVmBootstrapConfigAggregate({
2786
2881
  sender: args.sender,
@@ -2802,7 +2897,9 @@ async function deleteVmBootstrapConfig(args) {
2802
2897
  apiHost: args.apiHost,
2803
2898
  fetch: args.fetch
2804
2899
  });
2805
- const content = normalizeVmBootstrapConfigAggregate(aggregate);
2900
+ const content = normalizeVmBootstrapConfigAggregate(aggregate, {
2901
+ maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
2902
+ });
2806
2903
  delete content[args.deploymentToken];
2807
2904
  const publishResult = await publishVmBootstrapConfigAggregate({
2808
2905
  sender: args.sender,
@@ -3166,6 +3263,22 @@ function relayBootstrapInstanceItemHash(entry) {
3166
3263
  }
3167
3264
  return registrationIdInstanceItemHash(entry?.content?.registrationId);
3168
3265
  }
3266
+ function relayBootstrapMatchesCurrentWallet(entry, normalizedWalletAddress, activeInstanceHashes) {
3267
+ const instanceItemHash = relayBootstrapInstanceItemHash(entry);
3268
+ if (instanceItemHash && activeInstanceHashes.has(instanceItemHash)) {
3269
+ return true;
3270
+ }
3271
+ const candidateAddresses = [
3272
+ entry?.address,
3273
+ entry?.content?.ownerAddress,
3274
+ entry?.content?.authorization?.payload?.ownerAddress,
3275
+ entry?.content?.publisherAddress,
3276
+ entry?.content?.authorization?.payload?.publisherAddress
3277
+ ];
3278
+ return candidateAddresses.some(
3279
+ (value) => typeof value === "string" && value.trim().toLowerCase() === normalizedWalletAddress
3280
+ );
3281
+ }
3169
3282
  async function inspectInstanceRuntime(args) {
3170
3283
  const details = {
3171
3284
  messageStatus: String(
@@ -3789,6 +3902,7 @@ var SponsorRelayController = class {
3789
3902
  }
3790
3903
  }
3791
3904
  if (profile === "uc-go-peer" && args.deploymentToken) {
3905
+ let cleanupAggregateItemHash = null;
3792
3906
  await deleteVmBootstrapConfig({
3793
3907
  sender: this.state.wallet.address,
3794
3908
  deploymentToken: args.deploymentToken,
@@ -3797,6 +3911,14 @@ var SponsorRelayController = class {
3797
3911
  fetch: asJsonFetch,
3798
3912
  apiHost: this.client.apiHost,
3799
3913
  sync: true
3914
+ }).then((cleanupResult) => {
3915
+ cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
3916
+ this.trace("deploy:bootstrap-config-cleanup-complete", {
3917
+ itemHash: args.itemHash,
3918
+ deploymentToken: args.deploymentToken,
3919
+ aggregateItemHash: cleanupResult.aggregateItemHash,
3920
+ aggregateStatus: cleanupResult.aggregateStatus
3921
+ });
3800
3922
  }).catch((error) => {
3801
3923
  this.trace("deploy:bootstrap-config-cleanup-error", {
3802
3924
  itemHash: args.itemHash,
@@ -3804,8 +3926,75 @@ var SponsorRelayController = class {
3804
3926
  error: error instanceof Error ? error.message : String(error)
3805
3927
  });
3806
3928
  });
3929
+ await this.cleanupStaleVmBootstrapConfigHistory({
3930
+ itemHash: args.itemHash,
3931
+ currentAggregateItemHash: cleanupAggregateItemHash,
3932
+ stage: "publishing-bootstrap",
3933
+ progress: 99
3934
+ });
3807
3935
  }
3808
3936
  }
3937
+ async cleanupStaleVmBootstrapConfigHistory(args) {
3938
+ if (!this.state.wallet.address || !args.currentAggregateItemHash) {
3939
+ return;
3940
+ }
3941
+ const staleHashes = await listStaleVmBootstrapConfigAggregateMessageHashes({
3942
+ address: this.state.wallet.address,
3943
+ currentAggregateItemHash: args.currentAggregateItemHash,
3944
+ olderThanMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS,
3945
+ fetch: asJsonFetch,
3946
+ apiHost: this.client.apiHost
3947
+ }).catch((error) => {
3948
+ this.trace("deploy:bootstrap-config-history-scan-error", {
3949
+ itemHash: args.itemHash,
3950
+ currentAggregateItemHash: args.currentAggregateItemHash,
3951
+ error: error instanceof Error ? error.message : String(error)
3952
+ });
3953
+ return [];
3954
+ });
3955
+ if (staleHashes.length === 0) {
3956
+ return;
3957
+ }
3958
+ const staleCount = staleHashes.length;
3959
+ this.emitProgress({
3960
+ stage: args.stage,
3961
+ label: "Cleaning up stale handoff history",
3962
+ progress: args.progress,
3963
+ status: "warning",
3964
+ itemHash: args.itemHash,
3965
+ detail: `Forgetting ${staleCount} superseded Aleph handoff aggregate message${staleCount === 1 ? "" : "s"} older than 6 hours. MetaMask may request another cleanup signature.`
3966
+ });
3967
+ this.trace("deploy:bootstrap-config-history-cleanup-start", {
3968
+ itemHash: args.itemHash,
3969
+ currentAggregateItemHash: args.currentAggregateItemHash,
3970
+ staleHashes
3971
+ });
3972
+ await forgetAlephMessages({
3973
+ sender: this.state.wallet.address,
3974
+ hashes: staleHashes,
3975
+ reason: "Prune stale vm-bootstrap-config aggregate history older than 6 hours",
3976
+ signer: personalSign,
3977
+ hasher: sha256Hex,
3978
+ fetch: asJsonFetch,
3979
+ apiHost: this.client.apiHost,
3980
+ sync: true
3981
+ }).then((cleanupResult) => {
3982
+ this.trace("deploy:bootstrap-config-history-cleanup-complete", {
3983
+ itemHash: args.itemHash,
3984
+ currentAggregateItemHash: args.currentAggregateItemHash,
3985
+ staleHashes,
3986
+ forgetItemHash: cleanupResult.itemHash,
3987
+ forgetStatus: cleanupResult.status
3988
+ });
3989
+ }).catch((error) => {
3990
+ this.trace("deploy:bootstrap-config-history-cleanup-error", {
3991
+ itemHash: args.itemHash,
3992
+ currentAggregateItemHash: args.currentAggregateItemHash,
3993
+ staleHashes,
3994
+ error: error instanceof Error ? error.message : String(error)
3995
+ });
3996
+ });
3997
+ }
3809
3998
  syncLatestDeploymentProgress(instances) {
3810
3999
  const itemHash = this.state.lastDeploymentHash;
3811
4000
  if (!itemHash) {
@@ -4181,7 +4370,11 @@ var SponsorRelayController = class {
4181
4370
  instances.map((entry) => entry.instance.item_hash)
4182
4371
  );
4183
4372
  bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
4184
- (entry) => entry.address?.toLowerCase() === normalizedWalletAddress
4373
+ (entry) => relayBootstrapMatchesCurrentWallet(
4374
+ entry,
4375
+ normalizedWalletAddress,
4376
+ activeInstanceHashes
4377
+ )
4185
4378
  ).map((entry) => {
4186
4379
  const instanceItemHash = relayBootstrapInstanceItemHash(entry);
4187
4380
  const messageHash = entry.itemHash ?? entry.hash ?? null;
@@ -4200,6 +4393,8 @@ var SponsorRelayController = class {
4200
4393
  registrationId: entry.content.registrationId,
4201
4394
  multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
4202
4395
  browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
4396
+ ownerAddress: typeof entry.content.ownerAddress === "string" ? entry.content.ownerAddress : void 0,
4397
+ publisherAddress: typeof entry.content.publisherAddress === "string" ? entry.content.publisherAddress : void 0,
4203
4398
  updatedAt: entry.content.updatedAt
4204
4399
  } : null
4205
4400
  };
@@ -4571,6 +4766,7 @@ var SponsorRelayController = class {
4571
4766
  detail: failedDueToInvalidIpv6 ? `${attemptLabel} exposed runtime networking that cannot support proxy-backed HTTPS. Cleaning up this attempt before retrying another CRN.` : `${attemptLabel} failed: ${lastError.message} Cleaning up this attempt before retrying another CRN.`
4572
4767
  });
4573
4768
  if (attemptDeploymentToken) {
4769
+ let cleanupAggregateItemHash = null;
4574
4770
  this.emitProgress({
4575
4771
  stage: "deployment-confirmed",
4576
4772
  label: "Cleaning up failed attempt",
@@ -4592,6 +4788,7 @@ var SponsorRelayController = class {
4592
4788
  apiHost: this.client.apiHost,
4593
4789
  sync: true
4594
4790
  }).then((cleanupResult) => {
4791
+ cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
4595
4792
  this.trace("deploy:bootstrap-config-cleanup-complete", {
4596
4793
  itemHash: attemptItemHash,
4597
4794
  deploymentToken: attemptDeploymentToken,
@@ -4605,6 +4802,12 @@ var SponsorRelayController = class {
4605
4802
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4606
4803
  });
4607
4804
  });
4805
+ await this.cleanupStaleVmBootstrapConfigHistory({
4806
+ itemHash: attemptItemHash,
4807
+ currentAggregateItemHash: cleanupAggregateItemHash,
4808
+ stage: "deployment-confirmed",
4809
+ progress: 18
4810
+ });
4608
4811
  }
4609
4812
  this.emitProgress({
4610
4813
  stage: "deployment-confirmed",