nervur 0.20.0 → 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 (157) hide show
  1. package/README.md +229 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +5 -1
  4. package/dist/being/faculty.d.ts +1 -0
  5. package/dist/being/faculty.js +13 -7
  6. package/dist/being/index.d.ts +1 -0
  7. package/dist/being/index.js +1 -0
  8. package/dist/being/kind.d.ts +4 -0
  9. package/dist/being/kind.js +28 -0
  10. package/dist/being/types.d.ts +3 -4
  11. package/dist/browser/index.d.ts +35 -0
  12. package/dist/browser/index.js +188 -0
  13. package/dist/browser/worker.d.ts +12 -0
  14. package/dist/browser/worker.js +40 -0
  15. package/dist/browser.d.ts +1 -0
  16. package/dist/browser.js +7 -0
  17. package/dist/cli/command.d.ts +13 -0
  18. package/dist/cli/command.js +237 -0
  19. package/dist/cli/harbor.d.ts +19 -0
  20. package/dist/cli/harbor.js +99 -0
  21. package/dist/cli/main.d.ts +2 -0
  22. package/dist/cli/main.js +14 -0
  23. package/dist/contract/index.d.ts +16 -2
  24. package/dist/contract/index.js +13 -3
  25. package/dist/edge/index.d.ts +57 -0
  26. package/dist/edge/index.js +306 -0
  27. package/dist/edge.d.ts +1 -0
  28. package/dist/edge.js +8 -0
  29. package/dist/folder/index.d.ts +9 -4
  30. package/dist/folder/index.js +89 -84
  31. package/dist/harbor/carrying.d.ts +17 -0
  32. package/dist/harbor/carrying.js +38 -0
  33. package/dist/harbor/catalogue.d.ts +27 -0
  34. package/dist/harbor/catalogue.js +101 -0
  35. package/dist/harbor/dock.d.ts +32 -3
  36. package/dist/harbor/dock.js +98 -12
  37. package/dist/harbor/harbor.d.ts +27 -8
  38. package/dist/harbor/harbor.js +217 -26
  39. package/dist/harbor/index.d.ts +8 -2
  40. package/dist/harbor/index.js +8 -1
  41. package/dist/harbor/package.d.ts +12 -0
  42. package/dist/harbor/package.js +130 -0
  43. package/dist/harbor/probe.d.ts +18 -0
  44. package/dist/harbor/probe.js +15 -0
  45. package/dist/harbor/registry.d.ts +6 -3
  46. package/dist/harbor/registry.js +83 -7
  47. package/dist/harbor/relay.d.ts +15 -0
  48. package/dist/harbor/relay.js +387 -0
  49. package/dist/harbor/root-line.d.ts +9 -0
  50. package/dist/harbor/root-line.js +38 -0
  51. package/dist/harbor/terrain.d.ts +14 -1
  52. package/dist/harbor/terrain.js +19 -0
  53. package/dist/http/index.d.ts +4 -0
  54. package/dist/http/index.js +156 -0
  55. package/dist/http/websocket.d.ts +29 -0
  56. package/dist/http/websocket.js +133 -0
  57. package/dist/index.d.ts +5 -4
  58. package/dist/index.js +12 -6
  59. package/dist/line/answer.d.ts +8 -0
  60. package/dist/line/answer.js +60 -0
  61. package/dist/line/frame.d.ts +34 -0
  62. package/dist/line/frame.js +81 -0
  63. package/dist/line/ground.d.ts +3 -0
  64. package/dist/line/ground.js +9 -0
  65. package/dist/line/index.d.ts +5 -0
  66. package/dist/line/index.js +9 -0
  67. package/dist/line/push.d.ts +7 -0
  68. package/dist/line/push.js +92 -0
  69. package/dist/line/web.d.ts +30 -0
  70. package/dist/line/web.js +181 -0
  71. package/dist/node/index.d.ts +32 -0
  72. package/dist/node/index.js +164 -0
  73. package/dist/node.d.ts +1 -0
  74. package/dist/node.js +7 -0
  75. package/dist/pointer/bodies.d.ts +11 -3
  76. package/dist/pointer/bodies.js +47 -18
  77. package/dist/pointer/index.d.ts +1 -1
  78. package/dist/pointer/index.js +1 -1
  79. package/dist/pointer/world.d.ts +12 -5
  80. package/dist/pointer/world.js +29 -14
  81. package/dist/quo/address.d.ts +19 -0
  82. package/dist/quo/address.js +84 -0
  83. package/dist/quo/door.d.ts +1 -1
  84. package/dist/quo/door.js +6 -4
  85. package/dist/quo/index.d.ts +1 -0
  86. package/dist/quo/index.js +1 -0
  87. package/dist/quo/invitation.d.ts +1 -0
  88. package/dist/quo/invitation.js +9 -5
  89. package/dist/quo/keys.js +1 -1
  90. package/dist/tcp/frame.d.ts +25 -0
  91. package/dist/tcp/frame.js +64 -0
  92. package/dist/tcp/index.d.ts +24 -0
  93. package/dist/tcp/index.js +260 -0
  94. package/dist/ward/index.d.ts +3 -2
  95. package/dist/ward/index.js +2 -1
  96. package/dist/ward/pilot.d.ts +8 -0
  97. package/dist/ward/pilot.js +50 -0
  98. package/dist/ward/stance.d.ts +4 -4
  99. package/dist/ward/stance.js +10 -7
  100. package/dist/ward/ward-being.d.ts +4 -1
  101. package/dist/ward/ward-being.js +8 -2
  102. package/dist/ward/ward.d.ts +11 -2
  103. package/dist/ward/ward.js +41 -5
  104. package/package.json +28 -2
  105. package/src/being/being.ts +5 -1
  106. package/src/being/faculty.ts +14 -7
  107. package/src/being/index.ts +1 -0
  108. package/src/being/kind.ts +30 -0
  109. package/src/being/types.ts +8 -6
  110. package/src/browser/index.ts +207 -0
  111. package/src/browser/worker.ts +66 -0
  112. package/src/browser.ts +9 -0
  113. package/src/cli/command.ts +224 -0
  114. package/src/cli/harbor.ts +103 -0
  115. package/src/cli/main.ts +13 -0
  116. package/src/contract/index.ts +42 -10
  117. package/src/edge/index.ts +359 -0
  118. package/src/edge.ts +10 -0
  119. package/src/folder/index.ts +87 -84
  120. package/src/harbor/carrying.ts +56 -0
  121. package/src/harbor/catalogue.ts +125 -0
  122. package/src/harbor/dock.ts +122 -15
  123. package/src/harbor/harbor.ts +240 -36
  124. package/src/harbor/index.ts +10 -3
  125. package/src/harbor/package.ts +147 -0
  126. package/src/harbor/probe.ts +34 -0
  127. package/src/harbor/registry.ts +73 -9
  128. package/src/harbor/relay.ts +399 -0
  129. package/src/harbor/root-line.ts +39 -0
  130. package/src/harbor/terrain.ts +31 -3
  131. package/src/http/index.ts +150 -0
  132. package/src/http/websocket.ts +124 -0
  133. package/src/index.ts +39 -6
  134. package/src/line/answer.ts +59 -0
  135. package/src/line/frame.ts +88 -0
  136. package/src/line/ground.ts +19 -0
  137. package/src/line/index.ts +9 -0
  138. package/src/line/push.ts +112 -0
  139. package/src/line/web.ts +227 -0
  140. package/src/node/index.ts +170 -0
  141. package/src/node.ts +9 -0
  142. package/src/pointer/bodies.ts +45 -16
  143. package/src/pointer/index.ts +1 -1
  144. package/src/pointer/world.ts +38 -19
  145. package/src/quo/address.ts +87 -0
  146. package/src/quo/door.ts +6 -4
  147. package/src/quo/index.ts +1 -0
  148. package/src/quo/invitation.ts +10 -6
  149. package/src/quo/keys.ts +1 -1
  150. package/src/stand/main.ts +1 -0
  151. package/src/stand/stand.ts +87 -6
  152. package/src/tcp/index.ts +284 -0
  153. package/src/ward/index.ts +3 -2
  154. package/src/ward/pilot.ts +50 -0
  155. package/src/ward/stance.ts +14 -8
  156. package/src/ward/ward-being.ts +9 -3
  157. package/src/ward/ward.ts +53 -6
