@le-space/ui 0.3.30 → 0.3.32

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.3.30",
3
+ "version": "0.3.32",
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.3.30",
40
- "@le-space/core": "0.3.30",
39
+ "@le-space/browser": "0.3.32",
40
+ "@le-space/core": "0.3.32",
41
41
  "viem": "^2.38.5"
42
42
  },
43
43
  "peerDependencies": {
package/react/index.d.ts CHANGED
@@ -243,6 +243,22 @@ interface CompactInstanceRecord {
243
243
  instance: InstanceMessage;
244
244
  details: CompactInstanceDetails;
245
245
  }
246
+ interface CompactBootstrapRegistrationRecord {
247
+ messageHash: string | null;
248
+ hash: string | null;
249
+ itemHash: string | null;
250
+ address: string | null;
251
+ time: number | null;
252
+ instanceItemHash: string | null;
253
+ confirmed: boolean;
254
+ content: {
255
+ peerId: string;
256
+ registrationId?: string;
257
+ multiaddrs: string[];
258
+ browserMultiaddrs?: string[];
259
+ updatedAt: number;
260
+ } | null;
261
+ }
246
262
  interface SponsorRelayState {
247
263
  ready: boolean;
248
264
  open: boolean;
@@ -260,6 +276,7 @@ interface SponsorRelayState {
260
276
  refreshing: boolean;
261
277
  deploying: boolean;
262
278
  deletingInstanceHash: string | null;
279
+ deletingRegistrationHash: string | null;
263
280
  };
264
281
  statusText: string;
265
282
  errorText: string | null;
@@ -273,6 +290,8 @@ interface SponsorRelayState {
273
290
  crns: Crn[];
274
291
  selectedCrn: Crn | null;
275
292
  instances: CompactInstanceRecord[];
293
+ bootstrapRegistrations: CompactBootstrapRegistrationRecord[];
294
+ orphanBootstrapRegistrations: CompactBootstrapRegistrationRecord[];
276
295
  lastDeploymentHash: string | null;
277
296
  deploymentProgress: DeploymentProgressEvent;
278
297
  }
@@ -324,6 +343,7 @@ declare class SponsorRelayController {
324
343
  refresh(): Promise<void>;
325
344
  deploy(): Promise<void>;
326
345
  deleteInstance(instanceHash: string): Promise<void>;
346
+ deleteBootstrapRegistration(registrationHash: string): Promise<void>;
327
347
  }
328
348
 
329
349
  declare function SponsorRelayFab(props: SponsorRelayProps): react_jsx_runtime.JSX.Element;
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.3.30";
31
+ var UI_PACKAGE_VERSION = "0.3.32";
32
32
 
33
33
  // src/shared/format.ts
34
34
  function shortHash(value, head = 8, tail = 6) {
@@ -2869,7 +2869,14 @@ function normalizeVmBootstrapConfigSignalRecord(value) {
2869
2869
  instanceItemHash,
2870
2870
  updatedAt,
2871
2871
  publisherAddress: typeof candidate.publisherAddress === "string" && candidate.publisherAddress.trim() ? candidate.publisherAddress.trim() : null,
2872
- authorization: candidate.authorization
2872
+ authorization: candidate.authorization,
2873
+ peerId: typeof candidate.peerId === "string" && candidate.peerId.trim() ? candidate.peerId.trim() : null,
2874
+ probeMultiaddrs: Array.isArray(candidate.probeMultiaddrs) ? candidate.probeMultiaddrs.filter(
2875
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2876
+ ) : [],
2877
+ browserBootstrapMultiaddrs: Array.isArray(candidate.browserBootstrapMultiaddrs) ? candidate.browserBootstrapMultiaddrs.filter(
2878
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2879
+ ) : []
2873
2880
  };
2874
2881
  }
2875
2882
  async function fetchVmBootstrapConfigSignals(args) {
@@ -3037,7 +3044,8 @@ function defaultState(props = {}) {
3037
3044
  connectingWallet: false,
3038
3045
  refreshing: false,
3039
3046
  deploying: false,
3040
- deletingInstanceHash: null
3047
+ deletingInstanceHash: null,
3048
+ deletingRegistrationHash: null
3041
3049
  },
3042
3050
  statusText: "Ready",
3043
3051
  errorText: null,
@@ -3063,6 +3071,8 @@ function defaultState(props = {}) {
3063
3071
  crns: [],
3064
3072
  selectedCrn: null,
3065
3073
  instances: [],
3074
+ bootstrapRegistrations: [],
3075
+ orphanBootstrapRegistrations: [],
3066
3076
  lastDeploymentHash: null,
3067
3077
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
3068
3078
  };
@@ -3167,6 +3177,20 @@ function relayProfileForManifest(manifest) {
3167
3177
  }
3168
3178
  return null;
3169
3179
  }
3180
+ function registrationIdInstanceItemHash(registrationId) {
3181
+ const normalized = String(registrationId ?? "").trim();
3182
+ if (!normalized) return null;
3183
+ const parts = normalized.split(":").map((part) => part.trim()).filter(Boolean);
3184
+ const candidate = parts.at(-1) ?? "";
3185
+ return /^[0-9a-f]{64}$/i.test(candidate) ? candidate : null;
3186
+ }
3187
+ function relayBootstrapInstanceItemHash(entry) {
3188
+ const authorizedInstanceItemHash = typeof entry?.content?.authorization?.payload?.instanceItemHash === "string" ? entry.content.authorization.payload.instanceItemHash.trim() : "";
3189
+ if (authorizedInstanceItemHash) {
3190
+ return authorizedInstanceItemHash;
3191
+ }
3192
+ return registrationIdInstanceItemHash(entry?.content?.registrationId);
3193
+ }
3170
3194
  async function inspectInstanceRuntime(args) {
3171
3195
  const details = {
3172
3196
  messageStatus: String(
@@ -3401,6 +3425,7 @@ var SponsorRelayController = class {
3401
3425
  if (!setupPort) {
3402
3426
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3403
3427
  }
3428
+ let bootstrapSignalRelayMetadata = null;
3404
3429
  if (profile === "orbitdb-relay-pinner") {
3405
3430
  this.emitProgress({
3406
3431
  stage: "deployment-confirmed",
@@ -3501,11 +3526,6 @@ var SponsorRelayController = class {
3501
3526
  }
3502
3527
  runtime = latestRuntime;
3503
3528
  mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3504
- if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3505
- throw new Error(
3506
- "Aleph reserved a 2n6 proxy URL, but it never became active in time. Aborting guest HTTPS configuration to avoid a broken Caddy setup."
3507
- );
3508
- }
3509
3529
  }
3510
3530
  if (!runtime.hostIpv4) {
3511
3531
  throw new Error("Relay runtime lost its host IPv4 address while waiting for HTTPS activation.");
@@ -3587,46 +3607,85 @@ var SponsorRelayController = class {
3587
3607
  "The relay VM did not confirm that it applied the Aleph bootstrap config in time."
3588
3608
  );
3589
3609
  }
3590
- }
3591
- this.emitProgress({
3592
- stage: "publishing-bootstrap",
3593
- label: "Reading relay multiaddrs",
3594
- progress: 95,
3595
- status: "info",
3596
- itemHash: args.itemHash,
3597
- detail: "Waiting for the relay to report its final public multiaddrs."
3598
- });
3599
- const relayMetadata = await fetchRelayMetadataForRuntime({
3600
- runtime,
3601
- fetch: (url, init) => fetch(url, init),
3602
- preferSecureMetadata: profile === "uc-go-peer",
3603
- attempts: 80,
3604
- delayMs: 3e3,
3605
- timeoutMs: 24e4,
3606
- onAttempt: (result) => {
3607
- const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3608
- const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3609
- this.trace("deploy:relay-metadata-attempt", {
3610
+ bootstrapSignalRelayMetadata = typeof bootstrapConfigSignal.peerId === "string" && bootstrapConfigSignal.peerId.length > 0 && Array.isArray(bootstrapConfigSignal.probeMultiaddrs) && bootstrapConfigSignal.probeMultiaddrs.length > 0 ? {
3611
+ peerId: bootstrapConfigSignal.peerId,
3612
+ probeMultiaddrs: bootstrapConfigSignal.probeMultiaddrs,
3613
+ browserBootstrapMultiaddrs: bootstrapConfigSignal.browserBootstrapMultiaddrs ?? []
3614
+ } : null;
3615
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3616
+ this.trace("deploy:bootstrap-proxy-fallback", {
3610
3617
  itemHash: args.itemHash,
3611
- attempt: result.attempt,
3612
- attempts: result.attempts,
3613
- requestUrl: result.requestUrl,
3614
- metadataUrl: result.metadataUrl,
3615
- hostIpv4: result.hostIpv4,
3616
- setupPort: result.setupPort,
3617
- ok: result.ok,
3618
- status: result.status,
3619
- ready: result.ready,
3620
- error: result.error ?? null,
3621
- payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3622
- peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3623
- probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3624
- browserBootstrapMultiaddrCount: Array.isArray(
3625
- metadata?.browser_bootstrap_multiaddrs
3626
- ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3618
+ deploymentToken: args.deploymentToken,
3619
+ proxyUrl: runtime.proxyUrl,
3620
+ bootstrapSignalRelayMetadata
3627
3621
  });
3622
+ this.emitProgress({
3623
+ stage: "deployment-confirmed",
3624
+ label: "Continuing without HTTPS route",
3625
+ progress: 94,
3626
+ status: "warning",
3627
+ itemHash: args.itemHash,
3628
+ detail: "The 2n6 HTTPS route stayed inactive, so guest setup continued without Caddy. Secure browser routing can be enabled later when the proxy becomes available."
3629
+ });
3630
+ runtime = {
3631
+ ...runtime,
3632
+ proxyUrl: null,
3633
+ webAccess: runtime.webAccess ? {
3634
+ ...runtime.webAccess,
3635
+ active: false
3636
+ } : runtime.webAccess
3637
+ };
3628
3638
  }
3629
- });
3639
+ }
3640
+ let relayMetadata = bootstrapSignalRelayMetadata;
3641
+ if (relayMetadata) {
3642
+ this.trace("deploy:relay-metadata-from-bootstrap-signal", {
3643
+ itemHash: args.itemHash,
3644
+ peerId: relayMetadata.peerId,
3645
+ probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3646
+ browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3647
+ });
3648
+ } else {
3649
+ this.emitProgress({
3650
+ stage: "publishing-bootstrap",
3651
+ label: "Reading relay multiaddrs",
3652
+ progress: 95,
3653
+ status: "info",
3654
+ itemHash: args.itemHash,
3655
+ detail: "Waiting for the relay to report its final public multiaddrs."
3656
+ });
3657
+ relayMetadata = await fetchRelayMetadataForRuntime({
3658
+ runtime,
3659
+ fetch: (url, init) => fetch(url, init),
3660
+ preferSecureMetadata: profile === "uc-go-peer" && Boolean(runtime.proxyUrl),
3661
+ attempts: 80,
3662
+ delayMs: 3e3,
3663
+ timeoutMs: 24e4,
3664
+ onAttempt: (result) => {
3665
+ const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3666
+ const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3667
+ this.trace("deploy:relay-metadata-attempt", {
3668
+ itemHash: args.itemHash,
3669
+ attempt: result.attempt,
3670
+ attempts: result.attempts,
3671
+ requestUrl: result.requestUrl,
3672
+ metadataUrl: result.metadataUrl,
3673
+ hostIpv4: result.hostIpv4,
3674
+ setupPort: result.setupPort,
3675
+ ok: result.ok,
3676
+ status: result.status,
3677
+ ready: result.ready,
3678
+ error: result.error ?? null,
3679
+ payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3680
+ peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3681
+ probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3682
+ browserBootstrapMultiaddrCount: Array.isArray(
3683
+ metadata?.browser_bootstrap_multiaddrs
3684
+ ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3685
+ });
3686
+ }
3687
+ });
3688
+ }
3630
3689
  if (!relayMetadata) {
3631
3690
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3632
3691
  }
@@ -4095,10 +4154,16 @@ var SponsorRelayController = class {
4095
4154
  const manifest = manifestState.manifest;
4096
4155
  let balance = this.state.balance;
4097
4156
  let instances = [];
4157
+ let bootstrapRegistrations = [];
4158
+ let orphanBootstrapRegistrations = [];
4098
4159
  if (this.state.wallet.address) {
4099
- const [nextBalance, rawInstances] = await Promise.all([
4160
+ const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
4100
4161
  this.client.fetchBalance(this.state.wallet.address),
4101
- this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
4162
+ this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([]),
4163
+ fetchAlephBootstrapPosts({
4164
+ apiHost: this.client.apiHost,
4165
+ fetch
4166
+ }).catch(() => [])
4102
4167
  ]);
4103
4168
  balance = nextBalance;
4104
4169
  instances = await Promise.all(
@@ -4158,6 +4223,37 @@ var SponsorRelayController = class {
4158
4223
  };
4159
4224
  })
4160
4225
  );
4226
+ const normalizedWalletAddress = this.state.wallet.address.toLowerCase();
4227
+ const activeInstanceHashes = new Set(
4228
+ instances.map((entry) => entry.instance.item_hash)
4229
+ );
4230
+ bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
4231
+ (entry) => entry.address?.toLowerCase() === normalizedWalletAddress
4232
+ ).map((entry) => {
4233
+ const instanceItemHash = relayBootstrapInstanceItemHash(entry);
4234
+ const messageHash = entry.itemHash ?? entry.hash ?? null;
4235
+ return {
4236
+ messageHash,
4237
+ hash: entry.hash ?? null,
4238
+ itemHash: entry.itemHash ?? null,
4239
+ address: entry.address ?? null,
4240
+ time: entry.time ?? null,
4241
+ instanceItemHash,
4242
+ confirmed: Boolean(
4243
+ instanceItemHash && activeInstanceHashes.has(instanceItemHash)
4244
+ ),
4245
+ content: entry.content ? {
4246
+ peerId: entry.content.peerId,
4247
+ registrationId: entry.content.registrationId,
4248
+ multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
4249
+ browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
4250
+ updatedAt: entry.content.updatedAt
4251
+ } : null
4252
+ };
4253
+ });
4254
+ orphanBootstrapRegistrations = bootstrapRegistrations.filter(
4255
+ (entry) => !entry.confirmed
4256
+ );
4161
4257
  }
4162
4258
  let rootfsVerified = false;
4163
4259
  let rootfsResolution = null;
@@ -4188,6 +4284,8 @@ var SponsorRelayController = class {
4188
4284
  balance,
4189
4285
  crns,
4190
4286
  instances,
4287
+ bootstrapRegistrations,
4288
+ orphanBootstrapRegistrations,
4191
4289
  busy: { refreshing: false },
4192
4290
  statusText: "Relay sponsor data ready"
4193
4291
  });
@@ -4697,6 +4795,72 @@ var SponsorRelayController = class {
4697
4795
  });
4698
4796
  }
4699
4797
  }
4798
+ async deleteBootstrapRegistration(registrationHash) {
4799
+ this.trace("registration-delete:start", { registrationHash });
4800
+ if (!this.state.wallet.address) {
4801
+ this.patch({
4802
+ errorText: "Connect MetaMask before forgetting registrations."
4803
+ });
4804
+ return;
4805
+ }
4806
+ this.patch({
4807
+ busy: { deletingRegistrationHash: registrationHash },
4808
+ errorText: null,
4809
+ statusText: `Forgetting registration ${registrationHash}`
4810
+ });
4811
+ try {
4812
+ this.emitProgress({
4813
+ stage: "validating",
4814
+ label: "Validating registration cleanup",
4815
+ progress: 6,
4816
+ status: "warning",
4817
+ itemHash: registrationHash,
4818
+ detail: "Preparing Aleph FORGET message for the orphan bootstrap registration."
4819
+ });
4820
+ await forgetAlephMessages({
4821
+ sender: this.state.wallet.address,
4822
+ hashes: [registrationHash],
4823
+ reason: "Deleted orphan relay bootstrap registration from Sponsor Relay panel",
4824
+ signer: personalSign,
4825
+ hasher: sha256Hex,
4826
+ fetch: asJsonFetch,
4827
+ apiHost: this.client.apiHost,
4828
+ onProgress: (event) => {
4829
+ this.emitProgress(event);
4830
+ }
4831
+ });
4832
+ this.patch({
4833
+ busy: { deletingRegistrationHash: null },
4834
+ statusText: `Deletion submitted for ${registrationHash}`
4835
+ });
4836
+ this.emitProgress({
4837
+ stage: "refreshing-instances",
4838
+ label: "Refreshing registrations after delete",
4839
+ progress: 92,
4840
+ status: "info",
4841
+ itemHash: registrationHash,
4842
+ detail: "Reloading deployments and relay bootstrap registrations."
4843
+ });
4844
+ await this.refresh();
4845
+ this.trace("registration-delete:submitted", { registrationHash });
4846
+ this.emitProgress({
4847
+ stage: "completed",
4848
+ label: "Registration cleanup completed",
4849
+ progress: 100,
4850
+ status: "success",
4851
+ itemHash: registrationHash,
4852
+ detail: "Orphan relay bootstrap registration forgotten and state refreshed.",
4853
+ error: null
4854
+ });
4855
+ } catch (error) {
4856
+ this.trace("registration-delete:error", error);
4857
+ this.patch({
4858
+ busy: { deletingRegistrationHash: null },
4859
+ errorText: error instanceof Error ? error.message : String(error),
4860
+ statusText: "Registration cleanup failed"
4861
+ });
4862
+ }
4863
+ }
4700
4864
  };
4701
4865
  function createSponsorRelayController(props = {}) {
4702
4866
  return new SponsorRelayController(props);
@@ -4896,6 +5060,10 @@ function SponsorRelayFab(props) {
4896
5060
  const versionLabel = resolvedVersion.startsWith("v") ? resolvedVersion : `v${resolvedVersion}`;
4897
5061
  const launcherMode = props.launcherMode ?? "floating";
4898
5062
  const indicator = launcherIndicator(state);
5063
+ const confirmedRegistrationByInstanceHash = new Map(
5064
+ (state.bootstrapRegistrations ?? []).filter((entry) => entry.confirmed && entry.instanceItemHash).map((entry) => [entry.instanceItemHash, entry])
5065
+ );
5066
+ const orphanRegistrations = state.orphanBootstrapRegistrations ?? [];
4899
5067
  const [successFlash, setSuccessFlash] = React.useState(false);
4900
5068
  const [hovered, setHovered] = React.useState(false);
4901
5069
  const launcherRef = React.useRef(null);
@@ -5534,66 +5702,196 @@ function SponsorRelayFab(props) {
5534
5702
  "Latest deployment: ",
5535
5703
  shortHash(state.lastDeploymentHash)
5536
5704
  ] }) : null,
5537
- state.showInstances ? /* @__PURE__ */ jsx(
5705
+ state.showInstances ? /* @__PURE__ */ jsxs(
5538
5706
  "div",
5539
5707
  {
5540
5708
  style: { marginTop: "1rem", display: "grid", gap: "0.7rem" },
5541
- children: state.instances.map((entry) => /* @__PURE__ */ jsxs("details", { open: true, children: [
5542
- /* @__PURE__ */ jsx("summary", { children: (entry.instance.content?.metadata?.name ?? "relay") + " \xB7 " + shortHash(entry.instance.item_hash) }),
5543
- /* @__PURE__ */ jsxs(
5709
+ children: [
5710
+ state.instances.map((entry) => {
5711
+ const confirmedRegistration = confirmedRegistrationByInstanceHash.get(
5712
+ entry.instance.item_hash
5713
+ ) ?? null;
5714
+ return /* @__PURE__ */ jsxs("details", { open: true, children: [
5715
+ /* @__PURE__ */ jsx("summary", { children: /* @__PURE__ */ jsxs(
5716
+ "span",
5717
+ {
5718
+ style: {
5719
+ display: "inline-flex",
5720
+ alignItems: "center",
5721
+ gap: "0.55rem",
5722
+ flexWrap: "wrap"
5723
+ },
5724
+ children: [
5725
+ (entry.instance.content?.metadata?.name ?? "relay") + " \xB7 " + shortHash(entry.instance.item_hash),
5726
+ confirmedRegistration ? /* @__PURE__ */ jsxs(
5727
+ "span",
5728
+ {
5729
+ style: {
5730
+ display: "inline-flex",
5731
+ alignItems: "center",
5732
+ gap: "0.32rem",
5733
+ padding: "0.16rem 0.45rem",
5734
+ borderRadius: "999px",
5735
+ background: "rgba(34, 197, 94, 0.16)",
5736
+ color: "#86efac",
5737
+ fontSize: "0.7rem",
5738
+ fontWeight: 700
5739
+ },
5740
+ children: [
5741
+ /* @__PURE__ */ jsx(
5742
+ "span",
5743
+ {
5744
+ "aria-hidden": "true",
5745
+ style: {
5746
+ width: "0.45rem",
5747
+ height: "0.45rem",
5748
+ borderRadius: "999px",
5749
+ background: "#22c55e",
5750
+ boxShadow: "0 0 0 3px rgba(34, 197, 94, 0.18)"
5751
+ }
5752
+ }
5753
+ ),
5754
+ "Registration confirmed"
5755
+ ]
5756
+ }
5757
+ ) : null
5758
+ ]
5759
+ }
5760
+ ) }),
5761
+ /* @__PURE__ */ jsxs(
5762
+ "div",
5763
+ {
5764
+ style: {
5765
+ display: "grid",
5766
+ gap: "0.35rem",
5767
+ marginTop: "0.55rem"
5768
+ },
5769
+ children: [
5770
+ /* @__PURE__ */ jsxs("div", { children: [
5771
+ "Status: ",
5772
+ entry.details.messageStatus
5773
+ ] }),
5774
+ confirmedRegistration ? /* @__PURE__ */ jsxs("div", { children: [
5775
+ "Bootstrap registration:",
5776
+ " ",
5777
+ shortHash(
5778
+ confirmedRegistration.messageHash ?? confirmedRegistration.content?.registrationId ?? "confirmed",
5779
+ 14,
5780
+ 8
5781
+ )
5782
+ ] }) : null,
5783
+ /* @__PURE__ */ jsxs("div", { children: [
5784
+ "Host IPv4: ",
5785
+ entry.details.hostIpv4 ?? "-"
5786
+ ] }),
5787
+ /* @__PURE__ */ jsxs("div", { children: [
5788
+ "IPv6: ",
5789
+ entry.details.ipv6 ?? "-"
5790
+ ] }),
5791
+ /* @__PURE__ */ jsxs("div", { children: [
5792
+ "VM IPv4: ",
5793
+ entry.details.vmIpv4 ?? "-"
5794
+ ] }),
5795
+ /* @__PURE__ */ jsxs("div", { children: [
5796
+ "SSH: ",
5797
+ entry.details.sshCommand ?? "-"
5798
+ ] }),
5799
+ /* @__PURE__ */ jsxs("div", { children: [
5800
+ "Ports: ",
5801
+ joinMappedPorts(entry.details.mappedPorts)
5802
+ ] }),
5803
+ /* @__PURE__ */ jsxs("div", { children: [
5804
+ "Submitted:",
5805
+ " ",
5806
+ formatDateTime(
5807
+ entry.instance.reception_time ?? entry.instance.time
5808
+ )
5809
+ ] }),
5810
+ /* @__PURE__ */ jsx(
5811
+ "button",
5812
+ {
5813
+ type: "button",
5814
+ onClick: () => void controller.deleteInstance(
5815
+ entry.instance.item_hash
5816
+ ),
5817
+ children: state.busy.deletingInstanceHash === entry.instance.item_hash ? "Deleting\u2026" : "Delete"
5818
+ }
5819
+ )
5820
+ ]
5821
+ }
5822
+ )
5823
+ ] }, entry.instance.item_hash);
5824
+ }),
5825
+ orphanRegistrations.length > 0 ? /* @__PURE__ */ jsxs(
5544
5826
  "div",
5545
5827
  {
5546
5828
  style: {
5547
5829
  display: "grid",
5548
- gap: "0.35rem",
5549
- marginTop: "0.55rem"
5830
+ gap: "0.7rem",
5831
+ padding: "0.85rem",
5832
+ borderRadius: "1rem",
5833
+ border: "1px solid rgba(248, 113, 113, 0.22)",
5834
+ background: "linear-gradient(180deg, rgba(127, 29, 29, 0.18), rgba(69, 10, 10, 0.12))"
5550
5835
  },
5551
5836
  children: [
5552
- /* @__PURE__ */ jsxs("div", { children: [
5553
- "Status: ",
5554
- entry.details.messageStatus
5555
- ] }),
5556
- /* @__PURE__ */ jsxs("div", { children: [
5557
- "Host IPv4: ",
5558
- entry.details.hostIpv4 ?? "-"
5559
- ] }),
5560
- /* @__PURE__ */ jsxs("div", { children: [
5561
- "IPv6: ",
5562
- entry.details.ipv6 ?? "-"
5563
- ] }),
5564
- /* @__PURE__ */ jsxs("div", { children: [
5565
- "VM IPv4: ",
5566
- entry.details.vmIpv4 ?? "-"
5837
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.2rem" }, children: [
5838
+ /* @__PURE__ */ jsx("strong", { children: "Orphan bootstrap registrations" }),
5839
+ /* @__PURE__ */ jsx("small", { style: { color: "#fecaca", lineHeight: 1.35 }, children: "Current-wallet registrations without a matching instance. Forget them directly from here." })
5567
5840
  ] }),
5568
- /* @__PURE__ */ jsxs("div", { children: [
5569
- "SSH: ",
5570
- entry.details.sshCommand ?? "-"
5571
- ] }),
5572
- /* @__PURE__ */ jsxs("div", { children: [
5573
- "Ports: ",
5574
- joinMappedPorts(entry.details.mappedPorts)
5575
- ] }),
5576
- /* @__PURE__ */ jsxs("div", { children: [
5577
- "Submitted:",
5578
- " ",
5579
- formatDateTime(
5580
- entry.instance.reception_time ?? entry.instance.time
5581
- )
5582
- ] }),
5583
- /* @__PURE__ */ jsx(
5584
- "button",
5585
- {
5586
- type: "button",
5587
- onClick: () => void controller.deleteInstance(
5588
- entry.instance.item_hash
5589
- ),
5590
- children: state.busy.deletingInstanceHash === entry.instance.item_hash ? "Deleting\u2026" : "Delete"
5591
- }
5592
- )
5841
+ orphanRegistrations.map((entry) => {
5842
+ const registrationHash = entry.messageHash ?? entry.hash ?? null;
5843
+ return /* @__PURE__ */ jsxs(
5844
+ "div",
5845
+ {
5846
+ style: {
5847
+ display: "grid",
5848
+ gap: "0.35rem",
5849
+ padding: "0.75rem",
5850
+ borderRadius: "0.9rem",
5851
+ background: "rgba(15, 23, 42, 0.22)",
5852
+ border: "1px solid rgba(255,255,255,0.08)"
5853
+ },
5854
+ children: [
5855
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 700 }, children: (entry.content?.registrationId ?? "registration") + " \xB7 " + shortHash(registrationHash ?? "unknown") }),
5856
+ /* @__PURE__ */ jsxs("div", { children: [
5857
+ "Peer: ",
5858
+ entry.content?.peerId ?? "-"
5859
+ ] }),
5860
+ /* @__PURE__ */ jsxs("div", { children: [
5861
+ "Linked instance:",
5862
+ " ",
5863
+ entry.instanceItemHash ? shortHash(entry.instanceItemHash) : "missing"
5864
+ ] }),
5865
+ /* @__PURE__ */ jsxs("div", { children: [
5866
+ "Browser multiaddrs:",
5867
+ " ",
5868
+ String(entry.content?.browserMultiaddrs?.length ?? 0)
5869
+ ] }),
5870
+ /* @__PURE__ */ jsxs("div", { children: [
5871
+ "Updated:",
5872
+ " ",
5873
+ formatDateTime(entry.content?.updatedAt ?? entry.time)
5874
+ ] }),
5875
+ /* @__PURE__ */ jsx(
5876
+ "button",
5877
+ {
5878
+ type: "button",
5879
+ disabled: !registrationHash || state.busy.deletingRegistrationHash === registrationHash,
5880
+ onClick: () => registrationHash ? void controller.deleteBootstrapRegistration(
5881
+ registrationHash
5882
+ ) : void 0,
5883
+ children: state.busy.deletingRegistrationHash === registrationHash ? "Forgetting\u2026" : "Forget registration"
5884
+ }
5885
+ )
5886
+ ]
5887
+ },
5888
+ registrationHash ?? entry.content?.registrationId ?? `orphan-${entry.content?.peerId ?? "unknown"}`
5889
+ );
5890
+ })
5593
5891
  ]
5594
5892
  }
5595
- )
5596
- ] }, entry.instance.item_hash))
5893
+ ) : null
5894
+ ]
5597
5895
  }
5598
5896
  ) : null
