@nervur-org/dock 0.5.0 → 0.6.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.
- package/README.md +7 -4
- package/api/route.ts +4 -72
- package/beings/avatar.ts +11 -7
- package/beings/carry.ts +129 -116
- package/beings/clock.ts +11 -7
- package/beings/desk.ts +7 -6
- package/beings/doorbell.ts +36 -7
- package/beings/index.ts +17 -4
- package/beings/join.ts +7 -4
- package/beings/link.ts +1 -1
- package/beings/porter.ts +5 -1
- package/beings/presence.ts +15 -5
- package/beings/setup.ts +27 -21
- package/beings/user.ts +26 -17
- package/beings/way.ts +1 -1
- package/{dock → box}/agents.ts +37 -44
- package/{dock → box}/apns.ts +4 -4
- package/box/box.ts +31 -0
- package/box/contract.ts +41 -0
- package/box/dialers.ts +88 -0
- package/box/dock.ts +148 -0
- package/box/envoy.ts +96 -0
- package/box/faculty.ts +111 -0
- package/{dock → box}/fcm.ts +1 -1
- package/box/grants.ts +90 -0
- package/box/http.ts +93 -0
- package/{dock → box}/http2.ts +7 -7
- package/box/index.ts +201 -0
- package/box/join.ts +78 -0
- package/box/local.ts +37 -0
- package/{dock → box}/location.ts +15 -3
- package/{dock → box}/notifications.ts +17 -5
- package/box/push.ts +85 -0
- package/box/ring.ts +85 -0
- package/{dock → box}/service.ts +2 -2
- package/box/timer.ts +150 -0
- package/box/tree.ts +22 -0
- package/{dock → box}/webpush.ts +2 -2
- package/cli/client.ts +1 -1
- package/cli/daemon.ts +101 -103
- package/cli/estate.ts +2 -3
- package/cli/nervur.ts +44 -25
- package/cli/stand.ts +1 -1
- package/dist/api/route.d.ts +1 -2
- package/dist/api/route.js +4 -76
- package/dist/beings/avatar.d.ts +4 -3
- package/dist/beings/avatar.js +9 -6
- package/dist/beings/carry.d.ts +10 -12
- package/dist/beings/carry.js +119 -127
- package/dist/beings/clock.js +10 -7
- package/dist/beings/desk.js +8 -7
- package/dist/beings/doorbell.d.ts +3 -1
- package/dist/beings/doorbell.js +37 -4
- package/dist/beings/index.d.ts +24 -4
- package/dist/beings/index.js +15 -4
- package/dist/beings/join.d.ts +1 -0
- package/dist/beings/join.js +6 -4
- package/dist/beings/link.d.ts +0 -1
- package/dist/beings/link.js +1 -1
- package/dist/beings/porter.js +5 -1
- package/dist/beings/presence.d.ts +1 -0
- package/dist/beings/presence.js +12 -3
- package/dist/beings/setup.d.ts +5 -2
- package/dist/beings/setup.js +16 -15
- package/dist/beings/user.d.ts +4 -3
- package/dist/beings/user.js +18 -11
- package/dist/{dock → box}/agents.d.ts +7 -4
- package/dist/{dock → box}/agents.js +30 -37
- package/dist/{dock → box}/apns.d.ts +1 -1
- package/dist/{dock → box}/apns.js +3 -3
- package/dist/box/box.d.ts +9 -0
- package/dist/box/box.js +18 -0
- package/dist/box/contract.d.ts +6 -0
- package/dist/box/contract.js +35 -0
- package/dist/{dock → box}/dialers.d.ts +7 -4
- package/dist/box/dialers.js +76 -0
- package/dist/box/dock.d.ts +182 -0
- package/dist/box/dock.js +128 -0
- package/dist/box/envoy.d.ts +44 -0
- package/dist/box/envoy.js +75 -0
- package/dist/box/faculty.d.ts +68 -0
- package/dist/box/faculty.js +97 -0
- package/dist/{dock → box}/fcm.d.ts +1 -1
- package/dist/{dock → box}/grants.d.ts +4 -3
- package/dist/box/grants.js +78 -0
- package/dist/box/http.d.ts +26 -0
- package/dist/box/http.js +89 -0
- package/dist/{dock → box}/http2.d.ts +1 -1
- package/dist/{dock → box}/http2.js +6 -6
- package/dist/box/index.d.ts +46 -0
- package/dist/box/index.js +179 -0
- package/dist/box/join.d.ts +19 -0
- package/dist/box/join.js +67 -0
- package/dist/{dock → box}/local.d.ts +3 -2
- package/dist/box/local.js +27 -0
- package/dist/{dock → box}/location.d.ts +3 -0
- package/dist/box/location.js +21 -0
- package/dist/{dock → box}/notifications.d.ts +3 -0
- package/dist/{dock → box}/notifications.js +14 -4
- package/dist/{dock → box}/push.d.ts +12 -18
- package/dist/box/push.js +83 -0
- package/dist/box/ring.d.ts +18 -0
- package/dist/box/ring.js +55 -0
- package/dist/{dock → box}/service.d.ts +1 -1
- package/dist/{dock → box}/service.js +1 -1
- package/dist/{dock → box}/timer.d.ts +2 -0
- package/dist/box/timer.js +127 -0
- package/dist/box/tree.d.ts +5 -0
- package/dist/box/tree.js +14 -0
- package/dist/{dock → box}/webpush.d.ts +1 -1
- package/dist/cli/client.d.ts +1 -0
- package/dist/cli/daemon.d.ts +2 -8
- package/dist/cli/daemon.js +102 -107
- package/dist/cli/estate.js +2 -3
- package/dist/cli/nervur.js +44 -24
- package/dist/cli/stand.d.ts +0 -14
- package/dist/cli/stand.js +1 -1
- package/dist/harbor/browser.d.ts +8 -5
- package/dist/harbor/browser.js +20 -21
- package/dist/harbor/capacitor.d.ts +3 -3
- package/dist/harbor/capacitor.js +9 -8
- package/dist/harbor/disk.d.ts +9 -9
- package/dist/harbor/disk.js +56 -51
- package/dist/harbor/edge/alarm.d.ts +4 -2
- package/dist/harbor/edge/alarm.js +9 -19
- package/dist/harbor/edge/edge.d.ts +7 -6
- package/dist/harbor/edge/edge.js +58 -53
- package/dist/harbor/edge/exercise.js +4 -4
- package/dist/harbor/edge/given.d.ts +0 -1
- package/dist/harbor/edge/given.js +0 -29
- package/dist/harbor/edge/storage.d.ts +0 -2
- package/dist/harbor/edge/storage.js +2 -2
- package/dist/harbor/http.js +4 -2
- package/dist/harbor/sealed.d.ts +0 -5
- package/dist/harbor/sealed.js +0 -39
- package/dist/harbor/tauri.d.ts +2 -2
- package/dist/harbor/tauri.js +13 -10
- package/dist/human/app.d.ts +1 -1
- package/dist/human/app.js +10 -12
- package/dist/human/door.d.ts +0 -1
- package/dist/human/door.js +1 -1
- package/dist/human/html.js +1 -1
- package/dist/human/move.js +1 -1
- package/dist/human/tab.bundle.txt +3629 -2824
- package/dist/human/wake.d.ts +5 -2
- package/dist/human/wake.js +23 -14
- package/dist/human/web.d.ts +0 -1
- package/dist/human/web.js +3 -3
- package/dist/human/worker.bundle.txt +2945 -2118
- package/dist/human/worlds.js +3 -3
- package/dist/mcp/agent.d.ts +0 -1
- package/dist/mcp/agent.js +1 -1
- package/dist/mcp/allow.js +3 -2
- package/dist/mcp/http.d.ts +0 -1
- package/dist/mcp/http.js +1 -1
- package/dist/mcp/oauth.d.ts +3 -4
- package/dist/mcp/oauth.js +4 -4
- package/dist/mcp/route.d.ts +1 -1
- package/dist/mcp/runner.d.ts +0 -1
- package/dist/mcp/runner.js +1 -1
- package/dist/mcp/server.d.ts +0 -1
- package/dist/mcp/server.js +1 -1
- package/harbor/browser.ts +22 -24
- package/harbor/capacitor.ts +9 -8
- package/harbor/disk.ts +53 -48
- package/harbor/edge/alarm.ts +15 -12
- package/harbor/edge/edge.ts +61 -55
- package/harbor/edge/exercise.ts +4 -4
- package/harbor/edge/given.ts +3 -33
- package/harbor/edge/storage.ts +2 -2
- package/harbor/http.ts +5 -3
- package/harbor/sealed.ts +0 -35
- package/harbor/tauri.ts +13 -10
- package/human/app.ts +12 -13
- package/human/door.ts +1 -1
- package/human/html.ts +1 -1
- package/human/local.ts +1 -1
- package/human/move.ts +1 -1
- package/human/tab.bundle.txt +3629 -2824
- package/human/wake.ts +36 -16
- package/human/web.ts +4 -4
- package/human/worker.bundle.txt +2945 -2118
- package/human/worlds.ts +3 -3
- package/mcp/agent.ts +1 -1
- package/mcp/allow.ts +2 -1
- package/mcp/http.ts +1 -1
- package/mcp/oauth.ts +4 -4
- package/mcp/route.ts +1 -1
- package/mcp/runner.ts +1 -1
- package/mcp/server.ts +1 -1
- package/package.json +9 -8
- package/beings/envoy.ts +0 -218
- package/dist/beings/envoy.d.ts +0 -54
- package/dist/beings/envoy.js +0 -211
- package/dist/dock/console.d.ts +0 -6
- package/dist/dock/console.js +0 -20
- package/dist/dock/dialers.js +0 -75
- package/dist/dock/faculty.d.ts +0 -24
- package/dist/dock/faculty.js +0 -142
- package/dist/dock/grants.js +0 -99
- package/dist/dock/index.d.ts +0 -41
- package/dist/dock/index.js +0 -113
- package/dist/dock/local.js +0 -44
- package/dist/dock/location.js +0 -11
- package/dist/dock/push.js +0 -132
- package/dist/dock/timer.js +0 -149
- package/dock/console.ts +0 -23
- package/dock/dialers.ts +0 -85
- package/dock/faculty.ts +0 -148
- package/dock/grants.ts +0 -111
- package/dock/index.ts +0 -133
- package/dock/local.ts +0 -48
- package/dock/push.ts +0 -161
- package/dock/timer.ts +0 -174
- /package/dist/{dock → box}/fcm.js +0 -0
- /package/dist/{dock → box}/webpush.js +0 -0
package/box/timer.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The box's clock, and the one being on a device that reads one. A being who
|
|
3
|
+
// could read a clock could not be moved, since the moment she read would be
|
|
4
|
+
// her box's and no other, so the moment reaches every other being the one way
|
|
5
|
+
// anything reaches a being: as an ask at her door, from this faculty, as the
|
|
6
|
+
// occupant she named when she asked to be woken.
|
|
7
|
+
//
|
|
8
|
+
// She arms for the next due moment and for nothing else. Every watcher
|
|
9
|
+
// answers a round with when she is next due, this arms for the earliest of
|
|
10
|
+
// them, and a box whose watchers all say null arms nothing. A round is armed
|
|
11
|
+
// for one moment at a time and never overlaps the one before, and the moment
|
|
12
|
+
// is floored to the minute with half a second after it, since a watcher
|
|
13
|
+
// floors the moment she is handed.
|
|
14
|
+
//
|
|
15
|
+
// One method knows a terrain, `arm`, and a subclass replaces it: a
|
|
16
|
+
// `setTimeout` here, the object's storage alarm on a worker. Her moment is
|
|
17
|
+
// the box's clock.
|
|
18
|
+
import type { JsonObject } from 'nervur';
|
|
19
|
+
import { contract } from './contract.ts';
|
|
20
|
+
import { Faculty, WATCH, type Woke } from './faculty.ts';
|
|
21
|
+
|
|
22
|
+
const ASKS = {
|
|
23
|
+
watch: WATCH,
|
|
24
|
+
due: { description: 'bring my next round forward to this moment', input: { type: 'object', properties: { at: { type: 'number' } }, required: ['at'] } },
|
|
25
|
+
now: { description: 'this box’s moment', input: { type: 'object' } },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// What a being needs from a box that can wake her at a moment.
|
|
29
|
+
export const TIMER = contract('timer', ASKS);
|
|
30
|
+
|
|
31
|
+
const MINUTE = 60_000;
|
|
32
|
+
// A timeout further out than this is not trusted, so the arming is capped and
|
|
33
|
+
// made again when it goes off.
|
|
34
|
+
const DAY = 86_400_000;
|
|
35
|
+
|
|
36
|
+
export class Timer extends Faculty {
|
|
37
|
+
static override asks = ASKS;
|
|
38
|
+
static override fulfils = [TIMER];
|
|
39
|
+
|
|
40
|
+
#timeout: ReturnType<typeof setTimeout> | undefined;
|
|
41
|
+
#stopped = false;
|
|
42
|
+
#armed: number | null = null;
|
|
43
|
+
#round: Promise<Woke[]> | null = null;
|
|
44
|
+
|
|
45
|
+
// The moment she is armed for, or null while she is asleep.
|
|
46
|
+
get armed(): number | null {
|
|
47
|
+
return this.#armed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A watcher says she is due sooner than the box knew. It only ever brings
|
|
51
|
+
// the round forward.
|
|
52
|
+
due(args: JsonObject): JsonObject {
|
|
53
|
+
const at = args.at;
|
|
54
|
+
if (typeof at !== 'number' || !Number.isFinite(at)) return { error: 'a due carries the moment: { at }' };
|
|
55
|
+
const when = moment(at);
|
|
56
|
+
if (this.#armed === null || when < this.#armed) this.#arm(when);
|
|
57
|
+
return { armed: this.#armed };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Whoever was just filed is ticked at once, and what she answers is what
|
|
61
|
+
// the box arms for, so she is never waiting for a round nobody scheduled.
|
|
62
|
+
protected override async welcomed(id: string): Promise<void> {
|
|
63
|
+
await this.round(this.box.now(), id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The arming went off. A moment still well ahead is the day's cap, so it is
|
|
67
|
+
// armed again rather than fired.
|
|
68
|
+
async rang(): Promise<Woke[]> {
|
|
69
|
+
if (this.#stopped) return [];
|
|
70
|
+
if (this.#armed !== null && this.#armed - this.box.now() > 1_000) {
|
|
71
|
+
this.arm(this.#armed);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return this.fire(this.box.now());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The device's own call at a moment it names: how a suite proves a wake
|
|
78
|
+
// without waiting for one and how an operator makes a round happen now.
|
|
79
|
+
fire(now: number): Promise<Woke[]> {
|
|
80
|
+
return this.round(now);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// One round, and never two at once.
|
|
84
|
+
protected round(now: number, only?: string): Promise<Woke[]> {
|
|
85
|
+
if (this.#round) return this.#round;
|
|
86
|
+
const mine = this.#run(now, only);
|
|
87
|
+
this.#round = mine;
|
|
88
|
+
return mine.finally(() => {
|
|
89
|
+
if (this.#round === mine) this.#round = null;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async #run(now: number, only?: string): Promise<Woke[]> {
|
|
94
|
+
if (only === undefined) this.#arm(null);
|
|
95
|
+
const woke = await this.woke((w) => ({ ...w.args, now }), only);
|
|
96
|
+
if (woke === null) {
|
|
97
|
+
this.stop();
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
// A moment already gone is no arming.
|
|
101
|
+
const named = soonest(woke);
|
|
102
|
+
const said = named !== null && named > this.box.now() ? named : null;
|
|
103
|
+
const held = this.#armed;
|
|
104
|
+
const at = said === null ? held : held === null ? moment(said) : Math.min(held, moment(said));
|
|
105
|
+
if (at !== null && at !== this.#armed) this.#arm(at);
|
|
106
|
+
else if (at === null && this.#armed !== null) this.#arm(null);
|
|
107
|
+
return woke;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#arm(when: number | null): void {
|
|
111
|
+
if (this.#stopped) return;
|
|
112
|
+
this.#armed = when;
|
|
113
|
+
this.arm(when);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The one thing a terrain says about itself: be woken at this moment, or at
|
|
117
|
+
// no moment at all. Here a timeout, capped at a day, unreferenced so the box
|
|
118
|
+
// stays up for its work and never for this.
|
|
119
|
+
protected arm(when: number | null): void {
|
|
120
|
+
clearTimeout(this.#timeout);
|
|
121
|
+
this.#timeout = undefined;
|
|
122
|
+
if (when === null) return;
|
|
123
|
+
this.#timeout = setTimeout(() => void this.rang(), Math.min(Math.max(when - this.box.now(), 0), DAY));
|
|
124
|
+
this.#timeout.unref?.();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The device puts the box down.
|
|
128
|
+
stop(): void {
|
|
129
|
+
this.#stopped = true;
|
|
130
|
+
this.#armed = null;
|
|
131
|
+
this.arm(null);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
now(): JsonObject {
|
|
135
|
+
return { now: this.box.now() };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const moment = (at: number): number => Math.max(at, Math.floor(at / MINUTE) * MINUTE + 500);
|
|
140
|
+
|
|
141
|
+
function soonest(woke: Woke[]): number | null {
|
|
142
|
+
let at: number | null = null;
|
|
143
|
+
for (const w of woke) {
|
|
144
|
+
if (w.out === null || typeof w.out !== 'object') continue;
|
|
145
|
+
const next = (w.out as { next?: unknown }).next;
|
|
146
|
+
if (typeof next !== 'number' || !Number.isFinite(next)) continue;
|
|
147
|
+
if (at === null || next < at) at = next;
|
|
148
|
+
}
|
|
149
|
+
return at;
|
|
150
|
+
}
|
package/box/tree.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Classes as a harbor is handed them: a tree whose branches are folders and
|
|
3
|
+
// whose leaves are exports, the way a repository holds them. A module
|
|
4
|
+
// namespace is a branch, so `{ box: await import('./box/index.ts') }`
|
|
5
|
+
// is a tree, and so is a bundle's nested exports. The repository is a
|
|
6
|
+
// harbor's DNA; the cells are its memory.
|
|
7
|
+
import type { BeingClass } from 'nervur';
|
|
8
|
+
|
|
9
|
+
export type Tree = { readonly [name: string]: unknown };
|
|
10
|
+
|
|
11
|
+
// Every class in a tree, keyed by where it lives: the folders down to it and
|
|
12
|
+
// its export name, `box/Dock`. The key is the class's identity in a ward,
|
|
13
|
+
// as a being's key is hers, and it moves only when the class moves.
|
|
14
|
+
export function keyed(tree: Tree, at = ''): Record<string, BeingClass> {
|
|
15
|
+
const out: Record<string, BeingClass> = {};
|
|
16
|
+
for (const [name, v] of Object.entries(tree)) {
|
|
17
|
+
const key = at ? `${at}/${name}` : name;
|
|
18
|
+
if (typeof v === 'function' && 'prototype' in v) out[key] = v as BeingClass;
|
|
19
|
+
else if (v !== null && typeof v === 'object' && !Array.isArray(v)) Object.assign(out, keyed(v as Tree, key));
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
package/{dock → box}/webpush.ts
RENAMED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// `auth` of the subscription it made, so the one party a banner key would
|
|
13
13
|
// hide the line from on this path is the push service, which that
|
|
14
14
|
// encryption already excludes. A second seal would buy nothing and would
|
|
15
|
-
// make every tab stand a
|
|
15
|
+
// make every tab stand a box ward to keep a banner key in. So the world
|
|
16
16
|
// encrypts here, the browser decrypts, and the service worker shows what it
|
|
17
17
|
// was handed rather than a line it guessed at.
|
|
18
18
|
//
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// This is one ground of the three the bell has. APNs and FCM are the
|
|
24
24
|
// stores' own and want a platform key; this one stands where nothing is in
|
|
25
25
|
// the way, a keypair the world mints itself.
|
|
26
|
-
import type { Line, Ring, Token } from './
|
|
26
|
+
import type { Line, Ring, Token } from './ring.ts';
|
|
27
27
|
|
|
28
28
|
const subtle = () => globalThis.crypto.subtle;
|
|
29
29
|
const ES256 = { name: 'ECDSA', namedCurve: 'P-256' } as const;
|
package/cli/client.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { connect } from 'node:net';
|
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { sockPath, sidePath } from './daemon.ts';
|
|
7
7
|
|
|
8
|
-
export type Answer = { result: unknown } | { error: string };
|
|
8
|
+
export type Answer = { result: unknown; ward?: string } | { error: string };
|
|
9
9
|
|
|
10
10
|
// `via` names a standing the ward's user being holds at another ward: the
|
|
11
11
|
// ask is then an owner's ask at that ward's door, sealed, and the answer is
|
package/cli/daemon.ts
CHANGED
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
// <- { ok: as } | { error }, then one line per final text
|
|
33
33
|
//
|
|
34
34
|
// The proof for a local side is that it reached side.sock: the daemon mints
|
|
35
|
-
// a nonce for the connection on the `local` faculty of its
|
|
35
|
+
// a nonce for the connection on the `local` faculty of its box ward, the
|
|
36
36
|
// box's own way in, and the desk trades it at a standing there, once. A
|
|
37
37
|
// `local` proof arriving any other way names no nonce and is refused.
|
|
38
38
|
//
|
|
39
|
-
// Agents have no door: the `agents` faculty of the
|
|
39
|
+
// Agents have no door: the `agents` faculty of the box ward holds each
|
|
40
40
|
// agent's identity, her ward, her command and her folder, placed by the
|
|
41
41
|
// root with `keep` as a schedule is placed on the clock, and the daemon
|
|
42
42
|
// admits her on the local proof and runs the agent side beside her avatar
|
|
@@ -80,13 +80,12 @@
|
|
|
80
80
|
// through the droplet it dialed.
|
|
81
81
|
import { createServer, type Server, type Socket } from 'node:net';
|
|
82
82
|
import type { Server as HttpServer } from 'node:http';
|
|
83
|
-
import { unlink, chmod
|
|
84
|
-
import { existsSync } from 'node:fs';
|
|
83
|
+
import { unlink, chmod } from 'node:fs/promises';
|
|
85
84
|
import { createHash, randomBytes } from 'node:crypto';
|
|
86
85
|
import { tmpdir } from 'node:os';
|
|
87
86
|
import { join, resolve } from 'node:path';
|
|
88
87
|
import { createInterface } from 'node:readline';
|
|
89
|
-
import { isSilence, isWord, wordOf, type Invitation, type JsonObject } from 'nervur';
|
|
88
|
+
import { isSilence, isWord, wordOf, type Invitation, type JsonObject, type Stance } from 'nervur';
|
|
90
89
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
91
90
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
92
91
|
import { DiskHarbor, defaultWard, type Hosted } from '../harbor/disk.ts';
|
|
@@ -94,12 +93,10 @@ import type { Harbor } from 'nervur/harbor';
|
|
|
94
93
|
import type { Avatar, User } from '../beings/index.ts';
|
|
95
94
|
// an identity in a world: the three asks any terrain's routes make at a ward
|
|
96
95
|
export { avatarOf, letIn, find } from '../beings/join.ts';
|
|
97
|
-
import { avatarOf, letIn, find } from '../beings/join.ts';
|
|
98
|
-
import {
|
|
99
|
-
import { apnsHttp2, apnsOf } from '../
|
|
100
|
-
import { GRANTS, Grants } from '../
|
|
101
|
-
import { AGENTS, Agents, isProcess } from '../dock/agents.ts';
|
|
102
|
-
import { DIALERS, Dialers } from '../dock/dialers.ts';
|
|
96
|
+
import { avatarOf, isIdentity, letIn, find } from '../beings/join.ts';
|
|
97
|
+
import { Http as Wire, routed, webPush, type Ring } from '../box/index.ts';
|
|
98
|
+
import { apnsHttp2, apnsOf } from '../box/http2.ts';
|
|
99
|
+
import { AGENTS, Agents, BOX, BOX_SEED, DIALERS, Dialers, GRANTS, Grants, LOCAL, Local, PUSH, Push, TIMER, Timer, WIRE, boxOf, isProcess, keyLent, lent, settled, type Harborage } from '../box/index.ts';
|
|
103
100
|
import { mcpSide } from '../mcp/server.ts';
|
|
104
101
|
import { runnerSide, type Model } from '../mcp/runner.ts';
|
|
105
102
|
import { agentSide, processRun } from '../mcp/agent.ts';
|
|
@@ -118,7 +115,10 @@ import { listenHttp, type Http, type Quo } from '../harbor/http.ts';
|
|
|
118
115
|
export type { Routes } from '../mcp/route.ts';
|
|
119
116
|
export type Serving = { harbor: DiskHarbor; sock: string; side: string; http: Http | null; grants: Grants | null; oauth: OAuth | null; exchange: Exchange | null; mcp: McpHttp | null; agents: Agents | null; quo: Quo; tick(now: number): Promise<void>; close(): Promise<void> };
|
|
120
117
|
// `rings` is what a test puts in the push faculty beside what the device's
|
|
121
|
-
// environment gives: a recorder where a platform would be.
|
|
118
|
+
// environment gives: a recorder where a platform would be. `http.host` is
|
|
119
|
+
// the address the door binds, loopback when nothing says otherwise: a
|
|
120
|
+
// daemon on a café network is a harbor anyone on it can knock at, so the
|
|
121
|
+
// address is said by hand or it is 127.0.0.1.
|
|
122
122
|
export type Options = { http?: { port: number; host?: string }; routes?: Routes; apps?: Apps | null; dial?: string[]; rings?: Record<string, Ring> };
|
|
123
123
|
|
|
124
124
|
// The sockets live in the harbor's folder, where the folder's own permissions
|
|
@@ -139,17 +139,17 @@ export const sidePath = (dir: string) => socketPath(dir, 'side');
|
|
|
139
139
|
// the first time only; to change one, remove the occupant and admit again.
|
|
140
140
|
// The one path for a side on the device: agents, and sides on side.sock.
|
|
141
141
|
//
|
|
142
|
-
// The nonce is minted on the `local` faculty of this box's
|
|
142
|
+
// The nonce is minted on the `local` faculty of this box's box ward, in
|
|
143
143
|
// this process, as a device fires its own timer. A box that lends no such
|
|
144
144
|
// name has no local way in, and nothing here can make one up.
|
|
145
145
|
export async function admit(harbor: Harbor, hosted: Hosted, identity: string, wake = false, reach = false): Promise<{ avatar?: Avatar; error?: string }> {
|
|
146
|
-
const local =
|
|
146
|
+
const local = await lent<Local>(harbor, LOCAL);
|
|
147
147
|
if (!local) return { error: 'this box has no local way in' };
|
|
148
148
|
const found = await avatarOf(hosted, identity);
|
|
149
149
|
if (!found.avatar) return found;
|
|
150
150
|
const nonce = randomBytes(16).toString('hex');
|
|
151
151
|
local.mint(nonce, { user: hosted.record.user, client: identity, ...(wake ? { wake: true } : {}), ...(reach ? { reach: true } : {}) });
|
|
152
|
-
const entered = await found.avatar.enter({ ward: hosted.pk }, { kind:
|
|
152
|
+
const entered = await found.avatar.enter({ ward: hosted.pk }, { kind: 'local', nonce });
|
|
153
153
|
local.drop(nonce);
|
|
154
154
|
await hosted.save(); // the knock went through the ward's own door, which the harbor never sees
|
|
155
155
|
if ('error' in entered && !('asks' in entered)) return { error: entered.error };
|
|
@@ -169,8 +169,8 @@ async function tick(harbor: DiskHarbor, now: number): Promise<void> {
|
|
|
169
169
|
// booted a moment ago is still taking one, and a round that ran past her
|
|
170
170
|
// would be a schedule placed and a minute missed for no reason a reader
|
|
171
171
|
// could see.
|
|
172
|
-
await settled();
|
|
173
|
-
const timer =
|
|
172
|
+
await settled(harbor);
|
|
173
|
+
const timer = await lent<Timer>(harbor, TIMER);
|
|
174
174
|
if (!timer) return;
|
|
175
175
|
try {
|
|
176
176
|
for (const woke of await timer.fire(now)) {
|
|
@@ -203,17 +203,55 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
|
|
|
203
203
|
// world's own web origin; the push faculty reads the map at each ring, so
|
|
204
204
|
// it is this daemon's `Push` body and no static set on a class.
|
|
205
205
|
const rings: Record<string, Ring> = { ...options.rings };
|
|
206
|
+
// The web ring is hers alone: Web Push signed under the doorbell key she
|
|
207
|
+
// minted at the root's first ask, with this world's web origin as the
|
|
208
|
+
// subject a push service may complain to, known once the routes are.
|
|
209
|
+
let subject: string | undefined;
|
|
206
210
|
class DaemonPush extends Push {
|
|
211
|
+
static override stands(): boolean {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
#web: Ring | undefined;
|
|
207
215
|
protected override get rings(): Readonly<Record<string, Ring>> {
|
|
208
|
-
|
|
216
|
+
const key = this.key;
|
|
217
|
+
if (rings.web || !key || !subject) return rings;
|
|
218
|
+
return { ...rings, web: (this.#web ??= webPush({ key, subject })) };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const harbor = new DiskHarbor(dir);
|
|
222
|
+
// The lines this box holds open are dialed for its own harbor, and an
|
|
223
|
+
// agent runs as an identity admitted on the local proof with a side over
|
|
224
|
+
// her avatar: the two live things only this process has, handed in the
|
|
225
|
+
// class bodies it brings.
|
|
226
|
+
class DaemonDialers extends Dialers {
|
|
227
|
+
static override stands(): boolean {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
constructor(stance: Stance, box?: Harborage) {
|
|
231
|
+
super(stance, box, harbor);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
class DaemonAgents extends Agents {
|
|
235
|
+
static override stands(): boolean {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
constructor(stance: Stance, box?: Harborage) {
|
|
239
|
+
super(stance, box, async (row) => {
|
|
240
|
+
const hosted = harbor.wards.get(row.ward);
|
|
241
|
+
if (!hosted) throw new Error(`agent ${row.id}: this device hosts no ward ${row.ward}`);
|
|
242
|
+
const admitted = await admit(harbor, hosted, row.id);
|
|
243
|
+
if (!admitted.avatar) throw new Error(`agent ${row.id}: ${admitted.error}`);
|
|
244
|
+
// a process is the agent side, a model is the runner: both take a push as the event
|
|
245
|
+
return isProcess(row) ? agentSide(admitted.avatar, processRun(row.command, row.args, row.dir), () => hosted.save()) : runnerSide(admitted.avatar, row, () => {}, () => hosted.save());
|
|
246
|
+
});
|
|
209
247
|
}
|
|
210
248
|
}
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
harbor.
|
|
249
|
+
// What a daemon brings to its own ward: the timer, `local`, the way in for
|
|
250
|
+
// a process it saw on its own socket, the grants its routes honour, and
|
|
251
|
+
// the ring, the lines and the agents it builds here. A device with no
|
|
252
|
+
// daemon brings none of the last five, and the desk there is a world
|
|
253
|
+
// nobody walks into locally.
|
|
254
|
+
harbor.faculties = { box: { Timer, Local, Grants, Http: Wire }, cli: { DaemonPush, DaemonDialers, DaemonAgents } };
|
|
217
255
|
// A store that refused a save, once per ward. Every asker whose write was
|
|
218
256
|
// refused has already been told, at the door that caused it, and none of
|
|
219
257
|
// them is this operator: a disk that has stopped taking one world's writes
|
|
@@ -223,6 +261,14 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
|
|
|
223
261
|
if (faults === 1) console.log(JSON.stringify({ at: new Date().toISOString(), refused: name }));
|
|
224
262
|
};
|
|
225
263
|
await harbor.boot();
|
|
264
|
+
// A contract two classes clashed under is lent to nobody, and the Dock
|
|
265
|
+
// keeps which; an operator reads it here, once, at boot, and the box is
|
|
266
|
+
// up either way.
|
|
267
|
+
const stood = (await harbor.wards.get(BOX_SEED)?.ask('ask', { being: BOX, method: 'lends' })) as { clashes?: Record<string, string[]> } | undefined;
|
|
268
|
+
for (const [word, keys] of Object.entries(stood?.clashes ?? {})) console.log(JSON.stringify({ at: new Date().toISOString(), clash: word, between: keys }));
|
|
269
|
+
// What the box is, asked of the Dock and read from no file: its routes,
|
|
270
|
+
// its apps and its default ward, each placed by the root through her asks.
|
|
271
|
+
const box = await boxOf(harbor);
|
|
226
272
|
// A ward the folder keeps and the harbor could not open stops this daemon
|
|
227
273
|
// here. The core is right to carry on without it, since a device with many
|
|
228
274
|
// worlds must not lose them all to one, but a daemon's folder is an
|
|
@@ -250,7 +296,7 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
|
|
|
250
296
|
let mcp: McpHttp | null = null;
|
|
251
297
|
let agents: Agents | null = null;
|
|
252
298
|
// the dialers faculty holds the lines; the route's view of them is hers
|
|
253
|
-
const dialers =
|
|
299
|
+
const dialers = await lent<Dialers>(harbor, DIALERS);
|
|
254
300
|
if (!dialers) throw new Error('this box has no dialers faculty');
|
|
255
301
|
const quo: Quo = {
|
|
256
302
|
sockets: new Set(),
|
|
@@ -278,37 +324,37 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
|
|
|
278
324
|
// The box's grants, one faculty for every route: a bearer names an
|
|
279
325
|
// identity in a world, the identity names her avatar there, and the
|
|
280
326
|
// route only says the grammar. Nothing is minted for a bearer. Her
|
|
281
|
-
// cells are the
|
|
282
|
-
grants =
|
|
327
|
+
// cells are the box ward's to keep, so a token issued saves it.
|
|
328
|
+
grants = (await lent<Grants>(harbor, GRANTS)) ?? null;
|
|
283
329
|
if (!grants) throw new Error('this box has no grants');
|
|
284
|
-
|
|
285
|
-
const resolve = async (identity: string, ward: string) => {
|
|
330
|
+
const findIn = async (identity: string, ward: string) => {
|
|
286
331
|
const hosted = harbor.wards.get(ward);
|
|
287
332
|
return hosted ? find(hosted, identity) : { error: 'no such world' };
|
|
288
333
|
};
|
|
289
|
-
const
|
|
334
|
+
const joinIn = async (identity: string, invitation: Invitation, ward: string) => {
|
|
290
335
|
const hosted = harbor.wards.get(ward);
|
|
291
336
|
if (!hosted) return { error: 'no such world' };
|
|
292
337
|
const r = await letIn(hosted, identity, invitation);
|
|
293
338
|
return r.error ? { error: r.error } : {};
|
|
294
339
|
};
|
|
295
|
-
const routes = options.routes ??
|
|
340
|
+
const routes = options.routes ?? box.routes;
|
|
296
341
|
const here = `http://${http.host}:${http.port}`;
|
|
297
342
|
const at = { quo: routes?.quo ?? `${here}/quo`, web: routes?.web ?? `${here}/web` };
|
|
343
|
+
subject = new URL(at.web).origin;
|
|
298
344
|
// a stranger's public being as {}, or an occupant's avatar under a
|
|
299
345
|
// bearer; `web` is for its one human arrival, a redirect landing back
|
|
300
346
|
// which ward a caller who names none is at is the terrain's word, and
|
|
301
347
|
// on a disk it is the mark an operator wrote, read below at `defaultWard`
|
|
302
|
-
http.mount('/api', apiRoute({ wards: harbor.wards, here: () => harbor.defaultWard }, { grants, resolve, join
|
|
348
|
+
http.mount('/api', apiRoute({ wards: harbor.wards, here: () => harbor.defaultWard }, { grants, resolve: findIn, join: joinIn }));
|
|
303
349
|
// The web ring, under the world's own doorbell key: the push faculty
|
|
304
350
|
// mints it the first time this box serves a web route and keeps it in
|
|
305
351
|
// her cells, so it lives with the wards and in no file. The ring is
|
|
306
352
|
// Web Push signed under it, and the subject a push service may
|
|
307
353
|
// complain to is this world's own web route, which every world has.
|
|
308
|
-
const push =
|
|
354
|
+
const push = await lent<Push>(harbor, PUSH);
|
|
309
355
|
if (!push) throw new Error('this box has no push faculty');
|
|
310
|
-
const
|
|
311
|
-
|
|
356
|
+
const bell = (await harbor.wards.get(BOX_SEED)?.ask('ask', { being: await keyLent(harbor, PUSH), method: 'bell' })) as { bell?: string } | undefined;
|
|
357
|
+
if (typeof bell?.bell !== 'string') throw new Error('this box minted no doorbell key');
|
|
312
358
|
// The two rings the stores own. A world holds no platform key and
|
|
313
359
|
// never will, because Apple and Google sign a push only for the team
|
|
314
360
|
// that published the app, so a ring to a phone is handed to a bell
|
|
@@ -331,17 +377,17 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
|
|
|
331
377
|
// the worlds' pages: always, on the daemon's own door when no route names a public one
|
|
332
378
|
// the bundles the dock's build emitted: read here, because reading a
|
|
333
379
|
// file is the terrain's and a route is handed the code
|
|
334
|
-
const web = webRoute(harbor, { at, code: await bundles(), folder: diskFolder(harbor.dir), apps: options.apps ??
|
|
380
|
+
const web = webRoute(harbor, { at, code: await bundles(), folder: diskFolder(harbor.dir), apps: options.apps ?? box.apps, bell: bell.bell, shown: (n) => DiskHarbor.isShown(harbor.dir, n) });
|
|
335
381
|
if (routes) {
|
|
336
382
|
// the worlds a client may be allowed into: every ward here but the
|
|
337
383
|
// dock, which is the box's and nobody's world, by name and pk, the device's own first
|
|
338
384
|
const worlds = () =>
|
|
339
385
|
[...harbor.wards]
|
|
340
|
-
.filter(([ward]) => ward !==
|
|
386
|
+
.filter(([ward]) => ward !== BOX_SEED)
|
|
341
387
|
.sort(([a], [b]) => (a === harbor.defaultWard ? -1 : b === harbor.defaultWard ? 1 : a.localeCompare(b)))
|
|
342
388
|
.map(([ward, h]) => ({ ward, pk: h.pk, user: h.record.user }));
|
|
343
389
|
// the MCP endpoint: a bearer names an identity in a world, the identity names her avatar there, the side runs beside her
|
|
344
|
-
mcp = new McpHttp(
|
|
390
|
+
mcp = new McpHttp(findIn, async () => {
|
|
345
391
|
for (const h of harbor.wards.values()) await h.save();
|
|
346
392
|
});
|
|
347
393
|
const route = await mcpRoute(danceOnDisk(harbor.dir), routes, mcp, grants);
|
|
@@ -350,7 +396,7 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
|
|
|
350
396
|
http.mount('/.well-known', route.wellKnown); // the same discovery at the origin's root, where a client looks for an issuer that has a path
|
|
351
397
|
const g = grants;
|
|
352
398
|
mcp.gone = (identity, ward) => g.revoke(identity, ward); // removal at the ward ends the grant at the box
|
|
353
|
-
exchange = new Exchange({ oauth, worlds, join });
|
|
399
|
+
exchange = new Exchange({ oauth, worlds, join: joinIn });
|
|
354
400
|
}
|
|
355
401
|
const ex = exchange;
|
|
356
402
|
http.mount(
|
|
@@ -363,31 +409,25 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
|
|
|
363
409
|
(ex && (await ex.handle(req, rest))) ??
|
|
364
410
|
new Response('no such page', { status: 404, headers: { 'content-type': 'text/plain' } }),
|
|
365
411
|
);
|
|
412
|
+
// Every other path is the box's wire: a faculty that claims it answers.
|
|
413
|
+
// Mounted last, so every route above is tried first.
|
|
414
|
+
const wire = await lent<Wire>(harbor, WIRE);
|
|
415
|
+
if (wire) http.mount('/', (req) => wire.handle(req));
|
|
366
416
|
}
|
|
367
417
|
// the routes this box holds a socket to are rows on the dialers faculty,
|
|
368
418
|
// dialed from her cells at every boot; `dial` is a suite's way of placing one
|
|
369
419
|
for (const url of options.dial ?? []) {
|
|
370
420
|
const kept = dialers.keep({ url });
|
|
371
|
-
if ('error' in kept) throw new Error(
|
|
421
|
+
if ('error' in kept) throw new Error(JSON.stringify(kept.error));
|
|
372
422
|
}
|
|
373
|
-
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
harbor.defaultWard = defaultWard(await DiskHarbor.marked(harbor.dir), [...harbor.wards.keys()]);
|
|
378
|
-
// The agents faculty holds the table; this daemon is what runs a row:
|
|
379
|
-
// the identity admitted into her ward on the local proof, and the agent
|
|
380
|
-
// side over her avatar for as long as the daemon serves.
|
|
381
|
-
agents = facultyOf<Agents>(harbor, AGENTS) ?? null;
|
|
423
|
+
// Which of its wards is the default: the ward the door answers for hangs
|
|
424
|
+
// off it, and a guess failing in silence is not a way to find out.
|
|
425
|
+
harbor.defaultWard = defaultWard(box.default === null ? [] : [box.default], [...harbor.wards.keys()]);
|
|
426
|
+
agents = (await lent<Agents>(harbor, AGENTS)) ?? null;
|
|
382
427
|
if (!agents) throw new Error('this box has no agents faculty');
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const admitted = await admit(harbor, hosted, row.id);
|
|
387
|
-
if (!admitted.avatar) throw new Error(`agent ${row.id}: ${admitted.error}`);
|
|
388
|
-
// a process is the agent side, a model is the runner: both take a push as the event
|
|
389
|
-
return isProcess(row) ? agentSide(admitted.avatar, processRun(row.command, row.args, row.dir), () => hosted.save()) : runnerSide(admitted.avatar, row, () => {}, () => hosted.save());
|
|
390
|
-
});
|
|
428
|
+
// The box is up: the lines are dialed and the agents run, and the daemon
|
|
429
|
+
// serves once they have.
|
|
430
|
+
await harbor.box.raise();
|
|
391
431
|
} catch (e) {
|
|
392
432
|
for (const s of servers) s.close();
|
|
393
433
|
await agents?.close();
|
|
@@ -411,7 +451,7 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
|
|
|
411
451
|
// daemon fire what is due right now.
|
|
412
452
|
tick: (now: number) => tick(harbor, now),
|
|
413
453
|
close: async () => {
|
|
414
|
-
|
|
454
|
+
(await lent<Timer>(harbor, TIMER))?.stop();
|
|
415
455
|
await mcp?.close();
|
|
416
456
|
await agents?.close();
|
|
417
457
|
dialers.close();
|
|
@@ -435,7 +475,7 @@ async function sideConnection(harbor: DiskHarbor, c: Socket): Promise<void> {
|
|
|
435
475
|
const req = JSON.parse(hello) as { as?: string; ward?: string; run?: unknown };
|
|
436
476
|
hosted = harbor.wards.get(req.ward ?? harbor.defaultWard);
|
|
437
477
|
if (!hosted) throw new Error('no such ward');
|
|
438
|
-
if (
|
|
478
|
+
if (!isIdentity(hosted, req.as)) throw new Error('as is a word, and not a being of the ward');
|
|
439
479
|
as = req.as;
|
|
440
480
|
if (req.run !== undefined) {
|
|
441
481
|
const r = req.run as Partial<Model>;
|
|
@@ -527,55 +567,13 @@ async function handle(harbor: DiskHarbor, line: string): Promise<string> {
|
|
|
527
567
|
}
|
|
528
568
|
}
|
|
529
569
|
const out = await hosted.ask(req.method, req.args ?? {});
|
|
530
|
-
|
|
570
|
+
// the ward it landed on, named back, since a request naming none is on the default and the caller may need its name
|
|
571
|
+
return JSON.stringify({ id, ward: hosted.name, result: isSilence(out) ? { silence: true } : out });
|
|
531
572
|
} catch (e) {
|
|
532
573
|
return JSON.stringify({ id, error: e instanceof Error ? e.message : String(e) });
|
|
533
574
|
}
|
|
534
575
|
}
|
|
535
576
|
|
|
536
|
-
// The four routes by the one convention: `mcp.`, `web.`, `quo.` and `api.`
|
|
537
|
-
// under one domain, over https. A domain is said once and the names follow;
|
|
538
|
-
// an estate whose names break the convention edits the file it wrote.
|
|
539
|
-
export const DOMAIN = /^[a-z0-9.-]+\.[a-z]{2,}$/i;
|
|
540
|
-
export function routesFor(domain: string): Required<Routes> {
|
|
541
|
-
if (!DOMAIN.test(domain)) throw new Error(`${domain} is not a domain`);
|
|
542
|
-
return { mcp: `https://mcp.${domain}`, web: `https://web.${domain}`, quo: `https://quo.${domain}`, api: `https://api.${domain}` };
|
|
543
|
-
}
|
|
544
|
-
export async function writeRoutes(dir: string, domain: string): Promise<Required<Routes>> {
|
|
545
|
-
const routes = routesFor(domain);
|
|
546
|
-
await mkdir(dir, { recursive: true });
|
|
547
|
-
await writeFile(join(dir, 'routes.json'), JSON.stringify(routes) + '\n');
|
|
548
|
-
return routes;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// `<dir>/routes.json`: { mcp, web, quo?, api? }, the public origins the proxy serves.
|
|
552
|
-
export async function readRoutes(dir: string): Promise<Routes | null> {
|
|
553
|
-
const p = join(dir, 'routes.json');
|
|
554
|
-
if (!existsSync(p)) return null;
|
|
555
|
-
const r = JSON.parse(await readFile(p, 'utf8')) as Partial<Routes>;
|
|
556
|
-
if (typeof r.mcp !== 'string' || typeof r.web !== 'string') return null;
|
|
557
|
-
const out: Routes = { mcp: r.mcp.replace(/\/$/, ''), web: r.web.replace(/\/$/, '') };
|
|
558
|
-
if (typeof r.quo === 'string') out.quo = r.quo.replace(/\/$/, '');
|
|
559
|
-
if (typeof r.api === 'string') out.api = r.api.replace(/\/$/, '');
|
|
560
|
-
return out;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// `<dir>/app.json`: { apple: [...], android: [{ package, fingerprints }] }, the
|
|
564
|
-
// apps whose links this world's hostname opens, as the two platforms want
|
|
565
|
-
// them named. An estate that ships no app of its own writes no such file,
|
|
566
|
-
// serves no association file, and its links open the tab.
|
|
567
|
-
export async function readApps(dir: string): Promise<Apps | null> {
|
|
568
|
-
const p = join(dir, 'app.json');
|
|
569
|
-
if (!existsSync(p)) return null;
|
|
570
|
-
const r = JSON.parse(await readFile(p, 'utf8')) as Partial<Apps>;
|
|
571
|
-
const apple = Array.isArray(r.apple) ? r.apple.filter((x): x is string => typeof x === 'string') : [];
|
|
572
|
-
const android = Array.isArray(r.android)
|
|
573
|
-
? r.android
|
|
574
|
-
.filter((a) => typeof a?.package === 'string' && Array.isArray(a.fingerprints))
|
|
575
|
-
.map((a) => ({ package: a.package, fingerprints: a.fingerprints.filter((f): f is string => typeof f === 'string') }))
|
|
576
|
-
: [];
|
|
577
|
-
return apple.length === 0 && android.length === 0 ? null : { apple, android };
|
|
578
|
-
}
|
|
579
577
|
|
|
580
578
|
|
|
581
579
|
|
package/cli/estate.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// that device runs minus what it mints, its routes under the domain, no
|
|
5
5
|
// agents, no schedules, no classes of its own, and that device's unit and
|
|
6
6
|
// Caddyfile; and `mac/`, the launchd agent that runs the daemon on an
|
|
7
|
-
// operator's Mac as a dialer
|
|
7
|
+
// operator's Mac as a dialer. A package file
|
|
8
8
|
// that depends on this dock and nothing else, and one document to fill in.
|
|
9
9
|
// An estate needs nothing the dock does not give it, and this is the dock
|
|
10
10
|
// giving it.
|
|
@@ -12,7 +12,7 @@ import { existsSync } from 'node:fs';
|
|
|
12
12
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
13
|
import { basename, join } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { DOMAIN
|
|
15
|
+
import { DOMAIN } from '../box/index.ts';
|
|
16
16
|
|
|
17
17
|
// This package's own version, read beside the emitted or the source tree.
|
|
18
18
|
async function version(): Promise<string> {
|
|
@@ -33,7 +33,6 @@ export async function estate(dir: string, domain: string): Promise<{ dir: string
|
|
|
33
33
|
const files: Record<string, string> = {
|
|
34
34
|
'package.json': JSON.stringify({ name, private: true, description: `The ${name} estate: one owner's harbors, built on the dock and nothing else.`, type: 'module', dependencies: { '@nervur-org/dock': `^${await version()}` } }, null, 2) + '\n',
|
|
35
35
|
[`${name}.md`]: `# ${name}\n\nAn estate: all of one owner's harbors. One folder per harbor, each the harbor\ndirectory that device runs minus what it mints, plus that device's unit.\n\`droplet/\` is the first: its routes under \`${domain}\`, its agents, its\nclasses, its systemd unit and its Caddyfile. This folder depends on the\ndock and nothing else.\n\n## The droplet\n\nWhere it is, how it is reached, and what was done to stand it up: yours\nto write.\n`,
|
|
36
|
-
'droplet/routes.json': JSON.stringify(routesFor(domain)) + '\n',
|
|
37
36
|
'droplet/stand.ts': `// SPDX-License-Identifier: Apache-2.0\n// What the root stands up on this harbor, once, after the inits and with no\n// daemon over the folder: the beings the estate boots for its own working\n// and the relations between them, placed on real invitations. Run as\n// \`node droplet/stand.ts <dir>\`; twice is once.\nimport { stand } from '@nervur-org/dock/stand';\n\nconsole.log(await stand(process.argv[2] ?? process.env.NERVUR_DIR ?? '', { boot: [], wire: [] }));\n`,
|
|
38
37
|
'droplet/classes/index.ts': '// The classes this harbor holds beside the dock\'s built-in ones, exported\n// by name.\nexport {};\n',
|
|
39
38
|
'droplet/nervur.service': (await template('nervur.service')).replaceAll('ESTATE', name),
|