nervur 0.20.2 → 0.20.4
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.
- package/README.md +60 -15
- package/dist/cli/command.d.ts +1 -1
- package/dist/cli/command.js +30 -9
- package/dist/cli/harbor.d.ts +2 -5
- package/dist/cli/harbor.js +28 -36
- package/dist/cli/main.js +0 -0
- package/dist/edge/index.d.ts +17 -3
- package/dist/edge/index.js +40 -12
- package/dist/harbor/index.d.ts +1 -1
- package/dist/harbor/index.js +1 -1
- package/dist/harbor/package.d.ts +8 -0
- package/dist/harbor/package.js +37 -1
- package/dist/harbor/relay.js +20 -1
- package/dist/node/index.d.ts +5 -1
- package/dist/node/index.js +77 -13
- package/package.json +3 -2
- package/src/cli/command.ts +27 -9
- package/src/cli/harbor.ts +26 -35
- package/src/edge/index.ts +76 -22
- package/src/harbor/index.ts +1 -1
- package/src/harbor/package.ts +39 -1
- package/src/harbor/relay.ts +20 -1
- package/src/node/index.ts +77 -13
package/README.md
CHANGED
|
@@ -35,22 +35,58 @@ while it stands. A folder opens in one run at a time. Bundled for a
|
|
|
35
35
|
browser, in a page or a worker, it keeps itself in the IndexedDB
|
|
36
36
|
database `where` names, else `nervur`, its seed sealed under a key the
|
|
37
37
|
browser never hands out, speaks the web, and opens in one run at a time.
|
|
38
|
-
On Cloudflare Workers it is one Durable Object
|
|
38
|
+
On Cloudflare Workers it is one Durable Object, answering Quo at `/quo`
|
|
39
|
+
on whatever host reaches it, and asking over TCP and the web. Anywhere
|
|
40
|
+
else it lives as long as its process and speaks the web. Hand in a
|
|
41
|
+
terrain, as below, and that one is used.
|
|
42
|
+
|
|
43
|
+
## A harbor on the edge
|
|
44
|
+
|
|
45
|
+
An edge harbor is born on your machine and carried there, so you pilot it
|
|
46
|
+
from your own harbor, as you pilot one on a server. Make it and set it
|
|
47
|
+
up with the command below, naming where the edge will reach it:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
nervur init --dir ./edge
|
|
51
|
+
nervur reach https://harbor.example.workers.dev/quo --dir ./edge
|
|
52
|
+
nervur own pilot --dir ./edge
|
|
53
|
+
nervur edge pack ./package.json --dir ./edge
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`own` prints the owner invitation, which never leaves your machine.
|
|
57
|
+
`edge pack` prints the harbor's seed and writes its sealed package. Keep
|
|
58
|
+
the seed as the worker's secret, `wrangler secret put NERVUR_SEED`, and
|
|
59
|
+
bundle the package with the worker:
|
|
39
60
|
|
|
40
61
|
```js
|
|
41
62
|
import { harborObject } from 'nervur/edge';
|
|
63
|
+
import carried from './package.json';
|
|
42
64
|
import * as greetings from './greetings.js';
|
|
43
65
|
|
|
44
|
-
export const Harbor = harborObject([greetings],
|
|
45
|
-
// the worker's own code is the root: runs once the harbor stands
|
|
46
|
-
});
|
|
66
|
+
export const Harbor = harborObject({ modules: [greetings], package: carried });
|
|
47
67
|
```
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
and
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
The first time the object starts, the package lands in its storage and
|
|
70
|
+
the harbor opens under that seed; after that the storage is the harbor,
|
|
71
|
+
and a new deploy leaves it be. Then, from your own harbor:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
nervur pilot edge '<the owner invitation>' --dir ./mine
|
|
75
|
+
nervur host shop --via edge --dir ./mine
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`pilot` opens the web carrier where the invitation names a web address.
|
|
79
|
+
An object with no package draws its own seed, sealed under the worker's
|
|
80
|
+
`NERVUR_SECRET`, and `stood` runs the worker's own code on it as root:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
export const Shop = harborObject({
|
|
84
|
+
modules: [greetings],
|
|
85
|
+
stood: async (harbor) => {
|
|
86
|
+
// runs once the harbor stands
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
54
90
|
|
|
55
91
|
## A world in one process
|
|
56
92
|
|
|
@@ -175,8 +211,13 @@ stands a ward under a seed you name, or a drawn one.
|
|
|
175
211
|
- **Push, webPush**: a ring with no content, as Web Push sends it,
|
|
176
212
|
signed with VAPID, on fetch and Web Crypto.
|
|
177
213
|
|
|
178
|
-
|
|
179
|
-
|
|
214
|
+
Quo crosses two carriers, TCP and the web, and no third. MCP, an HTTP API
|
|
215
|
+
and every other outside protocol are gateway faculties: classes that
|
|
216
|
+
serve their clients, a UI in a browser, an MCP client, a caller holding
|
|
217
|
+
a bearer token, on one side, and stand as an ordinary being on the
|
|
218
|
+
other, offering standings, holding occupants and speaking Quo inbound
|
|
219
|
+
and outbound. They and every screen
|
|
220
|
+
are `@nervur-org/*`'s.
|
|
180
221
|
|
|
181
222
|
## The command
|
|
182
223
|
|
|
@@ -186,7 +227,7 @@ folder, Quo over TCP as its carrier. A module is a file that exports
|
|
|
186
227
|
|
|
187
228
|
```sh
|
|
188
229
|
nervur init --dir ./harbor
|
|
189
|
-
nervur module add ./greetings.js --dir ./harbor
|
|
230
|
+
nervur module add ./greetings.js --dir ./harbor # or @acme/greetings, or its id
|
|
190
231
|
nervur serve --dir ./harbor --port 7000
|
|
191
232
|
nervur reach tcp://harbor.example:7000 --dir ./harbor
|
|
192
233
|
nervur open web org.nervur.web --dir ./harbor
|
|
@@ -209,9 +250,13 @@ named this way is trusted first.
|
|
|
209
250
|
listens for Quo on TCP and for the root's asks on a local socket, open to
|
|
210
251
|
you alone. Every other command is one root ask, sent to the served
|
|
211
252
|
harbor, or answered from the folder when none is served. Each prints one
|
|
212
|
-
JSON line. `--dir` defaults to `$NERVUR_DIR`, then `~/.nervur`.
|
|
213
|
-
|
|
214
|
-
the
|
|
253
|
+
JSON line. `--dir` defaults to `$NERVUR_DIR`, then `~/.nervur`.
|
|
254
|
+
`module add` takes a file, an installed package, or a module's id, and
|
|
255
|
+
the catalogue keeps the id the module declares, so the same harbor finds
|
|
256
|
+
its code on any ground: on this machine through `modules.json` beside
|
|
257
|
+
the harbor, which remembers where each id was found, and on an edge or
|
|
258
|
+
in a browser in the bundle. Routes live in the harbor like everything
|
|
259
|
+
else, so a restart finds both. `nervur
|
|
215
260
|
modules` shows what runs, and `nervur module remove <module>` stops one.
|
|
216
261
|
|
|
217
262
|
A harbor elsewhere is piloted with the owner invitation its `init`
|
package/dist/cli/command.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const parse: (argv: string[]) => {
|
|
|
7
7
|
};
|
|
8
8
|
export declare const requestOf: (command: string, words: string[], flags: Record<string, string>) => RootRequest;
|
|
9
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[];
|
|
10
|
+
export declare const pilotRequests: (name: string, invitation: JsonObject, at?: string, web?: boolean) => RootRequest[];
|
|
11
11
|
export declare const via: (pilot: string, far: RootRequest) => RootRequest;
|
|
12
12
|
export declare const farAnswer: (local: JsonObject) => JsonObject;
|
|
13
13
|
export declare const SERVING = -1;
|
package/dist/cli/command.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// invitation for whoever made it
|
|
8
8
|
// nervur serve [--dir D] [--host H] [--port P] the harbor served: Quo over TCP,
|
|
9
9
|
// and the root line on its socket
|
|
10
|
-
// nervur module add <file>
|
|
10
|
+
// nervur module add <file | package | id> a module the catalogue runs, kept
|
|
11
|
+
// by the id it declares
|
|
11
12
|
// nervur module remove <module> a module it runs no more
|
|
12
13
|
// nervur modules what the catalogue runs
|
|
13
14
|
// nervur census [--ward W] the empty ask
|
|
@@ -18,6 +19,10 @@
|
|
|
18
19
|
// --with-seed the seed goes too
|
|
19
20
|
// nervur import <file> a carried harbor into an empty
|
|
20
21
|
// folder, `-` being this input
|
|
22
|
+
// nervur edge pack <file> the harbor for an edge: its seed
|
|
23
|
+
// printed, for the worker's
|
|
24
|
+
// NERVUR_SEED, and its sealed
|
|
25
|
+
// package written to the file
|
|
21
26
|
// nervur open <key> <kind> a faculty stands in the box ward
|
|
22
27
|
// nervur host <ward> [--seed S] a hosted ward stands
|
|
23
28
|
// nervur route <ward pk> <address... | none> where a ward is reached, in order
|
|
@@ -43,9 +48,11 @@
|
|
|
43
48
|
import { existsSync } from 'node:fs';
|
|
44
49
|
import { readFile } from 'node:fs/promises';
|
|
45
50
|
import { join, resolve } from 'node:path';
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
|
|
51
|
+
import { WEB_OPENING } from '../line/index.js';
|
|
52
|
+
import { harborDir, isFile } from '../node/index.js';
|
|
53
|
+
import { readAt, webAddress } from '../quo/index.js';
|
|
54
|
+
import { carry, isServed, land, open, pack, request, serve } from './harbor.js';
|
|
55
|
+
const USAGE = 'usage: nervur <init|serve|module|modules|census|sweep|open|host|route|reach|boot|unboot|public|invite|ask|own|disown|pilot|export|import|edge> [args] [--dir D] [--ward W] [--via V] [--seed S] [--with-seed] [--host H] [--port P]';
|
|
49
56
|
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
|
|
50
57
|
// The one flag that stands alone, since what it names is a yes.
|
|
51
58
|
const ALONE = 'with-seed';
|
|
@@ -89,7 +96,7 @@ export const requestOf = (command, words, flags) => {
|
|
|
89
96
|
case 'module': {
|
|
90
97
|
const [verb, what] = need(2);
|
|
91
98
|
if (verb === 'add')
|
|
92
|
-
return catalogue('add', { from: resolve(what) });
|
|
99
|
+
return catalogue('add', { from: isFile(what) ? resolve(what) : what });
|
|
93
100
|
if (verb === 'remove')
|
|
94
101
|
return catalogue('remove', { module: what });
|
|
95
102
|
throw new Error('module is add or remove');
|
|
@@ -185,6 +192,15 @@ export const main = async (argv, env, print) => {
|
|
|
185
192
|
const text = words[0] === '-' ? await standingIn() : await readFile(words[0], 'utf8');
|
|
186
193
|
return out({ landed: await land(dir, JSON.parse(text)) });
|
|
187
194
|
}
|
|
195
|
+
if (command === 'edge') {
|
|
196
|
+
if (words[0] !== 'pack' || words.length !== 2)
|
|
197
|
+
throw new Error('edge takes pack and a file');
|
|
198
|
+
if (flags.via !== undefined)
|
|
199
|
+
throw new Error('edge pack reads this folder, so it takes no --via');
|
|
200
|
+
if (await isServed(dir))
|
|
201
|
+
throw new Error(`a harbor is served from ${dir}: stop it first`);
|
|
202
|
+
return out(await pack(dir, words[1]));
|
|
203
|
+
}
|
|
188
204
|
if (command === 'serve') {
|
|
189
205
|
if (words.length > 0)
|
|
190
206
|
throw new Error('serve takes no argument');
|
|
@@ -200,7 +216,9 @@ export const main = async (argv, env, print) => {
|
|
|
200
216
|
throw new Error('pilot takes a name, an invitation, and an address where the invitation carries none');
|
|
201
217
|
const [name, invitation, at] = words;
|
|
202
218
|
const held = JSON.parse(invitation);
|
|
203
|
-
|
|
219
|
+
const census = (await request(dir, {}));
|
|
220
|
+
const web = census.answer?.beings !== undefined && Object.hasOwn(census.answer.beings, WEB_OPENING.key);
|
|
221
|
+
for (const req of pilotRequests(name, held, at, web)) {
|
|
204
222
|
const answer = await request(dir, req);
|
|
205
223
|
if (!failed(answer))
|
|
206
224
|
continue;
|
|
@@ -216,9 +234,12 @@ export const main = async (argv, env, print) => {
|
|
|
216
234
|
throw new Error('--via takes no --ward');
|
|
217
235
|
return out(farAnswer(await request(dir, via(flags.via, requestOf(command, words, flags)))));
|
|
218
236
|
};
|
|
219
|
-
// The root requests that hold a far ward:
|
|
220
|
-
//
|
|
221
|
-
|
|
237
|
+
// The root requests that hold a far ward: the web carrier opened, where the
|
|
238
|
+
// far ward is reached on the web and this harbor has none; its route where
|
|
239
|
+
// one is named, else the take learns it from the invitation's `at`; the
|
|
240
|
+
// pilot; the take.
|
|
241
|
+
export const pilotRequests = (name, invitation, at, web = false) => [
|
|
242
|
+
...(!web && readAt([...(at === undefined ? [] : [at]), ...readAt(invitation.at)]).some((a) => webAddress(a) !== null) ? [{ method: 'open', args: { key: WEB_OPENING.key, class: WEB_OPENING.kind } }] : []),
|
|
222
243
|
...(at === undefined ? [] : [{ method: 'route', args: { ward: invitation.ward ?? null, at: [at] } }]),
|
|
223
244
|
{ method: 'boot', args: { key: name, class: 'org.nervur.pilot' } },
|
|
224
245
|
{ method: 'ask', args: { being: name, method: 'hold', args: { invitation } } },
|
package/dist/cli/harbor.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { JsonObject } from '../being/index.ts';
|
|
2
|
-
import { Harbor, type RootRequest } from '../harbor/index.ts';
|
|
2
|
+
import { Harbor, type Carried, type RootRequest } from '../harbor/index.ts';
|
|
3
3
|
export declare const open: (dir: string) => Promise<Harbor>;
|
|
4
4
|
export declare const isServed: (dir: string) => Promise<boolean>;
|
|
5
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
6
|
export declare const carry: (dir: string, seed: boolean) => Promise<Carried>;
|
|
7
|
+
export declare const pack: (dir: string, file: string) => Promise<JsonObject>;
|
|
11
8
|
export declare const land: (dir: string, value: unknown) => Promise<string[]>;
|
|
12
9
|
export type Served = {
|
|
13
10
|
pk: string;
|
package/dist/cli/harbor.js
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
// the command unpacks the harbor from its folder, asks, and closes it, so
|
|
8
8
|
// one process at a time writes the folder.
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { hex
|
|
12
|
+
import { hex } from '../crypto/index.js';
|
|
13
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';
|
|
14
|
+
import { carryPlaces, Harbor, landPlaces, readCarried, rootLine } from '../harbor/index.js';
|
|
15
|
+
import { askServed, MODULES, NodeTerrain, socketOf } from '../node/index.js';
|
|
16
16
|
import { CryptoEntropy } from '../pointer/index.js';
|
|
17
17
|
import { TCP_OPENING } from '../tcp/index.js';
|
|
18
18
|
// A harbor of a folder, opened for one ask: it listens nowhere and serves
|
|
@@ -32,40 +32,33 @@ export const request = async (dir, req) => {
|
|
|
32
32
|
await harbor.close();
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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.
|
|
35
|
+
// The harbor of a folder, read as bytes alone, its seed only where the
|
|
36
|
+
// one who asks says so, since custody holds it and a package without it
|
|
37
|
+
// opens nowhere. No harbor opens, so nothing is asked and nothing is
|
|
38
|
+
// written.
|
|
54
39
|
export const carry = async (dir, seed) => {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
const places = await carryPlaces(new FolderMemory(dir));
|
|
41
|
+
const modules = await readFile(join(dir, MODULES), 'utf8').then((text) => JSON.parse(text), () => undefined);
|
|
42
|
+
return {
|
|
43
|
+
...(seed ? { seed: hex(await new FolderCustody(dir, new CryptoEntropy()).seed()) } : {}),
|
|
44
|
+
places,
|
|
45
|
+
...(modules ? { modules } : {}),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
// What `nervur edge pack` hands a deploy: the seed, for the worker's
|
|
49
|
+
// `NERVUR_SEED`, and the sealed package without it, for the bundle.
|
|
50
|
+
export const pack = async (dir, file) => {
|
|
51
|
+
if (!existsSync(join(dir, 'seed')))
|
|
52
|
+
throw new Error(`no harbor stands at ${dir}`);
|
|
53
|
+
const { seed, places } = await carry(dir, true);
|
|
54
|
+
await writeFile(file, `${JSON.stringify({ places })}\n`, { mode: 0o600 });
|
|
55
|
+
return { seed: seed, package: file, places: Object.keys(places).length };
|
|
63
56
|
};
|
|
64
57
|
// A carried harbor into an empty folder. A folder holding a seed or a
|
|
65
58
|
// place is left as it stands, and a package with no seed needs one there
|
|
66
59
|
// already, which a terrain that seals its custody writes its own way.
|
|
67
60
|
export const land = async (dir, value) => {
|
|
68
|
-
const held =
|
|
61
|
+
const held = readCarried(value);
|
|
69
62
|
if (held === null)
|
|
70
63
|
throw new Error('that is no carried harbor');
|
|
71
64
|
const memory = new FolderMemory(dir);
|
|
@@ -76,10 +69,9 @@ export const land = async (dir, value) => {
|
|
|
76
69
|
await writeFile(join(dir, 'seed'), `${held.seed}\n`, { mode: 0o600, flag: 'wx' });
|
|
77
70
|
else if (!existsSync(join(dir, 'seed')))
|
|
78
71
|
throw new Error('the package carries no seed, and none is held here');
|
|
79
|
-
|
|
80
|
-
await
|
|
81
|
-
|
|
82
|
-
return Object.keys(held.places);
|
|
72
|
+
if (held.modules && !existsSync(join(dir, MODULES)))
|
|
73
|
+
await writeFile(join(dir, MODULES), `${JSON.stringify(held.modules, null, 2)}\n`, { mode: 0o600 });
|
|
74
|
+
return landPlaces(memory, held);
|
|
83
75
|
};
|
|
84
76
|
// The harbor served: a harbor of the Node ground, with Quo over TCP on
|
|
85
77
|
// host and port.
|
package/dist/cli/main.js
CHANGED
|
File without changes
|
package/dist/edge/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Custody, Memory, type Entropy, type Module } from '../contract/index.ts';
|
|
2
|
-
import { Harbor, type GroundFaculty, type GroundProbe } from '../harbor/index.ts';
|
|
2
|
+
import { Harbor, type Carried, type GroundFaculty, type GroundProbe } from '../harbor/index.ts';
|
|
3
3
|
import { type WebListen } from '../line/index.ts';
|
|
4
4
|
import { PointerTerrain } from '../pointer/index.ts';
|
|
5
|
+
export type { Carried } from '../harbor/index.ts';
|
|
5
6
|
export declare const EDGE = "edge";
|
|
6
7
|
export interface EdgeStorage {
|
|
7
8
|
get(key: string): Promise<unknown>;
|
|
@@ -17,11 +18,13 @@ export interface EdgeState {
|
|
|
17
18
|
blockConcurrencyWhile<T>(work: () => Promise<T>): Promise<T>;
|
|
18
19
|
}
|
|
19
20
|
export type EdgeEnv = {
|
|
21
|
+
readonly NERVUR_SEED?: string;
|
|
20
22
|
readonly NERVUR_SECRET?: string;
|
|
21
23
|
};
|
|
22
24
|
export type EdgeGiven = {
|
|
23
25
|
readonly state: EdgeState;
|
|
24
26
|
readonly env: EdgeEnv;
|
|
27
|
+
readonly package?: Carried;
|
|
25
28
|
};
|
|
26
29
|
export declare class EdgeMemory extends Memory {
|
|
27
30
|
#private;
|
|
@@ -31,9 +34,13 @@ export declare class EdgeMemory extends Memory {
|
|
|
31
34
|
places(): Promise<string[]>;
|
|
32
35
|
forget(place: string): Promise<void>;
|
|
33
36
|
}
|
|
37
|
+
export type EdgeKeys = {
|
|
38
|
+
readonly seed?: string | undefined;
|
|
39
|
+
readonly secret?: string | undefined;
|
|
40
|
+
};
|
|
34
41
|
export declare class EdgeCustody extends Custody {
|
|
35
42
|
#private;
|
|
36
|
-
constructor(storage: EdgeStorage,
|
|
43
|
+
constructor(storage: EdgeStorage, keys: EdgeKeys, entropy?: Entropy, root?: string);
|
|
37
44
|
seed(): Promise<Uint8Array>;
|
|
38
45
|
}
|
|
39
46
|
export declare const edgeTcp: GroundFaculty;
|
|
@@ -46,12 +53,19 @@ export declare class EdgeTerrain extends PointerTerrain {
|
|
|
46
53
|
readonly given: EdgeGiven;
|
|
47
54
|
listening: WebListen | undefined;
|
|
48
55
|
constructor(parts: EdgeParts);
|
|
56
|
+
claim(): Promise<void>;
|
|
49
57
|
stand(harbor: Harbor): Promise<void>;
|
|
50
58
|
answer(request: Request): Promise<Response>;
|
|
51
59
|
}
|
|
52
60
|
export declare const edgeGround: GroundProbe;
|
|
61
|
+
export type HarborObjectParts = {
|
|
62
|
+
readonly modules: readonly Module[];
|
|
63
|
+
readonly package?: Carried | ((env: EdgeEnv) => Carried | undefined);
|
|
64
|
+
readonly path?: string;
|
|
65
|
+
readonly stood?: (harbor: Harbor) => Promise<void>;
|
|
66
|
+
};
|
|
53
67
|
export type HarborObjectClass = new (state: EdgeState, env: EdgeEnv) => {
|
|
54
68
|
readonly harbor: Promise<Harbor>;
|
|
55
69
|
fetch(request: Request): Promise<Response>;
|
|
56
70
|
};
|
|
57
|
-
export declare const harborObject: (modules:
|
|
71
|
+
export declare const harborObject: ({ modules, package: carried, path, stood }: HarborObjectParts) => HarborObjectClass;
|
package/dist/edge/index.js
CHANGED
|
@@ -8,7 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { Custody, Memory } from '../contract/index.js';
|
|
10
10
|
import { decrypt, encrypt, hkdf, isHex, unhex, utf8 } from '../crypto/index.js';
|
|
11
|
-
import { addGround, CarrierFaculty, Harbor, UNDIALED } from '../harbor/index.js';
|
|
11
|
+
import { addGround, CarrierFaculty, Harbor, landPlaces, UNDIALED } from '../harbor/index.js';
|
|
12
12
|
import { answerHeld, answerPost, askFrame, FrameReader, TCP, TCP_OPENING, WEB_OPENING, webFaculty, webPush } from '../line/index.js';
|
|
13
13
|
import { CryptoEntropy, HeldLoader, PointerTerrain } from '../pointer/index.js';
|
|
14
14
|
import { tcpAddress, tcpAt } from '../quo/index.js';
|
|
@@ -66,24 +66,28 @@ export class EdgeMemory extends Memory {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
// The seed, drawn once and kept sealed under a key the secret gives, so
|
|
70
|
-
// the storage alone opens nothing. A harbor with no secret does not stand.
|
|
71
69
|
export class EdgeCustody extends Custody {
|
|
72
70
|
#storage;
|
|
73
|
-
#
|
|
71
|
+
#keys;
|
|
74
72
|
#entropy;
|
|
75
73
|
#key;
|
|
76
|
-
constructor(storage,
|
|
74
|
+
constructor(storage, keys, entropy = new CryptoEntropy(), root = '') {
|
|
77
75
|
super();
|
|
78
76
|
this.#storage = storage;
|
|
79
|
-
this.#
|
|
77
|
+
this.#keys = keys;
|
|
80
78
|
this.#entropy = entropy;
|
|
81
79
|
this.#key = `${root}${SEED}`;
|
|
82
80
|
}
|
|
83
81
|
async seed() {
|
|
84
|
-
|
|
82
|
+
const { seed: named, secret } = this.#keys;
|
|
83
|
+
if (named !== undefined) {
|
|
84
|
+
if (!isHex(named.trim(), 32))
|
|
85
|
+
throw new Error('NERVUR_SEED is not 64 hex');
|
|
86
|
+
return unhex(named.trim());
|
|
87
|
+
}
|
|
88
|
+
if (!secret)
|
|
85
89
|
throw new Error('the worker holds no NERVUR_SECRET');
|
|
86
|
-
const key = await hkdf(utf8(
|
|
90
|
+
const key = await hkdf(utf8(secret), SEAL_INFO, 32);
|
|
87
91
|
return this.#storage.transaction(async (txn) => {
|
|
88
92
|
const kept = (await txn.get(this.#key));
|
|
89
93
|
if (kept) {
|
|
@@ -241,13 +245,25 @@ export class EdgeTerrain extends PointerTerrain {
|
|
|
241
245
|
super({
|
|
242
246
|
entropy,
|
|
243
247
|
loader: new HeldLoader(parts.modules ?? []),
|
|
244
|
-
|
|
248
|
+
// Only a harbor carried here takes the worker's NERVUR_SEED; every
|
|
249
|
+
// other object of the worker draws its own.
|
|
250
|
+
custody: new EdgeCustody(storage, { seed: parts.given.package ? parts.given.env.NERVUR_SEED : undefined, secret: parts.given.env.NERVUR_SECRET }, entropy),
|
|
245
251
|
memory: new EdgeMemory(storage),
|
|
246
252
|
faculties: [edgeTcp, webFaculty(edgeServe), webPush],
|
|
247
253
|
opens: [TCP_OPENING, WEB_OPENING],
|
|
248
254
|
});
|
|
249
255
|
this.given = parts.given;
|
|
250
256
|
}
|
|
257
|
+
// A carried package lands where the object keeps nothing yet, and only
|
|
258
|
+
// under the seed it was sealed with, the worker's `NERVUR_SEED`.
|
|
259
|
+
async claim() {
|
|
260
|
+
const { package: carried, env } = this.given;
|
|
261
|
+
if (!carried || (await this.memory.places()).length > 0)
|
|
262
|
+
return;
|
|
263
|
+
if (env.NERVUR_SEED === undefined)
|
|
264
|
+
throw new Error('a carried package opens only under NERVUR_SEED');
|
|
265
|
+
await landPlaces(this.memory, carried);
|
|
266
|
+
}
|
|
251
267
|
stand(harbor) {
|
|
252
268
|
this.#harbor = harbor;
|
|
253
269
|
return Promise.resolve();
|
|
@@ -283,7 +299,10 @@ export const edgeGround = {
|
|
|
283
299
|
fits: ({ given }) => isGiven(given),
|
|
284
300
|
terrain: ({ given, modules }) => new EdgeTerrain({ given: given, ...(modules ? { modules } : {}) }),
|
|
285
301
|
};
|
|
286
|
-
|
|
302
|
+
// A Durable Object class whose every instance is one harbor: opened once
|
|
303
|
+
// per object, its web carrier opened, and every request it receives
|
|
304
|
+
// answered by that carrier at `path`.
|
|
305
|
+
export const harborObject = ({ modules, package: carried, path = '/quo', stood }) => class HarborObject {
|
|
287
306
|
#opening;
|
|
288
307
|
// The object's harbor, once it stands.
|
|
289
308
|
get harbor() {
|
|
@@ -291,16 +310,25 @@ export const harborObject = (modules, stood) => class HarborObject {
|
|
|
291
310
|
}
|
|
292
311
|
constructor(state, env) {
|
|
293
312
|
addGround(edgeGround);
|
|
313
|
+
const held = typeof carried === 'function' ? carried(env) : carried;
|
|
314
|
+
const given = { state, env, ...(held ? { package: held } : {}) };
|
|
294
315
|
// Opened inside the object's block, so no request reaches it half
|
|
295
316
|
// open and its storage answers the opening.
|
|
296
317
|
this.#opening = state.blockConcurrencyWhile(async () => {
|
|
297
|
-
const harbor = await Harbor.open({ modules, given
|
|
318
|
+
const harbor = await Harbor.open({ modules, given });
|
|
319
|
+
if (!harbor.box.being(WEB_OPENING.key))
|
|
320
|
+
await harbor.root('open', { key: WEB_OPENING.key, class: WEB_OPENING.kind });
|
|
298
321
|
await stood?.(harbor);
|
|
299
322
|
return harbor;
|
|
300
323
|
});
|
|
301
324
|
}
|
|
302
325
|
async fetch(request) {
|
|
303
326
|
const harbor = await this.#opening;
|
|
304
|
-
|
|
327
|
+
const terrain = harbor.terrain;
|
|
328
|
+
if (terrain.listening?.path !== path) {
|
|
329
|
+
const url = new URL(request.url);
|
|
330
|
+
await harbor.root('ask', { being: WEB_OPENING.key, method: 'listen', args: { host: url.hostname, port: url.port === '' ? 0 : Number(url.port), path } });
|
|
331
|
+
}
|
|
332
|
+
return terrain.answer(request);
|
|
305
333
|
}
|
|
306
334
|
};
|
package/dist/harbor/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { Catalogue, CATALOGUE, type Cataloguing, type ModuleRow } from './catalo
|
|
|
2
2
|
export { BOX, Dock, type Failures, type Hosted, type Hosting } from './dock.ts';
|
|
3
3
|
export { CarrierFaculty, UNDIALED, type Dialed, type Ground, type GroundFaculty } from './carrying.ts';
|
|
4
4
|
export { Harbor } from './harbor.ts';
|
|
5
|
-
export { copyPackage, Package } from './package.ts';
|
|
5
|
+
export { carryPlaces, copyPackage, landPlaces, Package, readCarried, type Carried } from './package.ts';
|
|
6
6
|
export { readRootRequest, rootLine, type RootRequest } from './root-line.ts';
|
|
7
7
|
export { addGround, grounds, probeGround, type GroundProbe, type Probing } from './probe.ts';
|
|
8
8
|
export { Registry } from './registry.ts';
|
package/dist/harbor/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { Catalogue, CATALOGUE } from './catalogue.js';
|
|
|
5
5
|
export { BOX, Dock } from './dock.js';
|
|
6
6
|
export { CarrierFaculty, UNDIALED } from './carrying.js';
|
|
7
7
|
export { Harbor } from './harbor.js';
|
|
8
|
-
export { copyPackage, Package } from './package.js';
|
|
8
|
+
export { carryPlaces, copyPackage, landPlaces, Package, readCarried } from './package.js';
|
|
9
9
|
export { readRootRequest, rootLine } from './root-line.js';
|
|
10
10
|
export { addGround, grounds, probeGround } from './probe.js';
|
|
11
11
|
export { Registry } from './registry.js';
|
package/dist/harbor/package.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { Entropy, Memory, Rows } from '../contract/index.ts';
|
|
2
2
|
export declare const copyPackage: (from: Memory, to: Memory) => Promise<string[]>;
|
|
3
|
+
export type Carried = {
|
|
4
|
+
seed?: string;
|
|
5
|
+
places: Record<string, Record<string, string>>;
|
|
6
|
+
modules?: Record<string, string>;
|
|
7
|
+
};
|
|
8
|
+
export declare const readCarried: (value: unknown) => Carried | null;
|
|
9
|
+
export declare const carryPlaces: (memory: Memory) => Promise<Carried['places']>;
|
|
10
|
+
export declare const landPlaces: (memory: Memory, carried: Carried) => Promise<string[]>;
|
|
3
11
|
export declare class Package {
|
|
4
12
|
#private;
|
|
5
13
|
readonly memory: Memory;
|
package/dist/harbor/package.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// A row is the head, under the empty name, or one being, under her key. An
|
|
14
14
|
// entry that does not open refuses the whole ward, so nothing unpacks from
|
|
15
15
|
// bytes the package did not seal.
|
|
16
|
-
import { concat, decrypt, encrypt, hex, hkdf, utf8 } from '../crypto/index.js';
|
|
16
|
+
import { concat, decrypt, encrypt, hex, hkdf, isHex, unhex, utf8 } from '../crypto/index.js';
|
|
17
17
|
import { HEAD_ROW } from '../ward/index.js';
|
|
18
18
|
// Every place of one memory, written into another, and the places
|
|
19
19
|
// carried. The bytes are the package's own, sealed under a seed neither
|
|
@@ -26,6 +26,42 @@ export const copyPackage = async (from, to) => {
|
|
|
26
26
|
await to.write(place, await from.read(place));
|
|
27
27
|
return places;
|
|
28
28
|
};
|
|
29
|
+
// A carried harbor as JSON reads, or null for anything else.
|
|
30
|
+
export const readCarried = (value) => {
|
|
31
|
+
if (typeof value !== 'object' || value === null)
|
|
32
|
+
return null;
|
|
33
|
+
const { seed, places, modules } = value;
|
|
34
|
+
if (seed !== undefined && !isHex(seed, 32))
|
|
35
|
+
return null;
|
|
36
|
+
const hints = modules === undefined || (typeof modules === 'object' && modules !== null && !Array.isArray(modules) && Object.values(modules).every((m) => typeof m === 'string'));
|
|
37
|
+
if (!hints)
|
|
38
|
+
return null;
|
|
39
|
+
if (typeof places !== 'object' || places === null || Array.isArray(places))
|
|
40
|
+
return null;
|
|
41
|
+
for (const entries of Object.values(places)) {
|
|
42
|
+
if (typeof entries !== 'object' || entries === null || Array.isArray(entries))
|
|
43
|
+
return null;
|
|
44
|
+
for (const bytes of Object.values(entries))
|
|
45
|
+
if (!isHex(bytes))
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return { ...(seed === undefined ? {} : { seed }), places: places, ...(modules === undefined ? {} : { modules: modules }) };
|
|
49
|
+
};
|
|
50
|
+
// Every place of a memory, as a carried harbor holds it.
|
|
51
|
+
export const carryPlaces = async (memory) => {
|
|
52
|
+
const places = {};
|
|
53
|
+
for (const place of await memory.places()) {
|
|
54
|
+
places[place] = Object.fromEntries([...(await memory.read(place))].map(([name, bytes]) => [name, hex(bytes)]));
|
|
55
|
+
}
|
|
56
|
+
return places;
|
|
57
|
+
};
|
|
58
|
+
// A carried harbor's places, written into a memory, and their names.
|
|
59
|
+
export const landPlaces = async (memory, carried) => {
|
|
60
|
+
for (const [place, entries] of Object.entries(carried.places)) {
|
|
61
|
+
await memory.write(place, new Map(Object.entries(entries).map(([name, bytes]) => [name, unhex(bytes)])));
|
|
62
|
+
}
|
|
63
|
+
return Object.keys(carried.places);
|
|
64
|
+
};
|
|
29
65
|
const NONCE = 12;
|
|
30
66
|
const LABEL = utf8('nervur-package');
|
|
31
67
|
class PackageRows {
|
package/dist/harbor/relay.js
CHANGED
|
@@ -174,11 +174,22 @@ export const relayFaculty = (clock) => class Relay extends CarrierFaculty {
|
|
|
174
174
|
return { error: 'replies are a list of { n, box }' };
|
|
175
175
|
const id = asker.id;
|
|
176
176
|
this.#rung.delete(id);
|
|
177
|
+
const replied = new Set(replies.map((r) => r.n));
|
|
177
178
|
for (const { n, box } of replies) {
|
|
178
179
|
const taken = this.#taken.get(n);
|
|
179
180
|
if (taken?.collector === id)
|
|
180
181
|
this.#settle(taken, box === null ? null : unhex(box));
|
|
181
182
|
}
|
|
183
|
+
// A client answers every box it took in its next collect, so a box it
|
|
184
|
+
// left out never reached its doors, and waits again. A door that
|
|
185
|
+
// hears it twice answers the second as repeated.
|
|
186
|
+
for (const taken of [...this.#taken.values()]) {
|
|
187
|
+
if (taken.collector !== id || replied.has(taken.n))
|
|
188
|
+
continue;
|
|
189
|
+
this.#taken.delete(taken.n);
|
|
190
|
+
delete taken.collector;
|
|
191
|
+
this.#waiting.unshift(taken);
|
|
192
|
+
}
|
|
182
193
|
const wards = wardsOf(this.notes(asker));
|
|
183
194
|
if (args.wait === true && !this.#waiting.some((b) => wards.includes(b.ward)))
|
|
184
195
|
await this.#hold(wards);
|
|
@@ -259,6 +270,7 @@ export const relayClient = (ground, clock) => class RelayClient extends CarrierF
|
|
|
259
270
|
};
|
|
260
271
|
#awake = false;
|
|
261
272
|
#replies = [];
|
|
273
|
+
#turn = Promise.resolve();
|
|
262
274
|
// Ends every rest and schedule when the client sleeps or lets go.
|
|
263
275
|
#stop = () => { };
|
|
264
276
|
accepts() {
|
|
@@ -367,7 +379,14 @@ export const relayClient = (ground, clock) => class RelayClient extends CarrierF
|
|
|
367
379
|
// One collect: the replies owed go with it, and each box it brings is
|
|
368
380
|
// handed to this harbor's doors. The number of boxes, or null where the
|
|
369
381
|
// relay was not reached.
|
|
370
|
-
|
|
382
|
+
// Collects run one after another, so each carries the replies to every
|
|
383
|
+
// box the one before it brought, as the relay counts on.
|
|
384
|
+
#collect(wait) {
|
|
385
|
+
const run = this.#turn.then(() => this.#collectNow(wait));
|
|
386
|
+
this.#turn = run.catch(() => undefined);
|
|
387
|
+
return run;
|
|
388
|
+
}
|
|
389
|
+
async #collectNow(wait) {
|
|
371
390
|
const standing = this.stance.standings.get(CLIENT);
|
|
372
391
|
if (!standing)
|
|
373
392
|
return null;
|
package/dist/node/index.d.ts
CHANGED
|
@@ -5,14 +5,18 @@ import { PointerTerrain } from '../pointer/index.ts';
|
|
|
5
5
|
export declare const NODE = "node";
|
|
6
6
|
export declare const harborDir: (env?: Record<string, string | undefined>) => string;
|
|
7
7
|
export declare const socketOf: (dir: string) => string;
|
|
8
|
+
export declare const isFile: (from: string) => boolean;
|
|
8
9
|
export declare class ImportLoader extends Loader {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(bases?: readonly string[]);
|
|
9
12
|
load(from: string): Promise<Loaded | null>;
|
|
10
13
|
}
|
|
11
14
|
export declare class NodeLoader extends ImportLoader {
|
|
12
15
|
#private;
|
|
13
|
-
constructor(modules?: readonly Module[]);
|
|
16
|
+
constructor(modules?: readonly Module[], dir?: string);
|
|
14
17
|
load(from: string): Promise<Loaded | null>;
|
|
15
18
|
}
|
|
19
|
+
export declare const MODULES = "modules.json";
|
|
16
20
|
export declare const askServed: (dir: string, request: RootRequest) => Promise<JsonObject | undefined>;
|
|
17
21
|
export type NodeParts = {
|
|
18
22
|
readonly dir: string;
|