@le-space/rootfs 0.6.4 → 0.6.6

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.4",
3
+ "version": "0.6.6",
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,49 @@ 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 browser_multiaddr_transport(addr: str) -> str:
137
+ normalized = addr.lower()
138
+ if "/webtransport/" in normalized:
139
+ return "webtransport"
140
+ if "/webrtc-direct/" in normalized:
141
+ return "webrtc"
142
+ if "/ws" in normalized or "/wss" in normalized:
143
+ return "websocket"
144
+ return "other"
145
+
146
+
147
+ def select_compact_multiaddrs(values: list[str]) -> list[str]:
148
+ public_browser_addrs = filter_public_multiaddrs(values, browser_only=True)
149
+ per_transport_limit = max(1, COMPACT_MULTIADDR_LIMIT)
150
+ selected_by_transport: dict[str, int] = {}
151
+ result: list[str] = []
152
+
153
+ for addr in sorted(public_browser_addrs, key=browser_multiaddr_preference):
154
+ transport = browser_multiaddr_transport(addr)
155
+ selected = selected_by_transport.get(transport, 0)
156
+ if selected >= per_transport_limit:
157
+ continue
158
+ selected_by_transport[transport] = selected + 1
159
+ result.append(addr)
160
+
161
+ return result
162
+
163
+
115
164
  def json_dumps(payload: object) -> str:
116
165
  return json.dumps(payload, separators=(",", ":"))
117
166
 
@@ -339,60 +388,82 @@ def parse_post_record(entry: object) -> dict[str, object] | None:
339
388
  except json.JSONDecodeError:
340
389
  item_content = None
341
390
 
342
- if not isinstance(item_content, dict):
343
- return None
344
-
345
- content = item_content.get("content")
391
+ content_source = item_content if isinstance(item_content, dict) else entry
392
+ content = content_source.get("content")
346
393
  if not isinstance(content, dict):
347
394
  return None
348
395
 
349
396
  return {
350
397
  "item_hash": item_hash,
351
398
  "sender": sender,
399
+ "publisher_address": content.get("publisherAddress"),
352
400
  "registration_id": content.get("registrationId"),
401
+ "peer_id": content.get("peerId"),
402
+ "profile": content.get("profile"),
403
+ "updated_at": content.get("updatedAt"),
353
404
  }
354
405
 
355
406
 
407
+ def normalize_address(value: object) -> str:
408
+ return str(value or "").strip().lower()
409
+
410
+
411
+ def timestamp_ms(value: object) -> int | None:
412
+ if isinstance(value, bool):
413
+ return None
414
+ if isinstance(value, (int, float)):
415
+ return int(value)
416
+ if isinstance(value, str):
417
+ stripped = value.strip()
418
+ if stripped.isdigit():
419
+ return int(stripped)
420
+ return None
421
+
422
+
356
423
  def fetch_previous_hashes(
357
424
  api_host: str,
358
425
  channel: str,
359
426
  ref: str,
360
- post_type: str,
427
+ post_types: list[str],
361
428
  sender: str,
362
- registration_id: str | None,
363
429
  current_item_hash: str,
430
+ now_ms: int,
364
431
  ) -> list[str]:
365
- if not registration_id:
366
- return []
367
-
432
+ stale_cutoff_ms = now_ms - (STALE_RECORD_MAX_AGE_SECONDS * 1000)
433
+ normalized_sender = normalize_address(sender)
368
434
  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
435
+ for post_type in post_types:
436
+ for page in range(1, MAX_PREVIOUS_PAGES + 1):
437
+ url = (
438
+ f"{api_host.rstrip('/')}/api/v0/posts.json?"
439
+ f"channels={urllib.parse.quote(channel)}&"
440
+ f"refs={urllib.parse.quote(ref)}&"
441
+ f"types={urllib.parse.quote(post_type)}&"
442
+ f"pagination={PAGINATION}&page={page}"
443
+ )
444
+ payload = get_json(url)
445
+ posts = payload.get("posts")
446
+ if not isinstance(posts, list):
447
+ break
448
+
449
+ for entry in posts:
450
+ parsed = parse_post_record(entry)
451
+ if parsed is None:
452
+ continue
453
+ same_publisher = normalized_sender in {
454
+ normalize_address(parsed["sender"]),
455
+ normalize_address(parsed["publisher_address"]),
456
+ }
457
+ if not same_publisher:
458
+ continue
459
+ if parsed["item_hash"] == current_item_hash:
460
+ continue
461
+ updated_at = timestamp_ms(parsed["updated_at"])
462
+ if updated_at is not None and updated_at < stale_cutoff_ms:
463
+ found.append(str(parsed["item_hash"]))
464
+
465
+ if len(posts) < PAGINATION:
466
+ break
396
467
 
397
468
  return dedupe(found)
398
469
 
@@ -448,9 +519,6 @@ def main() -> None:
448
519
  browser_multiaddrs = filter_public_multiaddrs(
449
520
  metadata.get("browser_bootstrap_multiaddrs") or [], browser_only=True
450
521
  )
