@nervur-org/dock 0.3.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/GETTING_STARTED.md +128 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +37 -0
- package/api/quo-api.md +196 -0
- package/api/route.ts +227 -0
- package/beings/GLOSSARY.md +118 -0
- package/beings/WORLDS.md +567 -0
- package/beings/avatar.ts +101 -0
- package/beings/carry.ts +159 -0
- package/beings/clock.ts +552 -0
- package/beings/courier.ts +107 -0
- package/beings/desk.ts +128 -0
- package/beings/doorbell.ts +129 -0
- package/beings/envoy.ts +218 -0
- package/beings/index.ts +16 -0
- package/beings/join.ts +51 -0
- package/beings/link.ts +104 -0
- package/beings/look.ts +139 -0
- package/beings/porter.ts +43 -0
- package/beings/presence.ts +39 -0
- package/beings/quo-app-bells.md +280 -0
- package/beings/quo-dock.md +1942 -0
- package/beings/setup.ts +45 -0
- package/beings/side.ts +60 -0
- package/beings/user.ts +459 -0
- package/beings/way.ts +65 -0
- package/cli/bundles.ts +20 -0
- package/cli/client.ts +73 -0
- package/cli/daemon.ts +581 -0
- package/cli/estate/Caddyfile +33 -0
- package/cli/estate/quo.service +35 -0
- package/cli/estate/systems.quo.plist +44 -0
- package/cli/estate.ts +48 -0
- package/cli/folder.ts +27 -0
- package/cli/oauth.ts +32 -0
- package/cli/pilot.ts +38 -0
- package/cli/quo-dock.md +1942 -0
- package/cli/quo.ts +203 -0
- package/cli/stand.ts +160 -0
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.js +193 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +100 -0
- package/dist/beings/carry.d.ts +12 -0
- package/dist/beings/carry.js +174 -0
- package/dist/beings/clock.d.ts +161 -0
- package/dist/beings/clock.js +528 -0
- package/dist/beings/courier.d.ts +50 -0
- package/dist/beings/courier.js +96 -0
- package/dist/beings/desk.d.ts +53 -0
- package/dist/beings/desk.js +135 -0
- package/dist/beings/doorbell.d.ts +78 -0
- package/dist/beings/doorbell.js +133 -0
- package/dist/beings/envoy.d.ts +54 -0
- package/dist/beings/envoy.js +211 -0
- package/dist/beings/index.d.ts +14 -0
- package/dist/beings/index.js +16 -0
- package/dist/beings/join.d.ts +15 -0
- package/dist/beings/join.js +42 -0
- package/dist/beings/link.d.ts +14 -0
- package/dist/beings/link.js +91 -0
- package/dist/beings/look.d.ts +30 -0
- package/dist/beings/look.js +104 -0
- package/dist/beings/porter.d.ts +14 -0
- package/dist/beings/porter.js +41 -0
- package/dist/beings/presence.d.ts +8 -0
- package/dist/beings/presence.js +21 -0
- package/dist/beings/setup.d.ts +3 -0
- package/dist/beings/setup.js +39 -0
- package/dist/beings/side.d.ts +32 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +315 -0
- package/dist/beings/user.js +462 -0
- package/dist/beings/way.d.ts +19 -0
- package/dist/beings/way.js +28 -0
- package/dist/cli/bundles.d.ts +5 -0
- package/dist/cli/bundles.js +17 -0
- package/dist/cli/client.d.ts +15 -0
- package/dist/cli/client.js +58 -0
- package/dist/cli/daemon.d.ts +50 -0
- package/dist/cli/daemon.js +584 -0
- package/dist/cli/estate/Caddyfile +33 -0
- package/dist/cli/estate/quo.service +35 -0
- package/dist/cli/estate/systems.quo.plist +44 -0
- package/dist/cli/estate.d.ts +5 -0
- package/dist/cli/estate.js +50 -0
- package/dist/cli/folder.d.ts +2 -0
- package/dist/cli/folder.js +25 -0
- package/dist/cli/oauth.d.ts +2 -0
- package/dist/cli/oauth.js +32 -0
- package/dist/cli/pilot.d.ts +5 -0
- package/dist/cli/pilot.js +22 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +201 -0
- package/dist/cli/stand.d.ts +37 -0
- package/dist/cli/stand.js +110 -0
- package/dist/dock/agents.d.ts +102 -0
- package/dist/dock/agents.js +105 -0
- package/dist/dock/apns.d.ts +27 -0
- package/dist/dock/apns.js +98 -0
- package/dist/dock/console.d.ts +6 -0
- package/dist/dock/console.js +20 -0
- package/dist/dock/dialers.d.ts +56 -0
- package/dist/dock/dialers.js +75 -0
- package/dist/dock/faculty.d.ts +24 -0
- package/dist/dock/faculty.js +142 -0
- package/dist/dock/fcm.d.ts +13 -0
- package/dist/dock/fcm.js +77 -0
- package/dist/dock/grants.d.ts +41 -0
- package/dist/dock/grants.js +99 -0
- package/dist/dock/http2.d.ts +4 -0
- package/dist/dock/http2.js +68 -0
- package/dist/dock/index.d.ts +41 -0
- package/dist/dock/index.js +113 -0
- package/dist/dock/local.d.ts +13 -0
- package/dist/dock/local.js +44 -0
- package/dist/dock/location.d.ts +19 -0
- package/dist/dock/location.js +11 -0
- package/dist/dock/notifications.d.ts +38 -0
- package/dist/dock/notifications.js +60 -0
- package/dist/dock/push.d.ts +79 -0
- package/dist/dock/push.js +132 -0
- package/dist/dock/service.d.ts +4 -0
- package/dist/dock/service.js +107 -0
- package/dist/dock/timer.d.ts +52 -0
- package/dist/dock/timer.js +149 -0
- package/dist/dock/webpush.d.ts +12 -0
- package/dist/dock/webpush.js +144 -0
- package/dist/harbor/browser.d.ts +15 -0
- package/dist/harbor/browser.js +82 -0
- package/dist/harbor/capacitor.d.ts +28 -0
- package/dist/harbor/capacitor.js +218 -0
- package/dist/harbor/disk.d.ts +27 -0
- package/dist/harbor/disk.js +249 -0
- package/dist/harbor/edge/alarm.d.ts +10 -0
- package/dist/harbor/edge/alarm.js +38 -0
- package/dist/harbor/edge/edge.d.ts +54 -0
- package/dist/harbor/edge/edge.js +370 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +232 -0
- package/dist/harbor/edge/given.d.ts +26 -0
- package/dist/harbor/edge/given.js +42 -0
- package/dist/harbor/edge/platform.d.ts +39 -0
- package/dist/harbor/edge/storage.d.ts +30 -0
- package/dist/harbor/edge/storage.js +180 -0
- package/dist/harbor/edge/worker.d.ts +17 -0
- package/dist/harbor/edge/worker.js +67 -0
- package/dist/harbor/files.d.ts +15 -0
- package/dist/harbor/files.js +220 -0
- package/dist/harbor/http.d.ts +19 -0
- package/dist/harbor/http.js +177 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +0 -0
- package/dist/harbor/quo.d.ts +10 -0
- package/dist/harbor/quo.js +42 -0
- package/dist/harbor/resolve.d.ts +16 -0
- package/dist/harbor/resolve.js +17 -0
- package/dist/harbor/seal.d.ts +3 -0
- package/dist/harbor/seal.js +25 -0
- package/dist/harbor/sealed.d.ts +35 -0
- package/dist/harbor/sealed.js +162 -0
- package/dist/harbor/tauri.d.ts +16 -0
- package/dist/harbor/tauri.js +59 -0
- package/dist/harbor/ward.d.ts +9 -0
- package/dist/harbor/ward.js +15 -0
- package/dist/human/app.d.ts +32 -0
- package/dist/human/app.js +527 -0
- package/dist/human/apps.d.ts +10 -0
- package/dist/human/apps.js +29 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/door.d.ts +19 -0
- package/dist/human/door.js +92 -0
- package/dist/human/guest.d.ts +3 -0
- package/dist/human/guest.js +33 -0
- package/dist/human/html.d.ts +55 -0
- package/dist/human/html.js +451 -0
- package/dist/human/local.d.ts +7 -0
- package/dist/human/local.js +10 -0
- package/dist/human/mark.d.ts +8 -0
- package/dist/human/mark.js +47 -0
- package/dist/human/move.d.ts +17 -0
- package/dist/human/move.js +90 -0
- package/dist/human/screen.d.ts +19 -0
- package/dist/human/screen.js +119 -0
- package/dist/human/style.d.ts +2 -0
- package/dist/human/style.js +60 -0
- package/dist/human/tab.bundle.txt +4775 -0
- package/dist/human/tab.d.ts +15 -0
- package/dist/human/tab.js +409 -0
- package/dist/human/tree.d.ts +50 -0
- package/dist/human/tree.js +157 -0
- package/dist/human/wake.d.ts +34 -0
- package/dist/human/wake.js +179 -0
- package/dist/human/web.d.ts +33 -0
- package/dist/human/web.js +184 -0
- package/dist/human/worker.bundle.txt +3868 -0
- package/dist/human/worker.d.ts +1 -0
- package/dist/human/worker.js +81 -0
- package/dist/human/worlds.d.ts +24 -0
- package/dist/human/worlds.js +99 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/allow.d.ts +20 -0
- package/dist/mcp/allow.js +88 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +92 -0
- package/dist/mcp/oauth.d.ts +91 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/route.d.ts +15 -0
- package/dist/mcp/route.js +42 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +99 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +102 -0
- package/dock/agents.ts +121 -0
- package/dock/apns.ts +138 -0
- package/dock/console.ts +23 -0
- package/dock/dialers.ts +85 -0
- package/dock/faculty.ts +148 -0
- package/dock/fcm.ts +103 -0
- package/dock/grants.ts +111 -0
- package/dock/http2.ts +70 -0
- package/dock/index.ts +133 -0
- package/dock/local.ts +48 -0
- package/dock/location.ts +22 -0
- package/dock/notifications.ts +67 -0
- package/dock/push.ts +161 -0
- package/dock/quo-app-bells.md +280 -0
- package/dock/quo-dock.md +1942 -0
- package/dock/service.ts +100 -0
- package/dock/timer.ts +174 -0
- package/dock/webpush.ts +182 -0
- package/harbor/browser.ts +97 -0
- package/harbor/capacitor.ts +223 -0
- package/harbor/disk.ts +237 -0
- package/harbor/edge/alarm.ts +48 -0
- package/harbor/edge/edge.ts +407 -0
- package/harbor/edge/exercise.ts +241 -0
- package/harbor/edge/given.ts +106 -0
- package/harbor/edge/platform.d.ts +39 -0
- package/harbor/edge/storage.ts +179 -0
- package/harbor/edge/text.d.ts +15 -0
- package/harbor/edge/worker.ts +70 -0
- package/harbor/edge/wrangler.toml +30 -0
- package/harbor/files.ts +221 -0
- package/harbor/http.ts +188 -0
- package/harbor/idb.ts +0 -0
- package/harbor/quo-harbor.md +850 -0
- package/harbor/quo.ts +44 -0
- package/harbor/resolve.ts +31 -0
- package/harbor/seal.ts +26 -0
- package/harbor/sealed.ts +183 -0
- package/harbor/tauri.ts +79 -0
- package/harbor/ward.ts +28 -0
- package/human/app.ts +580 -0
- package/human/apps.ts +47 -0
- package/human/dom.ts +21 -0
- package/human/door.ts +113 -0
- package/human/guest.ts +34 -0
- package/human/html.ts +490 -0
- package/human/local.ts +30 -0
- package/human/mark.ts +53 -0
- package/human/move.ts +122 -0
- package/human/quo-human.md +1042 -0
- package/human/screen.ts +136 -0
- package/human/style.ts +63 -0
- package/human/tab.bundle.txt +4775 -0
- package/human/tab.ts +446 -0
- package/human/tree.ts +217 -0
- package/human/wake.ts +191 -0
- package/human/web.ts +259 -0
- package/human/worker.bundle.txt +3868 -0
- package/human/worker.ts +124 -0
- package/human/worlds.ts +127 -0
- package/mcp/agent.ts +94 -0
- package/mcp/allow.ts +131 -0
- package/mcp/http.ts +92 -0
- package/mcp/oauth.ts +270 -0
- package/mcp/quo-mcp.md +343 -0
- package/mcp/route.ts +45 -0
- package/mcp/runner.ts +136 -0
- package/mcp/server.ts +101 -0
- package/package.json +121 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The HTTP door: one listener on loopback that a reverse proxy fronts, and
|
|
3
|
+
// the routes mount on it by path prefix. `/health` is the harbor's own: the
|
|
4
|
+
// wards it hosts, by name and pk, and nothing a stranger can use. Each route
|
|
5
|
+
// lives in its own folder, `human/web.ts`, `harbor/quo.ts`, `mcp/route.ts`,
|
|
6
|
+
// and this file knows none of them: a route is a handler under a prefix.
|
|
7
|
+
//
|
|
8
|
+
// This is the route contract every side is written against, and it stands
|
|
9
|
+
// beside the disk harbor whose door it is. A route is `Request` in and
|
|
10
|
+
// `Response` out, the shapes every runtime speaks, so a side runs on this
|
|
11
|
+
// daemon, on a worker at the edge, in a browser's own service worker, on
|
|
12
|
+
// Deno or on Bun without a line of it changing. A handler answering nothing
|
|
13
|
+
// declines, and the mount tries what comes after it. A folder writing a
|
|
14
|
+
// route takes `Handler`, `readAll` and `Quo` from here and nothing of the
|
|
15
|
+
// daemon that mounts them.
|
|
16
|
+
//
|
|
17
|
+
// `listenHttp` is the one place Node's own objects appear: its door turns an
|
|
18
|
+
// `IncomingMessage` into a `Request` and writes a `Response` back onto a
|
|
19
|
+
// `ServerResponse`, and nothing downstream of it names either. The
|
|
20
|
+
// translation runs that one way only: a `Request` is never dressed up as an
|
|
21
|
+
// `IncomingMessage`, because a route writes headers and there would be
|
|
22
|
+
// nothing to write them onto.
|
|
23
|
+
//
|
|
24
|
+
// The listener half of the socket reach is here too, and for the same reason:
|
|
25
|
+
// holding a socket is the one thing no two runtimes do alike, so it stands
|
|
26
|
+
// with the terrain's own door and never in a route. `ws` is that half on
|
|
27
|
+
// Node; at the edge it is a `WebSocketPair` in the harbor there.
|
|
28
|
+
import { createServer as createHttp } from 'node:http';
|
|
29
|
+
import { Readable } from 'node:stream';
|
|
30
|
+
import { WebSocketServer } from 'ws';
|
|
31
|
+
import { Socket as Held } from '@nervur-org/nervur/harbor';
|
|
32
|
+
// What a request may carry, on the doors that read a body: enough for any ask
|
|
33
|
+
// a ward seals, and not a body chosen by whoever is on the other end.
|
|
34
|
+
// The bytes are a `Uint8Array` and not a `Buffer`: a route runs on every
|
|
35
|
+
// runtime, and node's own type is not one of the shapes they all speak.
|
|
36
|
+
const BODY = 1 << 20;
|
|
37
|
+
export async function readAll(req) {
|
|
38
|
+
if (!req.body)
|
|
39
|
+
return new Uint8Array(0);
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let size = 0;
|
|
42
|
+
const reader = req.body.getReader();
|
|
43
|
+
for (;;) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done)
|
|
46
|
+
break;
|
|
47
|
+
size += value.length;
|
|
48
|
+
if (size > BODY) {
|
|
49
|
+
await reader.cancel().catch(() => { });
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
chunks.push(value);
|
|
53
|
+
}
|
|
54
|
+
const all = new Uint8Array(size);
|
|
55
|
+
let at = 0;
|
|
56
|
+
for (const c of chunks) {
|
|
57
|
+
all.set(c, at);
|
|
58
|
+
at += c.length;
|
|
59
|
+
}
|
|
60
|
+
return all;
|
|
61
|
+
}
|
|
62
|
+
// The same body as text, for the doors that read one: JSON, a form, or a
|
|
63
|
+
// vendor's signed bytes a being reads as a string.
|
|
64
|
+
export async function readText(req) {
|
|
65
|
+
const raw = await readAll(req);
|
|
66
|
+
return raw === undefined ? undefined : new TextDecoder().decode(raw);
|
|
67
|
+
}
|
|
68
|
+
export async function listenHttp(harbor, host, port, quo) {
|
|
69
|
+
const mounts = new Map();
|
|
70
|
+
const server = createHttp((req, res) => {
|
|
71
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
72
|
+
const answer = async () => {
|
|
73
|
+
if (url.pathname === '/health') {
|
|
74
|
+
// the wards it hosts, and the directory: pks and whether each is a
|
|
75
|
+
// socket held here, which is what an operator needs to see a dialer.
|
|
76
|
+
// Open to any origin: a tab reads it, and it holds nothing a stranger can use.
|
|
77
|
+
return Response.json({
|
|
78
|
+
ok: true,
|
|
79
|
+
wards: Object.fromEntries([...harbor.wards].map(([n, h]) => [n, h.pk])),
|
|
80
|
+
reaches: Object.fromEntries([...harbor.reaches].map(([pk, b]) => [pk, b.held ? 'held' : 'reach'])),
|
|
81
|
+
sockets: quo.sockets.size,
|
|
82
|
+
dialers: quo.dialers.map((d) => ({ url: d.url, open: d.socket !== null })),
|
|
83
|
+
// How many saves the disk refused, per ward. The asker whose
|
|
84
|
+
// write was refused was told at the door; this is the operator's
|
|
85
|
+
// half, and a box that says nothing about it looks healthy while
|
|
86
|
+
// one world stops keeping anything.
|
|
87
|
+
faults: Object.fromEntries([...harbor.faults]),
|
|
88
|
+
}, { headers: { 'access-control-allow-origin': '*' } });
|
|
89
|
+
}
|
|
90
|
+
for (const [prefix, handler] of mounts) {
|
|
91
|
+
if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
|
|
92
|
+
const out = await handler(asRequest(req, url), url.pathname.slice(prefix.length));
|
|
93
|
+
if (out)
|
|
94
|
+
return out;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return Response.json({ error: 'no such route' }, { status: 404 });
|
|
99
|
+
};
|
|
100
|
+
void answer()
|
|
101
|
+
.then((out) => writeResponse(out, res))
|
|
102
|
+
.catch(() => {
|
|
103
|
+
if (!res.headersSent)
|
|
104
|
+
res.writeHead(500);
|
|
105
|
+
res.end();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
await new Promise((ok, no) => {
|
|
109
|
+
server.once('error', no);
|
|
110
|
+
server.listen(port, host, () => ok());
|
|
111
|
+
});
|
|
112
|
+
hold(server, harbor, quo);
|
|
113
|
+
const address = server.address();
|
|
114
|
+
const bound = typeof address === 'object' && address !== null ? address.port : port;
|
|
115
|
+
return { server, host, port: bound, mount: (prefix, handler) => mounts.set(prefix.replace(/\/$/, ''), handler) };
|
|
116
|
+
}
|
|
117
|
+
// A dialer's held socket, this terrain's way: an upgrade at the quo route on
|
|
118
|
+
// the daemon's own listener, held as the library's `Socket`. Its pks are bound
|
|
119
|
+
// at announce, each proven at its door, and unbound when the line drops. The
|
|
120
|
+
// route itself reads no socket, and every runtime holds one differently.
|
|
121
|
+
function hold(server, harbor, quo) {
|
|
122
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
123
|
+
server.on('upgrade', (req, socket, head) => {
|
|
124
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
125
|
+
if (url.pathname.replace(/\/$/, '') !== '/quo')
|
|
126
|
+
return void socket.destroy(); // the proxy rewrites the route root to /quo/
|
|
127
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
128
|
+
const s = new Held(ws, (pk, bytes) => harbor.deliver(pk, bytes), (far) => void harbor.bind(far, s, true), // a dialer's claims, each proven at its door: held here, reachable through this socket
|
|
129
|
+
() => {
|
|
130
|
+
harbor.unbind(s);
|
|
131
|
+
quo.sockets.delete(s);
|
|
132
|
+
});
|
|
133
|
+
quo.sockets.add(s);
|
|
134
|
+
s.announce([...harbor.doors.keys()]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Node's request as a `Request`: the one translation in, at the daemon's own
|
|
139
|
+
// door. A method that carries no body carries none here either, since the
|
|
140
|
+
// web shape refuses one.
|
|
141
|
+
function asRequest(req, url) {
|
|
142
|
+
const headers = new Headers();
|
|
143
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
144
|
+
if (v === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
if (Array.isArray(v))
|
|
147
|
+
for (const one of v)
|
|
148
|
+
headers.append(k, one);
|
|
149
|
+
else
|
|
150
|
+
headers.set(k, v);
|
|
151
|
+
}
|
|
152
|
+
const method = req.method ?? 'GET';
|
|
153
|
+
const bodied = method !== 'GET' && method !== 'HEAD';
|
|
154
|
+
return new Request(url, {
|
|
155
|
+
method,
|
|
156
|
+
headers,
|
|
157
|
+
...(bodied ? { body: Readable.toWeb(req), duplex: 'half' } : {}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// And the one translation out.
|
|
161
|
+
async function writeResponse(out, res) {
|
|
162
|
+
const headers = {};
|
|
163
|
+
out.headers.forEach((v, k) => {
|
|
164
|
+
headers[k] = k === 'set-cookie' ? [...(Array.isArray(headers[k]) ? headers[k] : []), v] : v;
|
|
165
|
+
});
|
|
166
|
+
res.writeHead(out.status, headers);
|
|
167
|
+
if (!out.body)
|
|
168
|
+
return void res.end();
|
|
169
|
+
const reader = out.body.getReader();
|
|
170
|
+
for (;;) {
|
|
171
|
+
const { done, value } = await reader.read();
|
|
172
|
+
if (done)
|
|
173
|
+
break;
|
|
174
|
+
res.write(Buffer.from(value));
|
|
175
|
+
}
|
|
176
|
+
res.end();
|
|
177
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Kept, type Store, type WardRecord } from '@nervur-org/nervur/harbor';
|
|
2
|
+
export declare class Idb implements Store {
|
|
3
|
+
#private;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
constructor(name?: string);
|
|
6
|
+
list(): Promise<string[]>;
|
|
7
|
+
load(name: string): Promise<Kept | undefined>;
|
|
8
|
+
put(name: string, kept: Kept): Promise<void>;
|
|
9
|
+
save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
|
|
10
|
+
record(name: string, record: WardRecord): Promise<void>;
|
|
11
|
+
take(name: string): Promise<Kept | undefined>;
|
|
12
|
+
hints(): Promise<Record<string, string>>;
|
|
13
|
+
hint(pk: string, url: string): Promise<void>;
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Handler } from './http.ts';
|
|
2
|
+
export type Deliver = {
|
|
3
|
+
deliver(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
|
|
4
|
+
};
|
|
5
|
+
export declare const OPEN: {
|
|
6
|
+
'access-control-allow-origin': string;
|
|
7
|
+
'access-control-allow-methods': string;
|
|
8
|
+
'access-control-allow-headers': string;
|
|
9
|
+
};
|
|
10
|
+
export declare function quoRoute(harbor: Deliver): Handler;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The quo. route: the door sealed bytes arrive at, the same route on every
|
|
3
|
+
// terrain. POST `/quo/<pk>` carries one ask to a pk this harbor holds or
|
|
4
|
+
// holds a socket for; bytes from here go to an own door or a held socket,
|
|
5
|
+
// never onward. A URL of this route is the base of every reach.
|
|
6
|
+
//
|
|
7
|
+
// It is `Request` in and `Response` out and names no runtime, so the daemon
|
|
8
|
+
// mounts it under `/quo` behind the proxy that maps the quo. hostname and the
|
|
9
|
+
// worker at the edge mounts it under its own prefix.
|
|
10
|
+
//
|
|
11
|
+
// The listener half of the socket reach is not here, because holding a socket
|
|
12
|
+
// is the one thing no two runtimes do alike: `ws` on Node, at the daemon's
|
|
13
|
+
// own door in `http.ts`, and a `WebSocketPair` at the edge, in the harbor
|
|
14
|
+
// there. What a dialer's socket becomes once it is held is the library's
|
|
15
|
+
// `Socket` on both.
|
|
16
|
+
import { SUITE } from '@nervur-org/nervur/harbor';
|
|
17
|
+
import { readAll } from './http.js';
|
|
18
|
+
// The route is public and carries sealed bytes, so any origin may POST
|
|
19
|
+
// to it: a tab on a world's web. reaching another world's quo. is the
|
|
20
|
+
// ordinary case, and a binary POST needs the preflight answered.
|
|
21
|
+
export const OPEN = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST, OPTIONS', 'access-control-allow-headers': 'content-type, quo-suite' };
|
|
22
|
+
export function quoRoute(harbor) {
|
|
23
|
+
return async (req, rest) => {
|
|
24
|
+
if (req.method === 'OPTIONS')
|
|
25
|
+
return new Response(null, { status: 204, headers: OPEN });
|
|
26
|
+
// The wire suite the caller speaks. Absent is this one, because a caller
|
|
27
|
+
// older than the header is this one. Anything else this door cannot open
|
|
28
|
+
// and says so as nothing delivered, rather than taking bytes that will
|
|
29
|
+
// never open and answering a silence that names no reason.
|
|
30
|
+
const suite = req.headers.get('quo-suite');
|
|
31
|
+
if (suite !== null && suite !== String(SUITE))
|
|
32
|
+
return Response.json({ error: `this door speaks wire suite ${SUITE}` }, { status: 404, headers: OPEN });
|
|
33
|
+
const pk = rest.replace(/^\//, '');
|
|
34
|
+
if (req.method !== 'POST' || !/^[0-9a-f]{128}$/.test(pk))
|
|
35
|
+
return Response.json({ error: 'POST /quo/<pk>' }, { status: 404, headers: OPEN });
|
|
36
|
+
const raw = await readAll(req);
|
|
37
|
+
const back = raw === undefined ? undefined : await harbor.deliver(pk, raw);
|
|
38
|
+
if (back === undefined)
|
|
39
|
+
return Response.json({ error: 'no reach for that pk' }, { status: 404, headers: OPEN });
|
|
40
|
+
return new Response(new Uint8Array(back), { headers: { 'content-type': 'application/octet-stream', ...OPEN } });
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type Context = {
|
|
2
|
+
parentURL?: string;
|
|
3
|
+
conditions: string[];
|
|
4
|
+
importAttributes: Record<string, string>;
|
|
5
|
+
};
|
|
6
|
+
type Resolved = {
|
|
7
|
+
url: string;
|
|
8
|
+
format?: string | null;
|
|
9
|
+
shortCircuit?: boolean;
|
|
10
|
+
};
|
|
11
|
+
type Next = (specifier: string, context: Context) => Promise<Resolved>;
|
|
12
|
+
export declare function initialize(data: {
|
|
13
|
+
parent: string;
|
|
14
|
+
}): void;
|
|
15
|
+
export declare function resolve(specifier: string, context: Context, next: Next): Promise<Resolved>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
let parent = '';
|
|
2
|
+
export function initialize(data) {
|
|
3
|
+
parent = data.parent;
|
|
4
|
+
}
|
|
5
|
+
const bare = (s) => !s.startsWith('.') && !s.startsWith('/') && !/^[a-z]+:/i.test(s);
|
|
6
|
+
export async function resolve(specifier, context, next) {
|
|
7
|
+
try {
|
|
8
|
+
return await next(specifier, context);
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
const missing = e.code === 'ERR_MODULE_NOT_FOUND';
|
|
12
|
+
const from = context.parentURL ?? '';
|
|
13
|
+
if (missing && parent && bare(specifier) && from.startsWith('file:') && !from.includes('/node_modules/'))
|
|
14
|
+
return next(specifier, { ...context, parentURL: parent });
|
|
15
|
+
throw e;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The seal: what a store does to a ward's bytes before they rest on a data
|
|
3
|
+
// store, under one key the secret store holds. One piece with three users,
|
|
4
|
+
// the edge under the platform's secret, the daemon under a key from its
|
|
5
|
+
// environment, the phone under a key from the Keychain. AES-GCM under a
|
|
6
|
+
// 32-byte key, a fresh nonce each time, nonce and ciphertext together as
|
|
7
|
+
// hex. WebCrypto only, so it runs wherever a store does.
|
|
8
|
+
import { arithmetic } from '@nervur-org/nervur/ward';
|
|
9
|
+
const { hex, unhex } = arithmetic;
|
|
10
|
+
// Bytes as the platform's crypto wants them: over a plain ArrayBuffer.
|
|
11
|
+
const plain = (b) => new Uint8Array(b);
|
|
12
|
+
export async function sealKey(secret) {
|
|
13
|
+
if (!/^[0-9a-f]{64}$/.test(secret))
|
|
14
|
+
throw new Error('a seal key is 32 bytes as hex');
|
|
15
|
+
return crypto.subtle.importKey('raw', plain(unhex(secret)), 'AES-GCM', false, ['encrypt', 'decrypt']);
|
|
16
|
+
}
|
|
17
|
+
export async function seal(key, bytes) {
|
|
18
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
19
|
+
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plain(bytes)));
|
|
20
|
+
return hex(iv) + hex(ct);
|
|
21
|
+
}
|
|
22
|
+
export async function open(key, sealed) {
|
|
23
|
+
const b = plain(unhex(sealed));
|
|
24
|
+
return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: b.subarray(0, 12) }, key, b.subarray(12)));
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Kept, Store, WardRecord } from '@nervur-org/nervur/harbor';
|
|
2
|
+
export type Files = {
|
|
3
|
+
exists(path: string): Promise<boolean>;
|
|
4
|
+
read(path: string): Promise<string>;
|
|
5
|
+
write(path: string, text: string): Promise<void>;
|
|
6
|
+
remove(path: string): Promise<void>;
|
|
7
|
+
mkdir(path: string): Promise<void>;
|
|
8
|
+
rmdir(path: string): Promise<void>;
|
|
9
|
+
list(path: string): Promise<string[]>;
|
|
10
|
+
};
|
|
11
|
+
export type Secrets = {
|
|
12
|
+
get(item: string): Promise<string | undefined>;
|
|
13
|
+
set(item: string, value: string): Promise<void>;
|
|
14
|
+
remove(item: string): Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare class Sealed implements Store {
|
|
17
|
+
#private;
|
|
18
|
+
readonly harbor: string;
|
|
19
|
+
private constructor();
|
|
20
|
+
static open(harbor: string, files: Files, secrets: Secrets): Promise<Sealed>;
|
|
21
|
+
static wipe(harbor: string, files: Files, secrets: Secrets): Promise<void>;
|
|
22
|
+
list(): Promise<string[]>;
|
|
23
|
+
load(name: string): Promise<Kept | undefined>;
|
|
24
|
+
put(name: string, kept: Kept): Promise<void>;
|
|
25
|
+
save(name: string, partition: Record<string, unknown>): Promise<void>;
|
|
26
|
+
record(name: string, record: WardRecord): Promise<void>;
|
|
27
|
+
take(name: string): Promise<Kept | undefined>;
|
|
28
|
+
hints(): Promise<Record<string, string>>;
|
|
29
|
+
hint(pk: string, url: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function inMemory(): {
|
|
32
|
+
files: Files;
|
|
33
|
+
secrets: Secrets;
|
|
34
|
+
disk: Map<string, string>;
|
|
35
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The sealed store: the store an app holds on a device in a hand, the phone
|
|
3
|
+
// and the desk alike. One key per harbor in the platform's secret store,
|
|
4
|
+
// this device only, so it never travels in a backup; one sealed file per
|
|
5
|
+
// ward, seed, partition and record as one blob, in a folder no backup
|
|
6
|
+
// copies. The seal is `seal.ts`, the same piece the daemon and the edge use.
|
|
7
|
+
//
|
|
8
|
+
// <files>/<harbor>/wards/<name>.sealed
|
|
9
|
+
// <files>/<harbor>/reach.json
|
|
10
|
+
// <secrets>: quo-<harbor> 32 bytes as hex
|
|
11
|
+
//
|
|
12
|
+
// Custody is the backup question, and opening the store answers it: a key
|
|
13
|
+
// with no folder is a reinstall, and the stale key is deleted; a folder
|
|
14
|
+
// with no key is a restore to another device, and the unreadable files are
|
|
15
|
+
// deleted. Either way the harbor starts fresh and there is never a twin.
|
|
16
|
+
//
|
|
17
|
+
// What a platform gives is two small things, files and secrets, and the
|
|
18
|
+
// rest is here once: the phone hands its plugins, the desk hands its core,
|
|
19
|
+
// and the memory harbor hands a map, which is where the rule is proven.
|
|
20
|
+
import { arithmetic } from '@nervur-org/nervur/ward';
|
|
21
|
+
import { sealKey, seal, open } from './seal.js';
|
|
22
|
+
const { hex, unhex } = arithmetic;
|
|
23
|
+
export class Sealed {
|
|
24
|
+
harbor;
|
|
25
|
+
#files;
|
|
26
|
+
#key;
|
|
27
|
+
#queues = new Map();
|
|
28
|
+
constructor(harbor, files, key) {
|
|
29
|
+
this.harbor = harbor;
|
|
30
|
+
this.#files = files;
|
|
31
|
+
this.#key = key;
|
|
32
|
+
}
|
|
33
|
+
// Open one harbor's store, applying the custody rule, minting the key and
|
|
34
|
+
// the folder when this is a fresh install.
|
|
35
|
+
static async open(harbor, files, secrets) {
|
|
36
|
+
const item = `quo-${harbor}`;
|
|
37
|
+
const root = `quo/${harbor}`;
|
|
38
|
+
let secret = await secrets.get(item);
|
|
39
|
+
const folder = await files.exists(root);
|
|
40
|
+
if (secret && !folder) {
|
|
41
|
+
await secrets.remove(item);
|
|
42
|
+
secret = undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!secret && folder)
|
|
45
|
+
await files.rmdir(root);
|
|
46
|
+
if (!secret) {
|
|
47
|
+
secret = hex(crypto.getRandomValues(new Uint8Array(32)));
|
|
48
|
+
await secrets.set(item, secret);
|
|
49
|
+
}
|
|
50
|
+
if (!(await files.exists(`${root}/wards`)))
|
|
51
|
+
await files.mkdir(`${root}/wards`);
|
|
52
|
+
return new Sealed(harbor, files, await sealKey(secret));
|
|
53
|
+
}
|
|
54
|
+
// Everything this harbor has, key and files: what a person does by hand
|
|
55
|
+
// to leave a device, and what a test does between two openings.
|
|
56
|
+
static async wipe(harbor, files, secrets) {
|
|
57
|
+
await secrets.remove(`quo-${harbor}`);
|
|
58
|
+
if (await files.exists(`quo/${harbor}`))
|
|
59
|
+
await files.rmdir(`quo/${harbor}`);
|
|
60
|
+
}
|
|
61
|
+
#path(name) {
|
|
62
|
+
return `quo/${this.harbor}/wards/${name}.sealed`;
|
|
63
|
+
}
|
|
64
|
+
async #read(name) {
|
|
65
|
+
const data = await this.#files.read(this.#path(name));
|
|
66
|
+
return JSON.parse(new TextDecoder().decode(await open(this.#key, data.trim())));
|
|
67
|
+
}
|
|
68
|
+
// A sealed ward is rewritten whole, so the read sits inside the queue
|
|
69
|
+
// with the write: two changes to one ward never lose each other's part.
|
|
70
|
+
#keep(name, change) {
|
|
71
|
+
const next = (this.#queues.get(name) ?? Promise.resolve()).then(async () => {
|
|
72
|
+
const blob = change((await this.#files.exists(this.#path(name))) ? await this.#read(name) : undefined);
|
|
73
|
+
const sealed = await seal(this.#key, new TextEncoder().encode(JSON.stringify(blob)));
|
|
74
|
+
await this.#files.write(this.#path(name), sealed + '\n');
|
|
75
|
+
});
|
|
76
|
+
this.#queues.set(name, next.catch(() => { }));
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
async list() {
|
|
80
|
+
const files = await this.#files.list(`quo/${this.harbor}/wards`);
|
|
81
|
+
return files.filter((f) => f.endsWith('.sealed')).map((f) => f.slice(0, -'.sealed'.length));
|
|
82
|
+
}
|
|
83
|
+
async load(name) {
|
|
84
|
+
if (!(await this.#files.exists(this.#path(name))))
|
|
85
|
+
return undefined;
|
|
86
|
+
const b = await this.#read(name);
|
|
87
|
+
return { seed: unhex(b.seed), partition: b.partition, record: b.record };
|
|
88
|
+
}
|
|
89
|
+
async put(name, kept) {
|
|
90
|
+
if (await this.#files.exists(this.#path(name)))
|
|
91
|
+
throw new Error(`ward ${name} already exists on this device`);
|
|
92
|
+
return this.#keep(name, () => ({ seed: hex(kept.seed), partition: kept.partition, record: kept.record }));
|
|
93
|
+
}
|
|
94
|
+
async save(name, partition) {
|
|
95
|
+
if (!(await this.#files.exists(this.#path(name))))
|
|
96
|
+
return; // a name not kept is nothing
|
|
97
|
+
return this.#keep(name, (b) => ({ ...b, partition }));
|
|
98
|
+
}
|
|
99
|
+
async record(name, record) {
|
|
100
|
+
if (!(await this.#files.exists(this.#path(name))))
|
|
101
|
+
return;
|
|
102
|
+
return this.#keep(name, (b) => ({ ...b, record }));
|
|
103
|
+
}
|
|
104
|
+
async take(name) {
|
|
105
|
+
const kept = await this.load(name);
|
|
106
|
+
if (!kept)
|
|
107
|
+
return undefined;
|
|
108
|
+
await this.#queues.get(name);
|
|
109
|
+
await this.#files.remove(this.#path(name));
|
|
110
|
+
return kept;
|
|
111
|
+
}
|
|
112
|
+
async hints() {
|
|
113
|
+
const p = `quo/${this.harbor}/reach.json`;
|
|
114
|
+
if (!(await this.#files.exists(p)))
|
|
115
|
+
return {};
|
|
116
|
+
return JSON.parse(await this.#files.read(p));
|
|
117
|
+
}
|
|
118
|
+
async hint(pk, url) {
|
|
119
|
+
const all = await this.hints();
|
|
120
|
+
all[pk] = url;
|
|
121
|
+
await this.#files.write(`quo/${this.harbor}/reach.json`, JSON.stringify(all) + '\n');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Files and secrets in memory, for a suite and for a box that keeps
|
|
125
|
+
// nothing: the custody rule is proven here once and holds on every
|
|
126
|
+
// platform that hands the two.
|
|
127
|
+
export function inMemory() {
|
|
128
|
+
const disk = new Map();
|
|
129
|
+
const dirs = new Set();
|
|
130
|
+
const vault = new Map();
|
|
131
|
+
const under = (dir) => [...disk.keys()].filter((k) => k.startsWith(`${dir}/`));
|
|
132
|
+
const files = {
|
|
133
|
+
exists: async (p) => disk.has(p) || dirs.has(p),
|
|
134
|
+
read: async (p) => {
|
|
135
|
+
const v = disk.get(p);
|
|
136
|
+
if (v === undefined)
|
|
137
|
+
throw new Error(`no such file: ${p}`);
|
|
138
|
+
return v;
|
|
139
|
+
},
|
|
140
|
+
write: async (p, text) => void disk.set(p, text),
|
|
141
|
+
remove: async (p) => void disk.delete(p),
|
|
142
|
+
mkdir: async (p) => {
|
|
143
|
+
const parts = p.split('/');
|
|
144
|
+
for (let i = 1; i <= parts.length; i += 1)
|
|
145
|
+
dirs.add(parts.slice(0, i).join('/'));
|
|
146
|
+
},
|
|
147
|
+
rmdir: async (p) => {
|
|
148
|
+
for (const k of under(p))
|
|
149
|
+
disk.delete(k);
|
|
150
|
+
for (const d of [...dirs])
|
|
151
|
+
if (d === p || d.startsWith(`${p}/`))
|
|
152
|
+
dirs.delete(d);
|
|
153
|
+
},
|
|
154
|
+
list: async (p) => under(p).map((k) => k.slice(p.length + 1)).filter((k) => !k.includes('/')),
|
|
155
|
+
};
|
|
156
|
+
const secrets = {
|
|
157
|
+
get: async (i) => vault.get(i),
|
|
158
|
+
set: async (i, v) => void vault.set(i, v),
|
|
159
|
+
remove: async (i) => void vault.delete(i),
|
|
160
|
+
};
|
|
161
|
+
return { files, secrets, disk };
|
|
162
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BeingClass, JsonObject } from '@nervur-org/nervur';
|
|
2
|
+
import { Notifications } from '../dock/index.ts';
|
|
3
|
+
import { BrowserHarbor } from './browser.ts';
|
|
4
|
+
import { type Files, type Secrets } from './sealed.ts';
|
|
5
|
+
export type Core = {
|
|
6
|
+
invoke(cmd: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
7
|
+
};
|
|
8
|
+
export declare class DeskNotifications extends Notifications {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(stance: ConstructorParameters<typeof Notifications>[0], core: Core);
|
|
11
|
+
notify(args: JsonObject): Promise<JsonObject>;
|
|
12
|
+
}
|
|
13
|
+
export declare const deskLent: Record<string, string>;
|
|
14
|
+
export declare function deskFiles(core: Core): Files;
|
|
15
|
+
export declare function deskSecrets(core: Core): Secrets;
|
|
16
|
+
export declare function deskHarbor(core: Core, name?: string, classes?: Record<string, BeingClass>): Promise<BrowserHarbor>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NOTIFICATIONS, Notifications } from '../dock/index.js';
|
|
2
|
+
import { BrowserHarbor } from './browser.js';
|
|
3
|
+
import { Sealed } from './sealed.js';
|
|
4
|
+
export class DeskNotifications extends Notifications {
|
|
5
|
+
#core;
|
|
6
|
+
constructor(stance, core) {
|
|
7
|
+
super(stance);
|
|
8
|
+
this.#core = core;
|
|
9
|
+
}
|
|
10
|
+
async notify(args) {
|
|
11
|
+
const title = typeof args.title === 'string' ? args.title : '';
|
|
12
|
+
const body = typeof args.body === 'string' ? args.body : '';
|
|
13
|
+
await this.#core.invoke('notify', { title, body });
|
|
14
|
+
return { shown: true };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// What a desk lends: its notification center, under the name every box
|
|
18
|
+
// lends it under. Location is what the platform gives, which on a desk is
|
|
19
|
+
// nothing, so it is not lent rather than lent and dumb. No timer either: a
|
|
20
|
+
// desk holds presences and no home, so no Clock lives on it and a Timer
|
|
21
|
+
// there would arm for nobody. A Timer stands only on a box that hosts a
|
|
22
|
+
// home.
|
|
23
|
+
export const deskLent = { [NOTIFICATIONS]: 'Notifications' };
|
|
24
|
+
// The two seams over the core's commands. Paths are relative to the app's
|
|
25
|
+
// data folder, and the core refuses one that climbs out of it.
|
|
26
|
+
export function deskFiles(core) {
|
|
27
|
+
return {
|
|
28
|
+
exists: (path) => core.invoke('file_exists', { path }),
|
|
29
|
+
read: (path) => core.invoke('file_read', { path }),
|
|
30
|
+
write: async (path, text) => void (await core.invoke('file_write', { path, text })),
|
|
31
|
+
remove: async (path) => void (await core.invoke('file_remove', { path })),
|
|
32
|
+
mkdir: async (path) => void (await core.invoke('dir_make', { path })),
|
|
33
|
+
rmdir: async (path) => void (await core.invoke('dir_remove', { path })),
|
|
34
|
+
list: (path) => core.invoke('dir_list', { path }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function deskSecrets(core) {
|
|
38
|
+
return {
|
|
39
|
+
get: async (item) => (await core.invoke('secret_get', { item })) ?? undefined,
|
|
40
|
+
set: async (item, value) => void (await core.invoke('secret_set', { item, value })),
|
|
41
|
+
remove: async (item) => void (await core.invoke('secret_remove', { item })),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// The desk app's one harbor, booted, lending what the desk has to its
|
|
45
|
+
// wards. The core hands the faculty its notification center by being in
|
|
46
|
+
// the class body the harbor holds, which is the one place a terrain's
|
|
47
|
+
// body may hold a live object.
|
|
48
|
+
export async function deskHarbor(core, name = 'quo', classes = {}) {
|
|
49
|
+
class Center extends DeskNotifications {
|
|
50
|
+
constructor(stance) {
|
|
51
|
+
super(stance, core);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const store = await Sealed.open(name, deskFiles(core), deskSecrets(core));
|
|
55
|
+
const h = new BrowserHarbor(store, { Notifications: Center, ...classes });
|
|
56
|
+
await h.stand(deskLent);
|
|
57
|
+
await h.boot();
|
|
58
|
+
return h;
|
|
59
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Hosted } from '@nervur-org/nervur/harbor';
|
|
2
|
+
export type Row = {
|
|
3
|
+
class: string | null;
|
|
4
|
+
public: boolean;
|
|
5
|
+
digest: string | null;
|
|
6
|
+
absent?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function beingsOf(h: Hosted): Promise<Record<string, Row>>;
|
|
9
|
+
export declare function publicKey(h: Hosted): Promise<string | null>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Every being of the ward, by key: the ones a door holds this run and the
|
|
2
|
+
// ones whose class did not construct, which are hers all the same.
|
|
3
|
+
export async function beingsOf(h) {
|
|
4
|
+
const bp = (await h.ask());
|
|
5
|
+
return bp.notes?.beings ?? {};
|
|
6
|
+
}
|
|
7
|
+
// The key of the one public being, or null. The ward marks her and the ward
|
|
8
|
+
// is asked, because no harbor reads that mark and none should: which being
|
|
9
|
+
// answers strangers is a decision, and a harbor decides nothing.
|
|
10
|
+
export async function publicKey(h) {
|
|
11
|
+
for (const [key, row] of Object.entries(await beingsOf(h)))
|
|
12
|
+
if (row.public)
|
|
13
|
+
return key;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Harbor } from '@nervur-org/nervur/harbor';
|
|
2
|
+
import type { Invitation, JsonObject } from '@nervur-org/nervur';
|
|
3
|
+
import { type Joined, type World } from './worlds.ts';
|
|
4
|
+
import { type Bell } from './wake.ts';
|
|
5
|
+
import type { Token } from '../dock/index.ts';
|
|
6
|
+
import type { Config } from './tab.ts';
|
|
7
|
+
export type Learn = (at: string) => Promise<Config>;
|
|
8
|
+
export declare const learn: Learn;
|
|
9
|
+
export type Dialing = Harbor & {
|
|
10
|
+
dial(url: string): unknown;
|
|
11
|
+
};
|
|
12
|
+
export type Ear = () => Promise<Token | null>;
|
|
13
|
+
export type AppOptions = {
|
|
14
|
+
harbor: Dialing;
|
|
15
|
+
kept: Joined;
|
|
16
|
+
root: HTMLElement;
|
|
17
|
+
learn?: Learn;
|
|
18
|
+
opened?: {
|
|
19
|
+
at: string;
|
|
20
|
+
invitation?: Invitation;
|
|
21
|
+
};
|
|
22
|
+
ear?: Ear;
|
|
23
|
+
bell?: Bell;
|
|
24
|
+
};
|
|
25
|
+
export type Running = {
|
|
26
|
+
show(pk: string): Promise<void>;
|
|
27
|
+
open(at: string, invitation?: Invitation): Promise<void>;
|
|
28
|
+
woken(): Promise<JsonObject[]>;
|
|
29
|
+
worlds(): World[];
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
export declare function app(o: AppOptions): Promise<Running>;
|