@le-space/ui 0.4.2 → 0.4.4
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 +3 -3
- package/react/index.d.ts +3 -0
- package/react/index.js +346 -16
- package/shared/index.d.ts +4 -1
- package/shared/index.js +345 -16
- package/svelte/SponsorRelayFab.svelte +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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.4.
|
|
40
|
-
"@le-space/core": "0.4.
|
|
39
|
+
"@le-space/browser": "0.4.4",
|
|
40
|
+
"@le-space/core": "0.4.4",
|
|
41
41
|
"viem": "^2.38.5"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
package/react/index.d.ts
CHANGED
|
@@ -256,6 +256,8 @@ interface CompactBootstrapRegistrationRecord {
|
|
|
256
256
|
registrationId?: string;
|
|
257
257
|
multiaddrs: string[];
|
|
258
258
|
browserMultiaddrs?: string[];
|
|
259
|
+
ownerAddress?: string;
|
|
260
|
+
publisherAddress?: string;
|
|
259
261
|
updatedAt: number;
|
|
260
262
|
} | null;
|
|
261
263
|
}
|
|
@@ -320,6 +322,7 @@ declare class SponsorRelayController {
|
|
|
320
322
|
private patch;
|
|
321
323
|
private emitProgress;
|
|
322
324
|
private configureRelayBootstrapRegistration;
|
|
325
|
+
private cleanupStaleVmBootstrapConfigHistory;
|
|
323
326
|
private syncLatestDeploymentProgress;
|
|
324
327
|
private canSkipRuntimeRefresh;
|
|
325
328
|
private noteRuntimeRefreshResult;
|
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.4.
|
|
31
|
+
var UI_PACKAGE_VERSION = "0.4.4";
|
|
32
32
|
|
|
33
33
|
// src/shared/format.ts
|
|
34
34
|
function shortHash(value, head = 8, tail = 6) {
|
|
@@ -1741,14 +1741,15 @@ function describeRuntimeAvailability(runtime) {
|
|
|
1741
1741
|
const mappedPorts2 = runtime.mappedPorts ?? {};
|
|
1742
1742
|
const hostIpv4 = runtime.hostIpv4 ?? null;
|
|
1743
1743
|
const ipv6 = runtime.ipv6 ?? null;
|
|
1744
|
+
const executionGuestIpv6 = execution?.networking?.ipv6_ip ?? null;
|
|
1744
1745
|
const proxyUrl = runtime.proxyUrl ?? null;
|
|
1745
1746
|
const schedulerSource = runtime.allocation?.source ?? null;
|
|
1746
1747
|
const webAccessActive = runtime.webAccess?.active ?? null;
|
|
1747
1748
|
const mappedPortCount = Object.keys(mappedPorts2).length;
|
|
1748
|
-
if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && !isLikelyGlobalIpv6(
|
|
1749
|
+
if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && executionGuestIpv6 && !isLikelyGlobalIpv6(executionGuestIpv6)) {
|
|
1749
1750
|
return {
|
|
1750
1751
|
state: "execution-invalid-public-ipv6",
|
|
1751
|
-
reason: `The CRN exposed runtime networking, but the guest IPv6
|
|
1752
|
+
reason: `The CRN exposed runtime networking, but the guest IPv6 (${executionGuestIpv6}) is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
|
|
1752
1753
|
schedulerSource,
|
|
1753
1754
|
executionSeen: Boolean(execution),
|
|
1754
1755
|
webAccessActive,
|
|
@@ -2698,6 +2699,7 @@ async function publishRelayBootstrapRegistration(args) {
|
|
|
2698
2699
|
var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
|
|
2699
2700
|
var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
|
|
2700
2701
|
var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
|
|
2702
|
+
var VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS = 6 * 60 * 60 * 1e3;
|
|
2701
2703
|
function parseJsonObject(value) {
|
|
2702
2704
|
try {
|
|
2703
2705
|
const parsed = JSON.parse(value);
|
|
@@ -2706,6 +2708,44 @@ function parseJsonObject(value) {
|
|
|
2706
2708
|
return null;
|
|
2707
2709
|
}
|
|
2708
2710
|
}
|
|
2711
|
+
function parseTimestampMs(value) {
|
|
2712
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2713
|
+
return value > 1e12 ? value : value * 1e3;
|
|
2714
|
+
}
|
|
2715
|
+
if (typeof value === "string" && value.trim()) {
|
|
2716
|
+
const numeric = Number(value);
|
|
2717
|
+
if (Number.isFinite(numeric)) {
|
|
2718
|
+
return numeric > 1e12 ? numeric : numeric * 1e3;
|
|
2719
|
+
}
|
|
2720
|
+
const parsed = Date.parse(value);
|
|
2721
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2722
|
+
}
|
|
2723
|
+
return null;
|
|
2724
|
+
}
|
|
2725
|
+
function normalizeVmBootstrapConfigRecord(deploymentTokenKey, value, options = {}) {
|
|
2726
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2727
|
+
const candidate = value;
|
|
2728
|
+
const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
|
|
2729
|
+
const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
|
|
2730
|
+
const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
|
|
2731
|
+
const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
|
|
2732
|
+
const createdAt = typeof candidate.createdAt === "string" ? candidate.createdAt.trim() : "";
|
|
2733
|
+
const expiresAt = typeof candidate.expiresAt === "string" ? candidate.expiresAt.trim() : "";
|
|
2734
|
+
if (!deploymentTokenKey.trim() || !deploymentToken || deploymentToken !== deploymentTokenKey.trim() || !profile || !ownerAddress || !instanceItemHash || !createdAt || !expiresAt) {
|
|
2735
|
+
return null;
|
|
2736
|
+
}
|
|
2737
|
+
const nowMs = Math.max(0, Number(options.nowMs ?? Date.now()));
|
|
2738
|
+
const maxRecordAgeMs = Math.max(
|
|
2739
|
+
6e4,
|
|
2740
|
+
Number(options.maxRecordAgeMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
|
|
2741
|
+
);
|
|
2742
|
+
const createdAtMs = parseTimestampMs(createdAt);
|
|
2743
|
+
const expiresAtMs = parseTimestampMs(expiresAt);
|
|
2744
|
+
if (createdAtMs == null || expiresAtMs == null) return null;
|
|
2745
|
+
if (expiresAtMs <= nowMs) return null;
|
|
2746
|
+
if (createdAtMs < nowMs - maxRecordAgeMs) return null;
|
|
2747
|
+
return candidate;
|
|
2748
|
+
}
|
|
2709
2749
|
async function fetchVmBootstrapConfigAggregate(address, options) {
|
|
2710
2750
|
const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
|
|
2711
2751
|
requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
|
|
@@ -2719,16 +2759,21 @@ async function fetchVmBootstrapConfigAggregate(address, options) {
|
|
|
2719
2759
|
if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return {};
|
|
2720
2760
|
return aggregate;
|
|
2721
2761
|
}
|
|
2722
|
-
function normalizeVmBootstrapConfigAggregate(aggregate) {
|
|
2762
|
+
function normalizeVmBootstrapConfigAggregate(aggregate, options = {}) {
|
|
2723
2763
|
if (!aggregate || typeof aggregate !== "object") return {};
|
|
2724
2764
|
return Object.fromEntries(
|
|
2725
|
-
Object.entries(aggregate).
|
|
2726
|
-
|
|
2727
|
-
|
|
2765
|
+
Object.entries(aggregate).map(([deploymentToken, record]) => [
|
|
2766
|
+
deploymentToken,
|
|
2767
|
+
normalizeVmBootstrapConfigRecord(deploymentToken, record, options)
|
|
2768
|
+
]).filter((entry) => entry[1] != null)
|
|
2728
2769
|
);
|
|
2729
2770
|
}
|
|
2730
2771
|
function createVmBootstrapConfigAggregateContent(args) {
|
|
2731
|
-
const
|
|
2772
|
+
const nowMs = Math.max(0, Number(args.now ?? Date.now() / 1e3) * 1e3);
|
|
2773
|
+
const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate, {
|
|
2774
|
+
nowMs,
|
|
2775
|
+
maxRecordAgeMs: args.maxRecordAgeMs
|
|
2776
|
+
});
|
|
2732
2777
|
content[args.record.deploymentToken] = args.record;
|
|
2733
2778
|
return {
|
|
2734
2779
|
address: args.sender,
|
|
@@ -2773,6 +2818,55 @@ async function publishVmBootstrapConfigAggregate(args) {
|
|
|
2773
2818
|
aggregateStatus
|
|
2774
2819
|
};
|
|
2775
2820
|
}
|
|
2821
|
+
function normalizeAggregateMessageHashRecord(value) {
|
|
2822
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2823
|
+
const candidate = value;
|
|
2824
|
+
const itemHash = typeof candidate.item_hash === "string" ? candidate.item_hash.trim() : "";
|
|
2825
|
+
if (!itemHash) return null;
|
|
2826
|
+
const content = candidate.content && typeof candidate.content === "object" && !Array.isArray(candidate.content) ? candidate.content : typeof candidate.item_content === "string" ? parseJsonObject(candidate.item_content) : null;
|
|
2827
|
+
if (!content || content.key !== VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY) return null;
|
|
2828
|
+
const timeMs = parseTimestampMs(candidate.time);
|
|
2829
|
+
if (timeMs == null) return null;
|
|
2830
|
+
return { itemHash, timeMs };
|
|
2831
|
+
}
|
|
2832
|
+
async function listStaleVmBootstrapConfigAggregateMessageHashes(args) {
|
|
2833
|
+
const address = args.address.trim();
|
|
2834
|
+
if (!address) return [];
|
|
2835
|
+
const currentAggregateItemHash = args.currentAggregateItemHash?.trim() ?? "";
|
|
2836
|
+
const olderThanMs = Math.max(
|
|
2837
|
+
6e4,
|
|
2838
|
+
Number(args.olderThanMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
|
|
2839
|
+
);
|
|
2840
|
+
const cutoffMs = Math.max(0, Number(args.nowMs ?? Date.now()) - olderThanMs);
|
|
2841
|
+
const pagination = Math.max(1, Number(args.pagination ?? 100));
|
|
2842
|
+
const maxPages = Math.max(1, Number(args.maxPages ?? 10));
|
|
2843
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
2844
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
2845
|
+
const requestUrl = new URL("/api/v0/messages.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
|
|
2846
|
+
requestUrl.searchParams.set("msgType", "AGGREGATE");
|
|
2847
|
+
requestUrl.searchParams.set("addresses", address);
|
|
2848
|
+
requestUrl.searchParams.set("message_statuses", "processed,pending,rejected,removing");
|
|
2849
|
+
requestUrl.searchParams.set("pagination", String(pagination));
|
|
2850
|
+
requestUrl.searchParams.set("page", String(page));
|
|
2851
|
+
requestUrl.searchParams.set("sortOrder", "-1");
|
|
2852
|
+
const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
|
|
2853
|
+
if (!response.ok) {
|
|
2854
|
+
throw new Error(`VM bootstrap config aggregate history request failed: ${response.status}`);
|
|
2855
|
+
}
|
|
2856
|
+
const payload = await response.json();
|
|
2857
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
2858
|
+
if (messages.length === 0) break;
|
|
2859
|
+
for (const message of messages) {
|
|
2860
|
+
const normalized = normalizeAggregateMessageHashRecord(message);
|
|
2861
|
+
if (!normalized) continue;
|
|
2862
|
+
if (normalized.itemHash === currentAggregateItemHash) continue;
|
|
2863
|
+
if (normalized.timeMs > cutoffMs) continue;
|
|
2864
|
+
hashes.add(normalized.itemHash);
|
|
2865
|
+
}
|
|
2866
|
+
if (messages.length < pagination) break;
|
|
2867
|
+
}
|
|
2868
|
+
return [...hashes];
|
|
2869
|
+
}
|
|
2776
2870
|
async function publishVmBootstrapConfig(args) {
|
|
2777
2871
|
const aggregate = await fetchVmBootstrapConfigAggregate(args.sender, {
|
|
2778
2872
|
apiHost: args.apiHost,
|
|
@@ -2781,7 +2875,8 @@ async function publishVmBootstrapConfig(args) {
|
|
|
2781
2875
|
const content = createVmBootstrapConfigAggregateContent({
|
|
2782
2876
|
sender: args.sender,
|
|
2783
2877
|
record: args.record,
|
|
2784
|
-
existingAggregate: aggregate
|
|
2878
|
+
existingAggregate: aggregate,
|
|
2879
|
+
maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
|
|
2785
2880
|
});
|
|
2786
2881
|
const result = await publishVmBootstrapConfigAggregate({
|
|
2787
2882
|
sender: args.sender,
|
|
@@ -2803,7 +2898,9 @@ async function deleteVmBootstrapConfig(args) {
|
|
|
2803
2898
|
apiHost: args.apiHost,
|
|
2804
2899
|
fetch: args.fetch
|
|
2805
2900
|
});
|
|
2806
|
-
const content = normalizeVmBootstrapConfigAggregate(aggregate
|
|
2901
|
+
const content = normalizeVmBootstrapConfigAggregate(aggregate, {
|
|
2902
|
+
maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
|
|
2903
|
+
});
|
|
2807
2904
|
delete content[args.deploymentToken];
|
|
2808
2905
|
const publishResult = await publishVmBootstrapConfigAggregate({
|
|
2809
2906
|
sender: args.sender,
|
|
@@ -3104,7 +3201,7 @@ function rootfsHealth(args) {
|
|
|
3104
3201
|
return {
|
|
3105
3202
|
tone: "error",
|
|
3106
3203
|
label: "not found on Aleph",
|
|
3107
|
-
detail: "The
|
|
3204
|
+
detail: "The manifest points to a rootfs STORE message that Aleph cannot find. Check that the manifest URL is correct and that the referenced rootfs was not forgotten."
|
|
3108
3205
|
};
|
|
3109
3206
|
}
|
|
3110
3207
|
if (!args.resolution) {
|
|
@@ -3137,8 +3234,8 @@ function rootfsHealth(args) {
|
|
|
3137
3234
|
}
|
|
3138
3235
|
return {
|
|
3139
3236
|
tone: "error",
|
|
3140
|
-
label: "not deployable",
|
|
3141
|
-
detail: args.resolution.rejectionReason ?? "Aleph rejected the rootfs reference."
|
|
3237
|
+
label: "manifest rootfs not deployable",
|
|
3238
|
+
detail: args.resolution.rejectionReason ?? (args.resolution.messageType === "STORE" ? "This manifest points to a rootfs STORE message that exists but is not deployable anymore, for example because it was forgotten or replaced. Double-check the manifest URL." : "Aleph rejected the rootfs reference. Double-check that the manifest URL points to the intended current rootfs.")
|
|
3142
3239
|
};
|
|
3143
3240
|
}
|
|
3144
3241
|
async function resolveManifest(args) {
|
|
@@ -3167,6 +3264,22 @@ function relayBootstrapInstanceItemHash(entry) {
|
|
|
3167
3264
|
}
|
|
3168
3265
|
return registrationIdInstanceItemHash(entry?.content?.registrationId);
|
|
3169
3266
|
}
|
|
3267
|
+
function relayBootstrapMatchesCurrentWallet(entry, normalizedWalletAddress, activeInstanceHashes) {
|
|
3268
|
+
const instanceItemHash = relayBootstrapInstanceItemHash(entry);
|
|
3269
|
+
if (instanceItemHash && activeInstanceHashes.has(instanceItemHash)) {
|
|
3270
|
+
return true;
|
|
3271
|
+
}
|
|
3272
|
+
const candidateAddresses = [
|
|
3273
|
+
entry?.address,
|
|
3274
|
+
entry?.content?.ownerAddress,
|
|
3275
|
+
entry?.content?.authorization?.payload?.ownerAddress,
|
|
3276
|
+
entry?.content?.publisherAddress,
|
|
3277
|
+
entry?.content?.authorization?.payload?.publisherAddress
|
|
3278
|
+
];
|
|
3279
|
+
return candidateAddresses.some(
|
|
3280
|
+
(value) => typeof value === "string" && value.trim().toLowerCase() === normalizedWalletAddress
|
|
3281
|
+
);
|
|
3282
|
+
}
|
|
3170
3283
|
async function inspectInstanceRuntime(args) {
|
|
3171
3284
|
const details = {
|
|
3172
3285
|
messageStatus: String(
|
|
@@ -3682,6 +3795,16 @@ var SponsorRelayController = class {
|
|
|
3682
3795
|
});
|
|
3683
3796
|
}
|
|
3684
3797
|
if (!relayMetadata) {
|
|
3798
|
+
await this.refresh().catch(() => void 0);
|
|
3799
|
+
const confirmedAfterRefresh = this.state.bootstrapRegistrations.some(
|
|
3800
|
+
(entry) => entry.instanceItemHash === args.itemHash && entry.confirmed
|
|
3801
|
+
);
|
|
3802
|
+
if (confirmedAfterRefresh) {
|
|
3803
|
+
this.trace("deploy:relay-metadata-recovered-from-refresh", {
|
|
3804
|
+
itemHash: args.itemHash
|
|
3805
|
+
});
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3685
3808
|
throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
|
|
3686
3809
|
}
|
|
3687
3810
|
const { peerId } = relayMetadata;
|
|
@@ -3790,6 +3913,7 @@ var SponsorRelayController = class {
|
|
|
3790
3913
|
}
|
|
3791
3914
|
}
|
|
3792
3915
|
if (profile === "uc-go-peer" && args.deploymentToken) {
|
|
3916
|
+
let cleanupAggregateItemHash = null;
|
|
3793
3917
|
await deleteVmBootstrapConfig({
|
|
3794
3918
|
sender: this.state.wallet.address,
|
|
3795
3919
|
deploymentToken: args.deploymentToken,
|
|
@@ -3798,6 +3922,14 @@ var SponsorRelayController = class {
|
|
|
3798
3922
|
fetch: asJsonFetch,
|
|
3799
3923
|
apiHost: this.client.apiHost,
|
|
3800
3924
|
sync: true
|
|
3925
|
+
}).then((cleanupResult) => {
|
|
3926
|
+
cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
|
|
3927
|
+
this.trace("deploy:bootstrap-config-cleanup-complete", {
|
|
3928
|
+
itemHash: args.itemHash,
|
|
3929
|
+
deploymentToken: args.deploymentToken,
|
|
3930
|
+
aggregateItemHash: cleanupResult.aggregateItemHash,
|
|
3931
|
+
aggregateStatus: cleanupResult.aggregateStatus
|
|
3932
|
+
});
|
|
3801
3933
|
}).catch((error) => {
|
|
3802
3934
|
this.trace("deploy:bootstrap-config-cleanup-error", {
|
|
3803
3935
|
itemHash: args.itemHash,
|
|
@@ -3805,8 +3937,75 @@ var SponsorRelayController = class {
|
|
|
3805
3937
|
error: error instanceof Error ? error.message : String(error)
|
|
3806
3938
|
});
|
|
3807
3939
|
});
|
|
3940
|
+
await this.cleanupStaleVmBootstrapConfigHistory({
|
|
3941
|
+
itemHash: args.itemHash,
|
|
3942
|
+
currentAggregateItemHash: cleanupAggregateItemHash,
|
|
3943
|
+
stage: "publishing-bootstrap",
|
|
3944
|
+
progress: 99
|
|
3945
|
+
});
|
|
3808
3946
|
}
|
|
3809
3947
|
}
|
|
3948
|
+
async cleanupStaleVmBootstrapConfigHistory(args) {
|
|
3949
|
+
if (!this.state.wallet.address || !args.currentAggregateItemHash) {
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
const staleHashes = await listStaleVmBootstrapConfigAggregateMessageHashes({
|
|
3953
|
+
address: this.state.wallet.address,
|
|
3954
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
3955
|
+
olderThanMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS,
|
|
3956
|
+
fetch: asJsonFetch,
|
|
3957
|
+
apiHost: this.client.apiHost
|
|
3958
|
+
}).catch((error) => {
|
|
3959
|
+
this.trace("deploy:bootstrap-config-history-scan-error", {
|
|
3960
|
+
itemHash: args.itemHash,
|
|
3961
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
3962
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3963
|
+
});
|
|
3964
|
+
return [];
|
|
3965
|
+
});
|
|
3966
|
+
if (staleHashes.length === 0) {
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
const staleCount = staleHashes.length;
|
|
3970
|
+
this.emitProgress({
|
|
3971
|
+
stage: args.stage,
|
|
3972
|
+
label: "Cleaning up stale handoff history",
|
|
3973
|
+
progress: args.progress,
|
|
3974
|
+
status: "warning",
|
|
3975
|
+
itemHash: args.itemHash,
|
|
3976
|
+
detail: `Forgetting ${staleCount} superseded Aleph handoff aggregate message${staleCount === 1 ? "" : "s"} older than 6 hours. MetaMask may request another cleanup signature.`
|
|
3977
|
+
});
|
|
3978
|
+
this.trace("deploy:bootstrap-config-history-cleanup-start", {
|
|
3979
|
+
itemHash: args.itemHash,
|
|
3980
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
3981
|
+
staleHashes
|
|
3982
|
+
});
|
|
3983
|
+
await forgetAlephMessages({
|
|
3984
|
+
sender: this.state.wallet.address,
|
|
3985
|
+
hashes: staleHashes,
|
|
3986
|
+
reason: "Prune stale vm-bootstrap-config aggregate history older than 6 hours",
|
|
3987
|
+
signer: personalSign,
|
|
3988
|
+
hasher: sha256Hex,
|
|
3989
|
+
fetch: asJsonFetch,
|
|
3990
|
+
apiHost: this.client.apiHost,
|
|
3991
|
+
sync: true
|
|
3992
|
+
}).then((cleanupResult) => {
|
|
3993
|
+
this.trace("deploy:bootstrap-config-history-cleanup-complete", {
|
|
3994
|
+
itemHash: args.itemHash,
|
|
3995
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
3996
|
+
staleHashes,
|
|
3997
|
+
forgetItemHash: cleanupResult.itemHash,
|
|
3998
|
+
forgetStatus: cleanupResult.status
|
|
3999
|
+
});
|
|
4000
|
+
}).catch((error) => {
|
|
4001
|
+
this.trace("deploy:bootstrap-config-history-cleanup-error", {
|
|
4002
|
+
itemHash: args.itemHash,
|
|
4003
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
4004
|
+
staleHashes,
|
|
4005
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4006
|
+
});
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
3810
4009
|
syncLatestDeploymentProgress(instances) {
|
|
3811
4010
|
const itemHash = this.state.lastDeploymentHash;
|
|
3812
4011
|
if (!itemHash) {
|
|
@@ -3882,6 +4081,20 @@ var SponsorRelayController = class {
|
|
|
3882
4081
|
});
|
|
3883
4082
|
return;
|
|
3884
4083
|
}
|
|
4084
|
+
const hasConfirmedBootstrapRegistration = this.state.bootstrapRegistrations.some(
|
|
4085
|
+
(entry) => entry.instanceItemHash === itemHash && entry.confirmed
|
|
4086
|
+
);
|
|
4087
|
+
if (hasConfirmedBootstrapRegistration) {
|
|
4088
|
+
this.emitProgress({
|
|
4089
|
+
stage: "completed",
|
|
4090
|
+
label: "Relay ready",
|
|
4091
|
+
progress: 100,
|
|
4092
|
+
status: "success",
|
|
4093
|
+
itemHash,
|
|
4094
|
+
detail: "Runtime networking is available and the relay bootstrap registration is confirmed on Aleph."
|
|
4095
|
+
});
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
3885
4098
|
this.emitProgress({
|
|
3886
4099
|
stage: "deployment-confirmed",
|
|
3887
4100
|
label: "Runtime ready",
|
|
@@ -4182,7 +4395,11 @@ var SponsorRelayController = class {
|
|
|
4182
4395
|
instances.map((entry) => entry.instance.item_hash)
|
|
4183
4396
|
);
|
|
4184
4397
|
bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
|
|
4185
|
-
(entry) =>
|
|
4398
|
+
(entry) => relayBootstrapMatchesCurrentWallet(
|
|
4399
|
+
entry,
|
|
4400
|
+
normalizedWalletAddress,
|
|
4401
|
+
activeInstanceHashes
|
|
4402
|
+
)
|
|
4186
4403
|
).map((entry) => {
|
|
4187
4404
|
const instanceItemHash = relayBootstrapInstanceItemHash(entry);
|
|
4188
4405
|
const messageHash = entry.itemHash ?? entry.hash ?? null;
|
|
@@ -4201,6 +4418,8 @@ var SponsorRelayController = class {
|
|
|
4201
4418
|
registrationId: entry.content.registrationId,
|
|
4202
4419
|
multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
|
|
4203
4420
|
browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
|
|
4421
|
+
ownerAddress: typeof entry.content.ownerAddress === "string" ? entry.content.ownerAddress : void 0,
|
|
4422
|
+
publisherAddress: typeof entry.content.publisherAddress === "string" ? entry.content.publisherAddress : void 0,
|
|
4204
4423
|
updatedAt: entry.content.updatedAt
|
|
4205
4424
|
} : null
|
|
4206
4425
|
};
|
|
@@ -4272,9 +4491,9 @@ var SponsorRelayController = class {
|
|
|
4272
4491
|
this.patch({ errorText: "Connect MetaMask before deploying." });
|
|
4273
4492
|
return;
|
|
4274
4493
|
}
|
|
4275
|
-
if (!this.state.manifest ||
|
|
4494
|
+
if (!this.state.manifest || this.state.rootfsHealth.tone !== "ok" || !this.state.pricingSummary.pricing || !this.state.pricingSummary.tier) {
|
|
4276
4495
|
this.patch({
|
|
4277
|
-
errorText: "Manifest, rootfs, and pricing must be ready before deploying."
|
|
4496
|
+
errorText: this.state.rootfsHealth.tone === "error" ? `Cannot deploy: ${this.state.rootfsHealth.detail ?? this.state.rootfsHealth.label}` : "Manifest, rootfs, and pricing must be ready before deploying."
|
|
4278
4497
|
});
|
|
4279
4498
|
return;
|
|
4280
4499
|
}
|
|
@@ -4316,6 +4535,7 @@ var SponsorRelayController = class {
|
|
|
4316
4535
|
for (const [candidateIndex, candidateCrn] of orderedCrns.entries()) {
|
|
4317
4536
|
let attemptItemHash = null;
|
|
4318
4537
|
let attemptDeploymentToken = null;
|
|
4538
|
+
let attemptProfile = null;
|
|
4319
4539
|
const crnAttemptLabel = `CRN ${candidateIndex + 1}/${orderedCrns.length}`;
|
|
4320
4540
|
const crnDisplayName = candidateCrn.name ?? candidateCrn.hash;
|
|
4321
4541
|
this.emitProgress({
|
|
@@ -4327,6 +4547,7 @@ var SponsorRelayController = class {
|
|
|
4327
4547
|
});
|
|
4328
4548
|
try {
|
|
4329
4549
|
const profile = relayProfileForManifest(this.state.manifest);
|
|
4550
|
+
attemptProfile = profile;
|
|
4330
4551
|
const sshPublicKey = profile === "uc-go-peer" ? appendDeploymentTokenToSshPublicKey(
|
|
4331
4552
|
this.state.sshPublicKey.trim(),
|
|
4332
4553
|
this.state.wallet.address,
|
|
@@ -4562,6 +4783,106 @@ var SponsorRelayController = class {
|
|
|
4562
4783
|
itemHash: attemptItemHash,
|
|
4563
4784
|
error: lastError.message
|
|
4564
4785
|
});
|
|
4786
|
+
if (attemptItemHash && attemptDeploymentToken && attemptProfile && !failedDueToInvalidIpv6) {
|
|
4787
|
+
try {
|
|
4788
|
+
const latestRuntime = await fetchVmRuntime({
|
|
4789
|
+
itemHash: attemptItemHash,
|
|
4790
|
+
fetch: (url, init) => fetch(url, init),
|
|
4791
|
+
crnHash: candidateCrn.hash,
|
|
4792
|
+
crns: this.state.crns,
|
|
4793
|
+
crnListUrl: this.props.crnListUrl,
|
|
4794
|
+
requirePublicGuestIpv6ForProxy: true
|
|
4795
|
+
});
|
|
4796
|
+
if (latestRuntime.diagnostics?.state === "ready") {
|
|
4797
|
+
const reconciledRelayMetadata = await fetchRelayMetadataForRuntime({
|
|
4798
|
+
runtime: latestRuntime,
|
|
4799
|
+
fetch: (url, init) => fetch(url, init),
|
|
4800
|
+
preferSecureMetadata: attemptProfile === "uc-go-peer" && Boolean(latestRuntime.proxyUrl),
|
|
4801
|
+
attempts: 3,
|
|
4802
|
+
delayMs: 1e3,
|
|
4803
|
+
timeoutMs: 15e3
|
|
4804
|
+
}).catch(() => null);
|
|
4805
|
+
if (reconciledRelayMetadata) {
|
|
4806
|
+
const registrationId = createRelayBootstrapRegistrationId(
|
|
4807
|
+
attemptProfile,
|
|
4808
|
+
this.state.instanceName.trim(),
|
|
4809
|
+
attemptItemHash
|
|
4810
|
+
);
|
|
4811
|
+
const visibleRegistration = await waitForRelayBootstrapRegistration({
|
|
4812
|
+
registrationId,
|
|
4813
|
+
peerId: reconciledRelayMetadata.peerId,
|
|
4814
|
+
fetch: asJsonFetch,
|
|
4815
|
+
apiHost: this.client.apiHost,
|
|
4816
|
+
attempts: 3,
|
|
4817
|
+
delayMs: 1e3
|
|
4818
|
+
}).catch(() => null);
|
|
4819
|
+
if (visibleRegistration) {
|
|
4820
|
+
this.trace("deploy:recovered-after-error", {
|
|
4821
|
+
itemHash: attemptItemHash,
|
|
4822
|
+
registrationId,
|
|
4823
|
+
peerId: reconciledRelayMetadata.peerId,
|
|
4824
|
+
error: lastError.message
|
|
4825
|
+
});
|
|
4826
|
+
this.rememberRuntimeDetails(attemptItemHash, {
|
|
4827
|
+
messageStatus: "processed",
|
|
4828
|
+
allocationSource: latestRuntime.allocation?.source ?? null,
|
|
4829
|
+
crnUrl: latestRuntime.allocation?.crnUrl ?? latestRuntime.execution?.crnUrl ?? null,
|
|
4830
|
+
hostIpv4: latestRuntime.hostIpv4 ?? null,
|
|
4831
|
+
ipv6: latestRuntime.ipv6 ?? null,
|
|
4832
|
+
vmIpv4: latestRuntime.execution?.networking?.ipv4_ip ?? null,
|
|
4833
|
+
webUrl: latestRuntime.webAccessUrl ?? latestRuntime.proxyUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null,
|
|
4834
|
+
sshCommand: latestRuntime.sshCommand ?? null,
|
|
4835
|
+
mappedPorts: latestRuntime.execution != null ? mappedPorts(latestRuntime.execution) : runtimeMappedPorts(latestRuntime.mappedPorts),
|
|
4836
|
+
execution: latestRuntime.execution ?? null,
|
|
4837
|
+
error: null
|
|
4838
|
+
});
|
|
4839
|
+
this.patch({
|
|
4840
|
+
busy: { deploying: false }
|
|
4841
|
+
});
|
|
4842
|
+
await this.refresh();
|
|
4843
|
+
this.lastCompletedDeploymentHash = attemptItemHash;
|
|
4844
|
+
this.emitProgress({
|
|
4845
|
+
stage: "completed",
|
|
4846
|
+
label: "Relay ready",
|
|
4847
|
+
progress: 100,
|
|
4848
|
+
status: "success",
|
|
4849
|
+
itemHash: attemptItemHash,
|
|
4850
|
+
detail: "The relay completed startup after a transient UI polling error. Runtime and bootstrap registration are confirmed."
|
|
4851
|
+
});
|
|
4852
|
+
return;
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
} catch (reconcileError) {
|
|
4857
|
+
this.trace("deploy:recovery-check-error", {
|
|
4858
|
+
itemHash: attemptItemHash,
|
|
4859
|
+
error: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
|
|
4860
|
+
});
|
|
4861
|
+
}
|
|
4862
|
+
await this.refresh().catch(() => void 0);
|
|
4863
|
+
const recoveredRegistration = this.state.bootstrapRegistrations.some(
|
|
4864
|
+
(entry) => entry.instanceItemHash === attemptItemHash && entry.confirmed
|
|
4865
|
+
);
|
|
4866
|
+
if (recoveredRegistration) {
|
|
4867
|
+
this.trace("deploy:recovered-from-refresh-state", {
|
|
4868
|
+
itemHash: attemptItemHash,
|
|
4869
|
+
error: lastError.message
|
|
4870
|
+
});
|
|
4871
|
+
this.patch({
|
|
4872
|
+
busy: { deploying: false }
|
|
4873
|
+
});
|
|
4874
|
+
this.lastCompletedDeploymentHash = attemptItemHash;
|
|
4875
|
+
this.emitProgress({
|
|
4876
|
+
stage: "completed",
|
|
4877
|
+
label: "Relay ready",
|
|
4878
|
+
progress: 100,
|
|
4879
|
+
status: "success",
|
|
4880
|
+
itemHash: attemptItemHash,
|
|
4881
|
+
detail: "The relay completed startup after a transient browser-side polling error. Runtime and bootstrap registration are confirmed."
|
|
4882
|
+
});
|
|
4883
|
+
return;
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4565
4886
|
if (attemptItemHash) {
|
|
4566
4887
|
this.emitProgress({
|
|
4567
4888
|
stage: "deployment-confirmed",
|
|
@@ -4572,6 +4893,7 @@ var SponsorRelayController = class {
|
|
|
4572
4893
|
detail: failedDueToInvalidIpv6 ? `${attemptLabel} exposed runtime networking that cannot support proxy-backed HTTPS. Cleaning up this attempt before retrying another CRN.` : `${attemptLabel} failed: ${lastError.message} Cleaning up this attempt before retrying another CRN.`
|
|
4573
4894
|
});
|
|
4574
4895
|
if (attemptDeploymentToken) {
|
|
4896
|
+
let cleanupAggregateItemHash = null;
|
|
4575
4897
|
this.emitProgress({
|
|
4576
4898
|
stage: "deployment-confirmed",
|
|
4577
4899
|
label: "Cleaning up failed attempt",
|
|
@@ -4593,6 +4915,7 @@ var SponsorRelayController = class {
|
|
|
4593
4915
|
apiHost: this.client.apiHost,
|
|
4594
4916
|
sync: true
|
|
4595
4917
|
}).then((cleanupResult) => {
|
|
4918
|
+
cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
|
|
4596
4919
|
this.trace("deploy:bootstrap-config-cleanup-complete", {
|
|
4597
4920
|
itemHash: attemptItemHash,
|
|
4598
4921
|
deploymentToken: attemptDeploymentToken,
|
|
@@ -4606,6 +4929,12 @@ var SponsorRelayController = class {
|
|
|
4606
4929
|
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
4607
4930
|
});
|
|
4608
4931
|
});
|
|
4932
|
+
await this.cleanupStaleVmBootstrapConfigHistory({
|
|
4933
|
+
itemHash: attemptItemHash,
|
|
4934
|
+
currentAggregateItemHash: cleanupAggregateItemHash,
|
|
4935
|
+
stage: "deployment-confirmed",
|
|
4936
|
+
progress: 18
|
|
4937
|
+
});
|
|
4609
4938
|
}
|
|
4610
4939
|
this.emitProgress({
|
|
4611
4940
|
stage: "deployment-confirmed",
|
|
@@ -5709,6 +6038,7 @@ function SponsorRelayFab(props) {
|
|
|
5709
6038
|
{
|
|
5710
6039
|
type: "button",
|
|
5711
6040
|
onClick: () => void (state.wallet.connected ? controller.deploy() : controller.connectWallet()),
|
|
6041
|
+
disabled: state.wallet.connected ? state.busy.deploying || state.rootfsHealth.tone !== "ok" : state.busy.connectingWallet,
|
|
5712
6042
|
style: { ...primaryButtonStyle, width: "100%", marginTop: "1rem" },
|
|
5713
6043
|
children: state.wallet.connected ? state.busy.deploying ? "Deploying\u2026" : "Deploy Relay" : "Connect MetaMask"
|
|
5714
6044
|
}
|
package/shared/index.d.ts
CHANGED
|
@@ -254,6 +254,8 @@ interface CompactBootstrapRegistrationRecord {
|
|
|
254
254
|
registrationId?: string;
|
|
255
255
|
multiaddrs: string[];
|
|
256
256
|
browserMultiaddrs?: string[];
|
|
257
|
+
ownerAddress?: string;
|
|
258
|
+
publisherAddress?: string;
|
|
257
259
|
updatedAt: number;
|
|
258
260
|
} | null;
|
|
259
261
|
}
|
|
@@ -309,7 +311,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
|
|
|
309
311
|
declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
|
|
310
312
|
declare const DEPLOYMENT_PENDING_WARNING_MS: number;
|
|
311
313
|
|
|
312
|
-
declare const UI_PACKAGE_VERSION = "0.4.
|
|
314
|
+
declare const UI_PACKAGE_VERSION = "0.4.4";
|
|
313
315
|
|
|
314
316
|
interface RootfsRequiredPortForward {
|
|
315
317
|
port: number;
|
|
@@ -368,6 +370,7 @@ declare class SponsorRelayController {
|
|
|
368
370
|
private patch;
|
|
369
371
|
private emitProgress;
|
|
370
372
|
private configureRelayBootstrapRegistration;
|
|
373
|
+
private cleanupStaleVmBootstrapConfigHistory;
|
|
371
374
|
private syncLatestDeploymentProgress;
|
|
372
375
|
private canSkipRuntimeRefresh;
|
|
373
376
|
private noteRuntimeRefreshResult;
|
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.4.
|
|
30
|
+
var UI_PACKAGE_VERSION = "0.4.4";
|
|
31
31
|
|
|
32
32
|
// src/shared/format.ts
|
|
33
33
|
function shortHash(value, head = 8, tail = 6) {
|
|
@@ -1740,14 +1740,15 @@ function describeRuntimeAvailability(runtime) {
|
|
|
1740
1740
|
const mappedPorts2 = runtime.mappedPorts ?? {};
|
|
1741
1741
|
const hostIpv4 = runtime.hostIpv4 ?? null;
|
|
1742
1742
|
const ipv6 = runtime.ipv6 ?? null;
|
|
1743
|
+
const executionGuestIpv6 = execution?.networking?.ipv6_ip ?? null;
|
|
1743
1744
|
const proxyUrl = runtime.proxyUrl ?? null;
|
|
1744
1745
|
const schedulerSource = runtime.allocation?.source ?? null;
|
|
1745
1746
|
const webAccessActive = runtime.webAccess?.active ?? null;
|
|
1746
1747
|
const mappedPortCount = Object.keys(mappedPorts2).length;
|
|
1747
|
-
if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && !isLikelyGlobalIpv6(
|
|
1748
|
+
if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && executionGuestIpv6 && !isLikelyGlobalIpv6(executionGuestIpv6)) {
|
|
1748
1749
|
return {
|
|
1749
1750
|
state: "execution-invalid-public-ipv6",
|
|
1750
|
-
reason: `The CRN exposed runtime networking, but the guest IPv6
|
|
1751
|
+
reason: `The CRN exposed runtime networking, but the guest IPv6 (${executionGuestIpv6}) is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
|
|
1751
1752
|
schedulerSource,
|
|
1752
1753
|
executionSeen: Boolean(execution),
|
|
1753
1754
|
webAccessActive,
|
|
@@ -2697,6 +2698,7 @@ async function publishRelayBootstrapRegistration(args) {
|
|
|
2697
2698
|
var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
|
|
2698
2699
|
var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
|
|
2699
2700
|
var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
|
|
2701
|
+
var VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS = 6 * 60 * 60 * 1e3;
|
|
2700
2702
|
function parseJsonObject(value) {
|
|
2701
2703
|
try {
|
|
2702
2704
|
const parsed = JSON.parse(value);
|
|
@@ -2705,6 +2707,44 @@ function parseJsonObject(value) {
|
|
|
2705
2707
|
return null;
|
|
2706
2708
|
}
|
|
2707
2709
|
}
|
|
2710
|
+
function parseTimestampMs(value) {
|
|
2711
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2712
|
+
return value > 1e12 ? value : value * 1e3;
|
|
2713
|
+
}
|
|
2714
|
+
if (typeof value === "string" && value.trim()) {
|
|
2715
|
+
const numeric = Number(value);
|
|
2716
|
+
if (Number.isFinite(numeric)) {
|
|
2717
|
+
return numeric > 1e12 ? numeric : numeric * 1e3;
|
|
2718
|
+
}
|
|
2719
|
+
const parsed = Date.parse(value);
|
|
2720
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2721
|
+
}
|
|
2722
|
+
return null;
|
|
2723
|
+
}
|
|
2724
|
+
function normalizeVmBootstrapConfigRecord(deploymentTokenKey, value, options = {}) {
|
|
2725
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2726
|
+
const candidate = value;
|
|
2727
|
+
const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
|
|
2728
|
+
const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
|
|
2729
|
+
const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
|
|
2730
|
+
const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
|
|
2731
|
+
const createdAt = typeof candidate.createdAt === "string" ? candidate.createdAt.trim() : "";
|
|
2732
|
+
const expiresAt = typeof candidate.expiresAt === "string" ? candidate.expiresAt.trim() : "";
|
|
2733
|
+
if (!deploymentTokenKey.trim() || !deploymentToken || deploymentToken !== deploymentTokenKey.trim() || !profile || !ownerAddress || !instanceItemHash || !createdAt || !expiresAt) {
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
const nowMs = Math.max(0, Number(options.nowMs ?? Date.now()));
|
|
2737
|
+
const maxRecordAgeMs = Math.max(
|
|
2738
|
+
6e4,
|
|
2739
|
+
Number(options.maxRecordAgeMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
|
|
2740
|
+
);
|
|
2741
|
+
const createdAtMs = parseTimestampMs(createdAt);
|
|
2742
|
+
const expiresAtMs = parseTimestampMs(expiresAt);
|
|
2743
|
+
if (createdAtMs == null || expiresAtMs == null) return null;
|
|
2744
|
+
if (expiresAtMs <= nowMs) return null;
|
|
2745
|
+
if (createdAtMs < nowMs - maxRecordAgeMs) return null;
|
|
2746
|
+
return candidate;
|
|
2747
|
+
}
|
|
2708
2748
|
async function fetchVmBootstrapConfigAggregate(address, options) {
|
|
2709
2749
|
const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
|
|
2710
2750
|
requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
|
|
@@ -2718,16 +2758,21 @@ async function fetchVmBootstrapConfigAggregate(address, options) {
|
|
|
2718
2758
|
if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return {};
|
|
2719
2759
|
return aggregate;
|
|
2720
2760
|
}
|
|
2721
|
-
function normalizeVmBootstrapConfigAggregate(aggregate) {
|
|
2761
|
+
function normalizeVmBootstrapConfigAggregate(aggregate, options = {}) {
|
|
2722
2762
|
if (!aggregate || typeof aggregate !== "object") return {};
|
|
2723
2763
|
return Object.fromEntries(
|
|
2724
|
-
Object.entries(aggregate).
|
|
2725
|
-
|
|
2726
|
-
|
|
2764
|
+
Object.entries(aggregate).map(([deploymentToken, record]) => [
|
|
2765
|
+
deploymentToken,
|
|
2766
|
+
normalizeVmBootstrapConfigRecord(deploymentToken, record, options)
|
|
2767
|
+
]).filter((entry) => entry[1] != null)
|
|
2727
2768
|
);
|
|
2728
2769
|
}
|
|
2729
2770
|
function createVmBootstrapConfigAggregateContent(args) {
|
|
2730
|
-
const
|
|
2771
|
+
const nowMs = Math.max(0, Number(args.now ?? Date.now() / 1e3) * 1e3);
|
|
2772
|
+
const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate, {
|
|
2773
|
+
nowMs,
|
|
2774
|
+
maxRecordAgeMs: args.maxRecordAgeMs
|
|
2775
|
+
});
|
|
2731
2776
|
content[args.record.deploymentToken] = args.record;
|
|
2732
2777
|
return {
|
|
2733
2778
|
address: args.sender,
|
|
@@ -2772,6 +2817,55 @@ async function publishVmBootstrapConfigAggregate(args) {
|
|
|
2772
2817
|
aggregateStatus
|
|
2773
2818
|
};
|
|
2774
2819
|
}
|
|
2820
|
+
function normalizeAggregateMessageHashRecord(value) {
|
|
2821
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2822
|
+
const candidate = value;
|
|
2823
|
+
const itemHash = typeof candidate.item_hash === "string" ? candidate.item_hash.trim() : "";
|
|
2824
|
+
if (!itemHash) return null;
|
|
2825
|
+
const content = candidate.content && typeof candidate.content === "object" && !Array.isArray(candidate.content) ? candidate.content : typeof candidate.item_content === "string" ? parseJsonObject(candidate.item_content) : null;
|
|
2826
|
+
if (!content || content.key !== VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY) return null;
|
|
2827
|
+
const timeMs = parseTimestampMs(candidate.time);
|
|
2828
|
+
if (timeMs == null) return null;
|
|
2829
|
+
return { itemHash, timeMs };
|
|
2830
|
+
}
|
|
2831
|
+
async function listStaleVmBootstrapConfigAggregateMessageHashes(args) {
|
|
2832
|
+
const address = args.address.trim();
|
|
2833
|
+
if (!address) return [];
|
|
2834
|
+
const currentAggregateItemHash = args.currentAggregateItemHash?.trim() ?? "";
|
|
2835
|
+
const olderThanMs = Math.max(
|
|
2836
|
+
6e4,
|
|
2837
|
+
Number(args.olderThanMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
|
|
2838
|
+
);
|
|
2839
|
+
const cutoffMs = Math.max(0, Number(args.nowMs ?? Date.now()) - olderThanMs);
|
|
2840
|
+
const pagination = Math.max(1, Number(args.pagination ?? 100));
|
|
2841
|
+
const maxPages = Math.max(1, Number(args.maxPages ?? 10));
|
|
2842
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
2843
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
2844
|
+
const requestUrl = new URL("/api/v0/messages.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
|
|
2845
|
+
requestUrl.searchParams.set("msgType", "AGGREGATE");
|
|
2846
|
+
requestUrl.searchParams.set("addresses", address);
|
|
2847
|
+
requestUrl.searchParams.set("message_statuses", "processed,pending,rejected,removing");
|
|
2848
|
+
requestUrl.searchParams.set("pagination", String(pagination));
|
|
2849
|
+
requestUrl.searchParams.set("page", String(page));
|
|
2850
|
+
requestUrl.searchParams.set("sortOrder", "-1");
|
|
2851
|
+
const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
|
|
2852
|
+
if (!response.ok) {
|
|
2853
|
+
throw new Error(`VM bootstrap config aggregate history request failed: ${response.status}`);
|
|
2854
|
+
}
|
|
2855
|
+
const payload = await response.json();
|
|
2856
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
2857
|
+
if (messages.length === 0) break;
|
|
2858
|
+
for (const message of messages) {
|
|
2859
|
+
const normalized = normalizeAggregateMessageHashRecord(message);
|
|
2860
|
+
if (!normalized) continue;
|
|
2861
|
+
if (normalized.itemHash === currentAggregateItemHash) continue;
|
|
2862
|
+
if (normalized.timeMs > cutoffMs) continue;
|
|
2863
|
+
hashes.add(normalized.itemHash);
|
|
2864
|
+
}
|
|
2865
|
+
if (messages.length < pagination) break;
|
|
2866
|
+
}
|
|
2867
|
+
return [...hashes];
|
|
2868
|
+
}
|
|
2775
2869
|
async function publishVmBootstrapConfig(args) {
|
|
2776
2870
|
const aggregate = await fetchVmBootstrapConfigAggregate(args.sender, {
|
|
2777
2871
|
apiHost: args.apiHost,
|
|
@@ -2780,7 +2874,8 @@ async function publishVmBootstrapConfig(args) {
|
|
|
2780
2874
|
const content = createVmBootstrapConfigAggregateContent({
|
|
2781
2875
|
sender: args.sender,
|
|
2782
2876
|
record: args.record,
|
|
2783
|
-
existingAggregate: aggregate
|
|
2877
|
+
existingAggregate: aggregate,
|
|
2878
|
+
maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
|
|
2784
2879
|
});
|
|
2785
2880
|
const result = await publishVmBootstrapConfigAggregate({
|
|
2786
2881
|
sender: args.sender,
|
|
@@ -2802,7 +2897,9 @@ async function deleteVmBootstrapConfig(args) {
|
|
|
2802
2897
|
apiHost: args.apiHost,
|
|
2803
2898
|
fetch: args.fetch
|
|
2804
2899
|
});
|
|
2805
|
-
const content = normalizeVmBootstrapConfigAggregate(aggregate
|
|
2900
|
+
const content = normalizeVmBootstrapConfigAggregate(aggregate, {
|
|
2901
|
+
maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
|
|
2902
|
+
});
|
|
2806
2903
|
delete content[args.deploymentToken];
|
|
2807
2904
|
const publishResult = await publishVmBootstrapConfigAggregate({
|
|
2808
2905
|
sender: args.sender,
|
|
@@ -3103,7 +3200,7 @@ function rootfsHealth(args) {
|
|
|
3103
3200
|
return {
|
|
3104
3201
|
tone: "error",
|
|
3105
3202
|
label: "not found on Aleph",
|
|
3106
|
-
detail: "The
|
|
3203
|
+
detail: "The manifest points to a rootfs STORE message that Aleph cannot find. Check that the manifest URL is correct and that the referenced rootfs was not forgotten."
|
|
3107
3204
|
};
|
|
3108
3205
|
}
|
|
3109
3206
|
if (!args.resolution) {
|
|
@@ -3136,8 +3233,8 @@ function rootfsHealth(args) {
|
|
|
3136
3233
|
}
|
|
3137
3234
|
return {
|
|
3138
3235
|
tone: "error",
|
|
3139
|
-
label: "not deployable",
|
|
3140
|
-
detail: args.resolution.rejectionReason ?? "Aleph rejected the rootfs reference."
|
|
3236
|
+
label: "manifest rootfs not deployable",
|
|
3237
|
+
detail: args.resolution.rejectionReason ?? (args.resolution.messageType === "STORE" ? "This manifest points to a rootfs STORE message that exists but is not deployable anymore, for example because it was forgotten or replaced. Double-check the manifest URL." : "Aleph rejected the rootfs reference. Double-check that the manifest URL points to the intended current rootfs.")
|
|
3141
3238
|
};
|
|
3142
3239
|
}
|
|
3143
3240
|
async function resolveManifest(args) {
|
|
@@ -3166,6 +3263,22 @@ function relayBootstrapInstanceItemHash(entry) {
|
|
|
3166
3263
|
}
|
|
3167
3264
|
return registrationIdInstanceItemHash(entry?.content?.registrationId);
|
|
3168
3265
|
}
|
|
3266
|
+
function relayBootstrapMatchesCurrentWallet(entry, normalizedWalletAddress, activeInstanceHashes) {
|
|
3267
|
+
const instanceItemHash = relayBootstrapInstanceItemHash(entry);
|
|
3268
|
+
if (instanceItemHash && activeInstanceHashes.has(instanceItemHash)) {
|
|
3269
|
+
return true;
|
|
3270
|
+
}
|
|
3271
|
+
const candidateAddresses = [
|
|
3272
|
+
entry?.address,
|
|
3273
|
+
entry?.content?.ownerAddress,
|
|
3274
|
+
entry?.content?.authorization?.payload?.ownerAddress,
|
|
3275
|
+
entry?.content?.publisherAddress,
|
|
3276
|
+
entry?.content?.authorization?.payload?.publisherAddress
|
|
3277
|
+
];
|
|
3278
|
+
return candidateAddresses.some(
|
|
3279
|
+
(value) => typeof value === "string" && value.trim().toLowerCase() === normalizedWalletAddress
|
|
3280
|
+
);
|
|
3281
|
+
}
|
|
3169
3282
|
async function inspectInstanceRuntime(args) {
|
|
3170
3283
|
const details = {
|
|
3171
3284
|
messageStatus: String(
|
|
@@ -3681,6 +3794,16 @@ var SponsorRelayController = class {
|
|
|
3681
3794
|
});
|
|
3682
3795
|
}
|
|
3683
3796
|
if (!relayMetadata) {
|
|
3797
|
+
await this.refresh().catch(() => void 0);
|
|
3798
|
+
const confirmedAfterRefresh = this.state.bootstrapRegistrations.some(
|
|
3799
|
+
(entry) => entry.instanceItemHash === args.itemHash && entry.confirmed
|
|
3800
|
+
);
|
|
3801
|
+
if (confirmedAfterRefresh) {
|
|
3802
|
+
this.trace("deploy:relay-metadata-recovered-from-refresh", {
|
|
3803
|
+
itemHash: args.itemHash
|
|
3804
|
+
});
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3684
3807
|
throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
|
|
3685
3808
|
}
|
|
3686
3809
|
const { peerId } = relayMetadata;
|
|
@@ -3789,6 +3912,7 @@ var SponsorRelayController = class {
|
|
|
3789
3912
|
}
|
|
3790
3913
|
}
|
|
3791
3914
|
if (profile === "uc-go-peer" && args.deploymentToken) {
|
|
3915
|
+
let cleanupAggregateItemHash = null;
|
|
3792
3916
|
await deleteVmBootstrapConfig({
|
|
3793
3917
|
sender: this.state.wallet.address,
|
|
3794
3918
|
deploymentToken: args.deploymentToken,
|
|
@@ -3797,6 +3921,14 @@ var SponsorRelayController = class {
|
|
|
3797
3921
|
fetch: asJsonFetch,
|
|
3798
3922
|
apiHost: this.client.apiHost,
|
|
3799
3923
|
sync: true
|
|
3924
|
+
}).then((cleanupResult) => {
|
|
3925
|
+
cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
|
|
3926
|
+
this.trace("deploy:bootstrap-config-cleanup-complete", {
|
|
3927
|
+
itemHash: args.itemHash,
|
|
3928
|
+
deploymentToken: args.deploymentToken,
|
|
3929
|
+
aggregateItemHash: cleanupResult.aggregateItemHash,
|
|
3930
|
+
aggregateStatus: cleanupResult.aggregateStatus
|
|
3931
|
+
});
|
|
3800
3932
|
}).catch((error) => {
|
|
3801
3933
|
this.trace("deploy:bootstrap-config-cleanup-error", {
|
|
3802
3934
|
itemHash: args.itemHash,
|
|
@@ -3804,8 +3936,75 @@ var SponsorRelayController = class {
|
|
|
3804
3936
|
error: error instanceof Error ? error.message : String(error)
|
|
3805
3937
|
});
|
|
3806
3938
|
});
|
|
3939
|
+
await this.cleanupStaleVmBootstrapConfigHistory({
|
|
3940
|
+
itemHash: args.itemHash,
|
|
3941
|
+
currentAggregateItemHash: cleanupAggregateItemHash,
|
|
3942
|
+
stage: "publishing-bootstrap",
|
|
3943
|
+
progress: 99
|
|
3944
|
+
});
|
|
3807
3945
|
}
|
|
3808
3946
|
}
|
|
3947
|
+
async cleanupStaleVmBootstrapConfigHistory(args) {
|
|
3948
|
+
if (!this.state.wallet.address || !args.currentAggregateItemHash) {
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3951
|
+
const staleHashes = await listStaleVmBootstrapConfigAggregateMessageHashes({
|
|
3952
|
+
address: this.state.wallet.address,
|
|
3953
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
3954
|
+
olderThanMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS,
|
|
3955
|
+
fetch: asJsonFetch,
|
|
3956
|
+
apiHost: this.client.apiHost
|
|
3957
|
+
}).catch((error) => {
|
|
3958
|
+
this.trace("deploy:bootstrap-config-history-scan-error", {
|
|
3959
|
+
itemHash: args.itemHash,
|
|
3960
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
3961
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3962
|
+
});
|
|
3963
|
+
return [];
|
|
3964
|
+
});
|
|
3965
|
+
if (staleHashes.length === 0) {
|
|
3966
|
+
return;
|
|
3967
|
+
}
|
|
3968
|
+
const staleCount = staleHashes.length;
|
|
3969
|
+
this.emitProgress({
|
|
3970
|
+
stage: args.stage,
|
|
3971
|
+
label: "Cleaning up stale handoff history",
|
|
3972
|
+
progress: args.progress,
|
|
3973
|
+
status: "warning",
|
|
3974
|
+
itemHash: args.itemHash,
|
|
3975
|
+
detail: `Forgetting ${staleCount} superseded Aleph handoff aggregate message${staleCount === 1 ? "" : "s"} older than 6 hours. MetaMask may request another cleanup signature.`
|
|
3976
|
+
});
|
|
3977
|
+
this.trace("deploy:bootstrap-config-history-cleanup-start", {
|
|
3978
|
+
itemHash: args.itemHash,
|
|
3979
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
3980
|
+
staleHashes
|
|
3981
|
+
});
|
|
3982
|
+
await forgetAlephMessages({
|
|
3983
|
+
sender: this.state.wallet.address,
|
|
3984
|
+
hashes: staleHashes,
|
|
3985
|
+
reason: "Prune stale vm-bootstrap-config aggregate history older than 6 hours",
|
|
3986
|
+
signer: personalSign,
|
|
3987
|
+
hasher: sha256Hex,
|
|
3988
|
+
fetch: asJsonFetch,
|
|
3989
|
+
apiHost: this.client.apiHost,
|
|
3990
|
+
sync: true
|
|
3991
|
+
}).then((cleanupResult) => {
|
|
3992
|
+
this.trace("deploy:bootstrap-config-history-cleanup-complete", {
|
|
3993
|
+
itemHash: args.itemHash,
|
|
3994
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
3995
|
+
staleHashes,
|
|
3996
|
+
forgetItemHash: cleanupResult.itemHash,
|
|
3997
|
+
forgetStatus: cleanupResult.status
|
|
3998
|
+
});
|
|
3999
|
+
}).catch((error) => {
|
|
4000
|
+
this.trace("deploy:bootstrap-config-history-cleanup-error", {
|
|
4001
|
+
itemHash: args.itemHash,
|
|
4002
|
+
currentAggregateItemHash: args.currentAggregateItemHash,
|
|
4003
|
+
staleHashes,
|
|
4004
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4005
|
+
});
|
|
4006
|
+
});
|
|
4007
|
+
}
|
|
3809
4008
|
syncLatestDeploymentProgress(instances) {
|
|
3810
4009
|
const itemHash = this.state.lastDeploymentHash;
|
|
3811
4010
|
if (!itemHash) {
|
|
@@ -3881,6 +4080,20 @@ var SponsorRelayController = class {
|
|
|
3881
4080
|
});
|
|
3882
4081
|
return;
|
|
3883
4082
|
}
|
|
4083
|
+
const hasConfirmedBootstrapRegistration = this.state.bootstrapRegistrations.some(
|
|
4084
|
+
(entry) => entry.instanceItemHash === itemHash && entry.confirmed
|
|
4085
|
+
);
|
|
4086
|
+
if (hasConfirmedBootstrapRegistration) {
|
|
4087
|
+
this.emitProgress({
|
|
4088
|
+
stage: "completed",
|
|
4089
|
+
label: "Relay ready",
|
|
4090
|
+
progress: 100,
|
|
4091
|
+
status: "success",
|
|
4092
|
+
itemHash,
|
|
4093
|
+
detail: "Runtime networking is available and the relay bootstrap registration is confirmed on Aleph."
|
|
4094
|
+
});
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
3884
4097
|
this.emitProgress({
|
|
3885
4098
|
stage: "deployment-confirmed",
|
|
3886
4099
|
label: "Runtime ready",
|
|
@@ -4181,7 +4394,11 @@ var SponsorRelayController = class {
|
|
|
4181
4394
|
instances.map((entry) => entry.instance.item_hash)
|
|
4182
4395
|
);
|
|
4183
4396
|
bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
|
|
4184
|
-
(entry) =>
|
|
4397
|
+
(entry) => relayBootstrapMatchesCurrentWallet(
|
|
4398
|
+
entry,
|
|
4399
|
+
normalizedWalletAddress,
|
|
4400
|
+
activeInstanceHashes
|
|
4401
|
+
)
|
|
4185
4402
|
).map((entry) => {
|
|
4186
4403
|
const instanceItemHash = relayBootstrapInstanceItemHash(entry);
|
|
4187
4404
|
const messageHash = entry.itemHash ?? entry.hash ?? null;
|
|
@@ -4200,6 +4417,8 @@ var SponsorRelayController = class {
|
|
|
4200
4417
|
registrationId: entry.content.registrationId,
|
|
4201
4418
|
multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
|
|
4202
4419
|
browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
|
|
4420
|
+
ownerAddress: typeof entry.content.ownerAddress === "string" ? entry.content.ownerAddress : void 0,
|
|
4421
|
+
publisherAddress: typeof entry.content.publisherAddress === "string" ? entry.content.publisherAddress : void 0,
|
|
4203
4422
|
updatedAt: entry.content.updatedAt
|
|
4204
4423
|
} : null
|
|
4205
4424
|
};
|
|
@@ -4271,9 +4490,9 @@ var SponsorRelayController = class {
|
|
|
4271
4490
|
this.patch({ errorText: "Connect MetaMask before deploying." });
|
|
4272
4491
|
return;
|
|
4273
4492
|
}
|
|
4274
|
-
if (!this.state.manifest ||
|
|
4493
|
+
if (!this.state.manifest || this.state.rootfsHealth.tone !== "ok" || !this.state.pricingSummary.pricing || !this.state.pricingSummary.tier) {
|
|
4275
4494
|
this.patch({
|
|
4276
|
-
errorText: "Manifest, rootfs, and pricing must be ready before deploying."
|
|
4495
|
+
errorText: this.state.rootfsHealth.tone === "error" ? `Cannot deploy: ${this.state.rootfsHealth.detail ?? this.state.rootfsHealth.label}` : "Manifest, rootfs, and pricing must be ready before deploying."
|
|
4277
4496
|
});
|
|
4278
4497
|
return;
|
|
4279
4498
|
}
|
|
@@ -4315,6 +4534,7 @@ var SponsorRelayController = class {
|
|
|
4315
4534
|
for (const [candidateIndex, candidateCrn] of orderedCrns.entries()) {
|
|
4316
4535
|
let attemptItemHash = null;
|
|
4317
4536
|
let attemptDeploymentToken = null;
|
|
4537
|
+
let attemptProfile = null;
|
|
4318
4538
|
const crnAttemptLabel = `CRN ${candidateIndex + 1}/${orderedCrns.length}`;
|
|
4319
4539
|
const crnDisplayName = candidateCrn.name ?? candidateCrn.hash;
|
|
4320
4540
|
this.emitProgress({
|
|
@@ -4326,6 +4546,7 @@ var SponsorRelayController = class {
|
|
|
4326
4546
|
});
|
|
4327
4547
|
try {
|
|
4328
4548
|
const profile = relayProfileForManifest(this.state.manifest);
|
|
4549
|
+
attemptProfile = profile;
|
|
4329
4550
|
const sshPublicKey = profile === "uc-go-peer" ? appendDeploymentTokenToSshPublicKey(
|
|
4330
4551
|
this.state.sshPublicKey.trim(),
|
|
4331
4552
|
this.state.wallet.address,
|
|
@@ -4561,6 +4782,106 @@ var SponsorRelayController = class {
|
|
|
4561
4782
|
itemHash: attemptItemHash,
|
|
4562
4783
|
error: lastError.message
|
|
4563
4784
|
});
|
|
4785
|
+
if (attemptItemHash && attemptDeploymentToken && attemptProfile && !failedDueToInvalidIpv6) {
|
|
4786
|
+
try {
|
|
4787
|
+
const latestRuntime = await fetchVmRuntime({
|
|
4788
|
+
itemHash: attemptItemHash,
|
|
4789
|
+
fetch: (url, init) => fetch(url, init),
|
|
4790
|
+
crnHash: candidateCrn.hash,
|
|
4791
|
+
crns: this.state.crns,
|
|
4792
|
+
crnListUrl: this.props.crnListUrl,
|
|
4793
|
+
requirePublicGuestIpv6ForProxy: true
|
|
4794
|
+
});
|
|
4795
|
+
if (latestRuntime.diagnostics?.state === "ready") {
|
|
4796
|
+
const reconciledRelayMetadata = await fetchRelayMetadataForRuntime({
|
|
4797
|
+
runtime: latestRuntime,
|
|
4798
|
+
fetch: (url, init) => fetch(url, init),
|
|
4799
|
+
preferSecureMetadata: attemptProfile === "uc-go-peer" && Boolean(latestRuntime.proxyUrl),
|
|
4800
|
+
attempts: 3,
|
|
4801
|
+
delayMs: 1e3,
|
|
4802
|
+
timeoutMs: 15e3
|
|
4803
|
+
}).catch(() => null);
|
|
4804
|
+
if (reconciledRelayMetadata) {
|
|
4805
|
+
const registrationId = createRelayBootstrapRegistrationId(
|
|
4806
|
+
attemptProfile,
|
|
4807
|
+
this.state.instanceName.trim(),
|
|
4808
|
+
attemptItemHash
|
|
4809
|
+
);
|
|
4810
|
+
const visibleRegistration = await waitForRelayBootstrapRegistration({
|
|
4811
|
+
registrationId,
|
|
4812
|
+
peerId: reconciledRelayMetadata.peerId,
|
|
4813
|
+
fetch: asJsonFetch,
|
|
4814
|
+
apiHost: this.client.apiHost,
|
|
4815
|
+
attempts: 3,
|
|
4816
|
+
delayMs: 1e3
|
|
4817
|
+
}).catch(() => null);
|
|
4818
|
+
if (visibleRegistration) {
|
|
4819
|
+
this.trace("deploy:recovered-after-error", {
|
|
4820
|
+
itemHash: attemptItemHash,
|
|
4821
|
+
registrationId,
|
|
4822
|
+
peerId: reconciledRelayMetadata.peerId,
|
|
4823
|
+
error: lastError.message
|
|
4824
|
+
});
|
|
4825
|
+
this.rememberRuntimeDetails(attemptItemHash, {
|
|
4826
|
+
messageStatus: "processed",
|
|
4827
|
+
allocationSource: latestRuntime.allocation?.source ?? null,
|
|
4828
|
+
crnUrl: latestRuntime.allocation?.crnUrl ?? latestRuntime.execution?.crnUrl ?? null,
|
|
4829
|
+
hostIpv4: latestRuntime.hostIpv4 ?? null,
|
|
4830
|
+
ipv6: latestRuntime.ipv6 ?? null,
|
|
4831
|
+
vmIpv4: latestRuntime.execution?.networking?.ipv4_ip ?? null,
|
|
4832
|
+
webUrl: latestRuntime.webAccessUrl ?? latestRuntime.proxyUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null,
|
|
4833
|
+
sshCommand: latestRuntime.sshCommand ?? null,
|
|
4834
|
+
mappedPorts: latestRuntime.execution != null ? mappedPorts(latestRuntime.execution) : runtimeMappedPorts(latestRuntime.mappedPorts),
|
|
4835
|
+
execution: latestRuntime.execution ?? null,
|
|
4836
|
+
error: null
|
|
4837
|
+
});
|
|
4838
|
+
this.patch({
|
|
4839
|
+
busy: { deploying: false }
|
|
4840
|
+
});
|
|
4841
|
+
await this.refresh();
|
|
4842
|
+
this.lastCompletedDeploymentHash = attemptItemHash;
|
|
4843
|
+
this.emitProgress({
|
|
4844
|
+
stage: "completed",
|
|
4845
|
+
label: "Relay ready",
|
|
4846
|
+
progress: 100,
|
|
4847
|
+
status: "success",
|
|
4848
|
+
itemHash: attemptItemHash,
|
|
4849
|
+
detail: "The relay completed startup after a transient UI polling error. Runtime and bootstrap registration are confirmed."
|
|
4850
|
+
});
|
|
4851
|
+
return;
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
} catch (reconcileError) {
|
|
4856
|
+
this.trace("deploy:recovery-check-error", {
|
|
4857
|
+
itemHash: attemptItemHash,
|
|
4858
|
+
error: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
|
|
4859
|
+
});
|
|
4860
|
+
}
|
|
4861
|
+
await this.refresh().catch(() => void 0);
|
|
4862
|
+
const recoveredRegistration = this.state.bootstrapRegistrations.some(
|
|
4863
|
+
(entry) => entry.instanceItemHash === attemptItemHash && entry.confirmed
|
|
4864
|
+
);
|
|
4865
|
+
if (recoveredRegistration) {
|
|
4866
|
+
this.trace("deploy:recovered-from-refresh-state", {
|
|
4867
|
+
itemHash: attemptItemHash,
|
|
4868
|
+
error: lastError.message
|
|
4869
|
+
});
|
|
4870
|
+
this.patch({
|
|
4871
|
+
busy: { deploying: false }
|
|
4872
|
+
});
|
|
4873
|
+
this.lastCompletedDeploymentHash = attemptItemHash;
|
|
4874
|
+
this.emitProgress({
|
|
4875
|
+
stage: "completed",
|
|
4876
|
+
label: "Relay ready",
|
|
4877
|
+
progress: 100,
|
|
4878
|
+
status: "success",
|
|
4879
|
+
itemHash: attemptItemHash,
|
|
4880
|
+
detail: "The relay completed startup after a transient browser-side polling error. Runtime and bootstrap registration are confirmed."
|
|
4881
|
+
});
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4564
4885
|
if (attemptItemHash) {
|
|
4565
4886
|
this.emitProgress({
|
|
4566
4887
|
stage: "deployment-confirmed",
|
|
@@ -4571,6 +4892,7 @@ var SponsorRelayController = class {
|
|
|
4571
4892
|
detail: failedDueToInvalidIpv6 ? `${attemptLabel} exposed runtime networking that cannot support proxy-backed HTTPS. Cleaning up this attempt before retrying another CRN.` : `${attemptLabel} failed: ${lastError.message} Cleaning up this attempt before retrying another CRN.`
|
|
4572
4893
|
});
|
|
4573
4894
|
if (attemptDeploymentToken) {
|
|
4895
|
+
let cleanupAggregateItemHash = null;
|
|
4574
4896
|
this.emitProgress({
|
|
4575
4897
|
stage: "deployment-confirmed",
|
|
4576
4898
|
label: "Cleaning up failed attempt",
|
|
@@ -4592,6 +4914,7 @@ var SponsorRelayController = class {
|
|
|
4592
4914
|
apiHost: this.client.apiHost,
|
|
4593
4915
|
sync: true
|
|
4594
4916
|
}).then((cleanupResult) => {
|
|
4917
|
+
cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
|
|
4595
4918
|
this.trace("deploy:bootstrap-config-cleanup-complete", {
|
|
4596
4919
|
itemHash: attemptItemHash,
|
|
4597
4920
|
deploymentToken: attemptDeploymentToken,
|
|
@@ -4605,6 +4928,12 @@ var SponsorRelayController = class {
|
|
|
4605
4928
|
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
4606
4929
|
});
|
|
4607
4930
|
});
|
|
4931
|
+
await this.cleanupStaleVmBootstrapConfigHistory({
|
|
4932
|
+
itemHash: attemptItemHash,
|
|
4933
|
+
currentAggregateItemHash: cleanupAggregateItemHash,
|
|
4934
|
+
stage: "deployment-confirmed",
|
|
4935
|
+
progress: 18
|
|
4936
|
+
});
|
|
4608
4937
|
}
|
|
4609
4938
|
this.emitProgress({
|
|
4610
4939
|
stage: "deployment-confirmed",
|
|
@@ -193,7 +193,7 @@ export let apiHost = undefined
|
|
|
193
193
|
|
|
194
194
|
<div class="actions">
|
|
195
195
|
{#if state.wallet.connected}
|
|
196
|
-
<button class="primary" type="button" on:click={() => controller.deploy()} disabled={state.busy.deploying ||
|
|
196
|
+
<button class="primary" type="button" on:click={() => controller.deploy()} disabled={state.busy.deploying || state.rootfsHealth.tone !== 'ok'}>
|
|
197
197
|
{state.busy.deploying ? 'Deploying…' : 'Deploy Relay'}
|
|
198
198
|
</button>
|
|
199
199
|
{:else}
|