@@ -0,0 +1,12 @@
1
+ import type { JsonObject } from '../being/index.ts';
2
+ import { type Harbor } from '../harbor/index.ts';
3
+ type Scope = {
4
+ addEventListener(type: string, listener: (event: never) => void): void;
5
+ };
6
+ export declare const ringAll: (harbor: Harbor) => Promise<JsonObject[]>;
7
+ export declare const serveWorker: (harbor: Promise<Harbor>, scope?: Scope) => void;
8
+ export type Reaching = {
9
+ postMessage(message: unknown, transfer: Transferable[]): void;
10
+ };
11
+ export declare const rootOf: (worker: Reaching) => (request: JsonObject) => Promise<JsonObject>;
12
+ export {};
@@ -0,0 +1,40 @@
1
+ import { RELAY_CLIENT, rootLine } from '../harbor/index.js';
2
+ const answer = async (harbor, data, reply) => {
3
+ if (!reply)
4
+ return;
5
+ let out;
6
+ try {
7
+ out = await rootLine(await harbor, data?.request);
8
+ }
9
+ catch {
10
+ out = { error: 'the harbor threw' };
11
+ }
12
+ reply.postMessage(out);
13
+ };
14
+ // Every relay client of the harbor asked to collect what waits.
15
+ export const ringAll = async (harbor) => {
16
+ const keys = Object.entries(harbor.box.partition.beings)
17
+ .filter(([, row]) => row.class === RELAY_CLIENT)
18
+ .map(([key]) => key);
19
+ return Promise.all(keys.map(async (being) => (await harbor.root('ask', { being, method: 'ring' }))));
20
+ };
21
+ // Called as the worker's script starts, so no tab's first message is
22
+ // lost while the harbor opens.
23
+ export const serveWorker = (harbor, scope = globalThis) => {
24
+ scope.addEventListener('connect', (event) => {
25
+ const port = event.ports[0];
26
+ port.onmessage = (message) => void answer(harbor, message.data, message.ports[0]);
27
+ port.start();
28
+ });
29
+ scope.addEventListener('message', (event) => void answer(harbor, event.data, event.ports[0]));
30
+ scope.addEventListener('push', (event) => event.waitUntil(harbor.then(ringAll)));
31
+ };
32
+ // A tab's root line to the worker's harbor.
33
+ export const rootOf = (worker) => (request) => new Promise((resolve) => {
34
+ const channel = new MessageChannel();
35
+ channel.port1.onmessage = (message) => {
36
+ channel.port1.close();
37
+ resolve(message.data);
38
+ };
39
+ worker.postMessage({ request }, [channel.port2]);
40
+ });
@@ -0,0 +1 @@
1
+ export * from './index.ts';
@@ -0,0 +1,7 @@
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.js';
5
+ import { addGround } from './harbor/index.js';
6
+ export * from './index.js';
7
+ addGround(browserGround);
@@ -0,0 +1,13 @@
1
+ import type { JsonObject } from '../being/index.ts';
2
+ import type { RootRequest } from '../harbor/index.ts';
3
+ export declare const parse: (argv: string[]) => {
4
+ command?: string;
5
+ words: string[];
6
+ flags: Record<string, string>;
7
+ };
8
+ export declare const requestOf: (command: string, words: string[], flags: Record<string, string>) => RootRequest;
9
+ export declare const main: (argv: string[], env: Record<string, string | undefined>, print: (line: string) => void) => Promise<number>;
10
+ export declare const pilotRequests: (name: string, invitation: JsonObject, at?: string) => RootRequest[];
11
+ export declare const via: (pilot: string, far: RootRequest) => RootRequest;
12
+ export declare const farAnswer: (local: JsonObject) => JsonObject;
13
+ export declare const SERVING = -1;
@@ -0,0 +1,237 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur`: a harbor on this machine. Each command is one root request, the
3
+ // same the root line carries from any front, printed as one JSON line.
4
+ //
5
+ // nervur init [--dir D] the harbor made: its seed drawn,
6
+ // its catalogue stood, and an owner
7
+ // invitation for whoever made it
8
+ // nervur serve [--dir D] [--host H] [--port P] the harbor served: Quo over TCP,
9
+ // and the root line on its socket
10
+ // nervur module add <file> a module the catalogue runs
11
+ // nervur module remove <module> a module it runs no more
12
+ // nervur modules what the catalogue runs
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
21
+ // nervur open <key> <kind> a faculty stands in the box ward
22
+ // nervur host <ward> [--seed S] a hosted ward stands
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
26
+ // nervur boot [--ward W] <key> <kind> a being stands
27
+ // nervur unboot [--ward W] <being> a being goes
28
+ // nervur public [--ward W] <key | none> the being a stranger reaches
29
+ // nervur invite [--ward W] <being> <id> an invitation on a being
30
+ // nervur ask [--ward W] [being] [method] [args] a being, asked as the owner
31
+ // nervur own [--ward W] <id> an invitation for an owner
32
+ // nervur disown [--ward W] <id> an owner no more
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
36
+ // nervur <command> --via <name> ... the command asked of the far ward
37
+ // that pilot holds, as its owner
38
+ //
39
+ // D is --dir, then $NERVUR_DIR, then ~/.nervur. A module file is named from
40
+ // where the command runs and kept by its absolute path. No --ward is the box
41
+ // ward, and a pilot holds one ward, so --via takes no --ward. The exit is 1
42
+ // when the answer is an error.
43
+ import { existsSync } from 'node:fs';
44
+ import { readFile } from 'node:fs/promises';
45
+ import { join, resolve } from 'node:path';
46
+ import { harborDir } from '../node/index.js';
47
+ import { carry, isServed, land, open, request, serve } from './harbor.js';
48
+ 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]';
49
+ const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
50
+ // The one flag that stands alone, since what it names is a yes.
51
+ const ALONE = 'with-seed';
52
+ export const parse = (argv) => {
53
+ const flags = {};
54
+ const words = [];
55
+ for (let i = 0; i < argv.length; i += 1) {
56
+ const arg = argv[i];
57
+ const name = arg.startsWith('--') ? arg.slice(2) : undefined;
58
+ if (name === undefined)
59
+ words.push(arg);
60
+ else if (name === ALONE)
61
+ flags[name] = 'yes';
62
+ else if (!FLAGS.has(name) || argv[i + 1] === undefined)
63
+ throw new Error(`--${name} is no flag with a value`);
64
+ else
65
+ flags[name] = argv[(i += 1)];
66
+ }
67
+ const [command, ...rest] = words;
68
+ return { ...(command === undefined ? {} : { command }), words: rest, flags };
69
+ };
70
+ // What this command was handed on its input, for `import -`.
71
+ const standingIn = async () => {
72
+ let text = '';
73
+ for await (const chunk of process.stdin)
74
+ text += chunk.toString('utf8');
75
+ return text;
76
+ };
77
+ const none = (word) => (word === 'none' ? null : word);
78
+ const json = (word) => (word === undefined ? undefined : JSON.parse(word));
79
+ const catalogue = (method, args = {}) => ({ method: 'ask', args: { being: 'catalogue', method, args } });
80
+ // The root request a command names, or a throw saying what it lacks.
81
+ export const requestOf = (command, words, flags) => {
82
+ const need = (n) => {
83
+ if (words.length !== n)
84
+ throw new Error(`${command} takes ${n} argument${n === 1 ? '' : 's'}`);
85
+ return words;
86
+ };
87
+ const at = flags.ward === undefined ? {} : { ward: flags.ward };
88
+ switch (command) {
89
+ case 'module': {
90
+ const [verb, what] = need(2);
91
+ if (verb === 'add')
92
+ return catalogue('add', { from: resolve(what) });
93
+ if (verb === 'remove')
94
+ return catalogue('remove', { module: what });
95
+ throw new Error('module is add or remove');
96
+ }
97
+ case 'modules':
98
+ return (need(0), catalogue('modules'));
99
+ case 'sweep':
100
+ return (need(0), { method: 'sweep', args: {} });
101
+ case 'census':
102
+ return (need(0), at);
103
+ case 'open': {
104
+ const [key, kind] = need(2);
105
+ return { method: 'open', args: { key: key, class: kind } };
106
+ }
107
+ case 'host': {
108
+ const [ward] = need(1);
109
+ return { method: 'host', args: { ward: ward, ...(flags.seed === undefined ? {} : { seed: flags.seed }) } };
110
+ }
111
+ case 'route': {
112
+ const [ward, ...where] = words;
113
+ if (ward === undefined || where.length === 0)
114
+ throw new Error('route takes a ward pk and its addresses, or none');
115
+ return { method: 'route', args: { ward, at: where.length === 1 && where[0] === 'none' ? null : where } };
116
+ }
117
+ case 'reach':
118
+ if (words.length === 0)
119
+ throw new Error('reach takes the addresses this harbor is reached at');
120
+ return { method: 'reach', args: { at: words } };
121
+ case 'boot': {
122
+ const [key, kind] = need(2);
123
+ return { ...at, method: 'boot', args: { key: key, class: kind } };
124
+ }
125
+ case 'unboot':
126
+ return { ...at, method: 'unboot', args: { being: need(1)[0] } };
127
+ case 'public':
128
+ return { ...at, method: 'public', args: { key: none(need(1)[0]) } };
129
+ case 'invite': {
130
+ const [being, id] = need(2);
131
+ return { ...at, method: 'invite', args: { being: being, id: id } };
132
+ }
133
+ case 'ask': {
134
+ if (words.length > 3)
135
+ throw new Error('ask takes at most a being, a method and its args');
136
+ const [being, method, args] = words;
137
+ return { ...at, method: 'ask', args: { ...(being === undefined ? {} : { being }), ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args: json(args) }) } };
138
+ }
139
+ case 'own':
140
+ case 'disown':
141
+ return { ...at, method: command, args: { id: need(1)[0] } };
142
+ default:
143
+ throw new Error(USAGE);
144
+ }
145
+ };
146
+ // An error answer at any depth the root line or an ask carries it.
147
+ const failed = (answer) => {
148
+ if (typeof answer !== 'object' || answer === null)
149
+ return false;
150
+ const a = answer;
151
+ return 'error' in a || failed(a.answer);
152
+ };
153
+ export const main = async (argv, env, print) => {
154
+ const { command, words, flags } = parse(argv);
155
+ if (command === undefined)
156
+ throw new Error(USAGE);
157
+ const dir = flags.dir ?? harborDir(env);
158
+ const out = (answer) => {
159
+ print(JSON.stringify(answer));
160
+ return failed(answer) ? 1 : 0;
161
+ };
162
+ if (command === 'init') {
163
+ if (words.length > 0)
164
+ throw new Error('init takes no argument');
165
+ if (existsSync(join(dir, 'seed')))
166
+ return out({ error: `a harbor stands at ${dir}` });
167
+ const harbor = await open(dir);
168
+ try {
169
+ const owner = await harbor.root('own', { id: 'owner' });
170
+ return out({ pk: harbor.box.pk, owner: owner });
171
+ }
172
+ finally {
173
+ await harbor.close();
174
+ }
175
+ }
176
+ if (command === 'export' || command === 'import') {
177
+ if (words.length !== (command === 'import' ? 1 : 0))
178
+ throw new Error(`${command} takes ${command === 'import' ? 'one file' : 'no argument'}`);
179
+ if (flags.via !== undefined)
180
+ throw new Error(`${command} reads this folder, so it takes no --via`);
181
+ if (await isServed(dir))
182
+ throw new Error(`a harbor is served from ${dir}: stop it first`);
183
+ if (command === 'export')
184
+ return out((await carry(dir, flags[ALONE] !== undefined)));
185
+ const text = words[0] === '-' ? await standingIn() : await readFile(words[0], 'utf8');
186
+ return out({ landed: await land(dir, JSON.parse(text)) });
187
+ }
188
+ if (command === 'serve') {
189
+ if (words.length > 0)
190
+ throw new Error('serve takes no argument');
191
+ const served = await serve(dir, flags.host, flags.port === undefined ? undefined : Number(flags.port));
192
+ const stop = () => void served.close().then(() => process.exit(0));
193
+ process.once('SIGINT', stop);
194
+ process.once('SIGTERM', stop);
195
+ print(JSON.stringify({ pk: served.pk, at: served.at, listening: served.listening, root: served.root }));
196
+ return SERVING;
197
+ }
198
+ if (command === 'pilot') {
199
+ if (words.length !== 2 && words.length !== 3)
200
+ throw new Error('pilot takes a name, an invitation, and an address where the invitation carries none');
201
+ const [name, invitation, at] = words;
202
+ const held = JSON.parse(invitation);
203
+ for (const req of pilotRequests(name, held, at)) {
204
+ const answer = await request(dir, req);
205
+ if (!failed(answer))
206
+ continue;
207
+ if (req.method === 'ask')
208
+ await request(dir, { method: 'unboot', args: { being: name } });
209
+ return out(answer);
210
+ }
211
+ return out({ piloting: name, ward: held.ward });
212
+ }
213
+ if (flags.via === undefined)
214
+ return out(await request(dir, requestOf(command, words, flags)));
215
+ if (flags.ward !== undefined)
216
+ throw new Error('--via takes no --ward');
217
+ return out(farAnswer(await request(dir, via(flags.via, requestOf(command, words, flags)))));
218
+ };
219
+ // The root requests that hold a far ward: its route where one is named,
220
+ // else the take learns it from the invitation's `at`; the pilot; the take.
221
+ export const pilotRequests = (name, invitation, at) => [
222
+ ...(at === undefined ? [] : [{ method: 'route', args: { ward: invitation.ward ?? null, at: [at] } }]),
223
+ { method: 'boot', args: { key: name, class: 'org.nervur.pilot' } },
224
+ { method: 'ask', args: { being: name, method: 'hold', args: { invitation } } },
225
+ ];
226
+ // A root request, asked of the far ward a pilot holds.
227
+ export const via = (pilot, far) => ({
228
+ method: 'ask',
229
+ args: { being: pilot, method: 'pilot', args: { ...(far.method === undefined ? {} : { method: far.method }), ...(far.args === undefined ? {} : { args: far.args }) } },
230
+ });
231
+ // The far ward-being's answer out of the pilot's, or the local error.
232
+ export const farAnswer = (local) => {
233
+ const inner = local.answer?.answer;
234
+ return typeof inner === 'object' && inner !== null && !Array.isArray(inner) ? inner : local;
235
+ };
236
+ // What `main` answers for a harbor it keeps serving: no exit yet.
237
+ export const SERVING = -1;
@@ -0,0 +1,19 @@
1
+ import type { JsonObject } from '../being/index.ts';
2
+ import { Harbor, type RootRequest } from '../harbor/index.ts';
3
+ export declare const open: (dir: string) => Promise<Harbor>;
4
+ export declare const isServed: (dir: string) => Promise<boolean>;
5
+ export declare const request: (dir: string, req: RootRequest) => Promise<JsonObject>;
6
+ export type Carried = {
7
+ seed?: string;
8
+ places: Record<string, Record<string, string>>;
9
+ };
10
+ export declare const carry: (dir: string, seed: boolean) => Promise<Carried>;
11
+ export declare const land: (dir: string, value: unknown) => Promise<string[]>;
12
+ export type Served = {
13
+ pk: string;
14
+ at: string;
15
+ listening: string[];
16
+ root: string;
17
+ close(): Promise<void>;
18
+ };
19
+ export declare const serve: (dir: string, host?: string, port?: number) => Promise<Served>;
@@ -0,0 +1,99 @@
1
+ // SPDX-License-Identifier: Apache-2.0
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.
5
+ //
6
+ // A root request goes to a served harbor over its socket. With none served,
7
+ // the command unpacks the harbor from its folder, asks, and closes it, so
8
+ // one process at a time writes the folder.
9
+ import { existsSync } from 'node:fs';
10
+ import { mkdir, writeFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import { hex, isHex, unhex } from '../crypto/index.js';
13
+ import { FolderCustody, FolderMemory } from '../folder/index.js';
14
+ import { Harbor, rootLine } from '../harbor/index.js';
15
+ import { askServed, NodeTerrain, socketOf } from '../node/index.js';
16
+ import { CryptoEntropy } from '../pointer/index.js';
17
+ import { TCP_OPENING } from '../tcp/index.js';
18
+ // A harbor of a folder, opened for one ask: it listens nowhere and serves
19
+ // nothing.
20
+ export const open = (dir) => Harbor.open(new NodeTerrain({ dir, wakes: false, serves: false }));
21
+ // Whether a harbor of this folder is served now.
22
+ export const isServed = async (dir) => (await askServed(dir, {})) !== undefined;
23
+ export const request = async (dir, req) => {
24
+ const served = await askServed(dir, req);
25
+ if (served !== undefined)
26
+ return served;
27
+ const harbor = await open(dir);
28
+ try {
29
+ return await rootLine(harbor, req);
30
+ }
31
+ finally {
32
+ await harbor.close();
33
+ }
34
+ };
35
+ const carried = (value) => {
36
+ if (typeof value !== 'object' || value === null)
37
+ return null;
38
+ const { seed, places } = value;
39
+ if (seed !== undefined && !isHex(seed, 32))
40
+ return null;
41
+ if (typeof places !== 'object' || places === null || Array.isArray(places))
42
+ return null;
43
+ for (const entries of Object.values(places)) {
44
+ if (typeof entries !== 'object' || entries === null || Array.isArray(entries))
45
+ return null;
46
+ for (const bytes of Object.values(entries))
47
+ if (!isHex(bytes))
48
+ return null;
49
+ }
50
+ return { ...(seed === undefined ? {} : { seed }), places: places };
51
+ };
52
+ // The harbor of a folder, read as bytes alone. No harbor opens, so
53
+ // nothing is asked and nothing is written.
54
+ export const carry = async (dir, seed) => {
55
+ const memory = new FolderMemory(dir);
56
+ const places = {};
57
+ for (const place of await memory.places()) {
58
+ places[place] = Object.fromEntries([...(await memory.read(place))].map(([name, bytes]) => [name, hex(bytes)]));
59
+ }
60
+ if (!seed)
61
+ return { places };
62
+ return { seed: hex(await new FolderCustody(dir, new CryptoEntropy()).seed()), places };
63
+ };
64
+ // A carried harbor into an empty folder. A folder holding a seed or a
65
+ // place is left as it stands, and a package with no seed needs one there
66
+ // already, which a terrain that seals its custody writes its own way.
67
+ export const land = async (dir, value) => {
68
+ const held = carried(value);
69
+ if (held === null)
70
+ throw new Error('that is no carried harbor');
71
+ const memory = new FolderMemory(dir);
72
+ if ((await memory.places()).length > 0 || (held.seed !== undefined && existsSync(join(dir, 'seed'))))
73
+ throw new Error(`a harbor stands at ${dir}`);
74
+ await mkdir(dir, { recursive: true, mode: 0o700 });
75
+ if (held.seed !== undefined)
76
+ await writeFile(join(dir, 'seed'), `${held.seed}\n`, { mode: 0o600, flag: 'wx' });
77
+ else if (!existsSync(join(dir, 'seed')))
78
+ 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
+ // The harbor served: a harbor of the Node ground, with Quo over TCP on
85
+ // host and port.
86
+ export const serve = async (dir, host, port) => {
87
+ const harbor = await Harbor.open(new NodeTerrain({ dir }));
88
+ if (!harbor.box.being(TCP_OPENING.key))
89
+ await harbor.root('open', { key: TCP_OPENING.key, class: TCP_OPENING.kind });
90
+ const tcp = harbor.box.being(TCP_OPENING.key);
91
+ if (host !== undefined || port !== undefined || tcp.listening().length === 0) {
92
+ const heard = (await harbor.root('ask', { being: TCP_OPENING.key, method: 'listen', args: { ...(host === undefined ? {} : { host }), ...(port === undefined ? {} : { port }) } }));
93
+ if (typeof heard.answer?.at !== 'string') {
94
+ await harbor.close();
95
+ throw new Error(`the harbor does not listen: ${JSON.stringify(heard)}`);
96
+ }
97
+ }
98
+ return { pk: harbor.box.pk, at: tcp.listening()[0], listening: harbor.carriers.flatMap((c) => c.listening()), root: socketOf(dir), close: () => harbor.close() };
99
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // The `nervur` program: `command.ts` over this process's arguments. A
4
+ // command that cannot run exits 2 and says why on stderr.
5
+ import { main, SERVING } from './command.js';
6
+ try {
7
+ const code = await main(process.argv.slice(2), process.env, (line) => process.stdout.write(`${line}\n`));
8
+ if (code !== SERVING)
9
+ process.exitCode = code;
10
+ }
11
+ catch (e) {
12
+ process.stderr.write(`nervur: ${e.message}\n`);
13
+ process.exitCode = 2;
14
+ }
@@ -16,10 +16,24 @@ export interface Rows {
16
16
  kept(): Promise<boolean>;
17
17
  }
18
18
  export declare abstract class Memory {
19
- abstract open(name: string): Promise<Rows>;
19
+ abstract read(place: string): Promise<Map<string, Uint8Array>>;
20
+ abstract write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
21
+ abstract places(): Promise<string[]>;
22
+ abstract forget(place: string): Promise<void>;
20
23
  }
21
24
  export declare abstract class Custody {
22
- abstract seed(name: string): Promise<Uint8Array>;
25
+ abstract seed(): Promise<Uint8Array>;
26
+ }
27
+ export type Module = {
28
+ readonly module: string;
29
+ readonly version: string;
30
+ readonly classes: readonly unknown[];
31
+ };
32
+ export type Loaded = Module & {
33
+ readonly from: string;
34
+ };
35
+ export declare abstract class Loader {
36
+ abstract load(from: string): Promise<Loaded | null>;
23
37
  }
24
38
  export declare abstract class Carrier {
25
39
  abstract carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
@@ -8,12 +8,22 @@ export class Entropy {
8
8
  // The moment, and a wait that ends.
9
9
  export class Clock {
10
10
  }
11
+ // A harbor's memory as a terrain keeps it: sealed entries it cannot read,
12
+ // in places, each entry by name. Every name is lowercase hex the kit
13
+ // chose, so a body may use it as a file name as it stands.
11
14
  export class Memory {
12
15
  }
13
- // A ward's seed by the harbor's name for it: thirty-two bytes, the same
14
- // every time that name is asked.
16
+ // The harbor's seed: thirty-two bytes, the same every time. It is the one
17
+ // secret a terrain holds.
15
18
  export class Custody {
16
19
  }
17
- // Bytes to a ward pk in; bytes, or null for nothing, out.
20
+ // Code, by the name a harbor keeps for it, or null where this loader has
21
+ // none. Code never crosses a door and never lives in a cell, so it is the
22
+ // one thing beside the seed a terrain hands in: a map, an import, a bundle.
23
+ export class Loader {
24
+ }
25
+ // Bytes to a ward pk in; bytes, or null for nothing, out. A harbor hands a
26
+ // terrain's carrier what it holds no route for; what it routes goes to its
27
+ // carrier faculties.
18
28
  export class Carrier {
19
29
  }
@@ -0,0 +1,57 @@
1
+ import { Custody, Memory, type Entropy, type Module } from '../contract/index.ts';
2
+ import { Harbor, type GroundFaculty, type GroundProbe } from '../harbor/index.ts';
3
+ import { type WebListen } from '../line/index.ts';
4
+ import { PointerTerrain } from '../pointer/index.ts';
5
+ export declare const EDGE = "edge";
6
+ export interface EdgeStorage {
7
+ get(key: string): Promise<unknown>;
8
+ put(entries: Record<string, unknown>): Promise<void>;
9
+ delete(keys: string[]): Promise<number>;
10
+ list(options: {
11
+ prefix: string;
12
+ }): Promise<Map<string, unknown>>;
13
+ transaction<T>(work: (txn: EdgeStorage) => Promise<T>): Promise<T>;
14
+ }
15
+ export interface EdgeState {
16
+ readonly storage: EdgeStorage;
17
+ blockConcurrencyWhile<T>(work: () => Promise<T>): Promise<T>;
18
+ }
19
+ export type EdgeEnv = {
20
+ readonly NERVUR_SECRET?: string;
21
+ };
22
+ export type EdgeGiven = {
23
+ readonly state: EdgeState;
24
+ readonly env: EdgeEnv;
25
+ };
26
+ export declare class EdgeMemory extends Memory {
27
+ #private;
28
+ constructor(storage: EdgeStorage, root?: string);
29
+ read(place: string): Promise<Map<string, Uint8Array>>;
30
+ write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
31
+ places(): Promise<string[]>;
32
+ forget(place: string): Promise<void>;
33
+ }
34
+ export declare class EdgeCustody extends Custody {
35
+ #private;
36
+ constructor(storage: EdgeStorage, secret: string | undefined, entropy?: Entropy, root?: string);
37
+ seed(): Promise<Uint8Array>;
38
+ }
39
+ export declare const edgeTcp: GroundFaculty;
40
+ export type EdgeParts = {
41
+ readonly given: EdgeGiven;
42
+ readonly modules?: readonly Module[];
43
+ };
44
+ export declare class EdgeTerrain extends PointerTerrain {
45
+ #private;
46
+ readonly given: EdgeGiven;
47
+ listening: WebListen | undefined;
48
+ constructor(parts: EdgeParts);
49
+ stand(harbor: Harbor): Promise<void>;
50
+ answer(request: Request): Promise<Response>;
51
+ }
52
+ export declare const edgeGround: GroundProbe;
53
+ export type HarborObjectClass = new (state: EdgeState, env: EdgeEnv) => {
54
+ readonly harbor: Promise<Harbor>;
55
+ fetch(request: Request): Promise<Response>;
56
+ };
57
+ export declare const harborObject: (modules: readonly Module[], stood?: (harbor: Harbor) => Promise<void>) => HarborObjectClass;