@quo-systems/quo 0.1.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.
@@ -0,0 +1,69 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The store: keeping wards by name. A ward is three things and the store
3
+ // keeps all three under one name: the seed, the partition and the ward
4
+ // record, which says where the class bodies come from and which being is
5
+ // the user's. Plus the directory's hints, which are the harbor's and
6
+ // survive a restart. The store reads nothing it keeps; the partition is
7
+ // values the ward wrote, and the seed is bytes only the ward derives from.
8
+ // Every terrain has one: files on a disk, IndexedDB in a tab, an object's
9
+ // storage on the edge; the memory store below is the library's own, for a
10
+ // harbor core with no device under it. `src/conformance/store.ts` is what
11
+ // every one of them passes.
12
+ export type WardRecord = { pk: string; code: string; user: string };
13
+ export type Kept = { seed: Uint8Array; partition: Record<string, unknown>; record: WardRecord };
14
+
15
+ export type Store = {
16
+ list(): Promise<string[]>;
17
+ load(name: string): Promise<Kept | undefined>;
18
+ // Keep all three under the name; a name already kept is refused.
19
+ put(name: string, kept: Kept): Promise<void>;
20
+ // The partition back, after a call; the record back, after a boot learned
21
+ // the pk. A name not kept is nothing, on both.
22
+ save(name: string, partition: Record<string, unknown>): Promise<void>;
23
+ record(name: string, record: WardRecord): Promise<void>;
24
+ // Take a ward out: the first move of a migration. What was kept comes back.
25
+ take(name: string): Promise<Kept | undefined>;
26
+ hints(): Promise<Record<string, string>>;
27
+ hint(pk: string, url: string): Promise<void>;
28
+ };
29
+
30
+ // The partition is values only, and the ward hands it out through a guard,
31
+ // so a store takes a copy of the values the way a file would: through JSON.
32
+ export const values = (p: Record<string, unknown>): Record<string, unknown> => JSON.parse(JSON.stringify(p)) as Record<string, unknown>;
33
+
34
+ // The store as a map, one process, nothing kept past it.
35
+ export class MemoryStore implements Store {
36
+ readonly rows = new Map<string, Kept>();
37
+ readonly reach = new Map<string, string>();
38
+
39
+ async list(): Promise<string[]> {
40
+ return [...this.rows.keys()];
41
+ }
42
+ async load(name: string): Promise<Kept | undefined> {
43
+ const row = this.rows.get(name);
44
+ return row && { seed: new Uint8Array(row.seed), partition: values(row.partition), record: { ...row.record } };
45
+ }
46
+ async put(name: string, kept: Kept): Promise<void> {
47
+ if (this.rows.has(name)) throw new Error(`ward ${name} already exists here`);
48
+ this.rows.set(name, { seed: new Uint8Array(kept.seed), partition: values(kept.partition), record: { ...kept.record } });
49
+ }
50
+ async save(name: string, partition: Record<string, unknown>): Promise<void> {
51
+ const row = this.rows.get(name);
52
+ if (row) row.partition = values(partition);
53
+ }
54
+ async record(name: string, record: WardRecord): Promise<void> {
55
+ const row = this.rows.get(name);
56
+ if (row) row.record = { ...record };
57
+ }
58
+ async take(name: string): Promise<Kept | undefined> {
59
+ const kept = await this.load(name);
60
+ this.rows.delete(name);
61
+ return kept;
62
+ }
63
+ async hints(): Promise<Record<string, string>> {
64
+ return Object.fromEntries(this.reach);
65
+ }
66
+ async hint(pk: string, url: string): Promise<void> {
67
+ this.reach.set(pk, url);
68
+ }
69
+ }
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The allowance. Every ask carries, in its signed body, the time it may still
3
+ // spend, in milliseconds. A being who says nothing gets her ward's default and
4
+ // never thinks about it; one who wants to say so passes a third argument, and
5
+ // is held to the ward's ceiling. The door reads it before anything is done
6
+ // under it and refuses a budget already gone, as one silence like every other
7
+ // refusal there. The sender bounds the whole of her ask to the same number,
8
+ // and a wait that ran out is silence, 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
+
15
+ // What a being may ask for is Wanted, and it belongs to the stance, where she
16
+ // reads it. This file holds only what the ward does with it.
17
+ import type { Wanted } from '../being/types.ts';
18
+
19
+ // A budget, as it crosses a door. Time in milliseconds.
20
+ export type Allowance = { time: number };
21
+
22
+ // How wide the default is, and how wide she may ask for, are the ward's own —
23
+ // wider is more patient, and no peer can tell the difference except by being
24
+ // refused. A being who never learns the third parameter exists writes correct
25
+ // code forever under these. They are two numbers because the third argument
26
+ // goes both ways: less for the ask she wants back quickly, more for the one
27
+ // she knows is slow.
28
+ export const DEFAULT: Allowance = { time: 30_000 };
29
+ export const CEILING: Allowance = { time: 300_000 };
30
+
31
+ // What she asked for, held to what her ward allows. A being cannot mint budget
32
+ // nobody granted her: asking for a minute where the ward gives thirty seconds
33
+ // is thirty seconds, silently, because the ceiling is not hers to know.
34
+ // Asking for nothing at all is the default, which is the whole point.
35
+ export function allow(wanted: Wanted | undefined, ceiling: Allowance = CEILING, base: Allowance = DEFAULT): Allowance {
36
+ const whole = (n: unknown, fallback: number) => (typeof n === 'number' && Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback);
37
+ return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
38
+ }
39
+
40
+ // Whether a budget has anything left to spend. A door reads this on arrival.
41
+ export const spent = (a: Allowance): boolean => !(a.time > 0);
42
+
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.
45
+ export const LATE = Symbol('late');
46
+
47
+ // A wait with an end. This is the one thing the ward times, and it covers the
48
+ // whole of an ask, not only the wire: a relation that comes back round is
49
+ // stopped at its own lane, before a single byte is sealed, and a bound that
50
+ // only watched the wire would never see it. A timer is the only way to stop
51
+ // waiting and every language that runs Quo has one; no runtime is named here.
52
+ //
53
+ // `rang` is called the moment the bell rings, before anyone waiting on the
54
+ // wait is told: whoever holds work that has not started yet reads it and does
55
+ // not start it. What comes back late is not read, and what has not left yet
56
+ // does not leave.
57
+ export async function within<T>(ms: number, work: Promise<T>, rang?: () => void): Promise<T | typeof LATE> {
58
+ let timer: ReturnType<typeof setTimeout> | undefined;
59
+ const bell = new Promise<typeof LATE>((ring) => {
60
+ timer = setTimeout(() => {
61
+ rang?.();
62
+ ring(LATE);
63
+ }, ms);
64
+ });
65
+ try {
66
+ return await Promise.race([work, bell]);
67
+ } finally {
68
+ clearTimeout(timer);
69
+ void work.then(
70
+ () => {},
71
+ () => {},
72
+ ); // whatever it was, it is nobody's answer now
73
+ }
74
+ }
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Four algorithms, named once and never negotiated: Ed25519 signs, X25519
3
+ // agrees, SHA-256 hashes, AES-256-GCM encrypts with the key derived through
4
+ // HKDF-SHA-256 under a fixed label. All four live in `crypto.subtle`, so this
5
+ // takes no package and runs the same in a browser tab as on a server -- on
6
+ // any terrain that carries them. SHA-256, AES-GCM and HKDF are everywhere;
7
+ // the two curves are recent, and a terrain without them is a terrain no ward
8
+ // runs on. `test/floor.test.ts` names the floor and probes for it. Subtle is
9
+ // asynchronous, so everything here is.
10
+ //
11
+ // Ported from an earlier kit's arithmetic. Same bytes, same vectors.
12
+ // `crypto.subtle` is read at every use and never captured at load. A browser
13
+ // on a plain http:// origin has `crypto` without `subtle`, and a terrain may
14
+ // install one after this module is first imported; a reference taken here
15
+ // would turn either into an unreadable failure deep inside a key import.
16
+ const subtle = (): SubtleCrypto => {
17
+ const s = globalThis.crypto?.subtle;
18
+ if (!s) throw new Error('this terrain has no crypto.subtle: a ward needs a secure context');
19
+ return s;
20
+ };
21
+
22
+ export const KEY = 32;
23
+ export const SIGNATURE = 64;
24
+ export const NONCE = 12;
25
+ export const TAG = 16;
26
+ const SEAL_INFO = new TextEncoder().encode('quo-seal');
27
+ const SEAL_SALT = new Uint8Array(0);
28
+
29
+ // A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
30
+ const ED_SECRET = unhex('302e020100300506032b657004220420');
31
+ const X_SECRET = unhex('302e020100300506032b656e04220420');
32
+ const ED = { name: 'Ed25519' };
33
+ const X = { name: 'X25519' };
34
+
35
+ const HEX = Array.from({ length: 256 }, (_, at) => at.toString(16).padStart(2, '0'));
36
+ export function hex(bytes: Uint8Array | ArrayBuffer): string {
37
+ let out = '';
38
+ for (const byte of new Uint8Array(bytes)) out += HEX[byte];
39
+ return out;
40
+ }
41
+ export function unhex(text: string): Uint8Array {
42
+ const out = new Uint8Array(text.length / 2);
43
+ for (let at = 0; at < out.length; at += 1) out[at] = parseInt(text.slice(at * 2, at * 2 + 2), 16);
44
+ return out;
45
+ }
46
+ export function concat(parts: Uint8Array[]): Uint8Array {
47
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
48
+ let at = 0;
49
+ for (const part of parts) {
50
+ out.set(part, at);
51
+ at += part.length;
52
+ }
53
+ return out;
54
+ }
55
+ export function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
56
+ if (a.length !== b.length) return false;
57
+ let diff = 0;
58
+ for (let at = 0; at < a.length; at += 1) diff |= a[at] ^ b[at];
59
+ return diff === 0;
60
+ }
61
+
62
+ // The eight small-order points. A public key among them verifies nothing.
63
+ const SMALL_ORDER = [
64
+ '0100000000000000000000000000000000000000000000000000000000000000',
65
+ 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
66
+ '0000000000000000000000000000000000000000000000000000000000000000',
67
+ '0000000000000000000000000000000000000000000000000000000000000080',
68
+ '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05',
69
+ 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
70
+ '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
71
+ 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
72
+ ].map(unhex);
73
+ export const smallOrder = (pk: Uint8Array): boolean => SMALL_ORDER.some((p) => sameBytes(p, pk));
74
+
75
+ const key32 = (value: Uint8Array, what: string): Uint8Array => {
76
+ if (!(value instanceof Uint8Array) || value.length !== KEY) throw new Error(`${what} is not a 32-byte key`);
77
+ return value;
78
+ };
79
+ const pkcs8 = (prefix: Uint8Array, value: Uint8Array, what: string) => concat([prefix, key32(value, what)]);
80
+ const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) =>
81
+ subtle().importKey('pkcs8', pkcs8(prefix, value, what) as BufferSource, alg, true, uses);
82
+ const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) =>
83
+ subtle().importKey('raw', key32(value, what) as BufferSource, alg, true, uses);
84
+
85
+ // Subtle exports the public half of a private key only through a JWK, where `x` is the 32 raw bytes in base64url.
86
+ async function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
87
+ const jwk = await subtle().exportKey('jwk', secret);
88
+ const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
89
+ const out = new Uint8Array(binary.length);
90
+ for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
91
+ return out;
92
+ }
93
+
94
+ export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
95
+ return new Uint8Array(await subtle().digest('SHA-256', concat(parts) as BufferSource));
96
+ }
97
+
98
+ export type Pair = { secret: Uint8Array; pk: Uint8Array };
99
+ export async function signingPair(seed: Uint8Array): Promise<Pair> {
100
+ const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
101
+ return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
102
+ }
103
+ export async function sealingPair(seed: Uint8Array): Promise<Pair> {
104
+ const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
105
+ return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
106
+ }
107
+
108
+ export async function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array> {
109
+ const key = await secretKey(ED, ED_SECRET, secret, 'secret', ['sign']);
110
+ return new Uint8Array(await subtle().sign(ED, key, message as BufferSource));
111
+ }
112
+ export async function verify(message: Uint8Array, signature: Uint8Array, pk: Uint8Array): Promise<boolean> {
113
+ if (!(signature instanceof Uint8Array) || signature.length !== SIGNATURE) return false;
114
+ if (!(pk instanceof Uint8Array) || pk.length !== KEY || smallOrder(pk)) return false;
115
+ try {
116
+ return await subtle().verify(ED, await publicKey(ED, pk, 'pk', ['verify']), signature as BufferSource, message as BufferSource);
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ export async function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uint8Array> {
123
+ const key = await secretKey(X, X_SECRET, secret, 'secret', ['deriveBits']);
124
+ const peer = await publicKey(X, peerPk, 'padlock', []);
125
+ const shared = new Uint8Array(await subtle().deriveBits({ name: X.name, public: peer }, key, KEY * 8));
126
+ if (shared.every((b) => b === 0)) throw new Error('dead agreement'); // a padlock that was not a real key
127
+ return shared;
128
+ }
129
+
130
+ // One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
131
+ // no randomness of its own: the key it pairs with is fresh on every message.
132
+ async function cipherKey(shared: Uint8Array, use: KeyUsage) {
133
+ const material = await subtle().importKey('raw', shared as BufferSource, 'HKDF', false, ['deriveBits']);
134
+ const out = new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SEAL_SALT, info: SEAL_INFO }, material, (KEY + NONCE) * 8));
135
+ return { key: await subtle().importKey('raw', out.subarray(0, KEY) as BufferSource, 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
136
+ }
137
+ // The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
138
+ export async function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
139
+ const { key, nonce } = await cipherKey(shared, 'encrypt');
140
+ return new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, plaintext as BufferSource));
141
+ }
142
+ export async function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
143
+ if (ciphertext.length < TAG) throw new Error('short input');
144
+ const { key, nonce } = await cipherKey(shared, 'decrypt');
145
+ return new Uint8Array(await subtle().decrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, ciphertext as BufferSource));
146
+ }
147
+
148
+ // A box: an ephemeral X25519 pk outside, one ciphertext sealed to the
149
+ // padlock. The answer to a box is a box sealed to that ephemeral pk, so the
150
+ // sender keeps the ephemeral secret until the answer comes.
151
+ export async function box(inside: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{ bytes: Uint8Array; ephemeral: Pair }> {
152
+ const ephemeral = await sealingPair(seed);
153
+ const shared = await agree(ephemeral.secret, padlock);
154
+ return { bytes: concat([ephemeral.pk, await encrypt(shared, inside, ephemeral.pk)]), ephemeral };
155
+ }
156
+ export async function unbox(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<Uint8Array> {
157
+ if (bytes.length <= KEY) throw new Error('short input');
158
+ const ephemeralPk = bytes.subarray(0, KEY);
159
+ const shared = await agree(padlockSecret, ephemeralPk);
160
+ return decrypt(shared, bytes.subarray(KEY), ephemeralPk);
161
+ }
@@ -0,0 +1,90 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Cells hold values, and nothing else. The partition promises a harbor it may
3
+ // persist a being's cells as it likes: as this process's objects, as a row, as
4
+ // a line of JSON on a disk. That promise is only worth what the cells honour.
5
+ //
6
+ // Nothing checked it. A being could write a Map, a Date, an undefined, a NaN.
7
+ // In a memory harbor the object survives, because there is nowhere for it to
8
+ // go and come back from. Through any harbor that keeps values it silently
9
+ // becomes {}, or a string, or absent, or null — and she is never told which
10
+ // harbor she is standing in. That is the memory harbor lying to her, which is
11
+ // the one thing it must never do.
12
+ //
13
+ // So the cells refuse the write. Loud, where she wrote it, in her own frame:
14
+ // a being holds three answers and a throw is not one of them, so this throw
15
+ // lands inside her method and the door turns it into the silence it already
16
+ // turns every throw into. She has not answered, and nothing was written down
17
+ // that a harbor would have to lie about later.
18
+ import type { Cells, Json } from '../being/types.ts';
19
+
20
+ // I-JSON, all the way down. A number that JSON cannot write is not a number
21
+ // a harbor can keep, and a key on the prototype is not a key she wrote.
22
+ function fault(v: unknown, path: string, seen: Set<object>): string | null {
23
+ if (v === null || typeof v === 'boolean' || typeof v === 'string') return null;
24
+ if (typeof v === 'number') return Number.isFinite(v) ? null : `${path} is ${String(v)}, which no harbor can write down`;
25
+ if (typeof v !== 'object') return `${path} is a ${typeof v}, which is not a value`;
26
+ if (seen.has(v)) return `${path} refers back to itself`;
27
+ seen.add(v);
28
+ try {
29
+ if (Array.isArray(v)) {
30
+ for (let i = 0; i < v.length; i += 1) {
31
+ const f = fault(v[i], `${path}[${i}]`, seen);
32
+ if (f) return f;
33
+ }
34
+ return null;
35
+ }
36
+ const proto = Object.getPrototypeOf(v);
37
+ if (proto !== Object.prototype && proto !== null) return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
38
+ for (const k of Object.keys(v)) {
39
+ const f = fault((v as Record<string, unknown>)[k], `${path}.${k}`, seen);
40
+ if (f) return f;
41
+ }
42
+ return null;
43
+ } finally {
44
+ seen.delete(v);
45
+ }
46
+ }
47
+
48
+ export const cellFault = (v: unknown, path: string): string | null => fault(v, path, new Set());
49
+
50
+ // The guard is one proxy at the root and one for every container read through
51
+ // it, so a write nested three deep is refused the same way a write at the top
52
+ // is. Wrappers are remembered, so reading the same array twice is the same
53
+ // object twice and a being may still compare what she holds.
54
+ const wrapped = new WeakMap<object, object>();
55
+ const guards = new WeakSet();
56
+
57
+ function guard<T extends object>(target: T, path: string): T {
58
+ if (guards.has(target)) return target;
59
+ const had = wrapped.get(target);
60
+ if (had) return had as T;
61
+ const p = new Proxy(target, {
62
+ get(t, k, r) {
63
+ const v = Reflect.get(t, k, r);
64
+ // A container reached through her cells is part of her cells.
65
+ return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v as object, `${path}.${String(k)}`) : v;
66
+ },
67
+ set(t, k, v, r) {
68
+ if (typeof k === 'string') {
69
+ const f = cellFault(v, `${path}.${k}`);
70
+ if (f) throw new TypeError(`cells hold values: ${f}`);
71
+ }
72
+ return Reflect.set(t, k, v, r);
73
+ },
74
+ defineProperty(t, k, d) {
75
+ if (typeof k === 'string' && 'value' in d) {
76
+ const f = cellFault(d.value, `${path}.${k}`);
77
+ if (f) throw new TypeError(`cells hold values: ${f}`);
78
+ }
79
+ return Reflect.defineProperty(t, k, d);
80
+ },
81
+ });
82
+ wrapped.set(target, p);
83
+ guards.add(p);
84
+ return p;
85
+ }
86
+
87
+ // Her cells, guarded. Called once per being at boot; the guarded object is
88
+ // what goes into the partition and what the stance hands her, so there is no
89
+ // second door onto the same cells.
90
+ export const guardCells = (cells: Cells): Cells => guard(cells as unknown as Record<string, Json>, 'cells') as unknown as Cells;
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Judgment at the door. Sealed bytes in, sealed bytes out. Every failure is
3
+ // one silence, sealed to whoever asked so that a stranger learns nothing,
4
+ // not even which case they hit. What passes is named and dispatched to her
5
+ // answer, and her digest for that asker rides back with the object.
6
+ import { isSilence, isUnreached } from '../being/silence.ts';
7
+ import { digest } from '../being/digest.ts';
8
+ import type { Asker, BeingLike, JsonObject } from '../being/types.ts';
9
+ import { at } from './partition.ts';
10
+ import type { Heirs } from './heirs.ts';
11
+ import { openAsk, sealReply, verifyAsk, type ReplyPayload, type WardKey } from './seal.ts';
12
+ import { spent } from './allowance.ts';
13
+ import { KEY, sealingPair } from './arithmetic.ts';
14
+
15
+ export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, unknown> } };
16
+ const SILENCE: ReplyPayload = { silence: true };
17
+
18
+ // One arrival at one being, already named. Catches every throw.
19
+ export async function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject): Promise<ReplyPayload> {
20
+ let out: Awaited<ReturnType<BeingLike['answer']>>;
21
+ try {
22
+ out = await door.being.answer(asker, method, args);
23
+ } catch {
24
+ return SILENCE; // she threw where she was asked. there is no answer.
25
+ }
26
+ // Nothing at all is not an answer either: a method that forgot to return
27
+ // has said nothing, and nothing is silence, never a value that JSON drops
28
+ // on the way out and the far side reads back as a fourth word.
29
+ if (out === undefined || isSilence(out) || isUnreached(out)) return SILENCE;
30
+ if (method === undefined) return { object: out, seen: null };
31
+ // The digest rides along, it is not the answer. She has already answered:
32
+ // a describe that will not run costs the digest, and nothing else.
33
+ return { object: out, seen: await seen(door, asker) };
34
+ }
35
+
36
+ async function seen(door: Door, asker: Asker): Promise<string | null> {
37
+ try {
38
+ const bp = await door.being.answer(asker);
39
+ return isSilence(bp) || isUnreached(bp) ? null : await digest(bp);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array) {
46
+ const judge = async (bytes: Uint8Array): Promise<{ reply: ReplyPayload; ephemeralPk: Uint8Array } | null> => {
47
+ const a = await openAsk(bytes, key.padlock);
48
+ if (!a) return null; // it did not open. there is nobody to answer.
49
+ const { to, payload, ephemeralPk } = a;
50
+ // The allowance, read before anything is done under it. A budget already
51
+ // gone is one silence like every other refusal here: a stranger learns
52
+ // nothing, not even that what she ran out of was time. Nothing is spent
53
+ // and nothing rotates, because nothing was heard.
54
+ if (spent({ time: payload.time })) return { reply: SILENCE, ephemeralPk };
55
+ // An ask with no doors left is one silence like a budget already gone.
56
+ // Nothing sets hops and nothing decrements it yet; this door refuses zero
57
+ // so that a relay chain invented later meets doors that already stop it.
58
+ if (payload.hops === 0) return { reply: SILENCE, ephemeralPk };
59
+ const args = payload.args ?? {};
60
+ if (to === null) {
61
+ const pk = publicKey();
62
+ const pub = pk !== null ? doors.get(pk) : undefined;
63
+ if (!pub || !(await verifyAsk(a, payload.by))) return { reply: SILENCE, ephemeralPk };
64
+ return { reply: await arrive(pub, {}, payload.method, args), ephemeralPk };
65
+ }
66
+ const h = heirs.admits(to, payload.by);
67
+ if (!h) return { reply: SILENCE, ephemeralPk };
68
+ const door = doors.get(h.being);
69
+ if (!door || !at(door.cells.occupants, h.id)) return { reply: SILENCE, ephemeralPk }; // she removed it, or she is gone
70
+ if (!(await verifyAsk(a, payload.by))) return { reply: SILENCE, ephemeralPk };
71
+ // Once only, and only now: the number is spent after the signature, so a
72
+ // stranger cannot burn a number she could not sign for, and together with
73
+ // the keys, so the same bytes twice rotate nothing and refusal writes nothing.
74
+ if (!heirs.honour(h, payload.by, payload.next, payload.seq)) return { reply: SILENCE, ephemeralPk };
75
+ return { reply: await arrive(door, { id: h.id }, payload.method, args), ephemeralPk };
76
+ };
77
+
78
+ // The door itself. Always answers bytes. When the ask did not open, the
79
+ // reply is sealed to the ephemeral pk on its lid, which is all a stranger
80
+ // holds, and says silence.
81
+ //
82
+ // A lid that is not a key -- a small-order point, of which the two curves
83
+ // have several each -- makes a dead agreement, and the seal refuses it. The
84
+ // door never throws: that reply is noise, sealed to a key nobody holds.
85
+ return async function door(bytes: Uint8Array): Promise<Uint8Array> {
86
+ const out = await judge(bytes);
87
+ const reply = out?.reply ?? SILENCE;
88
+ try {
89
+ return await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32));
90
+ } catch {
91
+ return sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32));
92
+ }
93
+ };
94
+ }
95
+
96
+ // The ephemeral pk on an ask that would not open, if the bytes are long
97
+ // enough to carry one. Otherwise a fresh key nobody holds: the reply is
98
+ // bytes, and it is noise.
99
+ function lid(bytes: Uint8Array, random: (n: number) => Uint8Array): Uint8Array {
100
+ if (bytes instanceof Uint8Array && bytes.length >= KEY) return bytes.subarray(0, KEY);
101
+ return random(KEY);
102
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The ground. The one object a harbor passes a ward at birth. Five things,
3
+ // never a sixth. Everything a runtime differs on arrives here, which is
4
+ // why the ward itself knows no runtime.
5
+ import type { Stance, BeingLike } from '../being/types.ts';
6
+
7
+ export type Ground = {
8
+ seed: string | Uint8Array; // the ward derives its pk from it and nothing else
9
+ memory: Record<string, unknown>; // the partition. the ward's files. opaque to the harbor
10
+ instantiate(className: string, stance: Stance): BeingLike | null; // the code half
11
+ // Sealed bytes to a ward pk. What comes back, or undefined.
12
+ //
13
+ // undefined is a promise, not a shrug: no door was reached, and nothing was
14
+ // delivered. The ward hands it to a being as unreached, which is the one
15
+ // answer that says asking again is safe, so a harbor may only return it
16
+ // when it knows the bytes never arrived — no reach for that pk, a socket
17
+ // that would not open, a link that is down.
18
+ //
19
+ // A harbor that sent the bytes and then gave up waiting knows no such
20
+ // thing: the far door may have heard and be working still. It must not
21
+ // answer at all in that case. The ward bounds every ask itself, and an
22
+ // answer that never comes is silence, which promises nothing. So a harbor
23
+ // may hold a shorter patience than the ward's for its own reasons — a
24
+ // socket it wants back, a queue it will not grow — and the two bounds never
25
+ // need to read each other: whichever ends first ends the ask, and each says
26
+ // only what it can honestly say.
27
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
28
+ random(n: number): Uint8Array; // entropy. every key a ward mints is drawn from it
29
+ };
30
+
31
+ // What a ward hands back. Two pointers.
32
+ export type WardPointers = {
33
+ door(bytes: Uint8Array): Promise<Uint8Array>;
34
+ ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
35
+ };
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Heirs. The door's view of every occupant of every being in the ward, by
3
+ // the heir pk the invitation carried. One rule: the key I hold for you may
4
+ // speak, and so may the key it announced last time. A knock is the first
5
+ // use of the heir, which the inviter announced on your behalf, and the heir
6
+ // dies as it speaks.
7
+ import type { Heir, Partition } from './partition.ts';
8
+
9
+ // How wide the span is, is the ward's own — wider is more forgiving of a
10
+ // rough road, and no peer can tell the difference except by being refused.
11
+ const SPAN = 64;
12
+
13
+ export class Heirs {
14
+ #p: Partition;
15
+ constructor(p: Partition) {
16
+ this.#p = p;
17
+ }
18
+ open(heir: string, being: string, id: string): void {
19
+ this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
20
+ }
21
+ close(heir: string): void {
22
+ delete this.#p.heirs[heir];
23
+ }
24
+ get(heir: string): Heir | undefined {
25
+ return this.#p.heirs[heir];
26
+ }
27
+ // May `by` speak for this heir? Returns the record if so, null if not.
28
+ // Does not write: the caller verifies the signature first, then settles.
29
+ admits(heir: string, by: string): Heir | null {
30
+ const h = this.#p.heirs[heir];
31
+ if (!h) return null;
32
+ if (h.fresh) return by === h.current ? h : null;
33
+ return by === h.current || by === h.announced ? h : null;
34
+ }
35
+ // Once only. A number above the mark is honoured and moves it; a number
36
+ // inside the span is honoured once and never again; a number at or below
37
+ // the span is refused, because a door that remembered every number ever
38
+ // seen would be a door with unbounded memory. Counting starts where
39
+ // strangers must agree: the first legal number is one.
40
+ //
41
+ // The number rides inside the signed body, so bytes caught on the road
42
+ // carry the number they were sent under and are refused as themselves. A
43
+ // caller who means to ask again asks again, under the next number, and is
44
+ // heard: retry and fire-and-forget stay hers to build. Only the accident
45
+ // and the interception are refused.
46
+ //
47
+ // Writes, so the caller settles it only once the signature has checked out.
48
+ // The same bytes twice are one honoured call and one silence.
49
+ spend(h: Heir, seq: number): boolean {
50
+ if (!Number.isSafeInteger(seq) || seq < 1) return false;
51
+ if (seq > h.mark) {
52
+ if (h.mark > 0) h.spent.push(h.mark);
53
+ h.mark = seq;
54
+ h.spent = h.spent.filter((past) => past > seq - SPAN);
55
+ return true;
56
+ }
57
+ if (seq === h.mark || seq <= h.mark - SPAN || h.spent.includes(seq)) return false;
58
+ h.spent.push(seq);
59
+ return true;
60
+ }
61
+
62
+ // The signature checked out. Spend the number and settle the keys, or do
63
+ // neither: a call that binds nothing must not burn a number on its way to
64
+ // being refused, or a stranger who cannot be heard would still leave a mark
65
+ // behind her. Every write below this line is one that is going to hold.
66
+ honour(h: Heir, by: string, next: string | null, seq: number): boolean {
67
+ if (h.fresh && next === null) return false; // a knock without a key of her own binds nothing
68
+ if (!this.spend(h, seq)) return false;
69
+ return this.settle(h, by, next);
70
+ }
71
+
72
+ // The signature checked out. Settle the keys: a fresh heir rotates at once
73
+ // to what it announced and must announce something; a current key replaces
74
+ // its announcement; an announced key becomes current.
75
+ settle(h: Heir, by: string, next: string | null): boolean {
76
+ if (h.fresh) {
77
+ if (next === null) return false; // a knock without a key of her own binds nothing
78
+ h.current = next;
79
+ h.announced = null;
80
+ h.fresh = false;
81
+ return true;
82
+ }
83
+ if (by === h.announced) h.current = by;
84
+ h.announced = next;
85
+ return true;
86
+ }
87
+ }
@@ -0,0 +1,14 @@
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.ts';
4
+ export type { Ground, WardPointers } from './ground.ts';
5
+ export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
6
+ export type { AskPayload, ReplyPayload } from './seal.ts';
7
+ // The seal and the arithmetic, for a kit in another language to check its
8
+ // bytes against, and for tests that speak to a door directly.
9
+ export * as seal from './seal.ts';
10
+ export * as arithmetic from './arithmetic.ts';
11
+ // The allowance every ask carries, for a kit in another language and for
12
+ // tests that speak to a door directly.
13
+ export { allow, spent, DEFAULT, CEILING } from './allowance.ts';
14
+ export type { Allowance } from './allowance.ts';