@le-space/ui 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/shared/index.js CHANGED
@@ -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.6.1";
30
+ var UI_PACKAGE_VERSION = "0.6.2";
31
31
 
32
32
  // src/shared/format.ts
33
33
  function shortHash(value, head = 8, tail = 6) {
@@ -2273,6 +2273,45 @@ async function configureOrbitdbRelaySetup2(args) {
2273
2273
  clearTimeout(timeout);
2274
2274
  }
2275
2275
  }
2276
+ async function configureUcanStore(args) {
2277
+ const controller = new AbortController();
2278
+ const timeout = setTimeout(
2279
+ () => controller.abort(),
2280
+ Number(args.timeoutMs ?? 18e4)
2281
+ );
2282
+ const payload = {
2283
+ public_ipv4: args.hostIpv4,
2284
+ public_ipv6: args.publicIpv6 ?? void 0,
2285
+ proxy_url: args.proxyUrl ?? void 0,
2286
+ webauthn_origin: args.webauthnOrigin ?? void 0,
2287
+ webauthn_origin_fallbacks: args.webauthnOriginFallbacks ?? void 0,
2288
+ admin_did: args.adminDid ?? void 0,
2289
+ bootstrap_package: args.bootstrapPackage ?? void 0,
2290
+ no_start: args.noStart === true ? true : void 0
2291
+ };
2292
+ try {
2293
+ const response = await args.fetch(
2294
+ `http://${args.hostIpv4}:${args.setupPort}/configure`,
2295
+ {
2296
+ method: "POST",
2297
+ headers: {
2298
+ "content-type": "application/json"
2299
+ },
2300
+ body: JSON.stringify(payload),
2301
+ signal: controller.signal
2302
+ }
2303
+ );
2304
+ const responsePayload = await response.json().catch(() => null);
2305
+ if (!response.ok) {
2306
+ throw new Error(
2307
+ `Service setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
2308
+ );
2309
+ }
2310
+ return responsePayload;
2311
+ } finally {
2312
+ clearTimeout(timeout);
2313
+ }
2314
+ }
2276
2315
  async function fetchUcGoPeerMetadata(args) {
2277
2316
  const attempts = Math.max(1, Number(args.attempts ?? 60));
2278
2317
  const delayMs = Math.max(0, Number(args.delayMs ?? 3e3));
@@ -2340,7 +2379,7 @@ async function fetchUcGoPeerMetadata(args) {
2340
2379
  }
2341
2380
  }
2342
2381
  throw new Error(
2343
- `Relay metadata did not become ready after ${attempts} attempts: ${lastError ? lastError : typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
2382
+ `Guest metadata did not become ready after ${attempts} attempts: ${lastError ? lastError : typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
2344
2383
  );
2345
2384
  }
2346
2385
 
@@ -3272,7 +3311,130 @@ function createDeploymentProgressEmitter() {
3272
3311
  };
3273
3312
  }
3274
3313
 
3314
+ // ../shared-types/src/ucan-store-bootstrap.ts
3315
+ function isRecord(value) {
3316
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3317
+ }
3318
+ function normalizeNonEmptyString(value) {
3319
+ if (typeof value !== "string") return null;
3320
+ const normalized = value.trim();
3321
+ return normalized ? normalized : null;
3322
+ }
3323
+ function normalizeDid(value) {
3324
+ const did = normalizeNonEmptyString(value);
3325
+ return did && did.startsWith("did:") ? did : null;
3326
+ }
3327
+ function normalizeOrigin(value) {
3328
+ const raw = normalizeNonEmptyString(value);
3329
+ if (!raw) return null;
3330
+ try {
3331
+ const parsed = new URL(raw);
3332
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || !parsed.hostname || parsed.pathname && parsed.pathname !== "/" || parsed.search || parsed.hash) {
3333
+ return null;
3334
+ }
3335
+ return parsed.origin;
3336
+ } catch {
3337
+ return null;
3338
+ }
3339
+ }
3340
+ function normalizeExpiration(value) {
3341
+ if (value == null) return void 0;
3342
+ if (value === null) return null;
3343
+ if (!Number.isInteger(value) || Number(value) < 0) return void 0;
3344
+ return Number(value);
3345
+ }
3346
+ function validateUcanStoreBootstrapPackage(value) {
3347
+ if (!isRecord(value)) {
3348
+ return {
3349
+ valid: false,
3350
+ errors: ["Bootstrap package must be a JSON object."],
3351
+ bootstrapPackage: null
3352
+ };
3353
+ }
3354
+ const errors = [];
3355
+ const operatorAddress = normalizeNonEmptyString(value.operatorAddress);
3356
+ if (!operatorAddress || !/^0x[a-fA-F0-9]{40}$/.test(operatorAddress)) {
3357
+ errors.push("operatorAddress must be a 0x-prefixed 20-byte Ethereum address.");
3358
+ }
3359
+ const adminDid = normalizeDid(value.adminDid);
3360
+ if (!adminDid) {
3361
+ errors.push("adminDid must be a non-empty DID string.");
3362
+ }
3363
+ const serviceDidRaw = value.serviceDid;
3364
+ const serviceDid = serviceDidRaw == null || serviceDidRaw === "" ? null : normalizeDid(serviceDidRaw);
3365
+ if (serviceDidRaw != null && serviceDidRaw !== "" && !serviceDid) {
3366
+ errors.push("serviceDid must be empty or a non-empty DID string.");
3367
+ }
3368
+ const spaceDid = normalizeDid(value.spaceDid);
3369
+ if (!spaceDid) {
3370
+ errors.push("spaceDid must be a non-empty DID string.");
3371
+ }
3372
+ const rootDelegationProof = normalizeNonEmptyString(value.rootDelegationProof);
3373
+ if (!rootDelegationProof) {
3374
+ errors.push("rootDelegationProof must be a non-empty proof string.");
3375
+ }
3376
+ const allowedCapabilities = Array.isArray(value.allowedCapabilities) ? value.allowedCapabilities.map((entry) => normalizeNonEmptyString(entry)).filter((entry) => Boolean(entry)) : [];
3377
+ if (allowedCapabilities.length === 0) {
3378
+ errors.push("allowedCapabilities must contain at least one capability string.");
3379
+ }
3380
+ const defaultUserDelegationExpiration = normalizeExpiration(
3381
+ value.defaultUserDelegationExpiration
3382
+ );
3383
+ if (value.defaultUserDelegationExpiration != null && defaultUserDelegationExpiration === void 0) {
3384
+ errors.push(
3385
+ "defaultUserDelegationExpiration must be null or a non-negative integer number of seconds."
3386
+ );
3387
+ }
3388
+ const maxUserDelegationExpiration = normalizeExpiration(
3389
+ value.maxUserDelegationExpiration
3390
+ );
3391
+ if (value.maxUserDelegationExpiration != null && maxUserDelegationExpiration === void 0) {
3392
+ errors.push(
3393
+ "maxUserDelegationExpiration must be null or a non-negative integer number of seconds."
3394
+ );
3395
+ }
3396
+ if (typeof defaultUserDelegationExpiration === "number" && typeof maxUserDelegationExpiration === "number" && defaultUserDelegationExpiration > maxUserDelegationExpiration) {
3397
+ errors.push(
3398
+ "defaultUserDelegationExpiration cannot exceed maxUserDelegationExpiration."
3399
+ );
3400
+ }
3401
+ const pwaOrigin = normalizeOrigin(value.pwaOrigin);
3402
+ if (!pwaOrigin) {
3403
+ errors.push("pwaOrigin must be an http(s) origin without path, query, or hash.");
3404
+ }
3405
+ const serviceOrigin = normalizeOrigin(value.serviceOrigin);
3406
+ if (!serviceOrigin) {
3407
+ errors.push("serviceOrigin must be an http(s) origin without path, query, or hash.");
3408
+ }
3409
+ if (errors.length > 0) {
3410
+ return {
3411
+ valid: false,
3412
+ errors,
3413
+ bootstrapPackage: null
3414
+ };
3415
+ }
3416
+ return {
3417
+ valid: true,
3418
+ errors: [],
3419
+ bootstrapPackage: {
3420
+ operatorAddress,
3421
+ adminDid,
3422
+ serviceDid,
3423
+ spaceDid,
3424
+ rootDelegationProof,
3425
+ allowedCapabilities,
3426
+ defaultUserDelegationExpiration,
3427
+ maxUserDelegationExpiration,
3428
+ pwaOrigin,
3429
+ serviceOrigin
3430
+ }
3431
+ };
3432
+ }
3433
+
3275
3434
  // src/shared/controller.ts
3435
+ function deploymentNoun(profile) {
3436
+ return profile === "ucan-store" ? "service" : "relay";
3437
+ }
3276
3438
  function asJsonFetch(input, init) {
3277
3439
  return fetch(input, init).then(async (response) => ({
3278
3440
  ok: response.ok,
@@ -3280,11 +3442,122 @@ function asJsonFetch(input, init) {
3280
3442
  json: async () => await response.json()
3281
3443
  }));
3282
3444
  }
3283
- function isConfirmedRelaySetupResult(value) {
3445
+ function isConfirmedGuestSetupResult(value) {
3284
3446
  return Boolean(
3285
3447
  value && typeof value === "object" && "status" in value && value.status === "configured"
3286
3448
  );
3287
3449
  }
3450
+ function defaultUcanStoreBootstrapInput(props) {
3451
+ return {
3452
+ adminDid: props.ucanStoreBootstrap?.adminDid ?? "",
3453
+ serviceDid: props.ucanStoreBootstrap?.serviceDid ?? "",
3454
+ spaceDid: props.ucanStoreBootstrap?.spaceDid ?? "",
3455
+ rootDelegationProof: props.ucanStoreBootstrap?.rootDelegationProof ?? "",
3456
+ allowedCapabilities: props.ucanStoreBootstrap?.allowedCapabilities ?? "",
3457
+ defaultUserDelegationExpiration: props.ucanStoreBootstrap?.defaultUserDelegationExpiration ?? "",
3458
+ maxUserDelegationExpiration: props.ucanStoreBootstrap?.maxUserDelegationExpiration ?? "",
3459
+ pwaOrigin: props.ucanStoreBootstrap?.pwaOrigin ?? "",
3460
+ serviceOrigin: props.ucanStoreBootstrap?.serviceOrigin ?? ""
3461
+ };
3462
+ }
3463
+ function currentPageOrigin() {
3464
+ try {
3465
+ const origin = globalThis.location?.origin?.trim();
3466
+ return origin ? origin : null;
3467
+ } catch {
3468
+ return null;
3469
+ }
3470
+ }
3471
+ function parseOptionalExpiration(value) {
3472
+ const normalized = value.trim();
3473
+ if (!normalized) return void 0;
3474
+ const parsed = Number.parseInt(normalized, 10);
3475
+ if (!Number.isInteger(parsed) || parsed < 0) {
3476
+ return null;
3477
+ }
3478
+ return parsed;
3479
+ }
3480
+ function parseAllowedCapabilities(value) {
3481
+ return value.split(/[\n,]/).map((entry) => entry.trim()).filter(Boolean);
3482
+ }
3483
+ function draftUcanStoreBootstrapErrors(args) {
3484
+ const errors = [];
3485
+ const { input } = args;
3486
+ if (!args.operatorAddress) {
3487
+ errors.push("Connect MetaMask before configuring the ucan-store bootstrap package.");
3488
+ }
3489
+ if (!input.adminDid.trim().startsWith("did:")) {
3490
+ errors.push("Admin DID must be a non-empty DID string.");
3491
+ }
3492
+ if (input.serviceDid.trim() && !input.serviceDid.trim().startsWith("did:")) {
3493
+ errors.push("Service DID must be empty or a non-empty DID string.");
3494
+ }
3495
+ if (!input.spaceDid.trim().startsWith("did:")) {
3496
+ errors.push("Space DID must be a non-empty DID string.");
3497
+ }
3498
+ if (!input.rootDelegationProof.trim()) {
3499
+ errors.push("Root delegation proof is required.");
3500
+ }
3501
+ if (parseAllowedCapabilities(input.allowedCapabilities).length === 0) {
3502
+ errors.push("Add at least one allowed capability.");
3503
+ }
3504
+ const defaultExpiration = parseOptionalExpiration(
3505
+ input.defaultUserDelegationExpiration
3506
+ );
3507
+ if (defaultExpiration === null) {
3508
+ errors.push(
3509
+ "Default user delegation expiration must be a non-negative integer number of seconds."
3510
+ );
3511
+ }
3512
+ const maxExpiration = parseOptionalExpiration(
3513
+ input.maxUserDelegationExpiration
3514
+ );
3515
+ if (maxExpiration === null) {
3516
+ errors.push(
3517
+ "Max user delegation expiration must be a non-negative integer number of seconds."
3518
+ );
3519
+ }
3520
+ if (typeof defaultExpiration === "number" && typeof maxExpiration === "number" && defaultExpiration > maxExpiration) {
3521
+ errors.push(
3522
+ "Default user delegation expiration cannot exceed the max user delegation expiration."
3523
+ );
3524
+ }
3525
+ const pwaOrigin = input.pwaOrigin.trim() || currentPageOrigin() || "";
3526
+ if (!pwaOrigin) {
3527
+ errors.push("PWA origin is required for the ucan-store bootstrap package.");
3528
+ }
3529
+ return errors;
3530
+ }
3531
+ function buildUcanStoreBootstrapPackage(args) {
3532
+ const candidate = {
3533
+ operatorAddress: args.operatorAddress,
3534
+ adminDid: args.input.adminDid.trim(),
3535
+ serviceDid: args.input.serviceDid.trim() || null,
3536
+ spaceDid: args.input.spaceDid.trim(),
3537
+ rootDelegationProof: args.input.rootDelegationProof.trim(),
3538
+ allowedCapabilities: parseAllowedCapabilities(args.input.allowedCapabilities),
3539
+ defaultUserDelegationExpiration: parseOptionalExpiration(
3540
+ args.input.defaultUserDelegationExpiration
3541
+ ),
3542
+ maxUserDelegationExpiration: parseOptionalExpiration(
3543
+ args.input.maxUserDelegationExpiration
3544
+ ),
3545
+ pwaOrigin: args.input.pwaOrigin.trim() || args.pwaOriginFallback?.trim() || currentPageOrigin() || "",
3546
+ serviceOrigin: args.input.serviceOrigin.trim() || args.serviceOriginFallback?.trim() || ""
3547
+ };
3548
+ if (candidate.defaultUserDelegationExpiration === void 0) {
3549
+ delete candidate.defaultUserDelegationExpiration;
3550
+ }
3551
+ if (candidate.maxUserDelegationExpiration === void 0) {
3552
+ delete candidate.maxUserDelegationExpiration;
3553
+ }
3554
+ if (candidate.serviceDid === null) {
3555
+ delete candidate.serviceDid;
3556
+ }
3557
+ return validateUcanStoreBootstrapPackage(
3558
+ candidate
3559
+ );
3560
+ }
3288
3561
  function defaultState(props = {}) {
3289
3562
  return {
3290
3563
  ready: false,
@@ -3299,6 +3572,7 @@ function defaultState(props = {}) {
3299
3572
  manifestJson: props.manifestJson ?? "",
3300
3573
  sshPublicKey: props.sshPublicKey ?? "",
3301
3574
  instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
3575
+ ucanStoreBootstrap: defaultUcanStoreBootstrapInput(props),
3302
3576
  tierId: DEFAULT_TIER_ID,
3303
3577
  showAdvanced: Boolean(props.manifestJson?.trim() || props.sshPublicKey?.trim()),
3304
3578
  showInstances: props.showInstances ?? true,
@@ -3433,13 +3707,33 @@ async function resolveManifest(args) {
3433
3707
  if (pasted) return pasted;
3434
3708
  return loadRootfsManifest(args.manifestUrl);
3435
3709
  }
3436
- function relayProfileForManifest(manifest) {
3710
+ function deploymentProfileForManifest(manifest) {
3437
3711
  if (manifest?.profile === "uc-go-peer") return "uc-go-peer";
3438
3712
  if (manifest?.profile === "orbitdb-relay") {
3439
3713
  return manifest.profile;
3440
3714
  }
3715
+ if (manifest?.profile === "ucan-store") return "ucan-store";
3716
+ return null;
3717
+ }
3718
+ function bootstrapRelayProfileForManifest(manifest) {
3719
+ const profile = deploymentProfileForManifest(manifest);
3720
+ if (profile === "uc-go-peer" || profile === "orbitdb-relay") {
3721
+ return profile;
3722
+ }
3441
3723
  return null;
3442
3724
  }
3725
+ function supportsBootstrapRegistrationsForManifest(manifest) {
3726
+ return bootstrapRelayProfileForManifest(manifest) != null;
3727
+ }
3728
+ function deploymentReadyLabel(profile) {
3729
+ return profile === "ucan-store" ? "Service ready" : "Relay ready";
3730
+ }
3731
+ function deploymentReadyDetail(profile, fallback) {
3732
+ if (profile === "ucan-store") {
3733
+ return "The upload service runtime is available and the deployment finished successfully.";
3734
+ }
3735
+ return fallback;
3736
+ }
3443
3737
  function registrationIdInstanceItemHash(registrationId) {
3444
3738
  const normalized = String(registrationId ?? "").trim();
3445
3739
  if (!normalized) return null;
@@ -3688,19 +3982,148 @@ var SponsorRelayController = class {
3688
3982
  this.progressEmitter.emit(nextEvent);
3689
3983
  }
3690
3984
  async configureRelayBootstrapRegistration(args) {
3691
- const profile = relayProfileForManifest(this.state.manifest);
3692
- if (!profile) return;
3693
- if (!this.state.wallet.address) {
3985
+ const deploymentProfile = deploymentProfileForManifest(this.state.manifest);
3986
+ const profile = bootstrapRelayProfileForManifest(this.state.manifest);
3987
+ if (!profile && deploymentProfile !== "ucan-store") return;
3988
+ if (profile && !this.state.wallet.address) {
3694
3989
  throw new Error("A connected wallet is required to register relay bootstrap addresses.");
3695
3990
  }
3696
- if (!args.runtime.hostIpv4) {
3697
- throw new Error("Relay runtime did not expose a host IPv4 address.");
3991
+ const runtimeHostIpv4 = args.runtime.hostIpv4;
3992
+ if (!runtimeHostIpv4) {
3993
+ throw new Error(
3994
+ `${deploymentProfile === "ucan-store" ? "Service" : "Relay"} runtime did not expose a host IPv4 address.`
3995
+ );
3698
3996
  }
3699
3997
  let runtime = args.runtime;
3700
3998
  let mappedPorts2 = runtime.mappedPorts ?? {};
3701
3999
  const setupPort = mappedPorts2["80"]?.host ?? null;
3702
4000
  if (!setupPort) {
3703
- throw new Error("Relay runtime did not expose the temporary setup endpoint.");
4001
+ throw new Error(
4002
+ `${deploymentProfile === "ucan-store" ? "Service" : "Relay"} runtime did not expose the temporary setup endpoint.`
4003
+ );
4004
+ }
4005
+ if (deploymentProfile === "ucan-store") {
4006
+ const serviceHostIpv4 = runtimeHostIpv4;
4007
+ const serviceProxyUrl = runtime.proxyUrl;
4008
+ if (!serviceProxyUrl) {
4009
+ throw new Error("Service runtime did not expose a proxy URL.");
4010
+ }
4011
+ const walletAddress2 = this.state.wallet.address;
4012
+ if (!walletAddress2) {
4013
+ throw new Error("A connected wallet is required to configure the ucan-store bootstrap package.");
4014
+ }
4015
+ const bootstrapValidation = buildUcanStoreBootstrapPackage({
4016
+ input: this.state.ucanStoreBootstrap,
4017
+ operatorAddress: walletAddress2,
4018
+ serviceOriginFallback: serviceProxyUrl,
4019
+ pwaOriginFallback: currentPageOrigin()
4020
+ });
4021
+ if (!bootstrapValidation.valid || !bootstrapValidation.bootstrapPackage) {
4022
+ throw new Error(
4023
+ bootstrapValidation.errors[0] ?? "The ucan-store bootstrap package is invalid."
4024
+ );
4025
+ }
4026
+ this.emitProgress({
4027
+ stage: "deployment-confirmed",
4028
+ label: "Configuring service",
4029
+ progress: 93,
4030
+ status: "info",
4031
+ itemHash: args.itemHash,
4032
+ detail: "Waiting for the guest setup endpoint before applying the upload service configuration."
4033
+ });
4034
+ const setupHealth = await waitForSetupEndpoint({
4035
+ hostIpv4: serviceHostIpv4,
4036
+ setupPort,
4037
+ fetch: (url, init) => fetch(url, init),
4038
+ attempts: 15,
4039
+ delayMs: 4e3,
4040
+ httpTimeoutMs: 1e4,
4041
+ onAttempt: (result, attempt, attempts) => {
4042
+ this.emitProgress({
4043
+ stage: "deployment-confirmed",
4044
+ label: "Waiting for guest setup",
4045
+ progress: 93,
4046
+ status: result.ok ? "success" : "info",
4047
+ itemHash: args.itemHash,
4048
+ detail: result.ok ? `Setup endpoint ${attempt}/${attempts}: ready at http://${serviceHostIpv4}:${setupPort}/health.` : `Setup endpoint ${attempt}/${attempts}: waiting for http://${serviceHostIpv4}:${setupPort}/health.`
4049
+ });
4050
+ }
4051
+ });
4052
+ if (!setupHealth.ok) {
4053
+ throw new Error(
4054
+ `Service setup endpoint did not become reachable at http://${serviceHostIpv4}:${setupPort}/health.`
4055
+ );
4056
+ }
4057
+ this.emitProgress({
4058
+ stage: "deployment-confirmed",
4059
+ label: "Applying service config",
4060
+ progress: 94,
4061
+ status: "info",
4062
+ itemHash: args.itemHash,
4063
+ detail: "Publishing the public HTTPS origin into the guest upload service configuration."
4064
+ });
4065
+ const configureResult = await configureUcanStore({
4066
+ hostIpv4: serviceHostIpv4,
4067
+ publicIpv6: runtime.ipv6,
4068
+ setupPort,
4069
+ proxyUrl: serviceProxyUrl,
4070
+ webauthnOrigin: serviceProxyUrl,
4071
+ adminDid: bootstrapValidation.bootstrapPackage.adminDid,
4072
+ bootstrapPackage: bootstrapValidation.bootstrapPackage,
4073
+ fetch: (url, init) => fetch(url, init),
4074
+ timeoutMs: 18e4
4075
+ });
4076
+ if (!isConfirmedGuestSetupResult(configureResult)) {
4077
+ throw new Error(
4078
+ `Service guest configuration could not be confirmed at http://${serviceHostIpv4}:${setupPort}/configure.`
4079
+ );
4080
+ }
4081
+ this.emitProgress({
4082
+ stage: "deployment-confirmed",
4083
+ label: "Waiting for service metadata",
4084
+ progress: 95,
4085
+ status: "info",
4086
+ itemHash: args.itemHash,
4087
+ detail: "Waiting for the upload service metadata endpoint to report the public service DID and PWA environment."
4088
+ });
4089
+ const serviceMetadata = await fetchUcGoPeerMetadata({
4090
+ hostIpv4: serviceHostIpv4,
4091
+ setupPort,
4092
+ fetch: (url, init) => fetch(url, init),
4093
+ attempts: 80,
4094
+ delayMs: 3e3,
4095
+ timeoutMs: 24e4,
4096
+ isReady: ({ payload, ok }) => {
4097
+ if (!ok || !payload || typeof payload !== "object") return false;
4098
+ const metadata2 = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
4099
+ const pwaEnv = metadata2?.pwa_env && typeof metadata2.pwa_env === "object" ? metadata2.pwa_env : null;
4100
+ return Boolean(
4101
+ typeof metadata2?.upload_service_did === "string" && metadata2.upload_service_did.length > 0 && typeof pwaEnv?.VITE_UPLOAD_SERVICE_URL === "string" && pwaEnv.VITE_UPLOAD_SERVICE_URL.length > 0
4102
+ );
4103
+ },
4104
+ onAttempt: ({ ready, attempt, attempts, requestUrl }) => {
4105
+ this.emitProgress({
4106
+ stage: "deployment-confirmed",
4107
+ label: ready ? "Service metadata ready" : "Waiting for service metadata",
4108
+ progress: 95,
4109
+ status: ready ? "success" : "info",
4110
+ itemHash: args.itemHash,
4111
+ detail: ready ? `Service metadata ${attempt}/${attempts}: public upload service wiring is ready.` : `Service metadata ${attempt}/${attempts}: waiting for ${requestUrl}.`
4112
+ });
4113
+ }
4114
+ });
4115
+ const metadata = serviceMetadata && typeof serviceMetadata === "object" ? serviceMetadata.metadata ?? null : null;
4116
+ if (!metadata || typeof metadata !== "object" || typeof metadata.upload_service_did !== "string") {
4117
+ throw new Error(
4118
+ "Service metadata did not include the public upload service DID."
4119
+ );
4120
+ }
4121
+ return;
4122
+ }
4123
+ if (!profile) return;
4124
+ const walletAddress = this.state.wallet.address;
4125
+ if (!walletAddress) {
4126
+ throw new Error("A connected wallet is required to register relay bootstrap addresses.");
3704
4127
  }
