@le-space/ui 0.3.11 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/shared/index.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.11";
41
+ var UI_PACKAGE_VERSION = "0.3.13";
42
42
 
43
43
  // src/shared/format.ts
44
44
  function shortHash(value, head = 8, tail = 6) {
@@ -1470,6 +1470,24 @@ function normalizeSshPublicKey(value) {
1470
1470
  function isValidSshPublicKey(value) {
1471
1471
  return SSH_PUBLIC_KEY_PATTERN.test(normalizeSshPublicKey(value));
1472
1472
  }
1473
+ function createDeploymentToken() {
1474
+ const randomUuid = globalThis.crypto?.randomUUID?.();
1475
+ if (typeof randomUuid === "string" && randomUuid.trim()) {
1476
+ return randomUuid;
1477
+ }
1478
+ return `deploy-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
1479
+ }
1480
+ function appendDeploymentTokenToSshPublicKey(sshPublicKey, ownerAddress, deploymentToken) {
1481
+ const normalized = normalizeSshPublicKey(sshPublicKey);
1482
+ if (!normalized) return normalized;
1483
+ const [algorithm = "", key = ""] = normalized.split(/\s+/, 3);
1484
+ const owner = ownerAddress.trim().toLowerCase();
1485
+ const token = deploymentToken.trim();
1486
+ if (!algorithm || !key || !owner || !token) {
1487
+ return normalized;
1488
+ }
1489
+ return `${algorithm} ${key} aleph-bootstrap-config:${owner}:${token}`;
1490
+ }
1473
1491
  function createReleaseMetadata(name, rootfsVersion, deployer = "shared-aleph-tooling") {
1474
1492
  return {
1475
1493
  name,
@@ -2008,51 +2026,6 @@ async function waitForSetupEndpoint(args) {
2008
2026
  }
2009
2027
  return result;
2010
2028
  }
2011
- async function configureUcGoPeer(args) {
2012
- const controller = new AbortController();
2013
- const timeout = setTimeout(
2014
- () => controller.abort(),
2015
- Number(args.timeoutMs ?? 18e4)
2016
- );
2017
- const payload = {
2018
- public_ipv4: args.hostIpv4,
2019
- public_ipv6: args.publicIpv6 ?? void 0,
2020
- proxy_url: args.proxyUrl ?? void 0,
2021
- tcp_port: args.tcpPort ?? void 0,
2022
- ws_port: args.wsPort ?? void 0,
2023
- udp_port: args.udpPort ?? void 0,
2024
- quic_port: args.quicPort ?? void 0,
2025
- webrtc_port: args.webrtcPort ?? void 0,
2026
- bootstrap_publisher_private_key: args.bootstrapPublisherPrivateKey ?? void 0,
2027
- bootstrap_publisher_libp2p_identity_b64: args.bootstrapPublisherLibp2pIdentityBase64 ?? void 0,
2028
- bootstrap_owner_private_key: args.bootstrapOwnerPrivateKey ?? void 0,
2029
- bootstrap_owner_authorization_b64: args.bootstrapOwnerAuthorizationBase64 ?? void 0,
2030
- bootstrap_registration_id: args.bootstrapRegistrationId ?? void 0,
2031
- no_start: args.noStart === true ? true : void 0
2032
- };
2033
- try {
2034
- const response = await args.fetch(
2035
- `http://${args.hostIpv4}:${args.setupPort}/configure`,
2036
- {
2037
- method: "POST",
2038
- headers: {
2039
- "content-type": "application/json"
2040
- },
2041
- body: JSON.stringify(payload),
2042
- signal: controller.signal
2043
- }
2044
- );
2045
- const responsePayload = await response.json().catch(() => null);
2046
- if (!response.ok) {
2047
- throw new Error(
2048
- `Relay setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
2049
- );
2050
- }
2051
- return responsePayload;
2052
- } finally {
2053
- clearTimeout(timeout);
2054
- }
2055
- }
2056
2029
  async function configureOrbitdbRelaySetup2(args) {
2057
2030
  const controller = new AbortController();
2058
2031
  const timeout = setTimeout(
@@ -2110,24 +2083,30 @@ async function fetchUcGoPeerMetadata(args) {
2110
2083
  () => controller.abort(),
2111
2084
  Number(args.timeoutMs ?? 18e4)
2112
2085
  );
2086
+ const requestUrl = args.metadataUrl && args.metadataUrl.trim() ? args.metadataUrl : `http://${args.hostIpv4}:${args.setupPort}/metadata`;
2113
2087
  try {
2114
2088
  const response = await args.fetch(
2115
- `http://${args.hostIpv4}:${args.setupPort}/metadata`,
2089
+ requestUrl,
2116
2090
  {
2117
2091
  signal: controller.signal
2118
2092
  }
2119
2093
  );
2120
2094
  const payload = await response.json().catch(() => null);
2121
2095
  lastPayload = payload;
2122
- args.onAttempt?.(
2123
- payload,
2124
- Boolean(
2125
- response.ok && payload && typeof payload === "object" && payload.status === "ready"
2126
- ),
2127
- attempt + 1,
2128
- attempts
2096
+ const ready = Boolean(
2097
+ response.ok && payload && typeof payload === "object" && payload.status === "ready"
2129
2098
  );
2130
- if (response.ok && payload && typeof payload === "object" && payload.status === "ready") {
2099
+ args.onAttempt?.({
2100
+ payload,
2101
+ ready,
2102
+ attempt: attempt + 1,
2103
+ attempts,
2104
+ requestUrl,
2105
+ ok: response.ok,
2106
+ status: response.status,
2107
+ error: null
2108
+ });
2109
+ if (ready) {
2131
2110
  return payload;
2132
2111
  }
2133
2112
  if (response.status >= 500) {
@@ -2135,6 +2114,18 @@ async function fetchUcGoPeerMetadata(args) {
2135
2114
  `Relay metadata request failed: ${response.status} ${typeof payload === "string" ? payload : JSON.stringify(payload ?? {})}`
2136
2115
  );
2137
2116
  }
2117
+ } catch (error) {
2118
+ args.onAttempt?.({
2119
+ payload: null,
2120
+ ready: false,
2121
+ attempt: attempt + 1,
2122
+ attempts,
2123
+ requestUrl,
2124
+ ok: false,
2125
+ status: null,
2126
+ error: error instanceof Error ? error.message : String(error)
2127
+ });
2128
+ throw error;
2138
2129
  } finally {
2139
2130
  clearTimeout(timeout);
2140
2131
  }
@@ -2194,6 +2185,9 @@ function asNumber4(value) {
2194
2185
  function normalizeHost(host) {
2195
2186
  return host.trim().toLowerCase();
2196
2187
  }
2188
+ function normalizeAddress(address) {
2189
+ return address.trim().toLowerCase();
2190
+ }
2197
2191
  function normalizeSignature(signature) {
2198
2192
  const trimmed = signature.trim();
2199
2193
  return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
@@ -2495,6 +2489,40 @@ async function fetchAlephBootstrapPosts(options = {}) {
2495
2489
  const payload = await response.json();
2496
2490
  return (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null);
2497
2491
  }
2492
+ function compareRelayBootstrapPostRecency(left, right) {
2493
+ const leftUpdatedAt = left.content?.updatedAt ?? 0;
2494
+ const rightUpdatedAt = right.content?.updatedAt ?? 0;
2495
+ if (leftUpdatedAt !== rightUpdatedAt) return leftUpdatedAt - rightUpdatedAt;
2496
+ const leftTime = left.time ?? 0;
2497
+ const rightTime = right.time ?? 0;
2498
+ return leftTime - rightTime;
2499
+ }
2500
+ function relayBootstrapRecordIdentity(post) {
2501
+ const content = post.content;
2502
+ if (!content) return null;
2503
+ if (content.registrationId) return `registration:${content.registrationId}`;
2504
+ if (post.address) return `sender:${normalizeAddress(post.address)}`;
2505
+ return content.peerId ? `peer:${content.peerId}` : null;
2506
+ }
2507
+ function selectCurrentRelayBootstrapPosts(posts, options = {}) {
2508
+ const maxAgeMs = options.maxAgeMs ?? DEFAULT_BOOTSTRAP_MAX_AGE_MS;
2509
+ const now = options.now ?? Date.now();
2510
+ const selected = /* @__PURE__ */ new Map();
2511
+ for (const post of posts) {
2512
+ const content = post.content;
2513
+ if (!content) continue;
2514
+ if (now - content.updatedAt > maxAgeMs) continue;
2515
+ const identity = relayBootstrapRecordIdentity(post);
2516
+ if (!identity) continue;
2517
+ const previous = selected.get(identity);
2518
+ if (!previous || compareRelayBootstrapPostRecency(post, previous) > 0) {
2519
+ selected.set(identity, post);
2520
+ }
2521
+ }
2522
+ return [...selected.values()].sort(
2523
+ (left, right) => compareRelayBootstrapPostRecency(right, left)
2524
+ );
2525
+ }
2498
2526
 
2499
2527
  // ../core/src/bootstrap-registration.ts
2500
2528
  async function publishRelayBootstrapRegistration(args) {
@@ -2601,6 +2629,319 @@ async function publishRelayBootstrapRegistration(args) {
2601
2629
  };
2602
2630
  }
2603
2631
 
2632
+ // ../core/src/bootstrap-config.ts
2633
+ var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
2634
+ async function fetchVmBootstrapConfigAggregate(address, options) {
2635
+ const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2636
+ requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
2637
+ const response = await options.fetch(requestUrl.toString(), { cache: "no-cache" });
2638
+ if (response.status === 404) return {};
2639
+ if (!response.ok) {
2640
+ throw new Error(`VM bootstrap config aggregate request failed: ${response.status}`);
2641
+ }
2642
+ const payload = await response.json();
2643
+ const aggregate = payload.data?.[VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY];
2644
+ if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return {};
2645
+ return aggregate;
2646
+ }
2647
+ function normalizeVmBootstrapConfigAggregate(aggregate) {
2648
+ if (!aggregate || typeof aggregate !== "object") return {};
2649
+ return Object.fromEntries(
2650
+ Object.entries(aggregate).filter(
2651
+ (entry) => Boolean(entry[1] && typeof entry[1] === "object" && !Array.isArray(entry[1]))
2652
+ )
2653
+ );
2654
+ }
2655
+ function createVmBootstrapConfigAggregateContent(args) {
2656
+ const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate);
2657
+ content[args.record.deploymentToken] = args.record;
2658
+ return {
2659
+ address: args.sender,
2660
+ key: VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY,
2661
+ content,
2662
+ time: args.now ?? Date.now() / 1e3
2663
+ };
2664
+ }
2665
+ async function createUnsignedVmBootstrapConfigAggregateMessage(args) {
2666
+ const itemContent = JSON.stringify(args.content);
2667
+ const itemHash = await args.hasher(itemContent);
2668
+ return {
2669
+ sender: args.sender,
2670
+ chain: "ETH",
2671
+ type: "AGGREGATE",
2672
+ item_hash: itemHash,
2673
+ item_type: "inline",
2674
+ item_content: itemContent,
2675
+ time: args.now ?? Date.now() / 1e3,
2676
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
2677
+ };
2678
+ }
2679
+ async function publishVmBootstrapConfigAggregate(args) {
2680
+ const unsignedMessage = await createUnsignedVmBootstrapConfigAggregateMessage({
2681
+ sender: args.sender,
2682
+ content: args.content,
2683
+ hasher: args.hasher,
2684
+ channel: args.channel
2685
+ });
2686
+ const message = await signAlephMessage(unsignedMessage, args.signer);
2687
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
2688
+ apiHost: args.apiHost,
2689
+ sync: args.sync,
2690
+ fetch: args.fetch
2691
+ });
2692
+ const aggregateStatus = httpStatus >= 200 && httpStatus < 300 ? response.message_status ?? "processed" : "unknown";
2693
+ if (aggregateStatus === "rejected") {
2694
+ throw new Error(`VM bootstrap config aggregate was rejected by Aleph: ${JSON.stringify(response.details ?? response)}`);
2695
+ }
2696
+ return {
2697
+ aggregateItemHash: message.item_hash,
2698
+ aggregateStatus
2699
+ };
2700
+ }
2701
+ async function publishVmBootstrapConfig(args) {
2702
+ const aggregate = await fetchVmBootstrapConfigAggregate(args.sender, {
2703
+ apiHost: args.apiHost,
2704
+ fetch: args.fetch
2705
+ });
2706
+ const content = createVmBootstrapConfigAggregateContent({
2707
+ sender: args.sender,
2708
+ record: args.record,
2709
+ existingAggregate: aggregate
2710
+ });
2711
+ const result = await publishVmBootstrapConfigAggregate({
2712
+ sender: args.sender,
2713
+ content,
2714
+ signer: args.signer,
2715
+ hasher: args.hasher,
2716
+ fetch: args.fetch,
2717
+ channel: args.channel,
2718
+ apiHost: args.apiHost,
2719
+ sync: args.sync
2720
+ });
2721
+ return {
2722
+ ...result,
2723
+ aggregate: content.content
2724
+ };
2725
+ }
2726
+ async function deleteVmBootstrapConfig(args) {
2727
+ const aggregate = await fetchVmBootstrapConfigAggregate(args.sender, {
2728
+ apiHost: args.apiHost,
2729
+ fetch: args.fetch
2730
+ });
2731
+ const content = normalizeVmBootstrapConfigAggregate(aggregate);
2732
+ delete content[args.deploymentToken];
2733
+ const publishResult = await publishVmBootstrapConfigAggregate({
2734
+ sender: args.sender,
2735
+ content: {
2736
+ address: args.sender,
2737
+ key: VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY,
2738
+ content,
2739
+ time: Date.now() / 1e3
2740
+ },
2741
+ signer: args.signer,
2742
+ hasher: args.hasher,
2743
+ fetch: args.fetch,
2744
+ channel: args.channel,
2745
+ apiHost: args.apiHost,
2746
+ sync: args.sync
2747
+ });
2748
+ return {
2749
+ ...publishResult,
2750
+ aggregate: content
2751
+ };
2752
+ }
2753
+
2754
+ // ../core/src/bootstrap-reconcile.ts
2755
+ function createRelayBootstrapRegistrationId(profile, instanceName, itemHash) {
2756
+ const normalizedName = String(instanceName ?? "").trim();
2757
+ const normalizedHash = String(itemHash ?? "").trim();
2758
+ if (!normalizedHash) {
2759
+ return `relay:${profile}:${normalizedName || "instance"}`;
2760
+ }
2761
+ return `relay:${profile}:${normalizedName || normalizedHash}:${normalizedHash}`;
2762
+ }
2763
+ function secureBootstrapMetadataUrl(proxyUrl) {
2764
+ const normalized = String(proxyUrl ?? "").trim();
2765
+ if (!normalized) return null;
2766
+ try {
2767
+ return new URL("/bootstrap/metadata", normalized).toString();
2768
+ } catch {
2769
+ return null;
2770
+ }
2771
+ }
2772
+ function extractRelayMetadata(payload) {
2773
+ const metadata = payload && typeof payload === "object" && payload.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
2774
+ const peerId = typeof metadata?.peer_id === "string" ? metadata.peer_id : null;
2775
+ const probeMultiaddrs = Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.filter((entry) => typeof entry === "string") : [];
2776
+ const browserBootstrapMultiaddrs = Array.isArray(metadata?.browser_bootstrap_multiaddrs) ? metadata.browser_bootstrap_multiaddrs.filter((entry) => typeof entry === "string") : [];
2777
+ if (!peerId || probeMultiaddrs.length === 0) return null;
2778
+ return {
2779
+ peerId,
2780
+ probeMultiaddrs,
2781
+ browserBootstrapMultiaddrs
2782
+ };
2783
+ }
2784
+ async function fetchOwnerInstances(address, options) {
2785
+ const url = new URL("/api/v0/messages.json", options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2786
+ url.searchParams.set("msgTypes", "INSTANCE");
2787
+ url.searchParams.set("addresses", address);
2788
+ url.searchParams.set("message_statuses", "processed,pending,rejected,removing");
2789
+ url.searchParams.set("pagination", "100");
2790
+ url.searchParams.set("page", "1");
2791
+ url.searchParams.set("sortOrder", "-1");
2792
+ const response = await options.fetch(url.toString(), { cache: "no-cache" });
2793
+ if (!response.ok) {
2794
+ throw new Error(`Instance list request failed: ${response.status}`);
2795
+ }
2796
+ const payload = await response.json();
2797
+ return (payload.messages ?? []).map((message) => ({
2798
+ ...message,
2799
+ status: typeof message.status === "string" && message.status.trim() ? message.status : message.confirmed ? "processed" : "pending"
2800
+ }));
2801
+ }
2802
+ async function fetchRelayMetadataForRuntime(args) {
2803
+ const setupPort = args.runtime.mappedPorts?.["80"]?.host ?? 80;
2804
+ const metadataUrl = args.preferSecureMetadata ? secureBootstrapMetadataUrl(args.runtime.proxyUrl) : null;
2805
+ if (args.preferSecureMetadata && !metadataUrl) {
2806
+ return null;
2807
+ }
2808
+ const payload = await fetchUcGoPeerMetadata({
2809
+ hostIpv4: args.runtime.hostIpv4 ?? "",
2810
+ setupPort,
2811
+ metadataUrl,
2812
+ fetch: args.fetch,
2813
+ attempts: args.attempts,
2814
+ delayMs: args.delayMs,
2815
+ timeoutMs: args.timeoutMs,
2816
+ onAttempt: (result) => {
2817
+ args.onAttempt?.({
2818
+ ...result,
2819
+ metadataUrl,
2820
+ hostIpv4: args.runtime.hostIpv4 ?? null,
2821
+ setupPort
2822
+ });
2823
+ }
2824
+ }).catch(() => null);
2825
+ return extractRelayMetadata(payload);
2826
+ }
2827
+ async function reconcileOwnerRelayBootstrapRegistrations(args) {
2828
+ const result = {
2829
+ refreshedRegistrations: [],
2830
+ forgottenHashes: [],
2831
+ skippedInstanceHashes: [],
2832
+ errors: []
2833
+ };
2834
+ const liveRegistrationIds = /* @__PURE__ */ new Set();
2835
+ if (args.current?.registrationId) {
2836
+ liveRegistrationIds.add(args.current.registrationId);
2837
+ result.refreshedRegistrations.push(args.current.registrationId);
2838
+ }
2839
+ const instances = await fetchOwnerInstances(args.instanceOwnerAddress, {
2840
+ apiHost: args.apiHost,
2841
+ fetch: args.fetch
2842
+ });
2843
+ const processedInstances = instances.filter((instance) => {
2844
+ const status = String(instance.status ?? "").trim().toLowerCase();
2845
+ return status === "processed";
2846
+ });
2847
+ for (const instance of processedInstances) {
2848
+ if (instance.item_hash === args.current?.itemHash) continue;
2849
+ const registrationId = createRelayBootstrapRegistrationId(
2850
+ args.profile,
2851
+ instance.content?.metadata?.name,
2852
+ instance.item_hash
2853
+ );
2854
+ try {
2855
+ const runtime = await fetchVmRuntime({
2856
+ itemHash: instance.item_hash,
2857
+ fetch: args.fetch,
2858
+ crns: args.crns,
2859
+ crnListUrl: args.crnListUrl
2860
+ });
2861
+ if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
2862
+ result.skippedInstanceHashes.push(instance.item_hash);
2863
+ continue;
2864
+ }
2865
+ const relayMetadata = await fetchRelayMetadataForRuntime({
2866
+ runtime,
2867
+ fetch: args.fetch,
2868
+ preferSecureMetadata: args.preferSecureMetadata,
2869
+ attempts: 3,
2870
+ delayMs: 1e3,
2871
+ timeoutMs: 1e4
2872
+ });
2873
+ if (!relayMetadata) {
2874
+ result.skippedInstanceHashes.push(instance.item_hash);
2875
+ continue;
2876
+ }
2877
+ liveRegistrationIds.add(registrationId);
2878
+ await publishRelayBootstrapRegistration({
2879
+ sender: args.sender,
2880
+ signer: args.signer,
2881
+ hasher: args.hasher,
2882
+ fetch: args.fetch,
2883
+ apiHost: args.apiHost,
2884
+ channel: args.channel,
2885
+ ref: args.ref,
2886
+ postType: args.postType,
2887
+ peerId: relayMetadata.peerId,
2888
+ multiaddrs: relayMetadata.probeMultiaddrs,
2889
+ browserMultiaddrs: relayMetadata.browserBootstrapMultiaddrs,
2890
+ registrationId,
2891
+ ownerAddress: args.ownerAddress,
2892
+ publisherAddress: args.publisherAddress,
2893
+ ownerSigner: args.ownerSigner,
2894
+ publisherSigner: args.publisherSigner,
2895
+ forgetPrevious: true,
2896
+ profile: args.profile,
2897
+ version: args.version,
2898
+ instanceItemHash: instance.item_hash,
2899
+ sync: true
2900
+ });
2901
+ result.refreshedRegistrations.push(registrationId);
2902
+ } catch (error) {
2903
+ result.errors.push({
2904
+ phase: "publish",
2905
+ itemHash: instance.item_hash,
2906
+ registrationId,
2907
+ message: error instanceof Error ? error.message : String(error)
2908
+ });
2909
+ }
2910
+ }
2911
+ try {
2912
+ const bootstrapPosts = await fetchAlephBootstrapPosts({
2913
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST2,
2914
+ channel: args.channel,
2915
+ ref: args.ref,
2916
+ postType: args.postType,
2917
+ fetch: args.fetch
2918
+ });
2919
+ const currentPosts = selectCurrentRelayBootstrapPosts(bootstrapPosts);
2920
+ const staleHashes = currentPosts.filter(
2921
+ (post) => post.address?.toLowerCase() === args.sender.toLowerCase() && post.content?.profile === args.profile && post.content?.registrationId && !liveRegistrationIds.has(post.content.registrationId)
2922
+ ).map((post) => post.itemHash ?? post.hash ?? "").filter(Boolean);
2923
+ if (staleHashes.length > 0) {
2924
+ await forgetAlephMessages({
2925
+ sender: args.sender,
2926
+ hashes: staleHashes,
2927
+ reason: `Forget stale ${args.profile} relay bootstrap registrations after owner reconcile`,
2928
+ signer: args.signer,
2929
+ hasher: args.hasher,
2930
+ fetch: args.fetch,
2931
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST2,
2932
+ sync: true
2933
+ });
2934
+ result.forgottenHashes.push(...staleHashes);
2935
+ }
2936
+ } catch (error) {
2937
+ result.errors.push({
2938
+ phase: "forget",
2939
+ message: error instanceof Error ? error.message : String(error)
2940
+ });
2941
+ }
2942
+ return result;
2943
+ }
2944
+
2604
2945
  // src/shared/events.ts
2605
2946
  function createDeploymentProgressEmitter() {
2606
2947
  const listeners = /* @__PURE__ */ new Set();
@@ -2621,6 +2962,13 @@ function createDeploymentProgressEmitter() {
2621
2962
  }
2622
2963
 
2623
2964
  // src/shared/controller.ts
2965
+ function asJsonFetch(input, init) {
2966
+ return fetch(input, init).then(async (response) => ({
2967
+ ok: response.ok,
2968
+ status: response.status,
2969
+ json: async () => await response.json()
2970
+ }));
2971
+ }
2624
2972
  function isConfirmedRelaySetupResult(value) {
2625
2973
  return Boolean(
2626
2974
  value && typeof value === "object" && "status" in value && value.status === "configured"
@@ -3063,36 +3411,36 @@ var SponsorRelayController = class {
3063
3411
  if (!setupPort) {
3064
3412
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3065
3413
  }
3066
- this.emitProgress({
3067
- stage: "deployment-confirmed",
3068
- label: "Configuring relay",
3069
- progress: 93,
3070
- status: "info",
3071
- itemHash: args.itemHash,
3072
- detail: "Waiting for the guest setup endpoint before collecting relay metadata."
3073
- });
3074
- const setupHealth = await waitForSetupEndpoint({
3075
- hostIpv4: args.runtime.hostIpv4,
3076
- setupPort,
3077
- fetch: (url, init) => fetch(url, init),
3078
- attempts: 15,
3079
- delayMs: 4e3,
3080
- httpTimeoutMs: 1e4
3081
- });
3082
- if (!setupHealth.ok) {
3083
- throw new Error(
3084
- `Relay setup endpoint did not become reachable at http://${args.runtime.hostIpv4}:${setupPort}/health.`
3085
- );
3086
- }
3087
- this.emitProgress({
3088
- stage: "deployment-confirmed",
3089
- label: "Applying relay networking",
3090
- progress: 94,
3091
- status: "info",
3092
- itemHash: args.itemHash,
3093
- detail: "Publishing the host port mapping into the guest relay configuration."
3094
- });
3095
3414
  if (profile === "orbitdb-relay-pinner") {
3415
+ this.emitProgress({
3416
+ stage: "deployment-confirmed",
3417
+ label: "Configuring relay",
3418
+ progress: 93,
3419
+ status: "info",
3420
+ itemHash: args.itemHash,
3421
+ detail: "Waiting for the guest setup endpoint before collecting relay metadata."
3422
+ });
3423
+ const setupHealth = await waitForSetupEndpoint({
3424
+ hostIpv4: args.runtime.hostIpv4,
3425
+ setupPort,
3426
+ fetch: (url, init) => fetch(url, init),
3427
+ attempts: 15,
3428
+ delayMs: 4e3,
3429
+ httpTimeoutMs: 1e4
3430
+ });
3431
+ if (!setupHealth.ok) {
3432
+ throw new Error(
3433
+ `Relay setup endpoint did not become reachable at http://${args.runtime.hostIpv4}:${setupPort}/health.`
3434
+ );
3435
+ }
3436
+ this.emitProgress({
3437
+ stage: "deployment-confirmed",
3438
+ label: "Applying relay networking",
3439
+ progress: 94,
3440
+ status: "info",
3441
+ itemHash: args.itemHash,
3442
+ detail: "Publishing the host port mapping into the guest relay configuration."
3443
+ });
3096
3444
  const tcpPort = mappedPorts2["9091"]?.host ?? 0;
3097
3445
  const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
3098
3446
  if (!tcpPort || !wsPort) {
@@ -3118,24 +3466,57 @@ var SponsorRelayController = class {
3118
3466
  );
3119
3467
  }
3120
3468
  } else {
3121
- const configureResult = await configureUcGoPeer({
3122
- hostIpv4: args.runtime.hostIpv4,
3123
- publicIpv6: args.runtime.ipv6,
3124
- setupPort,
3125
- tcpPort: mappedPorts2["9095"]?.host ?? null,
3126
- wsPort: mappedPorts2["9097"]?.host ?? mappedPorts2["9095"]?.host ?? null,
3127
- udpPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
3128
- quicPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
3129
- webrtcPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
3130
- proxyUrl: args.runtime.proxyUrl ?? void 0,
3131
- fetch: (url, init) => fetch(url, init),
3132
- timeoutMs: 18e4
3133
- });
3134
- if (!isConfirmedRelaySetupResult(configureResult)) {
3135
- throw new Error(
3136
- `Relay guest configuration could not be confirmed at http://${args.runtime.hostIpv4}:${setupPort}/configure.`
3137
- );
3469
+ if (!args.deploymentToken) {
3470
+ throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3138
3471
  }
3472
+ this.emitProgress({
3473
+ stage: "deployment-confirmed",
3474
+ label: "Publishing relay config",
3475
+ progress: 94,
3476
+ status: "info",
3477
+ itemHash: args.itemHash,
3478
+ detail: "Publishing runtime networking into the Aleph guest bootstrap config aggregate."
3479
+ });
3480
+ const record = {
3481
+ deploymentToken: args.deploymentToken,
3482
+ profile,
3483
+ ownerAddress: this.state.wallet.address,
3484
+ instanceItemHash: args.itemHash,
3485
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3486
+ expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3487
+ status: "pending",
3488
+ runtime: {
3489
+ publicIpv4: args.runtime.hostIpv4,
3490
+ publicIpv6: args.runtime.ipv6 ?? null,
3491
+ proxyUrl: args.runtime.proxyUrl ?? null,
3492
+ mappedPorts: mappedPorts2
3493
+ },
3494
+ bootstrap: {
3495
+ registrationId: createRelayBootstrapRegistrationId(
3496
+ profile,
3497
+ this.state.instanceName.trim(),
3498
+ args.itemHash
3499
+ )
3500
+ }
3501
+ };
3502
+ const bootstrapConfigPublication = await publishVmBootstrapConfig({
3503
+ sender: this.state.wallet.address,
3504
+ record,
3505
+ signer: personalSign,
3506
+ hasher: sha256Hex,
3507
+ fetch: asJsonFetch,
3508
+ apiHost: this.client.apiHost,
3509
+ sync: true
3510
+ });
3511
+ this.trace("deploy:bootstrap-config-published", {
3512
+ itemHash: args.itemHash,
3513
+ deploymentToken: args.deploymentToken,
3514
+ registrationId: record.bootstrap?.registrationId ?? null,
3515
+ proxyUrl: record.runtime.proxyUrl ?? null,
3516
+ mappedPorts: record.runtime.mappedPorts,
3517
+ aggregateItemHash: bootstrapConfigPublication.aggregateItemHash,
3518
+ aggregateStatus: bootstrapConfigPublication.aggregateStatus
3519
+ });
3139
3520
  }
3140
3521
  this.emitProgress({
3141
3522
  stage: "publishing-bootstrap",
@@ -3145,27 +3526,46 @@ var SponsorRelayController = class {
3145
3526
  itemHash: args.itemHash,
3146
3527
  detail: "Waiting for the relay to report its final public multiaddrs."
3147
3528
  });
3148
- const metadataResult = await fetchUcGoPeerMetadata({
3149
- hostIpv4: args.runtime.hostIpv4,
3150
- setupPort,
3151
- fetch: (url, init) => fetch(url, init),
3529
+ const relayMetadata = await fetchRelayMetadataForRuntime({
3530
+ runtime: args.runtime,
3531
+ fetch,
3532
+ preferSecureMetadata: profile === "uc-go-peer",
3152
3533
  attempts: 80,
3153
3534
  delayMs: 3e3,
3154
- timeoutMs: 24e4
3535
+ timeoutMs: 24e4,
3536
+ onAttempt: (result) => {
3537
+ const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3538
+ const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3539
+ this.trace("deploy:relay-metadata-attempt", {
3540
+ itemHash: args.itemHash,
3541
+ attempt: result.attempt,
3542
+ attempts: result.attempts,
3543
+ requestUrl: result.requestUrl,
3544
+ metadataUrl: result.metadataUrl,
3545
+ hostIpv4: result.hostIpv4,
3546
+ setupPort: result.setupPort,
3547
+ ok: result.ok,
3548
+ status: result.status,
3549
+ ready: result.ready,
3550
+ error: result.error ?? null,
3551
+ payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3552
+ peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3553
+ probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3554
+ browserBootstrapMultiaddrCount: Array.isArray(
3555
+ metadata?.browser_bootstrap_multiaddrs
3556
+ ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3557
+ });
3558
+ }
3155
3559
  });
3156
- const metadata = metadataResult && typeof metadataResult === "object" && metadataResult.metadata && typeof metadataResult.metadata === "object" ? metadataResult.metadata : null;
3157
- const peerId = typeof metadata?.peer_id === "string" ? metadata.peer_id : null;
3158
- const probeMultiaddrs = Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.filter(
3159
- (entry) => typeof entry === "string"
3160
- ) : [];
3161
- const browserBootstrapMultiaddrs = Array.isArray(
3162
- metadata?.browser_bootstrap_multiaddrs
3163
- ) ? metadata.browser_bootstrap_multiaddrs.filter(
3164
- (entry) => typeof entry === "string"
3165
- ) : [];
3166
- if (!peerId || probeMultiaddrs.length === 0) {
3560
+ if (!relayMetadata) {
3167
3561
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3168
3562
  }
3563
+ const { peerId, probeMultiaddrs, browserBootstrapMultiaddrs } = relayMetadata;
3564
+ const registrationId = createRelayBootstrapRegistrationId(
3565
+ profile,
3566
+ this.state.instanceName.trim(),
3567
+ args.itemHash
3568
+ );
3169
3569
  this.emitProgress({
3170
3570
  stage: "publishing-bootstrap",
3171
3571
  label: "Publishing bootstrap registration",
@@ -3178,25 +3578,65 @@ var SponsorRelayController = class {
3178
3578
  sender: this.state.wallet.address,
3179
3579
  signer: personalSign,
3180
3580
  hasher: sha256Hex,
3181
- fetch: (url, init) => fetch(url, init).then(async (response) => ({
3182
- ok: response.ok,
3183
- status: response.status,
3184
- json: async () => await response.json()
3185
- })),
3581
+ fetch: asJsonFetch,
3186
3582
  apiHost: this.client.apiHost,
3187
3583
  peerId,
3188
3584
  multiaddrs: probeMultiaddrs,
3189
3585
  browserMultiaddrs: browserBootstrapMultiaddrs,
3586
+ registrationId,
3190
3587
  profile,
3191
3588
  version: this.state.manifest?.version,
3589
+ forgetPrevious: true,
3192
3590
  sync: true
3193
3591
  });
3194
3592
  this.trace("deploy:bootstrap-registration", {
3195
3593
  itemHash: args.itemHash,
3196
3594
  peerId,
3197
- metadata,
3595
+ metadata: relayMetadata,
3198
3596
  publication
3199
3597
  });
3598
+ const reconcileResult = await reconcileOwnerRelayBootstrapRegistrations({
3599
+ instanceOwnerAddress: this.state.wallet.address,
3600
+ sender: this.state.wallet.address,
3601
+ signer: personalSign,
3602
+ hasher: sha256Hex,
3603
+ fetch: asJsonFetch,
3604
+ profile,
3605
+ apiHost: this.client.apiHost,
3606
+ crns: this.state.crns,
3607
+ crnListUrl: this.props.crnListUrl,
3608
+ preferSecureMetadata: profile === "uc-go-peer",
3609
+ current: {
3610
+ itemHash: args.itemHash,
3611
+ registrationId,
3612
+ peerId,
3613
+ probeMultiaddrs,
3614
+ browserBootstrapMultiaddrs
3615
+ }
3616
+ });
3617
+ if (reconcileResult.errors.length > 0) {
3618
+ this.trace("deploy:owner-bootstrap-reconcile-errors", {
3619
+ itemHash: args.itemHash,
3620
+ errors: reconcileResult.errors
3621
+ });
3622
+ }
3623
+ if (profile === "uc-go-peer" && args.deploymentToken) {
3624
+ await deleteVmBootstrapConfig({
3625
+ sender: this.state.wallet.address,
3626
+ deploymentToken: args.deploymentToken,
3627
+ signer: personalSign,
3628
+ hasher: sha256Hex,
3629
+ fetch: asJsonFetch,
3630
+ apiHost: this.client.apiHost,
3631
+ sync: true
3632
+ }).catch((error) => {
3633
+ this.trace("deploy:bootstrap-config-cleanup-error", {
3634
+ itemHash: args.itemHash,
3635
+ deploymentToken: args.deploymentToken,
3636
+ error: error instanceof Error ? error.message : String(error)
3637
+ });
3638
+ });
3639
+ }
3200
3640
  }
3201
3641
  syncLatestDeploymentProgress(instances) {
3202
3642
  const itemHash = this.state.lastDeploymentHash;
@@ -3668,6 +4108,7 @@ var SponsorRelayController = class {
3668
4108
  let lastError = null;
3669
4109
  for (const [candidateIndex, candidateCrn] of orderedCrns.entries()) {
3670
4110
  let attemptItemHash = null;
4111
+ let attemptDeploymentToken = null;
3671
4112
  this.emitProgress({
3672
4113
  stage: "selecting-crn",
3673
4114
  label: "Selecting CRN",
@@ -3676,10 +4117,16 @@ var SponsorRelayController = class {
3676
4117
  detail: `Trying ${candidateCrn.name ?? candidateCrn.hash} (${candidateIndex + 1}/${orderedCrns.length})`
3677
4118
  });
3678
4119
  try {
4120
+ const profile = relayProfileForManifest(this.state.manifest);
4121
+ const sshPublicKey = profile === "uc-go-peer" ? appendDeploymentTokenToSshPublicKey(
4122
+ this.state.sshPublicKey.trim(),
4123
+ this.state.wallet.address,
4124
+ attemptDeploymentToken = createDeploymentToken()
4125
+ ) : this.state.sshPublicKey.trim();
3679
4126
  const content = createInstanceContent({
3680
4127
  address: this.state.wallet.address,
3681
4128
  name: this.state.instanceName.trim(),
3682
- sshPublicKey: this.state.sshPublicKey.trim(),
4129
+ sshPublicKey,
3683
4130
  rootfsItemHash: this.state.manifest.rootfsItemHash,
3684
4131
  rootfsSizeMiB: Math.max(
3685
4132
  this.state.manifest.rootfsSizeMiB,
@@ -3859,7 +4306,8 @@ var SponsorRelayController = class {
3859
4306
  });
3860
4307
  await this.configureRelayBootstrapRegistration({
3861
4308
  itemHash: result.itemHash,
3862
- runtime
4309
+ runtime,
4310
+ deploymentToken: attemptDeploymentToken
3863
4311
  });
3864
4312
  this.patch({
3865
4313
  busy: { deploying: false }
@@ -3885,6 +4333,39 @@ var SponsorRelayController = class {
3885
4333
  error: lastError.message
3886
4334
  });
3887
4335
  if (attemptItemHash) {
4336
+ if (attemptDeploymentToken) {
4337
+ this.trace("deploy:bootstrap-config-cleanup-start", {
4338
+ itemHash: attemptItemHash,
4339
+ deploymentToken: attemptDeploymentToken
4340
+ });
4341
+ await deleteVmBootstrapConfig({
4342
+ sender: this.state.wallet.address,
4343
+ deploymentToken: attemptDeploymentToken,
4344
+ signer: personalSign,
4345
+ hasher: sha256Hex,
4346
+ fetch: asJsonFetch,
4347
+ apiHost: this.client.apiHost,
4348
+ sync: true
4349
+ }).then((cleanupResult) => {
4350
+ this.trace("deploy:bootstrap-config-cleanup-complete", {
4351
+ itemHash: attemptItemHash,
4352
+ deploymentToken: attemptDeploymentToken,
4353
+ aggregateItemHash: cleanupResult.aggregateItemHash,
4354
+ aggregateStatus: cleanupResult.aggregateStatus
4355
+ });
4356
+ }).catch((cleanupError) => {
4357
+ this.trace("deploy:bootstrap-config-cleanup-error", {
4358
+ itemHash: attemptItemHash,
4359
+ deploymentToken: attemptDeploymentToken,
4360
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4361
+ });
4362
+ });
4363
+ }
4364
+ this.trace("deploy:forget-failed-attempt-start", {
4365
+ itemHash: attemptItemHash,
4366
+ crnHash: candidateCrn.hash,
4367
+ crnName: candidateCrn.name
4368
+ });
3888
4369
  await forgetAlephMessages({
3889
4370
  sender: this.state.wallet.address,
3890
4371
  hashes: [attemptItemHash],
@@ -3903,6 +4384,11 @@ var SponsorRelayController = class {
3903
4384
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
3904
4385
  });
3905
4386
  });
4387
+ this.trace("deploy:forget-failed-attempt-complete", {
4388
+ itemHash: attemptItemHash,
4389
+ crnHash: candidateCrn.hash,
4390
+ crnName: candidateCrn.name
4391
+ });
3906
4392
  }
3907
4393
  if (candidateIndex < orderedCrns.length - 1) {
3908
4394
  this.emitProgress({
@@ -3963,16 +4449,35 @@ var SponsorRelayController = class {
3963
4449
  reason: "Deleted from Sponsor Relay panel",
3964
4450
  signer: personalSign,
3965
4451
  hasher: sha256Hex,
3966
- fetch: (url, init) => fetch(url, init).then(async (response) => ({
3967
- ok: response.ok,
3968
- status: response.status,
3969
- json: async () => await response.json()
3970
- })),
4452
+ fetch: asJsonFetch,
3971
4453
  apiHost: this.client.apiHost,
3972
4454
  onProgress: (event) => {
3973
4455
  this.emitProgress(event);
3974
4456
  }
3975
4457
  });
4458
+ const profile = relayProfileForManifest(this.state.manifest);
4459
+ if (profile) {
4460
+ await reconcileOwnerRelayBootstrapRegistrations({
4461
+ instanceOwnerAddress: this.state.wallet.address,
4462
+ sender: this.state.wallet.address,
4463
+ signer: personalSign,
4464
+ hasher: sha256Hex,
4465
+ fetch: asJsonFetch,
4466
+ profile,
4467
+ apiHost: this.client.apiHost,
4468
+ crns: this.state.crns,
4469
+ crnListUrl: this.props.crnListUrl,
4470
+ preferSecureMetadata: profile === "uc-go-peer"
4471
+ }).catch(
4472
+ (error) => {
4473
+ this.trace("delete:owner-bootstrap-reconcile-error", {
4474
+ instanceHash,
4475
+ profile,
4476
+ error: error instanceof Error ? error.message : String(error)
4477
+ });
4478
+ }
4479
+ );
4480
+ }
3976
4481
  this.patch({
3977
4482
  busy: { deletingInstanceHash: null },
3978
4483
  statusText: `Deletion submitted for ${instanceHash}`