@le-space/ui 0.3.21 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/ui",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "Floating Aleph sponsor relay UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,8 +36,8 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@le-space/browser": "0.3.21",
40
- "@le-space/core": "0.3.21",
39
+ "@le-space/browser": "0.3.23",
40
+ "@le-space/core": "0.3.23",
41
41
  "viem": "^2.38.5"
42
42
  },
43
43
  "peerDependencies": {
package/react/index.js CHANGED
@@ -39,7 +39,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
39
39
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
40
40
 
41
41
  // src/shared/package-version.ts
42
- var UI_PACKAGE_VERSION = "0.3.21";
42
+ var UI_PACKAGE_VERSION = "0.3.23";
43
43
 
44
44
  // src/shared/format.ts
45
45
  function shortHash(value, head = 8, tail = 6) {
@@ -2152,6 +2152,31 @@ var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
2152
2152
  var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
2153
2153
  var DEFAULT_BOOTSTRAP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2154
2154
  var DEFAULT_BOOTSTRAP_PAGINATION = 50;
2155
+ var RELAY_BOOTSTRAP_SIGNATURE_SCHEME = "personal_sign";
2156
+ function serializeOwnerAuthorizationPayload(payload) {
2157
+ return JSON.stringify({
2158
+ ownerAddress: payload.ownerAddress,
2159
+ publisherAddress: payload.publisherAddress,
2160
+ peerId: payload.peerId,
2161
+ registrationId: payload.registrationId,
2162
+ profile: payload.profile,
2163
+ version: payload.version,
2164
+ instanceItemHash: payload.instanceItemHash,
2165
+ issuedAt: payload.issuedAt,
2166
+ expiresAt: payload.expiresAt
2167
+ });
2168
+ }
2169
+ function serializeRelayProofPayload(payload) {
2170
+ return JSON.stringify({
2171
+ peerId: payload.peerId,
2172
+ multiaddrs: dedupeMultiaddrs(payload.multiaddrs),
2173
+ browserMultiaddrs: payload.browserMultiaddrs ? dedupeMultiaddrs(payload.browserMultiaddrs) : void 0,
2174
+ registrationId: payload.registrationId,
2175
+ profile: payload.profile,
2176
+ version: payload.version,
2177
+ updatedAt: payload.updatedAt
2178
+ });
2179
+ }
2155
2180
  function asTrimmedString(value) {
2156
2181
  return typeof value === "string" && value.trim() ? value.trim() : null;
2157
2182
  }
@@ -2163,9 +2188,48 @@ function asNumber4(value) {
2163
2188
  }
2164
2189
  return null;
2165
2190
  }
2191
+ function normalizeHost(host) {
2192
+ return host.trim().toLowerCase();
2193
+ }
2166
2194
  function normalizeAddress(address) {
2167
2195
  return address.trim().toLowerCase();
2168
2196
  }
2197
+ function normalizeSignature(signature) {
2198
+ const trimmed = signature.trim();
2199
+ return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
2200
+ }
2201
+ function splitMultiaddr(addr) {
2202
+ return addr.split("/").filter(Boolean);
2203
+ }
2204
+ function isPrivateIpv4(ip) {
2205
+ const parts = ip.split(".").map((part) => Number(part));
2206
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
2207
+ return false;
2208
+ }
2209
+ if (parts[0] === 10) return true;
2210
+ if (parts[0] === 127) return true;
2211
+ if (parts[0] === 0) return true;
2212
+ if (parts[0] === 169 && parts[1] === 254) return true;
2213
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
2214
+ if (parts[0] === 192 && parts[1] === 168) return true;
2215
+ if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true;
2216
+ return false;
2217
+ }
2218
+ function isPrivateIpv6(ip) {
2219
+ const normalized = ip.trim().toLowerCase();
2220
+ return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
2221
+ }
2222
+ function isLocalHostname(host) {
2223
+ const normalized = normalizeHost(host);
2224
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local");
2225
+ }
2226
+ function hasPeerId(addr) {
2227
+ return splitMultiaddr(addr).includes("p2p");
2228
+ }
2229
+ function isBrowserDialableMultiaddr(addr) {
2230
+ const normalized = addr.toLowerCase();
2231
+ return normalized.includes("/ws") || normalized.includes("/wss") || normalized.includes("/webtransport") || normalized.includes("/webrtc-direct");
2232
+ }
2169
2233
  function dedupeMultiaddrs(addrs) {
2170
2234
  const seen = /* @__PURE__ */ new Set();
2171
2235
  const result = [];
@@ -2177,6 +2241,33 @@ function dedupeMultiaddrs(addrs) {
2177
2241
  }
2178
2242
  return result;
2179
2243
  }
2244
+ function isPublicMultiaddr(addr) {
2245
+ const parts = splitMultiaddr(addr);
2246
+ for (let index = 0; index < parts.length; index += 1) {
2247
+ const protocol = parts[index];
2248
+ const value = parts[index + 1];
2249
+ if (protocol === "ip4" && value) {
2250
+ return !isPrivateIpv4(value);
2251
+ }
2252
+ if (protocol === "ip6" && value) {
2253
+ return !isPrivateIpv6(value);
2254
+ }
2255
+ if ((protocol === "dns4" || protocol === "dns6" || protocol === "dnsaddr") && value) {
2256
+ return !isLocalHostname(value);
2257
+ }
2258
+ }
2259
+ return false;
2260
+ }
2261
+ function filterPublicMultiaddrs(addrs, options = {}) {
2262
+ return dedupeMultiaddrs(addrs).filter((addr) => {
2263
+ if (!isPublicMultiaddr(addr)) return false;
2264
+ if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
2265
+ if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr)) {
2266
+ return false;
2267
+ }
2268
+ return true;
2269
+ });
2270
+ }
2180
2271
  function normalizeRelayBootstrapContent(value) {
2181
2272
  if (!value || typeof value !== "object") return null;
2182
2273
  const content = value;
@@ -2271,6 +2362,104 @@ function normalizeRelayBootstrapPostRecord(value) {
2271
2362
  content: normalizeRelayBootstrapContent(entry.content)
2272
2363
  };
2273
2364
  }
