nervur 0.20.1 → 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 (131) hide show
  1. package/README.md +130 -8
  2. package/dist/browser/index.d.ts +35 -0
  3. package/dist/browser/index.js +188 -0
  4. package/dist/browser/worker.d.ts +12 -0
  5. package/dist/browser/worker.js +40 -0
  6. package/dist/browser.d.ts +1 -0
  7. package/dist/browser.js +7 -0
  8. package/dist/cli/command.d.ts +1 -1
  9. package/dist/cli/command.js +59 -16
  10. package/dist/cli/harbor.d.ts +9 -11
  11. package/dist/cli/harbor.js +81 -123
  12. package/dist/contract/index.d.ts +2 -1
  13. package/dist/contract/index.js +3 -7
  14. package/dist/edge/index.d.ts +57 -0
  15. package/dist/edge/index.js +306 -0
  16. package/dist/edge.d.ts +1 -0
  17. package/dist/edge.js +8 -0
  18. package/dist/folder/index.d.ts +4 -0
  19. package/dist/folder/index.js +57 -14
  20. package/dist/harbor/carrying.d.ts +17 -0
  21. package/dist/harbor/carrying.js +38 -0
  22. package/dist/harbor/catalogue.d.ts +2 -0
  23. package/dist/harbor/catalogue.js +28 -20
  24. package/dist/harbor/dock.d.ts +22 -3
  25. package/dist/harbor/dock.js +69 -13
  26. package/dist/harbor/harbor.d.ts +14 -4
  27. package/dist/harbor/harbor.js +144 -15
  28. package/dist/harbor/index.d.ts +6 -3
  29. package/dist/harbor/index.js +4 -1
  30. package/dist/harbor/package.d.ts +2 -0
  31. package/dist/harbor/package.js +15 -0
  32. package/dist/harbor/probe.d.ts +18 -0
  33. package/dist/harbor/probe.js +15 -0
  34. package/dist/harbor/registry.d.ts +1 -0
  35. package/dist/harbor/registry.js +11 -0
  36. package/dist/harbor/relay.d.ts +15 -0
  37. package/dist/harbor/relay.js +387 -0
  38. package/dist/harbor/root-line.js +4 -2
  39. package/dist/harbor/terrain.d.ts +12 -0
  40. package/dist/harbor/terrain.js +19 -0
  41. package/dist/http/index.d.ts +4 -0
  42. package/dist/http/index.js +156 -0
  43. package/dist/http/websocket.d.ts +29 -0
  44. package/dist/http/websocket.js +133 -0
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.js +8 -2
  47. package/dist/line/answer.d.ts +8 -0
  48. package/dist/line/answer.js +60 -0
  49. package/dist/line/frame.d.ts +34 -0
  50. package/dist/line/frame.js +81 -0
  51. package/dist/line/ground.d.ts +3 -0
  52. package/dist/line/ground.js +9 -0
  53. package/dist/line/index.d.ts +5 -0
  54. package/dist/line/index.js +9 -0
  55. package/dist/line/push.d.ts +7 -0
  56. package/dist/line/push.js +92 -0
  57. package/dist/line/web.d.ts +30 -0
  58. package/dist/line/web.js +181 -0
  59. package/dist/node/index.d.ts +32 -0
  60. package/dist/node/index.js +164 -0
  61. package/dist/node.d.ts +1 -0
  62. package/dist/node.js +7 -0
  63. package/dist/pointer/bodies.d.ts +2 -0
  64. package/dist/pointer/bodies.js +10 -0
  65. package/dist/pointer/world.d.ts +7 -1
  66. package/dist/pointer/world.js +6 -0
  67. package/dist/quo/address.d.ts +19 -0
  68. package/dist/quo/address.js +84 -0
  69. package/dist/quo/door.d.ts +1 -1
  70. package/dist/quo/door.js +4 -2
  71. package/dist/quo/index.d.ts +1 -0
  72. package/dist/quo/index.js +1 -0
  73. package/dist/quo/invitation.d.ts +1 -0
  74. package/dist/quo/invitation.js +9 -5
  75. package/dist/tcp/frame.d.ts +0 -4
  76. package/dist/tcp/frame.js +0 -10
  77. package/dist/tcp/index.d.ts +8 -2
  78. package/dist/tcp/index.js +123 -27
  79. package/dist/ward/index.d.ts +2 -2
  80. package/dist/ward/index.js +1 -1
  81. package/dist/ward/stance.d.ts +2 -0
  82. package/dist/ward/stance.js +3 -1
  83. package/dist/ward/ward-being.d.ts +2 -1
  84. package/dist/ward/ward-being.js +1 -1
  85. package/dist/ward/ward.d.ts +7 -0
  86. package/dist/ward/ward.js +6 -0
  87. package/package.json +21 -2
  88. package/src/browser/index.ts +207 -0
  89. package/src/browser/worker.ts +66 -0
  90. package/src/browser.ts +9 -0
  91. package/src/cli/command.ts +53 -17
  92. package/src/cli/harbor.ts +82 -116
  93. package/src/contract/index.ts +11 -8
  94. package/src/edge/index.ts +359 -0
  95. package/src/edge.ts +10 -0
  96. package/src/folder/index.ts +57 -12
  97. package/src/harbor/carrying.ts +56 -0
  98. package/src/harbor/catalogue.ts +32 -17
  99. package/src/harbor/dock.ts +88 -17
  100. package/src/harbor/harbor.ts +146 -17
  101. package/src/harbor/index.ts +6 -3
  102. package/src/harbor/package.ts +16 -0
  103. package/src/harbor/probe.ts +34 -0
  104. package/src/harbor/registry.ts +11 -0
  105. package/src/harbor/relay.ts +399 -0
  106. package/src/harbor/root-line.ts +4 -1
  107. package/src/harbor/terrain.ts +28 -2
  108. package/src/http/index.ts +150 -0
  109. package/src/http/websocket.ts +124 -0
  110. package/src/index.ts +35 -2
  111. package/src/line/answer.ts +59 -0
  112. package/src/line/frame.ts +88 -0
  113. package/src/line/ground.ts +19 -0
  114. package/src/line/index.ts +9 -0
  115. package/src/line/push.ts +112 -0
  116. package/src/line/web.ts +227 -0
  117. package/src/node/index.ts +170 -0
  118. package/src/node.ts +9 -0
  119. package/src/pointer/bodies.ts +11 -0
  120. package/src/pointer/world.ts +18 -2
  121. package/src/quo/address.ts +87 -0
  122. package/src/quo/door.ts +4 -2
  123. package/src/quo/index.ts +1 -0
  124. package/src/quo/invitation.ts +10 -6
  125. package/src/stand/stand.ts +60 -21
  126. package/src/tcp/index.ts +142 -31
  127. package/src/ward/index.ts +2 -2
  128. package/src/ward/stance.ts +6 -1
  129. package/src/ward/ward-being.ts +2 -2
  130. package/src/ward/ward.ts +16 -0
  131. package/src/tcp/frame.ts +0 -78
