nervur 0.20.0 → 0.20.1
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 +107 -20
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +5 -1
- package/dist/being/faculty.d.ts +1 -0
- package/dist/being/faculty.js +13 -7
- package/dist/being/index.d.ts +1 -0
- package/dist/being/index.js +1 -0
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +3 -4
- package/dist/cli/command.d.ts +13 -0
- package/dist/cli/command.js +194 -0
- package/dist/cli/harbor.d.ts +21 -0
- package/dist/cli/harbor.js +141 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +15 -2
- package/dist/contract/index.js +16 -2
- package/dist/folder/index.d.ts +5 -4
- package/dist/folder/index.js +45 -83
- package/dist/harbor/catalogue.d.ts +25 -0
- package/dist/harbor/catalogue.js +93 -0
- package/dist/harbor/dock.d.ts +12 -2
- package/dist/harbor/dock.js +39 -9
- package/dist/harbor/harbor.d.ts +15 -6
- package/dist/harbor/harbor.js +83 -21
- package/dist/harbor/index.d.ts +4 -1
- package/dist/harbor/index.js +5 -1
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +115 -0
- package/dist/harbor/registry.d.ts +5 -3
- package/dist/harbor/registry.js +72 -7
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +36 -0
- package/dist/harbor/terrain.d.ts +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/pointer/bodies.d.ts +9 -3
- package/dist/pointer/bodies.js +37 -18
- package/dist/pointer/index.d.ts +1 -1
- package/dist/pointer/index.js +1 -1
- package/dist/pointer/world.d.ts +5 -4
- package/dist/pointer/world.js +23 -14
- package/dist/quo/door.js +2 -2
- package/dist/quo/keys.js +1 -1
- package/dist/tcp/frame.d.ts +29 -0
- package/dist/tcp/frame.js +74 -0
- package/dist/tcp/index.d.ts +18 -0
- package/dist/tcp/index.js +164 -0
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +1 -0
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +2 -4
- package/dist/ward/stance.js +7 -6
- package/dist/ward/ward-being.d.ts +2 -0
- package/dist/ward/ward-being.js +7 -1
- package/dist/ward/ward.d.ts +4 -2
- package/dist/ward/ward.js +35 -5
- package/package.json +8 -1
- package/src/being/being.ts +5 -1
- package/src/being/faculty.ts +14 -7
- package/src/being/index.ts +1 -0
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +8 -6
- package/src/cli/command.ts +188 -0
- package/src/cli/harbor.ts +137 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +38 -9
- package/src/folder/index.ts +44 -86
- package/src/harbor/catalogue.ts +110 -0
- package/src/harbor/dock.ts +48 -12
- package/src/harbor/harbor.ts +104 -29
- package/src/harbor/index.ts +6 -2
- package/src/harbor/package.ts +131 -0
- package/src/harbor/registry.ts +62 -9
- package/src/harbor/root-line.ts +36 -0
- package/src/harbor/terrain.ts +3 -1
- package/src/index.ts +5 -5
- package/src/pointer/bodies.ts +34 -16
- package/src/pointer/index.ts +1 -1
- package/src/pointer/world.ts +21 -18
- package/src/quo/door.ts +2 -2
- package/src/quo/keys.ts +1 -1
- package/src/stand/main.ts +1 -0
- package/src/stand/stand.ts +46 -4
- package/src/tcp/frame.ts +78 -0
- package/src/tcp/index.ts +173 -0
- package/src/ward/index.ts +1 -0
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +8 -7
- package/src/ward/ward-being.ts +7 -1
- package/src/ward/ward.ts +37 -6
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
// 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
|
|
20
|
+
//
|
|
21
|
+
// A root request goes to a served harbor over its socket. With none served,
|
|
22
|
+
// the command unpacks the harbor from its folder, asks, and closes it, so
|
|
23
|
+
// 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';
|
|
31
|
+
import { FolderCustody, FolderMemory } from '../folder/index.js';
|
|
32
|
+
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
|
+
});
|
|
86
|
+
export const request = async (dir, req) => {
|
|
87
|
+
const served = await overSocket(dir, req);
|
|
88
|
+
if (served !== undefined)
|
|
89
|
+
return served;
|
|
90
|
+
const { harbor, carrier } = await open(dir);
|
|
91
|
+
try {
|
|
92
|
+
return await rootLine(harbor, req);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await carrier.close();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
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.
|
|
100
|
+
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
|
+
};
|
|
141
|
+
};
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// The `nervur` program: `command.ts` over this process's arguments. A
|
|
4
|
+
// command that cannot run exits 2 and says why on stderr.
|
|
5
|
+
import { main, SERVING } from './command.js';
|
|
6
|
+
try {
|
|
7
|
+
const code = await main(process.argv.slice(2), process.env, (line) => process.stdout.write(`${line}\n`));
|
|
8
|
+
if (code !== SERVING)
|
|
9
|
+
process.exitCode = code;
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
process.stderr.write(`nervur: ${e.message}\n`);
|
|
13
|
+
process.exitCode = 2;
|
|
14
|
+
}
|
package/dist/contract/index.d.ts
CHANGED
|
@@ -16,11 +16,24 @@ export interface Rows {
|
|
|
16
16
|
kept(): Promise<boolean>;
|
|
17
17
|
}
|
|
18
18
|
export declare abstract class Memory {
|
|
19
|
-
abstract
|
|
19
|
+
abstract read(place: string): Promise<Map<string, Uint8Array>>;
|
|
20
|
+
abstract write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
|
|
20
21
|
}
|
|
21
22
|
export declare abstract class Custody {
|
|
22
|
-
abstract seed(
|
|
23
|
+
abstract seed(): Promise<Uint8Array>;
|
|
24
|
+
}
|
|
25
|
+
export type Module = {
|
|
26
|
+
readonly module: string;
|
|
27
|
+
readonly version: string;
|
|
28
|
+
readonly classes: readonly unknown[];
|
|
29
|
+
};
|
|
30
|
+
export type Loaded = Module & {
|
|
31
|
+
readonly from: string;
|
|
32
|
+
};
|
|
33
|
+
export declare abstract class Loader {
|
|
34
|
+
abstract load(from: string): Promise<Loaded | null>;
|
|
23
35
|
}
|
|
24
36
|
export declare abstract class Carrier {
|
|
25
37
|
abstract carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
38
|
+
route(_pk: string, _at: string | null): boolean;
|
|
26
39
|
}
|
package/dist/contract/index.js
CHANGED
|
@@ -8,12 +8,26 @@ export class Entropy {
|
|
|
8
8
|
// The moment, and a wait that ends.
|
|
9
9
|
export class Clock {
|
|
10
10
|
}
|
|
11
|
+
// A harbor's memory as a terrain keeps it: sealed entries it cannot read,
|
|
12
|
+
// in places, each entry by name. Every name is lowercase hex the kit
|
|
13
|
+
// chose, so a body may use it as a file name as it stands.
|
|
11
14
|
export class Memory {
|
|
12
15
|
}
|
|
13
|
-
//
|
|
14
|
-
//
|
|
16
|
+
// The harbor's seed: thirty-two bytes, the same every time. It is the one
|
|
17
|
+
// secret a terrain holds.
|
|
15
18
|
export class Custody {
|
|
16
19
|
}
|
|
20
|
+
// Code, by the name a harbor keeps for it, or null where this loader has
|
|
21
|
+
// none. Code never crosses a door and never lives in a cell, so it is the
|
|
22
|
+
// one thing beside the seed a terrain hands in: a map, an import, a bundle.
|
|
23
|
+
export class Loader {
|
|
24
|
+
}
|
|
17
25
|
// Bytes to a ward pk in; bytes, or null for nothing, out.
|
|
18
26
|
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
|
+
}
|
|
19
33
|
}
|
package/dist/folder/index.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { Custody, Memory, type Entropy
|
|
2
|
-
export declare const folderName: (name: string) => string;
|
|
1
|
+
import { Custody, Memory, type Entropy } from '../contract/index.ts';
|
|
3
2
|
export declare class FolderMemory extends Memory {
|
|
3
|
+
#private;
|
|
4
4
|
readonly root: string;
|
|
5
5
|
constructor(root: string);
|
|
6
|
-
|
|
6
|
+
read(place: string): Promise<Map<string, Uint8Array>>;
|
|
7
|
+
write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
|
|
7
8
|
}
|
|
8
9
|
export declare class FolderCustody extends Custody {
|
|
9
10
|
#private;
|
|
10
11
|
readonly root: string;
|
|
11
12
|
constructor(root: string, entropy: Entropy);
|
|
12
|
-
seed(
|
|
13
|
+
seed(): Promise<Uint8Array>;
|
|
13
14
|
}
|
package/dist/folder/index.js
CHANGED
|
@@ -1,102 +1,64 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// A harbor on a disk, for local proof: `nervur/folder`,
|
|
2
|
+
// A harbor on a disk, for local proof: `nervur/folder`, an entry that
|
|
3
3
|
// names a platform.
|
|
4
4
|
//
|
|
5
|
-
// <root
|
|
6
|
-
// <root>/<
|
|
7
|
-
// <root>/<ward>/<key>/being.json one being: class, cells, relations
|
|
5
|
+
// <root>/seed the harbor's seed, as hex
|
|
6
|
+
// <root>/<place>/<entry> one sealed entry of the harbor's memory
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// then renamed over, so a crash leaves the last
|
|
12
|
-
//
|
|
8
|
+
// Places and entries are the hex names the kit chose, so no name reaches
|
|
9
|
+
// outside its folder, and the disk shows no ward's name. A file is written
|
|
10
|
+
// whole, beside itself and then renamed over, so a crash leaves the last
|
|
11
|
+
// kept copy. The seed is a plain file: custody that seals it is a
|
|
12
|
+
// terrain's, in the dock.
|
|
13
13
|
import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { hex, isHex, unhex } from '../crypto/index.js';
|
|
16
16
|
import { Custody, Memory } from '../contract/index.js';
|
|
17
|
-
import { HEAD_ROW } from '../ward/index.js';
|
|
18
|
-
const HEAD = 'ward.json';
|
|
19
|
-
const BEING = 'being.json';
|
|
20
|
-
export const folderName = (name) => encodeURIComponent(name).replaceAll('.', '%2E').replaceAll('*', '%2A') || '%';
|
|
21
|
-
const nameOf = (folder) => (folder === '%' ? '' : decodeURIComponent(folder));
|
|
22
17
|
const missing = (e) => e.code === 'ENOENT';
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
catch (e) {
|
|
28
|
-
if (missing(e))
|
|
29
|
-
return undefined;
|
|
30
|
-
throw e;
|
|
31
|
-
}
|
|
18
|
+
const named = (name) => {
|
|
19
|
+
if (!isHex(name) || name === '')
|
|
20
|
+
throw new Error(`${name} is not a name the kit chose`);
|
|
21
|
+
return name;
|
|
32
22
|
};
|
|
33
|
-
const writeWhole = async (path,
|
|
34
|
-
await writeFile(`${path}.next`,
|
|
23
|
+
const writeWhole = async (path, bytes) => {
|
|
24
|
+
await writeFile(`${path}.next`, bytes, { mode: 0o600 });
|
|
35
25
|
await rename(`${path}.next`, path);
|
|
36
26
|
};
|
|
37
|
-
class FolderRows {
|
|
38
|
-
rows;
|
|
39
|
-
#dir;
|
|
40
|
-
#told = new Set();
|
|
41
|
-
#line = Promise.resolve(true);
|
|
42
|
-
constructor(dir, rows) {
|
|
43
|
-
this.#dir = dir;
|
|
44
|
-
this.rows = rows;
|
|
45
|
-
}
|
|
46
|
-
told(row) {
|
|
47
|
-
this.#told.add(row);
|
|
48
|
-
}
|
|
49
|
-
// Every row told since the last keep, written in order, one keep at a time.
|
|
50
|
-
kept() {
|
|
51
|
-
const rows = [...this.#told];
|
|
52
|
-
this.#told.clear();
|
|
53
|
-
this.#line = this.#line.then(() => this.#write(rows)).catch(() => false);
|
|
54
|
-
return this.#line;
|
|
55
|
-
}
|
|
56
|
-
async #write(told) {
|
|
57
|
-
await mkdir(this.#dir, { recursive: true, mode: 0o700 });
|
|
58
|
-
const beings = (this.rows.beings ?? {});
|
|
59
|
-
for (const row of told) {
|
|
60
|
-
if (row === HEAD_ROW) {
|
|
61
|
-
await writeWhole(join(this.#dir, HEAD), JSON.stringify(this.rows.ward ?? null));
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const dir = join(this.#dir, folderName(row));
|
|
65
|
-
if (!Object.hasOwn(beings, row)) {
|
|
66
|
-
await rm(dir, { recursive: true, force: true });
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
70
|
-
await writeWhole(join(dir, BEING), JSON.stringify(beings[row]));
|
|
71
|
-
}
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
27
|
export class FolderMemory extends Memory {
|
|
76
28
|
root;
|
|
29
|
+
#line = Promise.resolve();
|
|
77
30
|
constructor(root) {
|
|
78
31
|
super();
|
|
79
32
|
this.root = root;
|
|
80
33
|
}
|
|
81
|
-
// The
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch((e) => (missing(e) ? [] : Promise.reject(e)));
|
|
90
|
-
for (const entry of entries) {
|
|
91
|
-
if (!entry.isDirectory())
|
|
34
|
+
// The entries as the disk holds them now. A file that is not an entry,
|
|
35
|
+
// one still being written among them, is not read.
|
|
36
|
+
async read(place) {
|
|
37
|
+
const dir = join(this.root, named(place));
|
|
38
|
+
const out = new Map();
|
|
39
|
+
const files = await readdir(dir, { withFileTypes: true }).catch((e) => (missing(e) ? [] : Promise.reject(e)));
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
if (!file.isFile() || !isHex(file.name) || file.name === '')
|
|
92
42
|
continue;
|
|
93
|
-
|
|
94
|
-
if (row !== undefined)
|
|
95
|
-
beings[nameOf(entry.name)] = row;
|
|
43
|
+
out.set(file.name, new Uint8Array(await readFile(join(dir, file.name))));
|
|
96
44
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
// One keep at a time, each entry in the order given.
|
|
48
|
+
async write(place, entries) {
|
|
49
|
+
const dir = join(this.root, named(place));
|
|
50
|
+
const kept = this.#line.then(async () => {
|
|
51
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
52
|
+
for (const [name, bytes] of entries) {
|
|
53
|
+
const path = join(dir, named(name));
|
|
54
|
+
if (bytes === null)
|
|
55
|
+
await rm(path, { force: true });
|
|
56
|
+
else
|
|
57
|
+
await writeWhole(path, bytes);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
this.#line = kept.catch(() => undefined);
|
|
61
|
+
await kept;
|
|
100
62
|
}
|
|
101
63
|
}
|
|
102
64
|
export class FolderCustody extends Custody {
|
|
@@ -107,9 +69,9 @@ export class FolderCustody extends Custody {
|
|
|
107
69
|
this.root = root;
|
|
108
70
|
this.#entropy = entropy;
|
|
109
71
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const path = join(
|
|
72
|
+
// The seed the folder holds, or a drawn one written there the first time.
|
|
73
|
+
async seed() {
|
|
74
|
+
const path = join(this.root, 'seed');
|
|
113
75
|
const kept = await readFile(path, 'utf8').catch((e) => (missing(e) ? undefined : Promise.reject(e)));
|
|
114
76
|
if (kept !== undefined) {
|
|
115
77
|
const text = kept.trim();
|
|
@@ -118,7 +80,7 @@ export class FolderCustody extends Custody {
|
|
|
118
80
|
return unhex(text);
|
|
119
81
|
}
|
|
120
82
|
const seed = this.#entropy.draw(32);
|
|
121
|
-
await mkdir(
|
|
83
|
+
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
|
122
84
|
await writeFile(path, `${hex(seed)}\n`, { mode: 0o600, flag: 'wx' });
|
|
123
85
|
return seed;
|
|
124
86
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Faculty, type AskSpec, type BeingClass, type JsonObject, type Stance } from '../being/index.ts';
|
|
2
|
+
import type { Loaded } from '../contract/index.ts';
|
|
3
|
+
export declare const CATALOGUE = "catalogue";
|
|
4
|
+
export type ModuleRow = {
|
|
5
|
+
version: string;
|
|
6
|
+
from: string;
|
|
7
|
+
};
|
|
8
|
+
export interface Cataloguing {
|
|
9
|
+
load(from: string): Promise<Loaded | null>;
|
|
10
|
+
run(modules: readonly Loaded[]): string | null;
|
|
11
|
+
stands(kinds: readonly string[]): boolean;
|
|
12
|
+
kindsOf(module: string): string[];
|
|
13
|
+
}
|
|
14
|
+
export declare class Catalogue extends Faculty {
|
|
15
|
+
#private;
|
|
16
|
+
static readonly kind: string;
|
|
17
|
+
static cells: JsonObject;
|
|
18
|
+
static asks: Record<string, AskSpec>;
|
|
19
|
+
constructor(stance: Stance, harbor: Cataloguing);
|
|
20
|
+
wake(): Promise<void>;
|
|
21
|
+
add(args: JsonObject): Promise<JsonObject>;
|
|
22
|
+
remove(args: JsonObject): JsonObject;
|
|
23
|
+
modules(): JsonObject;
|
|
24
|
+
}
|
|
25
|
+
export declare const catalogueFor: (harbor: Cataloguing) => BeingClass;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The catalogue: the dock's first faculty, the code a harbor runs. Her
|
|
3
|
+
// cells name each module by the name it declares, its version, and where
|
|
4
|
+
// the terrain's loader finds it. The harbor wakes her before any other
|
|
5
|
+
// faculty, loads every module she names, and refuses to stand when one is
|
|
6
|
+
// missing.
|
|
7
|
+
//
|
|
8
|
+
// add { from } the module the loader finds there, run from now
|
|
9
|
+
// on; the same module again takes its new version
|
|
10
|
+
// remove { module } a module no being stands on, run no more
|
|
11
|
+
// modules what she names, each with the version running
|
|
12
|
+
//
|
|
13
|
+
// She is the kit's, of kind `org.nervur.catalogue`, and answers the root
|
|
14
|
+
// and an owner alone.
|
|
15
|
+
import { Faculty } from '../being/index.js';
|
|
16
|
+
import { ownerAsk } from '../ward/index.js';
|
|
17
|
+
export const CATALOGUE = 'catalogue';
|
|
18
|
+
const text = (v) => typeof v === 'string';
|
|
19
|
+
export class Catalogue extends Faculty {
|
|
20
|
+
static kind = 'org.nervur.catalogue';
|
|
21
|
+
static cells = { modules: {} };
|
|
22
|
+
static asks = {
|
|
23
|
+
add: ownerAsk('a module run from now on', { required: ['from'] }),
|
|
24
|
+
remove: ownerAsk('a module run no more', { required: ['module'] }),
|
|
25
|
+
modules: ownerAsk('the modules this harbor runs'),
|
|
26
|
+
};
|
|
27
|
+
#harbor;
|
|
28
|
+
#running = new Map();
|
|
29
|
+
constructor(stance, harbor) {
|
|
30
|
+
super(stance);
|
|
31
|
+
this.#harbor = harbor;
|
|
32
|
+
}
|
|
33
|
+
get #rows() {
|
|
34
|
+
return this.cells.modules;
|
|
35
|
+
}
|
|
36
|
+
// Every module her cells name, loaded and run, or a throw naming the one
|
|
37
|
+
// that cannot be.
|
|
38
|
+
async wake() {
|
|
39
|
+
for (const [module, row] of Object.entries(this.#rows)) {
|
|
40
|
+
const loaded = await this.#harbor.load(row.from);
|
|
41
|
+
if (loaded === null)
|
|
42
|
+
throw new Error(`module ${module} is not found at ${row.from}`);
|
|
43
|
+
if (loaded.module !== module)
|
|
44
|
+
throw new Error(`${row.from} is module ${loaded.module}, not ${module}`);
|
|
45
|
+
this.#running.set(module, loaded);
|
|
46
|
+
}
|
|
47
|
+
const refused = this.#harbor.run([...this.#running.values()]);
|
|
48
|
+
if (refused !== null)
|
|
49
|
+
throw new Error(refused);
|
|
50
|
+
}
|
|
51
|
+
async add(args) {
|
|
52
|
+
if (!text(args.from))
|
|
53
|
+
return { error: 'from is text' };
|
|
54
|
+
const loaded = await this.#harbor.load(args.from);
|
|
55
|
+
if (loaded === null)
|
|
56
|
+
return { error: 'no such module' };
|
|
57
|
+
const running = new Map(this.#running);
|
|
58
|
+
running.set(loaded.module, loaded);
|
|
59
|
+
const refused = this.#harbor.run([...running.values()]);
|
|
60
|
+
if (refused !== null)
|
|
61
|
+
return { error: refused };
|
|
62
|
+
this.#running.set(loaded.module, loaded);
|
|
63
|
+
this.#rows[loaded.module] = { version: loaded.version, from: loaded.from };
|
|
64
|
+
return { added: loaded.module, version: loaded.version };
|
|
65
|
+
}
|
|
66
|
+
remove(args) {
|
|
67
|
+
const module = args.module;
|
|
68
|
+
if (!text(module) || !this.#running.has(module))
|
|
69
|
+
return { error: 'no such module' };
|
|
70
|
+
if (this.#harbor.stands(this.#harbor.kindsOf(module)))
|
|
71
|
+
return { error: 'a being stands on this module' };
|
|
72
|
+
const running = new Map(this.#running);
|
|
73
|
+
running.delete(module);
|
|
74
|
+
this.#harbor.run([...running.values()]);
|
|
75
|
+
this.#running.delete(module);
|
|
76
|
+
delete this.#rows[module];
|
|
77
|
+
return { removed: module };
|
|
78
|
+
}
|
|
79
|
+
modules() {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const [module, row] of Object.entries(this.#rows))
|
|
82
|
+
out[module] = { version: row.version, running: this.#running.get(module)?.version ?? null, from: row.from };
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// The catalogue class one harbor stands: a being is born of her class and
|
|
87
|
+
// her stance alone, so the class holds its harbor.
|
|
88
|
+
export const catalogueFor = (harbor) => class extends Catalogue {
|
|
89
|
+
static kind = Catalogue.kind;
|
|
90
|
+
constructor(stance) {
|
|
91
|
+
super(stance, harbor);
|
|
92
|
+
}
|
|
93
|
+
};
|
package/dist/harbor/dock.d.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { type AskSpec, type Invitation, type JsonObject } from '../being/index.ts';
|
|
2
2
|
import { WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
|
|
3
3
|
export interface Hosting {
|
|
4
|
-
host(name: string, invitation: Invitation): Promise<
|
|
4
|
+
host(name: string, invitation: Invitation, seed?: string): Promise<string | null>;
|
|
5
|
+
route(pk: string, at: string | null): boolean;
|
|
6
|
+
keep(): Promise<boolean>;
|
|
5
7
|
}
|
|
8
|
+
export type Hosted = {
|
|
9
|
+
ward: string;
|
|
10
|
+
seed: string;
|
|
11
|
+
};
|
|
6
12
|
export declare const BOX = "box";
|
|
7
13
|
export declare class Dock extends WardBeing implements Lender {
|
|
8
14
|
#private;
|
|
15
|
+
static readonly kind: string;
|
|
9
16
|
static cells: JsonObject;
|
|
10
17
|
static asks: Record<string, AskSpec>;
|
|
11
18
|
constructor(stance: WardBeingStance, hosting: Hosting);
|
|
12
|
-
get hosted():
|
|
19
|
+
get hosted(): Hosted[];
|
|
20
|
+
get routes(): Record<string, string>;
|
|
21
|
+
route(args: JsonObject): JsonObject;
|
|
22
|
+
standCatalogue(): Promise<void>;
|
|
13
23
|
open(args: JsonObject): Promise<JsonObject>;
|
|
14
24
|
host(args: JsonObject): Promise<JsonObject>;
|
|
15
25
|
lend(args: JsonObject): Promise<JsonObject>;
|
package/dist/harbor/dock.js
CHANGED
|
@@ -5,23 +5,30 @@
|
|
|
5
5
|
//
|
|
6
6
|
// open { key, class } a faculty stands in the box ward, and the
|
|
7
7
|
// dock holds a standing on her under her key
|
|
8
|
-
// host { ward }
|
|
9
|
-
// the
|
|
8
|
+
// host { ward, seed? } a hosted ward stands under the seed named,
|
|
9
|
+
// or a drawn one, holding a standing on the
|
|
10
|
+
// dock; the dock keeps its name and seed
|
|
10
11
|
// lend { contract } asked by a hosted ward: an offer from a
|
|
11
12
|
// faculty that fulfils the contract
|
|
12
13
|
// retract { contract, heir } an offer that was not taken
|
|
13
|
-
|
|
14
|
+
// route { ward, at } where a ward pk is reached, `host:port`, or
|
|
15
|
+
// null to forget it; the dock keeps it and
|
|
16
|
+
// the harbor hands it to its carrier
|
|
17
|
+
import { Faculty, KIT } from '../being/index.js';
|
|
18
|
+
import { CATALOGUE } from './catalogue.js';
|
|
14
19
|
import { readInvitation } from '../quo/index.js';
|
|
15
20
|
import { ownerAsk, WardBeing } from '../ward/index.js';
|
|
16
21
|
export const BOX = 'box';
|
|
17
22
|
const text = (v) => typeof v === 'string';
|
|
18
23
|
const hosted = (_, notes) => notes?.hosted === true;
|
|
19
24
|
export class Dock extends WardBeing {
|
|
20
|
-
static
|
|
25
|
+
static kind = 'org.nervur.dock';
|
|
26
|
+
static cells = { hosted: [], routes: {} };
|
|
21
27
|
static asks = {
|
|
22
28
|
...WardBeing.asks,
|
|
23
29
|
open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
|
|
24
30
|
host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
|
|
31
|
+
route: ownerAsk('where a ward is reached, or null', { required: ['ward', 'at'] }),
|
|
25
32
|
lend: { description: 'an offer from a faculty fulfilling a contract', input: { type: 'object', required: ['contract'] }, for: hosted },
|
|
26
33
|
retract: { description: 'an offer that was not taken', input: { type: 'object', required: ['contract', 'heir'] }, for: hosted },
|
|
27
34
|
};
|
|
@@ -31,28 +38,51 @@ export class Dock extends WardBeing {
|
|
|
31
38
|
this.#hosting = hosting;
|
|
32
39
|
}
|
|
33
40
|
get hosted() {
|
|
34
|
-
return
|
|
41
|
+
return this.cells.hosted.map(({ ward, seed }) => ({ ward, seed }));
|
|
42
|
+
}
|
|
43
|
+
get routes() {
|
|
44
|
+
return { ...this.cells.routes };
|
|
45
|
+
}
|
|
46
|
+
route(args) {
|
|
47
|
+
const { ward, at } = args;
|
|
48
|
+
if (!text(ward) || (at !== null && !text(at)) || !this.#hosting.route(ward, at))
|
|
49
|
+
return { error: 'not routed' };
|
|
50
|
+
const routes = this.cells.routes;
|
|
51
|
+
if (at === null)
|
|
52
|
+
delete routes[ward];
|
|
53
|
+
else
|
|
54
|
+
routes[ward] = at;
|
|
55
|
+
return { routed: ward };
|
|
56
|
+
}
|
|
57
|
+
// Genesis: the catalogue stands first, and is kept at once.
|
|
58
|
+
async standCatalogue() {
|
|
59
|
+
const kind = 'org.nervur.catalogue';
|
|
60
|
+
if ((await this.stance.boot(kind, CATALOGUE, CATALOGUE)) === null || !(await this.#hosting.keep()))
|
|
61
|
+
throw new Error('the catalogue did not stand');
|
|
35
62
|
}
|
|
36
63
|
async open(args) {
|
|
37
64
|
if (!text(args.key) || !text(args.class))
|
|
38
65
|
return { error: 'key and class are text' };
|
|
39
|
-
if (!Faculty.fulfils(this.steward.classOf(args.class)))
|
|
66
|
+
if (args.class.startsWith(KIT) || !Faculty.fulfils(this.steward.classOf(args.class)))
|
|
40
67
|
return { error: 'no such faculty' };
|
|
41
68
|
const opened = await this.stance.boot(args.class, args.key, args.key);
|
|
42
69
|
return opened === null ? { error: 'not opened' } : { opened };
|
|
43
70
|
}
|
|
44
71
|
async host(args) {
|
|
45
72
|
const name = args.ward;
|
|
46
|
-
if (!text(name) || name === BOX || this.hosted.
|
|
73
|
+
if (!text(name) || name === BOX || this.hosted.some((h) => h.ward === name))
|
|
47
74
|
return { error: 'no such ward to host' };
|
|
75
|
+
if (args.seed !== undefined && !text(args.seed))
|
|
76
|
+
return { error: 'seed is text' };
|
|
48
77
|
const invitation = await this.stance.occupants.invite(name, { hosted: true });
|
|
49
78
|
if (invitation === null)
|
|
50
79
|
return { error: 'not hosted' };
|
|
51
|
-
|
|
80
|
+
const seed = await this.#hosting.host(name, invitation, args.seed);
|
|
81
|
+
if (seed === null) {
|
|
52
82
|
this.stance.occupants.remove(name);
|
|
53
83
|
return { error: 'not hosted' };
|
|
54
84
|
}
|
|
55
|
-
this.cells.hosted.push(name);
|
|
85
|
+
this.cells.hosted.push({ ward: name, seed });
|
|
56
86
|
return { hosted: name };
|
|
57
87
|
}
|
|
58
88
|
async lend(args) {
|