nervur 0.20.1 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +130 -8
  2. package/dist/browser/index.d.ts +35 -0
  3. package/dist/browser/index.js +188 -0
  4. package/dist/browser/worker.d.ts +12 -0
  5. package/dist/browser/worker.js +40 -0
  6. package/dist/browser.d.ts +1 -0
  7. package/dist/browser.js +7 -0
  8. package/dist/cli/command.d.ts +1 -1
  9. package/dist/cli/command.js +59 -16
  10. package/dist/cli/harbor.d.ts +9 -11
  11. package/dist/cli/harbor.js +81 -123
  12. package/dist/contract/index.d.ts +2 -1
  13. package/dist/contract/index.js +3 -7
  14. package/dist/edge/index.d.ts +57 -0
  15. package/dist/edge/index.js +306 -0
  16. package/dist/edge.d.ts +1 -0
  17. package/dist/edge.js +8 -0
  18. package/dist/folder/index.d.ts +4 -0
  19. package/dist/folder/index.js +57 -14
  20. package/dist/harbor/carrying.d.ts +17 -0
  21. package/dist/harbor/carrying.js +38 -0
  22. package/dist/harbor/catalogue.d.ts +2 -0
  23. package/dist/harbor/catalogue.js +28 -20
  24. package/dist/harbor/dock.d.ts +22 -3
  25. package/dist/harbor/dock.js +69 -13
  26. package/dist/harbor/harbor.d.ts +14 -4
  27. package/dist/harbor/harbor.js +144 -15
  28. package/dist/harbor/index.d.ts +6 -3
  29. package/dist/harbor/index.js +4 -1
  30. package/dist/harbor/package.d.ts +2 -0
  31. package/dist/harbor/package.js +15 -0
  32. package/dist/harbor/probe.d.ts +18 -0
  33. package/dist/harbor/probe.js +15 -0
  34. package/dist/harbor/registry.d.ts +1 -0
  35. package/dist/harbor/registry.js +11 -0
  36. package/dist/harbor/relay.d.ts +15 -0
  37. package/dist/harbor/relay.js +387 -0
  38. package/dist/harbor/root-line.js +4 -2
  39. package/dist/harbor/terrain.d.ts +12 -0
  40. package/dist/harbor/terrain.js +19 -0
  41. package/dist/http/index.d.ts +4 -0
  42. package/dist/http/index.js +156 -0
  43. package/dist/http/websocket.d.ts +29 -0
  44. package/dist/http/websocket.js +133 -0
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.js +8 -2
  47. package/dist/line/answer.d.ts +8 -0
  48. package/dist/line/answer.js +60 -0
  49. package/dist/line/frame.d.ts +34 -0
  50. package/dist/line/frame.js +81 -0
  51. package/dist/line/ground.d.ts +3 -0
  52. package/dist/line/ground.js +9 -0
  53. package/dist/line/index.d.ts +5 -0
  54. package/dist/line/index.js +9 -0
  55. package/dist/line/push.d.ts +7 -0
  56. package/dist/line/push.js +92 -0
  57. package/dist/line/web.d.ts +30 -0
  58. package/dist/line/web.js +181 -0
  59. package/dist/node/index.d.ts +32 -0
  60. package/dist/node/index.js +164 -0
  61. package/dist/node.d.ts +1 -0
  62. package/dist/node.js +7 -0
  63. package/dist/pointer/bodies.d.ts +2 -0
  64. package/dist/pointer/bodies.js +10 -0
  65. package/dist/pointer/world.d.ts +7 -1
  66. package/dist/pointer/world.js +6 -0
  67. package/dist/quo/address.d.ts +19 -0
  68. package/dist/quo/address.js +84 -0
  69. package/dist/quo/door.d.ts +1 -1
  70. package/dist/quo/door.js +4 -2
  71. package/dist/quo/index.d.ts +1 -0
  72. package/dist/quo/index.js +1 -0
  73. package/dist/quo/invitation.d.ts +1 -0
  74. package/dist/quo/invitation.js +9 -5
  75. package/dist/tcp/frame.d.ts +0 -4
  76. package/dist/tcp/frame.js +0 -10
  77. package/dist/tcp/index.d.ts +8 -2
  78. package/dist/tcp/index.js +123 -27
  79. package/dist/ward/index.d.ts +2 -2
  80. package/dist/ward/index.js +1 -1
  81. package/dist/ward/stance.d.ts +2 -0
  82. package/dist/ward/stance.js +3 -1
  83. package/dist/ward/ward-being.d.ts +2 -1
  84. package/dist/ward/ward-being.js +1 -1
  85. package/dist/ward/ward.d.ts +7 -0
  86. package/dist/ward/ward.js +6 -0
  87. package/package.json +21 -2
  88. package/src/browser/index.ts +207 -0
  89. package/src/browser/worker.ts +66 -0
  90. package/src/browser.ts +9 -0
  91. package/src/cli/command.ts +53 -17
  92. package/src/cli/harbor.ts +82 -116
  93. package/src/contract/index.ts +11 -8
  94. package/src/edge/index.ts +359 -0
  95. package/src/edge.ts +10 -0
  96. package/src/folder/index.ts +57 -12
  97. package/src/harbor/carrying.ts +56 -0
  98. package/src/harbor/catalogue.ts +32 -17
  99. package/src/harbor/dock.ts +88 -17
  100. package/src/harbor/harbor.ts +146 -17
  101. package/src/harbor/index.ts +6 -3
  102. package/src/harbor/package.ts +16 -0
  103. package/src/harbor/probe.ts +34 -0
  104. package/src/harbor/registry.ts +11 -0
  105. package/src/harbor/relay.ts +399 -0
  106. package/src/harbor/root-line.ts +4 -1
  107. package/src/harbor/terrain.ts +28 -2
  108. package/src/http/index.ts +150 -0
  109. package/src/http/websocket.ts +124 -0
  110. package/src/index.ts +35 -2
  111. package/src/line/answer.ts +59 -0
  112. package/src/line/frame.ts +88 -0
  113. package/src/line/ground.ts +19 -0
  114. package/src/line/index.ts +9 -0
  115. package/src/line/push.ts +112 -0
  116. package/src/line/web.ts +227 -0
  117. package/src/node/index.ts +170 -0
  118. package/src/node.ts +9 -0
  119. package/src/pointer/bodies.ts +11 -0
  120. package/src/pointer/world.ts +18 -2
  121. package/src/quo/address.ts +87 -0
  122. package/src/quo/door.ts +4 -2
  123. package/src/quo/index.ts +1 -0
  124. package/src/quo/invitation.ts +10 -6
  125. package/src/stand/stand.ts +60 -21
  126. package/src/tcp/index.ts +142 -31
  127. package/src/ward/index.ts +2 -2
  128. package/src/ward/stance.ts +6 -1
  129. package/src/ward/ward-being.ts +2 -2
  130. package/src/ward/ward.ts +16 -0
  131. package/src/tcp/frame.ts +0 -78
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The browser harbor in a worker, one for every tab. A shared, dedicated
3
+ // or service worker calls `serveWorker` as its script starts, with the
4
+ // harbor it opens; every tab reaches that harbor's root line with
5
+ // `rootOf`, one request on a channel of its own. A service worker that
6
+ // hears a push asks every relay client of its harbor to `ring`, since a
7
+ // push carries nothing but the news that something waits.
8
+ import type { JsonObject } from '../being/index.ts';
9
+ import { RELAY_CLIENT, rootLine, type Harbor } from '../harbor/index.ts';
10
+
11
+ type Asked = { request?: unknown };
12
+ type Answering = { postMessage(message: unknown): void };
13
+ type Scope = {
14
+ addEventListener(type: string, listener: (event: never) => void): void;
15
+ };
16
+ type Connecting = { ports: readonly MessagePort[] };
17
+ type Messaging = { data: unknown; ports: readonly MessagePort[] };
18
+ type Pushing = { waitUntil(work: Promise<unknown>): void };
19
+
20
+ const answer = async (harbor: Promise<Harbor>, data: unknown, reply: Answering | undefined): Promise<void> => {
21
+ if (!reply) return;
22
+ let out: JsonObject;
23
+ try {
24
+ out = await rootLine(await harbor, (data as Asked | null)?.request);
25
+ } catch {
26
+ out = { error: 'the harbor threw' };
27
+ }
28
+ reply.postMessage(out);
29
+ };
30
+
31
+ // Every relay client of the harbor asked to collect what waits.
32
+ export const ringAll = async (harbor: Harbor): Promise<JsonObject[]> => {
33
+ const keys = Object.entries(harbor.box.partition.beings)
34
+ .filter(([, row]) => row.class === RELAY_CLIENT)
35
+ .map(([key]) => key);
36
+ return Promise.all(keys.map(async (being) => (await harbor.root('ask', { being, method: 'ring' })) as JsonObject));
37
+ };
38
+
39
+ // Called as the worker's script starts, so no tab's first message is
40
+ // lost while the harbor opens.
41
+ export const serveWorker = (harbor: Promise<Harbor>, scope: Scope = globalThis as unknown as Scope): void => {
42
+ scope.addEventListener('connect', (event: Connecting) => {
43
+ const port = event.ports[0]!;
44
+ port.onmessage = (message) => void answer(harbor, message.data, message.ports[0]);
45
+ port.start();
46
+ });
47
+ scope.addEventListener('message', (event: Messaging) => void answer(harbor, event.data, event.ports[0]));
48
+ scope.addEventListener('push', (event: Pushing) => event.waitUntil(harbor.then(ringAll)));
49
+ };
50
+
51
+ // Where a tab sends root requests: a shared worker's port, a dedicated
52
+ // worker, or a service worker.
53
+ export type Reaching = { postMessage(message: unknown, transfer: Transferable[]): void };
54
+
55
+ // A tab's root line to the worker's harbor.
56
+ export const rootOf =
57
+ (worker: Reaching) =>
58
+ (request: JsonObject): Promise<JsonObject> =>
59
+ new Promise((resolve) => {
60
+ const channel = new MessageChannel();
61
+ channel.port1.onmessage = (message) => {
62
+ channel.port1.close();
63
+ resolve(message.data as JsonObject);
64
+ };
65
+ worker.postMessage({ request }, [channel.port2]);
66
+ });
package/src/browser.ts ADDED
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur` as a browser bundle reads it: the main entry, with the browser
3
+ // ground added, so `Harbor.open()` keeps a harbor in IndexedDB.
4
+ import { browserGround } from './browser/index.ts';
5
+ import { addGround } from './harbor/index.ts';
6
+
7
+ export * from './index.ts';
8
+
9
+ addGround(browserGround);
@@ -11,9 +11,18 @@
11
11
  // nervur module remove <module> a module it runs no more
12
12
  // nervur modules what the catalogue runs
13
13
  // nervur census [--ward W] the empty ask
14
+ // nervur sweep every place of the folder this
15
+ // harbor does not name, gone
16
+ // nervur export [--with-seed] the harbor as one JSON line, to
17
+ // carry it to another ground; with
18
+ // --with-seed the seed goes too
19
+ // nervur import <file> a carried harbor into an empty
20
+ // folder, `-` being this input
14
21
  // nervur open <key> <kind> a faculty stands in the box ward
15
22
  // nervur host <ward> [--seed S] a hosted ward stands
16
- // nervur route <ward pk> <host:port | none> where a ward is reached
23
+ // nervur route <ward pk> <address... | none> where a ward is reached, in order
24
+ // nervur reach <address...> where this harbor is reached, as
25
+ // every invitation it gives says
17
26
  // nervur boot [--ward W] <key> <kind> a being stands
18
27
  // nervur unboot [--ward W] <being> a being goes
19
28
  // nervur public [--ward W] <key | none> the being a stranger reaches
@@ -21,8 +30,9 @@
21
30
  // nervur ask [--ward W] [being] [method] [args] a being, asked as the owner
22
31
  // nervur own [--ward W] <id> an invitation for an owner
23
32
  // nervur disown [--ward W] <id> an owner no more
24
- // nervur pilot <name> <invitation> <host:port> a far ward held as its owner, by a
25
- // pilot being of the box ward
33
+ // nervur pilot <name> <invitation> [address] a far ward held as its owner, by a
34
+ // pilot being of the box ward, at
35
+ // the address or the invitation's at
26
36
  // nervur <command> --via <name> ... the command asked of the far ward
27
37
  // that pilot holds, as its owner
28
38
  //
@@ -31,14 +41,17 @@
31
41
  // ward, and a pilot holds one ward, so --via takes no --ward. The exit is 1
32
42
  // when the answer is an error.
33
43
  import { existsSync } from 'node:fs';
34
- import { homedir } from 'node:os';
44
+ import { readFile } from 'node:fs/promises';
35
45
  import { join, resolve } from 'node:path';
36
46
  import type { JsonObject } from '../being/index.ts';
37
47
  import type { RootRequest } from '../harbor/index.ts';
38
- import { open, request, serve } from './harbor.ts';
48
+ import { harborDir } from '../node/index.ts';
49
+ import { carry, isServed, land, open, request, serve } from './harbor.ts';
39
50
 
40
- const USAGE = 'usage: nervur <init|serve|module|modules|census|open|host|route|boot|unboot|public|invite|ask|own|disown|pilot> [args] [--dir D] [--ward W] [--via V] [--seed S] [--host H] [--port P]';
51
+ const USAGE = 'usage: nervur <init|serve|module|modules|census|sweep|open|host|route|reach|boot|unboot|public|invite|ask|own|disown|pilot|export|import> [args] [--dir D] [--ward W] [--via V] [--seed S] [--with-seed] [--host H] [--port P]';
41
52
  const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
53
+ // The one flag that stands alone, since what it names is a yes.
54
+ const ALONE = 'with-seed';
42
55
 
43
56
  export const parse = (argv: string[]): { command?: string; words: string[]; flags: Record<string, string> } => {
44
57
  const flags: Record<string, string> = {};
@@ -47,6 +60,7 @@ export const parse = (argv: string[]): { command?: string; words: string[]; flag
47
60
  const arg = argv[i]!;
48
61
  const name = arg.startsWith('--') ? arg.slice(2) : undefined;
49
62
  if (name === undefined) words.push(arg);
63
+ else if (name === ALONE) flags[name] = 'yes';
50
64
  else if (!FLAGS.has(name) || argv[i + 1] === undefined) throw new Error(`--${name} is no flag with a value`);
51
65
  else flags[name] = argv[(i += 1)]!;
52
66
  }
@@ -54,6 +68,13 @@ export const parse = (argv: string[]): { command?: string; words: string[]; flag
54
68
  return { ...(command === undefined ? {} : { command }), words: rest, flags };
55
69
  };
56
70
 
71
+ // What this command was handed on its input, for `import -`.
72
+ const standingIn = async (): Promise<string> => {
73
+ let text = '';
74
+ for await (const chunk of process.stdin) text += (chunk as Buffer).toString('utf8');
75
+ return text;
76
+ };
77
+
57
78
  const none = (word: string): string | null => (word === 'none' ? null : word);
58
79
  const json = (word: string | undefined): JsonObject | undefined => (word === undefined ? undefined : (JSON.parse(word) as JsonObject));
59
80
  const catalogue = (method: string, args: JsonObject = {}): RootRequest => ({ method: 'ask', args: { being: 'catalogue', method, args } });
@@ -74,6 +95,8 @@ export const requestOf = (command: string, words: string[], flags: Record<string
74
95
  }
75
96
  case 'modules':
76
97
  return (need(0), catalogue('modules'));
98
+ case 'sweep':
99
+ return (need(0), { method: 'sweep', args: {} });
77
100
  case 'census':
78
101
  return (need(0), at);
79
102
  case 'open': {
@@ -85,9 +108,13 @@ export const requestOf = (command: string, words: string[], flags: Record<string
85
108
  return { method: 'host', args: { ward: ward!, ...(flags.seed === undefined ? {} : { seed: flags.seed }) } };
86
109
  }
87
110
  case 'route': {
88
- const [ward, where] = need(2);
89
- return { method: 'route', args: { ward: ward!, at: none(where!) } };
111
+ const [ward, ...where] = words;
112
+ if (ward === undefined || where.length === 0) throw new Error('route takes a ward pk and its addresses, or none');
113
+ return { method: 'route', args: { ward, at: where.length === 1 && where[0] === 'none' ? null : where } };
90
114
  }
115
+ case 'reach':
116
+ if (words.length === 0) throw new Error('reach takes the addresses this harbor is reached at');
117
+ return { method: 'reach', args: { at: words } };
91
118
  case 'boot': {
92
119
  const [key, kind] = need(2);
93
120
  return { ...at, method: 'boot', args: { key: key!, class: kind! } };
@@ -123,7 +150,7 @@ const failed = (answer: unknown): boolean => {
123
150
  export const main = async (argv: string[], env: Record<string, string | undefined>, print: (line: string) => void): Promise<number> => {
124
151
  const { command, words, flags } = parse(argv);
125
152
  if (command === undefined) throw new Error(USAGE);
126
- const dir = flags.dir ?? env.NERVUR_DIR ?? join(homedir(), '.nervur');
153
+ const dir = flags.dir ?? harborDir(env);
127
154
  const out = (answer: JsonObject): number => {
128
155
  print(JSON.stringify(answer));
129
156
  return failed(answer) ? 1 : 0;
@@ -131,26 +158,34 @@ export const main = async (argv: string[], env: Record<string, string | undefine
131
158
  if (command === 'init') {
132
159
  if (words.length > 0) throw new Error('init takes no argument');
133
160
  if (existsSync(join(dir, 'seed'))) return out({ error: `a harbor stands at ${dir}` });
134
- const { harbor, carrier } = await open(dir);
161
+ const harbor = await open(dir);
135
162
  try {
136
163
  const owner = await harbor.root('own', { id: 'owner' });
137
164
  return out({ pk: harbor.box.pk, owner: owner as JsonObject });
138
165
  } finally {
139
- await carrier.close();
166
+ await harbor.close();
140
167
  }
141
168
  }
169
+ if (command === 'export' || command === 'import') {
170
+ if (words.length !== (command === 'import' ? 1 : 0)) throw new Error(`${command} takes ${command === 'import' ? 'one file' : 'no argument'}`);
171
+ if (flags.via !== undefined) throw new Error(`${command} reads this folder, so it takes no --via`);
172
+ if (await isServed(dir)) throw new Error(`a harbor is served from ${dir}: stop it first`);
173
+ if (command === 'export') return out((await carry(dir, flags[ALONE] !== undefined)) as unknown as JsonObject);
174
+ const text = words[0] === '-' ? await standingIn() : await readFile(words[0]!, 'utf8');
175
+ return out({ landed: await land(dir, JSON.parse(text)) });
176
+ }
142
177
  if (command === 'serve') {
143
178
  if (words.length > 0) throw new Error('serve takes no argument');
144
179
  const served = await serve(dir, flags.host, flags.port === undefined ? undefined : Number(flags.port));
145
180
  const stop = (): void => void served.close().then(() => process.exit(0));
146
181
  process.once('SIGINT', stop);
147
182
  process.once('SIGTERM', stop);
148
- print(JSON.stringify({ pk: served.pk, at: served.at, root: served.root }));
183
+ print(JSON.stringify({ pk: served.pk, at: served.at, listening: served.listening, root: served.root }));
149
184
  return SERVING;
150
185
  }
151
186
  if (command === 'pilot') {
152
- if (words.length !== 3) throw new Error('pilot takes 3 arguments');
153
- const [name, invitation, at] = words as [string, string, string];
187
+ if (words.length !== 2 && words.length !== 3) throw new Error('pilot takes a name, an invitation, and an address where the invitation carries none');
188
+ const [name, invitation, at] = words as [string, string, string | undefined];
154
189
  const held = JSON.parse(invitation) as JsonObject;
155
190
  for (const req of pilotRequests(name, held, at)) {
156
191
  const answer = await request(dir, req);
@@ -165,9 +200,10 @@ export const main = async (argv: string[], env: Record<string, string | undefine
165
200
  return out(farAnswer(await request(dir, via(flags.via, requestOf(command, words, flags)))));
166
201
  };
167
202
 
168
- // The root requests that hold a far ward: its route, the pilot, the take.
169
- export const pilotRequests = (name: string, invitation: JsonObject, at: string): RootRequest[] => [
170
- { method: 'route', args: { ward: invitation.ward ?? null, at } },
203
+ // The root requests that hold a far ward: its route where one is named,
204
+ // else the take learns it from the invitation's `at`; the pilot; the take.
205
+ export const pilotRequests = (name: string, invitation: JsonObject, at?: string): RootRequest[] => [
206
+ ...(at === undefined ? [] : [{ method: 'route', args: { ward: invitation.ward ?? null, at: [at] } }]),
171
207
  { method: 'boot', args: { key: name, class: 'org.nervur.pilot' } },
172
208
  { method: 'ask', args: { being: name, method: 'hold', args: { invitation } } },
173
209
  ];
package/src/cli/harbor.ts CHANGED
@@ -1,137 +1,103 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // A harbor on this machine, as the `nervur` command stands it: its seed and
3
- // sealed memory in a folder, its code imported from the files its
4
- // catalogue names, Quo over TCP as its carrier, and the root line on a
5
- // local socket beside them.
6
- //
7
- // <dir>/seed the harbor's seed
8
- // <dir>/<place>/ its sealed memory
9
- // <dir>/root.sock the root line while the harbor is served, open to
10
- // this user alone, or in the temporary folder for a
11
- // folder too deep for a socket path
2
+ // A harbor on this machine, as the `nervur` command stands it: a harbor of
3
+ // the Node ground in a folder, served while `serve` runs, and asked over
4
+ // its root line's socket.
12
5
  //
13
6
  // A root request goes to a served harbor over its socket. With none served,
14
7
  // the command unpacks the harbor from its folder, asks, and closes it, so
15
8
  // one process at a time writes the folder.
16
- import { createHash } from 'node:crypto';
17
- import { chmod, rm } from 'node:fs/promises';
18
- import { connect, createServer, type Server } from 'node:net';
19
- import { tmpdir } from 'node:os';
20
- import { join, resolve } from 'node:path';
21
- import { pathToFileURL } from 'node:url';
9
+ import { existsSync } from 'node:fs';
10
+ import { mkdir, writeFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
22
12
  import type { JsonObject } from '../being/index.ts';
23
- import { Loader, type Loaded } from '../contract/index.ts';
13
+ import { hex, isHex, unhex } from '../crypto/index.ts';
24
14
  import { FolderCustody, FolderMemory } from '../folder/index.ts';
25
- import { Harbor, rootLine, type RootRequest } from '../harbor/index.ts';
26
- import { CryptoEntropy, PointerTerrain } from '../pointer/index.ts';
27
- import { TcpCarrier, TcpListener } from '../tcp/index.ts';
28
-
29
- // A socket path has a hundred bytes or so on every system, so a folder too
30
- // deep for one puts its socket in this user's temporary folder, under a
31
- // name drawn from the folder's own path.
32
- const SOCKET_PATH = 100;
33
- export const socketOf = (dir: string): string => {
34
- const beside = join(resolve(dir), 'root.sock');
35
- if (Buffer.byteLength(beside) <= SOCKET_PATH) return beside;
36
- return join(tmpdir(), `nervur-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 32)}.sock`);
37
- };
38
-
39
- // Code from a file: a module that exports `module`, `version` and
40
- // `classes`, found again by its absolute path. A file that cannot be
41
- // imported, or exports no module, is none.
42
- export class ImportLoader extends Loader {
43
- async load(from: string): Promise<Loaded | null> {
44
- const path = resolve(from);
45
- let exported: { module?: unknown; version?: unknown; classes?: unknown };
46
- try {
47
- exported = (await import(pathToFileURL(path).href)) as typeof exported;
48
- } catch {
49
- return null;
50
- }
51
- const { module, version, classes } = exported;
52
- if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes)) return null;
53
- return { module, version, classes, from: path };
54
- }
55
- }
56
-
57
- export type Opened = { harbor: Harbor; carrier: TcpCarrier };
15
+ import { Harbor, rootLine, type CarrierFaculty, type RootRequest } from '../harbor/index.ts';
16
+ import { askServed, NodeTerrain, socketOf } from '../node/index.ts';
17
+ import { CryptoEntropy } from '../pointer/index.ts';
18
+ import { TCP_OPENING } from '../tcp/index.ts';
58
19
 
59
- export const open = async (dir: string): Promise<Opened> => {
60
- const entropy = new CryptoEntropy();
61
- const carrier = new TcpCarrier();
62
- const harbor = await Harbor.open(new PointerTerrain({ entropy, carrier, loader: new ImportLoader(), custody: new FolderCustody(dir, entropy), memory: new FolderMemory(dir) }));
63
- return { harbor, carrier };
64
- };
20
+ // A harbor of a folder, opened for one ask: it listens nowhere and serves
21
+ // nothing.
22
+ export const open = (dir: string): Promise<Harbor> => Harbor.open(new NodeTerrain({ dir, wakes: false, serves: false }));
65
23
 
66
- // One root request over a served harbor's socket, or undefined where none
67
- // is served.
68
- const overSocket = (dir: string, request: RootRequest): Promise<JsonObject | undefined> =>
69
- new Promise((done, fail) => {
70
- const socket = connect(socketOf(dir));
71
- let text = '';
72
- socket.once('connect', () => socket.write(`${JSON.stringify(request)}\n`));
73
- socket.on('data', (chunk: Buffer) => {
74
- text += chunk.toString('utf8');
75
- const end = text.indexOf('\n');
76
- if (end < 0) return;
77
- socket.end();
78
- done(JSON.parse(text.slice(0, end)) as JsonObject);
79
- });
80
- socket.once('error', (e: NodeJS.ErrnoException) => (e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? done(undefined) : fail(e)));
81
- });
24
+ // Whether a harbor of this folder is served now.
25
+ export const isServed = async (dir: string): Promise<boolean> => (await askServed(dir, {})) !== undefined;
82
26
 
83
27
  export const request = async (dir: string, req: RootRequest): Promise<JsonObject> => {
84
- const served = await overSocket(dir, req);
28
+ const served = await askServed(dir, req);
85
29
  if (served !== undefined) return served;
86
- const { harbor, carrier } = await open(dir);
30
+ const harbor = await open(dir);
87
31
  try {
88
32
  return await rootLine(harbor, req);
89
33
  } finally {
90
- await carrier.close();
34
+ await harbor.close();
35
+ }
36
+ };
37
+
38
+ // A harbor as one JSON value, for carrying it to another ground: every
39
+ // place of its memory with its sealed entries as hex, and its seed only
40
+ // where the one who asks says so, since custody holds it and a package
41
+ // without it opens nowhere.
42
+ export type Carried = { seed?: string; places: Record<string, Record<string, string>> };
43
+
44
+ const carried = (value: unknown): Carried | null => {
45
+ if (typeof value !== 'object' || value === null) return null;
46
+ const { seed, places } = value as { seed?: unknown; places?: unknown };
47
+ if (seed !== undefined && !isHex(seed, 32)) return null;
48
+ if (typeof places !== 'object' || places === null || Array.isArray(places)) return null;
49
+ for (const entries of Object.values(places)) {
50
+ if (typeof entries !== 'object' || entries === null || Array.isArray(entries)) return null;
51
+ for (const bytes of Object.values(entries)) if (!isHex(bytes)) return null;
52
+ }
53
+ return { ...(seed === undefined ? {} : { seed }), places: places as Carried['places'] };
54
+ };
55
+
56
+ // The harbor of a folder, read as bytes alone. No harbor opens, so
57
+ // nothing is asked and nothing is written.
58
+ export const carry = async (dir: string, seed: boolean): Promise<Carried> => {
59
+ const memory = new FolderMemory(dir);
60
+ const places: Carried['places'] = {};
61
+ for (const place of await memory.places()) {
62
+ places[place] = Object.fromEntries([...(await memory.read(place))].map(([name, bytes]) => [name, hex(bytes)]));
91
63
  }
64
+ if (!seed) return { places };
65
+ return { seed: hex(await new FolderCustody(dir, new CryptoEntropy()).seed()), places };
92
66
  };
93
67
 
94
- export type Served = { pk: string; at: string; root: string; close(): Promise<void> };
68
+ // A carried harbor into an empty folder. A folder holding a seed or a
69
+ // place is left as it stands, and a package with no seed needs one there
70
+ // already, which a terrain that seals its custody writes its own way.
71
+ export const land = async (dir: string, value: unknown): Promise<string[]> => {
72
+ const held = carried(value);
73
+ if (held === null) throw new Error('that is no carried harbor');
74
+ const memory = new FolderMemory(dir);
75
+ if ((await memory.places()).length > 0 || (held.seed !== undefined && existsSync(join(dir, 'seed')))) throw new Error(`a harbor stands at ${dir}`);
76
+ await mkdir(dir, { recursive: true, mode: 0o700 });
77
+ if (held.seed !== undefined) await writeFile(join(dir, 'seed'), `${held.seed}\n`, { mode: 0o600, flag: 'wx' });
78
+ else if (!existsSync(join(dir, 'seed'))) throw new Error('the package carries no seed, and none is held here');
79
+ for (const [place, entries] of Object.entries(held.places)) {
80
+ await memory.write(place, new Map(Object.entries(entries).map(([name, bytes]) => [name, unhex(bytes)])));
81
+ }
82
+ return Object.keys(held.places);
83
+ };
84
+
85
+ // A served harbor: its pk, its tcp address, every address its carriers
86
+ // listen at, and its root line's socket.
87
+ export type Served = { pk: string; at: string; listening: string[]; root: string; close(): Promise<void> };
95
88
 
96
- // The harbor served: Quo over TCP on host and port, the root line on the
97
- // socket. A socket file nobody answers on is a stale one, and is replaced.
89
+ // The harbor served: a harbor of the Node ground, with Quo over TCP on
90
+ // host and port.
98
91
  export const serve = async (dir: string, host?: string, port?: number): Promise<Served> => {
99
- if ((await overSocket(dir, {})) !== undefined) throw new Error(`a harbor is already served from ${dir}`);
100
- const { harbor, carrier } = await open(dir);
101
- const listener = await TcpListener.listen(harbor, host, port);
102
- const root = socketOf(dir);
103
- await rm(root, { force: true });
104
- const server: Server = createServer((socket) => {
105
- let text = '';
106
- socket.on('data', (chunk: Buffer) => {
107
- text += chunk.toString('utf8');
108
- for (let end = text.indexOf('\n'); end >= 0; end = text.indexOf('\n')) {
109
- const line = text.slice(0, end);
110
- text = text.slice(end + 1);
111
- let value: unknown;
112
- try {
113
- value = JSON.parse(line);
114
- } catch {
115
- value = undefined;
116
- }
117
- void rootLine(harbor, value)
118
- .catch(() => ({ error: 'the harbor threw' }))
119
- .then((answer) => socket.write(`${JSON.stringify(answer)}\n`));
120
- }
121
- });
122
- socket.on('error', () => socket.destroy());
123
- });
124
- await new Promise<void>((done) => server.listen(root, done));
125
- await chmod(root, 0o600);
126
- return {
127
- pk: harbor.box.pk,
128
- at: listener.at,
129
- root,
130
- close: async () => {
131
- await new Promise((done) => server.close(done));
132
- await rm(root, { force: true });
133
- await listener.close();
134
- await carrier.close();
135
- },
136
- };
92
+ const harbor = await Harbor.open(new NodeTerrain({ dir }));
93
+ if (!harbor.box.being(TCP_OPENING.key)) await harbor.root('open', { key: TCP_OPENING.key, class: TCP_OPENING.kind });
94
+ const tcp = harbor.box.being(TCP_OPENING.key) as CarrierFaculty;
95
+ if (host !== undefined || port !== undefined || tcp.listening().length === 0) {
96
+ const heard = (await harbor.root('ask', { being: TCP_OPENING.key, method: 'listen', args: { ...(host === undefined ? {} : { host }), ...(port === undefined ? {} : { port }) } })) as JsonObject;
97
+ if (typeof (heard.answer as JsonObject | undefined)?.at !== 'string') {
98
+ await harbor.close();
99
+ throw new Error(`the harbor does not listen: ${JSON.stringify(heard)}`);
100
+ }
101
+ }
102
+ return { pk: harbor.box.pk, at: tcp.listening()[0]!, listening: harbor.carriers.flatMap((c) => c.listening()), root: socketOf(dir), close: () => harbor.close() };
137
103
  };
@@ -36,8 +36,15 @@ export interface Rows {
36
36
  export abstract class Memory {
37
37
  // Every entry kept in a place; a place nothing was kept in is empty.
38
38
  abstract read(place: string): Promise<Map<string, Uint8Array>>;
39
- // One keep: each entry written whole, or removed where it is null.
39
+ // One keep, whole: every entry written and every null removed together,
40
+ // or none of them. A read after a keep that did not finish, in this run
41
+ // or in the run before it, holds the place as it was or as the keep
42
+ // leaves it, and never half of each.
40
43
  abstract write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
44
+ // Every place this memory holds something in, in any order.
45
+ abstract places(): Promise<string[]>;
46
+ // A place and everything in it, gone.
47
+ abstract forget(place: string): Promise<void>;
41
48
  }
42
49
 
43
50
  // The harbor's seed: thirty-two bytes, the same every time. It is the one
@@ -61,13 +68,9 @@ export abstract class Loader {
61
68
  abstract load(from: string): Promise<Loaded | null>;
62
69
  }
63
70
 
64
- // Bytes to a ward pk in; bytes, or null for nothing, out.
71
+ // Bytes to a ward pk in; bytes, or null for nothing, out. A harbor hands a
72
+ // terrain's carrier what it holds no route for; what it routes goes to its
73
+ // carrier faculties.
65
74
  export abstract class Carrier {
66
75
  abstract carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
67
- // Where a ward pk is reached, or null to forget it: true when this
68
- // carrier dials by address and took it. The addresses live in the dock's
69
- // cells, and the harbor hands them here as it unpacks.
70
- route(_pk: string, _at: string | null): boolean {
71
- return false;
72
- }
73
76
  }