@@ -1,20 +1,18 @@
1
1
  import type { JsonObject } from '../being/index.ts';
2
- import { Loader, type Loaded } from '../contract/index.ts';
3
2
  import { Harbor, type RootRequest } from '../harbor/index.ts';
4
- import { TcpCarrier } from '../tcp/index.ts';
5
- export declare const socketOf: (dir: string) => string;
6
- export declare class ImportLoader extends Loader {
7
- load(from: string): Promise<Loaded | null>;
8
- }
9
- export type Opened = {
10
- harbor: Harbor;
11
- carrier: TcpCarrier;
12
- };
13
- export declare const open: (dir: string) => Promise<Opened>;
3
+ export declare const open: (dir: string) => Promise<Harbor>;
4
+ export declare const isServed: (dir: string) => Promise<boolean>;
14
5
  export declare const request: (dir: string, req: RootRequest) => Promise<JsonObject>;
6
+ export type Carried = {
7
+ seed?: string;
8
+ places: Record<string, Record<string, string>>;
9
+ };
10
+ export declare const carry: (dir: string, seed: boolean) => Promise<Carried>;
11
+ export declare const land: (dir: string, value: unknown) => Promise<string[]>;
15
12
  export type Served = {
16
13
  pk: string;
17
14
  at: string;
15
+ listening: string[];
18
16
  root: string;
19
17
  close(): Promise<void>;
20
18
  };
@@ -1,141 +1,99 @@
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
1
  // SPDX-License-Identifier: Apache-2.0
10
- // A harbor on this machine, as the `nervur` command stands it: its seed and
11
- // sealed memory in a folder, its code imported from the files its
12
- // catalogue names, Quo over TCP as its carrier, and the root line on a
13
- // local socket beside them.
14
- //
15
- // <dir>/seed the harbor's seed
16
- // <dir>/<place>/ its sealed memory
17
- // <dir>/root.sock the root line while the harbor is served, open to
18
- // this user alone, or in the temporary folder for a
19
- // folder too deep for a socket path
2
+ // A harbor on this machine, as the `nervur` command stands it: a harbor of
3
+ // the Node ground in a folder, served while `serve` runs, and asked over
4
+ // its root line's socket.
20
5
  //
