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.
- package/README.md +229 -20
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +5 -1
- package/dist/being/faculty.d.ts +1 -0
- package/dist/being/faculty.js +13 -7
- package/dist/being/index.d.ts +1 -0
- package/dist/being/index.js +1 -0
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +3 -4
- package/dist/browser/index.d.ts +35 -0
- package/dist/browser/index.js +188 -0
- package/dist/browser/worker.d.ts +12 -0
- package/dist/browser/worker.js +40 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +7 -0
- package/dist/cli/command.d.ts +13 -0
- package/dist/cli/command.js +237 -0
- package/dist/cli/harbor.d.ts +19 -0
- package/dist/cli/harbor.js +99 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +16 -2
- package/dist/contract/index.js +13 -3
- package/dist/edge/index.d.ts +57 -0
- package/dist/edge/index.js +306 -0
- package/dist/edge.d.ts +1 -0
- package/dist/edge.js +8 -0
- package/dist/folder/index.d.ts +9 -4
- package/dist/folder/index.js +89 -84
- package/dist/harbor/carrying.d.ts +17 -0
- package/dist/harbor/carrying.js +38 -0
- package/dist/harbor/catalogue.d.ts +27 -0
- package/dist/harbor/catalogue.js +101 -0
- package/dist/harbor/dock.d.ts +32 -3
- package/dist/harbor/dock.js +98 -12
- package/dist/harbor/harbor.d.ts +27 -8
- package/dist/harbor/harbor.js +217 -26
- package/dist/harbor/index.d.ts +8 -2
- package/dist/harbor/index.js +8 -1
- package/dist/harbor/package.d.ts +12 -0
- package/dist/harbor/package.js +130 -0
- package/dist/harbor/probe.d.ts +18 -0
- package/dist/harbor/probe.js +15 -0
- package/dist/harbor/registry.d.ts +6 -3
- package/dist/harbor/registry.js +83 -7
- package/dist/harbor/relay.d.ts +15 -0
- package/dist/harbor/relay.js +387 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +38 -0
- package/dist/harbor/terrain.d.ts +14 -1
- package/dist/harbor/terrain.js +19 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +156 -0
- package/dist/http/websocket.d.ts +29 -0
- package/dist/http/websocket.js +133 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +12 -6
- package/dist/line/answer.d.ts +8 -0
- package/dist/line/answer.js +60 -0
- package/dist/line/frame.d.ts +34 -0
- package/dist/line/frame.js +81 -0
- package/dist/line/ground.d.ts +3 -0
- package/dist/line/ground.js +9 -0
- package/dist/line/index.d.ts +5 -0
- package/dist/line/index.js +9 -0
- package/dist/line/push.d.ts +7 -0
- package/dist/line/push.js +92 -0
- package/dist/line/web.d.ts +30 -0
- package/dist/line/web.js +181 -0
- package/dist/node/index.d.ts +32 -0
- package/dist/node/index.js +164 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +7 -0
- package/dist/pointer/bodies.d.ts +11 -3
- package/dist/pointer/bodies.js +47 -18
- package/dist/pointer/index.d.ts +1 -1
- package/dist/pointer/index.js +1 -1
- package/dist/pointer/world.d.ts +12 -5
- package/dist/pointer/world.js +29 -14
- package/dist/quo/address.d.ts +19 -0
- package/dist/quo/address.js +84 -0
- package/dist/quo/door.d.ts +1 -1
- package/dist/quo/door.js +6 -4
- package/dist/quo/index.d.ts +1 -0
- package/dist/quo/index.js +1 -0
- package/dist/quo/invitation.d.ts +1 -0
- package/dist/quo/invitation.js +9 -5
- package/dist/quo/keys.js +1 -1
- package/dist/tcp/frame.d.ts +25 -0
- package/dist/tcp/frame.js +64 -0
- package/dist/tcp/index.d.ts +24 -0
- package/dist/tcp/index.js +260 -0
- package/dist/ward/index.d.ts +3 -2
- package/dist/ward/index.js +2 -1
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +4 -4
- package/dist/ward/stance.js +10 -7
- package/dist/ward/ward-being.d.ts +4 -1
- package/dist/ward/ward-being.js +8 -2
- package/dist/ward/ward.d.ts +11 -2
- package/dist/ward/ward.js +41 -5
- package/package.json +28 -2
- package/src/being/being.ts +5 -1
- package/src/being/faculty.ts +14 -7
- package/src/being/index.ts +1 -0
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +8 -6
- package/src/browser/index.ts +207 -0
- package/src/browser/worker.ts +66 -0
- package/src/browser.ts +9 -0
- package/src/cli/command.ts +224 -0
- package/src/cli/harbor.ts +103 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +42 -10
- package/src/edge/index.ts +359 -0
- package/src/edge.ts +10 -0
- package/src/folder/index.ts +87 -84
- package/src/harbor/carrying.ts +56 -0
- package/src/harbor/catalogue.ts +125 -0
- package/src/harbor/dock.ts +122 -15
- package/src/harbor/harbor.ts +240 -36
- package/src/harbor/index.ts +10 -3
- package/src/harbor/package.ts +147 -0
- package/src/harbor/probe.ts +34 -0
- package/src/harbor/registry.ts +73 -9
- package/src/harbor/relay.ts +399 -0
- package/src/harbor/root-line.ts +39 -0
- package/src/harbor/terrain.ts +31 -3
- package/src/http/index.ts +150 -0
- package/src/http/websocket.ts +124 -0
- package/src/index.ts +39 -6
- package/src/line/answer.ts +59 -0
- package/src/line/frame.ts +88 -0
- package/src/line/ground.ts +19 -0
- package/src/line/index.ts +9 -0
- package/src/line/push.ts +112 -0
- package/src/line/web.ts +227 -0
- package/src/node/index.ts +170 -0
- package/src/node.ts +9 -0
- package/src/pointer/bodies.ts +45 -16
- package/src/pointer/index.ts +1 -1
- package/src/pointer/world.ts +38 -19
- package/src/quo/address.ts +87 -0
- package/src/quo/door.ts +6 -4
- package/src/quo/index.ts +1 -0
- package/src/quo/invitation.ts +10 -6
- package/src/quo/keys.ts +1 -1
- package/src/stand/main.ts +1 -0
- package/src/stand/stand.ts +87 -6
- package/src/tcp/index.ts +284 -0
- package/src/ward/index.ts +3 -2
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +14 -8
- package/src/ward/ward-being.ts +9 -3
- package/src/ward/ward.ts +53 -6
package/dist/line/web.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { concat, hex, isHex, unhex } from '../crypto/index.js';
|
|
2
|
+
import { CarrierFaculty, UNDIALED } from '../harbor/index.js';
|
|
3
|
+
import { webAddress } from '../quo/index.js';
|
|
4
|
+
import { ownerAsk } from '../ward/index.js';
|
|
5
|
+
import { askBody, readBody } from './frame.js';
|
|
6
|
+
export const MIN_POST = 65;
|
|
7
|
+
export const MAX_POST = 64 + 1_048_576;
|
|
8
|
+
const MAX_ID = 0xffff_ffff;
|
|
9
|
+
// Bytes on a buffer of their own, as a body and a message take them.
|
|
10
|
+
const own = (bytes) => new Uint8Array(bytes);
|
|
11
|
+
// One held line to one address.
|
|
12
|
+
class HeldLine {
|
|
13
|
+
#socket;
|
|
14
|
+
#waiting = new Map();
|
|
15
|
+
#next = 1;
|
|
16
|
+
constructor(socket) {
|
|
17
|
+
this.#socket = socket;
|
|
18
|
+
socket.binaryType = 'arraybuffer';
|
|
19
|
+
socket.addEventListener('message', (e) => {
|
|
20
|
+
const frame = typeof e.data === 'string' ? null : readBody(new Uint8Array(e.data));
|
|
21
|
+
if (frame === null)
|
|
22
|
+
return this.close();
|
|
23
|
+
if (frame.kind === 'ask')
|
|
24
|
+
return;
|
|
25
|
+
const done = this.#waiting.get(frame.id);
|
|
26
|
+
this.#waiting.delete(frame.id);
|
|
27
|
+
done?.(frame.kind === 'reply' ? frame.box : null);
|
|
28
|
+
});
|
|
29
|
+
socket.addEventListener('close', () => {
|
|
30
|
+
for (const done of this.#waiting.values())
|
|
31
|
+
done(null);
|
|
32
|
+
this.#waiting.clear();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// A line that opened with the subprotocol `quo`, or null.
|
|
36
|
+
static open(href, onClose) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
let socket;
|
|
39
|
+
try {
|
|
40
|
+
socket = new WebSocket(href, 'quo');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
resolve(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const line = new HeldLine(socket);
|
|
47
|
+
socket.addEventListener('open', () => (socket.protocol === 'quo' ? resolve(line) : (socket.close(), resolve(null))));
|
|
48
|
+
socket.addEventListener('close', () => {
|
|
49
|
+
onClose();
|
|
50
|
+
resolve(null);
|
|
51
|
+
});
|
|
52
|
+
socket.addEventListener('error', () => resolve(null));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
ask(pk, box) {
|
|
56
|
+
if (this.#socket.readyState !== WebSocket.OPEN)
|
|
57
|
+
return Promise.resolve(null);
|
|
58
|
+
const id = this.#next;
|
|
59
|
+
this.#next = this.#next === MAX_ID ? 1 : this.#next + 1;
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
this.#waiting.set(id, resolve);
|
|
62
|
+
this.#socket.send(own(askBody(id, pk, box)));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
close() {
|
|
66
|
+
this.#socket.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export class WebDialer {
|
|
70
|
+
#lines = new Map();
|
|
71
|
+
dial(at, pk, bytes) {
|
|
72
|
+
const address = webAddress(at);
|
|
73
|
+
if (address === null || !isHex(pk, 64))
|
|
74
|
+
return Promise.resolve(UNDIALED);
|
|
75
|
+
return address.held ? this.#held(address, pk, bytes) : this.#post(address, pk, bytes);
|
|
76
|
+
}
|
|
77
|
+
async #post(address, pk, bytes) {
|
|
78
|
+
let response;
|
|
79
|
+
try {
|
|
80
|
+
response = await fetch(address.href, { method: 'POST', body: own(concat(unhex(pk), bytes)) });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return UNDIALED;
|
|
84
|
+
}
|
|
85
|
+
const body = new Uint8Array(await response.arrayBuffer().catch(() => new ArrayBuffer(0)));
|
|
86
|
+
return response.status === 200 && body.length > 0 ? body : null;
|
|
87
|
+
}
|
|
88
|
+
async #held(address, pk, bytes) {
|
|
89
|
+
let line = this.#lines.get(address.href);
|
|
90
|
+
if (!line) {
|
|
91
|
+
line = HeldLine.open(address.href, () => this.#lines.delete(address.href));
|
|
92
|
+
this.#lines.set(address.href, line);
|
|
93
|
+
void line.then((held) => held ?? this.#lines.delete(address.href));
|
|
94
|
+
}
|
|
95
|
+
const held = await line;
|
|
96
|
+
return held ? held.ask(unhex(pk), bytes) : UNDIALED;
|
|
97
|
+
}
|
|
98
|
+
async close() {
|
|
99
|
+
const lines = [...this.#lines.values()];
|
|
100
|
+
this.#lines.clear();
|
|
101
|
+
for (const line of await Promise.all(lines))
|
|
102
|
+
line?.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export const WEB = 'org.nervur.web';
|
|
106
|
+
export const WEB_OPENING = { key: 'web', kind: WEB };
|
|
107
|
+
const listenOf = (value) => {
|
|
108
|
+
if (typeof value !== 'object' || value === null)
|
|
109
|
+
return null;
|
|
110
|
+
const { host, port, path, origins } = value;
|
|
111
|
+
const fine = typeof host === 'string' &&
|
|
112
|
+
Number.isInteger(port) &&
|
|
113
|
+
port >= 0 &&
|
|
114
|
+
port <= 65_535 &&
|
|
115
|
+
typeof path === 'string' &&
|
|
116
|
+
path.startsWith('/') &&
|
|
117
|
+
Array.isArray(origins) &&
|
|
118
|
+
origins.every((o) => typeof o === 'string');
|
|
119
|
+
return fine ? { host, port: port, path, origins: origins } : null;
|
|
120
|
+
};
|
|
121
|
+
// Quo over the web as a carrier faculty of one harbor, listening through
|
|
122
|
+
// `serve` where the ground has one:
|
|
123
|
+
//
|
|
124
|
+
// listen { host?, port?, path?, origins? }
|
|
125
|
+
// listen there from now on, 127.0.0.1, a free port, `/`
|
|
126
|
+
// and every origin unless named, and on every wake at the
|
|
127
|
+
// port it bound
|
|
128
|
+
// deaf listen nowhere from now on
|
|
129
|
+
export const webFaculty = (serve) => (ground) => class WebFaculty extends CarrierFaculty {
|
|
130
|
+
static kind = WEB;
|
|
131
|
+
static asks = {
|
|
132
|
+
listen: ownerAsk('listen for Quo over the web, and on every wake'),
|
|
133
|
+
deaf: ownerAsk('listen nowhere'),
|
|
134
|
+
};
|
|
135
|
+
#dialer = new WebDialer();
|
|
136
|
+
#listener;
|
|
137
|
+
accepts(address) {
|
|
138
|
+
return webAddress(address) !== null;
|
|
139
|
+
}
|
|
140
|
+
dial(address, pk, bytes) {
|
|
141
|
+
return this.#dialer.dial(address, pk, bytes);
|
|
142
|
+
}
|
|
143
|
+
listening() {
|
|
144
|
+
return this.#listener ? [...this.#listener.at] : [];
|
|
145
|
+
}
|
|
146
|
+
async awake() {
|
|
147
|
+
const at = listenOf(this.cells.listen);
|
|
148
|
+
if (at && serve && !this.#listener)
|
|
149
|
+
this.#listener = await serve(ground, at);
|
|
150
|
+
}
|
|
151
|
+
async sleep() {
|
|
152
|
+
await this.#dialer.close();
|
|
153
|
+
await this.#listener?.close();
|
|
154
|
+
this.#listener = undefined;
|
|
155
|
+
}
|
|
156
|
+
async listen(args) {
|
|
157
|
+
if (!serve)
|
|
158
|
+
return { error: 'this ground listens nowhere on the web' };
|
|
159
|
+
const at = listenOf({ host: args.host ?? '127.0.0.1', port: args.port ?? 0, path: args.path ?? '/', origins: args.origins ?? ['*'] });
|
|
160
|
+
if (at === null)
|
|
161
|
+
return { error: 'host is text, port a port, path a path and origins a list' };
|
|
162
|
+
await this.#listener?.close();
|
|
163
|
+
this.#listener = undefined;
|
|
164
|
+
try {
|
|
165
|
+
this.#listener = await serve(ground, at);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
return { error: e.message };
|
|
169
|
+
}
|
|
170
|
+
this.cells.listen = { ...at, port: this.#listener.port };
|
|
171
|
+
return { at: [...this.#listener.at] };
|
|
172
|
+
}
|
|
173
|
+
async deaf() {
|
|
174
|
+
await this.#listener?.close();
|
|
175
|
+
this.#listener = undefined;
|
|
176
|
+
delete this.cells.listen;
|
|
177
|
+
return { deaf: true };
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
// The ward pk and box a post carries, or null for a body that is no ask.
|
|
181
|
+
export const readPost = (body) => body.length < MIN_POST || body.length > MAX_POST ? null : { pk: hex(body.subarray(0, 64)), box: body.slice(64) };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { JsonObject } from '../being/index.ts';
|
|
2
|
+
import { Loader, type Loaded, type Module } from '../contract/index.ts';
|
|
3
|
+
import { type GroundProbe, type Harbor, type RootRequest } from '../harbor/index.ts';
|
|
4
|
+
import { PointerTerrain } from '../pointer/index.ts';
|
|
5
|
+
export declare const NODE = "node";
|
|
6
|
+
export declare const harborDir: (env?: Record<string, string | undefined>) => string;
|
|
7
|
+
export declare const socketOf: (dir: string) => string;
|
|
8
|
+
export declare class ImportLoader extends Loader {
|
|
9
|
+
load(from: string): Promise<Loaded | null>;
|
|
10
|
+
}
|
|
11
|
+
export declare class NodeLoader extends ImportLoader {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(modules?: readonly Module[]);
|
|
14
|
+
load(from: string): Promise<Loaded | null>;
|
|
15
|
+
}
|
|
16
|
+
export declare const askServed: (dir: string, request: RootRequest) => Promise<JsonObject | undefined>;
|
|
17
|
+
export type NodeParts = {
|
|
18
|
+
readonly dir: string;
|
|
19
|
+
readonly modules?: readonly Module[];
|
|
20
|
+
readonly wakes?: boolean;
|
|
21
|
+
readonly serves?: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare class NodeTerrain extends PointerTerrain {
|
|
24
|
+
#private;
|
|
25
|
+
readonly dir: string;
|
|
26
|
+
readonly serves: boolean;
|
|
27
|
+
constructor(parts: NodeParts);
|
|
28
|
+
claim(): Promise<void>;
|
|
29
|
+
stand(harbor: Harbor): Promise<void>;
|
|
30
|
+
release(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export declare const nodeGround: GroundProbe;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
10
|
+
// `nervur/node`: the Node ground. A harbor keeps its seed and sealed
|
|
11
|
+
// memory in a folder, finds its code among the modules it is handed and
|
|
12
|
+
// then in files, speaks TCP from genesis and the web and Web Push where
|
|
13
|
+
// the root opens them, and serves the root line on a local socket, so the
|
|
14
|
+
// `nervur` command reaches it while it stands.
|
|
15
|
+
//
|
|
16
|
+
// <dir>/seed the harbor's seed
|
|
17
|
+
// <dir>/<place>/ its sealed memory
|
|
18
|
+
// <dir>/root.sock the root line while the harbor is served, open to
|
|
19
|
+
// this user alone, or in the temporary folder for a
|
|
20
|
+
// folder too deep for a socket path
|
|
21
|
+
//
|
|
22
|
+
// One run holds a folder: a terrain refuses a folder another run serves.
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import { chmod, rm } from 'node:fs/promises';
|
|
25
|
+
import { connect, createServer } from 'node:net';
|
|
26
|
+
import { homedir, tmpdir } from 'node:os';
|
|
27
|
+
import { join, resolve } from 'node:path';
|
|
28
|
+
import { pathToFileURL } from 'node:url';
|
|
29
|
+
import { Loader } from '../contract/index.js';
|
|
30
|
+
import { FolderCustody, FolderMemory } from '../folder/index.js';
|
|
31
|
+
import { rootLine } from '../harbor/index.js';
|
|
32
|
+
import { webGround } from '../http/index.js';
|
|
33
|
+
import { webPush } from '../line/index.js';
|
|
34
|
+
import { CryptoEntropy, HeldLoader, PointerTerrain } from '../pointer/index.js';
|
|
35
|
+
import { TCP_OPENING, tcpFaculty } from '../tcp/index.js';
|
|
36
|
+
export const NODE = 'node';
|
|
37
|
+
// Where a harbor of this user lives unless named: $NERVUR_DIR, then
|
|
38
|
+
// ~/.nervur.
|
|
39
|
+
export const harborDir = (env = process.env) => env.NERVUR_DIR ?? join(homedir(), '.nervur');
|
|
40
|
+
// A socket path has a hundred bytes or so on every system, so a folder too
|
|
41
|
+
// deep for one puts its socket in this user's temporary folder, under a
|
|
42
|
+
// name drawn from the folder's own path.
|
|
43
|
+
const SOCKET_PATH = 100;
|
|
44
|
+
export const socketOf = (dir) => {
|
|
45
|
+
const beside = join(resolve(dir), 'root.sock');
|
|
46
|
+
if (Buffer.byteLength(beside) <= SOCKET_PATH)
|
|
47
|
+
return beside;
|
|
48
|
+
return join(tmpdir(), `nervur-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 32)}.sock`);
|
|
49
|
+
};
|
|
50
|
+
// Code from a file: a module that exports `module`, `version` and
|
|
51
|
+
// `classes`, found again by its absolute path. A file that cannot be
|
|
52
|
+
// imported, or exports no module, is none.
|
|
53
|
+
export class ImportLoader extends Loader {
|
|
54
|
+
async load(from) {
|
|
55
|
+
const path = resolve(from);
|
|
56
|
+
let exported;
|
|
57
|
+
try {
|
|
58
|
+
exported = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const { module, version, classes } = exported;
|
|
64
|
+
if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes))
|
|
65
|
+
return null;
|
|
66
|
+
return { module, version, classes, from: path };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// The modules a harbor is handed, by name, and then files.
|
|
70
|
+
export class NodeLoader extends ImportLoader {
|
|
71
|
+
#held;
|
|
72
|
+
constructor(modules = []) {
|
|
73
|
+
super();
|
|
74
|
+
this.#held = new HeldLoader(modules);
|
|
75
|
+
}
|
|
76
|
+
async load(from) {
|
|
77
|
+
return (await this.#held.load(from)) ?? super.load(from);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// One root request over a served harbor's socket, or undefined where none
|
|
81
|
+
// is served.
|
|
82
|
+
export const askServed = (dir, request) => new Promise((done, fail) => {
|
|
83
|
+
const socket = connect(socketOf(dir));
|
|
84
|
+
let text = '';
|
|
85
|
+
socket.once('connect', () => socket.write(`${JSON.stringify(request)}\n`));
|
|
86
|
+
socket.on('data', (chunk) => {
|
|
87
|
+
text += chunk.toString('utf8');
|
|
88
|
+
const end = text.indexOf('\n');
|
|
89
|
+
if (end < 0)
|
|
90
|
+
return;
|
|
91
|
+
socket.end();
|
|
92
|
+
done(JSON.parse(text.slice(0, end)));
|
|
93
|
+
});
|
|
94
|
+
socket.once('error', (e) => (e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? done(undefined) : fail(e)));
|
|
95
|
+
});
|
|
96
|
+
export class NodeTerrain extends PointerTerrain {
|
|
97
|
+
dir;
|
|
98
|
+
serves;
|
|
99
|
+
#server;
|
|
100
|
+
constructor(parts) {
|
|
101
|
+
const entropy = new CryptoEntropy();
|
|
102
|
+
super({
|
|
103
|
+
entropy,
|
|
104
|
+
loader: new NodeLoader(parts.modules),
|
|
105
|
+
custody: new FolderCustody(parts.dir, entropy),
|
|
106
|
+
memory: new FolderMemory(parts.dir),
|
|
107
|
+
faculties: [tcpFaculty, webGround, webPush],
|
|
108
|
+
opens: [TCP_OPENING],
|
|
109
|
+
wakes: parts.wakes ?? true,
|
|
110
|
+
});
|
|
111
|
+
this.dir = parts.dir;
|
|
112
|
+
this.serves = parts.serves ?? true;
|
|
113
|
+
}
|
|
114
|
+
async claim() {
|
|
115
|
+
if ((await askServed(this.dir, {})) !== undefined)
|
|
116
|
+
throw new Error(`a harbor is already served from ${this.dir}`);
|
|
117
|
+
}
|
|
118
|
+
// The root line on the socket. A socket file nobody answers on is a
|
|
119
|
+
// stale one, and is replaced.
|
|
120
|
+
async stand(harbor) {
|
|
121
|
+
if (!this.serves)
|
|
122
|
+
return;
|
|
123
|
+
const root = socketOf(this.dir);
|
|
124
|
+
await rm(root, { force: true });
|
|
125
|
+
const server = createServer((socket) => {
|
|
126
|
+
let text = '';
|
|
127
|
+
socket.on('data', (chunk) => {
|
|
128
|
+
text += chunk.toString('utf8');
|
|
129
|
+
for (let end = text.indexOf('\n'); end >= 0; end = text.indexOf('\n')) {
|
|
130
|
+
const line = text.slice(0, end);
|
|
131
|
+
text = text.slice(end + 1);
|
|
132
|
+
let value;
|
|
133
|
+
try {
|
|
134
|
+
value = JSON.parse(line);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
value = undefined;
|
|
138
|
+
}
|
|
139
|
+
void rootLine(harbor, value)
|
|
140
|
+
.catch(() => ({ error: 'the harbor threw' }))
|
|
141
|
+
.then((answer) => socket.write(`${JSON.stringify(answer)}\n`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
socket.on('error', () => socket.destroy());
|
|
145
|
+
});
|
|
146
|
+
await new Promise((done) => server.listen(root, done));
|
|
147
|
+
await chmod(root, 0o600);
|
|
148
|
+
this.#server = server;
|
|
149
|
+
}
|
|
150
|
+
async release() {
|
|
151
|
+
const server = this.#server;
|
|
152
|
+
if (!server)
|
|
153
|
+
return;
|
|
154
|
+
this.#server = undefined;
|
|
155
|
+
await new Promise((done) => server.close(done));
|
|
156
|
+
await rm(socketOf(this.dir), { force: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Node, and the engines that answer as Node does.
|
|
160
|
+
export const nodeGround = {
|
|
161
|
+
name: NODE,
|
|
162
|
+
fits: () => typeof process !== 'undefined' && typeof process.versions?.node === 'string',
|
|
163
|
+
terrain: ({ modules, where }) => new NodeTerrain({ dir: where ?? harborDir(), ...(modules ? { modules } : {}) }),
|
|
164
|
+
};
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.ts';
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// `nervur` as Node reads it: the main entry, with the Node ground added, so
|
|
3
|
+
// `Harbor.open()` keeps a harbor in a folder and serves its root line.
|
|
4
|
+
import { addGround } from './harbor/index.js';
|
|
5
|
+
import { nodeGround } from './node/index.js';
|
|
6
|
+
export * from './index.js';
|
|
7
|
+
addGround(nodeGround);
|
package/dist/pointer/bodies.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Clock, Custody, Entropy, Memory, type
|
|
1
|
+
import { Clock, Custody, Entropy, Loader, Memory, type Loaded, type Module } from '../contract/index.ts';
|
|
2
2
|
export declare class CryptoEntropy extends Entropy {
|
|
3
3
|
draw(length: number): Uint8Array;
|
|
4
4
|
}
|
|
@@ -12,9 +12,17 @@ export declare class SystemClock extends Clock {
|
|
|
12
12
|
export declare class HeldCustody extends Custody {
|
|
13
13
|
#private;
|
|
14
14
|
constructor(entropy: Entropy);
|
|
15
|
-
seed(
|
|
15
|
+
seed(): Promise<Uint8Array>;
|
|
16
|
+
}
|
|
17
|
+
export declare class HeldLoader extends Loader {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(modules?: readonly Module[]);
|
|
20
|
+
load(from: string): Promise<Loaded | null>;
|
|
16
21
|
}
|
|
17
22
|
export declare class VolatileMemory extends Memory {
|
|
18
23
|
#private;
|
|
19
|
-
|
|
24
|
+
read(place: string): Promise<Map<string, Uint8Array>>;
|
|
25
|
+
write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
|
|
26
|
+
places(): Promise<string[]>;
|
|
27
|
+
forget(place: string): Promise<void>;
|
|
20
28
|
}
|
package/dist/pointer/bodies.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The pointer terrain's bodies: each contract fulfilled in one process,
|
|
3
3
|
// with nothing beyond JavaScript.
|
|
4
|
-
import { Clock, Custody, Entropy, Memory } from '../contract/index.js';
|
|
4
|
+
import { Clock, Custody, Entropy, Loader, Memory } from '../contract/index.js';
|
|
5
5
|
export class CryptoEntropy extends Entropy {
|
|
6
6
|
draw(length) {
|
|
7
7
|
return globalThis.crypto.getRandomValues(new Uint8Array(length));
|
|
@@ -17,29 +17,58 @@ export class SystemClock extends Clock {
|
|
|
17
17
|
return { done, cancel: () => clearTimeout(timer) };
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
//
|
|
20
|
+
// The harbor's seed, drawn once and held by this object.
|
|
21
21
|
export class HeldCustody extends Custody {
|
|
22
|
-
#
|
|
23
|
-
#seeds = new Map();
|
|
22
|
+
#seed;
|
|
24
23
|
constructor(entropy) {
|
|
25
24
|
super();
|
|
26
|
-
this.#
|
|
25
|
+
this.#seed = entropy.draw(32);
|
|
27
26
|
}
|
|
28
|
-
seed(
|
|
29
|
-
|
|
30
|
-
if (!seed)
|
|
31
|
-
this.#seeds.set(name, (seed = this.#entropy.draw(32)));
|
|
32
|
-
return Promise.resolve(seed);
|
|
27
|
+
seed() {
|
|
28
|
+
return Promise.resolve(this.#seed.slice());
|
|
33
29
|
}
|
|
34
30
|
}
|
|
35
|
-
//
|
|
36
|
-
|
|
31
|
+
// Modules handed to this object, found by the name each declares.
|
|
32
|
+
export class HeldLoader extends Loader {
|
|
33
|
+
#modules = new Map();
|
|
34
|
+
constructor(modules = []) {
|
|
35
|
+
super();
|
|
36
|
+
for (const m of modules)
|
|
37
|
+
this.#modules.set(m.module, m);
|
|
38
|
+
}
|
|
39
|
+
load(from) {
|
|
40
|
+
const m = this.#modules.get(from);
|
|
41
|
+
return Promise.resolve(m ? { module: m.module, version: m.version, classes: m.classes, from } : null);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Entries that live as long as this object, copied in and out, which is
|
|
45
|
+
// what a restart in one process finds.
|
|
37
46
|
export class VolatileMemory extends Memory {
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
#places = new Map();
|
|
48
|
+
read(place) {
|
|
49
|
+
const entries = this.#places.get(place) ?? new Map();
|
|
50
|
+
return Promise.resolve(new Map([...entries].map(([name, bytes]) => [name, bytes.slice()])));
|
|
51
|
+
}
|
|
52
|
+
// One object holds it, so a keep is whole by the run of one turn.
|
|
53
|
+
write(place, entries) {
|
|
54
|
+
let kept = this.#places.get(place);
|
|
55
|
+
if (!kept)
|
|
56
|
+
this.#places.set(place, (kept = new Map()));
|
|
57
|
+
for (const [name, bytes] of entries) {
|
|
58
|
+
if (bytes === null)
|
|
59
|
+
kept.delete(name);
|
|
60
|
+
else
|
|
61
|
+
kept.set(name, bytes.slice());
|
|
62
|
+
}
|
|
63
|
+
if (kept.size === 0)
|
|
64
|
+
this.#places.delete(place);
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
}
|
|
67
|
+
places() {
|
|
68
|
+
return Promise.resolve([...this.#places.keys()]);
|
|
69
|
+
}
|
|
70
|
+
forget(place) {
|
|
71
|
+
this.#places.delete(place);
|
|
72
|
+
return Promise.resolve();
|
|
44
73
|
}
|
|
45
74
|
}
|
package/dist/pointer/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { CryptoEntropy, HeldCustody, SystemClock, VolatileMemory } from './bodies.ts';
|
|
1
|
+
export { CryptoEntropy, HeldCustody, HeldLoader, SystemClock, VolatileMemory } from './bodies.ts';
|
|
2
2
|
export { NoCarrier, PointerCarrier, PointerTerrain, World, type PointerParts } from './world.ts';
|
package/dist/pointer/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The pointer dock: a whole world with `nervur` alone.
|
|
3
|
-
export { CryptoEntropy, HeldCustody, SystemClock, VolatileMemory } from './bodies.js';
|
|
3
|
+
export { CryptoEntropy, HeldCustody, HeldLoader, SystemClock, VolatileMemory } from './bodies.js';
|
|
4
4
|
export { NoCarrier, PointerCarrier, PointerTerrain, World } from './world.js';
|
package/dist/pointer/world.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
3
|
-
import { Harbor, Terrain } from '../harbor/index.ts';
|
|
1
|
+
import { Carrier, type Clock, type Custody, type Entropy, type Loader, type Memory, type Module } from '../contract/index.ts';
|
|
2
|
+
import { Harbor, Terrain, type GroundFaculty, type Opening } from '../harbor/index.ts';
|
|
4
3
|
export declare class NoCarrier extends Carrier {
|
|
5
4
|
carry(): Promise<Uint8Array | null>;
|
|
6
5
|
}
|
|
@@ -15,6 +14,10 @@ export type PointerParts = {
|
|
|
15
14
|
custody?: Custody;
|
|
16
15
|
memory?: Memory;
|
|
17
16
|
carrier?: Carrier;
|
|
17
|
+
loader?: Loader;
|
|
18
|
+
faculties?: readonly GroundFaculty[];
|
|
19
|
+
opens?: readonly Opening[];
|
|
20
|
+
wakes?: boolean;
|
|
18
21
|
};
|
|
19
22
|
export declare class PointerTerrain extends Terrain {
|
|
20
23
|
readonly entropy: Entropy;
|
|
@@ -22,13 +25,17 @@ export declare class PointerTerrain extends Terrain {
|
|
|
22
25
|
readonly custody: Custody;
|
|
23
26
|
readonly memory: Memory;
|
|
24
27
|
readonly carrier: Carrier;
|
|
28
|
+
readonly loader: Loader;
|
|
29
|
+
readonly faculties: readonly GroundFaculty[];
|
|
30
|
+
readonly opens: readonly Opening[];
|
|
31
|
+
readonly wakes: boolean;
|
|
25
32
|
constructor(parts?: PointerParts);
|
|
26
33
|
}
|
|
27
34
|
export declare class World {
|
|
28
35
|
#private;
|
|
29
36
|
readonly cut: Set<string>;
|
|
30
|
-
harbor(name: string,
|
|
37
|
+
harbor(name: string, modules?: readonly Module[], parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor>;
|
|
31
38
|
get(name: string): Harbor;
|
|
32
|
-
restart(name: string,
|
|
39
|
+
restart(name: string, parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor>;
|
|
33
40
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
34
41
|
}
|
package/dist/pointer/world.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The pointer world: harbors in one process, an address book from ward pk
|
|
3
|
+
// to harbor that stands in for the internet, and weather between them. A
|
|
4
|
+
// restart drops a harbor and opens a new one on the terrain the world
|
|
5
|
+
// keeps for it, so only what its memory kept comes back.
|
|
1
6
|
import { Carrier } from '../contract/index.js';
|
|
2
7
|
import { Harbor, Terrain } from '../harbor/index.js';
|
|
3
|
-
import { CryptoEntropy, HeldCustody, SystemClock, VolatileMemory } from './bodies.js';
|
|
8
|
+
import { CryptoEntropy, HeldCustody, HeldLoader, SystemClock, VolatileMemory } from './bodies.js';
|
|
4
9
|
// A carrier that reaches no one: a harbor alone.
|
|
5
10
|
export class NoCarrier extends Carrier {
|
|
6
11
|
carry() {
|
|
@@ -24,6 +29,10 @@ export class PointerTerrain extends Terrain {
|
|
|
24
29
|
custody;
|
|
25
30
|
memory;
|
|
26
31
|
carrier;
|
|
32
|
+
loader;
|
|
33
|
+
faculties;
|
|
34
|
+
opens;
|
|
35
|
+
wakes;
|
|
27
36
|
constructor(parts = {}) {
|
|
28
37
|
super();
|
|
29
38
|
this.entropy = parts.entropy ?? new CryptoEntropy();
|
|
@@ -31,20 +40,26 @@ export class PointerTerrain extends Terrain {
|
|
|
31
40
|
this.custody = parts.custody ?? new HeldCustody(this.entropy);
|
|
32
41
|
this.memory = parts.memory ?? new VolatileMemory();
|
|
33
42
|
this.carrier = parts.carrier ?? new NoCarrier();
|
|
43
|
+
this.loader = parts.loader ?? new HeldLoader();
|
|
44
|
+
this.faculties = parts.faculties ?? [];
|
|
45
|
+
this.opens = parts.opens ?? [];
|
|
46
|
+
this.wakes = parts.wakes ?? true;
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
export class World {
|
|
37
50
|
#places = new Map();
|
|
38
51
|
// Harbors the world reaches no one in, nor anyone out of.
|
|
39
52
|
cut = new Set();
|
|
40
|
-
// A harbor of this world
|
|
41
|
-
//
|
|
42
|
-
|
|
53
|
+
// A harbor of this world, whose loader holds `modules` unless `parts`
|
|
54
|
+
// names a loader. `parts` replaces the pointer bodies, a folder's memory
|
|
55
|
+
// and custody among them. The harbor runs only the modules its catalogue
|
|
56
|
+
// was told to add.
|
|
57
|
+
async harbor(name, modules = [], parts = {}) {
|
|
43
58
|
if (this.#places.has(name))
|
|
44
59
|
throw new Error(`a harbor named ${name} stands`);
|
|
45
|
-
const terrain = new PointerTerrain({ ...parts, carrier: new PointerCarrier(this) });
|
|
46
|
-
const harbor = await Harbor.open(terrain
|
|
47
|
-
this.#places.set(name, { harbor, terrain
|
|
60
|
+
const terrain = new PointerTerrain({ loader: new HeldLoader(modules), ...parts, carrier: new PointerCarrier(this) });
|
|
61
|
+
const harbor = await Harbor.open(terrain);
|
|
62
|
+
this.#places.set(name, { harbor, terrain });
|
|
48
63
|
return harbor;
|
|
49
64
|
}
|
|
50
65
|
get(name) {
|
|
@@ -53,17 +68,17 @@ export class World {
|
|
|
53
68
|
throw new Error(`no harbor named ${name}`);
|
|
54
69
|
return place.harbor;
|
|
55
70
|
}
|
|
56
|
-
// The harbor opened again on its own terrain
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
async restart(name,
|
|
71
|
+
// The harbor opened again on its own terrain. `parts` replaces bodies of
|
|
72
|
+
// the terrain, as a new process would build them over the same disk, or
|
|
73
|
+
// with other code.
|
|
74
|
+
async restart(name, parts) {
|
|
60
75
|
const place = this.#places.get(name);
|
|
61
76
|
if (!place)
|
|
62
77
|
throw new Error(`no harbor named ${name}`);
|
|
63
78
|
const t = place.terrain;
|
|
64
|
-
const terrain = parts ? new PointerTerrain({ entropy: t.entropy, clock: t.clock, custody: t.custody, memory: t.memory, ...parts, carrier: t.carrier }) : t;
|
|
65
|
-
const harbor = await Harbor.open(terrain
|
|
66
|
-
this.#places.set(name, {
|
|
79
|
+
const terrain = parts ? new PointerTerrain({ entropy: t.entropy, clock: t.clock, custody: t.custody, memory: t.memory, loader: t.loader, ...parts, carrier: t.carrier }) : t;
|
|
80
|
+
const harbor = await Harbor.open(terrain);
|
|
81
|
+
this.#places.set(name, { harbor, terrain });
|
|
67
82
|
return harbor;
|
|
68
83
|
}
|
|
69
84
|
carry(pk, bytes) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const schemeOf: (s: unknown) => string | null;
|
|
2
|
+
export type TcpAddress = {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const tcpAddress: (s: unknown) => TcpAddress | null;
|
|
7
|
+
export declare const tcpAt: (host: string, port: number) => string;
|
|
8
|
+
export type WebAddress = {
|
|
9
|
+
scheme: 'http' | 'https' | 'ws' | 'wss';
|
|
10
|
+
secure: boolean;
|
|
11
|
+
held: boolean;
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
path: string;
|
|
15
|
+
href: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const webAddress: (s: unknown) => WebAddress | null;
|
|
18
|
+
export declare const unspecified: (s: unknown) => boolean;
|
|
19
|
+
export declare const readAt: (value: unknown) => string[];
|