@quo-systems/dock 0.2.2 → 0.2.4
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/beings/avatar.ts +8 -1
- package/beings/carry.ts +1 -1
- package/beings/quo-dock.md +53 -39
- package/beings/setup.ts +2 -1
- package/beings/user.ts +48 -1
- package/cli/daemon.ts +51 -25
- package/cli/estate/quo.service +0 -1
- package/dist/beings/avatar.js +9 -2
- package/dist/beings/carry.js +2 -2
- package/dist/beings/setup.js +2 -1
- package/dist/beings/user.d.ts +89 -0
- package/dist/beings/user.js +47 -1
- package/dist/cli/daemon.d.ts +10 -2
- package/dist/cli/daemon.js +51 -23
- package/dist/cli/estate/quo.service +0 -1
- package/dist/harbor/capacitor.d.ts +16 -0
- package/dist/harbor/capacitor.js +135 -0
- package/dist/harbor/disk.js +8 -3
- package/dist/harbor/edge/edge.js +2 -1
- package/dist/harbor/edge/exercise.js +2 -1
- package/dist/harbor/edge/storage.d.ts +0 -3
- package/dist/harbor/edge/storage.js +6 -25
- package/dist/harbor/files.d.ts +2 -1
- package/dist/harbor/files.js +87 -22
- package/dist/harbor/seal.d.ts +3 -0
- package/dist/harbor/seal.js +25 -0
- package/dist/human/door.d.ts +2 -0
- package/dist/human/door.js +19 -7
- package/dist/human/html.d.ts +2 -0
- package/dist/human/html.js +43 -17
- package/dist/human/local.d.ts +10 -0
- package/dist/human/local.js +10 -0
- package/dist/human/screen.js +26 -8
- package/dist/human/tab.d.ts +1 -0
- package/dist/human/tab.js +59 -3
- package/dist/human/tree.d.ts +40 -0
- package/dist/human/tree.js +89 -0
- package/dist/human/web.d.ts +1 -0
- package/dist/human/web.js +36 -8
- package/dist/mcp/oauth.js +2 -2
- package/dist/mcp/route.js +1 -1
- package/dist/mcp/runner.js +5 -2
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/web/exchange.d.ts +5 -9
- package/dist/mcp/web/exchange.js +47 -98
- package/harbor/capacitor.ts +142 -0
- package/harbor/disk.ts +8 -3
- package/harbor/edge/edge.ts +2 -1
- package/harbor/edge/exercise.ts +2 -1
- package/harbor/edge/storage.ts +6 -25
- package/harbor/files.ts +79 -19
- package/harbor/quo-harbor.md +58 -15
- package/harbor/seal.ts +26 -0
- package/human/door.ts +20 -7
- package/human/html.ts +41 -17
- package/human/local.ts +26 -0
- package/human/quo-human.md +114 -19
- package/human/screen.ts +21 -7
- package/human/tab.ts +50 -5
- package/human/tree.ts +129 -0
- package/human/web.ts +36 -9
- package/mcp/oauth.ts +2 -2
- package/mcp/quo-mcp.md +27 -20
- package/mcp/route.ts +1 -1
- package/mcp/runner.ts +9 -6
- package/mcp/server.ts +1 -1
- package/mcp/web/exchange.ts +62 -96
- package/package.json +13 -2
package/mcp/quo-mcp.md
CHANGED
|
@@ -39,10 +39,11 @@ connected to the same user being see two different tool lists, and neither
|
|
|
39
39
|
can call what it cannot see: the gate is one decision for describe and for
|
|
40
40
|
dispatch.
|
|
41
41
|
|
|
42
|
-
A being who answers `look`, the trunk's
|
|
42
|
+
A being who answers `look`, the trunk's presentation ask, is listed with
|
|
43
43
|
what it says: `asks.NAME.title` is the tool's title, and `readOnly`,
|
|
44
44
|
`destructive` and `idempotent` are the annotations of the same names with
|
|
45
|
-
`Hint` after them.
|
|
45
|
+
`Hint` after them. Neither `look` nor `page`, the screen's presentation
|
|
46
|
+
ask, is a tool: a model reads no page. A carried standing's
|
|
46
47
|
asks, `acme-book` on the user being for a model the human let reach, carry
|
|
47
48
|
the far being's hints the same way, read from the notes the carrier writes.
|
|
48
49
|
The side asks `look` once per digest of the describe. Nothing else changes:
|
|
@@ -67,26 +68,32 @@ the two words give it what it needs to decide.
|
|
|
67
68
|
|
|
68
69
|
### A remote MCP client connects
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
The client speaks OAuth and lands the human on the allow page of the web
|
|
72
|
+
route, where nothing is typed but an invitation: the one the root minted
|
|
73
|
+
for this client on the user being with `quo invite`, and the identity it
|
|
74
|
+
will be. The invitation names its ward, so the world the client is let
|
|
75
|
+
into is the invitation's, and the grant remembers it: a bearer is an
|
|
76
|
+
identity in a world and a session is that identity's there. A session
|
|
77
|
+
opening under a grant finds the avatar as she stands and mints nothing; an
|
|
78
|
+
identity the user being removed hears `removed` there, which is 401.
|
|
79
|
+
`/mcp` stays one endpoint per harbor: `mcp/route.ts` is the route the
|
|
80
|
+
daemon mounts, the exchange in `oauth.ts` in front and the endpoint in
|
|
81
|
+
`http.ts` behind it, with the route's store, `<dir>/oauth.json`, the
|
|
82
|
+
route's own and never a ward's.
|
|
76
83
|
|
|
77
84
|
```
|
|
78
|
-
client mcp. route
|
|
79
|
-
|--
|
|
80
|
-
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|<-- initialize
|
|
85
|
+
root user being client mcp. route avatar
|
|
86
|
+
|-- invite ------->| | | |
|
|
87
|
+
|<-- invitation ---| | | |
|
|
88
|
+
| ... the link reaches the human by any channel ... | |
|
|
89
|
+
| | |-- OAuth -------->| |
|
|
90
|
+
| | | the allow page: identity, invitation
|
|
91
|
+
| | | |-- boot or find avatar for the identity -->|
|
|
92
|
+
| | | |-- knock(invitation) -------------------->|
|
|
93
|
+
| |<-- knock -------------------------------------------------------------------|
|
|
94
|
+
| |--- answer ----------------------------------------------------------------->|
|
|
95
|
+
| | | | take |
|
|
96
|
+
| | |<-- initialize ---| |
|
|
90
97
|
|-- tools/list -->|-- ask() ------------------------------------------------>|--> user being
|
|
91
98
|
|<-- tool list ---|<-- describe for this asker --------------------------------|
|
|
92
99
|
|-- tools/call -->|-- ask(name, args, { time }) ---------------------------->|--> user being
|
package/mcp/route.ts
CHANGED
|
@@ -24,7 +24,7 @@ export async function mcpRoute(dir: string, routes: Routes, mcp: McpHttp | null)
|
|
|
24
24
|
await writeFile(file + '.tmp', JSON.stringify(s), { mode: 0o600 });
|
|
25
25
|
await rename(file + '.tmp', file);
|
|
26
26
|
}));
|
|
27
|
-
const oauth = new OAuth({ issuer: routes.mcp, resource: `${routes.mcp}/mcp`, finish: (id) => `${routes.web}/
|
|
27
|
+
const oauth = new OAuth({ issuer: routes.mcp, resource: `${routes.mcp}/mcp`, finish: (id) => `${routes.web}/allow?request=${id}`, store, persist });
|
|
28
28
|
const handler: Handler = async (req, res, rest) => {
|
|
29
29
|
if (await oauth.handle(req, res, rest)) return;
|
|
30
30
|
if (rest === '/mcp') {
|
package/mcp/runner.ts
CHANGED
|
@@ -32,13 +32,16 @@ type Completion = { choices: { message: { role: 'assistant'; content: string | n
|
|
|
32
32
|
// Her describe, spoken as a tools array. Name, description and input are
|
|
33
33
|
// verbatim, with the one narrowing an endpoint has asked for: an ask that
|
|
34
34
|
// declares no properties is sent with an empty `properties`, because
|
|
35
|
-
// LM Studio refuses a parameters schema without one.
|
|
35
|
+
// LM Studio refuses a parameters schema without one. `look` and `page` are
|
|
36
|
+
// presentation, for a screen, and are not functions.
|
|
36
37
|
export function tools(bp: Blueprint): Tool[] {
|
|
37
|
-
return bp.asks
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
return bp.asks
|
|
39
|
+
.filter((a) => a.name !== 'look' && a.name !== 'page')
|
|
40
|
+
.map((a) => {
|
|
41
|
+
const t: Tool = { type: 'function', function: { name: a.name, parameters: { properties: {}, ...a.input, type: 'object' } } };
|
|
42
|
+
if (a.description !== undefined) t.function.description = a.description;
|
|
43
|
+
return t;
|
|
44
|
+
});
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
// One tool call, one ask, one tool message: the three words cross as the
|
package/mcp/server.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const DESCRIBE: Tool = { name: 'describe', description: 'the empty ask: h
|
|
|
31
31
|
// reads. The `look` ask itself is presentation, and is not a tool.
|
|
32
32
|
export function tools(bp: Blueprint, look: Look = {}): Tool[] {
|
|
33
33
|
const asks = bp.asks
|
|
34
|
-
.filter((a) => a.name !== 'look')
|
|
34
|
+
.filter((a) => a.name !== 'look' && a.name !== 'page') // presentation, for a screen, and not a tool
|
|
35
35
|
.map((a) => {
|
|
36
36
|
const t: Tool = { name: a.name, inputSchema: { ...a.input, type: 'object' } };
|
|
37
37
|
if (a.description !== undefined) t.description = a.description;
|
package/mcp/web/exchange.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The exchange
|
|
3
|
-
// explicitly not the human side:
|
|
4
|
-
// credential exchange the way
|
|
5
|
-
//
|
|
6
|
-
// the
|
|
2
|
+
// The exchange page on the web route. One static page, hand-written, and
|
|
3
|
+
// explicitly not the human side: it renders no blueprint. It ends the
|
|
4
|
+
// credential exchange the way every way in ends: with a knock. Nothing is
|
|
5
|
+
// typed to get in but an invitation, the one the root minted for this
|
|
6
|
+
// client on the user being with `quo invite` and sent as a link or a line;
|
|
7
|
+
// the avatar the identity boots knocks with it, and the OAuth request
|
|
8
|
+
// completes with the identity the human chose. No password, no cookie, no
|
|
9
|
+
// session: the invitation is the credential, it is used once, and from the
|
|
10
|
+
// knock on there is no token anywhere but the route's own grant.
|
|
7
11
|
//
|
|
8
|
-
// GET /
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
+
// GET /allow?request=ID who is asking, the identity it will be, and
|
|
13
|
+
// the field for the invitation
|
|
14
|
+
// POST /allow allow as that identity with that invitation,
|
|
15
|
+
// or deny
|
|
12
16
|
//
|
|
13
|
-
// The
|
|
14
|
-
//
|
|
15
|
-
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
17
|
+
// The invitation names its ward, so the world the client lands in is the
|
|
18
|
+
// one the invitation was minted on, and there is nothing to choose.
|
|
16
19
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
20
|
+
import type { Invitation } from '@quo-systems/quo';
|
|
17
21
|
import type { OAuth } from '../oauth.ts';
|
|
18
22
|
import { readForm } from '../oauth.ts';
|
|
23
|
+
import { parse } from '../../beings/link.ts';
|
|
19
24
|
|
|
20
|
-
export
|
|
21
|
-
const COOKIE = 'quo_exchange';
|
|
22
|
-
|
|
23
|
-
export type Admit = (identity: string, wake: boolean, reach: boolean, ward: string) => Promise<{ error?: string }>;
|
|
25
|
+
export type Join = (identity: string, invitation: Invitation, ward: string) => Promise<{ error?: string }>;
|
|
24
26
|
export type Options = {
|
|
25
27
|
oauth: OAuth;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
worlds: () => { ward: string; user: string }[]; // the worlds of this harbor, the first the default: a ward with a public being, and its user being's name
|
|
29
|
-
now?: () => number;
|
|
28
|
+
join: Join; // the daemon: boot or find the avatar for this identity in that world, and knock
|
|
29
|
+
worlds: () => { ward: string; pk: string; user: string }[]; // the worlds of this harbor, by name and pk
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const esc = (s: string) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c);
|
|
@@ -39,122 +39,88 @@ export const suggest = (name: string) =>
|
|
|
39
39
|
.replace(/^-|-$/g, '')
|
|
40
40
|
.slice(0, 40) || 'client';
|
|
41
41
|
|
|
42
|
+
// The invitation as typed: the link's compact form, `ward.heir.secret`, or
|
|
43
|
+
// the whole link it came in, or the JSON value `quo invite` printed.
|
|
44
|
+
export function invitationOf(text: string): Invitation | null {
|
|
45
|
+
const t = text.trim();
|
|
46
|
+
const hash = t.indexOf('#');
|
|
47
|
+
const linked = parse(hash === -1 ? `#quo=${t}` : t.slice(hash));
|
|
48
|
+
if (linked?.heir) return linked;
|
|
49
|
+
try {
|
|
50
|
+
const v = JSON.parse(t) as Record<string, unknown>;
|
|
51
|
+
if (v && typeof v === 'object' && typeof v.ward === 'string' && typeof v.heir === 'string' && typeof v.secret === 'string') return { ward: v.ward, heir: v.heir, secret: v.secret };
|
|
52
|
+
} catch {
|
|
53
|
+
/* not JSON either */
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
export class Exchange {
|
|
43
59
|
readonly o: Options;
|
|
44
|
-
readonly secret = randomBytes(32); // sessions die with the daemon, and that is fine: they live ten minutes
|
|
45
60
|
constructor(o: Options) {
|
|
46
61
|
this.o = o;
|
|
47
62
|
}
|
|
48
|
-
now() {
|
|
49
|
-
return this.o.now?.() ?? Date.now();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---- the session cookie: an expiry, signed
|
|
53
|
-
|
|
54
|
-
mint(): string {
|
|
55
|
-
const exp = String(this.now() + SESSION_TTL);
|
|
56
|
-
return `${exp}.${createHmac('sha256', this.secret).update(exp).digest('base64url')}`;
|
|
57
|
-
}
|
|
58
|
-
valid(req: IncomingMessage): boolean {
|
|
59
|
-
const m = /(?:^|;\s*)quo_exchange=([^;]+)/.exec(req.headers.cookie ?? '');
|
|
60
|
-
const [exp, sig] = (m?.[1] ?? '').split('.');
|
|
61
|
-
if (!exp || !sig) return false;
|
|
62
|
-
const want = createHmac('sha256', this.secret).update(exp).digest('base64url');
|
|
63
|
-
return sig.length === want.length && timingSafeEqual(Buffer.from(sig), Buffer.from(want)) && Number(exp) > this.now();
|
|
64
|
-
}
|
|
65
63
|
|
|
66
64
|
async handle(req: IncomingMessage, res: ServerResponse, rest: string): Promise<boolean> {
|
|
67
65
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
68
|
-
const page = (status: number, body: string
|
|
69
|
-
res.writeHead(status, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store'
|
|
66
|
+
const page = (status: number, body: string): true => {
|
|
67
|
+
res.writeHead(status, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
70
68
|
res.end(shell(body));
|
|
69
|
+
return true;
|
|
71
70
|
};
|
|
72
|
-
const go = (to: string
|
|
73
|
-
res.writeHead(302, { location: to, 'cache-control': 'no-store'
|
|
71
|
+
const go = (to: string): true => {
|
|
72
|
+
res.writeHead(302, { location: to, 'cache-control': 'no-store' });
|
|
74
73
|
res.end();
|
|
74
|
+
return true;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
if (rest === '/login' && req.method === 'GET') {
|
|
78
|
-
const request = url.searchParams.get('request') ?? '';
|
|
79
|
-
const p = this.o.oauth.pending(request);
|
|
80
|
-
if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone. Start again from the app that sent you here.</p>`), true;
|
|
81
|
-
if (this.o.password() === undefined) return page(503, `<h1>Closed</h1><p>This world takes no logins: no owner password is set.</p>`), true;
|
|
82
|
-
if (this.valid(req)) return go(`/allow?request=${encodeURIComponent(request)}`), true;
|
|
83
|
-
return page(200, loginForm(request, p.client.client_name)), true;
|
|
84
|
-
}
|
|
85
|
-
if (rest === '/login' && req.method === 'POST') {
|
|
86
|
-
const f = await readForm(req);
|
|
87
|
-
const request = f.get('request') ?? '';
|
|
88
|
-
const p = this.o.oauth.pending(request);
|
|
89
|
-
if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone.</p>`), true;
|
|
90
|
-
const want = this.o.password();
|
|
91
|
-
const got = f.get('password') ?? '';
|
|
92
|
-
if (want === undefined) return page(503, `<h1>Closed</h1>`), true;
|
|
93
|
-
if (got.length !== want.length || !timingSafeEqual(Buffer.from(got), Buffer.from(want))) {
|
|
94
|
-
await new Promise((ok) => setTimeout(ok, 300));
|
|
95
|
-
return page(401, loginForm(request, p.client.client_name, 'That is not the password.')), true;
|
|
96
|
-
}
|
|
97
|
-
return go(`/allow?request=${encodeURIComponent(request)}`, { 'set-cookie': `${COOKIE}=${this.mint()}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${SESSION_TTL / 1000}` }), true;
|
|
98
|
-
}
|
|
99
77
|
if (rest === '/allow' && req.method === 'GET') {
|
|
100
78
|
const request = url.searchParams.get('request') ?? '';
|
|
101
79
|
const p = this.o.oauth.pending(request);
|
|
102
|
-
if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone.</p>`)
|
|
103
|
-
|
|
104
|
-
return page(200, allowForm(request, p.client.client_name, p.redirect_uri, suggest(p.client.client_name), this.o.worlds())), true;
|
|
80
|
+
if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone. Start again from the app that sent you here.</p>`);
|
|
81
|
+
return page(200, allowForm(request, p.client.client_name, p.redirect_uri, suggest(p.client.client_name), this.o.worlds()));
|
|
105
82
|
}
|
|
106
83
|
if (rest === '/allow' && req.method === 'POST') {
|
|
107
84
|
const f = await readForm(req);
|
|
108
85
|
const request = f.get('request') ?? '';
|
|
109
86
|
const p = this.o.oauth.pending(request);
|
|
110
|
-
if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone.</p>`)
|
|
111
|
-
if (!this.valid(req)) return go(`/login?request=${encodeURIComponent(request)}`), true;
|
|
87
|
+
if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone.</p>`);
|
|
112
88
|
if (f.get('decision') !== 'allow') {
|
|
113
89
|
const out = await this.o.oauth.deny(request);
|
|
114
|
-
return 'redirect' in out ? go(out.redirect) : page(400, `<h1>Gone</h1>`)
|
|
90
|
+
return 'redirect' in out ? go(out.redirect) : page(400, `<h1>Gone</h1>`);
|
|
115
91
|
}
|
|
116
92
|
const worlds = this.o.worlds();
|
|
117
|
-
const
|
|
118
|
-
|
|
93
|
+
const again = (err: string) => page(400, allowForm(request, p.client.client_name, p.redirect_uri, word(f.get('identity')) ?? suggest(p.client.client_name), worlds, err));
|
|
94
|
+
const inv = invitationOf(f.get('invitation') ?? '');
|
|
95
|
+
if (!inv) return again('That is not an invitation.');
|
|
96
|
+
const world = worlds.find((w) => w.pk === inv.ward);
|
|
97
|
+
if (!world) return again('That invitation is for a world that does not live here.');
|
|
119
98
|
const identity = word(f.get('identity'));
|
|
120
|
-
if (identity === null || identity === world.user || identity === 'desk') return
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
99
|
+
if (identity === null || identity === world.user || identity === 'desk') return again('An identity is one word, and not the user or the desk.');
|
|
100
|
+
const joined = await this.o.join(identity, inv, world.ward);
|
|
101
|
+
if (joined.error) return again(`Not in: ${joined.error}.`);
|
|
123
102
|
const out = await this.o.oauth.complete(request, identity, world.ward);
|
|
124
|
-
return 'redirect' in out ? go(out.redirect
|
|
103
|
+
return 'redirect' in out ? go(out.redirect) : page(400, `<h1>Gone</h1>`);
|
|
125
104
|
}
|
|
126
105
|
return false;
|
|
127
106
|
}
|
|
128
107
|
}
|
|
129
108
|
|
|
130
109
|
const shell = (body: string) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>quo</title>
|
|
131
|
-
<style>body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.4rem}label{display:block;margin:1rem 0 .25rem}input{font:inherit;padding:.5rem;width:100%;box-sizing:border-box}button{font:inherit;padding:.5rem 1rem;margin:1rem .5rem 0 0}.err{color:#b00}.who{background:#f4f4f4;padding:.75rem 1rem;border-radius:.5rem}code{word-break:break-all}</style>
|
|
110
|
+
<style>body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.4rem}label{display:block;margin:1rem 0 .25rem}input,textarea{font:inherit;padding:.5rem;width:100%;box-sizing:border-box}textarea{font-family:ui-monospace,monospace;min-height:5rem}button{font:inherit;padding:.5rem 1rem;margin:1rem .5rem 0 0}.err{color:#b00}.who{background:#f4f4f4;padding:.75rem 1rem;border-radius:.5rem}code{word-break:break-all}</style>
|
|
132
111
|
</head><body>${body}</body></html>`;
|
|
133
112
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
${err ? `<p class="err">${esc(err)}</p>` : ''}
|
|
137
|
-
<form method="post" action="/login"><input type="hidden" name="request" value="${esc(request)}">
|
|
138
|
-
<label for="p">Owner password</label><input id="p" name="password" type="password" autocomplete="current-password" autofocus required>
|
|
139
|
-
<button type="submit">Log in</button></form>`;
|
|
140
|
-
|
|
141
|
-
// The world is a choice when the harbor has more than one; the user named
|
|
142
|
-
// is the first world's, and the page says which world each identity lands in.
|
|
113
|
+
// The one page. The world is the invitation's, so the page only says which
|
|
114
|
+
// worlds live here and whose they are.
|
|
143
115
|
const allowForm = (request: string, client: string, redirect: string, identity: string, worlds: { ward: string; user: string }[], err = '') => {
|
|
144
|
-
const
|
|
145
|
-
const pick =
|
|
146
|
-
worlds.length > 1
|
|
147
|
-
? `<label for="wd">World</label><select id="wd" name="ward">${worlds.map((w) => `<option value="${esc(w.ward)}">${esc(w.ward)}, ${esc(w.user)}'s</option>`).join('')}</select>`
|
|
148
|
-
: `<input type="hidden" name="ward" value="${esc(worlds[0]?.ward ?? 'main')}">`;
|
|
116
|
+
const whose = worlds.map((w) => `<strong>${esc(w.user)}</strong> in ${esc(w.ward)}`).join(', ');
|
|
149
117
|
return `<h1>Allow ${esc(client)}?</h1>
|
|
150
|
-
<div class="who"><p><strong>${esc(client)}</strong> asks to be an occupant of
|
|
151
|
-
<p>It will see exactly what
|
|
118
|
+
<div class="who"><p><strong>${esc(client)}</strong> asks to be an occupant of ${whose || 'a world here'}.</p>
|
|
119
|
+
<p>It will see exactly what the user being shows the identity below, and nothing else. You can remove it any time.</p>
|
|
152
120
|
<p>It returns to <code>${esc(redirect)}</code>.</p></div>
|
|
153
121
|
${err ? `<p class="err">${esc(err)}</p>` : ''}
|
|
154
122
|
<form method="post" action="/allow"><input type="hidden" name="request" value="${esc(request)}">
|
|
155
|
-
${pick}
|
|
156
123
|
<label for="i">Identity</label><input id="i" name="identity" value="${esc(identity)}" pattern="[\\w.-]{1,40}" required>
|
|
157
|
-
<label for="
|
|
158
|
-
<label for="w"><input id="w" name="wake" type="checkbox" style="width:auto"> May wake your other devices: hand an agent an event through ${esc(user)}</label>
|
|
124
|
+
<label for="v">Invitation</label><textarea id="v" name="invitation" autofocus required placeholder="the invitation the owner minted for this client, as the link or as printed"></textarea>
|
|
159
125
|
<button type="submit" name="decision" value="allow">Allow</button><button type="submit" name="decision" value="deny">Deny</button></form>`;
|
|
160
126
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quo-systems/dock",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "The dock: what every estate on Quo needs and nobody writes twice. A daemon and the quo command, the front desk, the user being and the avatar, harbors on disk, in a tab and on the edge, the model sides and the screen.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"types": "./dist/harbor/browser.d.ts",
|
|
32
32
|
"default": "./dist/harbor/browser.js"
|
|
33
33
|
},
|
|
34
|
+
"./harbor/capacitor": {
|
|
35
|
+
"types": "./dist/harbor/capacitor.d.ts",
|
|
36
|
+
"default": "./dist/harbor/capacitor.js"
|
|
37
|
+
},
|
|
34
38
|
"./cli/daemon": {
|
|
35
39
|
"types": "./dist/cli/daemon.d.ts",
|
|
36
40
|
"default": "./dist/cli/daemon.js"
|
|
@@ -55,6 +59,10 @@
|
|
|
55
59
|
"types": "./dist/human/html.d.ts",
|
|
56
60
|
"default": "./dist/human/html.js"
|
|
57
61
|
},
|
|
62
|
+
"./human/local": {
|
|
63
|
+
"types": "./dist/human/local.d.ts",
|
|
64
|
+
"default": "./dist/human/local.js"
|
|
65
|
+
},
|
|
58
66
|
"./harbor/edge/worker": {
|
|
59
67
|
"types": "./dist/harbor/edge/worker.d.ts",
|
|
60
68
|
"default": "./dist/harbor/edge/worker.js"
|
|
@@ -67,8 +75,11 @@
|
|
|
67
75
|
"prepublishOnly": "cd ../.. && npm run check && npm run check:terrain"
|
|
68
76
|
},
|
|
69
77
|
"dependencies": {
|
|
78
|
+
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
|
79
|
+
"@capacitor/core": "^8.5.1",
|
|
80
|
+
"@capacitor/filesystem": "^8.1.3",
|
|
70
81
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
71
|
-
"@quo-systems/quo": "^0.2.
|
|
82
|
+
"@quo-systems/quo": "^0.2.4",
|
|
72
83
|
"esbuild": "^0.28.2",
|
|
73
84
|
"ws": "^8.21.3"
|
|
74
85
|
},
|