@dcasegr/skein 0.0.0-stage → 0.1.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 +525 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +782 -0
- package/dist/host/router.js +2411 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
// The bootstrap loader (issue #4): one loader, two sources, no genesis by code
|
|
2
|
+
// alone. An instance boots from a **system tree** (docs/BOOTSTRAP.md):
|
|
3
|
+
//
|
|
4
|
+
// bin/<name>.wasm a handler program's module (WASI), or
|
|
5
|
+
// bin/<name>.cid the CID (raw, bafkrei…) of a module the source or the kernel holds
|
|
6
|
+
// bin/<name>.json optional: the program record's {inputs, services, description}
|
|
7
|
+
// etc/config.json optional: {defaults, peers, names, collect, feeds, roles} (keys hex, $self, $infer or a provider's $<name>; feeds: feeds.ts)
|
|
8
|
+
// etc/dispatch.json [{transport?, address, prefix?, filters?, program?, fn?, …}] (#77, #143: the routes; genesis.ts DispatchSpec)
|
|
9
|
+
// etc/apps.json optional (#141, #152): {install: [<directory> | {url, hash}], routes?: [<root's own route>]}
|
|
10
|
+
// … anything else: the instance's files (SOUL.md, skills/ …)
|
|
11
|
+
// (etc/subscriptions.json, etc/routes.json, etc/reads.json — the forms before #77 and #115 — are refused, #143)
|
|
12
|
+
//
|
|
13
|
+
// The loader **pre-fills the store** with the tree's objects (git-raw blobs
|
|
14
|
+
// and trees, each hash-checked by the kernel's `putblock`; modules as raw
|
|
15
|
+
// blocks), puts a program record per bin/ module, and writes the genesis from
|
|
16
|
+
// the tree's config and routes, naming the tree — the kernel sets
|
|
17
|
+
// `main` to it when it processes the genesis. That is the state an authorised
|
|
18
|
+
// `objects` + `head` pair would have produced, written directly, and a
|
|
19
|
+
// function of the log (a replay copies the tree and gets the same state).
|
|
20
|
+
//
|
|
21
|
+
// The sources only differ in where the objects come from (`Objects`): a
|
|
22
|
+
// directory scanned into memory (A), a store file (`--boot <cid> --from`),
|
|
23
|
+
// or a verified chain packet (B, packet.ts). An instance with no tree takes
|
|
24
|
+
// the stock system in code (genesis.ts codeSystem) through the same writer; a
|
|
25
|
+
// packet with a checkpoint (#30's state record) is restored instead: its
|
|
26
|
+
// blocks put, the state pointer moved, nothing rebuilt. The router's
|
|
27
|
+
// hydration of an empty store and `skein-host add --boot/--packet` both call
|
|
28
|
+
// `boot` here.
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
33
|
+
import { dirname, join, posix, resolve } from "node:path";
|
|
34
|
+
import { gunzipSync } from "node:zlib";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
import { CID } from "multiformats/cid";
|
|
37
|
+
import { encode, parse as parseCid } from "../runtime/cid.js";
|
|
38
|
+
import { programRecord, RAW, rawCid, wasmKind } from "../runtime/programs.js";
|
|
39
|
+
import { GIT_RAW, hashBlob, hashTree, parseTree, sha1Cid } from "../runtime/tree.js";
|
|
40
|
+
import { scan } from "../dev/scan.js";
|
|
41
|
+
import { now as clockNow } from "./clock.js";
|
|
42
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
43
|
+
import { appRecordIn, mergeConfig, planInstall, planRootRoute, readStoredApp } from "./plan.js";
|
|
44
|
+
import { CLAIM_ROW, codeSystem, HANDSHAKE_ROW, rowsOf, resolveSystem, STOCK_HTTP, writeSystemGenesis } from "./genesis.js";
|
|
45
|
+
import { appIgnore } from "./install.js";
|
|
46
|
+
export const DAG_CBOR = 0x71;
|
|
47
|
+
// The module and program-record helpers live in ../runtime/programs.ts (no node APIs: the plan uses them in a browser too, #92).
|
|
48
|
+
export { programRecord, RAW, rawCid, wasmKind };
|
|
49
|
+
export const BIN = "bin";
|
|
50
|
+
export const CONFIG = "etc/config.json";
|
|
51
|
+
export const DISPATCH = "etc/dispatch.json";
|
|
52
|
+
/** The forms before #77 and #115, refused (#143: they spoke of senders). */
|
|
53
|
+
export const GONE = ["etc/subscriptions.json", "etc/routes.json", "etc/reads.json"];
|
|
54
|
+
/** #141, #152: the apps an image installs at birth, and root's own routes (ImageApps). */
|
|
55
|
+
export const APPS = "etc/apps.json";
|
|
56
|
+
/** Blocks in memory: what a scanned directory or a verified packet becomes. */
|
|
57
|
+
export class MemBlocks {
|
|
58
|
+
map = new Map();
|
|
59
|
+
async get(cid) { return this.map.get(cid.toString())?.bytes; }
|
|
60
|
+
async bytes(cid) { const b = this.map.get(cid.toString()); if (!b)
|
|
61
|
+
throw new Error(`not found: ${cid}`); return b.bytes; }
|
|
62
|
+
async has(cid) { return this.map.has(cid.toString()); }
|
|
63
|
+
async putBlock(cid, bytes) { this.map.set(cid.toString(), { cid, bytes }); }
|
|
64
|
+
}
|
|
65
|
+
/** A git object's body if it is a `<type>` object. */
|
|
66
|
+
export function gitBody(object, type) {
|
|
67
|
+
const nul = object.indexOf(0);
|
|
68
|
+
if (nul < 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
const head = Buffer.from(object.subarray(0, nul)).toString("latin1");
|
|
71
|
+
return head === `${type} ${object.length - nul - 1}` ? object.subarray(nul + 1) : undefined;
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------- sources
|
|
74
|
+
/** Source A: a directory on disk, hashed into git objects in memory (scan.ts). */
|
|
75
|
+
export async function dirSource(dir, o = {}) {
|
|
76
|
+
const objects = new MemBlocks();
|
|
77
|
+
const root = await scan(objects, dir, o);
|
|
78
|
+
return { root, objects };
|
|
79
|
+
}
|
|
80
|
+
/** An entry's key: its path, or its tarball's tree id. */
|
|
81
|
+
export const appKey = (a) => typeof a === "string" ? posix.normalize(a).replace(/\/$/, "") : a.hash;
|
|
82
|
+
const isImageApp = (a) => typeof a === "string" ? a.length > 0 : !!a && typeof a === "object" && typeof a.url === "string" && typeof a.hash === "string" && /^[0-9a-f]{40}$/.test(a.hash) && Object.keys(a).length === 2;
|
|
83
|
+
const need = async (objects, cid, what) => {
|
|
84
|
+
const b = await objects.get(cid);
|
|
85
|
+
if (!b)
|
|
86
|
+
throw new Error(`incomplete: ${what} (${cid}) is not in the source`);
|
|
87
|
+
return b;
|
|
88
|
+
};
|
|
89
|
+
/** Every git object reachable from `root` (trees, blobs, gitlinked records), each checked against its id. */
|
|
90
|
+
export async function treeObjects(objects, root) {
|
|
91
|
+
const out = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const visit = async (cid, what, tree) => {
|
|
94
|
+
if (seen.has(cid.toString()))
|
|
95
|
+
return;
|
|
96
|
+
seen.add(cid.toString());
|
|
97
|
+
if (cid.code !== GIT_RAW)
|
|
98
|
+
throw new Error(`${what}: ${cid} is not a git object`);
|
|
99
|
+
const bytes = await need(objects, cid, what);
|
|
100
|
+
if (!Buffer.from(createHash("sha1").update(bytes).digest()).equals(Buffer.from(cid.multihash.digest)))
|
|
101
|
+
throw new Error(`hash mismatch: ${what} (${cid})`);
|
|
102
|
+
if (!tree) {
|
|
103
|
+
out.push({ cid, bytes });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const entries = parseTree(bytes, cid);
|
|
107
|
+
out.push({ cid, bytes, entries });
|
|
108
|
+
for (const e of entries)
|
|
109
|
+
await visit(e.cid, `${what}/${e.name}`, e.mode === "40000");
|
|
110
|
+
};
|
|
111
|
+
await visit(root, "", true);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** Read a system tree out of `objects` (docs/BOOTSTRAP.md): its objects, programs, modules, config and subscriptions. */
|
|
115
|
+
export async function readSystemTree(objects, root) {
|
|
116
|
+
const all = await treeObjects(objects, root);
|
|
117
|
+
const byCid = new Map(all.map((x) => [x.cid.toString(), x]));
|
|
118
|
+
const entriesOf = (cid) => cid ? byCid.get(cid.toString())?.entries : undefined;
|
|
119
|
+
const at = (path) => {
|
|
120
|
+
let dir = root, found;
|
|
121
|
+
for (const seg of path.split("/")) {
|
|
122
|
+
found = entriesOf(dir)?.find((e) => e.name === seg);
|
|
123
|
+
if (!found)
|
|
124
|
+
return undefined;
|
|
125
|
+
dir = found.mode === "40000" ? found.cid : undefined;
|
|
126
|
+
}
|
|
127
|
+
return found;
|
|
128
|
+
};
|
|
129
|
+
const text = (e, path) => {
|
|
130
|
+
const b = gitBody(byCid.get(e.cid.toString()).bytes, "blob");
|
|
131
|
+
if (!b)
|
|
132
|
+
throw new Error(`${path}: not a file`);
|
|
133
|
+
return Buffer.from(b).toString("utf8");
|
|
134
|
+
};
|
|
135
|
+
const json = (path) => {
|
|
136
|
+
const e = at(path);
|
|
137
|
+
if (!e)
|
|
138
|
+
return undefined;
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(text(e, path));
|
|
141
|
+
}
|
|
142
|
+
catch (x) {
|
|
143
|
+
throw new Error(`${path}: ${x.message}`);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const bin = at(BIN);
|
|
147
|
+
const programs = [];
|
|
148
|
+
const modules = [];
|
|
149
|
+
const binEntries = bin?.mode === "40000" ? entriesOf(bin.cid) ?? [] : [];
|
|
150
|
+
const names = [...new Set(binEntries.map((e) => /^(.+)\.(wasm|cid)$/.exec(e.name)?.[1]).filter((n) => !!n))].sort();
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
const wasm = binEntries.find((e) => e.name === `${name}.wasm`), cidFile = binEntries.find((e) => e.name === `${name}.cid`);
|
|
153
|
+
if (wasm && cidFile)
|
|
154
|
+
throw new Error(`bin/${name}: both a .wasm and a .cid`);
|
|
155
|
+
let module;
|
|
156
|
+
if (wasm) {
|
|
157
|
+
const bytes = gitBody(byCid.get(wasm.cid.toString()).bytes, "blob");
|
|
158
|
+
// A preview1 core module (version 1) or a WASI 0.2 component (#34: version 0x0d, layer 1): stored as a raw
|
|
159
|
+
// block either way; the kernel's runner tells them apart by this preamble when it runs one.
|
|
160
|
+
if (wasmKind(bytes) === undefined)
|
|
161
|
+
throw new Error(`bin/${name}.wasm: not a wasm module or component`);
|
|
162
|
+
module = rawCid(bytes);
|
|
163
|
+
modules.push({ cid: module, bytes: new Uint8Array(bytes), name });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const t = text(cidFile, `bin/${name}.cid`).trim();
|
|
167
|
+
try {
|
|
168
|
+
module = parseCid(t);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
throw new Error(`bin/${name}.cid: not a CID: ${JSON.stringify(t)}`);
|
|
172
|
+
}
|
|
173
|
+
if (module.code !== RAW)
|
|
174
|
+
throw new Error(`bin/${name}.cid: ${module} is not a raw module CID`);
|
|
175
|
+
modules.push({ cid: module, bytes: await objects.get(module), name });
|
|
176
|
+
}
|
|
177
|
+
const meta = json(`${BIN}/${name}.json`) ?? {};
|
|
178
|
+
programs.push({ name, module, record: programRecord(name, module, meta) });
|
|
179
|
+
}
|
|
180
|
+
for (const g of GONE)
|
|
181
|
+
if (at(g))
|
|
182
|
+
throw new Error(`${g}: the form before #77/#115 is gone (#143: it spoke of senders) — name routes in ${DISPATCH}`);
|
|
183
|
+
const dispatch = json(DISPATCH);
|
|
184
|
+
if (dispatch === undefined)
|
|
185
|
+
throw new Error(`${DISPATCH}: missing (a system tree names its routes)`);
|
|
186
|
+
if (!Array.isArray(dispatch))
|
|
187
|
+
throw new Error(`${DISPATCH}: not a list`);
|
|
188
|
+
const config = json(CONFIG) ?? {};
|
|
189
|
+
if (config.defaults && Object.values(config.defaults).some((v) => typeof v !== "string"))
|
|
190
|
+
throw new Error(`${CONFIG}: every default is a string`);
|
|
191
|
+
const apps = json(APPS);
|
|
192
|
+
checkImageApps(apps);
|
|
193
|
+
const appTrees = {};
|
|
194
|
+
for (const a of apps?.install ?? []) {
|
|
195
|
+
if (typeof a !== "string")
|
|
196
|
+
continue;
|
|
197
|
+
const e = at(appKey(a));
|
|
198
|
+
if (e?.mode === "40000")
|
|
199
|
+
appTrees[appKey(a)] = e.cid;
|
|
200
|
+
}
|
|
201
|
+
return { ...(apps ? { apps, appTrees } : {}), root, objects: all.map(({ cid, bytes }) => ({ cid, bytes })), programs, modules, config, dispatch };
|
|
202
|
+
}
|
|
203
|
+
/** etc/apps.json's shape (#141, #152), refused with what it wants. */
|
|
204
|
+
export function checkImageApps(apps) {
|
|
205
|
+
if (apps === undefined)
|
|
206
|
+
return;
|
|
207
|
+
if (!apps || !Array.isArray(apps.install) || !apps.install.every(isImageApp) || (apps.routes !== undefined && !Array.isArray(apps.routes)))
|
|
208
|
+
throw new Error(`${APPS}: want {install: [<directory, relative to the image> | {url: <tarball>, hash: <its tree's git id, 40 hex>}], routes?: [<root's own route>]}`);
|
|
209
|
+
if (apps.reads !== undefined)
|
|
210
|
+
throw new Error(`${APPS}: reads: gone (#143) — root's own read is a route in \`routes\` (filters only)`);
|
|
211
|
+
const keys = apps.install.map(appKey);
|
|
212
|
+
const twice = keys.filter((k, i) => keys.indexOf(k) !== i);
|
|
213
|
+
if (twice.length)
|
|
214
|
+
throw new Error(`${APPS}: ${twice.join(", ")} listed twice`);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Boot an empty store through its kernel: pre-fill and write the genesis (a
|
|
218
|
+
* tree, or the stock system), or restore a checkpoint. The kernel processes
|
|
219
|
+
* the genesis when it starts (the dispatch rows; `main` → the tree).
|
|
220
|
+
*/
|
|
221
|
+
export async function boot(k, src, c, time = clockNow()) {
|
|
222
|
+
if (await k.store.log.tip())
|
|
223
|
+
throw new Error("the store already has a log: boot only into an empty store");
|
|
224
|
+
if (src.kind === "checkpoint") {
|
|
225
|
+
for (const b of src.blocks)
|
|
226
|
+
await k.putBlock(b.cid, b.bytes);
|
|
227
|
+
await k.restore(src.state);
|
|
228
|
+
return { state: src.state, objects: src.blocks.length, programs: [] };
|
|
229
|
+
}
|
|
230
|
+
// A tree takes from the kernel only the front door, unless its bin/ has one (#70: the middleware every provider's answer comes in through, as `local`); the other pinned records would sit in the store unreferenced (a replay would not reproduce them). No shell (#83): the shell is an app.
|
|
231
|
+
if (src.kind === "code") {
|
|
232
|
+
const kernelPrograms = await k.call("programs", c.mailbox ? ["frontdoor", "messagebox"] : undefined);
|
|
233
|
+
const entry = await writeSystemGenesis(k, c, codeSystem(c, kernelPrograms), time);
|
|
234
|
+
return { entry, objects: 0, programs: Object.keys(kernelPrograms) };
|
|
235
|
+
}
|
|
236
|
+
const t = await readSystemTree(src.objects, src.root);
|
|
237
|
+
const kernelPrograms = t.programs.some((p) => p.name === "frontdoor") ? {} : await k.call("programs", ["frontdoor"]);
|
|
238
|
+
let n = 0;
|
|
239
|
+
for (const o of t.objects)
|
|
240
|
+
if (!(await k.hasBlock(o.cid))) {
|
|
241
|
+
await k.putBlock(o.cid, o.bytes);
|
|
242
|
+
n++;
|
|
243
|
+
}
|
|
244
|
+
for (const m of t.modules) {
|
|
245
|
+
if (m.bytes) {
|
|
246
|
+
if (!(await k.hasBlock(m.cid))) {
|
|
247
|
+
await k.putBlock(m.cid, m.bytes);
|
|
248
|
+
n++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (!(await k.hasBlock(m.cid)))
|
|
252
|
+
throw new Error(`incomplete: bin/${m.name}.cid names ${m.cid}, which neither the source nor the kernel holds`);
|
|
253
|
+
}
|
|
254
|
+
// The front door is the kernel's when the tree brings none; every handler comes from bin/
|
|
255
|
+
const programs = { ...(kernelPrograms.frontdoor ? { frontdoor: kernelPrograms.frontdoor } : {}) };
|
|
256
|
+
for (const p of t.programs)
|
|
257
|
+
programs[p.name] = await k.store.put(p.record);
|
|
258
|
+
// A tree that names no http route takes the stock ones (the handshake, the messagebox's, the explorer), as a tree
|
|
259
|
+
// with no etc/routes.json did before #143; each is dropped when the tree lacks its program.
|
|
260
|
+
// The handshake route is the transport's own: a tree that names http routes of its own gets it too.
|
|
261
|
+
const specs = t.dispatch.some((r) => r.transport === "http")
|
|
262
|
+
? [...t.dispatch, ...(t.dispatch.some((r) => r.transport === "http" && r.address === HANDSHAKE_ROW.address) ? [] : [HANDSHAKE_ROW])]
|
|
263
|
+
: [...t.dispatch, ...STOCK_HTTP];
|
|
264
|
+
let s = resolveSystem(c, programs, t.config, t.root, specs);
|
|
265
|
+
const isClaim = (r) => r.program === "kernel" && r.fn === "claim";
|
|
266
|
+
// An image (#89, #143): no root, so it must be claimable — its routes carry the claim route.
|
|
267
|
+
if (!c.root?.length && !s.dispatch.some(isClaim))
|
|
268
|
+
throw new Error(`an image (a genesis with no root) needs a claim route in ${DISPATCH}: ${JSON.stringify(CLAIM_ROW)}`);
|
|
269
|
+
// An image booted with root named (#142: the host skein): born as a claim would leave it — no claim route.
|
|
270
|
+
if (c.root?.length && s.dispatch.some(isClaim))
|
|
271
|
+
s = { ...s, dispatch: s.dispatch.filter((r) => !isClaim(r)) };
|
|
272
|
+
let apps;
|
|
273
|
+
if (t.apps) {
|
|
274
|
+
const born = await installAtBirth(k, src.objects, t, s, c, src.apps);
|
|
275
|
+
s = { ...s, dispatch: [...s.dispatch, ...born.rows], heads: born.heads };
|
|
276
|
+
n += born.put;
|
|
277
|
+
apps = born.apps;
|
|
278
|
+
}
|
|
279
|
+
const entry = await writeSystemGenesis(k, c, s, time);
|
|
280
|
+
return { entry, tree: t.root, objects: n, programs: Object.keys(programs), ...(apps ? { apps } : {}) };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* #141, #152: the apps an image installs at birth (etc/apps.json), each a directory install — the
|
|
284
|
+
* app's tree (in the image; a directory beside it, `resolved` by the source; a tarball fetched and
|
|
285
|
+
* checked, appTarball), its objects put — planned as an install by messages would be (plan.ts:
|
|
286
|
+
* readStoredApp over the app's tree, planInstall) against the instance as its genesis will stand —
|
|
287
|
+
* the same records, routes and heads — and root's own routes (planRootRoute). An app's config given
|
|
288
|
+
* at creation (`appConfig`) is merged over its manifest's, as an install's `--config` is. Their
|
|
289
|
+
* objects and records are put into the store; the routes and heads go into the genesis.
|
|
290
|
+
*/
|
|
291
|
+
export async function installAtBirth(k, imageObjects, t, s, c, resolved = {}) {
|
|
292
|
+
const fetched = new MemBlocks();
|
|
293
|
+
const objects = anyOf(imageObjects, fetched);
|
|
294
|
+
const local = new MemBlocks();
|
|
295
|
+
const bytes = async (cid) => {
|
|
296
|
+
const b = local.map.get(cid.toString())?.bytes ?? await objects.get(cid);
|
|
297
|
+
if (!b)
|
|
298
|
+
throw new Error(`not found: ${cid}`);
|
|
299
|
+
return b;
|
|
300
|
+
};
|
|
301
|
+
const store = {
|
|
302
|
+
bytes,
|
|
303
|
+
has: async (cid) => local.map.has(cid.toString()) || await k.hasBlock(cid),
|
|
304
|
+
get: async (cid) => dagCbor.decode(await bytes(cid)),
|
|
305
|
+
putBlock: async (cid, b) => { local.map.set(cid.toString(), { cid, bytes: b }); },
|
|
306
|
+
};
|
|
307
|
+
const view = {
|
|
308
|
+
store, identity: c.identity, programs: s.programs,
|
|
309
|
+
addressBook: (c.addressBook ?? []).map((e) => ({ key: Buffer.from(e.key).toString("hex"), transport: e.transport, address: e.address, ...(e.handle ? { handle: e.handle } : {}), ...(e.domain ? { domain: e.domain } : {}) })),
|
|
310
|
+
heads: [], dispatch: [...s.dispatch],
|
|
311
|
+
};
|
|
312
|
+
const rows = [];
|
|
313
|
+
const heads = new Map();
|
|
314
|
+
let put = 0;
|
|
315
|
+
const put1 = async (r) => { if (!(await k.hasBlock(r.cid))) {
|
|
316
|
+
await k.putBlock(r.cid, r.bytes);
|
|
317
|
+
put++;
|
|
318
|
+
} };
|
|
319
|
+
const apps = [];
|
|
320
|
+
for (const entry of t.apps.install) {
|
|
321
|
+
const key = appKey(entry);
|
|
322
|
+
let tree = t.appTrees?.[key] ?? resolved[key];
|
|
323
|
+
if (!tree && typeof entry === "string")
|
|
324
|
+
throw new Error(`${APPS}: ${entry} is not a directory of the image (a directory beside it is read when the image is booted from its directory)`);
|
|
325
|
+
if (!tree)
|
|
326
|
+
tree = await appTarball(entry, imageObjects, fetched);
|
|
327
|
+
if (!t.appTrees?.[key])
|
|
328
|
+
for (const o of await treeObjects(objects, tree))
|
|
329
|
+
await put1(o);
|
|
330
|
+
const app = await readStoredApp(store, tree);
|
|
331
|
+
// #142: an app's config at birth (the host skein's onboard: its domain, origin, name and note from host.env), merged as `--config` is.
|
|
332
|
+
const config = c.appConfig?.[app.checked.manifest.name];
|
|
333
|
+
if (config)
|
|
334
|
+
app.checked.manifest.config = mergeConfig(app.checked.manifest.config, config);
|
|
335
|
+
const p = await planInstall(app, view, { modules: objects });
|
|
336
|
+
if (p.start)
|
|
337
|
+
throw new Error(`${APPS}: ${p.app}: its start message is root's, sent by an install by message; a genesis carries no message (install ${p.app} after the claim)`);
|
|
338
|
+
for (const r of p.records) {
|
|
339
|
+
local.map.set(r.cid.toString(), r);
|
|
340
|
+
await put1(r);
|
|
341
|
+
}
|
|
342
|
+
for (const h of p.heads) {
|
|
343
|
+
heads.set(h.name, h.tree);
|
|
344
|
+
view.heads = [...view.heads.filter((x) => x.name !== h.name), { name: h.name, root: h.tree }];
|
|
345
|
+
}
|
|
346
|
+
for (const r of p.rows) {
|
|
347
|
+
rows.push(r.row);
|
|
348
|
+
view.dispatch.push(r.row);
|
|
349
|
+
}
|
|
350
|
+
apps.push({ name: p.app, version: p.version, notes: p.notes });
|
|
351
|
+
}
|
|
352
|
+
// #147: a root route's handler may be `<app>.<role>` — an app installed above, its program record (as `skein routes add`).
|
|
353
|
+
const named = { ...s.programs };
|
|
354
|
+
for (const r of t.apps.routes ?? []) {
|
|
355
|
+
const role = typeof r.program === "string" ? /^([a-z0-9][a-z0-9-]*)\.([A-Za-z0-9_-]+)$/.exec(r.program) : null;
|
|
356
|
+
if (!role || named[r.program])
|
|
357
|
+
continue;
|
|
358
|
+
const app = await appRecordIn(view, role[1]);
|
|
359
|
+
if (!app)
|
|
360
|
+
throw new Error(`${APPS}: route ${r.address}: handler ${r.program}: no app ${role[1]} installed at birth`);
|
|
361
|
+
const cid = app.record.programs[role[2]];
|
|
362
|
+
if (!cid)
|
|
363
|
+
throw new Error(`${APPS}: route ${r.address}: handler ${r.program}: app ${role[1]} has no program ${role[2]} (${Object.keys(app.record.programs).join(", ")})`);
|
|
364
|
+
named[r.program] = cid;
|
|
365
|
+
}
|
|
366
|
+
for (const r of rowsOf(t.apps.routes ?? [], named)) {
|
|
367
|
+
const p = await planRootRoute(view, "add", r);
|
|
368
|
+
for (const x of p.rows) {
|
|
369
|
+
rows.push(x.row);
|
|
370
|
+
view.dispatch.push(x.row);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { rows, heads: Object.fromEntries(heads), put, apps };
|
|
374
|
+
}
|
|
375
|
+
/** The program records' CIDs a tree's bin/ yields (as `boot` puts them), without a kernel. */
|
|
376
|
+
export function programCid(record) { return encode(record).cid; }
|
|
377
|
+
/** The stock system as files (`skein-host system <dir>`): bin/*.cid + bin/*.json from the kernel's records, etc/. */
|
|
378
|
+
export async function stockSystemFiles(k) {
|
|
379
|
+
const programs = await k.call("programs");
|
|
380
|
+
const files = {};
|
|
381
|
+
for (const [name, cid] of Object.entries(programs)) {
|
|
382
|
+
const r = await k.store.get(cid);
|
|
383
|
+
files[`${BIN}/${name}.cid`] = `${r.code.wasm.toString()}\n`;
|
|
384
|
+
files[`${BIN}/${name}.json`] = `${JSON.stringify({ inputs: r.inputs, services: r.services, description: r.description }, null, 2)}\n`;
|
|
385
|
+
}
|
|
386
|
+
const { DEFAULTS } = await import("../runtime/log.js");
|
|
387
|
+
const { STOCK_DISPATCH, STOCK_ROLES, STOCK_SCOPES } = await import("./genesis.js");
|
|
388
|
+
files[CONFIG] = `${JSON.stringify({ defaults: DEFAULTS, collect: ["completions"], scopes: STOCK_SCOPES, roles: STOCK_ROLES }, null, 2)}\n`;
|
|
389
|
+
// The stock routes (#77, #143): the boxes, the events and the HTTP routes (the admin routes are every genesis's).
|
|
390
|
+
files[DISPATCH] = `${JSON.stringify(STOCK_DISPATCH, null, 2)}\n`;
|
|
391
|
+
return files;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Boot a store file that no router holds yet (`skein-host add --boot/--packet`,
|
|
395
|
+
* tests): start its kernel, `boot`, stop it. The router then hydrates it as
|
|
396
|
+
* any other store; the kernel processes the genesis at that first start.
|
|
397
|
+
*/
|
|
398
|
+
export async function bootStore(o, src, c, time) {
|
|
399
|
+
const { Kernel, KernelHost } = await import("./kernel.js");
|
|
400
|
+
const { mkdirSync } = await import("node:fs");
|
|
401
|
+
const { dirname } = await import("node:path");
|
|
402
|
+
const { parseRocksSpec } = await import("../runtime/index-store.js");
|
|
403
|
+
// #158: a RocksDB set boots in the host's one kernel process (this node process's, shared with its router, or one of its own).
|
|
404
|
+
const rocks = parseRocksSpec(o.db);
|
|
405
|
+
// The settled split: a SQLite set boots on the wasm kernel (this node process's machine for it).
|
|
406
|
+
const { parseSqliteSpec } = await import("../runtime/index-store.js");
|
|
407
|
+
const sq = parseSqliteSpec(o.db);
|
|
408
|
+
if (sq) {
|
|
409
|
+
const { WasmHost } = await import("./wasm-kernel.js");
|
|
410
|
+
const net = o.env?.SKEIN_NETWORK ?? process.env.SKEIN_NETWORK;
|
|
411
|
+
const host = WasmHost.acquire(sq.db, { network: net ?? "main", log: o.log });
|
|
412
|
+
try {
|
|
413
|
+
const k = await host.open({ db: o.db, handle: o.handle, domain: o.domain, log: o.log });
|
|
414
|
+
try {
|
|
415
|
+
return await boot(k, src, c, time);
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
await k.stop();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
await host.release();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (rocks) {
|
|
426
|
+
const host = KernelHost.acquire(rocks.dir, { command: o.command, env: o.env, log: o.log });
|
|
427
|
+
try {
|
|
428
|
+
const k = await host.open({ db: o.db, handle: o.handle, domain: o.domain, log: o.log });
|
|
429
|
+
try {
|
|
430
|
+
return await boot(k, src, c, time);
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
await k.stop();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await host.release();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (!o.db.startsWith("lmdb:"))
|
|
441
|
+
mkdirSync(dirname(o.db), { recursive: true }); // an LMDB set (#96): the kernel makes its environment
|
|
442
|
+
const k = new Kernel({ db: o.db, handle: o.handle, domain: o.domain, command: o.command, env: o.env, log: o.log });
|
|
443
|
+
try {
|
|
444
|
+
return await boot(k, src, c, time);
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
await k.stop(5000);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// ---------------------------------------------------------------- more sources
|
|
451
|
+
/** Blocks out of a store (a store file opened read-only, or anything with `bytes`). */
|
|
452
|
+
export function storeObjects(s) {
|
|
453
|
+
// A block the store does not have (or cannot read) is absent: the loader refuses what names it, with the CID.
|
|
454
|
+
return { get: async (cid) => { try {
|
|
455
|
+
return await s.bytes(cid);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return undefined;
|
|
459
|
+
} } };
|
|
460
|
+
}
|
|
461
|
+
/** The first source that has the block. */
|
|
462
|
+
export function anyOf(...sources) {
|
|
463
|
+
return { get: async (cid) => { for (const s of sources) {
|
|
464
|
+
const b = await s.get(cid);
|
|
465
|
+
if (b)
|
|
466
|
+
return b;
|
|
467
|
+
} return undefined; } };
|
|
468
|
+
}
|
|
469
|
+
/** The modules in a wasm directory (the repo's wasm/: the pinned builds), by their raw CIDs; hashed on first use. */
|
|
470
|
+
export function wasmDirObjects(dir) {
|
|
471
|
+
let byCid;
|
|
472
|
+
return {
|
|
473
|
+
get: async (cid) => {
|
|
474
|
+
if (cid.code !== RAW)
|
|
475
|
+
return undefined;
|
|
476
|
+
const { readdirSync, readFileSync } = await import("node:fs");
|
|
477
|
+
const { join } = await import("node:path");
|
|
478
|
+
if (!byCid) {
|
|
479
|
+
byCid = new Map();
|
|
480
|
+
for (const f of readdirSync(dir))
|
|
481
|
+
if (f.endsWith(".wasm") || f.endsWith(".zip"))
|
|
482
|
+
byCid.set(rawCid(readFileSync(join(dir, f))).toString(), join(dir, f));
|
|
483
|
+
}
|
|
484
|
+
const p = byCid.get(cid.toString());
|
|
485
|
+
return p ? new Uint8Array(readFileSync(p)) : undefined;
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
490
|
+
/** The repo's pinned modules (wasm/): what a system tree's `bin/*.cid` names. */
|
|
491
|
+
export const WASM_DIR = join(ROOT, "wasm");
|
|
492
|
+
/** The default image (#89): a user skein's — no root in it, a claim route. */
|
|
493
|
+
export const DEFAULT_IMAGE = join(ROOT, "images/default");
|
|
494
|
+
/** The host image's own part (#142): merged over the default image (mergeImages), the host skein's image. */
|
|
495
|
+
export const HOST_IMAGE = join(ROOT, "images/host");
|
|
496
|
+
/**
|
|
497
|
+
* Two image trees merged (#142: images are starting trees that merge): every path of `over` added to
|
|
498
|
+
* `base`. A directory in both is merged; a file in both must be the same blob — except `etc/apps.json`,
|
|
499
|
+
* whose `install` and `routes` lists are joined (an app in both, by appKey, is refused; a root route of
|
|
500
|
+
* `over` at the same transport, address and prefix as one of `base`'s replaces it, #147). Any other path in both is a
|
|
501
|
+
* collision, refused with its path. The installer's own checks (boxes, heads, paths) run when the merged
|
|
502
|
+
* image's apps are installed at birth (installAtBirth). The new trees and the merged apps.json go into
|
|
503
|
+
* `out`; every other object stays where it was.
|
|
504
|
+
*/
|
|
505
|
+
export async function mergeImages(objects, base, over, out) {
|
|
506
|
+
const tree = async (cid) => parseTree(await need(objects, cid, "an image tree"), cid);
|
|
507
|
+
const blob = async (cid, path) => {
|
|
508
|
+
const b = gitBody(await need(objects, cid, path), "blob");
|
|
509
|
+
if (!b)
|
|
510
|
+
throw new Error(`${path}: not a file`);
|
|
511
|
+
return Buffer.from(b).toString("utf8");
|
|
512
|
+
};
|
|
513
|
+
const merge = async (a, b, at) => {
|
|
514
|
+
const entries = new Map((await tree(a)).map((e) => [e.name, e]));
|
|
515
|
+
for (const e of await tree(b)) {
|
|
516
|
+
const path = at ? `${at}/${e.name}` : e.name;
|
|
517
|
+
const had = entries.get(e.name);
|
|
518
|
+
if (!had || had.cid.equals(e.cid)) {
|
|
519
|
+
entries.set(e.name, e);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (had.mode === "40000" && e.mode === "40000") {
|
|
523
|
+
entries.set(e.name, { ...e, cid: await merge(had.cid, e.cid, path) });
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (path === APPS && had.mode !== "40000" && e.mode !== "40000") {
|
|
527
|
+
const x = JSON.parse(await blob(had.cid, path)), y = JSON.parse(await blob(e.cid, path));
|
|
528
|
+
const theirs = new Set((y.install ?? []).map(appKey));
|
|
529
|
+
const both = (x.install ?? []).map(appKey).filter((p) => theirs.has(p));
|
|
530
|
+
if (both.length)
|
|
531
|
+
throw new Error(`${APPS}: ${both.join(", ")} installed by both images`);
|
|
532
|
+
// #147: a root route of `over` at a route of `base`'s (same transport, address, prefix) replaces it.
|
|
533
|
+
const key = (r) => `${r.transport ?? "mailbox"} ${r.address}${r.prefix ? " prefix" : ""}`;
|
|
534
|
+
const mine = new Set((y.routes ?? []).map(key));
|
|
535
|
+
const routes = [...(x.routes ?? []).filter((r) => !mine.has(key(r))), ...(y.routes ?? [])];
|
|
536
|
+
const h = hashBlob(new TextEncoder().encode(`${JSON.stringify({ install: [...(x.install ?? []), ...(y.install ?? [])], ...(routes.length ? { routes } : {}) }, null, 2)}\n`));
|
|
537
|
+
await out.putBlock(h.cid, h.object);
|
|
538
|
+
entries.set(e.name, { ...e, cid: h.cid });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
throw new Error(`images: ${path} is in both images (a merge adds paths; it does not replace one)`);
|
|
542
|
+
}
|
|
543
|
+
const t = hashTree([...entries.values()]);
|
|
544
|
+
await out.putBlock(t.cid, t.object);
|
|
545
|
+
return t.cid;
|
|
546
|
+
};
|
|
547
|
+
return await merge(base, over, "");
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* #152: an image directory with no `bin/` is an overlay — it carries what it adds (an app repository's
|
|
551
|
+
* `image/`: `etc/apps.json` alone) and is merged over the default image before boot, as images/host is;
|
|
552
|
+
* one with `bin/` is a complete image of its own.
|
|
553
|
+
*/
|
|
554
|
+
export function isOverlayImage(dir) {
|
|
555
|
+
const bin = join(dir, BIN);
|
|
556
|
+
return !(existsSync(bin) && statSync(bin).isDirectory());
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* #142, #152: an overlay image directory (`dir`: images/host, an app repository's `image/`) merged over
|
|
560
|
+
* `base` (mergeImages: an app in both refused, a root route at the same place replaced, any other path in
|
|
561
|
+
* both refused unless it is the same blob). The apps each names beside it are kept, by appKey.
|
|
562
|
+
*/
|
|
563
|
+
export async function overlayImage(base, dir) {
|
|
564
|
+
const own = await imageDir(dir);
|
|
565
|
+
const merged = new MemBlocks();
|
|
566
|
+
const objects = anyOf(merged, base.objects, own.objects);
|
|
567
|
+
const root = await mergeImages(objects, base.root, own.root, merged);
|
|
568
|
+
return { root, objects, apps: { ...base.apps, ...own.apps } };
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* An image as a boot source (#89, #90): a directory (default: the default
|
|
572
|
+
* image), its objects with the repo's pinned modules beside them, and the
|
|
573
|
+
* apps it names by a directory outside it (imageDir).
|
|
574
|
+
*/
|
|
575
|
+
export async function imageSource(dir = DEFAULT_IMAGE) {
|
|
576
|
+
const d = await imageDir(dir);
|
|
577
|
+
return { kind: "tree", root: d.root, objects: anyOf(d.objects, wasmDirObjects(WASM_DIR)), apps: d.apps };
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* #152: an image directory scanned, and each app its etc/apps.json names by a directory outside it
|
|
581
|
+
* (`..`, `../<dir>`, an absolute path) scanned from there as `skein install <dir>` reads it
|
|
582
|
+
* (install.ts appIgnore): the trees by appKey (BootSource `apps`), their objects with the image's.
|
|
583
|
+
*/
|
|
584
|
+
export async function imageDir(dir) {
|
|
585
|
+
const d = await dirSource(dir);
|
|
586
|
+
const apps = {};
|
|
587
|
+
const file = join(dir, APPS);
|
|
588
|
+
if (!existsSync(file))
|
|
589
|
+
return { ...d, apps };
|
|
590
|
+
let list;
|
|
591
|
+
try {
|
|
592
|
+
list = JSON.parse(readFileSync(file, "utf8"));
|
|
593
|
+
}
|
|
594
|
+
catch (x) {
|
|
595
|
+
throw new Error(`${APPS}: ${x.message}`);
|
|
596
|
+
}
|
|
597
|
+
checkImageApps(list);
|
|
598
|
+
for (const a of list.install) {
|
|
599
|
+
if (typeof a !== "string")
|
|
600
|
+
continue;
|
|
601
|
+
const key = appKey(a);
|
|
602
|
+
if (posix.isAbsolute(key) || key === ".." || key.startsWith("../"))
|
|
603
|
+
apps[key] = await scan(d.objects, resolve(dir, a), { ignore: appIgnore });
|
|
604
|
+
}
|
|
605
|
+
return { ...d, apps };
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* #152: an app by release tarball, `hash` its tree's git id: the tree the image's objects hold
|
|
609
|
+
* already, else fetched from `url` (gzip or plain tar), unpacked (a tarball holding one directory,
|
|
610
|
+
* as a tag's archive does, is that directory), scanned into `into` as `skein install <dir>` reads
|
|
611
|
+
* a directory (install.ts appIgnore), and refused unless its tree is `hash`.
|
|
612
|
+
*/
|
|
613
|
+
export async function appTarball(a, have, into, get = fetch) {
|
|
614
|
+
const want = sha1Cid(Buffer.from(a.hash, "hex"));
|
|
615
|
+
if (await have.get(want))
|
|
616
|
+
return want;
|
|
617
|
+
const r = await get(a.url);
|
|
618
|
+
if (!r.ok)
|
|
619
|
+
throw new Error(`${APPS}: ${a.url}: ${r.status} ${r.statusText}`);
|
|
620
|
+
let bytes = new Uint8Array(await r.arrayBuffer());
|
|
621
|
+
if (bytes[0] === 0x1f && bytes[1] === 0x8b)
|
|
622
|
+
bytes = gunzipSync(bytes);
|
|
623
|
+
const dir = mkdtempSync(join(tmpdir(), "skein-app-"));
|
|
624
|
+
try {
|
|
625
|
+
const x = spawnSync("tar", ["-x", "-f", "-", "-C", dir], { input: bytes });
|
|
626
|
+
if (x.status !== 0)
|
|
627
|
+
throw new Error(`${APPS}: ${a.url}: not a tarball (${x.stderr.toString().trim() || `tar exit ${x.status}`})`);
|
|
628
|
+
const top = readdirSync(dir, { withFileTypes: true });
|
|
629
|
+
const root = await scan(into, top.length === 1 && top[0].isDirectory() ? join(dir, top[0].name) : dir, { ignore: appIgnore });
|
|
630
|
+
if (!root.equals(want))
|
|
631
|
+
throw new Error(`${APPS}: ${a.url}: its tree is ${Buffer.from(root.multihash.digest).toString("hex")}, not ${a.hash}`);
|
|
632
|
+
return root;
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
rmSync(dir, { recursive: true, force: true });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/** Source B: a packet file, verified (packet.ts) — a system tree to boot, or a checkpoint to restore. */
|
|
639
|
+
export async function packetSource(bytes, o = {}) {
|
|
640
|
+
const { readPacket } = await import("./packet.js");
|
|
641
|
+
const v = await readPacket(bytes, o);
|
|
642
|
+
return v.kind === "tree" ? { kind: "tree", root: v.scope, objects: v.blocks } : { kind: "checkpoint", state: v.scope, blocks: v.list };
|
|
643
|
+
}
|