@le-space/ui 0.3.31 → 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.31",
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.31",
40
- "@le-space/core": "0.3.31",
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.31";
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) {
@@ -3044,7 +3044,8 @@ function defaultState(props = {}) {
3044
3044
  connectingWallet: false,
3045
3045
  refreshing: false,
3046
3046
  deploying: false,
3047
- deletingInstanceHash: null
3047
+ deletingInstanceHash: null,
3048
+ deletingRegistrationHash: null
3048
3049
  },
3049
3050
  statusText: "Ready",
3050
3051
  errorText: null,
@@ -3070,6 +3071,8 @@ function defaultState(props = {}) {
3070
3071
  crns: [],
3071
3072
  selectedCrn: null,
3072
3073
  instances: [],
3074
+ bootstrapRegistrations: [],
3075
+ orphanBootstrapRegistrations: [],
3073
3076
  lastDeploymentHash: null,
3074
3077
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
3075
3078
  };
@@ -3174,6 +3177,20 @@ function relayProfileForManifest(manifest) {
3174
3177
  }
3175
3178
  return null;
3176
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
+ }
3177
3194
  async function inspectInstanceRuntime(args) {
3178
3195
  const details = {
3179
3196
  messageStatus: String(
@@ -4137,10 +4154,16 @@ var SponsorRelayController = class {
4137
4154
  const manifest = manifestState.manifest;
4138
4155
  let balance = this.state.balance;
4139
4156
  let instances = [];
4157
+ let bootstrapRegistrations = [];
4158
+ let orphanBootstrapRegistrations = [];
4140
4159
  if (this.state.wallet.address) {
4141
- const [nextBalance, rawInstances] = await Promise.all([
4160
+ const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
4142
4161
  this.client.fetchBalance(this.state.wallet.address),
4143
- 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(() => [])
4144
4167
  ]);
4145
4168
  balance = nextBalance;
4146
4169
  instances = await Promise.all(
@@ -4200,6 +4223,37 @@ var SponsorRelayController = class {
4200
4223
  };
4201
4224
  })
4202
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
+ );
4203
4257
  }
4204
4258
  let rootfsVerified = false;
4205
4259
  let rootfsResolution = null;
@@ -4230,6 +4284,8 @@ var SponsorRelayController = class {
4230
4284
  balance,
4231
4285
  crns,
4232
4286
  instances,
4287
+ bootstrapRegistrations,
4288
+ orphanBootstrapRegistrations,
4233
4289
  busy: { refreshing: false },
4234
4290
  statusText: "Relay sponsor data ready"
4235
4291
  });
@@ -4739,6 +4795,72 @@ var SponsorRelayController = class {
4739
4795
  });
4740
4796
  }
4741
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
+ }
4742
4864
  };
4743
4865
  function createSponsorRelayController(props = {}) {
4744
4866
  return new SponsorRelayController(props);
@@ -4938,6 +5060,10 @@ function SponsorRelayFab(props) {
4938
5060
  const versionLabel = resolvedVersion.startsWith("v") ? resolvedVersion : `v${resolvedVersion}`;
4939
5061
  const launcherMode = props.launcherMode ?? "floating";
4940
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 ?? [];
4941
5067
  const [successFlash, setSuccessFlash] = React.useState(false);
4942
5068
  const [hovered, setHovered] = React.useState(false);
4943
5069
  const launcherRef = React.useRef(null);
@@ -5576,66 +5702,196 @@ function SponsorRelayFab(props) {
5576
5702
  "Latest deployment: ",
5577
5703
  shortHash(state.lastDeploymentHash)
5578
5704
  ] }) : null,
