@nervur-org/dock 0.4.1 → 0.6.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 (225) hide show
  1. package/README.md +17 -15
  2. package/api/route.ts +4 -72
  3. package/beings/avatar.ts +11 -7
  4. package/beings/carry.ts +129 -116
  5. package/beings/clock.ts +11 -7
  6. package/beings/desk.ts +7 -6
  7. package/beings/doorbell.ts +36 -7
  8. package/beings/index.ts +17 -4
  9. package/beings/join.ts +7 -4
  10. package/beings/link.ts +1 -1
  11. package/beings/porter.ts +5 -1
  12. package/beings/presence.ts +15 -5
  13. package/beings/setup.ts +27 -21
  14. package/beings/user.ts +26 -17
  15. package/beings/way.ts +1 -1
  16. package/{dock → box}/agents.ts +37 -44
  17. package/{dock → box}/apns.ts +4 -4
  18. package/box/box.ts +31 -0
  19. package/box/contract.ts +41 -0
  20. package/box/dialers.ts +88 -0
  21. package/box/dock.ts +148 -0
  22. package/box/envoy.ts +96 -0
  23. package/box/faculty.ts +111 -0
  24. package/{dock → box}/fcm.ts +1 -1
  25. package/box/grants.ts +90 -0
  26. package/box/http.ts +93 -0
  27. package/{dock → box}/http2.ts +7 -7
  28. package/box/index.ts +201 -0
  29. package/box/join.ts +78 -0
  30. package/box/local.ts +37 -0
  31. package/{dock → box}/location.ts +15 -3
  32. package/{dock → box}/notifications.ts +17 -5
  33. package/box/push.ts +85 -0
  34. package/box/ring.ts +85 -0
  35. package/{dock → box}/service.ts +2 -2
  36. package/box/timer.ts +150 -0
  37. package/box/tree.ts +22 -0
  38. package/{dock → box}/webpush.ts +2 -2
  39. package/cli/client.ts +1 -1
  40. package/cli/daemon.ts +101 -103
  41. package/cli/estate.ts +2 -3
  42. package/cli/nervur.ts +44 -25
  43. package/cli/stand.ts +1 -1
  44. package/dist/api/route.d.ts +1 -2
  45. package/dist/api/route.js +4 -76
  46. package/dist/beings/avatar.d.ts +4 -3
  47. package/dist/beings/avatar.js +9 -6
  48. package/dist/beings/carry.d.ts +10 -12
  49. package/dist/beings/carry.js +119 -127
  50. package/dist/beings/clock.js +10 -7
  51. package/dist/beings/desk.js +8 -7
  52. package/dist/beings/doorbell.d.ts +3 -1
  53. package/dist/beings/doorbell.js +37 -4
  54. package/dist/beings/index.d.ts +24 -4
  55. package/dist/beings/index.js +15 -4
  56. package/dist/beings/join.d.ts +1 -0
  57. package/dist/beings/join.js +6 -4
  58. package/dist/beings/link.d.ts +0 -1
  59. package/dist/beings/link.js +1 -1
  60. package/dist/beings/porter.js +5 -1
  61. package/dist/beings/presence.d.ts +1 -0
  62. package/dist/beings/presence.js +12 -3
  63. package/dist/beings/setup.d.ts +5 -2
  64. package/dist/beings/setup.js +16 -15
  65. package/dist/beings/user.d.ts +4 -3
  66. package/dist/beings/user.js +18 -11
  67. package/dist/{dock → box}/agents.d.ts +7 -4
  68. package/dist/{dock → box}/agents.js +30 -37
  69. package/dist/{dock → box}/apns.d.ts +1 -1
  70. package/dist/{dock → box}/apns.js +3 -3
  71. package/dist/box/box.d.ts +9 -0
  72. package/dist/box/box.js +18 -0
  73. package/dist/box/contract.d.ts +6 -0
  74. package/dist/box/contract.js +35 -0
  75. package/dist/{dock → box}/dialers.d.ts +7 -4
  76. package/dist/box/dialers.js +76 -0
  77. package/dist/box/dock.d.ts +182 -0
  78. package/dist/box/dock.js +128 -0
  79. package/dist/box/envoy.d.ts +44 -0
  80. package/dist/box/envoy.js +75 -0
  81. package/dist/box/faculty.d.ts +68 -0
  82. package/dist/box/faculty.js +97 -0
  83. package/dist/{dock → box}/fcm.d.ts +1 -1
  84. package/dist/{dock → box}/grants.d.ts +4 -3
  85. package/dist/box/grants.js +78 -0
  86. package/dist/box/http.d.ts +26 -0
  87. package/dist/box/http.js +89 -0
  88. package/dist/{dock → box}/http2.d.ts +1 -1
  89. package/dist/{dock → box}/http2.js +6 -6
  90. package/dist/box/index.d.ts +46 -0
  91. package/dist/box/index.js +179 -0
  92. package/dist/box/join.d.ts +19 -0
  93. package/dist/box/join.js +67 -0
  94. package/dist/{dock → box}/local.d.ts +3 -2
  95. package/dist/box/local.js +27 -0
  96. package/dist/{dock → box}/location.d.ts +3 -0
  97. package/dist/box/location.js +21 -0
  98. package/dist/{dock → box}/notifications.d.ts +3 -0
  99. package/dist/{dock → box}/notifications.js +14 -4
  100. package/dist/{dock → box}/push.d.ts +12 -18
  101. package/dist/box/push.js +83 -0
  102. package/dist/box/ring.d.ts +18 -0
  103. package/dist/box/ring.js +55 -0
  104. package/dist/{dock → box}/service.d.ts +1 -1
  105. package/dist/{dock → box}/service.js +1 -1
  106. package/dist/{dock → box}/timer.d.ts +2 -0
  107. package/dist/box/timer.js +127 -0
  108. package/dist/box/tree.d.ts +5 -0
  109. package/dist/box/tree.js +14 -0
  110. package/dist/{dock → box}/webpush.d.ts +1 -1
  111. package/dist/cli/client.d.ts +1 -0
  112. package/dist/cli/daemon.d.ts +2 -8
  113. package/dist/cli/daemon.js +102 -107
  114. package/dist/cli/estate.js +2 -3
  115. package/dist/cli/nervur.js +44 -24
  116. package/dist/cli/stand.d.ts +0 -14
  117. package/dist/cli/stand.js +1 -1
  118. package/dist/harbor/browser.d.ts +8 -5
  119. package/dist/harbor/browser.js +20 -21
  120. package/dist/harbor/capacitor.d.ts +3 -3
  121. package/dist/harbor/capacitor.js +9 -8
  122. package/dist/harbor/disk.d.ts +9 -9
  123. package/dist/harbor/disk.js +56 -51
  124. package/dist/harbor/edge/alarm.d.ts +4 -2
  125. package/dist/harbor/edge/alarm.js +9 -19
  126. package/dist/harbor/edge/edge.d.ts +7 -6
  127. package/dist/harbor/edge/edge.js +58 -53
  128. package/dist/harbor/edge/exercise.js +4 -4
  129. package/dist/harbor/edge/given.d.ts +0 -1
  130. package/dist/harbor/edge/given.js +0 -29
  131. package/dist/harbor/edge/storage.d.ts +0 -2
  132. package/dist/harbor/edge/storage.js +2 -2
  133. package/dist/harbor/http.js +4 -2
  134. package/dist/harbor/sealed.d.ts +0 -5
  135. package/dist/harbor/sealed.js +0 -39
  136. package/dist/harbor/tauri.d.ts +2 -2
  137. package/dist/harbor/tauri.js +13 -10
  138. package/dist/human/app.d.ts +1 -1
  139. package/dist/human/app.js +10 -12
  140. package/dist/human/door.d.ts +0 -1
  141. package/dist/human/door.js +1 -1
  142. package/dist/human/html.js +1 -1
  143. package/dist/human/move.js +1 -1
  144. package/dist/human/tab.bundle.txt +3629 -2824
  145. package/dist/human/wake.d.ts +5 -2
  146. package/dist/human/wake.js +23 -14
  147. package/dist/human/web.d.ts +0 -1
  148. package/dist/human/web.js +3 -3
  149. package/dist/human/worker.bundle.txt +2945 -2118
  150. package/dist/human/worlds.js +3 -3
  151. package/dist/mcp/agent.d.ts +0 -1
  152. package/dist/mcp/agent.js +1 -1
  153. package/dist/mcp/allow.js +3 -2
  154. package/dist/mcp/http.d.ts +0 -1
  155. package/dist/mcp/http.js +1 -1
  156. package/dist/mcp/oauth.d.ts +3 -4
  157. package/dist/mcp/oauth.js +4 -4
  158. package/dist/mcp/route.d.ts +1 -1
  159. package/dist/mcp/runner.d.ts +0 -1
  160. package/dist/mcp/runner.js +1 -1
  161. package/dist/mcp/server.d.ts +0 -1
  162. package/dist/mcp/server.js +1 -1
  163. package/harbor/browser.ts +22 -24
  164. package/harbor/capacitor.ts +9 -8
  165. package/harbor/disk.ts +53 -48
  166. package/harbor/edge/alarm.ts +15 -12
  167. package/harbor/edge/edge.ts +61 -55
  168. package/harbor/edge/exercise.ts +4 -4
  169. package/harbor/edge/given.ts +3 -33
  170. package/harbor/edge/storage.ts +2 -2
  171. package/harbor/http.ts +5 -3
  172. package/harbor/sealed.ts +0 -35
  173. package/harbor/tauri.ts +13 -10
  174. package/human/app.ts +12 -13
  175. package/human/door.ts +1 -1
  176. package/human/html.ts +1 -1
  177. package/human/local.ts +1 -1
  178. package/human/move.ts +1 -1
  179. package/human/tab.bundle.txt +3629 -2824
  180. package/human/wake.ts +36 -16
  181. package/human/web.ts +4 -4
  182. package/human/worker.bundle.txt +2945 -2118
  183. package/human/worlds.ts +3 -3
  184. package/mcp/agent.ts +1 -1
  185. package/mcp/allow.ts +2 -1
  186. package/mcp/http.ts +1 -1
  187. package/mcp/oauth.ts +4 -4
  188. package/mcp/route.ts +1 -1
  189. package/mcp/runner.ts +1 -1
  190. package/mcp/server.ts +1 -1
  191. package/package.json +10 -18
  192. package/GETTING_STARTED.md +0 -138
  193. package/GLOSSARY.md +0 -118
  194. package/WORLDS.md +0 -569
  195. package/beings/envoy.ts +0 -218
  196. package/dist/beings/envoy.d.ts +0 -54
  197. package/dist/beings/envoy.js +0 -211
  198. package/dist/dock/console.d.ts +0 -6
  199. package/dist/dock/console.js +0 -20
  200. package/dist/dock/dialers.js +0 -75
  201. package/dist/dock/faculty.d.ts +0 -24
  202. package/dist/dock/faculty.js +0 -142
  203. package/dist/dock/grants.js +0 -99
  204. package/dist/dock/index.d.ts +0 -41
  205. package/dist/dock/index.js +0 -113
  206. package/dist/dock/local.js +0 -44
  207. package/dist/dock/location.js +0 -11
  208. package/dist/dock/push.js +0 -132
  209. package/dist/dock/timer.js +0 -149
  210. package/dock/console.ts +0 -23
  211. package/dock/dialers.ts +0 -85
  212. package/dock/faculty.ts +0 -148
  213. package/dock/grants.ts +0 -111
  214. package/dock/index.ts +0 -133
  215. package/dock/local.ts +0 -48
  216. package/dock/push.ts +0 -161
  217. package/dock/timer.ts +0 -174
  218. package/quo-api.md +0 -196
  219. package/quo-app-bells.md +0 -280
  220. package/quo-dock.md +0 -1945
  221. package/quo-harbor.md +0 -874
  222. package/quo-human.md +0 -1042
  223. package/quo-mcp.md +0 -343
  224. /package/dist/{dock → box}/fcm.js +0 -0
  225. /package/dist/{dock → box}/webpush.js +0 -0
