nervur 0.18.0 → 0.19.1

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