451
- if not multiaddrs:
452
- print(json_dumps({"status": "skipped", "reason": "no public multiaddrs"}))
453
- return
454
522
 
455
523
  publisher_address = address_from_private_key(publisher_private_key)
456
524
  now_ms = int(time.time() * 1000)
@@ -461,6 +529,17 @@ def main() -> None:
461
529
  ref = env_values.get("ALEPH_BOOTSTRAP_REF", DEFAULT_REF).strip() or DEFAULT_REF
462
530
  post_type = env_values.get("ALEPH_BOOTSTRAP_POST_TYPE", DEFAULT_POST_TYPE).strip() or DEFAULT_POST_TYPE
463
531
  api_host = env_values.get("ALEPH_BOOTSTRAP_API_HOST", DEFAULT_API_HOST).strip() or DEFAULT_API_HOST
532
+ compact = post_type == COMPACT_POST_TYPE
533
+ published_multiaddrs = (
534
+ select_compact_multiaddrs(browser_multiaddrs or multiaddrs)
535
+ if compact
536
+ else multiaddrs
537
+ )
538
+ published_browser_multiaddrs = [] if compact else browser_multiaddrs
539
+ if not published_multiaddrs:
540
+ reason = "no public browser multiaddrs" if compact else "no public multiaddrs"
541
+ print(json_dumps({"status": "skipped", "reason": reason}))
542
+ return
464
543
 
465
544
  owner_authorization = load_owner_authorization(
466
545
  env_values,
@@ -477,8 +556,8 @@ def main() -> None:
477
556
 
478
557
  proof_payload = relay_proof_payload(
479
558
  peer_id,
480
- multiaddrs,
481
- browser_multiaddrs,
559
+ published_multiaddrs,
560
+ published_browser_multiaddrs,
482
561
  registration_id,
483
562
  profile,
484
563
  version,
@@ -492,8 +571,8 @@ def main() -> None:
492
571
  post_content = build_post_content(
493
572
  publisher_address,
494
573
  peer_id,
495
- multiaddrs,
496
- browser_multiaddrs,
574
+ published_multiaddrs,
575
+ published_browser_multiaddrs,
497
576
  registration_id,
498
577
  profile,
499
578
  version,
@@ -514,10 +593,10 @@ def main() -> None:
514
593
  api_host,
515
594
  channel,
516
595
  ref,
517
- post_type,
596
+ [post_type, LEGACY_POST_TYPE] if compact else [post_type],
518
597
  publisher_address,
519
- registration_id,
520
598
  str(unsigned_message["item_hash"]),
599
+ now_ms,
521
600
  )
522
601
  forget_response = broadcast_forget(
523
602
  api_host, publisher_address, publisher_private_key, previous_hashes, channel
@@ -531,9 +610,10 @@ def main() -> None:
531
610
  "itemHash": unsigned_message["item_hash"],
532
611
  "sender": publisher_address,
533
612
  "peerId": peer_id,
534
- "publishedMultiaddrs": multiaddrs,
535
- "publishedBrowserMultiaddrs": browser_multiaddrs,
613
+ "publishedMultiaddrs": published_multiaddrs,
614
+ "publishedBrowserMultiaddrs": published_browser_multiaddrs or published_multiaddrs,
536
615
  "forgottenHashes": previous_hashes,
616
+ "forgetStaleOlderThanSeconds": STALE_RECORD_MAX_AGE_SECONDS,
537
617
  "forgetResponse": forget_response[1] if forget_response else None,
538
618
  "response": response,
539
619
  }
@@ -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,
@@ -16,8 +16,10 @@ SERVICE_GROUP="${SERVICE_GROUP:-ucan-store}"
16
16
  PUBLIC_IPV4=""
17
17
  PUBLIC_IPV6=""
18
18
  PROXY_HOSTNAME=""
19
+ SERVICE_HOSTNAME=""
19
20
  SERVICE_DID=""
20
21
  SERVICE_ORIGIN=""
22
+ PUBLIC_STORAGE_ORIGIN=""
21
23
  WEBAUTHN_ORIGIN=""
22
24
  WEBAUTHN_ORIGIN_FALLBACKS=""
23
25
  ADMIN_DID=""
@@ -34,6 +36,7 @@ Usage:
34
36
  [--proxy-hostname <hostname>] \
35
37
  [--service-did <did>] \
36
38
  [--service-origin <origin>] \
39
+ [--public-storage-origin <origin>] \
37
40
  [--webauthn-origin <origin>] \
38
41
  [--webauthn-origin-fallbacks <csv>] \
39
42
  [--admin-did <did>] \
@@ -72,19 +75,67 @@ PY
72
75
  }
73
76
 
74
77
  write_caddyfile() {
75
- 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
76
109
  mkdir -p "$(dirname "${CADDYFILE}")"
77
110
  cat > "${CADDYFILE}" <<EOF
78
111
  {
79
112
  auto_https disable_redirects
80
113
  }
81
114
 
82
- ${hostname} {
115
+ ${site_label} {
83
116
  reverse_proxy 127.0.0.1:${PROXY_PORT}
84
117
  }
85
118
  EOF
86
119
  }
87
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
+
88
139
  derive_service_did_from_hostname() {
89
140
  python3 - "${1:-}" <<'PY'
90
141
  import sys
@@ -148,7 +199,15 @@ install_bootstrap_package() {
148
199
  else
149
200
  chmod 0644 "${BOOTSTRAP_PACKAGE_FILE}"
150
201
  fi
151
- chmod 0644 "${BOOTSTRAP_VERIFICATION_FILE}"
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
152
211
  }
153
212
 
154
213
  while [ "$#" -gt 0 ]; do
@@ -173,6 +232,10 @@ while [ "$#" -gt 0 ]; do
173
232
  SERVICE_ORIGIN="${2:-}"
174
233
  shift 2
175
234
  ;;
235
+ --public-storage-origin)
236
+ PUBLIC_STORAGE_ORIGIN="${2:-}"
237
+ shift 2
238
+ ;;
176
239
  --webauthn-origin)
177
240
  WEBAUTHN_ORIGIN="${2:-}"
178
241
  shift 2
@@ -214,6 +277,7 @@ if [ -z "${PUBLIC_IPV4}" ]; then
214
277
  exit 1
215
278
  fi
216
279
  SERVICE_ORIGIN="${SERVICE_ORIGIN%/}"
280
+ PUBLIC_STORAGE_ORIGIN="${PUBLIC_STORAGE_ORIGIN%/}"
217
281
 
218
282
  touch "${ENV_FILE}"
219
283
 
@@ -242,17 +306,24 @@ fi
242
306
  if [ -z "${SERVICE_ORIGIN}" ] && [ -n "${PROXY_HOSTNAME}" ]; then
243
307
  SERVICE_ORIGIN="https://${PROXY_HOSTNAME}"
244
308
  fi
309
+ SERVICE_HOSTNAME="$(derive_hostname_from_origin "${SERVICE_ORIGIN}")"
245
310
 
246
311
  write_env_var "UCAN_STORE_SERVICE_DID" "${SERVICE_DID}"
312
+ write_env_var "UCAN_STORE_SERVICE_HOSTNAME" "${SERVICE_HOSTNAME}"
247
313
 
248
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}"
249
320
  write_env_var "PUBLIC_UPLOAD_SERVICE_URL" "${SERVICE_ORIGIN}"