21
6
  // A root request goes to a served harbor over its socket. With none served,
22
7
  // the command unpacks the harbor from its folder, asks, and closes it, so
23
8
  // one process at a time writes the folder.
24
- import { createHash } from 'node:crypto';
25
- import { chmod, rm } from 'node:fs/promises';
26
- import { connect, createServer } from 'node:net';
27
- import { tmpdir } from 'node:os';
28
- import { join, resolve } from 'node:path';
29
- import { pathToFileURL } from 'node:url';
30
- import { Loader } from '../contract/index.js';
9
+ import { existsSync } from 'node:fs';
10
+ import { mkdir, writeFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import { hex, isHex, unhex } from '../crypto/index.js';
31
13
  import { FolderCustody, FolderMemory } from '../folder/index.js';
32
14
  import { Harbor, rootLine } from '../harbor/index.js';
33
- import { CryptoEntropy, PointerTerrain } from '../pointer/index.js';
34
- import { TcpCarrier, TcpListener } from '../tcp/index.js';
35
- // A socket path has a hundred bytes or so on every system, so a folder too
36
- // deep for one puts its socket in this user's temporary folder, under a
37
- // name drawn from the folder's own path.
38
- const SOCKET_PATH = 100;
39
- export const socketOf = (dir) => {
40
- const beside = join(resolve(dir), 'root.sock');
41
- if (Buffer.byteLength(beside) <= SOCKET_PATH)
42
- return beside;
43
- return join(tmpdir(), `nervur-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 32)}.sock`);
44
- };
45
- // Code from a file: a module that exports `module`, `version` and
46
- // `classes`, found again by its absolute path. A file that cannot be
47
- // imported, or exports no module, is none.
48
- export class ImportLoader extends Loader {
49
- async load(from) {
50
- const path = resolve(from);
51
- let exported;
52
- try {
53
- exported = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)));
54
- }
55
- catch {
56
- return null;
57
- }
58
- const { module, version, classes } = exported;
59
- if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes))
60
- return null;
61
- return { module, version, classes, from: path };
62
- }
63
- }
64
- export const open = async (dir) => {
65
- const entropy = new CryptoEntropy();
66
- const carrier = new TcpCarrier();
67
- const harbor = await Harbor.open(new PointerTerrain({ entropy, carrier, loader: new ImportLoader(), custody: new FolderCustody(dir, entropy), memory: new FolderMemory(dir) }));
68
- return { harbor, carrier };
69
- };
70
- // One root request over a served harbor's socket, or undefined where none
71
- // is served.
72
- const overSocket = (dir, request) => new Promise((done, fail) => {
73
- const socket = connect(socketOf(dir));
74
- let text = '';
75
- socket.once('connect', () => socket.write(`${JSON.stringify(request)}\n`));
76
- socket.on('data', (chunk) => {
77
- text += chunk.toString('utf8');
78
- const end = text.indexOf('\n');
79
- if (end < 0)
80
- return;
81
- socket.end();
82
- done(JSON.parse(text.slice(0, end)));
83
- });
84
- socket.once('error', (e) => (e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? done(undefined) : fail(e)));
85
- });
15
+ import { askServed, NodeTerrain, socketOf } from '../node/index.js';
16
+ import { CryptoEntropy } from '../pointer/index.js';
17
+ import { TCP_OPENING } from '../tcp/index.js';
18
+ // A harbor of a folder, opened for one ask: it listens nowhere and serves
19
+ // nothing.
20
+ export const open = (dir) => Harbor.open(new NodeTerrain({ dir, wakes: false, serves: false }));
21
+ // Whether a harbor of this folder is served now.
22
+ export const isServed = async (dir) => (await askServed(dir, {})) !== undefined;
86
23
  export const request = async (dir, req) => {
87
- const served = await overSocket(dir, req);
24
+ const served = await askServed(dir, req);
88
25
  if (served !== undefined)
89
26
  return served;
90
- const { harbor, carrier } = await open(dir);
27
+ const harbor = await open(dir);
91
28
  try {
92
29
  return await rootLine(harbor, req);
93
30
  }
94
31
  finally {
95
- await carrier.close();
32
+ await harbor.close();
96
33
  }
97
34
  };
