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/src/vector/world.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Nothing here draws from the device. A door's reply is sealed under a fresh
|
|
9
9
|
// ephemeral key and a fresh nonce, so the bytes are fixed only where the
|
|
10
10
|
// entropy is, and every draw in this file comes from one written-down stream.
|
|
11
|
-
// That is what lets `
|
|
11
|
+
// That is what lets `quo/vectors/door.json` be a corpus and not a
|
|
12
12
|
// sample, and what lets a kit stand in vector mode for a verifier that holds
|
|
13
13
|
// no key: the spec's chapter of that name is the contract this file keeps.
|
|
14
14
|
import { MemoryHarbor } from '../harbor/memory.ts';
|
|
@@ -23,10 +23,10 @@ import type { Answer, Asker, BeingClass, Blueprint, Invitation, Json, JsonObject
|
|
|
23
23
|
|
|
24
24
|
// ---- the stream
|
|
25
25
|
//
|
|
26
|
-
// SplitMix64
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
26
|
+
// SplitMix64, written out in the spec's vector-mode chapter with both
|
|
27
|
+
// multipliers and all three shifts, because a kit that reproduces it by
|
|
28
|
+
// recognising it is a kit that guessed. This is that, and it moves only
|
|
29
|
+
// when that does. It is
|
|
30
30
|
// not a cipher and is not meant to be one. Its whole job is that the bytes
|
|
31
31
|
// this file produces are the same bytes twice.
|
|
32
32
|
const GAMMA = 0x9e3779b97f4a7c15n;
|
|
@@ -50,12 +50,23 @@ export function splitmix64(seed: bigint): () => Uint8Array {
|
|
|
50
50
|
export const STREAM_SEED = 20250901n;
|
|
51
51
|
|
|
52
52
|
let draw = splitmix64(STREAM_SEED);
|
|
53
|
+
// How many sixty-four bit words have been spent since the stream was reset.
|
|
54
|
+
// A record carries this number at the moment of its arrival, so a kit that
|
|
55
|
+
// built the door's state its own way sets its stream here and draws what
|
|
56
|
+
// this one would have drawn. It is the whole of what the setup's history is
|
|
57
|
+
// worth to a stranger: not how the world was made, but where the stream
|
|
58
|
+
// stands when the door is about to answer.
|
|
59
|
+
let spent = 0;
|
|
60
|
+
export const drawn = (): number => spent;
|
|
53
61
|
// Entropy, from the stream and never from the device. Every signer secret,
|
|
54
62
|
// every ephemeral seed, every byte of noise and every key a ward in this
|
|
55
63
|
// world mints comes through here, in the order the code asks for it.
|
|
56
64
|
export const rnd = (n = 32): Uint8Array => {
|
|
57
65
|
const out = new Uint8Array(n);
|
|
58
|
-
for (let i = 0; i < n; i += 8)
|
|
66
|
+
for (let i = 0; i < n; i += 8) {
|
|
67
|
+
out.set(draw().subarray(0, Math.min(8, n - i)), i);
|
|
68
|
+
spent += 1;
|
|
69
|
+
}
|
|
59
70
|
return out;
|
|
60
71
|
};
|
|
61
72
|
|
|
@@ -154,6 +165,7 @@ let counted = 1000;
|
|
|
154
165
|
|
|
155
166
|
export async function world() {
|
|
156
167
|
draw = splitmix64(STREAM_SEED); // a fresh world is a fresh stream: two runs, one corpus
|
|
168
|
+
spent = 0;
|
|
157
169
|
counted = 1000;
|
|
158
170
|
const a = new MemoryHarbor(),
|
|
159
171
|
b = new MemoryHarbor();
|
package/src/ward/arithmetic.ts
CHANGED
|
@@ -58,8 +58,10 @@ export function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
|
|
|
58
58
|
return diff === 0;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// The eight small-order points of Ed25519, in their canonical encoding
|
|
62
|
-
//
|
|
61
|
+
// The eight small-order points of Ed25519, in their canonical encoding, and
|
|
62
|
+
// the two points with x = 0 spelled with the sign bit set, which a decoder
|
|
63
|
+
// that ignores the bit reads as the same point. A public key among them
|
|
64
|
+
// verifies nothing. An encoding whose y coordinate is
|
|
63
65
|
// not reduced, y at or above the field's prime p, names one of the same
|
|
64
66
|
// points under another spelling, and a terrain's verify may accept a zero
|
|
65
67
|
// signature under it; the field has room for the spelling, so it is refused
|
|
@@ -73,6 +75,8 @@ const SMALL_ORDER = [
|
|
|
73
75
|
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
|
|
74
76
|
'26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
|
|
75
77
|
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
|
|
78
|
+
'0100000000000000000000000000000000000000000000000000000000000080',
|
|
79
|
+
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
|
76
80
|
].map(unhex);
|
|
77
81
|
// y >= p, read little-endian with the sign bit masked off: the top byte is
|
|
78
82
|
// 0x7f under the mask, every middle byte is 0xff, and the low byte is at
|
package/src/ward/ground.ts
CHANGED
|
@@ -1,110 +1,99 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The ground. The one object a harbor passes a ward at birth
|
|
3
|
-
// the
|
|
4
|
-
// the ward itself knows no runtime.
|
|
2
|
+
// The ground. The one object a harbor passes a ward at birth, and the whole
|
|
3
|
+
// of the device to it: six things, and everything a runtime differs on
|
|
4
|
+
// arrives in one of them, which is why the ward itself knows no runtime.
|
|
5
|
+
//
|
|
6
|
+
// seed who the ward is. the pk derives from it and from nothing else
|
|
7
|
+
// random entropy. every key the ward mints is drawn from it
|
|
8
|
+
// memory what the ward remembers: one interface, a body per terrain
|
|
9
|
+
// instantiate the dna: a class name and a stance in, a being or nothing out
|
|
10
|
+
// carry the wire: a ward pk and sealed bytes in, sealed bytes or nothing out
|
|
11
|
+
// box the device, as one invitation on the box's own being
|
|
12
|
+
//
|
|
13
|
+
// The spec lists nine things and says a kit gathers them however its
|
|
14
|
+
// language gathers things. This kit gathers the partition, wrote, keep and
|
|
15
|
+
// calling into memory, because they are one concern with four moments, and
|
|
16
|
+
// a ward is handed one body and told how to use it rather than four calls
|
|
17
|
+
// it must keep in step.
|
|
5
18
|
import type { Stance, BeingLike, BeingClass, Invitation } from '../being/types.ts';
|
|
6
19
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
20
|
+
// Memory. Four moments, and they are the promise a body keeps:
|
|
21
|
+
//
|
|
22
|
+
// rows the values the ward holds and writes into directly. synchronous,
|
|
23
|
+
// always, because the door judges an arrival in one pass and a
|
|
24
|
+
// disk in the middle of it would be a door that waits on a device
|
|
25
|
+
// told the ward says it after every write, naming the row: a being by
|
|
26
|
+
// her key, or `HEAD` for everything that is not one being's.
|
|
27
|
+
// nothing comes back. a body that keeps the rows saves in the
|
|
28
|
+
// order it was told, so a being driven in process is kept the way
|
|
29
|
+
// one reached through a door is
|
|
30
|
+
// kept keep what has been written, now, and say whether it was. the
|
|
31
|
+
// ward asks it once at the end of an arrival, after the being has
|
|
32
|
+
// answered and before the answer is sealed, the last moment
|
|
33
|
+
// anything can still be said about it. false is a store that
|
|
34
|
+
// refused, and the ward says so with the word a failed ask already
|
|
35
|
+
// has: `threw` to an asker the door holds, silence to the ask
|
|
36
|
+
// pointer. never a reason
|
|
37
|
+
// during a call of this ward's has begun, and what comes back lowers it.
|
|
38
|
+
// the ward raises it before it reaches out and lowers it in a
|
|
39
|
+
// finally, so a call that threw lowers too, and lowering twice is
|
|
40
|
+
// lowering once. calls nest. a body may go on writing while it is
|
|
41
|
+
// raised; what it may not do is take a save made there as the
|
|
42
|
+
// point it puts a refused ward back to
|
|
11
43
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
44
|
+
// A body that keeps nothing has rows, is told and does nothing, answers yes,
|
|
45
|
+
// and raises nothing: `volatile` below. The ward has one path either way.
|
|
46
|
+
export type Memory = {
|
|
47
|
+
readonly rows: Record<string, unknown>;
|
|
48
|
+
told(row: string): void;
|
|
49
|
+
kept(): Promise<boolean>;
|
|
50
|
+
during(): () => void;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// The body that keeps nothing: the process is the memory.
|
|
54
|
+
export const volatile = (rows: Record<string, unknown> = {}): Memory => ({ rows, told: () => {}, kept: () => Promise.resolve(true), during: () => () => {} });
|
|
55
|
+
|
|
56
|
+
// The device is one standing. What a box can do is beings, in a ward its
|
|
57
|
+
// harbor booted and roots, and one being there, the box's own, holds a
|
|
58
|
+
// standing at each of them under the name it is lent as. The ground carries
|
|
59
|
+
// one invitation on her, minted for this ward alone; the ward takes it at
|
|
60
|
+
// boot under `BOX`, as its own first being, and from then on the device is
|
|
61
|
+
// reached the way anything is reached: what it offers is her describe, and a
|
|
62
|
+
// lend is the ask `OFFER` on that standing, naming what a being wants.
|
|
19
63
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
64
|
+
// She answers the lent being's own invitation, minted by that being on herself,
|
|
65
|
+
// and the ward knocks and takes it in the being's name. No root mints for a
|
|
66
|
+
// lend, and the invitation never reaches the being: it is the device's, a
|
|
67
|
+
// value she could copy is one she could hand to anyone, so the ward holds
|
|
68
|
+
// it and hands her the id. Which ward may have which name is the box's own
|
|
69
|
+
// gate, reading who asks, and a stranger's ward holds no standing at her.
|
|
24
70
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
export
|
|
71
|
+
// A harbor with no box leaves it out, and every lend is null. A ward that
|
|
72
|
+
// could not take it at boot, the box down or the invitation spent, boots all
|
|
73
|
+
// the same, lends nothing this run, and tries again at the next.
|
|
74
|
+
export { BOX } from '../being/lent.ts';
|
|
75
|
+
export const OFFER = 'offer';
|
|
76
|
+
// A lend that was offered and not taken leaves nothing: the ward says so to
|
|
77
|
+
// the box, naming the heir, and the lent being removes what she minted.
|
|
78
|
+
export const RETRACT = 'retract';
|
|
29
79
|
|
|
30
80
|
export type Ground = {
|
|
31
|
-
seed: string | Uint8Array;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// or `HEAD` for everything that is not one being's. It says so after every
|
|
36
|
-
// write, a key rotated, a relation taken, a cell she set, and says nothing
|
|
37
|
-
// else: this word is told, never asked, and nothing comes back from it. A
|
|
38
|
-
// harbor that keeps the partition saves in the order it was told, so a
|
|
39
|
-
// being driven in process is kept the way one reached through a door is.
|
|
40
|
-
// Whether any of it was kept is one question asked once, at the end of an
|
|
41
|
-
// arrival, and `keep` is where. A harbor that keeps nothing leaves both
|
|
42
|
-
// out.
|
|
43
|
-
//
|
|
44
|
-
// The row is named because a being's cells are the one part of a partition
|
|
45
|
-
// that grows without limit, and a harbor told only that something moved has
|
|
46
|
-
// to write down every being in the ward to be sure of one. What a harbor
|
|
47
|
-
// does with the name is its own: writing the whole partition on every word
|
|
48
|
-
// is correct, and slower.
|
|
49
|
-
wrote?: (row: string) => void;
|
|
50
|
-
// Keep what has been written, now, and say whether it was kept. The ward
|
|
51
|
-
// calls it once at the end of an arrival, after the being has answered and
|
|
52
|
-
// before the answer is sealed, which is the last moment anything can still
|
|
53
|
-
// be said about it: a reply sealed to an asker's lid cannot be unsaid, and
|
|
54
|
-
// only the ward can seal.
|
|
55
|
-
//
|
|
56
|
-
// False is a store that refused, and the ward says so with the word a
|
|
57
|
-
// failed ask already has: `threw` to an asker the door holds, silence to
|
|
58
|
-
// the ask pointer and to a stranger. It is never a reason; a full disk is
|
|
59
|
-
// no more the far side's business than any other insides of this device.
|
|
60
|
-
//
|
|
61
|
-
// A harbor that keeps nothing leaves it out, and so does one that would
|
|
62
|
-
// rather count a refusal than answer it: absent means the ward asks
|
|
63
|
-
// nothing and seals what the being said, which is what every harbor did
|
|
64
|
-
// before this existed. A harbor that writes it saves here and nowhere
|
|
65
|
-
// else for this arrival, since `wrote` has already named every row.
|
|
66
|
-
keep?: () => Promise<boolean>;
|
|
67
|
-
// A call of this ward's has begun, and what comes back says it has ended.
|
|
68
|
-
// The ward alone knows where a call begins: a being reaching another being,
|
|
69
|
-
// in this process or over the wire, is one relation and two halves, and she
|
|
70
|
-
// writes her side of it before the bytes go out and again when they come
|
|
71
|
-
// back. Between those two writes her rows are half done, and a harbor that
|
|
72
|
-
// wrote them and then had to put them back would stand the ward at a point
|
|
73
|
-
// no call ever stood at.
|
|
74
|
-
//
|
|
75
|
-
// It is told, and the one thing that comes back is how to lower it. The
|
|
76
|
-
// ward raises it before it reaches out and lowers it in a finally, so a
|
|
77
|
-
// call that threw lowers too, and lowering twice is lowering once. Calls
|
|
78
|
-
// nest: a ward is in flight while any of its beings is.
|
|
79
|
-
//
|
|
80
|
-
// A harbor is free to keep writing while it is raised, because the store is
|
|
81
|
-
// what survives a restart. What it may not do is take a save made there as
|
|
82
|
-
// the point it puts a refused ward back to. A harbor that keeps nothing
|
|
83
|
-
// leaves it out.
|
|
84
|
-
calling?: () => () => void;
|
|
81
|
+
seed: string | Uint8Array;
|
|
82
|
+
random(n: number): Uint8Array;
|
|
83
|
+
memory: Memory;
|
|
84
|
+
instantiate(className: string, stance: Stance): BeingLike | null;
|
|
85
85
|
// Sealed bytes to a ward pk. What comes back, or undefined.
|
|
86
86
|
//
|
|
87
87
|
// undefined is a promise, not a shrug: no door was reached, and nothing was
|
|
88
88
|
// delivered. The ward hands it to a being as unreached, which is the one
|
|
89
89
|
// answer that says asking again is safe, so a harbor may only return it
|
|
90
90
|
// when it knows the bytes never arrived: no reach for that pk, a socket
|
|
91
|
-
// that would not open, a link that is down.
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// answer at all in that case. The ward bounds every ask itself, and an
|
|
96
|
-
// answer that never comes inside that bound is `late`, which promises
|
|
97
|
-
// nothing either way. So a harbor may hold a shorter patience than the
|
|
98
|
-
// ward's for its own reasons, a socket it wants back or a queue it will
|
|
99
|
-
// not grow, and the two bounds never need to read each other: whichever
|
|
100
|
-
// ends first ends the ask, and each says only what it can honestly say.
|
|
91
|
+
// that would not open, a link that is down. A harbor that sent the bytes
|
|
92
|
+
// and then gave up waiting knows no such thing and must not answer at all:
|
|
93
|
+
// the ward bounds every ask itself, and an answer that never comes inside
|
|
94
|
+
// that bound is `late`, which promises nothing either way.
|
|
101
95
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
|
|
102
|
-
|
|
103
|
-
// A standing at one of the harbor's own beings, by the name that harbor
|
|
104
|
-
// knows it under. Built per ward, so which ward may ask for which name is
|
|
105
|
-
// the harbor's own decision and a stranger's ward is lent nothing. A harbor
|
|
106
|
-
// with nothing to lend leaves it out, and every name answers false.
|
|
107
|
-
lend?: Lend;
|
|
96
|
+
box?: Invitation;
|
|
108
97
|
};
|
|
109
98
|
|
|
110
99
|
// What a ward hands back. Two pointers. The door answers bytes, always, and
|
|
@@ -117,11 +106,8 @@ export type WardPointers = {
|
|
|
117
106
|
ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
118
107
|
};
|
|
119
108
|
|
|
120
|
-
// ---- what every harbor builds a ground out of.
|
|
121
|
-
//
|
|
122
|
-
// themselves apart: what a memory harbor and a real one differ on is the
|
|
123
|
-
// route and the store, and nothing here. A second kit writes its own harbor
|
|
124
|
-
// and may write these again; they are convenience, never contract.
|
|
109
|
+
// ---- what every harbor builds a ground out of. Convenience, never contract:
|
|
110
|
+
// a second kit writes its own harbor and may write these again.
|
|
125
111
|
|
|
126
112
|
// The code half of a ground, and the map back to what it made. A class is
|
|
127
113
|
// found by own key only, since `constructor` is a name Object lends every
|
|
@@ -140,18 +126,14 @@ export const maker =
|
|
|
140
126
|
return obj;
|
|
141
127
|
};
|
|
142
128
|
|
|
143
|
-
// Entropy, from the one place every terrain that runs Quo has it.
|
|
144
|
-
// ward mints is drawn from this, so a harbor that wants another source hands
|
|
145
|
-
// its own and nothing here has to know.
|
|
129
|
+
// Entropy, from the one place every terrain that runs Quo has it.
|
|
146
130
|
export const entropy = (n: number): Uint8Array => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
147
131
|
|
|
148
132
|
// A ward's pk, learned the way anyone learns anything: by asking. The empty
|
|
149
133
|
// ask on the ask pointer is the ward's own describe and its notes carry the
|
|
150
134
|
// pk. A harbor has no other way to it and wants none: the ward mints it from
|
|
151
135
|
// the seed, and a harbor that read it off the seed itself would be a second
|
|
152
|
-
// derivation to keep in step with the first.
|
|
153
|
-
// else is not one this harbor can route to, and says so here rather than
|
|
154
|
-
// leaving an undefined pk in a directory.
|
|
136
|
+
// derivation to keep in step with the first.
|
|
155
137
|
export async function learnPk(w: WardPointers): Promise<string> {
|
|
156
138
|
const notes = (await w.ask()) as { notes?: { pk?: unknown } } | null;
|
|
157
139
|
const pk = notes?.notes?.pk;
|
package/src/ward/heirs.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
import type { DoorWord } from '../being/types.ts';
|
|
8
8
|
import { GONE, type Heir, type Partition } from './partition.ts';
|
|
9
9
|
|
|
10
|
-
// How wide the span is, is the
|
|
11
|
-
// rough road, and
|
|
10
|
+
// How wide the span is, is the spec's and never a ward's. Wider is more
|
|
11
|
+
// forgiving of a rough road, and that is exactly why it is not a ward's to
|
|
12
|
+
// choose: a peer tells two doors apart by being refused at one and answered
|
|
13
|
+
// at the other, which is one relation dying over one number.
|
|
12
14
|
const SPAN = 64;
|
|
13
15
|
|
|
14
16
|
export class Heirs {
|
package/src/ward/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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.ts';
|
|
4
|
-
export type { Ground, WardPointers,
|
|
4
|
+
export type { Ground, WardPointers, Memory } from './ground.ts';
|
|
5
|
+
export { BOX, OFFER, RETRACT, volatile } from './ground.ts';
|
|
5
6
|
export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
|
|
6
7
|
// The three bounds on what a partition may hold, for a kit that must keep
|
|
7
8
|
// the same ones and for a test that pins them.
|
package/src/ward/json.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
|
|
14
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
15
|
+
|
|
16
|
+
const fail = (at: number, why: string): never => {
|
|
17
|
+
throw new Error(`JSON at ${at}: ${why}`);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// A number text, held to the Values chapter. A text naming a whole number
|
|
21
|
+
// stands only if that number is exactly a double, or if the text is how
|
|
22
|
+
// ECMAScript writes the double it reads as. A text past the largest double,
|
|
23
|
+
// or naming a value other than zero that rounds to zero, is refused, and so
|
|
24
|
+
// is minus zero in any spelling.
|
|
25
|
+
export function numberFault(literal: string): string | null {
|
|
26
|
+
const m = /^(-?)(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(literal);
|
|
27
|
+
if (!m) return 'not a number';
|
|
28
|
+
const [, sign, whole, frac = '', exp = '0'] = m;
|
|
29
|
+
const v = Number(literal);
|
|
30
|
+
if (!Number.isFinite(v)) return 'past the largest double';
|
|
31
|
+
let digits = (whole + frac).replace(/^0+/, '');
|
|
32
|
+
if (v === 0) {
|
|
33
|
+
if (digits !== '') 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) return null; // a fraction: the double nearest it, as every reader takes it
|
|
43
|
+
const exact = BigInt(digits) * 10n ** e;
|
|
44
|
+
if (exact === (v < 0 ? -BigInt(v) : BigInt(v))) return null;
|
|
45
|
+
return literal === String(v) ? null : 'a whole number that is not exactly a double';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Bytes to a value, or a throw.
|
|
49
|
+
export function readJson(input: Uint8Array | string): unknown {
|
|
50
|
+
const s = typeof input === 'string' ? input : utf8.decode(input);
|
|
51
|
+
let at = 0;
|
|
52
|
+
const space = () => {
|
|
53
|
+
for (;;) {
|
|
54
|
+
const c = s.charCodeAt(at);
|
|
55
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) at += 1;
|
|
56
|
+
else return;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const string = (): string => {
|
|
60
|
+
const start = at;
|
|
61
|
+
at += 1;
|
|
62
|
+
for (;;) {
|
|
63
|
+
const c = s.charCodeAt(at);
|
|
64
|
+
if (Number.isNaN(c)) fail(start, 'an unterminated string');
|
|
65
|
+
if (c === 0x22) break;
|
|
66
|
+
if (c < 0x20) fail(at, 'a control character in a string');
|
|
67
|
+
at += c === 0x5c ? 2 : 1;
|
|
68
|
+
}
|
|
69
|
+
at += 1;
|
|
70
|
+
// The escapes are JSON's own, and the runtime reads them the one way.
|
|
71
|
+
return JSON.parse(s.slice(start, at)) as string;
|
|
72
|
+
};
|
|
73
|
+
const value = (): unknown => {
|
|
74
|
+
space();
|
|
75
|
+
const c = s[at];
|
|
76
|
+
if (c === '{') {
|
|
77
|
+
at += 1;
|
|
78
|
+
const out: Record<string, unknown> = {};
|
|
79
|
+
const keys = new Set<string>();
|
|
80
|
+
space();
|
|
81
|
+
if (s[at] === '}') {
|
|
82
|
+
at += 1;
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
for (;;) {
|
|
86
|
+
space();
|
|
87
|
+
if (s[at] !== '"') fail(at, 'a key that is not a string');
|
|
88
|
+
const where = at;
|
|
89
|
+
const k = string();
|
|
90
|
+
if (keys.has(k)) fail(where, `two keys named ${JSON.stringify(k)}`);
|
|
91
|
+
keys.add(k);
|
|
92
|
+
space();
|
|
93
|
+
if (s[at] !== ':') fail(at, 'a key with no colon');
|
|
94
|
+
at += 1;
|
|
95
|
+
// Defined, never assigned: a key named `__proto__` is an own key here
|
|
96
|
+
// as it is in JSON.parse, and the value rule refuses it after.
|
|
97
|
+
Object.defineProperty(out, k, { value: value(), enumerable: true, writable: true, configurable: true });
|
|
98
|
+
space();
|
|
99
|
+
if (s[at] === ',') at += 1;
|
|
100
|
+
else if (s[at] === '}') {
|
|
101
|
+
at += 1;
|
|
102
|
+
return out;
|
|
103
|
+
} else fail(at, 'an object not closed');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (c === '[') {
|
|
107
|
+
at += 1;
|
|
108
|
+
const out: unknown[] = [];
|
|
109
|
+
space();
|
|
110
|
+
if (s[at] === ']') {
|
|
111
|
+
at += 1;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
for (;;) {
|
|
115
|
+
out.push(value());
|
|
116
|
+
space();
|
|
117
|
+
if (s[at] === ',') at += 1;
|
|
118
|
+
else if (s[at] === ']') {
|
|
119
|
+
at += 1;
|
|
120
|
+
return out;
|
|
121
|
+
} else fail(at, 'a list not closed');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (c === '"') return string();
|
|
125
|
+
for (const [word, v] of [['true', true], ['false', false], ['null', null]] as const) {
|
|
126
|
+
if (s.startsWith(word, at)) {
|
|
127
|
+
at += word.length;
|
|
128
|
+
return v;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const m = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
132
|
+
m.lastIndex = at;
|
|
133
|
+
const hit = m.exec(s);
|
|
134
|
+
if (!hit) return fail(at, 'no value');
|
|
135
|
+
const why = numberFault(hit[0]);
|
|
136
|
+
if (why !== null) fail(at, `${hit[0]} is ${why}`);
|
|
137
|
+
at += hit[0].length;
|
|
138
|
+
return Number(hit[0]);
|
|
139
|
+
};
|
|
140
|
+
const out = value();
|
|
141
|
+
space();
|
|
142
|
+
if (at !== s.length) fail(at, 'text after the value');
|
|
143
|
+
return out;
|
|
144
|
+
}
|
package/src/ward/partition.ts
CHANGED
|
@@ -312,7 +312,7 @@ export const restoreRow = (p: Partition, row: string, value: Record<string, unkn
|
|
|
312
312
|
drop(p.beings ?? {}, row);
|
|
313
313
|
// Her bind goes with her: a being the store never kept has no relation
|
|
314
314
|
// anybody is standing on, so there is nothing to leave forward.
|
|
315
|
-
drop(
|
|
315
|
+
drop(p.bind ?? {}, row);
|
|
316
316
|
return;
|
|
317
317
|
}
|
|
318
318
|
// Behind the guard and never through it: a being's cells in a partition
|
|
@@ -326,7 +326,7 @@ export const restoreRow = (p: Partition, row: string, value: Record<string, unkn
|
|
|
326
326
|
for (const k of Object.keys(held)) drop(held, k);
|
|
327
327
|
for (const [k, v] of Object.entries(kept)) put(held, k, v);
|
|
328
328
|
};
|
|
329
|
-
into(
|
|
329
|
+
into(p.beings ?? {}, (value.cells ?? emptyCells()) as unknown as Record<string, unknown>);
|
|
330
330
|
};
|
|
331
331
|
|
|
332
332
|
// The partition back from its rows. A head that is absent is a partition
|
package/src/ward/seal.ts
CHANGED
|
@@ -23,9 +23,9 @@ import type { DoorWord, Json, JsonObject } from '../being/types.ts';
|
|
|
23
23
|
import { isDoorWord } from '../being/silence.ts';
|
|
24
24
|
import { KEY, SIGNATURE, box, concat, derive, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.ts';
|
|
25
25
|
import { cellFault } from './cells.ts';
|
|
26
|
+
import { readJson } from './json.ts';
|
|
26
27
|
|
|
27
28
|
const utf8 = new TextEncoder();
|
|
28
|
-
const text = new TextDecoder();
|
|
29
29
|
|
|
30
30
|
export type WardKey = { sign: Uint8Array; padlock: Uint8Array; signPk: Uint8Array; padlockPk: Uint8Array; pk: string };
|
|
31
31
|
|
|
@@ -54,7 +54,7 @@ export const SIZE = 1024 * 1024;
|
|
|
54
54
|
// designs and no separation at all: one secret would be doing two jobs with
|
|
55
55
|
// nothing said about it, and a second kit would have to reproduce a
|
|
56
56
|
// construction nobody named. HKDF-SHA-256 under a label is the separation
|
|
57
|
-
// said out loud, and it is what `
|
|
57
|
+
// said out loud, and it is what `quo/vectors/framing.json` pins.
|
|
58
58
|
//
|
|
59
59
|
// Bytes are key material and text is not. A seed handed in as bytes of the
|
|
60
60
|
// key length is taken as it stands, which is what a harbor mints; anything
|
|
@@ -115,7 +115,7 @@ export async function openAsk(bytes: Uint8Array, padlockSecret: Uint8Array): Pro
|
|
|
115
115
|
if (inside.length <= SIGNATURE) return null;
|
|
116
116
|
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
117
117
|
const signature = inside.subarray(inside.length - SIGNATURE);
|
|
118
|
-
const payload =
|
|
118
|
+
const payload = readJson(body) as Sealed;
|
|
119
119
|
// The heir, or null for the public being. Absent is not null: a payload
|
|
120
120
|
// that never named one is malformed, like any other missing field.
|
|
121
121
|
const to = payload.to;
|
|
@@ -150,7 +150,9 @@ export async function sealReply(reply: ReplyPayload, ephemeralPk: Uint8Array, wa
|
|
|
150
150
|
// lied, or it is not a reply. Every ward on the wire is somebody else's code,
|
|
151
151
|
// so the shape is checked here and nowhere later: a reply is `{ silence }`,
|
|
152
152
|
// `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
|
|
153
|
-
// object
|
|
153
|
+
// object that is a value and seen a digest or null. A field beside a shape's
|
|
154
|
+
// own makes it none of the three, since a kit that dropped the field would
|
|
155
|
+
// hand her an answer where this one hands her silence.
|
|
154
156
|
// Anything else is bytes that said nothing, and her ward never hands her a
|
|
155
157
|
// throw or an absent value for what a stranger wrote.
|
|
156
158
|
export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, signPk: Uint8Array): Promise<ReplyPayload | null> {
|
|
@@ -160,13 +162,15 @@ export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, sig
|
|
|
160
162
|
if (inside.length <= SIGNATURE) return null;
|
|
161
163
|
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
162
164
|
if (!(await verify(body, inside.subarray(inside.length - SIGNATURE), signPk))) return null;
|
|
163
|
-
const reply =
|
|
165
|
+
const reply = readJson(body);
|
|
164
166
|
if (reply === null || typeof reply !== 'object' || Array.isArray(reply)) return null;
|
|
165
167
|
const r = reply as Record<string, unknown>;
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
if (
|
|
168
|
+
const keys = Object.keys(r).sort().join(',');
|
|
169
|
+
if (keys === 'silence') return r.silence === true ? { silence: true } : null;
|
|
170
|
+
if (keys === 'quo') return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
171
|
+
if (keys !== 'object,seen') return null;
|
|
169
172
|
if (r.seen !== null && !isHex(r.seen)) return null;
|
|
173
|
+
if (cellFault(r.object, 'object') !== null) return null; // values in both directions: nested past the bound is none of the shapes
|
|
170
174
|
return { object: r.object as Json, seen: r.seen };
|
|
171
175
|
} catch {
|
|
172
176
|
return null;
|
package/src/ward/stance.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type Inside = {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
// An invitation as it travels: the far ward's pk, the heir pk, the heir's secret seed. Or just the ward, for its public being.
|
|
44
|
-
|
|
44
|
+
type SealedInvitation ={ ward: string; heir?: string; secret?: string };
|
|
45
45
|
|
|
46
46
|
// The three calls on `standings` share the object with the ids she takes, so
|
|
47
47
|
// a standing named after one of them would be unreachable: `standings.knock`
|