250
321
  write_env_var "PUBLIC_REVOCATION_URL" "${SERVICE_ORIGIN}"
251
322
  write_env_var "PUBLIC_RECEIPTS_URL" "${SERVICE_ORIGIN}/receipt/"
252
323
  fi
253
324
 
254
325
  if [ -n "${PROXY_HOSTNAME}" ]; then
255
- write_caddyfile "${PROXY_HOSTNAME}"
326
+ write_caddyfile "${PROXY_HOSTNAME}" "${SERVICE_HOSTNAME}"
256
327
  fi
257
328
 
258
329
  touch "${READY_FILE}"
@@ -284,5 +355,6 @@ if [ "${START_SERVICE}" = "1" ]; then
284
355
  --runtime-service-origin "${SERVICE_ORIGIN}" \
285
356
  --admin-did "${ADMIN_DID}" \
286
357
  --summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null
358
+ install_bootstrap_verification_permissions
287
359
  fi
288
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
  }
@@ -139,7 +139,9 @@ class Handler(BaseHTTPRequestHandler):
139
139
  proxy_hostname = parsed_proxy.hostname if parsed_proxy and parsed_proxy.hostname else proxy_candidate
140
140
  service_did = str(payload.get("service_did") or "").strip()
141
141
  service_origin = str(payload.get("service_origin") or "").strip()
142
+ public_storage_origin = str(payload.get("public_storage_origin") or "").strip()
142
143
  admin_did = str(payload.get("admin_did") or "").strip()
144
+ admin_api_token = str(payload.get("admin_api_token") or "").strip()
143
145
  webauthn_origin = str(payload.get("webauthn_origin") or "").strip()
144
146
  webauthn_origin_fallbacks = str(payload.get("webauthn_origin_fallbacks") or "").strip()
145
147
  no_start = bool(payload.get("no_start"))
@@ -169,8 +171,12 @@ class Handler(BaseHTTPRequestHandler):
169
171
  args.extend(["--service-did", service_did])
170
172
  if service_origin:
171
173
  args.extend(["--service-origin", service_origin])
174
+ if public_storage_origin:
175
+ args.extend(["--public-storage-origin", public_storage_origin])
172
176
  if admin_did:
173
177
  args.extend(["--admin-did", admin_did])
178
+ if admin_api_token:
179
+ args.extend(["--admin-api-token", admin_api_token])
174
180
  if webauthn_origin:
175
181
  args.extend(["--webauthn-origin", webauthn_origin])
176
182
  if webauthn_origin_fallbacks: