@nervur-org/dock 0.3.0

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 (285) hide show
  1. package/GETTING_STARTED.md +128 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +37 -0
  5. package/api/quo-api.md +196 -0
  6. package/api/route.ts +227 -0
  7. package/beings/GLOSSARY.md +118 -0
  8. package/beings/WORLDS.md +567 -0
  9. package/beings/avatar.ts +101 -0
  10. package/beings/carry.ts +159 -0
  11. package/beings/clock.ts +552 -0
  12. package/beings/courier.ts +107 -0
  13. package/beings/desk.ts +128 -0
  14. package/beings/doorbell.ts +129 -0
  15. package/beings/envoy.ts +218 -0
  16. package/beings/index.ts +16 -0
  17. package/beings/join.ts +51 -0
  18. package/beings/link.ts +104 -0
  19. package/beings/look.ts +139 -0
  20. package/beings/porter.ts +43 -0
  21. package/beings/presence.ts +39 -0
  22. package/beings/quo-app-bells.md +280 -0
  23. package/beings/quo-dock.md +1942 -0
  24. package/beings/setup.ts +45 -0
  25. package/beings/side.ts +60 -0
  26. package/beings/user.ts +459 -0
  27. package/beings/way.ts +65 -0
  28. package/cli/bundles.ts +20 -0
  29. package/cli/client.ts +73 -0
  30. package/cli/daemon.ts +581 -0
  31. package/cli/estate/Caddyfile +33 -0
  32. package/cli/estate/quo.service +35 -0
  33. package/cli/estate/systems.quo.plist +44 -0
  34. package/cli/estate.ts +48 -0
  35. package/cli/folder.ts +27 -0
  36. package/cli/oauth.ts +32 -0
  37. package/cli/pilot.ts +38 -0
  38. package/cli/quo-dock.md +1942 -0
  39. package/cli/quo.ts +203 -0
  40. package/cli/stand.ts +160 -0
  41. package/dist/api/route.d.ts +21 -0
  42. package/dist/api/route.js +193 -0
  43. package/dist/beings/avatar.d.ts +32 -0
  44. package/dist/beings/avatar.js +100 -0
  45. package/dist/beings/carry.d.ts +12 -0
  46. package/dist/beings/carry.js +174 -0
  47. package/dist/beings/clock.d.ts +161 -0
  48. package/dist/beings/clock.js +528 -0
  49. package/dist/beings/courier.d.ts +50 -0
  50. package/dist/beings/courier.js +96 -0
  51. package/dist/beings/desk.d.ts +53 -0
  52. package/dist/beings/desk.js +135 -0
  53. package/dist/beings/doorbell.d.ts +78 -0
  54. package/dist/beings/doorbell.js +133 -0
  55. package/dist/beings/envoy.d.ts +54 -0
  56. package/dist/beings/envoy.js +211 -0
  57. package/dist/beings/index.d.ts +14 -0
  58. package/dist/beings/index.js +16 -0
  59. package/dist/beings/join.d.ts +15 -0
  60. package/dist/beings/join.js +42 -0
  61. package/dist/beings/link.d.ts +14 -0
  62. package/dist/beings/link.js +91 -0
  63. package/dist/beings/look.d.ts +30 -0
  64. package/dist/beings/look.js +104 -0
  65. package/dist/beings/porter.d.ts +14 -0
  66. package/dist/beings/porter.js +41 -0
  67. package/dist/beings/presence.d.ts +8 -0
  68. package/dist/beings/presence.js +21 -0
  69. package/dist/beings/setup.d.ts +3 -0
  70. package/dist/beings/setup.js +39 -0
  71. package/dist/beings/side.d.ts +32 -0
  72. package/dist/beings/side.js +37 -0
  73. package/dist/beings/user.d.ts +315 -0
  74. package/dist/beings/user.js +462 -0
  75. package/dist/beings/way.d.ts +19 -0
  76. package/dist/beings/way.js +28 -0
  77. package/dist/cli/bundles.d.ts +5 -0
  78. package/dist/cli/bundles.js +17 -0
  79. package/dist/cli/client.d.ts +15 -0
  80. package/dist/cli/client.js +58 -0
  81. package/dist/cli/daemon.d.ts +50 -0
  82. package/dist/cli/daemon.js +584 -0
  83. package/dist/cli/estate/Caddyfile +33 -0
  84. package/dist/cli/estate/quo.service +35 -0
  85. package/dist/cli/estate/systems.quo.plist +44 -0
  86. package/dist/cli/estate.d.ts +5 -0
  87. package/dist/cli/estate.js +50 -0
  88. package/dist/cli/folder.d.ts +2 -0
  89. package/dist/cli/folder.js +25 -0
  90. package/dist/cli/oauth.d.ts +2 -0
  91. package/dist/cli/oauth.js +32 -0
  92. package/dist/cli/pilot.d.ts +5 -0
  93. package/dist/cli/pilot.js +22 -0
  94. package/dist/cli/quo.d.ts +2 -0
  95. package/dist/cli/quo.js +201 -0
  96. package/dist/cli/stand.d.ts +37 -0
  97. package/dist/cli/stand.js +110 -0
  98. package/dist/dock/agents.d.ts +102 -0
  99. package/dist/dock/agents.js +105 -0
  100. package/dist/dock/apns.d.ts +27 -0
  101. package/dist/dock/apns.js +98 -0
  102. package/dist/dock/console.d.ts +6 -0
  103. package/dist/dock/console.js +20 -0
  104. package/dist/dock/dialers.d.ts +56 -0
  105. package/dist/dock/dialers.js +75 -0
  106. package/dist/dock/faculty.d.ts +24 -0
  107. package/dist/dock/faculty.js +142 -0
  108. package/dist/dock/fcm.d.ts +13 -0
  109. package/dist/dock/fcm.js +77 -0
  110. package/dist/dock/grants.d.ts +41 -0
  111. package/dist/dock/grants.js +99 -0
  112. package/dist/dock/http2.d.ts +4 -0
  113. package/dist/dock/http2.js +68 -0
  114. package/dist/dock/index.d.ts +41 -0
  115. package/dist/dock/index.js +113 -0
  116. package/dist/dock/local.d.ts +13 -0
  117. package/dist/dock/local.js +44 -0
  118. package/dist/dock/location.d.ts +19 -0
  119. package/dist/dock/location.js +11 -0
  120. package/dist/dock/notifications.d.ts +38 -0
  121. package/dist/dock/notifications.js +60 -0
  122. package/dist/dock/push.d.ts +79 -0
  123. package/dist/dock/push.js +132 -0
  124. package/dist/dock/service.d.ts +4 -0
  125. package/dist/dock/service.js +107 -0
  126. package/dist/dock/timer.d.ts +52 -0
  127. package/dist/dock/timer.js +149 -0
  128. package/dist/dock/webpush.d.ts +12 -0
  129. package/dist/dock/webpush.js +144 -0
  130. package/dist/harbor/browser.d.ts +15 -0
  131. package/dist/harbor/browser.js +82 -0
  132. package/dist/harbor/capacitor.d.ts +28 -0
  133. package/dist/harbor/capacitor.js +218 -0
  134. package/dist/harbor/disk.d.ts +27 -0
  135. package/dist/harbor/disk.js +249 -0
  136. package/dist/harbor/edge/alarm.d.ts +10 -0
  137. package/dist/harbor/edge/alarm.js +38 -0
  138. package/dist/harbor/edge/edge.d.ts +54 -0
  139. package/dist/harbor/edge/edge.js +370 -0
  140. package/dist/harbor/edge/exercise.d.ts +7 -0
  141. package/dist/harbor/edge/exercise.js +232 -0
  142. package/dist/harbor/edge/given.d.ts +26 -0
  143. package/dist/harbor/edge/given.js +42 -0
  144. package/dist/harbor/edge/platform.d.ts +39 -0
  145. package/dist/harbor/edge/storage.d.ts +30 -0
  146. package/dist/harbor/edge/storage.js +180 -0
  147. package/dist/harbor/edge/worker.d.ts +17 -0
  148. package/dist/harbor/edge/worker.js +67 -0
  149. package/dist/harbor/files.d.ts +15 -0
  150. package/dist/harbor/files.js +220 -0
  151. package/dist/harbor/http.d.ts +19 -0
  152. package/dist/harbor/http.js +177 -0
  153. package/dist/harbor/idb.d.ts +15 -0
  154. package/dist/harbor/idb.js +0 -0
  155. package/dist/harbor/quo.d.ts +10 -0
  156. package/dist/harbor/quo.js +42 -0
  157. package/dist/harbor/resolve.d.ts +16 -0
  158. package/dist/harbor/resolve.js +17 -0
  159. package/dist/harbor/seal.d.ts +3 -0
  160. package/dist/harbor/seal.js +25 -0
  161. package/dist/harbor/sealed.d.ts +35 -0
  162. package/dist/harbor/sealed.js +162 -0
  163. package/dist/harbor/tauri.d.ts +16 -0
  164. package/dist/harbor/tauri.js +59 -0
  165. package/dist/harbor/ward.d.ts +9 -0
  166. package/dist/harbor/ward.js +15 -0
  167. package/dist/human/app.d.ts +32 -0
  168. package/dist/human/app.js +527 -0
  169. package/dist/human/apps.d.ts +10 -0
  170. package/dist/human/apps.js +29 -0
  171. package/dist/human/dom.d.ts +2 -0
  172. package/dist/human/dom.js +15 -0
  173. package/dist/human/door.d.ts +19 -0
  174. package/dist/human/door.js +92 -0
  175. package/dist/human/guest.d.ts +3 -0
  176. package/dist/human/guest.js +33 -0
  177. package/dist/human/html.d.ts +55 -0
  178. package/dist/human/html.js +451 -0
  179. package/dist/human/local.d.ts +7 -0
  180. package/dist/human/local.js +10 -0
  181. package/dist/human/mark.d.ts +8 -0
  182. package/dist/human/mark.js +47 -0
  183. package/dist/human/move.d.ts +17 -0
  184. package/dist/human/move.js +90 -0
  185. package/dist/human/screen.d.ts +19 -0
  186. package/dist/human/screen.js +119 -0
  187. package/dist/human/style.d.ts +2 -0
  188. package/dist/human/style.js +60 -0
  189. package/dist/human/tab.bundle.txt +4775 -0
  190. package/dist/human/tab.d.ts +15 -0
  191. package/dist/human/tab.js +409 -0
  192. package/dist/human/tree.d.ts +50 -0
  193. package/dist/human/tree.js +157 -0
  194. package/dist/human/wake.d.ts +34 -0
  195. package/dist/human/wake.js +179 -0
  196. package/dist/human/web.d.ts +33 -0
  197. package/dist/human/web.js +184 -0
  198. package/dist/human/worker.bundle.txt +3868 -0
  199. package/dist/human/worker.d.ts +1 -0
  200. package/dist/human/worker.js +81 -0
  201. package/dist/human/worlds.d.ts +24 -0
  202. package/dist/human/worlds.js +99 -0
  203. package/dist/mcp/agent.d.ts +10 -0
  204. package/dist/mcp/agent.js +78 -0
  205. package/dist/mcp/allow.d.ts +20 -0
  206. package/dist/mcp/allow.js +88 -0
  207. package/dist/mcp/http.d.ts +27 -0
  208. package/dist/mcp/http.js +92 -0
  209. package/dist/mcp/oauth.d.ts +91 -0
  210. package/dist/mcp/oauth.js +241 -0
  211. package/dist/mcp/route.d.ts +15 -0
  212. package/dist/mcp/route.js +42 -0
  213. package/dist/mcp/runner.d.ts +45 -0
  214. package/dist/mcp/runner.js +99 -0
  215. package/dist/mcp/server.d.ts +11 -0
  216. package/dist/mcp/server.js +102 -0
  217. package/dock/agents.ts +121 -0
  218. package/dock/apns.ts +138 -0
  219. package/dock/console.ts +23 -0
  220. package/dock/dialers.ts +85 -0
  221. package/dock/faculty.ts +148 -0
  222. package/dock/fcm.ts +103 -0
  223. package/dock/grants.ts +111 -0
  224. package/dock/http2.ts +70 -0
  225. package/dock/index.ts +133 -0
  226. package/dock/local.ts +48 -0
  227. package/dock/location.ts +22 -0
  228. package/dock/notifications.ts +67 -0
  229. package/dock/push.ts +161 -0
  230. package/dock/quo-app-bells.md +280 -0
  231. package/dock/quo-dock.md +1942 -0
  232. package/dock/service.ts +100 -0
  233. package/dock/timer.ts +174 -0
  234. package/dock/webpush.ts +182 -0
  235. package/harbor/browser.ts +97 -0
  236. package/harbor/capacitor.ts +223 -0
  237. package/harbor/disk.ts +237 -0
  238. package/harbor/edge/alarm.ts +48 -0
  239. package/harbor/edge/edge.ts +407 -0
  240. package/harbor/edge/exercise.ts +241 -0
  241. package/harbor/edge/given.ts +106 -0
  242. package/harbor/edge/platform.d.ts +39 -0
  243. package/harbor/edge/storage.ts +179 -0
  244. package/harbor/edge/text.d.ts +15 -0
  245. package/harbor/edge/worker.ts +70 -0
  246. package/harbor/edge/wrangler.toml +30 -0
  247. package/harbor/files.ts +221 -0
  248. package/harbor/http.ts +188 -0
  249. package/harbor/idb.ts +0 -0
  250. package/harbor/quo-harbor.md +850 -0
  251. package/harbor/quo.ts +44 -0
  252. package/harbor/resolve.ts +31 -0
  253. package/harbor/seal.ts +26 -0
  254. package/harbor/sealed.ts +183 -0
  255. package/harbor/tauri.ts +79 -0
  256. package/harbor/ward.ts +28 -0
  257. package/human/app.ts +580 -0
  258. package/human/apps.ts +47 -0
  259. package/human/dom.ts +21 -0
  260. package/human/door.ts +113 -0
  261. package/human/guest.ts +34 -0
  262. package/human/html.ts +490 -0
  263. package/human/local.ts +30 -0
  264. package/human/mark.ts +53 -0
  265. package/human/move.ts +122 -0
  266. package/human/quo-human.md +1042 -0
  267. package/human/screen.ts +136 -0
  268. package/human/style.ts +63 -0
  269. package/human/tab.bundle.txt +4775 -0
  270. package/human/tab.ts +446 -0
  271. package/human/tree.ts +217 -0
  272. package/human/wake.ts +191 -0
  273. package/human/web.ts +259 -0
  274. package/human/worker.bundle.txt +3868 -0
  275. package/human/worker.ts +124 -0
  276. package/human/worlds.ts +127 -0
  277. package/mcp/agent.ts +94 -0
  278. package/mcp/allow.ts +131 -0
  279. package/mcp/http.ts +92 -0
  280. package/mcp/oauth.ts +270 -0
  281. package/mcp/quo-mcp.md +343 -0
  282. package/mcp/route.ts +45 -0
  283. package/mcp/runner.ts +136 -0
  284. package/mcp/server.ts +101 -0
  285. package/package.json +121 -0
