nervur 0.19.2 → 0.20.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 (235) hide show
  1. package/README.md +156 -104
  2. package/dist/being/being.d.ts +8 -15
  3. package/dist/being/being.js +20 -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 +10 -0
  7. package/dist/being/faculty.js +74 -0
  8. package/dist/being/index.d.ts +5 -5
  9. package/dist/being/index.js +6 -5
  10. package/dist/being/kind.d.ts +4 -0
  11. package/dist/being/kind.js +28 -0
  12. package/dist/being/types.d.ts +35 -63
  13. package/dist/being/types.js +3 -57
  14. package/dist/being/words.d.ts +13 -0
  15. package/dist/being/words.js +14 -0
  16. package/dist/cli/command.d.ts +13 -0
  17. package/dist/cli/command.js +194 -0
  18. package/dist/cli/harbor.d.ts +21 -0
  19. package/dist/cli/harbor.js +141 -0
  20. package/dist/cli/main.d.ts +2 -0
  21. package/dist/cli/main.js +14 -0
  22. package/dist/contract/index.d.ts +39 -0
  23. package/dist/contract/index.js +33 -0
  24. package/dist/crypto/aes.d.ts +3 -0
  25. package/dist/crypto/aes.js +24 -0
  26. package/dist/crypto/bytes.d.ts +6 -0
  27. package/dist/crypto/bytes.js +33 -0
  28. package/dist/crypto/ed25519.d.ts +3 -0
  29. package/dist/crypto/ed25519.js +85 -0
  30. package/dist/crypto/hash.d.ts +2 -0
  31. package/dist/crypto/hash.js +11 -0
  32. package/dist/crypto/index.d.ts +7 -0
  33. package/dist/crypto/index.js +10 -0
  34. package/dist/crypto/json.d.ts +8 -0
  35. package/dist/crypto/json.js +250 -0
  36. package/dist/crypto/mlkem.d.ts +12 -0
  37. package/dist/crypto/mlkem.js +36 -0
  38. package/dist/crypto/subtle.d.ts +10 -0
  39. package/dist/crypto/subtle.js +29 -0
  40. package/dist/crypto/x25519.d.ts +2 -0
  41. package/dist/crypto/x25519.js +21 -0
  42. package/dist/folder/index.d.ts +14 -0
  43. package/dist/folder/index.js +87 -0
  44. package/dist/harbor/catalogue.d.ts +25 -0
  45. package/dist/harbor/catalogue.js +93 -0
  46. package/dist/harbor/dock.d.ts +29 -0
  47. package/dist/harbor/dock.js +122 -0
  48. package/dist/harbor/harbor.d.ts +31 -0
  49. package/dist/harbor/harbor.js +146 -0
  50. package/dist/harbor/index.d.ts +7 -10
  51. package/dist/harbor/index.js +9 -18
  52. package/dist/harbor/package.d.ts +10 -0
  53. package/dist/harbor/package.js +115 -0
  54. package/dist/harbor/registry.d.ts +9 -0
  55. package/dist/harbor/registry.js +78 -0
  56. package/dist/harbor/root-line.d.ts +9 -0
  57. package/dist/harbor/root-line.js +36 -0
  58. package/dist/harbor/terrain.d.ts +9 -0
  59. package/dist/harbor/terrain.js +2 -0
  60. package/dist/index.d.ts +7 -0
  61. package/dist/index.js +14 -0
  62. package/dist/pointer/bodies.d.ts +26 -0
  63. package/dist/pointer/bodies.js +64 -0
  64. package/dist/pointer/index.d.ts +2 -0
  65. package/dist/pointer/index.js +4 -0
  66. package/dist/pointer/world.d.ts +35 -0
  67. package/dist/pointer/world.js +85 -0
  68. package/dist/quo/door.d.ts +34 -0
  69. package/dist/quo/door.js +172 -0
  70. package/dist/quo/index.d.ts +8 -0
  71. package/dist/quo/index.js +11 -0
  72. package/dist/quo/invitation.d.ts +7 -0
  73. package/dist/quo/invitation.js +15 -0
  74. package/dist/quo/keys.d.ts +40 -0
  75. package/dist/quo/keys.js +79 -0
  76. package/dist/quo/payload.d.ts +15 -0
  77. package/dist/quo/payload.js +55 -0
  78. package/dist/quo/relations.d.ts +40 -0
  79. package/dist/quo/relations.js +33 -0
  80. package/dist/quo/reply.d.ts +13 -0
  81. package/dist/quo/reply.js +35 -0
  82. package/dist/quo/seal.d.ts +42 -0
  83. package/dist/quo/seal.js +78 -0
  84. package/dist/quo/standing.d.ts +39 -0
  85. package/dist/quo/standing.js +90 -0
  86. package/dist/tcp/frame.d.ts +29 -0
  87. package/dist/tcp/frame.js +74 -0
  88. package/dist/tcp/index.d.ts +18 -0
  89. package/dist/tcp/index.js +164 -0
  90. package/dist/ward/allowance.d.ts +13 -9
  91. package/dist/ward/allowance.js +28 -67
  92. package/dist/ward/cells.d.ts +6 -6
  93. package/dist/ward/cells.js +67 -179
  94. package/dist/ward/index.d.ts +6 -10
  95. package/dist/ward/index.js +7 -13
  96. package/dist/ward/partition.d.ts +44 -60
  97. package/dist/ward/partition.js +56 -285
  98. package/dist/ward/pilot.d.ts +8 -0
  99. package/dist/ward/pilot.js +50 -0
  100. package/dist/ward/stance.d.ts +27 -20
  101. package/dist/ward/stance.js +174 -406
  102. package/dist/ward/ward-being.d.ts +35 -0
  103. package/dist/ward/ward-being.js +82 -0
  104. package/dist/ward/ward.d.ts +45 -11
  105. package/dist/ward/ward.js +239 -357
  106. package/package.json +24 -31
  107. package/src/being/being.ts +35 -78
  108. package/src/being/digest.ts +7 -35
  109. package/src/being/faculty.ts +73 -0
  110. package/src/being/index.ts +6 -6
  111. package/src/being/kind.ts +30 -0
  112. package/src/being/types.ts +50 -146
  113. package/src/being/words.ts +31 -0
  114. package/src/cli/command.ts +188 -0
  115. package/src/cli/harbor.ts +137 -0
  116. package/src/cli/main.ts +13 -0
  117. package/src/contract/index.ts +73 -0
  118. package/src/crypto/aes.ts +26 -0
  119. package/src/crypto/bytes.ts +37 -0
  120. package/src/crypto/ed25519.ts +84 -0
  121. package/src/crypto/hash.ts +14 -0
  122. package/src/crypto/index.ts +10 -0
  123. package/src/crypto/json.ts +241 -0
  124. package/src/crypto/mlkem.ts +38 -0
  125. package/src/crypto/subtle.ts +33 -0
  126. package/src/crypto/x25519.ts +21 -0
  127. package/src/folder/index.ts +92 -0
  128. package/src/harbor/catalogue.ts +110 -0
  129. package/src/harbor/dock.ts +137 -0
  130. package/src/harbor/harbor.ts +180 -0
  131. package/src/harbor/index.ts +9 -19
  132. package/src/harbor/package.ts +131 -0
  133. package/src/harbor/registry.ts +73 -0
  134. package/src/harbor/root-line.ts +36 -0
  135. package/src/harbor/terrain.ts +15 -0
  136. package/src/index.ts +20 -0
  137. package/src/pointer/bodies.ts +65 -0
  138. package/src/pointer/index.ts +4 -0
  139. package/src/pointer/world.ts +94 -0
  140. package/src/quo/door.ts +178 -0
  141. package/src/quo/index.ts +11 -0
  142. package/src/quo/invitation.ts +15 -0
  143. package/src/quo/keys.ts +91 -0
  144. package/src/quo/payload.ts +61 -0
  145. package/src/quo/relations.ts +62 -0
  146. package/src/quo/reply.ts +38 -0
  147. package/src/quo/seal.ts +101 -0
  148. package/src/quo/standing.ts +111 -0
  149. package/src/stand/main.ts +20 -0
  150. package/src/stand/stand.ts +220 -0
  151. package/src/tcp/frame.ts +78 -0
  152. package/src/tcp/index.ts +173 -0
  153. package/src/ward/allowance.ts +37 -75
  154. package/src/ward/cells.ts +63 -176
  155. package/src/ward/index.ts +7 -17
  156. package/src/ward/partition.ts +86 -326
  157. package/src/ward/pilot.ts +50 -0
  158. package/src/ward/stance.ts +186 -420
  159. package/src/ward/ward-being.ts +103 -0
  160. package/src/ward/ward.ts +256 -359
  161. package/dist/being/lent.d.ts +0 -32
  162. package/dist/being/lent.js +0 -72
  163. package/dist/being/silence.d.ts +0 -12
  164. package/dist/being/silence.js +0 -41
  165. package/dist/conformance/assert.d.ts +0 -11
  166. package/dist/conformance/assert.js +0 -106
  167. package/dist/conformance/beings.d.ts +0 -199
  168. package/dist/conformance/beings.js +0 -188
  169. package/dist/conformance/estate.d.ts +0 -5
  170. package/dist/conformance/estate.js +0 -388
  171. package/dist/conformance/index.d.ts +0 -80
  172. package/dist/conformance/index.js +0 -819
  173. package/dist/conformance/reach.d.ts +0 -10
  174. package/dist/conformance/reach.js +0 -72
  175. package/dist/conformance/store.d.ts +0 -5
  176. package/dist/conformance/store.js +0 -113
  177. package/dist/harbor/box.d.ts +0 -121
  178. package/dist/harbor/box.js +0 -121
  179. package/dist/harbor/core.d.ts +0 -55
  180. package/dist/harbor/core.js +0 -662
  181. package/dist/harbor/dial.d.ts +0 -9
  182. package/dist/harbor/dial.js +0 -81
  183. package/dist/harbor/memory.d.ts +0 -29
  184. package/dist/harbor/memory.js +0 -104
  185. package/dist/harbor/reach.d.ts +0 -36
  186. package/dist/harbor/reach.js +0 -199
  187. package/dist/harbor/store.d.ts +0 -35
  188. package/dist/harbor/store.js +0 -62
  189. package/dist/vector/cases.d.ts +0 -42
  190. package/dist/vector/cases.js +0 -223
  191. package/dist/vector/index.d.ts +0 -6
  192. package/dist/vector/index.js +0 -8
  193. package/dist/vector/stand.d.ts +0 -9
  194. package/dist/vector/stand.js +0 -77
  195. package/dist/vector/world.d.ts +0 -144
  196. package/dist/vector/world.js +0 -209
  197. package/dist/ward/arithmetic.d.ts +0 -31
  198. package/dist/ward/arithmetic.js +0 -249
  199. package/dist/ward/door.d.ts +0 -18
  200. package/dist/ward/door.js +0 -186
  201. package/dist/ward/ground.d.ts +0 -29
  202. package/dist/ward/ground.js +0 -56
  203. package/dist/ward/heirs.d.ts +0 -13
  204. package/dist/ward/heirs.js +0 -117
  205. package/dist/ward/json.d.ts +0 -2
  206. package/dist/ward/json.js +0 -163
  207. package/dist/ward/owner.d.ts +0 -13
  208. package/dist/ward/owner.js +0 -220
  209. package/dist/ward/seal.d.ts +0 -52
  210. package/dist/ward/seal.js +0 -150
  211. package/src/being/lent.ts +0 -72
  212. package/src/being/silence.ts +0 -46
  213. package/src/conformance/assert.ts +0 -100
  214. package/src/conformance/beings.ts +0 -188
  215. package/src/conformance/estate.ts +0 -412
  216. package/src/conformance/index.ts +0 -965
  217. package/src/conformance/reach.ts +0 -83
  218. package/src/conformance/store.ts +0 -125
  219. package/src/harbor/box.ts +0 -131
  220. package/src/harbor/core.ts +0 -699
  221. package/src/harbor/dial.ts +0 -112
  222. package/src/harbor/memory.ts +0 -123
  223. package/src/harbor/reach.ts +0 -221
  224. package/src/harbor/store.ts +0 -91
  225. package/src/vector/cases.ts +0 -257
  226. package/src/vector/index.ts +0 -11
  227. package/src/vector/stand.ts +0 -76
  228. package/src/vector/world.ts +0 -232
  229. package/src/ward/arithmetic.ts +0 -251
  230. package/src/ward/door.ts +0 -186
  231. package/src/ward/ground.ts +0 -142
  232. package/src/ward/heirs.ts +0 -116
  233. package/src/ward/json.ts +0 -144
  234. package/src/ward/owner.ts +0 -214
  235. package/src/ward/seal.ts +0 -178
@@ -1,170 +1,74 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The Being side of Quo, as types. Everything a being author can hold.
3
- // Nothing here knows a runtime, a key, or a wire.
4
- import type { silence } from './silence.ts';
2
+ // Everything a being author holds, as types.
3
+ import type { Json } from '../crypto/index.ts';
4
+ import type { Invitation } from '../quo/index.ts';
5
+ import type { Silence, Word } from './words.ts';
5
6
 
6
- // I-JSON. Everything that crosses an edge.
7
- export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
7
+ export type { Invitation, Json };
8
8
  export type JsonObject = { [key: string]: Json };
9
9
 
10
- // The asker: her own id for the being at the door. {} at a public being.
11
- export type Asker = { id: string } | { id?: undefined };
12
-
13
- // The ids the ward speaks under, and no being may mint. An asker id is hers:
14
- // she chooses the strings, and the ward has no business in her namespace. But
15
- // the ward must sometimes stand at her door itself — its describe asks every
16
- // being under OWNER — and an id it shares with one of her occupants is two
17
- // different parties wearing one name. She could not tell them apart, and the
18
- // ward would shape its describe through an occupant's gate.
19
- //
20
- // So the ward's words are shouted, and refused at the mint. A word can only
21
- // be reserved before anyone has used it: a partition already holding an
22
- // occupant of that name could never have it taken back. PUBLIC guards nothing
23
- // today — a public asker is {} and carries no id at all — and that is exactly
24
- // why it is claimed now, while claiming it is free.
10
+ // Who is at the door: the id she filed the occupant under, `{}` for nobody,
11
+ // and `{ id: OWNER }` for whoever holds the ward's unsealed ask.
12
+ export type Asker = { readonly id: string } | { readonly id?: undefined };
25
13
  export const OWNER = 'OWNER';
