@le-space/rootfs 0.6.4 → 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.4",
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,
@@ -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: