nervur 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +229 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +5 -1
  4. package/dist/being/faculty.d.ts +1 -0
  5. package/dist/being/faculty.js +13 -7
  6. package/dist/being/index.d.ts +1 -0
  7. package/dist/being/index.js +1 -0
  8. package/dist/being/kind.d.ts +4 -0
  9. package/dist/being/kind.js +28 -0
  10. package/dist/being/types.d.ts +3 -4
  11. package/dist/browser/index.d.ts +35 -0
  12. package/dist/browser/index.js +188 -0
  13. package/dist/browser/worker.d.ts +12 -0
  14. package/dist/browser/worker.js +40 -0
  15. package/dist/browser.d.ts +1 -0
  16. package/dist/browser.js +7 -0
  17. package/dist/cli/command.d.ts +13 -0
  18. package/dist/cli/command.js +237 -0
  19. package/dist/cli/harbor.d.ts +19 -0
  20. package/dist/cli/harbor.js +99 -0
  21. package/dist/cli/main.d.ts +2 -0
  22. package/dist/cli/main.js +14 -0
  23. package/dist/contract/index.d.ts +16 -2
  24. package/dist/contract/index.js +13 -3
  25. package/dist/edge/index.d.ts +57 -0
  26. package/dist/edge/index.js +306 -0
  27. package/dist/edge.d.ts +1 -0
  28. package/dist/edge.js +8 -0
  29. package/dist/folder/index.d.ts +9 -4
  30. package/dist/folder/index.js +89 -84
  31. package/dist/harbor/carrying.d.ts +17 -0
  32. package/dist/harbor/carrying.js +38 -0
  33. package/dist/harbor/catalogue.d.ts +27 -0
  34. package/dist/harbor/catalogue.js +101 -0
  35. package/dist/harbor/dock.d.ts +32 -3
  36. package/dist/harbor/dock.js +98 -12
  37. package/dist/harbor/harbor.d.ts +27 -8
  38. package/dist/harbor/harbor.js +217 -26
  39. package/dist/harbor/index.d.ts +8 -2
  40. package/dist/harbor/index.js +8 -1
  41. package/dist/harbor/package.d.ts +12 -0
  42. package/dist/harbor/package.js +130 -0
  43. package/dist/harbor/probe.d.ts +18 -0
  44. package/dist/harbor/probe.js +15 -0
  45. package/dist/harbor/registry.d.ts +6 -3
  46. package/dist/harbor/registry.js +83 -7
  47. package/dist/harbor/relay.d.ts +15 -0
  48. package/dist/harbor/relay.js +387 -0
  49. package/dist/harbor/root-line.d.ts +9 -0
  50. package/dist/harbor/root-line.js +38 -0
  51. package/dist/harbor/terrain.d.ts +14 -1
  52. package/dist/harbor/terrain.js +19 -0
  53. package/dist/http/index.d.ts +4 -0
  54. package/dist/http/index.js +156 -0
  55. package/dist/http/websocket.d.ts +29 -0
  56. package/dist/http/websocket.js +133 -0
  57. package/dist/index.d.ts +5 -4
  58. package/dist/index.js +12 -6
  59. package/dist/line/answer.d.ts +8 -0
  60. package/dist/line/answer.js +60 -0
  61. package/dist/line/frame.d.ts +34 -0
  62. package/dist/line/frame.js +81 -0
  63. package/dist/line/ground.d.ts +3 -0
  64. package/dist/line/ground.js +9 -0
  65. package/dist/line/index.d.ts +5 -0
  66. package/dist/line/index.js +9 -0
  67. package/dist/line/push.d.ts +7 -0
  68. package/dist/line/push.js +92 -0
  69. package/dist/line/web.d.ts +30 -0
  70. package/dist/line/web.js +181 -0
  71. package/dist/node/index.d.ts +32 -0
  72. package/dist/node/index.js +164 -0
  73. package/dist/node.d.ts +1 -0
  74. package/dist/node.js +7 -0
  75. package/dist/pointer/bodies.d.ts +11 -3
  76. package/dist/pointer/bodies.js +47 -18
  77. package/dist/pointer/index.d.ts +1 -1
  78. package/dist/pointer/index.js +1 -1
  79. package/dist/pointer/world.d.ts +12 -5
  80. package/dist/pointer/world.js +29 -14
  81. package/dist/quo/address.d.ts +19 -0
  82. package/dist/quo/address.js +84 -0
  83. package/dist/quo/door.d.ts +1 -1
  84. package/dist/quo/door.js +6 -4
  85. package/dist/quo/index.d.ts +1 -0
  86. package/dist/quo/index.js +1 -0
  87. package/dist/quo/invitation.d.ts +1 -0
  88. package/dist/quo/invitation.js +9 -5
  89. package/dist/quo/keys.js +1 -1
  90. package/dist/tcp/frame.d.ts +25 -0
  91. package/dist/tcp/frame.js +64 -0
  92. package/dist/tcp/index.d.ts +24 -0
  93. package/dist/tcp/index.js +260 -0
  94. package/dist/ward/index.d.ts +3 -2
  95. package/dist/ward/index.js +2 -1
  96. package/dist/ward/pilot.d.ts +8 -0
  97. package/dist/ward/pilot.js +50 -0
  98. package/dist/ward/stance.d.ts +4 -4
  99. package/dist/ward/stance.js +10 -7
  100. package/dist/ward/ward-being.d.ts +4 -1
  101. package/dist/ward/ward-being.js +8 -2
  102. package/dist/ward/ward.d.ts +11 -2
  103. package/dist/ward/ward.js +41 -5
  104. package/package.json +28 -2
  105. package/src/being/being.ts +5 -1
  106. package/src/being/faculty.ts +14 -7
  107. package/src/being/index.ts +1 -0
  108. package/src/being/kind.ts +30 -0
  109. package/src/being/types.ts +8 -6
  110. package/src/browser/index.ts +207 -0
  111. package/src/browser/worker.ts +66 -0
  112. package/src/browser.ts +9 -0
  113. package/src/cli/command.ts +224 -0
  114. package/src/cli/harbor.ts +103 -0
  115. package/src/cli/main.ts +13 -0
  116. package/src/contract/index.ts +42 -10
  117. package/src/edge/index.ts +359 -0
  118. package/src/edge.ts +10 -0
  119. package/src/folder/index.ts +87 -84
  120. package/src/harbor/carrying.ts +56 -0
  121. package/src/harbor/catalogue.ts +125 -0
  122. package/src/harbor/dock.ts +122 -15
  123. package/src/harbor/harbor.ts +240 -36
  124. package/src/harbor/index.ts +10 -3
  125. package/src/harbor/package.ts +147 -0
  126. package/src/harbor/probe.ts +34 -0
  127. package/src/harbor/registry.ts +73 -9
  128. package/src/harbor/relay.ts +399 -0
  129. package/src/harbor/root-line.ts +39 -0
  130. package/src/harbor/terrain.ts +31 -3
  131. package/src/http/index.ts +150 -0
  132. package/src/http/websocket.ts +124 -0
  133. package/src/index.ts +39 -6
  134. package/src/line/answer.ts +59 -0
  135. package/src/line/frame.ts +88 -0
  136. package/src/line/ground.ts +19 -0
  137. package/src/line/index.ts +9 -0
  138. package/src/line/push.ts +112 -0
  139. package/src/line/web.ts +227 -0
  140. package/src/node/index.ts +170 -0
  141. package/src/node.ts +9 -0
  142. package/src/pointer/bodies.ts +45 -16
  143. package/src/pointer/index.ts +1 -1
  144. package/src/pointer/world.ts +38 -19
  145. package/src/quo/address.ts +87 -0
  146. package/src/quo/door.ts +6 -4
  147. package/src/quo/index.ts +1 -0
  148. package/src/quo/invitation.ts +10 -6
  149. package/src/quo/keys.ts +1 -1
  150. package/src/stand/main.ts +1 -0
  151. package/src/stand/stand.ts +87 -6
  152. package/src/tcp/index.ts +284 -0
  153. package/src/ward/index.ts +3 -2
  154. package/src/ward/pilot.ts +50 -0
  155. package/src/ward/stance.ts +14 -8
  156. package/src/ward/ward-being.ts +9 -3
  157. package/src/ward/ward.ts +53 -6
