@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,370 @@
|
|
|
1
|
+
import { isSilence } from '@nervur-org/nervur';
|
|
2
|
+
import { User, Desk, Avatar, Clock } from '../../beings/index.js';
|
|
3
|
+
import { setup } from '../../beings/setup.js';
|
|
4
|
+
import { Harbor, Socket as Held } from '@nervur-org/nervur/harbor';
|
|
5
|
+
import { quoRoute } from '../quo.js';
|
|
6
|
+
import { webRoute } from '../../human/web.js';
|
|
7
|
+
import { DOCK, TIMER, Console, facultyOf, settled, stand } from '../../dock/index.js';
|
|
8
|
+
import { Alarm, alarmsIn } from './alarm.js';
|
|
9
|
+
import { Grants, GRANTS } from '../../dock/grants.js';
|
|
10
|
+
import { find, letIn } from '../../beings/join.js';
|
|
11
|
+
import { apiRoute } from '../../api/route.js';
|
|
12
|
+
import { mcpRoute } from '../../mcp/route.js';
|
|
13
|
+
import { McpHttp } from '../../mcp/http.js';
|
|
14
|
+
import { Exchange } from '../../mcp/allow.js';
|
|
15
|
+
import { DurableStorage, danceInStorage, folderInStorage, settingsInStorage, wearInStorage } from './storage.js';
|
|
16
|
+
import { LENT } from './given.js';
|
|
17
|
+
import { sealKey } from '../seal.js';
|
|
18
|
+
// The beings a deployed worker carries. The Clock is among them because this
|
|
19
|
+
// terrain lends a timer: a box that lends one and holds no class that can ask
|
|
20
|
+
// for a wake lends a name nobody there could use.
|
|
21
|
+
export const BUILT_IN = { User, Desk, Avatar, Clock };
|
|
22
|
+
const json = (status, v, headers = {}) => new Response(JSON.stringify(v), { status, headers: { 'content-type': 'application/json', ...headers } });
|
|
23
|
+
export class EdgeHarbor extends Harbor {
|
|
24
|
+
sockets = new Set();
|
|
25
|
+
root;
|
|
26
|
+
// What the deploy carried, held from the worker's first line.
|
|
27
|
+
deployed;
|
|
28
|
+
// The rows: the settings a root writes, and the design a world wears.
|
|
29
|
+
settings;
|
|
30
|
+
folder;
|
|
31
|
+
wear;
|
|
32
|
+
// The model side's dance, one key in the same object: clients, pending
|
|
33
|
+
// requests and codes, all of them living minutes.
|
|
34
|
+
dance;
|
|
35
|
+
// The object's storage, unprefixed and unsealed: the alarm is the object's
|
|
36
|
+
// own and is no row.
|
|
37
|
+
#storage;
|
|
38
|
+
#booted;
|
|
39
|
+
// The MCP endpoint, one per instance: a session is a client's connection
|
|
40
|
+
// and belongs to the harbor, not to the request that opened it.
|
|
41
|
+
#mcp;
|
|
42
|
+
// A deployed worker: the box ward stands and lends what this terrain can
|
|
43
|
+
// answer. The exercise builds harbors with the constructor instead and
|
|
44
|
+
// says nothing, so it stands no ward of its own, as a command on a disk
|
|
45
|
+
// folder stands none.
|
|
46
|
+
static async open(state, env, deployed = {}) {
|
|
47
|
+
if (!env.QUO_SEED_KEY)
|
|
48
|
+
throw new Error('no QUO_SEED_KEY: a seed is never kept plain on the edge');
|
|
49
|
+
const harbor = new EdgeHarbor(new DurableStorage(state.storage, await sealKey(env.QUO_SEED_KEY)), env.QUO_ROOT, deployed);
|
|
50
|
+
harbor.lent = LENT;
|
|
51
|
+
return harbor;
|
|
52
|
+
}
|
|
53
|
+
constructor(store, root, deployed = {}) {
|
|
54
|
+
// The faculty classes this terrain can stand, beside the beings: the box
|
|
55
|
+
// ward's own front desk and the grants its routes honour. They are named
|
|
56
|
+
// here and not left to the deploy, because `lent` names classes and a
|
|
57
|
+
// name the loader cannot find is a ward that would not boot.
|
|
58
|
+
super(store, async () => ({ ...BUILT_IN, Console, Grants, Alarm, ...deployed.classes }));
|
|
59
|
+
this.root = root;
|
|
60
|
+
this.deployed = deployed;
|
|
61
|
+
this.#storage = store.storage;
|
|
62
|
+
// The rows live beside the wards in the same object, under the same
|
|
63
|
+
// prefix, so an exercise standing two harbors in one object gives each
|
|
64
|
+
// its own settings and its own design.
|
|
65
|
+
this.settings = settingsInStorage(store.storage, store.prefix);
|
|
66
|
+
this.folder = folderInStorage(store.storage, store.prefix);
|
|
67
|
+
this.wear = wearInStorage(store.storage, store.prefix);
|
|
68
|
+
this.dance = danceInStorage(store.storage, store.prefix);
|
|
69
|
+
}
|
|
70
|
+
// The whole of what a side is handed, read at every request: the deploy's
|
|
71
|
+
// half never changes and the rows are worn while the worker runs.
|
|
72
|
+
async given() {
|
|
73
|
+
return { ...this.deployed, settings: await this.settings.load(), folder: this.folder };
|
|
74
|
+
}
|
|
75
|
+
// What this box lends, by name. Set before the first request and read at
|
|
76
|
+
// the boot; a harbor that says nothing stands no ward of its own and lends
|
|
77
|
+
// nothing. `given.ts` holds the list a deployed worker uses and why it is
|
|
78
|
+
// that list.
|
|
79
|
+
lent;
|
|
80
|
+
// Once per instance, on the first request: the platform may have woken a
|
|
81
|
+
// fresh instance, and every ward boots from storage before anything is
|
|
82
|
+
// answered.
|
|
83
|
+
ready() {
|
|
84
|
+
return (this.#booted ??= this.boot());
|
|
85
|
+
}
|
|
86
|
+
// The box's own ward first, because a being born in any other ward lends
|
|
87
|
+
// from her first line and a ground that could not reach it yet would leave
|
|
88
|
+
// her holding nothing until her next birth. Then every ward the object
|
|
89
|
+
// keeps, then the beings who took what the box lends: a box is up when
|
|
90
|
+
// they have landed.
|
|
91
|
+
async boot() {
|
|
92
|
+
if (this.lent) {
|
|
93
|
+
this.lend = await stand(this, this.lent);
|
|
94
|
+
// A token issued is a cell written, and the cells are the dock ward's
|
|
95
|
+
// partition like every faculty's: there is no second place on this
|
|
96
|
+
// terrain for the box to keep its state.
|
|
97
|
+
const grants = facultyOf(this, GRANTS);
|
|
98
|
+
if (grants)
|
|
99
|
+
grants.persist = () => this.wards.get(DOCK)?.save() ?? Promise.resolve();
|
|
100
|
+
// The clock here is the object's alarm, and only the object may set its
|
|
101
|
+
// own, so the timer is handed the two calls the same way.
|
|
102
|
+
const timer = facultyOf(this, TIMER);
|
|
103
|
+
if (timer)
|
|
104
|
+
timer.alarms = alarmsIn(this.#storage);
|
|
105
|
+
}
|
|
106
|
+
await super.boot();
|
|
107
|
+
await settled();
|
|
108
|
+
}
|
|
109
|
+
// The alarm went off: no request, no route and nobody asking, which is the
|
|
110
|
+
// whole point of it. The box ward is up by now, so the round runs and the
|
|
111
|
+
// timer arms for whatever the Clocks say they are next due at.
|
|
112
|
+
//
|
|
113
|
+
// The wards are written here because no ask returned to write them: a wake
|
|
114
|
+
// went through a Clock's own door and wrote her fires, and the harbor never
|
|
115
|
+
// saw it. A box whose alarm fired and whose partition was not written would
|
|
116
|
+
// fire the same minute again on its next wake.
|
|
117
|
+
async rang() {
|
|
118
|
+
const timer = facultyOf(this, TIMER);
|
|
119
|
+
if (!timer)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
await timer.rang();
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await timer.setting;
|
|
126
|
+
for (const w of this.wards.values())
|
|
127
|
+
await w.save();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// The box's grants, for the routes that honour a bearer. Undefined is a
|
|
131
|
+
// harbor that stands no box ward, which is the exercise and nothing a
|
|
132
|
+
// deploy does.
|
|
133
|
+
get grants() {
|
|
134
|
+
return facultyOf(this, GRANTS);
|
|
135
|
+
}
|
|
136
|
+
// The web side, built per request because its whole configuration is rows
|
|
137
|
+
// a root writes while the worker runs. It is the same `webRoute` the daemon
|
|
138
|
+
// mounts, handed the same things by a terrain that has no disk: the bundles
|
|
139
|
+
// from the deploy, the design out of storage, the origins out of the
|
|
140
|
+
// settings, and the worker's own address when no root has named one.
|
|
141
|
+
//
|
|
142
|
+
// A deployment carrying no bundles serves no page at all, and says so
|
|
143
|
+
// rather than serving one that loads nothing.
|
|
144
|
+
//
|
|
145
|
+
// The credential exchange's allow page shares this prefix, as it does on
|
|
146
|
+
// the daemon: the worlds' pages first, then the page where a human hands
|
|
147
|
+
// an invitation and the model side's dance finishes.
|
|
148
|
+
async web(request, path, base) {
|
|
149
|
+
const given = await this.given();
|
|
150
|
+
if (!given.code)
|
|
151
|
+
return json(404, { error: 'this deployment carries no page code' });
|
|
152
|
+
const at = await this.routes(base);
|
|
153
|
+
const route = webRoute(this, { at, code: given.code, folder: given.folder, apps: given.settings.apps ?? null, bell: given.settings.bell ?? null });
|
|
154
|
+
const rest = path.slice('/web'.length);
|
|
155
|
+
const page = await route(request, rest);
|
|
156
|
+
if (page)
|
|
157
|
+
return page;
|
|
158
|
+
const exchange = await this.exchange(base);
|
|
159
|
+
return (await exchange?.handle(request, rest)) ?? new Response('no such page', { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
160
|
+
}
|
|
161
|
+
// Where the four routes answer, as the world outside sees them: what a root
|
|
162
|
+
// named, and the worker's own address under this harbor's prefix for every
|
|
163
|
+
// one it did not.
|
|
164
|
+
async routes(base) {
|
|
165
|
+
const r = (await this.settings.load()).routes;
|
|
166
|
+
return { quo: r?.quo ?? `${base}/quo`, web: r?.web ?? `${base}/web`, mcp: r?.mcp ?? `${base}/mcp`, api: r?.api ?? `${base}/api` };
|
|
167
|
+
}
|
|
168
|
+
// Which ward a caller who names none is at. On a disk it is the mark an
|
|
169
|
+
// operator wrote in the folder; here it is the one world this object holds,
|
|
170
|
+
// and an object holding several says nothing rather than guessing, exactly
|
|
171
|
+
// as a folder with several wards and no mark does. The box's own ward is
|
|
172
|
+
// furniture and never a candidate.
|
|
173
|
+
here() {
|
|
174
|
+
const worlds = [...this.wards.keys()].filter((n) => n !== DOCK);
|
|
175
|
+
return worlds.length === 1 ? worlds[0] : '';
|
|
176
|
+
}
|
|
177
|
+
// What a route asks of a ward here: her avatar as she stands, and her way
|
|
178
|
+
// in with an invitation. Both are `beings/join.ts`, the same two the daemon
|
|
179
|
+
// hands its own routes.
|
|
180
|
+
#resolve = async (identity, ward) => {
|
|
181
|
+
const hosted = this.wards.get(ward);
|
|
182
|
+
return hosted ? find(hosted, identity) : { error: 'no such world' };
|
|
183
|
+
};
|
|
184
|
+
#join = async (identity, invitation, ward) => {
|
|
185
|
+
const hosted = this.wards.get(ward);
|
|
186
|
+
if (!hosted)
|
|
187
|
+
return { error: 'no such world' };
|
|
188
|
+
const r = await letIn(hosted, identity, invitation);
|
|
189
|
+
return r.error ? { error: r.error } : {};
|
|
190
|
+
};
|
|
191
|
+
// The worlds a client may be let into: every ward here but the box's own.
|
|
192
|
+
#worlds = () => [...this.wards]
|
|
193
|
+
.filter(([ward]) => ward !== DOCK)
|
|
194
|
+
.map(([ward, h]) => ({ ward, pk: h.pk, user: h.record.user }));
|
|
195
|
+
// The program side. The same `apiRoute` the daemon mounts, on the grants of
|
|
196
|
+
// this box's own ward: a request with no bearer is the stranger every route
|
|
197
|
+
// knows, and one with a bearer this box granted is an occupant.
|
|
198
|
+
async api(request, path, base) {
|
|
199
|
+
const grants = this.grants;
|
|
200
|
+
if (!grants)
|
|
201
|
+
return json(503, { error: 'this box stands no ward of its own, so it grants nothing' });
|
|
202
|
+
const at = await this.routes(base);
|
|
203
|
+
const route = apiRoute({ wards: this.wards, here: () => this.here() }, { grants, resolve: this.#resolve, join: this.#join, web: at.web });
|
|
204
|
+
return (await route(request, path.slice('/api'.length))) ?? json(404, { error: 'no such route' });
|
|
205
|
+
}
|
|
206
|
+
// The model side: the OAuth dance, kept in this object's own storage, and
|
|
207
|
+
// the MCP endpoint beside it. The endpoint's sessions live on this harbor
|
|
208
|
+
// and not on the request, because a session is a client's connection and
|
|
209
|
+
// outlives the fetch that opened it; the platform keeps the object awake
|
|
210
|
+
// while one is held and a client that comes back to a woken instance
|
|
211
|
+
// reconnects, which is what a reconnect is for.
|
|
212
|
+
async mcp(request, path, base) {
|
|
213
|
+
const grants = this.grants;
|
|
214
|
+
if (!grants)
|
|
215
|
+
return json(503, { error: 'this box stands no ward of its own, so it grants nothing' });
|
|
216
|
+
const endpoint = (this.#mcp ??= new McpHttp(this.#resolve, async () => {
|
|
217
|
+
for (const h of this.wards.values())
|
|
218
|
+
await h.save();
|
|
219
|
+
}));
|
|
220
|
+
endpoint.gone = (identity, ward) => grants.revoke(identity, ward); // removal at the ward ends the grant at the box
|
|
221
|
+
const route = await mcpRoute(this.dance, await this.routes(base), endpoint, grants);
|
|
222
|
+
const rest = path.slice('/mcp'.length);
|
|
223
|
+
return (await route.handler(request, rest)) ?? json(404, { error: 'no such route' });
|
|
224
|
+
}
|
|
225
|
+
// The dance's discovery under this prefix, for a caller that reaches the
|
|
226
|
+
// harbor at a path rather than at an origin of its own.
|
|
227
|
+
async wellKnown(request, path, base) {
|
|
228
|
+
const grants = this.grants;
|
|
229
|
+
if (!grants)
|
|
230
|
+
return json(404, { error: 'no such route' });
|
|
231
|
+
const route = await mcpRoute(this.dance, await this.routes(base), null, grants);
|
|
232
|
+
return (await route.wellKnown(request, path.slice('/.well-known'.length))) ?? json(404, { error: 'no such route' });
|
|
233
|
+
}
|
|
234
|
+
// The exchange, for the allow page on the web side: the dance it finishes
|
|
235
|
+
// is the same one the model side keeps.
|
|
236
|
+
async exchange(base) {
|
|
237
|
+
const grants = this.grants;
|
|
238
|
+
if (!grants)
|
|
239
|
+
return undefined;
|
|
240
|
+
const route = await mcpRoute(this.dance, await this.routes(base), null, grants);
|
|
241
|
+
return new Exchange({ oauth: route.oauth, worlds: this.#worlds, join: this.#join });
|
|
242
|
+
}
|
|
243
|
+
// The worker hands the request here with the path after the harbor's
|
|
244
|
+
// prefix, and with that prefix as an address, since a page tells a tab
|
|
245
|
+
// where to dial: `/quo/<pk>`, `/quo`, `/health`, `/root`, `/web`.
|
|
246
|
+
// Every door of this object, and one thing after all of them: the arming.
|
|
247
|
+
// A schedule kept at any of them moves the moment the box is armed for, an
|
|
248
|
+
// arming here is a write to the object's storage, and the ask that caused
|
|
249
|
+
// it cannot answer for it. So the response waits for the write, because a
|
|
250
|
+
// box that answered "kept" and was put to sleep before its alarm landed is
|
|
251
|
+
// a schedule nothing would fire.
|
|
252
|
+
async fetch(request, path, base) {
|
|
253
|
+
await this.ready();
|
|
254
|
+
const out = await this.#route(request, path, base);
|
|
255
|
+
await facultyOf(this, TIMER)?.setting;
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
async #route(request, path, base) {
|
|
259
|
+
if (path === '/health')
|
|
260
|
+
return json(200, this.health(), { 'access-control-allow-origin': '*' });
|
|
261
|
+
if (path === '/root')
|
|
262
|
+
return this.rootAsk(request);
|
|
263
|
+
if (path === '/web' || path.startsWith('/web/'))
|
|
264
|
+
return this.web(request, path, base);
|
|
265
|
+
if (path === '/api' || path.startsWith('/api/'))
|
|
266
|
+
return this.api(request, path, base);
|
|
267
|
+
if (path === '/mcp' || path.startsWith('/mcp/'))
|
|
268
|
+
return this.mcp(request, path, base);
|
|
269
|
+
if (path.startsWith('/.well-known/'))
|
|
270
|
+
return this.wellKnown(request, path, base);
|
|
271
|
+
// The sealed bytes are the same route the daemon mounts; the upgrade is
|
|
272
|
+
// this terrain's own listener half, a socket pair held by the object.
|
|
273
|
+
if (path === '/quo' || path.startsWith('/quo/')) {
|
|
274
|
+
if ((path === '/quo' || path === '/quo/') && request.headers.get('upgrade')?.toLowerCase() === 'websocket')
|
|
275
|
+
return this.upgrade();
|
|
276
|
+
return (await quoRoute(this)(request, path.slice('/quo'.length))) ?? json(404, { error: 'no such route' });
|
|
277
|
+
}
|
|
278
|
+
return json(404, { error: 'no such route' });
|
|
279
|
+
}
|
|
280
|
+
health() {
|
|
281
|
+
return {
|
|
282
|
+
ok: true,
|
|
283
|
+
wards: Object.fromEntries([...this.wards].map(([n, h]) => [n, h.pk])),
|
|
284
|
+
reaches: Object.fromEntries([...this.reaches].map(([pk, b]) => [pk, b.held ? 'held' : 'reach'])),
|
|
285
|
+
sockets: this.sockets.size,
|
|
286
|
+
// How many saves this object's storage refused, per ward. A refusal is
|
|
287
|
+
// already the answer of the ask that caused it, so nobody is waiting to
|
|
288
|
+
// be told; this is the other half, which is an operator asking the box
|
|
289
|
+
// how it is and being told about the one fault a healthy-looking box
|
|
290
|
+
// can carry for hours. Zero wards refused is an empty object, and the
|
|
291
|
+
// shape does not move.
|
|
292
|
+
faults: Object.fromEntries([...this.faults]),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// A dialer's socket: the server end of a pair, held here, its pks bound
|
|
296
|
+
// at announce and unbound at close. The platform keeps the object awake
|
|
297
|
+
// while the socket is open.
|
|
298
|
+
upgrade() {
|
|
299
|
+
const pair = new WebSocketPair();
|
|
300
|
+
const server = pair[1];
|
|
301
|
+
server.accept();
|
|
302
|
+
const s = new Held(server, (pk, bytes) => this.deliver(pk, bytes), (far) => void this.bind(far, s, true), () => {
|
|
303
|
+
this.unbind(s);
|
|
304
|
+
this.sockets.delete(s);
|
|
305
|
+
});
|
|
306
|
+
this.sockets.add(s);
|
|
307
|
+
s.announce([...this.doors.keys()]);
|
|
308
|
+
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
309
|
+
}
|
|
310
|
+
// The root's door. Under the secret only; without one configured the
|
|
311
|
+
// edge has no root at all, and every owner it will ever have was invited
|
|
312
|
+
// by a root that existed when it did.
|
|
313
|
+
async rootAsk(request) {
|
|
314
|
+
const bearer = request.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ?? '';
|
|
315
|
+
if (request.method !== 'POST' || !this.root || bearer.length !== this.root.length || !same(bearer, this.root))
|
|
316
|
+
return json(401, { error: 'the root holds the secret' });
|
|
317
|
+
let body = {};
|
|
318
|
+
try {
|
|
319
|
+
body = (await request.json());
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return json(400, { error: 'a root ask is JSON' });
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
if (body.init) {
|
|
326
|
+
const name = typeof body.init.ward === 'string' ? body.init.ward : 'main';
|
|
327
|
+
const user = typeof body.init.user === 'string' ? body.init.user : 'me';
|
|
328
|
+
if (this.wards.has(name))
|
|
329
|
+
return json(409, { error: `ward ${name} already exists here` });
|
|
330
|
+
const hosted = await this.create(name, user);
|
|
331
|
+
await setup(hosted, user);
|
|
332
|
+
return json(200, { name, pk: hosted.pk, user });
|
|
333
|
+
}
|
|
334
|
+
// The settings, merged over what is kept and answered whole, so a root
|
|
335
|
+
// that writes one field sees the rest and a root that writes nothing
|
|
336
|
+
// reads the row. This is how a box is configured after it is deployed:
|
|
337
|
+
// the origins the proxy serves, the apps table and the bell key.
|
|
338
|
+
if (body.settings) {
|
|
339
|
+
const settings = { ...(await this.settings.load()), ...body.settings };
|
|
340
|
+
await this.settings.save(settings);
|
|
341
|
+
return json(200, { settings });
|
|
342
|
+
}
|
|
343
|
+
// One design file put there, or taken away with null, and the names now
|
|
344
|
+
// worn. A world wears a design only when it holds `tokens.css`.
|
|
345
|
+
if (body.wear) {
|
|
346
|
+
for (const [file, text] of Object.entries(body.wear)) {
|
|
347
|
+
if (!/^[\w.-]+$/.test(file) && !/^fonts\/[\w.-]+$/.test(file))
|
|
348
|
+
return json(400, { error: `not a design file: ${file}` });
|
|
349
|
+
await this.wear.put(file, typeof text === 'string' ? text : null);
|
|
350
|
+
}
|
|
351
|
+
return json(200, { worn: await this.wear.worn() });
|
|
352
|
+
}
|
|
353
|
+
const hosted = this.wards.get(typeof body.ward === 'string' ? body.ward : 'main');
|
|
354
|
+
if (!hosted)
|
|
355
|
+
return json(404, { error: 'no such ward' });
|
|
356
|
+
const out = await hosted.ask(typeof body.method === 'string' ? body.method : undefined, (body.args ?? {}));
|
|
357
|
+
return json(200, { result: isSilence(out) ? { silence: true } : out });
|
|
358
|
+
}
|
|
359
|
+
catch (e) {
|
|
360
|
+
return json(500, { error: e instanceof Error ? e.message : String(e) });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Constant time, as far as a comparison in a worker can be.
|
|
365
|
+
function same(a, b) {
|
|
366
|
+
let out = 0;
|
|
367
|
+
for (let i = 0; i < a.length; i++)
|
|
368
|
+
out |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
369
|
+
return out === 0;
|
|
370
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
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 '@nervur-org/nervur/conformance';
|
|
11
|
+
import { request as byRequest } from '@nervur-org/nervur/harbor';
|
|
12
|
+
import { Printer, Shop, Customer, Echo, Member } from '@nervur-org/nervur/conformance';
|
|
13
|
+
import { EdgeHarbor } from './edge.js';
|
|
14
|
+
import { DurableStorage } from './storage.js';
|
|
15
|
+
import { ROW_CEILING } from './given.js';
|
|
16
|
+
import { told } from '@nervur-org/nervur';
|
|
17
|
+
import { sealKey } from '../seal.js';
|
|
18
|
+
const assert = {
|
|
19
|
+
equal(a, b, m) {
|
|
20
|
+
if (a !== b)
|
|
21
|
+
throw new Error(`${m}: ${JSON.stringify(a)} is not ${JSON.stringify(b)}`);
|
|
22
|
+
},
|
|
23
|
+
deepEqual(a, b, m = 'not equal') {
|
|
24
|
+
if (JSON.stringify(a) !== JSON.stringify(b))
|
|
25
|
+
throw new Error(`${m}: ${JSON.stringify(a)} is not ${JSON.stringify(b)}`);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
// The suite's own classes, and no bundles: the exercise mounts no side and
|
|
29
|
+
// serves no page, so this deployment names no code.
|
|
30
|
+
const DEPLOYED = { classes: { Printer, Shop, Customer, Echo, Member } };
|
|
31
|
+
// The harbors the exercise made, by sub name, for the router below.
|
|
32
|
+
const subs = new Map();
|
|
33
|
+
async function pair(state, env, run) {
|
|
34
|
+
const key = await sealKey(env.QUO_SEED_KEY ?? '');
|
|
35
|
+
const names = [`a-${run}-${Date.now()}`, `b-${run}-${Date.now()}`];
|
|
36
|
+
const harbors = names.map((n) => new EdgeHarbor(new DurableStorage(state.storage, key, `x:${n}:`), undefined, DEPLOYED));
|
|
37
|
+
for (const [i, h] of harbors.entries()) {
|
|
38
|
+
await h.ready();
|
|
39
|
+
subs.set(names[i], h);
|
|
40
|
+
}
|
|
41
|
+
return { harbors, names };
|
|
42
|
+
}
|
|
43
|
+
function probe(base, state, env) {
|
|
44
|
+
let n = 0;
|
|
45
|
+
return async () => {
|
|
46
|
+
const p = await pair(state, env, n++);
|
|
47
|
+
const wardOf = new Map();
|
|
48
|
+
const where = new Map();
|
|
49
|
+
let k = 0;
|
|
50
|
+
const at = (name) => {
|
|
51
|
+
const h = p.harbors[where.get(name)];
|
|
52
|
+
const w = h.wards.get(name);
|
|
53
|
+
return { h, w, p: h.partitionOf(name) };
|
|
54
|
+
};
|
|
55
|
+
// the far harbor learns the pk by a hint: the link's reach hint, this world's route
|
|
56
|
+
const learn = (i, w) => p.harbors[1 - i].hint(w.pk, `${base}/x/${p.names[i]}/quo`);
|
|
57
|
+
return {
|
|
58
|
+
oneWard: false,
|
|
59
|
+
async boot(key, Class, cells = {}, { isPublic = false } = {}) {
|
|
60
|
+
for (const h of p.harbors)
|
|
61
|
+
h.classes[Class.name] = Class;
|
|
62
|
+
const name = `W-${key}`;
|
|
63
|
+
const i = where.get(name) ?? k++ % 2;
|
|
64
|
+
const h = p.harbors[i];
|
|
65
|
+
const w = h.wards.get(name) ?? (await h.create(name));
|
|
66
|
+
if (!where.has(name)) {
|
|
67
|
+
where.set(name, i);
|
|
68
|
+
learn(i, w);
|
|
69
|
+
}
|
|
70
|
+
wardOf.set(key, name);
|
|
71
|
+
assert.deepEqual(await w.ask('boot', { key, class: Class.name }), { booted: key });
|
|
72
|
+
if (isPublic)
|
|
73
|
+
assert.deepEqual(await w.ask('public', { key }), { public: key });
|
|
74
|
+
if (Object.keys(cells).length)
|
|
75
|
+
Object.assign(at(name).p.beings[key], cells);
|
|
76
|
+
return {
|
|
77
|
+
pk: w.pk,
|
|
78
|
+
get cells() {
|
|
79
|
+
return at(name).p.beings[key];
|
|
80
|
+
},
|
|
81
|
+
get being() {
|
|
82
|
+
return at(name).w.being(key);
|
|
83
|
+
},
|
|
84
|
+
minted: { has: (pk) => at(name).p.bind[key].minted.includes(pk) },
|
|
85
|
+
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 }),
|
|
86
|
+
set down(v) {
|
|
87
|
+
for (const each of p.harbors) {
|
|
88
|
+
if (v)
|
|
89
|
+
each.down.add(at(name).w.pk);
|
|
90
|
+
else
|
|
91
|
+
each.down.delete(at(name).w.pk);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
get down() {
|
|
95
|
+
return p.harbors[0].down.has(at(name).w.pk);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
heir(inv) {
|
|
100
|
+
for (const h of p.harbors)
|
|
101
|
+
for (const w of h.wards.values())
|
|
102
|
+
if (w.pk === inv.ward) {
|
|
103
|
+
const r = h.partitionOf(w.name).heirs[inv.heir];
|
|
104
|
+
return r && { current: r.current, announced: r.announced, fresh: r.fresh };
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
},
|
|
108
|
+
census() {
|
|
109
|
+
const c = { wards: {}, binds: {}, cells: {} };
|
|
110
|
+
for (const h of p.harbors)
|
|
111
|
+
for (const w of h.wards.values()) {
|
|
112
|
+
const pt = h.partitionOf(w.name);
|
|
113
|
+
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] }])) };
|
|
114
|
+
for (const [key, b] of Object.entries(pt.bind)) {
|
|
115
|
+
c.binds[key] = {
|
|
116
|
+
ward: w.pk,
|
|
117
|
+
standings: Object.fromEntries(Object.entries(b.standings).map(([id, s]) => [id, { ward: s.ward, heir: s.heir, seq: s.seq }])),
|
|
118
|
+
occupants: { ...b.occupants },
|
|
119
|
+
knocks: Object.keys(b.knocks),
|
|
120
|
+
answered: Object.keys(b.answered),
|
|
121
|
+
minted: [...b.minted],
|
|
122
|
+
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),
|
|
123
|
+
};
|
|
124
|
+
c.cells[key] = pt.beings[key];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return c;
|
|
128
|
+
},
|
|
129
|
+
// seed, partition and record lifted out of one object's storage into
|
|
130
|
+
// the other's, same seed, same pk; the old copy gone before the new boots
|
|
131
|
+
owner(key) {
|
|
132
|
+
return (method, args) => at(wardOf.get(key)).w.ask(method, args);
|
|
133
|
+
},
|
|
134
|
+
async migrate(key) {
|
|
135
|
+
const name = wardOf.get(key);
|
|
136
|
+
const from = where.get(name), to = 1 - from;
|
|
137
|
+
const pk = p.harbors[from].wards.get(name).pk;
|
|
138
|
+
const kept = (await p.harbors[from].drop(name));
|
|
139
|
+
const w = await p.harbors[to].adopt(name, kept);
|
|
140
|
+
assert.equal(w.pk, pk, 'a ward that moved harbor changed its pk');
|
|
141
|
+
where.set(name, to);
|
|
142
|
+
p.harbors[from].reaches.delete(pk);
|
|
143
|
+
learn(to, w);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// `/exercise` runs the suite; `/x/<sub>/quo/<pk>` is a sub-harbor's route,
|
|
149
|
+
// which is how the two reach each other through the worker.
|
|
150
|
+
export async function exercise(request, base, path, state, env) {
|
|
151
|
+
const sub = /^\/x\/([\w.-]+)(\/.*)$/.exec(path);
|
|
152
|
+
if (sub) {
|
|
153
|
+
const h = subs.get(sub[1]);
|
|
154
|
+
if (!h)
|
|
155
|
+
return new Response(JSON.stringify({ error: 'no such harbor' }), { status: 404, headers: { 'content-type': 'application/json' } });
|
|
156
|
+
return h.fetch(request, sub[2], `${base}/x/${sub[1]}`);
|
|
157
|
+
}
|
|
158
|
+
const queued = [];
|
|
159
|
+
const test = (name, opts, fn) => {
|
|
160
|
+
queued.push({ name, skip: opts.skip ?? false, fn });
|
|
161
|
+
};
|
|
162
|
+
const key = await sealKey(env.QUO_SEED_KEY ?? '');
|
|
163
|
+
let views = 0;
|
|
164
|
+
conformStore('durable storage', async () => new DurableStorage(state.storage, key, `s:${views++}:${Date.now()}:`), { test });
|
|
165
|
+
let lines = 0;
|
|
166
|
+
conformReach('request, through the worker', async () => {
|
|
167
|
+
const name = `r-${lines++}-${Date.now()}`;
|
|
168
|
+
const h = new EdgeHarbor(new DurableStorage(state.storage, key, `x:${name}:`), undefined, DEPLOYED);
|
|
169
|
+
await h.ready();
|
|
170
|
+
subs.set(name, h);
|
|
171
|
+
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) };
|
|
172
|
+
}, { test });
|
|
173
|
+
conform('edge, two harbors in one object by request', probe(base, state, env), { canDown: true, canLend: false, canRestart: false, test });
|
|
174
|
+
// The ceiling, on the terrain that has one. A being whose row is past it is
|
|
175
|
+
// refused by the store before the platform is asked, the arrival that wrote
|
|
176
|
+
// the row is a failed ask at the door, and she is left standing at what the
|
|
177
|
+
// object keeps rather than at what she was answered.
|
|
178
|
+
// The ceiling here is small, so what crosses the door is a few kilobytes:
|
|
179
|
+
// a row of the platform's real size costs a great deal to carry and proves
|
|
180
|
+
// the same refusal. That the number a deployment runs under is the
|
|
181
|
+
// platform's own is the line below it, which asks the store directly.
|
|
182
|
+
test('[edge] a row past the object s ceiling is refused, the arrival that wrote it fails at the door, and she stands at what is kept', {}, async () => {
|
|
183
|
+
const key = await sealKey(env.QUO_SEED_KEY ?? '');
|
|
184
|
+
const small = 8_000;
|
|
185
|
+
const h = new EdgeHarbor(new DurableStorage(state.storage, key, `c:${Date.now()}:`, small), undefined, DEPLOYED);
|
|
186
|
+
await h.ready();
|
|
187
|
+
const w = await h.create(`ceiling-${Date.now()}`);
|
|
188
|
+
for (const [k, c] of [
|
|
189
|
+
['P', 'Printer'],
|
|
190
|
+
['A', 'Customer'],
|
|
191
|
+
])
|
|
192
|
+
assert.deepEqual(await w.ask('boot', { key: k, class: c }), { booted: k });
|
|
193
|
+
const printer = w.being('P');
|
|
194
|
+
const alice = w.being('A');
|
|
195
|
+
const inv = (await printer.invite('a'));
|
|
196
|
+
assert.deepEqual(await alice.knock(inv, 'print', { doc: 'one' }), { printed: 'one' });
|
|
197
|
+
const out = await alice.knock(inv, 'print', { doc: 'x'.repeat(small * 2) });
|
|
198
|
+
assert.equal(told(out), 'threw', 'a row the object will not keep is a failed ask at the door');
|
|
199
|
+
const jobs = printer.cells.jobs.map((j) => j.doc);
|
|
200
|
+
assert.deepEqual(jobs, ['one'], 'she stands at what is kept, and not at what she was answered');
|
|
201
|
+
assert.deepEqual(await alice.knock(inv, 'print', { doc: 'two' }), { printed: 'two' }, 'and a small write after it is kept');
|
|
202
|
+
// The number a deployment runs under, asked of the store itself: this is
|
|
203
|
+
// the platform's published ceiling and nothing this suite chose.
|
|
204
|
+
const store = new DurableStorage(state.storage, key, `c2:${Date.now()}:`);
|
|
205
|
+
const kept = { seed: new Uint8Array(32).fill(3), partition: {}, record: { pk: '', code: 'x', user: '' } };
|
|
206
|
+
await store.put('big', kept);
|
|
207
|
+
const over = { beings: { B: { fat: 'x'.repeat(ROW_CEILING) } }, bind: { B: {} } };
|
|
208
|
+
let refused = false;
|
|
209
|
+
try {
|
|
210
|
+
await store.save('big', over, ['B']);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
refused = true;
|
|
214
|
+
}
|
|
215
|
+
assert.equal(refused, true, 'a row past the platform s own ceiling is refused by the store');
|
|
216
|
+
});
|
|
217
|
+
const results = [];
|
|
218
|
+
for (const { name, skip, fn } of queued) {
|
|
219
|
+
if (skip) {
|
|
220
|
+
results.push({ name, skip });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
await fn();
|
|
225
|
+
results.push({ name });
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
results.push({ name, error: String(e?.stack ?? e) });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return new Response(JSON.stringify({ results }), { headers: { 'content-type': 'application/json' } });
|
|
232
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BeingClass } from '@nervur-org/nervur';
|
|
2
|
+
import type { Apps, Folder } from '../../human/web.ts';
|
|
3
|
+
import type { Routes } from '../../mcp/route.ts';
|
|
4
|
+
export declare const LENT: Record<string, string>;
|
|
5
|
+
export declare const ROW_CEILING = 2000000;
|
|
6
|
+
export type Code = {
|
|
7
|
+
tab: string;
|
|
8
|
+
worker: string;
|
|
9
|
+
};
|
|
10
|
+
export type Settings = {
|
|
11
|
+
routes?: Routes;
|
|
12
|
+
apps?: Apps | null;
|
|
13
|
+
bell?: string | null;
|
|
14
|
+
};
|
|
15
|
+
export type SettingsStore = {
|
|
16
|
+
load(): Promise<Settings>;
|
|
17
|
+
save(settings: Settings): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export type Deployed = {
|
|
20
|
+
code?: Code;
|
|
21
|
+
classes?: Record<string, BeingClass>;
|
|
22
|
+
};
|
|
23
|
+
export type Given = Deployed & {
|
|
24
|
+
settings: Settings;
|
|
25
|
+
folder: Folder;
|
|
26
|
+
};
|