@le-space/ui 0.9.2 → 0.9.4

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.4",
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.4",
48
+ "@le-space/core": "0.9.4",
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.4";
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,
@@ -3938,12 +3954,30 @@ function validateUcanStoreBootstrapPackage(value) {
3938
3954
  function deploymentNoun(profile) {
3939
3955
  return profile === "ucan-store" ? "service" : "relay";
3940
3956
  }
3957
+ function describeRequest(input, init) {
3958
+ const method = String(init?.method ?? "GET").toUpperCase();
3959
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : typeof input?.url === "string" ? input.url : "<unknown url>";
3960
+ return `${method} ${url}`;
3961
+ }
3962
+ function deploymentFailureMessage(error, stage) {
3963
+ const message = error instanceof Error ? error.message : String(error);
3964
+ const step = String(stage ?? "").trim();
3965
+ return step ? `${message} (while: ${step})` : message;
3966
+ }
3941
3967
  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
- }));
3968
+ return fetch(input, init).then(
3969
+ async (response) => ({
3970
+ ok: response.ok,
3971
+ status: response.status,
3972
+ json: async () => await response.json()
3973
+ }),
3974
+ (error) => {
3975
+ const reason = error instanceof Error ? error.message : String(error);
3976
+ throw new Error(`${reason} (${describeRequest(input, init)})`, {
3977
+ cause: error
3978
+ });
3979
+ }
3980
+ );
3947
3981
  }
