@le-space/rootfs 0.3.32 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/rootfs",
3
- "version": "0.3.32",
3
+ "version": "0.4.0",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -306,22 +306,19 @@ def sign_aleph_message(unsigned_message: dict[str, object], private_key: str) ->
306
306
 
307
307
  def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, dict]:
308
308
  url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
309
- request_body = {"sync": True, "message": message}
310
- max_attempts = 3
309
+ attempts = [
310
+ {"sync": True, "message": message},
311
+ {**message, "sync": True},
312
+ dict(message),
313
+ ]
311
314
  last: tuple[int, dict] | None = None
312
- for index in range(max_attempts):
313
- last = post_json(url, request_body)
315
+ for attempt in attempts:
316
+ last = post_json(url, attempt)
314
317
  if 200 <= last[0] < 300:
315
318
  return last
316
- publication_status = last[1].get("publication_status") if isinstance(last[1], dict) else None
317
- retryable = last[0] >= 500 or (
318
- isinstance(publication_status, dict)
319
- and isinstance(publication_status.get("status"), str)
320
- and publication_status.get("status").strip().lower() == "error"
321
- )
322
- if index >= max_attempts - 1 or not retryable:
323
- raise RuntimeError(f"Aleph broadcast failed: {last[0]} {json_dumps(last[1])}")
324
- raise RuntimeError("Aleph broadcast failed: retry budget exhausted")
319
+ if last is None:
320
+ raise RuntimeError("Aleph broadcast failed before any attempt was made")
321
+ raise RuntimeError(f"Aleph broadcast failed: {last[0]} {json_dumps(last[1])}")
325
322
 
326
323
 
327
324
  def parse_post_record(entry: object) -> dict[str, object] | None:
@@ -155,16 +155,22 @@ def is_retryable_broadcast_failure(http_status: int, payload: object) -> bool:
155
155
 
156
156
  def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, object]:
157
157
  url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
158
- request_body = {"sync": True, "message": message}
159
- max_attempts = 3
160
- for index in range(max_attempts):
161
- http_status, payload = post_json(url, request_body)
158
+ attempts = [
159
+ {"sync": True, "message": message},
160
+ {**message, "sync": True},
161
+ dict(message),
162
+ ]
163
+ for index, attempt in enumerate(attempts):
164
+ http_status, payload = post_json(url, attempt)
162
165
  if 200 <= http_status < 300:
163
166
  return http_status, payload
164
- can_retry = index < max_attempts - 1 and is_retryable_broadcast_failure(http_status, payload)
167
+ can_retry = index < len(attempts) - 1 and (
168
+ is_invalid_message_format(http_status, payload)
169
+ or is_retryable_broadcast_failure(http_status, payload)
170
+ )
165
171
  if not can_retry:
166
172
  raise RuntimeError(f"Aleph broadcast failed: {http_status} {json_dumps(payload)}")
167
- raise RuntimeError("Aleph broadcast failed: retry budget exhausted")
173
+ raise RuntimeError("Aleph broadcast failed: no compatible request format was accepted")
168
174
 
169
175
 
170
176
  def parse_post_record(entry: object) -> dict[str, object] | None:
@@ -246,13 +246,13 @@ def build_post_content(
246
246
  owner_authorization: dict[str, object] | None,
247
247
  relay_proof: dict[str, object],
248
248
  now_ms: int,
249
- now_seconds: float,
250
249
  ref: str,
251
250
  post_type: str,
252
251
  ) -> dict[str, object]:
253
252
  content = {
254
253
  "peerId": peer_id,
255
254
  "multiaddrs": multiaddrs,
255
+ "browserMultiaddrs": browser_multiaddrs if browser_multiaddrs else None,
256
256
  "registrationId": registration_id,
257
257
  "profile": profile,
258
258
  "version": version,
@@ -266,14 +266,12 @@ def build_post_content(
266
266
  "relayProof": relay_proof,
267
267
  "updatedAt": now_ms,
268
268
  }
269
- if browser_multiaddrs:
270
- content["browserMultiaddrs"] = browser_multiaddrs
271
269
  return {
272
270
  "type": post_type,
273
271
  "address": sender,
274
272
  "ref": ref,
275
273
  "content": content,
276
- "time": now_seconds,
274
+ "time": now_ms,
277
275
  }
278
276
 
279
277
 
@@ -349,16 +347,22 @@ def is_retryable_broadcast_failure(http_status: int, payload: object) -> bool:
349
347
 
350
348
  def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, object]:
351
349
  url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
