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.
Files changed (205) hide show
  1. package/README.md +66 -101
  2. package/dist/being/being.d.ts +7 -15
  3. package/dist/being/being.js +16 -67
  4. package/dist/being/digest.d.ts +1 -1
  5. package/dist/being/digest.js +7 -27
  6. package/dist/being/faculty.d.ts +9 -0
  7. package/dist/being/faculty.js +68 -0
  8. package/dist/being/index.d.ts +4 -5
  9. package/dist/being/index.js +5 -5
  10. package/dist/being/types.d.ts +35 -62
  11. package/dist/being/types.js +3 -57
  12. package/dist/being/words.d.ts +13 -0
  13. package/dist/being/words.js +14 -0
  14. package/dist/contract/index.d.ts +26 -0
  15. package/dist/contract/index.js +19 -0
  16. package/dist/crypto/aes.d.ts +3 -0
  17. package/dist/crypto/aes.js +24 -0
  18. package/dist/crypto/bytes.d.ts +6 -0
  19. package/dist/crypto/bytes.js +33 -0
  20. package/dist/crypto/ed25519.d.ts +3 -0
  21. package/dist/crypto/ed25519.js +85 -0
  22. package/dist/crypto/hash.d.ts +2 -0
  23. package/dist/crypto/hash.js +11 -0
  24. package/dist/crypto/index.d.ts +7 -0
  25. package/dist/crypto/index.js +10 -0
  26. package/dist/crypto/json.d.ts +8 -0
  27. package/dist/crypto/json.js +250 -0
  28. package/dist/crypto/mlkem.d.ts +12 -0
  29. package/dist/crypto/mlkem.js +36 -0
  30. package/dist/crypto/subtle.d.ts +10 -0
  31. package/dist/crypto/subtle.js +29 -0
  32. package/dist/crypto/x25519.d.ts +2 -0
  33. package/dist/crypto/x25519.js +21 -0
  34. package/dist/folder/index.d.ts +13 -0
  35. package/dist/folder/index.js +125 -0
  36. package/dist/harbor/dock.d.ts +19 -0
  37. package/dist/harbor/dock.js +92 -0
  38. package/dist/harbor/harbor.d.ts +22 -0
  39. package/dist/harbor/harbor.js +84 -0
  40. package/dist/harbor/index.d.ts +4 -10
  41. package/dist/harbor/index.js +5 -18
  42. package/dist/harbor/registry.d.ts +7 -0
  43. package/dist/harbor/registry.js +13 -0
  44. package/dist/harbor/terrain.d.ts +8 -0
  45. package/dist/harbor/terrain.js +2 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.js +14 -0
  48. package/dist/pointer/bodies.d.ts +20 -0
  49. package/dist/pointer/bodies.js +45 -0
  50. package/dist/pointer/index.d.ts +2 -0
  51. package/dist/pointer/index.js +4 -0
  52. package/dist/pointer/world.d.ts +34 -0
  53. package/dist/pointer/world.js +76 -0
  54. package/dist/quo/door.d.ts +34 -0
  55. package/dist/quo/door.js +172 -0
  56. package/dist/quo/index.d.ts +8 -0
  57. package/dist/quo/index.js +11 -0
  58. package/dist/quo/invitation.d.ts +7 -0
  59. package/dist/quo/invitation.js +15 -0
  60. package/dist/quo/keys.d.ts +40 -0
  61. package/dist/quo/keys.js +79 -0
  62. package/dist/quo/payload.d.ts +15 -0
  63. package/dist/quo/payload.js +55 -0
  64. package/dist/quo/relations.d.ts +40 -0
  65. package/dist/quo/relations.js +33 -0
  66. package/dist/quo/reply.d.ts +13 -0
  67. package/dist/quo/reply.js +35 -0
  68. package/dist/quo/seal.d.ts +42 -0
  69. package/dist/quo/seal.js +78 -0
  70. package/dist/quo/standing.d.ts +39 -0
  71. package/dist/quo/standing.js +90 -0
  72. package/dist/ward/allowance.d.ts +13 -9
  73. package/dist/ward/allowance.js +28 -67
  74. package/dist/ward/cells.d.ts +6 -6
  75. package/dist/ward/cells.js +67 -179
  76. package/dist/ward/index.d.ts +5 -10
  77. package/dist/ward/index.js +6 -13
  78. package/dist/ward/partition.d.ts +44 -60
  79. package/dist/ward/partition.js +56 -285
  80. package/dist/ward/stance.d.ts +29 -20
  81. package/dist/ward/stance.js +173 -406
  82. package/dist/ward/ward-being.d.ts +33 -0
  83. package/dist/ward/ward-being.js +76 -0
  84. package/dist/ward/ward.d.ts +43 -11
  85. package/dist/ward/ward.js +215 -363
  86. package/package.json +19 -33
  87. package/src/being/being.ts +31 -78
  88. package/src/being/digest.ts +7 -35
  89. package/src/being/faculty.ts +66 -0
  90. package/src/being/index.ts +5 -6
  91. package/src/being/types.ts +47 -145
  92. package/src/being/words.ts +31 -0
  93. package/src/contract/index.ts +44 -0
  94. package/src/crypto/aes.ts +26 -0
  95. package/src/crypto/bytes.ts +37 -0
  96. package/src/crypto/ed25519.ts +84 -0
  97. package/src/crypto/hash.ts +14 -0
  98. package/src/crypto/index.ts +10 -0
  99. package/src/crypto/json.ts +241 -0
  100. package/src/crypto/mlkem.ts +38 -0
  101. package/src/crypto/subtle.ts +33 -0
  102. package/src/crypto/x25519.ts +21 -0
  103. package/src/folder/index.ts +134 -0
  104. package/src/harbor/dock.ts +101 -0
  105. package/src/harbor/harbor.ts +105 -0
  106. package/src/harbor/index.ts +5 -19
  107. package/src/harbor/registry.ts +20 -0
  108. package/src/harbor/terrain.ts +13 -0
  109. package/src/index.ts +20 -0
  110. package/src/pointer/bodies.ts +47 -0
  111. package/src/pointer/index.ts +4 -0
  112. package/src/pointer/world.ts +91 -0
  113. package/src/quo/door.ts +178 -0
  114. package/src/quo/index.ts +11 -0
  115. package/src/quo/invitation.ts +15 -0
  116. package/src/quo/keys.ts +91 -0
  117. package/src/quo/payload.ts +61 -0
  118. package/src/quo/relations.ts +62 -0
  119. package/src/quo/reply.ts +38 -0
  120. package/src/quo/seal.ts +101 -0
  121. package/src/quo/standing.ts +111 -0
  122. package/src/stand/main.ts +19 -0
  123. package/src/stand/stand.ts +178 -0
  124. package/src/ward/allowance.ts +37 -75
  125. package/src/ward/cells.ts +63 -176
  126. package/src/ward/index.ts +6 -17
  127. package/src/ward/partition.ts +86 -326
  128. package/src/ward/stance.ts +185 -420
  129. package/src/ward/ward-being.ts +97 -0
  130. package/src/ward/ward.ts +229 -363
  131. package/dist/being/lent.d.ts +0 -32
  132. package/dist/being/lent.js +0 -72
  133. package/dist/being/silence.d.ts +0 -12
  134. package/dist/being/silence.js +0 -41
  135. package/dist/conformance/assert.d.ts +0 -11
  136. package/dist/conformance/assert.js +0 -106
  137. package/dist/conformance/beings.d.ts +0 -199
  138. package/dist/conformance/beings.js +0 -188
  139. package/dist/conformance/estate.d.ts +0 -5
  140. package/dist/conformance/estate.js +0 -388
  141. package/dist/conformance/index.d.ts +0 -80
  142. package/dist/conformance/index.js +0 -819
  143. package/dist/conformance/reach.d.ts +0 -10
  144. package/dist/conformance/reach.js +0 -72
  145. package/dist/conformance/store.d.ts +0 -5
  146. package/dist/conformance/store.js +0 -113
  147. package/dist/harbor/box.d.ts +0 -121
  148. package/dist/harbor/box.js +0 -121
  149. package/dist/harbor/core.d.ts +0 -55
  150. package/dist/harbor/core.js +0 -662
  151. package/dist/harbor/dial.d.ts +0 -9
  152. package/dist/harbor/dial.js +0 -81
  153. package/dist/harbor/memory.d.ts +0 -29
  154. package/dist/harbor/memory.js +0 -104
  155. package/dist/harbor/reach.d.ts +0 -36
  156. package/dist/harbor/reach.js +0 -199
  157. package/dist/harbor/store.d.ts +0 -35
  158. package/dist/harbor/store.js +0 -62
  159. package/dist/vector/cases.d.ts +0 -42
  160. package/dist/vector/cases.js +0 -223
  161. package/dist/vector/index.d.ts +0 -6
  162. package/dist/vector/index.js +0 -8
  163. package/dist/vector/stand.d.ts +0 -9
  164. package/dist/vector/stand.js +0 -77
  165. package/dist/vector/world.d.ts +0 -144
  166. package/dist/vector/world.js +0 -209
  167. package/dist/ward/arithmetic.d.ts +0 -31
  168. package/dist/ward/arithmetic.js +0 -249
  169. package/dist/ward/door.d.ts +0 -18
  170. package/dist/ward/door.js +0 -186
  171. package/dist/ward/ground.d.ts +0 -29
  172. package/dist/ward/ground.js +0 -56
  173. package/dist/ward/heirs.d.ts +0 -13
  174. package/dist/ward/heirs.js +0 -117
  175. package/dist/ward/json.d.ts +0 -2
  176. package/dist/ward/json.js +0 -163
  177. package/dist/ward/owner.d.ts +0 -13
  178. package/dist/ward/owner.js +0 -220
  179. package/dist/ward/seal.d.ts +0 -52
  180. package/dist/ward/seal.js +0 -150
  181. package/src/being/lent.ts +0 -72
  182. package/src/being/silence.ts +0 -46
  183. package/src/conformance/assert.ts +0 -100
  184. package/src/conformance/beings.ts +0 -188
  185. package/src/conformance/estate.ts +0 -412
  186. package/src/conformance/index.ts +0 -965
  187. package/src/conformance/reach.ts +0 -83
  188. package/src/conformance/store.ts +0 -125
  189. package/src/harbor/box.ts +0 -131
  190. package/src/harbor/core.ts +0 -699
  191. package/src/harbor/dial.ts +0 -112
  192. package/src/harbor/memory.ts +0 -123
  193. package/src/harbor/reach.ts +0 -221
  194. package/src/harbor/store.ts +0 -91
  195. package/src/vector/cases.ts +0 -257
  196. package/src/vector/index.ts +0 -11
  197. package/src/vector/stand.ts +0 -76
  198. package/src/vector/world.ts +0 -232
  199. package/src/ward/arithmetic.ts +0 -251
  200. package/src/ward/door.ts +0 -186
  201. package/src/ward/ground.ts +0 -142
  202. package/src/ward/heirs.ts +0 -116
  203. package/src/ward/json.ts +0 -144
  204. package/src/ward/owner.ts +0 -214
  205. package/src/ward/seal.ts +0 -178
