@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
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ Shared rootfs contract parsing, reference profile assets, and build helpers.
|
|
|
9
9
|
- copied `uc-go-peer` reference contract and guest asset set
|
|
10
10
|
- copied `orbitdb-relay` reference contract and guest asset set,
|
|
11
11
|
including the delayed first-start, setup endpoint, Caddy, and AutoTLS flow
|
|
12
|
+
- added a `ucan-store` reference contract scaffold with documented service
|
|
13
|
+
ports and consumer env expectations
|
|
12
14
|
|
|
13
15
|
## Reference assets
|
|
14
16
|
|
|
@@ -18,3 +20,5 @@ Current shared reference profiles live under:
|
|
|
18
20
|
- `reference/uc-go-peer/rootfs/*`
|
|
19
21
|
- `reference/orbitdb-relay/contract.json`
|
|
20
22
|
- `reference/orbitdb-relay/rootfs/*`
|
|
23
|
+
- `reference/ucan-store/contract.json`
|
|
24
|
+
- `reference/ucan-store/rootfs/*`
|
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ucan-store Rootfs Profile
|
|
2
|
+
|
|
3
|
+
This profile is the shared `relay-button` scaffold for deploying the
|
|
4
|
+
`ucan-store` upload service VM that backs the rebranded upload wall.
|
|
5
|
+
|
|
6
|
+
Current scope:
|
|
7
|
+
|
|
8
|
+
- shared rootfs contract for the `ucan-store` profile
|
|
9
|
+
- required public VM ports for Aleph guest setup plus the proxied upload API
|
|
10
|
+
- documented browser/runtime configuration inputs used by the existing PWA
|
|
11
|
+
- a prebaked qcow2 scaffold for the current local upload service runtime
|
|
12
|
+
- guest-side setup/configure scripts that publish the service URLs and DID back to deployment tooling
|
|
13
|
+
- stable guest-side service signer persistence with an Ed25519 default and hostname-aware DID selection
|
|
14
|
+
- a guest-side request guard that narrows public UCAN invocations to the configured bootstrap envelope
|
|
15
|
+
- a guest-side admin delegation issuance API that can mint importable child delegations from the service DID
|
|
16
|
+
- a public service manifest endpoint for domain-first runtime discovery by a generic PWA
|
|
17
|
+
|
|
18
|
+
Known inputs from the current `ucan-upload-wall` sources:
|
|
19
|
+
|
|
20
|
+
- Internal upload API process port: `STORACHA_LOCAL_PORT` with default `8787`
|
|
21
|
+
- Internal bootstrap-policy request guard port: `UCAN_STORE_PROXY_PORT` with default `8788`
|
|
22
|
+
- Local WebAuthn origin inputs: `WEBAUTHN_ORIGIN`, `WEBAUTHN_ORIGIN_FALLBACKS`
|
|
23
|
+
- Browser upload-service wiring:
|
|
24
|
+
- `VITE_UPLOAD_SERVICE_URL`
|
|
25
|
+
- `VITE_UPLOAD_SERVICE_DID`
|
|
26
|
+
- `VITE_REVOCATION_URL`
|
|
27
|
+
- `VITE_REVOCATION_DID`
|
|
28
|
+
- `VITE_RECEIPTS_URL`
|
|
29
|
+
- Optional direct Helia test wiring:
|
|
30
|
+
- `VITE_HELIA_PEER_ID`
|
|
31
|
+
- `VITE_HELIA_ADDRS`
|
|
32
|
+
|
|
33
|
+
What is not implemented yet in this shared profile:
|
|
34
|
+
|
|
35
|
+
- a fixed public Helia/libp2p listener contract for direct IPFS fetches
|
|
36
|
+
- service-specific Aleph VM workflow glue for full bootstrap-package collection in `relay-button`
|
|
37
|
+
|
|
38
|
+
Current bootstrap-package support in this shared profile:
|
|
39
|
+
|
|
40
|
+
- canonical JSON bootstrap package shape accepted by the shared node deploy runner
|
|
41
|
+
- guest-side storage of the bootstrap package on the VM
|
|
42
|
+
- guest-side structural validation for:
|
|
43
|
+
- `operatorAddress`
|
|
44
|
+
- `adminDid`
|
|
45
|
+
- optional `serviceDid`
|
|
46
|
+
- `spaceDid`
|
|
47
|
+
- `rootDelegationProof`
|
|
48
|
+
- `allowedCapabilities`
|
|
49
|
+
- delegation expiration policy
|
|
50
|
+
- `pwaOrigin`
|
|
51
|
+
- `serviceOrigin`
|
|
52
|
+
- runtime consistency checks that compare:
|
|
53
|
+
- bootstrap `serviceDid` against the running service DID when provided
|
|
54
|
+
- bootstrap `serviceOrigin` against the configured public upload-service origin
|
|
55
|
+
- `ucan-store.service` startup gating that:
|
|
56
|
+
- requires a bootstrap package by default
|
|
57
|
+
- re-validates the package before the service stays up
|
|
58
|
+
- probes the live `did:web` document and rejects startup on DID/origin mismatches
|
|
59
|
+
- stable service identity handling that:
|
|
60
|
+
- persists one Ed25519 signer on the VM under `/var/lib/ucan-store`
|
|
61
|
+
- reuses that signer across restarts instead of generating a fresh test key
|
|
62
|
+
- prefers an explicit `UCAN_STORE_SERVICE_DID` override when configured
|
|
63
|
+
- otherwise derives `did:web:<proxy-hostname>` once a public hostname is assigned
|
|
64
|
+
- otherwise falls back to the persisted signer `did:key`
|
|
65
|
+
- cryptographic bootstrap proof verification that checks:
|
|
66
|
+
- root delegation signature
|
|
67
|
+
- root delegation issuer against `adminDid`
|
|
68
|
+
- root delegation audience against the configured service DID
|
|
69
|
+
- delegation lifetime (`nbf` / `exp`)
|
|
70
|
+
- coverage of the configured `allowedCapabilities` for the configured `spaceDid`
|
|
71
|
+
- request-time public invocation narrowing that rejects:
|
|
72
|
+
- capabilities outside the configured `allowedCapabilities`
|
|
73
|
+
- requests for a different `spaceDid`
|
|
74
|
+
- invocation proof trees that do not include the configured bootstrap root delegation
|
|
75
|
+
- service-side user delegation issuance that:
|
|
76
|
+
- uses the persisted service signer as issuer
|
|
77
|
+
- chains back to the stored root bootstrap delegation
|
|
78
|
+
- exports a CAR-backed `m...` proof string the current UI can import directly
|
|
79
|
+
- enforces configured capability and expiration policy
|
|
80
|
+
- can be protected with `UCAN_STORE_ADMIN_API_TOKEN`
|
|
81
|
+
- public service discovery metadata that:
|
|
82
|
+
- is exposed at `/.well-known/ucan-store.json`
|
|
83
|
+
- has a `/service-manifest.json` alias
|
|
84
|
+
- surfaces service DID/origin, PWA origin, allowed capabilities, and
|
|
85
|
+
delegation issuance metadata for runtime binding
|
|
86
|
+
|
|
87
|
+
This still does not emit protocol-native UCAN error receipts from the guard
|
|
88
|
+
layer yet; rejected requests currently fail at HTTP level before they reach the
|
|
89
|
+
underlying upload API.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "ucan-store",
|
|
4
|
+
"displayName": "UCAN Store Upload Service",
|
|
5
|
+
"source": {
|
|
6
|
+
"repository": "external",
|
|
7
|
+
"subdirectory": "."
|
|
8
|
+
},
|
|
9
|
+
"rootfs": {
|
|
10
|
+
"profile": "ucan-store",
|
|
11
|
+
"installMode": "prebaked",
|
|
12
|
+
"installDir": "/opt/ucan-store",
|
|
13
|
+
"binaryPath": "/usr/local/bin/ucan-store",
|
|
14
|
+
"dataDir": "/var/lib/ucan-store",
|
|
15
|
+
"envFile": "/etc/default/ucan-store"
|
|
16
|
+
},
|
|
17
|
+
"services": {
|
|
18
|
+
"bootstrap": "ucan-store-bootstrap.service",
|
|
19
|
+
"main": "ucan-store.service",
|
|
20
|
+
"autotlsRefresh": "ucan-store-autotls-refresh.service"
|
|
21
|
+
},
|
|
22
|
+
"ports": [
|
|
23
|
+
{
|
|
24
|
+
"port": 22,
|
|
25
|
+
"tcp": true,
|
|
26
|
+
"udp": false,
|
|
27
|
+
"purpose": "SSH"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"port": 80,
|
|
31
|
+
"tcp": true,
|
|
32
|
+
"udp": false,
|
|
33
|
+
"purpose": "Temporary setup endpoint"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"port": 443,
|
|
37
|
+
"tcp": true,
|
|
38
|
+
"udp": false,
|
|
39
|
+
"purpose": "HTTPS upload API, did:web discovery, revocation, and receipt proxy"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"manifest": {
|
|
43
|
+
"copyTarget": "web/public/rootfs/ucan-store/latest.json",
|
|
44
|
+
"notes": "The ucan-store image prebakes the current upload-wall local upload service into the Aleph guest, keeps the upstream HTTP worker on internal port 8787, exposes a temporary guest setup endpoint on port 80, runs a bootstrap-policy request guard on internal port 8788, persists one Ed25519 service signer under /var/lib/ucan-store, and proxies the public upload API, did:web document, revocation, receipt, service manifest, and optional admin delegation issuance routes through Caddy on port 443 once a proxy hostname is assigned. The admin issuance API can export CAR-backed `m...` delegation proofs for manual UI import when UCAN_STORE_ADMIN_API_TOKEN is configured, and the public `/.well-known/ucan-store.json` manifest exposes runtime binding metadata for a generic PWA. The browser consumer currently wires the service with VITE_UPLOAD_SERVICE_URL, VITE_UPLOAD_SERVICE_DID, VITE_REVOCATION_URL, VITE_REVOCATION_DID, and VITE_RECEIPTS_URL. Helia/IPFS peer exposure remains a later milestone and is not part of this base contract."
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
FROM --platform=linux/amd64 debian:12
|
|
2
|
+
|
|
3
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
4
|
+
ENV LIBGUESTFS_BACKEND=direct
|
|
5
|
+
|
|
6
|
+
RUN apt-get update \
|
|
7
|
+
&& apt-get install -y --no-install-recommends \
|
|
8
|
+
ca-certificates \
|
|
9
|
+
curl \
|
|
10
|
+
libguestfs-tools \
|
|
11
|
+
linux-image-amd64 \
|
|
12
|
+
python3 \
|
|
13
|
+
qemu-system-x86 \
|
|
14
|
+
qemu-utils \
|
|
15
|
+
tar \
|
|
16
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
17
|
+
|
|
18
|
+
WORKDIR /workspace/shared-rootfs
|
|
19
|
+
|
|
20
|
+
COPY . .
|
|
21
|
+
|
|
22
|
+
RUN chmod 0755 ./build-rootfs.sh ./build-rootfs-image.sh ./read-rootfs-contract.py \
|
|
23
|
+
./ucan-store-bootstrap.sh ./ucan-store-configure.sh ./ucan-store-bootstrap-verify.mjs ./ucan-store-request-guard.mjs ./ucan-store-describe.py ./ucan-store-service-start.sh ./ucan-store-setup-server.py \
|
|
24
|
+
./ucan_store_bootstrap_validate.py ./ucan-store-upload-api-context.js ./ucan-store-upload-api-service-identity.js
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ucan-store Rootfs Runtime
|
|
2
|
+
|
|
3
|
+
This shared rootfs profile packages the current `ucan-upload-wall` local upload
|
|
4
|
+
service into an Aleph guest image.
|
|
5
|
+
|
|
6
|
+
Runtime shape:
|
|
7
|
+
|
|
8
|
+
- `80/tcp`: temporary setup endpoint used by deployment tooling before first start
|
|
9
|
+
- `443/tcp`: public HTTPS proxy for upload API, `did:web`, revocations, and receipts
|
|
10
|
+
- internal `127.0.0.1:8787`: upstream upload API worker from `local-storacha-api`
|
|
11
|
+
- internal `127.0.0.1:8788`: bootstrap-policy request guard in front of the upload API
|
|
12
|
+
|
|
13
|
+
Service identity shape:
|
|
14
|
+
|
|
15
|
+
- default signer algorithm: `Ed25519`
|
|
16
|
+
- signer persistence: `/var/lib/ucan-store/service-ed25519.key`
|
|
17
|
+
- explicit DID override: `UCAN_STORE_SERVICE_DID`
|
|
18
|
+
- hostname-derived DID fallback: `did:web:<proxy-hostname>`
|
|
19
|
+
- no-host fallback: persisted `did:key`
|
|
20
|
+
|
|
21
|
+
Delegation issuance shape:
|
|
22
|
+
|
|
23
|
+
- admin endpoint: `POST /admin/delegations`
|
|
24
|
+
- policy endpoint: `GET /admin/delegations/policy`
|
|
25
|
+
- auth: `Authorization: Bearer $UCAN_STORE_ADMIN_API_TOKEN`
|
|
26
|
+
- export format: `m...` multibase base64 UCAN CAR proof string
|
|
27
|
+
- chaining: child delegations include the persisted bootstrap root delegation as proof
|
|
28
|
+
|
|
29
|
+
Service discovery shape:
|
|
30
|
+
|
|
31
|
+
- public manifest: `GET /.well-known/ucan-store.json`
|
|
32
|
+
- alias: `GET /service-manifest.json`
|
|
33
|
+
- purpose: expose service DID/origin, PWA origin, allowed capabilities, and
|
|
34
|
+
delegation issuance metadata for a generic domain-bound PWA
|
|
35
|
+
|
|
36
|
+
The guest publishes metadata for the browser PWA after configuration, including:
|
|
37
|
+
|
|
38
|
+
- `VITE_UPLOAD_SERVICE_URL`
|
|
39
|
+
- `VITE_UPLOAD_SERVICE_DID`
|
|
40
|
+
- `VITE_REVOCATION_URL`
|
|
41
|
+
- `VITE_REVOCATION_DID`
|
|
42
|
+
- `VITE_RECEIPTS_URL`
|
|
43
|
+
|
|
44
|
+
Not included in this base milestone:
|
|
45
|
+
|
|
46
|
+
- public Helia/libp2p exposure
|
|
47
|
+
- Filecoin/archive publishing glue
|
|
48
|
+
|
|
49
|
+
Bootstrap package handling in the current guest scaffold:
|
|
50
|
+
|
|
51
|
+
- the setup endpoint now accepts a canonical `bootstrap_package` JSON object
|
|
52
|
+
- the package is persisted on the VM for the running upload service
|
|
53
|
+
- guest metadata includes a `bootstrap_validation` summary
|
|
54
|
+
- guest metadata now also includes `bootstrap_proof_validation`
|
|
55
|
+
- guest metadata now also includes `service_identity`
|
|
56
|
+
- guest metadata now also includes `delegation_issuance`
|
|
57
|
+
- guest metadata now also includes `service_manifest`
|
|
58
|
+
- invalid package shape or runtime mismatches fail metadata publication
|
|
59
|
+
- `ucan-store.service` now verifies the persisted package again at startup and
|
|
60
|
+
refuses to keep the upload service running when the package is missing
|
|
61
|
+
(by default), malformed, or inconsistent with the runtime DID/origin
|
|
62
|
+
- the upload service now reuses one persisted Ed25519 signer on the VM instead
|
|
63
|
+
of generating a fresh test identity on every boot
|
|
64
|
+
- the request guard can now mint long-lived service-to-user delegations when an
|
|
65
|
+
admin bearer token is configured, enforcing:
|
|
66
|
+
- `allowedCapabilities`
|
|
67
|
+
- `spaceDid`
|
|
68
|
+
- `defaultUserDelegationExpiration`
|
|
69
|
+
- `maxUserDelegationExpiration`
|
|
70
|
+
- the request guard now also publishes a public runtime manifest that a generic
|
|
71
|
+
PWA can fetch via the service domain without relying on build-time-only
|
|
72
|
+
`VITE_UPLOAD_SERVICE_*` values
|
|
73
|
+
- the guest now performs cryptographic verification of the bootstrap root
|
|
74
|
+
delegation with the installed `ucanto` / Storacha packages before the
|
|
75
|
+
service is allowed to stay up
|
|
76
|
+
- public requests now pass through a local request guard that only forwards
|
|
77
|
+
UCAN invocations when:
|
|
78
|
+
- the invocation capability `with` matches the configured `spaceDid`
|
|
79
|
+
- the invocation `can` stays inside the configured `allowedCapabilities`
|
|
80
|
+
- the invocation proof tree includes the configured bootstrap root delegation
|
|
81
|
+
|
|
82
|
+
The current guest still does not expose this issuance flow through
|
|
83
|
+
`relay-button` yet, and it still does not provide signed multi-tenant manifest
|
|
84
|
+
publication; those remain follow-up steps.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
PROJECT_DIR="${PROJECT_DIR:-$(cd "${SCRIPT_DIR}/../../../../.." && pwd)}"
|
|
6
|
+
ROOTFS_CONTRACT_FILE="${ROOTFS_CONTRACT_FILE:-${PROJECT_DIR}/root-profiles/ucan-store.json}"
|
|
7
|
+
OUT_DIR="${OUT_DIR:-${PROJECT_DIR}/dist-rootfs}"
|
|
8
|
+
BASE_URL="${BASE_URL:-https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2}"
|
|
9
|
+
BASE_IMAGE="${OUT_DIR}/debian-12-genericcloud-amd64.qcow2"
|
|
10
|
+
IMAGE="${OUT_DIR}/aleph-ucan-store.qcow2"
|
|
11
|
+
APP_TAR="${OUT_DIR}/ucan-store-runtime.tar"
|
|
12
|
+
ROOTFS_IMAGE_SIZE="${ROOTFS_IMAGE_SIZE:-20G}"
|
|
13
|
+
ROOTFS_SPARSIFY="${ROOTFS_SPARSIFY:-1}"
|
|
14
|
+
HOST_UID="${HOST_UID:-}"
|
|
15
|
+
HOST_GID="${HOST_GID:-}"
|
|
16
|
+
|
|
17
|
+
require() {
|
|
18
|
+
command -v "$1" >/dev/null 2>&1 || {
|
|
19
|
+
echo "Missing required command: $1" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
require curl
|
|
25
|
+
require qemu-img
|
|
26
|
+
require virt-customize
|
|
27
|
+
require python3
|
|
28
|
+
require tar
|
|
29
|
+
|
|
30
|
+
eval "$(python3 "${SCRIPT_DIR}/read-rootfs-contract.py" "${ROOTFS_CONTRACT_FILE}")"
|
|
31
|
+
|
|
32
|
+
if [ "${ROOTFS_CONTRACT_PROFILE}" != "ucan-store" ]; then
|
|
33
|
+
echo "Only the ucan-store rootfs profile is supported, got: ${ROOTFS_CONTRACT_PROFILE}" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
if [ "${ROOTFS_CONTRACT_INSTALL_MODE}" != "prebaked" ]; then
|
|
37
|
+
echo "Only prebaked install mode is supported, got: ${ROOTFS_CONTRACT_INSTALL_MODE}" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
if [ ! -d "${PROJECT_DIR}/local-storacha-api" ]; then
|
|
41
|
+
echo "Missing local-storacha-api directory: ${PROJECT_DIR}/local-storacha-api" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
if [ ! -f "${PROJECT_DIR}/web/package.json" ]; then
|
|
45
|
+
echo "Missing web/package.json: ${PROJECT_DIR}/web/package.json" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
if [ ! -f "${PROJECT_DIR}/web/package-lock.json" ]; then
|
|
49
|
+
echo "Missing web/package-lock.json: ${PROJECT_DIR}/web/package-lock.json" >&2
|
|
50
|
+
echo "The current ucan-store rootfs scaffold expects the upload wall web app to use npm lockfiles." >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
mkdir -p "${OUT_DIR}"
|
|
55
|
+
|
|
56
|
+
echo "Building ucan-store image in prebaked mode"
|
|
57
|
+
|
|
58
|
+
if [ ! -f "${BASE_IMAGE}" ]; then
|
|
59
|
+
echo "Downloading base image from ${BASE_URL}"
|
|
60
|
+
curl --fail --show-error --location \
|
|
61
|
+
--retry 5 \
|
|
62
|
+
--retry-all-errors \
|
|
63
|
+
--retry-delay 5 \
|
|
64
|
+
--connect-timeout 20 \
|
|
65
|
+
--max-time 300 \
|
|
66
|
+
"${BASE_URL}" -o "${BASE_IMAGE}"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
cp "${BASE_IMAGE}" "${IMAGE}"
|
|
70
|
+
qemu-img resize "${IMAGE}" "${ROOTFS_IMAGE_SIZE}"
|
|
71
|
+
|
|
72
|
+
tar \
|
|
73
|
+
--exclude ".git" \
|
|
74
|
+
-C "${PROJECT_DIR}" \
|
|
75
|
+
-cf "${APP_TAR}" \
|
|
76
|
+
local-storacha-api \
|
|
77
|
+
web/package.json \
|
|
78
|
+
web/package-lock.json \
|
|
79
|
+
README.md
|
|
80
|
+
|
|
81
|
+
virt-customize \
|
|
82
|
+
-a "${IMAGE}" \
|
|
83
|
+
--mkdir "${ROOTFS_CONTRACT_INSTALL_DIR}" \
|
|
84
|
+
--mkdir "${ROOTFS_CONTRACT_DATA_DIR}" \
|
|
85
|
+
--mkdir /usr/local/share/ucan-store \
|
|
86
|
+
--copy-in "${APP_TAR}:/opt" \
|
|
87
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-bootstrap.sh:/usr/local/sbin" \
|
|
88
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-configure.sh:/usr/local/sbin" \
|
|
89
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-bootstrap-verify.mjs:/usr/local/sbin" \
|
|
90
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-request-guard.mjs:/usr/local/sbin" \
|
|
91
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-upload-api-context.js:/usr/local/share/ucan-store" \
|
|
92
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-upload-api-service-identity.js:/usr/local/share/ucan-store" \
|
|
93
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-describe.py:/usr/local/sbin" \
|
|
94
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-setup-server.py:/usr/local/sbin" \
|
|
95
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-service-start.sh:/usr/local/sbin" \
|
|
96
|
+
--copy-in "${SCRIPT_DIR}/ucan_store_bootstrap_validate.py:/usr/local/sbin" \
|
|
97
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-bootstrap.service:/etc/systemd/system" \
|
|
98
|
+
--copy-in "${SCRIPT_DIR}/ucan-store-autotls-refresh.service:/etc/systemd/system" \
|
|
99
|
+
--copy-in "${SCRIPT_DIR}/ucan-store.service:/etc/systemd/system" \
|
|
100
|
+
--run-command "tar -xf /opt/$(basename "${APP_TAR}") -C ${ROOTFS_CONTRACT_INSTALL_DIR}" \
|
|
101
|
+
--run-command "chmod 0755 /usr/local/sbin/ucan-store-bootstrap.sh" \
|
|
102
|
+
--run-command "chmod 0755 /usr/local/sbin/ucan-store-configure.sh" \
|
|
103
|
+
--run-command "chmod 0755 /usr/local/sbin/ucan-store-bootstrap-verify.mjs" \
|
|
104
|
+
--run-command "chmod 0755 /usr/local/sbin/ucan-store-request-guard.mjs" \
|
|
105
|
+
--run-command "chmod 0755 /usr/local/sbin/ucan-store-describe.py" \
|
|
106
|
+
--run-command "chmod 0755 /usr/local/sbin/ucan-store-setup-server.py" \
|
|
107
|
+
--run-command "chmod 0755 /usr/local/sbin/ucan-store-service-start.sh" \
|
|
108
|
+
--run-command "chmod 0755 /usr/local/sbin/ucan_store_bootstrap_validate.py" \
|
|
109
|
+
--run-command "INSTALL_DIR=${ROOTFS_CONTRACT_INSTALL_DIR} DATA_DIR=${ROOTFS_CONTRACT_DATA_DIR} ENV_FILE=${ROOTFS_CONTRACT_ENV_FILE} SERVICE_USER=ucan-store APP_BINARY=${ROOTFS_CONTRACT_BINARY_PATH} /usr/local/sbin/ucan-store-bootstrap.sh all" \
|
|
110
|
+
--run-command "systemctl enable ${ROOTFS_CONTRACT_BOOTSTRAP_SERVICE}" \
|
|
111
|
+
--run-command "systemctl enable ${ROOTFS_CONTRACT_AUTOTLS_SERVICE}" \
|
|
112
|
+
--run-command "systemctl enable ${ROOTFS_CONTRACT_MAIN_SERVICE}" \
|
|
113
|
+
--run-command "rm -f /opt/$(basename "${APP_TAR}")"
|
|
114
|
+
|
|
115
|
+
if [ "${ROOTFS_SPARSIFY}" = "1" ] && command -v virt-sparsify >/dev/null 2>&1; then
|
|
116
|
+
SPARSE_IMAGE="${IMAGE%.qcow2}.sparse.qcow2"
|
|
117
|
+
rm -f "${SPARSE_IMAGE}"
|
|
118
|
+
echo "Sparsifying and compressing ${IMAGE}..."
|
|
119
|
+
virt-sparsify --compress "${IMAGE}" "${SPARSE_IMAGE}"
|
|
120
|
+
mv "${SPARSE_IMAGE}" "${IMAGE}"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
echo "Rootfs image ready at ${IMAGE}"
|
|
124
|
+
|
|
125
|
+
if [ -n "${HOST_UID}" ] && [ -n "${HOST_GID}" ]; then
|
|
126
|
+
chown -R "${HOST_UID}:${HOST_GID}" "${OUT_DIR}"
|
|
127
|
+
fi
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
APP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
6
|
+
PROJECT_DIR="${PROJECT_DIR:-$(cd "${SCRIPT_DIR}/../../../../.." && pwd)}"
|
|
7
|
+
OUT_DIR="${OUT_DIR:-${APP_DIR}/dist-rootfs}"
|
|
8
|
+
ROOTFS_CONTRACT_FILE="${ROOTFS_CONTRACT_FILE:-${PROJECT_DIR}/root-profiles/ucan-store.json}"
|
|
9
|
+
ROOTFS_BUILD_DRIVER="${ROOTFS_BUILD_DRIVER:-auto}"
|
|
10
|
+
ROOTFS_SIZE_MIB="${ROOTFS_SIZE_MIB:-20480}"
|
|
11
|
+
ROOTFS_IMAGE_SIZE="${ROOTFS_IMAGE_SIZE:-20G}"
|
|
12
|
+
ROOTFS_VERSION="${ROOTFS_VERSION:-}"
|
|
13
|
+
CHANNEL="${CHANNEL:-ALEPH-CLOUDSOLUTIONS}"
|
|
14
|
+
SKIP_UPLOAD="${SKIP_UPLOAD:-0}"
|
|
15
|
+
SKIP_BUILD="${SKIP_BUILD:-0}"
|
|
16
|
+
IPFS_ADD_URL="${IPFS_ADD_URL:-https://ipfs.aleph.cloud/api/v0/add}"
|
|
17
|
+
ROOTFS_CID=""
|
|
18
|
+
ROOTFS_ITEM_HASH=""
|
|
19
|
+
|
|
20
|
+
require() {
|
|
21
|
+
command -v "$1" >/dev/null 2>&1 || {
|
|
22
|
+
echo "Missing required command: $1" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
die() {
|
|
28
|
+
echo "$*" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
load_rootfs_contract() {
|
|
33
|
+
require python3
|
|
34
|
+
[ -f "${ROOTFS_CONTRACT_FILE}" ] || die "Rootfs contract does not exist: ${ROOTFS_CONTRACT_FILE}"
|
|
35
|
+
eval "$(python3 "${SCRIPT_DIR}/read-rootfs-contract.py" "${ROOTFS_CONTRACT_FILE}")"
|
|
36
|
+
|
|
37
|
+
if [ "${ROOTFS_CONTRACT_PROFILE}" != "ucan-store" ]; then
|
|
38
|
+
die "Only the ucan-store rootfs profile is supported, got: ${ROOTFS_CONTRACT_PROFILE}"
|
|
39
|
+
fi
|
|
40
|
+
if [ "${ROOTFS_CONTRACT_INSTALL_MODE}" != "prebaked" ]; then
|
|
41
|
+
die "Only prebaked install mode is supported, got: ${ROOTFS_CONTRACT_INSTALL_MODE}"
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resolve_rootfs_version() {
|
|
46
|
+
if [ -n "${ROOTFS_VERSION}" ]; then
|
|
47
|
+
printf '%s\n' "${ROOTFS_VERSION}"
|
|
48
|
+
return
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [ -d "${PROJECT_DIR}/.git" ]; then
|
|
52
|
+
local short_sha
|
|
53
|
+
short_sha="$(git -C "${PROJECT_DIR}" rev-parse --short HEAD)"
|
|
54
|
+
local build_date
|
|
55
|
+
build_date="$(date -u +%Y%m%d)"
|
|
56
|
+
printf 'ucan-store-git-%s-%s\n' "${build_date}" "${short_sha}"
|
|
57
|
+
return
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
printf 'ucan-store-v0.1.0\n'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
build_with_host_tools() {
|
|
64
|
+
echo "Using host virt-customize/qemu-img toolchain."
|
|
65
|
+
ROOTFS_CONTRACT_FILE="${ROOTFS_CONTRACT_FILE}" \
|
|
66
|
+
OUT_DIR="${OUT_DIR}" \
|
|
67
|
+
ROOTFS_IMAGE_SIZE="${ROOTFS_IMAGE_SIZE}" \
|
|
68
|
+
PROJECT_DIR="${PROJECT_DIR}" \
|
|
69
|
+
bash "${SCRIPT_DIR}/build-rootfs-image.sh"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
build_with_docker() {
|
|
73
|
+
require docker
|
|
74
|
+
|
|
75
|
+
if ! docker info >/dev/null 2>&1; then
|
|
76
|
+
die "Docker is installed, but the Docker daemon is not running."
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
echo "Using Dockerized Debian/libguestfs builder."
|
|
80
|
+
docker build --platform linux/amd64 \
|
|
81
|
+
-t ucan-store-rootfs-builder:local \
|
|
82
|
+
-f "${SCRIPT_DIR}/Dockerfile.rootfs" \
|
|
83
|
+
"${SCRIPT_DIR}"
|
|
84
|
+
|
|
85
|
+
docker run --rm --privileged --platform linux/amd64 \
|
|
86
|
+
-e LIBGUESTFS_BACKEND=direct \
|
|
87
|
+
-e ROOTFS_CONTRACT_FILE=/workspace/shared-rootfs/input-rootfs-contract.json \
|
|
88
|
+
-e OUT_DIR=/workspace/ucan-store/dist-rootfs \
|
|
89
|
+
-e ROOTFS_IMAGE_SIZE="${ROOTFS_IMAGE_SIZE}" \
|
|
90
|
+
-e PROJECT_DIR=/workspace/ucan-store \
|
|
91
|
+
-v "${PROJECT_DIR}:/workspace/ucan-store" \
|
|
92
|
+
-v "${SCRIPT_DIR}:/workspace/shared-rootfs" \
|
|
93
|
+
-v "${ROOTFS_CONTRACT_FILE}:/workspace/shared-rootfs/input-rootfs-contract.json:ro" \
|
|
94
|
+
-w /workspace/shared-rootfs \
|
|
95
|
+
ucan-store-rootfs-builder:local \
|
|
96
|
+
/bin/bash /workspace/shared-rootfs/build-rootfs-image.sh
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sync_manifest_copy_target() {
|
|
100
|
+
local manifest_path="${OUT_DIR}/rootfs-manifest.json"
|
|
101
|
+
local copy_target="${ROOTFS_CONTRACT_MANIFEST_COPY_TARGET:-}"
|
|
102
|
+
local resolved_target
|
|
103
|
+
local target_dir
|
|
104
|
+
local versioned_target
|
|
105
|
+
|
|
106
|
+
[ -n "${copy_target}" ] || return 0
|
|
107
|
+
[ -f "${manifest_path}" ] || die "Manifest does not exist: ${manifest_path}"
|
|
108
|
+
|
|
109
|
+
if [[ "${copy_target}" = /* ]]; then
|
|
110
|
+
resolved_target="${copy_target}"
|
|
111
|
+
else
|
|
112
|
+
resolved_target="${PROJECT_DIR}/${copy_target}"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
target_dir="$(dirname "${resolved_target}")"
|
|
116
|
+
mkdir -p "${target_dir}"
|
|
117
|
+
cp "${manifest_path}" "${resolved_target}"
|
|
118
|
+
|
|
119
|
+
versioned_target="${target_dir}/${ROOTFS_VERSION}.json"
|
|
120
|
+
cp "${manifest_path}" "${versioned_target}"
|
|
121
|
+
|
|
122
|
+
echo "Copied rootfs manifest to ${resolved_target}"
|
|
123
|
+
echo "Copied versioned rootfs manifest to ${versioned_target}"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
write_manifest() {
|
|
127
|
+
local rootfs_cid="${1:-}"
|
|
128
|
+
local rootfs_item_hash="${2:-}"
|
|
129
|
+
local rootfs_source_size_bytes=""
|
|
130
|
+
|
|
131
|
+
if [ -f "${OUT_DIR}/ipfs-add-response.jsonl" ]; then
|
|
132
|
+
rootfs_source_size_bytes="$(python3 - "${OUT_DIR}/ipfs-add-response.jsonl" <<'PY'
|
|
133
|
+
import json
|
|
134
|
+
import sys
|
|
135
|
+
from pathlib import Path
|
|
136
|
+
|
|
137
|
+
lines = [line for line in Path(sys.argv[1]).read_text().splitlines() if line.strip()]
|
|
138
|
+
if not lines:
|
|
139
|
+
raise SystemExit(0)
|
|
140
|
+
|
|
141
|
+
payload = json.loads(lines[-1])
|
|
142
|
+
size = payload.get("Size")
|
|
143
|
+
if isinstance(size, str) and size.isdigit():
|
|
144
|
+
print(size)
|
|
145
|
+
elif isinstance(size, int) and size > 0:
|
|
146
|
+
print(size)
|
|
147
|
+
PY
|
|
148
|
+
)"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
echo '{'
|
|
153
|
+
echo ' "profile": "ucan-store",'
|
|
154
|
+
echo " \"version\": \"${ROOTFS_VERSION}\","
|
|
155
|
+
echo ' "rootfsInstallStrategy": "prebaked",'
|
|
156
|
+
echo ' "requiresBootstrapNetwork": false,'
|
|
157
|
+
echo ' "bootstrapSummary": "Node.js, Caddy, and the current local upload service runtime are prebaked into the image.",'
|
|
158
|
+
if [[ "${rootfs_source_size_bytes}" =~ ^[0-9]+$ ]]; then
|
|
159
|
+
echo " \"rootfsSourceSizeBytes\": ${rootfs_source_size_bytes},"
|
|
160
|
+
fi
|
|
161
|
+
printf ' "requiredPortForwards": %s,\n' "${ROOTFS_CONTRACT_PORT_FORWARDS_JSON}"
|
|
162
|
+
if [ -n "${rootfs_cid}" ]; then
|
|
163
|
+
echo " \"rootfsCid\": \"${rootfs_cid}\","
|
|
164
|
+
fi
|
|
165
|
+
if [ -n "${rootfs_item_hash}" ]; then
|
|
166
|
+
echo " \"rootfsItemHash\": \"${rootfs_item_hash}\","
|
|
167
|
+
fi
|
|
168
|
+
echo " \"rootfsSizeMiB\": ${ROOTFS_SIZE_MIB},"
|
|
169
|
+
echo " \"createdAt\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
|
|
170
|
+
printf ' "notes": "%s"\n' "${ROOTFS_CONTRACT_MANIFEST_NOTES}"
|
|
171
|
+
echo '}'
|
|
172
|
+
} > "${OUT_DIR}/rootfs-manifest.json"
|
|
173
|
+
|
|
174
|
+
echo "Rootfs manifest written to ${OUT_DIR}/rootfs-manifest.json"
|
|
175
|
+
sync_manifest_copy_target
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
upload_rootfs_to_ipfs() {
|
|
179
|
+
require curl
|
|
180
|
+
[ -f "${OUT_DIR}/aleph-ucan-store.qcow2" ] || die "Missing rootfs image: ${OUT_DIR}/aleph-ucan-store.qcow2"
|
|
181
|
+
|
|
182
|
+
curl --fail --show-error --silent \
|
|
183
|
+
-X POST \
|
|
184
|
+
-F "file=@${OUT_DIR}/aleph-ucan-store.qcow2" \
|
|
185
|
+
"${IPFS_ADD_URL}" | tee "${OUT_DIR}/ipfs-add-response.jsonl" >/dev/null
|
|
186
|
+
|
|
187
|
+
ROOTFS_CID="$(python3 - "${OUT_DIR}/ipfs-add-response.jsonl" <<'PY'
|
|
188
|
+
import json
|
|
189
|
+
import sys
|
|
190
|
+
from pathlib import Path
|
|
191
|
+
|
|
192
|
+
lines = [line for line in Path(sys.argv[1]).read_text().splitlines() if line.strip()]
|
|
193
|
+
if not lines:
|
|
194
|
+
raise SystemExit("No IPFS add response lines found")
|
|
195
|
+
payload = json.loads(lines[-1])
|
|
196
|
+
cid = payload.get("Hash")
|
|
197
|
+
if not isinstance(cid, str) or not cid.strip():
|
|
198
|
+
raise SystemExit("IPFS add response is missing Hash")
|
|
199
|
+
print(cid.strip())
|
|
200
|
+
PY
|
|
201
|
+
)"
|
|
202
|
+
|
|
203
|
+
echo "Uploaded rootfs to IPFS with CID ${ROOTFS_CID}"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
publish_store_message() {
|
|
207
|
+
[ -n "${ROOTFS_CID}" ] || die "Cannot publish rootfs STORE message without ROOTFS_CID"
|
|
208
|
+
die "Aleph STORE message publication is not wired for the shared ucan-store rootfs yet. Re-run with SKIP_UPLOAD=1 until the publication workflow is implemented."
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
load_rootfs_contract
|
|
212
|
+
ROOTFS_VERSION="$(resolve_rootfs_version)"
|
|
213
|
+
|
|
214
|
+
mkdir -p "${OUT_DIR}"
|
|
215
|
+
|
|
216
|
+
if [ "${SKIP_BUILD}" != "1" ]; then
|
|
217
|
+
case "${ROOTFS_BUILD_DRIVER}" in
|
|
218
|
+
host)
|
|
219
|
+
build_with_host_tools
|
|
220
|
+
;;
|
|
221
|
+
docker)
|
|
222
|
+
build_with_docker
|
|
223
|
+
;;
|
|
224
|
+
auto)
|
|
225
|
+
if command -v virt-customize >/dev/null 2>&1 && command -v qemu-img >/dev/null 2>&1; then
|
|
226
|
+
build_with_host_tools
|
|
227
|
+
else
|
|
228
|
+
build_with_docker
|
|
229
|
+
fi
|
|
230
|
+
;;
|
|
231
|
+
*)
|
|
232
|
+
die "Unsupported ROOTFS_BUILD_DRIVER=${ROOTFS_BUILD_DRIVER}"
|
|
233
|
+
;;
|
|
234
|
+
esac
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
if [ "${SKIP_UPLOAD}" != "1" ]; then
|
|
238
|
+
upload_rootfs_to_ipfs
|
|
239
|
+
publish_store_message
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
write_manifest "${ROOTFS_CID}" "${ROOTFS_ITEM_HASH}"
|