@dcasegr/skein 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +525 -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 +782 -0
  89. package/dist/host/router.js +2411 -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,222 @@
1
+ // An app's emitted events, served live to its page (#148). Any program of an
2
+ // app emits events ({kind: "event", event, app, topic, …}, log.zig
3
+ // eventRecord, #119); the kernel lists each on its step's update (`emitted`)
4
+ // and, once the step is committed, hands it to the host with its place in the
5
+ // log (providers.ts: the `emitted` hook, for every event whatever its name). This module serves
6
+ // them on the instance's origin, the host's own route (no program, no entry,
7
+ // nothing logged, unsigned — a read like /.live, #138, #143):
8
+ //
9
+ // GET /<app>/.events?event=<name>&topic=<t1>&topic=<t2>…
10
+ //
11
+ // `event` is required, one name; `topic` is repeated, one per topic (at
12
+ // least one; a topic is any text, so no separator is taken out of it). Sent:
13
+ // only the events of that app (the record's `app`, the kernel's), that name,
14
+ // whose `topic` is one of those. Nothing about any name is the host's: a
15
+ // price is one more event.
16
+ //
17
+ // The answer is a Server-Sent Events stream (text/event-stream), one message
18
+ // per event record, in log order:
19
+ //
20
+ // id: <n>.<seq>.<i>.<thread> the event's place in the log: the entry its step processed (`n`), the
21
+ // step's place in its thread (`seq`), the record's place in the update's
22
+ // `emitted` (`i`), the thread (its origin's CID). The entry alone is not a
23
+ // place: one entry can step several threads
24
+ // event: <name> the event's name
25
+ // data: <the record> the record as DAG-JSON (as the explorer renders records: a link
26
+ // {"/": "<cid>"}, bytes {"/": {"bytes": "<base64>"}}), one line
27
+ //
28
+ // On connect:
29
+ // - with Last-Event-ID (the browser's EventSource sends it on reconnect):
30
+ // every matching event after that place, folded from the log (p2p.ts
31
+ // emittedEvents, the order the kernel folds by), then live. A place no
32
+ // longer in the log (or never in it) resumes after its entry: the
33
+ // matching events of later entries. One that is not a place: 400;
34
+ // - without: the current value — the LAST matching event per requested
35
+ // topic (one per topic that has one), in log order — then live.
36
+ //
37
+ // Live: the kernel's `emit` notice carries each event's place in the log
38
+ // beside the record (`place`: {n, seq, i, thread}; the record is unchanged),
39
+ // so a live event is sent as it is handed over, its id made from the notice,
40
+ // with no log read; the log is read only for a new subscription's first
41
+ // messages (Last-Event-ID, or the current value). An id handed over again (a
42
+ // start re-offers what a waiting thread awaits) is not sent again. Nothing is
43
+ // kept but the open subscriptions and the ids lately sent: one registry per
44
+ // instance, in memory. An app's subscriptions close at its uninstall (no row
45
+ // of it left in the dispatch table), all of an instance's when it stops (the
46
+ // instance manager, a reclaim) and when the host shuts down.
47
+ //
48
+ // Beats (#138) are not events: not served here (GET /<app>/.live/<topic>).
49
+ import * as dagCbor from "@ipld/dag-cbor";
50
+ import * as dagJson from "@ipld/dag-json";
51
+ import { emittedEvents } from "./p2p.js";
52
+ /** The query of GET /<app>/.events (`event`, `topic` repeated) and Last-Event-ID, or why it is not one. */
53
+ export function eventsQuery(app, params, lastEventId) {
54
+ const event = params.get("event") ?? "";
55
+ if (!event)
56
+ return { problem: "event: the event's name (required)" };
57
+ if (params.getAll("event").length > 1)
58
+ return { problem: "event: one name" };
59
+ const topics = [...new Set(params.getAll("topic"))];
60
+ if (!topics.length || topics.some((t) => !t))
61
+ return { problem: "topic: one or more topics (repeat topic=, one per topic)" };
62
+ const after = lastEventId?.trim() || undefined;
63
+ if (after !== undefined && !placeOf(after))
64
+ return { problem: `Last-Event-ID: ${JSON.stringify(after)} is not an event's place (<n>.<seq>.<i>.<thread>)` };
65
+ return { app, event, topics, ...(after ? { after } : {}) };
66
+ }
67
+ /** An event id's parts, or undefined. */
68
+ export function placeOf(id) {
69
+ const m = /^(\d+)\.(\d+)\.(\d+)\.([a-z0-9]+)$/.exec(id);
70
+ return m ? { n: Number(m[1]), seq: Number(m[2]), i: Number(m[3]), thread: m[4] } : undefined;
71
+ }
72
+ /** One event as an SSE message. */
73
+ export function sseMessage(d) {
74
+ return `id: ${d.id}\nevent: ${d.event}\ndata: ${d.data}\n\n`;
75
+ }
76
+ /** Every event record a stream serves in the log, in log order, as delivered (resume and the current value). */
77
+ export async function foldEvents(store) {
78
+ const places = await emittedEvents(store, (rec, p) => typeof rec.event === "string" && typeof rec.app === "string" && typeof rec.topic === "string" ? p : undefined);
79
+ const out = [];
80
+ for (const p of places) {
81
+ const d = deliveredOf(dagCbor.decode(await store.bytes(p.record)), p);
82
+ if (d)
83
+ out.push(d);
84
+ }
85
+ return out;
86
+ }
87
+ /** Whether a subscription takes an event. */
88
+ const takes = (q, d) => d.app === q.app && d.event === q.event && q.topics.includes(d.topic);
89
+ /** What a subscription is sent first (folded from the log): after its Last-Event-ID, else the last per topic. */
90
+ export function backlog(all, q) {
91
+ if (q.after !== undefined) {
92
+ const i = all.findIndex((d) => d.id === q.after);
93
+ const n = placeOf(q.after).n;
94
+ return (i >= 0 ? all.slice(i + 1) : all.filter((d) => d.n > n)).filter((d) => takes(q, d));
95
+ }
96
+ const last = new Map();
97
+ for (const d of all)
98
+ if (takes(q, d))
99
+ last.set(d.topic, d);
100
+ const ids = new Set([...last.values()].map((d) => d.id));
101
+ return all.filter((d) => ids.has(d.id));
102
+ }
103
+ /** The id of the event at `p`: `<n>.<seq>.<i>.<thread>` — the fold's (foldEvents), free from the notice. */
104
+ export const idOf = (p) => `${p.n}.${p.seq}.${p.i}.${p.thread}`;
105
+ /** A handed-over event as delivered, or undefined when it is not one a stream serves (no app, no topic). */
106
+ export function deliveredOf(rec, p) {
107
+ if (rec.kind !== "event" || typeof rec.event !== "string" || typeof rec.app !== "string" || typeof rec.topic !== "string")
108
+ return undefined;
109
+ return { id: idOf(p), n: p.n, event: rec.event, app: rec.app, topic: rec.topic, data: new TextDecoder().decode(dagJson.encode(rec)) };
110
+ }
111
+ /** How many ids sent an instance remembers (a start hands an awaited event over again). */
112
+ const SENT_MAX = 10_000;
113
+ /** The registry of open subscriptions, per instance (#148). */
114
+ export class EventStreams {
115
+ subs = new Map();
116
+ /** Per instance: the ids handed over lately, in order: one handed over again is not sent again. */
117
+ sent = new Map();
118
+ o;
119
+ constructor(o) { this.o = o; }
120
+ /** Whether `handle` has an open subscription. */
121
+ open(handle) { return (this.subs.get(handle)?.size ?? 0) > 0; }
122
+ /** How many subscriptions `handle` has open (tests). */
123
+ count(handle) { return this.subs.get(handle)?.size ?? 0; }
124
+ /**
125
+ * Open a subscription for `handle`: its backlog folded from the log and sent, then each new
126
+ * matching event as it is handed over (what came during the fold, after it, once). `send` writes
127
+ * one event; `close` ends the stream (the host's: an uninstall, the instance stopping). The
128
+ * returned function forgets it (the client went).
129
+ */
130
+ async subscribe(handle, q, send, close) {
131
+ const sub = { q, send, close, held: [] };
132
+ let set = this.subs.get(handle);
133
+ if (!set)
134
+ this.subs.set(handle, set = new Set());
135
+ set.add(sub);
136
+ const forget = () => this.remove(handle, sub);
137
+ try {
138
+ const s = this.o.open(handle);
139
+ let all = [];
140
+ if (s)
141
+ try {
142
+ all = await foldEvents(s);
143
+ }
144
+ finally {
145
+ s.close();
146
+ }
147
+ const first = backlog(all, q);
148
+ const had = new Set(first.map((d) => d.id));
149
+ const held = sub.held;
150
+ delete sub.held;
151
+ if (!set.has(sub))
152
+ return forget; // gone meanwhile
153
+ for (const d of first)
154
+ sub.send(d);
155
+ for (const d of held)
156
+ if (!had.has(d.id))
157
+ sub.send(d);
158
+ }
159
+ catch (e) {
160
+ forget();
161
+ throw e;
162
+ }
163
+ return forget;
164
+ }
165
+ /**
166
+ * An event handed over for `handle` (after its step's commit), with its place in the log: sent
167
+ * as it is to each open subscription it matches — no log read. One handed over again (a start
168
+ * re-offers what a waiting thread awaits) is not sent again.
169
+ */
170
+ emitted(handle, rec, place) {
171
+ const subs = this.subs.get(handle);
172
+ if (!subs?.size)
173
+ return;
174
+ const d = deliveredOf(rec, place);
175
+ if (!d)
176
+ return;
177
+ let sent = this.sent.get(handle);
178
+ if (!sent)
179
+ this.sent.set(handle, sent = new Set());
180
+ if (sent.has(d.id))
181
+ return;
182
+ sent.add(d.id);
183
+ if (sent.size > SENT_MAX)
184
+ sent.delete(sent.values().next().value);
185
+ for (const s of subs)
186
+ if (takes(s.q, d)) {
187
+ if (s.held)
188
+ s.held.push(d);
189
+ else
190
+ s.send(d);
191
+ }
192
+ }
193
+ /** The apps `handle` has installed now (rows in its dispatch table): the others' subscriptions close. */
194
+ keep(handle, apps) {
195
+ for (const s of [...this.subs.get(handle) ?? []])
196
+ if (!apps.has(s.q.app))
197
+ this.end(handle, s);
198
+ }
199
+ /** Close every subscription of `handle` (it stops). */
200
+ drop(handle) {
201
+ for (const s of [...this.subs.get(handle) ?? []])
202
+ this.end(handle, s);
203
+ }
204
+ /** Close them all (the host shuts down). */
205
+ stop() { for (const h of [...this.subs.keys()])
206
+ this.drop(h); }
207
+ remove(handle, s) {
208
+ const set = this.subs.get(handle);
209
+ set?.delete(s);
210
+ if (set && !set.size) {
211
+ this.subs.delete(handle);
212
+ this.sent.delete(handle);
213
+ }
214
+ }
215
+ end(handle, s) {
216
+ this.remove(handle, s);
217
+ try {
218
+ s.close();
219
+ }
220
+ catch { /* the client's socket is gone already */ }
221
+ }
222
+ }
@@ -0,0 +1,40 @@
1
+ import { Transaction } from "@bsv/sdk";
2
+ export interface ArcadeStatus {
3
+ txStatus: string;
4
+ merklePath?: string;
5
+ blockHash?: string;
6
+ blockHeight?: number;
7
+ }
8
+ export declare class FakeArcade {
9
+ url: string;
10
+ eventsUrl: string;
11
+ /** "ok"; "reject": a 400 with a reason; "busy": 503 + Retry-After (not taken). */
12
+ mode: "ok" | "reject" | "busy";
13
+ readonly txs: Map<string, ArcadeStatus & {
14
+ token: string;
15
+ }>;
16
+ readonly posts: Uint8Array[];
17
+ readonly postHeaders: Array<Record<string, string | string[] | undefined>>;
18
+ readonly gets: string[];
19
+ /** The Last-Event-ID of each SSE connection, in order (undefined: none sent). */
20
+ readonly connects: Array<string | undefined>;
21
+ private log;
22
+ private conns;
23
+ private clock;
24
+ private servers;
25
+ static start(): Promise<FakeArcade>;
26
+ /** The transaction Arcade received in a POST body: Extended Format, else raw. */
27
+ static txOf(body: Uint8Array): Transaction;
28
+ private api;
29
+ private events;
30
+ private write;
31
+ /** A status for a transaction Arcade took: recorded, and streamed to its token's connections. The event's id. */
32
+ emit(txid: string, s: ArcadeStatus): string;
33
+ /** The body Arcade would post to a webhook for the last status of `txid`. */
34
+ webhookBody(txid: string): Record<string, unknown>;
35
+ /** Open SSE connections. */
36
+ get streams(): number;
37
+ /** Drop every SSE connection (a network blip). */
38
+ drop(): void;
39
+ close(): Promise<void>;
40
+ }
@@ -0,0 +1,149 @@
1
+ // A fake Arcade (bsv-blockchain/arcade) for the tests (#58): its API on one
2
+ // port — POST /tx (Extended Format or raw bytes; 202 {txid, status,
3
+ // txStatus: "RECEIVED"}, a duplicate echoing its current status, 400 with a
4
+ // reason, 503 + Retry-After under "backpressure"), GET /tx/:txid (404 for a
5
+ // txid it never took) — and its SSE service on another: GET
6
+ // /events?callbackToken=<token>, every transaction submitted under the
7
+ // token, ids increasing (nanosecond timestamps, as Arcade's), `Last-Event-ID`
8
+ // replaying what came after it, the current status of each transaction
9
+ // without one. The test drives the statuses (`emit`); Arcade's webhook is the
10
+ // test's to post.
11
+ import { createServer } from "node:http";
12
+ import { Transaction } from "@bsv/sdk";
13
+ export class FakeArcade {
14
+ url = "";
15
+ eventsUrl = "";
16
+ /** "ok"; "reject": a 400 with a reason; "busy": 503 + Retry-After (not taken). */
17
+ mode = "ok";
18
+ txs = new Map();
19
+ posts = [];
20
+ postHeaders = [];
21
+ gets = [];
22
+ /** The Last-Event-ID of each SSE connection, in order (undefined: none sent). */
23
+ connects = [];
24
+ log = [];
25
+ conns = [];
26
+ clock = 1790000000000000000n;
27
+ servers = [];
28
+ static async start() {
29
+ const a = new FakeArcade();
30
+ const api = createServer((req, res) => void a.api(req, res));
31
+ const events = createServer((req, res) => a.events(req, res));
32
+ for (const s of [api, events])
33
+ await new Promise((r) => s.listen(0, "127.0.0.1", r));
34
+ a.servers = [api, events];
35
+ a.url = `http://127.0.0.1:${api.address().port}`;
36
+ a.eventsUrl = `http://127.0.0.1:${events.address().port}/events`;
37
+ return a;
38
+ }
39
+ /** The transaction Arcade received in a POST body: Extended Format, else raw. */
40
+ static txOf(body) {
41
+ try {
42
+ return Transaction.fromEF([...body]);
43
+ }
44
+ catch {
45
+ return Transaction.fromBinary([...body]);
46
+ } // not EF: raw, whose failure the caller answers 400
47
+ }
48
+ async api(req, res) {
49
+ const chunks = [];
50
+ for await (const c of req)
51
+ chunks.push(c);
52
+ const body = new Uint8Array(Buffer.concat(chunks));
53
+ const send = (status, v, h = {}) => { res.writeHead(status, { "content-type": "application/json", ...h }); res.end(JSON.stringify(v)); };
54
+ const path = new URL(req.url ?? "/", "http://x").pathname;
55
+ if (req.method === "POST" && path === "/tx") {
56
+ this.posts.push(body);
57
+ this.postHeaders.push({ ...req.headers });
58
+ if (this.mode === "busy")
59
+ return send(503, { status: 503, title: "Service Unavailable", detail: "backpressure" }, { "retry-after": "1" });
60
+ let tx;
61
+ try {
62
+ tx = FakeArcade.txOf(body);
63
+ }
64
+ catch (e) {
65
+ return send(400, { status: 400, reason: `not a transaction: ${e.message}` });
66
+ }
67
+ const txid = tx.id("hex");
68
+ if (this.mode === "reject")
69
+ return send(400, { status: 400, txid, reason: "rejected by the test" });
70
+ const had = this.txs.get(txid);
71
+ if (had)
72
+ return send(202, { txid, status: 202, txStatus: had.txStatus });
73
+ this.txs.set(txid, { txStatus: "RECEIVED", token: String(req.headers["x-callbacktoken"] ?? "") });
74
+ return send(202, { txid, status: 202, txStatus: "RECEIVED" });
75
+ }
76
+ const m = /^\/tx\/([0-9a-f]{64})$/.exec(path);
77
+ if (req.method === "GET" && m) {
78
+ this.gets.push(m[1]);
79
+ const t = this.txs.get(m[1]);
80
+ if (!t)
81
+ return send(404, { status: 404, title: "Not Found", detail: "transaction not found" });
82
+ return send(200, { txid: m[1], txStatus: t.txStatus, status: t.txStatus, blockHash: t.blockHash ?? null, blockHeight: t.blockHeight ?? 0, merklePath: t.merklePath ?? null, extraInfo: "" });
83
+ }
84
+ send(404, { status: 404, title: "Not Found" });
85
+ }
86
+ events(req, res) {
87
+ const u = new URL(req.url ?? "/", "http://x");
88
+ if (u.pathname !== "/events") {
89
+ res.writeHead(404).end();
90
+ return;
91
+ }
92
+ const token = u.searchParams.get("callbackToken") ?? "";
93
+ const last = req.headers["last-event-id"];
94
+ this.connects.push(last);
95
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
96
+ res.write(": keepalive\n\n");
97
+ const mine = this.log.filter((e) => e.token === token);
98
+ if (last !== undefined) {
99
+ for (const e of mine)
100
+ if (BigInt(e.id) > BigInt(last))
101
+ this.write(res, e);
102
+ }
103
+ else {
104
+ // No id: the current status of every transaction under the token.
105
+ const latest = new Map();
106
+ for (const e of mine)
107
+ latest.set(String(e.data.txid), e);
108
+ for (const e of latest.values())
109
+ this.write(res, e);
110
+ }
111
+ const c = { res, token };
112
+ this.conns.push(c);
113
+ res.on("close", () => { this.conns = this.conns.filter((x) => x !== c); });
114
+ }
115
+ write(res, e) {
116
+ res.write(`id: ${e.id}\nevent: status\ndata: ${JSON.stringify(e.data)}\n\n`);
117
+ }
118
+ /** A status for a transaction Arcade took: recorded, and streamed to its token's connections. The event's id. */
119
+ emit(txid, s) {
120
+ const t = this.txs.get(txid);
121
+ if (!t)
122
+ throw new Error(`the fake Arcade never took ${txid}`);
123
+ // Its status now (a reorg's SEEN_ON_NETWORK drops the block and the path).
124
+ this.txs.set(txid, { token: t.token, ...s });
125
+ this.clock += 1000000n;
126
+ const e = { id: String(this.clock), token: t.token, data: { timestamp: new Date().toISOString(), txid, ...s } };
127
+ this.log.push(e);
128
+ for (const c of this.conns)
129
+ if (c.token === t.token)
130
+ this.write(c.res, e);
131
+ return e.id;
132
+ }
133
+ /** The body Arcade would post to a webhook for the last status of `txid`. */
134
+ webhookBody(txid) {
135
+ const e = this.log.filter((x) => x.data.txid === txid).at(-1);
136
+ if (!e)
137
+ throw new Error(`no status for ${txid}`);
138
+ return e.data;
139
+ }
140
+ /** Open SSE connections. */
141
+ get streams() { return this.conns.length; }
142
+ /** Drop every SSE connection (a network blip). */
143
+ drop() { for (const c of this.conns)
144
+ c.res.end(); this.conns = []; }
145
+ async close() {
146
+ this.drop();
147
+ await Promise.all(this.servers.map((s) => new Promise((r) => { s.closeAllConnections?.(); s.close(() => r()); })));
148
+ }
149
+ }
@@ -0,0 +1,4 @@
1
+ import type { HostDb } from "./instances.ts";
2
+ import type { Router, RouterRequest, RouterResponse } from "./router.ts";
3
+ /** The router's `discovery` option: manifest and resolve over host.db's rows. */
4
+ export declare function fakeDiscovery(db: HostDb, router: () => Router): (req: RouterRequest, url: URL) => Promise<RouterResponse | undefined>;
@@ -0,0 +1,58 @@
1
+ // A stand-in for the host skein's BRC-169 server, for tests and equivs that
2
+ // run no host skein (#113). On a host, the router's own origin's discovery
3
+ // requests go to the host skein's onboarding app (shruggr/skein-onboard),
4
+ // which answers from its records; a test host that only wants its agents
5
+ // and mailbox instances to find each other (the resolve program, the loop,
6
+ // the corpus) answers them here instead, from host.db, as the router did
7
+ // before #113: an agent's own identity, a mailbox instance's owner's, the
8
+ // instance's origin as the messagebox, a certificate signed by the
9
+ // certifier key with a serial number of the binding; and a registration
10
+ // (POST /account/register, the #113 contract: signed over `register
11
+ // <name>@<domain>`) answered with the mailbox instance the manager's create
12
+ // makes, without a certificate (the browser page's own mailbox). Not part of
13
+ // anything that runs (the router's `discovery` option is a test's).
14
+ import { Hash, ProtoWallet, Utils } from "@bsv/sdk";
15
+ import { issueHandleCertificate, RESOLVE_PATH, SEARCH_PATH } from "./handles.js";
16
+ import { domainOf, REGISTER_PROTOCOL } from "./router.js";
17
+ const json = (status, v) => ({ status, headers: { "content-type": "application/json" }, body: new TextEncoder().encode(JSON.stringify(v)) });
18
+ /** The router's `discovery` option: manifest and resolve over host.db's rows. */
19
+ export function fakeDiscovery(db, router) {
20
+ return async (req, url) => {
21
+ const r = router();
22
+ if (req.method === "GET" && url.pathname === "/manifest.json") {
23
+ const { publicKey } = await r.certifier.getPublicKey({ identityKey: true });
24
+ return json(200, { metanet: { trust: { publicKey }, handles: { version: "1.0", resolve: `${r.origin()}${RESOLVE_PATH}`, search: `${r.origin()}${SEARCH_PATH}` } } });
25
+ }
26
+ if (req.method === "GET" && url.pathname === RESOLVE_PATH) {
27
+ const q = (url.searchParams.get("handle") ?? "").replace(/^@/, "");
28
+ const own = domainOf(url.hostname);
29
+ const [h0, d] = q.includes("@") ? q.split("@") : [q, own];
30
+ const handle = h0.split("+")[0].toLowerCase(), domain = (d ?? own).toLowerCase();
31
+ if (!handle)
32
+ return json(400, { metanetHandles: "1.0", error: { code: "malformed-handle", message: "want ?handle=<handle>" } });
33
+ const row = db.get(handle);
34
+ const key = db.identityOf(handle, domain);
35
+ if (!row || row.status !== "enabled" || !key)
36
+ return json(404, { metanetHandles: "1.0", error: { code: "handle-not-found", message: `no handle ${handle}@${domain} here` } });
37
+ const serial = Utils.toBase64(Hash.sha256(Utils.toArray(`${handle}@${domain} ${key}`, "utf8")));
38
+ const c = await issueHandleCertificate(r.certifier, handle, domain, key, serial);
39
+ const certificate = { type: c.type, serialNumber: c.serialNumber, subject: c.subject, certifier: c.certifier, revocationOutpoint: c.revocationOutpoint, fields: c.fields, signature: c.signature };
40
+ return json(200, { metanetHandles: "1.0", handle, domain, identityKey: key, certificate, messagebox: r.originOf(handle), ttl: 300, revoked: false });
41
+ }
42
+ if (req.method === "POST" && url.pathname === "/account/register") {
43
+ const b = JSON.parse(new TextDecoder().decode(req.body));
44
+ const domain = await r.handleDomain();
45
+ const v = await new ProtoWallet("anyone").verifySignature({ protocolID: REGISTER_PROTOCOL, keyID: b.username, counterparty: b.identityKey, data: Utils.toArray(`register ${b.username}@${domain}`, "utf8"), signature: Utils.toArray(b.signature, "hex") }).catch(() => ({ valid: false }));
46
+ if (!v.valid)
47
+ return json(401, { error: "the signature does not verify for that identity" });
48
+ try {
49
+ const c = await r.createInstance(b.username, b.identityKey, { image: "mailbox", domain });
50
+ return json(200, { handle: c.handle, domain, identityKey: b.identityKey, messagebox: c.url });
51
+ }
52
+ catch (e) {
53
+ return json(409, { error: e.message });
54
+ }
55
+ }
56
+ return undefined;
57
+ };
58
+ }
@@ -0,0 +1,127 @@
1
+ import { CID } from "multiformats/cid";
2
+ export type FeedSpec = {
3
+ kind: "headers";
4
+ url: string;
5
+ box?: string;
6
+ };
7
+ export interface FeedsOptions {
8
+ /** A run of headers (parents first) for `handle`'s box: into the host chain, the event on a change (Router.chainHeaders, #156). */
9
+ headers(handle: string, box: string, raws: Uint8Array[]): Promise<unknown>;
10
+ log?(source: string, line: string): void;
11
+ /** Items waiting for admission per instance (default 1000). */
12
+ maxQueue?: number;
13
+ /** Reconnect backoff, ms (default 500 → 30 000, doubling). */
14
+ backoff?: {
15
+ min: number;
16
+ max: number;
17
+ };
18
+ fetch?: typeof fetch;
19
+ }
20
+ export declare const DEFAULT_BOX = "chain";
21
+ /** A transaction's CID: bitcoin-tx (0xb1), dbl-sha2-256 (0x56) over the txid in internal byte order. */
22
+ export declare function txCid(txid: string): CID;
23
+ /** The feeds a genesis declares (`feeds`), the malformed ones (and a pre-#58 `arc-callback`) left out. */
24
+ export declare function feedsOf(g: Record<string, unknown> | null | undefined): FeedSpec[];
25
+ /**
26
+ * The 80 bytes of a header given as chaintracks' JSON (Arcade's tip stream):
27
+ * version, previousHash, merkleRoot (display order: reversed into raw),
28
+ * time, bits, nonce — and, with `hash`, checked against their sha256d. Not
29
+ * that form: undefined. A `hash` that does not match: an Error saying so.
30
+ */
31
+ export declare function headerOfFields(o: Record<string, unknown>): Uint8Array | Error | undefined;
32
+ /**
33
+ * Header bytes out of one SSE event's data: hex, JSON with header/raw/hex,
34
+ * chaintracks' fields (headerOfFields), or an array of those. A header whose
35
+ * `hash` is not its fields' is left out, and `drop` told why.
36
+ */
37
+ export declare function headersOf(data: string, drop?: (why: string) => void): Uint8Array[];
38
+ /** Arcade's (and ARC's) status JSON — an SSE event's data, a webhook's body — as a `status` event record, or why not. */
39
+ export declare function statusOf(v: unknown): Record<string, unknown> | string;
40
+ /** One event of a text/event-stream: its data lines joined, its `id` and `event` fields if it had them. */
41
+ export interface SseEvent {
42
+ data: string;
43
+ id?: string;
44
+ event?: string;
45
+ }
46
+ export interface SseOptions {
47
+ /** Request headers besides Accept and Last-Event-ID. */
48
+ headers?: Record<string, string>;
49
+ /** Resume after this event id (a remembered one: the first connection sends it as Last-Event-ID). */
50
+ lastId?: string;
51
+ /** Reconnect backoff, ms (default 500 → 30 000, doubling; back to the minimum once an event arrived). */
52
+ backoff?: {
53
+ min: number;
54
+ max: number;
55
+ };
56
+ fetch?: typeof fetch;
57
+ log?(line: string): void;
58
+ }
59
+ /**
60
+ * One SSE subscription held open (#33, #58): connects, parses the stream,
61
+ * hands each event to `onEvent` — awaited before the next one is read and
62
+ * before its id counts as seen, so a caller that remembers `lastId` after it
63
+ * never skips an event — and reconnects with backoff, sending the last id
64
+ * seen as `Last-Event-ID`.
65
+ */
66
+ export declare class SseStream {
67
+ readonly url: string;
68
+ lastId?: string;
69
+ private abort;
70
+ private stopped;
71
+ private readonly o;
72
+ private readonly onEvent;
73
+ private running?;
74
+ constructor(url: string, onEvent: (ev: SseEvent) => Promise<void> | void, o?: SseOptions);
75
+ start(): this;
76
+ /** Close the connection; no reconnect. Resolves once the loop has ended (an event being handled finishes first). */
77
+ stop(): Promise<void>;
78
+ private say;
79
+ private connect;
80
+ /** Parse the event stream (text/event-stream): `data:` lines until a blank line make one event; `id:` and `event:` go with it. */
81
+ private read;
82
+ }
83
+ /** Headers per backfill run (#141): one `headers` frame each, parents first (#156). */
84
+ export declare const BACKFILL_RUN = 2016;
85
+ export declare class Feeds {
86
+ readonly o: FeedsOptions;
87
+ private sse;
88
+ private declared;
89
+ private hosted;
90
+ private listeners;
91
+ private queues;
92
+ private stopped;
93
+ constructor(o: FeedsOptions);
94
+ private say;
95
+ /** `handle`'s feeds are these (replacing what it declared before). */
96
+ declare(handle: string, specs: FeedSpec[]): void;
97
+ /**
98
+ * The host's own feed (#102) for `handle`: subscribed to it (`spec`), or not (undefined); a genesis's feeds are kept.
99
+ * `backfill` (#141): on subscribing, the headers the instance lacks up to the host's tip — read after the
100
+ * subscription is in place; until they are queued, the live items wait behind them (held), and a live header
101
+ * the backfill carries too is dropped. Admitted first, in runs of BACKFILL_RUN, then the live stream.
102
+ */
103
+ host(handle: string, spec: FeedSpec | undefined, backfill?: () => Promise<Uint8Array[]>): Promise<void>;
104
+ private queue;
105
+ /**
106
+ * The host's own listener on a headers feed (#132: the default image's
107
+ * chain part grows by it): every header the stream brings, as well as the
108
+ * subscribers'. The connection stays open while a listener is on it.
109
+ */
110
+ listen(url: string, fn: (raws: Uint8Array[]) => void): void;
111
+ /** Whether `handle` is subscribed to the host's own feed. */
112
+ hosts(handle: string): boolean;
113
+ /** Subscribe `handle` to what it declared and the host's feed for it, and to nothing else. */
114
+ private subscribe;
115
+ /** The one connection to `url`, opened if it is not. */
116
+ private connect;
117
+ /** The declared feeds of `handle`. */
118
+ of(handle: string): FeedSpec[];
119
+ /** Close every connection (no reconnect); resolves once their loops have ended. */
120
+ stop(): Promise<void>;
121
+ /** Queue a run of headers for `handle` (bounded), and drain the queue serially. */
122
+ push(handle: string, box: string, raws: Uint8Array[]): void;
123
+ /** How many items wait for `handle` (tests). */
124
+ pending(handle: string): number;
125
+ private drain;
126
+ private dispatch;
127
+ }