@le-space/ui 0.7.0 → 0.8.0

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/shared/index.d.ts CHANGED
@@ -253,6 +253,12 @@ interface CompactInstanceDetails {
253
253
  vmIpv4: string | null;
254
254
  webUrl: string | null;
255
255
  sshCommand: string | null;
256
+ /**
257
+ * Hostname of the deployment's HTTPS proxy. The mapped ports are plain HTTP,
258
+ * which a browser on an HTTPS page refuses to fetch, so this is the only
259
+ * address the card can read the relay's own documents from.
260
+ */
261
+ proxyHostname: string | null;
256
262
  mappedPorts: Array<{
257
263
  label: string;
258
264
  hostPort: number | null;
@@ -260,6 +266,32 @@ interface CompactInstanceDetails {
260
266
  execution: InstanceExecution | null;
261
267
  error: string | null;
262
268
  }
269
+ /** One transport's worth of addresses, as the relay itself groups them. */
270
+ interface SponsorRelayAddressGroup {
271
+ key: string;
272
+ label: string;
273
+ addresses: string[];
274
+ }
275
+ /**
276
+ * What a deployment reports about itself at `/multiaddrs`. Deliberately not
277
+ * normalised into "the best address": the card shows what the relay says, and
278
+ * choosing between addresses is the consumer's business, not this UI's.
279
+ */
280
+ interface SponsorRelayInstanceAddresses {
281
+ status: 'idle' | 'loading' | 'ready' | 'unsupported' | 'error';
282
+ /** Transport groups, empty for a profile that publishes no libp2p addresses. */
283
+ groups: SponsorRelayAddressGroup[];
284
+ /** Ready-to-paste environment block, when the profile publishes one. */
285
+ pwaEnv: Record<string, string> | null;
286
+ /** Raw payload, rendered verbatim on the health tab. */
287
+ payload: Record<string, unknown> | null;
288
+ peerId: string | null;
289
+ version: string | null;
290
+ fetchedAt: number | null;
291
+ error: string | null;
292
+ /** Source URL, shown so a person can curl exactly what the card read. */
293
+ sourceUrl: string | null;
294
+ }
263
295
  interface CompactInstanceRecord {
264
296
  instance: InstanceMessage;
265
297
  details: CompactInstanceDetails;
@@ -324,6 +356,13 @@ interface SponsorRelayState {
324
356
  orphanBootstrapRegistrations: CompactBootstrapRegistrationRecord[];
325
357
  lastDeploymentHash: string | null;
326
358
  deploymentProgress: DeploymentProgressEvent;
359
+ /**
360
+ * What each deployment reports at `/multiaddrs`, keyed by instance item hash.
361
+ * Fetched on demand rather than with the instance list: AutoTLS addresses only
362
+ * appear a minute or two after a deploy reports success, so this is state that
363
+ * has to be refreshable on its own.
364
+ */
365
+ instanceAddresses: Record<string, SponsorRelayInstanceAddresses>;
327
366
  }
328
367
  type SponsorRelaySubscriber = (state: SponsorRelayState) => void;
329
368
 
@@ -347,7 +386,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
347
386
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
348
387
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
349
388
 
350
- declare const UI_PACKAGE_VERSION = "0.7.0";
389
+ declare const UI_PACKAGE_VERSION = "0.8.0";
351
390
 
352
391
  interface RootfsRequiredPortForward {
353
392
  port: number;
@@ -383,6 +422,14 @@ declare function connectWallet(provider?: EthereumProviderLike | null): Promise<
383
422
  declare function personalSign(address: string, message: string, provider?: EthereumProviderLike | null): Promise<string>;
384
423
  declare function watchWallet(onChange: () => void, provider?: EthereumProviderLike | null): () => void;
385
424
 
425
+ /**
426
+ * Normalise the two address documents in the fleet into one list of groups.
427
+ *
428
+ * orbitdb-relay reports `byTransport`; uc-go-peer's describe output uses
429
+ * `<transport>_multiaddrs` keys. Neither is rewritten or ranked here — the card
430
+ * shows what the relay said, and picking an address is the consumer's job.
431
+ */
432
+ declare function parseInstanceAddressPayload(payload: Record<string, unknown>): SponsorRelayInstanceAddresses;
386
433
  declare function rootfsHealth(args: {
387
434
  manifestState: RootfsManifestState;
388
435
  rootfsVerified: boolean;
@@ -438,6 +485,18 @@ declare class SponsorRelayController {
438
485
  private refreshWalletDerivedState;
439
486
  refresh(): Promise<void>;
440
487
  deploy(): Promise<void>;
488
+ /**
489
+ * Read one deployment's own address document over its HTTPS proxy.
490
+ *
491
+ * The mapped ports are plain HTTP, which a browser on an HTTPS page refuses to
492
+ * fetch, so the proxy hostname is the only way in. Deliberately single-shot:
493
+ * unlike the deploy-time readiness probe this backs a button someone pressed,
494
+ * and a caller who wants newer data presses it again.
495
+ */
496
+ loadInstanceAddresses(instanceHash: string, options?: {
497
+ force?: boolean;
498
+ }): Promise<void>;
499
+ private patchInstanceAddresses;
441
500
  deleteInstance(instanceHash: string): Promise<void>;
442
501
  deleteBootstrapRegistration(registrationHash: string): Promise<void>;
443
502
  }
@@ -450,4 +509,4 @@ interface DeploymentProgressEmitter {
450
509
  }
451
510
  declare function createDeploymentProgressEmitter(): DeploymentProgressEmitter;
452
511
 
453
- export { type CompactBootstrapRegistrationRecord, type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, DEFAULT_UCAN_STORE_ALLOWED_CAPABILITIES, DEFAULT_UCAN_STORE_MAX_DELEGATION_EXPIRATION_SECONDS, DEFAULT_UCAN_STORE_PWA_ORIGIN, DEFAULT_UCAN_STORE_SERVICE_DID, DEFAULT_UCAN_STORE_SERVICE_ORIGIN, DEFAULT_UCAN_STORE_USER_DELEGATION_EXPIRATION_SECONDS, 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 SponsorRelayUcanStoreBootstrapInput, type SponsorRelayWalletState, UI_PACKAGE_VERSION, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, formatTierSpecLabel, getEthereumProvider, joinMappedPorts, joinRequiredPortForwards, personalSign, resolveManifestSource, rootfsHealth, shortHash, toChecksumAddress, watchWallet };
512
+ export { type CompactBootstrapRegistrationRecord, type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, DEFAULT_UCAN_STORE_ALLOWED_CAPABILITIES, DEFAULT_UCAN_STORE_MAX_DELEGATION_EXPIRATION_SECONDS, DEFAULT_UCAN_STORE_PWA_ORIGIN, DEFAULT_UCAN_STORE_SERVICE_DID, DEFAULT_UCAN_STORE_SERVICE_ORIGIN, DEFAULT_UCAN_STORE_USER_DELEGATION_EXPIRATION_SECONDS, 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, type SponsorRelayAddressGroup, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayInstanceAddresses, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayUcanStoreBootstrapInput, type SponsorRelayWalletState, UI_PACKAGE_VERSION, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, formatTierSpecLabel, getEthereumProvider, joinMappedPorts, joinRequiredPortForwards, parseInstanceAddressPayload, personalSign, resolveManifestSource, rootfsHealth, shortHash, toChecksumAddress, watchWallet };
package/shared/index.js CHANGED
@@ -42,7 +42,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
42
42
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
43
43
 
44
44
  // src/shared/package-version.ts
45
- var UI_PACKAGE_VERSION = "0.7.0";
45
+ var UI_PACKAGE_VERSION = "0.8.0";
46
46
 
47
47
  // src/shared/format.ts
48
48
  function shortHash(value, head = 8, tail = 6) {
@@ -3831,7 +3831,8 @@ function defaultState(props = {}) {
3831
3831
  bootstrapRegistrations: [],
3832
3832
  orphanBootstrapRegistrations: [],
3833
3833
  lastDeploymentHash: null,
3834
- deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
3834
+ deploymentProgress: IDLE_DEPLOYMENT_PROGRESS,
3835
+ instanceAddresses: {}
3835
3836
  };
3836
3837
  }
3837
3838
  function manifestLoadErrorState(error) {
@@ -3859,6 +3860,83 @@ async function sha256Hex(payload) {
3859
3860
  );
3860
3861
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
3861
3862
  }
3863
+ function hostnameOfUrl(value) {
3864
+ if (!value) return null;
3865
+ try {
3866
+ return new URL(value).hostname || null;
3867
+ } catch {
3868
+ return null;
3869
+ }
3870
+ }
3871
+ function emptyInstanceAddresses() {
3872
+ return {
3873
+ status: "idle",
3874
+ groups: [],
3875
+ pwaEnv: null,
3876
+ payload: null,
3877
+ peerId: null,
3878
+ version: null,
3879
+ fetchedAt: null,
3880
+ error: null,
3881
+ sourceUrl: null
3882
+ };
3883
+ }
3884
+ var TRANSPORT_LABELS = {
3885
+ tcp: "TCP",
3886
+ websocket: "WebSocket",
3887
+ webrtc: "WebRTC",
3888
+ direct_tcp: "Direct TCP",
3889
+ autotls_wss: "AutoTLS WSS",
3890
+ proxy_wss: "Proxy WSS",
3891
+ webtransport: "WebTransport",
3892
+ webrtc_direct: "WebRTC Direct",
3893
+ browser_bootstrap: "Browser bootstrap",
3894
+ probe: "Probe"
3895
+ };
3896
+ function transportLabel(key) {
3897
+ return TRANSPORT_LABELS[key] ?? key.replace(/[_-]+/g, " ").replace(/^./, (first) => first.toUpperCase());
3898
+ }
3899
+ function addressList(value) {
3900
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && Boolean(entry.trim())) : [];
3901
+ }
3902
+ function parseInstanceAddressPayload(payload) {
3903
+ const groups = [];
3904
+ const byTransport = payload.byTransport;
3905
+ if (byTransport && typeof byTransport === "object") {
3906
+ for (const [key, value] of Object.entries(
3907
+ byTransport
3908
+ )) {
3909
+ const addresses = addressList(value);
3910
+ if (addresses.length > 0) {
3911
+ groups.push({ key, label: transportLabel(key), addresses });
3912
+ }
3913
+ }
3914
+ }
3915
+ for (const [key, value] of Object.entries(payload)) {
3916
+ if (!key.endsWith("_multiaddrs")) continue;
3917
+ const addresses = addressList(value);
3918
+ if (addresses.length === 0) continue;
3919
+ const groupKey = key.replace(/_multiaddrs$/, "");
3920
+ groups.push({
3921
+ key: groupKey,
3922
+ label: transportLabel(groupKey),
3923
+ addresses
3924
+ });
3925
+ }
3926
+ const pwaEnvRaw = payload.pwa_env;
3927
+ const pwaEnv = pwaEnvRaw && typeof pwaEnvRaw === "object" ? Object.fromEntries(
3928
+ Object.entries(pwaEnvRaw).filter(([, entry]) => typeof entry === "string").map(([entryKey, entry]) => [entryKey, entry])
3929
+ ) : null;
3930
+ const peerId = typeof payload.peerId === "string" ? payload.peerId : typeof payload.peer_id === "string" ? payload.peer_id : null;
3931
+ return {
3932
+ ...emptyInstanceAddresses(),
3933
+ groups,
3934
+ pwaEnv,
3935
+ payload,
3936
+ peerId,
3937
+ version: typeof payload.version === "string" ? payload.version : null
3938
+ };
3939
+ }
3862
3940
  function mappedPorts(execution) {
3863
3941
  return Object.entries(execution?.networking?.mapped_ports ?? {}).map(
3864
3942
  ([port, mapping]) => ({
@@ -4007,6 +4085,7 @@ async function inspectInstanceRuntime(args) {
4007
4085
  vmIpv4: null,
4008
4086
  webUrl: null,
4009
4087
  sshCommand: null,
4088
+ proxyHostname: null,
4010
4089
  mappedPorts: [],
4011
4090
  execution: null,
4012
4091
  error: null
@@ -4071,6 +4150,7 @@ async function inspectInstanceRuntime(args) {
4071
4150
  details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
4072
4151
  details.vmIpv4 = execution.networking.ipv4_ip ?? null;
4073
4152
  details.webUrl = execution.networking.proxy_url ?? details.webUrl;
4153
+ details.proxyHostname = hostnameOfUrl(details.webUrl);
4074
4154
  details.mappedPorts = mappedPorts(execution);
4075
4155
  details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
4076
4156
  return { details, lookup };
@@ -4420,10 +4500,14 @@ var SponsorRelayController = class {
4420
4500
  detail: "Publishing the host port mapping into the guest relay configuration."
4421
4501
  });
4422
4502
  const tcpPort = mappedPorts2["9091"]?.host ?? 0;
4423
- const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
4503
+ const wsPort = mappedPorts2["9092"]?.host ?? 0;
4424
4504
  if (!tcpPort || !wsPort) {
4505
+ const missing = [
4506
+ tcpPort ? null : "9091 (libp2p TCP)",
4507
+ wsPort ? null : "9092 (libp2p WebSocket)"
4508
+ ].filter(Boolean);
4425
4509
  throw new Error(
4426
- "OrbitDB relay runtime is missing required mapped TCP/WS ports."
4510
+ `OrbitDB relay runtime has no mapped host port for ${missing.join(" and ")}. Deploy a rootfs whose manifest requests these forwards.`
4427
4511
  );
4428
4512
  }
4429
4513
  const configureResult = await configureOrbitdbRelaySetup2({
@@ -5406,6 +5490,7 @@ var SponsorRelayController = class {
5406
5490
  vmIpv4: null,
5407
5491
  webUrl: null,
5408
5492
  sshCommand: null,
5493
+ proxyHostname: null,
5409
5494
  mappedPorts: [],
5410
5495
  execution: null,
5411
5496
  error: null
@@ -5821,6 +5906,9 @@ var SponsorRelayController = class {
5821
5906
  vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
5822
5907
  webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
5823
5908
  sshCommand: runtime.sshCommand ?? null,
5909
+ proxyHostname: hostnameOfUrl(
5910
+ runtime.proxyUrl ?? runtime.webAccessUrl ?? runtime.execution?.networking?.proxy_url ?? null
5911
+ ),
5824
5912
  mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
5825
5913
  execution: runtime.execution ?? null,
5826
5914
  error: null
@@ -5918,6 +6006,9 @@ var SponsorRelayController = class {
5918
6006
  vmIpv4: latestRuntime.execution?.networking?.ipv4_ip ?? null,
5919
6007
  webUrl: latestRuntime.webAccessUrl ?? latestRuntime.proxyUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null,
5920
6008
  sshCommand: latestRuntime.sshCommand ?? null,
6009
+ proxyHostname: hostnameOfUrl(
6010
+ latestRuntime.proxyUrl ?? latestRuntime.webAccessUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null
6011
+ ),
5921
6012
  mappedPorts: latestRuntime.execution != null ? mappedPorts(latestRuntime.execution) : runtimeMappedPorts(latestRuntime.mappedPorts),
5922
6013
  execution: latestRuntime.execution ?? null,
5923
6014
  error: null
@@ -6098,6 +6189,82 @@ var SponsorRelayController = class {
6098
6189
  });
6099
6190
  }
6100
6191
  }
6192
+ /**
6193
+ * Read one deployment's own address document over its HTTPS proxy.
6194
+ *
6195
+ * The mapped ports are plain HTTP, which a browser on an HTTPS page refuses to
6196
+ * fetch, so the proxy hostname is the only way in. Deliberately single-shot:
6197
+ * unlike the deploy-time readiness probe this backs a button someone pressed,
6198
+ * and a caller who wants newer data presses it again.
6199
+ */
6200
+ async loadInstanceAddresses(instanceHash, options = {}) {
6201
+ const existing = this.state.instanceAddresses[instanceHash];
6202
+ if (!options.force && existing && existing.status !== "idle") return;
6203
+ const record = this.state.instances.find(
6204
+ (entry) => entry.instance.item_hash === instanceHash
6205
+ );
6206
+ const proxyHostname = record?.details.proxyHostname ?? null;
6207
+ if (!proxyHostname) {
6208
+ this.patchInstanceAddresses(instanceHash, {
6209
+ ...emptyInstanceAddresses(),
6210
+ status: "unsupported",
6211
+ error: "This deployment has no HTTPS proxy hostname yet, so its addresses cannot be read from the browser."
6212
+ });
6213
+ return;
6214
+ }
6215
+ const sourceUrl = `https://${proxyHostname}/multiaddrs`;
6216
+ this.patchInstanceAddresses(instanceHash, {
6217
+ ...existing ?? emptyInstanceAddresses(),
6218
+ status: "loading",
6219
+ sourceUrl,
6220
+ error: null
6221
+ });
6222
+ try {
6223
+ const abort = new AbortController();
6224
+ const timeout = globalThis.setTimeout(() => abort.abort(), 1e4);
6225
+ const response = await fetch(sourceUrl, {
6226
+ cache: "no-cache",
6227
+ signal: abort.signal
6228
+ });
6229
+ globalThis.clearTimeout(timeout);
6230
+ if (response.status === 404) {
6231
+ this.patchInstanceAddresses(instanceHash, {
6232
+ ...emptyInstanceAddresses(),
6233
+ status: "unsupported",
6234
+ sourceUrl,
6235
+ fetchedAt: Date.now(),
6236
+ error: "This profile does not publish an address document."
6237
+ });
6238
+ return;
6239
+ }
6240
+ if (!response.ok) {
6241
+ throw new Error(`${sourceUrl} answered ${response.status}.`);
6242
+ }
6243
+ const payload = await response.json();
6244
+ this.patchInstanceAddresses(instanceHash, {
6245
+ ...parseInstanceAddressPayload(payload),
6246
+ status: "ready",
6247
+ sourceUrl,
6248
+ fetchedAt: Date.now(),
6249
+ error: null
6250
+ });
6251
+ } catch (error) {
6252
+ this.patchInstanceAddresses(instanceHash, {
6253
+ ...this.state.instanceAddresses[instanceHash] ?? emptyInstanceAddresses(),
6254
+ status: "error",
6255
+ sourceUrl,
6256
+ error: error instanceof Error ? error.message : String(error)
6257
+ });
6258
+ }
6259
+ }
6260
+ patchInstanceAddresses(instanceHash, value) {
6261
+ this.patch({
6262
+ instanceAddresses: {
6263
+ ...this.state.instanceAddresses,
6264
+ [instanceHash]: value
6265
+ }
6266
+ });
6267
+ }
6101
6268
  async deleteInstance(instanceHash) {
6102
6269
  this.trace("delete:start", { instanceHash });
6103
6270
  if (!this.state.wallet.address) {
@@ -6302,6 +6469,7 @@ export {
6302
6469
  getEthereumProvider,
6303
6470
  joinMappedPorts,
6304
6471
  joinRequiredPortForwards,
6472
+ parseInstanceAddressPayload,
6305
6473
  personalSign,
6306
6474
  resolveManifestSource,
6307
6475
  rootfsHealth,
package/styles.css CHANGED
@@ -42,6 +42,11 @@
42
42
  --relay-surface: rgba(11, 14, 21, 0.55);
43
43
  --relay-surface-border: var(--relay-horizon);
44
44
  --relay-field-bg: rgba(11, 14, 21, 0.85);
45
+ /* Opaque on purpose. A select's drop-down takes the control's own colours on
46
+ the platforms that honour author styling, and it opens over the page rather
47
+ than over the panel - a translucent background would composite against
48
+ whatever is behind it and leave light text on a light list. */
49
+ --relay-menu-bg: var(--relay-deep-space);
45
50
  --relay-accent: var(--relay-coral);
46
51
  --relay-accent-contrast: #0b0e15;
47
52
  --relay-link: var(--relay-cyan);
@@ -81,6 +86,7 @@
81
86
  --relay-surface: rgba(20, 27, 46, 0.04);
82
87
  --relay-surface-border: #d9e0ec;
83
88
  --relay-field-bg: #ffffff;
89
+ --relay-menu-bg: #ffffff;
84
90
  --relay-accent: var(--relay-coral);
85
91
  --relay-accent-contrast: #ffffff;
86
92
  --relay-link: var(--relay-cyan);