@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,430 @@
1
+ // The host's management database (#23): which instances this host runs, one
2
+ // row each, in $SKEIN_HOME/host.db. Host-side state, outside every instance's
3
+ // graph: nothing in src/runtime reads it. Rows get here by hand (`skein-host
4
+ // add`), by the instance manager (`create`, #90), or by registration; `skein-host
5
+ // run` (the router, router.ts) serves every enabled row.
6
+ //
7
+ // #40: a row is an agent (its own identity) or a **mailbox instance** — an
8
+ // instance whose system is only the front door and the messagebox, keeping
9
+ // mail for an identity outside the host (`owner`: a user's wallet, the
10
+ // inference peer). There is no messagebox host: every identity's mailbox is
11
+ // an instance at its own origin. Beside the rows, the **fuel ledger**: what
12
+ // each caller's calls (#40: the front door's reads, which the log never sees)
13
+ // cost, per instance and op. And the broadcaster's three (#58, #65, arc.ts):
14
+ // the last event id taken from Arcade's stream, the statuses already routed,
15
+ // and the queue of broadcasts Arcade has not taken yet. And the cron
16
+ // provider's one (#69, cron.ts): the schedules the instances asked for.
17
+ // And the host's own settings (#90): which row is the host skein — the
18
+ // operator's instance (`skein-host init`), whose identity alone reaches the
19
+ // instance manager. And billing's (#130, billing.ts): each billed instance as
20
+ // the host last read it (asleep since when: the grace), the periods it ticked
21
+ // (each period's log record, which its tick commits to), and the payments its
22
+ // wallet made (the `payment` events). And overlay sync's: the peers each
23
+ // instance's overlay topics heard beats from (`topic_peers`).
24
+ import { DatabaseSync } from "node:sqlite";
25
+ const DDL = `
26
+ CREATE TABLE IF NOT EXISTS instances (
27
+ handle TEXT PRIMARY KEY, -- the BRC-169 handle's user part, e.g. martha
28
+ domain TEXT NOT NULL DEFAULT 'localhost',
29
+ identity TEXT, -- the instance's identity key, hex: the signer's child for the handle
30
+ store TEXT NOT NULL, -- its runtime.db, or lmdb:<env dir>: its set (its handle) in the host's LMDB environment (#96)
31
+ tree TEXT, -- the root CID of the directory last deployed into it (skein-host deploy)
32
+ source TEXT, -- that directory, for skein-host deploy --all
33
+ knows TEXT, -- JSON array of the handles this agent knows (its ROSTER.md); ["*"]: everyone; NULL: nobody
34
+ status TEXT NOT NULL DEFAULT 'enabled' CHECK (status IN ('enabled', 'disabled')),
35
+ created_at TEXT NOT NULL,
36
+ kind TEXT NOT NULL DEFAULT 'agent' CHECK (kind IN ('agent', 'mailbox')),
37
+ owner TEXT, -- a mailbox instance's: the identity (hex) whose mailbox it is
38
+ topics TEXT -- JSON array: the overlay names (topics, lookup services) the host routes to it at the bare host (#137); NULL: none
39
+ );
40
+ -- The fuel of calls (#40): per instance, caller (hex, or '' for none) and op (the route).
41
+ CREATE TABLE IF NOT EXISTS fuel_ledger (
42
+ instance TEXT NOT NULL,
43
+ caller TEXT NOT NULL,
44
+ op TEXT NOT NULL,
45
+ calls INTEGER NOT NULL DEFAULT 0,
46
+ fuel INTEGER NOT NULL DEFAULT 0,
47
+ updated_at TEXT NOT NULL,
48
+ PRIMARY KEY (instance, caller, op)
49
+ ) WITHOUT ROWID;
50
+ -- The broadcaster's subscription (#58): the last event id taken from each SSE stream (Last-Event-ID on resume).
51
+ CREATE TABLE IF NOT EXISTS stream_cursor (
52
+ stream TEXT PRIMARY KEY, -- the events URL
53
+ last_id TEXT NOT NULL,
54
+ updated_at TEXT NOT NULL
55
+ ) WITHOUT ROWID;
56
+ -- The statuses already routed (#58): txid + txStatus + blockHash, so a redelivered one writes nothing.
57
+ CREATE TABLE IF NOT EXISTS status_seen (
58
+ txid TEXT NOT NULL,
59
+ status TEXT NOT NULL, -- txStatus, a space, the block hash ('' if none)
60
+ at TEXT NOT NULL,
61
+ PRIMARY KEY (txid, status)
62
+ ) WITHOUT ROWID;
63
+ -- The broadcaster's queue (#58, #65, arc.ts): a transaction an instance broadcast (its broadcast event),
64
+ -- held until Arcade answers it taken or refused, tried again with backoff while it does not.
65
+ CREATE TABLE IF NOT EXISTS broadcast_queue (
66
+ txid TEXT PRIMARY KEY,
67
+ body BLOB NOT NULL, -- what goes to Arcade: Extended Format, or the raw transaction
68
+ instances TEXT NOT NULL, -- JSON array: the instances that broadcast it
69
+ attempts INTEGER NOT NULL DEFAULT 0,
70
+ next_at INTEGER NOT NULL, -- ms: when it is posted (again)
71
+ since INTEGER NOT NULL, -- ms: when it was first queued (given up a day on)
72
+ last_error TEXT
73
+ ) WITHOUT ROWID;
74
+ -- The cron provider's schedules (#69, cron.ts): what each instance asked to be ticked, by name, until it
75
+ -- says stop (an \`at\` one until it fired).
76
+ CREATE TABLE IF NOT EXISTS cron_schedule (
77
+ instance TEXT NOT NULL, -- the handle the ticks go to
78
+ name TEXT NOT NULL,
79
+ recipient TEXT NOT NULL, -- its identity key (hex): the ticks' recipient
80
+ spec BLOB NOT NULL, -- dag-cbor {every | at, box, body?}
81
+ request TEXT NOT NULL, -- the tick request's CID
82
+ next INTEGER NOT NULL, -- ms: the next tick
83
+ PRIMARY KEY (instance, name)
84
+ ) WITHOUT ROWID;
85
+ -- The host's settings (#90): key → value. \`host_skein\`: the handle of the host skein. \`image\`, \`image_chain\` (#132):
86
+ -- the default image's current root tree and its \`chain\` tree (image-chain.ts).
87
+ CREATE TABLE IF NOT EXISTS host_settings (
88
+ key TEXT PRIMARY KEY,
89
+ value TEXT NOT NULL
90
+ ) WITHOUT ROWID;
91
+ -- The default image's chain part (#132, image-chain.ts): the git objects of its \`chain/\` (the header blocks, the tip,
92
+ -- their trees) and its root tree, as the host grows them; only the current ones (a replaced object is deleted).
93
+ CREATE TABLE IF NOT EXISTS image_blocks (
94
+ cid TEXT PRIMARY KEY, -- the object's CID (git-raw, sha1)
95
+ bytes BLOB NOT NULL -- the git object ("blob <n>\\0…", "tree <n>\\0…")
96
+ );
97
+ -- Billing (#130, billing.ts): a billed instance as the host last read its kernel's billing state; asleep_since
98
+ -- (ms) is the host's clock when it went asleep (the grace counts from it), NULL when awake.
99
+ CREATE TABLE IF NOT EXISTS billing (
100
+ instance TEXT PRIMARY KEY,
101
+ host TEXT, -- the host row's key (hex)
102
+ tally TEXT, -- nanosats (decimal)
103
+ allocation TEXT, -- nanosats (decimal)
104
+ asleep_since INTEGER,
105
+ mismatch TEXT, -- why this host does not serve its terms, if so
106
+ updated_at TEXT NOT NULL
107
+ ) WITHOUT ROWID;
108
+ -- The periods ticked (#130): the host's log record for each (dag-cbor), the CID its tick commits to.
109
+ CREATE TABLE IF NOT EXISTS billing_ticks (
110
+ instance TEXT NOT NULL,
111
+ at INTEGER NOT NULL, -- the tick's time (ms)
112
+ log TEXT NOT NULL, -- the period record's CID
113
+ record BLOB NOT NULL,
114
+ PRIMARY KEY (instance, at)
115
+ ) WITHOUT ROWID;
116
+ -- The payments (#130): each \`payment\` event an instance's wallet emitted to this host.
117
+ CREATE TABLE IF NOT EXISTS billing_payments (
118
+ instance TEXT NOT NULL,
119
+ txid TEXT NOT NULL,
120
+ amount INTEGER NOT NULL, -- sats
121
+ at INTEGER NOT NULL, -- ms, when the host took it
122
+ beef BLOB NOT NULL, -- the Atomic BEEF
123
+ remittance TEXT NOT NULL, -- JSON {derivationPrefix, derivationSuffix, senderIdentityKey}
124
+ ours INTEGER NOT NULL, -- 1: its output pays the key the remittance derives from this host's
125
+ checkpoint TEXT, -- the state record CID its other output commits
126
+ PRIMARY KEY (instance, txid)
127
+ ) WITHOUT ROWID;
128
+ -- Overlay sync (the settled design, David 2026-10-30): per instance and overlay topic, the peers whose beats on
129
+ -- <topic>-live were kept, with the addresses the node knew for them; a restart dials them and asks at once.
130
+ CREATE TABLE IF NOT EXISTS topic_peers (
131
+ handle TEXT NOT NULL,
132
+ topic TEXT NOT NULL, -- the overlay topic (not its -live)
133
+ peer_id TEXT NOT NULL, -- the libp2p peer ID
134
+ addrs TEXT NOT NULL, -- JSON array of multiaddrs (may be empty)
135
+ last_seen INTEGER NOT NULL, -- ms: the last beat kept from it
136
+ PRIMARY KEY (handle, topic, peer_id)
137
+ ) WITHOUT ROWID;`;
138
+ /**
139
+ * A handle: one hostname label (lower-case letters, digits and "-", at most
140
+ * 63, no "-" at either end), so `<handle>.<domain>` is the instance's origin.
141
+ * The one grammar for every row (H22): `add`, the instance manager's
142
+ * `create`, a mailbox instance.
143
+ */
144
+ export const HANDLE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
145
+ /**
146
+ * A path-only handle (#161): a token id `<txid>_<vout>` (64 lower-case hex digits, `_`, the output
147
+ * index). Not a hostname label, so its instance is published under no hostname: it is reached at
148
+ * `/@<handle>/…` on the host's origin only (Router.target, originOf). Everything else about its row is
149
+ * a row's.
150
+ */
151
+ export const TOKEN_HANDLE = /^[0-9a-f]{64}_[0-9]+$/;
152
+ /** Whether `handle` is a handle this host takes (#161): a hostname label (HANDLE), or a path-only token id (TOKEN_HANDLE). */
153
+ export const validHandle = (handle) => HANDLE.test(handle) || TOKEN_HANDLE.test(handle);
154
+ /** Whether `handle` is path-only (#161): a token id, reached by `/@<handle>` and never by a hostname. */
155
+ export const pathOnly = (handle) => TOKEN_HANDLE.test(handle);
156
+ const FIELDS = ["domain", "identity", "store", "tree", "source", "knows", "status", "kind", "owner", "topics"];
157
+ /** The overlay names a row is routed by (#137): its `topics` column, a list (none: empty). */
158
+ export function topicsOf(row) {
159
+ return row.topics ? JSON.parse(row.topics) : [];
160
+ }
161
+ /** The handles a row knows: a list, or "all" (every other enabled row). */
162
+ export function knowsOf(row) {
163
+ const k = row.knows ? JSON.parse(row.knows) : [];
164
+ return k.includes("*") ? "all" : k;
165
+ }
166
+ /** The `knows` column for a list of handles (`*` among them: everyone); an empty list is null. */
167
+ export function knowsColumn(handles) {
168
+ if (handles === "all" || handles.includes("*"))
169
+ return JSON.stringify(["*"]);
170
+ const k = [...new Set(handles)];
171
+ return k.length ? JSON.stringify(k) : null;
172
+ }
173
+ export class HostDb {
174
+ db;
175
+ /** `readOnly`: an existing file, read only (an instance's resolver beside the supervisor): no schema, no migration. */
176
+ constructor(path, o = {}) {
177
+ this.db = new DatabaseSync(path, { readOnly: o.readOnly ?? false });
178
+ this.db.exec("PRAGMA busy_timeout = 5000;"); // `run` holds it open while `deploy`/`add` write
179
+ if (o.readOnly)
180
+ return;
181
+ this.db.exec(DDL);
182
+ // Older files: columns added since. Files from before #33 also carry
183
+ // wallet_url and wallet_originator (an instance's own wallet-api): nothing
184
+ // reads them, and an insert leaves them to their defaults.
185
+ const cols = new Set(this.db.prepare("PRAGMA table_info(instances)").all().map((c) => c.name));
186
+ if (!cols.has("source"))
187
+ this.db.exec("ALTER TABLE instances ADD COLUMN source TEXT");
188
+ if (!cols.has("knows"))
189
+ this.db.exec("ALTER TABLE instances ADD COLUMN knows TEXT");
190
+ if (!cols.has("kind"))
191
+ this.db.exec("ALTER TABLE instances ADD COLUMN kind TEXT NOT NULL DEFAULT 'agent'");
192
+ if (!cols.has("owner"))
193
+ this.db.exec("ALTER TABLE instances ADD COLUMN owner TEXT");
194
+ if (!cols.has("topics"))
195
+ this.db.exec("ALTER TABLE instances ADD COLUMN topics TEXT");
196
+ }
197
+ /**
198
+ * Insert `handle`, or update the given fields of the row that has it (so a
199
+ * provisioning script can run again). A new row needs `store`.
200
+ */
201
+ add(handle, f, now = new Date()) {
202
+ const set = FIELDS.filter((k) => f[k] !== undefined);
203
+ if (this.get(handle)) {
204
+ if (set.length)
205
+ this.db.prepare(`UPDATE instances SET ${set.map((k) => `${k} = ?`).join(", ")} WHERE handle = ?`).run(...set.map((k) => f[k]), handle);
206
+ }
207
+ else {
208
+ // A new row's handle is a hostname label; a row an older file already has keeps its handle.
209
+ if (!validHandle(handle))
210
+ throw new Error(`bad 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)`);
211
+ if (!f.store)
212
+ throw new Error(`${handle}: a new instance needs a store`);
213
+ const cols = ["handle", ...set, "created_at"];
214
+ this.db.prepare(`INSERT INTO instances (${cols.join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`).run(handle, ...set.map((k) => f[k]), now.toISOString());
215
+ }
216
+ return this.get(handle);
217
+ }
218
+ get(handle) {
219
+ const r = this.db.prepare("SELECT * FROM instances WHERE handle = ?").get(handle);
220
+ return r ? { ...r } : undefined; // node:sqlite rows have a null prototype
221
+ }
222
+ /**
223
+ * The identity handle@domain names (BRC-169's answer): an agent's own, or
224
+ * the owner of a mailbox instance (whose mailbox the instance is).
225
+ */
226
+ identityOf(handle, domain) {
227
+ const r = this.db.prepare("SELECT identity, kind, owner FROM instances WHERE handle = ? AND domain = ?").get(handle, domain);
228
+ if (!r)
229
+ return undefined;
230
+ return (r.kind === "mailbox" ? r.owner : r.identity) ?? undefined;
231
+ }
232
+ list(status) {
233
+ const q = status ? this.db.prepare("SELECT * FROM instances WHERE status = ? ORDER BY created_at, handle").all(status) : this.db.prepare("SELECT * FROM instances ORDER BY created_at, handle").all();
234
+ return q.map((r) => ({ ...r }));
235
+ }
236
+ /** Set which handles a row knows (knowsColumn); false if there is no such row. */
237
+ setKnows(handle, handles) {
238
+ return Number(this.db.prepare("UPDATE instances SET knows = ? WHERE handle = ?").run(knowsColumn(handles), handle).changes) > 0;
239
+ }
240
+ /** Set the overlay names a row is routed by (#137; topicsOf); an empty list is null; false if there is no such row. */
241
+ setTopics(handle, topics) {
242
+ return Number(this.db.prepare("UPDATE instances SET topics = ? WHERE handle = ?").run(topics.length ? JSON.stringify(topics) : null, handle).changes) > 0;
243
+ }
244
+ /** Set a row's status; false if there is no such row. */
245
+ setStatus(handle, status) {
246
+ return Number(this.db.prepare("UPDATE instances SET status = ? WHERE handle = ?").run(status, handle).changes) > 0;
247
+ }
248
+ /** Remove the row (not its store or wallet); false if there was none. */
249
+ remove(handle) {
250
+ return Number(this.db.prepare("DELETE FROM instances WHERE handle = ?").run(handle).changes) > 0;
251
+ }
252
+ /** The enabled mailbox instance for identity `owner` (hex). */
253
+ /** A beat kept from `peer` on `handle`'s overlay `topic` (sync): upserted with its addresses (kept when none are known now). */
254
+ seenPeer(handle, topic, peer, addrs, now) {
255
+ this.db.prepare("INSERT INTO topic_peers (handle, topic, peer_id, addrs, last_seen) VALUES (?, ?, ?, ?, ?) ON CONFLICT (handle, topic, peer_id) DO UPDATE SET addrs = CASE WHEN excluded.addrs = '[]' THEN addrs ELSE excluded.addrs END, last_seen = excluded.last_seen")
256
+ .run(handle, topic, peer, JSON.stringify(addrs), now);
257
+ }
258
+ /** The peers `handle`'s overlay `topic` heard beats from, the latest first. */
259
+ topicPeers(handle, topic) {
260
+ return this.db.prepare("SELECT peer_id, addrs, last_seen FROM topic_peers WHERE handle = ? AND topic = ? ORDER BY last_seen DESC, peer_id").all(handle, topic)
261
+ .map((r) => ({ peer: String(r.peer_id), addrs: JSON.parse(String(r.addrs)), lastSeen: Number(r.last_seen) }));
262
+ }
263
+ /** Forget the peers not heard from since `before` (ms). → how many rows went. */
264
+ prunePeers(before) {
265
+ return Number(this.db.prepare("DELETE FROM topic_peers WHERE last_seen < ?").run(before).changes);
266
+ }
267
+ mailboxOf(owner) {
268
+ const r = this.db.prepare("SELECT * FROM instances WHERE kind = 'mailbox' AND owner = ? AND status = 'enabled'").get(owner);
269
+ return r ? { ...r } : undefined;
270
+ }
271
+ /** Add calls and fuel to the ledger (#40): `rows` aggregated by (instance, caller, op). */
272
+ charge(rows, now = new Date()) {
273
+ const st = this.db.prepare("INSERT INTO fuel_ledger (instance, caller, op, calls, fuel, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (instance, caller, op) DO UPDATE SET calls = calls + excluded.calls, fuel = fuel + excluded.fuel, updated_at = excluded.updated_at");
274
+ for (const r of rows)
275
+ st.run(r.instance, r.caller, r.op, r.calls, r.fuel, now.toISOString());
276
+ }
277
+ ledger(instance) {
278
+ const q = instance ? this.db.prepare("SELECT * FROM fuel_ledger WHERE instance = ? ORDER BY fuel DESC").all(instance) : this.db.prepare("SELECT * FROM fuel_ledger ORDER BY instance, fuel DESC").all();
279
+ return q.map((r) => ({ ...r }));
280
+ }
281
+ /** The last event id taken from an SSE stream (#58), if any. */
282
+ cursor(stream) {
283
+ const r = this.db.prepare("SELECT last_id FROM stream_cursor WHERE stream = ?").get(stream);
284
+ return r?.last_id;
285
+ }
286
+ setCursor(stream, id, now = new Date()) {
287
+ this.db.prepare("INSERT INTO stream_cursor (stream, last_id, updated_at) VALUES (?, ?, ?) ON CONFLICT (stream) DO UPDATE SET last_id = excluded.last_id, updated_at = excluded.updated_at").run(stream, id, now.toISOString());
288
+ }
289
+ /** Note a status as routed (#58); false if it was already. */
290
+ markStatus(txid, status, now = new Date()) {
291
+ return Number(this.db.prepare("INSERT OR IGNORE INTO status_seen (txid, status, at) VALUES (?, ?, ?)").run(txid, status, now.toISOString()).changes) > 0;
292
+ }
293
+ hasStatus(txid, status) {
294
+ return this.db.prepare("SELECT 1 FROM status_seen WHERE txid = ? AND status = ?").get(txid, status) !== undefined;
295
+ }
296
+ /** Queue a broadcast (#65, arc.ts), due now; one already queued keeps its schedule and gains `instance`. */
297
+ queueBroadcast(txid, body, instance, now) {
298
+ const had = this.db.prepare("SELECT instances FROM broadcast_queue WHERE txid = ?").get(txid);
299
+ if (had) {
300
+ const hs = new Set(JSON.parse(had.instances));
301
+ hs.add(instance);
302
+ this.db.prepare("UPDATE broadcast_queue SET instances = ?, next_at = MIN(next_at, ?) WHERE txid = ?").run(JSON.stringify([...hs]), now, txid);
303
+ return;
304
+ }
305
+ this.db.prepare("INSERT INTO broadcast_queue (txid, body, instances, attempts, next_at, since) VALUES (?, ?, ?, 0, ?, ?)").run(txid, body, JSON.stringify([instance]), now, now);
306
+ }
307
+ /** The queued broadcasts due at `now`, oldest first. */
308
+ dueBroadcasts(now) {
309
+ return this.db.prepare("SELECT txid, body, instances, attempts, since FROM broadcast_queue WHERE next_at <= ? ORDER BY next_at, txid").all(now).map((r) => {
310
+ const x = r;
311
+ return { txid: x.txid, body: new Uint8Array(x.body), instances: JSON.parse(x.instances), attempts: Number(x.attempts), since: Number(x.since) };
312
+ });
313
+ }
314
+ /** When the next queued broadcast is due (ms), if any. */
315
+ nextBroadcastAt() {
316
+ const r = this.db.prepare("SELECT MIN(next_at) AS n FROM broadcast_queue").get();
317
+ return r?.n == null ? undefined : Number(r.n);
318
+ }
319
+ /** A queued broadcast Arcade did not take: one more attempt, posted again at `next`. */
320
+ retryBroadcast(txid, next, error) {
321
+ this.db.prepare("UPDATE broadcast_queue SET attempts = attempts + 1, next_at = ?, last_error = ? WHERE txid = ?").run(next, error, txid);
322
+ }
323
+ /** A broadcast Arcade answered (or one given up): off the queue. */
324
+ unqueueBroadcast(txid) {
325
+ this.db.prepare("DELETE FROM broadcast_queue WHERE txid = ?").run(txid);
326
+ }
327
+ /** The queue as it stands. */
328
+ broadcasts() {
329
+ return this.db.prepare("SELECT txid, attempts, next_at, last_error FROM broadcast_queue ORDER BY next_at, txid").all().map((r) => {
330
+ const x = r;
331
+ return { txid: x.txid, attempts: Number(x.attempts), next: Number(x.next_at), ...(x.last_error ? { error: x.last_error } : {}) };
332
+ });
333
+ }
334
+ /** Keep a cron schedule (#69): an instance's, by name (a later one replaces it). */
335
+ saveSchedule(s) {
336
+ this.db.prepare("INSERT INTO cron_schedule (instance, name, recipient, spec, request, next) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (instance, name) DO UPDATE SET recipient = excluded.recipient, spec = excluded.spec, request = excluded.request, next = excluded.next")
337
+ .run(s.instance, s.name, s.recipient, s.spec, s.request, s.next);
338
+ }
339
+ /** Drop an instance's schedule; false if it had none by that name. */
340
+ dropSchedule(instance, name) {
341
+ return Number(this.db.prepare("DELETE FROM cron_schedule WHERE instance = ? AND name = ?").run(instance, name).changes) > 0;
342
+ }
343
+ /** Every kept schedule. */
344
+ schedules() {
345
+ return this.db.prepare("SELECT instance, name, recipient, spec, request, next FROM cron_schedule ORDER BY instance, name").all().map((r) => {
346
+ const x = r;
347
+ return { instance: x.instance, name: x.name, recipient: x.recipient, spec: new Uint8Array(x.spec), request: x.request, next: Number(x.next) };
348
+ });
349
+ }
350
+ /** The enabled row whose identity is `identity`. */
351
+ byIdentity(identity) {
352
+ const r = this.db.prepare("SELECT * FROM instances WHERE identity = ? AND status = 'enabled'").get(identity);
353
+ return r ? { ...r } : undefined;
354
+ }
355
+ /** A host setting (#90), if set. */
356
+ setting(key) {
357
+ const r = this.db.prepare("SELECT value FROM host_settings WHERE key = ?").get(key);
358
+ return r?.value;
359
+ }
360
+ setSetting(key, value) {
361
+ this.db.prepare("INSERT INTO host_settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value").run(key, value);
362
+ }
363
+ /** An object of the default image's chain part (#132), if held. */
364
+ imageBlock(cid) {
365
+ const r = this.db.prepare("SELECT bytes FROM image_blocks WHERE cid = ?").get(cid);
366
+ return r ? new Uint8Array(r.bytes) : undefined;
367
+ }
368
+ /** One write of the image's chain part (#132): objects put, objects dropped, settings set — all or nothing. */
369
+ writeImage(puts, drops, settings) {
370
+ this.db.exec("BEGIN IMMEDIATE");
371
+ try {
372
+ const put = this.db.prepare("INSERT INTO image_blocks (cid, bytes) VALUES (?, ?) ON CONFLICT (cid) DO NOTHING");
373
+ const drop = this.db.prepare("DELETE FROM image_blocks WHERE cid = ?");
374
+ for (const c of drops)
375
+ drop.run(c);
376
+ for (const [c, b] of puts)
377
+ put.run(c, b);
378
+ for (const [k, v] of Object.entries(settings))
379
+ this.setSetting(k, v);
380
+ this.db.exec("COMMIT");
381
+ }
382
+ catch (e) {
383
+ this.db.exec("ROLLBACK");
384
+ throw e;
385
+ }
386
+ }
387
+ /** A billed instance as last read (#130); asleep: when it went asleep (ms) is kept from the first time seen. */
388
+ setBilling(instance, b, now, at = new Date()) {
389
+ const had = this.billing(instance);
390
+ const since = b.asleep ? (had?.asleep_since ?? now) : null;
391
+ this.db.prepare("INSERT INTO billing (instance, host, tally, allocation, asleep_since, mismatch, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (instance) DO UPDATE SET host = excluded.host, tally = excluded.tally, allocation = excluded.allocation, asleep_since = excluded.asleep_since, mismatch = excluded.mismatch, updated_at = excluded.updated_at")
392
+ .run(instance, b.host ?? null, b.tally?.toString() ?? null, b.allocation?.toString() ?? null, since, b.mismatch ?? null, at.toISOString());
393
+ }
394
+ billing(instance) {
395
+ const r = this.db.prepare("SELECT * FROM billing WHERE instance = ?").get(instance);
396
+ return r ? { ...r } : undefined;
397
+ }
398
+ billings() {
399
+ return this.db.prepare("SELECT * FROM billing ORDER BY instance").all().map((r) => ({ ...r }));
400
+ }
401
+ /** The instances asleep since before `now - graceMs` (#130: the host may reclaim them). */
402
+ reclaimable(now, graceMs) {
403
+ return this.db.prepare("SELECT instance, asleep_since FROM billing WHERE asleep_since IS NOT NULL AND asleep_since <= ? ORDER BY asleep_since").all(now - graceMs).map((r) => ({ ...r }));
404
+ }
405
+ forgetBilling(instance) {
406
+ this.db.prepare("DELETE FROM billing WHERE instance = ?").run(instance);
407
+ }
408
+ /** A period ticked (#130): its log record and CID. */
409
+ billingTick(instance, at, log, record) {
410
+ this.db.prepare("INSERT OR REPLACE INTO billing_ticks (instance, at, log, record) VALUES (?, ?, ?, ?)").run(instance, at, log, record);
411
+ }
412
+ billingTicks(instance) {
413
+ return this.db.prepare("SELECT at, log, record FROM billing_ticks WHERE instance = ? ORDER BY at").all(instance).map((r) => { const x = r; return { at: Number(x.at), log: x.log, record: new Uint8Array(x.record) }; });
414
+ }
415
+ /** A payment taken (#130); false if it was already. */
416
+ billingPayment(p) {
417
+ return Number(this.db.prepare("INSERT OR IGNORE INTO billing_payments (instance, txid, amount, at, beef, remittance, ours, checkpoint) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
418
+ .run(p.instance, p.txid, p.amount, p.at, p.beef, JSON.stringify(p.remittance), p.ours ? 1 : 0, p.checkpoint ?? null).changes) > 0;
419
+ }
420
+ billingPayments(instance) {
421
+ const q = instance ? this.db.prepare("SELECT instance, txid, amount, at, ours, checkpoint, remittance FROM billing_payments WHERE instance = ? ORDER BY at").all(instance) : this.db.prepare("SELECT instance, txid, amount, at, ours, checkpoint, remittance FROM billing_payments ORDER BY instance, at").all();
422
+ return q.map((r) => { const x = r; return { ...x, amount: Number(x.amount), at: Number(x.at), ours: x.ours === 1 }; });
423
+ }
424
+ /** The host skein (#90): the operator's instance (`skein-host init`), the one row whose identity reaches the instance manager. */
425
+ hostSkein() {
426
+ const h = this.setting("host_skein");
427
+ return h ? this.get(h) : undefined;
428
+ }
429
+ close() { this.db.close(); }
430
+ }