@nervur-org/dock 0.5.0 → 0.6.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/README.md +7 -4
- package/api/route.ts +4 -72
- package/beings/avatar.ts +11 -7
- package/beings/carry.ts +129 -116
- package/beings/clock.ts +11 -7
- package/beings/desk.ts +7 -6
- package/beings/doorbell.ts +36 -7
- package/beings/index.ts +17 -4
- package/beings/join.ts +7 -4
- package/beings/link.ts +1 -1
- package/beings/porter.ts +5 -1
- package/beings/presence.ts +15 -5
- package/beings/setup.ts +27 -21
- package/beings/user.ts +26 -17
- package/beings/way.ts +1 -1
- package/{dock → box}/agents.ts +37 -44
- package/{dock → box}/apns.ts +4 -4
- package/box/box.ts +31 -0
- package/box/contract.ts +41 -0
- package/box/dialers.ts +88 -0
- package/box/dock.ts +148 -0
- package/box/envoy.ts +96 -0
- package/box/faculty.ts +111 -0
- package/{dock → box}/fcm.ts +1 -1
- package/box/grants.ts +90 -0
- package/box/http.ts +93 -0
- package/{dock → box}/http2.ts +7 -7
- package/box/index.ts +201 -0
- package/box/join.ts +78 -0
- package/box/local.ts +37 -0
- package/{dock → box}/location.ts +15 -3
- package/{dock → box}/notifications.ts +17 -5
- package/box/push.ts +85 -0
- package/box/ring.ts +85 -0
- package/{dock → box}/service.ts +2 -2
- package/box/timer.ts +150 -0
- package/box/tree.ts +22 -0
- package/{dock → box}/webpush.ts +2 -2
- package/cli/client.ts +1 -1
- package/cli/daemon.ts +101 -103
- package/cli/estate.ts +2 -3
- package/cli/nervur.ts +44 -25
- package/cli/stand.ts +1 -1
- package/dist/api/route.d.ts +1 -2
- package/dist/api/route.js +4 -76
- package/dist/beings/avatar.d.ts +4 -3
- package/dist/beings/avatar.js +9 -6
- package/dist/beings/carry.d.ts +10 -12
- package/dist/beings/carry.js +119 -127
- package/dist/beings/clock.js +10 -7
- package/dist/beings/desk.js +8 -7
- package/dist/beings/doorbell.d.ts +3 -1
- package/dist/beings/doorbell.js +37 -4
- package/dist/beings/index.d.ts +24 -4
- package/dist/beings/index.js +15 -4
- package/dist/beings/join.d.ts +1 -0
- package/dist/beings/join.js +6 -4
- package/dist/beings/link.d.ts +0 -1
- package/dist/beings/link.js +1 -1
- package/dist/beings/porter.js +5 -1
- package/dist/beings/presence.d.ts +1 -0
- package/dist/beings/presence.js +12 -3
- package/dist/beings/setup.d.ts +5 -2
- package/dist/beings/setup.js +16 -15
- package/dist/beings/user.d.ts +4 -3
- package/dist/beings/user.js +18 -11
- package/dist/{dock → box}/agents.d.ts +7 -4
- package/dist/{dock → box}/agents.js +30 -37
- package/dist/{dock → box}/apns.d.ts +1 -1
- package/dist/{dock → box}/apns.js +3 -3
- package/dist/box/box.d.ts +9 -0
- package/dist/box/box.js +18 -0
- package/dist/box/contract.d.ts +6 -0
- package/dist/box/contract.js +35 -0
- package/dist/{dock → box}/dialers.d.ts +7 -4
- package/dist/box/dialers.js +76 -0
- package/dist/box/dock.d.ts +182 -0
- package/dist/box/dock.js +128 -0
- package/dist/box/envoy.d.ts +44 -0
- package/dist/box/envoy.js +75 -0
- package/dist/box/faculty.d.ts +68 -0
- package/dist/box/faculty.js +97 -0
- package/dist/{dock → box}/fcm.d.ts +1 -1
- package/dist/{dock → box}/grants.d.ts +4 -3
- package/dist/box/grants.js +78 -0
- package/dist/box/http.d.ts +26 -0
- package/dist/box/http.js +89 -0
- package/dist/{dock → box}/http2.d.ts +1 -1
- package/dist/{dock → box}/http2.js +6 -6
- package/dist/box/index.d.ts +46 -0
- package/dist/box/index.js +179 -0
- package/dist/box/join.d.ts +19 -0
- package/dist/box/join.js +67 -0
- package/dist/{dock → box}/local.d.ts +3 -2
- package/dist/box/local.js +27 -0
- package/dist/{dock → box}/location.d.ts +3 -0
- package/dist/box/location.js +21 -0
- package/dist/{dock → box}/notifications.d.ts +3 -0
- package/dist/{dock → box}/notifications.js +14 -4
- package/dist/{dock → box}/push.d.ts +12 -18
- package/dist/box/push.js +83 -0
- package/dist/box/ring.d.ts +18 -0
- package/dist/box/ring.js +55 -0
- package/dist/{dock → box}/service.d.ts +1 -1
- package/dist/{dock → box}/service.js +1 -1
- package/dist/{dock → box}/timer.d.ts +2 -0
- package/dist/box/timer.js +127 -0
- package/dist/box/tree.d.ts +5 -0
- package/dist/box/tree.js +14 -0
- package/dist/{dock → box}/webpush.d.ts +1 -1
- package/dist/cli/client.d.ts +1 -0
- package/dist/cli/daemon.d.ts +2 -8
- package/dist/cli/daemon.js +102 -107
- package/dist/cli/estate.js +2 -3
- package/dist/cli/nervur.js +44 -24
- package/dist/cli/stand.d.ts +0 -14
- package/dist/cli/stand.js +1 -1
- package/dist/harbor/browser.d.ts +8 -5
- package/dist/harbor/browser.js +20 -21
- package/dist/harbor/capacitor.d.ts +3 -3
- package/dist/harbor/capacitor.js +9 -8
- package/dist/harbor/disk.d.ts +9 -9
- package/dist/harbor/disk.js +56 -51
- package/dist/harbor/edge/alarm.d.ts +4 -2
- package/dist/harbor/edge/alarm.js +9 -19
- package/dist/harbor/edge/edge.d.ts +7 -6
- package/dist/harbor/edge/edge.js +58 -53
- package/dist/harbor/edge/exercise.js +4 -4
- package/dist/harbor/edge/given.d.ts +0 -1
- package/dist/harbor/edge/given.js +0 -29
- package/dist/harbor/edge/storage.d.ts +0 -2
- package/dist/harbor/edge/storage.js +2 -2
- package/dist/harbor/http.js +4 -2
- package/dist/harbor/sealed.d.ts +0 -5
- package/dist/harbor/sealed.js +0 -39
- package/dist/harbor/tauri.d.ts +2 -2
- package/dist/harbor/tauri.js +13 -10
- package/dist/human/app.d.ts +1 -1
- package/dist/human/app.js +10 -12
- package/dist/human/door.d.ts +0 -1
- package/dist/human/door.js +1 -1
- package/dist/human/html.js +1 -1
- package/dist/human/move.js +1 -1
- package/dist/human/tab.bundle.txt +3629 -2824
- package/dist/human/wake.d.ts +5 -2
- package/dist/human/wake.js +23 -14
- package/dist/human/web.d.ts +0 -1
- package/dist/human/web.js +3 -3
- package/dist/human/worker.bundle.txt +2945 -2118
- package/dist/human/worlds.js +3 -3
- package/dist/mcp/agent.d.ts +0 -1
- package/dist/mcp/agent.js +1 -1
- package/dist/mcp/allow.js +3 -2
- package/dist/mcp/http.d.ts +0 -1
- package/dist/mcp/http.js +1 -1
- package/dist/mcp/oauth.d.ts +3 -4
- package/dist/mcp/oauth.js +4 -4
- package/dist/mcp/route.d.ts +1 -1
- package/dist/mcp/runner.d.ts +0 -1
- package/dist/mcp/runner.js +1 -1
- package/dist/mcp/server.d.ts +0 -1
- package/dist/mcp/server.js +1 -1
- package/harbor/browser.ts +22 -24
- package/harbor/capacitor.ts +9 -8
- package/harbor/disk.ts +53 -48
- package/harbor/edge/alarm.ts +15 -12
- package/harbor/edge/edge.ts +61 -55
- package/harbor/edge/exercise.ts +4 -4
- package/harbor/edge/given.ts +3 -33
- package/harbor/edge/storage.ts +2 -2
- package/harbor/http.ts +5 -3
- package/harbor/sealed.ts +0 -35
- package/harbor/tauri.ts +13 -10
- package/human/app.ts +12 -13
- package/human/door.ts +1 -1
- package/human/html.ts +1 -1
- package/human/local.ts +1 -1
- package/human/move.ts +1 -1
- package/human/tab.bundle.txt +3629 -2824
- package/human/wake.ts +36 -16
- package/human/web.ts +4 -4
- package/human/worker.bundle.txt +2945 -2118
- package/human/worlds.ts +3 -3
- package/mcp/agent.ts +1 -1
- package/mcp/allow.ts +2 -1
- package/mcp/http.ts +1 -1
- package/mcp/oauth.ts +4 -4
- package/mcp/route.ts +1 -1
- package/mcp/runner.ts +1 -1
- package/mcp/server.ts +1 -1
- package/package.json +9 -8
- package/beings/envoy.ts +0 -218
- package/dist/beings/envoy.d.ts +0 -54
- package/dist/beings/envoy.js +0 -211
- package/dist/dock/console.d.ts +0 -6
- package/dist/dock/console.js +0 -20
- package/dist/dock/dialers.js +0 -75
- package/dist/dock/faculty.d.ts +0 -24
- package/dist/dock/faculty.js +0 -142
- package/dist/dock/grants.js +0 -99
- package/dist/dock/index.d.ts +0 -41
- package/dist/dock/index.js +0 -113
- package/dist/dock/local.js +0 -44
- package/dist/dock/location.js +0 -11
- package/dist/dock/push.js +0 -132
- package/dist/dock/timer.js +0 -149
- package/dock/console.ts +0 -23
- package/dock/dialers.ts +0 -85
- package/dock/faculty.ts +0 -148
- package/dock/grants.ts +0 -111
- package/dock/index.ts +0 -133
- package/dock/local.ts +0 -48
- package/dock/push.ts +0 -161
- package/dock/timer.ts +0 -174
- /package/dist/{dock → box}/fcm.js +0 -0
- /package/dist/{dock → box}/webpush.js +0 -0
package/human/worlds.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// `tab.ts` is the shell over this, and this is what the memory harbor proves.
|
|
12
12
|
import type { Harbor, Hosted } from 'nervur/harbor';
|
|
13
13
|
import { Avatar, USER } from '../beings/avatar.ts';
|
|
14
|
-
import { settled } from '../
|
|
14
|
+
import { settled } from '../box/join.ts';
|
|
15
15
|
|
|
16
16
|
// Which worlds a device has joined, and where each one lives. A tab learns
|
|
17
17
|
// this from the page it is on, one world per origin, and needs it only to
|
|
@@ -113,14 +113,14 @@ export async function fresh(ward: Hosted): Promise<Relation> {
|
|
|
113
113
|
// is a tab that hangs.
|
|
114
114
|
for (let n = 1; n <= 64; n++) {
|
|
115
115
|
const key = `r${n}`;
|
|
116
|
-
const out = (await ward.ask('boot', { key, class: 'Presence' })) as { error?: string };
|
|
116
|
+
const out = (await ward.ask('boot', { key, class: 'beings/Presence' })) as { error?: string };
|
|
117
117
|
if (out.error) continue;
|
|
118
118
|
// She takes what the box lends at her birth, and birth is synchronous
|
|
119
119
|
// while taking a standing is not. She is handed back when they have
|
|
120
120
|
// landed, not before: a screen painted from her describe a moment early
|
|
121
121
|
// is a page missing every faculty of the box, and nothing paints it
|
|
122
122
|
// again until the person navigates.
|
|
123
|
-
await settled();
|
|
123
|
+
await settled(ward);
|
|
124
124
|
return { key, avatar: ward.being(key) as Avatar };
|
|
125
125
|
}
|
|
126
126
|
throw new Error('the tab could not boot an avatar: every key it tries is taken');
|
package/mcp/agent.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type Run = (event: JsonObject) => Promise<Json>;
|
|
|
21
21
|
|
|
22
22
|
// The ask on the user being that takes the result. Gated to devices; the
|
|
23
23
|
// agent's client identity is what the gate reads.
|
|
24
|
-
|
|
24
|
+
const REPORT = 'report';
|
|
25
25
|
|
|
26
26
|
// `claude -p` or any CLI agent: the command runs in the agent's folder with
|
|
27
27
|
// the event, as JSON, as its last argument. The result is what it printed.
|
package/mcp/allow.ts
CHANGED
|
@@ -92,7 +92,8 @@ export class Exchange {
|
|
|
92
92
|
const world = worlds.find((w) => w.pk === inv.ward);
|
|
93
93
|
if (!world) return again('That invitation is for a world that does not live here.');
|
|
94
94
|
const identity = word(f.get('identity'));
|
|
95
|
-
|
|
95
|
+
// the ward judges the rest: an identity that is one of its beings is refused at the join
|
|
96
|
+
if (identity === null) return again('An identity is one word.');
|
|
96
97
|
const joined = await this.o.join(identity, inv, world.ward);
|
|
97
98
|
if (joined.error) return again(`Not in: ${joined.error}.`);
|
|
98
99
|
const out = await this.o.oauth.complete(request, identity, world.ward);
|
package/mcp/http.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { Serving } from '../beings/side.ts';
|
|
|
14
14
|
import { mcpSide } from './server.ts';
|
|
15
15
|
|
|
16
16
|
export type Session = { identity: string; ward: string; transport: StreamableHTTPServerTransport; serving: Serving; touched: number };
|
|
17
|
-
|
|
17
|
+
const SESSION_IDLE =60 * 60 * 1000;
|
|
18
18
|
// An identity in a world: one harbor holds many, and the grant names which.
|
|
19
19
|
export type Resolve = (identity: string, ward: string) => Promise<{ avatar?: Avatar; error?: string }>;
|
|
20
20
|
|
package/mcp/oauth.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// S256, the code grant, refresh, opaque tokens. It is a route, not Quo. Its
|
|
6
6
|
// one job is to end with a client identity, which the front desk trades for
|
|
7
7
|
// an invitation and the avatar knocks with; the grant it issues is the
|
|
8
|
-
// box's, `
|
|
8
|
+
// box's, `box/grants.ts`, honoured on every route, and what this file
|
|
9
9
|
// keeps for itself is only the dance: clients, pending requests and codes.
|
|
10
10
|
// No secret of Quo's is ever here.
|
|
11
11
|
//
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
// The web route finishes a request by calling `complete(request, identity)`
|
|
19
19
|
// after its allow page, where the human hands the invitation the root minted;
|
|
20
20
|
// that returns the redirect that carries the code back to the client.
|
|
21
|
-
import { REFRESH_TTL, type Grants } from '../
|
|
21
|
+
import { REFRESH_TTL, type Bearers as Grants } from '../box/grants.ts';
|
|
22
22
|
|
|
23
|
-
export { ACCESS_TTL, REFRESH_TTL } from '../
|
|
23
|
+
export { ACCESS_TTL, REFRESH_TTL } from '../box/grants.ts';
|
|
24
24
|
|
|
25
25
|
// A code, a client id: every key here is a string the far side chose, and a
|
|
26
26
|
// bare lookup would find `__proto__` and hand back an object with no
|
|
@@ -244,7 +244,7 @@ function withQuery(base: string, params: Record<string, string | null>): string
|
|
|
244
244
|
return u.toString();
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
async function readBody(req: Request, limit = 64 * 1024): Promise<string> {
|
|
248
248
|
const s = await req.text();
|
|
249
249
|
if (s.length > limit) throw new Error('too large');
|
|
250
250
|
return s;
|
package/mcp/route.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { OAuth, type Store } from './oauth.ts';
|
|
10
10
|
import type { McpHttp } from './http.ts';
|
|
11
11
|
import type { Handler } from '../harbor/http.ts';
|
|
12
|
-
import type { Grants } from '../
|
|
12
|
+
import type { Bearers as Grants } from '../box/grants.ts';
|
|
13
13
|
|
|
14
14
|
// The routes' public faces, as the exchange and the metadata name them.
|
|
15
15
|
export type Routes = { mcp: string; web: string; quo?: string; api?: string };
|
package/mcp/runner.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { presentation } from '../beings/look.ts';
|
|
|
19
19
|
// from the device and never from cells. `turns` is the ceiling of model
|
|
20
20
|
// calls in one turn of the conversation; ten by default.
|
|
21
21
|
export type Model = { url: string; model: string; key?: string; turns?: number };
|
|
22
|
-
|
|
22
|
+
const TURNS = 10;
|
|
23
23
|
|
|
24
24
|
// The wire shapes, only as much as the loop reads and writes.
|
|
25
25
|
export type Tool = { type: 'function'; function: { name: string; description?: string; parameters: JsonObject } };
|
package/mcp/server.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const VERSION = '0.0.0';
|
|
|
22
22
|
export type { Subject } from '../beings/side.ts';
|
|
23
23
|
|
|
24
24
|
// The empty ask as a tool: her describe whole, asks and notes.
|
|
25
|
-
|
|
25
|
+
const DESCRIBE: Tool ={ name: 'describe', description: 'the empty ask: her describe, the asks and the notes', inputSchema: { type: 'object' } };
|
|
26
26
|
|
|
27
27
|
// Her describe, spoken as tools, the empty ask first. Name, description and
|
|
28
28
|
// input are verbatim; an output schema crosses when she declared one. Her
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nervur-org/dock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The dock: what every estate on Quo needs and nobody writes twice. A daemon and the nervur 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",
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"types": "./dist/harbor/disk.d.ts",
|
|
29
29
|
"default": "./dist/harbor/disk.js"
|
|
30
30
|
},
|
|
31
|
-
"./cli": "./dist/cli/nervur.js",
|
|
32
31
|
"./stand": {
|
|
33
32
|
"types": "./dist/cli/stand.d.ts",
|
|
34
33
|
"default": "./dist/cli/stand.js"
|
|
@@ -80,9 +79,10 @@
|
|
|
80
79
|
},
|
|
81
80
|
"scripts": {
|
|
82
81
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node build/bundle.ts && chmod +x dist/cli/nervur.js && cp harbor/edge/platform.d.ts dist/harbor/edge/ && cp -R cli/estate dist/cli/",
|
|
83
|
-
"check": "node
|
|
84
|
-
"deep": "node --test \"test/terrain/*.test.ts\"",
|
|
85
|
-
"
|
|
82
|
+
"check": "node ../../test.mjs \"test/*.test.ts\"",
|
|
83
|
+
"deep": "node --test --test-timeout=900000 --test-concurrency=1 \"test/terrain/*.test.ts\"",
|
|
84
|
+
"cover": "node --test --test-timeout=300000 --test-concurrency=1 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-exclude=\"**/harbor/idb.ts\" --test-coverage-lines=97 --test-coverage-branches=85 --test-coverage-functions=91 \"test/*.test.ts\"",
|
|
85
|
+
"prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
|
@@ -91,9 +91,10 @@
|
|
|
91
91
|
"@capacitor/filesystem": "^8.1.3",
|
|
92
92
|
"@capacitor/geolocation": "^8.2.2",
|
|
93
93
|
"@capacitor/local-notifications": "^8.3.1",
|
|
94
|
+
"@capacitor/push-notifications": "^8.1.2",
|
|
94
95
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
95
|
-
"nervur": "^0.
|
|
96
|
-
"@nervur-org/ui": "^0.
|
|
96
|
+
"nervur": "^0.19.0",
|
|
97
|
+
"@nervur-org/ui": "^0.5.0",
|
|
97
98
|
"ws": "^8.21.3"
|
|
98
99
|
},
|
|
99
100
|
"devDependencies": {
|
|
@@ -107,7 +108,7 @@
|
|
|
107
108
|
"files": [
|
|
108
109
|
"dist",
|
|
109
110
|
"beings",
|
|
110
|
-
"
|
|
111
|
+
"box",
|
|
111
112
|
"cli",
|
|
112
113
|
"harbor",
|
|
113
114
|
"mcp",
|
package/beings/envoy.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The envoy base: what every envoy is and no envoy has alone. An envoy is
|
|
3
|
-
// a being of the org's standing in for a ward she does not run, an account
|
|
4
|
-
// at a thing that speaks no Quo, and the trunk's envoy chapter says the
|
|
5
|
-
// seven things she is. This is those seven as one class an author extends,
|
|
6
|
-
// so that a vendor's envoy is her asks out, her signature and her decoding,
|
|
7
|
-
// and nothing else.
|
|
8
|
-
//
|
|
9
|
-
// Two gates, one credential, one way out, one way in, one memory, one way
|
|
10
|
-
// back. `credential` and `forget` are the owner's; `deliver`, for an envoy
|
|
11
|
-
// that is called in, is the owner's too, and the daemon asks it with the
|
|
12
|
-
// vendor's raw bytes and headers. What an occupant may ask is the class's
|
|
13
|
-
// asks out, gated by `occupant` or by her own reading of the notes.
|
|
14
|
-
//
|
|
15
|
-
// Nothing here knows a vendor's name. The signature is `verify`, the
|
|
16
|
-
// vocabulary is `decode`, and what a delivery wakes is `wake`, each a
|
|
17
|
-
// method the class fills; the base keeps the discipline around them: the
|
|
18
|
-
// bytes checked before anything is parsed, the id kept bounded so a retry
|
|
19
|
-
// lands once, `{ ok }` answered before the wake goes out, a wake that meets
|
|
20
|
-
// silence written down and never retried.
|
|
21
|
-
import { Being, isSilence, isWord, wordOf } from 'nervur';
|
|
22
|
-
import type { Asker, AskSpec, JsonObject, OccupantRecord } from 'nervur';
|
|
23
|
-
|
|
24
|
-
// The two gates. The owner is the ward's ask pointer, `{ id: 'OWNER' }`,
|
|
25
|
-
// which is how the daemon asks `deliver` and how the root places a
|
|
26
|
-
// credential; an occupant is anyone holding a standing at her.
|
|
27
|
-
export const owner = (_rec: OccupantRecord | undefined, asker: Asker): boolean => asker.id === 'OWNER';
|
|
28
|
-
export const occupant = (rec: OccupantRecord | undefined): boolean => rec !== undefined;
|
|
29
|
-
|
|
30
|
-
// The third gate: who may place what the vendor gave this account. WORLDS
|
|
31
|
-
// has placing an envoy as an ask on home, shown by the gate to the org's
|
|
32
|
-
// presences and to its model, with the box never logged into for it, so
|
|
33
|
-
// this cannot be the owner alone. It is the owner or an occupant whose
|
|
34
|
-
// note says she may, placed with her standing the way every other note is,
|
|
35
|
-
// and it is never a default: an org that marked nobody has the root's
|
|
36
|
-
// shell and nothing else, which is where this began.
|
|
37
|
-
export const places = (rec: OccupantRecord | undefined, asker: Asker): boolean => asker.id === 'OWNER' || rec?.notes.credential === true;
|
|
38
|
-
|
|
39
|
-
// A credential as a form, for a class that wants one. `at` is never among
|
|
40
|
-
// the fields and cannot be: it is where this box sends the org's key, so a
|
|
41
|
-
// form that set it would be a form that points the box at anywhere and
|
|
42
|
-
// makes it fetch there with the account's own secret. Where a vendor
|
|
43
|
-
// answers is the class's, since the class is the thing that speaks that
|
|
44
|
-
// vendor, and only the owner may name another.
|
|
45
|
-
export const CREDENTIAL = (properties: Record<string, JsonObject>, required: string[]): AskSpec => ({
|
|
46
|
-
description: 'what the vendor gave this account: kept in her cells, and never shown again',
|
|
47
|
-
input: { type: 'object', properties, required },
|
|
48
|
-
for: places,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// One delivery, decoded by the class into what the base needs: the
|
|
52
|
-
// vendor's id for it, its kind in the vendor's words, which vendor id it
|
|
53
|
-
// is about, and the values the class read off it.
|
|
54
|
-
export type Delivery = { id: string; kind: string; about: string | null; data: JsonObject };
|
|
55
|
-
// What a delivery wakes: one ask on the way back to whoever the vendor id
|
|
56
|
-
// was remembered for, or nothing.
|
|
57
|
-
export type Wake = { method: string; args: JsonObject };
|
|
58
|
-
// What a way out heard: the status, and the body as an object, `{}` when
|
|
59
|
-
// there was none or it was not JSON.
|
|
60
|
-
export type Heard = { ok: boolean; status: number; body: JsonObject };
|
|
61
|
-
|
|
62
|
-
const SEEN = 200;
|
|
63
|
-
const FRESH = 5 * 60_000;
|
|
64
|
-
|
|
65
|
-
export class Envoy extends Being {
|
|
66
|
-
static override cells: JsonObject = { credential: null, about: {}, seen: [], unwoken: [] };
|
|
67
|
-
static override asks: Record<string, AskSpec> = {
|
|
68
|
-
credential: { description: 'what the vendor gave the account: kept in her cells, never shown', input: { type: 'object' }, for: places },
|
|
69
|
-
forget: { description: 'forget the credential', input: { type: 'object' }, for: places },
|
|
70
|
-
};
|
|
71
|
-
// The one ask in, for an envoy the vendor calls: a class that is called
|
|
72
|
-
// in spreads it into her asks, `{ ...Envoy.asks, ...Envoy.IN, ...hers }`,
|
|
73
|
-
// and a one-way envoy shows no deliver at all.
|
|
74
|
-
static IN: Record<string, AskSpec> = {
|
|
75
|
-
deliver: { description: 'the vendor delivers: its raw bytes and headers, from the daemon as the owner', input: { type: 'object', properties: { body: { type: 'string' }, headers: { type: 'object' } }, required: ['body', 'headers'] }, for: owner },
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// 1. Her credential is in her cells, and she gets it herself. The owner
|
|
79
|
-
// places what the vendor gave, values only; `check` is the class's word
|
|
80
|
-
// on what it must hold, a reason or null.
|
|
81
|
-
protected check(c: JsonObject): string | null {
|
|
82
|
-
for (const [k, v] of Object.entries(c)) if (typeof v !== 'string' || v === '') return `${k} is a non-empty string`;
|
|
83
|
-
return Object.keys(c).length ? null : 'a credential has at least one field';
|
|
84
|
-
}
|
|
85
|
-
// Where the vendor answers, as the class that speaks that vendor knows
|
|
86
|
-
// it. A class that leaves it null has an owner who must name one once.
|
|
87
|
-
static at: string | null = null;
|
|
88
|
-
|
|
89
|
-
credential(args: JsonObject, asker: Asker): JsonObject {
|
|
90
|
-
// `at` is the one field a form may not carry. From anyone but the
|
|
91
|
-
// owner it is dropped rather than refused, since a human pasting a key
|
|
92
|
-
// has said nothing about it either way, and what stands is the one
|
|
93
|
-
// already placed or the class's own.
|
|
94
|
-
const { at: named, ...rest } = args;
|
|
95
|
-
const at = asker.id === 'OWNER' && typeof named === 'string' ? named : ((this.held?.at as string | undefined) ?? (this.constructor as typeof Envoy).at);
|
|
96
|
-
const c: JsonObject = { ...rest, ...(typeof at === 'string' ? { at } : {}) };
|
|
97
|
-
const why = this.check(c);
|
|
98
|
-
if (why) return { error: why };
|
|
99
|
-
this.cells.credential = c;
|
|
100
|
-
return { kept: true };
|
|
101
|
-
}
|
|
102
|
-
forget(): JsonObject {
|
|
103
|
-
this.cells.credential = null;
|
|
104
|
-
return { kept: false };
|
|
105
|
-
}
|
|
106
|
-
protected get held(): JsonObject | null {
|
|
107
|
-
return (this.cells.credential as JsonObject | null) ?? null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// 2. Her asks out are the vendor's surface, narrowed: one fetch, from
|
|
111
|
-
// `at` in her credential with `key` as the bearer, the answer as values.
|
|
112
|
-
// The class reshapes what it heard and passes nothing through raw.
|
|
113
|
-
protected async out(path: string, init: { method?: string; body?: JsonObject; headers?: Record<string, string> } = {}): Promise<Heard> {
|
|
114
|
-
const c = this.held;
|
|
115
|
-
if (!c || typeof c.at !== 'string') return { ok: false, status: 0, body: { error: 'no credential: the owner has not placed one' } };
|
|
116
|
-
let r: Response;
|
|
117
|
-
try {
|
|
118
|
-
r = await fetch(`${c.at}${path}`, {
|
|
119
|
-
method: init.method ?? (init.body ? 'POST' : 'GET'),
|
|
120
|
-
headers: { ...(typeof c.key === 'string' ? { authorization: `Bearer ${c.key}` } : {}), ...(init.body ? { 'content-type': 'application/json' } : {}), ...(init.headers ?? {}) },
|
|
121
|
-
...(init.body ? { body: JSON.stringify(init.body) } : {}),
|
|
122
|
-
});
|
|
123
|
-
} catch {
|
|
124
|
-
return { ok: false, status: 0, body: { error: 'the vendor is unreachable' } };
|
|
125
|
-
}
|
|
126
|
-
const v = (await r.json().catch(() => ({}))) as unknown;
|
|
127
|
-
const body = v !== null && typeof v === 'object' && !Array.isArray(v) ? (v as JsonObject) : {};
|
|
128
|
-
return { ok: r.ok, status: r.status, body };
|
|
129
|
-
}
|
|
130
|
-
// The vendor's no, as an error object in her own words.
|
|
131
|
-
protected refused(h: Heard): JsonObject {
|
|
132
|
-
return h.status === 0 ? h.body : { error: `the vendor refused: ${h.status}${typeof h.body.message === 'string' ? ` ${h.body.message}` : ''}` };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// 4. Her cells are what the vendor told her: the map from the vendor's id
|
|
136
|
-
// to whom it is about, written when she asked out, read when called in.
|
|
137
|
-
protected remember(vendorId: string, by: string, more: JsonObject = {}): void {
|
|
138
|
-
(this.cells.about as Record<string, JsonObject>)[vendorId] = { by, ...more };
|
|
139
|
-
}
|
|
140
|
-
protected recall(vendorId: string): (JsonObject & { by: string }) | undefined {
|
|
141
|
-
const m = this.cells.about as Record<string, JsonObject>;
|
|
142
|
-
return Object.hasOwn(m, vendorId) ? (m[vendorId] as JsonObject & { by: string }) : undefined;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 3. `deliver` is her one ask in. The daemon asks it as the owner with
|
|
146
|
-
// the raw body and the headers; an empty body is a redirect landing,
|
|
147
|
-
// the query in the headers under `:query`. The signature is checked
|
|
148
|
-
// before anything is parsed; a delivery she has seen is `{ ok }` at once
|
|
149
|
-
// and wakes nobody twice; and 6, `{ ok }` goes back before the wake.
|
|
150
|
-
async deliver(args: JsonObject): Promise<JsonObject> {
|
|
151
|
-
const body = typeof args.body === 'string' ? args.body : '';
|
|
152
|
-
const headers = (args.headers && typeof args.headers === 'object' && !Array.isArray(args.headers) ? args.headers : {}) as Record<string, string>;
|
|
153
|
-
if (body === '') return this.landed(headers[':query'] ?? '', headers);
|
|
154
|
-
if (!(await this.verify(body, headers))) return { error: 'bad signature' };
|
|
155
|
-
const d = this.decode(body, headers);
|
|
156
|
-
if ('error' in d) return d as JsonObject;
|
|
157
|
-
const seen = this.cells.seen as string[];
|
|
158
|
-
const mark = `${d.kind}:${d.id}`;
|
|
159
|
-
if (seen.includes(mark)) return { ok: true };
|
|
160
|
-
seen.push(mark);
|
|
161
|
-
while (seen.length > SEEN) seen.shift();
|
|
162
|
-
void this.#wake(d);
|
|
163
|
-
return { ok: true };
|
|
164
|
-
}
|
|
165
|
-
// The class's word on the vendor's signature, over the bytes as sent.
|
|
166
|
-
protected verify(_body: string, _headers: Record<string, string>): Promise<boolean> | boolean {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
// The class's reading of the vendor's vocabulary.
|
|
170
|
-
protected decode(_body: string, _headers: Record<string, string>): Delivery | { error: string } {
|
|
171
|
-
return { error: 'not called in' };
|
|
172
|
-
}
|
|
173
|
-
// Where a redirect lands: an OAuth code in the query, for a class that
|
|
174
|
-
// begins its account that way. The base takes nothing from it.
|
|
175
|
-
protected landed(_query: string, _headers: Record<string, string>): JsonObject | Promise<JsonObject> {
|
|
176
|
-
return { ok: true };
|
|
177
|
-
}
|
|
178
|
-
// A signature over text with a shared secret, hex, the shape most
|
|
179
|
-
// vendors sign in: here once so a class writes the vendor's framing only.
|
|
180
|
-
protected async hmac(secret: string, text: string): Promise<string> {
|
|
181
|
-
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
182
|
-
return [...new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text)))].map((x) => x.toString(16).padStart(2, '0')).join('');
|
|
183
|
-
}
|
|
184
|
-
// Whether a vendor's timestamp is within five minutes of now, in seconds
|
|
185
|
-
// or in milliseconds, since vendors sign in either and a class should
|
|
186
|
-
// not have to know which.
|
|
187
|
-
protected fresh(ts: string | number): boolean {
|
|
188
|
-
const t = Number(ts);
|
|
189
|
-
if (!Number.isFinite(t)) return false;
|
|
190
|
-
const ms = t < 1e11 ? t * 1000 : t;
|
|
191
|
-
return Math.abs(Date.now() - ms) <= FRESH;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 5. Her way back is a standing placed like any other, `to:<id>` for the
|
|
195
|
-
// occupant a vendor id was remembered for, handed to her at the knock.
|
|
196
|
-
// A delivery is one ask on it, in her own words, said by `wake`; the
|
|
197
|
-
// being woken cannot tell it from a clock.
|
|
198
|
-
protected wake(_d: Delivery, _about: (JsonObject & { by: string }) | undefined): Wake | null {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
async #wake(d: Delivery): Promise<void> {
|
|
202
|
-
const about = d.about === null ? undefined : this.recall(d.about);
|
|
203
|
-
const w = this.wake(d, about);
|
|
204
|
-
if (!w) return;
|
|
205
|
-
const unwoken = this.cells.unwoken as JsonObject[];
|
|
206
|
-
// a wake the class shaped wrong is written down, never dropped on the floor
|
|
207
|
-
if (typeof w.method !== 'string' || !w.method || w.args === null || typeof w.args !== 'object') return void unwoken.push({ id: d.id, kind: d.kind, why: 'a wake is { method, args }' });
|
|
208
|
-
const back = about ? this.standings[`to:${about.by}`] : undefined;
|
|
209
|
-
if (!back) return void unwoken.push({ id: d.id, kind: d.kind, why: about ? `no way back to ${about.by}` : 'about nobody she remembers' });
|
|
210
|
-
const out = await back.ask(w.method, w.args);
|
|
211
|
-
if (isSilence(out) || out === undefined) unwoken.push({ id: d.id, kind: d.kind, why: 'silence' });
|
|
212
|
-
else if (isWord(out)) unwoken.push({ id: d.id, kind: d.kind, why: wordOf(out) });
|
|
213
|
-
}
|
|
214
|
-
// What she could not wake, for the device's own code and a suite.
|
|
215
|
-
get unwoken(): JsonObject[] {
|
|
216
|
-
return this.cells.unwoken as JsonObject[];
|
|
217
|
-
}
|
|
218
|
-
}
|
package/dist/beings/envoy.d.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { Being } from 'nervur';
|
|
2
|
-
import type { Asker, AskSpec, JsonObject, OccupantRecord } from 'nervur';
|
|
3
|
-
export declare const owner: (_rec: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
4
|
-
export declare const occupant: (rec: OccupantRecord | undefined) => boolean;
|
|
5
|
-
export declare const places: (rec: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
6
|
-
export declare const CREDENTIAL: (properties: Record<string, JsonObject>, required: string[]) => AskSpec;
|
|
7
|
-
export type Delivery = {
|
|
8
|
-
id: string;
|
|
9
|
-
kind: string;
|
|
10
|
-
about: string | null;
|
|
11
|
-
data: JsonObject;
|
|
12
|
-
};
|
|
13
|
-
export type Wake = {
|
|
14
|
-
method: string;
|
|
15
|
-
args: JsonObject;
|
|
16
|
-
};
|
|
17
|
-
export type Heard = {
|
|
18
|
-
ok: boolean;
|
|
19
|
-
status: number;
|
|
20
|
-
body: JsonObject;
|
|
21
|
-
};
|
|
22
|
-
export declare class Envoy extends Being {
|
|
23
|
-
#private;
|
|
24
|
-
static cells: JsonObject;
|
|
25
|
-
static asks: Record<string, AskSpec>;
|
|
26
|
-
static IN: Record<string, AskSpec>;
|
|
27
|
-
protected check(c: JsonObject): string | null;
|
|
28
|
-
static at: string | null;
|
|
29
|
-
credential(args: JsonObject, asker: Asker): JsonObject;
|
|
30
|
-
forget(): JsonObject;
|
|
31
|
-
protected get held(): JsonObject | null;
|
|
32
|
-
protected out(path: string, init?: {
|
|
33
|
-
method?: string;
|
|
34
|
-
body?: JsonObject;
|
|
35
|
-
headers?: Record<string, string>;
|
|
36
|
-
}): Promise<Heard>;
|
|
37
|
-
protected refused(h: Heard): JsonObject;
|
|
38
|
-
protected remember(vendorId: string, by: string, more?: JsonObject): void;
|
|
39
|
-
protected recall(vendorId: string): (JsonObject & {
|
|
40
|
-
by: string;
|
|
41
|
-
}) | undefined;
|
|
42
|
-
deliver(args: JsonObject): Promise<JsonObject>;
|
|
43
|
-
protected verify(_body: string, _headers: Record<string, string>): Promise<boolean> | boolean;
|
|
44
|
-
protected decode(_body: string, _headers: Record<string, string>): Delivery | {
|
|
45
|
-
error: string;
|
|
46
|
-
};
|
|
47
|
-
protected landed(_query: string, _headers: Record<string, string>): JsonObject | Promise<JsonObject>;
|
|
48
|
-
protected hmac(secret: string, text: string): Promise<string>;
|
|
49
|
-
protected fresh(ts: string | number): boolean;
|
|
50
|
-
protected wake(_d: Delivery, _about: (JsonObject & {
|
|
51
|
-
by: string;
|
|
52
|
-
}) | undefined): Wake | null;
|
|
53
|
-
get unwoken(): JsonObject[];
|
|
54
|
-
}
|