@le-space/rootfs 0.1.3

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.
Files changed (4) hide show
  1. package/README.md +16 -0
  2. package/index.d.ts +249 -0
  3. package/index.js +640 -0
  4. package/package.json +18 -0
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @shared-aleph/rootfs
2
+
3
+ Shared rootfs contract parsing, reference profile assets, and build helpers.
4
+
5
+ ## Current scope
6
+
7
+ - typed parsing and validation of rootfs contract JSON
8
+ - shell-env mapping compatible with the existing UC rootfs builder
9
+ - copied `uc-go-peer` reference contract and guest asset set
10
+
11
+ ## Reference assets
12
+
13
+ The first shared reference profile lives under:
14
+
15
+ - `reference/uc-go-peer/contract.json`
16
+ - `reference/uc-go-peer/rootfs/*`
package/index.d.ts ADDED
@@ -0,0 +1,249 @@
1
+ interface RootfsContractPort {
2
+ port: number;
3
+ tcp?: boolean;
4
+ udp?: boolean;
5
+ purpose?: string;
6
+ }
7
+ interface RootfsContractRootfs {
8
+ profile: string;
9
+ installMode: string;
10
+ installDir: string;
11
+ binaryPath: string;
12
+ dataDir: string;
13
+ envFile: string;
14
+ }
15
+ interface RootfsContractServices {
16
+ bootstrap: string;
17
+ main: string;
18
+ autotlsRefresh: string;
19
+ }
20
+ interface RootfsContractManifest {
21
+ copyTarget: string;
22
+ notes?: string;
23
+ }
24
+ interface RootfsContractSource {
25
+ repository?: string;
26
+ subdirectory?: string;
27
+ }
28
+ interface RootfsContract {
29
+ schemaVersion: number;
30
+ id: string;
31
+ displayName?: string;
32
+ source?: RootfsContractSource;
33
+ rootfs: RootfsContractRootfs;
34
+ services: RootfsContractServices;
35
+ ports: RootfsContractPort[];
36
+ manifest: RootfsContractManifest;
37
+ }
38
+ interface RootfsContractState {
39
+ contract: RootfsContract | null;
40
+ valid: boolean;
41
+ errors: string[];
42
+ }
43
+ declare function validateRootfsContract(input: unknown): RootfsContractState;
44
+ declare function parseRootfsContract(input: string | unknown): RootfsContract;
45
+ declare function readRootfsContractFile(path: string): Promise<RootfsContract>;
46
+ declare function contractShellEnv(contract: RootfsContract, contractPath?: string): Record<string, string>;
47
+ declare function referenceProfileRoot(profile: string): string;
48
+ declare function referenceProfileContractPath(profile: string): string;
49
+ declare function referenceProfileRootfsDir(profile: string): string;
50
+
51
+ type RootfsBuildDriver = 'auto' | 'host' | 'docker';
52
+ interface RootfsBuildOptions {
53
+ projectDir: string;
54
+ alephDir?: string;
55
+ outDir?: string;
56
+ contractPath?: string;
57
+ driver?: RootfsBuildDriver;
58
+ rootfsSizeMiB?: number;
59
+ rootfsImageSize?: string;
60
+ rootfsVersion?: string;
61
+ channel?: string;
62
+ skipUpload?: boolean;
63
+ skipBuild?: boolean;
64
+ ipfsAddUrl?: string;
65
+ ipfsGatewayUrl?: string;
66
+ alephApiHost?: string;
67
+ alephMessageWaitAttempts?: number;
68
+ alephMessageWaitDelaySeconds?: number;
69
+ alephPinAttempts?: number;
70
+ alephPinDelaySeconds?: number;
71
+ ipfsGatewayWaitAttempts?: number;
72
+ ipfsGatewayWaitDelaySeconds?: number;
73
+ gitShortSha?: string | null;
74
+ now?: Date;
75
+ }
76
+ interface RootfsBuildPlan {
77
+ contract: RootfsContract;
78
+ contractPath: string;
79
+ projectDir: string;
80
+ alephDir: string;
81
+ outDir: string;
82
+ driver: RootfsBuildDriver;
83
+ rootfsSizeMiB: number;
84
+ rootfsImageSize: string;
85
+ rootfsVersion: string;
86
+ channel: string;
87
+ skipUpload: boolean;
88
+ skipBuild: boolean;
89
+ ipfsAddUrl: string;
90
+ ipfsGatewayUrl: string;
91
+ alephApiHost: string;
92
+ alephMessageWaitAttempts: number;
93
+ alephMessageWaitDelaySeconds: number;
94
+ alephPinAttempts: number;
95
+ alephPinDelaySeconds: number;
96
+ ipfsGatewayWaitAttempts: number;
97
+ ipfsGatewayWaitDelaySeconds: number;
98
+ manifestPath: string;
99
+ latestManifestPath: string | null;
100
+ versionedManifestPath: string | null;
101
+ imagePath: string;
102
+ baseImagePath: string;
103
+ binaryPath: string;
104
+ }
105
+ declare function deriveRootfsVersion(options?: Pick<RootfsBuildOptions, 'rootfsVersion' | 'gitShortSha' | 'now'>): string;
106
+ declare function createRootfsBuildPlan(contract: RootfsContract, options: RootfsBuildOptions): RootfsBuildPlan;
107
+ declare function rootfsBuildShellEnv(plan: RootfsBuildPlan): Record<string, string>;
108
+
109
+ interface RootfsManifest {
110
+ profile: string;
111
+ version: string;
112
+ rootfsInstallStrategy: string;
113
+ requiresBootstrapNetwork: boolean;
114
+ bootstrapSummary: string;
115
+ rootfsSourceSizeBytes?: number;
116
+ requiredPortForwards: RootfsContract["ports"];
117
+ rootfsCid?: string;
118
+ rootfsItemHash?: string;
119
+ rootfsSizeMiB: number;
120
+ createdAt: string;
121
+ notes: string;
122
+ }
123
+ interface RootfsManifestOptions {
124
+ createdAt?: string;
125
+ rootfsCid?: string;
126
+ rootfsItemHash?: string;
127
+ rootfsSourceSizeBytes?: number;
128
+ }
129
+ interface RootfsManifestOutputPaths {
130
+ primaryPath: string;
131
+ copyTargetPath?: string;
132
+ versionedTargetPath?: string;
133
+ }
134
+ declare function rootfsSourceSizeBytesFromIpfsAddResponse(content: string): number | undefined;
135
+ declare function createRootfsManifest(plan: RootfsBuildPlan, contract: RootfsContract, options?: RootfsManifestOptions): RootfsManifest;
136
+ declare function resolveRootfsManifestOutputPaths(plan: RootfsBuildPlan): RootfsManifestOutputPaths;
137
+ declare function serializeRootfsManifest(manifest: RootfsManifest): string;
138
+
139
+ interface RootfsToolchainAvailability {
140
+ hasDocker: boolean;
141
+ dockerDaemonRunning?: boolean;
142
+ hasVirtCustomize: boolean;
143
+ githubActions?: boolean;
144
+ }
145
+ interface RootfsCommandPlan {
146
+ command: string;
147
+ args: string[];
148
+ env?: Record<string, string>;
149
+ workdir?: string;
150
+ }
151
+ interface RootfsExecutionPlan {
152
+ mode: "host" | "docker";
153
+ reason: string;
154
+ referenceRootfsDir: string;
155
+ prepareCommand?: RootfsCommandPlan;
156
+ runCommand: RootfsCommandPlan;
157
+ }
158
+ interface RootfsExecutionPlanOptions {
159
+ referenceRootfsDir?: string;
160
+ projectMountPath?: string;
161
+ rootfsMountPath?: string;
162
+ dockerImageTag?: string;
163
+ }
164
+ declare function createHostRootfsExecutionPlan(plan: RootfsBuildPlan, options?: RootfsExecutionPlanOptions): RootfsExecutionPlan;
165
+ declare function createDockerRootfsExecutionPlan(plan: RootfsBuildPlan, options?: RootfsExecutionPlanOptions): RootfsExecutionPlan;
166
+ declare function selectRootfsExecutionPlan(plan: RootfsBuildPlan, availability: RootfsToolchainAvailability, options?: RootfsExecutionPlanOptions): RootfsExecutionPlan;
167
+
168
+ interface RootfsIpfsAddEntry {
169
+ Name?: string;
170
+ Hash?: string;
171
+ Size?: string | number;
172
+ }
173
+ interface RootfsStoreMessageResponse {
174
+ item_hash: string;
175
+ }
176
+ interface RootfsPublicationArtifacts {
177
+ ipfsAddResponsePath: string;
178
+ storeMessagePath: string;
179
+ storeMessageStderrPath: string;
180
+ }
181
+ interface RootfsPublicationResult {
182
+ cid: string;
183
+ itemHash: string;
184
+ sourceSizeBytes?: number;
185
+ }
186
+ interface RootfsStoreMessageStatus {
187
+ status: string;
188
+ rejectionSummary?: string;
189
+ }
190
+ declare function publicationArtifacts(plan: RootfsBuildPlan): RootfsPublicationArtifacts;
191
+ declare function parseIpfsAddResponse(content: string): RootfsIpfsAddEntry[];
192
+ declare function extractRootfsCid(entries: RootfsIpfsAddEntry[]): string;
193
+ declare function extractRootfsSourceSizeBytes(entries: RootfsIpfsAddEntry[]): number | undefined;
194
+ declare function parseStoreMessageResponse(content: string): RootfsStoreMessageResponse;
195
+ declare function summarizeStoreMessageFailure(stderrContent: string): string;
196
+ declare function parseStoreMessageStatus(content: string): RootfsStoreMessageStatus;
197
+ declare function createRootfsPublicationResult(ipfsAddContent: string, storeMessageContent: string): RootfsPublicationResult;
198
+
199
+ interface RootfsBuildPipeline {
200
+ buildPlan: RootfsBuildPlan;
201
+ executionPlan: RootfsExecutionPlan;
202
+ publicationArtifacts: RootfsPublicationArtifacts;
203
+ manifestPaths: RootfsManifestOutputPaths;
204
+ }
205
+ interface RootfsFinalizeResult {
206
+ manifest: RootfsManifest;
207
+ manifestJson: string;
208
+ manifestPaths: RootfsManifestOutputPaths;
209
+ publication?: RootfsPublicationResult;
210
+ }
211
+ interface RootfsFinalizeOptions extends RootfsManifestOptions {
212
+ ipfsAddResponseContent?: string;
213
+ storeMessageContent?: string;
214
+ }
215
+ declare function createRootfsBuildPipeline(buildPlan: RootfsBuildPlan, availability: RootfsToolchainAvailability, options?: RootfsExecutionPlanOptions): RootfsBuildPipeline;
216
+ declare function createHostRootfsBuildPipeline(buildPlan: RootfsBuildPlan, options?: RootfsExecutionPlanOptions): RootfsBuildPipeline;
217
+ declare function createDockerRootfsBuildPipeline(buildPlan: RootfsBuildPlan, options?: RootfsExecutionPlanOptions): RootfsBuildPipeline;
218
+ declare function finalizeRootfsBuildPipeline(buildPlan: RootfsBuildPlan, options?: RootfsFinalizeOptions): RootfsFinalizeResult;
219
+
220
+ interface RootfsExecutedCommand {
221
+ command: string;
222
+ args: string[];
223
+ workdir?: string;
224
+ env?: Record<string, string>;
225
+ }
226
+ interface RootfsCommandRunner {
227
+ run(command: RootfsExecutedCommand): Promise<void>;
228
+ }
229
+ interface RootfsFileReader {
230
+ readText(path: string): Promise<string>;
231
+ }
232
+ interface RootfsExecutorDependencies extends RootfsCommandRunner, RootfsFileReader {
233
+ }
234
+ interface RootfsExecutionResult {
235
+ pipeline: RootfsBuildPipeline;
236
+ executedCommands: RootfsExecutedCommand[];
237
+ }
238
+ interface RootfsPublishExecutionResult extends RootfsExecutionResult {
239
+ finalized: RootfsFinalizeResult;
240
+ }
241
+ interface RootfsPublishOptions extends RootfsExecutionPlanOptions {
242
+ createdAt?: string;
243
+ referenceRootfsDir?: string;
244
+ }
245
+ declare function createRootfsScriptCommand(buildPlan: RootfsBuildPlan, referenceRootfsDir?: string): RootfsExecutedCommand;
246
+ declare function buildRootfs(buildPlan: RootfsBuildPlan, deps: RootfsCommandRunner, availability: RootfsToolchainAvailability, options?: RootfsExecutionPlanOptions): Promise<RootfsExecutionResult>;
247
+ declare function publishRootfs(buildPlan: RootfsBuildPlan, deps: RootfsExecutorDependencies, options?: RootfsPublishOptions): Promise<RootfsPublishExecutionResult>;
248
+
249
+ export { type RootfsBuildDriver, type RootfsBuildOptions, type RootfsBuildPipeline, type RootfsBuildPlan, type RootfsCommandPlan, type RootfsCommandRunner, type RootfsContract, type RootfsContractManifest, type RootfsContractPort, type RootfsContractRootfs, type RootfsContractServices, type RootfsContractSource, type RootfsContractState, type RootfsExecutedCommand, type RootfsExecutionPlan, type RootfsExecutionPlanOptions, type RootfsExecutionResult, type RootfsExecutorDependencies, type RootfsFileReader, type RootfsFinalizeOptions, type RootfsFinalizeResult, type RootfsIpfsAddEntry, type RootfsManifest, type RootfsManifestOptions, type RootfsManifestOutputPaths, type RootfsPublicationArtifacts, type RootfsPublicationResult, type RootfsPublishExecutionResult, type RootfsPublishOptions, type RootfsStoreMessageResponse, type RootfsStoreMessageStatus, type RootfsToolchainAvailability, buildRootfs, contractShellEnv, createDockerRootfsBuildPipeline, createDockerRootfsExecutionPlan, createHostRootfsBuildPipeline, createHostRootfsExecutionPlan, createRootfsBuildPipeline, createRootfsBuildPlan, createRootfsManifest, createRootfsPublicationResult, createRootfsScriptCommand, deriveRootfsVersion, extractRootfsCid, extractRootfsSourceSizeBytes, finalizeRootfsBuildPipeline, parseIpfsAddResponse, parseRootfsContract, parseStoreMessageResponse, parseStoreMessageStatus, publicationArtifacts, publishRootfs, readRootfsContractFile, referenceProfileContractPath, referenceProfileRoot, referenceProfileRootfsDir, resolveRootfsManifestOutputPaths, rootfsBuildShellEnv, rootfsSourceSizeBytesFromIpfsAddResponse, selectRootfsExecutionPlan, serializeRootfsManifest, summarizeStoreMessageFailure, validateRootfsContract };
package/index.js ADDED
@@ -0,0 +1,640 @@
1
+ // src/contract.ts
2
+ import { readFile } from "fs/promises";
3
+ import { fileURLToPath } from "url";
4
+ function asObject(value) {
5
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
6
+ }
7
+ function asString(value) {
8
+ return typeof value === "string" && value.trim() ? value.trim() : null;
9
+ }
10
+ function asNumber(value) {
11
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
12
+ }
13
+ function parsePorts(value, errors) {
14
+ if (!Array.isArray(value)) {
15
+ errors.push("ports must be an array");
16
+ return [];
17
+ }
18
+ return value.flatMap((entry, index) => {
19
+ const port = asObject(entry);
20
+ if (!port) {
21
+ errors.push(`ports[${index}] must be an object`);
22
+ return [];
23
+ }
24
+ const portNumber = asNumber(port.port);
25
+ if (portNumber == null || portNumber < 1 || portNumber > 65535) {
26
+ errors.push(`ports[${index}].port must be an integer between 1 and 65535`);
27
+ return [];
28
+ }
29
+ return [{
30
+ port: portNumber,
31
+ tcp: port.tcp === true,
32
+ udp: port.udp === true,
33
+ purpose: asString(port.purpose) ?? void 0
34
+ }];
35
+ });
36
+ }
37
+ function validateRootfsContract(input) {
38
+ const errors = [];
39
+ const payload = asObject(input);
40
+ if (!payload) {
41
+ return { contract: null, valid: false, errors: ["rootfs contract must be an object"] };
42
+ }
43
+ const rootfs = asObject(payload.rootfs);
44
+ const services = asObject(payload.services);
45
+ const manifest = asObject(payload.manifest);
46
+ const source = asObject(payload.source) ?? void 0;
47
+ const schemaVersion = asNumber(payload.schemaVersion);
48
+ if (schemaVersion == null || !Number.isInteger(schemaVersion)) {
49
+ errors.push("schemaVersion must be an integer");
50
+ }
51
+ const id = asString(payload.id);
52
+ if (!id) {
53
+ errors.push("id must be a non-empty string");
54
+ }
55
+ if (!rootfs) {
56
+ errors.push("rootfs must be an object");
57
+ }
58
+ if (!services) {
59
+ errors.push("services must be an object");
60
+ }
61
+ if (!manifest) {
62
+ errors.push("manifest must be an object");
63
+ }
64
+ const profile = asString(rootfs?.profile);
65
+ if (!profile) errors.push("rootfs.profile must be a non-empty string");
66
+ const installMode = asString(rootfs?.installMode);
67
+ if (!installMode) errors.push("rootfs.installMode must be a non-empty string");
68
+ const installDir = asString(rootfs?.installDir);
69
+ if (!installDir) errors.push("rootfs.installDir must be a non-empty string");
70
+ const binaryPath = asString(rootfs?.binaryPath) ?? "/usr/local/bin/universal-chat-go";
71
+ const dataDir = asString(rootfs?.dataDir);
72
+ if (!dataDir) errors.push("rootfs.dataDir must be a non-empty string");
73
+ const envFile = asString(rootfs?.envFile);
74
+ if (!envFile) errors.push("rootfs.envFile must be a non-empty string");
75
+ const bootstrap = asString(services?.bootstrap);
76
+ if (!bootstrap) errors.push("services.bootstrap must be a non-empty string");
77
+ const main = asString(services?.main);
78
+ if (!main) errors.push("services.main must be a non-empty string");
79
+ const autotlsRefresh = asString(services?.autotlsRefresh);
80
+ if (!autotlsRefresh) errors.push("services.autotlsRefresh must be a non-empty string");
81
+ const copyTarget = asString(manifest?.copyTarget);
82
+ if (!copyTarget) errors.push("manifest.copyTarget must be a non-empty string");
83
+ const ports = parsePorts(payload.ports, errors);
84
+ if (errors.length > 0 || !schemaVersion || !id || !profile || !installMode || !installDir || !dataDir || !envFile || !bootstrap || !main || !autotlsRefresh || !copyTarget) {
85
+ return { contract: null, valid: false, errors };
86
+ }
87
+ return {
88
+ contract: {
89
+ schemaVersion,
90
+ id,
91
+ displayName: asString(payload.displayName) ?? void 0,
92
+ source: source ? {
93
+ repository: asString(source.repository) ?? void 0,
94
+ subdirectory: asString(source.subdirectory) ?? void 0
95
+ } : void 0,
96
+ rootfs: { profile, installMode, installDir, binaryPath, dataDir, envFile },
97
+ services: { bootstrap, main, autotlsRefresh },
98
+ ports,
99
+ manifest: { copyTarget, notes: asString(manifest?.notes) ?? void 0 }
100
+ },
101
+ valid: true,
102
+ errors: []
103
+ };
104
+ }
105
+ function parseRootfsContract(input) {
106
+ const payload = typeof input === "string" ? JSON.parse(input) : input;
107
+ const result = validateRootfsContract(payload);
108
+ if (!result.valid || !result.contract) {
109
+ throw new Error(`Invalid rootfs contract: ${result.errors.join("; ")}`);
110
+ }
111
+ return result.contract;
112
+ }
113
+ async function readRootfsContractFile(path4) {
114
+ return parseRootfsContract(await readFile(path4, "utf8"));
115
+ }
116
+ function contractShellEnv(contract, contractPath = "") {
117
+ return {
118
+ ROOTFS_CONTRACT_PATH: contractPath,
119
+ ROOTFS_CONTRACT_ID: contract.id,
120
+ ROOTFS_CONTRACT_PROFILE: contract.rootfs.profile,
121
+ ROOTFS_CONTRACT_INSTALL_MODE: contract.rootfs.installMode,
122
+ ROOTFS_CONTRACT_SOURCE_SUBDIRECTORY: contract.source?.subdirectory ?? "",
123
+ ROOTFS_CONTRACT_INSTALL_DIR: contract.rootfs.installDir,
124
+ ROOTFS_CONTRACT_BINARY_PATH: contract.rootfs.binaryPath,
125
+ ROOTFS_CONTRACT_DATA_DIR: contract.rootfs.dataDir,
126
+ ROOTFS_CONTRACT_ENV_FILE: contract.rootfs.envFile,
127
+ ROOTFS_CONTRACT_MAIN_SERVICE: contract.services.main,
128
+ ROOTFS_CONTRACT_BOOTSTRAP_SERVICE: contract.services.bootstrap,
129
+ ROOTFS_CONTRACT_AUTOTLS_SERVICE: contract.services.autotlsRefresh,
130
+ ROOTFS_CONTRACT_MANIFEST_COPY_TARGET: contract.manifest.copyTarget,
131
+ ROOTFS_CONTRACT_MANIFEST_NOTES: contract.manifest.notes ?? "",
132
+ ROOTFS_CONTRACT_PORT_FORWARDS_JSON: JSON.stringify(contract.ports)
133
+ };
134
+ }
135
+ function referenceProfileRoot(profile) {
136
+ return fileURLToPath(new URL(`../reference/${profile}/`, import.meta.url));
137
+ }
138
+ function referenceProfileContractPath(profile) {
139
+ return fileURLToPath(new URL(`../reference/${profile}/contract.json`, import.meta.url));
140
+ }
141
+ function referenceProfileRootfsDir(profile) {
142
+ return fileURLToPath(new URL(`../reference/${profile}/rootfs/`, import.meta.url));
143
+ }
144
+
145
+ // src/build-plan.ts
146
+ import path from "path";
147
+ function positiveInteger(value, fallback) {
148
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
149
+ }
150
+ function deriveRootfsVersion(options = {}) {
151
+ if (options.rootfsVersion && options.rootfsVersion.trim()) {
152
+ return options.rootfsVersion.trim();
153
+ }
154
+ if (options.gitShortSha && options.gitShortSha.trim()) {
155
+ const now = options.now ?? /* @__PURE__ */ new Date();
156
+ const yyyy = String(now.getUTCFullYear());
157
+ const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
158
+ const dd = String(now.getUTCDate()).padStart(2, "0");
159
+ return `uc-go-peer-git-${yyyy}${mm}${dd}-${options.gitShortSha.trim()}`;
160
+ }
161
+ return "uc-go-peer-v0.1.0";
162
+ }
163
+ function createRootfsBuildPlan(contract, options) {
164
+ const projectDir = path.resolve(options.projectDir);
165
+ const alephDir = path.resolve(options.alephDir ?? path.join(projectDir, "go-peer/aleph"));
166
+ const outDir = path.resolve(options.outDir ?? path.join(alephDir, "dist-rootfs"));
167
+ const contractPath = path.resolve(options.contractPath ?? path.join(alephDir, "root-profiles", `${contract.id}.json`));
168
+ const rootfsVersion = deriveRootfsVersion(options);
169
+ const copyTarget = contract.manifest.copyTarget?.trim() ?? "";
170
+ const latestManifestPath = copyTarget ? path.resolve(copyTarget.startsWith("/") ? copyTarget : path.join(projectDir, copyTarget)) : null;
171
+ const versionedManifestPath = latestManifestPath ? path.join(path.dirname(latestManifestPath), `${rootfsVersion}.json`) : null;
172
+ return {
173
+ contract,
174
+ contractPath,
175
+ projectDir,
176
+ alephDir,
177
+ outDir,
178
+ driver: options.driver ?? "auto",
179
+ rootfsSizeMiB: positiveInteger(options.rootfsSizeMiB, 20480),
180
+ rootfsImageSize: options.rootfsImageSize?.trim() || "20G",
181
+ rootfsVersion,
182
+ channel: options.channel?.trim() || "ALEPH-CLOUDSOLUTIONS",
183
+ skipUpload: options.skipUpload === true,
184
+ skipBuild: options.skipBuild === true,
185
+ ipfsAddUrl: options.ipfsAddUrl?.trim() || "https://ipfs.aleph.cloud/api/v0/add",
186
+ ipfsGatewayUrl: options.ipfsGatewayUrl?.trim() || "https://ipfs.aleph.cloud/ipfs",
187
+ alephApiHost: options.alephApiHost?.trim() || "https://api2.aleph.im",
188
+ alephMessageWaitAttempts: positiveInteger(options.alephMessageWaitAttempts, 60),
189
+ alephMessageWaitDelaySeconds: positiveInteger(options.alephMessageWaitDelaySeconds, 5),
190
+ alephPinAttempts: positiveInteger(options.alephPinAttempts, 4),
191
+ alephPinDelaySeconds: positiveInteger(options.alephPinDelaySeconds, 10),
192
+ ipfsGatewayWaitAttempts: positiveInteger(options.ipfsGatewayWaitAttempts, 30),
193
+ ipfsGatewayWaitDelaySeconds: positiveInteger(options.ipfsGatewayWaitDelaySeconds, 10),
194
+ manifestPath: path.join(outDir, "rootfs-manifest.json"),
195
+ latestManifestPath,
196
+ versionedManifestPath,
197
+ imagePath: path.join(outDir, "aleph-uc-go-peer.qcow2"),
198
+ baseImagePath: path.join(outDir, "debian-12-genericcloud-amd64.qcow2"),
199
+ binaryPath: path.join(outDir, "universal-chat-go")
200
+ };
201
+ }
202
+ function rootfsBuildShellEnv(plan) {
203
+ return {
204
+ OUT_DIR: plan.outDir,
205
+ ROOTFS_CONTRACT_FILE: plan.contractPath,
206
+ ROOTFS_BUILD_DRIVER: plan.driver,
207
+ ROOTFS_SIZE_MIB: String(plan.rootfsSizeMiB),
208
+ ROOTFS_IMAGE_SIZE: plan.rootfsImageSize,
209
+ ROOTFS_VERSION: plan.rootfsVersion,
210
+ CHANNEL: plan.channel,
211
+ SKIP_UPLOAD: plan.skipUpload ? "1" : "0",
212
+ SKIP_BUILD: plan.skipBuild ? "1" : "0",
213
+ IPFS_ADD_URL: plan.ipfsAddUrl,
214
+ IPFS_GATEWAY_URL: plan.ipfsGatewayUrl,
215
+ ALEPH_API_HOST: plan.alephApiHost,
216
+ ALEPH_MESSAGE_WAIT_ATTEMPTS: String(plan.alephMessageWaitAttempts),
217
+ ALEPH_MESSAGE_WAIT_DELAY_SECONDS: String(plan.alephMessageWaitDelaySeconds),
218
+ ALEPH_PIN_ATTEMPTS: String(plan.alephPinAttempts),
219
+ ALEPH_PIN_DELAY_SECONDS: String(plan.alephPinDelaySeconds),
220
+ IPFS_GATEWAY_WAIT_ATTEMPTS: String(plan.ipfsGatewayWaitAttempts),
221
+ IPFS_GATEWAY_WAIT_DELAY_SECONDS: String(plan.ipfsGatewayWaitDelaySeconds)
222
+ };
223
+ }
224
+
225
+ // src/manifest.ts
226
+ import { dirname, extname, isAbsolute, join } from "path";
227
+ function rootfsSourceSizeBytesFromIpfsAddResponse(content) {
228
+ const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
229
+ if (lines.length === 0) {
230
+ return void 0;
231
+ }
232
+ const payload = JSON.parse(lines.at(-1) ?? "{}");
233
+ const size = payload.Size;
234
+ if (typeof size === "number" && Number.isFinite(size) && size > 0) {
235
+ return size;
236
+ }
237
+ if (typeof size === "string" && /^\d+$/u.test(size)) {
238
+ return Number(size);
239
+ }
240
+ return void 0;
241
+ }
242
+ function createRootfsManifest(plan, contract, options = {}) {
243
+ const manifest = {
244
+ profile: contract.rootfs.profile,
245
+ version: plan.rootfsVersion,
246
+ rootfsInstallStrategy: contract.rootfs.installMode,
247
+ requiresBootstrapNetwork: false,
248
+ bootstrapSummary: "Dependencies are preinstalled in the image.",
249
+ requiredPortForwards: contract.ports,
250
+ rootfsSizeMiB: plan.rootfsSizeMiB,
251
+ createdAt: options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
252
+ notes: contract.manifest.notes ?? ""
253
+ };
254
+ if (typeof options.rootfsSourceSizeBytes === "number" && Number.isFinite(options.rootfsSourceSizeBytes) && options.rootfsSourceSizeBytes > 0) {
255
+ manifest.rootfsSourceSizeBytes = options.rootfsSourceSizeBytes;
256
+ }
257
+ if (options.rootfsCid) {
258
+ manifest.rootfsCid = options.rootfsCid;
259
+ }
260
+ if (options.rootfsItemHash) {
261
+ manifest.rootfsItemHash = options.rootfsItemHash;
262
+ }
263
+ return manifest;
264
+ }
265
+ function resolveRootfsManifestOutputPaths(plan) {
266
+ const paths = {
267
+ primaryPath: plan.manifestPath
268
+ };
269
+ const copyTarget = plan.latestManifestPath;
270
+ if (!copyTarget) {
271
+ return paths;
272
+ }
273
+ const resolvedCopyTarget = isAbsolute(copyTarget) ? copyTarget : join(plan.projectDir, copyTarget);
274
+ paths.copyTargetPath = resolvedCopyTarget;
275
+ const copyTargetExt = extname(resolvedCopyTarget) || ".json";
276
+ paths.versionedTargetPath = join(dirname(resolvedCopyTarget), `${plan.rootfsVersion}${copyTargetExt}`);
277
+ return paths;
278
+ }
279
+ function serializeRootfsManifest(manifest) {
280
+ return `${JSON.stringify(manifest, null, 2)}
281
+ `;
282
+ }
283
+
284
+ // src/execution-plan.ts
285
+ import path2 from "path";
286
+ function ensurePathWithin(parent, child, label) {
287
+ const relative = path2.relative(parent, child);
288
+ if (relative.startsWith("..") || path2.isAbsolute(relative)) {
289
+ throw new Error(`${label} must be inside ${parent}`);
290
+ }
291
+ return relative || ".";
292
+ }
293
+ function containerPathForProjectFile(hostPath, projectDir, mountPath, label) {
294
+ const relative = ensurePathWithin(projectDir, hostPath, label);
295
+ return path2.posix.join(mountPath, relative.split(path2.sep).join("/"));
296
+ }
297
+ function resolveReferenceRootfsDir(contract, override) {
298
+ if (override) {
299
+ return path2.resolve(override);
300
+ }
301
+ return referenceProfileRootfsDir(contract.id);
302
+ }
303
+ function createHostRootfsExecutionPlan(plan, options = {}) {
304
+ const referenceRootfsDir = resolveReferenceRootfsDir(plan.contract, options.referenceRootfsDir);
305
+ return {
306
+ mode: "host",
307
+ reason: "Using host virt-customize/qemu-img toolchain.",
308
+ referenceRootfsDir,
309
+ runCommand: {
310
+ command: "bash",
311
+ args: [path2.join(referenceRootfsDir, "build-rootfs-image.sh")],
312
+ workdir: referenceRootfsDir,
313
+ env: {
314
+ PROJECT_DIR: plan.projectDir,
315
+ OUT_DIR: plan.outDir,
316
+ ROOTFS_CONTRACT_FILE: plan.contractPath,
317
+ ROOTFS_IMAGE_SIZE: plan.rootfsImageSize
318
+ }
319
+ }
320
+ };
321
+ }
322
+ function createDockerRootfsExecutionPlan(plan, options = {}) {
323
+ const referenceRootfsDir = resolveReferenceRootfsDir(plan.contract, options.referenceRootfsDir);
324
+ const projectMountPath = options.projectMountPath ?? "/workspace/project";
325
+ const rootfsMountPath = options.rootfsMountPath ?? "/workspace/shared-rootfs";
326
+ const dockerImageTag = options.dockerImageTag ?? `${plan.contract.id}-rootfs-builder:local`;
327
+ const containerProjectDir = projectMountPath;
328
+ const containerContractPath = containerPathForProjectFile(plan.contractPath, plan.projectDir, projectMountPath, "contractPath");
329
+ const containerOutDir = containerPathForProjectFile(plan.outDir, plan.projectDir, projectMountPath, "outDir");
330
+ return {
331
+ mode: "docker",
332
+ reason: "Using Dockerized Debian/libguestfs builder.",
333
+ referenceRootfsDir,
334
+ prepareCommand: {
335
+ command: "docker",
336
+ args: [
337
+ "build",
338
+ "--platform",
339
+ "linux/amd64",
340
+ "-t",
341
+ dockerImageTag,
342
+ "-f",
343
+ path2.join(referenceRootfsDir, "Dockerfile.rootfs"),
344
+ referenceRootfsDir
345
+ ]
346
+ },
347
+ runCommand: {
348
+ command: "docker",
349
+ args: [
350
+ "run",
351
+ "--rm",
352
+ "--privileged",
353
+ "--platform",
354
+ "linux/amd64",
355
+ "-e",
356
+ "LIBGUESTFS_BACKEND=direct",
357
+ "-e",
358
+ `ROOTFS_CONTRACT_FILE=${containerContractPath}`,
359
+ "-e",
360
+ `OUT_DIR=${containerOutDir}`,
361
+ "-e",
362
+ `ROOTFS_IMAGE_SIZE=${plan.rootfsImageSize}`,
363
+ "-e",
364
+ `PROJECT_DIR=${containerProjectDir}`,
365
+ "-v",
366
+ `${plan.projectDir}:${projectMountPath}`,
367
+ "-v",
368
+ `${referenceRootfsDir}:${rootfsMountPath}`,
369
+ "-w",
370
+ rootfsMountPath,
371
+ dockerImageTag,
372
+ "bash",
373
+ path2.posix.join(rootfsMountPath, "build-rootfs-image.sh")
374
+ ]
375
+ }
376
+ };
377
+ }
378
+ function selectRootfsExecutionPlan(plan, availability, options = {}) {
379
+ if (plan.driver === "host") {
380
+ if (!availability.hasVirtCustomize) {
381
+ throw new Error("ROOTFS_BUILD_DRIVER=host requested, but virt-customize is not available.");
382
+ }
383
+ return createHostRootfsExecutionPlan(plan, options);
384
+ }
385
+ if (plan.driver === "docker") {
386
+ if (!availability.hasDocker) {
387
+ throw new Error("ROOTFS_BUILD_DRIVER=docker requested, but docker is not available.");
388
+ }
389
+ if (availability.dockerDaemonRunning === false) {
390
+ throw new Error("ROOTFS_BUILD_DRIVER=docker requested, but the Docker daemon is not running.");
391
+ }
392
+ return createDockerRootfsExecutionPlan(plan, options);
393
+ }
394
+ if (availability.githubActions && availability.hasDocker && availability.dockerDaemonRunning !== false) {
395
+ return createDockerRootfsExecutionPlan(plan, options);
396
+ }
397
+ if (availability.hasVirtCustomize) {
398
+ return createHostRootfsExecutionPlan(plan, options);
399
+ }
400
+ if (availability.hasDocker && availability.dockerDaemonRunning !== false) {
401
+ return createDockerRootfsExecutionPlan(plan, options);
402
+ }
403
+ throw new Error("No supported rootfs build toolchain is available.");
404
+ }
405
+
406
+ // src/publication.ts
407
+ import { join as join2 } from "path";
408
+ function publicationArtifacts(plan) {
409
+ return {
410
+ ipfsAddResponsePath: join2(plan.outDir, "ipfs-add-response.jsonl"),
411
+ storeMessagePath: join2(plan.outDir, "store-message.json"),
412
+ storeMessageStderrPath: join2(plan.outDir, "store-message.stderr.log")
413
+ };
414
+ }
415
+ function parseIpfsAddResponse(content) {
416
+ return content.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
417
+ }
418
+ function extractRootfsCid(entries) {
419
+ if (entries.length === 0) {
420
+ throw new Error("No response received from the IPFS add endpoint");
421
+ }
422
+ const cid = entries.at(-1)?.Hash?.trim();
423
+ if (!cid) {
424
+ throw new Error(`IPFS add response did not include a Hash: ${JSON.stringify(entries.at(-1) ?? {})}`);
425
+ }
426
+ return cid;
427
+ }
428
+ function extractRootfsSourceSizeBytes(entries) {
429
+ const size = entries.at(-1)?.Size;
430
+ if (typeof size === "number" && Number.isFinite(size) && size > 0) {
431
+ return size;
432
+ }
433
+ if (typeof size === "string" && /^\d+$/u.test(size)) {
434
+ return Number(size);
435
+ }
436
+ return void 0;
437
+ }
438
+ function parseStoreMessageResponse(content) {
439
+ const payload = JSON.parse(content);
440
+ const itemHash = payload.item_hash?.trim();
441
+ if (!itemHash) {
442
+ throw new Error("Failed to extract Aleph item hash from store message response");
443
+ }
444
+ return { item_hash: itemHash };
445
+ }
446
+ function summarizeStoreMessageFailure(stderrContent) {
447
+ return stderrContent.trim() || "Aleph pin failed without stderr output";
448
+ }
449
+ function parseStoreMessageStatus(content) {
450
+ const payload = JSON.parse(content);
451
+ const status = payload.status?.trim() ?? "";
452
+ if (status !== "rejected") {
453
+ return { status };
454
+ }
455
+ const firstError = Array.isArray(payload.details?.errors) ? payload.details.errors[0] : void 0;
456
+ if (payload.error_code === 5 && firstError && firstError.account_balance != null && firstError.required_balance != null) {
457
+ return {
458
+ status,
459
+ rejectionSummary: `insufficient Aleph balance: account has ${firstError.account_balance}, required is ${firstError.required_balance}`
460
+ };
461
+ }
462
+ if (payload.error_code == null) {
463
+ return {
464
+ status,
465
+ rejectionSummary: JSON.stringify(payload.details ?? {})
466
+ };
467
+ }
468
+ return {
469
+ status,
470
+ rejectionSummary: `error ${payload.error_code}: ${JSON.stringify(payload.details ?? {})}`
471
+ };
472
+ }
473
+ function createRootfsPublicationResult(ipfsAddContent, storeMessageContent) {
474
+ const entries = parseIpfsAddResponse(ipfsAddContent);
475
+ const storeMessage = parseStoreMessageResponse(storeMessageContent);
476
+ return {
477
+ cid: extractRootfsCid(entries),
478
+ itemHash: storeMessage.item_hash,
479
+ sourceSizeBytes: extractRootfsSourceSizeBytes(entries)
480
+ };
481
+ }
482
+
483
+ // src/orchestration.ts
484
+ function createRootfsBuildPipeline(buildPlan, availability, options = {}) {
485
+ return {
486
+ buildPlan,
487
+ executionPlan: selectRootfsExecutionPlan(buildPlan, availability, options),
488
+ publicationArtifacts: publicationArtifacts(buildPlan),
489
+ manifestPaths: resolveRootfsManifestOutputPaths(buildPlan)
490
+ };
491
+ }
492
+ function createHostRootfsBuildPipeline(buildPlan, options = {}) {
493
+ return {
494
+ buildPlan,
495
+ executionPlan: createHostRootfsExecutionPlan(buildPlan, options),
496
+ publicationArtifacts: publicationArtifacts(buildPlan),
497
+ manifestPaths: resolveRootfsManifestOutputPaths(buildPlan)
498
+ };
499
+ }
500
+ function createDockerRootfsBuildPipeline(buildPlan, options = {}) {
501
+ return {
502
+ buildPlan,
503
+ executionPlan: createDockerRootfsExecutionPlan(buildPlan, options),
504
+ publicationArtifacts: publicationArtifacts(buildPlan),
505
+ manifestPaths: resolveRootfsManifestOutputPaths(buildPlan)
506
+ };
507
+ }
508
+ function finalizeRootfsBuildPipeline(buildPlan, options = {}) {
509
+ let publication;
510
+ if (options.ipfsAddResponseContent && options.storeMessageContent) {
511
+ publication = createRootfsPublicationResult(options.ipfsAddResponseContent, options.storeMessageContent);
512
+ }
513
+ const manifest = createRootfsManifest(buildPlan, buildPlan.contract, {
514
+ createdAt: options.createdAt,
515
+ rootfsCid: publication?.cid ?? options.rootfsCid,
516
+ rootfsItemHash: publication?.itemHash ?? options.rootfsItemHash,
517
+ rootfsSourceSizeBytes: publication?.sourceSizeBytes ?? options.rootfsSourceSizeBytes
518
+ });
519
+ return {
520
+ manifest,
521
+ manifestJson: serializeRootfsManifest(manifest),
522
+ manifestPaths: resolveRootfsManifestOutputPaths(buildPlan),
523
+ publication
524
+ };
525
+ }
526
+
527
+ // src/executor.ts
528
+ import path3 from "path";
529
+ function rootfsScriptDir(buildPlan, override) {
530
+ return override ? path3.resolve(override) : referenceProfileRootfsDir(buildPlan.contract.id);
531
+ }
532
+ function createRootfsScriptCommand(buildPlan, referenceRootfsDir) {
533
+ const scriptDir = rootfsScriptDir(buildPlan, referenceRootfsDir);
534
+ return {
535
+ command: "bash",
536
+ args: [path3.join(scriptDir, "build-rootfs.sh")],
537
+ workdir: scriptDir,
538
+ env: rootfsBuildShellEnv(buildPlan)
539
+ };
540
+ }
541
+ async function buildRootfs(buildPlan, deps, availability, options = {}) {
542
+ const pipeline = createRootfsBuildPipeline(buildPlan, availability, options);
543
+ const executedCommands = [];
544
+ if (pipeline.executionPlan.prepareCommand) {
545
+ const prepareCommand = {
546
+ command: pipeline.executionPlan.prepareCommand.command,
547
+ args: pipeline.executionPlan.prepareCommand.args,
548
+ workdir: pipeline.executionPlan.prepareCommand.workdir,
549
+ env: pipeline.executionPlan.prepareCommand.env
550
+ };
551
+ await deps.run(prepareCommand);
552
+ executedCommands.push(prepareCommand);
553
+ }
554
+ const runCommand = {
555
+ command: pipeline.executionPlan.runCommand.command,
556
+ args: pipeline.executionPlan.runCommand.args,
557
+ workdir: pipeline.executionPlan.runCommand.workdir,
558
+ env: pipeline.executionPlan.runCommand.env
559
+ };
560
+ await deps.run(runCommand);
561
+ executedCommands.push(runCommand);
562
+ return {
563
+ pipeline,
564
+ executedCommands
565
+ };
566
+ }
567
+ async function publishRootfs(buildPlan, deps, options = {}) {
568
+ const command = createRootfsScriptCommand(buildPlan, options.referenceRootfsDir);
569
+ await deps.run(command);
570
+ let ipfsAddResponseContent;
571
+ let storeMessageContent;
572
+ const publicationArtifacts2 = {
573
+ ipfsAddResponsePath: path3.join(buildPlan.outDir, "ipfs-add-response.jsonl"),
574
+ storeMessagePath: path3.join(buildPlan.outDir, "store-message.json"),
575
+ storeMessageStderrPath: path3.join(buildPlan.outDir, "store-message.stderr.log")
576
+ };
577
+ if (!buildPlan.skipUpload) {
578
+ ipfsAddResponseContent = await deps.readText(publicationArtifacts2.ipfsAddResponsePath);
579
+ storeMessageContent = await deps.readText(publicationArtifacts2.storeMessagePath);
580
+ }
581
+ const finalized = finalizeRootfsBuildPipeline(buildPlan, {
582
+ createdAt: options.createdAt,
583
+ ipfsAddResponseContent,
584
+ storeMessageContent
585
+ });
586
+ return {
587
+ pipeline: {
588
+ buildPlan,
589
+ executionPlan: {
590
+ mode: "docker",
591
+ reason: "Running shared rootfs build orchestrator script.",
592
+ referenceRootfsDir: command.workdir ?? rootfsScriptDir(buildPlan, options.referenceRootfsDir),
593
+ runCommand: {
594
+ command: command.command,
595
+ args: command.args,
596
+ workdir: command.workdir,
597
+ env: command.env
598
+ }
599
+ },
600
+ publicationArtifacts: publicationArtifacts2,
601
+ manifestPaths: finalized.manifestPaths
602
+ },
603
+ executedCommands: [command],
604
+ finalized
605
+ };
606
+ }
607
+ export {
608
+ buildRootfs,
609
+ contractShellEnv,
610
+ createDockerRootfsBuildPipeline,
611
+ createDockerRootfsExecutionPlan,
612
+ createHostRootfsBuildPipeline,
613
+ createHostRootfsExecutionPlan,
614
+ createRootfsBuildPipeline,
615
+ createRootfsBuildPlan,
616
+ createRootfsManifest,
617
+ createRootfsPublicationResult,
618
+ createRootfsScriptCommand,
619
+ deriveRootfsVersion,
620
+ extractRootfsCid,
621
+ extractRootfsSourceSizeBytes,
622
+ finalizeRootfsBuildPipeline,
623
+ parseIpfsAddResponse,
624
+ parseRootfsContract,
625
+ parseStoreMessageResponse,
626
+ parseStoreMessageStatus,
627
+ publicationArtifacts,
628
+ publishRootfs,
629
+ readRootfsContractFile,
630
+ referenceProfileContractPath,
631
+ referenceProfileRoot,
632
+ referenceProfileRootfsDir,
633
+ resolveRootfsManifestOutputPaths,
634
+ rootfsBuildShellEnv,
635
+ rootfsSourceSizeBytesFromIpfsAddResponse,
636
+ selectRootfsExecutionPlan,
637
+ serializeRootfsManifest,
638
+ summarizeStoreMessageFailure,
639
+ validateRootfsContract
640
+ };
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@le-space/rootfs",
3
+ "version": "0.1.3",
4
+ "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./index.js",
8
+ "types": "./index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "import": "./index.js"
13
+ }
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ }
18
+ }