@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/api/route.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The api. route: HTTP as the fourth rendering of one describe. It is
|
|
3
|
+
// `Request` in and `Response` out and names no terrain, so the daemon mounts
|
|
4
|
+
// it under `/api` behind the proxy that maps the api. hostname, and the
|
|
5
|
+
// worker at the edge mounts the same route under its own prefix.
|
|
6
|
+
// Beside forms, tools and the CLI, it speaks a being's describe and asks as
|
|
7
|
+
// plain JSON, and it hears every rung a route hears. A request carrying no
|
|
8
|
+
// bearer is the stranger every route knows: the named ward's public being
|
|
9
|
+
// asked as `{}`, which is what a browser fetch, a crawler or a page
|
|
10
|
+
// rendering server side is. A request carrying a bearer the box granted is
|
|
11
|
+
// an occupant: the identity's avatar in the ward the grant names, asked
|
|
12
|
+
// through her one standing at the world's being, so what a script sees
|
|
13
|
+
// with a bearer is what a model sees with the same bearer as tools and a
|
|
14
|
+
// tab sees as forms. A world is a ward, so `/api/<ward>` is the describe
|
|
15
|
+
// for this asker and `/api/<ward>/<ask>` is one ask, args from the query on
|
|
16
|
+
// a GET or a JSON object on a POST. A caller who names no ward is at the
|
|
17
|
+
// device's own, as a tab at the web route's root is: `GET /api` is the
|
|
18
|
+
// describe of that ward, for a stranger and under a bearer alike.
|
|
19
|
+
//
|
|
20
|
+
// A program gets its bearer the way the allow page gives one to a model,
|
|
21
|
+
// without the page: `POST /api` with the identity and the invitation the
|
|
22
|
+
// root minted for it trades the invitation for tokens, the avatar knocks,
|
|
23
|
+
// and the invitation is spent. `POST /api` with a refresh token rotates.
|
|
24
|
+
//
|
|
25
|
+
// What she says crosses as the model side crosses it: an object is 200
|
|
26
|
+
// with that JSON, an error object she answered is 400 with that JSON, and
|
|
27
|
+
// no object at all is 503 `{ error: 'silence' }`. For a stranger, silence,
|
|
28
|
+
// a throw, a wait that ran out and a ward with nobody at the door are one
|
|
29
|
+
// answer here, as they are one answer on the wire: a stranger learns which
|
|
30
|
+
// world exists and nothing else about it. For an occupant the ward's
|
|
31
|
+
// words cross by name, and `removed` is 401 with the grant forgotten, so a
|
|
32
|
+
// script that was removed at the user being starts over with a fresh
|
|
33
|
+
// invitation. A world that is not here, and a path that is not one of the
|
|
34
|
+
// shapes, are the route's own 404 and never a being's.
|
|
35
|
+
import type { Handler } from '../harbor/http.ts';
|
|
36
|
+
import { readText } from '../harbor/http.ts';
|
|
37
|
+
import type { Hosted } from '@nervur-org/nervur/harbor';
|
|
38
|
+
import type { Avatar } from '../beings/avatar.ts';
|
|
39
|
+
import { invitationOf } from '../beings/link.ts';
|
|
40
|
+
import { landed, POLICY } from '../human/door.ts';
|
|
41
|
+
import { CSS } from '../human/style.ts';
|
|
42
|
+
import type { Grants } from '../dock/grants.ts';
|
|
43
|
+
import { isInvitation, isSilence, isWord, wordOf, type Invitation, type JsonObject } from '@nervur-org/nervur';
|
|
44
|
+
|
|
45
|
+
const open = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET, POST, OPTIONS', 'access-control-allow-headers': 'content-type, authorization' };
|
|
46
|
+
|
|
47
|
+
// The harbor this route answers for, as the route needs it: the wards it
|
|
48
|
+
// holds, and which of them a caller who names none is at. The second is the
|
|
49
|
+
// terrain's word and never a guess of the route's: a disk reads the mark an
|
|
50
|
+
// operator wrote, a worker holds the one world in its object. A terrain with
|
|
51
|
+
// no answer says the empty string, and the path that named no ward is no
|
|
52
|
+
// such world.
|
|
53
|
+
export type Held = { wards: ReadonlyMap<string, Hosted>; here(): string };
|
|
54
|
+
|
|
55
|
+
export type Options = {
|
|
56
|
+
grants: Grants;
|
|
57
|
+
// Her avatar as she stands in a world, for a bearer: nothing is minted.
|
|
58
|
+
resolve: (identity: string, ward: string) => Promise<{ avatar?: Avatar; error?: string }>;
|
|
59
|
+
// Boot or find the avatar for this identity in that world, and knock.
|
|
60
|
+
join: (identity: string, invitation: Invitation, ward: string) => Promise<{ error?: string }>;
|
|
61
|
+
// Where the worlds' pages are, for the one arrival here that is a human:
|
|
62
|
+
// a redirect from somewhere a world sent her. Without it she still gets a
|
|
63
|
+
// page, with no way back offered on it.
|
|
64
|
+
web?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const word = (s: unknown) => (typeof s === 'string' && /^[\w.-]{1,40}$/.test(s) ? s : null);
|
|
68
|
+
|
|
69
|
+
export function apiRoute(harbor: Held, o: Options): Handler {
|
|
70
|
+
return async (req, rest) => {
|
|
71
|
+
const json = (status: number, body: unknown) => Response.json(body, { status, headers: open });
|
|
72
|
+
// The one page this road serves, and it wears the one stylesheet every
|
|
73
|
+
// screen wears. No cross-origin header on it: a page is for the browser
|
|
74
|
+
// that was sent here and never for a script somewhere else. Its policy
|
|
75
|
+
// is the allow page's, and for the same reason: a page that runs no
|
|
76
|
+
// script of its own should be allowed none.
|
|
77
|
+
const page = (status: number, body: string) =>
|
|
78
|
+
new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>quo</title>\n<style>${CSS}</style>\n</head><body>${body}</body></html>`, {
|
|
79
|
+
status,
|
|
80
|
+
headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': POLICY, 'referrer-policy': 'no-referrer' },
|
|
81
|
+
});
|
|
82
|
+
// Where this world lives, as the daemon was told and not as the request
|
|
83
|
+
// said: a Host header is the caller's to write, and a page that printed
|
|
84
|
+
// one would print whatever a caller wanted a human to read. Falling back
|
|
85
|
+
// to it is for a box configured with no web address at all, where the
|
|
86
|
+
// request is the only thing that knows.
|
|
87
|
+
const hostOf = (r: Request) => {
|
|
88
|
+
try {
|
|
89
|
+
return o.web === undefined ? (r.headers.get('host') ?? '') : new URL(o.web).host;
|
|
90
|
+
} catch {
|
|
91
|
+
return r.headers.get('host') ?? '';
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: open });
|
|
95
|
+
if (req.method !== 'GET' && req.method !== 'POST') return json(405, { error: 'GET or POST' });
|
|
96
|
+
const body = async (): Promise<JsonObject | null> => {
|
|
97
|
+
const raw = await readText(req);
|
|
98
|
+
if (raw === undefined) return null;
|
|
99
|
+
let parsed: unknown = {};
|
|
100
|
+
try {
|
|
101
|
+
parsed = raw.length === 0 ? {} : JSON.parse(raw);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return parsed === null || typeof parsed !== 'object' || Array.isArray(parsed) ? null : (parsed as JsonObject);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// The trade: an invitation for a grant, or a refresh token for the next.
|
|
109
|
+
if ((rest === '' || rest === '/') && req.method === 'POST') {
|
|
110
|
+
const b = await body();
|
|
111
|
+
if (b === null) return json(400, { error: 'the body is a JSON object' });
|
|
112
|
+
if (typeof b.refresh === 'string') {
|
|
113
|
+
const t = await o.grants.refresh(b.refresh);
|
|
114
|
+
return t ? json(200, t) : json(400, { error: 'invalid_grant' });
|
|
115
|
+
}
|
|
116
|
+
const inv = typeof b.invitation === 'string' ? invitationOf(b.invitation) : isInvitation(b.invitation) ? b.invitation : null;
|
|
117
|
+
if (!inv) return json(400, { error: 'that is not an invitation' });
|
|
118
|
+
const world = [...harbor.wards].find(([, h]) => h.pk === inv.ward);
|
|
119
|
+
if (!world) return json(400, { error: 'that invitation is for a world that does not live here' });
|
|
120
|
+
const [ward, hosted] = world;
|
|
121
|
+
const identity = word(b.identity);
|
|
122
|
+
if (identity === null || identity === hosted.record.user || identity === 'desk') return json(400, { error: 'an identity is one word, and not the user or the desk' });
|
|
123
|
+
// Nobody at a page vouches for a program, so a trade is a knock that
|
|
124
|
+
// spends: an identity already in refreshes, or is removed at the user
|
|
125
|
+
// being and invited again, and never re-enters on a spent invitation.
|
|
126
|
+
if ((await o.resolve(identity, ward)).avatar) return json(400, { error: 'already in' });
|
|
127
|
+
const joined = await o.join(identity, inv, ward);
|
|
128
|
+
if (joined.error) return json(400, { error: joined.error });
|
|
129
|
+
return json(200, await o.grants.issue(identity, ward, 'api'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// No ward named is the device's own ward, and never a default the route
|
|
133
|
+
// picks: the harbor points at one of its wards and says that one is the
|
|
134
|
+
// device's, and its door answers for it.
|
|
135
|
+
const [, named = '', ask = '', ...more] = rest.split('/');
|
|
136
|
+
const ward = named === '' ? harbor.here() : named;
|
|
137
|
+
|
|
138
|
+
// The delivery: a vendor's webhook to an envoy, `/api/<ward>/<being>/deliver`.
|
|
139
|
+
// Not a stranger's ask and not an occupant's: the owner asking her
|
|
140
|
+
// `deliver` with the raw bytes and the headers, and nothing else. The
|
|
141
|
+
// body is read as a string and never parsed here, since the vendor
|
|
142
|
+
// signed the bytes and a reparsed body fails her check; no bearer is
|
|
143
|
+
// read, since the rung is not the caller's to prove. She answers `{ ok }`
|
|
144
|
+
// or her error object, and the route carries it.
|
|
145
|
+
// A GET on the same path is the same ask with an empty body: where an
|
|
146
|
+
// OAuth redirect lands, the code in the query. The query crosses inside
|
|
147
|
+
// the headers under `:query`, a name no header on the wire can have, so
|
|
148
|
+
// her one ask in stays one.
|
|
149
|
+
if (more.length === 1 && more[0] === 'deliver') {
|
|
150
|
+
if (req.method !== 'POST' && req.method !== 'GET') return json(405, { error: 'POST, or GET for a redirect' });
|
|
151
|
+
const hosted = harbor.wards.get(ward);
|
|
152
|
+
if (!hosted || !/^[\w-]+$/.test(ask)) return json(404, { error: 'no such world' });
|
|
153
|
+
const raw = req.method === 'GET' ? '' : await readText(req);
|
|
154
|
+
if (raw === undefined) return json(413, { error: 'too big' });
|
|
155
|
+
const headers: Record<string, string> = {};
|
|
156
|
+
req.headers.forEach((v, k) => (headers[k] = v));
|
|
157
|
+
const query = new URL(req.url).search.slice(1);
|
|
158
|
+
if (query) headers[':query'] = query;
|
|
159
|
+
const out = await hosted.ask('ask', { being: ask, method: 'deliver', args: { body: raw, headers } });
|
|
160
|
+
await hosted.save();
|
|
161
|
+
// A GET here is a browser and a browser is a human: a vendor a world
|
|
162
|
+
// sent her to has opened this URL in her tab, so what she is looking
|
|
163
|
+
// at is whatever the envoy answered a machine. She gets a page
|
|
164
|
+
// instead, and only she does; a vendor asking for JSON is answered in
|
|
165
|
+
// JSON as it always was, and so is a GET that says nothing about what
|
|
166
|
+
// it wants.
|
|
167
|
+
const wants = req.method === 'GET' && (req.headers.get('accept') ?? '').includes('text/html');
|
|
168
|
+
if (wants) {
|
|
169
|
+
const ok = out !== null && typeof out === 'object' && !Array.isArray(out) && typeof (out as { error?: unknown }).error !== 'string';
|
|
170
|
+
return page(ok ? 200 : 400, landed({ world: ward, host: hostOf(req), ...(o.web ? { at: `${o.web.replace(/\/$/, '')}/${encodeURIComponent(ward)}` } : {}), ok }));
|
|
171
|
+
}
|
|
172
|
+
if (isSilence(out) || out === null || typeof out !== 'object' || Array.isArray(out)) return json(503, { error: 'silence' });
|
|
173
|
+
const said = (out as { error?: unknown }).error;
|
|
174
|
+
if (said === 'no such being' || said === 'unknown ask') return json(404, { error: 'no such envoy' });
|
|
175
|
+
if (said === 'silence' || said === 'threw' || said === 'late') return json(503, { error: 'silence' });
|
|
176
|
+
if (typeof said === 'string') return json(400, out);
|
|
177
|
+
return json(200, out);
|
|
178
|
+
}
|
|
179
|
+
if (!/^[\w.-]+$/.test(ward) || more.length > 0 || (ask !== '' && !/^[\w-]+$/.test(ask))) return json(404, { error: 'GET /api for the device’s ward, GET /api/<ward> for the describe, GET or POST /api/<ward>/<ask> for one ask, POST /api/<ward>/<being>/deliver for a delivery' });
|
|
180
|
+
const hosted = harbor.wards.get(ward);
|
|
181
|
+
if (!hosted) return json(404, { error: 'no such world' });
|
|
182
|
+
let args: JsonObject;
|
|
183
|
+
if (req.method === 'POST') {
|
|
184
|
+
const b = await body();
|
|
185
|
+
if (b === null) return json(400, { error: 'the body is a JSON object' });
|
|
186
|
+
args = b;
|
|
187
|
+
} else {
|
|
188
|
+
const url = new URL(req.url);
|
|
189
|
+
args = Object.fromEntries(url.searchParams.entries());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const carried = (req.headers.get('authorization') ?? '') !== '';
|
|
193
|
+
const grant = o.grants.bearer(req);
|
|
194
|
+
if (carried && grant === null) return json(401, { error: 'unauthorized' });
|
|
195
|
+
if (grant === null) {
|
|
196
|
+
// The owner's `ask` with no being named: the public being, asked as
|
|
197
|
+
// nobody, bounded and judged by the ward, which is the in-process twin
|
|
198
|
+
// of the bytes a stranger would send. The route holds none of that. A
|
|
199
|
+
// world with nobody at the door and a being who said nothing are one
|
|
200
|
+
// answer here, as they are one answer on the wire.
|
|
201
|
+
const out = await hosted.ask('ask', ask === '' ? {} : { method: ask, args });
|
|
202
|
+
if (isSilence(out) || out === null || typeof out !== 'object' || Array.isArray(out)) return json(503, { error: 'silence' });
|
|
203
|
+
const said = (out as { error?: unknown }).error;
|
|
204
|
+
if (said === 'silence' || said === 'threw' || said === 'late' || said === 'no such being') return json(503, { error: 'silence' });
|
|
205
|
+
if (typeof said === 'string') return json(400, out);
|
|
206
|
+
return json(200, out);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// An occupant: the grant names a world, and the path must name the same
|
|
210
|
+
// one, since an avatar is one relation in one world.
|
|
211
|
+
if (grant.ward !== ward) return json(403, { error: 'not your world' });
|
|
212
|
+
const found = await o.resolve(grant.identity, ward);
|
|
213
|
+
if (found.error === 'removed') {
|
|
214
|
+
await o.grants.revoke(grant.identity, ward);
|
|
215
|
+
return json(401, { error: 'removed' });
|
|
216
|
+
}
|
|
217
|
+
if (!found.avatar) return json(401, { error: found.error ?? 'not joined' });
|
|
218
|
+
const out = ask === '' ? await found.avatar.tools() : await found.avatar.call(ask, args);
|
|
219
|
+
await hosted.save();
|
|
220
|
+
if (isSilence(out)) return json(503, { error: 'silence' });
|
|
221
|
+
if (isWord(out)) return json(503, { error: wordOf(out) });
|
|
222
|
+
if (out === null || typeof out !== 'object' || Array.isArray(out)) return json(200, out);
|
|
223
|
+
const said = (out as { error?: unknown }).error;
|
|
224
|
+
if (typeof said === 'string' && !('asks' in out)) return json(400, out);
|
|
225
|
+
return json(200, out);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# GLOSSARY
|
|
2
|
+
|
|
3
|
+
Every word the papers above the spec use, in one place. The protocol's own
|
|
4
|
+
words, being, ward, harbor, standing, occupant, invitation, knock and the
|
|
5
|
+
rest, are in the glossary of `quo/SPEC.md`, which assumes nothing and
|
|
6
|
+
is not repeated here; what the library spells for itself is in
|
|
7
|
+
`papers/quo-kit.md`. A word here is a convention of the dock or of an
|
|
8
|
+
estate, and changes no word of the spec. `papers/WORLDS.md` is where the
|
|
9
|
+
first seven are reasoned; `papers/quo-dock.md` and the papers of its sides
|
|
10
|
+
are where the rest are built.
|
|
11
|
+
|
|
12
|
+
## A person and a world
|
|
13
|
+
|
|
14
|
+
- **org**: the unit every "you" names: a company, a family, a club, a
|
|
15
|
+
service, and at the smallest one human alone, an org of one. An org owns
|
|
16
|
+
wards, holds standings, trusts boxes and is trusted. Never an avatar, an
|
|
17
|
+
account or a user id; each of those is a thing an org holds.
|
|
18
|
+
- **world**: a ward, as a human sees it: one seed, one partition, one body
|
|
19
|
+
of code, one owner.
|
|
20
|
+
- **home**: the one ward an org owns on a harbor it trusts; the org's own
|
|
21
|
+
being is its being. Also that being's standing at the ward's own door as
|
|
22
|
+
an owner, `home`. One per org, and an org may have none yet.
|
|
23
|
+
- **org being**: home's own being, one per org, running day and night,
|
|
24
|
+
holding the org's standings and its members as occupants. An estate's
|
|
25
|
+
own class for an organisation; the user being for an org of one.
|
|
26
|
+
- **presence**: a ward a person owns on a harbor in her hand, one per world
|
|
27
|
+
reached from that device. The tab, the phone and the desk are presences
|
|
28
|
+
of the same shape, and nothing in a hand is a home.
|
|
29
|
+
- **presence being**: the being of a world who lives on a device, booted in
|
|
30
|
+
the presence ward when the world is joined. She lends every faculty the
|
|
31
|
+
box has at her birth and carries them, so a world reaches the device only
|
|
32
|
+
by asking her.
|
|
33
|
+
- **app**: a browser of worlds, one harbor and a presence ward per world,
|
|
34
|
+
the same shell on every terrain. It has no account and no person of its
|
|
35
|
+
own; which app a person uses is settled at home, and an adopter's app is
|
|
36
|
+
only the first one its member meets.
|
|
37
|
+
- **membership**: an id in a ward someone else owns. An employee id, a
|
|
38
|
+
member id, a customer id. Never a ward of hers.
|
|
39
|
+
- **hub**: home, as the one place every relation of hers meets.
|
|
40
|
+
- **rule**: home answering one standing by asking another.
|
|
41
|
+
- **device**: a harbor in her hand, with a ward of its own holding what that
|
|
42
|
+
box can do. One OS user is one harbor, and the app has no people of its
|
|
43
|
+
own.
|
|
44
|
+
|
|
45
|
+
## The dock
|
|
46
|
+
|
|
47
|
+
- **dock**: what every estate needs and nobody writes twice,
|
|
48
|
+
`packages/dock/`, published as `@nervur-org/dock`.
|
|
49
|
+
- **dock ward**: the ward a harbor boots and roots for itself, holding the
|
|
50
|
+
faculties that box lends. Never a home, and never migrated.
|
|
51
|
+
- **estate**: all of one owner's harbors, with what is theirs alone, in a
|
|
52
|
+
tree of the owner's own and never published.
|
|
53
|
+
- **daemon**: `quo serve`, the one process on a device that holds a harbor.
|
|
54
|
+
- **route**: a hostname a reverse proxy sends to one process: `web.`,
|
|
55
|
+
`quo.`, `mcp.`, `api.`.
|
|
56
|
+
- **user being**: the org being of an org of one: a human's own, holding
|
|
57
|
+
all of her standings. The dock's `User` class, which also serves a
|
|
58
|
+
membership the desk makes in another org's ward.
|
|
59
|
+
- **front desk**: the being every fresh client meets; hands out invitations
|
|
60
|
+
minted by the ward's being, and is what `quo init` marks public in a
|
|
61
|
+
fresh ward. Any ward may mark one being of any class public instead.
|
|
62
|
+
- **avatar**: a being that is someone's voice from outside Quo, one per
|
|
63
|
+
client identity.
|
|
64
|
+
- **side**: the interface an avatar speaks through to someone outside Quo:
|
|
65
|
+
a screen, an MCP server, a runner, an agent.
|
|
66
|
+
- **client identity**: what a credential exchange establishes; one avatar,
|
|
67
|
+
for life.
|
|
68
|
+
- **proof**: a vouch the front desk trades once for an invitation: the
|
|
69
|
+
daemon's nonce for a local side, or the link a world mailed to an address
|
|
70
|
+
it wants proof of. Everyone else arrives with an invitation.
|
|
71
|
+
- **way in**: a being who says who a proof of one kind is for, and who sends
|
|
72
|
+
the proof out to an address where the kind has one. The desk holds a
|
|
73
|
+
standing at each, under `way:<kind>`: the box's, lent by the dock ward,
|
|
74
|
+
and the world's own, in the world's ward.
|
|
75
|
+
- **carrier**: a being who shows her standings' asks as her own and
|
|
76
|
+
forwards. The user being and the device are two.
|
|
77
|
+
- **envoy**: a being of the org's standing in for a ward nobody here runs,
|
|
78
|
+
one being per vendor and one relation per account, on the base
|
|
79
|
+
`beings/envoy.ts`. Her asks out are the vendor's surface, narrowed; her
|
|
80
|
+
one ask in, `deliver`, is the owner's, the vendor's bytes reaching her
|
|
81
|
+
own body through the api route and never a door; her credential is in
|
|
82
|
+
her cells, placed by the owner and shown to nobody; and a delivery is one
|
|
83
|
+
ask on the way back her occupant handed her at the knock.
|
|
84
|
+
- **faculty**: a being of the dock ward, standing for one thing the box can
|
|
85
|
+
do: a timer, a line, a store, a vendor's API. Reached by `lend`, held as
|
|
86
|
+
an ordinary standing, and she wakes whoever invited her. She holds what
|
|
87
|
+
the device holds and begins holding it at birth.
|
|
88
|
+
- **ring**: one platform's way of waking a device by its token, held by the
|
|
89
|
+
push faculty: `web`, `apns`, `fcm`. It carries the banner sealed to that
|
|
90
|
+
device's banner key, and nothing else.
|
|
91
|
+
- **bell**: a ward that wakes devices, with a public being for a phone
|
|
92
|
+
nobody knows and one doorbell per device. `papers/quo-app-bells.md`.
|
|
93
|
+
- **doorbell**: one device's being at a bell. Its cells hold that device's
|
|
94
|
+
platform token, and its occupants are the worlds that device allowed to
|
|
95
|
+
wake it, one standing each.
|
|
96
|
+
- **porter**: a bell's public being, where a phone nobody knows arrives as
|
|
97
|
+
`{}` and comes away with a doorbell of its own.
|
|
98
|
+
- **banner key**: the pair a device keeps in the cells of its notifications
|
|
99
|
+
faculty. A world seals a notification to its public half; the secret half
|
|
100
|
+
opens banners and nothing else, and never the world.
|
|
101
|
+
- **timer**: the faculty that reads the box's clock, and the only thing on a
|
|
102
|
+
device that may. She wakes whoever invited her, every minute.
|
|
103
|
+
- **clock**: the being who wakes beings at a time, on standings the root
|
|
104
|
+
placed. She holds the schedules and invited the timer.
|
|
105
|
+
- **gate**: `for(occupant, asker)`; the whole permission model.
|
|
106
|
+
- **look**: how a being is presented, as values; one optional ask.
|
|
107
|
+
- **page**: a being's page as a tree of values in the screen's grammar; one
|
|
108
|
+
optional ask, read by a screen alone.
|
|
109
|
+
- **owner pilot**: `quo pilot`, the model side over a ward's owner asks,
|
|
110
|
+
reaching the daemon's socket; with `--via`, a ward elsewhere through a
|
|
111
|
+
standing. Under cron, the estate's own agent.
|
|
112
|
+
|
|
113
|
+
## The model side
|
|
114
|
+
|
|
115
|
+
- **constitution**: the folder and instructions an autonomous agent runs
|
|
116
|
+
under; governance, never permission.
|
|
117
|
+
- **runner**: an inline MCP client for a model that speaks function
|
|
118
|
+
calling; it drives the loop the MCP client would, over one conversation.
|