@le-space/rootfs 0.3.0 → 0.3.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.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,8 @@ PY_LIBP2P_DIR="${PY_LIBP2P_DIR:-${REPO_DIR}/py-libp2p}"
11
11
  ORBITDB_RELAY_PINNER_DIR="${ORBITDB_RELAY_PINNER_DIR:-}"
12
12
  UNIVERSAL_CONNECTIVITY_DIR="${UNIVERSAL_CONNECTIVITY_DIR:-}"
13
13
  OUT_DIR="${OUT_DIR:-${APP_DIR}/dist-rootfs}"
14
+ HOST_UID="${HOST_UID:-}"
15
+ HOST_GID="${HOST_GID:-}"
14
16
  BASE_URL="${BASE_URL:-https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2}"
15
17
  BASE_IMAGE="${OUT_DIR}/debian-12-genericcloud-amd64.qcow2"
16
18
  IMAGE_SIZE="${IMAGE_SIZE:-${ROOTFS_IMAGE_SIZE:-20G}}"
@@ -224,15 +226,19 @@ case "${ROOTFS_PROFILE}" in
224
226
  --copy-in "${APP_TAR}:/opt"
225
227
  --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap.sh:/usr/local/sbin"
226
228
  --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-configure.sh:/usr/local/sbin"
229
+ --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap-refresh.py:/usr/local/sbin"
227
230
  --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-autotls-refresh.py:/usr/local/sbin"
228
231
  --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-setup-server.py:/usr/local/sbin"
229
232
  --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-describe.py:/usr/local/sbin"
230
233
  --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap.service:/etc/systemd/system"
234
+ --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap-refresh.service:/etc/systemd/system"
235
+ --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap-refresh.timer:/etc/systemd/system"
231
236
  --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-autotls-refresh.service:/etc/systemd/system"
232
237
  --copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap.conf:/etc/systemd/system/orbitdb-relay-pinner.service.d"
233
238
  --run-command "tar -xf /opt/$(basename "${APP_TAR}") -C /opt/orbitdb-relay-pinner"
234
239
  --run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-bootstrap.sh"
235
240
  --run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-configure.sh"
241
+ --run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-bootstrap-refresh.py"
236
242
  --run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-autotls-refresh.py"
237
243
  --run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-setup-server.py"
238
244
  --run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-describe.py"
@@ -333,3 +339,7 @@ if [ "${ROOTFS_SPARSIFY}" = "1" ] && command -v virt-sparsify >/dev/null 2>&1; t
333
339
  fi
334
340
 
335
341
  echo "Rootfs image ready at ${IMAGE}"