package/src/ward/heirs.ts DELETED
@@ -1,116 +0,0 @@
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 { DoorWord } from '../being/types.ts';
8
- import { GONE, type Heir, type Partition } from './partition.ts';
9
-
10
- // How wide the span is, is the spec's and never a ward's. Wider is more
11
- // forgiving of a rough road, and that is exactly why it is not a ward's to
12
- // choose: a peer tells two doors apart by being refused at one and answered
13
- // at the other, which is one relation dying over one number.
14
- const SPAN = 64;
15
-
16
- export class Heirs {
17
- #p: Partition;
18
- #wrote: () => void;
19
- constructor(p: Partition, wrote: () => void = () => {}) {
20
- this.#p = p;
21
- this.#wrote = wrote;
22
- }
23
- open(heir: string, being: string, id: string): void {
24
- this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
25
- this.#wrote();
26
- }
27
- // The id was removed. The heir is forgotten, and its last keys are kept
28
- // apart, bounded, so that whoever still holds them hears `removed` at the
29
- // door, once they have signed as themselves, and nobody else hears a thing.
30
- close(heir: string): void {
31
- const h = this.#p.heirs[heir];
32
- if (!h) return;
33
- delete this.#p.heirs[heir];
34
- this.#p.gone[heir] = { current: h.current, announced: h.announced };
35
- const keys = Object.keys(this.#p.gone);
36
- for (const old of keys.slice(0, Math.max(0, keys.length - GONE))) delete this.#p.gone[old];
37
- this.#wrote();
38
- }
39
- // May `by` speak for a relation she removed? True only for the keys the
40
- // door held when the id went, which nobody but their holder has.
41
- gone(heir: string, by: string): boolean {
42
- const g = this.#p.gone[heir];
43
- return !!g && (by === g.current || by === g.announced);
44
- }
45
- // May `by` speak for this heir? Returns the record if so, null if not.
46
- // Does not write: the caller verifies the signature first, then settles.
47
- admits(heir: string, by: string): Heir | null {
48
- const h = this.#p.heirs[heir];
49
- if (!h) return null;
50
- if (h.fresh) return by === h.current ? h : null;
51
- return by === h.current || by === h.announced ? h : null;
52
- }
53
- // Once only. A number above the mark is honoured and moves it; a number
54
- // inside the span is honoured once and never again; a number at or below
55
- // the span is refused, because a door that remembered every number ever
56
- // seen would be a door with unbounded memory. Counting starts where
57
- // strangers must agree: the first legal number is one.
58
- //
59
- // The number rides inside the signed body, so bytes caught on the road
60
- // carry the number they were sent under and are refused as themselves. A
61
- // caller who means to ask again asks again, under the next number, and is
62
- // heard: retry and fire-and-forget stay hers to build. Only the accident
63
- // and the interception are refused.
64
- //
65
- // Writes, so the caller settles it only once the signature has checked out.
66
- // The same bytes twice are one honoured call and one silence.
67
- spend(h: Heir, seq: number): boolean {
68
- if (!Number.isSafeInteger(seq) || seq < 1) return false;
69
- if (seq > h.mark) {
70
- if (h.mark > 0) h.spent.push(h.mark);
71
- h.mark = seq;
72
- h.spent = h.spent.filter((past) => past > seq - SPAN);
73
- return true;
74
- }
75
- if (seq === h.mark || seq <= h.mark - SPAN || h.spent.includes(seq)) return false;
76
- h.spent.push(seq);
77
- return true;
78
- }
79
-
80
- // The signature checked out. Spend the number and settle the keys, or do
81
- // neither: a call that binds nothing must not burn a number on its way to
82
- // being refused, or a stranger who cannot be heard would still leave a mark
83
- // behind her. Every write below this line is one that is going to hold.
84
- // This is where a knock is judged and where `unannounced` is said. settle
85
- // below is the writing alone, and it judges nothing: one owner for the
86
- // question, one for the answer.
87
- honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord {
88
- if (h.fresh && (next === null || next === h.current)) return 'unannounced'; // a knock without a key of her own binds nothing, and the heir is not a key of her own
89
- if (!this.spend(h, seq)) return 'repeated';
90
- this.settle(h, by, next);
91
- this.#wrote(); // the number and the keys moved together
92
- return true;
93
- }
94
-
95
- // The signature checked out and honour has judged. Settle the keys: a fresh
96
- // heir rotates at once to what it announced, which honour has already held
97
- // to being something other than itself, so that the heir dies as it speaks;
98
- // a current key replaces its announcement; an announced key becomes current.
99
- settle(h: Heir, by: string, next: string | null): void {
100
- if (h.fresh) {
101
- if (next === null) return; // honour judged this. the compiler has not read it, so the narrowing stays.
102
- h.current = next;
103
- h.announced = null;
104
- h.fresh = false;
105
- return;
106
- }
107
- if (by === h.announced) h.current = by;
108
- // A send that announced nothing leaves the spare standing. This kit
109
- // announces on every send but a public one, which holds no heir and
110
- // reaches none of this; a kit that skips one is a kit whose own next is
111
- // still the key it announced last time, and forgetting it here would meet
112
- // that key with silence and kill a healthy relation. Nothing is dropped
113
- // that was vouched for until something replaces it.
114
- if (next !== null) h.announced = next;
115
- }
116
- }
package/src/ward/json.ts DELETED
@@ -1,144 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The one reader of JSON that crossed an edge. A runtime's own parser is
3
- // where two kits stop agreeing: it keeps the last of two keys of one name,
4
- // rounds a whole number no double holds, reads a number past the doubles as
5
- // infinity, and mends bytes that are not UTF-8 into a replacement character.
6
- // Each of those is text two kits read two ways, so each is refused here, where
7
- // it is read, with a throw the caller already turns into what bytes that said
8
- // nothing are.
9
- //
10
- // What a value is beyond its text, a lone surrogate, a key named `__proto__`,
11
- // a nesting past the bound, is the value rule's, and `cellFault` in cells.ts
12
- // is its one reader. This file holds the rules that live only in the text.
13
-
14
- const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
15
-
16
- const fail = (at: number, why: string): never => {
17
- throw new Error(`JSON at ${at}: ${why}`);
18
- };
19
-
20
- // A number text, held to the Values chapter. A text naming a whole number
21
- // stands only if that number is exactly a double, or if the text is how
22
- // ECMAScript writes the double it reads as. A text past the largest double,
23
- // or naming a value other than zero that rounds to zero, is refused, and so
24
- // is minus zero in any spelling.
25
- export function numberFault(literal: string): string | null {
26
- const m = /^(-?)(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(literal);
27
- if (!m) return 'not a number';
28
- const [, sign, whole, frac = '', exp = '0'] = m;
29
- const v = Number(literal);
30
- if (!Number.isFinite(v)) return 'past the largest double';
31
- let digits = (whole + frac).replace(/^0+/, '');
32
- if (v === 0) {
33
- if (digits !== '') return 'a value other than zero that rounds to zero';
34
- return sign ? 'minus zero' : null;
35
- }
36
- // The exact value is digits times ten to e. A finite double bounds e from
37
- // above, so the power below is never larger than the double's own width.
38
- let e = BigInt(exp) - BigInt(frac.length);
39
- const trimmed = digits.replace(/0+$/, '');
40
- e += BigInt(digits.length - trimmed.length);
41
- digits = trimmed;
42
- if (e < 0n) return null; // a fraction: the double nearest it, as every reader takes it
43
- const exact = BigInt(digits) * 10n ** e;
44
- if (exact === (v < 0 ? -BigInt(v) : BigInt(v))) return null;
45
- return literal === String(v) ? null : 'a whole number that is not exactly a double';
46
- }
47
-
48
- // Bytes to a value, or a throw.
49
- export function readJson(input: Uint8Array | string): unknown {
50
- const s = typeof input === 'string' ? input : utf8.decode(input);
51
- let at = 0;
52
- const space = () => {
53
- for (;;) {
54
- const c = s.charCodeAt(at);
55
- if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) at += 1;
56
- else return;
57
- }
58
- };
59
- const string = (): string => {
60
- const start = at;
61
- at += 1;
62
- for (;;) {
63
- const c = s.charCodeAt(at);
64
- if (Number.isNaN(c)) fail(start, 'an unterminated string');
65
- if (c === 0x22) break;
66
- if (c < 0x20) fail(at, 'a control character in a string');
67
- at += c === 0x5c ? 2 : 1;
68
- }
69
- at += 1;
70
- // The escapes are JSON's own, and the runtime reads them the one way.
71
- return JSON.parse(s.slice(start, at)) as string;
72
- };
73
- const value = (): unknown => {
74
- space();
75
- const c = s[at];
76
- if (c === '{') {
77
- at += 1;
78
- const out: Record<string, unknown> = {};
79
- const keys = new Set<string>();
80
- space();
81
- if (s[at] === '}') {
82
- at += 1;
83
- return out;
84
- }
85
- for (;;) {
86
- space();
87
- if (s[at] !== '"') fail(at, 'a key that is not a string');
88
- const where = at;
89
- const k = string();
90
- if (keys.has(k)) fail(where, `two keys named ${JSON.stringify(k)}`);
91
- keys.add(k);
92
- space();
93
- if (s[at] !== ':') fail(at, 'a key with no colon');
94
- at += 1;
95
- // Defined, never assigned: a key named `__proto__` is an own key here
96
- // as it is in JSON.parse, and the value rule refuses it after.
97
- Object.defineProperty(out, k, { value: value(), enumerable: true, writable: true, configurable: true });
98
- space();
99
- if (s[at] === ',') at += 1;
100
- else if (s[at] === '}') {
101
- at += 1;
102
- return out;
103
- } else fail(at, 'an object not closed');
104
- }
105
- }
106
- if (c === '[') {
107
- at += 1;
108
- const out: unknown[] = [];
109
- space();
110
- if (s[at] === ']') {
111
- at += 1;
112
- return out;
113
- }
114
- for (;;) {
115
- out.push(value());
116
- space();
117
- if (s[at] === ',') at += 1;
118
- else if (s[at] === ']') {
119
- at += 1;
120
- return out;
121
- } else fail(at, 'a list not closed');
122
- }
123
- }
124
- if (c === '"') return string();
125
- for (const [word, v] of [['true', true], ['false', false], ['null', null]] as const) {
126
- if (s.startsWith(word, at)) {
127
- at += word.length;
128
- return v;
129
- }
130
- }
131
- const m = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
132
- m.lastIndex = at;
133
- const hit = m.exec(s);
134
- if (!hit) return fail(at, 'no value');
135
- const why = numberFault(hit[0]);
136
- if (why !== null) fail(at, `${hit[0]} is ${why}`);
137
- at += hit[0].length;
138
- return Number(hit[0]);
139
- };
140
- const out = value();
141
- space();
142
- if (at !== s.length) fail(at, 'text after the value');
143
- return out;
144
- }
package/src/ward/owner.ts DELETED
@@ -1,214 +0,0 @@
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. The ward is a
4
- // being to her owner: the empty ask is her describe, and it carries her asks
5
- // with a description and an input each, so a side renders them as it
6
- // renders any being's. Seven asks: boot, public, invite, knock, remove and
7
- // unboot, which is boot's inverse and takes her relations with her, and ask,
8
- // which reaches into a being and asks her. The knock is a being's knock made
9
- // for her, and the standing is written into her cells under the id the owner
10
- // gave; remove takes a relation out of her by id, the mirror of it.
11
- import { isSilence, isWord, wordOf } from '../being/silence.ts';
12
- import { OWNER } from '../being/types.ts';
13
- import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
14
- import { put } from './partition.ts';
15
- import { seen, answered } from './door.ts';
16
- import { within, allow, DEFAULT, LATE } from './allowance.ts';
17
- import type { Resident } from './ward.ts';
18
-
19
- export type OwnerSide = {
20
- pk: string;
21
- doors: Map<string, Resident>;
22
- absent(): Record<string, string | null>; // the rows no door holds this run: key to class
23
- publicKey(): string | null;
24
- // Class first, then key, as the ground and a being's own boot both say it.
25
- instantiate(className: string, key: string): Resident | 'threw' | null;
26
- unboot(key: string): string[] | null;
27
- setPublic(key: string | null): void; // null takes the mark off, and the being stays
28
- };
29
-
30
- const str = { type: 'string' };
31
- export const OWNER_ASKS: Ask[] = [
32
- { name: 'boot', description: 'boot a being by class name, under a key the owner chooses', input: { type: 'object', properties: { key: str, class: str }, required: ['key', 'class'] } },
33
- { name: 'public', description: 'mark a booted being as the one public being of the ward, reached by anyone at the bare pk; null takes the mark off and leaves her booted', input: { type: 'object', properties: { key: str }, required: ['key'] } },
34
- { name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by, with the notes she will read on it; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str, notes: { type: 'object' } }, required: ['being', 'id'] } },
35
- {
36
- name: 'knock',
37
- description: 'knock for a being of the ward with an invitation, and take the standing under id if answered; being is a key booted, or { boot: class, key } to boot her first',
38
- input: { type: 'object', properties: { being: { description: 'a key, or { boot, key }' }, id: str, invitation: { type: 'object' }, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } }, required: ['being', 'id', 'invitation'] },
39
- },
40
- { name: 'remove', description: 'take a relation out of a being of the ward by id, occupant or standing; on the ward pk it unseats an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
41
- { name: 'unboot', description: 'take a being out of the ward, with every relation she holds; her occupants hear removed, and the ward itself is refused', input: { type: 'object', properties: { being: str }, required: ['being'] } },
42
- {
43
- name: 'ask',
44
- description: 'ask a being of the ward, as the owner; with no being it is the public being asked as nobody, which is what a stranger would hear',
45
- input: { type: 'object', properties: { being: str, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } } },
46
- },
47
- ];
48
-
49
- export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json> {
50
- // Every name the owner gives is a string, or it is nothing. Coerced instead
51
- // of checked, an object passed where a name belongs becomes the string
52
- // "[object Object]", and two owners who each got the shape wrong would be
53
- // handed one being. Not `word`: a word in this tree is what a ward says when
54
- // no object came back, and this file imports those.
55
- const named = (v: unknown): string | null => (typeof v === 'string' ? v : null);
56
-
57
- if (method === undefined) return describe(w);
58
- if (method === 'boot') {
59
- const key = named(args.key),
60
- className = named(args.class);
61
- if (key === null || className === null) return { error: 'no such class, or key taken' };
62
- const door = w.instantiate(className, key);
63
- if (door === 'threw') return { error: 'threw at birth' };
64
- return door ? { booted: door.key } : { error: 'no such class, or key taken' };
65
- }
66
- if (method === 'public') {
67
- // A ward may have one public being, and no more. Marking a second would
68
- // leave the first holding every relation she had, reachable by nobody at
69
- // the bare pk, and told by nobody that she had been replaced. One absent
70
- // this run is reachable by nobody already, so the mark may move off her.
71
- // Null takes the mark off: the being stays and keeps every relation she
72
- // holds, and the ward answers arrivals for no heir with silence, as one
73
- // that never had a public being does. Retiring her is not destroying her.
74
- if (args.key === null) {
75
- w.setPublic(null);
76
- return { public: null };
77
- }
78
- const key = named(args.key);
79
- if (key === null || key === w.pk || !w.doors.has(key)) return { error: 'no such being' };
80
- const standing = w.publicKey();
81
- if (standing !== null && standing !== key && w.doors.has(standing)) return { error: 'a ward has one public being' };
82
- w.setPublic(key);
83
- return { public: key };
84
- }
85
- if (method === 'invite') {
86
- // On the ward itself, only the root may invite: an occupant of the ward
87
- // is an owner, and ownership is minted by the ask pointer alone. An owner
88
- // at the door asking to invite on the ward is refused, so that a carried
89
- // key can pilot but never hand piloting on.
90
- const being = named(args.being),
91
- id = named(args.id);
92
- if (being === null || id === null) return { error: 'no such being' };
93
- if (being === w.pk && asker.id !== OWNER) return { error: 'no such being' };
94
- // The notes are the terms the owner mints under, and the being reads them
95
- // on her gate. Placing an occupant is the owner's, and saying what it may
96
- // do is part of placing it; the work behind the gate is still hers alone.
97
- const notes = args.notes;
98
- const door = w.doors.get(being);
99
- if (!door) return { error: 'no such being' };
100
- return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes) : undefined);
101
- }
102
- if (method === 'knock') {
103
- const b = args.being as string | { boot: unknown; key: unknown };
104
- let door = typeof b === 'string' ? w.doors.get(b) : undefined;
105
- if (!door && b && typeof b === 'object') {
106
- // new or existing: the owner names a being of theirs, and a key already
107
- // booted is a being of theirs, not a class that failed to instantiate.
108
- const key = named(b.key),
109
- className = named(b.boot);
110
- if (key === null || className === null) return { error: 'no such being' };
111
- const made = w.instantiate(className, key);
112
- if (made === 'threw') return { error: 'threw at birth' };
113
- // Made or already there, the door is the one under the key she named:
114
- // instantiate boots under that key or refuses, and never another.
115
- door = w.doors.get(key);
116
- }
117
- const id = named(args.id);
118
- // Never for the ward itself, for the reason invite refuses it: the ward
119
- // is a being to nobody outside, and holds no relation of its own. A
120
- // standing written into its cells would be the owner's reach dressed as
121
- // a being's, and no ask of the ward's could ever use it.
122
- if (!door || id === null || door.key === w.pk) return { error: 'no such being' };
123
- const inv = args.invitation as Invitation;
124
- // The owner is a caller like any other and may say what this knock may
125
- // spend. Saying nothing is the ward's default, as it is for a being.
126
- const out = await door.stance.standings.knock(inv, args.method as string | undefined, (args.args as JsonObject) ?? {}, args.wanted as Wanted | undefined);
127
- if (isSilence(out)) return { error: 'silence' };
128
- if (isWord(out)) return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
129
- return { taken: await door.stance.standings.take(id, inv), answer: out };
130
- }
131
- if (method === 'remove') {
132
- // The mirror of knock: a relation out of a being, by id, occupant or
133
- // standing, since the two share one namespace. On the ward itself the
134
- // id is an owner, and only the root may unseat one, for the reason only
135
- // the root may invite one: ownership moves by the root alone.
136
- const being = named(args.being),
137
- id = named(args.id);
138
- if (being === null || id === null) return { error: 'no such being' };
139
- if (being === w.pk && asker.id !== OWNER) return { error: 'no such being' };
140
- const door = w.doors.get(being);
141
- if (!door) return { error: 'no such being' };
142
- if (Object.hasOwn(door.cells.occupants, id)) door.stance.occupants.remove(id);
143
- else if (Object.hasOwn(door.cells.standings, id)) door.stance.standings.remove(id);
144
- else return { error: 'nothing to remove' };
145
- return { removed: id };
146
- }
147
- if (method === 'unboot') {
148
- // The inverse of boot, and the only way a being leaves a ward. Her
149
- // relations go with her, so an occupant of hers hears `removed` from the
150
- // keys the door kept, exactly as it would had the owner removed each id
151
- // by hand; the owner hears which ids those were. The ward itself is
152
- // refused, for the reason knock is: it is a being to nobody outside, and
153
- // unbooting it would be a ward deleting itself from inside its own map,
154
- // leaving its owners bound to a door that is gone.
155
- const being = named(args.being);
156
- if (being === null || being === w.pk) return { error: 'no such being' };
157
- const removed = w.unboot(being);
158
- return removed === null ? { error: 'no such being' } : { unbooted: being, removed };
159
- }
160
- if (method === 'ask') {
161
- // The owner's other power: to reach into a being and ask her. It is
162
- // strictly less than unboot, which takes her out with every relation she
163
- // holds, and it is the third asker of the ward-to-being edge finally
164
- // filled in by the ward rather than minted by whoever holds the pointer.
165
- // Bounded, judged and named here, so no side outside the ward ever holds
166
- // a being's answer and writes the door's discipline again beside it.
167
- const being = args.being === undefined ? null : named(args.being);
168
- if (args.being !== undefined && being === null) return { error: 'no such being' };
169
- // A method is a name or it is the empty ask. Coerced instead of checked,
170
- // a shape passed where a name belongs becomes a string she never declared,
171
- // and she would answer `unknown ask` to something nobody asked.
172
- if (args.method !== undefined && typeof args.method !== 'string') return { error: 'an ask is named by a word' };
173
- // No being named is the in-process twin of bytes for nobody: the public
174
- // being, asked as nobody, unbound, so a throw of hers is silence. Her
175
- // insides are not a stranger's to read, and a device serving her to
176
- // strangers must hear what a stranger hears and not one word more.
177
- const key = being ?? w.publicKey();
178
- const door = key === null ? undefined : w.doors.get(key);
179
- if (!door || door.key === w.pk) return { error: 'no such being' };
180
- const bound = being !== null;
181
- // The owner is a caller like any other: wanted says what this ask may
182
- // spend, and saying nothing is the ward's default. A wait that ran out is
183
- // late, the same word a being would hear, said as the object an owner hears.
184
- const out = await within(allow(args.wanted as Wanted | undefined).time, answered(door, bound ? { id: OWNER } : {}, args.method, (args.args as JsonObject) ?? {}, bound));
185
- if (out === LATE) return { error: 'late' };
186
- if ('quo' in out) return { error: out.quo };
187
- if (!('object' in out)) return { error: 'silence' };
188
- return out.object;
189
- }
190
- return { error: 'unknown ask' };
191
- }
192
-
193
- // The ward's describe for its owner: its beings, their classes, a digest each,
194
- // as she would describe herself to the owner. A being absent this run is
195
- // listed too, with a null digest and absent true, so the owner can see her
196
- // and take her out.
197
- async function describe(w: OwnerSide): Promise<Json> {
198
- const beings: Record<string, Json> = {};
199
- // Every being is asked at once and each is bounded on her own. One being
200
- // whose describe never settles is one null digest, not a ward that never
201
- // answers its owner: the harbor learns the pk by this very ask at boot, so
202
- // an unbounded wait here is a ward no restart can bring back.
203
- const asked = [...w.doors].filter(([key]) => key !== w.pk);
204
- const digests = await Promise.all(asked.map(async ([, door]) => (await within(DEFAULT.time, seen(door, { id: OWNER })))));
205
- for (const [i, [key, door]] of asked.entries()) {
206
- const d = digests[i];
207
- put(beings, key, { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d === LATE ? null : d });
208
- }
209
- for (const [key, cls] of Object.entries(w.absent())) put(beings, key, { class: cls, public: w.publicKey() === key, digest: null, absent: true });
210
- return {
211
- asks: OWNER_ASKS,
212
- notes: { pk: w.pk, beings },
213
- };
214
- }
package/src/ward/seal.ts DELETED
@@ -1,178 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The seal. What a ward does to bytes on the way out and on the way in.
3
- //
4
- // A being's key is one 32-byte seed: an Ed25519 pair. It signs, it never
5
- // seals. The ward's key is one seed too, giving both an Ed25519 pair to sign
6
- // replies and an X25519 padlock every ask is sealed to. Beings never own a
7
- // padlock: asks are sealed to the ward, replies to the ephemeral key of the
8
- // ask they answer.
9
- //
10
- // ask on the wire box = eph X pk || AESGCM( payload || signature ) sealed to the ward padlock
11
- // payload = JSON { to, by, next, seq, time, hops?, method?, args } signed by `by`
12
- //
13
- // Nothing rides outside the box. `to` names the heir this ask is for, and it
14
- // is inside with everything else: a heir is the one name in a relation that
15
- // never rotates, so on the outside it would be a handle an intermediary could
16
- // hold forever, and the same handle anyone who ever saw the invitation
17
- // already has. What a harbor needs to route is the ward pk, which it carries
18
- // itself. The door opens every ask with its own padlock and reads `to` after,
19
- // so nothing was ever gained by having it in the clear.
20
- // reply on the wire box = eph X pk || AESGCM( reply || signature ) sealed to the ask's eph pk
21
- // reply = JSON { object, seen } | { silence: true } | { quo: word } signed by the ward key
22
- import type { DoorWord, Json, JsonObject } from '../being/types.ts';
23
- import { isDoorWord } from '../being/silence.ts';
24
- import { KEY, SIGNATURE, box, concat, derive, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.ts';
25
- import { cellFault } from './cells.ts';
26
- import { readJson } from './json.ts';
27
-
28
- const utf8 = new TextEncoder();
29
-
30
- export type WardKey = { sign: Uint8Array; padlock: Uint8Array; signPk: Uint8Array; padlockPk: Uint8Array; pk: string };
31
-
32
- // The two labels a ward's keys are derived under. They are here and not in
33
- // arithmetic.ts because a label is a decision and that file is arithmetic.
34
- // `quo-seal` is taken: it is the label the message cipher derives its key and
35
- // nonce under, from an agreement rather than from a seed, and two derivations
36
- // answering to one name is how a kit is read wrong.
37
- const WARD_SIGN = new TextEncoder().encode('quo-ward-sign');
38
- const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
39
-
40
- // The one size in Quo: one mebibyte of bytes each way. It is read before
41
- // anything is opened, so bytes above it are never decrypted, never parsed and
42
- // never allocated against. Refusing them is bytes that said nothing, which is
43
- // already silence, so there is no tenth word and no new door case. Breadth
44
- // costs bytes, so this one number bounds every other breadth on the wire, and
45
- // what a being holds is not the protocol's business. An ask is a message and
46
- // not a file; what is larger is asked for in pieces.
47
- export const SIZE = 1024 * 1024;
48
-
49
- // The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
50
- //
51
- // One seed, two curves, and each secret derived from it under its own label.
52
- // Fed the seed straight, the two scalars differ only because Ed25519 hashes
53
- // what it is given and X25519 clamps it raw, which is an accident of the two
54
- // designs and no separation at all: one secret would be doing two jobs with
55
- // nothing said about it, and a second kit would have to reproduce a
56
- // construction nobody named. HKDF-SHA-256 under a label is the separation
57
- // said out loud, and it is what `quo/vectors/framing.json` pins.
58
- //
59
- // Bytes are key material and text is not. A seed handed in as bytes of the
60
- // key length is taken as it stands, which is what a harbor mints; anything
61
- // else, of any length, is hashed to that length first. So a thirty-two
62
- // character name is a name like any other, and not a key because it happened
63
- // to be the right size.
64
- export async function wardKey(seed: string | Uint8Array): Promise<WardKey> {
65
- const seed32 = typeof seed === 'string' ? await sha256(utf8.encode(seed)) : seed.length === KEY ? seed : await sha256(seed);
66
- const s = await signingPair(await derive(seed32, WARD_SIGN, KEY));
67
- const p = await sealingPair(await derive(seed32, WARD_SEAL, KEY));
68
- return { sign: s.secret, padlock: p.secret, signPk: s.pk, padlockPk: p.pk, pk: hex(s.pk) + hex(p.pk) };
69
- }
70
- export const wardSignPk = (pk: string): Uint8Array => unhex(pk.slice(0, KEY * 2));
71
- export const wardPadlock = (pk: string): Uint8Array => unhex(pk.slice(KEY * 2));
72
- export const isWardPk = (pk: unknown): pk is string => typeof pk === 'string' && /^[0-9a-f]{128}$/.test(pk);
73
- // Thirty-two bytes as hex, which is how this kit writes every single key: a
74
- // heir pk, a heir secret, a being's own key, and a digest, which is the same
75
- // thirty-two bytes of SHA-256. A ward pk is two of these and has its own
76
- // shape above. The two are spelled here and nowhere else, so a door, a
77
- // stance, a harbor and a conformance suite all refuse the same string.
78
- export const isHex = (v: unknown): v is string => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v);
79
-
80
- // A being's relation key: a seed, and the pk it signs as.
81
- export async function beingKey(seed: Uint8Array): Promise<{ seed: string; pk: string }> {
82
- return { seed: hex(seed), pk: hex((await signingPair(seed)).pk) };
83
- }
84
-
85
- // time is the allowance: what this ask may still spend. It rides inside the
86
- // signed body, so a budget caught on the road cannot be widened by whoever
87
- // caught it.
88
- // hops is reserved and nothing sets it. An ask carries no count of doors
89
- // today, so a chain of relays is bounded by time alone. A relay chain that
90
- // ever wants a count needs every door on it to refuse at zero, and a door
91
- // written after the count was invented cannot make the doors before it
92
- // enforce anything. So the field is named and refused here now, while it
93
- // costs two lines: a door of this version already counts as a correct
94
- // participant in a chain nobody has designed yet.
95
- export type AskPayload = { by: string; next: string | null; seq: number; time: number; hops?: number; method?: string; args: JsonObject };
96
- // What is actually sealed: the payload with the heir it is for, or null for
97
- // the public being. `to` is signed with the rest, so it cannot be moved.
98
- type Sealed = AskPayload & { to: string | null };
99
- export type ReplyPayload = { object: Json; seen: string | null } | { silence: true } | { quo: DoorWord };
100
-
101
- // Seal an ask. Returns the bytes and the ephemeral secret the reply will be sealed to.
102
- export async function sealAsk(to: string | null, payload: AskPayload, signer: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{ bytes: Uint8Array; ephemeral: Uint8Array }> {
103
- const body = utf8.encode(JSON.stringify({ to, ...payload } satisfies Sealed));
104
- const inside = concat([body, await sign(body, signer)]);
105
- const { bytes, ephemeral } = await box(inside, padlock, seed);
106
- return { bytes, ephemeral: ephemeral.secret };
107
- }
108
-
109
- // Open an ask at the door. Null when it does not open or does not parse.
110
- // Verification is the door's own step, because only the door knows which key may speak.
111
- export async function openAsk(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<{ to: string | null; payload: AskPayload; body: Uint8Array; signature: Uint8Array; ephemeralPk: Uint8Array } | null> {
112
- try {
113
- if (!(bytes instanceof Uint8Array) || bytes.length < 1 || bytes.length > SIZE) return null;
114
- const inside = await unbox(bytes, padlockSecret);
115
- if (inside.length <= SIGNATURE) return null;
116
- const body = inside.subarray(0, inside.length - SIGNATURE);
117
- const signature = inside.subarray(inside.length - SIGNATURE);
118
- const payload = readJson(body) as Sealed;
119
- // The heir, or null for the public being. Absent is not null: a payload
120
- // that never named one is malformed, like any other missing field.
121
- const to = payload.to;
122
- if (to !== null && !isHex(to)) return null;
123
- if (!isHex(payload.by)) return null;
124
- if (payload.next !== null && !isHex(payload.next)) return null;
125
- if (payload.method !== undefined && typeof payload.method !== 'string') return null; // a name, or the empty ask. never a number, never an object.
126
- if (payload.args !== undefined && (payload.args === null || typeof payload.args !== 'object' || Array.isArray(payload.args))) return null; // args are one object, or absent. a string or a list is not an ask.
127
- if (payload.args !== undefined && cellFault(payload.args, 'args') !== null) return null; // and values all the way down: no key named __proto__, no nesting past the bound
128
- if (!Number.isSafeInteger(payload.seq) || payload.seq < 1) return null; // her count for this relation. one and up, and a whole number.
129
- // The allowance is not read here. It is the door's, D2, and the door is
130
- // its one reader: `spent` in allowance.ts says what a budget is and what
131
- // is left of it, in one sentence, for a payload that opened like any other.
132
- // The hop count, if this ask carries one: a whole number, never below
133
- // zero. Absent is the ordinary ask and always will be.
134
- if (payload.hops !== undefined && (!Number.isSafeInteger(payload.hops) || payload.hops < 0)) return null;
135
- return { to, payload, body, signature, ephemeralPk: bytes.subarray(0, KEY) };
136
- } catch {
137
- return null;
138
- }
139
- }
140
- export const verifyAsk = (a: { body: Uint8Array; signature: Uint8Array }, pk: string): Promise<boolean> => verify(a.body, a.signature, unhex(pk));
141
-
142
- // Seal a reply to the ephemeral pk of the ask it answers, signed by the ward.
143
- export async function sealReply(reply: ReplyPayload, ephemeralPk: Uint8Array, wardSign: Uint8Array, seed: Uint8Array): Promise<Uint8Array> {
144
- const body = utf8.encode(JSON.stringify(reply));
145
- return (await box(concat([body, await sign(body, wardSign)]), ephemeralPk, seed)).bytes;
146
- }
147
-
148
- // Open a reply with the ephemeral secret kept from the ask, and verify it
149
- // came from the ward it was sent to. Null is silence: it did not open, it
150
- // lied, or it is not a reply. Every ward on the wire is somebody else's code,
151
- // so the shape is checked here and nowhere later: a reply is `{ silence }`,
152
- // `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
153
- // object that is a value and seen a digest or null. A field beside a shape's
154
- // own makes it none of the three, since a kit that dropped the field would
155
- // hand her an answer where this one hands her silence.
156
- // Anything else is bytes that said nothing, and her ward never hands her a
157
- // throw or an absent value for what a stranger wrote.
158
- export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, signPk: Uint8Array): Promise<ReplyPayload | null> {
159
- try {
160
- if (!(bytes instanceof Uint8Array) || bytes.length > SIZE) return null;
161
- const inside = await unbox(bytes, ephemeralSecret);
162
- if (inside.length <= SIGNATURE) return null;
163
- const body = inside.subarray(0, inside.length - SIGNATURE);
164
- if (!(await verify(body, inside.subarray(inside.length - SIGNATURE), signPk))) return null;
165
- const reply = readJson(body);
166
- if (reply === null || typeof reply !== 'object' || Array.isArray(reply)) return null;
167
- const r = reply as Record<string, unknown>;
168
- const keys = Object.keys(r).sort().join(',');
169
- if (keys === 'silence') return r.silence === true ? { silence: true } : null;
170
- if (keys === 'quo') return isDoorWord(r.quo) ? { quo: r.quo } : null;
171
- if (keys !== 'object,seen') return null;
172
- if (r.seen !== null && !isHex(r.seen)) return null;
173
- if (cellFault(r.object, 'object') !== null) return null; // values in both directions: nested past the bound is none of the shapes
174
- return { object: r.object as Json, seen: r.seen };
175
- } catch {
176
- return null;
177
- }
178
- }