@le-space/ui 0.3.6 → 0.3.8

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.6",
3
+ "version": "0.3.8",
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.6",
40
- "@le-space/core": "0.3.6",
39
+ "@le-space/browser": "0.3.8",
40
+ "@le-space/core": "0.3.8",
41
41
  "viem": "^2.38.5"
42
42
  },
43
43
  "peerDependencies": {
package/react/index.js CHANGED
@@ -1,6 +1,37 @@
1
1
  // src/react/SponsorRelayFab.tsx
2
2
  import React from "react";
3
3
 
4
+ // package.json
5
+ var package_default = {
6
+ name: "@le-space/ui",
7
+ version: "0.3.8",
8
+ description: "Floating Aleph sponsor relay UI components for Svelte and React.",
9
+ license: "MIT",
10
+ type: "module",
11
+ main: "./src/shared/index.ts",
12
+ types: "./src/shared/index.ts",
13
+ dependencies: {
14
+ "@le-space/browser": "workspace:*",
15
+ "@le-space/core": "workspace:*",
16
+ viem: "^2.38.5"
17
+ },
18
+ peerDependencies: {
19
+ react: ">=18",
20
+ "react-dom": ">=18",
21
+ svelte: ">=4"
22
+ },
23
+ devDependencies: {
24
+ "@types/react": "^18.3.27",
25
+ "@types/react-dom": "^18.3.7"
26
+ },
27
+ scripts: {
28
+ build: 'echo "ui build placeholder"',
29
+ "build:publish": "tsup src/shared/index.ts --format esm --dts --clean --tsconfig tsconfig.publish.json --out-dir dist/shared && tsup src/react/index.ts --format esm --dts --tsconfig tsconfig.publish.json --out-dir dist/react",
30
+ test: "pnpm run build:publish && node --test ./test/**/*.test.mjs",
31
+ lint: 'echo "ui lint placeholder"'
32
+ }
33
+ };
34
+
4
35
  // src/shared/constants.ts
5
36
  var DEFAULT_INSTANCE_NAME = "sponsor-relay";
6
37
  var DEFAULT_MANIFEST_URL = "./rootfs-manifest.json";
@@ -3646,158 +3677,156 @@ var SponsorRelayController = class {
3646
3677
  this.state.pricingSummary.pricing,
3647
3678
  this.state.pricingSummary.tier
3648
3679
  );
3649
- this.emitProgress({
3650
- stage: "selecting-crn",
3651
- label: "Selecting CRN",
3652
- progress: 18,
3653
- status: "info",
3654
- detail: this.state.selectedCrn?.name ?? this.state.selectedCrn?.hash ?? "Auto-selected compatible CRN"
3655
- });
3656
- const content = createInstanceContent({
3657
- address: this.state.wallet.address,
3658
- name: this.state.instanceName.trim(),
3659
- sshPublicKey: this.state.sshPublicKey.trim(),
3660
- rootfsItemHash: this.state.manifest.rootfsItemHash,
3661
- rootfsSizeMiB: Math.max(
3662
- this.state.manifest.rootfsSizeMiB,
3663
- spec.diskMiB
3664
- ),
3665
- vcpus: spec.vcpus,
3666
- memoryMiB: spec.memoryMiB,
3667
- rootfsVersion: this.state.manifest.version,
3668
- crnHash: this.state.selectedCrn?.hash
3669
- });
3670
- const result = await deployInstance({
3671
- sender: this.state.wallet.address,
3672
- content,
3673
- hasher: sha256Hex,
3674
- signer: personalSign,
3675
- fetch: (url, init) => fetch(url, init),
3676
- apiHost: this.client.apiHost,
3677
- sync: false,
3678
- onProgress: (event) => {
3679
- this.emitProgress(event);
3680
- }
3681
- });
3682
- this.patch({
3683
- statusText: `Deployment submitted: ${result.itemHash}`,
3684
- lastDeploymentHash: result.itemHash
3685
- });
3686
- this.trace("deploy:broadcasted", result);
3687
- this.lastRuntimeReadyHash = null;
3688
- const inspection = await waitForDeploymentResult2(result.itemHash, {
3689
- rootfsRef: this.state.manifest.rootfsItemHash,
3690
- apiHost: this.client.apiHost,
3691
- fetch: (url, init) => fetch(url, init),
3692
- attempts: UI_DEPLOY_WAIT_ATTEMPTS,
3693
- delayMs: UI_DEPLOY_WAIT_DELAY_MS,
3694
- onAttempt: (inspectionResult) => {
3695
- if (inspectionResult.status === "processed") {
3696
- this.emitProgress({
3697
- stage: "deployment-confirmed",
3698
- label: "Deployment accepted by Aleph",
3699
- progress: 82,
3700
- status: "success",
3701
- itemHash: result.itemHash,
3702
- detail: "Aleph processed the instance message."
3703
- });
3704
- return;
3705
- }
3706
- if (inspectionResult.status === "rejected") {
3707
- this.emitProgress({
3708
- stage: "deployment-rejected",
3709
- label: "Deployment rejected",
3710
- progress: 100,
3711
- status: "error",
3712
- itemHash: result.itemHash,
3713
- detail: inspectionResult.rejectionReason ?? "Aleph rejected the deployment.",
3714
- error: inspectionResult.rejectionReason ?? "Aleph rejected the deployment."
3715
- });
3716
- return;
3717
- }
3718
- this.emitProgress({
3719
- stage: "waiting-for-aleph",
3720
- label: "Waiting for Aleph",
3721
- progress: 76,
3722
- status: "info",
3723
- itemHash: result.itemHash,
3724
- detail: "Deployment submitted. Waiting for Aleph to process the instance message."
3725
- });
3680
+ const compatibleCrns2 = compatibleCrnsForTier(this.state.crns, {
3681
+ ...this.state,
3682
+ pricingSummary: {
3683
+ ...this.state.pricingSummary,
3684
+ pricing: this.state.pricingSummary.pricing,
3685
+ tier: this.state.pricingSummary.tier
3726
3686
  }
3727
3687
  });
3728
- if (inspection.status !== "processed") {
3729
- throw new Error(
3730
- inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
3731
- );
3688
+ const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
3689
+ const orderedCrns = [
3690
+ ...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
3691
+ ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
3692
+ ].slice(0, 5);
3693
+ if (orderedCrns.length === 0) {
3694
+ throw new Error("No compatible CRN is currently available for this tier.");
3732
3695
  }
3733
- this.trace("deploy:aleph-processed", inspection);
3734
- if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
3696
+ const attemptErrors = [];
3697
+ let lastError = null;
3698
+ for (const [candidateIndex, candidateCrn] of orderedCrns.entries()) {
3699
+ let attemptItemHash = null;
3735
3700
  this.emitProgress({
3736
- stage: "refreshing-instances",
3737
- label: "Publishing port forwards",
3738
- progress: 86,
3701
+ stage: "selecting-crn",
3702
+ label: "Selecting CRN",
3703
+ progress: 18,
3739
3704
  status: "info",
3740
- itemHash: result.itemHash,
3741
- detail: "Publishing the required Aleph port-forward aggregate from the manifest."
3742
- });
3743
- await ensureInstancePortForwards({
3744
- sender: this.state.wallet.address,
3745
- instanceItemHash: result.itemHash,
3746
- manifest: toSharedRootfsManifest(this.state.manifest),
3747
- signer: personalSign,
3748
- hasher: sha256Hex,
3749
- fetch: (url, init) => fetch(url, init),
3750
- apiHost: this.client.apiHost,
3751
- sync: true
3752
- });
3753
- this.trace("deploy:port-forwards-published", {
3754
- itemHash: result.itemHash,
3755
- requiredPortForwards: this.state.manifest.requiredPortForwards
3705
+ detail: `Trying ${candidateCrn.name ?? candidateCrn.hash} (${candidateIndex + 1}/${orderedCrns.length})`
3756
3706
  });
3757
- }
3758
- if (this.state.selectedCrn?.address) {
3759
- this.trace("deploy:notifying-crn", {
3760
- itemHash: result.itemHash,
3761
- crnName: this.state.selectedCrn.name,
3762
- crnHash: this.state.selectedCrn.hash,
3763
- crnUrl: this.state.selectedCrn.address
3764
- });
3765
- await notifyCrnAllocationWithRetry({
3766
- crnUrl: this.state.selectedCrn.address,
3767
- itemHash: result.itemHash,
3768
- fetch: (url, init) => fetch(url, init),
3769
- onProgress: (event) => {
3770
- this.emitProgress(event);
3707
+ try {
3708
+ const content = createInstanceContent({
3709
+ address: this.state.wallet.address,
3710
+ name: this.state.instanceName.trim(),
3711
+ sshPublicKey: this.state.sshPublicKey.trim(),
3712
+ rootfsItemHash: this.state.manifest.rootfsItemHash,
3713
+ rootfsSizeMiB: Math.max(
3714
+ this.state.manifest.rootfsSizeMiB,
3715
+ spec.diskMiB
3716
+ ),
3717
+ vcpus: spec.vcpus,
3718
+ memoryMiB: spec.memoryMiB,
3719
+ rootfsVersion: this.state.manifest.version,
3720
+ crnHash: candidateCrn.hash
3721
+ });
3722
+ const result = await deployInstance({
3723
+ sender: this.state.wallet.address,
3724
+ content,
3725
+ hasher: sha256Hex,
3726
+ signer: personalSign,
3727
+ fetch: (url, init) => fetch(url, init),
3728
+ apiHost: this.client.apiHost,
3729
+ sync: false,
3730
+ onProgress: (event) => {
3731
+ this.emitProgress(event);
3732
+ }
3733
+ });
3734
+ attemptItemHash = result.itemHash;
3735
+ this.patch({
3736
+ statusText: `Deployment submitted: ${result.itemHash}`,
3737
+ lastDeploymentHash: result.itemHash
3738
+ });
3739
+ this.trace("deploy:broadcasted", {
3740
+ ...result,
3741
+ crnHash: candidateCrn.hash,
3742
+ crnName: candidateCrn.name
3743
+ });
3744
+ this.lastRuntimeReadyHash = null;
3745
+ const inspection = await waitForDeploymentResult2(result.itemHash, {
3746
+ rootfsRef: this.state.manifest.rootfsItemHash,
3747
+ apiHost: this.client.apiHost,
3748
+ fetch: (url, init) => fetch(url, init),
3749
+ attempts: UI_DEPLOY_WAIT_ATTEMPTS,
3750
+ delayMs: UI_DEPLOY_WAIT_DELAY_MS,
3751
+ onAttempt: (inspectionResult) => {
3752
+ if (inspectionResult.status === "processed") {
3753
+ this.emitProgress({
3754
+ stage: "deployment-confirmed",
3755
+ label: "Deployment accepted by Aleph",
3756
+ progress: 82,
3757
+ status: "success",
3758
+ itemHash: result.itemHash,
3759
+ detail: "Aleph processed the instance message."
3760
+ });
3761
+ return;
3762
+ }
3763
+ if (inspectionResult.status === "rejected") {
3764
+ this.emitProgress({
3765
+ stage: "deployment-rejected",
3766
+ label: "Deployment rejected",
3767
+ progress: 100,
3768
+ status: "error",
3769
+ itemHash: result.itemHash,
3770
+ detail: inspectionResult.rejectionReason ?? "Aleph rejected the deployment.",
3771
+ error: inspectionResult.rejectionReason ?? "Aleph rejected the deployment."
3772
+ });
3773
+ return;
3774
+ }
3775
+ this.emitProgress({
3776
+ stage: "waiting-for-aleph",
3777
+ label: "Waiting for Aleph",
3778
+ progress: 76,
3779
+ status: "info",
3780
+ itemHash: result.itemHash,
3781
+ detail: "Deployment submitted. Waiting for Aleph to process the instance message."
3782
+ });
3783
+ }
3784
+ });
3785
+ if (inspection.status !== "processed") {
3786
+ throw new Error(
3787
+ inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
3788
+ );
3771
3789
  }
3772
- });
3773
- }
3774
- this.emitProgress({
3775
- stage: "deployment-confirmed",
3776
- label: "Waiting for runtime",
3777
- progress: 90,
3778
- status: "warning",
3779
- itemHash: result.itemHash,
3780
- detail: "Aleph accepted the deployment. Waiting for runtime networking and mapped ports."
3781
- });
3782
- const runtime = await waitForVmRuntime({
3783
- itemHash: result.itemHash,
3784
- fetch: (url, init) => fetch(url, init),
3785
- crnHash: this.state.selectedCrn?.hash,
3786
- crns: this.state.crns,
3787
- crnListUrl: this.props.crnListUrl,
3788
- attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3789
- delayMs: UI_RUNTIME_WAIT_DELAY_MS,
3790
- onAttempt: (runtime2) => {
3791
- if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
3790
+ this.trace("deploy:aleph-processed", inspection);
3791
+ if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
3792
3792
  this.emitProgress({
3793
- stage: "completed",
3794
- label: "Runtime ready",
3795
- progress: 100,
3796
- status: "success",
3793
+ stage: "refreshing-instances",
3794
+ label: "Publishing port forwards",
3795
+ progress: 86,
3796
+ status: "info",
3797
+ itemHash: result.itemHash,
3798
+ detail: "Publishing the required Aleph port-forward aggregate from the manifest."
3799
+ });
3800
+ await ensureInstancePortForwards({
3801
+ sender: this.state.wallet.address,
3802
+ instanceItemHash: result.itemHash,
3803
+ manifest: toSharedRootfsManifest(this.state.manifest),
3804
+ signer: personalSign,
3805
+ hasher: sha256Hex,
3806
+ fetch: (url, init) => fetch(url, init),
3807
+ apiHost: this.client.apiHost,
3808
+ sync: true
3809
+ });
3810
+ this.trace("deploy:port-forwards-published", {
3811
+ itemHash: result.itemHash,
3812
+ requiredPortForwards: this.state.manifest.requiredPortForwards
3813
+ });
3814
+ }
3815
+ if (candidateCrn.address) {
3816
+ this.trace("deploy:notifying-crn", {
3797
3817
  itemHash: result.itemHash,
3798
- detail: "Runtime networking and mapped ports are now available."
3818
+ crnName: candidateCrn.name,
3819
+ crnHash: candidateCrn.hash,
3820
+ crnUrl: candidateCrn.address
3821
+ });
3822
+ await notifyCrnAllocationWithRetry({
3823
+ crnUrl: candidateCrn.address,
3824
+ itemHash: result.itemHash,
3825
+ fetch: (url, init) => fetch(url, init),
3826
+ onProgress: (event) => {
3827
+ this.emitProgress(event);
3828
+ }
3799
3829
  });
3800
- return;
3801
3830
  }
3802
3831
  this.emitProgress({
3803
3832
  stage: "deployment-confirmed",
@@ -3805,46 +3834,122 @@ var SponsorRelayController = class {
3805
3834
  progress: 90,
3806
3835
  status: "warning",
3807
3836
  itemHash: result.itemHash,
3808
- detail: runtime2.diagnostics?.reason ?? "Waiting for CRN runtime networking and mapped ports."
3837
+ detail: "Aleph accepted the deployment. Waiting for runtime networking and mapped ports."
3838
+ });
3839
+ const runtime = await waitForVmRuntime({
3840
+ itemHash: result.itemHash,
3841
+ fetch: (url, init) => fetch(url, init),
3842
+ crnHash: candidateCrn.hash,
3843
+ crns: this.state.crns,
3844
+ crnListUrl: this.props.crnListUrl,
3845
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3846
+ delayMs: UI_RUNTIME_WAIT_DELAY_MS,
3847
+ onAttempt: (runtime2) => {
3848
+ if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
3849
+ this.emitProgress({
3850
+ stage: "completed",
3851
+ label: "Runtime ready",
3852
+ progress: 100,
3853
+ status: "success",
3854
+ itemHash: result.itemHash,
3855
+ detail: "Runtime networking and mapped ports are now available."
3856
+ });
3857
+ return;
3858
+ }
3859
+ this.emitProgress({
3860
+ stage: "deployment-confirmed",
3861
+ label: "Waiting for runtime",
3862
+ progress: 90,
3863
+ status: "warning",
3864
+ itemHash: result.itemHash,
3865
+ detail: runtime2.diagnostics?.reason ?? "Waiting for CRN runtime networking and mapped ports."
3866
+ });
3867
+ }
3868
+ });
3869
+ if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
3870
+ throw new Error(
3871
+ runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
3872
+ );
3873
+ }
3874
+ this.trace("deploy:runtime-ready", runtime);
3875
+ this.lastRuntimeReadyHash = result.itemHash;
3876
+ this.rememberRuntimeDetails(result.itemHash, {
3877
+ messageStatus: "processed",
3878
+ allocationSource: runtime.allocation?.source ?? null,
3879
+ crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
3880
+ hostIpv4: runtime.hostIpv4 ?? null,
3881
+ ipv6: runtime.ipv6 ?? null,
3882
+ vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
3883
+ webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
3884
+ sshCommand: runtime.sshCommand ?? null,
3885
+ mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
3886
+ execution: runtime.execution ?? null,
3887
+ error: null
3888
+ });
3889
+ await this.configureRelayBootstrapRegistration({
3890
+ itemHash: result.itemHash,
3891
+ runtime
3892
+ });
3893
+ this.patch({
3894
+ busy: { deploying: false }
3895
+ });
3896
+ this.emitProgress({
3897
+ stage: "refreshing-instances",
3898
+ label: "Refreshing instances",
3899
+ progress: 96,
3900
+ status: "info",
3901
+ itemHash: result.itemHash,
3902
+ detail: "Reloading deployments and runtime state."
3903
+ });
3904
+ await this.refresh();
3905
+ return;
3906
+ } catch (error) {
3907
+ lastError = error instanceof Error ? error : new Error(String(error));
3908
+ const attemptLabel = candidateCrn.name ?? candidateCrn.hash;
3909
+ attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
3910
+ this.trace("deploy:crn-attempt-error", {
3911
+ crnHash: candidateCrn.hash,
3912
+ crnName: candidateCrn.name,
3913
+ itemHash: attemptItemHash,
3914
+ error: lastError.message
3809
3915
  });
3916
+ if (attemptItemHash) {
3917
+ await forgetAlephMessages({
3918
+ sender: this.state.wallet.address,
3919
+ hashes: [attemptItemHash],
3920
+ reason: `Discard failed Sponsor Relay deployment attempt on ${attemptLabel}`,
3921
+ signer: personalSign,
3922
+ hasher: sha256Hex,
3923
+ fetch: (url, init) => fetch(url, init).then(async (response) => ({
3924
+ ok: response.ok,
3925
+ status: response.status,
3926
+ json: async () => await response.json()
3927
+ })),
3928
+ apiHost: this.client.apiHost
3929
+ }).catch((cleanupError) => {
3930
+ this.trace("deploy:cleanup-error", {
3931
+ itemHash: attemptItemHash,
3932
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
3933
+ });
3934
+ });
3935
+ }
3936
+ if (candidateIndex < orderedCrns.length - 1) {
3937
+ this.emitProgress({
3938
+ stage: "selecting-crn",
3939
+ label: "Retrying another CRN",
3940
+ progress: 20,
3941
+ status: "warning",
3942
+ itemHash: attemptItemHash,
3943
+ detail: `${attemptLabel} failed: ${lastError.message}`,
3944
+ error: lastError.message
3945
+ });
3946
+ continue;
3947
+ }
3810
3948
  }
3811
- });
3812
- if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
3813
- throw new Error(
3814
- runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
3815
- );
3816
3949
  }
3817
- this.trace("deploy:runtime-ready", runtime);
3818
- this.lastRuntimeReadyHash = result.itemHash;
3819
- this.rememberRuntimeDetails(result.itemHash, {
3820
- messageStatus: "processed",
3821
- allocationSource: runtime.allocation?.source ?? null,
3822
- crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
3823
- hostIpv4: runtime.hostIpv4 ?? null,
3824
- ipv6: runtime.ipv6 ?? null,
3825
- vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
3826
- webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
3827
- sshCommand: runtime.sshCommand ?? null,
3828
- mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
3829
- execution: runtime.execution ?? null,
3830
- error: null
3831
- });
3832
- await this.configureRelayBootstrapRegistration({
3833
- itemHash: result.itemHash,
3834
- runtime
3835
- });
3836
- this.patch({
3837
- busy: { deploying: false }
3838
- });
3839
- this.emitProgress({
3840
- stage: "refreshing-instances",
3841
- label: "Refreshing instances",
3842
- progress: 96,
3843
- status: "info",
3844
- itemHash: result.itemHash,
3845
- detail: "Reloading deployments and runtime state."
3846
- });
3847
- await this.refresh();
3950
+ throw new Error(
3951
+ attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
3952
+ );
3848
3953
  } catch (error) {
3849
3954
  this.trace("deploy:error", error);
3850
3955
  this.patch({
@@ -4103,6 +4208,7 @@ function launcherIndicator(state) {
4103
4208
  }
4104
4209
  function SponsorRelayFab(props) {
4105
4210
  const { controller, state } = useSponsorRelayController(props);
4211
+ const versionLabel = `v${package_default.version}`;
4106
4212
  const launcherMode = props.launcherMode ?? "floating";
4107
4213
  const indicator = launcherIndicator(state);
4108
4214
  const [successFlash, setSuccessFlash] = React.useState(false);
@@ -4333,16 +4439,33 @@ function SponsorRelayFab(props) {
4333
4439
  },
4334
4440
  children: [
4335
4441
  /* @__PURE__ */ jsxs("div", { children: [
4336
- /* @__PURE__ */ jsx(
4442
+ /* @__PURE__ */ jsxs(
4337
4443
  "div",
4338
4444
  {
4339
4445
  style: {
4446
+ display: "flex",
4447
+ alignItems: "center",
4448
+ gap: "0.45rem",
4340
4449
  color: "#9fb2ca",
4341
4450
  fontSize: "0.72rem",
4342
4451
  textTransform: "uppercase",
4343
4452
  letterSpacing: "0.08em"
4344
4453
  },
4345
- children: "Aleph VM credit deployer"
4454
+ children: [
4455
+ /* @__PURE__ */ jsx("span", { children: "Aleph VM credit deployer" }),
4456
+ /* @__PURE__ */ jsx(
4457
+ "span",
4458
+ {
4459
+ style: {
4460
+ fontSize: "0.62rem",
4461
+ letterSpacing: "0.04em",
4462
+ textTransform: "none",
4463
+ color: "rgba(191, 219, 254, 0.82)"
4464
+ },
4465
+ children: versionLabel
4466
+ }
4467
+ )
4468
+ ]
4346
4469
  }
4347
4470
  ),
4348
4471
  /* @__PURE__ */ jsx(
package/shared/index.js CHANGED
@@ -3645,158 +3645,156 @@ var SponsorRelayController = class {
3645
3645
  this.state.pricingSummary.pricing,
3646
3646
  this.state.pricingSummary.tier
3647
3647
  );
3648
- this.emitProgress({
3649
- stage: "selecting-crn",
3650
- label: "Selecting CRN",
3651
- progress: 18,
3652
- status: "info",
3653
- detail: this.state.selectedCrn?.name ?? this.state.selectedCrn?.hash ?? "Auto-selected compatible CRN"
3654
- });
3655
- const content = createInstanceContent({
3656
- address: this.state.wallet.address,
3657
- name: this.state.instanceName.trim(),
3658
- sshPublicKey: this.state.sshPublicKey.trim(),
3659
- rootfsItemHash: this.state.manifest.rootfsItemHash,
3660
- rootfsSizeMiB: Math.max(
3661
- this.state.manifest.rootfsSizeMiB,
3662
- spec.diskMiB
3663
- ),
3664
- vcpus: spec.vcpus,
3665
- memoryMiB: spec.memoryMiB,
3666
- rootfsVersion: this.state.manifest.version,
3667
- crnHash: this.state.selectedCrn?.hash
3668
- });
3669
- const result = await deployInstance({
3670
- sender: this.state.wallet.address,
3671
- content,
3672
- hasher: sha256Hex,
3673
- signer: personalSign,
3674
- fetch: (url, init) => fetch(url, init),
3675
- apiHost: this.client.apiHost,
3676
- sync: false,
3677
- onProgress: (event) => {
3678
- this.emitProgress(event);
3679
- }
3680
- });
3681
- this.patch({
3682
- statusText: `Deployment submitted: ${result.itemHash}`,
3683
- lastDeploymentHash: result.itemHash
3684
- });
3685
- this.trace("deploy:broadcasted", result);
3686
- this.lastRuntimeReadyHash = null;
3687
- const inspection = await waitForDeploymentResult2(result.itemHash, {
3688
- rootfsRef: this.state.manifest.rootfsItemHash,
3689
- apiHost: this.client.apiHost,
3690
- fetch: (url, init) => fetch(url, init),
3691
- attempts: UI_DEPLOY_WAIT_ATTEMPTS,
3692
- delayMs: UI_DEPLOY_WAIT_DELAY_MS,
3693
- onAttempt: (inspectionResult) => {
3694
- if (inspectionResult.status === "processed") {
3695
- this.emitProgress({
3696
- stage: "deployment-confirmed",
3697
- label: "Deployment accepted by Aleph",
3698
- progress: 82,
3699
- status: "success",
3700
- itemHash: result.itemHash,
3701
- detail: "Aleph processed the instance message."
3702
- });
3703
- return;
3704
- }
3705
- if (inspectionResult.status === "rejected") {
3706
- this.emitProgress({
3707
- stage: "deployment-rejected",
3708
- label: "Deployment rejected",
3709
- progress: 100,
3710
- status: "error",
3711
- itemHash: result.itemHash,
3712
- detail: inspectionResult.rejectionReason ?? "Aleph rejected the deployment.",
3713
- error: inspectionResult.rejectionReason ?? "Aleph rejected the deployment."
3714
- });
3715
- return;
3716
- }
3717
- this.emitProgress({
3718
- stage: "waiting-for-aleph",
3719
- label: "Waiting for Aleph",
3720
- progress: 76,
3721
- status: "info",
3722
- itemHash: result.itemHash,
3723
- detail: "Deployment submitted. Waiting for Aleph to process the instance message."
3724
- });
3648
+ const compatibleCrns2 = compatibleCrnsForTier(this.state.crns, {
3649
+ ...this.state,
3650
+ pricingSummary: {
3651
+ ...this.state.pricingSummary,
3652
+ pricing: this.state.pricingSummary.pricing,
3653
+ tier: this.state.pricingSummary.tier
3725
3654
  }
3726
3655
  });
3727
- if (inspection.status !== "processed") {
3728
- throw new Error(
3729
- inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
3730
- );
3656
+ const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
3657
+ const orderedCrns = [
3658
+ ...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
3659
+ ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
3660
+ ].slice(0, 5);
3661
+ if (orderedCrns.length === 0) {
3662
+ throw new Error("No compatible CRN is currently available for this tier.");
3731
3663
  }
3732
- this.trace("deploy:aleph-processed", inspection);
3733
- if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
3664
+ const attemptErrors = [];
3665
+ let lastError = null;
3666
+ for (const [candidateIndex, candidateCrn] of orderedCrns.entries()) {
3667
+ let attemptItemHash = null;
3734
3668
  this.emitProgress({
3735
- stage: "refreshing-instances",
3736
- label: "Publishing port forwards",
3737
- progress: 86,
3669
+ stage: "selecting-crn",
3670
+ label: "Selecting CRN",
3671
+ progress: 18,
3738
3672
  status: "info",
3739
- itemHash: result.itemHash,
3740
- detail: "Publishing the required Aleph port-forward aggregate from the manifest."
3741
- });
3742
- await ensureInstancePortForwards({
3743
- sender: this.state.wallet.address,
3744
- instanceItemHash: result.itemHash,
3745
- manifest: toSharedRootfsManifest(this.state.manifest),
3746
- signer: personalSign,
3747
- hasher: sha256Hex,
3748
- fetch: (url, init) => fetch(url, init),
3749
- apiHost: this.client.apiHost,
3750
- sync: true
3751
- });
3752
- this.trace("deploy:port-forwards-published", {
3753
- itemHash: result.itemHash,
3754
- requiredPortForwards: this.state.manifest.requiredPortForwards
3673
+ detail: `Trying ${candidateCrn.name ?? candidateCrn.hash} (${candidateIndex + 1}/${orderedCrns.length})`
3755
3674
  });
3756
- }
3757
- if (this.state.selectedCrn?.address) {
3758
- this.trace("deploy:notifying-crn", {
3759
- itemHash: result.itemHash,
3760
- crnName: this.state.selectedCrn.name,
3761
- crnHash: this.state.selectedCrn.hash,
3762
- crnUrl: this.state.selectedCrn.address
3763
- });
3764
- await notifyCrnAllocationWithRetry({
3765
- crnUrl: this.state.selectedCrn.address,
3766
- itemHash: result.itemHash,
3767
- fetch: (url, init) => fetch(url, init),
3768
- onProgress: (event) => {
3769
- this.emitProgress(event);
3675
+ try {
3676
+ const content = createInstanceContent({
3677
+ address: this.state.wallet.address,
3678
+ name: this.state.instanceName.trim(),
3679
+ sshPublicKey: this.state.sshPublicKey.trim(),
3680
+ rootfsItemHash: this.state.manifest.rootfsItemHash,
3681
+ rootfsSizeMiB: Math.max(
3682
+ this.state.manifest.rootfsSizeMiB,
3683
+ spec.diskMiB
3684
+ ),
3685
+ vcpus: spec.vcpus,
3686
+ memoryMiB: spec.memoryMiB,
3687
+ rootfsVersion: this.state.manifest.version,
3688
+ crnHash: candidateCrn.hash
3689
+ });
3690
+ const result = await deployInstance({
3691
+ sender: this.state.wallet.address,
3692
+ content,
3693
+ hasher: sha256Hex,
3694
+ signer: personalSign,
3695
+ fetch: (url, init) => fetch(url, init),
3696
+ apiHost: this.client.apiHost,
3697
+ sync: false,
3698
+ onProgress: (event) => {
3699
+ this.emitProgress(event);
3700
+ }
3701
+ });
3702
+ attemptItemHash = result.itemHash;
3703
+ this.patch({
3704
+ statusText: `Deployment submitted: ${result.itemHash}`,
3705
+ lastDeploymentHash: result.itemHash
3706
+ });
3707
+ this.trace("deploy:broadcasted", {
3708
+ ...result,
3709
+ crnHash: candidateCrn.hash,
3710
+ crnName: candidateCrn.name
3711
+ });
3712
+ this.lastRuntimeReadyHash = null;
3713
+ const inspection = await waitForDeploymentResult2(result.itemHash, {
3714
+ rootfsRef: this.state.manifest.rootfsItemHash,
3715
+ apiHost: this.client.apiHost,
3716
+ fetch: (url, init) => fetch(url, init),
3717
+ attempts: UI_DEPLOY_WAIT_ATTEMPTS,
3718
+ delayMs: UI_DEPLOY_WAIT_DELAY_MS,
3719
+ onAttempt: (inspectionResult) => {
3720
+ if (inspectionResult.status === "processed") {
3721
+ this.emitProgress({
3722
+ stage: "deployment-confirmed",
3723
+ label: "Deployment accepted by Aleph",
3724
+ progress: 82,
3725
+ status: "success",
3726
+ itemHash: result.itemHash,
3727
+ detail: "Aleph processed the instance message."
3728
+ });
3729
+ return;
3730
+ }
3731
+ if (inspectionResult.status === "rejected") {
3732
+ this.emitProgress({
3733
+ stage: "deployment-rejected",
3734
+ label: "Deployment rejected",
3735
+ progress: 100,
3736
+ status: "error",
3737
+ itemHash: result.itemHash,
3738
+ detail: inspectionResult.rejectionReason ?? "Aleph rejected the deployment.",
3739
+ error: inspectionResult.rejectionReason ?? "Aleph rejected the deployment."
3740
+ });
3741
+ return;
3742
+ }
3743
+ this.emitProgress({
3744
+ stage: "waiting-for-aleph",
3745
+ label: "Waiting for Aleph",
3746
+ progress: 76,
3747
+ status: "info",
3748
+ itemHash: result.itemHash,
3749
+ detail: "Deployment submitted. Waiting for Aleph to process the instance message."
3750
+ });
3751
+ }
3752
+ });
3753
+ if (inspection.status !== "processed") {
3754
+ throw new Error(
3755
+ inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
3756
+ );
3770
3757
  }
3771
- });
3772
- }
3773
- this.emitProgress({
3774
- stage: "deployment-confirmed",
3775
- label: "Waiting for runtime",
3776
- progress: 90,
3777
- status: "warning",
3778
- itemHash: result.itemHash,
3779
- detail: "Aleph accepted the deployment. Waiting for runtime networking and mapped ports."
3780
- });
3781
- const runtime = await waitForVmRuntime({
3782
- itemHash: result.itemHash,
3783
- fetch: (url, init) => fetch(url, init),
3784
- crnHash: this.state.selectedCrn?.hash,
3785
- crns: this.state.crns,
3786
- crnListUrl: this.props.crnListUrl,
3787
- attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3788
- delayMs: UI_RUNTIME_WAIT_DELAY_MS,
3789
- onAttempt: (runtime2) => {
3790
- if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
3758
+ this.trace("deploy:aleph-processed", inspection);
3759
+ if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
3791
3760
  this.emitProgress({
3792
- stage: "completed",
3793
- label: "Runtime ready",
3794
- progress: 100,
3795
- status: "success",
3761
+ stage: "refreshing-instances",
3762
+ label: "Publishing port forwards",
3763
+ progress: 86,
3764
+ status: "info",
3765
+ itemHash: result.itemHash,
3766
+ detail: "Publishing the required Aleph port-forward aggregate from the manifest."
3767
+ });
3768
+ await ensureInstancePortForwards({
3769
+ sender: this.state.wallet.address,
3770
+ instanceItemHash: result.itemHash,
3771
+ manifest: toSharedRootfsManifest(this.state.manifest),
3772
+ signer: personalSign,
3773
+ hasher: sha256Hex,
3774
+ fetch: (url, init) => fetch(url, init),
3775
+ apiHost: this.client.apiHost,
3776
+ sync: true
3777
+ });
3778
+ this.trace("deploy:port-forwards-published", {
3779
+ itemHash: result.itemHash,
3780
+ requiredPortForwards: this.state.manifest.requiredPortForwards
3781
+ });
3782
+ }
3783
+ if (candidateCrn.address) {
3784
+ this.trace("deploy:notifying-crn", {
3785
+ itemHash: result.itemHash,
3786
+ crnName: candidateCrn.name,
3787
+ crnHash: candidateCrn.hash,
3788
+ crnUrl: candidateCrn.address
3789
+ });
3790
+ await notifyCrnAllocationWithRetry({
3791
+ crnUrl: candidateCrn.address,
3796
3792
  itemHash: result.itemHash,
3797
- detail: "Runtime networking and mapped ports are now available."
3793
+ fetch: (url, init) => fetch(url, init),
3794
+ onProgress: (event) => {
3795
+ this.emitProgress(event);
3796
+ }
3798
3797
  });
3799
- return;
3800
3798
  }
3801
3799
  this.emitProgress({
3802
3800
  stage: "deployment-confirmed",
@@ -3804,46 +3802,122 @@ var SponsorRelayController = class {
3804
3802
  progress: 90,
3805
3803
  status: "warning",
3806
3804
  itemHash: result.itemHash,
3807
- detail: runtime2.diagnostics?.reason ?? "Waiting for CRN runtime networking and mapped ports."
3805
+ detail: "Aleph accepted the deployment. Waiting for runtime networking and mapped ports."
3806
+ });
3807
+ const runtime = await waitForVmRuntime({
3808
+ itemHash: result.itemHash,
3809
+ fetch: (url, init) => fetch(url, init),
3810
+ crnHash: candidateCrn.hash,
3811
+ crns: this.state.crns,
3812
+ crnListUrl: this.props.crnListUrl,
3813
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3814
+ delayMs: UI_RUNTIME_WAIT_DELAY_MS,
3815
+ onAttempt: (runtime2) => {
3816
+ if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
3817
+ this.emitProgress({
3818
+ stage: "completed",
3819
+ label: "Runtime ready",
3820
+ progress: 100,
3821
+ status: "success",
3822
+ itemHash: result.itemHash,
3823
+ detail: "Runtime networking and mapped ports are now available."
3824
+ });
3825
+ return;
3826
+ }
3827
+ this.emitProgress({
3828
+ stage: "deployment-confirmed",
3829
+ label: "Waiting for runtime",
3830
+ progress: 90,
3831
+ status: "warning",
3832
+ itemHash: result.itemHash,
3833
+ detail: runtime2.diagnostics?.reason ?? "Waiting for CRN runtime networking and mapped ports."
3834
+ });
3835
+ }
3836
+ });
3837
+ if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
3838
+ throw new Error(
3839
+ runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
3840
+ );
3841
+ }
3842
+ this.trace("deploy:runtime-ready", runtime);
3843
+ this.lastRuntimeReadyHash = result.itemHash;
3844
+ this.rememberRuntimeDetails(result.itemHash, {
3845
+ messageStatus: "processed",
3846
+ allocationSource: runtime.allocation?.source ?? null,
3847
+ crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
3848
+ hostIpv4: runtime.hostIpv4 ?? null,
3849
+ ipv6: runtime.ipv6 ?? null,
3850
+ vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
3851
+ webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
3852
+ sshCommand: runtime.sshCommand ?? null,
3853
+ mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
3854
+ execution: runtime.execution ?? null,
3855
+ error: null
3856
+ });
3857
+ await this.configureRelayBootstrapRegistration({
3858
+ itemHash: result.itemHash,
3859
+ runtime
3860
+ });
3861
+ this.patch({
3862
+ busy: { deploying: false }
3863
+ });
3864
+ this.emitProgress({
3865
+ stage: "refreshing-instances",
3866
+ label: "Refreshing instances",
3867
+ progress: 96,
3868
+ status: "info",
3869
+ itemHash: result.itemHash,
3870
+ detail: "Reloading deployments and runtime state."
3808
3871
  });
3872
+ await this.refresh();
3873
+ return;
3874
+ } catch (error) {
3875
+ lastError = error instanceof Error ? error : new Error(String(error));
3876
+ const attemptLabel = candidateCrn.name ?? candidateCrn.hash;
3877
+ attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
3878
+ this.trace("deploy:crn-attempt-error", {
3879
+ crnHash: candidateCrn.hash,
3880
+ crnName: candidateCrn.name,
3881
+ itemHash: attemptItemHash,
3882
+ error: lastError.message
3883
+ });
3884
+ if (attemptItemHash) {
3885
+ await forgetAlephMessages({
3886
+ sender: this.state.wallet.address,
3887
+ hashes: [attemptItemHash],
3888
+ reason: `Discard failed Sponsor Relay deployment attempt on ${attemptLabel}`,
3889
+ signer: personalSign,
3890
+ hasher: sha256Hex,
3891
+ fetch: (url, init) => fetch(url, init).then(async (response) => ({
3892
+ ok: response.ok,
3893
+ status: response.status,
3894
+ json: async () => await response.json()
3895
+ })),
3896
+ apiHost: this.client.apiHost
3897
+ }).catch((cleanupError) => {
3898
+ this.trace("deploy:cleanup-error", {
3899
+ itemHash: attemptItemHash,
3900
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
3901
+ });
3902
+ });
3903
+ }
3904
+ if (candidateIndex < orderedCrns.length - 1) {
3905
+ this.emitProgress({
3906
+ stage: "selecting-crn",
3907
+ label: "Retrying another CRN",
3908
+ progress: 20,
3909
+ status: "warning",
3910
+ itemHash: attemptItemHash,
3911
+ detail: `${attemptLabel} failed: ${lastError.message}`,
3912
+ error: lastError.message
3913
+ });
3914
+ continue;
3915
+ }
3809
3916
  }
3810
- });
3811
- if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
3812
- throw new Error(
3813
- runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
3814
- );
3815
3917
  }
3816
- this.trace("deploy:runtime-ready", runtime);
3817
- this.lastRuntimeReadyHash = result.itemHash;
3818
- this.rememberRuntimeDetails(result.itemHash, {
3819
- messageStatus: "processed",
3820
- allocationSource: runtime.allocation?.source ?? null,
3821
- crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
3822
- hostIpv4: runtime.hostIpv4 ?? null,
3823
- ipv6: runtime.ipv6 ?? null,
3824
- vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
3825
- webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
3826
- sshCommand: runtime.sshCommand ?? null,
3827
- mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
3828
- execution: runtime.execution ?? null,
3829
- error: null
3830
- });
3831
- await this.configureRelayBootstrapRegistration({
3832
- itemHash: result.itemHash,
3833
- runtime
3834
- });
3835
- this.patch({
3836
- busy: { deploying: false }
3837
- });
3838
- this.emitProgress({
3839
- stage: "refreshing-instances",
3840
- label: "Refreshing instances",
3841
- progress: 96,
3842
- status: "info",
3843
- itemHash: result.itemHash,
3844
- detail: "Reloading deployments and runtime state."
3845
- });
3846
- await this.refresh();
3918
+ throw new Error(
3919
+ attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
3920
+ );
3847
3921
  } catch (error) {
3848
3922
  this.trace("deploy:error", error);
3849
3923
  this.patch({
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import { onDestroy, onMount } from 'svelte'
3
+ import packageJson from '../../package.json'
3
4
 
4
5
  import { createSponsorRelayController, formatDateTime, formatNumber, formatTierSpecLabel, joinMappedPorts, joinRequiredPortForwards, shortHash } from '../shared/index'
5
6
  import AccordionSection from './components/AccordionSection.svelte'
@@ -37,6 +38,7 @@ export let apiHost = undefined
37
38
  })
38
39
 
39
40
  let state = controller.getState()
41
+ const versionLabel = `v${packageJson.version}`
40
42
 
41
43
  onMount(async () => {
42
44
  const unsubscribe = controller.subscribe((next) => {
@@ -62,7 +64,7 @@ export let apiHost = undefined
62
64
  <aside class="panel">
63
65
  <div class="panel-head">
64
66
  <div>
65
- <p class="eyebrow">Aleph VM credit deployer</p>
67
+ <p class="eyebrow">Aleph VM credit deployer <span class="eyebrow-version">{versionLabel}</span></p>
66
68
  <h2>Sponsor Relay</h2>
67
69
  </div>
68
70
  <button class="refresh" type="button" on:click={() => controller.refresh()} disabled={state.busy.refreshing}>
@@ -322,6 +324,13 @@ export let apiHost = undefined
322
324
  letter-spacing: 0.08em;
323
325
  }
324
326
 
327
+ .eyebrow-version {
328
+ font-size: 0.62rem;
329
+ letter-spacing: 0.04em;
330
+ text-transform: none;
331
+ color: rgba(191, 219, 254, 0.82);
332
+ }
333
+
325
334
  h2,
326
335
  h3,
327
336
  strong {