nervur 0.20.4 → 0.21.0

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
@@ -43,38 +43,74 @@ terrain, as below, and that one is used.
43
43
  ## A harbor on the edge
44
44
 
45
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:
46
+ from your own harbor, as you pilot one on a server. The worker is a
47
+ folder of its own, an ES module project (`"type": "module"` in its
48
+ `package.json`), holding the worker and its `wrangler.toml`:
60
49
 
61
50
  ```js
51
+ // worker.js
62
52
  import { harborObject } from 'nervur/edge';
63
- import carried from './package.json';
53
+ import carried from './package.edge.json';
64
54
  import * as greetings from './greetings.js';
65
55
 
66
56
  export const Harbor = harborObject({ modules: [greetings], package: carried });
57
+
58
+ export default {
59
+ fetch(request, env) {
60
+ return env.HARBOR.get(env.HARBOR.idFromName('main')).fetch(request);
61
+ },
62
+ };
63
+ ```
64
+
65
+ ```toml
66
+ # wrangler.toml
67
+ name = "harbor"
68
+ main = "worker.js"
69
+ compatibility_date = "2026-09-01"
70
+
71
+ [[durable_objects.bindings]]
72
+ name = "HARBOR"
73
+ class_name = "Harbor"
74
+
75
+ [[migrations]]
76
+ tag = "v1"
77
+ new_sqlite_classes = ["Harbor"]
67
78
  ```
68
79
 
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:
80
+ Make the harbor and set it up with the command, from that folder, naming
81
+ where the edge will reach it:
82
+
83
+ ```sh
84
+ nervur init --dir ./harbor
85
+ nervur module add ./greetings.js --dir ./harbor
86
+ nervur reach http://127.0.0.1:8787/quo --dir ./harbor
87
+ nervur own me --dir ./harbor
88
+ ```
89
+
90
+ `own` prints the owner invitation, which never leaves your machine. Run
91
+ it on this machine first:
92
+
93
+ ```sh
94
+ nervur edge dev ./package.edge.json --dir ./harbor
95
+ ```
96
+
97
+ `edge dev` packs the harbor into `package.edge.json` and runs your
98
+ `wrangler dev` with its seed, keeping the object's storage in
99
+ `.nervur-edge`, so the harbor stands again each time you run it. The
100
+ first time the object starts, the package lands in its storage and the
101
+ harbor opens under that seed; after that the storage is the harbor, and
102
+ a new deploy leaves it be. Then, from your own harbor:
72
103
 
73
104
  ```sh
74
105
  nervur pilot edge '<the owner invitation>' --dir ./mine
75
106
  nervur host shop --via edge --dir ./mine
76
107
  ```
77
108
 
109
+ For Cloudflare, name the address it will answer at with `reach`, then
110
+ `nervur edge pack ./package.edge.json --dir ./harbor` prints the seed and
111
+ writes the package. Keep the seed as the worker's secret,
112
+ `wrangler secret put NERVUR_SEED`, and `wrangler deploy`.
113
+
78
114
  `pilot` opens the web carrier where the invitation names a web address.
79
115
  An object with no package draws its own seed, sealed under the worker's
80
116
  `NERVUR_SECRET`, and `stood` runs the worker's own code on it as root:
@@ -23,6 +23,10 @@
23
23
  // printed, for the worker's
24
24
  // NERVUR_SEED, and its sealed
25
25
  // package written to the file
26
+ // nervur edge dev <file> [--port P] the harbor packed to the file and
27
+ // run by the worker's own wrangler
28
+ // dev, from this folder, at
29
+ // http://127.0.0.1:P/quo
26
30
  // nervur open <key> <kind> a faculty stands in the box ward
27
31
  // nervur host <ward> [--seed S] a hosted ward stands
28
32
  // nervur route <ward pk> <address... | none> where a ward is reached, in order
@@ -51,7 +55,10 @@ import { join, resolve } from 'node:path';
51
55
  import { WEB_OPENING } from '../line/index.js';
52
56
  import { harborDir, isFile } from '../node/index.js';
53
57
  import { readAt, webAddress } from '../quo/index.js';
58
+ import { edgeDev } from './edge.js';
54
59
  import { carry, isServed, land, open, pack, request, serve } from './harbor.js';
60
+ // Where `edge dev` answers unless --port names another, wrangler's own.
61
+ const EDGE_PORT = 8787;
55
62
  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]';
56
63
  const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
57
64
  // The one flag that stands alone, since what it names is a yes.
@@ -193,13 +200,20 @@ export const main = async (argv, env, print) => {
193
200
  return out({ landed: await land(dir, JSON.parse(text)) });
194
201
  }
