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/src/pointer/world.ts
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
// to harbor that stands in for the internet, and weather between them. A
|
|
4
4
|
// restart drops a harbor and opens a new one on the terrain the world
|
|
5
5
|
// keeps for it, so only what its memory kept comes back.
|
|
6
|
-
import type
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { CryptoEntropy, HeldCustody, SystemClock, VolatileMemory } from './bodies.ts';
|
|
6
|
+
import { Carrier, type Clock, type Custody, type Entropy, type Loader, type Memory, type Module } from '../contract/index.ts';
|
|
7
|
+
import { Harbor, Terrain, type GroundFaculty, type Opening } from '../harbor/index.ts';
|
|
8
|
+
import { CryptoEntropy, HeldCustody, HeldLoader, SystemClock, VolatileMemory } from './bodies.ts';
|
|
10
9
|
|
|
11
10
|
// A carrier that reaches no one: a harbor alone.
|
|
12
11
|
export class NoCarrier extends Carrier {
|
|
@@ -27,7 +26,17 @@ export class PointerCarrier extends Carrier {
|
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export type PointerParts = {
|
|
29
|
+
export type PointerParts = {
|
|
30
|
+
entropy?: Entropy;
|
|
31
|
+
clock?: Clock;
|
|
32
|
+
custody?: Custody;
|
|
33
|
+
memory?: Memory;
|
|
34
|
+
carrier?: Carrier;
|
|
35
|
+
loader?: Loader;
|
|
36
|
+
faculties?: readonly GroundFaculty[];
|
|
37
|
+
opens?: readonly Opening[];
|
|
38
|
+
wakes?: boolean;
|
|
39
|
+
};
|
|
31
40
|
|
|
32
41
|
export class PointerTerrain extends Terrain {
|
|
33
42
|
readonly entropy: Entropy;
|
|
@@ -35,6 +44,10 @@ export class PointerTerrain extends Terrain {
|
|
|
35
44
|
readonly custody: Custody;
|
|
36
45
|
readonly memory: Memory;
|
|
37
46
|
readonly carrier: Carrier;
|
|
47
|
+
readonly loader: Loader;
|
|
48
|
+
override readonly faculties: readonly GroundFaculty[];
|
|
49
|
+
override readonly opens: readonly Opening[];
|
|
50
|
+
override readonly wakes: boolean;
|
|
38
51
|
|
|
39
52
|
constructor(parts: PointerParts = {}) {
|
|
40
53
|
super();
|
|
@@ -43,23 +56,29 @@ export class PointerTerrain extends Terrain {
|
|
|
43
56
|
this.custody = parts.custody ?? new HeldCustody(this.entropy);
|
|
44
57
|
this.memory = parts.memory ?? new VolatileMemory();
|
|
45
58
|
this.carrier = parts.carrier ?? new NoCarrier();
|
|
59
|
+
this.loader = parts.loader ?? new HeldLoader();
|
|
60
|
+
this.faculties = parts.faculties ?? [];
|
|
61
|
+
this.opens = parts.opens ?? [];
|
|
62
|
+
this.wakes = parts.wakes ?? true;
|
|
46
63
|
}
|
|
47
64
|
}
|
|
48
65
|
|
|
49
|
-
type Place = { harbor: Harbor; terrain: Terrain
|
|
66
|
+
type Place = { harbor: Harbor; terrain: Terrain };
|
|
50
67
|
|
|
51
68
|
export class World {
|
|
52
69
|
readonly #places = new Map<string, Place>();
|
|
53
70
|
// Harbors the world reaches no one in, nor anyone out of.
|
|
54
71
|
readonly cut = new Set<string>();
|
|
55
72
|
|
|
56
|
-
// A harbor of this world
|
|
57
|
-
//
|
|
58
|
-
|
|
73
|
+
// A harbor of this world, whose loader holds `modules` unless `parts`
|
|
74
|
+
// names a loader. `parts` replaces the pointer bodies, a folder's memory
|
|
75
|
+
// and custody among them. The harbor runs only the modules its catalogue
|
|
76
|
+
// was told to add.
|
|
77
|
+
async harbor(name: string, modules: readonly Module[] = [], parts: Omit<PointerParts, 'carrier'> = {}): Promise<Harbor> {
|
|
59
78
|
if (this.#places.has(name)) throw new Error(`a harbor named ${name} stands`);
|
|
60
|
-
const terrain = new PointerTerrain({ ...parts, carrier: new PointerCarrier(this) });
|
|
61
|
-
const harbor = await Harbor.open(terrain
|
|
62
|
-
this.#places.set(name, { harbor, terrain
|
|
79
|
+
const terrain = new PointerTerrain({ loader: new HeldLoader(modules), ...parts, carrier: new PointerCarrier(this) });
|
|
80
|
+
const harbor = await Harbor.open(terrain);
|
|
81
|
+
this.#places.set(name, { harbor, terrain });
|
|
63
82
|
return harbor;
|
|
64
83
|
}
|
|
65
84
|
|
|
@@ -69,16 +88,16 @@ export class World {
|
|
|
69
88
|
return place.harbor;
|
|
70
89
|
}
|
|
71
90
|
|
|
72
|
-
// The harbor opened again on its own terrain
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
async restart(name: string,
|
|
91
|
+
// The harbor opened again on its own terrain. `parts` replaces bodies of
|
|
92
|
+
// the terrain, as a new process would build them over the same disk, or
|
|
93
|
+
// with other code.
|
|
94
|
+
async restart(name: string, parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor> {
|
|
76
95
|
const place = this.#places.get(name);
|
|
77
96
|
if (!place) throw new Error(`no harbor named ${name}`);
|
|
78
97
|
const t = place.terrain;
|
|
79
|
-
const terrain = parts ? new PointerTerrain({ entropy: t.entropy, clock: t.clock, custody: t.custody, memory: t.memory, ...parts, carrier: t.carrier }) : t;
|
|
80
|
-
const harbor = await Harbor.open(terrain
|
|
81
|
-
this.#places.set(name, {
|
|
98
|
+
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;
|
|
99
|
+
const harbor = await Harbor.open(terrain);
|
|
100
|
+
this.#places.set(name, { harbor, terrain });
|
|
82
101
|
return harbor;
|
|
83
102
|
}
|
|
84
103
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Addresses, as `SPEC.md` names them in an invitation's `at`: a URI of
|
|
3
|
+
// RFC 3986 whose scheme, read in any case, names a carrier.
|
|
4
|
+
// `CARRIER-TCP.md` writes `tcp://host:port`, and `CARRIER-WEB.md` writes
|
|
5
|
+
// `http` and `https` for the post and `ws` and `wss` for the held line.
|
|
6
|
+
|
|
7
|
+
// RFC 3986 appendix B, with the characters a URI may hold checked apart.
|
|
8
|
+
const SPLIT = /^(?:([^:/?#]+):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/;
|
|
9
|
+
const URI_CHARS = /^(?:[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=]|%[0-9A-Fa-f]{2})*$/;
|
|
10
|
+
const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/;
|
|
11
|
+
const REG_NAME = /^(?:[A-Za-z0-9\-._~!$&'()*+,;=]|%[0-9A-Fa-f]{2})*$/;
|
|
12
|
+
const IP_LITERAL = /^\[(?:[0-9A-Fa-f:.]+|v[0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+)\]$/;
|
|
13
|
+
|
|
14
|
+
type Parts = { scheme: string; authority: string | null; userinfo: string | null; host: string | null; port: string | null; path: string; query: string | null; fragment: string | null };
|
|
15
|
+
|
|
16
|
+
// A URI with a scheme, taken apart, or null. `host` is as written,
|
|
17
|
+
// brackets included, and `port` the digits as written.
|
|
18
|
+
const parse = (s: unknown): Parts | null => {
|
|
19
|
+
if (typeof s !== 'string' || !URI_CHARS.test(s)) return null;
|
|
20
|
+
const m = SPLIT.exec(s);
|
|
21
|
+
if (!m || m[1] === undefined || !SCHEME.test(m[1])) return null;
|
|
22
|
+
const out: Parts = { scheme: m[1].toLowerCase(), authority: m[2] ?? null, userinfo: null, host: null, port: null, path: m[3] ?? '', query: m[4] ?? null, fragment: m[5] ?? null };
|
|
23
|
+
if (out.authority === null) return out;
|
|
24
|
+
let rest = out.authority;
|
|
25
|
+
const at = rest.lastIndexOf('@');
|
|
26
|
+
if (at >= 0) {
|
|
27
|
+
out.userinfo = rest.slice(0, at);
|
|
28
|
+
rest = rest.slice(at + 1);
|
|
29
|
+
}
|
|
30
|
+
const hp = /^(\[[^\]]*\]|[^:[\]]*)(?::([0-9]*))?$/.exec(rest);
|
|
31
|
+
if (!hp) return null;
|
|
32
|
+
const host = hp[1]!;
|
|
33
|
+
if (host.startsWith('[') ? !IP_LITERAL.test(host) : !REG_NAME.test(host)) return null;
|
|
34
|
+
out.host = host;
|
|
35
|
+
out.port = hp[2] ?? null;
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// The host as a socket takes it: an IPv6 literal without its brackets.
|
|
40
|
+
const bare = (host: string): string => (host.startsWith('[') ? host.slice(1, -1) : host);
|
|
41
|
+
|
|
42
|
+
// The scheme an address names, lowercase, or null for what is no URI.
|
|
43
|
+
export const schemeOf = (s: unknown): string | null => parse(s)?.scheme ?? null;
|
|
44
|
+
|
|
45
|
+
export type TcpAddress = { host: string; port: number };
|
|
46
|
+
|
|
47
|
+
// A `tcp` address as `CARRIER-TCP.md` writes one, or null.
|
|
48
|
+
export const tcpAddress = (s: unknown): TcpAddress | null => {
|
|
49
|
+
const a = parse(s);
|
|
50
|
+
if (!a || a.scheme !== 'tcp' || a.authority === null) return null;
|
|
51
|
+
if (a.userinfo !== null || a.path !== '' || a.query !== null || a.fragment !== null) return null;
|
|
52
|
+
if (!a.host || a.port === null || !/^[0-9]+$/.test(a.port)) return null;
|
|
53
|
+
const port = Number(a.port);
|
|
54
|
+
if (port < 1 || port > 65_535) return null;
|
|
55
|
+
return { host: bare(a.host), port };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// The `tcp` address of a host and port.
|
|
59
|
+
export const tcpAt = (host: string, port: number): string => `tcp://${host.includes(':') ? `[${host}]` : host}:${port}`;
|
|
60
|
+
|
|
61
|
+
export type WebAddress = { scheme: 'http' | 'https' | 'ws' | 'wss'; secure: boolean; held: boolean; host: string; port: number; path: string; href: string };
|
|
62
|
+
const WEB = new Set(['http', 'https', 'ws', 'wss']);
|
|
63
|
+
|
|
64
|
+
// An address of `CARRIER-WEB.md`, or null. `path` is the path and query
|
|
65
|
+
// a request names, `/` when both are empty.
|
|
66
|
+
export const webAddress = (s: unknown): WebAddress | null => {
|
|
67
|
+
const a = parse(s);
|
|
68
|
+
if (!a || !WEB.has(a.scheme) || a.authority === null) return null;
|
|
69
|
+
if (a.userinfo !== null || a.fragment !== null || !a.host) return null;
|
|
70
|
+
if (a.port !== null && a.port !== '' && (!/^[0-9]+$/.test(a.port) || Number(a.port) > 65_535)) return null;
|
|
71
|
+
const scheme = a.scheme as WebAddress['scheme'];
|
|
72
|
+
const secure = scheme === 'https' || scheme === 'wss';
|
|
73
|
+
const port = a.port ? Number(a.port) : secure ? 443 : 80;
|
|
74
|
+
const path = `${a.path || '/'}${a.query !== null ? `?${a.query}` : ''}`;
|
|
75
|
+
return { scheme, secure, held: scheme === 'ws' || scheme === 'wss', host: bare(a.host), port, path, href: s as string };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Whether a tcp or web address names the unspecified host, where a
|
|
79
|
+
// listener binds every interface and no caller can dial.
|
|
80
|
+
export const unspecified = (s: unknown): boolean => {
|
|
81
|
+
const host = (tcpAddress(s) ?? webAddress(s))?.host;
|
|
82
|
+
return host === '0.0.0.0' || host === '::';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// An invitation's `at` as `SPEC.md` reads it: the strings that are URIs
|
|
86
|
+
// with a scheme, in their order. A value that is no array is absent.
|
|
87
|
+
export const readAt = (value: unknown): string[] => (Array.isArray(value) ? value.filter((s): s is string => parse(s) !== null) : []);
|
package/src/quo/door.ts
CHANGED
|
@@ -42,16 +42,18 @@ export class Door {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// A new heir, held fresh, and its invitation. The lock is drawn at the
|
|
45
|
-
// first invitation that names
|
|
46
|
-
|
|
45
|
+
// first invitation that names an heir, before the heir's secret.
|
|
46
|
+
// A new heir, and its invitation, naming in `at` the addresses given.
|
|
47
|
+
async invite(at: readonly string[] = []): Promise<{ heir: string; invitation: Invitation }> {
|
|
47
48
|
let lock = this.relations.lock();
|
|
48
49
|
if (!lock) this.relations.setLock((lock = drawLock(this.draw)));
|
|
49
50
|
const heir = await SigningKey.draw(this.draw);
|
|
50
51
|
this.relations.set(heir.id, { state: 'fresh' });
|
|
51
|
-
|
|
52
|
+
const invitation = { ward: this.key.pk, heir: heir.id, secret: hex(heir.secret), lock: hex(lock.ek), ...(at.length > 0 ? { at: [...at] } : {}) };
|
|
53
|
+
return { heir: heir.id, invitation };
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
// The door stops holding
|
|
56
|
+
// The door stops holding an heir. A fresh one leaves nothing, a spent one
|
|
55
57
|
// leaves the keys kept at removal.
|
|
56
58
|
remove(heir: string): boolean {
|
|
57
59
|
const record = this.relations.get(heir);
|
package/src/quo/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Quo's wire: keys, the invitation, the payload, the reply, the boxes, the
|
|
3
3
|
// door and the standing. It knows no being, no ward and no terrain.
|
|
4
|
+
export { readAt, schemeOf, tcpAddress, tcpAt, unspecified, webAddress, type TcpAddress, type WebAddress } from './address.ts';
|
|
4
5
|
export { Door, SPAN, type Behind, type Choice, type Judged } from './door.ts';
|
|
5
6
|
export { readInvitation, type Invitation } from './invitation.ts';
|
|
6
7
|
export { Ephemeral, SigningKey, WardKey, type Draw } from './keys.ts';
|
package/src/quo/invitation.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The invitation: `{ ward, heir, secret, lock }
|
|
2
|
+
// The invitation: `{ ward, heir, secret, lock, at? }`. A door reads no
|
|
3
|
+
// route from it; `at`, when present, names where the ward is reached.
|
|
3
4
|
import { EK, encapsulate, isHex, unhex } from '../crypto/index.ts';
|
|
5
|
+
import { readAt } from './address.ts';
|
|
4
6
|
|
|
5
|
-
export type Invitation = { readonly ward: string; readonly heir: string; readonly secret: string; readonly lock: string };
|
|
7
|
+
export type Invitation = { readonly ward: string; readonly heir: string; readonly secret: string; readonly lock: string; readonly at?: string[] };
|
|
6
8
|
|
|
7
|
-
// An invitation, or null for any other shape. A field beside the
|
|
8
|
-
// ignored,
|
|
9
|
+
// An invitation, or null for any other shape. A field beside the five is
|
|
10
|
+
// ignored, the lock is held to the modulus check, and `at` keeps the
|
|
11
|
+
// addresses it holds, an `at` with none or of another shape being absent.
|
|
9
12
|
export const readInvitation = (value: unknown): Invitation | null => {
|
|
10
13
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
|
|
11
|
-
const { ward, heir, secret, lock } = value as Record<string, unknown>;
|
|
14
|
+
const { ward, heir, secret, lock, at } = value as Record<string, unknown>;
|
|
12
15
|
if (!isHex(ward, 64) || !isHex(heir, 32) || !isHex(secret, 32) || !isHex(lock, EK)) return null;
|
|
13
16
|
if (encapsulate(unhex(lock), new Uint8Array(32)) === null) return null;
|
|
14
|
-
|
|
17
|
+
const addresses = readAt(at);
|
|
18
|
+
return { ward, heir, secret, lock, ...(addresses.length > 0 ? { at: addresses } : {}) };
|
|
15
19
|
};
|
package/src/quo/keys.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const LABEL = {
|
|
|
15
15
|
wardSeal: utf8('quo-ward-seal'),
|
|
16
16
|
} as const;
|
|
17
17
|
|
|
18
|
-
// An Ed25519 pair:
|
|
18
|
+
// An Ed25519 pair: an heir, or a standing's own key.
|
|
19
19
|
export class SigningKey {
|
|
20
20
|
readonly secret: Uint8Array;
|
|
21
21
|
readonly pk: Uint8Array;
|
package/src/stand/main.ts
CHANGED
package/src/stand/stand.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The stand of `quo/vectors/HARNESS.md`,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// The stand of `quo/vectors/HARNESS.md`, over `quo/`, `nervur/tcp` and
|
|
3
|
+
// `nervur/http`: part one judged, and part two carried, its wards
|
|
4
|
+
// answering on one tcp listener and one web listener, a post's and a held
|
|
5
|
+
// line's, and asking through a dialer of each.
|
|
5
6
|
import { hex, isHex, unhex } from '../crypto/index.ts';
|
|
6
|
-
import {
|
|
7
|
+
import { UNDIALED, type Dialed } from '../harbor/index.ts';
|
|
8
|
+
import { webServe } from '../http/index.ts';
|
|
9
|
+
import { WebDialer, type WebListener } from '../line/index.ts';
|
|
10
|
+
import { Door, freshStanding, MemoryRelations, readInvitation, Standing, tcpAddress, WardKey, webAddress, type Behind, type Choice, type Draw, type Invitation, type Payload, type Read, type Sent } from '../quo/index.ts';
|
|
11
|
+
import { TcpCarrier, TcpListener } from '../tcp/index.ts';
|
|
7
12
|
|
|
8
13
|
type Answer = Record<string, unknown>;
|
|
9
14
|
const ERRORS = ['bad request', 'no such op', 'no such ward', 'not reached', 'ward stood', 'name held'] as const;
|
|
@@ -51,11 +56,43 @@ const OPS = new Set(['ward', 'invite', 'release', 'arrive', 'ask', 'read', 'list
|
|
|
51
56
|
export class Stand {
|
|
52
57
|
readonly #wards = new Map<string, StandWard>();
|
|
53
58
|
readonly #draw: Draw;
|
|
59
|
+
readonly #tcp = new TcpCarrier();
|
|
60
|
+
readonly #web = new WebDialer();
|
|
61
|
+
readonly #routes = new Map<string, string>();
|
|
62
|
+
#tcpListener: Promise<TcpListener> | undefined;
|
|
63
|
+
#webListener: Promise<WebListener> | undefined;
|
|
54
64
|
|
|
55
65
|
constructor(draw: Draw) {
|
|
56
66
|
this.#draw = draw;
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
async close(): Promise<void> {
|
|
70
|
+
await this.#tcp.close();
|
|
71
|
+
await this.#web.close();
|
|
72
|
+
await (await this.#tcpListener)?.close();
|
|
73
|
+
await (await this.#webListener)?.close();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Every ward this program stands, as a listener reaches them.
|
|
77
|
+
readonly #doors = {
|
|
78
|
+
arrive: async (pk: string, bytes: Uint8Array): Promise<Uint8Array | null> => {
|
|
79
|
+
const ward = this.#wards.get(pk);
|
|
80
|
+
return ward ? (await ward.door.arrive(bytes)).bytes : null;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Bytes to a ward at one address, over the carrier its scheme names.
|
|
85
|
+
#dial(at: string, pk: string, bytes: Uint8Array): Promise<Dialed> {
|
|
86
|
+
return tcpAddress(at) ? this.#tcp.dial(at, pk, bytes) : this.#web.dial(at, pk, bytes);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The addresses this program listens at, for an invitation's `at`.
|
|
90
|
+
async #listening(): Promise<string[]> {
|
|
91
|
+
const tcp = await this.#tcpListener;
|
|
92
|
+
const web = await this.#webListener;
|
|
93
|
+
return [...(tcp ? [tcp.at] : []), ...(web ? web.at : [])];
|
|
94
|
+
}
|
|
95
|
+
|
|
59
96
|
async handle(line: string): Promise<Answer> {
|
|
60
97
|
let request: unknown;
|
|
61
98
|
try {
|
|
@@ -90,9 +127,53 @@ export class Stand {
|
|
|
90
127
|
return this.#ask(r);
|
|
91
128
|
case 'read':
|
|
92
129
|
return this.#read(r);
|
|
130
|
+
case 'listen':
|
|
131
|
+
return this.#listen(r.scheme);
|
|
132
|
+
case 'route':
|
|
133
|
+
return this.#route(r);
|
|
93
134
|
default:
|
|
94
|
-
return
|
|
135
|
+
return this.#send(r);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// One listener of a scheme this program stands, answering for every ward
|
|
140
|
+
// it stands.
|
|
141
|
+
async #listen(scheme: unknown): Promise<Answer> {
|
|
142
|
+
if (scheme === undefined || scheme === 'tcp') {
|
|
143
|
+
this.#tcpListener ??= TcpListener.listen(this.#doors);
|
|
144
|
+
return { at: (await this.#tcpListener).at };
|
|
145
|
+
}
|
|
146
|
+
if (scheme !== 'http' && scheme !== 'ws') return refuse('bad request');
|
|
147
|
+
this.#webListener ??= webServe(this.#doors, { host: '127.0.0.1', port: 0, path: '/quo', origins: ['*'] });
|
|
148
|
+
return { at: (await this.#webListener).at.find((at) => at.startsWith(`${scheme}:`))! };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#route(r: Record<string, unknown>): Answer {
|
|
152
|
+
const far = r.far;
|
|
153
|
+
if (!isHex(far, 64) || (tcpAddress(r.at) === null && webAddress(r.at) === null)) return refuse('bad request');
|
|
154
|
+
this.#routes.set(far, r.at as string);
|
|
155
|
+
return { routed: far };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async #send(r: Record<string, unknown>): Promise<Answer> {
|
|
159
|
+
const invitation = inviteOf(r.invitation);
|
|
160
|
+
const method = r.method === undefined ? undefined : text(r.method);
|
|
161
|
+
const args = r.args === undefined ? undefined : objectText(r.args);
|
|
162
|
+
const ward = this.#ward_(r.ward);
|
|
163
|
+
const held = this.#standing(ward, invitation);
|
|
164
|
+
const sent = await held.standing.ask(method, args);
|
|
165
|
+
if (sent === null) return refuse('bad request');
|
|
166
|
+
held.last = sent;
|
|
167
|
+
const far = invitation.ward;
|
|
168
|
+
const route = this.#routes.get(far);
|
|
169
|
+
let reply: Uint8Array | null = null;
|
|
170
|
+
for (const at of route === undefined ? (invitation.at ?? []) : [route]) {
|
|
171
|
+
const out = await this.#dial(at, far, sent.bytes);
|
|
172
|
+
if (out === UNDIALED) continue;
|
|
173
|
+
reply = out;
|
|
174
|
+
break;
|
|
95
175
|
}
|
|
176
|
+
return { read: readOf(await held.standing.read(sent, reply)) };
|
|
96
177
|
}
|
|
97
178
|
|
|
98
179
|
async #ward(seed: string, reach: string | undefined): Promise<Answer> {
|
|
@@ -109,7 +190,7 @@ export class Stand {
|
|
|
109
190
|
const ward = this.#ward_(r.ward);
|
|
110
191
|
reached(reach);
|
|
111
192
|
if (ward.names.has(name)) refuse('name held');
|
|
112
|
-
const { heir, invitation } = await ward.door.invite();
|
|
193
|
+
const { heir, invitation } = await ward.door.invite(await this.#listening());
|
|
113
194
|
ward.names.set(name, heir);
|
|
114
195
|
ward.reaches.set(heir, reach);
|
|
115
196
|
return { invitation };
|