352
- request_body = {"sync": True, "message": message}
353
- max_attempts = 3
354
- for index in range(max_attempts):
355
- http_status, payload = post_json(url, request_body)
350
+ attempts = [
351
+ {"sync": True, "message": message},
352
+ {**message, "sync": True},
353
+ dict(message),
354
+ ]
355
+ for index, attempt in enumerate(attempts):
356
+ http_status, payload = post_json(url, attempt)
356
357
  if 200 <= http_status < 300:
357
358
  return http_status, payload
358
- can_retry = index < max_attempts - 1 and is_retryable_broadcast_failure(http_status, payload)
359
+ can_retry = index < len(attempts) - 1 and (
360
+ is_invalid_message_format(http_status, payload)
361
+ or is_retryable_broadcast_failure(http_status, payload)
362
+ )
359
363
  if not can_retry:
360
364
  raise RuntimeError(f"Aleph broadcast failed: {http_status} {json_dumps(payload)}")
361
- raise RuntimeError("Aleph broadcast failed: retry budget exhausted")
365
+ raise RuntimeError("Aleph broadcast failed: no compatible request format was accepted")
362
366
 
363
367
 
364
368
  def parse_post_record(entry: object) -> dict[str, object] | None:
@@ -534,7 +538,6 @@ def main() -> None:
534
538
  owner_authorization,
535
539
  relay_proof,
536
540
  now_ms,
537
- now_seconds,
538
541
  ref,
539
542
  post_type,
540
543
  )
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- import base64
3
- import hashlib
4
2
  import ipaddress
5
3
  import json
6
4
  import os
@@ -9,8 +7,6 @@ import threading
9
7
  import time
10
8
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
11
9
  from pathlib import Path
12
- from urllib import error as urllib_error
13
- from urllib import request as urllib_request
14
10
  from urllib.error import HTTPError, URLError
15
11
  from urllib.parse import urlencode, urlsplit
16
12
  from urllib.request import urlopen
