@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
package/beings/look.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A look: what a being may say about how she is presented, as values and
|
|
3
|
+
// no more. One optional ask, `look`, answers it, and every side reads the
|
|
4
|
+
// part it understands: a screen reads the tokens and paints her section, a
|
|
5
|
+
// model side reads the hints per ask and writes them as tool annotations.
|
|
6
|
+
// Nothing here is capability, so nothing here is in a blueprint or a digest,
|
|
7
|
+
// and Quo never sees it. The vocabulary is closed: a token that is not
|
|
8
|
+
// listed does not exist, and one that fails its shape is dropped, not
|
|
9
|
+
// fixed. A being cannot paint outside her section, and she cannot reach a
|
|
10
|
+
// server through a look: no token carries a stylesheet or code, an image is
|
|
11
|
+
// a data URI, and the one token that names places, `stands`, is inert. It
|
|
12
|
+
// is a list of origins a side reads to decide whether to vouch for an
|
|
13
|
+
// address the being's own page named, and nothing in the dock ever fetches
|
|
14
|
+
// one.
|
|
15
|
+
import type { Blueprint, Json } from '@nervur-org/nervur';
|
|
16
|
+
|
|
17
|
+
// What a side may do with one ask. `title` is a human label instead of the
|
|
18
|
+
// name; `readOnly` says the ask changes nothing, so a screen may run it on
|
|
19
|
+
// its own and a model side marks it so; `destructive` asks for care;
|
|
20
|
+
// `idempotent` says asking twice is asking once; `icon` is one short string
|
|
21
|
+
// for a button. A being who marks an ask read-only and then writes has lied
|
|
22
|
+
// to her own page, and nothing enforces it, the way nothing enforces that
|
|
23
|
+
// her empty ask is safe to repeat.
|
|
24
|
+
export type Hint = { title?: string; readOnly?: boolean; destructive?: boolean; idempotent?: boolean; icon?: string };
|
|
25
|
+
export type Look = {
|
|
26
|
+
name?: string;
|
|
27
|
+
logo?: string; // an image as a data URI, never a URL
|
|
28
|
+
accent?: string; // hex colours
|
|
29
|
+
background?: string;
|
|
30
|
+
foreground?: string;
|
|
31
|
+
font?: string; // a font stack, plain characters
|
|
32
|
+
radius?: number; // 0 to 40, in px
|
|
33
|
+
order?: string[]; // her asks in the order she wants them shown
|
|
34
|
+
asks?: Record<string, Hint>;
|
|
35
|
+
// The origins this being stands in for. An envoy speaks for a vendor
|
|
36
|
+
// whose pages are that vendor's own, so a human she sends to a checkout
|
|
37
|
+
// or a consent screen is going somewhere the being's own world does not
|
|
38
|
+
// serve. She says here which places those are, once, in her look rather
|
|
39
|
+
// than in an answer: a look is asked once per digest and is the same for
|
|
40
|
+
// every answer she gives, so what she stands in for is a standing claim a
|
|
41
|
+
// human could be shown, and never a place that changes under one reply.
|
|
42
|
+
// A side vouches for an address on her page when its origin is in this
|
|
43
|
+
// list or is the world's own, and shows any other address as words.
|
|
44
|
+
stands?: string[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const COLOUR = /^#[0-9a-fA-F]{3,8}$/;
|
|
48
|
+
const FONT = /^[\w\s,'"-]{1,80}$/;
|
|
49
|
+
const LOGO = /^data:image\/(svg\+xml|png|jpeg|webp);base64,[A-Za-z0-9+/=]{1,40000}$/;
|
|
50
|
+
const NAME = /^[^<>&]{1,60}$/;
|
|
51
|
+
const ICON = /^[^<>&"']{1,8}$/;
|
|
52
|
+
const STANDS = 8; // origins a being may stand in for
|
|
53
|
+
const ORIGIN = 200; // characters one of them may be
|
|
54
|
+
|
|
55
|
+
const str = (v: Json | undefined, re: RegExp): string | undefined => (typeof v === 'string' && re.test(v) ? v : undefined);
|
|
56
|
+
|
|
57
|
+
// The look as a side may use it: every token held to its shape, everything
|
|
58
|
+
// else gone. A value that is not an object is no look at all.
|
|
59
|
+
export function sanitise(v: Json | undefined): Look {
|
|
60
|
+
if (v === null || v === undefined || typeof v !== 'object' || Array.isArray(v)) return {};
|
|
61
|
+
const l: Look = {};
|
|
62
|
+
const name = str(v.name, NAME);
|
|
63
|
+
if (name !== undefined) l.name = name;
|
|
64
|
+
const logo = str(v.logo, LOGO);
|
|
65
|
+
if (logo !== undefined) l.logo = logo;
|
|
66
|
+
for (const k of ['accent', 'background', 'foreground'] as const) {
|
|
67
|
+
const c = str(v[k], COLOUR);
|
|
68
|
+
if (c !== undefined) l[k] = c;
|
|
69
|
+
}
|
|
70
|
+
const font = str(v.font, FONT);
|
|
71
|
+
if (font !== undefined) l.font = font;
|
|
72
|
+
if (typeof v.radius === 'number' && Number.isFinite(v.radius) && v.radius >= 0 && v.radius <= 40) l.radius = Math.round(v.radius);
|
|
73
|
+
if (Array.isArray(v.order)) l.order = v.order.filter((x): x is string => typeof x === 'string');
|
|
74
|
+
if (Array.isArray(v.stands)) {
|
|
75
|
+
// An origin and never a page: whatever she writes is parsed and reduced
|
|
76
|
+
// to its scheme, host and port, so a path, a query or a fragment she
|
|
77
|
+
// put there cannot narrow what a side vouches for into something a
|
|
78
|
+
// reader would misjudge. Bounded, because a list nobody could read is a
|
|
79
|
+
// list nobody can check.
|
|
80
|
+
const stands: string[] = [];
|
|
81
|
+
for (const x of v.stands.slice(0, STANDS)) {
|
|
82
|
+
if (typeof x !== 'string' || x.length > ORIGIN) continue;
|
|
83
|
+
try {
|
|
84
|
+
const u = new URL(x);
|
|
85
|
+
if ((u.protocol === 'https:' || u.protocol === 'http:') && !stands.includes(u.origin)) stands.push(u.origin);
|
|
86
|
+
} catch {
|
|
87
|
+
/* not an origin */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (stands.length) l.stands = stands;
|
|
91
|
+
}
|
|
92
|
+
if (v.asks !== null && typeof v.asks === 'object' && !Array.isArray(v.asks)) {
|
|
93
|
+
const asks: Record<string, Hint> = {};
|
|
94
|
+
for (const [ask, h] of Object.entries(v.asks)) {
|
|
95
|
+
if (h === null || typeof h !== 'object' || Array.isArray(h)) continue;
|
|
96
|
+
const hint: Hint = {};
|
|
97
|
+
const title = str(h.title, NAME);
|
|
98
|
+
if (title !== undefined) hint.title = title;
|
|
99
|
+
const icon = str(h.icon, ICON);
|
|
100
|
+
if (icon !== undefined) hint.icon = icon;
|
|
101
|
+
for (const k of ['readOnly', 'destructive', 'idempotent'] as const) if (typeof h[k] === 'boolean') hint[k] = h[k];
|
|
102
|
+
if (Object.keys(hint).length) asks[ask] = hint;
|
|
103
|
+
}
|
|
104
|
+
if (Object.keys(asks).length) l.asks = asks;
|
|
105
|
+
}
|
|
106
|
+
return l;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The one hint a side reads first: whether this ask is in the look at all.
|
|
110
|
+
export const hint = (l: Look | undefined, name: string): Hint => l?.asks?.[name] ?? {};
|
|
111
|
+
|
|
112
|
+
// A carrier's notes say which asks are a standing's, how she looks, and
|
|
113
|
+
// which carried ask is her page: `standings: { id: { asks: [name], look?,
|
|
114
|
+
// page? } }`. Every side reads them here.
|
|
115
|
+
export type Group = { asks?: string[]; look?: Json; page?: string };
|
|
116
|
+
export function groups(bp: Blueprint | null): Record<string, Group> {
|
|
117
|
+
const n = bp?.notes;
|
|
118
|
+
const g = n !== null && n !== undefined && typeof n === 'object' && !Array.isArray(n) ? n.standings : undefined;
|
|
119
|
+
return g !== null && g !== undefined && typeof g === 'object' && !Array.isArray(g) ? (g as Record<string, Group>) : {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The asks that are presentation and never a form nor a tool: her look, her
|
|
123
|
+
// page, and the page of every standing she carries, named in her notes.
|
|
124
|
+
export function presentation(bp: Blueprint | null): Set<string> {
|
|
125
|
+
const p = new Set(['look', 'page']);
|
|
126
|
+
for (const g of Object.values(groups(bp))) if (typeof g.page === 'string') p.add(g.page);
|
|
127
|
+
return p;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The hint for one ask, hers or a standing's: a carried ask is looked up
|
|
131
|
+
// under its bare name in that standing's look.
|
|
132
|
+
export function hintFor(bp: Blueprint | null, mine: Look, name: string): Hint {
|
|
133
|
+
for (const [id, g] of Object.entries(groups(bp))) {
|
|
134
|
+
if (!g.asks?.includes(name)) continue;
|
|
135
|
+
const prefix = `${id}-`;
|
|
136
|
+
return hint(sanitise(g.look), name.startsWith(prefix) ? name.slice(prefix.length) : name);
|
|
137
|
+
}
|
|
138
|
+
return hint(mine, name);
|
|
139
|
+
}
|
package/beings/porter.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The bell's public being. She is the one door a phone nobody knows can
|
|
3
|
+
// reach: her asker is `{}` and she can tell, which is what the spec puts a
|
|
4
|
+
// public being there for. A stranger says hello, she boots that device a
|
|
5
|
+
// doorbell of its own and hands back its one way in, and from then on the
|
|
6
|
+
// phone is a named occupant with a heir and a count, which is where
|
|
7
|
+
// anything that must happen once belongs.
|
|
8
|
+
//
|
|
9
|
+
// She needs no owner standing and no root. A being boots a being of her
|
|
10
|
+
// ward and holds a standing to it, which is the whole of what she does.
|
|
11
|
+
//
|
|
12
|
+
// Her answer is safe to repeat, as a public being's must be: saying hello
|
|
13
|
+
// twice is two doorbells and no harm. A stranger who keeps knocking is the
|
|
14
|
+
// harbor's, never hers and never the door's, and the count below is only so
|
|
15
|
+
// that a loop in this session stops somewhere a human can see.
|
|
16
|
+
import { Being, isSilence, isWord, wordOf } from '@nervur-org/nervur';
|
|
17
|
+
import type { JsonObject } from '@nervur-org/nervur';
|
|
18
|
+
|
|
19
|
+
const ROOM = 64;
|
|
20
|
+
|
|
21
|
+
export class Porter extends Being {
|
|
22
|
+
static override cells: JsonObject = { minted: 0 };
|
|
23
|
+
static override asks = {
|
|
24
|
+
hello: { description: 'a device asks for a doorbell of its own', input: { type: 'object' } },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async hello(_args: JsonObject): Promise<JsonObject> {
|
|
28
|
+
const minted = typeof this.cells.minted === 'number' ? this.cells.minted : 0;
|
|
29
|
+
if (minted >= ROOM) return { error: 'this bell holds no more doorbells' };
|
|
30
|
+
|
|
31
|
+
const key = `doorbell:${minted + 1}`;
|
|
32
|
+
const held = await this.boot('Doorbell', key, key);
|
|
33
|
+
if (held === null) return { error: 'this bell could not make a doorbell' };
|
|
34
|
+
this.cells.minted = minted + 1;
|
|
35
|
+
|
|
36
|
+
// The doorbell hands out its one way in, and only while it has no
|
|
37
|
+
// device. So the porter cannot keep a key to a phone she introduced.
|
|
38
|
+
const out = await this.standings[held]?.ask('claim', {});
|
|
39
|
+
if (out === undefined || isSilence(out)) return { error: 'the doorbell said nothing' };
|
|
40
|
+
if (isWord(out)) return { error: wordOf(out) };
|
|
41
|
+
return { doorbell: key, ...(out as JsonObject) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The presence being: the being of a world who lives on a device. She is
|
|
3
|
+
// an avatar, one standing at the world's user being and a side for whoever
|
|
4
|
+
// is at the screen, and she is the device's face to that world. At her
|
|
5
|
+
// birth she lends every name the box lends and holds each as a standing
|
|
6
|
+
// named for the faculty; a name the box does not lend is simply not held.
|
|
7
|
+
// She carries those standings, so the user being who holds a way back to
|
|
8
|
+
// her sees `location-where` and `notifications-notify` in her describe and
|
|
9
|
+
// asks them in her name, and the phone is reached by asking her and never
|
|
10
|
+
// by holding anything of the box.
|
|
11
|
+
//
|
|
12
|
+
// She asks again at every birth, since a restart and a move are one event
|
|
13
|
+
// to her, and birth is synchronous while taking a standing is not, so what
|
|
14
|
+
// she holds is settled after her constructor returns; the terrain waits on
|
|
15
|
+
// `settled` before it says the box is up.
|
|
16
|
+
import type { Asker, OccupantRecord, Stance } from '@nervur-org/nervur';
|
|
17
|
+
import { Avatar, PUSHER, USER } from './avatar.ts';
|
|
18
|
+
import { FACULTIES, join } from '../dock/index.ts';
|
|
19
|
+
|
|
20
|
+
export class Presence extends Avatar {
|
|
21
|
+
// Her world's user being sees the box, and nobody else: the way back she
|
|
22
|
+
// handed over at the join is the one occupant who may ask through her.
|
|
23
|
+
static override carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean {
|
|
24
|
+
return occupant?.id === PUSHER;
|
|
25
|
+
}
|
|
26
|
+
// Her standing at the user being is hers to speak through and never to
|
|
27
|
+
// carry: the user being carries her, so carrying it back would be a
|
|
28
|
+
// loop, and it is the one standing that is not a faculty.
|
|
29
|
+
static override skips(id: string): boolean {
|
|
30
|
+
return id === USER;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Which names the box lent her, by name, settled after birth.
|
|
34
|
+
readonly lent: Promise<Record<string, string | null>>;
|
|
35
|
+
constructor(stance: Stance) {
|
|
36
|
+
super(stance);
|
|
37
|
+
this.lent = Promise.all(Object.keys(FACULTIES).map(async (name) => [name, await join(this, name)] as const)).then((all) => Object.fromEntries(all));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# The bells of a Quo app
|
|
2
|
+
|
|
3
|
+
A ward on a dialer is reachable only while it holds a line. A closed tab, a
|
|
4
|
+
phone asleep, an app in the background: the world has something to say and
|
|
5
|
+
nobody is listening. The bell is how such a device is reached, the one way
|
|
6
|
+
its platform allows, by waking it.
|
|
7
|
+
|
|
8
|
+
Everything here is four pieces, and none of them is any publisher's. An
|
|
9
|
+
adopter who ships their own Quo app under their own name runs the same four
|
|
10
|
+
with their own keys, changes nothing in the library and nothing in the dock,
|
|
11
|
+
and is not asking anyone's permission.
|
|
12
|
+
|
|
13
|
+
## Why a service exists at all
|
|
14
|
+
|
|
15
|
+
Apple and Google accept a push only when it is signed with the key of the
|
|
16
|
+
team that published the app. A world's box holds no such key and never
|
|
17
|
+
will, because the key belongs to whoever publishes the app and to nobody
|
|
18
|
+
else. So a world hands the ring on to a service that publisher runs.
|
|
19
|
+
|
|
20
|
+
This is the one place in Quo where a world does not reach a person
|
|
21
|
+
directly, and it is not a design choice. It is the platform's rule, and the
|
|
22
|
+
whole of this paper is about making that hop carry as little as it possibly
|
|
23
|
+
can.
|
|
24
|
+
|
|
25
|
+
Web Push needs no service. A world mints its own keypair and speaks to the
|
|
26
|
+
browser's push service itself, so tabs and desks are free of this entirely.
|
|
27
|
+
|
|
28
|
+
## The porter
|
|
29
|
+
|
|
30
|
+
A bell is a ward, and its public being is the porter. A phone nobody knows
|
|
31
|
+
arrives at her as `{}`, which is the one place in Quo a stranger is
|
|
32
|
+
answered by design.
|
|
33
|
+
|
|
34
|
+
She does one thing. A device says hello, she brings a doorbell into being
|
|
35
|
+
for it, and hands back the way in. The device knocks, and from then on it is
|
|
36
|
+
a named occupant with a heir and a count, which is where anything that must
|
|
37
|
+
happen once belongs. Her own answer is safe to repeat, as a public being's
|
|
38
|
+
must be: saying hello twice is two doorbells and no harm.
|
|
39
|
+
|
|
40
|
+
A stranger who keeps knocking is the harbor's, never hers and never the
|
|
41
|
+
door's. A bell needs no attestation, no account and no door of its own
|
|
42
|
+
invention, and it should have none.
|
|
43
|
+
|
|
44
|
+
## The doorbell
|
|
45
|
+
|
|
46
|
+
One being per device. Two things live in it.
|
|
47
|
+
|
|
48
|
+
**The platform token**, in her cells, written by the device and read by
|
|
49
|
+
nobody. It leaves for nowhere. No world ever sees it, so no world can copy
|
|
50
|
+
it, hand it on, or keep ringing after it is told to stop. When the platform
|
|
51
|
+
hands the phone a new token, the phone writes it here once and not one world
|
|
52
|
+
is told anything.
|
|
53
|
+
|
|
54
|
+
**One occupant per allowed world.** The device asks the doorbell for a way
|
|
55
|
+
in, hands it to a world it has joined, and that world knocks and holds an
|
|
56
|
+
ordinary standing. That row is the whole of the permission, and it is held
|
|
57
|
+
at the bell, by neither the world that rings nor the publisher who holds the
|
|
58
|
+
key.
|
|
59
|
+
|
|
60
|
+
So the pair is recorded independently: this device, that world. A device
|
|
61
|
+
with presence in many worlds has many rows here, and every one of them
|
|
62
|
+
stands or falls on its own.
|
|
63
|
+
|
|
64
|
+
## Allowing and disallowing
|
|
65
|
+
|
|
66
|
+
**Allowing is an invitation the device mints.** A world can only ring a
|
|
67
|
+
phone that chose it. There is no list of approved worlds anywhere, and there
|
|
68
|
+
must not be: gating it would make the publisher the gatekeeper of every
|
|
69
|
+
world's relation with its own member, which is the thing Quo exists to
|
|
70
|
+
abolish, and it buys nothing, because the consent is already there in the
|
|
71
|
+
act of handing over the invitation.
|
|
72
|
+
|
|
73
|
+
**Disallowing is `remove`.** One occupant goes. The heir is forgotten and
|
|
74
|
+
its last keys are kept apart, bounded, so that world hears `removed` once at
|
|
75
|
+
the door and nobody else hears a thing. Her other worlds are untouched. It
|
|
76
|
+
takes effect at the bell, so it does not depend on the removed world being
|
|
77
|
+
honest about it, and there is nothing it could have kept that would still
|
|
78
|
+
work.
|
|
79
|
+
|
|
80
|
+
## What a ring carries
|
|
81
|
+
|
|
82
|
+
A world rings its standing. It names no device, no token and no world,
|
|
83
|
+
because the doorbell reads both ends from the standing the ask arrived on.
|
|
84
|
+
There is nothing in a ring to forge.
|
|
85
|
+
|
|
86
|
+
It carries one thing: a payload sealed to the device's own key. Not to the
|
|
87
|
+
bell's, not to the world's, not to anyone else's. The bell passes ciphertext
|
|
88
|
+
it could not read if it tried, the platform carries ciphertext, and on the
|
|
89
|
+
device a notification service extension unseals it before the banner is
|
|
90
|
+
drawn. What the person sees is an ordinary, immediate notification.
|
|
91
|
+
|
|
92
|
+
So the claim is not that the doorbell carries nothing. It is that the
|
|
93
|
+
doorbell carries nothing anyone but her device can read, which is the
|
|
94
|
+
stronger promise, because the weaker one bought its purity with the banner
|
|
95
|
+
itself: a notification composed after the app wakes is one the platform may
|
|
96
|
+
delay at its own discretion and one a person who force quit the app never
|
|
97
|
+
sees at all.
|
|
98
|
+
|
|
99
|
+
The payload is bounded by what the platform will carry, four kilobytes on
|
|
100
|
+
APNs, and the doorbell refuses a seal that will not fit rather than letting
|
|
101
|
+
a platform nobody is reading refuse it two hops later.
|
|
102
|
+
|
|
103
|
+
Web Push carries the same line and seals it once rather than twice. The
|
|
104
|
+
standard encrypts a body to keys only that browser holds, so the only party
|
|
105
|
+
a banner key would hide it from is the push service, which that encryption
|
|
106
|
+
already excludes. A second seal would buy nothing and would make every tab
|
|
107
|
+
keep a banner key for it. So a world encrypts the line itself, the browser
|
|
108
|
+
opens it, and the worker shows what it was handed.
|
|
109
|
+
|
|
110
|
+
## Where a bell runs
|
|
111
|
+
|
|
112
|
+
Anywhere a harbor runs, and it is the same bell. A bell is a ward: the
|
|
113
|
+
porter, a doorbell per device, and the push faculty of the ward it stands
|
|
114
|
+
on. Nothing in it knows a platform.
|
|
115
|
+
|
|
116
|
+
What differs is one thing, and it is the thing a faculty is for. The
|
|
117
|
+
faculty's rings are built by the terrain out of what that terrain has: a
|
|
118
|
+
worker's `fetch` speaks HTTP/2 and an edge bell rings APNs with it, a
|
|
119
|
+
droplet speaks it with `node:http2` and rings the same service, and a
|
|
120
|
+
terrain with neither holds no `apns` ring and says so. A publisher who
|
|
121
|
+
starts on a worker and moves to a box moves the ward, keeps its pk, and
|
|
122
|
+
neither a device's standing nor a world's is disturbed.
|
|
123
|
+
|
|
124
|
+
So the edge is a convenience and never a requirement. It is a good first
|
|
125
|
+
home because a bell keeps almost nothing and must answer whenever any world
|
|
126
|
+
rings, and that is all it is.
|
|
127
|
+
|
|
128
|
+
## Nothing is configured
|
|
129
|
+
|
|
130
|
+
A bell has no configuration file and wants none. What it holds, it holds the
|
|
131
|
+
way every being holds anything.
|
|
132
|
+
|
|
133
|
+
- **Who may ring** is an occupant, invited by the device.
|
|
134
|
+
- **What is rung** is a token in a doorbell's cells, written by the device.
|
|
135
|
+
- **Which service a world hands a ring to** is a cell on the push faculty,
|
|
136
|
+
set by the root, because whose service it is belongs to an estate and not
|
|
137
|
+
to a world.
|
|
138
|
+
- **The platform keys** are the terrain's secrets and reach the faculty as
|
|
139
|
+
its rings are built, never a file in a tree and never a cell.
|
|
140
|
+
|
|
141
|
+
Rotation is the same answer twice. A heir rotates as it speaks, which is
|
|
142
|
+
Quo's and needs nobody. A platform token rotates by the device writing it
|
|
143
|
+
once, and because no world was ever given one, no world is told.
|
|
144
|
+
|
|
145
|
+
## The banner key
|
|
146
|
+
|
|
147
|
+
A world seals to a key that opens banners and nothing else.
|
|
148
|
+
|
|
149
|
+
It is the device's, one pair, and it lives in the cells of the box's
|
|
150
|
+
notifications faculty, which is the being that shows a human a line. That is
|
|
151
|
+
the source of truth and the only place the pair is kept. The public half
|
|
152
|
+
goes to a world when the device admits it, beside the invitation to the
|
|
153
|
+
doorbell. The secret half never leaves the device.
|
|
154
|
+
|
|
155
|
+
Where a platform's own notification process can read it is the faculty's
|
|
156
|
+
business, and this is the whole reason the key sits there rather than in a
|
|
157
|
+
being of the library's. On a phone the extension that draws the banner is a
|
|
158
|
+
second process, so the faculty puts the secret in the keychain group it
|
|
159
|
+
shares with it. On a desk and in a tab the terrain has its own answer. A
|
|
160
|
+
faculty is the terrain's hand, and a key that must be reachable by the
|
|
161
|
+
platform belongs in the hand and not in the library.
|
|
162
|
+
|
|
163
|
+
A tab keeps none. Web Push is encrypted to the subscription's own keys and
|
|
164
|
+
a browser draws its own banners in its own worker, so there is neither a
|
|
165
|
+
second process to reach nor a second seal to open.
|
|
166
|
+
|
|
167
|
+
Minting the key is where the person is asked. A device that has just minted
|
|
168
|
+
one is a device about to be rung, and the ring that carries a banner is an
|
|
169
|
+
alert the platform draws for nobody who has not allowed it. Registering for
|
|
170
|
+
the doorbell asked for nothing, deliberately, because a doorbell displays
|
|
171
|
+
nothing; so the phone asks in the same act as it plants the key, and a
|
|
172
|
+
refusal costs the line and not the wake. The key stays where it is, the
|
|
173
|
+
world is admitted, and what arrives is a push the person reads when she
|
|
174
|
+
opens the app.
|
|
175
|
+
|
|
176
|
+
It is not the ward's own key, and that is deliberate. The ward's seed opens
|
|
177
|
+
everything the person sees and is kept so that it never leaves the device at
|
|
178
|
+
all. A banner key opens banners. If the process that draws notifications
|
|
179
|
+
were ever turned against her, the second loses her a line of text and the
|
|
180
|
+
first would lose her the world.
|
|
181
|
+
|
|
182
|
+
## What a bell knows, and what it cannot
|
|
183
|
+
|
|
184
|
+
It knows which device, which world, and when. It cannot know what was said.
|
|
185
|
+
|
|
186
|
+
That is the honest accounting, and the design holds it to the minimum: keep
|
|
187
|
+
counters and no log of rings, name a doorbell by nothing that identifies a
|
|
188
|
+
person, and hold occupants as keys with no world's name beside them. A bell
|
|
189
|
+
that kept more would be a registry of worlds, and the app exists not to have
|
|
190
|
+
one.
|
|
191
|
+
|
|
192
|
+
The publisher cannot add a world to a device's doorbell. Only the device
|
|
193
|
+
can, because only the device holds the standing that mints a way in. The
|
|
194
|
+
list is held at the bell and it is not the bell's to write.
|
|
195
|
+
|
|
196
|
+
## The three kinds
|
|
197
|
+
|
|
198
|
+
**Web.** A world mints its own keypair and rings the browser's push service
|
|
199
|
+
itself. The line rides in the ring, encrypted end to end by the standard to
|
|
200
|
+
the two keys of that subscription, no bell is involved, and no banner key is
|
|
201
|
+
wanted.
|
|
202
|
+
|
|
203
|
+
**iOS.** APNs, signed under the publisher's key, spoken over HTTP/2. The
|
|
204
|
+
service extension is what makes a sealed payload into a banner, and it needs
|
|
205
|
+
the device's key reachable from a second process.
|
|
206
|
+
|
|
207
|
+
**Android.** The same shape under FCM, with a data message the app composes
|
|
208
|
+
from.
|
|
209
|
+
|
|
210
|
+
A bell that holds no key for a kind says so to whoever rings that kind,
|
|
211
|
+
which is what a publisher who rings no Android phones says to an Android
|
|
212
|
+
phone. It does not pretend it rang.
|
|
213
|
+
|
|
214
|
+
## What this refuses
|
|
215
|
+
|
|
216
|
+
- **An account at the bell.** The standing is the identity. An account would
|
|
217
|
+
be the registry of worlds, and there is no third thing it would buy.
|
|
218
|
+
- **Approving worlds.** The device approves. Nobody else is asked.
|
|
219
|
+
- **A token that travels.** The moment a world holds one, disallowing is a
|
|
220
|
+
request rather than a fact.
|
|
221
|
+
- **A payload the bell can read.** Including one it merely promises not to.
|
|
222
|
+
- **A configuration file.** A bell is occupants, standings and cells. A
|
|
223
|
+
second place to say who may ring would be a second answer to a question
|
|
224
|
+
that already has one.
|
|
225
|
+
- **A terrain in the design.** Edge or droplet is where the ward sleeps, and
|
|
226
|
+
a bell that worked one way on one and another way on the other would be
|
|
227
|
+
two bells.
|
|
228
|
+
- **Anything publisher-specific in the packages.** Team ids, bundle ids, a
|
|
229
|
+
bell's address and a push key are an estate's, carried in by its
|
|
230
|
+
environment. What is published is the porter, the doorbell, the faculty
|
|
231
|
+
and the service, and they name nobody.
|
|
232
|
+
|
|
233
|
+
## Where the tree stands
|
|
234
|
+
|
|
235
|
+
The one place under this paper where a gap between it and the code is named.
|
|
236
|
+
|
|
237
|
+
Each of the three grounds has drawn a line on a screen, and each in the
|
|
238
|
+
shape this paper gives it. On an iPhone the app minted its banner key,
|
|
239
|
+
planted the secret half where the extension reads it, took a device token
|
|
240
|
+
from Apple, and a ring under the team's own key carried a hundred and
|
|
241
|
+
thirty bytes of ciphertext that the extension opened. On Android the ring
|
|
242
|
+
carried an empty data map and the device composed the line itself. In a
|
|
243
|
+
real Chrome the world found the tab unreachable, rang its subscription, and
|
|
244
|
+
the worker drew what it was handed.
|
|
245
|
+
|
|
246
|
+
- **A simulator draws no sealed banner**, because it runs no notification
|
|
247
|
+
service extension at all. It proves everything on either side of that
|
|
248
|
+
move: the pair minted and kept across a reinstall, the secret half
|
|
249
|
+
planted, and the seal opened by the extension's own code against a
|
|
250
|
+
payload a world sealed to that device. The move itself is an iPhone's.
|
|
251
|
+
- **A browser wants a real one.** The Chromium a bench drives answers
|
|
252
|
+
`showNotification` neither way, so the line that draws it is the one line
|
|
253
|
+
no test here runs. A browser also asks the human with a button and never
|
|
254
|
+
on load, so the permission is a click and no suite can spring it.
|
|
255
|
+
- **Android is pointed at a Firebase project by a file a build carries**
|
|
256
|
+
and the tree ignores, since the file holds a key. The project is the
|
|
257
|
+
publisher's own and holds the one app it ships. Its id is minted once and
|
|
258
|
+
never moves, so a project opened for something else keeps that name for
|
|
259
|
+
as long as it lives, which is a thing to know before opening one.
|
|
260
|
+
- **The ring on real hardware holds past the first.** A phone with the app
|
|
261
|
+
backgrounded draws the banner, and so does one locked with the screen
|
|
262
|
+
off. Five rings two seconds apart all arrived, in order, none coalesced
|
|
263
|
+
and none dropped, so the platform throttles nothing at that rate. And a
|
|
264
|
+
ring lands after a full power cycle on an app that has not run since the
|
|
265
|
+
device booted, which is custody at its strongest: the doorbell standing
|
|
266
|
+
and the banner key are both kept, and nothing has to be launched to
|
|
267
|
+
receive. A background push reaches no app the human force quit, by
|
|
268
|
+
Apple's rule, so a phone under test is backgrounded and not closed.
|
|
269
|
+
- **An Android device reaches its bell over `wss` and never `ws`.** The
|
|
270
|
+
shell is served from `https://localhost` there, and that ground's WebView
|
|
271
|
+
refuses a cleartext socket from an HTTPS page as mixed content, so a bell
|
|
272
|
+
on a bare address on the same network cannot be dialled at all and the
|
|
273
|
+
device comes away with no doorbell. A phone's WebView on the other ground
|
|
274
|
+
allows it. So a bell under test wants a real name and a certificate,
|
|
275
|
+
where the other ground is content with an address on the wire.
|
|
276
|
+
- **A ring costs about 290ms on the box side**, measured from the ask to
|
|
277
|
+
the answer that comes back only once the bell has handed it to Apple and
|
|
278
|
+
Apple has taken it. It is steady under a burst: five rings measured 291,
|
|
279
|
+
294, 291, 295 and 295. What the platform then does with it before a
|
|
280
|
+
screen lights is the platform's and is not ours to measure.
|