@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.
- package/README.md +4 -0
- package/package.json +1 -1
- package/reference/ucan-store/README.md +89 -0
- package/reference/ucan-store/contract.json +46 -0
- package/reference/ucan-store/rootfs/Dockerfile.rootfs +24 -0
- package/reference/ucan-store/rootfs/README.md +84 -0
- package/reference/ucan-store/rootfs/build-rootfs-image.sh +127 -0
- package/reference/ucan-store/rootfs/build-rootfs.sh +242 -0
- package/reference/ucan-store/rootfs/read-rootfs-contract.py +72 -0
- package/reference/ucan-store/rootfs/ucan-store-autotls-refresh.service +12 -0
- package/reference/ucan-store/rootfs/ucan-store-bootstrap-verify.mjs +409 -0
- package/reference/ucan-store/rootfs/ucan-store-bootstrap.service +21 -0
- package/reference/ucan-store/rootfs/ucan-store-bootstrap.sh +155 -0
- package/reference/ucan-store/rootfs/ucan-store-configure.sh +261 -0
- package/reference/ucan-store/rootfs/ucan-store-describe.py +150 -0
- package/reference/ucan-store/rootfs/ucan-store-request-guard.mjs +740 -0
- package/reference/ucan-store/rootfs/ucan-store-service-start.sh +158 -0
- package/reference/ucan-store/rootfs/ucan-store-setup-server.py +229 -0
- package/reference/ucan-store/rootfs/ucan-store-upload-api-context.js +108 -0
- package/reference/ucan-store/rootfs/ucan-store-upload-api-service-identity.js +116 -0
- package/reference/ucan-store/rootfs/ucan-store.service +19 -0
- package/reference/ucan-store/rootfs/ucan_store_bootstrap_validate.py +226 -0
|
@@ -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()
|