@nervur-org/dock 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -15
- package/api/route.ts +4 -72
- package/beings/avatar.ts +11 -7
- package/beings/carry.ts +129 -116
- package/beings/clock.ts +11 -7
- package/beings/desk.ts +7 -6
- package/beings/doorbell.ts +36 -7
- package/beings/index.ts +17 -4
- package/beings/join.ts +7 -4
- package/beings/link.ts +1 -1
- package/beings/porter.ts +5 -1
- package/beings/presence.ts +15 -5
- package/beings/setup.ts +27 -21
- package/beings/user.ts +26 -17
- package/beings/way.ts +1 -1
- package/{dock → box}/agents.ts +37 -44
- package/{dock → box}/apns.ts +4 -4
- package/box/box.ts +31 -0
- package/box/contract.ts +41 -0
- package/box/dialers.ts +88 -0
- package/box/dock.ts +148 -0
- package/box/envoy.ts +96 -0
- package/box/faculty.ts +111 -0
- package/{dock → box}/fcm.ts +1 -1
- package/box/grants.ts +90 -0
- package/box/http.ts +93 -0
- package/{dock → box}/http2.ts +7 -7
- package/box/index.ts +201 -0
- package/box/join.ts +78 -0
- package/box/local.ts +37 -0
- package/{dock → box}/location.ts +15 -3
- package/{dock → box}/notifications.ts +17 -5
- package/box/push.ts +85 -0
- package/box/ring.ts +85 -0
- package/{dock → box}/service.ts +2 -2
- package/box/timer.ts +150 -0
- package/box/tree.ts +22 -0
- package/{dock → box}/webpush.ts +2 -2
- package/cli/client.ts +1 -1
- package/cli/daemon.ts +101 -103
- package/cli/estate.ts +2 -3
- package/cli/nervur.ts +44 -25
- package/cli/stand.ts +1 -1
- package/dist/api/route.d.ts +1 -2
- package/dist/api/route.js +4 -76
- package/dist/beings/avatar.d.ts +4 -3
- package/dist/beings/avatar.js +9 -6
- package/dist/beings/carry.d.ts +10 -12
- package/dist/beings/carry.js +119 -127
- package/dist/beings/clock.js +10 -7
- package/dist/beings/desk.js +8 -7
- package/dist/beings/doorbell.d.ts +3 -1
- package/dist/beings/doorbell.js +37 -4
- package/dist/beings/index.d.ts +24 -4
- package/dist/beings/index.js +15 -4
- package/dist/beings/join.d.ts +1 -0
- package/dist/beings/join.js +6 -4
- package/dist/beings/link.d.ts +0 -1
- package/dist/beings/link.js +1 -1
- package/dist/beings/porter.js +5 -1
- package/dist/beings/presence.d.ts +1 -0
- package/dist/beings/presence.js +12 -3
- package/dist/beings/setup.d.ts +5 -2
- package/dist/beings/setup.js +16 -15
- package/dist/beings/user.d.ts +4 -3
- package/dist/beings/user.js +18 -11
- package/dist/{dock → box}/agents.d.ts +7 -4
- package/dist/{dock → box}/agents.js +30 -37
- package/dist/{dock → box}/apns.d.ts +1 -1
- package/dist/{dock → box}/apns.js +3 -3
- package/dist/box/box.d.ts +9 -0
- package/dist/box/box.js +18 -0
- package/dist/box/contract.d.ts +6 -0
- package/dist/box/contract.js +35 -0
- package/dist/{dock → box}/dialers.d.ts +7 -4
- package/dist/box/dialers.js +76 -0
- package/dist/box/dock.d.ts +182 -0
- package/dist/box/dock.js +128 -0
- package/dist/box/envoy.d.ts +44 -0
- package/dist/box/envoy.js +75 -0
- package/dist/box/faculty.d.ts +68 -0
- package/dist/box/faculty.js +97 -0
- package/dist/{dock → box}/fcm.d.ts +1 -1
- package/dist/{dock → box}/grants.d.ts +4 -3
- package/dist/box/grants.js +78 -0
- package/dist/box/http.d.ts +26 -0
- package/dist/box/http.js +89 -0
- package/dist/{dock → box}/http2.d.ts +1 -1
- package/dist/{dock → box}/http2.js +6 -6
- package/dist/box/index.d.ts +46 -0
- package/dist/box/index.js +179 -0
- package/dist/box/join.d.ts +19 -0
- package/dist/box/join.js +67 -0
- package/dist/{dock → box}/local.d.ts +3 -2
- package/dist/box/local.js +27 -0
- package/dist/{dock → box}/location.d.ts +3 -0
- package/dist/box/location.js +21 -0
- package/dist/{dock → box}/notifications.d.ts +3 -0
- package/dist/{dock → box}/notifications.js +14 -4
- package/dist/{dock → box}/push.d.ts +12 -18
- package/dist/box/push.js +83 -0
- package/dist/box/ring.d.ts +18 -0
- package/dist/box/ring.js +55 -0
- package/dist/{dock → box}/service.d.ts +1 -1
- package/dist/{dock → box}/service.js +1 -1
- package/dist/{dock → box}/timer.d.ts +2 -0
- package/dist/box/timer.js +127 -0
- package/dist/box/tree.d.ts +5 -0
- package/dist/box/tree.js +14 -0
- package/dist/{dock → box}/webpush.d.ts +1 -1
- package/dist/cli/client.d.ts +1 -0
- package/dist/cli/daemon.d.ts +2 -8
- package/dist/cli/daemon.js +102 -107
- package/dist/cli/estate.js +2 -3
- package/dist/cli/nervur.js +44 -24
- package/dist/cli/stand.d.ts +0 -14
- package/dist/cli/stand.js +1 -1
- package/dist/harbor/browser.d.ts +8 -5
- package/dist/harbor/browser.js +20 -21
- package/dist/harbor/capacitor.d.ts +3 -3
- package/dist/harbor/capacitor.js +9 -8
- package/dist/harbor/disk.d.ts +9 -9
- package/dist/harbor/disk.js +56 -51
- package/dist/harbor/edge/alarm.d.ts +4 -2
- package/dist/harbor/edge/alarm.js +9 -19
- package/dist/harbor/edge/edge.d.ts +7 -6
- package/dist/harbor/edge/edge.js +58 -53
- package/dist/harbor/edge/exercise.js +4 -4
- package/dist/harbor/edge/given.d.ts +0 -1
- package/dist/harbor/edge/given.js +0 -29
- package/dist/harbor/edge/storage.d.ts +0 -2
- package/dist/harbor/edge/storage.js +2 -2
- package/dist/harbor/http.js +4 -2
- package/dist/harbor/sealed.d.ts +0 -5
- package/dist/harbor/sealed.js +0 -39
- package/dist/harbor/tauri.d.ts +2 -2
- package/dist/harbor/tauri.js +13 -10
- package/dist/human/app.d.ts +1 -1
- package/dist/human/app.js +10 -12
- package/dist/human/door.d.ts +0 -1
- package/dist/human/door.js +1 -1
- package/dist/human/html.js +1 -1
- package/dist/human/move.js +1 -1
- package/dist/human/tab.bundle.txt +3629 -2824
- package/dist/human/wake.d.ts +5 -2
- package/dist/human/wake.js +23 -14
- package/dist/human/web.d.ts +0 -1
- package/dist/human/web.js +3 -3
- package/dist/human/worker.bundle.txt +2945 -2118
- package/dist/human/worlds.js +3 -3
- package/dist/mcp/agent.d.ts +0 -1
- package/dist/mcp/agent.js +1 -1
- package/dist/mcp/allow.js +3 -2
- package/dist/mcp/http.d.ts +0 -1
- package/dist/mcp/http.js +1 -1
- package/dist/mcp/oauth.d.ts +3 -4
- package/dist/mcp/oauth.js +4 -4
- package/dist/mcp/route.d.ts +1 -1
- package/dist/mcp/runner.d.ts +0 -1
- package/dist/mcp/runner.js +1 -1
- package/dist/mcp/server.d.ts +0 -1
- package/dist/mcp/server.js +1 -1
- package/harbor/browser.ts +22 -24
- package/harbor/capacitor.ts +9 -8
- package/harbor/disk.ts +53 -48
- package/harbor/edge/alarm.ts +15 -12
- package/harbor/edge/edge.ts +61 -55
- package/harbor/edge/exercise.ts +4 -4
- package/harbor/edge/given.ts +3 -33
- package/harbor/edge/storage.ts +2 -2
- package/harbor/http.ts +5 -3
- package/harbor/sealed.ts +0 -35
- package/harbor/tauri.ts +13 -10
- package/human/app.ts +12 -13
- package/human/door.ts +1 -1
- package/human/html.ts +1 -1
- package/human/local.ts +1 -1
- package/human/move.ts +1 -1
- package/human/tab.bundle.txt +3629 -2824
- package/human/wake.ts +36 -16
- package/human/web.ts +4 -4
- package/human/worker.bundle.txt +2945 -2118
- package/human/worlds.ts +3 -3
- package/mcp/agent.ts +1 -1
- package/mcp/allow.ts +2 -1
- package/mcp/http.ts +1 -1
- package/mcp/oauth.ts +4 -4
- package/mcp/route.ts +1 -1
- package/mcp/runner.ts +1 -1
- package/mcp/server.ts +1 -1
- package/package.json +10 -18
- package/GETTING_STARTED.md +0 -138
- package/GLOSSARY.md +0 -118
- package/WORLDS.md +0 -569
- package/beings/envoy.ts +0 -218
- package/dist/beings/envoy.d.ts +0 -54
- package/dist/beings/envoy.js +0 -211
- package/dist/dock/console.d.ts +0 -6
- package/dist/dock/console.js +0 -20
- package/dist/dock/dialers.js +0 -75
- package/dist/dock/faculty.d.ts +0 -24
- package/dist/dock/faculty.js +0 -142
- package/dist/dock/grants.js +0 -99
- package/dist/dock/index.d.ts +0 -41
- package/dist/dock/index.js +0 -113
- package/dist/dock/local.js +0 -44
- package/dist/dock/location.js +0 -11
- package/dist/dock/push.js +0 -132
- package/dist/dock/timer.js +0 -149
- package/dock/console.ts +0 -23
- package/dock/dialers.ts +0 -85
- package/dock/faculty.ts +0 -148
- package/dock/grants.ts +0 -111
- package/dock/index.ts +0 -133
- package/dock/local.ts +0 -48
- package/dock/push.ts +0 -161
- package/dock/timer.ts +0 -174
- package/quo-api.md +0 -196
- package/quo-app-bells.md +0 -280
- package/quo-dock.md +0 -1945
- package/quo-harbor.md +0 -874
- package/quo-human.md +0 -1042
- package/quo-mcp.md +0 -343
- /package/dist/{dock → box}/fcm.js +0 -0
- /package/dist/{dock → box}/webpush.js +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Box } from 'nervur/harbor';
|
|
2
|
+
import type { Asker, JsonObject, OccupantRecord, Reply } from 'nervur';
|
|
3
|
+
export { BOX } from 'nervur/harbor';
|
|
4
|
+
declare const root: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
5
|
+
export type Routes = {
|
|
6
|
+
mcp: string;
|
|
7
|
+
web: string;
|
|
8
|
+
quo?: string;
|
|
9
|
+
api?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const DOMAIN: RegExp;
|
|
12
|
+
export declare function routesFor(domain: string): Required<Routes>;
|
|
13
|
+
export type Apps = {
|
|
14
|
+
apple: string[];
|
|
15
|
+
android: {
|
|
16
|
+
package: string;
|
|
17
|
+
fingerprints: string[];
|
|
18
|
+
}[];
|
|
19
|
+
};
|
|
20
|
+
export type BoxCells = {
|
|
21
|
+
routes: Routes | null;
|
|
22
|
+
apps: Apps | null;
|
|
23
|
+
default: string | null;
|
|
24
|
+
};
|
|
25
|
+
export declare class Dock extends Box {
|
|
26
|
+
#private;
|
|
27
|
+
static carries(occupant: OccupantRecord | undefined): boolean;
|
|
28
|
+
static skips(id: string): boolean;
|
|
29
|
+
static cells: JsonObject;
|
|
30
|
+
static asks: {
|
|
31
|
+
offer: {
|
|
32
|
+
description: string;
|
|
33
|
+
input: {
|
|
34
|
+
type: string;
|
|
35
|
+
properties: {
|
|
36
|
+
name: {
|
|
37
|
+
type: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
required: string[];
|
|
41
|
+
};
|
|
42
|
+
for: (occ: OccupantRecord | undefined) => boolean;
|
|
43
|
+
};
|
|
44
|
+
retract: {
|
|
45
|
+
description: string;
|
|
46
|
+
input: {
|
|
47
|
+
type: string;
|
|
48
|
+
properties: {
|
|
49
|
+
name: {
|
|
50
|
+
type: string;
|
|
51
|
+
};
|
|
52
|
+
heir: {
|
|
53
|
+
type: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
required: string[];
|
|
57
|
+
};
|
|
58
|
+
for: (occ: OccupantRecord | undefined) => boolean;
|
|
59
|
+
};
|
|
60
|
+
host: {
|
|
61
|
+
description: string;
|
|
62
|
+
input: {
|
|
63
|
+
type: string;
|
|
64
|
+
properties: {
|
|
65
|
+
seed: {
|
|
66
|
+
type: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
required: string[];
|
|
70
|
+
};
|
|
71
|
+
for: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
72
|
+
};
|
|
73
|
+
roll: {
|
|
74
|
+
description: string;
|
|
75
|
+
input: {
|
|
76
|
+
type: string;
|
|
77
|
+
};
|
|
78
|
+
for: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
79
|
+
};
|
|
80
|
+
box: {
|
|
81
|
+
description: string;
|
|
82
|
+
input: {
|
|
83
|
+
type: string;
|
|
84
|
+
};
|
|
85
|
+
for: typeof root;
|
|
86
|
+
};
|
|
87
|
+
domain: {
|
|
88
|
+
description: string;
|
|
89
|
+
input: {
|
|
90
|
+
type: string;
|
|
91
|
+
properties: {
|
|
92
|
+
domain: {
|
|
93
|
+
type: string;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
required: string[];
|
|
97
|
+
};
|
|
98
|
+
for: typeof root;
|
|
99
|
+
};
|
|
100
|
+
apps: {
|
|
101
|
+
description: string;
|
|
102
|
+
input: {
|
|
103
|
+
type: string;
|
|
104
|
+
properties: {
|
|
105
|
+
apple: {
|
|
106
|
+
type: string;
|
|
107
|
+
items: {
|
|
108
|
+
type: string;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
android: {
|
|
112
|
+
type: string;
|
|
113
|
+
items: {
|
|
114
|
+
type: string;
|
|
115
|
+
properties: {
|
|
116
|
+
package: {
|
|
117
|
+
type: string;
|
|
118
|
+
};
|
|
119
|
+
fingerprints: {
|
|
120
|
+
type: string;
|
|
121
|
+
items: {
|
|
122
|
+
type: string;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
required: string[];
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
for: typeof root;
|
|
132
|
+
};
|
|
133
|
+
default: {
|
|
134
|
+
description: string;
|
|
135
|
+
input: {
|
|
136
|
+
type: string;
|
|
137
|
+
properties: {
|
|
138
|
+
ward: {
|
|
139
|
+
type: string;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
required: string[];
|
|
143
|
+
};
|
|
144
|
+
for: typeof root;
|
|
145
|
+
};
|
|
146
|
+
lends: {
|
|
147
|
+
description: string;
|
|
148
|
+
input: {
|
|
149
|
+
type: string;
|
|
150
|
+
};
|
|
151
|
+
for: typeof root;
|
|
152
|
+
};
|
|
153
|
+
stood: {
|
|
154
|
+
description: string;
|
|
155
|
+
input: {
|
|
156
|
+
type: string;
|
|
157
|
+
properties: {
|
|
158
|
+
lends: {
|
|
159
|
+
type: string;
|
|
160
|
+
};
|
|
161
|
+
titles: {
|
|
162
|
+
type: string;
|
|
163
|
+
};
|
|
164
|
+
clashes: {
|
|
165
|
+
type: string;
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
required: string[];
|
|
169
|
+
};
|
|
170
|
+
for: typeof root;
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
|
|
174
|
+
offer(args: JsonObject): Promise<JsonObject>;
|
|
175
|
+
retract(args: JsonObject): Promise<JsonObject>;
|
|
176
|
+
lends(): JsonObject;
|
|
177
|
+
box(): JsonObject;
|
|
178
|
+
domain(args: JsonObject): JsonObject;
|
|
179
|
+
apps(args: JsonObject): JsonObject;
|
|
180
|
+
default(args: JsonObject): JsonObject;
|
|
181
|
+
stood(args: JsonObject): JsonObject;
|
|
182
|
+
}
|
package/dist/box/dock.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The box ward's own being, and so the harbor's. She is the kit's Box, the
|
|
3
|
+
// box as one standing that every hosted ward holds, and two things more.
|
|
4
|
+
//
|
|
5
|
+
// She lends by contract: a being asks her for a contract's digest, and she
|
|
6
|
+
// holds each faculty under its key, so her cells keep the index from digest
|
|
7
|
+
// to key, written whole by the root at every boot from what this run's
|
|
8
|
+
// classes say. A name she is asked to offer is a digest first and a key
|
|
9
|
+
// second, and then it is the kit's offer.
|
|
10
|
+
//
|
|
11
|
+
// And she carries: she holds a standing at every faculty her harbor stood
|
|
12
|
+
// and shows their asks as her own to whoever the root let in, so the one
|
|
13
|
+
// screen a device has with no world joined draws her the way it draws any
|
|
14
|
+
// being.
|
|
15
|
+
import { OWNER } from 'nervur';
|
|
16
|
+
import { Box, BOX_SEED } from 'nervur/harbor';
|
|
17
|
+
import { carry } from '../beings/carry.js';
|
|
18
|
+
export { BOX } from 'nervur/harbor';
|
|
19
|
+
const root = (_occ, asker) => asker.id === OWNER;
|
|
20
|
+
export const DOMAIN = /^[a-z0-9.-]+\.[a-z]{2,}$/i;
|
|
21
|
+
export function routesFor(domain) {
|
|
22
|
+
if (!DOMAIN.test(domain))
|
|
23
|
+
throw new Error(`${domain} is not a domain`);
|
|
24
|
+
return { mcp: `https://mcp.${domain}`, web: `https://web.${domain}`, quo: `https://quo.${domain}`, api: `https://api.${domain}` };
|
|
25
|
+
}
|
|
26
|
+
const APPS = { type: 'object', properties: { apple: { type: 'array', items: { type: 'string' } }, android: { type: 'array', items: { type: 'object', properties: { package: { type: 'string' }, fingerprints: { type: 'array', items: { type: 'string' } } }, required: ['package', 'fingerprints'] } } } };
|
|
27
|
+
export class Dock extends Box {
|
|
28
|
+
static carries(occupant) {
|
|
29
|
+
return occupant !== undefined;
|
|
30
|
+
}
|
|
31
|
+
// She holds every faculty twice: under its key, which is the lend seam
|
|
32
|
+
// and plumbing, and under the word the first contract it is lent under
|
|
33
|
+
// calls itself, which is the section a screen shows. A key is a path
|
|
34
|
+
// through the class tree and always carries a slash; a title never does.
|
|
35
|
+
// Both are this run's: `stood` writes the index whole and puts down any
|
|
36
|
+
// title standing this run does not say.
|
|
37
|
+
static skips(id) {
|
|
38
|
+
return id.includes('/');
|
|
39
|
+
}
|
|
40
|
+
static cells = { lends: {}, titles: {}, clashes: {}, hosted: [], routes: null, apps: null, default: null };
|
|
41
|
+
static asks = {
|
|
42
|
+
...Box.asks,
|
|
43
|
+
box: { description: 'what this box is: its routes, its apps and its default ward, each null until said', input: { type: 'object' }, for: root },
|
|
44
|
+
domain: { description: 'the four routes by the convention, mcp., web., quo. and api. under this domain over https, kept until said again', input: { type: 'object', properties: { domain: { type: 'string' } }, required: ['domain'] }, for: root },
|
|
45
|
+
apps: { description: 'the apps this box’s links open, apple app ids and android packages with their fingerprints; neither named forgets them', input: APPS, for: root },
|
|
46
|
+
default: { description: 'the ward a caller means when naming none; the harbor holds it to the wards it hosts at boot', input: { type: 'object', properties: { ward: { type: 'string' } }, required: ['ward'] }, for: root },
|
|
47
|
+
lends: { description: 'which faculty is lent under each contract, by digest to key, and which contracts are lent to nobody because two classes clashed under them', input: { type: 'object' }, for: root },
|
|
48
|
+
stood: {
|
|
49
|
+
description: 'what this boot stood: each contract, by digest, to the faculty that fulfils it, each title to the faculty shown under it, and each clash to the keys that clashed; a title held that this does not say goes',
|
|
50
|
+
input: { type: 'object', properties: { lends: { type: 'object' }, titles: { type: 'object' }, clashes: { type: 'object' } }, required: ['lends', 'titles', 'clashes'] },
|
|
51
|
+
for: root,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
answer(asker, method, args = {}) {
|
|
55
|
+
return carry(this, asker, method, args, (a, m, g) => super.answer(a, m, g));
|
|
56
|
+
}
|
|
57
|
+
#key(name) {
|
|
58
|
+
const index = this.cells.lends;
|
|
59
|
+
return Object.hasOwn(index, name) ? index[name] : name;
|
|
60
|
+
}
|
|
61
|
+
offer(args) {
|
|
62
|
+
return super.offer({ name: this.#key(args.name) });
|
|
63
|
+
}
|
|
64
|
+
retract(args) {
|
|
65
|
+
return super.retract({ ...args, name: this.#key(args.name) });
|
|
66
|
+
}
|
|
67
|
+
lends() {
|
|
68
|
+
return { lends: this.cells.lends, clashes: this.cells.clashes ?? {} };
|
|
69
|
+
}
|
|
70
|
+
// What the box is, placed by the root and read by the harbor: nothing here
|
|
71
|
+
// comes from a file.
|
|
72
|
+
box() {
|
|
73
|
+
return { routes: this.cells.routes ?? null, apps: this.cells.apps ?? null, default: this.cells.default ?? null };
|
|
74
|
+
}
|
|
75
|
+
domain(args) {
|
|
76
|
+
if (typeof args.domain !== 'string' || !DOMAIN.test(args.domain))
|
|
77
|
+
return { error: `${JSON.stringify(args.domain)} is not a domain` };
|
|
78
|
+
this.cells.routes = routesFor(args.domain);
|
|
79
|
+
return { routes: this.cells.routes };
|
|
80
|
+
}
|
|
81
|
+
apps(args) {
|
|
82
|
+
const apple = Array.isArray(args.apple) ? args.apple.filter((x) => typeof x === 'string') : [];
|
|
83
|
+
const android = Array.isArray(args.android)
|
|
84
|
+
? args.android
|
|
85
|
+
.filter((a) => typeof a?.package === 'string' && Array.isArray(a.fingerprints))
|
|
86
|
+
.map((a) => ({ package: a.package, fingerprints: a.fingerprints.filter((f) => typeof f === 'string') }))
|
|
87
|
+
: [];
|
|
88
|
+
this.cells.apps = apple.length === 0 && android.length === 0 ? null : { apple, android };
|
|
89
|
+
return { apps: this.cells.apps };
|
|
90
|
+
}
|
|
91
|
+
default(args) {
|
|
92
|
+
if (typeof args.ward !== 'string' || args.ward === '' || args.ward === BOX_SEED)
|
|
93
|
+
return { error: 'a ward is a name, and the box ward is never the default' };
|
|
94
|
+
this.cells.default = args.ward;
|
|
95
|
+
return { default: args.ward };
|
|
96
|
+
}
|
|
97
|
+
// The index, written whole at every boot: what stands is what this run's
|
|
98
|
+
// classes say, and nothing an earlier run said, the clashes with it, by
|
|
99
|
+
// the word the contract calls itself to the keys that clashed. Her title
|
|
100
|
+
// standings are held to it: one under a title this run shows nobody
|
|
101
|
+
// under, or that now names another faculty, goes here, and the root
|
|
102
|
+
// removes the far end. She answers what she holds and what went, so the
|
|
103
|
+
// root places only what is not there.
|
|
104
|
+
stood(args) {
|
|
105
|
+
const { lends, titles, clashes } = args;
|
|
106
|
+
if (!isIndex(lends))
|
|
107
|
+
return { error: 'lends is a digest to a key' };
|
|
108
|
+
if (!isIndex(titles))
|
|
109
|
+
return { error: 'titles is a title to a key' };
|
|
110
|
+
if (clashes === null || typeof clashes !== 'object' || Array.isArray(clashes) || !Object.values(clashes).every((ks) => Array.isArray(ks) && ks.every((k) => typeof k === 'string')))
|
|
111
|
+
return { error: 'clashes is a word to keys' };
|
|
112
|
+
this.cells.clashes = clashes;
|
|
113
|
+
const was = (this.cells.titles ?? {});
|
|
114
|
+
const pruned = [];
|
|
115
|
+
for (const id of Object.keys(this.cells.standings)) {
|
|
116
|
+
if (id.includes('/'))
|
|
117
|
+
continue;
|
|
118
|
+
if (Object.hasOwn(titles, id) && was[id] === titles[id])
|
|
119
|
+
continue;
|
|
120
|
+
this.standings.remove(id);
|
|
121
|
+
pruned.push(id);
|
|
122
|
+
}
|
|
123
|
+
this.cells.lends = lends;
|
|
124
|
+
this.cells.titles = titles;
|
|
125
|
+
return { lends, held: Object.keys(this.cells.standings), pruned };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const isIndex = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && Object.values(v).every((k) => typeof k === 'string');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Asker, JsonObject } from 'nervur';
|
|
2
|
+
import { Faculty } from './faculty.ts';
|
|
3
|
+
export declare const WATCH_ACCOUNT: {
|
|
4
|
+
description: string;
|
|
5
|
+
input: {
|
|
6
|
+
type: string;
|
|
7
|
+
properties: {
|
|
8
|
+
invitation: {
|
|
9
|
+
type: string;
|
|
10
|
+
};
|
|
11
|
+
method: {
|
|
12
|
+
type: string;
|
|
13
|
+
};
|
|
14
|
+
args: {
|
|
15
|
+
type: string;
|
|
16
|
+
};
|
|
17
|
+
account: {
|
|
18
|
+
type: string;
|
|
19
|
+
};
|
|
20
|
+
secret: {
|
|
21
|
+
type: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
required: string[];
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export type Delivery = {
|
|
28
|
+
id: string;
|
|
29
|
+
method: string;
|
|
30
|
+
args: JsonObject;
|
|
31
|
+
};
|
|
32
|
+
export declare class EnvoyFaculty extends Faculty {
|
|
33
|
+
#private;
|
|
34
|
+
static cells: JsonObject;
|
|
35
|
+
watch(args: JsonObject, asker: Asker): Promise<JsonObject>;
|
|
36
|
+
get waking(): Promise<void>;
|
|
37
|
+
inbound(args: JsonObject): Promise<JsonObject>;
|
|
38
|
+
protected account(_body: string, _headers: Record<string, string>): string | null;
|
|
39
|
+
protected verify(_body: string, _headers: Record<string, string>, _secret: string): Promise<boolean> | boolean;
|
|
40
|
+
protected decode(_body: string, _headers: Record<string, string>): Delivery | {
|
|
41
|
+
error: string;
|
|
42
|
+
};
|
|
43
|
+
protected hmac(secret: string, text: string): Promise<string>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Faculty, WATCH } from './faculty.js';
|
|
2
|
+
// A watch for an account: the invitation back, the ask to wake her with,
|
|
3
|
+
// her account at the vendor and the secret the vendor signs with.
|
|
4
|
+
export const WATCH_ACCOUNT = {
|
|
5
|
+
description: 'wake me for my account: an invitation back, the ask to wake me with, the account id and its signing secret',
|
|
6
|
+
input: { type: 'object', properties: { ...WATCH.input.properties, account: { type: 'string' }, secret: { type: 'string' } }, required: ['invitation', 'method', 'account', 'secret'] },
|
|
7
|
+
};
|
|
8
|
+
const SEEN = 200;
|
|
9
|
+
export class EnvoyFaculty extends Faculty {
|
|
10
|
+
static cells = { watched: {}, minted: 0, seen: [] };
|
|
11
|
+
async watch(args, asker) {
|
|
12
|
+
if (typeof args.account !== 'string' || !args.account || typeof args.secret !== 'string' || !args.secret)
|
|
13
|
+
return { error: 'an account and its secret' };
|
|
14
|
+
const out = await super.watch(args, asker);
|
|
15
|
+
const id = out.watching;
|
|
16
|
+
if (typeof id === 'string')
|
|
17
|
+
Object.assign(this.cells.watched[id], { account: args.account, secret: args.secret });
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
#waking = Promise.resolve();
|
|
21
|
+
// The wake that went out after the last `{ ok }`, for the device's own
|
|
22
|
+
// code and a suite.
|
|
23
|
+
get waking() {
|
|
24
|
+
return this.#waking;
|
|
25
|
+
}
|
|
26
|
+
// The vendor's bytes, as they arrived on a path she claims, handed by the
|
|
27
|
+
// http faculty; nothing in them is trusted before `verify`.
|
|
28
|
+
async inbound(args) {
|
|
29
|
+
const body = typeof args.body === 'string' ? args.body : '';
|
|
30
|
+
const headers = (args.headers !== null && typeof args.headers === 'object' && !Array.isArray(args.headers) ? args.headers : {});
|
|
31
|
+
const account = this.account(body, headers);
|
|
32
|
+
const watched = this.cells.watched;
|
|
33
|
+
const mine = account === null ? [] : Object.entries(watched).filter(([, w]) => w.account === account);
|
|
34
|
+
if (mine.length === 0)
|
|
35
|
+
return { error: 'nobody watches that account' };
|
|
36
|
+
// every watcher of the account whose secret the bytes were signed with
|
|
37
|
+
const at = [];
|
|
38
|
+
for (const [id, w] of mine)
|
|
39
|
+
if (await this.verify(body, headers, w.secret))
|
|
40
|
+
at.push(id);
|
|
41
|
+
if (at.length === 0)
|
|
42
|
+
return { error: 'bad signature' };
|
|
43
|
+
const d = this.decode(body, headers);
|
|
44
|
+
if ('error' in d)
|
|
45
|
+
return d;
|
|
46
|
+
const seen = this.cells.seen;
|
|
47
|
+
if (seen.includes(d.id))
|
|
48
|
+
return { ok: true };
|
|
49
|
+
seen.push(d.id);
|
|
50
|
+
while (seen.length > SEEN)
|
|
51
|
+
seen.shift();
|
|
52
|
+
this.#waking = (async () => {
|
|
53
|
+
for (const id of at)
|
|
54
|
+
await this.standings[id]?.ask(d.method, d.args);
|
|
55
|
+
})();
|
|
56
|
+
return { ok: true };
|
|
57
|
+
}
|
|
58
|
+
// Which account the bytes are for: the one thing read before they are
|
|
59
|
+
// verified, since it names whose secret verifies them.
|
|
60
|
+
account(_body, _headers) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
verify(_body, _headers, _secret) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
decode(_body, _headers) {
|
|
67
|
+
return { error: 'this faculty is not called in' };
|
|
68
|
+
}
|
|
69
|
+
// A signature over text with a shared secret, hex: the shape most vendors
|
|
70
|
+
// sign in, here once so a leaf writes only the vendor's framing.
|
|
71
|
+
async hmac(secret, text) {
|
|
72
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
73
|
+
return [...new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text)))].map((x) => x.toString(16).padStart(2, '0')).join('');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Lent } from 'nervur';
|
|
2
|
+
import type { Asker, JsonObject, OccupantRecord, Stance } from 'nervur';
|
|
3
|
+
import type { Contract } from './contract.ts';
|
|
4
|
+
import { type Harborage } from './box.ts';
|
|
5
|
+
export type Watched = {
|
|
6
|
+
by: string;
|
|
7
|
+
method: string;
|
|
8
|
+
args: JsonObject;
|
|
9
|
+
};
|
|
10
|
+
export type Woke = {
|
|
11
|
+
id: string;
|
|
12
|
+
by: string;
|
|
13
|
+
out: unknown;
|
|
14
|
+
};
|
|
15
|
+
export declare const HTTP = "http:";
|
|
16
|
+
export declare const INBOUND: {
|
|
17
|
+
description: string;
|
|
18
|
+
input: {
|
|
19
|
+
type: string;
|
|
20
|
+
properties: {
|
|
21
|
+
body: {
|
|
22
|
+
type: string;
|
|
23
|
+
};
|
|
24
|
+
headers: {
|
|
25
|
+
type: string;
|
|
26
|
+
};
|
|
27
|
+
query: {
|
|
28
|
+
type: string;
|
|
29
|
+
};
|
|
30
|
+
method: {
|
|
31
|
+
type: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
required: string[];
|
|
35
|
+
};
|
|
36
|
+
for: (occ: OccupantRecord | undefined) => boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare const WATCH: {
|
|
39
|
+
description: string;
|
|
40
|
+
input: {
|
|
41
|
+
type: string;
|
|
42
|
+
properties: {
|
|
43
|
+
invitation: {
|
|
44
|
+
type: string;
|
|
45
|
+
};
|
|
46
|
+
method: {
|
|
47
|
+
type: string;
|
|
48
|
+
};
|
|
49
|
+
args: {
|
|
50
|
+
type: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
required: string[];
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export declare class Faculty extends Lent {
|
|
57
|
+
static cells: JsonObject;
|
|
58
|
+
static fulfils: Contract[];
|
|
59
|
+
static stands(): boolean;
|
|
60
|
+
static claims: string[];
|
|
61
|
+
readonly box: Harborage;
|
|
62
|
+
constructor(stance: Stance, box?: Harborage);
|
|
63
|
+
inbound(_args: JsonObject): Promise<JsonObject> | JsonObject;
|
|
64
|
+
watch(args: JsonObject, asker: Asker): Promise<JsonObject>;
|
|
65
|
+
protected welcomed(_id: string): Promise<void>;
|
|
66
|
+
protected woke(round?: (w: Watched) => JsonObject, only?: string): Promise<Woke[] | null>;
|
|
67
|
+
get watching(): Record<string, Watched>;
|
|
68
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A faculty: a being of the box ward standing for one thing outside every
|
|
3
|
+
// being. She says everything about herself on her class: the contracts she
|
|
4
|
+
// fulfils, whether she can work on this runtime, and the paths she claims on
|
|
5
|
+
// the box's wire. She is born holding her harbor's box. What every faculty
|
|
6
|
+
// shares is here: `watch`, the one way a being asks to be woken, and
|
|
7
|
+
// `woke`, one round of waking whoever asked.
|
|
8
|
+
import { Lent, isInvitation, isSilence, isWord, wordOf } from 'nervur';
|
|
9
|
+
import { ALONE } from './box.js';
|
|
10
|
+
// The ask the http faculty makes of a faculty that claims a path, and only
|
|
11
|
+
// the http faculty: the root places her one occupant per claimed path,
|
|
12
|
+
// `http:<path>`.
|
|
13
|
+
export const HTTP = 'http:';
|
|
14
|
+
export const INBOUND = {
|
|
15
|
+
description: 'bytes that arrived on a path this faculty claims',
|
|
16
|
+
input: { type: 'object', properties: { body: { type: 'string' }, headers: { type: 'object' }, query: { type: 'string' }, method: { type: 'string' } }, required: ['body', 'headers'] },
|
|
17
|
+
for: (occ) => occ?.id.startsWith(HTTP) === true,
|
|
18
|
+
};
|
|
19
|
+
export const WATCH = {
|
|
20
|
+
description: 'wake me: an invitation back, the ask to wake me with, and what to add to it',
|
|
21
|
+
input: { type: 'object', properties: { invitation: { type: 'object' }, method: { type: 'string' }, args: { type: 'object' } }, required: ['invitation', 'method'] },
|
|
22
|
+
};
|
|
23
|
+
export class Faculty extends Lent {
|
|
24
|
+
static cells = { watched: {}, minted: 0 };
|
|
25
|
+
// The contracts she fulfils; the box lends her under each one's digest.
|
|
26
|
+
static fulfils = [];
|
|
27
|
+
// Whether she can work on the runtime reading this, answered honestly.
|
|
28
|
+
// One that cannot is never booted and never lent.
|
|
29
|
+
static stands() {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
// The paths on the box's wire she answers. The http faculty hands her the
|
|
33
|
+
// bytes that arrive on one as `inbound`, and nobody else may ask it.
|
|
34
|
+
static claims = [];
|
|
35
|
+
// What her harbor is to her, handed at birth.
|
|
36
|
+
box;
|
|
37
|
+
constructor(stance, box = ALONE) {
|
|
38
|
+
super(stance);
|
|
39
|
+
this.box = box;
|
|
40
|
+
}
|
|
41
|
+
// Bytes on a path she claims: the body as text, the headers, the query and
|
|
42
|
+
// the method. This body is called in by nobody; a faculty that claims a
|
|
43
|
+
// path says what the bytes mean.
|
|
44
|
+
inbound(_args) {
|
|
45
|
+
return { error: 'this faculty is not called in' };
|
|
46
|
+
}
|
|
47
|
+
// Someone wants waking. She hands an invitation back and says what to ask
|
|
48
|
+
// her; this knocks it, takes it under an id of this being's own, and files
|
|
49
|
+
// it. The empty ask is the knock, because every being answers one.
|
|
50
|
+
async watch(args, asker) {
|
|
51
|
+
const inv = args.invitation;
|
|
52
|
+
if (!isInvitation(inv) || typeof args.method !== 'string')
|
|
53
|
+
return { error: 'an invitation and a method' };
|
|
54
|
+
const extra = args.args;
|
|
55
|
+
if (extra !== undefined && (extra === null || typeof extra !== 'object' || Array.isArray(extra)))
|
|
56
|
+
return { error: 'args is one object' };
|
|
57
|
+
const out = await this.knock(inv);
|
|
58
|
+
if (isSilence(out) || isWord(out))
|
|
59
|
+
return { error: 'no way back' };
|
|
60
|
+
const cells = this.cells;
|
|
61
|
+
const id = await this.take(`wake:${(cells.minted += 1)}`, inv);
|
|
62
|
+
if (id === null)
|
|
63
|
+
return { error: 'not taken' };
|
|
64
|
+
cells.watched[id] = { by: asker.id ?? '', method: args.method, args: extra ?? {} };
|
|
65
|
+
await this.welcomed(id);
|
|
66
|
+
return { watching: id };
|
|
67
|
+
}
|
|
68
|
+
// Somebody new is filed, and the answer to her `watch` waits for this. A
|
|
69
|
+
// faculty that owes a new subscriber something at once says it here.
|
|
70
|
+
async welcomed(_id) { }
|
|
71
|
+
// One round. Each subscriber is asked what she asked to be asked, with
|
|
72
|
+
// whatever this round adds; `only` is one of them. A subscriber who removed
|
|
73
|
+
// the occupant says `removed` once and her row goes. Null is this being
|
|
74
|
+
// herself taken out of her ward, so whoever holds an interval puts it down.
|
|
75
|
+
async woke(round = (w) => w.args, only) {
|
|
76
|
+
const watched = this.cells.watched;
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const [id, w] of Object.entries(watched)) {
|
|
79
|
+
if (only !== undefined && id !== only)
|
|
80
|
+
continue;
|
|
81
|
+
const said = await this.standings[id]?.ask(w.method, round(w));
|
|
82
|
+
if (said === undefined || (isWord(said) && wordOf(said) === 'dropped'))
|
|
83
|
+
return null;
|
|
84
|
+
if (isWord(said) && wordOf(said) === 'removed') {
|
|
85
|
+
this.standings.remove(id);
|
|
86
|
+
delete watched[id];
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
out.push({ id, by: w.by, out: said });
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
// Who is waiting, by this being's own id for the relation.
|
|
94
|
+
get watching() {
|
|
95
|
+
return this.cells.watched;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Faculty } from './faculty.ts';
|
|
2
|
-
export declare const GRANTS
|
|
2
|
+
export declare const GRANTS: import("nervur").Blueprint;
|
|
3
3
|
export declare const ACCESS_TTL: number;
|
|
4
4
|
export declare const REFRESH_TTL: number;
|
|
5
5
|
export type Grant = {
|
|
@@ -25,9 +25,9 @@ export declare class Grants extends Faculty {
|
|
|
25
25
|
refresh: {};
|
|
26
26
|
};
|
|
27
27
|
static asks: {};
|
|
28
|
-
|
|
29
|
-
now: () => number;
|
|
28
|
+
static fulfils: import("nervur").Blueprint[];
|
|
30
29
|
get kept(): Kept;
|
|
30
|
+
now(): number;
|
|
31
31
|
issue(identity: string, ward: string, client: string): Promise<Tokens>;
|
|
32
32
|
refresh(refresh_token: string): Promise<Tokens | null>;
|
|
33
33
|
bearer(req: Request): {
|
|
@@ -37,5 +37,6 @@ export declare class Grants extends Faculty {
|
|
|
37
37
|
revoke(identity: string, ward: string): Promise<void>;
|
|
38
38
|
sweep(): void;
|
|
39
39
|
}
|
|
40
|
+
export type Bearers = Pick<Grants, 'issue' | 'refresh' | 'bearer' | 'revoke' | 'now'>;
|
|
40
41
|
export declare const offered: (req: Request) => string;
|
|
41
42
|
export {};
|