195
202
  if (command === 'edge') {
196
- if (words[0] !== 'pack' || words.length !== 2)
197
- throw new Error('edge takes pack and a file');
203
+ if ((words[0] !== 'pack' && words[0] !== 'dev') || words.length !== 2)
204
+ throw new Error('edge takes pack or dev, and a file');
198
205
  if (flags.via !== undefined)
199
- throw new Error('edge pack reads this folder, so it takes no --via');
206
+ throw new Error(`edge ${words[0]} reads this folder, so it takes no --via`);
200
207
  if (await isServed(dir))
201
208
  throw new Error(`a harbor is served from ${dir}: stop it first`);
202
- return out(await pack(dir, words[1]));
209
+ if (words[0] === 'pack')
210
+ return out(await pack(dir, words[1]));
211
+ const dev = await edgeDev(dir, words[1], process.cwd(), flags.port === undefined ? EDGE_PORT : Number(flags.port));
212
+ const stop = () => void dev.close().then(() => process.exit(0));
213
+ process.once('SIGINT', stop);
214
+ process.once('SIGTERM', stop);
215
+ print(JSON.stringify({ at: dev.at, places: dev.places }));
216
+ return SERVING;
203
217
  }
204
218
  if (command === 'serve') {
205
219
  if (words.length > 0)
@@ -0,0 +1,7 @@
1
+ export declare const EDGE_STATE = ".nervur-edge";
2
+ export type EdgeDev = {
3
+ readonly at: string;
4
+ readonly places: number;
5
+ close(): Promise<void>;
6
+ };
7
+ export declare const edgeDev: (dir: string, file: string, cwd: string, port: number) => Promise<EdgeDev>;
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur edge dev`: an edge harbor on this machine, as it will stand on
3
+ // Cloudflare. The harbor of a folder is packed into the file the worker
4
+ // imports, and the worker's own `wrangler dev` runs it, in the folder the
5
+ // command runs from, with the seed handed as `NERVUR_SEED` through the
6
+ // environment so no listing of processes shows it. The object's storage is
7
+ // kept under `.nervur-edge` there, so a harbor landed once stands again
8
+ // across restarts, as it does on Cloudflare.
9
+ //
10
+ // wrangler is the worker's tool and never the kit's: the one who deploys
11
+ // an edge has it already, and `edge dev` finds it where npm put it.
12
+ import { spawn } from 'node:child_process';
13
+ import { join } from 'node:path';
14
+ import { pack } from './harbor.js';
15
+ export const EDGE_STATE = '.nervur-edge';
16
+ const READY_TRIES = 240;
17
+ const READY_EVERY = 250;
18
+ const answers = (url) => fetch(url).then(() => true, () => false);
19
+ // Resolved once wrangler answers at `url`, rejected once it exits or never
20
+ // answers.
21
+ const ready = async (child, url, said) => {
22
+ let gone = false;
23
+ child.once('exit', () => (gone = true));
24
+ for (let i = 0; i < READY_TRIES; i += 1) {
25
+ if (gone)
26
+ throw new Error(`wrangler dev exited: ${said()}`);
27
+ if (await answers(url))
28
+ return;
29
+ await new Promise((r) => setTimeout(r, READY_EVERY));
30
+ }
31
+ throw new Error(`wrangler dev never answered at ${url}: ${said()}`);
32
+ };
33
+ export const edgeDev = async (dir, file, cwd, port) => {
34
+ const { seed, places } = (await pack(dir, file));
35
+ const child = spawn('npx', ['--no-install', 'wrangler', 'dev', '--ip', '127.0.0.1', '--port', String(port), '--persist-to', join(cwd, EDGE_STATE), '--log-level', 'warn'], {
36
+ cwd,
37
+ env: { ...process.env, NERVUR_SEED: seed, CLOUDFLARE_INCLUDE_PROCESS_ENV: 'true' },
38
+ stdio: ['ignore', 'ignore', 'pipe'],
39
+ detached: true,
40
+ });
41
+ let err = '';
42
+ child.stderr.on('data', (chunk) => (err = `${err}${chunk.toString('utf8')}`.slice(-4000)));
43
+ const at = `http://127.0.0.1:${String(port)}/quo`;
44
+ const close = () => new Promise((done) => {
45
+ if (child.exitCode !== null)
46
+ return done();
47
+ child.once('exit', () => done());
48
+ process.kill(-child.pid, 'SIGTERM');
49
+ });
50
+ try {
51
+ await ready(child, at, () => err);
52
+ }
53
+ catch (e) {
54
+ await close();
55
+ throw e;
56
+ }
57
+ return { at, places, close };
58
+ };
package/dist/index.d.ts CHANGED
@@ -5,4 +5,4 @@ export { Carrier, Clock, Custody, Entropy, Loader, Memory, type Loaded, type Mod
5
5
  export { CEILING, DEFAULT, DEPTH, ownerAsk, Pilot, pilots, WardBeing, type Steward, type WardBeingStance } from './ward/index.ts';
6
6
  export { BOX, CarrierFaculty, Catalogue, copyPackage, Dock, Harbor, addGround, grounds, readRootRequest, Registry, Push, PUSH, RELAY, RELAY_CLIENT, rootLine, Terrain, UNDIALED, type Dialed, type Failures, type Ground, type GroundFaculty, type GroundProbe, type Opening, type Probing, type RootRequest, } from './harbor/index.ts';
7
7
  export { CryptoEntropy, HeldCustody, HeldLoader, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World, type PointerParts } from './pointer/index.ts';
8
- export { NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush, type WebListen, type WebListener, type WebServe } from './line/index.ts';
8
+ export { answerPost, NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush, type WebListen, type WebListener, type WebServe } from './line/index.ts';
package/dist/index.js CHANGED
@@ -15,6 +15,6 @@ export { CEILING, DEFAULT, DEPTH, ownerAsk, Pilot, pilots, WardBeing } from './w
15
15
  export { BOX, CarrierFaculty, Catalogue, copyPackage, Dock, Harbor, addGround, grounds, readRootRequest, Registry, Push, PUSH, RELAY, RELAY_CLIENT, rootLine, Terrain, UNDIALED, } from './harbor/index.js';
16
16
  // The pointer dock: a whole world in one process.
17
17
  export { CryptoEntropy, HeldCustody, HeldLoader, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World } from './pointer/index.js';
18
- export { NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush } from './line/index.js';
18
+ export { answerPost, NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush } from './line/index.js';
19
19
  // The ground that fits everywhere, tried after any a platform entry adds.
20
20
  addGround(neutralGround);
@@ -0,0 +1,15 @@
1
+ import { Custody, Entropy, Loader, Memory } from '../contract/index.ts';
2
+ import type { Expect } from './expect.ts';
3
+ export type Scene = [string, () => Promise<void>];
4
+ export declare const custodyScenes: (name: string, make: () => [Custody, Custody], expect: Expect) => Scene[];
5
+ export type Cut = {
6
+ cut: Memory;
7
+ fresh: Memory;
8
+ };
9
+ export declare const memoryScenes: (name: string, make: () => [Memory, Memory], expect: Expect, half?: () => Cut) => Scene[];
10
+ export declare const entropyScenes: (name: string, make: () => Entropy, expect: Expect) => Scene[];
11
+ export declare const loaderScenes: (name: string, make: () => {
12
+ loader: Loader;
13
+ found: string;
14
+ missing: string;
15
+ }, expect: Expect) => Scene[];
@@ -0,0 +1,115 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The suites of the bodies, Custody, Memory, Entropy and Loader, as scenes
3
+ // with no platform under them, so Node's runner, every engine and a
4
+ // stranger's own runner hold a body to the same assertions.
5
+ import { Custody, Entropy, Loader, Memory } from '../contract/index.js';
6
+ const bytes = (...values) => Uint8Array.from(values);
7
+ // A place as plain values, in name order, so two can be compared as JSON.
8
+ const plain = (place) => [...place].map(([n, b]) => [n, [...b]]).sort(([a], [b]) => (a < b ? -1 : 1));
9
+ // `make` gives two bodies over one keeping, as two runs would build them,
10
+ // and a fresh keeping each call.
11
+ export const custodyScenes = (name, make, expect) => [
12
+ [
13
+ `[contract] ${name} fulfils Custody: one seed of thirty-two bytes, the same every time`,
14
+ async () => {
15
+ const [custody, again] = make();
16
+ expect.ok(custody instanceof Custody);
17
+ const seed = await custody.seed();
18
+ expect.equal(seed.length, 32);
19
+ const first = [...seed];
20
+ seed.fill(0);
21
+ expect.same([...(await custody.seed())], first, 'a seed handed out is a copy');
22
+ expect.same([...(await again.seed())], first, 'another body over the keeping holds the same');
23
+ expect.ok([...(await make()[0].seed())].join() !== first.join(), 'another keeping holds another seed');
24
+ const [fresh, rival] = make();
25
+ const [a, b] = await Promise.all([fresh.seed(), rival.seed()]);
26
+ expect.same([...a], [...b], 'two first asks at once keep one seed');
27
+ },
28
+ ],
29
+ ];
30
+ export const memoryScenes = (name, make, expect, half) => [
31
+ [
32
+ `[contract] ${name} fulfils Memory: it names its places, and forgets one whole`,
33
+ async () => {
34
+ const [memory, again] = make();
35
+ expect.same(await memory.places(), []);
36
+ await memory.write('aa', new Map([['01', bytes(1)]]));
37
+ await memory.write('bb', new Map([['01', bytes(2)], ['02', bytes(3)]]));
38
+ expect.same([...(await again.places())].sort(), ['aa', 'bb']);
39
+ await memory.forget('aa');
40
+ expect.same(await again.places(), ['bb']);
41
+ expect.same(plain(await again.read('aa')), []);
42
+ await memory.forget('aa');
43
+ await memory.write('bb', new Map([['01', null], ['02', null]]));
44
+ expect.same(await again.places(), [], 'a place holding nothing is no place');
45
+ },
46
+ ],
47
+ ...(half
48
+ ? [
49
+ [
50
+ `[contract] ${name} fulfils Memory: a keep that stops partway is whole or is nothing`,
51
+ async () => {
52
+ const { cut, fresh } = half();
53
+ await cut.write('aa', new Map([['01', bytes(1)], ['02', bytes(2)]])).catch(() => undefined);
54
+ const before = plain(await fresh.read('aa'));
55
+ await cut.write('aa', new Map([['01', bytes(9)], ['02', null], ['03', bytes(3)]])).catch(() => undefined);
56
+ const after = JSON.stringify(plain(await fresh.read('aa')));
57
+ const whole = JSON.stringify(plain(new Map([['01', bytes(9)], ['03', bytes(3)]])));
58
+ expect.ok(after === JSON.stringify(before) || after === whole, `a keep cut short left ${after}`);
59
+ },
60
+ ],
61
+ ]
62
+ : []),
63
+ [
64
+ `[contract] ${name} fulfils Memory: a place read again holds what was kept, and nothing else`,
65
+ async () => {
66
+ const [memory, again] = make();
67
+ expect.ok(memory instanceof Memory);
68
+ expect.same(plain(await memory.read('aa')), []);
69
+ const written = bytes(1, 2, 3);
70
+ await memory.write('aa', new Map([['01', written], ['02', bytes(4)]]));
71
+ written[0] = 9;
72
+ await memory.write('bb', new Map([['01', bytes(5)]]));
73
+ await memory.write('aa', new Map([['02', null], ['03', bytes()], ['04', null]]));
74
+ const read = await again.read('aa');
75
+ expect.same(plain(read), [['01', [1, 2, 3]], ['03', []]]);
76
+ expect.ok(read.get('01') instanceof Uint8Array, 'an entry is bytes');
77
+ read.get('01')[0] = 7;
78
+ expect.same([...(await memory.read('aa')).get('01')], [1, 2, 3], 'an entry handed out is a copy');
79
+ expect.same(plain(await again.read('bb')), [['01', [5]]]);
80
+ expect.same(plain(await again.read('cc')), []);
81
+ },
82
+ ],
83
+ ];
84
+ export const entropyScenes = (name, make, expect) => [
85
+ [
86
+ `[contract] ${name} fulfils Entropy: bytes of the length asked, never the same twice`,
87
+ () => {
88
+ const entropy = make();
89
+ expect.ok(entropy instanceof Entropy);
90
+ expect.equal(entropy.draw(0).length, 0);
91
+ expect.equal(entropy.drawer(33).length, 33);
92
+ expect.ok([...entropy.draw(32)].join() !== [...entropy.draw(32)].join(), 'two draws differ');
93
+ return Promise.resolve();
94
+ },
95
+ ],
96
+ ];
97
+ // `make` gives a loader, a name it finds a module by, and one it finds
98
+ // nothing by.
99
+ export const loaderScenes = (name, make, expect) => [
100
+ [
101
+ `[contract] ${name} fulfils Loader: a module by its name, found again by the name it gives back, and nothing else`,
102
+ async () => {
103
+ const { loader, found, missing } = make();
104
+ expect.ok(loader instanceof Loader);
105
+ const loaded = await loader.load(found);
106
+ expect.ok(loaded !== null, `nothing found at ${found}`);
107
+ expect.equal(typeof loaded.module, 'string');
108
+ expect.equal(typeof loaded.version, 'string');
109
+ expect.ok(Array.isArray(loaded.classes) && loaded.classes.every((C) => typeof C === 'function'), 'the classes are classes');
110
+ const again = await loader.load(loaded.from);
111
+ expect.same(again && { module: again.module, version: again.version, from: again.from }, { module: loaded.module, version: loaded.version, from: loaded.from });
112
+ expect.equal(await loader.load(missing), null);
113
+ },
114
+ ],
115
+ ];
@@ -0,0 +1,6 @@
1
+ export type Expect = {
2
+ equal(actual: unknown, expected: unknown, message?: string): void;
3
+ same(actual: unknown, expected: unknown, message?: string): void;
4
+ ok(value: unknown, message?: string): void;
5
+ };
6
+ export declare const expect: Expect;
@@ -0,0 +1,18 @@
1
+ const fail = (message, detail) => {
2
+ throw new Error(`${message ? `${message}: ` : ''}${detail}`);
3
+ };
4
+ export const expect = {
5
+ equal: (actual, expected, message) => {
6
+ if (!Object.is(actual, expected))
7
+ fail(message, `${String(actual)} is not ${String(expected)}`);
8
+ },
9
+ same: (actual, expected, message) => {
10
+ const [a, b] = [JSON.stringify(actual), JSON.stringify(expected)];
11
+ if (a !== b)
12
+ fail(message, `${a} is not ${b}`);
13
+ },
14
+ ok: (value, message) => {
15
+ if (!value)
16
+ fail(message, 'not true');
17
+ },
18
+ };
@@ -0,0 +1,3 @@
1
+ export { entropyScenes, custodyScenes, loaderScenes, memoryScenes, type Cut, type Scene } from './contracts.ts';
2
+ export { expect, type Expect } from './expect.ts';
3
+ export { law, lawScenes, type Bodies, type Terrain } from './law.ts';
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur/proof`: the suites a body of a stranger's passes to be supported,
3
+ // the kit's own. Each is a list of scenes, a title and a function, run by
4
+ // whatever runner the stranger keeps, in the engine the body is for:
5
+ //
6
+ // for (const [title, scene] of memoryScenes('S3Memory', make, expect, half)) test(title, scene);
7
+ //
8
+ // `expect` throws on the first assertion that fails. The law's scenes stand
9
+ // harbors on a stranger's bodies and hold the unpacking law over them.
10
+ export { entropyScenes, custodyScenes, loaderScenes, memoryScenes } from './contracts.js';
11
+ export { expect } from './expect.js';
12
+ export { law, lawScenes } from './law.js';
@@ -0,0 +1,52 @@
1
+ import { Being, Faculty, type Asker, type JsonObject } from '../being/index.ts';
2
+ import { Memory, type Custody, type Module } from '../contract/index.ts';
3
+ import { type Harbor } from '../harbor/index.ts';
4
+ import { World } from '../pointer/index.ts';
5
+ import type { Expect } from './expect.ts';
6
+ export type Bodies = {
7
+ custody: Custody;
8
+ memory: Memory;
9
+ };
10
+ export type Terrain = {
11
+ name: string;
12
+ make(): Bodies;
13
+ again(bodies: Bodies): Bodies;
14
+ };
15
+ declare abstract class Echoes extends Faculty {
16
+ static kind: string;
17
+ }
18
+ declare class Echo extends Echoes {
19
+ static kind: string;
20
+ static asks: {
21
+ echo: {};
22
+ };
23
+ echo(args: JsonObject, asker: Asker): JsonObject;
24
+ }
25
+ declare class Shop extends Being {
26
+ static kind: string;
27
+ static cells: {
28
+ sold: string[];
29
+ };
30
+ static asks: {
31
+ buy: {};
32
+ };
33
+ buy(args: JsonObject, asker: Asker): JsonObject;
34
+ }
35
+ declare class Customer extends Being {
36
+ static kind: string;
37
+ static asks: {
38
+ join: {};
39
+ buy: {};
40
+ echo: {};
41
+ spoil: {};
42
+ };
43
+ spoil(): JsonObject;
44
+ join(args: JsonObject): Promise<JsonObject>;
45
+ buy(args: JsonObject): Promise<JsonObject>;
46
+ echo(args: JsonObject): Promise<JsonObject>;
47
+ }
48
+ export declare const classes: (typeof Echo | typeof Shop | typeof Customer)[];
49
+ export declare const law: Module;
50
+ export declare const standOne: (world: World, name: string, parts?: Partial<Bodies>) => Promise<Harbor>;
51
+ export declare const lawScenes: (t: Terrain, expect: Expect) => [string, () => Promise<void>][];
52
+ export {};