@nervur-org/dock 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/api/route.ts +4 -72
- package/beings/avatar.ts +11 -7
- package/beings/carry.ts +129 -116
- package/beings/clock.ts +11 -7
- package/beings/desk.ts +7 -6
- package/beings/doorbell.ts +36 -7
- package/beings/index.ts +17 -4
- package/beings/join.ts +7 -4
- package/beings/link.ts +1 -1
- package/beings/porter.ts +5 -1
- package/beings/presence.ts +15 -5
- package/beings/setup.ts +27 -21
- package/beings/user.ts +26 -17
- package/beings/way.ts +1 -1
- package/{dock → box}/agents.ts +37 -44
- package/{dock → box}/apns.ts +4 -4
- package/box/box.ts +31 -0
- package/box/contract.ts +41 -0
- package/box/dialers.ts +88 -0
- package/box/dock.ts +148 -0
- package/box/envoy.ts +96 -0
- package/box/faculty.ts +111 -0
- package/{dock → box}/fcm.ts +1 -1
- package/box/grants.ts +90 -0
- package/box/http.ts +93 -0
- package/{dock → box}/http2.ts +7 -7
- package/box/index.ts +201 -0
- package/box/join.ts +78 -0
- package/box/local.ts +37 -0
- package/{dock → box}/location.ts +15 -3
- package/{dock → box}/notifications.ts +17 -5
- package/box/push.ts +85 -0
- package/box/ring.ts +85 -0
- package/{dock → box}/service.ts +2 -2
- package/box/timer.ts +150 -0
- package/box/tree.ts +22 -0
- package/{dock → box}/webpush.ts +2 -2
- package/cli/client.ts +1 -1
- package/cli/daemon.ts +101 -103
- package/cli/estate.ts +2 -3
- package/cli/nervur.ts +44 -25
- package/cli/stand.ts +1 -1
- package/dist/api/route.d.ts +1 -2
- package/dist/api/route.js +4 -76
- package/dist/beings/avatar.d.ts +4 -3
- package/dist/beings/avatar.js +9 -6
- package/dist/beings/carry.d.ts +10 -12
- package/dist/beings/carry.js +119 -127
- package/dist/beings/clock.js +10 -7
- package/dist/beings/desk.js +8 -7
- package/dist/beings/doorbell.d.ts +3 -1
- package/dist/beings/doorbell.js +37 -4
- package/dist/beings/index.d.ts +24 -4
- package/dist/beings/index.js +15 -4
- package/dist/beings/join.d.ts +1 -0
- package/dist/beings/join.js +6 -4
- package/dist/beings/link.d.ts +0 -1
- package/dist/beings/link.js +1 -1
- package/dist/beings/porter.js +5 -1
- package/dist/beings/presence.d.ts +1 -0
- package/dist/beings/presence.js +12 -3
- package/dist/beings/setup.d.ts +5 -2
- package/dist/beings/setup.js +16 -15
- package/dist/beings/user.d.ts +4 -3
- package/dist/beings/user.js +18 -11
- package/dist/{dock → box}/agents.d.ts +7 -4
- package/dist/{dock → box}/agents.js +30 -37
- package/dist/{dock → box}/apns.d.ts +1 -1
- package/dist/{dock → box}/apns.js +3 -3
- package/dist/box/box.d.ts +9 -0
- package/dist/box/box.js +18 -0
- package/dist/box/contract.d.ts +6 -0
- package/dist/box/contract.js +35 -0
- package/dist/{dock → box}/dialers.d.ts +7 -4
- package/dist/box/dialers.js +76 -0
- package/dist/box/dock.d.ts +182 -0
- package/dist/box/dock.js +128 -0
- package/dist/box/envoy.d.ts +44 -0
- package/dist/box/envoy.js +75 -0
- package/dist/box/faculty.d.ts +68 -0
- package/dist/box/faculty.js +97 -0
- package/dist/{dock → box}/fcm.d.ts +1 -1
- package/dist/{dock → box}/grants.d.ts +4 -3
- package/dist/box/grants.js +78 -0
- package/dist/box/http.d.ts +26 -0
- package/dist/box/http.js +89 -0
- package/dist/{dock → box}/http2.d.ts +1 -1
- package/dist/{dock → box}/http2.js +6 -6
- package/dist/box/index.d.ts +46 -0
- package/dist/box/index.js +179 -0
- package/dist/box/join.d.ts +19 -0
- package/dist/box/join.js +67 -0
- package/dist/{dock → box}/local.d.ts +3 -2
- package/dist/box/local.js +27 -0
- package/dist/{dock → box}/location.d.ts +3 -0
- package/dist/box/location.js +21 -0
- package/dist/{dock → box}/notifications.d.ts +3 -0
- package/dist/{dock → box}/notifications.js +14 -4
- package/dist/{dock → box}/push.d.ts +12 -18
- package/dist/box/push.js +83 -0
- package/dist/box/ring.d.ts +18 -0
- package/dist/box/ring.js +55 -0
- package/dist/{dock → box}/service.d.ts +1 -1
- package/dist/{dock → box}/service.js +1 -1
- package/dist/{dock → box}/timer.d.ts +2 -0
- package/dist/box/timer.js +127 -0
- package/dist/box/tree.d.ts +5 -0
- package/dist/box/tree.js +14 -0
- package/dist/{dock → box}/webpush.d.ts +1 -1
- package/dist/cli/client.d.ts +1 -0
- package/dist/cli/daemon.d.ts +2 -8
- package/dist/cli/daemon.js +102 -107
- package/dist/cli/estate.js +2 -3
- package/dist/cli/nervur.js +44 -24
- package/dist/cli/stand.d.ts +0 -14
- package/dist/cli/stand.js +1 -1
- package/dist/harbor/browser.d.ts +8 -5
- package/dist/harbor/browser.js +20 -21
- package/dist/harbor/capacitor.d.ts +3 -3
- package/dist/harbor/capacitor.js +9 -8
- package/dist/harbor/disk.d.ts +9 -9
- package/dist/harbor/disk.js +56 -51
- package/dist/harbor/edge/alarm.d.ts +4 -2
- package/dist/harbor/edge/alarm.js +9 -19
- package/dist/harbor/edge/edge.d.ts +7 -6
- package/dist/harbor/edge/edge.js +58 -53
- package/dist/harbor/edge/exercise.js +4 -4
- package/dist/harbor/edge/given.d.ts +0 -1
- package/dist/harbor/edge/given.js +0 -29
- package/dist/harbor/edge/storage.d.ts +0 -2
- package/dist/harbor/edge/storage.js +2 -2
- package/dist/harbor/http.js +4 -2
- package/dist/harbor/sealed.d.ts +0 -5
- package/dist/harbor/sealed.js +0 -39
- package/dist/harbor/tauri.d.ts +2 -2
- package/dist/harbor/tauri.js +13 -10
- package/dist/human/app.d.ts +1 -1
- package/dist/human/app.js +10 -12
- package/dist/human/door.d.ts +0 -1
- package/dist/human/door.js +1 -1
- package/dist/human/html.js +1 -1
- package/dist/human/move.js +1 -1
- package/dist/human/tab.bundle.txt +3629 -2824
- package/dist/human/wake.d.ts +5 -2
- package/dist/human/wake.js +23 -14
- package/dist/human/web.d.ts +0 -1
- package/dist/human/web.js +3 -3
- package/dist/human/worker.bundle.txt +2945 -2118
- package/dist/human/worlds.js +3 -3
- package/dist/mcp/agent.d.ts +0 -1
- package/dist/mcp/agent.js +1 -1
- package/dist/mcp/allow.js +3 -2
- package/dist/mcp/http.d.ts +0 -1
- package/dist/mcp/http.js +1 -1
- package/dist/mcp/oauth.d.ts +3 -4
- package/dist/mcp/oauth.js +4 -4
- package/dist/mcp/route.d.ts +1 -1
- package/dist/mcp/runner.d.ts +0 -1
- package/dist/mcp/runner.js +1 -1
- package/dist/mcp/server.d.ts +0 -1
- package/dist/mcp/server.js +1 -1
- package/harbor/browser.ts +22 -24
- package/harbor/capacitor.ts +9 -8
- package/harbor/disk.ts +53 -48
- package/harbor/edge/alarm.ts +15 -12
- package/harbor/edge/edge.ts +61 -55
- package/harbor/edge/exercise.ts +4 -4
- package/harbor/edge/given.ts +3 -33
- package/harbor/edge/storage.ts +2 -2
- package/harbor/http.ts +5 -3
- package/harbor/sealed.ts +0 -35
- package/harbor/tauri.ts +13 -10
- package/human/app.ts +12 -13
- package/human/door.ts +1 -1
- package/human/html.ts +1 -1
- package/human/local.ts +1 -1
- package/human/move.ts +1 -1
- package/human/tab.bundle.txt +3629 -2824
- package/human/wake.ts +36 -16
- package/human/web.ts +4 -4
- package/human/worker.bundle.txt +2945 -2118
- package/human/worlds.ts +3 -3
- package/mcp/agent.ts +1 -1
- package/mcp/allow.ts +2 -1
- package/mcp/http.ts +1 -1
- package/mcp/oauth.ts +4 -4
- package/mcp/route.ts +1 -1
- package/mcp/runner.ts +1 -1
- package/mcp/server.ts +1 -1
- package/package.json +9 -8
- package/beings/envoy.ts +0 -218
- package/dist/beings/envoy.d.ts +0 -54
- package/dist/beings/envoy.js +0 -211
- package/dist/dock/console.d.ts +0 -6
- package/dist/dock/console.js +0 -20
- package/dist/dock/dialers.js +0 -75
- package/dist/dock/faculty.d.ts +0 -24
- package/dist/dock/faculty.js +0 -142
- package/dist/dock/grants.js +0 -99
- package/dist/dock/index.d.ts +0 -41
- package/dist/dock/index.js +0 -113
- package/dist/dock/local.js +0 -44
- package/dist/dock/location.js +0 -11
- package/dist/dock/push.js +0 -132
- package/dist/dock/timer.js +0 -149
- package/dock/console.ts +0 -23
- package/dock/dialers.ts +0 -85
- package/dock/faculty.ts +0 -148
- package/dock/grants.ts +0 -111
- package/dock/index.ts +0 -133
- package/dock/local.ts +0 -48
- package/dock/push.ts +0 -161
- package/dock/timer.ts +0 -174
- /package/dist/{dock → box}/fcm.js +0 -0
- /package/dist/{dock → box}/webpush.js +0 -0
package/dist/dock/index.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { isInvitation } from 'nervur';
|
|
2
|
-
import { Faculty } from './faculty.js';
|
|
3
|
-
import { Console, CONSOLE } from './console.js';
|
|
4
|
-
import { Local } from './local.js';
|
|
5
|
-
import { Location } from './location.js';
|
|
6
|
-
import { Notifications } from './notifications.js';
|
|
7
|
-
import { Push } from './push.js';
|
|
8
|
-
import { Timer } from './timer.js';
|
|
9
|
-
export { Console, CONSOLE } from './console.js';
|
|
10
|
-
export { Faculty, join, settled } from './faculty.js';
|
|
11
|
-
export { Local, LOCAL } from './local.js';
|
|
12
|
-
export { Location } from './location.js';
|
|
13
|
-
export { Notifications } from './notifications.js';
|
|
14
|
-
export { Push, isToken, bellOf, banner, sealBanner, SEAL } from './push.js';
|
|
15
|
-
export { Timer } from './timer.js';
|
|
16
|
-
// The three grounds the push faculty rings through, and the bell service
|
|
17
|
-
// that holds a publisher's keys: what the box can do, beside the faculty.
|
|
18
|
-
export { webPush, mintVapid, publicOf } from './webpush.js';
|
|
19
|
-
export { apnsPush, pkcs8, HOSTS } from './apns.js';
|
|
20
|
-
export { fcmPush, isAccount, DOORS } from './fcm.js';
|
|
21
|
-
export { handedTo, ringing, routed } from './service.js';
|
|
22
|
-
// The names this package lends under. They are conventions of the dock and
|
|
23
|
-
// nothing of Quo's: a name is the harbor's namespace, and a being holds each
|
|
24
|
-
// one under an id of her own choosing.
|
|
25
|
-
export const LOCATION = 'location';
|
|
26
|
-
export const NOTIFICATIONS = 'notifications';
|
|
27
|
-
export const PUSH = 'push';
|
|
28
|
-
export const TIMER = 'timer';
|
|
29
|
-
// The classes every dock ward may hold, by class name. A terrain hands its
|
|
30
|
-
// own subclasses under the same names and the harbor holds those instead.
|
|
31
|
-
// `local` and `grants` are not among them: the box's way in and the box's
|
|
32
|
-
// grants stand only where a terrain serves routes, and that terrain adds
|
|
33
|
-
// them to what it lends. `Grants` is not in this file at all, so that a tab
|
|
34
|
-
// bundling this index carries no route's notion: the terrain that serves
|
|
35
|
-
// routes hands her class to its harbor, as the daemon hands its own push.
|
|
36
|
-
export const FACULTIES = { [LOCATION]: 'Location', [NOTIFICATIONS]: 'Notifications', [PUSH]: 'Push', [TIMER]: 'Timer' };
|
|
37
|
-
export const FACULTY_CLASSES = { Console, Local, Location, Notifications, Push, Timer };
|
|
38
|
-
// The name a harbor keeps its own ward under. It is a ward in every other
|
|
39
|
-
// respect, and this one is never adopted or dropped as a migration: its
|
|
40
|
-
// beings are the box's and die with it.
|
|
41
|
-
export const DOCK = 'dock';
|
|
42
|
-
// Put the dock ward up and answer the `lend` this harbor hands its wards.
|
|
43
|
-
// `lent` says which names this box offers and which class stands for each;
|
|
44
|
-
// a name it does not hold is null, and so is every other kind of no.
|
|
45
|
-
//
|
|
46
|
-
// The harbor keeps this ward's ask pointer rather than handing it out, so it
|
|
47
|
-
// is the root here, and the root may invite on any being of its ward. That
|
|
48
|
-
// is the whole of what `lend` does: it never mints, it asks its own root to.
|
|
49
|
-
export async function stand(harbor, lent = FACULTIES) {
|
|
50
|
-
const kept = await harbor.store.load(DOCK);
|
|
51
|
-
const ward = harbor.wards.get(DOCK) ?? (kept ? await harbor.host(DOCK, kept) : await harbor.create(DOCK, CONSOLE));
|
|
52
|
-
return standOn(ward, lent);
|
|
53
|
-
}
|
|
54
|
-
// The faculties up in a ward already made, and the `lend` that reaches them.
|
|
55
|
-
// Apart from `stand` so a suite can put its own stubs in its own ward and
|
|
56
|
-
// get the same thing a box gets. It is run again at every boot and says the
|
|
57
|
-
// same thing twice without harm: a key already booted is refused, and an
|
|
58
|
-
// invitation on an id already held is null.
|
|
59
|
-
export async function standOn(ward, lent = FACULTIES) {
|
|
60
|
-
for (const [name, className] of Object.entries(lent))
|
|
61
|
-
await ward.ask('boot', { key: name, class: className });
|
|
62
|
-
// The box as one being, carrying what it stood. She is the page a device
|
|
63
|
-
// that has joined no world shows, and she is placed by the root because
|
|
64
|
-
// the root is the box: a standing at every faculty, minted and knocked in
|
|
65
|
-
// one move, as an owner places any relation into a being of its ward.
|
|
66
|
-
await ward.ask('boot', { key: CONSOLE, class: 'Console' });
|
|
67
|
-
for (const name of Object.keys(lent)) {
|
|
68
|
-
const inv = await ward.ask('invite', { being: name, id: `${CONSOLE}:${name}` });
|
|
69
|
-
if (!isInvitation(inv))
|
|
70
|
-
continue;
|
|
71
|
-
await ward.ask('knock', { being: CONSOLE, id: name, invitation: inv });
|
|
72
|
-
}
|
|
73
|
-
await ward.save?.();
|
|
74
|
-
return lending(ward);
|
|
75
|
-
}
|
|
76
|
-
// The root's half alone: a name and a taker in, whether the ward took it out.
|
|
77
|
-
// It never mints; it asks the root of the ward it holds to, which is what a
|
|
78
|
-
// root may do on any being of its ward. And it unmints: a taker that would
|
|
79
|
-
// not take leaves an occupant on that faculty which nobody holds, so the root
|
|
80
|
-
// removes it before answering, and a refused lend costs the box nothing.
|
|
81
|
-
//
|
|
82
|
-
// Any being of the dock ward is lent by her key, not only the ones a terrain
|
|
83
|
-
// stood: the faculties the dock ships are examples, and an estate boots its
|
|
84
|
-
// own into the dock ward, of a class in its own `classes/`, and lends it the
|
|
85
|
-
// same way. A name nobody there answers to is refused at the invite.
|
|
86
|
-
//
|
|
87
|
-
// The id is drawn and never counted. A faculty's occupants are in her cells
|
|
88
|
-
// and outlive the process, so a count that started again at every boot would
|
|
89
|
-
// name, on the first lend after a restart, an id she already holds, and the
|
|
90
|
-
// invite would be refused for the rest of the box's life.
|
|
91
|
-
export function lending(ward) {
|
|
92
|
-
return async (name, take) => {
|
|
93
|
-
const id = `lent:${fresh()}`;
|
|
94
|
-
const out = await ward.ask('invite', { being: name, id });
|
|
95
|
-
if (!isInvitation(out))
|
|
96
|
-
return false;
|
|
97
|
-
const took = await take(out);
|
|
98
|
-
if (!took)
|
|
99
|
-
await ward.ask('remove', { being: name, id });
|
|
100
|
-
await ward.save?.();
|
|
101
|
-
return took;
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
const fresh = () => Array.from(globalThis.crypto.getRandomValues(new Uint8Array(8)))
|
|
105
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
106
|
-
.join('');
|
|
107
|
-
// One faculty of this box, as the device's own code holds it: the object the
|
|
108
|
-
// harbor made, in this process. Not a path around a door, and nothing of it
|
|
109
|
-
// crosses an edge; it is how a device fires its own timer at a moment it
|
|
110
|
-
// names and how a suite reads what a stub recorded.
|
|
111
|
-
export function facultyOf(harbor, name) {
|
|
112
|
-
return harbor.wards.get(DOCK)?.being(name);
|
|
113
|
-
}
|
package/dist/dock/local.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The box's way in: `local`, one faculty of the dock ward. The daemon saw a
|
|
3
|
-
// process reach a socket only the device's user can open, and that is the
|
|
4
|
-
// whole of the vouch; file permissions are the device's rules, which is what
|
|
5
|
-
// `quo/SPEC.md` allows a harbor to hand the root by.
|
|
6
|
-
//
|
|
7
|
-
// She holds the nonces this box minted, in this process and never in cells:
|
|
8
|
-
// a vouch that outlived a restart would be a vouch for a process that is
|
|
9
|
-
// gone. Each is honoured once, and a `local` proof arriving any other way
|
|
10
|
-
// names no nonce and is refused.
|
|
11
|
-
//
|
|
12
|
-
// The device's own code mints on her directly, as a device fires its own
|
|
13
|
-
// timer: `mint` is not an ask, it crosses no door, and nothing at a door
|
|
14
|
-
// can reach it. The desk asks `verify` at her standing, as she asks any way
|
|
15
|
-
// in.
|
|
16
|
-
import { Faculty } from './faculty.js';
|
|
17
|
-
import { VERIFY } from '../beings/way.js';
|
|
18
|
-
// The name the dock ward lends her under. A box with no local side lends no
|
|
19
|
-
// such name, and the desk there holds no such standing.
|
|
20
|
-
export const LOCAL = 'local';
|
|
21
|
-
export class Local extends Faculty {
|
|
22
|
-
static asks = { verify: VERIFY };
|
|
23
|
-
#nonces = new Map();
|
|
24
|
-
// A nonce for one client this box saw itself, spent by the first proof
|
|
25
|
-
// that names it. The caller drops it when the admit is over: a nonce that
|
|
26
|
-
// was never spent is not a way in that stays open.
|
|
27
|
-
mint(nonce, who) {
|
|
28
|
-
this.#nonces.set(nonce, who);
|
|
29
|
-
}
|
|
30
|
-
drop(nonce) {
|
|
31
|
-
this.#nonces.delete(nonce);
|
|
32
|
-
}
|
|
33
|
-
verify(args) {
|
|
34
|
-
const proof = args.proof;
|
|
35
|
-
if (!proof || typeof proof !== 'object' || Array.isArray(proof))
|
|
36
|
-
return null;
|
|
37
|
-
const nonce = proof.nonce;
|
|
38
|
-
const who = typeof nonce === 'string' ? this.#nonces.get(nonce) : undefined;
|
|
39
|
-
if (!who)
|
|
40
|
-
return null;
|
|
41
|
-
this.#nonces.delete(nonce);
|
|
42
|
-
return who;
|
|
43
|
-
}
|
|
44
|
-
}
|
package/dist/dock/location.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Faculty } from './faculty.js';
|
|
2
|
-
export class Location extends Faculty {
|
|
3
|
-
static asks = {
|
|
4
|
-
where: { description: 'where this box is', input: { type: 'object' } },
|
|
5
|
-
};
|
|
6
|
-
// The platform's own permission prompt is the first ask's, and a refusal is
|
|
7
|
-
// the error object every ask after it answers.
|
|
8
|
-
where() {
|
|
9
|
-
return Promise.resolve({ error: 'this box cannot say where it is' });
|
|
10
|
-
}
|
|
11
|
-
}
|
package/dist/dock/push.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Waking a device that is not reachable, the one way its platform allows: a
|
|
3
|
-
// push that carries the device's token and the banner sealed to that
|
|
4
|
-
// device's own key, which nothing between the world and the phone can
|
|
5
|
-
// read. A token names its
|
|
6
|
-
// kind, and a kind is one ring: `web` is Web Push under a keypair a world
|
|
7
|
-
// mints itself, `apns` and `fcm` are the two stores' own, handed on to the
|
|
8
|
-
// publisher's bell service by a world that holds no platform key. A box that
|
|
9
|
-
// was given no ring for a kind says so, which is what a world that rings no
|
|
10
|
-
// iPhones says to one.
|
|
11
|
-
//
|
|
12
|
-
// The rings are live objects and not values, so they are not cells: a
|
|
13
|
-
// terrain subclasses and builds its own in `rings`, out of what its
|
|
14
|
-
// environment and this being's cells give it.
|
|
15
|
-
import { OWNER } from 'nervur';
|
|
16
|
-
import { arithmetic } from 'nervur/ward';
|
|
17
|
-
import { Faculty } from './faculty.js';
|
|
18
|
-
import { mintVapid } from './webpush.js';
|
|
19
|
-
export const isToken = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && typeof v.kind === 'string';
|
|
20
|
-
// The bell a token names, when it names one a ring can reach.
|
|
21
|
-
export function bellOf(token) {
|
|
22
|
-
if (typeof token.bell !== 'string')
|
|
23
|
-
return undefined;
|
|
24
|
-
try {
|
|
25
|
-
const u = new URL(token.bell);
|
|
26
|
-
return u.protocol === 'https:' || u.protocol === 'http:' ? token.bell : undefined;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// What a platform will carry, so a seal too big is refused where a human
|
|
33
|
-
// can still read why rather than two hops later by a service nobody reads.
|
|
34
|
-
export const SEAL = 4096;
|
|
35
|
-
// The line a device shows when it was woken while nobody was looking,
|
|
36
|
-
// composed from one object. A pushed object is whatever the far being sent,
|
|
37
|
-
// so the two names a notification has are taken when they are there and the
|
|
38
|
-
// object is shown as it is when they are not.
|
|
39
|
-
export function banner(o) {
|
|
40
|
-
const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : '');
|
|
41
|
-
const title = text(o.title) || text(o.from) || 'Nervur';
|
|
42
|
-
const body = text(o.body) ||
|
|
43
|
-
text(o.said) ||
|
|
44
|
-
Object.entries(o)
|
|
45
|
-
.filter(([k]) => k !== 'title' && k !== 'from')
|
|
46
|
-
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
47
|
-
.join(', ');
|
|
48
|
-
return { title, body };
|
|
49
|
-
}
|
|
50
|
-
// A banner sealed to a device's banner key, as hex, or null when it will
|
|
51
|
-
// not seal or will not fit. The world seals and the process that draws the
|
|
52
|
-
// banner opens it; no doorbell, no bell service and no platform between
|
|
53
|
-
// them holds either half, which is why a ring may carry a line at all.
|
|
54
|
-
export async function sealBanner(line, key) {
|
|
55
|
-
if (!/^[0-9a-f]{64}$/.test(key))
|
|
56
|
-
return null;
|
|
57
|
-
try {
|
|
58
|
-
const boxed = await arithmetic.box(new TextEncoder().encode(JSON.stringify(line)), arithmetic.unhex(key), globalThis.crypto.getRandomValues(new Uint8Array(32)));
|
|
59
|
-
const wire = arithmetic.hex(boxed.bytes);
|
|
60
|
-
return wire.length > SEAL ? null : wire;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// The root and nobody else: where a box hands a phone's ring is the box's own.
|
|
67
|
-
const root = (_occ, asker) => asker.id === OWNER;
|
|
68
|
-
export class Push extends Faculty {
|
|
69
|
-
// What she keeps, beside every faculty's: the world's doorbell key, a P-256
|
|
70
|
-
// JWK she mints herself the first time a terrain asks for one, so a Web
|
|
71
|
-
// Push is signed under a key that lives with the wards and in no file; and
|
|
72
|
-
// the bell service a phone's ring is handed to when its token names none,
|
|
73
|
-
// a row the root keeps, since whose service it is belongs to an estate.
|
|
74
|
-
static cells = { watched: {}, minted: 0, key: null, service: null };
|
|
75
|
-
static asks = {
|
|
76
|
-
ring: { description: 'wake a device: its token, the banner sealed to it, or the line itself where the ground encrypts it', input: { type: 'object', properties: { token: { type: 'object' }, sealed: { type: 'string' }, line: { type: 'object', properties: { title: { type: 'string' }, body: { type: 'string' } }, required: ['title', 'body'] } }, required: ['token'] } },
|
|
77
|
-
kinds: { description: 'which kinds this box can ring', input: { type: 'object' } },
|
|
78
|
-
keep: { description: 'the bell service a phone is rung through when its token names none; null forgets it', input: { type: 'object', properties: { service: { type: ['string', 'null'] } }, required: ['service'] }, for: root },
|
|
79
|
-
};
|
|
80
|
-
// The doorbell key, minted once and kept. A terrain asks for it when it
|
|
81
|
-
// serves a web route; a box that never does holds none and rings no tabs.
|
|
82
|
-
async mint() {
|
|
83
|
-
if (this.cells.key === null)
|
|
84
|
-
this.cells.key = (await mintVapid()).key;
|
|
85
|
-
return this.cells.key;
|
|
86
|
-
}
|
|
87
|
-
get key() {
|
|
88
|
-
return this.cells.key;
|
|
89
|
-
}
|
|
90
|
-
get service() {
|
|
91
|
-
return typeof this.cells.service === 'string' ? this.cells.service : undefined;
|
|
92
|
-
}
|
|
93
|
-
keep(args) {
|
|
94
|
-
const service = args.service;
|
|
95
|
-
if (service !== null && (typeof service !== 'string' || !/^https?:\/\/\S+$/.test(service)))
|
|
96
|
-
return { error: 'a bell service is a URL over http or https, or null' };
|
|
97
|
-
this.cells.service = service === null ? null : service.replace(/\/$/, '');
|
|
98
|
-
return { service: this.cells.service };
|
|
99
|
-
}
|
|
100
|
-
// What this box can ring. A terrain overrides it and builds its rings from
|
|
101
|
-
// its environment and this being's cells; the base rings nothing.
|
|
102
|
-
get rings() {
|
|
103
|
-
return {};
|
|
104
|
-
}
|
|
105
|
-
kinds() {
|
|
106
|
-
return { kinds: Object.keys(this.rings) };
|
|
107
|
-
}
|
|
108
|
-
async ring(args) {
|
|
109
|
-
const token = args.token;
|
|
110
|
-
if (!isToken(token))
|
|
111
|
-
return { error: 'a token names its kind' };
|
|
112
|
-
const sealed = args.sealed;
|
|
113
|
-
if (sealed !== undefined && typeof sealed !== 'string')
|
|
114
|
-
return { error: 'a seal is text' };
|
|
115
|
-
if (typeof sealed === 'string' && sealed.length > SEAL)
|
|
116
|
-
return { error: `a seal is at most ${SEAL} bytes` };
|
|
117
|
-
const said = args.line;
|
|
118
|
-
if (said !== undefined && (said === null || typeof said !== 'object' || Array.isArray(said) || typeof said.title !== 'string' || typeof said.body !== 'string'))
|
|
119
|
-
return { error: 'a line is a title and a body' };
|
|
120
|
-
const line = said;
|
|
121
|
-
const r = Object.hasOwn(this.rings, token.kind) ? this.rings[token.kind] : undefined;
|
|
122
|
-
if (!r)
|
|
123
|
-
return { error: `this box cannot ring a ${token.kind}` };
|
|
124
|
-
try {
|
|
125
|
-
await r.ring(token, sealed, line);
|
|
126
|
-
return { rang: token.kind };
|
|
127
|
-
}
|
|
128
|
-
catch (e) {
|
|
129
|
-
return { error: e instanceof Error ? e.message : 'the ring threw' };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
package/dist/dock/timer.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { Faculty } from './faculty.js';
|
|
2
|
-
const MINUTE = 60_000;
|
|
3
|
-
// A timeout further out than this is not trusted, so the arming is capped and
|
|
4
|
-
// made again when it goes off. It is the base class's own bound and no rule
|
|
5
|
-
// of the faculty: a terrain whose alarm holds a month arms for the month.
|
|
6
|
-
const DAY = 86_400_000;
|
|
7
|
-
export class Timer extends Faculty {
|
|
8
|
-
static asks = {
|
|
9
|
-
watch: { description: 'wake me when I say I am next due', input: { type: 'object', properties: { invitation: { type: 'object' }, method: { type: 'string' }, args: { type: 'object' } }, required: ['invitation', 'method'] } },
|
|
10
|
-
due: { description: 'bring my next round forward to this moment', input: { type: 'object', properties: { at: { type: 'number' } }, required: ['at'] } },
|
|
11
|
-
now: { description: 'this box’s moment', input: { type: 'object' } },
|
|
12
|
-
};
|
|
13
|
-
#timeout;
|
|
14
|
-
#stopped = false;
|
|
15
|
-
#armed = null;
|
|
16
|
-
#round = null;
|
|
17
|
-
// The moment she is armed for, or null while she is asleep. For the
|
|
18
|
-
// device's own code and for a suite; it crosses no door.
|
|
19
|
-
get armed() {
|
|
20
|
-
return this.#armed;
|
|
21
|
-
}
|
|
22
|
-
// A watcher says she is due sooner than the box knew. It only ever brings
|
|
23
|
-
// the round forward: what fires is decided at the tick, in the watcher's
|
|
24
|
-
// own cells, so an arming that moved later on somebody's word would be a
|
|
25
|
-
// schedule somebody else held silenced. The door is the whole gate here:
|
|
26
|
-
// a standing at a faculty comes from `lend` alone, and this adds no second
|
|
27
|
-
// one.
|
|
28
|
-
due(args) {
|
|
29
|
-
const at = args.at;
|
|
30
|
-
if (typeof at !== 'number' || !Number.isFinite(at))
|
|
31
|
-
return { error: 'a due carries the moment: { at }' };
|
|
32
|
-
const when = moment(at);
|
|
33
|
-
if (this.#armed === null || when < this.#armed)
|
|
34
|
-
this.#arm(when);
|
|
35
|
-
return { armed: this.#armed };
|
|
36
|
-
}
|
|
37
|
-
// Whoever was just filed is ticked at once, and what she answers is what
|
|
38
|
-
// the box arms for. One move covers a box booting, a home moving in and a
|
|
39
|
-
// watcher born long after the box was up: she is never waiting for a round
|
|
40
|
-
// nobody scheduled.
|
|
41
|
-
async welcomed(id) {
|
|
42
|
-
await this.round(Date.now(), id);
|
|
43
|
-
}
|
|
44
|
-
// The arming went off. A moment still well ahead is the day's cap this
|
|
45
|
-
// class put on its own timeout, so it is armed again rather than fired: a
|
|
46
|
-
// terrain calls this and never has to know how far out the arming was.
|
|
47
|
-
async rang() {
|
|
48
|
-
if (this.#stopped)
|
|
49
|
-
return [];
|
|
50
|
-
if (this.#armed !== null && this.#armed - Date.now() > 1_000) {
|
|
51
|
-
this.arm(this.#armed);
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
return this.fire(Date.now());
|
|
55
|
-
}
|
|
56
|
-
// The moment, read here and handed on. A device's own code may call it at a
|
|
57
|
-
// moment it names, which is how a suite proves a wake without waiting for
|
|
58
|
-
// one and how an operator makes a round happen now.
|
|
59
|
-
fire(now) {
|
|
60
|
-
return this.round(now);
|
|
61
|
-
}
|
|
62
|
-
// One round, and never two: a second while one is in flight is handed the
|
|
63
|
-
// one already running. What every watcher answered decides the next
|
|
64
|
-
// arming, and a `due` that landed while the round ran is not lost.
|
|
65
|
-
round(now, only) {
|
|
66
|
-
if (this.#round)
|
|
67
|
-
return this.#round;
|
|
68
|
-
const mine = this.#run(now, only);
|
|
69
|
-
this.#round = mine;
|
|
70
|
-
return mine.finally(() => {
|
|
71
|
-
if (this.#round === mine)
|
|
72
|
-
this.#round = null;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
async #run(now, only) {
|
|
76
|
-
if (only === undefined)
|
|
77
|
-
this.#arm(null); // the arming is spent, whether it went off or the device called this by hand
|
|
78
|
-
const woke = await this.woke((w) => ({ ...w.args, now }), only);
|
|
79
|
-
if (woke === null) {
|
|
80
|
-
this.stop(); // her ward took her out; there is nobody left to wake
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
83
|
-
// A moment already gone is no arming. Every watcher works out when she is
|
|
84
|
-
// next due from the moment she was handed, so a round fired by hand at a
|
|
85
|
-
// moment that is not this box's answers one that is not this box's
|
|
86
|
-
// either, and a box that armed for it would run a round for a minute long
|
|
87
|
-
// past. A round at the box's own moment always names one still ahead.
|
|
88
|
-
const named = soonest(woke);
|
|
89
|
-
const said = named !== null && named > Date.now() ? named : null;
|
|
90
|
-
const held = this.#armed;
|
|
91
|
-
const at = said === null ? held : held === null ? moment(said) : Math.min(held, moment(said));
|
|
92
|
-
if (at !== null && at !== this.#armed)
|
|
93
|
-
this.#arm(at);
|
|
94
|
-
else if (at === null && this.#armed !== null)
|
|
95
|
-
this.#arm(null);
|
|
96
|
-
return woke;
|
|
97
|
-
}
|
|
98
|
-
#arm(when) {
|
|
99
|
-
if (this.#stopped)
|
|
100
|
-
return;
|
|
101
|
-
this.#armed = when;
|
|
102
|
-
this.arm(when);
|
|
103
|
-
}
|
|
104
|
-
// The one thing a terrain says about itself: be woken at this moment, or at
|
|
105
|
-
// no moment at all. A subclass replaces this and nothing else of her.
|
|
106
|
-
//
|
|
107
|
-
// Here it is a timeout, capped at a day and armed again when the cap goes
|
|
108
|
-
// off. The box stays up for its work and never for this, so the handle is
|
|
109
|
-
// unreferenced.
|
|
110
|
-
arm(when) {
|
|
111
|
-
clearTimeout(this.#timeout);
|
|
112
|
-
this.#timeout = undefined;
|
|
113
|
-
if (when === null)
|
|
114
|
-
return;
|
|
115
|
-
this.#timeout = setTimeout(() => void this.rang(), Math.min(Math.max(when - Date.now(), 0), DAY));
|
|
116
|
-
this.#timeout.unref?.();
|
|
117
|
-
}
|
|
118
|
-
// A being her ward took out reaches nobody, so nothing is left to wake. The
|
|
119
|
-
// device calls this when it puts the box down.
|
|
120
|
-
stop() {
|
|
121
|
-
this.#stopped = true;
|
|
122
|
-
this.#armed = null;
|
|
123
|
-
this.arm(null);
|
|
124
|
-
}
|
|
125
|
-
now() {
|
|
126
|
-
return { now: Date.now() };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// Half a second past the minute a moment falls in, and never before the
|
|
130
|
-
// moment itself: a watcher floors what she is handed, so a round that fired a
|
|
131
|
-
// hair early would floor to the minute before and fire nothing.
|
|
132
|
-
const moment = (at) => Math.max(at, Math.floor(at / MINUTE) * MINUTE + 500);
|
|
133
|
-
// What one round learned. Every watcher says when she is next due, and the
|
|
134
|
-
// box arms for the earliest of them. A watcher who named no moment has
|
|
135
|
-
// nothing to be woken for, and so has one who answered something that is no
|
|
136
|
-
// tally at all.
|
|
137
|
-
function soonest(woke) {
|
|
138
|
-
let at = null;
|
|
139
|
-
for (const w of woke) {
|
|
140
|
-
if (w.out === null || typeof w.out !== 'object')
|
|
141
|
-
continue;
|
|
142
|
-
const next = w.out.next;
|
|
143
|
-
if (typeof next !== 'number' || !Number.isFinite(next))
|
|
144
|
-
continue;
|
|
145
|
-
if (at === null || next < at)
|
|
146
|
-
at = next;
|
|
147
|
-
}
|
|
148
|
-
return at;
|
|
149
|
-
}
|
package/dock/console.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The box, as one being. A device has no surface of its own, so a box that
|
|
3
|
-
// has joined no world has nothing to draw: the Console is that page and only
|
|
4
|
-
// that. She carries the faculties her harbor stood, showing each one's asks
|
|
5
|
-
// as her own, and the ordinary screen draws her exactly as it draws a
|
|
6
|
-
// world's user being, through one avatar holding one standing.
|
|
7
|
-
//
|
|
8
|
-
// She exists because a fresh install otherwise asks for location and
|
|
9
|
-
// notifications in its manifest and shows nothing that ever asks for them,
|
|
10
|
-
// which is a store reviewer's first question and a human's second.
|
|
11
|
-
//
|
|
12
|
-
// She carries for whoever the harbor's root let in, and for nobody else: the
|
|
13
|
-
// root is the box, and it invites the one avatar its own screen runs.
|
|
14
|
-
import { Carrier } from '../beings/carry.ts';
|
|
15
|
-
import type { Asker, OccupantRecord } from 'nervur';
|
|
16
|
-
|
|
17
|
-
export const CONSOLE = 'console';
|
|
18
|
-
|
|
19
|
-
export class Console extends Carrier {
|
|
20
|
-
static override carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean {
|
|
21
|
-
return occupant !== undefined;
|
|
22
|
-
}
|
|
23
|
-
}
|
package/dock/dialers.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The box's dialers: `dialers`, one faculty of the dock ward, standing for
|
|
3
|
-
// the sockets this box holds open to other harbors' `quo.` routes. A Mac
|
|
4
|
-
// behind NAT, a phone on the mobile network, a droplet dialing home: each
|
|
5
|
-
// is a dialer, and what it dials is a row in her cells, placed once by the
|
|
6
|
-
// root with `keep` the way a schedule is placed on the clock and an agent
|
|
7
|
-
// on the agents faculty, in no file.
|
|
8
|
-
//
|
|
9
|
-
// A row is one URL, the `quo.` route of the harbor dialed. She holds the
|
|
10
|
-
// socket from her birth on, as every faculty holds what the box holds: one
|
|
11
|
-
// line per row, announcing this harbor's wards on open, binding the far
|
|
12
|
-
// side's pks, reconnecting with backoff when the line drops, and closed
|
|
13
|
-
// when the row is dropped or the box goes down. The dialer itself is the
|
|
14
|
-
// library's, written on the standard socket surface, so this faculty holds
|
|
15
|
-
// on every terrain that has a socket; the daemon hands her the harbor to
|
|
16
|
-
// dial for, as it hands the agents faculty how to run a row.
|
|
17
|
-
import { OWNER } from 'nervur';
|
|
18
|
-
import type { Asker, JsonObject, OccupantRecord } from 'nervur';
|
|
19
|
-
import { dial, type Dialer, type Harbor } from 'nervur/harbor';
|
|
20
|
-
import { Faculty } from './faculty.ts';
|
|
21
|
-
|
|
22
|
-
// The name the dock ward holds her under.
|
|
23
|
-
export const DIALERS = 'dialers';
|
|
24
|
-
|
|
25
|
-
export type Row = { url: string };
|
|
26
|
-
|
|
27
|
-
// The root and nobody else: a line held open is the device's own to place.
|
|
28
|
-
const root = (_occ: OccupantRecord | undefined, asker: Asker) => asker.id === OWNER;
|
|
29
|
-
|
|
30
|
-
const URL_ = { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] };
|
|
31
|
-
|
|
32
|
-
export class Dialers extends Faculty {
|
|
33
|
-
static override cells: JsonObject = { watched: {}, minted: 0, dialers: {} };
|
|
34
|
-
static override asks = {
|
|
35
|
-
keep: { description: 'hold a socket to that quo. route, from now and from every boot, until it is dropped', input: URL_, for: root },
|
|
36
|
-
drop: { description: 'close the socket to that route and forget it', input: URL_, for: root },
|
|
37
|
-
list: { description: 'every route she dials, and whether the line is open now', input: { type: 'object' }, for: root },
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
harbor: Harbor | undefined;
|
|
41
|
-
readonly open = new Map<string, Dialer>();
|
|
42
|
-
|
|
43
|
-
private get rows(): Record<string, Row> {
|
|
44
|
-
return this.cells.dialers as unknown as Record<string, Row>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Every row dialed, once the daemon has handed the harbor to dial for.
|
|
48
|
-
start(harbor: Harbor): void {
|
|
49
|
-
this.harbor = harbor;
|
|
50
|
-
for (const row of Object.values(this.rows)) this.#dial(row.url);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
#dial(url: string): void {
|
|
54
|
-
if (!this.harbor || this.open.has(url)) return;
|
|
55
|
-
this.open.set(url, dial(this.harbor, url));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
keep(args: JsonObject): JsonObject {
|
|
59
|
-
const url = args.url;
|
|
60
|
-
if (typeof url !== 'string' || !/^https?:\/\/\S+$/.test(url)) return { error: 'url is the quo. route of the harbor dialed, over http or https' };
|
|
61
|
-
const clean = url.replace(/\/$/, '');
|
|
62
|
-
this.rows[clean] = { url: clean };
|
|
63
|
-
this.#dial(clean);
|
|
64
|
-
return { kept: clean };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
drop(args: JsonObject): JsonObject {
|
|
68
|
-
const url = typeof args.url === 'string' ? args.url.replace(/\/$/, '') : '';
|
|
69
|
-
if (!this.rows[url]) return { error: 'no such route' };
|
|
70
|
-
this.open.get(url)?.close();
|
|
71
|
-
this.open.delete(url);
|
|
72
|
-
delete this.rows[url];
|
|
73
|
-
return { dropped: url };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
list(): JsonObject {
|
|
77
|
-
return { dialers: Object.keys(this.rows).map((url) => ({ url, open: this.open.get(url)?.socket !== null && this.open.has(url) })) };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// The device puts the box down: every line closes, the rows stay.
|
|
81
|
-
close(): void {
|
|
82
|
-
for (const d of this.open.values()) d.close();
|
|
83
|
-
this.open.clear();
|
|
84
|
-
}
|
|
85
|
-
}
|