@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,597 @@
1
+ // The app manifest (#72, #76, #77, #79, #143; docs/APPS.md §2): `etc/app.json`
2
+ // in an app's tree, checked and normalised here for the install client
3
+ // (install.ts, `skein install`, #124). Pure: the tree is asked only whether a
4
+ // path exists.
5
+ //
6
+ // What the checks enforce (each failure is one line of ManifestError):
7
+ //
8
+ // kind "app"
9
+ // name lower-case [a-z0-9][a-z0-9._-]*: the app's box, and the prefix of every head it
10
+ // writes (`<name>/app` its root); not a stock box, head or program name (objects,
11
+ // head, dispatch, peers, grant, grants, main, wallet, kernel, frontdoor, messagebox,
12
+ // resolve, …)
13
+ // version semver
14
+ // programs role → "bin/<x>.wasm" | "bin/<x>.cid" (a file in the tree), a bare name: a
15
+ // program the instance already has by name in its genesis, or (#83) a shell
16
+ // program: {code: "shell", modules: {<command>: "bin/<x>.wasm"} (brush and
17
+ // coreutils among them), support?: {<command>: {mount: <absolute path>, files:
18
+ // {<path under mount>: <a file in the tree>}, env?: {<name>: <text>}}},
19
+ // description?} — the record the kernel's shell runs (docs/APPS.md, "A shell
20
+ // program"), its modules and support files this tree's
21
+ // routes[] the routes the app asks for (#143; the kernel's route table, docs/APPS.md §2):
22
+ // {transport?: "mailbox" (default) | "event" | "http" | "libp2p", address, prefix?: true,
23
+ // filters?: [<filter>], handler?: <handler>, …settings}. A mailbox or event route's
24
+ // address is a box relative to the app (#128): "" or the app's name is the app's own
25
+ // box `<name>`, "x" the box `<name>/x` (an empty, "." or ".." segment, whitespace or a
26
+ // control character is refused: no box outside the app's); an http route's a path
27
+ // relative to /<name>/ (a leading "/" too: "/submit" → "/<name>/submit", "/" →
28
+ // "/<name>/"; a ".." or "." segment, an encoded dot or slash (%2e, %2f), a backslash, a
29
+ // NUL or a URL is refused), `prefix: true` for a prefix; a libp2p route's a topic or
30
+ // "/<protocol>" (global, not namespaced; exact). `filters`, in order: "kernel.brc104",
31
+ // "kernel.beef", one of this app's own (a name its `filters` declares) or another
32
+ // app's ("<app>.<filter>"); none on an event route. `handler`: "<role>.<fn>" — a
33
+ // function of one of `programs` — or, for an app with one program, "<fn>"; a mailbox
34
+ // or event route may name only "<role>" (the program stepped, no function named). An
35
+ // http route or a libp2p protocol route ("/<protocol>", #154) with no handler is a READ
36
+ // route: its filters answer (the last one), and nothing is logged; a libp2p topic
37
+ // route names its handler (a topic carries no answer). The same key (transport,
38
+ // address as served, prefix) twice is refused. No `sender` (#143): who may run a
39
+ // function is `roles`'.
40
+ // filters {<filter>: <handler>}: the app's functions any route may list as a filter
41
+ // (#143: `<app>.<filter>`), each "<role>.<fn>", "<fn>" (one program) or "<role>" (its
42
+ // function named as the filter). A filter runs before anything is recorded, in the
43
+ // deterministic profile, and answers {reject} | {answer} | {pass} (docs/APPS.md §2).
44
+ // roles {<role>: [<fn>…]}: the functions each of the app's roles gates (#143), granted as
45
+ // `<app>.<role>` by root; "user" (any principal) and "root" are the standard roles,
46
+ // usable by name. A function no role lists is open to whatever its route's filters
47
+ // let through.
48
+ // provides[] {interface: "<name>/<major>", functions: {<fn>: {writes: bool, args?, answer?}}}
49
+ // — `writes` required; `args`/`answer` shapes: a type name (string, int,
50
+ // ms, bytes, cid, bool, map, any), [shape], or {key[?]: shape}
51
+ // requires[] "<name>/<major>"
52
+ // start, stop {body: {…}}; into the app's box: a mailbox route at it
53
+ // config a map (the programs read it from the head's root record)
54
+ // config.overlay an overlay app (APPS.md §6): {topics?: {<topic>: <role>}, lookups?: {<service>:
55
+ // <role> | {program: <role>, topics?: [<topic>]}}, gossip?: {<topic>: bool}}; the engine
56
+ // is the role `overlay`. Its wiring is derived (overlayWiring) and added to the routes
57
+ // the manifest names itself — an explicit route with the same key wins. `topics` may be absent
58
+ // or empty: a dynamic overlay (Mandala, an AMM) registers its topics by a call at runtime
59
+ // and the engine adds and drops their routes itself; a manifest may still pre-configure
60
+ // topics (OpNS: one global topic). No prefix declarations (#120): any other field is refused.
61
+ //
62
+ // Gone (#143): `dispatch` (now `routes`), `reads` (a read is a route with filters and no handler),
63
+ // `sender`, `$owner` and every `$<name>`; the form before #77 (`handler`, `boxes`, `heads`).
64
+ export class ManifestError extends Error {
65
+ problems;
66
+ constructor(problems) { super(`etc/app.json:\n ${problems.join("\n ")}`); this.problems = problems; }
67
+ }
68
+ export const RESERVED_NAMES = ["objects", "head", "dispatch", "peers", "grant", "grants", "claim", "subscribe", "routes", "main", "sessions", "wallet", "kernel", "frontdoor", "messagebox", "resolve", "billing", "tick", "reads", "root", "user"];
69
+ /** The fields gone, refused: the form before #77 (#79), and #143's. */
70
+ const GONE_FIELDS = {
71
+ handler: "the form before #77 is gone (#79): name routes, and write only heads under the app's name",
72
+ boxes: "the form before #77 is gone (#79): name routes",
73
+ heads: "the form before #77 is gone (#79): an app writes only heads under its own name",
74
+ dispatch: "gone (#143): name `routes` ({transport?, address, prefix?, filters?, handler?, …}, no sender)",
75
+ reads: "gone (#143): a read is a route with filters and no handler (the last filter answers)",
76
+ };
77
+ const NAME = /^[a-z0-9][a-z0-9._-]*$/;
78
+ const ROLE = /^[a-z0-9][a-z0-9_-]*$/;
79
+ const FN = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
80
+ const FILTER = /^[A-Za-z0-9_-]+$/;
81
+ const SEMVER = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
82
+ const INTERFACE = /^[a-z0-9][a-z0-9._-]*\/\d+$/;
83
+ const TYPES = ["string", "int", "ms", "bytes", "cid", "bool", "map", "any"];
84
+ const KERNEL_FILTERS = ["kernel.brc104", "kernel.beef"];
85
+ const isMap = (v) => !!v && typeof v === "object" && !Array.isArray(v);
86
+ /** Why `shape` is not a shape (APPS.md §2), or undefined. */
87
+ export function shapeProblem(shape, at) {
88
+ if (typeof shape === "string")
89
+ return TYPES.includes(shape) ? undefined : `${at}: unknown type ${JSON.stringify(shape)} (${TYPES.join(", ")}, [shape], {key: shape})`;
90
+ if (Array.isArray(shape))
91
+ return shape.length === 1 ? shapeProblem(shape[0], `${at}[]`) : `${at}: an array shape names one element shape`;
92
+ if (isMap(shape)) {
93
+ for (const [k, v] of Object.entries(shape)) {
94
+ const p = shapeProblem(v, `${at}.${k}`);
95
+ if (p)
96
+ return p;
97
+ }
98
+ return undefined;
99
+ }
100
+ return `${at}: a shape is a type name, [shape] or {key: shape}`;
101
+ }
102
+ /**
103
+ * An http route's path as served: relative to /<app>/ (a leading "/" too). Throws
104
+ * on anything that could name a path outside that prefix.
105
+ */
106
+ export function appPath(app, p) {
107
+ if (typeof p !== "string")
108
+ throw new Error("not text");
109
+ if (/^[a-z][a-z0-9+.-]*:/i.test(p))
110
+ throw new Error(`${JSON.stringify(p)} is a URL or a scheme, not a path under /${app}/`);
111
+ if (/[\\\0?#]/.test(p) || /%(2e|2f|5c|00)/i.test(p))
112
+ throw new Error(`${JSON.stringify(p)}: a backslash, NUL, query, fragment or an encoded dot or slash`);
113
+ const segs = p.split("/");
114
+ if (segs.some((s) => s === ".." || s === "."))
115
+ throw new Error(`${JSON.stringify(p)}: a "." or ".." segment`);
116
+ const rest = p.replace(/^\/+/, "").replace(/\/{2,}/g, "/");
117
+ return `/${app}/${rest}`;
118
+ }
119
+ /**
120
+ * A box as the kernel's table holds it (#128): relative to the app, as an http
121
+ * path is — "" or the app's own name is the app's box `<app>`; "x" is `<app>/x`. Throws on a
122
+ * box that is not one: an empty, "." or ".." segment (a leading, trailing or doubled "/"),
123
+ * whitespace or a control character, or more than 128 bytes resolved (the messagebox
124
+ * server's limit).
125
+ */
126
+ export function appBox(app, b) {
127
+ if (typeof b !== "string")
128
+ throw new Error("not text");
129
+ if (b === "" || b === app)
130
+ return app;
131
+ if (/[\s\0-\x1f\x7f]/.test(b))
132
+ throw new Error(`box ${JSON.stringify(b)}: whitespace or a control character`);
133
+ if (b.split("/").some((s) => s === "" || s === "." || s === ".."))
134
+ throw new Error(`box ${JSON.stringify(b)}: an empty, "." or ".." segment (no box outside ${app}/)`);
135
+ const box = `${app}/${b}`;
136
+ if (new TextEncoder().encode(box).length > 128)
137
+ throw new Error(`box ${JSON.stringify(box)}: more than 128 bytes`);
138
+ return box;
139
+ }
140
+ /** A route's address as served: an http path under /<app>/ (appPath); a box under the app's (appBox); a libp2p name as written. */
141
+ export function rowAddress(app, r) {
142
+ return r.transport === "http" ? appPath(app, r.address) : r.transport === "mailbox" || r.transport === "event" ? appBox(app, r.address) : r.address;
143
+ }
144
+ /** A route's key as the kernel's table knows it (dispatch.ts rowKey): `<transport> <address as served>[ prefix]`. */
145
+ export function routeKey(app, r) {
146
+ return `${r.transport} ${rowAddress(app, r)}${r.prefix ? " prefix" : ""}`;
147
+ }
148
+ /**
149
+ * A handler (or a declared filter's function) resolved against the app's programs (#143):
150
+ * "<role>.<fn>"; a bare name that is a role — the program, no function named; any other bare name
151
+ * — a function of the app's one program. Undefined: none of these.
152
+ */
153
+ export function handlerOf(h, roles) {
154
+ if (typeof h !== "string" || !h)
155
+ return undefined;
156
+ const dot = h.indexOf(".");
157
+ if (dot > 0) {
158
+ const role = h.slice(0, dot), fn = h.slice(dot + 1);
159
+ return roles.includes(role) && FN.test(fn) ? { role, fn } : undefined;
160
+ }
161
+ if (roles.includes(h))
162
+ return { role: h };
163
+ return roles.length === 1 && FN.test(h) ? { role: roles[0], fn: h } : undefined;
164
+ }
165
+ /** A route's filter as the kernel's table names it (#143): the kernel's; an own filter's name → `<app>.<name>`; another app's as written. */
166
+ export const filterRef = (app, f) => f.includes(".") ? f : `${app}.${f}`;
167
+ /** The fields a route does not have (#143). */
168
+ const NOT_ROUTE_FIELDS = {
169
+ sender: "gone (#143): a route has no sender — its filters say who a request is from, and `roles` gate functions",
170
+ program: "name the handler: \"<role>.<fn>\" (#143)",
171
+ fn: "name the handler: \"<role>.<fn>\" (#143)",
172
+ filter: "gone (#143): name `filters`, a list (\"kernel.beef\")",
173
+ optional: "gone (#143): a route from a provider is a route like any other",
174
+ app: "app is set by the install",
175
+ };
176
+ /** Why a route is not one (checked against the roles, the declared filters and the app's name), or undefined. */
177
+ function routeProblem(app, r, roles, declared) {
178
+ if (!isMap(r))
179
+ return "not a map";
180
+ for (const [k, why] of Object.entries(NOT_ROUTE_FIELDS))
181
+ if (r[k] !== undefined)
182
+ return `${k}: ${why}`;
183
+ const t = r.transport ?? "mailbox";
184
+ if (t !== "mailbox" && t !== "event" && t !== "http" && t !== "libp2p")
185
+ return `transport ${JSON.stringify(t)} is not mailbox, event, http or libp2p`;
186
+ if (typeof r.address !== "string" || (!r.address && (t === "http" || t === "libp2p")))
187
+ return "address is not text";
188
+ if (t === "mailbox" || t === "event") {
189
+ try {
190
+ appBox(app, r.address);
191
+ }
192
+ catch (e) {
193
+ return e.message;
194
+ }
195
+ if (r.prefix !== undefined)
196
+ return `a ${t} route has no prefix`;
197
+ }
198
+ else if (t === "http") {
199
+ try {
200
+ appPath(app, r.address);
201
+ }
202
+ catch (e) {
203
+ return e.message;
204
+ }
205
+ if (r.prefix !== undefined && r.prefix !== true)
206
+ return "prefix is true or absent";
207
+ }
208
+ else {
209
+ if (/[\s\0]/.test(r.address))
210
+ return `address ${JSON.stringify(r.address)} is not a topic or /protocol`;
211
+ if (r.prefix !== undefined)
212
+ return "a libp2p route has no prefix";
213
+ }
214
+ const filters = r.filters ?? [];
215
+ if (!Array.isArray(filters) || filters.some((f) => typeof f !== "string"))
216
+ return "filters is a list of filter names";
217
+ if (filters.length && t === "event" && !(filters.length === 1 && filters[0] === "kernel.beef"))
218
+ return "an event route has no filters but kernel.beef (an event is the host's wiring: the door validates a BEEF, #121)";
219
+ for (const f of filters) {
220
+ if (f.startsWith("kernel.")) {
221
+ if (!KERNEL_FILTERS.includes(f))
222
+ return `filter ${f}: the kernel's are ${KERNEL_FILTERS.join(", ")}`;
223
+ continue;
224
+ }
225
+ const dot = f.lastIndexOf(".");
226
+ if (dot < 0) {
227
+ if (!declared(f))
228
+ return `filter ${f}: not one of this app's (its \`filters\` declare none by that name)`;
229
+ continue;
230
+ }
231
+ if (!NAME.test(f.slice(0, dot)) || !FILTER.test(f.slice(dot + 1)))
232
+ return `filter ${JSON.stringify(f)}: not <app>.<filter>`;
233
+ }
234
+ if (r.handler === undefined) {
235
+ if (t !== "http" && !(t === "libp2p" && r.address.startsWith("/")))
236
+ return "handler: a mailbox, event or libp2p topic route names its handler (\"<role>.<fn>\")";
237
+ if (!filters.length)
238
+ return "a read route (no handler) names its filters: the last one answers";
239
+ return undefined;
240
+ }
241
+ const h = handlerOf(r.handler, roles);
242
+ if (!h)
243
+ return `handler ${JSON.stringify(r.handler)} is not "<role>.<fn>" of a role in programs${roles.length === 1 ? ", or a function of the app's one program" : ""}`;
244
+ if (!h.fn && (t === "http" || t === "libp2p"))
245
+ return `handler ${JSON.stringify(r.handler)}: an ${t} route names a function ("<role>.<fn>")`;
246
+ return undefined;
247
+ }
248
+ /** Check and normalise a manifest. `has(path)`: whether the tree has a file at the path. */
249
+ export function checkManifest(json, has) {
250
+ const bad = [];
251
+ if (!isMap(json))
252
+ throw new ManifestError(["not a JSON object"]);
253
+ const m = json;
254
+ if (m.kind !== "app")
255
+ bad.push(`kind: want "app"`);
256
+ const name = typeof m.name === "string" ? m.name : "";
257
+ if (!NAME.test(name))
258
+ bad.push(`name: ${JSON.stringify(m.name)} is not a name ([a-z0-9][a-z0-9._-]*)`);
259
+ else if (RESERVED_NAMES.includes(name))
260
+ bad.push(`name: ${name} is a stock box, head, program or role of the instance`);
261
+ const app = name || "app";
262
+ if (typeof m.version !== "string" || !SEMVER.test(m.version))
263
+ bad.push(`version: ${JSON.stringify(m.version)} is not semver`);
264
+ if (m.description !== undefined && typeof m.description !== "string")
265
+ bad.push("description: not text");
266
+ if (m.config !== undefined && !isMap(m.config))
267
+ bad.push("config: not a map");
268
+ for (const [f, why] of Object.entries(GONE_FIELDS))
269
+ if (m[f] !== undefined)
270
+ bad.push(`${f}: ${why}`);
271
+ // programs
272
+ const sources = {};
273
+ if (!isMap(m.programs))
274
+ bad.push("programs: want {role: \"bin/<x>.wasm\" | \"bin/<x>.cid\" | <an instance program's name> | {code: \"shell\", modules, support?}}");
275
+ else
276
+ for (const [role, p] of Object.entries(m.programs)) {
277
+ if (!ROLE.test(role)) {
278
+ bad.push(`programs: ${JSON.stringify(role)} is not a role name ([a-z0-9][a-z0-9_-]*)`);
279
+ continue;
280
+ }
281
+ if (isMap(p)) {
282
+ const sh = shellSource(p, has, `programs.${role}`);
283
+ if (Array.isArray(sh))
284
+ bad.push(...sh);
285
+ else
286
+ sources[role] = sh;
287
+ continue;
288
+ }
289
+ const file = typeof p === "string" ? /^bin\/([A-Za-z0-9._-]+)\.(wasm|cid)$/.exec(p) : null;
290
+ if (file) {
291
+ if (!has(p))
292
+ bad.push(`programs.${role}: ${p} is not in the tree`);
293
+ sources[role] = { kind: file[2], path: p, name: file[1] };
294
+ }
295
+ else if (typeof p === "string" && /^[a-z0-9][a-z0-9_-]*$/.test(p))
296
+ sources[role] = { kind: "instance", name: p };
297
+ else
298
+ bad.push(`programs.${role}: ${JSON.stringify(p)} is not bin/<x>.wasm, bin/<x>.cid, a program name or a shell program`);
299
+ }
300
+ const roleNames = Object.keys(sources);
301
+ // filters (#143): the app's functions any route may list
302
+ const filters = {};
303
+ if (m.filters !== undefined && !isMap(m.filters))
304
+ bad.push("filters: want {<filter>: \"<role>.<fn>\" | \"<fn>\" | \"<role>\"}");
305
+ for (const [f, h] of Object.entries(isMap(m.filters) ? m.filters : {})) {
306
+ if (!FILTER.test(f)) {
307
+ bad.push(`filters: ${JSON.stringify(f)} is not a filter name ([A-Za-z0-9_-]+)`);
308
+ continue;
309
+ }
310
+ if (!handlerOf(h, roleNames)) {
311
+ bad.push(`filters.${f}: ${JSON.stringify(h)} is not "<role>.<fn>" of a role in programs, a role, or a function of the app's one program`);
312
+ continue;
313
+ }
314
+ filters[f] = h;
315
+ }
316
+ // roles (#143): the functions each role gates
317
+ if (m.roles !== undefined && !isMap(m.roles))
318
+ bad.push("roles: want {<role>: [<fn>…]}");
319
+ for (const [r, fns] of Object.entries(isMap(m.roles) ? m.roles : {})) {
320
+ if (!ROLE.test(r))
321
+ bad.push(`roles: ${JSON.stringify(r)} is not a role name ([a-z0-9][a-z0-9_-]*; "root" and "user" are the standard roles)`);
322
+ else if (!Array.isArray(fns) || fns.some((f) => typeof f !== "string" || !FN.test(f)))
323
+ bad.push(`roles.${r}: want a list of the functions it gates`);
324
+ }
325
+ // routes (#143)
326
+ const routes = [];
327
+ const addRoute = (r, at) => {
328
+ const k = routeKey(app, r);
329
+ if (routes.some((x) => routeKey(app, x) === k)) {
330
+ bad.push(`${at}: ${k} twice`);
331
+ return;
332
+ }
333
+ routes.push(r);
334
+ };
335
+ if (m.routes !== undefined && !Array.isArray(m.routes))
336
+ bad.push("routes: not a list");
337
+ for (const [i, r] of (Array.isArray(m.routes) ? m.routes : []).entries()) {
338
+ const why = routeProblem(app, r, roleNames, (f) => f in filters);
339
+ if (why) {
340
+ bad.push(`routes[${i}]: ${why}`);
341
+ continue;
342
+ }
343
+ const route = r;
344
+ addRoute({ ...route, transport: route.transport ?? "mailbox" }, `routes[${i}]`);
345
+ }
346
+ // provides, requires
347
+ const provides = [];
348
+ if (m.provides !== undefined && !Array.isArray(m.provides))
349
+ bad.push("provides: not a list");
350
+ for (const [i, p] of (Array.isArray(m.provides) ? m.provides : []).entries()) {
351
+ if (!isMap(p) || typeof p.interface !== "string" || !INTERFACE.test(p.interface)) {
352
+ bad.push(`provides[${i}]: interface is not <name>/<major>`);
353
+ continue;
354
+ }
355
+ if (!isMap(p.functions) || !Object.keys(p.functions).length) {
356
+ bad.push(`provides[${i}] (${p.interface}): functions is a non-empty map`);
357
+ continue;
358
+ }
359
+ for (const [f, d] of Object.entries(p.functions)) {
360
+ const at = `provides[${i}].${p.interface}.${f}`;
361
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(f))
362
+ bad.push(`${at}: not a function name`);
363
+ if (!isMap(d) || typeof d.writes !== "boolean") {
364
+ bad.push(`${at}: writes (true | false) is required`);
365
+ continue;
366
+ }
367
+ for (const k of ["args", "answer"])
368
+ if (d[k] !== undefined) {
369
+ const why = shapeProblem(d[k], `${at}.${k}`);
370
+ if (why)
371
+ bad.push(why);
372
+ }
373
+ }
374
+ provides.push(p);
375
+ }
376
+ const requires = [];
377
+ if (m.requires !== undefined && !Array.isArray(m.requires))
378
+ bad.push("requires: not a list");
379
+ for (const r of Array.isArray(m.requires) ? m.requires : []) {
380
+ if (typeof r !== "string" || !INTERFACE.test(r))
381
+ bad.push(`requires: ${JSON.stringify(r)} is not <name>/<major>`);
382
+ else
383
+ requires.push(r);
384
+ }
385
+ // config.overlay: the overlay app's wiring, derived (APPS.md §6), under what the manifest names itself.
386
+ const derived = { routes: [] };
387
+ const ov = isMap(m.config) ? m.config.overlay : undefined;
388
+ if (ov !== undefined) {
389
+ const w = overlayWiring(app, ov, (r) => r in sources);
390
+ if (Array.isArray(w))
391
+ bad.push(...w.map((p) => `config.overlay: ${p}`));
392
+ else {
393
+ const have = new Set(routes.map((r) => routeKey(app, r)));
394
+ for (const r of w.routes)
395
+ if (!have.has(routeKey(app, r))) {
396
+ routes.push(r);
397
+ derived.routes.push(routeKey(app, r));
398
+ }
399
+ // The derived /lookup's filter: the engine's `lookup` (a manifest may name its own).
400
+ if (!("lookup" in filters))
401
+ filters.lookup = `${OVERLAY_ROLE}.lookup`;
402
+ }
403
+ }
404
+ // start, stop
405
+ for (const k of ["start", "stop"]) {
406
+ if (m[k] === undefined)
407
+ continue;
408
+ if (!isMap(m[k]) || !isMap(m[k].body))
409
+ bad.push(`${k}: want {body: {…}}`);
410
+ }
411
+ if ((m.start !== undefined || m.stop !== undefined) && !routes.some((r) => r.transport === "mailbox" && rowAddress(app, r) === name))
412
+ bad.push(`start/stop go into the app's box: routes must have a mailbox route for ${name}`);
413
+ if (bad.length)
414
+ throw new ManifestError(bad);
415
+ const { routes: _r, filters: _f, ...rest } = m;
416
+ void _r;
417
+ void _f;
418
+ const out = { ...rest, routes, ...(Object.keys(filters).length ? { filters } : {}), provides, requires };
419
+ return { manifest: out, sources, derived };
420
+ }
421
+ // ---------------------------------------------------------------- a shell program (#83; APPS.md "A shell program")
422
+ const COMMAND = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
423
+ /** A path in the tree: relative, no "." or ".." segment, no backslash or NUL. */
424
+ const isTreePath = (p) => typeof p === "string" && p.length > 0 && !p.startsWith("/") && !/[\\\0]/.test(p) && p.split("/").every((x) => x !== "" && x !== "." && x !== "..");
425
+ /**
426
+ * A shell program as the manifest names it (#83), checked: `code: "shell"`;
427
+ * `modules` command → bin/<x>.wasm in the tree, brush and coreutils among
428
+ * them (every other name a command the shell runs; two names may share a
429
+ * module: `node` is qjs); `support` per command of `modules`: an absolute
430
+ * `mount`, `files` (path under the mount → a file in the tree), `env` text →
431
+ * text. The install turns it into the record the kernel's shell runs.
432
+ */
433
+ export function shellSource(p, has, at) {
434
+ const bad = [];
435
+ if (p.code !== "shell")
436
+ return [`${at}: a program given as a map is a shell program: {code: "shell", modules, support?}`];
437
+ for (const k of Object.keys(p))
438
+ if (!["code", "modules", "support", "description"].includes(k))
439
+ bad.push(`${at}.${k}: not a field of a shell program (code, modules, support, description)`);
440
+ if (p.description !== undefined && typeof p.description !== "string")
441
+ bad.push(`${at}.description: not text`);
442
+ const modules = {};
443
+ if (!isMap(p.modules))
444
+ bad.push(`${at}.modules: want {<command>: "bin/<x>.wasm"}`);
445
+ else {
446
+ for (const [cmd, path] of Object.entries(p.modules)) {
447
+ if (!COMMAND.test(cmd))
448
+ bad.push(`${at}.modules: ${JSON.stringify(cmd)} is not a command name`);
449
+ else if (typeof path !== "string" || !/^bin\/[A-Za-z0-9._-]+\.wasm$/.test(path))
450
+ bad.push(`${at}.modules.${cmd}: ${JSON.stringify(path)} is not bin/<x>.wasm`);
451
+ else if (!has(path))
452
+ bad.push(`${at}.modules.${cmd}: ${path} is not in the tree`);
453
+ else
454
+ modules[cmd] = path;
455
+ }
456
+ for (const need of ["brush", "coreutils"])
457
+ if (!(need in p.modules))
458
+ bad.push(`${at}.modules: no ${need} (the shell is brush over coreutils)`);
459
+ }
460
+ const support = {};
461
+ if (p.support !== undefined && !isMap(p.support))
462
+ bad.push(`${at}.support: want {<command>: {mount, files, env?}}`);
463
+ for (const [cmd, x] of Object.entries(isMap(p.support) ? p.support : {})) {
464
+ const where = `${at}.support.${cmd}`;
465
+ if (!isMap(p.modules) || !(cmd in p.modules)) {
466
+ bad.push(`${where}: ${cmd} is not a command in modules`);
467
+ continue;
468
+ }
469
+ if (!isMap(x)) {
470
+ bad.push(`${where}: want {mount, files, env?}`);
471
+ continue;
472
+ }
473
+ if (typeof x.mount !== "string" || !x.mount.startsWith("/") || x.mount.split("/").some((s) => s === "." || s === ".."))
474
+ bad.push(`${where}.mount: not an absolute path`);
475
+ const files = {};
476
+ if (!isMap(x.files))
477
+ bad.push(`${where}.files: want {<path under the mount>: <a file in the tree>}`);
478
+ else
479
+ for (const [under, path] of Object.entries(x.files)) {
480
+ if (!isTreePath(under))
481
+ bad.push(`${where}.files: ${JSON.stringify(under)} is not a relative path`);
482
+ else if (!isTreePath(path) || !has(path))
483
+ bad.push(`${where}.files.${under}: ${JSON.stringify(path)} is not a file in the tree`);
484
+ else
485
+ files[under] = path;
486
+ }
487
+ const env = {};
488
+ if (x.env !== undefined && !isMap(x.env))
489
+ bad.push(`${where}.env: want {<name>: <text>}`);
490
+ for (const [k, v] of Object.entries(isMap(x.env) ? x.env : {})) {
491
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k) || typeof v !== "string")
492
+ bad.push(`${where}.env.${k}: want a name and text`);
493
+ else
494
+ env[k] = v;
495
+ }
496
+ support[cmd] = { mount: String(x.mount), files, env };
497
+ }
498
+ if (bad.length)
499
+ return bad;
500
+ return { kind: "shell", modules, support, ...(typeof p.description === "string" ? { description: p.description } : {}) };
501
+ }
502
+ // ---------------------------------------------------------------- an overlay app's wiring (APPS.md §6)
503
+ /** The role of an overlay app's engine (its `config.overlay` is the engine's). */
504
+ export const OVERLAY_ROLE = "overlay";
505
+ const TOPIC = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
506
+ /** The fields of `config.overlay` (`status` is refused with its own reason). */
507
+ const OVERLAY_FIELDS = ["topics", "lookups", "gossip", "market", "validator", "terms"];
508
+ /**
509
+ * The wiring `config.overlay` asks for (APPS.md §6, #79, #143), or its problems. For each topic:
510
+ * the libp2p routes `<topic>` → submit (kernel.beef), `<topic>-admit` → peerAdmit, `<topic>-proof`
511
+ * → peerProof; the http route `/submit` (kernel.beef: a submission validates itself — #135, signed
512
+ * or validated) and `/lookup` (BRC-24's POST: answered by the engine's `lookup`, a filter — a read
513
+ * route, nothing logged); and the app's own box twice: an `event` route (what its libp2p routes
514
+ * admit — a gossiped submission, a peer's admit) and a `mailbox` route (its own watch of a
515
+ * submission the chain app has accepted, sent by the host's loopback). All to the role `overlay`,
516
+ * the engine, which the manifest must declare `lookup` a filter of. No `chain` or `status` box (the
517
+ * chain app's): the engine writes `<app>/…` only. No topics is accepted: the box routes, `/submit`
518
+ * and `/lookup` and no per-topic routes — the topics registered at runtime get theirs from the
519
+ * engine (#120). `market` ({window: <ms>}), `validator` ({every: <ms>}) and `terms` (a map, the
520
+ * beat's initial terms: skein-overlay 0.17.0) are the engine's own settings, read from the app record:
521
+ * checked, no routes derived. A field other than these is
522
+ * refused (no prefix declarations, #120). `isRole` tells which roles the manifest has.
523
+ */
524
+ export function overlayWiring(app, ov, isRole) {
525
+ const bad = [];
526
+ if (!isMap(ov))
527
+ return ["want {topics?, lookups?, gossip?, market?, validator?, terms?}"];
528
+ if (!isRole(OVERLAY_ROLE))
529
+ bad.push(`the engine is the role "${OVERLAY_ROLE}": programs has none`);
530
+ const topics = [];
531
+ if (ov.topics !== undefined && !isMap(ov.topics))
532
+ bad.push("topics: want {<topic>: <role>}");
533
+ else
534
+ for (const [t, role] of Object.entries(isMap(ov.topics) ? ov.topics : {})) {
535
+ if (!TOPIC.test(t))
536
+ bad.push(`topics: ${JSON.stringify(t)} is not a topic name`);
537
+ else if (typeof role !== "string" || !isRole(role))
538
+ bad.push(`topics.${t}: ${JSON.stringify(role)} is not a role in programs`);
539
+ else
540
+ topics.push(t);
541
+ }
542
+ const unknown = Object.keys(ov).filter((k) => !OVERLAY_FIELDS.includes(k) && k !== "status");
543
+ if (unknown.length)
544
+ bad.push(`${unknown.join(", ")}: not a field (want ${OVERLAY_FIELDS.join(", ")}; no prefix declarations, #120)`);
545
+ if (ov.lookups !== undefined && !isMap(ov.lookups))
546
+ bad.push("lookups: want {<service>: <role> | {program: <role>, topics?: [<topic>]}}");
547
+ for (const [service, l] of Object.entries(isMap(ov.lookups) ? ov.lookups : {})) {
548
+ if (!TOPIC.test(service)) {
549
+ bad.push(`lookups: ${JSON.stringify(service)} is not a service name`);
550
+ continue;
551
+ }
552
+ const role = typeof l === "string" ? l : isMap(l) ? l.program : undefined;
553
+ if (typeof role !== "string" || !isRole(role))
554
+ bad.push(`lookups.${service}: program ${JSON.stringify(role)} is not a role in programs`);
555
+ if (isMap(l) && l.topics !== undefined && (!Array.isArray(l.topics) || l.topics.some((t) => typeof t !== "string" || !isMap(ov.topics) || !(t in ov.topics))))
556
+ bad.push(`lookups.${service}.topics: want a list of the topics the overlay serves`);
557
+ const extra = isMap(l) ? Object.keys(l).filter((k) => k !== "program" && k !== "topics") : [];
558
+ if (extra.length)
559
+ bad.push(`lookups.${service}: ${extra.join(", ")}: not a field (want program, topics)`);
560
+ }
561
+ if (ov.status !== undefined)
562
+ bad.push("status: gone (#79): statuses are the chain app's (its `status` route); the overlay admits on the chain app's answer");
563
+ if (ov.gossip !== undefined && (!isMap(ov.gossip) || Object.entries(ov.gossip).some(([t, on]) => typeof on !== "boolean" || !topics.includes(t))))
564
+ bad.push("gossip: want {<topic the overlay serves>: true | false}");
565
+ const ms = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
566
+ if (ov.market !== undefined && (!isMap(ov.market) || !ms(ov.market.window) || Object.keys(ov.market).some((k) => k !== "window")))
567
+ bad.push("market: want {window: <ms>}");
568
+ if (ov.validator !== undefined && (!isMap(ov.validator) || !ms(ov.validator.every) || Object.keys(ov.validator).some((k) => k !== "every")))
569
+ bad.push("validator: want {every: <ms>}");
570
+ // skein-overlay 0.17.0 (skein-amm#4): the beat's initial terms, opaque to the host (the head <app>/terms wins once written).
571
+ if (ov.terms !== undefined && ov.terms !== null && !isMap(ov.terms))
572
+ bad.push("terms: want a map (the beat's initial terms) or null");
573
+ if (bad.length)
574
+ return bad;
575
+ const engine = (fn) => `${OVERLAY_ROLE}.${fn}`;
576
+ // #121, #143: a submission's BEEF is decoded at the kernel's door (kernel.beef): the handler gets its pointer record.
577
+ const beef = ["kernel.beef"];
578
+ return {
579
+ topics,
580
+ routes: [
581
+ { transport: "event", address: app, handler: OVERLAY_ROLE },
582
+ { transport: "mailbox", address: app, handler: OVERLAY_ROLE },
583
+ { transport: "http", address: "/submit", filters: beef, handler: engine("submit") },
584
+ { transport: "http", address: "/lookup", filters: [`${app}.lookup`] },
585
+ ...topics.flatMap((t) => [
586
+ { transport: "libp2p", address: t, filters: beef, handler: engine("submit") },
587
+ { transport: "libp2p", address: `${t}-admit`, handler: engine("peerAdmit") },
588
+ { transport: "libp2p", address: `${t}-proof`, handler: engine("peerProof") },
589
+ ]),
590
+ ],
591
+ };
592
+ }
593
+ /** The interfaces `requires` names that no installed app provides. */
594
+ export function missingInterfaces(requires, installed) {
595
+ const have = new Set(installed.flatMap((a) => (a.provides ?? []).map((p) => p.interface)));
596
+ return requires.filter((r) => !have.has(r));
597
+ }