@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,2389 @@
1
+ // The host as a reverse proxy (#40): each instance is an HTTP server — its
2
+ // front door (programs/frontdoor) — at an origin of its own, and the router
3
+ // only picks the instance a request is for and forwards it. Routing comes
4
+ // before authentication, because a BRC-104 handshake does not name its
5
+ // recipient: the URL is the recipient. The router holds no sessions and no
6
+ // mailbox: it keeps the hostname → instance map (host.db), the kernels it has
7
+ // hydrated, the providers (the waker and the cron provider among them), the
8
+ // feeds, the broadcaster, the fuel ledger.
9
+ //
10
+ // http://<handle>.localhost:<port>/… an instance's origin (the Host header). The stock AuthFetch keeps one
11
+ // session per origin, and shakes hands at <origin>/.well-known/auth: this
12
+ // is the form it is served by. (SKEIN_INSTANCE_ORIGIN: another template.)
13
+ // http://<host>:<port>/@<handle>/… the same instance for our own clients (a dev form): the router strips
14
+ // the prefix for the routes; the client signs the path it sent, and its
15
+ // handshake goes under the prefix (src/client/raw.ts).
16
+ // The host's own origin (a host name whose first label is no instance) is the host skein's BRC-169 server
17
+ // (#113): these requests go to the host skein, as any request goes to its instance, under the onboarding
18
+ // app's routes (shruggr/skein-onboard: it registers, certifies through the certifier provider, records and
19
+ // answers; DISCOVERY below):
20
+ // GET /manifest.json → /onboard/manifest.json BRC-169 §5.1
21
+ // GET /.well-known/metanet-handles/resolve → /onboard/resolve BRC-169 §5.2
22
+ // GET /.well-known/metanet-handles/search → /onboard/search BRC-169 §5.6
23
+ // POST /.well-known/auth → /.well-known/auth the BRC-103 handshake (#135: a registration is signed)
24
+ // POST /account/register → /onboard/register a mailbox instance and its certificate (signed)
25
+ // POST /account/profile → /onboard/profile a handle holder's signed profile
26
+ // GET /bsvalias/id/<handle>[@<domain>] → /onboard/bsvalias/id/… the paymail PKI, from the same records
27
+ // With no host skein: 404 (tests may answer them with a fixture: `discovery`).
28
+ // GET /skeins?key=<hex> at the host's own origin: the skeins here whose head `grants` names the key
29
+ // among root's holders, [{handle, identity, url}] (no wallet token tracks them)
30
+ // GET /.well-known/skein-host at every host name, an instance's too (#103): {origin, domain} — this
31
+ // router's origin and the handle domain (the onboarding app's
32
+ // config.onboard.domain), for a page an instance serves (the management
33
+ // site) to find the manifest and register
34
+ // GET /<app>/.live/<topic> at an instance's origin (#138): the liveness tool's beats for that app's
35
+ // liveness on the topic (liveness.ts), served by the host, no program:
36
+ // JSON [{sender: <identity key, hex>, at: <ms>, body: <base64>, from: <peer
37
+ // ID>}], newest first; 404 when the app keeps no liveness for the topic.
38
+ // Unsigned, nothing logged; metered as a read (billing.ts)
39
+ // GET /<app>/.events?event=<name>&topic=<t>[&topic=<t2>…]
40
+ // at an instance's origin (#148): a Server-Sent Events stream of the app's
41
+ // emitted events of that name whose `topic` is one of those (events.ts),
42
+ // served by the host, no program: `id` the event's place in the log
43
+ // (<n>.<seq>.<i>.<thread>), `event` its name, `data` the record as DAG-JSON.
44
+ // First the last one per topic (or, with Last-Event-ID, every one after
45
+ // it, folded from the log), then each as handed over (its place in the
46
+ // kernel's notice: no log read). 400 without
47
+ // `event` or a `topic`; 404 when the app is not installed. Unsigned,
48
+ // nothing logged; each message metered as a read. Closed at the app's
49
+ // uninstall and when the instance stops
50
+ // POST /arc/callback Arcade's webhook (Bearer: the host token)
51
+ //
52
+ // The broadcaster (#58, #65, arc.ts) takes the instances' broadcast events
53
+ // (a durable queue in host.db, retries, one Arcade session) and routes what
54
+ // Arcade says of a transaction to every instance whose state holds it (a
55
+ // `has` of its CID, cached): a proof as an unsigned event in box `chain`,
56
+ // any other status as a signed message from the status provider.
57
+ //
58
+ // A request for an instance goes to one of its two doors (#135, frontdoor.ts
59
+ // serveHttp): a read — a path the instance's reads head names — is served by
60
+ // a `call` (no entry; its fuel metered); a signed request for a message route
61
+ // (#68, #66) is appended as received — one `request` entry, verified by
62
+ // nothing here — and the client's connection held until the thread the kernel
63
+ // steps the front door on comes to rest; its answer, signed on the session
64
+ // inside, is the response. Past `answerWaitMs`, or at shutdown, 503 +
65
+ // Retry-After. An unsigned request goes through the door only at an open row
66
+ // whose filter validates the payload (#135: signed or validated; answered
67
+ // plain); otherwise for a message route it is 401, for nothing 404, answered
68
+ // without an entry.
69
+ //
70
+ // What the instances send out goes through the host's providers (#70,
71
+ // providers.ts): each kernel hands over the messages its steps emitted
72
+ // (unsigned, #126 step 4) to a `local` provider or a libp2p recipient, and the provider's
73
+ // answer comes back as a signed message, a `local` request entry. The HTTP
74
+ // proxy (`fetch`) answers a URL of this host's own in process (the same
75
+ // path, no socket) and sends any other out; the waker keeps the instances'
76
+ // deadlines and their shells' sleeps (#69); the cron provider their
77
+ // schedules, each tick a signed message (#69, cron.ts; host.db
78
+ // `cron_schedule`); the status provider tells the instances that subscribe
79
+ // to it how their transactions stand (#65); the libp2p node is the
80
+ // instance's own (p2p.ts); the instance manager (#90) creates, starts and
81
+ // stops instances for the host skein alone (host.db `host_settings`:
82
+ // `skein-host init` made it; createInstance claims a new instance before
83
+ // its row is enabled, which publishes its hostname; image `mailbox`, #113, a
84
+ // mailbox instance for a registration); the certifier (#113) signs handle
85
+ // certificates for the host skein alone. Each genesis it writes names the providers' keys
86
+ // in its address book (`addressBook`). A kernel's broadcast events go to the
87
+ // broadcaster (#65).
88
+ //
89
+ // The libp2p host (#51, p2p.ts) is the router's too: one node per instance
90
+ // whose genesis declares `libp2p` or whose dispatch table has a libp2p row
91
+ // (#72, #77: a topic or a protocol), each topic message and stream
92
+ // frame one request entry (`p2pInbound`: the verdict or the frame's answer
93
+ // read off its thread and handed back to GossipSub or the stream). The node
94
+ // follows the dispatch table: once the kernel has processed what the host
95
+ // handed it (`settle`), the router reads the head (a kernel read), and when
96
+ // it moved, declares the instance's libp2p config again — an install's
97
+ // topics subscribed, an uninstall's unsubscribed, live (`syncDispatch`).
98
+ // The host's headers feed (#102, `headersFeed`, SKEIN_HEADERS_URL; feeds.ts)
99
+ // follows the table the same way: an enabled instance is subscribed while
100
+ // a row of it takes events in box `chain` (the chain app's), else not. The
101
+ // router itself listens to it too (#132, image-chain.ts): the default image
102
+ // carries the whole header chain and grows with every header the feed
103
+ // brings (filled from genesis at start from the feed's chaintracks history),
104
+ // and createInstance boots each new skein from the image as it stands.
105
+ //
106
+ // Nothing here is an instance's clock but its providers (#69): a schedule
107
+ // originates in a program's step, as a message to the cron provider; an
108
+ // idle-stopped instance is hydrated for a tick only if something in it
109
+ // subscribes the tick's box (from the cron provider, or from anyone).
110
+ import { createServer } from "node:http";
111
+ import { join } from "node:path";
112
+ import { KeyDeriver, P2PKH, PrivateKey, ProtoWallet, Transaction } from "@bsv/sdk";
113
+ import * as dagCbor from "@ipld/dag-cbor";
114
+ import { CID } from "multiformats/cid";
115
+ import { rootIdentity } from "../runtime/identity.js";
116
+ import { DEFAULTS, short, stampMs } from "../runtime/log.js";
117
+ import { ARC_ROUTE, Broadcaster } from "./arc.js";
118
+ import { DEFAULT_BOX, Feeds, feedsOf, txCid } from "./feeds.js";
119
+ import { Cron, dbSchedules } from "./cron.js";
120
+ import { currentDispatch, takesEvent, takesMail } from "../runtime/dispatch.js";
121
+ import { isSet, parseRocksSpec, parseSqliteSpec, storeSpec } from "../runtime/index-store.js";
122
+ import { WasmHost } from "./wasm-kernel.js";
123
+ import { openStoreFile, storeExists } from "./stores.js";
124
+ import { rootOf } from "./install.js";
125
+ import { OVERLAY_ROLE } from "./manifest.js";
126
+ import { appHead } from "./plan.js";
127
+ import { existsSync, realpathSync, statSync } from "node:fs";
128
+ import { now as clockNow } from "./clock.js";
129
+ import { boot, bootStore, imageSource, isOverlayImage } from "./boot.js";
130
+ import { chainHeader, historyOf, ImageChain, skeinTip } from "./image-chain.js";
131
+ import { admitOf, BEEF_PROTOCOL, OverlaySync, SYNC_PROTOCOL } from "./sync.js";
132
+ import { ANSWER_WAIT_MS, appendRequest, cidOf, headerMap, serveHttp, unavailable } from "./frontdoor.js";
133
+ import { keyBytes, keyHex } from "./genesis.js";
134
+ import { HANDLE, pathOnly, TOKEN_HANDLE, topicsOf, validHandle } from "./instances.js";
135
+ import { signClaim } from "../client/raw.js";
136
+ import { DoorAnswered, Kernel, KernelHost } from "./kernel.js";
137
+ import { foldLiveness, livenessEvent, livenessOf } from "./liveness.js";
138
+ import { EventStreams, eventsQuery, sseMessage } from "./events.js";
139
+ import { beaconEvent, beaconsOf, DEFAULT_LISTEN, emittedEvents, foldBeacons, foldSubscriptions, libp2pConfig, P2PHost, subscribedTopics, subscriptionEvent, subscriptionsOf } from "./p2p.js";
140
+ import { peerIdFromMultihash, peerIdFromString } from "@libp2p/peer-id";
141
+ import { MESSAGE_KEY_ID, MESSAGE_PROTOCOL, Providers, tooLarge } from "./providers.js";
142
+ import { closeControl, listenControl } from "./control.js";
143
+ import { Deliverer } from "./deliver.js";
144
+ import { certify, RESOLVE_PATH, SEARCH_PATH } from "./handles.js";
145
+ import { allocation, BILLING_HEAD, closedBy, mismatch, Period, periodRecord, priceHost, stateOf, termsOf, tickBody } from "./billing.js";
146
+ import * as Digest from "multiformats/hashes/digest";
147
+ const json = (status, v) => ({ status, headers: { "content-type": "application/json" }, body: new TextEncoder().encode(JSON.stringify(v)) });
148
+ const KEY = /^0[23][0-9a-f]{64}$/;
149
+ /** #148: an app's event stream, at an instance's origin. */
150
+ const EVENTS_ROUTE = /^\/([^/]+)\/\.events$/;
151
+ /** The apps a dispatch table names (an app with no row left is uninstalled). */
152
+ const appsOf = (rows) => new Set(rows.map((r) => r.app).filter((x) => typeof x === "string"));
153
+ export { HANDLE };
154
+ /** Handles no instance may take on this host (H3), besides the router origin's own first label (Router.reserved): `id` (the router's own in production) and `host` (the host skein's). */
155
+ export const RESERVED_HANDLES = new Set(["id", "host"]);
156
+ /** The registration signature (#113: the onboarding app checks it): [2, "skein register"], key ID the username, counterparty anyone, over `register <username>@<domain>`. */
157
+ export const REGISTER_PROTOCOL = [2, "skein register"];
158
+ /** A handle's domain as a manager's `create` takes it: a host name, lower case. */
159
+ const DOMAIN = /^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/;
160
+ /** The onboarding app (shruggr/skein-onboard, #90, #113): the host skein's app that serves BRC-169 on the host's own origin. */
161
+ export const ONBOARD_APP = "onboard";
162
+ /**
163
+ * The host's own origin's BRC-169 requests (#113) → the host skein's routes
164
+ * (the onboarding app's, under /onboard/): method + path → route.
165
+ */
166
+ export const DISCOVERY = {
167
+ "GET /manifest.json": `/${ONBOARD_APP}/manifest.json`,
168
+ [`GET ${RESOLVE_PATH}`]: `/${ONBOARD_APP}/resolve`,
169
+ [`GET ${SEARCH_PATH}`]: `/${ONBOARD_APP}/search`,
170
+ "POST /account/register": `/${ONBOARD_APP}/register`,
171
+ // #135: a registration is a signed request: the stock client's handshake at the host's own origin is the host skein's.
172
+ "POST /.well-known/auth": "/.well-known/auth",
173
+ "POST /account/profile": `/${ONBOARD_APP}/profile`,
174
+ };
175
+ /** The paymail PKI (`GET /bsvalias/id/<handle>[@<domain>]`) → the app's prefix row `/onboard/bsvalias/id/…`. */
176
+ export const PAYMAIL_PREFIX = "/bsvalias/id/";
177
+ /** The BRC-169 domain a host name stands for: a dev router at a loopback address answers for `localhost`. */
178
+ export const domainOf = (hostname) => {
179
+ const h = hostname.toLowerCase();
180
+ return h === "127.0.0.1" || h === "::1" || h === "[::1]" || h === "" ? "localhost" : h;
181
+ };
182
+ /**
183
+ * The BRC-169 domain of this host (H4): its public origin's host name
184
+ * (SKEIN_ROUTER_ORIGIN, default the loopback: `localhost`). What a created
185
+ * instance, a claim and a mailbox instance record when nothing names one,
186
+ * and what /.well-known/skein-host answers.
187
+ */
188
+ export const hostDomain = (origin) => {
189
+ try {
190
+ return domainOf(new URL(origin.replace("{port}", "0")).hostname);
191
+ }
192
+ catch {
193
+ return "localhost";
194
+ } // an origin that is no URL: the loopback's
195
+ };
196
+ /**
197
+ * SKEIN_HTTP=fetch: the HTTP proxy's requests (#70, the `fetch` provider)
198
+ * performed for real; `timeoutMs` (default 30 000) bounds the whole exchange,
199
+ * `maxBytes` (#91) the response body: reading stops past it, and the request
200
+ * fails.
201
+ */
202
+ export async function fetchHttp(req) {
203
+ const ac = new AbortController();
204
+ const timer = setTimeout(() => ac.abort(new Error(`timed out after ${req.timeoutMs ?? 30_000} ms`)), req.timeoutMs ?? 30_000);
205
+ try {
206
+ const r = await fetch(req.url, { method: req.method, headers: req.headers, body: req.body, signal: ac.signal });
207
+ const headers = Object.fromEntries(r.headers.entries());
208
+ if (req.maxBytes === undefined || !r.body)
209
+ return { status: r.status, headers, body: new Uint8Array(await r.arrayBuffer()) };
210
+ const chunks = [];
211
+ let n = 0;
212
+ for await (const c of r.body) {
213
+ n += c.length;
214
+ if (n > req.maxBytes) {
215
+ ac.abort();
216
+ throw new Error(tooLarge(req.maxBytes));
217
+ }
218
+ chunks.push(c);
219
+ }
220
+ return { status: r.status, headers, body: new Uint8Array(Buffer.concat(chunks)) };
221
+ }
222
+ finally {
223
+ clearTimeout(timer);
224
+ }
225
+ }
226
+ /**
227
+ * The warning for an agent whose genesis names no owner messagebox
228
+ * (`defaults.ownerMessagebox`): its sends to the owner — every answer to him —
229
+ * fail, since nothing else tells it where his mailbox is. Undefined if it names one.
230
+ */
231
+ export function noOwnerMessagebox(genesis) {
232
+ // An image (#89, #143) names no root: the claim brings the claimant's messagebox (into the address book).
233
+ if (genesis && genesis.root === undefined)
234
+ return undefined;
235
+ const d = genesis?.defaults;
236
+ if (typeof d?.ownerMessagebox === "string" && d.ownerMessagebox)
237
+ return undefined;
238
+ return "WARNING: its genesis names no owner messagebox (defaults.ownerMessagebox): nothing it sends the owner (its answers) can be delivered. " +
239
+ "Create the owner's mailbox instance (skein-host add <name> --mailbox --owner <key>) or set SKEIN_OWNER_MESSAGEBOX, then re-genesis it (a new store)";
240
+ }
241
+ /** A sendMessage request's box and recipient, for the delivery log line ("" if the body is neither CBOR nor JSON). */
242
+ function describeDelivery(req) {
243
+ const b = req.body ?? new Uint8Array();
244
+ let m;
245
+ try {
246
+ m = dagCbor.decode(b).message;
247
+ }
248
+ catch {
249
+ try {
250
+ m = JSON.parse(new TextDecoder().decode(b)).message;
251
+ }
252
+ catch { /* neither: the line names no box; the delivery's result is logged beside it */ }
253
+ }
254
+ if (!m)
255
+ return "";
256
+ const to = m.recipient instanceof Uint8Array ? Buffer.from(m.recipient).toString("hex") : typeof m.recipient === "string" ? m.recipient : "";
257
+ return `${String(m.messageBox ?? "?")}${to ? ` for ${short(to)}` : ""}`;
258
+ }
259
+ /** An error answer's description (JSON {description | error}), else its first 200 bytes. */
260
+ function errorOf(body) {
261
+ const t = new TextDecoder().decode(body.subarray(0, 2000));
262
+ try {
263
+ const j = JSON.parse(t);
264
+ return String(j.description ?? j.error ?? t.slice(0, 200));
265
+ }
266
+ catch {
267
+ return t.slice(0, 200);
268
+ } // not JSON: the text itself is the description
269
+ }
270
+ export class Router {
271
+ o;
272
+ feeds;
273
+ /** The cron provider's schedules (#69). */
274
+ cron;
275
+ /** The host's broadcaster (#58, #65): present when the host has an Arcade. */
276
+ arc;
277
+ /** The libp2p host (#51): one node per instance whose genesis declares libp2p. */
278
+ p2p;
279
+ /** shruggr/skein#150: overlay sync over the libp2p node (sync.ts). */
280
+ sync;
281
+ /** The host's providers (#70): what carries the instances' messages out. */
282
+ providers;
283
+ /** #148: the pages' subscriptions to their apps' emitted events (GET /<app>/.events), per instance. */
284
+ events;
285
+ /** The certifier (#100, #113): signs the BRC-169 handle certificates, for the host skein's onboarding app (the `certifier` provider). */
286
+ certifier;
287
+ /** #157: the `deliver` intention's carrier (BRC-103/104, the sessions the host keeps). */
288
+ deliverer;
289
+ loaded = new Map();
290
+ loading = new Map();
291
+ /** #156: the host chain record each running instance was last sent (a `chain` event), by handle; cleared at each start. */
292
+ sentChain = new Map();
293
+ /** Rows being created (#90): hydrated before their hostname is published (disabled until claimed). */
294
+ unpublished = new Set();
295
+ queues = new Map();
296
+ /** The libp2p nodes following their dispatch tables (#72: syncDispatch after a settle). */
297
+ syncing = new Set();
298
+ idleTimer;
299
+ ledgerTimer;
300
+ /** The fuel of calls not yet written to the ledger: instance\0caller\0op → {calls, fuel}. */
301
+ owed = new Map();
302
+ stopped = false;
303
+ closing;
304
+ /** Settles when the router starts shutting down: every client still waiting on a thread is answered 503 (#66). */
305
+ stopping;
306
+ stopNow;
307
+ /** Requests being forwarded (their clients held on a thread). */
308
+ inflight = new Set();
309
+ servers = [];
310
+ port = 0;
311
+ /** The control socket (control.ts), while this router listens on one. */
312
+ control;
313
+ /** The default image (#89), with its chain part (#132): what createInstance boots a new skein from. */
314
+ image;
315
+ /** #130: each billed instance as last read (its host row's terms, the kernel's billing state), and what this host metered since its last tick. */
316
+ views = new Map();
317
+ periods = new Map();
318
+ /** Instances a tick is on its way to; and those sent their first (billing starts with it) since this router started: the timer sends it again. */
319
+ ticking = new Set();
320
+ firstTicks = new Set();
321
+ tickTimer;
322
+ /** The host's provider keys (its billing key among them, #130). */
323
+ providerKey;
324
+ /** The one-process kernels this router holds (#158), by database directory. */
325
+ kernelHosts = new Map();
326
+ wasmHosts = new Map();
327
+ /** The wasm kernel's host over the SQLite database at `db` (the settled split): its machines, its host chain. */
328
+ wasmHost(db) {
329
+ let h = this.wasmHosts.get(db);
330
+ if (!h) {
331
+ h = WasmHost.acquire(db, { network: this.chainNetwork() ?? "main", log: (l) => this.say("kernel", l) });
332
+ this.wasmHosts.set(db, h);
333
+ }
334
+ return h;
335
+ }
336
+ /** The `skein-kernel host` process for the RocksDB database at `dir` (#158), started on first use. */
337
+ kernelHost(dir) {
338
+ let h = this.kernelHosts.get(dir);
339
+ if (!h) {
340
+ h = KernelHost.acquire(dir, { command: this.o.kernel?.command, env: this.kernelEnv(), log: (l) => this.say("kernel", l) });
341
+ this.kernelHosts.set(dir, h);
342
+ }
343
+ return h;
344
+ }
345
+ constructor(o) {
346
+ this.o = o;
347
+ if (o.rocksStore)
348
+ this.kernelHost(o.rocksStore); // started with the router: the host's reads go through it (stores.ts)
349
+ this.image = new ImageChain({ db: o.db, history: historyOf(o.headersFeed), log: (l) => this.say("router", l) });
350
+ this.stopping = new Promise((r) => { this.stopNow = r; });
351
+ this.feeds = new Feeds({
352
+ headers: (h, box, raws) => this.chainHeaders(h, box, raws), log: (s, l) => this.say(s, l),
353
+ maxQueue: o.feeds?.maxQueue, backoff: o.feeds?.backoff,
354
+ });
355
+ this.cron = new Cron({
356
+ now: () => stampMs(this.now()), store: dbSchedules(o.db), log: (s, l) => this.say(s, l),
357
+ tick: async (s, body) => {
358
+ const row = this.o.db.get(s.instance);
359
+ if (!row || row.status !== "enabled") {
360
+ this.cron.forget(s.instance);
361
+ throw new Error("the instance is gone");
362
+ }
363
+ await this.providers.send(s.instance, "cron", keyBytes(s.recipient), s.box, body);
364
+ },
365
+ // An idle-stopped instance is hydrated for a tick only if something in it takes the box (from the cron provider, or anyone).
366
+ wake: async (s) => {
367
+ const l = this.loaded.get(s.instance);
368
+ if ((l && !l.kernel.gone) || this.loading.has(s.instance))
369
+ return true;
370
+ return await this.subscribes(s.instance, s.box, this.providers.key("cron"));
371
+ },
372
+ });
373
+ if (o.arc) {
374
+ this.arc = new Broadcaster({
375
+ arc: o.arc, db: o.db, fetch: o.arcFetch, backoff: o.feeds?.backoff, ...(o.arcRetry ? { retry: o.arcRetry } : {}), log: (s, l) => this.say(s, l),
376
+ admit: (h, box, ev) => this.admitEvent(h, box, ev),
377
+ status: async (h, body, subject) => {
378
+ const id = this.o.db.get(h)?.identity;
379
+ if (!id)
380
+ throw new Error("its identity is not known yet");
381
+ await this.providers.send(h, "status", keyBytes(id), "chain/status", body, subject);
382
+ },
383
+ now: () => stampMs(this.now()),
384
+ instances: () => ({ all: this.o.db.list("enabled").map((r) => r.handle), running: [...this.loaded].filter(([, l]) => !l.kernel.gone).map(([h]) => h) }),
385
+ holds: async (h, txid) => await (await this.hydrate(h)).kernel.hasBlock(txCid(txid)),
386
+ });
387
+ }
388
+ if (o.peerKeyFor) {
389
+ this.p2p = new P2PHost({
390
+ host: o.libp2p ?? { listen: DEFAULT_LISTEN, bootstrap: [], dht: "off", relays: [], mdns: false },
391
+ keyOf: o.peerKeyFor, discoveryMs: o.libp2pDiscoveryMs,
392
+ inbound: (h, c) => this.p2pInbound(h, c),
393
+ log: (s, l) => this.say(s, l),
394
+ // #126: a beacon's beat signed by the instance's signer, as an intention request is.
395
+ sign: (h, data) => this.signAs(h, data),
396
+ identity: async (h) => { const id = this.o.db.get(h)?.identity; return id ? keyBytes(id) : undefined; },
397
+ beat: (h, topic, b) => { this.sync?.beat(h, topic, b); void this.rememberPeer(h, topic, b.from); },
398
+ });
399
+ const p2pHost = this.p2p;
400
+ this.sync = new OverlaySync({
401
+ overlayOf: (h, liveTopic) => this.loaded.get(h)?.beacons.find((b) => b.topic === liveTopic)?.app,
402
+ local: (h, app, fn, query) => this.overlayRead(h, app, fn, query),
403
+ ownBeat: (h, liveTopic) => this.loaded.get(h)?.beacons.find((b) => b.topic === liveTopic)?.body,
404
+ deliver: (h, peer, beef) => this.p2pInbound(h, { transport: "libp2p", protocol: BEEF_PROTOCOL, from: peerIdFromString(peer).toMultihash().bytes, body: beef }),
405
+ header: async (h, height) => {
406
+ const row = this.o.db.get(h);
407
+ if (!row || !storeExists(storeSpec(row)))
408
+ return undefined;
409
+ const st = openStoreFile(storeSpec(row), { readOnly: true });
410
+ try {
411
+ return await chainHeader(st, height);
412
+ }
413
+ finally {
414
+ st.close();
415
+ }
416
+ },
417
+ request: (h, peer, body) => p2pHost.request(h, peer, SYNC_PROTOCOL, body),
418
+ beats: (h, liveTopic) => p2pHost.live.live(h, liveTopic),
419
+ self: (h) => p2pHost.peerId(h),
420
+ log: (s, l) => this.say(s, l),
421
+ });
422
+ }
423
+ // Without a host's own provider keys: a router's own, fresh (a test's).
424
+ const own = o.providerKeyFor ? undefined : new KeyDeriver(PrivateKey.fromRandom());
425
+ const keyOf = o.providerKeyFor ?? ((n) => own.derivePrivateKey([2, "skein provider"], n, "self"));
426
+ this.providerKey = keyOf;
427
+ // The certifier key is the certifier provider's (signer.ts certifierKey: providerKey("certifier")).
428
+ this.certifier = new ProtoWallet(keyOf("certifier"));
429
+ const p2p = this.p2p;
430
+ this.deliverer = new Deliverer({ http: (req, from) => this.http(req, from), log: (s, l) => this.say(s, l) });
431
+ this.events = new EventStreams({
432
+ open: (h) => { const row = this.o.db.get(h); return row && storeExists(storeSpec(row)) ? openStoreFile(storeSpec(row), { readOnly: true }) : undefined; },
433
+ });
434
+ this.providers = new Providers({
435
+ keyOf,
436
+ append: (h, pkg) => this.appendLocal(h, pkg),
437
+ identity: async (h) => { const id = this.o.db.get(h)?.identity; return id ? keyBytes(id) : undefined; },
438
+ sign: (h, data) => this.signAs(h, data),
439
+ fetch: (req, from) => this.http(req, from),
440
+ ...(this.arc ? { broadcast: (h, tx, beef) => { this.arc.enqueue(h, tx, beef); } } : {}),
441
+ ...(p2p ? { topicEvent: (h, rec) => this.topicEvent(h, rec) } : {}),
442
+ ...(o.appEvent ? { appEvent: o.appEvent } : {}),
443
+ emitted: (h, rec, place) => this.events.emitted(h, rec, place),
444
+ deliver: (h, id, rec) => this.deliver(h, id, rec),
445
+ cron: (h, sender, id, body) => this.cron.request(h, sender, id, body),
446
+ // #90: the instance manager acts for the host skein alone.
447
+ manager: {
448
+ from: () => { const r = this.o.db.hostSkein(); return r?.identity ? { handle: r.handle, identity: r.identity } : undefined; },
449
+ // #162: SKEIN_MANAGER_FOR — these instances too (each by its own identity).
450
+ alsoFor: () => (this.o.managerFor ?? []).flatMap((h) => { const r = this.o.db.get(h); return r?.identity && r.status === "enabled" ? [{ handle: r.handle, identity: r.identity }] : []; }),
451
+ request: (box, body) => this.manage(box, body),
452
+ },
453
+ // #113: the certifier signs for the host skein alone (the onboarding app records each issue).
454
+ certifier: (box, body) => certify(this.certifier, box, body),
455
+ // #130: a payment is this host's: kept and broadcast.
456
+ ...(o.billing ? { payment: (h, rec) => this.payment(h, rec) } : {}),
457
+ ...(p2p ? {
458
+ p2p: {
459
+ publish: (h, topic, body) => p2p.publish(h, topic, body),
460
+ dial: (h, peer, protocol, frames) => p2p.dial(h, peer, protocol, frames),
461
+ send: (h, s, body) => p2p.send(h, s, body),
462
+ close: (h, s) => p2p.close(h, s),
463
+ },
464
+ } : {}),
465
+ now: () => stampMs(this.now()),
466
+ log: (s, l) => this.say(s, l),
467
+ });
468
+ if (o.cron !== false)
469
+ this.cron.start();
470
+ const idle = o.idleMs ?? 0;
471
+ if (idle > 0)
472
+ this.idleTimer = setInterval(() => void this.reap(idle), Math.max(50, Math.min(idle / 4, 10_000)));
473
+ this.ledgerTimer = setInterval(() => this.flushLedger(), o.ledgerMs ?? 5000);
474
+ if (o.billing)
475
+ this.tickTimer = setInterval(() => void this.tickAll(), o.billing.tickMs);
476
+ }
477
+ say(source, line) { this.o.log?.(source, line); }
478
+ now() { return (this.o.now ?? clockNow)(); }
479
+ /** The router's clock, as entries are stamped. */
480
+ nowStamp() { return this.now(); }
481
+ /** This host's own origin. */
482
+ origin() { return (this.o.origin ?? "http://127.0.0.1:{port}").replace("{port}", String(this.port || this.o.port || 0)); }
483
+ /**
484
+ * The first label of the router's own origin, when its host name is a
485
+ * name (`id` for id.skein.nexus), else undefined: no instance may take it,
486
+ * so a request to the router's origin never reaches an instance (H3).
487
+ */
488
+ ownLabel() {
489
+ let h;
490
+ try {
491
+ h = new URL(this.origin()).hostname.toLowerCase();
492
+ }
493
+ catch {
494
+ return undefined;
495
+ } // an origin that is no URL names no label
496
+ if (!h.includes(".") || /^[0-9.]+$/.test(h) || h.startsWith("["))
497
+ return undefined;
498
+ return h.split(".")[0];
499
+ }
500
+ /** Whether `handle` is reserved on this host: `id`, `host` (RESERVED_HANDLES), or the router origin's own first label. */
501
+ reserved(handle) {
502
+ return RESERVED_HANDLES.has(handle) || (handle === this.ownLabel() && !this.ownLabelInstance(handle));
503
+ }
504
+ /**
505
+ * The router's own label as an instance (2026-10-28, one public name): allowed only when the operator names
506
+ * that handle in SKEIN_MANAGER_FOR (managerFor) — the Open Exchange's config instance at the host's own
507
+ * origin. Its hostname then serves the instance; the router keeps /.well-known/skein-host, /skeins and
508
+ * /arc/callback there. Any other row named like the router's origin still never takes its requests.
509
+ */
510
+ ownLabelInstance(handle) {
511
+ return handle === this.ownLabel() && this.managerFor(handle);
512
+ }
513
+ /** This host's BRC-169 domain: its origin's host name (hostDomain). */
514
+ domain() { return hostDomain(this.origin()); }
515
+ /** An instance's origin: where its front door is, what BRC-169 publishes as its messagebox. */
516
+ originOf(handle) {
517
+ // #161: a path-only handle (a token id) has no hostname: its origin is /@<handle> on the host's own.
518
+ if (pathOnly(handle))
519
+ return `${this.origin().replace(/\/+$/, "")}/@${handle}`;
520
+ return (this.o.instanceOrigin ?? "http://{handle}.localhost:{port}").replace("{handle}", handle).replace("{port}", String(this.port || this.o.port || 0));
521
+ }
522
+ /** Hydrate every enabled row once (recovery at hydrate time: what a waiting thread awaits is handed to the providers again), then let them idle out. */
523
+ async start() {
524
+ // #132: the image's chain part, filled from the feed's history (from genesis once; then from near its tip), then grown by the feed.
525
+ if (this.o.headersFeed) {
526
+ void this.image.start();
527
+ this.feeds.listen(this.o.headersFeed, (raws) => { void this.image.headers(raws); });
528
+ }
529
+ for (const row of this.o.db.list("enabled")) {
530
+ try {
531
+ await this.hydrate(row.handle);
532
+ }
533
+ catch (e) {
534
+ this.say(row.handle, `not started: ${e.message}`);
535
+ }
536
+ }
537
+ }
538
+ /**
539
+ * Stop everything this router started (#61), once: the providers' (the
540
+ * waker's, the cron provider's), the idle reaper's and the ledger's timers
541
+ * (the owed fuel written first), its
542
+ * servers and their connections, the control socket (its file removed), the header feeds' and the broadcaster's
543
+ * SSE clients, the libp2p nodes, and every kernel process it spawned — one
544
+ * still hydrating included. Afterwards nothing of it keeps the process
545
+ * alive: `skein-host run`'s shutdown and every one-shot command that builds
546
+ * a router (`add --boot/--packet`) end with it.
547
+ */
548
+ close() {
549
+ this.closing ??= this.shutdown();
550
+ return this.closing;
551
+ }
552
+ /** The same as `close()`. */
553
+ stop() { return this.close(); }
554
+ /**
555
+ * Listen on the control socket at `path` ($SKEIN_HOME/host.sock for
556
+ * `skein-host run`; mode 0600, no HTTP route): `skein-host event` sends its
557
+ * event here while this router runs, and it reaches the instance as a tick
558
+ * due now (cronEvent). Removed by `close()`.
559
+ */
560
+ async listenControl(path) {
561
+ if (this.stopped)
562
+ throw new Error("the router is stopping");
563
+ const server = await listenControl(path, {
564
+ event: async (h, box, ev) => (await this.cronEvent(h, box, ev)).toString(),
565
+ reclaim: (h) => this.reclaim(h),
566
+ message: async (h, m, body) => (await this.messageLocal(h, m, body)).toString(),
567
+ });
568
+ this.control = { server, path };
569
+ this.say("router", `control socket at ${path}`);
570
+ }
571
+ async shutdown() {
572
+ this.stopped = true;
573
+ this.sync?.stop();
574
+ // Clients waiting on a thread are answered 503 + Retry-After before the servers close (#66).
575
+ this.stopNow();
576
+ if (this.inflight.size) {
577
+ await Promise.all([...this.inflight].map((p) => p.catch(() => { }))); // each answers its own client (a 503 now): this only waits for them
578
+ await new Promise((r) => setTimeout(r, 50)); // their 503s written before the sockets close
579
+ }
580
+ clearInterval(this.idleTimer);
581
+ clearInterval(this.ledgerTimer);
582
+ clearInterval(this.tickTimer);
583
+ this.flushLedger();
584
+ const closed = this.servers.map((s) => new Promise((r) => { s.close(() => r()); s.closeAllConnections(); }));
585
+ this.servers = [];
586
+ if (this.control)
587
+ closed.push(closeControl(this.control.server, this.control.path));
588
+ this.control = undefined;
589
+ this.providers.stop();
590
+ this.events.stop();
591
+ await Promise.all([this.cron.stop(), this.feeds.stop(), this.arc?.stop(), this.p2p?.stop()]);
592
+ // A kernel still hydrating: its load finishes (or fails) first, then it is stopped with the rest.
593
+ await Promise.all([...this.loading.values()].map((p) => p.catch(() => { }))); // a failed load was reported to whoever asked for it
594
+ await Promise.all([...this.loaded.values()].map((l) => l.kernel.stop()));
595
+ this.loaded.clear();
596
+ // #158: the one-process kernels released (the last holder's release stops the process: its database flushed).
597
+ await Promise.all([...this.kernelHosts.values()].map((h) => h.release()));
598
+ this.kernelHosts.clear();
599
+ await Promise.all([...this.wasmHosts.values()].map((h) => h.release()));
600
+ this.wasmHosts.clear();
601
+ await Promise.all(closed);
602
+ }
603
+ /**
604
+ * `skein-host event` (#69): a message from the cron provider into `box`, as
605
+ * a tick due now would be — {...event, kind: event.kind ?? "cron", due:
606
+ * now}, signed by the provider and appended as a `local` request; the
607
+ * instance takes it by its subscription to the box (from the cron provider,
608
+ * or from anyone). The entry's CID.
609
+ */
610
+ async cronEvent(handle, box, event) {
611
+ const row = this.o.db.get(handle);
612
+ if (!row || row.status !== "enabled")
613
+ throw new Error(`no enabled instance ${handle}`);
614
+ const l = await this.hydrate(handle);
615
+ return await this.providers.send(handle, "cron", keyBytes(l.identity), box, { ...event, kind: event.kind ?? "cron", due: stampMs(this.now()) });
616
+ }
617
+ /**
618
+ * A plain entry (#29, #65): a self-validating event from the host's wiring
619
+ * (a header feed, the broadcaster's proof), admitted into the instance in
620
+ * `box` with its record beside the entry (#164: the kernel puts it — after
621
+ * the door, when `box`'s event route names kernel.beef: its BEEFs decoded
622
+ * and checked, the bytes never stored; a BUMP that does not check, or no
623
+ * BEEF, is turned away: DoorAnswered, thrown, nothing written). The kernel
624
+ * routes it by its `subject` to the thread awaiting that record, else to
625
+ * `box`'s event route.
626
+ */
627
+ async admitEvent(handle, box, event) {
628
+ return await this.serial(handle, async () => {
629
+ const l = await this.hydrate(handle);
630
+ const closed = this.closed(handle);
631
+ if (closed)
632
+ throw new Error(`not forwarded (#130): ${closed}`);
633
+ const e = await l.kernel.admit({ box, event: cidOf(event) }, { event }, this.now());
634
+ this.settle(l);
635
+ return e;
636
+ });
637
+ }
638
+ /**
639
+ * #157: a `deliver` intention from `handle`, carried (deliver.ts: BRC-103/104 with the instance's
640
+ * key) and its answer admitted in box `deliver` — it stands on its own at the door (the server's
641
+ * signature). No answer: nothing admitted, the thread rests (offered again at its next start).
642
+ */
643
+ async deliver(handle, id, rec) {
644
+ const l = await this.hydrate(handle);
645
+ const mail = await l.kernel.store.get(rec.message);
646
+ const body = dagCbor.encode(await l.kernel.store.get(mail.body));
647
+ const ans = await this.deliverer.carry(handle, l.wallet, id, rec, mail, body);
648
+ if (!ans)
649
+ return false;
650
+ try {
651
+ await this.admitEvent(handle, "deliver", ans);
652
+ this.say(handle, `deliver → ${String(rec.url)}: HTTP ${ans.status}, the signed answer admitted`);
653
+ return true;
654
+ }
655
+ catch (e) {
656
+ this.say(handle, `deliver → ${String(rec.url)}: the answer turned away: ${e.message}`);
657
+ return false;
658
+ }
659
+ }
660
+ /**
661
+ * A message for an instance (#70): a provider's signed answer, a forwarded
662
+ * claim, or the loopback's own emit, appended as received — a `local`
663
+ * request entry, {kind: "message", message, body}; the front door checks it
664
+ * (the signature, or for the loopback the record in the store) and routes
665
+ * it. Nobody waits on it.
666
+ */
667
+ async appendLocal(handle, pkg) {
668
+ return await this.serial(handle, async () => {
669
+ const l = await this.hydrate(handle);
670
+ // #130: the gate lets through the instance's own messages to itself (the loopback), nothing else.
671
+ const closed = this.closed(handle);
672
+ if (closed && !isLoopback(pkg, l.identity))
673
+ throw new Error(`not forwarded (#130): ${closed}`);
674
+ const e = await appendRequest(l.kernel, "local", pkg, this.now());
675
+ this.settle(l);
676
+ return e;
677
+ });
678
+ }
679
+ /**
680
+ * #142: a message signed outside the host (the client `skein`, with the operator's key), delivered on
681
+ * the host machine over the control socket: its mail record (dag-cbor) and body, appended as a `local`
682
+ * request (appendLocal — the path a forwarded claim takes). The front door checks the signature and
683
+ * admits it by the dispatch table as it would any signed message; this waits for its verdict and
684
+ * throws on a refusal. The host signs nothing. The entry.
685
+ */
686
+ async messageLocal(handle, messageBytes, body) {
687
+ if (!this.o.db.get(handle))
688
+ throw new Error(`no instance ${handle}`);
689
+ let message;
690
+ try {
691
+ message = dagCbor.decode(messageBytes);
692
+ }
693
+ catch {
694
+ throw new Error("the message is not dag-cbor");
695
+ }
696
+ if (!message || typeof message !== "object" || Array.isArray(message) || !message.signature)
697
+ throw new Error("not a signed message (a mail record with its signature)");
698
+ // #143: turned away at the door (a filter, the transport's check) writes no entry: DoorAnswered, thrown.
699
+ const entry = await this.appendLocal(handle, { kind: "message", message, body });
700
+ const l = await this.hydrate(handle);
701
+ const a = await l.kernel.answer(entry, this.o.answerWaitMs ?? ANSWER_WAIT_MS);
702
+ if (a.state === "errored")
703
+ throw new Error(`the front door failed: ${a.error}`);
704
+ if (a.state !== "finished")
705
+ throw new Error(`not judged in time (${a.state}); entry ${entry}`);
706
+ const v = dagCbor.decode(a.answer);
707
+ if (v.verdict !== "accept")
708
+ throw new Error(`${v.verdict ?? "not accepted"}: ${v.reason ?? ""}`.trim());
709
+ return entry;
710
+ }
711
+ /** Sign `data` with `handle`'s signer: [2, "metanet handles envelope"] / `send` / anyone — an intention request's signature (providers.ts), a beacon's beat (p2p.ts beaconFrame). */
712
+ async signAs(handle, data) {
713
+ const row = this.o.db.get(handle);
714
+ if (!row)
715
+ throw new Error(`no instance ${handle}`);
716
+ const { signature } = await (await this.o.walletFor(row)).createSignature({ protocolID: MESSAGE_PROTOCOL, keyID: MESSAGE_KEY_ID, counterparty: "anyone", data: [...data] });
717
+ return Uint8Array.from(signature);
718
+ }
719
+ /**
720
+ * An inbound libp2p message or stream frame (#51, #68): appended as
721
+ * received (the message with its signature; the frame), and the verdict
722
+ * — or the frame's answer — read off the thread the front door is stepped
723
+ * on, once it comes to rest (#66). The front door verifies, routes, and
724
+ * admits what an accept carries (the message's `p2p` event first); a
725
+ * redelivered message it accepted before is `ignore`. Its thread failing,
726
+ * or not answering within the wait, is `ignore` (no penalty). A frame a
727
+ * route's filter answers at the door (a read, #143: nothing written) is
728
+ * answered with the filter's body (#154: the overlay's sync protocol).
729
+ */
730
+ async p2pInbound(handle, call) {
731
+ const l = await this.hydrate(handle);
732
+ const closed = this.closed(handle);
733
+ if (closed)
734
+ return { verdict: "ignore", reason: `not forwarded (#130): ${closed}` };
735
+ const source = `libp2p:${call.topic ?? call.protocol ?? "?"}`;
736
+ let from = "";
737
+ try {
738
+ from = peerIdFromMultihash(Digest.decode(call.from)).toString();
739
+ }
740
+ catch { /* not a peer ID: only this log line's name; the front door judges the record */ }
741
+ // The settled sync design (David 2026-10-30, skein-overlay 0.20.0): an `admit` announcement on an overlay
742
+ // topic is the host's, never the instance's — fetched from the announcer unless held (sync.ts), nothing
743
+ // logged; GossipSub's verdict ignore (no forward: each node announces its own admission).
744
+ if (call.topic !== undefined && this.sync) {
745
+ const app = this.loaded.get(handle)?.beacons.find((b) => b.topic === `${call.topic}-live`)?.app;
746
+ const txid = app ? admitOf(call.topic, call.body) : undefined;
747
+ if (app && txid && from) {
748
+ this.sync.admit(handle, app, call.topic, txid, from);
749
+ return { verdict: "ignore", reason: "an announcement: the host fetches it" };
750
+ }
751
+ }
752
+ const record = call.topic !== undefined
753
+ ? { kind: "p2p", topic: call.topic, from: call.from, seqno: call.seqno ?? new Uint8Array(), signature: call.signature ?? new Uint8Array(), body: call.body }
754
+ : { kind: "p2p-frame", protocol: call.protocol ?? "", from: call.from, body: call.body };
755
+ let entry;
756
+ try {
757
+ entry = await appendRequest(l.kernel, "libp2p", record, this.now());
758
+ }
759
+ catch (e) {
760
+ if (!(e instanceof DoorAnswered))
761
+ throw e;
762
+ // #154: a frame on a stream answered by a filter (a read: nothing written) — its body is the answer frame.
763
+ if (call.topic === undefined && e.answer.kind === "answer")
764
+ return { verdict: "accept", body: e.answer.body ?? new Uint8Array() };
765
+ // #121, #143: turned away at the door (nothing written). A bad signature or a bad BEEF is the sender's fault
766
+ // (reject: GossipSub penalises the forwarder); what this instance cannot judge (no chain state, a failing check) is ignore.
767
+ const reason = e.answer.reason ?? "turned away";
768
+ const verdict = e.answer.status < 500 && !/no chain state/.test(reason) ? "reject" : "ignore";
769
+ this.say(handle, `${source} from ${from}: ${verdict} (turned away at the door: ${reason})`);
770
+ return { verdict, reason };
771
+ }
772
+ const a = await l.kernel.answer(entry, this.o.answerWaitMs ?? ANSWER_WAIT_MS);
773
+ this.settle(l);
774
+ if (a.state !== "finished") {
775
+ const why = a.state === "errored" ? `the front door failed: ${a.error}` : `not answered in time (${a.state})`;
776
+ this.say(handle, `${source} from ${from}: ${why}`);
777
+ return { verdict: "ignore", reason: why };
778
+ }
779
+ const r = dagCbor.decode(a.answer);
780
+ const verdict = r.verdict === "accept" || r.verdict === "reject" ? r.verdict : "ignore";
781
+ if (verdict !== "accept" || process.env.SKEIN_LIBP2P_VERBOSE)
782
+ this.say(handle, `${source} from ${from}: ${verdict}${r.reason ? ` (${r.reason})` : ""}`);
783
+ return { verdict, ...(r.body instanceof Uint8Array ? { body: r.body } : {}), ...(r.close ? { close: true } : {}), ...(r.reason ? { reason: r.reason } : {}) };
784
+ }
785
+ /** Until nothing is queued and every loaded kernel has processed what it was given (tests, corpus). */
786
+ async settled() {
787
+ for (let i = 0; i < 1000; i++) {
788
+ await Promise.all([...this.queues.values()]);
789
+ await Promise.all([...this.loaded.values()].map((l) => l.kernel.idle().catch(() => { }))); // a kernel that exited is reported by its exit (hydrate's `exited`): this only waits
790
+ await this.providers.idle(); // what the providers carry now (#70): its answers are entries
791
+ await this.arc?.idle(); // the broadcaster's posts (#65): their answers are statuses to route
792
+ await Promise.all([...this.syncing]); // the libp2p nodes following their dispatch tables (#72, #77)
793
+ await new Promise((r) => setImmediate(r));
794
+ if (!this.syncing.size && !this.queues.size && !this.providers.busy() && !this.arc?.busy() && [...this.loaded.values()].every((l) => l.kernel.busy === 0))
795
+ return;
796
+ }
797
+ }
798
+ /**
799
+ * Run `f` after everything else queued for this instance: the host's own admissions (events from its
800
+ * wiring, `local` messages: the waker's, the providers', the loopback, a forwarded claim) reach the
801
+ * kernel in the order the host made them, each after its hydrate and `closed` check, and `settled`
802
+ * waits on them. Not the log's order: the kernel assigns every entry's place (#163), so HTTP and
803
+ * libp2p arrivals, which do not queue here, need no queue.
804
+ */
805
+ serial(handle, f) {
806
+ const prev = this.queues.get(handle) ?? Promise.resolve();
807
+ // f's failure goes to its caller through `next`; the queue only keeps the order.
808
+ const next = prev.catch(() => { }).then(f);
809
+ const tail = next.catch(() => { });
810
+ this.queues.set(handle, tail);
811
+ void tail.then(() => { if (this.queues.get(handle) === tail)
812
+ this.queues.delete(handle); });
813
+ return next;
814
+ }
815
+ // ---------------------------------------------------------------- the host chain (#156)
816
+ /**
817
+ * The network the host chain is on: SKEIN_NETWORK (`network`), else the genesis defaults'
818
+ * `walletNetwork` (a test host's), else the kernel's default (main).
819
+ */
820
+ chainNetwork() {
821
+ const d = this.o.genesis?.defaults;
822
+ return this.o.network ?? (typeof d?.walletNetwork === "string" ? d.walletNetwork : undefined);
823
+ }
824
+ /** The kernels' environment: the configured one, and the host chain's network (#156). */
825
+ kernelEnv() {
826
+ const net = this.chainNetwork();
827
+ return net ? { ...this.o.kernel?.env, SKEIN_NETWORK: net } : this.o.kernel?.env;
828
+ }
829
+ /**
830
+ * A run of headers from a feed for `handle` (#156): into the host chain — the one kernel
831
+ * process's for a RocksDB set (an instance not running is not hydrated for it: it gets the
832
+ * chain at its next start), the instance's own serve kernel's otherwise — then, when the
833
+ * record changed for it, the `chain` event.
834
+ */
835
+ async chainHeaders(handle, box, raws) {
836
+ const row = this.o.db.get(handle);
837
+ if (!row)
838
+ return;
839
+ const rocks = parseRocksSpec(storeSpec(row));
840
+ const sq = parseSqliteSpec(storeSpec(row));
841
+ let tip;
842
+ if (rocks || sq) {
843
+ tip = rocks ? await this.kernelHost(rocks.dir).chainHeaders(raws) : await this.wasmHost(sq.db).chainHeaders(raws);
844
+ const l = this.loaded.get(handle);
845
+ if (!l || l.kernel.gone)
846
+ return;
847
+ }
848
+ else {
849
+ tip = await (await this.hydrate(handle)).kernel.chainHeaders(raws);
850
+ }
851
+ await this.chainEvent(handle, box, tip);
852
+ }
853
+ /** The `chain` event (#156) for `handle` in `box`, unless it was sent that record already since its start. */
854
+ async chainEvent(handle, box, tip) {
855
+ const key = tip.root.toString();
856
+ if (this.sentChain.get(handle) === key)
857
+ return;
858
+ this.sentChain.set(handle, key);
859
+ try {
860
+ await this.admitEvent(handle, box, { kind: "chain", root: tip.root, height: tip.height });
861
+ }
862
+ catch (e) {
863
+ if (this.sentChain.get(handle) === key)
864
+ this.sentChain.delete(handle);
865
+ throw e;
866
+ }
867
+ }
868
+ /**
869
+ * Fresh-host bootstrap (#156): the host chain empty and the instance's tree (its head `main`,
870
+ * an image's) carrying `chain/headers`: those imported into the host chain before the
871
+ * instance's first event. A tree with none, or a host chain with a tip: nothing.
872
+ */
873
+ async chainBootstrap(handle, kernel) {
874
+ if (await kernel.chainTip())
875
+ return;
876
+ const tree = await kernel.call("head", "main");
877
+ if (!tree || tree.code !== 0x78)
878
+ return;
879
+ try {
880
+ const t0 = Date.now();
881
+ const got = await kernel.chainImport(tree);
882
+ this.say("router", `host chain: ${got.added} headers imported from ${handle}'s image (tip ${got.height}, ${Date.now() - t0} ms)`);
883
+ }
884
+ catch (e) {
885
+ const m = e.message;
886
+ if (!/NoChain|NotEmpty/.test(m))
887
+ this.say(handle, `host chain: the image's chain not imported: ${m}`);
888
+ }
889
+ }
890
+ /** At start (and once an app takes box `chain` — an install): the host chain's current record, as the `chain` event. */
891
+ async chainBirth(handle) {
892
+ const l = this.loaded.get(handle);
893
+ if (!l || l.kernel.gone)
894
+ return;
895
+ const rows = l.rows ?? (await l.kernel.dispatch()).rows;
896
+ if (!rows.some((r) => r.address === DEFAULT_BOX && takesEvent(r, DEFAULT_BOX)))
897
+ return;
898
+ const tip = await l.kernel.chainTip();
899
+ if (tip)
900
+ await this.chainEvent(handle, DEFAULT_BOX, tip);
901
+ }
902
+ // ---------------------------------------------------------------- hydration
903
+ /** The instance's kernel, started (and its genesis written) if it is not loaded. */
904
+ hydrate(handle) {
905
+ const l = this.loaded.get(handle);
906
+ if (l && !l.kernel.gone)
907
+ return Promise.resolve(l);
908
+ let p = this.loading.get(handle);
909
+ if (!p) {
910
+ p = this.load(handle).finally(() => this.loading.delete(handle));
911
+ this.loading.set(handle, p);
912
+ }
913
+ return p;
914
+ }
915
+ async load(handle) {
916
+ if (this.stopped)
917
+ throw new Error("the router is stopping");
918
+ const row = this.o.db.get(handle);
919
+ if (!row || (row.status !== "enabled" && !this.unpublished.has(handle)))
920
+ throw new Error(`no enabled instance ${handle}`);
921
+ const wallet = await this.o.walletFor(row);
922
+ const identity = await rootIdentity(wallet);
923
+ if (row.identity && row.identity !== identity)
924
+ throw new Error(`its signer is ${short(identity)}, not the recorded identity ${short(row.identity)}`);
925
+ const ko = {
926
+ db: storeSpec(row), handle: row.handle, domain: row.domain, wallet, command: this.o.kernel?.command, env: this.kernelEnv(),
927
+ log: (line) => this.say(handle, line),
928
+ emit: (o) => this.providers.deliver(handle, o),
929
+ exited: (code, signal) => {
930
+ if (this.loaded.get(handle)?.kernel === kernel)
931
+ this.loaded.delete(handle);
932
+ if (code !== 0)
933
+ this.say(handle, `kernel exited (${signal ?? `code ${code}`})`);
934
+ },
935
+ };
936
+ // #158: a RocksDB set is an instance of the host's one kernel process (opened there: hydrate); else a serve process of its own.
937
+ // The settled split (2026-10-31): a SQLite set's machine is the wasm kernel, loaded in this process on a worker.
938
+ const rocks = parseRocksSpec(ko.db);
939
+ const sq = parseSqliteSpec(ko.db);
940
+ const kernel = sq ? await this.wasmHost(sq.db).open(ko) : rocks ? await this.kernelHost(rocks.dir).open(ko) : new Kernel(ko);
941
+ let g;
942
+ try {
943
+ if (!(await kernel.store.log.tip())) {
944
+ // No tree was loaded into this store (`skein-host add --boot/--packet` does that): the stock system, through the same loader.
945
+ const b = await boot(kernel, { kind: "code" }, this.genesisConfig(row, identity), this.now());
946
+ this.say(handle, `genesis ${b.entry}`);
947
+ }
948
+ g = await kernel.genesis();
949
+ if (!g)
950
+ throw new Error("the store's log does not start with a genesis this kernel reads (format 3, #40): start a new store (re-genesis)");
951
+ if (keyHex(g.identity) !== identity)
952
+ throw new Error(`the signer (${short(identity)}) is not this instance's identity (${short(keyHex(g.identity))})`);
953
+ await kernel.start();
954
+ // #156: after the start (the genesis processed: its tree is the head `main`), before any event is admitted.
955
+ await this.chainBootstrap(handle, kernel);
956
+ await kernel.running(identity);
957
+ void kernel.idle().catch(() => { }); // busy until what start resumed is done; a kernel that exits meanwhile is reported by its exit
958
+ if (!row.identity)
959
+ this.o.db.add(row.handle, { identity });
960
+ if (row.kind !== "mailbox") {
961
+ const w = noOwnerMessagebox(g);
962
+ if (w)
963
+ this.say(handle, w);
964
+ }
965
+ this.feeds.declare(handle, feedsOf(g));
966
+ }
967
+ catch (e) {
968
+ await kernel.stop(1000);
969
+ throw e;
970
+ }
971
+ // #119: the apps' subscriptions, read from the log (the kernel writes nothing for them).
972
+ const subs = await this.foldSubscriptions(storeSpec(row)).catch((e) => { this.say(handle, `libp2p: the subscriptions not read: ${e.message}`); return []; });
973
+ // #126: the apps' beacons, read from the log the same way.
974
+ const beacons = await this.foldBeacons(storeSpec(row)).catch((e) => { this.say(handle, `libp2p: the beacons not read: ${e.message}`); return []; });
975
+ // #138: the apps' liveness, read from the log the same way.
976
+ const live = await this.readLiveness(storeSpec(row)).catch((e) => { this.say(handle, `libp2p: the liveness not read: ${e.message}`); return []; });
977
+ const l = { row, kernel, identity, wallet, genesis: g, subs, beacons, live };
978
+ // Its node (the genesis's libp2p and the dispatch table's libp2p rows), before anything it runs can publish or dial; the host's headers feed.
979
+ await this.syncDispatch(l, true);
980
+ this.loaded.set(handle, l);
981
+ // #156: the host chain as it stands, once at each start (not awaited: an admit made here would wait on this load).
982
+ this.sentChain.delete(handle);
983
+ void this.chainBirth(handle).catch((e) => this.say(handle, `host chain: the start event not sent: ${e.message}`));
984
+ // The settled sync design: the peers each overlay topic's beats came from (host.db), asked at once.
985
+ if (this.sync)
986
+ void this.askKnownPeers(l).catch((e) => this.say(handle, `sync: the known peers not asked: ${e.message}`));
987
+ await this.syncBilling(l).catch((e) => this.say(handle, `billing: not read: ${e.message}`));
988
+ this.say("router", `hydrated ${handle} (${short(identity)})`);
989
+ return l;
990
+ }
991
+ /**
992
+ * Whether a stopped instance's route table routes an entry in `box` — an event, or (given a
993
+ * sender) a message: the kernel's rule (#77, #143: dispatch.zig forMail/forEvent, by the box
994
+ * alone; who may run the function is the gate's, when it is routed). Read from its store file,
995
+ * read-only, with no kernel.
996
+ */
997
+ async subscribes(handle, box, sender) {
998
+ const row = this.o.db.get(handle);
999
+ if (!row || !storeExists(storeSpec(row)))
1000
+ return false;
1001
+ const s = openStoreFile(storeSpec(row), { readOnly: true });
1002
+ try {
1003
+ const rows = await currentDispatch(s);
1004
+ return !!rows?.some((r) => sender !== undefined ? takesMail(r, box) : takesEvent(r, box));
1005
+ }
1006
+ finally {
1007
+ s.close();
1008
+ }
1009
+ }
1010
+ /**
1011
+ * Boot a row's empty store from a system tree or a checkpoint (issue #4,
1012
+ * boot.ts) before its first hydration; the row's identity is its signer's.
1013
+ */
1014
+ async bootRow(handle, src, o = {}) {
1015
+ const row = this.o.db.get(handle);
1016
+ if (!row)
1017
+ throw new Error(`no instance ${handle}`);
1018
+ if (this.loaded.has(handle))
1019
+ throw new Error(`${handle} is loaded: boot only an empty store`);
1020
+ if (o.image && src.kind !== "tree")
1021
+ throw new Error("an image is a system tree");
1022
+ if (o.manager && !o.image)
1023
+ throw new Error("the instance manager is the host skein's, and the host skein is an image");
1024
+ if ((o.owner || o.appConfig) && !o.image)
1025
+ throw new Error("an owner and app config at birth are an image's (#142)");
1026
+ const identity = await rootIdentity(await this.o.walletFor(row));
1027
+ // #142: an image booted with its owner named (the host skein): the owner's rows and its apps' `$owner` rows at birth, no claim row.
1028
+ const c = src.kind === "checkpoint" ? { identity, root: [this.o.owner ?? identity], handle: row.handle, domain: row.domain } : o.image ? { ...this.imageConfig(row, identity, { manager: o.manager }), ...(o.owner ? { root: [o.owner] } : {}), ...(o.appConfig ? { appConfig: o.appConfig } : {}) } : this.genesisConfig(row, identity, src.kind === "code");
1029
+ const b = await bootStore({ db: storeSpec(row), handle: row.handle, domain: row.domain, command: this.o.kernel?.command, env: this.kernelEnv(), log: (l) => this.say(handle, l) }, src, c, this.now());
1030
+ this.o.db.add(row.handle, { identity, ...(b.tree ? { tree: b.tree.toString() } : {}) });
1031
+ return b;
1032
+ }
1033
+ /** The owner's messagebox: configured, else its mailbox instance here. */
1034
+ ownerMessagebox() {
1035
+ if (this.o.ownerMessagebox)
1036
+ return this.o.ownerMessagebox;
1037
+ const mb = this.o.owner ? this.o.db.mailboxOf(this.o.owner) : undefined;
1038
+ return mb ? this.originOf(mb.handle) : undefined;
1039
+ }
1040
+ /**
1041
+ * A new agent's address book seed (#70): this host's providers — the HTTP
1042
+ * proxy, the waker, the cron provider (#69), the libp2p node (when it runs
1043
+ * one), the status provider (#65, when it has an Arcade) — and the owner's
1044
+ * mailbox, when it knows it.
1045
+ */
1046
+ addressSeed(handle) {
1047
+ const names = ["fetch", "waker", "cron", ...(this.p2p ? ["libp2p"] : []), ...(this.arc ? ["status"] : []), ...(handle !== undefined && this.managerFor(handle) ? ["manager"] : [])];
1048
+ const out = this.providers.entries(names);
1049
+ const mb = this.ownerMessagebox();
1050
+ if (this.o.owner && mb)
1051
+ out.push({ key: keyBytes(this.o.owner), transport: "mailbox", address: mb, ...(this.o.ownerHandle ?? {}) });
1052
+ return out;
1053
+ }
1054
+ /**
1055
+ * What this host brings to an image's genesis (#89): only its own facts —
1056
+ * the instance's identity, handle and domain, its providers in the address
1057
+ * book, where its domain resolves, the host's defaults. No owner, no
1058
+ * owner's mailbox, no inference peer, no names: the image is the same for
1059
+ * everyone, and the owner is the claim's sender (#127). `manager`
1060
+ * (#90, #113): the host skein's — the instance manager and the certifier
1061
+ * in its address book too (no other instance's book names them).
1062
+ */
1063
+ imageConfig(row, identity, o = {}) {
1064
+ // #162: an instance SKEIN_MANAGER_FOR names gets the instance manager (not the certifier), as the host skein has it.
1065
+ const seed = this.providers.entries(["fetch", "waker", "cron", ...(this.p2p ? ["libp2p"] : []), ...(this.arc ? ["status"] : []), ...(o.manager ? ["manager", "certifier"] : this.managerFor(row.handle) ? ["manager"] : [])]);
1066
+ return {
1067
+ identity, handle: row.handle, domain: row.domain, resolveOrigin: this.origin(), network: this.o.network, addressBook: seed,
1068
+ providers: Object.fromEntries(seed.map((e) => [e.address, Buffer.from(e.key).toString("hex")])),
1069
+ feeds: this.o.genesis?.feeds, defaults: this.o.genesis?.defaults, overrides: this.o.fuelPerStep ? { fuelPerStep: this.o.fuelPerStep } : undefined,
1070
+ warn: (l) => this.say(row.handle, l),
1071
+ };
1072
+ }
1073
+ /** #162: whether SKEIN_MANAGER_FOR names `handle` (the instance manager in its address book, its messages taken). */
1074
+ managerFor(handle) { return (this.o.managerFor ?? []).includes(handle); }
1075
+ /**
1076
+ * #162: the image a create names, as a boot source — the built-in `default` (this host's, with its
1077
+ * chain part), or a directory `<imagesDir>/<name>` (SKEIN_IMAGES_DIR, default $SKEIN_HOME/images;
1078
+ * the layout `skein-host add --image <dir>` takes: with no bin/ merged over the default image as
1079
+ * images/host is, with bin/ a complete image). Its path resolved first (a link to an app repository's
1080
+ * image/ reads `..` beside the repository's image, not beside the link).
1081
+ */
1082
+ async namedImage(name, host) {
1083
+ if (host || name === undefined || name === "default")
1084
+ return await this.image.source({ host, ...(name ? { image: name } : {}) });
1085
+ const dir = this.imageDir(name);
1086
+ if (!dir)
1087
+ throw new Error(`image ${JSON.stringify(name)}: this host has the default image, mailbox instances${this.o.imagesDir ? ` and the images in ${this.o.imagesDir}` : ""}`);
1088
+ return isOverlayImage(dir) ? await this.image.source({ overlay: dir }) : await imageSource(dir);
1089
+ }
1090
+ /** #162: an image by name: its directory under imagesDir (resolved), or undefined. */
1091
+ imageDir(name) {
1092
+ if (!this.o.imagesDir || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name === "host" || name === "mailbox")
1093
+ return undefined;
1094
+ try {
1095
+ const d = realpathSync(join(this.o.imagesDir, name));
1096
+ return statSync(d).isDirectory() ? d : undefined;
1097
+ }
1098
+ catch {
1099
+ return undefined;
1100
+ } // no such image here
1101
+ }
1102
+ /** Whether `owner` holds root on the instance (#143: the head `grants`): claimed by that key. */
1103
+ async claimedBy(l, owner) {
1104
+ const root = await l.kernel.call("head", "grants");
1105
+ if (!root)
1106
+ return false;
1107
+ const g = await l.kernel.store.get(root).catch(() => undefined);
1108
+ return (g?.roles?.root ?? []).some((k) => k instanceof Uint8Array && Buffer.from(k).toString("hex") === owner);
1109
+ }
1110
+ /**
1111
+ * A new instance from an image, owned by `owner` (#90: the instance
1112
+ * manager's `create`; `skein-host init` for the host skein). In this order:
1113
+ * the row, disabled (no hostname: nothing reaches it); its identity
1114
+ * derived (the signer's, key ID = the handle) and its store booted from the
1115
+ * image (only `default`, the default image, so far); its kernel started
1116
+ * unpublished; `claim` (#127) — the owner's own signed claim, a message in
1117
+ * box `claim` naming no recipient (it was signed before the instance
1118
+ * existed), with its body — forwarded into it as a `local` request, its
1119
+ * first entry after the genesis: the front door checks the signature, and
1120
+ * the kernel takes the owner from the signer (the owner's admin rows
1121
+ * written, the claim row removed); only then (`publish`, default true) the
1122
+ * row enabled, which publishes its hostname. No request can reach the claim
1123
+ * row first. The host signs nothing for the owner. With no `claim` (a bare
1124
+ * image: `skein-host init`), the instance keeps its claim row, from anyone:
1125
+ * the first sender of a claim owns it. `host`: the host skein — the
1126
+ * instance manager and the certifier in its address book, and recorded as
1127
+ * the host skein (host.db). `domain`: the handle's domain, recorded with
1128
+ * the row (default `localhost`). A refusal throws (a bad or taken handle, a
1129
+ * bad key, another image, a claim not `owner`'s or refused: the row is left
1130
+ * disabled, its store kept for a look).
1131
+ *
1132
+ * `owned` (#164): a non-host image instance born owned as the host skein
1133
+ * is — its genesis's `root` is `owner`, no claim (none taken, none made:
1134
+ * the image's claim route is not seeded), no instance manager or
1135
+ * certifier in its address book (those stay the host skein's and
1136
+ * SKEIN_MANAGER_FOR's).
1137
+ *
1138
+ * Image `mailbox` (#113): a mailbox instance for `owner` — the front door
1139
+ * and the messagebox, its owner in its genesis (no claim) — published at
1140
+ * once (`publish`). The same owner and handle again: the same answer (it
1141
+ * exists, enabled again if it was not, at `domain` if given); a key with
1142
+ * another mailbox here is refused. One creation path: the manager's `create` (the onboarding app's
1143
+ * registrations) and `skein-host add --mailbox` both come here.
1144
+ */
1145
+ async createInstance(handle, owner, o = {}) {
1146
+ if (!validHandle(handle))
1147
+ throw new Error(`handle ${JSON.stringify(handle)}: lower-case letters, digits and "-", at most 63, as a hostname label; or a token id <txid>_<vout> (path-only, #161)`);
1148
+ if (this.reserved(handle) && !(o.host && handle === "host"))
1149
+ throw new Error(`handle ${handle} is reserved`);
1150
+ if (!KEY.test(owner))
1151
+ throw new Error("owner: not an identity key (33 bytes)");
1152
+ if (o.image !== undefined && o.image !== "default" && o.image !== "mailbox" && !this.imageDir(o.image))
1153
+ throw new Error(`image ${JSON.stringify(o.image)}: this host has the default image${this.o.imagesDir ? `, the images in ${this.o.imagesDir}` : ""} and mailbox instances`);
1154
+ if (o.domain !== undefined && !DOMAIN.test(o.domain))
1155
+ throw new Error(`domain ${JSON.stringify(o.domain)}: a host name, lower case`);
1156
+ if (o.image === "mailbox")
1157
+ return await this.createMailbox(handle, owner, o);
1158
+ if (o.host && (o.claim !== undefined || o.needsClaim))
1159
+ throw new Error("the host skein is born owned (#142): no claim");
1160
+ // #164: an owned image instance is born owned as the host skein is: no claim.
1161
+ if (o.owned && (o.claim !== undefined || o.needsClaim))
1162
+ throw new Error("an owned instance is born owned (#164): no claim");
1163
+ const owned = o.host || o.owned;
1164
+ let claim = o.claim !== undefined ? claimOf(o.claim) : undefined;
1165
+ // #162: a one-key host claims what it creates itself — the claim signed by its shared key (the owner).
1166
+ if (!claim && !owned && this.o.claimer && o.needsClaim) {
1167
+ const me = (await this.o.claimer.getPublicKey({ identityKey: true })).publicKey;
1168
+ if (owner !== me)
1169
+ throw new Error(`owner: on a one-key host the claim is the host's own key's (${short(me)}): name it or no owner, or forward the owner's signed claim`);
1170
+ claim = claimOf(await signClaim(this.o.claimer));
1171
+ }
1172
+ if (!claim && o.needsClaim)
1173
+ claimOf(undefined);
1174
+ if (claim)
1175
+ claimProblem(claim, owner);
1176
+ if (this.o.db.get(handle))
1177
+ throw new Error(`handle ${handle} is taken`);
1178
+ const store = this.newStore(handle);
1179
+ if (storeExists(storeSpec({ store, handle })))
1180
+ throw new Error(`handle ${handle}: a store is at ${storeSpec({ store, handle })} already`);
1181
+ this.o.db.add(handle, { store, status: "disabled", domain: o.domain ?? await this.handleDomain(), ...(o.topics?.length ? { topics: JSON.stringify(o.topics) } : {}) }); // #137: the names it is routed by. H4: never the column's `localhost` on a host with a domain
1182
+ this.unpublished.add(handle);
1183
+ try {
1184
+ await this.image.ready; // #132: born with the chain as the host holds it
1185
+ // #142, #143: the host skein from the host image (images/host over images/default), its genesis's `root` the
1186
+ // operator's key, `appConfig` merged.
1187
+ // #162: any instance's app config at birth (a create's `config`), the host skein's owner at birth.
1188
+ // #164: an owned instance's genesis names `owner` root (boot.ts then seeds no claim route), as the host skein's does.
1189
+ await this.bootRow(handle, await this.namedImage(o.image, o.host), { image: true, manager: o.host, ...(owned ? { owner } : {}), ...(o.appConfig ? { appConfig: o.appConfig } : {}) });
1190
+ const l = await this.hydrate(handle);
1191
+ if (claim) {
1192
+ // #127: the owner's own message, forwarded as signed; the host signs nothing for the owner. #143: one the door
1193
+ // turns away (its signature does not hold) writes no entry.
1194
+ let entry;
1195
+ try {
1196
+ entry = await this.appendLocal(handle, { kind: "message", message: claim.message, body: claim.body });
1197
+ }
1198
+ catch (e) {
1199
+ if (e instanceof DoorAnswered)
1200
+ throw new Error(`the claim message was refused at the door (${e.answer.reason ?? e.answer.status}); ${handle} is left disabled`);
1201
+ throw e;
1202
+ }
1203
+ await this.queues.get(handle);
1204
+ await l.kernel.idle();
1205
+ if (!(await this.claimedBy(l, owner)))
1206
+ throw new Error(`the claim ${entry} was refused (its log says why); ${handle} is left disabled`);
1207
+ }
1208
+ if (o.host)
1209
+ this.o.db.setSetting("host_skein", handle);
1210
+ if (o.publish !== false) {
1211
+ this.o.db.setStatus(handle, "enabled");
1212
+ await this.refollowHeaders(handle);
1213
+ }
1214
+ const tip = this.image.tip();
1215
+ this.say("router", `created ${handle} (${short(l.identity)}) from the ${o.host ? "host" : o.image ?? "default"} image${tip ? ` (its chain to ${tip.height})` : ""}, ${owned ? `owned by ${short(owner)}${o.host ? "" : " (born owned, #164)"}` : claim ? `claimed by ${short(owner)}${o.claim === undefined ? " (the host's own key, #162)" : ""}` : "unclaimed (its claim row from anyone)"}${o.publish !== false ? `, at ${this.originOf(handle)}` : ""}${o.host ? ": the host skein" : ""}`);
1216
+ return { handle, identity: l.identity, url: this.originOf(handle) };
1217
+ }
1218
+ finally {
1219
+ this.unpublished.delete(handle);
1220
+ }
1221
+ }
1222
+ /** host.db's `store` for a new instance (#96): its set in the host's LMDB environment (SKEIN_STORE), else its own file. */
1223
+ newStore(handle) {
1224
+ if (this.o.sqliteStore)
1225
+ return `sqlite:${this.o.sqliteStore}`; // the settled split: the wasm kernel
1226
+ if (this.o.rocksStore)
1227
+ return `rocksdb:${this.o.rocksStore}`; // #158
1228
+ return this.o.lmdbStore ? `lmdb:${this.o.lmdbStore}` : join(this.o.home ?? ".", "instances", handle, "runtime.db");
1229
+ }
1230
+ /** createInstance's image `mailbox` (#113). */
1231
+ async createMailbox(handle, owner, o) {
1232
+ const had = this.o.db.get(handle);
1233
+ if (had) {
1234
+ if (had.kind !== "mailbox" || had.owner !== owner)
1235
+ throw new Error(`handle ${handle} is taken`);
1236
+ // The handle's domain is the asker's (a row registered before #113 took the request's host name).
1237
+ if (o.domain && had.domain !== o.domain)
1238
+ this.o.db.add(handle, { domain: o.domain });
1239
+ if (o.publish !== false && had.status !== "enabled")
1240
+ this.o.db.setStatus(handle, "enabled");
1241
+ const identity = had.identity ?? await rootIdentity(await this.o.walletFor(had));
1242
+ return { handle, identity, url: this.originOf(handle) };
1243
+ }
1244
+ const mine = this.o.db.list().find((r) => r.kind === "mailbox" && r.owner === owner);
1245
+ if (mine)
1246
+ throw new Error(`${short(owner)} has a mailbox instance here already: ${mine.handle}`);
1247
+ const store = this.newStore(handle);
1248
+ if (storeExists(storeSpec({ store, handle })))
1249
+ throw new Error(`handle ${handle}: a store is at ${storeSpec({ store, handle })} already`);
1250
+ this.o.db.add(handle, { store, status: "disabled", kind: "mailbox", owner, ...(o.domain ? { domain: o.domain } : {}) });
1251
+ await this.bootRow(handle, { kind: "code" });
1252
+ const identity = this.o.db.get(handle).identity;
1253
+ if (o.publish !== false)
1254
+ this.o.db.setStatus(handle, "enabled");
1255
+ this.say("router", `created mailbox instance ${handle}@${this.o.db.get(handle).domain} (${short(identity)}) for ${short(owner)}${o.publish !== false ? `, at ${this.originOf(handle)}` : ""}`);
1256
+ return { handle, identity, url: this.originOf(handle) };
1257
+ }
1258
+ /**
1259
+ * The instance manager's work (#90; providers.ts `manager`): a message from
1260
+ * the host skein in box `create`, `start` or `stop` → the answer body. A
1261
+ * refusal throws (the provider answers {error}).
1262
+ */
1263
+ async manage(box, b) {
1264
+ const handle = typeof b.handle === "string" ? b.handle : "";
1265
+ if (box === "create") {
1266
+ // #162: on a one-key host with no claim forwarded, the owner is the host's own key (named or not).
1267
+ const shared = this.o.claimer && b.claim == null ? (await this.o.claimer.getPublicKey({ identityKey: true })).publicKey : undefined;
1268
+ const owner = b.owner instanceof Uint8Array ? Buffer.from(b.owner).toString("hex") : typeof b.owner === "string" ? b.owner : shared ?? "";
1269
+ if (b.image != null && typeof b.image !== "string")
1270
+ throw new Error("image: the image's name (text)");
1271
+ if (b.domain != null && typeof b.domain !== "string")
1272
+ throw new Error("domain: a host name (text)");
1273
+ // #162: `config` — the new instance's app config at birth, by app name (as an install's --config), any image's.
1274
+ if (b.config != null && (typeof b.config !== "object" || Array.isArray(b.config) || b.config instanceof Uint8Array || Object.values(b.config).some((x) => !x || typeof x !== "object" || Array.isArray(x))))
1275
+ throw new Error("config: the apps' config at birth, {<app>: {…}}");
1276
+ // #127: a skein from the default image comes with its owner's signed claim (the registrant's, forwarded); #162: a one-key host's own.
1277
+ // skein-amm#4: `beef` — an Atomic BEEF (the token's deploy) handed into the new instance once it is up.
1278
+ if (b.beef != null && !(b.beef instanceof Uint8Array))
1279
+ throw new Error("beef: an Atomic BEEF (bytes)");
1280
+ const topics = topicsArg(b.topics);
1281
+ const c = await this.createInstance(handle, owner, { image: b.image ?? undefined, ...(typeof b.domain === "string" ? { domain: b.domain } : {}), ...(b.claim != null ? { claim: b.claim } : {}), ...(b.config != null ? { appConfig: b.config } : {}), ...(topics ? { topics } : {}), needsClaim: true });
1282
+ const answer = { handle: c.handle, identity: keyBytes(c.identity), url: c.url };
1283
+ if (b.beef instanceof Uint8Array)
1284
+ answer.delivered = await this.deliverBeef(c.handle, b.beef);
1285
+ return answer;
1286
+ }
1287
+ if (box !== "start" && box !== "stop")
1288
+ throw new Error(`the instance manager takes create, start and stop: not ${box}`);
1289
+ const row = this.o.db.get(handle);
1290
+ if (!row)
1291
+ throw new Error(`no instance ${handle}`);
1292
+ if (row.handle === this.o.db.hostSkein()?.handle)
1293
+ throw new Error(`${handle} is the host skein: it is not started or stopped by a message from itself`);
1294
+ if (box === "start") {
1295
+ // #137: `topics` replaces the names the row is routed by; absent keeps them.
1296
+ const topics = topicsArg(b.topics);
1297
+ if (topics)
1298
+ this.o.db.setTopics(handle, topics);
1299
+ this.o.db.setStatus(handle, "enabled");
1300
+ await this.hydrate(handle);
1301
+ await this.refollowHeaders(handle);
1302
+ this.say("router", `${handle}: started by the instance manager`);
1303
+ return { handle, started: true, url: this.originOf(handle) };
1304
+ }
1305
+ this.o.db.setStatus(handle, "disabled");
1306
+ await this.refollowHeaders(handle);
1307
+ const l = this.loaded.get(handle);
1308
+ if (l) {
1309
+ this.loaded.delete(handle);
1310
+ await l.kernel.stop();
1311
+ }
1312
+ // skein-amm#4: off its libp2p node too (as reclaim), so a stopped instance's beacons stop beating; a start's hydrate declares it again.
1313
+ await this.p2p?.declare(handle, undefined).catch(() => { });
1314
+ this.events.drop(handle);
1315
+ this.say("router", `${handle}: stopped by the instance manager`);
1316
+ return { handle, stopped: true };
1317
+ }
1318
+ /**
1319
+ * A create's `beef` (skein-amm#4): the Atomic BEEF handed into the new instance's receive row
1320
+ * (/skein/overlay/beef/1.0.0) exactly as a catch-up's fetched BEEF is (sync.ts `deliver`), from the
1321
+ * host's own peer, and the answer → {verdict, reason?}. No topic is named here: the instance's engine
1322
+ * judges it under the topics its config declares. Turned away at the door (unmined, a bad BUMP): reported,
1323
+ * not retried; the instance stays created.
1324
+ */
1325
+ async deliverBeef(handle, beef) {
1326
+ let from = new Uint8Array();
1327
+ try {
1328
+ if (this.p2p)
1329
+ from = peerIdFromString(this.p2p.peerId(handle)).toMultihash().bytes;
1330
+ }
1331
+ catch { /* no peer: the record names none */ }
1332
+ // The door checks the BUMPs against chain/state: the host chain's event (#156, sent at the start, not awaited there) taken first.
1333
+ await this.chainBirth(handle).catch(() => { });
1334
+ const l = this.loaded.get(handle);
1335
+ if (l) {
1336
+ await this.queues.get(handle);
1337
+ await l.kernel.idle().catch(() => { });
1338
+ }
1339
+ try {
1340
+ const r = await this.p2pInbound(handle, { transport: "libp2p", protocol: BEEF_PROTOCOL, from, body: beef });
1341
+ this.say("router", `${handle}: the create's BEEF delivered: ${r.verdict}${r.reason ? ` (${r.reason})` : ""}`);
1342
+ return { verdict: r.verdict, ...(r.reason ? { reason: r.reason } : {}) };
1343
+ }
1344
+ catch (e) {
1345
+ const reason = e.message;
1346
+ this.say("router", `${handle}: the create's BEEF not delivered: ${reason}`);
1347
+ return { verdict: "ignore", reason };
1348
+ }
1349
+ }
1350
+ /** What this host brings to a new instance's genesis (boot.ts): the owner and its messagebox, the inference peer, their names, the host's defaults. */
1351
+ genesisConfig(row, identity, code = true) {
1352
+ if (row.kind === "mailbox") {
1353
+ if (!row.owner)
1354
+ throw new Error(`${row.handle}: a mailbox instance names its owner`);
1355
+ return { identity, root: [row.owner], handle: row.handle, domain: row.domain, mailbox: true, network: this.o.network };
1356
+ }
1357
+ if (!this.o.owner)
1358
+ throw new Error("an empty store needs the owner's identity key (SKEIN_OWNER) for its genesis");
1359
+ const genesisDefaults = this.o.genesis?.defaults;
1360
+ const hostDefaults = { ...genesisDefaults, ...(this.o.fuelPerStep ? { fuelPerStep: this.o.fuelPerStep } : {}) };
1361
+ const warn = (l) => this.say(row.handle, l);
1362
+ const facts = { ownerMessagebox: this.ownerMessagebox(), resolveOrigin: this.origin(), addressBook: this.addressSeed(row.handle), providers: Object.fromEntries(this.addressSeed(row.handle).filter((e) => e.transport === "local").map((e) => [e.address, Buffer.from(e.key).toString("hex")])) };
1363
+ if (!code) {
1364
+ // A system tree: its config wins; the host fills what it leaves unset. SKEIN_FUEL_PER_STEP stays an explicit (dev) override.
1365
+ return {
1366
+ identity, root: [this.o.owner], handle: row.handle, domain: row.domain, infer: this.o.infer, ownerHandle: this.o.ownerHandle, inferHandle: this.o.inferHandle,
1367
+ feeds: this.o.genesis?.feeds, defaults: genesisDefaults, overrides: this.o.fuelPerStep ? { fuelPerStep: this.o.fuelPerStep } : undefined, warn, network: this.o.network, ...facts,
1368
+ };
1369
+ }
1370
+ return {
1371
+ identity, root: [this.o.owner], handle: row.handle, domain: row.domain, infer: this.o.infer, ownerHandle: this.o.ownerHandle, inferHandle: this.o.inferHandle,
1372
+ // Code genesis takes DEFAULTS under the host's.
1373
+ defaults: Object.keys(hostDefaults).length ? { ...DEFAULTS, ...hostDefaults } : undefined,
1374
+ subscriptions: this.o.genesis?.subscriptions,
1375
+ feeds: this.o.genesis?.feeds,
1376
+ libp2p: this.o.genesis?.libp2p, dispatch: this.o.genesis?.dispatch,
1377
+ network: this.o.network, ...facts,
1378
+ };
1379
+ }
1380
+ /**
1381
+ * The kernel counts as busy until it has processed what was just admitted
1382
+ * (its `idle` answers after the drain); then the libp2p node and the host's
1383
+ * headers feed follow the dispatch table (#72, #77, #102).
1384
+ */
1385
+ settle(l) {
1386
+ const p = l.kernel.idle().then(() => this.syncDispatch(l)).then(() => this.syncBilling(l)).catch((e) => this.say(l.row.handle, `the dispatch table not followed: ${e.message}`));
1387
+ this.syncing.add(p);
1388
+ void p.finally(() => this.syncing.delete(p));
1389
+ }
1390
+ /**
1391
+ * What the host does by the instance's dispatch table (#72, #77, #102):
1392
+ * read the table (the kernel's `dispatch` frame); when its chain's tip
1393
+ * moved since it was last followed (or `first`), the host's headers feed
1394
+ * subscribes or unsubscribes it (followHeaders), and its libp2p node is
1395
+ * declared again as its genesis and the table ask (p2p.ts libp2pConfig) —
1396
+ * started, reconfigured (topics subscribed and unsubscribed, protocols
1397
+ * handled and unhandled) or stopped. A failure is logged, not fatal.
1398
+ */
1399
+ async syncDispatch(l, first = false) {
1400
+ if ((!this.p2p && !this.o.headersFeed && !this.events.open(l.row.handle)) || l.kernel.gone)
1401
+ return;
1402
+ const d = await l.kernel.dispatch().catch((e) => { this.say(l.row.handle, `the dispatch table not read: ${e.message}`); return undefined; });
1403
+ if (d === undefined)
1404
+ return;
1405
+ const key = d.tip ? d.tip.toString() : "";
1406
+ if (!first && key === (l.dispatchTip ?? ""))
1407
+ return;
1408
+ l.dispatchTip = key;
1409
+ l.rows = d.rows;
1410
+ this.followHeaders(l.row.handle, d.rows);
1411
+ // #148: an uninstalled app's event streams close.
1412
+ if (this.events.open(l.row.handle))
1413
+ this.events.keep(l.row.handle, appsOf(d.rows));
1414
+ // #126: an uninstalled app's beacons stop — an app with no row left in the table (an uninstall
1415
+ // removes every row naming the app and leaves its heads).
1416
+ if (l.beacons.length || l.live.length) {
1417
+ const apps = appsOf(d.rows);
1418
+ l.beacons = l.beacons.filter((b) => apps.has(b.app));
1419
+ l.live = l.live.filter((x) => apps.has(x.app)); // #138: and its liveness
1420
+ }
1421
+ await this.declareP2P(l, first);
1422
+ }
1423
+ /** The instance's libp2p node declared as its genesis, its table and its apps' subscriptions (#119) ask. */
1424
+ async declareP2P(l, first = false) {
1425
+ if (!this.p2p)
1426
+ return;
1427
+ const rows = (l.rows ?? []);
1428
+ await this.p2p.declare(l.row.handle, libp2pConfig(l.genesis, rows, subscribedTopics(l.subs), l.beacons.length > 0 || l.live.length > 0)).catch((e) => this.say(l.row.handle, `libp2p: ${first ? "not started" : "not reconfigured"}: ${e.message}`));
1429
+ this.p2p.beacons(l.row.handle, l.beacons);
1430
+ this.p2p.liveness(l.row.handle, l.live);
1431
+ }
1432
+ /** #138: the apps' liveness, folded from the instance's log (liveness.ts livenessOf), the store file read-only. */
1433
+ async readLiveness(store) {
1434
+ if (!this.p2p || !storeExists(store))
1435
+ return [];
1436
+ const s = openStoreFile(store, { readOnly: true });
1437
+ try {
1438
+ return await livenessOf(s);
1439
+ }
1440
+ finally {
1441
+ s.close();
1442
+ }
1443
+ }
1444
+ /** #126: the apps' beacons, folded from the instance's log (p2p.ts beaconsOf), the store file read-only. */
1445
+ async foldBeacons(store) {
1446
+ if (!this.p2p || !storeExists(store))
1447
+ return [];
1448
+ const s = openStoreFile(store, { readOnly: true });
1449
+ try {
1450
+ return await beaconsOf(s);
1451
+ }
1452
+ finally {
1453
+ s.close();
1454
+ }
1455
+ }
1456
+ /**
1457
+ * #119: the apps' subscriptions, folded from the instance's log — the
1458
+ * libp2p node's recovery at hydrate, by reading (p2p.ts subscriptionsOf;
1459
+ * the store file opened read-only beside the kernel, as `subscribes` reads it).
1460
+ */
1461
+ async foldSubscriptions(store) {
1462
+ if (!this.p2p || !storeExists(store))
1463
+ return [];
1464
+ const s = openStoreFile(store, { readOnly: true });
1465
+ try {
1466
+ return await subscriptionsOf(s);
1467
+ }
1468
+ finally {
1469
+ s.close();
1470
+ }
1471
+ }
1472
+ /**
1473
+ * A `subscribe` / `unsubscribe` event from `handle` (#119, after its step's
1474
+ * commit; the kernel checked it as it was emitted): the subscriptions
1475
+ * change and the node is declared again — the topic subscribed, or left
1476
+ * when no subscription, row or genesis topic takes it any more. Which app's
1477
+ * program runs for a message on it is the kernel's (the subscription). An
1478
+ * instance still loading reads the event from its log at hydrate; it is
1479
+ * applied after that too (the same result).
1480
+ */
1481
+ topicEvent(handle, rec) {
1482
+ if (rec.event === "beacon" || rec.event === "unbeacon")
1483
+ return this.beaconEvent(handle, rec);
1484
+ // An overlay before skein-overlay 0.20.0 emits want / unwant: nothing asks for them any more (the settled sync design).
1485
+ if (rec.event === "want" || rec.event === "unwant")
1486
+ return;
1487
+ if (rec.event === "liveness" || rec.event === "unliveness")
1488
+ return this.livenessEvent(handle, rec);
1489
+ const e = subscriptionEvent(rec);
1490
+ if ("refused" in e) {
1491
+ this.say(handle, `libp2p: ${String(rec.event)}: ${e.refused}`);
1492
+ return;
1493
+ }
1494
+ const topic = e.event === "subscribe" ? e.sub.topic : e.topic;
1495
+ const app = e.event === "subscribe" ? e.sub.app : e.app;
1496
+ const apply = async () => {
1497
+ const l = this.loaded.get(handle) ?? await this.loading.get(handle)?.catch(() => undefined);
1498
+ if (!l || l.kernel.gone)
1499
+ return;
1500
+ foldSubscriptions([e], l.subs);
1501
+ this.say(handle, `libp2p: ${e.event} ${topic} (app ${app}${e.event === "subscribe" ? ` → ${e.sub.program}.${e.sub.fn}` : ""})`);
1502
+ await this.declareP2P(l);
1503
+ };
1504
+ const p = apply().catch((x) => this.say(handle, `libp2p: ${e.event} ${topic}: ${x.message}`));
1505
+ this.syncing.add(p);
1506
+ void p.finally(() => this.syncing.delete(p));
1507
+ }
1508
+ /**
1509
+ * A `liveness` / `unliveness` event from `handle` (#138, after its step's commit; the kernel
1510
+ * checked its shape as it was emitted): the liveness changes and the node's liveness tool follows
1511
+ * it (P2PHost.liveness) — the topic subscribed without admitting its messages, or left. An instance
1512
+ * still loading reads it from its log.
1513
+ */
1514
+ livenessEvent(handle, rec) {
1515
+ const e = livenessEvent(rec);
1516
+ if ("refused" in e) {
1517
+ this.say(handle, `libp2p: ${String(rec.event)}: ${e.refused}`);
1518
+ return;
1519
+ }
1520
+ const apply = async () => {
1521
+ const l = this.loaded.get(handle) ?? await this.loading.get(handle)?.catch(() => undefined);
1522
+ if (!l || l.kernel.gone)
1523
+ return;
1524
+ foldLiveness([e], l.live);
1525
+ await this.declareP2P(l);
1526
+ };
1527
+ const p = apply().catch((x) => this.say(handle, `libp2p: ${e.event}: ${x.message}`));
1528
+ this.syncing.add(p);
1529
+ void p.finally(() => this.syncing.delete(p));
1530
+ }
1531
+ /**
1532
+ * GET /<app>/.live/<topic> at an instance's origin (#138): the liveness tool's set for the app's
1533
+ * liveness on the topic, served by the host — no program, no entry, nothing logged; metered as a
1534
+ * read (bytes served). 404 when the app keeps no liveness for the topic (or the host has no libp2p).
1535
+ */
1536
+ async liveRead(handle, route, app, topic) {
1537
+ const none = () => json(404, { status: "error", code: "ERR_NOT_FOUND", description: `app ${app} keeps no liveness for ${topic}` });
1538
+ if (!this.p2p)
1539
+ return none();
1540
+ try {
1541
+ await this.hydrate(handle);
1542
+ }
1543
+ catch (e) {
1544
+ return json(503, { status: "error", code: "ERR_UNAVAILABLE", description: e.message });
1545
+ }
1546
+ const closed = this.closed(handle);
1547
+ if (closed)
1548
+ return json(402, { status: "error", code: "ERR_PAYMENT_REQUIRED", description: closed });
1549
+ const beats = this.p2p.live.read(handle, app, topic);
1550
+ if (!beats)
1551
+ return none();
1552
+ const r = json(200, beats.map((b) => ({ sender: b.sender, at: b.at, body: Buffer.from(b.body).toString("base64"), from: b.from })));
1553
+ this.meter(handle, { at: stampMs(this.now()), op: `GET ${route}`, fuel: 0, bytes: r.body.length });
1554
+ return r;
1555
+ }
1556
+ /**
1557
+ * GET /<app>/.events at an instance's origin (#148, events.ts): the app's emitted events of the
1558
+ * asked name and topics as a Server-Sent Events stream — the backlog folded from the log, then each
1559
+ * as it is handed over — held open until the client goes, the app is uninstalled or the instance
1560
+ * stops. The host's own (no program, no entry, nothing logged); each message metered as a read.
1561
+ */
1562
+ async eventStream(handle, route, app, url, req, res, cors) {
1563
+ const answer = (r) => { if (!res.headersSent) {
1564
+ res.writeHead(r.status, { ...cors, ...r.headers, "content-length": String(r.body.length) });
1565
+ res.end(r.body);
1566
+ } };
1567
+ try {
1568
+ const last = req.headers["last-event-id"];
1569
+ const q = eventsQuery(app, url.searchParams, Array.isArray(last) ? last[0] : last);
1570
+ if ("problem" in q)
1571
+ return answer(json(400, { status: "error", code: "ERR_BAD_REQUEST", description: q.problem }));
1572
+ if (this.stopped)
1573
+ return answer(json(503, { status: "error", code: "ERR_UNAVAILABLE", description: "the host is shutting down" }));
1574
+ let l;
1575
+ try {
1576
+ l = await this.hydrate(handle);
1577
+ }
1578
+ catch (e) {
1579
+ return answer(json(503, { status: "error", code: "ERR_UNAVAILABLE", description: e.message }));
1580
+ }
1581
+ const closed = this.closed(handle);
1582
+ if (closed)
1583
+ return answer(json(402, { status: "error", code: "ERR_PAYMENT_REQUIRED", description: closed }));
1584
+ if (!appsOf((await l.kernel.dispatch()).rows).has(app))
1585
+ return answer(json(404, { status: "error", code: "ERR_NOT_FOUND", description: `no app ${app} installed here` }));
1586
+ res.writeHead(200, { ...cors, "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
1587
+ res.flushHeaders();
1588
+ let forget;
1589
+ let gone = false;
1590
+ res.on("close", () => { gone = true; forget?.(); });
1591
+ const send = (d) => {
1592
+ const m = sseMessage(d);
1593
+ res.write(m);
1594
+ this.meter(handle, { at: stampMs(this.now()), op: `GET ${route}`, fuel: 0, bytes: Buffer.byteLength(m) });
1595
+ };
1596
+ forget = await this.events.subscribe(handle, q, send, () => res.end());
1597
+ if (gone)
1598
+ forget();
1599
+ }
1600
+ catch (e) {
1601
+ this.say(handle, `events: ${e.message}`);
1602
+ if (res.headersSent)
1603
+ res.end();
1604
+ else
1605
+ answer(json(500, { status: "error", code: "ERR_INTERNAL", description: e.message }));
1606
+ }
1607
+ }
1608
+ /** How long a peer that beat on a topic is remembered without beating again (ms): 30 days (David to review). */
1609
+ static PEER_KEEP_MS = 30 * 86_400_000;
1610
+ /**
1611
+ * A beat kept from `peer` on `liveTopic` at `handle` (the settled sync design): when an overlay app
1612
+ * of the instance beats on that topic, the peer and its addresses (the node's peer store) upserted
1613
+ * into host.db `topic_peers`, keyed by the overlay topic. What a restart asks first.
1614
+ */
1615
+ async rememberPeer(handle, liveTopic, peer) {
1616
+ if (!liveTopic.endsWith("-live") || !this.p2p)
1617
+ return;
1618
+ if (!this.loaded.get(handle)?.beacons.some((b) => b.topic === liveTopic))
1619
+ return;
1620
+ try {
1621
+ this.o.db.seenPeer(handle, liveTopic.slice(0, -"-live".length), peer, await this.p2p.peerAddrs(handle, peer), stampMs(this.now()));
1622
+ }
1623
+ catch (e) {
1624
+ this.say(handle, `sync: peer ${peer} not remembered: ${e.message}`);
1625
+ }
1626
+ }
1627
+ /**
1628
+ * At a start (hydrate): for each overlay topic the instance beats on, the peers host.db remembers
1629
+ * (rows older than PEER_KEEP_MS pruned first) dialled, a few at a time, failures dropped silently;
1630
+ * each that answers asked at once — the locator and the tail (sync.ts `reconnect`) — without waiting
1631
+ * for a beat.
1632
+ */
1633
+ async askKnownPeers(l) {
1634
+ const handle = l.row.handle;
1635
+ if (!this.p2p || !this.sync)
1636
+ return;
1637
+ this.o.db.prunePeers(stampMs(this.now()) - Router.PEER_KEEP_MS);
1638
+ const jobs = [];
1639
+ for (const b of l.beacons) {
1640
+ if (!b.topic.endsWith("-live"))
1641
+ continue;
1642
+ const topic = b.topic.slice(0, -"-live".length);
1643
+ for (const r of this.o.db.topicPeers(handle, topic))
1644
+ if (r.peer !== this.p2p.peerId(handle))
1645
+ jobs.push({ app: b.app, topic, peer: r.peer, addrs: r.addrs });
1646
+ }
1647
+ const p2p = this.p2p, sync = this.sync;
1648
+ const one = async (j) => {
1649
+ try {
1650
+ await p2p.connect(handle, j.peer, j.addrs);
1651
+ }
1652
+ catch {
1653
+ return;
1654
+ } // a peer gone: dropped silently
1655
+ sync.reconnect(handle, j.app, j.topic, j.peer);
1656
+ };
1657
+ for (let i = 0; i < jobs.length; i += 4)
1658
+ await Promise.all(jobs.slice(i, i + 4).map(one));
1659
+ }
1660
+ /**
1661
+ * shruggr/skein#154: our own overlay's read, for the sync's puller (its locator and what it lacks) —
1662
+ * the engine's function `fn` (`anchors`, `admitted`) by a kernel call (the `call` op; nothing
1663
+ * appended, nothing logged), not through the app's http table, which an app need not publish. The
1664
+ * program is the app record's engine (its role `overlay`); the argument is what a read route's
1665
+ * filter is handed (docs/APPS.md "Filters": the route handler contract's fields), `match` naming the
1666
+ * app, so the engine reads its configuration from the app record as it does for its read routes.
1667
+ * Its answer is the read route's `{status, type, body}`: the body's JSON, or a throw on another status.
1668
+ */
1669
+ async overlayRead(handle, app, fn, query) {
1670
+ const l = await this.hydrate(handle);
1671
+ const root = await l.kernel.call("head", appHead(app));
1672
+ if (!root)
1673
+ throw new Error(`${app}: no app record`);
1674
+ const engine = (await l.kernel.store.get(root)).programs?.[OVERLAY_ROLE];
1675
+ if (!(engine instanceof CID))
1676
+ throw new Error(`${app}: its app record has no engine (role ${OVERLAY_ROLE})`);
1677
+ const qs = new URLSearchParams(query).toString();
1678
+ const route = `/${app}/${fn}`;
1679
+ const http = { method: "GET", path: route, route, query: qs ? `?${qs}` : "", headers: {}, body: new Uint8Array() };
1680
+ const arg = { transport: "http", request: { kind: "http", ...http }, match: { transport: "http", address: route, app }, ...http, contentType: "" };
1681
+ const c = await l.kernel.invoke(engine, fn, dagCbor.encode(arg), { now: stampMs(this.now()) });
1682
+ if (!c.ok)
1683
+ throw new Error(`${app}.${fn}: ${c.error}`);
1684
+ const a = dagCbor.decode(c.result);
1685
+ const text = a.body instanceof Uint8Array ? Buffer.from(a.body).toString("utf8") : "";
1686
+ if (a.status !== 200)
1687
+ throw new Error(`${app}.${fn}: ${a.status} ${text.slice(0, 200)}`);
1688
+ return JSON.parse(text);
1689
+ }
1690
+ /**
1691
+ * A `beacon` / `unbeacon` event from `handle` (#126, after its step's commit;
1692
+ * the kernel checked its shape as it was emitted): the beacons change and
1693
+ * the node beats them (P2PHost.beacons) — started if a beacon needs it; the
1694
+ * topic is not subscribed. An instance still loading reads it from its log.
1695
+ */
1696
+ beaconEvent(handle, rec) {
1697
+ const e = beaconEvent(rec);
1698
+ if ("refused" in e) {
1699
+ this.say(handle, `libp2p: ${String(rec.event)}: ${e.refused}`);
1700
+ return;
1701
+ }
1702
+ const apply = async () => {
1703
+ const l = this.loaded.get(handle) ?? await this.loading.get(handle)?.catch(() => undefined);
1704
+ if (!l || l.kernel.gone)
1705
+ return;
1706
+ foldBeacons([e], l.beacons);
1707
+ await this.declareP2P(l);
1708
+ };
1709
+ const p = apply().catch((x) => this.say(handle, `libp2p: ${e.event}: ${x.message}`));
1710
+ this.syncing.add(p);
1711
+ void p.finally(() => this.syncing.delete(p));
1712
+ }
1713
+ /**
1714
+ * The host's headers feed (#102) for an instance whose dispatch table is
1715
+ * `rows`: subscribed (box `chain`) while it is enabled and a row takes
1716
+ * events in box `chain` by name (the chain app's `event` row; dispatch.zig
1717
+ * forEvent) — a catch-all `*` row (a mailbox instance's) is not one —
1718
+ * else not. One line when that changes.
1719
+ */
1720
+ followHeaders(handle, rows) {
1721
+ const url = this.o.headersFeed;
1722
+ if (!url)
1723
+ return;
1724
+ const want = this.o.db.get(handle)?.status === "enabled" && rows.some((r) => r.address === DEFAULT_BOX && takesEvent(r, DEFAULT_BOX));
1725
+ if (want === this.feeds.hosts(handle))
1726
+ return;
1727
+ // #141: on subscribing, the headers it lacks first — from its tip (its chain state's, else its image's) to the host's.
1728
+ const backfill = async () => {
1729
+ const row = this.o.db.get(handle);
1730
+ if (!row || !storeExists(storeSpec(row)))
1731
+ return [];
1732
+ const s = openStoreFile(storeSpec(row), { readOnly: true });
1733
+ let tip;
1734
+ try {
1735
+ tip = await skeinTip(s);
1736
+ }
1737
+ finally {
1738
+ s.close();
1739
+ }
1740
+ return await this.image.after(tip);
1741
+ };
1742
+ void this.feeds.host(handle, want ? { kind: "headers", url, box: DEFAULT_BOX } : undefined, want ? backfill : undefined);
1743
+ this.say("router", `${handle}: ${want ? "subscribed to" : "unsubscribed from"} the host's headers feed ${url}`);
1744
+ // #156: an app that takes box `chain` installed into a running instance: the host chain as it stands, now.
1745
+ if (want)
1746
+ void this.chainBirth(handle).catch((e) => this.say(handle, `host chain: the install event not sent: ${e.message}`));
1747
+ }
1748
+ /** Follow the host's headers feed for an instance whose status changed (#102): its table read again. */
1749
+ async refollowHeaders(handle) {
1750
+ if (!this.o.headersFeed)
1751
+ return;
1752
+ const l = this.loaded.get(handle);
1753
+ const d = l && !l.kernel.gone ? await l.kernel.dispatch().catch((e) => { this.say(handle, `the dispatch table not read: ${e.message}`); return undefined; }) : undefined;
1754
+ this.followHeaders(handle, d?.rows ?? []);
1755
+ }
1756
+ /**
1757
+ * The waker answers every wake-me due by this router's clock, and the cron
1758
+ * provider sends every tick due (#69) — what their timers do in real time,
1759
+ * for a clock that is not (a script clock: tests, the corpus).
1760
+ */
1761
+ async wake() {
1762
+ this.providers.wakeDue();
1763
+ await this.cron.tick();
1764
+ await this.providers.idle();
1765
+ }
1766
+ /** An instance's next wake (ms), if any (the host page): the waker's earliest, or its next tick (#69). */
1767
+ nextWake(handle) {
1768
+ const xs = [this.providers.nextWake(handle), ...this.cron.of(handle).map((s) => s.next)].filter((x) => x !== undefined);
1769
+ return xs.length ? Math.min(...xs) : undefined;
1770
+ }
1771
+ /** Stop kernels idle for `ms` with nothing queued. Their deadlines and sleeps stay with the waker, their schedules with the cron provider. */
1772
+ async reap(ms) {
1773
+ for (const [handle, l] of this.loaded) {
1774
+ if (l.kernel.busy > 0 || this.queues.has(handle) || Date.now() - l.kernel.last < ms)
1775
+ continue;
1776
+ this.loaded.delete(handle);
1777
+ this.say("router", `${handle}: idle, stopped`);
1778
+ await l.kernel.stop();
1779
+ }
1780
+ }
1781
+ // ---------------------------------------------------------------- the fuel ledger
1782
+ charge(instance, caller, op, fuel) {
1783
+ const k = `${instance}\0${caller}\0${op}`;
1784
+ const x = this.owed.get(k) ?? { calls: 0, fuel: 0 };
1785
+ x.calls += 1;
1786
+ x.fuel += fuel;
1787
+ this.owed.set(k, x);
1788
+ }
1789
+ /** Write what calls cost since the last flush (host.db, never the log). */
1790
+ flushLedger() {
1791
+ if (!this.owed.size)
1792
+ return;
1793
+ const rows = [...this.owed].map(([k, v]) => { const [instance, caller, op] = k.split("\0"); return { instance, caller, op, ...v }; });
1794
+ this.owed.clear();
1795
+ try {
1796
+ this.o.db.charge(rows);
1797
+ }
1798
+ catch (e) {
1799
+ this.say("router", `ledger: ${e.message}`);
1800
+ }
1801
+ }
1802
+ // ---------------------------------------------------------------- billing (#130, billing.ts)
1803
+ /** Why this host forwards nothing to `handle` (undefined: it forwards as usual). */
1804
+ closed(handle) {
1805
+ return closedBy(this.views.get(handle));
1806
+ }
1807
+ /** A billed instance as this host last read it (#130). */
1808
+ billingView(handle) { return this.views.get(handle); }
1809
+ /** A line of this host's log for `handle`'s period (#130): a request it served, a read call it made. */
1810
+ meter(handle, line) {
1811
+ if (!this.o.billing || !this.views.get(handle)?.terms)
1812
+ return;
1813
+ let p = this.periods.get(handle);
1814
+ if (!p)
1815
+ this.periods.set(handle, (p = new Period(line.at)));
1816
+ p.add(line);
1817
+ }
1818
+ /**
1819
+ * Read the instance's billing (#130): its host row's terms (the dispatch table) and its kernel's
1820
+ * billing state (the head `billing`), checked against what this host supports. A skein with a host
1821
+ * row this host has not ticked yet is ticked now; one whose unreported amounts would take its tally
1822
+ * to its allocation, early. When it went asleep (and woke) goes to host.db (the grace).
1823
+ */
1824
+ async syncBilling(l) {
1825
+ const cfg = this.o.billing;
1826
+ if (!cfg || l.kernel.gone)
1827
+ return;
1828
+ const handle = l.row.handle;
1829
+ // The table as syncDispatch just read it (it follows the table when the host has libp2p or a headers feed), else read now.
1830
+ const rows = (this.p2p || this.o.headersFeed) && l.rows ? l.rows : (await l.kernel.dispatch()).rows;
1831
+ const terms = termsOf(rows);
1832
+ if (!terms) {
1833
+ if (this.views.has(handle)) {
1834
+ this.views.delete(handle);
1835
+ this.o.db.forgetBilling(handle);
1836
+ this.say(handle, "billing: no host row: not billed");
1837
+ }
1838
+ return;
1839
+ }
1840
+ const root = await l.kernel.call("head", BILLING_HEAD);
1841
+ const state = root ? stateOf(await l.kernel.store.get(root)) : undefined;
1842
+ const why = mismatch(cfg, this.providers.key("billing"), terms);
1843
+ const was = this.views.get(handle);
1844
+ const view = { terms, ...(state ? { state } : {}), ...(why ? { mismatch: why } : {}) };
1845
+ this.views.set(handle, view);
1846
+ const mine = state && state.host === terms.host ? state : undefined;
1847
+ this.o.db.setBilling(handle, { host: terms.host, ...(mine ? { tally: mine.tally, allocation: allocation(mine) } : {}), asleep: !!mine?.asleep, ...(why ? { mismatch: why } : {}) }, stampMs(this.now()));
1848
+ const closed = closedBy(view);
1849
+ if (closed !== closedBy(was))
1850
+ this.say(handle, closed ? `billing: closed — ${closed}` : "billing: open");
1851
+ if (why || mine?.asleep)
1852
+ return;
1853
+ if (mine)
1854
+ this.firstTicks.delete(handle);
1855
+ else if (this.firstTicks.has(handle))
1856
+ return; // sent: the timer sends it again
1857
+ else
1858
+ this.firstTicks.add(handle);
1859
+ const p = this.periods.get(handle);
1860
+ const owed = p ? priceHost(terms.rates, p.fuel, p.served) : 0n;
1861
+ if (!mine || (p && !p.empty && mine.tally + owed >= allocation(mine)))
1862
+ void this.billingTick(handle).catch((e) => this.say(handle, `billing: tick: ${e.message}`));
1863
+ }
1864
+ /** Tick every billed instance this router holds (the timer, every tickMs). */
1865
+ async tickAll() {
1866
+ for (const [handle, l] of this.loaded) {
1867
+ if (l.kernel.gone || !this.views.get(handle)?.terms)
1868
+ continue;
1869
+ await this.billingTick(handle).catch((e) => this.say(handle, `billing: tick: ${e.message}`));
1870
+ }
1871
+ }
1872
+ /**
1873
+ * The host's tick to `handle` (#130 decided 4): signed by its billing key, in the host row's box —
1874
+ * {kind: "tick", at, allowance, fuel, served, log} — the period since its last tick closed, its log
1875
+ * record kept (host.db billing_ticks: `log` is that record's CID). Not to an instance the gate
1876
+ * holds (asleep, other terms), nor twice at once. The tick's entry, or undefined.
1877
+ */
1878
+ async billingTick(handle) {
1879
+ const cfg = this.o.billing;
1880
+ const v = this.views.get(handle);
1881
+ if (!cfg || !v?.terms || this.closed(handle) || this.ticking.has(handle))
1882
+ return undefined;
1883
+ const l = this.loaded.get(handle);
1884
+ if (!l || l.kernel.gone)
1885
+ return undefined;
1886
+ this.ticking.add(handle);
1887
+ try {
1888
+ const at = stampMs(this.now());
1889
+ const p = this.periods.get(handle) ?? new Period(at);
1890
+ this.periods.set(handle, new Period(at));
1891
+ const log = periodRecord(l.identity, p, at);
1892
+ this.o.db.billingTick(handle, at, log.cid.toString(), log.bytes);
1893
+ return await this.providers.send(handle, "billing", keyBytes(l.identity), v.terms.address, tickBody(at, cfg, p, log.cid));
1894
+ }
1895
+ finally {
1896
+ this.ticking.delete(handle);
1897
+ }
1898
+ }
1899
+ /**
1900
+ * A `payment` event from `handle` (#130 decided 6: its pay step's, the kernel lets no other step
1901
+ * emit one): this host is the payee. Its wiring: the payment kept (host.db billing_payments, with
1902
+ * whether its output pays the key the BRC-29 remittance derives from this host's billing key) and
1903
+ * broadcast (its Arcade, once).
1904
+ */
1905
+ payment(handle, rec) {
1906
+ const p = (async () => {
1907
+ const beef = rec.tx instanceof Uint8Array ? rec.tx : undefined;
1908
+ const amount = typeof rec.amount === "number" ? rec.amount : undefined;
1909
+ const vout = typeof rec.outputIndex === "number" ? rec.outputIndex : undefined;
1910
+ const rem = rec.remittance;
1911
+ if (!beef || amount === undefined || vout === undefined || !rem) {
1912
+ this.say(handle, "billing: a payment event without its transaction, amount or remittance: ignored");
1913
+ return;
1914
+ }
1915
+ const tx = Transaction.fromAtomicBEEF([...beef]);
1916
+ let ours = false;
1917
+ try {
1918
+ const key = new KeyDeriver(this.providerKey("billing")).derivePrivateKey([2, "3241645161d8"], `${rem.derivationPrefix} ${rem.derivationSuffix}`, String(rem.senderIdentityKey));
1919
+ const out = tx.outputs[vout];
1920
+ ours = !!out && out.satoshis === amount && out.lockingScript.toHex() === new P2PKH().lock(key.toPublicKey().toHash()).toHex();
1921
+ }
1922
+ catch { /* a remittance that derives nothing: kept, not ours */ }
1923
+ const checkpoint = CID.asCID(rec.checkpoint)?.toString();
1924
+ const txid = tx.id("hex");
1925
+ const fresh = this.o.db.billingPayment({ instance: handle, txid, amount, at: stampMs(this.now()), beef, remittance: rem, ours, ...(checkpoint ? { checkpoint } : {}) });
1926
+ if (fresh && this.arc)
1927
+ this.arc.enqueue(handle, Uint8Array.from(tx.toBinary()), beef);
1928
+ this.say(handle, `billing: payment ${txid.slice(0, 8)}… ${amount} sats${ours ? "" : " (its output does not pay this host's derived key)"}${checkpoint ? `, checkpoint ${checkpoint.slice(-8)}` : ""}`);
1929
+ })().catch((e) => this.say(handle, `billing: payment: ${e.message}`));
1930
+ this.syncing.add(p);
1931
+ void p.finally(() => this.syncing.delete(p));
1932
+ }
1933
+ /**
1934
+ * Reclaim an instance (#130 decided 9; `skein-host reclaim`): its kernel stopped, its row and its
1935
+ * billing removed, its store deleted. The host's call to make, after the grace; nothing here makes
1936
+ * it on its own. Not the host skein.
1937
+ */
1938
+ async reclaim(handle) {
1939
+ const row = this.o.db.get(handle);
1940
+ if (!row)
1941
+ throw new Error(`no instance ${handle}`);
1942
+ if (row.handle === this.o.db.hostSkein()?.handle)
1943
+ throw new Error(`${handle} is the host skein`);
1944
+ this.o.db.setStatus(handle, "disabled");
1945
+ await this.refollowHeaders(handle);
1946
+ const l = this.loaded.get(handle);
1947
+ if (l) {
1948
+ this.loaded.delete(handle);
1949
+ await l.kernel.stop();
1950
+ }
1951
+ await this.p2p?.declare(handle, undefined).catch(() => { });
1952
+ this.events.drop(handle);
1953
+ this.cron.forget(handle);
1954
+ this.views.delete(handle);
1955
+ this.periods.delete(handle);
1956
+ this.o.db.forgetBilling(handle);
1957
+ this.o.db.remove(handle);
1958
+ if (isSet(row.store)) {
1959
+ // #96, #158: the host never writes an LMDB environment or a RocksDB database; the set stays in it (its handle cannot be created again here).
1960
+ this.say("router", `${handle}: reclaimed (its set in ${row.store} left in place: the host writes no instance store)`);
1961
+ return;
1962
+ }
1963
+ await removeStore(row.store);
1964
+ this.say("router", `${handle}: reclaimed (its store ${row.store} deleted)`);
1965
+ }
1966
+ // ---------------------------------------------------------------- HTTP
1967
+ /** Whether a URL is this host's own (answered in process: no DNS, no socket). */
1968
+ isLocal(url) {
1969
+ let u;
1970
+ try {
1971
+ u = new URL(url);
1972
+ }
1973
+ catch {
1974
+ return false;
1975
+ } // a URL that does not parse is not this host's (the caller's fetch reports it)
1976
+ if (!this.port)
1977
+ return false;
1978
+ const mine = (o) => { try {
1979
+ return new URL(o).host === u.host;
1980
+ }
1981
+ catch {
1982
+ return false;
1983
+ } }; // an origin setting that is no URL matches nothing
1984
+ if (mine(this.origin()))
1985
+ return true;
1986
+ const port = u.port || (u.protocol === "https:" ? "443" : "80");
1987
+ if (port !== String(this.port))
1988
+ return false;
1989
+ const h = u.hostname.toLowerCase();
1990
+ return h === "127.0.0.1" || h === "localhost" || h === "[::1]" || h === "::1" || h.endsWith(".localhost");
1991
+ }
1992
+ /** Which instance a URL is for: `/@<handle>` (stripped for the routes), or a `<handle>.` host name. */
1993
+ target(url) {
1994
+ const enabled = (h) => this.o.db.get(h)?.status === "enabled";
1995
+ const m = /^\/@([^/]+)(\/.*)?$/.exec(url.pathname);
1996
+ if (m) {
1997
+ const h = decodeURIComponent(m[1]);
1998
+ return enabled(h) ? { handle: h, route: m[2] ?? "/" } : undefined;
1999
+ }
2000
+ const name = url.hostname.toLowerCase();
2001
+ const label = name.includes(".") ? name.split(".")[0] : "";
2002
+ if (label && HANDLE.test(label) && (label !== this.ownLabel() || this.ownLabelInstance(label)) && enabled(label))
2003
+ return { handle: label, route: url.pathname }; // #161: a path-only handle never by hostname
2004
+ return undefined;
2005
+ }
2006
+ /**
2007
+ * The HTTP proxy's request (#70, the `fetch` provider): this host's own URLs in
2008
+ * process, the rest as configured. A delivery (`POST …/sendMessage`, from
2009
+ * instance `from`) is logged in one line: where it went, how, and the result.
2010
+ */
2011
+ async http(req, from) {
2012
+ const local = this.isLocal(req.url);
2013
+ const delivery = req.method === "POST" && /\/sendMessage$/.test(new URL(req.url, "http://x").pathname);
2014
+ const said = (what) => { if (delivery)
2015
+ this.say(from ?? "router", `deliver${((d) => d ? ` ${d}` : "")(describeDelivery(req))} → ${req.url} (${local ? "local" : "remote"}): ${what}`); };
2016
+ try {
2017
+ let r;
2018
+ if (local) {
2019
+ const d = await this.dispatch({ method: req.method, url: req.url, headers: headerMap(req.headers ?? {}), body: req.body ?? new Uint8Array(), ...(from ? { from } : {}) });
2020
+ r = { status: d.status, headers: d.headers, body: d.body };
2021
+ }
2022
+ else {
2023
+ const f = this.o.http ?? (process.env.SKEIN_HTTP === "fetch" ? fetchHttp : undefined);
2024
+ if (!f)
2025
+ throw new Error(`this host answers no http beyond its own (${req.url})`);
2026
+ r = await f(req);
2027
+ }
2028
+ said(r.status === 200 ? "200 delivered" : `HTTP ${r.status} ${errorOf(r.body)}`.trimEnd());
2029
+ return r;
2030
+ }
2031
+ catch (e) {
2032
+ said(`failed: ${e.message}`);
2033
+ throw e;
2034
+ }
2035
+ }
2036
+ /** One request, whoever made it: a socket's, or an instance's own http. */
2037
+ async dispatch(req) {
2038
+ const url = new URL(req.url);
2039
+ if (req.method === "GET" && url.pathname === "/.well-known/skein-host") {
2040
+ // #130: the terms this host bills by — what an owner grants as the host row (`skein host`).
2041
+ const b = this.o.billing;
2042
+ const billing = b ? { key: this.providers.key("billing"), x: b.x, rates: b.rates, allowance: b.allowance, tickMs: b.tickMs, graceMs: b.graceMs } : undefined;
2043
+ return json(200, { origin: this.origin(), domain: await this.handleDomain(), ...(billing ? { billing } : {}) });
2044
+ }
2045
+ const t = this.target(url);
2046
+ // The router's own paths at its own hostname stay the router's when an instance takes that label (ownLabelInstance).
2047
+ if (t && !/^\/@/.test(url.pathname) && this.ownLabelInstance(t.handle)) {
2048
+ if (req.method === "GET" && url.pathname === "/skeins")
2049
+ return await this.skeinsHeldBy(url.searchParams.get("key") ?? "");
2050
+ if (url.pathname === `${ARC_ROUTE}/callback`)
2051
+ return await this.arcRequest(req, url.pathname);
2052
+ }
2053
+ // #161: an overlay request routed by its topic goes to the token instance it names, at an instance's
2054
+ // hostname too (one public name for the pages and the wallets' provider URL, 2026-10-28).
2055
+ if (req.method === "POST" && (url.pathname === "/submit" || url.pathname === "/lookup") && (!t || t.route === url.pathname)) {
2056
+ const o = this.overlayTarget(url.pathname, req);
2057
+ if ("problem" in o)
2058
+ return json(400, { status: "error", code: "ERR_BAD_REQUEST", description: o.problem });
2059
+ if (o.handle)
2060
+ return await this.held(this.forward(o.handle, url.pathname, url, req));
2061
+ }
2062
+ const serve = async (x) => {
2063
+ // #138: the liveness tool's read, the host's own (no program).
2064
+ const live = req.method === "GET" ? /^\/([^/]+)\/\.live\/([^/]+)$/.exec(x.route) : null;
2065
+ if (live)
2066
+ return await this.held(this.liveRead(x.handle, x.route, decodeURIComponent(live[1]), decodeURIComponent(live[2])));
2067
+ // #148: a stream, served on a socket only (handler).
2068
+ if (req.method === "GET" && EVENTS_ROUTE.test(x.route))
2069
+ return json(400, { status: "error", code: "ERR_BAD_REQUEST", description: "an event stream (text/event-stream) is served on a connection of its own, not in process" });
2070
+ return await this.held(this.forward(x.handle, x.route, url, req));
2071
+ };
2072
+ if (t)
2073
+ return await serve(t);
2074
+ const path = url.pathname;
2075
+ if (req.method === "GET" && path === "/skeins")
2076
+ return await this.skeinsHeldBy(url.searchParams.get("key") ?? "");
2077
+ // #113: the host's own origin's BRC-169 requests are the host skein's (its onboarding app's routes).
2078
+ const route = DISCOVERY[`${req.method} ${path}`] ?? (req.method === "GET" && path.startsWith(PAYMAIL_PREFIX) ? `/${ONBOARD_APP}${path}` : undefined);
2079
+ if (route) {
2080
+ const host = this.o.db.hostSkein();
2081
+ if (host?.status === "enabled")
2082
+ return await this.held(this.forward(host.handle, route, url, req));
2083
+ const f = await this.o.discovery?.(req, url);
2084
+ if (f)
2085
+ return f;
2086
+ return json(404, { status: "error", code: "ERR_NOT_FOUND", description: "this host has no host skein (skein-host init): no BRC-169 here" });
2087
+ }
2088
+ if (path === `${ARC_ROUTE}/callback`)
2089
+ return await this.arcRequest(req, path);
2090
+ // SKEIN_ROOT_INSTANCE: a hostname that is no instance's serves the root instance (the standalone's one app at localhost:<port>/),
2091
+ // after the host's own paths above.
2092
+ const root = this.o.rootInstance;
2093
+ if (root && this.o.db.get(root)?.status === "enabled")
2094
+ return await serve({ handle: root, route: path });
2095
+ return json(404, { status: "error", code: "ERR_NOT_FOUND", description: "no instance here: an instance is at http://<handle>.localhost:<port>/ or /@<handle>/" });
2096
+ }
2097
+ /**
2098
+ * #161, #137: an overlay request at the bare host — POST /submit (the topics its X-Topics names: a
2099
+ * comma list or a JSON array, as the overlay reads it) or POST /lookup (its JSON body's `service`,
2100
+ * and a `topic` beside it or in its query) — routed to the instance that serves the names:
2101
+ *
2102
+ * - a token's name, `tm_color_<id>` / `ls_color_<id>` whose `<id>` is an enabled path-only
2103
+ * instance here: that instance (#161, the handle naming convention; checked first);
2104
+ * - any other name (`tm_color`, `ls_color`, …): the enabled instance whose `topics` (the
2105
+ * names the instance manager's create or start named, host.db) contain it. Whether the instance
2106
+ * has run yet does not matter. Two instances taking the same name: a problem (400, "N instances
2107
+ * here take <name>").
2108
+ *
2109
+ * Every name that maps must map to the same instance (else a problem: 400); a name mapping to no
2110
+ * instance here beside one that does is a problem too. Nothing maps: no handle (the hostname's
2111
+ * instance, or the bare host, as before).
2112
+ */
2113
+ overlayTarget(path, req) {
2114
+ const names = [];
2115
+ if (path === "/submit") {
2116
+ const h = (req.headers["x-topics"] ?? "").trim();
2117
+ if (!h)
2118
+ return {};
2119
+ if (h.startsWith("[")) {
2120
+ let j;
2121
+ try {
2122
+ j = JSON.parse(h);
2123
+ }
2124
+ catch {
2125
+ return {};
2126
+ } // the overlay's own refusal, at no instance: the bare host's
2127
+ if (!Array.isArray(j))
2128
+ return {};
2129
+ for (const t of j)
2130
+ if (typeof t === "string")
2131
+ names.push(t);
2132
+ }
2133
+ else
2134
+ names.push(...h.split(",").map((x) => x.trim()).filter(Boolean));
2135
+ }
2136
+ else {
2137
+ let j;
2138
+ try {
2139
+ j = JSON.parse(new TextDecoder().decode(req.body));
2140
+ }
2141
+ catch {
2142
+ return {};
2143
+ }
2144
+ if (!j || typeof j !== "object")
2145
+ return {};
2146
+ const q = j.query;
2147
+ for (const t of [j.service, j.topic, q && typeof q === "object" ? q.topic : undefined])
2148
+ if (typeof t === "string")
2149
+ names.push(t);
2150
+ }
2151
+ const enabled = (h) => this.o.db.get(h)?.status === "enabled";
2152
+ const rows = this.o.db.list("enabled");
2153
+ const mapped = [];
2154
+ for (const name of names) {
2155
+ const id = /^(?:tm|ls)_color_(.+)$/.exec(name)?.[1];
2156
+ if (id && TOKEN_HANDLE.test(id) && enabled(id)) {
2157
+ mapped.push(id);
2158
+ continue;
2159
+ }
2160
+ const by = rows.filter((r) => topicsOf(r).includes(name)).map((r) => r.handle);
2161
+ if (by.length > 1)
2162
+ return { problem: `${by.length} instances here take ${name} (${by.join(", ")}): send it to one of them (/@<handle>/${path.slice(1)})` };
2163
+ mapped.push(by[0]);
2164
+ }
2165
+ const hit = [...new Set(mapped.filter((x) => !!x))];
2166
+ if (!hit.length)
2167
+ return {};
2168
+ if (hit.length > 1)
2169
+ return { problem: `the names map to ${hit.length} instances here (${hit.join(", ")}): one request goes to one; send one per instance` };
2170
+ const stray = names.filter((_, i) => !mapped[i]);
2171
+ if (stray.length)
2172
+ return { problem: `${stray.join(", ")}: no instance here takes ${stray.length > 1 ? "these" : "it"} beside ${hit[0]}: send one request per instance` };
2173
+ return { handle: hit[0] };
2174
+ }
2175
+ /**
2176
+ * GET /skeins?key=<hex> at the host's own origin: the skeins on this host whose head `grants`
2177
+ * (#143) names `key` among root's holders — each enabled agent row's store file read read-only,
2178
+ * no kernel — as JSON [{handle, identity, url}]. A key's skeins are the host's to say (no token
2179
+ * in a wallet tracks them). Unsigned, anyone; mailbox instances are not listed.
2180
+ */
2181
+ async skeinsHeldBy(key) {
2182
+ if (!/^0[23][0-9a-f]{64}$/.test(key))
2183
+ return json(400, { status: "error", code: "ERR_BAD_REQUEST", description: "key: an identity key (33 bytes, hex)" });
2184
+ const out = [];
2185
+ for (const row of this.o.db.list("enabled")) {
2186
+ if (row.kind === "mailbox" || !row.identity || !storeExists(storeSpec(row)))
2187
+ continue;
2188
+ let roots;
2189
+ const s = openStoreFile(storeSpec(row), { readOnly: true });
2190
+ try {
2191
+ roots = await rootOf(s);
2192
+ }
2193
+ catch {
2194
+ roots = [];
2195
+ }
2196
+ finally {
2197
+ s.close();
2198
+ }
2199
+ if (roots.includes(key))
2200
+ out.push({ handle: row.handle, identity: row.identity, url: this.originOf(row.handle) });
2201
+ }
2202
+ return json(200, out);
2203
+ }
2204
+ /** A forwarded request, held among the in-flight ones (shutdown answers them 503). */
2205
+ async held(p) {
2206
+ this.inflight.add(p);
2207
+ try {
2208
+ return await p;
2209
+ }
2210
+ finally {
2211
+ this.inflight.delete(p);
2212
+ }
2213
+ }
2214
+ /**
2215
+ * The handle domain (#113): the host skein's onboarding app's
2216
+ * `config.onboard.domain` (the app record, a kernel read), the one setting
2217
+ * every handle here is at. With no host skein or no app: this router's
2218
+ * origin's host name as a domain.
2219
+ */
2220
+ async handleDomain() {
2221
+ const fallback = this.domain();
2222
+ const host = this.o.db.hostSkein();
2223
+ if (host?.status !== "enabled")
2224
+ return fallback;
2225
+ try {
2226
+ const l = await this.hydrate(host.handle);
2227
+ const root = await l.kernel.call("head", `${ONBOARD_APP}/app`);
2228
+ if (!root)
2229
+ return fallback;
2230
+ const rec = await l.kernel.store.get(root);
2231
+ const d = rec.config?.[ONBOARD_APP]?.domain;
2232
+ return typeof d === "string" && d ? d.toLowerCase() : "localhost";
2233
+ }
2234
+ catch (e) {
2235
+ this.say("router", `the handle domain: not read from ${host.handle}: ${e.message}`);
2236
+ return fallback;
2237
+ }
2238
+ }
2239
+ /** Arcade's webhook (#58, arc.ts); 404 when the host has no Arcade. */
2240
+ async arcRequest(req, path) {
2241
+ if (!this.arc)
2242
+ return json(404, { status: "error", code: "ERR_NOT_FOUND", description: "this host has no Arcade" });
2243
+ if (req.method === "POST")
2244
+ return await this.arc.callback(req.headers, req.body);
2245
+ return json(404, { status: "error", code: "ERR_NOT_FOUND", description: `no ${req.method} ${path}` });
2246
+ }
2247
+ /**
2248
+ * A request for an instance (#68, #66): appended as received, and the
2249
+ * client's connection held until the thread it launched comes to rest —
2250
+ * then its answer, signed on the session inside. Past `answerWaitMs`, or
2251
+ * when the router shuts down, 503 + Retry-After (the thread goes on). A
2252
+ * read's call fuel (the explorer's) is charged to the ledger.
2253
+ */
2254
+ async forward(handle, route, url, req) {
2255
+ if (this.stopped)
2256
+ return unavailable("the host is shutting down");
2257
+ let l;
2258
+ try {
2259
+ l = await this.hydrate(handle);
2260
+ }
2261
+ catch (e) {
2262
+ return json(503, { status: "error", code: "ERR_UNAVAILABLE", description: e.message });
2263
+ }
2264
+ // #130: asleep, or terms this host does not serve: nothing is forwarded.
2265
+ const closed = this.closed(handle);
2266
+ if (closed)
2267
+ return json(402, { status: "error", code: "ERR_PAYMENT_REQUIRED", description: closed });
2268
+ // #135: two doors — a read by a call, a signed request through the door (serveHttp).
2269
+ const a = await serveHttp(l.kernel, { method: req.method, path: url.pathname, route, query: url.search, headers: req.headers, body: req.body }, { now: this.now(), waitMs: this.o.answerWaitMs, stop: this.stopping });
2270
+ this.settle(l);
2271
+ // Charged to the identity the front door verified (H13), never to a header as the client sent it.
2272
+ if (a.fuel !== undefined)
2273
+ this.charge(handle, a.caller ?? "", `${route} (read)`, a.fuel);
2274
+ // #130: what never reaches the log — the read call's fuel, the bytes served — for its next tick.
2275
+ this.meter(handle, { at: stampMs(this.now()), op: `${req.method} ${route}`, ...(a.caller ? { caller: a.caller } : {}), fuel: a.fuel ?? 0, bytes: a.body.length });
2276
+ return { status: a.status, headers: a.headers, body: a.body };
2277
+ }
2278
+ /** The HTTP handler: CORS, then dispatch. */
2279
+ handler() {
2280
+ return (req, res) => {
2281
+ const cors = {
2282
+ "access-control-allow-origin": "*", "access-control-allow-headers": "*", "access-control-allow-methods": "*", "access-control-expose-headers": "*",
2283
+ };
2284
+ if (req.method === "OPTIONS") {
2285
+ res.writeHead(200, cors).end();
2286
+ return;
2287
+ }
2288
+ // #148: an event stream is held open, not buffered.
2289
+ if (req.method === "GET") {
2290
+ const url = new URL(`http://${req.headers.host ?? `127.0.0.1:${this.port}`}${req.url ?? "/"}`);
2291
+ const t = this.target(url);
2292
+ const m = t ? EVENTS_ROUTE.exec(t.route) : null;
2293
+ if (t && m) {
2294
+ void this.eventStream(t.handle, t.route, decodeURIComponent(m[1]), url, req, res, cors);
2295
+ return;
2296
+ }
2297
+ }
2298
+ const chunks = [];
2299
+ req.on("data", (c) => chunks.push(c));
2300
+ req.on("end", () => {
2301
+ const url = `http://${req.headers.host ?? `127.0.0.1:${this.port}`}${req.url ?? "/"}`;
2302
+ this.dispatch({ method: req.method ?? "GET", url, headers: headerMap(req.headers), body: new Uint8Array(Buffer.concat(chunks)) })
2303
+ .then((r) => { res.writeHead(r.status, { ...cors, ...r.headers, "content-length": String(r.body.length) }); res.end(r.body); })
2304
+ .catch((e) => { if (!res.headersSent) {
2305
+ res.writeHead(500, { ...cors, "content-type": "application/json" });
2306
+ res.end(JSON.stringify({ status: "error", code: "ERR_INTERNAL", description: e.message }));
2307
+ } });
2308
+ });
2309
+ };
2310
+ }
2311
+ /**
2312
+ * Listen on `port` (0: any) at each of `hosts` (default the IPv4 and IPv6
2313
+ * loopbacks: `<handle>.localhost` resolves to either).
2314
+ */
2315
+ async listen(port, hosts = ["127.0.0.1", "::1"]) {
2316
+ const list = Array.isArray(hosts) ? hosts : [hosts];
2317
+ for (const [i, host] of list.entries()) {
2318
+ const server = createServer(this.handler());
2319
+ // A client's idle socket is the client's to close (undici: 4 s): a server that closes first races a
2320
+ // client whose event loop was busy past the server's 5 s default — a large install message (#83:
2321
+ // a module whole) is seconds of the SDK's per-byte checks — and the client's next request is reset.
2322
+ server.keepAliveTimeout = 65_000;
2323
+ server.headersTimeout = 66_000;
2324
+ try {
2325
+ await new Promise((resolve, reject) => {
2326
+ server.once("error", reject);
2327
+ server.listen(i === 0 ? port : this.port, host, () => { server.off("error", reject); resolve(); });
2328
+ });
2329
+ }
2330
+ catch (e) {
2331
+ if (i === 0)
2332
+ throw e;
2333
+ this.say("router", `not listening on ${host}: ${e.message}`);
2334
+ continue;
2335
+ }
2336
+ if (i === 0)
2337
+ this.port = server.address().port;
2338
+ this.servers.push(server);
2339
+ }
2340
+ // The host is up: its one status subscription (#58), resumed where it left off.
2341
+ this.arc?.start();
2342
+ return this.servers[0];
2343
+ }
2344
+ }
2345
+ /** A create's `claim` ({message, body}), checked for its shape (throws; undefined: missing). */
2346
+ /**
2347
+ * The instance manager's `topics` (#137): the overlay names (gossip topics and lookup services alike)
2348
+ * the host routes to the instance at the bare host — plain non-empty strings, no duplicates; absent → undefined.
2349
+ */
2350
+ function topicsArg(v) {
2351
+ if (v == null)
2352
+ return undefined;
2353
+ if (!Array.isArray(v) || v.some((t) => typeof t !== "string" || !t) || new Set(v).size !== v.length)
2354
+ throw new Error("topics: the overlay names the host routes to it, [text, …], each non-empty, no duplicates");
2355
+ return v;
2356
+ }
2357
+ function claimOf(v) {
2358
+ const c = v;
2359
+ if (!c || typeof c !== "object" || !c.message || typeof c.message !== "object" || !(c.body instanceof Uint8Array)) {
2360
+ throw new Error("claim: the owner's signed claim {message, body} (#127: a message in box claim, signed by the owner, naming no recipient)");
2361
+ }
2362
+ return { message: c.message, body: c.body };
2363
+ }
2364
+ /** Why a claim cannot be forwarded for `owner` (throws): not a claim, another sender, a recipient named. The signature is the front door's to check. */
2365
+ function claimProblem(c, owner) {
2366
+ const m = c.message;
2367
+ if (m.kind !== "mail" || m.op !== "put" || m.box !== "claim")
2368
+ throw new Error("claim: not a message in box claim");
2369
+ if (!(m.sender instanceof Uint8Array) || Buffer.from(m.sender).toString("hex") !== owner)
2370
+ throw new Error("claim: its sender is not the owner");
2371
+ if (m.recipient !== undefined)
2372
+ throw new Error("claim: it names a recipient (a claim is signed before the instance exists: it names none)");
2373
+ if (!(m.signature instanceof Uint8Array))
2374
+ throw new Error("claim: not signed");
2375
+ }
2376
+ /** #130: a package that is the instance's own message to itself (the loopback: unsigned, sender and recipient its identity). */
2377
+ function isLoopback(pkg, identity) {
2378
+ const m = pkg.message;
2379
+ const hex = (k) => k instanceof Uint8Array ? Buffer.from(k).toString("hex") : "";
2380
+ return !!m && m.signature === undefined && hex(m.sender) === identity && hex(m.recipient) === identity;
2381
+ }
2382
+ /** #130: a reclaimed instance's store: the file, its -wal and -shm, and its directory when that empties. */
2383
+ async function removeStore(store) {
2384
+ const fs = await import("node:fs/promises");
2385
+ const { dirname } = await import("node:path");
2386
+ for (const f of [store, `${store}-wal`, `${store}-shm`])
2387
+ await fs.rm(f, { force: true });
2388
+ await fs.rmdir(dirname(store)).catch(() => { }); // not empty, or not its own: left
2389
+ }