2365
+ function buildRelayBootstrapPostContent(args) {
2366
+ const now = args.now ?? Date.now() / 1e3;
2367
+ const updatedAt = args.now ?? Date.now();
2368
+ return {
2369
+ type: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2370
+ address: args.sender,
2371
+ ...args.ref ? { ref: args.ref } : {},
2372
+ content: {
2373
+ peerId: args.peerId,
2374
+ multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
2375
+ browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2376
+ browserDialableOnly: true
2377
+ }) : void 0,
2378
+ registrationId: args.registrationId,
2379
+ profile: args.profile,
2380
+ version: args.version,
2381
+ ownerAddress: args.ownerAddress,
2382
+ publisherAddress: args.publisherAddress,
2383
+ authorization: args.authorization,
2384
+ relayProof: args.relayProof,
2385
+ updatedAt: Math.round(updatedAt)
2386
+ },
2387
+ time: now
2388
+ };
2389
+ }
2390
+ async function createRelayBootstrapPost(args) {
2391
+ const nowMillis = args.now ?? Date.now();
2392
+ const nowSeconds = nowMillis / 1e3;
2393
+ const itemContent = buildRelayBootstrapPostContent({
2394
+ sender: args.sender,
2395
+ peerId: args.peerId,
2396
+ multiaddrs: args.multiaddrs,
2397
+ browserMultiaddrs: args.browserMultiaddrs,
2398
+ registrationId: args.registrationId,
2399
+ ref: args.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF,
2400
+ postType: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2401
+ profile: args.profile,
2402
+ version: args.version,
2403
+ ownerAddress: args.ownerAddress,
2404
+ publisherAddress: args.publisherAddress,
2405
+ authorization: args.authorization,
2406
+ relayProof: args.relayProof,
2407
+ now: nowMillis
2408
+ });
2409
+ itemContent.time = nowSeconds;
2410
+ const serialized = JSON.stringify(itemContent);
2411
+ const itemHash = await args.hasher(serialized);
2412
+ return {
2413
+ channel: args.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL,
2414
+ sender: args.sender,
2415
+ chain: "ETH",
2416
+ type: "POST",
2417
+ time: nowSeconds,
2418
+ item_type: "inline",
2419
+ item_content: serialized,
2420
+ item_hash: itemHash
2421
+ };
2422
+ }
2423
+ async function signRelayBootstrapAuthorization(args) {
2424
+ const payload = {
2425
+ ownerAddress: args.ownerAddress,
2426
+ publisherAddress: args.publisherAddress,
2427
+ peerId: args.peerId,
2428
+ registrationId: args.registrationId,
2429
+ profile: args.profile,
2430
+ version: args.version,
2431
+ instanceItemHash: args.instanceItemHash,
2432
+ issuedAt: args.issuedAt ?? Date.now(),
2433
+ expiresAt: args.expiresAt
2434
+ };
2435
+ const serialized = serializeOwnerAuthorizationPayload(payload);
2436
+ const signature = await args.signer(args.ownerAddress, serialized);
2437
+ return {
2438
+ scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
2439
+ payload,
2440
+ signature: normalizeSignature(signature)
2441
+ };
2442
+ }
2443
+ async function signRelayBootstrapProof(args) {
2444
+ const payload = {
2445
+ peerId: args.peerId,
2446
+ multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
2447
+ browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2448
+ browserDialableOnly: true
2449
+ }) : void 0,
2450
+ registrationId: args.registrationId,
2451
+ profile: args.profile,
2452
+ version: args.version,
2453
+ updatedAt: args.updatedAt ?? Date.now()
2454
+ };
2455
+ const serialized = serializeRelayProofPayload(payload);
2456
+ const signature = await args.signer(args.publisherAddress, serialized);
2457
+ return {
2458
+ scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
2459
+ payload,
2460
+ signature: normalizeSignature(signature)
2461
+ };
2462
+ }
2274
2463
  async function fetchAlephBootstrapPosts(options = {}) {
2275
2464
  const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
2276
2465
  if (typeof fetchImpl !== "function") {
@@ -2389,6 +2578,109 @@ async function waitForRelayBootstrapRegistration(args) {
2389
2578
  }
2390
2579
  return null;
2391
2580
  }
2581
+ async function publishRelayBootstrapRegistration(args) {
2582
+ const publisherAddress = args.publisherAddress ?? args.sender;
2583
+ const ownerAddress = args.ownerAddress;
2584
+ let ownerAuthorization = args.ownerAuthorization;
2585
+ let relayProof = args.relayProof;
2586
+ if (!ownerAuthorization && ownerAddress && args.ownerSigner) {
2587
+ ownerAuthorization = await signRelayBootstrapAuthorization({
2588
+ ownerAddress,
2589
+ publisherAddress,
2590
+ peerId: args.peerId,
2591
+ registrationId: args.registrationId,
2592
+ profile: args.profile,
2593
+ version: args.version,
2594
+ instanceItemHash: args.instanceItemHash,
2595
+ issuedAt: args.authorizationIssuedAt ?? args.now ?? Date.now(),
2596
+ expiresAt: args.authorizationExpiresAt,
2597
+ signer: args.ownerSigner
2598
+ });
2599
+ }
2600
+ if (!relayProof && args.publisherSigner) {
2601
+ relayProof = await signRelayBootstrapProof({
2602
+ publisherAddress,
2603
+ peerId: args.peerId,
2604
+ multiaddrs: args.multiaddrs,
2605
+ browserMultiaddrs: args.browserMultiaddrs,
2606
+ registrationId: args.registrationId,
2607
+ profile: args.profile,
2608
+ version: args.version,
2609
+ updatedAt: args.now ?? Date.now(),
2610
+ signer: args.publisherSigner
2611
+ });
2612
+ }
2613
+ const unsigned = await createRelayBootstrapPost({
2614
+ sender: args.sender,
2615
+ peerId: args.peerId,
2616
+ multiaddrs: args.multiaddrs,
2617
+ browserMultiaddrs: args.browserMultiaddrs,
2618
+ registrationId: args.registrationId,
2619
+ channel: args.channel,
2620
+ ref: args.ref,
2621
+ postType: args.postType,
2622
+ profile: args.profile,
2623
+ version: args.version,
2624
+ ownerAddress,
2625
+ publisherAddress,
2626
+ authorization: ownerAuthorization,
2627
+ relayProof,
2628
+ now: args.now,
2629
+ hasher: args.hasher
2630
+ });
2631
+ const content = JSON.parse(unsigned.item_content);
2632
+ if ((content.content?.multiaddrs?.length ?? 0) === 0) {
2633
+ return {
2634
+ status: "skipped",
2635
+ reason: "No public relay multiaddrs remained after filtering.",
2636
+ publishedMultiaddrs: [],
2637
+ publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? []
2638
+ };
2639
+ }
2640
+ const message = await signAlephMessage(unsigned, args.signer);
2641
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
2642
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2643
+ sync: args.sync ?? true,
2644
+ fetch: args.fetch
2645
+ });
2646
+ let forgottenHashes = [];
2647
+ let forgetResult;
2648
+ if (args.forgetPrevious && args.registrationId) {
2649
+ const previousPosts = await fetchAlephBootstrapPosts({
2650
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2651
+ channel: args.channel,
2652
+ ref: args.ref,
2653
+ postType: args.postType,
2654
+ fetch: args.fetch
2655
+ });
2656
+ forgottenHashes = previousPosts.filter(
2657
+ (post) => post.address?.toLowerCase() === args.sender.toLowerCase() && post.content?.registrationId === args.registrationId && (post.itemHash ?? post.hash) != null && (post.itemHash ?? post.hash) !== unsigned.item_hash
2658
+ ).map((post) => post.itemHash ?? post.hash ?? "").filter(Boolean);
2659
+ if (forgottenHashes.length > 0) {
2660
+ forgetResult = await forgetAlephMessages({
2661
+ sender: args.sender,
2662
+ hashes: forgottenHashes,
2663
+ reason: args.forgetPreviousReason ?? `Replace older relay bootstrap records for ${args.registrationId}`,
2664
+ signer: args.signer,
2665
+ hasher: args.hasher,
2666
+ fetch: args.fetch,
2667
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2668
+ sync: args.sync ?? true
2669
+ });
2670
+ }
2671
+ }
2672
+ return {
2673
+ status: "published",
2674
+ itemHash: unsigned.item_hash,
2675
+ message,
2676
+ response,
2677
+ httpStatus,
2678
+ publishedMultiaddrs: content.content?.multiaddrs ?? [],
2679
+ publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? [],
2680
+ forgottenHashes,
2681
+ forgetResult
2682
+ };
2683
+ }
2392
2684
 