@@ -27,36 +23,12 @@ AUTHORIZED_KEYS_FILE = os.environ.get("AUTHORIZED_KEYS_FILE", "/root/.ssh/author
27
23
  ALEPH_API_HOST = os.environ.get("ALEPH_API_HOST", "https://api2.aleph.im")
28
24
  BOOTSTRAP_CONFIG_KEY = os.environ.get("BOOTSTRAP_CONFIG_KEY", "vm-bootstrap-config")
29
25
  BOOTSTRAP_CONFIG_POLL_SECONDS = float(os.environ.get("BOOTSTRAP_CONFIG_POLL_SECONDS", "5"))
30
- BOOTSTRAP_CONFIG_SIGNAL_REF = os.environ.get("BOOTSTRAP_CONFIG_SIGNAL_REF", "vm-bootstrap-config")
31
- BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = os.environ.get(
32
- "BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE", "vm-bootstrap-config-status"
33
- )
34
- BOOTSTRAP_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
35
26
  DESCRIBE_TIMEOUT_SECONDS = int(os.environ.get("DESCRIBE_TIMEOUT_SECONDS", "15"))
36
27
  DESCRIBE_INTERVAL_SECONDS = float(os.environ.get("DESCRIBE_INTERVAL_SECONDS", "1"))
37
28
 
38
29
  CONFIGURE_LOCK = threading.Lock()
39
30
 
40
31
 
41
- def _json_dumps(payload: object) -> str:
42
- return json.dumps(payload, separators=(",", ":"))
43
-
44
-
45
- def _parse_env_file(path: str) -> dict[str, str]:
46
- values: dict[str, str] = {}
47
- if not os.path.exists(path):
48
- return values
49
-
50
- with open(path, encoding="utf-8") as handle:
51
- for line in handle:
52
- stripped = line.strip()
53
- if not stripped or stripped.startswith("#") or "=" not in stripped:
54
- continue
55
- key, value = stripped.split("=", 1)
56
- values[key.strip()] = value.strip()
57
- return values
58
-
59
-
60
32
  def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
61
33
  handler.send_header("Access-Control-Allow-Origin", "*")
62
34
  handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
@@ -171,7 +143,7 @@ def _build_configure_args(record: dict) -> list[str]:
171
143
  return args
172
144
 
173
145
 
174
- def _run_configure_process(args: list[str], bootstrap_record: dict | None = None) -> tuple[bool, str]:
146
+ def _run_configure_process(args: list[str]) -> tuple[bool, str]:
175
147
  with CONFIGURE_LOCK:
176
148
  if os.path.exists(READY_FILE):
177
149
  return True, "ready"
@@ -181,223 +153,9 @@ def _run_configure_process(args: list[str], bootstrap_record: dict | None = None
181
153
  return False, error.stderr.strip() or error.stdout.strip() or str(error)
182
154
 
183
155
  _start_metadata_generation()
184
- if isinstance(bootstrap_record, dict):
185
- try:
186
- _publish_vm_bootstrap_config_signal(bootstrap_record)
187
- except Exception as error: # pragma: no cover - runtime error path
188
- return False, f"configured relay but failed to publish bootstrap config signal: {error}"
189
156
  return True, result.stdout.strip()
190
157
 
191
158
 
192
- def _address_from_private_key(private_key: str) -> str:
193
- try:
194
- from eth_account import Account
195
- except ImportError as error: # pragma: no cover - runtime dependency
196
- raise RuntimeError(
197
- "eth-account is required for guest-side bootstrap status publishing"
198
- ) from error
199
-
200
- return Account.from_key(private_key).address
201
-
202
-
203
- def _sign_personal_message(private_key: str, payload: str) -> str:
204
- try:
205
- from eth_account import Account
206
- from eth_account.messages import encode_defunct
207
- except ImportError as error: # pragma: no cover - runtime dependency
208
- raise RuntimeError(
209
- "eth-account is required for guest-side bootstrap status publishing"
210
- ) from error
211
-
212
- message = encode_defunct(text=payload)
213
- signed = Account.sign_message(message, private_key=private_key)
214
- signature = signed.signature.hex()
215
- return signature if signature.startswith("0x") else f"0x{signature}"
216
-
217
-
218
- def _signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
219
- return "\n".join([chain, sender, message_type, item_hash])
220
-
221
-
222
- def _iter_validation_errors(payload: object) -> list[dict]:
223
- if isinstance(payload, list):
224
- return [entry for entry in payload if isinstance(entry, dict)]
225
- return []
226
-
227
-
228
- def _is_invalid_message_format(http_status: int, payload: object) -> bool:
229
- if http_status != 422:
230
- return False
231
-
232
- if isinstance(payload, dict):
233
- details = payload.get("details")
234
- if isinstance(details, str) and "InvalidMessageFormat" in details:
235
- return True
236
- if isinstance(details, dict):
237
- message = details.get("message")
238
- if isinstance(message, str) and "InvalidMessageFormat" in message:
239
- return True
240
-
241
- for entry in _iter_validation_errors(payload):
242
- message = entry.get("msg")
243
- location = entry.get("loc")
244
- if isinstance(message, str) and "InvalidMessageFormat" in message:
245
- return True
246
- if message == "Field required" and location == ["message"]:
247
- return True
248
-
249
- return False
250
-
251
-
252
- def _is_retryable_broadcast_failure(http_status: int, payload: object) -> bool:
253
- if http_status >= 500:
254
- return True
255
- if isinstance(payload, dict):
256
- publication_status = payload.get("publication_status")
257
- if isinstance(publication_status, dict):
258
- status = publication_status.get("status")
259
- if isinstance(status, str) and status.strip().lower() == "error":
260
- return True
261
- return False
262
-
263
-
264
- def _post_json(url: str, body: dict[str, object]) -> tuple[int, object]:
265
- data = _json_dumps(body).encode("utf-8")
266
- request = urllib_request.Request(
267
- url,
268
- data=data,
269
- headers={"content-type": "application/json"},
270
- method="POST",
271
- )
272
- try:
273
- with urllib_request.urlopen(request, timeout=30) as response:
274
- payload = response.read().decode("utf-8")
275
- return response.status, json.loads(payload or "{}")
276
- except urllib_error.HTTPError as error:
277
- payload = error.read().decode("utf-8")
278
- try:
279
- return error.code, json.loads(payload or "{}")
280
- except json.JSONDecodeError:
281
- return error.code, {"details": payload}
282
-
283
-
284
- def _broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, object]:
285
- url = f"{api_host.rstrip('/')}/api/v0/messages"
286
- request_body = {"sync": True, "message": message}
287
- max_attempts = 3
288
- for index in range(max_attempts):
289
- http_status, payload = _post_json(url, request_body)
290
- if 200 <= http_status < 300:
291
- return http_status, payload
292
- can_retry = index < max_attempts - 1 and _is_retryable_broadcast_failure(http_status, payload)
293
- if not can_retry:
294
- raise RuntimeError(
295
- f"Aleph bootstrap status broadcast failed: {http_status} {_json_dumps(payload)}"
296
- )
297
- raise RuntimeError("Aleph bootstrap status broadcast failed: retry budget exhausted")
298
-
299
-
300
- def _publish_vm_bootstrap_config_signal(record: dict) -> None:
301
- env_values = _parse_env_file(ENV_FILE)
302
- publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
303
- if not publisher_private_key:
304
- raise RuntimeError("missing ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY")
305
-
306
- owner_authorization_b64 = env_values.get("ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64", "").strip()
307
- owner_authorization = None
308
- if owner_authorization_b64:
309
- decoded = base64.b64decode(owner_authorization_b64).decode("utf-8")
310
- parsed = json.loads(decoded)
311
- if isinstance(parsed, dict):
312
- owner_authorization = parsed
313
-
314
- owner_address = ""
315
- if isinstance(owner_authorization, dict):
316
- payload = owner_authorization.get("payload")
317
- if isinstance(payload, dict):
318
- owner_address = str(payload.get("ownerAddress") or "").strip()
319
- if not owner_address:
320
- owner_address = str(record.get("ownerAddress") or "").strip()
321
- if not owner_address:
322
- raise RuntimeError("missing ownerAddress for bootstrap config signal")
323
-
324
- deployment_token = str(record.get("deploymentToken") or "").strip()
325
- profile = str(record.get("profile") or "").strip()
326
- instance_item_hash = str(record.get("instanceItemHash") or "").strip()
327
- if not deployment_token or not profile or not instance_item_hash:
328
- raise RuntimeError("bootstrap config signal is missing deployment metadata")
329
-
330
- metadata, metadata_error = _fresh_metadata_payload()
331
- if not metadata:
332
- raise RuntimeError(
333
- f"bootstrap config signal metadata is not ready: {metadata_error or 'metadata unavailable'}"
334
- )
335
-
336
- peer_id = metadata.get("peer_id")
337
- probe_multiaddrs = metadata.get("probe_multiaddrs")
338
- browser_bootstrap_multiaddrs = metadata.get("browser_bootstrap_multiaddrs")
339
- if not isinstance(peer_id, str) or not peer_id.strip():
340
- raise RuntimeError("bootstrap config signal is missing peer_id")
341
- if not isinstance(probe_multiaddrs, list) or not any(
342
- isinstance(entry, str) and entry.strip() for entry in probe_multiaddrs
343
- ):
344
- raise RuntimeError("bootstrap config signal is missing probe_multiaddrs")
345
- if not isinstance(browser_bootstrap_multiaddrs, list):
346
- browser_bootstrap_multiaddrs = []
347
-
348
- publisher_address = _address_from_private_key(publisher_private_key)
349
- now_ms = int(time.time() * 1000)
350
- content = {
351
- "type": BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE,
352
- "address": publisher_address,
353
- "ref": BOOTSTRAP_CONFIG_SIGNAL_REF,
354
- "content": {
355
- "deploymentToken": deployment_token,
356
- "status": "applied",
357
- "profile": profile,
358
- "ownerAddress": owner_address,
359
- "instanceItemHash": instance_item_hash,
360
- "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now_ms / 1000)),
361
- "publisherAddress": publisher_address,
362
- "authorization": owner_authorization,
363
- "peerId": peer_id.strip(),
364
- "probeMultiaddrs": [
365
- entry.strip()
366
- for entry in probe_multiaddrs
367
- if isinstance(entry, str) and entry.strip()
368
- ],
369
- "browserBootstrapMultiaddrs": [
370
- entry.strip()
371
- for entry in browser_bootstrap_multiaddrs
372
- if isinstance(entry, str) and entry.strip()
373
- ],
374
- },
375
- "time": now_ms,
376
- }
377
- item_content = _json_dumps(content)
378
- unsigned_message = {
379
- "channel": BOOTSTRAP_CHANNEL,
380
- "sender": publisher_address,
381
- "chain": "ETH",
382
- "type": "POST",
383
- "time": now_ms / 1000,
384
- "item_type": "inline",
385
- "item_content": item_content,
386
- "item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
387
- }
388
- signed_message = dict(unsigned_message)
389
- signed_message["signature"] = _sign_personal_message(
390
- publisher_private_key,
391
- _signature_payload(
392
- str(unsigned_message["chain"]),
393
- str(unsigned_message["sender"]),
394
- str(unsigned_message["type"]),
395
- str(unsigned_message["item_hash"]),
396
- ),
397
- )
398
- _broadcast_aleph_message(ALEPH_API_HOST, signed_message)
399
-
400
-
401
159
  def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
402
160
  owner_address, deployment_token = _extract_guest_bootstrap_locator()
403
161
  if not owner_address or not deployment_token:
@@ -439,7 +197,7 @@ def _poll_bootstrap_config_loop() -> None:
439
197
  except ValueError:
440
198
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
441
199
  continue
442
- ok, _ = _run_configure_process(args, record)
200
+ ok, _ = _run_configure_process(args)
443
201
  if ok:
444
202
  return
445
203
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)