nervur 0.21.1 → 0.21.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 +42 -30
- package/dist/cli/command.js +23 -15
- package/dist/cli/edge.d.ts +1 -1
- package/dist/cli/edge.js +30 -11
- package/dist/cli/harbor.d.ts +1 -1
- package/dist/cli/harbor.js +18 -6
- package/dist/edge/index.d.ts +23 -13
- package/dist/edge/index.js +103 -37
- package/dist/harbor/dock.d.ts +5 -1
- package/dist/harbor/dock.js +57 -8
- package/dist/harbor/harbor.d.ts +3 -1
- package/dist/harbor/harbor.js +78 -12
- package/dist/harbor/index.d.ts +1 -0
- package/dist/harbor/index.js +1 -0
- package/dist/harbor/package.d.ts +2 -1
- package/dist/harbor/package.js +12 -6
- package/dist/harbor/registry.js +6 -1
- package/dist/harbor/store.d.ts +13 -0
- package/dist/harbor/store.js +53 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/proof/index.d.ts +1 -1
- package/dist/proof/index.js +4 -2
- package/dist/proof/law.d.ts +40 -2
- package/dist/proof/law.js +250 -4
- package/dist/ward/ward.js +18 -12
- package/package.json +1 -1
- package/src/cli/command.ts +23 -15
- package/src/cli/edge.ts +30 -11
- package/src/cli/harbor.ts +15 -6
- package/src/edge/index.ts +142 -65
- package/src/harbor/dock.ts +60 -11
- package/src/harbor/harbor.ts +78 -14
- package/src/harbor/index.ts +1 -0
- package/src/harbor/package.ts +16 -6
- package/src/harbor/registry.ts +7 -1
- package/src/harbor/store.ts +69 -0
- package/src/index.ts +2 -0
- package/src/proof/index.ts +4 -2
- package/src/proof/law.ts +243 -4
- package/src/ward/ward.ts +15 -10
package/README.md
CHANGED
|
@@ -35,31 +35,29 @@ 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
|
|
39
|
-
on whatever host reaches it, and asking
|
|
38
|
+
On Cloudflare Workers it is one Durable Object of many a worker carries,
|
|
39
|
+
answering Quo at `/<name>/quo` on whatever host reaches it, and asking
|
|
40
|
+
over TCP and the web. Anywhere
|
|
40
41
|
else it lives as long as its process and speaks the web. Hand in a
|
|
41
42
|
terrain, as below, and that one is used.
|
|
42
43
|
|
|
43
44
|
## A harbor on the edge
|
|
44
45
|
|
|
45
46
|
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.
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
from your own harbor, as you pilot one on a server. One worker carries as
|
|
48
|
+
many harbors as you pack into it, each a Durable Object by its name,
|
|
49
|
+
answering at `/<name>/quo`. The worker is a folder of its own, an ES
|
|
50
|
+
module project (`"type": "module"` in its `package.json`), holding the
|
|
51
|
+
worker and its `wrangler.toml`:
|
|
49
52
|
|
|
50
53
|
```js
|
|
51
54
|
// worker.js
|
|
52
|
-
import { harborObject } from 'nervur/edge';
|
|
53
|
-
import
|
|
55
|
+
import { harborObject, harborWorker } from 'nervur/edge';
|
|
56
|
+
import pack from './pack.edge.json';
|
|
54
57
|
import * as greetings from './greetings.js';
|
|
55
58
|
|
|
56
|
-
export const Harbor = harborObject({ modules: [greetings],
|
|
57
|
-
|
|
58
|
-
export default {
|
|
59
|
-
fetch(request, env) {
|
|
60
|
-
return env.HARBOR.get(env.HARBOR.idFromName('main')).fetch(request);
|
|
61
|
-
},
|
|
62
|
-
};
|
|
59
|
+
export const Harbor = harborObject({ modules: [greetings], pack });
|
|
60
|
+
export default harborWorker();
|
|
63
61
|
```
|
|
64
62
|
|
|
65
63
|
```toml
|
|
@@ -83,7 +81,7 @@ where the edge will reach it:
|
|
|
83
81
|
```sh
|
|
84
82
|
nervur init --dir ./harbor
|
|
85
83
|
nervur module add ./greetings.js --dir ./harbor
|
|
86
|
-
nervur reach http://127.0.0.1:8787/quo --dir ./harbor
|
|
84
|
+
nervur reach http://127.0.0.1:8787/main/quo --dir ./harbor
|
|
87
85
|
nervur own me --dir ./harbor
|
|
88
86
|
```
|
|
89
87
|
|
|
@@ -91,34 +89,41 @@ nervur own me --dir ./harbor
|
|
|
91
89
|
it on this machine first:
|
|
92
90
|
|
|
93
91
|
```sh
|
|
94
|
-
nervur edge dev ./
|
|
92
|
+
nervur edge dev ./pack.edge.json main --dir ./harbor
|
|
95
93
|
```
|
|
96
94
|
|
|
97
|
-
`edge dev` packs the harbor into `
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
harbor
|
|
102
|
-
a new deploy leaves it be. Then, from your own harbor:
|
|
95
|
+
`edge dev` packs the harbor as `main` into `pack.edge.json`, beside any
|
|
96
|
+
harbor packed there before, and runs your `wrangler dev`, keeping the
|
|
97
|
+
objects' storage and a secret of its own in `.nervur-edge`, so the
|
|
98
|
+
harbor stands again each time you run it. The first time its object
|
|
99
|
+
starts, the harbor lands in its storage; after that the storage is the
|
|
100
|
+
harbor, and a new deploy leaves it be. Then, from your own harbor:
|
|
103
101
|
|
|
104
102
|
```sh
|
|
105
103
|
nervur pilot edge '<the owner invitation>' --dir ./mine
|
|
106
104
|
nervur host shop --via edge --dir ./mine
|
|
107
105
|
```
|
|
108
106
|
|
|
109
|
-
For Cloudflare,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`
|
|
107
|
+
For Cloudflare, keep one secret for the worker, `wrangler secret put
|
|
108
|
+
NERVUR_SECRET`, and hold the same one as `NERVUR_SECRET` in your shell.
|
|
109
|
+
Name each harbor's address with `reach`, pack each with
|
|
110
|
+
`nervur edge pack ./pack.edge.json <name> --dir <its folder>`, and
|
|
111
|
+
`wrangler deploy`. A pack holds every seed sealed under that secret, so
|
|
112
|
+
it ships in the bundle and opens nowhere else, and a new harbor is a new
|
|
113
|
+
deploy, never a new secret.
|
|
113
114
|
|
|
114
115
|
`pilot` opens the web carrier where the invitation names a web address.
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
A harbor may also be born on the edge, under a name `born` allows, with
|
|
117
|
+
a seed of its own sealed the same way, and `stood` runs the worker's own
|
|
118
|
+
code on it as root. A name the pack does not hold and `born` does not
|
|
119
|
+
allow answers 404, so a stranger's request mints nothing:
|
|
117
120
|
|
|
118
121
|
```js
|
|
119
|
-
export const
|
|
122
|
+
export const Harbor = harborObject({
|
|
120
123
|
modules: [greetings],
|
|
121
|
-
|
|
124
|
+
pack,
|
|
125
|
+
born: (name) => name === 'shop',
|
|
126
|
+
stood: async (harbor, name) => {
|
|
122
127
|
// runs once the harbor stands
|
|
123
128
|
},
|
|
124
129
|
});
|
|
@@ -212,6 +217,13 @@ catalogue, and the root's asks write the rest: `add` on the catalogue,
|
|
|
212
217
|
`open`, `host`, `route`, and `boot` on a ward. `host { ward, seed? }`
|
|
213
218
|
stands a ward under a seed you name, or a drawn one.
|
|
214
219
|
|
|
220
|
+
A ward's place is kept by the root memory unless you name a store.
|
|
221
|
+
`open { key: 'vault', class: 'org.nervur.ground-store' }` opens the
|
|
222
|
+
store every ground ships, and `host { ward, store: 'vault' }` keeps the
|
|
223
|
+
ward there. A class of yours extending `Store` keeps a ward in a bucket
|
|
224
|
+
or a database the same way, and `move { ward, store }` carries a ward
|
|
225
|
+
between keepers whole.
|
|
226
|
+
|
|
215
227
|
## What it holds
|
|
216
228
|
|
|
217
229
|
- **Being, Faculty.** What you extend. A faculty's contract is its class
|
package/dist/cli/command.js
CHANGED
|
@@ -19,16 +19,19 @@
|
|
|
19
19
|
// --with-seed the seed goes too
|
|
20
20
|
// nervur import <file> a carried harbor into an empty
|
|
21
21
|
// folder, `-` being this input
|
|
22
|
-
// nervur edge pack <file>
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// nervur edge dev <file> [--port P]
|
|
22
|
+
// nervur edge pack <file> <name> the harbor for an edge, under its
|
|
23
|
+
// name, in the pack the file holds:
|
|
24
|
+
// its seed sealed under
|
|
25
|
+
// $NERVUR_SECRET, and its places
|
|
26
|
+
// nervur edge dev <file> <name> [--port P] the harbor packed so, and the pack
|
|
27
27
|
// run by the worker's own wrangler
|
|
28
28
|
// dev, from this folder, at
|
|
29
|
-
// http://127.0.0.1:P
|
|
29
|
+
// http://127.0.0.1:P/<name>/quo
|
|
30
30
|
// nervur open <key> <kind> a faculty stands in the box ward
|
|
31
|
-
// nervur host <ward> [--seed S]
|
|
31
|
+
// nervur host <ward> [--seed S] [--store K] a hosted ward stands, kept by the
|
|
32
|
+
// root memory or the store K
|
|
33
|
+
// nervur move <ward> <store | root> a hosted ward carried to a store,
|
|
34
|
+
// or back to the root memory
|
|
32
35
|
// nervur route <ward pk> <address... | none> where a ward is reached, in order
|
|
33
36
|
// nervur reach <address...> where this harbor is reached, as
|
|
34
37
|
// every invitation it gives says
|
|
@@ -59,8 +62,8 @@ import { edgeDev } from './edge.js';
|
|
|
59
62
|
import { carry, isServed, land, open, pack, request, serve } from './harbor.js';
|
|
60
63
|
// Where `edge dev` answers unless --port names another, wrangler's own.
|
|
61
64
|
const EDGE_PORT = 8787;
|
|
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]';
|
|
63
|
-
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
|
|
65
|
+
const USAGE = 'usage: nervur <init|serve|module|modules|census|sweep|open|host|move|route|reach|boot|unboot|public|invite|ask|own|disown|pilot|export|import|edge> [args] [--dir D] [--ward W] [--via V] [--seed S] [--store K] [--with-seed] [--host H] [--port P]';
|
|
66
|
+
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'store', 'host', 'port']);
|
|
64
67
|
// The one flag that stands alone, since what it names is a yes.
|
|
65
68
|
const ALONE = 'with-seed';
|
|
66
69
|
export const parse = (argv) => {
|
|
@@ -120,7 +123,11 @@ export const requestOf = (command, words, flags) => {
|
|
|
120
123
|
}
|
|
121
124
|
case 'host': {
|
|
122
125
|
const [ward] = need(1);
|
|
123
|
-
return { method: 'host', args: { ward: ward, ...(flags.seed === undefined ? {} : { seed: flags.seed }) } };
|
|
126
|
+
return { method: 'host', args: { ward: ward, ...(flags.seed === undefined ? {} : { seed: flags.seed }), ...(flags.store === undefined ? {} : { store: flags.store }) } };
|
|
127
|
+
}
|
|
128
|
+
case 'move': {
|
|
129
|
+
const [ward, store] = need(2);
|
|
130
|
+
return { method: 'move', args: { ward: ward, store: store === 'root' ? null : store } };
|
|
124
131
|
}
|
|
125
132
|
case 'route': {
|
|
126
133
|
const [ward, ...where] = words;
|
|
@@ -200,15 +207,15 @@ export const main = async (argv, env, print) => {
|
|
|
200
207
|
return out({ landed: await land(dir, JSON.parse(text)) });
|
|
201
208
|
}
|
|
202
209
|
if (command === 'edge') {
|
|
203
|
-
if ((words[0] !== 'pack' && words[0] !== 'dev') || words.length !==
|
|
204
|
-
throw new Error('edge takes pack or dev, and
|
|
210
|
+
if ((words[0] !== 'pack' && words[0] !== 'dev') || words.length !== 3)
|
|
211
|
+
throw new Error('edge takes pack or dev, a file, and the harbor name');
|
|
205
212
|
if (flags.via !== undefined)
|
|
206
213
|
throw new Error(`edge ${words[0]} reads this folder, so it takes no --via`);
|
|
207
214
|
if (await isServed(dir))
|
|
208
215
|
throw new Error(`a harbor is served from ${dir}: stop it first`);
|
|
209
216
|
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));
|
|
217
|
+
return out(await pack(dir, words[1], words[2], env.NERVUR_SECRET));
|
|
218
|
+
const dev = await edgeDev(dir, words[1], words[2], process.cwd(), flags.port === undefined ? EDGE_PORT : Number(flags.port), env.NERVUR_SECRET);
|
|
212
219
|
const stop = () => void dev.close().then(() => process.exit(0));
|
|
213
220
|
process.once('SIGINT', stop);
|
|
214
221
|
process.once('SIGTERM', stop);
|
|
@@ -231,7 +238,8 @@ export const main = async (argv, env, print) => {
|
|
|
231
238
|
const [name, invitation, at] = words;
|
|
232
239
|
const held = JSON.parse(invitation);
|
|
233
240
|
const census = (await request(dir, {}));
|
|
234
|
-
const
|
|
241
|
+
const beings = census.answer?.notes?.beings;
|
|
242
|
+
const web = beings !== undefined && Object.hasOwn(beings, WEB_OPENING.key);
|
|
235
243
|
for (const req of pilotRequests(name, held, at, web)) {
|
|
236
244
|
const answer = await request(dir, req);
|
|
237
245
|
if (!failed(answer))
|
package/dist/cli/edge.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export type EdgeDev = {
|
|
|
4
4
|
readonly places: number;
|
|
5
5
|
close(): Promise<void>;
|
|
6
6
|
};
|
|
7
|
-
export declare const edgeDev: (dir: string, file: string, cwd: string, port: number) => Promise<EdgeDev>;
|
|
7
|
+
export declare const edgeDev: (dir: string, file: string, name: string, cwd: string, port: number, named?: string) => Promise<EdgeDev>;
|
package/dist/cli/edge.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// `nervur edge dev`: an edge harbor on this machine, as it will stand on
|
|
3
|
-
// Cloudflare. The harbor of a folder is packed
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// Cloudflare. The harbor of a folder is packed under its name into the
|
|
4
|
+
// pack the worker imports, beside the harbors the pack holds already, and
|
|
5
|
+
// the worker's own `wrangler dev` runs them all, in the folder the command
|
|
6
|
+
// runs from, with the secret handed as `NERVUR_SECRET` through the
|
|
7
|
+
// environment so no listing of processes shows it. The secret is the one
|
|
8
|
+
// the command's environment names, or one drawn once and kept under
|
|
9
|
+
// `.nervur-edge`, beside the objects' storage, so a harbor landed once
|
|
10
|
+
// stands again across restarts, as it does on Cloudflare.
|
|
9
11
|
//
|
|
10
12
|
// wrangler is the worker's tool and never the kit's: the one who deploys
|
|
11
13
|
// an edge has it already, and `edge dev` finds it where npm put it.
|
|
12
14
|
import { spawn } from 'node:child_process';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
17
|
import { join } from 'node:path';
|
|
18
|
+
import { hex } from '../crypto/index.js';
|
|
19
|
+
import { CryptoEntropy } from '../pointer/index.js';
|
|
14
20
|
import { pack } from './harbor.js';
|
|
15
21
|
export const EDGE_STATE = '.nervur-edge';
|
|
16
22
|
const READY_TRIES = 240;
|
|
@@ -30,17 +36,30 @@ const ready = async (child, url, said) => {
|
|
|
30
36
|
}
|
|
31
37
|
throw new Error(`wrangler dev never answered at ${url}: ${said()}`);
|
|
32
38
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
// The secret `edge dev` runs under: the one named, or the one kept here.
|
|
40
|
+
const devSecret = async (state, named) => {
|
|
41
|
+
if (named)
|
|
42
|
+
return named;
|
|
43
|
+
const file = join(state, 'secret');
|
|
44
|
+
if (!existsSync(file)) {
|
|
45
|
+
await mkdir(state, { recursive: true, mode: 0o700 });
|
|
46
|
+
await writeFile(file, `${hex(new CryptoEntropy().draw(32))}\n`, { mode: 0o600, flag: 'wx' });
|
|
47
|
+
}
|
|
48
|
+
return (await readFile(file, 'utf8')).trim();
|
|
49
|
+
};
|
|
50
|
+
export const edgeDev = async (dir, file, name, cwd, port, named) => {
|
|
51
|
+
const state = join(cwd, EDGE_STATE);
|
|
52
|
+
const secret = await devSecret(state, named);
|
|
53
|
+
const { places } = (await pack(dir, file, name, secret));
|
|
54
|
+
const child = spawn('npx', ['--no-install', 'wrangler', 'dev', '--ip', '127.0.0.1', '--port', String(port), '--persist-to', state, '--log-level', 'warn'], {
|
|
36
55
|
cwd,
|
|
37
|
-
env: { ...process.env,
|
|
56
|
+
env: { ...process.env, NERVUR_SECRET: secret, CLOUDFLARE_INCLUDE_PROCESS_ENV: 'true' },
|
|
38
57
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
39
58
|
detached: true,
|
|
40
59
|
});
|
|
41
60
|
let err = '';
|
|
42
61
|
child.stderr.on('data', (chunk) => (err = `${err}${chunk.toString('utf8')}`.slice(-4000)));
|
|
43
|
-
const at = `http://127.0.0.1:${String(port)}/quo`;
|
|
62
|
+
const at = `http://127.0.0.1:${String(port)}/${name}/quo`;
|
|
44
63
|
const close = () => new Promise((done) => {
|
|
45
64
|
if (child.exitCode !== null)
|
|
46
65
|
return done();
|
package/dist/cli/harbor.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ 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
6
|
export declare const carry: (dir: string, seed: boolean) => Promise<Carried>;
|
|
7
|
-
export declare const pack: (dir: string, file: string) => Promise<JsonObject>;
|
|
7
|
+
export declare const pack: (dir: string, file: string, name: string, secret: string | undefined) => Promise<JsonObject>;
|
|
8
8
|
export declare const land: (dir: string, value: unknown) => Promise<string[]>;
|
|
9
9
|
export type Served = {
|
|
10
10
|
pk: string;
|
package/dist/cli/harbor.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { hex } from '../crypto/index.js';
|
|
12
|
+
import { hex, unhex } from '../crypto/index.js';
|
|
13
|
+
import { isHarborName, readPack, sealSeed } from '../edge/index.js';
|
|
13
14
|
import { FolderCustody, FolderMemory } from '../folder/index.js';
|
|
14
15
|
import { carryPlaces, Harbor, landPlaces, readCarried, rootLine } from '../harbor/index.js';
|
|
15
16
|
import { askServed, MODULES, NodeTerrain, socketOf } from '../node/index.js';
|
|
@@ -45,14 +46,25 @@ export const carry = async (dir, seed) => {
|
|
|
45
46
|
...(modules ? { modules } : {}),
|
|
46
47
|
};
|
|
47
48
|
};
|
|
48
|
-
// What `nervur edge pack` hands a deploy: the
|
|
49
|
-
//
|
|
50
|
-
|
|
49
|
+
// What `nervur edge pack` hands a deploy: the harbor of a folder, under
|
|
50
|
+
// its name, in the pack the worker bundles, its seed sealed under the
|
|
51
|
+
// worker's `NERVUR_SECRET` beside its sealed places. Every other harbor
|
|
52
|
+
// the pack holds stays, so one worker carries them all, and nothing in the
|
|
53
|
+
// pack opens without the secret.
|
|
54
|
+
export const pack = async (dir, file, name, secret) => {
|
|
55
|
+
if (!isHarborName(name))
|
|
56
|
+
throw new Error(`${name} is no harbor name: lowercase letters, digits and -`);
|
|
57
|
+
if (!secret)
|
|
58
|
+
throw new Error('edge pack seals the seed under NERVUR_SECRET, and none is set');
|
|
51
59
|
if (!existsSync(join(dir, 'seed')))
|
|
52
60
|
throw new Error(`no harbor stands at ${dir}`);
|
|
53
61
|
const { seed, places } = await carry(dir, true);
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
const held = existsSync(file) ? readPack(JSON.parse(await readFile(file, 'utf8'))) : {};
|
|
63
|
+
if (held === null)
|
|
64
|
+
throw new Error(`${file} is no pack`);
|
|
65
|
+
const packed = { ...held, [name]: { sealed: await sealSeed(secret, unhex(seed)), places } };
|
|
66
|
+
await writeFile(file, `${JSON.stringify(packed)}\n`, { mode: 0o600 });
|
|
67
|
+
return { packed: name, package: file, harbors: Object.keys(packed).sort(), places: Object.keys(places).length };
|
|
56
68
|
};
|
|
57
69
|
// A carried harbor into an empty folder. A folder holding a seed or a
|
|
58
70
|
// place is left as it stands, and a package with no seed needs one there
|
package/dist/edge/index.d.ts
CHANGED
|
@@ -18,14 +18,21 @@ export interface EdgeState {
|
|
|
18
18
|
blockConcurrencyWhile<T>(work: () => Promise<T>): Promise<T>;
|
|
19
19
|
}
|
|
20
20
|
export type EdgeEnv = {
|
|
21
|
-
readonly NERVUR_SEED?: string;
|
|
22
21
|
readonly NERVUR_SECRET?: string;
|
|
23
22
|
};
|
|
24
23
|
export type EdgeGiven = {
|
|
25
24
|
readonly state: EdgeState;
|
|
26
25
|
readonly env: EdgeEnv;
|
|
27
|
-
readonly package?:
|
|
26
|
+
readonly package?: Packed;
|
|
28
27
|
};
|
|
28
|
+
export type Packed = {
|
|
29
|
+
readonly sealed: string;
|
|
30
|
+
readonly places: Carried['places'];
|
|
31
|
+
};
|
|
32
|
+
export type Pack = Record<string, Packed>;
|
|
33
|
+
export declare const isHarborName: (name: unknown) => name is string;
|
|
34
|
+
export declare const readPack: (value: unknown) => Pack | null;
|
|
35
|
+
export declare const sealSeed: (secret: string, seed: Uint8Array, entropy?: Entropy) => Promise<string>;
|
|
29
36
|
export declare class EdgeMemory extends Memory {
|
|
30
37
|
#private;
|
|
31
38
|
constructor(storage: EdgeStorage, root?: string);
|
|
@@ -34,13 +41,11 @@ export declare class EdgeMemory extends Memory {
|
|
|
34
41
|
places(): Promise<string[]>;
|
|
35
42
|
forget(place: string): Promise<void>;
|
|
36
43
|
}
|
|
37
|
-
export type EdgeKeys = {
|
|
38
|
-
readonly seed?: string | undefined;
|
|
39
|
-
readonly secret?: string | undefined;
|
|
40
|
-
};
|
|
41
44
|
export declare class EdgeCustody extends Custody {
|
|
42
45
|
#private;
|
|
43
|
-
constructor(storage: EdgeStorage,
|
|
46
|
+
constructor(storage: EdgeStorage, secret: string | undefined, entropy?: Entropy, root?: string);
|
|
47
|
+
kept(): Promise<boolean>;
|
|
48
|
+
land(sealed: string, txn?: EdgeStorage): Promise<void>;
|
|
44
49
|
seed(): Promise<Uint8Array>;
|
|
45
50
|
}
|
|
46
51
|
export declare const edgeTcp: GroundFaculty;
|
|
@@ -58,14 +63,19 @@ export declare class EdgeTerrain extends PointerTerrain {
|
|
|
58
63
|
answer(request: Request): Promise<Response>;
|
|
59
64
|
}
|
|
60
65
|
export declare const edgeGround: GroundProbe;
|
|
66
|
+
export declare const harborNameOf: (request: Request) => string | null;
|
|
61
67
|
export type HarborObjectParts = {
|
|
62
68
|
readonly modules: readonly Module[];
|
|
63
|
-
readonly
|
|
64
|
-
readonly
|
|
65
|
-
readonly stood?: (harbor: Harbor) => Promise<void>;
|
|
69
|
+
readonly pack?: Pack | ((env: EdgeEnv) => Pack | undefined);
|
|
70
|
+
readonly born?: (name: string) => boolean;
|
|
71
|
+
readonly stood?: (harbor: Harbor, name: string) => Promise<void>;
|
|
66
72
|
};
|
|
67
|
-
export type
|
|
68
|
-
|
|
73
|
+
export type HarborObject = {
|
|
74
|
+
harborFor(request: Request): Promise<Harbor | null>;
|
|
69
75
|
fetch(request: Request): Promise<Response>;
|
|
70
76
|
};
|
|
71
|
-
export
|
|
77
|
+
export type HarborObjectClass = new (state: EdgeState, env: EdgeEnv) => HarborObject;
|
|
78
|
+
export declare const harborObject: ({ modules, pack, born, stood }: HarborObjectParts) => HarborObjectClass;
|
|
79
|
+
export declare const harborWorker: (binding?: string) => {
|
|
80
|
+
fetch(request: Request, env: Record<string, unknown>): Promise<Response>;
|
|
81
|
+
};
|
package/dist/edge/index.js
CHANGED
|
@@ -7,8 +7,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
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, landPlaces, UNDIALED } from '../harbor/index.js';
|
|
10
|
+
import { concat, decrypt, encrypt, hex, hkdf, isHex, unhex, utf8 } from '../crypto/index.js';
|
|
11
|
+
import { addGround, CarrierFaculty, Harbor, landPlaces, readCarried, 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';
|
|
@@ -20,6 +20,30 @@ const SEED = 'c/seed';
|
|
|
20
20
|
const BATCH = 128;
|
|
21
21
|
const SEAL_INFO = utf8('nervur edge custody');
|
|
22
22
|
const MAX_ID = 0xffff_ffff;
|
|
23
|
+
// A harbor's name on the edge: one path segment, so it is where the
|
|
24
|
+
// harbor answers, `/<name>/quo`.
|
|
25
|
+
export const isHarborName = (name) => typeof name === 'string' && /^[a-z0-9][a-z0-9-]{0,62}$/.test(name);
|
|
26
|
+
const NONCE = 12;
|
|
27
|
+
// A pack as JSON reads, or null for anything else.
|
|
28
|
+
export const readPack = (value) => {
|
|
29
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
30
|
+
return null;
|
|
31
|
+
const pack = {};
|
|
32
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
33
|
+
const { sealed, places } = (entry ?? {});
|
|
34
|
+
const carried = readCarried({ places });
|
|
35
|
+
if (!isHarborName(name) || !isHex(sealed) || sealed.length <= NONCE * 2 || carried === null)
|
|
36
|
+
return null;
|
|
37
|
+
pack[name] = { sealed: sealed, places: carried.places };
|
|
38
|
+
}
|
|
39
|
+
return pack;
|
|
40
|
+
};
|
|
41
|
+
const sealKey = (secret) => hkdf(utf8(secret), SEAL_INFO, 32);
|
|
42
|
+
// A seed sealed under a worker's secret, as a pack keeps it.
|
|
43
|
+
export const sealSeed = async (secret, seed, entropy = new CryptoEntropy()) => {
|
|
44
|
+
const nonce = entropy.draw(NONCE);
|
|
45
|
+
return hex(concat(nonce, await encrypt(await sealKey(secret), nonce, new Uint8Array(0), seed)));
|
|
46
|
+
};
|
|
23
47
|
const chunks = (items) => Array.from({ length: Math.ceil(items.length / BATCH) }, (_, i) => items.slice(i * BATCH, (i + 1) * BATCH));
|
|
24
48
|
const bytesOf = (value) => new Uint8Array(value instanceof ArrayBuffer ? value : value);
|
|
25
49
|
// A body keeps its keys under `root`, so one object may hold more than one
|
|
@@ -66,28 +90,34 @@ export class EdgeMemory extends Memory {
|
|
|
66
90
|
});
|
|
67
91
|
}
|
|
68
92
|
}
|
|
93
|
+
// The seed, kept sealed under a key the worker's secret gives, so the
|
|
94
|
+
// storage alone opens nothing: the one a pack landed, or one drawn the
|
|
95
|
+
// first time it is asked for. A worker with no secret stands no harbor.
|
|
69
96
|
export class EdgeCustody extends Custody {
|
|
70
97
|
#storage;
|
|
71
|
-
#
|
|
98
|
+
#secret;
|
|
72
99
|
#entropy;
|
|
73
100
|
#key;
|
|
74
|
-
constructor(storage,
|
|
101
|
+
constructor(storage, secret, entropy = new CryptoEntropy(), root = '') {
|
|
75
102
|
super();
|
|
76
103
|
this.#storage = storage;
|
|
77
|
-
this.#
|
|
104
|
+
this.#secret = secret;
|
|
78
105
|
this.#entropy = entropy;
|
|
79
106
|
this.#key = `${root}${SEED}`;
|
|
80
107
|
}
|
|
108
|
+
// Whether a seed is kept, so a pack lands only where none is.
|
|
109
|
+
async kept() {
|
|
110
|
+
return (await this.#storage.get(this.#key)) !== undefined;
|
|
111
|
+
}
|
|
112
|
+
// A packed seed, sealed as `sealSeed` sealed it, kept as it came.
|
|
113
|
+
async land(sealed, txn = this.#storage) {
|
|
114
|
+
const bytes = unhex(sealed);
|
|
115
|
+
await txn.put({ [this.#key]: { nonce: bytes.slice(0, NONCE), sealed: bytes.slice(NONCE) } });
|
|
116
|
+
}
|
|
81
117
|
async seed() {
|
|
82
|
-
|
|
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)
|
|
118
|
+
if (!this.#secret)
|
|
89
119
|
throw new Error('the worker holds no NERVUR_SECRET');
|
|
90
|
-
const key = await
|
|
120
|
+
const key = await sealKey(this.#secret);
|
|
91
121
|
return this.#storage.transaction(async (txn) => {
|
|
92
122
|
const kept = (await txn.get(this.#key));
|
|
93
123
|
if (kept) {
|
|
@@ -97,7 +127,7 @@ export class EdgeCustody extends Custody {
|
|
|
97
127
|
return seed;
|
|
98
128
|
}
|
|
99
129
|
const seed = this.#entropy.draw(32);
|
|
100
|
-
const nonce = this.#entropy.draw(
|
|
130
|
+
const nonce = this.#entropy.draw(NONCE);
|
|
101
131
|
await txn.put({ [this.#key]: { nonce, sealed: await encrypt(key, nonce, new Uint8Array(0), seed) } });
|
|
102
132
|
return seed.slice();
|
|
103
133
|
});
|
|
@@ -245,24 +275,24 @@ export class EdgeTerrain extends PointerTerrain {
|
|
|
245
275
|
super({
|
|
246
276
|
entropy,
|
|
247
277
|
loader: new HeldLoader(parts.modules ?? []),
|
|
248
|
-
|
|
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),
|
|
278
|
+
custody: new EdgeCustody(storage, parts.given.env.NERVUR_SECRET, entropy),
|
|
251
279
|
memory: new EdgeMemory(storage),
|
|
252
280
|
faculties: [edgeTcp, webFaculty(edgeServe), webPush],
|
|
253
281
|
opens: [TCP_OPENING, WEB_OPENING],
|
|
254
282
|
});
|
|
255
283
|
this.given = parts.given;
|
|
256
284
|
}
|
|
257
|
-
// A
|
|
258
|
-
//
|
|
285
|
+
// A packed harbor lands where the object keeps no seed yet: its places
|
|
286
|
+
// first and its seed last, so a start cut short lands it whole again,
|
|
287
|
+
// and once the seed is kept the storage is the harbor and a new deploy
|
|
288
|
+
// leaves it be. A seed sealed under another secret does not open.
|
|
259
289
|
async claim() {
|
|
260
|
-
const
|
|
261
|
-
|
|
290
|
+
const packed = this.given.package;
|
|
291
|
+
const custody = this.custody;
|
|
292
|
+
if (!packed || (await custody.kept()))
|
|
262
293
|
return;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
await landPlaces(this.memory, carried);
|
|
294
|
+
await landPlaces(this.memory, { places: packed.places });
|
|
295
|
+
await custody.land(packed.sealed);
|
|
266
296
|
}
|
|
267
297
|
stand(harbor) {
|
|
268
298
|
this.#harbor = harbor;
|
|
@@ -299,32 +329,56 @@ export const edgeGround = {
|
|
|
299
329
|
fits: ({ given }) => isGiven(given),
|
|
300
330
|
terrain: ({ given, modules }) => new EdgeTerrain({ given: given, ...(modules ? { modules } : {}) }),
|
|
301
331
|
};
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
332
|
+
// The harbor a request is for: the first segment of its path.
|
|
333
|
+
export const harborNameOf = (request) => {
|
|
334
|
+
const name = new URL(request.url).pathname.split('/')[1];
|
|
335
|
+
return isHarborName(name) ? name : null;
|
|
336
|
+
};
|
|
337
|
+
// A Durable Object class whose every instance is one harbor, by the name
|
|
338
|
+
// the worker reached it under: opened at its first request, the one its
|
|
339
|
+
// pack holds under that name or one `born` allows, its web carrier opened,
|
|
340
|
+
// and every request it receives answered by that carrier at
|
|
341
|
+
// `/<name>/quo`. An object answers the one name it opened under, and a
|
|
342
|
+
// name it may not open is 404, with nothing written.
|
|
343
|
+
export const harborObject = ({ modules, pack, born, stood }) => class {
|
|
344
|
+
#state;
|
|
345
|
+
#env;
|
|
346
|
+
#name;
|
|
306
347
|
#opening;
|
|
307
|
-
// The object's harbor, once it stands.
|
|
308
|
-
get harbor() {
|
|
309
|
-
return this.#opening;
|
|
310
|
-
}
|
|
311
348
|
constructor(state, env) {
|
|
312
349
|
addGround(edgeGround);
|
|
313
|
-
|
|
314
|
-
|
|
350
|
+
this.#state = state;
|
|
351
|
+
this.#env = env;
|
|
352
|
+
}
|
|
353
|
+
// The harbor a request names, opened once, or null where it names
|
|
354
|
+
// another harbor or none.
|
|
355
|
+
harborFor(request) {
|
|
356
|
+
const name = harborNameOf(request);
|
|
357
|
+
if (name === null || (this.#name !== undefined && this.#name !== name))
|
|
358
|
+
return Promise.resolve(null);
|
|
359
|
+
const packed = typeof pack === 'function' ? pack(this.#env) : pack;
|
|
360
|
+
const held = packed && Object.hasOwn(packed, name) ? packed[name] : undefined;
|
|
361
|
+
if (this.#name === undefined && !held && !born?.(name))
|
|
362
|
+
return Promise.resolve(null);
|
|
363
|
+
this.#name = name;
|
|
315
364
|
// Opened inside the object's block, so no request reaches it half
|
|
316
365
|
// open and its storage answers the opening.
|
|
317
|
-
this.#opening
|
|
366
|
+
this.#opening ??= this.#state.blockConcurrencyWhile(async () => {
|
|
367
|
+
const given = { state: this.#state, env: this.#env, ...(held ? { package: held } : {}) };
|
|
318
368
|
const harbor = await Harbor.open({ modules, given });
|
|
319
369
|
if (!harbor.box.being(WEB_OPENING.key))
|
|
320
370
|
await harbor.root('open', { key: WEB_OPENING.key, class: WEB_OPENING.kind });
|
|
321
|
-
await stood?.(harbor);
|
|
371
|
+
await stood?.(harbor, name);
|
|
322
372
|
return harbor;
|
|
323
373
|
});
|
|
374
|
+
return this.#opening;
|
|
324
375
|
}
|
|
325
376
|
async fetch(request) {
|
|
326
|
-
const harbor = await this
|
|
377
|
+
const harbor = await this.harborFor(request);
|
|
378
|
+
if (!harbor)
|
|
379
|
+
return new Response(null, { status: 404 });
|
|
327
380
|
const terrain = harbor.terrain;
|
|
381
|
+
const path = `/${this.#name}/quo`;
|
|
328
382
|
if (terrain.listening?.path !== path) {
|
|
329
383
|
const url = new URL(request.url);
|
|
330
384
|
await harbor.root('ask', { being: WEB_OPENING.key, method: 'listen', args: { host: url.hostname, port: url.port === '' ? 0 : Number(url.port), path } });
|
|
@@ -332,3 +386,15 @@ export const harborObject = ({ modules, package: carried, path = '/quo', stood }
|
|
|
332
386
|
return terrain.answer(request);
|
|
333
387
|
}
|
|
334
388
|
};
|
|
389
|
+
// A worker of many harbors: each request goes to the object of the
|
|
390
|
+
// harbor its path names, in the Durable Object namespace bound as
|
|
391
|
+
// `binding`, and a path naming none is 404.
|
|
392
|
+
export const harborWorker = (binding = 'HARBOR') => ({
|
|
393
|
+
fetch(request, env) {
|
|
394
|
+
const name = harborNameOf(request);
|
|
395
|
+
const objects = env[binding];
|
|
396
|
+
if (name === null || !objects)
|
|
397
|
+
return Promise.resolve(new Response(null, { status: 404 }));
|
|
398
|
+
return objects.get(objects.idFromName(name)).fetch(request);
|
|
399
|
+
},
|
|
400
|
+
});
|