package/dock/local.ts DELETED
@@ -1,48 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The box's way in: `local`, one faculty of the dock ward. The daemon saw a
3
- // process reach a socket only the device's user can open, and that is the
4
- // whole of the vouch; file permissions are the device's rules, which is what
5
- // `quo/SPEC.md` allows a harbor to hand the root by.
6
- //
7
- // She holds the nonces this box minted, in this process and never in cells:
8
- // a vouch that outlived a restart would be a vouch for a process that is
9
- // gone. Each is honoured once, and a `local` proof arriving any other way
10
- // names no nonce and is refused.
11
- //
12
- // The device's own code mints on her directly, as a device fires its own
13
- // timer: `mint` is not an ask, it crosses no door, and nothing at a door
14
- // can reach it. The desk asks `verify` at her standing, as she asks any way
15
- // in.
16
- import { Faculty } from './faculty.ts';
17
- import type { JsonObject } from 'nervur';
18
- import { VERIFY, type Verified } from '../beings/way.ts';
19
-
20
- // The name the dock ward lends her under. A box with no local side lends no
21
- // such name, and the desk there holds no such standing.
22
- export const LOCAL = 'local';
23
-
24
- export class Local extends Faculty {
25
- static override asks = { verify: VERIFY };
26
-
27
- readonly #nonces = new Map<string, Verified>();
28
-
29
- // A nonce for one client this box saw itself, spent by the first proof
30
- // that names it. The caller drops it when the admit is over: a nonce that
31
- // was never spent is not a way in that stays open.
32
- mint(nonce: string, who: Verified): void {
33
- this.#nonces.set(nonce, who);
34
- }
35
- drop(nonce: string): void {
36
- this.#nonces.delete(nonce);
37
- }
38
-
39
- verify(args: JsonObject): Verified | null {
40
- const proof = args.proof;
41
- if (!proof || typeof proof !== 'object' || Array.isArray(proof)) return null;
42
- const nonce = (proof as JsonObject).nonce;
43
- const who = typeof nonce === 'string' ? this.#nonces.get(nonce) : undefined;
44
- if (!who) return null;
45
- this.#nonces.delete(nonce as string);
46
- return who;
47
- }
48
- }
package/dock/push.ts DELETED
@@ -1,161 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // Waking a device that is not reachable, the one way its platform allows: a
3
- // push that carries the device's token and the banner sealed to that
4
- // device's own key, which nothing between the world and the phone can
5
- // read. A token names its
6
- // kind, and a kind is one ring: `web` is Web Push under a keypair a world
7
- // mints itself, `apns` and `fcm` are the two stores' own, handed on to the
8
- // publisher's bell service by a world that holds no platform key. A box that
9
- // was given no ring for a kind says so, which is what a world that rings no
10
- // iPhones says to one.
11
- //
12
- // The rings are live objects and not values, so they are not cells: a
13
- // terrain subclasses and builds its own in `rings`, out of what its
14
- // environment and this being's cells give it.
15
- import { OWNER } from 'nervur';
16
- import { arithmetic } from 'nervur/ward';
17
- import type { Asker, JsonObject, OccupantRecord } from 'nervur';
18
- import { Faculty } from './faculty.ts';
19
- import { mintVapid } from './webpush.ts';
20
-
21
- // What a device hands a world so it can be woken, kept in that device's
22
- // occupant notes and re-sent whenever the device comes to the foreground,
23
- // since a push service may hand a device a new one at any time. A token of
24
- // a store's kind names its bell, the address of the service holding the
25
- // keys of the app that minted it, and that app, so a world rings a phone
26
- // running an app it never shipped and holds no key; Web Push is the
27
- // world's own key and names no service.
28
- export type Token = { kind: string; bell?: string; app?: string } & JsonObject;
29
- export const isToken = (v: unknown): v is Token => v !== null && typeof v === 'object' && !Array.isArray(v) && typeof (v as { kind?: unknown }).kind === 'string';
30
-
31
- // The bell a token names, when it names one a ring can reach.
32
- export function bellOf(token: Token): string | undefined {
33
- if (typeof token.bell !== 'string') return undefined;
34
- try {
35
- const u = new URL(token.bell);
36
- return u.protocol === 'https:' || u.protocol === 'http:' ? token.bell : undefined;
37
- } catch {
38
- return undefined;
39
- }
40
- }
41
-
42
- // One platform's way of ringing. It is handed the token whole, because only
43
- // the platform knows what is in one, and it says nothing back: a ring either
44
- // left or threw.
45
- //
46
- // `sealed` is the banner, sealed to the device's banner key by the world
47
- // that rang. Nothing between the world and the device can read it: not this
48
- // box, not the bell service, not the platform that carries it. A ring with
49
- // neither a seal nor a line wakes the device and says nothing, which is
50
- // what a world with nothing to show sends.
51
- //
52
- // `line` is the same banner, unsealed, and only a ground whose own
53
- // encryption the world performs may take one: Web Push, where RFC 8291
54
- // encrypts the body to keys only that browser holds and the world holds the
55
- // keypair the subscription was made under. A store's ground is handed the
56
- // seal and never this, because what carries it is a service the world does
57
- // not own.
58
- export type Line = { title: string; body: string };
59
- export type Ring = { ring(token: Token, sealed?: string, line?: Line): Promise<void> };
60
-
61
- // What a platform will carry, so a seal too big is refused where a human
62
- // can still read why rather than two hops later by a service nobody reads.
63
- export const SEAL = 4096;
64
-
65
- // The line a device shows when it was woken while nobody was looking,
66
- // composed from one object. A pushed object is whatever the far being sent,
67
- // so the two names a notification has are taken when they are there and the
68
- // object is shown as it is when they are not.
69
- export function banner(o: JsonObject): Line {
70
- const text = (v: unknown) => (typeof v === 'string' && v.trim() ? v.trim() : '');
71
- const title = text(o.title) || text(o.from) || 'Nervur';
72
- const body =
73
- text(o.body) ||
74
- text(o.said) ||
75
- Object.entries(o)
76
- .filter(([k]) => k !== 'title' && k !== 'from')
77
- .map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
78
- .join(', ');
79
- return { title, body };
80
- }
81
-
82
- // A banner sealed to a device's banner key, as hex, or null when it will
83
- // not seal or will not fit. The world seals and the process that draws the
84
- // banner opens it; no doorbell, no bell service and no platform between
85
- // them holds either half, which is why a ring may carry a line at all.
86
- export async function sealBanner(line: JsonObject, key: string): Promise<string | null> {
87
- if (!/^[0-9a-f]{64}$/.test(key)) return null;
88
- try {
89
- const boxed = await arithmetic.box(new TextEncoder().encode(JSON.stringify(line)), arithmetic.unhex(key), globalThis.crypto.getRandomValues(new Uint8Array(32)));
90
- const wire = arithmetic.hex(boxed.bytes);
91
- return wire.length > SEAL ? null : wire;
92
- } catch {
93
- return null;
94
- }
95
- }
96
-
97
- // The root and nobody else: where a box hands a phone's ring is the box's own.
98
- const root = (_occ: OccupantRecord | undefined, asker: Asker) => asker.id === OWNER;
99
-
100
- export class Push extends Faculty {
101
- // What she keeps, beside every faculty's: the world's doorbell key, a P-256
102
- // JWK she mints herself the first time a terrain asks for one, so a Web
103
- // Push is signed under a key that lives with the wards and in no file; and
104
- // the bell service a phone's ring is handed to when its token names none,
105
- // a row the root keeps, since whose service it is belongs to an estate.
106
- static override cells: JsonObject = { watched: {}, minted: 0, key: null, service: null };
107
- static override asks = {
108
- ring: { description: 'wake a device: its token, the banner sealed to it, or the line itself where the ground encrypts it', input: { type: 'object', properties: { token: { type: 'object' }, sealed: { type: 'string' }, line: { type: 'object', properties: { title: { type: 'string' }, body: { type: 'string' } }, required: ['title', 'body'] } }, required: ['token'] } },
109
- kinds: { description: 'which kinds this box can ring', input: { type: 'object' } },
110
- keep: { description: 'the bell service a phone is rung through when its token names none; null forgets it', input: { type: 'object', properties: { service: { type: ['string', 'null'] } }, required: ['service'] }, for: root },
111
- };
112
-
113
- // The doorbell key, minted once and kept. A terrain asks for it when it
114
- // serves a web route; a box that never does holds none and rings no tabs.
115
- async mint(): Promise<JsonWebKey> {
116
- if (this.cells.key === null) this.cells.key = (await mintVapid()).key as unknown as JsonObject;
117
- return this.cells.key as unknown as JsonWebKey;
118
- }
119
- get key(): JsonWebKey | null {
120
- return this.cells.key as unknown as JsonWebKey | null;
121
- }
122
- get service(): string | undefined {
123
- return typeof this.cells.service === 'string' ? this.cells.service : undefined;
124
- }
125
-
126
- keep(args: JsonObject): JsonObject {
127
- const service = args.service;
128
- if (service !== null && (typeof service !== 'string' || !/^https?:\/\/\S+$/.test(service))) return { error: 'a bell service is a URL over http or https, or null' };
129
- this.cells.service = service === null ? null : service.replace(/\/$/, '');
130
- return { service: this.cells.service };
131
- }
132
-
133
- // What this box can ring. A terrain overrides it and builds its rings from
134
- // its environment and this being's cells; the base rings nothing.
135
- protected get rings(): Readonly<Record<string, Ring>> {
136
- return {};
137
- }
138
-
139
- kinds(): JsonObject {
140
- return { kinds: Object.keys(this.rings) };
141
- }
142
-
143
- async ring(args: JsonObject): Promise<JsonObject> {
144
- const token = args.token;
145
- if (!isToken(token)) return { error: 'a token names its kind' };
146
- const sealed = args.sealed;
147
- if (sealed !== undefined && typeof sealed !== 'string') return { error: 'a seal is text' };
148
- if (typeof sealed === 'string' && sealed.length > SEAL) return { error: `a seal is at most ${SEAL} bytes` };
149
- const said = args.line;
150
- if (said !== undefined && (said === null || typeof said !== 'object' || Array.isArray(said) || typeof said.title !== 'string' || typeof said.body !== 'string')) return { error: 'a line is a title and a body' };
151
- const line = said as Line | undefined;
152
- const r = Object.hasOwn(this.rings, token.kind) ? this.rings[token.kind] : undefined;
153
- if (!r) return { error: `this box cannot ring a ${token.kind}` };
154
- try {
155
- await r.ring(token, sealed, line);
156
- return { rang: token.kind };
157
- } catch (e) {
158
- return { error: e instanceof Error ? e.message : 'the ring threw' };
159
- }
160
- }
161
- }
package/dock/timer.ts DELETED
@@ -1,174 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The box's clock, and the one being on a device that reads one. A being who
3
- // could read a clock could not be moved, since the moment she read would be
4
- // her box's and no other, so the moment reaches every other being the one way
5
- // anything reaches a being: as an ask at her door, from this faculty, as the
6
- // occupant she named when she asked to be woken.
7
- //
8
- // She arms for the next due moment and for nothing else. Every watcher
9
- // answers a round with when she is next due, this arms for the earliest of
10
- // them, and a box whose watchers all say null arms nothing and sleeps until
11
- // somebody keeps something. So a box with no schedules never ticks, and a
12
- // home on the edge with a sweep at three in the morning wakes once a night.
13
- //
14
- // A round is armed for one moment at a time and never overlaps the one
15
- // before, and the moment is floored to the minute with half a second after
16
- // it, since a watcher floors the moment she is handed and a round that fired
17
- // a hair early would floor to the minute before.
18
- //
19
- // One method knows a terrain, `arm`, and a subclass replaces it: a
20
- // `setTimeout` here, the object's storage alarm on a worker. Nothing else in
21
- // her reads a platform.
22
- import type { JsonObject } from 'nervur';
23
- import { Faculty, type Woke } from './faculty.ts';
24
-
25
- const MINUTE = 60_000;
26
- // A timeout further out than this is not trusted, so the arming is capped and
27
- // made again when it goes off. It is the base class's own bound and no rule
28
- // of the faculty: a terrain whose alarm holds a month arms for the month.
29
- const DAY = 86_400_000;
30
-
31
- export class Timer extends Faculty {
32
- static override asks = {
33
- watch: { description: 'wake me when I say I am next due', input: { type: 'object', properties: { invitation: { type: 'object' }, method: { type: 'string' }, args: { type: 'object' } }, required: ['invitation', 'method'] } },
34
- due: { description: 'bring my next round forward to this moment', input: { type: 'object', properties: { at: { type: 'number' } }, required: ['at'] } },
35
- now: { description: 'this box’s moment', input: { type: 'object' } },
36
- };
37
-
38
- #timeout: ReturnType<typeof setTimeout> | undefined;
39
- #stopped = false;
40
- #armed: number | null = null;
41
- #round: Promise<Woke[]> | null = null;
42
-
43
- // The moment she is armed for, or null while she is asleep. For the
44
- // device's own code and for a suite; it crosses no door.
45
- get armed(): number | null {
46
- return this.#armed;
47
- }
48
-
49
- // A watcher says she is due sooner than the box knew. It only ever brings
50
- // the round forward: what fires is decided at the tick, in the watcher's
51
- // own cells, so an arming that moved later on somebody's word would be a
52
- // schedule somebody else held silenced. The door is the whole gate here:
53
- // a standing at a faculty comes from `lend` alone, and this adds no second
54
- // one.
55
- due(args: JsonObject): JsonObject {
56
- const at = args.at;
57
- if (typeof at !== 'number' || !Number.isFinite(at)) return { error: 'a due carries the moment: { at }' };
58
- const when = moment(at);
59
- if (this.#armed === null || when < this.#armed) this.#arm(when);
60
- return { armed: this.#armed };
61
- }
62
-
63
- // Whoever was just filed is ticked at once, and what she answers is what
64
- // the box arms for. One move covers a box booting, a home moving in and a
65
- // watcher born long after the box was up: she is never waiting for a round
66
- // nobody scheduled.
67
- protected override async welcomed(id: string): Promise<void> {
68
- await this.round(Date.now(), id);
69
- }
70
-
71
- // The arming went off. A moment still well ahead is the day's cap this
72
- // class put on its own timeout, so it is armed again rather than fired: a
73
- // terrain calls this and never has to know how far out the arming was.
74
- async rang(): Promise<Woke[]> {
75
- if (this.#stopped) return [];
76
- if (this.#armed !== null && this.#armed - Date.now() > 1_000) {
77
- this.arm(this.#armed);
78
- return [];
79
- }
80
- return this.fire(Date.now());
81
- }
82
-
83
- // The moment, read here and handed on. A device's own code may call it at a
84
- // moment it names, which is how a suite proves a wake without waiting for
85
- // one and how an operator makes a round happen now.
86
- fire(now: number): Promise<Woke[]> {
87
- return this.round(now);
88
- }
89
-
90
- // One round, and never two: a second while one is in flight is handed the
91
- // one already running. What every watcher answered decides the next
92
- // arming, and a `due` that landed while the round ran is not lost.
93
- protected round(now: number, only?: string): Promise<Woke[]> {
94
- if (this.#round) return this.#round;
95
- const mine = this.#run(now, only);
96
- this.#round = mine;
97
- return mine.finally(() => {
98
- if (this.#round === mine) this.#round = null;
99
- });
100
- }
101
-
102
- async #run(now: number, only?: string): Promise<Woke[]> {
103
- if (only === undefined) this.#arm(null); // the arming is spent, whether it went off or the device called this by hand
104
- const woke = await this.woke((w) => ({ ...w.args, now }), only);
105
- if (woke === null) {
106
- this.stop(); // her ward took her out; there is nobody left to wake
107
- return [];
108
- }
109
- // A moment already gone is no arming. Every watcher works out when she is
110
- // next due from the moment she was handed, so a round fired by hand at a
111
- // moment that is not this box's answers one that is not this box's
112
- // either, and a box that armed for it would run a round for a minute long
113
- // past. A round at the box's own moment always names one still ahead.
114
- const named = soonest(woke);
115
- const said = named !== null && named > Date.now() ? named : null;
116
- const held = this.#armed;
117
- const at = said === null ? held : held === null ? moment(said) : Math.min(held, moment(said));
118
- if (at !== null && at !== this.#armed) this.#arm(at);
119
- else if (at === null && this.#armed !== null) this.#arm(null);
120
- return woke;
121
- }
122
-
123
- #arm(when: number | null): void {
124
- if (this.#stopped) return;
125
- this.#armed = when;
126
- this.arm(when);
127
- }
128
-
129
- // The one thing a terrain says about itself: be woken at this moment, or at
130
- // no moment at all. A subclass replaces this and nothing else of her.
131
- //
132
- // Here it is a timeout, capped at a day and armed again when the cap goes
133
- // off. The box stays up for its work and never for this, so the handle is
134
- // unreferenced.
135
- protected arm(when: number | null): void {
136
- clearTimeout(this.#timeout);
137
- this.#timeout = undefined;
138
- if (when === null) return;
139
- this.#timeout = setTimeout(() => void this.rang(), Math.min(Math.max(when - Date.now(), 0), DAY));
140
- this.#timeout.unref?.();
141
- }
142
-
143
- // A being her ward took out reaches nobody, so nothing is left to wake. The
144
- // device calls this when it puts the box down.
145
- stop(): void {
146
- this.#stopped = true;
147
- this.#armed = null;
148
- this.arm(null);
149
- }
150
-
151
- now(): JsonObject {
152
- return { now: Date.now() };
153
- }
154
- }
155
-
156
- // Half a second past the minute a moment falls in, and never before the
157
- // moment itself: a watcher floors what she is handed, so a round that fired a
158
- // hair early would floor to the minute before and fire nothing.
159
- const moment = (at: number): number => Math.max(at, Math.floor(at / MINUTE) * MINUTE + 500);
160
-
161
- // What one round learned. Every watcher says when she is next due, and the
162
- // box arms for the earliest of them. A watcher who named no moment has
163
- // nothing to be woken for, and so has one who answered something that is no
164
- // tally at all.
165
- function soonest(woke: Woke[]): number | null {
166
- let at: number | null = null;
167
- for (const w of woke) {
168
- if (w.out === null || typeof w.out !== 'object') continue;
169
- const next = (w.out as { next?: unknown }).next;
170
- if (typeof next !== 'number' || !Number.isFinite(next)) continue;
171
- if (at === null || next < at) at = next;
172
- }
173
- return at;
174
- }
package/quo-api.md DELETED
@@ -1,196 +0,0 @@
1
- # The api route
2
-
3
- This is the fourth rendering of one describe. The screen speaks a being's
4
- describe as forms, the model side speaks it as tools, the CLI speaks it as
5
- lines, and this route speaks it as plain HTTP and JSON. It is one route of
6
- the daemon described in `papers/quo-dock.md`, which owns the routes, the
7
- grants, the avatar and the shared invariants; this document assumes all of
8
- that and adds only what the route does. `papers/WORLDS.md` decides what
9
- every route is held to: a route says the grammar a caller speaks and never
10
- who the caller is, and the same credential is honoured on every route.
11
-
12
- ## What it is for
13
-
14
- A route hears every rung. A request carrying nothing is the stranger every
15
- route knows: an ask at the named ward's public being as `{}`, which is what
16
- every stranger is at every door, and what a browser fetch, a page
17
- rendering server side, a crawler and a tab on another origin are. A
18
- request carrying a bearer the box granted is an occupant: the identity's
19
- avatar in the world the grant names, asked through her one standing at the
20
- world's being. What a script sees with a bearer is what a model sees with
21
- the same bearer as tools and a tab sees as forms, because all three are
22
- one avatar's describe for one identity, and the gate is one decision for
23
- describe and for dispatch. The route judges nothing and names nobody: the
24
- public being answers a stranger by her own gate, the world's being answers
25
- an occupant by hers, and the route carries the answer.
26
-
27
- A grant is the box's and may die at any moment: a box wiped, an occupant
28
- removed at the world's being, thirty days without a refresh. Nothing of
29
- the world dies with it, since the avatar and her standing are the ward's;
30
- only the box's memory of which token names her is gone, and the holder
31
- trades again on a fresh invitation and lands on the same avatar. So a
32
- bearer is held by something with a human near it: a model client whose
33
- human clicks the allow page again, a script whose root mints again, a
34
- SaaS whose operator signs up again as at any vendor. A program of the
35
- org's that must run without a human is not a caller of this route. It is
36
- a harbor of the org with a ward and a standing, or an agent the daemon
37
- boots on the box, and either speaks Quo on keys that do not expire.
38
-
39
- A world is a ward, so the route names one. Every ward `init` made holds a
40
- desk, who is public for the first hello and answers a stranger nothing
41
- else, so a world's public reads live in a ward of their own, and the route is what
42
- makes a second ward reachable by name from outside: the corpus of a court
43
- watcher, a catalogue, a board. An occupant's world is the one her
44
- invitation named, and a bearer on any other world is refused.
45
-
46
- ## The mapping
47
-
48
- `packages/dock/api/route.ts` is the route, `Request` in and `Response` out
49
- and naming no terrain: the daemon mounts it under `/api` and the proxy maps
50
- it from the api. hostname, and the worker at the edge mounts the same file
51
- under its harbor's prefix. It asks its harbor for the wards it holds and for
52
- which of them a caller who names none is at, and for nothing else.
53
-
54
- | request | Quo |
55
- | ------------------------------------ | -------------------------------------------------------------------- |
56
- | `POST /api` `{identity, invitation}` | the trade: her avatar knocks with it, and a grant is issued |
57
- | `POST /api` `{refresh}` | the next grant; the old refresh token dies |
58
- | `GET /api` | the describe of the default ward, as `GET /api/<that ward>` |
59
- | `GET /api/<ward>` | the describe: the public being for nobody, the avatar under a bearer |
60
- | `GET /api/<ward>/<ask>?k=v` | that ask, the query as args, every value a string |
61
- | `POST /api/<ward>/<ask>` JSON body | that ask, the object as args |
62
- | `POST /api/<ward>/<being>/deliver` | a vendor's delivery, the raw body and the headers to that envoy |
63
- | `GET /api/<ward>/<being>/deliver` | the same with an empty body: a redirect lands here, query in headers |
64
- | `OPTIONS` | the preflight, any origin, `authorization` allowed |
65
-
66
- What a caller leaves unsaid follows `papers/WORLDS.md`, and none of it is
67
- a default the route picks. A credential names an identity in a world, so an
68
- occupant's ward is her grant's and a bearer on any other ward is refused.
69
- A stranger carries nothing that names a world, so a stranger who names no
70
- ward is at the ward the terrain says it is at, the one the box hosts or the
71
- one it marks, and `GET /api` is what a tab at the web
72
- route's root sees and a model's first tool list. A being is never named,
73
- because a caller
74
- does not choose who answers: naming a ward is naming a door, and behind it
75
- the ward decides, the public being as nobody for a stranger and the
76
- identity's avatar for an occupant. The one path that names a being is the
77
- delivery, and there it is not a choice either: the envoy claimed that path
78
- at her birth, and a delivery with no envoy named has nowhere to land and
79
- is 404, never the public being.
80
-
81
- The trade is the allow page of the model side without the page. Nobody at
82
- a page vouches for a program, so a trade is a knock that spends: the
83
- invitation is the one the root minted for that identity on the world's
84
- being with `nervur invite`, sent as a line, and the avatar knocks with it
85
- inside the trade. An identity already in is refused with `already in` and
86
- refreshes instead, or is removed at the world's being and invited again,
87
- and never re-enters on a spent invitation. What comes back is the box's
88
- grant, `dock/grants.ts`: an access token for an hour, a refresh token for
89
- thirty days, the same grant the OAuth dance issues, honoured on the model
90
- route too.
91
-
92
- What she says crosses as the model side crosses it:
93
-
94
- | she answered | the client gets |
95
- | ------------------------------ | ------------------------------------------------------ |
96
- | an object | 200 with that JSON |
97
- | `{ error }` | 400 with that JSON |
98
- | silence, a throw, a wait spent | 503 `{ error: 'silence' }`: the work may have happened |
99
- | an ask not hers for this asker | 400 `{ error: 'unknown ask' }`, her own answer |
100
-
101
- For a stranger, a ward with nobody at the door is 503 beside them, and
102
- not a 404 of its own: on the wire a stranger who asks a ward that has no
103
- public being hears one silence, and a device speaking for strangers says
104
- what the door would. A stranger learns which world exists here and nothing
105
- else about it. For an occupant the ward's words cross by name, `unreached`
106
- as 503 `{ error: 'unreached' }`, and `removed` is 401 `{ error: 'removed'
107
- }` with the grant forgotten at the box, so a script the world's being
108
- removed starts over with a fresh invitation, exactly as a model client
109
- does.
110
-
111
- And what the route says on its own: 404 for a ward not here and for a path
112
- that is not one of the shapes, 400 for a body that is not a JSON object,
113
- 400 `{ error: 'already in' }` for a trade on an identity that holds a live
114
- standing, 401 `{ error: 'unauthorized' }` for a bearer nobody issued or one
115
- that died, 403 `{ error: 'not your world' }` for a bearer on a world the
116
- grant does not name, 413 for a body past a megabyte, 405 for any other
117
- method. Every answer is JSON and open to any origin: the route carries
118
- nothing a stranger can use that she did not choose to say, and a bearer is
119
- the caller's to keep.
120
-
121
- The route holds none of the judgement. For a stranger it asks the ward the
122
- owner's `ask` with no being named, which is the public being asked as
123
- nobody, and the ward bounds the wait, names the three choices and saves
124
- before the pointer returns: a public being may write while answering, a
125
- tally, a board, and what she wrote is hers to keep. For an occupant it
126
- asks her avatar, `tools` for the describe and `call` for an ask, the two
127
- things every side asks of her, and saves the ward after.
128
-
129
- ## The delivery
130
-
131
- A vendor's webhook is not a stranger's ask and does not land as `{}`, and
132
- it is not an occupant's ask either: it is the owner asking the envoy
133
- `deliver`, as `papers/quo-dock.md` says of envoys, and the route carries
134
- the bytes and nothing of the judgement. `POST /api/<ward>/<being>/deliver`
135
- is its own row and not an ask: the body is read as a string and never
136
- parsed here, since the vendor signed the bytes and a reparsed body fails
137
- her check, and the rule that a body is a JSON object does not apply. The
138
- route asks the ward's owner pointer for that being's `deliver` with `{
139
- body, headers }`, and answers what she answers: `{ ok }` as 200, her error
140
- object as 400, silence as 503. No bearer is read on this path, because the
141
- rung is not the caller's to prove: the signature is hers to check, and
142
- what fails it is refused by her and never by the route.
143
-
144
- A `GET` on the same path is the same ask with an empty body, and is where
145
- an OAuth redirect lands when an envoy begins an account, the code in the
146
- query. The query crosses inside the headers object under the name
147
- `:query`, which no header on the wire can carry, so her one ask in stays
148
- one and an author writes no second door for redirects. What she answers
149
- crosses as JSON like any delivery.
150
-
151
- **And a browser gets a page.** This is a road for programs with exactly one
152
- human arrival, and it is somebody who has just paid for something: a vendor
153
- returns her by opening this URL in her tab, so without a page she ends a
154
- payment reading what an envoy says to a machine. A `GET` whose `accept`
155
- names HTML is answered with the human side's landing page, saying which of
156
- the two happened and offering the world's own page, since a tab a vendor
157
- opened has no history to go back through. What the envoy answered is never
158
- on it. Everything else is unchanged: a vendor asking for JSON is answered
159
- in JSON, a `GET` that says nothing about what it wants is answered in JSON,
160
- and the page carries no cross-origin header, since a page is for the
161
- browser that was sent here and never for a script somewhere else.
162
-
163
- On its own the route says 404 for a ward not here, for a being not hers
164
- and for a third segment that is not `deliver`, 413 past a megabyte and 405
165
- for any method but `POST` and `GET`. A vendor that retries on anything but a quick
166
- 200 lands once, because she keeps the delivery id and answers `{ ok }` for
167
- one she has seen before she wakes anyone.
168
-
169
- ## What is not here
170
-
171
- A ward's delivery reaching more than one envoy, and a vendor that cannot
172
- sign what it sends. Both stand when a first user does.
173
-
174
- ## The proof
175
-
176
- `packages/dock/test/api.test.ts`, over a real daemon on loopback. A
177
- stranger: a world with a public board described and asked by anyone, args
178
- by query and by body, what she wrote kept across a reboot, no ward named
179
- being the device's own, and every refusal. An occupant: an invitation
180
- traded for a grant, the describe under the bearer being the avatar's own
181
- with no ward named and with hers, an ask through her, another world
182
- refused, refresh rotating, removal at the world's being ending the grant,
183
- and the identity let in again on a fresh invitation. One identity, two
184
- grammars: a bearer the OAuth dance issued honoured here and its refresh
185
- rotating at the trade, a bearer the trade issued opening an MCP session,
186
- and the tool list being this route's describe with her look and page
187
- left out. The delivery: signed bytes landing on an envoy's `deliver` as
188
- the owner, a good signature kept and woken once, a retry landing once, a
189
- bad one her refusal as 400, no bearer read, what she kept outliving the
190
- daemon, and the route's own 404, 405 and 413.
191
-
192
- The same route on a second terrain is
193
- `packages/dock/test/terrain/edge.test.ts`, behind `npm run deep:dock`: the
194
- trade at a worker's `/api`, the ask through her avatar, and the answer
195
- compared against a Node daemon doing the same thing, because a rendering of
196
- one describe must not differ by terrain.