@quo-systems/quo 0.1.0 → 0.2.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 +9 -6
- package/SPEC.md +235 -137
- package/dist/being/being.d.ts +22 -0
- package/dist/being/being.js +78 -0
- package/dist/being/digest.d.ts +3 -0
- package/dist/being/digest.js +19 -0
- package/dist/being/index.d.ts +5 -0
- package/dist/being/index.js +6 -0
- package/dist/being/silence.d.ts +10 -0
- package/dist/being/silence.js +29 -0
- package/dist/being/types.d.ts +84 -0
- package/dist/being/types.js +16 -0
- package/dist/conformance/assert.d.ts +11 -0
- package/dist/conformance/assert.js +72 -0
- package/dist/conformance/beings.d.ts +114 -0
- package/dist/conformance/beings.js +126 -0
- package/dist/conformance/estate.d.ts +5 -0
- package/dist/conformance/estate.js +316 -0
- package/dist/conformance/index.d.ts +65 -0
- package/dist/conformance/index.js +448 -0
- package/dist/conformance/reach.d.ts +10 -0
- package/dist/conformance/reach.js +72 -0
- package/dist/conformance/store.d.ts +5 -0
- package/dist/conformance/store.js +97 -0
- package/dist/harbor/core.d.ts +46 -0
- package/dist/harbor/core.js +218 -0
- package/dist/harbor/dial.d.ts +8 -0
- package/dist/harbor/dial.js +45 -0
- package/dist/harbor/index.d.ts +6 -0
- package/dist/harbor/index.js +10 -0
- package/dist/harbor/memory.d.ts +24 -0
- package/dist/harbor/memory.js +68 -0
- package/dist/harbor/reach.d.ts +36 -0
- package/dist/harbor/reach.js +166 -0
- package/dist/harbor/store.d.ts +33 -0
- package/dist/harbor/store.js +41 -0
- package/dist/ward/allowance.d.ts +10 -0
- package/dist/ward/allowance.js +60 -0
- package/dist/ward/arithmetic.d.ts +26 -0
- package/dist/ward/arithmetic.js +159 -0
- package/dist/ward/cells.d.ts +3 -0
- package/dist/ward/cells.js +79 -0
- package/dist/ward/door.d.ts +16 -0
- package/dist/ward/door.js +127 -0
- package/dist/ward/ground.d.ts +15 -0
- package/dist/ward/ground.js +1 -0
- package/dist/ward/heirs.d.ts +14 -0
- package/dist/ward/heirs.js +102 -0
- package/dist/ward/index.d.ts +8 -0
- package/dist/ward/index.js +10 -0
- package/dist/ward/owner.d.ts +32 -0
- package/dist/ward/owner.js +116 -0
- package/dist/ward/partition.d.ts +57 -0
- package/dist/ward/partition.js +64 -0
- package/dist/ward/seal.d.ts +50 -0
- package/dist/ward/seal.js +109 -0
- package/dist/ward/stance.d.ts +19 -0
- package/dist/ward/stance.js +259 -0
- package/dist/ward/ward.d.ts +2 -0
- package/dist/ward/ward.js +197 -0
- package/package.json +21 -29
- package/src/being/index.ts +1 -1
- package/src/being/silence.ts +26 -8
- package/src/being/types.ts +13 -3
- package/src/conformance/beings.ts +5 -5
- package/src/conformance/estate.ts +11 -6
- package/src/conformance/index.ts +14 -12
- package/src/harbor/core.ts +56 -6
- package/src/harbor/dial.ts +1 -1
- package/src/harbor/memory.ts +6 -1
- package/src/ward/door.ts +54 -32
- package/src/ward/ground.ts +6 -2
- package/src/ward/heirs.ts +20 -5
- package/src/ward/owner.ts +4 -3
- package/src/ward/partition.ts +10 -0
- package/src/ward/seal.ts +8 -5
- package/src/ward/stance.ts +20 -14
- package/src/ward/ward.ts +9 -9
- package/vectors/framing.json +7 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Ward } from './ward.ts';
|
|
2
|
+
export type { Ground, WardPointers } from './ground.ts';
|
|
3
|
+
export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
|
|
4
|
+
export type { AskPayload, ReplyPayload } from './seal.ts';
|
|
5
|
+
export * as seal from './seal.ts';
|
|
6
|
+
export * as arithmetic from './arithmetic.ts';
|
|
7
|
+
export { allow, spent, DEFAULT, CEILING } from './allowance.ts';
|
|
8
|
+
export type { Allowance } from './allowance.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// @quo-systems/quo/ward — one function. Every ward is the same ward.
|
|
3
|
+
export { Ward } from './ward.js';
|
|
4
|
+
// The seal and the arithmetic, for a kit in another language to check its
|
|
5
|
+
// bytes against, and for tests that speak to a door directly.
|
|
6
|
+
export * as seal from './seal.js';
|
|
7
|
+
export * as arithmetic from './arithmetic.js';
|
|
8
|
+
// The allowance every ask carries, for a kit in another language and for
|
|
9
|
+
// tests that speak to a door directly.
|
|
10
|
+
export { allow, spent, DEFAULT, CEILING } from './allowance.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
|
|
2
|
+
export type OwnerSide = {
|
|
3
|
+
pk: string;
|
|
4
|
+
doors: Map<string, {
|
|
5
|
+
key: string;
|
|
6
|
+
being: {
|
|
7
|
+
answer: (...a: never[]) => unknown;
|
|
8
|
+
};
|
|
9
|
+
cells: {
|
|
10
|
+
class?: string;
|
|
11
|
+
occupants: Record<string, unknown>;
|
|
12
|
+
standings: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
stance: {
|
|
15
|
+
occupants: {
|
|
16
|
+
invite(id: string): Promise<Invitation | null>;
|
|
17
|
+
remove(id: string): void;
|
|
18
|
+
};
|
|
19
|
+
standings: {
|
|
20
|
+
knock(inv: Invitation, m?: string, a?: JsonObject, w?: Wanted): Promise<unknown>;
|
|
21
|
+
take(id: string, inv: Invitation): Promise<string | null>;
|
|
22
|
+
remove(id: string): void;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
publicKey(): string | null;
|
|
27
|
+
instantiate(key: string, className: string, isPublic?: boolean): {
|
|
28
|
+
key: string;
|
|
29
|
+
} | null;
|
|
30
|
+
};
|
|
31
|
+
export declare const OWNER_ASKS: readonly ['boot', 'invite', 'knock', 'remove'];
|
|
32
|
+
export declare function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The owner's asks. The root owner holds the ask pointer; every other owner
|
|
3
|
+
// is an occupant of the ward itself, invited by the root. Five asks:
|
|
4
|
+
// describe, boot, invite, knock, remove. The knock is a being's knock made
|
|
5
|
+
// for her, and the standing is written into her cells under the id the
|
|
6
|
+
// owner gave; remove takes a relation out of her by id, the mirror of it.
|
|
7
|
+
import { isSilence, isWord, wordOf } from '../being/silence.js';
|
|
8
|
+
import { digest } from '../being/digest.js';
|
|
9
|
+
import { OWNER } from '../being/types.js';
|
|
10
|
+
export const OWNER_ASKS = ['boot', 'invite', 'knock', 'remove'];
|
|
11
|
+
export async function ownerAnswer(w, asker, method, args) {
|
|
12
|
+
// Every name the owner gives is a word. Coerced instead of checked, an
|
|
13
|
+
// object passed where a name belongs becomes the string "[object Object]",
|
|
14
|
+
// and two owners who each got the shape wrong would be handed one being.
|
|
15
|
+
const word = (v) => (typeof v === 'string' ? v : null);
|
|
16
|
+
if (method === undefined)
|
|
17
|
+
return describe(w);
|
|
18
|
+
if (method === 'boot') {
|
|
19
|
+
// A ward may have one public being, and no more. Marking a second would
|
|
20
|
+
// leave the first holding every relation she had, reachable by nobody at
|
|
21
|
+
// the bare pk, and told by nobody that she had been replaced.
|
|
22
|
+
if (args.public === true && w.publicKey() !== null)
|
|
23
|
+
return { error: 'a ward has one public being' };
|
|
24
|
+
const key = word(args.key), className = word(args.class);
|
|
25
|
+
if (key === null || className === null)
|
|
26
|
+
return { error: 'no such class, or key taken' };
|
|
27
|
+
const door = w.instantiate(key, className, args.public === true);
|
|
28
|
+
return door ? { booted: door.key } : { error: 'no such class, or key taken' };
|
|
29
|
+
}
|
|
30
|
+
if (method === 'invite') {
|
|
31
|
+
// On the ward itself, only the root may invite: an occupant of the ward
|
|
32
|
+
// is an owner, and ownership is minted by the ask pointer alone. An owner
|
|
33
|
+
// at the door asking to invite on the ward is refused, so that a carried
|
|
34
|
+
// key can pilot but never hand piloting on.
|
|
35
|
+
const being = word(args.being), id = word(args.id);
|
|
36
|
+
if (being === null || id === null)
|
|
37
|
+
return { error: 'no such being' };
|
|
38
|
+
if (being === w.pk && asker.id !== OWNER)
|
|
39
|
+
return { error: 'no such being' };
|
|
40
|
+
const door = w.doors.get(being);
|
|
41
|
+
return door ? await door.stance.occupants.invite(id) : { error: 'no such being' };
|
|
42
|
+
}
|
|
43
|
+
if (method === 'knock') {
|
|
44
|
+
const b = args.being;
|
|
45
|
+
let door = typeof b === 'string' ? w.doors.get(b) : undefined;
|
|
46
|
+
if (!door && b && typeof b === 'object') {
|
|
47
|
+
// new or existing: the owner names a being of theirs, and a key already
|
|
48
|
+
// booted is a being of theirs, not a class that failed to instantiate.
|
|
49
|
+
const key = word(b.key), className = word(b.boot);
|
|
50
|
+
if (key === null || className === null)
|
|
51
|
+
return { error: 'no such being' };
|
|
52
|
+
const made = w.instantiate(key, className);
|
|
53
|
+
door = made ? w.doors.get(made.key) : w.doors.get(key);
|
|
54
|
+
}
|
|
55
|
+
const id = word(args.id);
|
|
56
|
+
// Never for the ward itself, for the reason invite refuses it: the ward
|
|
57
|
+
// is a being to nobody outside, and holds no relation of its own. A
|
|
58
|
+
// standing written into its cells would be the owner's reach dressed as
|
|
59
|
+
// a being's, and no ask of the ward's could ever use it.
|
|
60
|
+
if (!door || id === null || door.key === w.pk)
|
|
61
|
+
return { error: 'no such being' };
|
|
62
|
+
const inv = args.invitation;
|
|
63
|
+
// The owner is a caller like any other and may say what this knock may
|
|
64
|
+
// spend. Saying nothing is the ward's default, as it is for a being.
|
|
65
|
+
const out = await door.stance.standings.knock(inv, args.method, args.args ?? {}, args.wanted);
|
|
66
|
+
if (isSilence(out))
|
|
67
|
+
return { error: 'silence' };
|
|
68
|
+
if (isWord(out))
|
|
69
|
+
return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
|
|
70
|
+
return { taken: await door.stance.standings.take(id, inv), answer: out };
|
|
71
|
+
}
|
|
72
|
+
if (method === 'remove') {
|
|
73
|
+
// The mirror of knock: a relation out of a being, by id, occupant or
|
|
74
|
+
// standing, since the two share one namespace. On the ward itself the
|
|
75
|
+
// id is an owner, and only the root may unseat one, for the reason only
|
|
76
|
+
// the root may invite one: ownership moves by the root alone.
|
|
77
|
+
const being = word(args.being), id = word(args.id);
|
|
78
|
+
if (being === null || id === null)
|
|
79
|
+
return { error: 'no such being' };
|
|
80
|
+
if (being === w.pk && asker.id !== OWNER)
|
|
81
|
+
return { error: 'no such being' };
|
|
82
|
+
const door = w.doors.get(being);
|
|
83
|
+
if (!door)
|
|
84
|
+
return { error: 'no such being' };
|
|
85
|
+
if (Object.hasOwn(door.cells.occupants, id))
|
|
86
|
+
door.stance.occupants.remove(id);
|
|
87
|
+
else if (Object.hasOwn(door.cells.standings, id))
|
|
88
|
+
door.stance.standings.remove(id);
|
|
89
|
+
else
|
|
90
|
+
return { error: 'nothing to remove' };
|
|
91
|
+
return { removed: id };
|
|
92
|
+
}
|
|
93
|
+
return { error: 'unknown ask' };
|
|
94
|
+
}
|
|
95
|
+
// The ward's describe for its owner: its beings, their classes, a digest each,
|
|
96
|
+
// as she would describe herself to the owner.
|
|
97
|
+
async function describe(w) {
|
|
98
|
+
const beings = {};
|
|
99
|
+
for (const [key, door] of w.doors) {
|
|
100
|
+
if (key === w.pk)
|
|
101
|
+
continue;
|
|
102
|
+
let d = null;
|
|
103
|
+
try {
|
|
104
|
+
const bp = await door.being.answer({ id: OWNER });
|
|
105
|
+
d = isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// she threw where she was asked. there is no blueprint, and d stays null.
|
|
109
|
+
}
|
|
110
|
+
beings[key] = { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
asks: OWNER_ASKS.map((name) => ({ name, input: { type: 'object' } })),
|
|
114
|
+
notes: { pk: w.pk, beings },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Cells } from '../being/types.ts';
|
|
2
|
+
export declare const VERSION = "pre-1.0.0";
|
|
3
|
+
export declare const READS: readonly string[];
|
|
4
|
+
export type Heir = {
|
|
5
|
+
being: string;
|
|
6
|
+
id: string;
|
|
7
|
+
current: string;
|
|
8
|
+
announced: string | null;
|
|
9
|
+
fresh: boolean;
|
|
10
|
+
mark: number;
|
|
11
|
+
spent: number[];
|
|
12
|
+
last?: {
|
|
13
|
+
seq: number;
|
|
14
|
+
reply: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type StandingKeys = {
|
|
18
|
+
ward: string;
|
|
19
|
+
heir: string | null;
|
|
20
|
+
current: string;
|
|
21
|
+
next: string | null;
|
|
22
|
+
seq: number;
|
|
23
|
+
};
|
|
24
|
+
export type Bind = {
|
|
25
|
+
standings: Record<string, StandingKeys>;
|
|
26
|
+
occupants: Record<string, string>;
|
|
27
|
+
knocks: Record<string, {
|
|
28
|
+
current: string;
|
|
29
|
+
next: string | null;
|
|
30
|
+
spoke: boolean;
|
|
31
|
+
sent: boolean;
|
|
32
|
+
seq: number;
|
|
33
|
+
}>;
|
|
34
|
+
answered: Record<string, true>;
|
|
35
|
+
minted: string[];
|
|
36
|
+
};
|
|
37
|
+
export type Gone = {
|
|
38
|
+
current: string;
|
|
39
|
+
announced: string | null;
|
|
40
|
+
};
|
|
41
|
+
export declare const GONE = 256;
|
|
42
|
+
export type Partition = {
|
|
43
|
+
version: string;
|
|
44
|
+
beings: Record<string, Cells & {
|
|
45
|
+
class?: string;
|
|
46
|
+
}>;
|
|
47
|
+
bind: Record<string, Bind>;
|
|
48
|
+
heirs: Record<string, Heir>;
|
|
49
|
+
gone: Record<string, Gone>;
|
|
50
|
+
public: string | null;
|
|
51
|
+
};
|
|
52
|
+
export declare function open(memory: Record<string, unknown>): Partition;
|
|
53
|
+
export declare const at: <T>(rec: Record<string, T>, key: string) => T | undefined;
|
|
54
|
+
export declare const put: <T>(rec: Record<string, T>, key: string, value: T) => T;
|
|
55
|
+
export declare const drop: (rec: Record<string, unknown>, key: string) => void;
|
|
56
|
+
export declare const emptyCells: () => Cells;
|
|
57
|
+
export declare const emptyBind: () => Bind;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The shape of everything below. A ward stamps what it writes and refuses
|
|
2
|
+
// what it cannot read: the partition outlives the ward that wrote it, and a
|
|
3
|
+
// ward that guessed at a shape it did not know would not fail, it would answer
|
|
4
|
+
// wrongly on relations it had silently misread.
|
|
5
|
+
//
|
|
6
|
+
// One value, and it does not move until 1.0.0. Before a release there is
|
|
7
|
+
// nobody holding a partition of another shape to tell apart: if the types
|
|
8
|
+
// below change, the partition is thrown away, not migrated to and not counted.
|
|
9
|
+
// A number that climbed with every change would be a changelog, and the only
|
|
10
|
+
// question asked here is answered by equality — is this a partition this ward
|
|
11
|
+
// can read. At 1.0.0 this changes once, and from then on it means something.
|
|
12
|
+
export const VERSION = 'pre-1.0.0';
|
|
13
|
+
// Every version this ward can read, newest last. Today it is one, and the
|
|
14
|
+
// seam below is what makes it able to be more than one later: a partition
|
|
15
|
+
// written under an older readable version is carried forward by `upgrade`
|
|
16
|
+
// before the ward touches it. The seam is here before 1.0.0 because after
|
|
17
|
+
// 1.0.0 there is nowhere to put it. A partition holds every secret and every
|
|
18
|
+
// relation a world has, so the first shape change after the freeze cannot be
|
|
19
|
+
// answered by throwing it away, and a ward that meets a shape it has no seam
|
|
20
|
+
// for refuses to boot with everything still inside it.
|
|
21
|
+
export const READS = [VERSION];
|
|
22
|
+
// Carry a partition written under an older readable version forward to this
|
|
23
|
+
// one. There is nothing to carry today, because READS has one member. Each
|
|
24
|
+
// step added here is from one member of READS to the next.
|
|
25
|
+
function upgrade(p) {
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
28
|
+
export const GONE = 256;
|
|
29
|
+
// The one place a ward reads state that another ward wrote. A fresh
|
|
30
|
+
// memory becomes a partition of this version; anything already written must
|
|
31
|
+
// say it is this version, and a ward that cannot read it does not boot. Birth is
|
|
32
|
+
// where a ward is allowed to be loud: silence is the door's word, for asks
|
|
33
|
+
// that were made, and no ask has been made yet.
|
|
34
|
+
export function open(memory) {
|
|
35
|
+
const p = memory;
|
|
36
|
+
if (Object.keys(memory).length > 0 && p.version !== VERSION) {
|
|
37
|
+
if (!READS.includes(p.version))
|
|
38
|
+
throw new Error(`partition is version ${p.version}, this ward reads ${READS.join(', ')}`);
|
|
39
|
+
upgrade(p).version = VERSION;
|
|
40
|
+
}
|
|
41
|
+
p.version ??= VERSION;
|
|
42
|
+
p.beings ??= {};
|
|
43
|
+
p.bind ??= {};
|
|
44
|
+
p.heirs ??= {};
|
|
45
|
+
p.gone ??= {};
|
|
46
|
+
p.public ??= null;
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
// One record, reached by a key a being or an owner chose. A bare lookup finds
|
|
50
|
+
// every name on Object's prototype, so `__proto__` answers with the prototype
|
|
51
|
+
// itself: read, it is there where nothing was put; written, it reaches every
|
|
52
|
+
// object alive in the runtime. Own keys only, both ways, and a write that
|
|
53
|
+
// names one of those lands on the record where it belongs.
|
|
54
|
+
export const at = (rec, key) => (Object.hasOwn(rec, key) ? rec[key] : undefined);
|
|
55
|
+
export const put = (rec, key, value) => {
|
|
56
|
+
Object.defineProperty(rec, key, { value, writable: true, enumerable: true, configurable: true });
|
|
57
|
+
return value;
|
|
58
|
+
};
|
|
59
|
+
export const drop = (rec, key) => {
|
|
60
|
+
if (Object.hasOwn(rec, key))
|
|
61
|
+
delete rec[key];
|
|
62
|
+
};
|
|
63
|
+
export const emptyCells = () => ({ standings: {}, occupants: {} });
|
|
64
|
+
export const emptyBind = () => ({ standings: {}, occupants: {}, knocks: {}, answered: {}, minted: [] });
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { DoorWord, Json, JsonObject } from '../being/types.ts';
|
|
2
|
+
export type WardKey = {
|
|
3
|
+
sign: Uint8Array;
|
|
4
|
+
padlock: Uint8Array;
|
|
5
|
+
signPk: Uint8Array;
|
|
6
|
+
padlockPk: Uint8Array;
|
|
7
|
+
pk: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function wardKey(seed: string | Uint8Array): Promise<WardKey>;
|
|
10
|
+
export declare const wardSignPk: (pk: string) => Uint8Array;
|
|
11
|
+
export declare const wardPadlock: (pk: string) => Uint8Array;
|
|
12
|
+
export declare const isWardPk: (pk: unknown) => pk is string;
|
|
13
|
+
export declare function beingKey(seed: Uint8Array): Promise<{
|
|
14
|
+
seed: string;
|
|
15
|
+
pk: string;
|
|
16
|
+
}>;
|
|
17
|
+
export type AskPayload = {
|
|
18
|
+
by: string;
|
|
19
|
+
next: string | null;
|
|
20
|
+
seq: number;
|
|
21
|
+
time: number;
|
|
22
|
+
hops?: number;
|
|
23
|
+
method?: string;
|
|
24
|
+
args: JsonObject;
|
|
25
|
+
};
|
|
26
|
+
export type ReplyPayload = {
|
|
27
|
+
object: Json;
|
|
28
|
+
seen: string | null;
|
|
29
|
+
} | {
|
|
30
|
+
silence: true;
|
|
31
|
+
} | {
|
|
32
|
+
quo: DoorWord;
|
|
33
|
+
};
|
|
34
|
+
export declare function sealAsk(to: string | null, payload: AskPayload, signer: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{
|
|
35
|
+
bytes: Uint8Array;
|
|
36
|
+
ephemeral: Uint8Array;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function openAsk(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<{
|
|
39
|
+
to: string | null;
|
|
40
|
+
payload: AskPayload;
|
|
41
|
+
body: Uint8Array;
|
|
42
|
+
signature: Uint8Array;
|
|
43
|
+
ephemeralPk: Uint8Array;
|
|
44
|
+
} | null>;
|
|
45
|
+
export declare const verifyAsk: (a: {
|
|
46
|
+
body: Uint8Array;
|
|
47
|
+
signature: Uint8Array;
|
|
48
|
+
}, pk: string) => Promise<boolean>;
|
|
49
|
+
export declare function sealReply(reply: ReplyPayload, ephemeralPk: Uint8Array, wardSign: Uint8Array, seed: Uint8Array): Promise<Uint8Array>;
|
|
50
|
+
export declare function openReply(bytes: unknown, ephemeralSecret: Uint8Array, signPk: Uint8Array): Promise<ReplyPayload | null>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { isDoorWord } from '../being/silence.js';
|
|
2
|
+
import { KEY, SIGNATURE, box, concat, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.js';
|
|
3
|
+
const utf8 = new TextEncoder();
|
|
4
|
+
const text = new TextDecoder();
|
|
5
|
+
// The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
|
|
6
|
+
export async function wardKey(seed) {
|
|
7
|
+
const raw = typeof seed === 'string' ? utf8.encode(seed) : seed;
|
|
8
|
+
const seed32 = raw.length === KEY ? raw : await sha256(raw);
|
|
9
|
+
const s = await signingPair(seed32);
|
|
10
|
+
const p = await sealingPair(seed32);
|
|
11
|
+
return { sign: s.secret, padlock: p.secret, signPk: s.pk, padlockPk: p.pk, pk: hex(s.pk) + hex(p.pk) };
|
|
12
|
+
}
|
|
13
|
+
export const wardSignPk = (pk) => unhex(pk.slice(0, KEY * 2));
|
|
14
|
+
export const wardPadlock = (pk) => unhex(pk.slice(KEY * 2));
|
|
15
|
+
export const isWardPk = (pk) => typeof pk === 'string' && /^[0-9a-f]{128}$/.test(pk);
|
|
16
|
+
// A being's relation key: a seed, and the pk it signs as.
|
|
17
|
+
export async function beingKey(seed) {
|
|
18
|
+
return { seed: hex(seed), pk: hex((await signingPair(seed)).pk) };
|
|
19
|
+
}
|
|
20
|
+
// Seal an ask. Returns the bytes and the ephemeral secret the reply will be sealed to.
|
|
21
|
+
export async function sealAsk(to, payload, signer, padlock, seed) {
|
|
22
|
+
const body = utf8.encode(JSON.stringify({ to, ...payload }));
|
|
23
|
+
const inside = concat([body, await sign(body, signer)]);
|
|
24
|
+
const { bytes, ephemeral } = await box(inside, padlock, seed);
|
|
25
|
+
return { bytes, ephemeral: ephemeral.secret };
|
|
26
|
+
}
|
|
27
|
+
// Open an ask at the door. Null when it does not open or does not parse.
|
|
28
|
+
// Verification is the door's own step, because only the door knows which key may speak.
|
|
29
|
+
export async function openAsk(bytes, padlockSecret) {
|
|
30
|
+
try {
|
|
31
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < 1)
|
|
32
|
+
return null;
|
|
33
|
+
const inside = await unbox(bytes, padlockSecret);
|
|
34
|
+
if (inside.length <= SIGNATURE)
|
|
35
|
+
return null;
|
|
36
|
+
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
37
|
+
const signature = inside.subarray(inside.length - SIGNATURE);
|
|
38
|
+
const payload = JSON.parse(text.decode(body));
|
|
39
|
+
// The heir, or null for the public being. Absent is not null: a payload
|
|
40
|
+
// that never named one is malformed, like any other missing field.
|
|
41
|
+
const to = payload.to;
|
|
42
|
+
if (to !== null && !(typeof to === 'string' && /^[0-9a-f]{64}$/.test(to)))
|
|
43
|
+
return null;
|
|
44
|
+
if (typeof payload.by !== 'string' || !/^[0-9a-f]{64}$/.test(payload.by))
|
|
45
|
+
return null;
|
|
46
|
+
if (payload.next !== null && !(typeof payload.next === 'string' && /^[0-9a-f]{64}$/.test(payload.next)))
|
|
47
|
+
return null;
|
|
48
|
+
if (payload.method !== undefined && typeof payload.method !== 'string')
|
|
49
|
+
return null; // a name, or the empty ask. never a number, never an object.
|
|
50
|
+
if (payload.args !== undefined && (payload.args === null || typeof payload.args !== 'object' || Array.isArray(payload.args)))
|
|
51
|
+
return null; // args are one object, or absent. a string or a list is not an ask.
|
|
52
|
+
if (!Number.isSafeInteger(payload.seq) || payload.seq < 1)
|
|
53
|
+
return null; // her count for this relation. one and up, and a whole number.
|
|
54
|
+
// The allowance. A whole number, and one that has already run out is not an
|
|
55
|
+
// ask this door will open: it is refused as the bytes it is, above.
|
|
56
|
+
if (!Number.isSafeInteger(payload.time) || payload.time <= 0)
|
|
57
|
+
return null;
|
|
58
|
+
// The hop count, if this ask carries one: a whole number, never below
|
|
59
|
+
// zero. Absent is the ordinary ask and always will be.
|
|
60
|
+
if (payload.hops !== undefined && (!Number.isSafeInteger(payload.hops) || payload.hops < 0))
|
|
61
|
+
return null;
|
|
62
|
+
return { to, payload, body, signature, ephemeralPk: bytes.subarray(0, KEY) };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export const verifyAsk = (a, pk) => verify(a.body, a.signature, unhex(pk));
|
|
69
|
+
// Seal a reply to the ephemeral pk of the ask it answers, signed by the ward.
|
|
70
|
+
export async function sealReply(reply, ephemeralPk, wardSign, seed) {
|
|
71
|
+
const body = utf8.encode(JSON.stringify(reply));
|
|
72
|
+
return (await box(concat([body, await sign(body, wardSign)]), ephemeralPk, seed)).bytes;
|
|
73
|
+
}
|
|
74
|
+
// Open a reply with the ephemeral secret kept from the ask, and verify it
|
|
75
|
+
// came from the ward it was sent to. Null is silence: it did not open, it
|
|
76
|
+
// lied, or it is not a reply. Every ward on the wire is somebody else's code,
|
|
77
|
+
// so the shape is checked here and nowhere later: a reply is `{ silence }`,
|
|
78
|
+
// `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
|
|
79
|
+
// object present and seen a digest or null.
|
|
80
|
+
// Anything else is bytes that said nothing, and her ward never hands her a
|
|
81
|
+
// throw or an absent value for what a stranger wrote.
|
|
82
|
+
export async function openReply(bytes, ephemeralSecret, signPk) {
|
|
83
|
+
try {
|
|
84
|
+
if (!(bytes instanceof Uint8Array))
|
|
85
|
+
return null;
|
|
86
|
+
const inside = await unbox(bytes, ephemeralSecret);
|
|
87
|
+
if (inside.length <= SIGNATURE)
|
|
88
|
+
return null;
|
|
89
|
+
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
90
|
+
if (!(await verify(body, inside.subarray(inside.length - SIGNATURE), signPk)))
|
|
91
|
+
return null;
|
|
92
|
+
const reply = JSON.parse(text.decode(body));
|
|
93
|
+
if (reply === null || typeof reply !== 'object' || Array.isArray(reply))
|
|
94
|
+
return null;
|
|
95
|
+
const r = reply;
|
|
96
|
+
if (r.silence === true)
|
|
97
|
+
return { silence: true };
|
|
98
|
+
if (Object.hasOwn(r, 'quo'))
|
|
99
|
+
return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
100
|
+
if (!Object.hasOwn(r, 'object') || r.object === undefined)
|
|
101
|
+
return null;
|
|
102
|
+
if (r.seen !== null && !(typeof r.seen === 'string' && /^[0-9a-f]{64}$/.test(r.seen)))
|
|
103
|
+
return null;
|
|
104
|
+
return { object: r.object, seen: r.seen };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Cells, JsonObject, Silence, Stance, Wanted, Word } from '../being/types.ts';
|
|
2
|
+
import { type Bind, type StandingKeys } from './partition.ts';
|
|
3
|
+
import { type ReplyPayload } from './seal.ts';
|
|
4
|
+
export type Inside = {
|
|
5
|
+
pk: string;
|
|
6
|
+
mintKey(bind: Bind): Promise<{
|
|
7
|
+
seed: string;
|
|
8
|
+
pk: string;
|
|
9
|
+
}>;
|
|
10
|
+
openHeir(heir: string, being: string, id: string): void;
|
|
11
|
+
closeHeir(heir: string): void;
|
|
12
|
+
send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
|
|
13
|
+
};
|
|
14
|
+
export type SealedInvitation = {
|
|
15
|
+
ward: string;
|
|
16
|
+
heir?: string;
|
|
17
|
+
secret?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function buildStance(inside: Inside, key: string, cells: Cells, bind: Bind): Stance;
|