@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
@@ -1,5 +1,5 @@
1
1
  import { Avatar, USER } from '../beings/avatar.js';
2
- import { settled } from '../dock/faculty.js';
2
+ import { settled } from '../box/join.js';
3
3
  // The list as a browser keeps it, the tab's and the app's alike: one item
4
4
  // under one key. A device with no storage still works and forgets, which is
5
5
  // why every read has a floor.
@@ -84,7 +84,7 @@ export async function fresh(ward) {
84
84
  // is a tab that hangs.
85
85
  for (let n = 1; n <= 64; n++) {
86
86
  const key = `r${n}`;
87
- const out = (await ward.ask('boot', { key, class: 'Presence' }));
87
+ const out = (await ward.ask('boot', { key, class: 'beings/Presence' }));
88
88
  if (out.error)
89
89
  continue;
90
90
  // She takes what the box lends at her birth, and birth is synchronous
@@ -92,7 +92,7 @@ export async function fresh(ward) {
92
92
  // landed, not before: a screen painted from her describe a moment early
93
93
  // is a page missing every faculty of the box, and nothing paints it
94
94
  // again until the person navigates.
95
- await settled();
95
+ await settled(ward);
96
96
  return { key, avatar: ward.being(key) };
97
97
  }
98
98
  throw new Error('the tab could not boot an avatar: every key it tries is taken');
@@ -2,7 +2,6 @@ import type { Json, JsonObject } from 'nervur';
2
2
  import type { Avatar } from '../beings/avatar.ts';
3
3
  import { type Serving } from '../beings/side.ts';
4
4
  export type Run = (event: JsonObject) => Promise<Json>;
5
- export declare const REPORT = "report";
6
5
  export declare function processRun(command: string, args: string[], dir: string): Run;
7
6
  export type Agent = Serving & {
8
7
  idle(): Promise<void>;
package/dist/mcp/agent.js CHANGED
@@ -13,7 +13,7 @@ import { spawn } from 'node:child_process';
13
13
  import { word, wordText, SILENCE_TEXT, UNREACHED_TEXT } from '../beings/side.js';
14
14
  // The ask on the user being that takes the result. Gated to devices; the
15
15
  // agent's client identity is what the gate reads.
16
- export const REPORT = 'report';
16
+ const REPORT = 'report';
17
17
  // `claude -p` or any CLI agent: the command runs in the agent's folder with
18
18
  // the event, as JSON, as its last argument. The result is what it printed.
19
19
  // A non-zero exit is an error object with what it said on stderr.
package/dist/mcp/allow.js CHANGED
@@ -49,8 +49,9 @@ export class Exchange {
49
49
  if (!world)
50
50
  return again('That invitation is for a world that does not live here.');
51
51
  const identity = word(f.get('identity'));
52
- if (identity === null || identity === world.user || identity === 'desk')
53
- return again('An identity is one word, and not the user or the desk.');
52
+ // the ward judges the rest: an identity that is one of its beings is refused at the join
53
+ if (identity === null)
54
+ return again('An identity is one word.');
54
55
  const joined = await this.o.join(identity, inv, world.ward);
55
56
  if (joined.error)
56
57
  return again(`Not in: ${joined.error}.`);
@@ -8,7 +8,6 @@ export type Session = {
8
8
  serving: Serving;
9
9
  touched: number;
10
10
  };
11
- export declare const SESSION_IDLE: number;
12
11
  export type Resolve = (identity: string, ward: string) => Promise<{
13
12
  avatar?: Avatar;
14
13
  error?: string;
package/dist/mcp/http.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // for an hour is dropped, since HTTP tells the server nothing else.
11
11
  import { WebStandardStreamableHTTPServerTransport as StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
12
12
  import { mcpSide } from './server.js';
13
- export const SESSION_IDLE = 60 * 60 * 1000;
13
+ const SESSION_IDLE = 60 * 60 * 1000;
14
14
  export class McpHttp {
15
15
  sessions = new Map();
16
16
  resolve;
@@ -1,5 +1,5 @@
1
- import { type Grants } from '../dock/grants.ts';
2
- export { ACCESS_TTL, REFRESH_TTL } from '../dock/grants.ts';
1
+ import { type Bearers as Grants } from '../box/grants.ts';
2
+ export { ACCESS_TTL, REFRESH_TTL } from '../box/grants.ts';
3
3
  export declare const CODE_TTL: number;
4
4
  export type Client = {
5
5
  client_id: string;
@@ -82,10 +82,9 @@ export declare class OAuth {
82
82
  error: string;
83
83
  }>;
84
84
  token(body: URLSearchParams): Promise<Record<string, unknown>>;
85
- issue(identity: string, ward: string, client_id: string): Promise<import("../dock/grants.ts").Tokens>;
85
+ issue(identity: string, ward: string, client_id: string): Promise<import("../box/grants.ts").Tokens>;
86
86
  sweep(): void;
87
87
  handle(req: Request, rest: string): Promise<Response | undefined>;
88
88
  challenge(): Response;
89
89
  }
90
- export declare function readBody(req: Request, limit?: number): Promise<string>;
91
90
  export declare function readForm(req: Request): Promise<URLSearchParams>;
package/dist/mcp/oauth.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // S256, the code grant, refresh, opaque tokens. It is a route, not Quo. Its
6
6
  // one job is to end with a client identity, which the front desk trades for
7
7
  // an invitation and the avatar knocks with; the grant it issues is the
8
- // box's, `dock/grants.ts`, honoured on every route, and what this file
8
+ // box's, `box/grants.ts`, honoured on every route, and what this file
9
9
  // keeps for itself is only the dance: clients, pending requests and codes.
10
10
  // No secret of Quo's is ever here.
11
11
  //
@@ -18,8 +18,8 @@
18
18
  // The web route finishes a request by calling `complete(request, identity)`
19
19
  // after its allow page, where the human hands the invitation the root minted;
20
20
  // that returns the redirect that carries the code back to the client.
21
- import { REFRESH_TTL } from '../dock/grants.js';
22
- export { ACCESS_TTL, REFRESH_TTL } from '../dock/grants.js';
21
+ import { REFRESH_TTL } from '../box/grants.js';
22
+ export { ACCESS_TTL, REFRESH_TTL } from '../box/grants.js';
23
23
  // A code, a client id: every key here is a string the far side chose, and a
24
24
  // bare lookup would find `__proto__` and hand back an object with no
25
25
  // identity. Own keys only.
@@ -212,7 +212,7 @@ function withQuery(base, params) {
212
212
  u.searchParams.set(k, v);
213
213
  return u.toString();
214
214
  }
215
- export async function readBody(req, limit = 64 * 1024) {
215
+ async function readBody(req, limit = 64 * 1024) {
216
216
  const s = await req.text();
217
217
  if (s.length > limit)
218
218
  throw new Error('too large');
@@ -1,7 +1,7 @@
1
1
  import { OAuth, type Store } from './oauth.ts';
2
2
  import type { McpHttp } from './http.ts';
3
3
  import type { Handler } from '../harbor/http.ts';
4
- import type { Grants } from '../dock/grants.ts';
4
+ import type { Bearers as Grants } from '../box/grants.ts';
5
5
  export type Routes = {
6
6
  mcp: string;
7
7
  web: string;
@@ -7,7 +7,6 @@ export type Model = {
7
7
  key?: string;
8
8
  turns?: number;
9
9
  };
10
- export declare const TURNS = 10;
11
10
  export type Tool = {
12
11
  type: 'function';
13
12
  function: {
@@ -1,6 +1,6 @@
1
1
  import { word, wordText, SILENCE_TEXT, UNREACHED_TEXT } from '../beings/side.js';
2
2
  import { presentation } from '../beings/look.js';
3
- export const TURNS = 10;
3
+ const TURNS = 10;
4
4
  // Her describe, spoken as a tools array. Name, description and input are
5
5
  // verbatim, with the one narrowing an endpoint has asked for: an ask that
6
6
  // declares no properties is sent with an empty `properties`, because
@@ -6,6 +6,5 @@ import { type Look } from '../beings/look.ts';
6
6
  export declare const NAME = "nervur";
7
7
  export declare const VERSION = "0.0.0";
8
8
  export type { Subject } from '../beings/side.ts';
9
- export declare const DESCRIBE: Tool;
10
9
  export declare function tools(bp: Blueprint, look?: Look): Tool[];
11
10
  export declare function mcpSide(avatar: Subject, transport: Transport, after?: () => Promise<void>): Promise<Serving>;
@@ -16,7 +16,7 @@ import { isSilence, isWord, digest } from 'nervur';
16
16
  export const NAME = 'nervur';
17
17
  export const VERSION = '0.0.0';
18
18
  // The empty ask as a tool: her describe whole, asks and notes.
19
- export const DESCRIBE = { name: 'describe', description: 'the empty ask: her describe, the asks and the notes', inputSchema: { type: 'object' } };
19
+ const DESCRIBE = { name: 'describe', description: 'the empty ask: her describe, the asks and the notes', inputSchema: { type: 'object' } };
20
20
  // Her describe, spoken as tools, the empty ask first. Name, description and
21
21
  // input are verbatim; an output schema crosses when she declared one. Her
22
22
  // look, when she has one, is the hints: a title, and the annotations a host
package/harbor/browser.ts CHANGED
@@ -7,56 +7,54 @@
7
7
  // a second tab on one origin meets the lock and is a screen, not a harbor.
8
8
  // In an app the store is handed in, and the process that holds it is the
9
9
  // lease: an app has one harbor and nothing else can open its files. The
10
- // classes are the estate's beings, handed in with the bundle, plus whatever
11
- // a page hands in beside them.
10
+ // classes are the dock's beings under `beings/`, plus whatever a page hands
11
+ // in beside them under `classes/`.
12
12
  import type { BeingClass } from 'nervur';
13
- import { User, Desk, Avatar, Presence, Courier } from '../beings/index.ts';
14
- import { Harbor, dial, type Dialer, type Lend, type Store } from 'nervur/harbor';
15
- import { FACULTY_CLASSES, settled, stand } from '../dock/index.ts';
13
+ import { BEINGS } from '../beings/index.ts';
14
+ import { Harbor, dial, type Dialer, type Store } from 'nervur/harbor';
15
+ import { BOX_SEED, harborageOver, keyed, settled, standHarbor, type Tree } from '../box/index.ts';
16
16
  import { Idb } from './idb.ts';
17
17
 
18
- export const BUILT_IN: Record<string, BeingClass> = { User, Desk, Avatar, Presence, Courier, ...FACULTY_CLASSES };
19
-
20
18
  export class BrowserHarbor extends Harbor {
21
19
  readonly name: string;
22
20
  readonly dialers: Dialer[] = [];
23
21
  readonly #locked: boolean;
24
22
  #release: (() => void) | undefined;
25
23
 
26
- // `lend` is what this box lends the beings of the wards it hosts: a name
27
- // to an invitation on a being of its own ward. `stand` builds one; a bare
28
- // tab hands none and every name answers null.
29
- constructor(store: string | Store = 'nervur', classes: Record<string, BeingClass> = {}, lend?: Lend) {
24
+ constructor(store: string | Store = 'nervur', classes: Record<string, BeingClass> = {}) {
30
25
  const s = typeof store === 'string' ? new Idb(store) : store;
31
- super(s, async () => ({ ...BUILT_IN, ...classes }), lend);
26
+ super(s, async () => keyed({ ...BEINGS, classes }));
32
27
  this.name = typeof store === 'string' ? store : 'nervur';
33
28
  this.#locked = typeof store === 'string';
34
29
  }
35
30
 
36
- // What this box lends, by name. A bare tab says nothing, stands no ward of
37
- // its own and lends nothing.
38
- lent: Record<string, string> | undefined;
31
+ // The classes this terrain brings to its own ward, as a tree. A bare tab
32
+ // brings none, stands no ward of its own and lends nothing.
33
+ faculties: Tree | undefined;
34
+ // What this harbor is to its faculties, up once it has booted.
35
+ readonly box = harborageOver(() => this.wards.get(BOX_SEED)?.save() ?? Promise.resolve());
39
36
 
40
- // What this box lends is said before it boots, and standing the ward is
41
- // part of booting: a being born in any other ward lends from her first
42
- // line, and a ground that could not reach it yet would leave her holding
43
- // nothing until her next birth.
44
- async stand(lent: Record<string, string>): Promise<void> {
45
- this.lent = lent;
46
- if (this.wards.size > 0) this.lend = await stand(this, lent);
37
+ // What this box brings is said before it boots, and standing its own ward
38
+ // is act one of booting: a being born in any other ward takes the box at
39
+ // her ward's birth, and a box that was not standing yet would leave that
40
+ // ward holding nothing until its next boot.
41
+ async stand(faculties: Tree): Promise<void> {
42
+ this.faculties = faculties;
43
+ if (this.wards.size > 0) await standHarbor(this, faculties, this.box);
47
44
  }
48
45
 
49
46
  // Take the lock and boot every ward kept here. A lock already held is
50
47
  // another tab's harbor over this seed, and this one refuses to boot.
51
48
  override async boot(): Promise<void> {
52
49
  if (this.#locked) await this.#lease();
53
- if (this.lent) this.lend = await stand(this, this.lent);
50
+ if (this.faculties) await standHarbor(this, this.faculties, this.box);
54
51
  await super.boot();
55
52
  // Every being who takes what this box lends does it at her birth, and
56
53
  // birth is synchronous while taking a standing is not. A box is up when
57
54
  // they have landed, not before, here as on a daemon: a page painted
58
55
  // ahead of them shows a device that can do nothing.
59
- await settled();
56
+ await settled(this);
57
+ await this.box.raise();
60
58
  }
61
59
 
62
60
  // Hold one socket to a world's quo. route.
@@ -17,7 +17,7 @@ import { Geolocation } from '@capacitor/geolocation';
17
17
  import { LocalNotifications } from '@capacitor/local-notifications';
18
18
  import { PushNotifications } from '@capacitor/push-notifications';
19
19
  import type { BeingClass, JsonObject } from 'nervur';
20
- import { LOCATION, NOTIFICATIONS, Location, Notifications, type Token, type Where } from '../dock/index.ts';
20
+ import { Location, Notifications, type Token, type Where } from '../box/index.ts';
21
21
  import { BrowserHarbor } from './browser.ts';
22
22
  import { Sealed, type Files, type Secrets } from './sealed.ts';
23
23
 
@@ -34,6 +34,9 @@ const asked = (what: string) => new Error(`the human is being asked to allow ${w
34
34
  const refused = (what: string) => new Error(`${what} is not allowed on this device`);
35
35
 
36
36
  export class PhoneLocation extends Location {
37
+ static override stands(): boolean {
38
+ return Capacitor.isPluginAvailable('Geolocation');
39
+ }
37
40
  override async where(): Promise<Where | JsonObject> {
38
41
  const { location } = await Geolocation.checkPermissions();
39
42
  if (location === 'prompt' || location === 'prompt-with-rationale') {
@@ -47,6 +50,9 @@ export class PhoneLocation extends Location {
47
50
  }
48
51
 
49
52
  export class PhoneNotifications extends Notifications {
53
+ static override stands(): boolean {
54
+ return Capacitor.isPluginAvailable('LocalNotifications');
55
+ }
50
56
  override async notify(args: JsonObject): Promise<JsonObject> {
51
57
  const title = typeof args.title === 'string' ? args.title : '';
52
58
  const body = typeof args.body === 'string' ? args.body : '';
@@ -101,11 +107,6 @@ export class PhoneNotifications extends Notifications {
101
107
  }
102
108
  }
103
109
 
104
- // What a phone lends: the two faculties above under the names every box
105
- // lends them under, and nothing a phone has not. The harbor holds these
106
- // class bodies for its own ward, and boots one being of each in it.
107
- export const PHONE_FACULTIES: Record<string, BeingClass> = { Location: PhoneLocation, Notifications: PhoneNotifications };
108
- export const phoneLent: Record<string, string> = { [LOCATION]: 'Location', [NOTIFICATIONS]: 'Notifications' };
109
110
 
110
111
  // The phone's ear, as `human/app.ts` asks for one: the token this device is
111
112
  // reachable at while it sleeps, or null.
@@ -163,8 +164,8 @@ export function phoneEar(sandbox = false, within = 10_000, named: { bell?: strin
163
164
  // told `wake` every time the app comes to the foreground, since a phone
164
165
  // asleep loses its sockets silently.
165
166
  export async function nativeHarbor(name = 'nervur', classes: Record<string, BeingClass> = {}): Promise<BrowserHarbor> {
166
- const h = new BrowserHarbor(await Native.open(name), { ...PHONE_FACULTIES, ...classes });
167
- await h.stand(phoneLent);
167
+ const h = new BrowserHarbor(await Native.open(name), classes);
168
+ await h.stand({ harbor: { PhoneLocation, PhoneNotifications } });
168
169
  await h.boot();
169
170
  await App.addListener('appStateChange', ({ isActive }) => {
170
171
  if (isActive) h.wake();
package/harbor/disk.ts CHANGED
@@ -2,9 +2,8 @@
2
2
  // The node daemon's harbor: the core over a folder on disk. The store is
3
3
  // `files.ts`, one folder per ward; the class bodies come from a module the
4
4
  // ward record names, loaded from the folder; the lease is a pid file. The
5
- // estate's own beings, the user being, the front desk and the avatar, are
6
- // held by every harbor here: they are what every side needs, and a folder's
7
- // class source adds to them.
5
+ // dock's beings are held by every harbor here under `beings/`, and a
6
+ // folder's class source adds to them under the folder it sits in.
8
7
  //
9
8
  // <dir>/
10
9
  // lease pid of the one process that holds this harbor
@@ -16,26 +15,25 @@
16
15
  // reach.json the directory's hints
17
16
  import { mkdir, readFile, writeFile, unlink, stat } from 'node:fs/promises';
18
17
  import { existsSync } from 'node:fs';
19
- import { join, resolve, isAbsolute } from 'node:path';
18
+ import { dirname, join, resolve, isAbsolute, relative } from 'node:path';
20
19
  import { pathToFileURL } from 'node:url';
21
20
  import { register } from 'node:module';
22
21
  import process from 'node:process';
23
22
  import type { BeingClass } from 'nervur';
24
- import { User, Desk, Avatar, Presence, Clock, Courier } from '../beings/index.ts';
23
+ import { BEINGS } from '../beings/index.ts';
25
24
  import { setup } from '../beings/setup.ts';
26
- import { Harbor, DEFAULT_CODE, type Hosted, type Lend } from 'nervur/harbor';
27
- import { FACULTY_CLASSES, settled, stand } from '../dock/index.ts';
25
+ import { Harbor, DEFAULT_CODE, BOX_SEED, type Hosted } from 'nervur/harbor';
26
+ import { BOX, harborageOver, keyed, settled, standHarbor, type Tree } from '../box/index.ts';
28
27
  import { Files } from './files.ts';
29
28
 
30
29
  export type { Hosted, Bound } from 'nervur/harbor';
31
30
  export type { WardRecord } from 'nervur/harbor';
32
- export const BUILT_IN: Record<string, BeingClass> = { User, Desk, Avatar, Presence, Clock, Courier, ...FACULTY_CLASSES };
33
31
 
34
32
  // The default ward, the one a caller means when naming none: the one marked,
35
33
  // or the one ward hosted when there is exactly one, which is every harbor an
36
34
  // adopter stands with one `nervur init`. Several wards and none marked is
37
35
  // refused by name, since agents and a door would otherwise hang off a
38
- // guess. The dock ward is furniture, never a candidate.
36
+ // guess. The box ward is furniture, never a candidate.
39
37
  export function defaultWard(marked: string[], hosted: string[]): string {
40
38
  if (marked.length > 1) throw new Error(`this device marks ${marked.join(', ')} as default, and one at most may be`);
41
39
  const one = marked[0];
@@ -43,7 +41,7 @@ export function defaultWard(marked: string[], hosted: string[]): string {
43
41
  if (!hosted.includes(one)) throw new Error(`this device says its default ward is ${one}, and hosts no such ward`);
44
42
  return one;
45
43
  }
46
- const [first, ...more] = hosted.filter((n) => n !== 'dock');
44
+ const [first, ...more] = hosted.filter((n) => n !== BOX_SEED);
47
45
  if (first !== undefined && more.length === 0) return first;
48
46
  const names = first === undefined ? [] : [first, ...more];
49
47
  throw new Error(names.length === 0 ? 'this device hosts no ward: run `nervur init`' : `this device hosts ${names.join(', ')} and says which is the default: nervur default <name>`);
@@ -89,19 +87,30 @@ export class DiskHarbor extends Harbor {
89
87
  // ward and nothing in Quo knows of it: a harbor is not a being and has no
90
88
  // self to be, so it can only point at one of its wards. Its agents boot
91
89
  // there, and its door answers for it when a caller names no ward. The one
92
- // ward it hosts when there is one, the one marked otherwise, resolved by
93
- // the daemon at boot; empty until then.
90
+ // ward it hosts when there is one, the one the Dock's cells say otherwise,
91
+ // placed by the root's `default` ask and resolved by the daemon at boot;
92
+ // empty until then.
94
93
  defaultWard = '';
95
94
 
96
- // The mark is a file in the ward's own folder, `wards/<name>/default`, so
97
- // it moves with the ward and nothing beside the wards says it. One at most:
98
- // marking one takes the mark off every other.
99
- static async setDefault(dir: string, name: string): Promise<void> {
95
+ // The root saying what the box is, with no daemon over the folder: the
96
+ // Dock's own asks, `domain`, `apps` and `default`, on the box ward stood
97
+ // alone, which the lease refuses while a daemon serves. The daemon reads
98
+ // what she keeps at its next boot, so a domain said is stop, say, start,
99
+ // like an init.
100
+ static async say(dir: string, method: 'domain' | 'apps' | 'default', args: Record<string, unknown>): Promise<Record<string, unknown>> {
100
101
  const h = new DiskHarbor(dir);
101
- const names = await h.store.list();
102
- if (!names.includes(name)) throw new Error(`no ward ${name} in ${h.dir}`);
103
- for (const n of names) await unlink(join(h.dir, 'wards', n, 'default')).catch(() => {});
104
- await writeFile(join(h.dir, 'wards', name, 'default'), '');
102
+ h.faculties = {};
103
+ await h.boot();
104
+ try {
105
+ const ward = h.wards.get(BOX_SEED);
106
+ if (!ward) throw new Error('this box stands no ward of its own');
107
+ const out = (await ward.ask('ask', { being: BOX, method, args })) as Record<string, unknown>;
108
+ if (typeof out.error === 'string') throw new Error(out.error);
109
+ await ward.save();
110
+ return out;
111
+ } finally {
112
+ await h.close();
113
+ }
105
114
  }
106
115
 
107
116
  // Whether a ward is shown at the web route: listed at its root and its
@@ -122,52 +131,45 @@ export class DiskHarbor extends Harbor {
122
131
  return existsSync(join(resolve(dir), 'wards', name, 'shown'));
123
132
  }
124
133
 
125
- static async marked(dir: string): Promise<string[]> {
126
- const h = new DiskHarbor(dir);
127
- return (await h.store.list()).filter((n) => existsSync(join(h.dir, 'wards', n, 'default')));
128
- }
129
-
130
- // The default ward read from the folder alone, for a command with no
131
- // daemon in front of it and no `--ward` given.
132
- static async defaultOf(dir: string): Promise<string> {
133
- const h = new DiskHarbor(dir);
134
- return defaultWard(await DiskHarbor.marked(dir), await h.store.list());
135
- }
136
134
  #held = false;
137
135
 
138
- // `lend` is what this box lends the beings of every ward here: a name to
139
- // an invitation on a being of its own ward, which `stand` puts up. A
140
- // folder booted by a command lends nothing.
141
- // `own` are this terrain's own class bodies, the faculty subclasses a box
142
- // has: they stand in front of the built-ins, so a daemon that can ring
143
- // hands its own `Push` and the dock ward boots that one.
144
- constructor(dir: string, lend?: Lend, own: Record<string, BeingClass> = {}) {
136
+ constructor(dir: string) {
145
137
  const abs = resolve(dir);
146
138
  // The key the environment gives, the edge's name for it: from the
147
139
  // Keychain through the app that spawned this daemon, or nothing on a
148
140
  // droplet, whose folder stays plain under its file modes.
149
- super(new Files(abs, process.env.NERVUR_SEED_KEY), async (rec) => ({ ...BUILT_IN, ...own, ...(await loadClasses(isAbsolute(rec.code) ? rec.code : join(abs, rec.code))) }), lend);
141
+ // A class is keyed where it lives: the dock's under `beings/`, the
142
+ // folder's under the folder its source sits in, `classes/Corpus`.
143
+ const lookup = async (rec: { code: string }) => {
144
+ const path = isAbsolute(rec.code) ? rec.code : join(abs, rec.code);
145
+ return keyed({ ...BEINGS, [relative(abs, dirname(path))]: await loadClasses(path) });
146
+ };
147
+ super(new Files(abs, process.env.NERVUR_SEED_KEY), lookup);
150
148
  this.dir = abs;
151
149
  }
152
150
 
153
- // What this box lends, by name. Set before `boot` and read there; a box
154
- // that says nothing stands no ward of its own and lends nothing.
155
- lent: Record<string, string> | undefined;
151
+ // The classes this terrain brings to its own ward, as a tree. Set before
152
+ // `boot` and read there; a box that brings none stands no ward of its own
153
+ // and lends nothing. The folder's own classes join them under the folder
154
+ // they live in, so an estate's faculty stands beside the terrain's.
155
+ faculties: Tree | undefined;
156
+ // What this harbor is to its faculties. The terrain raises it once the box
157
+ // is up, which on a daemon is after its routes are mounted.
158
+ readonly box = harborageOver(() => this.wards.get(BOX_SEED)?.save() ?? Promise.resolve());
156
159
 
157
160
  // Create a harbor folder with one ward, and the root's setup in it: the
158
161
  // front desk, public, and one user being, with the desk holding a standing
159
162
  // to her placed by the root's knock. Boots the ward once, with no lease, to
160
163
  // do that and to learn its pk, and writes the record. Refuses a folder that
161
164
  // has one.
162
- static async init(dir: string, name = 'main', user = 'me', cls = 'User', mark = false, shown = false): Promise<{ dir: string; name: string; pk: string; user: string }> {
165
+ static async init(dir: string, name = 'main', user = 'me', cls?: string, shown = false): Promise<{ dir: string; name: string; pk: string; user: string }> {
163
166
  const h = new DiskHarbor(dir);
164
167
  if ((await h.store.list()).includes(name)) throw new Error(`ward ${name} already exists in ${h.dir}`);
165
168
  await mkdir(join(h.dir, 'classes'), { recursive: true });
166
169
  const classes = join(h.dir, DEFAULT_CODE);
167
- if (!existsSync(classes)) await writeFile(classes, '// The classes this harbor holds beside the built-in ones. Export each one by name.\nexport {};\n');
170
+ if (!existsSync(classes)) await writeFile(classes, "// The classes this harbor holds beside the dock's beings. Each export is classes/<its name>.\nexport {};\n");
168
171
  const hosted = await h.create(name, user);
169
- await setup(hosted, user, cls);
170
- if (mark) await DiskHarbor.setDefault(h.dir, name);
172
+ await setup(hosted, user, cls ? { home: cls } : {});
171
173
  if (shown) await DiskHarbor.setShown(h.dir, name, true);
172
174
  return { dir: h.dir, name, pk: hosted.pk, user };
173
175
  }
@@ -181,12 +183,15 @@ export class DiskHarbor extends Harbor {
181
183
  // it yet would leave her holding nothing until her next birth.
182
184
  override async boot(): Promise<void> {
183
185
  await this.#lease();
184
- if (this.lent) this.lend = await stand(this, this.lent);
186
+ if (this.faculties) {
187
+ const tree = { ...this.faculties, [dirname(DEFAULT_CODE)]: await loadClasses(join(this.dir, DEFAULT_CODE)) };
188
+ await standHarbor(this, tree, this.box);
189
+ }
185
190
  await super.boot();
186
191
  // Every being who takes what this box lends does it at her birth, and
187
192
  // birth is synchronous while taking a standing is not. A box is up when
188
193
  // they have landed, not before.
189
- await settled();
194
+ await settled(this);
190
195
  }
191
196
 
192
197
  // Every ward's writing finished, then the lease. A ward says it wrote and
@@ -6,12 +6,11 @@
6
6
  // is a terrain's answer and what it does when it wakes is not.
7
7
  //
8
8
  // One harbor is one Durable Object, and every ward of it lives in that
9
- // object, so the alarm wakes the whole harbor and a tick from her to a home's
10
- // Clock is in-process here as it is on a disk. Only the object may set its
11
- // own alarm, so the two calls are handed to her after the box ward stands,
12
- // the way the grants are handed their save: a faculty is built by her ward
13
- // with a stance and nothing else.
14
- import { Timer } from '../../dock/index.ts';
9
+ // object, so the alarm wakes the whole harbor. Only the object may set its
10
+ // own alarm, so the two calls are handed to her at birth by the class body
11
+ // the object brings.
12
+ import type { Stance } from 'nervur';
13
+ import { Timer, type Harborage } from '../../box/index.ts';
15
14
  import type { Storage } from './platform.d.ts';
16
15
 
17
16
  // Be woken at this moment, or at no moment at all. One alarm per object,
@@ -27,11 +26,16 @@ export const alarmsIn =
27
26
  };
28
27
 
29
28
  export class Alarm extends Timer {
30
- // Handed in after the box ward stands. A timer holding none arms nothing
31
- // and wakes nobody, which is a harbor standing no box ward.
32
- alarms: Alarms | undefined;
29
+ // Handed at birth by the object that brings her. A timer holding none arms
30
+ // nothing and wakes nobody.
31
+ readonly alarms: Alarms | undefined;
33
32
  #setting: Promise<void> = Promise.resolve();
34
33
 
34
+ constructor(stance: Stance, box?: Harborage, alarms?: Alarms) {
35
+ super(stance, box);
36
+ this.alarms = alarms;
37
+ }
38
+
35
39
  // The arming written, for the device's own code and a suite: on this
36
40
  // terrain an arming is a write to storage, and a reader of the alarm waits
37
41
  // for it. It crosses no door.
@@ -40,9 +44,8 @@ export class Alarm extends Timer {
40
44
  }
41
45
 
42
46
  protected override arm(when: number | null): void {
43
- // A faculty arms inside whatever ask learned the moment, and a write is
44
- // not something that ask can answer for, so this is left to finish on its
45
- // own. An alarm the platform refused is armed again by the next round.
47
+ // An arming is left to finish on its own; an alarm the platform refused is
48
+ // armed again by the next round.
46
49
  this.#setting = (this.alarms?.(when) ?? Promise.resolve()).catch(() => {});
47
50
  }
48
51
  }