@le-space/rootfs 0.6.3 → 0.6.5

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.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,10 +27,16 @@ DESCRIBE_SCRIPT = os.environ.get(
27
27
  DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api.aleph.im")
28
28
  DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
29
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")
30
+ COMPACT_POST_TYPE = "relay-bootstrap-v2"
31
+ LEGACY_POST_TYPE = "relay-bootstrap"
32
+ DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", COMPACT_POST_TYPE)
31
33
  DEFAULT_PROFILE = os.environ.get("ALEPH_BOOTSTRAP_PROFILE", "orbitdb-relay")
32
34
  MAX_PREVIOUS_PAGES = int(os.environ.get("ALEPH_BOOTSTRAP_MAX_PREVIOUS_PAGES", "5"))
33
35
  PAGINATION = int(os.environ.get("ALEPH_BOOTSTRAP_PAGINATION", "50"))
36
+ COMPACT_MULTIADDR_LIMIT = int(os.environ.get("ALEPH_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT", "3"))
37
+ STALE_RECORD_MAX_AGE_SECONDS = int(
38
+ os.environ.get("ALEPH_BOOTSTRAP_STALE_RECORD_MAX_AGE_SECONDS", str(6 * 60 * 60))
39
+ )
34
40
 
35
41
 
36
42
  def parse_env_file(path: str) -> dict[str, str]:
@@ -112,6 +118,28 @@ def filter_public_multiaddrs(values: list[str], browser_only: bool = False) -> l
112
118
  return dedupe(filtered)
113
119
 
114
120
 
121
+ def browser_multiaddr_preference(addr: str) -> int:
122
+ normalized = addr.lower()
123
+ if "/tcp/443/tls/ws/" in normalized:
124
+ return 0
125
+ if "/tls/ws/" in normalized:
126
+ return 1
127
+ if "/webtransport/" in normalized:
128
+ return 2
129
+ if "/webrtc-direct/" in normalized:
130
+ return 3
131
+ if "/ws" in normalized:
132
+ return 4
133
+ return 5
134
+
135
+
136
+ def select_compact_multiaddrs(values: list[str]) -> list[str]:
137
+ public_browser_addrs = filter_public_multiaddrs(values, browser_only=True)
138
+ return sorted(public_browser_addrs, key=browser_multiaddr_preference)[
139
+ : max(1, COMPACT_MULTIADDR_LIMIT)
140
+ ]
141
+
142
+
115
143
  def json_dumps(payload: object) -> str:
116
144
  return json.dumps(payload, separators=(",", ":"))
117
145
 
@@ -339,60 +367,82 @@ def parse_post_record(entry: object) -> dict[str, object] | None:
339
367
  except json.JSONDecodeError:
340
368
  item_content = None
341
369
 
342
- if not isinstance(item_content, dict):
343
- return None
344
-
345
- content = item_content.get("content")
370
+ content_source = item_content if isinstance(item_content, dict) else entry
371
+ content = content_source.get("content")
346
372
  if not isinstance(content, dict):
347
373
  return None
348
374
 
349
375
  return {
350
376
  "item_hash": item_hash,
351
377
  "sender": sender,
378
+ "publisher_address": content.get("publisherAddress"),
352
379
  "registration_id": content.get("registrationId"),
380
+ "peer_id": content.get("peerId"),
381
+ "profile": content.get("profile"),
382
+ "updated_at": content.get("updatedAt"),
353
383
  }
354
384
 
355
385
 
386
+ def normalize_address(value: object) -> str:
387
+ return str(value or "").strip().lower()
388
+
389
+
390
+ def timestamp_ms(value: object) -> int | None:
391
+ if isinstance(value, bool):
392
+ return None
393
+ if isinstance(value, (int, float)):
394
+ return int(value)
395
+ if isinstance(value, str):
396
+ stripped = value.strip()
397
+ if stripped.isdigit():
398
+ return int(stripped)
399
+ return None
400
+
401
+
356
402
  def fetch_previous_hashes(
357
403
  api_host: str,
358
404
  channel: str,
359
405
  ref: str,
360
- post_type: str,
406
+ post_types: list[str],
361
407
  sender: str,
362
- registration_id: str | None,
363
408
  current_item_hash: str,
409
+ now_ms: int,
364
410
  ) -> list[str]:
365
- if not registration_id:
366
- return []
367
-
411
+ stale_cutoff_ms = now_ms - (STALE_RECORD_MAX_AGE_SECONDS * 1000)
412
+ normalized_sender = normalize_address(sender)
368
413
  found: list[str] = []
369
- for page in range(1, MAX_PREVIOUS_PAGES + 1):
370
- url = (
371
- f"{api_host.rstrip('/')}/api/v0/posts.json?"
372
- f"channels={urllib.parse.quote(channel)}&"
373
- f"refs={urllib.parse.quote(ref)}&"
374
- f"types={urllib.parse.quote(post_type)}&"
375
- f"pagination={PAGINATION}&page={page}"
376
- )
377
- payload = get_json(url)
378
- posts = payload.get("posts")
379
- if not isinstance(posts, list):
380
- break
381
-
382
- for entry in posts:
383
- parsed = parse_post_record(entry)
384
- if parsed is None:
385
- continue
386
- if str(parsed["sender"]).lower() != sender.lower():
387
- continue
388
- if parsed["registration_id"] != registration_id:
389
- continue
390
- if parsed["item_hash"] == current_item_hash:
391
- continue
392
- found.append(str(parsed["item_hash"]))
393
-
394
- if len(posts) < PAGINATION:
395
- break
414
+ for post_type in post_types:
415
+ for page in range(1, MAX_PREVIOUS_PAGES + 1):
416
+ url = (
417
+ f"{api_host.rstrip('/')}/api/v0/posts.json?"
418
+ f"channels={urllib.parse.quote(channel)}&"
419
+ f"refs={urllib.parse.quote(ref)}&"
420
+ f"types={urllib.parse.quote(post_type)}&"
421
+ f"pagination={PAGINATION}&page={page}"
422
+ )
423
+ payload = get_json(url)
424
+ posts = payload.get("posts")
425
+ if not isinstance(posts, list):
426
+ break
427
+
428
+ for entry in posts:
429
+ parsed = parse_post_record(entry)
430
+ if parsed is None:
431
+ continue
432
+ same_publisher = normalized_sender in {
433
+ normalize_address(parsed["sender"]),
434
+ normalize_address(parsed["publisher_address"]),
435
+ }
436
+ if not same_publisher:
437
+ continue
438
+ if parsed["item_hash"] == current_item_hash:
439
+ continue
440
+ updated_at = timestamp_ms(parsed["updated_at"])
441
+ if updated_at is not None and updated_at < stale_cutoff_ms:
442
+ found.append(str(parsed["item_hash"]))
443
+
444
+ if len(posts) < PAGINATION:
445
+ break
396
446
 
397
447
  return dedupe(found)
398
448
 
@@ -448,9 +498,6 @@ def main() -> None:
448
498
  browser_multiaddrs = filter_public_multiaddrs(
449
499
  metadata.get("browser_bootstrap_multiaddrs") or [], browser_only=True
450
500
  )
451
- if not multiaddrs:
452
- print(json_dumps({"status": "skipped", "reason": "no public multiaddrs"}))
453
- return
454
501
 
455
502
  publisher_address = address_from_private_key(publisher_private_key)
456
503
  now_ms = int(time.time() * 1000)
@@ -461,6 +508,17 @@ def main() -> None:
461
508
  ref = env_values.get("ALEPH_BOOTSTRAP_REF", DEFAULT_REF).strip() or DEFAULT_REF
462
509
  post_type = env_values.get("ALEPH_BOOTSTRAP_POST_TYPE", DEFAULT_POST_TYPE).strip() or DEFAULT_POST_TYPE
463
510
  api_host = env_values.get("ALEPH_BOOTSTRAP_API_HOST", DEFAULT_API_HOST).strip() or DEFAULT_API_HOST
511
+ compact = post_type == COMPACT_POST_TYPE
512
+ published_multiaddrs = (
513
+ select_compact_multiaddrs(browser_multiaddrs or multiaddrs)
514
+ if compact
515
+ else multiaddrs
516
+ )
517
+ published_browser_multiaddrs = [] if compact else browser_multiaddrs
518
+ if not published_multiaddrs:
519
+ reason = "no public browser multiaddrs" if compact else "no public multiaddrs"
520
+ print(json_dumps({"status": "skipped", "reason": reason}))
521
+ return
464
522
 
465
523
  owner_authorization = load_owner_authorization(
466
524
  env_values,
@@ -477,8 +535,8 @@ def main() -> None:
477
535
 
478
536
  proof_payload = relay_proof_payload(
479
537
  peer_id,
480
- multiaddrs,
481
- browser_multiaddrs,
538
+ published_multiaddrs,
539
+ published_browser_multiaddrs,
482
540
  registration_id,
483
541
  profile,
484
542
  version,
@@ -492,8 +550,8 @@ def main() -> None:
492
550
  post_content = build_post_content(
493
551
  publisher_address,
494
552
  peer_id,
495
- multiaddrs,
496
- browser_multiaddrs,
553
+ published_multiaddrs,
554
+ published_browser_multiaddrs,
497
555
  registration_id,
498
556
  profile,
499
557
  version,
@@ -514,10 +572,10 @@ def main() -> None:
514
572
  api_host,
515
573
  channel,
516
574
  ref,
517
- post_type,
575
+ [post_type, LEGACY_POST_TYPE] if compact else [post_type],
518
576
  publisher_address,
519
- registration_id,
520
577
  str(unsigned_message["item_hash"]),
578
+ now_ms,
521
579
  )
522
580
  forget_response = broadcast_forget(
523
581
  api_host, publisher_address, publisher_private_key, previous_hashes, channel
@@ -531,9 +589,10 @@ def main() -> None:
531
589
  "itemHash": unsigned_message["item_hash"],
532
590
  "sender": publisher_address,
533
591
  "peerId": peer_id,
534
- "publishedMultiaddrs": multiaddrs,
535
- "publishedBrowserMultiaddrs": browser_multiaddrs,
592
+ "publishedMultiaddrs": published_multiaddrs,
593
+ "publishedBrowserMultiaddrs": published_browser_multiaddrs or published_multiaddrs,
536
594
  "forgottenHashes": previous_hashes,
595
+ "forgetStaleOlderThanSeconds": STALE_RECORD_MAX_AGE_SECONDS,
537
596
  "forgetResponse": forget_response[1] if forget_response else None,
538
597
  "response": response,
539
598
  }
@@ -25,10 +25,16 @@ DESCRIBE_SCRIPT = os.environ.get("DESCRIBE_SCRIPT", "/usr/local/sbin/uc-go-peer-
25
25
  DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api.aleph.im")
26
26
  DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
27
27
  DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
28
- DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", "relay-bootstrap")
28
+ COMPACT_POST_TYPE = "relay-bootstrap-v2"
29
+ LEGACY_POST_TYPE = "relay-bootstrap"
30
+ DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", COMPACT_POST_TYPE)
29
31
  DEFAULT_PROFILE = os.environ.get("ALEPH_BOOTSTRAP_PROFILE", "uc-go-peer")
30
32
  MAX_PREVIOUS_PAGES = int(os.environ.get("ALEPH_BOOTSTRAP_MAX_PREVIOUS_PAGES", "5"))
31
33
  PAGINATION = int(os.environ.get("ALEPH_BOOTSTRAP_PAGINATION", "50"))
34
+ COMPACT_MULTIADDR_LIMIT = int(os.environ.get("ALEPH_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT", "3"))
35
+ STALE_RECORD_MAX_AGE_SECONDS = int(
36
+ os.environ.get("ALEPH_BOOTSTRAP_STALE_RECORD_MAX_AGE_SECONDS", str(6 * 60 * 60))
37
+ )
32
38
 
33
39
 
34
40
  def parse_env_file(path: str) -> dict[str, str]:
@@ -110,6 +116,28 @@ def filter_public_multiaddrs(values: list[str], browser_only: bool = False) -> l
110
116
  return dedupe(filtered)
111
117
 
112
118
 
119
+ def browser_multiaddr_preference(addr: str) -> int:
120
+ normalized = addr.lower()
121
+ if "/tcp/443/tls/ws/" in normalized:
122
+ return 0
123
+ if "/tls/ws/" in normalized:
124
+ return 1
125
+ if "/webtransport/" in normalized:
126
+ return 2
127
+ if "/webrtc-direct/" in normalized:
128
+ return 3
129
+ if "/ws" in normalized:
130
+ return 4
131
+ return 5
132
+
133
+
134
+ def select_compact_multiaddrs(values: list[str]) -> list[str]:
135
+ public_browser_addrs = filter_public_multiaddrs(values, browser_only=True)
136
+ return sorted(public_browser_addrs, key=browser_multiaddr_preference)[
137
+ : max(1, COMPACT_MULTIADDR_LIMIT)
138
+ ]
139
+
140
+
113
141
  def json_dumps(payload: object) -> str:
114
142
  return json.dumps(payload, separators=(",", ":"))
115
143
 
@@ -413,60 +441,82 @@ def parse_post_record(entry: object) -> dict[str, object] | None:
413
441
  except json.JSONDecodeError:
414
442
  item_content = None
415
443
 
416
- if not isinstance(item_content, dict):
417
- return None
418
-
419
- content = item_content.get("content")
444
+ content_source = item_content if isinstance(item_content, dict) else entry
445
+ content = content_source.get("content")
420
446
  if not isinstance(content, dict):
421
447
  return None
422
448
 
423
449
  return {
424
450
  "item_hash": item_hash,
425
451
  "sender": sender,
452
+ "publisher_address": content.get("publisherAddress"),
426
453
  "registration_id": content.get("registrationId"),
454
+ "peer_id": content.get("peerId"),
455
+ "profile": content.get("profile"),
456
+ "updated_at": content.get("updatedAt"),
427
457
  }
428
458
 
429
459
 
460
+ def normalize_address(value: object) -> str:
461
+ return str(value or "").strip().lower()
462
+
463
+
464
+ def timestamp_ms(value: object) -> int | None:
465
+ if isinstance(value, bool):
466
+ return None
467
+ if isinstance(value, (int, float)):
468
+ return int(value)
469
+ if isinstance(value, str):
470
+ stripped = value.strip()
471
+ if stripped.isdigit():
472
+ return int(stripped)
473
+ return None
474
+
475
+
430
476
  def fetch_previous_hashes(
431
477
  api_host: str,
432
478
  channel: str,
433
479
  ref: str,
434
- post_type: str,
480
+ post_types: list[str],
435
481
  sender: str,
436
- registration_id: str | None,
437
482
  current_item_hash: str,
483
+ now_ms: int,
438
484
  ) -> list[str]:
439
- if not registration_id:
440
- return []
441
-
485
+ stale_cutoff_ms = now_ms - (STALE_RECORD_MAX_AGE_SECONDS * 1000)
486
+ normalized_sender = normalize_address(sender)
442
487
  found: list[str] = []
443
- for page in range(1, MAX_PREVIOUS_PAGES + 1):
444
- url = (
445
- f"{api_host.rstrip('/')}/api/v0/posts.json?"
446
- f"channels={urllib.parse.quote(channel)}&"
447
- f"refs={urllib.parse.quote(ref)}&"
448
- f"types={urllib.parse.quote(post_type)}&"
449
- f"pagination={PAGINATION}&page={page}"
450
- )
451
- payload = get_json(url)
452
- posts = payload.get("posts")
453
- if not isinstance(posts, list):
454
- break
455
-
456
- for entry in posts:
457
- parsed = parse_post_record(entry)
458
- if parsed is None:
459
- continue
460
- if str(parsed["sender"]).lower() != sender.lower():
461
- continue
462
- if parsed["registration_id"] != registration_id:
463
- continue
464
- if parsed["item_hash"] == current_item_hash:
465
- continue
466
- found.append(str(parsed["item_hash"]))
467
-
468
- if len(posts) < PAGINATION:
469
- break
488
+ for post_type in post_types:
489
+ for page in range(1, MAX_PREVIOUS_PAGES + 1):
490
+ url = (
491
+ f"{api_host.rstrip('/')}/api/v0/posts.json?"
492
+ f"channels={urllib.parse.quote(channel)}&"
493
+ f"refs={urllib.parse.quote(ref)}&"
494
+ f"types={urllib.parse.quote(post_type)}&"
495
+ f"pagination={PAGINATION}&page={page}"
496
+ )
497
+ payload = get_json(url)
498
+ posts = payload.get("posts")
499
+ if not isinstance(posts, list):
500
+ break
501
+
502
+ for entry in posts:
503
+ parsed = parse_post_record(entry)
504
+ if parsed is None:
505
+ continue
506
+ same_publisher = normalized_sender in {
507
+ normalize_address(parsed["sender"]),
508
+ normalize_address(parsed["publisher_address"]),
509
+ }
510
+ if not same_publisher:
511
+ continue
512
+ if parsed["item_hash"] == current_item_hash:
513
+ continue
514
+ updated_at = timestamp_ms(parsed["updated_at"])
515
+ if updated_at is not None and updated_at < stale_cutoff_ms:
516
+ found.append(str(parsed["item_hash"]))
517
+
518
+ if len(posts) < PAGINATION:
519
+ break
470
520
 
471
521
  return dedupe(found)
472
522
 
@@ -522,9 +572,6 @@ def main() -> None:
522
572
  browser_multiaddrs = filter_public_multiaddrs(
523
573
  metadata.get("browser_bootstrap_multiaddrs") or [], browser_only=True
524
574
  )
525
- if not multiaddrs:
526
- print(json_dumps({"status": "skipped", "reason": "no public multiaddrs"}))
527
- return
528
575
 
529
576
  publisher_address = address_from_private_key(publisher_private_key)
530
577
  now_ms = int(time.time() * 1000)
@@ -535,6 +582,17 @@ def main() -> None:
535
582
  ref = env_values.get("ALEPH_BOOTSTRAP_REF", DEFAULT_REF).strip() or DEFAULT_REF
536
583
  post_type = env_values.get("ALEPH_BOOTSTRAP_POST_TYPE", DEFAULT_POST_TYPE).strip() or DEFAULT_POST_TYPE
537
584
  api_host = env_values.get("ALEPH_BOOTSTRAP_API_HOST", DEFAULT_API_HOST).strip() or DEFAULT_API_HOST
585
+ compact = post_type == COMPACT_POST_TYPE
586
+ published_multiaddrs = (
587
+ select_compact_multiaddrs(browser_multiaddrs or multiaddrs)
588
+ if compact
589
+ else multiaddrs
590
+ )
591
+ published_browser_multiaddrs = [] if compact else browser_multiaddrs
592
+ if not published_multiaddrs:
593
+ reason = "no public browser multiaddrs" if compact else "no public multiaddrs"
594
+ print(json_dumps({"status": "skipped", "reason": reason}))
595
+ return
538
596
 
539
597
  owner_authorization = load_owner_authorization(
540
598
  env_values,
@@ -549,8 +607,8 @@ def main() -> None:
549
607
 
550
608
  proof_payload = relay_proof_payload(
551
609
  peer_id,
552
- multiaddrs,
553
- browser_multiaddrs,
610
+ published_multiaddrs,
611
+ published_browser_multiaddrs,
554
612
  registration_id,
555
613
  profile,
556
614
  version,
@@ -564,8 +622,8 @@ def main() -> None:
564
622
  post_content = build_post_content(
565
623
  publisher_address,
566
624
  peer_id,
567
- multiaddrs,
568
- browser_multiaddrs,
625
+ published_multiaddrs,
626
+ published_browser_multiaddrs,
569
627
  registration_id,
570
628
  profile,
571
629
  version,
@@ -588,10 +646,10 @@ def main() -> None:
588
646
  api_host,
589
647
  channel,
590
648
  ref,
591
- post_type,
649
+ [post_type, LEGACY_POST_TYPE] if compact else [post_type],
592
650
  publisher_address,
593
- registration_id,
594
651
  str(unsigned_message["item_hash"]),
652
+ now_ms,
595
653
  )
596
654
  forget_response = broadcast_forget(
597
655
  api_host, publisher_address, publisher_private_key, previous_hashes, channel
@@ -605,9 +663,10 @@ def main() -> None:
605
663
  "itemHash": unsigned_message["item_hash"],
606
664
  "sender": publisher_address,
607
665
  "peerId": peer_id,
608
- "publishedMultiaddrs": multiaddrs,
609
- "publishedBrowserMultiaddrs": browser_multiaddrs,
666
+ "publishedMultiaddrs": published_multiaddrs,
667
+ "publishedBrowserMultiaddrs": published_browser_multiaddrs or published_multiaddrs,
610
668
  "forgottenHashes": previous_hashes,
669
+ "forgetStaleOlderThanSeconds": STALE_RECORD_MAX_AGE_SECONDS,
611
670
  "ownerAuthorizationPresent": owner_authorization is not None,
612
671
  "forgetResponse": forget_response[1] if forget_response else None,
613
672
  "response": response,
@@ -131,6 +131,9 @@ EOF
131
131
  write_env_var "PUBLIC_RECEIPTS_URL" ""
132
132
 
133
133
  rm -f "${BOOTSTRAP_PACKAGE_FILE}" "${BOOTSTRAP_VERIFICATION_FILE}"
134
+
135
+ # Caddy is started explicitly after /configure writes the deployment hostname.
136
+ systemctl disable caddy.service >/dev/null 2>&1 || true
134
137
  }
135
138
 
136
139
  case "${PHASE}" in
@@ -12,9 +12,14 @@ CADDY_SERVICE="${CADDY_SERVICE:-caddy.service}"
12
12
  CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}"
13
13
  SERVICE_PORT="${SERVICE_PORT:-8787}"
14
14
  PROXY_PORT="${PROXY_PORT:-8788}"
15
+ SERVICE_GROUP="${SERVICE_GROUP:-ucan-store}"
15
16
  PUBLIC_IPV4=""
16
17
  PUBLIC_IPV6=""
17
18
  PROXY_HOSTNAME=""
19
+ SERVICE_HOSTNAME=""
20
+ SERVICE_DID=""
21
+ SERVICE_ORIGIN=""
22
+ PUBLIC_STORAGE_ORIGIN=""
18
23
  WEBAUTHN_ORIGIN=""
19
24
  WEBAUTHN_ORIGIN_FALLBACKS=""
20
25
  ADMIN_DID=""
@@ -29,6 +34,9 @@ Usage:
29
34
  --public-ipv4 <ip> \
30
35
  [--public-ipv6 <ipv6>] \
31
36
  [--proxy-hostname <hostname>] \
37
+ [--service-did <did>] \
38
+ [--service-origin <origin>] \
39
+ [--public-storage-origin <origin>] \
32
40
  [--webauthn-origin <origin>] \
33
41
  [--webauthn-origin-fallbacks <csv>] \
34
42
  [--admin-did <did>] \
@@ -67,19 +75,67 @@ PY
67
75
  }
68
76
 
69
77
  write_caddyfile() {
70
- local hostname="$1"
78
+ local hostnames=()
79
+ local hostname
80
+ local known
81
+ for hostname in "$@"; do
82
+ if [ -z "${hostname}" ]; then
83
+ continue
84
+ fi
85
+ known="0"
86
+ if [ "${#hostnames[@]}" -gt 0 ]; then
87
+ for existing in "${hostnames[@]}"; do
88
+ if [ "${existing}" = "${hostname}" ]; then
89
+ known="1"
90
+ break
91
+ fi
92
+ done
93
+ fi
94
+ if [ "${known}" = "0" ]; then
95
+ hostnames+=("${hostname}")
96
+ fi
97
+ done
98
+
99
+ if [ "${#hostnames[@]}" -eq 0 ]; then
100
+ return 0
101
+ fi
102
+
103
+ local site_label
104
+ local index
105
+ site_label="${hostnames[0]}"
106
+ for ((index = 1; index < ${#hostnames[@]}; index++)); do
107
+ site_label+=", ${hostnames[${index}]}"
108
+ done
71
109
  mkdir -p "$(dirname "${CADDYFILE}")"
72
110
  cat > "${CADDYFILE}" <<EOF
73
111
  {
74
112
  auto_https disable_redirects
75
113
  }
76
114
 
77
- ${hostname} {
115
+ ${site_label} {
78
116
  reverse_proxy 127.0.0.1:${PROXY_PORT}
79
117
  }
80
118
  EOF
81
119
  }
82
120
 
121
+ derive_hostname_from_origin() {
122
+ python3 - "${1:-}" <<'PY'
123
+ import sys
124
+ from urllib.parse import urlsplit
125
+
126
+ raw = (sys.argv[1] or "").strip()
127
+ if not raw:
128
+ raise SystemExit(0)
129
+
130
+ parsed = urlsplit(raw)
131
+ host = (parsed.hostname or "").strip().lower()
132
+ if not host:
133
+ raise SystemExit(0)
134
+
135
+ print(host)
136
+ PY
137
+ }
138
+
83
139
  derive_service_did_from_hostname() {
84
140
  python3 - "${1:-}" <<'PY'
85
141
  import sys
@@ -138,8 +194,20 @@ install_bootstrap_package() {
138
194
  --admin-did "${ADMIN_DID}" \
139
195
  --summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null
140
196
  cp "${source_file}" "${BOOTSTRAP_PACKAGE_FILE}"
141
- chmod 0640 "${BOOTSTRAP_PACKAGE_FILE}"
142
- chmod 0644 "${BOOTSTRAP_VERIFICATION_FILE}"
197
+ if chgrp "${SERVICE_GROUP}" "${BOOTSTRAP_PACKAGE_FILE}" 2>/dev/null; then
198
+ chmod 0640 "${BOOTSTRAP_PACKAGE_FILE}"
199
+ else
200
+ chmod 0644 "${BOOTSTRAP_PACKAGE_FILE}"
201
+ fi
202
+ install_bootstrap_verification_permissions
203
+ }
204
+
205
+ install_bootstrap_verification_permissions() {
206
+ if chgrp "${SERVICE_GROUP}" "${BOOTSTRAP_VERIFICATION_FILE}" 2>/dev/null; then
207
+ chmod 0664 "${BOOTSTRAP_VERIFICATION_FILE}"
208
+ else
209
+ chmod 0666 "${BOOTSTRAP_VERIFICATION_FILE}"
210
+ fi
143
211
  }
144
212
 
145
213
  while [ "$#" -gt 0 ]; do
@@ -156,6 +224,18 @@ while [ "$#" -gt 0 ]; do
156
224
  PROXY_HOSTNAME="${2:-}"
157
225
  shift 2
158
226
  ;;
227
+ --service-did)
228
+ SERVICE_DID="${2:-}"
229
+ shift 2
230
+ ;;
231
+ --service-origin)
232
+ SERVICE_ORIGIN="${2:-}"
233
+ shift 2
234
+ ;;
235
+ --public-storage-origin)
236
+ PUBLIC_STORAGE_ORIGIN="${2:-}"
237
+ shift 2
238
+ ;;
159
239
  --webauthn-origin)
160
240
  WEBAUTHN_ORIGIN="${2:-}"
161
241
  shift 2
@@ -196,6 +276,8 @@ if [ -z "${PUBLIC_IPV4}" ]; then
196
276
  usage >&2
197
277
  exit 1
198
278
  fi
279
+ SERVICE_ORIGIN="${SERVICE_ORIGIN%/}"
280
+ PUBLIC_STORAGE_ORIGIN="${PUBLIC_STORAGE_ORIGIN%/}"
199
281
 
200
282
  touch "${ENV_FILE}"
201
283
 
@@ -217,14 +299,31 @@ if [ -n "${WEBAUTHN_ORIGIN}" ]; then
217
299
  fi
218
300
  write_env_var "WEBAUTHN_ORIGIN_FALLBACKS" "${WEBAUTHN_ORIGIN_FALLBACKS}"
219
301
 
302
+ if [ -z "${SERVICE_DID}" ] && [ -n "${PROXY_HOSTNAME}" ]; then
303
+ SERVICE_DID="$(derive_service_did_from_hostname "${PROXY_HOSTNAME}")"
304
+ fi
305
+
306
+ if [ -z "${SERVICE_ORIGIN}" ] && [ -n "${PROXY_HOSTNAME}" ]; then
307
+ SERVICE_ORIGIN="https://${PROXY_HOSTNAME}"
308
+ fi
309
+ SERVICE_HOSTNAME="$(derive_hostname_from_origin "${SERVICE_ORIGIN}")"
310
+
311
+ write_env_var "UCAN_STORE_SERVICE_DID" "${SERVICE_DID}"
312
+ write_env_var "UCAN_STORE_SERVICE_HOSTNAME" "${SERVICE_HOSTNAME}"
313
+
314
+ if [ -n "${SERVICE_ORIGIN}" ]; then
315
+ if [ -z "${PUBLIC_STORAGE_ORIGIN}" ]; then
316
+ PUBLIC_STORAGE_ORIGIN="${SERVICE_ORIGIN}"
317
+ fi
318
+ write_env_var "UCAN_STORE_SERVICE_ORIGIN" "${SERVICE_ORIGIN}"
319
+ write_env_var "UCAN_STORE_PUBLIC_STORAGE_ORIGIN" "${PUBLIC_STORAGE_ORIGIN}"
320
+ write_env_var "PUBLIC_UPLOAD_SERVICE_URL" "${SERVICE_ORIGIN}"
321
+ write_env_var "PUBLIC_REVOCATION_URL" "${SERVICE_ORIGIN}"
322
+ write_env_var "PUBLIC_RECEIPTS_URL" "${SERVICE_ORIGIN}/receipt/"
323
+ fi
324
+
220
325
  if [ -n "${PROXY_HOSTNAME}" ]; then
221
- write_env_var "UCAN_STORE_SERVICE_DID" "$(derive_service_did_from_hostname "${PROXY_HOSTNAME}")"
222
- write_caddyfile "${PROXY_HOSTNAME}"
223
- write_env_var "PUBLIC_UPLOAD_SERVICE_URL" "https://${PROXY_HOSTNAME}"
224
- write_env_var "PUBLIC_REVOCATION_URL" "https://${PROXY_HOSTNAME}"
225
- write_env_var "PUBLIC_RECEIPTS_URL" "https://${PROXY_HOSTNAME}/receipt/"
226
- else
227
- write_env_var "UCAN_STORE_SERVICE_DID" ""
326
+ write_caddyfile "${PROXY_HOSTNAME}" "${SERVICE_HOSTNAME}"
228
327
  fi
229
328
 
230
329
  touch "${READY_FILE}"
@@ -236,26 +335,26 @@ if [ "${START_SERVICE}" = "1" ]; then
236
335
  systemctl restart "${CADDY_SERVICE}"
237
336
  fi
238
337
 
239
- SERVICE_DID="$(probe_service_did)"
240
- write_env_var "UCAN_STORE_SERVICE_DID" "${SERVICE_DID}"
338
+ PROBED_SERVICE_DID="$(probe_service_did)"
339
+ if [ -z "${SERVICE_DID}" ]; then
340
+ SERVICE_DID="${PROBED_SERVICE_DID}"
341
+ write_env_var "UCAN_STORE_SERVICE_DID" "${SERVICE_DID}"
342
+ fi
241
343
  write_env_var "PUBLIC_UPLOAD_SERVICE_DID" "${SERVICE_DID}"
242
344
  write_env_var "PUBLIC_REVOCATION_DID" "${SERVICE_DID}"
243
345
 
244
346
  if [ -f "${BOOTSTRAP_PACKAGE_FILE}" ]; then
245
- RUNTIME_SERVICE_ORIGIN=""
246
- if [ -n "${PROXY_HOSTNAME}" ]; then
247
- RUNTIME_SERVICE_ORIGIN="https://${PROXY_HOSTNAME}"
248
- fi
249
347
  python3 "${BOOTSTRAP_VALIDATOR}" \
250
348
  --package-file "${BOOTSTRAP_PACKAGE_FILE}" \
251
349
  --runtime-service-did "${SERVICE_DID}" \
252
- --runtime-service-origin "${RUNTIME_SERVICE_ORIGIN}" \
350
+ --runtime-service-origin "${SERVICE_ORIGIN}" \
253
351
  --admin-did "${ADMIN_DID}" >/dev/null
254
352
  node "${BOOTSTRAP_CRYPTO_VERIFIER}" \
255
353
  --package-file "${BOOTSTRAP_PACKAGE_FILE}" \
256
354
  --runtime-service-did "${SERVICE_DID}" \
257
- --runtime-service-origin "${RUNTIME_SERVICE_ORIGIN}" \
355
+ --runtime-service-origin "${SERVICE_ORIGIN}" \
258
356
  --admin-did "${ADMIN_DID}" \
259
357
  --summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null
358
+ install_bootstrap_verification_permissions
260
359
  fi
261
360
  fi
@@ -23,6 +23,7 @@ const DEFAULT_REVOCATION_URL =
23
23
  process.env.PUBLIC_REVOCATION_URL || '';
24
24
  const DEFAULT_RECEIPTS_URL =
25
25
  process.env.PUBLIC_RECEIPTS_URL || '';
26
+ const PROTOCOL_CAPABILITIES = new Set(['ucan/conclude', 'filecoin/offer']);
26
27
 
27
28
  function printHelp() {
28
29
  console.log(`Usage:
@@ -144,6 +145,10 @@ function capabilityCovers(provided, required) {
144
145
  return false;
145
146
  }
146
147
 
148
+ function isProtocolCapability(capability) {
149
+ return PROTOCOL_CAPABILITIES.has(capability);
150
+ }
151
+
147
152
  function isTruthy(value) {
148
153
  return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
149
154
  }
@@ -511,8 +516,8 @@ async function proxyRequest(req, res, config, bodyBuffer = null) {
511
516
  res.setHeader(key, value);
512
517
  }
513
518
  res.setHeader('Access-Control-Allow-Origin', '*');
514
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
515
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
519
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
520
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Amz-Checksum-Sha256');
516
521
  const arrayBuffer = await response.arrayBuffer();
517
522
  res.end(Buffer.from(arrayBuffer));
518
523
  }
@@ -523,8 +528,8 @@ function sendJson(res, status, payload) {
523
528
  res.setHeader('Content-Type', 'application/json');
524
529
  res.setHeader('Content-Length', String(body.length));
525
530
  res.setHeader('Access-Control-Allow-Origin', '*');
526
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
527
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
531
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
532
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Amz-Checksum-Sha256');
528
533
  res.end(body);
529
534
  }
530
535
 
@@ -549,6 +554,14 @@ async function enforceRequestPolicy(bodyBuffer, req, config) {
549
554
  });
550
555
 
551
556
  for (const invocation of message.invocations ?? []) {
557
+ const capabilities = invocation.capabilities ?? [];
558
+ const isProtocolInvocation =
559
+ capabilities.length > 0 &&
560
+ capabilities.every((capability) => isProtocolCapability(normalizeString(capability?.can)));
561
+ if (isProtocolInvocation) {
562
+ continue;
563
+ }
564
+
552
565
  const proofCids = collectProofCids(invocation.proofs ?? []);
553
566
  if (config.policy.rootDelegationCid && !proofCids.has(config.policy.rootDelegationCid)) {
554
567
  return {
@@ -558,9 +571,12 @@ async function enforceRequestPolicy(bodyBuffer, req, config) {
558
571
  };
559
572
  }
560
573
 
561
- for (const capability of invocation.capabilities ?? []) {
574
+ for (const capability of capabilities) {
562
575
  const withValue = normalizeString(capability?.with);
563
576
  const canValue = normalizeString(capability?.can);
577
+ if (isProtocolCapability(canValue)) {
578
+ continue;
579
+ }
564
580
  if (!withValue || !canValue) {
565
581
  return {
566
582
  ok: false,
@@ -639,8 +655,8 @@ async function main() {
639
655
  if (req.method === 'OPTIONS') {
640
656
  res.statusCode = 204;
641
657
  res.setHeader('Access-Control-Allow-Origin', '*');
642
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
643
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
658
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
659
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Amz-Checksum-Sha256');
644
660
  res.end();
645
661
  return;
646
662
  }
@@ -137,7 +137,11 @@ class Handler(BaseHTTPRequestHandler):
137
137
  proxy_candidate if "://" in proxy_candidate else f"https://{proxy_candidate}"
138
138
  ) if proxy_candidate else None
139
139
  proxy_hostname = parsed_proxy.hostname if parsed_proxy and parsed_proxy.hostname else proxy_candidate
140
+ service_did = str(payload.get("service_did") or "").strip()
141
+ service_origin = str(payload.get("service_origin") or "").strip()
142
+ public_storage_origin = str(payload.get("public_storage_origin") or "").strip()
140
143
  admin_did = str(payload.get("admin_did") or "").strip()
144
+ admin_api_token = str(payload.get("admin_api_token") or "").strip()
141
145
  webauthn_origin = str(payload.get("webauthn_origin") or "").strip()
142
146
  webauthn_origin_fallbacks = str(payload.get("webauthn_origin_fallbacks") or "").strip()
143
147
  no_start = bool(payload.get("no_start"))
@@ -163,8 +167,16 @@ class Handler(BaseHTTPRequestHandler):
163
167
  args.extend(["--public-ipv6", public_ipv6])
164
168
  if proxy_hostname:
165
169
  args.extend(["--proxy-hostname", proxy_hostname])
170
+ if service_did:
171
+ args.extend(["--service-did", service_did])
172
+ if service_origin:
173
+ args.extend(["--service-origin", service_origin])
174
+ if public_storage_origin:
175
+ args.extend(["--public-storage-origin", public_storage_origin])
166
176
  if admin_did:
167
177
  args.extend(["--admin-did", admin_did])
178
+ if admin_api_token:
179
+ args.extend(["--admin-api-token", admin_api_token])
168
180
  if webauthn_origin:
169
181
  args.extend(["--webauthn-origin", webauthn_origin])
170
182
  if webauthn_origin_fallbacks: