nervur 0.18.0 → 0.19.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 +7 -7
- package/dist/being/index.d.ts +1 -0
- package/dist/being/index.js +1 -0
- package/dist/being/lent.d.ts +32 -0
- package/dist/being/lent.js +63 -0
- package/dist/being/types.js +1 -1
- package/dist/conformance/beings.d.ts +2 -1
- package/dist/conformance/beings.js +7 -2
- package/dist/conformance/index.d.ts +0 -1
- package/dist/conformance/index.js +8 -5
- package/dist/harbor/box.d.ts +121 -0
- package/dist/harbor/box.js +121 -0
- package/dist/harbor/core.d.ts +5 -5
- package/dist/harbor/core.js +73 -72
- package/dist/harbor/index.d.ts +3 -1
- package/dist/harbor/index.js +3 -0
- package/dist/harbor/memory.d.ts +8 -4
- package/dist/harbor/memory.js +43 -11
- package/dist/harbor/reach.js +9 -1
- package/dist/harbor/store.js +1 -1
- package/dist/vector/cases.d.ts +4 -3
- package/dist/vector/cases.js +33 -5
- package/dist/vector/stand.js +2 -1
- package/dist/vector/world.d.ts +1 -0
- package/dist/vector/world.js +17 -6
- package/dist/ward/arithmetic.js +6 -2
- package/dist/ward/ground.d.ts +13 -7
- package/dist/ward/ground.js +29 -11
- package/dist/ward/heirs.js +4 -2
- package/dist/ward/index.d.ts +2 -1
- package/dist/ward/index.js +1 -0
- package/dist/ward/json.d.ts +2 -0
- package/dist/ward/json.js +163 -0
- package/dist/ward/partition.js +2 -2
- package/dist/ward/seal.js +14 -9
- package/dist/ward/stance.d.ts +0 -5
- package/dist/ward/ward.d.ts +1 -1
- package/dist/ward/ward.js +62 -27
- package/package.json +7 -5
- package/src/being/index.ts +1 -0
- package/src/being/lent.ts +66 -0
- package/src/being/types.ts +1 -1
- package/src/conformance/beings.ts +8 -3
- package/src/conformance/index.ts +18 -15
- package/src/harbor/box.ts +131 -0
- package/src/harbor/core.ts +23 -22
- package/src/harbor/index.ts +4 -1
- package/src/harbor/memory.ts +52 -12
- package/src/harbor/reach.ts +7 -1
- package/src/harbor/store.ts +1 -1
- package/src/vector/cases.ts +37 -9
- package/src/vector/stand.ts +2 -1
- package/src/vector/world.ts +18 -6
- package/src/ward/arithmetic.ts +6 -2
- package/src/ward/ground.ts +85 -103
- package/src/ward/heirs.ts +4 -2
- package/src/ward/index.ts +2 -1
- package/src/ward/json.ts +144 -0
- package/src/ward/partition.ts +2 -2
- package/src/ward/seal.ts +12 -8
- package/src/ward/stance.ts +1 -1
- package/src/ward/ward.ts +59 -27
package/dist/ward/ground.js
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// The body that keeps nothing: the process is the memory.
|
|
2
|
+
export const volatile = (rows = {}) => ({ rows, told: () => { }, kept: () => Promise.resolve(true), during: () => () => { } });
|
|
3
|
+
// The device is one standing. What a box can do is beings, in a ward its
|
|
4
|
+
// harbor booted and roots, and one being there, the box's own, holds a
|
|
5
|
+
// standing at each of them under the name it is lent as. The ground carries
|
|
6
|
+
// one invitation on her, minted for this ward alone; the ward takes it at
|
|
7
|
+
// boot under `BOX`, as its own first being, and from then on the device is
|
|
8
|
+
// reached the way anything is reached: what it offers is her describe, and a
|
|
9
|
+
// lend is the ask `OFFER` on that standing, naming what a being wants.
|
|
10
|
+
//
|
|
11
|
+
// She answers the lent being's own invitation, minted by that being on herself,
|
|
12
|
+
// and the ward knocks and takes it in the being's name. No root mints for a
|
|
13
|
+
// lend, and the invitation never reaches the being: it is the device's, a
|
|
14
|
+
// value she could copy is one she could hand to anyone, so the ward holds
|
|
15
|
+
// it and hands her the id. Which ward may have which name is the box's own
|
|
16
|
+
// gate, reading who asks, and a stranger's ward holds no standing at her.
|
|
17
|
+
//
|
|
18
|
+
// A harbor with no box leaves it out, and every lend is null. A ward that
|
|
19
|
+
// could not take it at boot, the box down or the invitation spent, boots all
|
|
20
|
+
// the same, lends nothing this run, and tries again at the next.
|
|
21
|
+
export { BOX } from '../being/lent.js';
|
|
22
|
+
export const OFFER = 'offer';
|
|
23
|
+
// A lend that was offered and not taken leaves nothing: the ward says so to
|
|
24
|
+
// the box, naming the heir, and the lent being removes what she minted.
|
|
25
|
+
export const RETRACT = 'retract';
|
|
26
|
+
// ---- what every harbor builds a ground out of. Convenience, never contract:
|
|
27
|
+
// a second kit writes its own harbor and may write these again.
|
|
6
28
|
// The code half of a ground, and the map back to what it made. A class is
|
|
7
29
|
// found by own key only, since `constructor` is a name Object lends every
|
|
8
30
|
// registry, and the first registry holding the name wins, so a ward's own
|
|
@@ -18,17 +40,13 @@ export const maker = (objects, ...registries) => (className, stance) => {
|
|
|
18
40
|
objects.set(stance.cells, obj);
|
|
19
41
|
return obj;
|
|
20
42
|
};
|
|
21
|
-
// Entropy, from the one place every terrain that runs Quo has it.
|
|
22
|
-
// ward mints is drawn from this, so a harbor that wants another source hands
|
|
23
|
-
// its own and nothing here has to know.
|
|
43
|
+
// Entropy, from the one place every terrain that runs Quo has it.
|
|
24
44
|
export const entropy = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
25
45
|
// A ward's pk, learned the way anyone learns anything: by asking. The empty
|
|
26
46
|
// ask on the ask pointer is the ward's own describe and its notes carry the
|
|
27
47
|
// pk. A harbor has no other way to it and wants none: the ward mints it from
|
|
28
48
|
// the seed, and a harbor that read it off the seed itself would be a second
|
|
29
|
-
// derivation to keep in step with the first.
|
|
30
|
-
// else is not one this harbor can route to, and says so here rather than
|
|
31
|
-
// leaving an undefined pk in a directory.
|
|
49
|
+
// derivation to keep in step with the first.
|
|
32
50
|
export async function learnPk(w) {
|
|
33
51
|
const notes = (await w.ask());
|
|
34
52
|
const pk = notes?.notes?.pk;
|
package/dist/ward/heirs.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { GONE } from './partition.js';
|
|
2
|
-
// How wide the span is, is the
|
|
3
|
-
// rough road, and
|
|
2
|
+
// How wide the span is, is the spec's and never a ward's. Wider is more
|
|
3
|
+
// forgiving of a rough road, and that is exactly why it is not a ward's to
|
|
4
|
+
// choose: a peer tells two doors apart by being refused at one and answered
|
|
5
|
+
// at the other, which is one relation dying over one number.
|
|
4
6
|
const SPAN = 64;
|
|
5
7
|
export class Heirs {
|
|
6
8
|
#p;
|
package/dist/ward/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Ward } from './ward.ts';
|
|
2
|
-
export type { Ground, WardPointers,
|
|
2
|
+
export type { Ground, WardPointers, Memory } from './ground.ts';
|
|
3
|
+
export { BOX, OFFER, RETRACT, volatile } from './ground.ts';
|
|
3
4
|
export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
|
|
4
5
|
export { GONE, MINTED, KNOCKS } from './partition.ts';
|
|
5
6
|
export type { AskPayload, ReplyPayload } from './seal.ts';
|
package/dist/ward/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// nervur/ward — one function. Every ward is the same ward.
|
|
3
3
|
export { Ward } from './ward.js';
|
|
4
|
+
export { BOX, OFFER, RETRACT, volatile } from './ground.js';
|
|
4
5
|
// The three bounds on what a partition may hold, for a kit that must keep
|
|
5
6
|
// the same ones and for a test that pins them.
|
|
6
7
|
export { GONE, MINTED, KNOCKS } from './partition.js';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The one reader of JSON that crossed an edge. A runtime's own parser is
|
|
3
|
+
// where two kits stop agreeing: it keeps the last of two keys of one name,
|
|
4
|
+
// rounds a whole number no double holds, reads a number past the doubles as
|
|
5
|
+
// infinity, and mends bytes that are not UTF-8 into a replacement character.
|
|
6
|
+
// Each of those is text two kits read two ways, so each is refused here, where
|
|
7
|
+
// it is read, with a throw the caller already turns into what bytes that said
|
|
8
|
+
// nothing are.
|
|
9
|
+
//
|
|
10
|
+
// What a value is beyond its text, a lone surrogate, a key named `__proto__`,
|
|
11
|
+
// a nesting past the bound, is the value rule's, and `cellFault` in cells.ts
|
|
12
|
+
// is its one reader. This file holds the rules that live only in the text.
|
|
13
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
14
|
+
const fail = (at, why) => {
|
|
15
|
+
throw new Error(`JSON at ${at}: ${why}`);
|
|
16
|
+
};
|
|
17
|
+
// A number text, held to the Values chapter. A text naming a whole number
|
|
18
|
+
// stands only if that number is exactly a double, or if the text is how
|
|
19
|
+
// ECMAScript writes the double it reads as. A text past the largest double,
|
|
20
|
+
// or naming a value other than zero that rounds to zero, is refused, and so
|
|
21
|
+
// is minus zero in any spelling.
|
|
22
|
+
export function numberFault(literal) {
|
|
23
|
+
const m = /^(-?)(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(literal);
|
|
24
|
+
if (!m)
|
|
25
|
+
return 'not a number';
|
|
26
|
+
const [, sign, whole, frac = '', exp = '0'] = m;
|
|
27
|
+
const v = Number(literal);
|
|
28
|
+
if (!Number.isFinite(v))
|
|
29
|
+
return 'past the largest double';
|
|
30
|
+
let digits = (whole + frac).replace(/^0+/, '');
|
|
31
|
+
if (v === 0) {
|
|
32
|
+
if (digits !== '')
|
|
33
|
+
return 'a value other than zero that rounds to zero';
|
|
34
|
+
return sign ? 'minus zero' : null;
|
|
35
|
+
}
|
|
36
|
+
// The exact value is digits times ten to e. A finite double bounds e from
|
|
37
|
+
// above, so the power below is never larger than the double's own width.
|
|
38
|
+
let e = BigInt(exp) - BigInt(frac.length);
|
|
39
|
+
const trimmed = digits.replace(/0+$/, '');
|
|
40
|
+
e += BigInt(digits.length - trimmed.length);
|
|
41
|
+
digits = trimmed;
|
|
42
|
+
if (e < 0n)
|
|
43
|
+
return null; // a fraction: the double nearest it, as every reader takes it
|
|
44
|
+
const exact = BigInt(digits) * 10n ** e;
|
|
45
|
+
if (exact === (v < 0 ? -BigInt(v) : BigInt(v)))
|
|
46
|
+
return null;
|
|
47
|
+
return literal === String(v) ? null : 'a whole number that is not exactly a double';
|
|
48
|
+
}
|
|
49
|
+
// Bytes to a value, or a throw.
|
|
50
|
+
export function readJson(input) {
|
|
51
|
+
const s = typeof input === 'string' ? input : utf8.decode(input);
|
|
52
|
+
let at = 0;
|
|
53
|
+
const space = () => {
|
|
54
|
+
for (;;) {
|
|
55
|
+
const c = s.charCodeAt(at);
|
|
56
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d)
|
|
57
|
+
at += 1;
|
|
58
|
+
else
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const string = () => {
|
|
63
|
+
const start = at;
|
|
64
|
+
at += 1;
|
|
65
|
+
for (;;) {
|
|
66
|
+
const c = s.charCodeAt(at);
|
|
67
|
+
if (Number.isNaN(c))
|
|
68
|
+
fail(start, 'an unterminated string');
|
|
69
|
+
if (c === 0x22)
|
|
70
|
+
break;
|
|
71
|
+
if (c < 0x20)
|
|
72
|
+
fail(at, 'a control character in a string');
|
|
73
|
+
at += c === 0x5c ? 2 : 1;
|
|
74
|
+
}
|
|
75
|
+
at += 1;
|
|
76
|
+
// The escapes are JSON's own, and the runtime reads them the one way.
|
|
77
|
+
return JSON.parse(s.slice(start, at));
|
|
78
|
+
};
|
|
79
|
+
const value = () => {
|
|
80
|
+
space();
|
|
81
|
+
const c = s[at];
|
|
82
|
+
if (c === '{') {
|
|
83
|
+
at += 1;
|
|
84
|
+
const out = {};
|
|
85
|
+
const keys = new Set();
|
|
86
|
+
space();
|
|
87
|
+
if (s[at] === '}') {
|
|
88
|
+
at += 1;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
for (;;) {
|
|
92
|
+
space();
|
|
93
|
+
if (s[at] !== '"')
|
|
94
|
+
fail(at, 'a key that is not a string');
|
|
95
|
+
const where = at;
|
|
96
|
+
const k = string();
|
|
97
|
+
if (keys.has(k))
|
|
98
|
+
fail(where, `two keys named ${JSON.stringify(k)}`);
|
|
99
|
+
keys.add(k);
|
|
100
|
+
space();
|
|
101
|
+
if (s[at] !== ':')
|
|
102
|
+
fail(at, 'a key with no colon');
|
|
103
|
+
at += 1;
|
|
104
|
+
// Defined, never assigned: a key named `__proto__` is an own key here
|
|
105
|
+
// as it is in JSON.parse, and the value rule refuses it after.
|
|
106
|
+
Object.defineProperty(out, k, { value: value(), enumerable: true, writable: true, configurable: true });
|
|
107
|
+
space();
|
|
108
|
+
if (s[at] === ',')
|
|
109
|
+
at += 1;
|
|
110
|
+
else if (s[at] === '}') {
|
|
111
|
+
at += 1;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
fail(at, 'an object not closed');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (c === '[') {
|
|
119
|
+
at += 1;
|
|
120
|
+
const out = [];
|
|
121
|
+
space();
|
|
122
|
+
if (s[at] === ']') {
|
|
123
|
+
at += 1;
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
for (;;) {
|
|
127
|
+
out.push(value());
|
|
128
|
+
space();
|
|
129
|
+
if (s[at] === ',')
|
|
130
|
+
at += 1;
|
|
131
|
+
else if (s[at] === ']') {
|
|
132
|
+
at += 1;
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
fail(at, 'a list not closed');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (c === '"')
|
|
140
|
+
return string();
|
|
141
|
+
for (const [word, v] of [['true', true], ['false', false], ['null', null]]) {
|
|
142
|
+
if (s.startsWith(word, at)) {
|
|
143
|
+
at += word.length;
|
|
144
|
+
return v;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const m = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
148
|
+
m.lastIndex = at;
|
|
149
|
+
const hit = m.exec(s);
|
|
150
|
+
if (!hit)
|
|
151
|
+
return fail(at, 'no value');
|
|
152
|
+
const why = numberFault(hit[0]);
|
|
153
|
+
if (why !== null)
|
|
154
|
+
fail(at, `${hit[0]} is ${why}`);
|
|
155
|
+
at += hit[0].length;
|
|
156
|
+
return Number(hit[0]);
|
|
157
|
+
};
|
|
158
|
+
const out = value();
|
|
159
|
+
space();
|
|
160
|
+
if (at !== s.length)
|
|
161
|
+
fail(at, 'text after the value');
|
|
162
|
+
return out;
|
|
163
|
+
}
|
package/dist/ward/partition.js
CHANGED
|
@@ -250,7 +250,7 @@ export const restoreRow = (p, row, value) => {
|
|
|
250
250
|
drop(p.beings ?? {}, row);
|
|
251
251
|
// Her bind goes with her: a being the store never kept has no relation
|
|
252
252
|
// anybody is standing on, so there is nothing to leave forward.
|
|
253
|
-
drop(
|
|
253
|
+
drop(p.bind ?? {}, row);
|
|
254
254
|
return;
|
|
255
255
|
}
|
|
256
256
|
// Behind the guard and never through it: a being's cells in a partition
|
|
@@ -267,7 +267,7 @@ export const restoreRow = (p, row, value) => {
|
|
|
267
267
|
for (const [k, v] of Object.entries(kept))
|
|
268
268
|
put(held, k, v);
|
|
269
269
|
};
|
|
270
|
-
into(
|
|
270
|
+
into(p.beings ?? {}, (value.cells ?? emptyCells()));
|
|
271
271
|
};
|
|
272
272
|
// The partition back from its rows. A head that is absent is a partition
|
|
273
273
|
// nobody has written, and `open` makes an empty one of it.
|
package/dist/ward/seal.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isDoorWord } from '../being/silence.js';
|
|
2
2
|
import { KEY, SIGNATURE, box, concat, derive, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.js';
|
|
3
3
|
import { cellFault } from './cells.js';
|
|
4
|
+
import { readJson } from './json.js';
|
|
4
5
|
const utf8 = new TextEncoder();
|
|
5
|
-
const text = new TextDecoder();
|
|
6
6
|
// The two labels a ward's keys are derived under. They are here and not in
|
|
7
7
|
// arithmetic.ts because a label is a decision and that file is arithmetic.
|
|
8
8
|
// `quo-seal` is taken: it is the label the message cipher derives its key and
|
|
@@ -26,7 +26,7 @@ export const SIZE = 1024 * 1024;
|
|
|
26
26
|
// designs and no separation at all: one secret would be doing two jobs with
|
|
27
27
|
// nothing said about it, and a second kit would have to reproduce a
|
|
28
28
|
// construction nobody named. HKDF-SHA-256 under a label is the separation
|
|
29
|
-
// said out loud, and it is what `
|
|
29
|
+
// said out loud, and it is what `quo/vectors/framing.json` pins.
|
|
30
30
|
//
|
|
31
31
|
// Bytes are key material and text is not. A seed handed in as bytes of the
|
|
32
32
|
// key length is taken as it stands, which is what a harbor mints; anything
|
|
@@ -70,7 +70,7 @@ export async function openAsk(bytes, padlockSecret) {
|
|
|
70
70
|
return null;
|
|
71
71
|
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
72
72
|
const signature = inside.subarray(inside.length - SIGNATURE);
|
|
73
|
-
const payload =
|
|
73
|
+
const payload = readJson(body);
|
|
74
74
|
// The heir, or null for the public being. Absent is not null: a payload
|
|
75
75
|
// that never named one is malformed, like any other missing field.
|
|
76
76
|
const to = payload.to;
|
|
@@ -112,7 +112,9 @@ export async function sealReply(reply, ephemeralPk, wardSign, seed) {
|
|
|
112
112
|
// lied, or it is not a reply. Every ward on the wire is somebody else's code,
|
|
113
113
|
// so the shape is checked here and nowhere later: a reply is `{ silence }`,
|
|
114
114
|
// `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
|
|
115
|
-
// object
|
|
115
|
+
// object that is a value and seen a digest or null. A field beside a shape's
|
|
116
|
+
// own makes it none of the three, since a kit that dropped the field would
|
|
117
|
+
// hand her an answer where this one hands her silence.
|
|
116
118
|
// Anything else is bytes that said nothing, and her ward never hands her a
|
|
117
119
|
// throw or an absent value for what a stranger wrote.
|
|
118
120
|
export async function openReply(bytes, ephemeralSecret, signPk) {
|
|
@@ -125,18 +127,21 @@ export async function openReply(bytes, ephemeralSecret, signPk) {
|
|
|
125
127
|
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
126
128
|
if (!(await verify(body, inside.subarray(inside.length - SIGNATURE), signPk)))
|
|
127
129
|
return null;
|
|
128
|
-
const reply =
|
|
130
|
+
const reply = readJson(body);
|
|
129
131
|
if (reply === null || typeof reply !== 'object' || Array.isArray(reply))
|
|
130
132
|
return null;
|
|
131
133
|
const r = reply;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
const keys = Object.keys(r).sort().join(',');
|
|
135
|
+
if (keys === 'silence')
|
|
136
|
+
return r.silence === true ? { silence: true } : null;
|
|
137
|
+
if (keys === 'quo')
|
|
135
138
|
return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
136
|
-
if (
|
|
139
|
+
if (keys !== 'object,seen')
|
|
137
140
|
return null;
|
|
138
141
|
if (r.seen !== null && !isHex(r.seen))
|
|
139
142
|
return null;
|
|
143
|
+
if (cellFault(r.object, 'object') !== null)
|
|
144
|
+
return null; // values in both directions: nested past the bound is none of the shapes
|
|
140
145
|
return { object: r.object, seen: r.seen };
|
|
141
146
|
}
|
|
142
147
|
catch {
|
package/dist/ward/stance.d.ts
CHANGED
|
@@ -17,9 +17,4 @@ export type Inside = {
|
|
|
17
17
|
unmake(key: string): void;
|
|
18
18
|
wrote(): void;
|
|
19
19
|
};
|
|
20
|
-
export type SealedInvitation = {
|
|
21
|
-
ward: string;
|
|
22
|
-
heir?: string;
|
|
23
|
-
secret?: string;
|
|
24
|
-
};
|
|
25
20
|
export declare function buildStance(inside: Inside, key: string, cells: Cells, bind: Bind): Stance;
|
package/dist/ward/ward.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Cells, Stance } from '../being/types.ts';
|
|
2
|
-
import type
|
|
2
|
+
import { type Ground, type WardPointers } from './ground.ts';
|
|
3
3
|
import { type Bind } from './partition.ts';
|
|
4
4
|
import { type Door } from './door.ts';
|
|
5
5
|
export type Resident = Door & {
|
package/dist/ward/ward.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// a stance built by the same code. Mints every key, seals every ask that
|
|
6
6
|
// leaves, opens every one that arrives.
|
|
7
7
|
import { silence, isSilence, unreached, isWord, word } from '../being/silence.js';
|
|
8
|
-
import { OWNER } from '../being/types.js';
|
|
8
|
+
import { OWNER, isInvitation } from '../being/types.js';
|
|
9
|
+
import { BOX, OFFER, RETRACT } from './ground.js';
|
|
9
10
|
import { at, drop, open, put, emptyBind, emptyCells, HEAD, MINTED } from './partition.js';
|
|
10
11
|
import { Heirs } from './heirs.js';
|
|
11
12
|
import { makeDoor } from './door.js';
|
|
@@ -30,14 +31,50 @@ class Self {
|
|
|
30
31
|
#birthing = new Set(); // keys whose constructor is running: taken, though no door and no row say so yet
|
|
31
32
|
door;
|
|
32
33
|
ask;
|
|
34
|
+
// Birth, in order: the ward as its own first being, the box taken, and
|
|
35
|
+
// only then every being the partition names constructed again. A being
|
|
36
|
+
// who takes what her box lends does it at her birth, so the box must be
|
|
37
|
+
// hers to ask before she is born, or a restart would be the one birth at
|
|
38
|
+
// which the device lends nothing.
|
|
33
39
|
static async boot(ground) {
|
|
34
|
-
|
|
40
|
+
const w = new Self(ground, await wardKey(ground.seed));
|
|
41
|
+
await w.#takeBox();
|
|
42
|
+
w.#reborn();
|
|
43
|
+
return w;
|
|
44
|
+
}
|
|
45
|
+
// The device, taken as one standing under `BOX` by the ward's own being,
|
|
46
|
+
// once: a ward woken from its partition already holds it. A knock that is
|
|
47
|
+
// not answered leaves the ward up and lending nothing this run.
|
|
48
|
+
async #takeBox() {
|
|
49
|
+
const inv = this.g.box;
|
|
50
|
+
const mine = this.doors.get(this.pk)?.stance;
|
|
51
|
+
if (!inv || !mine || mine.standings[BOX])
|
|
52
|
+
return;
|
|
53
|
+
const out = await mine.standings.knock(inv);
|
|
54
|
+
if (isSilence(out) || isWord(out))
|
|
55
|
+
return;
|
|
56
|
+
await mine.standings.take(BOX, inv);
|
|
57
|
+
}
|
|
58
|
+
// What this device lends this ward's beings: the box's being, asked to
|
|
59
|
+
// offer a name, answers the lent being's own invitation, and the taker knocks
|
|
60
|
+
// and takes it. The ward reads nothing in the value but that it is one.
|
|
61
|
+
async #lend(name, take) {
|
|
62
|
+
const box = this.doors.get(this.pk)?.stance.standings[BOX];
|
|
63
|
+
if (!box)
|
|
64
|
+
return false;
|
|
65
|
+
const inv = await box.ask(OFFER, { name });
|
|
66
|
+
if (!isInvitation(inv))
|
|
67
|
+
return false;
|
|
68
|
+
const took = await take(inv);
|
|
69
|
+
if (!took)
|
|
70
|
+
await box.ask(RETRACT, { name, heir: inv.heir ?? null });
|
|
71
|
+
return took;
|
|
35
72
|
}
|
|
36
73
|
constructor(ground, key) {
|
|
37
74
|
this.g = ground;
|
|
38
75
|
this.key = key;
|
|
39
76
|
this.pk = key.pk;
|
|
40
|
-
this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
|
|
77
|
+
this.p = open(ground.memory.rows); // the partition: the ward's own state, and every being's row
|
|
41
78
|
// Opening is itself a write: a memory nobody has written comes back with
|
|
42
79
|
// a version stamped on it and the ward's own tables under it. A ward that
|
|
43
80
|
// said nothing here would have every being of it kept and the head that
|
|
@@ -47,6 +84,24 @@ class Self {
|
|
|
47
84
|
this.heirs = new Heirs(this.p, () => this.#wrote(HEAD));
|
|
48
85
|
this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n), () => this.#keep()); // pointer one
|
|
49
86
|
this.#boot(this.pk, () => this); // the ward's ward is itself
|
|
87
|
+
// pointer two. the owner. the same answer every named asker gets, asker filled in as owner.
|
|
88
|
+
this.ask = async (method, args = {}) => {
|
|
89
|
+
try {
|
|
90
|
+
const out = await this.answer({ id: OWNER }, method, args);
|
|
91
|
+
// The same question the door asks, in the same place: after she has
|
|
92
|
+
// answered and before the owner holds the answer. Silence is what a
|
|
93
|
+
// failed ask says here, so a store that refused is silence, and the
|
|
94
|
+
// owner reads it as it reads every other ask that did not take.
|
|
95
|
+
if (!(await this.#keep().catch(() => false)))
|
|
96
|
+
return silence;
|
|
97
|
+
return isSilence(out) || isWord(out) ? silence : out;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return silence;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
#reborn() {
|
|
50
105
|
// a restart is silent: every being in the cells is constructed again, unasked.
|
|
51
106
|
// A throw on a boot the owner asked for is that boot's error, and nothing
|
|
52
107
|
// half-lives. This boot nobody asked for, and there is nobody to hand an
|
|
@@ -68,22 +123,6 @@ class Self {
|
|
|
68
123
|
/* she is not here this run. the ward is, and so is everyone else. */
|
|
69
124
|
}
|
|
70
125
|
}
|
|
71
|
-
// pointer two. the owner. the same answer every named asker gets, asker filled in as owner.
|
|
72
|
-
this.ask = async (method, args = {}) => {
|
|
73
|
-
try {
|
|
74
|
-
const out = await this.answer({ id: OWNER }, method, args);
|
|
75
|
-
// The same question the door asks, in the same place: after she has
|
|
76
|
-
// answered and before the owner holds the answer. Silence is what a
|
|
77
|
-
// failed ask says here, so a store that refused is silence, and the
|
|
78
|
-
// owner reads it as it reads every other ask that did not take.
|
|
79
|
-
if (!(await this.#keep().catch(() => false)))
|
|
80
|
-
return silence;
|
|
81
|
-
return isSilence(out) || isWord(out) ? silence : out;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return silence;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
126
|
}
|
|
88
127
|
// ---- the ward as a being. asked only by its owners: the root, through
|
|
89
128
|
// the ask pointer, and its occupants, through the door, whom only the root
|
|
@@ -116,14 +155,14 @@ class Self {
|
|
|
116
155
|
// because a harbor told only that something moved must keep all of it, and
|
|
117
156
|
// a being's row is the one part of a partition that grows without limit.
|
|
118
157
|
#wrote(row) {
|
|
119
|
-
this.g.
|
|
158
|
+
this.g.memory.told(row);
|
|
120
159
|
}
|
|
121
160
|
// Whether what was written is kept, asked once at the end of an arrival
|
|
122
161
|
// and never during one. A harbor that keeps nothing was never going to
|
|
123
162
|
// refuse anything, so its silence here is yes: a ward driven with no store
|
|
124
163
|
// under it answers exactly as it did before a harbor could say no.
|
|
125
164
|
async #keep() {
|
|
126
|
-
return this.g.
|
|
165
|
+
return this.g.memory.kept();
|
|
127
166
|
}
|
|
128
167
|
// The rows no door holds this run: beings whose class threw at birth or
|
|
129
168
|
// is not the harbor's to give. Key to class.
|
|
@@ -233,11 +272,7 @@ class Self {
|
|
|
233
272
|
// never came about takes her out again.
|
|
234
273
|
relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
|
|
235
274
|
unmake: (k) => void this.#unboot(k),
|
|
236
|
-
|
|
237
|
-
// inside the harbor's own call and reads nothing in the value: which
|
|
238
|
-
// names there are, which ward may ask for one, and what becomes of one
|
|
239
|
-
// she did not take, is the harbor's and no word of the ward.
|
|
240
|
-
lend: async (name, take) => (await this.g.lend?.(name, take)) ?? false,
|
|
275
|
+
lend: (name, take) => this.#lend(name, take),
|
|
241
276
|
wrote: () => this.#wrote(key),
|
|
242
277
|
}, key, cells, bind);
|
|
243
278
|
const being = make(stance);
|
|
@@ -274,7 +309,7 @@ class Self {
|
|
|
274
309
|
// the reply opens, and whatever the being writes on either side of her
|
|
275
310
|
// own ask. A harbor may write those rows down; what it may not do is put
|
|
276
311
|
// the ward back to them.
|
|
277
|
-
const lower = this.g.
|
|
312
|
+
const lower = this.g.memory.during();
|
|
278
313
|
try {
|
|
279
314
|
return await this.#sent(bind, keys, method, args, wanted, row);
|
|
280
315
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor
|
|
3
|
+
"version": "0.19.1",
|
|
4
|
+
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
7
7
|
"protocol",
|
|
@@ -43,9 +43,11 @@
|
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
46
|
-
"check": "node
|
|
47
|
-
"
|
|
48
|
-
"
|
|
46
|
+
"check": "node ../../test.mjs \"test/*.test.ts\"",
|
|
47
|
+
"experimental": "node ../../test.mjs \"experimental/*.test.ts\"",
|
|
48
|
+
"deep": "node --test --test-timeout=300000 \"test/terrain/*.test.ts\"",
|
|
49
|
+
"cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/*.test.ts\"",
|
|
50
|
+
"prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
|
|
49
51
|
},
|
|
50
52
|
"publishConfig": {
|
|
51
53
|
"access": "public"
|
package/src/being/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// nervur — the Being side. What a being author imports, if anything.
|
|
3
3
|
export { Being, type AskSpec } from './being.ts';
|
|
4
|
+
export { Lent, BOX, LENT_ASKS } from './lent.ts';
|
|
4
5
|
export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
|
|
5
6
|
export { digest, canonical } from './digest.ts';
|
|
6
7
|
export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.ts';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A lent being: a being of the box's own ward, standing for one thing the
|
|
3
|
+
// device can do. What makes her one is two asks the box's being alone may
|
|
4
|
+
// make, whom she knows as the occupant `box`. `offer`: she mints an
|
|
5
|
+
// invitation on herself and answers it, so that a ward the box vouches for
|
|
6
|
+
// may knock and take a standing at her. `retract`: an offer that was not
|
|
7
|
+
// taken, named by its heir, is removed, so a refused lend leaves nothing.
|
|
8
|
+
//
|
|
9
|
+
// The two are the base's and never a subclass's to declare: a class says
|
|
10
|
+
// her own asks and nothing of the seam she is lent through, so they are
|
|
11
|
+
// answered here before her asks are read, and shown to the box alone. No
|
|
12
|
+
// root mints for her, and nothing else about her is decided here.
|
|
13
|
+
//
|
|
14
|
+
// A convenience, not a contract: a being that answers the two the same way
|
|
15
|
+
// to `box` is lendable, whatever she extends.
|
|
16
|
+
import { Being } from './being.ts';
|
|
17
|
+
import type { Asker, Blueprint, JsonObject, Reply } from './types.ts';
|
|
18
|
+
|
|
19
|
+
// The one word for the box's being: her key in the box's ward, the id every
|
|
20
|
+
// lent being knows her by, and the standing every hosted ward takes at
|
|
21
|
+
// birth.
|
|
22
|
+
export const BOX = 'box';
|
|
23
|
+
|
|
24
|
+
export const LENT_ASKS = {
|
|
25
|
+
offer: { name: 'offer', description: 'an invitation on me, for a ward the box vouches for', input: { type: 'object' } },
|
|
26
|
+
retract: { name: 'retract', description: 'an offer that was not taken, by its heir', input: { type: 'object', properties: { heir: { type: 'string' } }, required: ['heir'] } },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class Lent extends Being {
|
|
30
|
+
// The offers she has open, heir to the id she minted under.
|
|
31
|
+
#open(): Record<string, string> {
|
|
32
|
+
return ((this.cells.open as Record<string, string> | undefined) ??= {});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async offer(): Promise<JsonObject> {
|
|
36
|
+
const n = ((this.cells.lent as number | undefined) ?? 0) + 1;
|
|
37
|
+
this.cells.lent = n;
|
|
38
|
+
const id = `lent:${n}`;
|
|
39
|
+
const inv = await this.invite(id);
|
|
40
|
+
if (inv === null) return { error: 'could not mint' };
|
|
41
|
+
this.#open()[inv.heir!] = id;
|
|
42
|
+
return inv;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
retract(args: JsonObject): JsonObject {
|
|
46
|
+
const open = this.#open();
|
|
47
|
+
const heir = typeof args.heir === 'string' ? args.heir : '';
|
|
48
|
+
const id = Object.hasOwn(open, heir) ? open[heir] : undefined;
|
|
49
|
+
if (id === undefined) return { retracted: null };
|
|
50
|
+
this.occupants.remove(id);
|
|
51
|
+
delete open[heir];
|
|
52
|
+
return { retracted: id };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override describe(asker: Asker): Blueprint {
|
|
56
|
+
const bp = super.describe(asker);
|
|
57
|
+
if (asker.id !== BOX) return bp;
|
|
58
|
+
return { ...bp, asks: [...bp.asks, LENT_ASKS.offer, LENT_ASKS.retract] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
|
|
62
|
+
if (asker.id === BOX && method === 'offer') return this.offer();
|
|
63
|
+
if (asker.id === BOX && method === 'retract') return this.retract(args);
|
|
64
|
+
return super.answer(asker, method, args);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/being/types.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const invitationArgs = (inv: Invitation): JsonObject => ({
|
|
|
43
43
|
|
|
44
44
|
// Whether a value that arrived is an invitation. The shape is the spec's, so
|
|
45
45
|
// the reading of it is too, and it is one reading: a form that
|
|
46
|
-
// asks a guest for one, a
|
|
46
|
+
// asks a guest for one, a lent being handed one to wake her maker by, and a
|
|
47
47
|
// shell that finds one in a link all ask the same question, and a kinder
|
|
48
48
|
// answer in one of them is a value that fails at a door instead of at the
|
|
49
49
|
// edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// shop, a customer on the base class, a maker, a member, and one raw being
|
|
4
4
|
// with no base at all.
|
|
5
5
|
import { Being } from '../being/being.ts';
|
|
6
|
+
import { Lent } from '../being/lent.ts';
|
|
6
7
|
import { answered, isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
|
|
7
8
|
import { invitationArgs } from '../being/types.ts';
|
|
8
9
|
import type { Ask, Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
@@ -156,9 +157,13 @@ export class Maker extends Being {
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
// A member is also lendable: lent by a box that stands her in its own ward,
|
|
161
|
+
// she offers an invitation on herself when the box's being asks, and is an
|
|
162
|
+
// ordinary being to everyone else.
|
|
163
|
+
export class Member extends Lent {
|
|
164
|
+
static override cells = { ...Lent.cells, heard: [] as JsonObject[] };
|
|
161
165
|
static override asks = {
|
|
166
|
+
...Lent.asks,
|
|
162
167
|
ping: { input: { type: 'object' } },
|
|
163
168
|
relay: { input: { type: 'object', properties: { id: { type: 'string' }, method: { type: 'string' }, time: { type: 'number' } }, required: ['id'] } },
|
|
164
169
|
};
|
|
@@ -178,6 +183,6 @@ export class Member extends Being {
|
|
|
178
183
|
const out = await standing.ask(method as string | undefined, (args ?? {}) as JsonObject, time === undefined ? undefined : { time: time as number });
|
|
179
184
|
if (isSilence(out)) return { said: 'silence', out: null };
|
|
180
185
|
if (isWord(out)) return { said: wordOf(out), out: null };
|
|
181
|
-
return { said: null, out: out
|
|
186
|
+
return { said: null, out: out };
|
|
182
187
|
}
|
|
183
188
|
}
|