@dcasegr/skein 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +525 -2
  2. package/bin/skein-host.js +22 -0
  3. package/bin/skein.js +8 -0
  4. package/dist/client/admin-cli.d.ts +10 -0
  5. package/dist/client/admin-cli.js +370 -0
  6. package/dist/client/admin.d.ts +139 -0
  7. package/dist/client/admin.js +256 -0
  8. package/dist/client/bundle.d.ts +9 -0
  9. package/dist/client/bundle.js +34 -0
  10. package/dist/client/cli.d.ts +44 -0
  11. package/dist/client/cli.js +306 -0
  12. package/dist/client/client.d.ts +161 -0
  13. package/dist/client/client.js +237 -0
  14. package/dist/client/config.d.ts +18 -0
  15. package/dist/client/config.js +31 -0
  16. package/dist/client/conversation.d.ts +62 -0
  17. package/dist/client/conversation.js +113 -0
  18. package/dist/client/raw.d.ts +94 -0
  19. package/dist/client/raw.js +154 -0
  20. package/dist/client/target.d.ts +34 -0
  21. package/dist/client/target.js +158 -0
  22. package/dist/dev/cli.d.ts +8 -0
  23. package/dist/dev/cli.js +248 -0
  24. package/dist/dev/scan.d.ts +15 -0
  25. package/dist/dev/scan.js +173 -0
  26. package/dist/envelope-cbor.d.ts +110 -0
  27. package/dist/envelope-cbor.js +160 -0
  28. package/dist/envelope.d.ts +38 -0
  29. package/dist/envelope.js +101 -0
  30. package/dist/host/arc.d.ts +131 -0
  31. package/dist/host/arc.js +425 -0
  32. package/dist/host/billing.d.ts +102 -0
  33. package/dist/host/billing.js +167 -0
  34. package/dist/host/boot.d.ts +246 -0
  35. package/dist/host/boot.js +643 -0
  36. package/dist/host/cli.d.ts +57 -0
  37. package/dist/host/cli.js +1253 -0
  38. package/dist/host/clock.d.ts +3 -0
  39. package/dist/host/clock.js +8 -0
  40. package/dist/host/control.d.ts +45 -0
  41. package/dist/host/control.js +170 -0
  42. package/dist/host/cron.d.ts +99 -0
  43. package/dist/host/cron.js +196 -0
  44. package/dist/host/deliver.d.ts +53 -0
  45. package/dist/host/deliver.js +251 -0
  46. package/dist/host/deploy.d.ts +29 -0
  47. package/dist/host/deploy.js +55 -0
  48. package/dist/host/door-answer.d.ts +24 -0
  49. package/dist/host/door-answer.js +21 -0
  50. package/dist/host/events.d.ts +89 -0
  51. package/dist/host/events.js +222 -0
  52. package/dist/host/fake-arcade.d.ts +40 -0
  53. package/dist/host/fake-arcade.js +149 -0
  54. package/dist/host/fake-discovery.d.ts +4 -0
  55. package/dist/host/fake-discovery.js +58 -0
  56. package/dist/host/feeds.d.ts +127 -0
  57. package/dist/host/feeds.js +422 -0
  58. package/dist/host/frontdoor.d.ts +92 -0
  59. package/dist/host/frontdoor.js +172 -0
  60. package/dist/host/genesis.d.ts +259 -0
  61. package/dist/host/genesis.js +353 -0
  62. package/dist/host/handles.d.ts +40 -0
  63. package/dist/host/handles.js +88 -0
  64. package/dist/host/hostenv.d.ts +30 -0
  65. package/dist/host/hostenv.js +83 -0
  66. package/dist/host/image-chain.d.ts +127 -0
  67. package/dist/host/image-chain.js +440 -0
  68. package/dist/host/install.d.ts +22 -0
  69. package/dist/host/install.js +139 -0
  70. package/dist/host/instances.d.ts +227 -0
  71. package/dist/host/instances.js +430 -0
  72. package/dist/host/kernel.d.ts +246 -0
  73. package/dist/host/kernel.js +603 -0
  74. package/dist/host/liveness.d.ts +65 -0
  75. package/dist/host/liveness.js +155 -0
  76. package/dist/host/manifest.d.ts +160 -0
  77. package/dist/host/manifest.js +597 -0
  78. package/dist/host/p2p.d.ts +337 -0
  79. package/dist/host/p2p.js +1002 -0
  80. package/dist/host/packet.d.ts +121 -0
  81. package/dist/host/packet.js +562 -0
  82. package/dist/host/plan.d.ts +154 -0
  83. package/dist/host/plan.js +418 -0
  84. package/dist/host/providers.d.ts +238 -0
  85. package/dist/host/providers.js +565 -0
  86. package/dist/host/roster.d.ts +72 -0
  87. package/dist/host/roster.js +172 -0
  88. package/dist/host/router.d.ts +782 -0
  89. package/dist/host/router.js +2411 -0
  90. package/dist/host/signer.d.ts +49 -0
  91. package/dist/host/signer.js +167 -0
  92. package/dist/host/store-sqlite.d.ts +39 -0
  93. package/dist/host/store-sqlite.js +170 -0
  94. package/dist/host/stores.d.ts +43 -0
  95. package/dist/host/stores.js +342 -0
  96. package/dist/host/sync.d.ts +121 -0
  97. package/dist/host/sync.js +467 -0
  98. package/dist/host/testbeef.d.ts +21 -0
  99. package/dist/host/testbeef.js +37 -0
  100. package/dist/host/testhost.d.ts +149 -0
  101. package/dist/host/testhost.js +211 -0
  102. package/dist/host/vcdiff.d.ts +9 -0
  103. package/dist/host/vcdiff.js +210 -0
  104. package/dist/host/wasm-kernel.d.ts +87 -0
  105. package/dist/host/wasm-kernel.js +296 -0
  106. package/dist/host/wasm-worker.d.ts +1 -0
  107. package/dist/host/wasm-worker.js +186 -0
  108. package/dist/peers/cron.d.ts +33 -0
  109. package/dist/peers/cron.js +75 -0
  110. package/dist/peers/infer-main.d.ts +2 -0
  111. package/dist/peers/infer-main.js +74 -0
  112. package/dist/peers/infer.d.ts +163 -0
  113. package/dist/peers/infer.js +367 -0
  114. package/dist/runtime/beef.d.ts +28 -0
  115. package/dist/runtime/beef.js +107 -0
  116. package/dist/runtime/bitcoin.d.ts +36 -0
  117. package/dist/runtime/bitcoin.js +141 -0
  118. package/dist/runtime/cid.d.ts +14 -0
  119. package/dist/runtime/cid.js +51 -0
  120. package/dist/runtime/dispatch.d.ts +92 -0
  121. package/dist/runtime/dispatch.js +142 -0
  122. package/dist/runtime/envelope.d.ts +60 -0
  123. package/dist/runtime/envelope.js +101 -0
  124. package/dist/runtime/heads.d.ts +25 -0
  125. package/dist/runtime/heads.js +37 -0
  126. package/dist/runtime/identity.d.ts +28 -0
  127. package/dist/runtime/identity.js +67 -0
  128. package/dist/runtime/index-store.d.ts +152 -0
  129. package/dist/runtime/index-store.js +715 -0
  130. package/dist/runtime/log.d.ts +62 -0
  131. package/dist/runtime/log.js +104 -0
  132. package/dist/runtime/programs.d.ts +30 -0
  133. package/dist/runtime/programs.js +61 -0
  134. package/dist/runtime/records.d.ts +135 -0
  135. package/dist/runtime/records.js +149 -0
  136. package/dist/runtime/sqlite.d.ts +33 -0
  137. package/dist/runtime/sqlite.js +604 -0
  138. package/dist/runtime/store.d.ts +141 -0
  139. package/dist/runtime/store.js +24 -0
  140. package/dist/runtime/syscalls.d.ts +9 -0
  141. package/dist/runtime/syscalls.js +41 -0
  142. package/dist/runtime/tree.d.ts +60 -0
  143. package/dist/runtime/tree.js +196 -0
  144. package/dist/runtime/types.d.ts +100 -0
  145. package/dist/runtime/types.js +7 -0
  146. package/dist/testapps.d.ts +101 -0
  147. package/dist/testapps.js +275 -0
  148. package/dist/testkit.d.ts +25 -0
  149. package/dist/testkit.js +48 -0
  150. package/dist/wallet.d.ts +35 -0
  151. package/dist/wallet.js +87 -0
  152. package/images/README.md +68 -0
  153. package/images/default/README.md +38 -0
  154. package/images/default/bin/frontdoor.cid +1 -0
  155. package/images/default/bin/frontdoor.json +5 -0
  156. package/images/default/bin/messagebox.cid +1 -0
  157. package/images/default/bin/messagebox.json +8 -0
  158. package/images/default/bin/wallet.cid +1 -0
  159. package/images/default/bin/wallet.json +14 -0
  160. package/images/default/etc/config.json +1 -0
  161. package/images/default/etc/dispatch.json +82 -0
  162. package/images/host/etc/apps.json +8 -0
  163. package/kernel-zig/README.md +1247 -0
  164. package/kernel-zig/web/client.js +65 -0
  165. package/kernel-zig/web/kernel.d.ts +59 -0
  166. package/kernel-zig/web/kernel.js +329 -0
  167. package/kernel-zig/web/store.js +67 -0
  168. package/kernel-zig/web/worker.js +79 -0
  169. package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
  170. package/package.json +77 -4
  171. package/wasm/README.md +35 -0
  172. package/wasm/frontdoor.wasm +0 -0
  173. package/wasm/messagebox.wasm +0 -0
  174. package/wasm/resolve.wasm +0 -0
  175. package/wasm/wallet.wasm +0 -0
  176. package/wasm/wire-probe.wasm +0 -0