package/cli/quo.ts ADDED
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // `quo`: the one command of the estate. Thin on purpose: each subcommand
4
+ // picks a folder, reaches the daemon or the disk, and hands off. Logic that
5
+ // appears here was missing from a being or a side, and moves there.
6
+ //
7
+ // quo init [--dir D] [--ward W] [--user U] [--class C] [--default] [--show] [--domain DOM] mint a seed, boot a ward over a disk store, write both; C is the class of the ward's home being, User unless said; --default marks it the default ward; --show shows it at the web route; --domain writes the four routes under DOM
8
+ // quo default [--dir D] <ward> mark one ward the default, the one a caller means when naming none; the one ward hosted is the default unmarked
9
+ // quo show|hide [--dir D] <ward> show a ward at the web route, listed at its root and its public being's page painted for a stranger; hidden at init, and a hidden ward's page is the plain door
10
+ // quo domain [--dir D] <domain> write routes.json by the convention: mcp., web., quo. and api. under the domain, over https
11
+ // quo serve [--dir D] [--http PORT] the daemon: harbor from disk, the two sockets, the HTTP door on loopback, a socket held to every route the dialers faculty keeps
12
+ // quo census [--dir D] [--ward W] [--via S] the empty ask: pk and every being
13
+ // quo boot [--dir D] [--ward W] [--via S] <json> { key, class, public? }
14
+ // quo invite [--dir D] [--ward W] [--via S] <json> { being, id }; with routes.json naming web., the answer carries the link
15
+ // quo knock [--dir D] [--ward W] [--via S] <json> { being | { boot, key }, id, invitation, method?, args?, wanted? }
16
+ // quo remove [--dir D] [--ward W] [--via S] <json> { being, id }: a relation out of a being; on the ward pk, an owner
17
+ // quo unboot [--dir D] [--ward W] [--via S] <json> { being }: a being out of the ward, with every relation she holds
18
+ // quo ask [--dir D] [--ward W] <being> [method] [json] the root asking a being of its own ward, as the device
19
+ // quo side [--dir D] [--ward W] --as NAME an avatar over stdio for a local MCP client; NAME is the client identity
20
+ // quo run [--dir D] [--ward W] --as NAME --url URL --model NAME [--turns N] a human at a terminal talking to a model through an avatar: a line in, the model's final text out
21
+ // quo pilot [--dir D] [--ward W] [--via S] the owner pilot over stdio: census, boot, invite, knock, remove
22
+ // quo reach [--dir D] <pk> <url> a hint for the harbor's directory: that ward pk lives at that quo. route
23
+ // quo estate <dir> --domain D an estate folder of the shape every estate has: a droplet's quo directory, unit and Caddyfile, a Mac's launchd agent, on this dock
24
+ //
25
+ // D defaults to $QUO_DIR, then ~/.quo. W defaults to the default ward, and
26
+ // init names a new ward main when none is given. init takes
27
+ // --user NAME for the user being, defaulting to the device's user. run
28
+ // takes the model's base URL as --url or $QUO_MODEL_URL, its name as
29
+ // --model or $QUO_MODEL, a bearer from $QUO_MODEL_KEY, read here and never
30
+ // kept, and --turns as the ceiling of model calls in one turn. S is a
31
+ // standing the user being holds at another ward, one the root there invited
32
+ // as an owner: with --via, the owner asks go to that ward's door, sealed,
33
+ // and the answers are its. Without it they are the root's, on the pointer.
34
+ import { homedir, userInfo } from 'node:os';
35
+ import { join } from 'node:path';
36
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
37
+ import { isInvitation } from '@nervur-org/nervur';
38
+ import { DiskHarbor } from '../harbor/disk.ts';
39
+ import { serve, readRoutes, writeRoutes } from './daemon.ts';
40
+ import { ask, askBeing, side, reach, type Run } from './client.ts';
41
+ import { link } from '../beings/link.ts';
42
+ import { pilotSide } from './pilot.ts';
43
+ import { estate } from './estate.ts';
44
+
45
+ const OWNER_ASKS = new Set(['boot', 'public', 'invite', 'knock', 'remove', 'unboot']);
46
+
47
+ function parse(argv: string[]) {
48
+ const flags: Record<string, string> = {};
49
+ const rest: string[] = [];
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i] ?? '';
52
+ // a flag takes the word after it, unless that word is a flag or there is
53
+ // none: --default stands alone
54
+ if (a.startsWith('--')) flags[a.slice(2)] = argv[i + 1] === undefined || argv[i + 1]!.startsWith('--') ? '' : argv[++i]!;
55
+ else rest.push(a);
56
+ }
57
+ const [cmd, json, more, third] = rest;
58
+ return { cmd, json, more, third, flags, dir: flags.dir ?? process.env.QUO_DIR ?? join(homedir(), '.quo'), ward: flags.ward, via: flags.via };
59
+ }
60
+
61
+ const print = (v: unknown) => console.log(JSON.stringify(v, null, 2));
62
+
63
+ async function main(argv: string[]): Promise<number> {
64
+ const { cmd, json, more, third, flags, dir, via } = parse(argv);
65
+ if (cmd === 'init') {
66
+ const made = await DiskHarbor.init(dir, flags.ward, flags.user ?? userInfo().username, flags.class, 'default' in flags, 'show' in flags);
67
+ print(flags.domain ? { ...made, routes: await writeRoutes(dir, flags.domain) } : made);
68
+ return 0;
69
+ }
70
+ if (cmd === 'default') {
71
+ if (!json) {
72
+ console.error('quo default <ward>');
73
+ return 2;
74
+ }
75
+ await DiskHarbor.setDefault(dir, json);
76
+ print({ default: json });
77
+ return 0;
78
+ }
79
+ if (cmd === 'show' || cmd === 'hide') {
80
+ if (!json) {
81
+ console.error(`quo ${cmd} <ward>`);
82
+ return 2;
83
+ }
84
+ await DiskHarbor.setShown(dir, json, cmd === 'show');
85
+ print({ [cmd === 'show' ? 'shown' : 'hidden']: json });
86
+ return 0;
87
+ }
88
+ if (cmd === 'domain') {
89
+ if (!json) {
90
+ console.error('quo domain <domain>');
91
+ return 2;
92
+ }
93
+ print({ routes: await writeRoutes(dir, json) });
94
+ return 0;
95
+ }
96
+ // Every command that reaches a ward is on one: the one named, or the
97
+ // default, read from the folder the way the daemon reads it, and only once
98
+ // the command is held right.
99
+ const wardOf = async () => flags.ward ?? (await DiskHarbor.defaultOf(dir));
100
+ if (cmd === 'side' || cmd === 'run') {
101
+ if (!flags.as) {
102
+ console.error(`quo ${cmd} needs --as NAME`);
103
+ return 2;
104
+ }
105
+ let run: Run | undefined;
106
+ if (cmd === 'run') {
107
+ const url = flags.url ?? process.env.QUO_MODEL_URL;
108
+ const model = flags.model ?? process.env.QUO_MODEL;
109
+ if (!url || !model) {
110
+ console.error('quo run needs --url URL and --model NAME, or QUO_MODEL_URL and QUO_MODEL');
111
+ return 2;
112
+ }
113
+ const key = process.env.QUO_MODEL_KEY;
114
+ const turns = flags.turns !== undefined ? Number(flags.turns) : undefined;
115
+ if (turns !== undefined && !(Number.isInteger(turns) && turns > 0)) {
116
+ console.error('--turns is a positive integer');
117
+ return 2;
118
+ }
119
+ run = { url, model, ...(key ? { key } : {}), ...(turns !== undefined ? { turns } : {}) };
120
+ }
121
+ await side(dir, flags.as, await wardOf(), run); // stdio to the daemon, until one end closes
122
+ return -1;
123
+ }
124
+ if (cmd === 'pilot') {
125
+ const ward = await wardOf();
126
+ await ask(dir, undefined, {}, ward); // no daemon is a clear word now, not at the first tool call
127
+ const serving = await pilotSide(dir, ward, new StdioServerTransport(), via);
128
+ process.stdin.once('end', () => void serving.close().then(() => process.exit(0)));
129
+ return -1;
130
+ }
131
+ if (cmd === 'serve') {
132
+ const port = flags.http ?? process.env.QUO_HTTP;
133
+ const s = await serve(dir, port !== undefined ? { http: { port: Number(port) } } : {});
134
+ print({ dir: s.harbor.dir, sock: s.sock, side: s.side, http: s.http ? `http://${s.http.host}:${s.http.port}` : null, dial: s.quo.dialers.map((d) => d.url), wards: Object.fromEntries([...s.harbor.wards].map(([n, h]) => [n, h.pk])) });
135
+ const stop = () => void s.close().then(() => process.exit(0));
136
+ process.once('SIGINT', stop);
137
+ process.once('SIGTERM', stop);
138
+ return -1; // stays up
139
+ }
140
+ if (cmd === 'estate') {
141
+ if (!json || !flags.domain) {
142
+ console.error('quo estate needs <dir> --domain D');
143
+ return 2;
144
+ }
145
+ print(await estate(json, flags.domain));
146
+ return 0;
147
+ }
148
+ // The root reaching into a being of its own ward. Two positionals and the
149
+ // args as JSON, because a human writes this one by hand: `quo ask clock keep
150
+ // '{"id":"nightly","ask":"desk.hello","cron":"0 3 * * *"}'`. With no
151
+ // method it is that being's describe, as the device sees her.
152
+ if (cmd === 'ask') {
153
+ if (!json) {
154
+ console.error('quo ask needs <being> [method] [json]');
155
+ return 2;
156
+ }
157
+ const out = await askBeing(dir, json, more, third ? (JSON.parse(third) as Record<string, unknown>) : {}, await wardOf());
158
+ print('error' in out ? { error: out.error } : out.result);
159
+ const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
160
+ return failed ? 1 : 0;
161
+ }
162
+ if (cmd === 'reach') {
163
+ if (!json || !more) {
164
+ console.error('quo reach needs <pk> <url>');
165
+ return 2;
166
+ }
167
+ const out = await reach(dir, json, more);
168
+ print('error' in out ? { error: out.error } : out.result);
169
+ return 'error' in out ? 1 : 0;
170
+ }
171
+ if (cmd === 'census' || (cmd !== undefined && OWNER_ASKS.has(cmd))) {
172
+ const args = json ? (JSON.parse(json) as Record<string, unknown>) : {};
173
+ const ward = await wardOf();
174
+ let out = await ask(dir, cmd === 'census' ? undefined : cmd, args, ward, via);
175
+ // an invitation minted here is handed on as a link when the harbor's
176
+ // routes name a web. origin: the world's page with the invitation in
177
+ // the fragment, one string to send to the person it is for
178
+ const minted = cmd === 'invite' && !via && 'result' in out && isInvitation(out.result) ? out.result : null;
179
+ const routes = minted ? await readRoutes(dir) : null;
180
+ // the ward answers null to an invite on an id already held, standing or
181
+ // occupant, and null on a terminal is nothing to read: it is a refusal
182
+ // here, in words, and an exit code a script can tell apart
183
+ if (cmd === 'invite' && 'result' in out && out.result === null) out = { result: { error: 'id taken' } };
184
+ if (minted && routes) print({ ...minted, link: link(`${routes.web}/${encodeURIComponent(ward)}`, minted) });
185
+ else print('error' in out ? { error: out.error } : out.result);
186
+ // an error object the ward answered is an ordinary answer, and still a
187
+ // non-zero exit, so a script can tell a boot that happened from one that did not.
188
+ const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
189
+ return failed ? 1 : 0;
190
+ }
191
+ console.error('usage: quo init|default|show|hide|domain|serve|census|boot|public|invite|knock|remove|unboot|ask|side|run|pilot|reach|estate [--dir D] [--ward W] [--via S] [--as NAME] [--url URL --model NAME] [json]');
192
+ return 2;
193
+ }
194
+
195
+ main(process.argv.slice(2)).then(
196
+ (code) => {
197
+ if (code >= 0) process.exit(code);
198
+ },
199
+ (e: unknown) => {
200
+ console.error(e instanceof Error ? e.message : String(e));
201
+ process.exit(1);
202
+ },
203
+ );
package/cli/stand.ts ADDED
@@ -0,0 +1,160 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Standing a harbor up: the beings an estate boots for its own working and
3
+ // the relations between them, placed by the root once, from the estate's
4
+ // own `stand.ts`, on real invitations. The desk and the user being, the
5
+ // bell, an estate's front, the clock with her keep and her voice: each is
6
+ // furniture the estate owns rather than a world anyone keeps on it, and each
7
+ // is placed the way every relation is placed, an invite on the far ward's
8
+ // owner pointer and a knock on the near one. Nothing here is read at boot:
9
+ // the wards are the whole truth about what stands, and a script is a
10
+ // recipe run once, never a second record kept in step.
11
+ //
12
+ // Five rules, and the first is the one the rest exist to protect.
13
+ //
14
+ // **It places ordinary relations.** Real keys, rotating, written into both
15
+ // beings' cells, removable by hand afterwards like any other. Never a channel
16
+ // the harbor grants itself. The moment a standing exists because the harbor
17
+ // said so rather than because an invitation was minted and taken, a being's
18
+ // occupants stop being the whole truth about who can reach her, and every
19
+ // gate in this tree rests on that being true.
20
+ //
21
+ // **It is idempotent.** A being already booted is left alone and a standing
22
+ // already held is left alone, so a script run twice stands the estate once.
23
+ //
24
+ // **Same harbor only.** Both ends name a ward this device hosts. Across
25
+ // harbors an invitation is not bookkeeping, it is consent, and there is no
26
+ // shared root to automate it with: those relations stay a human's to make.
27
+ //
28
+ // **It adds and never removes.** A line taken out of a script leaves its
29
+ // relation standing, because silently unplacing a relation is a wake that
30
+ // stops with nobody told. Removal is a deliberate `quo remove`.
31
+ //
32
+ // **It fails loudly.** A line that will not place stops the script where it
33
+ // is, and a harbor half stood is said, never served.
34
+ import type { Invitation, JsonObject } from '@nervur-org/nervur';
35
+ import type { Harbor } from '@nervur-org/nervur/harbor';
36
+ import { DiskHarbor } from '../harbor/disk.ts';
37
+
38
+ // A being the estate boots, and where; `public` marks her as the one being
39
+ // of her ward that answers strangers, an estate's front in place of the desk.
40
+ export type Boot = { ward: string; key: string; class: string; public?: boolean };
41
+
42
+ // One arrow. `from` ends up holding the standing, `to` ends up holding the
43
+ // occupant, which is what a relation in Quo is: one direction, and the answer
44
+ // rides the ask back. `as` names the standing on the near side and defaults to
45
+ // the far being's key, because that is the name a schedule or a caller will
46
+ // write. `known` names the occupant on the far side and defaults to the near
47
+ // being's key. `notes` are the terms the occupant is minted under, hers to
48
+ // read on her gate and nobody else's to write, which is how a watcher is
49
+ // declared here rather than in code.
50
+ export type Wire = { from: string; to: string; as?: string; known?: string; notes?: JsonObject };
51
+
52
+ export type Stand = { boot?: Boot[]; wire?: Wire[] };
53
+
54
+ const KEY = /^[\w.:-]+$/;
55
+ const RESERVED = new Set(['knock', 'take', 'remove']);
56
+
57
+ const fail = (why: string): never => {
58
+ throw new Error(`stand: ${why}`);
59
+ };
60
+
61
+ // `<ward>.<key>`, split at the first dot: a ward's name is a plain word and a
62
+ // being's key is not, so the ambiguity only ever falls one way.
63
+ function at(v: string, what: string): { ward: string; key: string } {
64
+ const dot = v.indexOf('.');
65
+ if (dot < 1) return fail(`${what}: "${v}" is not "<ward>.<being>"`);
66
+ return { ward: v.slice(0, dot), key: v.slice(dot + 1) };
67
+ }
68
+
69
+ type Wards = { wards: Map<string, { being(key: string): unknown; ask(method: string, args: JsonObject): Promise<unknown> }>; partitionOf(name: string): Record<string, unknown> | undefined };
70
+
71
+ // A name a wire would use, read off the partition the harbor already holds:
72
+ // whether a being holds it as a standing and whether she holds it as an
73
+ // occupant. Standings and occupants are one namespace, so a name held under
74
+ // the other kind is a collision and never this wire.
75
+ function holds(h: Wards, ward: string, key: string, id: string): { standing: boolean; occupant: boolean } {
76
+ const p = h.partitionOf(ward) as { bind?: Record<string, { standings?: Record<string, unknown> }>; beings?: Record<string, { occupants?: Record<string, unknown> }> } | undefined;
77
+ return { standing: Object.hasOwn(p?.bind?.[key]?.standings ?? {}, id), occupant: Object.hasOwn(p?.beings?.[key]?.occupants ?? {}, id) };
78
+ }
79
+
80
+ const ward = (h: Wards, name: string, why: string) => h.wards.get(name) ?? fail(`${why}: this device hosts no ward ${name}`);
81
+
82
+ // Stand a booted harbor up. Everything declared and absent is placed;
83
+ // everything declared and present is left exactly as it is.
84
+ //
85
+ // Idempotency is read off the mint and not off anybody's cells. A being is
86
+ // whatever answers, `Being` or a bare object with one method, so there is no
87
+ // shape here to reach into: what says a wire already stands is that the far
88
+ // being refuses to mint that occupant a second time. `invite` answers null
89
+ // for exactly one reason once the reader has held the id to a word the ward
90
+ // does not reserve, which is that the id is taken, and an id taken is this
91
+ // wire, placed the last time the script ran.
92
+ export async function standUp(harbor: Harbor | Wards, decl: Stand): Promise<{ booted: string[]; wired: string[]; already: string[] }> {
93
+ const h = harbor as Wards;
94
+ const booted: string[] = [];
95
+ for (const b of decl.boot ?? []) {
96
+ const w = ward(h, b.ward, `boot ${b.ward}.${b.key}`);
97
+ if (!w.being(b.key)) {
98
+ const out = (await w.ask('boot', { key: b.key, class: b.class })) as { error?: string };
99
+ if (out.error) fail(`boot ${b.ward}.${b.key} as ${b.class}: ${out.error}`);
100
+ booted.push(`${b.ward}.${b.key}`);
101
+ }
102
+ // the mark is one per ward and the script is what says who holds it:
103
+ // taken off whoever holds it, the desk init put there, and put on her,
104
+ // which is the same two asks whether or not she held it already
105
+ if (b.public) {
106
+ const off = (await w.ask('public', { key: null })) as { error?: string };
107
+ if (off.error) fail(`public ${b.ward}.${b.key}: ${off.error}`);
108
+ const out = (await w.ask('public', { key: b.key })) as { error?: string };
109
+ if (out.error) fail(`public ${b.ward}.${b.key}: ${out.error}`);
110
+ }
111
+ }
112
+ const wired: string[] = [];
113
+ const already: string[] = [];
114
+ for (const one of decl.wire ?? []) {
115
+ const from = at(one.from, 'from'),
116
+ to = at(one.to, 'to');
117
+ const as = one.as ?? to.key,
118
+ known = one.known ?? from.key;
119
+ if (!KEY.test(as) || !KEY.test(known)) fail(`a name is a word: ${one.from} -> ${one.to}`);
120
+ // The three the stance keeps for itself. A ward refuses them at invite
121
+ // and at take, and refusing them here is what leaves `invite` answering
122
+ // null for one reason only, which is the whole of how this stays
123
+ // idempotent without reading anybody's cells.
124
+ if (RESERVED.has(as) || RESERVED.has(known)) fail(`knock, take and remove are the ward's own words: ${one.from} -> ${one.to}`);
125
+ const near = ward(h, from.ward, `wire ${one.from}`),
126
+ far = ward(h, to.ward, `wire ${one.to}`);
127
+ if (!near.being(from.key)) fail(`wire ${one.from}: no such being`);
128
+ if (!far.being(to.key)) fail(`wire ${one.to}: no such being`);
129
+ // A name held under the other kind is not this wire and never will be:
130
+ // said now, before a heir is spent on it, rather than read as a wire
131
+ // already placed or refused at the knock with the far side already bound.
132
+ const atFar = holds(h, to.ward, to.key, known);
133
+ const atNear = holds(h, from.ward, from.key, as);
134
+ if (atFar.standing) fail(`wire ${one.from} -> ${one.to}: ${to.key} already holds a standing named ${known}, and a name is one of the two`);
135
+ if (atNear.occupant) fail(`wire ${one.from} -> ${one.to}: ${from.key} already holds an occupant named ${as}, and a name is one of the two`);
136
+ const inv = (await far.ask('invite', { being: to.key, id: known, ...(one.notes ? { notes: one.notes } : {}) })) as Invitation | { error?: string } | null;
137
+ if (inv === null) {
138
+ already.push(`${one.from} -> ${one.to}`);
139
+ continue;
140
+ }
141
+ if ('error' in inv) fail(`wire ${one.from} -> ${one.to}: ${inv.error}`);
142
+ const took = (await near.ask('knock', { being: from.key, id: as, invitation: inv })) as { taken?: string; error?: string };
143
+ if (took.taken !== as) fail(`wire ${one.from} -> ${one.to}: ${took.error ?? `${from.key} would not take ${as}`}`);
144
+ wired.push(`${one.from} -> ${one.to}`);
145
+ }
146
+ return { booted, wired, already };
147
+ }
148
+
149
+ // What an estate's `stand.ts` calls: the harbor folder booted here, with no
150
+ // daemon over it, stood up and written back. The lease refuses a folder a
151
+ // daemon holds, so the recipe is stop, stand, start, like an init.
152
+ export async function stand(dir: string, decl: Stand): Promise<{ booted: string[]; wired: string[]; already: string[] }> {
153
+ const h = new DiskHarbor(dir);
154
+ await h.boot();
155
+ try {
156
+ return await standUp(h, decl);
157
+ } finally {
158
+ await h.close();
159
+ }
160
+ }
@@ -0,0 +1,21 @@
1
+ import type { Handler } from '../harbor/http.ts';
2
+ import type { Hosted } from '@nervur-org/nervur/harbor';
3
+ import type { Avatar } from '../beings/avatar.ts';
4
+ import type { Grants } from '../dock/grants.ts';
5
+ import { type Invitation } from '@nervur-org/nervur';
6
+ export type Held = {
7
+ wards: ReadonlyMap<string, Hosted>;
8
+ here(): string;
9
+ };
10
+ export type Options = {
11
+ grants: Grants;
12
+ resolve: (identity: string, ward: string) => Promise<{
13
+ avatar?: Avatar;
14
+ error?: string;
15
+ }>;
16
+ join: (identity: string, invitation: Invitation, ward: string) => Promise<{
17
+ error?: string;
18
+ }>;
19
+ web?: string;
20
+ };
21
+ export declare function apiRoute(harbor: Held, o: Options): Handler;
@@ -0,0 +1,193 @@
1
+ import { readText } from '../harbor/http.js';
2
+ import { invitationOf } from '../beings/link.js';
3
+ import { landed, POLICY } from '../human/door.js';
4
+ import { CSS } from '../human/style.js';
5
+ import { isInvitation, isSilence, isWord, wordOf } from '@nervur-org/nervur';
6
+ const open = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET, POST, OPTIONS', 'access-control-allow-headers': 'content-type, authorization' };
7
+ const word = (s) => (typeof s === 'string' && /^[\w.-]{1,40}$/.test(s) ? s : null);
8
+ export function apiRoute(harbor, o) {
9
+ return async (req, rest) => {
10
+ const json = (status, body) => Response.json(body, { status, headers: open });
11
+ // The one page this road serves, and it wears the one stylesheet every
12
+ // screen wears. No cross-origin header on it: a page is for the browser
13
+ // that was sent here and never for a script somewhere else. Its policy
14
+ // is the allow page's, and for the same reason: a page that runs no
15
+ // script of its own should be allowed none.
16
+ const page = (status, body) => new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>quo</title>\n<style>${CSS}</style>\n</head><body>${body}</body></html>`, {
17
+ status,
18
+ headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': POLICY, 'referrer-policy': 'no-referrer' },
19
+ });
20
+ // Where this world lives, as the daemon was told and not as the request
21
+ // said: a Host header is the caller's to write, and a page that printed
22
+ // one would print whatever a caller wanted a human to read. Falling back
23
+ // to it is for a box configured with no web address at all, where the
24
+ // request is the only thing that knows.
25
+ const hostOf = (r) => {
26
+ try {
27
+ return o.web === undefined ? (r.headers.get('host') ?? '') : new URL(o.web).host;
28
+ }
29
+ catch {
30
+ return r.headers.get('host') ?? '';
31
+ }
32
+ };
33
+ if (req.method === 'OPTIONS')
34
+ return new Response(null, { status: 204, headers: open });
35
+ if (req.method !== 'GET' && req.method !== 'POST')
36
+ return json(405, { error: 'GET or POST' });
37
+ const body = async () => {
38
+ const raw = await readText(req);
39
+ if (raw === undefined)
40
+ return null;
41
+ let parsed = {};
42
+ try {
43
+ parsed = raw.length === 0 ? {} : JSON.parse(raw);
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ return parsed === null || typeof parsed !== 'object' || Array.isArray(parsed) ? null : parsed;
49
+ };
50
+ // The trade: an invitation for a grant, or a refresh token for the next.
51
+ if ((rest === '' || rest === '/') && req.method === 'POST') {
52
+ const b = await body();
53
+ if (b === null)
54
+ return json(400, { error: 'the body is a JSON object' });
55
+ if (typeof b.refresh === 'string') {
56
+ const t = await o.grants.refresh(b.refresh);
57
+ return t ? json(200, t) : json(400, { error: 'invalid_grant' });
58
+ }
59
+ const inv = typeof b.invitation === 'string' ? invitationOf(b.invitation) : isInvitation(b.invitation) ? b.invitation : null;
60
+ if (!inv)
61
+ return json(400, { error: 'that is not an invitation' });
62
+ const world = [...harbor.wards].find(([, h]) => h.pk === inv.ward);
63
+ if (!world)
64
+ return json(400, { error: 'that invitation is for a world that does not live here' });
65
+ const [ward, hosted] = world;
66
+ const identity = word(b.identity);
67
+ if (identity === null || identity === hosted.record.user || identity === 'desk')
68
+ return json(400, { error: 'an identity is one word, and not the user or the desk' });
69
+ // Nobody at a page vouches for a program, so a trade is a knock that
70
+ // spends: an identity already in refreshes, or is removed at the user
71
+ // being and invited again, and never re-enters on a spent invitation.
72
+ if ((await o.resolve(identity, ward)).avatar)
73
+ return json(400, { error: 'already in' });
74
+ const joined = await o.join(identity, inv, ward);
75
+ if (joined.error)
76
+ return json(400, { error: joined.error });
77
+ return json(200, await o.grants.issue(identity, ward, 'api'));
78
+ }
79
+ // No ward named is the device's own ward, and never a default the route
80
+ // picks: the harbor points at one of its wards and says that one is the
81
+ // device's, and its door answers for it.
82
+ const [, named = '', ask = '', ...more] = rest.split('/');
83
+ const ward = named === '' ? harbor.here() : named;
84
+ // The delivery: a vendor's webhook to an envoy, `/api/<ward>/<being>/deliver`.
85
+ // Not a stranger's ask and not an occupant's: the owner asking her
86
+ // `deliver` with the raw bytes and the headers, and nothing else. The
87
+ // body is read as a string and never parsed here, since the vendor
88
+ // signed the bytes and a reparsed body fails her check; no bearer is
89
+ // read, since the rung is not the caller's to prove. She answers `{ ok }`
90
+ // or her error object, and the route carries it.
91
+ // A GET on the same path is the same ask with an empty body: where an
92
+ // OAuth redirect lands, the code in the query. The query crosses inside
93
+ // the headers under `:query`, a name no header on the wire can have, so
94
+ // her one ask in stays one.
95
+ if (more.length === 1 && more[0] === 'deliver') {
96
+ if (req.method !== 'POST' && req.method !== 'GET')
97
+ return json(405, { error: 'POST, or GET for a redirect' });
98
+ const hosted = harbor.wards.get(ward);
99
+ if (!hosted || !/^[\w-]+$/.test(ask))
100
+ return json(404, { error: 'no such world' });
101
+ const raw = req.method === 'GET' ? '' : await readText(req);
102
+ if (raw === undefined)
103
+ return json(413, { error: 'too big' });
104
+ const headers = {};
105
+ req.headers.forEach((v, k) => (headers[k] = v));
106
+ const query = new URL(req.url).search.slice(1);
107
+ if (query)
108
+ headers[':query'] = query;
109
+ const out = await hosted.ask('ask', { being: ask, method: 'deliver', args: { body: raw, headers } });
110
+ await hosted.save();
111
+ // A GET here is a browser and a browser is a human: a vendor a world
112
+ // sent her to has opened this URL in her tab, so what she is looking
113
+ // at is whatever the envoy answered a machine. She gets a page
114
+ // instead, and only she does; a vendor asking for JSON is answered in
115
+ // JSON as it always was, and so is a GET that says nothing about what
116
+ // it wants.
117
+ const wants = req.method === 'GET' && (req.headers.get('accept') ?? '').includes('text/html');
118
+ if (wants) {
119
+ const ok = out !== null && typeof out === 'object' && !Array.isArray(out) && typeof out.error !== 'string';
120
+ return page(ok ? 200 : 400, landed({ world: ward, host: hostOf(req), ...(o.web ? { at: `${o.web.replace(/\/$/, '')}/${encodeURIComponent(ward)}` } : {}), ok }));
121
+ }
122
+ if (isSilence(out) || out === null || typeof out !== 'object' || Array.isArray(out))
123
+ return json(503, { error: 'silence' });
124
+ const said = out.error;
125
+ if (said === 'no such being' || said === 'unknown ask')
126
+ return json(404, { error: 'no such envoy' });
127
+ if (said === 'silence' || said === 'threw' || said === 'late')
128
+ return json(503, { error: 'silence' });
129
+ if (typeof said === 'string')
130
+ return json(400, out);
131
+ return json(200, out);
132
+ }
133
+ if (!/^[\w.-]+$/.test(ward) || more.length > 0 || (ask !== '' && !/^[\w-]+$/.test(ask)))
134
+ return json(404, { error: 'GET /api for the device’s ward, GET /api/<ward> for the describe, GET or POST /api/<ward>/<ask> for one ask, POST /api/<ward>/<being>/deliver for a delivery' });
135
+ const hosted = harbor.wards.get(ward);
136
+ if (!hosted)
137
+ return json(404, { error: 'no such world' });
138
+ let args;
139
+ if (req.method === 'POST') {
140
+ const b = await body();
141
+ if (b === null)
142
+ return json(400, { error: 'the body is a JSON object' });
143
+ args = b;
144
+ }
145
+ else {
146
+ const url = new URL(req.url);
147
+ args = Object.fromEntries(url.searchParams.entries());
148
+ }
149
+ const carried = (req.headers.get('authorization') ?? '') !== '';
150
+ const grant = o.grants.bearer(req);
151
+ if (carried && grant === null)
152
+ return json(401, { error: 'unauthorized' });
153
+ if (grant === null) {
154
+ // The owner's `ask` with no being named: the public being, asked as
155
+ // nobody, bounded and judged by the ward, which is the in-process twin
156
+ // of the bytes a stranger would send. The route holds none of that. A
157
+ // world with nobody at the door and a being who said nothing are one
158
+ // answer here, as they are one answer on the wire.
159
+ const out = await hosted.ask('ask', ask === '' ? {} : { method: ask, args });
160
+ if (isSilence(out) || out === null || typeof out !== 'object' || Array.isArray(out))
161
+ return json(503, { error: 'silence' });
162
+ const said = out.error;
163
+ if (said === 'silence' || said === 'threw' || said === 'late' || said === 'no such being')
164
+ return json(503, { error: 'silence' });
165
+ if (typeof said === 'string')
166
+ return json(400, out);
167
+ return json(200, out);
168
+ }
169
+ // An occupant: the grant names a world, and the path must name the same
170
+ // one, since an avatar is one relation in one world.
171
+ if (grant.ward !== ward)
172
+ return json(403, { error: 'not your world' });
173
+ const found = await o.resolve(grant.identity, ward);
174
+ if (found.error === 'removed') {
175
+ await o.grants.revoke(grant.identity, ward);
176
+ return json(401, { error: 'removed' });
177
+ }
178
+ if (!found.avatar)
179
+ return json(401, { error: found.error ?? 'not joined' });
180
+ const out = ask === '' ? await found.avatar.tools() : await found.avatar.call(ask, args);
181
+ await hosted.save();
182
+ if (isSilence(out))
183
+ return json(503, { error: 'silence' });
184
+ if (isWord(out))
185
+ return json(503, { error: wordOf(out) });
186
+ if (out === null || typeof out !== 'object' || Array.isArray(out))
187
+ return json(200, out);
188
+ const said = out.error;
189
+ if (typeof said === 'string' && !('asks' in out))
190
+ return json(400, out);
191
+ return json(200, out);
192
+ };
193
+ }
@@ -0,0 +1,32 @@
1
+ import type { Answer, Blueprint, Invitation, JsonObject, OccupantRecord, Wanted } from '@nervur-org/nervur';
2
+ import { Carrier } from './carry.ts';
3
+ export declare const USER = "user";
4
+ export declare const PUSHER = "user:push";
5
+ export declare class Avatar extends Carrier {
6
+ static cells: {
7
+ inbox: JsonObject[];
8
+ };
9
+ static asks: {
10
+ notify: {
11
+ description: string;
12
+ input: {
13
+ type: string;
14
+ };
15
+ for: (occ: OccupantRecord | undefined) => boolean;
16
+ };
17
+ };
18
+ readonly ears: Set<(object: JsonObject) => void>;
19
+ notify(args: JsonObject): {
20
+ notified: boolean;
21
+ };
22
+ enter(desk: Invitation, proof: JsonObject): Promise<Blueprint | {
23
+ error: string;
24
+ }>;
25
+ join(invitation: Invitation): Promise<Blueprint | {
26
+ error: string;
27
+ }>;
28
+ tools(): Promise<Blueprint | {
29
+ error: string;
30
+ }>;
31
+ call(name: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
32
+ }