@le-space/ui 0.9.2 → 0.9.5

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.9.2",
3
+ "version": "0.9.5",
4
4
  "description": "Floating Aleph Relay Button UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,8 +44,8 @@
44
44
  "url": "https://github.com/NiKrause/relay-button/issues"
45
45
  },
46
46
  "dependencies": {
47
- "@le-space/browser": "0.9.2",
48
- "@le-space/core": "0.9.2",
47
+ "@le-space/browser": "0.9.5",
48
+ "@le-space/core": "0.9.5",
49
49
  "viem": "^2.53.1"
50
50
  },
51
51
  "peerDependencies": {
package/react/index.d.ts CHANGED
@@ -424,6 +424,7 @@ declare class SponsorRelayController {
424
424
  private emit;
425
425
  private trace;
426
426
  private patch;
427
+ private lastProgressLabel;
427
428
  private emitProgress;
428
429
  private configureRelayBootstrapRegistration;
429
430
  private probeRelayReachableOverProxy;
package/react/index.js CHANGED
@@ -96,7 +96,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
96
96
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
97
97
 
98
98
  // src/shared/package-version.ts
99
- var UI_PACKAGE_VERSION = "0.9.2";
99
+ var UI_PACKAGE_VERSION = "0.9.5";
100
100
 
101
101
  // src/shared/format.ts
102
102
  function shortHash(value, head = 8, tail = 6) {
@@ -2830,7 +2830,8 @@ function browserMultiaddrTransport(addr) {
2830
2830
  }
2831
2831
  function selectCompactRelayBootstrapMultiaddrs(addrs, limit = DEFAULT_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT) {
2832
2832
  const entries = filterPublicMultiaddrs(addrs, {
2833
- browserDialableOnly: true
2833
+ browserDialableOnly: true,
2834
+ allowInsecureWebSockets: true
2834
2835
  }).map((addr, index) => ({
2835
2836
  addr,
2836
2837
  index,
@@ -2900,12 +2901,23 @@ function isLocalHostname(host) {
2900
2901
  function hasPeerId(addr) {
2901
2902
  return splitMultiaddr(addr).includes("p2p");
2902
2903
  }
2903
- function isBrowserDialableMultiaddr(addr) {
2904
+ function isSecureWebSocketMultiaddr(addr) {
2905
+ const parts = splitMultiaddr(addr.toLowerCase());
2906
+ if (parts.includes("wss")) return true;
2907
+ const wsIndex = parts.indexOf("ws");
2908
+ return wsIndex > 0 && parts.lastIndexOf("tls", wsIndex) >= 0;
2909
+ }
2910
+ function isWebSocketMultiaddr(addr) {
2911
+ const parts = splitMultiaddr(addr.toLowerCase());
2912
+ return parts.includes("ws") || parts.includes("wss");
2913
+ }
2914
+ function isBrowserDialableMultiaddr(addr, options = {}) {
2904
2915
  const normalized = addr.toLowerCase();
2905
2916
  if (normalized.includes("/webtransport") || normalized.includes("/webrtc-direct")) {
2906
2917
  return normalized.includes("/certhash/");
2907
2918
  }
2908
- return normalized.includes("/ws") || normalized.includes("/wss");
2919
+ if (!isWebSocketMultiaddr(addr)) return false;
2920
+ return (options.allowInsecureWebSockets ?? false) || isSecureWebSocketMultiaddr(addr);
2909
2921
  }
2910
2922
  function dedupeMultiaddrs(addrs) {
2911
2923
  const seen = /* @__PURE__ */ new Set();
@@ -2939,7 +2951,9 @@ function filterPublicMultiaddrs(addrs, options = {}) {
2939
2951
  return dedupeMultiaddrs(addrs).filter((addr) => {
2940
2952
  if (!isPublicMultiaddr(addr)) return false;
2941
2953
  if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
2942
- if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr)) {
2954
+ if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr, {
2955
+ allowInsecureWebSockets: options.allowInsecureWebSockets
2956
+ })) {
2943
2957
  return false;
2944
2958
  }
2945
2959
  return true;
@@ -3054,7 +3068,8 @@ function buildRelayBootstrapPostContent(args) {
3054
3068
  const updatedAt = args.now ?? Date.now();
3055
3069
  const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
3056
3070
  const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
3057
- browserDialableOnly: true
3071
+ browserDialableOnly: true,
3072
+ allowInsecureWebSockets: true
3058
3073
  }) : void 0;
3059
3074
  const compactMultiaddrs = selectCompactRelayBootstrapMultiaddrs(
3060
3075
  fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
@@ -3136,7 +3151,8 @@ async function signRelayBootstrapAuthorization(args) {
3136
3151
  async function signRelayBootstrapProof(args) {
3137
3152
  const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
3138
3153
  const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
3139
- browserDialableOnly: true
3154
+ browserDialableOnly: true,
3155
+ allowInsecureWebSockets: true
3140
3156
  }) : void 0;
3141
3157
  const compactMultiaddrs = selectCompactRelayBootstrapMultiaddrs(
3142
3158
  fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
@@ -3191,20 +3207,24 @@ async function fetchAlephBootstrapPosts(options = {}) {
3191
3207
  }
3192
3208
  const payload = await response.json();
3193
3209
  const posts = (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null).sort((left, right) => compareRelayBootstrapPostRecency(right, left));
3194
- posts.forEach(assertSupportedRelayBootstrapPost);
3195
- return posts;
3210
+ return usableRelayBootstrapPosts(posts, options.onUnsupportedPost);
3196
3211
  }
3197
- function assertSupportedRelayBootstrapPost(post) {
3212
+ function unsupportedRelayBootstrapReason(post) {
3198
3213
  if (post.type === "relay-bootstrap") {
3199
- throw new Error(
3200
- "Legacy relay-bootstrap record encountered. Only relay-bootstrap-v2 is supported."
3201
- );
3214
+ return "legacy relay-bootstrap record; only relay-bootstrap-v2 is supported";
3202
3215
  }
3203
3216
  if (post.type && post.type !== DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE) {
3204
- throw new Error(
3205
- `Unsupported relay bootstrap post type: ${post.type}. Expected ${DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE}.`
3206
- );
3217
+ return `unsupported post type ${post.type}; expected ${DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE}`;
3207
3218
  }
3219
+ return null;
3220
+ }
3221
+ function usableRelayBootstrapPosts(posts, onUnsupportedPost) {
3222
+ return posts.filter((post) => {
3223
+ const reason = unsupportedRelayBootstrapReason(post);
3224
+ if (!reason) return true;
3225
+ onUnsupportedPost?.({ hash: post.itemHash ?? post.hash, reason });
3226
+ return false;
3227
+ });
3208
3228
  }
3209
3229
  function compareRelayBootstrapPostRecency(left, right) {
3210
3230
  const leftUpdatedAt = left.content?.updatedAt ?? 0;
@@ -3225,8 +3245,7 @@ function selectCurrentRelayBootstrapPosts(posts, options = {}) {
3225
3245
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_BOOTSTRAP_MAX_AGE_MS;
3226
3246
  const now = options.now ?? Date.now();
3227
3247
  const selected = /* @__PURE__ */ new Map();
3228
- for (const post of posts) {
3229
- assertSupportedRelayBootstrapPost(post);
3248
+ for (const post of usableRelayBootstrapPosts(posts, options.onUnsupportedPost)) {
3230
3249
  const content = post.content;
3231
3250
  if (!content) continue;
3232
3251
  if (now - content.updatedAt > maxAgeMs) continue;
@@ -3938,12 +3957,51 @@ function validateUcanStoreBootstrapPackage(value) {
3938
3957
  function deploymentNoun(profile) {
3939
3958
  return profile === "ucan-store" ? "service" : "relay";
3940
3959
  }
3960
+ function describeRequest(input, init) {
3961
+ const method = String(init?.method ?? "GET").toUpperCase();
3962
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : typeof input?.url === "string" ? input.url : "<unknown url>";
3963
+ return `${method} ${url}`;
3964
+ }
3965
+ function describeThrown(error) {
3966
+ if (error instanceof Error) {
3967
+ const cause = error.cause;
3968
+ const causeText = cause != null && cause !== error ? ` (cause: ${describeThrown(cause)})` : "";
3969
+ return `${error.message}${causeText}`;
3970
+ }
3971
+ if (typeof error === "object" && error !== null) {
3972
+ const message = error.message;
3973
+ if (typeof message === "string" && message.trim()) return message;
3974
+ try {
3975
+ const json = JSON.stringify(error);
3976
+ if (json && json !== "{}") {
3977
+ return json.length > 400 ? `${json.slice(0, 400)}\u2026` : json;
3978
+ }
3979
+ } catch {
3980
+ }
3981
+ const name = error.constructor?.name;
3982
+ return name && name !== "Object" ? `<${name}>` : "<object with no message>";
3983
+ }
3984
+ return String(error);
3985
+ }
3986
+ function deploymentFailureMessage(error, stage) {
3987
+ const message = describeThrown(error);
3988
+ const step = String(stage ?? "").trim();
3989
+ return step ? `${message} (while: ${step})` : message;
3990
+ }
3941
3991
  function asJsonFetch(input, init) {
3942
- return fetch(input, init).then(async (response) => ({
3943
- ok: response.ok,
3944
- status: response.status,
3945
- json: async () => await response.json()
3946
- }));
3992
+ return fetch(input, init).then(
3993
+ async (response) => ({
3994
+ ok: response.ok,
3995
+ status: response.status,
3996
+ json: async () => await response.json()
3997
+ }),
3998
+ (error) => {
3999
+ const reason = describeThrown(error);
4000
+ throw new Error(`${reason} (${describeRequest(input, init)})`, {
4001
+ cause: error
4002
+ });
4003
+ }
4004
+ );
3947
4005
  }
3948
4006
  function isConfirmedGuestSetupResult(value) {
3949
4007
  return Boolean(
@@ -4126,7 +4184,7 @@ function defaultState(props = {}) {
4126
4184
  };
4127
4185
  }
4128
4186
  function manifestLoadErrorState(error) {
4129
- const message = error instanceof Error ? error.message : String(error);
4187
+ const message = describeThrown(error);
4130
4188
  return {
4131
4189
  manifest: null,
4132
4190
  valid: false,
@@ -4171,7 +4229,7 @@ function hostnameOfUrl(value) {
4171
4229
  }
4172
4230
  var INSTANCE_ADDRESS_TIMEOUT_MS = 1e4;
4173
4231
  function describeInstanceAddressFailure(error, sourceUrl) {
4174
- const raw = error instanceof Error ? error.message : String(error);
4232
+ const raw = describeThrown(error);
4175
4233
  if (error instanceof Error && error.name === "AbortError") {
4176
4234
  return `${sourceUrl} did not answer within ${INSTANCE_ADDRESS_TIMEOUT_MS / 1e3}s. Press Refresh to try again.`;
4177
4235
  }
@@ -4470,7 +4528,7 @@ async function inspectInstanceRuntime(args) {
4470
4528
  return {
4471
4529
  details: {
4472
4530
  ...details,
4473
- error: error instanceof Error ? error.message : String(error)
4531
+ error: describeThrown(error)
4474
4532
  },
4475
4533
  lookup
4476
4534
  };
@@ -4586,11 +4644,15 @@ var SponsorRelayController = class {
4586
4644
  };
4587
4645
  this.emit();
4588
4646
  }
4647
+ lastProgressLabel = null;
4589
4648
  emitProgress(event) {
4590
4649
  const nextEvent = {
4591
4650
  ...event,
4592
4651
  timestamp: Date.now()
4593
4652
  };
4653
+ if (nextEvent.stage !== "error") {
4654
+ this.lastProgressLabel = nextEvent.label;
4655
+ }
4594
4656
  this.trace(`progress:${nextEvent.stage}`, {
4595
4657
  label: nextEvent.label,
4596
4658
  progress: nextEvent.progress,
@@ -5295,7 +5357,7 @@ var SponsorRelayController = class {
5295
5357
  this.trace("deploy:bootstrap-config-cleanup-error", {
5296
5358
  itemHash: args.itemHash,
5297
5359
  deploymentToken: args.deploymentToken,
5298
- error: error instanceof Error ? error.message : String(error)
5360
+ error: describeThrown(error)
5299
5361
  });
5300
5362
  });
5301
5363
  await this.cleanupStaleVmBootstrapConfigHistory({
@@ -5363,7 +5425,7 @@ var SponsorRelayController = class {
5363
5425
  this.trace("deploy:bootstrap-config-history-scan-error", {
5364
5426
  itemHash: args.itemHash,
5365
5427
  currentAggregateItemHash: args.currentAggregateItemHash,
5366
- error: error instanceof Error ? error.message : String(error)
5428
+ error: describeThrown(error)
5367
5429
  });
5368
5430
  return [];
5369
5431
  });
@@ -5406,7 +5468,7 @@ var SponsorRelayController = class {
5406
5468
  itemHash: args.itemHash,
5407
5469
  currentAggregateItemHash: args.currentAggregateItemHash,
5408
5470
  staleHashes,
5409
- error: error instanceof Error ? error.message : String(error)
5471
+ error: describeThrown(error)
5410
5472
  });
5411
5473
  });
5412
5474
  }
@@ -5727,7 +5789,7 @@ var SponsorRelayController = class {
5727
5789
  } catch (error) {
5728
5790
  this.patch({
5729
5791
  busy: { connectingWallet: false },
5730
- errorText: error instanceof Error ? error.message : String(error),
5792
+ errorText: describeThrown(error),
5731
5793
  statusText: "Wallet connection failed"
5732
5794
  });
5733
5795
  }
@@ -5943,7 +6005,7 @@ var SponsorRelayController = class {
5943
6005
  this.trace("refresh:error", error);
5944
6006
  this.patch({
5945
6007
  busy: { refreshing: false },
5946
- errorText: error instanceof Error ? error.message : String(error),
6008
+ errorText: describeThrown(error),
5947
6009
  statusText: "Refresh failed"
5948
6010
  });
5949
6011
  }
@@ -6278,7 +6340,7 @@ var SponsorRelayController = class {
6278
6340
  });
6279
6341
  return;
6280
6342
  } catch (error) {
6281
- lastError = error instanceof Error ? error : new Error(String(error));
6343
+ lastError = error instanceof Error ? error : new Error(describeThrown(error), { cause: error });
6282
6344
  const attemptLabel = crnDisplayName;
6283
6345
  const failedDueToInvalidIpv6 = /not globally routable|public guest IPv6/i.test(lastError.message);
6284
6346
  attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
@@ -6507,10 +6569,15 @@ var SponsorRelayController = class {
6507
6569
  attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
6508
6570
  );
6509
6571
  } catch (error) {
6510
- this.trace("deploy:error", error);
6572
+ const message = deploymentFailureMessage(error, this.lastProgressLabel);
6573
+ this.trace("deploy:error", {
6574
+ message,
6575
+ stage: this.lastProgressLabel,
6576
+ error
6577
+ });
6511
6578
  this.patch({
6512
6579
  busy: { deploying: false },
6513
- errorText: error instanceof Error ? error.message : String(error),
6580
+ errorText: message,
6514
6581
  statusText: "Deployment failed"
6515
6582
  });
6516
6583
  this.emitProgress({
@@ -6518,7 +6585,7 @@ var SponsorRelayController = class {
6518
6585
  label: "Deployment failed",
6519
6586
  progress: 100,
6520
6587
  status: "error",
6521
- error: error instanceof Error ? error.message : String(error)
6588
+ error: message
6522
6589
  });
6523
6590
  }
6524
6591
  }
@@ -6702,7 +6769,7 @@ var SponsorRelayController = class {
6702
6769
  this.trace("delete:error", error);
6703
6770
  this.patch({
6704
6771
  busy: { deletingInstanceHash: null },
6705
- errorText: error instanceof Error ? error.message : String(error),
6772
+ errorText: describeThrown(error),
6706
6773
  statusText: "Delete failed"
6707
6774
  });
6708
6775
  }
@@ -6768,7 +6835,7 @@ var SponsorRelayController = class {
6768
6835
  this.trace("registration-delete:error", error);
6769
6836
  this.patch({
6770
6837
  busy: { deletingRegistrationHash: null },
6771
- errorText: error instanceof Error ? error.message : String(error),
6838
+ errorText: describeThrown(error),
6772
6839
  statusText: "Registration cleanup failed"
6773
6840
  });
6774
6841
  }
package/shared/index.d.ts CHANGED
@@ -480,7 +480,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
480
480
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
481
481
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
482
482
 
483
- declare const UI_PACKAGE_VERSION = "0.9.2";
483
+ declare const UI_PACKAGE_VERSION = "0.9.5";
484
484
 
485
485
  interface RootfsRequiredPortForward {
486
486
  port: number;
@@ -516,6 +516,48 @@ declare function connectWallet(provider?: EthereumProviderLike | null): Promise<
516
516
  declare function personalSign(address: string, message: string, provider?: EthereumProviderLike | null): Promise<string>;
517
517
  declare function watchWallet(onChange: () => void, provider?: EthereumProviderLike | null): () => void;
518
518
 
519
+ /**
520
+ * What a request was, for an error that otherwise says nothing.
521
+ *
522
+ * `fetch` rejects with a bare `TypeError: Failed to fetch` for every network
523
+ * failure — no URL, no method, no cause. Carried up through the deploy path
524
+ * that becomes "Relay Button deployment failed: Failed to fetch", which is
525
+ * where a run of this project ended with no way to tell which of a dozen calls
526
+ * had failed.
527
+ *
528
+ * Exported for its test; `init.method` is read defensively because a call site
529
+ * may pass none.
530
+ */
531
+ declare function describeRequest(input: RequestInfo | URL, init?: RequestInit): string;
532
+ /**
533
+ * The failure message, with the step it happened in.
534
+ *
535
+ * A deployment walks two dozen calls across Aleph, a CRN and a guest VM. When
536
+ * one of them fails with a message that names none of them — `Failed to fetch`
537
+ * is the standard case — the step is the only thing that narrows it down, and
538
+ * it is already known.
539
+ *
540
+ * Exported for its test.
541
+ */
542
+ /**
543
+ * Say what was thrown, even when it was not an `Error`.
544
+ *
545
+ * `String(value)` on a plain object is `"[object Object]"`, and that is what a
546
+ * user saw when every CRN failed at once: five candidates, five identical
547
+ * `[object Object]`, and no way to tell whether they had failed for one reason
548
+ * or five. Not every rejection in this path is an `Error` — an Aleph or CRN
549
+ * response rejected as `{ code, message }` is an object, and so is anything a
550
+ * dependency throws as a bare literal.
551
+ *
552
+ * Order matters: a `message` field is what the thrower meant to say, so it wins
553
+ * over a dump of the whole object. The dump is the fallback, truncated, because
554
+ * an unreadable line is still better than a line saying nothing — and a cyclic
555
+ * object must not take the error handler down with it.
556
+ *
557
+ * Exported for its test.
558
+ */
559
+ declare function describeThrown(error: unknown): string;
560
+ declare function deploymentFailureMessage(error: unknown, stage: string | null | undefined): string;
519
561
  /**
520
562
  * Normalise the two address documents in the fleet into one list of groups.
521
563
  *
@@ -550,6 +592,7 @@ declare class SponsorRelayController {
550
592
  private emit;
551
593
  private trace;
552
594
  private patch;
595
+ private lastProgressLabel;
553
596
  private emitProgress;
554
597
  private configureRelayBootstrapRegistration;
555
598
  private probeRelayReachableOverProxy;
@@ -603,4 +646,4 @@ interface DeploymentProgressEmitter {
603
646
  }
604
647
  declare function createDeploymentProgressEmitter(): DeploymentProgressEmitter;
605
648
 
606
- export { type CompactBootstrapRegistrationRecord, type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_FAB_CORNER, 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, FAB_DRAG_THRESHOLD, FAB_EDGE_MARGIN, type FabCorner, type FabPanelPlacement, type FabPanelSide, type FabPlacement, type FabPoint, type FabSize, type FabViewport, 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, anchorPanelToLauncher, buildSshCommand, clampToViewport, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, dragExceedsThreshold, formatDateTime, formatNumber, formatTierSpecLabel, getEthereumProvider, joinMappedPorts, joinRequiredPortForwards, parseInstanceAddressPayload, personalSign, readStoredPosition, resolveCornerPlacement, resolveManifestSource, rootfsHealth, shortHash, toChecksumAddress, watchWallet, writeStoredPosition };
649
+ export { type CompactBootstrapRegistrationRecord, type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_FAB_CORNER, 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, FAB_DRAG_THRESHOLD, FAB_EDGE_MARGIN, type FabCorner, type FabPanelPlacement, type FabPanelSide, type FabPlacement, type FabPoint, type FabSize, type FabViewport, 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, anchorPanelToLauncher, buildSshCommand, clampToViewport, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, deploymentFailureMessage, describeRequest, describeThrown, dragExceedsThreshold, formatDateTime, formatNumber, formatTierSpecLabel, getEthereumProvider, joinMappedPorts, joinRequiredPortForwards, parseInstanceAddressPayload, personalSign, readStoredPosition, resolveCornerPlacement, resolveManifestSource, rootfsHealth, shortHash, toChecksumAddress, watchWallet, writeStoredPosition };
package/shared/index.js CHANGED
@@ -96,7 +96,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
96
96
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
97
97
 
98
98
  // src/shared/package-version.ts
99
- var UI_PACKAGE_VERSION = "0.9.2";
99
+ var UI_PACKAGE_VERSION = "0.9.5";
100
100
 
101
101
  // src/shared/format.ts
102
102
  function shortHash(value, head = 8, tail = 6) {
@@ -2830,7 +2830,8 @@ function browserMultiaddrTransport(addr) {
2830
2830
  }
2831
2831
  function selectCompactRelayBootstrapMultiaddrs(addrs, limit = DEFAULT_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT) {
2832
2832
  const entries = filterPublicMultiaddrs(addrs, {
2833
- browserDialableOnly: true
2833
+ browserDialableOnly: true,
2834
+ allowInsecureWebSockets: true
2834
2835
  }).map((addr, index) => ({
2835
2836
  addr,
2836
2837
  index,
@@ -2900,12 +2901,23 @@ function isLocalHostname(host) {
2900
2901
  function hasPeerId(addr) {
2901
2902
  return splitMultiaddr(addr).includes("p2p");
2902
2903
  }
2903
- function isBrowserDialableMultiaddr(addr) {
2904
+ function isSecureWebSocketMultiaddr(addr) {
2905
+ const parts = splitMultiaddr(addr.toLowerCase());
2906
+ if (parts.includes("wss")) return true;
2907
+ const wsIndex = parts.indexOf("ws");
2908
+ return wsIndex > 0 && parts.lastIndexOf("tls", wsIndex) >= 0;
2909
+ }
2910
+ function isWebSocketMultiaddr(addr) {
2911
+ const parts = splitMultiaddr(addr.toLowerCase());
2912
+ return parts.includes("ws") || parts.includes("wss");
2913
+ }
2914
+ function isBrowserDialableMultiaddr(addr, options = {}) {
2904
2915
  const normalized = addr.toLowerCase();
2905
2916
  if (normalized.includes("/webtransport") || normalized.includes("/webrtc-direct")) {
2906
2917
  return normalized.includes("/certhash/");
2907
2918
  }
2908
- return normalized.includes("/ws") || normalized.includes("/wss");
2919
+ if (!isWebSocketMultiaddr(addr)) return false;
2920
+ return (options.allowInsecureWebSockets ?? false) || isSecureWebSocketMultiaddr(addr);
2909
2921
  }
2910
2922
  function dedupeMultiaddrs(addrs) {
2911
2923
  const seen = /* @__PURE__ */ new Set();
@@ -2939,7 +2951,9 @@ function filterPublicMultiaddrs(addrs, options = {}) {
2939
2951
  return dedupeMultiaddrs(addrs).filter((addr) => {
2940
2952
  if (!isPublicMultiaddr(addr)) return false;
2941
2953
  if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
2942
- if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr)) {
2954
+ if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr, {
2955
+ allowInsecureWebSockets: options.allowInsecureWebSockets
2956
+ })) {
2943
2957
  return false;
2944
2958
  }
2945
2959
  return true;
@@ -3054,7 +3068,8 @@ function buildRelayBootstrapPostContent(args) {
3054
3068
  const updatedAt = args.now ?? Date.now();
3055
3069
  const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
3056
3070
  const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
3057
- browserDialableOnly: true
3071
+ browserDialableOnly: true,
3072
+ allowInsecureWebSockets: true
3058
3073
  }) : void 0;
3059
3074
  const compactMultiaddrs = selectCompactRelayBootstrapMultiaddrs(
3060
3075
  fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
@@ -3136,7 +3151,8 @@ async function signRelayBootstrapAuthorization(args) {
3136
3151
  async function signRelayBootstrapProof(args) {
3137
3152
  const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
3138
3153
  const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
3139
- browserDialableOnly: true
3154
+ browserDialableOnly: true,
3155
+ allowInsecureWebSockets: true
3140
3156
  }) : void 0;
3141
3157
  const compactMultiaddrs = selectCompactRelayBootstrapMultiaddrs(
3142
3158
  fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
@@ -3191,20 +3207,24 @@ async function fetchAlephBootstrapPosts(options = {}) {
3191
3207
  }
3192
3208
  const payload = await response.json();
3193
3209
  const posts = (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null).sort((left, right) => compareRelayBootstrapPostRecency(right, left));
3194
- posts.forEach(assertSupportedRelayBootstrapPost);
3195
- return posts;
3210
+ return usableRelayBootstrapPosts(posts, options.onUnsupportedPost);
3196
3211
  }
3197
- function assertSupportedRelayBootstrapPost(post) {
3212
+ function unsupportedRelayBootstrapReason(post) {
3198
3213
  if (post.type === "relay-bootstrap") {
3199
- throw new Error(
3200
- "Legacy relay-bootstrap record encountered. Only relay-bootstrap-v2 is supported."
3201
- );
3214
+ return "legacy relay-bootstrap record; only relay-bootstrap-v2 is supported";
3202
3215
  }
3203
3216
  if (post.type && post.type !== DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE) {
3204
- throw new Error(
3205
- `Unsupported relay bootstrap post type: ${post.type}. Expected ${DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE}.`
3206
- );
3217
+ return `unsupported post type ${post.type}; expected ${DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE}`;
3207
3218
  }
3219
+ return null;
3220
+ }
3221
+ function usableRelayBootstrapPosts(posts, onUnsupportedPost) {
3222
+ return posts.filter((post) => {
3223
+ const reason = unsupportedRelayBootstrapReason(post);
3224
+ if (!reason) return true;
3225
+ onUnsupportedPost?.({ hash: post.itemHash ?? post.hash, reason });
3226
+ return false;
3227
+ });
3208
3228
  }
3209
3229
  function compareRelayBootstrapPostRecency(left, right) {
3210
3230
  const leftUpdatedAt = left.content?.updatedAt ?? 0;
@@ -3225,8 +3245,7 @@ function selectCurrentRelayBootstrapPosts(posts, options = {}) {
3225
3245
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_BOOTSTRAP_MAX_AGE_MS;
3226
3246
  const now = options.now ?? Date.now();
3227
3247
  const selected = /* @__PURE__ */ new Map();
3228
- for (const post of posts) {
3229
- assertSupportedRelayBootstrapPost(post);
3248
+ for (const post of usableRelayBootstrapPosts(posts, options.onUnsupportedPost)) {
3230
3249
  const content = post.content;
3231
3250
  if (!content) continue;
3232
3251
  if (now - content.updatedAt > maxAgeMs) continue;
@@ -3938,12 +3957,51 @@ function validateUcanStoreBootstrapPackage(value) {
3938
3957
  function deploymentNoun(profile) {
3939
3958
  return profile === "ucan-store" ? "service" : "relay";
3940
3959
  }
3960
+ function describeRequest(input, init) {
3961
+ const method = String(init?.method ?? "GET").toUpperCase();
3962
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : typeof input?.url === "string" ? input.url : "<unknown url>";
3963
+ return `${method} ${url}`;
3964
+ }
3965
+ function describeThrown(error) {
3966
+ if (error instanceof Error) {
3967
+ const cause = error.cause;
3968
+ const causeText = cause != null && cause !== error ? ` (cause: ${describeThrown(cause)})` : "";
3969
+ return `${error.message}${causeText}`;
3970
+ }
3971
+ if (typeof error === "object" && error !== null) {
3972
+ const message = error.message;
3973
+ if (typeof message === "string" && message.trim()) return message;
3974
+ try {
3975
+ const json = JSON.stringify(error);
3976
+ if (json && json !== "{}") {
3977
+ return json.length > 400 ? `${json.slice(0, 400)}\u2026` : json;
3978
+ }
3979
+ } catch {
3980
+ }
3981
+ const name = error.constructor?.name;
3982
+ return name && name !== "Object" ? `<${name}>` : "<object with no message>";
3983
+ }
3984
+ return String(error);
3985
+ }
3986
+ function deploymentFailureMessage(error, stage) {
3987
+ const message = describeThrown(error);
3988
+ const step = String(stage ?? "").trim();
3989
+ return step ? `${message} (while: ${step})` : message;
3990
+ }
3941
3991
  function asJsonFetch(input, init) {
3942
- return fetch(input, init).then(async (response) => ({
3943
- ok: response.ok,
3944
- status: response.status,
3945
- json: async () => await response.json()
3946
- }));
3992
+ return fetch(input, init).then(
3993
+ async (response) => ({
3994
+ ok: response.ok,
3995
+ status: response.status,
3996
+ json: async () => await response.json()
3997
+ }),
3998
+ (error) => {
3999
+ const reason = describeThrown(error);
4000
+ throw new Error(`${reason} (${describeRequest(input, init)})`, {
4001
+ cause: error
4002
+ });
4003
+ }
4004
+ );
3947
4005
  }
3948
4006
  function isConfirmedGuestSetupResult(value) {
3949
4007
  return Boolean(
@@ -4126,7 +4184,7 @@ function defaultState(props = {}) {
4126
4184
  };
4127
4185
  }
4128
4186
  function manifestLoadErrorState(error) {
4129
- const message = error instanceof Error ? error.message : String(error);
4187
+ const message = describeThrown(error);
4130
4188
  return {
4131
4189
  manifest: null,
4132
4190
  valid: false,
@@ -4171,7 +4229,7 @@ function hostnameOfUrl(value) {
4171
4229
  }
4172
4230
  var INSTANCE_ADDRESS_TIMEOUT_MS = 1e4;
4173
4231
  function describeInstanceAddressFailure(error, sourceUrl) {
4174
- const raw = error instanceof Error ? error.message : String(error);
4232
+ const raw = describeThrown(error);
4175
4233
  if (error instanceof Error && error.name === "AbortError") {
4176
4234
  return `${sourceUrl} did not answer within ${INSTANCE_ADDRESS_TIMEOUT_MS / 1e3}s. Press Refresh to try again.`;
4177
4235
  }
@@ -4470,7 +4528,7 @@ async function inspectInstanceRuntime(args) {
4470
4528
  return {
4471
4529
  details: {
4472
4530
  ...details,
4473
- error: error instanceof Error ? error.message : String(error)
4531
+ error: describeThrown(error)
4474
4532
  },
4475
4533
  lookup
4476
4534
  };
@@ -4586,11 +4644,15 @@ var SponsorRelayController = class {
4586
4644
  };
4587
4645
  this.emit();
4588
4646
  }
4647
+ lastProgressLabel = null;
4589
4648
  emitProgress(event) {
4590
4649
  const nextEvent = {
4591
4650
  ...event,
4592
4651
  timestamp: Date.now()
4593
4652
  };
4653
+ if (nextEvent.stage !== "error") {
4654
+ this.lastProgressLabel = nextEvent.label;
4655
+ }
4594
4656
  this.trace(`progress:${nextEvent.stage}`, {
4595
4657
  label: nextEvent.label,
4596
4658
  progress: nextEvent.progress,
@@ -5295,7 +5357,7 @@ var SponsorRelayController = class {
5295
5357
  this.trace("deploy:bootstrap-config-cleanup-error", {
5296
5358
  itemHash: args.itemHash,
5297
5359
  deploymentToken: args.deploymentToken,
5298
- error: error instanceof Error ? error.message : String(error)
5360
+ error: describeThrown(error)
5299
5361
  });
5300
5362
  });
5301
5363
  await this.cleanupStaleVmBootstrapConfigHistory({
@@ -5363,7 +5425,7 @@ var SponsorRelayController = class {
5363
5425
  this.trace("deploy:bootstrap-config-history-scan-error", {
5364
5426
  itemHash: args.itemHash,
5365
5427
  currentAggregateItemHash: args.currentAggregateItemHash,
5366
- error: error instanceof Error ? error.message : String(error)
5428
+ error: describeThrown(error)
5367
5429
  });
5368
5430
  return [];
5369
5431
  });
@@ -5406,7 +5468,7 @@ var SponsorRelayController = class {
5406
5468
  itemHash: args.itemHash,
5407
5469
  currentAggregateItemHash: args.currentAggregateItemHash,
5408
5470
  staleHashes,
5409
- error: error instanceof Error ? error.message : String(error)
5471
+ error: describeThrown(error)
5410
5472
  });
5411
5473
  });
5412
5474
  }
@@ -5727,7 +5789,7 @@ var SponsorRelayController = class {
5727
5789
  } catch (error) {
5728
5790
  this.patch({
5729
5791
  busy: { connectingWallet: false },
5730
- errorText: error instanceof Error ? error.message : String(error),
5792
+ errorText: describeThrown(error),
5731
5793
  statusText: "Wallet connection failed"
5732
5794
  });
5733
5795
  }
@@ -5943,7 +6005,7 @@ var SponsorRelayController = class {
5943
6005
  this.trace("refresh:error", error);
5944
6006
  this.patch({
5945
6007
  busy: { refreshing: false },
5946
- errorText: error instanceof Error ? error.message : String(error),
6008
+ errorText: describeThrown(error),
5947
6009
  statusText: "Refresh failed"
5948
6010
  });
5949
6011
  }
@@ -6278,7 +6340,7 @@ var SponsorRelayController = class {
6278
6340
  });
6279
6341
  return;
6280
6342
  } catch (error) {
6281
- lastError = error instanceof Error ? error : new Error(String(error));
6343
+ lastError = error instanceof Error ? error : new Error(describeThrown(error), { cause: error });
6282
6344
  const attemptLabel = crnDisplayName;
6283
6345
  const failedDueToInvalidIpv6 = /not globally routable|public guest IPv6/i.test(lastError.message);
6284
6346
  attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
@@ -6507,10 +6569,15 @@ var SponsorRelayController = class {
6507
6569
  attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
6508
6570
  );
6509
6571
  } catch (error) {
6510
- this.trace("deploy:error", error);
6572
+ const message = deploymentFailureMessage(error, this.lastProgressLabel);
6573
+ this.trace("deploy:error", {
6574
+ message,
6575
+ stage: this.lastProgressLabel,
6576
+ error
6577
+ });
6511
6578
  this.patch({
6512
6579
  busy: { deploying: false },
6513
- errorText: error instanceof Error ? error.message : String(error),
6580
+ errorText: message,
6514
6581
  statusText: "Deployment failed"
6515
6582
  });
6516
6583
  this.emitProgress({
@@ -6518,7 +6585,7 @@ var SponsorRelayController = class {
6518
6585
  label: "Deployment failed",
6519
6586
  progress: 100,
6520
6587
  status: "error",
6521
- error: error instanceof Error ? error.message : String(error)
6588
+ error: message
6522
6589
  });
6523
6590
  }
6524
6591
  }
@@ -6702,7 +6769,7 @@ var SponsorRelayController = class {
6702
6769
  this.trace("delete:error", error);
6703
6770
  this.patch({
6704
6771
  busy: { deletingInstanceHash: null },
6705
- errorText: error instanceof Error ? error.message : String(error),
6772
+ errorText: describeThrown(error),
6706
6773
  statusText: "Delete failed"
6707
6774
  });
6708
6775
  }
@@ -6768,7 +6835,7 @@ var SponsorRelayController = class {
6768
6835
  this.trace("registration-delete:error", error);
6769
6836
  this.patch({
6770
6837
  busy: { deletingRegistrationHash: null },
6771
- errorText: error instanceof Error ? error.message : String(error),
6838
+ errorText: describeThrown(error),
6772
6839
  statusText: "Registration cleanup failed"
6773
6840
  });
6774
6841
  }
@@ -6808,6 +6875,9 @@ export {
6808
6875
  connectWallet,
6809
6876
  createDeploymentProgressEmitter,
6810
6877
  createSponsorRelayController,
6878
+ deploymentFailureMessage,
6879
+ describeRequest,
6880
+ describeThrown,
6811
6881
  dragExceedsThreshold,
6812
6882
  formatDateTime,
6813
6883
  formatNumber,
@@ -212,8 +212,19 @@ export let apiHosts = undefined
212
212
  state = next
213
213
  })
214
214
 
215
- await controller.init()
215
+ // Before `controller.init()`, not after it, and the order is the whole
216
+ // feature. Placement reads storage and the viewport — no network — while
217
+ // `init()` fetches the manifest and the account's instances. Behind that
218
+ // await, `placement` stays null for as long as Aleph takes to answer, and
219
+ // `handleDragStart` returns early on a null placement: the launcher simply
220
+ // cannot be dragged. Measured in simple-todo: still immovable 20 seconds
221
+ // after the app was ready, with pointerdown, eight pointermoves and
222
+ // pointerup all arriving at the button.
223
+ //
224
+ // The React build never had this — it resolves placement in its own effect.
216
225
  initPlacement()
226
+
227
+ await controller.init()
217
228
  window.addEventListener('resize', handleViewportChange)
218
229
  window.addEventListener('orientationchange', handleViewportChange)
219
230
  return () => {