@onlyne/node-host 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/acquire.d.ts +62 -0
  2. package/dist/acquire.d.ts.map +1 -0
  3. package/dist/acquire.js +146 -0
  4. package/dist/acquire.js.map +1 -0
  5. package/dist/host-text.d.ts +2 -0
  6. package/dist/host-text.d.ts.map +1 -0
  7. package/dist/host-text.js +103 -0
  8. package/dist/host-text.js.map +1 -0
  9. package/dist/i18n.d.ts +9 -0
  10. package/dist/i18n.d.ts.map +1 -0
  11. package/dist/i18n.js +19 -0
  12. package/dist/i18n.js.map +1 -0
  13. package/dist/index.d.ts +29 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +74 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/keys.d.ts +20 -0
  18. package/dist/keys.d.ts.map +1 -0
  19. package/dist/keys.js +144 -0
  20. package/dist/keys.js.map +1 -0
  21. package/dist/log.d.ts +43 -0
  22. package/dist/log.d.ts.map +1 -0
  23. package/dist/log.js +77 -0
  24. package/dist/log.js.map +1 -0
  25. package/dist/network.d.ts +27 -0
  26. package/dist/network.d.ts.map +1 -0
  27. package/dist/network.js +350 -0
  28. package/dist/network.js.map +1 -0
  29. package/dist/node.d.ts +20 -0
  30. package/dist/node.d.ts.map +1 -0
  31. package/dist/node.js +153 -0
  32. package/dist/node.js.map +1 -0
  33. package/dist/store.d.ts +44 -0
  34. package/dist/store.d.ts.map +1 -0
  35. package/dist/store.js +219 -0
  36. package/dist/store.js.map +1 -0
  37. package/dist/tui-text.d.ts +37 -0
  38. package/dist/tui-text.d.ts.map +1 -0
  39. package/dist/tui-text.js +69 -0
  40. package/dist/tui-text.js.map +1 -0
  41. package/dist/tui.d.ts +19 -0
  42. package/dist/tui.d.ts.map +1 -0
  43. package/dist/tui.js +400 -0
  44. package/dist/tui.js.map +1 -0
  45. package/dist/types.d.ts +62 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +2 -0
  48. package/dist/types.js.map +1 -0
  49. package/package.json +46 -10
  50. package/src/i18n.ts +0 -22
  51. package/src/index.ts +0 -82
  52. package/src/keys.ts +0 -158
  53. package/src/log.ts +0 -103
  54. package/src/network.ts +0 -382
  55. package/src/tui-text.ts +0 -73
  56. package/src/tui.ts +0 -329