2393
2685
  // ../core/src/bootstrap-config.ts
2394
2686
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
@@ -3216,9 +3508,61 @@ var SponsorRelayController = class {
3216
3508
  visibleRegistration
3217
3509
  });
3218
3510
  if (!visibleRegistration) {
3219
- throw new Error(
3220
- "Relay bootstrap registration did not become visible on Aleph."
3221
- );
3511
+ this.trace("deploy:bootstrap-registration-fallback-start", {
3512
+ itemHash: args.itemHash,
3513
+ registrationId,
3514
+ peerId,
3515
+ probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3516
+ browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3517
+ });
3518
+ this.emitProgress({
3519
+ stage: "publishing-bootstrap",
3520
+ label: "Publishing bootstrap fallback",
3521
+ progress: 98,
3522
+ status: "warning",
3523
+ itemHash: args.itemHash,
3524
+ detail: "Guest bootstrap registration was delayed. Publishing the relay bootstrap record from the browser."
3525
+ });
3526
+ const fallbackPublication = await publishRelayBootstrapRegistration({
3527
+ sender: this.state.wallet.address,
3528
+ signer: personalSign,
3529
+ hasher: sha256Hex,
3530
+ fetch: asJsonFetch,
3531
+ apiHost: this.client.apiHost,
3532
+ peerId,
3533
+ multiaddrs: relayMetadata.probeMultiaddrs,
3534
+ browserMultiaddrs: relayMetadata.browserBootstrapMultiaddrs,
3535
+ registrationId,
3536
+ profile,
3537
+ sync: true,
3538
+ forgetPrevious: true
3539
+ });
3540
+ this.trace("deploy:bootstrap-registration-fallback-published", {
3541
+ itemHash: args.itemHash,
3542
+ registrationId,
3543
+ peerId,
3544
+ fallbackPublication
3545
+ });
3546
+ const fallbackVisibleRegistration = await waitForRelayBootstrapRegistration({
3547
+ sender: this.state.wallet.address,
3548
+ registrationId,
3549
+ peerId,
3550
+ fetch: asJsonFetch,
3551
+ apiHost: this.client.apiHost,
3552
+ attempts: 24,
3553
+ delayMs: 2500
3554
+ });
3555
+ this.trace("deploy:bootstrap-registration-fallback-visible", {
3556
+ itemHash: args.itemHash,
3557
+ registrationId,
3558
+ peerId,
3559
+ fallbackVisibleRegistration
3560
+ });
3561
+ if (!fallbackVisibleRegistration) {
3562
+ throw new Error(
3563
+ "Relay bootstrap registration did not become visible on Aleph."
3564
+ );
3565
+ }
3222
3566
  }
