@le-space/ui 0.3.7 → 0.3.9

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.7",
3
+ "version": "0.3.9",
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.7",
40
- "@le-space/core": "0.3.7",
39
+ "@le-space/browser": "0.3.9",
40
+ "@le-space/core": "0.3.9",
41
41
  "viem": "^2.38.5"
42
42
  },
43
43
  "peerDependencies": {
package/react/index.js CHANGED
@@ -4,7 +4,7 @@ import React from "react";
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "@le-space/ui",
7
- version: "0.3.7",
7
+ version: "0.3.9",
8
8
  description: "Floating Aleph sponsor relay UI components for Svelte and React.",
9
9
  license: "MIT",
10
10
  type: "module",
@@ -3677,158 +3677,156 @@ var SponsorRelayController = class {
3677
3677
  this.state.pricingSummary.pricing,
3678
3678
  this.state.pricingSummary.tier
3679
3679
  );
3680
- this.emitProgress({
3681
- stage: "selecting-crn",
3682
- label: "Selecting CRN",
3683
- progress: 18,
3684
- status: "info",
3685
- detail: this.state.selectedCrn?.name ?? this.state.selectedCrn?.hash ?? "Auto-selected compatible CRN"
3686
- });
3687
- const content = createInstanceContent({
3688
- address: this.state.wallet.address,
3689
- name: this.state.instanceName.trim(),
3690
- sshPublicKey: this.state.sshPublicKey.trim(),
3691
- rootfsItemHash: this.state.manifest.rootfsItemHash,
3692
- rootfsSizeMiB: Math.max(
3693
- this.state.manifest.rootfsSizeMiB,
3694
- spec.diskMiB
3695
- ),
3696
- vcpus: spec.vcpus,
3697
- memoryMiB: spec.memoryMiB,
3698
- rootfsVersion: this.state.manifest.version,
3699
- crnHash: this.state.selectedCrn?.hash
3700
- });
3701
- const result = await deployInstance({
3702
- sender: this.state.wallet.address,
3703
- content,
3704
- hasher: sha256Hex,
3705
- signer: personalSign,
3706
- fetch: (url, init) => fetch(url, init),
3707
- apiHost: this.client.apiHost,
3708
- sync: false,
3709
- onProgress: (event) => {
3710
- this.emitProgress(event);
3711
- }
3712
- });
3713
- this.patch({
3714
- statusText: `Deployment submitted: ${result.itemHash}`,
3715
- lastDeploymentHash: result.itemHash
3716
- });
3717
- this.trace("deploy:broadcasted", result);
3718
- this.lastRuntimeReadyHash = null;
3719
- const inspection = await waitForDeploymentResult2(result.itemHash, {
3720
- rootfsRef: this.state.manifest.rootfsItemHash,
3721
- apiHost: this.client.apiHost,
3722
- fetch: (url, init) => fetch(url, init),
3723
- attempts: UI_DEPLOY_WAIT_ATTEMPTS,
3724
- delayMs: UI_DEPLOY_WAIT_DELAY_MS,
3725
- onAttempt: (inspectionResult) => {
3726
- if (inspectionResult.status === "processed") {
3727
- this.emitProgress({
3728
- stage: "deployment-confirmed",
3729
- label: "Deployment accepted by Aleph",
3730
- progress: 82,
3731
- status: "success",
3732
- itemHash: result.itemHash,
3733
- detail: "Aleph processed the instance message."
3734
- });
3735
- return;
3736
- }
3737
- if (inspectionResult.status === "rejected") {
3738
- this.emitProgress({
3739
- stage: "deployment-rejected",
3740
- label: "Deployment rejected",
3741
- progress: 100,
3742
- status: "error",
3743
- itemHash: result.itemHash,
3744
- detail: inspectionResult.rejectionReason ?? "Aleph rejected the deployment.",
3745
- error: inspectionResult.rejectionReason ?? "Aleph rejected the deployment."
3746
- });
3747
- return;
3748
- }
3749
- this.emitProgress({
3750
- stage: "waiting-for-aleph",
3751
- label: "Waiting for Aleph",
3752
- progress: 76,
3753
- status: "info",
3754
- itemHash: result.itemHash,
3755
- detail: "Deployment submitted. Waiting for Aleph to process the instance message."
3756
- });
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
3757
3686
  }
3758
3687
  });
3759
- if (inspection.status !== "processed") {
3760
- throw new Error(
3761
- inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
3762
- );
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.");
3763
3695
  }
3764
- this.trace("deploy:aleph-processed", inspection);
3765
- 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;
3766
3700
  this.emitProgress({
3767
- stage: "refreshing-instances",
3768
- label: "Publishing port forwards",
3769
- progress: 86,
3701
+ stage: "selecting-crn",
3702
+ label: "Selecting CRN",
3703
+ progress: 18,
3770
3704
  status: "info",
3771
- itemHash: result.itemHash,
3772
- detail: "Publishing the required Aleph port-forward aggregate from the manifest."
3773
- });
3774
- await ensureInstancePortForwards({
3775
- sender: this.state.wallet.address,
3776
- instanceItemHash: result.itemHash,
3777
- manifest: toSharedRootfsManifest(this.state.manifest),
3778
- signer: personalSign,
3779
- hasher: sha256Hex,
3780
- fetch: (url, init) => fetch(url, init),
3781
- apiHost: this.client.apiHost,
3782
- sync: true
3783
- });
3784
- this.trace("deploy:port-forwards-published", {
3785
- itemHash: result.itemHash,
3786
- requiredPortForwards: this.state.manifest.requiredPortForwards
3705
+ detail: `Trying ${candidateCrn.name ?? candidateCrn.hash} (${candidateIndex + 1}/${orderedCrns.length})`
3787
3706
  });
3788
- }
3789
- if (this.state.selectedCrn?.address) {
3790
- this.trace("deploy:notifying-crn", {
3791
- itemHash: result.itemHash,
3792
- crnName: this.state.selectedCrn.name,
3793
- crnHash: this.state.selectedCrn.hash,
3794
- crnUrl: this.state.selectedCrn.address
3795
- });
3796
- await notifyCrnAllocationWithRetry({
3797
- crnUrl: this.state.selectedCrn.address,
3798
- itemHash: result.itemHash,
3799
- fetch: (url, init) => fetch(url, init),
3800
- onProgress: (event) => {
3801
- 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
+ );
3802
3789
  }
3803
- });
3804
- }
3805
- this.emitProgress({
3806
- stage: "deployment-confirmed",
3807
- label: "Waiting for runtime",
3808
- progress: 90,
3809
- status: "warning",
3810
- itemHash: result.itemHash,
3811
- detail: "Aleph accepted the deployment. Waiting for runtime networking and mapped ports."
3812
- });
3813
- const runtime = await waitForVmRuntime({
3814
- itemHash: result.itemHash,
3815
- fetch: (url, init) => fetch(url, init),
3816
- crnHash: this.state.selectedCrn?.hash,
3817
- crns: this.state.crns,
3818
- crnListUrl: this.props.crnListUrl,
3819
- attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3820
- delayMs: UI_RUNTIME_WAIT_DELAY_MS,
3821
- onAttempt: (runtime2) => {
3822
- 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) {
3823
3792
  this.emitProgress({
3824
- stage: "completed",
3825
- label: "Runtime ready",
3826
- progress: 100,
3827
- 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", {
3817
+ itemHash: result.itemHash,
3818
+ crnName: candidateCrn.name,
3819
+ crnHash: candidateCrn.hash,
3820
+ crnUrl: candidateCrn.address
3821
+ });
3822
+ await notifyCrnAllocationWithRetry({
3823
+ crnUrl: candidateCrn.address,
3828
3824
  itemHash: result.itemHash,
3829
- detail: "Runtime networking and mapped ports are now available."
3825
+ fetch: (url, init) => fetch(url, init),
3826
+ onProgress: (event) => {
3827
+ this.emitProgress(event);
3828
+ }
3830
3829
  });
3831
- return;
3832
3830
  }
3833
3831
  this.emitProgress({
3834
3832
  stage: "deployment-confirmed",
@@ -3836,46 +3834,122 @@ var SponsorRelayController = class {
3836
3834
  progress: 90,
3837
3835
  status: "warning",
3838
3836
  itemHash: result.itemHash,
3839
- 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."
3840
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
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
+ }
3841
3948
  }
3842
- });
3843
- if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
3844
- throw new Error(
3845
- runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
3846
- );
3847
3949
  }
