@le-space/rootfs 0.6.36 → 0.6.38

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/index.d.ts CHANGED
@@ -115,6 +115,8 @@ interface RootfsManifest {
115
115
  version: string;
116
116
  rootfsInstallStrategy: string;
117
117
  requiresBootstrapNetwork: boolean;
118
+ /** Guest fetches its bootstrap config from the Aleph aggregate itself. */
119
+ supportsBootstrapConfigAggregate?: boolean;
118
120
  bootstrapSummary: string;
119
121
  rootfsSourceSizeBytes?: number;
120
122
  requiredPortForwards: RootfsContract["ports"];
package/index.js CHANGED
@@ -351,6 +351,9 @@ function createRootfsManifest(plan, contract, options = {}) {
351
351
  if (contract.rootfs.profile === "playwright-runner") {
352
352
  manifest.playwrightVersion = "1.61.1";
353
353
  }
354
+ if (contract.rootfs.profile === "uc-go-peer" || contract.rootfs.profile === "orbitdb-relay") {
355
+ manifest.supportsBootstrapConfigAggregate = true;
356
+ }
354
357
  if (typeof options.rootfsSourceSizeBytes === "number" && Number.isFinite(options.rootfsSourceSizeBytes) && options.rootfsSourceSizeBytes > 0) {
355
358
  manifest.rootfsSourceSizeBytes = options.rootfsSourceSizeBytes;
356
359
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/rootfs",
3
- "version": "0.6.36",
3
+ "version": "0.6.38",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,6 +40,7 @@ QUIC_PORT=""
40
40
  BOOTSTRAP_PUBLISHER_PRIVATE_KEY=""
41
41
  BOOTSTRAP_PUBLISHER_LIBP2P_IDENTITY_HEX=""
42
42
  BOOTSTRAP_OWNER_PRIVATE_KEY=""
43
+ BOOTSTRAP_OWNER_ADDRESS=""
43
44
  BOOTSTRAP_OWNER_AUTHORIZATION_B64=""
44
45
  BOOTSTRAP_REGISTRATION_ID=""
45
46
  START_SERVICE=1
@@ -60,6 +61,7 @@ Usage:
60
61
  [--bootstrap-publisher-private-key <hex>] \
61
62
  [--bootstrap-publisher-libp2p-identity-hex <hex>] \
62
63
  [--bootstrap-owner-private-key <hex>] \
64
+ [--bootstrap-owner-address <0x…>] \
63
65
  [--bootstrap-owner-authorization-b64 <base64>] \
64
66
  [--bootstrap-registration-id <id>] \
65
67
  [--no-start]
@@ -69,6 +71,33 @@ marks the relay as ready, and optionally starts the relay plus Caddy services.
69
71
  EOF
70
72
  }
71
73
 
74
+ current_bootstrap_publisher_private_key() {
75
+ local existing
76
+ existing="$(grep -E '^[#[:space:]]*ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY=' "${ENV_FILE}" 2>/dev/null | tail -n1 | cut -d= -f2- || true)"
77
+ if [ -n "${existing}" ]; then
78
+ printf '%s\n' "${existing}"
79
+ return
80
+ fi
81
+ printf '\n'
82
+ }
83
+
84
+ # The bootstrap publisher key signs this relay's own Aleph registrations, so
85
+ # it never has to leave the guest. Generating it here means the deployer does
86
+ # not have to send a secret to the plain-HTTP setup endpoint, which runs
87
+ # before Caddy/TLS exists and would otherwise put the key on the wire in the
88
+ # clear. An already-provisioned key is reused so the relay keeps its identity
89
+ # across reconfigurations.
90
+ generate_bootstrap_publisher_private_key() {
91
+ while true; do
92
+ local candidate
93
+ candidate="$(openssl rand -hex 32)"
94
+ if [ -n "${candidate}" ] && [ "${candidate}" != "0000000000000000000000000000000000000000000000000000000000000000" ]; then
95
+ printf '0x%s\n' "${candidate}"
96
+ return
97
+ fi
98
+ done
99
+ }
100
+
72
101
  write_env_var() {
73
102
  local key="$1"
74
103
  local value="$2"
@@ -176,6 +205,10 @@ while [ "$#" -gt 0 ]; do
176
205
  BOOTSTRAP_OWNER_PRIVATE_KEY="${2:-}"
177
206
  shift 2
178
207
  ;;
208
+ --bootstrap-owner-address)
209
+ BOOTSTRAP_OWNER_ADDRESS="${2:-}"
210
+ shift 2
211
+ ;;
179
212
  --bootstrap-owner-authorization-b64)
180
213
  BOOTSTRAP_OWNER_AUTHORIZATION_B64="${2:-}"
181
214
  shift 2
@@ -267,6 +300,12 @@ fi
267
300
  if [ -n "${QUIC_PORT}" ]; then
268
301
  write_env_var "EXTERNAL_RELAY_QUIC_PORT" "${QUIC_PORT}"
269
302
  fi
303
+ if [ -z "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
304
+ BOOTSTRAP_PUBLISHER_PRIVATE_KEY="$(current_bootstrap_publisher_private_key)"
305
+ fi
306
+ if [ -z "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
307
+ BOOTSTRAP_PUBLISHER_PRIVATE_KEY="$(generate_bootstrap_publisher_private_key)"
308
+ fi
270
309
  if [ -n "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
271
310
  write_env_var "ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY" "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}"
272
311
  fi
@@ -276,6 +315,9 @@ fi
276
315
  if [ -n "${BOOTSTRAP_OWNER_PRIVATE_KEY}" ]; then
277
316
  write_env_var "ALEPH_BOOTSTRAP_OWNER_PRIVATE_KEY" "${BOOTSTRAP_OWNER_PRIVATE_KEY}"
278
317
  fi
318
+ if [ -n "${BOOTSTRAP_OWNER_ADDRESS}" ]; then
319
+ write_env_var "ALEPH_BOOTSTRAP_OWNER_ADDRESS" "${BOOTSTRAP_OWNER_ADDRESS}"
320
+ fi
279
321
  if [ -n "${BOOTSTRAP_OWNER_AUTHORIZATION_B64}" ]; then
280
322
  write_env_var "ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64" "${BOOTSTRAP_OWNER_AUTHORIZATION_B64}"
281
323
  fi
@@ -51,6 +51,29 @@ def normalize_multiaddrs(payload: dict) -> list[str]:
51
51
  return [entry for entry in values if isinstance(entry, str) and entry.strip()]
52
52
 
53
53
 
54
+ def bootstrap_publisher_address(env_values: dict[str, str]) -> str | None:
55
+ """Address of the key this relay signs its own Aleph registrations with.
56
+
57
+ The guest generates that key itself, so the deployer cannot know the
58
+ address up front. Reporting it here lets the deployer bind an owner
59
+ authorization to the relay's real publisher identity instead of having to
60
+ send the key to the guest over the plain-HTTP setup endpoint.
61
+ """
62
+ private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
63
+ if not private_key:
64
+ return None
65
+
66
+ try:
67
+ from eth_account import Account
68
+ except ImportError:
69
+ return None
70
+
71
+ try:
72
+ return Account.from_key(private_key).address
73
+ except (ValueError, TypeError):
74
+ return None
75
+
76
+
54
77
  def build_grouped_multiaddrs(env_values: dict[str, str], all_multiaddrs: list[str], peer_id: str) -> dict[str, list[str]]:
55
78
  proxy_hostname = env_values.get("PROXY_HOSTNAME", "").strip().lower()
56
79
  direct_tcp_multiaddrs = [addr for addr in all_multiaddrs if "/tcp/" in addr and "/ws" not in addr]
@@ -148,6 +171,7 @@ def main() -> None:
148
171
  env_values = parse_env_file(ENV_FILE)
149
172
  payload = {
150
173
  "peer_id": peer_id,
174
+ "bootstrap_publisher_address": bootstrap_publisher_address(env_values),
151
175
  "announce_addrs": [
152
176
  entry.strip()
153
177
  for entry in env_values.get("VITE_APPEND_ANNOUNCE", "").split(",")
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env python3
2
+ import base64
3
+ import hashlib
2
4
  import ipaddress
3
5
  import json
4
6
  import os
@@ -6,7 +8,9 @@ import subprocess
6
8
  import threading
7
9
  import time
8
10
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9
- from urllib.parse import urlsplit
11
+ from urllib.error import HTTPError, URLError
12
+ from urllib.parse import urlencode, urlsplit
13
+ from urllib.request import Request, urlopen
10
14
 
11
15
 
12
16
  ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay")
@@ -18,6 +22,26 @@ DESCRIBE_SCRIPT = "/usr/local/sbin/orbitdb-relay-describe.py"
18
22
  METADATA_FILE = os.environ.get("METADATA_FILE", "/run/orbitdb-relay-setup-metadata.json")
19
23
  METADATA_ERROR_FILE = os.environ.get("METADATA_ERROR_FILE", "/run/orbitdb-relay-setup-metadata.error")
20
24
 
25
+ # Guest-side bootstrap config handoff.
26
+ #
27
+ # A browser served over HTTPS cannot POST to this server: it listens on plain
28
+ # HTTP (it has to exist before Caddy/TLS does), so the request is blocked as
29
+ # mixed content. Instead the deployer publishes the configuration into a
30
+ # public Aleph aggregate and puts a locator into the VM's SSH key; the guest
31
+ # reads that locator, fetches its own configuration over HTTPS, applies it,
32
+ # and publishes a signed acknowledgement the deployer waits for.
33
+ AUTHORIZED_KEYS_FILE = os.environ.get("AUTHORIZED_KEYS_FILE", "/root/.ssh/authorized_keys")
34
+ ALEPH_API_HOST = os.environ.get("ALEPH_API_HOST", "https://api.aleph.im")
35
+ BOOTSTRAP_CONFIG_KEY = os.environ.get("BOOTSTRAP_CONFIG_KEY", "vm-bootstrap-config")
36
+ BOOTSTRAP_CONFIG_POLL_SECONDS = float(os.environ.get("BOOTSTRAP_CONFIG_POLL_SECONDS", "5"))
37
+ BOOTSTRAP_CONFIG_SIGNAL_REF = os.environ.get("BOOTSTRAP_CONFIG_SIGNAL_REF", "vm-bootstrap-config")
38
+ BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = os.environ.get(
39
+ "BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE", "vm-bootstrap-config-signal"
40
+ )
41
+ BOOTSTRAP_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
42
+
43
+ CONFIGURE_LOCK = threading.Lock()
44
+
21
45
 
22
46
  def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
23
47
  handler.send_header("Access-Control-Allow-Origin", "*")
@@ -49,6 +73,323 @@ def _validate_proxy_hostname(value: object) -> str | None:
49
73
  return parsed.hostname
50
74
 
51
75
 
76
+ def _json_dumps(payload: object) -> str:
77
+ return json.dumps(payload, separators=(",", ":"))
78
+
79
+
80
+ def _parse_env_file(path: str) -> dict[str, str]:
81
+ values: dict[str, str] = {}
82
+ if not os.path.exists(path):
83
+ return values
84
+
85
+ with open(path, encoding="utf-8") as handle:
86
+ for line in handle:
87
+ stripped = line.strip()
88
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
89
+ continue
90
+ key, value = stripped.split("=", 1)
91
+ values[key.strip()] = value.strip()
92
+ return values
93
+
94
+
95
+ def _extract_guest_bootstrap_locator() -> tuple[str | None, str | None]:
96
+ """Read the deployment locator the deployer left in our SSH key.
97
+
98
+ The Aleph INSTANCE message carries an SSH public key with an extra
99
+ `aleph-bootstrap-config:<owner>:<token>` part, so it lands in the guest's
100
+ authorized_keys and gives us everything needed to find our own record.
101
+ """
102
+ try:
103
+ with open(AUTHORIZED_KEYS_FILE, encoding="utf-8") as handle:
104
+ for raw_line in handle:
105
+ line = raw_line.strip()
106
+ if not line or line.startswith("#"):
107
+ continue
108
+ for part in line.split():
109
+ if part.startswith("aleph-bootstrap-config:"):
110
+ _, owner_address, deployment_token = part.split(":", 2)
111
+ owner_address = owner_address.strip()
112
+ deployment_token = deployment_token.strip()
113
+ if owner_address and deployment_token:
114
+ return owner_address, deployment_token
115
+ except (FileNotFoundError, ValueError):
116
+ return None, None
117
+
118
+ return None, None
119
+
120
+
121
+ def _address_from_private_key(private_key: str) -> str:
122
+ from eth_account import Account
123
+
124
+ return Account.from_key(private_key).address
125
+
126
+
127
+ def _sign_personal_message(private_key: str, payload: str) -> str:
128
+ from eth_account import Account
129
+ from eth_account.messages import encode_defunct
130
+
131
+ signed = Account.sign_message(encode_defunct(text=payload), private_key=private_key)
132
+ signature = signed.signature.hex()
133
+ return signature if signature.startswith("0x") else f"0x{signature}"
134
+
135
+
136
+ def _signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
137
+ return "\n".join([chain, sender, message_type, item_hash])
138
+
139
+
140
+ def _broadcast_aleph_message(api_host: str, message: dict[str, object]) -> None:
141
+ body = json.dumps({"message": message, "sync": True}).encode("utf-8")
142
+ request = Request(
143
+ f"{api_host.rstrip('/')}/api/v0/messages",
144
+ data=body,
145
+ headers={"content-type": "application/json"},
146
+ method="POST",
147
+ )
148
+ with urlopen(request, timeout=30) as response:
149
+ response.read()
150
+
151
+
152
+ def _fresh_metadata_payload() -> tuple[dict | None, str | None]:
153
+ if os.path.exists(METADATA_FILE):
154
+ try:
155
+ with open(METADATA_FILE, encoding="utf-8") as handle:
156
+ return json.load(handle), None
157
+ except (OSError, json.JSONDecodeError) as error:
158
+ return None, str(error)
159
+ if os.path.exists(METADATA_ERROR_FILE):
160
+ with open(METADATA_ERROR_FILE, encoding="utf-8") as handle:
161
+ return None, handle.read().strip() or "metadata generation failed"
162
+ return None, None
163
+
164
+
165
+ def _build_configure_args_from_record(record: dict) -> list[str]:
166
+ """Map an Aleph bootstrap config record onto configure.sh arguments.
167
+
168
+ Deliberately mirrors the /configure payload mapping, minus every secret:
169
+ the record lives in a *public* aggregate, so it may only carry network
170
+ facts, the registration id, the owner address and a signed authorization.
171
+ """
172
+ runtime = record.get("runtime") if isinstance(record.get("runtime"), dict) else {}
173
+ bootstrap = record.get("bootstrap") if isinstance(record.get("bootstrap"), dict) else {}
174
+
175
+ public_ipv4 = str(runtime.get("publicIpv4") or "").strip()
176
+ if not public_ipv4:
177
+ raise ValueError("bootstrap config is missing runtime.publicIpv4")
178
+
179
+ mapped = runtime.get("mappedPorts") if isinstance(runtime.get("mappedPorts"), dict) else {}
180
+
181
+ def host_port(*container_ports: str) -> int | None:
182
+ for container_port in container_ports:
183
+ entry = mapped.get(container_port)
184
+ if isinstance(entry, dict):
185
+ host = entry.get("host")
186
+ if isinstance(host, int) and host > 0:
187
+ return host
188
+ return None
189
+
190
+ tcp_port = host_port("9091")
191
+ ws_port = host_port("9092", "443")
192
+ if not tcp_port or not ws_port:
193
+ raise ValueError("bootstrap config is missing the mapped TCP/WS ports")
194
+
195
+ args = [
196
+ CONFIGURE_SCRIPT,
197
+ "--public-ipv4",
198
+ public_ipv4,
199
+ "--tcp-port",
200
+ str(tcp_port),
201
+ "--ws-port",
202
+ str(ws_port),
203
+ ]
204
+
205
+ public_ipv6 = str(runtime.get("publicIpv6") or "").strip()
206
+ if public_ipv6:
207
+ args.extend(["--public-ipv6", public_ipv6])
208
+
209
+ proxy_hostname = _validate_proxy_hostname(runtime.get("proxyUrl"))
210
+ if proxy_hostname:
211
+ args.extend(["--proxy-hostname", proxy_hostname])
212
+
213
+ for flag, container_ports in (
214
+ ("--metrics-port", ("9090",)),
215
+ ("--metrics-https-port", ("443",)),
216
+ ("--webrtc-port", ("9093",)),
217
+ ("--quic-port", ("9094",)),
218
+ ):
219
+ value = host_port(*container_ports)
220
+ if value:
221
+ args.extend([flag, str(value)])
222
+
223
+ registration_id = str(bootstrap.get("registrationId") or "").strip()
224
+ if registration_id:
225
+ args.extend(["--bootstrap-registration-id", registration_id])
226
+
227
+ owner_address = str(record.get("ownerAddress") or "").strip()
228
+ if owner_address:
229
+ args.extend(["--bootstrap-owner-address", owner_address])
230
+
231
+ owner_authorization = str(bootstrap.get("ownerAuthorizationBase64") or "").strip()
232
+ if owner_authorization:
233
+ args.extend(["--bootstrap-owner-authorization-b64", owner_authorization])
234
+
235
+ return args
236
+
237
+
238
+ def _run_configure_process(args: list[str], bootstrap_record: dict | None = None) -> tuple[bool, str]:
239
+ with CONFIGURE_LOCK:
240
+ if os.path.exists(READY_FILE):
241
+ return True, "ready"
242
+ try:
243
+ result = subprocess.run(args, check=True, capture_output=True, text=True)
244
+ except subprocess.CalledProcessError as error:
245
+ return False, error.stderr.strip() or error.stdout.strip() or str(error)
246
+
247
+ _clear_metadata_state()
248
+ _generate_metadata_files()
249
+
250
+ if isinstance(bootstrap_record, dict):
251
+ try:
252
+ _publish_vm_bootstrap_config_signal(bootstrap_record)
253
+ except Exception as error: # pragma: no cover - runtime error path
254
+ return False, f"configured relay but failed to publish bootstrap signal: {error}"
255
+ return True, result.stdout.strip()
256
+
257
+
258
+ def _publish_vm_bootstrap_config_signal(record: dict) -> None:
259
+ """Tell the deployer we applied the config; it blocks until this appears."""
260
+ env_values = _parse_env_file(ENV_FILE)
261
+ publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
262
+ if not publisher_private_key:
263
+ raise RuntimeError("missing ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY")
264
+
265
+ owner_authorization = None
266
+ owner_authorization_b64 = env_values.get("ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64", "").strip()
267
+ if owner_authorization_b64:
268
+ try:
269
+ parsed = json.loads(base64.b64decode(owner_authorization_b64).decode("utf-8"))
270
+ if isinstance(parsed, dict):
271
+ owner_authorization = parsed
272
+ except (ValueError, json.JSONDecodeError):
273
+ owner_authorization = None
274
+
275
+ owner_address = str(record.get("ownerAddress") or "").strip()
276
+ deployment_token = str(record.get("deploymentToken") or "").strip()
277
+ profile = str(record.get("profile") or "").strip()
278
+ instance_item_hash = str(record.get("instanceItemHash") or "").strip()
279
+ if not deployment_token or not profile or not instance_item_hash:
280
+ raise RuntimeError("bootstrap config signal is missing deployment metadata")
281
+
282
+ metadata, metadata_error = _fresh_metadata_payload()
283
+ if not metadata:
284
+ raise RuntimeError(
285
+ f"bootstrap config signal metadata is not ready: {metadata_error or 'unavailable'}"
286
+ )
287
+
288
+ peer_id = metadata.get("peer_id")
289
+ probe_multiaddrs = metadata.get("probe_multiaddrs")
290
+ browser_bootstrap_multiaddrs = metadata.get("browser_bootstrap_multiaddrs")
291
+ if not isinstance(peer_id, str) or not peer_id.strip():
292
+ raise RuntimeError("bootstrap config signal is missing peer_id")
293
+ if not isinstance(probe_multiaddrs, list) or not any(
294
+ isinstance(entry, str) and entry.strip() for entry in probe_multiaddrs
295
+ ):
296
+ raise RuntimeError("bootstrap config signal is missing probe_multiaddrs")
297
+ if not isinstance(browser_bootstrap_multiaddrs, list):
298
+ browser_bootstrap_multiaddrs = []
299
+
300
+ publisher_address = _address_from_private_key(publisher_private_key)
301
+ now_ms = int(time.time() * 1000)
302
+ content = {
303
+ "type": BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE,
304
+ "address": publisher_address,
305
+ "ref": BOOTSTRAP_CONFIG_SIGNAL_REF,
306
+ "content": {
307
+ "deploymentToken": deployment_token,
308
+ "status": "applied",
309
+ "profile": profile,
310
+ "ownerAddress": owner_address,
311
+ "instanceItemHash": instance_item_hash,
312
+ "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now_ms / 1000)),
313
+ "publisherAddress": publisher_address,
314
+ "authorization": owner_authorization,
315
+ "peerId": peer_id.strip(),
316
+ "probeMultiaddrs": [
317
+ entry.strip()
318
+ for entry in probe_multiaddrs
319
+ if isinstance(entry, str) and entry.strip()
320
+ ],
321
+ "browserBootstrapMultiaddrs": [
322
+ entry.strip()
323
+ for entry in browser_bootstrap_multiaddrs
324
+ if isinstance(entry, str) and entry.strip()
325
+ ],
326
+ },
327
+ "time": now_ms,
328
+ }
329
+ item_content = _json_dumps(content)
330
+ message = {
331
+ "channel": BOOTSTRAP_CHANNEL,
332
+ "sender": publisher_address,
333
+ "chain": "ETH",
334
+ "type": "POST",
335
+ "time": now_ms / 1000,
336
+ "item_type": "inline",
337
+ "item_content": item_content,
338
+ "item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
339
+ }
340
+ message["signature"] = _sign_personal_message(
341
+ publisher_private_key,
342
+ _signature_payload(
343
+ str(message["chain"]),
344
+ str(message["sender"]),
345
+ str(message["type"]),
346
+ str(message["item_hash"]),
347
+ ),
348
+ )
349
+ _broadcast_aleph_message(ALEPH_API_HOST, message)
350
+
351
+
352
+ def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
353
+ owner_address, deployment_token = _extract_guest_bootstrap_locator()
354
+ if not owner_address or not deployment_token:
355
+ return owner_address, deployment_token, None
356
+
357
+ request_url = (
358
+ f"{ALEPH_API_HOST.rstrip('/')}/api/v0/aggregates/{owner_address}.json?"
359
+ f"{urlencode({'keys': BOOTSTRAP_CONFIG_KEY})}"
360
+ )
361
+
362
+ try:
363
+ with urlopen(request_url, timeout=10) as response:
364
+ payload = json.loads(response.read().decode("utf-8") or "{}")
365
+ except (HTTPError, URLError, json.JSONDecodeError, TimeoutError):
366
+ return owner_address, deployment_token, None
367
+
368
+ aggregate = payload.get("data", {}).get(BOOTSTRAP_CONFIG_KEY, {})
369
+ if not isinstance(aggregate, dict):
370
+ return owner_address, deployment_token, None
371
+
372
+ record = aggregate.get(deployment_token)
373
+ return owner_address, deployment_token, record if isinstance(record, dict) else None
374
+
375
+
376
+ def _poll_bootstrap_config_loop() -> None:
377
+ while not os.path.exists(READY_FILE):
378
+ _, deployment_token, record = _load_vm_bootstrap_record()
379
+ if deployment_token and isinstance(record, dict):
380
+ status = str(record.get("status") or "").strip().lower()
381
+ if status == "pending":
382
+ try:
383
+ args = _build_configure_args_from_record(record)
384
+ except ValueError:
385
+ time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
386
+ continue
387
+ ok, _ = _run_configure_process(args, record)
388
+ if ok:
389
+ return
390
+ time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
391
+
392
+
52
393
  class Handler(BaseHTTPRequestHandler):
53
394
  server_version = "OrbitdbRelaySetup/1.0"
54
395
 
@@ -143,7 +484,11 @@ class Handler(BaseHTTPRequestHandler):
143
484
  bootstrap_publisher_libp2p_identity_hex = payload.get(
144
485
  "bootstrap_publisher_libp2p_identity_hex"
145
486
  )
146
- bootstrap_owner_private_key = payload.get("bootstrap_owner_private_key")
487
+ # NOTE: `bootstrap_owner_private_key` is deliberately NOT accepted
488
+ # here. This endpoint is plain HTTP (it runs before Caddy/TLS
489
+ # exists), so anything sent to it crosses the network in the
490
+ # clear. The owner only ever needs to hand the guest a *signed
491
+ # authorization*, never the key itself.
147
492
  bootstrap_owner_authorization_b64 = payload.get("bootstrap_owner_authorization_b64")
148
493
  bootstrap_registration_id = payload.get("bootstrap_registration_id")
149
494
  no_start = bool(payload.get("no_start"))
@@ -182,8 +527,6 @@ class Handler(BaseHTTPRequestHandler):
182
527
  str(bootstrap_publisher_libp2p_identity_hex),
183
528
  ]
184
529
  )
185
- if bootstrap_owner_private_key is not None:
186
- args.extend(["--bootstrap-owner-private-key", str(bootstrap_owner_private_key)])
187
530
  if bootstrap_owner_authorization_b64 is not None:
188
531
  args.extend(
189
532
  ["--bootstrap-owner-authorization-b64", str(bootstrap_owner_authorization_b64)]
@@ -256,6 +599,13 @@ def _generate_metadata_files() -> None:
256
599
 
257
600
 
258
601
  def main() -> None:
602
+ # Poll for our own Aleph bootstrap config alongside the HTTP listener, so
603
+ # a deployer that cannot reach this plain-HTTP endpoint (any page served
604
+ # over HTTPS) can hand the configuration over through Aleph instead. The
605
+ # first path to succeed wins; the other becomes a no-op once READY_FILE
606
+ # exists and CONFIGURE_LOCK serialises them.
607
+ threading.Thread(target=_poll_bootstrap_config_loop, daemon=True).start()
608
+
259
609
  server = ThreadingHTTPServer(("0.0.0.0", 80), Handler)
260
610
  server.serve_forever()
261
611