package/src/tui.ts DELETED
@@ -1,329 +0,0 @@
1
- import { trustPhase, type KeyExchange, type OnlyneNode, type TrustPhase } from '@onlyne/lib';
2
- import type { LogRing } from './log.ts';
3
- import { t, type TuiTextKey } from './tui-text.ts';
4
-
5
- /** One peer as the terminal shows it: a discovery address plus whatever the trust table knows. */
6
- interface Row {
7
- node: string;
8
- endpoint?: string;
9
- phase?: TrustPhase;
10
- fingerprint?: string;
11
- /** Resources this gateway serves whose binding points at this node: where events go now. */
12
- delivery?: string[];
13
- }
14
-
15
- const PHASE_KEY: Record<TrustPhase, TuiTextKey> = {
16
- exchanged: 'phase.exchanged',
17
- requested: 'phase.requested',
18
- invited: 'phase.invited',
19
- revoked: 'phase.revoked',
20
- };
21
-
22
- const NO_PHASE = '—';
23
- const SHORT = (node: string): string => node.split(':').pop() ?? node;
24
-
25
- function failureText(error: unknown): string {
26
- if (error instanceof Error && 'error' in error) {
27
- const inner = (error as { error?: { message?: string } }).error;
28
- if (typeof inner?.message === 'string') return inner.message;
29
- }
30
- return String(error);
31
- }
32
-
33
- type Mode = { kind: 'list' } | { kind: 'confirm'; exchange: KeyExchange } | { kind: 'input' };
34
-
35
- /**
36
- * The gateway's foreground surface: one screen for the peers it discovered, the trust stage
37
- * of each, and the four actions that change one. Every confirmation shows the fingerprint
38
- * first, because that string — not the node id — is what the two operators compare.
39
- */
40
- export class GatewayTui {
41
- #rows: Row[] = [];
42
- #index = 0;
43
- #mode: Mode = { kind: 'list' };
44
- #input = '';
45
- #message = '';
46
- #busy = false;
47
- #closed = false;
48
- #refreshTimer?: ReturnType<typeof setInterval>;
49
- #statusLines: string[] = [];
50
-
51
- constructor(
52
- private readonly node: OnlyneNode,
53
- private readonly ring: LogRing,
54
- private readonly endpoint: string,
55
- private readonly onChange: () => void = () => undefined,
56
- private readonly status?: () => Promise<string[]>,
57
- ) {}
58
-
59
- async start(): Promise<void> {
60
- process.stdout.write('\x1b[?1049h\x1b[?25l');
61
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
62
- process.stdin.resume();
63
- process.stdin.setEncoding('utf8');
64
- process.stdin.on('data', this.#onData);
65
- process.stdout.on('resize', this.#onResize);
66
- await this.#refresh();
67
- this.#refreshTimer = setInterval(() => { void this.#refresh(); }, 1000);
68
- }
69
-
70
- async close(): Promise<void> {
71
- if (this.#closed) return;
72
- this.#closed = true;
73
- clearInterval(this.#refreshTimer);
74
- process.stdin.off('data', this.#onData);
75
- process.stdout.off('resize', this.#onResize);
76
- process.stdin.pause();
77
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
78
- process.stdout.write('\x1b[?25h\x1b[?1049l');
79
- }
80
-
81
- #onResize = (): void => { this.#draw(); };
82
-
83
- #onData = (chunk: string): void => {
84
- if (this.#closed) return;
85
- // One `data` event can carry a whole paste or a burst of keystrokes, so it is split into
86
- // keys rather than treated as one. Each key runs in order; `#busy` still gates actions.
87
- for (const token of tokens(chunk)) void this.#handle(token);
88
- };
89
-
90
- async #handle(token: Token): Promise<void> {
91
- if (this.#busy) return;
92
- if (this.#mode.kind === 'confirm') {
93
- if (token === 'y') await this.#settle(true);
94
- else if (token === 'n' || token === 'esc') { this.#mode = { kind: 'list' }; this.#message = t('notice.cancelled'); this.#draw(); }
95
- return;
96
- }
97
- if (this.#mode.kind === 'input') {
98
- if (token === 'esc') { this.#mode = { kind: 'list' }; this.#input = ''; this.#draw(); return; }
99
- if (token === 'enter') { await this.#connect(); return; }
100
- if (token === 'backspace') { this.#input = this.#input.slice(0, -1); this.#draw(); return; }
101
- if (typeof token === 'string' && token.length === 1 && token >= ' ') { this.#input += token; this.#draw(); }
102
- return;
103
- }
104
- if (token === 'q') { await this.close(); this.onChange(); return; }
105
- if (token === 'up') { this.#move(-1); return; }
106
- if (token === 'down') { this.#move(1); return; }
107
- if (token === 'c') { this.#mode = { kind: 'input' }; this.#input = ''; this.#message = t('notice.enterAddress'); this.#draw(); return; }
108
- const row = this.#rows[this.#index];
109
- if (row === undefined) return;
110
- if (token === 't') { await this.#begin(row); return; }
111
- if (token === 'a') { await this.#accept(row); return; }
112
- if (token === 'r') { await this.#revoke(row); return; }
113
- }
114
-
115
- #move(delta: number): void {
116
- if (this.#rows.length === 0) return;
117
- this.#index = (this.#index + delta + this.#rows.length) % this.#rows.length;
118
- this.#draw();
119
- }
120
-
121
- async #refresh(): Promise<void> {
122
- if (this.#closed) return;
123
- try {
124
- const [trust, records, status] = await Promise.all([
125
- this.node.trust.list(this.node.auth),
126
- this.node.discovery.listRecords(),
127
- this.status?.() ?? [],
128
- ]);
129
- this.#statusLines = status;
130
- const handlers = new Map<string, string[]>();
131
- const resources = await this.node.resources.list(this.node.auth, { page_size: 100 });
132
- for (const resource of resources.items) {
133
- const list = handlers.get(resource.binding.handler) ?? [];
134
- list.push(resource.resource_ref.id);
135
- handlers.set(resource.binding.handler, list);
136
- }
137
- const rows = new Map<string, Row>();
138
- for (const record of records) {
139
- if (record.node_id === this.node.auth.actor) continue;
140
- rows.set(record.node_id, { node: record.node_id, endpoint: record.base_url });
141
- }
142
- for (const entry of trust) {
143
- if (entry.node_id === this.node.auth.actor) continue;
144
- const row = rows.get(entry.node_id) ?? { node: entry.node_id };
145
- rows.set(entry.node_id, { ...row, phase: trustPhase(entry), fingerprint: entry.fingerprint, endpoint: row.endpoint ?? entry.base_url });
146
- }
147
- for (const [handler, ids] of handlers) {
148
- if (handler === this.node.auth.actor) continue;
149
- const row = rows.get(handler) ?? { node: handler };
150
- rows.set(handler, { ...row, delivery: ids });
151
- }
152
- this.#rows = [...rows.values()].sort((a, b) => (a.node < b.node ? -1 : a.node > b.node ? 1 : 0));
153
- if (this.#index >= this.#rows.length) this.#index = Math.max(0, this.#rows.length - 1);
154
- this.#draw();
155
- } catch (error) {
156
- this.#message = t('notice.refreshFailed', { error: failureText(error) });
157
- this.#draw();
158
- }
159
- }
160
-
161
- async #begin(row: Row): Promise<void> {
162
- if (row.phase === 'exchanged') { this.#message = t('notice.alreadyExchanged', { node: SHORT(row.node) }); this.#draw(); return; }
163
- await this.#run(async () => {
164
- const exchange = await this.node.trust.begin(this.node.auth, row.node);
165
- this.#mode = { kind: 'confirm', exchange };
166
- this.#message = '';
167
- });
168
- }
169
-
170
- async #accept(row: Row): Promise<void> {
171
- if (row.phase !== 'invited' || row.fingerprint === undefined) {
172
- this.#message = t('notice.noRequest', { node: SHORT(row.node) });
173
- this.#draw();
174
- return;
175
- }
176
- const fingerprint = row.fingerprint;
177
- await this.#run(async () => {
178
- await this.node.trust.accept(this.node.auth, row.node, fingerprint);
179
- this.#message = t('notice.accepted', { node: SHORT(row.node) });
180
- });
181
- }
182
-
183
- async #revoke(row: Row): Promise<void> {
184
- if (row.phase === undefined) { this.#message = t('notice.noTrust', { node: SHORT(row.node) }); this.#draw(); return; }
185
- await this.#run(async () => {
186
- await this.node.trust.revoke(this.node.auth, row.node);
187
- this.#message = t('notice.revoked', { node: SHORT(row.node) });
188
- });
189
- }
190
-
191
- async #connect(): Promise<void> {
192
- const value = this.#input.trim();
193
- this.#mode = { kind: 'list' };
194
- this.#input = '';
195
- if (value === '') { this.#message = t('notice.noAddress'); this.#draw(); return; }
196
- await this.#run(async () => {
197
- const url = new URL(value.includes('://') ? value : `http://${value}`);
198
- url.pathname = url.pathname.replace(/\/+$/, '') || '/onlyne/v3';
199
- const record = await this.node.discovery.observe(url.href, 'manual');
200
- this.#message = t('notice.connected', { node: record.node_id });
201
- });
202
- }
203
-
204
- async #settle(accept: boolean): Promise<void> {
205
- const mode = this.#mode;
206
- if (mode.kind !== 'confirm') return;
207
- if (!accept) { this.#mode = { kind: 'list' }; this.#message = t('notice.cancelled'); this.#draw(); return; }
208
- this.#mode = { kind: 'list' };
209
- await this.#run(async () => {
210
- const entry = await mode.exchange.confirm(mode.exchange.fingerprint);
211
- this.#message = t('notice.submitted', { node: SHORT(entry.node_id), phase: t(PHASE_KEY[trustPhase(entry)]) });
212
- });
213
- }
214
-
215
- async #run(work: () => Promise<void>): Promise<void> {
216
- this.#busy = true;
217
- this.#draw();
218
- try { await work(); }
219
- catch (error) { this.#message = t('notice.operationFailed', { error: failureText(error) }); }
220
- finally {
221
- this.#busy = false;
222
- await this.#refresh();
223
- }
224
- }
225
-
226
- #draw(): void {
227
- if (this.#closed) return;
228
- // One cell short of the terminal width: writing the last cell auto-wraps, which would
229
- // consume a second row per line and push the frame upward.
230
- const width = Math.max(48, (process.stdout.columns ?? 80) - 1);
231
- const height = Math.max(16, process.stdout.rows ?? 24);
232
- const lines: string[] = [];
233
- const push = (text: string): void => { lines.push(fit(text, width)); };
234
-
235
- push(t('title', { actor: this.node.auth.actor, endpoint: this.endpoint }));
236
- push(rule(width));
237
-
238
- if (this.#statusLines.length > 0) {
239
- const budget = Math.max(3, Math.floor(height / 3));
240
- for (const line of this.#statusLines.slice(0, budget)) push(line);
241
- if (this.#statusLines.length > budget) push('…');
242
- push(rule(width));
243
- }
244
-
245
- const peerHeader = this.#rows.length === 0 ? t('peers.none') : t('peers.count', { count: this.#rows.length });
246
- push(peerHeader);
247
- for (const [index, row] of this.#rows.entries()) {
248
- const marker = index === this.#index ? '▸' : ' ';
249
- const phase = row.phase === undefined ? NO_PHASE : t(PHASE_KEY[row.phase]);
250
- // Delivery is the binding's answer, not the trust stage's: a node can be exchanged and
251
- // still receive nothing, and the handler can be a node with no key exchange at all.
252
- const delivery = row.delivery === undefined ? '' : t('column.delivery');
253
- push(`${marker} ${SHORT(row.node).padEnd(14)} ${phase.padEnd(14)} ${delivery.padEnd(8)} ${row.endpoint ?? ''}`);
254
- }
255
-
256
- push(rule(width));
257
- const selected = this.#rows[this.#index];
258
- if (selected === undefined) {
259
- push(t('hint.idle'));
260
- } else {
261
- push(selected.node);
262
- push(`${t('label.fingerprint').padEnd(6)}${selected.fingerprint ?? t('value.notExchanged')}`);
263
- push(`${t('label.endpoint').padEnd(6)}${selected.endpoint ?? t('value.unknown')}`);
264
- if (selected.delivery !== undefined) push(`${t('label.delivery').padEnd(6)}${selected.delivery.join(t('list.separator'))}`);
265
- }
266
-
267
- push(rule(width));
268
- if (this.#mode.kind === 'confirm') {
269
- const exchange = this.#mode.exchange;
270
- push(t('confirm.title', { node: SHORT(exchange.node_id) }));
271
- push(` ${exchange.fingerprint}`);
272
- push(t('confirm.keys'));
273
- } else if (this.#mode.kind === 'input') {
274
- push(t('input.prompt', { value: this.#input }));
275
- } else {
276
- push(t('help.keys'));
277
- }
278
- if (this.#message !== '') push(this.#message);
279
- if (this.#busy) push('…');
280
-
281
- const logLines = Math.max(0, height - lines.length - 1);
282
- if (logLines > 0) {
283
- push(rule(width));
284
- for (const line of this.ring.tail(logLines)) push(line);
285
- }
286
-
287
- let frame = '\x1b[H';
288
- for (let index = 0; index < height; index++) {
289
- frame += `${lines[index] ?? ''}\x1b[K${index === height - 1 ? '' : '\r\n'}`;
290
- }
291
- frame += '\x1b[J';
292
- process.stdout.write(frame);
293
- }
294
- }
295
-
296
- type Token = 'up' | 'down' | 'enter' | 'esc' | 'backspace' | 'y' | 'n' | 'q' | 't' | 'a' | 'r' | 'c' | string;
297
-
298
- /** Splits one stdin chunk into keys: CSI arrows, a bare escape, and every printable character. */
299
- function tokens(chunk: string): Token[] {
300
- const out: Token[] = [];
301
- let rest = chunk.replace(/\x1b\[20[01]~/g, '');
302
- while (rest.length > 0) {
303
- if (rest.startsWith('\x1b[A')) { out.push('up'); rest = rest.slice(3); continue; }
304
- if (rest.startsWith('\x1b[B')) { out.push('down'); rest = rest.slice(3); continue; }
305
- if (rest.startsWith('\x1b[')) {
306
- // Any other control sequence is not a key this surface uses; drop it whole.
307
- const end = rest.slice(2).search(/[@-~]/);
308
- rest = end < 0 ? '' : rest.slice(end + 3);
309
- continue;
310
- }
311
- if (rest.startsWith('\x1b')) { out.push('esc'); rest = rest.slice(1); continue; }
312
- const character = rest[0]!;
313
- if (character === '\r' || character === '\n') out.push('enter');
314
- else if (character === '\x7f' || character === '\b') out.push('backspace');
315
- else if (character === '\x03') out.push('q');
316
- else if (character >= ' ') out.push(character);
317
- rest = rest.slice(1);
318
- }
319
- return out;
320
- }
321
-
322
- function fit(text: string, width: number): string {
323
- if (text.length <= width) return text;
324
- return `${text.slice(0, width - 1)}…`;
325
- }
326
-
327
- function rule(width: number): string {
328
- return '─'.repeat(width);
329
- }