3948
3982
  function isConfirmedGuestSetupResult(value) {
3949
3983
  return Boolean(
@@ -4586,11 +4620,15 @@ var SponsorRelayController = class {
4586
4620
  };
4587
4621
  this.emit();
4588
4622
  }
4623
+ lastProgressLabel = null;
4589
4624
  emitProgress(event) {
4590
4625
  const nextEvent = {
4591
4626
  ...event,
4592
4627
  timestamp: Date.now()
4593
4628
  };
4629
+ if (nextEvent.stage !== "error") {
4630
+ this.lastProgressLabel = nextEvent.label;
4631
+ }
4594
4632
  this.trace(`progress:${nextEvent.stage}`, {
4595
4633
  label: nextEvent.label,
4596
4634
  progress: nextEvent.progress,
@@ -6507,10 +6545,15 @@ var SponsorRelayController = class {
6507
6545
  attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
6508
6546
  );
6509
6547
  } catch (error) {
6510
- this.trace("deploy:error", error);
6548
+ const message = deploymentFailureMessage(error, this.lastProgressLabel);
6549
+ this.trace("deploy:error", {
6550
+ message,
6551
+ stage: this.lastProgressLabel,
6552
+ error
6553
+ });
6511
6554
  this.patch({
6512
6555
  busy: { deploying: false },
6513
- errorText: error instanceof Error ? error.message : String(error),
6556
+ errorText: message,
6514
6557
  statusText: "Deployment failed"
6515
6558
  });
6516
6559
  this.emitProgress({
@@ -6518,7 +6561,7 @@ var SponsorRelayController = class {
6518
6561
  label: "Deployment failed",
6519
6562
  progress: 100,
6520
6563
  status: "error",
6521
- error: error instanceof Error ? error.message : String(error)
6564
+ error: message
6522
6565
  });
6523
6566
  }
6524
6567
  }
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.4";
484
484
 
485
485
  interface RootfsRequiredPortForward {
486
486
  port: number;
@@ -516,6 +516,30 @@ 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
+ declare function deploymentFailureMessage(error: unknown, stage: string | null | undefined): string;
519
543
  /**
520
544
  * Normalise the two address documents in the fleet into one list of groups.
521
545
  *
@@ -550,6 +574,7 @@ declare class SponsorRelayController {
550
574
  private emit;
551
575
  private trace;
552
576
  private patch;
577
+ private lastProgressLabel;
553
578
  private emitProgress;
554
579
  private configureRelayBootstrapRegistration;
555
580
  private probeRelayReachableOverProxy;
@@ -603,4 +628,4 @@ interface DeploymentProgressEmitter {
603
628
  }
604
629
  declare function createDeploymentProgressEmitter(): DeploymentProgressEmitter;
605
630
 
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 };
631
+ 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, 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.4";
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,
@@ -3938,12 +3954,30 @@ function validateUcanStoreBootstrapPackage(value) {
3938
3954
  function deploymentNoun(profile) {
3939
3955
  return profile === "ucan-store" ? "service" : "relay";
3940
3956
  }
3957
+ function describeRequest(input, init) {
3958
+ const method = String(init?.method ?? "GET").toUpperCase();
3959
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : typeof input?.url === "string" ? input.url : "<unknown url>";
3960
+ return `${method} ${url}`;
3961
+ }
3962
+ function deploymentFailureMessage(error, stage) {
3963
+ const message = error instanceof Error ? error.message : String(error);
3964
+ const step = String(stage ?? "").trim();
3965
+ return step ? `${message} (while: ${step})` : message;
3966
+ }
3941
3967
  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
- }));
3968
+ return fetch(input, init).then(
3969
+ async (response) => ({
3970
+ ok: response.ok,
3971
+ status: response.status,
3972
+ json: async () => await response.json()
3973
+ }),
3974
+ (error) => {
3975
+ const reason = error instanceof Error ? error.message : String(error);
3976
+ throw new Error(`${reason} (${describeRequest(input, init)})`, {
3977
+ cause: error
3978
+ });
3979
+ }
3980
+ );
3947
3981
  }
3948
3982
  function isConfirmedGuestSetupResult(value) {
3949
3983
  return Boolean(
@@ -4586,11 +4620,15 @@ var SponsorRelayController = class {
4586
4620
  };
4587
4621
  this.emit();
4588
4622
  }
4623
+ lastProgressLabel = null;
4589
4624
  emitProgress(event) {
4590
4625
  const nextEvent = {
4591
4626
  ...event,
4592
4627
  timestamp: Date.now()
4593
4628
  };
4629
+ if (nextEvent.stage !== "error") {
4630
+ this.lastProgressLabel = nextEvent.label;
4631
+ }
4594
4632
  this.trace(`progress:${nextEvent.stage}`, {
4595
4633
  label: nextEvent.label,
4596
4634
  progress: nextEvent.progress,
@@ -6507,10 +6545,15 @@ var SponsorRelayController = class {
6507
6545
  attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
6508
6546
  );
6509
6547
  } catch (error) {
6510
- this.trace("deploy:error", error);
6548
+ const message = deploymentFailureMessage(error, this.lastProgressLabel);
6549
+ this.trace("deploy:error", {
6550
+ message,
6551
+ stage: this.lastProgressLabel,
6552
+ error
6553
+ });
6511
6554
  this.patch({
6512
6555
  busy: { deploying: false },
6513
- errorText: error instanceof Error ? error.message : String(error),
6556
+ errorText: message,
6514
6557
  statusText: "Deployment failed"
6515
6558
  });
6516
6559
  this.emitProgress({
@@ -6518,7 +6561,7 @@ var SponsorRelayController = class {
6518
6561
  label: "Deployment failed",
6519
6562
  progress: 100,
6520
6563
  status: "error",
6521
- error: error instanceof Error ? error.message : String(error)
6564
+ error: message
6522
6565
  });
6523
6566
  }
6524
6567
  }
@@ -6808,6 +6851,8 @@ export {
6808
6851
  connectWallet,
6809
6852
  createDeploymentProgressEmitter,
6810
6853
  createSponsorRelayController,
6854
+ deploymentFailureMessage,
6855
+ describeRequest,
6811
6856
  dragExceedsThreshold,
6812
6857
  formatDateTime,
6813
6858
  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 () => {