3705
4128
  let bootstrapSignalRelayMetadata = null;
3706
4129
  if (profile === "orbitdb-relay") {
@@ -3713,7 +4136,7 @@ var SponsorRelayController = class {
3713
4136
  detail: "Waiting for the guest setup endpoint before collecting relay metadata."
3714
4137
  });
3715
4138
  const setupHealth = await waitForSetupEndpoint({
3716
- hostIpv4: args.runtime.hostIpv4,
4139
+ hostIpv4: runtimeHostIpv4,
3717
4140
  setupPort,
3718
4141
  fetch: (url, init) => fetch(url, init),
3719
4142
  attempts: 15,
@@ -3726,13 +4149,13 @@ var SponsorRelayController = class {
3726
4149
  progress: 93,
3727
4150
  status: result.ok ? "success" : "info",
3728
4151
  itemHash: args.itemHash,
3729
- detail: result.ok ? `Setup endpoint ${attempt}/${attempts}: ready at http://${args.runtime.hostIpv4}:${setupPort}/health.` : `Setup endpoint ${attempt}/${attempts}: waiting for http://${args.runtime.hostIpv4}:${setupPort}/health.`
4152
+ detail: result.ok ? `Setup endpoint ${attempt}/${attempts}: ready at http://${runtimeHostIpv4}:${setupPort}/health.` : `Setup endpoint ${attempt}/${attempts}: waiting for http://${runtimeHostIpv4}:${setupPort}/health.`
3730
4153
  });
3731
4154
  }
