@le-space/ui 0.6.23 → 0.6.25

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/README.md CHANGED
@@ -4,7 +4,7 @@ Floating Aleph relay deployment UI components for Svelte and React.
4
4
 
5
5
  Primary goals:
6
6
 
7
- - Svelte-first floating `Sponsor Relay` panel
7
+ - Svelte-first floating `Relay Button` panel
8
8
  - lightweight React wrapper
9
9
  - shared controller and browser-safe Aleph helpers
10
10
  - minimal host configuration beyond a rootfs manifest URL
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@le-space/ui",
3
- "version": "0.6.23",
4
- "description": "Floating Aleph sponsor relay UI components for Svelte and React.",
3
+ "version": "0.6.25",
4
+ "description": "Floating Aleph Relay Button UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "files": [
@@ -44,8 +44,8 @@
44
44
  "url": "https://github.com/NiKrause/relay-button/issues"
45
45
  },
46
46
  "dependencies": {
47
- "@le-space/browser": "0.6.23",
48
- "@le-space/core": "0.6.23",
47
+ "@le-space/browser": "0.6.25",
48
+ "@le-space/core": "0.6.25",
49
49
  "viem": "^2.53.1"
50
50
  },
51
51
  "peerDependencies": {
package/react/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
- type MessageStatus = 'processed' | 'pending' | 'rejected' | 'unknown';
3
+ type MessageStatus = 'processed' | 'pending' | 'rejected' | 'removing' | 'removed' | 'unknown';
4
4
  interface BalanceResponse {
5
5
  address: string;
6
6
  balance: string;
@@ -235,6 +235,7 @@ interface SponsorRelayRootfsHealth {
235
235
  tone: SponsorRelayHealthTone;
236
236
  label: string;
237
237
  detail: string | null;
238
+ code?: 'manifest-invalid' | 'rootfs-not-found' | 'rootfs-verifying' | 'rootfs-pending' | 'rootfs-unavailable' | 'rootfs-ready';
238
239
  }
239
240
  interface CompactInstanceDetails {
240
241
  messageStatus: string;
package/react/index.js CHANGED
@@ -43,7 +43,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
43
43
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
44
44
 
45
45
  // src/shared/package-version.ts
46
- var UI_PACKAGE_VERSION = "0.6.23";
46
+ var UI_PACKAGE_VERSION = "0.6.25";
47
47
 
48
48
  // src/shared/format.ts
49
49
  function shortHash(value, head = 8, tail = 6) {
@@ -647,7 +647,7 @@ async function verifyRootfsExists(itemHash, apiHost = DEFAULT_ALEPH_API_HOST) {
647
647
  function normalizeStatus(status) {
648
648
  if (typeof status !== "string") return "unknown";
649
649
  const normalized = status.toLowerCase();
650
- if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
650
+ if (normalized === "processed" || normalized === "pending" || normalized === "rejected" || normalized === "removing" || normalized === "removed") {
651
651
  return normalized;
652
652
  }
653
653
  return "unknown";
@@ -672,6 +672,7 @@ function parseCidFromPayload(payload) {
672
672
  }
673
673
  function parseRejectionReason(payload) {
674
674
  const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
675
+ const reason = typeof payload.reason === "string" ? payload.reason.trim() : "";
675
676
  const details = payload.details && typeof payload.details === "object" ? payload.details : null;
676
677
  const rawErrors = Array.isArray(details?.errors) ? details.errors : [];
677
678
  const firstError = rawErrors[0] && typeof rawErrors[0] === "object" ? rawErrors[0] : null;
@@ -686,6 +687,18 @@ function parseRejectionReason(payload) {
686
687
  };
687
688
  }
688
689
  }
690
+ if (reason === "balance_insufficient") {
691
+ return {
692
+ rejectionErrorCode: errorCode,
693
+ rejectionReason: "The rootfs STORE is being removed because its publisher no longer has enough Aleph credits. Your connected MetaMask balance may be sufficient, but this manifest must point to a newly published, processed rootfs STORE."
694
+ };
695
+ }
696
+ if (reason) {
697
+ return {
698
+ rejectionErrorCode: errorCode,
699
+ rejectionReason: `Aleph marked the rootfs STORE as unavailable: ${reason.replaceAll("_", " ")}.`
700
+ };
701
+ }
689
702
  return {
690
703
  rejectionErrorCode: errorCode,
691
704
  rejectionReason: errorCode != null ? `Rejected by Aleph (error code ${errorCode}).` : null
@@ -727,11 +740,12 @@ async function resolveRootfsReference(itemHash, apiHost = DEFAULT_ALEPH_API_HOST
727
740
  const firstMessage = Array.isArray(payload.messages) && payload.messages[0] && typeof payload.messages[0] === "object" ? payload.messages[0] : null;
728
741
  const messageObject = payload.message && typeof payload.message === "object" ? payload.message : null;
729
742
  const cid = parseCidFromPayload(payload);
730
- const rejection = normalizeStatus(payload.status) === "rejected" ? parseRejectionReason(payload) : { rejectionErrorCode: null, rejectionReason: null };
743
+ const messageStatus = normalizeStatus(payload.status);
744
+ const rejection = messageStatus === "processed" || messageStatus === "pending" ? { rejectionErrorCode: null, rejectionReason: null } : parseRejectionReason(payload);
731
745
  const gateway = cid ? await probeGateway(cid, gatewayBaseUrl) : { gatewayUrl: null, gatewayStatus: "unknown", gatewayError: null };
732
746
  return {
733
747
  itemHash,
734
- messageStatus: normalizeStatus(payload.status),
748
+ messageStatus,
735
749
  messageType: String(payload.type || messageObject?.type || firstMessage?.type || "").toUpperCase() || null,
736
750
  cid,
737
751
  receptionTime: typeof payload.reception_time === "string" ? payload.reception_time : null,
@@ -2472,8 +2486,7 @@ import { keccak256, recoverMessageAddress, stringToHex } from "viem";
2472
2486
  var DEFAULT_ALEPH_API_HOST3 = "https://api.aleph.im";
2473
2487
  var DEFAULT_ALEPH_BOOTSTRAP_CHANNEL = "simple-todo";
2474
2488
  var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
2475
- var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
2476
- var DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE = "relay-bootstrap-v2";
2489
+ var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap-v2";
2477
2490
  var DEFAULT_BOOTSTRAP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2478
2491
  var DEFAULT_BOOTSTRAP_PAGINATION = 50;
2479
2492
  var DEFAULT_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT = 3;
@@ -2507,9 +2520,6 @@ function serializeRelayProofPayload(payload) {
2507
2520
  function relayBootstrapMultiaddrsHash(addrs) {
2508
2521
  return keccak256(stringToHex(JSON.stringify(dedupeMultiaddrs(addrs))));
2509
2522
  }
2510
- function compactPostType(postType) {
2511
- return (postType ?? DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE) === DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE;
2512
- }
2513
2523
  function browserMultiaddrPreference(addr) {
2514
2524
  const normalized = addr.toLowerCase();
2515
2525
  if (normalized.includes("/tcp/443/tls/ws/")) return 0;
@@ -2739,8 +2749,6 @@ function normalizeRelayBootstrapPostRecord(value) {
2739
2749
  function buildRelayBootstrapPostContent(args) {
2740
2750
  const now = args.now ?? Date.now() / 1e3;
2741
2751
  const updatedAt = args.now ?? Date.now();
2742
- const postType = args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE;
2743
- const compact = args.compact ?? compactPostType(postType);
2744
2752
  const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
2745
2753
  const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2746
2754
  browserDialableOnly: true
@@ -2750,13 +2758,13 @@ function buildRelayBootstrapPostContent(args) {
2750
2758
  args.compactMultiaddrLimit
2751
2759
  );
2752
2760
  return {
2753
- type: postType,
2761
+ type: DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2754
2762
  address: args.sender,
2755
2763
  ...args.ref ? { ref: args.ref } : {},
2756
2764
  content: {
2757
2765
  peerId: args.peerId,
2758
- multiaddrs: compact ? compactMultiaddrs : fullMultiaddrs,
2759
- browserMultiaddrs: compact ? void 0 : fullBrowserMultiaddrs,
2766
+ multiaddrs: compactMultiaddrs,
2767
+ browserMultiaddrs: void 0,
2760
2768
  registrationId: args.registrationId,
2761
2769
  profile: args.profile,
2762
2770
  version: args.version,
@@ -2779,14 +2787,12 @@ async function createRelayBootstrapPost(args) {
2779
2787
  browserMultiaddrs: args.browserMultiaddrs,
2780
2788
  registrationId: args.registrationId,
2781
2789
  ref: args.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF,
2782
- postType: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2783
2790
  profile: args.profile,
2784
2791
  version: args.version,
2785
2792
  ownerAddress: args.ownerAddress,
2786
2793
  publisherAddress: args.publisherAddress,
2787
2794
  authorization: args.authorization,
2788
2795
  relayProof: args.relayProof,
2789
- compact: args.compact,
2790
2796
  compactMultiaddrLimit: args.compactMultiaddrLimit,
2791
2797
  now: nowMillis
2792
2798
  });
@@ -2833,12 +2839,11 @@ async function signRelayBootstrapProof(args) {
2833
2839
  fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
2834
2840
  args.compactMultiaddrLimit
2835
2841
  );
2836
- const compact = args.compact ?? false;
2837
2842
  const payload = {
2838
2843
  peerId: args.peerId,
2839
- multiaddrs: compact ? void 0 : fullMultiaddrs,
2840
- browserMultiaddrs: compact ? void 0 : fullBrowserMultiaddrs,
2841
- multiaddrsHash: compact ? relayBootstrapMultiaddrsHash(compactMultiaddrs) : void 0,
2844
+ multiaddrs: void 0,
2845
+ browserMultiaddrs: void 0,
2846
+ multiaddrsHash: relayBootstrapMultiaddrsHash(compactMultiaddrs),
2842
2847
  registrationId: args.registrationId,
2843
2848
  profile: args.profile,
2844
2849
  version: args.version,
@@ -2868,10 +2873,7 @@ async function fetchAlephBootstrapPosts(options = {}) {
2868
2873
  options.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL
2869
2874
  );
2870
2875
  url.searchParams.set("refs", options.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF);
2871
- url.searchParams.set(
2872
- "types",
2873
- options.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE
2874
- );
2876
+ url.searchParams.set("types", DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE);
2875
2877
  url.searchParams.set(
2876
2878
  "pagination",
2877
2879
  String(options.pagination ?? DEFAULT_BOOTSTRAP_PAGINATION)
@@ -2885,7 +2887,21 @@ async function fetchAlephBootstrapPosts(options = {}) {
2885
2887
  throw new Error(`Aleph bootstrap post lookup failed: ${response.status}`);
2886
2888
  }
2887
2889
  const payload = await response.json();
2888
- return (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null).sort((left, right) => compareRelayBootstrapPostRecency(right, left));
2890
+ const posts = (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null).sort((left, right) => compareRelayBootstrapPostRecency(right, left));
2891
+ posts.forEach(assertSupportedRelayBootstrapPost);
2892
+ return posts;
2893
+ }
2894
+ function assertSupportedRelayBootstrapPost(post) {
2895
+ if (post.type === "relay-bootstrap") {
2896
+ throw new Error(
2897
+ "Legacy relay-bootstrap record encountered. Only relay-bootstrap-v2 is supported."
2898
+ );
2899
+ }
2900
+ if (post.type && post.type !== DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE) {
2901
+ throw new Error(
2902
+ `Unsupported relay bootstrap post type: ${post.type}. Expected ${DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE}.`
2903
+ );
2904
+ }
2889
2905
  }
2890
2906
  function compareRelayBootstrapPostRecency(left, right) {
2891
2907
  const leftUpdatedAt = left.content?.updatedAt ?? 0;
@@ -2907,6 +2923,7 @@ function selectCurrentRelayBootstrapPosts(posts, options = {}) {
2907
2923
  const now = options.now ?? Date.now();
2908
2924
  const selected = /* @__PURE__ */ new Map();
2909
2925
  for (const post of posts) {
2926
+ assertSupportedRelayBootstrapPost(post);
2910
2927
  const content = post.content;
2911
2928
  if (!content) continue;
2912
2929
  if (now - content.updatedAt > maxAgeMs) continue;
@@ -2932,14 +2949,12 @@ async function waitForRelayBootstrapRegistration(args) {
2932
2949
  const expectedSender = args.sender?.trim().toLowerCase() || null;
2933
2950
  const expectedRegistrationId = args.registrationId?.trim() || null;
2934
2951
  const expectedPeerId = args.peerId?.trim() || null;
2935
- const postType = args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE;
2936
2952
  const sleepImpl = args.sleep ?? sleep;
2937
2953
  for (let attempt = 0; attempt < attempts; attempt += 1) {
2938
2954
  const posts = await fetchAlephBootstrapPosts({
2939
2955
  apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2940
2956
  channel: args.channel,
2941
2957
  ref: args.ref,
2942
- postType,
2943
2958
  fetch: args.fetch
2944
2959
  });
2945
2960
  const currentPosts = selectCurrentRelayBootstrapPosts(posts);
@@ -2978,8 +2993,6 @@ async function waitForRelayBootstrapRegistration(args) {
2978
2993
  async function publishRelayBootstrapRegistration(args) {
2979
2994
  const publisherAddress = args.publisherAddress ?? args.sender;
2980
2995
  const ownerAddress = args.ownerAddress;
2981
- const postType = args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE;
2982
- const compact = postType === DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE;
2983
2996
  let ownerAuthorization = args.ownerAuthorization;
2984
2997
  let relayProof = args.relayProof;
2985
2998
  if (!ownerAuthorization && ownerAddress && args.ownerSigner) {
@@ -3006,7 +3019,6 @@ async function publishRelayBootstrapRegistration(args) {
3006
3019
  profile: args.profile,
3007
3020
  version: args.version,
3008
3021
  updatedAt: args.now ?? Date.now(),
3009
- compact,
3010
3022
  compactMultiaddrLimit: args.compactMultiaddrLimit,
3011
3023
  signer: args.publisherSigner
3012
3024
  });
@@ -3019,14 +3031,12 @@ async function publishRelayBootstrapRegistration(args) {
3019
3031
  registrationId: args.registrationId,
3020
3032
  channel: args.channel,
3021
3033
  ref: args.ref,
3022
- postType,
3023
3034
  profile: args.profile,
3024
3035
  version: args.version,
3025
3036
  ownerAddress,
3026
3037
  publisherAddress,
3027
3038
  authorization: ownerAuthorization,
3028
3039
  relayProof,
3029
- compact,
3030
3040
  compactMultiaddrLimit: args.compactMultiaddrLimit,
3031
3041
  now: args.now,
3032
3042
  hasher: args.hasher
@@ -3049,18 +3059,12 @@ async function publishRelayBootstrapRegistration(args) {
3049
3059
  let forgottenHashes = [];
3050
3060
  let forgetResult;
3051
3061
  if (args.forgetPrevious && args.registrationId) {
3052
- const previousPostTypes = compact ? [postType, DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE] : [postType];
3053
- const previousPosts = (await Promise.all(
3054
- previousPostTypes.map(
3055
- (previousPostType) => fetchAlephBootstrapPosts({
3056
- apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
3057
- channel: args.channel,
3058
- ref: args.ref,
3059
- postType: previousPostType,
3060
- fetch: args.fetch
3061
- })
3062
- )
3063
- )).flat();
3062
+ const previousPosts = await fetchAlephBootstrapPosts({
3063
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
3064
+ channel: args.channel,
3065
+ ref: args.ref,
3066
+ fetch: args.fetch
3067
+ });
3064
3068
  const forgottenHashSet = new Set(
3065
3069
  previousPosts.filter((post) => {
3066
3070
  const hash = post.itemHash ?? post.hash;
@@ -3662,7 +3666,9 @@ function draftUcanStoreBootstrapErrors(args) {
3662
3666
  const errors = [];
3663
3667
  const { input } = args;
3664
3668
  if (!args.operatorAddress) {
3665
- errors.push("Connect MetaMask before configuring the ucan-store bootstrap package.");
3669
+ errors.push(
3670
+ "Connect MetaMask before configuring the ucan-store bootstrap package."
3671
+ );
3666
3672
  }
3667
3673
  if (!input.adminDid.trim().startsWith("did:")) {
3668
3674
  errors.push("Admin DID must be a non-empty DID string.");
@@ -3713,7 +3719,9 @@ function buildUcanStoreBootstrapPackage(args) {
3713
3719
  serviceDid: args.input.serviceDid.trim() || null,
3714
3720
  spaceDid: args.input.spaceDid.trim(),
3715
3721
  rootDelegationProof: args.input.rootDelegationProof.trim(),
3716
- allowedCapabilities: parseAllowedCapabilities(args.input.allowedCapabilities),
3722
+ allowedCapabilities: parseAllowedCapabilities(
3723
+ args.input.allowedCapabilities
3724
+ ),
3717
3725
  defaultUserDelegationExpiration: parseOptionalExpiration(
3718
3726
  args.input.defaultUserDelegationExpiration
3719
3727
  ),
@@ -3752,7 +3760,9 @@ function defaultState(props = {}) {
3752
3760
  instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
3753
3761
  ucanStoreBootstrap: defaultUcanStoreBootstrapInput(props),
3754
3762
  tierId: DEFAULT_TIER_ID,
3755
- showAdvanced: Boolean(props.manifestJson?.trim() || props.sshPublicKey?.trim()),
3763
+ showAdvanced: Boolean(
3764
+ props.manifestJson?.trim() || props.sshPublicKey?.trim()
3765
+ ),
3756
3766
  showInstances: props.showInstances ?? true,
3757
3767
  showPasteManifest: Boolean(props.manifestJson?.trim()),
3758
3768
  busy: {
@@ -3836,48 +3846,55 @@ function rootfsHealth(args) {
3836
3846
  return {
3837
3847
  tone: "error",
3838
3848
  label: "manifest invalid",
3839
- detail: args.manifestState.errors[0] ?? "Manifest could not be parsed."
3849
+ detail: args.manifestState.errors[0] ?? "Manifest could not be parsed.",
3850
+ code: "manifest-invalid"
3840
3851
  };
3841
3852
  }
3842
3853
  if (!args.rootfsVerified) {
3843
3854
  return {
3844
3855
  tone: "error",
3845
3856
  label: "not found on Aleph",
3846
- detail: "The manifest points to a rootfs STORE message that Aleph cannot find. Check that the manifest URL is correct and that the referenced rootfs was not forgotten."
3857
+ detail: "The manifest points to a rootfs STORE message that Aleph cannot find. Check that the manifest URL is correct and that the referenced rootfs was not forgotten.",
3858
+ code: "rootfs-not-found"
3847
3859
  };
3848
3860
  }
3849
3861
  if (!args.resolution) {
3850
3862
  return {
3851
3863
  tone: "caution",
3852
3864
  label: "verifying rootfs",
3853
- detail: "The rootfs reference is still being resolved."
3865
+ detail: "The rootfs reference is still being resolved.",
3866
+ code: "rootfs-verifying"
3854
3867
  };
3855
3868
  }
3856
3869
  if (args.resolution.messageStatus === "processed") {
3857
3870
  return {
3858
3871
  tone: "ok",
3859
3872
  label: "deployable",
3860
- detail: args.resolution.gatewayUrl ?? "Rootfs verified on Aleph."
3873
+ detail: args.resolution.gatewayUrl ?? "Rootfs verified on Aleph.",
3874
+ code: "rootfs-ready"
3861
3875
  };
3862
3876
  }
3863
3877
  if (args.resolution.messageStatus === "pending" && args.resolution.gatewayStatus === "reachable") {
3864
3878
  return {
3865
3879
  tone: "caution",
3866
3880
  label: "pending but reachable",
3867
- detail: "Gateway probe succeeded even though Aleph still reports pending."
3881
+ detail: "Gateway probe succeeded even though Aleph still reports pending.",
3882
+ code: "rootfs-pending"
3868
3883
  };
3869
3884
  }
3870
3885
  if (args.resolution.messageStatus === "pending") {
3871
3886
  return {
3872
3887
  tone: "caution",
3873
3888
  label: "pending on Aleph",
3874
- detail: "Wait until the STORE message is processed."
3889
+ detail: "Wait until the STORE message is processed.",
3890
+ code: "rootfs-pending"
3875
3891
  };
3876
3892
  }
3877
3893
  return {
3878
3894
  tone: "error",
3879
3895
  label: "manifest rootfs not deployable",
3880
- detail: args.resolution.rejectionReason ?? (args.resolution.messageType === "STORE" ? "This manifest points to a rootfs STORE message that exists but is not deployable anymore, for example because it was forgotten or replaced. Double-check the manifest URL." : "Aleph rejected the rootfs reference. Double-check that the manifest URL points to the intended current rootfs.")
3896
+ detail: args.resolution.rejectionReason ?? (args.resolution.messageType === "STORE" ? "This manifest points to a rootfs STORE message that exists but is not deployable anymore, for example because it was forgotten or replaced. Double-check the manifest URL." : "Aleph rejected the rootfs reference. Double-check that the manifest URL points to the intended current rootfs."),
3897
+ code: "rootfs-unavailable"
3881
3898
  };
3882
3899
  }
3883
3900
  async function resolveManifest(args) {
@@ -4165,7 +4182,9 @@ var SponsorRelayController = class {
4165
4182
  const profile = bootstrapRelayProfileForManifest(this.state.manifest);
4166
4183
  if (!profile && deploymentProfile !== "ucan-store") return;
4167
4184
  if (profile && !this.state.wallet.address) {
4168
- throw new Error("A connected wallet is required to register relay bootstrap addresses.");
4185
+ throw new Error(
4186
+ "A connected wallet is required to register relay bootstrap addresses."
4187
+ );
4169
4188
  }
4170
4189
  const runtimeHostIpv4 = args.runtime.hostIpv4;
4171
4190
  if (!runtimeHostIpv4) {
@@ -4189,7 +4208,9 @@ var SponsorRelayController = class {
4189
4208
  }
4190
4209
  const walletAddress2 = this.state.wallet.address;
4191
4210
  if (!walletAddress2) {
4192
- throw new Error("A connected wallet is required to configure the ucan-store bootstrap package.");
4211
+ throw new Error(
4212
+ "A connected wallet is required to configure the ucan-store bootstrap package."
4213
+ );
4193
4214
  }
4194
4215
  const bootstrapValidation = buildUcanStoreBootstrapPackage({
4195
4216
  input: this.state.ucanStoreBootstrap,
@@ -4304,7 +4325,9 @@ var SponsorRelayController = class {
4304
4325
  if (!profile) return;
4305
4326
  const walletAddress = this.state.wallet.address;
4306
4327
  if (!walletAddress) {
4307
- throw new Error("A connected wallet is required to register relay bootstrap addresses.");
4328
+ throw new Error(
4329
+ "A connected wallet is required to register relay bootstrap addresses."
4330
+ );
4308
4331
  }
4309
4332
  let bootstrapSignalRelayMetadata = null;
4310
4333
  if (profile === "orbitdb-relay") {
@@ -4350,7 +4373,9 @@ var SponsorRelayController = class {
4350
4373
  const tcpPort = mappedPorts2["9091"]?.host ?? 0;
4351
4374
  const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
4352
4375
  if (!tcpPort || !wsPort) {
4353
- throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
4376
+ throw new Error(
4377
+ "OrbitDB relay runtime is missing required mapped TCP/WS ports."
4378
+ );
4354
4379
  }
4355
4380
  const configureResult = await configureOrbitdbRelaySetup2({
4356
4381
  hostIpv4: runtimeHostIpv4,
@@ -4373,7 +4398,9 @@ var SponsorRelayController = class {
4373
4398
  }
4374
4399
  } else {
4375
4400
  if (!args.deploymentToken) {
4376
- throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
4401
+ throw new Error(
4402
+ "Missing deployment token for Aleph bootstrap config handoff."
4403
+ );
4377
4404
  }
4378
4405
  if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
4379
4406
  this.emitProgress({
@@ -4599,7 +4626,19 @@ var SponsorRelayController = class {
4599
4626
  });
4600
4627
  return;
4601
4628
  }
4602
- throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
4629
+ throw new Error(
4630
+ "Relay metadata did not include a peer ID and public multiaddrs."
4631
+ );
4632
+ }
4633
+ if (profile === "uc-go-peer") {
4634
+ const browserDialableMultiaddrs = relayMetadata.browserBootstrapMultiaddrs.filter(
4635
+ (multiaddr) => /\/(?:tls\/ws|wss)\/p2p\//.test(multiaddr) || /\/(?:webtransport|webrtc-direct)\//.test(multiaddr) && /\/certhash\//.test(multiaddr)
4636
+ );
4637
+ if (browserDialableMultiaddrs.length === 0) {
4638
+ throw new Error(
4639
+ "Relay metadata did not include a browser-dialable WSS address or a WebTransport/WebRTC Direct address with certhash."
4640
+ );
4641
+ }
4603
4642
  }
4604
4643
  const { peerId } = relayMetadata;
4605
4644
  const registrationId = createRelayBootstrapRegistrationId(
@@ -5151,18 +5190,10 @@ var SponsorRelayController = class {
5151
5190
  const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
5152
5191
  this.client.fetchBalance(this.state.wallet.address),
5153
5192
  this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([]),
5154
- bootstrapEnabled ? Promise.all(
5155
- [
5156
- DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE,
5157
- DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE
5158
- ].map(
5159
- (postType) => fetchAlephBootstrapPosts({
5160
- apiHost: this.client.apiHost,
5161
- postType,
5162
- fetch
5163
- }).catch(() => [])
5164
- )
5165
- ).then((pages) => pages.flat()) : Promise.resolve([])
5193
+ bootstrapEnabled ? fetchAlephBootstrapPosts({
5194
+ apiHost: this.client.apiHost,
5195
+ fetch
5196
+ }) : Promise.resolve([])
5166
5197
  ]);
5167
5198
  balance = nextBalance;
5168
5199
  instances = await Promise.all(
@@ -5226,7 +5257,9 @@ var SponsorRelayController = class {
5226
5257
  const activeInstanceHashes = new Set(
5227
5258
  instances.map((entry) => entry.instance.item_hash)
5228
5259
  );
5229
- bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
5260
+ bootstrapRegistrations = selectCurrentRelayBootstrapPosts(
5261
+ rawBootstrapPosts
5262
+ ).filter(
5230
5263
  (entry) => relayBootstrapMatchesCurrentWallet(
5231
5264
  entry,
5232
5265
  normalizedWalletAddress,
@@ -5249,7 +5282,9 @@ var SponsorRelayController = class {
5249
5282
  peerId: entry.content.peerId,
5250
5283
  registrationId: entry.content.registrationId,
5251
5284
  multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
5252
- browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
5285
+ browserMultiaddrs: Array.isArray(
5286
+ entry.content.browserMultiaddrs
5287
+ ) ? entry.content.browserMultiaddrs : [],
5253
5288
  ownerAddress: typeof entry.content.ownerAddress === "string" ? entry.content.ownerAddress : void 0,
5254
5289
  publisherAddress: typeof entry.content.publisherAddress === "string" ? entry.content.publisherAddress : void 0,
5255
5290
  updatedAt: entry.content.updatedAt
@@ -5370,7 +5405,9 @@ var SponsorRelayController = class {
5370
5405
  ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
5371
5406
  ].slice(0, 5);
5372
5407
  if (orderedCrns.length === 0) {
5373
- throw new Error("No compatible CRN is currently available for this tier.");
5408
+ throw new Error(
5409
+ "No compatible CRN is currently available for this tier."
5410
+ );
5374
5411
  }
5375
5412
  const attemptErrors = [];
5376
5413
  let lastError = null;
@@ -5803,7 +5840,7 @@ var SponsorRelayController = class {
5803
5840
  await forgetAlephMessages({
5804
5841
  sender: this.state.wallet.address,
5805
5842
  hashes: [attemptItemHash],
5806
- reason: `Discard failed Sponsor Relay deployment attempt on ${attemptLabel}`,
5843
+ reason: `Discard failed Relay Button deployment attempt on ${attemptLabel}`,
5807
5844
  signer: personalSign,
5808
5845
  hasher: sha256Hex,
5809
5846
  fetch: (url, init) => fetch(url, init).then(async (response) => ({
@@ -5913,7 +5950,7 @@ var SponsorRelayController = class {
5913
5950
  await forgetAlephMessages({
5914
5951
  sender: this.state.wallet.address,
5915
5952
  hashes: hashesToForget,
5916
- reason: linkedRegistrationCount > 0 ? `Deleted instance and ${linkedRegistrationCount} linked relay bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"} from Sponsor Relay panel` : "Deleted from Sponsor Relay panel",
5953
+ reason: linkedRegistrationCount > 0 ? `Deleted instance and ${linkedRegistrationCount} linked relay bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"} from Relay Button panel` : "Deleted from Relay Button panel",
5917
5954
  signer: personalSign,
5918
5955
  hasher: sha256Hex,
5919
5956
  fetch: asJsonFetch,
@@ -5982,7 +6019,7 @@ var SponsorRelayController = class {
5982
6019
  await forgetAlephMessages({
5983
6020
  sender: this.state.wallet.address,
5984
6021
  hashes: [registrationHash],
5985
- reason: "Deleted orphan relay bootstrap registration from Sponsor Relay panel",
6022
+ reason: "Deleted orphan relay bootstrap registration from Relay Button panel",
5986
6023
  signer: personalSign,
5987
6024
  hasher: sha256Hex,
5988
6025
  fetch: asJsonFetch,
@@ -6187,7 +6224,7 @@ function supportsBootstrapUi(state) {
6187
6224
  return stateDeploymentProfile(state) !== "ucan-store";
6188
6225
  }
6189
6226
  function deploymentPanelTitle(state) {
6190
- return stateDeploymentProfile(state) === "ucan-store" ? "Sponsor Service" : "Sponsor Relay";
6227
+ return stateDeploymentProfile(state) === "ucan-store" ? "Sponsor Service" : "Relay Button";
6191
6228
  }
6192
6229
  function deploymentLauncherLabel(state, compactInlineLabel, launcherMode) {
6193
6230
  if (launcherMode === "inline" && compactInlineLabel) {
@@ -6397,7 +6434,8 @@ function SponsorRelayFab(props) {
6397
6434
  maxHeight: "calc(100vh - 12.5rem)"
6398
6435
  };
6399
6436
  const launcherLabel = deploymentLauncherLabel(state, compactInlineLabel, launcherMode);
6400
- const deployCallToAction = state.wallet.connected ? state.busy.deploying ? "Deploying\u2026" : deploymentButtonLabel(state) : "Connect MetaMask";
6437
+ const rootfsBlocked = state.rootfsHealth.tone === "error";
6438
+ const deployCallToAction = state.wallet.connected ? state.busy.deploying ? "Deploying\u2026" : rootfsBlocked ? "Deployment blocked \u2014 Rootfs unavailable" : deploymentButtonLabel(state) : "Connect MetaMask";
6401
6439
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6402
6440
  /* @__PURE__ */ jsx("style", { children: `@keyframes leSpaceRelayPulse { 0%, 100% { transform: scale(1); opacity: 0.74; } 50% { transform: scale(1.22); opacity: 1; } }` }),
6403
6441
  /* @__PURE__ */ jsxs(
@@ -7082,13 +7120,55 @@ function SponsorRelayFab(props) {
7082
7120
  )
7083
7121
  ] })
7084
7122
  ] }),
7123
+ rootfsBlocked ? /* @__PURE__ */ jsxs(
7124
+ "div",
7125
+ {
7126
+ role: "alert",
7127
+ id: "relay-rootfs-deployment-blocker",
7128
+ style: {
7129
+ display: "grid",
7130
+ gap: "0.55rem",
7131
+ marginTop: "1rem",
7132
+ padding: "0.9rem",
7133
+ borderRadius: "1rem",
7134
+ border: "1px solid rgba(248, 113, 113, 0.58)",
7135
+ background: "rgba(127, 29, 29, 0.34)",
7136
+ color: "#fee2e2"
7137
+ },
7138
+ children: [
7139
+ /* @__PURE__ */ jsx("strong", { children: "Rootfs unavailable \u2014 deployment blocked" }),
7140
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "0.82rem", lineHeight: 1.45 }, children: state.rootfsHealth.detail }),
7141
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.75rem", lineHeight: 1.4 }, children: [
7142
+ "This is separate from the connected MetaMask balance. The manifest must reference an Aleph rootfs STORE with status ",
7143
+ /* @__PURE__ */ jsx("code", { children: "processed" }),
7144
+ "."
7145
+ ] }),
7146
+ state.manifest?.rootfsItemHash ? /* @__PURE__ */ jsxs("code", { style: { fontSize: "0.7rem", overflowWrap: "anywhere" }, children: [
7147
+ "Rootfs: ",
7148
+ state.manifest.rootfsItemHash,
7149
+ state.rootfsResolution?.messageStatus ? ` \xB7 Aleph status: ${state.rootfsResolution.messageStatus}` : ""
7150
+ ] }) : null,
7151
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "0.5rem" }, children: [
7152
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void controller.refresh(), disabled: state.busy.refreshing, style: { ...secondaryButtonStyle, padding: "0.55rem 0.7rem" }, children: state.busy.refreshing ? "Checking\u2026" : "Retry validation" }),
7153
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => controller.setShowAdvanced(true), style: { ...secondaryButtonStyle, padding: "0.55rem 0.7rem" }, children: "Edit manifest URL" }),
7154
+ /^https?:\/\//.test(state.manifestUrl) ? /* @__PURE__ */ jsx("a", { href: state.manifestUrl, target: "_blank", rel: "noreferrer", style: { color: "#bfdbfe", alignSelf: "center", fontSize: "0.78rem" }, children: "Open manifest" }) : null
7155
+ ] })
7156
+ ]
7157
+ }
7158
+ ) : null,
7085
7159
  /* @__PURE__ */ jsx(
7086
7160
  "button",
7087
7161
  {
7088
7162
  type: "button",
7089
7163
  onClick: () => void (state.wallet.connected ? controller.deploy() : controller.connectWallet()),
7090
7164
  disabled: state.wallet.connected ? state.busy.deploying || state.rootfsHealth.tone !== "ok" : state.busy.connectingWallet,
7091
- style: { ...primaryButtonStyle, width: "100%", marginTop: "1rem" },
7165
+ "aria-describedby": rootfsBlocked ? "relay-rootfs-deployment-blocker" : void 0,
7166
+ style: {
7167
+ ...primaryButtonStyle,
7168
+ width: "100%",
7169
+ marginTop: "1rem",
7170
+ ...rootfsBlocked ? { background: "rgba(148, 163, 184, 0.22)", borderColor: "rgba(248, 113, 113, 0.45)", color: "#fee2e2", boxShadow: "none", cursor: "not-allowed" } : {}
7171
+ },
7092
7172
  children: deployCallToAction
7093
7173
  }
7094
7174
  ),
package/shared/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- type MessageStatus = 'processed' | 'pending' | 'rejected' | 'unknown';
1
+ type MessageStatus = 'processed' | 'pending' | 'rejected' | 'removing' | 'removed' | 'unknown';
2
2
  interface BalanceResponse {
3
3
  address: string;
4
4
  balance: string;
@@ -233,6 +233,7 @@ interface SponsorRelayRootfsHealth {
233
233
  tone: SponsorRelayHealthTone;
234
234
  label: string;
235
235
  detail: string | null;
236
+ code?: 'manifest-invalid' | 'rootfs-not-found' | 'rootfs-verifying' | 'rootfs-pending' | 'rootfs-unavailable' | 'rootfs-ready';
236
237
  }
237
238
  interface CompactInstanceDetails {
238
239
  messageStatus: string;
@@ -331,7 +332,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
331
332
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
332
333
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
333
334
 
334
- declare const UI_PACKAGE_VERSION = "0.6.23";
335
+ declare const UI_PACKAGE_VERSION = "0.6.25";
335
336
 
336
337
  interface RootfsRequiredPortForward {
337
338
  port: number;
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.6.23";
45
+ var UI_PACKAGE_VERSION = "0.6.25";
46
46
 
47
47
  // src/shared/format.ts
48
48
  function shortHash(value, head = 8, tail = 6) {
@@ -646,7 +646,7 @@ async function verifyRootfsExists(itemHash, apiHost = DEFAULT_ALEPH_API_HOST) {
646
646
  function normalizeStatus(status) {
647
647
  if (typeof status !== "string") return "unknown";
648
648
  const normalized = status.toLowerCase();
649
- if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
649
+ if (normalized === "processed" || normalized === "pending" || normalized === "rejected" || normalized === "removing" || normalized === "removed") {
650
650
  return normalized;
651
651
  }
652
652
  return "unknown";
@@ -671,6 +671,7 @@ function parseCidFromPayload(payload) {
671
671
  }
672
672
  function parseRejectionReason(payload) {
673
673
  const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
674
+ const reason = typeof payload.reason === "string" ? payload.reason.trim() : "";
674
675
  const details = payload.details && typeof payload.details === "object" ? payload.details : null;
675
676
  const rawErrors = Array.isArray(details?.errors) ? details.errors : [];
676
677
  const firstError = rawErrors[0] && typeof rawErrors[0] === "object" ? rawErrors[0] : null;
@@ -685,6 +686,18 @@ function parseRejectionReason(payload) {
685
686
  };
686
687
  }
687
688
  }
689
+ if (reason === "balance_insufficient") {
690
+ return {
691
+ rejectionErrorCode: errorCode,
692
+ rejectionReason: "The rootfs STORE is being removed because its publisher no longer has enough Aleph credits. Your connected MetaMask balance may be sufficient, but this manifest must point to a newly published, processed rootfs STORE."
693
+ };
694
+ }
695
+ if (reason) {
696
+ return {
697
+ rejectionErrorCode: errorCode,
698
+ rejectionReason: `Aleph marked the rootfs STORE as unavailable: ${reason.replaceAll("_", " ")}.`
699
+ };
700
+ }
688
701
  return {
689
702
  rejectionErrorCode: errorCode,
690
703
  rejectionReason: errorCode != null ? `Rejected by Aleph (error code ${errorCode}).` : null
@@ -726,11 +739,12 @@ async function resolveRootfsReference(itemHash, apiHost = DEFAULT_ALEPH_API_HOST
726
739
  const firstMessage = Array.isArray(payload.messages) && payload.messages[0] && typeof payload.messages[0] === "object" ? payload.messages[0] : null;
727
740
  const messageObject = payload.message && typeof payload.message === "object" ? payload.message : null;
728
741
  const cid = parseCidFromPayload(payload);
729
- const rejection = normalizeStatus(payload.status) === "rejected" ? parseRejectionReason(payload) : { rejectionErrorCode: null, rejectionReason: null };
742
+ const messageStatus = normalizeStatus(payload.status);
743
+ const rejection = messageStatus === "processed" || messageStatus === "pending" ? { rejectionErrorCode: null, rejectionReason: null } : parseRejectionReason(payload);
730
744
  const gateway = cid ? await probeGateway(cid, gatewayBaseUrl) : { gatewayUrl: null, gatewayStatus: "unknown", gatewayError: null };
731
745
  return {
732
746
  itemHash,
733
- messageStatus: normalizeStatus(payload.status),
747
+ messageStatus,
734
748
  messageType: String(payload.type || messageObject?.type || firstMessage?.type || "").toUpperCase() || null,
735
749
  cid,
736
750
  receptionTime: typeof payload.reception_time === "string" ? payload.reception_time : null,
@@ -2471,8 +2485,7 @@ import { keccak256, recoverMessageAddress, stringToHex } from "viem";
2471
2485
  var DEFAULT_ALEPH_API_HOST3 = "https://api.aleph.im";
2472
2486
  var DEFAULT_ALEPH_BOOTSTRAP_CHANNEL = "simple-todo";
2473
2487
  var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
2474
- var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
2475
- var DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE = "relay-bootstrap-v2";
2488
+ var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap-v2";
2476
2489
  var DEFAULT_BOOTSTRAP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2477
2490
  var DEFAULT_BOOTSTRAP_PAGINATION = 50;
2478
2491
  var DEFAULT_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT = 3;
@@ -2506,9 +2519,6 @@ function serializeRelayProofPayload(payload) {
2506
2519
  function relayBootstrapMultiaddrsHash(addrs) {
2507
2520
  return keccak256(stringToHex(JSON.stringify(dedupeMultiaddrs(addrs))));
2508
2521
  }
2509
- function compactPostType(postType) {
2510
- return (postType ?? DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE) === DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE;
2511
- }
2512
2522
  function browserMultiaddrPreference(addr) {
2513
2523
  const normalized = addr.toLowerCase();
2514
2524
  if (normalized.includes("/tcp/443/tls/ws/")) return 0;
@@ -2738,8 +2748,6 @@ function normalizeRelayBootstrapPostRecord(value) {
2738
2748
  function buildRelayBootstrapPostContent(args) {
2739
2749
  const now = args.now ?? Date.now() / 1e3;
2740
2750
  const updatedAt = args.now ?? Date.now();
2741
- const postType = args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE;
2742
- const compact = args.compact ?? compactPostType(postType);
2743
2751
  const fullMultiaddrs = filterPublicMultiaddrs(args.multiaddrs);
2744
2752
  const fullBrowserMultiaddrs = args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2745
2753
  browserDialableOnly: true
@@ -2749,13 +2757,13 @@ function buildRelayBootstrapPostContent(args) {
2749
2757
  args.compactMultiaddrLimit
2750
2758
  );
2751
2759
  return {
2752
- type: postType,
2760
+ type: DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2753
2761
  address: args.sender,
2754
2762
  ...args.ref ? { ref: args.ref } : {},
2755
2763
  content: {
2756
2764
  peerId: args.peerId,
2757
- multiaddrs: compact ? compactMultiaddrs : fullMultiaddrs,
2758
- browserMultiaddrs: compact ? void 0 : fullBrowserMultiaddrs,
2765
+ multiaddrs: compactMultiaddrs,
2766
+ browserMultiaddrs: void 0,
2759
2767
  registrationId: args.registrationId,
2760
2768
  profile: args.profile,
2761
2769
  version: args.version,
@@ -2778,14 +2786,12 @@ async function createRelayBootstrapPost(args) {
2778
2786
  browserMultiaddrs: args.browserMultiaddrs,
2779
2787
  registrationId: args.registrationId,
2780
2788
  ref: args.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF,
2781
- postType: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2782
2789
  profile: args.profile,
2783
2790
  version: args.version,
2784
2791
  ownerAddress: args.ownerAddress,
2785
2792
  publisherAddress: args.publisherAddress,
2786
2793
  authorization: args.authorization,
2787
2794
  relayProof: args.relayProof,
2788
- compact: args.compact,
2789
2795
  compactMultiaddrLimit: args.compactMultiaddrLimit,
2790
2796
  now: nowMillis
2791
2797
  });
@@ -2832,12 +2838,11 @@ async function signRelayBootstrapProof(args) {
2832
2838
  fullBrowserMultiaddrs && fullBrowserMultiaddrs.length > 0 ? fullBrowserMultiaddrs : fullMultiaddrs,
2833
2839
  args.compactMultiaddrLimit
2834
2840
  );
2835
- const compact = args.compact ?? false;
2836
2841
  const payload = {
2837
2842
  peerId: args.peerId,
2838
- multiaddrs: compact ? void 0 : fullMultiaddrs,
2839
- browserMultiaddrs: compact ? void 0 : fullBrowserMultiaddrs,
2840
- multiaddrsHash: compact ? relayBootstrapMultiaddrsHash(compactMultiaddrs) : void 0,
2843
+ multiaddrs: void 0,
2844
+ browserMultiaddrs: void 0,
2845
+ multiaddrsHash: relayBootstrapMultiaddrsHash(compactMultiaddrs),
2841
2846
  registrationId: args.registrationId,
2842
2847
  profile: args.profile,
2843
2848
  version: args.version,
@@ -2867,10 +2872,7 @@ async function fetchAlephBootstrapPosts(options = {}) {
2867
2872
  options.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL
2868
2873
  );
2869
2874
  url.searchParams.set("refs", options.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF);
2870
- url.searchParams.set(
2871
- "types",
2872
- options.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE
2873
- );
2875
+ url.searchParams.set("types", DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE);
2874
2876
  url.searchParams.set(
2875
2877
  "pagination",
2876
2878
  String(options.pagination ?? DEFAULT_BOOTSTRAP_PAGINATION)
@@ -2884,7 +2886,21 @@ async function fetchAlephBootstrapPosts(options = {}) {
2884
2886
  throw new Error(`Aleph bootstrap post lookup failed: ${response.status}`);
2885
2887
  }
2886
2888
  const payload = await response.json();
2887
- return (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null).sort((left, right) => compareRelayBootstrapPostRecency(right, left));
2889
+ const posts = (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null).sort((left, right) => compareRelayBootstrapPostRecency(right, left));
2890
+ posts.forEach(assertSupportedRelayBootstrapPost);
2891
+ return posts;
2892
+ }
2893
+ function assertSupportedRelayBootstrapPost(post) {
2894
+ if (post.type === "relay-bootstrap") {
2895
+ throw new Error(
2896
+ "Legacy relay-bootstrap record encountered. Only relay-bootstrap-v2 is supported."
2897
+ );
2898
+ }
2899
+ if (post.type && post.type !== DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE) {
2900
+ throw new Error(
2901
+ `Unsupported relay bootstrap post type: ${post.type}. Expected ${DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE}.`
2902
+ );
2903
+ }
2888
2904
  }
2889
2905
  function compareRelayBootstrapPostRecency(left, right) {
2890
2906
  const leftUpdatedAt = left.content?.updatedAt ?? 0;
@@ -2906,6 +2922,7 @@ function selectCurrentRelayBootstrapPosts(posts, options = {}) {
2906
2922
  const now = options.now ?? Date.now();
2907
2923
  const selected = /* @__PURE__ */ new Map();
2908
2924
  for (const post of posts) {
2925
+ assertSupportedRelayBootstrapPost(post);
2909
2926
  const content = post.content;
2910
2927
  if (!content) continue;
2911
2928
  if (now - content.updatedAt > maxAgeMs) continue;
@@ -2931,14 +2948,12 @@ async function waitForRelayBootstrapRegistration(args) {
2931
2948
  const expectedSender = args.sender?.trim().toLowerCase() || null;
2932
2949
  const expectedRegistrationId = args.registrationId?.trim() || null;
2933
2950
  const expectedPeerId = args.peerId?.trim() || null;
2934
- const postType = args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE;
2935
2951
  const sleepImpl = args.sleep ?? sleep;
2936
2952
  for (let attempt = 0; attempt < attempts; attempt += 1) {
2937
2953
  const posts = await fetchAlephBootstrapPosts({
2938
2954
  apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2939
2955
  channel: args.channel,
2940
2956
  ref: args.ref,
2941
- postType,
2942
2957
  fetch: args.fetch
2943
2958
  });
2944
2959
  const currentPosts = selectCurrentRelayBootstrapPosts(posts);
@@ -2977,8 +2992,6 @@ async function waitForRelayBootstrapRegistration(args) {
2977
2992
  async function publishRelayBootstrapRegistration(args) {
2978
2993
  const publisherAddress = args.publisherAddress ?? args.sender;
2979
2994
  const ownerAddress = args.ownerAddress;
2980
- const postType = args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE;
2981
- const compact = postType === DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE;
2982
2995
  let ownerAuthorization = args.ownerAuthorization;
2983
2996
  let relayProof = args.relayProof;
2984
2997
  if (!ownerAuthorization && ownerAddress && args.ownerSigner) {
@@ -3005,7 +3018,6 @@ async function publishRelayBootstrapRegistration(args) {
3005
3018
  profile: args.profile,
3006
3019
  version: args.version,
3007
3020
  updatedAt: args.now ?? Date.now(),
3008
- compact,
3009
3021
  compactMultiaddrLimit: args.compactMultiaddrLimit,
3010
3022
  signer: args.publisherSigner
3011
3023
  });
@@ -3018,14 +3030,12 @@ async function publishRelayBootstrapRegistration(args) {
3018
3030
  registrationId: args.registrationId,
3019
3031
  channel: args.channel,
3020
3032
  ref: args.ref,
3021
- postType,
3022
3033
  profile: args.profile,
3023
3034
  version: args.version,
3024
3035
  ownerAddress,
3025
3036
  publisherAddress,
3026
3037
  authorization: ownerAuthorization,
3027
3038
  relayProof,
3028
- compact,
3029
3039
  compactMultiaddrLimit: args.compactMultiaddrLimit,
3030
3040
  now: args.now,
3031
3041
  hasher: args.hasher
@@ -3048,18 +3058,12 @@ async function publishRelayBootstrapRegistration(args) {
3048
3058
  let forgottenHashes = [];
3049
3059
  let forgetResult;
3050
3060
  if (args.forgetPrevious && args.registrationId) {
3051
- const previousPostTypes = compact ? [postType, DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE] : [postType];
3052
- const previousPosts = (await Promise.all(
3053
- previousPostTypes.map(
3054
- (previousPostType) => fetchAlephBootstrapPosts({
3055
- apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
3056
- channel: args.channel,
3057
- ref: args.ref,
3058
- postType: previousPostType,
3059
- fetch: args.fetch
3060
- })
3061
- )
3062
- )).flat();
3061
+ const previousPosts = await fetchAlephBootstrapPosts({
3062
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
3063
+ channel: args.channel,
3064
+ ref: args.ref,
3065
+ fetch: args.fetch
3066
+ });
3063
3067
  const forgottenHashSet = new Set(
3064
3068
  previousPosts.filter((post) => {
3065
3069
  const hash = post.itemHash ?? post.hash;
@@ -3661,7 +3665,9 @@ function draftUcanStoreBootstrapErrors(args) {
3661
3665
  const errors = [];
3662
3666
  const { input } = args;
3663
3667
  if (!args.operatorAddress) {
3664
- errors.push("Connect MetaMask before configuring the ucan-store bootstrap package.");
3668
+ errors.push(
3669
+ "Connect MetaMask before configuring the ucan-store bootstrap package."
3670
+ );
3665
3671
  }
3666
3672
  if (!input.adminDid.trim().startsWith("did:")) {
3667
3673
  errors.push("Admin DID must be a non-empty DID string.");
@@ -3712,7 +3718,9 @@ function buildUcanStoreBootstrapPackage(args) {
3712
3718
  serviceDid: args.input.serviceDid.trim() || null,
3713
3719
  spaceDid: args.input.spaceDid.trim(),
3714
3720
  rootDelegationProof: args.input.rootDelegationProof.trim(),
3715
- allowedCapabilities: parseAllowedCapabilities(args.input.allowedCapabilities),
3721
+ allowedCapabilities: parseAllowedCapabilities(
3722
+ args.input.allowedCapabilities
3723
+ ),
3716
3724
  defaultUserDelegationExpiration: parseOptionalExpiration(
3717
3725
  args.input.defaultUserDelegationExpiration
3718
3726
  ),
@@ -3751,7 +3759,9 @@ function defaultState(props = {}) {
3751
3759
  instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
3752
3760
  ucanStoreBootstrap: defaultUcanStoreBootstrapInput(props),
3753
3761
  tierId: DEFAULT_TIER_ID,
3754
- showAdvanced: Boolean(props.manifestJson?.trim() || props.sshPublicKey?.trim()),
3762
+ showAdvanced: Boolean(
3763
+ props.manifestJson?.trim() || props.sshPublicKey?.trim()
3764
+ ),
3755
3765
  showInstances: props.showInstances ?? true,
3756
3766
  showPasteManifest: Boolean(props.manifestJson?.trim()),
3757
3767
  busy: {
@@ -3835,48 +3845,55 @@ function rootfsHealth(args) {
3835
3845
  return {
3836
3846
  tone: "error",
3837
3847
  label: "manifest invalid",
3838
- detail: args.manifestState.errors[0] ?? "Manifest could not be parsed."
3848
+ detail: args.manifestState.errors[0] ?? "Manifest could not be parsed.",
3849
+ code: "manifest-invalid"
3839
3850
  };
3840
3851
  }
3841
3852
  if (!args.rootfsVerified) {
3842
3853
  return {
3843
3854
  tone: "error",
3844
3855
  label: "not found on Aleph",
3845
- detail: "The manifest points to a rootfs STORE message that Aleph cannot find. Check that the manifest URL is correct and that the referenced rootfs was not forgotten."
3856
+ detail: "The manifest points to a rootfs STORE message that Aleph cannot find. Check that the manifest URL is correct and that the referenced rootfs was not forgotten.",
3857
+ code: "rootfs-not-found"
3846
3858
  };
3847
3859
  }
3848
3860
  if (!args.resolution) {
3849
3861
  return {
3850
3862
  tone: "caution",
3851
3863
  label: "verifying rootfs",
3852
- detail: "The rootfs reference is still being resolved."
3864
+ detail: "The rootfs reference is still being resolved.",
3865
+ code: "rootfs-verifying"
3853
3866
  };
3854
3867
  }
3855
3868
  if (args.resolution.messageStatus === "processed") {
3856
3869
  return {
3857
3870
  tone: "ok",
3858
3871
  label: "deployable",
3859
- detail: args.resolution.gatewayUrl ?? "Rootfs verified on Aleph."
3872
+ detail: args.resolution.gatewayUrl ?? "Rootfs verified on Aleph.",
3873
+ code: "rootfs-ready"
3860
3874
  };
3861
3875
  }
3862
3876
  if (args.resolution.messageStatus === "pending" && args.resolution.gatewayStatus === "reachable") {
3863
3877
  return {
3864
3878
  tone: "caution",
3865
3879
  label: "pending but reachable",
3866
- detail: "Gateway probe succeeded even though Aleph still reports pending."
3880
+ detail: "Gateway probe succeeded even though Aleph still reports pending.",
3881
+ code: "rootfs-pending"
3867
3882
  };
3868
3883
  }
3869
3884
  if (args.resolution.messageStatus === "pending") {
3870
3885
  return {
3871
3886
  tone: "caution",
3872
3887
  label: "pending on Aleph",
3873
- detail: "Wait until the STORE message is processed."
3888
+ detail: "Wait until the STORE message is processed.",
3889
+ code: "rootfs-pending"
3874
3890
  };
3875
3891
  }
3876
3892
  return {
3877
3893
  tone: "error",
3878
3894
  label: "manifest rootfs not deployable",
3879
- detail: args.resolution.rejectionReason ?? (args.resolution.messageType === "STORE" ? "This manifest points to a rootfs STORE message that exists but is not deployable anymore, for example because it was forgotten or replaced. Double-check the manifest URL." : "Aleph rejected the rootfs reference. Double-check that the manifest URL points to the intended current rootfs.")
3895
+ detail: args.resolution.rejectionReason ?? (args.resolution.messageType === "STORE" ? "This manifest points to a rootfs STORE message that exists but is not deployable anymore, for example because it was forgotten or replaced. Double-check the manifest URL." : "Aleph rejected the rootfs reference. Double-check that the manifest URL points to the intended current rootfs."),
3896
+ code: "rootfs-unavailable"
3880
3897
  };
3881
3898
  }
3882
3899
  async function resolveManifest(args) {
@@ -4164,7 +4181,9 @@ var SponsorRelayController = class {
4164
4181
  const profile = bootstrapRelayProfileForManifest(this.state.manifest);
4165
4182
  if (!profile && deploymentProfile !== "ucan-store") return;
4166
4183
  if (profile && !this.state.wallet.address) {
4167
- throw new Error("A connected wallet is required to register relay bootstrap addresses.");
4184
+ throw new Error(
4185
+ "A connected wallet is required to register relay bootstrap addresses."
4186
+ );
4168
4187
  }
4169
4188
  const runtimeHostIpv4 = args.runtime.hostIpv4;
4170
4189
  if (!runtimeHostIpv4) {
@@ -4188,7 +4207,9 @@ var SponsorRelayController = class {
4188
4207
  }
4189
4208
  const walletAddress2 = this.state.wallet.address;
4190
4209
  if (!walletAddress2) {
4191
- throw new Error("A connected wallet is required to configure the ucan-store bootstrap package.");
4210
+ throw new Error(
4211
+ "A connected wallet is required to configure the ucan-store bootstrap package."
4212
+ );
4192
4213
  }
4193
4214
  const bootstrapValidation = buildUcanStoreBootstrapPackage({
4194
4215
  input: this.state.ucanStoreBootstrap,
@@ -4303,7 +4324,9 @@ var SponsorRelayController = class {
4303
4324
  if (!profile) return;
4304
4325
  const walletAddress = this.state.wallet.address;
4305
4326
  if (!walletAddress) {
4306
- throw new Error("A connected wallet is required to register relay bootstrap addresses.");
4327
+ throw new Error(
4328
+ "A connected wallet is required to register relay bootstrap addresses."
4329
+ );
4307
4330
  }
4308
4331
  let bootstrapSignalRelayMetadata = null;
4309
4332
  if (profile === "orbitdb-relay") {
@@ -4349,7 +4372,9 @@ var SponsorRelayController = class {
4349
4372
  const tcpPort = mappedPorts2["9091"]?.host ?? 0;
4350
4373
  const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
4351
4374
  if (!tcpPort || !wsPort) {
4352
- throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
4375
+ throw new Error(
4376
+ "OrbitDB relay runtime is missing required mapped TCP/WS ports."
4377
+ );
4353
4378
  }
4354
4379
  const configureResult = await configureOrbitdbRelaySetup2({
4355
4380
  hostIpv4: runtimeHostIpv4,
@@ -4372,7 +4397,9 @@ var SponsorRelayController = class {
4372
4397
  }
4373
4398
  } else {
4374
4399
  if (!args.deploymentToken) {
4375
- throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
4400
+ throw new Error(
4401
+ "Missing deployment token for Aleph bootstrap config handoff."
4402
+ );
4376
4403
  }
4377
4404
  if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
4378
4405
  this.emitProgress({
@@ -4598,7 +4625,19 @@ var SponsorRelayController = class {
4598
4625
  });
4599
4626
  return;
4600
4627
  }
4601
- throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
4628
+ throw new Error(
4629
+ "Relay metadata did not include a peer ID and public multiaddrs."
4630
+ );
4631
+ }
4632
+ if (profile === "uc-go-peer") {
4633
+ const browserDialableMultiaddrs = relayMetadata.browserBootstrapMultiaddrs.filter(
4634
+ (multiaddr) => /\/(?:tls\/ws|wss)\/p2p\//.test(multiaddr) || /\/(?:webtransport|webrtc-direct)\//.test(multiaddr) && /\/certhash\//.test(multiaddr)
4635
+ );
4636
+ if (browserDialableMultiaddrs.length === 0) {
4637
+ throw new Error(
4638
+ "Relay metadata did not include a browser-dialable WSS address or a WebTransport/WebRTC Direct address with certhash."
4639
+ );
4640
+ }
4602
4641
  }
4603
4642
  const { peerId } = relayMetadata;
4604
4643
  const registrationId = createRelayBootstrapRegistrationId(
@@ -5150,18 +5189,10 @@ var SponsorRelayController = class {
5150
5189
  const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
5151
5190
  this.client.fetchBalance(this.state.wallet.address),
5152
5191
  this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([]),
5153
- bootstrapEnabled ? Promise.all(
5154
- [
5155
- DEFAULT_ALEPH_BOOTSTRAP_COMPACT_POST_TYPE,
5156
- DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE
5157
- ].map(
5158
- (postType) => fetchAlephBootstrapPosts({
5159
- apiHost: this.client.apiHost,
5160
- postType,
5161
- fetch
5162
- }).catch(() => [])
5163
- )
5164
- ).then((pages) => pages.flat()) : Promise.resolve([])
5192
+ bootstrapEnabled ? fetchAlephBootstrapPosts({
5193
+ apiHost: this.client.apiHost,
5194
+ fetch
5195
+ }) : Promise.resolve([])
5165
5196
  ]);
5166
5197
  balance = nextBalance;
5167
5198
  instances = await Promise.all(
@@ -5225,7 +5256,9 @@ var SponsorRelayController = class {
5225
5256
  const activeInstanceHashes = new Set(
5226
5257
  instances.map((entry) => entry.instance.item_hash)
5227
5258
  );
5228
- bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
5259
+ bootstrapRegistrations = selectCurrentRelayBootstrapPosts(
5260
+ rawBootstrapPosts
5261
+ ).filter(
5229
5262
  (entry) => relayBootstrapMatchesCurrentWallet(
5230
5263
  entry,
5231
5264
  normalizedWalletAddress,
@@ -5248,7 +5281,9 @@ var SponsorRelayController = class {
5248
5281
  peerId: entry.content.peerId,
5249
5282
  registrationId: entry.content.registrationId,
5250
5283
  multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
5251
- browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
5284
+ browserMultiaddrs: Array.isArray(
5285
+ entry.content.browserMultiaddrs
5286
+ ) ? entry.content.browserMultiaddrs : [],
5252
5287
  ownerAddress: typeof entry.content.ownerAddress === "string" ? entry.content.ownerAddress : void 0,
5253
5288
  publisherAddress: typeof entry.content.publisherAddress === "string" ? entry.content.publisherAddress : void 0,
5254
5289
  updatedAt: entry.content.updatedAt
@@ -5369,7 +5404,9 @@ var SponsorRelayController = class {
5369
5404
  ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
5370
5405
  ].slice(0, 5);
5371
5406
  if (orderedCrns.length === 0) {
5372
- throw new Error("No compatible CRN is currently available for this tier.");
5407
+ throw new Error(
5408
+ "No compatible CRN is currently available for this tier."
5409
+ );
5373
5410
  }
5374
5411
  const attemptErrors = [];
5375
5412
  let lastError = null;
@@ -5802,7 +5839,7 @@ var SponsorRelayController = class {
5802
5839
  await forgetAlephMessages({
5803
5840
  sender: this.state.wallet.address,
5804
5841
  hashes: [attemptItemHash],
5805
- reason: `Discard failed Sponsor Relay deployment attempt on ${attemptLabel}`,
5842
+ reason: `Discard failed Relay Button deployment attempt on ${attemptLabel}`,
5806
5843
  signer: personalSign,
5807
5844
  hasher: sha256Hex,
5808
5845
  fetch: (url, init) => fetch(url, init).then(async (response) => ({
@@ -5912,7 +5949,7 @@ var SponsorRelayController = class {
5912
5949
  await forgetAlephMessages({
5913
5950
  sender: this.state.wallet.address,
5914
5951
  hashes: hashesToForget,
5915
- reason: linkedRegistrationCount > 0 ? `Deleted instance and ${linkedRegistrationCount} linked relay bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"} from Sponsor Relay panel` : "Deleted from Sponsor Relay panel",
5952
+ reason: linkedRegistrationCount > 0 ? `Deleted instance and ${linkedRegistrationCount} linked relay bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"} from Relay Button panel` : "Deleted from Relay Button panel",
5916
5953
  signer: personalSign,
5917
5954
  hasher: sha256Hex,
5918
5955
  fetch: asJsonFetch,
@@ -5981,7 +6018,7 @@ var SponsorRelayController = class {
5981
6018
  await forgetAlephMessages({
5982
6019
  sender: this.state.wallet.address,
5983
6020
  hashes: [registrationHash],
5984
- reason: "Deleted orphan relay bootstrap registration from Sponsor Relay panel",
6021
+ reason: "Deleted orphan relay bootstrap registration from Relay Button panel",
5985
6022
  signer: personalSign,
5986
6023
  hasher: sha256Hex,
5987
6024
  fetch: asJsonFetch,
@@ -51,7 +51,7 @@ export let apiHosts = undefined
51
51
  }
52
52
 
53
53
  function deploymentTitle() {
54
- return deploymentProfile() === 'ucan-store' ? 'Sponsor Service' : 'Sponsor Relay'
54
+ return deploymentProfile() === 'ucan-store' ? 'Service Button' : 'Relay Button'
55
55
  }
56
56
 
57
57
  function deploymentInstanceFallbackLabel() {
@@ -62,6 +62,12 @@ export let apiHosts = undefined
62
62
  return deploymentProfile() === 'ucan-store' ? 'Deploy Service' : 'Deploy Relay'
63
63
  }
64
64
 
65
+ function deploymentCallToAction() {
66
+ if (state.busy.deploying) return 'Deploying…'
67
+ if (state.rootfsHealth.tone === 'error') return 'Deployment blocked — Rootfs unavailable'
68
+ return deploymentButtonLabel()
69
+ }
70
+
65
71
  function bootstrapUiEnabled() {
66
72
  return deploymentProfile() !== 'ucan-store'
67
73
  }
@@ -264,10 +270,28 @@ export let apiHosts = undefined
264
270
  </div>
265
271
  </div>
266
272
 
273
+ {#if state.rootfsHealth.tone === 'error'}
274
+ <div class="rootfs-blocker" id="relay-rootfs-deployment-blocker" role="alert">
275
+ <strong>Rootfs unavailable — deployment blocked</strong>
276
+ <p>{state.rootfsHealth.detail}</p>
277
+ <small>This is separate from the connected MetaMask balance. The manifest must reference an Aleph rootfs STORE with status <code>processed</code>.</small>
278
+ {#if state.manifest?.rootfsItemHash}
279
+ <code class="rootfs-reference">Rootfs: {state.manifest.rootfsItemHash}{state.rootfsResolution?.messageStatus ? ` · Aleph status: ${state.rootfsResolution.messageStatus}` : ''}</code>
280
+ {/if}
281
+ <div class="rootfs-actions">
282
+ <button class="refresh" type="button" on:click={() => controller.refresh()} disabled={state.busy.refreshing}>{state.busy.refreshing ? 'Checking…' : 'Retry validation'}</button>
283
+ <button class="refresh" type="button" on:click={() => controller.setShowAdvanced(true)}>Edit manifest URL</button>
284
+ {#if /^https?:\/\//.test(state.manifestUrl)}
285
+ <a href={state.manifestUrl} target="_blank" rel="noreferrer">Open manifest</a>
286
+ {/if}
287
+ </div>
288
+ </div>
289
+ {/if}
290
+
267
291
  <div class="actions">
268
292
  {#if state.wallet.connected}
269
- <button class="primary" type="button" on:click={() => controller.deploy()} disabled={state.busy.deploying || state.rootfsHealth.tone !== 'ok'}>
270
- {state.busy.deploying ? 'Deploying…' : deploymentButtonLabel()}
293
+ <button class:blocked={state.rootfsHealth.tone === 'error'} class="primary" type="button" aria-describedby={state.rootfsHealth.tone === 'error' ? 'relay-rootfs-deployment-blocker' : undefined} on:click={() => controller.deploy()} disabled={state.busy.deploying || state.rootfsHealth.tone !== 'ok'}>
294
+ {deploymentCallToAction()}
271
295
  </button>
272
296
  {:else}
273
297
  <button class="primary" type="button" on:click={() => controller.connectWallet()} disabled={state.busy.connectingWallet}>
@@ -470,6 +494,25 @@ export let apiHosts = undefined
470
494
  line-height: 1.35;
471
495
  }
472
496
 
497
+ .rootfs-blocker {
498
+ display: grid;
499
+ gap: 0.55rem;
500
+ margin-top: 1rem;
501
+ padding: 0.9rem;
502
+ border: 1px solid rgba(248, 113, 113, 0.58);
503
+ border-radius: 1rem;
504
+ background: rgba(127, 29, 29, 0.34);
505
+ color: #fee2e2;
506
+ }
507
+
508
+ .rootfs-blocker p,
509
+ .rootfs-blocker small { margin: 0; line-height: 1.45; }
510
+ .rootfs-reference { overflow-wrap: anywhere; font-size: 0.7rem; }
511
+ .rootfs-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
512
+ .rootfs-actions .refresh { padding: 0.55rem 0.7rem; }
513
+ .rootfs-actions a { color: #bfdbfe; font-size: 0.78rem; }
514
+ .primary.blocked { border-color: rgba(248, 113, 113, 0.45); background: rgba(148, 163, 184, 0.22); color: #fee2e2; box-shadow: none; cursor: not-allowed; }
515
+
473
516
  .eyebrow,
474
517
  .field span,
475
518
  .metric-card span,
@@ -5,7 +5,7 @@
5
5
  </script>
6
6
 
7
7
  <button class={`launcher ${mode} ${open ? 'open' : ''}`} type="button" on:click={onToggle}>
8
- <span>Sponsor Relay</span>
8
+ <span>Relay Button</span>
9
9
  </button>
10
10
 
11
11
  <style>