3223
3567
  if (profile === "uc-go-peer" && args.deploymentToken) {
3224
3568
  await deleteVmBootstrapConfig({
@@ -4200,6 +4544,12 @@ function progressBadgeLabel(stage) {
4200
4544
  }
4201
4545
  }
4202
4546
  var COMPLETED_PROGRESS_VISIBLE_MS = 12e3;
4547
+ var POLLING_STAGES = /* @__PURE__ */ new Set([
4548
+ "waiting-for-aleph",
4549
+ "deployment-confirmed",
4550
+ "publishing-bootstrap",
4551
+ "refreshing-instances"
4552
+ ]);
4203
4553
  function isDeploymentProgressVisible(state) {
4204
4554
  const stage = state.deploymentProgress.stage;
4205
4555
  if (stage === "idle") {
@@ -4213,6 +4563,21 @@ function isDeploymentProgressVisible(state) {
4213
4563
  }
4214
4564
  return true;
4215
4565
  }
4566
+ function pollingIndicator(state) {
4567
+ if (state.busy.refreshing) {
4568
+ return {
4569
+ label: "Checking relay state",
4570
+ detail: state.statusText || "Refreshing relay sponsor data from Aleph and the selected CRN."
4571
+ };
4572
+ }
4573
+ if (!POLLING_STAGES.has(state.deploymentProgress.stage)) {
4574
+ return null;
4575
+ }
4576
+ return {
4577
+ label: state.deploymentProgress.label || "Polling relay state",
4578
+ detail: state.deploymentProgress.detail || state.statusText || "Waiting for the next confirmed relay state from Aleph."
4579
+ };
4580
+ }
4216
4581
  function launcherIndicator(state) {
4217
4582
  if (state.deploymentProgress.stage !== "idle" && state.deploymentProgress.stage !== "completed") {
4218
4583
  return {
@@ -4354,6 +4719,7 @@ function SponsorRelayFab(props) {
4354
4719
  }, [launcherMode]);
4355
4720
  const progressActive = state.deploymentProgress.stage !== "idle" && state.deploymentProgress.stage !== "completed" && state.deploymentProgress.stage !== "error";
4356
4721
  const deploymentProgressVisible = isDeploymentProgressVisible(state);
4722
+ const pollingState = pollingIndicator(state);
4357
4723
  const pulseScale = progressActive ? 1.03 : hovered ? 1.015 : 1;
4358
4724
  const pulseShadow = progressActive ? `0 0 0 4px ${progressToneColor(indicator.tone)}22, 0 12px 28px rgba(15, 23, 42, 0.22)` : successFlash ? `0 0 0 5px rgba(55, 214, 122, 0.22), 0 14px 32px rgba(55, 214, 122, 0.22)` : launcherMode === "inline" ? hovered ? "var(--le-space-sponsor-relay-launcher-hover-shadow, 0 14px 30px rgba(79, 70, 229, 0.28))" : "var(--le-space-sponsor-relay-launcher-shadow, 0 10px 24px rgba(99, 102, 241, 0.22))" : void 0;
4359
4725
  const inlineButtonBackground = themedLauncherBackground;
@@ -4374,6 +4740,7 @@ function SponsorRelayFab(props) {
4374
4740
  };
4375
4741
  const launcherLabel = launcherMode === "inline" && compactInlineLabel ? "Relay" : "Sponsor Relay";
4376
4742
  return /* @__PURE__ */ jsxs(Fragment, { children: [
4743
+ /* @__PURE__ */ jsx("style", { children: `@keyframes leSpaceRelayPulse { 0%, 100% { transform: scale(1); opacity: 0.74; } 50% { transform: scale(1.22); opacity: 1; } }` }),
4377
4744
  /* @__PURE__ */ jsxs(
4378
4745
  "button",
4379
4746
  {
@@ -4468,7 +4835,8 @@ function SponsorRelayFab(props) {
4468
4835
  background: progressToneColor(indicator.tone),
4469
4836
  boxShadow: `0 0 0 ${progressActive ? "4px" : "3px"} ${progressToneColor(indicator.tone)}22`,
4470
4837
  transform: `scale(${progressActive ? 1.12 : successFlash ? 1.18 : 1})`,
4471
- transition: "transform 180ms ease, box-shadow 220ms ease"
4838
+ transition: "transform 180ms ease, box-shadow 220ms ease",
4839
+ animation: pollingState ? "leSpaceRelayPulse 1.6s ease-in-out infinite" : void 0
4472
4840
  }
4473
4841
  }
4474
4842
  ),
@@ -4550,6 +4918,52 @@ function SponsorRelayFab(props) {
4550
4918
  ),
4551
4919
  /* @__PURE__ */ jsx("p", { style: { color: "#9fb2ca" }, children: state.statusText }),
4552
4920
  state.errorText ? /* @__PURE__ */ jsx("p", { style: { color: "#ffd9d9" }, children: state.errorText }) : null,
4921
+ pollingState ? /* @__PURE__ */ jsxs(
4922
+ "div",
4923
+ {
4924
+ "aria-live": "polite",
4925
+ style: {
4926
+ marginTop: "0.25rem",
4927
+ marginBottom: "0.8rem",
4928
+ padding: "0.7rem 0.8rem",
4929
+ borderRadius: "1rem",
4930
+ border: "1px solid rgba(125, 211, 252, 0.18)",
4931
+ background: "linear-gradient(180deg, rgba(59, 130, 246, 0.12), rgba(59, 130, 246, 0.05))",
4932
+ display: "grid",
4933
+ gap: "0.28rem"
4934
+ },
4935
+ children: [
4936
+ /* @__PURE__ */ jsxs(
4937
+ "div",
4938
+ {
4939
+ style: {
4940
+ display: "inline-flex",
4941
+ alignItems: "center",
4942
+ gap: "0.45rem"
4943
+ },
4944
+ children: [
4945
+ /* @__PURE__ */ jsx(
4946
+ "span",
4947
+ {
4948
+ "aria-hidden": "true",
4949
+ style: {
4950
+ width: "0.62rem",
4951
+ height: "0.62rem",
4952
+ borderRadius: "999px",
4953
+ background: progressToneColor("info"),
4954
+ boxShadow: `0 0 0 3px ${progressToneColor("info")}22`,
4955
+ animation: "leSpaceRelayPulse 1.6s ease-in-out infinite"
4956
+ }
4957
+ }
4958
+ ),
4959
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: "0.88rem" }, children: pollingState.label })
4960
+ ]
4961
+ }
4962
+ ),
4963
+ /* @__PURE__ */ jsx("small", { style: { color: "#9fb2ca", lineHeight: 1.35 }, children: pollingState.detail })
4964
+ ]
4965
+ }
4966
+ ) : null,
4553
4967
  deploymentProgressVisible ? /* @__PURE__ */ jsxs(
4554
4968
  "div",
4555
4969
  {
package/shared/index.d.ts CHANGED
@@ -304,7 +304,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
304
304
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
305
305
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
306
306
 
307
- declare const UI_PACKAGE_VERSION = "0.3.21";
307
+ declare const UI_PACKAGE_VERSION = "0.3.23";
308
308
 
309
309
  interface RootfsRequiredPortForward {
310
310
  port: number;
package/shared/index.js CHANGED
@@ -38,7 +38,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
38
38
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
39
39
 
40
40
  // src/shared/package-version.ts
41
- var UI_PACKAGE_VERSION = "0.3.21";
41
+ var UI_PACKAGE_VERSION = "0.3.23";
42
42
 
43
43
  // src/shared/format.ts
44
44
  function shortHash(value, head = 8, tail = 6) {
@@ -2151,6 +2151,31 @@ var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
2151
2151
  var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
2152
2152
  var DEFAULT_BOOTSTRAP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2153
2153
  var DEFAULT_BOOTSTRAP_PAGINATION = 50;
2154
+ var RELAY_BOOTSTRAP_SIGNATURE_SCHEME = "personal_sign";
2155
+ function serializeOwnerAuthorizationPayload(payload) {
2156
+ return JSON.stringify({
2157
+ ownerAddress: payload.ownerAddress,
2158
+ publisherAddress: payload.publisherAddress,
2159
+ peerId: payload.peerId,
2160
+ registrationId: payload.registrationId,
2161
+ profile: payload.profile,
2162
+ version: payload.version,
2163
+ instanceItemHash: payload.instanceItemHash,
2164
+ issuedAt: payload.issuedAt,
2165
+ expiresAt: payload.expiresAt
2166
+ });
2167
+ }
2168
+ function serializeRelayProofPayload(payload) {
2169
+ return JSON.stringify({
2170
+ peerId: payload.peerId,
2171
+ multiaddrs: dedupeMultiaddrs(payload.multiaddrs),
2172
+ browserMultiaddrs: payload.browserMultiaddrs ? dedupeMultiaddrs(payload.browserMultiaddrs) : void 0,
2173
+ registrationId: payload.registrationId,
2174
+ profile: payload.profile,
2175
+ version: payload.version,
2176
+ updatedAt: payload.updatedAt
2177
+ });
2178
+ }
2154
2179
  function asTrimmedString(value) {
2155
2180
  return typeof value === "string" && value.trim() ? value.trim() : null;
2156
2181
  }
@@ -2162,9 +2187,48 @@ function asNumber4(value) {
2162
2187
  }
2163
2188
  return null;
2164
2189
  }
2190
+ function normalizeHost(host) {
2191
+ return host.trim().toLowerCase();
2192
+ }
2165
2193
  function normalizeAddress(address) {
2166
2194
  return address.trim().toLowerCase();
2167
2195
  }
2196
+ function normalizeSignature(signature) {
2197
+ const trimmed = signature.trim();
2198
+ return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
2199
+ }
2200
+ function splitMultiaddr(addr) {
2201
+ return addr.split("/").filter(Boolean);
2202
+ }
2203
+ function isPrivateIpv4(ip) {
2204
+ const parts = ip.split(".").map((part) => Number(part));
2205
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
2206
+ return false;
2207
+ }
2208
+ if (parts[0] === 10) return true;
2209
+ if (parts[0] === 127) return true;
2210
+ if (parts[0] === 0) return true;
2211
+ if (parts[0] === 169 && parts[1] === 254) return true;
2212
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
2213
+ if (parts[0] === 192 && parts[1] === 168) return true;
2214
+ if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true;
2215
+ return false;
2216
+ }
2217
+ function isPrivateIpv6(ip) {
2218
+ const normalized = ip.trim().toLowerCase();
2219
+ return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
2220
+ }
2221
+ function isLocalHostname(host) {
2222
+ const normalized = normalizeHost(host);
2223
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local");
2224
+ }
2225
+ function hasPeerId(addr) {
2226
+ return splitMultiaddr(addr).includes("p2p");
2227
+ }
2228
+ function isBrowserDialableMultiaddr(addr) {
2229
+ const normalized = addr.toLowerCase();
2230
+ return normalized.includes("/ws") || normalized.includes("/wss") || normalized.includes("/webtransport") || normalized.includes("/webrtc-direct");
2231
+ }
2168
2232
  function dedupeMultiaddrs(addrs) {
2169
2233
  const seen = /* @__PURE__ */ new Set();
2170
2234
  const result = [];
@@ -2176,6 +2240,33 @@ function dedupeMultiaddrs(addrs) {
2176
2240
  }
2177
2241
  return result;
2178
2242
  }
2243
+ function isPublicMultiaddr(addr) {
2244
+ const parts = splitMultiaddr(addr);
2245
+ for (let index = 0; index < parts.length; index += 1) {
2246
+ const protocol = parts[index];
2247
+ const value = parts[index + 1];
2248
+ if (protocol === "ip4" && value) {
2249
+ return !isPrivateIpv4(value);
2250
+ }
2251
+ if (protocol === "ip6" && value) {
2252
+ return !isPrivateIpv6(value);
2253
+ }
2254
+ if ((protocol === "dns4" || protocol === "dns6" || protocol === "dnsaddr") && value) {
2255
+ return !isLocalHostname(value);
2256
+ }
2257
+ }
2258
+ return false;
2259
+ }
2260
+ function filterPublicMultiaddrs(addrs, options = {}) {
2261
+ return dedupeMultiaddrs(addrs).filter((addr) => {
2262
+ if (!isPublicMultiaddr(addr)) return false;
2263
+ if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
2264
+ if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr)) {
2265
+ return false;
2266
+ }
2267
+ return true;
2268
+ });
2269
+ }
2179
2270
  function normalizeRelayBootstrapContent(value) {
2180
2271
  if (!value || typeof value !== "object") return null;
2181
2272
  const content = value;
@@ -2270,6 +2361,104 @@ function normalizeRelayBootstrapPostRecord(value) {
2270
2361
  content: normalizeRelayBootstrapContent(entry.content)
2271
2362
  };
2272
2363
  }
2364
+ function buildRelayBootstrapPostContent(args) {
2365
+ const now = args.now ?? Date.now() / 1e3;
2366
+ const updatedAt = args.now ?? Date.now();
2367
+ return {
2368
+ type: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2369
+ address: args.sender,
2370
+ ...args.ref ? { ref: args.ref } : {},
2371
+ content: {
2372
+ peerId: args.peerId,
2373
+ multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
2374
+ browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2375
+ browserDialableOnly: true
2376
+ }) : void 0,
2377
+ registrationId: args.registrationId,
2378
+ profile: args.profile,
2379
+ version: args.version,
2380
+ ownerAddress: args.ownerAddress,
2381
+ publisherAddress: args.publisherAddress,
2382
+ authorization: args.authorization,
2383
+ relayProof: args.relayProof,
2384
+ updatedAt: Math.round(updatedAt)
2385
+ },
2386
+ time: now
2387
+ };
2388
+ }
2389
+ async function createRelayBootstrapPost(args) {
2390
+ const nowMillis = args.now ?? Date.now();
2391
+ const nowSeconds = nowMillis / 1e3;
2392
+ const itemContent = buildRelayBootstrapPostContent({
2393
+ sender: args.sender,
2394
+ peerId: args.peerId,
2395
+ multiaddrs: args.multiaddrs,
2396
+ browserMultiaddrs: args.browserMultiaddrs,
2397
+ registrationId: args.registrationId,
2398
+ ref: args.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF,
2399
+ postType: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2400
+ profile: args.profile,
2401
+ version: args.version,
2402
+ ownerAddress: args.ownerAddress,
2403
+ publisherAddress: args.publisherAddress,
2404
+ authorization: args.authorization,
2405
+ relayProof: args.relayProof,
2406
+ now: nowMillis
2407
+ });
2408
+ itemContent.time = nowSeconds;
2409
+ const serialized = JSON.stringify(itemContent);
2410
+ const itemHash = await args.hasher(serialized);
2411
+ return {
2412
+ channel: args.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL,
2413
+ sender: args.sender,
2414
+ chain: "ETH",
2415
+ type: "POST",
2416
+ time: nowSeconds,
2417
+ item_type: "inline",
2418
+ item_content: serialized,
2419
+ item_hash: itemHash
2420
+ };
2421
+ }
2422
+ async function signRelayBootstrapAuthorization(args) {
2423
+ const payload = {
2424
+ ownerAddress: args.ownerAddress,
2425
+ publisherAddress: args.publisherAddress,
2426
+ peerId: args.peerId,
2427
+ registrationId: args.registrationId,
2428
+ profile: args.profile,
2429
+ version: args.version,
2430
+ instanceItemHash: args.instanceItemHash,
2431
+ issuedAt: args.issuedAt ?? Date.now(),
2432
+ expiresAt: args.expiresAt
2433
+ };
2434
+ const serialized = serializeOwnerAuthorizationPayload(payload);
2435
+ const signature = await args.signer(args.ownerAddress, serialized);
2436
+ return {
2437
+ scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
2438
+ payload,
2439
+ signature: normalizeSignature(signature)
2440
+ };
2441
+ }
2442
+ async function signRelayBootstrapProof(args) {
2443
+ const payload = {
2444
+ peerId: args.peerId,
2445
+ multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
2446
+ browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2447
+ browserDialableOnly: true
2448
+ }) : void 0,
2449
+ registrationId: args.registrationId,
2450
+ profile: args.profile,
2451
+ version: args.version,
2452
+ updatedAt: args.updatedAt ?? Date.now()
2453
+ };
2454
+ const serialized = serializeRelayProofPayload(payload);
2455
+ const signature = await args.signer(args.publisherAddress, serialized);
2456
+ return {
2457
+ scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
2458
+ payload,
2459
+ signature: normalizeSignature(signature)
2460
+ };
2461
+ }
2273
2462
  async function fetchAlephBootstrapPosts(options = {}) {
2274
2463
  const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
2275
2464
  if (typeof fetchImpl !== "function") {
@@ -2388,6 +2577,109 @@ async function waitForRelayBootstrapRegistration(args) {
2388
2577
  }
2389
2578
  return null;
2390
2579
  }
2580
+ async function publishRelayBootstrapRegistration(args) {
2581
+ const publisherAddress = args.publisherAddress ?? args.sender;
2582
+ const ownerAddress = args.ownerAddress;
2583
+ let ownerAuthorization = args.ownerAuthorization;
2584
+ let relayProof = args.relayProof;
2585
+ if (!ownerAuthorization && ownerAddress && args.ownerSigner) {
2586
+ ownerAuthorization = await signRelayBootstrapAuthorization({
2587
+ ownerAddress,
2588
+ publisherAddress,
2589
+ peerId: args.peerId,
2590
+ registrationId: args.registrationId,
2591
+ profile: args.profile,
2592
+ version: args.version,
2593
+ instanceItemHash: args.instanceItemHash,
2594
+ issuedAt: args.authorizationIssuedAt ?? args.now ?? Date.now(),
2595
+ expiresAt: args.authorizationExpiresAt,
2596
+ signer: args.ownerSigner
2597
+ });
2598
+ }
2599
+ if (!relayProof && args.publisherSigner) {
2600
+ relayProof = await signRelayBootstrapProof({
2601
+ publisherAddress,
2602
+ peerId: args.peerId,
2603
+ multiaddrs: args.multiaddrs,
2604
+ browserMultiaddrs: args.browserMultiaddrs,
2605
+ registrationId: args.registrationId,
2606
+ profile: args.profile,
2607
+ version: args.version,
2608
+ updatedAt: args.now ?? Date.now(),
2609
+ signer: args.publisherSigner
2610
+ });
2611
+ }
2612
+ const unsigned = await createRelayBootstrapPost({
2613
+ sender: args.sender,
2614
+ peerId: args.peerId,
2615
+ multiaddrs: args.multiaddrs,
2616
+ browserMultiaddrs: args.browserMultiaddrs,
2617
+ registrationId: args.registrationId,
2618
+ channel: args.channel,
2619
+ ref: args.ref,
2620
+ postType: args.postType,
2621
+ profile: args.profile,
2622
+ version: args.version,
2623
+ ownerAddress,
2624
+ publisherAddress,
2625
+ authorization: ownerAuthorization,
2626
+ relayProof,
2627
+ now: args.now,
2628
+ hasher: args.hasher
2629
+ });
2630
+ const content = JSON.parse(unsigned.item_content);
2631
+ if ((content.content?.multiaddrs?.length ?? 0) === 0) {
2632
+ return {
2633
+ status: "skipped",
2634
+ reason: "No public relay multiaddrs remained after filtering.",
2635
+ publishedMultiaddrs: [],
2636
+ publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? []
2637
+ };
2638
+ }
2639
+ const message = await signAlephMessage(unsigned, args.signer);
2640
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
2641
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2642
+ sync: args.sync ?? true,
2643
+ fetch: args.fetch
2644
+ });
2645
+ let forgottenHashes = [];
2646
+ let forgetResult;
2647
+ if (args.forgetPrevious && args.registrationId) {
2648
+ const previousPosts = await fetchAlephBootstrapPosts({
2649
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2650
+ channel: args.channel,
2651
+ ref: args.ref,
2652
+ postType: args.postType,
2653
+ fetch: args.fetch
2654
+ });
2655
+ forgottenHashes = previousPosts.filter(
2656
+ (post) => post.address?.toLowerCase() === args.sender.toLowerCase() && post.content?.registrationId === args.registrationId && (post.itemHash ?? post.hash) != null && (post.itemHash ?? post.hash) !== unsigned.item_hash
2657
+ ).map((post) => post.itemHash ?? post.hash ?? "").filter(Boolean);
2658
+ if (forgottenHashes.length > 0) {
2659
+ forgetResult = await forgetAlephMessages({
2660
+ sender: args.sender,
2661
+ hashes: forgottenHashes,
2662
+ reason: args.forgetPreviousReason ?? `Replace older relay bootstrap records for ${args.registrationId}`,
2663
+ signer: args.signer,
2664
+ hasher: args.hasher,
2665
+ fetch: args.fetch,
2666
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2667
+ sync: args.sync ?? true
2668
+ });
2669
+ }
2670
+ }
2671
+ return {
2672
+ status: "published",
2673
+ itemHash: unsigned.item_hash,
2674
+ message,
2675
+ response,
2676
+ httpStatus,
2677
+ publishedMultiaddrs: content.content?.multiaddrs ?? [],
2678
+ publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? [],
2679
+ forgottenHashes,
2680
+ forgetResult
2681
+ };
2682
+ }
2391
2683
 
