@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.
- 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,72 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import shlex
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def shell_assign(name: str, value: str) -> str:
|
|
13
|
+
return f"{name}={shlex.quote(value)}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> int:
|
|
17
|
+
parser = argparse.ArgumentParser(description="Read a ucan-store rootfs contract and emit shell exports.")
|
|
18
|
+
parser.add_argument("contract", type=Path, help="Path to the rootfs contract JSON file")
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
payload = json.loads(args.contract.read_text())
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
print(f"Missing rootfs contract: {args.contract}", file=sys.stderr)
|
|
25
|
+
return 1
|
|
26
|
+
except json.JSONDecodeError as exc:
|
|
27
|
+
print(f"Invalid rootfs contract JSON in {args.contract}: {exc}", file=sys.stderr)
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
rootfs = payload.get("rootfs") or {}
|
|
31
|
+
services = payload.get("services") or {}
|
|
32
|
+
source = payload.get("source") or {}
|
|
33
|
+
manifest = payload.get("manifest") or {}
|
|
34
|
+
ports = payload.get("ports") or []
|
|
35
|
+
|
|
36
|
+
profile = rootfs.get("profile")
|
|
37
|
+
install_mode = rootfs.get("installMode")
|
|
38
|
+
|
|
39
|
+
if not isinstance(profile, str) or not profile.strip():
|
|
40
|
+
print("Rootfs contract is missing rootfs.profile", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
if not isinstance(install_mode, str) or not install_mode.strip():
|
|
43
|
+
print("Rootfs contract is missing rootfs.installMode", file=sys.stderr)
|
|
44
|
+
return 1
|
|
45
|
+
if not isinstance(ports, list):
|
|
46
|
+
print("Rootfs contract field ports must be a list", file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
lines = [
|
|
50
|
+
shell_assign("ROOTFS_CONTRACT_PATH", str(args.contract.resolve())),
|
|
51
|
+
shell_assign("ROOTFS_CONTRACT_ID", str(payload.get("id", ""))),
|
|
52
|
+
shell_assign("ROOTFS_CONTRACT_PROFILE", profile.strip()),
|
|
53
|
+
shell_assign("ROOTFS_CONTRACT_INSTALL_MODE", install_mode.strip()),
|
|
54
|
+
shell_assign("ROOTFS_CONTRACT_SOURCE_SUBDIRECTORY", str(source.get("subdirectory", ""))),
|
|
55
|
+
shell_assign("ROOTFS_CONTRACT_INSTALL_DIR", str(rootfs.get("installDir", ""))),
|
|
56
|
+
shell_assign("ROOTFS_CONTRACT_BINARY_PATH", str(rootfs.get("binaryPath", "/usr/local/bin/ucan-store"))),
|
|
57
|
+
shell_assign("ROOTFS_CONTRACT_DATA_DIR", str(rootfs.get("dataDir", ""))),
|
|
58
|
+
shell_assign("ROOTFS_CONTRACT_ENV_FILE", str(rootfs.get("envFile", ""))),
|
|
59
|
+
shell_assign("ROOTFS_CONTRACT_MAIN_SERVICE", str(services.get("main", ""))),
|
|
60
|
+
shell_assign("ROOTFS_CONTRACT_BOOTSTRAP_SERVICE", str(services.get("bootstrap", ""))),
|
|
61
|
+
shell_assign("ROOTFS_CONTRACT_AUTOTLS_SERVICE", str(services.get("autotlsRefresh", ""))),
|
|
62
|
+
shell_assign("ROOTFS_CONTRACT_MANIFEST_COPY_TARGET", str(manifest.get("copyTarget", ""))),
|
|
63
|
+
shell_assign("ROOTFS_CONTRACT_MANIFEST_NOTES", str(manifest.get("notes", ""))),
|
|
64
|
+
shell_assign("ROOTFS_CONTRACT_PORT_FORWARDS_JSON", json.dumps(ports, separators=(",", ":"))),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
print("\n".join(lines))
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Placeholder AutoTLS refresh hook for ucan-store
|
|
3
|
+
After=ucan-store.service
|
|
4
|
+
Wants=ucan-store.service
|
|
5
|
+
ConditionPathExists=/etc/default/ucan-store.ready
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=oneshot
|
|
9
|
+
ExecStart=/bin/true
|
|
10
|
+
|
|
11
|
+
[Install]
|
|
12
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_WEB_ROOT = process.env.WEB_ROOT || '/opt/ucan-store/web';
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const args = {
|
|
10
|
+
packageFile: '',
|
|
11
|
+
adminDid: '',
|
|
12
|
+
runtimeServiceDid: '',
|
|
13
|
+
runtimeServiceOrigin: '',
|
|
14
|
+
webRoot: DEFAULT_WEB_ROOT,
|
|
15
|
+
summaryFile: '',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
+
const arg = argv[index];
|
|
20
|
+
const value = argv[index + 1] ?? '';
|
|
21
|
+
switch (arg) {
|
|
22
|
+
case '--package-file':
|
|
23
|
+
args.packageFile = value;
|
|
24
|
+
index += 1;
|
|
25
|
+
break;
|
|
26
|
+
case '--admin-did':
|
|
27
|
+
args.adminDid = value;
|
|
28
|
+
index += 1;
|
|
29
|
+
break;
|
|
30
|
+
case '--runtime-service-did':
|
|
31
|
+
args.runtimeServiceDid = value;
|
|
32
|
+
index += 1;
|
|
33
|
+
break;
|
|
34
|
+
case '--runtime-service-origin':
|
|
35
|
+
args.runtimeServiceOrigin = value;
|
|
36
|
+
index += 1;
|
|
37
|
+
break;
|
|
38
|
+
case '--web-root':
|
|
39
|
+
args.webRoot = value;
|
|
40
|
+
index += 1;
|
|
41
|
+
break;
|
|
42
|
+
case '--summary-file':
|
|
43
|
+
args.summaryFile = value;
|
|
44
|
+
index += 1;
|
|
45
|
+
break;
|
|
46
|
+
case '-h':
|
|
47
|
+
case '--help':
|
|
48
|
+
printHelp();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!args.packageFile) {
|
|
56
|
+
throw new Error('--package-file is required');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function printHelp() {
|
|
63
|
+
console.log(`Usage:
|
|
64
|
+
ucan-store-bootstrap-verify.mjs \\
|
|
65
|
+
--package-file <path> \\
|
|
66
|
+
[--admin-did <did>] \\
|
|
67
|
+
[--runtime-service-did <did>] \\
|
|
68
|
+
[--runtime-service-origin <origin>] \\
|
|
69
|
+
[--web-root <path>] \\
|
|
70
|
+
[--summary-file <path>]`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeString(value) {
|
|
74
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeOrigin(value) {
|
|
78
|
+
const normalized = normalizeString(value);
|
|
79
|
+
if (!normalized) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL(normalized);
|
|
84
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if ((url.pathname && url.pathname !== '/') || url.search || url.hash) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return `${url.protocol}//${url.host}`;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function base64UrlToBase64(value) {
|
|
97
|
+
const padded = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
98
|
+
const remainder = padded.length % 4;
|
|
99
|
+
if (remainder === 0) {
|
|
100
|
+
return padded;
|
|
101
|
+
}
|
|
102
|
+
return padded + '='.repeat(4 - remainder);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decodeProofString(proof) {
|
|
106
|
+
if (proof.startsWith('m')) {
|
|
107
|
+
return new Uint8Array(Buffer.from(proof.slice(1), 'base64'));
|
|
108
|
+
}
|
|
109
|
+
if (proof.startsWith('u')) {
|
|
110
|
+
return new Uint8Array(Buffer.from(base64UrlToBase64(proof.slice(1)), 'base64'));
|
|
111
|
+
}
|
|
112
|
+
throw new Error('Unsupported delegation proof multibase prefix');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function capabilityCovers(provided, required) {
|
|
116
|
+
if (provided === '*' || provided === required) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (provided.endsWith('/*')) {
|
|
120
|
+
const prefix = provided.slice(0, -1);
|
|
121
|
+
return required.startsWith(prefix);
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parsePackageSpecifier(specifier) {
|
|
127
|
+
if (specifier.startsWith('@')) {
|
|
128
|
+
const [scope, name, ...rest] = specifier.split('/');
|
|
129
|
+
return {
|
|
130
|
+
pkgName: `${scope}/${name}`,
|
|
131
|
+
subpath: rest.length ? `./${rest.join('/')}` : '.',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const [name, ...rest] = specifier.split('/');
|
|
135
|
+
return {
|
|
136
|
+
pkgName: name,
|
|
137
|
+
subpath: rest.length ? `./${rest.join('/')}` : '.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function resolveExportTarget(pkgRoot, subpath) {
|
|
142
|
+
const packageJsonPath = path.join(pkgRoot, 'package.json');
|
|
143
|
+
const raw = await fs.readFile(packageJsonPath, 'utf8');
|
|
144
|
+
const pkg = JSON.parse(raw);
|
|
145
|
+
const exportsField = pkg.exports;
|
|
146
|
+
|
|
147
|
+
if (!exportsField) {
|
|
148
|
+
return pkg.main ? path.join(pkgRoot, pkg.main) : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let target = null;
|
|
152
|
+
if (typeof exportsField === 'string') {
|
|
153
|
+
target = subpath === '.' ? exportsField : null;
|
|
154
|
+
} else if (exportsField[subpath]) {
|
|
155
|
+
target = exportsField[subpath];
|
|
156
|
+
} else if (subpath === '.' && exportsField['.']) {
|
|
157
|
+
target = exportsField['.'];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!target) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (typeof target === 'string') {
|
|
164
|
+
return path.join(pkgRoot, target);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const entry = target.import ?? target.default ?? target.require ?? null;
|
|
168
|
+
return entry ? path.join(pkgRoot, entry) : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function resolveFromWebNodeModules(webRoot, specifier) {
|
|
172
|
+
const { pkgName, subpath } = parsePackageSpecifier(specifier);
|
|
173
|
+
const pkgRoot = path.join(webRoot, 'node_modules', pkgName);
|
|
174
|
+
return resolveExportTarget(pkgRoot, subpath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildModuleLoader(webRoot) {
|
|
178
|
+
const packageJsonUrl = pathToFileURL(path.join(webRoot, 'package.json')).href;
|
|
179
|
+
|
|
180
|
+
return async (specifier) => {
|
|
181
|
+
let resolved = null;
|
|
182
|
+
if (typeof import.meta.resolve === 'function') {
|
|
183
|
+
try {
|
|
184
|
+
const resolvedUrl = import.meta.resolve(specifier, packageJsonUrl);
|
|
185
|
+
resolved = resolvedUrl.startsWith('file://')
|
|
186
|
+
? fileURLToPath(resolvedUrl)
|
|
187
|
+
: resolvedUrl;
|
|
188
|
+
} catch {
|
|
189
|
+
resolved = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!resolved) {
|
|
193
|
+
resolved = await resolveFromWebNodeModules(webRoot, specifier);
|
|
194
|
+
}
|
|
195
|
+
return import(pathToFileURL(resolved).href);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function parseDelegation(proof, importFromWeb) {
|
|
200
|
+
const bytes = decodeProofString(proof);
|
|
201
|
+
try {
|
|
202
|
+
const { extract } = await importFromWeb('@ucanto/core/delegation');
|
|
203
|
+
const extracted = await withMutedConsoleLog(() => extract(bytes));
|
|
204
|
+
if (extracted?.ok) {
|
|
205
|
+
return extracted.ok;
|
|
206
|
+
}
|
|
207
|
+
if (extracted && !extracted.error) {
|
|
208
|
+
return extracted;
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Fall back to Storacha proof parser.
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const Proof = await importFromWeb('@storacha/client/proof');
|
|
215
|
+
return withMutedConsoleLog(() => Proof.parse(proof));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function verifyDelegationSignature(delegation, importFromWeb) {
|
|
219
|
+
const issuerDid = delegation?.issuer?.did?.();
|
|
220
|
+
if (!issuerDid) {
|
|
221
|
+
return { ok: false, error: 'Delegation issuer DID is missing.' };
|
|
222
|
+
}
|
|
223
|
+
if (!issuerDid.startsWith('did:key:')) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
error: `Delegation issuer ${issuerDid} is not a did:key DID; bootstrap verification currently supports did:key issuers only.`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { Verifier } = await importFromWeb('@ucanto/principal');
|
|
231
|
+
const UCAN = await importFromWeb('@ipld/dag-ucan');
|
|
232
|
+
const verifier = Verifier.parse(issuerDid);
|
|
233
|
+
const valid = await withMutedConsoleLog(() =>
|
|
234
|
+
UCAN.verifySignature(delegation.data, verifier),
|
|
235
|
+
);
|
|
236
|
+
return valid
|
|
237
|
+
? { ok: true }
|
|
238
|
+
: { ok: false, error: `Delegation signature verification failed for issuer ${issuerDid}.` };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function withMutedConsoleLog(fn) {
|
|
242
|
+
const originalLog = console.log;
|
|
243
|
+
console.log = () => {};
|
|
244
|
+
try {
|
|
245
|
+
return await fn();
|
|
246
|
+
} finally {
|
|
247
|
+
console.log = originalLog;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function main() {
|
|
252
|
+
const args = parseArgs(process.argv.slice(2));
|
|
253
|
+
const importFromWeb = buildModuleLoader(args.webRoot);
|
|
254
|
+
const errors = [];
|
|
255
|
+
const warnings = [];
|
|
256
|
+
|
|
257
|
+
const raw = await fs.readFile(args.packageFile, 'utf8');
|
|
258
|
+
const payload = JSON.parse(raw);
|
|
259
|
+
const adminDid = normalizeString(args.adminDid) ?? normalizeString(payload.adminDid);
|
|
260
|
+
const packageServiceDid = normalizeString(payload.serviceDid);
|
|
261
|
+
const runtimeServiceDid = normalizeString(args.runtimeServiceDid);
|
|
262
|
+
const runtimeServiceOrigin = normalizeOrigin(args.runtimeServiceOrigin);
|
|
263
|
+
const allowedCapabilities = Array.isArray(payload.allowedCapabilities)
|
|
264
|
+
? payload.allowedCapabilities.map(normalizeString).filter(Boolean)
|
|
265
|
+
: [];
|
|
266
|
+
const spaceDid = normalizeString(payload.spaceDid);
|
|
267
|
+
const proof = normalizeString(payload.rootDelegationProof);
|
|
268
|
+
|
|
269
|
+
if (!proof) {
|
|
270
|
+
throw new Error('Bootstrap package rootDelegationProof is missing.');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const delegation = await parseDelegation(proof, importFromWeb);
|
|
274
|
+
const issuerDid = delegation?.issuer?.did?.() ?? null;
|
|
275
|
+
const audienceDid = delegation?.audience?.did?.() ?? null;
|
|
276
|
+
const capabilities = Array.isArray(delegation?.capabilities)
|
|
277
|
+
? delegation.capabilities.map((capability) => ({
|
|
278
|
+
with: normalizeString(capability?.with) ?? null,
|
|
279
|
+
can: normalizeString(capability?.can) ?? null,
|
|
280
|
+
}))
|
|
281
|
+
: [];
|
|
282
|
+
|
|
283
|
+
if (!issuerDid) {
|
|
284
|
+
errors.push('Delegation issuer DID could not be determined.');
|
|
285
|
+
}
|
|
286
|
+
if (!audienceDid) {
|
|
287
|
+
errors.push('Delegation audience DID could not be determined.');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (adminDid && issuerDid && adminDid !== issuerDid) {
|
|
291
|
+
errors.push(`Delegation issuer ${issuerDid} does not match configured admin DID ${adminDid}.`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const expectedAudience = runtimeServiceDid ?? packageServiceDid;
|
|
295
|
+
if (expectedAudience && audienceDid && expectedAudience !== audienceDid) {
|
|
296
|
+
errors.push(
|
|
297
|
+
`Delegation audience ${audienceDid} does not match expected service DID ${expectedAudience}.`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (runtimeServiceDid && packageServiceDid && runtimeServiceDid !== packageServiceDid) {
|
|
302
|
+
errors.push(
|
|
303
|
+
`Runtime service DID ${runtimeServiceDid} does not match bootstrap package serviceDid ${packageServiceDid}.`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const signatureCheck = await verifyDelegationSignature(delegation, importFromWeb);
|
|
308
|
+
if (!signatureCheck.ok) {
|
|
309
|
+
errors.push(signatureCheck.error);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
313
|
+
const expiration =
|
|
314
|
+
typeof delegation?.expiration === 'number' && Number.isFinite(delegation.expiration)
|
|
315
|
+
? delegation.expiration
|
|
316
|
+
: null;
|
|
317
|
+
const notBefore =
|
|
318
|
+
typeof delegation?.notBefore === 'number' && Number.isFinite(delegation.notBefore)
|
|
319
|
+
? delegation.notBefore
|
|
320
|
+
: null;
|
|
321
|
+
|
|
322
|
+
if (expiration !== null && expiration <= nowSeconds) {
|
|
323
|
+
errors.push(`Delegation expired at ${new Date(expiration * 1000).toISOString()}.`);
|
|
324
|
+
}
|
|
325
|
+
if (notBefore !== null && notBefore > nowSeconds) {
|
|
326
|
+
errors.push(`Delegation is not valid before ${new Date(notBefore * 1000).toISOString()}.`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (spaceDid) {
|
|
330
|
+
const invalidScopes = capabilities.filter(
|
|
331
|
+
(capability) => capability.with && capability.with !== spaceDid,
|
|
332
|
+
);
|
|
333
|
+
if (invalidScopes.length > 0) {
|
|
334
|
+
warnings.push(
|
|
335
|
+
`Delegation contains capabilities outside the configured space DID ${spaceDid}.`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const missingCapabilities = [];
|
|
341
|
+
for (const requiredCapability of allowedCapabilities) {
|
|
342
|
+
const covered = capabilities.some(
|
|
343
|
+
(capability) =>
|
|
344
|
+
capability.with === spaceDid &&
|
|
345
|
+
capability.can &&
|
|
346
|
+
capabilityCovers(capability.can, requiredCapability),
|
|
347
|
+
);
|
|
348
|
+
if (!covered) {
|
|
349
|
+
missingCapabilities.push(requiredCapability);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (missingCapabilities.length > 0) {
|
|
353
|
+
errors.push(
|
|
354
|
+
`Delegation does not cover required capabilities: ${missingCapabilities.join(', ')}.`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const packageServiceOrigin = normalizeOrigin(payload.serviceOrigin);
|
|
359
|
+
if (runtimeServiceOrigin && packageServiceOrigin && runtimeServiceOrigin !== packageServiceOrigin) {
|
|
360
|
+
errors.push(
|
|
361
|
+
`Runtime service origin ${runtimeServiceOrigin} does not match bootstrap package serviceOrigin ${packageServiceOrigin}.`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const summary = {
|
|
366
|
+
status: errors.length === 0 ? 'valid' : 'invalid',
|
|
367
|
+
valid: errors.length === 0,
|
|
368
|
+
errors,
|
|
369
|
+
warnings,
|
|
370
|
+
verification: {
|
|
371
|
+
issuerDid,
|
|
372
|
+
audienceDid,
|
|
373
|
+
adminDid,
|
|
374
|
+
packageServiceDid,
|
|
375
|
+
runtimeServiceDid,
|
|
376
|
+
runtimeServiceOrigin,
|
|
377
|
+
packageServiceOrigin,
|
|
378
|
+
spaceDid,
|
|
379
|
+
signatureValid: signatureCheck.ok,
|
|
380
|
+
expiration,
|
|
381
|
+
notBefore,
|
|
382
|
+
capabilities,
|
|
383
|
+
allowedCapabilities,
|
|
384
|
+
missingCapabilities,
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
if (args.summaryFile) {
|
|
389
|
+
await fs.mkdir(path.dirname(args.summaryFile), { recursive: true });
|
|
390
|
+
await fs.writeFile(args.summaryFile, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
394
|
+
if (!summary.valid) {
|
|
395
|
+
process.exitCode = 1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
main().catch((error) => {
|
|
400
|
+
const summary = {
|
|
401
|
+
status: 'invalid',
|
|
402
|
+
valid: false,
|
|
403
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
404
|
+
warnings: [],
|
|
405
|
+
verification: null,
|
|
406
|
+
};
|
|
407
|
+
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Temporary ucan-store setup endpoint
|
|
3
|
+
Wants=network-online.target
|
|
4
|
+
After=network-online.target
|
|
5
|
+
ConditionPathExists=!/etc/default/ucan-store.ready
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Environment=ENV_FILE=/etc/default/ucan-store
|
|
9
|
+
Environment=READY_FILE=/etc/default/ucan-store.ready
|
|
10
|
+
Environment=SERVICE_NAME=ucan-store.service
|
|
11
|
+
Environment=BOOTSTRAP_SERVICE=ucan-store-bootstrap.service
|
|
12
|
+
Environment=METADATA_FILE=/run/ucan-store-setup-metadata.json
|
|
13
|
+
Environment=METADATA_ERROR_FILE=/run/ucan-store-setup-metadata.error
|
|
14
|
+
ExecStart=/usr/bin/python3 /usr/local/sbin/ucan-store-setup-server.py
|
|
15
|
+
StandardOutput=journal
|
|
16
|
+
StandardError=journal
|
|
17
|
+
Restart=on-failure
|
|
18
|
+
RestartSec=2s
|
|
19
|
+
|
|
20
|
+
[Install]
|
|
21
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
INSTALL_DIR="${INSTALL_DIR:-/opt/ucan-store}"
|
|
5
|
+
SERVICE_USER="${SERVICE_USER:-ucan-store}"
|
|
6
|
+
DATA_DIR="${DATA_DIR:-/var/lib/ucan-store}"
|
|
7
|
+
ENV_FILE="${ENV_FILE:-/etc/default/ucan-store}"
|
|
8
|
+
READY_FILE="${READY_FILE:-/etc/default/ucan-store.ready}"
|
|
9
|
+
BOOTSTRAP_PACKAGE_FILE="${BOOTSTRAP_PACKAGE_FILE:-/etc/ucan-store/bootstrap-package.json}"
|
|
10
|
+
BOOTSTRAP_VERIFICATION_FILE="${BOOTSTRAP_VERIFICATION_FILE:-/etc/ucan-store/bootstrap-verification.json}"
|
|
11
|
+
APP_BINARY="${APP_BINARY:-/usr/local/bin/ucan-store}"
|
|
12
|
+
NODE_MIN_MAJOR="${NODE_MIN_MAJOR:-22}"
|
|
13
|
+
UPLOAD_API_CONTEXT_TEMPLATE="${UPLOAD_API_CONTEXT_TEMPLATE:-/usr/local/share/ucan-store/ucan-store-upload-api-context.js}"
|
|
14
|
+
UPLOAD_API_SERVICE_IDENTITY_TEMPLATE="${UPLOAD_API_SERVICE_IDENTITY_TEMPLATE:-/usr/local/share/ucan-store/ucan-store-upload-api-service-identity.js}"
|
|
15
|
+
PHASE="${1:-all}"
|
|
16
|
+
|
|
17
|
+
if [ ! -d "${INSTALL_DIR}" ]; then
|
|
18
|
+
echo "Missing ${INSTALL_DIR}; the rootfs build did not copy ucan-store." >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
write_env_var() {
|
|
23
|
+
local key="$1"
|
|
24
|
+
local value="$2"
|
|
25
|
+
|
|
26
|
+
if grep -Eq "^[#[:space:]]*${key}=" "${ENV_FILE}"; then
|
|
27
|
+
sed -i "s|^[#[:space:]]*${key}=.*|${key}=${value}|" "${ENV_FILE}"
|
|
28
|
+
else
|
|
29
|
+
printf '%s=%s\n' "${key}" "${value}" >> "${ENV_FILE}"
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
run_phase_base() {
|
|
34
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
35
|
+
apt-get update
|
|
36
|
+
apt-get install -y ca-certificates curl gnupg caddy python3
|
|
37
|
+
|
|
38
|
+
if ! command -v node >/dev/null 2>&1 || [ "$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0)" -lt "${NODE_MIN_MAJOR}" ]; then
|
|
39
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
40
|
+
apt-get install -y nodejs
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
rm -rf /var/lib/apt/lists/*
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
run_phase_build() {
|
|
47
|
+
if [ ! -f "${INSTALL_DIR}/web/package.json" ]; then
|
|
48
|
+
echo "Missing ${INSTALL_DIR}/web/package.json" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
if [ ! -f "${INSTALL_DIR}/web/package-lock.json" ]; then
|
|
52
|
+
echo "Missing ${INSTALL_DIR}/web/package-lock.json" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
mkdir -p "${DATA_DIR}" "$(dirname "${ENV_FILE}")"
|
|
57
|
+
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|
|
58
|
+
useradd --system --home "${DATA_DIR}" --create-home --shell /usr/sbin/nologin "${SERVICE_USER}"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DATA_DIR}" "${INSTALL_DIR}"
|
|
62
|
+
|
|
63
|
+
if command -v runuser >/dev/null 2>&1; then
|
|
64
|
+
runuser -u "${SERVICE_USER}" -- env HOME="${DATA_DIR}" bash -lc "cd '${INSTALL_DIR}/web' && npm ci"
|
|
65
|
+
else
|
|
66
|
+
echo "runuser is required to install npm dependencies as ${SERVICE_USER}" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
install_upload_api_context_override
|
|
71
|
+
|
|
72
|
+
rm -rf "${DATA_DIR}/.npm" "${INSTALL_DIR}/web/node_modules/.cache"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
install_upload_api_context_override() {
|
|
76
|
+
local helper_dir="${INSTALL_DIR}/web/node_modules/@storacha/upload-api/dist/test/helpers"
|
|
77
|
+
mkdir -p "${helper_dir}"
|
|
78
|
+
|
|
79
|
+
if [ ! -f "${UPLOAD_API_CONTEXT_TEMPLATE}" ]; then
|
|
80
|
+
echo "Missing upload-api context template: ${UPLOAD_API_CONTEXT_TEMPLATE}" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
if [ ! -f "${UPLOAD_API_SERVICE_IDENTITY_TEMPLATE}" ]; then
|
|
84
|
+
echo "Missing upload-api service identity template: ${UPLOAD_API_SERVICE_IDENTITY_TEMPLATE}" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
cp "${UPLOAD_API_CONTEXT_TEMPLATE}" "${helper_dir}/context.js"
|
|
89
|
+
cp "${UPLOAD_API_SERVICE_IDENTITY_TEMPLATE}" "${helper_dir}/ucan-store-service-identity.js"
|
|
90
|
+
chown "${SERVICE_USER}:${SERVICE_USER}" "${helper_dir}/context.js" "${helper_dir}/ucan-store-service-identity.js"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
run_phase_finalize() {
|
|
94
|
+
mkdir -p "${DATA_DIR}" "$(dirname "${ENV_FILE}")" /etc/caddy "$(dirname "${BOOTSTRAP_PACKAGE_FILE}")"
|
|
95
|
+
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DATA_DIR}" "${INSTALL_DIR}"
|
|
96
|
+
|
|
97
|
+
cat > "${APP_BINARY}" <<'EOF'
|
|
98
|
+
#!/usr/bin/env bash
|
|
99
|
+
set -euo pipefail
|
|
100
|
+
exec /usr/bin/node /opt/ucan-store/local-storacha-api/index.mjs "$@"
|
|
101
|
+
EOF
|
|
102
|
+
chmod 0755 "${APP_BINARY}"
|
|
103
|
+
|
|
104
|
+
touch "${ENV_FILE}"
|
|
105
|
+
chmod 0640 "${ENV_FILE}"
|
|
106
|
+
chown "root:${SERVICE_USER}" "${ENV_FILE}"
|
|
107
|
+
rm -f "${READY_FILE}"
|
|
108
|
+
|
|
109
|
+
write_env_var "NODE_ENV" "production"
|
|
110
|
+
write_env_var "STORACHA_LOCAL_PORT" "8787"
|
|
111
|
+
write_env_var "UCAN_STORE_PROXY_PORT" "8788"
|
|
112
|
+
write_env_var "UCAN_STORE_SERVICE_KEY_ALGORITHM" "ed25519"
|
|
113
|
+
write_env_var "UCAN_STORE_SERVICE_SIGNER_FILE" "${DATA_DIR}/service-ed25519.key"
|
|
114
|
+
write_env_var "UCAN_STORE_SERVICE_DID" ""
|
|
115
|
+
write_env_var "UCAN_STORE_ADMIN_API_TOKEN" ""
|
|
116
|
+
write_env_var "WEBAUTHN_ORIGIN" "http://localhost:5173"
|
|
117
|
+
write_env_var "WEBAUTHN_ORIGIN_FALLBACKS" ""
|
|
118
|
+
write_env_var "UCAN_STORE_ADMIN_DID" ""
|
|
119
|
+
write_env_var "UCAN_STORE_BOOTSTRAP_PACKAGE_FILE" "${BOOTSTRAP_PACKAGE_FILE}"
|
|
120
|
+
write_env_var "UCAN_STORE_BOOTSTRAP_VERIFICATION_FILE" "${BOOTSTRAP_VERIFICATION_FILE}"
|
|
121
|
+
write_env_var "UCAN_STORE_REQUIRE_BOOTSTRAP_PACKAGE" "1"
|
|
122
|
+
write_env_var "BOOTSTRAP_VERIFICATION_TIMEOUT_SECONDS" "60"
|
|
123
|
+
write_env_var "BOOTSTRAP_VERIFICATION_INTERVAL_SECONDS" "2"
|
|
124
|
+
write_env_var "PUBLIC_IPV4" ""
|
|
125
|
+
write_env_var "PUBLIC_IPV6" ""
|
|
126
|
+
write_env_var "PROXY_HOSTNAME" ""
|
|
127
|
+
write_env_var "PUBLIC_UPLOAD_SERVICE_URL" ""
|
|
128
|
+
write_env_var "PUBLIC_UPLOAD_SERVICE_DID" ""
|
|
129
|
+
write_env_var "PUBLIC_REVOCATION_URL" ""
|
|
130
|
+
write_env_var "PUBLIC_REVOCATION_DID" ""
|
|
131
|
+
write_env_var "PUBLIC_RECEIPTS_URL" ""
|
|
132
|
+
|
|
133
|
+
rm -f "${BOOTSTRAP_PACKAGE_FILE}" "${BOOTSTRAP_VERIFICATION_FILE}"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case "${PHASE}" in
|
|
137
|
+
base)
|
|
138
|
+
run_phase_base
|
|
139
|
+
;;
|
|
140
|
+
build)
|
|
141
|
+
run_phase_build
|
|
142
|
+
;;
|
|
143
|
+
finalize)
|
|
144
|
+
run_phase_finalize
|
|
145
|
+
;;
|
|
146
|
+
all)
|
|
147
|
+
run_phase_base
|
|
148
|
+
run_phase_build
|
|
149
|
+
run_phase_finalize
|
|
150
|
+
;;
|
|
151
|
+
*)
|
|
152
|
+
echo "Unknown phase: ${PHASE}" >&2
|
|
153
|
+
exit 1
|
|
154
|
+
;;
|
|
155
|
+
esac
|