@quo-systems/dock 0.1.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.
Files changed (94) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +27 -0
  4. package/beings/avatar.ts +84 -0
  5. package/beings/desk.ts +46 -0
  6. package/beings/index.ts +5 -0
  7. package/beings/quo-dock.md +698 -0
  8. package/beings/setup.ts +23 -0
  9. package/beings/side.ts +51 -0
  10. package/beings/user.ts +78 -0
  11. package/cli/client.ts +65 -0
  12. package/cli/daemon.ts +582 -0
  13. package/cli/quo.ts +126 -0
  14. package/dist/beings/avatar.d.ts +32 -0
  15. package/dist/beings/avatar.js +82 -0
  16. package/dist/beings/desk.d.ts +42 -0
  17. package/dist/beings/desk.js +43 -0
  18. package/dist/beings/index.d.ts +3 -0
  19. package/dist/beings/index.js +5 -0
  20. package/dist/beings/setup.d.ts +2 -0
  21. package/dist/beings/setup.js +16 -0
  22. package/dist/beings/side.d.ts +25 -0
  23. package/dist/beings/side.js +37 -0
  24. package/dist/beings/user.d.ts +108 -0
  25. package/dist/beings/user.js +74 -0
  26. package/dist/cli/client.d.ts +14 -0
  27. package/dist/cli/client.js +51 -0
  28. package/dist/cli/daemon.d.ts +57 -0
  29. package/dist/cli/daemon.js +581 -0
  30. package/dist/cli/quo.d.ts +2 -0
  31. package/dist/cli/quo.js +121 -0
  32. package/dist/harbor/browser.d.ts +13 -0
  33. package/dist/harbor/browser.js +50 -0
  34. package/dist/harbor/disk.d.ts +19 -0
  35. package/dist/harbor/disk.js +126 -0
  36. package/dist/harbor/edge/edge.d.ts +26 -0
  37. package/dist/harbor/edge/edge.js +123 -0
  38. package/dist/harbor/edge/exercise.d.ts +7 -0
  39. package/dist/harbor/edge/exercise.js +179 -0
  40. package/dist/harbor/edge/storage.d.ts +20 -0
  41. package/dist/harbor/edge/storage.js +81 -0
  42. package/dist/harbor/edge/worker.d.ts +14 -0
  43. package/dist/harbor/edge/worker.js +47 -0
  44. package/dist/harbor/files.d.ts +14 -0
  45. package/dist/harbor/files.js +83 -0
  46. package/dist/harbor/idb.d.ts +15 -0
  47. package/dist/harbor/idb.js +85 -0
  48. package/dist/human/dom.d.ts +2 -0
  49. package/dist/human/dom.js +15 -0
  50. package/dist/human/html.d.ts +32 -0
  51. package/dist/human/html.js +136 -0
  52. package/dist/human/screen.d.ts +11 -0
  53. package/dist/human/screen.js +66 -0
  54. package/dist/human/tab.d.ts +7 -0
  55. package/dist/human/tab.js +68 -0
  56. package/dist/mcp/agent.d.ts +10 -0
  57. package/dist/mcp/agent.js +78 -0
  58. package/dist/mcp/http.d.ts +27 -0
  59. package/dist/mcp/http.js +96 -0
  60. package/dist/mcp/oauth.d.ts +98 -0
  61. package/dist/mcp/oauth.js +241 -0
  62. package/dist/mcp/pilot.d.ts +6 -0
  63. package/dist/mcp/pilot.js +68 -0
  64. package/dist/mcp/runner.d.ts +45 -0
  65. package/dist/mcp/runner.js +94 -0
  66. package/dist/mcp/server.d.ts +9 -0
  67. package/dist/mcp/server.js +63 -0
  68. package/dist/mcp/web/exchange.d.ts +23 -0
  69. package/dist/mcp/web/exchange.js +136 -0
  70. package/harbor/browser.ts +63 -0
  71. package/harbor/disk.ts +120 -0
  72. package/harbor/edge/edge.ts +147 -0
  73. package/harbor/edge/exercise.ts +187 -0
  74. package/harbor/edge/platform.d.ts +28 -0
  75. package/harbor/edge/storage.ts +82 -0
  76. package/harbor/edge/worker.ts +47 -0
  77. package/harbor/edge/wrangler.toml +21 -0
  78. package/harbor/files.ts +89 -0
  79. package/harbor/idb.ts +90 -0
  80. package/harbor/quo-harbor.md +403 -0
  81. package/human/dom.ts +21 -0
  82. package/human/html.ts +164 -0
  83. package/human/quo-human.md +86 -0
  84. package/human/screen.ts +76 -0
  85. package/human/tab.ts +84 -0
  86. package/mcp/agent.ts +94 -0
  87. package/mcp/http.ts +96 -0
  88. package/mcp/oauth.ts +260 -0
  89. package/mcp/pilot.ts +73 -0
  90. package/mcp/quo-mcp.md +262 -0
  91. package/mcp/runner.ts +131 -0
  92. package/mcp/server.ts +67 -0
  93. package/mcp/web/exchange.ts +146 -0
  94. package/package.json +89 -0
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // `quo`: the one command of the estate. Thin on purpose: each subcommand
4
+ // picks a folder, reaches the daemon or the disk, and hands off. Logic that
5
+ // appears here was missing from a being or a side, and moves there.
6
+ //
7
+ // quo init [--dir D] [--ward W] mint a seed, boot a ward over a disk store, write both
8
+ // quo serve [--dir D] [--http PORT] [--dial URL] the daemon: harbor from disk, the two sockets, the HTTP door on loopback, a socket held to URL
9
+ // quo census [--dir D] [--ward W] [--via S] the empty ask: pk and every being
10
+ // quo boot [--dir D] [--ward W] [--via S] <json> { key, class, public? }
11
+ // quo invite [--dir D] [--ward W] [--via S] <json> { being, id }
12
+ // quo knock [--dir D] [--ward W] [--via S] <json> { being | { boot, key }, id, invitation, method?, args?, wanted? }
13
+ // quo remove [--dir D] [--ward W] [--via S] <json> { being, id }: a relation out of a being; on the ward pk, an owner
14
+ // quo side [--dir D] [--ward W] --as NAME an avatar over stdio for a local MCP client; NAME is the client identity
15
+ // quo run [--dir D] [--ward W] --as NAME --url URL --model NAME [--turns N] a human at a terminal talking to a model through an avatar: a line in, the model's final text out
16
+ // quo pilot [--dir D] [--ward W] [--via S] the owner pilot over stdio: census, boot, invite, knock, remove
17
+ // quo reach [--dir D] <pk> <url> a hint for the harbor's directory: that ward pk lives at that quo. route
18
+ //
19
+ // D defaults to $QUO_DIR, then ~/.quo. W defaults to main. init takes
20
+ // --user NAME for the user being, defaulting to the device's user. run
21
+ // takes the model's base URL as --url or $QUO_MODEL_URL, its name as
22
+ // --model or $QUO_MODEL, a bearer from $QUO_MODEL_KEY, read here and never
23
+ // kept, and --turns as the ceiling of model calls in one turn. S is a
24
+ // standing the user being holds at another ward, one the root there invited
25
+ // as an owner: with --via, the owner asks go to that ward's door, sealed,
26
+ // and the answers are its. Without it they are the root's, on the pointer.
27
+ import { homedir, userInfo } from 'node:os';
28
+ import { join } from 'node:path';
29
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
30
+ import { DiskHarbor } from '../harbor/disk.js';
31
+ import { serve } from './daemon.js';
32
+ import { ask, side, reach } from './client.js';
33
+ import { pilotSide } from '../mcp/pilot.js';
34
+ const OWNER_ASKS = new Set(['boot', 'invite', 'knock', 'remove']);
35
+ function parse(argv) {
36
+ const flags = {};
37
+ const rest = [];
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const a = argv[i] ?? '';
40
+ if (a.startsWith('--'))
41
+ flags[a.slice(2)] = argv[++i] ?? '';
42
+ else
43
+ rest.push(a);
44
+ }
45
+ const [cmd, json, more] = rest;
46
+ return { cmd, json, more, flags, dir: flags.dir ?? process.env.QUO_DIR ?? join(homedir(), '.quo'), ward: flags.ward ?? 'main', via: flags.via };
47
+ }
48
+ const print = (v) => console.log(JSON.stringify(v, null, 2));
49
+ async function main(argv) {
50
+ const { cmd, json, more, flags, dir, ward, via } = parse(argv);
51
+ if (cmd === 'init') {
52
+ print(await DiskHarbor.init(dir, ward, flags.user ?? userInfo().username));
53
+ return 0;
54
+ }
55
+ if (cmd === 'side' || cmd === 'run') {
56
+ if (!flags.as) {
57
+ console.error(`quo ${cmd} needs --as NAME`);
58
+ return 2;
59
+ }
60
+ let run;
61
+ if (cmd === 'run') {
62
+ const url = flags.url ?? process.env.QUO_MODEL_URL;
63
+ const model = flags.model ?? process.env.QUO_MODEL;
64
+ if (!url || !model) {
65
+ console.error('quo run needs --url URL and --model NAME, or QUO_MODEL_URL and QUO_MODEL');
66
+ return 2;
67
+ }
68
+ const key = process.env.QUO_MODEL_KEY;
69
+ const turns = flags.turns !== undefined ? Number(flags.turns) : undefined;
70
+ if (turns !== undefined && !(Number.isInteger(turns) && turns > 0)) {
71
+ console.error('--turns is a positive integer');
72
+ return 2;
73
+ }
74
+ run = { url, model, ...(key ? { key } : {}), ...(turns !== undefined ? { turns } : {}) };
75
+ }
76
+ await side(dir, flags.as, ward, run); // stdio to the daemon, until one end closes
77
+ return -1;
78
+ }
79
+ if (cmd === 'pilot') {
80
+ await ask(dir, undefined, {}, ward); // no daemon is a clear word now, not at the first tool call
81
+ const serving = await pilotSide(dir, ward, new StdioServerTransport(), (line) => console.error(`pilot ${line}`), via);
82
+ process.stdin.once('end', () => void serving.close().then(() => process.exit(0)));
83
+ return -1;
84
+ }
85
+ if (cmd === 'serve') {
86
+ const port = flags.http ?? process.env.QUO_HTTP;
87
+ const s = await serve(dir, { ...(port !== undefined ? { http: { port: Number(port) } } : {}), ...(flags.dial ? { dial: [flags.dial] } : {}) });
88
+ print({ dir: s.harbor.dir, sock: s.sock, side: s.side, http: s.http ? `http://${s.http.host}:${s.http.port}` : null, dial: s.quo.dialers.map((d) => d.url), wards: Object.fromEntries([...s.harbor.wards].map(([n, h]) => [n, h.pk])) });
89
+ const stop = () => void s.close().then(() => process.exit(0));
90
+ process.once('SIGINT', stop);
91
+ process.once('SIGTERM', stop);
92
+ return -1; // stays up
93
+ }
94
+ if (cmd === 'reach') {
95
+ if (!json || !more) {
96
+ console.error('quo reach needs <pk> <url>');
97
+ return 2;
98
+ }
99
+ const out = await reach(dir, json, more);
100
+ print('error' in out ? { error: out.error } : out.result);
101
+ return 'error' in out ? 1 : 0;
102
+ }
103
+ if (cmd === 'census' || (cmd !== undefined && OWNER_ASKS.has(cmd))) {
104
+ const args = json ? JSON.parse(json) : {};
105
+ const out = await ask(dir, cmd === 'census' ? undefined : cmd, args, ward, via);
106
+ print('error' in out ? { error: out.error } : out.result);
107
+ // an error object the ward answered is an ordinary answer, and still a
108
+ // non-zero exit, so a script can tell a boot that happened from one that did not.
109
+ const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
110
+ return failed ? 1 : 0;
111
+ }
112
+ console.error('usage: quo init|serve|census|boot|invite|knock|remove|side|run|pilot|reach [--dir D] [--ward W] [--via S] [--as NAME] [--url URL --model NAME] [--dial URL] [json]');
113
+ return 2;
114
+ }
115
+ main(process.argv.slice(2)).then((code) => {
116
+ if (code >= 0)
117
+ process.exit(code);
118
+ }, (e) => {
119
+ console.error(e instanceof Error ? e.message : String(e));
120
+ process.exit(1);
121
+ });
@@ -0,0 +1,13 @@
1
+ import type { BeingClass } from '@quo-systems/quo';
2
+ import { Harbor, type Dialer } from '@quo-systems/quo/harbor';
3
+ import { Idb } from './idb.ts';
4
+ export declare const BUILT_IN: Record<string, BeingClass>;
5
+ export declare class BrowserHarbor extends Harbor {
6
+ #private;
7
+ readonly db: Idb;
8
+ readonly dialers: Dialer[];
9
+ constructor(name?: string, classes?: Record<string, BeingClass>);
10
+ boot(): Promise<void>;
11
+ dial(url: string): Dialer;
12
+ close(): Promise<void>;
13
+ }
@@ -0,0 +1,50 @@
1
+ import { User, Desk, Avatar } from '../beings/index.js';
2
+ import { Harbor, dial } from '@quo-systems/quo/harbor';
3
+ import { Idb } from './idb.js';
4
+ export const BUILT_IN = { User, Desk, Avatar };
5
+ export class BrowserHarbor extends Harbor {
6
+ db;
7
+ dialers = [];
8
+ #release;
9
+ constructor(name = 'quo', classes = {}) {
10
+ const db = new Idb(name);
11
+ super(db, async () => ({ ...BUILT_IN, ...classes }));
12
+ this.db = db;
13
+ }
14
+ // Take the lock and boot every ward kept here. A lock already held is
15
+ // another tab's harbor over this seed, and this one refuses to boot.
16
+ async boot() {
17
+ await this.#lease();
18
+ await super.boot();
19
+ }
20
+ // Hold one socket to a world's quo. route.
21
+ dial(url) {
22
+ const d = dial(this, url);
23
+ this.dialers.push(d);
24
+ return d;
25
+ }
26
+ async close() {
27
+ for (const d of this.dialers.splice(0))
28
+ d.close();
29
+ this.db.close();
30
+ this.#release?.();
31
+ this.#release = undefined;
32
+ }
33
+ async #lease() {
34
+ const locks = globalThis.navigator?.locks;
35
+ if (!locks)
36
+ return; // no web locks on this terrain: nothing vouches, as in the memory harbor
37
+ const granted = await new Promise((ok) => {
38
+ void locks.request(`quo:${this.db.name}`, { ifAvailable: true }, (lock) => {
39
+ if (!lock) {
40
+ ok(false);
41
+ return;
42
+ }
43
+ ok(true);
44
+ return new Promise((release) => (this.#release = release)); // held until close
45
+ });
46
+ });
47
+ if (!granted)
48
+ throw new Error(`harbor ${this.db.name} is held by another tab`);
49
+ }
50
+ }
@@ -0,0 +1,19 @@
1
+ import type { BeingClass } from '@quo-systems/quo';
2
+ import { Harbor, type Hosted } from '@quo-systems/quo/harbor';
3
+ export type { Hosted, Bound } from '@quo-systems/quo/harbor';
4
+ export type { WardRecord } from '@quo-systems/quo/harbor';
5
+ export declare const BUILT_IN: Record<string, BeingClass>;
6
+ export declare class DiskHarbor extends Harbor {
7
+ #private;
8
+ readonly dir: string;
9
+ constructor(dir: string);
10
+ static init(dir: string, name?: string, user?: string): Promise<{
11
+ dir: string;
12
+ name: string;
13
+ pk: string;
14
+ user: string;
15
+ }>;
16
+ boot(): Promise<void>;
17
+ close(): Promise<void>;
18
+ adoptFolder(name: string): Promise<Hosted>;
19
+ }
@@ -0,0 +1,126 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // SPDX-License-Identifier: Apache-2.0
10
+ // The node daemon's harbor: the core over a folder on disk. The store is
11
+ // `files.ts`, one folder per ward; the class bodies come from a module the
12
+ // ward record names, loaded from the folder; the lease is a pid file. The
13
+ // estate's own beings, the user being, the front desk and the avatar, are
14
+ // held by every harbor here: they are what every side needs, and a folder's
15
+ // class source adds to them.
16
+ //
17
+ // <dir>/
18
+ // lease pid of the one process that holds this harbor
19
+ // classes/index.ts the default class source, a module exporting classes
20
+ // wards/<name>/ seed, partition.json, ward.json: see files.ts
21
+ // reach.json the directory's hints
22
+ import { mkdir, readFile, writeFile, unlink, stat } from 'node:fs/promises';
23
+ import { existsSync } from 'node:fs';
24
+ import { join, resolve, isAbsolute } from 'node:path';
25
+ import { pathToFileURL } from 'node:url';
26
+ import process from 'node:process';
27
+ import { User, Desk, Avatar } from '../beings/index.js';
28
+ import { setup } from '../beings/setup.js';
29
+ import { Harbor, DEFAULT_CODE } from '@quo-systems/quo/harbor';
30
+ import { Files } from './files.js';
31
+ export const BUILT_IN = { User, Desk, Avatar };
32
+ // Folders held by a harbor in this process. The lease file names a pid, and
33
+ // a second harbor in the same pid would pass that check while still being a
34
+ // second ward with one pk.
35
+ const HELD = new Set();
36
+ // The class source is a module. Every export that is a class is a class the
37
+ // harbor holds, under its export name. The harbor never sees a body: it
38
+ // constructs when a ward names a class, and never chooses one.
39
+ async function loadClasses(path) {
40
+ if (!existsSync(path))
41
+ return {};
42
+ // Keyed by the file's own time, so a source that changed on disk is read
43
+ // again and a harbor boots on what it pins, not on what it once loaded.
44
+ const { mtimeMs } = await stat(path);
45
+ const mod = (await import(__rewriteRelativeImportExtension(`${pathToFileURL(path).href}?v=${mtimeMs}`)));
46
+ const out = {};
47
+ for (const [name, v] of Object.entries(mod))
48
+ if (typeof v === 'function' && 'prototype' in v)
49
+ out[name] = v;
50
+ return out;
51
+ }
52
+ export class DiskHarbor extends Harbor {
53
+ dir;
54
+ #held = false;
55
+ constructor(dir) {
56
+ const abs = resolve(dir);
57
+ super(new Files(abs), async (rec) => ({ ...BUILT_IN, ...(await loadClasses(isAbsolute(rec.code) ? rec.code : join(abs, rec.code))) }));
58
+ this.dir = abs;
59
+ }
60
+ // Create a harbor folder with one ward, and the root's setup in it: the
61
+ // front desk, public, and one user being, with the desk holding a standing
62
+ // to her placed by the root's knock. Boots the ward once, with no lease, to
63
+ // do that and to learn its pk, and writes the record. Refuses a folder that
64
+ // has one.
65
+ static async init(dir, name = 'main', user = 'me') {
66
+ const h = new DiskHarbor(dir);
67
+ if (existsSync(join(h.dir, 'wards', name, 'seed')))
68
+ throw new Error(`ward ${name} already exists in ${h.dir}`);
69
+ await mkdir(join(h.dir, 'classes'), { recursive: true });
70
+ const classes = join(h.dir, DEFAULT_CODE);
71
+ if (!existsSync(classes))
72
+ await writeFile(classes, '// The classes this harbor holds beside the built-in ones. Export each one by name.\nexport {};\n');
73
+ const hosted = await h.create(name, user);
74
+ await setup(hosted, user);
75
+ return { dir: h.dir, name, pk: hosted.pk, user };
76
+ }
77
+ // Take the lease and boot every ward in the folder. The lease is the
78
+ // harbor's vouch made a store property: two processes over one seed are two
79
+ // wards with one pk, so a held lease refuses the boot.
80
+ async boot() {
81
+ await this.#lease();
82
+ await super.boot();
83
+ }
84
+ async close() {
85
+ if (!this.#held)
86
+ return;
87
+ await unlink(join(this.dir, 'lease')).catch(() => { });
88
+ HELD.delete(this.dir);
89
+ this.#held = false;
90
+ }
91
+ // Boot a ward whose folder was placed here by hand.
92
+ async adoptFolder(name) {
93
+ const kept = await this.store.load(name);
94
+ if (!kept)
95
+ throw new Error(`no ward ${name} in ${this.dir}`);
96
+ return this.host(name, kept);
97
+ }
98
+ async #lease() {
99
+ if (HELD.has(this.dir))
100
+ throw new Error(`harbor ${this.dir} is held by pid ${process.pid}`);
101
+ const path = join(this.dir, 'lease');
102
+ if (existsSync(path)) {
103
+ const pid = Number(await readFile(path, 'utf8'));
104
+ let alive = pid !== process.pid; // our own pid with no entry in HELD is a lease we left behind
105
+ if (alive) {
106
+ try {
107
+ process.kill(pid, 0);
108
+ }
109
+ catch (e) {
110
+ alive = e.code === 'EPERM'; // EPERM: somebody is home, and not us. anything else: a corpse
111
+ }
112
+ }
113
+ if (alive)
114
+ throw new Error(`harbor ${this.dir} is held by pid ${pid}`);
115
+ await unlink(path).catch(() => { }); // the corpse, so the write below can be exclusive
116
+ }
117
+ try {
118
+ await writeFile(path, String(process.pid), { flag: 'wx', mode: 0o600 });
119
+ }
120
+ catch {
121
+ throw new Error(`harbor ${this.dir} is held: another daemon took the lease first`);
122
+ }
123
+ HELD.add(this.dir);
124
+ this.#held = true;
125
+ }
126
+ }
@@ -0,0 +1,26 @@
1
+ import type { BeingClass } from '@quo-systems/quo';
2
+ import { Harbor, Socket as Held } from '@quo-systems/quo/harbor';
3
+ import { DurableStorage } from './storage.ts';
4
+ import type { State, Env } from './platform.d.ts';
5
+ export declare const BUILT_IN: Record<string, BeingClass>;
6
+ export declare class EdgeHarbor extends Harbor {
7
+ #private;
8
+ readonly sockets: Set<Held>;
9
+ readonly root: string | undefined;
10
+ static open(state: State, env: Env, classes?: Record<string, BeingClass>): Promise<EdgeHarbor>;
11
+ constructor(store: DurableStorage, root: string | undefined, classes: Record<string, BeingClass>);
12
+ ready(): Promise<void>;
13
+ fetch(request: Request, path: string): Promise<Response>;
14
+ health(): {
15
+ ok: boolean;
16
+ wards: {
17
+ [k: string]: string;
18
+ };
19
+ reaches: {
20
+ [k: string]: string;
21
+ };
22
+ sockets: number;
23
+ };
24
+ upgrade(): Response;
25
+ rootAsk(request: Request): Promise<Response>;
26
+ }
@@ -0,0 +1,123 @@
1
+ import { isSilence } from '@quo-systems/quo';
2
+ import { User, Desk, Avatar } from '../../beings/index.js';
3
+ import { setup } from '../../beings/setup.js';
4
+ import { Harbor, Socket as Held, SUITE } from '@quo-systems/quo/harbor';
5
+ import { DurableStorage, sealKey } from './storage.js';
6
+ export const BUILT_IN = { User, Desk, Avatar };
7
+ const json = (status, v, headers = {}) => new Response(JSON.stringify(v), { status, headers: { 'content-type': 'application/json', ...headers } });
8
+ const open = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST, OPTIONS', 'access-control-allow-headers': 'content-type, quo-suite' };
9
+ export class EdgeHarbor extends Harbor {
10
+ sockets = new Set();
11
+ root;
12
+ #booted;
13
+ static async open(state, env, classes = {}) {
14
+ if (!env.QUO_SEED_KEY)
15
+ throw new Error('no QUO_SEED_KEY: a seed is never kept plain on the edge');
16
+ return new EdgeHarbor(new DurableStorage(state.storage, await sealKey(env.QUO_SEED_KEY)), env.QUO_ROOT, classes);
17
+ }
18
+ constructor(store, root, classes) {
19
+ super(store, async () => ({ ...BUILT_IN, ...classes }));
20
+ this.root = root;
21
+ }
22
+ // Once per instance, on the first request: the platform may have woken a
23
+ // fresh instance, and every ward boots from storage before anything is
24
+ // answered.
25
+ ready() {
26
+ return (this.#booted ??= this.boot());
27
+ }
28
+ // The worker hands the request here with the path after the harbor's
29
+ // prefix: `/quo/<pk>`, `/quo`, `/health`, `/root`.
30
+ async fetch(request, path) {
31
+ await this.ready();
32
+ if (path === '/health')
33
+ return json(200, this.health(), { 'access-control-allow-origin': '*' });
34
+ if (path === '/root')
35
+ return this.rootAsk(request);
36
+ if (path === '/quo' || path === '/quo/') {
37
+ if (request.headers.get('upgrade')?.toLowerCase() === 'websocket')
38
+ return this.upgrade();
39
+ return json(404, { error: 'POST /quo/<pk>' }, open);
40
+ }
41
+ if (path.startsWith('/quo/')) {
42
+ if (request.method === 'OPTIONS')
43
+ return new Response(null, { status: 204, headers: open });
44
+ const pk = path.slice(5);
45
+ if (request.method !== 'POST' || !/^[0-9a-f]{128}$/.test(pk))
46
+ return json(404, { error: 'POST /quo/<pk>' }, open);
47
+ // The wire suite, read before anything is carried: a caller speaking
48
+ // another one is answered nothing delivered, as the daemon's door does.
49
+ const suite = request.headers.get('quo-suite');
50
+ if (suite !== null && suite !== String(SUITE))
51
+ return json(404, { error: `this door speaks wire suite ${SUITE}` }, open);
52
+ const back = await this.deliver(pk, new Uint8Array(await request.arrayBuffer()));
53
+ if (back === undefined)
54
+ return json(404, { error: 'no reach for that pk' }, open);
55
+ return new Response(back, { status: 200, headers: { 'content-type': 'application/octet-stream', ...open } });
56
+ }
57
+ return json(404, { error: 'no such route' });
58
+ }
59
+ health() {
60
+ return {
61
+ ok: true,
62
+ wards: Object.fromEntries([...this.wards].map(([n, h]) => [n, h.pk])),
63
+ reaches: Object.fromEntries([...this.reaches].map(([pk, b]) => [pk, b.held ? 'held' : 'reach'])),
64
+ sockets: this.sockets.size,
65
+ };
66
+ }
67
+ // A dialer's socket: the server end of a pair, held here, its pks bound
68
+ // at announce and unbound at close. The platform keeps the object awake
69
+ // while the socket is open.
70
+ upgrade() {
71
+ const pair = new WebSocketPair();
72
+ const server = pair[1];
73
+ server.accept();
74
+ const s = new Held(server, (pk, bytes) => this.deliver(pk, bytes), (far) => void this.bind(far, s, true), () => {
75
+ this.unbind(s);
76
+ this.sockets.delete(s);
77
+ });
78
+ this.sockets.add(s);
79
+ s.announce([...this.doors.keys()]);
80
+ return new Response(null, { status: 101, webSocket: pair[0] });
81
+ }
82
+ // The root's door. Under the secret only; without one configured the
83
+ // edge has no root at all, and every owner it will ever have was invited
84
+ // by a root that existed when it did.
85
+ async rootAsk(request) {
86
+ const bearer = request.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ?? '';
87
+ if (request.method !== 'POST' || !this.root || bearer.length !== this.root.length || !same(bearer, this.root))
88
+ return json(401, { error: 'the root holds the secret' });
89
+ let body = {};
90
+ try {
91
+ body = (await request.json());
92
+ }
93
+ catch {
94
+ return json(400, { error: 'a root ask is JSON' });
95
+ }
96
+ try {
97
+ if (body.init) {
98
+ const name = typeof body.init.ward === 'string' ? body.init.ward : 'main';
99
+ const user = typeof body.init.user === 'string' ? body.init.user : 'me';
100
+ if (this.wards.has(name))
101
+ return json(409, { error: `ward ${name} already exists here` });
102
+ const hosted = await this.create(name, user);
103
+ await setup(hosted, user);
104
+ return json(200, { name, pk: hosted.pk, user });
105
+ }
106
+ const hosted = this.wards.get(typeof body.ward === 'string' ? body.ward : 'main');
107
+ if (!hosted)
108
+ return json(404, { error: 'no such ward' });
109
+ const out = await hosted.ask(typeof body.method === 'string' ? body.method : undefined, (body.args ?? {}));
110
+ return json(200, { result: isSilence(out) ? { silence: true } : out });
111
+ }
112
+ catch (e) {
113
+ return json(500, { error: e instanceof Error ? e.message : String(e) });
114
+ }
115
+ }
116
+ }
117
+ // Constant time, as far as a comparison in a worker can be.
118
+ function same(a, b) {
119
+ let out = 0;
120
+ for (let i = 0; i < a.length; i++)
121
+ out |= a.charCodeAt(i) ^ b.charCodeAt(i);
122
+ return out === 0;
123
+ }
@@ -0,0 +1,7 @@
1
+ import type { State, Env } from './platform.d.ts';
2
+ export type Result = {
3
+ name: string;
4
+ skip?: string;
5
+ error?: string;
6
+ };
7
+ export declare function exercise(request: Request, base: string, path: string, state: State, env: Env): Promise<Response>;
@@ -0,0 +1,179 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // What the edge terrain runs, inside the platform: the library's
3
+ // conformance suite against two edge harbors in one object, each over its
4
+ // own prefixed view of the object's storage, reaching each other by request
5
+ // through the worker's own origin under `/x/<sub>/quo`, so every relation
6
+ // crosses the route. Down is the reach refused on both; migrate lifts seed,
7
+ // partition and record out of one view into the other and boots there. It
8
+ // reports rather than throws, so the worker hands the list back and Node
9
+ // asserts on it. Only with `QUO_EXERCISE` set, which a deployment never sets.
10
+ import { conform, conformStore, conformReach } from '@quo-systems/quo/conformance';
11
+ import { request as byRequest } from '@quo-systems/quo/harbor';
12
+ import { Printer, Shop, Customer, Echo, Member } from '@quo-systems/quo/conformance';
13
+ import { EdgeHarbor } from './edge.js';
14
+ import { DurableStorage, sealKey } from './storage.js';
15
+ const assert = {
16
+ equal(a, b, m) {
17
+ if (a !== b)
18
+ throw new Error(`${m}: ${JSON.stringify(a)} is not ${JSON.stringify(b)}`);
19
+ },
20
+ deepEqual(a, b, m = 'not equal') {
21
+ if (JSON.stringify(a) !== JSON.stringify(b))
22
+ throw new Error(`${m}: ${JSON.stringify(a)} is not ${JSON.stringify(b)}`);
23
+ },
24
+ };
25
+ const CLASSES = { Printer, Shop, Customer, Echo, Member };
26
+ // The harbors the exercise made, by sub name, for the router below.
27
+ const subs = new Map();
28
+ async function pair(state, env, run) {
29
+ const key = await sealKey(env.QUO_SEED_KEY ?? '');
30
+ const names = [`a-${run}-${Date.now()}`, `b-${run}-${Date.now()}`];
31
+ const harbors = names.map((n) => new EdgeHarbor(new DurableStorage(state.storage, key, `x:${n}:`), undefined, CLASSES));
32
+ for (const [i, h] of harbors.entries()) {
33
+ await h.ready();
34
+ subs.set(names[i], h);
35
+ }
36
+ return { harbors, names };
37
+ }
38
+ function probe(base, state, env) {
39
+ let n = 0;
40
+ return async () => {
41
+ const p = await pair(state, env, n++);
42
+ const wardOf = new Map();
43
+ const where = new Map();
44
+ let k = 0;
45
+ const at = (name) => {
46
+ const h = p.harbors[where.get(name)];
47
+ const w = h.wards.get(name);
48
+ return { h, w, p: w.partition };
49
+ };
50
+ // the far harbor learns the pk by a hint: the link's reach hint, this world's route
51
+ const learn = (i, w) => p.harbors[1 - i].hint(w.pk, `${base}/x/${p.names[i]}/quo`);
52
+ return {
53
+ oneWard: false,
54
+ async boot(key, Class, cells = {}, { isPublic = false } = {}) {
55
+ for (const h of p.harbors)
56
+ h.classes[Class.name] = Class;
57
+ const name = `W-${key}`;
58
+ const i = where.get(name) ?? k++ % 2;
59
+ const h = p.harbors[i];
60
+ const w = h.wards.get(name) ?? (await h.create(name));
61
+ if (!where.has(name)) {
62
+ where.set(name, i);
63
+ learn(i, w);
64
+ }
65
+ wardOf.set(key, name);
66
+ assert.deepEqual(await w.ask('boot', { key, class: Class.name, public: isPublic }), { booted: key });
67
+ if (Object.keys(cells).length)
68
+ Object.assign(at(name).p.beings[key], cells);
69
+ return {
70
+ pk: w.pk,
71
+ get cells() {
72
+ return at(name).p.beings[key];
73
+ },
74
+ get being() {
75
+ return at(name).w.being(key);
76
+ },
77
+ minted: { has: (pk) => at(name).p.bind[key].minted.includes(pk) },
78
+ forgeKnock: (inv) => void (at(name).p.bind[key].knocks[`${inv.ward}:${inv.heir}`] = { current: 'ab'.repeat(32), next: null, spoke: true, sent: true, seq: 0 }),
79
+ set down(v) {
80
+ for (const each of p.harbors) {
81
+ if (v)
82
+ each.down.add(at(name).w.pk);
83
+ else
84
+ each.down.delete(at(name).w.pk);
85
+ }
86
+ },
87
+ get down() {
88
+ return p.harbors[0].down.has(at(name).w.pk);
89
+ },
90
+ };
91
+ },
92
+ heir(inv) {
93
+ for (const h of p.harbors)
94
+ for (const w of h.wards.values())
95
+ if (w.pk === inv.ward) {
96
+ const r = w.partition.heirs[inv.heir];
97
+ return r && { current: r.current, announced: r.announced, fresh: r.fresh };
98
+ }
99
+ return undefined;
100
+ },
101
+ census() {
102
+ const c = { wards: {}, binds: {}, cells: {} };
103
+ for (const h of p.harbors)
104
+ for (const w of h.wards.values()) {
105
+ const pt = w.partition;
106
+ c.wards[w.pk] = { beings: Object.keys(pt.beings), public: pt.public, heirs: Object.fromEntries(Object.entries(pt.heirs).map(([pk, r]) => [pk, { being: r.being, id: r.id, current: r.current, announced: r.announced, fresh: r.fresh, mark: r.mark, spent: [...r.spent] }])) };
107
+ for (const [key, b] of Object.entries(pt.bind)) {
108
+ c.binds[key] = {
109
+ ward: w.pk,
110
+ standings: Object.fromEntries(Object.entries(b.standings).map(([id, s]) => [id, { ward: s.ward, heir: s.heir, seq: s.seq }])),
111
+ occupants: { ...b.occupants },
112
+ knocks: Object.keys(b.knocks),
113
+ answered: Object.keys(b.answered),
114
+ minted: [...b.minted],
115
+ secrets: [...Object.values(b.standings).flatMap((s) => [s.current, s.next]), ...Object.values(b.knocks).flatMap((s) => [s.current, s.next])].filter((x) => x !== null),
116
+ };
117
+ c.cells[key] = pt.beings[key];
118
+ }
119
+ }
120
+ return c;
121
+ },
122
+ // seed, partition and record lifted out of one object's storage into
123
+ // the other's, same seed, same pk; the old copy gone before the new boots
124
+ async migrate(key) {
125
+ const name = wardOf.get(key);
126
+ const from = where.get(name), to = 1 - from;
127
+ const pk = p.harbors[from].wards.get(name).pk;
128
+ const kept = (await p.harbors[from].drop(name));
129
+ const w = await p.harbors[to].adopt(name, kept);
130
+ assert.equal(w.pk, pk, 'a ward that moved harbor changed its pk');
131
+ where.set(name, to);
132
+ p.harbors[from].reaches.delete(pk);
133
+ learn(to, w);
134
+ },
135
+ };
136
+ };
137
+ }
138
+ // `/exercise` runs the suite; `/x/<sub>/quo/<pk>` is a sub-harbor's route,
139
+ // which is how the two reach each other through the worker.
140
+ export async function exercise(request, base, path, state, env) {
141
+ const sub = /^\/x\/([\w.-]+)(\/.*)$/.exec(path);
142
+ if (sub) {
143
+ const h = subs.get(sub[1]);
144
+ if (!h)
145
+ return new Response(JSON.stringify({ error: 'no such harbor' }), { status: 404, headers: { 'content-type': 'application/json' } });
146
+ return h.fetch(request, sub[2]);
147
+ }
148
+ const queued = [];
149
+ const test = (name, opts, fn) => {
150
+ queued.push({ name, skip: opts.skip ?? false, fn });
151
+ };
152
+ const key = await sealKey(env.QUO_SEED_KEY ?? '');
153
+ let views = 0;
154
+ conformStore('durable storage', async () => new DurableStorage(state.storage, key, `s:${views++}:${Date.now()}:`), { test });
155
+ let lines = 0;
156
+ conformReach('request, through the worker', async () => {
157
+ const name = `r-${lines++}-${Date.now()}`;
158
+ const h = new EdgeHarbor(new DurableStorage(state.storage, key, `x:${name}:`), undefined, CLASSES);
159
+ await h.ready();
160
+ subs.set(name, h);
161
+ return { reach: byRequest(`${base}/x/${name}/quo`), hold: async (pk, door) => void h.doors.set(pk, async (b) => ({ bytes: await door(b), heard: true })), drop: async () => void subs.delete(name) };
162
+ }, { test });
163
+ conform('edge, two harbors in one object by request', probe(base, state, env), { canDown: true, test });
164
+ const results = [];
165
+ for (const { name, skip, fn } of queued) {
166
+ if (skip) {
167
+ results.push({ name, skip });
168
+ continue;
169
+ }
170
+ try {
171
+ await fn();
172
+ results.push({ name });
173
+ }
174
+ catch (e) {
175
+ results.push({ name, error: String(e?.stack ?? e) });
176
+ }
177
+ }
178
+ return new Response(JSON.stringify({ results }), { headers: { 'content-type': 'application/json' } });
179
+ }