@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.
Files changed (176) hide show
  1. package/README.md +522 -2
  2. package/bin/skein-host.js +22 -0
  3. package/bin/skein.js +8 -0
  4. package/dist/client/admin-cli.d.ts +10 -0
  5. package/dist/client/admin-cli.js +370 -0
  6. package/dist/client/admin.d.ts +139 -0
  7. package/dist/client/admin.js +256 -0
  8. package/dist/client/bundle.d.ts +9 -0
  9. package/dist/client/bundle.js +34 -0
  10. package/dist/client/cli.d.ts +44 -0
  11. package/dist/client/cli.js +306 -0
  12. package/dist/client/client.d.ts +161 -0
  13. package/dist/client/client.js +237 -0
  14. package/dist/client/config.d.ts +18 -0
  15. package/dist/client/config.js +31 -0
  16. package/dist/client/conversation.d.ts +62 -0
  17. package/dist/client/conversation.js +113 -0
  18. package/dist/client/raw.d.ts +94 -0
  19. package/dist/client/raw.js +154 -0
  20. package/dist/client/target.d.ts +34 -0
  21. package/dist/client/target.js +158 -0
  22. package/dist/dev/cli.d.ts +8 -0
  23. package/dist/dev/cli.js +248 -0
  24. package/dist/dev/scan.d.ts +15 -0
  25. package/dist/dev/scan.js +173 -0
  26. package/dist/envelope-cbor.d.ts +110 -0
  27. package/dist/envelope-cbor.js +160 -0
  28. package/dist/envelope.d.ts +38 -0
  29. package/dist/envelope.js +101 -0
  30. package/dist/host/arc.d.ts +131 -0
  31. package/dist/host/arc.js +425 -0
  32. package/dist/host/billing.d.ts +102 -0
  33. package/dist/host/billing.js +167 -0
  34. package/dist/host/boot.d.ts +246 -0
  35. package/dist/host/boot.js +643 -0
  36. package/dist/host/cli.d.ts +57 -0
  37. package/dist/host/cli.js +1253 -0
  38. package/dist/host/clock.d.ts +3 -0
  39. package/dist/host/clock.js +8 -0
  40. package/dist/host/control.d.ts +45 -0
  41. package/dist/host/control.js +170 -0
  42. package/dist/host/cron.d.ts +99 -0
  43. package/dist/host/cron.js +196 -0
  44. package/dist/host/deliver.d.ts +53 -0
  45. package/dist/host/deliver.js +251 -0
  46. package/dist/host/deploy.d.ts +29 -0
  47. package/dist/host/deploy.js +55 -0
  48. package/dist/host/door-answer.d.ts +24 -0
  49. package/dist/host/door-answer.js +21 -0
  50. package/dist/host/events.d.ts +89 -0
  51. package/dist/host/events.js +222 -0
  52. package/dist/host/fake-arcade.d.ts +40 -0
  53. package/dist/host/fake-arcade.js +149 -0
  54. package/dist/host/fake-discovery.d.ts +4 -0
  55. package/dist/host/fake-discovery.js +58 -0
  56. package/dist/host/feeds.d.ts +127 -0
  57. package/dist/host/feeds.js +422 -0
  58. package/dist/host/frontdoor.d.ts +92 -0
  59. package/dist/host/frontdoor.js +172 -0
  60. package/dist/host/genesis.d.ts +259 -0
  61. package/dist/host/genesis.js +353 -0
  62. package/dist/host/handles.d.ts +40 -0
  63. package/dist/host/handles.js +88 -0
  64. package/dist/host/hostenv.d.ts +30 -0
  65. package/dist/host/hostenv.js +83 -0
  66. package/dist/host/image-chain.d.ts +127 -0
  67. package/dist/host/image-chain.js +440 -0
  68. package/dist/host/install.d.ts +22 -0
  69. package/dist/host/install.js +139 -0
  70. package/dist/host/instances.d.ts +227 -0
  71. package/dist/host/instances.js +430 -0
  72. package/dist/host/kernel.d.ts +246 -0
  73. package/dist/host/kernel.js +603 -0
  74. package/dist/host/liveness.d.ts +65 -0
  75. package/dist/host/liveness.js +155 -0
  76. package/dist/host/manifest.d.ts +160 -0
  77. package/dist/host/manifest.js +597 -0
  78. package/dist/host/p2p.d.ts +337 -0
  79. package/dist/host/p2p.js +1002 -0
  80. package/dist/host/packet.d.ts +121 -0
  81. package/dist/host/packet.js +562 -0
  82. package/dist/host/plan.d.ts +154 -0
  83. package/dist/host/plan.js +418 -0
  84. package/dist/host/providers.d.ts +238 -0
  85. package/dist/host/providers.js +565 -0
  86. package/dist/host/roster.d.ts +72 -0
  87. package/dist/host/roster.js +172 -0
  88. package/dist/host/router.d.ts +773 -0
  89. package/dist/host/router.js +2389 -0
  90. package/dist/host/signer.d.ts +49 -0
  91. package/dist/host/signer.js +167 -0
  92. package/dist/host/store-sqlite.d.ts +39 -0
  93. package/dist/host/store-sqlite.js +170 -0
  94. package/dist/host/stores.d.ts +43 -0
  95. package/dist/host/stores.js +342 -0
  96. package/dist/host/sync.d.ts +121 -0
  97. package/dist/host/sync.js +467 -0
  98. package/dist/host/testbeef.d.ts +21 -0
  99. package/dist/host/testbeef.js +37 -0
  100. package/dist/host/testhost.d.ts +149 -0
  101. package/dist/host/testhost.js +211 -0
  102. package/dist/host/vcdiff.d.ts +9 -0
  103. package/dist/host/vcdiff.js +210 -0
  104. package/dist/host/wasm-kernel.d.ts +87 -0
  105. package/dist/host/wasm-kernel.js +296 -0
  106. package/dist/host/wasm-worker.d.ts +1 -0
  107. package/dist/host/wasm-worker.js +186 -0
  108. package/dist/peers/cron.d.ts +33 -0
  109. package/dist/peers/cron.js +75 -0
  110. package/dist/peers/infer-main.d.ts +2 -0
  111. package/dist/peers/infer-main.js +74 -0
  112. package/dist/peers/infer.d.ts +163 -0
  113. package/dist/peers/infer.js +367 -0
  114. package/dist/runtime/beef.d.ts +28 -0
  115. package/dist/runtime/beef.js +107 -0
  116. package/dist/runtime/bitcoin.d.ts +36 -0
  117. package/dist/runtime/bitcoin.js +141 -0
  118. package/dist/runtime/cid.d.ts +14 -0
  119. package/dist/runtime/cid.js +51 -0
  120. package/dist/runtime/dispatch.d.ts +92 -0
  121. package/dist/runtime/dispatch.js +142 -0
  122. package/dist/runtime/envelope.d.ts +60 -0
  123. package/dist/runtime/envelope.js +101 -0
  124. package/dist/runtime/heads.d.ts +25 -0
  125. package/dist/runtime/heads.js +37 -0
  126. package/dist/runtime/identity.d.ts +28 -0
  127. package/dist/runtime/identity.js +67 -0
  128. package/dist/runtime/index-store.d.ts +152 -0
  129. package/dist/runtime/index-store.js +715 -0
  130. package/dist/runtime/log.d.ts +62 -0
  131. package/dist/runtime/log.js +104 -0
  132. package/dist/runtime/programs.d.ts +30 -0
  133. package/dist/runtime/programs.js +61 -0
  134. package/dist/runtime/records.d.ts +135 -0
  135. package/dist/runtime/records.js +149 -0
  136. package/dist/runtime/sqlite.d.ts +33 -0
  137. package/dist/runtime/sqlite.js +604 -0
  138. package/dist/runtime/store.d.ts +141 -0
  139. package/dist/runtime/store.js +24 -0
  140. package/dist/runtime/syscalls.d.ts +9 -0
  141. package/dist/runtime/syscalls.js +41 -0
  142. package/dist/runtime/tree.d.ts +60 -0
  143. package/dist/runtime/tree.js +196 -0
  144. package/dist/runtime/types.d.ts +100 -0
  145. package/dist/runtime/types.js +7 -0
  146. package/dist/testapps.d.ts +101 -0
  147. package/dist/testapps.js +275 -0
  148. package/dist/testkit.d.ts +25 -0
  149. package/dist/testkit.js +48 -0
  150. package/dist/wallet.d.ts +35 -0
  151. package/dist/wallet.js +87 -0
  152. package/images/README.md +68 -0
  153. package/images/default/README.md +38 -0
  154. package/images/default/bin/frontdoor.cid +1 -0
  155. package/images/default/bin/frontdoor.json +5 -0
  156. package/images/default/bin/messagebox.cid +1 -0
  157. package/images/default/bin/messagebox.json +8 -0
  158. package/images/default/bin/wallet.cid +1 -0
  159. package/images/default/bin/wallet.json +14 -0
  160. package/images/default/etc/config.json +1 -0
  161. package/images/default/etc/dispatch.json +82 -0
  162. package/images/host/etc/apps.json +8 -0
  163. package/kernel-zig/README.md +1247 -0
  164. package/kernel-zig/web/client.js +65 -0
  165. package/kernel-zig/web/kernel.d.ts +59 -0
  166. package/kernel-zig/web/kernel.js +329 -0
  167. package/kernel-zig/web/store.js +67 -0
  168. package/kernel-zig/web/worker.js +79 -0
  169. package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
  170. package/package.json +77 -4
  171. package/wasm/README.md +35 -0
  172. package/wasm/frontdoor.wasm +0 -0
  173. package/wasm/messagebox.wasm +0 -0
  174. package/wasm/resolve.wasm +0 -0
  175. package/wasm/wallet.wasm +0 -0
  176. 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();