@@ -0,0 +1,1253 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types --no-warnings
2
+ // `skein-host`: the host's management database (instances.ts, $SKEIN_HOME/host.db)
3
+ // and the router (router.ts, #33) that serves every enabled row.
4
+ // skein-host run the host (#142): on the first run it makes the host skein, then serves
5
+ // skein-host init [--handle host] the first run's part alone: the host skein made (once), nothing served
6
+ // skein-host grant <key> [--role root|<app>.<role>] [--remove] [--instance <handle>] a grant (#143)
7
+ // skein-host add <handle> [--domain d] [--derive] [--identity hex] [--store path] [--tree cid] [--knows a,b|*] [--disabled]
8
+ // skein-host add <handle> --mailbox --owner <hex> [--domain d] a mailbox instance (#40) for an identity outside the
9
+ // host: Router.createInstance with image `mailbox`, the instance manager's own create (#113)
10
+ // skein-host knows <handle> [a,b | --all | --none]
11
+ // skein-host list handle, kind, status, identity, front-door key, wallet|owner, store, tree, libp2p peer ID
12
+ // skein-host identity <handle> [--peer] an instance's identity key (from the master secret); --peer: its libp2p peer ID
13
+ // skein-host mailboxes the mailbox instances: handle, whose (owner), front-door key, status, store
14
+ // skein-host ledger [handle] the fuel ledger: what callers' calls (the front doors' reads) cost
15
+ // skein-host enable|disable|remove <handle>
16
+ // skein-host run
17
+ // skein-host roster [--for <handle>]
18
+ // skein-host peers <handle> list its address book, read from its store
19
+ // skein-host import-handles the host.db mailbox instances the host skein's onboarding app has no record
20
+ // of (#113), each with the request that adopts it — for the owner's wallet to send
21
+ // skein-host event <handle> <box> [json]
22
+ // skein-host add <handle> --boot <dir|tree-cid> [--from store.db] | --packet <file> [--scope cid] [--proofs roots.json]
23
+ // skein-host add <handle> --image <default | host | dir | tree-cid [--from store.db] | outpoint>
24
+ // skein-host system <dir>
25
+ // skein-host pack <handle|dir|tree-cid> <out> [--from store.db] [--tree cid] [--checkpoint] [--form ordfs|git] [--no-index] [--mined roots.json]
26
+ // skein-host chain [handle] #156: the host chain's tip — height, hash, record — the RocksDB database's
27
+ // (SKEIN_STORE_ROCKSDB; through the running kernel process), else <handle>'s own store's
28
+ // skein-host migrate-store <handle> #96: its runtime.db copied into its set of the host's LMDB environment (SKEIN_STORE)
29
+ // `add --boot/--packet` runs the loader (boot.ts, #4) on the new row's empty
30
+ // store: its objects pre-filled, its genesis from the system tree (or a
31
+ // checkpoint restored). `system` writes the stock system as such a tree;
32
+ // `pack` writes a packet (packet.ts: synthetic transactions, never broadcast).
33
+ // `add --image` (#89) boots the new row from an image: a system tree whose
34
+ // genesis names no owner and carries the claim row (`default`: the repo's
35
+ // images/default, the default image; `host`: images/host merged over it, the
36
+ // onboarding app installed at birth; a directory with bin/: a complete image
37
+ // of its own; a directory with no bin/ (an app repository's image/, only
38
+ // etc/apps.json): merged over the default image as images/host is — either
39
+ // way the apps its etc/apps.json names installed at birth, #152; an outpoint is
40
+ // refused until the ORDFS app exists). Its claim row admits anyone: the first claim's sender owns it
41
+ // (#127) — the owner claims it (`skein claim --instance <handle>` or
42
+ // `skein claim <its origin>`, or a wallet's own claim message); the host
43
+ // holds no owner's key and sends no claim.
44
+ // `run` (#142) is the one command a host needs. Its settings are $SKEIN_HOME/host.env (every `SKEIN_*`
45
+ // line; the environment wins: hostenv.ts) — they configure the commands, and through `run` how the host
46
+ // skein is first built; a later change does not reach into a live skein. On the first run (no host skein in
47
+ // host.db) it makes the master secret and host.db as before, the operator's key (SKEIN_OPERATOR_KEY,
48
+ // default $SKEIN_HOME/operator.key: used if present, made if not, plain, 0600) and the HOST SKEIN —
49
+ // Router.createInstance with `host`: from the host image (images/host merged over images/default: the
50
+ // onboarding app), its genesis's `root` the operator's key (#143: root from birth;
51
+ // no claim route), the onboarding app's config
52
+ // from host.env (domain SKEIN_HANDLE_DOMAIN, default the router origin's domain; origin
53
+ // SKEIN_ROUTER_ORIGIN; name SKEIN_HOST_NAME; note SKEIN_HOST_NOTE), the instance manager and the certifier in
54
+ // its address book, published at once — and prints its identity and URLs. Later runs start what exists.
55
+ // `init` is the first run's part alone. `grant` grants a role — root by default — to another key (a
56
+ // browser wallet's) in the host skein: the operator's message, signed with its key and handed to the running host
57
+ // (src/client/target.ts, the control socket's `message` op).
58
+ // `add` inserts, or updates the given fields of an existing row. A new row's
59
+ // identity is the signer's (signer.ts, #18): derived from the router's master
60
+ // secret with key ID = the handle, no wallet process; `--derive` sets it again
61
+ // on an existing row (its store must then be a new one: re-genesis).
62
+ // `roster` prints the front end's roster (roster.ts), which `run` also serves
63
+ // at /roster.json; `roster --for h` prints h's ROSTER.md (#27: the rows it
64
+ // `knows`; `knows` sets them). `peers <handle> list` reads an instance's
65
+ // address book from its store. Nothing here writes into a running instance
66
+ // on its own: installing an app, a dispatch row, an address-book entry, a
67
+ // directory into `main` are the owner's messages (the client `skein`, #142,
68
+ // signs them with the operator's key; the management page with a wallet).
69
+ // Nothing registers itself anywhere: a key in no address book is "no route".
70
+ // `event` (#60, #69) sends one message from the host's cron provider into a
71
+ // box of an instance now, as a tick due now would be (cron.ts): the JSON
72
+ // object given, its kind "cron" unless it names one, `due` now; the instance
73
+ // takes it by its dispatch row for that box (from the cron provider, or from
74
+ // anyone). A store has one kernel: while `run` is up the event goes over its
75
+ // control socket ($SKEIN_HOME/host.sock, control.ts) and the running router
76
+ // sends it; with the host down, through a router of its own, closed
77
+ // afterwards. A router that answers at SKEIN_HOST_URL / SKEIN_ROUTER_PORT
78
+ // with no control socket here is refused.
79
+ // `run` is the router (#40: a reverse proxy — each
80
+ // instance is an HTTP server, its front door, at http://<handle>.localhost:<port>
81
+ // or /@<handle>) on SKEIN_ROUTER_PORT, a `skein-kernel serve` per instance
82
+ // (every enabled row's started at start, any other on demand; stopped only
83
+ // past SKEIN_IDLE_MS, which is unset by default), the providers (the waker, the cron
84
+ // provider among them), the signer, the fuel ledger; plus the host page. It
85
+ // reads ($SKEIN_HOME/host.env, under the environment):
86
+ // SKEIN_HOME default ~/.skein; host.db, master.key, operator.key and host.env live here
87
+ // SKEIN_OPERATOR_KEY the operator's key file (#142), default $SKEIN_HOME/operator.key: the host skein's owner
88
+ // SKEIN_HANDLE_DOMAIN, SKEIN_HOST_NAME, SKEIN_HOST_NOTE the onboarding app's config at the host skein's birth
89
+ // SKEIN_MASTER_KEY the master secret (hex), else SKEIN_MASTER_KEY_FILE, else $SKEIN_HOME/master.key (made if absent)
90
+ // SKEIN_KEY_FILE #160: one-key mode — the ONE key every instance signs with (a file, read and never made), or
91
+ // SKEIN_KEY (hex); refused beside SKEIN_MASTER_KEY(_FILE). Every row the same identity, one peer key
92
+ // (signer.ts Signer.oneKey), one libp2p node for all (p2p.ts); a create claimed by the host's key (#162)
93
+ // SKEIN_MANAGER_FOR #162: the instances (handles, comma-separated) besides the host skein the instance manager acts for:
94
+ // the manager in their address book at birth, their create/start/stop taken
95
+ // SKEIN_IMAGES_DIR #162: where a create's image by name is (<dir>/<name>), default $SKEIN_HOME/images
96
+ // SKEIN_ROUTER_PORT the router, default 8100: an instance at http://<handle>.localhost:8100 (or /@<handle>)
97
+ // SKEIN_ROOT_INSTANCE the instance served at / of a hostname that is no instance's (the standalone's app at http://localhost:<port>/)
98
+ // SKEIN_ROUTER_BIND the addresses the router listens on, a comma list (default 127.0.0.1,::1): a tunnel on another box needs the LAN/tailnet address here
99
+ // SKEIN_ROUTER_ORIGIN the router's own public origin, default http://127.0.0.1:{port}: where BRC-169 discovery is
100
+ // answered (the host skein's onboarding app, #113), and the geneses' resolveOrigin. The manifest's
101
+ // URLs, the handle domain, the host's name and note are the onboarding app's config
102
+ // (config.onboard), written at the host skein's birth from these settings
103
+ // SKEIN_INSTANCE_ORIGIN an instance's origin template, default http://{handle}.localhost:{port}
104
+ // SKEIN_OWNER_MESSAGEBOX the owner's messagebox URL for new geneses, default its mailbox instance here
105
+ // SKEIN_NETWORK the network the whole host runs on: main | test | regtest, default main (any other value:
106
+ // skein-host refuses to start). Every genesis the host makes has it as defaults.walletNetwork,
107
+ // under a tree's own config; the chain app takes its network from that default, so
108
+ // SKEIN_HEADERS_URL must feed this network's headers or the chain app refuses them
109
+ // SKEIN_IDLE_MS stop a kernel this long after its last work (ms); default 0: never (#40)
110
+ // SKEIN_ANSWER_WAIT_MS how long the front door holds a request waiting on its thread's answer (ms) before 503 +
111
+ // Retry-After; default 120000 (two minutes; frontdoor.ts)
112
+ // SKEIN_OWNER a new instance's owner; SKEIN_OWNER_HANDLE its genesis name, default david@localhost
113
+ // SKEIN_INFER a new instance's peers.infer; SKEIN_INFER_HANDLE its genesis name, default infer@localhost
114
+ // SKEIN_FUEL_PER_STEP a new genesis's fuelPerStep
115
+ // SKEIN_HOST_PORT the host page (/) and roster (/roster.json) on 127.0.0.1, default 4600
116
+ // SKEIN_KERNEL_BIN the kernel binary, default kernel-zig/zig-out/bin/skein-kernel
117
+ // SKEIN_STORE #96: the host's LMDB environment (a directory, e.g. $SKEIN_HOME/store): a new instance's store is
118
+ // its set there (host.db `lmdb:<dir>`; the set its handle), its kernel given SKEIN_STORE. Unset: a file
119
+ // per instance, $SKEIN_HOME/instances/<handle>/runtime.db. Existing rows keep theirs until migrate-store
120
+ // SKEIN_STORE_ROCKSDB #158: the host's RocksDB database (a directory, e.g. $SKEIN_HOME/store-rocksdb), run by ONE
121
+ // `skein-kernel host` process the router starts (every instance in it, a worker pool,
122
+ // SKEIN_WORKERS threads, default the CPU count): a new instance's store is its set there
123
+ // (host.db `rocksdb:<dir>`). Over SKEIN_STORE. The host's reads of such a set go through that
124
+ // process (its host-read socket), so `ls`, explore and the other readers need the host running;
125
+ // the one-shot commands that boot (`add --image`, `init`) open the database themselves, so the
126
+ // host must be stopped for them (RocksDB: one process per database)
127
+ // SKEIN_STORE_SQLITE the settled split (2026-10-31): the host's ONE SQLite database (a file, e.g. $SKEIN_HOME/store.sqlite):
128
+ // a new instance's store is its set there (host.db `sqlite:<path>`; the set its handle), run by the
129
+ // WASM kernel this process loads (one worker per active instance, wasm-kernel.ts). Over
130
+ // SKEIN_STORE_ROCKSDB and SKEIN_STORE. Unset, and no native kernel binary here (the npm package
131
+ // ships none): $SKEIN_HOME/store.sqlite
132
+ // SKEIN_ARC_URL the host's Arcade (#58, #65, arc.ts): where the instances' broadcast events go (a durable
133
+ // queue), one status subscription, the instances' status provider; SKEIN_ARC_TOKEN its one callback token (required with it);
134
+ // SKEIN_ARC_EVENTS_URL its SSE service (default <url>/events); SKEIN_ARC_CALLBACK_URL where Arcade
135
+ // posts webhooks (this router's /arc/callback as Arcade reaches it; unset: SSE only)
136
+ // SKEIN_BILLING `off`: this host bills no one (#130, billing.ts); else it bills a skein whose host row names its
137
+ // key, by SKEIN_BILLING_X (sats a skein prepays at a time), SKEIN_BILLING_RATES (JSON {fuel,
138
+ // storage, served, fetch, authfetch, publish}: whole sats), SKEIN_BILLING_ALLOWANCE (the free
139
+ // allowance per skein, sats), SKEIN_BILLING_TICK_MS, SKEIN_BILLING_GRACE_MS — small dev defaults
140
+ // (billing.ts DEV_BILLING); published at /.well-known/skein-host
141
+ // SKEIN_HEADERS_URL the host's headers feed (#102, feeds.ts): an SSE stream of block headers (hex, or chaintracks'
142
+ // JSON: Arcade's http://127.0.0.1:8083/chaintracks/v2/tip/stream) that every enabled instance
143
+ // whose dispatch table takes events in box `chain` (the chain app's row) is subscribed to;
144
+ // and the default image's chain part (#132, image-chain.ts): every header it brings is
145
+ // appended to the image, which a chaintracks URL's history (`…/tip/stream` → `…/headers`)
146
+ // fills from genesis at start
147
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
148
+ import { tmpdir } from "node:os";
149
+ import { CID as CIDClass, decode as decodeCbor, parse as parseCid } from "../runtime/cid.js";
150
+ import { headTree, MAIN } from "../runtime/heads.js";
151
+ import { anyOf, DEFAULT_IMAGE, dirSource, imageSource, isOverlayImage, MemBlocks, packetSource, stockSystemFiles, storeObjects, WASM_DIR, wasmDirObjects } from "./boot.js";
152
+ import { Kernel, KERNEL_BIN } from "./kernel.js";
153
+ import { dirname, join, resolve } from "node:path";
154
+ import { parseArgs } from "node:util";
155
+ import { short } from "../runtime/log.js";
156
+ import { isLmdb, isRocks, storeSpec } from "../runtime/index-store.js";
157
+ import { openStoreFile, readHostChain, storeBlocks, storeExists } from "./stores.js";
158
+ import { spawnSync } from "node:child_process";
159
+ import { connect } from "node:net";
160
+ import { oneKeySetting, signerOf } from "./signer.js";
161
+ import { CONTROL_SOCKET, controlRequest } from "./control.js";
162
+ import { homeOf as homeOfVars, identityOf, operatorKey, withHostEnv } from "./hostenv.js";
163
+ import { hostArcConfig } from "./arc.js";
164
+ import { hostP2PConfig, peerIdOf } from "./p2p.js";
165
+ import { addressBook } from "./deploy.js";
166
+ import { fetchHttp, hostDomain, Router } from "./router.js";
167
+ import { billingConfig, DEV_BILLING, NSAT } from "./billing.js";
168
+ import { HostDb, knowsColumn, knowsOf, pathOnly } from "./instances.js";
169
+ import { deployedIdentity, hostPage, parseIdentity, roster, rosterFor, serveRoster } from "./roster.js";
170
+ const USAGE = `usage:
171
+ skein-host run the host: on the first run the host skein (owned by the operator's key), then the router on :8100
172
+ (instances at <handle>.localhost:8100), a kernel per instance on demand; host page and roster on :4600
173
+ skein-host init [--handle host] the first run's part alone: the host skein (#142), once; nothing served
174
+ skein-host grant <key> [--role root|<app>.<role>] [--remove] [--instance <handle>] [--dry-run]
175
+ a grant (#143; root by default) in the host skein (or <handle>):
176
+ the operator's message, to the running host
177
+ skein-host add <handle> [--domain d] [--derive] [--identity hex] [--store path] [--tree cid] [--knows a,b|*] [--disabled]
178
+ skein-host add <handle> --mailbox --owner <hex> [--domain d] a mailbox instance for an identity outside the host (#40): the instance manager's create (#113)
179
+ skein-host knows <handle> [a,b | --all | --none] which agents its ROSTER.md lists; no list: print them
180
+ skein-host list handle, kind, status, identity, front-door key, wallet|owner, store, tree, libp2p peer ID
181
+ skein-host identity <handle> [--peer] an instance's identity key; --peer: its libp2p peer ID (#51)
182
+ skein-host mailboxes the mailbox instances: handle, owner, front-door key, status, store
183
+ skein-host ledger [handle] the fuel ledger: calls and fuel per instance, caller, op
184
+ skein-host billing [handle] #130: each billed instance as the router last read it (tally, allocation, asleep since,
185
+ terms not served), its ticks' log records and its payments
186
+ skein-host reclaim [--grace ms] [--yes] #130: the instances asleep past the grace (SKEIN_BILLING_GRACE_MS); --yes reclaims them:
187
+ disabled, row and store removed (through the running router)
188
+ skein-host enable|disable|remove <handle>
189
+ skein-host roster the front end's roster JSON
190
+ skein-host roster --for <handle> that agent's ROSTER.md
191
+ skein-host peers <handle> list its address book, from its store: key, transport, address, handle, source
192
+ skein-host import-handles the mailbox instances in host.db the host skein's onboarding app has no record of (#113), each with
193
+ the owner's request that adopts it (onboard.adopt): \`1sat authfetch POST <origin>/onboard/call --body '…'\`
194
+ skein-host event <handle> <box> [json] a message from the cron provider into <box> now, as a tick due now ({...json, kind: "cron" unless named, due: now}); through the running router's control socket, else a router of its own
195
+ skein-host add <handle> --boot <dir | tree-cid [--from store.db]> boot a new instance from a system tree (docs/BOOTSTRAP.md)
196
+ skein-host add <handle> --packet <file> [--scope cid] [--proofs roots.json] … from a packet: a system tree, or a checkpoint to restore
197
+ skein-host add <handle> --image <default | host | dir | tree-cid [--from store.db] | outpoint> a new instance from an image: no owner, a claim row from anyone (#89; claim it: skein claim)
198
+ skein-host system <dir> write the stock system (what code genesis has) as a system tree
199
+ skein-host pack <handle|dir|tree-cid> <out> [--from store.db] [--tree cid] [--checkpoint] [--form ordfs|git] [--no-index] [--mined roots.json]
200
+ skein-host chain [handle] #156: the host chain's tip (height, hash, record): the RocksDB database's
201
+ (SKEIN_STORE_ROCKSDB, through the running kernel process), else <handle>'s own store's
202
+ skein-host migrate-store <handle> #96: its runtime.db copied into its set of the host's LMDB environment (SKEIN_STORE); the row then
203
+ names the set, the file kept beside as runtime.db.migrated-<date>. With the host stopped
204
+ settings: $SKEIN_HOME/host.env (every SKEIN_* line; the environment wins). The owner's messages (installing an app,
205
+ dispatch rows, the address book, a directory into main) are the client's: \`skein install|dispatch|… --instance <handle>\``;
206
+ export const homeOf = homeOfVars;
207
+ export async function main(argv, given) {
208
+ const [cmd, ...rest] = argv;
209
+ const env = { ...given, vars: withHostEnv(given.vars) }; // #142: host.env's SKEIN_* under the environment
210
+ const home = homeOf(env.vars);
211
+ if (!cmd || cmd === "help" || cmd === "-h" || cmd === "--help") {
212
+ env.out(USAGE);
213
+ return cmd ? 0 : 2;
214
+ }
215
+ try {
216
+ hostNetwork(env.vars);
217
+ }
218
+ catch (e) {
219
+ env.err(`skein-host: ${e.message}`);
220
+ return 2;
221
+ }
222
+ mkdirSync(home, { recursive: true });
223
+ const db = new HostDb(join(home, "host.db"));
224
+ try {
225
+ switch (cmd) {
226
+ case "add": {
227
+ const { values: v, positionals: [handle] } = parseArgs({
228
+ args: rest, allowPositionals: true,
229
+ options: {
230
+ domain: { type: "string" }, identity: { type: "string" }, derive: { type: "boolean" }, store: { type: "string" }, tree: { type: "string" }, knows: { type: "string" }, disabled: { type: "boolean" },
231
+ boot: { type: "string" }, from: { type: "string" }, packet: { type: "string" }, scope: { type: "string" }, proofs: { type: "string" }, image: { type: "string" },
232
+ mailbox: { type: "boolean" }, owner: { type: "string" },
233
+ },
234
+ });
235
+ if (!handle) {
236
+ env.err(USAGE);
237
+ return 2;
238
+ }
239
+ const f = { domain: v.domain, identity: v.identity, store: v.store, tree: v.tree, status: v.disabled ? "disabled" : undefined };
240
+ if (v.knows !== undefined)
241
+ f.knows = knowsColumn(handles(v.knows));
242
+ if (v.mailbox) {
243
+ // A mailbox instance (#40): the front door and the messagebox, keeping mail for --owner. #113: the instance
244
+ // manager's own create (image `mailbox`), through a router of this command's own — the one creation path.
245
+ if (!v.owner || !/^0[23][0-9a-f]{64}$/.test(v.owner)) {
246
+ env.err("skein-host add --mailbox: --owner <identity key, hex>");
247
+ return 2;
248
+ }
249
+ if ([v.boot, v.packet, v.image, v.identity, v.store, v.tree].some((x) => x !== undefined) || v.derive) {
250
+ env.err("skein-host add --mailbox: only --owner and --domain");
251
+ return 2;
252
+ }
253
+ return await mailboxCmd(db, handle, v.owner, v.domain, env);
254
+ }
255
+ else if (v.owner) {
256
+ env.err("skein-host add: --owner goes with --mailbox");
257
+ return 2;
258
+ }
259
+ if (!db.get(handle)) {
260
+ const sqlite = sqliteStoreOf(env.vars, home);
261
+ f.store ??= sqlite ? `sqlite:${sqlite}` : env.vars.SKEIN_STORE_ROCKSDB ? `rocksdb:${resolve(env.vars.SKEIN_STORE_ROCKSDB)}` : env.vars.SKEIN_STORE ? `lmdb:${resolve(env.vars.SKEIN_STORE)}` : join(home, "instances", handle, "runtime.db"); // #96, #158, the settled split
262
+ f.domain ??= hostDomain(env.vars.SKEIN_ROUTER_ORIGIN ?? "http://127.0.0.1"); // the host's domain (H4), as the router's
263
+ }
264
+ // The identity is the signer's (#18): derived from the master secret, key ID = the handle.
265
+ if (f.identity === undefined && (v.derive || !db.get(handle)?.identity))
266
+ f.identity = signerOf(env.vars, home).identity(handle);
267
+ if ([v.boot, v.packet, v.image].filter((x) => x !== undefined).length > 1) {
268
+ env.err("skein-host add: one of --boot, --packet, --image");
269
+ return 2;
270
+ }
271
+ if (v.image !== undefined && isOutpoint(v.image)) {
272
+ env.err(`skein-host add --image ${v.image}: an image by outpoint is read through the ORDFS app, which is not built yet; give a directory or a tree CID`);
273
+ return 2;
274
+ }
275
+ const r = db.add(handle, f);
276
+ env.out(`${r.handle}@${r.domain} ${r.status} · store ${r.store}${r.identity ? ` · ${short(r.identity)}` : ""}`);
277
+ if (v.boot || v.packet || v.image !== undefined) {
278
+ // The loader (#4): pre-fill the new store and write its genesis from the tree (or restore a checkpoint).
279
+ // A one-shot (#61): the router it boots through is closed before `add` returns, so the process exits.
280
+ const router = new Router({ ...routerOptions(db, env), idleMs: 0 });
281
+ try {
282
+ // `default`: the default image as this host holds it (#132: with its chain part, image-chain.ts).
283
+ // `host`: images/host merged over it (#142), unclaimed.
284
+ // A directory with no bin/ (#152: an app repository's image/, etc/apps.json alone): merged over the
285
+ // default image as images/host is, with the same chain part. A directory with bin/: a complete image.
286
+ const overlay = v.image !== undefined && existsSync(v.image) && statSync(v.image).isDirectory() && isOverlayImage(v.image) ? resolve(v.image) : undefined;
287
+ const src = v.image === "default" || v.image === "host" ? await router.image.source({ image: v.image })
288
+ : overlay ? await router.image.source({ overlay })
289
+ : await bootSourceOf(v.image !== undefined ? { boot: v.image, from: v.from } : v, r);
290
+ const b = await router.bootRow(handle, src, { image: v.image !== undefined });
291
+ env.out(b.state ? `${handle}: restored checkpoint ${b.state} (${b.objects} blocks)` : `${handle}: booted from ${v.image !== undefined ? "the image " : ""}${b.tree} · ${b.objects} objects pre-filled · programs ${b.programs.join(", ")} · genesis ${b.entry}${v.image !== undefined ? ` · no owner: claim it (skein claim --instance ${handle})` : ""}`);
292
+ }
293
+ catch (e) {
294
+ env.err(`skein-host add ${handle}: ${e.message}`);
295
+ return 1;
296
+ }
297
+ finally {
298
+ await router.close();
299
+ }
300
+ }
301
+ return 0;
302
+ }
303
+ case "init":
304
+ return await initCmd(db, rest, env);
305
+ case "grant":
306
+ return await grantCmd(db, rest, env);
307
+ case "identity": {
308
+ // The signer's key (signer.ts) for an instance: the BRC-104 identity its front door answers as.
309
+ const [handle, flag, ...more] = rest;
310
+ if (!handle || more.length || (flag !== undefined && flag !== "--peer")) {
311
+ env.err(USAGE);
312
+ return 2;
313
+ }
314
+ const signer = signerOf(env.vars, home);
315
+ // The libp2p peer ID (#51): the identity multihash of the peer key, a child of the instance's root key (#129: key ID libp2p:<handle>, self).
316
+ env.out(flag ? peerIdOf(signer.peerKey(handle)).toString() : signer.identity(handle));
317
+ return 0;
318
+ }
319
+ case "mailboxes": {
320
+ const door = frontDoorKeys(env.vars, home);
321
+ for (const r of db.list().filter((x) => x.kind === "mailbox"))
322
+ env.out([`${r.handle}@${r.domain}`, r.owner ?? "-", door(r), r.status, r.store].join("\t"));
323
+ return 0;
324
+ }
325
+ case "ledger":
326
+ for (const l of db.ledger(rest[0]))
327
+ env.out([l.instance, l.caller || "-", l.op, String(l.calls), String(l.fuel), l.updated_at].join("\t"));
328
+ return 0;
329
+ case "billing":
330
+ return billingCmd(db, rest, env);
331
+ case "reclaim":
332
+ return await reclaimCmd(db, rest, env);
333
+ case "list": {
334
+ const door = frontDoorKeys(env.vars, home);
335
+ const peer = peerIds(env.vars, home);
336
+ const host = db.setting("host_skein");
337
+ for (const r of db.list())
338
+ env.out([`${r.handle}@${r.domain}`, r.handle === host ? "host" : r.kind ?? "agent", r.status, r.identity ?? "-", door(r), r.kind === "mailbox" ? `owner ${r.owner}` : "-", r.store, r.tree ?? "-", peer(r)].join("\t"));
339
+ return 0;
340
+ }
341
+ case "enable":
342
+ case "disable":
343
+ case "remove": {
344
+ const [handle] = rest;
345
+ if (!handle) {
346
+ env.err(USAGE);
347
+ return 2;
348
+ }
349
+ const ok = cmd === "remove" ? db.remove(handle) : db.setStatus(handle, cmd === "enable" ? "enabled" : "disabled");
350
+ if (!ok) {
351
+ env.err(`skein-host ${cmd}: no instance ${handle}`);
352
+ return 1;
353
+ }
354
+ env.out(`${handle}: ${cmd === "remove" ? "removed (its store and wallet are untouched)" : `${cmd}d`}`);
355
+ return 0;
356
+ }
357
+ case "run":
358
+ return await run(db, rest, env);
359
+ case "knows":
360
+ return knowsCmd(db, rest, env);
361
+ case "roster":
362
+ return await rosterCmd(db, rest, env);
363
+ case "peers":
364
+ return await peersCmd(db, rest, env);
365
+ case "event":
366
+ return await eventCmd(db, rest, env);
367
+ case "import-handles":
368
+ return await importHandlesCmd(db, rest, env);
369
+ case "system":
370
+ return await systemCmd(rest, env);
371
+ case "pack":
372
+ return await packCmd(db, rest, env);
373
+ case "migrate-store":
374
+ return await migrateStoreCmd(db, rest, env);
375
+ case "chain": {
376
+ // #156: the host chain's tip (a RocksDB host's: one per host; a serve instance's: in its own store).
377
+ const [handle] = rest;
378
+ const row = handle ? db.get(handle) : undefined;
379
+ if (handle && !row) {
380
+ env.err(`skein-host chain: no instance ${handle}`);
381
+ return 1;
382
+ }
383
+ const sqlite = sqliteStoreOf(env.vars, homeOf(env.vars));
384
+ const spec = row ? storeSpec(row) : sqlite ? `sqlite:${sqlite}#host/chain` : env.vars.SKEIN_STORE_ROCKSDB ? `rocksdb:${resolve(env.vars.SKEIN_STORE_ROCKSDB)}#host/chain` : undefined;
385
+ if (!spec) {
386
+ env.err("skein-host chain: a handle (its own store's host chain), or SKEIN_STORE_ROCKSDB (the host's)");
387
+ return 2;
388
+ }
389
+ const c = readHostChain(spec);
390
+ env.out(c ? `${c.height}\t${c.hash}\t${c.root.toString()}` : "no host chain");
391
+ return 0;
392
+ }
393
+ default:
394
+ env.err(USAGE);
395
+ return 2;
396
+ }
397
+ }
398
+ finally {
399
+ if (cmd !== "run")
400
+ db.close(); // run keeps it (and closes it on a signal)
401
+ }
402
+ }
403
+ /** The network the whole host runs on (SKEIN_NETWORK, default main): a network fact, written as every genesis's `defaults.walletNetwork`. */
404
+ export const NETWORKS = ["main", "test", "regtest"];
405
+ export function hostNetwork(v) {
406
+ const n = v.SKEIN_NETWORK || "main";
407
+ if (!NETWORKS.includes(n))
408
+ throw new Error(`SKEIN_NETWORK is main, test or regtest (default main), not ${JSON.stringify(n)}`);
409
+ return n;
410
+ }
411
+ const handles = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
412
+ /**
413
+ * The key each row's front door signs its BRC-104 sessions with (#40): the
414
+ * signer's, derived from the master secret with key ID = the handle — for an
415
+ * agent and a mailbox instance alike, and before the row was ever hydrated.
416
+ * The master secret is only read here, never made: with none, "-".
417
+ */
418
+ function frontDoorKeys(vars, home) {
419
+ const file = vars.SKEIN_MASTER_KEY_FILE || join(home, "master.key");
420
+ if (!oneKeySetting(vars) && !vars.SKEIN_MASTER_KEY && !existsSync(file))
421
+ return () => "-";
422
+ const signer = signerOf(vars, home);
423
+ return (row) => signer.identity(row.handle);
424
+ }
425
+ /** Each row's libp2p peer ID (#51): derived from the instance's root key (#129: key ID libp2p:<handle>, self), whether or not it runs a node; "-" with no master secret. */
426
+ function peerIds(vars, home) {
427
+ const file = vars.SKEIN_MASTER_KEY_FILE || join(home, "master.key");
428
+ if (!oneKeySetting(vars) && !vars.SKEIN_MASTER_KEY && !existsSync(file))
429
+ return () => "-";
430
+ const signer = signerOf(vars, home);
431
+ return (row) => peerIdOf(signer.peerKey(row.handle)).toString();
432
+ }
433
+ function knowsCmd(db, rest, env) {
434
+ const { values: v, positionals: [handle, list, ...more] } = parseArgs({ args: rest, allowPositionals: true, options: { all: { type: "boolean" }, none: { type: "boolean" } } });
435
+ if (!handle || more.length || [list !== undefined, v.all, v.none].filter(Boolean).length > 1) {
436
+ env.err(USAGE);
437
+ return 2;
438
+ }
439
+ if (!db.get(handle)) {
440
+ env.err(`skein-host knows: no instance ${handle}`);
441
+ return 1;
442
+ }
443
+ if (list !== undefined || v.all || v.none) {
444
+ const k = v.all ? "all" : v.none ? [] : handles(list);
445
+ for (const h of k === "all" ? [] : k)
446
+ if (h !== "*" && !db.get(h))
447
+ env.err(`skein-host knows: no instance ${h} (yet): kept, listed once it exists`);
448
+ db.setKnows(handle, k);
449
+ }
450
+ const k = knowsOf(db.get(handle));
451
+ env.out(`${handle} knows ${k === "all" ? "everyone" : k.length ? k.join(", ") : "nobody"}`);
452
+ return 0;
453
+ }
454
+ /**
455
+ * An instance's origin here (router.ts originOf): its front door, its
456
+ * messagebox — SKEIN_INSTANCE_ORIGIN (default http://{handle}.localhost:{port})
457
+ * on SKEIN_ROUTER_PORT (default 8100).
458
+ */
459
+ export function originOf(vars, handle) {
460
+ // #161: a path-only handle (a token id): /@<handle> on the router's own origin.
461
+ if (pathOnly(handle))
462
+ return `${(vars.SKEIN_ROUTER_ORIGIN || "http://127.0.0.1:{port}").replace("{port}", vars.SKEIN_ROUTER_PORT || "8100").replace(/\/+$/, "")}/@${handle}`;
463
+ return (vars.SKEIN_INSTANCE_ORIGIN || "http://{handle}.localhost:{port}").replace("{handle}", handle).replace("{port}", vars.SKEIN_ROUTER_PORT || "8100");
464
+ }
465
+ /** `skein-host peers <handle> list`: the instance's address book, read from its store (#124: writing it is the owner's `peers` messages, `skein peers`). */
466
+ async function peersCmd(db, rest, env) {
467
+ const [handle, op, ...more] = rest;
468
+ if (!handle || more.length || op !== "list") {
469
+ env.err(USAGE);
470
+ return 2;
471
+ }
472
+ const row = db.get(handle);
473
+ if (!row) {
474
+ env.err(`skein-host peers: no instance ${handle}`);
475
+ return 1;
476
+ }
477
+ if (row.kind === "mailbox") {
478
+ env.err(`skein-host peers: ${handle} is a mailbox instance: it keeps mail, it sends nothing`);
479
+ return 1;
480
+ }
481
+ const s = openRow(row, env);
482
+ try {
483
+ for (const e of await addressBook(s.blocks))
484
+ env.out([e.key, e.transport, e.address, e.handle ? `${e.handle}@${e.domain ?? ""}` : "-", e.source ?? "-"].join("\t"));
485
+ return 0;
486
+ }
487
+ finally {
488
+ await s.close?.();
489
+ }
490
+ }
491
+ /**
492
+ * `skein-host event <handle> <box> [json]` (#60, #69): one message from the
493
+ * host's cron provider into `box` now — what a tick due now is
494
+ * (Router.cronEvent: signed by the provider, appended as a `local` request).
495
+ * While `skein-host run` is up, its kernel holds the instance's store, so the
496
+ * event goes to that router over its control socket ($SKEIN_HOME/host.sock)
497
+ * and it sends it. Otherwise it goes through a router of this command's own, a
498
+ * one-shot's (no ticks, #61's close): the steps it starts run before that
499
+ * router is closed. A router answering for the host on HTTP with no control
500
+ * socket here (another SKEIN_HOME, or a router from before the socket) is
501
+ * refused: a second kernel must not write the store.
502
+ */
503
+ async function eventCmd(db, rest, env) {
504
+ const [handle, box, json, ...more] = rest;
505
+ if (!handle || !box || more.length) {
506
+ env.err(USAGE);
507
+ return 2;
508
+ }
509
+ const row = db.get(handle);
510
+ if (!row) {
511
+ env.err(`skein-host event: no instance ${handle}`);
512
+ return 1;
513
+ }
514
+ if (row.status !== "enabled") {
515
+ env.err(`skein-host event: ${handle} is disabled`);
516
+ return 1;
517
+ }
518
+ let given = {};
519
+ if (json !== undefined) {
520
+ try {
521
+ given = JSON.parse(json);
522
+ }
523
+ catch (e) {
524
+ env.err(`skein-host event: the event is not JSON: ${e.message}`);
525
+ return 2;
526
+ }
527
+ }
528
+ if (!given || typeof given !== "object" || Array.isArray(given)) {
529
+ env.err("skein-host event: the event is a JSON object");
530
+ return 2;
531
+ }
532
+ const event = given;
533
+ const kind = String(event.kind ?? "cron");
534
+ // The running router, over its control socket: it admits with the kernel it holds.
535
+ const sock = join(homeOf(env.vars), CONTROL_SOCKET);
536
+ let answer;
537
+ try {
538
+ answer = await controlRequest(sock, { op: "event", handle, box, event });
539
+ }
540
+ catch (e) {
541
+ env.err(`skein-host event ${handle}: the router's control socket: ${e.message}`);
542
+ return 1;
543
+ }
544
+ if (answer) {
545
+ if (!answer.ok) {
546
+ env.err(`skein-host event ${handle}: ${answer.error}`);
547
+ return 1;
548
+ }
549
+ env.out(`${handle}: ${kind} from the cron provider into ${box} as ${answer.entry} (by the running router)`);
550
+ return 0;
551
+ }
552
+ const running = await routerAt(env.vars);
553
+ if (running) {
554
+ env.err(`skein-host event: a router serves this host at ${running}, but no control socket answers at ${sock} (another SKEIN_HOME, or a router started before the socket): its kernel holds ${handle}'s store, and a second one must not write it. Run this with the router's SKEIN_HOME, or restart the router`);
555
+ return 1;
556
+ }
557
+ const router = new Router({ ...routerOptions(db, env), idleMs: 0, cron: false });
558
+ try {
559
+ const e = await router.cronEvent(handle, box, event);
560
+ await router.settled();
561
+ env.out(`${handle}: ${kind} from the cron provider into ${box} as ${e}`);
562
+ return 0;
563
+ }
564
+ catch (e) {
565
+ env.err(`skein-host event ${handle}: ${e.message}`);
566
+ return 1;
567
+ }
568
+ finally {
569
+ await router.close();
570
+ }
571
+ }
572
+ /** The onboarding app's config at the host skein's birth (#142), from the settings: domain, origin, name, note. */
573
+ export function onboardConfig(v) {
574
+ const port = v.SKEIN_ROUTER_PORT || "8100";
575
+ const origin = (v.SKEIN_ROUTER_ORIGIN || "http://127.0.0.1:{port}").replace("{port}", port).replace(/\/+$/, "");
576
+ return { onboard: { onboard: { domain: v.SKEIN_HANDLE_DOMAIN || hostDomain(origin), origin, ...(v.SKEIN_HOST_NAME ? { name: v.SKEIN_HOST_NAME } : {}), ...(v.SKEIN_HOST_NOTE ? { note: v.SKEIN_HOST_NOTE } : {}) } } };
577
+ }
578
+ /**
579
+ * The first run's part (#142): the operator's key (made if absent) and the host skein —
580
+ * Router.createInstance with `host`: the host image, the operator's key its owner in its genesis, the
581
+ * onboarding app's config from the settings — published at once. What it prints is what a human needs.
582
+ */
583
+ async function createHostSkein(router, db, handle, env) {
584
+ const op = operatorKey(env.vars, { create: true });
585
+ const owner = identityOf(op.key);
586
+ const t0 = Date.now();
587
+ const config = onboardConfig(env.vars);
588
+ const c = await router.createInstance(handle, owner, { host: true, appConfig: config });
589
+ const cfg = config.onboard.onboard;
590
+ env.out(`skein-host: first run — the host skein ${c.handle} ${c.identity} at ${c.url}, owned by the operator's key ${owner} (${op.path}${op.made ? ", made now, mode 0600" : ""}) · ${Date.now() - t0} ms`);
591
+ env.out(`skein-host: apps chain, git, site, onboard installed at birth · handles @${cfg.domain} · registrations and BRC-169 at ${cfg.origin}/`);
592
+ env.out(`skein-host: to manage it from a browser wallet: skein-host grant <the wallet's identity key> --role root · the operator's own messages: skein install|routes|grant|… --instance ${c.handle}`);
593
+ return db.get(handle);
594
+ }
595
+ /**
596
+ * `skein-host init [--handle host]` (#90, #142): the first run's part alone — the host skein made through
597
+ * a router of this command's own (closed afterwards; the running router hydrates it on its first
598
+ * request). Once: a second `init` prints which instance is the host skein.
599
+ */
600
+ async function initCmd(db, rest, env) {
601
+ const { values: v, positionals } = parseArgs({ args: rest, allowPositionals: true, options: { handle: { type: "string" } } });
602
+ if (positionals.length) {
603
+ env.err(USAGE);
604
+ return 2;
605
+ }
606
+ const had = db.hostSkein();
607
+ if (had) {
608
+ if (v.handle !== undefined && v.handle !== had.handle) {
609
+ env.err(`skein-host init: the host skein is ${had.handle} already`);
610
+ return 1;
611
+ }
612
+ env.out(`the host skein: ${had.handle}@${had.domain} (${had.identity ? short(had.identity) : "-"}) ${had.status}`);
613
+ return 0;
614
+ }
615
+ if (await routerAt(env.vars)) {
616
+ env.err("skein-host init: a router serves this host: `skein-host run` makes the host skein on its first run (restart it)");
617
+ return 1;
618
+ }
619
+ const router = new Router({ ...routerOptions(db, env), idleMs: 0, cron: false });
620
+ try {
621
+ await createHostSkein(router, db, v.handle ?? "host", env);
622
+ return 0;
623
+ }
624
+ catch (e) {
625
+ env.err(`skein-host init: ${e.message}`);
626
+ return 1;
627
+ }
628
+ finally {
629
+ await router.close();
630
+ }
631
+ }
632
+ /**
633
+ * `skein-host grant <key> [--role <role>] [--remove] [--instance <handle>] [--dry-run]` (#142, #143): the
634
+ * kernel's `grant` operation — root (or `<app>.<role>`) to the key — in the host skein (or `handle`): the
635
+ * operator's message (src/client/admin.ts planGrant), signed with its key and handed to the running host
636
+ * over its control socket.
637
+ */
638
+ async function grantCmd(db, rest, env) {
639
+ const { values: v, positionals: [key, ...more] } = parseArgs({ args: rest, allowPositionals: true, options: { role: { type: "string" }, remove: { type: "boolean" }, instance: { type: "string" }, "dry-run": { type: "boolean" } } });
640
+ if (!key || more.length) {
641
+ env.err(USAGE);
642
+ return 2;
643
+ }
644
+ const handle = v.instance ?? db.hostSkein()?.handle;
645
+ if (!handle) {
646
+ env.err("skein-host grant: no host skein yet (skein-host run)");
647
+ return 1;
648
+ }
649
+ const { planGrant, messageJson } = await import("../client/admin.js");
650
+ const { localTarget } = await import("../client/target.js");
651
+ const { keyWallet } = await import("../wallet.js");
652
+ let t;
653
+ try {
654
+ t = localTarget(env.vars, handle, keyWallet(operatorKey(env.vars).key));
655
+ const p = await planGrant(await t.view(), key, { ...(v.role ? { role: v.role } : {}), ...(v.remove ? { remove: true } : {}) });
656
+ for (const l of p.prompt)
657
+ env.out(l);
658
+ if (v["dry-run"]) {
659
+ for (const m of p.messages)
660
+ env.out(messageJson(p.recipient, m));
661
+ return 0;
662
+ }
663
+ for (const m of p.messages)
664
+ await t.send(m.box, m.body);
665
+ env.out(`${handle}: ${p.messages.length ? `${v.role ?? "root"} ${v.remove ? "revoked from" : "granted to"}` : "nothing to change for"} ${short(key)}`);
666
+ return 0;
667
+ }
668
+ catch (e) {
669
+ env.err(`skein-host grant: ${e.message}`);
670
+ return 1;
671
+ }
672
+ finally {
673
+ await t?.close();
674
+ }
675
+ }
676
+ /**
677
+ * `skein-host add <handle> --mailbox --owner <hex> [--domain d]` (#40, #113):
678
+ * Router.createInstance with image `mailbox` — what the instance manager does
679
+ * for the onboarding app's registrations — through a router of this
680
+ * command's own. The same owner and handle again: kept (printed).
681
+ */
682
+ async function mailboxCmd(db, handle, owner, domain, env) {
683
+ const router = new Router({ ...routerOptions(db, env), idleMs: 0, cron: false });
684
+ try {
685
+ const c = await router.createInstance(handle, owner, { image: "mailbox", ...(domain ? { domain } : {}) });
686
+ const r = db.get(handle);
687
+ env.out(`${r.handle}@${r.domain} ${r.status} · store ${r.store} · ${short(c.identity)} · mailbox instance for ${short(owner)} at ${c.url}`);
688
+ return 0;
689
+ }
690
+ catch (e) {
691
+ env.err(`skein-host add --mailbox: ${e.message}`);
692
+ return 1;
693
+ }
694
+ finally {
695
+ await router.close();
696
+ }
697
+ }
698
+ /** An outpoint as an image is named on chain: `<txid>_<vout>` (or `.`/`:`). */
699
+ const isOutpoint = (s) => /^[0-9a-f]{64}[_.:][0-9]+$/.test(s);
700
+ /**
701
+ * `skein-host import-handles` (#113): the mailbox instances host.db has and
702
+ * the host skein's onboarding app does not (a registration before the app
703
+ * took them; its public resolve says), each printed with the request that
704
+ * adopts it: `{fn: "onboard.adopt", args: {handle, owner}}` POSTed to the
705
+ * host skein's /onboard/call on the owner's session — the owner's wallet
706
+ * sends it (#124: `1sat authfetch POST <origin>/onboard/call --body '…'`).
707
+ * The app asks the instance manager (which answers for the existing row, at
708
+ * the app's domain), issues the certificate and records it, so the handle
709
+ * resolves. The running router serves the host skein.
710
+ */
711
+ async function importHandlesCmd(db, rest, env) {
712
+ if (rest.length) {
713
+ env.err(USAGE);
714
+ return 2;
715
+ }
716
+ const host = db.hostSkein();
717
+ if (!host) {
718
+ env.err("skein-host import-handles: no host skein (skein-host init)");
719
+ return 1;
720
+ }
721
+ const local = `${(env.vars.SKEIN_HOST_URL || `http://127.0.0.1:${env.vars.SKEIN_ROUTER_PORT || 8100}`).replace(/\/+$/, "")}/@${host.handle}`;
722
+ const origin = originOf(env.vars, host.handle);
723
+ let n = 0;
724
+ for (const r of db.list().filter((x) => x.kind === "mailbox" && x.owner)) {
725
+ const known = await fetch(`${local}/onboard/resolve?handle=${encodeURIComponent(r.handle)}`).then((x) => x.json()).catch(() => ({ identityKey: undefined }));
726
+ if (known.identityKey === r.owner)
727
+ continue;
728
+ n++;
729
+ env.out(`1sat authfetch POST ${origin}/onboard/call --body '${JSON.stringify({ fn: "onboard.adopt", args: { handle: r.handle, owner: r.owner } })}'`);
730
+ }
731
+ env.err(n ? `${n} mailbox instance${n === 1 ? "" : "s"} to adopt: the owner's wallet sends each line above` : "every mailbox instance is recorded");
732
+ return 0;
733
+ }
734
+ /** The URL of a router answering for this host (its /manifest.json), if one does. */
735
+ async function routerAt(vars) {
736
+ const base = (vars.SKEIN_HOST_URL || `http://127.0.0.1:${vars.SKEIN_ROUTER_PORT || 8100}`).replace(/\/+$/, "");
737
+ try {
738
+ const r = await fetch(`${base}/manifest.json`, { signal: AbortSignal.timeout(2000) });
739
+ const m = await r.json();
740
+ return r.ok && m?.metanet ? base : undefined;
741
+ }
742
+ catch {
743
+ return undefined;
744
+ } // nothing answering there (or not a router): the command runs its own
745
+ }
746
+ /**
747
+ * IDENTITY.md's fields per row, for ROSTER.md: from `pending` (the directory
748
+ * about to be deployed into that row), else the row's deployed tree in its
749
+ * store, else (not admitted yet) its `source` directory; else empty.
750
+ */
751
+ function identities(env, pending = new Map()) {
752
+ const seen = new Map();
753
+ const fromDir = (dir) => {
754
+ const p = join(dir, "IDENTITY.md");
755
+ return existsSync(p) ? parseIdentity(readFileSync(p, "utf8")) : undefined;
756
+ };
757
+ return async (row) => {
758
+ let f = seen.get(row.handle);
759
+ if (f)
760
+ return f;
761
+ const dir = pending.get(row.handle);
762
+ if (dir)
763
+ f = fromDir(dir);
764
+ else {
765
+ const s = openRow(row, env);
766
+ try {
767
+ f = await deployedIdentity(row, s.blocks);
768
+ }
769
+ finally {
770
+ await s.close?.();
771
+ }
772
+ if (!f && row.source)
773
+ f = fromDir(row.source);
774
+ }
775
+ f ??= { displayName: "", description: "" };
776
+ seen.set(row.handle, f);
777
+ return f;
778
+ };
779
+ }
780
+ async function rosterCmd(db, rest, env) {
781
+ const { values: v, positionals } = parseArgs({ args: rest, allowPositionals: true, options: { for: { type: "string" } } });
782
+ if (positionals.length) {
783
+ env.err(USAGE);
784
+ return 2;
785
+ }
786
+ const rows = db.list("enabled");
787
+ if (v.for !== undefined) {
788
+ const row = db.get(v.for);
789
+ if (!row) {
790
+ env.err(`skein-host roster: no instance ${v.for}`);
791
+ return 1;
792
+ }
793
+ const text = await rosterFor(row, rows, identities(env));
794
+ if (text)
795
+ env.out(text.trimEnd());
796
+ else
797
+ env.err(`${row.handle} knows nobody: no ROSTER.md`);
798
+ return 0;
799
+ }
800
+ env.out(JSON.stringify(await roster(rows, async (row) => openRow(row, env), () => false), null, 2));
801
+ return 0;
802
+ }
803
+ /** A row's store to read while something else may be writing it: read-only, if it exists. */
804
+ function openRow(row, env) {
805
+ const given = env.store?.(row);
806
+ if (given)
807
+ return { blocks: given };
808
+ if (!storeExists(storeSpec(row)))
809
+ return {};
810
+ const s = openStoreFile(storeSpec(row), { readOnly: true });
811
+ return { blocks: s, close: () => s.close() };
812
+ }
813
+ const ROOT = resolve(dirname(new URL(import.meta.url).pathname), "../..");
814
+ /**
815
+ * The signer (#18, signer.ts): every instance's wallet is a ProtoWallet over a
816
+ * key derived from the router's master secret (key ID = the handle); the
817
+ * router's BRC-104 identity is another child of it, and so are its providers'
818
+ * keys (#70: the HTTP proxy, the waker, the cron provider, the libp2p node, the status provider,
819
+ * the instance manager, and the certifier, #113: its key the certifier key, BRC-169's trust anchor).
820
+ */
821
+ function wallets(v, home) {
822
+ const signer = signerOf(v, home); // #160: one-key mode (SKEIN_KEY_FILE / SKEIN_KEY), else the master key's
823
+ return {
824
+ walletFor: (row) => signer.wallet(row.handle), peerKeyFor: (handle) => signer.peerKey(handle), providerKeyFor: (name) => signer.providerKey(name),
825
+ // #162: a one-key host claims the instances it creates with its own key.
826
+ ...(signer.shared ? { claimer: signer.wallet("") } : {}),
827
+ };
828
+ }
829
+ /** The router's options from the environment (`run`, and `add --boot/--packet`, which boots through it). */
830
+ function routerOptions(db, env) {
831
+ const v = env.vars;
832
+ const home = homeOf(v);
833
+ const named = (s, d) => { const [handle, domain = "localhost"] = (s || d).split("@"); return { handle: handle, domain }; };
834
+ return {
835
+ db, ...wallets(v, home),
836
+ owner: v.SKEIN_OWNER, infer: v.SKEIN_INFER, ownerHandle: named(v.SKEIN_OWNER_HANDLE, "david@localhost"), inferHandle: named(v.SKEIN_INFER_HANDLE, "infer@localhost"),
837
+ fuelPerStep: v.SKEIN_FUEL_PER_STEP, idleMs: v.SKEIN_IDLE_MS !== undefined ? Number(v.SKEIN_IDLE_MS) : undefined, home,
838
+ origin: v.SKEIN_ROUTER_ORIGIN, instanceOrigin: v.SKEIN_INSTANCE_ORIGIN, ownerMessagebox: v.SKEIN_OWNER_MESSAGEBOX, port: Number(v.SKEIN_ROUTER_PORT ?? 8100),
839
+ network: hostNetwork(v),
840
+ kernel: { command: v.SKEIN_KERNEL_BIN, env: { SKEIN_HOME: home } },
841
+ ...(v.SKEIN_STORE ? { lmdbStore: resolve(v.SKEIN_STORE) } : {}),
842
+ ...(v.SKEIN_STORE_ROCKSDB ? { rocksStore: resolve(v.SKEIN_STORE_ROCKSDB) } : {}), // #158
843
+ ...((s) => (s ? { sqliteStore: s } : {}))(sqliteStoreOf(v, home)), // the settled split: the wasm kernel
844
+ // #162: the instances besides the host skein the instance manager takes messages from; the images a create names.
845
+ ...(v.SKEIN_MANAGER_FOR ? { managerFor: handles(v.SKEIN_MANAGER_FOR) } : {}),
846
+ ...(v.SKEIN_ROOT_INSTANCE ? { rootInstance: v.SKEIN_ROOT_INSTANCE } : {}),
847
+ imagesDir: resolve(v.SKEIN_IMAGES_DIR || join(home, "images")),
848
+ // #142: settings host.env may give (the environment's are read where they are used, too).
849
+ ...(v.SKEIN_HTTP === "fetch" ? { http: fetchHttp } : {}),
850
+ ...(Number(v.SKEIN_ANSWER_WAIT_MS) > 0 ? { answerWaitMs: Number(v.SKEIN_ANSWER_WAIT_MS) } : {}),
851
+ libp2p: hostP2PConfig(v, home),
852
+ headersFeed: v.SKEIN_HEADERS_URL || undefined,
853
+ arc: hostArcConfig(v, home),
854
+ billing: billingConfig(v),
855
+ log: (source, line) => env.out(`[${source}] ${line}`),
856
+ };
857
+ }
858
+ /** `skein-host billing [handle]` (#130): what host.db holds of billing — the instances, their ticks, their payments. */
859
+ function billingCmd(db, rest, env) {
860
+ const [handle] = rest;
861
+ const sats = (n) => n === null ? "-" : `${(BigInt(n) / NSAT).toString()}.${(BigInt(n) % NSAT).toString().padStart(9, "0")}`;
862
+ for (const b of db.billings()) {
863
+ if (handle && b.instance !== handle)
864
+ continue;
865
+ env.out([b.instance, `host ${b.host?.slice(0, 8) ?? "-"}`, `tally ${sats(b.tally)}`, `allocation ${sats(b.allocation)}`, b.asleep_since !== null ? `asleep since ${new Date(b.asleep_since).toISOString()}` : "awake", b.mismatch ? `not served: ${b.mismatch}` : "", b.updated_at].filter(Boolean).join("\t"));
866
+ if (!handle)
867
+ continue;
868
+ for (const t of db.billingTicks(b.instance))
869
+ env.out(` tick ${new Date(t.at).toISOString()} log ${t.log}`);
870
+ for (const p of db.billingPayments(b.instance))
871
+ env.out(` payment ${p.txid} ${p.amount} sats ${new Date(p.at).toISOString()}${p.ours ? "" : " (not to this host's key)"}${p.checkpoint ? ` checkpoint ${p.checkpoint}` : ""}`);
872
+ }
873
+ return 0;
874
+ }
875
+ /**
876
+ * `skein-host reclaim [--grace ms] [--yes]` (#130 decided 9): the instances asleep longer than the
877
+ * grace (host.db: since when the router saw it asleep), listed; with --yes each is reclaimed —
878
+ * through the running router's control socket when one runs (it holds their kernels), else here.
879
+ * The host's call to make: nothing reclaims on its own.
880
+ */
881
+ async function reclaimCmd(db, rest, env) {
882
+ const { values: v, positionals } = parseArgs({ args: rest, allowPositionals: true, options: { grace: { type: "string" }, yes: { type: "boolean" } } });
883
+ if (positionals.length) {
884
+ env.err(USAGE);
885
+ return 2;
886
+ }
887
+ const grace = v.grace !== undefined ? Number(v.grace) : (billingConfig(env.vars) ?? DEV_BILLING).graceMs;
888
+ if (!Number.isFinite(grace) || grace < 0) {
889
+ env.err("skein-host reclaim --grace: ms");
890
+ return 2;
891
+ }
892
+ const due = db.reclaimable(Date.now(), grace).filter((r) => r.instance !== db.hostSkein()?.handle);
893
+ for (const r of due)
894
+ env.out(`${r.instance}\tasleep since ${new Date(r.asleep_since).toISOString()}`);
895
+ if (!v.yes) {
896
+ env.out(`${due.length} past the grace (${grace} ms)${due.length ? "; --yes reclaims them" : ""}`);
897
+ return 0;
898
+ }
899
+ const sock = join(homeOf(env.vars), CONTROL_SOCKET);
900
+ let failed = 0;
901
+ for (const r of due) {
902
+ try {
903
+ const answer = await controlRequest(sock, { op: "reclaim", handle: r.instance });
904
+ if (answer && !answer.ok)
905
+ throw new Error(answer.error);
906
+ if (!answer) {
907
+ if (await routerAt(env.vars))
908
+ throw new Error("a router serves this host with no control socket here: it holds the store; reclaim through it");
909
+ const router = new Router({ ...routerOptions(db, env), idleMs: 0, cron: false });
910
+ try {
911
+ await router.reclaim(r.instance);
912
+ }
913
+ finally {
914
+ await router.close();
915
+ }
916
+ }
917
+ env.out(`${r.instance}: reclaimed`);
918
+ }
919
+ catch (e) {
920
+ failed++;
921
+ env.err(`skein-host reclaim ${r.instance}: ${e.message}`);
922
+ }
923
+ }
924
+ return failed ? 1 : 0;
925
+ }
926
+ // ---------------------------------------------------------------- bootstrap (#4)
927
+ /** The default image (#89): one genesis for everyone, no owner in it, a claim row (boot.ts). */
928
+ export { DEFAULT_IMAGE };
929
+ /** The source `add --boot/--packet` names (boot.ts): a directory, a tree CID in a store, or a packet file. */
930
+ async function bootSourceOf(v, row) {
931
+ if (v.packet) {
932
+ const { rootsTracker } = await import("./packet.js");
933
+ return packetSource(new Uint8Array(readFileSync(v.packet)), {
934
+ scope: v.scope ? parseCid(v.scope) : undefined,
935
+ chainTracker: v.proofs ? rootsTracker(JSON.parse(readFileSync(v.proofs, "utf8"))) : undefined,
936
+ });
937
+ }
938
+ const spec = v.boot;
939
+ if (existsSync(spec) && statSync(spec).isDirectory())
940
+ return await imageSource(resolve(spec));
941
+ const root = parseCid(spec);
942
+ const path = v.from ?? storeSpec(row);
943
+ if (!storeExists(path))
944
+ throw new Error(`--boot ${spec}: no store at ${path} to read the tree from (--from)`);
945
+ const s = openStoreFile(path, { readOnly: true });
946
+ // The objects are read now: the row's own store is about to be written by its kernel.
947
+ const blocks = new MemBlocks();
948
+ const { closure } = await import("./packet.js");
949
+ const src = storeObjects(s);
950
+ const c = await closure(root, (x) => src.get(x));
951
+ s.close();
952
+ for (const b of c.blocks)
953
+ await blocks.putBlock(b.cid, b.bytes);
954
+ return { kind: "tree", root, objects: anyOf(blocks, wasmDirObjects(WASM_DIR)) };
955
+ }
956
+ /** `skein-host system <dir>`: the stock system (what code genesis writes) as a system tree to start from. */
957
+ async function systemCmd(rest, env) {
958
+ const [dir, ...more] = rest;
959
+ if (!dir || more.length) {
960
+ env.err(USAGE);
961
+ return 2;
962
+ }
963
+ const tmp = mkdtempSync(join(tmpdir(), "skein-system-"));
964
+ const k = new Kernel({ db: join(tmp, "k.db"), handle: "system", domain: "localhost", command: env.vars.SKEIN_KERNEL_BIN, env: { SKEIN_HOME: tmp } });
965
+ try {
966
+ const files = await stockSystemFiles(k);
967
+ for (const [p, text] of Object.entries(files)) {
968
+ mkdirSync(dirname(join(dir, p)), { recursive: true });
969
+ writeFileSync(join(dir, p), text);
970
+ }
971
+ env.out(`${dir}: ${Object.keys(files).length} files (${Object.keys(files).filter((f) => f.endsWith(".cid")).length} programs, etc/config.json, etc/dispatch.json)`);
972
+ return 0;
973
+ }
974
+ finally {
975
+ await k.stop(5000);
976
+ rmSync(tmp, { recursive: true, force: true });
977
+ }
978
+ }
979
+ /** An index node or a state record (#30): history a checkpoint leaves behind (dump.zig isIndexBlock). */
980
+ function isIndexBlock(cid, bytes) {
981
+ if (cid.code !== 0x71)
982
+ return false;
983
+ let v;
984
+ try {
985
+ v = decodeCbor(bytes);
986
+ }
987
+ catch {
988
+ return false;
989
+ } // not dag-cbor: not an index block
990
+ if (v && typeof v === "object" && !Array.isArray(v))
991
+ return v.kind === "skein-state";
992
+ if (!Array.isArray(v) || v.length !== 2 || !Array.isArray(v[1]) || (v[0] !== null && !CIDClass.asCID(v[0])))
993
+ return false;
994
+ return v[1].length > 0 && v[1].every((e) => Array.isArray(e) && e.length === 3 && e[0] instanceof Uint8Array && (e[2] === null || !!CIDClass.asCID(e[2])));
995
+ }
996
+ /** Every block of a store except the index's (nodes and state records): a checkpoint's extras candidates. A file, or an LMDB set's members (#96). */
997
+ function looseRecords(spec) {
998
+ const out = [];
999
+ for (const { cid, bytes } of storeBlocks(spec))
1000
+ if (!isIndexBlock(cid, bytes))
1001
+ out.push({ cid, bytes });
1002
+ return out;
1003
+ }
1004
+ /** `skein-host pack <handle|dir|tree-cid> <out>`: a packet (packet.ts) of a system tree or, with --checkpoint, a whole instance. */
1005
+ async function packCmd(db, rest, env) {
1006
+ const { values: v, positionals: [what, out, ...more] } = parseArgs({
1007
+ args: rest, allowPositionals: true,
1008
+ options: { from: { type: "string" }, checkpoint: { type: "boolean" }, form: { type: "string" }, "no-index": { type: "boolean" }, mined: { type: "string" }, tree: { type: "string" } },
1009
+ });
1010
+ if (!what || !out || more.length || (v.form !== undefined && v.form !== "ordfs" && v.form !== "git")) {
1011
+ env.err(USAGE);
1012
+ return 2;
1013
+ }
1014
+ const { writePacket } = await import("./packet.js");
1015
+ let scope, objects, close = () => { }, extras;
1016
+ const row = db.get(what);
1017
+ if (row) {
1018
+ if (!storeExists(storeSpec(row))) {
1019
+ env.err(`skein-host pack: ${row.handle} has no store yet`);
1020
+ return 1;
1021
+ }
1022
+ const s = openStoreFile(storeSpec(row), { readOnly: true });
1023
+ close = () => s.close();
1024
+ objects = anyOf(storeObjects(s), wasmDirObjects(WASM_DIR));
1025
+ if (v.checkpoint) {
1026
+ if (!s.state) {
1027
+ env.err(`skein-host pack: ${row.handle}'s store predates the state record (#30)`);
1028
+ close();
1029
+ return 1;
1030
+ }
1031
+ scope = s.state().cid;
1032
+ extras = looseRecords(storeSpec(row));
1033
+ }
1034
+ else if (v.tree)
1035
+ scope = parseCid(v.tree);
1036
+ else {
1037
+ // The system tree it booted from, else its `main`.
1038
+ const tip = await s.log.tip();
1039
+ let first = tip ? await s.get(tip) : undefined;
1040
+ while (first && first.prev)
1041
+ first = await s.get(first.prev);
1042
+ const g = first?.genesis ? await s.get(first.genesis) : undefined;
1043
+ const main = await headTree(s, MAIN);
1044
+ const t = g?.tree ?? main;
1045
+ if (!t) {
1046
+ env.err(`skein-host pack: ${row.handle} has no system tree and no main (--tree, or --checkpoint)`);
1047
+ close();
1048
+ return 1;
1049
+ }
1050
+ scope = t;
1051
+ }
1052
+ }
1053
+ else if (existsSync(what) && statSync(what).isDirectory()) {
1054
+ const d = await dirSource(resolve(what));
1055
+ scope = d.root;
1056
+ objects = anyOf(d.objects, wasmDirObjects(WASM_DIR));
1057
+ }
1058
+ else {
1059
+ scope = parseCid(what);
1060
+ if (!v.from) {
1061
+ env.err("skein-host pack <tree-cid>: --from <store.db> (where the objects are)");
1062
+ return 2;
1063
+ }
1064
+ const s = openStoreFile(v.from, { readOnly: true });
1065
+ close = () => s.close();
1066
+ objects = anyOf(storeObjects(s), wasmDirObjects(WASM_DIR));
1067
+ }
1068
+ try {
1069
+ const w = await writePacket(scope, objects, { form: v.form, noIndex: v["no-index"], mined: v.mined !== undefined, extras });
1070
+ writeFileSync(out, w.bytes);
1071
+ if (v.mined)
1072
+ writeFileSync(v.mined, `${JSON.stringify(w.roots, null, 2)}\n`);
1073
+ env.out(`${out}: scope ${scope} · ${w.objects} objects in ${w.txids.length} transaction(s) (synthetic, not broadcast) · ${w.bytes.length} bytes${v.mined ? ` · header roots in ${v.mined}` : ""}`);
1074
+ return 0;
1075
+ }
1076
+ catch (e) {
1077
+ env.err(`skein-host pack: ${e.message}`);
1078
+ return 1;
1079
+ }
1080
+ finally {
1081
+ close();
1082
+ }
1083
+ }
1084
+ /** Whether something answers on the control socket (a running host holds its instances' kernels). */
1085
+ function controlAlive(sock) {
1086
+ if (!existsSync(sock))
1087
+ return Promise.resolve(false);
1088
+ return new Promise((resolve) => {
1089
+ const c = connect(sock);
1090
+ c.once("connect", () => { c.destroy(); resolve(true); });
1091
+ c.once("error", () => resolve(false));
1092
+ });
1093
+ }
1094
+ /**
1095
+ * `skein-host migrate-store <handle>` (#96): the row's runtime.db copied into its set of the host's
1096
+ * LMDB environment (SKEIN_STORE) by `skein-kernel migrate` (blocks, the state pointer, the size); the
1097
+ * row then names the set (`lmdb:<dir>`) and the file is kept beside, renamed
1098
+ * runtime.db.migrated-<date> (never deleted here). Only with the host stopped: its kernel would
1099
+ * hold the store; a running host (its control socket or router answering) is refused.
1100
+ */
1101
+ async function migrateStoreCmd(db, rest, env) {
1102
+ const [handle, ...more] = rest;
1103
+ if (!handle || more.length) {
1104
+ env.err(USAGE);
1105
+ return 2;
1106
+ }
1107
+ // #158: SKEIN_STORE_ROCKSDB the target (from a file or an LMDB set), else SKEIN_STORE's LMDB environment (from a file).
1108
+ const rocks = env.vars.SKEIN_STORE_ROCKSDB ? resolve(env.vars.SKEIN_STORE_ROCKSDB) : undefined;
1109
+ const dir = rocks ?? (env.vars.SKEIN_STORE ? resolve(env.vars.SKEIN_STORE) : undefined);
1110
+ if (!dir) {
1111
+ env.err("skein-host migrate-store: neither SKEIN_STORE_ROCKSDB (the host's RocksDB database) nor SKEIN_STORE (its LMDB environment) is set");
1112
+ return 2;
1113
+ }
1114
+ const row = db.get(handle);
1115
+ if (!row) {
1116
+ env.err(`skein-host migrate-store: no instance ${handle}`);
1117
+ return 1;
1118
+ }
1119
+ if (isRocks(row.store) || (!rocks && isLmdb(row.store))) {
1120
+ env.err(`skein-host migrate-store: ${handle}'s store is a set already (${row.store})`);
1121
+ return 1;
1122
+ }
1123
+ if (!isLmdb(row.store) && !existsSync(row.store)) {
1124
+ env.err(`skein-host migrate-store: ${handle} has no store at ${row.store}`);
1125
+ return 1;
1126
+ }
1127
+ if (await controlAlive(join(homeOf(env.vars), CONTROL_SOCKET)) || await routerAt(env.vars)) {
1128
+ env.err("skein-host migrate-store: the host is running (its kernel holds the store): stop it first, migrate, then start it");
1129
+ return 1;
1130
+ }
1131
+ const target = rocks ? `rocksdb:${dir}` : dir;
1132
+ const r = spawnSync(env.vars.SKEIN_KERNEL_BIN || KERNEL_BIN, ["migrate", "--from", storeSpec(row), "--into", target, "--set", handle], { encoding: "utf8", maxBuffer: 1 << 26 });
1133
+ if (r.status !== 0) {
1134
+ env.err(`skein-host migrate-store ${handle}: ${(r.stderr || r.stdout || `exit ${r.status}`).trim()}`);
1135
+ return 1;
1136
+ }
1137
+ const store = rocks ? `rocksdb:${dir}` : `lmdb:${dir}`;
1138
+ if (isLmdb(row.store)) {
1139
+ // An LMDB set: left in its environment (the host writes none, as reclaim leaves it).
1140
+ db.add(handle, { store });
1141
+ env.out(`${handle}: migrated into ${store} (set ${handle}) ${r.stdout.trim()} · its LMDB set left in ${row.store}`);
1142
+ return 0;
1143
+ }
1144
+ const kept = `${row.store}.migrated-${new Date().toISOString().slice(0, 10)}`;
1145
+ for (const ext of ["", "-wal", "-shm"])
1146
+ if (existsSync(`${row.store}${ext}`))
1147
+ renameSync(`${row.store}${ext}`, `${kept}${ext}`);
1148
+ db.add(handle, { store });
1149
+ env.out(`${handle}: migrated into ${store} (set ${handle}) ${r.stdout.trim()} · the file kept as ${kept}`);
1150
+ return 0;
1151
+ }
1152
+ /**
1153
+ * `skein-host run`: the router (router.ts) on SKEIN_ROUTER_PORT (default
1154
+ * 8100, the messagebox URL clients already use), every enabled row hydrated
1155
+ * once at start (stopped only past SKEIN_IDLE_MS), and the host page and roster on
1156
+ * SKEIN_HOST_PORT. A skein's explorer is its own (`/explore`, its owner's),
1157
+ * read by the management site (shruggr/skein-site, #92).
1158
+ */
1159
+ export async function runHost(db, env, o = {}) {
1160
+ const v = env.vars;
1161
+ const home = homeOf(v);
1162
+ const router = new Router({ ...routerOptions(db, env), ...o.router });
1163
+ const mport = Number(v.SKEIN_ROUTER_PORT ?? 8100);
1164
+ const binds = (v.SKEIN_ROUTER_BIND ?? "").split(",").map((x) => x.trim()).filter(Boolean);
1165
+ const mserver = await router.listen(mport, binds.length ? binds : undefined).then((s) => s, (e) => { env.err(`skein-host: router: ${e.message}`); return undefined; });
1166
+ // The control socket (#60): `skein-host event` reaches this router's kernels through it.
1167
+ const sock = join(home, CONTROL_SOCKET);
1168
+ await router.listenControl(sock).then(() => env.out(`skein-host: control socket at ${sock}`), (e) => env.err(`skein-host: control socket: ${e.message}`));
1169
+ const messagebox = mserver ? mserver.address().port : undefined;
1170
+ if (messagebox !== undefined)
1171
+ env.out(`skein-host: router at http://127.0.0.1:${messagebox} · an instance at ${router.originOf("<handle>")} (or /@<handle>)`);
1172
+ env.out(`skein-host: network ${router.o.network} (SKEIN_NETWORK): every new genesis's defaults.walletNetwork, the chain app's network`);
1173
+ const omb = router.ownerMessagebox();
1174
+ if (omb)
1175
+ env.out(`skein-host: the owner's messagebox for new geneses: ${omb}`);
1176
+ else if (v.SKEIN_OWNER)
1177
+ env.err(`skein-host: WARNING: the owner ${short(v.SKEIN_OWNER)} has no mailbox instance here and SKEIN_OWNER_MESSAGEBOX is unset: a new agent's genesis names no owner messagebox, and its answers cannot be delivered (scripts/host/up.sh makes the mailbox first)`);
1178
+ if (router.arc)
1179
+ env.out(`skein-host: broadcaster: broadcast events → Arcade ${router.o.arc.url}; proofs and statuses (the status provider) from ${router.arc.eventsUrl()}${router.o.arc.callbackUrl ? ` and webhooks at /arc/callback (${router.o.arc.callbackUrl})` : ""}`);
1180
+ else
1181
+ env.out("skein-host: no Arcade (SKEIN_ARC_URL): a broadcast event is dropped; a new genesis names no status provider");
1182
+ await router.start();
1183
+ // #142: the first run makes the host skein (owned by the operator's key); later runs start what exists.
1184
+ if (!db.hostSkein() && o.firstRun !== false) {
1185
+ try {
1186
+ await createHostSkein(router, db, "host", env);
1187
+ }
1188
+ catch (e) {
1189
+ env.err(`skein-host: the host skein: ${e.message}`);
1190
+ }
1191
+ }
1192
+ const enabled = db.list("enabled");
1193
+ env.out(`skein-host: routing for ${enabled.length} enabled instances (${enabled.map((r) => r.handle).join(", ") || "none"})`);
1194
+ const hs = db.hostSkein();
1195
+ env.out(hs ? `skein-host: the host skein is ${hs.handle} (${hs.identity ? short(hs.identity) : "-"}) at ${router.originOf(hs.handle)}: the instance manager takes its messages only` : "skein-host: no host skein: the instance manager acts for nobody");
1196
+ const live = (row) => router.loaded.has(row.handle);
1197
+ const port = Number(v.SKEIN_HOST_PORT || 4600);
1198
+ const page = async () => hostPage(db.list("enabled").map((r) => {
1199
+ const l = router.loaded.get(r.handle);
1200
+ return {
1201
+ handle: r.handle, domain: r.domain, identity: r.identity ?? "", status: l ? "live" : "idle",
1202
+ store: r.store, tree: r.tree ?? "", pid: l?.kernel.pid, restarts: 0, origin: router.originOf(r.handle),
1203
+ wake: router.nextWake(r.handle),
1204
+ };
1205
+ }), { router: messagebox !== undefined ? `http://127.0.0.1:${messagebox}` : undefined, originOf: (h) => router.originOf(h), mailboxes: db.list("enabled").filter((r) => r.kind === "mailbox") });
1206
+ const server = await serveRoster(port, () => roster(db.list("enabled"), async (row) => openRow(row, env), live), "127.0.0.1", page)
1207
+ .then((s) => { env.out(`skein-host: host page at http://127.0.0.1:${s.address().port}/ · roster at /roster.json`); return s; }, (e) => { env.err(`skein-host: host server: ${e.message}`); return undefined; });
1208
+ return {
1209
+ router, messagebox, server, port: server ? server.address().port : undefined,
1210
+ async stop() {
1211
+ server?.close();
1212
+ await router.close();
1213
+ },
1214
+ };
1215
+ }
1216
+ /** `skein-host run`: runHost until SIGINT/SIGTERM, then stop every kernel and exit. */
1217
+ async function run(db, rest, env) {
1218
+ const { positionals } = parseArgs({ args: rest, allowPositionals: true, options: {} });
1219
+ if (positionals.length) {
1220
+ env.err(USAGE);
1221
+ db.close();
1222
+ return 2;
1223
+ }
1224
+ const host = await runHost(db, env);
1225
+ let stopping = false;
1226
+ const stop = async (sig) => {
1227
+ if (stopping)
1228
+ return;
1229
+ stopping = true;
1230
+ env.out(`${sig}: stopping`);
1231
+ await host.stop();
1232
+ db.close();
1233
+ process.exit(0);
1234
+ };
1235
+ process.on("SIGINT", () => void stop("SIGINT"));
1236
+ process.on("SIGTERM", () => void stop("SIGTERM"));
1237
+ return 0;
1238
+ }
1239
+ if (import.meta.main) {
1240
+ process.exitCode = await main(process.argv.slice(2), { vars: process.env, out: (l) => process.stdout.write(`${l}\n`), err: (l) => process.stderr.write(`${l}\n`) });
1241
+ }
1242
+ /**
1243
+ * The host's SQLite database for new instances (the settled split, 2026-10-31): SKEIN_STORE_SQLITE, else
1244
+ * $SKEIN_HOME/store.sqlite when this installation has no native kernel binary (the npm package ships the
1245
+ * wasm kernel only) and no other shared store is named; else none (a native kernel per instance store).
1246
+ */
1247
+ export function sqliteStoreOf(v, home) {
1248
+ if (v.SKEIN_STORE_SQLITE)
1249
+ return resolve(v.SKEIN_STORE_SQLITE);
1250
+ if (v.SKEIN_STORE_ROCKSDB || v.SKEIN_STORE)
1251
+ return undefined;
1252
+ return existsSync(v.SKEIN_KERNEL_BIN || KERNEL_BIN) ? undefined : join(home, "store.sqlite");
1253
+ }