2392
2684
  // ../core/src/bootstrap-config.ts
2393
2685
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
@@ -3215,9 +3507,61 @@ var SponsorRelayController = class {
3215
3507
  visibleRegistration
3216
3508
  });
3217
3509
  if (!visibleRegistration) {
3218
- throw new Error(
3219
- "Relay bootstrap registration did not become visible on Aleph."
3220
- );
3510
+ this.trace("deploy:bootstrap-registration-fallback-start", {
3511
+ itemHash: args.itemHash,
3512
+ registrationId,
3513
+ peerId,
3514
+ probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3515
+ browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3516
+ });
3517
+ this.emitProgress({
3518
+ stage: "publishing-bootstrap",
3519
+ label: "Publishing bootstrap fallback",
3520
+ progress: 98,
3521
+ status: "warning",
3522
+ itemHash: args.itemHash,
3523
+ detail: "Guest bootstrap registration was delayed. Publishing the relay bootstrap record from the browser."
3524
+ });
3525
+ const fallbackPublication = await publishRelayBootstrapRegistration({
3526
+ sender: this.state.wallet.address,
3527
+ signer: personalSign,
3528
+ hasher: sha256Hex,
3529
+ fetch: asJsonFetch,
3530
+ apiHost: this.client.apiHost,
3531
+ peerId,
3532
+ multiaddrs: relayMetadata.probeMultiaddrs,
3533
+ browserMultiaddrs: relayMetadata.browserBootstrapMultiaddrs,
3534
+ registrationId,
3535
+ profile,
3536
+ sync: true,
3537
+ forgetPrevious: true
3538
+ });
3539
+ this.trace("deploy:bootstrap-registration-fallback-published", {
3540
+ itemHash: args.itemHash,
3541
+ registrationId,
3542
+ peerId,
3543
+ fallbackPublication
3544
+ });
3545
+ const fallbackVisibleRegistration = await waitForRelayBootstrapRegistration({
3546
+ sender: this.state.wallet.address,
3547
+ registrationId,
3548
+ peerId,
3549
+ fetch: asJsonFetch,
3550
+ apiHost: this.client.apiHost,
3551
+ attempts: 24,
3552
+ delayMs: 2500
3553
+ });
3554
+ this.trace("deploy:bootstrap-registration-fallback-visible", {
3555
+ itemHash: args.itemHash,
3556
+ registrationId,
3557
+ peerId,
3558
+ fallbackVisibleRegistration
3559
+ });
3560
+ if (!fallbackVisibleRegistration) {
3561
+ throw new Error(
3562
+ "Relay bootstrap registration did not become visible on Aleph."
3563
+ );
3564
+ }
3221
3565
  }