342
+
343
+ if [ -n "${HOST_UID}" ] && [ -n "${HOST_GID}" ]; then
344
+ chown -R "${HOST_UID}:${HOST_GID}" "${OUT_DIR}"
345
+ fi
@@ -0,0 +1,546 @@
1
+ #!/usr/bin/env python3
2
+ import base64
3
+ import hashlib
4
+ import ipaddress
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+
14
+ try:
15
+ from eth_account import Account
16
+ from eth_account.messages import encode_defunct
17
+ except ImportError as error: # pragma: no cover - runtime dependency
18
+ raise SystemExit(
19
+ "eth-account is required for guest-side bootstrap refresh publishing"
20
+ ) from error
21
+
22
+
23
+ ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay-pinner")
24
+ DESCRIBE_SCRIPT = os.environ.get(
25
+ "DESCRIBE_SCRIPT", "/usr/local/sbin/orbitdb-relay-pinner-describe.py"
26
+ )
27
+ DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api2.aleph.im")
28
+ DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
29
+ DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
30
+ DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", "relay-bootstrap")
31
+ DEFAULT_PROFILE = os.environ.get("ALEPH_BOOTSTRAP_PROFILE", "orbitdb-relay-pinner")
32
+ MAX_PREVIOUS_PAGES = int(os.environ.get("ALEPH_BOOTSTRAP_MAX_PREVIOUS_PAGES", "5"))
33
+ PAGINATION = int(os.environ.get("ALEPH_BOOTSTRAP_PAGINATION", "50"))
34
+
35
+
36
+ def parse_env_file(path: str) -> dict[str, str]:
37
+ values: dict[str, str] = {}
38
+ if not os.path.exists(path):
39
+ return values
40
+
41
+ with open(path, encoding="utf-8") as handle:
42
+ for line in handle:
43
+ stripped = line.strip()
44
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
45
+ continue
46
+ key, value = stripped.split("=", 1)
47
+ values[key.strip()] = value.strip()
48
+ return values
49
+
50
+
51
+ def dedupe(values: list[str]) -> list[str]:
52
+ seen: set[str] = set()
53
+ result: list[str] = []
54
+ for value in values:
55
+ if value and value not in seen:
56
+ seen.add(value)
57
+ result.append(value)
58
+ return result
59
+
60
+
61
+ def split_multiaddr(addr: str) -> list[str]:
62
+ return [part for part in addr.split("/") if part]
63
+
64
+
65
+ def has_peer_id(addr: str) -> bool:
66
+ return "p2p" in split_multiaddr(addr)
67
+
68
+
69
+ def is_browser_dialable(addr: str) -> bool:
70
+ normalized = addr.lower()
71
+ return any(
72
+ token in normalized
73
+ for token in ("/ws", "/wss", "/webtransport", "/webrtc-direct")
74
+ )
75
+
76
+
77
+ def is_public_multiaddr(addr: str) -> bool:
78
+ parts = split_multiaddr(addr)
79
+ if not parts:
80
+ return False
81
+
82
+ try:
83
+ for index, token in enumerate(parts[:-1]):
84
+ value = parts[index + 1]
85
+ if token == "ip4" and ipaddress.ip_address(value).is_private:
86
+ return False
87
+ if token == "ip6":
88
+ ip = ipaddress.ip_address(value)
89
+ if ip.is_loopback or ip.is_private or ip.is_link_local:
90
+ return False
91
+ if token in ("dns", "dns4", "dns6"):
92
+ host = value.strip().lower()
93
+ if host == "localhost" or host.endswith(".localhost") or host.endswith(".local"):
94
+ return False
95
+ except ValueError:
96
+ return False
97
+
98
+ return has_peer_id(addr)
99
+
100
+
101
+ def filter_public_multiaddrs(values: list[str], browser_only: bool = False) -> list[str]:
102
+ filtered: list[str] = []
103
+ for value in values:
104
+ if not isinstance(value, str):
105
+ continue
106
+ candidate = value.strip()
107
+ if not candidate or not is_public_multiaddr(candidate):
108
+ continue
109
+ if browser_only and not is_browser_dialable(candidate):
110
+ continue
111
+ filtered.append(candidate)
112
+ return dedupe(filtered)
113
+
114
+
115
+ def json_dumps(payload: object) -> str:
116
+ return json.dumps(payload, separators=(",", ":"))
117
+
118
+
119
+ def sign_personal_message(private_key: str, payload: str) -> str:
120
+ message = encode_defunct(text=payload)
121
+ signed = Account.sign_message(message, private_key=private_key)
122
+ return signed.signature.hex()
123
+
124
+
125
+ def address_from_private_key(private_key: str) -> str:
126
+ return Account.from_key(private_key).address
127
+
128
+
129
+ def signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
130
+ return "\n".join([chain, sender, message_type, item_hash])
131
+
132
+
133
+ def relay_authorization_payload(
134
+ owner_address: str,
135
+ publisher_address: str,
136
+ peer_id: str,
137
+ registration_id: str | None,
138
+ profile: str | None,
139
+ version: str | None,
140
+ issued_at: int,
141
+ ) -> dict[str, object]:
142
+ return {
143
+ "ownerAddress": owner_address,
144
+ "publisherAddress": publisher_address,
145
+ "peerId": peer_id,
146
+ "registrationId": registration_id,
147
+ "profile": profile,
148
+ "version": version,
149
+ "instanceItemHash": None,
150
+ "issuedAt": issued_at,
151
+ "expiresAt": None,
152
+ }
153
+
154
+
155
+ def relay_proof_payload(
156
+ peer_id: str,
157
+ multiaddrs: list[str],
158
+ browser_multiaddrs: list[str],
159
+ registration_id: str | None,
160
+ profile: str | None,
161
+ version: str | None,
162
+ updated_at: int,
163
+ ) -> dict[str, object]:
164
+ payload: dict[str, object] = {
165
+ "peerId": peer_id,
166
+ "multiaddrs": dedupe(multiaddrs),
167
+ "registrationId": registration_id,
168
+ "profile": profile,
169
+ "version": version,
170
+ "updatedAt": updated_at,
171
+ }
172
+ if browser_multiaddrs:
173
+ payload["browserMultiaddrs"] = dedupe(browser_multiaddrs)
174
+ return payload
175
+
176
+
177
+ def load_owner_authorization(
178
+ env_values: dict[str, str],
179
+ publisher_address: str,
180
+ peer_id: str,
181
+ registration_id: str | None,
182
+ profile: str | None,
183
+ version: str | None,
184
+ issued_at: int,
185
+ ) -> dict[str, object] | None:
186
+ encoded = env_values.get("ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64", "").strip()
187
+ if encoded:
188
+ decoded = base64.b64decode(encoded).decode("utf-8")
189
+ payload = json.loads(decoded)
190
+ if isinstance(payload, dict):
191
+ return payload
192
+
193
+ owner_private_key = env_values.get("ALEPH_BOOTSTRAP_OWNER_PRIVATE_KEY", "").strip()
194
+ if not owner_private_key:
195
+ return None
196
+
197
+ owner_address = address_from_private_key(owner_private_key)
198
+ payload = relay_authorization_payload(
199
+ owner_address,
200
+ publisher_address,
201
+ peer_id,
202
+ registration_id,
203
+ profile,
204
+ version,
205
+ issued_at,
206
+ )
207
+ return {
208
+ "scheme": "personal_sign",
209
+ "payload": payload,
210
+ "signature": sign_personal_message(owner_private_key, json_dumps(payload)),
211
+ }
212
+
213
+
214
+ def post_json(url: str, body: dict[str, object]) -> tuple[int, dict]:
215
+ data = json_dumps(body).encode("utf-8")
216
+ request = urllib.request.Request(
217
+ url,
218
+ data=data,
219
+ headers={"content-type": "application/json"},
220
+ method="POST",
221
+ )
222
+ try:
223
+ with urllib.request.urlopen(request, timeout=30) as response:
224
+ payload = response.read().decode("utf-8")
225
+ return response.status, json.loads(payload or "{}")
226
+ except urllib.error.HTTPError as error:
227
+ payload = error.read().decode("utf-8")
228
+ try:
229
+ return error.code, json.loads(payload or "{}")
230
+ except json.JSONDecodeError:
231
+ return error.code, {"details": payload}
232
+
233
+
234
+ def get_json(url: str) -> dict:
235
+ with urllib.request.urlopen(url, timeout=30) as response:
236
+ return json.loads(response.read().decode("utf-8") or "{}")
237
+
238
+
239
+ def build_post_content(
240
+ sender: str,
241
+ peer_id: str,
242
+ multiaddrs: list[str],
243
+ browser_multiaddrs: list[str],
244
+ registration_id: str | None,
245
+ profile: str | None,
246
+ version: str | None,
247
+ owner_authorization: dict[str, object] | None,
248
+ relay_proof: dict[str, object],
249
+ now_ms: int,
250
+ ref: str,
251
+ post_type: str,
252
+ ) -> dict[str, object]:
253
+ content = {
254
+ "peerId": peer_id,
255
+ "multiaddrs": multiaddrs,
256
+ "browserMultiaddrs": browser_multiaddrs if browser_multiaddrs else None,
257
+ "registrationId": registration_id,
258
+ "profile": profile,
259
+ "version": version,
260
+ "ownerAddress": (
261
+ owner_authorization.get("payload", {}).get("ownerAddress")
262
+ if isinstance(owner_authorization, dict)
263
+ else None
264
+ ),
265
+ "publisherAddress": sender,
266
+ "authorization": owner_authorization,
267
+ "relayProof": relay_proof,
268
+ "updatedAt": now_ms,
269
+ }
270
+ return {
271
+ "type": post_type,
272
+ "address": sender,
273
+ "ref": ref,
274
+ "content": content,
275
+ "time": now_ms,
276
+ }
277
+
278
+
279
+ def build_unsigned_message(sender: str, item_content: str, now_seconds: float, channel: str) -> dict[str, object]:
280
+ item_hash = hashlib.sha256(item_content.encode("utf-8")).hexdigest()
281
+ return {
282
+ "channel": channel,
283
+ "sender": sender,
284
+ "chain": "ETH",
285
+ "type": "POST",
286
+ "time": now_seconds,
287
+ "item_type": "inline",
288
+ "item_content": item_content,
289
+ "item_hash": item_hash,
290
+ }
291
+
292
+
293
+ def sign_aleph_message(unsigned_message: dict[str, object], private_key: str) -> dict[str, object]:
294
+ signed = dict(unsigned_message)
295
+ signed["signature"] = sign_personal_message(
296
+ private_key,
297
+ signature_payload(
298
+ str(unsigned_message["chain"]),
299
+ str(unsigned_message["sender"]),
300
+ str(unsigned_message["type"]),
301
+ str(unsigned_message["item_hash"]),
302
+ ),
303
+ )
304
+ return signed
305
+
306
+
307
+ def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, dict]:
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
+ ]
314
+ last: tuple[int, dict] | None = None
315
+ for attempt in attempts:
316
+ last = post_json(url, attempt)
317
+ if 200 <= last[0] < 300:
318
+ 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])}")
322
+
323
+
324
+ def parse_post_record(entry: object) -> dict[str, object] | None:
325
+ if not isinstance(entry, dict):
326
+ return None
327
+ item_hash = entry.get("item_hash") or entry.get("hash")
328
+ if not isinstance(item_hash, str) or not item_hash:
329
+ return None
330
+
331
+ sender = entry.get("address") or entry.get("sender")
332
+ item_content = entry.get("item_content")
333
+ if isinstance(item_content, str):
334
+ try:
335
+ item_content = json.loads(item_content)
336
+ except json.JSONDecodeError:
337
+ item_content = None
338
+
339
+ if not isinstance(item_content, dict):
340
+ return None
341
+
342
+ content = item_content.get("content")
343
+ if not isinstance(content, dict):
344
+ return None
345
+
346
+ return {
347
+ "item_hash": item_hash,
348
+ "sender": sender,
349
+ "registration_id": content.get("registrationId"),
350
+ }
351
+
352
+
353
+ def fetch_previous_hashes(
354
+ api_host: str,
355
+ channel: str,
356
+ ref: str,
357
+ post_type: str,
358
+ sender: str,
359
+ registration_id: str | None,
360
+ current_item_hash: str,
361
+ ) -> list[str]:
362
+ if not registration_id:
363
+ return []
364
+
365
+ found: list[str] = []
366
+ for page in range(1, MAX_PREVIOUS_PAGES + 1):
367
+ url = (
368
+ f"{api_host.rstrip('/')}/api/v0/posts.json?"
369
+ f"channels={urllib.parse.quote(channel)}&"
370
+ f"refs={urllib.parse.quote(ref)}&"
371
+ f"types={urllib.parse.quote(post_type)}&"
372
+ f"pagination={PAGINATION}&page={page}"
373
+ )
374
+ payload = get_json(url)
375
+ posts = payload.get("posts")
376
+ if not isinstance(posts, list):
377
+ break
378
+
379
+ for entry in posts:
380
+ parsed = parse_post_record(entry)
381
+ if parsed is None:
382
+ continue
383
+ if str(parsed["sender"]).lower() != sender.lower():
384
+ continue
385
+ if parsed["registration_id"] != registration_id:
386
+ continue
387
+ if parsed["item_hash"] == current_item_hash:
388
+ continue
389
+ found.append(str(parsed["item_hash"]))
390
+
391
+ if len(posts) < PAGINATION:
392
+ break
393
+
394
+ return dedupe(found)
395
+
396
+
397
+ def broadcast_forget(
398
+ api_host: str,
399
+ sender: str,
400
+ private_key: str,
401
+ hashes: list[str],
402
+ channel: str,
403
+ ) -> tuple[int, dict] | None:
404
+ if not hashes:
405
+ return None
406
+
407
+ now_seconds = time.time()
408
+ item_content = json_dumps(
409
+ {
410
+ "address": sender,
411
+ "time": now_seconds,
412
+ "hashes": hashes,
413
+ "aggregates": [],
414
+ "reason": f"Replace older relay bootstrap records for {sender}",
415
+ }
416
+ )
417
+ unsigned = {
418
+ "sender": sender,
419
+ "chain": "ETH",
420
+ "type": "FORGET",
421
+ "item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
422
+ "item_type": "inline",
423
+ "item_content": item_content,
424
+ "time": now_seconds,
425
+ "channel": channel,
426
+ }
427
+ return broadcast_aleph_message(api_host, sign_aleph_message(unsigned, private_key))
428
+
429
+
430
+ def main() -> None:
431
+ env_values = parse_env_file(ENV_FILE)
432
+ publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
433
+ registration_id = env_values.get("ALEPH_BOOTSTRAP_REGISTRATION_ID", "").strip() or None
434
+ if not publisher_private_key:
435
+ print(json_dumps({"status": "skipped", "reason": "missing publisher key"}))
436
+ return
437
+
438
+ describe = subprocess.run([DESCRIBE_SCRIPT], check=True, capture_output=True, text=True)
439
+ metadata = json.loads(describe.stdout.strip() or "{}")
440
+ peer_id = metadata.get("peer_id")
441
+ if not isinstance(peer_id, str) or not peer_id.strip():
442
+ raise SystemExit("unable to discover relay peer ID from describe script")
443
+
444
+ multiaddrs = filter_public_multiaddrs(metadata.get("probe_multiaddrs") or [])
445
+ browser_multiaddrs = filter_public_multiaddrs(
446
+ metadata.get("browser_bootstrap_multiaddrs") or [], browser_only=True
447
+ )
448
+ if not multiaddrs:
449
+ print(json_dumps({"status": "skipped", "reason": "no public multiaddrs"}))
450
+ return
451
+
452
+ publisher_address = address_from_private_key(publisher_private_key)
453
+ now_ms = int(time.time() * 1000)
454
+ now_seconds = now_ms / 1000
455
+ profile = env_values.get("ALEPH_BOOTSTRAP_PROFILE", DEFAULT_PROFILE).strip() or DEFAULT_PROFILE
456
+ version = env_values.get("ALEPH_BOOTSTRAP_VERSION", "").strip() or None
457
+ channel = env_values.get("ALEPH_BOOTSTRAP_CHANNEL", DEFAULT_CHANNEL).strip() or DEFAULT_CHANNEL
458
+ ref = env_values.get("ALEPH_BOOTSTRAP_REF", DEFAULT_REF).strip() or DEFAULT_REF
459
+ post_type = env_values.get("ALEPH_BOOTSTRAP_POST_TYPE", DEFAULT_POST_TYPE).strip() or DEFAULT_POST_TYPE
460
+ api_host = env_values.get("ALEPH_BOOTSTRAP_API_HOST", DEFAULT_API_HOST).strip() or DEFAULT_API_HOST
461
+
462
+ owner_authorization = load_owner_authorization(
463
+ env_values,
464
+ publisher_address,
465
+ peer_id,
466
+ registration_id,
467
+ profile,
468
+ version,
469
+ now_ms,
470
+ )
471
+ if owner_authorization is None:
472
+ print(json_dumps({"status": "skipped", "reason": "missing owner authorization"}))
473
+ return
474
+
475
+ proof_payload = relay_proof_payload(
476
+ peer_id,
477
+ multiaddrs,
478
+ browser_multiaddrs,
479
+ registration_id,
480
+ profile,
481
+ version,
482
+ now_ms,
483
+ )
484
+ relay_proof = {
485
+ "scheme": "personal_sign",
486
+ "payload": proof_payload,
487
+ "signature": sign_personal_message(publisher_private_key, json_dumps(proof_payload)),
488
+ }
489
+ post_content = build_post_content(
490
+ publisher_address,
491
+ peer_id,
492
+ multiaddrs,
493
+ browser_multiaddrs,
494
+ registration_id,
495
+ profile,
496
+ version,
497
+ owner_authorization,
498
+ relay_proof,
499
+ now_ms,
500
+ ref,
501
+ post_type,
502
+ )
503
+ item_content = json_dumps(post_content)
504
+ unsigned_message = build_unsigned_message(
505
+ publisher_address, item_content, now_seconds, channel
506
+ )
507
+ signed_message = sign_aleph_message(unsigned_message, publisher_private_key)
508
+ http_status, response = broadcast_aleph_message(api_host, signed_message)
509
+
510
+ previous_hashes = fetch_previous_hashes(
511
+ api_host,
512
+ channel,
513
+ ref,
514
+ post_type,
515
+ publisher_address,
516
+ registration_id,
517
+ str(unsigned_message["item_hash"]),
518
+ )
519
+ forget_response = broadcast_forget(
520
+ api_host, publisher_address, publisher_private_key, previous_hashes, channel
521
+ )
522
+
523
+ print(
524
+ json_dumps(
525
+ {
526
+ "status": "published",
527
+ "httpStatus": http_status,
528
+ "itemHash": unsigned_message["item_hash"],
529
+ "sender": publisher_address,
530
+ "peerId": peer_id,
531
+ "publishedMultiaddrs": multiaddrs,
532
+ "publishedBrowserMultiaddrs": browser_multiaddrs,
533
+ "forgottenHashes": previous_hashes,
534
+ "forgetResponse": forget_response[1] if forget_response else None,
535
+ "response": response,
536
+ }
537
+ )
538
+ )
539
+
540
+
541
+ if __name__ == "__main__":
542
+ try:
543
+ main()
544
+ except Exception as error: # pragma: no cover - runtime error path
545
+ print(json_dumps({"status": "error", "error": str(error)}), file=sys.stderr)
546
+ raise
@@ -0,0 +1,13 @@
1
+ [Unit]
2
+ Description=Refresh Aleph bootstrap registration for orbitdb-relay-pinner
3
+ Wants=network-online.target
4
+ After=network-online.target orbitdb-relay-pinner.service
5
+ ConditionPathExists=/etc/default/orbitdb-relay-pinner.ready
6
+
7
+ [Service]
8
+ Type=oneshot
9
+ Environment=ENV_FILE=/etc/default/orbitdb-relay-pinner
10
+ Environment=DESCRIBE_SCRIPT=/usr/local/sbin/orbitdb-relay-pinner-describe.py
11
+ ExecStart=/usr/bin/python3 /usr/local/sbin/orbitdb-relay-pinner-bootstrap-refresh.py
12
+ StandardOutput=journal
13
+ StandardError=journal
@@ -0,0 +1,12 @@
1
+ [Unit]
2
+ Description=Periodic Aleph bootstrap refresh for orbitdb-relay-pinner
3
+
4
+ [Timer]
5
+ OnBootSec=20m
6
+ OnUnitActiveSec=6h
7
+ RandomizedDelaySec=15m
8
+ Persistent=true
9
+ Unit=orbitdb-relay-pinner-bootstrap-refresh.service
10
+
11
+ [Install]
12
+ WantedBy=timers.target
@@ -15,7 +15,7 @@ fi
15
15
 
16
16
  export DEBIAN_FRONTEND=noninteractive
17
17
  apt-get update
18
- apt-get install -y ca-certificates curl gnupg python3 build-essential caddy
18
+ apt-get install -y ca-certificates curl gnupg python3 python3-pip build-essential caddy
19
19
 
20
20
  if ! command -v node >/dev/null 2>&1 || [ "$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0)" -lt "${NODE_MIN_MAJOR}" ]; then
21
21
  curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
@@ -34,6 +34,8 @@ if ! command -v npm >/dev/null 2>&1; then
34
34
  exit 1
35
35
  fi
36
36
 
37
+ python3 -m pip install --break-system-packages --no-cache-dir eth-account
38
+
37
39
  mkdir -p "${DATA_DIR}" "$(dirname "${ENV_FILE}")"
38
40
 
39
41
  if ! id "${SERVICE_USER}" >/dev/null 2>&1; then