@le-space/rootfs 0.6.0 → 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,226 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import re
8
+ from hashlib import sha256
9
+ from urllib.parse import urlsplit
10
+
11
+
12
+ ETH_ADDRESS_RE = re.compile(r"^0x[a-fA-F0-9]{40}$")
13
+
14
+
15
+ def _non_empty_string(value: object) -> str | None:
16
+ if not isinstance(value, str):
17
+ return None
18
+ normalized = value.strip()
19
+ return normalized or None
20
+
21
+
22
+ def _did(value: object) -> str | None:
23
+ normalized = _non_empty_string(value)
24
+ if normalized and normalized.startswith("did:"):
25
+ return normalized
26
+ return None
27
+
28
+
29
+ def _origin(value: object) -> str | None:
30
+ normalized = _non_empty_string(value)
31
+ if not normalized:
32
+ return None
33
+ try:
34
+ parsed = urlsplit(normalized)
35
+ except ValueError:
36
+ return None
37
+
38
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
39
+ return None
40
+ if parsed.path not in ("", "/") or parsed.query or parsed.fragment:
41
+ return None
42
+ return f"{parsed.scheme}://{parsed.netloc}"
43
+
44
+
45
+ def load_bootstrap_package(path: str) -> object | None:
46
+ if not path or not os.path.exists(path):
47
+ return None
48
+ with open(path, encoding="utf-8") as handle:
49
+ return json.load(handle)
50
+
51
+
52
+ def summarize_bootstrap_package(
53
+ path: str,
54
+ *,
55
+ runtime_service_did: str | None = None,
56
+ runtime_service_origin: str | None = None,
57
+ admin_did: str | None = None,
58
+ allow_missing: bool = False,
59
+ ) -> dict[str, object]:
60
+ if not path or not os.path.exists(path):
61
+ if allow_missing:
62
+ return {
63
+ "status": "missing",
64
+ "valid": False,
65
+ "errors": [],
66
+ "warnings": ["Bootstrap package is not configured yet."],
67
+ "package": None,
68
+ }
69
+ raise FileNotFoundError(path or "bootstrap package path missing")
70
+
71
+ payload = load_bootstrap_package(path)
72
+ if not isinstance(payload, dict):
73
+ return {
74
+ "status": "invalid",
75
+ "valid": False,
76
+ "errors": ["Bootstrap package must be a JSON object."],
77
+ "warnings": [],
78
+ "package": None,
79
+ }
80
+
81
+ errors: list[str] = []
82
+ warnings: list[str] = []
83
+
84
+ operator_address = _non_empty_string(payload.get("operatorAddress"))
85
+ if not operator_address or not ETH_ADDRESS_RE.match(operator_address):
86
+ errors.append(
87
+ "operatorAddress must be a 0x-prefixed 20-byte Ethereum address.",
88
+ )
89
+
90
+ package_admin_did = _did(payload.get("adminDid"))
91
+ if not package_admin_did:
92
+ errors.append("adminDid must be a non-empty DID string.")
93
+
94
+ service_did_raw = payload.get("serviceDid")
95
+ service_did = None
96
+ if service_did_raw not in (None, ""):
97
+ service_did = _did(service_did_raw)
98
+ if not service_did:
99
+ errors.append("serviceDid must be empty or a non-empty DID string.")
100
+
101
+ space_did = _did(payload.get("spaceDid"))
102
+ if not space_did:
103
+ errors.append("spaceDid must be a non-empty DID string.")
104
+
105
+ root_delegation_proof = _non_empty_string(payload.get("rootDelegationProof"))
106
+ if not root_delegation_proof:
107
+ errors.append("rootDelegationProof must be a non-empty proof string.")
108
+
109
+ allowed_capabilities_raw = payload.get("allowedCapabilities")
110
+ allowed_capabilities = []
111
+ if isinstance(allowed_capabilities_raw, list):
112
+ allowed_capabilities = [
113
+ capability
114
+ for capability in (_non_empty_string(entry) for entry in allowed_capabilities_raw)
115
+ if capability
116
+ ]
117
+ if not allowed_capabilities:
118
+ errors.append("allowedCapabilities must contain at least one capability string.")
119
+
120
+ default_expiration = payload.get("defaultUserDelegationExpiration")
121
+ if default_expiration is not None and (
122
+ not isinstance(default_expiration, int) or default_expiration < 0
123
+ ):
124
+ errors.append(
125
+ "defaultUserDelegationExpiration must be null or a non-negative integer number of seconds.",
126
+ )
127
+
128
+ max_expiration = payload.get("maxUserDelegationExpiration")
129
+ if max_expiration is not None and (
130
+ not isinstance(max_expiration, int) or max_expiration < 0
131
+ ):
132
+ errors.append(
133
+ "maxUserDelegationExpiration must be null or a non-negative integer number of seconds.",
134
+ )
135
+ if (
136
+ isinstance(default_expiration, int)
137
+ and isinstance(max_expiration, int)
138
+ and default_expiration > max_expiration
139
+ ):
140
+ errors.append(
141
+ "defaultUserDelegationExpiration cannot exceed maxUserDelegationExpiration.",
142
+ )
143
+
144
+ pwa_origin = _origin(payload.get("pwaOrigin"))
145
+ if not pwa_origin:
146
+ errors.append("pwaOrigin must be an http(s) origin without path, query, or hash.")
147
+
148
+ service_origin = _origin(payload.get("serviceOrigin"))
149
+ if not service_origin:
150
+ errors.append("serviceOrigin must be an http(s) origin without path, query, or hash.")
151
+
152
+ if admin_did and package_admin_did and admin_did != package_admin_did:
153
+ errors.append("Configured admin DID does not match bootstrap package adminDid.")
154
+
155
+ normalized_runtime_origin = _origin(runtime_service_origin) if runtime_service_origin else None
156
+ service_did_matches_runtime = None
157
+ if runtime_service_did and service_did:
158
+ service_did_matches_runtime = runtime_service_did == service_did
159
+ if not service_did_matches_runtime:
160
+ errors.append("Bootstrap package serviceDid does not match the running service DID.")
161
+
162
+ service_origin_matches_runtime = None
163
+ if normalized_runtime_origin and service_origin:
164
+ service_origin_matches_runtime = normalized_runtime_origin == service_origin
165
+ if not service_origin_matches_runtime:
166
+ errors.append(
167
+ "Bootstrap package serviceOrigin does not match the configured public upload-service origin.",
168
+ )
169
+
170
+ summary = {
171
+ "status": "valid" if not errors else "invalid",
172
+ "valid": len(errors) == 0,
173
+ "errors": errors,
174
+ "warnings": warnings,
175
+ "package": {
176
+ "operatorAddress": operator_address,
177
+ "adminDid": package_admin_did,
178
+ "serviceDid": service_did,
179
+ "spaceDid": space_did,
180
+ "rootDelegationProofPresent": bool(root_delegation_proof),
181
+ "rootDelegationProofSha256": sha256(
182
+ (root_delegation_proof or "").encode("utf-8"),
183
+ ).hexdigest()
184
+ if root_delegation_proof
185
+ else None,
186
+ "allowedCapabilities": allowed_capabilities,
187
+ "defaultUserDelegationExpiration": default_expiration
188
+ if isinstance(default_expiration, int)
189
+ else None,
190
+ "maxUserDelegationExpiration": max_expiration
191
+ if isinstance(max_expiration, int)
192
+ else None,
193
+ "pwaOrigin": pwa_origin,
194
+ "serviceOrigin": service_origin,
195
+ },
196
+ "runtimeChecks": {
197
+ "serviceDidMatchesRuntime": service_did_matches_runtime,
198
+ "serviceOriginMatchesRuntime": service_origin_matches_runtime,
199
+ },
200
+ }
201
+ return summary
202
+
203
+
204
+ def main() -> None:
205
+ parser = argparse.ArgumentParser()
206
+ parser.add_argument("--package-file", required=True)
207
+ parser.add_argument("--runtime-service-did", default="")
208
+ parser.add_argument("--runtime-service-origin", default="")
209
+ parser.add_argument("--admin-did", default="")
210
+ parser.add_argument("--allow-missing", action="store_true")
211
+ args = parser.parse_args()
212
+
213
+ summary = summarize_bootstrap_package(
214
+ args.package_file,
215
+ runtime_service_did=_non_empty_string(args.runtime_service_did),
216
+ runtime_service_origin=_non_empty_string(args.runtime_service_origin),
217
+ admin_did=_non_empty_string(args.admin_did),
218
+ allow_missing=args.allow_missing,
219
+ )
220
+ print(json.dumps(summary))
221
+ if summary.get("status") == "invalid":
222
+ raise SystemExit(1)
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()