nervur 0.20.2 → 0.20.3

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