@@ -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 { BeingClass } from '../being/index.ts';
7
- import { Carrier, type Clock, type Custody, type Entropy, type Memory } from '../contract/index.ts';
8
- import { Harbor, Terrain } from '../harbor/index.ts';
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 = { entropy?: Entropy; clock?: Clock; custody?: Custody; memory?: Memory; carrier?: Carrier };
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; classes: readonly BeingClass[] };
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. `parts` replaces the pointer bodies, a folder's
57
- // memory and custody among them.
58
- async harbor(name: string, classes: readonly BeingClass[], parts: Omit<PointerParts, 'carrier'> = {}): Promise<Harbor> {
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, classes);
62
- this.#places.set(name, { harbor, terrain, classes });
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, with the classes it was
73
- // made with unless others are named. `parts` replaces bodies of the
74
- // terrain, as a new process would build them over the same disk.
75
- async restart(name: string, classes?: readonly BeingClass[], parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor> {
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, classes ?? place.classes);
81
- this.#places.set(name, { ...place, harbor, terrain });
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 a heir, before the heir's secret.
46
- async invite(): Promise<{ heir: string; invitation: Invitation }> {
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
- return { heir: heir.id, invitation: { ward: this.key.pk, heir: heir.id, secret: hex(heir.secret), lock: hex(lock.ek) } };
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 a heir. A fresh one leaves nothing, a spent one
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';
@@ -1,15 +1,19 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The invitation: `{ ward, heir, secret, lock }`, a value with no route.
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 four is
8
- // ignored, and the lock is held to the modulus check.
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
- return { ward, heir, secret, lock };
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: a heir, or a standing's own key.
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
@@ -17,3 +17,4 @@ for await (const line of createInterface({ input: process.stdin, crlfDelay: Infi
17
17
  );
18
18
  }
19
19
  await Promise.all(pending);
20
+ await stand.close();
@@ -1,9 +1,14 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The stand of `quo/vectors/HARNESS.md`, part one, over `quo/` alone. Part
3
- // two carries bytes over TCP, which is the dock's, so its three requests
4
- // are `bad request`.
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 { Door, freshStanding, MemoryRelations, readInvitation, Standing, WardKey, type Behind, type Choice, type Draw, type Invitation, type Payload, type Read, type Sent } from '../quo/index.ts';
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 refuse('bad request');
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 };