@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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { BrowserHarbor } from '../harbor/browser.js';
|
|
2
|
+
import { drained, notice } from './wake.js';
|
|
3
|
+
// The line the ring carried, when it carried one. A Web Push body is
|
|
4
|
+
// encrypted to this subscription's own keys and the browser has already
|
|
5
|
+
// opened it by the time it arrives here, so this is the world's own line
|
|
6
|
+
// and not a guess at one. A ring that carried nothing, and a body that is
|
|
7
|
+
// not a line, are the same thing: nothing to show yet.
|
|
8
|
+
const carried = (event) => {
|
|
9
|
+
try {
|
|
10
|
+
const said = event.data?.json();
|
|
11
|
+
if (!said || typeof said.title !== 'string')
|
|
12
|
+
return null;
|
|
13
|
+
return { title: said.title, body: typeof said.body === 'string' ? said.body : '' };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const sw = globalThis;
|
|
20
|
+
// Every step of a wake is bounded: a push event lives for as long as the
|
|
21
|
+
// platform gives it, and a wake that found nothing shows nothing.
|
|
22
|
+
const within = (ms, work, floor) => Promise.race([work, new Promise((ok) => setTimeout(() => ok(floor), ms))]);
|
|
23
|
+
// A page that is open holds the harbor. It is handed a port, it drains on
|
|
24
|
+
// its own harbor and answers what it found; the first page to answer is the
|
|
25
|
+
// one that held it, and no page answering means none did.
|
|
26
|
+
async function fromPage() {
|
|
27
|
+
const pages = await sw.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
|
28
|
+
if (pages.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
return within(5000, new Promise((ok) => {
|
|
31
|
+
for (const page of pages) {
|
|
32
|
+
const channel = new MessageChannel();
|
|
33
|
+
channel.port1.onmessage = (e) => {
|
|
34
|
+
const said = (e.data ?? {});
|
|
35
|
+
if (Array.isArray(said.missed))
|
|
36
|
+
ok(said.missed);
|
|
37
|
+
};
|
|
38
|
+
page.postMessage({ quo: 'wake' }, [channel.port2]);
|
|
39
|
+
}
|
|
40
|
+
}), null);
|
|
41
|
+
}
|
|
42
|
+
// No page open: this worker is the device's harbor for the length of the
|
|
43
|
+
// wake. A boot that meets the lock is a page that holds it and did not
|
|
44
|
+
// answer, and this worker leaves it alone rather than opening a second
|
|
45
|
+
// harbor over one seed.
|
|
46
|
+
async function here(cfg) {
|
|
47
|
+
const harbor = new BrowserHarbor('quo');
|
|
48
|
+
try {
|
|
49
|
+
await harbor.boot();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
harbor.dial(cfg.quo);
|
|
56
|
+
return await within(20000, drained(harbor, Object.values(cfg.wards).map((w) => w.pk)), []);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await harbor.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function woken(cfg, said) {
|
|
63
|
+
const objects = (await fromPage()) ?? (await here(cfg));
|
|
64
|
+
// What the world sent is what the human reads, since the world knows what
|
|
65
|
+
// it rang about and this worker knows only what it managed to drain. What
|
|
66
|
+
// was drained composes the line when the ring carried none, and a ring
|
|
67
|
+
// with neither is a device that was already told: a notification saying
|
|
68
|
+
// so would be the doorbell inventing news.
|
|
69
|
+
if (!said && objects.length === 0)
|
|
70
|
+
return;
|
|
71
|
+
const { title, body } = said ?? notice(objects);
|
|
72
|
+
// Bounded like every other step, because a browser build with no
|
|
73
|
+
// notification platform under it answers this call neither way, and a
|
|
74
|
+
// wake that waits on it is a worker the browser keeps alive for nothing.
|
|
75
|
+
await within(5000, sw.registration.showNotification(title, { body, tag: 'quo' }), undefined);
|
|
76
|
+
}
|
|
77
|
+
const cfg = sw.quo;
|
|
78
|
+
sw.addEventListener('install', () => void sw.skipWaiting());
|
|
79
|
+
sw.addEventListener('activate', (e) => e.waitUntil(sw.clients.claim()));
|
|
80
|
+
if (cfg)
|
|
81
|
+
sw.addEventListener('push', (e) => e.waitUntil(woken(cfg, carried(e))));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Harbor, Hosted } from '@nervur-org/nervur/harbor';
|
|
2
|
+
import { Avatar } from '../beings/avatar.ts';
|
|
3
|
+
export type World = {
|
|
4
|
+
pk: string;
|
|
5
|
+
at: string;
|
|
6
|
+
name: string;
|
|
7
|
+
home?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type Joined = {
|
|
10
|
+
read(): World[];
|
|
11
|
+
write(worlds: World[]): void;
|
|
12
|
+
};
|
|
13
|
+
export declare const KEY = "quo-worlds";
|
|
14
|
+
export declare function inStorage(storage: Pick<Storage, 'getItem' | 'setItem'>): Joined;
|
|
15
|
+
export declare function joined(list: World[], met: World): World[];
|
|
16
|
+
export declare function ordered(list: World[]): World[];
|
|
17
|
+
export declare const localName: (pk: string) => string;
|
|
18
|
+
export declare function world(harbor: Harbor, pk: string): Promise<Hosted>;
|
|
19
|
+
export type Relation = {
|
|
20
|
+
key: string;
|
|
21
|
+
avatar: Avatar;
|
|
22
|
+
};
|
|
23
|
+
export declare function relations(ward: Hosted): Relation[];
|
|
24
|
+
export declare function fresh(ward: Hosted): Promise<Relation>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Avatar, USER } from '../beings/avatar.js';
|
|
2
|
+
import { settled } from '../dock/faculty.js';
|
|
3
|
+
// The list as a browser keeps it, the tab's and the app's alike: one item,
|
|
4
|
+
// under the key the tab has always used. A device with no storage still
|
|
5
|
+
// works and forgets, which is why every read has a floor.
|
|
6
|
+
export const KEY = 'quo-worlds';
|
|
7
|
+
export function inStorage(storage) {
|
|
8
|
+
return {
|
|
9
|
+
read: () => {
|
|
10
|
+
try {
|
|
11
|
+
const raw = JSON.parse(storage.getItem(KEY) ?? 'null');
|
|
12
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
13
|
+
return [];
|
|
14
|
+
return Object.entries(raw).flatMap(([pk, v]) => {
|
|
15
|
+
const w = (v ?? {});
|
|
16
|
+
const at = typeof w.at === 'string' ? w.at : typeof w.url === 'string' ? w.url : '';
|
|
17
|
+
return at ? [{ pk, at, name: typeof w.name === 'string' ? w.name : '', ...(w.home === true ? { home: true } : {}) }] : [];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
write: (worlds) => {
|
|
25
|
+
try {
|
|
26
|
+
storage.setItem(KEY, JSON.stringify(Object.fromEntries(worlds.map((w) => [w.pk, { at: w.at, url: w.at, name: w.name, ...(w.home ? { home: true } : {}) }]))));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* forgets */
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// One world joined, or met again: the same pk keeps its place in the list
|
|
35
|
+
// and takes the newer address, any name it has gained and whether it is a
|
|
36
|
+
// home, and a world not seen before goes to the end. The list is in the
|
|
37
|
+
// order they were joined; `ordered` is how a switcher shows them.
|
|
38
|
+
export function joined(list, met) {
|
|
39
|
+
const at = list.findIndex((w) => w.pk === met.pk);
|
|
40
|
+
if (at === -1)
|
|
41
|
+
return [...list, met];
|
|
42
|
+
const out = [...list];
|
|
43
|
+
out[at] = { ...out[at], at: met.at, ...(met.name ? { name: met.name } : {}), ...(met.home !== undefined ? { home: met.home } : {}) };
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
// Home first, memberships behind it, each in the order joined: what a
|
|
47
|
+
// person thinks of as "me" before the worlds that know an id of hers. A
|
|
48
|
+
// membership with no home stands where it is.
|
|
49
|
+
export function ordered(list) {
|
|
50
|
+
return [...list.filter((w) => w.home), ...list.filter((w) => !w.home)];
|
|
51
|
+
}
|
|
52
|
+
// The local ward for a far world, by its pk: created on first sight, on
|
|
53
|
+
// the tab's own seed, empty.
|
|
54
|
+
export const localName = (pk) => `w-${pk.slice(0, 16)}`;
|
|
55
|
+
export async function world(harbor, pk) {
|
|
56
|
+
const name = localName(pk);
|
|
57
|
+
return harbor.wards.get(name) ?? (await harbor.create(name, 'me'));
|
|
58
|
+
}
|
|
59
|
+
export function relations(ward) {
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const key of ward.keys()) {
|
|
62
|
+
const a = ward.being(key);
|
|
63
|
+
if (a instanceof Avatar && a.standings[USER])
|
|
64
|
+
out.push({ key, avatar: a });
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
// A presence being for a relation not yet made: one who holds no standing
|
|
69
|
+
// at a world, whether left by a guest who never came in or booted now under
|
|
70
|
+
// the next free key. She is the one who knocks, as a guest or with an
|
|
71
|
+
// invitation, and she becomes a relation the moment she takes her standing.
|
|
72
|
+
// She is the being of that world who lives on this device, holding what the
|
|
73
|
+
// box lends from her birth, and a bare tab lends her nothing.
|
|
74
|
+
export async function fresh(ward) {
|
|
75
|
+
for (const key of ward.keys()) {
|
|
76
|
+
const a = ward.being(key);
|
|
77
|
+
if (a instanceof Avatar && !a.standings[USER])
|
|
78
|
+
return { key, avatar: a };
|
|
79
|
+
}
|
|
80
|
+
// The next free key, found by asking rather than by reading a table: boot
|
|
81
|
+
// refuses a key that has a row, so the ward is the one that knows, and a
|
|
82
|
+
// row whose class did not construct this run is a taken key the harbor
|
|
83
|
+
// has no object for. The bound is there because a loop that asks forever
|
|
84
|
+
// is a tab that hangs.
|
|
85
|
+
for (let n = 1; n <= 64; n++) {
|
|
86
|
+
const key = `r${n}`;
|
|
87
|
+
const out = (await ward.ask('boot', { key, class: 'Presence' }));
|
|
88
|
+
if (out.error)
|
|
89
|
+
continue;
|
|
90
|
+
// She takes what the box lends at her birth, and birth is synchronous
|
|
91
|
+
// while taking a standing is not. She is handed back when they have
|
|
92
|
+
// landed, not before: a screen painted from her describe a moment early
|
|
93
|
+
// is a page missing every faculty of the box, and nothing paints it
|
|
94
|
+
// again until the person navigates.
|
|
95
|
+
await settled();
|
|
96
|
+
return { key, avatar: ward.being(key) };
|
|
97
|
+
}
|
|
98
|
+
throw new Error('the tab could not boot an avatar: every key it tries is taken');
|
|
99
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Json, JsonObject } from '@nervur-org/nervur';
|
|
2
|
+
import type { Avatar } from '../beings/avatar.ts';
|
|
3
|
+
import { type Serving } from '../beings/side.ts';
|
|
4
|
+
export type Run = (event: JsonObject) => Promise<Json>;
|
|
5
|
+
export declare const REPORT = "report";
|
|
6
|
+
export declare function processRun(command: string, args: string[], dir: string): Run;
|
|
7
|
+
export type Agent = Serving & {
|
|
8
|
+
idle(): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export declare function agentSide(avatar: Avatar, run: Run, after?: () => Promise<void>): Agent;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The model side as an event-driven agent: the world is the loop. The user
|
|
3
|
+
// being holds a standing to the agent's avatar and asks her when something
|
|
4
|
+
// happens; that ask is a push, and her `notify` answers at once, so the ask
|
|
5
|
+
// is never held open. The push lands in her inbox, which is the queue, in
|
|
6
|
+
// her cells. This side drains the inbox one event at a time: it starts a
|
|
7
|
+
// run with the event, and when the run ends she asks the user being back on
|
|
8
|
+
// her own standing with the result. One run at a time, in the order the
|
|
9
|
+
// events came, across a restart too, because the queue is in the partition
|
|
10
|
+
// and not in this process. What a run may do is the gate; how it behaves
|
|
11
|
+
// is its constitution, the folder it runs in.
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { word, wordText, SILENCE_TEXT, UNREACHED_TEXT } from '../beings/side.js';
|
|
14
|
+
// The ask on the user being that takes the result. Gated to devices; the
|
|
15
|
+
// agent's client identity is what the gate reads.
|
|
16
|
+
export const REPORT = 'report';
|
|
17
|
+
// `claude -p` or any CLI agent: the command runs in the agent's folder with
|
|
18
|
+
// the event, as JSON, as its last argument. The result is what it printed.
|
|
19
|
+
// A non-zero exit is an error object with what it said on stderr.
|
|
20
|
+
export function processRun(command, args, dir) {
|
|
21
|
+
return (event) => new Promise((ok) => {
|
|
22
|
+
const child = spawn(command, [...args, JSON.stringify(event)], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
23
|
+
let out = '';
|
|
24
|
+
let err = '';
|
|
25
|
+
child.stdout.on('data', (c) => (out += c.toString()));
|
|
26
|
+
child.stderr.on('data', (c) => (err += c.toString()));
|
|
27
|
+
child.on('error', (e) => ok({ error: e.message }));
|
|
28
|
+
child.on('close', (code) => ok(code === 0 ? { output: out.trim() } : { error: `exit ${code}`, stderr: err.trim() }));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// `after` runs when her cells changed and when an ask is done, as in the
|
|
32
|
+
// other sides: a harbor that must write what the ward changed hooks it.
|
|
33
|
+
export function agentSide(avatar, run, after = async () => { }) {
|
|
34
|
+
let draining = Promise.resolve();
|
|
35
|
+
let open = true;
|
|
36
|
+
const inbox = () => avatar.cells.inbox;
|
|
37
|
+
async function one(event) {
|
|
38
|
+
let result;
|
|
39
|
+
try {
|
|
40
|
+
result = await run(event);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
result = { error: e instanceof Error ? e.message : String(e) };
|
|
44
|
+
}
|
|
45
|
+
// The report is an ask like any other. Silence and unreached are told
|
|
46
|
+
// apart and dropped: a report is never retried, and never held.
|
|
47
|
+
const w = word(await avatar.call(REPORT, { event, result }));
|
|
48
|
+
if (w.word === 'silence' || w.word === 'unreached' || w.word === 'quo')
|
|
49
|
+
say({ dropped: w.word === 'silence' ? SILENCE_TEXT : w.word === 'quo' ? wordText(w.name) : UNREACHED_TEXT });
|
|
50
|
+
await after();
|
|
51
|
+
}
|
|
52
|
+
// One at a time, first in first out, until the inbox is empty.
|
|
53
|
+
function drain() {
|
|
54
|
+
draining = draining.then(async () => {
|
|
55
|
+
while (open && inbox().length > 0) {
|
|
56
|
+
const event = inbox().shift();
|
|
57
|
+
await after(); // the event left the queue before the run starts: a crash mid-run loses it, and never runs it twice
|
|
58
|
+
await one(event);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return draining;
|
|
62
|
+
}
|
|
63
|
+
// A dropped report is told to her other sides, if any: the agent itself has no ear for it.
|
|
64
|
+
const say = (object) => { for (const ear of avatar.ears)
|
|
65
|
+
if (ear !== ear0)
|
|
66
|
+
ear(object); };
|
|
67
|
+
const ear0 = () => void drain();
|
|
68
|
+
avatar.ears.add(ear0);
|
|
69
|
+
void drain(); // whatever waited while no side was up
|
|
70
|
+
return {
|
|
71
|
+
idle: () => draining,
|
|
72
|
+
close: async () => {
|
|
73
|
+
open = false;
|
|
74
|
+
avatar.ears.delete(ear0);
|
|
75
|
+
await draining;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Invitation } from '@nervur-org/nervur';
|
|
2
|
+
import type { OAuth } from './oauth.ts';
|
|
3
|
+
export type Join = (identity: string, invitation: Invitation, ward: string) => Promise<{
|
|
4
|
+
error?: string;
|
|
5
|
+
}>;
|
|
6
|
+
export type Options = {
|
|
7
|
+
oauth: OAuth;
|
|
8
|
+
join: Join;
|
|
9
|
+
worlds: () => {
|
|
10
|
+
ward: string;
|
|
11
|
+
pk: string;
|
|
12
|
+
user: string;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
export declare const suggest: (name: string) => string;
|
|
16
|
+
export declare class Exchange {
|
|
17
|
+
readonly o: Options;
|
|
18
|
+
constructor(o: Options);
|
|
19
|
+
handle(req: Request, rest: string): Promise<Response | undefined>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readForm } from './oauth.js';
|
|
2
|
+
import { invitationOf } from '../beings/link.js';
|
|
3
|
+
import { CSS } from '../human/style.js';
|
|
4
|
+
import { POLICY } from '../human/door.js';
|
|
5
|
+
import { MARK_SVG } from '../human/mark.js';
|
|
6
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c);
|
|
7
|
+
const word = (s) => (typeof s === 'string' && /^[\w.-]{1,40}$/.test(s) ? s : null);
|
|
8
|
+
// A client's name as an identity: lowercase words joined by dashes.
|
|
9
|
+
export const suggest = (name) => name
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-|-$/g, '')
|
|
13
|
+
.slice(0, 40) || 'client';
|
|
14
|
+
export class Exchange {
|
|
15
|
+
o;
|
|
16
|
+
constructor(o) {
|
|
17
|
+
this.o = o;
|
|
18
|
+
}
|
|
19
|
+
// A page in, a page out: this side answers as the endpoint beside it does,
|
|
20
|
+
// and a path that is not one of its two is nothing it declines by
|
|
21
|
+
// answering nothing.
|
|
22
|
+
async handle(req, rest) {
|
|
23
|
+
const url = new URL(req.url);
|
|
24
|
+
const page = (status, body) => new Response(shell(body), { status, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': POLICY, 'referrer-policy': 'no-referrer' } });
|
|
25
|
+
const go = (to) => new Response(null, { status: 302, headers: { location: to, 'cache-control': 'no-store' } });
|
|
26
|
+
if (rest === '/allow' && req.method === 'GET') {
|
|
27
|
+
const request = url.searchParams.get('request') ?? '';
|
|
28
|
+
const p = this.o.oauth.pending(request);
|
|
29
|
+
if (!p)
|
|
30
|
+
return page(400, `<h1>Nothing to allow</h1><p>This request is gone. Start again from the app that sent you here.</p>`);
|
|
31
|
+
return page(200, allowForm(request, p.client.client_name, p.redirect_uri, suggest(p.client.client_name), this.o.worlds()));
|
|
32
|
+
}
|
|
33
|
+
if (rest === '/allow' && req.method === 'POST') {
|
|
34
|
+
const f = await readForm(req);
|
|
35
|
+
const request = f.get('request') ?? '';
|
|
36
|
+
const p = this.o.oauth.pending(request);
|
|
37
|
+
if (!p)
|
|
38
|
+
return page(400, `<h1>Nothing to allow</h1><p>This request is gone.</p>`);
|
|
39
|
+
if (f.get('decision') !== 'allow') {
|
|
40
|
+
const out = await this.o.oauth.deny(request);
|
|
41
|
+
return 'redirect' in out ? go(out.redirect) : page(400, `<h1>Gone</h1>`);
|
|
42
|
+
}
|
|
43
|
+
const worlds = this.o.worlds();
|
|
44
|
+
const again = (err) => page(400, allowForm(request, p.client.client_name, p.redirect_uri, word(f.get('identity')) ?? suggest(p.client.client_name), worlds, err));
|
|
45
|
+
const inv = invitationOf(f.get('invitation') ?? '');
|
|
46
|
+
if (!inv)
|
|
47
|
+
return again('That is not an invitation.');
|
|
48
|
+
const world = worlds.find((w) => w.pk === inv.ward);
|
|
49
|
+
if (!world)
|
|
50
|
+
return again('That invitation is for a world that does not live here.');
|
|
51
|
+
const identity = word(f.get('identity'));
|
|
52
|
+
if (identity === null || identity === world.user || identity === 'desk')
|
|
53
|
+
return again('An identity is one word, and not the user or the desk.');
|
|
54
|
+
const joined = await this.o.join(identity, inv, world.ward);
|
|
55
|
+
if (joined.error)
|
|
56
|
+
return again(`Not in: ${joined.error}.`);
|
|
57
|
+
const out = await this.o.oauth.complete(request, identity, world.ward);
|
|
58
|
+
return 'redirect' in out ? go(out.redirect) : page(400, `<h1>Gone</h1>`);
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// The one stylesheet, and no colour of this page's own. The policy is the
|
|
64
|
+
// one every page Quo writes itself is served under, `human/door.ts`, since
|
|
65
|
+
// this is one of them: it runs no script of its own and wants none.
|
|
66
|
+
const shell = (body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>quo</title>
|
|
67
|
+
<style>${CSS}</style>
|
|
68
|
+
</head><body><main class="door">${body}</main></body></html>`;
|
|
69
|
+
// The one page, and every sentence on it is something a human needs in
|
|
70
|
+
// order to decide. Who is asking, what it will be able to see, that it is
|
|
71
|
+
// bounded by a gate rather than by a promise, that she can end it, and
|
|
72
|
+
// where it goes when she is done. The world is the invitation's, so the
|
|
73
|
+
// page says which worlds live here and whose they are, and asks her to
|
|
74
|
+
// choose none of them.
|
|
75
|
+
const allowForm = (request, client, redirect, identity, worlds, err = '') => {
|
|
76
|
+
const whose = worlds.map((w) => `<strong>${esc(w.user)}</strong> in ${esc(w.ward)}`).join(', ');
|
|
77
|
+
return `${MARK_SVG}
|
|
78
|
+
<h1 class="t-title">Allow ${esc(client)}?</h1>
|
|
79
|
+
<p class="t-lead"><strong>${esc(client)}</strong> asks to be let into ${whose || 'a world here'}, as an occupant under a name you give it.</p>
|
|
80
|
+
<p>It will see exactly what that world shows that name, decided by her gate and by nothing it can ask for. It cannot become anything else later, and it can be taken out of the world at any time.</p>
|
|
81
|
+
<p class="t-quiet">The invitation below is the whole credential. It is spent once, here, and nothing is stored afterwards: no password, no cookie, no session. If you did not mint it for ${esc(client)} yourself, close this page.</p>
|
|
82
|
+
<p class="t-quiet">When you are done it returns to <code>${esc(redirect)}</code>.</p>
|
|
83
|
+
${err ? `<p class="answer error">${esc(err)}</p>` : ''}
|
|
84
|
+
<form method="post" action=""><input type="hidden" name="request" value="${esc(request)}">
|
|
85
|
+
<p><label for="i">The name it will be known by</label><input id="i" name="identity" value="${esc(identity)}" pattern="[\\w.-]{1,40}" required></p>
|
|
86
|
+
<p><label for="v">The invitation you minted for it</label><textarea id="v" name="invitation" autofocus required placeholder="as the link, or as printed"></textarea></p>
|
|
87
|
+
<p><button type="submit" name="decision" value="allow">Allow</button> <button type="submit" name="decision" value="deny">Deny</button></p></form>`;
|
|
88
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WebStandardStreamableHTTPServerTransport as StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
2
|
+
import type { Avatar } from '../beings/avatar.ts';
|
|
3
|
+
import type { Serving } from '../beings/side.ts';
|
|
4
|
+
export type Session = {
|
|
5
|
+
identity: string;
|
|
6
|
+
ward: string;
|
|
7
|
+
transport: StreamableHTTPServerTransport;
|
|
8
|
+
serving: Serving;
|
|
9
|
+
touched: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const SESSION_IDLE: number;
|
|
12
|
+
export type Resolve = (identity: string, ward: string) => Promise<{
|
|
13
|
+
avatar?: Avatar;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare class McpHttp {
|
|
17
|
+
readonly sessions: Map<string, Session>;
|
|
18
|
+
readonly resolve: Resolve;
|
|
19
|
+
readonly after: () => Promise<void>;
|
|
20
|
+
now: () => number;
|
|
21
|
+
gone: (identity: string, ward: string) => Promise<void>;
|
|
22
|
+
constructor(resolve: Resolve, after?: () => Promise<void>);
|
|
23
|
+
handle(req: Request, identity: string, ward: string): Promise<Response>;
|
|
24
|
+
sweep(): void;
|
|
25
|
+
drop(sid: string): void;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
package/dist/mcp/http.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The MCP endpoint on the mcp route: streamable HTTP, one session per
|
|
3
|
+
// client connection, one avatar per client identity. A bearer names the
|
|
4
|
+
// identity, the identity names the avatar, and the side from `server.ts`
|
|
5
|
+
// runs beside her exactly as it does over stdio. A session belongs to the
|
|
6
|
+
// identity that opened it; a bearer of another identity on the same session
|
|
7
|
+
// is refused. Sessions live in memory and die with the daemon, which is
|
|
8
|
+
// what a reconnect is for: the standing is the state, and it is on disk. A
|
|
9
|
+
// client that goes away without a DELETE leaves a session behind; one idle
|
|
10
|
+
// for an hour is dropped, since HTTP tells the server nothing else.
|
|
11
|
+
import { WebStandardStreamableHTTPServerTransport as StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
12
|
+
import { mcpSide } from './server.js';
|
|
13
|
+
export const SESSION_IDLE = 60 * 60 * 1000;
|
|
14
|
+
export class McpHttp {
|
|
15
|
+
sessions = new Map();
|
|
16
|
+
resolve;
|
|
17
|
+
after;
|
|
18
|
+
now = () => Date.now();
|
|
19
|
+
// What to do with an identity the user being has removed: the route's
|
|
20
|
+
// revoke, so the client's tokens go with the occupant. Set by whoever
|
|
21
|
+
// mounts the route beside the credential exchange.
|
|
22
|
+
gone = async () => { };
|
|
23
|
+
constructor(resolve, after = async () => { }) {
|
|
24
|
+
this.resolve = resolve;
|
|
25
|
+
this.after = after;
|
|
26
|
+
}
|
|
27
|
+
// `identity` is what the bearer named; the caller has already turned a
|
|
28
|
+
// stranger away. A request on a known session goes to it. A request with
|
|
29
|
+
// no session opens one, if it is an initialize; anything else is 400.
|
|
30
|
+
async handle(req, identity, ward) {
|
|
31
|
+
const sid = req.headers.get('mcp-session-id') ?? undefined;
|
|
32
|
+
const json = (status, body) => Response.json(body, { status });
|
|
33
|
+
this.sweep();
|
|
34
|
+
if (sid !== undefined) {
|
|
35
|
+
const s = this.sessions.get(sid);
|
|
36
|
+
if (!s)
|
|
37
|
+
return json(404, { jsonrpc: '2.0', error: { code: -32001, message: 'no such session' }, id: null });
|
|
38
|
+
if (s.identity !== identity || s.ward !== ward)
|
|
39
|
+
return json(403, { jsonrpc: '2.0', error: { code: -32003, message: 'not your session' }, id: null });
|
|
40
|
+
s.touched = this.now();
|
|
41
|
+
const out = await s.transport.handleRequest(req);
|
|
42
|
+
if (req.method === 'DELETE')
|
|
43
|
+
this.drop(sid);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
if (req.method !== 'POST')
|
|
47
|
+
return json(400, { jsonrpc: '2.0', error: { code: -32000, message: 'no session' }, id: null });
|
|
48
|
+
const found = await this.resolve(identity, ward);
|
|
49
|
+
// A session opens on her describe, and admit already asked it. The one
|
|
50
|
+
// word the door says for an identity the user being removed is
|
|
51
|
+
// `removed`, under the key it bound for her avatar: in MCP's vocabulary
|
|
52
|
+
// that is 401, the client drops its token and starts the exchange again,
|
|
53
|
+
// and the route forgets the grant.
|
|
54
|
+
if (found.error === 'removed') {
|
|
55
|
+
await this.gone(identity, ward);
|
|
56
|
+
for (const [key, s] of this.sessions)
|
|
57
|
+
if (s.identity === identity && s.ward === ward)
|
|
58
|
+
this.drop(key);
|
|
59
|
+
return json(401, { jsonrpc: '2.0', error: { code: -32001, message: 'removed: the user being removed this identity' }, id: null });
|
|
60
|
+
}
|
|
61
|
+
if (!found.avatar)
|
|
62
|
+
return json(500, { jsonrpc: '2.0', error: { code: -32002, message: found.error ?? 'no avatar' }, id: null });
|
|
63
|
+
const transport = new StreamableHTTPServerTransport({
|
|
64
|
+
// the web crypto's, not node's: a session id is a fresh uuid on every
|
|
65
|
+
// runtime and this endpoint names no terrain
|
|
66
|
+
sessionIdGenerator: () => globalThis.crypto.randomUUID(),
|
|
67
|
+
onsessioninitialized: (s) => {
|
|
68
|
+
this.sessions.set(s, { identity, ward, transport, serving, touched: this.now() });
|
|
69
|
+
},
|
|
70
|
+
onsessionclosed: (s) => this.drop(s),
|
|
71
|
+
});
|
|
72
|
+
const serving = await mcpSide(found.avatar, transport, this.after);
|
|
73
|
+
return transport.handleRequest(req);
|
|
74
|
+
}
|
|
75
|
+
sweep() {
|
|
76
|
+
const now = this.now();
|
|
77
|
+
for (const [sid, s] of this.sessions)
|
|
78
|
+
if (now - s.touched > SESSION_IDLE)
|
|
79
|
+
this.drop(sid);
|
|
80
|
+
}
|
|
81
|
+
drop(sid) {
|
|
82
|
+
const s = this.sessions.get(sid);
|
|
83
|
+
if (!s)
|
|
84
|
+
return;
|
|
85
|
+
this.sessions.delete(sid);
|
|
86
|
+
void s.serving.close().catch(() => { });
|
|
87
|
+
}
|
|
88
|
+
async close() {
|
|
89
|
+
for (const sid of [...this.sessions.keys()])
|
|
90
|
+
this.drop(sid);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type Grants } from '../dock/grants.ts';
|
|
2
|
+
export { ACCESS_TTL, REFRESH_TTL } from '../dock/grants.ts';
|
|
3
|
+
export declare const CODE_TTL: number;
|
|
4
|
+
export type Client = {
|
|
5
|
+
client_id: string;
|
|
6
|
+
client_name: string;
|
|
7
|
+
redirect_uris: string[];
|
|
8
|
+
exp: number;
|
|
9
|
+
};
|
|
10
|
+
export type Pending = {
|
|
11
|
+
client_id: string;
|
|
12
|
+
redirect_uri: string;
|
|
13
|
+
challenge: string;
|
|
14
|
+
state: string | null;
|
|
15
|
+
resource: string | null;
|
|
16
|
+
exp: number;
|
|
17
|
+
};
|
|
18
|
+
export type Dance = {
|
|
19
|
+
clients: Record<string, Client>;
|
|
20
|
+
pending: Record<string, Pending>;
|
|
21
|
+
codes: Record<string, Pending & {
|
|
22
|
+
identity: string;
|
|
23
|
+
ward: string;
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
export declare const emptyDance: () => Dance;
|
|
27
|
+
export type Store = {
|
|
28
|
+
load(): Promise<Dance>;
|
|
29
|
+
save(dance: Dance): Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
export type Options = {
|
|
32
|
+
issuer: string;
|
|
33
|
+
resource: string;
|
|
34
|
+
finish: (request: string) => string;
|
|
35
|
+
store: Store;
|
|
36
|
+
grants: Grants;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
};
|
|
39
|
+
export declare class OAuth {
|
|
40
|
+
readonly o: Options;
|
|
41
|
+
readonly dance: Dance;
|
|
42
|
+
static open(o: Options): Promise<OAuth>;
|
|
43
|
+
constructor(o: Options, dance: Dance);
|
|
44
|
+
keep(): Promise<void>;
|
|
45
|
+
now(): number;
|
|
46
|
+
metadata(): {
|
|
47
|
+
issuer: string;
|
|
48
|
+
authorization_endpoint: string;
|
|
49
|
+
token_endpoint: string;
|
|
50
|
+
registration_endpoint: string;
|
|
51
|
+
response_types_supported: string[];
|
|
52
|
+
grant_types_supported: string[];
|
|
53
|
+
code_challenge_methods_supported: string[];
|
|
54
|
+
token_endpoint_auth_methods_supported: string[];
|
|
55
|
+
scopes_supported: string[];
|
|
56
|
+
};
|
|
57
|
+
protectedResource(): {
|
|
58
|
+
resource: string;
|
|
59
|
+
authorization_servers: string[];
|
|
60
|
+
bearer_methods_supported: string[];
|
|
61
|
+
};
|
|
62
|
+
register(body: unknown): Promise<Client | {
|
|
63
|
+
error: string;
|
|
64
|
+
}>;
|
|
65
|
+
client(id: string): Client | undefined;
|
|
66
|
+
authorize(q: URLSearchParams): Promise<{
|
|
67
|
+
redirect: string;
|
|
68
|
+
} | {
|
|
69
|
+
error: string;
|
|
70
|
+
}>;
|
|
71
|
+
pending(id: string): (Pending & {
|
|
72
|
+
client: Client;
|
|
73
|
+
}) | null;
|
|
74
|
+
complete(id: string, identity: string, ward: string): Promise<{
|
|
75
|
+
redirect: string;
|
|
76
|
+
} | {
|
|
77
|
+
error: string;
|
|
78
|
+
}>;
|
|
79
|
+
deny(id: string): Promise<{
|
|
80
|
+
redirect: string;
|
|
81
|
+
} | {
|
|
82
|
+
error: string;
|
|
83
|
+
}>;
|
|
84
|
+
token(body: URLSearchParams): Promise<Record<string, unknown>>;
|
|
85
|
+
issue(identity: string, ward: string, client_id: string): Promise<import("../dock/grants.ts").Tokens>;
|
|
86
|
+
sweep(): void;
|
|
87
|
+
handle(req: Request, rest: string): Promise<Response | undefined>;
|
|
88
|
+
challenge(): Response;
|
|
89
|
+
}
|
|
90
|
+
export declare function readBody(req: Request, limit?: number): Promise<string>;
|
|
91
|
+
export declare function readForm(req: Request): Promise<URLSearchParams>;
|