@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,527 @@
|
|
|
1
|
+
import { screenSide } from './screen.js';
|
|
2
|
+
import { domSurface } from './dom.js';
|
|
3
|
+
import { guest } from './guest.js';
|
|
4
|
+
import { door, held as heldPage } from './door.js';
|
|
5
|
+
import { USER } from '../beings/avatar.js';
|
|
6
|
+
import { DOCK, CONSOLE, NOTIFICATIONS, settled } from '../dock/index.js';
|
|
7
|
+
import { fresh, joined, localName, ordered, relations, world } from './worlds.js';
|
|
8
|
+
import { giveWorld, takeWorld } from './move.js';
|
|
9
|
+
import { invitationOf, reachOf } from '../beings/link.js';
|
|
10
|
+
import { doorbell, drained, told, notice as noticeOf } from './wake.js';
|
|
11
|
+
// The fetch, as every terrain but a test does it: the world's address, its
|
|
12
|
+
// own origin asking, and JSON. A world answers this to an app and to
|
|
13
|
+
// nobody else, `web.ts`.
|
|
14
|
+
export const learn = async (at) => {
|
|
15
|
+
const r = await fetch(at, { headers: { accept: 'application/json' } });
|
|
16
|
+
if (!r.ok)
|
|
17
|
+
throw new Error(`${at} answered ${r.status}`);
|
|
18
|
+
return (await r.json());
|
|
19
|
+
};
|
|
20
|
+
const el = (tag, text = '', attrs = {}) => {
|
|
21
|
+
const e = document.createElement(tag);
|
|
22
|
+
if (text)
|
|
23
|
+
e.textContent = text;
|
|
24
|
+
for (const [k, v] of Object.entries(attrs))
|
|
25
|
+
e.setAttribute(k, v);
|
|
26
|
+
return e;
|
|
27
|
+
};
|
|
28
|
+
export async function app(o) {
|
|
29
|
+
const look = o.learn ?? learn;
|
|
30
|
+
const nav = el('nav', '', { class: 'worlds nv-strip' });
|
|
31
|
+
const status = el('p', 'booting', { class: 'state nv-t-quiet' });
|
|
32
|
+
const screen = el('div');
|
|
33
|
+
o.root.append(nav, status, screen);
|
|
34
|
+
const say = (s) => (status.textContent = s);
|
|
35
|
+
// A device is one harbor, and this browser may already hold it in
|
|
36
|
+
// another tab. That tab is the device; this one is the held page, as a
|
|
37
|
+
// world's tab is, and it offers nothing that takes the lock.
|
|
38
|
+
try {
|
|
39
|
+
await o.harbor.boot();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
status.remove();
|
|
43
|
+
screen.innerHTML = heldPage({ host: location.host });
|
|
44
|
+
const none = async () => { };
|
|
45
|
+
return { show: none, open: none, woken: async () => [], worlds: () => [], close: none };
|
|
46
|
+
}
|
|
47
|
+
// Every world this device holds is dialed, not only the one on screen: a
|
|
48
|
+
// world reaches a device by the socket the device opened, and a device
|
|
49
|
+
// that dialed only what it is looking at could be woken by nothing else.
|
|
50
|
+
const dialed = new Set();
|
|
51
|
+
const dial = (quo) => {
|
|
52
|
+
if (dialed.has(quo))
|
|
53
|
+
return;
|
|
54
|
+
dialed.add(quo);
|
|
55
|
+
o.harbor.dial(quo);
|
|
56
|
+
};
|
|
57
|
+
let list = o.kept.read();
|
|
58
|
+
let showing = null;
|
|
59
|
+
let side = null;
|
|
60
|
+
// How this device is woken, asked of the terrain and never decided here.
|
|
61
|
+
let heard = null;
|
|
62
|
+
const listen = async () => {
|
|
63
|
+
if (!o.ear)
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
heard = await o.ear();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
heard = null; // a device whose ear will not answer is a device with no doorbell, and everything else stands
|
|
70
|
+
}
|
|
71
|
+
return heard;
|
|
72
|
+
};
|
|
73
|
+
// This device's doorbell, at the bell the build named. It is taken once
|
|
74
|
+
// and kept, and the platform's token goes to it and to nowhere else.
|
|
75
|
+
let bellAt = null;
|
|
76
|
+
// Telling one world how to wake this device, which is `wake.ts`'s and not
|
|
77
|
+
// the shell's: the shell holds the doorbell and the ear, and says where
|
|
78
|
+
// the ward is saved.
|
|
79
|
+
const tell = async (ward, rel) => {
|
|
80
|
+
await told(rel.avatar, bellAt, heard);
|
|
81
|
+
await ward.save(); // her keys rotated on those asks
|
|
82
|
+
};
|
|
83
|
+
// What the switcher switches between is the human's versions of
|
|
84
|
+
// themselves, one world each and a ward each: an employee at one world, a
|
|
85
|
+
// consultant at a second, family at a third, and never one of them
|
|
86
|
+
// knowing about another. So a device with one version has no switcher at
|
|
87
|
+
// all, since a control over one thing can only do nothing. It is taken
|
|
88
|
+
// out of the page rather than hidden, because the stylesheet lays it out
|
|
89
|
+
// by class and a class beats the hidden attribute.
|
|
90
|
+
//
|
|
91
|
+
// Its last entry is never a world: it is this device, the box's own ward
|
|
92
|
+
// shown through the Console, where a human sees what the box lends and
|
|
93
|
+
// leaves a world. A box that lends nothing has no such page.
|
|
94
|
+
const switcher = () => {
|
|
95
|
+
nav.replaceChildren();
|
|
96
|
+
const device = hasDevice();
|
|
97
|
+
if (list.length + (device ? 1 : 0) < 2) {
|
|
98
|
+
nav.remove();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!nav.isConnected)
|
|
102
|
+
o.root.insertBefore(nav, o.root.firstChild);
|
|
103
|
+
for (const w of ordered(list)) {
|
|
104
|
+
const b = el('button', w.name || w.pk.slice(0, 8), { type: 'button', class: w.home ? 'home nv-chip' : 'nv-chip' });
|
|
105
|
+
if (w.pk === showing)
|
|
106
|
+
b.setAttribute('aria-current', 'true');
|
|
107
|
+
b.onclick = () => void show(w.pk).catch((e) => say(String(e)));
|
|
108
|
+
nav.append(b);
|
|
109
|
+
}
|
|
110
|
+
if (device) {
|
|
111
|
+
const b = el('button', 'this device', { type: 'button', class: 'device nv-chip' });
|
|
112
|
+
if (showing === DEVICE)
|
|
113
|
+
b.setAttribute('aria-current', 'true');
|
|
114
|
+
b.onclick = () => void showDevice().catch((e) => say(String(e)));
|
|
115
|
+
nav.append(b);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const remember = (w) => {
|
|
119
|
+
list = joined(list, w);
|
|
120
|
+
o.kept.write(list);
|
|
121
|
+
switcher();
|
|
122
|
+
};
|
|
123
|
+
// One world on screen: its ward, whoever was in it last, and her page.
|
|
124
|
+
// A world whose address will not answer is still a world: the ward and
|
|
125
|
+
// the standings are here, and the switcher keeps it.
|
|
126
|
+
const show = async (pk) => {
|
|
127
|
+
const w = list.find((x) => x.pk === pk);
|
|
128
|
+
if (!w)
|
|
129
|
+
return void say('no such world');
|
|
130
|
+
showing = pk;
|
|
131
|
+
await side?.close();
|
|
132
|
+
side = null;
|
|
133
|
+
screen.replaceChildren();
|
|
134
|
+
held.remove();
|
|
135
|
+
if (!status.isConnected)
|
|
136
|
+
o.root.insertBefore(status, screen);
|
|
137
|
+
say(`reaching ${w.name || w.at}`);
|
|
138
|
+
switcher();
|
|
139
|
+
let cfg = null;
|
|
140
|
+
try {
|
|
141
|
+
cfg = await look(w.at);
|
|
142
|
+
dial(cfg.quo);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
say(`${w.name || w.at} did not answer: ${e instanceof Error ? e.message : String(e)}`);
|
|
146
|
+
}
|
|
147
|
+
const ward = await world(o.harbor, pk);
|
|
148
|
+
const named = cfg ? Object.entries(cfg.wards).find(([, v]) => v.pk === pk) : undefined;
|
|
149
|
+
if (cfg && named)
|
|
150
|
+
remember({ pk, at: w.at, name: named[0] });
|
|
151
|
+
const rels = relations(ward);
|
|
152
|
+
const back = rels[rels.length - 1];
|
|
153
|
+
if (back) {
|
|
154
|
+
const had = await back.avatar.tools();
|
|
155
|
+
await ward.save(); // her keys rotated on that ask, whatever it answered
|
|
156
|
+
if ('asks' in had)
|
|
157
|
+
return inside(ward, back, `in, at ${w.name || named?.[0] || w.at}`, had);
|
|
158
|
+
say(`not in (${had.error})`);
|
|
159
|
+
}
|
|
160
|
+
await atDoor(ward, pk, w, named?.[1]?.public === true);
|
|
161
|
+
};
|
|
162
|
+
const inside = async (ward, rel, notice, bp) => {
|
|
163
|
+
// Whether this world is a home is the user being's word, in her notes,
|
|
164
|
+
// and the switcher puts home first
|
|
165
|
+
const w = list.find((x) => x.pk === showing);
|
|
166
|
+
if (w)
|
|
167
|
+
remember({ ...w, home: isHome(bp) });
|
|
168
|
+
await side?.close();
|
|
169
|
+
// A device has no surface of its own, and nothing of it is handed to a
|
|
170
|
+
// world: what this box can do is beings of its own ward, and the being
|
|
171
|
+
// of this world who lives on this box holds standings at them. What a
|
|
172
|
+
// world reaches is her, on the standing it already holds.
|
|
173
|
+
const note = notice;
|
|
174
|
+
status.remove();
|
|
175
|
+
screen.replaceChildren();
|
|
176
|
+
// The world's page address travels with the side, since that is what an
|
|
177
|
+
// invitation she answers becomes a link on: a being knows no routes and
|
|
178
|
+
// the side is where one is composed, in an app exactly as in a tab.
|
|
179
|
+
side = await screenSide(rel.avatar, domSurface(screen), {
|
|
180
|
+
after: () => ward.save(),
|
|
181
|
+
notice: note,
|
|
182
|
+
// A world that ended the relation is over, so the list of worlds is
|
|
183
|
+
// put up with its way out in reach. Nothing is dropped: one answer at
|
|
184
|
+
// one moment cannot delete a ward, and the far world is never asked.
|
|
185
|
+
ended: async () => {
|
|
186
|
+
leaving();
|
|
187
|
+
screen.after(held);
|
|
188
|
+
},
|
|
189
|
+
...(w ? { at: w.at } : {}),
|
|
190
|
+
});
|
|
191
|
+
// This world learns how to wake this device, and hands over whatever it
|
|
192
|
+
// pushed while nobody could be reached. Both are ordinary asks on this
|
|
193
|
+
// relation, sealed like every other, and each object lands on the avatar
|
|
194
|
+
// exactly where a push would have landed.
|
|
195
|
+
await tell(ward, rel);
|
|
196
|
+
if (showing && showing !== DEVICE)
|
|
197
|
+
await drained(o.harbor, [showing]);
|
|
198
|
+
};
|
|
199
|
+
// At the door of a world this device has no standing in: the public
|
|
200
|
+
// being's page when there is one, and a form whose answer is an
|
|
201
|
+
// invitation is the way in; the door page when there is nobody, since
|
|
202
|
+
// then a link is the only way and there is nothing to type.
|
|
203
|
+
const atDoor = async (ward, pk, w, isPublic) => {
|
|
204
|
+
if (!isPublic) {
|
|
205
|
+
status.remove();
|
|
206
|
+
screen.innerHTML = door({ world: w.name || undefined, host: hostOf(w.at) });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
say(`at the door of ${w.name || hostOf(w.at)}`);
|
|
210
|
+
const rel = await fresh(ward);
|
|
211
|
+
const gate = guest(rel.avatar, pk);
|
|
212
|
+
const s = await screenSide(gate, domSurface(screen), {
|
|
213
|
+
after: () => ward.save(),
|
|
214
|
+
notice: `a guest at ${w.name || hostOf(w.at)}: what she shows strangers`,
|
|
215
|
+
at: w.at,
|
|
216
|
+
admit: async (inv) => {
|
|
217
|
+
await s.close();
|
|
218
|
+
await admit(ward, inv, `in, as a guest of ${w.name || hostOf(w.at)}`);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
side = s;
|
|
222
|
+
};
|
|
223
|
+
// The box's own ward, as its harbor stood it: the Console carrying every
|
|
224
|
+
// faculty this box lends. A box that lends nothing stands none, and there
|
|
225
|
+
// is no page of its own to show.
|
|
226
|
+
const deviceWard = async () => {
|
|
227
|
+
const ward = o.harbor.wards.get(DOCK);
|
|
228
|
+
if (!ward || !ward.being(CONSOLE))
|
|
229
|
+
return null;
|
|
230
|
+
return ward;
|
|
231
|
+
};
|
|
232
|
+
const hasDevice = () => o.harbor.wards.get(DOCK)?.being(CONSOLE) !== undefined;
|
|
233
|
+
// The device shown by itself: the last entry of the switcher, and the
|
|
234
|
+
// whole screen of a device that has joined no world. A fresh install has
|
|
235
|
+
// no world's page to carry her forms, and a first screen that says only
|
|
236
|
+
// "open a link someone sent you" is an app with permissions in its
|
|
237
|
+
// manifest and nothing that ever asks for them, which is a store
|
|
238
|
+
// reviewer's first question and a human's second.
|
|
239
|
+
//
|
|
240
|
+
// She is shown by the ordinary screen and by no other way: one avatar in
|
|
241
|
+
// the box's own ward holding her standing under the name a world's user
|
|
242
|
+
// being would stand under, so the side sees what it always sees. The root
|
|
243
|
+
// places it, because the root is the box.
|
|
244
|
+
const deviceScreen = async () => {
|
|
245
|
+
const ward = await deviceWard();
|
|
246
|
+
if (!ward)
|
|
247
|
+
return null;
|
|
248
|
+
const had = relations(ward)[0];
|
|
249
|
+
if (had)
|
|
250
|
+
return { ward, rel: had };
|
|
251
|
+
const rel = await fresh(ward);
|
|
252
|
+
const inv = await ward.ask('invite', { being: CONSOLE, id: rel.key });
|
|
253
|
+
if (!inv || typeof inv !== 'object')
|
|
254
|
+
throw new Error('this device is already held under that name');
|
|
255
|
+
const took = (await ward.ask('knock', { being: rel.key, id: USER, invitation: inv }));
|
|
256
|
+
if (took.taken !== USER)
|
|
257
|
+
throw new Error(took.error ?? 'the shell could not take this device');
|
|
258
|
+
await ward.save();
|
|
259
|
+
return { ward, rel };
|
|
260
|
+
};
|
|
261
|
+
// The device's page: what the box lends, as forms that ask the faculty
|
|
262
|
+
// and show its answer, a refusal included; the worlds this device is in,
|
|
263
|
+
// each with a way out; and the one sentence about custody a human needs.
|
|
264
|
+
const showDevice = async () => {
|
|
265
|
+
let found = null;
|
|
266
|
+
try {
|
|
267
|
+
found = await deviceScreen();
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
return void say(`this device would not answer: ${e instanceof Error ? e.message : String(e)}`);
|
|
271
|
+
}
|
|
272
|
+
if (!found)
|
|
273
|
+
return void say('this device lends nothing');
|
|
274
|
+
const { ward, rel } = found;
|
|
275
|
+
showing = DEVICE;
|
|
276
|
+
await side?.close();
|
|
277
|
+
side = null;
|
|
278
|
+
screen.replaceChildren();
|
|
279
|
+
if (!status.isConnected)
|
|
280
|
+
o.root.insertBefore(status, screen);
|
|
281
|
+
say('');
|
|
282
|
+
switcher();
|
|
283
|
+
side = await screenSide(rel.avatar, domSurface(screen), { after: () => ward.save(), notice: CUSTODY });
|
|
284
|
+
leaving();
|
|
285
|
+
screen.after(held);
|
|
286
|
+
};
|
|
287
|
+
// The worlds this device is in, each with a way out and a way to another
|
|
288
|
+
// device. Leaving is the local ward dropped whole, seed and every
|
|
289
|
+
// standing in it, and the world forgotten from the list: the far world
|
|
290
|
+
// is never asked and never told, as WORLDS says of a lost phone, and it
|
|
291
|
+
// sees one device gone. Moving is the Courier: this device offers, the
|
|
292
|
+
// human carries the offer to the other device, and this copy goes the
|
|
293
|
+
// moment the other one says it landed.
|
|
294
|
+
const held = el('section', '', { class: 'held' });
|
|
295
|
+
const offer = el('p', '', { class: 'invitation' });
|
|
296
|
+
let giving = null;
|
|
297
|
+
const stopGiving = async () => {
|
|
298
|
+
const g = giving;
|
|
299
|
+
giving = null;
|
|
300
|
+
offer.textContent = '';
|
|
301
|
+
await g?.close();
|
|
302
|
+
leaving();
|
|
303
|
+
};
|
|
304
|
+
const give = async (w) => {
|
|
305
|
+
if (giving)
|
|
306
|
+
return stopGiving();
|
|
307
|
+
const cfg = await look(w.at);
|
|
308
|
+
dial(cfg.quo);
|
|
309
|
+
giving = await giveWorld(o.harbor, w, cfg.quo);
|
|
310
|
+
offer.textContent = giving.offer;
|
|
311
|
+
leaving();
|
|
312
|
+
void giving.moved.then(async () => {
|
|
313
|
+
// it is on the other device now, and this one is out of the world
|
|
314
|
+
giving = null;
|
|
315
|
+
offer.textContent = '';
|
|
316
|
+
list = list.filter((x) => x.pk !== w.pk);
|
|
317
|
+
o.kept.write(list);
|
|
318
|
+
leaving();
|
|
319
|
+
switcher();
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
const leaving = () => {
|
|
323
|
+
held.replaceChildren();
|
|
324
|
+
if (list.length === 0)
|
|
325
|
+
return;
|
|
326
|
+
held.append(el('p', 'the worlds this device is in', { class: 'quiet' }));
|
|
327
|
+
for (const w of list) {
|
|
328
|
+
const name = w.name || w.pk.slice(0, 8);
|
|
329
|
+
const b = el('button', `leave ${name}`, { type: 'button', 'data-leave': w.pk, class: 'nv-button', 'data-variant': 'danger' });
|
|
330
|
+
b.onclick = () => void leave(w.pk).catch((e) => say(String(e)));
|
|
331
|
+
const m = el('button', giving ? 'stop offering it' : `move ${name} to another device`, { type: 'button', 'data-give': w.pk, class: 'nv-button', 'data-variant': 'quiet' });
|
|
332
|
+
m.onclick = () => void give(w).catch((e) => say(String(e)));
|
|
333
|
+
held.append(b, m);
|
|
334
|
+
}
|
|
335
|
+
held.append(offer);
|
|
336
|
+
};
|
|
337
|
+
const leave = async (pk) => {
|
|
338
|
+
const name = localName(pk);
|
|
339
|
+
if (o.harbor.wards.has(name))
|
|
340
|
+
await o.harbor.drop(name);
|
|
341
|
+
list = list.filter((w) => w.pk !== pk);
|
|
342
|
+
o.kept.write(list);
|
|
343
|
+
leaving();
|
|
344
|
+
switcher();
|
|
345
|
+
};
|
|
346
|
+
const cold = async () => {
|
|
347
|
+
await showDevice();
|
|
348
|
+
say('no worlds yet: open a link someone sent you');
|
|
349
|
+
};
|
|
350
|
+
const admit = async (ward, inv, notice) => {
|
|
351
|
+
say('knocking');
|
|
352
|
+
const rel = await fresh(ward);
|
|
353
|
+
let got = await rel.avatar.join(inv);
|
|
354
|
+
await ward.save(); // the knock went through her own door, which the harbor never sees
|
|
355
|
+
if (!('asks' in got))
|
|
356
|
+
return void say(`not in: ${got.error}`);
|
|
357
|
+
// What came back is the far being as she was during the knock. If
|
|
358
|
+
// anything on this device was still landing then, she read a device that
|
|
359
|
+
// can do nothing and carries that, and the empty ask is the only way
|
|
360
|
+
// anyone learns a shape moved: nothing pushes. So the page asks once
|
|
361
|
+
// more, with this box quiet, and paints what is there rather than what
|
|
362
|
+
// was there a moment ago. A page paints once, so asking once more here
|
|
363
|
+
// is the difference between a device with its faculties on screen and a
|
|
364
|
+
// device that looks empty until the person navigates away and back.
|
|
365
|
+
await settled();
|
|
366
|
+
const now = await rel.avatar.tools();
|
|
367
|
+
if ('asks' in now)
|
|
368
|
+
got = now;
|
|
369
|
+
await inside(ward, rel, notice, got);
|
|
370
|
+
};
|
|
371
|
+
// A link, whether the app was opened by it or handed it while running.
|
|
372
|
+
// The invitation names its ward by pk, so the address in the link is
|
|
373
|
+
// where to reach that world and never which world it is.
|
|
374
|
+
const open = async (at, invitation) => {
|
|
375
|
+
let cfg = null;
|
|
376
|
+
try {
|
|
377
|
+
cfg = await look(at);
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
return void say(`${at} did not answer: ${e instanceof Error ? e.message : String(e)}`);
|
|
381
|
+
}
|
|
382
|
+
dial(cfg.quo);
|
|
383
|
+
const pk = invitation?.ward ?? (cfg.ward ? cfg.wards[cfg.ward]?.pk : undefined);
|
|
384
|
+
if (!pk)
|
|
385
|
+
return void say('that link names no world');
|
|
386
|
+
const name = Object.entries(cfg.wards).find(([, v]) => v.pk === pk)?.[0] ?? '';
|
|
387
|
+
remember({ pk, at, name });
|
|
388
|
+
if (!invitation)
|
|
389
|
+
return show(pk);
|
|
390
|
+
showing = pk;
|
|
391
|
+
switcher();
|
|
392
|
+
if (!status.isConnected)
|
|
393
|
+
o.root.insertBefore(status, screen);
|
|
394
|
+
screen.replaceChildren();
|
|
395
|
+
await admit(await world(o.harbor, pk), invitation, 'in, by the link');
|
|
396
|
+
};
|
|
397
|
+
// Taking a world from another device. The device that holds the world
|
|
398
|
+
// shows an offer, an invitation next to the rendezvous to reach it
|
|
399
|
+
// through; the human carries it here, by eye or by camera, and the ward
|
|
400
|
+
// arrives whole: the avatars, the standings, everything won there, with
|
|
401
|
+
// the far world never asked and never told. This device dials the note
|
|
402
|
+
// first, because a device reaches a pk only through a listener it holds
|
|
403
|
+
// a socket to, and one that has joined no world dials nothing at all.
|
|
404
|
+
const taking = el('form', '', { class: 'move' });
|
|
405
|
+
const field = el('input', '', { name: 'offer', placeholder: 'the offer the other device shows', autocomplete: 'off', spellcheck: 'false' });
|
|
406
|
+
const said = el('p', '', { class: 'quiet' });
|
|
407
|
+
taking.append(el('label', 'take a world from another device'), field, el('button', 'take it', { type: 'submit', class: 'nv-button', 'data-variant': 'quiet' }), said);
|
|
408
|
+
o.root.append(taking);
|
|
409
|
+
taking.onsubmit = (e) => {
|
|
410
|
+
e.preventDefault();
|
|
411
|
+
const inv = invitationOf(field.value);
|
|
412
|
+
if (!inv)
|
|
413
|
+
return void (said.textContent = 'that is not an offer');
|
|
414
|
+
const note = reachOf(field.value);
|
|
415
|
+
if (note)
|
|
416
|
+
dial(note);
|
|
417
|
+
said.textContent = 'taking it…';
|
|
418
|
+
void (async () => {
|
|
419
|
+
const out = await takeWorld(o.harbor, inv);
|
|
420
|
+
if ('error' in out)
|
|
421
|
+
return void (said.textContent = out.error);
|
|
422
|
+
said.textContent = '';
|
|
423
|
+
field.value = '';
|
|
424
|
+
remember(out.world);
|
|
425
|
+
await show(out.world.pk);
|
|
426
|
+
})().catch((why) => (said.textContent = String(why)));
|
|
427
|
+
};
|
|
428
|
+
// The device is back. A ring carries nothing, so waking is three moves and
|
|
429
|
+
// the harbor already made the first: it told its dialers, since a socket
|
|
430
|
+
// that died while the device slept died silently. What is left is to say
|
|
431
|
+
// the token again, because a platform may have handed out a new one, and
|
|
432
|
+
// to ask every world what it missed.
|
|
433
|
+
//
|
|
434
|
+
// Every world and not only the one on screen: a device is woken for
|
|
435
|
+
// whichever world had something to say, and one that drained only what it
|
|
436
|
+
// was looking at would leave the rest waiting for a second wake that may
|
|
437
|
+
// never come.
|
|
438
|
+
const woken = async () => {
|
|
439
|
+
await listen();
|
|
440
|
+
// The token goes to this device's doorbell and to nobody else, which is
|
|
441
|
+
// the whole of a rotation: one write, and no world is told. A world
|
|
442
|
+
// that has no doorbell yet, because it was down when this device joined
|
|
443
|
+
// it, is given one here.
|
|
444
|
+
if (heard)
|
|
445
|
+
await bellAt?.keep(heard);
|
|
446
|
+
for (const w of list) {
|
|
447
|
+
const ward = o.harbor.wards.get(localName(w.pk));
|
|
448
|
+
if (!ward)
|
|
449
|
+
continue;
|
|
450
|
+
for (const rel of relations(ward))
|
|
451
|
+
await tell(ward, rel);
|
|
452
|
+
}
|
|
453
|
+
const got = await drained(o.harbor, list.map((w) => w.pk));
|
|
454
|
+
// What the human reads, composed here from what crossed sealed. Only
|
|
455
|
+
// when nobody was looking: a page that is open already shows every one
|
|
456
|
+
// of these where the push would have landed, and a banner over it would
|
|
457
|
+
// be the same news twice.
|
|
458
|
+
if (got.length && globalThis.document?.visibilityState !== 'visible')
|
|
459
|
+
await shown(noticeOf(got));
|
|
460
|
+
return got;
|
|
461
|
+
};
|
|
462
|
+
// The line the human reads, shown by the box's own notifications being.
|
|
463
|
+
// A box that lends none shows nothing, and a refusal is hers to answer;
|
|
464
|
+
// the shell asks and does not read the answer, since the news is already
|
|
465
|
+
// on the page where the push landed.
|
|
466
|
+
const shown = async (line) => {
|
|
467
|
+
const dock = o.harbor.wards.get(DOCK);
|
|
468
|
+
if (!dock)
|
|
469
|
+
return;
|
|
470
|
+
await dock.ask('ask', { being: NOTIFICATIONS, method: 'notify', args: line }).catch(() => { });
|
|
471
|
+
};
|
|
472
|
+
// The device ward before anything is dialed: a harbor announces the wards
|
|
473
|
+
// it hosts when it opens a socket, and a ward made after that is a pk the
|
|
474
|
+
// far world cannot knock. A device whose ward will not stand is a device
|
|
475
|
+
// with no grounds and a shell that works in every other way, so nothing
|
|
476
|
+
// here stops the app; the cold screen is where that is said out loud.
|
|
477
|
+
try {
|
|
478
|
+
await deviceWard();
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
/* said by the cold screen, which is the one place a device speaks for itself */
|
|
482
|
+
}
|
|
483
|
+
await listen();
|
|
484
|
+
// The bell is dialed like a world, because a device reaches a pk only
|
|
485
|
+
// through a listener it holds a socket to; then the doorbell, taken the
|
|
486
|
+
// first time this device ever ran and kept from then on, and the
|
|
487
|
+
// platform's token said to it.
|
|
488
|
+
if (o.bell) {
|
|
489
|
+
dial(o.bell.at);
|
|
490
|
+
bellAt = await doorbell(o.harbor, o.bell);
|
|
491
|
+
if (heard)
|
|
492
|
+
await bellAt?.keep(heard);
|
|
493
|
+
}
|
|
494
|
+
switcher();
|
|
495
|
+
if (o.opened)
|
|
496
|
+
await open(o.opened.at, o.opened.invitation);
|
|
497
|
+
else if (list.length)
|
|
498
|
+
await show(list[list.length - 1].pk);
|
|
499
|
+
else
|
|
500
|
+
await cold();
|
|
501
|
+
return {
|
|
502
|
+
show,
|
|
503
|
+
open,
|
|
504
|
+
woken,
|
|
505
|
+
worlds: () => list,
|
|
506
|
+
close: async () => {
|
|
507
|
+
await side?.close();
|
|
508
|
+
side = null;
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
// What the switcher shows when the device's own page is up: not a pk, and
|
|
513
|
+
// never a world.
|
|
514
|
+
const DEVICE = 'device';
|
|
515
|
+
// A world is a home when the being reached there says so: the user being
|
|
516
|
+
// writes `home` in her notes, and no other being does.
|
|
517
|
+
const isHome = (bp) => bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) && bp.notes.home === true;
|
|
518
|
+
// The one sentence about custody a human needs, on the device's page.
|
|
519
|
+
const CUSTODY = 'this device: what it lends, and the worlds it is in. A reinstall starts fresh, so a world that holds nothing of you elsewhere must invite you again.';
|
|
520
|
+
const hostOf = (at) => {
|
|
521
|
+
try {
|
|
522
|
+
return new URL(at).host;
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return at;
|
|
526
|
+
}
|
|
527
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Apps = {
|
|
2
|
+
apple: string[];
|
|
3
|
+
android: {
|
|
4
|
+
package: string;
|
|
5
|
+
fingerprints: string[];
|
|
6
|
+
}[];
|
|
7
|
+
};
|
|
8
|
+
export declare const APPLE = "/.well-known/apple-app-site-association";
|
|
9
|
+
export declare const ANDROID = "/.well-known/assetlinks.json";
|
|
10
|
+
export declare function association(apps: Apps | null | undefined, path: string, notApp?: readonly string[]): string | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The two paths, which are the platforms' and not ours.
|
|
2
|
+
export const APPLE = '/.well-known/apple-app-site-association';
|
|
3
|
+
export const ANDROID = '/.well-known/assetlinks.json';
|
|
4
|
+
// The claim `path` should answer with, or `null` when this hostname claims
|
|
5
|
+
// no app there: no apps at all, none of that platform, or a path that is
|
|
6
|
+
// neither file. `null` is a 404 and not an empty file, because an empty
|
|
7
|
+
// claim and no claim mean the same thing to a phone and only one of them
|
|
8
|
+
// is honest about a hostname that ships no app.
|
|
9
|
+
//
|
|
10
|
+
// `notApp` is the paths on this hostname that are the browser's even when
|
|
11
|
+
// the app is installed. A world's route has one, the credential exchange,
|
|
12
|
+
// which belongs in the browser that started it; the app's own domain has
|
|
13
|
+
// none, since it has no pages at all.
|
|
14
|
+
export function association(apps, path, notApp = []) {
|
|
15
|
+
if (!apps)
|
|
16
|
+
return null;
|
|
17
|
+
if (path === ANDROID) {
|
|
18
|
+
if (!apps.android.length)
|
|
19
|
+
return null;
|
|
20
|
+
return JSON.stringify(apps.android.map((a) => ({ relation: ['delegate_permission/common.handle_all_urls'], target: { namespace: 'android_app', package_name: a.package, sha256_cert_fingerprints: a.fingerprints } })));
|
|
21
|
+
}
|
|
22
|
+
if (path === APPLE) {
|
|
23
|
+
if (!apps.apple.length)
|
|
24
|
+
return null;
|
|
25
|
+
const components = [...notApp.map((p) => ({ '/': p, exclude: true })), { '/': '/*' }];
|
|
26
|
+
return JSON.stringify({ applinks: { details: [{ appIDs: apps.apple, components }] } });
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function domSurface(root) {
|
|
2
|
+
const s = { show: (html) => (root.innerHTML = html), onSubmit: undefined };
|
|
3
|
+
root.addEventListener('submit', (ev) => {
|
|
4
|
+
const f = ev.target;
|
|
5
|
+
const ask = f?.dataset.ask;
|
|
6
|
+
if (!f || ask === undefined)
|
|
7
|
+
return;
|
|
8
|
+
ev.preventDefault();
|
|
9
|
+
const raw = {};
|
|
10
|
+
for (const [k, v] of new FormData(f))
|
|
11
|
+
raw[k] = typeof v === 'string' ? v : '';
|
|
12
|
+
s.onSubmit?.(ask, raw);
|
|
13
|
+
});
|
|
14
|
+
return s;
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Node, type Painter } from './tree.ts';
|
|
2
|
+
export type Door = {
|
|
3
|
+
world?: string;
|
|
4
|
+
host: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function doorTree(at: Door): Node;
|
|
7
|
+
export declare function door(at: Door): string;
|
|
8
|
+
export declare function heldTree(at: Door): Node;
|
|
9
|
+
export declare function held(at: Door): string;
|
|
10
|
+
export declare const POLICY = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
|
|
11
|
+
export declare const ours: Painter;
|
|
12
|
+
export type Landed = {
|
|
13
|
+
world?: string;
|
|
14
|
+
host: string;
|
|
15
|
+
at?: string;
|
|
16
|
+
ok: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function landedTree(l: Landed): Node;
|
|
19
|
+
export declare function landed(l: Landed): string;
|