@le-space/ui 0.3.27 → 0.3.30

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.27",
3
+ "version": "0.3.30",
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.27",
40
- "@le-space/core": "0.3.27",
39
+ "@le-space/browser": "0.3.30",
40
+ "@le-space/core": "0.3.30",
41
41
  "viem": "^2.38.5"
42
42
  },
43
43
  "peerDependencies": {
package/react/index.js CHANGED
@@ -28,7 +28,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
28
28
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
29
29
 
30
30
  // src/shared/package-version.ts
31
- var UI_PACKAGE_VERSION = "0.3.27";
31
+ var UI_PACKAGE_VERSION = "0.3.30";
32
32
 
33
33
  // src/shared/format.ts
34
34
  function shortHash(value, head = 8, tail = 6) {
@@ -1878,6 +1878,13 @@ async function waitForVmRuntime(args) {
1878
1878
  }
1879
1879
 
1880
1880
  // ../core/src/guest.ts
1881
+ function proxyHostnameFromUrl(value) {
1882
+ try {
1883
+ return value ? new URL(value).hostname : null;
1884
+ } catch {
1885
+ return null;
1886
+ }
1887
+ }
1881
1888
  async function defaultHttpProbe(fetch2, url, timeoutMs = 1e4) {
1882
1889
  try {
1883
1890
  const controller = new AbortController();
@@ -2119,6 +2126,59 @@ async function fetchUcGoPeerMetadata(args) {
2119
2126
  `Relay metadata did not become ready after ${attempts} attempts: ${lastError ? lastError : typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
2120
2127
  );
2121
2128
  }
2129
+ async function verifyUcGoPeerReachability(args) {
2130
+ const checks = {};
2131
+ const mappedPorts2 = args.mappedPorts ?? {};
2132
+ const hostIpv4 = args.hostIpv4;
2133
+ const skippedInternalPorts = new Set(
2134
+ (args.skipInternalPorts ?? ["80"]).map((value) => String(value))
2135
+ );
2136
+ const httpProbe = args.httpProbe ?? ((url, timeoutMs) => defaultHttpProbe(args.fetch, url, timeoutMs));
2137
+ if (hostIpv4) {
2138
+ for (const [internalPort, mapping] of Object.entries(mappedPorts2)) {
2139
+ if (skippedInternalPorts.has(String(internalPort))) continue;
2140
+ if (mapping?.tcp === true && mapping?.host) {
2141
+ checks[`tcp:${internalPort}`] = {
2142
+ host: hostIpv4,
2143
+ port: mapping.host,
2144
+ ...await args.tcpProbe(
2145
+ hostIpv4,
2146
+ mapping.host,
2147
+ Number(args.tcpTimeoutMs ?? 5e3)
2148
+ )
2149
+ };
2150
+ } else if (mapping?.udp === true && mapping?.host) {
2151
+ checks[`udp:${internalPort}`] = {
2152
+ host: hostIpv4,
2153
+ port: mapping.host,
2154
+ ok: null,
2155
+ note: "UDP mapping published; CI does not perform an application-level UDP handshake probe."
2156
+ };
2157
+ }
2158
+ }
2159
+ }
2160
+ if (args.proxyUrl && args.verifyProxyHttp !== false) {
2161
+ checks["https:proxy"] = await httpProbe(
2162
+ args.proxyUrl,
2163
+ Number(args.httpTimeoutMs ?? 1e4)
2164
+ );
2165
+ }
2166
+ const proxyHostname = proxyHostnameFromUrl(args.proxyUrl);
2167
+ if (proxyHostname) {
2168
+ checks["tcp:proxy-443"] = await args.tcpProbe(
2169
+ proxyHostname,
2170
+ 443,
2171
+ Number(args.tcpTimeoutMs ?? 5e3)
2172
+ );
2173
+ }
2174
+ const failedChecks = Object.entries(checks).filter(
2175
+ ([, value]) => value?.ok === false
2176
+ );
2177
+ return {
2178
+ ok: failedChecks.length === 0,
2179
+ checks
2180
+ };
2181
+ }
2122
2182
 
2123
2183
  // ../aleph-bootstrap/src/index.ts
2124
2184
  import { recoverMessageAddress } from "viem";
@@ -3219,6 +3279,8 @@ var UI_DEPLOY_WAIT_ATTEMPTS = 60;
3219
3279
  var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
3220
3280
  var UI_RUNTIME_WAIT_ATTEMPTS = 40;
3221
3281
  var UI_RUNTIME_WAIT_DELAY_MS = 5e3;
3282
+ var UI_REACHABILITY_VERIFY_ATTEMPTS = 25;
3283
+ var UI_REACHABILITY_VERIFY_DELAY_MS = 3e3;
3222
3284
  function hasUsableRuntime(details) {
3223
3285
  return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
3224
3286
  }
@@ -3325,15 +3387,16 @@ var SponsorRelayController = class {
3325
3387
  this.progressEmitter.emit(nextEvent);
3326
3388
  }
3327
3389
  async configureRelayBootstrapRegistration(args) {
3390
+ let runtime = args.runtime;
3328
3391
  const profile = relayProfileForManifest(this.state.manifest);
3329
3392
  if (!profile) return;
3330
3393
  if (!this.state.wallet.address) {
3331
3394
  throw new Error("A connected wallet is required to register relay bootstrap addresses.");
3332
3395
  }
3333
- if (!args.runtime.hostIpv4) {
3396
+ if (!runtime.hostIpv4) {
3334
3397
  throw new Error("Relay runtime did not expose a host IPv4 address.");
3335
3398
  }
3336
- const mappedPorts2 = args.runtime.mappedPorts ?? {};
3399
+ let mappedPorts2 = runtime.mappedPorts ?? {};
3337
3400
  const setupPort = mappedPorts2["80"]?.host ?? null;
3338
3401
  if (!setupPort) {
3339
3402
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
@@ -3348,7 +3411,7 @@ var SponsorRelayController = class {
3348
3411
  detail: "Waiting for the guest setup endpoint before collecting relay metadata."
3349
3412
  });
3350
3413
  const setupHealth = await waitForSetupEndpoint({
3351
- hostIpv4: args.runtime.hostIpv4,
3414
+ hostIpv4: runtime.hostIpv4,
3352
3415
  setupPort,
3353
3416
  fetch: (url, init) => fetch(url, init),
3354
3417
  attempts: 15,
@@ -3374,12 +3437,12 @@ var SponsorRelayController = class {
3374
3437
  throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
3375
3438
  }
3376
3439
  const configureResult = await configureOrbitdbRelaySetup2({
3377
- hostIpv4: args.runtime.hostIpv4,
3378
- publicIpv6: args.runtime.ipv6,
3440
+ hostIpv4: runtime.hostIpv4,
3441
+ publicIpv6: runtime.ipv6,
3379
3442
  setupPort,
3380
3443
  tcpPort,
3381
3444
  wsPort,
3382
- proxyUrl: args.runtime.proxyUrl ?? void 0,
3445
+ proxyUrl: runtime.proxyUrl ?? void 0,
3383
3446
  metricsPort: mappedPorts2["9090"]?.host ?? null,
3384
3447
  metricsHttpsPort: mappedPorts2["443"]?.host ?? null,
3385
3448
  webrtcPort: mappedPorts2["9093"]?.host ?? null,
@@ -3396,6 +3459,59 @@ var SponsorRelayController = class {
3396
3459
  if (!args.deploymentToken) {
3397
3460
  throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3398
3461
  }
3462
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3463
+ this.emitProgress({
3464
+ stage: "deployment-confirmed",
3465
+ label: "Waiting for HTTPS route",
3466
+ progress: 93,
3467
+ status: "warning",
3468
+ itemHash: args.itemHash,
3469
+ detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
3470
+ });
3471
+ let latestRuntime = runtime;
3472
+ for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
3473
+ if (latestRuntime.webAccess?.active === true) {
3474
+ break;
3475
+ }
3476
+ await new Promise(
3477
+ (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
3478
+ );
3479
+ latestRuntime = await fetchVmRuntime({
3480
+ itemHash: args.itemHash,
3481
+ fetch: (url, init) => fetch(url, init),
3482
+ crnHash: runtime.selectedCrn?.hash ?? runtime.allocation?.crnHash ?? void 0,
3483
+ crns: this.state.crns,
3484
+ crnListUrl: this.props.crnListUrl
3485
+ }).catch(() => latestRuntime);
3486
+ this.emitProgress({
3487
+ stage: "deployment-confirmed",
3488
+ label: "Waiting for HTTPS route",
3489
+ progress: 93,
3490
+ status: latestRuntime.webAccess?.active === true ? "info" : "warning",
3491
+ itemHash: args.itemHash,
3492
+ detail: `2n6 activation ${attempt + 1}/${UI_RUNTIME_WAIT_ATTEMPTS}: ${latestRuntime.webAccess?.active === true ? "active" : "still pending"} for ${latestRuntime.proxyUrl ?? runtime.proxyUrl ?? "reserved proxy URL"}.`
3493
+ });
3494
+ this.trace("deploy:bootstrap-proxy-activation", {
3495
+ itemHash: args.itemHash,
3496
+ attempt: attempt + 1,
3497
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3498
+ active: latestRuntime.webAccess?.active === true,
3499
+ proxyUrl: latestRuntime.proxyUrl ?? null
3500
+ });
3501
+ }
3502
+ runtime = latestRuntime;
3503
+ mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3504
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3505
+ throw new Error(
3506
+ "Aleph reserved a 2n6 proxy URL, but it never became active in time. Aborting guest HTTPS configuration to avoid a broken Caddy setup."
3507
+ );
3508
+ }
3509
+ }
3510
+ if (!runtime.hostIpv4) {
3511
+ throw new Error("Relay runtime lost its host IPv4 address while waiting for HTTPS activation.");
3512
+ }
3513
+ const runtimeHostIpv4 = runtime.hostIpv4;
3514
+ const activeProxyUrl = runtime.webAccess?.active === true ? runtime.proxyUrl ?? null : null;
3399
3515
  this.emitProgress({
3400
3516
  stage: "deployment-confirmed",
3401
3517
  label: "Publishing relay config",
@@ -3413,9 +3529,9 @@ var SponsorRelayController = class {
3413
3529
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3414
3530
  status: "pending",
3415
3531
  runtime: {
3416
- publicIpv4: args.runtime.hostIpv4,
3417
- publicIpv6: args.runtime.ipv6 ?? null,
3418
- proxyUrl: args.runtime.proxyUrl ?? null,
3532
+ publicIpv4: runtimeHostIpv4,
3533
+ publicIpv6: runtime.ipv6 ?? null,
3534
+ proxyUrl: activeProxyUrl,
3419
3535
  mappedPorts: mappedPorts2
3420
3536
  },
3421
3537
  bootstrap: {
@@ -3481,7 +3597,7 @@ var SponsorRelayController = class {
3481
3597
  detail: "Waiting for the relay to report its final public multiaddrs."
3482
3598
  });
3483
3599
  const relayMetadata = await fetchRelayMetadataForRuntime({
3484
- runtime: args.runtime,
3600
+ runtime,
3485
3601
  fetch: (url, init) => fetch(url, init),
3486
3602
  preferSecureMetadata: profile === "uc-go-peer",
3487
3603
  attempts: 80,
@@ -3514,6 +3630,55 @@ var SponsorRelayController = class {
3514
3630
  if (!relayMetadata) {
3515
3631
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3516
3632
  }
3633
+ if (profile === "uc-go-peer" && runtime.proxyUrl) {
3634
+ this.emitProgress({
3635
+ stage: "publishing-bootstrap",
3636
+ label: "Verifying reachability",
3637
+ progress: 96,
3638
+ status: "info",
3639
+ itemHash: args.itemHash,
3640
+ detail: "Checking that the secure relay endpoint is reachable before trusting the deployment as ready."
3641
+ });
3642
+ const verificationUrl = (() => {
3643
+ try {
3644
+ return new URL("/bootstrap/metadata", runtime.proxyUrl).toString();
3645
+ } catch {
3646
+ return runtime.proxyUrl ?? "";
3647
+ }
3648
+ })();
3649
+ let verificationResult = null;
3650
+ for (let attempt = 0; attempt < UI_REACHABILITY_VERIFY_ATTEMPTS; attempt += 1) {
3651
+ verificationResult = await verifyUcGoPeerReachability({
3652
+ proxyUrl: verificationUrl,
3653
+ verifyProxyHttp: true,
3654
+ fetch: (url, init) => fetch(url, init),
3655
+ tcpProbe: async () => ({ ok: null })
3656
+ });
3657
+ this.trace("deploy:reachability-verification-attempt", {
3658
+ itemHash: args.itemHash,
3659
+ attempt: attempt + 1,
3660
+ attempts: UI_REACHABILITY_VERIFY_ATTEMPTS,
3661
+ verificationUrl,
3662
+ ok: verificationResult.ok,
3663
+ checks: verificationResult.checks
3664
+ });
3665
+ const proxyHttpCheck2 = verificationResult.checks["https:proxy"];
3666
+ if (verificationResult.ok && proxyHttpCheck2?.ok === true) {
3667
+ break;
3668
+ }
3669
+ if (attempt < UI_REACHABILITY_VERIFY_ATTEMPTS - 1) {
3670
+ await new Promise(
3671
+ (resolve) => globalThis.setTimeout(resolve, UI_REACHABILITY_VERIFY_DELAY_MS)
3672
+ );
3673
+ }
3674
+ }
3675
+ const proxyHttpCheck = verificationResult?.checks["https:proxy"];
3676
+ if (!verificationResult?.ok || proxyHttpCheck?.ok !== true) {
3677
+ throw new Error(
3678
+ proxyHttpCheck?.error ? `Secure relay endpoint did not become reachable in time: ${proxyHttpCheck.error}` : "Secure relay endpoint did not become reachable in time."
3679
+ );
3680
+ }
3681
+ }
3517
3682
  const { peerId } = relayMetadata;
3518
3683
  const registrationId = createRelayBootstrapRegistrationId(
3519
3684
  profile,
@@ -3631,6 +3796,7 @@ var SponsorRelayController = class {
3631
3796
  const currentProgress = this.state.deploymentProgress;
3632
3797
  const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
3633
3798
  const keepTerminalSuccess = this.lastCompletedDeploymentHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
3799
+ const keepSpecificInFlightProgress = currentProgressIsForLatestHash && currentProgress.stage !== "completed" && currentProgress.stage !== "error" && currentProgress.progress >= 93;
3634
3800
  const status = latest.details.messageStatus;
3635
3801
  if (status === "rejected") {
3636
3802
  this.emitProgress({
@@ -3673,6 +3839,16 @@ var SponsorRelayController = class {
3673
3839
  });
3674
3840
  return;
3675
3841
  }
3842
+ if (keepSpecificInFlightProgress) {
3843
+ this.trace("progress:retain-inflight", {
3844
+ reason: "latest refresh would overwrite a more specific in-flight deployment phase",
3845
+ itemHash,
3846
+ currentStage: currentProgress.stage,
3847
+ currentLabel: currentProgress.label,
3848
+ currentProgress: currentProgress.progress
3849
+ });
3850
+ return;
3851
+ }
3676
3852
  this.emitProgress({
3677
3853
  stage: "deployment-confirmed",
3678
3854
  label: "Waiting for runtime",
@@ -3683,6 +3859,16 @@ var SponsorRelayController = class {
3683
3859
  });
3684
3860
  return;
3685
3861
  }
3862
+ if (keepSpecificInFlightProgress) {
3863
+ this.trace("progress:retain-inflight", {
3864
+ reason: "latest refresh would overwrite a more specific in-flight deployment phase",
3865
+ itemHash,
3866
+ currentStage: currentProgress.stage,
3867
+ currentLabel: currentProgress.label,
3868
+ currentProgress: currentProgress.progress
3869
+ });
3870
+ return;
3871
+ }
3686
3872
  this.emitProgress({
3687
3873
  stage: "deployment-confirmed",
3688
3874
  label: "Runtime ready",
@@ -4313,6 +4499,15 @@ var SponsorRelayController = class {
4313
4499
  });
4314
4500
  if (attemptItemHash) {
4315
4501
  if (attemptDeploymentToken) {
4502
+ this.emitProgress({
4503
+ stage: "refreshing-instances",
4504
+ label: "Cleaning up failed attempt",
4505
+ progress: 16,
4506
+ status: "warning",
4507
+ itemHash: attemptItemHash,
4508
+ detail: `Removing bootstrap handoff state for failed deployment on ${attemptLabel}.`,
4509
+ error: lastError.message
4510
+ });
4316
4511
  this.trace("deploy:bootstrap-config-cleanup-start", {
4317
4512
  itemHash: attemptItemHash,
4318
4513
  deploymentToken: attemptDeploymentToken
@@ -4338,8 +4533,26 @@ var SponsorRelayController = class {
4338
4533
  deploymentToken: attemptDeploymentToken,
4339
4534
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4340
4535
  });
4536
+ this.emitProgress({
4537
+ stage: "refreshing-instances",
4538
+ label: "Cleanup warning",
4539
+ progress: 16,
4540
+ status: "warning",
4541
+ itemHash: attemptItemHash,
4542
+ detail: `Bootstrap cleanup reported an issue for ${attemptLabel}, but retry cleanup will continue.`,
4543
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4544
+ });
4341
4545
  });
4342
4546
  }
4547
+ this.emitProgress({
4548
+ stage: "refreshing-instances",
4549
+ label: "Discarding failed deployment",
4550
+ progress: 17,
4551
+ status: "warning",
4552
+ itemHash: attemptItemHash,
4553
+ detail: `Forgetting failed deployment ${attemptItemHash.slice(0, 12)} on ${attemptLabel} before retrying.`,
4554
+ error: lastError.message
4555
+ });
4343
4556
  this.trace("deploy:forget-failed-attempt-start", {
4344
4557
  itemHash: attemptItemHash,
4345
4558
  crnHash: candidateCrn.hash,
@@ -4362,12 +4575,30 @@ var SponsorRelayController = class {
4362
4575
  itemHash: attemptItemHash,
4363
4576
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4364
4577
  });
4578
+ this.emitProgress({
4579
+ stage: "refreshing-instances",
4580
+ label: "Cleanup warning",
4581
+ progress: 17,
4582
+ status: "warning",
4583
+ itemHash: attemptItemHash,
4584
+ detail: `Failed deployment cleanup reported an issue for ${attemptLabel}.`,
4585
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4586
+ });
4365
4587
  });
4366
4588
  this.trace("deploy:forget-failed-attempt-complete", {
4367
4589
  itemHash: attemptItemHash,
4368
4590
  crnHash: candidateCrn.hash,
4369
4591
  crnName: candidateCrn.name
4370
4592
  });
4593
+ this.emitProgress({
4594
+ stage: "refreshing-instances",
4595
+ label: "Cleanup complete",
4596
+ progress: 18,
4597
+ status: "info",
4598
+ itemHash: attemptItemHash,
4599
+ detail: `Finished cleaning up failed deployment on ${attemptLabel}.`,
4600
+ error: null
4601
+ });
4371
4602
  }
4372
4603
  if (candidateIndex < orderedCrns.length - 1) {
4373
4604
  this.emitProgress({
package/shared/index.d.ts CHANGED
@@ -290,7 +290,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
290
290
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
291
291
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
292
292
 
293
- declare const UI_PACKAGE_VERSION = "0.3.27";
293
+ declare const UI_PACKAGE_VERSION = "0.3.30";
294
294
 
295
295
  interface RootfsRequiredPortForward {
296
296
  port: number;
package/shared/index.js CHANGED
@@ -27,7 +27,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
27
27
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
28
28
 
29
29
  // src/shared/package-version.ts
30
- var UI_PACKAGE_VERSION = "0.3.27";
30
+ var UI_PACKAGE_VERSION = "0.3.30";
31
31
 
32
32
  // src/shared/format.ts
33
33
  function shortHash(value, head = 8, tail = 6) {
@@ -1877,6 +1877,13 @@ async function waitForVmRuntime(args) {
1877
1877
  }
1878
1878
 
1879
1879
  // ../core/src/guest.ts
1880
+ function proxyHostnameFromUrl(value) {
1881
+ try {
1882
+ return value ? new URL(value).hostname : null;
1883
+ } catch {
1884
+ return null;
1885
+ }
1886
+ }
1880
1887
  async function defaultHttpProbe(fetch2, url, timeoutMs = 1e4) {
1881
1888
  try {
1882
1889
  const controller = new AbortController();
@@ -2118,6 +2125,59 @@ async function fetchUcGoPeerMetadata(args) {
2118
2125
  `Relay metadata did not become ready after ${attempts} attempts: ${lastError ? lastError : typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
2119
2126
  );
2120
2127
  }
2128
+ async function verifyUcGoPeerReachability(args) {
2129
+ const checks = {};
2130
+ const mappedPorts2 = args.mappedPorts ?? {};
2131
+ const hostIpv4 = args.hostIpv4;
2132
+ const skippedInternalPorts = new Set(
2133
+ (args.skipInternalPorts ?? ["80"]).map((value) => String(value))
2134
+ );
2135
+ const httpProbe = args.httpProbe ?? ((url, timeoutMs) => defaultHttpProbe(args.fetch, url, timeoutMs));
2136
+ if (hostIpv4) {
2137
+ for (const [internalPort, mapping] of Object.entries(mappedPorts2)) {
2138
+ if (skippedInternalPorts.has(String(internalPort))) continue;
2139
+ if (mapping?.tcp === true && mapping?.host) {
2140
+ checks[`tcp:${internalPort}`] = {
2141
+ host: hostIpv4,
2142
+ port: mapping.host,
2143
+ ...await args.tcpProbe(
2144
+ hostIpv4,
2145
+ mapping.host,
2146
+ Number(args.tcpTimeoutMs ?? 5e3)
2147
+ )
2148
+ };
2149
+ } else if (mapping?.udp === true && mapping?.host) {
2150
+ checks[`udp:${internalPort}`] = {
2151
+ host: hostIpv4,
2152
+ port: mapping.host,
2153
+ ok: null,
2154
+ note: "UDP mapping published; CI does not perform an application-level UDP handshake probe."
2155
+ };
2156
+ }
2157
+ }
2158
+ }
2159
+ if (args.proxyUrl && args.verifyProxyHttp !== false) {
2160
+ checks["https:proxy"] = await httpProbe(
2161
+ args.proxyUrl,
2162
+ Number(args.httpTimeoutMs ?? 1e4)
2163
+ );
2164
+ }
2165
+ const proxyHostname = proxyHostnameFromUrl(args.proxyUrl);
2166
+ if (proxyHostname) {
2167
+ checks["tcp:proxy-443"] = await args.tcpProbe(
2168
+ proxyHostname,
2169
+ 443,
2170
+ Number(args.tcpTimeoutMs ?? 5e3)
2171
+ );
2172
+ }
2173
+ const failedChecks = Object.entries(checks).filter(
2174
+ ([, value]) => value?.ok === false
2175
+ );
2176
+ return {
2177
+ ok: failedChecks.length === 0,
2178
+ checks
2179
+ };
2180
+ }
2121
2181
 
2122
2182
  // ../aleph-bootstrap/src/index.ts
2123
2183
  import { recoverMessageAddress } from "viem";
@@ -3218,6 +3278,8 @@ var UI_DEPLOY_WAIT_ATTEMPTS = 60;
3218
3278
  var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
3219
3279
  var UI_RUNTIME_WAIT_ATTEMPTS = 40;
3220
3280
  var UI_RUNTIME_WAIT_DELAY_MS = 5e3;
3281
+ var UI_REACHABILITY_VERIFY_ATTEMPTS = 25;
3282
+ var UI_REACHABILITY_VERIFY_DELAY_MS = 3e3;
3221
3283
  function hasUsableRuntime(details) {
3222
3284
  return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
3223
3285
  }
@@ -3324,15 +3386,16 @@ var SponsorRelayController = class {
3324
3386
  this.progressEmitter.emit(nextEvent);
3325
3387
  }
3326
3388
  async configureRelayBootstrapRegistration(args) {
3389
+ let runtime = args.runtime;
3327
3390
  const profile = relayProfileForManifest(this.state.manifest);
3328
3391
  if (!profile) return;
3329
3392
  if (!this.state.wallet.address) {
3330
3393
  throw new Error("A connected wallet is required to register relay bootstrap addresses.");
3331
3394
  }
3332
- if (!args.runtime.hostIpv4) {
3395
+ if (!runtime.hostIpv4) {
3333
3396
  throw new Error("Relay runtime did not expose a host IPv4 address.");
3334
3397
  }
3335
- const mappedPorts2 = args.runtime.mappedPorts ?? {};
3398
+ let mappedPorts2 = runtime.mappedPorts ?? {};
3336
3399
  const setupPort = mappedPorts2["80"]?.host ?? null;
3337
3400
  if (!setupPort) {
3338
3401
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
@@ -3347,7 +3410,7 @@ var SponsorRelayController = class {
3347
3410
  detail: "Waiting for the guest setup endpoint before collecting relay metadata."
3348
3411
  });
3349
3412
  const setupHealth = await waitForSetupEndpoint({
3350
- hostIpv4: args.runtime.hostIpv4,
3413
+ hostIpv4: runtime.hostIpv4,
3351
3414
  setupPort,
3352
3415
  fetch: (url, init) => fetch(url, init),
3353
3416
  attempts: 15,
@@ -3373,12 +3436,12 @@ var SponsorRelayController = class {
3373
3436
  throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
3374
3437
  }
3375
3438
  const configureResult = await configureOrbitdbRelaySetup2({
3376
- hostIpv4: args.runtime.hostIpv4,
3377
- publicIpv6: args.runtime.ipv6,
3439
+ hostIpv4: runtime.hostIpv4,
3440
+ publicIpv6: runtime.ipv6,
3378
3441
  setupPort,
3379
3442
  tcpPort,
3380
3443
  wsPort,
3381
- proxyUrl: args.runtime.proxyUrl ?? void 0,
3444
+ proxyUrl: runtime.proxyUrl ?? void 0,
3382
3445
  metricsPort: mappedPorts2["9090"]?.host ?? null,
3383
3446
  metricsHttpsPort: mappedPorts2["443"]?.host ?? null,
3384
3447
  webrtcPort: mappedPorts2["9093"]?.host ?? null,
@@ -3395,6 +3458,59 @@ var SponsorRelayController = class {
3395
3458
  if (!args.deploymentToken) {
3396
3459
  throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3397
3460
  }
3461
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3462
+ this.emitProgress({
3463
+ stage: "deployment-confirmed",
3464
+ label: "Waiting for HTTPS route",
3465
+ progress: 93,
3466
+ status: "warning",
3467
+ itemHash: args.itemHash,
3468
+ detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
3469
+ });
3470
+ let latestRuntime = runtime;
3471
+ for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
3472
+ if (latestRuntime.webAccess?.active === true) {
3473
+ break;
3474
+ }
3475
+ await new Promise(
3476
+ (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
3477
+ );
3478
+ latestRuntime = await fetchVmRuntime({
3479
+ itemHash: args.itemHash,
3480
+ fetch: (url, init) => fetch(url, init),
3481
+ crnHash: runtime.selectedCrn?.hash ?? runtime.allocation?.crnHash ?? void 0,
3482
+ crns: this.state.crns,
3483
+ crnListUrl: this.props.crnListUrl
3484
+ }).catch(() => latestRuntime);
3485
+ this.emitProgress({
3486
+ stage: "deployment-confirmed",
3487
+ label: "Waiting for HTTPS route",
3488
+ progress: 93,
3489
+ status: latestRuntime.webAccess?.active === true ? "info" : "warning",
3490
+ itemHash: args.itemHash,
3491
+ detail: `2n6 activation ${attempt + 1}/${UI_RUNTIME_WAIT_ATTEMPTS}: ${latestRuntime.webAccess?.active === true ? "active" : "still pending"} for ${latestRuntime.proxyUrl ?? runtime.proxyUrl ?? "reserved proxy URL"}.`
3492
+ });
3493
+ this.trace("deploy:bootstrap-proxy-activation", {
3494
+ itemHash: args.itemHash,
3495
+ attempt: attempt + 1,
3496
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3497
+ active: latestRuntime.webAccess?.active === true,
3498
+ proxyUrl: latestRuntime.proxyUrl ?? null
3499
+ });
3500
+ }
3501
+ runtime = latestRuntime;
3502
+ mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3503
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3504
+ throw new Error(
3505
+ "Aleph reserved a 2n6 proxy URL, but it never became active in time. Aborting guest HTTPS configuration to avoid a broken Caddy setup."
3506
+ );
3507
+ }
3508
+ }
3509
+ if (!runtime.hostIpv4) {
3510
+ throw new Error("Relay runtime lost its host IPv4 address while waiting for HTTPS activation.");
3511
+ }
3512
+ const runtimeHostIpv4 = runtime.hostIpv4;
3513
+ const activeProxyUrl = runtime.webAccess?.active === true ? runtime.proxyUrl ?? null : null;
3398
3514
  this.emitProgress({
3399
3515
  stage: "deployment-confirmed",
3400
3516
  label: "Publishing relay config",
@@ -3412,9 +3528,9 @@ var SponsorRelayController = class {
3412
3528
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3413
3529
  status: "pending",
3414
3530
  runtime: {
3415
- publicIpv4: args.runtime.hostIpv4,
3416
- publicIpv6: args.runtime.ipv6 ?? null,
3417
- proxyUrl: args.runtime.proxyUrl ?? null,
3531
+ publicIpv4: runtimeHostIpv4,
3532
+ publicIpv6: runtime.ipv6 ?? null,
3533
+ proxyUrl: activeProxyUrl,
3418
3534
  mappedPorts: mappedPorts2
3419
3535
  },
3420
3536
  bootstrap: {
@@ -3480,7 +3596,7 @@ var SponsorRelayController = class {
3480
3596
  detail: "Waiting for the relay to report its final public multiaddrs."
3481
3597
  });
3482
3598
  const relayMetadata = await fetchRelayMetadataForRuntime({
3483
- runtime: args.runtime,
3599
+ runtime,
3484
3600
  fetch: (url, init) => fetch(url, init),
3485
3601
  preferSecureMetadata: profile === "uc-go-peer",
3486
3602
  attempts: 80,
@@ -3513,6 +3629,55 @@ var SponsorRelayController = class {
3513
3629
  if (!relayMetadata) {
3514
3630
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3515
3631
  }
3632
+ if (profile === "uc-go-peer" && runtime.proxyUrl) {
3633
+ this.emitProgress({
3634
+ stage: "publishing-bootstrap",
3635
+ label: "Verifying reachability",
3636
+ progress: 96,
3637
+ status: "info",
3638
+ itemHash: args.itemHash,
3639
+ detail: "Checking that the secure relay endpoint is reachable before trusting the deployment as ready."
3640
+ });
3641
+ const verificationUrl = (() => {
3642
+ try {
3643
+ return new URL("/bootstrap/metadata", runtime.proxyUrl).toString();
3644
+ } catch {
3645
+ return runtime.proxyUrl ?? "";
3646
+ }
3647
+ })();
3648
+ let verificationResult = null;
3649
+ for (let attempt = 0; attempt < UI_REACHABILITY_VERIFY_ATTEMPTS; attempt += 1) {
3650
+ verificationResult = await verifyUcGoPeerReachability({
3651
+ proxyUrl: verificationUrl,
3652
+ verifyProxyHttp: true,
3653
+ fetch: (url, init) => fetch(url, init),
3654
+ tcpProbe: async () => ({ ok: null })
3655
+ });
3656
+ this.trace("deploy:reachability-verification-attempt", {
3657
+ itemHash: args.itemHash,
3658
+ attempt: attempt + 1,
3659
+ attempts: UI_REACHABILITY_VERIFY_ATTEMPTS,
3660
+ verificationUrl,
3661
+ ok: verificationResult.ok,
3662
+ checks: verificationResult.checks
3663
+ });
3664
+ const proxyHttpCheck2 = verificationResult.checks["https:proxy"];
3665
+ if (verificationResult.ok && proxyHttpCheck2?.ok === true) {
3666
+ break;
3667
+ }
3668
+ if (attempt < UI_REACHABILITY_VERIFY_ATTEMPTS - 1) {
3669
+ await new Promise(
3670
+ (resolve) => globalThis.setTimeout(resolve, UI_REACHABILITY_VERIFY_DELAY_MS)
3671
+ );
3672
+ }
3673
+ }
3674
+ const proxyHttpCheck = verificationResult?.checks["https:proxy"];
3675
+ if (!verificationResult?.ok || proxyHttpCheck?.ok !== true) {
3676
+ throw new Error(
3677
+ proxyHttpCheck?.error ? `Secure relay endpoint did not become reachable in time: ${proxyHttpCheck.error}` : "Secure relay endpoint did not become reachable in time."
3678
+ );
3679
+ }
3680
+ }
3516
3681
  const { peerId } = relayMetadata;
3517
3682
  const registrationId = createRelayBootstrapRegistrationId(
3518
3683
  profile,
@@ -3630,6 +3795,7 @@ var SponsorRelayController = class {
3630
3795
  const currentProgress = this.state.deploymentProgress;
3631
3796
  const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
3632
3797
  const keepTerminalSuccess = this.lastCompletedDeploymentHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
3798
+ const keepSpecificInFlightProgress = currentProgressIsForLatestHash && currentProgress.stage !== "completed" && currentProgress.stage !== "error" && currentProgress.progress >= 93;
3633
3799
  const status = latest.details.messageStatus;
3634
3800
  if (status === "rejected") {
3635
3801
  this.emitProgress({
@@ -3672,6 +3838,16 @@ var SponsorRelayController = class {
3672
3838
  });
3673
3839
  return;
3674
3840
  }
3841
+ if (keepSpecificInFlightProgress) {
3842
+ this.trace("progress:retain-inflight", {
3843
+ reason: "latest refresh would overwrite a more specific in-flight deployment phase",
3844
+ itemHash,
3845
+ currentStage: currentProgress.stage,
3846
+ currentLabel: currentProgress.label,
3847
+ currentProgress: currentProgress.progress
3848
+ });
3849
+ return;
3850
+ }
3675
3851
  this.emitProgress({
3676
3852
  stage: "deployment-confirmed",
3677
3853
  label: "Waiting for runtime",
@@ -3682,6 +3858,16 @@ var SponsorRelayController = class {
3682
3858
  });
3683
3859
  return;
3684
3860
  }
3861
+ if (keepSpecificInFlightProgress) {
3862
+ this.trace("progress:retain-inflight", {
3863
+ reason: "latest refresh would overwrite a more specific in-flight deployment phase",
3864
+ itemHash,
3865
+ currentStage: currentProgress.stage,
3866
+ currentLabel: currentProgress.label,
3867
+ currentProgress: currentProgress.progress
3868
+ });
3869
+ return;
3870
+ }
3685
3871
  this.emitProgress({
3686
3872
  stage: "deployment-confirmed",
3687
3873
  label: "Runtime ready",
@@ -4312,6 +4498,15 @@ var SponsorRelayController = class {
4312
4498
  });
4313
4499
  if (attemptItemHash) {
4314
4500
  if (attemptDeploymentToken) {
4501
+ this.emitProgress({
4502
+ stage: "refreshing-instances",
4503
+ label: "Cleaning up failed attempt",
4504
+ progress: 16,
4505
+ status: "warning",
4506
+ itemHash: attemptItemHash,
4507
+ detail: `Removing bootstrap handoff state for failed deployment on ${attemptLabel}.`,
4508
+ error: lastError.message
4509
+ });
4315
4510
  this.trace("deploy:bootstrap-config-cleanup-start", {
4316
4511
  itemHash: attemptItemHash,
4317
4512
  deploymentToken: attemptDeploymentToken
@@ -4337,8 +4532,26 @@ var SponsorRelayController = class {
4337
4532
  deploymentToken: attemptDeploymentToken,
4338
4533
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4339
4534
  });
4535
+ this.emitProgress({
4536
+ stage: "refreshing-instances",
4537
+ label: "Cleanup warning",
4538
+ progress: 16,
4539
+ status: "warning",
4540
+ itemHash: attemptItemHash,
4541
+ detail: `Bootstrap cleanup reported an issue for ${attemptLabel}, but retry cleanup will continue.`,
4542
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4543
+ });
4340
4544
  });
4341
4545
  }
4546
+ this.emitProgress({
4547
+ stage: "refreshing-instances",
4548
+ label: "Discarding failed deployment",
4549
+ progress: 17,
4550
+ status: "warning",
4551
+ itemHash: attemptItemHash,
4552
+ detail: `Forgetting failed deployment ${attemptItemHash.slice(0, 12)} on ${attemptLabel} before retrying.`,
4553
+ error: lastError.message
4554
+ });
4342
4555
  this.trace("deploy:forget-failed-attempt-start", {
4343
4556
  itemHash: attemptItemHash,
4344
4557
  crnHash: candidateCrn.hash,
@@ -4361,12 +4574,30 @@ var SponsorRelayController = class {
4361
4574
  itemHash: attemptItemHash,
4362
4575
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4363
4576
  });
4577
+ this.emitProgress({
4578
+ stage: "refreshing-instances",
4579
+ label: "Cleanup warning",
4580
+ progress: 17,
4581
+ status: "warning",
4582
+ itemHash: attemptItemHash,
4583
+ detail: `Failed deployment cleanup reported an issue for ${attemptLabel}.`,
4584
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4585
+ });
4364
4586
  });
4365
4587
  this.trace("deploy:forget-failed-attempt-complete", {
4366
4588
  itemHash: attemptItemHash,
4367
4589
  crnHash: candidateCrn.hash,
4368
4590
  crnName: candidateCrn.name
4369
4591
  });
4592
+ this.emitProgress({
4593
+ stage: "refreshing-instances",
4594
+ label: "Cleanup complete",
4595
+ progress: 18,
4596
+ status: "info",
4597
+ itemHash: attemptItemHash,
4598
+ detail: `Finished cleaning up failed deployment on ${attemptLabel}.`,
4599
+ error: null
4600
+ });
4370
4601
  }
4371
4602
  if (candidateIndex < orderedCrns.length - 1) {
4372
4603
  this.emitProgress({