5599
5897
  ] })
package/shared/index.d.ts CHANGED
@@ -241,6 +241,22 @@ interface CompactInstanceRecord {
241
241
  instance: InstanceMessage;
242
242
  details: CompactInstanceDetails;
243
243
  }
244
+ interface CompactBootstrapRegistrationRecord {
245
+ messageHash: string | null;
246
+ hash: string | null;
247
+ itemHash: string | null;
248
+ address: string | null;
249
+ time: number | null;
250
+ instanceItemHash: string | null;
251
+ confirmed: boolean;
252
+ content: {
253
+ peerId: string;
254
+ registrationId?: string;
255
+ multiaddrs: string[];
256
+ browserMultiaddrs?: string[];
257
+ updatedAt: number;
258
+ } | null;
259
+ }
244
260
  interface SponsorRelayState {
245
261
  ready: boolean;
246
262
  open: boolean;
@@ -258,6 +274,7 @@ interface SponsorRelayState {
258
274
  refreshing: boolean;
259
275
  deploying: boolean;
260
276
  deletingInstanceHash: string | null;
277
+ deletingRegistrationHash: string | null;
261
278
  };
262
279
  statusText: string;
263
280
  errorText: string | null;
@@ -271,6 +288,8 @@ interface SponsorRelayState {
271
288
  crns: Crn[];
272
289
  selectedCrn: Crn | null;
273
290
  instances: CompactInstanceRecord[];
291
+ bootstrapRegistrations: CompactBootstrapRegistrationRecord[];
292
+ orphanBootstrapRegistrations: CompactBootstrapRegistrationRecord[];
274
293
  lastDeploymentHash: string | null;
275
294
  deploymentProgress: DeploymentProgressEvent;
276
295
  }
@@ -290,7 +309,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
290
309
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
291
310
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
292
311
 
293
- declare const UI_PACKAGE_VERSION = "0.3.30";
312
+ declare const UI_PACKAGE_VERSION = "0.3.32";
294
313
 
295
314
  interface RootfsRequiredPortForward {
296
315
  port: number;
@@ -372,6 +391,7 @@ declare class SponsorRelayController {
372
391
  refresh(): Promise<void>;
373
392
  deploy(): Promise<void>;
374
393
  deleteInstance(instanceHash: string): Promise<void>;
394
+ deleteBootstrapRegistration(registrationHash: string): Promise<void>;
375
395
  }
376
396
  declare function createSponsorRelayController(props?: SponsorRelayProps): SponsorRelayController;
377
397
 
@@ -382,4 +402,4 @@ interface DeploymentProgressEmitter {
382
402
  }
383
403
  declare function createDeploymentProgressEmitter(): DeploymentProgressEmitter;
384
404
 
385
- export { type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, DEPLOYMENT_PENDING_WARNING_MS, type DeploymentProgressEmitter, type EthereumProviderLike, IDLE_DEPLOYMENT_PROGRESS, INSTANCE_RUNTIME_PENDING_REFRESH_MS, INSTANCE_RUNTIME_READY_REFRESH_MS, INSTANCE_RUNTIME_STALE_REFRESH_MS, MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS, RECENT_INSTANCE_RUNTIME_GRACE_MS, REFRESH_INTERVAL_MS, ROOTFS_MISSING_STATE, STALE_INSTANCE_ALLOCATION_COOLDOWN_MS, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayWalletState, UI_PACKAGE_VERSION, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, formatTierSpecLabel, getEthereumProvider, joinMappedPorts, joinRequiredPortForwards, personalSign, resolveManifestSource, shortHash, toChecksumAddress, watchWallet };
405
+ export { type CompactBootstrapRegistrationRecord, type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, DEPLOYMENT_PENDING_WARNING_MS, type DeploymentProgressEmitter, type EthereumProviderLike, IDLE_DEPLOYMENT_PROGRESS, INSTANCE_RUNTIME_PENDING_REFRESH_MS, INSTANCE_RUNTIME_READY_REFRESH_MS, INSTANCE_RUNTIME_STALE_REFRESH_MS, MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS, RECENT_INSTANCE_RUNTIME_GRACE_MS, REFRESH_INTERVAL_MS, ROOTFS_MISSING_STATE, STALE_INSTANCE_ALLOCATION_COOLDOWN_MS, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayWalletState, UI_PACKAGE_VERSION, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, formatTierSpecLabel, getEthereumProvider, joinMappedPorts, joinRequiredPortForwards, personalSign, resolveManifestSource, shortHash, toChecksumAddress, watchWallet };
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.3.30";
30
+ var UI_PACKAGE_VERSION = "0.3.32";
31
31
 
32
32
  // src/shared/format.ts
33
33
  function shortHash(value, head = 8, tail = 6) {
@@ -2868,7 +2868,14 @@ function normalizeVmBootstrapConfigSignalRecord(value) {
2868
2868
  instanceItemHash,
2869
2869
  updatedAt,
2870
2870
  publisherAddress: typeof candidate.publisherAddress === "string" && candidate.publisherAddress.trim() ? candidate.publisherAddress.trim() : null,
2871
- authorization: candidate.authorization
2871
+ authorization: candidate.authorization,
2872
+ peerId: typeof candidate.peerId === "string" && candidate.peerId.trim() ? candidate.peerId.trim() : null,
2873
+ probeMultiaddrs: Array.isArray(candidate.probeMultiaddrs) ? candidate.probeMultiaddrs.filter(
2874
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2875
+ ) : [],
2876
+ browserBootstrapMultiaddrs: Array.isArray(candidate.browserBootstrapMultiaddrs) ? candidate.browserBootstrapMultiaddrs.filter(
2877
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2878
+ ) : []
2872
2879
  };
2873
2880
  }
2874
2881
  async function fetchVmBootstrapConfigSignals(args) {
@@ -3036,7 +3043,8 @@ function defaultState(props = {}) {
3036
3043
  connectingWallet: false,
3037
3044
  refreshing: false,
3038
3045
  deploying: false,
3039
- deletingInstanceHash: null
3046
+ deletingInstanceHash: null,
3047
+ deletingRegistrationHash: null
3040
3048
  },
3041
3049
  statusText: "Ready",
3042
3050
  errorText: null,
@@ -3062,6 +3070,8 @@ function defaultState(props = {}) {
3062
3070
  crns: [],
3063
3071
  selectedCrn: null,
3064
3072
  instances: [],
3073
+ bootstrapRegistrations: [],
3074
+ orphanBootstrapRegistrations: [],
3065
3075
  lastDeploymentHash: null,
3066
3076
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
3067
3077
  };
@@ -3166,6 +3176,20 @@ function relayProfileForManifest(manifest) {
3166
3176
  }
3167
3177
  return null;
3168
3178
  }
3179
+ function registrationIdInstanceItemHash(registrationId) {
3180
+ const normalized = String(registrationId ?? "").trim();
3181
+ if (!normalized) return null;
3182
+ const parts = normalized.split(":").map((part) => part.trim()).filter(Boolean);
3183
+ const candidate = parts.at(-1) ?? "";
3184
+ return /^[0-9a-f]{64}$/i.test(candidate) ? candidate : null;
3185
+ }
3186
+ function relayBootstrapInstanceItemHash(entry) {
3187
+ const authorizedInstanceItemHash = typeof entry?.content?.authorization?.payload?.instanceItemHash === "string" ? entry.content.authorization.payload.instanceItemHash.trim() : "";
3188
+ if (authorizedInstanceItemHash) {
3189
+ return authorizedInstanceItemHash;
3190
+ }
3191
+ return registrationIdInstanceItemHash(entry?.content?.registrationId);
3192
+ }
3169
3193
  async function inspectInstanceRuntime(args) {
3170
3194
  const details = {
3171
3195
  messageStatus: String(
@@ -3400,6 +3424,7 @@ var SponsorRelayController = class {
3400
3424
  if (!setupPort) {
3401
3425
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3402
3426
  }
3427
+ let bootstrapSignalRelayMetadata = null;
3403
3428
  if (profile === "orbitdb-relay-pinner") {
3404
3429
  this.emitProgress({
3405
3430
  stage: "deployment-confirmed",
@@ -3500,11 +3525,6 @@ var SponsorRelayController = class {
3500
3525
  }
3501
3526
  runtime = latestRuntime;
3502
3527
  mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3503
- if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3504
- throw new Error(
3505
- "Aleph reserved a 2n6 proxy URL, but it never became active in time. Aborting guest HTTPS configuration to avoid a broken Caddy setup."
3506
- );
3507
- }
3508
3528
  }
3509
3529
  if (!runtime.hostIpv4) {
3510
3530
  throw new Error("Relay runtime lost its host IPv4 address while waiting for HTTPS activation.");
@@ -3586,46 +3606,85 @@ var SponsorRelayController = class {
3586
3606
  "The relay VM did not confirm that it applied the Aleph bootstrap config in time."
3587
3607
  );
3588
3608
  }
3589
- }
3590
- this.emitProgress({
3591
- stage: "publishing-bootstrap",
3592
- label: "Reading relay multiaddrs",
3593
- progress: 95,
3594
- status: "info",
3595
- itemHash: args.itemHash,
3596
- detail: "Waiting for the relay to report its final public multiaddrs."
3597
- });
3598
- const relayMetadata = await fetchRelayMetadataForRuntime({
3599
- runtime,
3600
- fetch: (url, init) => fetch(url, init),
3601
- preferSecureMetadata: profile === "uc-go-peer",
3602
- attempts: 80,
3603
- delayMs: 3e3,
3604
- timeoutMs: 24e4,
3605
- onAttempt: (result) => {
3606
- const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3607
- const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3608
- this.trace("deploy:relay-metadata-attempt", {
3609
+ bootstrapSignalRelayMetadata = typeof bootstrapConfigSignal.peerId === "string" && bootstrapConfigSignal.peerId.length > 0 && Array.isArray(bootstrapConfigSignal.probeMultiaddrs) && bootstrapConfigSignal.probeMultiaddrs.length > 0 ? {
3610
+ peerId: bootstrapConfigSignal.peerId,
3611
+ probeMultiaddrs: bootstrapConfigSignal.probeMultiaddrs,
3612
+ browserBootstrapMultiaddrs: bootstrapConfigSignal.browserBootstrapMultiaddrs ?? []
3613
+ } : null;
3614
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3615
+ this.trace("deploy:bootstrap-proxy-fallback", {
3609
3616
  itemHash: args.itemHash,
3610
- attempt: result.attempt,
3611
- attempts: result.attempts,
3612
- requestUrl: result.requestUrl,
3613
- metadataUrl: result.metadataUrl,
3614
- hostIpv4: result.hostIpv4,
3615
- setupPort: result.setupPort,
3616
- ok: result.ok,
3617
- status: result.status,
3618
- ready: result.ready,
3619
- error: result.error ?? null,
3620
- payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3621
- peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3622
- probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3623
- browserBootstrapMultiaddrCount: Array.isArray(
3624
- metadata?.browser_bootstrap_multiaddrs
3625
- ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3617
+ deploymentToken: args.deploymentToken,
3618
+ proxyUrl: runtime.proxyUrl,
3619
+ bootstrapSignalRelayMetadata
3626
3620
  });
3621
+ this.emitProgress({
3622
+ stage: "deployment-confirmed",
3623
+ label: "Continuing without HTTPS route",
3624
+ progress: 94,
3625
+ status: "warning",
3626
+ itemHash: args.itemHash,
3627
+ detail: "The 2n6 HTTPS route stayed inactive, so guest setup continued without Caddy. Secure browser routing can be enabled later when the proxy becomes available."
3628
+ });
3629
+ runtime = {
3630
+ ...runtime,
3631
+ proxyUrl: null,
3632
+ webAccess: runtime.webAccess ? {
3633
+ ...runtime.webAccess,
3634
+ active: false
3635
+ } : runtime.webAccess
3636
+ };
3627
3637
  }
3628
- });
3638
+ }
3639
+ let relayMetadata = bootstrapSignalRelayMetadata;
3640
+ if (relayMetadata) {
3641
+ this.trace("deploy:relay-metadata-from-bootstrap-signal", {
3642
+ itemHash: args.itemHash,
3643
+ peerId: relayMetadata.peerId,
3644
+ probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3645
+ browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3646
+ });
3647
+ } else {
3648
+ this.emitProgress({
3649
+ stage: "publishing-bootstrap",
3650
+ label: "Reading relay multiaddrs",
3651
+ progress: 95,
3652
+ status: "info",
3653
+ itemHash: args.itemHash,
3654
+ detail: "Waiting for the relay to report its final public multiaddrs."
3655
+ });
3656
+ relayMetadata = await fetchRelayMetadataForRuntime({
3657
+ runtime,
3658
+ fetch: (url, init) => fetch(url, init),
3659
+ preferSecureMetadata: profile === "uc-go-peer" && Boolean(runtime.proxyUrl),
3660
+ attempts: 80,
3661
+ delayMs: 3e3,
3662
+ timeoutMs: 24e4,
3663
+ onAttempt: (result) => {
3664
+ const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3665
+ const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3666
+ this.trace("deploy:relay-metadata-attempt", {
3667
+ itemHash: args.itemHash,
3668
+ attempt: result.attempt,
3669
+ attempts: result.attempts,
3670
+ requestUrl: result.requestUrl,
3671
+ metadataUrl: result.metadataUrl,
3672
+ hostIpv4: result.hostIpv4,
3673
+ setupPort: result.setupPort,
3674
+ ok: result.ok,
3675
+ status: result.status,
3676
+ ready: result.ready,
3677
+ error: result.error ?? null,
3678
+ payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3679
+ peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3680
+ probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3681
+ browserBootstrapMultiaddrCount: Array.isArray(
3682
+ metadata?.browser_bootstrap_multiaddrs
3683
+ ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3684
+ });
3685
+ }
3686
+ });
3687
+ }
3629
3688
  if (!relayMetadata) {
3630
3689
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3631
3690
  }
@@ -4094,10 +4153,16 @@ var SponsorRelayController = class {
4094
4153
  const manifest = manifestState.manifest;
4095
4154
  let balance = this.state.balance;
4096
4155
  let instances = [];
4156
+ let bootstrapRegistrations = [];
4157
+ let orphanBootstrapRegistrations = [];
4097
4158
  if (this.state.wallet.address) {
4098
- const [nextBalance, rawInstances] = await Promise.all([
4159
+ const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
4099
4160
  this.client.fetchBalance(this.state.wallet.address),
4100
- this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
4161
+ this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([]),
4162
+ fetchAlephBootstrapPosts({
4163
+ apiHost: this.client.apiHost,
4164
+ fetch
4165
+ }).catch(() => [])
4101
4166
  ]);
4102
4167
  balance = nextBalance;
4103
4168
  instances = await Promise.all(
@@ -4157,6 +4222,37 @@ var SponsorRelayController = class {
4157
4222
  };
4158
4223
  })
4159
4224
  );
4225
+ const normalizedWalletAddress = this.state.wallet.address.toLowerCase();
4226
+ const activeInstanceHashes = new Set(
4227
+ instances.map((entry) => entry.instance.item_hash)
4228
+ );
4229
+ bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
4230
+ (entry) => entry.address?.toLowerCase() === normalizedWalletAddress
4231
+ ).map((entry) => {
4232
+ const instanceItemHash = relayBootstrapInstanceItemHash(entry);
4233
+ const messageHash = entry.itemHash ?? entry.hash ?? null;
4234
+ return {
4235
+ messageHash,
4236
+ hash: entry.hash ?? null,
4237
+ itemHash: entry.itemHash ?? null,
4238
+ address: entry.address ?? null,
4239
+ time: entry.time ?? null,
4240
+ instanceItemHash,
4241
+ confirmed: Boolean(
4242
+ instanceItemHash && activeInstanceHashes.has(instanceItemHash)
4243
+ ),
4244
+ content: entry.content ? {
4245
+ peerId: entry.content.peerId,
4246
+ registrationId: entry.content.registrationId,
4247
+ multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
4248
+ browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
4249
+ updatedAt: entry.content.updatedAt
4250
+ } : null
4251
+ };
4252
+ });
4253
+ orphanBootstrapRegistrations = bootstrapRegistrations.filter(
4254
+ (entry) => !entry.confirmed
4255
+ );
4160
4256
  }
4161
4257
  let rootfsVerified = false;
4162
4258
  let rootfsResolution = null;
@@ -4187,6 +4283,8 @@ var SponsorRelayController = class {
4187
4283
  balance,
4188
4284
  crns,
4189
4285
  instances,
4286
+ bootstrapRegistrations,
4287
+ orphanBootstrapRegistrations,
4190
4288
  busy: { refreshing: false },
4191
4289
  statusText: "Relay sponsor data ready"
4192
4290
  });
@@ -4696,6 +4794,72 @@ var SponsorRelayController = class {
4696
4794
  });
4697
4795
  }
4698
4796
  }
4797
+ async deleteBootstrapRegistration(registrationHash) {
4798
+ this.trace("registration-delete:start", { registrationHash });
4799
+ if (!this.state.wallet.address) {
4800
+ this.patch({
4801
+ errorText: "Connect MetaMask before forgetting registrations."
4802
+ });
4803
+ return;
4804
+ }
4805
+ this.patch({
4806
+ busy: { deletingRegistrationHash: registrationHash },
4807
+ errorText: null,
4808
+ statusText: `Forgetting registration ${registrationHash}`
4809
+ });
4810
+ try {
4811
+ this.emitProgress({
4812
+ stage: "validating",
4813
+ label: "Validating registration cleanup",
4814
+ progress: 6,
4815
+ status: "warning",
4816
+ itemHash: registrationHash,
4817
+ detail: "Preparing Aleph FORGET message for the orphan bootstrap registration."
4818
+ });
4819
+ await forgetAlephMessages({
4820
+ sender: this.state.wallet.address,
4821
+ hashes: [registrationHash],
4822
+ reason: "Deleted orphan relay bootstrap registration from Sponsor Relay panel",
4823
+ signer: personalSign,
4824
+ hasher: sha256Hex,
4825
+ fetch: asJsonFetch,
4826
+ apiHost: this.client.apiHost,
4827
+ onProgress: (event) => {
4828
+ this.emitProgress(event);
4829
+ }
4830
+ });
4831
+ this.patch({
4832
+ busy: { deletingRegistrationHash: null },
4833
+ statusText: `Deletion submitted for ${registrationHash}`
4834
+ });
4835
+ this.emitProgress({
4836
+ stage: "refreshing-instances",
4837
+ label: "Refreshing registrations after delete",
4838
+ progress: 92,
4839
+ status: "info",
4840
+ itemHash: registrationHash,
4841
+ detail: "Reloading deployments and relay bootstrap registrations."
4842
+ });
4843
+ await this.refresh();
4844
+ this.trace("registration-delete:submitted", { registrationHash });
4845
+ this.emitProgress({
4846
+ stage: "completed",
4847
+ label: "Registration cleanup completed",
4848
+ progress: 100,
4849
+ status: "success",
4850
+ itemHash: registrationHash,
4851
+ detail: "Orphan relay bootstrap registration forgotten and state refreshed.",
4852
+ error: null
4853
+ });
4854
+ } catch (error) {
4855
+ this.trace("registration-delete:error", error);
4856
+ this.patch({
4857
+ busy: { deletingRegistrationHash: null },
4858
+ errorText: error instanceof Error ? error.message : String(error),
4859
+ statusText: "Registration cleanup failed"
4860
+ });
4861
+ }
4862
+ }
4699
4863
  };
4700
4864
  function createSponsorRelayController(props = {}) {
4701
4865
  return new SponsorRelayController(props);
@@ -49,6 +49,12 @@ export let apiHost = undefined
49
49
  $: pollingDetail = state.busy.refreshing
50
50
  ? (state.statusText || 'Refreshing relay sponsor data from Aleph and the selected CRN.')
51
51
  : (state.deploymentProgress.detail || state.statusText || 'Waiting for the next confirmed relay state from Aleph.')
52
+ $: confirmedRegistrationByInstanceHash = new Map(
53
+ (state.bootstrapRegistrations ?? [])
54
+ .filter((entry) => entry.confirmed && entry.instanceItemHash)
55
+ .map((entry) => [entry.instanceItemHash, entry]),
56
+ )
57
+ $: orphanRegistrations = state.orphanBootstrapRegistrations ?? []
52
58
 
53
59
  onMount(async () => {
54
60
  const unsubscribe = controller.subscribe((next) => {
@@ -220,12 +226,19 @@ export let apiHost = undefined
220
226
 
221
227
  {#each state.instances as entry}
222
228
  <AccordionSection title={`${entry.instance.content?.metadata?.name ?? 'relay'} · ${shortHash(entry.instance.item_hash)}`} open={true}>
229
+ {@const confirmedRegistration = confirmedRegistrationByInstanceHash.get(entry.instance.item_hash)}
223
230
  <div class="instance-topline">
224
231
  <div class="chip-row">
225
232
  <span class="chip">{entry.details.messageStatus}</span>
226
233
  {#if entry.details.crnUrl}
227
234
  <span class="chip">{entry.details.crnUrl.replace(/^https?:\/\//, '')}</span>
228
235
  {/if}
236
+ {#if confirmedRegistration}
237
+ <span class="chip chip-confirmed">
238
+ <span class="chip-dot-confirmed"></span>
239
+ Registration confirmed
240
+ </span>
241
+ {/if}
229
242
  </div>
230
243
  <button
231
244
  class="delete"
@@ -267,6 +280,13 @@ export let apiHost = undefined
267
280
  <strong>{joinMappedPorts(entry.details.mappedPorts)}</strong>
268
281
  </div>
269
282
 
283
+ {#if confirmedRegistration}
284
+ <div class="mono-block">
285
+ <span>Bootstrap Registration</span>
286
+ <strong>{shortHash(confirmedRegistration.messageHash ?? confirmedRegistration.content?.registrationId ?? 'confirmed', 14, 8)}</strong>
287
+ </div>
288
+ {/if}
289
+
270
290
  <div class="link-row">
271
291
  <CopyButton text={entry.instance.item_hash} label="Copy hash" />
272
292
  {#if entry.details.webUrl}
@@ -281,6 +301,34 @@ export let apiHost = undefined
281
301
  {/if}
282
302
  </AccordionSection>
283
303
  {/each}
304
+
305
+ {#if orphanRegistrations.length > 0}
306
+ <div class="orphan-box">
307
+ <div class="orphan-head">
308
+ <strong>Orphan bootstrap registrations</strong>
309
+ <small>Current-wallet registrations without a matching instance. Forget them directly from here.</small>
310
+ </div>
311
+
312
+ {#each orphanRegistrations as entry}
313
+ {@const registrationHash = entry.messageHash ?? entry.hash}
314
+ <div class="orphan-card">
315
+ <div class="orphan-title">{entry.content?.registrationId ?? 'registration'} · {shortHash(registrationHash ?? 'unknown')}</div>
316
+ <div>Peer: {entry.content?.peerId ?? '-'}</div>
317
+ <div>Linked instance: {entry.instanceItemHash ? shortHash(entry.instanceItemHash) : 'missing'}</div>
318
+ <div>Browser multiaddrs: {String(entry.content?.browserMultiaddrs?.length ?? 0)}</div>
319
+ <div>Updated: {formatDateTime(entry.content?.updatedAt ?? entry.time)}</div>
320
+ <button
321
+ class="delete"
322
+ type="button"
323
+ disabled={!registrationHash || state.busy.deletingRegistrationHash === registrationHash}
324
+ on:click={() => registrationHash && controller.deleteBootstrapRegistration(registrationHash)}
325
+ >
326
+ {state.busy.deletingRegistrationHash === registrationHash ? 'Forgetting…' : 'Forget registration'}
327
+ </button>
328
+ </div>
329
+ {/each}
330
+ </div>
331
+ {/if}
284
332
  </section>
285
333
  {/if}
286
334
  </aside>
@@ -401,6 +449,22 @@ export let apiHost = undefined
401
449
  color: #ffd4d4;
402
450
  }
403
451
 
452
+ .chip-confirmed {
453
+ background: rgba(34, 197, 94, 0.16);
454
+ color: #86efac;
455
+ display: inline-flex;
456
+ align-items: center;
457
+ gap: 0.32rem;
458
+ }
459
+
460
+ .chip-dot-confirmed {
461
+ width: 0.45rem;
462
+ height: 0.45rem;
463
+ border-radius: 999px;
464
+ background: #22c55e;
465
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
466
+ }
467
+
404
468
  .status-strip,
405
469
  .metrics {
406
470
  display: grid;
@@ -425,6 +489,38 @@ export let apiHost = undefined
425
489
  gap: 0.7rem;
426
490
  }
427
491
 
492
+ .orphan-box {
493
+ display: grid;
494
+ gap: 0.7rem;
495
+ padding: 0.85rem;
496
+ border-radius: 1rem;
497
+ border: 1px solid rgba(248, 113, 113, 0.22);
498
+ background: linear-gradient(180deg, rgba(127, 29, 29, 0.18), rgba(69, 10, 10, 0.12));
499
+ }
500
+
501
+ .orphan-head {
502
+ display: grid;
503
+ gap: 0.2rem;
504
+ }
505
+
506
+ .orphan-head small {
507
+ color: #fecaca;
508
+ line-height: 1.35;
509
+ }
510
+
511
+ .orphan-card {
512
+ display: grid;
513
+ gap: 0.35rem;
514
+ padding: 0.75rem;
515
+ border-radius: 0.9rem;
516
+ background: rgba(15, 23, 42, 0.22);
517
+ border: 1px solid rgba(255, 255, 255, 0.08);
518
+ }
519
+
520
+ .orphan-title {
521
+ font-weight: 700;
522
+ }
523
+
428
524
  .alert {
429
525
  margin: 0.8rem 0 0;
430
526
  padding: 0.75rem 0.85rem;