26
- export const PUBLIC = 'PUBLIC';
27
- export const RESERVED_IDS: readonly string[] = [OWNER, PUBLIC];
28
14
 
29
- // An invitation is a value. Opaque to her: the far ward's pk, a heir pk,
30
- // and the heir's secret. Without a heir it addresses a ward's public being.
31
- export type Invitation = { ward: string; heir?: string; secret?: string };
15
+ // The ward-being's key in her ward, and so the id every being she makes
16
+ // knows her by.
17
+ export const WARD = 'ward';
32
18
 
33
- // An invitation as one object of values, which is what it already is. A being
34
- // who hands one on sends it as args, and args are values; this is the one
35
- // place that is spelled, so nobody spells it with a cast. Absent stays absent:
36
- // an invitation to a public being carries no heir and no secret, and a key
37
- // present and undefined is not the same object once JSON has been through it.
38
- export const invitationArgs = (inv: Invitation): JsonObject => ({
39
- ward: inv.ward,
40
- ...(inv.heir !== undefined ? { heir: inv.heir } : {}),
41
- ...(inv.secret !== undefined ? { secret: inv.secret } : {}),
42
- });
19
+ // What comes back from an ask, and what a being answers.
20
+ export type Answer = Json | Silence | Word;
21
+ export type Reply = Json | Silence | undefined;
43
22
 
