nervur 0.19.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -101
- package/dist/being/being.d.ts +7 -15
- package/dist/being/being.js +16 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +9 -0
- package/dist/being/faculty.js +68 -0
- package/dist/being/index.d.ts +4 -5
- package/dist/being/index.js +5 -5
- package/dist/being/types.d.ts +35 -62
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/contract/index.d.ts +26 -0
- package/dist/contract/index.js +19 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +13 -0
- package/dist/folder/index.js +125 -0
- package/dist/harbor/dock.d.ts +19 -0
- package/dist/harbor/dock.js +92 -0
- package/dist/harbor/harbor.d.ts +22 -0
- package/dist/harbor/harbor.js +84 -0
- package/dist/harbor/index.d.ts +4 -10
- package/dist/harbor/index.js +5 -18
- package/dist/harbor/registry.d.ts +7 -0
- package/dist/harbor/registry.js +13 -0
- package/dist/harbor/terrain.d.ts +8 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +20 -0
- package/dist/pointer/bodies.js +45 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +34 -0
- package/dist/pointer/world.js +76 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- package/dist/ward/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +5 -10
- package/dist/ward/index.js +6 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/stance.d.ts +29 -20
- package/dist/ward/stance.js +173 -406
- package/dist/ward/ward-being.d.ts +33 -0
- package/dist/ward/ward-being.js +76 -0
- package/dist/ward/ward.d.ts +43 -11
- package/dist/ward/ward.js +215 -363
- package/package.json +19 -33
- package/src/being/being.ts +31 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +66 -0
- package/src/being/index.ts +5 -6
- package/src/being/types.ts +47 -145
- package/src/being/words.ts +31 -0
- package/src/contract/index.ts +44 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +134 -0
- package/src/harbor/dock.ts +101 -0
- package/src/harbor/harbor.ts +105 -0
- package/src/harbor/index.ts +5 -19
- package/src/harbor/registry.ts +20 -0
- package/src/harbor/terrain.ts +13 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +47 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +91 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +19 -0
- package/src/stand/stand.ts +178 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +6 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/stance.ts +185 -420
- package/src/ward/ward-being.ts +97 -0
- package/src/ward/ward.ts +229 -363
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Ephemeral, type Draw, type SigningKey } from './keys.ts';
|
|
2
|
+
export declare const SIZE = 1048576;
|
|
3
|
+
export declare const HEAD = 32;
|
|
4
|
+
export declare const SIGNATURE = 64;
|
|
5
|
+
export declare const SEALED_HEAD: number;
|
|
6
|
+
export declare const ZERO_EDGE: Uint8Array<ArrayBuffer>;
|
|
7
|
+
export declare const ZERO_HEAD: Uint8Array<ArrayBuffer>;
|
|
8
|
+
export declare const knockEdge: (shared: Uint8Array) => Promise<Uint8Array>;
|
|
9
|
+
export declare const follow: (edge: Uint8Array, agreement: Uint8Array) => Promise<Uint8Array>;
|
|
10
|
+
export declare const signBody: (text: Uint8Array, key: SigningKey) => Promise<Uint8Array>;
|
|
11
|
+
export declare const splitBody: (body: Uint8Array) => {
|
|
12
|
+
text: Uint8Array;
|
|
13
|
+
signature: Uint8Array;
|
|
14
|
+
} | null;
|
|
15
|
+
export declare const checkBody: (pk: Uint8Array, body: {
|
|
16
|
+
text: Uint8Array;
|
|
17
|
+
signature: Uint8Array;
|
|
18
|
+
}) => Promise<boolean>;
|
|
19
|
+
export declare const sealAsk: (lid: Ephemeral, padlock: Uint8Array, head: Uint8Array, edge: Uint8Array, body: Uint8Array, ciphertext?: Uint8Array) => Promise<Uint8Array | null>;
|
|
20
|
+
export type OpenedHead = {
|
|
21
|
+
lid: Uint8Array;
|
|
22
|
+
agreement: Uint8Array;
|
|
23
|
+
head: Uint8Array;
|
|
24
|
+
rest: Uint8Array;
|
|
25
|
+
};
|
|
26
|
+
export declare const openHead: (bytes: Uint8Array, agreement: Uint8Array) => Promise<OpenedHead | null>;
|
|
27
|
+
export declare const splitKnock: (rest: Uint8Array) => {
|
|
28
|
+
ciphertext: Uint8Array;
|
|
29
|
+
sealedBody: Uint8Array;
|
|
30
|
+
} | null;
|
|
31
|
+
export declare const openBody: (opened: OpenedHead, edge: Uint8Array, sealedBody: Uint8Array) => Promise<Uint8Array | null>;
|
|
32
|
+
export type SealedReply = {
|
|
33
|
+
bytes: Uint8Array;
|
|
34
|
+
agreement: Uint8Array;
|
|
35
|
+
};
|
|
36
|
+
export type ReplyKey = {
|
|
37
|
+
ephemeral: Ephemeral;
|
|
38
|
+
agreement: Uint8Array;
|
|
39
|
+
};
|
|
40
|
+
export declare const replyKey: (draw: Draw, lid: Uint8Array) => Promise<ReplyKey | null>;
|
|
41
|
+
export declare const sealReply: (key: ReplyKey, body: Uint8Array) => Promise<Uint8Array>;
|
|
42
|
+
export declare const openReply: (bytes: Uint8Array, lid: Ephemeral) => Promise<SealedReply | null>;
|
package/dist/quo/seal.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The three boxes and the keys between them.
|
|
3
|
+
//
|
|
4
|
+
// ask box lid (32) || AES( head ) || AES( body )
|
|
5
|
+
// knock box lid (32) || AES( head ) || ML-KEM ciphertext (1088) || AES( body )
|
|
6
|
+
// reply box ephemeral pk (32) || AES( body )
|
|
7
|
+
// body JSON text || signature (64)
|
|
8
|
+
//
|
|
9
|
+
// The head of an ask and the body of a reply are sealed under `quo-seal`
|
|
10
|
+
// over the agreement. The body of an ask is sealed under `quo-edge-seal`
|
|
11
|
+
// over the agreement and then the edge key. Every ciphertext's additional
|
|
12
|
+
// data is its own box's ephemeral pk.
|
|
13
|
+
import { CIPHERTEXT, concat, decrypt, encrypt, hkdf, sign, TAG, verify } from '../crypto/index.js';
|
|
14
|
+
import { Ephemeral, LABEL } from './keys.js';
|
|
15
|
+
export const SIZE = 1_048_576;
|
|
16
|
+
export const HEAD = 32;
|
|
17
|
+
export const SIGNATURE = 64;
|
|
18
|
+
export const SEALED_HEAD = HEAD + TAG;
|
|
19
|
+
export const ZERO_EDGE = new Uint8Array(32);
|
|
20
|
+
export const ZERO_HEAD = new Uint8Array(HEAD);
|
|
21
|
+
const cipher = async (label, ...material) => {
|
|
22
|
+
const out = await hkdf(concat(...material), label, 44);
|
|
23
|
+
return { key: out.subarray(0, 32), nonce: out.subarray(32) };
|
|
24
|
+
};
|
|
25
|
+
const seal = async (label, material, lid, plaintext) => {
|
|
26
|
+
const { key, nonce } = await cipher(label, ...material);
|
|
27
|
+
return encrypt(key, nonce, lid, plaintext);
|
|
28
|
+
};
|
|
29
|
+
const open = async (label, material, lid, ciphertext) => {
|
|
30
|
+
const { key, nonce } = await cipher(label, ...material);
|
|
31
|
+
return decrypt(key, nonce, lid, ciphertext);
|
|
32
|
+
};
|
|
33
|
+
// A knock's edge key, from the shared secret the lock decapsulates.
|
|
34
|
+
export const knockEdge = (shared) => hkdf(shared, LABEL.lock, 32);
|
|
35
|
+
// The edge key that follows `edge` after a reply of this agreement.
|
|
36
|
+
export const follow = (edge, agreement) => hkdf(concat(edge, agreement), LABEL.edge, 32);
|
|
37
|
+
export const signBody = async (text, key) => concat(text, await sign(key.secret, text));
|
|
38
|
+
// The text and signature of a body, or null for a body of sixty-four bytes
|
|
39
|
+
// or fewer.
|
|
40
|
+
export const splitBody = (body) => body.length <= SIGNATURE ? null : { text: body.subarray(0, -SIGNATURE), signature: body.subarray(-SIGNATURE) };
|
|
41
|
+
export const checkBody = (pk, body) => verify(pk, body.text, body.signature);
|
|
42
|
+
// An ask box sealed under `lid` to a padlock, or null for a padlock that
|
|
43
|
+
// takes no seal. `ciphertext` makes it a knock box.
|
|
44
|
+
export const sealAsk = async (lid, padlock, head, edge, body, ciphertext) => {
|
|
45
|
+
const agreement = await lid.agree(padlock);
|
|
46
|
+
if (agreement === null)
|
|
47
|
+
return null;
|
|
48
|
+
const sealedHead = await seal(LABEL.seal, [agreement], lid.pk, head);
|
|
49
|
+
const sealedBody = await seal(LABEL.edgeSeal, [agreement, edge], lid.pk, body);
|
|
50
|
+
return concat(lid.pk, sealedHead, ciphertext ?? new Uint8Array(0), sealedBody);
|
|
51
|
+
};
|
|
52
|
+
export const openHead = async (bytes, agreement) => {
|
|
53
|
+
if (bytes.length < HEAD + SEALED_HEAD)
|
|
54
|
+
return null;
|
|
55
|
+
const lid = bytes.subarray(0, HEAD);
|
|
56
|
+
const head = await open(LABEL.seal, [agreement], lid, bytes.subarray(HEAD, HEAD + SEALED_HEAD));
|
|
57
|
+
return head === null ? null : { lid, agreement, head, rest: bytes.subarray(HEAD + SEALED_HEAD) };
|
|
58
|
+
};
|
|
59
|
+
// A knock's rest split into its ciphertext and sealed body.
|
|
60
|
+
export const splitKnock = (rest) => rest.length < CIPHERTEXT ? null : { ciphertext: rest.subarray(0, CIPHERTEXT), sealedBody: rest.subarray(CIPHERTEXT) };
|
|
61
|
+
export const openBody = (opened, edge, sealedBody) => open(LABEL.edgeSeal, [opened.agreement, edge], opened.lid, sealedBody);
|
|
62
|
+
export const replyKey = async (draw, lid) => {
|
|
63
|
+
const ephemeral = await Ephemeral.draw(draw);
|
|
64
|
+
const agreement = await ephemeral.agree(lid);
|
|
65
|
+
return agreement === null ? null : { ephemeral, agreement };
|
|
66
|
+
};
|
|
67
|
+
export const sealReply = async (key, body) => concat(key.ephemeral.pk, await seal(LABEL.seal, [key.agreement], key.ephemeral.pk, body));
|
|
68
|
+
// A reply's body and agreement, opened with the lid's secret, or null.
|
|
69
|
+
export const openReply = async (bytes, lid) => {
|
|
70
|
+
if (bytes.length > SIZE || bytes.length < HEAD + TAG)
|
|
71
|
+
return null;
|
|
72
|
+
const pk = bytes.subarray(0, HEAD);
|
|
73
|
+
const agreement = await lid.agree(pk);
|
|
74
|
+
if (agreement === null)
|
|
75
|
+
return null;
|
|
76
|
+
const body = await open(LABEL.seal, [agreement], pk, bytes.subarray(HEAD));
|
|
77
|
+
return body === null ? null : { bytes: body, agreement };
|
|
78
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Invitation } from './invitation.ts';
|
|
2
|
+
import { Ephemeral, type Draw } from './keys.ts';
|
|
3
|
+
import { type Reply } from './reply.ts';
|
|
4
|
+
export type Read = Reply | {
|
|
5
|
+
readonly nothing: true;
|
|
6
|
+
};
|
|
7
|
+
type Knock = {
|
|
8
|
+
readonly bytes: string;
|
|
9
|
+
readonly lid: string;
|
|
10
|
+
readonly next: string;
|
|
11
|
+
readonly edge: string;
|
|
12
|
+
readonly seq: number;
|
|
13
|
+
};
|
|
14
|
+
export type StandingState = {
|
|
15
|
+
readonly key: string | null;
|
|
16
|
+
readonly edge: string | null;
|
|
17
|
+
readonly sent: number;
|
|
18
|
+
readonly moved: number;
|
|
19
|
+
readonly knock: Knock | null;
|
|
20
|
+
readonly probed: boolean;
|
|
21
|
+
};
|
|
22
|
+
export declare const freshStanding: () => StandingState;
|
|
23
|
+
export type Sent = {
|
|
24
|
+
readonly bytes: Uint8Array;
|
|
25
|
+
readonly lid: Ephemeral;
|
|
26
|
+
readonly next: string;
|
|
27
|
+
readonly edge: string;
|
|
28
|
+
readonly seq: number;
|
|
29
|
+
};
|
|
30
|
+
export declare class Standing {
|
|
31
|
+
#private;
|
|
32
|
+
readonly invitation: Invitation;
|
|
33
|
+
state: StandingState;
|
|
34
|
+
readonly draw: Draw;
|
|
35
|
+
constructor(invitation: Invitation, state: StandingState, draw: Draw);
|
|
36
|
+
ask(method?: string, args?: string): Promise<Sent | null>;
|
|
37
|
+
read(sent: Sent, bytes: Uint8Array | null): Promise<Read>;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The standing's side of a relation: it seals asks and reads replies, and
|
|
3
|
+
// moves to its announced key and the following edge key when an object
|
|
4
|
+
// comes back to an ask above every ask it has moved on, and on nothing else.
|
|
5
|
+
//
|
|
6
|
+
// Its state is a value, so a ward keeps it in its partition.
|
|
7
|
+
import { encapsulate, hex, unhex } from '../crypto/index.js';
|
|
8
|
+
import { Ephemeral, SigningKey, wardHalves } from './keys.js';
|
|
9
|
+
import { writePayload } from './payload.js';
|
|
10
|
+
import { readReply } from './reply.js';
|
|
11
|
+
import { checkBody, follow, knockEdge, openReply, sealAsk, signBody, splitBody } from './seal.js';
|
|
12
|
+
export const freshStanding = () => ({ key: null, edge: null, sent: 0, moved: 0, knock: null, probed: false });
|
|
13
|
+
export class Standing {
|
|
14
|
+
invitation;
|
|
15
|
+
state;
|
|
16
|
+
draw;
|
|
17
|
+
constructor(invitation, state, draw) {
|
|
18
|
+
this.invitation = invitation;
|
|
19
|
+
this.state = state;
|
|
20
|
+
this.draw = draw;
|
|
21
|
+
}
|
|
22
|
+
async ask(method, args) {
|
|
23
|
+
const s = this.state;
|
|
24
|
+
if (s.key !== null)
|
|
25
|
+
return this.#ask(unhex(s.key), unhex(s.edge), method, args);
|
|
26
|
+
if (s.knock === null)
|
|
27
|
+
return this.#knock(method, args);
|
|
28
|
+
if (!s.probed) {
|
|
29
|
+
this.state = { ...s, probed: true };
|
|
30
|
+
return this.#ask(unhex(s.knock.next), unhex(s.knock.edge), method, args);
|
|
31
|
+
}
|
|
32
|
+
this.state = { ...s, probed: false };
|
|
33
|
+
// The same bytes again: a new lid would be a new box.
|
|
34
|
+
return { bytes: unhex(s.knock.bytes), lid: await Ephemeral.from(unhex(s.knock.lid)), next: s.knock.next, edge: s.knock.edge, seq: s.knock.seq };
|
|
35
|
+
}
|
|
36
|
+
async read(sent, bytes) {
|
|
37
|
+
if (bytes === null)
|
|
38
|
+
return { nothing: true };
|
|
39
|
+
const reply = await this.#open(sent, bytes);
|
|
40
|
+
if (reply === null)
|
|
41
|
+
return { silence: true };
|
|
42
|
+
if ('object' in reply.text && sent.seq > this.state.moved) {
|
|
43
|
+
const edge = await follow(unhex(sent.edge), reply.agreement);
|
|
44
|
+
this.state = { ...this.state, key: sent.next, edge: hex(edge), moved: sent.seq, knock: null, probed: false };
|
|
45
|
+
}
|
|
46
|
+
return reply.text;
|
|
47
|
+
}
|
|
48
|
+
async #knock(method, args) {
|
|
49
|
+
const heir = await SigningKey.from(unhex(this.invitation.secret));
|
|
50
|
+
const next = await SigningKey.draw(this.draw);
|
|
51
|
+
const lid = await Ephemeral.draw(this.draw);
|
|
52
|
+
const sent = encapsulate(unhex(this.invitation.lock), this.draw(32));
|
|
53
|
+
if (sent === null)
|
|
54
|
+
return null;
|
|
55
|
+
const edge = await knockEdge(sent.shared);
|
|
56
|
+
const seq = this.state.sent + 1;
|
|
57
|
+
const body = await this.#body(heir, next, seq, method, args);
|
|
58
|
+
const bytes = await sealAsk(lid, unhex(wardHalves(this.invitation.ward).padlock), unhex(this.invitation.heir), edge, body, sent.ciphertext);
|
|
59
|
+
if (bytes === null)
|
|
60
|
+
return null;
|
|
61
|
+
this.state = { ...this.state, sent: seq, knock: { bytes: hex(bytes), lid: hex(lid.secret), next: hex(next.secret), edge: hex(edge), seq } };
|
|
62
|
+
return { bytes, lid, next: hex(next.secret), edge: hex(edge), seq };
|
|
63
|
+
}
|
|
64
|
+
async #ask(secret, edge, method, args) {
|
|
65
|
+
const by = await SigningKey.from(secret);
|
|
66
|
+
const next = await SigningKey.draw(this.draw);
|
|
67
|
+
const lid = await Ephemeral.draw(this.draw);
|
|
68
|
+
const seq = this.state.sent + 1;
|
|
69
|
+
const body = await this.#body(by, next, seq, method, args);
|
|
70
|
+
const bytes = await sealAsk(lid, unhex(wardHalves(this.invitation.ward).padlock), unhex(this.invitation.heir), edge, body);
|
|
71
|
+
if (bytes === null)
|
|
72
|
+
return null;
|
|
73
|
+
this.state = { ...this.state, sent: seq };
|
|
74
|
+
return { bytes, lid, next: hex(next.secret), edge: hex(edge), seq };
|
|
75
|
+
}
|
|
76
|
+
#body(by, next, seq, method, args) {
|
|
77
|
+
const text = writePayload({ to: this.invitation.heir, by: by.id, next: next.id, seq, ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args }) });
|
|
78
|
+
return signBody(new TextEncoder().encode(text), by);
|
|
79
|
+
}
|
|
80
|
+
async #open(sent, bytes) {
|
|
81
|
+
const opened = await openReply(bytes, sent.lid);
|
|
82
|
+
const body = opened && splitBody(opened.bytes);
|
|
83
|
+
if (!opened || !body)
|
|
84
|
+
return null;
|
|
85
|
+
if (!(await checkBody(unhex(wardHalves(this.invitation.ward).signing), body)))
|
|
86
|
+
return null;
|
|
87
|
+
const text = readReply(body.text);
|
|
88
|
+
return text && { text, agreement: opened.agreement };
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/ward/allowance.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const CEILING: Allowance;
|
|
7
|
-
export declare function allow(wanted: Wanted | undefined, ceiling?: Allowance, base?: Allowance): Allowance;
|
|
8
|
-
export declare const spent: (a: Allowance) => boolean;
|
|
1
|
+
import type { Clock } from '../contract/index.ts';
|
|
2
|
+
import type { Wanted } from '../being/index.ts';
|
|
3
|
+
export declare const DEFAULT = 30000;
|
|
4
|
+
export declare const CEILING = 300000;
|
|
5
|
+
export declare const allow: (wanted: Wanted | undefined) => number;
|
|
9
6
|
export declare const LATE: unique symbol;
|
|
10
|
-
export
|
|
7
|
+
export type Late = typeof LATE;
|
|
8
|
+
export declare class Bell {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(clock: Clock, ms: number);
|
|
11
|
+
get rang(): boolean;
|
|
12
|
+
race<T>(work: Promise<T>): Promise<T | Late>;
|
|
13
|
+
cancel(): void;
|
|
14
|
+
}
|
package/dist/ward/allowance.js
CHANGED
|
@@ -1,71 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// refusal there. The sender bounds the whole of her ask to the same number,
|
|
8
|
-
// and a wait that ran out is the word late, never silence and never unreached.
|
|
9
|
-
//
|
|
10
|
-
// Each ask is bounded on its own. The time an arriving call has left does not
|
|
11
|
-
// bound the asks a being makes while answering it: attributing her onward ask
|
|
12
|
-
// to the arrival that caused it needs a context the ward cannot get without
|
|
13
|
-
// naming a runtime, and the ward names none. Every wait still ends.
|
|
14
|
-
// How wide the default is, and how wide she may ask for, are the ward's own —
|
|
15
|
-
// wider is more patient, and no peer can tell the difference except by being
|
|
16
|
-
// refused. A being who never learns the third parameter exists writes correct
|
|
17
|
-
// code forever under these. They are two numbers because the third argument
|
|
18
|
-
// goes both ways: less for the ask she wants back quickly, more for the one
|
|
19
|
-
// she knows is slow.
|
|
20
|
-
export const DEFAULT = { time: 30_000 };
|
|
21
|
-
export const CEILING = { time: 300_000 };
|
|
22
|
-
// What she asked for, held to what her ward allows. A being cannot mint budget
|
|
23
|
-
// nobody granted her: asking for a minute where the ward gives thirty seconds
|
|
24
|
-
// is thirty seconds, silently, because the ceiling is not hers to know.
|
|
25
|
-
// Asking for nothing at all is the default, which is the whole point.
|
|
26
|
-
export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
|
|
27
|
-
// Floored first, then tested: a fraction of a millisecond is not a budget,
|
|
28
|
-
// and falls to the default like every other number that is not a positive
|
|
29
|
-
// whole one.
|
|
30
|
-
const whole = (n, fallback) => {
|
|
31
|
-
const w = typeof n === 'number' ? Math.floor(n) : NaN;
|
|
32
|
-
return Number.isFinite(w) && w > 0 ? w : fallback;
|
|
33
|
-
};
|
|
34
|
-
return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
|
|
35
|
-
}
|
|
36
|
-
// Whether a budget has anything left to spend, and the one reader of it: the
|
|
37
|
-
// door calls this on arrival and nothing else asks the question anywhere. A
|
|
38
|
-
// time that is not a whole number above zero is not a budget that ran out, it
|
|
39
|
-
// is no budget at all, and the door refuses both the same way, since a peer
|
|
40
|
-
// learns nothing from the difference. `allow` never makes one, so only bytes
|
|
41
|
-
// off the road ever carry one.
|
|
42
|
-
export const spent = (a) => !(Number.isSafeInteger(a.time) && a.time > 0);
|
|
43
|
-
// What a wait that ran out comes back as. Its own value, held by nobody
|
|
44
|
-
// outside the ward, so no answer from any door can be mistaken for it.
|
|
1
|
+
export const DEFAULT = 30_000;
|
|
2
|
+
export const CEILING = 300_000;
|
|
3
|
+
export const allow = (wanted) => {
|
|
4
|
+
const whole = typeof wanted?.time === 'number' ? Math.floor(wanted.time) : NaN;
|
|
5
|
+
return Math.min(Number.isFinite(whole) && whole > 0 ? whole : DEFAULT, CEILING);
|
|
6
|
+
};
|
|
45
7
|
export const LATE = Symbol('late');
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
8
|
+
// One ask's bell. It bounds the wait of the being who asked and the send
|
|
9
|
+
// that holds the relation's line, so a far side that never answers holds
|
|
10
|
+
// neither for longer than the allowance.
|
|
11
|
+
export class Bell {
|
|
12
|
+
#wait;
|
|
13
|
+
#late;
|
|
14
|
+
#rang = false;
|
|
15
|
+
constructor(clock, ms) {
|
|
16
|
+
this.#wait = clock.wait(ms);
|
|
17
|
+
this.#late = this.#wait.done.then(() => {
|
|
18
|
+
this.#rang = true;
|
|
19
|
+
return LATE;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
get rang() {
|
|
23
|
+
return this.#rang;
|
|
24
|
+
}
|
|
25
|
+
race(work) {
|
|
26
|
+
work.catch(() => { });
|
|
27
|
+
return Promise.race([work, this.#late]);
|
|
66
28
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
void work.then(() => { }, () => { }); // whatever it was, it is nobody's answer now
|
|
29
|
+
cancel() {
|
|
30
|
+
this.#wait.cancel();
|
|
70
31
|
}
|
|
71
32
|
}
|
package/dist/ward/cells.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { JsonObject } from '../being/index.ts';
|
|
2
2
|
export declare const DEPTH = 64;
|
|
3
|
-
export declare
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
export declare class Cells {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(wrote: () => void);
|
|
6
|
+
guard(cells: JsonObject): JsonObject;
|
|
7
|
+
}
|
package/dist/ward/cells.js
CHANGED
|
@@ -1,186 +1,74 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// nesting that forty levels deep is inside the depth bound and is a trillion
|
|
8
|
-
// values once written down. What is walked here is the object graph, which is
|
|
9
|
-
// small; what a harbor keeps is the tree it expands to, which is not. So the
|
|
10
|
-
// walk counts the tree while it proves the graph, and refuses a value nobody
|
|
11
|
-
// could write down. This is a bound on what a being holds, so it is this
|
|
12
|
-
// kit's number and no word of the spec.
|
|
13
|
-
export const BREADTH = 1 << 20;
|
|
14
|
-
// Text a harbor can write down: every surrogate in a pair. A lone one is a
|
|
15
|
-
// code unit with no character, which JSON escapes and UTF-8 cannot encode at
|
|
16
|
-
// all, so a harbor that keeps strings as bytes loses it and one that keeps
|
|
17
|
-
// the object does not. I-JSON says a string is text, and this is that rule
|
|
18
|
-
// where the value is written rather than where it is read.
|
|
19
|
-
const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
|
|
20
|
-
// I-JSON, all the way down. A number that JSON cannot write is not a number
|
|
21
|
-
// a harbor can keep, a key on the prototype is not a key she wrote, and a
|
|
22
|
-
// hole in a list, or a key named `__proto__`, is a thing JSON writes one way
|
|
23
|
-
// and a runtime reads another.
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A being's cells, guarded. Her cells are kept by whatever memory her
|
|
3
|
+
// terrain gives, and a memory that writes JSON would hand back something
|
|
4
|
+
// other than what she wrote if she wrote anything JSON cannot hold. So a
|
|
5
|
+
// write that is not a value throws where she wrote it, in her own frame,
|
|
6
|
+
// and her answer becomes silence like any throw.
|
|
24
7
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (typeof v === 'string')
|
|
32
|
-
return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
|
|
33
|
-
if (typeof v === 'number') {
|
|
34
|
-
if (!Number.isFinite(v))
|
|
35
|
-
return `${path} is ${String(v)}, which no harbor can write down`;
|
|
36
|
-
// Minus zero is a number JSON writes as `0` and reads back as `0`. A
|
|
37
|
-
// harbor that keeps values would hand her the sign she wrote and one that
|
|
38
|
-
// writes JSON would not, and she is never told which she is standing in.
|
|
39
|
-
return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
|
|
40
|
-
}
|
|
41
|
-
if (typeof v !== 'object')
|
|
42
|
-
return `${path} is a ${typeof v}, which is not a value`;
|
|
43
|
-
if (seen.has(v))
|
|
44
|
-
return `${path} refers back to itself`;
|
|
45
|
-
const counted = sized.get(v);
|
|
46
|
-
if (counted !== undefined)
|
|
47
|
-
return counted;
|
|
48
|
-
if (depth >= DEPTH)
|
|
49
|
-
return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
|
|
50
|
-
seen.add(v);
|
|
51
|
-
try {
|
|
52
|
-
let size = 1;
|
|
53
|
-
const under = (where, x) => {
|
|
54
|
-
const r = walk(x, where, seen, sized, depth + 1);
|
|
55
|
-
if (typeof r === 'string')
|
|
56
|
-
return r;
|
|
57
|
-
size += r;
|
|
58
|
-
return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
|
|
59
|
-
};
|
|
60
|
-
if (Array.isArray(v)) {
|
|
61
|
-
for (let i = 0; i < v.length; i += 1) {
|
|
62
|
-
if (!(i in v))
|
|
63
|
-
return `${path}[${i}] is a hole, which no harbor can write down`;
|
|
64
|
-
const f = under(`${path}[${i}]`, v[i]);
|
|
65
|
-
if (f)
|
|
66
|
-
return f;
|
|
67
|
-
}
|
|
68
|
-
sized.set(v, size);
|
|
69
|
-
return size;
|
|
70
|
-
}
|
|
71
|
-
const proto = Object.getPrototypeOf(v);
|
|
72
|
-
if (proto !== Object.prototype && proto !== null)
|
|
73
|
-
return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
|
|
74
|
-
for (const k of Object.keys(v)) {
|
|
75
|
-
if (k === '__proto__')
|
|
76
|
-
return `${path}.__proto__ is a key no harbor can keep`;
|
|
77
|
-
const f = under(`${path}.${k}`, v[k]);
|
|
78
|
-
if (f)
|
|
79
|
-
return f;
|
|
80
|
-
}
|
|
81
|
-
sized.set(v, size);
|
|
82
|
-
return size;
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
seen.delete(v);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
export const cellFault = (v, path) => {
|
|
89
|
-
const out = walk(v, path, new Set(), new Map(), 0);
|
|
90
|
-
return typeof out === 'string' ? out : null;
|
|
91
|
-
};
|
|
92
|
-
// The three keys at the root of her cells that are the ward's: it writes
|
|
93
|
-
// them, she reads them, and a write of hers there is refused like a non-value.
|
|
94
|
-
const WARDS = new Set(['standings', 'occupants', 'class']);
|
|
95
|
-
// The guard is one proxy at the root and one for every container read through
|
|
96
|
-
// it, so a write nested three deep is refused the same way a write at the top
|
|
97
|
-
// is. Wrappers are remembered, so reading the same array twice is the same
|
|
98
|
-
// object twice and a being may still compare what she holds.
|
|
99
|
-
const wrapped = new WeakMap();
|
|
100
|
-
const targets = new WeakMap();
|
|
101
|
-
const guards = new WeakSet();
|
|
102
|
-
// A name no partition can hold, wherever it appears. It is here beside the
|
|
103
|
-
// guard that refuses it, because the ward refuses it too: a relation named
|
|
104
|
-
// this one would be written into the bind table and then throw on her cells,
|
|
105
|
-
// which is half a relation and a throw where a null is promised. One rule,
|
|
106
|
-
// one place, read by the guard and by the stance.
|
|
107
|
-
export const unkeepable = (k) => k === '__proto__';
|
|
108
|
-
// A key a write may land on: a string that is not `__proto__`, and not one
|
|
109
|
-
// of the ward's at the root. A symbol key is a thing JSON never writes.
|
|
8
|
+
// What passes is stored as a fresh copy of its JSON text, so a reference
|
|
9
|
+
// she kept to what she wrote is not a way around the guard. Every key lands
|
|
10
|
+
// as an own key, `__proto__` included. A list grows at its end: a slot or a
|
|
11
|
+
// length past it would leave a hole.
|
|
12
|
+
import { writeValue } from '../crypto/index.js';
|
|
13
|
+
export const DEPTH = 64;
|
|
110
14
|
const refuse = (why) => {
|
|
111
15
|
throw new TypeError(`cells hold values: ${why}`);
|
|
112
16
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// length set past what she wrote, or a slot beyond it, would leave holes.
|
|
123
|
-
if (Array.isArray(t)) {
|
|
124
|
-
if (k === 'length') {
|
|
125
|
-
if (typeof v !== 'number' || v > t.length)
|
|
126
|
-
refuse(`${path}.length set past what she wrote would leave holes`);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const i = Number(k);
|
|
130
|
-
if (Number.isInteger(i) && i > t.length)
|
|
131
|
-
refuse(`${path}[${i}] would leave a hole`);
|
|
17
|
+
const copy = (value, room, where) => {
|
|
18
|
+
const text = writeValue(value, room);
|
|
19
|
+
return text === undefined ? refuse(`${where} is not a value JSON keeps, within ${room} levels`) : JSON.parse(text);
|
|
20
|
+
};
|
|
21
|
+
export class Cells {
|
|
22
|
+
#guards = new WeakMap();
|
|
23
|
+
#wrote;
|
|
24
|
+
constructor(wrote) {
|
|
25
|
+
this.#wrote = wrote;
|
|
132
26
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
refuse(f);
|
|
162
|
-
const ok = Reflect.defineProperty(t, k, d);
|
|
163
|
-
wrote();
|
|
164
|
-
return ok;
|
|
165
|
-
},
|
|
166
|
-
deleteProperty(t, k) {
|
|
167
|
-
if (root && typeof k === 'string' && WARDS.has(k))
|
|
168
|
-
refuse(`${path}.${k} is the ward's to keep`);
|
|
169
|
-
const ok = Reflect.deleteProperty(t, k);
|
|
170
|
-
wrote();
|
|
27
|
+
// The guarded view of a being's cells.
|
|
28
|
+
guard(cells) {
|
|
29
|
+
return this.#guard(cells, 'cells', 1);
|
|
30
|
+
}
|
|
31
|
+
#guard(target, path, level) {
|
|
32
|
+
const had = this.#guards.get(target);
|
|
33
|
+
if (had)
|
|
34
|
+
return had;
|
|
35
|
+
const room = DEPTH - level;
|
|
36
|
+
const place = (t, k, value) => {
|
|
37
|
+
if (typeof k !== 'string')
|
|
38
|
+
return refuse(`${path} takes no symbol key`);
|
|
39
|
+
const where = `${path}.${k}`;
|
|
40
|
+
if (Array.isArray(t)) {
|
|
41
|
+
if (k === 'length') {
|
|
42
|
+
if (typeof value !== 'number' || value > t.length)
|
|
43
|
+
refuse(`${where} past its end would leave a hole`);
|
|
44
|
+
t.length = value;
|
|
45
|
+
this.#wrote();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (!/^(0|[1-9]\d*)$/.test(k))
|
|
49
|
+
refuse(`${where} is not a slot of a list`);
|
|
50
|
+
if (Number(k) > t.length)
|
|
51
|
+
refuse(`${where} would leave a hole`);
|
|
52
|
+
}
|
|
53
|
+
const ok = Reflect.defineProperty(t, k, { value: copy(value, room, where), enumerable: true, writable: true, configurable: true });
|
|
54
|
+
this.#wrote();
|
|
171
55
|
return ok;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
56
|
+
};
|
|
57
|
+
const proxy = new Proxy(target, {
|
|
58
|
+
get: (t, k, r) => {
|
|
59
|
+
const v = Reflect.get(t, k, r);
|
|
60
|
+
return typeof k === 'string' && Object.hasOwn(t, k) && typeof v === 'object' && v !== null ? this.#guard(v, `${path}.${k}`, level + 1) : v;
|
|
61
|
+
},
|
|
62
|
+
set: (t, k, v) => place(t, k, v),
|
|
63
|
+
defineProperty: (t, k, d) => ('value' in d ? place(t, k, d.value) : refuse(`${path}.${String(k)} is an accessor`)),
|
|
64
|
+
deleteProperty: (t, k) => {
|
|
65
|
+
const ok = Reflect.deleteProperty(t, k);
|
|
66
|
+
this.#wrote();
|
|
67
|
+
return ok;
|
|
68
|
+
},
|
|
69
|
+
setPrototypeOf: () => refuse(`${path} keeps its prototype`),
|
|
70
|
+
});
|
|
71
|
+
this.#guards.set(target, proxy);
|
|
72
|
+
return proxy;
|
|
73
|
+
}
|
|
178
74
|
}
|
|
179
|
-
// Her cells, guarded. Called once per being at boot; the guarded object is
|
|
180
|
-
// what goes into the partition and what the stance hands her, so there is no
|
|
181
|
-
// second door onto the same cells. Every write that passes says so to the
|
|
182
|
-
// ward, which says so to its harbor.
|
|
183
|
-
export const guardCells = (cells, wrote = () => { }) => guard(cells, 'cells', wrote, true);
|
|
184
|
-
// The cells behind the guard, for the ward alone: the one writer of the
|
|
185
|
-
// three keys that are its own.
|
|
186
|
-
export const unguarded = (cells) => (targets.get(cells) ?? cells);
|