@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,158 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ENV_FILE="${ENV_FILE:-/etc/default/ucan-store}"
5
+ APP_BINARY="${APP_BINARY:-/usr/local/bin/ucan-store}"
6
+ BOOTSTRAP_VALIDATOR="${BOOTSTRAP_VALIDATOR:-/usr/local/sbin/ucan_store_bootstrap_validate.py}"
7
+ BOOTSTRAP_CRYPTO_VERIFIER="${BOOTSTRAP_CRYPTO_VERIFIER:-/usr/local/sbin/ucan-store-bootstrap-verify.mjs}"
8
+ REQUEST_GUARD="${REQUEST_GUARD:-/usr/local/sbin/ucan-store-request-guard.mjs}"
9
+ BOOTSTRAP_PACKAGE_FILE_DEFAULT="${BOOTSTRAP_PACKAGE_FILE:-/etc/ucan-store/bootstrap-package.json}"
10
+ BOOTSTRAP_VERIFICATION_FILE_DEFAULT="${BOOTSTRAP_VERIFICATION_FILE:-/etc/ucan-store/bootstrap-verification.json}"
11
+ VERIFY_TIMEOUT_DEFAULT="${BOOTSTRAP_VERIFICATION_TIMEOUT_SECONDS:-60}"
12
+ VERIFY_INTERVAL_DEFAULT="${BOOTSTRAP_VERIFICATION_INTERVAL_SECONDS:-2}"
13
+ APP_PID=""
14
+ GUARD_PID=""
15
+
16
+ if [ -f "${ENV_FILE}" ]; then
17
+ set -a
18
+ # shellcheck disable=SC1090
19
+ . "${ENV_FILE}"
20
+ set +a
21
+ fi
22
+
23
+ SERVICE_PORT="${STORACHA_LOCAL_PORT:-8787}"
24
+ BOOTSTRAP_PACKAGE_FILE="${UCAN_STORE_BOOTSTRAP_PACKAGE_FILE:-${BOOTSTRAP_PACKAGE_FILE_DEFAULT}}"
25
+ BOOTSTRAP_VERIFICATION_FILE="${UCAN_STORE_BOOTSTRAP_VERIFICATION_FILE:-${BOOTSTRAP_VERIFICATION_FILE_DEFAULT}}"
26
+ PUBLIC_UPLOAD_SERVICE_URL="${PUBLIC_UPLOAD_SERVICE_URL:-}"
27
+ ADMIN_DID="${UCAN_STORE_ADMIN_DID:-}"
28
+ VERIFY_TIMEOUT_SECONDS="${BOOTSTRAP_VERIFICATION_TIMEOUT_SECONDS:-${VERIFY_TIMEOUT_DEFAULT}}"
29
+ VERIFY_INTERVAL_SECONDS="${BOOTSTRAP_VERIFICATION_INTERVAL_SECONDS:-${VERIFY_INTERVAL_DEFAULT}}"
30
+ REQUIRE_BOOTSTRAP_PACKAGE="${UCAN_STORE_REQUIRE_BOOTSTRAP_PACKAGE:-1}"
31
+ BOOTSTRAP_VALIDATION_MODE="enforce"
32
+
33
+ is_truthy() {
34
+ case "${1:-}" in
35
+ 1|true|TRUE|yes|YES|on|ON)
36
+ return 0
37
+ ;;
38
+ *)
39
+ return 1
40
+ ;;
41
+ esac
42
+ }
43
+
44
+ terminate_child() {
45
+ if [ -n "${APP_PID}" ] && kill -0 "${APP_PID}" 2>/dev/null; then
46
+ kill "${APP_PID}" 2>/dev/null || true
47
+ wait "${APP_PID}" 2>/dev/null || true
48
+ fi
49
+ if [ -n "${GUARD_PID}" ] && kill -0 "${GUARD_PID}" 2>/dev/null; then
50
+ kill "${GUARD_PID}" 2>/dev/null || true
51
+ wait "${GUARD_PID}" 2>/dev/null || true
52
+ fi
53
+ }
54
+
55
+ validate_bootstrap_preflight() {
56
+ if [ ! -f "${BOOTSTRAP_PACKAGE_FILE}" ]; then
57
+ if is_truthy "${REQUIRE_BOOTSTRAP_PACKAGE}"; then
58
+ echo "Missing required bootstrap package: ${BOOTSTRAP_PACKAGE_FILE}" >&2
59
+ exit 1
60
+ fi
61
+ BOOTSTRAP_VALIDATION_MODE="skip"
62
+ echo "Bootstrap package not configured; skipping startup validation." >&2
63
+ return 0
64
+ fi
65
+
66
+ if ! python3 "${BOOTSTRAP_VALIDATOR}" \
67
+ --package-file "${BOOTSTRAP_PACKAGE_FILE}" \
68
+ --runtime-service-origin "${PUBLIC_UPLOAD_SERVICE_URL}" \
69
+ --admin-did "${ADMIN_DID}" >/dev/null; then
70
+ echo "Bootstrap package preflight validation failed." >&2
71
+ exit 1
72
+ fi
73
+
74
+ if ! node "${BOOTSTRAP_CRYPTO_VERIFIER}" \
75
+ --package-file "${BOOTSTRAP_PACKAGE_FILE}" \
76
+ --runtime-service-origin "${PUBLIC_UPLOAD_SERVICE_URL}" \
77
+ --admin-did "${ADMIN_DID}" \
78
+ --summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null; then
79
+ echo "Bootstrap delegation cryptographic preflight validation failed." >&2
80
+ exit 1
81
+ fi
82
+ }
83
+
84
+ probe_service_did() {
85
+ python3 - "${SERVICE_PORT}" "${VERIFY_TIMEOUT_SECONDS}" "${VERIFY_INTERVAL_SECONDS}" <<'PY'
86
+ import json
87
+ import sys
88
+ import time
89
+ import urllib.error
90
+ import urllib.request
91
+
92
+ port = int(sys.argv[1])
93
+ timeout_seconds = float(sys.argv[2])
94
+ interval_seconds = float(sys.argv[3])
95
+ url = f"http://127.0.0.1:{port}/.well-known/did.json"
96
+ deadline = time.monotonic() + timeout_seconds
97
+ last_error = None
98
+
99
+ while time.monotonic() < deadline:
100
+ try:
101
+ with urllib.request.urlopen(url, timeout=5) as response:
102
+ payload = json.loads(response.read().decode("utf-8"))
103
+ did = payload.get("id")
104
+ if isinstance(did, str) and did.strip():
105
+ print(did.strip())
106
+ raise SystemExit(0)
107
+ last_error = "did.json missing id"
108
+ except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:
109
+ last_error = str(error)
110
+ time.sleep(interval_seconds)
111
+
112
+ raise SystemExit(last_error or "service DID probe timed out")
113
+ PY
114
+ }
115
+
116
+ trap terminate_child EXIT INT TERM
117
+ validate_bootstrap_preflight
118
+
119
+ "${APP_BINARY}" "$@" &
120
+ APP_PID="$!"
121
+
122
+ if [ "${BOOTSTRAP_VALIDATION_MODE}" = "enforce" ]; then
123
+ SERVICE_DID="$(probe_service_did)"
124
+ RUNTIME_SERVICE_ORIGIN=""
125
+ if [ -n "${PUBLIC_UPLOAD_SERVICE_URL}" ]; then
126
+ RUNTIME_SERVICE_ORIGIN="$(python3 - <<'PY'
127
+ import os
128
+ from urllib.parse import urlsplit
129
+
130
+ value = os.environ.get("PUBLIC_UPLOAD_SERVICE_URL", "").strip()
131
+ parts = urlsplit(value)
132
+ if parts.scheme and parts.netloc:
133
+ print(f"{parts.scheme}://{parts.netloc}")
134
+ PY
135
+ )"
136
+ fi
137
+ python3 "${BOOTSTRAP_VALIDATOR}" \
138
+ --package-file "${BOOTSTRAP_PACKAGE_FILE}" \
139
+ --runtime-service-did "${SERVICE_DID}" \
140
+ --runtime-service-origin "${RUNTIME_SERVICE_ORIGIN}" \
141
+ --admin-did "${ADMIN_DID}" >/dev/null
142
+ node "${BOOTSTRAP_CRYPTO_VERIFIER}" \
143
+ --package-file "${BOOTSTRAP_PACKAGE_FILE}" \
144
+ --runtime-service-did "${SERVICE_DID}" \
145
+ --runtime-service-origin "${RUNTIME_SERVICE_ORIGIN}" \
146
+ --admin-did "${ADMIN_DID}" \
147
+ --summary-file "${BOOTSTRAP_VERIFICATION_FILE}" >/dev/null
148
+ fi
149
+
150
+ node "${REQUEST_GUARD}" &
151
+ GUARD_PID="$!"
152
+
153
+ set +e
154
+ wait -n "${APP_PID}" "${GUARD_PID}"
155
+ STATUS="$?"
156
+ set -e
157
+ terminate_child
158
+ exit "${STATUS}"
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import tempfile
6
+ import threading
7
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
+ from urllib.parse import urlsplit
9
+
10
+
11
+ ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/ucan-store")
12
+ READY_FILE = os.environ.get("READY_FILE", "/etc/default/ucan-store.ready")
13
+ SERVICE_NAME = os.environ.get("SERVICE_NAME", "ucan-store.service")
14
+ BOOTSTRAP_SERVICE = os.environ.get("BOOTSTRAP_SERVICE", "ucan-store-bootstrap.service")
15
+ CONFIGURE_SCRIPT = "/usr/local/sbin/ucan-store-configure.sh"
16
+ DESCRIBE_SCRIPT = "/usr/local/sbin/ucan-store-describe.py"
17
+ METADATA_FILE = os.environ.get("METADATA_FILE", "/run/ucan-store-setup-metadata.json")
18
+ METADATA_ERROR_FILE = os.environ.get("METADATA_ERROR_FILE", "/run/ucan-store-setup-metadata.error")
19
+
20
+
21
+ def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
22
+ handler.send_header("Access-Control-Allow-Origin", "*")
23
+ handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
24
+ handler.send_header("Access-Control-Allow-Headers", "content-type")
25
+
26
+
27
+ def _clear_metadata_state() -> None:
28
+ for path in (METADATA_FILE, METADATA_ERROR_FILE):
29
+ try:
30
+ os.remove(path)
31
+ except FileNotFoundError:
32
+ pass
33
+
34
+
35
+ def _generate_metadata_files() -> None:
36
+ try:
37
+ describe = subprocess.run(
38
+ [DESCRIBE_SCRIPT],
39
+ check=True,
40
+ capture_output=True,
41
+ text=True,
42
+ )
43
+ payload = json.loads(describe.stdout.strip() or "{}")
44
+ with open(METADATA_FILE, "w", encoding="utf-8") as handle:
45
+ json.dump(payload, handle)
46
+ except (subprocess.CalledProcessError, json.JSONDecodeError) as error:
47
+ with open(METADATA_ERROR_FILE, "w", encoding="utf-8") as handle:
48
+ handle.write(str(error))
49
+
50
+
51
+ def _stop_bootstrap_service() -> None:
52
+ subprocess.run(["systemctl", "stop", BOOTSTRAP_SERVICE], check=False)
53
+
54
+
55
+ class Handler(BaseHTTPRequestHandler):
56
+ server_version = "UcanStoreSetup/1.0"
57
+
58
+ def _request_path(self) -> str:
59
+ return urlsplit(self.path).path
60
+
61
+ def _send_json(self, status: int, payload: dict) -> None:
62
+ body = json.dumps(payload).encode("utf-8")
63
+ self.send_response(status)
64
+ _cors_headers(self)
65
+ self.send_header("Content-Type", "application/json")
66
+ self.send_header("Content-Length", str(len(body)))
67
+ self.end_headers()
68
+ self.wfile.write(body)
69
+
70
+ def log_message(self, format: str, *args) -> None: # noqa: A003
71
+ return
72
+
73
+ def do_OPTIONS(self) -> None: # noqa: N802
74
+ self.send_response(204)
75
+ _cors_headers(self)
76
+ self.end_headers()
77
+
78
+ def do_GET(self) -> None: # noqa: N802
79
+ if self._request_path() == "/metadata":
80
+ self._handle_metadata()
81
+ return
82
+
83
+ if self._request_path() not in ("/", "/health"):
84
+ self._send_json(404, {"status": "not-found"})
85
+ return
86
+
87
+ self._send_json(
88
+ 200,
89
+ {
90
+ "status": "waiting-for-service-config",
91
+ "ready": os.path.exists(READY_FILE),
92
+ "env_file": ENV_FILE,
93
+ "metadata_ready": os.path.exists(METADATA_FILE),
94
+ },
95
+ )
96
+
97
+ def _handle_metadata(self) -> None:
98
+ if os.path.exists(METADATA_FILE):
99
+ with open(METADATA_FILE, encoding="utf-8") as handle:
100
+ metadata = json.load(handle)
101
+ self._send_json(200, {"status": "ready", "metadata": metadata})
102
+ threading.Thread(target=self.server.shutdown, daemon=True).start() # type: ignore[arg-type]
103
+ threading.Thread(target=_stop_bootstrap_service, daemon=True).start()
104
+ return
105
+
106
+ if os.path.exists(METADATA_ERROR_FILE):
107
+ with open(METADATA_ERROR_FILE, encoding="utf-8") as handle:
108
+ error_message = handle.read().strip() or "metadata generation failed"
109
+ self._send_json(500, {"status": "error", "error": error_message})
110
+ threading.Thread(target=self.server.shutdown, daemon=True).start() # type: ignore[arg-type]
111
+ threading.Thread(target=_stop_bootstrap_service, daemon=True).start()
112
+ return
113
+
114
+ self._send_json(202, {"status": "pending"})
115
+
116
+ def do_POST(self) -> None: # noqa: N802
117
+ if self._request_path() != "/configure":
118
+ self._send_json(404, {"status": "not-found"})
119
+ return
120
+
121
+ try:
122
+ content_length = int(self.headers.get("Content-Length", "0"))
123
+ except ValueError:
124
+ self._send_json(400, {"status": "bad-request", "error": "Invalid Content-Length"})
125
+ return
126
+
127
+ try:
128
+ payload = json.loads(self.rfile.read(content_length).decode("utf-8") or "{}")
129
+ except json.JSONDecodeError as error:
130
+ self._send_json(400, {"status": "bad-request", "error": f"Invalid JSON body: {error}"})
131
+ return
132
+
133
+ public_ipv4 = str(payload.get("public_ipv4") or "").strip()
134
+ public_ipv6 = str(payload.get("public_ipv6") or "").strip()
135
+ proxy_candidate = str(payload.get("proxy_url") or "").strip()
136
+ parsed_proxy = urlsplit(
137
+ proxy_candidate if "://" in proxy_candidate else f"https://{proxy_candidate}"
138
+ ) if proxy_candidate else None
139
+ proxy_hostname = parsed_proxy.hostname if parsed_proxy and parsed_proxy.hostname else proxy_candidate
140
+ admin_did = str(payload.get("admin_did") or "").strip()
141
+ webauthn_origin = str(payload.get("webauthn_origin") or "").strip()
142
+ webauthn_origin_fallbacks = str(payload.get("webauthn_origin_fallbacks") or "").strip()
143
+ no_start = bool(payload.get("no_start"))
144
+ bootstrap_package = payload.get("bootstrap_package")
145
+
146
+ if not public_ipv4:
147
+ self._send_json(400, {"status": "bad-request", "error": "public_ipv4 is required"})
148
+ return
149
+ if bootstrap_package not in (None, "") and not isinstance(bootstrap_package, dict):
150
+ self._send_json(
151
+ 400,
152
+ {"status": "bad-request", "error": "bootstrap_package must be a JSON object when provided"},
153
+ )
154
+ return
155
+
156
+ args = [
157
+ CONFIGURE_SCRIPT,
158
+ "--public-ipv4",
159
+ public_ipv4,
160
+ ]
161
+ bootstrap_package_file = None
162
+ if public_ipv6:
163
+ args.extend(["--public-ipv6", public_ipv6])
164
+ if proxy_hostname:
165
+ args.extend(["--proxy-hostname", proxy_hostname])
166
+ if admin_did:
167
+ args.extend(["--admin-did", admin_did])
168
+ if webauthn_origin:
169
+ args.extend(["--webauthn-origin", webauthn_origin])
170
+ if webauthn_origin_fallbacks:
171
+ args.extend(["--webauthn-origin-fallbacks", webauthn_origin_fallbacks])
172
+ if isinstance(bootstrap_package, dict):
173
+ with tempfile.NamedTemporaryFile(
174
+ mode="w",
175
+ encoding="utf-8",
176
+ prefix="ucan-store-bootstrap-",
177
+ suffix=".json",
178
+ dir="/run",
179
+ delete=False,
180
+ ) as handle:
181
+ json.dump(bootstrap_package, handle)
182
+ bootstrap_package_file = handle.name
183
+ args.extend(["--bootstrap-package-file", bootstrap_package_file])
184
+ if no_start:
185
+ args.append("--no-start")
186
+
187
+ try:
188
+ result = subprocess.run(
189
+ args,
190
+ check=True,
191
+ capture_output=True,
192
+ text=True,
193
+ )
194
+ except subprocess.CalledProcessError as error:
195
+ self._send_json(
196
+ 500,
197
+ {
198
+ "status": "error",
199
+ "error": error.stderr.strip() or error.stdout.strip() or str(error),
200
+ },
201
+ )
202
+ return
203
+ finally:
204
+ if bootstrap_package_file:
205
+ try:
206
+ os.remove(bootstrap_package_file)
207
+ except FileNotFoundError:
208
+ pass
209
+
210
+ _clear_metadata_state()
211
+ threading.Thread(target=_generate_metadata_files, daemon=True).start()
212
+
213
+ self._send_json(
214
+ 200,
215
+ {
216
+ "status": "configured",
217
+ "stdout": result.stdout.strip(),
218
+ "metadata_pending": True,
219
+ },
220
+ )
221
+
222
+
223
+ def main() -> None:
224
+ server = ThreadingHTTPServer(("0.0.0.0", 80), Handler)
225
+ server.serve_forever()
226
+
227
+
228
+ if __name__ == "__main__":
229
+ main()
@@ -0,0 +1,108 @@
1
+ import * as Signer from '@ucanto/principal/ed25519';
2
+ import * as AggregatorCaps from '@storacha/capabilities/filecoin/aggregator';
3
+ import {
4
+ getConnection,
5
+ getMockService,
6
+ getStoreImplementations as getFilecoinStoreImplementations,
7
+ getQueueImplementations as getFilecoinQueueImplementations,
8
+ } from '@storacha/filecoin-api/test/context/service';
9
+ import * as Email from '../../utils/email.js';
10
+ import { create as createRevocationChecker } from '../../utils/revocation.js';
11
+ import { createServer, connect } from '../../lib.js';
12
+ import { confirmConfirmationUrl } from './utils.js';
13
+ import { getServiceStorageImplementations } from '../storage/index.js';
14
+ import { getExternalServiceImplementations } from '../external-service/index.js';
15
+ import { createServiceIdentity } from './ucan-store-service-identity.js';
16
+
17
+ export const createContext = async (options = { requirePaymentPlan: false }) => {
18
+ const requirePaymentPlan = options.requirePaymentPlan;
19
+ const { id: serviceSigner } = await createServiceIdentity(process.env);
20
+ const aggregatorSigner = await Signer.generate();
21
+ const dealTrackerSigner = await Signer.generate();
22
+ const service = getMockService();
23
+ const aggregatorServiceProof = await AggregatorCaps.pieceOffer.delegate({
24
+ issuer: aggregatorSigner,
25
+ audience: serviceSigner,
26
+ with: aggregatorSigner.did(),
27
+ expiration: Infinity,
28
+ });
29
+ const dealTrackerConnection = getConnection(dealTrackerSigner, service).connection;
30
+ const serviceStores = await getServiceStorageImplementations(options);
31
+ const queuedMessages = new Map();
32
+ const {
33
+ storefront: { filecoinSubmitQueue, pieceOfferQueue },
34
+ } = getFilecoinQueueImplementations(queuedMessages);
35
+ const {
36
+ storefront: { pieceStore, receiptStore, taskStore },
37
+ } = getFilecoinStoreImplementations();
38
+ const email = Email.debug();
39
+ const externalServices = await getExternalServiceImplementations({
40
+ ...options,
41
+ serviceID: serviceSigner,
42
+ });
43
+ const serviceContext = {
44
+ id: serviceSigner,
45
+ signer: serviceSigner,
46
+ email,
47
+ requirePaymentPlan,
48
+ url: new URL('http://localhost:8787'),
49
+ ...serviceStores,
50
+ ...externalServices,
51
+ getServiceConnection: () => connection,
52
+ ...createRevocationChecker({
53
+ revocationsStorage: serviceStores.revocationsStorage,
54
+ }),
55
+ errorReporter: {
56
+ catch(error) {
57
+ if (options.assert) {
58
+ options.assert.fail(error);
59
+ } else {
60
+ throw error;
61
+ }
62
+ },
63
+ },
64
+ filecoinSubmitQueue,
65
+ pieceOfferQueue,
66
+ pieceStore,
67
+ receiptStore,
68
+ taskStore,
69
+ aggregatorInvocationConfig: {
70
+ issuer: serviceSigner,
71
+ with: serviceSigner.did(),
72
+ audience: aggregatorSigner,
73
+ proofs: [aggregatorServiceProof],
74
+ },
75
+ dealTrackerService: {
76
+ connection: dealTrackerConnection,
77
+ invocationConfig: {
78
+ issuer: serviceSigner,
79
+ with: serviceSigner.did(),
80
+ audience: dealTrackerSigner,
81
+ },
82
+ },
83
+ maxUploadSize: 5_000_000_000,
84
+ };
85
+ const connection = connect({
86
+ id: serviceContext.id,
87
+ channel: createServer(serviceContext),
88
+ });
89
+ return {
90
+ ...serviceContext,
91
+ ...serviceStores,
92
+ ...externalServices,
93
+ mail: serviceContext.email,
94
+ service: serviceContext.id,
95
+ connection,
96
+ grantAccess: (mail) => confirmConfirmationUrl(connection, mail),
97
+ fetch,
98
+ };
99
+ };
100
+
101
+ export const cleanupContext = (context) =>
102
+ Promise.all([
103
+ context.carStoreBucket.deactivate(),
104
+ context.blobsStorage.deactivate(),
105
+ context.indexingService.deactivate(),
106
+ context.claimsService.deactivate(),
107
+ ...context.storageProviders.map((provider) => provider.deactivate()),
108
+ ]);
@@ -0,0 +1,116 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import * as Signer from '@ucanto/principal/ed25519';
4
+
5
+ const DEFAULT_SIGNER_FILE = '/var/lib/ucan-store/service-ed25519.key';
6
+ const DEFAULT_ALGORITHM = 'ed25519';
7
+
8
+ const normalizeString = (value) => (typeof value === 'string' ? value.trim() : '');
9
+
10
+ const parseUrlLike = (value) => {
11
+ const normalized = normalizeString(value);
12
+ if (!normalized) {
13
+ return null;
14
+ }
15
+
16
+ try {
17
+ return new URL(normalized);
18
+ } catch {
19
+ try {
20
+ return new URL(`https://${normalized}`);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ };
26
+
27
+ const didWebFromUrlLike = (value) => {
28
+ const parsed = parseUrlLike(value);
29
+ if (!parsed || !parsed.hostname) {
30
+ return '';
31
+ }
32
+
33
+ const host = parsed.hostname.toLowerCase();
34
+ const port = parsed.port ? `%3A${parsed.port}` : '';
35
+ return `did:web:${host}${port}`;
36
+ };
37
+
38
+ const resolveServiceDid = (env, signer) => {
39
+ const explicitDid =
40
+ normalizeString(env.UCAN_STORE_SERVICE_DID) ||
41
+ normalizeString(env.PUBLIC_UPLOAD_SERVICE_DID);
42
+ if (explicitDid) {
43
+ return explicitDid;
44
+ }
45
+
46
+ const hostnameDid = didWebFromUrlLike(env.PROXY_HOSTNAME);
47
+ if (hostnameDid) {
48
+ return hostnameDid;
49
+ }
50
+
51
+ const publicUrlDid = didWebFromUrlLike(env.PUBLIC_UPLOAD_SERVICE_URL);
52
+ if (publicUrlDid) {
53
+ return publicUrlDid;
54
+ }
55
+
56
+ return signer.did();
57
+ };
58
+
59
+ const writeSignerFile = async (signerFile, signer) => {
60
+ const parentDir = path.dirname(signerFile);
61
+ const encoded = `${Signer.format(signer)}\n`;
62
+ const tempFile = `${signerFile}.tmp-${process.pid}-${Date.now()}`;
63
+
64
+ await fs.mkdir(parentDir, { recursive: true });
65
+ await fs.writeFile(tempFile, encoded, { mode: 0o600 });
66
+ await fs.rename(tempFile, signerFile);
67
+ };
68
+
69
+ export const loadOrCreateServiceSigner = async (env = process.env) => {
70
+ const algorithm = normalizeString(env.UCAN_STORE_SERVICE_KEY_ALGORITHM) || DEFAULT_ALGORITHM;
71
+ if (algorithm !== DEFAULT_ALGORITHM) {
72
+ throw new Error(
73
+ `Unsupported UCAN store service signer algorithm: ${algorithm}. Only ${DEFAULT_ALGORITHM} is currently supported.`
74
+ );
75
+ }
76
+
77
+ const signerFile = normalizeString(env.UCAN_STORE_SERVICE_SIGNER_FILE) || DEFAULT_SIGNER_FILE;
78
+
79
+ try {
80
+ const existing = normalizeString(await fs.readFile(signerFile, 'utf8'));
81
+ if (!existing) {
82
+ throw new Error(`Service signer file is empty: ${signerFile}`);
83
+ }
84
+ return {
85
+ signer: Signer.parse(existing),
86
+ signerFile,
87
+ algorithm,
88
+ created: false,
89
+ };
90
+ } catch (error) {
91
+ if (error?.code !== 'ENOENT') {
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ const signer = await Signer.generate();
97
+ await writeSignerFile(signerFile, signer);
98
+ return {
99
+ signer,
100
+ signerFile,
101
+ algorithm,
102
+ created: true,
103
+ };
104
+ };
105
+
106
+ export const createServiceIdentity = async (env = process.env) => {
107
+ const { signer, signerFile, algorithm, created } = await loadOrCreateServiceSigner(env);
108
+ const serviceDid = resolveServiceDid(env, signer);
109
+ return {
110
+ signer,
111
+ id: serviceDid === signer.did() ? signer : signer.withDID(serviceDid),
112
+ signerFile,
113
+ algorithm,
114
+ created,
115
+ };
116
+ };
@@ -0,0 +1,19 @@
1
+ [Unit]
2
+ Description=ucan-store upload service
3
+ Wants=network-online.target
4
+ After=network-online.target
5
+ ConditionPathExists=/etc/default/ucan-store.ready
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=ucan-store
10
+ Group=ucan-store
11
+ WorkingDirectory=/opt/ucan-store
12
+ EnvironmentFile=-/etc/default/ucan-store
13
+ ExecStart=/usr/local/sbin/ucan-store-service-start.sh
14
+ Restart=always
15
+ RestartSec=5
16
+ NoNewPrivileges=true
17
+
18
+ [Install]
19
+ WantedBy=multi-user.target