@dcasegr/skein 0.0.0-stage → 0.1.1
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 +522 -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 +773 -0
- package/dist/host/router.js +2389 -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,353 @@
|
|
|
1
|
+
// Format 10 (#146: the BEEF envelope beside the pointer record; format 9, #143: routes without senders, filters, roles and grants, root instead of an owner; format 8,
|
|
2
|
+
// #77: the kernel's tables — one route table, admin operations in the kernel, write scope by app name;
|
|
3
|
+
// format 7, #65, #69; format 6, #70, #67; #68: requests appended as received; format 3, #40; format 2,
|
|
4
|
+
// #33: unsigned entries, keys as bytes) on the host's side: the genesis a new instance's log starts with,
|
|
5
|
+
// and entries.
|
|
6
|
+
//
|
|
7
|
+
// entry {kind: "log", prev, n, time, genesis | request+transport | mail | event+box} — no `sig`: no host key;
|
|
8
|
+
// the genesis entry is built here (nextEntry: n = 0, no prev); every other the kernel appends
|
|
9
|
+
// (#163: Kernel.admit hands over the body and its stamp, the kernel assigns prev and n)
|
|
10
|
+
// genesis {kind: "genesis", identity: bytes(33), root?: [bytes(33)], handle, domain, programs,
|
|
11
|
+
// dispatch: [<route>], roles?: {<role>: ["<program>.<fn>"]},
|
|
12
|
+
// scopes?: {<program name>: [<head name | prefix/>]}, peers?: {role: bytes(33)},
|
|
13
|
+
// defaults, names?: [{identityKey: bytes(33), handle, domain}], collect, tree?,
|
|
14
|
+
// feeds?: [{kind: "headers", url, box?}] (#58: statuses come from the host's broadcaster, arc.ts),
|
|
15
|
+
// libp2p?: {topics: [string], protocols: [string], listen?: [multiaddr]},
|
|
16
|
+
// addressBook?: [{key: bytes(33), transport, address, handle?, domain?}],
|
|
17
|
+
// heads?: {<head name>: <record CID>}} (#141: an image's installed apps, `<app>/app`)
|
|
18
|
+
// route {transport: "mailbox" | "event" | "http" | "libp2p" | "local", address, prefix?: true,
|
|
19
|
+
// filters?: ["kernel.brc104" | "kernel.beef" | "<app>.<filter>"], program?: <program record CID> | "kernel",
|
|
20
|
+
// fn?, …settings}
|
|
21
|
+
// (`dispatch`: the seed of the kernel's route table, docs/APPS.md §2 — the admin routes (boxes
|
|
22
|
+
// objects, head, dispatch, peers, grant → the kernel, gated by root), the boxes programs take,
|
|
23
|
+
// the events the host's wiring brings, the HTTP paths and libp2p topics and protocols. No
|
|
24
|
+
// `sender` (#143): who may run a function is the grants'. `root`: the initial root holders
|
|
25
|
+
// (#143; #142: the host skein's operator key) — none: an image, its routes carry the claim
|
|
26
|
+
// route and root comes with the claim. `roles`: what gates the genesis's own programs'
|
|
27
|
+
// functions — STOCK_ROLES: the explorer is root's. `scopes`: the heads a genesis-wired
|
|
28
|
+
// program may advance — STOCK_SCOPES, plus a tree's config `scopes`.)
|
|
29
|
+
// (`addressBook`, #70: the address book's seed — the host's providers (transport `local`, address
|
|
30
|
+
// their name: fetch, waker, cron, libp2p, status; #126: no role) and the root holder's mailbox —
|
|
31
|
+
// written into the head `peers` at the genesis)
|
|
32
|
+
// (no `jobs`, #69: a schedule is a program's message to the cron provider; a genesis naming
|
|
33
|
+
// them is refused)
|
|
34
|
+
// (`libp2p`, #51: the host's libp2p node runs for the instance, subscribes `topics` and serves
|
|
35
|
+
// `protocols`; each message or frame is a front-door step routed by its `libp2p` route)
|
|
36
|
+
// (`feeds`: what the host holds for the instance, feeds.ts; `defaults.ownerMessagebox`: the root
|
|
37
|
+
// holder's messagebox URL — the one peer a genesis names; `defaults.resolveOrigin`: where the
|
|
38
|
+
// instance's own domain is looked up (a dev host))
|
|
39
|
+
//
|
|
40
|
+
// The stamp is the host's clock at admission (#10): the sequence is the
|
|
41
|
+
// order. A genesis is written from a **system** (issue #4, docs/BOOTSTRAP.md):
|
|
42
|
+
// the programs, the routes, the config — read from a system tree by the
|
|
43
|
+
// loader (boot.ts), which then also names the tree (`tree`: `main` starts
|
|
44
|
+
// there), or, for an instance with no tree, the stock system in code
|
|
45
|
+
// (codeSystem: the kernel's own pinned programs and STOCK_DISPATCH). The same
|
|
46
|
+
// `genesisRecord` writes both. The forms before #77 and #115
|
|
47
|
+
// (etc/subscriptions.json, etc/routes.json, etc/reads.json) are gone (#143):
|
|
48
|
+
// they spoke of senders.
|
|
49
|
+
import { parse as parseCid } from "../runtime/cid.js";
|
|
50
|
+
import { isFilterRef, rowKey } from "../runtime/dispatch.js";
|
|
51
|
+
import { DEFAULTS, nextEntry } from "../runtime/log.js";
|
|
52
|
+
import { now as clockNow } from "./clock.js";
|
|
53
|
+
/** Session expiry (#33 part 2, #40): the instance's own policy, judged against the session record's stamp (a day). */
|
|
54
|
+
export const SESSION_DEFAULTS = { sessionTtlMs: "86400000" };
|
|
55
|
+
export const keyBytes = (hex) => {
|
|
56
|
+
if (!/^0[23][0-9a-f]{64}$/.test(hex))
|
|
57
|
+
throw new TypeError(`not an identity key: ${hex}`);
|
|
58
|
+
return Uint8Array.from(Buffer.from(hex, "hex"));
|
|
59
|
+
};
|
|
60
|
+
export const keyHex = (k) => k instanceof Uint8Array ? Buffer.from(k).toString("hex") : String(k);
|
|
61
|
+
/**
|
|
62
|
+
* The kernel's admin operations (#77, #143: and `grant`), one per table, each gated by root. Every
|
|
63
|
+
* genesis carries their routes — `{address: <op>, program: kernel, fn: <op>}` — ahead of its own
|
|
64
|
+
* (#31: a bare genesis is the tables and the admin routes; a bundle adds what its apps ask for).
|
|
65
|
+
* Delegating is a grant (root, or an app's role), never another route.
|
|
66
|
+
*/
|
|
67
|
+
export const ADMIN_OPS = ["objects", "head", "dispatch", "peers", "grant"];
|
|
68
|
+
export const ADMIN_ROWS = ADMIN_OPS.map((op) => ({ address: op, program: "kernel", fn: op }));
|
|
69
|
+
/** What a kernel route may name: the admin operations, the claim (#89), and the host's tick (#130: the host row, billing.ts). */
|
|
70
|
+
export const KERNEL_OPS = [...ADMIN_OPS, "claim", "tick"];
|
|
71
|
+
/**
|
|
72
|
+
* The claim route (#89, #143): an image's route to the kernel. A message in box `claim`, from anyone:
|
|
73
|
+
* root is granted to its sender (#127) and this route removed (a second claim finds no route). The
|
|
74
|
+
* claimant's own message (`skein claim`), or a claim signed before the instance existed, which the
|
|
75
|
+
* host's instance manager forwards as its first entry before the hostname is published
|
|
76
|
+
* (Router.createInstance).
|
|
77
|
+
*/
|
|
78
|
+
export const CLAIM_ROW = { address: "claim", program: "kernel", fn: "claim" };
|
|
79
|
+
/** The BRC-103 handshake's route (#143): the front door's, no filters (it makes the session). */
|
|
80
|
+
export const HANDSHAKE_ROW = { transport: "http", address: "/.well-known/auth", program: "frontdoor", fn: "handshake" };
|
|
81
|
+
/**
|
|
82
|
+
* The stock HTTP routes (#40, #143): the BRC-33 messagebox, at the root and under /messagebox (where
|
|
83
|
+
* older clients were pointed), behind kernel.brc104 (a session: the caller is its key); the
|
|
84
|
+
* explorer's route (the front door's `explore`: log, threads, heads, records as JSON), behind
|
|
85
|
+
* kernel.brc104 and gated by root (STOCK_ROLES); the handshake.
|
|
86
|
+
*/
|
|
87
|
+
export const STOCK_HTTP = [
|
|
88
|
+
HANDSHAKE_ROW,
|
|
89
|
+
...["", "/messagebox"].flatMap((p) => ["sendMessage", "listMessages", "acknowledgeMessage"].map((fn) => ({ transport: "http", address: `${p}/${fn}`, filters: ["kernel.brc104"], program: "messagebox", fn }))),
|
|
90
|
+
{ transport: "http", address: "/explore", prefix: true, filters: ["kernel.brc104"], program: "frontdoor", fn: "explore" },
|
|
91
|
+
];
|
|
92
|
+
/** What gates the genesis's own programs' functions (#143): the explorer is root's. */
|
|
93
|
+
export const STOCK_ROLES = { root: ["frontdoor.explore"] };
|
|
94
|
+
/**
|
|
95
|
+
* The stock seed (#77), after the admin routes: the reserved box the host admits events into, `:ack`
|
|
96
|
+
* (the messagebox moves a reader's pointer: an event, #143); and the stock HTTP routes. No `run`, no
|
|
97
|
+
* `chat` (#83): the shell and the chat loop are apps. No `register` box (#40): registration is
|
|
98
|
+
* application wiring.
|
|
99
|
+
*/
|
|
100
|
+
export const STOCK_DISPATCH = [
|
|
101
|
+
{ transport: "event", address: ":ack", program: "messagebox" },
|
|
102
|
+
...STOCK_HTTP,
|
|
103
|
+
];
|
|
104
|
+
/**
|
|
105
|
+
* A mailbox instance's seed (#40): the messagebox's acknowledgements, and every message in any box to
|
|
106
|
+
* the messagebox (kept for its root; a mailbox exists only where such a route does).
|
|
107
|
+
*/
|
|
108
|
+
export const MAILBOX_DISPATCH = [
|
|
109
|
+
{ transport: "event", address: ":ack", program: "messagebox" },
|
|
110
|
+
{ address: "*", program: "messagebox" },
|
|
111
|
+
...STOCK_HTTP,
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* The write scopes of the stock genesis-wired programs (#77): the heads each
|
|
115
|
+
* may advance, by program name. The front door's sessions; the messagebox's
|
|
116
|
+
* lists (#126: its outbound sessions are the kernel's, authfetch); the wallet's records (#79: `wallet/…`, its
|
|
117
|
+
* coins, actions, drafts and outputs — the chain is the chain app's). The
|
|
118
|
+
* resolve program's records (#87: `resolve/peers`, what a BRC-169 lookup
|
|
119
|
+
* found — never the address book, which only root's `peers` messages
|
|
120
|
+
* change; the messagebox's delivery reads them for a key the address book
|
|
121
|
+
* does not name).
|
|
122
|
+
*/
|
|
123
|
+
export const STOCK_SCOPES = {
|
|
124
|
+
frontdoor: ["frontdoor/"],
|
|
125
|
+
messagebox: ["mailbox"],
|
|
126
|
+
resolve: ["resolve/"],
|
|
127
|
+
wallet: ["wallet/"],
|
|
128
|
+
// #78: the chain module (shruggr/skein-chain) wired at boot by a system tree as `bin/chain.wasm`:
|
|
129
|
+
// its heads are `chain/…`, as an installed chain app's are by the name rule.
|
|
130
|
+
chain: ["chain/"],
|
|
131
|
+
};
|
|
132
|
+
/** A system's libp2p, checked: the genesis's `libp2p` and the routes its protocols add (those the tree lacks). */
|
|
133
|
+
export function libp2pIn(spec, specs) {
|
|
134
|
+
if (spec === undefined)
|
|
135
|
+
return { specs };
|
|
136
|
+
const bad = (why) => new Error(`etc/config.json: libp2p ${why} (want {topics?: [string], protocols?: {protocol: program | {program, fn?}}, listen?: [multiaddr]})`);
|
|
137
|
+
if (!spec || typeof spec !== "object")
|
|
138
|
+
throw bad("is not an object");
|
|
139
|
+
const strs = (v, what) => {
|
|
140
|
+
if (v === undefined)
|
|
141
|
+
return [];
|
|
142
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string" || !x))
|
|
143
|
+
throw bad(`${what} is not a list of strings`);
|
|
144
|
+
return v;
|
|
145
|
+
};
|
|
146
|
+
const topics = strs(spec.topics, "topics");
|
|
147
|
+
const listen = strs(spec.listen, "listen");
|
|
148
|
+
const ps = spec.protocols ?? {};
|
|
149
|
+
if (typeof ps !== "object" || Array.isArray(ps))
|
|
150
|
+
throw bad("protocols is not an object");
|
|
151
|
+
const out = [...specs];
|
|
152
|
+
for (const [protocol, h] of Object.entries(ps)) {
|
|
153
|
+
const program = typeof h === "string" ? h : h?.program;
|
|
154
|
+
if (!protocol || typeof program !== "string" || !program)
|
|
155
|
+
throw bad(`protocols.${protocol} names no program`);
|
|
156
|
+
if (!out.some((r) => r.transport === "libp2p" && r.address === protocol))
|
|
157
|
+
out.push({ transport: "libp2p", address: protocol, program, fn: (typeof h === "object" && h.fn) || "libp2p" });
|
|
158
|
+
}
|
|
159
|
+
return { libp2p: { topics, protocols: Object.keys(ps), ...(listen.length ? { listen } : {}) }, specs: out };
|
|
160
|
+
}
|
|
161
|
+
/** A key as a system writes it: hex, or `$self` / `$infer` (the host's), or a provider's `$<name>` (Genesis2Config.providers). */
|
|
162
|
+
export function keyOf(s, c) {
|
|
163
|
+
if (s === "$owner")
|
|
164
|
+
throw new Error("$owner: gone (#143: there is no owner; root holders are the genesis's `root`, granted by a claim or a grant)");
|
|
165
|
+
if (s === "$self") {
|
|
166
|
+
if (!c.identity)
|
|
167
|
+
throw new Error("$self: no instance identity here");
|
|
168
|
+
return keyBytes(c.identity);
|
|
169
|
+
}
|
|
170
|
+
if (s === "$infer") {
|
|
171
|
+
if (!c.infer)
|
|
172
|
+
throw new Error("the system names $infer, and this host has no inference peer (SKEIN_INFER)");
|
|
173
|
+
return keyBytes(c.infer);
|
|
174
|
+
}
|
|
175
|
+
if (s.startsWith("$")) {
|
|
176
|
+
const k = c.providers?.[s.slice(1)];
|
|
177
|
+
if (!k)
|
|
178
|
+
throw new NoProvider(s);
|
|
179
|
+
return keyBytes(k);
|
|
180
|
+
}
|
|
181
|
+
return keyBytes(s);
|
|
182
|
+
}
|
|
183
|
+
/** A `$<name>` that this host has no provider for. */
|
|
184
|
+
export class NoProvider extends Error {
|
|
185
|
+
provider;
|
|
186
|
+
constructor(provider) { super(`no provider ${provider} on this host`); this.provider = provider; }
|
|
187
|
+
}
|
|
188
|
+
/** The program a spec names: by name in `programs`, else a CID; `kernel` for an admin route. */
|
|
189
|
+
function programOf(h, programs) {
|
|
190
|
+
if (h === "kernel")
|
|
191
|
+
return "kernel";
|
|
192
|
+
const p = programs[h];
|
|
193
|
+
if (p)
|
|
194
|
+
return p;
|
|
195
|
+
try {
|
|
196
|
+
return parseCid(h);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
} // neither a program's name nor a CID: the caller refuses the route and says so
|
|
201
|
+
}
|
|
202
|
+
const isBox = (b) => typeof b === "string" && b.length > 0 && !/[\s\0]/.test(b);
|
|
203
|
+
const TRANSPORTS = ["mailbox", "event", "http", "libp2p", "local"];
|
|
204
|
+
/** The routes of a system's specs (etc/dispatch.json, #77, #143), programs resolved; a route to a program the system lacks is dropped. */
|
|
205
|
+
export function rowsOf(specs, programs) {
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const s of specs) {
|
|
208
|
+
if (!s || typeof s !== "object" || !isBox(s.address) || (s.program !== undefined && typeof s.program !== "string"))
|
|
209
|
+
throw new Error(`etc/dispatch.json: bad route ${JSON.stringify(s)} (want {transport?, address, prefix?, filters?, program?, fn?, …})`);
|
|
210
|
+
if (s.sender !== undefined)
|
|
211
|
+
throw new Error(`etc/dispatch.json: route ${s.address}: \`sender\` is gone (#143: a route has none — filters say who it is from, roles gate functions)`);
|
|
212
|
+
const transport = s.transport ?? "mailbox";
|
|
213
|
+
if (!TRANSPORTS.includes(transport))
|
|
214
|
+
throw new Error(`etc/dispatch.json: route ${s.address}: transport ${JSON.stringify(transport)} is not mailbox, event, http, libp2p or local`);
|
|
215
|
+
if (s.filters !== undefined && (!Array.isArray(s.filters) || !s.filters.every(isFilterRef)))
|
|
216
|
+
throw new Error(`etc/dispatch.json: route ${s.address}: filters is a list of "kernel.brc104", "kernel.beef" or "<app>.<filter>"`);
|
|
217
|
+
if (s.program === undefined) {
|
|
218
|
+
if (!(transport === "http" || (transport === "libp2p" && s.address.startsWith("/"))) || !s.filters?.length)
|
|
219
|
+
throw new Error(`etc/dispatch.json: route ${s.address}: no program — only an http or libp2p protocol (/…) read route (its filters answer) has none`);
|
|
220
|
+
const { transport: _t, ...rest } = s;
|
|
221
|
+
void _t;
|
|
222
|
+
out.push({ ...rest, transport });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const program = programOf(s.program, programs);
|
|
226
|
+
if (!program)
|
|
227
|
+
continue;
|
|
228
|
+
if (program === "kernel" && (typeof s.fn !== "string" || !KERNEL_OPS.includes(s.fn)))
|
|
229
|
+
throw new Error(`etc/dispatch.json: route ${s.address}: a kernel route's fn is one of ${KERNEL_OPS.join(", ")}`);
|
|
230
|
+
const { transport: _t, address, prefix, filters, program: _p, fn, ...settings } = s;
|
|
231
|
+
void _t;
|
|
232
|
+
void _p;
|
|
233
|
+
out.push({ transport, address, ...(prefix ? { prefix: true } : {}), ...(filters?.length ? { filters } : {}), program, ...(fn ? { fn } : {}), ...settings });
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Resolve a system's routes, config and programs for one instance (both genesis paths). `specs` is
|
|
239
|
+
* its etc/dispatch.json. The order: the admin routes (every genesis), the dispatch specs, the
|
|
240
|
+
* host's extra box routes and routes, then the protocols' routes. A route with an admin route's key
|
|
241
|
+
* is left out (the kernel's operation keeps the box), with a warning.
|
|
242
|
+
*/
|
|
243
|
+
export function resolveSystem(c, programs, config = {}, tree, specs = []) {
|
|
244
|
+
if (config.jobs !== undefined)
|
|
245
|
+
throw new Error("etc/config.json: `jobs` are gone (#69): a schedule is a program's message to the cron provider ({fn: \"tick\", every | at, box, body?, name}, docs/MESSAGES.md)");
|
|
246
|
+
if (config.scopes !== undefined && (typeof config.scopes !== "object" || Array.isArray(config.scopes) || Object.values(config.scopes).some((v) => !Array.isArray(v) || v.some((h) => typeof h !== "string" || !h))))
|
|
247
|
+
throw new Error("etc/config.json: scopes is {<program name>: [<head name | prefix/>]}");
|
|
248
|
+
if (config.roles !== undefined && (typeof config.roles !== "object" || Array.isArray(config.roles) || Object.values(config.roles).some((v) => !Array.isArray(v) || v.some((f) => typeof f !== "string" || !f.includes(".")))))
|
|
249
|
+
throw new Error("etc/config.json: roles is {<role>: [\"<program>.<fn>\"]}");
|
|
250
|
+
const p2p = libp2pIn(config.libp2p ?? c.libp2p, [...specs, ...(c.subscriptions ?? []).map((x) => ({ address: x.box, program: x.handler.toString() })), ...(c.dispatch ?? [])]);
|
|
251
|
+
const admin = rowsOf(ADMIN_ROWS, programs);
|
|
252
|
+
const dispatch = [...admin];
|
|
253
|
+
for (const r of rowsOf(p2p.specs, programs)) {
|
|
254
|
+
if (dispatch.some((a) => rowKey(a) === rowKey(r))) {
|
|
255
|
+
if (admin.some((a) => rowKey(a) === rowKey(r)) && r.program !== "kernel")
|
|
256
|
+
c.warn?.(`a route for the admin box ${r.address} is left out: the kernel's ${r.address} operation keeps it`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
dispatch.push(r);
|
|
260
|
+
}
|
|
261
|
+
const root = c.root ?? [];
|
|
262
|
+
// A tree booted with root and no root holder's handle (#142: the host skein from its image) names no one.
|
|
263
|
+
let names = root.length && (c.ownerHandle || !tree) ? [{ identityKey: keyBytes(root[0]), ...(c.ownerHandle ?? { handle: "david", domain: "localhost" }) }] : [];
|
|
264
|
+
if (c.infer)
|
|
265
|
+
names.push({ identityKey: keyBytes(c.infer), ...(c.inferHandle ?? { handle: "infer", domain: "localhost" }) });
|
|
266
|
+
if (config.names)
|
|
267
|
+
names = config.names.map((n) => ({ identityKey: keyOf(n.identityKey, c), handle: n.handle, domain: n.domain }));
|
|
268
|
+
const peers = config.peers
|
|
269
|
+
? Object.fromEntries(Object.entries(config.peers).map(([role, k]) => [role, keyOf(k, c)]))
|
|
270
|
+
: c.infer ? { infer: keyBytes(c.infer) } : undefined;
|
|
271
|
+
return {
|
|
272
|
+
programs, dispatch, roles: mergeRoles(STOCK_ROLES, config.roles ?? {}), scopes: { ...STOCK_SCOPES, ...c.scopes, ...config.scopes }, ...(peers ? { peers } : {}),
|
|
273
|
+
defaults: mergeDefaults(c, { ...(config.owner?.messagebox ? { ownerMessagebox: config.owner.messagebox } : {}), ...config.defaults }),
|
|
274
|
+
names, collect: config.collect ?? ["completions"], ...(tree ? { tree } : {}),
|
|
275
|
+
...(feedsIn(config.feeds ?? c.feeds)),
|
|
276
|
+
...(p2p.libp2p ? { libp2p: p2p.libp2p } : {}),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** Two role maps joined (#143): each role's functions, both lists' (a tree's roles add to the stock ones, never replace them). */
|
|
280
|
+
function mergeRoles(a, b) {
|
|
281
|
+
const out = {};
|
|
282
|
+
for (const [r, fs] of [...Object.entries(a), ...Object.entries(b)])
|
|
283
|
+
out[r] = [...new Set([...(out[r] ?? []), ...fs])];
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/** A system's feeds, checked. */
|
|
287
|
+
function feedsIn(fs) {
|
|
288
|
+
if (!fs)
|
|
289
|
+
return {};
|
|
290
|
+
if (!Array.isArray(fs))
|
|
291
|
+
throw new Error("etc/config.json: feeds is a list");
|
|
292
|
+
for (const f of fs) {
|
|
293
|
+
if (f?.kind === "headers" && typeof f.url === "string" && f.url)
|
|
294
|
+
continue;
|
|
295
|
+
if (f?.kind === "arc-callback")
|
|
296
|
+
throw new Error("etc/config.json: the arc-callback feed is gone (#58): transaction statuses come from the host's broadcaster (SKEIN_ARC_URL), routed to every instance holding the transaction");
|
|
297
|
+
throw new Error(`etc/config.json: a feed is {kind: "headers", url, box?}: ${JSON.stringify(f)}`);
|
|
298
|
+
}
|
|
299
|
+
return fs.length ? { feeds: fs } : {};
|
|
300
|
+
}
|
|
301
|
+
/** DEFAULTS < the host's fill-ins (its facts, its network) < the tree's config < explicit overrides (warned when they replace a tree value). */
|
|
302
|
+
function mergeDefaults(c, tree = {}) {
|
|
303
|
+
for (const [k, v] of Object.entries(c.overrides ?? {})) {
|
|
304
|
+
if (tree[k] !== undefined && tree[k] !== v)
|
|
305
|
+
c.warn?.(`host override: ${k} = ${v} replaces the system tree's ${k} = ${tree[k]}`);
|
|
306
|
+
}
|
|
307
|
+
return { ...DEFAULTS, ...SESSION_DEFAULTS, ...hostFacts(c), ...networkDefault(c), ...c.defaults, ...tree, ...c.overrides };
|
|
308
|
+
}
|
|
309
|
+
/** The host's network (SKEIN_NETWORK) as the genesis default `walletNetwork`: a network fact, beside the host facts. */
|
|
310
|
+
function networkDefault(c) {
|
|
311
|
+
return c.network ? { walletNetwork: c.network } : {};
|
|
312
|
+
}
|
|
313
|
+
/** What the host knows at genesis (#40): the root holder's messagebox, where its own domain resolves. */
|
|
314
|
+
function hostFacts(c) {
|
|
315
|
+
return { ...(c.ownerMessagebox ? { ownerMessagebox: c.ownerMessagebox } : {}), ...(c.resolveOrigin ? { resolveOrigin: c.resolveOrigin } : {}) };
|
|
316
|
+
}
|
|
317
|
+
/** The stock system in code: the kernel's pinned programs (its `programs` frame) and the stock seed. */
|
|
318
|
+
export function codeSystem(c, programs) {
|
|
319
|
+
const known = (s) => s.program === undefined || s.program === "kernel" || !!programs[s.program];
|
|
320
|
+
if (c.mailbox) {
|
|
321
|
+
// A mailbox instance: the front door and the messagebox, nothing else.
|
|
322
|
+
const mine = { frontdoor: programs.frontdoor, messagebox: programs.messagebox };
|
|
323
|
+
const s = resolveSystem({ ...c, defaults: undefined, overrides: undefined, infer: undefined, subscriptions: undefined, dispatch: undefined }, mine, { peers: {}, names: [] }, undefined, MAILBOX_DISPATCH);
|
|
324
|
+
return { ...s, collect: [], defaults: { ...SESSION_DEFAULTS, ...networkDefault(c), ...c.defaults, ...c.overrides } };
|
|
325
|
+
}
|
|
326
|
+
// Code genesis has always taken the host's defaults whole (DEFAULTS when none).
|
|
327
|
+
return { ...resolveSystem({ ...c, defaults: undefined, overrides: undefined }, programs, {}, undefined, STOCK_DISPATCH.filter(known)), defaults: { ...SESSION_DEFAULTS, ...hostFacts(c), ...networkDefault(c), ...(c.defaults ? { ...c.defaults, ...c.overrides } : c.overrides ? { ...DEFAULTS, ...c.overrides } : DEFAULTS) } };
|
|
328
|
+
}
|
|
329
|
+
/** The genesis record: who the instance is, and its system. */
|
|
330
|
+
export function genesisRecord(c, s) {
|
|
331
|
+
return {
|
|
332
|
+
kind: "genesis", identity: keyBytes(c.identity), handle: c.handle, domain: c.domain, ...(c.root?.length ? { root: c.root.map(keyBytes) } : {}),
|
|
333
|
+
...(Object.keys(s.roles).length ? { roles: s.roles } : {}),
|
|
334
|
+
...(c.addressBook?.length ? { addressBook: c.addressBook.map((e) => ({ key: e.key, transport: e.transport, address: e.address, ...(e.handle ? { handle: e.handle } : {}), ...(e.domain ? { domain: e.domain } : {}) })) } : {}),
|
|
335
|
+
programs: s.programs, dispatch: s.dispatch, ...(Object.keys(s.scopes).length ? { scopes: s.scopes } : {}), ...(s.peers ? { peers: s.peers } : {}),
|
|
336
|
+
defaults: s.defaults, names: s.names, collect: s.collect, ...(s.tree ? { tree: s.tree } : {}), ...(s.feeds ? { feeds: s.feeds } : {}),
|
|
337
|
+
...(s.libp2p ? { libp2p: s.libp2p } : {}), ...(s.heads && Object.keys(s.heads).length ? { heads: s.heads } : {}),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** The genesis record for a new instance over the kernel's programs (name → program record CID): code genesis. */
|
|
341
|
+
export function genesis2(c, programs) {
|
|
342
|
+
return genesisRecord(c, codeSystem(c, programs));
|
|
343
|
+
}
|
|
344
|
+
/** Write a genesis over `s` into an empty store through its kernel; the entry's CID. */
|
|
345
|
+
export async function writeSystemGenesis(k, c, s, time = clockNow()) {
|
|
346
|
+
const g = await k.store.put(genesisRecord(c, s));
|
|
347
|
+
return await k.store.log.append(await nextEntry(k.store, { genesis: g }, time)); // the kernel's log takes a format-10 entry; Store's type is format 1
|
|
348
|
+
}
|
|
349
|
+
/** Code genesis (the stock system) into an empty store; the entry's CID. */
|
|
350
|
+
export async function writeGenesis(k, c, time = clockNow()) {
|
|
351
|
+
const programs = await k.call("programs");
|
|
352
|
+
return await writeSystemGenesis(k, c, codeSystem(c, programs), time);
|
|
353
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Certificate, type ProtoWallet } from "@bsv/sdk";
|
|
2
|
+
/** §4.5: the handle-certificate type, base64(SHA-256("metanet-handles handle certificate v1")). */
|
|
3
|
+
export declare const HANDLE_CERTIFICATE_TYPE: string;
|
|
4
|
+
/**
|
|
5
|
+
* The revocation outpoint every certificate carries: BRC-52's disabled
|
|
6
|
+
* sentinel. Revocation not implemented — the host has no wallet (#100).
|
|
7
|
+
*/
|
|
8
|
+
export declare const NO_REVOCATION_OUTPOINT: string;
|
|
9
|
+
/** The well-known paths of BRC-169's resolve (§5.2) and search (§5.6) endpoints, on the host's own origin. */
|
|
10
|
+
export declare const RESOLVE_PATH = "/.well-known/metanet-handles/resolve";
|
|
11
|
+
export declare const SEARCH_PATH = "/.well-known/metanet-handles/search";
|
|
12
|
+
/** A certificate as JSON carries it. */
|
|
13
|
+
export type CertificateJson = {
|
|
14
|
+
type: string;
|
|
15
|
+
serialNumber: string;
|
|
16
|
+
subject: string;
|
|
17
|
+
certifier: string;
|
|
18
|
+
revocationOutpoint: string;
|
|
19
|
+
fields: Record<string, string>;
|
|
20
|
+
signature: string;
|
|
21
|
+
};
|
|
22
|
+
/** §4.1: the resolver's copy of the certificate for handle@domain → subject under `serialNumber`, signed by the certifier. */
|
|
23
|
+
export declare function issueHandleCertificate(certifier: ProtoWallet, handle: string, domain: string, subject: string, serialNumber: string): Promise<Certificate>;
|
|
24
|
+
/**
|
|
25
|
+
* The holder's copy (#103, §4.6): the same binding with BRC-52 encrypted
|
|
26
|
+
* fields, and `keyringForSubject` for the wallet's `acquireCertificate`
|
|
27
|
+
* (acquisitionProtocol "direct", keyringRevealer "certifier").
|
|
28
|
+
*/
|
|
29
|
+
export declare function issueSubjectCertificate(certifier: ProtoWallet, handle: string, domain: string, subject: string, serialNumber: string): Promise<{
|
|
30
|
+
certificate: CertificateJson;
|
|
31
|
+
keyringForSubject: Record<string, string>;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* The certifier provider's work (#113, providers.ts): a message from the
|
|
35
|
+
* host skein in box `issue`, body {handle, domain, subject: bytes(33),
|
|
36
|
+
* serialNumber, issuance?} → {certificate, holder: {certificate,
|
|
37
|
+
* keyringForSubject}, serialNumber, issuance?}. A refusal throws (the
|
|
38
|
+
* provider answers {error}).
|
|
39
|
+
*/
|
|
40
|
+
export declare function certify(certifier: ProtoWallet, box: string, b: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The host's BRC-169 certifier (#100, #113; ~/Work/bsv/BRCs peer-to-peer/0169.md
|
|
2
|
+
// §4.1, §4.5): the key that signs the handle certificates, and nothing else.
|
|
3
|
+
// What a handle is, who holds it, which serial number its certificate gets
|
|
4
|
+
// and what was issued when are the host skein's: its onboarding app
|
|
5
|
+
// (shruggr/skein-onboard) registers the handle, puts the issuance record,
|
|
6
|
+
// takes its hash as the serial number, asks the certifier provider to sign
|
|
7
|
+
// (`issue`, providers.ts), and records the answer under its own head. The key
|
|
8
|
+
// stays here, outside every instance: the certifier key (signer.ts
|
|
9
|
+
// certifierKey: the master secret's child under [2, "skein provider"], key ID
|
|
10
|
+
// `certifier`), which the manifest the app serves publishes as
|
|
11
|
+
// `metanet.trust.publicKey` (the address book's `certifier` entry).
|
|
12
|
+
//
|
|
13
|
+
// One issue is two signatures of the same binding (type, serial number,
|
|
14
|
+
// subject, certifier, revocation outpoint):
|
|
15
|
+
//
|
|
16
|
+
// - **The resolver's copy**: `fields.handle` and `fields.domain` Base64 of the
|
|
17
|
+
// plaintext, as in §A.3, not BRC-52 field encryption — resolve answers it to
|
|
18
|
+
// anyone, and a resolver checks the fields against what it resolved (§4.1
|
|
19
|
+
// rule 3). The SDK's Certificate serializes and signs it (BRC-52: protocol
|
|
20
|
+
// [2, "certificate signature"], key ID `<type> <serialNumber>`,
|
|
21
|
+
// counterparty anyone; RFC 6979, so the same bytes for the same binding).
|
|
22
|
+
// - **The holder's copy** (#103): BRC-52 field encryption and a keyring for
|
|
23
|
+
// the subject (the SDK's MasterCertificate.issueCertificateForSubject),
|
|
24
|
+
// because a BRC-100 wallet's `acquireCertificate` (direct) takes only that:
|
|
25
|
+
// it rebuilds a MasterCertificate, which wants a keyring entry per field,
|
|
26
|
+
// and decrypts the fields with it. Its field ciphertexts (and so its
|
|
27
|
+
// signature) differ on every issue.
|
|
28
|
+
//
|
|
29
|
+
// Revocation is not implemented: the host has no wallet. §4.1 wants an
|
|
30
|
+
// outpoint the certifier controls and spends on revocation; the certificate
|
|
31
|
+
// carries BRC-52's disabled sentinel (64 zeros, vout 0) instead. The
|
|
32
|
+
// onboarding app keeps every issue (a new serial each time, the earlier
|
|
33
|
+
// records the trail), which is what revocation will need.
|
|
34
|
+
import { Certificate, Hash, MasterCertificate, Utils } from "@bsv/sdk";
|
|
35
|
+
/** §4.5: the handle-certificate type, base64(SHA-256("metanet-handles handle certificate v1")). */
|
|
36
|
+
export const HANDLE_CERTIFICATE_TYPE = Utils.toBase64(Hash.sha256(Utils.toArray("metanet-handles handle certificate v1", "utf8")));
|
|
37
|
+
/**
|
|
38
|
+
* The revocation outpoint every certificate carries: BRC-52's disabled
|
|
39
|
+
* sentinel. Revocation not implemented — the host has no wallet (#100).
|
|
40
|
+
*/
|
|
41
|
+
export const NO_REVOCATION_OUTPOINT = `${"00".repeat(32)}.0`;
|
|
42
|
+
/** The well-known paths of BRC-169's resolve (§5.2) and search (§5.6) endpoints, on the host's own origin. */
|
|
43
|
+
export const RESOLVE_PATH = "/.well-known/metanet-handles/resolve";
|
|
44
|
+
export const SEARCH_PATH = "/.well-known/metanet-handles/search";
|
|
45
|
+
const KEY = /^0[23][0-9a-f]{64}$/;
|
|
46
|
+
const SERIAL = /^[A-Za-z0-9+/]{43}=$/;
|
|
47
|
+
const asJson = (c) => ({ type: c.type, serialNumber: c.serialNumber, subject: c.subject, certifier: c.certifier, revocationOutpoint: c.revocationOutpoint, fields: { ...c.fields }, signature: c.signature ?? "" });
|
|
48
|
+
/** §4.1: the resolver's copy of the certificate for handle@domain → subject under `serialNumber`, signed by the certifier. */
|
|
49
|
+
export async function issueHandleCertificate(certifier, handle, domain, subject, serialNumber) {
|
|
50
|
+
const { publicKey } = await certifier.getPublicKey({ identityKey: true });
|
|
51
|
+
const fields = { handle: Utils.toBase64(Utils.toArray(handle, "utf8")), domain: Utils.toBase64(Utils.toArray(domain, "utf8")) };
|
|
52
|
+
const c = new Certificate(HANDLE_CERTIFICATE_TYPE, serialNumber, subject, publicKey, NO_REVOCATION_OUTPOINT, fields);
|
|
53
|
+
await c.sign(certifier);
|
|
54
|
+
return c;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The holder's copy (#103, §4.6): the same binding with BRC-52 encrypted
|
|
58
|
+
* fields, and `keyringForSubject` for the wallet's `acquireCertificate`
|
|
59
|
+
* (acquisitionProtocol "direct", keyringRevealer "certifier").
|
|
60
|
+
*/
|
|
61
|
+
export async function issueSubjectCertificate(certifier, handle, domain, subject, serialNumber) {
|
|
62
|
+
const c = await MasterCertificate.issueCertificateForSubject(certifier, subject, { handle, domain }, HANDLE_CERTIFICATE_TYPE, async () => NO_REVOCATION_OUTPOINT, serialNumber);
|
|
63
|
+
return { certificate: asJson(c), keyringForSubject: { ...c.masterKeyring } };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The certifier provider's work (#113, providers.ts): a message from the
|
|
67
|
+
* host skein in box `issue`, body {handle, domain, subject: bytes(33),
|
|
68
|
+
* serialNumber, issuance?} → {certificate, holder: {certificate,
|
|
69
|
+
* keyringForSubject}, serialNumber, issuance?}. A refusal throws (the
|
|
70
|
+
* provider answers {error}).
|
|
71
|
+
*/
|
|
72
|
+
export async function certify(certifier, box, b) {
|
|
73
|
+
if (box !== "issue")
|
|
74
|
+
throw new Error(`the certifier takes box issue, not ${box}`);
|
|
75
|
+
const handle = typeof b.handle === "string" ? b.handle : "";
|
|
76
|
+
const domain = typeof b.domain === "string" ? b.domain : "";
|
|
77
|
+
const subject = b.subject instanceof Uint8Array ? Buffer.from(b.subject).toString("hex") : typeof b.subject === "string" ? b.subject : "";
|
|
78
|
+
const serialNumber = typeof b.serialNumber === "string" ? b.serialNumber : "";
|
|
79
|
+
if (!handle || !domain)
|
|
80
|
+
throw new Error("issue wants {handle, domain, subject, serialNumber}");
|
|
81
|
+
if (!KEY.test(subject))
|
|
82
|
+
throw new Error("subject: not an identity key (33 bytes)");
|
|
83
|
+
if (!SERIAL.test(serialNumber))
|
|
84
|
+
throw new Error("serialNumber: base64 of 32 bytes");
|
|
85
|
+
const plain = await issueHandleCertificate(certifier, handle, domain, subject, serialNumber);
|
|
86
|
+
const holder = await issueSubjectCertificate(certifier, handle, domain, subject, serialNumber);
|
|
87
|
+
return { certificate: asJson(plain), holder, serialNumber, ...(b.issuance !== undefined ? { issuance: b.issuance } : {}) };
|
|
88
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PrivateKey } from "@bsv/sdk";
|
|
2
|
+
export type Vars = Record<string, string | undefined>;
|
|
3
|
+
/** The host's home: SKEIN_HOME, default ~/.skein. */
|
|
4
|
+
export declare const homeOf: (vars: Vars) => string;
|
|
5
|
+
export declare const HOST_ENV = "host.env";
|
|
6
|
+
export declare const OPERATOR_KEY = "operator.key";
|
|
7
|
+
/** The `SKEIN_*` settings of a host.env file (empty when there is none). */
|
|
8
|
+
export declare function readHostEnv(file: string): Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* The variables the commands see: host.env's `SKEIN_*` settings under the environment's (the environment
|
|
11
|
+
* wins). SKEIN_HOME comes from the environment (it says where host.env is).
|
|
12
|
+
*/
|
|
13
|
+
export declare function withHostEnv(vars: Vars): Vars;
|
|
14
|
+
/** Where the operator's key is: SKEIN_OPERATOR_KEY, default $SKEIN_HOME/operator.key. */
|
|
15
|
+
export declare const operatorKeyPath: (vars: Vars) => string;
|
|
16
|
+
/** A key file's key: one line, hex (64) or WIF. */
|
|
17
|
+
export declare function parseKey(text: string, where: string): PrivateKey;
|
|
18
|
+
/**
|
|
19
|
+
* The operator's key: the file at SKEIN_OPERATOR_KEY (default $SKEIN_HOME/operator.key) if present, else,
|
|
20
|
+
* with `create`, a new key written there (hex, mode 0600). `made` says it was created now.
|
|
21
|
+
*/
|
|
22
|
+
export declare function operatorKey(vars: Vars, o?: {
|
|
23
|
+
create?: boolean;
|
|
24
|
+
}): {
|
|
25
|
+
key: PrivateKey;
|
|
26
|
+
path: string;
|
|
27
|
+
made: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** The identity key (hex) of a private key. */
|
|
30
|
+
export declare const identityOf: (key: PrivateKey) => string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The host's configuration file and the operator's key.
|
|
2
|
+
//
|
|
3
|
+
// `$SKEIN_HOME/host.env` holds the host's settings: every `SKEIN_*` line
|
|
4
|
+
// (`KEY=value`, `export KEY=value`, quotes stripped, `#` comments). The
|
|
5
|
+
// environment wins over the file. It configures the commands (`skein-host`,
|
|
6
|
+
// `skein`) and, through them, how `skein-host run` builds the host skein the
|
|
7
|
+
// first time; once the host skein exists, a change to host.env does not
|
|
8
|
+
// reach into it.
|
|
9
|
+
//
|
|
10
|
+
// The operator's key (`SKEIN_OPERATOR_KEY`, a path, default
|
|
11
|
+
// `$SKEIN_HOME/operator.key`): the key that owns the host skein and that the
|
|
12
|
+
// client `skein` signs with. A plain file, one line, hex (or WIF), mode
|
|
13
|
+
// 0600: if present it is used, if not it is created.
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { PrivateKey } from "@bsv/sdk";
|
|
17
|
+
/** The host's home: SKEIN_HOME, default ~/.skein. */
|
|
18
|
+
export const homeOf = (vars) => vars.SKEIN_HOME || join(vars.HOME ?? ".", ".skein");
|
|
19
|
+
export const HOST_ENV = "host.env";
|
|
20
|
+
export const OPERATOR_KEY = "operator.key";
|
|
21
|
+
/** The `SKEIN_*` settings of a host.env file (empty when there is none). */
|
|
22
|
+
export function readHostEnv(file) {
|
|
23
|
+
const out = {};
|
|
24
|
+
if (!existsSync(file))
|
|
25
|
+
return out;
|
|
26
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
27
|
+
const m = /^\s*(?:export\s+)?(SKEIN_[A-Z0-9_]+)=(.*)$/.exec(line);
|
|
28
|
+
if (!m)
|
|
29
|
+
continue;
|
|
30
|
+
let v = m[2].trim();
|
|
31
|
+
const q = /^(["'])(.*)\1$/.exec(v);
|
|
32
|
+
if (q)
|
|
33
|
+
v = q[2];
|
|
34
|
+
else
|
|
35
|
+
v = v.replace(/\s+#.*$/, "");
|
|
36
|
+
out[m[1]] = v;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The variables the commands see: host.env's `SKEIN_*` settings under the environment's (the environment
|
|
42
|
+
* wins). SKEIN_HOME comes from the environment (it says where host.env is).
|
|
43
|
+
*/
|
|
44
|
+
export function withHostEnv(vars) {
|
|
45
|
+
const file = readHostEnv(join(homeOf(vars), HOST_ENV));
|
|
46
|
+
const out = { ...vars };
|
|
47
|
+
for (const [k, v] of Object.entries(file))
|
|
48
|
+
if (out[k] === undefined || out[k] === "")
|
|
49
|
+
out[k] = v;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/** Where the operator's key is: SKEIN_OPERATOR_KEY, default $SKEIN_HOME/operator.key. */
|
|
53
|
+
export const operatorKeyPath = (vars) => vars.SKEIN_OPERATOR_KEY || join(homeOf(vars), OPERATOR_KEY);
|
|
54
|
+
/** A key file's key: one line, hex (64) or WIF. */
|
|
55
|
+
export function parseKey(text, where) {
|
|
56
|
+
const s = text.trim();
|
|
57
|
+
if (/^[0-9a-fA-F]{64}$/.test(s))
|
|
58
|
+
return PrivateKey.fromHex(s.toLowerCase());
|
|
59
|
+
try {
|
|
60
|
+
return PrivateKey.fromWif(s);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error(`${where}: not a private key (64 hex digits, or WIF)`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The operator's key: the file at SKEIN_OPERATOR_KEY (default $SKEIN_HOME/operator.key) if present, else,
|
|
68
|
+
* with `create`, a new key written there (hex, mode 0600). `made` says it was created now.
|
|
69
|
+
*/
|
|
70
|
+
export function operatorKey(vars, o = {}) {
|
|
71
|
+
const path = operatorKeyPath(vars);
|
|
72
|
+
if (existsSync(path))
|
|
73
|
+
return { key: parseKey(readFileSync(path, "utf8"), path), path, made: false };
|
|
74
|
+
if (!o.create)
|
|
75
|
+
throw new Error(`no operator key at ${path} (SKEIN_OPERATOR_KEY): \`skein-host run\` makes it, or put the key there (hex or WIF, mode 0600)`);
|
|
76
|
+
const key = PrivateKey.fromRandom();
|
|
77
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
78
|
+
writeFileSync(path, `${key.toHex()}\n`, { mode: 0o600 });
|
|
79
|
+
chmodSync(path, 0o600);
|
|
80
|
+
return { key, path, made: true };
|
|
81
|
+
}
|
|
82
|
+
/** The identity key (hex) of a private key. */
|
|
83
|
+
export const identityOf = (key) => key.toPublicKey().toString();
|