98
- // The harbor served: Quo over TCP on host and port, the root line on the
99
- // socket. A socket file nobody answers on is a stale one, and is replaced.
35
+ const carried = (value) => {
36
+ if (typeof value !== 'object' || value === null)
37
+ return null;
38
+ const { seed, places } = value;
39
+ if (seed !== undefined && !isHex(seed, 32))
40
+ return null;
41
+ if (typeof places !== 'object' || places === null || Array.isArray(places))
42
+ return null;
43
+ for (const entries of Object.values(places)) {
44
+ if (typeof entries !== 'object' || entries === null || Array.isArray(entries))
45
+ return null;
46
+ for (const bytes of Object.values(entries))
47
+ if (!isHex(bytes))
48
+ return null;
49
+ }
50
+ return { ...(seed === undefined ? {} : { seed }), places: places };
51
+ };
52
+ // The harbor of a folder, read as bytes alone. No harbor opens, so
53
+ // nothing is asked and nothing is written.
54
+ export const carry = async (dir, seed) => {
55
+ const memory = new FolderMemory(dir);
56
+ const places = {};
57
+ for (const place of await memory.places()) {
58
+ places[place] = Object.fromEntries([...(await memory.read(place))].map(([name, bytes]) => [name, hex(bytes)]));
59
+ }
60
+ if (!seed)
61
+ return { places };
62
+ return { seed: hex(await new FolderCustody(dir, new CryptoEntropy()).seed()), places };
63
+ };
64
+ // A carried harbor into an empty folder. A folder holding a seed or a
65
+ // place is left as it stands, and a package with no seed needs one there
66
+ // already, which a terrain that seals its custody writes its own way.
67
+ export const land = async (dir, value) => {
68
+ const held = carried(value);
69
+ if (held === null)
70
+ throw new Error('that is no carried harbor');
71
+ const memory = new FolderMemory(dir);
72
+ if ((await memory.places()).length > 0 || (held.seed !== undefined && existsSync(join(dir, 'seed'))))
73
+ throw new Error(`a harbor stands at ${dir}`);
74
+ await mkdir(dir, { recursive: true, mode: 0o700 });
75
+ if (held.seed !== undefined)
76
+ await writeFile(join(dir, 'seed'), `${held.seed}\n`, { mode: 0o600, flag: 'wx' });
77
+ else if (!existsSync(join(dir, 'seed')))
78
+ throw new Error('the package carries no seed, and none is held here');
79
+ for (const [place, entries] of Object.entries(held.places)) {
80
+ await memory.write(place, new Map(Object.entries(entries).map(([name, bytes]) => [name, unhex(bytes)])));
81
+ }
82
+ return Object.keys(held.places);
83
+ };
84
+ // The harbor served: a harbor of the Node ground, with Quo over TCP on
85
+ // host and port.
100
86
  export const serve = async (dir, host, port) => {
101
- if ((await overSocket(dir, {})) !== undefined)
102
- throw new Error(`a harbor is already served from ${dir}`);
103
- const { harbor, carrier } = await open(dir);
104
- const listener = await TcpListener.listen(harbor, host, port);
105
- const root = socketOf(dir);
106
- await rm(root, { force: true });
107
- const server = createServer((socket) => {
108
- let text = '';
109
- socket.on('data', (chunk) => {
110
- text += chunk.toString('utf8');
111
- for (let end = text.indexOf('\n'); end >= 0; end = text.indexOf('\n')) {
112
- const line = text.slice(0, end);
113
- text = text.slice(end + 1);
114
- let value;
115
- try {
116
- value = JSON.parse(line);
117
- }
118
- catch {
119
- value = undefined;
120
- }
121
- void rootLine(harbor, value)
122
- .catch(() => ({ error: 'the harbor threw' }))
123
- .then((answer) => socket.write(`${JSON.stringify(answer)}\n`));
124
- }
125
- });
126
- socket.on('error', () => socket.destroy());
127
- });
128
- await new Promise((done) => server.listen(root, done));
129
- await chmod(root, 0o600);
130
- return {
131
- pk: harbor.box.pk,
132
- at: listener.at,
133
- root,
134
- close: async () => {
135
- await new Promise((done) => server.close(done));
136
- await rm(root, { force: true });
137
- await listener.close();
138
- await carrier.close();
139
- },
140
- };
87
+ const harbor = await Harbor.open(new NodeTerrain({ dir }));
88
+ if (!harbor.box.being(TCP_OPENING.key))
89
+ await harbor.root('open', { key: TCP_OPENING.key, class: TCP_OPENING.kind });
90
+ const tcp = harbor.box.being(TCP_OPENING.key);
91
+ if (host !== undefined || port !== undefined || tcp.listening().length === 0) {
92
+ const heard = (await harbor.root('ask', { being: TCP_OPENING.key, method: 'listen', args: { ...(host === undefined ? {} : { host }), ...(port === undefined ? {} : { port }) } }));
93
+ if (typeof heard.answer?.at !== 'string') {
94
+ await harbor.close();
95
+ throw new Error(`the harbor does not listen: ${JSON.stringify(heard)}`);
96
+ }
97
+ }
98
+ return { pk: harbor.box.pk, at: tcp.listening()[0], listening: harbor.carriers.flatMap((c) => c.listening()), root: socketOf(dir), close: () => harbor.close() };
141
99
  };
