@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,422 @@
1
+ // Router-held feeds (#33 part 2, #29's revision): long-lived subscriptions the
2
+ // router keeps on behalf of instances. A headers feed's headers go to the
3
+ // host chain (#156): the header chain the host keeps once, in its kernel
4
+ // process (`headers` frames, kernel.ts; a serve instance's in its own store);
5
+ // on a changed record the subscribed instance is sent the event
6
+ //
7
+ // chain {kind: "chain", root: <the host chain record>, height}
8
+ //
9
+ // in its box (Router.chainHeaders: one `headers` run per SSE event, the
10
+ // backfill's in runs of 2016; the per-instance `header` events are gone).
11
+ //
12
+ // (The broadcaster's proofs travel the same way, #65: {kind: "proof",
13
+ // subject, txid, path, …} in box `chain`, arc.ts; its other statuses are
14
+ // signed messages from the status provider. `statusOf` reads Arcade's JSON
15
+ // for it: {kind: "status", subject: <tx CID>, txid, txStatus, merklePath?:
16
+ // bytes, blockHeight?, blockHash?, extraInfo?}.)
17
+ //
18
+ // An instance declares its header feeds in its config (etc/config.json,
19
+ // `feeds`), which its genesis carries:
20
+ //
21
+ // {kind: "headers", url, box?} an SSE stream of block headers (ChainTracks-style):
22
+ // each event's data is the header's hex (160 digits), or
23
+ // JSON {header | raw | hex: "<hex>"}, or chaintracks' JSON
24
+ // {version, previousHash, merkleRoot, time, bits, nonce, height?, hash?}
25
+ // (Arcade's /chaintracks/v2/tip/stream; the hashes in display
26
+ // order: the 80 bytes serialized from the fields, and a `hash`
27
+ // that is not their sha256d is logged and dropped), or an array of them
28
+ //
29
+ // The host has one headers feed of its own besides (#102, SKEIN_HEADERS_URL;
30
+ // the Router's `headersFeed`): every enabled instance whose dispatch table
31
+ // takes events in box `chain` is subscribed to it, the others not; the router
32
+ // keeps that current as dispatch tables change (`host`). A genesis's `feeds`
33
+ // are in addition. The host listens to it itself too (#132, `listen`): every
34
+ // header it brings grows the default image's chain part (image-chain.ts).
35
+ //
36
+ // `box` defaults to "chain" (the wallet's sender-less subscription). Nothing
37
+ // here judges an item: the kernel's host chain tracker validates headers (and
38
+ // the chain app proofs). One SSE connection per URL, fanned out to every subscriber;
39
+ // reconnects with exponential backoff (and `Last-Event-ID`); each instance's
40
+ // queue of items not yet admitted holds at most `maxQueue` (the oldest go
41
+ // first, logged).
42
+ //
43
+ // The transaction half is the host's broadcaster (#58, #65, arc.ts): one
44
+ // Arcade subscription for the whole host over the same SSE client
45
+ // (SseStream), each proof and status routed to every instance that holds the
46
+ // transaction. The
47
+ // per-instance `arc-callback` feed is gone: a genesis that still declares one
48
+ // has it ignored.
49
+ import { Hash, Utils } from "@bsv/sdk";
50
+ import { CID } from "multiformats/cid";
51
+ import * as Digest from "multiformats/hashes/digest";
52
+ export const DEFAULT_BOX = "chain";
53
+ /** A transaction's CID: bitcoin-tx (0xb1), dbl-sha2-256 (0x56) over the txid in internal byte order. */
54
+ export function txCid(txid) {
55
+ return CID.createV1(0xb1, Digest.create(0x56, Uint8Array.from(Buffer.from(txid, "hex").reverse())));
56
+ }
57
+ /** The feeds a genesis declares (`feeds`), the malformed ones (and a pre-#58 `arc-callback`) left out. */
58
+ export function feedsOf(g) {
59
+ const fs = g?.feeds;
60
+ if (!Array.isArray(fs))
61
+ return [];
62
+ const out = [];
63
+ for (const f of fs) {
64
+ const box = typeof f?.box === "string" && f.box ? f.box : undefined;
65
+ if (f?.kind === "headers" && typeof f.url === "string")
66
+ out.push({ kind: "headers", url: f.url, ...(box ? { box } : {}) });
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * The 80 bytes of a header given as chaintracks' JSON (Arcade's tip stream):
72
+ * version, previousHash, merkleRoot (display order: reversed into raw),
73
+ * time, bits, nonce — and, with `hash`, checked against their sha256d. Not
74
+ * that form: undefined. A `hash` that does not match: an Error saying so.
75
+ */
76
+ export function headerOfFields(o) {
77
+ const u32 = (x) => typeof x === "number" && Number.isInteger(x) && x >= 0 && x <= 0xffffffff;
78
+ const h32 = (x) => typeof x === "string" && /^[0-9a-fA-F]{64}$/.test(x);
79
+ if (!u32(o.version) || !h32(o.previousHash) || !h32(o.merkleRoot) || !u32(o.time) || !u32(o.bits) || !u32(o.nonce))
80
+ return undefined;
81
+ const w = new Utils.Writer();
82
+ w.writeUInt32LE(o.version);
83
+ w.writeReverse(Utils.toArray(o.previousHash, "hex"));
84
+ w.writeReverse(Utils.toArray(o.merkleRoot, "hex"));
85
+ w.writeUInt32LE(o.time).writeUInt32LE(o.bits).writeUInt32LE(o.nonce);
86
+ const raw = w.toArray();
87
+ if (o.hash !== undefined) {
88
+ const hash = Utils.toHex(Hash.hash256(raw).reverse());
89
+ if (typeof o.hash !== "string" || o.hash.toLowerCase() !== hash)
90
+ return new Error(`header ${String(o.height ?? "?")}: its hash ${String(o.hash)} is not its fields' (${hash}): dropped`);
91
+ }
92
+ return Uint8Array.from(raw);
93
+ }
94
+ /**
95
+ * Header bytes out of one SSE event's data: hex, JSON with header/raw/hex,
96
+ * chaintracks' fields (headerOfFields), or an array of those. A header whose
97
+ * `hash` is not its fields' is left out, and `drop` told why.
98
+ */
99
+ export function headersOf(data, drop) {
100
+ const hex = (s) => typeof s === "string" && /^[0-9a-fA-F]{160}$/.test(s) ? Uint8Array.from(Buffer.from(s, "hex")) : undefined;
101
+ const t = data.trim();
102
+ const direct = hex(t);
103
+ if (direct)
104
+ return [direct];
105
+ let v;
106
+ try {
107
+ v = JSON.parse(t);
108
+ }
109
+ catch {
110
+ return [];
111
+ } // neither hex nor JSON: no header; the caller logs an event with none
112
+ const one = (x) => {
113
+ if (typeof x === "string")
114
+ return hex(x);
115
+ const o = x;
116
+ if (!o || typeof o !== "object")
117
+ return undefined;
118
+ const h = hex(o.header) ?? hex(o.raw) ?? hex(o.hex);
119
+ if (h)
120
+ return h;
121
+ const f = headerOfFields(o);
122
+ if (f instanceof Error) {
123
+ drop?.(f.message);
124
+ return undefined;
125
+ }
126
+ return f;
127
+ };
128
+ const list = Array.isArray(v) ? v : Array.isArray(v?.headers) ? v.headers : [v];
129
+ return list.map(one).filter((x) => !!x);
130
+ }
131
+ /** Arcade's (and ARC's) status JSON — an SSE event's data, a webhook's body — as a `status` event record, or why not. */
132
+ export function statusOf(v) {
133
+ const o = v;
134
+ if (!o || typeof o !== "object")
135
+ return "not a JSON object";
136
+ const txid = o.txid;
137
+ if (typeof txid !== "string" || !/^[0-9a-f]{64}$/i.test(txid))
138
+ return "no txid";
139
+ if (typeof o.txStatus !== "string" || !o.txStatus)
140
+ return "no txStatus";
141
+ const ev = { kind: "status", subject: txCid(txid.toLowerCase()), txid: txid.toLowerCase(), txStatus: o.txStatus };
142
+ if (typeof o.merklePath === "string" && o.merklePath) {
143
+ if (!/^([0-9a-f]{2})+$/i.test(o.merklePath))
144
+ return "merklePath is not hex";
145
+ ev.merklePath = Uint8Array.from(Buffer.from(o.merklePath, "hex"));
146
+ }
147
+ if (typeof o.blockHeight === "number")
148
+ ev.blockHeight = o.blockHeight;
149
+ if (typeof o.blockHash === "string" && o.blockHash)
150
+ ev.blockHash = o.blockHash;
151
+ if (typeof o.extraInfo === "string" && o.extraInfo)
152
+ ev.extraInfo = o.extraInfo;
153
+ return ev;
154
+ }
155
+ /**
156
+ * One SSE subscription held open (#33, #58): connects, parses the stream,
157
+ * hands each event to `onEvent` — awaited before the next one is read and
158
+ * before its id counts as seen, so a caller that remembers `lastId` after it
159
+ * never skips an event — and reconnects with backoff, sending the last id
160
+ * seen as `Last-Event-ID`.
161
+ */
162
+ export class SseStream {
163
+ url;
164
+ lastId;
165
+ abort = new AbortController();
166
+ stopped = false;
167
+ o;
168
+ onEvent;
169
+ running;
170
+ constructor(url, onEvent, o = {}) {
171
+ this.url = url;
172
+ this.onEvent = onEvent;
173
+ this.o = o;
174
+ this.lastId = o.lastId;
175
+ }
176
+ start() { this.running ??= this.connect(); return this; }
177
+ /** Close the connection; no reconnect. Resolves once the loop has ended (an event being handled finishes first). */
178
+ async stop() { this.stopped = true; this.abort.abort(); await this.running; }
179
+ say(line) { this.o.log?.(line); }
180
+ async connect() {
181
+ const { min, max } = this.o.backoff ?? { min: 500, max: 30_000 };
182
+ let wait = min;
183
+ while (!this.stopped) {
184
+ try {
185
+ const res = await (this.o.fetch ?? fetch)(this.url, {
186
+ headers: { ...this.o.headers, accept: "text/event-stream", ...(this.lastId !== undefined ? { "last-event-id": this.lastId } : {}) },
187
+ signal: this.abort.signal,
188
+ });
189
+ if (!res.ok || !res.body)
190
+ throw new Error(`HTTP ${res.status}`);
191
+ this.say(`connected${this.lastId !== undefined ? ` (after event ${this.lastId})` : ""}`);
192
+ await this.read(res.body, () => { wait = min; });
193
+ if (this.stopped)
194
+ return;
195
+ this.say(`closed; reconnecting in ${wait} ms`);
196
+ }
197
+ catch (e) {
198
+ if (this.stopped)
199
+ return;
200
+ this.say(`${e.message}; reconnecting in ${wait} ms`);
201
+ }
202
+ await new Promise((r) => { const t = setTimeout(r, wait); this.abort.signal.addEventListener("abort", () => { clearTimeout(t); r(); }, { once: true }); });
203
+ wait = Math.min(max, wait * 2);
204
+ }
205
+ }
206
+ /** Parse the event stream (text/event-stream): `data:` lines until a blank line make one event; `id:` and `event:` go with it. */
207
+ async read(body, onData) {
208
+ const dec = new TextDecoder();
209
+ let buf = "";
210
+ let data = [];
211
+ let id;
212
+ let event;
213
+ const reader = body.getReader();
214
+ try {
215
+ for (;;) {
216
+ const { value, done } = await reader.read();
217
+ if (done)
218
+ return;
219
+ buf += dec.decode(value, { stream: true });
220
+ let nl;
221
+ while ((nl = buf.search(/\r\n|\n|\r/)) >= 0) {
222
+ const line = buf.slice(0, nl);
223
+ buf = buf.slice(nl + (buf.startsWith("\r\n", nl) ? 2 : 1));
224
+ if (line === "") {
225
+ if (data.length) {
226
+ onData();
227
+ try {
228
+ await this.onEvent({ data: data.join("\n"), ...(id !== undefined ? { id } : {}), ...(event !== undefined ? { event } : {}) });
229
+ }
230
+ catch (e) {
231
+ this.say(`an event not taken: ${e.message}`);
232
+ }
233
+ }
234
+ if (this.stopped)
235
+ return;
236
+ if (id !== undefined)
237
+ this.lastId = id;
238
+ data = [];
239
+ id = undefined;
240
+ event = undefined;
241
+ continue;
242
+ }
243
+ if (line.startsWith(":"))
244
+ continue;
245
+ const c = line.indexOf(":");
246
+ const field = c < 0 ? line : line.slice(0, c);
247
+ const v = c < 0 ? "" : line.slice(c + 1).replace(/^ /, "");
248
+ if (field === "data")
249
+ data.push(v);
250
+ else if (field === "id")
251
+ id = v;
252
+ else if (field === "event")
253
+ event = v;
254
+ }
255
+ }
256
+ }
257
+ finally {
258
+ reader.releaseLock?.();
259
+ }
260
+ }
261
+ }
262
+ /** Headers per backfill run (#141): one `headers` frame each, parents first (#156). */
263
+ export const BACKFILL_RUN = 2016;
264
+ export class Feeds {
265
+ o;
266
+ sse = new Map();
267
+ declared = new Map();
268
+ hosted = new Map();
269
+ listeners = new Map();
270
+ queues = new Map();
271
+ stopped = false;
272
+ constructor(o) { this.o = o; }
273
+ say(source, line) { this.o.log?.(source, line); }
274
+ /** `handle`'s feeds are these (replacing what it declared before). */
275
+ declare(handle, specs) {
276
+ if (this.stopped)
277
+ return;
278
+ this.declared.set(handle, specs);
279
+ this.subscribe(handle);
280
+ }
281
+ /**
282
+ * The host's own feed (#102) for `handle`: subscribed to it (`spec`), or not (undefined); a genesis's feeds are kept.
283
+ * `backfill` (#141): on subscribing, the headers the instance lacks up to the host's tip — read after the
284
+ * subscription is in place; until they are queued, the live items wait behind them (held), and a live header
285
+ * the backfill carries too is dropped. Admitted first, in runs of BACKFILL_RUN, then the live stream.
286
+ */
287
+ host(handle, spec, backfill) {
288
+ if (this.stopped)
289
+ return Promise.resolve();
290
+ if (spec)
291
+ this.hosted.set(handle, spec);
292
+ else
293
+ this.hosted.delete(handle);
294
+ if (!spec || !backfill) {
295
+ this.subscribe(handle);
296
+ return Promise.resolve();
297
+ }
298
+ const q = this.queue(handle);
299
+ q.held = true;
300
+ this.subscribe(handle);
301
+ return backfill().then((raws) => {
302
+ if (!raws.length)
303
+ return;
304
+ const have = new Set(raws.map((r) => Buffer.from(r).toString("hex")));
305
+ q.items = q.items.map((x) => ({ ...x, raws: x.raws.filter((r) => !have.has(Buffer.from(r).toString("hex"))) })).filter((x) => x.raws.length > 0);
306
+ const runs = [];
307
+ for (let i = 0; i < raws.length; i += BACKFILL_RUN)
308
+ runs.push({ box: spec.box ?? DEFAULT_BOX, raws: raws.slice(i, i + BACKFILL_RUN) });
309
+ q.items.unshift(...runs);
310
+ this.say(handle, `feed: ${raws.length} header${raws.length === 1 ? "" : "s"} it lacks pushed first (the backfill), then the live stream`);
311
+ }).catch((e) => this.say(handle, `feed: the backfill not read: ${e.message}`)).finally(() => {
312
+ q.held = false;
313
+ if (!q.running && q.items.length)
314
+ void this.drain(handle, q);
315
+ });
316
+ }
317
+ queue(handle) {
318
+ let q = this.queues.get(handle);
319
+ if (!q)
320
+ this.queues.set(handle, (q = { items: [], running: false, dropped: 0 }));
321
+ return q;
322
+ }
323
+ /**
324
+ * The host's own listener on a headers feed (#132: the default image's
325
+ * chain part grows by it): every header the stream brings, as well as the
326
+ * subscribers'. The connection stays open while a listener is on it.
327
+ */
328
+ listen(url, fn) {
329
+ if (this.stopped)
330
+ return;
331
+ this.listeners.set(url, fn);
332
+ this.connect(url);
333
+ }
334
+ /** Whether `handle` is subscribed to the host's own feed. */
335
+ hosts(handle) { return this.hosted.has(handle); }
336
+ /** Subscribe `handle` to what it declared and the host's feed for it, and to nothing else. */
337
+ subscribe(handle) {
338
+ const h = this.hosted.get(handle);
339
+ const specs = [...(this.declared.get(handle) ?? []), ...(h ? [h] : [])];
340
+ for (const [url, s] of this.sse) {
341
+ if (s.subscribers.has(handle) && !specs.some((f) => f.kind === "headers" && f.url === url))
342
+ s.subscribers.delete(handle);
343
+ if (!s.subscribers.size && !this.listeners.has(url)) {
344
+ void s.stream.stop();
345
+ this.sse.delete(url);
346
+ }
347
+ }
348
+ for (const f of specs) {
349
+ if (f.kind !== "headers")
350
+ continue;
351
+ this.connect(f.url).subscribers.set(handle, f.box ?? DEFAULT_BOX);
352
+ }
353
+ }
354
+ /** The one connection to `url`, opened if it is not. */
355
+ connect(url) {
356
+ let s = this.sse.get(url);
357
+ if (!s) {
358
+ const subscribers = new Map();
359
+ const stream = new SseStream(url, (ev) => this.dispatch(url, subscribers, ev.data), {
360
+ backoff: this.o.backoff, fetch: this.o.fetch, log: (l) => this.say("router", `feed ${url}: ${l}`),
361
+ });
362
+ s = { stream, subscribers };
363
+ this.sse.set(url, s);
364
+ stream.start();
365
+ }
366
+ return s;
367
+ }
368
+ /** The declared feeds of `handle`. */
369
+ of(handle) { return this.declared.get(handle) ?? []; }
370
+ /** Close every connection (no reconnect); resolves once their loops have ended. */
371
+ async stop() {
372
+ this.stopped = true;
373
+ const streams = [...this.sse.values()].map((s) => s.stream.stop());
374
+ this.sse.clear();
375
+ await Promise.all(streams);
376
+ }
377
+ /** Queue a run of headers for `handle` (bounded), and drain the queue serially. */
378
+ push(handle, box, raws) {
379
+ const q = this.queue(handle);
380
+ q.items.push({ box, raws });
381
+ const max = this.o.maxQueue ?? 1000;
382
+ if (q.items.length > max) {
383
+ const n = q.items.length - max;
384
+ q.items.splice(0, n);
385
+ q.dropped += n;
386
+ this.say(handle, `feed: queue full (${max}): dropped the ${n} oldest (${q.dropped} so far)`);
387
+ }
388
+ if (!q.running && !q.held)
389
+ void this.drain(handle, q);
390
+ }
391
+ /** How many items wait for `handle` (tests). */
392
+ pending(handle) { return this.queues.get(handle)?.items.length ?? 0; }
393
+ async drain(handle, q) {
394
+ q.running = true;
395
+ try {
396
+ while (q.items.length && !this.stopped && !q.held) {
397
+ const { box, raws } = q.items.shift();
398
+ try {
399
+ await this.o.headers(handle, box, raws);
400
+ }
401
+ catch (e) {
402
+ this.say(handle, `feed: ${raws.length} header${raws.length === 1 ? "" : "s"} not taken: ${e.message}`);
403
+ }
404
+ }
405
+ }
406
+ finally {
407
+ q.running = false;
408
+ }
409
+ }
410
+ dispatch(url, subscribers, data) {
411
+ let dropped = false;
412
+ const hs = headersOf(data, (why) => { dropped = true; this.say("router", `feed ${url}: ${why}`); });
413
+ if (!hs.length) {
414
+ if (!dropped)
415
+ this.say("router", `feed ${url}: an event with no header in it: ignored`);
416
+ return;
417
+ }
418
+ this.listeners.get(url)?.(hs);
419
+ for (const [handle, box] of subscribers)
420
+ this.push(handle, box, hs);
421
+ }
422
+ }
@@ -0,0 +1,92 @@
1
+ import type { CID } from "multiformats/cid";
2
+ import type { Stamp } from "../runtime/syscalls.ts";
3
+ import { type DoorAnswer } from "./door-answer.ts";
4
+ import type { KernelSurface as Kernel, RequestAnswer } from "./kernel.ts";
5
+ /** A raw request as the host appends it: `path` as the client signed it, `route` what the routes table sees. */
6
+ export interface FrontRequest {
7
+ method: string;
8
+ path: string;
9
+ route?: string;
10
+ query: string;
11
+ headers: Record<string, string>;
12
+ body: Uint8Array;
13
+ }
14
+ export interface FrontAnswer {
15
+ status: number;
16
+ headers: Record<string, string>;
17
+ body: Uint8Array;
18
+ /** The request's entry and the thread it launched (absent: not processed within the wait). */
19
+ entry?: CID;
20
+ thread?: CID;
21
+ /** Fuel of the door's filters and the answering call, when the answer was the door's or a read (the steps' fuel is on their updates). */
22
+ fuel?: number;
23
+ /**
24
+ * The caller (hex) of an answer the host metered: a read after the request's thread (its
25
+ * answer's `read.caller`), or the principal the door's filters yielded for an answer it gave
26
+ * without an entry. Absent when the request named none.
27
+ */
28
+ caller?: string;
29
+ }
30
+ /** How long a synchronous client waits on its request's thread by default (ms): SKEIN_ANSWER_WAIT_MS, else two minutes. */
31
+ export declare const ANSWER_WAIT_MS: number;
32
+ /** The Retry-After (s) a client that waited past the bound, or met a host shutting down, is told. */
33
+ export declare const RETRY_AFTER_S = 5;
34
+ /** 503 + Retry-After: the wait's bound passed (the thread goes on), or the host is shutting down. */
35
+ export declare function unavailable(description: string, extra?: Partial<FrontAnswer>): FrontAnswer;
36
+ /** Lower-cased header names, one value each (joined with ", "). */
37
+ export declare function headerMap(h: Record<string, string | string[] | undefined> | Headers): Record<string, string>;
38
+ /**
39
+ * Append a package as received (#68): one `request` entry naming it and its
40
+ * transport. The record goes with the frame, not into the store (#121): the
41
+ * kernel's door matches the route, runs its filters and the gate, and puts
42
+ * the package as it hands it back (a BEEF's bytes never stored), so the entry
43
+ * written — its CID, returned — may name another record than this. A package
44
+ * the door turns away or answers writes nothing (#143): DoorAnswered.
45
+ */
46
+ export declare function appendRequest(k: Kernel, transport: string, record: Record<string, unknown>, time?: Stamp): Promise<CID>;
47
+ /** An HTTP request's record (log.zig: {kind: "http", method, path, route, query, headers, body}). */
48
+ export declare function httpRecord(req: FrontRequest): Record<string, unknown>;
49
+ /**
50
+ * One HTTP request through the instance: appended, then waited on (#66)
51
+ * until its thread comes to rest or `waitMs` passes (503 + Retry-After).
52
+ * `stop`, when it settles first, answers 503 too (the host shutting down).
53
+ */
54
+ export declare function frontDoor(k: Kernel, req: FrontRequest, o?: {
55
+ now?: Stamp;
56
+ waitMs?: number;
57
+ stop?: Promise<unknown>;
58
+ }): Promise<FrontAnswer>;
59
+ /**
60
+ * A request thread's answer as the HTTP response: its last step's, a read after it, or 500/503.
61
+ * `req`, the request as received: a door refusal of a signed one is answered by the front door.
62
+ */
63
+ export declare function httpAnswer(k: Kernel, entry: CID, a: RequestAnswer, req?: FrontRequest): Promise<FrontAnswer>;
64
+ /**
65
+ * The door's answer without an entry (#143: a filter's rejection or answer — a read route's — or the
66
+ * gate's refusal), as the HTTP response: a signed request gets it signed on its session — a
67
+ * front-door call (fn "respond") with the request as received, at the request's time — and an
68
+ * unsigned one, or one whose session does not verify (unknown, expired: the stock client shakes
69
+ * hands again), gets it plain.
70
+ */
71
+ export declare function doorAnswer(k: Kernel, d: DoorAnswer, req: FrontRequest, now?: Stamp): Promise<FrontAnswer>;
72
+ /** The BRC-104 handshake's path: a route of its own (the front door's fn "handshake"). */
73
+ export declare const HANDSHAKE_PATH = "/.well-known/auth";
74
+ /** Whether a request carries BRC-104 headers (x-bsv-auth-*): a signed request. */
75
+ export declare const isSigned: (headers: Record<string, string>) => boolean;
76
+ /**
77
+ * One HTTP request through the kernel's door (#143): its route's filters decide — admitted and
78
+ * waited on, or answered at once with nothing logged (a read route; a rejection; the gate's refusal).
79
+ */
80
+ export declare function serveHttp(k: Kernel, req: FrontRequest, o?: {
81
+ now?: Stamp;
82
+ waitMs?: number;
83
+ stop?: Promise<unknown>;
84
+ }): Promise<FrontAnswer>;
85
+ /** A fetch over one instance's door (serveHttp), no socket: for a client in a test, or a URL of the host's own. */
86
+ export declare function frontDoorFetch(k: Kernel, o?: {
87
+ now?: () => Stamp;
88
+ route?: (path: string) => string;
89
+ onAnswer?: (a: FrontAnswer) => void;
90
+ waitMs?: number;
91
+ }): typeof fetch;
92
+ export declare const cidOf: (v: unknown) => CID;