@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,370 @@
1
+ // The operator's admin commands (#124, #142, #143): each builds root's messages (admin.ts, plan.ts), signs
2
+ // them in this process with the operator's key (SKEIN_OPERATOR_KEY, default $SKEIN_HOME/operator.key;
3
+ // hostenv.ts) and delivers them (target.ts) — no other process, no wallet command.
4
+ //
5
+ // skein install <catalog-name | url#commit | dir> <where> [--config <file.json>] [--dry-run]
6
+ // skein uninstall <app> <where> [--dry-run]
7
+ // skein routes add|remove [--transport t] [--prefix] [--filters f,g] [--fn f] [--settings json] <address> [<handler>] <where> [--dry-run]
8
+ // skein grant add|remove <key> [--role root|<app>.<role>] <where> [--dry-run]
9
+ // skein peers add <key> <address> [--transport mailbox|libp2p|local] [--handle h@d] <where> [--dry-run]
10
+ // skein peers remove <key> <where> [--dry-run]
11
+ // skein host add|remove (<host-key> --x sats [--rates json] | --from <host origin>) <where> [--dry-run]
12
+ // skein claim [--messagebox url] [--handle h@d] <where> [--dry-run]
13
+ // skein deploy <dir> [--only glob,glob | --all] <where> [--dry-run]
14
+ //
15
+ // <where> is the instance: `--instance <handle>` (on this host machine: host.db's row, its store read
16
+ // read-only, the messages over the running host's control socket), or its origin `<url>` (one BRC-104
17
+ // session: the messages, and the explorer's reads — the owner's), or `--store <runtime.db>` (its store
18
+ // file, read-only: planned and printed, nothing sent). `--dry-run` prints the prompt and the messages
19
+ // (their `/sendMessage` JSON, one a line) and sends nothing.
20
+ //
21
+ // `install` of a catalog name or `<url>#<commit>` sends one message to the instance's git app — `git.clone
22
+ // {url, hash}`: the VM fetches that commit by hash and keeps its tree (#91) — reads the answer, plans the
23
+ // install from the stored tree, and sends the head, the rows and the start: nothing of the app's tree or
24
+ // modules crosses from here. A catalog name is looked up in the instance's own site tree
25
+ // (www/catalog.json). A directory is read here and its objects sent (what the instance lacks); installing
26
+ // a tree the instance holds already (an app installed before, an image's app) sends only what changes:
27
+ // the owner's rows after a claim. `--dry-run` with a clone still sends the clone (the tree is read from the
28
+ // instance), then sends nothing more.
29
+ import { existsSync, readFileSync, statSync } from "node:fs";
30
+ import { resolve } from "node:path";
31
+ import { parseArgs } from "node:util";
32
+ import { appHead, readStoredApp } from "../host/plan.js";
33
+ import { identityOf, operatorKey } from "../host/hostenv.js";
34
+ import { keyWallet } from "../wallet.js";
35
+ import { catalogOf, messageJson, planClaim, planDeploy, planGrant, planHost, planInstallApp, planPeers, planRoute, planUninstallApp } from "./admin.js";
36
+ import { localTarget, remoteTarget, stdoutOf } from "./target.js";
37
+ export const ADMIN_USAGE = `usage (the operator's messages, signed with SKEIN_OPERATOR_KEY, default $SKEIN_HOME/operator.key):
38
+ skein install <catalog-name | url#commit | dir> <where> [--config <file.json>] [--dry-run]
39
+ an app (docs/APPS.md §3): a name from the instance's catalog or a repository
40
+ and commit, cloned by the instance's git app; head <app>/app, its rows, start
41
+ skein uninstall <app> <where> [--dry-run] its stop, then its routes removed (the heads are left)
42
+ skein routes add [--transport mailbox|event|http|libp2p] [--prefix] [--filters f,g] [--fn f] [--settings json] <address> [<handler>] <where> [--dry-run]
43
+ root's own route (#143): <handler> a program record CID, a genesis program,
44
+ or <app>.<role>; an http route with --filters and no handler is a read route
45
+ (the site at /: routes add --prefix --filters site.get --settings '{"root":"www"}' /)
46
+ skein routes remove [--transport t] [--prefix] <address> <where> [--dry-run]
47
+ skein grant add|remove <key> [--role root|<app>.<role>] <where> [--dry-run] a grant (#143; root by default)
48
+ skein peers add <key> <address> [--transport mailbox|libp2p|local] [--handle h@d] <where> [--dry-run]
49
+ skein peers remove <key> <where> [--dry-run] an address-book entry (#70)
50
+ skein host add|remove (<host-key> --x sats [--rates json] | --from <host origin>) <where> [--dry-run]
51
+ the host row (#130)
52
+ skein claim [--messagebox url] [--handle h@d] <where> [--dry-run] an image's claim (#127, #143): root to the key that sends it
53
+ skein deploy <dir> [--only glob,glob | --all] <where> [--dry-run] a directory into main (objects, head)
54
+ <where>: --instance <handle> (this host machine: over its control socket) | <origin> (one BRC-104 session)
55
+ | --store <runtime.db> (read only: planned and printed, nothing sent)`;
56
+ export const ADMIN_COMMANDS = ["install", "uninstall", "routes", "grant", "peers", "host", "claim", "deploy"];
57
+ const where = { instance: { type: "string" }, store: { type: "string" }, "dry-run": { type: "boolean" } };
58
+ const isUrl = (s) => /^https?:\/\/[^/]/.test(s);
59
+ async function sessionOf(v, origin, env) {
60
+ const given = [v.instance !== undefined, origin !== undefined, v.store !== undefined].filter(Boolean).length;
61
+ if (given !== 1)
62
+ throw new Error("where is the instance: --instance <handle>, its origin <url>, or --store <runtime.db> (one)");
63
+ if (v.store !== undefined) {
64
+ const { openStoreFile, storeExists } = await import("../host/stores.js");
65
+ if (!storeExists(v.store))
66
+ throw new Error(`--store ${v.store}: no such file (or LMDB set, lmdb:<env dir>#<set>)`);
67
+ const { instanceView } = await import("../host/install.js");
68
+ const s = openStoreFile(v.store, { readOnly: true });
69
+ let view;
70
+ try {
71
+ view = await instanceView(s);
72
+ }
73
+ catch (e) {
74
+ s.close();
75
+ throw e;
76
+ }
77
+ return { view: async () => view, identity: async () => view.identity, dry: true, close: async () => s.close() };
78
+ }
79
+ const key = operatorKey(env.vars).key;
80
+ const wallet = keyWallet(key);
81
+ const target = v.instance !== undefined ? localTarget(env.vars, v.instance, wallet) : await remoteTarget(origin, wallet);
82
+ return { target, view: () => target.view(), identity: async () => target.identity, dry: !!v["dry-run"], close: () => target.close(), signer: key.toPublicKey().toString() };
83
+ }
84
+ /** The origin among the positionals: the last one when it is a URL and the command takes one more than it needs. */
85
+ function splitOrigin(positionals, needs) {
86
+ if (positionals.length === needs + 1 && isUrl(positionals.at(-1)))
87
+ return { args: positionals.slice(0, -1), origin: positionals.at(-1) };
88
+ return { args: positionals };
89
+ }
90
+ const json = (what, text) => {
91
+ let v;
92
+ try {
93
+ v = JSON.parse(text);
94
+ }
95
+ catch {
96
+ v = undefined;
97
+ }
98
+ if (!v || typeof v !== "object" || Array.isArray(v))
99
+ throw new Error(`${what}: a JSON object`);
100
+ return v;
101
+ };
102
+ /** Print the plan; unless it is a dry run, send its messages in order (signed here, over the target). */
103
+ async function deliver(p, s, env) {
104
+ for (const l of p.prompt)
105
+ env.out(l);
106
+ if (s.dry || !s.target) {
107
+ for (const m of p.messages)
108
+ env.out(messageJson(p.recipient, m));
109
+ env.out(`${p.messages.length} message${p.messages.length === 1 ? "" : "s"} to ${p.recipient} (not sent)`);
110
+ return 0;
111
+ }
112
+ const t0 = Date.now();
113
+ for (const m of p.messages)
114
+ await s.target.send(m.box, m.body);
115
+ env.out(`${s.target.where}: ${p.messages.length} message${p.messages.length === 1 ? "" : "s"} sent (${Date.now() - t0} ms)`);
116
+ return 0;
117
+ }
118
+ /** `skein <command> …` for the admin commands. */
119
+ export async function adminMain(cmd, argv, env) {
120
+ let s;
121
+ try {
122
+ switch (cmd) {
123
+ case "install": {
124
+ const { values: v, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { ...where, config: { type: "string" } } });
125
+ const { args: [spec, ...more], origin } = splitOrigin(positionals, 1);
126
+ if (!spec || more.length) {
127
+ env.err(ADMIN_USAGE);
128
+ return 2;
129
+ }
130
+ const config = v.config !== undefined ? json(`--config ${v.config}`, readFileSync(v.config, "utf8")) : undefined;
131
+ s = await sessionOf(v, origin, env);
132
+ return await install(spec, s, config, env);
133
+ }
134
+ case "uninstall": {
135
+ const { values: v, positionals } = parseArgs({ args: argv, allowPositionals: true, options: where });
136
+ const { args: [app, ...more], origin } = splitOrigin(positionals, 1);
137
+ if (!app || more.length) {
138
+ env.err(ADMIN_USAGE);
139
+ return 2;
140
+ }
141
+ s = await sessionOf(v, origin, env);
142
+ return await deliver(await planUninstallApp(app, await s.view()), s, env);
143
+ }
144
+ case "routes": {
145
+ const { values: v, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { ...where, transport: { type: "string" }, prefix: { type: "boolean" }, filters: { type: "string" }, fn: { type: "string" }, settings: { type: "string" } } });
146
+ const origin = positionals.length > 1 && isUrl(positionals.at(-1)) ? positionals.at(-1) : undefined;
147
+ const [op, address, handler, ...more] = origin ? positionals.slice(0, -1) : positionals;
148
+ if (!address || more.length || (op !== "add" && op !== "remove") || (op === "remove" && handler !== undefined)) {
149
+ env.err(ADMIN_USAGE);
150
+ return 2;
151
+ }
152
+ const settings = v.settings !== undefined ? json("skein routes --settings", v.settings) : undefined;
153
+ const filters = v.filters !== undefined ? v.filters.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
154
+ s = await sessionOf(v, origin, env);
155
+ const programs = handler ? await handlerPrograms(s, handler) : {};
156
+ return await deliver(await planRoute(await s.view(), { op, address, ...(v.transport ? { transport: v.transport } : {}), ...(v.prefix ? { prefix: true } : {}), ...(filters ? { filters } : {}), ...(handler ? { handler } : {}), ...(v.fn ? { fn: v.fn } : {}), ...(settings ? { settings } : {}) }, programs), s, env);
157
+ }
158
+ case "grant": {
159
+ const { values: v, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { ...where, role: { type: "string" } } });
160
+ const { args: [op, key, ...more], origin } = splitOrigin(positionals, 2);
161
+ if (!key || more.length || (op !== "add" && op !== "remove")) {
162
+ env.err(ADMIN_USAGE);
163
+ return 2;
164
+ }
165
+ s = await sessionOf(v, origin, env);
166
+ return await deliver(await planGrant(await s.view(), key, { ...(v.role ? { role: v.role } : {}), ...(op === "remove" ? { remove: true } : {}) }), s, env);
167
+ }
168
+ case "peers": {
169
+ const { values: v, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { ...where, transport: { type: "string" }, handle: { type: "string" } } });
170
+ const op = positionals[0];
171
+ const { args: [, key, address, ...more], origin } = splitOrigin(positionals, op === "remove" ? 2 : 3);
172
+ const shape = op === "remove" ? !!key && !address : op === "add" ? !!key && !!address : false;
173
+ if (more.length || !shape || ((v.transport ?? v.handle) !== undefined && op !== "add")) {
174
+ env.err(ADMIN_USAGE);
175
+ return 2;
176
+ }
177
+ const transport = (v.transport ?? "mailbox");
178
+ if (!["mailbox", "libp2p", "local"].includes(transport)) {
179
+ env.err("skein peers: --transport is mailbox, libp2p or local");
180
+ return 2;
181
+ }
182
+ if (op === "add" && transport === "mailbox" && !isUrl(address)) {
183
+ env.err(`skein peers: ${address} is not an http(s) URL`);
184
+ return 2;
185
+ }
186
+ const named = v.handle !== undefined ? handleOf(v.handle) : {};
187
+ const change = op === "add" ? { op: "add", key: key, transport, address: address, ...named } : { op: "remove", key: key };
188
+ s = await sessionOf(v, origin, env);
189
+ return await deliver(planPeers(await s.identity(), [change]), s, env);
190
+ }
191
+ case "claim": {
192
+ const { values: v, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { ...where, messagebox: { type: "string" }, handle: { type: "string" } } });
193
+ const { args: more, origin } = splitOrigin(positionals, 0);
194
+ if (more.length) {
195
+ env.err(ADMIN_USAGE);
196
+ return 2;
197
+ }
198
+ if (v.messagebox !== undefined && !isUrl(v.messagebox)) {
199
+ env.err(`skein claim: ${v.messagebox} is not an http(s) URL`);
200
+ return 2;
201
+ }
202
+ const named = v.handle !== undefined ? handleOf(v.handle) : {};
203
+ s = await sessionOf(v, origin, env);
204
+ const p = planClaim(await s.identity(), { messagebox: v.messagebox, ...named });
205
+ if (s.dry || !s.target)
206
+ return await deliver(p, s, env);
207
+ for (const l of p.prompt)
208
+ env.out(l);
209
+ await s.target.claim(p.messages[0].body);
210
+ env.out(`${s.target.where}: the claim sent`);
211
+ return 0;
212
+ }
213
+ case "host": {
214
+ const { values: v, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { ...where, x: { type: "string" }, rates: { type: "string" }, from: { type: "string" } } });
215
+ const { args: [op, key, ...more], origin } = splitOrigin(positionals, v.from !== undefined ? 1 : 2);
216
+ if (more.length || (op !== "add" && op !== "remove") || (!key && v.from === undefined) || (key && v.from !== undefined)) {
217
+ env.err(ADMIN_USAGE);
218
+ return 2;
219
+ }
220
+ let terms;
221
+ if (v.from !== undefined) {
222
+ const r = await fetch(new URL("/.well-known/skein-host", v.from));
223
+ const b = (await r.json()).billing;
224
+ if (!r.ok || !b)
225
+ throw new Error(`${v.from}: no billing terms at /.well-known/skein-host`);
226
+ terms = { key: b.key, x: b.x, rates: b.rates };
227
+ }
228
+ else {
229
+ const rates = v.rates !== undefined ? json("skein host --rates", v.rates) : undefined;
230
+ terms = { key: key, x: Number(v.x ?? (op === "remove" ? 1 : NaN)), ...(rates ? { rates } : {}) };
231
+ }
232
+ s = await sessionOf(v, origin, env);
233
+ return await deliver(planHost(await s.identity(), op, terms), s, env);
234
+ }
235
+ case "deploy": {
236
+ const { values: v, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { ...where, only: { type: "string" }, all: { type: "boolean" } } });
237
+ const { args: [dir, ...more], origin } = splitOrigin(positionals, 1);
238
+ if (!dir || more.length || (v.all && v.only !== undefined)) {
239
+ env.err(ADMIN_USAGE);
240
+ return 2;
241
+ }
242
+ const only = v.all ? [] : v.only !== undefined ? v.only.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
243
+ s = await sessionOf(v, origin, env);
244
+ return await deliver(await planDeploy(resolve(dir), await s.view(), { only }), s, env);
245
+ }
246
+ default:
247
+ env.err(ADMIN_USAGE);
248
+ return 2;
249
+ }
250
+ }
251
+ catch (e) {
252
+ env.err(`skein ${cmd}: ${e.message}`);
253
+ return 1;
254
+ }
255
+ finally {
256
+ await s?.close();
257
+ }
258
+ }
259
+ function handleOf(given) {
260
+ const h = given.replace(/^@/, "");
261
+ const at = h.lastIndexOf("@");
262
+ const named = at > 0 ? { handle: h.slice(0, at), domain: h.slice(at + 1) } : { handle: h };
263
+ if (!named.handle || (at > 0 && !named.domain))
264
+ throw new Error(`--handle ${given}: want handle@domain`);
265
+ return named;
266
+ }
267
+ /** The programs a handler may name (#125): the genesis's by name, and `<app>.<role>` — the installed app's. */
268
+ async function handlerPrograms(s, handler) {
269
+ const role = /^([a-z0-9][a-z0-9-]*)\.([A-Za-z0-9_-]+)$/.exec(handler);
270
+ const needsView = !!role || !/^baf/.test(handler);
271
+ if (!needsView)
272
+ return {};
273
+ const view = await s.view();
274
+ const programs = { ...view.programs };
275
+ if (role && !programs[handler]) {
276
+ const { appRecordIn } = await import("../host/plan.js");
277
+ const app = await appRecordIn(view, role[1]);
278
+ if (!app)
279
+ throw new Error(`handler ${handler}: no app ${role[1]} installed (no head ${role[1]}/app)`);
280
+ const cid = app.record.programs[role[2]];
281
+ if (!cid)
282
+ throw new Error(`handler ${handler}: app ${role[1]} has no program ${role[2]} (${Object.keys(app.record.programs).join(", ")})`);
283
+ programs[handler] = cid;
284
+ }
285
+ return programs;
286
+ }
287
+ const hexOf = (k) => (k instanceof Uint8Array ? Buffer.from(k).toString("hex") : typeof k === "string" ? k : "");
288
+ /**
289
+ * `skein install`: the tree — a directory read here, or a commit the instance's git app clones by hash (a
290
+ * catalog name gives the URL and the commit) — then the plan from it, printed, and its messages sent.
291
+ */
292
+ async function install(spec, s, config, env) {
293
+ const { wasmDirObjects, WASM_DIR } = await import("../host/boot.js");
294
+ let view = await s.view();
295
+ let tree;
296
+ let cloned;
297
+ if (existsSync(spec) && statSync(spec).isDirectory()) {
298
+ const { readApp } = await import("../host/install.js");
299
+ tree = await readApp(resolve(spec));
300
+ }
301
+ else {
302
+ let url, hash;
303
+ const m = /^(.+)#([0-9a-fA-F]{40})$/.exec(spec);
304
+ if (m) {
305
+ url = m[1];
306
+ hash = m[2].toLowerCase();
307
+ }
308
+ else if (/^[a-z0-9][a-z0-9-]*$/.test(spec)) {
309
+ const cat = await catalogOf(view);
310
+ const e = cat.find((x) => x.name === spec);
311
+ if (!e)
312
+ throw new Error(`${spec}: not in the instance's catalog (it names ${cat.map((x) => x.name).join(", ") || "nothing"}); give <url>#<commit>`);
313
+ url = e.url;
314
+ hash = e.hash.toLowerCase();
315
+ env.out(`catalog: ${e.name}${e.version ? ` ${e.version}` : ""} = ${url}#${hash}`);
316
+ }
317
+ else
318
+ throw new Error(`${spec}: a catalog name, <url>#<40-hex commit>, or a directory`);
319
+ if (!s.target)
320
+ throw new Error("--store reads a store file: it cannot ask the instance's git app to clone (give a directory, or --instance / an origin)");
321
+ // #143: the git app's box is a route; its function is gated by root (git's roles) — the gate judges the sender.
322
+ const gitRoute = view.dispatch.find((r) => r.transport === "mailbox" && r.address === "git");
323
+ if (!gitRoute) {
324
+ throw new Error(`the instance has no route to box git${view.heads.some((h) => h.name === appHead("git")) ? "" : " (no git app installed)"}`);
325
+ }
326
+ // The gate, read before anything is sent (the kernel's own check runs on the message; a refused one runs nothing).
327
+ const { gatePasses } = await import("./admin.js");
328
+ const gate = await gatePasses(view, gitRoute, s.signer);
329
+ if (!gate.pass)
330
+ throw new Error(`box git is gated (${gate.roles.join(", ")}) and ${s.signer.slice(0, 10)}… holds none of those roles: nothing sent`);
331
+ const t0 = Date.now();
332
+ const id = await s.target.send("git", { fn: "git.clone", args: { url, hash } });
333
+ const end = await s.target.answer(id);
334
+ if (end.state === "errored")
335
+ throw new Error(`git.clone: the git app's thread errored: ${JSON.stringify(end.error ?? {})}`);
336
+ const out = stdoutOf(end);
337
+ if (out?.error)
338
+ throw new Error(`git.clone: ${out.error.code}: ${out.error.message}`);
339
+ if (!out?.result?.tree || !out.result.app)
340
+ throw new Error(`git.clone: no {tree, app} in the answer (${JSON.stringify(out)})`);
341
+ cloned = { tree: out.result.tree, app: out.result.app };
342
+ env.out(`git.clone ${url}#${hash}: tree ${cloned.tree} · app record ${cloned.app} (${Date.now() - t0} ms, in the instance)`);
343
+ view = await s.view();
344
+ tree = await readStoredApp(view.store, cloned.tree);
345
+ }
346
+ const p = await planInstallApp(tree, view, { config, modules: wasmDirObjects(WASM_DIR) });
347
+ if (cloned && !config && !p.recordCid.equals(cloned.app))
348
+ p.prompt.push(` note the plan's app record ${p.recordCid} is not the git app's ${cloned.app}`);
349
+ await deliver(p, s, env);
350
+ if (s.dry || !s.target)
351
+ return 0;
352
+ // Installed when its head names the record (the kernel takes the messages in order).
353
+ // Its start may move the head on at once (the app's `state`): the record without `state` is what counts.
354
+ const { encode } = await import("../runtime/cid.js");
355
+ const bare = (r) => { const { state: _s, ...rest } = r; void _s; return encode(rest).cid; };
356
+ const want = bare(p.record);
357
+ const deadline = Date.now() + 120_000;
358
+ for (;;) {
359
+ const v = await s.view();
360
+ const root = v.heads.find((h) => h.name === appHead(p.app))?.root;
361
+ const rec = root ? await v.store.get(root).catch(() => undefined) : undefined;
362
+ if (rec && bare(rec).equals(want))
363
+ break;
364
+ if (Date.now() > deadline)
365
+ throw new Error(`${p.app}: head ${appHead(p.app)} does not name ${p.recordCid} after 120 s (the instance's log says why)`);
366
+ await new Promise((r) => setTimeout(r, 300));
367
+ }
368
+ env.out(`installed ${p.app} ${p.version} into ${s.target.where}`);
369
+ return 0;
370
+ }
@@ -0,0 +1,139 @@
1
+ import type { CID } from "multiformats/cid";
2
+ import { type DispatchRow } from "../runtime/dispatch.ts";
3
+ import { type AddressEntry } from "../host/deploy.ts";
4
+ import { type AppRecord, type AppTree, type InstanceView } from "../host/plan.ts";
5
+ import type { Objects } from "../host/boot.ts";
6
+ /** One admin message: the box and the body (a record value: what the kernel's operation takes). */
7
+ export interface AdminMessage {
8
+ box: string;
9
+ body: unknown;
10
+ }
11
+ /** What the admin commands build: the prompt (what the messages do, as the page shows it), the recipient, the messages in order. */
12
+ export interface AdminPlan {
13
+ prompt: string[];
14
+ recipient: string;
15
+ messages: AdminMessage[];
16
+ }
17
+ /** A message as the `/sendMessage` JSON body. */
18
+ export declare function messageJson(recipient: string, m: AdminMessage): string;
19
+ /**
20
+ * Install (or upgrade) the app `tree` into the instance `view` reads (docs/APPS.md §3): objects (≤ 1 MiB
21
+ * bundles), the head `<app>/app`, a dispatch message per row that changes, the start. `config` is merged
22
+ * over the manifest's (one level deep per program). `modules`: where a `bin/<x>.cid` module the instance
23
+ * lacks is found (this repo's wasm/ directory, boot.ts wasmDirObjects).
24
+ */
25
+ export declare function planInstallApp(tree: AppTree, view: InstanceView, o?: {
26
+ config?: Record<string, unknown>;
27
+ modules?: Objects;
28
+ }): Promise<AdminPlan & {
29
+ app: string;
30
+ version: string;
31
+ recordCid: CID;
32
+ record: AppRecord;
33
+ }>;
34
+ /** Uninstall: the app's `stop`, then every route the table holds for it removed; its heads are left. */
35
+ export declare function planUninstallApp(name: string, view: InstanceView): Promise<AdminPlan>;
36
+ /** One of root's own routes as `skein routes` takes it (#143). */
37
+ export interface RouteArgs {
38
+ op: "add" | "remove";
39
+ transport?: "mailbox" | "event" | "http" | "libp2p";
40
+ address: string;
41
+ prefix?: boolean;
42
+ filters?: string[];
43
+ /** A program record CID, a genesis program's name, or `<app>.<role>` (resolved by the caller into `programs`); none: an http read route. */
44
+ handler?: string;
45
+ fn?: string;
46
+ settings?: Record<string, unknown>;
47
+ }
48
+ /**
49
+ * Root's own route (#125, #143; `skein routes`): one `dispatch` add or remove of a route with no `app` —
50
+ * an app's upgrade or uninstall leaves it. A mailbox or event route at a box, an http route at a path
51
+ * (with `filters` and a handler's `fn`, or filters only: a read route — the site at `/`: filters
52
+ * ["site.get"], settings {root: "www"}), a libp2p route at a topic. No sender (#143). A remove names the
53
+ * key (transport, address, prefix) only.
54
+ */
55
+ export declare function planRoute(view: InstanceView, a: RouteArgs, programs?: Record<string, CID>): Promise<AdminPlan>;
56
+ /**
57
+ * The claim (#89, #127, #143): one message in box `claim` to an image (its claim route takes anyone) —
58
+ * its sender (the wallet that sends it) is granted root; the body carries only the claimant's
59
+ * mailbox entry, if any.
60
+ */
61
+ export declare function planClaim(recipient: string, o?: {
62
+ messagebox?: string;
63
+ handle?: string;
64
+ domain?: string;
65
+ }): AdminPlan;
66
+ /** The host's terms as it publishes them (/.well-known/skein-host `billing`, #130). */
67
+ export interface HostTerms {
68
+ key: string;
69
+ x: number;
70
+ rates?: Record<string, number>;
71
+ }
72
+ /**
73
+ * The host row (#130): root's choice of a host and its rates — one `dispatch` message adding (or
74
+ * removing) the kernel route {mailbox, billing, host: <the host's key>, kernel, tick, x, rates} (#143:
75
+ * the host's key a setting, no sender). The first such route is the host's; the kernel bills by it,
76
+ * the host ticks it. Removing it ends billing.
77
+ */
78
+ export declare function planHost(recipient: string, op: "add" | "remove", t: HostTerms): AdminPlan;
79
+ export type PeerChange = ({
80
+ op: "add";
81
+ } & AddressEntry) | {
82
+ op: "remove";
83
+ key: string;
84
+ };
85
+ /** Address-book changes (#40, #70): one message to `peers` each. */
86
+ export declare function planPeers(recipient: string, changes: PeerChange[]): AdminPlan;
87
+ /**
88
+ * A directory into the instance's `main` (#23): its git objects in ≤ 1 MiB bundles to `objects`, the root
89
+ * on the last (the kernel sets `main` when there is none), then `head {name: "main", tree}` when `main` is
90
+ * another tree. `only` filters the directory (deploy.ts onlyIgnore; default DEFAULT_ONLY; [] for all of it).
91
+ * Records the view's store has are not sent. With no view (`recipient` alone) everything is sent, and the head.
92
+ */
93
+ export declare function planDeploy(dir: string, target: InstanceView | {
94
+ identity: string;
95
+ }, o?: {
96
+ only?: string[];
97
+ }): Promise<AdminPlan & {
98
+ root: CID;
99
+ }>;
100
+ /**
101
+ * A read of the instance's explorer (`/explore…`; root's, #121, #143): the answer DAG-JSON decoded, undefined
102
+ * for a 404 — the client's on its BRC-104 session (target.ts remoteTarget).
103
+ */
104
+ export type ExplorerRead = (path: string) => Promise<unknown>;
105
+ /**
106
+ * The instance as the install plan reads it (plan.ts InstanceView), from its explorer: heads, the genesis
107
+ * (identity, programs), the address book, the route table, and the store by CID —
108
+ * what the management page's view reads (shruggr/skein-site www/app.js `Skein.view`).
109
+ */
110
+ export declare function explorerView(read: ExplorerRead): Promise<InstanceView>;
111
+ /**
112
+ * A grant (#142, #143; `skein-host grant`): the kernel's `grant` operation — `role` (root by default,
113
+ * or an app's `<app>.<role>`) to `key`, or with `remove` from it. One message to box `grant`, sent by
114
+ * root. Nothing is sent when the grants already say so.
115
+ */
116
+ export declare function planGrant(view: InstanceView, key: string, o?: {
117
+ role?: string;
118
+ remove?: boolean;
119
+ }): Promise<AdminPlan>;
120
+ /** One app the management page offers (the site's www/catalog.json): a repository and a commit. */
121
+ export interface CatalogEntry {
122
+ name: string;
123
+ version?: string;
124
+ url: string;
125
+ hash: string;
126
+ image?: string;
127
+ description?: string;
128
+ }
129
+ /** The catalog in the instance's own site tree (its app `site`: www/catalog.json), and the names in it. */
130
+ export declare function catalogOf(view: InstanceView): Promise<CatalogEntry[]>;
131
+ /**
132
+ * The kernel's gate (#143, kernel-zig/src/grants.zig), read from the instance's view: the roles that
133
+ * gate a route's handler (its app's `roles` for its fn; a kernel admin box's is root) and whether
134
+ * `key` passes them. Root passes anything; `user` passes any key; no gating roles: open.
135
+ */
136
+ export declare function gatePasses(view: InstanceView, row: DispatchRow, key: string): Promise<{
137
+ pass: boolean;
138
+ roles: string[];
139
+ }>;