@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,141 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
import type { Block, Ref, ThreadState, Ms } from "./types.ts";
|
|
3
|
+
import type { Message } from "./records.ts";
|
|
4
|
+
import { type Stamp } from "./syscalls.ts";
|
|
5
|
+
export declare class NotFound extends Error {
|
|
6
|
+
readonly cid: string;
|
|
7
|
+
constructor(cid: string);
|
|
8
|
+
}
|
|
9
|
+
/** putMessage refused the record. The store is unchanged. */
|
|
10
|
+
export declare class Rejected extends Error {
|
|
11
|
+
readonly reason: "bad-signature" | "duplicate-seq" | "duplicate-envelope" | "duplicate-outcome" | "out-of-order";
|
|
12
|
+
constructor(reason: Rejected["reason"], message: string);
|
|
13
|
+
}
|
|
14
|
+
export interface Blocks {
|
|
15
|
+
put(value: Block): Promise<CID>;
|
|
16
|
+
get<T extends Block = Block>(cid: CID): Promise<T>;
|
|
17
|
+
has(cid: CID): Promise<boolean>;
|
|
18
|
+
/** The stored dag-cbor bytes, exactly as hashed. Throws NotFound. */
|
|
19
|
+
bytes(cid: CID): Promise<Uint8Array>;
|
|
20
|
+
/** Store bytes under a CID minted elsewhere (git-raw, raw). The caller vouches the CID matches. */
|
|
21
|
+
putBlock(cid: CID, bytes: Uint8Array): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface Chains {
|
|
24
|
+
/** Write an origin block and register its chain (tip = origin, seq 0). */
|
|
25
|
+
open(origin: Block): Promise<CID>;
|
|
26
|
+
/**
|
|
27
|
+
* Write an update {...body, origin, prev: tip, seq: n+1}; move the tip. Serialised per origin.
|
|
28
|
+
* `body.at` is required: the store has no clock (the runtime passes log time).
|
|
29
|
+
*/
|
|
30
|
+
append(origin: CID, body: Record<string, unknown>): Promise<CID>;
|
|
31
|
+
/** Latest update, or the origin itself if there are none. */
|
|
32
|
+
tip(origin: CID): Promise<CID>;
|
|
33
|
+
/** origin → tip, in seq order. */
|
|
34
|
+
history(origin: CID): AsyncIterable<CID>;
|
|
35
|
+
/** Given any block in a chain, its origin (identity). */
|
|
36
|
+
originOf(cid: CID): Promise<CID>;
|
|
37
|
+
}
|
|
38
|
+
export interface Filter {
|
|
39
|
+
kind?: "thread" | "node" | "head" | "message";
|
|
40
|
+
thread?: CID;
|
|
41
|
+
program?: CID;
|
|
42
|
+
from?: string;
|
|
43
|
+
to?: string;
|
|
44
|
+
state?: ThreadState[];
|
|
45
|
+
parentless?: boolean;
|
|
46
|
+
since?: Ms;
|
|
47
|
+
before?: Ms;
|
|
48
|
+
limit?: number;
|
|
49
|
+
orderBy?: "at" | "tipAt";
|
|
50
|
+
}
|
|
51
|
+
export interface Edges {
|
|
52
|
+
refsFrom(cid: CID): Promise<Ref[]>;
|
|
53
|
+
refsTo(cid: CID): Promise<Array<Ref & {
|
|
54
|
+
from: CID;
|
|
55
|
+
}>>;
|
|
56
|
+
/** Structural narrowing over origins. Never text search. Newest first (by `orderBy`). */
|
|
57
|
+
query(filter: Filter): AsyncIterable<CID>;
|
|
58
|
+
/** Drop and rebuild the index from the blocks. */
|
|
59
|
+
rebuild(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export interface Handle {
|
|
62
|
+
[k: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
export interface Live {
|
|
65
|
+
handles: {
|
|
66
|
+
set(thread: CID, handle: Handle): Promise<void>;
|
|
67
|
+
get(thread: CID): Promise<Handle | undefined>;
|
|
68
|
+
clear(thread: CID): Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
/** Thread origins whose tip state is not finished — the scheduler's worklist. */
|
|
71
|
+
resting(filter?: Pick<Filter, "state" | "limit">): AsyncIterable<CID>;
|
|
72
|
+
/** Threads whose `until` has passed (time trigger). */
|
|
73
|
+
due(now: Ms): AsyncIterable<CID>;
|
|
74
|
+
/** Threads waiting on `thread` (event trigger: it came to rest, wake these). */
|
|
75
|
+
waitersOn(thread: CID): AsyncIterable<CID>;
|
|
76
|
+
/** Thread origins whose tip has `waitingFrom === identity` (a message from it should wake them). */
|
|
77
|
+
waitingFrom(identity: string): AsyncIterable<CID>;
|
|
78
|
+
/** Thread origins whose tip `awaits` includes this emitted envelope: a reply naming it (`replyTo`) is theirs. */
|
|
79
|
+
awaiting(envelope: CID): AsyncIterable<CID>;
|
|
80
|
+
/** The scheduler's position in the log: entries with n < cursor have been processed. Not derived. */
|
|
81
|
+
cursor: {
|
|
82
|
+
get(): Promise<number>;
|
|
83
|
+
set(n: number): Promise<void>;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export type LogEntry = {
|
|
87
|
+
kind: "log";
|
|
88
|
+
prev: CID | null;
|
|
89
|
+
n: number;
|
|
90
|
+
time: Stamp;
|
|
91
|
+
/** Exactly one of: the genesis record (n = 0) … */
|
|
92
|
+
genesis?: CID;
|
|
93
|
+
/** … an admitted envelope's signed part (its JSON object without `content`, as dag-cbor), the BRC-33 box it arrived in, and its plaintext body … */
|
|
94
|
+
envelope?: CID;
|
|
95
|
+
box?: string;
|
|
96
|
+
body?: CID;
|
|
97
|
+
/** … a wake: the thread whose `until` this entry reaches … */
|
|
98
|
+
wake?: CID;
|
|
99
|
+
/** … or a delivery outcome: what became of an emit (docs/MESSAGES.md, "Outcomes"). At most one per emit. */
|
|
100
|
+
outcome?: Outcome;
|
|
101
|
+
/** DER, by the instance identity over the entry's dag-cbor without `sig` (log.ts). */
|
|
102
|
+
sig: Uint8Array;
|
|
103
|
+
};
|
|
104
|
+
/** What the delivery provider reports of an emit: sent, or refused for good (`reason`). */
|
|
105
|
+
export type Outcome = {
|
|
106
|
+
emit: CID;
|
|
107
|
+
status: "delivered" | "failed";
|
|
108
|
+
reason?: string;
|
|
109
|
+
};
|
|
110
|
+
export interface Log {
|
|
111
|
+
/**
|
|
112
|
+
* Append a signed entry. It must extend the tip (prev = tip, n = tip.n + 1,
|
|
113
|
+
* time not before tip's) or it is Rejected "out-of-order"; an envelope
|
|
114
|
+
* already in the log is Rejected "duplicate-envelope", a second outcome for
|
|
115
|
+
* one emit "duplicate-outcome". The store does not check the signature
|
|
116
|
+
* (log.ts does).
|
|
117
|
+
*/
|
|
118
|
+
append(entry: LogEntry): Promise<CID>;
|
|
119
|
+
/** The latest entry (the state hash), or undefined for an empty log. */
|
|
120
|
+
tip(): Promise<CID | undefined>;
|
|
121
|
+
/** Entries with n >= from, in order. */
|
|
122
|
+
entries(from?: number): AsyncIterable<{
|
|
123
|
+
cid: CID;
|
|
124
|
+
entry: LogEntry;
|
|
125
|
+
}>;
|
|
126
|
+
/** The entry that admitted an envelope record, if any. */
|
|
127
|
+
byEnvelope(envelope: CID): Promise<CID | undefined>;
|
|
128
|
+
/** The outcome entry for an emit record, if any. */
|
|
129
|
+
outcomeOf(emit: CID): Promise<CID | undefined>;
|
|
130
|
+
}
|
|
131
|
+
export interface Store extends Blocks {
|
|
132
|
+
/** Verify and store a signed message; index it by (from, seq). Rejects a bad signature or a second message at one (from, seq). */
|
|
133
|
+
putMessage(m: Message): Promise<CID>;
|
|
134
|
+
chains: Chains;
|
|
135
|
+
log: Log;
|
|
136
|
+
edges: Edges;
|
|
137
|
+
live: Live;
|
|
138
|
+
close(): Promise<void>;
|
|
139
|
+
}
|
|
140
|
+
/** The checks every Log.append makes, given the current tip entry. Throws Rejected. */
|
|
141
|
+
export declare function checkExtends(entry: LogEntry, tipCid: CID | undefined, tip: LogEntry | undefined): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The store interface. Five layers: blocks, chains, the input log, edges, live.
|
|
2
|
+
// Everything that reads a store here (the explorer, skein-dev, skein-host)
|
|
3
|
+
// calls only this. Two implementations over one SQLite file (node:sqlite):
|
|
4
|
+
// the kernel's format through its index (index-store.ts), and the tables of
|
|
5
|
+
// sqlite.ts (a store of blocks the tests and tools write, and files from
|
|
6
|
+
// before the index of #30).
|
|
7
|
+
import { stampNs } from "./syscalls.js";
|
|
8
|
+
export class NotFound extends Error {
|
|
9
|
+
cid;
|
|
10
|
+
constructor(cid) { super(`not found: ${cid}`); this.cid = cid; }
|
|
11
|
+
}
|
|
12
|
+
/** putMessage refused the record. The store is unchanged. */
|
|
13
|
+
export class Rejected extends Error {
|
|
14
|
+
reason;
|
|
15
|
+
constructor(reason, message) { super(message); this.reason = reason; }
|
|
16
|
+
}
|
|
17
|
+
/** The checks every Log.append makes, given the current tip entry. Throws Rejected. */
|
|
18
|
+
export function checkExtends(entry, tipCid, tip) {
|
|
19
|
+
const okPrev = tipCid ? entry.prev !== null && entry.prev.equals(tipCid) : entry.prev === null;
|
|
20
|
+
const okN = entry.n === (tip ? tip.n + 1 : 0);
|
|
21
|
+
const okTime = !tip || stampNs(entry.time) >= stampNs(tip.time);
|
|
22
|
+
if (!okPrev || !okN || !okTime)
|
|
23
|
+
throw new Rejected("out-of-order", `log: entry #${entry.n} does not extend the tip${okTime ? "" : " (stamped before it)"}`);
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
/** A log-entry stamp: whole seconds and nanoseconds since the Unix epoch (both safe integers, nsec < 1e9). */
|
|
3
|
+
export type Stamp = [sec: number, nsec: number];
|
|
4
|
+
export declare const stampNs: (s: Stamp) => bigint;
|
|
5
|
+
export declare const msStamp: (ms: number) => Stamp;
|
|
6
|
+
/** The later of two stamps. */
|
|
7
|
+
export declare const maxStamp: (a: Stamp, b: Stamp) => Stamp;
|
|
8
|
+
/** The random stream for (entry, thread): SHA-256(key ‖ counter as u64 BE), block after block. */
|
|
9
|
+
export declare function entropy(entry: CID, thread: CID): (len: number) => Uint8Array;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Time and randomness as the log defines them (docs/ARCH.md, "Time and
|
|
2
|
+
// randomness"; the kernel's syscalls.zig). Both are pure functions of records.
|
|
3
|
+
//
|
|
4
|
+
// Time. Every log entry carries its admission stamp, `time: [sec, nsec]`;
|
|
5
|
+
// inside the machine every clock reads from the stamp of the entry a step is
|
|
6
|
+
// driven by, never from a wall clock.
|
|
7
|
+
//
|
|
8
|
+
// Random. random_get draws from a SHA-256 counter stream keyed by
|
|
9
|
+
// (current log entry CID, thread origin CID): distinct per entry and per
|
|
10
|
+
// sibling thread, fixed on replay, and with no seed anywhere. Never use it for
|
|
11
|
+
// secrets — it is a pure function of public records. The kernel's stream is
|
|
12
|
+
// checked against this one (kernel-zig/test/fixtures.json).
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
export const stampNs = (s) => BigInt(s[0]) * 1000000000n + BigInt(s[1]);
|
|
15
|
+
export const msStamp = (ms) => [Math.floor(ms / 1000), (ms % 1000) * 1_000_000];
|
|
16
|
+
/** The later of two stamps. */
|
|
17
|
+
export const maxStamp = (a, b) => (stampNs(a) >= stampNs(b) ? a : b);
|
|
18
|
+
/** The random stream for (entry, thread): SHA-256(key ‖ counter as u64 BE), block after block. */
|
|
19
|
+
export function entropy(entry, thread) {
|
|
20
|
+
const key = createHash("sha256").update(entry.bytes).update(thread.bytes).digest();
|
|
21
|
+
let counter = 0n;
|
|
22
|
+
let buf = new Uint8Array(0);
|
|
23
|
+
let off = 0;
|
|
24
|
+
return (len) => {
|
|
25
|
+
const out = new Uint8Array(len);
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < len) {
|
|
28
|
+
if (off === buf.length) {
|
|
29
|
+
const c = new Uint8Array(8);
|
|
30
|
+
new DataView(c.buffer).setBigUint64(0, counter++);
|
|
31
|
+
buf = new Uint8Array(createHash("sha256").update(key).update(c).digest());
|
|
32
|
+
off = 0;
|
|
33
|
+
}
|
|
34
|
+
const take = Math.min(len - i, buf.length - off);
|
|
35
|
+
out.set(buf.subarray(off, off + take), i);
|
|
36
|
+
i += take;
|
|
37
|
+
off += take;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CID } from "multiformats/cid";
|
|
2
|
+
import type { Blocks } from "./store.ts";
|
|
3
|
+
export declare const GIT_RAW = 120;
|
|
4
|
+
/** Modes we write. */
|
|
5
|
+
export type Mode = "100644" | "100755" | "120000" | "40000";
|
|
6
|
+
/** Modes we read: 160000 (gitlink/submodule) only arrives from real repos. */
|
|
7
|
+
export type EntryMode = Mode | "160000";
|
|
8
|
+
export interface Entry {
|
|
9
|
+
mode: EntryMode;
|
|
10
|
+
name: string;
|
|
11
|
+
cid: CID;
|
|
12
|
+
}
|
|
13
|
+
export interface Leaf {
|
|
14
|
+
mode: EntryMode;
|
|
15
|
+
cid: CID;
|
|
16
|
+
}
|
|
17
|
+
/** All this module needs from a store. */
|
|
18
|
+
export type TreeBlocks = Pick<Blocks, "bytes" | "has" | "putBlock">;
|
|
19
|
+
export declare function gitCid(sha1hex: string): CID;
|
|
20
|
+
export declare function gitSha(cid: CID): string;
|
|
21
|
+
export declare function sha1Cid(digest: Uint8Array): CID;
|
|
22
|
+
export declare function header(type: "blob" | "tree", len: number): Buffer;
|
|
23
|
+
export declare function blobCid(bytes: Uint8Array): CID;
|
|
24
|
+
export declare function hashBlob(bytes: Uint8Array): {
|
|
25
|
+
cid: CID;
|
|
26
|
+
object: Uint8Array;
|
|
27
|
+
};
|
|
28
|
+
export declare function hashTree(entries: Entry[]): {
|
|
29
|
+
cid: CID;
|
|
30
|
+
object: Uint8Array;
|
|
31
|
+
};
|
|
32
|
+
/** The empty tree: what a run with no tree starts from (the kernel's tree.zig hashTree(&.{})). */
|
|
33
|
+
export declare const EMPTY_TREE: CID<unknown, number, number, import("multiformats/cid").Version>;
|
|
34
|
+
export declare function parseTree(object: Uint8Array, cid?: CID): Entry[];
|
|
35
|
+
export declare function readTree(blocks: TreeBlocks, cid: CID): Promise<Entry[]>;
|
|
36
|
+
export declare function readBlob(blocks: TreeBlocks, cid: CID): Promise<Uint8Array>;
|
|
37
|
+
/** The entry at `path` ("" is the root tree itself), or undefined. */
|
|
38
|
+
export declare function lookup(blocks: TreeBlocks, tree: CID, path: string): Promise<Leaf | undefined>;
|
|
39
|
+
/** File contents (a symlink's contents are its target). */
|
|
40
|
+
export declare function readFile(blocks: TreeBlocks, tree: CID, path: string): Promise<Uint8Array>;
|
|
41
|
+
/** Every entry under `path`, depth first in tree order. Directories only with `dirs`. */
|
|
42
|
+
export declare function walk(blocks: TreeBlocks, tree: CID, path?: string, opts?: {
|
|
43
|
+
dirs?: boolean;
|
|
44
|
+
}): AsyncGenerator<{
|
|
45
|
+
path: string;
|
|
46
|
+
mode: EntryMode;
|
|
47
|
+
cid: CID;
|
|
48
|
+
}>;
|
|
49
|
+
export interface Change {
|
|
50
|
+
type: "added" | "removed" | "modified" | "renamed";
|
|
51
|
+
path: string;
|
|
52
|
+
/** renamed: the old path. */
|
|
53
|
+
from?: string;
|
|
54
|
+
a?: Leaf;
|
|
55
|
+
b?: Leaf;
|
|
56
|
+
}
|
|
57
|
+
/** File-level changes a → b, sorted by path. Identical subtrees are skipped unread. */
|
|
58
|
+
export declare function diff(blocks: TreeBlocks, a: CID, b: CID, opts?: {
|
|
59
|
+
renames?: boolean;
|
|
60
|
+
}): Promise<Change[]>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// The filesystem as records. Files and directories are git objects byte for
|
|
2
|
+
// byte ("blob <len>\0…", "tree <len>\0…"), addressed CIDv1(git-raw, sha1 of
|
|
3
|
+
// the whole object), so the digest *is* git's object id: a real repo, gib and
|
|
4
|
+
// this store share one id space with no translation layer.
|
|
5
|
+
//
|
|
6
|
+
// Pure: hashing and reading only. Scanning a directory and checking a tree out
|
|
7
|
+
// onto a disk are outside the machine (src/dev/scan.ts).
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { CID } from "multiformats/cid";
|
|
10
|
+
import * as Digest from "multiformats/hashes/digest";
|
|
11
|
+
export const GIT_RAW = 0x78;
|
|
12
|
+
const SHA1 = 0x11;
|
|
13
|
+
const MODES = new Set(["100644", "100755", "120000", "40000", "160000"]);
|
|
14
|
+
// ---------------------------------------------------------------- ids
|
|
15
|
+
export function gitCid(sha1hex) {
|
|
16
|
+
if (!/^[0-9a-f]{40}$/.test(sha1hex))
|
|
17
|
+
throw new Error(`not a sha1: ${sha1hex}`);
|
|
18
|
+
return CID.createV1(GIT_RAW, Digest.create(SHA1, Buffer.from(sha1hex, "hex")));
|
|
19
|
+
}
|
|
20
|
+
export function gitSha(cid) {
|
|
21
|
+
if (cid.code !== GIT_RAW || cid.multihash.code !== SHA1)
|
|
22
|
+
throw new Error(`not a git object cid: ${cid}`);
|
|
23
|
+
return Buffer.from(cid.multihash.digest).toString("hex");
|
|
24
|
+
}
|
|
25
|
+
export function sha1Cid(digest) {
|
|
26
|
+
return CID.createV1(GIT_RAW, Digest.create(SHA1, digest));
|
|
27
|
+
}
|
|
28
|
+
export function header(type, len) {
|
|
29
|
+
return Buffer.from(`${type} ${len}\0`, "latin1");
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------- hashing
|
|
32
|
+
export function blobCid(bytes) {
|
|
33
|
+
return sha1Cid(createHash("sha1").update(header("blob", bytes.length)).update(bytes).digest());
|
|
34
|
+
}
|
|
35
|
+
export function hashBlob(bytes) {
|
|
36
|
+
const object = Buffer.concat([header("blob", bytes.length), bytes]);
|
|
37
|
+
return { cid: sha1Cid(createHash("sha1").update(object).digest()), object };
|
|
38
|
+
}
|
|
39
|
+
export function hashTree(entries) {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const keyed = entries.map((e) => {
|
|
42
|
+
if (!MODES.has(e.mode))
|
|
43
|
+
throw new Error(`bad mode ${e.mode} for ${e.name}`);
|
|
44
|
+
if (!e.name || e.name.includes("/") || e.name.includes("\0"))
|
|
45
|
+
throw new Error(`bad entry name: ${JSON.stringify(e.name)}`);
|
|
46
|
+
if (seen.has(e.name))
|
|
47
|
+
throw new Error(`duplicate entry: ${e.name}`);
|
|
48
|
+
seen.add(e.name);
|
|
49
|
+
gitSha(e.cid); // throws unless it is a git object id
|
|
50
|
+
// git orders as if a directory's name ended in "/" (so "foo.txt" < "foo" dir < "foo0").
|
|
51
|
+
return { e, key: Buffer.from(e.mode === "40000" ? `${e.name}/` : e.name, "utf8") };
|
|
52
|
+
});
|
|
53
|
+
keyed.sort((a, b) => Buffer.compare(a.key, b.key));
|
|
54
|
+
const parts = [];
|
|
55
|
+
for (const { e } of keyed)
|
|
56
|
+
parts.push(Buffer.from(`${e.mode} ${e.name}\0`, "utf8"), e.cid.multihash.digest);
|
|
57
|
+
const body = Buffer.concat(parts);
|
|
58
|
+
const object = Buffer.concat([header("tree", body.length), body]);
|
|
59
|
+
return { cid: sha1Cid(createHash("sha1").update(object).digest()), object };
|
|
60
|
+
}
|
|
61
|
+
/** The empty tree: what a run with no tree starts from (the kernel's tree.zig hashTree(&.{})). */
|
|
62
|
+
export const EMPTY_TREE = hashTree([]).cid;
|
|
63
|
+
// ---------------------------------------------------------------- reading
|
|
64
|
+
function body(object, type, cid) {
|
|
65
|
+
const buf = Buffer.from(object.buffer, object.byteOffset, object.byteLength);
|
|
66
|
+
const nul = buf.indexOf(0);
|
|
67
|
+
const head = nul < 0 ? "" : buf.toString("latin1", 0, nul);
|
|
68
|
+
if (head !== `${type} ${buf.length - nul - 1}`)
|
|
69
|
+
throw new Error(`not a git ${type}: ${cid}`);
|
|
70
|
+
return buf.subarray(nul + 1);
|
|
71
|
+
}
|
|
72
|
+
export function parseTree(object, cid) {
|
|
73
|
+
const b = body(object, "tree", cid);
|
|
74
|
+
const out = [];
|
|
75
|
+
let i = 0;
|
|
76
|
+
while (i < b.length) {
|
|
77
|
+
const sp = b.indexOf(0x20, i);
|
|
78
|
+
const z = sp < 0 ? -1 : b.indexOf(0, sp);
|
|
79
|
+
if (z < 0 || z + 21 > b.length)
|
|
80
|
+
throw new Error(`truncated tree: ${cid}`);
|
|
81
|
+
const mode = b.toString("latin1", i, sp);
|
|
82
|
+
if (!MODES.has(mode))
|
|
83
|
+
throw new Error(`unsupported mode ${mode} in ${cid}`);
|
|
84
|
+
out.push({ mode: mode, name: b.toString("utf8", sp + 1, z), cid: sha1Cid(b.subarray(z + 1, z + 21)) });
|
|
85
|
+
i = z + 21;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
export async function readTree(blocks, cid) {
|
|
90
|
+
gitSha(cid);
|
|
91
|
+
return parseTree(await blocks.bytes(cid), cid);
|
|
92
|
+
}
|
|
93
|
+
export async function readBlob(blocks, cid) {
|
|
94
|
+
gitSha(cid);
|
|
95
|
+
return body(await blocks.bytes(cid), "blob", cid);
|
|
96
|
+
}
|
|
97
|
+
/** The entry at `path` ("" is the root tree itself), or undefined. */
|
|
98
|
+
export async function lookup(blocks, tree, path) {
|
|
99
|
+
let at = { mode: "40000", cid: tree };
|
|
100
|
+
for (const seg of path.split("/").filter(Boolean)) {
|
|
101
|
+
if (at.mode !== "40000")
|
|
102
|
+
return undefined;
|
|
103
|
+
const e = (await readTree(blocks, at.cid)).find((x) => x.name === seg);
|
|
104
|
+
if (!e)
|
|
105
|
+
return undefined;
|
|
106
|
+
at = e;
|
|
107
|
+
}
|
|
108
|
+
return at;
|
|
109
|
+
}
|
|
110
|
+
/** File contents (a symlink's contents are its target). */
|
|
111
|
+
export async function readFile(blocks, tree, path) {
|
|
112
|
+
const e = await lookup(blocks, tree, path);
|
|
113
|
+
if (!e)
|
|
114
|
+
throw new Error(`no such path: ${path}`);
|
|
115
|
+
if (e.mode === "40000" || e.mode === "160000")
|
|
116
|
+
throw new Error(`not a file: ${path}`);
|
|
117
|
+
return readBlob(blocks, e.cid);
|
|
118
|
+
}
|
|
119
|
+
/** Every entry under `path`, depth first in tree order. Directories only with `dirs`. */
|
|
120
|
+
export async function* walk(blocks, tree, path = "", opts = {}) {
|
|
121
|
+
const start = await lookup(blocks, tree, path);
|
|
122
|
+
if (!start || start.mode !== "40000")
|
|
123
|
+
throw new Error(`not a directory: ${path}`);
|
|
124
|
+
const prefix = path.split("/").filter(Boolean).join("/");
|
|
125
|
+
async function* rec(cid, pre) {
|
|
126
|
+
for (const e of await readTree(blocks, cid)) {
|
|
127
|
+
const p = pre ? `${pre}/${e.name}` : e.name;
|
|
128
|
+
if (e.mode === "40000") {
|
|
129
|
+
if (opts.dirs)
|
|
130
|
+
yield { path: p, mode: e.mode, cid: e.cid };
|
|
131
|
+
yield* rec(e.cid, p);
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
yield { path: p, mode: e.mode, cid: e.cid };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
yield* rec(start.cid, prefix);
|
|
138
|
+
}
|
|
139
|
+
/** File-level changes a → b, sorted by path. Identical subtrees are skipped unread. */
|
|
140
|
+
export async function diff(blocks, a, b, opts = {}) {
|
|
141
|
+
const out = [];
|
|
142
|
+
const all = async (cid, pre, type) => {
|
|
143
|
+
for await (const e of walk(blocks, cid))
|
|
144
|
+
out.push({ type, path: `${pre}/${e.path}`, [type === "added" ? "b" : "a"]: { mode: e.mode, cid: e.cid } });
|
|
145
|
+
};
|
|
146
|
+
const rec = async (x, y, pre) => {
|
|
147
|
+
const ex = new Map((await readTree(blocks, x)).map((e) => [e.name, e]));
|
|
148
|
+
const ey = new Map((await readTree(blocks, y)).map((e) => [e.name, e]));
|
|
149
|
+
for (const name of new Set([...ex.keys(), ...ey.keys()])) {
|
|
150
|
+
const l = ex.get(name), r = ey.get(name);
|
|
151
|
+
if (l && r && l.mode === r.mode && l.cid.equals(r.cid))
|
|
152
|
+
continue;
|
|
153
|
+
const p = pre ? `${pre}/${name}` : name;
|
|
154
|
+
const lDir = l?.mode === "40000", rDir = r?.mode === "40000";
|
|
155
|
+
if (lDir && rDir) {
|
|
156
|
+
await rec(l.cid, r.cid, p);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (lDir)
|
|
160
|
+
await all(l.cid, p, "removed");
|
|
161
|
+
if (rDir)
|
|
162
|
+
await all(r.cid, p, "added");
|
|
163
|
+
const lf = l && !lDir ? { mode: l.mode, cid: l.cid } : undefined;
|
|
164
|
+
const rf = r && !rDir ? { mode: r.mode, cid: r.cid } : undefined;
|
|
165
|
+
if (lf && rf)
|
|
166
|
+
out.push({ type: "modified", path: p, a: lf, b: rf });
|
|
167
|
+
else if (lf)
|
|
168
|
+
out.push({ type: "removed", path: p, a: lf });
|
|
169
|
+
else if (rf)
|
|
170
|
+
out.push({ type: "added", path: p, b: rf });
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
await rec(a, b, "");
|
|
174
|
+
let changes = out;
|
|
175
|
+
if (opts.renames ?? true) {
|
|
176
|
+
// Exact renames only: the same blob gone from one path and new at another.
|
|
177
|
+
const gone = new Map();
|
|
178
|
+
for (const c of out) {
|
|
179
|
+
if (c.type !== "removed")
|
|
180
|
+
continue;
|
|
181
|
+
const k = c.a.cid.toString();
|
|
182
|
+
gone.set(k, [...(gone.get(k) ?? []), c]);
|
|
183
|
+
}
|
|
184
|
+
const paired = new Set();
|
|
185
|
+
changes = out.flatMap((c) => {
|
|
186
|
+
if (c.type !== "added")
|
|
187
|
+
return [c];
|
|
188
|
+
const from = gone.get(c.b.cid.toString())?.shift();
|
|
189
|
+
if (!from)
|
|
190
|
+
return [c];
|
|
191
|
+
paired.add(from);
|
|
192
|
+
return [{ type: "renamed", path: c.path, from: from.path, a: from.a, b: c.b }];
|
|
193
|
+
}).filter((c) => !paired.has(c));
|
|
194
|
+
}
|
|
195
|
+
return changes.sort((x, y) => (x.path < y.path ? -1 : x.path > y.path ? 1 : 0));
|
|
196
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
export type Ms = number;
|
|
3
|
+
export type Rel = "depends-on" | "launched" | "produced" | "about" | "replies-to" | (string & {});
|
|
4
|
+
export interface Ref {
|
|
5
|
+
to: CID | string;
|
|
6
|
+
rel: Rel;
|
|
7
|
+
locator?: string;
|
|
8
|
+
}
|
|
9
|
+
export type ThreadState = "running" | "waiting" | "finished" | "errored" | "out-of-context" | "dropped";
|
|
10
|
+
export interface ThreadOrigin {
|
|
11
|
+
kind: "thread";
|
|
12
|
+
program: CID;
|
|
13
|
+
args: unknown;
|
|
14
|
+
launchedBy?: CID;
|
|
15
|
+
input?: CID;
|
|
16
|
+
at: Ms;
|
|
17
|
+
nonce?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ThreadUpdate {
|
|
20
|
+
origin: CID;
|
|
21
|
+
prev: CID;
|
|
22
|
+
seq: number;
|
|
23
|
+
at: Ms;
|
|
24
|
+
state: ThreadState;
|
|
25
|
+
waitingOn?: CID[];
|
|
26
|
+
waitingFrom?: string;
|
|
27
|
+
awaits?: CID[];
|
|
28
|
+
until?: Ms;
|
|
29
|
+
resolution?: CID;
|
|
30
|
+
error?: {
|
|
31
|
+
kind: "cant-do" | "blew-up";
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
note?: string;
|
|
35
|
+
input?: CID;
|
|
36
|
+
refs?: Ref[];
|
|
37
|
+
result?: unknown;
|
|
38
|
+
}
|
|
39
|
+
export type Emission = {
|
|
40
|
+
type: "thinking";
|
|
41
|
+
text: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: "text";
|
|
44
|
+
text: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: "say";
|
|
47
|
+
text: string;
|
|
48
|
+
call?: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: "page";
|
|
51
|
+
markdown: string;
|
|
52
|
+
call?: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: "launched";
|
|
55
|
+
thread: CID;
|
|
56
|
+
label?: string;
|
|
57
|
+
call?: string;
|
|
58
|
+
} | {
|
|
59
|
+
type: "tool_result";
|
|
60
|
+
thread: CID;
|
|
61
|
+
ok: boolean;
|
|
62
|
+
content: string;
|
|
63
|
+
call?: string;
|
|
64
|
+
} | {
|
|
65
|
+
type: "conclusion";
|
|
66
|
+
text: string;
|
|
67
|
+
exitCode?: number;
|
|
68
|
+
} | {
|
|
69
|
+
type: string;
|
|
70
|
+
[k: string]: unknown;
|
|
71
|
+
};
|
|
72
|
+
export interface Rest {
|
|
73
|
+
state: ThreadState;
|
|
74
|
+
waitingOn?: CID[];
|
|
75
|
+
}
|
|
76
|
+
export interface NodeOrigin {
|
|
77
|
+
kind: "node";
|
|
78
|
+
thread: CID;
|
|
79
|
+
prev: CID[];
|
|
80
|
+
request: unknown;
|
|
81
|
+
refs: Ref[];
|
|
82
|
+
at: Ms;
|
|
83
|
+
nonce?: string;
|
|
84
|
+
input?: CID;
|
|
85
|
+
}
|
|
86
|
+
export interface NodeUpdate {
|
|
87
|
+
origin: CID;
|
|
88
|
+
prev: CID;
|
|
89
|
+
seq: number;
|
|
90
|
+
at: Ms;
|
|
91
|
+
emit?: Emission;
|
|
92
|
+
rest?: Rest;
|
|
93
|
+
input?: CID;
|
|
94
|
+
}
|
|
95
|
+
export type Origin = ThreadOrigin | NodeOrigin;
|
|
96
|
+
export type Update = ThreadUpdate | NodeUpdate;
|
|
97
|
+
export type Block = Origin | Update | {
|
|
98
|
+
kind: string;
|
|
99
|
+
[k: string]: unknown;
|
|
100
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Block shapes. Every value here is encoded as dag-cbor and identified by its
|
|
2
|
+
// CIDv1 (codec dag-cbor 0x71, multihash sha2-256 0x12). See docs/MODEL.md.
|
|
3
|
+
//
|
|
4
|
+
// CIDs are carried as multiformats `CID` instances inside blocks (dag-cbor
|
|
5
|
+
// tag 42) and as base32 strings (`bafy…`) everywhere a string is needed:
|
|
6
|
+
// the CLI, the index tables, URLs (`skein://bafy…`).
|
|
7
|
+
export {};
|