@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
package/dist/dev/cli.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types --no-warnings
|
|
2
|
+
// `skein-dev`: the developer's tools. OUTSIDE the machine: this process reads
|
|
3
|
+
// the disk and opens an instance's store file directly, which the kernel
|
|
4
|
+
// itself never lets anything do.
|
|
5
|
+
//
|
|
6
|
+
// - bootstrap: `install` puts the pinned wasm modules into the store file
|
|
7
|
+
// (they exceed a message; docs/OPEN.md). Trees and commands come from the
|
|
8
|
+
// client (bin/skein) through the messagebox.
|
|
9
|
+
// - inspection: `log`, `ls`, `show`, `refs` read the store.
|
|
10
|
+
//
|
|
11
|
+
// Replay is the kernel's: `skein-kernel replay <source.db> <out.db>`.
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { mkdirSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { fmt, isCID, parse } from "../runtime/cid.js";
|
|
18
|
+
import { readLog } from "../runtime/log.js";
|
|
19
|
+
import { MODULES, rawCid } from "../runtime/programs.js";
|
|
20
|
+
import { openStoreFile } from "../runtime/index-store.js";
|
|
21
|
+
const skeinHome = () => process.env.SKEIN_HOME || join(homedir(), ".skein");
|
|
22
|
+
export const dbPath = () => process.env.SKEIN_DB || join(skeinHome(), "runtime.db");
|
|
23
|
+
const WASM_DIR = fileURLToPath(new URL("../../wasm/", import.meta.url));
|
|
24
|
+
function openDefault(path) {
|
|
25
|
+
const p = path || dbPath();
|
|
26
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
27
|
+
return openStoreFile(p);
|
|
28
|
+
}
|
|
29
|
+
class UsageError extends Error {
|
|
30
|
+
}
|
|
31
|
+
function parseArgs(argv, flags) {
|
|
32
|
+
const out = { pos: [], opts: {} };
|
|
33
|
+
const all = { help: "boolean", ...flags };
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
const a = argv[i];
|
|
36
|
+
if (a === "--") {
|
|
37
|
+
out.pos.push(...argv.slice(i + 1));
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (a === "-h") {
|
|
41
|
+
out.opts.help = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!a.startsWith("--")) {
|
|
45
|
+
out.pos.push(a);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const eq = a.indexOf("=");
|
|
49
|
+
const name = a.slice(2, eq < 0 ? undefined : eq);
|
|
50
|
+
const kind = all[name];
|
|
51
|
+
if (!kind)
|
|
52
|
+
throw new UsageError(`unknown flag --${name}`);
|
|
53
|
+
if (kind === "boolean") {
|
|
54
|
+
out.opts[name] = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const v = eq >= 0 ? a.slice(eq + 1) : argv[++i];
|
|
58
|
+
if (v === undefined)
|
|
59
|
+
throw new UsageError(`--${name} needs a value`);
|
|
60
|
+
out.opts[name] = v;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
const short = (c) => c.toString().slice(-12);
|
|
65
|
+
const text = (b) => (b instanceof Uint8Array ? Buffer.from(b).toString("utf8") : String(b ?? ""));
|
|
66
|
+
function jsonify(v) {
|
|
67
|
+
if (isCID(v))
|
|
68
|
+
return { "/": fmt(v) };
|
|
69
|
+
if (v instanceof Uint8Array)
|
|
70
|
+
return v.length <= 64 ? { bytes: Buffer.from(v).toString("hex") } : { text: text(v) };
|
|
71
|
+
if (Array.isArray(v))
|
|
72
|
+
return v.map(jsonify);
|
|
73
|
+
if (v && typeof v === "object")
|
|
74
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, jsonify(x)]));
|
|
75
|
+
return v;
|
|
76
|
+
}
|
|
77
|
+
/** Put the pinned wasm modules into the store under their CIDs (the shell's are the shell app's, #83: installed with it). */
|
|
78
|
+
export async function install(store) {
|
|
79
|
+
const out = [];
|
|
80
|
+
const all = Object.entries(MODULES).map(([n, c]) => [`${n}.wasm`, c]);
|
|
81
|
+
for (const [file, cid] of all) {
|
|
82
|
+
const name = file.replace(/\.wasm$/, "");
|
|
83
|
+
if (await store.has(cid))
|
|
84
|
+
continue;
|
|
85
|
+
const bytes = await readFile(join(WASM_DIR, file));
|
|
86
|
+
if (!rawCid(bytes).equals(cid))
|
|
87
|
+
throw new Error(`wasm/${file} does not match the pinned ${fmt(cid)} (rebuilt? update src/runtime/programs.ts)`);
|
|
88
|
+
await store.putBlock(cid, bytes);
|
|
89
|
+
out.push(`${name} ${fmt(cid)}`);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
export async function resolveCid(store, s) {
|
|
94
|
+
try {
|
|
95
|
+
return parse(s);
|
|
96
|
+
}
|
|
97
|
+
catch { /* a prefix or suffix */ }
|
|
98
|
+
const all = await store.findByPrefix("");
|
|
99
|
+
const hits = all.filter((c) => fmt(c).startsWith(s) || fmt(c).startsWith(`bafy${s}`) || fmt(c).endsWith(s));
|
|
100
|
+
if (hits.length !== 1)
|
|
101
|
+
throw new Error(hits.length ? `ambiguous: ${s}` : `no thread matches ${s}`);
|
|
102
|
+
return hits[0];
|
|
103
|
+
}
|
|
104
|
+
async function withStore(fn, path) {
|
|
105
|
+
const store = openDefault(path);
|
|
106
|
+
try {
|
|
107
|
+
return await fn(store);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await store.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const COMMANDS = {
|
|
114
|
+
install: {
|
|
115
|
+
usage: `skein-dev install
|
|
116
|
+
Put the pinned wasm modules (the shell's brush, coreutils and tools, the
|
|
117
|
+
wallet) into the store file under their CIDs: the bootstrap side door
|
|
118
|
+
(docs/OPEN.md); objects bigger than a message cannot arrive any other way yet.`,
|
|
119
|
+
flags: {},
|
|
120
|
+
async run(_a, env) {
|
|
121
|
+
const done = await withStore(install);
|
|
122
|
+
env.out(done.length ? done.map((l) => `installed ${l}`).join("\n") : "modules already in the store");
|
|
123
|
+
return 0;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
log: {
|
|
127
|
+
usage: `skein-dev log [--limit n]
|
|
128
|
+
The input log: position, admission stamp, entry CID (the last is the state hash), and what it admits.`,
|
|
129
|
+
flags: { limit: "string" },
|
|
130
|
+
async run(a, env) {
|
|
131
|
+
await withStore(async (store) => {
|
|
132
|
+
const all = await readLog(store);
|
|
133
|
+
const lim = a.opts.limit ? Number(a.opts.limit) : all.length;
|
|
134
|
+
for (const { cid, entry } of all.slice(-lim)) {
|
|
135
|
+
const when = new Date(entry.time[0] * 1000 + Math.floor(entry.time[1] / 1e6)).toISOString();
|
|
136
|
+
const e8 = entry;
|
|
137
|
+
const what = entry.genesis ? `genesis ${fmt(entry.genesis)}` : e8.request ? `${e8.transport} request ${fmt(e8.request)}`
|
|
138
|
+
: e8.mail ? `mail ${fmt(e8.mail)}` : e8.event ? `${entry.box} event ${fmt(e8.event)}` : entry.envelope ? `${entry.box} envelope ${fmt(entry.envelope)}`
|
|
139
|
+
: entry.outcome ? `outcome ${entry.outcome.status} emit ${fmt(entry.outcome.emit)}${entry.outcome.reason ? `: ${entry.outcome.reason}` : ""}` : `wake ${entry.wake ? fmt(entry.wake) : "?"}`;
|
|
140
|
+
env.out(`${String(entry.n).padStart(4)} ${when} ${fmt(cid)} ${what}`);
|
|
141
|
+
}
|
|
142
|
+
const tip = await store.log.tip();
|
|
143
|
+
env.out(`state ${tip ? fmt(tip) : "(empty)"} · processed ${await store.live.cursor.get()}/${all.length}`);
|
|
144
|
+
});
|
|
145
|
+
return 0;
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
ls: {
|
|
149
|
+
usage: `skein-dev ls [--limit 20] threads, newest first: cid, state, command`,
|
|
150
|
+
flags: { limit: "string" },
|
|
151
|
+
async run(a, env) {
|
|
152
|
+
await withStore(async (store) => {
|
|
153
|
+
for await (const t of store.edges.query({ kind: "thread", limit: Number(a.opts.limit ?? 20) })) {
|
|
154
|
+
const o = await store.get(t);
|
|
155
|
+
const tip = await store.chains.tip(t);
|
|
156
|
+
const u = tip.equals(t) ? undefined : await store.get(tip);
|
|
157
|
+
env.out(`${short(t)} ${(u?.state ?? "new").padEnd(8)} ${o.args?.cmd ?? ""}`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return 0;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
show: {
|
|
164
|
+
usage: `skein-dev show <cid-or-suffix> a thread (origin and updates) or any record as JSON`,
|
|
165
|
+
flags: {},
|
|
166
|
+
async run(a, env) {
|
|
167
|
+
if (!a.pos[0])
|
|
168
|
+
throw new UsageError("missing <cid>");
|
|
169
|
+
await withStore(async (store) => {
|
|
170
|
+
const cid = await resolveCid(store, a.pos[0]);
|
|
171
|
+
const b = await store.get(cid);
|
|
172
|
+
env.out(`${fmt(cid)}\n${JSON.stringify(jsonify(b), null, 2)}`);
|
|
173
|
+
if (b.kind !== "thread")
|
|
174
|
+
return;
|
|
175
|
+
for await (const u of store.chains.history(cid)) {
|
|
176
|
+
if (u.equals(cid))
|
|
177
|
+
continue;
|
|
178
|
+
const x = await store.get(u);
|
|
179
|
+
const res = x.result;
|
|
180
|
+
env.out(`#${x.seq} ${x.state}${x.waitingFrom ? ` from ${x.waitingFrom.slice(0, 12)}` : ""} input …${x.input ? short(x.input) : "?"} ${fmt(u)}`);
|
|
181
|
+
if (res)
|
|
182
|
+
env.out(` exit ${res.exitCode} · tree ${res.tree ? fmt(res.tree) : "?"}\n stdout: ${text(res.stdout).trimEnd().replace(/\n/g, "\n ")}${res.stderr?.length ? `\n stderr: ${text(res.stderr).trimEnd()}` : ""}`);
|
|
183
|
+
if (x.error)
|
|
184
|
+
env.out(` ${x.error.kind}: ${x.error.message}`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return 0;
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
refs: {
|
|
191
|
+
usage: `skein-dev refs <cid-or-suffix> pointers out of the record's chain, and into it`,
|
|
192
|
+
flags: {},
|
|
193
|
+
async run(a, env) {
|
|
194
|
+
if (!a.pos[0])
|
|
195
|
+
throw new UsageError("missing <cid>");
|
|
196
|
+
await withStore(async (store) => {
|
|
197
|
+
const cid = await resolveCid(store, a.pos[0]);
|
|
198
|
+
const origin = await store.chains.originOf(cid).catch(() => cid);
|
|
199
|
+
const line = (r, other) => ` ${r.rel.padEnd(11)} ${isCID(other) ? fmt(other) : String(other)}${r.locator ? ` ${r.locator}` : ""}`;
|
|
200
|
+
env.out(`from ${fmt(origin)}`);
|
|
201
|
+
for (const r of await store.edges.refsFrom(origin))
|
|
202
|
+
env.out(line(r, r.to));
|
|
203
|
+
env.out(`to ${fmt(cid)}`);
|
|
204
|
+
for (const r of await store.edges.refsTo(cid))
|
|
205
|
+
env.out(line(r, r.from));
|
|
206
|
+
});
|
|
207
|
+
return 0;
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
const HELP = `skein-dev — developer tools, outside the machine
|
|
212
|
+
|
|
213
|
+
skein-dev install put the pinned wasm modules into the store file (bootstrap)
|
|
214
|
+
skein-dev log | ls | show <cid> | refs <cid>
|
|
215
|
+
|
|
216
|
+
Trees and commands come from the client (bin/skein import / run) through the
|
|
217
|
+
instance's front door. Replay: skein-kernel replay <source.db> <out.db>. Env: SKEIN_HOME (~/.skein), SKEIN_DB ($SKEIN_HOME/runtime.db).`;
|
|
218
|
+
export async function main(argv) {
|
|
219
|
+
const env = {
|
|
220
|
+
out: (l) => process.stdout.write(`${l}\n`),
|
|
221
|
+
err: (l) => process.stderr.write(`${l}\n`),
|
|
222
|
+
write: (fd, b) => void (fd === 1 ? process.stdout : process.stderr).write(b),
|
|
223
|
+
};
|
|
224
|
+
const [name, ...rest] = argv;
|
|
225
|
+
if (!name || name === "--help" || name === "-h" || name === "help") {
|
|
226
|
+
env.out(HELP);
|
|
227
|
+
return name ? 0 : 1;
|
|
228
|
+
}
|
|
229
|
+
const cmd = COMMANDS[name];
|
|
230
|
+
if (!cmd) {
|
|
231
|
+
env.err(`unknown command: ${name}\n\n${HELP}`);
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const a = parseArgs(rest, cmd.flags);
|
|
236
|
+
if (a.opts.help) {
|
|
237
|
+
env.out(cmd.usage);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
return await cmd.run(a, env);
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
env.err(e instanceof UsageError ? `${e.message}\n\n${cmd.usage}` : `skein-dev ${name}: ${e.message}`);
|
|
244
|
+
return e instanceof UsageError ? 2 : 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (import.meta.main)
|
|
248
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
import { type TreeBlocks } from "../runtime/tree.ts";
|
|
3
|
+
export interface ScanOptions {
|
|
4
|
+
/** relPath is "/"-separated from the scanned root. Replaces the default. */
|
|
5
|
+
ignore?: (relPath: string) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function defaultIgnore(rel: string): boolean;
|
|
8
|
+
/** Hash a directory into the store; returns the root tree. Empty directories vanish, as in git. */
|
|
9
|
+
export declare function scan(blocks: TreeBlocks, dir: string, opts?: ScanOptions): Promise<CID>;
|
|
10
|
+
/**
|
|
11
|
+
* Check `tree` out into `dir`: afterwards scan(dir) === tree. Anything not in the
|
|
12
|
+
* tree is removed, except paths `ignore` matches (by default .git and node_modules).
|
|
13
|
+
* Never writes through an existing symlink.
|
|
14
|
+
*/
|
|
15
|
+
export declare function materialize(blocks: TreeBlocks, tree: CID, dir: string, opts?: ScanOptions): Promise<void>;
|
package/dist/dev/scan.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Directory <-> tree objects on a real disk. Outside the machine: the runtime
|
|
2
|
+
// never reads or writes a disk. This is the client's job (docs/ARCH.md, "How a
|
|
3
|
+
// file gets in and out"); skein-dev uses it as a stand-in for the client.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { createReadStream } from "node:fs";
|
|
6
|
+
import * as fs from "node:fs/promises";
|
|
7
|
+
import { join, resolve, sep } from "node:path";
|
|
8
|
+
import { blobCid, hashBlob, hashTree, header, readBlob, readTree, sha1Cid } from "../runtime/tree.js";
|
|
9
|
+
// Above this, hash from a stream first so a file the store already has is never held whole.
|
|
10
|
+
const STREAM_OVER = 1 << 20;
|
|
11
|
+
export function defaultIgnore(rel) {
|
|
12
|
+
const base = rel.slice(rel.lastIndexOf("/") + 1);
|
|
13
|
+
return base === ".git" || base === "node_modules";
|
|
14
|
+
}
|
|
15
|
+
// Bounds open file descriptors; directory recursion itself is unbounded (readdir holds no fd across awaits).
|
|
16
|
+
function gate(n) {
|
|
17
|
+
let active = 0;
|
|
18
|
+
const waiting = [];
|
|
19
|
+
return async (fn) => {
|
|
20
|
+
while (active >= n)
|
|
21
|
+
await new Promise((r) => waiting.push(r));
|
|
22
|
+
active++;
|
|
23
|
+
try {
|
|
24
|
+
return await fn();
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
active--;
|
|
28
|
+
waiting.shift()?.();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function putBlob(blocks, bytes) {
|
|
33
|
+
const cid = blobCid(bytes);
|
|
34
|
+
if (!(await blocks.has(cid)))
|
|
35
|
+
await blocks.putBlock(cid, hashBlob(bytes).object);
|
|
36
|
+
return cid;
|
|
37
|
+
}
|
|
38
|
+
async function streamCid(path, size) {
|
|
39
|
+
const h = createHash("sha1").update(header("blob", size));
|
|
40
|
+
let n = 0;
|
|
41
|
+
for await (const chunk of createReadStream(path)) {
|
|
42
|
+
h.update(chunk);
|
|
43
|
+
n += chunk.length;
|
|
44
|
+
}
|
|
45
|
+
return n === size ? sha1Cid(h.digest()) : undefined; // changed underfoot: caller rereads
|
|
46
|
+
}
|
|
47
|
+
async function scanFile(blocks, path, name) {
|
|
48
|
+
const st = await fs.lstat(path);
|
|
49
|
+
// git records only the owner's execute bit (core.filemode).
|
|
50
|
+
const mode = st.mode & 0o100 ? "100755" : "100644";
|
|
51
|
+
if (st.size > STREAM_OVER) {
|
|
52
|
+
const cid = await streamCid(path, st.size);
|
|
53
|
+
if (cid && (await blocks.has(cid)))
|
|
54
|
+
return { mode, name, cid };
|
|
55
|
+
}
|
|
56
|
+
return { mode, name, cid: await putBlob(blocks, await fs.readFile(path)) };
|
|
57
|
+
}
|
|
58
|
+
/** Hash a directory into the store; returns the root tree. Empty directories vanish, as in git. */
|
|
59
|
+
export async function scan(blocks, dir, opts = {}) {
|
|
60
|
+
const ignore = opts.ignore ?? defaultIgnore;
|
|
61
|
+
const limit = gate(64);
|
|
62
|
+
const tree = async (abs, rel) => {
|
|
63
|
+
const dirents = await fs.readdir(abs, { withFileTypes: true });
|
|
64
|
+
const found = await Promise.all(dirents.map(async (d) => {
|
|
65
|
+
const r = rel ? `${rel}/${d.name}` : d.name;
|
|
66
|
+
if (ignore(r))
|
|
67
|
+
return undefined;
|
|
68
|
+
const p = join(abs, d.name);
|
|
69
|
+
if (d.isDirectory()) {
|
|
70
|
+
const cid = await tree(p, r);
|
|
71
|
+
return cid && { mode: "40000", name: d.name, cid };
|
|
72
|
+
}
|
|
73
|
+
if (d.isSymbolicLink()) {
|
|
74
|
+
return limit(async () => ({ mode: "120000", name: d.name, cid: await putBlob(blocks, await fs.readlink(p, { encoding: "buffer" })) }));
|
|
75
|
+
}
|
|
76
|
+
if (d.isFile())
|
|
77
|
+
return limit(() => scanFile(blocks, p, d.name));
|
|
78
|
+
return undefined; // fifos, sockets, devices: git skips them too
|
|
79
|
+
}));
|
|
80
|
+
const entries = found.filter((e) => e !== undefined);
|
|
81
|
+
if (entries.length === 0 && rel !== "")
|
|
82
|
+
return undefined;
|
|
83
|
+
const { cid, object } = hashTree(entries);
|
|
84
|
+
if (!(await blocks.has(cid)))
|
|
85
|
+
await blocks.putBlock(cid, object);
|
|
86
|
+
return cid;
|
|
87
|
+
};
|
|
88
|
+
return (await tree(resolve(dir), ""));
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------- materialize
|
|
91
|
+
function checkName(name, rel) {
|
|
92
|
+
// Trees come from anywhere; a name is one path segment or nothing. ".git" is refused as git does.
|
|
93
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0") || name.toLowerCase() === ".git") {
|
|
94
|
+
throw new Error(`refusing tree entry ${JSON.stringify(name)} under ${JSON.stringify(rel || ".")}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function lstatOr(path) {
|
|
98
|
+
try {
|
|
99
|
+
return await fs.lstat(path);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (e.code === "ENOENT")
|
|
103
|
+
return undefined;
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check `tree` out into `dir`: afterwards scan(dir) === tree. Anything not in the
|
|
109
|
+
* tree is removed, except paths `ignore` matches (by default .git and node_modules).
|
|
110
|
+
* Never writes through an existing symlink.
|
|
111
|
+
*/
|
|
112
|
+
export async function materialize(blocks, tree, dir, opts = {}) {
|
|
113
|
+
const ignore = opts.ignore ?? defaultIgnore;
|
|
114
|
+
const root = resolve(dir);
|
|
115
|
+
await fs.mkdir(root, { recursive: true });
|
|
116
|
+
const limit = gate(32);
|
|
117
|
+
const inside = (p) => {
|
|
118
|
+
if (p !== root && !p.startsWith(root + sep))
|
|
119
|
+
throw new Error(`refusing to write outside ${root}: ${p}`);
|
|
120
|
+
};
|
|
121
|
+
const leaf = async (e, p, st) => {
|
|
122
|
+
const data = await readBlob(blocks, e.cid);
|
|
123
|
+
if (e.mode === "120000") {
|
|
124
|
+
if (st?.isSymbolicLink() && (await fs.readlink(p, { encoding: "buffer" })).equals(data))
|
|
125
|
+
return;
|
|
126
|
+
if (st)
|
|
127
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
128
|
+
await fs.symlink(Buffer.from(data), p);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const exec = e.mode === "100755";
|
|
132
|
+
if (st?.isFile()) {
|
|
133
|
+
if (st.size !== data.length || !(await fs.readFile(p)).equals(data))
|
|
134
|
+
await fs.writeFile(p, data);
|
|
135
|
+
if (Boolean(st.mode & 0o100) !== exec) {
|
|
136
|
+
// Add x wherever r is set (git's 0777&~umask shape); drop all x otherwise.
|
|
137
|
+
await fs.chmod(p, exec ? st.mode | ((st.mode & 0o444) >> 2) : st.mode & ~0o111);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (st)
|
|
142
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
143
|
+
// "wx": O_EXCL refuses to follow anything that raced into place.
|
|
144
|
+
await fs.writeFile(p, data, { mode: exec ? 0o777 : 0o666, flag: "wx" });
|
|
145
|
+
};
|
|
146
|
+
const checkout = async (cid, abs, rel) => {
|
|
147
|
+
const entries = await readTree(blocks, cid);
|
|
148
|
+
for (const e of entries)
|
|
149
|
+
checkName(e.name, rel);
|
|
150
|
+
const want = new Set(entries.map((e) => e.name));
|
|
151
|
+
// Clear stale entries first so a file<->dir swap finds its slot empty.
|
|
152
|
+
for (const name of await fs.readdir(abs)) {
|
|
153
|
+
if (!want.has(name) && !ignore(rel ? `${rel}/${name}` : name))
|
|
154
|
+
await fs.rm(join(abs, name), { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
await Promise.all(entries.map(async (e) => {
|
|
157
|
+
const p = join(abs, e.name);
|
|
158
|
+
inside(p);
|
|
159
|
+
const st = await lstatOr(p);
|
|
160
|
+
if (e.mode === "40000" || e.mode === "160000") {
|
|
161
|
+
if (st && !st.isDirectory())
|
|
162
|
+
await fs.rm(p, { force: true }); // a symlink here must not be descended
|
|
163
|
+
if (!st?.isDirectory())
|
|
164
|
+
await fs.mkdir(p);
|
|
165
|
+
if (e.mode === "40000")
|
|
166
|
+
await checkout(e.cid, p, rel ? `${rel}/${e.name}` : e.name);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await limit(() => leaf(e, p, st));
|
|
170
|
+
}));
|
|
171
|
+
};
|
|
172
|
+
await checkout(tree, root, "");
|
|
173
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { WalletInterface } from "@bsv/sdk";
|
|
2
|
+
import { type Envelope } from "./envelope.ts";
|
|
3
|
+
import { CID } from "multiformats/cid";
|
|
4
|
+
export interface CborEnvelope {
|
|
5
|
+
metanetHandles: "1.0";
|
|
6
|
+
recipient: {
|
|
7
|
+
handle: string;
|
|
8
|
+
tag?: string;
|
|
9
|
+
domain: string;
|
|
10
|
+
};
|
|
11
|
+
sender: {
|
|
12
|
+
identityKey: Uint8Array;
|
|
13
|
+
handle?: string;
|
|
14
|
+
domain?: string;
|
|
15
|
+
};
|
|
16
|
+
created: string;
|
|
17
|
+
quoteId?: string;
|
|
18
|
+
contentHash: Uint8Array;
|
|
19
|
+
content: Uint8Array;
|
|
20
|
+
signature: Uint8Array;
|
|
21
|
+
}
|
|
22
|
+
export type CborSigned = Omit<CborEnvelope, "content">;
|
|
23
|
+
export declare function isCborSigned(x: unknown): x is CborSigned;
|
|
24
|
+
export declare const isCborEnvelope: (x: unknown) => x is CborEnvelope;
|
|
25
|
+
/** The preimage: dag-cbor of the envelope without content and signature. */
|
|
26
|
+
export declare function preimage(env: Partial<CborEnvelope>): Uint8Array;
|
|
27
|
+
export declare function cborSignedPart(env: CborEnvelope | CborSigned): CborSigned;
|
|
28
|
+
export declare function verifyCbor(env: CborEnvelope | CborSigned): boolean;
|
|
29
|
+
/** Seal `body` (dag-cbor) to `recipient` in the §7.3 form. */
|
|
30
|
+
export declare function sealCbor(wallet: WalletInterface, args: {
|
|
31
|
+
recipient: {
|
|
32
|
+
identityKey: string;
|
|
33
|
+
handle: string;
|
|
34
|
+
domain: string;
|
|
35
|
+
};
|
|
36
|
+
sender?: {
|
|
37
|
+
handle?: string;
|
|
38
|
+
domain?: string;
|
|
39
|
+
};
|
|
40
|
+
body: Uint8Array;
|
|
41
|
+
created?: string;
|
|
42
|
+
}): Promise<CborEnvelope>;
|
|
43
|
+
/** Decrypt a §7.3 envelope's content as its recipient; checked against contentHash. */
|
|
44
|
+
export declare function openCbor(wallet: WalletInterface, env: CborEnvelope): Promise<{
|
|
45
|
+
body: Uint8Array;
|
|
46
|
+
senderIdentityKey: string;
|
|
47
|
+
}>;
|
|
48
|
+
/** Either form: what a recipient needs from an envelope it was handed. */
|
|
49
|
+
export type AnyEnvelope = Envelope | CborEnvelope;
|
|
50
|
+
export declare function formOf(x: unknown): "json" | "cbor" | undefined;
|
|
51
|
+
/** The sender's identity key (hex), the recipient (hex, from BRC-78), whether the signature verifies, the signed part and the message id (its CID as kept). */
|
|
52
|
+
export declare function inspect(env: AnyEnvelope): {
|
|
53
|
+
form: "json" | "cbor";
|
|
54
|
+
sender: string;
|
|
55
|
+
recipient: string;
|
|
56
|
+
verified: boolean;
|
|
57
|
+
signed: object;
|
|
58
|
+
id: CID;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A reply on a BRC-104 session with the recipient's native messagebox: the
|
|
62
|
+
* compact §7.3 form, {type: "reply", replyTo, body} — sender, recipient,
|
|
63
|
+
* host and the envelope signature left out, because the session supplies
|
|
64
|
+
* them. It travels as BRC-231 bytes (so the signed request carries the body
|
|
65
|
+
* itself). The first message on a session, and anything for an external box,
|
|
66
|
+
* is a full envelope.
|
|
67
|
+
*/
|
|
68
|
+
export interface CompactReply {
|
|
69
|
+
type: "reply";
|
|
70
|
+
replyTo: CID;
|
|
71
|
+
body: Uint8Array;
|
|
72
|
+
}
|
|
73
|
+
export declare function isCompactReply(x: unknown): x is CompactReply;
|
|
74
|
+
/**
|
|
75
|
+
* The record a session reply is kept as — the same entry shape as a full
|
|
76
|
+
* envelope's ({envelope, box, body}), its envelope record carrying the proof:
|
|
77
|
+
* the BRC-104 signature over `payload` (the signed request) with the session
|
|
78
|
+
* nonces; `messageId` is SHA-256 of that payload.
|
|
79
|
+
*/
|
|
80
|
+
export declare function sessionRecord(r: CompactReply, sender: string, created: string, s: {
|
|
81
|
+
payload: Uint8Array;
|
|
82
|
+
signature: Uint8Array;
|
|
83
|
+
nonce: string;
|
|
84
|
+
yourNonce: string;
|
|
85
|
+
}): {
|
|
86
|
+
type: "reply";
|
|
87
|
+
replyTo: CID<unknown, number, number, import("multiformats/cid").Version>;
|
|
88
|
+
sender: {
|
|
89
|
+
identityKey: Uint8Array<ArrayBuffer>;
|
|
90
|
+
};
|
|
91
|
+
created: string;
|
|
92
|
+
contentHash: Uint8Array<ArrayBuffer>;
|
|
93
|
+
messageId: Uint8Array<ArrayBuffer>;
|
|
94
|
+
session: {
|
|
95
|
+
payload: Uint8Array<ArrayBufferLike>;
|
|
96
|
+
signature: Uint8Array<ArrayBufferLike>;
|
|
97
|
+
nonce: string;
|
|
98
|
+
yourNonce: string;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
/** How a JSON client carries §7.3 bytes in a BRC-33 JSON body: {"dag-cbor": "<base64>"}. */
|
|
102
|
+
export declare const wrapCbor: (env: CborEnvelope) => {
|
|
103
|
+
"dag-cbor": string;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* A BRC-33 body as an envelope: §7.3 dag-cbor (its bytes, as BRC-231 carries
|
|
107
|
+
* them, or `{"dag-cbor": "<base64>"}`, how a JSON client carries those bytes,
|
|
108
|
+
* or the decoded map), or §7.2 JSON (the object, or its JSON text).
|
|
109
|
+
*/
|
|
110
|
+
export declare function asEnvelope(raw: unknown): AnyEnvelope | undefined;
|