@@ -18,6 +18,8 @@ export interface Rows {
18
18
  export declare abstract class Memory {
19
19
  abstract read(place: string): Promise<Map<string, Uint8Array>>;
20
20
  abstract write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
21
+ abstract places(): Promise<string[]>;
22
+ abstract forget(place: string): Promise<void>;
21
23
  }
22
24
  export declare abstract class Custody {
23
25
  abstract seed(): Promise<Uint8Array>;
@@ -35,5 +37,4 @@ export declare abstract class Loader {
35
37
  }
36
38
  export declare abstract class Carrier {
37
39
  abstract carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
38
- route(_pk: string, _at: string | null): boolean;
39
40
  }
@@ -22,12 +22,8 @@ export class Custody {
22
22
  // one thing beside the seed a terrain hands in: a map, an import, a bundle.
23
23
  export class Loader {
24
24
  }
25
- // Bytes to a ward pk in; bytes, or null for nothing, out.
25
+ // Bytes to a ward pk in; bytes, or null for nothing, out. A harbor hands a
26
+ // terrain's carrier what it holds no route for; what it routes goes to its
27
+ // carrier faculties.
26
28
  export class Carrier {
27
- // Where a ward pk is reached, or null to forget it: true when this
28
- // carrier dials by address and took it. The addresses live in the dock's
29
- // cells, and the harbor hands them here as it unpacks.
30
- route(_pk, _at) {
31
- return false;
32
- }
33
29
  }
@@ -0,0 +1,57 @@
1
+ import { Custody, Memory, type Entropy, type Module } from '../contract/index.ts';
2
+ import { Harbor, type GroundFaculty, type GroundProbe } from '../harbor/index.ts';
3
+ import { type WebListen } from '../line/index.ts';
4
+ import { PointerTerrain } from '../pointer/index.ts';
5
+ export declare const EDGE = "edge";
6
+ export interface EdgeStorage {
7
+ get(key: string): Promise<unknown>;
8
+ put(entries: Record<string, unknown>): Promise<void>;
9
+ delete(keys: string[]): Promise<number>;
10
+ list(options: {
11
+ prefix: string;
12
+ }): Promise<Map<string, unknown>>;
13
+ transaction<T>(work: (txn: EdgeStorage) => Promise<T>): Promise<T>;
14
+ }
15
+ export interface EdgeState {
16
+ readonly storage: EdgeStorage;
17
+ blockConcurrencyWhile<T>(work: () => Promise<T>): Promise<T>;
18
+ }
19
+ export type EdgeEnv = {
20
+ readonly NERVUR_SECRET?: string;
21
+ };
22
+ export type EdgeGiven = {
23
+ readonly state: EdgeState;
24
+ readonly env: EdgeEnv;
25
+ };
26
+ export declare class EdgeMemory extends Memory {
27
+ #private;
28
+ constructor(storage: EdgeStorage, root?: string);
29
+ read(place: string): Promise<Map<string, Uint8Array>>;
30
+ write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
31
+ places(): Promise<string[]>;
32
+ forget(place: string): Promise<void>;
33
+ }
34
+ export declare class EdgeCustody extends Custody {
35
+ #private;
36
+ constructor(storage: EdgeStorage, secret: string | undefined, entropy?: Entropy, root?: string);
37
+ seed(): Promise<Uint8Array>;
38
+ }
39
+ export declare const edgeTcp: GroundFaculty;
40
+ export type EdgeParts = {
41
+ readonly given: EdgeGiven;
42
+ readonly modules?: readonly Module[];
43
+ };
44
+ export declare class EdgeTerrain extends PointerTerrain {
45
+ #private;
46
+ readonly given: EdgeGiven;
47
+ listening: WebListen | undefined;
48
+ constructor(parts: EdgeParts);
49
+ stand(harbor: Harbor): Promise<void>;
50
+ answer(request: Request): Promise<Response>;
51
+ }
52
+ export declare const edgeGround: GroundProbe;
53
+ export type HarborObjectClass = new (state: EdgeState, env: EdgeEnv) => {
54
+ readonly harbor: Promise<Harbor>;
55
+ fetch(request: Request): Promise<Response>;
56
+ };
57
+ export declare const harborObject: (modules: readonly Module[], stood?: (harbor: Harbor) => Promise<void>) => HarborObjectClass;
@@ -0,0 +1,306 @@
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
+ import { Custody, Memory } from '../contract/index.js';
10
+ import { decrypt, encrypt, hkdf, isHex, unhex, utf8 } from '../crypto/index.js';
11
+ import { addGround, CarrierFaculty, Harbor, UNDIALED } from '../harbor/index.js';
12
+ import { answerHeld, answerPost, askFrame, FrameReader, TCP, TCP_OPENING, WEB_OPENING, webFaculty, webPush } from '../line/index.js';
13
+ import { CryptoEntropy, HeldLoader, PointerTerrain } from '../pointer/index.js';
14
+ import { tcpAddress, tcpAt } from '../quo/index.js';
15
+ import { ownerAsk } from '../ward/index.js';
16
+ export const EDGE = 'edge';
17
+ const ENTRY = 'm/';
18
+ const SEED = 'c/seed';
19
+ // The most keys one storage call takes.
20
+ const BATCH = 128;
21
+ const SEAL_INFO = utf8('nervur edge custody');
22
+ const MAX_ID = 0xffff_ffff;
23
+ const chunks = (items) => Array.from({ length: Math.ceil(items.length / BATCH) }, (_, i) => items.slice(i * BATCH, (i + 1) * BATCH));
24
+ const bytesOf = (value) => new Uint8Array(value instanceof ArrayBuffer ? value : value);
25
+ // A body keeps its keys under `root`, so one object may hold more than one
26
+ // keeping.
27
+ export class EdgeMemory extends Memory {
28
+ #storage;
29
+ #entry;
30
+ constructor(storage, root = '') {
31
+ super();
32
+ this.#storage = storage;
33
+ this.#entry = `${root}${ENTRY}`;
34
+ }
35
+ async read(place) {
36
+ const prefix = `${this.#entry}${place}/`;
37
+ const listed = await this.#storage.list({ prefix });
38
+ return new Map([...listed].map(([key, value]) => [key.slice(prefix.length), bytesOf(value).slice()]));
39
+ }
40
+ write(place, entries) {
41
+ const put = [];
42
+ const gone = [];
43
+ for (const [name, bytes] of entries) {
44
+ if (bytes === null)
45
+ gone.push(`${this.#entry}${place}/${name}`);
46
+ else
47
+ put.push([`${this.#entry}${place}/${name}`, bytes.slice()]);
48
+ }
49
+ return this.#storage.transaction(async (txn) => {
50
+ for (const batch of chunks(put))
51
+ await txn.put(Object.fromEntries(batch));
52
+ for (const batch of chunks(gone))
53
+ await txn.delete(batch);
54
+ });
55
+ }
56
+ async places() {
57
+ const entry = this.#entry;
58
+ const keys = [...(await this.#storage.list({ prefix: entry })).keys()];
59
+ return [...new Set(keys.map((key) => key.slice(entry.length, key.indexOf('/', entry.length))))];
60
+ }
61
+ async forget(place) {
62
+ const keys = [...(await this.#storage.list({ prefix: `${this.#entry}${place}/` })).keys()];
63
+ await this.#storage.transaction(async (txn) => {
64
+ for (const batch of chunks(keys))
65
+ await txn.delete(batch);
66
+ });
67
+ }
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
+ export class EdgeCustody extends Custody {
72
+ #storage;
73
+ #secret;
74
+ #entropy;
75
+ #key;
76
+ constructor(storage, secret, entropy = new CryptoEntropy(), root = '') {
77
+ super();
78
+ this.#storage = storage;
79
+ this.#secret = secret;
80
+ this.#entropy = entropy;
81
+ this.#key = `${root}${SEED}`;
82
+ }
83
+ async seed() {
84
+ if (!this.#secret)
85
+ throw new Error('the worker holds no NERVUR_SECRET');
86
+ const key = await hkdf(utf8(this.#secret), SEAL_INFO, 32);
87
+ return this.#storage.transaction(async (txn) => {
88
+ const kept = (await txn.get(this.#key));
89
+ if (kept) {
90
+ const seed = await decrypt(key, bytesOf(kept.nonce), new Uint8Array(0), bytesOf(kept.sealed));
91
+ if (seed === null)
92
+ throw new Error('the seed does not open under this NERVUR_SECRET');
93
+ return seed;
94
+ }
95
+ const seed = this.#entropy.draw(32);
96
+ const nonce = this.#entropy.draw(12);
97
+ await txn.put({ [this.#key]: { nonce, sealed: await encrypt(key, nonce, new Uint8Array(0), seed) } });
98
+ return seed.slice();
99
+ });
100
+ }
101
+ }
102
+ class EdgeLine {
103
+ #socket;
104
+ #writer;
105
+ #waiting = new Map();
106
+ #next = 1;
107
+ #closed = false;
108
+ constructor(socket, onClose) {
109
+ this.#socket = socket;
110
+ this.#writer = socket.writable.getWriter();
111
+ const reader = new FrameReader();
112
+ void (async () => {
113
+ try {
114
+ for await (const chunk of socket.readable) {
115
+ for (const frame of reader.push(new Uint8Array(chunk))) {
116
+ if (frame.kind === 'ask')
117
+ continue;
118
+ const done = this.#waiting.get(frame.id);
119
+ this.#waiting.delete(frame.id);
120
+ done?.(frame.kind === 'reply' ? frame.box : null);
121
+ }
122
+ if (reader.bad)
123
+ break;
124
+ }
125
+ }
126
+ catch {
127
+ // A line that broke answers nothing more.
128
+ }
129
+ this.#closed = true;
130
+ onClose();
131
+ for (const done of this.#waiting.values())
132
+ done(null);
133
+ this.#waiting.clear();
134
+ await socket.close().catch(() => undefined);
135
+ })();
136
+ }
137
+ ask(pk, box) {
138
+ if (this.#closed)
139
+ return Promise.resolve(null);
140
+ const id = this.#next;
141
+ this.#next = this.#next === MAX_ID ? 1 : this.#next + 1;
142
+ return new Promise((resolve) => {
143
+ this.#waiting.set(id, resolve);
144
+ this.#writer.write(askFrame(id, pk, box)).catch(() => resolve(null));
145
+ });
146
+ }
147
+ close() {
148
+ this.#closed = true;
149
+ return this.#socket.close().catch(() => undefined);
150
+ }
151
+ }
152
+ const sockets = 'cloudflare:sockets';
153
+ const connector = async () => {
154
+ try {
155
+ return (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ sockets))).connect;
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ };
161
+ // Quo over TCP from a worker: it dials, and takes no inbound TCP.
162
+ export const edgeTcp = () => class EdgeTcp extends CarrierFaculty {
163
+ static kind = TCP;
164
+ static asks = {
165
+ listen: ownerAsk('an edge takes no inbound TCP'),
166
+ deaf: ownerAsk('listen nowhere'),
167
+ };
168
+ #lines = new Map();
169
+ accepts(address) {
170
+ return tcpAddress(address) !== null;
171
+ }
172
+ async dial(address, pk, bytes) {
173
+ const at = tcpAddress(address);
174
+ if (at === null || !isHex(pk, 64))
175
+ return UNDIALED;
176
+ const line = await this.#line(at);
177
+ return line ? line.ask(unhex(pk), bytes) : UNDIALED;
178
+ }
179
+ #line({ host, port }) {
180
+ const at = tcpAt(host, port);
181
+ let line = this.#lines.get(at);
182
+ if (!line) {
183
+ line = (async () => {
184
+ const connect = await connector();
185
+ if (!connect)
186
+ return null;
187
+ try {
188
+ const socket = connect({ hostname: host, port });
189
+ await socket.opened;
190
+ return new EdgeLine(socket, () => this.#lines.delete(at));
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ })();
196
+ this.#lines.set(at, line);
197
+ void line.then((open) => open ?? this.#lines.delete(at));
198
+ }
199
+ return line;
200
+ }
201
+ async sleep() {
202
+ const lines = [...this.#lines.values()];
203
+ this.#lines.clear();
204
+ for (const line of await Promise.all(lines))
205
+ await line?.close();
206
+ }
207
+ listen() {
208
+ return { error: 'an edge takes no inbound TCP' };
209
+ }
210
+ deaf() {
211
+ return { deaf: true };
212
+ }
213
+ };
214
+ // Where the edge's web carrier listens: the object's own requests, at the
215
+ // host and path its cells name. A port of 0 is the edge's own https.
216
+ const edgeServe = (ground, listen) => {
217
+ const terrain = ground.terrain;
218
+ if (!(terrain instanceof EdgeTerrain))
219
+ return Promise.reject(new Error('this harbor stands on no edge'));
220
+ terrain.listening = listen;
221
+ const authority = listen.port === 0 ? listen.host : `${tcpAt(listen.host, listen.port).slice('tcp://'.length)}`;
222
+ const [post, held] = listen.port === 0 ? ['https', 'wss'] : ['http', 'ws'];
223
+ return Promise.resolve({
224
+ port: listen.port,
225
+ at: [`${post}://${authority}${listen.path}`, `${held}://${authority}${listen.path}`],
226
+ close: () => {
227
+ if (terrain.listening === listen)
228
+ terrain.listening = undefined;
229
+ return Promise.resolve();
230
+ },
231
+ });
232
+ };
233
+ export class EdgeTerrain extends PointerTerrain {
234
+ given;
235
+ // Where the web carrier listens now, as the object answers requests.
236
+ listening;
237
+ #harbor;
238
+ constructor(parts) {
239
+ const entropy = new CryptoEntropy();
240
+ const { storage } = parts.given.state;
241
+ super({
242
+ entropy,
243
+ loader: new HeldLoader(parts.modules ?? []),
244
+ custody: new EdgeCustody(storage, parts.given.env.NERVUR_SECRET, entropy),
245
+ memory: new EdgeMemory(storage),
246
+ faculties: [edgeTcp, webFaculty(edgeServe), webPush],
247
+ opens: [TCP_OPENING, WEB_OPENING],
248
+ });
249
+ this.given = parts.given;
250
+ }
251
+ stand(harbor) {
252
+ this.#harbor = harbor;
253
+ return Promise.resolve();
254
+ }
255
+ // One request to the object: a post, or a held line opened, where the
256
+ // web carrier listens.
257
+ answer(request) {
258
+ const listen = this.listening;
259
+ const harbor = this.#harbor;
260
+ if (!listen || !harbor)
261
+ return Promise.resolve(new Response(null, { status: 404 }));
262
+ if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket')
263
+ return answerPost(harbor, listen, request);
264
+ const offered = (request.headers.get('sec-websocket-protocol') ?? '').split(',').map((p) => p.trim());
265
+ if (new URL(request.url).pathname !== listen.path || !offered.includes('quo'))
266
+ return Promise.resolve(new Response(null, { status: 400 }));
267
+ const Pair = globalThis.WebSocketPair;
268
+ const pair = new Pair();
269
+ const [client, server] = [pair[0], pair[1]];
270
+ server.accept();
271
+ const side = { send: (body) => server.send(new Uint8Array(body)), close: () => server.close() };
272
+ server.addEventListener('message', (event) => answerHeld(harbor, side, typeof event.data === 'string' ? event.data : new Uint8Array(event.data)));
273
+ return Promise.resolve(new Response(null, { status: 101, webSocket: client, headers: { 'sec-websocket-protocol': 'quo' } }));
274
+ }
275
+ }
276
+ const isGiven = (value) => {
277
+ const given = value;
278
+ return typeof given?.state?.storage?.transaction === 'function' && typeof given.env === 'object';
279
+ };
280
+ // A harbor handed a Durable Object's state and environment.
281
+ export const edgeGround = {
282
+ name: EDGE,
283
+ fits: ({ given }) => isGiven(given),
284
+ terrain: ({ given, modules }) => new EdgeTerrain({ given: given, ...(modules ? { modules } : {}) }),
285
+ };
286
+ export const harborObject = (modules, stood) => class HarborObject {
287
+ #opening;
288
+ // The object's harbor, once it stands.
289
+ get harbor() {
290
+ return this.#opening;
291
+ }
292
+ constructor(state, env) {
293
+ addGround(edgeGround);
294
+ // Opened inside the object's block, so no request reaches it half
295
+ // open and its storage answers the opening.
296
+ this.#opening = state.blockConcurrencyWhile(async () => {
297
+ const harbor = await Harbor.open({ modules, given: { state, env } });
298
+ await stood?.(harbor);
299
+ return harbor;
300
+ });
301
+ }
302
+ async fetch(request) {
303
+ const harbor = await this.#opening;
304
+ return harbor.terrain.answer(request);
305
+ }
306
+ };
package/dist/edge.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './index.ts';
package/dist/edge.js ADDED
@@ -0,0 +1,8 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur` as a Workers bundle reads it: the main entry, with the edge
3
+ // ground added, so `Harbor.open({ given: { state, env } })` in a Durable
4
+ // Object keeps a harbor in its storage.
5
+ import { edgeGround } from './edge/index.js';
6
+ import { addGround } from './harbor/index.js';
7
+ export * from './index.js';
8
+ addGround(edgeGround);