@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
package/beings/setup.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The root's setup of a fresh ward, as every terrain does it: the front desk
|
|
3
|
+
// booted public, one user being, the desk holding a standing to her under
|
|
4
|
+
// `user:<name>`, placed by the root's knock, and she holding her own ward's
|
|
5
|
+
// door as an owner under `home`. She names the desk `desk`,
|
|
6
|
+
// and that id is the only thing that makes it the desk. A disk harbor does
|
|
7
|
+
// this at `quo init`, the edge at its root route's init; the memory
|
|
8
|
+
// harbor's test world does the same by hand.
|
|
9
|
+
import type { Invitation } from '@nervur-org/nervur';
|
|
10
|
+
import type { Hosted } from '@nervur-org/nervur/harbor';
|
|
11
|
+
import { DESK, HOME } from './user.ts';
|
|
12
|
+
|
|
13
|
+
// The root boots and steps back: it invites an owner on the ward's own pk
|
|
14
|
+
// and the user being knocks for it, so from then on placing a being,
|
|
15
|
+
// minting a way in or removing one is an ask on her, shown by her gate to a
|
|
16
|
+
// device that may mint, and the box is not logged into for it. The same
|
|
17
|
+
// standing a user being of another ward takes to own this one; here it is
|
|
18
|
+
// her own.
|
|
19
|
+
export async function setupHome(hosted: Hosted, user: string): Promise<void> {
|
|
20
|
+
const inv = (await hosted.ask('invite', { being: hosted.pk, id: HOME })) as Invitation | { error?: string };
|
|
21
|
+
if ('error' in inv) throw new Error(`home: ${inv.error}`);
|
|
22
|
+
const placed = (await hosted.ask('knock', { being: user, id: HOME, invitation: inv })) as { taken?: string; error?: string };
|
|
23
|
+
if (placed.taken !== HOME) throw new Error(`home: ${placed.error ?? 'she could not take her own door'}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The ward's home being is a `User` unless the device says which class: an
|
|
27
|
+
// organisation's ward has an org's home being, of the estate's own class,
|
|
28
|
+
// and everything else of setup, the desk and the door, is placed on her the
|
|
29
|
+
// same way.
|
|
30
|
+
export async function setup(hosted: Hosted, user: string, cls = 'User'): Promise<void> {
|
|
31
|
+
if (!/^[\w.-]+$/.test(user) || user === 'desk') throw new Error(`a user is a word, and not desk: ${user}`);
|
|
32
|
+
const boot = async (args: Record<string, unknown>) => {
|
|
33
|
+
const out = (await hosted.ask('boot', args)) as { booted?: string; error?: string };
|
|
34
|
+
if (out.error) throw new Error(`init: ${out.error}`);
|
|
35
|
+
};
|
|
36
|
+
await boot({ key: 'desk', class: 'Desk' });
|
|
37
|
+
const pub = (await hosted.ask('public', { key: 'desk' })) as { error?: string };
|
|
38
|
+
if (pub.error) throw new Error(`init: ${pub.error}`);
|
|
39
|
+
await boot({ key: user, class: cls });
|
|
40
|
+
const inv = (await hosted.ask('invite', { being: user, id: DESK })) as Invitation;
|
|
41
|
+
// the desk's first hello names her, since a being does not know her own key
|
|
42
|
+
const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello', args: { name: user } })) as { taken?: string };
|
|
43
|
+
if (placed.taken !== `user:${user}`) throw new Error('init: the desk could not reach the user being');
|
|
44
|
+
await setupHome(hosted, user);
|
|
45
|
+
}
|
package/beings/side.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The side: the one interface an avatar speaks through to someone outside
|
|
3
|
+
// Quo. A side takes the avatar object, in-process, and renders three things
|
|
4
|
+
// its own way: her describe as what that someone reads, a call as an ask on
|
|
5
|
+
// her one standing, and a push as a notification. Sides in this repository:
|
|
6
|
+
// a screen, an MCP server, a runner, an event-driven agent. None knows
|
|
7
|
+
// another exists, and none of this file names a platform.
|
|
8
|
+
import { isSilence, isUnreached, isWord, wordOf } from '@nervur-org/nervur';
|
|
9
|
+
import type { Answer, Blueprint, Json, JsonObject, Wanted, WordName } from '@nervur-org/nervur';
|
|
10
|
+
import type { Avatar } from './avatar.ts';
|
|
11
|
+
|
|
12
|
+
// A side, running: it holds the avatar until closed.
|
|
13
|
+
export type Serving = { close(): Promise<void> };
|
|
14
|
+
export type Side = (avatar: Avatar) => Promise<Serving>;
|
|
15
|
+
|
|
16
|
+
// What a side speaks for: the empty ask and a named one, and the ears a
|
|
17
|
+
// push reaches. An avatar on her one standing is one; the owner's asks on a
|
|
18
|
+
// ward are another; a guest at a world's public being is a third.
|
|
19
|
+
export type Subject = {
|
|
20
|
+
tools(): Promise<Blueprint | { error: string }>;
|
|
21
|
+
call(name: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
22
|
+
ears: Set<(object: JsonObject) => void>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// The three words for "no object", and the object itself, as every side must
|
|
26
|
+
// tell them apart before rendering. An error object is an ordinary answer
|
|
27
|
+
// that the side may mark; silence and unreached are not objects at all.
|
|
28
|
+
export type Word =
|
|
29
|
+
| { word: 'object'; value: Json }
|
|
30
|
+
| { word: 'error'; value: JsonObject }
|
|
31
|
+
| { word: 'silence' }
|
|
32
|
+
| { word: 'unreached' }
|
|
33
|
+
| { word: 'quo'; name: WordName }; // the ward's word, named: why no object came
|
|
34
|
+
|
|
35
|
+
export function word(answer: Answer): Word {
|
|
36
|
+
if (isSilence(answer)) return { word: 'silence' };
|
|
37
|
+
if (isUnreached(answer)) return { word: 'unreached' };
|
|
38
|
+
if (isWord(answer)) return { word: 'quo', name: wordOf(answer) };
|
|
39
|
+
if (answer !== null && typeof answer === 'object' && !Array.isArray(answer) && 'error' in answer) return { word: 'error', value: answer };
|
|
40
|
+
return { word: 'object', value: answer };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// What a side says about the two words that carry nothing, so that whoever
|
|
44
|
+
// reads them can decide: silence promises nothing, unreached promises that
|
|
45
|
+
// nothing was delivered.
|
|
46
|
+
export const SILENCE_TEXT = 'silence: the work may have happened, and no answer came back';
|
|
47
|
+
export const UNREACHED_TEXT = 'unreached: nothing was delivered; asking again is safe';
|
|
48
|
+
// And the ward's other words, each with what a side says of it.
|
|
49
|
+
export const WORD_TEXT: Record<WordName, string> = {
|
|
50
|
+
unreached: UNREACHED_TEXT,
|
|
51
|
+
late: 'late: the wait ran out; the work may have happened',
|
|
52
|
+
invitation: 'invitation: that is not an invitation',
|
|
53
|
+
dropped: 'dropped: the standing is gone',
|
|
54
|
+
removed: 'removed: the far being removed you',
|
|
55
|
+
absent: 'absent: the far being did not come back this run',
|
|
56
|
+
unannounced: 'unannounced: the knock announced no key of its own',
|
|
57
|
+
repeated: 'repeated: that number was already honoured',
|
|
58
|
+
threw: 'threw: the far being threw',
|
|
59
|
+
};
|
|
60
|
+
export const wordText = (name: WordName): string => WORD_TEXT[name];
|
package/beings/user.ts
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The user being. One per human, running wherever the human's world runs.
|
|
3
|
+
// She holds the human's standings, and everyone who acts for the human is
|
|
4
|
+
// her occupant, seeing what her gate shows. Two of her occupants are
|
|
5
|
+
// special only by the id the root chose for them: `desk`, the front desk,
|
|
6
|
+
// who may ask her to invite a device; and each device, whose id is the
|
|
7
|
+
// client identity the desk established. A device she let mint brings the
|
|
8
|
+
// next one in, so a human adds a device from a device already in and only
|
|
9
|
+
// the first one has to come from the root, on the box.
|
|
10
|
+
import { isInvitation, isSilence, isWord, wordOf } from '@nervur-org/nervur';
|
|
11
|
+
import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Stance } from '@nervur-org/nervur';
|
|
12
|
+
import { Carrier } from './carry.ts';
|
|
13
|
+
import { PUSH, banner, isToken, join, sealBanner } from '../dock/index.ts';
|
|
14
|
+
|
|
15
|
+
export const DESK = 'desk';
|
|
16
|
+
// The prefix her doorbell standings are filed under, one per device: the
|
|
17
|
+
// way she rings that device, and never a way anything else may ask on.
|
|
18
|
+
export const BELL = 'bell';
|
|
19
|
+
// Her standing at her own ward's door, as an owner: the root placed it at
|
|
20
|
+
// setup, and through it she boots, invites and removes in her own world.
|
|
21
|
+
export const HOME = 'home';
|
|
22
|
+
const isDesk = (occ: OccupantRecord | undefined) => occ?.id === DESK;
|
|
23
|
+
const isDevice = (occ: OccupantRecord | undefined) => occ !== undefined && occ.id !== DESK;
|
|
24
|
+
// A device is known by its client identity. The desk writes it as a note
|
|
25
|
+
// when it asks her to mint; the root, minting on her directly, writes
|
|
26
|
+
// nothing, and then the id the root chose is the identity.
|
|
27
|
+
const rootMinted = (occ: OccupantRecord | undefined) => isDevice(occ) && occ?.notes.client === undefined;
|
|
28
|
+
const client = (occ: OccupantRecord | undefined) => (typeof occ?.notes.client === 'string' ? occ.notes.client : rootMinted(occ) ? occ!.id : null);
|
|
29
|
+
|
|
30
|
+
// The three words that travel with a minting, each a note on the occupant
|
|
31
|
+
// minted: `wake` may ring her other devices, `reach` may see and ask what
|
|
32
|
+
// she holds, `mint` may bring the next device in. What a device holds is
|
|
33
|
+
// also the most it may write on a device of its own, so a chain of devices
|
|
34
|
+
// never gains a power along the way.
|
|
35
|
+
//
|
|
36
|
+
// The desk speaks the human's word at the exchange and holds all three. A
|
|
37
|
+
// device the root minted reaches and may wake without a note, since the
|
|
38
|
+
// root owns everything she holds and chose to mint it. `mint` is never
|
|
39
|
+
// implied, not even there: it admits somebody else, which is not the root's
|
|
40
|
+
// own reaching, and it is said out loud or not at all. The root says it by
|
|
41
|
+
// minting with `{ notes: { mint: true } }`, which is the ward's owner
|
|
42
|
+
// invite, so a harbor with no desk can still let a device beget devices.
|
|
43
|
+
const MINTINGS = ['wake', 'reach', 'mint', 'revoke'] as const;
|
|
44
|
+
// How many unreached pushes she keeps for one device. A device gone for a
|
|
45
|
+
// month is a device that will be told what is recent, not everything.
|
|
46
|
+
const OUTBOX = 32;
|
|
47
|
+
type Minting = (typeof MINTINGS)[number];
|
|
48
|
+
// Whether a ring left. A doorbell and the bell both say `rang`, and every
|
|
49
|
+
// other answer, a word, silence or an error object, is a device unreached.
|
|
50
|
+
const rang = (out: unknown): boolean => !isSilence(out) && !isWord(out) && out !== null && typeof out === 'object' && !Array.isArray(out) && (out as JsonObject).rang !== undefined && (out as JsonObject).rang !== false;
|
|
51
|
+
const held = (occ: OccupantRecord | undefined): Record<Minting, boolean> =>
|
|
52
|
+
isDesk(occ)
|
|
53
|
+
? { wake: true, reach: true, mint: true, revoke: true }
|
|
54
|
+
: { wake: occ?.notes.wake === true || rootMinted(occ), reach: occ?.notes.reach === true || rootMinted(occ), mint: occ?.notes.mint === true, revoke: occ?.notes.revoke === true };
|
|
55
|
+
const mayWake = (occ: OccupantRecord | undefined) => held(occ).wake;
|
|
56
|
+
// The desk mints for a client it vouched for; a device of hers mints the
|
|
57
|
+
// next one. Nobody else, and no ask on her lets an occupant grant itself
|
|
58
|
+
// a note it does not hold.
|
|
59
|
+
const mayMint = (occ: OccupantRecord | undefined) => isDesk(occ) || (isDevice(occ) && held(occ).mint);
|
|
60
|
+
|
|
61
|
+
// Revoking is its own word and never `mint`'s. A device that mints adds
|
|
62
|
+
// devices, which is noisy and stays visible; one that revokes can take the
|
|
63
|
+
// human's own devices out of her world, so a stolen phone that could do
|
|
64
|
+
// both would lock her out before she reached a laptop. It follows `mint`
|
|
65
|
+
// otherwise: never a default, held only where it was granted, and handed on
|
|
66
|
+
// by a device that holds it and no further. A world where nobody was given
|
|
67
|
+
// it has the act out of reach and the root's own removals instead, and that
|
|
68
|
+
// is a choice an org makes rather than one the dock makes for it.
|
|
69
|
+
const mayRevoke = (occ: OccupantRecord | undefined) => isDesk(occ) || (isDevice(occ) && held(occ).revoke);
|
|
70
|
+
|
|
71
|
+
// She carries her standings, acme, the calendar, the house, for a device the
|
|
72
|
+
// human allowed to reach them at the exchange: the note says so. A device
|
|
73
|
+
// without the note sees her own asks alone, and a model sees acme only
|
|
74
|
+
// because the human said it may. A device the root minted reaches: the root
|
|
75
|
+
// is the owner of everything she holds and chose to mint it.
|
|
76
|
+
export class User extends Carrier {
|
|
77
|
+
static override carries(occ: OccupantRecord | undefined): boolean {
|
|
78
|
+
return isDevice(occ) && held(occ).reach;
|
|
79
|
+
}
|
|
80
|
+
// `ways` holds a device's way back that could not be taken when it was
|
|
81
|
+
// handed over: the knock was unreached, a tab whose pk the far harbor had
|
|
82
|
+
// not bound yet, a phone in a tunnel. Nothing delivered spends nothing,
|
|
83
|
+
// so the invitation is tried again at the next push.
|
|
84
|
+
// The bell standing is plumbing and carries nothing: a device that may
|
|
85
|
+
// reach what she holds sees acme and the house, and never the envoy that
|
|
86
|
+
// wakes it. Her ways back are plumbing too, and a way back to a presence
|
|
87
|
+
// being carries that device's faculties, `to:phone-location-where`, which
|
|
88
|
+
// is how a model at home learns where the human is.
|
|
89
|
+
static override hidden(id: string): boolean {
|
|
90
|
+
return super.hidden(id) || id === PUSH || id.startsWith(`${BELL}:`);
|
|
91
|
+
}
|
|
92
|
+
// Her own door is carried to a device that may mint, and to nobody else:
|
|
93
|
+
// `mint` is the word for a device trusted to change who is in her world,
|
|
94
|
+
// and owning the world from that device is that trust said in full. A
|
|
95
|
+
// device that reaches sees acme and the house, and no `home-boot`.
|
|
96
|
+
static override shows(id: string, occ: OccupantRecord | undefined): boolean {
|
|
97
|
+
return id !== HOME || held(occ).mint;
|
|
98
|
+
}
|
|
99
|
+
static override cells = { name: '', reports: [] as JsonObject[], ways: {} as Record<string, Invitation>, outbox: {} as Record<string, JsonObject[]> };
|
|
100
|
+
|
|
101
|
+
// Her standing at the box's push faculty, taken again at every birth: a
|
|
102
|
+
// standing points at a being and not at a box, and a ward that moved is
|
|
103
|
+
// handed the new harbor's ground. It is how she rings a browser endpoint,
|
|
104
|
+
// which is the one kind of doorbell a world holds itself, since an
|
|
105
|
+
// endpoint answers only to the VAPID secret this world minted. Birth is
|
|
106
|
+
// synchronous and taking a standing is not, so the promise is kept and
|
|
107
|
+
// the ring waits on it.
|
|
108
|
+
//
|
|
109
|
+
// A box that lends no push rings no tabs, which is what a world with no
|
|
110
|
+
// web route behind it is.
|
|
111
|
+
readonly #push: Promise<string | null>;
|
|
112
|
+
constructor(stance: Stance) {
|
|
113
|
+
super(stance);
|
|
114
|
+
this.#push = join(this, PUSH);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A world whose own being she is, is a home: a device reaching her holds
|
|
118
|
+
// a standing at home and not a membership, and she says so in her notes,
|
|
119
|
+
// since nothing else in a describe tells a shell which of its worlds is
|
|
120
|
+
// the human's own. She knows it by the door she holds: the being setup
|
|
121
|
+
// knocked on the ward's own door as its owner is home's, and a member of
|
|
122
|
+
// the same class the desk made in an org's ward holds no door and says
|
|
123
|
+
// nothing.
|
|
124
|
+
override describe(asker: Asker): Blueprint {
|
|
125
|
+
const bp = super.describe(asker);
|
|
126
|
+
if (!this.standings[HOME]) return bp;
|
|
127
|
+
const notes = bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) ? bp.notes : {};
|
|
128
|
+
return { ...bp, notes: { ...notes, home: true } };
|
|
129
|
+
}
|
|
130
|
+
static override asks = {
|
|
131
|
+
hello: { description: 'say hello, and hand back an invitation so she can reach you', input: { type: 'object', properties: { invitation: { type: 'object' } } } },
|
|
132
|
+
whoami: { description: 'who she thinks you are', input: { type: 'object' }, for: isDevice },
|
|
133
|
+
device: { description: 'mint an invitation for another device', input: { type: 'object', properties: { client: { type: 'string' }, wake: { type: 'boolean' }, reach: { type: 'boolean' }, mint: { type: 'boolean' }, revoke: { type: 'boolean' } }, required: ['client'] }, for: mayMint },
|
|
134
|
+
push: { description: 'push an object to a device: wake it with an event', input: { type: 'object', properties: { client: { type: 'string' }, object: { type: 'object' } }, required: ['client', 'object'] }, for: mayWake },
|
|
135
|
+
chores: { description: 'what the agent may run', input: { type: 'object' }, for: (occ: OccupantRecord | undefined) => client(occ) === 'agent' },
|
|
136
|
+
look: { description: 'how she is shown', input: { type: 'object' }, for: isDevice },
|
|
137
|
+
page: { description: 'her page, as a tree of values', input: { type: 'object' }, for: isDevice },
|
|
138
|
+
name: { description: 'what she is called', input: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, for: rootMinted },
|
|
139
|
+
devices: { description: 'the devices in this world, and which one you are', input: { type: 'object' }, for: isDevice },
|
|
140
|
+
join: { description: 'join a world with an invitation: she knocks, hands back a way to reach her, and keeps the standing', input: { type: 'object', properties: { invitation: { type: 'object', description: 'the invitation that world minted' }, name: { type: 'string', description: 'what to call it here' } }, required: ['invitation', 'name'] }, for: mayMint },
|
|
141
|
+
forget: { description: 'revoke a device: drop its way in and her way back to it, in one act', input: { type: 'object', properties: { client: { type: 'string' } }, required: ['client'] }, for: mayRevoke },
|
|
142
|
+
report: { description: 'what a run of yours found', input: { type: 'object', properties: { event: { type: 'object' }, result: {} }, required: ['event', 'result'] }, for: isDevice },
|
|
143
|
+
bell: { description: 'how to wake this device: a standing at its own doorbell and the banner key it seals to, or a browser push endpoint', input: { type: 'object', properties: { invitation: { type: 'object' }, banner: { type: 'string' }, token: { type: 'object' } } }, for: isDevice },
|
|
144
|
+
missed: { description: 'what this device missed while it was unreachable', input: { type: 'object' }, for: isDevice },
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Anyone may say hello. A device that hands her an invitation in the args
|
|
148
|
+
// is taken as a standing under its own id, so she can push to it later;
|
|
149
|
+
// if it cannot be taken now, it is kept and tried at the next push. The
|
|
150
|
+
// desk's first hello names her: a being does not know her own key, and the
|
|
151
|
+
// root's setup knocks with the name it booted her under. Once named, she
|
|
152
|
+
// keeps it.
|
|
153
|
+
async hello(args: JsonObject, asker: Asker) {
|
|
154
|
+
if (asker.id !== undefined && args.invitation && typeof args.invitation === 'object' && !Array.isArray(args.invitation)) {
|
|
155
|
+
await this.wayBack(asker.id, args.invitation as Invitation);
|
|
156
|
+
}
|
|
157
|
+
if (asker.id === DESK && !this.cells.name && typeof args.name === 'string' && /^[\w.-]{1,80}$/.test(args.name)) this.cells.name = args.name;
|
|
158
|
+
return { welcome: asker.id ?? null, name: this.cells.name };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Take a device's way back, or keep it for later when the knock did not
|
|
162
|
+
// reach. A refusal spends the heir and is final; the way is dropped.
|
|
163
|
+
async wayBack(id: string, inv: Invitation): Promise<boolean> {
|
|
164
|
+
const ways = this.cells.ways as Record<string, Invitation>;
|
|
165
|
+
const back = await this.knock(inv);
|
|
166
|
+
if (!isSilence(back) && !isWord(back)) {
|
|
167
|
+
await this.take(`to:${id}`, inv);
|
|
168
|
+
delete ways[id];
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (isWord(back) && wordOf(back) !== 'unreached') delete ways[id];
|
|
172
|
+
else ways[id] = inv;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
whoami(_args: JsonObject, asker: Asker) {
|
|
177
|
+
return { id: asker.id ?? null, client: client(this.occupant(asker)) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The front desk asks, or a device of hers that may mint; she mints. The
|
|
181
|
+
// client identity goes into the occupant's notes, and that is what every
|
|
182
|
+
// gate reads. The three words travel with it, each asked for and each
|
|
183
|
+
// written only if the asker holds it: devices beget devices, and a device
|
|
184
|
+
// hands on no more than it was given.
|
|
185
|
+
async device(args: JsonObject, asker: Asker) {
|
|
186
|
+
const c = typeof args.client === 'string' ? args.client : null;
|
|
187
|
+
if (c === null) return { error: 'client is a string' };
|
|
188
|
+
const mine = held(this.occupant(asker));
|
|
189
|
+
const inv = await this.invite(c);
|
|
190
|
+
if (inv === null) return { error: 'id taken' };
|
|
191
|
+
const rec = this.cells.occupants[c];
|
|
192
|
+
if (rec) {
|
|
193
|
+
rec.notes.client = c;
|
|
194
|
+
for (const w of MINTINGS) if (args[w] === true && mine[w]) rec.notes[w] = true;
|
|
195
|
+
}
|
|
196
|
+
return inv as unknown as JsonObject;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Her way out and the device's way in are two relations, and Quo keeps
|
|
200
|
+
// them apart. She joins them herself, here, as her own rule: she does
|
|
201
|
+
// not reach a device she no longer admits. Without it a revoked device
|
|
202
|
+
// stops being able to ask her and keeps receiving everything she pushes.
|
|
203
|
+
async push(args: JsonObject) {
|
|
204
|
+
const c = typeof args.client === 'string' ? args.client : null;
|
|
205
|
+
if (c === null || !this.cells.occupants[c]) return { error: 'no such device, or it gave no way back' };
|
|
206
|
+
const ways = this.cells.ways as Record<string, Invitation>;
|
|
207
|
+
const object = (args.object as JsonObject) ?? {};
|
|
208
|
+
if (!this.standings[`to:${c}`] && ways[c]) await this.wayBack(c, ways[c]);
|
|
209
|
+
const st = this.standings[`to:${c}`];
|
|
210
|
+
// A way back she still could not take is a device that is not there:
|
|
211
|
+
// the knock itself was unreached, which is the same nothing delivered
|
|
212
|
+
// as an unreached ask, and it is rung like one.
|
|
213
|
+
if (!st) return ways[c] ? this.unreached(c, object) : { error: 'no such device, or it gave no way back' };
|
|
214
|
+
const out = await st.ask('notify', object);
|
|
215
|
+
if (isWord(out) && wordOf(out) === 'unreached') return this.unreached(c, object);
|
|
216
|
+
return isSilence(out) ? { error: 'silence' } : isWord(out) ? { error: wordOf(out) } : { pushed: out };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Nothing was delivered, so the device is asleep, closed or in a tunnel.
|
|
220
|
+
// The object waits in her outbox under that device, bounded and oldest
|
|
221
|
+
// out, since a device gone for a month must not grow without end; then
|
|
222
|
+
// she rings, and the pusher hears both facts. What the ring carries is
|
|
223
|
+
// the device's token and nothing of the object: the device wakes, dials,
|
|
224
|
+
// and drains it over its own standing, sealed.
|
|
225
|
+
private async unreached(id: string, object: JsonObject) {
|
|
226
|
+
const box = ((this.cells.outbox as Record<string, JsonObject[]>)[id] ??= []);
|
|
227
|
+
box.push(object);
|
|
228
|
+
while (box.length > OUTBOX) box.shift();
|
|
229
|
+
return { unreached: true, rang: await this.ring(id, object) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// A phone is rung on the standing it handed her, with the line sealed to
|
|
233
|
+
// the banner key it handed her beside it. Who is ringing is the standing
|
|
234
|
+
// the ring arrives on, so the ask carries no world, no pk and no token,
|
|
235
|
+
// and there is nothing in it to forge. A browser is rung on its endpoint
|
|
236
|
+
// through this box's own push faculty, because an endpoint answers only
|
|
237
|
+
// to the key this world minted, and the line goes with it unsealed:
|
|
238
|
+
// Web Push encrypts a body to the subscription's own keys, so a second
|
|
239
|
+
// seal would hide it from nobody it is not already hidden from.
|
|
240
|
+
private async ring(id: string, object: JsonObject): Promise<boolean> {
|
|
241
|
+
const rec = this.cells.occupants[id];
|
|
242
|
+
const at = this.standings[`${BELL}:${id}`];
|
|
243
|
+
const key = rec?.notes.banner;
|
|
244
|
+
if (at && typeof key === 'string') {
|
|
245
|
+
const sealed = await sealBanner(banner(object), key);
|
|
246
|
+
return sealed === null ? false : rang(await at.ask('ring', { sealed }));
|
|
247
|
+
}
|
|
248
|
+
const token = rec?.notes.bell;
|
|
249
|
+
if (!isToken(token)) return false;
|
|
250
|
+
await this.#push;
|
|
251
|
+
const st = this.standings[PUSH];
|
|
252
|
+
if (!st) return false;
|
|
253
|
+
// A browser is handed the line itself. The ground encrypts it under the
|
|
254
|
+
// subscription's own keys, RFC 8291, and the box that does it is this
|
|
255
|
+
// world's own, so the line is never seen by anything between them.
|
|
256
|
+
return rang(await st.ask('ring', { token: token as unknown as JsonObject, line: banner(object) }));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// A device says how it is woken, and what it says is kept on its own
|
|
260
|
+
// occupant, so forgetting the device forgets the way to wake it in the
|
|
261
|
+
// same act.
|
|
262
|
+
//
|
|
263
|
+
// A phone or a desk hands a standing at a doorbell of its own, and the
|
|
264
|
+
// banner key that doorbell never learns. This world holds a way to wake
|
|
265
|
+
// that one device and nothing it could hand on: no other world rings on
|
|
266
|
+
// this standing, the device cuts it by itself at the doorbell, and the
|
|
267
|
+
// platform's token rotating is one write there that no world is told of.
|
|
268
|
+
//
|
|
269
|
+
// A browser hands its endpoint instead and needs no doorbell: an endpoint
|
|
270
|
+
// is rung only by whoever holds the VAPID secret the subscription was
|
|
271
|
+
// made under, and that secret is this world's own.
|
|
272
|
+
//
|
|
273
|
+
// A store's token never arrives here. The same token at every world is
|
|
274
|
+
// the bearer credential the doorbell exists to take away, so one is
|
|
275
|
+
// refused where a human can read why.
|
|
276
|
+
async bell(args: JsonObject, asker: Asker): Promise<JsonObject> {
|
|
277
|
+
const c = client(this.occupant(asker));
|
|
278
|
+
const rec = c === null ? undefined : this.cells.occupants[c];
|
|
279
|
+
if (c === null || !rec) return { error: 'no client' };
|
|
280
|
+
// Asked with nothing, she says what she already holds. That is what
|
|
281
|
+
// makes handing a doorbell over idempotent: a standing does not rotate
|
|
282
|
+
// the way a token did, so a device hands one over when this world has
|
|
283
|
+
// none and never again, and the doorbell's list of the worlds it allows
|
|
284
|
+
// stays a list a human can read and cut.
|
|
285
|
+
if (args.invitation === undefined && args.token === undefined) {
|
|
286
|
+
const at = this.standings[`${BELL}:${c}`];
|
|
287
|
+
return { kept: at && typeof rec.notes.banner === 'string' ? 'doorbell' : isToken(rec.notes.bell) ? 'web' : null };
|
|
288
|
+
}
|
|
289
|
+
if (isInvitation(args.invitation)) {
|
|
290
|
+
const key = args.banner;
|
|
291
|
+
if (typeof key !== 'string' || !/^[0-9a-f]{64}$/.test(key)) return { error: 'a banner key is thirty two bytes as hex' };
|
|
292
|
+
const id = `${BELL}:${c}`;
|
|
293
|
+
this.standings.remove(id);
|
|
294
|
+
// She knocks and then takes, as every side does: an invitation is
|
|
295
|
+
// taken only where it was answered. What she asks for is the
|
|
296
|
+
// blueprint, which at a doorbell is the one ask `ring`.
|
|
297
|
+
const out = await this.knock(args.invitation);
|
|
298
|
+
if (isSilence(out) || isWord(out)) return { error: isSilence(out) ? 'silence' : wordOf(out) };
|
|
299
|
+
if ((await this.take(id, args.invitation)) === null) return { error: 'that invitation was refused' };
|
|
300
|
+
rec.notes.banner = key;
|
|
301
|
+
delete rec.notes.bell;
|
|
302
|
+
return { kept: 'doorbell' };
|
|
303
|
+
}
|
|
304
|
+
if (!isToken(args.token)) return { error: 'a token names its kind' };
|
|
305
|
+
if (args.token.kind !== 'web') return { error: 'a device hands a standing at its own doorbell, never a store token' };
|
|
306
|
+
rec.notes.bell = args.token;
|
|
307
|
+
return { kept: 'web' };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The device is back. It drains what it missed and composes what the
|
|
311
|
+
// human sees from these objects, which crossed sealed and were never on
|
|
312
|
+
// a push service. Emptied as it is handed over: an ask that answered is
|
|
313
|
+
// an ask that arrived, and a device that lost the answer has lost one
|
|
314
|
+
// notification, not its relation.
|
|
315
|
+
missed(_args: JsonObject, asker: Asker) {
|
|
316
|
+
const c = client(this.occupant(asker));
|
|
317
|
+
if (c === null) return { error: 'no client' };
|
|
318
|
+
const box = this.cells.outbox as Record<string, JsonObject[]>;
|
|
319
|
+
const out = box[c] ?? [];
|
|
320
|
+
delete box[c];
|
|
321
|
+
return { missed: out };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// She joins a world. This is the day a homeless org gains a home: an org
|
|
325
|
+
// whose whole existence was standings in other orgs' worlds boots a home
|
|
326
|
+
// on a box it trusts, and that home joins each of those from there.
|
|
327
|
+
// Nothing is converted and no id changes, because from the far world's
|
|
328
|
+
// side this home is one more thing of hers holding a standing, exactly as
|
|
329
|
+
// her phone is: her device there minted the invitation with `device`, and
|
|
330
|
+
// this is the other half, the knock.
|
|
331
|
+
//
|
|
332
|
+
// It is a device that may mint who asks it, and that needs no word of its
|
|
333
|
+
// own: `mint` is what shows her own door to a device, so one that holds
|
|
334
|
+
// it can boot and invite in her world already, and adding one standing to
|
|
335
|
+
// what she carries is less than that. In practice such a device holds
|
|
336
|
+
// `reach` as well, since a device that joined a world it could not then
|
|
337
|
+
// see would be a device doing something for somebody else. She hands back a way for the far being
|
|
338
|
+
// to reach her, as a device does at hello, and a far being who refuses it
|
|
339
|
+
// is joined all the same.
|
|
340
|
+
async join(args: JsonObject): Promise<JsonObject> {
|
|
341
|
+
const inv = args.invitation;
|
|
342
|
+
if (!isInvitation(inv)) return { error: 'that is not an invitation' };
|
|
343
|
+
const name = typeof args.name === 'string' ? args.name.trim() : '';
|
|
344
|
+
if (!/^[\w.-]{1,80}$/.test(name) || name === HOME || name === PUSH) return { error: 'a name is a word, and not home or the push' };
|
|
345
|
+
if (this.standings[name]) return { error: 'name taken' };
|
|
346
|
+
const back = `from:${name}`;
|
|
347
|
+
const mine = await this.invite(back);
|
|
348
|
+
if (mine === null) return { error: 'name taken' };
|
|
349
|
+
const out = await this.knock(inv, 'hello', { invitation: mine as unknown as JsonObject });
|
|
350
|
+
if (isSilence(out) || isWord(out)) {
|
|
351
|
+
this.occupants.remove(back);
|
|
352
|
+
return { error: isSilence(out) ? 'silence' : wordOf(out) };
|
|
353
|
+
}
|
|
354
|
+
if (out !== null && typeof out === 'object' && !Array.isArray(out) && 'error' in out) this.occupants.remove(back);
|
|
355
|
+
const took = await this.take(name, inv);
|
|
356
|
+
if (took === null) {
|
|
357
|
+
this.occupants.remove(back);
|
|
358
|
+
return { error: 'that invitation was refused' };
|
|
359
|
+
}
|
|
360
|
+
return { joined: name };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Her devices, by the names they were given, with what each was granted
|
|
364
|
+
// and which of them is asking. Without this a human cannot use `forget`
|
|
365
|
+
// or `push` at all: both name a device, and nobody who has just lost a
|
|
366
|
+
// phone remembers what it was called. It is read-only and says nothing
|
|
367
|
+
// secret; a device is a name and three words, and the keys are in the
|
|
368
|
+
// relations rather than here.
|
|
369
|
+
// People-shaped rows, since a page says devices are people: a name, and
|
|
370
|
+
// a line that says what each may do and which one is asking.
|
|
371
|
+
devices(_args: JsonObject, asker: Asker) {
|
|
372
|
+
const me = client(this.occupant(asker));
|
|
373
|
+
return Object.entries(this.cells.occupants)
|
|
374
|
+
.filter(([id]) => id !== DESK)
|
|
375
|
+
.map(([id, rec]) => {
|
|
376
|
+
const notes = (rec as OccupantRecord).notes;
|
|
377
|
+
const name = typeof notes.client === 'string' ? notes.client : id;
|
|
378
|
+
const has = MINTINGS.filter((w) => held(rec as OccupantRecord)[w]);
|
|
379
|
+
const line = [...(name === me ? ['this one'] : []), ...(has.length ? [`may ${has.join(', ')}`] : [])].join(' · ');
|
|
380
|
+
return { name, line, client: name, ...(name === me ? { you: true } : {}), ...(has.length ? { may: has } : {}) };
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Revocation is one act at her, because only she knows both ids. A side
|
|
385
|
+
// that had to remove two relations by hand could leave half of one
|
|
386
|
+
// standing, and every side would have to remember which half.
|
|
387
|
+
forget(args: JsonObject) {
|
|
388
|
+
const c = typeof args.client === 'string' ? args.client : null;
|
|
389
|
+
if (c === null) return { error: 'client is a string' };
|
|
390
|
+
this.occupants.remove(c);
|
|
391
|
+
this.standings.remove(`to:${c}`);
|
|
392
|
+
this.standings.remove(`${BELL}:${c}`);
|
|
393
|
+
delete (this.cells.ways as Record<string, Invitation>)[c];
|
|
394
|
+
delete (this.cells.outbox as Record<string, JsonObject[]>)[c];
|
|
395
|
+
return { forgot: c };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
chores() {
|
|
399
|
+
return { chores: ['census', 'report'] };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// How a device shows her: her name as the title, and what each ask is
|
|
403
|
+
// called. Values only; the screen and the model side each read their part.
|
|
404
|
+
look() {
|
|
405
|
+
return {
|
|
406
|
+
name: (this.cells.name as string) || 'you',
|
|
407
|
+
order: ['whoami', 'devices', 'device', 'join', 'push', 'forget', 'name'],
|
|
408
|
+
asks: { whoami: { title: 'who am I', readOnly: true }, devices: { title: 'your devices', readOnly: true }, device: { title: 'invite a device' }, join: { title: 'join a world' }, push: { title: 'wake a device' }, forget: { title: 'take a device out of your world', destructive: true }, name: { title: 'call her' } },
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Her page for a device: her name, who this device is, the worlds she
|
|
413
|
+
// holds as sections, and the three things a human does here by hand,
|
|
414
|
+
// bringing her next device in, waking one and naming her. The invitation
|
|
415
|
+
// `device` answers is a way in, and the side paints it as the link it is,
|
|
416
|
+
// since a being does not know where her page lives. `hello` and `report` are wiring, a
|
|
417
|
+
// device's first word and an agent's callback; they stay in her describe
|
|
418
|
+
// under the gate and off her page, since a page is presentation and the
|
|
419
|
+
// gate is permission. A device that may not do a thing sees no form for
|
|
420
|
+
// it, however the page names it.
|
|
421
|
+
page(_args: JsonObject, asker: Asker) {
|
|
422
|
+
return {
|
|
423
|
+
kind: 'stack',
|
|
424
|
+
of: [
|
|
425
|
+
{ kind: 'text', text: (this.cells.name as string) || 'you', role: 'title' },
|
|
426
|
+
{ kind: 'text', text: `on ${client(this.occupant(asker)) ?? 'a device'}`, role: 'quiet' },
|
|
427
|
+
{ kind: 'text', text: 'your devices', role: 'label' },
|
|
428
|
+
{ kind: 'answer', ask: 'devices', as: 'people' },
|
|
429
|
+
// every standing she carries as its section, and the door of her
|
|
430
|
+
// own world as a door: seven owner asks are a page of their own
|
|
431
|
+
...Object.keys(this.cells.standings)
|
|
432
|
+
.sort((a, b) => (a === HOME ? 1 : b === HOME ? -1 : a.localeCompare(b)))
|
|
433
|
+
.map((id) => (id === HOME ? { kind: 'standing', id, as: 'door' } : { kind: 'standing', id })),
|
|
434
|
+
{ kind: 'form', ask: 'device' },
|
|
435
|
+
{ kind: 'form', ask: 'join' },
|
|
436
|
+
{ kind: 'form', ask: 'push' },
|
|
437
|
+
{ kind: 'form', ask: 'forget' },
|
|
438
|
+
{ kind: 'form', ask: 'name' },
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// A device the root minted may say what she is called: the root trusts
|
|
444
|
+
// it with its id and its reach already, and a being does not know her own
|
|
445
|
+
// key. A name is a word; she keeps the last one given.
|
|
446
|
+
name(args: JsonObject) {
|
|
447
|
+
const name = typeof args.name === 'string' ? args.name.trim() : '';
|
|
448
|
+
if (!/^[\w.-]{1,80}$/.test(name)) return { error: 'a name is a word' };
|
|
449
|
+
this.cells.name = name;
|
|
450
|
+
return { named: name };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// A device that ran something for her says what it found. Kept, so that
|
|
454
|
+
// whoever renders her can show it; the ask itself is the callback.
|
|
455
|
+
report(args: JsonObject, asker: Asker) {
|
|
456
|
+
(this.cells.reports as JsonObject[]).push({ from: asker.id ?? null, event: args.event ?? null, result: args.result ?? null });
|
|
457
|
+
return { reported: true };
|
|
458
|
+
}
|
|
459
|
+
}
|
package/beings/way.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A way in: one being that turns a proof of one kind into who it is for.
|
|
3
|
+
// The desk holds a standing at each of them, under `way:<kind>`, and that
|
|
4
|
+
// id is the whole of what makes a being a way in: there is no table, no
|
|
5
|
+
// registry and nothing set on a class, so a way in is added and removed the
|
|
6
|
+
// way every other relation is, by the root's knock and by remove.
|
|
7
|
+
//
|
|
8
|
+
// Two kinds of holder, and both are ordinary beings:
|
|
9
|
+
//
|
|
10
|
+
// - the box's, lent by the dock ward, as `local` is: the daemon vouches for
|
|
11
|
+
// a process it saw on its own socket, and what it knows lives in that
|
|
12
|
+
// being, on the box, dying with it.
|
|
13
|
+
// - the world's own, in the world's ward, as an estate's post is: she holds
|
|
14
|
+
// the addresses, mints what she handed out and spends it once.
|
|
15
|
+
//
|
|
16
|
+
// A kind is a word the org chose and the dock knows none of them. There is
|
|
17
|
+
// no list here, no registry and no flag on a class, so mail, a message, a
|
|
18
|
+
// code on a screen, a letter, a voice on a call or a sign-in a person holds
|
|
19
|
+
// at some other company are each one kind, named nowhere in this package.
|
|
20
|
+
// Building one nobody here imagined is booting a being and knocking her
|
|
21
|
+
// onto the desk, and that is the whole of it.
|
|
22
|
+
//
|
|
23
|
+
// A way in answers `verify`. One that hands its proof out itself answers
|
|
24
|
+
// `send` beside it, and the desk shows the form for it because her standing
|
|
25
|
+
// says the being can be asked it. She judges neither: she does not know what
|
|
26
|
+
// an address is, whether one was sent to before, or what a link says.
|
|
27
|
+
import type { AskSpec, Json } from '@nervur-org/nervur';
|
|
28
|
+
|
|
29
|
+
export type Proof = { kind: string; [more: string]: Json };
|
|
30
|
+
// Who the proof is for, and how far the device it admits may go. `make` is
|
|
31
|
+
// the world saying this is a way in that brings its own people: a being
|
|
32
|
+
// under that key is made when there is none, which is what a world anyone
|
|
33
|
+
// may join needs and a world with a fixed household must never say. What
|
|
34
|
+
// is made is the org's being about that person, a membership and never a
|
|
35
|
+
// home: `true` makes a `User`, a class name makes one of the estate's own.
|
|
36
|
+
export type Verified = { user: string; client: string; wake?: boolean; reach?: boolean; mint?: boolean; revoke?: boolean; make?: boolean | string };
|
|
37
|
+
|
|
38
|
+
// The desk's id for the way in of one kind, and the kind read back off it.
|
|
39
|
+
// The kinds a world offers are the ids the desk holds, and nothing else.
|
|
40
|
+
export const WAY = (kind: string): string => `way:${kind}`;
|
|
41
|
+
export const kindOf = (id: string): string | null => (id.startsWith('way:') ? id.slice(4) : null);
|
|
42
|
+
|
|
43
|
+
// The two asks a way in may be asked. Every implementation declares these,
|
|
44
|
+
// so one suite reaches all of them and the desk asks one thing of each.
|
|
45
|
+
export const VERIFY: AskSpec = { description: 'who this proof is for', input: { type: 'object', properties: { proof: { type: 'object' } }, required: ['proof'] } };
|
|
46
|
+
export const SEND: AskSpec = { description: 'send a way in to an address of that kind', input: { type: 'object', properties: { to: { type: 'string' } }, required: ['to'] } };
|
|
47
|
+
|
|
48
|
+
// What came back from `verify`, read before it is trusted. A way in is
|
|
49
|
+
// somebody else's being, so an answer that is not this shape is a refusal
|
|
50
|
+
// and never a caller the desk makes up.
|
|
51
|
+
export function verified(out: unknown): Verified | null {
|
|
52
|
+
if (out === null || typeof out !== 'object' || Array.isArray(out)) return null;
|
|
53
|
+
const v = out as Record<string, unknown>;
|
|
54
|
+
if (typeof v.user !== 'string' || typeof v.client !== 'string') return null;
|
|
55
|
+
const make = v.make;
|
|
56
|
+
return {
|
|
57
|
+
user: v.user,
|
|
58
|
+
client: v.client,
|
|
59
|
+
...(v.wake === true ? { wake: true } : {}),
|
|
60
|
+
...(v.reach === true ? { reach: true } : {}),
|
|
61
|
+
...(v.mint === true ? { mint: true } : {}),
|
|
62
|
+
...(v.revoke === true ? { revoke: true } : {}),
|
|
63
|
+
...(make === true || typeof make === 'string' ? { make } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
package/cli/bundles.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Where the daemon finds the browser bundles the dock's build emitted. The
|
|
3
|
+
// route is handed two strings and reads nothing; this file is the terrain's
|
|
4
|
+
// half, and it is Node's because the daemon is. One path answers in both
|
|
5
|
+
// trees, because `dist` mirrors the source layout: `human/<name>.bundle.txt`
|
|
6
|
+
// is beside this file's own folder, whether this file is the source the
|
|
7
|
+
// daemon runs from or the JavaScript in an installed package.
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
export type Bundles = { tab: string; worker: string };
|
|
12
|
+
|
|
13
|
+
const read = async (name: string) => {
|
|
14
|
+
const at = fileURLToPath(new URL(`../human/${name}.bundle.txt`, import.meta.url));
|
|
15
|
+
const text = await readFile(at, 'utf8').catch(() => null);
|
|
16
|
+
if (text === null) throw new Error(`no ${name} bundle at ${at}: run the dock's build, which emits human/${name}.bundle.txt`);
|
|
17
|
+
return text;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const bundles = async (): Promise<Bundles> => ({ tab: await read('tab'), worker: await read('worker') });
|