3732
4155
  });
3733
4156
  if (!setupHealth.ok) {
3734
4157
  throw new Error(
3735
- `Relay setup endpoint did not become reachable at http://${args.runtime.hostIpv4}:${setupPort}/health.`
4158
+ `Relay setup endpoint did not become reachable at http://${runtimeHostIpv4}:${setupPort}/health.`
3736
4159
  );
3737
4160
  }
3738
4161
  this.emitProgress({
@@ -3749,7 +4172,7 @@ var SponsorRelayController = class {
3749
4172
  throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
3750
4173
  }
3751
4174
  const configureResult = await configureOrbitdbRelaySetup2({
3752
- hostIpv4: args.runtime.hostIpv4,
4175
+ hostIpv4: runtimeHostIpv4,
3753
4176
  publicIpv6: args.runtime.ipv6,
3754
4177
  setupPort,
3755
4178
  tcpPort,
@@ -3762,7 +4185,7 @@ var SponsorRelayController = class {
3762
4185
  fetch: (url, init) => fetch(url, init),
3763
4186
  timeoutMs: 18e4
3764
4187
  });
3765
- if (!isConfirmedRelaySetupResult(configureResult)) {
4188
+ if (!isConfirmedGuestSetupResult(configureResult)) {
3766
4189
  throw new Error(
3767
4190
  `Relay guest configuration could not be confirmed at http://${args.runtime.hostIpv4}:${setupPort}/configure.`
3768
4191
  );
@@ -3820,7 +4243,7 @@ var SponsorRelayController = class {
3820
4243
  "Relay runtime lost its host IPv4 address while waiting for HTTPS activation."
3821
4244
  );
3822
4245
  }
3823
- const runtimeHostIpv4 = runtime.hostIpv4;
4246
+ const runtimeHostIpv42 = runtime.hostIpv4;
3824
4247
  const activeProxyUrl = runtime.webAccess?.active === true ? runtime.proxyUrl ?? null : null;
3825
4248
  this.emitProgress({
3826
4249
  stage: "deployment-confirmed",
@@ -3833,13 +4256,13 @@ var SponsorRelayController = class {
3833
4256
  const record = {
3834
4257
  deploymentToken: args.deploymentToken,
3835
4258
  profile,
3836
- ownerAddress: this.state.wallet.address,
4259
+ ownerAddress: walletAddress,
3837
4260
  instanceItemHash: args.itemHash,
3838
4261
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3839
4262
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3840
4263
  status: "pending",
3841
4264
  runtime: {
3842
- publicIpv4: runtimeHostIpv4,
4265
+ publicIpv4: runtimeHostIpv42,
3843
4266
  publicIpv6: runtime.ipv6 ?? null,
3844
4267
  proxyUrl: activeProxyUrl,
3845
4268
  mappedPorts: mappedPorts2
@@ -3853,7 +4276,7 @@ var SponsorRelayController = class {
3853
4276
  }
3854
4277
  };
3855
4278
  const bootstrapConfigPublication = await publishVmBootstrapConfig({
3856
- sender: this.state.wallet.address,
4279
+ sender: walletAddress,
3857
4280
  record,
3858
4281
  signer: personalSign,
3859
4282
  hasher: sha256Hex,
@@ -3880,7 +4303,7 @@ var SponsorRelayController = class {
3880
4303
  });
3881
4304
  const bootstrapConfigSignal = await waitForVmBootstrapConfigSignal({
3882
4305
  deploymentToken: args.deploymentToken,
3883
- ownerAddress: this.state.wallet.address,
4306
+ ownerAddress: walletAddress,
3884
4307
  instanceItemHash: args.itemHash,
3885
4308
  fetch: asJsonFetch,
3886
4309
  apiHost: this.client.apiHost,
@@ -4052,7 +4475,7 @@ var SponsorRelayController = class {
4052
4475
  detail: "Guest bootstrap registration was delayed. Publishing the relay bootstrap record from the browser."
4053
4476
  });
4054
4477
  const fallbackPublication = await publishRelayBootstrapRegistration({
4055
- sender: this.state.wallet.address,
4478
+ sender: walletAddress,
4056
4479
  signer: personalSign,
4057
4480
  hasher: sha256Hex,
4058
4481
  fetch: asJsonFetch,
@@ -4072,7 +4495,7 @@ var SponsorRelayController = class {
4072
4495
  fallbackPublication
4073
4496
  });
4074
4497
  const fallbackVisibleRegistration = await waitForRelayBootstrapRegistration({
4075
- sender: this.state.wallet.address,
4498
+ sender: walletAddress,
4076
4499
  registrationId,
4077
4500
  peerId,
4078
4501
  fetch: asJsonFetch,
@@ -4105,7 +4528,7 @@ var SponsorRelayController = class {
4105
4528
  if (profile === "uc-go-peer" && args.deploymentToken) {
4106
4529
  let cleanupAggregateItemHash = null;
4107
4530
  await deleteVmBootstrapConfig({
4108
- sender: this.state.wallet.address,
4531
+ sender: walletAddress,
4109
4532
  deploymentToken: args.deploymentToken,
4110
4533
  signer: personalSign,
4111
4534
  hasher: sha256Hex,
@@ -4271,17 +4694,35 @@ var SponsorRelayController = class {
4271
4694
  });
4272
4695
  return;
4273
4696
  }
4697
+ const currentProfile = deploymentProfileForManifest(this.state.manifest);
4274
4698
  const hasConfirmedBootstrapRegistration = this.state.bootstrapRegistrations.some(
4275
4699
  (entry) => entry.instanceItemHash === itemHash && entry.confirmed
4276
4700
  );
4701
+ if (currentProfile === "ucan-store") {
4702
+ this.emitProgress({
4703
+ stage: "completed",
4704
+ label: deploymentReadyLabel(currentProfile),
4705
+ progress: 100,
4706
+ status: "success",
4707
+ itemHash,
4708
+ detail: deploymentReadyDetail(
4709
+ currentProfile,
4710
+ "Runtime networking is available and the deployment finished successfully."
4711
+ )
4712
+ });
4713
+ return;
4714
+ }
4277
4715
  if (hasConfirmedBootstrapRegistration) {
4278
4716
  this.emitProgress({
4279
4717
  stage: "completed",
4280
- label: "Relay ready",
4718
+ label: deploymentReadyLabel(currentProfile),
4281
4719
  progress: 100,
4282
4720
  status: "success",
4283
4721
  itemHash,
4284
- detail: "Runtime networking is available and the relay bootstrap registration is confirmed on Aleph."
4722
+ detail: deploymentReadyDetail(
4723
+ currentProfile,
4724
+ "Runtime networking is available and the relay bootstrap registration is confirmed on Aleph."
4725
+ )
4285
4726
  });
4286
4727
  return;
4287
4728
  }
@@ -4291,7 +4732,7 @@ var SponsorRelayController = class {
4291
4732
  progress: 92,
4292
4733
  status: "info",
4293
4734
  itemHash,
4294
- detail: "Deployment processed and runtime networking is available. Finishing relay setup and bootstrap verification."
4735
+ detail: `Deployment processed and runtime networking is available. Finishing ${deploymentNoun(currentProfile)} setup and verification.`
4295
4736
  });
4296
4737
  }
4297
4738
  canSkipRuntimeRefresh(instance) {
@@ -4404,6 +4845,15 @@ var SponsorRelayController = class {
4404
4845
  setInstanceName(instanceName) {
4405
4846
  this.patch({ instanceName });
4406
4847
  }
4848
+ setUcanStoreBootstrapField(field, value) {
4849
+ this.patch({
4850
+ ucanStoreBootstrap: {
4851
+ ...this.state.ucanStoreBootstrap,
4852
+ [field]: value
4853
+ },
4854
+ showAdvanced: this.state.showAdvanced || Boolean(value.trim())
4855
+ });
4856
+ }
4407
4857
  setTierId(tierId) {
4408
4858
  this.patch({ tierId });
4409
4859
  this.recomputePricingSummary();
@@ -4489,6 +4939,7 @@ var SponsorRelayController = class {
4489
4939
  }
4490
4940
  }
4491
4941
  async refresh() {
4942
+ const currentProfile = deploymentProfileForManifest(this.state.manifest);
4492
4943
  this.trace("refresh:start", {
4493
4944
  wallet: this.state.wallet.address,
4494
4945
  manifestUrl: this.state.manifestUrl,
@@ -4497,7 +4948,7 @@ var SponsorRelayController = class {
4497
4948
  this.patch({
4498
4949
  busy: { refreshing: true },
4499
4950
  errorText: null,
4500
- statusText: "Refreshing relay sponsor data"
4951
+ statusText: currentProfile === "ucan-store" ? "Refreshing service deployment data" : "Refreshing relay deployment data"
4501
4952
  });
4502
4953
  try {
4503
4954
  const [pricingSummary, crns] = await Promise.all([
@@ -4509,6 +4960,8 @@ var SponsorRelayController = class {
4509
4960
  manifestJson: this.state.manifestJson
4510
4961
  }).catch((error) => manifestLoadErrorState(error));
4511
4962
  const manifest = manifestState.manifest;
4963
+ const resolvedProfile = deploymentProfileForManifest(manifest);
4964
+ const bootstrapEnabled = supportsBootstrapRegistrationsForManifest(manifest);
4512
4965
  let balance = this.state.balance;
4513
4966
  let instances = [];
4514
4967
  let bootstrapRegistrations = [];
@@ -4517,10 +4970,10 @@ var SponsorRelayController = class {
4517
4970
  const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
4518
4971
  this.client.fetchBalance(this.state.wallet.address),
4519
4972
  this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([]),
4520
- fetchAlephBootstrapPosts({
4973
+ bootstrapEnabled ? fetchAlephBootstrapPosts({
4521
4974
  apiHost: this.client.apiHost,
4522
4975
  fetch
4523
- }).catch(() => [])
4976
+ }).catch(() => []) : Promise.resolve([])
4524
4977
  ]);
4525
4978
  balance = nextBalance;
4526
4979
  instances = await Promise.all(
@@ -4650,7 +5103,7 @@ var SponsorRelayController = class {
4650
5103
  bootstrapRegistrations,
4651
5104
  orphanBootstrapRegistrations,
4652
5105
  busy: { refreshing: false },
4653
- statusText: "Relay sponsor data ready"
5106
+ statusText: resolvedProfile === "ucan-store" ? "Service deployment data ready" : "Relay deployment data ready"
4654
5107
  });
4655
5108
  this.trace("refresh:success", {
4656
5109
  manifestValid: manifestState.valid,
@@ -4687,6 +5140,16 @@ var SponsorRelayController = class {
4687
5140
  });
4688
5141
  return;
4689
5142
  }
5143
+ if (deploymentProfileForManifest(this.state.manifest) === "ucan-store") {
5144
+ const draftErrors = draftUcanStoreBootstrapErrors({
5145
+ input: this.state.ucanStoreBootstrap,
5146
+ operatorAddress: this.state.wallet.address
5147
+ });
5148
+ if (draftErrors.length > 0) {
5149
+ this.patch({ errorText: draftErrors[0] });
5150
+ return;
5151
+ }
5152
+ }
4690
5153
  this.patch({
4691
5154
  busy: { deploying: true },
4692
5155
  errorText: null,
@@ -4736,7 +5199,7 @@ var SponsorRelayController = class {
4736
5199
  detail: `Trying ${crnDisplayName}`
4737
5200
  });
4738
5201
  try {
4739
- const profile = relayProfileForManifest(this.state.manifest);
5202
+ const profile = deploymentProfileForManifest(this.state.manifest);
4740
5203
  attemptProfile = profile;
4741
5204
  const sshPublicKey = profile === "uc-go-peer" ? appendDeploymentTokenToSshPublicKey(
4742
5205
  this.state.sshPublicKey.trim(),
@@ -4955,11 +5418,14 @@ var SponsorRelayController = class {
4955
5418
  this.lastCompletedDeploymentHash = result.itemHash;
4956
5419
  this.emitProgress({
4957
5420
  stage: "completed",
4958
- label: "Relay ready",
5421
+ label: deploymentReadyLabel(attemptProfile),
4959
5422
  progress: 100,
4960
5423
  status: "success",
4961
5424
  itemHash: result.itemHash,
4962
- detail: "Relay runtime, setup, metadata, and bootstrap registration are confirmed."
5425
+ detail: deploymentReadyDetail(
5426
+ attemptProfile,
5427
+ "Relay runtime, setup, metadata, and bootstrap registration are confirmed."
5428
+ )
4963
5429
  });
4964
5430
  return;
4965
5431
  } catch (error) {
@@ -5033,11 +5499,14 @@ var SponsorRelayController = class {
5033
5499
  this.lastCompletedDeploymentHash = attemptItemHash;
5034
5500
  this.emitProgress({
5035
5501
  stage: "completed",
5036
- label: "Relay ready",
5502
+ label: deploymentReadyLabel(attemptProfile),
5037
5503
  progress: 100,
5038
5504
  status: "success",
5039
5505
  itemHash: attemptItemHash,
5040
- detail: "The relay completed startup after a transient UI polling error. Runtime and bootstrap registration are confirmed."
5506
+ detail: deploymentReadyDetail(
5507
+ attemptProfile,
5508
+ "The relay completed startup after a transient UI polling error. Runtime and bootstrap registration are confirmed."
5509
+ )
5041
5510
  });
5042
5511
  return;
5043
5512
  }
@@ -5064,11 +5533,14 @@ var SponsorRelayController = class {
5064
5533
  this.lastCompletedDeploymentHash = attemptItemHash;
5065
5534
  this.emitProgress({
5066
5535
  stage: "completed",
5067
- label: "Relay ready",
5536
+ label: deploymentReadyLabel(attemptProfile),
5068
5537
  progress: 100,
5069
5538
  status: "success",
5070
5539
  itemHash: attemptItemHash,
5071
- detail: "The relay completed startup after a transient browser-side polling error. Runtime and bootstrap registration are confirmed."
5540
+ detail: deploymentReadyDetail(
5541
+ attemptProfile,
5542
+ "The relay completed startup after a transient browser-side polling error. Runtime and bootstrap registration are confirmed."
5543
+ )
5072
5544
  });
5073
5545
  return;
5074
5546
  }