@nervur-org/dock 0.4.1 → 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 +17 -15
- 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 +10 -18
- package/GETTING_STARTED.md +0 -138
- package/GLOSSARY.md +0 -118
- package/WORLDS.md +0 -569
- 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/quo-api.md +0 -196
- package/quo-app-bells.md +0 -280
- package/quo-dock.md +0 -1945
- package/quo-harbor.md +0 -874
- package/quo-human.md +0 -1042
- package/quo-mcp.md +0 -343
- /package/dist/{dock → box}/fcm.js +0 -0
- /package/dist/{dock → box}/webpush.js +0 -0
package/README.md
CHANGED
|
@@ -14,21 +14,23 @@ npx nervur serve --dir my-estate/droplet
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
`nervur estate` writes an estate folder: one folder per harbor, the first a
|
|
17
|
-
droplet's
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
droplet's, with its stand script, its classes folder, its systemd unit and
|
|
18
|
+
a Caddyfile mapping the four routes under your domain, beside a Mac's
|
|
19
|
+
launchd agent, a package file that depends on the dock and nothing else,
|
|
20
|
+
and one document to fill in. Add your beings to `droplet/classes/` and your
|
|
21
|
+
placement to `droplet/stand.ts`. What the box does on its own, the routes
|
|
22
|
+
it answers under, the agents it runs and the lines it holds, is placed with
|
|
23
|
+
`nervur` and kept in the box's own cells, never in a file.
|
|
24
|
+
|
|
25
|
+
The documentation is at <https://nervur.org/docs>: the trunk of this
|
|
26
|
+
package, the human, model, api and harbor sides, the bells, what home,
|
|
27
|
+
presence and membership mean, and every word above the spec. This package
|
|
28
|
+
carries its code and this file, and nothing written for a reader lives
|
|
29
|
+
inside it, because a document and a release move at different speeds.
|
|
30
|
+
|
|
31
|
+
Quo itself is at <https://quo.systems>: the spec, the vectors a kit is
|
|
32
|
+
checked against, and the verifier that replays them. This package
|
|
33
|
+
implements it and never restates it.
|
|
32
34
|
|
|
33
35
|
## Versions
|
|
34
36
|
|
package/api/route.ts
CHANGED
|
@@ -37,9 +37,8 @@ import { readText } from '../harbor/http.ts';
|
|
|
37
37
|
import type { Hosted } from 'nervur/harbor';
|
|
38
38
|
import type { Avatar } from '../beings/avatar.ts';
|
|
39
39
|
import { invitationOf } from '../beings/link.ts';
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import type { Grants } from '../dock/grants.ts';
|
|
40
|
+
import { isIdentity } from '../beings/join.ts';
|
|
41
|
+
import type { Bearers as Grants } from '../box/grants.ts';
|
|
43
42
|
import { isInvitation, isSilence, isWord, wordOf, type Invitation, type JsonObject } from 'nervur';
|
|
44
43
|
|
|
45
44
|
const open = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET, POST, OPTIONS', 'access-control-allow-headers': 'content-type, authorization' };
|
|
@@ -58,10 +57,6 @@ export type Options = {
|
|
|
58
57
|
resolve: (identity: string, ward: string) => Promise<{ avatar?: Avatar; error?: string }>;
|
|
59
58
|
// Boot or find the avatar for this identity in that world, and knock.
|
|
60
59
|
join: (identity: string, invitation: Invitation, ward: string) => Promise<{ error?: string }>;
|
|
61
|
-
// Where the worlds' pages are, for the one arrival here that is a human:
|
|
62
|
-
// a redirect from somewhere a world sent her. Without it she still gets a
|
|
63
|
-
// page, with no way back offered on it.
|
|
64
|
-
web?: string;
|
|
65
60
|
};
|
|
66
61
|
|
|
67
62
|
const word = (s: unknown) => (typeof s === 'string' && /^[\w.-]{1,40}$/.test(s) ? s : null);
|
|
@@ -69,28 +64,6 @@ const word = (s: unknown) => (typeof s === 'string' && /^[\w.-]{1,40}$/.test(s)
|
|
|
69
64
|
export function apiRoute(harbor: Held, o: Options): Handler {
|
|
70
65
|
return async (req, rest) => {
|
|
71
66
|
const json = (status: number, body: unknown) => Response.json(body, { status, headers: open });
|
|
72
|
-
// The one page this road serves, and it wears the one stylesheet every
|
|
73
|
-
// screen wears. No cross-origin header on it: a page is for the browser
|
|
74
|
-
// that was sent here and never for a script somewhere else. Its policy
|
|
75
|
-
// is the allow page's, and for the same reason: a page that runs no
|
|
76
|
-
// script of its own should be allowed none.
|
|
77
|
-
const page = (status: number, body: string) =>
|
|
78
|
-
new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>nervur</title>\n<style>${CSS}</style>\n</head><body>${body}</body></html>`, {
|
|
79
|
-
status,
|
|
80
|
-
headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': POLICY, 'referrer-policy': 'no-referrer' },
|
|
81
|
-
});
|
|
82
|
-
// Where this world lives, as the daemon was told and not as the request
|
|
83
|
-
// said: a Host header is the caller's to write, and a page that printed
|
|
84
|
-
// one would print whatever a caller wanted a human to read. Falling back
|
|
85
|
-
// to it is for a box configured with no web address at all, where the
|
|
86
|
-
// request is the only thing that knows.
|
|
87
|
-
const hostOf = (r: Request) => {
|
|
88
|
-
try {
|
|
89
|
-
return o.web === undefined ? (r.headers.get('host') ?? '') : new URL(o.web).host;
|
|
90
|
-
} catch {
|
|
91
|
-
return r.headers.get('host') ?? '';
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
67
|
if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: open });
|
|
95
68
|
if (req.method !== 'GET' && req.method !== 'POST') return json(405, { error: 'GET or POST' });
|
|
96
69
|
const body = async (): Promise<JsonObject | null> => {
|
|
@@ -119,7 +92,7 @@ export function apiRoute(harbor: Held, o: Options): Handler {
|
|
|
119
92
|
if (!world) return json(400, { error: 'that invitation is for a world that does not live here' });
|
|
120
93
|
const [ward, hosted] = world;
|
|
121
94
|
const identity = word(b.identity);
|
|
122
|
-
if (
|
|
95
|
+
if (!isIdentity(hosted, identity)) return json(400, { error: 'an identity is one word, and not a being of the ward' });
|
|
123
96
|
// Nobody at a page vouches for a program, so a trade is a knock that
|
|
124
97
|
// spends: an identity already in refreshes, or is removed at the user
|
|
125
98
|
// being and invited again, and never re-enters on a spent invitation.
|
|
@@ -135,48 +108,7 @@ export function apiRoute(harbor: Held, o: Options): Handler {
|
|
|
135
108
|
const [, named = '', ask = '', ...more] = rest.split('/');
|
|
136
109
|
const ward = named === '' ? harbor.here() : named;
|
|
137
110
|
|
|
138
|
-
|
|
139
|
-
// Not a stranger's ask and not an occupant's: the owner asking her
|
|
140
|
-
// `deliver` with the raw bytes and the headers, and nothing else. The
|
|
141
|
-
// body is read as a string and never parsed here, since the vendor
|
|
142
|
-
// signed the bytes and a reparsed body fails her check; no bearer is
|
|
143
|
-
// read, since the rung is not the caller's to prove. She answers `{ ok }`
|
|
144
|
-
// or her error object, and the route carries it.
|
|
145
|
-
// A GET on the same path is the same ask with an empty body: where an
|
|
146
|
-
// OAuth redirect lands, the code in the query. The query crosses inside
|
|
147
|
-
// the headers under `:query`, a name no header on the wire can have, so
|
|
148
|
-
// her one ask in stays one.
|
|
149
|
-
if (more.length === 1 && more[0] === 'deliver') {
|
|
150
|
-
if (req.method !== 'POST' && req.method !== 'GET') return json(405, { error: 'POST, or GET for a redirect' });
|
|
151
|
-
const hosted = harbor.wards.get(ward);
|
|
152
|
-
if (!hosted || !/^[\w-]+$/.test(ask)) return json(404, { error: 'no such world' });
|
|
153
|
-
const raw = req.method === 'GET' ? '' : await readText(req);
|
|
154
|
-
if (raw === undefined) return json(413, { error: 'too big' });
|
|
155
|
-
const headers: Record<string, string> = {};
|
|
156
|
-
req.headers.forEach((v, k) => (headers[k] = v));
|
|
157
|
-
const query = new URL(req.url).search.slice(1);
|
|
158
|
-
if (query) headers[':query'] = query;
|
|
159
|
-
const out = await hosted.ask('ask', { being: ask, method: 'deliver', args: { body: raw, headers } });
|
|
160
|
-
await hosted.save();
|
|
161
|
-
// A GET here is a browser and a browser is a human: a vendor a world
|
|
162
|
-
// sent her to has opened this URL in her tab, so what she is looking
|
|
163
|
-
// at is whatever the envoy answered a machine. She gets a page
|
|
164
|
-
// instead, and only she does; a vendor asking for JSON is answered in
|
|
165
|
-
// JSON as it always was, and so is a GET that says nothing about what
|
|
166
|
-
// it wants.
|
|
167
|
-
const wants = req.method === 'GET' && (req.headers.get('accept') ?? '').includes('text/html');
|
|
168
|
-
if (wants) {
|
|
169
|
-
const ok = out !== null && typeof out === 'object' && !Array.isArray(out) && typeof (out as { error?: unknown }).error !== 'string';
|
|
170
|
-
return page(ok ? 200 : 400, landed({ world: ward, host: hostOf(req), ...(o.web ? { at: `${o.web.replace(/\/$/, '')}/${encodeURIComponent(ward)}` } : {}), ok }));
|
|
171
|
-
}
|
|
172
|
-
if (isSilence(out) || out === null || typeof out !== 'object' || Array.isArray(out)) return json(503, { error: 'silence' });
|
|
173
|
-
const said = (out as { error?: unknown }).error;
|
|
174
|
-
if (said === 'no such being' || said === 'unknown ask') return json(404, { error: 'no such envoy' });
|
|
175
|
-
if (said === 'silence' || said === 'threw' || said === 'late') return json(503, { error: 'silence' });
|
|
176
|
-
if (typeof said === 'string') return json(400, out);
|
|
177
|
-
return json(200, out);
|
|
178
|
-
}
|
|
179
|
-
if (!/^[\w.-]+$/.test(ward) || more.length > 0 || (ask !== '' && !/^[\w-]+$/.test(ask))) return json(404, { error: 'GET /api for the device’s ward, GET /api/<ward> for the describe, GET or POST /api/<ward>/<ask> for one ask, POST /api/<ward>/<being>/deliver for a delivery' });
|
|
111
|
+
if (!/^[\w.-]+$/.test(ward) || more.length > 0 || (ask !== '' && !/^[\w-]+$/.test(ask))) return json(404, { error: 'GET /api for the device’s ward, GET /api/<ward> for the describe, GET or POST /api/<ward>/<ask> for one ask' });
|
|
180
112
|
const hosted = harbor.wards.get(ward);
|
|
181
113
|
if (!hosted) return json(404, { error: 'no such world' });
|
|
182
114
|
let args: JsonObject;
|
package/beings/avatar.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// calls the four methods below: one of the two ways in, `enter` behind a
|
|
8
8
|
// route's proof or `join` with an invitation, then `tools` and `call`.
|
|
9
9
|
// Nothing of a side is in her cells.
|
|
10
|
-
import { isSilence, isWord, wordOf } from 'nervur';
|
|
11
|
-
import type { Answer, Blueprint, Invitation, JsonObject, OccupantRecord, Wanted } from 'nervur';
|
|
12
|
-
import {
|
|
10
|
+
import { Being, isSilence, isWord, wordOf } from 'nervur';
|
|
11
|
+
import type { Answer, Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Wanted } from 'nervur';
|
|
12
|
+
import { carry } from './carry.ts';
|
|
13
13
|
|
|
14
14
|
// Her one standing, and the id under which the user being pushes to her.
|
|
15
15
|
// Two ids for one far being, because standings and occupants share one
|
|
@@ -17,15 +17,19 @@ import { Carrier } from './carry.ts';
|
|
|
17
17
|
export const USER = 'user';
|
|
18
18
|
export const PUSHER = 'user:push';
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
export class Avatar extends
|
|
20
|
+
// She carries for nobody, until a subclass says who: the presence being
|
|
21
|
+
// carries the box's faculties to her user being, and a bare avatar holds
|
|
22
|
+
// one standing and shows nothing of it.
|
|
23
|
+
export class Avatar extends Being {
|
|
24
24
|
static override cells = { inbox: [] as JsonObject[] };
|
|
25
25
|
static override asks = {
|
|
26
26
|
notify: { description: 'a push from the user being', input: { type: 'object' }, for: (occ: OccupantRecord | undefined) => occ?.id === PUSHER },
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
override answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
|
|
30
|
+
return carry(this, asker, method, args, (a, m, g) => super.answer(a, m, g));
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
// Her sides' ears. One identity is one avatar for life, and two sessions
|
|
30
34
|
// of that identity are two sides on her at once, so a push goes to every
|
|
31
35
|
// ear present; each side adds its own on open and removes it on close.
|
package/beings/carry.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Carrying: a being shows the asks of the standings she holds as asks of
|
|
3
|
+
// her own, and forwards. Whoever she carries for sees, in one describe,
|
|
4
4
|
// what she can be asked and what every being she holds can be asked, each
|
|
5
5
|
// under that standing's name; asking one is an ask on that standing, in her
|
|
6
6
|
// name, so the far being sees her and never who is behind her. A user being
|
|
7
|
-
// carries
|
|
8
|
-
//
|
|
7
|
+
// carries her org for the human's devices; a presence being carries her
|
|
8
|
+
// device's faculties to her world; the Dock carries the box's faculties to
|
|
9
|
+
// a screen. Nothing here knows what she is.
|
|
10
|
+
//
|
|
11
|
+
// It is composed and never extended: carrying is something a being does,
|
|
12
|
+
// not what she is, so a being on any rung of the kit's ladder carries by
|
|
13
|
+
// answering through `carry` and saying her rules as statics of her class.
|
|
14
|
+
// The four rules have a default each, and a class says only the ones she
|
|
15
|
+
// means to change.
|
|
9
16
|
//
|
|
10
17
|
// A carried ask is `<id>-<name>`, and the mapping back is kept in her cells
|
|
11
18
|
// and never parsed from the name, so an id with a dash in it is no trouble.
|
|
@@ -18,13 +25,49 @@
|
|
|
18
25
|
// `look` once per digest, and keeps both in her cells. So a describe of
|
|
19
26
|
// hers may cost one ask per stale standing, which is the price of a page
|
|
20
27
|
// that is never more than one ask behind.
|
|
21
|
-
import {
|
|
22
|
-
import type { Asker, Blueprint, JsonObject, OccupantRecord, Reply } from 'nervur';
|
|
28
|
+
import { isSilence, isWord, wordOf } from 'nervur';
|
|
29
|
+
import type { Asker, Being, Blueprint, JsonObject, OccupantRecord, Reply } from 'nervur';
|
|
23
30
|
import { sanitise, type Look } from './look.ts';
|
|
24
31
|
|
|
25
32
|
type Carried = Record<string, { id: string; ask: string }>;
|
|
26
33
|
type Looks = Record<string, { digest: string | null; look: Look }>;
|
|
27
34
|
|
|
35
|
+
// The rules a carrying class declares as statics.
|
|
36
|
+
export type Carrying = {
|
|
37
|
+
// Who sees what she carries. Nobody, until the class says who.
|
|
38
|
+
carries(occupant: OccupantRecord | undefined, asker: Asker): boolean;
|
|
39
|
+
// Standings that are plumbing: their own asks are never carried, and
|
|
40
|
+
// what they carry is. A way back to a device is one, by the dock's own
|
|
41
|
+
// convention, so a bare avatar's inbox never shows on her and a presence
|
|
42
|
+
// being's faculties do, under `to:<id>-<name>`, which is how home carries
|
|
43
|
+
// her phone. A class may name more.
|
|
44
|
+
hidden(id: string): boolean;
|
|
45
|
+
// Standings she never reads for carrying at all: a presence being's
|
|
46
|
+
// standing at her own user being is one, since carrying it would carry
|
|
47
|
+
// the user being's carrying of her, and round.
|
|
48
|
+
skips(id: string): boolean;
|
|
49
|
+
// Which of the standings she carries this asker may see. Every one,
|
|
50
|
+
// until the class keeps some for fewer: a user being shows her own door
|
|
51
|
+
// to a device that may mint and to nobody else.
|
|
52
|
+
shows(id: string, occupant: OccupantRecord | undefined, asker: Asker): boolean;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const CARRIES: Carrying = {
|
|
56
|
+
carries: () => false,
|
|
57
|
+
hidden: (id) => id.startsWith('to:'),
|
|
58
|
+
skips: () => false,
|
|
59
|
+
shows: () => true,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Her rules, read off her class; one she left unsaid is the default.
|
|
63
|
+
function rulesOf(self: Being): Carrying {
|
|
64
|
+
const C = self.constructor as Partial<Carrying>;
|
|
65
|
+
return { carries: C.carries ?? CARRIES.carries, hidden: C.hidden ?? CARRIES.hidden, skips: C.skips ?? CARRIES.skips, shows: C.shows ?? CARRIES.shows };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const carriedOf = (self: Being): Carried => ((self.cells.carried as Carried | undefined) ??= {});
|
|
69
|
+
const looksOf = (self: Being): Looks => ((self.cells.looks as Looks | undefined) ??= {});
|
|
70
|
+
|
|
28
71
|
// The names a far being carries, read off her notes: every ask her
|
|
29
72
|
// `standings` groups name, and each group's page. Empty when she carries
|
|
30
73
|
// nothing, which a bare avatar does not.
|
|
@@ -40,120 +83,90 @@ function carriedBy(bp: Blueprint): Set<string> {
|
|
|
40
83
|
return out;
|
|
41
84
|
}
|
|
42
85
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// answered, a device came back or a relation was dropped and taken again:
|
|
90
|
-
// history, and no fact about her. Carried into her blueprint in that
|
|
91
|
-
// order, her shape would come out differently on two runs that made her
|
|
92
|
-
// the same way, and her digest with it. The digest is how everyone
|
|
93
|
-
// holding a standing on her learns she changed, so a shape that follows
|
|
94
|
-
// history says she changed when nothing about her did, and a watcher
|
|
95
|
-
// refreshes or walks away over the order two lends came back in.
|
|
96
|
-
for (const [id, rec] of [...Object.entries(this.cells.standings)].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
97
|
-
if (C.skips(id)) continue;
|
|
98
|
-
const st = this.standings[id];
|
|
99
|
-
if (!st) continue;
|
|
100
|
-
if (!rec.blueprint || rec.seen !== rec.digest) await st.ask(); // her ward writes the record
|
|
101
|
-
if (!rec.blueprint) continue;
|
|
102
|
-
// plumbing carries what it carries and nothing of its own: the names
|
|
103
|
-
// the far being's notes group under her standings, or none
|
|
104
|
-
const through = C.hidden(id) ? carriedBy(rec.blueprint) : null;
|
|
105
|
-
if (through && through.size === 0) continue;
|
|
106
|
-
// The mapping is written for every standing she carries, whoever is
|
|
107
|
-
// asking: it is one map in her cells, and the last describe must not
|
|
108
|
-
// decide what the next asker may call. Who sees a standing is decided
|
|
109
|
-
// here and again at answer, by `shows`.
|
|
110
|
-
const shown = C.shows(id, occ, asker);
|
|
111
|
-
// her look is kept here once per digest and never carried as an ask;
|
|
112
|
-
// her page follows every call, so it is carried as one and named in
|
|
113
|
-
// the notes, where every side finds it and none takes it for a form
|
|
114
|
-
const names: string[] = [];
|
|
115
|
-
const group: { asks: string[]; look?: Look; page?: string } = { asks: names };
|
|
116
|
-
for (const a of rec.blueprint.asks) {
|
|
117
|
-
if (a.name === 'look') continue;
|
|
118
|
-
if (through && !through.has(a.name)) continue;
|
|
119
|
-
const name = `${id}-${a.name}`;
|
|
120
|
-
if (own.has(name)) continue;
|
|
121
|
-
carried[name] = { id, ask: a.name };
|
|
122
|
-
if (!shown) continue;
|
|
123
|
-
bp.asks.push({ ...a, name });
|
|
124
|
-
if (a.name === 'page') group.page = name;
|
|
125
|
-
else names.push(name);
|
|
126
|
-
}
|
|
86
|
+
// Her describe with her standings carried into it, for an asker who may
|
|
87
|
+
// see them. Every other asker gets her describe alone.
|
|
88
|
+
export async function blueprint(self: Being, asker: Asker): Promise<Blueprint> {
|
|
89
|
+
const rules = rulesOf(self);
|
|
90
|
+
const bp = self.describe(asker);
|
|
91
|
+
const occ = self.occupant(asker);
|
|
92
|
+
if (!rules.carries(occ, asker)) return bp;
|
|
93
|
+
const own = new Set(bp.asks.map((a) => a.name));
|
|
94
|
+
const carried: Carried = {};
|
|
95
|
+
const groups: Record<string, { asks: string[]; look?: Look }> = {};
|
|
96
|
+
const looks = looksOf(self);
|
|
97
|
+
// By id, and never in the order she happened to take them. Her cells keep
|
|
98
|
+
// standings in the order they were taken, which is the order a lend
|
|
99
|
+
// answered, a device came back or a relation was dropped and taken again:
|
|
100
|
+
// history, and no fact about her. Carried into her blueprint in that
|
|
101
|
+
// order, her shape would come out differently on two runs that made her
|
|
102
|
+
// the same way, and her digest with it. The digest is how everyone
|
|
103
|
+
// holding a standing on her learns she changed, so a shape that follows
|
|
104
|
+
// history says she changed when nothing about her did, and a watcher
|
|
105
|
+
// refreshes or walks away over the order two lends came back in.
|
|
106
|
+
for (const [id, rec] of [...Object.entries(self.cells.standings)].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
107
|
+
if (rules.skips(id)) continue;
|
|
108
|
+
const st = self.standings[id];
|
|
109
|
+
if (!st) continue;
|
|
110
|
+
if (!rec.blueprint || rec.seen !== rec.digest) await st.ask(); // her ward writes the record
|
|
111
|
+
if (!rec.blueprint) continue;
|
|
112
|
+
// plumbing carries what it carries and nothing of its own: the names
|
|
113
|
+
// the far being's notes group under her standings, or none
|
|
114
|
+
const through = rules.hidden(id) ? carriedBy(rec.blueprint) : null;
|
|
115
|
+
if (through && through.size === 0) continue;
|
|
116
|
+
// The mapping is written for every standing she carries, whoever is
|
|
117
|
+
// asking: it is one map in her cells, and the last describe must not
|
|
118
|
+
// decide what the next asker may call. Who sees a standing is decided
|
|
119
|
+
// here and again at answer, by `shows`.
|
|
120
|
+
const shown = rules.shows(id, occ, asker);
|
|
121
|
+
// her look is kept here once per digest and never carried as an ask;
|
|
122
|
+
// her page follows every call, so it is carried as one and named in
|
|
123
|
+
// the notes, where every side finds it and none takes it for a form
|
|
124
|
+
const names: string[] = [];
|
|
125
|
+
const group: { asks: string[]; look?: Look; page?: string } = { asks: names };
|
|
126
|
+
for (const a of rec.blueprint.asks) {
|
|
127
|
+
if (a.name === 'look') continue;
|
|
128
|
+
if (through && !through.has(a.name)) continue;
|
|
129
|
+
const name = `${id}-${a.name}`;
|
|
130
|
+
if (own.has(name)) continue;
|
|
131
|
+
carried[name] = { id, ask: a.name };
|
|
127
132
|
if (!shown) continue;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
bp.asks.push({ ...a, name });
|
|
134
|
+
if (a.name === 'page') group.page = name;
|
|
135
|
+
else names.push(name);
|
|
136
|
+
}
|
|
137
|
+
if (!shown) continue;
|
|
138
|
+
if (rec.blueprint.asks.some((a) => a.name === 'look')) {
|
|
139
|
+
const kept = looks[id];
|
|
140
|
+
if (!kept || kept.digest !== rec.digest) {
|
|
141
|
+
const l = await st.ask('look');
|
|
142
|
+
looks[id] = { digest: rec.digest, look: isSilence(l) || isWord(l) ? {} : sanitise(l) };
|
|
135
143
|
}
|
|
136
|
-
|
|
144
|
+
group.look = looks[id].look;
|
|
137
145
|
}
|
|
138
|
-
|
|
139
|
-
this.cells.carried = carried;
|
|
140
|
-
const notes = bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) ? bp.notes : {};
|
|
141
|
-
return { asks: bp.asks, notes: { ...notes, standings: groups as unknown as JsonObject } };
|
|
146
|
+
groups[id] = group;
|
|
142
147
|
}
|
|
148
|
+
for (const id of Object.keys(looks)) if (!(id in groups)) delete looks[id];
|
|
149
|
+
self.cells.carried = carried;
|
|
150
|
+
const notes = bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) ? bp.notes : {};
|
|
151
|
+
return { asks: bp.asks, notes: { ...notes, standings: groups as unknown as JsonObject } };
|
|
152
|
+
}
|
|
143
153
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
// Her answer, carrying: the empty ask is her carried blueprint, a carried
|
|
155
|
+
// name is an ask on that standing in her name, and everything else is her
|
|
156
|
+
// own, which the class hands in as `own`.
|
|
157
|
+
export async function carry(self: Being, asker: Asker, method: string | undefined, args: JsonObject, own: (asker: Asker, method: string, args: JsonObject) => Promise<Reply>): Promise<Reply> {
|
|
158
|
+
if (method === undefined) return blueprint(self, asker);
|
|
159
|
+
const rules = rulesOf(self);
|
|
160
|
+
const carried = carriedOf(self);
|
|
161
|
+
const to = Object.hasOwn(carried, method) ? carried[method] : undefined;
|
|
162
|
+
const occ = self.occupant(asker);
|
|
163
|
+
if (to && rules.carries(occ, asker) && rules.shows(to.id, occ, asker)) {
|
|
164
|
+
const st = self.standings[to.id];
|
|
165
|
+
if (!st) return { error: 'unknown ask' };
|
|
166
|
+
const out = await st.ask(to.ask, args);
|
|
167
|
+
if (isSilence(out)) return out;
|
|
168
|
+
if (isWord(out)) return { error: wordOf(out) };
|
|
169
|
+
return out;
|
|
158
170
|
}
|
|
171
|
+
return own(asker, method, args);
|
|
159
172
|
}
|
package/beings/clock.ts
CHANGED
|
@@ -31,7 +31,11 @@
|
|
|
31
31
|
// process the way every cell does, and is gone when it is dropped.
|
|
32
32
|
import { Being, isSilence, isWord, wordOf, OWNER } from 'nervur';
|
|
33
33
|
import type { Asker, Invitation, Json, JsonObject, OccupantRecord, Stance } from 'nervur';
|
|
34
|
-
import { TIMER
|
|
34
|
+
import { TIMER as TIMING } from '../box/timer.ts';
|
|
35
|
+
import { join } from '../box/join.ts';
|
|
36
|
+
|
|
37
|
+
// Her standing at the box's timer, under an id of her own.
|
|
38
|
+
const TIMER = 'timer';
|
|
35
39
|
|
|
36
40
|
// The root and nobody else. `OWNER` is one of the ward's own words, refused at
|
|
37
41
|
// every mint, so no occupant can ever wear it: an ask gated this way is the
|
|
@@ -311,7 +315,7 @@ export class Clock extends Being {
|
|
|
311
315
|
readonly joined: Promise<string | null>;
|
|
312
316
|
constructor(stance: Stance) {
|
|
313
317
|
super(stance);
|
|
314
|
-
this.joined = join(this, TIMER, { method: 'tick' });
|
|
318
|
+
this.joined = join(this, TIMING, TIMER, { method: 'tick' });
|
|
315
319
|
}
|
|
316
320
|
|
|
317
321
|
static override asks = {
|
|
@@ -377,15 +381,15 @@ export class Clock extends Being {
|
|
|
377
381
|
// it keeps them, for the same reason the base does.
|
|
378
382
|
protected async told(): Promise<void> {
|
|
379
383
|
if (!Object.hasOwn(this.cells.standings, TIMER)) return;
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
const now =
|
|
384
|
+
const hers = this.standings[TIMER];
|
|
385
|
+
const heard = await hers?.ask('now', {});
|
|
386
|
+
const now = heard !== null && typeof heard === 'object' ? (heard as { now?: unknown }).now : undefined;
|
|
383
387
|
if (typeof now !== 'number' || !Number.isFinite(now)) return;
|
|
384
388
|
// A schedule due at this very moment is a round now, not a round at the
|
|
385
389
|
// next moment after it: what she was handed is a minute already begun,
|
|
386
390
|
// and the schedule kept inside it names it.
|
|
387
391
|
const at = this.schedules().some((s) => this.#ready(s, now)) ? now : this.#soonest(floor(now));
|
|
388
|
-
if (at !== null) await
|
|
392
|
+
if (at !== null) await hers?.ask('due', { at });
|
|
389
393
|
}
|
|
390
394
|
|
|
391
395
|
// One list, her own cell and nothing else.
|
|
@@ -536,7 +540,7 @@ export const CLOCK = 'clock';
|
|
|
536
540
|
// because it is the same act: the owner creates and places, and the work is
|
|
537
541
|
// hers from then on.
|
|
538
542
|
export async function setupClock(hosted: Owner, wakes: string[] = []): Promise<void> {
|
|
539
|
-
const out = (await hosted.ask('boot', { key: CLOCK, class: 'Clock' })) as { error?: string };
|
|
543
|
+
const out = (await hosted.ask('boot', { key: CLOCK, class: 'beings/Clock' })) as { error?: string };
|
|
540
544
|
if (out.error) throw new Error(`clock: ${out.error}`);
|
|
541
545
|
for (const being of wakes) await placeClock(hosted, being);
|
|
542
546
|
}
|
package/beings/desk.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// way in turns a proof of one kind into who it is for: which user, which
|
|
9
9
|
// client identity. The ways in are beings, and the desk reaches each of
|
|
10
10
|
// them as she reaches anything, at a standing under `way:<kind>`: the
|
|
11
|
-
// world's own, placed by the root, and the box's, lent by the
|
|
11
|
+
// world's own, placed by the root, and the box's, lent by the box ward and
|
|
12
12
|
// taken at her birth, so a box with no local side offers no such kind.
|
|
13
13
|
//
|
|
14
14
|
// A kind whose proof the world has to hand out first answers `send` beside
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// place, so a side that renders her renders every way into this world.
|
|
19
19
|
import { Being, isSilence, isWord, wordOf } from 'nervur';
|
|
20
20
|
import type { Asker, Blueprint, JsonObject, Reply, Stance } from 'nervur';
|
|
21
|
-
import { join } from '../
|
|
22
|
-
import { LOCAL } from '../
|
|
21
|
+
import { join } from '../box/join.ts';
|
|
22
|
+
import { LOCAL } from '../box/local.ts';
|
|
23
23
|
import { WAY, kindOf, verified } from './way.ts';
|
|
24
24
|
|
|
25
25
|
export class Desk extends Being {
|
|
@@ -34,7 +34,7 @@ export class Desk extends Being {
|
|
|
34
34
|
// nothing under that id and the kind is not offered.
|
|
35
35
|
constructor(stance: Stance) {
|
|
36
36
|
super(stance);
|
|
37
|
-
void join(this, LOCAL,
|
|
37
|
+
void join(this, LOCAL, WAY('local'));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// The ids of the ways in she holds, in the order they were placed.
|
|
@@ -114,8 +114,9 @@ export class Desk extends Being {
|
|
|
114
114
|
// key. A way in that does not say `make` reaches an existing household
|
|
115
115
|
// and nothing else, which is what the local kind on a device does.
|
|
116
116
|
if (!this.standings[id] && (who.make === true || typeof who.make === 'string')) {
|
|
117
|
-
|
|
118
|
-
if ((
|
|
117
|
+
// a key the ward already holds, her own included, is refused by the boot
|
|
118
|
+
if (!/^[\w.-]{1,80}$/.test(who.user)) return { error: 'a user is a word' };
|
|
119
|
+
if ((await this.boot(typeof who.make === 'string' ? who.make : 'beings/User', who.user, id)) === null) return { error: 'no such user' };
|
|
119
120
|
await this.standings[id]!.ask('hello', { name: who.user });
|
|
120
121
|
}
|
|
121
122
|
const user = this.standings[id];
|