@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,179 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The device's half of the doorbell, worn by every shell: the tab's, the
|
|
3
|
+
// worker's behind a closed tab, and the app's. A ring carries nothing but a
|
|
4
|
+
// sealed line, so waking is three moves and the first two are the harbor's
|
|
5
|
+
// own. The device tells its dialers it is back, since a socket that died
|
|
6
|
+
// while it slept died silently; it asks each relation in each world what it
|
|
7
|
+
// missed, which is an ordinary ask on an ordinary standing, sealed like
|
|
8
|
+
// every other; and it composes what the human sees from the objects that
|
|
9
|
+
// came back, here, on the device, from bytes no push service ever held.
|
|
10
|
+
//
|
|
11
|
+
// Each object lands on the avatar exactly where a push would have landed,
|
|
12
|
+
// so a page that is open shows what it missed in its own notices as well.
|
|
13
|
+
//
|
|
14
|
+
// A phone and a desk hold a doorbell of their own at a bell, and hand the
|
|
15
|
+
// platform's token to that doorbell and to nobody else; what a world is
|
|
16
|
+
// given is a standing at that doorbell and the banner key it seals to. A
|
|
17
|
+
// tab hands its browser endpoint instead and keeps no doorbell, because a
|
|
18
|
+
// browser endpoint is rung only by whoever holds the VAPID secret the
|
|
19
|
+
// subscription was made under, and that secret is the world's own.
|
|
20
|
+
import { isInvitation, isSilence, isWord } from '@nervur-org/nervur';
|
|
21
|
+
import { DOCK, CONSOLE, NOTIFICATIONS, facultyOf } from '../dock/index.js';
|
|
22
|
+
import { banner } from '../dock/push.js';
|
|
23
|
+
import { localName, relations } from './worlds.js';
|
|
24
|
+
// The token a browser's push service hands a page: the endpoint, and the
|
|
25
|
+
// two keys of the subscription made at it. RFC 8291 encrypts a body to
|
|
26
|
+
// those keys, which is how a ring carries the world's own line, so a token
|
|
27
|
+
// without them is a device that is woken and told nothing.
|
|
28
|
+
export const webToken = (endpoint, p256dh, auth) => ({ kind: 'web', endpoint, ...(p256dh && auth ? { p256dh, auth } : {}) });
|
|
29
|
+
// The id this device holds its doorbell under, on the box's own being.
|
|
30
|
+
export const DOORBELL = 'doorbell';
|
|
31
|
+
const said = (out) => (out === undefined || isSilence(out) || isWord(out) || out === null || typeof out !== 'object' || Array.isArray(out) ? null : out);
|
|
32
|
+
// The box's own being, as the device's own code reaches its own object in
|
|
33
|
+
// its own process. Nothing of this crosses an edge.
|
|
34
|
+
const box = (harbor) => harbor.wards.get(DOCK)?.being(CONSOLE);
|
|
35
|
+
// The doorbell this device holds, taken the first time and kept after that.
|
|
36
|
+
// A stranger phone knocks the bell's public being, is booted a doorbell of
|
|
37
|
+
// its own and comes away with the one way in; the standing lives on the
|
|
38
|
+
// box's own being, because a doorbell is the device's and not any world's.
|
|
39
|
+
//
|
|
40
|
+
// Null is a device with no bell behind it, which is what a build that names
|
|
41
|
+
// none looks like and what a bell that will not answer looks like. Each of
|
|
42
|
+
// them works in every other way.
|
|
43
|
+
export async function doorbell(harbor, bell) {
|
|
44
|
+
if (!bell)
|
|
45
|
+
return null;
|
|
46
|
+
const ward = harbor.wards.get(DOCK);
|
|
47
|
+
const me = box(harbor);
|
|
48
|
+
if (!ward || !me)
|
|
49
|
+
return null;
|
|
50
|
+
if (!me.standings[DOORBELL]) {
|
|
51
|
+
const hello = said(await me.standings.knock({ ward: bell.pk }, 'hello', {}));
|
|
52
|
+
const inv = hello?.invitation;
|
|
53
|
+
if (!isInvitation(inv))
|
|
54
|
+
return null;
|
|
55
|
+
// The ward's own knock, which knocks and takes in one act: an
|
|
56
|
+
// invitation is taken only where it was answered, so the two halves are
|
|
57
|
+
// never split.
|
|
58
|
+
const took = said(await ward.ask('knock', { being: CONSOLE, id: DOORBELL, invitation: inv }));
|
|
59
|
+
await ward.save();
|
|
60
|
+
if (took?.taken !== DOORBELL)
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const mine = me.standings[DOORBELL];
|
|
64
|
+
if (!mine)
|
|
65
|
+
return null;
|
|
66
|
+
return {
|
|
67
|
+
keep: async (token) => {
|
|
68
|
+
const out = said(await mine.ask('keep', { token }));
|
|
69
|
+
await ward.save();
|
|
70
|
+
return typeof out?.kept === 'string';
|
|
71
|
+
},
|
|
72
|
+
admit: async () => {
|
|
73
|
+
// The banner key is minted on the first ask and kept, and a terrain
|
|
74
|
+
// whose banners are drawn by a second process plants the secret half
|
|
75
|
+
// where that process reads it while this is happening.
|
|
76
|
+
const keys = facultyOf(harbor, NOTIFICATIONS);
|
|
77
|
+
const key = keys ? said(await keys.banner({}))?.banner : undefined;
|
|
78
|
+
if (typeof key !== 'string')
|
|
79
|
+
return null;
|
|
80
|
+
const out = said(await mine.ask('admit', {}));
|
|
81
|
+
await ward.save();
|
|
82
|
+
const inv = out?.invitation;
|
|
83
|
+
return isInvitation(inv) ? { invitation: inv, banner: key } : null;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// One world told how to wake this device. What a world is given is a
|
|
88
|
+
// standing at this device's doorbell and the banner key it seals to, never
|
|
89
|
+
// the platform's token: the same token at every world is a bearer
|
|
90
|
+
// credential, and a standing is not one.
|
|
91
|
+
//
|
|
92
|
+
// A standing does not rotate, so it is handed over once. The world is asked
|
|
93
|
+
// what it already holds and told nothing when it holds a doorbell, which is
|
|
94
|
+
// what keeps the doorbell's list of the worlds it allows a list a human can
|
|
95
|
+
// read and cut, rather than one row per time a page was opened. A browser
|
|
96
|
+
// endpoint is said every time, because a push service may hand out a new
|
|
97
|
+
// one at any moment and a stale one rings an empty house.
|
|
98
|
+
//
|
|
99
|
+
// A device with no bell behind it and no browser endpoint says nothing, and
|
|
100
|
+
// works in every other way.
|
|
101
|
+
export async function told(rel, at, token) {
|
|
102
|
+
const has = said(await rel.call('bell', {}));
|
|
103
|
+
const kept = typeof has?.kept === 'string' ? has.kept : null;
|
|
104
|
+
if (at) {
|
|
105
|
+
if (kept === 'doorbell')
|
|
106
|
+
return kept;
|
|
107
|
+
const got = await at.admit();
|
|
108
|
+
if (!got)
|
|
109
|
+
return null;
|
|
110
|
+
const out = said(await rel.call('bell', { invitation: got.invitation, banner: got.banner }));
|
|
111
|
+
return typeof out?.kept === 'string' ? out.kept : null;
|
|
112
|
+
}
|
|
113
|
+
if (token?.kind !== 'web')
|
|
114
|
+
return null;
|
|
115
|
+
const out = said(await rel.call('bell', { token }));
|
|
116
|
+
return typeof out?.kept === 'string' ? out.kept : null;
|
|
117
|
+
}
|
|
118
|
+
// The tab's doorbell: the worker registered on this world's origin, whose
|
|
119
|
+
// scope is the folder it is served from and so covers every world page
|
|
120
|
+
// there, and a subscription at whatever push service this browser keeps.
|
|
121
|
+
// The world's key goes over as the base64url string it already is. Null is
|
|
122
|
+
// a device with no doorbell, which is a browser with no push service, a
|
|
123
|
+
// human who was asked and said no, or a human who has not been asked; each
|
|
124
|
+
// of them works in every other way, and the last is why `ask` exists.
|
|
125
|
+
export async function subscribe(web, key, ask = false) {
|
|
126
|
+
const workers = globalThis.navigator?.serviceWorker;
|
|
127
|
+
if (!workers || !('PushManager' in globalThis) || !('Notification' in globalThis))
|
|
128
|
+
return null;
|
|
129
|
+
try {
|
|
130
|
+
const registration = await workers.register(`${web}/sw.js`, { type: 'module' });
|
|
131
|
+
await workers.ready;
|
|
132
|
+
if (Notification.permission === 'default' && ask)
|
|
133
|
+
await Notification.requestPermission();
|
|
134
|
+
if (Notification.permission !== 'granted')
|
|
135
|
+
return null;
|
|
136
|
+
const held = await registration.pushManager.getSubscription();
|
|
137
|
+
const sub = held ?? (await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key }));
|
|
138
|
+
// The keys are read off the subscription as it writes them itself,
|
|
139
|
+
// base64url, which is the form a world encrypts to.
|
|
140
|
+
const keys = sub.toJSON().keys;
|
|
141
|
+
return webToken(sub.endpoint, keys?.p256dh, keys?.auth);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null; // a browser with no push service behind it: no doorbell, and everything else stands
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// What every relation in every world here missed, in the order it was
|
|
148
|
+
// pushed, drained and gone. A world this device is not in has no relation
|
|
149
|
+
// and answers nothing.
|
|
150
|
+
export async function drained(harbor, pks) {
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const pk of pks) {
|
|
153
|
+
const ward = harbor.wards.get(localName(pk));
|
|
154
|
+
if (!ward)
|
|
155
|
+
continue;
|
|
156
|
+
for (const rel of relations(ward)) {
|
|
157
|
+
const got = await rel.avatar.call('missed');
|
|
158
|
+
const objects = got.missed;
|
|
159
|
+
if (!Array.isArray(objects))
|
|
160
|
+
continue;
|
|
161
|
+
for (const o of objects) {
|
|
162
|
+
if (o === null || typeof o !== 'object' || Array.isArray(o))
|
|
163
|
+
continue;
|
|
164
|
+
rel.avatar.notify(o); // where the push would have arrived
|
|
165
|
+
out.push(o);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
await ward.save(); // her keys rotated on that ask, whatever it answered
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
// What the human reads on the lock screen, composed from the objects and
|
|
173
|
+
// from nothing else. It is the same line a world seals into a ring, made
|
|
174
|
+
// here from what actually crossed, and the human is told how many are
|
|
175
|
+
// behind the last one.
|
|
176
|
+
export function notice(objects) {
|
|
177
|
+
const line = banner(objects[objects.length - 1] ?? {});
|
|
178
|
+
return { ...line, body: objects.length > 1 ? `${line.body} (and ${objects.length - 1} more)` : line.body };
|
|
179
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Handler } from '../harbor/http.ts';
|
|
2
|
+
import type { Hosted } from '@nervur-org/nervur/harbor';
|
|
3
|
+
import { type Apps } from './apps.ts';
|
|
4
|
+
export type { Apps };
|
|
5
|
+
export type Folder = {
|
|
6
|
+
wearing(): Promise<{
|
|
7
|
+
tokens: boolean;
|
|
8
|
+
fonts: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
design(file: string): Promise<Uint8Array<ArrayBuffer> | string | null>;
|
|
11
|
+
serving(): Promise<boolean>;
|
|
12
|
+
beings(): Promise<string | null>;
|
|
13
|
+
};
|
|
14
|
+
export declare const BARE_FOLDER: Folder;
|
|
15
|
+
export type WebOptions = {
|
|
16
|
+
at: {
|
|
17
|
+
quo: string;
|
|
18
|
+
web: string;
|
|
19
|
+
};
|
|
20
|
+
code: {
|
|
21
|
+
tab: string;
|
|
22
|
+
worker: string;
|
|
23
|
+
};
|
|
24
|
+
folder?: Folder;
|
|
25
|
+
apps?: Apps | null;
|
|
26
|
+
bell?: string | null;
|
|
27
|
+
shown?: (ward: string) => boolean;
|
|
28
|
+
};
|
|
29
|
+
export declare const TAB_CODE = "tab/index.js";
|
|
30
|
+
export declare const TAB_SOURCE = "tab/index.ts";
|
|
31
|
+
export declare function webRoute(harbor: {
|
|
32
|
+
wards: ReadonlyMap<string, Hosted>;
|
|
33
|
+
}, o: WebOptions): Handler;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { DOCK } from '../dock/index.js';
|
|
2
|
+
import { publicKey } from '../harbor/ward.js';
|
|
3
|
+
import { ANDROID, APPLE, association } from './apps.js';
|
|
4
|
+
import { door } from './door.js';
|
|
5
|
+
import { BARE, CSS } from './style.js';
|
|
6
|
+
// A world with nothing beside its wards: no design, no code of its own. It
|
|
7
|
+
// is what a terrain hands the route when it holds no folder at all.
|
|
8
|
+
export const BARE_FOLDER = {
|
|
9
|
+
wearing: () => Promise.resolve({ tokens: false, fonts: false }),
|
|
10
|
+
design: () => Promise.resolve(null),
|
|
11
|
+
serving: () => Promise.resolve(false),
|
|
12
|
+
beings: () => Promise.resolve(null),
|
|
13
|
+
};
|
|
14
|
+
// The paths under `/web` that are the exchange's, never a ward's name.
|
|
15
|
+
const RESERVED_PATHS = new Set(['allow', 'tab.js', 'beings.js', 'sw.js', 'design', '.well-known']);
|
|
16
|
+
// The design a world wears: `design/` in the harbor folder, the compiled
|
|
17
|
+
// face of one design and the fonts it speaks in, served under this route
|
|
18
|
+
// and linked by every page after the kit. A harbor with none wears the
|
|
19
|
+
// kit's own face. Only these files, by these names, and nothing else in
|
|
20
|
+
// the folder is reachable.
|
|
21
|
+
const DESIGN = 'design';
|
|
22
|
+
const DESIGN_FILES = { 'tokens.css': 'text/css; charset=utf-8', 'fonts.css': 'text/css; charset=utf-8' };
|
|
23
|
+
const FONT = /^fonts\/[\w.-]+\.woff2$/;
|
|
24
|
+
// The origins a Quo app's webview speaks from: Tauri's on the desk,
|
|
25
|
+
// Capacitor's on iOS, and the plain loopback Android's webview uses. They
|
|
26
|
+
// are the only origins a world tells its config to, and the fetch is a
|
|
27
|
+
// plain GET asking for JSON, which no browser preflights.
|
|
28
|
+
const APP_ORIGINS = new Set(['tauri://localhost', 'capacitor://localhost', 'http://localhost', 'https://localhost']);
|
|
29
|
+
const appAsking = (req) => {
|
|
30
|
+
const origin = req.headers.get('origin');
|
|
31
|
+
if (origin === null || !APP_ORIGINS.has(origin))
|
|
32
|
+
return null;
|
|
33
|
+
return (req.headers.get('accept') ?? '').includes('application/json') ? origin : null;
|
|
34
|
+
};
|
|
35
|
+
// The world's code for the tab: a module in the harbor folder, the twin of
|
|
36
|
+
// `classes/index.ts`, exporting by name the classes the tab boots into its
|
|
37
|
+
// local ward for a world of this harbor. Served bundled as `beings.js` on
|
|
38
|
+
// this origin, and one origin is one world's code. It is a built file: the
|
|
39
|
+
// estate that deploys this harbor bundles its own tab source and puts the
|
|
40
|
+
// result in the folder, because nothing here runs a compiler.
|
|
41
|
+
export const TAB_CODE = 'tab/index.js';
|
|
42
|
+
// The source that built file comes from. It is named here for one reason:
|
|
43
|
+
// a folder holding it and no built file has a build that did not run, and
|
|
44
|
+
// that is said at the path rather than passed over in silence.
|
|
45
|
+
export const TAB_SOURCE = 'tab/index.ts';
|
|
46
|
+
export function webRoute(harbor, o) {
|
|
47
|
+
const folder = o.folder ?? BARE_FOLDER;
|
|
48
|
+
const quoOrigin = (() => {
|
|
49
|
+
try {
|
|
50
|
+
const u = new URL(o.at.quo);
|
|
51
|
+
return `${u.origin} ${u.origin.replace(/^http/, 'ws')}`;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
const host = (() => {
|
|
58
|
+
try {
|
|
59
|
+
return new URL(o.at.web).host;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return o.at.web;
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
// `worker-src` is named because it falls back to `default-src`, which is
|
|
66
|
+
// none: without it the page's own service worker, the ear that hears the
|
|
67
|
+
// doorbell while the tab is closed, is refused by the policy.
|
|
68
|
+
const policy = `default-src 'none'; script-src 'self'; worker-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self' ${quoOrigin}; form-action 'self'; base-uri 'none'; frame-ancestors 'none'`;
|
|
69
|
+
const design = async () => {
|
|
70
|
+
const worn = await folder.wearing();
|
|
71
|
+
if (!worn.tokens)
|
|
72
|
+
return `<style>${CSS}</style>`;
|
|
73
|
+
const fonts = worn.fonts ? `<link rel="stylesheet" href="${o.at.web}/${DESIGN}/fonts.css">` : '';
|
|
74
|
+
return `<style>${BARE}</style>${fonts}<link rel="stylesheet" href="${o.at.web}/${DESIGN}/tokens.css">`;
|
|
75
|
+
};
|
|
76
|
+
const shell = async (body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>quo</title>
|
|
77
|
+
${await design()}
|
|
78
|
+
</head><body>${body}</body></html>`;
|
|
79
|
+
const esc = (v) => v.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c);
|
|
80
|
+
const html = async (status, body) => new Response(await shell(body), { status, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': policy, 'referrer-policy': 'no-referrer' } });
|
|
81
|
+
// Each ward is asked which being it marks public: the harbor reads no
|
|
82
|
+
// partition and the mark is the ward's, so the config a page is handed is
|
|
83
|
+
// built from describes, one per world on this harbor.
|
|
84
|
+
// The box's own ward is not a world and is never offered as one: its
|
|
85
|
+
// beings are the harbor's, reached by `lend` from this box alone, and a
|
|
86
|
+
// page that listed it would be offering a stranger a device's own things.
|
|
87
|
+
// A ward is at its door for a stranger's page only when it is shown: the
|
|
88
|
+
// public being still answers a proof and a link still knocks, but a hidden
|
|
89
|
+
// ward's page is the plain door page and its root line is not written.
|
|
90
|
+
const shown = o.shown ?? (() => true);
|
|
91
|
+
const wards = async () => Object.fromEntries(await Promise.all([...harbor.wards].filter(([n]) => n !== DOCK).map(async ([n, h]) => [n, { pk: h.pk, public: shown(n) && (await publicKey(h)) !== null }])));
|
|
92
|
+
const config = async (ward, being) => ({ quo: o.at.quo, web: o.at.web, wards: await wards(), beings: await folder.serving(), ...(o.bell ? { bell: o.bell } : {}), ...(ward ? { ward } : {}), ...(being ? { being } : {}) });
|
|
93
|
+
const tab = async (ward, being) => {
|
|
94
|
+
return `<script id="quo" type="application/json">${JSON.stringify(await config(ward, being)).replace(/</g, '\\u003c')}</script><script type="module" src="${o.at.web}/tab.js"></script>`;
|
|
95
|
+
};
|
|
96
|
+
// The same config the tab is handed in its page, for an app that is not a
|
|
97
|
+
// tab: it learns a world by this one fetch and never loads the bundle,
|
|
98
|
+
// because its code is its own bundle and a world's code never runs on a
|
|
99
|
+
// phone. Only the app origins are answered, and only this; a browser on
|
|
100
|
+
// any other origin is told nothing, and gets the page instead.
|
|
101
|
+
const asJson = async (origin, ward) => new Response(JSON.stringify(await config(ward)), { headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', 'access-control-allow-origin': origin, vary: 'origin' } });
|
|
102
|
+
return async (req, rest) => {
|
|
103
|
+
const parts = rest.split('/').filter(Boolean);
|
|
104
|
+
if (req.method === 'GET' && (rest === '' || rest === '/')) {
|
|
105
|
+
// the harbor's own config, every ward on it: an app that was sent a
|
|
106
|
+
// link into a ward it does not know learns the whole harbor at once
|
|
107
|
+
const app = appAsking(req);
|
|
108
|
+
if (app !== null)
|
|
109
|
+
return asJson(app);
|
|
110
|
+
const open = [];
|
|
111
|
+
for (const [n, h] of harbor.wards) {
|
|
112
|
+
const key = shown(n) ? await publicKey(h) : null;
|
|
113
|
+
if (key !== null)
|
|
114
|
+
open.push(`<li class="nv-item nv-person"><span class="nv-avatar">${esc(n.charAt(0).toUpperCase())}</span><div class="nv-stack"><p class="nv-t-body"><a href="${o.at.web}/${encodeURIComponent(n)}">${esc(n)}</a></p><p class="nv-t-quiet">${esc(key)} at the door · <span class="nv-key">${h.pk.slice(0, 16)}…</span></p></div></li>`);
|
|
115
|
+
}
|
|
116
|
+
const list = open.join('');
|
|
117
|
+
return html(200, `${list ? `<main data-door><div class="nv-hero"><h1 class="nv-t-title">worlds</h1><p class="nv-t-lead">The worlds on this box that show a face. Each is entered by a link someone sends you.</p><ul class="nv-list" role="list">${list}</ul></div></main>` : door({ host })}${await tab()}`);
|
|
118
|
+
}
|
|
119
|
+
// Where the two platforms look before they let an app claim this
|
|
120
|
+
// hostname's links. The estate names its apps and nothing else is
|
|
121
|
+
// decided here: the file is the claim, the platform verifies it over
|
|
122
|
+
// TLS, and a hostname that names no app is a hostname whose links open
|
|
123
|
+
// the tab. The exchange page is not the app's on purpose: a credential
|
|
124
|
+
// exchange belongs in the browser that started it.
|
|
125
|
+
if (req.method === 'GET' && (rest === APPLE || rest === ANDROID)) {
|
|
126
|
+
const body = association(o.apps, rest, ['/allow*']);
|
|
127
|
+
if (body === null)
|
|
128
|
+
return undefined;
|
|
129
|
+
return new Response(body, { headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } });
|
|
130
|
+
}
|
|
131
|
+
if (rest === '/tab.js' && req.method === 'GET')
|
|
132
|
+
return new Response(o.code.tab, { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' } });
|
|
133
|
+
// The service worker: the page's ear while the page is closed. It is
|
|
134
|
+
// served from the root of this route so that its scope covers every
|
|
135
|
+
// world's page, and its config is written above the bundle, since a
|
|
136
|
+
// worker has no page to be handed a JSON script in. A world with no
|
|
137
|
+
// doorbell serves no worker, and its pages register none.
|
|
138
|
+
if (rest === '/sw.js' && req.method === 'GET') {
|
|
139
|
+
if (!o.bell)
|
|
140
|
+
return undefined;
|
|
141
|
+
const said = `self.quo=${JSON.stringify(await config()).replace(/</g, '\\u003c')};\n${o.code.worker}`;
|
|
142
|
+
return new Response(said, { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store', 'service-worker-allowed': '/' } });
|
|
143
|
+
}
|
|
144
|
+
// the design this world wears, when the harbor folder holds one: its
|
|
145
|
+
// stylesheet, its font faces, and the font files those name
|
|
146
|
+
if (rest.startsWith(`/${DESIGN}/`) && req.method === 'GET') {
|
|
147
|
+
const file = rest.slice(DESIGN.length + 2);
|
|
148
|
+
const type = DESIGN_FILES[file] ?? (FONT.test(file) ? 'font/woff2' : null);
|
|
149
|
+
if (type === null)
|
|
150
|
+
return undefined;
|
|
151
|
+
const bytes = await folder.design(file);
|
|
152
|
+
if (bytes === null)
|
|
153
|
+
return undefined;
|
|
154
|
+
return new Response(bytes, { headers: { 'content-type': type, 'cache-control': 'public, max-age=86400' } });
|
|
155
|
+
}
|
|
156
|
+
// the world's code for the tab, when the harbor folder holds any; a
|
|
157
|
+
// harbor with none has no such path. A folder holding the source and no
|
|
158
|
+
// built file is a build that did not run, and the page is told so here
|
|
159
|
+
// rather than quietly booting a world with no classes in it.
|
|
160
|
+
if (rest === '/beings.js' && req.method === 'GET') {
|
|
161
|
+
if (!(await folder.serving()))
|
|
162
|
+
return undefined;
|
|
163
|
+
const own = await folder.beings();
|
|
164
|
+
if (own === null)
|
|
165
|
+
return new Response(`this world holds ${TAB_SOURCE} and no built ${TAB_CODE}; whoever deploys it emits that file`, { status: 500, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } });
|
|
166
|
+
return new Response(own, { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' } });
|
|
167
|
+
}
|
|
168
|
+
const wardName = parts[0] ?? '';
|
|
169
|
+
if (!wardName || RESERVED_PATHS.has(wardName))
|
|
170
|
+
return undefined;
|
|
171
|
+
const hosted = harbor.wards.get(wardName);
|
|
172
|
+
if (!hosted)
|
|
173
|
+
return html(404, `<h1>no such world</h1><p>no ward named ${esc(wardName)} on this harbor.</p>`);
|
|
174
|
+
if (parts.length === 1 && req.method === 'GET') {
|
|
175
|
+
const app = appAsking(req);
|
|
176
|
+
return app === null ? html(200, await tab(wardName)) : asJson(app, wardName);
|
|
177
|
+
}
|
|
178
|
+
// a being's page: one standing the user being carries, by its id, as the whole page. No router:
|
|
179
|
+
// the path is a key, and the tab paints nothing for a key her describe does not carry
|
|
180
|
+
if (parts.length === 2 && req.method === 'GET' && /^[\w.-]{1,80}$/.test(parts[1]))
|
|
181
|
+
return html(200, await tab(wardName, parts[1]));
|
|
182
|
+
return undefined;
|
|
183
|
+
};
|
|
184
|
+
}
|