3848
- this.trace("deploy:runtime-ready", runtime);
3849
- this.lastRuntimeReadyHash = result.itemHash;
3850
- this.rememberRuntimeDetails(result.itemHash, {
3851
- messageStatus: "processed",
3852
- allocationSource: runtime.allocation?.source ?? null,
3853
- crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
3854
- hostIpv4: runtime.hostIpv4 ?? null,
3855
- ipv6: runtime.ipv6 ?? null,
3856
- vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
3857
- webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
3858
- sshCommand: runtime.sshCommand ?? null,
3859
- mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
3860
- execution: runtime.execution ?? null,
3861
- error: null
3862
- });
3863
- await this.configureRelayBootstrapRegistration({
3864
- itemHash: result.itemHash,
3865
- runtime
3866
- });
3867
- this.patch({
3868
- busy: { deploying: false }
3869
- });
3870
- this.emitProgress({
3871
- stage: "refreshing-instances",
3872
- label: "Refreshing instances",
3873
- progress: 96,
3874
- status: "info",
3875
- itemHash: result.itemHash,
3876
- detail: "Reloading deployments and runtime state."
3877
- });
3878
- await this.refresh();
3950
+ throw new Error(
3951
+ attemptErrors.length > 0 ? `All compatible CRNs failed. ${attemptErrors.join(" | ")}` : lastError?.message ?? "All compatible CRNs failed."
3952
+ );
3879
3953
  } catch (error) {
3880
3954
  this.trace("deploy:error", error);
3881
3955
  this.patch({
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,6 +1,5 @@
1
1
  <script>
2
2
  import { onDestroy, onMount } from 'svelte'
3
- import packageJson from '../../package.json'
4
3
 
5
4
  import { createSponsorRelayController, formatDateTime, formatNumber, formatTierSpecLabel, joinMappedPorts, joinRequiredPortForwards, shortHash } from '../shared/index'
6
5
  import AccordionSection from './components/AccordionSection.svelte'
@@ -17,6 +16,7 @@ export let instanceName = 'sponsor-relay'
17
16
  export let showInstances = true
18
17
  export let openByDefault = false
19
18
  export let launcherMode = 'floating'
19
+ export let version = ''
20
20
  export let apiHost = undefined
21
21
  export let crnListUrl = undefined
22
22
  export let schedulerApiHost = undefined
@@ -38,7 +38,9 @@ export let apiHost = undefined
38
38
  })
39
39
 
40
40
  let state = controller.getState()
41
- const versionLabel = `v${packageJson.version}`
41
+ const versionLabel = version.trim()
42
+ ? (version.trim().startsWith('v') ? version.trim() : `v${version.trim()}`)
43
+ : ''
42
44
 
43
45
  onMount(async () => {
44
46
  const unsubscribe = controller.subscribe((next) => {
@@ -64,7 +66,12 @@ export let apiHost = undefined
64
66
  <aside class="panel">
65
67
  <div class="panel-head">
66
68
  <div>
67
- <p class="eyebrow">Aleph VM credit deployer <span class="eyebrow-version">{versionLabel}</span></p>
69
+ <p class="eyebrow">
70
+ Aleph VM credit deployer
71
+ {#if versionLabel}
72
+ <span class="eyebrow-version">{versionLabel}</span>
73
+ {/if}
74
+ </p>
68
75
  <h2>Sponsor Relay</h2>
69
76
  </div>
70
77
  <button class="refresh" type="button" on:click={() => controller.refresh()} disabled={state.busy.refreshing}>