44
- // Whether a value that arrived is an invitation. The shape is the spec's, so
45
- // the reading of it is too, and it is one reading: a form that
46
- // asks a guest for one, a lent being handed one to wake her maker by, and a
47
- // shell that finds one in a link all ask the same question, and a kinder
48
- // answer in one of them is a value that fails at a door instead of at the
49
- // edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
50
- // and a heir comes with its secret or neither comes: an invitation to a
51
- // public being carries no heir, and a heir with no secret opens nothing.
52
- export const isInvitation = (v: unknown): v is Invitation => {
53
- if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
54
- const o = v as Record<string, unknown>;
55
- if (typeof o.ward !== 'string' || !/^[0-9a-f]{128}$/.test(o.ward)) return false;
56
- const heir = typeof o.heir === 'string',
57
- secret = typeof o.secret === 'string';
58
- return (heir && secret) || (!heir && !secret && !('heir' in o) && !('secret' in o));
59
- };
23
+ export type Wanted = { readonly time?: number };
60
24
 
61
- // A blueprint is an MCP tool list plus notes.
25
+ // A blueprint: the asks a being can be asked, as an MCP tool list, and notes.
62
26
  export type Schema = JsonObject;
63
- export type Ask = { name: string; description?: string; input: Schema; output?: Schema };
27
+ export type Ask = { name: string; description?: string; input: Schema };
64
28
  export type Blueprint = { asks: Ask[]; notes: Json };
65
29
 
66
- // Whether what came back from an empty ask is a blueprint. Every describe on
67
- // the far side of a door is somebody else's code, so nothing may be written
68
- // into a standing's record as a blueprint without being read as one first: a
69
- // side walks `asks` by name, and a list of anything else, or no list at all,
70
- // is a side that breaks on a far ward's answer. The asks are checked to the
71
- // depth a side reads them, a name and an input each, and no further: what a
72
- // far being puts beside those is hers.
73
- export const isBlueprint = (v: unknown): v is Blueprint => {
74
- if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
75
- const { asks, notes } = v as { asks?: unknown; notes?: unknown };
76
- if (!Array.isArray(asks) || notes === undefined) return false;
77
- return asks.every((a) => a !== null && typeof a === 'object' && !Array.isArray(a) && typeof (a as Ask).name === 'string' && (a as Ask).input !== null && typeof (a as Ask).input === 'object' && !Array.isArray((a as Ask).input));
78
- };
79
-
80
- export type StandingRecord = { id: string; digest: string | null; blueprint: Blueprint | null; seen: string | null };
81
- export type OccupantRecord = { id: string; notes: JsonObject };
82
- export type Cells = {
83
- standings: Record<string, StandingRecord>;
84
- occupants: Record<string, OccupantRecord>;
85
- [hers: string]: Json;
86
- };
87
-
88
- export type Silence = typeof silence;
89
-
90
- // The ward's words: what her ward says when no object came back and it knows
91
- // why. A door says the first five to a key it has bound, and rides them on
92
- // the wire as `{ quo: word }`; her own ward says the last four to her and
93
- // they never leave the ward. Each is one frozen object under one symbol key,
94
- // so no being makes one by accident and none crosses an edge as a value. A
95
- // being who reaches for the symbol on purpose makes an object her ward reads
96
- // as her having thrown, which is what a being who returns nonsense gets: the
97
- // key is a guard against collision, never against her.
98
- export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
99
- export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
100
- export type WordName = DoorWord | WardWord;
101
- export const WORD_KEY: unique symbol = Symbol.for('quo.word');
102
- export type Word<W extends WordName = WordName> = { readonly [K in typeof WORD_KEY]: W };
103
- export type Unreached = Word<'unreached'>;
104
-
105
- // What comes back from an ask or a knock.
106
- export type Answer = Json | Silence | Word;
107
- // What a being answers.
108
- export type Reply = Json | Silence;
109
-
110
- // What a being may ask for when she wants to say so herself: the time this
111
- // one ask may spend. Optional, and so is saying anything at all: an ask that
112
- // says nothing gets her ward's default, which is the ordinary way to ask.
113
- export type Wanted = { time?: number };
114
-
115
- export type Standing = {
30
+ export interface Standing {
116
31
  readonly id: string;
117
32
  ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
118
- };
33
+ }
119
34
 
120
- export type Standings = {
121
- knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
35
+ // Her side of the relations she holds.
36
+ export interface Standings {
37
+ // Knocks with the empty ask and holds the relation under `id`: the id, or
38
+ // null where the id is taken or reserved, or no object came back.
122
39
  take(id: string, invitation: Invitation): Promise<string | null>;
123
- remove(id: string): void;
124
- } & { readonly [id: string]: Standing | undefined };
40
+ get(id: string): Standing | undefined;
41
+ ids(): string[];
42
+ remove(id: string): boolean;
43
+ }
125
44
 
