@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 +1 -1
- package/reference/orbitdb-relay/rootfs/orbitdb-relay-bootstrap-refresh.py +107 -48
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap-refresh.py +107 -48
- package/reference/ucan-store/rootfs/ucan-store-bootstrap.sh +3 -0
- package/reference/ucan-store/rootfs/ucan-store-configure.sh +118 -19
- package/reference/ucan-store/rootfs/ucan-store-request-guard.mjs +23 -7
- package/reference/ucan-store/rootfs/ucan-store-setup-server.py +12 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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":
|
|
535
|
-
"publishedBrowserMultiaddrs":
|
|
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
|
-
|
|
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
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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":
|
|
609
|
-
"publishedBrowserMultiaddrs":
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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 "${
|
|
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 "${
|
|
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
|
|
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:
|