@le-space/rootfs 0.4.1 → 0.4.3

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.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -306,19 +306,22 @@ 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
- attempts = [
310
- {"sync": True, "message": message},
311
- {**message, "sync": True},
312
- dict(message),
313
- ]
309
+ request_body = {"sync": True, "message": message}
310
+ max_attempts = 3
314
311
  last: tuple[int, dict] | None = None
315
- for attempt in attempts:
316
- last = post_json(url, attempt)
312
+ for index in range(max_attempts):
313
+ last = post_json(url, request_body)
317
314
  if 200 <= last[0] < 300:
318
315
  return last
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])}")
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")
322
325
 
323
326
 
324
327
  def parse_post_record(entry: object) -> dict[str, object] | None:
@@ -155,22 +155,16 @@ 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
- 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)
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)
165
162
  if 200 <= http_status < 300:
166
163
  return 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
- )
164
+ can_retry = index < max_attempts - 1 and is_retryable_broadcast_failure(http_status, payload)
171
165
  if not can_retry:
172
166
  raise RuntimeError(f"Aleph broadcast failed: {http_status} {json_dumps(payload)}")
173
- raise RuntimeError("Aleph broadcast failed: no compatible request format was accepted")
167
+ raise RuntimeError("Aleph broadcast failed: retry budget exhausted")
174
168
 
175
169
 
176
170
  def parse_post_record(entry: object) -> dict[str, object] | None:
@@ -129,6 +129,21 @@ def signature_payload(chain: str, sender: str, message_type: str, item_hash: str
129
129
  return "\n".join([chain, sender, message_type, item_hash])
130
130
 
131
131
 
132
+ def registration_id_instance_item_hash(registration_id: str | None) -> str | None:
133
+ normalized = str(registration_id or "").strip()
134
+ if not normalized:
135
+ return None
136
+
137
+ parts = [part.strip() for part in normalized.split(":") if part.strip()]
138
+ if not parts:
139
+ return None
140
+
141
+ candidate = parts[-1]
142
+ if len(candidate) == 64 and all(char in "0123456789abcdefABCDEF" for char in candidate):
143
+ return candidate
144
+ return None
145
+
146
+
132
147
  def relay_authorization_payload(
133
148
  owner_address: str,
134
149
  publisher_address: str,
@@ -136,6 +151,7 @@ def relay_authorization_payload(
136
151
  registration_id: str | None,
137
152
  profile: str | None,
138
153
  version: str | None,
154
+ instance_item_hash: str | None,
139
155
  issued_at: int,
140
156
  ) -> dict[str, object]:
141
157
  return {
@@ -145,7 +161,7 @@ def relay_authorization_payload(
145
161
  "registrationId": registration_id,
146
162
  "profile": profile,
147
163
  "version": version,
148
- "instanceItemHash": None,
164
+ "instanceItemHash": instance_item_hash,
149
165
  "issuedAt": issued_at,
150
166
  "expiresAt": None,
151
167
  }
@@ -194,6 +210,7 @@ def load_owner_authorization(
194
210
  return None
195
211
 
196
212
  owner_address = address_from_private_key(owner_private_key)
213
+ instance_item_hash = registration_id_instance_item_hash(registration_id)
197
214
  payload = relay_authorization_payload(
198
215
  owner_address,
199
216
  publisher_address,
@@ -201,6 +218,7 @@ def load_owner_authorization(
201
218
  registration_id,
202
219
  profile,
203
220
  version,
221
+ instance_item_hash,
204
222
  issued_at,
205
223
  )
206
224
  return {
@@ -210,6 +228,28 @@ def load_owner_authorization(
210
228
  }
211
229
 
212
230
 
231
+ def resolve_owner_address(
232
+ env_values: dict[str, str],
233
+ owner_authorization: dict[str, object] | None,
234
+ ) -> str | None:
235
+ if isinstance(owner_authorization, dict):
236
+ payload = owner_authorization.get("payload")
237
+ if isinstance(payload, dict):
238
+ owner_address = str(payload.get("ownerAddress") or "").strip()
239
+ if owner_address:
240
+ return owner_address
241
+
242
+ owner_address = env_values.get("ALEPH_BOOTSTRAP_OWNER_ADDRESS", "").strip()
243
+ if owner_address:
244
+ return owner_address
245
+
246
+ owner_private_key = env_values.get("ALEPH_BOOTSTRAP_OWNER_PRIVATE_KEY", "").strip()
247
+ if owner_private_key:
248
+ return address_from_private_key(owner_private_key)
249
+
250
+ return None
251
+
252
+
213
253
  def post_json(url: str, body: dict[str, object]) -> tuple[int, dict]:
214
254
  data = json_dumps(body).encode("utf-8")
215
255
  request = urllib.request.Request(
@@ -243,35 +283,34 @@ def build_post_content(
243
283
  registration_id: str | None,
244
284
  profile: str | None,
245
285
  version: str | None,
286
+ owner_address: str | None,
246
287
  owner_authorization: dict[str, object] | None,
247
288
  relay_proof: dict[str, object],
248
289
  now_ms: int,
290
+ now_seconds: float,
249
291
  ref: str,
250
292
  post_type: str,
251
293
  ) -> dict[str, object]:
252
294
  content = {
253
295
  "peerId": peer_id,
254
296
  "multiaddrs": multiaddrs,
255
- "browserMultiaddrs": browser_multiaddrs if browser_multiaddrs else None,
256
297
  "registrationId": registration_id,
257
298
  "profile": profile,
258
299
  "version": version,
259
- "ownerAddress": (
260
- owner_authorization.get("payload", {}).get("ownerAddress")
261
- if isinstance(owner_authorization, dict)
262
- else None
263
- ),
300
+ "ownerAddress": owner_address,
264
301
  "publisherAddress": sender,
265
302
  "authorization": owner_authorization,
266
303
  "relayProof": relay_proof,
267
304
  "updatedAt": now_ms,
268
305
  }
306
+ if browser_multiaddrs:
307
+ content["browserMultiaddrs"] = browser_multiaddrs
269
308
  return {
270
309
  "type": post_type,
271
310
  "address": sender,
272
311
  "ref": ref,
273
312
  "content": content,
274
- "time": now_ms,
313
+ "time": now_seconds,
275
314
  }
276
315
 
277
316
 
@@ -347,22 +386,16 @@ def is_retryable_broadcast_failure(http_status: int, payload: object) -> bool:
347
386
 
348
387
  def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, object]:
349
388
  url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
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)
389
+ request_body = {"sync": True, "message": message}
390
+ max_attempts = 3
391
+ for index in range(max_attempts):
392
+ http_status, payload = post_json(url, request_body)
357
393
  if 200 <= http_status < 300:
358
394
  return 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
- )
395
+ can_retry = index < max_attempts - 1 and is_retryable_broadcast_failure(http_status, payload)
363
396
  if not can_retry:
364
397
  raise RuntimeError(f"Aleph broadcast failed: {http_status} {json_dumps(payload)}")
365
- raise RuntimeError("Aleph broadcast failed: no compatible request format was accepted")
398
+ raise RuntimeError("Aleph broadcast failed: retry budget exhausted")
366
399
 
367
400
 
368
401
  def parse_post_record(entry: object) -> dict[str, object] | None:
@@ -512,6 +545,7 @@ def main() -> None:
512
545
  version,
513
546
  now_ms,
514
547
  )
548
+ owner_address = resolve_owner_address(env_values, owner_authorization)
515
549
 
516
550
  proof_payload = relay_proof_payload(
517
551
  peer_id,
@@ -535,9 +569,11 @@ def main() -> None:
535
569
  registration_id,
536
570
  profile,
537
571
  version,
572
+ owner_address,
538
573
  owner_authorization,
539
574
  relay_proof,
540
575
  now_ms,
576
+ now_seconds,
541
577
  ref,
542
578
  post_type,
543
579
  )
@@ -26,6 +26,7 @@ UDP_PORT=""
26
26
  BOOTSTRAP_PUBLISHER_PRIVATE_KEY=""
27
27
  BOOTSTRAP_PUBLISHER_LIBP2P_IDENTITY_B64=""
28
28
  BOOTSTRAP_OWNER_PRIVATE_KEY=""
29
+ BOOTSTRAP_OWNER_ADDRESS=""
29
30
  BOOTSTRAP_OWNER_AUTHORIZATION_B64=""
30
31
  BOOTSTRAP_REGISTRATION_ID=""
31
32
  START_SERVICE=1
@@ -46,6 +47,7 @@ Usage:
46
47
  [--bootstrap-publisher-private-key <hex>] \
47
48
  [--bootstrap-publisher-libp2p-identity-b64 <base64>] \
48
49
  [--bootstrap-owner-private-key <hex>] \
50
+ [--bootstrap-owner-address <address>] \
49
51
  [--bootstrap-owner-authorization-b64 <base64>] \
50
52
  [--bootstrap-registration-id <id>] \
51
53
  [--no-start]
@@ -169,6 +171,10 @@ while [ "$#" -gt 0 ]; do
169
171
  BOOTSTRAP_OWNER_PRIVATE_KEY="${2:-}"
170
172
  shift 2
171
173
  ;;
174
+ --bootstrap-owner-address)
175
+ BOOTSTRAP_OWNER_ADDRESS="${2:-}"
176
+ shift 2
177
+ ;;
172
178
  --bootstrap-owner-authorization-b64)
173
179
  BOOTSTRAP_OWNER_AUTHORIZATION_B64="${2:-}"
174
180
  shift 2
@@ -275,6 +281,7 @@ fi
275
281
  if [ -n "${BOOTSTRAP_OWNER_PRIVATE_KEY}" ]; then
276
282
  write_env_var "ALEPH_BOOTSTRAP_OWNER_PRIVATE_KEY" "${BOOTSTRAP_OWNER_PRIVATE_KEY}"
277
283
  fi
284
+ write_env_var "ALEPH_BOOTSTRAP_OWNER_ADDRESS" "${BOOTSTRAP_OWNER_ADDRESS}"
278
285
  if [ -n "${BOOTSTRAP_OWNER_AUTHORIZATION_B64}" ]; then
279
286
  write_env_var "ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64" "${BOOTSTRAP_OWNER_AUTHORIZATION_B64}"
280
287
  fi
@@ -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
@@ -7,6 +9,8 @@ import threading
7
9
  import time
8
10
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9
11
  from pathlib import Path
12
+ from urllib import error as urllib_error
13
+ from urllib import request as urllib_request
10
14
  from urllib.error import HTTPError, URLError
11
15
  from urllib.parse import urlencode, urlsplit
12
16
  from urllib.request import urlopen
@@ -23,12 +27,36 @@ AUTHORIZED_KEYS_FILE = os.environ.get("AUTHORIZED_KEYS_FILE", "/root/.ssh/author
23
27
  ALEPH_API_HOST = os.environ.get("ALEPH_API_HOST", "https://api2.aleph.im")
24
28
  BOOTSTRAP_CONFIG_KEY = os.environ.get("BOOTSTRAP_CONFIG_KEY", "vm-bootstrap-config")
25
29
  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")
26
35
  DESCRIBE_TIMEOUT_SECONDS = int(os.environ.get("DESCRIBE_TIMEOUT_SECONDS", "15"))
27
36
  DESCRIBE_INTERVAL_SECONDS = float(os.environ.get("DESCRIBE_INTERVAL_SECONDS", "1"))
28
37
 
29
38
  CONFIGURE_LOCK = threading.Lock()
30
39
 
31
40
 
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
+
32
60
  def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
33
61
  handler.send_header("Access-Control-Allow-Origin", "*")
34
62
  handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
@@ -136,6 +164,9 @@ def _build_configure_args(record: dict) -> list[str]:
136
164
  registration_id = str(bootstrap.get("registrationId") or "").strip()
137
165
  if registration_id:
138
166
  args.extend(["--bootstrap-registration-id", registration_id])
167
+ owner_address = str(record.get("ownerAddress") or "").strip()
168
+ if owner_address:
169
+ args.extend(["--bootstrap-owner-address", owner_address])
139
170
  owner_auth = str(bootstrap.get("ownerAuthorizationBase64") or "").strip()
140
171
  if owner_auth:
141
172
  args.extend(["--bootstrap-owner-authorization-b64", owner_auth])
@@ -143,7 +174,7 @@ def _build_configure_args(record: dict) -> list[str]:
143
174
  return args
144
175
 
145
176
 
146
- def _run_configure_process(args: list[str]) -> tuple[bool, str]:
177
+ def _run_configure_process(args: list[str], bootstrap_record: dict | None = None) -> tuple[bool, str]:
147
178
  with CONFIGURE_LOCK:
148
179
  if os.path.exists(READY_FILE):
149
180
  return True, "ready"
@@ -153,9 +184,223 @@ def _run_configure_process(args: list[str]) -> tuple[bool, str]:
153
184
  return False, error.stderr.strip() or error.stdout.strip() or str(error)
154
185
 
155
186
  _start_metadata_generation()
187
+ if isinstance(bootstrap_record, dict):
188
+ try:
189
+ _publish_vm_bootstrap_config_signal(bootstrap_record)
190
+ except Exception as error: # pragma: no cover - runtime error path
191
+ return False, f"configured relay but failed to publish bootstrap config signal: {error}"
156
192
  return True, result.stdout.strip()
157
193
 
158
194
 
195
+ def _address_from_private_key(private_key: str) -> str:
196
+ try:
197
+ from eth_account import Account
198
+ except ImportError as error: # pragma: no cover - runtime dependency
199
+ raise RuntimeError(
200
+ "eth-account is required for guest-side bootstrap status publishing"
201
+ ) from error
202
+
203
+ return Account.from_key(private_key).address
204
+
205
+
206
+ def _sign_personal_message(private_key: str, payload: str) -> str:
207
+ try:
208
+ from eth_account import Account
209
+ from eth_account.messages import encode_defunct
210
+ except ImportError as error: # pragma: no cover - runtime dependency
211
+ raise RuntimeError(
212
+ "eth-account is required for guest-side bootstrap status publishing"
213
+ ) from error
214
+
215
+ message = encode_defunct(text=payload)
216
+ signed = Account.sign_message(message, private_key=private_key)
217
+ signature = signed.signature.hex()
218
+ return signature if signature.startswith("0x") else f"0x{signature}"
219
+
220
+
221
+ def _signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
222
+ return "\n".join([chain, sender, message_type, item_hash])
223
+
224
+
225
+ def _iter_validation_errors(payload: object) -> list[dict]:
226
+ if isinstance(payload, list):
227
+ return [entry for entry in payload if isinstance(entry, dict)]
228
+ return []
229
+
230
+
231
+ def _is_invalid_message_format(http_status: int, payload: object) -> bool:
232
+ if http_status != 422:
233
+ return False
234
+
235
+ if isinstance(payload, dict):
236
+ details = payload.get("details")
237
+ if isinstance(details, str) and "InvalidMessageFormat" in details:
238
+ return True
239
+ if isinstance(details, dict):
240
+ message = details.get("message")
241
+ if isinstance(message, str) and "InvalidMessageFormat" in message:
242
+ return True
243
+
244
+ for entry in _iter_validation_errors(payload):
245
+ message = entry.get("msg")
246
+ location = entry.get("loc")
247
+ if isinstance(message, str) and "InvalidMessageFormat" in message:
248
+ return True
249
+ if message == "Field required" and location == ["message"]:
250
+ return True
251
+
252
+ return False
253
+
254
+
255
+ def _is_retryable_broadcast_failure(http_status: int, payload: object) -> bool:
256
+ if http_status >= 500:
257
+ return True
258
+ if isinstance(payload, dict):
259
+ publication_status = payload.get("publication_status")
260
+ if isinstance(publication_status, dict):
261
+ status = publication_status.get("status")
262
+ if isinstance(status, str) and status.strip().lower() == "error":
263
+ return True
264
+ return False
265
+
266
+
267
+ def _post_json(url: str, body: dict[str, object]) -> tuple[int, object]:
268
+ data = _json_dumps(body).encode("utf-8")
269
+ request = urllib_request.Request(
270
+ url,
271
+ data=data,
272
+ headers={"content-type": "application/json"},
273
+ method="POST",
274
+ )
275
+ try:
276
+ with urllib_request.urlopen(request, timeout=30) as response:
277
+ payload = response.read().decode("utf-8")
278
+ return response.status, json.loads(payload or "{}")
279
+ except urllib_error.HTTPError as error:
280
+ payload = error.read().decode("utf-8")
281
+ try:
282
+ return error.code, json.loads(payload or "{}")
283
+ except json.JSONDecodeError:
284
+ return error.code, {"details": payload}
285
+
286
+
287
+ def _broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, object]:
288
+ url = f"{api_host.rstrip('/')}/api/v0/messages"
289
+ request_body = {"sync": True, "message": message}
290
+ max_attempts = 3
291
+ for index in range(max_attempts):
292
+ http_status, payload = _post_json(url, request_body)
293
+ if 200 <= http_status < 300:
294
+ return http_status, payload
295
+ can_retry = index < max_attempts - 1 and _is_retryable_broadcast_failure(http_status, payload)
296
+ if not can_retry:
297
+ raise RuntimeError(
298
+ f"Aleph bootstrap status broadcast failed: {http_status} {_json_dumps(payload)}"
299
+ )
300
+ raise RuntimeError("Aleph bootstrap status broadcast failed: retry budget exhausted")
301
+
302
+
303
+ def _publish_vm_bootstrap_config_signal(record: dict) -> None:
304
+ env_values = _parse_env_file(ENV_FILE)
305
+ publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
306
+ if not publisher_private_key:
307
+ raise RuntimeError("missing ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY")
308
+
309
+ owner_authorization_b64 = env_values.get("ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64", "").strip()
310
+ owner_authorization = None
311
+ if owner_authorization_b64:
312
+ decoded = base64.b64decode(owner_authorization_b64).decode("utf-8")
313
+ parsed = json.loads(decoded)
314
+ if isinstance(parsed, dict):
315
+ owner_authorization = parsed
316
+
317
+ owner_address = ""
318
+ if isinstance(owner_authorization, dict):
319
+ payload = owner_authorization.get("payload")
320
+ if isinstance(payload, dict):
321
+ owner_address = str(payload.get("ownerAddress") or "").strip()
322
+ if not owner_address:
323
+ owner_address = str(record.get("ownerAddress") or "").strip()
324
+ if not owner_address:
325
+ raise RuntimeError("missing ownerAddress for bootstrap config signal")
326
+
327
+ deployment_token = str(record.get("deploymentToken") or "").strip()
328
+ profile = str(record.get("profile") or "").strip()
329
+ instance_item_hash = str(record.get("instanceItemHash") or "").strip()
330
+ if not deployment_token or not profile or not instance_item_hash:
331
+ raise RuntimeError("bootstrap config signal is missing deployment metadata")
332
+
333
+ metadata, metadata_error = _fresh_metadata_payload()
334
+ if not metadata:
335
+ raise RuntimeError(
336
+ f"bootstrap config signal metadata is not ready: {metadata_error or 'metadata unavailable'}"
337
+ )
338
+
339
+ peer_id = metadata.get("peer_id")
340
+ probe_multiaddrs = metadata.get("probe_multiaddrs")
341
+ browser_bootstrap_multiaddrs = metadata.get("browser_bootstrap_multiaddrs")
342
+ if not isinstance(peer_id, str) or not peer_id.strip():
343
+ raise RuntimeError("bootstrap config signal is missing peer_id")
344
+ if not isinstance(probe_multiaddrs, list) or not any(
345
+ isinstance(entry, str) and entry.strip() for entry in probe_multiaddrs
346
+ ):
347
+ raise RuntimeError("bootstrap config signal is missing probe_multiaddrs")
348
+ if not isinstance(browser_bootstrap_multiaddrs, list):
349
+ browser_bootstrap_multiaddrs = []
350
+
351
+ publisher_address = _address_from_private_key(publisher_private_key)
352
+ now_ms = int(time.time() * 1000)
353
+ content = {
354
+ "type": BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE,
355
+ "address": publisher_address,
356
+ "ref": BOOTSTRAP_CONFIG_SIGNAL_REF,
357
+ "content": {
358
+ "deploymentToken": deployment_token,
359
+ "status": "applied",
360
+ "profile": profile,
361
+ "ownerAddress": owner_address,
362
+ "instanceItemHash": instance_item_hash,
363
+ "updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now_ms / 1000)),
364
+ "publisherAddress": publisher_address,
365
+ "authorization": owner_authorization,
366
+ "peerId": peer_id.strip(),
367
+ "probeMultiaddrs": [
368
+ entry.strip()
369
+ for entry in probe_multiaddrs
370
+ if isinstance(entry, str) and entry.strip()
371
+ ],
372
+ "browserBootstrapMultiaddrs": [
373
+ entry.strip()
374
+ for entry in browser_bootstrap_multiaddrs
375
+ if isinstance(entry, str) and entry.strip()
376
+ ],
377
+ },
378
+ "time": now_ms,
379
+ }
380
+ item_content = _json_dumps(content)
381
+ unsigned_message = {
382
+ "channel": BOOTSTRAP_CHANNEL,
383
+ "sender": publisher_address,
384
+ "chain": "ETH",
385
+ "type": "POST",
386
+ "time": now_ms / 1000,
387
+ "item_type": "inline",
388
+ "item_content": item_content,
389
+ "item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
390
+ }
391
+ signed_message = dict(unsigned_message)
392
+ signed_message["signature"] = _sign_personal_message(
393
+ publisher_private_key,
394
+ _signature_payload(
395
+ str(unsigned_message["chain"]),
396
+ str(unsigned_message["sender"]),
397
+ str(unsigned_message["type"]),
398
+ str(unsigned_message["item_hash"]),
399
+ ),
400
+ )
401
+ _broadcast_aleph_message(ALEPH_API_HOST, signed_message)
402
+
403
+
159
404
  def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
160
405
  owner_address, deployment_token = _extract_guest_bootstrap_locator()
161
406
  if not owner_address or not deployment_token:
@@ -197,7 +442,7 @@ def _poll_bootstrap_config_loop() -> None:
197
442
  except ValueError:
198
443
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
199
444
  continue
200
- ok, _ = _run_configure_process(args)
445
+ ok, _ = _run_configure_process(args, record)
201
446
  if ok:
202
447
  return
203
448
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
@@ -274,6 +519,7 @@ class Handler(BaseHTTPRequestHandler):
274
519
  "bootstrap_publisher_libp2p_identity_b64"
275
520
  )
276
521
  bootstrap_owner_private_key = payload.get("bootstrap_owner_private_key")
522
+ bootstrap_owner_address = payload.get("bootstrap_owner_address")
277
523
  bootstrap_owner_authorization_b64 = payload.get("bootstrap_owner_authorization_b64")
278
524
  bootstrap_registration_id = payload.get("bootstrap_registration_id")
279
525
  no_start = bool(payload.get("no_start"))
@@ -306,6 +552,8 @@ class Handler(BaseHTTPRequestHandler):
306
552
  )
307
553
  if bootstrap_owner_private_key is not None:
308
554
  args.extend(["--bootstrap-owner-private-key", str(bootstrap_owner_private_key)])
555
+ if bootstrap_owner_address is not None:
556
+ args.extend(["--bootstrap-owner-address", str(bootstrap_owner_address)])
309
557
  if bootstrap_owner_authorization_b64 is not None:
310
558
  args.extend(
311
559
  ["--bootstrap-owner-authorization-b64", str(bootstrap_owner_authorization_b64)]