126
- export type Occupants = {
127
- // awaitable: a key is minted. The notes are the terms it is minted under,
128
- // written on the occupant for her gate to read, and hers alone after that.
45
+ // The door's side: the ids she invited.
46
+ export interface Occupants {
129
47
  invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
48
+ notes(id: string): JsonObject | undefined;
49
+ ids(): string[];
50
+ remove(id: string): boolean;
51
+ }
130
52
 
131
- remove(id: string): void;
132
- };
133
-
134
- // The stance. What every being, in every language, is handed at birth.
135
- export type Stance = {
136
- readonly cells: Cells;
53
+ // What every being is handed at birth.
54
+ export interface Stance {
55
+ readonly key: string;
56
+ readonly cells: JsonObject;
137
57
  readonly occupants: Occupants;
138
58
  readonly standings: Standings;
139
- // A standing at one of the things this device can do, by the harbor's name
140
- // for it, under an id of hers. The id back, or null: this box lends no such
141
- // name, the id already names a record or is a reserved word, or the
142
- // relation was refused.
143
- //
144
- // Her ward asks the ground, then knocks and takes in her name, as it does
145
- // for a being she boots. So the invitation never reaches her, because it is
146
- // the device's capability and not her own relation to give away; what she
147
- // holds afterwards is an ordinary standing that counts, rotates, can be
148
- // removed and hears `removed`.
149
- //
150
- // The ground is the booting harbor's, so this reaches the box she is
151
- // running on and no other. A standing she wakes up holding names a being on
152
- // the box she was on when it was made, and a migration is a restart she
153
- // cannot tell from any other, so a being who wants the box she is on asks
154
- // again at every birth.
155
- lend(name: string, id: string): Promise<string | null>;
156
- // A new being of her ward, by class name, under a key she chooses. The key
157
- // back, or null: the key is taken, the harbor holds no such class, or the
158
- // class threw at birth. A being may make; only the owner reaches into
159
- // another. Naming an id asks for a way back too: the being made holds an
160
- // occupant for her maker under the maker's own key, and the maker holds
161
- // the standing under that id. A relation that could not be made is a boot
162
- // that made nobody, and the being made goes with it.
163
- boot(className: string, key: string, id?: string): Promise<string | null>;
164
- };
59
+ // A new being of her ward, by kind, under a key she chooses. With an id,
60
+ // the new being invites her under her key, and she takes it under id.
61
+ boot(kind: string, key: string, id?: string): Promise<string | null>;
62
+ // A standing under `id` at the faculty this harbor opened for a contract:
63
+ // the id, or null where none is open, the contract declares no kind of
64
+ // its own, or the relation was refused.
65
+ lend(contract: abstract new (...args: never[]) => unknown, id: string): Promise<string | null>;
66
+ }
165
67
 
166
- // The raw shape of a being. Anything with these two is a being.
68
+ // Anything with `answer` is a being.
167
69
  export interface BeingLike {
168
- answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
70
+ answer(asker: Asker, method: string | undefined, args: JsonObject): Reply | Promise<Reply>;
71
+ describe?(asker: Asker): Blueprint;
169
72
  }