5579
- state.showInstances ? /* @__PURE__ */ jsx(
5705
+ state.showInstances ? /* @__PURE__ */ jsxs(
5580
5706
  "div",
5581
5707
  {
5582
5708
  style: { marginTop: "1rem", display: "grid", gap: "0.7rem" },
5583
- children: state.instances.map((entry) => /* @__PURE__ */ jsxs("details", { open: true, children: [
5584
- /* @__PURE__ */ jsx("summary", { children: (entry.instance.content?.metadata?.name ?? "relay") + " \xB7 " + shortHash(entry.instance.item_hash) }),
5585
- /* @__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(
5586
5826
  "div",
5587
5827
  {
5588
5828
  style: {
5589
5829
  display: "grid",
5590
- gap: "0.35rem",
5591
- 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))"
5592
5835
  },
5593
5836
  children: [
5594
- /* @__PURE__ */ jsxs("div", { children: [
5595
- "Status: ",
5596
- entry.details.messageStatus
5597
- ] }),
5598
- /* @__PURE__ */ jsxs("div", { children: [
5599
- "Host IPv4: ",
5600
- entry.details.hostIpv4 ?? "-"
5601
- ] }),
5602
- /* @__PURE__ */ jsxs("div", { children: [
5603
- "IPv6: ",
5604
- entry.details.ipv6 ?? "-"
5605
- ] }),
5606
- /* @__PURE__ */ jsxs("div", { children: [
5607
- "VM IPv4: ",
5608
- 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." })
5609
5840
  ] }),
5610
- /* @__PURE__ */ jsxs("div", { children: [
5611
- "SSH: ",
5612
- entry.details.sshCommand ?? "-"
5613
- ] }),
5614
- /* @__PURE__ */ jsxs("div", { children: [
5615
- "Ports: ",
5616
- joinMappedPorts(entry.details.mappedPorts)
5617
- ] }),
5618
- /* @__PURE__ */ jsxs("div", { children: [
5619
- "Submitted:",
5620
- " ",
5621
- formatDateTime(
5622
- entry.instance.reception_time ?? entry.instance.time
5623
- )
5624
- ] }),
5625
- /* @__PURE__ */ jsx(
5626
- "button",
5627
- {
5628
- type: "button",
5629
- onClick: () => void controller.deleteInstance(
5630
- entry.instance.item_hash
5631
- ),
5632
- children: state.busy.deletingInstanceHash === entry.instance.item_hash ? "Deleting\u2026" : "Delete"
5633
- }
5634
- )
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
+ })
5635
5891
  ]
5636
5892
  }
5637
- )
5638
- ] }, entry.instance.item_hash))
5893
+ ) : null
5894
+ ]
5639
5895
  }
5640
5896
  ) : null
5641
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.31";
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.31";
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) {
@@ -3043,7 +3043,8 @@ function defaultState(props = {}) {
3043
3043
  connectingWallet: false,
3044
3044
  refreshing: false,
3045
3045
  deploying: false,
3046
- deletingInstanceHash: null
3046
+ deletingInstanceHash: null,
3047
+ deletingRegistrationHash: null
3047
3048
  },
3048
3049
  statusText: "Ready",
3049
3050
  errorText: null,
@@ -3069,6 +3070,8 @@ function defaultState(props = {}) {
3069
3070
  crns: [],
3070
3071
  selectedCrn: null,
3071
3072
  instances: [],
3073
+ bootstrapRegistrations: [],
3074
+ orphanBootstrapRegistrations: [],
3072
3075
  lastDeploymentHash: null,
3073
3076
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
3074
3077
  };
@@ -3173,6 +3176,20 @@ function relayProfileForManifest(manifest) {
3173
3176
  }
3174
3177
  return null;
3175
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
+ }
3176
3193
  async function inspectInstanceRuntime(args) {
3177
3194
  const details = {
3178
3195
  messageStatus: String(
@@ -4136,10 +4153,16 @@ var SponsorRelayController = class {
4136
4153
  const manifest = manifestState.manifest;
4137
4154
  let balance = this.state.balance;
4138
4155
  let instances = [];
4156
+ let bootstrapRegistrations = [];
4157
+ let orphanBootstrapRegistrations = [];
4139
4158
  if (this.state.wallet.address) {
4140
- const [nextBalance, rawInstances] = await Promise.all([
4159
+ const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
4141
4160
  this.client.fetchBalance(this.state.wallet.address),
4142
- 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(() => [])
4143
4166
  ]);
4144
4167
  balance = nextBalance;
4145
4168
  instances = await Promise.all(
@@ -4199,6 +4222,37 @@ var SponsorRelayController = class {
4199
4222
  };
4200
4223
  })
4201
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
+ );
4202
4256
  }
4203
4257
  let rootfsVerified = false;
4204
4258
  let rootfsResolution = null;
@@ -4229,6 +4283,8 @@ var SponsorRelayController = class {
4229
4283
  balance,
4230
4284
  crns,
4231
4285
  instances,
4286
+ bootstrapRegistrations,
4287
+ orphanBootstrapRegistrations,
4232
4288
  busy: { refreshing: false },
4233
4289
  statusText: "Relay sponsor data ready"
4234
4290
  });
@@ -4738,6 +4794,72 @@ var SponsorRelayController = class {
4738
4794
  });
4739
4795
  }
4740
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
+ }
4741
4863
  };
4742
4864
  function createSponsorRelayController(props = {}) {
4743
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;