@le-space/rootfs 0.6.1 → 0.6.2

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.
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ENV_FILE="${ENV_FILE:-/etc/default/ucan-store}"
5
+ READY_FILE="${READY_FILE:-/etc/default/ucan-store.ready}"
6
+ BOOTSTRAP_PACKAGE_FILE="${BOOTSTRAP_PACKAGE_FILE:-/etc/ucan-store/bootstrap-package.json}"
7
+ BOOTSTRAP_VALIDATOR="${BOOTSTRAP_VALIDATOR:-/usr/local/sbin/ucan_store_bootstrap_validate.py}"
8
+ BOOTSTRAP_CRYPTO_VERIFIER="${BOOTSTRAP_CRYPTO_VERIFIER:-/usr/local/sbin/ucan-store-bootstrap-verify.mjs}"
9
+ BOOTSTRAP_VERIFICATION_FILE="${BOOTSTRAP_VERIFICATION_FILE:-/etc/ucan-store/bootstrap-verification.json}"
10
+ SERVICE_NAME="${SERVICE_NAME:-ucan-store.service}"
11
+ CADDY_SERVICE="${CADDY_SERVICE:-caddy.service}"
12
+ CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}"
13
+ SERVICE_PORT="${SERVICE_PORT:-8787}"
14
+ PROXY_PORT="${PROXY_PORT:-8788}"
15
+ PUBLIC_IPV4=""
16
+ PUBLIC_IPV6=""
17
+ PROXY_HOSTNAME=""
18
+ WEBAUTHN_ORIGIN=""
19
+ WEBAUTHN_ORIGIN_FALLBACKS=""
20
+ ADMIN_DID=""
21
+ ADMIN_API_TOKEN=""
22
+ BOOTSTRAP_PACKAGE_INPUT_FILE=""
23
+ START_SERVICE=1
24
+
25
+ usage() {
26
+ cat <<'EOF'
27
+ Usage:
28
+ ucan-store-configure.sh \
29
+ --public-ipv4 <ip> \
30
+ [--public-ipv6 <ipv6>] \
31
+ [--proxy-hostname <hostname>] \
32
+ [--webauthn-origin <origin>] \
33
+ [--webauthn-origin-fallbacks <csv>] \
34
+ [--admin-did <did>] \
35
+ [--admin-api-token <token>] \
36
+ [--bootstrap-package-file <path>] \
37
+ [--no-start]
38
+
39
+ Writes the public service wiring for the current ucan-store deployment,
40
+ enables the guest service, and stores the resulting PWA-facing `VITE_*` values.
41
+ EOF
42
+ }
43
+
44
+ write_env_var() {
45
+ local key="$1"
46
+ local value="$2"
47
+
48
+ python3 - "${ENV_FILE}" "${key}" "${value}" <<'PY'
49
+ import re
50
+ import sys
51
+ from pathlib import Path
52
+
53
+ env_file, key, value = sys.argv[1:4]
54
+ target = Path(env_file)
55
+ pattern = re.compile(rf"^[#\s]*{re.escape(key)}=.*$", re.MULTILINE)
56
+
57
+ raw = target.read_text(encoding="utf-8") if target.exists() else ""
58
+ replacement = f"{key}={value}"
59
+
60
+ if pattern.search(raw):
61
+ updated = pattern.sub(replacement, raw, count=1)
62
+ else:
63
+ updated = raw + ("" if raw.endswith("\n") or not raw else "\n") + replacement + "\n"
64
+
65
+ target.write_text(updated, encoding="utf-8")
66
+ PY
67
+ }
68
+
69
+ write_caddyfile() {
70
+ local hostname="$1"
71
+ mkdir -p "$(dirname "${CADDYFILE}")"
72
+ cat > "${CADDYFILE}" <<EOF
73
+ {
74
+ auto_https disable_redirects
75
+ }
76
+
77
+ ${hostname} {
78
+ reverse_proxy 127.0.0.1:${PROXY_PORT}
79
+ }
80
+ EOF
81
+ }
82
+
83
+ derive_service_did_from_hostname() {
84
+ python3 - "${1:-}" <<'PY'
85
+ import sys
86
+ from urllib.parse import urlsplit
87
+
88
+ raw = (sys.argv[1] or "").strip()
89
+ if not raw:
90
+ raise SystemExit(0)
91
+
92
+ parsed = urlsplit(raw if "://" in raw else f"https://{raw}")
93
+ host = (parsed.hostname or "").strip().lower()
94
+ if not host:
95
+ raise SystemExit(0)
96
+
97
+ port = f"%3A{parsed.port}" if parsed.port else ""
98
+ print(f"did:web:{host}{port}")
99
+ PY
100
+ }
101
+
102
+ probe_service_did() {
103
+ python3 - "${SERVICE_PORT}" <<'PY'
104
+ import json
105
+ import sys
106
+ import time
107
+ import urllib.error
108
+ import urllib.request
109
+
110
+ port = int(sys.argv[1])
111
+ url = f"http://127.0.0.1:{port}/.well-known/did.json"
112
+ deadline = time.time() + 60
113
+ last_error = None
114
+
115
+ while time.time() < deadline:
116
+ try:
117
+ with urllib.request.urlopen(url, timeout=5) as response:
118
+ payload = json.loads(response.read().decode("utf-8"))
119
+ did = payload.get("id")
120
+ if isinstance(did, str) and did.strip():
121
+ print(did.strip())
122
+ raise SystemExit(0)
123
+ last_error = "did.json missing id"
124
+ except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:
125
+ last_error = str(error)
126
+ time.sleep(2)
127
+
128
+ raise SystemExit(last_error or "service DID probe timed out")
129
+ PY
130
+ }
131
+
132
+ install_bootstrap_package() {
133
+ local source_file="$1"
134
+ mkdir -p "$(dirname "${BOOTSTRAP_PACKAGE_FILE}")"
135
+ python3 "${BOOTSTRAP_VALIDATOR}" --package-file "${source_file}" >/dev/null
136
+ node "${BOOTSTRAP_CRYPTO_VERIFIER}" \
137
+ --package-file "${source_file}" \
138
+ --admin-did "${ADMIN_DID}" \
139
+ --summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null
140
+ cp "${source_file}" "${BOOTSTRAP_PACKAGE_FILE}"
141
+ chmod 0640 "${BOOTSTRAP_PACKAGE_FILE}"
142
+ chmod 0644 "${BOOTSTRAP_VERIFICATION_FILE}"
143
+ }
144
+
145
+ while [ "$#" -gt 0 ]; do
146
+ case "$1" in
147
+ --public-ipv4)
148
+ PUBLIC_IPV4="${2:-}"
149
+ shift 2
150
+ ;;
151
+ --public-ipv6)
152
+ PUBLIC_IPV6="${2:-}"
153
+ shift 2
154
+ ;;
155
+ --proxy-hostname)
156
+ PROXY_HOSTNAME="${2:-}"
157
+ shift 2
158
+ ;;
159
+ --webauthn-origin)
160
+ WEBAUTHN_ORIGIN="${2:-}"
161
+ shift 2
162
+ ;;
163
+ --webauthn-origin-fallbacks)
164
+ WEBAUTHN_ORIGIN_FALLBACKS="${2:-}"
165
+ shift 2
166
+ ;;
167
+ --admin-did)
168
+ ADMIN_DID="${2:-}"
169
+ shift 2
170
+ ;;
171
+ --admin-api-token)
172
+ ADMIN_API_TOKEN="${2:-}"
173
+ shift 2
174
+ ;;
175
+ --bootstrap-package-file)
176
+ BOOTSTRAP_PACKAGE_INPUT_FILE="${2:-}"
177
+ shift 2
178
+ ;;
179
+ --no-start)
180
+ START_SERVICE=0
181
+ shift
182
+ ;;
183
+ -h|--help)
184
+ usage
185
+ exit 0
186
+ ;;
187
+ *)
188
+ echo "Unknown argument: $1" >&2
189
+ usage >&2
190
+ exit 1
191
+ ;;
192
+ esac
193
+ done
194
+
195
+ if [ -z "${PUBLIC_IPV4}" ]; then
196
+ usage >&2
197
+ exit 1
198
+ fi
199
+
200
+ touch "${ENV_FILE}"
201
+
202
+ write_env_var "PUBLIC_IPV4" "${PUBLIC_IPV4}"
203
+ write_env_var "PUBLIC_IPV6" "${PUBLIC_IPV6}"
204
+ write_env_var "PROXY_HOSTNAME" "${PROXY_HOSTNAME}"
205
+ write_env_var "UCAN_STORE_SERVICE_KEY_ALGORITHM" "${UCAN_STORE_SERVICE_KEY_ALGORITHM:-ed25519}"
206
+ write_env_var "UCAN_STORE_ADMIN_DID" "${ADMIN_DID}"
207
+ write_env_var "UCAN_STORE_ADMIN_API_TOKEN" "${ADMIN_API_TOKEN}"
208
+ write_env_var "UCAN_STORE_BOOTSTRAP_PACKAGE_FILE" "${BOOTSTRAP_PACKAGE_FILE}"
209
+ write_env_var "UCAN_STORE_BOOTSTRAP_VERIFICATION_FILE" "${BOOTSTRAP_VERIFICATION_FILE}"
210
+
211
+ if [ -n "${BOOTSTRAP_PACKAGE_INPUT_FILE}" ]; then
212
+ install_bootstrap_package "${BOOTSTRAP_PACKAGE_INPUT_FILE}"
213
+ fi
214
+
215
+ if [ -n "${WEBAUTHN_ORIGIN}" ]; then
216
+ write_env_var "WEBAUTHN_ORIGIN" "${WEBAUTHN_ORIGIN}"
217
+ fi
218
+ write_env_var "WEBAUTHN_ORIGIN_FALLBACKS" "${WEBAUTHN_ORIGIN_FALLBACKS}"
219
+
220
+ 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" ""
228
+ fi
229
+
230
+ touch "${READY_FILE}"
231
+
232
+ if [ "${START_SERVICE}" = "1" ]; then
233
+ systemctl daemon-reload
234
+ systemctl restart "${SERVICE_NAME}"
235
+ if [ -n "${PROXY_HOSTNAME}" ]; then
236
+ systemctl restart "${CADDY_SERVICE}"
237
+ fi
238
+
239
+ SERVICE_DID="$(probe_service_did)"
240
+ write_env_var "UCAN_STORE_SERVICE_DID" "${SERVICE_DID}"
241
+ write_env_var "PUBLIC_UPLOAD_SERVICE_DID" "${SERVICE_DID}"
242
+ write_env_var "PUBLIC_REVOCATION_DID" "${SERVICE_DID}"
243
+
244
+ 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
+ python3 "${BOOTSTRAP_VALIDATOR}" \
250
+ --package-file "${BOOTSTRAP_PACKAGE_FILE}" \
251
+ --runtime-service-did "${SERVICE_DID}" \
252
+ --runtime-service-origin "${RUNTIME_SERVICE_ORIGIN}" \
253
+ --admin-did "${ADMIN_DID}" >/dev/null
254
+ node "${BOOTSTRAP_CRYPTO_VERIFIER}" \
255
+ --package-file "${BOOTSTRAP_PACKAGE_FILE}" \
256
+ --runtime-service-did "${SERVICE_DID}" \
257
+ --runtime-service-origin "${RUNTIME_SERVICE_ORIGIN}" \
258
+ --admin-did "${ADMIN_DID}" \
259
+ --summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null
260
+ fi
261
+ fi
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import time
7
+ import urllib.error
8
+ import urllib.request
9
+
10
+ from ucan_store_bootstrap_validate import summarize_bootstrap_package
11
+
12
+
13
+ ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/ucan-store")
14
+ SERVICE_PORT = int(os.environ.get("STORACHA_LOCAL_PORT", "8787"))
15
+ WAIT_TIMEOUT_SECONDS = int(os.environ.get("DESCRIBE_WAIT_TIMEOUT_SECONDS", "120"))
16
+ WAIT_INTERVAL_SECONDS = float(os.environ.get("DESCRIBE_WAIT_INTERVAL_SECONDS", "2"))
17
+ BOOTSTRAP_PACKAGE_FILE = os.environ.get(
18
+ "BOOTSTRAP_PACKAGE_FILE",
19
+ "/etc/ucan-store/bootstrap-package.json",
20
+ )
21
+ BOOTSTRAP_VERIFICATION_FILE = os.environ.get(
22
+ "BOOTSTRAP_VERIFICATION_FILE",
23
+ "/etc/ucan-store/bootstrap-verification.json",
24
+ )
25
+
26
+
27
+ def parse_env_file(path: str) -> dict[str, str]:
28
+ values: dict[str, str] = {}
29
+ if not os.path.exists(path):
30
+ return values
31
+
32
+ with open(path, encoding="utf-8") as handle:
33
+ for line in handle:
34
+ stripped = line.strip()
35
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
36
+ continue
37
+ key, value = stripped.split("=", 1)
38
+ values[key.strip()] = value.strip()
39
+ return values
40
+
41
+
42
+ def fetch_service_did(port: int) -> str:
43
+ url = f"http://127.0.0.1:{port}/.well-known/did.json"
44
+ deadline = time.monotonic() + WAIT_TIMEOUT_SECONDS
45
+ last_error = None
46
+
47
+ while time.monotonic() < deadline:
48
+ try:
49
+ with urllib.request.urlopen(url, timeout=5) as response:
50
+ payload = json.loads(response.read().decode("utf-8"))
51
+ did = payload.get("id")
52
+ if isinstance(did, str) and did.strip():
53
+ return did.strip()
54
+ last_error = "did.json missing id"
55
+ except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:
56
+ last_error = str(error)
57
+ time.sleep(WAIT_INTERVAL_SECONDS)
58
+
59
+ raise SystemExit(last_error or "unable to discover upload service DID")
60
+
61
+
62
+ def read_json_file(path: str) -> dict[str, object] | None:
63
+ if not path or not os.path.exists(path):
64
+ return None
65
+ with open(path, encoding="utf-8") as handle:
66
+ payload = json.load(handle)
67
+ return payload if isinstance(payload, dict) else None
68
+
69
+
70
+ def main() -> None:
71
+ env_values = parse_env_file(ENV_FILE)
72
+ service_did = env_values.get("PUBLIC_UPLOAD_SERVICE_DID", "").strip() or fetch_service_did(SERVICE_PORT)
73
+ upload_url = env_values.get("PUBLIC_UPLOAD_SERVICE_URL", "").strip()
74
+ revocation_url = env_values.get("PUBLIC_REVOCATION_URL", "").strip() or upload_url
75
+ receipts_url = env_values.get("PUBLIC_RECEIPTS_URL", "").strip() or (upload_url.rstrip("/") + "/receipt/" if upload_url else "")
76
+ bootstrap_package_file = (
77
+ env_values.get("UCAN_STORE_BOOTSTRAP_PACKAGE_FILE", "").strip()
78
+ or BOOTSTRAP_PACKAGE_FILE
79
+ )
80
+ bootstrap_verification_file = (
81
+ env_values.get("UCAN_STORE_BOOTSTRAP_VERIFICATION_FILE", "").strip()
82
+ or BOOTSTRAP_VERIFICATION_FILE
83
+ )
84
+ bootstrap_validation = summarize_bootstrap_package(
85
+ bootstrap_package_file,
86
+ runtime_service_did=service_did,
87
+ runtime_service_origin=upload_url or None,
88
+ admin_did=env_values.get("UCAN_STORE_ADMIN_DID", "").strip() or None,
89
+ allow_missing=True,
90
+ )
91
+ bootstrap_proof_validation = read_json_file(bootstrap_verification_file)
92
+ if bootstrap_validation.get("status") == "invalid":
93
+ raise SystemExit(
94
+ "bootstrap package validation failed: "
95
+ + "; ".join(bootstrap_validation.get("errors", [])),
96
+ )
97
+ if bootstrap_proof_validation and bootstrap_proof_validation.get("status") == "invalid":
98
+ raise SystemExit(
99
+ "bootstrap proof verification failed: "
100
+ + "; ".join(bootstrap_proof_validation.get("errors", [])),
101
+ )
102
+
103
+ payload = {
104
+ "service_did": service_did,
105
+ "service_identity": {
106
+ "did": service_did,
107
+ "configured_did": env_values.get("UCAN_STORE_SERVICE_DID", "").strip() or None,
108
+ "key_algorithm": env_values.get(
109
+ "UCAN_STORE_SERVICE_KEY_ALGORITHM",
110
+ "",
111
+ ).strip()
112
+ or None,
113
+ },
114
+ "delegation_issuance": {
115
+ "enabled": bool(env_values.get("UCAN_STORE_ADMIN_API_TOKEN", "").strip()),
116
+ "policy_endpoint": f"{upload_url}/admin/delegations/policy" if upload_url else None,
117
+ "issue_endpoint": f"{upload_url}/admin/delegations" if upload_url else None,
118
+ "proof_format": "ucan-car-multibase-base64",
119
+ },
120
+ "service_manifest": {
121
+ "endpoint": f"{upload_url}/.well-known/ucan-store.json" if upload_url else None,
122
+ "alias": f"{upload_url}/service-manifest.json" if upload_url else None,
123
+ "binding": "domain-first",
124
+ },
125
+ "upload_service_url": upload_url or None,
126
+ "upload_service_did": service_did,
127
+ "revocation_url": revocation_url or None,
128
+ "revocation_did": env_values.get("PUBLIC_REVOCATION_DID", "").strip() or service_did,
129
+ "receipts_url": receipts_url or None,
130
+ "proxy_hostname": env_values.get("PROXY_HOSTNAME", "").strip() or None,
131
+ "public_ipv4": env_values.get("PUBLIC_IPV4", "").strip() or None,
132
+ "public_ipv6": env_values.get("PUBLIC_IPV6", "").strip() or None,
133
+ "admin_did": env_values.get("UCAN_STORE_ADMIN_DID", "").strip() or None,
134
+ "webauthn_origin": env_values.get("WEBAUTHN_ORIGIN", "").strip() or None,
135
+ "webauthn_origin_fallbacks": env_values.get("WEBAUTHN_ORIGIN_FALLBACKS", "").strip() or None,
136
+ "bootstrap_validation": bootstrap_validation,
137
+ "bootstrap_proof_validation": bootstrap_proof_validation,
138
+ "pwa_env": {
139
+ "VITE_UPLOAD_SERVICE_URL": upload_url or None,
140
+ "VITE_UPLOAD_SERVICE_DID": service_did,
141
+ "VITE_REVOCATION_URL": revocation_url or None,
142
+ "VITE_REVOCATION_DID": env_values.get("PUBLIC_REVOCATION_DID", "").strip() or service_did,
143
+ "VITE_RECEIPTS_URL": receipts_url or None,
144
+ },
145
+ }
146
+ print(json.dumps(payload))
147
+
148
+
149
+ if __name__ == "__main__":
150
+ main()