170
- export type BeingClass = new (stance: Stance) => BeingLike;
73
+ // A class that stands. Its kind is its own, as `kind.ts` says.
74
+ export type BeingClass = (new (stance: Stance) => BeingLike) & { readonly name: string; readonly kind: string };
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // What a being is told when no object came back. Silence is one symbol.
3
+ // A word is one frozen object per name, so a being never makes one by
4
+ // accident, and two copies of the package in one bundle still agree.
5
+ //
6
+ // removed, unannounced, repeated a far door's words, as Quo says them
7
+ // unreached nothing came back: asking again is safe
8
+ // late the allowance ran out
9
+ // dropped the standing was removed while asking
10
+ // invitation what was handed is no invitation
11
+ import type { Word as DoorWord } from '../quo/index.ts';
12
+
13
+ export const silence: unique symbol = Symbol.for('nervur.silence');
14
+ export type Silence = typeof silence;
15
+ export const isSilence = (x: unknown): x is Silence => x === silence;
16
+
17
+ export type WordName = DoorWord | 'unreached' | 'late' | 'dropped' | 'invitation';
18
+ const KEY: unique symbol = Symbol.for('nervur.word');
19
+ export type Word<W extends WordName = WordName> = { readonly [KEY]: W };
20
+
21
+ const made = new Map<WordName, Word>();
22
+ export const word = <W extends WordName>(name: W): Word<W> => {
23
+ let w = made.get(name);
24
+ if (!w) made.set(name, (w = Object.freeze({ [KEY]: name })));
25
+ return w as Word<W>;
26
+ };
27
+ export const isWord = (x: unknown): x is Word => typeof x === 'object' && x !== null && typeof (x as Record<symbol, unknown>)[KEY] === 'string';
28
+
29
+ // A word by its name, and anything else as it is, so one comparison says
30
+ // which answer came back.
31
+ export const told = (x: unknown): unknown => (isWord(x) ? x[KEY] : x);
@@ -0,0 +1,188 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur`: a harbor on this machine. Each command is one root request, the
3
+ // same the root line carries from any front, printed as one JSON line.
4
+ //
5
+ // nervur init [--dir D] the harbor made: its seed drawn,
6
+ // its catalogue stood, and an owner
7
+ // invitation for whoever made it
8
+ // nervur serve [--dir D] [--host H] [--port P] the harbor served: Quo over TCP,
9
+ // and the root line on its socket
10
+ // nervur module add <file> a module the catalogue runs
11
+ // nervur module remove <module> a module it runs no more
12
+ // nervur modules what the catalogue runs
13
+ // nervur census [--ward W] the empty ask
14
+ // nervur open <key> <kind> a faculty stands in the box ward
15
+ // nervur host <ward> [--seed S] a hosted ward stands
16
+ // nervur route <ward pk> <host:port | none> where a ward is reached
17
+ // nervur boot [--ward W] <key> <kind> a being stands
18
+ // nervur unboot [--ward W] <being> a being goes
19
+ // nervur public [--ward W] <key | none> the being a stranger reaches
20
+ // nervur invite [--ward W] <being> <id> an invitation on a being
21
+ // nervur ask [--ward W] [being] [method] [args] a being, asked as the owner
22
+ // nervur own [--ward W] <id> an invitation for an owner
23
+ // nervur disown [--ward W] <id> an owner no more
24
+ // nervur pilot <name> <invitation> <host:port> a far ward held as its owner, by a
25
+ // pilot being of the box ward
26
+ // nervur <command> --via <name> ... the command asked of the far ward
27
+ // that pilot holds, as its owner
28
+ //
29
+ // D is --dir, then $NERVUR_DIR, then ~/.nervur. A module file is named from
30
+ // where the command runs and kept by its absolute path. No --ward is the box
31
+ // ward, and a pilot holds one ward, so --via takes no --ward. The exit is 1
32
+ // when the answer is an error.
33
+ import { existsSync } from 'node:fs';
34
+ import { homedir } from 'node:os';
35
+ import { join, resolve } from 'node:path';
36
+ import type { JsonObject } from '../being/index.ts';
37
+ import type { RootRequest } from '../harbor/index.ts';
38
+ import { open, request, serve } from './harbor.ts';
39
+
40
+ const USAGE = 'usage: nervur <init|serve|module|modules|census|open|host|route|boot|unboot|public|invite|ask|own|disown|pilot> [args] [--dir D] [--ward W] [--via V] [--seed S] [--host H] [--port P]';
41
+ const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
42
+
43
+ export const parse = (argv: string[]): { command?: string; words: string[]; flags: Record<string, string> } => {
44
+ const flags: Record<string, string> = {};
45
+ const words: string[] = [];
46
+ for (let i = 0; i < argv.length; i += 1) {
47
+ const arg = argv[i]!;
48
+ const name = arg.startsWith('--') ? arg.slice(2) : undefined;
49
+ if (name === undefined) words.push(arg);
50
+ else if (!FLAGS.has(name) || argv[i + 1] === undefined) throw new Error(`--${name} is no flag with a value`);
51
+ else flags[name] = argv[(i += 1)]!;
52
+ }
53
+ const [command, ...rest] = words;
54
+ return { ...(command === undefined ? {} : { command }), words: rest, flags };
55
+ };
56
+
57
+ const none = (word: string): string | null => (word === 'none' ? null : word);
58
+ const json = (word: string | undefined): JsonObject | undefined => (word === undefined ? undefined : (JSON.parse(word) as JsonObject));
59
+ const catalogue = (method: string, args: JsonObject = {}): RootRequest => ({ method: 'ask', args: { being: 'catalogue', method, args } });
60
+
61
+ // The root request a command names, or a throw saying what it lacks.
62
+ export const requestOf = (command: string, words: string[], flags: Record<string, string>): RootRequest => {
63
+ const need = (n: number): string[] => {
64
+ if (words.length !== n) throw new Error(`${command} takes ${n} argument${n === 1 ? '' : 's'}`);
65
+ return words;
66
+ };
67
+ const at = flags.ward === undefined ? {} : { ward: flags.ward };
68
+ switch (command) {
69
+ case 'module': {
70
+ const [verb, what] = need(2);
71
+ if (verb === 'add') return catalogue('add', { from: resolve(what!) });
72
+ if (verb === 'remove') return catalogue('remove', { module: what! });
73
+ throw new Error('module is add or remove');
74
+ }
75
+ case 'modules':
76
+ return (need(0), catalogue('modules'));
77
+ case 'census':
78
+ return (need(0), at);
79
+ case 'open': {
80
+ const [key, kind] = need(2);
81
+ return { method: 'open', args: { key: key!, class: kind! } };
82
+ }
83
+ case 'host': {
84
+ const [ward] = need(1);
85
+ return { method: 'host', args: { ward: ward!, ...(flags.seed === undefined ? {} : { seed: flags.seed }) } };
86
+ }
87
+ case 'route': {
88
+ const [ward, where] = need(2);
89
+ return { method: 'route', args: { ward: ward!, at: none(where!) } };
90
+ }
91
+ case 'boot': {
92
+ const [key, kind] = need(2);
93
+ return { ...at, method: 'boot', args: { key: key!, class: kind! } };
94
+ }
95
+ case 'unboot':
96
+ return { ...at, method: 'unboot', args: { being: need(1)[0]! } };
97
+ case 'public':
98
+ return { ...at, method: 'public', args: { key: none(need(1)[0]!) } };
99
+ case 'invite': {
100
+ const [being, id] = need(2);
101
+ return { ...at, method: 'invite', args: { being: being!, id: id! } };
102
+ }
103
+ case 'ask': {
104
+ if (words.length > 3) throw new Error('ask takes at most a being, a method and its args');
105
+ const [being, method, args] = words;
106
+ return { ...at, method: 'ask', args: { ...(being === undefined ? {} : { being }), ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args: json(args)! }) } };
107
+ }
108
+ case 'own':
109
+ case 'disown':
110
+ return { ...at, method: command, args: { id: need(1)[0]! } };
111
+ default:
112
+ throw new Error(USAGE);
113
+ }
114
+ };
115
+
116
+ // An error answer at any depth the root line or an ask carries it.
117
+ const failed = (answer: unknown): boolean => {
118
+ if (typeof answer !== 'object' || answer === null) return false;
119
+ const a = answer as JsonObject;
120
+ return 'error' in a || failed(a.answer);
121
+ };
122
+
123
+ export const main = async (argv: string[], env: Record<string, string | undefined>, print: (line: string) => void): Promise<number> => {
124
+ const { command, words, flags } = parse(argv);
125
+ if (command === undefined) throw new Error(USAGE);
126
+ const dir = flags.dir ?? env.NERVUR_DIR ?? join(homedir(), '.nervur');
127
+ const out = (answer: JsonObject): number => {
128
+ print(JSON.stringify(answer));
129
+ return failed(answer) ? 1 : 0;
130
+ };
131
+ if (command === 'init') {
132
+ if (words.length > 0) throw new Error('init takes no argument');
133
+ if (existsSync(join(dir, 'seed'))) return out({ error: `a harbor stands at ${dir}` });
134
+ const { harbor, carrier } = await open(dir);
135
+ try {
136
+ const owner = await harbor.root('own', { id: 'owner' });
137
+ return out({ pk: harbor.box.pk, owner: owner as JsonObject });
138
+ } finally {
139
+ await carrier.close();
140
+ }
141
+ }
142
+ if (command === 'serve') {
143
+ if (words.length > 0) throw new Error('serve takes no argument');
144
+ const served = await serve(dir, flags.host, flags.port === undefined ? undefined : Number(flags.port));
145
+ const stop = (): void => void served.close().then(() => process.exit(0));
146
+ process.once('SIGINT', stop);
147
+ process.once('SIGTERM', stop);
148
+ print(JSON.stringify({ pk: served.pk, at: served.at, root: served.root }));
149
+ return SERVING;
150
+ }
151
+ if (command === 'pilot') {
152
+ if (words.length !== 3) throw new Error('pilot takes 3 arguments');
153
+ const [name, invitation, at] = words as [string, string, string];
154
+ const held = JSON.parse(invitation) as JsonObject;
155
+ for (const req of pilotRequests(name, held, at)) {
156
+ const answer = await request(dir, req);
157
+ if (!failed(answer)) continue;
158
+ if (req.method === 'ask') await request(dir, { method: 'unboot', args: { being: name } });
159
+ return out(answer);
160
+ }
161
+ return out({ piloting: name, ward: held.ward! });
162
+ }
163
+ if (flags.via === undefined) return out(await request(dir, requestOf(command, words, flags)));
164
+ if (flags.ward !== undefined) throw new Error('--via takes no --ward');
165
+ return out(farAnswer(await request(dir, via(flags.via, requestOf(command, words, flags)))));
166
+ };
167
+
168
+ // The root requests that hold a far ward: its route, the pilot, the take.
169
+ export const pilotRequests = (name: string, invitation: JsonObject, at: string): RootRequest[] => [
170
+ { method: 'route', args: { ward: invitation.ward ?? null, at } },
171
+ { method: 'boot', args: { key: name, class: 'org.nervur.pilot' } },
172
+ { method: 'ask', args: { being: name, method: 'hold', args: { invitation } } },
173
+ ];
174
+
175
+ // A root request, asked of the far ward a pilot holds.
176
+ export const via = (pilot: string, far: RootRequest): RootRequest => ({
177
+ method: 'ask',
178
+ args: { being: pilot, method: 'pilot', args: { ...(far.method === undefined ? {} : { method: far.method }), ...(far.args === undefined ? {} : { args: far.args }) } },
179
+ });
180
+
181
+ // The far ward-being's answer out of the pilot's, or the local error.
182
+ export const farAnswer = (local: JsonObject): JsonObject => {
183
+ const inner = (local.answer as JsonObject | undefined)?.answer;
184
+ return typeof inner === 'object' && inner !== null && !Array.isArray(inner) ? inner : local;
185
+ };
186
+
187
+ // What `main` answers for a harbor it keeps serving: no exit yet.
188
+ export const SERVING = -1;
@@ -0,0 +1,137 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A harbor on this machine, as the `nervur` command stands it: its seed and
3
+ // sealed memory in a folder, its code imported from the files its
4
+ // catalogue names, Quo over TCP as its carrier, and the root line on a
5
+ // local socket beside them.
6
+ //
7
+ // <dir>/seed the harbor's seed
8
+ // <dir>/<place>/ its sealed memory
9
+ // <dir>/root.sock the root line while the harbor is served, open to
10
+ // this user alone, or in the temporary folder for a
11
+ // folder too deep for a socket path
12
+ //
13
+ // A root request goes to a served harbor over its socket. With none served,
14
+ // the command unpacks the harbor from its folder, asks, and closes it, so
15
+ // one process at a time writes the folder.
16
+ import { createHash } from 'node:crypto';
17
+ import { chmod, rm } from 'node:fs/promises';
18
+ import { connect, createServer, type Server } from 'node:net';
19
+ import { tmpdir } from 'node:os';
20
+ import { join, resolve } from 'node:path';
21
+ import { pathToFileURL } from 'node:url';
22
+ import type { JsonObject } from '../being/index.ts';
23
+ import { Loader, type Loaded } from '../contract/index.ts';
24
+ import { FolderCustody, FolderMemory } from '../folder/index.ts';
25
+ import { Harbor, rootLine, type RootRequest } from '../harbor/index.ts';
26
+ import { CryptoEntropy, PointerTerrain } from '../pointer/index.ts';
27
+ import { TcpCarrier, TcpListener } from '../tcp/index.ts';
28
+
29
+ // A socket path has a hundred bytes or so on every system, so a folder too
30
+ // deep for one puts its socket in this user's temporary folder, under a
31
+ // name drawn from the folder's own path.
32
+ const SOCKET_PATH = 100;
33
+ export const socketOf = (dir: string): string => {
34
+ const beside = join(resolve(dir), 'root.sock');
35
+ if (Buffer.byteLength(beside) <= SOCKET_PATH) return beside;
36
+ return join(tmpdir(), `nervur-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 32)}.sock`);
37
+ };
38
+
39
+ // Code from a file: a module that exports `module`, `version` and
40
+ // `classes`, found again by its absolute path. A file that cannot be
41
+ // imported, or exports no module, is none.
42
+ export class ImportLoader extends Loader {
43
+ async load(from: string): Promise<Loaded | null> {
44
+ const path = resolve(from);
45
+ let exported: { module?: unknown; version?: unknown; classes?: unknown };
46
+ try {
47
+ exported = (await import(pathToFileURL(path).href)) as typeof exported;
48
+ } catch {
49
+ return null;
50
+ }
51
+ const { module, version, classes } = exported;
52
+ if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes)) return null;
53
+ return { module, version, classes, from: path };
54
+ }
55
+ }
56
+
57
+ export type Opened = { harbor: Harbor; carrier: TcpCarrier };
58
+
59
+ export const open = async (dir: string): Promise<Opened> => {
60
+ const entropy = new CryptoEntropy();
61
+ const carrier = new TcpCarrier();
62
+ const harbor = await Harbor.open(new PointerTerrain({ entropy, carrier, loader: new ImportLoader(), custody: new FolderCustody(dir, entropy), memory: new FolderMemory(dir) }));
63
+ return { harbor, carrier };
64
+ };
65
+
66
+ // One root request over a served harbor's socket, or undefined where none
67
+ // is served.
68
+ const overSocket = (dir: string, request: RootRequest): Promise<JsonObject | undefined> =>
69
+ new Promise((done, fail) => {
70
+ const socket = connect(socketOf(dir));
71
+ let text = '';
72
+ socket.once('connect', () => socket.write(`${JSON.stringify(request)}\n`));
73
+ socket.on('data', (chunk: Buffer) => {
74
+ text += chunk.toString('utf8');
75
+ const end = text.indexOf('\n');
76
+ if (end < 0) return;
77
+ socket.end();
78
+ done(JSON.parse(text.slice(0, end)) as JsonObject);
79
+ });
80
+ socket.once('error', (e: NodeJS.ErrnoException) => (e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? done(undefined) : fail(e)));
81
+ });
82
+
83
+ export const request = async (dir: string, req: RootRequest): Promise<JsonObject> => {
84
+ const served = await overSocket(dir, req);
85
+ if (served !== undefined) return served;
86
+ const { harbor, carrier } = await open(dir);
87
+ try {
88
+ return await rootLine(harbor, req);
89
+ } finally {
90
+ await carrier.close();
91
+ }
92
+ };
93
+
94
+ export type Served = { pk: string; at: string; root: string; close(): Promise<void> };
95
+
96
+ // The harbor served: Quo over TCP on host and port, the root line on the
97
+ // socket. A socket file nobody answers on is a stale one, and is replaced.
98
+ export const serve = async (dir: string, host?: string, port?: number): Promise<Served> => {
99
+ if ((await overSocket(dir, {})) !== undefined) throw new Error(`a harbor is already served from ${dir}`);
100
+ const { harbor, carrier } = await open(dir);
101
+ const listener = await TcpListener.listen(harbor, host, port);
102
+ const root = socketOf(dir);
103
+ await rm(root, { force: true });
104
+ const server: Server = createServer((socket) => {
105
+ let text = '';
106
+ socket.on('data', (chunk: Buffer) => {
107
+ text += chunk.toString('utf8');
108
+ for (let end = text.indexOf('\n'); end >= 0; end = text.indexOf('\n')) {
109
+ const line = text.slice(0, end);
110
+ text = text.slice(end + 1);
111
+ let value: unknown;
112
+ try {
113
+ value = JSON.parse(line);
114
+ } catch {
115
+ value = undefined;
116
+ }
117
+ void rootLine(harbor, value)
118
+ .catch(() => ({ error: 'the harbor threw' }))
119
+ .then((answer) => socket.write(`${JSON.stringify(answer)}\n`));
120
+ }
121
+ });
122
+ socket.on('error', () => socket.destroy());
123
+ });
124
+ await new Promise<void>((done) => server.listen(root, done));
125
+ await chmod(root, 0o600);
126
+ return {
127
+ pk: harbor.box.pk,
128
+ at: listener.at,
129
+ root,
130
+ close: async () => {
131
+ await new Promise((done) => server.close(done));
132
+ await rm(root, { force: true });
133
+ await listener.close();
134
+ await carrier.close();
135
+ },
136
+ };
137
+ };
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // The `nervur` program: `command.ts` over this process's arguments. A
4
+ // command that cannot run exits 2 and says why on stderr.
5
+ import { main, SERVING } from './command.ts';
6
+
7
+ try {
8
+ const code = await main(process.argv.slice(2), process.env, (line) => process.stdout.write(`${line}\n`));
9
+ if (code !== SERVING) process.exitCode = code;
10
+ } catch (e) {
11
+ process.stderr.write(`nervur: ${(e as Error).message}\n`);
12
+ process.exitCode = 2;
13
+ }