3222
3566
  if (profile === "uc-go-peer" && args.deploymentToken) {
3223
3567
  await deleteVmBootstrapConfig({
@@ -42,6 +42,15 @@ export let apiHost = undefined
42
42
  const versionLabel = resolvedVersion.trim()
43
43
  ? (resolvedVersion.trim().startsWith('v') ? resolvedVersion.trim() : `v${resolvedVersion.trim()}`)
44
44
  : ''
45
+ const pollingStages = new Set(['waiting-for-aleph', 'deployment-confirmed', 'publishing-bootstrap', 'refreshing-instances'])
46
+
47
+ $: pollingActive = state.busy.refreshing || pollingStages.has(state.deploymentProgress.stage)
48
+ $: pollingLabel = state.busy.refreshing
49
+ ? 'Checking relay state'
50
+ : (state.deploymentProgress.label || 'Polling relay state')
51
+ $: pollingDetail = state.busy.refreshing
52
+ ? (state.statusText || 'Refreshing relay sponsor data from Aleph and the selected CRN.')
53
+ : (state.deploymentProgress.detail || state.statusText || 'Waiting for the next confirmed relay state from Aleph.')
45
54
 
46
55
  onMount(async () => {
47
56
  const unsubscribe = controller.subscribe((next) => {
@@ -113,6 +122,16 @@ export let apiHost = undefined
113
122
  {/if}
114
123
  <p class="status-text">{state.statusText}</p>
115
124
 
125
+ {#if pollingActive}
126
+ <div class="polling-row" aria-live="polite">
127
+ <div class="polling-head">
128
+ <StatusLed tone="idle" pulse={true} />
129
+ <strong>{pollingLabel}</strong>
130
+ </div>
131
+ <small>{pollingDetail}</small>
132
+ </div>
133
+ {/if}
134
+
116
135
  <div class="grid">
117
136
  <label class="field">
118
137
  <span>Instance Name</span>
@@ -319,6 +338,31 @@ export let apiHost = undefined
319
338
  justify-content: space-between;
320
339
  }
321
340
 
341
+ .polling-row {
342
+ display: grid;
343
+ gap: 0.28rem;
344
+ margin: 0.25rem 0 0.8rem;
345
+ padding: 0.7rem 0.85rem;
346
+ border-radius: 1rem;
347
+ background: linear-gradient(180deg, rgba(59, 130, 246, 0.12), rgba(59, 130, 246, 0.05));
348
+ border: 1px solid rgba(125, 211, 252, 0.18);
349
+ }
350
+
351
+ .polling-head {
352
+ display: inline-flex;
353
+ align-items: center;
354
+ gap: 0.45rem;
355
+ }
356
+
357
+ .polling-row strong {
358
+ font-size: 0.88rem;
359
+ }
360
+
361
+ .polling-row small {
362
+ color: var(--relay-text-dim);
363
+ line-height: 1.35;
364
+ }
365
+
322
366
  .eyebrow,
323
367
  .mini-label,
324
368
  .field span,