@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
|
@@ -0,0 +1,4775 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// ../quo/dist/being/being.js
|
|
8
|
+
var RESERVED = /* @__PURE__ */ new Set(["answer", "describe", "stance", "cells", "standings", "occupants", "occupant", "invite", "knock", "take", "boot", "lend", "constructor"]);
|
|
9
|
+
var wrote = (self, name) => {
|
|
10
|
+
for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
11
|
+
if (Object.hasOwn(p, name))
|
|
12
|
+
return typeof p[name] === "function";
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
};
|
|
16
|
+
var Being = class {
|
|
17
|
+
// Her cells' defaults. Merged in at birth, only where a key is missing, so
|
|
18
|
+
// a restart keeps what she wrote.
|
|
19
|
+
static cells = {};
|
|
20
|
+
// What she can be asked. Declaration order is blueprint order, except a
|
|
21
|
+
// name that reads as an array index, which the language lists first.
|
|
22
|
+
static asks = {};
|
|
23
|
+
stance;
|
|
24
|
+
constructor(stance) {
|
|
25
|
+
this.stance = stance;
|
|
26
|
+
const C = this.constructor;
|
|
27
|
+
for (const name of Object.keys(C.asks)) {
|
|
28
|
+
if (RESERVED.has(name))
|
|
29
|
+
throw new Error(`ask '${name}' is a reserved name`);
|
|
30
|
+
if (!wrote(this, name))
|
|
31
|
+
throw new Error(`ask '${name}' has no method on the prototype`);
|
|
32
|
+
}
|
|
33
|
+
for (const [k, v] of Object.entries(C.cells))
|
|
34
|
+
if (!Object.hasOwn(stance.cells, k))
|
|
35
|
+
stance.cells[k] = structuredClone(v);
|
|
36
|
+
}
|
|
37
|
+
get cells() {
|
|
38
|
+
return this.stance.cells;
|
|
39
|
+
}
|
|
40
|
+
get standings() {
|
|
41
|
+
return this.stance.standings;
|
|
42
|
+
}
|
|
43
|
+
get occupants() {
|
|
44
|
+
return this.stance.occupants;
|
|
45
|
+
}
|
|
46
|
+
// A standing at one of the things this device can do, under an id of hers.
|
|
47
|
+
// The ward knocks and takes it for her; the invitation never reaches her.
|
|
48
|
+
lend(name, id) {
|
|
49
|
+
return this.stance.lend(name, id);
|
|
50
|
+
}
|
|
51
|
+
invite(id, notes) {
|
|
52
|
+
return this.stance.occupants.invite(id, notes);
|
|
53
|
+
}
|
|
54
|
+
knock(invitation, method, args, wanted) {
|
|
55
|
+
return this.stance.standings.knock(invitation, method, args, wanted);
|
|
56
|
+
}
|
|
57
|
+
take(id, invitation) {
|
|
58
|
+
return this.stance.standings.take(id, invitation);
|
|
59
|
+
}
|
|
60
|
+
// A new being of her ward, by class name, under a key she chooses. With an
|
|
61
|
+
// id, she holds a standing to the being she made, who knows her by her key.
|
|
62
|
+
boot(className, key, id) {
|
|
63
|
+
return this.stance.boot(className, key, id);
|
|
64
|
+
}
|
|
65
|
+
// The occupant record for whoever is at the door. Undefined at a public being.
|
|
66
|
+
occupant(asker) {
|
|
67
|
+
return asker.id !== void 0 && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : void 0;
|
|
68
|
+
}
|
|
69
|
+
// Her blueprint for this asker. Override to shape it by hand.
|
|
70
|
+
describe(asker) {
|
|
71
|
+
const C = this.constructor;
|
|
72
|
+
const rec = this.occupant(asker);
|
|
73
|
+
const asks2 = [];
|
|
74
|
+
for (const [name, spec] of Object.entries(C.asks)) {
|
|
75
|
+
if (spec.for && !spec.for(rec, asker))
|
|
76
|
+
continue;
|
|
77
|
+
const ask = { name, input: spec.input ?? { type: "object" } };
|
|
78
|
+
if (spec.description !== void 0)
|
|
79
|
+
ask.description = spec.description;
|
|
80
|
+
if (spec.output !== void 0)
|
|
81
|
+
ask.output = spec.output;
|
|
82
|
+
asks2.push(ask);
|
|
83
|
+
}
|
|
84
|
+
return { asks: asks2, notes: {} };
|
|
85
|
+
}
|
|
86
|
+
// The one function. Override to wrap it; call super to keep the dispatch.
|
|
87
|
+
async answer(asker, method, args = {}) {
|
|
88
|
+
if (method === void 0)
|
|
89
|
+
return this.describe(asker);
|
|
90
|
+
const C = this.constructor;
|
|
91
|
+
const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : void 0;
|
|
92
|
+
if (!spec || spec.for && !spec.for(this.occupant(asker), asker))
|
|
93
|
+
return { error: "unknown ask" };
|
|
94
|
+
const fn = this[method];
|
|
95
|
+
return fn.call(this, args ?? {}, asker);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ../quo/dist/being/types.js
|
|
100
|
+
var OWNER = "OWNER";
|
|
101
|
+
var PUBLIC = "PUBLIC";
|
|
102
|
+
var RESERVED_IDS = [OWNER, PUBLIC];
|
|
103
|
+
var isInvitation = (v) => {
|
|
104
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
105
|
+
return false;
|
|
106
|
+
const o = v;
|
|
107
|
+
if (typeof o.ward !== "string" || !/^[0-9a-f]{128}$/.test(o.ward))
|
|
108
|
+
return false;
|
|
109
|
+
const heir = typeof o.heir === "string", secret = typeof o.secret === "string";
|
|
110
|
+
return heir && secret || !heir && !secret && !("heir" in o) && !("secret" in o);
|
|
111
|
+
};
|
|
112
|
+
var isBlueprint = (v) => {
|
|
113
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
114
|
+
return false;
|
|
115
|
+
const { asks: asks2, notes } = v;
|
|
116
|
+
if (!Array.isArray(asks2) || notes === void 0)
|
|
117
|
+
return false;
|
|
118
|
+
return asks2.every((a) => a !== null && typeof a === "object" && !Array.isArray(a) && typeof a.name === "string" && a.input !== null && typeof a.input === "object" && !Array.isArray(a.input));
|
|
119
|
+
};
|
|
120
|
+
var WORD_KEY = /* @__PURE__ */ Symbol.for("quo.word");
|
|
121
|
+
|
|
122
|
+
// ../quo/dist/being/silence.js
|
|
123
|
+
var silence = /* @__PURE__ */ Symbol.for("quo.silence");
|
|
124
|
+
var isSilence = (x) => x === silence;
|
|
125
|
+
var DOOR_WORDS = ["removed", "absent", "unannounced", "repeated", "threw"];
|
|
126
|
+
var isDoorWord = (s) => typeof s === "string" && DOOR_WORDS.includes(s);
|
|
127
|
+
var WORDS = /* @__PURE__ */ new Map();
|
|
128
|
+
var word = (name) => {
|
|
129
|
+
let w = WORDS.get(name);
|
|
130
|
+
if (!w)
|
|
131
|
+
WORDS.set(name, w = Object.freeze({ [WORD_KEY]: name }));
|
|
132
|
+
return w;
|
|
133
|
+
};
|
|
134
|
+
var isWord = (x) => x !== null && typeof x === "object" && typeof x[WORD_KEY] === "string";
|
|
135
|
+
var wordOf = (x) => x[WORD_KEY];
|
|
136
|
+
var unreached = () => word("unreached");
|
|
137
|
+
var isUnreached = (x) => isWord(x) && wordOf(x) === "unreached";
|
|
138
|
+
|
|
139
|
+
// ../quo/dist/being/digest.js
|
|
140
|
+
var absent = (v) => v === void 0 || typeof v === "function" || typeof v === "symbol";
|
|
141
|
+
var canonical = (v) => {
|
|
142
|
+
if (Array.isArray(v))
|
|
143
|
+
return `[${Array.from(v, (slot) => absent(slot) ? "null" : canonical(slot)).join(",")}]`;
|
|
144
|
+
if (v !== null && typeof v === "object") {
|
|
145
|
+
return `{${Object.keys(v).filter((k) => !absent(v[k])).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`;
|
|
146
|
+
}
|
|
147
|
+
return JSON.stringify(v);
|
|
148
|
+
};
|
|
149
|
+
var hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
150
|
+
var subtle = () => {
|
|
151
|
+
const s = globalThis.crypto?.subtle;
|
|
152
|
+
if (!s)
|
|
153
|
+
throw new Error("this terrain has no crypto.subtle: a being needs a secure context to be described");
|
|
154
|
+
return s;
|
|
155
|
+
};
|
|
156
|
+
var digest = async (blueprint) => hex(await subtle().digest("SHA-256", new TextEncoder().encode(canonical(blueprint))));
|
|
157
|
+
|
|
158
|
+
// beings/look.ts
|
|
159
|
+
var COLOUR = /^#[0-9a-fA-F]{3,8}$/;
|
|
160
|
+
var FONT = /^[\w\s,'"-]{1,80}$/;
|
|
161
|
+
var LOGO = /^data:image\/(svg\+xml|png|jpeg|webp);base64,[A-Za-z0-9+/=]{1,40000}$/;
|
|
162
|
+
var NAME = /^[^<>&]{1,60}$/;
|
|
163
|
+
var ICON = /^[^<>&"']{1,8}$/;
|
|
164
|
+
var STANDS = 8;
|
|
165
|
+
var ORIGIN = 200;
|
|
166
|
+
var str = (v, re) => typeof v === "string" && re.test(v) ? v : void 0;
|
|
167
|
+
function sanitise(v) {
|
|
168
|
+
if (v === null || v === void 0 || typeof v !== "object" || Array.isArray(v)) return {};
|
|
169
|
+
const l = {};
|
|
170
|
+
const name = str(v.name, NAME);
|
|
171
|
+
if (name !== void 0) l.name = name;
|
|
172
|
+
const logo = str(v.logo, LOGO);
|
|
173
|
+
if (logo !== void 0) l.logo = logo;
|
|
174
|
+
for (const k of ["accent", "background", "foreground"]) {
|
|
175
|
+
const c = str(v[k], COLOUR);
|
|
176
|
+
if (c !== void 0) l[k] = c;
|
|
177
|
+
}
|
|
178
|
+
const font = str(v.font, FONT);
|
|
179
|
+
if (font !== void 0) l.font = font;
|
|
180
|
+
if (typeof v.radius === "number" && Number.isFinite(v.radius) && v.radius >= 0 && v.radius <= 40) l.radius = Math.round(v.radius);
|
|
181
|
+
if (Array.isArray(v.order)) l.order = v.order.filter((x) => typeof x === "string");
|
|
182
|
+
if (Array.isArray(v.stands)) {
|
|
183
|
+
const stands2 = [];
|
|
184
|
+
for (const x of v.stands.slice(0, STANDS)) {
|
|
185
|
+
if (typeof x !== "string" || x.length > ORIGIN) continue;
|
|
186
|
+
try {
|
|
187
|
+
const u = new URL(x);
|
|
188
|
+
if ((u.protocol === "https:" || u.protocol === "http:") && !stands2.includes(u.origin)) stands2.push(u.origin);
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (stands2.length) l.stands = stands2;
|
|
193
|
+
}
|
|
194
|
+
if (v.asks !== null && typeof v.asks === "object" && !Array.isArray(v.asks)) {
|
|
195
|
+
const asks2 = {};
|
|
196
|
+
for (const [ask, h] of Object.entries(v.asks)) {
|
|
197
|
+
if (h === null || typeof h !== "object" || Array.isArray(h)) continue;
|
|
198
|
+
const hint2 = {};
|
|
199
|
+
const title2 = str(h.title, NAME);
|
|
200
|
+
if (title2 !== void 0) hint2.title = title2;
|
|
201
|
+
const icon = str(h.icon, ICON);
|
|
202
|
+
if (icon !== void 0) hint2.icon = icon;
|
|
203
|
+
for (const k of ["readOnly", "destructive", "idempotent"]) if (typeof h[k] === "boolean") hint2[k] = h[k];
|
|
204
|
+
if (Object.keys(hint2).length) asks2[ask] = hint2;
|
|
205
|
+
}
|
|
206
|
+
if (Object.keys(asks2).length) l.asks = asks2;
|
|
207
|
+
}
|
|
208
|
+
return l;
|
|
209
|
+
}
|
|
210
|
+
var hint = (l, name) => l?.asks?.[name] ?? {};
|
|
211
|
+
function groups(bp) {
|
|
212
|
+
const n = bp?.notes;
|
|
213
|
+
const g = n !== null && n !== void 0 && typeof n === "object" && !Array.isArray(n) ? n.standings : void 0;
|
|
214
|
+
return g !== null && g !== void 0 && typeof g === "object" && !Array.isArray(g) ? g : {};
|
|
215
|
+
}
|
|
216
|
+
function presentation(bp) {
|
|
217
|
+
const p = /* @__PURE__ */ new Set(["look", "page"]);
|
|
218
|
+
for (const g of Object.values(groups(bp))) if (typeof g.page === "string") p.add(g.page);
|
|
219
|
+
return p;
|
|
220
|
+
}
|
|
221
|
+
function hintFor(bp, mine2, name) {
|
|
222
|
+
for (const [id, g] of Object.entries(groups(bp))) {
|
|
223
|
+
if (!g.asks?.includes(name)) continue;
|
|
224
|
+
const prefix = `${id}-`;
|
|
225
|
+
return hint(sanitise(g.look), name.startsWith(prefix) ? name.slice(prefix.length) : name);
|
|
226
|
+
}
|
|
227
|
+
return hint(mine2, name);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// beings/carry.ts
|
|
231
|
+
function carriedBy(bp) {
|
|
232
|
+
const out = /* @__PURE__ */ new Set();
|
|
233
|
+
const notes = bp.notes;
|
|
234
|
+
const groups2 = notes !== null && typeof notes === "object" && !Array.isArray(notes) ? notes.standings : void 0;
|
|
235
|
+
if (!groups2 || typeof groups2 !== "object" || Array.isArray(groups2)) return out;
|
|
236
|
+
for (const g of Object.values(groups2)) {
|
|
237
|
+
if (Array.isArray(g?.asks)) {
|
|
238
|
+
for (const n of g.asks) if (typeof n === "string") out.add(n);
|
|
239
|
+
}
|
|
240
|
+
if (typeof g?.page === "string") out.add(g.page);
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
var Carrier = class extends Being {
|
|
245
|
+
// Who sees what she carries. Nobody, until a subclass says who.
|
|
246
|
+
static carries(_occupant, _asker) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
// Standings that are plumbing: their own asks are never carried, and
|
|
250
|
+
// what they carry is. A way back to a device is one, by the dock's own
|
|
251
|
+
// convention, so a bare avatar's inbox never shows on her and a presence
|
|
252
|
+
// being's faculties do, under `to:<id>-<name>`, which is how home carries
|
|
253
|
+
// her phone. A subclass may name more.
|
|
254
|
+
static hidden(id) {
|
|
255
|
+
return id.startsWith("to:");
|
|
256
|
+
}
|
|
257
|
+
// Standings she never reads for carrying at all: a presence being's
|
|
258
|
+
// standing at her own user being is one, since carrying it would carry
|
|
259
|
+
// the user being's carrying of her, and round. None, until a subclass
|
|
260
|
+
// says which.
|
|
261
|
+
static skips(_id) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
// Which of the standings she carries this asker may see. Every one,
|
|
265
|
+
// until a subclass keeps some for fewer: a user being shows her own door
|
|
266
|
+
// to a device that may mint and to nobody else.
|
|
267
|
+
static shows(_id, _occupant, _asker) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
get carried() {
|
|
271
|
+
return this.cells.carried ??= {};
|
|
272
|
+
}
|
|
273
|
+
get looks() {
|
|
274
|
+
return this.cells.looks ??= {};
|
|
275
|
+
}
|
|
276
|
+
// Her describe with her standings carried into it, for an asker who may
|
|
277
|
+
// see them. Every other asker gets her describe alone.
|
|
278
|
+
async blueprint(asker) {
|
|
279
|
+
const C = this.constructor;
|
|
280
|
+
const bp = this.describe(asker);
|
|
281
|
+
if (!C.carries(this.occupant(asker), asker)) return bp;
|
|
282
|
+
const own = new Set(bp.asks.map((a) => a.name));
|
|
283
|
+
const carried = {};
|
|
284
|
+
const groups2 = {};
|
|
285
|
+
const occ = this.occupant(asker);
|
|
286
|
+
for (const [id, rec] of [...Object.entries(this.cells.standings)].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
287
|
+
if (C.skips(id)) continue;
|
|
288
|
+
const st = this.standings[id];
|
|
289
|
+
if (!st) continue;
|
|
290
|
+
if (!rec.blueprint || rec.seen !== rec.digest) await st.ask();
|
|
291
|
+
if (!rec.blueprint) continue;
|
|
292
|
+
const through = C.hidden(id) ? carriedBy(rec.blueprint) : null;
|
|
293
|
+
if (through && through.size === 0) continue;
|
|
294
|
+
const shown = C.shows(id, occ, asker);
|
|
295
|
+
const names = [];
|
|
296
|
+
const group = { asks: names };
|
|
297
|
+
for (const a of rec.blueprint.asks) {
|
|
298
|
+
if (a.name === "look") continue;
|
|
299
|
+
if (through && !through.has(a.name)) continue;
|
|
300
|
+
const name = `${id}-${a.name}`;
|
|
301
|
+
if (own.has(name)) continue;
|
|
302
|
+
carried[name] = { id, ask: a.name };
|
|
303
|
+
if (!shown) continue;
|
|
304
|
+
bp.asks.push({ ...a, name });
|
|
305
|
+
if (a.name === "page") group.page = name;
|
|
306
|
+
else names.push(name);
|
|
307
|
+
}
|
|
308
|
+
if (!shown) continue;
|
|
309
|
+
if (rec.blueprint.asks.some((a) => a.name === "look")) {
|
|
310
|
+
const kept2 = this.looks[id];
|
|
311
|
+
if (!kept2 || kept2.digest !== rec.digest) {
|
|
312
|
+
const l = await st.ask("look");
|
|
313
|
+
this.looks[id] = { digest: rec.digest, look: isSilence(l) || isWord(l) ? {} : sanitise(l) };
|
|
314
|
+
}
|
|
315
|
+
group.look = this.looks[id].look;
|
|
316
|
+
}
|
|
317
|
+
groups2[id] = group;
|
|
318
|
+
}
|
|
319
|
+
for (const id of Object.keys(this.looks)) if (!(id in groups2)) delete this.looks[id];
|
|
320
|
+
this.cells.carried = carried;
|
|
321
|
+
const notes = bp.notes !== null && typeof bp.notes === "object" && !Array.isArray(bp.notes) ? bp.notes : {};
|
|
322
|
+
return { asks: bp.asks, notes: { ...notes, standings: groups2 } };
|
|
323
|
+
}
|
|
324
|
+
async answer(asker, method, args = {}) {
|
|
325
|
+
if (method === void 0) return this.blueprint(asker);
|
|
326
|
+
const C = this.constructor;
|
|
327
|
+
const to = Object.hasOwn(this.carried, method) ? this.carried[method] : void 0;
|
|
328
|
+
const occ = this.occupant(asker);
|
|
329
|
+
if (to && C.carries(occ, asker) && C.shows(to.id, occ, asker)) {
|
|
330
|
+
const st = this.standings[to.id];
|
|
331
|
+
if (!st) return { error: "unknown ask" };
|
|
332
|
+
const out = await st.ask(to.ask, args);
|
|
333
|
+
if (isSilence(out)) return out;
|
|
334
|
+
if (isWord(out)) return { error: wordOf(out) };
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
return super.answer(asker, method, args);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// dock/faculty.ts
|
|
342
|
+
var Faculty = class extends Being {
|
|
343
|
+
static cells = { watched: {}, minted: 0 };
|
|
344
|
+
// Someone wants waking. She hands the invitation she minted and says what
|
|
345
|
+
// to ask her; this knocks it, takes it under an id of this being's own,
|
|
346
|
+
// and files it. The id never crosses: it is this side's name for the
|
|
347
|
+
// relation, and the caller's name for the same relation is her own.
|
|
348
|
+
//
|
|
349
|
+
// The empty ask is the knock, because every being answers one and asking
|
|
350
|
+
// for a named ask would make every subscriber owe a method she does not
|
|
351
|
+
// otherwise need.
|
|
352
|
+
async watch(args, asker) {
|
|
353
|
+
const inv = args.invitation;
|
|
354
|
+
if (!isInvitation(inv) || typeof args.method !== "string") return { error: "an invitation and a method" };
|
|
355
|
+
const extra = args.args;
|
|
356
|
+
if (extra !== void 0 && (extra === null || typeof extra !== "object" || Array.isArray(extra))) return { error: "args is one object" };
|
|
357
|
+
const out = await this.knock(inv);
|
|
358
|
+
if (isSilence(out) || isWord(out)) return { error: "no way back" };
|
|
359
|
+
const cells = this.cells;
|
|
360
|
+
const id = await this.take(`wake:${cells.minted += 1}`, inv);
|
|
361
|
+
if (id === null) return { error: "not taken" };
|
|
362
|
+
cells.watched[id] = { by: asker.id ?? "", method: args.method, args: extra ?? {} };
|
|
363
|
+
await this.welcomed(id);
|
|
364
|
+
return { watching: id };
|
|
365
|
+
}
|
|
366
|
+
// Somebody new is filed, and the answer to her `watch` waits for this. A
|
|
367
|
+
// faculty that owes a new subscriber something at once says it here: the
|
|
368
|
+
// timer wakes her, learns when she is next due and arms the box for it, so
|
|
369
|
+
// a being born long after the box was up is not waiting for a round nobody
|
|
370
|
+
// scheduled. Most faculties owe nothing, and this is why they say nothing.
|
|
371
|
+
async welcomed(_id) {
|
|
372
|
+
}
|
|
373
|
+
// One round of waking. Each subscriber is asked what she asked to be
|
|
374
|
+
// asked, with whatever this round adds. A subscriber who removed the
|
|
375
|
+
// occupant says `removed` once, and her row goes with the standing; there
|
|
376
|
+
// is nothing to retry and nothing to remember.
|
|
377
|
+
//
|
|
378
|
+
// `only` is one subscriber of the round's own choosing, named by this
|
|
379
|
+
// being's own id for the relation, and it is the same round down to one
|
|
380
|
+
// row: a faculty that wakes somebody just filed learns exactly what a full
|
|
381
|
+
// round would have learned about her.
|
|
382
|
+
//
|
|
383
|
+
// `dropped` is this being herself: her ward took her out, so the standings
|
|
384
|
+
// she holds reach nobody. Null says so, and whoever holds the interval puts
|
|
385
|
+
// it down rather than waking an empty room for as long as the box is up.
|
|
386
|
+
async woke(round = (w) => w.args, only) {
|
|
387
|
+
const cells = this.cells;
|
|
388
|
+
const out = [];
|
|
389
|
+
for (const [id, w] of Object.entries(cells.watched)) {
|
|
390
|
+
if (only !== void 0 && id !== only) continue;
|
|
391
|
+
const said2 = await this.standings[id]?.ask(w.method, round(w));
|
|
392
|
+
if (said2 === void 0 || isWord(said2) && wordOf(said2) === "dropped") return null;
|
|
393
|
+
if (isWord(said2) && wordOf(said2) === "removed") {
|
|
394
|
+
this.standings.remove(id);
|
|
395
|
+
delete cells.watched[id];
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
out.push({ id, by: w.by, out: said2 });
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
// Who is waiting, by this being's own id for the relation. For a device's
|
|
403
|
+
// own code and for a suite; it crosses no door.
|
|
404
|
+
get watching() {
|
|
405
|
+
return this.cells.watched;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
var flight = /* @__PURE__ */ new Set();
|
|
409
|
+
var settled = async () => {
|
|
410
|
+
while (flight.size) {
|
|
411
|
+
const now = [...flight];
|
|
412
|
+
await Promise.allSettled(now);
|
|
413
|
+
for (const p of now) flight.delete(p);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
function join(b, name, watch, id = name) {
|
|
417
|
+
const p = joining(b, name, watch, id);
|
|
418
|
+
flight.add(p);
|
|
419
|
+
void p.catch(() => {
|
|
420
|
+
});
|
|
421
|
+
return p;
|
|
422
|
+
}
|
|
423
|
+
async function joining(b, name, watch, id) {
|
|
424
|
+
await Promise.resolve();
|
|
425
|
+
b.standings.remove(id);
|
|
426
|
+
b.occupants.remove(`${id}:wake`);
|
|
427
|
+
const got = await b.lend(name, id);
|
|
428
|
+
if (got === null) return null;
|
|
429
|
+
if (!watch) return got;
|
|
430
|
+
const back = await b.invite(`${id}:wake`);
|
|
431
|
+
if (!back) return got;
|
|
432
|
+
await b.standings[id]?.ask("watch", { invitation: back, method: watch.method, ...watch.args ? { args: watch.args } : {} });
|
|
433
|
+
return got;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// dock/console.ts
|
|
437
|
+
var CONSOLE = "console";
|
|
438
|
+
var Console = class extends Carrier {
|
|
439
|
+
static carries(occupant2, _asker) {
|
|
440
|
+
return occupant2 !== void 0;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// beings/way.ts
|
|
445
|
+
var WAY = (kind) => `way:${kind}`;
|
|
446
|
+
var kindOf = (id) => id.startsWith("way:") ? id.slice(4) : null;
|
|
447
|
+
var VERIFY = { description: "who this proof is for", input: { type: "object", properties: { proof: { type: "object" } }, required: ["proof"] } };
|
|
448
|
+
function verified(out) {
|
|
449
|
+
if (out === null || typeof out !== "object" || Array.isArray(out)) return null;
|
|
450
|
+
const v = out;
|
|
451
|
+
if (typeof v.user !== "string" || typeof v.client !== "string") return null;
|
|
452
|
+
const make = v.make;
|
|
453
|
+
return {
|
|
454
|
+
user: v.user,
|
|
455
|
+
client: v.client,
|
|
456
|
+
...v.wake === true ? { wake: true } : {},
|
|
457
|
+
...v.reach === true ? { reach: true } : {},
|
|
458
|
+
...v.mint === true ? { mint: true } : {},
|
|
459
|
+
...v.revoke === true ? { revoke: true } : {},
|
|
460
|
+
...make === true || typeof make === "string" ? { make } : {}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// dock/local.ts
|
|
465
|
+
var LOCAL = "local";
|
|
466
|
+
var Local = class extends Faculty {
|
|
467
|
+
static asks = { verify: VERIFY };
|
|
468
|
+
#nonces = /* @__PURE__ */ new Map();
|
|
469
|
+
// A nonce for one client this box saw itself, spent by the first proof
|
|
470
|
+
// that names it. The caller drops it when the admit is over: a nonce that
|
|
471
|
+
// was never spent is not a way in that stays open.
|
|
472
|
+
mint(nonce, who) {
|
|
473
|
+
this.#nonces.set(nonce, who);
|
|
474
|
+
}
|
|
475
|
+
drop(nonce) {
|
|
476
|
+
this.#nonces.delete(nonce);
|
|
477
|
+
}
|
|
478
|
+
verify(args) {
|
|
479
|
+
const proof = args.proof;
|
|
480
|
+
if (!proof || typeof proof !== "object" || Array.isArray(proof)) return null;
|
|
481
|
+
const nonce = proof.nonce;
|
|
482
|
+
const who = typeof nonce === "string" ? this.#nonces.get(nonce) : void 0;
|
|
483
|
+
if (!who) return null;
|
|
484
|
+
this.#nonces.delete(nonce);
|
|
485
|
+
return who;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// dock/location.ts
|
|
490
|
+
var Location = class extends Faculty {
|
|
491
|
+
static asks = {
|
|
492
|
+
where: { description: "where this box is", input: { type: "object" } }
|
|
493
|
+
};
|
|
494
|
+
// The platform's own permission prompt is the first ask's, and a refusal is
|
|
495
|
+
// the error object every ask after it answers.
|
|
496
|
+
where() {
|
|
497
|
+
return Promise.resolve({ error: "this box cannot say where it is" });
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// ../quo/dist/ward/cells.js
|
|
502
|
+
var DEPTH = 64;
|
|
503
|
+
var BREADTH = 1 << 20;
|
|
504
|
+
var WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
|
|
505
|
+
function walk(v, path, seen2, sized, depth) {
|
|
506
|
+
if (v === null || typeof v === "boolean")
|
|
507
|
+
return 1;
|
|
508
|
+
if (typeof v === "string")
|
|
509
|
+
return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
|
|
510
|
+
if (typeof v === "number") {
|
|
511
|
+
if (!Number.isFinite(v))
|
|
512
|
+
return `${path} is ${String(v)}, which no harbor can write down`;
|
|
513
|
+
return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
|
|
514
|
+
}
|
|
515
|
+
if (typeof v !== "object")
|
|
516
|
+
return `${path} is a ${typeof v}, which is not a value`;
|
|
517
|
+
if (seen2.has(v))
|
|
518
|
+
return `${path} refers back to itself`;
|
|
519
|
+
const counted = sized.get(v);
|
|
520
|
+
if (counted !== void 0)
|
|
521
|
+
return counted;
|
|
522
|
+
if (depth >= DEPTH)
|
|
523
|
+
return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
|
|
524
|
+
seen2.add(v);
|
|
525
|
+
try {
|
|
526
|
+
let size = 1;
|
|
527
|
+
const under2 = (where, x) => {
|
|
528
|
+
const r = walk(x, where, seen2, sized, depth + 1);
|
|
529
|
+
if (typeof r === "string")
|
|
530
|
+
return r;
|
|
531
|
+
size += r;
|
|
532
|
+
return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
|
|
533
|
+
};
|
|
534
|
+
if (Array.isArray(v)) {
|
|
535
|
+
for (let i = 0; i < v.length; i += 1) {
|
|
536
|
+
if (!(i in v))
|
|
537
|
+
return `${path}[${i}] is a hole, which no harbor can write down`;
|
|
538
|
+
const f = under2(`${path}[${i}]`, v[i]);
|
|
539
|
+
if (f)
|
|
540
|
+
return f;
|
|
541
|
+
}
|
|
542
|
+
sized.set(v, size);
|
|
543
|
+
return size;
|
|
544
|
+
}
|
|
545
|
+
const proto = Object.getPrototypeOf(v);
|
|
546
|
+
if (proto !== Object.prototype && proto !== null)
|
|
547
|
+
return `${path} is a ${v.constructor?.name ?? "object"}, which is not a value`;
|
|
548
|
+
for (const k of Object.keys(v)) {
|
|
549
|
+
if (k === "__proto__")
|
|
550
|
+
return `${path}.__proto__ is a key no harbor can keep`;
|
|
551
|
+
const f = under2(`${path}.${k}`, v[k]);
|
|
552
|
+
if (f)
|
|
553
|
+
return f;
|
|
554
|
+
}
|
|
555
|
+
sized.set(v, size);
|
|
556
|
+
return size;
|
|
557
|
+
} finally {
|
|
558
|
+
seen2.delete(v);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
var cellFault = (v, path) => {
|
|
562
|
+
const out = walk(v, path, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), 0);
|
|
563
|
+
return typeof out === "string" ? out : null;
|
|
564
|
+
};
|
|
565
|
+
var WARDS = /* @__PURE__ */ new Set(["standings", "occupants", "class"]);
|
|
566
|
+
var wrapped = /* @__PURE__ */ new WeakMap();
|
|
567
|
+
var targets = /* @__PURE__ */ new WeakMap();
|
|
568
|
+
var guards = /* @__PURE__ */ new WeakSet();
|
|
569
|
+
var unkeepable = (k) => k === "__proto__";
|
|
570
|
+
var refuse = (why) => {
|
|
571
|
+
throw new TypeError(`cells hold values: ${why}`);
|
|
572
|
+
};
|
|
573
|
+
function keyFault(t, k, v, path, root2) {
|
|
574
|
+
if (typeof k !== "string")
|
|
575
|
+
refuse(`${path} takes no symbol key`);
|
|
576
|
+
if (unkeepable(k))
|
|
577
|
+
refuse(`${path}.__proto__ is a key no harbor can keep`);
|
|
578
|
+
if (root2 && WARDS.has(k))
|
|
579
|
+
refuse(`${path}.${k} is the ward's to write`);
|
|
580
|
+
if (Array.isArray(t)) {
|
|
581
|
+
if (k === "length") {
|
|
582
|
+
if (typeof v !== "number" || v > t.length)
|
|
583
|
+
refuse(`${path}.length set past what she wrote would leave holes`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const i = Number(k);
|
|
587
|
+
if (Number.isInteger(i) && i > t.length)
|
|
588
|
+
refuse(`${path}[${i}] would leave a hole`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function guard(target, path, wrote2, root2 = false) {
|
|
592
|
+
if (guards.has(target))
|
|
593
|
+
return target;
|
|
594
|
+
const had = wrapped.get(target);
|
|
595
|
+
if (had)
|
|
596
|
+
return had;
|
|
597
|
+
const p = new Proxy(target, {
|
|
598
|
+
get(t, k, r) {
|
|
599
|
+
const v = Reflect.get(t, k, r);
|
|
600
|
+
return v !== null && typeof v === "object" && !ArrayBuffer.isView(v) ? guard(v, `${path}.${String(k)}`, wrote2) : v;
|
|
601
|
+
},
|
|
602
|
+
set(t, k, v, r) {
|
|
603
|
+
keyFault(t, k, v, path, root2);
|
|
604
|
+
const f = cellFault(v, `${path}.${String(k)}`);
|
|
605
|
+
if (f)
|
|
606
|
+
refuse(f);
|
|
607
|
+
const ok = Reflect.set(t, k, v, r);
|
|
608
|
+
wrote2();
|
|
609
|
+
return ok;
|
|
610
|
+
},
|
|
611
|
+
defineProperty(t, k, d) {
|
|
612
|
+
if (!("value" in d))
|
|
613
|
+
refuse(`${path}.${String(k)} is an accessor, which no harbor can keep`);
|
|
614
|
+
keyFault(t, k, d.value, path, root2);
|
|
615
|
+
const f = cellFault(d.value, `${path}.${String(k)}`);
|
|
616
|
+
if (f)
|
|
617
|
+
refuse(f);
|
|
618
|
+
const ok = Reflect.defineProperty(t, k, d);
|
|
619
|
+
wrote2();
|
|
620
|
+
return ok;
|
|
621
|
+
},
|
|
622
|
+
deleteProperty(t, k) {
|
|
623
|
+
if (root2 && typeof k === "string" && WARDS.has(k))
|
|
624
|
+
refuse(`${path}.${k} is the ward's to keep`);
|
|
625
|
+
const ok = Reflect.deleteProperty(t, k);
|
|
626
|
+
wrote2();
|
|
627
|
+
return ok;
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
wrapped.set(target, p);
|
|
631
|
+
targets.set(p, target);
|
|
632
|
+
guards.add(p);
|
|
633
|
+
return p;
|
|
634
|
+
}
|
|
635
|
+
var guardCells = (cells, wrote2 = () => {
|
|
636
|
+
}) => guard(cells, "cells", wrote2, true);
|
|
637
|
+
var unguarded = (cells) => targets.get(cells) ?? cells;
|
|
638
|
+
|
|
639
|
+
// ../quo/dist/ward/partition.js
|
|
640
|
+
var VERSION = "pre-1.0.0";
|
|
641
|
+
var READS = [VERSION];
|
|
642
|
+
function upgrade(p) {
|
|
643
|
+
return p;
|
|
644
|
+
}
|
|
645
|
+
var GONE = 256;
|
|
646
|
+
var MINTED = 8;
|
|
647
|
+
var KNOCKS = 64;
|
|
648
|
+
var prune = (knocks, answered4) => {
|
|
649
|
+
const names = Object.keys(knocks);
|
|
650
|
+
if (names.length <= KNOCKS)
|
|
651
|
+
return;
|
|
652
|
+
const order = [...names.filter((n) => !answered4[n]), ...names.filter((n) => answered4[n])];
|
|
653
|
+
for (const old of order.slice(0, names.length - KNOCKS)) {
|
|
654
|
+
delete knocks[old];
|
|
655
|
+
delete answered4[old];
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
var fault = (path, want) => {
|
|
659
|
+
throw new Error(`partition ${path} is not ${want}`);
|
|
660
|
+
};
|
|
661
|
+
var table = (v, path) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : fault(path, "an object");
|
|
662
|
+
var list = (v, path) => Array.isArray(v) ? v : fault(path, "an array");
|
|
663
|
+
var text = (v, path) => void (typeof v === "string" || fault(path, "a string"));
|
|
664
|
+
var orNull = (v, path) => void (v === null || typeof v === "string" || fault(path, "a string or null"));
|
|
665
|
+
var whole = (v, path) => void (Number.isSafeInteger(v) || fault(path, "a whole number"));
|
|
666
|
+
var flag = (v, path) => void (typeof v === "boolean" || fault(path, "true or false"));
|
|
667
|
+
function read(p) {
|
|
668
|
+
for (const [key, cells] of Object.entries(table(p.beings, "beings"))) {
|
|
669
|
+
const c = table(cells, `beings.${key}`);
|
|
670
|
+
table(c.standings, `beings.${key}.standings`);
|
|
671
|
+
table(c.occupants, `beings.${key}.occupants`);
|
|
672
|
+
if (c.class !== void 0)
|
|
673
|
+
text(c.class, `beings.${key}.class`);
|
|
674
|
+
}
|
|
675
|
+
for (const [key, bind] of Object.entries(table(p.bind, "bind"))) {
|
|
676
|
+
const b = table(bind, `bind.${key}`);
|
|
677
|
+
for (const [id, keys] of Object.entries(table(b.standings, `bind.${key}.standings`))) {
|
|
678
|
+
const w = `bind.${key}.standings.${id}`;
|
|
679
|
+
const s = table(keys, w);
|
|
680
|
+
text(s.ward, `${w}.ward`);
|
|
681
|
+
orNull(s.heir, `${w}.heir`);
|
|
682
|
+
text(s.current, `${w}.current`);
|
|
683
|
+
orNull(s.next, `${w}.next`);
|
|
684
|
+
whole(s.seq, `${w}.seq`);
|
|
685
|
+
}
|
|
686
|
+
for (const [id, pk] of Object.entries(table(b.occupants, `bind.${key}.occupants`)))
|
|
687
|
+
text(pk, `bind.${key}.occupants.${id}`);
|
|
688
|
+
for (const [name, keys] of Object.entries(table(b.knocks, `bind.${key}.knocks`))) {
|
|
689
|
+
const w = `bind.${key}.knocks.${name}`;
|
|
690
|
+
const k = table(keys, w);
|
|
691
|
+
text(k.current, `${w}.current`);
|
|
692
|
+
orNull(k.next, `${w}.next`);
|
|
693
|
+
flag(k.spoke, `${w}.spoke`);
|
|
694
|
+
flag(k.sent, `${w}.sent`);
|
|
695
|
+
whole(k.seq, `${w}.seq`);
|
|
696
|
+
}
|
|
697
|
+
table(b.answered, `bind.${key}.answered`);
|
|
698
|
+
for (const [i, pk] of list(b.minted, `bind.${key}.minted`).entries())
|
|
699
|
+
text(pk, `bind.${key}.minted[${i}]`);
|
|
700
|
+
}
|
|
701
|
+
for (const [pk, heir] of Object.entries(table(p.heirs, "heirs"))) {
|
|
702
|
+
const h = table(heir, `heirs.${pk}`);
|
|
703
|
+
text(h.being, `heirs.${pk}.being`);
|
|
704
|
+
text(h.id, `heirs.${pk}.id`);
|
|
705
|
+
text(h.current, `heirs.${pk}.current`);
|
|
706
|
+
orNull(h.announced, `heirs.${pk}.announced`);
|
|
707
|
+
flag(h.fresh, `heirs.${pk}.fresh`);
|
|
708
|
+
whole(h.mark, `heirs.${pk}.mark`);
|
|
709
|
+
for (const [i, n] of list(h.spent, `heirs.${pk}.spent`).entries())
|
|
710
|
+
whole(n, `heirs.${pk}.spent[${i}]`);
|
|
711
|
+
}
|
|
712
|
+
for (const [pk, gone] of Object.entries(table(p.gone, "gone"))) {
|
|
713
|
+
const g = table(gone, `gone.${pk}`);
|
|
714
|
+
text(g.current, `gone.${pk}.current`);
|
|
715
|
+
orNull(g.announced, `gone.${pk}.announced`);
|
|
716
|
+
}
|
|
717
|
+
orNull(p.public, "public");
|
|
718
|
+
return p;
|
|
719
|
+
}
|
|
720
|
+
function open(memory) {
|
|
721
|
+
const p = memory;
|
|
722
|
+
if (Object.keys(memory).length > 0 && p.version !== VERSION) {
|
|
723
|
+
if (!READS.includes(p.version))
|
|
724
|
+
throw new Error(`partition is version ${p.version}, this ward reads ${READS.join(", ")}`);
|
|
725
|
+
upgrade(p).version = VERSION;
|
|
726
|
+
}
|
|
727
|
+
p.version ??= VERSION;
|
|
728
|
+
p.beings ??= {};
|
|
729
|
+
p.bind ??= {};
|
|
730
|
+
p.heirs ??= {};
|
|
731
|
+
p.gone ??= {};
|
|
732
|
+
p.public ??= null;
|
|
733
|
+
return read(p);
|
|
734
|
+
}
|
|
735
|
+
var at = (rec, key) => Object.hasOwn(rec, key) ? rec[key] : void 0;
|
|
736
|
+
var put = (rec, key, value) => {
|
|
737
|
+
Object.defineProperty(rec, key, { value, writable: true, enumerable: true, configurable: true });
|
|
738
|
+
return value;
|
|
739
|
+
};
|
|
740
|
+
var drop = (rec, key) => {
|
|
741
|
+
if (Object.hasOwn(rec, key))
|
|
742
|
+
delete rec[key];
|
|
743
|
+
};
|
|
744
|
+
var emptyCells = () => ({ standings: {}, occupants: {} });
|
|
745
|
+
var emptyBind = () => ({ standings: {}, occupants: {}, knocks: {}, answered: {}, minted: [] });
|
|
746
|
+
var HEAD = "";
|
|
747
|
+
var STANDING = /* @__PURE__ */ new Set(["beings", "bind", "heirs", "gone"]);
|
|
748
|
+
var rowOf = (p, row) => {
|
|
749
|
+
const all = p;
|
|
750
|
+
if (row === HEAD)
|
|
751
|
+
return Object.fromEntries(Object.entries(all).filter(([k]) => k !== "beings" && k !== "bind"));
|
|
752
|
+
const cells = at(p.beings ?? {}, row);
|
|
753
|
+
return cells === void 0 ? void 0 : { cells, bind: at(p.bind ?? {}, row) ?? emptyBind() };
|
|
754
|
+
};
|
|
755
|
+
var rowsIn = (p) => [HEAD, ...Object.keys(p.beings ?? {})];
|
|
756
|
+
var restoreRow = (p, row, value) => {
|
|
757
|
+
const all = p;
|
|
758
|
+
if (row === HEAD) {
|
|
759
|
+
for (const k of Object.keys(all))
|
|
760
|
+
if (!STANDING.has(k))
|
|
761
|
+
drop(all, k);
|
|
762
|
+
for (const [k, v] of Object.entries(value ?? {}))
|
|
763
|
+
if (!STANDING.has(k))
|
|
764
|
+
put(all, k, v);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (value === void 0) {
|
|
768
|
+
drop(p.beings ?? {}, row);
|
|
769
|
+
drop(p.bind ?? {}, row);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const into = (rows, kept2) => {
|
|
773
|
+
const guarded = at(rows, row);
|
|
774
|
+
if (guarded === void 0)
|
|
775
|
+
return void put(rows, row, kept2);
|
|
776
|
+
const held3 = unguarded(guarded);
|
|
777
|
+
for (const k of Object.keys(held3))
|
|
778
|
+
drop(held3, k);
|
|
779
|
+
for (const [k, v] of Object.entries(kept2))
|
|
780
|
+
put(held3, k, v);
|
|
781
|
+
};
|
|
782
|
+
into(p.beings ?? {}, value.cells ?? emptyCells());
|
|
783
|
+
};
|
|
784
|
+
var fromRows = (rows) => {
|
|
785
|
+
const out = { ...rows[HEAD] ?? {} };
|
|
786
|
+
const beings = {};
|
|
787
|
+
const bind = {};
|
|
788
|
+
let any = Object.keys(out).length > 0;
|
|
789
|
+
for (const [row, value] of Object.entries(rows)) {
|
|
790
|
+
if (row === HEAD)
|
|
791
|
+
continue;
|
|
792
|
+
put(beings, row, value.cells);
|
|
793
|
+
put(bind, row, value.bind);
|
|
794
|
+
any = true;
|
|
795
|
+
}
|
|
796
|
+
if (!any)
|
|
797
|
+
return {};
|
|
798
|
+
out.beings = beings;
|
|
799
|
+
out.bind = bind;
|
|
800
|
+
return out;
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// ../quo/dist/ward/heirs.js
|
|
804
|
+
var SPAN = 64;
|
|
805
|
+
var Heirs = class {
|
|
806
|
+
#p;
|
|
807
|
+
#wrote;
|
|
808
|
+
constructor(p, wrote2 = () => {
|
|
809
|
+
}) {
|
|
810
|
+
this.#p = p;
|
|
811
|
+
this.#wrote = wrote2;
|
|
812
|
+
}
|
|
813
|
+
open(heir, being, id) {
|
|
814
|
+
this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
|
|
815
|
+
this.#wrote();
|
|
816
|
+
}
|
|
817
|
+
// The id was removed. The heir is forgotten, and its last keys are kept
|
|
818
|
+
// apart, bounded, so that whoever still holds them hears `removed` at the
|
|
819
|
+
// door, once they have signed as themselves, and nobody else hears a thing.
|
|
820
|
+
close(heir) {
|
|
821
|
+
const h = this.#p.heirs[heir];
|
|
822
|
+
if (!h)
|
|
823
|
+
return;
|
|
824
|
+
delete this.#p.heirs[heir];
|
|
825
|
+
this.#p.gone[heir] = { current: h.current, announced: h.announced };
|
|
826
|
+
const keys = Object.keys(this.#p.gone);
|
|
827
|
+
for (const old of keys.slice(0, Math.max(0, keys.length - GONE)))
|
|
828
|
+
delete this.#p.gone[old];
|
|
829
|
+
this.#wrote();
|
|
830
|
+
}
|
|
831
|
+
// May `by` speak for a relation she removed? True only for the keys the
|
|
832
|
+
// door held when the id went, which nobody but their holder has.
|
|
833
|
+
gone(heir, by) {
|
|
834
|
+
const g = this.#p.gone[heir];
|
|
835
|
+
return !!g && (by === g.current || by === g.announced);
|
|
836
|
+
}
|
|
837
|
+
// May `by` speak for this heir? Returns the record if so, null if not.
|
|
838
|
+
// Does not write: the caller verifies the signature first, then settles.
|
|
839
|
+
admits(heir, by) {
|
|
840
|
+
const h = this.#p.heirs[heir];
|
|
841
|
+
if (!h)
|
|
842
|
+
return null;
|
|
843
|
+
if (h.fresh)
|
|
844
|
+
return by === h.current ? h : null;
|
|
845
|
+
return by === h.current || by === h.announced ? h : null;
|
|
846
|
+
}
|
|
847
|
+
// Once only. A number above the mark is honoured and moves it; a number
|
|
848
|
+
// inside the span is honoured once and never again; a number at or below
|
|
849
|
+
// the span is refused, because a door that remembered every number ever
|
|
850
|
+
// seen would be a door with unbounded memory. Counting starts where
|
|
851
|
+
// strangers must agree: the first legal number is one.
|
|
852
|
+
//
|
|
853
|
+
// The number rides inside the signed body, so bytes caught on the road
|
|
854
|
+
// carry the number they were sent under and are refused as themselves. A
|
|
855
|
+
// caller who means to ask again asks again, under the next number, and is
|
|
856
|
+
// heard: retry and fire-and-forget stay hers to build. Only the accident
|
|
857
|
+
// and the interception are refused.
|
|
858
|
+
//
|
|
859
|
+
// Writes, so the caller settles it only once the signature has checked out.
|
|
860
|
+
// The same bytes twice are one honoured call and one silence.
|
|
861
|
+
spend(h, seq) {
|
|
862
|
+
if (!Number.isSafeInteger(seq) || seq < 1)
|
|
863
|
+
return false;
|
|
864
|
+
if (seq > h.mark) {
|
|
865
|
+
if (h.mark > 0)
|
|
866
|
+
h.spent.push(h.mark);
|
|
867
|
+
h.mark = seq;
|
|
868
|
+
h.spent = h.spent.filter((past) => past > seq - SPAN);
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
if (seq === h.mark || seq <= h.mark - SPAN || h.spent.includes(seq))
|
|
872
|
+
return false;
|
|
873
|
+
h.spent.push(seq);
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
// The signature checked out. Spend the number and settle the keys, or do
|
|
877
|
+
// neither: a call that binds nothing must not burn a number on its way to
|
|
878
|
+
// being refused, or a stranger who cannot be heard would still leave a mark
|
|
879
|
+
// behind her. Every write below this line is one that is going to hold.
|
|
880
|
+
// This is where a knock is judged and where `unannounced` is said. settle
|
|
881
|
+
// below is the writing alone, and it judges nothing: one owner for the
|
|
882
|
+
// question, one for the answer.
|
|
883
|
+
honour(h, by, next, seq) {
|
|
884
|
+
if (h.fresh && (next === null || next === h.current))
|
|
885
|
+
return "unannounced";
|
|
886
|
+
if (!this.spend(h, seq))
|
|
887
|
+
return "repeated";
|
|
888
|
+
this.settle(h, by, next);
|
|
889
|
+
this.#wrote();
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
// The signature checked out and honour has judged. Settle the keys: a fresh
|
|
893
|
+
// heir rotates at once to what it announced, which honour has already held
|
|
894
|
+
// to being something other than itself, so that the heir dies as it speaks;
|
|
895
|
+
// a current key replaces its announcement; an announced key becomes current.
|
|
896
|
+
settle(h, by, next) {
|
|
897
|
+
if (h.fresh) {
|
|
898
|
+
if (next === null)
|
|
899
|
+
return;
|
|
900
|
+
h.current = next;
|
|
901
|
+
h.announced = null;
|
|
902
|
+
h.fresh = false;
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (by === h.announced)
|
|
906
|
+
h.current = by;
|
|
907
|
+
if (next !== null)
|
|
908
|
+
h.announced = next;
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// ../quo/dist/ward/arithmetic.js
|
|
913
|
+
var arithmetic_exports = {};
|
|
914
|
+
__export(arithmetic_exports, {
|
|
915
|
+
KEY: () => KEY,
|
|
916
|
+
NONCE: () => NONCE,
|
|
917
|
+
SIGNATURE: () => SIGNATURE,
|
|
918
|
+
TAG: () => TAG,
|
|
919
|
+
agree: () => agree,
|
|
920
|
+
box: () => box,
|
|
921
|
+
concat: () => concat,
|
|
922
|
+
decrypt: () => decrypt,
|
|
923
|
+
derive: () => derive,
|
|
924
|
+
encrypt: () => encrypt,
|
|
925
|
+
heldKeys: () => heldKeys,
|
|
926
|
+
hex: () => hex2,
|
|
927
|
+
sameBytes: () => sameBytes,
|
|
928
|
+
sealingPair: () => sealingPair,
|
|
929
|
+
sha256: () => sha256,
|
|
930
|
+
sign: () => sign,
|
|
931
|
+
signingPair: () => signingPair,
|
|
932
|
+
smallOrder: () => smallOrder,
|
|
933
|
+
unbox: () => unbox,
|
|
934
|
+
unhex: () => unhex,
|
|
935
|
+
verify: () => verify
|
|
936
|
+
});
|
|
937
|
+
var subtle2 = () => {
|
|
938
|
+
const s = globalThis.crypto?.subtle;
|
|
939
|
+
if (!s)
|
|
940
|
+
throw new Error("this terrain has no crypto.subtle: a ward needs a secure context");
|
|
941
|
+
return s;
|
|
942
|
+
};
|
|
943
|
+
var KEY = 32;
|
|
944
|
+
var SIGNATURE = 64;
|
|
945
|
+
var NONCE = 12;
|
|
946
|
+
var TAG = 16;
|
|
947
|
+
var SEAL_INFO = new TextEncoder().encode("quo-seal");
|
|
948
|
+
var SALT = new Uint8Array(0);
|
|
949
|
+
var ED_SECRET = unhex("302e020100300506032b657004220420");
|
|
950
|
+
var X_SECRET = unhex("302e020100300506032b656e04220420");
|
|
951
|
+
var ED = { name: "Ed25519" };
|
|
952
|
+
var X = { name: "X25519" };
|
|
953
|
+
var HEX = Array.from({ length: 256 }, (_, at3) => at3.toString(16).padStart(2, "0"));
|
|
954
|
+
function hex2(bytes) {
|
|
955
|
+
let out = "";
|
|
956
|
+
for (const byte of new Uint8Array(bytes))
|
|
957
|
+
out += HEX[byte];
|
|
958
|
+
return out;
|
|
959
|
+
}
|
|
960
|
+
function unhex(text3) {
|
|
961
|
+
const out = new Uint8Array(text3.length / 2);
|
|
962
|
+
for (let at3 = 0; at3 < out.length; at3 += 1)
|
|
963
|
+
out[at3] = parseInt(text3.slice(at3 * 2, at3 * 2 + 2), 16);
|
|
964
|
+
return out;
|
|
965
|
+
}
|
|
966
|
+
function concat(parts) {
|
|
967
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
968
|
+
let at3 = 0;
|
|
969
|
+
for (const part of parts) {
|
|
970
|
+
out.set(part, at3);
|
|
971
|
+
at3 += part.length;
|
|
972
|
+
}
|
|
973
|
+
return out;
|
|
974
|
+
}
|
|
975
|
+
function sameBytes(a, b) {
|
|
976
|
+
if (a.length !== b.length)
|
|
977
|
+
return false;
|
|
978
|
+
let diff = 0;
|
|
979
|
+
for (let at3 = 0; at3 < a.length; at3 += 1)
|
|
980
|
+
diff |= a[at3] ^ b[at3];
|
|
981
|
+
return diff === 0;
|
|
982
|
+
}
|
|
983
|
+
var SMALL_ORDER = [
|
|
984
|
+
"0100000000000000000000000000000000000000000000000000000000000000",
|
|
985
|
+
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
986
|
+
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
987
|
+
"0000000000000000000000000000000000000000000000000000000000000080",
|
|
988
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
|
989
|
+
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
990
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85",
|
|
991
|
+
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa"
|
|
992
|
+
].map(unhex);
|
|
993
|
+
var unreduced = (pk) => (pk[31] & 127) === 127 && pk[0] >= 237 && pk.subarray(1, 31).every((b) => b === 255);
|
|
994
|
+
var smallOrder = (pk) => unreduced(pk) || SMALL_ORDER.some((p) => sameBytes(p, pk));
|
|
995
|
+
var key32 = (value, what) => {
|
|
996
|
+
if (!(value instanceof Uint8Array) || value.length !== KEY)
|
|
997
|
+
throw new Error(`${what} is not a 32-byte key`);
|
|
998
|
+
return value;
|
|
999
|
+
};
|
|
1000
|
+
var pkcs8 = (prefix, value, what) => concat([prefix, key32(value, what)]);
|
|
1001
|
+
var KEYS = 512;
|
|
1002
|
+
var imported = /* @__PURE__ */ new Map();
|
|
1003
|
+
var keep = (id, make) => {
|
|
1004
|
+
const had = imported.get(id);
|
|
1005
|
+
if (had !== void 0) {
|
|
1006
|
+
imported.delete(id);
|
|
1007
|
+
imported.set(id, had);
|
|
1008
|
+
return had;
|
|
1009
|
+
}
|
|
1010
|
+
const made = make();
|
|
1011
|
+
made.catch(() => imported.delete(id));
|
|
1012
|
+
imported.set(id, made);
|
|
1013
|
+
if (imported.size > KEYS)
|
|
1014
|
+
imported.delete(imported.keys().next().value);
|
|
1015
|
+
return made;
|
|
1016
|
+
};
|
|
1017
|
+
var heldKeys = () => ({ held: imported.size, bound: KEYS });
|
|
1018
|
+
var secretKey = (alg, prefix, value, what, uses) => {
|
|
1019
|
+
const bytes = pkcs8(prefix, value, what);
|
|
1020
|
+
return keep(`${alg.name}|${uses.join("+")}|${hex2(bytes)}`, () => subtle2().importKey("pkcs8", bytes, alg, true, uses));
|
|
1021
|
+
};
|
|
1022
|
+
var publicKey = (alg, value, what, uses) => {
|
|
1023
|
+
const bytes = key32(value, what);
|
|
1024
|
+
return keep(`${alg.name}|${uses.join("+")}|pk|${hex2(bytes)}`, () => subtle2().importKey("raw", bytes, alg, true, uses));
|
|
1025
|
+
};
|
|
1026
|
+
var publics = /* @__PURE__ */ new WeakMap();
|
|
1027
|
+
function rawPublic(secret) {
|
|
1028
|
+
const had = publics.get(secret);
|
|
1029
|
+
if (had !== void 0)
|
|
1030
|
+
return had;
|
|
1031
|
+
const read2 = (async () => {
|
|
1032
|
+
const jwk = await subtle2().exportKey("jwk", secret);
|
|
1033
|
+
const binary = atob(jwk.x.replaceAll("-", "+").replaceAll("_", "/"));
|
|
1034
|
+
const out = new Uint8Array(binary.length);
|
|
1035
|
+
for (let at3 = 0; at3 < binary.length; at3 += 1)
|
|
1036
|
+
out[at3] = binary.charCodeAt(at3);
|
|
1037
|
+
return out;
|
|
1038
|
+
})();
|
|
1039
|
+
publics.set(secret, read2);
|
|
1040
|
+
return read2;
|
|
1041
|
+
}
|
|
1042
|
+
async function sha256(...parts) {
|
|
1043
|
+
return new Uint8Array(await subtle2().digest("SHA-256", concat(parts)));
|
|
1044
|
+
}
|
|
1045
|
+
async function signingPair(seed2) {
|
|
1046
|
+
const secret = await secretKey(ED, ED_SECRET, seed2, "seed", ["sign"]);
|
|
1047
|
+
return { secret: Uint8Array.from(seed2), pk: Uint8Array.from(await rawPublic(secret)) };
|
|
1048
|
+
}
|
|
1049
|
+
async function sealingPair(seed2) {
|
|
1050
|
+
const secret = await secretKey(X, X_SECRET, seed2, "seed", ["deriveBits"]);
|
|
1051
|
+
return { secret: Uint8Array.from(seed2), pk: Uint8Array.from(await rawPublic(secret)) };
|
|
1052
|
+
}
|
|
1053
|
+
async function sign(message, secret) {
|
|
1054
|
+
const key = await secretKey(ED, ED_SECRET, secret, "secret", ["sign"]);
|
|
1055
|
+
return new Uint8Array(await subtle2().sign(ED, key, message));
|
|
1056
|
+
}
|
|
1057
|
+
async function verify(message, signature, pk) {
|
|
1058
|
+
if (!(signature instanceof Uint8Array) || signature.length !== SIGNATURE)
|
|
1059
|
+
return false;
|
|
1060
|
+
if (!(pk instanceof Uint8Array) || pk.length !== KEY || smallOrder(pk))
|
|
1061
|
+
return false;
|
|
1062
|
+
try {
|
|
1063
|
+
return await subtle2().verify(ED, await publicKey(ED, pk, "pk", ["verify"]), signature, message);
|
|
1064
|
+
} catch {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function agree(secret, peerPk) {
|
|
1069
|
+
const key = await secretKey(X, X_SECRET, secret, "secret", ["deriveBits"]);
|
|
1070
|
+
const peer = await publicKey(X, peerPk, "padlock", []);
|
|
1071
|
+
const shared = new Uint8Array(await subtle2().deriveBits({ name: X.name, public: peer }, key, KEY * 8));
|
|
1072
|
+
if (shared.every((b) => b === 0))
|
|
1073
|
+
throw new Error("dead agreement");
|
|
1074
|
+
return shared;
|
|
1075
|
+
}
|
|
1076
|
+
async function derive(secret, label, bytes) {
|
|
1077
|
+
const material = await subtle2().importKey("raw", secret, "HKDF", false, ["deriveBits"]);
|
|
1078
|
+
return new Uint8Array(await subtle2().deriveBits({ name: "HKDF", hash: "SHA-256", salt: SALT, info: label }, material, bytes * 8));
|
|
1079
|
+
}
|
|
1080
|
+
async function cipherKey(shared, use) {
|
|
1081
|
+
const out = await derive(shared, SEAL_INFO, KEY + NONCE);
|
|
1082
|
+
return { key: await subtle2().importKey("raw", out.subarray(0, KEY), "AES-GCM", false, [use]), nonce: out.subarray(KEY) };
|
|
1083
|
+
}
|
|
1084
|
+
async function encrypt(shared, plaintext, aad) {
|
|
1085
|
+
const { key, nonce } = await cipherKey(shared, "encrypt");
|
|
1086
|
+
return new Uint8Array(await subtle2().encrypt({ name: "AES-GCM", iv: nonce, additionalData: key32(aad, "aad"), tagLength: TAG * 8 }, key, plaintext));
|
|
1087
|
+
}
|
|
1088
|
+
async function decrypt(shared, ciphertext, aad) {
|
|
1089
|
+
if (ciphertext.length < TAG)
|
|
1090
|
+
throw new Error("short input");
|
|
1091
|
+
const { key, nonce } = await cipherKey(shared, "decrypt");
|
|
1092
|
+
return new Uint8Array(await subtle2().decrypt({ name: "AES-GCM", iv: nonce, additionalData: key32(aad, "aad"), tagLength: TAG * 8 }, key, ciphertext));
|
|
1093
|
+
}
|
|
1094
|
+
async function box(inside, padlock, seed2) {
|
|
1095
|
+
const ephemeral = await sealingPair(seed2);
|
|
1096
|
+
const shared = await agree(ephemeral.secret, padlock);
|
|
1097
|
+
return { bytes: concat([ephemeral.pk, await encrypt(shared, inside, ephemeral.pk)]), ephemeral };
|
|
1098
|
+
}
|
|
1099
|
+
async function unbox(bytes, padlockSecret) {
|
|
1100
|
+
if (bytes.length <= KEY)
|
|
1101
|
+
throw new Error("short input");
|
|
1102
|
+
const ephemeralPk = bytes.subarray(0, KEY);
|
|
1103
|
+
const shared = await agree(padlockSecret, ephemeralPk);
|
|
1104
|
+
return decrypt(shared, bytes.subarray(KEY), ephemeralPk);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// ../quo/dist/ward/seal.js
|
|
1108
|
+
var utf8 = new TextEncoder();
|
|
1109
|
+
var text2 = new TextDecoder();
|
|
1110
|
+
var WARD_SIGN = new TextEncoder().encode("quo-ward-sign");
|
|
1111
|
+
var WARD_SEAL = new TextEncoder().encode("quo-ward-seal");
|
|
1112
|
+
var SIZE = 1024 * 1024;
|
|
1113
|
+
async function wardKey(seed2) {
|
|
1114
|
+
const seed32 = typeof seed2 === "string" ? await sha256(utf8.encode(seed2)) : seed2.length === KEY ? seed2 : await sha256(seed2);
|
|
1115
|
+
const s = await signingPair(await derive(seed32, WARD_SIGN, KEY));
|
|
1116
|
+
const p = await sealingPair(await derive(seed32, WARD_SEAL, KEY));
|
|
1117
|
+
return { sign: s.secret, padlock: p.secret, signPk: s.pk, padlockPk: p.pk, pk: hex2(s.pk) + hex2(p.pk) };
|
|
1118
|
+
}
|
|
1119
|
+
var wardSignPk = (pk) => unhex(pk.slice(0, KEY * 2));
|
|
1120
|
+
var wardPadlock = (pk) => unhex(pk.slice(KEY * 2));
|
|
1121
|
+
var isWardPk = (pk) => typeof pk === "string" && /^[0-9a-f]{128}$/.test(pk);
|
|
1122
|
+
var isHex = (v) => typeof v === "string" && /^[0-9a-f]{64}$/.test(v);
|
|
1123
|
+
async function beingKey(seed2) {
|
|
1124
|
+
return { seed: hex2(seed2), pk: hex2((await signingPair(seed2)).pk) };
|
|
1125
|
+
}
|
|
1126
|
+
async function sealAsk(to, payload, signer, padlock, seed2) {
|
|
1127
|
+
const body = utf8.encode(JSON.stringify({ to, ...payload }));
|
|
1128
|
+
const inside = concat([body, await sign(body, signer)]);
|
|
1129
|
+
const { bytes, ephemeral } = await box(inside, padlock, seed2);
|
|
1130
|
+
return { bytes, ephemeral: ephemeral.secret };
|
|
1131
|
+
}
|
|
1132
|
+
async function openAsk(bytes, padlockSecret) {
|
|
1133
|
+
try {
|
|
1134
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < 1 || bytes.length > SIZE)
|
|
1135
|
+
return null;
|
|
1136
|
+
const inside = await unbox(bytes, padlockSecret);
|
|
1137
|
+
if (inside.length <= SIGNATURE)
|
|
1138
|
+
return null;
|
|
1139
|
+
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
1140
|
+
const signature = inside.subarray(inside.length - SIGNATURE);
|
|
1141
|
+
const payload = JSON.parse(text2.decode(body));
|
|
1142
|
+
const to = payload.to;
|
|
1143
|
+
if (to !== null && !isHex(to))
|
|
1144
|
+
return null;
|
|
1145
|
+
if (!isHex(payload.by))
|
|
1146
|
+
return null;
|
|
1147
|
+
if (payload.next !== null && !isHex(payload.next))
|
|
1148
|
+
return null;
|
|
1149
|
+
if (payload.method !== void 0 && typeof payload.method !== "string")
|
|
1150
|
+
return null;
|
|
1151
|
+
if (payload.args !== void 0 && (payload.args === null || typeof payload.args !== "object" || Array.isArray(payload.args)))
|
|
1152
|
+
return null;
|
|
1153
|
+
if (payload.args !== void 0 && cellFault(payload.args, "args") !== null)
|
|
1154
|
+
return null;
|
|
1155
|
+
if (!Number.isSafeInteger(payload.seq) || payload.seq < 1)
|
|
1156
|
+
return null;
|
|
1157
|
+
if (payload.hops !== void 0 && (!Number.isSafeInteger(payload.hops) || payload.hops < 0))
|
|
1158
|
+
return null;
|
|
1159
|
+
return { to, payload, body, signature, ephemeralPk: bytes.subarray(0, KEY) };
|
|
1160
|
+
} catch {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
var verifyAsk = (a, pk) => verify(a.body, a.signature, unhex(pk));
|
|
1165
|
+
async function sealReply(reply, ephemeralPk, wardSign, seed2) {
|
|
1166
|
+
const body = utf8.encode(JSON.stringify(reply));
|
|
1167
|
+
return (await box(concat([body, await sign(body, wardSign)]), ephemeralPk, seed2)).bytes;
|
|
1168
|
+
}
|
|
1169
|
+
async function openReply(bytes, ephemeralSecret, signPk) {
|
|
1170
|
+
try {
|
|
1171
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > SIZE)
|
|
1172
|
+
return null;
|
|
1173
|
+
const inside = await unbox(bytes, ephemeralSecret);
|
|
1174
|
+
if (inside.length <= SIGNATURE)
|
|
1175
|
+
return null;
|
|
1176
|
+
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
1177
|
+
if (!await verify(body, inside.subarray(inside.length - SIGNATURE), signPk))
|
|
1178
|
+
return null;
|
|
1179
|
+
const reply = JSON.parse(text2.decode(body));
|
|
1180
|
+
if (reply === null || typeof reply !== "object" || Array.isArray(reply))
|
|
1181
|
+
return null;
|
|
1182
|
+
const r = reply;
|
|
1183
|
+
if (r.silence === true)
|
|
1184
|
+
return { silence: true };
|
|
1185
|
+
if (Object.hasOwn(r, "quo"))
|
|
1186
|
+
return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
1187
|
+
if (!Object.hasOwn(r, "object") || r.object === void 0)
|
|
1188
|
+
return null;
|
|
1189
|
+
if (r.seen !== null && !isHex(r.seen))
|
|
1190
|
+
return null;
|
|
1191
|
+
return { object: r.object, seen: r.seen };
|
|
1192
|
+
} catch {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// ../quo/dist/ward/allowance.js
|
|
1198
|
+
var DEFAULT = { time: 3e4 };
|
|
1199
|
+
var CEILING = { time: 3e5 };
|
|
1200
|
+
function allow(wanted, ceiling = CEILING, base = DEFAULT) {
|
|
1201
|
+
const whole2 = (n, fallback) => {
|
|
1202
|
+
const w = typeof n === "number" ? Math.floor(n) : NaN;
|
|
1203
|
+
return Number.isFinite(w) && w > 0 ? w : fallback;
|
|
1204
|
+
};
|
|
1205
|
+
return { time: Math.min(whole2(wanted?.time, base.time), ceiling.time) };
|
|
1206
|
+
}
|
|
1207
|
+
var spent = (a) => !(Number.isSafeInteger(a.time) && a.time > 0);
|
|
1208
|
+
var LATE = /* @__PURE__ */ Symbol("late");
|
|
1209
|
+
async function within(ms, work, rang2) {
|
|
1210
|
+
let timer;
|
|
1211
|
+
const bell = new Promise((ring) => {
|
|
1212
|
+
timer = setTimeout(() => {
|
|
1213
|
+
rang2?.();
|
|
1214
|
+
ring(LATE);
|
|
1215
|
+
}, ms);
|
|
1216
|
+
});
|
|
1217
|
+
try {
|
|
1218
|
+
return await Promise.race([work, bell]);
|
|
1219
|
+
} finally {
|
|
1220
|
+
clearTimeout(timer);
|
|
1221
|
+
void work.then(() => {
|
|
1222
|
+
}, () => {
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// ../quo/dist/ward/door.js
|
|
1228
|
+
var SILENCE = { silence: true };
|
|
1229
|
+
var said = (quo) => ({ quo });
|
|
1230
|
+
async function answered2(door2, asker, method, args, bound) {
|
|
1231
|
+
const threw = bound ? said("threw") : SILENCE;
|
|
1232
|
+
let out;
|
|
1233
|
+
try {
|
|
1234
|
+
out = await door2.being.answer(asker, method, args);
|
|
1235
|
+
} catch {
|
|
1236
|
+
return threw;
|
|
1237
|
+
}
|
|
1238
|
+
if (out === void 0 || isSilence(out))
|
|
1239
|
+
return SILENCE;
|
|
1240
|
+
if (isWord(out))
|
|
1241
|
+
return threw;
|
|
1242
|
+
if (cellFault(out, "answer") !== null)
|
|
1243
|
+
return threw;
|
|
1244
|
+
return { object: out, seen: null };
|
|
1245
|
+
}
|
|
1246
|
+
async function arrive(door2, asker, method, args, bound) {
|
|
1247
|
+
const reply = await answered2(door2, asker, method, args, bound);
|
|
1248
|
+
if (method === void 0 || !("object" in reply))
|
|
1249
|
+
return reply;
|
|
1250
|
+
return { object: reply.object, seen: await seen(door2, asker) };
|
|
1251
|
+
}
|
|
1252
|
+
async function seen(door2, asker) {
|
|
1253
|
+
try {
|
|
1254
|
+
const bp = await door2.being.answer(asker);
|
|
1255
|
+
if (isSilence(bp) || isWord(bp))
|
|
1256
|
+
return null;
|
|
1257
|
+
if (cellFault(bp, "blueprint") !== null)
|
|
1258
|
+
return null;
|
|
1259
|
+
return await digest(bp);
|
|
1260
|
+
} catch {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function makeDoor(key, heirs, doors, publicKey2, random, keep3) {
|
|
1265
|
+
const judge = async (bytes) => {
|
|
1266
|
+
const a = await openAsk(bytes, key.padlock);
|
|
1267
|
+
if (!a)
|
|
1268
|
+
return null;
|
|
1269
|
+
const { to, payload, ephemeralPk } = a;
|
|
1270
|
+
const refuse2 = () => ({ reply: SILENCE, ephemeralPk, heard: false });
|
|
1271
|
+
if (spent({ time: payload.time }) || payload.hops === 0)
|
|
1272
|
+
return refuse2();
|
|
1273
|
+
const args = payload.args ?? {};
|
|
1274
|
+
if (to === null) {
|
|
1275
|
+
const pk = publicKey2();
|
|
1276
|
+
const pub = pk !== null ? doors.get(pk) : void 0;
|
|
1277
|
+
if (!pub || !await verifyAsk(a, payload.by))
|
|
1278
|
+
return refuse2();
|
|
1279
|
+
return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: false };
|
|
1280
|
+
}
|
|
1281
|
+
const h = heirs.admits(to, payload.by);
|
|
1282
|
+
if (!h) {
|
|
1283
|
+
if (!heirs.gone(to, payload.by) || !await verifyAsk(a, payload.by))
|
|
1284
|
+
return refuse2();
|
|
1285
|
+
return { reply: said("removed"), ephemeralPk, heard: true };
|
|
1286
|
+
}
|
|
1287
|
+
if (!await verifyAsk(a, payload.by))
|
|
1288
|
+
return refuse2();
|
|
1289
|
+
if (!heirs.admits(to, payload.by))
|
|
1290
|
+
return heirs.gone(to, payload.by) ? { reply: said("removed"), ephemeralPk, heard: true } : refuse2();
|
|
1291
|
+
const door2 = doors.get(h.being);
|
|
1292
|
+
if (!door2)
|
|
1293
|
+
return { reply: said("absent"), ephemeralPk, heard: true };
|
|
1294
|
+
if (!at(door2.cells.occupants, h.id))
|
|
1295
|
+
return { reply: said("removed"), ephemeralPk, heard: true };
|
|
1296
|
+
const honoured = heirs.honour(h, payload.by, payload.next, payload.seq);
|
|
1297
|
+
if (honoured !== true)
|
|
1298
|
+
return { reply: said(honoured), ephemeralPk, heard: true };
|
|
1299
|
+
return { reply: await arrive(door2, { id: h.id }, payload.method, args, true), ephemeralPk, heard: true };
|
|
1300
|
+
};
|
|
1301
|
+
return async function door2(bytes) {
|
|
1302
|
+
const out = await judge(bytes).catch(() => null);
|
|
1303
|
+
let reply = out?.reply ?? SILENCE;
|
|
1304
|
+
const heard = out?.heard ?? false;
|
|
1305
|
+
if (!await keep3().catch(() => false))
|
|
1306
|
+
reply = heard ? said("threw") : SILENCE;
|
|
1307
|
+
try {
|
|
1308
|
+
return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
|
|
1309
|
+
} catch {
|
|
1310
|
+
return { bytes: await sealReply(SILENCE, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function lid(bytes, random) {
|
|
1315
|
+
if (bytes instanceof Uint8Array && bytes.length >= KEY)
|
|
1316
|
+
return bytes.subarray(0, KEY);
|
|
1317
|
+
return random(KEY);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// ../quo/dist/ward/stance.js
|
|
1321
|
+
var CALLS = ["knock", "take", "remove"];
|
|
1322
|
+
function buildStance(inside, key, cells, bind) {
|
|
1323
|
+
const named = (method) => method === void 0 || typeof method === "string";
|
|
1324
|
+
const reserved = (id) => RESERVED_IDS.includes(id) || CALLS.includes(id) || unkeepable(id);
|
|
1325
|
+
const nameOf = (inv) => inv.heir === void 0 ? `public:${inv.ward}` : `${inv.ward}:${inv.heir}`;
|
|
1326
|
+
const valid = (inv) => {
|
|
1327
|
+
const i = inv;
|
|
1328
|
+
if (!i || typeof i !== "object" || !isWardPk(i.ward))
|
|
1329
|
+
return false;
|
|
1330
|
+
if (i.heir === void 0)
|
|
1331
|
+
return i.secret === void 0;
|
|
1332
|
+
return isHex(i.heir) && isHex(i.secret);
|
|
1333
|
+
};
|
|
1334
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
1335
|
+
const lane = (name, run) => {
|
|
1336
|
+
const mine2 = (lanes.get(name) ?? Promise.resolve()).then(run, run);
|
|
1337
|
+
const quiet = mine2.then(() => {
|
|
1338
|
+
}, () => {
|
|
1339
|
+
});
|
|
1340
|
+
lanes.set(name, quiet);
|
|
1341
|
+
void quiet.then(() => {
|
|
1342
|
+
if (lanes.get(name) === quiet)
|
|
1343
|
+
lanes.delete(name);
|
|
1344
|
+
});
|
|
1345
|
+
return mine2;
|
|
1346
|
+
};
|
|
1347
|
+
const bounded = async (wanted, work) => {
|
|
1348
|
+
let late = false;
|
|
1349
|
+
const out = await within(allow(wanted).time, work(() => !late), () => {
|
|
1350
|
+
late = true;
|
|
1351
|
+
});
|
|
1352
|
+
return out === LATE ? word("late") : out;
|
|
1353
|
+
};
|
|
1354
|
+
const read2 = (r) => {
|
|
1355
|
+
if (isSilence(r) || isWord(r))
|
|
1356
|
+
return { answer: r, door: false, seen: null };
|
|
1357
|
+
if ("silence" in r)
|
|
1358
|
+
return { answer: silence, door: true, seen: null };
|
|
1359
|
+
if ("quo" in r)
|
|
1360
|
+
return { answer: word(r.quo), door: true, seen: null };
|
|
1361
|
+
return { answer: r.object, door: true, seen: r.seen };
|
|
1362
|
+
};
|
|
1363
|
+
const honoured = (r) => r.door && !(isWord(r.answer) && wordOf(r.answer) !== "threw");
|
|
1364
|
+
const answered4 = (r) => r.door && !isSilence(r.answer) && !isWord(r.answer);
|
|
1365
|
+
const send = async (keys, method, args, wanted, rec, live = () => true) => {
|
|
1366
|
+
const r = read2(await inside.send(bind, keys, method, args, wanted));
|
|
1367
|
+
if (rec && method !== void 0 && r.seen !== null && live())
|
|
1368
|
+
rec.seen = r.seen;
|
|
1369
|
+
return r.answer;
|
|
1370
|
+
};
|
|
1371
|
+
const knocking = async (inv, method, args, wanted, live = () => true) => {
|
|
1372
|
+
const name = nameOf(inv);
|
|
1373
|
+
const taken = takenAs(name);
|
|
1374
|
+
if (taken !== void 0) {
|
|
1375
|
+
const keys2 = at(bind.standings, taken);
|
|
1376
|
+
if (!keys2)
|
|
1377
|
+
return word("dropped");
|
|
1378
|
+
return lane(`ask:${taken}`, () => asking(taken, keys2, at(cells.standings, taken), method, args, wanted, live));
|
|
1379
|
+
}
|
|
1380
|
+
const k = bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 };
|
|
1381
|
+
prune(bind.knocks, bind.answered);
|
|
1382
|
+
if (inv.heir !== void 0 && !k.spoke && k.sent) {
|
|
1383
|
+
const own = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
|
|
1384
|
+
const r2 = read2(await inside.send(bind, own, method, args, wanted));
|
|
1385
|
+
k.seq = own.seq;
|
|
1386
|
+
k.current = own.current;
|
|
1387
|
+
k.next = own.next;
|
|
1388
|
+
inside.wrote();
|
|
1389
|
+
if (!r2.door) {
|
|
1390
|
+
if (!isSilence(r2.answer))
|
|
1391
|
+
return r2.answer;
|
|
1392
|
+
} else if (!isSilence(r2.answer)) {
|
|
1393
|
+
k.spoke = true;
|
|
1394
|
+
if (answered4(r2))
|
|
1395
|
+
bind.answered[name] = true;
|
|
1396
|
+
return r2.answer;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
const first = inv.heir !== void 0 && !k.spoke;
|
|
1400
|
+
const keys = first ? { ward: inv.ward, heir: inv.heir, current: inv.secret, next: k.current, seq: k.seq } : { ward: inv.ward, heir: inv.heir ?? null, current: k.current, next: k.next, seq: k.seq };
|
|
1401
|
+
const r = read2(await inside.send(bind, keys, method, args, wanted));
|
|
1402
|
+
k.sent = true;
|
|
1403
|
+
k.seq = keys.seq;
|
|
1404
|
+
if (!first) {
|
|
1405
|
+
k.current = keys.current;
|
|
1406
|
+
k.next = keys.next;
|
|
1407
|
+
}
|
|
1408
|
+
if (honoured(r))
|
|
1409
|
+
k.spoke = true;
|
|
1410
|
+
if (answered4(r))
|
|
1411
|
+
bind.answered[name] = true;
|
|
1412
|
+
inside.wrote();
|
|
1413
|
+
return r.answer;
|
|
1414
|
+
};
|
|
1415
|
+
const keyName = (k) => k.heir === null ? `public:${k.ward}` : `${k.ward}:${k.heir}`;
|
|
1416
|
+
const takenAs = (name) => Object.keys(bind.standings).find((id) => keyName(bind.standings[id]) === name);
|
|
1417
|
+
const gone = () => !inside.live();
|
|
1418
|
+
const calls = {
|
|
1419
|
+
knock: async (inv, method, args = {}, wanted) => {
|
|
1420
|
+
if (gone())
|
|
1421
|
+
return word("dropped");
|
|
1422
|
+
if (!named(method))
|
|
1423
|
+
return unreached();
|
|
1424
|
+
if (!valid(inv))
|
|
1425
|
+
return word("invitation");
|
|
1426
|
+
const id = takenAs(nameOf(inv));
|
|
1427
|
+
if (id !== void 0)
|
|
1428
|
+
return standingAt(id).ask(method, args, wanted);
|
|
1429
|
+
return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => live() ? knocking(inv, method, args, wanted, live) : Promise.resolve(word("late"))));
|
|
1430
|
+
},
|
|
1431
|
+
// Take consumes. Until now the relation lived in the knock record, under
|
|
1432
|
+
// the ward and the heir; from now it lives in the standing, under her id.
|
|
1433
|
+
// It never lives in both: two records seeded from one relation are two
|
|
1434
|
+
// lanes speaking for one line of keys and one count, and whichever spoke
|
|
1435
|
+
// last holds it while the other is refused. She would read that refusal as
|
|
1436
|
+
// the far being falling quiet, which is a thing a sovereign being may do,
|
|
1437
|
+
// so the loss would look exactly like an answer she is owed nothing of.
|
|
1438
|
+
// One relation, one home, and the invitation is spent: knocking it again
|
|
1439
|
+
// is another consumed it, and taking it again births nothing.
|
|
1440
|
+
//
|
|
1441
|
+
// So it is spoken on the relation's lane, like every other word spoken
|
|
1442
|
+
// for it. A take reads the count and the keys a knock writes, and copies
|
|
1443
|
+
// them into the standing. Off the lane it reads them between a knock's
|
|
1444
|
+
// send and its answer: the standing is born holding a number the far door
|
|
1445
|
+
// is about to honour and a key it is about to rotate past, and her first
|
|
1446
|
+
// ask on it is refused. The relation she took correctly would be dead
|
|
1447
|
+
// because a second knock on the same invitation was careless.
|
|
1448
|
+
take: async (id, inv) => {
|
|
1449
|
+
if (gone() || !valid(inv))
|
|
1450
|
+
return null;
|
|
1451
|
+
const name = nameOf(inv);
|
|
1452
|
+
return lane(`knock:${name}`, () => {
|
|
1453
|
+
if (!bind.answered[name])
|
|
1454
|
+
return Promise.resolve(null);
|
|
1455
|
+
if (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))
|
|
1456
|
+
return Promise.resolve(null);
|
|
1457
|
+
const k = bind.knocks[name];
|
|
1458
|
+
put(bind.standings, id, { ward: inv.ward, heir: inv.heir ?? null, current: k.current, next: k.next, seq: k.seq });
|
|
1459
|
+
put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
|
|
1460
|
+
delete bind.knocks[name];
|
|
1461
|
+
delete bind.answered[name];
|
|
1462
|
+
inside.wrote();
|
|
1463
|
+
return Promise.resolve(id);
|
|
1464
|
+
});
|
|
1465
|
+
},
|
|
1466
|
+
remove: (id) => {
|
|
1467
|
+
if (gone())
|
|
1468
|
+
return;
|
|
1469
|
+
drop(cells.standings, id);
|
|
1470
|
+
drop(bind.standings, id);
|
|
1471
|
+
inside.wrote();
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
const standingAt = (id) => ({
|
|
1475
|
+
id,
|
|
1476
|
+
// The keys are read now, not when the lane reaches this ask: an ask
|
|
1477
|
+
// issued while the standing stood is answered even if she drops it in
|
|
1478
|
+
// the next line. Waiting for the lane is not a reason to lose it.
|
|
1479
|
+
ask: (method, args = {}, wanted) => {
|
|
1480
|
+
if (gone())
|
|
1481
|
+
return Promise.resolve(word("dropped"));
|
|
1482
|
+
if (!named(method))
|
|
1483
|
+
return Promise.resolve(unreached());
|
|
1484
|
+
const keys = at(bind.standings, id);
|
|
1485
|
+
if (!keys)
|
|
1486
|
+
return Promise.resolve(word("dropped"));
|
|
1487
|
+
const rec = at(cells.standings, id);
|
|
1488
|
+
return bounded(wanted, (live) => lane(`ask:${id}`, () => live() ? asking(id, keys, rec, method, args, wanted, live) : Promise.resolve(word("late"))));
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
const asking = async (id, keys, rec, method, args, wanted, live = () => true) => {
|
|
1492
|
+
const out = await send(keys, method, args, wanted, rec, live);
|
|
1493
|
+
if (method === void 0 && !isSilence(out) && !isWord(out)) {
|
|
1494
|
+
if (cellFault(out, "blueprint") !== null)
|
|
1495
|
+
return silence;
|
|
1496
|
+
if (rec && live() && at(cells.standings, id) === rec) {
|
|
1497
|
+
rec.blueprint = isBlueprint(out) ? out : null;
|
|
1498
|
+
rec.digest = rec.seen = await digest(out);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return out;
|
|
1502
|
+
};
|
|
1503
|
+
const standings = new Proxy(calls, {
|
|
1504
|
+
// Own keys only: `in` would also find every name on Object's prototype,
|
|
1505
|
+
// and a standing she took as `constructor` or `toString` would answer with
|
|
1506
|
+
// Object's, not hers.
|
|
1507
|
+
get: (t, id) => typeof id === "string" && Object.hasOwn(t, id) ? t[id] : typeof id === "string" && at(cells.standings, id) ? standingAt(id) : void 0
|
|
1508
|
+
});
|
|
1509
|
+
return {
|
|
1510
|
+
cells,
|
|
1511
|
+
// A being may make. The ward refuses what it refuses the owner: a key
|
|
1512
|
+
// already booted, a class the harbor does not hold, and a throw at birth
|
|
1513
|
+
// leaves the partition as it was. Reaching into another is still the
|
|
1514
|
+
// owner's alone: what a maker may have is a relation to what she made,
|
|
1515
|
+
// by naming an id, and it is made the way every relation is. The being
|
|
1516
|
+
// made mints an occupant for her maker under the maker's own key, so she
|
|
1517
|
+
// knows who made her by that name and by nothing else; her maker knocks,
|
|
1518
|
+
// takes it under the id she gave, and holds one standing like any other.
|
|
1519
|
+
// Nothing half-lives: a relation that could not be made unmakes the
|
|
1520
|
+
// being, who is a moment old and known to nobody.
|
|
1521
|
+
boot: async (className, made, id) => {
|
|
1522
|
+
if (gone())
|
|
1523
|
+
return null;
|
|
1524
|
+
if (id !== void 0 && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id)))
|
|
1525
|
+
return null;
|
|
1526
|
+
const born = inside.instantiate(className, made);
|
|
1527
|
+
if (born === null || id === void 0)
|
|
1528
|
+
return born;
|
|
1529
|
+
const inv = await inside.relate(born, key);
|
|
1530
|
+
if (inv) {
|
|
1531
|
+
const out = await calls.knock(inv);
|
|
1532
|
+
if (!isSilence(out) && !isWord(out) && await calls.take(id, inv) === id)
|
|
1533
|
+
return born;
|
|
1534
|
+
}
|
|
1535
|
+
inside.unmake(born);
|
|
1536
|
+
return null;
|
|
1537
|
+
},
|
|
1538
|
+
occupants: {
|
|
1539
|
+
// rotation one: the ward mints the occupant's first key and gives the secret away.
|
|
1540
|
+
// it keeps the pk beside the id and nothing else. the invitation IS the key.
|
|
1541
|
+
invite: async (id, notes) => {
|
|
1542
|
+
if (gone() || reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
|
|
1543
|
+
return null;
|
|
1544
|
+
if (notes !== void 0 && cellFault(notes, "notes") !== null)
|
|
1545
|
+
return null;
|
|
1546
|
+
const k = await inside.mintKey(bind);
|
|
1547
|
+
if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
|
|
1548
|
+
return null;
|
|
1549
|
+
put(cells.occupants, id, { id, notes: notes ? JSON.parse(JSON.stringify(notes)) : {} });
|
|
1550
|
+
inside.openHeir(k.pk, key, id);
|
|
1551
|
+
put(bind.occupants, id, k.pk);
|
|
1552
|
+
inside.wrote();
|
|
1553
|
+
return { ward: inside.pk, heir: k.pk, secret: k.seed };
|
|
1554
|
+
},
|
|
1555
|
+
remove: (id) => {
|
|
1556
|
+
if (gone())
|
|
1557
|
+
return;
|
|
1558
|
+
drop(cells.occupants, id);
|
|
1559
|
+
const heir = at(bind.occupants, id);
|
|
1560
|
+
drop(bind.occupants, id);
|
|
1561
|
+
if (heir)
|
|
1562
|
+
inside.closeHeir(heir);
|
|
1563
|
+
inside.wrote();
|
|
1564
|
+
}
|
|
1565
|
+
},
|
|
1566
|
+
standings,
|
|
1567
|
+
// A standing at one of the things this device can do. The same three
|
|
1568
|
+
// moves her boot makes, with the harbor's root doing the inviting instead
|
|
1569
|
+
// of a being of this ward: ask the ground for the name, knock with what
|
|
1570
|
+
// comes back, take it under the id she gave. She is handed the id.
|
|
1571
|
+
//
|
|
1572
|
+
// The invitation never reaches her, and that is the difference between
|
|
1573
|
+
// this relation and every other she holds. Her own invitations are hers
|
|
1574
|
+
// to give away, because giving one away is giving away her own relation.
|
|
1575
|
+
// This one is the device's, minted for this ward alone, and a value she
|
|
1576
|
+
// could copy is a capability she could hand to anyone. A being who wants
|
|
1577
|
+
// to lend her device access to another does it in the open, by an ask of
|
|
1578
|
+
// her own that forwards to this standing, where her gate reads who is
|
|
1579
|
+
// asking and she can stop.
|
|
1580
|
+
lend: async (name, id) => {
|
|
1581
|
+
if (gone() || typeof name !== "string" || typeof id !== "string")
|
|
1582
|
+
return null;
|
|
1583
|
+
if (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))
|
|
1584
|
+
return null;
|
|
1585
|
+
const took = await inside.lend(name, async (inv) => {
|
|
1586
|
+
if (gone() || reserved(id) || at(cells.standings, id) || at(cells.occupants, id))
|
|
1587
|
+
return false;
|
|
1588
|
+
const out = await calls.knock(inv);
|
|
1589
|
+
if (isSilence(out) || isWord(out))
|
|
1590
|
+
return false;
|
|
1591
|
+
return await calls.take(id, inv) === id;
|
|
1592
|
+
});
|
|
1593
|
+
return took ? id : null;
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// ../quo/dist/ward/owner.js
|
|
1599
|
+
var str2 = { type: "string" };
|
|
1600
|
+
var OWNER_ASKS = [
|
|
1601
|
+
{ name: "boot", description: "boot a being by class name, under a key the owner chooses", input: { type: "object", properties: { key: str2, class: str2 }, required: ["key", "class"] } },
|
|
1602
|
+
{ name: "public", description: "mark a booted being as the one public being of the ward, reached by anyone at the bare pk; null takes the mark off and leaves her booted", input: { type: "object", properties: { key: str2 }, required: ["key"] } },
|
|
1603
|
+
{ name: "invite", description: "mint an invitation on a being of the ward, under the id she will know the occupant by, with the notes she will read on it; on the ward pk it mints an owner, and only the root may", input: { type: "object", properties: { being: str2, id: str2, notes: { type: "object" } }, required: ["being", "id"] } },
|
|
1604
|
+
{
|
|
1605
|
+
name: "knock",
|
|
1606
|
+
description: "knock for a being of the ward with an invitation, and take the standing under id if answered; being is a key booted, or { boot: class, key } to boot her first",
|
|
1607
|
+
input: { type: "object", properties: { being: { description: "a key, or { boot, key }" }, id: str2, invitation: { type: "object" }, method: str2, args: { type: "object" }, wanted: { type: "object", properties: { time: { type: "number" } } } }, required: ["being", "id", "invitation"] }
|
|
1608
|
+
},
|
|
1609
|
+
{ name: "remove", description: "take a relation out of a being of the ward by id, occupant or standing; on the ward pk it unseats an owner, and only the root may", input: { type: "object", properties: { being: str2, id: str2 }, required: ["being", "id"] } },
|
|
1610
|
+
{ name: "unboot", description: "take a being out of the ward, with every relation she holds; her occupants hear removed, and the ward itself is refused", input: { type: "object", properties: { being: str2 }, required: ["being"] } },
|
|
1611
|
+
{
|
|
1612
|
+
name: "ask",
|
|
1613
|
+
description: "ask a being of the ward, as the owner; with no being it is the public being asked as nobody, which is what a stranger would hear",
|
|
1614
|
+
input: { type: "object", properties: { being: str2, method: str2, args: { type: "object" }, wanted: { type: "object", properties: { time: { type: "number" } } } } }
|
|
1615
|
+
}
|
|
1616
|
+
];
|
|
1617
|
+
async function ownerAnswer(w, asker, method, args) {
|
|
1618
|
+
const named = (v) => typeof v === "string" ? v : null;
|
|
1619
|
+
if (method === void 0)
|
|
1620
|
+
return describe(w);
|
|
1621
|
+
if (method === "boot") {
|
|
1622
|
+
const key = named(args.key), className = named(args.class);
|
|
1623
|
+
if (key === null || className === null)
|
|
1624
|
+
return { error: "no such class, or key taken" };
|
|
1625
|
+
const door2 = w.instantiate(className, key);
|
|
1626
|
+
if (door2 === "threw")
|
|
1627
|
+
return { error: "threw at birth" };
|
|
1628
|
+
return door2 ? { booted: door2.key } : { error: "no such class, or key taken" };
|
|
1629
|
+
}
|
|
1630
|
+
if (method === "public") {
|
|
1631
|
+
if (args.key === null) {
|
|
1632
|
+
w.setPublic(null);
|
|
1633
|
+
return { public: null };
|
|
1634
|
+
}
|
|
1635
|
+
const key = named(args.key);
|
|
1636
|
+
if (key === null || key === w.pk || !w.doors.has(key))
|
|
1637
|
+
return { error: "no such being" };
|
|
1638
|
+
const standing2 = w.publicKey();
|
|
1639
|
+
if (standing2 !== null && standing2 !== key && w.doors.has(standing2))
|
|
1640
|
+
return { error: "a ward has one public being" };
|
|
1641
|
+
w.setPublic(key);
|
|
1642
|
+
return { public: key };
|
|
1643
|
+
}
|
|
1644
|
+
if (method === "invite") {
|
|
1645
|
+
const being = named(args.being), id = named(args.id);
|
|
1646
|
+
if (being === null || id === null)
|
|
1647
|
+
return { error: "no such being" };
|
|
1648
|
+
if (being === w.pk && asker.id !== OWNER)
|
|
1649
|
+
return { error: "no such being" };
|
|
1650
|
+
const notes = args.notes;
|
|
1651
|
+
const door2 = w.doors.get(being);
|
|
1652
|
+
if (!door2)
|
|
1653
|
+
return { error: "no such being" };
|
|
1654
|
+
return await door2.stance.occupants.invite(id, notes !== null && typeof notes === "object" && !Array.isArray(notes) ? notes : void 0);
|
|
1655
|
+
}
|
|
1656
|
+
if (method === "knock") {
|
|
1657
|
+
const b = args.being;
|
|
1658
|
+
let door2 = typeof b === "string" ? w.doors.get(b) : void 0;
|
|
1659
|
+
if (!door2 && b && typeof b === "object") {
|
|
1660
|
+
const key = named(b.key), className = named(b.boot);
|
|
1661
|
+
if (key === null || className === null)
|
|
1662
|
+
return { error: "no such being" };
|
|
1663
|
+
const made = w.instantiate(className, key);
|
|
1664
|
+
if (made === "threw")
|
|
1665
|
+
return { error: "threw at birth" };
|
|
1666
|
+
door2 = w.doors.get(key);
|
|
1667
|
+
}
|
|
1668
|
+
const id = named(args.id);
|
|
1669
|
+
if (!door2 || id === null || door2.key === w.pk)
|
|
1670
|
+
return { error: "no such being" };
|
|
1671
|
+
const inv = args.invitation;
|
|
1672
|
+
const out = await door2.stance.standings.knock(inv, args.method, args.args ?? {}, args.wanted);
|
|
1673
|
+
if (isSilence(out))
|
|
1674
|
+
return { error: "silence" };
|
|
1675
|
+
if (isWord(out))
|
|
1676
|
+
return { error: wordOf(out) };
|
|
1677
|
+
return { taken: await door2.stance.standings.take(id, inv), answer: out };
|
|
1678
|
+
}
|
|
1679
|
+
if (method === "remove") {
|
|
1680
|
+
const being = named(args.being), id = named(args.id);
|
|
1681
|
+
if (being === null || id === null)
|
|
1682
|
+
return { error: "no such being" };
|
|
1683
|
+
if (being === w.pk && asker.id !== OWNER)
|
|
1684
|
+
return { error: "no such being" };
|
|
1685
|
+
const door2 = w.doors.get(being);
|
|
1686
|
+
if (!door2)
|
|
1687
|
+
return { error: "no such being" };
|
|
1688
|
+
if (Object.hasOwn(door2.cells.occupants, id))
|
|
1689
|
+
door2.stance.occupants.remove(id);
|
|
1690
|
+
else if (Object.hasOwn(door2.cells.standings, id))
|
|
1691
|
+
door2.stance.standings.remove(id);
|
|
1692
|
+
else
|
|
1693
|
+
return { error: "nothing to remove" };
|
|
1694
|
+
return { removed: id };
|
|
1695
|
+
}
|
|
1696
|
+
if (method === "unboot") {
|
|
1697
|
+
const being = named(args.being);
|
|
1698
|
+
if (being === null || being === w.pk)
|
|
1699
|
+
return { error: "no such being" };
|
|
1700
|
+
const removed = w.unboot(being);
|
|
1701
|
+
return removed === null ? { error: "no such being" } : { unbooted: being, removed };
|
|
1702
|
+
}
|
|
1703
|
+
if (method === "ask") {
|
|
1704
|
+
const being = args.being === void 0 ? null : named(args.being);
|
|
1705
|
+
if (args.being !== void 0 && being === null)
|
|
1706
|
+
return { error: "no such being" };
|
|
1707
|
+
if (args.method !== void 0 && typeof args.method !== "string")
|
|
1708
|
+
return { error: "an ask is named by a word" };
|
|
1709
|
+
const key = being ?? w.publicKey();
|
|
1710
|
+
const door2 = key === null ? void 0 : w.doors.get(key);
|
|
1711
|
+
if (!door2 || door2.key === w.pk)
|
|
1712
|
+
return { error: "no such being" };
|
|
1713
|
+
const bound = being !== null;
|
|
1714
|
+
const out = await within(allow(args.wanted).time, answered2(door2, bound ? { id: OWNER } : {}, args.method, args.args ?? {}, bound));
|
|
1715
|
+
if (out === LATE)
|
|
1716
|
+
return { error: "late" };
|
|
1717
|
+
if ("quo" in out)
|
|
1718
|
+
return { error: out.quo };
|
|
1719
|
+
if (!("object" in out))
|
|
1720
|
+
return { error: "silence" };
|
|
1721
|
+
return out.object;
|
|
1722
|
+
}
|
|
1723
|
+
return { error: "unknown ask" };
|
|
1724
|
+
}
|
|
1725
|
+
async function describe(w) {
|
|
1726
|
+
const beings = {};
|
|
1727
|
+
const asked = [...w.doors].filter(([key]) => key !== w.pk);
|
|
1728
|
+
const digests = await Promise.all(asked.map(async ([, door2]) => await within(DEFAULT.time, seen(door2, { id: OWNER }))));
|
|
1729
|
+
for (const [i, [key, door2]] of asked.entries()) {
|
|
1730
|
+
const d = digests[i];
|
|
1731
|
+
put(beings, key, { class: door2.cells.class ?? null, public: w.publicKey() === key, digest: d === LATE ? null : d });
|
|
1732
|
+
}
|
|
1733
|
+
for (const [key, cls] of Object.entries(w.absent()))
|
|
1734
|
+
put(beings, key, { class: cls, public: w.publicKey() === key, digest: null, absent: true });
|
|
1735
|
+
return {
|
|
1736
|
+
asks: OWNER_ASKS,
|
|
1737
|
+
notes: { pk: w.pk, beings }
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// ../quo/dist/ward/ward.js
|
|
1742
|
+
async function Ward(ground) {
|
|
1743
|
+
const w = await Self.boot(ground);
|
|
1744
|
+
return { door: w.door, ask: w.ask };
|
|
1745
|
+
}
|
|
1746
|
+
var Self = class _Self {
|
|
1747
|
+
key;
|
|
1748
|
+
pk;
|
|
1749
|
+
g;
|
|
1750
|
+
p;
|
|
1751
|
+
heirs;
|
|
1752
|
+
doors = /* @__PURE__ */ new Map();
|
|
1753
|
+
#birthing = /* @__PURE__ */ new Set();
|
|
1754
|
+
// keys whose constructor is running: taken, though no door and no row say so yet
|
|
1755
|
+
door;
|
|
1756
|
+
ask;
|
|
1757
|
+
static async boot(ground) {
|
|
1758
|
+
return new _Self(ground, await wardKey(ground.seed));
|
|
1759
|
+
}
|
|
1760
|
+
constructor(ground, key) {
|
|
1761
|
+
this.g = ground;
|
|
1762
|
+
this.key = key;
|
|
1763
|
+
this.pk = key.pk;
|
|
1764
|
+
this.p = open(ground.memory);
|
|
1765
|
+
this.#wrote(HEAD);
|
|
1766
|
+
this.heirs = new Heirs(this.p, () => this.#wrote(HEAD));
|
|
1767
|
+
this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n), () => this.#keep());
|
|
1768
|
+
this.#boot(this.pk, () => this);
|
|
1769
|
+
for (const [k, c] of Object.entries(this.p.beings)) {
|
|
1770
|
+
const className = c.class;
|
|
1771
|
+
if (k === this.pk || !className)
|
|
1772
|
+
continue;
|
|
1773
|
+
try {
|
|
1774
|
+
this.#boot(k, (stance) => this.g.instantiate(className, stance));
|
|
1775
|
+
} catch {
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
this.ask = async (method, args = {}) => {
|
|
1779
|
+
try {
|
|
1780
|
+
const out = await this.answer({ id: OWNER }, method, args);
|
|
1781
|
+
if (!await this.#keep().catch(() => false))
|
|
1782
|
+
return silence;
|
|
1783
|
+
return isSilence(out) || isWord(out) ? silence : out;
|
|
1784
|
+
} catch {
|
|
1785
|
+
return silence;
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
// ---- the ward as a being. asked only by its owners: the root, through
|
|
1790
|
+
// the ask pointer, and its occupants, through the door, whom only the root
|
|
1791
|
+
// can invite. A stranger cannot be a named asker here, because the door
|
|
1792
|
+
// names nobody the ward did not bind.
|
|
1793
|
+
async answer(asker, method, args = {}) {
|
|
1794
|
+
if (asker.id === void 0)
|
|
1795
|
+
return silence;
|
|
1796
|
+
return ownerAnswer({
|
|
1797
|
+
pk: this.pk,
|
|
1798
|
+
doors: this.doors,
|
|
1799
|
+
absent: () => this.#absent(),
|
|
1800
|
+
publicKey: () => this.p.public,
|
|
1801
|
+
instantiate: (className, key) => this.#instantiate(className, key),
|
|
1802
|
+
unboot: (key) => this.#unboot(key),
|
|
1803
|
+
setPublic: (key) => {
|
|
1804
|
+
this.p.public = key;
|
|
1805
|
+
this.#wrote(HEAD);
|
|
1806
|
+
}
|
|
1807
|
+
}, asker, method, args);
|
|
1808
|
+
}
|
|
1809
|
+
// ---- ward functions. on the object. no stance reaches them.
|
|
1810
|
+
// The partition was written, and which row of it: a being by her key, or
|
|
1811
|
+
// the head. The harbor is told, and nothing more: what it does with the
|
|
1812
|
+
// word is its own, and the ward never learns. Every write says its row,
|
|
1813
|
+
// because a harbor told only that something moved must keep all of it, and
|
|
1814
|
+
// a being's row is the one part of a partition that grows without limit.
|
|
1815
|
+
#wrote(row) {
|
|
1816
|
+
this.g.wrote?.(row);
|
|
1817
|
+
}
|
|
1818
|
+
// Whether what was written is kept, asked once at the end of an arrival
|
|
1819
|
+
// and never during one. A harbor that keeps nothing was never going to
|
|
1820
|
+
// refuse anything, so its silence here is yes: a ward driven with no store
|
|
1821
|
+
// under it answers exactly as it did before a harbor could say no.
|
|
1822
|
+
async #keep() {
|
|
1823
|
+
return this.g.keep ? await this.g.keep() : true;
|
|
1824
|
+
}
|
|
1825
|
+
// The rows no door holds this run: beings whose class threw at birth or
|
|
1826
|
+
// is not the harbor's to give. Key to class.
|
|
1827
|
+
#absent() {
|
|
1828
|
+
const out = {};
|
|
1829
|
+
for (const [k, c] of Object.entries(this.p.beings))
|
|
1830
|
+
if (k !== this.pk && !this.doors.has(k))
|
|
1831
|
+
put(out, k, c.class ?? null);
|
|
1832
|
+
return out;
|
|
1833
|
+
}
|
|
1834
|
+
// A new being under a key nobody holds. A key with a row in the partition
|
|
1835
|
+
// is booted, whether a door holds it this run or not: an absent being's
|
|
1836
|
+
// row is hers, with every relation in it, and no class is booted over it.
|
|
1837
|
+
// A throw at birth is that boot's refusal and nothing half-lives; the
|
|
1838
|
+
// ward says which, since the owner hears objects.
|
|
1839
|
+
#instantiate(className, key) {
|
|
1840
|
+
if (typeof key !== "string" || typeof className !== "string" || !key || this.doors.has(key) || at(this.p.beings, key) || this.#birthing.has(key))
|
|
1841
|
+
return null;
|
|
1842
|
+
let door2;
|
|
1843
|
+
this.#birthing.add(key);
|
|
1844
|
+
try {
|
|
1845
|
+
door2 = this.#boot(key, (stance) => this.g.instantiate(className, stance));
|
|
1846
|
+
} catch {
|
|
1847
|
+
return "threw";
|
|
1848
|
+
} finally {
|
|
1849
|
+
this.#birthing.delete(key);
|
|
1850
|
+
}
|
|
1851
|
+
if (!door2)
|
|
1852
|
+
return null;
|
|
1853
|
+
unguarded(door2.cells).class = className;
|
|
1854
|
+
this.#wrote(key);
|
|
1855
|
+
return door2;
|
|
1856
|
+
}
|
|
1857
|
+
// The inverse of boot, and the only way a being leaves a ward. Her
|
|
1858
|
+
// relations go first and by the same calls she would have used herself,
|
|
1859
|
+
// so every occupant hears `removed` at the door from the keys it kept,
|
|
1860
|
+
// rather than meeting a being who is simply not there any more. What is
|
|
1861
|
+
// left after that is her cells and her bind table, and they go with her:
|
|
1862
|
+
// a row naming a being no door holds would be read on the next restart
|
|
1863
|
+
// and boot her again. An absent being leaves the same way: she has no
|
|
1864
|
+
// stance to speak for her, so the ward closes her heirs itself.
|
|
1865
|
+
#unboot(key) {
|
|
1866
|
+
if (key === this.pk)
|
|
1867
|
+
return null;
|
|
1868
|
+
const door2 = this.doors.get(key);
|
|
1869
|
+
const cells = door2?.cells ?? at(this.p.beings, key);
|
|
1870
|
+
if (!cells)
|
|
1871
|
+
return null;
|
|
1872
|
+
const ids = (r) => r !== null && typeof r === "object" ? Object.keys(r) : [];
|
|
1873
|
+
const occupants = ids(cells.occupants), standings = ids(cells.standings);
|
|
1874
|
+
if (door2) {
|
|
1875
|
+
for (const id of occupants)
|
|
1876
|
+
door2.stance.occupants.remove(id);
|
|
1877
|
+
for (const id of standings)
|
|
1878
|
+
door2.stance.standings.remove(id);
|
|
1879
|
+
} else {
|
|
1880
|
+
const bind = at(this.p.bind, key);
|
|
1881
|
+
for (const heir of Object.values(bind?.occupants ?? {}))
|
|
1882
|
+
this.heirs.close(heir);
|
|
1883
|
+
}
|
|
1884
|
+
if (this.p.public === key)
|
|
1885
|
+
this.p.public = null;
|
|
1886
|
+
drop(this.p.beings, key);
|
|
1887
|
+
drop(this.p.bind, key);
|
|
1888
|
+
this.doors.delete(key);
|
|
1889
|
+
this.#wrote(key);
|
|
1890
|
+
this.#wrote(HEAD);
|
|
1891
|
+
return [...occupants, ...standings];
|
|
1892
|
+
}
|
|
1893
|
+
// Nothing is written until there is somebody to write it for: a class the
|
|
1894
|
+
// harbor does not know makes no being, and a boot that made nobody must
|
|
1895
|
+
// leave the partition as it found it.
|
|
1896
|
+
//
|
|
1897
|
+
// `make` is who to build with the stance this raises. Two callers and two
|
|
1898
|
+
// makers: the ground's `instantiate` for a being of the ward, and the ward
|
|
1899
|
+
// itself, which is the first being in its own map and is already made.
|
|
1900
|
+
#boot(key, make) {
|
|
1901
|
+
const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote(key));
|
|
1902
|
+
const bind = at(this.p.bind, key) ?? emptyBind();
|
|
1903
|
+
const stance = buildStance({
|
|
1904
|
+
pk: this.pk,
|
|
1905
|
+
// This stance, not merely this key: unboot and boot again under the
|
|
1906
|
+
// same key makes a new being, and the old stance is not hers.
|
|
1907
|
+
live: () => this.doors.get(key)?.stance === stance,
|
|
1908
|
+
mintKey: (b) => this.#mintKey(b, key),
|
|
1909
|
+
openHeir: (heir, being2, id) => this.heirs.open(heir, being2, id),
|
|
1910
|
+
closeHeir: (heir) => this.heirs.close(heir),
|
|
1911
|
+
send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted, key),
|
|
1912
|
+
// A throw at birth is null to her, not a throw in her method: she asked
|
|
1913
|
+
// for a being and got none, and her own answer is still hers to give.
|
|
1914
|
+
instantiate: (className, k) => {
|
|
1915
|
+
const made = this.#instantiate(className, k);
|
|
1916
|
+
return made && made !== "threw" ? made.key : null;
|
|
1917
|
+
},
|
|
1918
|
+
// The two halves of a maker's way back, each the ward's own call: the
|
|
1919
|
+
// being made mints an occupant for her maker, and a relation that
|
|
1920
|
+
// never came about takes her out again.
|
|
1921
|
+
relate: async (k, id) => await this.doors.get(k)?.stance.occupants.invite(id) ?? null,
|
|
1922
|
+
unmake: (k) => void this.#unboot(k),
|
|
1923
|
+
// What this device lends this ward's beings. The ward knocks and takes
|
|
1924
|
+
// inside the harbor's own call and reads nothing in the value: which
|
|
1925
|
+
// names there are, which ward may ask for one, and what becomes of one
|
|
1926
|
+
// she did not take, is the harbor's and no word of the ward.
|
|
1927
|
+
lend: async (name, take) => await this.g.lend?.(name, take) ?? false,
|
|
1928
|
+
wrote: () => this.#wrote(key)
|
|
1929
|
+
}, key, cells, bind);
|
|
1930
|
+
const being = make(stance);
|
|
1931
|
+
if (!being || typeof being.answer !== "function")
|
|
1932
|
+
return null;
|
|
1933
|
+
put(this.p.beings, key, cells);
|
|
1934
|
+
put(this.p.bind, key, bind);
|
|
1935
|
+
const door2 = { key, cells, bind, stance, being };
|
|
1936
|
+
this.doors.set(key, door2);
|
|
1937
|
+
this.#wrote(key);
|
|
1938
|
+
return door2;
|
|
1939
|
+
}
|
|
1940
|
+
async #mintKey(bind, row) {
|
|
1941
|
+
const k = await beingKey(this.g.random(32));
|
|
1942
|
+
bind.minted.push(k.pk);
|
|
1943
|
+
if (bind.minted.length > MINTED)
|
|
1944
|
+
bind.minted.splice(0, bind.minted.length - MINTED);
|
|
1945
|
+
this.#wrote(row);
|
|
1946
|
+
return k;
|
|
1947
|
+
}
|
|
1948
|
+
// One send for every door, the ward's own included. Mine: never leaves.
|
|
1949
|
+
// Not mine: carry. Signs as the standing's current key, announces its
|
|
1950
|
+
// next, seals to the far ward, opens the reply with the ephemeral secret,
|
|
1951
|
+
// and rotates to the announced key once the far door has answered under
|
|
1952
|
+
// the current one.
|
|
1953
|
+
async #send(bind, keys, method, args, wanted, row) {
|
|
1954
|
+
const lower = this.g.calling?.();
|
|
1955
|
+
try {
|
|
1956
|
+
return await this.#sent(bind, keys, method, args, wanted, row);
|
|
1957
|
+
} finally {
|
|
1958
|
+
lower?.();
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
async #sent(bind, keys, method, args, wanted, row) {
|
|
1962
|
+
const budget = allow(wanted);
|
|
1963
|
+
const by = (await beingKey(unhex(keys.current))).pk;
|
|
1964
|
+
if (keys.next === null && keys.heir !== null)
|
|
1965
|
+
keys.next = (await this.#mintKey(bind, row)).seed;
|
|
1966
|
+
const next = keys.next === null ? null : (await beingKey(unhex(keys.next))).pk;
|
|
1967
|
+
const seq = keys.seq += 1;
|
|
1968
|
+
this.#wrote(row);
|
|
1969
|
+
if (args === null || typeof args !== "object" || Array.isArray(args) || cellFault(args, "args") !== null)
|
|
1970
|
+
return unreached();
|
|
1971
|
+
let sealed;
|
|
1972
|
+
try {
|
|
1973
|
+
sealed = await sealAsk(keys.heir, { by, next, seq, time: budget.time, method, args }, unhex(keys.current), wardPadlock(keys.ward), this.g.random(32));
|
|
1974
|
+
} catch {
|
|
1975
|
+
return unreached();
|
|
1976
|
+
}
|
|
1977
|
+
const { bytes, ephemeral } = sealed;
|
|
1978
|
+
const carried = (keys.ward === this.pk ? this.door(new Uint8Array(bytes)).then((r) => r.bytes) : this.g.carry(keys.ward, bytes)).then((b) => b, () => void 0);
|
|
1979
|
+
const out = await within(budget.time, carried);
|
|
1980
|
+
if (out === LATE)
|
|
1981
|
+
return word("late");
|
|
1982
|
+
if (out === void 0)
|
|
1983
|
+
return unreached();
|
|
1984
|
+
const reply = await openReply(out, ephemeral, wardSignPk(keys.ward));
|
|
1985
|
+
if (!reply)
|
|
1986
|
+
return silence;
|
|
1987
|
+
if ("object" in reply && keys.next !== null) {
|
|
1988
|
+
keys.current = keys.next;
|
|
1989
|
+
keys.next = null;
|
|
1990
|
+
}
|
|
1991
|
+
this.#wrote(row);
|
|
1992
|
+
return reply;
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
// dock/notifications.ts
|
|
1997
|
+
var seed = () => globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
1998
|
+
var Notifications = class extends Faculty {
|
|
1999
|
+
// The banner key, as hex: the seed it was made from, kept so the pair can
|
|
2000
|
+
// be made again, and the public half a device hands out.
|
|
2001
|
+
static cells = { watched: {}, minted: 0, banner: null };
|
|
2002
|
+
static asks = {
|
|
2003
|
+
notify: { description: "show the human a line", input: { type: "object", properties: { title: { type: "string" }, body: { type: "string" } }, required: ["title"] } },
|
|
2004
|
+
banner: { description: "this device's banner key, the public half, which a world seals a notification to", input: { type: "object" } }
|
|
2005
|
+
};
|
|
2006
|
+
notify(_args) {
|
|
2007
|
+
return Promise.resolve({ error: "this box shows no notifications" });
|
|
2008
|
+
}
|
|
2009
|
+
// The pair, minted once and kept. Every call after the first is the same
|
|
2010
|
+
// key, because a device that minted a new one on every ask would be a
|
|
2011
|
+
// device whose worlds all seal to a key it has forgotten.
|
|
2012
|
+
async key() {
|
|
2013
|
+
if (typeof this.cells.banner !== "string") {
|
|
2014
|
+
this.cells.banner = arithmetic_exports.hex(seed());
|
|
2015
|
+
await this.plant(await arithmetic_exports.sealingPair(arithmetic_exports.unhex(this.cells.banner)));
|
|
2016
|
+
}
|
|
2017
|
+
return arithmetic_exports.sealingPair(arithmetic_exports.unhex(this.cells.banner));
|
|
2018
|
+
}
|
|
2019
|
+
async banner(_args) {
|
|
2020
|
+
return { banner: arithmetic_exports.hex((await this.key()).pk) };
|
|
2021
|
+
}
|
|
2022
|
+
// Where this terrain's notification process reads the secret half. The
|
|
2023
|
+
// base plants nothing, which is right for a terrain that draws its own
|
|
2024
|
+
// banners in the same process; a phone overrides it and writes into the
|
|
2025
|
+
// keychain group its extension shares with the app.
|
|
2026
|
+
plant(_pair) {
|
|
2027
|
+
return Promise.resolve();
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
// dock/webpush.ts
|
|
2032
|
+
var subtle3 = () => globalThis.crypto.subtle;
|
|
2033
|
+
var ES256 = { name: "ECDSA", namedCurve: "P-256" };
|
|
2034
|
+
var b64url = (bytes) => {
|
|
2035
|
+
let binary = "";
|
|
2036
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
2037
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
2038
|
+
};
|
|
2039
|
+
var unb64url = (v) => {
|
|
2040
|
+
const binary = atob(v.replaceAll("-", "+").replaceAll("_", "/"));
|
|
2041
|
+
const out = new Uint8Array(binary.length);
|
|
2042
|
+
for (let at3 = 0; at3 < binary.length; at3 += 1) out[at3] = binary.charCodeAt(at3);
|
|
2043
|
+
return out;
|
|
2044
|
+
};
|
|
2045
|
+
function publicOf(key) {
|
|
2046
|
+
if (typeof key.x !== "string" || typeof key.y !== "string") throw new Error("a vapid key is a P-256 JWK");
|
|
2047
|
+
const x = unb64url(key.x), y = unb64url(key.y);
|
|
2048
|
+
const raw = new Uint8Array(65);
|
|
2049
|
+
raw[0] = 4;
|
|
2050
|
+
raw.set(x, 1);
|
|
2051
|
+
raw.set(y, 33);
|
|
2052
|
+
return b64url(raw);
|
|
2053
|
+
}
|
|
2054
|
+
async function mintVapid() {
|
|
2055
|
+
const pair = await subtle3().generateKey(ES256, true, ["sign", "verify"]);
|
|
2056
|
+
const key = await subtle3().exportKey("jwk", pair.privateKey);
|
|
2057
|
+
return { key, publicKey: publicOf(key) };
|
|
2058
|
+
}
|
|
2059
|
+
var OVERHEAD = 16 + 4 + 1 + 65 + 1 + 16;
|
|
2060
|
+
|
|
2061
|
+
// dock/push.ts
|
|
2062
|
+
var isToken = (v) => v !== null && typeof v === "object" && !Array.isArray(v) && typeof v.kind === "string";
|
|
2063
|
+
var SEAL = 4096;
|
|
2064
|
+
function banner(o) {
|
|
2065
|
+
const text3 = (v) => typeof v === "string" && v.trim() ? v.trim() : "";
|
|
2066
|
+
const title2 = text3(o.title) || text3(o.from) || "quo";
|
|
2067
|
+
const body = text3(o.body) || text3(o.said) || Object.entries(o).filter(([k]) => k !== "title" && k !== "from").map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
|
|
2068
|
+
return { title: title2, body };
|
|
2069
|
+
}
|
|
2070
|
+
async function sealBanner(line, key) {
|
|
2071
|
+
if (!/^[0-9a-f]{64}$/.test(key)) return null;
|
|
2072
|
+
try {
|
|
2073
|
+
const boxed = await arithmetic_exports.box(new TextEncoder().encode(JSON.stringify(line)), arithmetic_exports.unhex(key), globalThis.crypto.getRandomValues(new Uint8Array(32)));
|
|
2074
|
+
const wire = arithmetic_exports.hex(boxed.bytes);
|
|
2075
|
+
return wire.length > SEAL ? null : wire;
|
|
2076
|
+
} catch {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
var root = (_occ, asker) => asker.id === OWNER;
|
|
2081
|
+
var Push = class extends Faculty {
|
|
2082
|
+
// What she keeps, beside every faculty's: the world's doorbell key, a P-256
|
|
2083
|
+
// JWK she mints herself the first time a terrain asks for one, so a Web
|
|
2084
|
+
// Push is signed under a key that lives with the wards and in no file; and
|
|
2085
|
+
// the bell service a phone's ring is handed to when its token names none,
|
|
2086
|
+
// a row the root keeps, since whose service it is belongs to an estate.
|
|
2087
|
+
static cells = { watched: {}, minted: 0, key: null, service: null };
|
|
2088
|
+
static asks = {
|
|
2089
|
+
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"] } },
|
|
2090
|
+
kinds: { description: "which kinds this box can ring", input: { type: "object" } },
|
|
2091
|
+
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 }
|
|
2092
|
+
};
|
|
2093
|
+
// The doorbell key, minted once and kept. A terrain asks for it when it
|
|
2094
|
+
// serves a web route; a box that never does holds none and rings no tabs.
|
|
2095
|
+
async mint() {
|
|
2096
|
+
if (this.cells.key === null) this.cells.key = (await mintVapid()).key;
|
|
2097
|
+
return this.cells.key;
|
|
2098
|
+
}
|
|
2099
|
+
get key() {
|
|
2100
|
+
return this.cells.key;
|
|
2101
|
+
}
|
|
2102
|
+
get service() {
|
|
2103
|
+
return typeof this.cells.service === "string" ? this.cells.service : void 0;
|
|
2104
|
+
}
|
|
2105
|
+
keep(args) {
|
|
2106
|
+
const service = args.service;
|
|
2107
|
+
if (service !== null && (typeof service !== "string" || !/^https?:\/\/\S+$/.test(service))) return { error: "a bell service is a URL over http or https, or null" };
|
|
2108
|
+
this.cells.service = service === null ? null : service.replace(/\/$/, "");
|
|
2109
|
+
return { service: this.cells.service };
|
|
2110
|
+
}
|
|
2111
|
+
// What this box can ring. A terrain overrides it and builds its rings from
|
|
2112
|
+
// its environment and this being's cells; the base rings nothing.
|
|
2113
|
+
get rings() {
|
|
2114
|
+
return {};
|
|
2115
|
+
}
|
|
2116
|
+
kinds() {
|
|
2117
|
+
return { kinds: Object.keys(this.rings) };
|
|
2118
|
+
}
|
|
2119
|
+
async ring(args) {
|
|
2120
|
+
const token = args.token;
|
|
2121
|
+
if (!isToken(token)) return { error: "a token names its kind" };
|
|
2122
|
+
const sealed = args.sealed;
|
|
2123
|
+
if (sealed !== void 0 && typeof sealed !== "string") return { error: "a seal is text" };
|
|
2124
|
+
if (typeof sealed === "string" && sealed.length > SEAL) return { error: `a seal is at most ${SEAL} bytes` };
|
|
2125
|
+
const said2 = args.line;
|
|
2126
|
+
if (said2 !== void 0 && (said2 === null || typeof said2 !== "object" || Array.isArray(said2) || typeof said2.title !== "string" || typeof said2.body !== "string")) return { error: "a line is a title and a body" };
|
|
2127
|
+
const line = said2;
|
|
2128
|
+
const r = Object.hasOwn(this.rings, token.kind) ? this.rings[token.kind] : void 0;
|
|
2129
|
+
if (!r) return { error: `this box cannot ring a ${token.kind}` };
|
|
2130
|
+
try {
|
|
2131
|
+
await r.ring(token, sealed, line);
|
|
2132
|
+
return { rang: token.kind };
|
|
2133
|
+
} catch (e) {
|
|
2134
|
+
return { error: e instanceof Error ? e.message : "the ring threw" };
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
|
|
2139
|
+
// dock/timer.ts
|
|
2140
|
+
var MINUTE = 6e4;
|
|
2141
|
+
var DAY = 864e5;
|
|
2142
|
+
var Timer = class extends Faculty {
|
|
2143
|
+
static asks = {
|
|
2144
|
+
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"] } },
|
|
2145
|
+
due: { description: "bring my next round forward to this moment", input: { type: "object", properties: { at: { type: "number" } }, required: ["at"] } },
|
|
2146
|
+
now: { description: "this box\u2019s moment", input: { type: "object" } }
|
|
2147
|
+
};
|
|
2148
|
+
#timeout;
|
|
2149
|
+
#stopped = false;
|
|
2150
|
+
#armed = null;
|
|
2151
|
+
#round = null;
|
|
2152
|
+
// The moment she is armed for, or null while she is asleep. For the
|
|
2153
|
+
// device's own code and for a suite; it crosses no door.
|
|
2154
|
+
get armed() {
|
|
2155
|
+
return this.#armed;
|
|
2156
|
+
}
|
|
2157
|
+
// A watcher says she is due sooner than the box knew. It only ever brings
|
|
2158
|
+
// the round forward: what fires is decided at the tick, in the watcher's
|
|
2159
|
+
// own cells, so an arming that moved later on somebody's word would be a
|
|
2160
|
+
// schedule somebody else held silenced. The door is the whole gate here:
|
|
2161
|
+
// a standing at a faculty comes from `lend` alone, and this adds no second
|
|
2162
|
+
// one.
|
|
2163
|
+
due(args) {
|
|
2164
|
+
const at3 = args.at;
|
|
2165
|
+
if (typeof at3 !== "number" || !Number.isFinite(at3)) return { error: "a due carries the moment: { at }" };
|
|
2166
|
+
const when = moment(at3);
|
|
2167
|
+
if (this.#armed === null || when < this.#armed) this.#arm(when);
|
|
2168
|
+
return { armed: this.#armed };
|
|
2169
|
+
}
|
|
2170
|
+
// Whoever was just filed is ticked at once, and what she answers is what
|
|
2171
|
+
// the box arms for. One move covers a box booting, a home moving in and a
|
|
2172
|
+
// watcher born long after the box was up: she is never waiting for a round
|
|
2173
|
+
// nobody scheduled.
|
|
2174
|
+
async welcomed(id) {
|
|
2175
|
+
await this.round(Date.now(), id);
|
|
2176
|
+
}
|
|
2177
|
+
// The arming went off. A moment still well ahead is the day's cap this
|
|
2178
|
+
// class put on its own timeout, so it is armed again rather than fired: a
|
|
2179
|
+
// terrain calls this and never has to know how far out the arming was.
|
|
2180
|
+
async rang() {
|
|
2181
|
+
if (this.#stopped) return [];
|
|
2182
|
+
if (this.#armed !== null && this.#armed - Date.now() > 1e3) {
|
|
2183
|
+
this.arm(this.#armed);
|
|
2184
|
+
return [];
|
|
2185
|
+
}
|
|
2186
|
+
return this.fire(Date.now());
|
|
2187
|
+
}
|
|
2188
|
+
// The moment, read here and handed on. A device's own code may call it at a
|
|
2189
|
+
// moment it names, which is how a suite proves a wake without waiting for
|
|
2190
|
+
// one and how an operator makes a round happen now.
|
|
2191
|
+
fire(now) {
|
|
2192
|
+
return this.round(now);
|
|
2193
|
+
}
|
|
2194
|
+
// One round, and never two: a second while one is in flight is handed the
|
|
2195
|
+
// one already running. What every watcher answered decides the next
|
|
2196
|
+
// arming, and a `due` that landed while the round ran is not lost.
|
|
2197
|
+
round(now, only) {
|
|
2198
|
+
if (this.#round) return this.#round;
|
|
2199
|
+
const mine2 = this.#run(now, only);
|
|
2200
|
+
this.#round = mine2;
|
|
2201
|
+
return mine2.finally(() => {
|
|
2202
|
+
if (this.#round === mine2) this.#round = null;
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
async #run(now, only) {
|
|
2206
|
+
if (only === void 0) this.#arm(null);
|
|
2207
|
+
const woke = await this.woke((w) => ({ ...w.args, now }), only);
|
|
2208
|
+
if (woke === null) {
|
|
2209
|
+
this.stop();
|
|
2210
|
+
return [];
|
|
2211
|
+
}
|
|
2212
|
+
const named = soonest(woke);
|
|
2213
|
+
const said2 = named !== null && named > Date.now() ? named : null;
|
|
2214
|
+
const held3 = this.#armed;
|
|
2215
|
+
const at3 = said2 === null ? held3 : held3 === null ? moment(said2) : Math.min(held3, moment(said2));
|
|
2216
|
+
if (at3 !== null && at3 !== this.#armed) this.#arm(at3);
|
|
2217
|
+
else if (at3 === null && this.#armed !== null) this.#arm(null);
|
|
2218
|
+
return woke;
|
|
2219
|
+
}
|
|
2220
|
+
#arm(when) {
|
|
2221
|
+
if (this.#stopped) return;
|
|
2222
|
+
this.#armed = when;
|
|
2223
|
+
this.arm(when);
|
|
2224
|
+
}
|
|
2225
|
+
// The one thing a terrain says about itself: be woken at this moment, or at
|
|
2226
|
+
// no moment at all. A subclass replaces this and nothing else of her.
|
|
2227
|
+
//
|
|
2228
|
+
// Here it is a timeout, capped at a day and armed again when the cap goes
|
|
2229
|
+
// off. The box stays up for its work and never for this, so the handle is
|
|
2230
|
+
// unreferenced.
|
|
2231
|
+
arm(when) {
|
|
2232
|
+
clearTimeout(this.#timeout);
|
|
2233
|
+
this.#timeout = void 0;
|
|
2234
|
+
if (when === null) return;
|
|
2235
|
+
this.#timeout = setTimeout(() => void this.rang(), Math.min(Math.max(when - Date.now(), 0), DAY));
|
|
2236
|
+
this.#timeout.unref?.();
|
|
2237
|
+
}
|
|
2238
|
+
// A being her ward took out reaches nobody, so nothing is left to wake. The
|
|
2239
|
+
// device calls this when it puts the box down.
|
|
2240
|
+
stop() {
|
|
2241
|
+
this.#stopped = true;
|
|
2242
|
+
this.#armed = null;
|
|
2243
|
+
this.arm(null);
|
|
2244
|
+
}
|
|
2245
|
+
now() {
|
|
2246
|
+
return { now: Date.now() };
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
var moment = (at3) => Math.max(at3, Math.floor(at3 / MINUTE) * MINUTE + 500);
|
|
2250
|
+
function soonest(woke) {
|
|
2251
|
+
let at3 = null;
|
|
2252
|
+
for (const w of woke) {
|
|
2253
|
+
if (w.out === null || typeof w.out !== "object") continue;
|
|
2254
|
+
const next = w.out.next;
|
|
2255
|
+
if (typeof next !== "number" || !Number.isFinite(next)) continue;
|
|
2256
|
+
if (at3 === null || next < at3) at3 = next;
|
|
2257
|
+
}
|
|
2258
|
+
return at3;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// dock/apns.ts
|
|
2262
|
+
var LIFE = 45 * 60 * 1e3;
|
|
2263
|
+
|
|
2264
|
+
// dock/fcm.ts
|
|
2265
|
+
var LIFE2 = 59 * 60 * 1e3;
|
|
2266
|
+
|
|
2267
|
+
// dock/index.ts
|
|
2268
|
+
var LOCATION = "location";
|
|
2269
|
+
var NOTIFICATIONS = "notifications";
|
|
2270
|
+
var PUSH = "push";
|
|
2271
|
+
var TIMER = "timer";
|
|
2272
|
+
var FACULTIES = { [LOCATION]: "Location", [NOTIFICATIONS]: "Notifications", [PUSH]: "Push", [TIMER]: "Timer" };
|
|
2273
|
+
var FACULTY_CLASSES = { Console, Local, Location, Notifications, Push, Timer };
|
|
2274
|
+
var DOCK = "dock";
|
|
2275
|
+
async function stand(harbor, lent = FACULTIES) {
|
|
2276
|
+
const kept2 = await harbor.store.load(DOCK);
|
|
2277
|
+
const ward = harbor.wards.get(DOCK) ?? (kept2 ? await harbor.host(DOCK, kept2) : await harbor.create(DOCK, CONSOLE));
|
|
2278
|
+
return standOn(ward, lent);
|
|
2279
|
+
}
|
|
2280
|
+
async function standOn(ward, lent = FACULTIES) {
|
|
2281
|
+
for (const [name, className] of Object.entries(lent)) await ward.ask("boot", { key: name, class: className });
|
|
2282
|
+
await ward.ask("boot", { key: CONSOLE, class: "Console" });
|
|
2283
|
+
for (const name of Object.keys(lent)) {
|
|
2284
|
+
const inv = await ward.ask("invite", { being: name, id: `${CONSOLE}:${name}` });
|
|
2285
|
+
if (!isInvitation(inv)) continue;
|
|
2286
|
+
await ward.ask("knock", { being: CONSOLE, id: name, invitation: inv });
|
|
2287
|
+
}
|
|
2288
|
+
await ward.save?.();
|
|
2289
|
+
return lending(ward);
|
|
2290
|
+
}
|
|
2291
|
+
function lending(ward) {
|
|
2292
|
+
return async (name, take) => {
|
|
2293
|
+
const id = `lent:${fresh()}`;
|
|
2294
|
+
const out = await ward.ask("invite", { being: name, id });
|
|
2295
|
+
if (!isInvitation(out)) return false;
|
|
2296
|
+
const took = await take(out);
|
|
2297
|
+
if (!took) await ward.ask("remove", { being: name, id });
|
|
2298
|
+
await ward.save?.();
|
|
2299
|
+
return took;
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
var fresh = () => Array.from(globalThis.crypto.getRandomValues(new Uint8Array(8))).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2303
|
+
|
|
2304
|
+
// beings/user.ts
|
|
2305
|
+
var DESK = "desk";
|
|
2306
|
+
var BELL = "bell";
|
|
2307
|
+
var HOME = "home";
|
|
2308
|
+
var isDesk = (occ) => occ?.id === DESK;
|
|
2309
|
+
var isDevice = (occ) => occ !== void 0 && occ.id !== DESK;
|
|
2310
|
+
var rootMinted = (occ) => isDevice(occ) && occ?.notes.client === void 0;
|
|
2311
|
+
var client = (occ) => typeof occ?.notes.client === "string" ? occ.notes.client : rootMinted(occ) ? occ.id : null;
|
|
2312
|
+
var MINTINGS = ["wake", "reach", "mint", "revoke"];
|
|
2313
|
+
var OUTBOX = 32;
|
|
2314
|
+
var rang = (out) => !isSilence(out) && !isWord(out) && out !== null && typeof out === "object" && !Array.isArray(out) && out.rang !== void 0 && out.rang !== false;
|
|
2315
|
+
var held = (occ) => isDesk(occ) ? { wake: true, reach: true, mint: true, revoke: true } : { wake: occ?.notes.wake === true || rootMinted(occ), reach: occ?.notes.reach === true || rootMinted(occ), mint: occ?.notes.mint === true, revoke: occ?.notes.revoke === true };
|
|
2316
|
+
var mayWake = (occ) => held(occ).wake;
|
|
2317
|
+
var mayMint = (occ) => isDesk(occ) || isDevice(occ) && held(occ).mint;
|
|
2318
|
+
var mayRevoke = (occ) => isDesk(occ) || isDevice(occ) && held(occ).revoke;
|
|
2319
|
+
var User = class extends Carrier {
|
|
2320
|
+
static carries(occ) {
|
|
2321
|
+
return isDevice(occ) && held(occ).reach;
|
|
2322
|
+
}
|
|
2323
|
+
// `ways` holds a device's way back that could not be taken when it was
|
|
2324
|
+
// handed over: the knock was unreached, a tab whose pk the far harbor had
|
|
2325
|
+
// not bound yet, a phone in a tunnel. Nothing delivered spends nothing,
|
|
2326
|
+
// so the invitation is tried again at the next push.
|
|
2327
|
+
// The bell standing is plumbing and carries nothing: a device that may
|
|
2328
|
+
// reach what she holds sees acme and the house, and never the envoy that
|
|
2329
|
+
// wakes it. Her ways back are plumbing too, and a way back to a presence
|
|
2330
|
+
// being carries that device's faculties, `to:phone-location-where`, which
|
|
2331
|
+
// is how a model at home learns where the human is.
|
|
2332
|
+
static hidden(id) {
|
|
2333
|
+
return super.hidden(id) || id === PUSH || id.startsWith(`${BELL}:`);
|
|
2334
|
+
}
|
|
2335
|
+
// Her own door is carried to a device that may mint, and to nobody else:
|
|
2336
|
+
// `mint` is the word for a device trusted to change who is in her world,
|
|
2337
|
+
// and owning the world from that device is that trust said in full. A
|
|
2338
|
+
// device that reaches sees acme and the house, and no `home-boot`.
|
|
2339
|
+
static shows(id, occ) {
|
|
2340
|
+
return id !== HOME || held(occ).mint;
|
|
2341
|
+
}
|
|
2342
|
+
static cells = { name: "", reports: [], ways: {}, outbox: {} };
|
|
2343
|
+
// Her standing at the box's push faculty, taken again at every birth: a
|
|
2344
|
+
// standing points at a being and not at a box, and a ward that moved is
|
|
2345
|
+
// handed the new harbor's ground. It is how she rings a browser endpoint,
|
|
2346
|
+
// which is the one kind of doorbell a world holds itself, since an
|
|
2347
|
+
// endpoint answers only to the VAPID secret this world minted. Birth is
|
|
2348
|
+
// synchronous and taking a standing is not, so the promise is kept and
|
|
2349
|
+
// the ring waits on it.
|
|
2350
|
+
//
|
|
2351
|
+
// A box that lends no push rings no tabs, which is what a world with no
|
|
2352
|
+
// web route behind it is.
|
|
2353
|
+
#push;
|
|
2354
|
+
constructor(stance) {
|
|
2355
|
+
super(stance);
|
|
2356
|
+
this.#push = join(this, PUSH);
|
|
2357
|
+
}
|
|
2358
|
+
// A world whose own being she is, is a home: a device reaching her holds
|
|
2359
|
+
// a standing at home and not a membership, and she says so in her notes,
|
|
2360
|
+
// since nothing else in a describe tells a shell which of its worlds is
|
|
2361
|
+
// the human's own. She knows it by the door she holds: the being setup
|
|
2362
|
+
// knocked on the ward's own door as its owner is home's, and a member of
|
|
2363
|
+
// the same class the desk made in an org's ward holds no door and says
|
|
2364
|
+
// nothing.
|
|
2365
|
+
describe(asker) {
|
|
2366
|
+
const bp = super.describe(asker);
|
|
2367
|
+
if (!this.standings[HOME]) return bp;
|
|
2368
|
+
const notes = bp.notes !== null && typeof bp.notes === "object" && !Array.isArray(bp.notes) ? bp.notes : {};
|
|
2369
|
+
return { ...bp, notes: { ...notes, home: true } };
|
|
2370
|
+
}
|
|
2371
|
+
static asks = {
|
|
2372
|
+
hello: { description: "say hello, and hand back an invitation so she can reach you", input: { type: "object", properties: { invitation: { type: "object" } } } },
|
|
2373
|
+
whoami: { description: "who she thinks you are", input: { type: "object" }, for: isDevice },
|
|
2374
|
+
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 },
|
|
2375
|
+
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 },
|
|
2376
|
+
chores: { description: "what the agent may run", input: { type: "object" }, for: (occ) => client(occ) === "agent" },
|
|
2377
|
+
look: { description: "how she is shown", input: { type: "object" }, for: isDevice },
|
|
2378
|
+
page: { description: "her page, as a tree of values", input: { type: "object" }, for: isDevice },
|
|
2379
|
+
name: { description: "what she is called", input: { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, for: rootMinted },
|
|
2380
|
+
devices: { description: "the devices in this world, and which one you are", input: { type: "object" }, for: isDevice },
|
|
2381
|
+
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 },
|
|
2382
|
+
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 },
|
|
2383
|
+
report: { description: "what a run of yours found", input: { type: "object", properties: { event: { type: "object" }, result: {} }, required: ["event", "result"] }, for: isDevice },
|
|
2384
|
+
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 },
|
|
2385
|
+
missed: { description: "what this device missed while it was unreachable", input: { type: "object" }, for: isDevice }
|
|
2386
|
+
};
|
|
2387
|
+
// Anyone may say hello. A device that hands her an invitation in the args
|
|
2388
|
+
// is taken as a standing under its own id, so she can push to it later;
|
|
2389
|
+
// if it cannot be taken now, it is kept and tried at the next push. The
|
|
2390
|
+
// desk's first hello names her: a being does not know her own key, and the
|
|
2391
|
+
// root's setup knocks with the name it booted her under. Once named, she
|
|
2392
|
+
// keeps it.
|
|
2393
|
+
async hello(args, asker) {
|
|
2394
|
+
if (asker.id !== void 0 && args.invitation && typeof args.invitation === "object" && !Array.isArray(args.invitation)) {
|
|
2395
|
+
await this.wayBack(asker.id, args.invitation);
|
|
2396
|
+
}
|
|
2397
|
+
if (asker.id === DESK && !this.cells.name && typeof args.name === "string" && /^[\w.-]{1,80}$/.test(args.name)) this.cells.name = args.name;
|
|
2398
|
+
return { welcome: asker.id ?? null, name: this.cells.name };
|
|
2399
|
+
}
|
|
2400
|
+
// Take a device's way back, or keep it for later when the knock did not
|
|
2401
|
+
// reach. A refusal spends the heir and is final; the way is dropped.
|
|
2402
|
+
async wayBack(id, inv) {
|
|
2403
|
+
const ways = this.cells.ways;
|
|
2404
|
+
const back = await this.knock(inv);
|
|
2405
|
+
if (!isSilence(back) && !isWord(back)) {
|
|
2406
|
+
await this.take(`to:${id}`, inv);
|
|
2407
|
+
delete ways[id];
|
|
2408
|
+
return true;
|
|
2409
|
+
}
|
|
2410
|
+
if (isWord(back) && wordOf(back) !== "unreached") delete ways[id];
|
|
2411
|
+
else ways[id] = inv;
|
|
2412
|
+
return false;
|
|
2413
|
+
}
|
|
2414
|
+
whoami(_args, asker) {
|
|
2415
|
+
return { id: asker.id ?? null, client: client(this.occupant(asker)) };
|
|
2416
|
+
}
|
|
2417
|
+
// The front desk asks, or a device of hers that may mint; she mints. The
|
|
2418
|
+
// client identity goes into the occupant's notes, and that is what every
|
|
2419
|
+
// gate reads. The three words travel with it, each asked for and each
|
|
2420
|
+
// written only if the asker holds it: devices beget devices, and a device
|
|
2421
|
+
// hands on no more than it was given.
|
|
2422
|
+
async device(args, asker) {
|
|
2423
|
+
const c = typeof args.client === "string" ? args.client : null;
|
|
2424
|
+
if (c === null) return { error: "client is a string" };
|
|
2425
|
+
const mine2 = held(this.occupant(asker));
|
|
2426
|
+
const inv = await this.invite(c);
|
|
2427
|
+
if (inv === null) return { error: "id taken" };
|
|
2428
|
+
const rec = this.cells.occupants[c];
|
|
2429
|
+
if (rec) {
|
|
2430
|
+
rec.notes.client = c;
|
|
2431
|
+
for (const w of MINTINGS) if (args[w] === true && mine2[w]) rec.notes[w] = true;
|
|
2432
|
+
}
|
|
2433
|
+
return inv;
|
|
2434
|
+
}
|
|
2435
|
+
// Her way out and the device's way in are two relations, and Quo keeps
|
|
2436
|
+
// them apart. She joins them herself, here, as her own rule: she does
|
|
2437
|
+
// not reach a device she no longer admits. Without it a revoked device
|
|
2438
|
+
// stops being able to ask her and keeps receiving everything she pushes.
|
|
2439
|
+
async push(args) {
|
|
2440
|
+
const c = typeof args.client === "string" ? args.client : null;
|
|
2441
|
+
if (c === null || !this.cells.occupants[c]) return { error: "no such device, or it gave no way back" };
|
|
2442
|
+
const ways = this.cells.ways;
|
|
2443
|
+
const object = args.object ?? {};
|
|
2444
|
+
if (!this.standings[`to:${c}`] && ways[c]) await this.wayBack(c, ways[c]);
|
|
2445
|
+
const st = this.standings[`to:${c}`];
|
|
2446
|
+
if (!st) return ways[c] ? this.unreached(c, object) : { error: "no such device, or it gave no way back" };
|
|
2447
|
+
const out = await st.ask("notify", object);
|
|
2448
|
+
if (isWord(out) && wordOf(out) === "unreached") return this.unreached(c, object);
|
|
2449
|
+
return isSilence(out) ? { error: "silence" } : isWord(out) ? { error: wordOf(out) } : { pushed: out };
|
|
2450
|
+
}
|
|
2451
|
+
// Nothing was delivered, so the device is asleep, closed or in a tunnel.
|
|
2452
|
+
// The object waits in her outbox under that device, bounded and oldest
|
|
2453
|
+
// out, since a device gone for a month must not grow without end; then
|
|
2454
|
+
// she rings, and the pusher hears both facts. What the ring carries is
|
|
2455
|
+
// the device's token and nothing of the object: the device wakes, dials,
|
|
2456
|
+
// and drains it over its own standing, sealed.
|
|
2457
|
+
async unreached(id, object) {
|
|
2458
|
+
const box2 = this.cells.outbox[id] ??= [];
|
|
2459
|
+
box2.push(object);
|
|
2460
|
+
while (box2.length > OUTBOX) box2.shift();
|
|
2461
|
+
return { unreached: true, rang: await this.ring(id, object) };
|
|
2462
|
+
}
|
|
2463
|
+
// A phone is rung on the standing it handed her, with the line sealed to
|
|
2464
|
+
// the banner key it handed her beside it. Who is ringing is the standing
|
|
2465
|
+
// the ring arrives on, so the ask carries no world, no pk and no token,
|
|
2466
|
+
// and there is nothing in it to forge. A browser is rung on its endpoint
|
|
2467
|
+
// through this box's own push faculty, because an endpoint answers only
|
|
2468
|
+
// to the key this world minted, and the line goes with it unsealed:
|
|
2469
|
+
// Web Push encrypts a body to the subscription's own keys, so a second
|
|
2470
|
+
// seal would hide it from nobody it is not already hidden from.
|
|
2471
|
+
async ring(id, object) {
|
|
2472
|
+
const rec = this.cells.occupants[id];
|
|
2473
|
+
const at3 = this.standings[`${BELL}:${id}`];
|
|
2474
|
+
const key = rec?.notes.banner;
|
|
2475
|
+
if (at3 && typeof key === "string") {
|
|
2476
|
+
const sealed = await sealBanner(banner(object), key);
|
|
2477
|
+
return sealed === null ? false : rang(await at3.ask("ring", { sealed }));
|
|
2478
|
+
}
|
|
2479
|
+
const token = rec?.notes.bell;
|
|
2480
|
+
if (!isToken(token)) return false;
|
|
2481
|
+
await this.#push;
|
|
2482
|
+
const st = this.standings[PUSH];
|
|
2483
|
+
if (!st) return false;
|
|
2484
|
+
return rang(await st.ask("ring", { token, line: banner(object) }));
|
|
2485
|
+
}
|
|
2486
|
+
// A device says how it is woken, and what it says is kept on its own
|
|
2487
|
+
// occupant, so forgetting the device forgets the way to wake it in the
|
|
2488
|
+
// same act.
|
|
2489
|
+
//
|
|
2490
|
+
// A phone or a desk hands a standing at a doorbell of its own, and the
|
|
2491
|
+
// banner key that doorbell never learns. This world holds a way to wake
|
|
2492
|
+
// that one device and nothing it could hand on: no other world rings on
|
|
2493
|
+
// this standing, the device cuts it by itself at the doorbell, and the
|
|
2494
|
+
// platform's token rotating is one write there that no world is told of.
|
|
2495
|
+
//
|
|
2496
|
+
// A browser hands its endpoint instead and needs no doorbell: an endpoint
|
|
2497
|
+
// is rung only by whoever holds the VAPID secret the subscription was
|
|
2498
|
+
// made under, and that secret is this world's own.
|
|
2499
|
+
//
|
|
2500
|
+
// A store's token never arrives here. The same token at every world is
|
|
2501
|
+
// the bearer credential the doorbell exists to take away, so one is
|
|
2502
|
+
// refused where a human can read why.
|
|
2503
|
+
async bell(args, asker) {
|
|
2504
|
+
const c = client(this.occupant(asker));
|
|
2505
|
+
const rec = c === null ? void 0 : this.cells.occupants[c];
|
|
2506
|
+
if (c === null || !rec) return { error: "no client" };
|
|
2507
|
+
if (args.invitation === void 0 && args.token === void 0) {
|
|
2508
|
+
const at3 = this.standings[`${BELL}:${c}`];
|
|
2509
|
+
return { kept: at3 && typeof rec.notes.banner === "string" ? "doorbell" : isToken(rec.notes.bell) ? "web" : null };
|
|
2510
|
+
}
|
|
2511
|
+
if (isInvitation(args.invitation)) {
|
|
2512
|
+
const key = args.banner;
|
|
2513
|
+
if (typeof key !== "string" || !/^[0-9a-f]{64}$/.test(key)) return { error: "a banner key is thirty two bytes as hex" };
|
|
2514
|
+
const id = `${BELL}:${c}`;
|
|
2515
|
+
this.standings.remove(id);
|
|
2516
|
+
const out = await this.knock(args.invitation);
|
|
2517
|
+
if (isSilence(out) || isWord(out)) return { error: isSilence(out) ? "silence" : wordOf(out) };
|
|
2518
|
+
if (await this.take(id, args.invitation) === null) return { error: "that invitation was refused" };
|
|
2519
|
+
rec.notes.banner = key;
|
|
2520
|
+
delete rec.notes.bell;
|
|
2521
|
+
return { kept: "doorbell" };
|
|
2522
|
+
}
|
|
2523
|
+
if (!isToken(args.token)) return { error: "a token names its kind" };
|
|
2524
|
+
if (args.token.kind !== "web") return { error: "a device hands a standing at its own doorbell, never a store token" };
|
|
2525
|
+
rec.notes.bell = args.token;
|
|
2526
|
+
return { kept: "web" };
|
|
2527
|
+
}
|
|
2528
|
+
// The device is back. It drains what it missed and composes what the
|
|
2529
|
+
// human sees from these objects, which crossed sealed and were never on
|
|
2530
|
+
// a push service. Emptied as it is handed over: an ask that answered is
|
|
2531
|
+
// an ask that arrived, and a device that lost the answer has lost one
|
|
2532
|
+
// notification, not its relation.
|
|
2533
|
+
missed(_args, asker) {
|
|
2534
|
+
const c = client(this.occupant(asker));
|
|
2535
|
+
if (c === null) return { error: "no client" };
|
|
2536
|
+
const box2 = this.cells.outbox;
|
|
2537
|
+
const out = box2[c] ?? [];
|
|
2538
|
+
delete box2[c];
|
|
2539
|
+
return { missed: out };
|
|
2540
|
+
}
|
|
2541
|
+
// She joins a world. This is the day a homeless org gains a home: an org
|
|
2542
|
+
// whose whole existence was standings in other orgs' worlds boots a home
|
|
2543
|
+
// on a box it trusts, and that home joins each of those from there.
|
|
2544
|
+
// Nothing is converted and no id changes, because from the far world's
|
|
2545
|
+
// side this home is one more thing of hers holding a standing, exactly as
|
|
2546
|
+
// her phone is: her device there minted the invitation with `device`, and
|
|
2547
|
+
// this is the other half, the knock.
|
|
2548
|
+
//
|
|
2549
|
+
// It is a device that may mint who asks it, and that needs no word of its
|
|
2550
|
+
// own: `mint` is what shows her own door to a device, so one that holds
|
|
2551
|
+
// it can boot and invite in her world already, and adding one standing to
|
|
2552
|
+
// what she carries is less than that. In practice such a device holds
|
|
2553
|
+
// `reach` as well, since a device that joined a world it could not then
|
|
2554
|
+
// see would be a device doing something for somebody else. She hands back a way for the far being
|
|
2555
|
+
// to reach her, as a device does at hello, and a far being who refuses it
|
|
2556
|
+
// is joined all the same.
|
|
2557
|
+
async join(args) {
|
|
2558
|
+
const inv = args.invitation;
|
|
2559
|
+
if (!isInvitation(inv)) return { error: "that is not an invitation" };
|
|
2560
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
2561
|
+
if (!/^[\w.-]{1,80}$/.test(name) || name === HOME || name === PUSH) return { error: "a name is a word, and not home or the push" };
|
|
2562
|
+
if (this.standings[name]) return { error: "name taken" };
|
|
2563
|
+
const back = `from:${name}`;
|
|
2564
|
+
const mine2 = await this.invite(back);
|
|
2565
|
+
if (mine2 === null) return { error: "name taken" };
|
|
2566
|
+
const out = await this.knock(inv, "hello", { invitation: mine2 });
|
|
2567
|
+
if (isSilence(out) || isWord(out)) {
|
|
2568
|
+
this.occupants.remove(back);
|
|
2569
|
+
return { error: isSilence(out) ? "silence" : wordOf(out) };
|
|
2570
|
+
}
|
|
2571
|
+
if (out !== null && typeof out === "object" && !Array.isArray(out) && "error" in out) this.occupants.remove(back);
|
|
2572
|
+
const took = await this.take(name, inv);
|
|
2573
|
+
if (took === null) {
|
|
2574
|
+
this.occupants.remove(back);
|
|
2575
|
+
return { error: "that invitation was refused" };
|
|
2576
|
+
}
|
|
2577
|
+
return { joined: name };
|
|
2578
|
+
}
|
|
2579
|
+
// Her devices, by the names they were given, with what each was granted
|
|
2580
|
+
// and which of them is asking. Without this a human cannot use `forget`
|
|
2581
|
+
// or `push` at all: both name a device, and nobody who has just lost a
|
|
2582
|
+
// phone remembers what it was called. It is read-only and says nothing
|
|
2583
|
+
// secret; a device is a name and three words, and the keys are in the
|
|
2584
|
+
// relations rather than here.
|
|
2585
|
+
// People-shaped rows, since a page says devices are people: a name, and
|
|
2586
|
+
// a line that says what each may do and which one is asking.
|
|
2587
|
+
devices(_args, asker) {
|
|
2588
|
+
const me = client(this.occupant(asker));
|
|
2589
|
+
return Object.entries(this.cells.occupants).filter(([id]) => id !== DESK).map(([id, rec]) => {
|
|
2590
|
+
const notes = rec.notes;
|
|
2591
|
+
const name = typeof notes.client === "string" ? notes.client : id;
|
|
2592
|
+
const has = MINTINGS.filter((w) => held(rec)[w]);
|
|
2593
|
+
const line = [...name === me ? ["this one"] : [], ...has.length ? [`may ${has.join(", ")}`] : []].join(" \xB7 ");
|
|
2594
|
+
return { name, line, client: name, ...name === me ? { you: true } : {}, ...has.length ? { may: has } : {} };
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
// Revocation is one act at her, because only she knows both ids. A side
|
|
2598
|
+
// that had to remove two relations by hand could leave half of one
|
|
2599
|
+
// standing, and every side would have to remember which half.
|
|
2600
|
+
forget(args) {
|
|
2601
|
+
const c = typeof args.client === "string" ? args.client : null;
|
|
2602
|
+
if (c === null) return { error: "client is a string" };
|
|
2603
|
+
this.occupants.remove(c);
|
|
2604
|
+
this.standings.remove(`to:${c}`);
|
|
2605
|
+
this.standings.remove(`${BELL}:${c}`);
|
|
2606
|
+
delete this.cells.ways[c];
|
|
2607
|
+
delete this.cells.outbox[c];
|
|
2608
|
+
return { forgot: c };
|
|
2609
|
+
}
|
|
2610
|
+
chores() {
|
|
2611
|
+
return { chores: ["census", "report"] };
|
|
2612
|
+
}
|
|
2613
|
+
// How a device shows her: her name as the title, and what each ask is
|
|
2614
|
+
// called. Values only; the screen and the model side each read their part.
|
|
2615
|
+
look() {
|
|
2616
|
+
return {
|
|
2617
|
+
name: this.cells.name || "you",
|
|
2618
|
+
order: ["whoami", "devices", "device", "join", "push", "forget", "name"],
|
|
2619
|
+
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" } }
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
// Her page for a device: her name, who this device is, the worlds she
|
|
2623
|
+
// holds as sections, and the three things a human does here by hand,
|
|
2624
|
+
// bringing her next device in, waking one and naming her. The invitation
|
|
2625
|
+
// `device` answers is a way in, and the side paints it as the link it is,
|
|
2626
|
+
// since a being does not know where her page lives. `hello` and `report` are wiring, a
|
|
2627
|
+
// device's first word and an agent's callback; they stay in her describe
|
|
2628
|
+
// under the gate and off her page, since a page is presentation and the
|
|
2629
|
+
// gate is permission. A device that may not do a thing sees no form for
|
|
2630
|
+
// it, however the page names it.
|
|
2631
|
+
page(_args, asker) {
|
|
2632
|
+
return {
|
|
2633
|
+
kind: "stack",
|
|
2634
|
+
of: [
|
|
2635
|
+
{ kind: "text", text: this.cells.name || "you", role: "title" },
|
|
2636
|
+
{ kind: "text", text: `on ${client(this.occupant(asker)) ?? "a device"}`, role: "quiet" },
|
|
2637
|
+
{ kind: "text", text: "your devices", role: "label" },
|
|
2638
|
+
{ kind: "answer", ask: "devices", as: "people" },
|
|
2639
|
+
// every standing she carries as its section, and the door of her
|
|
2640
|
+
// own world as a door: seven owner asks are a page of their own
|
|
2641
|
+
...Object.keys(this.cells.standings).sort((a, b) => a === HOME ? 1 : b === HOME ? -1 : a.localeCompare(b)).map((id) => id === HOME ? { kind: "standing", id, as: "door" } : { kind: "standing", id }),
|
|
2642
|
+
{ kind: "form", ask: "device" },
|
|
2643
|
+
{ kind: "form", ask: "join" },
|
|
2644
|
+
{ kind: "form", ask: "push" },
|
|
2645
|
+
{ kind: "form", ask: "forget" },
|
|
2646
|
+
{ kind: "form", ask: "name" }
|
|
2647
|
+
]
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
// A device the root minted may say what she is called: the root trusts
|
|
2651
|
+
// it with its id and its reach already, and a being does not know her own
|
|
2652
|
+
// key. A name is a word; she keeps the last one given.
|
|
2653
|
+
name(args) {
|
|
2654
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
2655
|
+
if (!/^[\w.-]{1,80}$/.test(name)) return { error: "a name is a word" };
|
|
2656
|
+
this.cells.name = name;
|
|
2657
|
+
return { named: name };
|
|
2658
|
+
}
|
|
2659
|
+
// A device that ran something for her says what it found. Kept, so that
|
|
2660
|
+
// whoever renders her can show it; the ask itself is the callback.
|
|
2661
|
+
report(args, asker) {
|
|
2662
|
+
this.cells.reports.push({ from: asker.id ?? null, event: args.event ?? null, result: args.result ?? null });
|
|
2663
|
+
return { reported: true };
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
|
|
2667
|
+
// beings/desk.ts
|
|
2668
|
+
var Desk = class extends Being {
|
|
2669
|
+
static asks = {
|
|
2670
|
+
hello: { description: "how to get in", input: { type: "object" } },
|
|
2671
|
+
send: { description: "send a way in to an address of yours", input: { type: "object", properties: { kind: { type: "string" }, to: { type: "string" } }, required: ["kind", "to"] } },
|
|
2672
|
+
device: { description: "trade a proof for a device invitation", input: { type: "object", properties: { proof: { type: "object" } }, required: ["proof"] } }
|
|
2673
|
+
};
|
|
2674
|
+
// The box's way in, taken at every birth as any being takes what her box
|
|
2675
|
+
// lends. A box that lends none, a tab or the edge, leaves her holding
|
|
2676
|
+
// nothing under that id and the kind is not offered.
|
|
2677
|
+
constructor(stance) {
|
|
2678
|
+
super(stance);
|
|
2679
|
+
void join(this, LOCAL, void 0, WAY(LOCAL));
|
|
2680
|
+
}
|
|
2681
|
+
// The ids of the ways in she holds, in the order they were placed.
|
|
2682
|
+
get ways() {
|
|
2683
|
+
const out = [];
|
|
2684
|
+
for (const id of Object.keys(this.cells.standings)) {
|
|
2685
|
+
const kind = kindOf(id);
|
|
2686
|
+
if (kind !== null && this.standings[id]) out.push({ id, kind });
|
|
2687
|
+
}
|
|
2688
|
+
return out;
|
|
2689
|
+
}
|
|
2690
|
+
// Before she is described, she learns what her ways in can be asked. A
|
|
2691
|
+
// standing's blueprint is filled by an empty ask and by nothing else, and
|
|
2692
|
+
// a way in placed on her a moment ago has none yet; each is asked once,
|
|
2693
|
+
// and a way in that will not describe stays a kind she can only verify.
|
|
2694
|
+
async answer(asker, method, args = {}) {
|
|
2695
|
+
if (method === void 0) {
|
|
2696
|
+
for (const w of this.ways) if (this.cells.standings[w.id]?.blueprint === null) await this.standings[w.id]?.ask();
|
|
2697
|
+
}
|
|
2698
|
+
return await super.answer(asker, method, args);
|
|
2699
|
+
}
|
|
2700
|
+
// The kinds are the ways in she holds and change with them, so the form a
|
|
2701
|
+
// stranger sees is shaped now rather than declared once: `send` is there
|
|
2702
|
+
// only where a way in says it can be asked to send, and it names the kinds
|
|
2703
|
+
// that can. A world with none shows no such form, and a link is the only
|
|
2704
|
+
// way in, as it always was.
|
|
2705
|
+
describe(asker) {
|
|
2706
|
+
const bp = super.describe(asker);
|
|
2707
|
+
const kinds = this.ways.filter((w) => this.cells.standings[w.id]?.blueprint?.asks.some((a) => a.name === "send")).map((w) => w.kind);
|
|
2708
|
+
const asks2 = bp.asks.flatMap((a) => {
|
|
2709
|
+
if (a.name !== "send") return [a];
|
|
2710
|
+
if (kinds.length === 0) return [];
|
|
2711
|
+
return [{ ...a, input: { type: "object", properties: { kind: { type: "string", enum: kinds, default: kinds[0] }, to: { type: "string" } }, required: ["kind", "to"] } }];
|
|
2712
|
+
});
|
|
2713
|
+
return { ...bp, asks: asks2 };
|
|
2714
|
+
}
|
|
2715
|
+
// A stranger learns the proof kinds and nothing else. Who lives on this
|
|
2716
|
+
// world is not the caller's to know: the way in names the user, so
|
|
2717
|
+
// nobody at the door ever has to, and a world with a thousand of them
|
|
2718
|
+
// would otherwise hand the whole list to anyone who knocked.
|
|
2719
|
+
hello(_args, asker) {
|
|
2720
|
+
return { desk: true, asker: asker.id ?? null, kinds: this.ways.map((w) => w.kind) };
|
|
2721
|
+
}
|
|
2722
|
+
// A stranger asks for a way in to an address that is hers. Nothing of this
|
|
2723
|
+
// world changes here and nothing is minted: the way in sends, and the
|
|
2724
|
+
// proof that comes back is what admits her.
|
|
2725
|
+
async send(args) {
|
|
2726
|
+
const { kind, to } = args;
|
|
2727
|
+
if (typeof kind !== "string" || typeof to !== "string") return { error: "kind and to are words" };
|
|
2728
|
+
const way = this.standings[WAY(kind)];
|
|
2729
|
+
if (!way) return { error: "no such way in" };
|
|
2730
|
+
const out = await way.ask("send", { to });
|
|
2731
|
+
if (isSilence(out) || isWord(out)) return { error: "no such way in" };
|
|
2732
|
+
return out;
|
|
2733
|
+
}
|
|
2734
|
+
async device(args) {
|
|
2735
|
+
const proof = args.proof;
|
|
2736
|
+
if (!proof || typeof proof !== "object" || Array.isArray(proof) || typeof proof.kind !== "string") return { error: "proof has a kind" };
|
|
2737
|
+
const way = this.standings[WAY(proof.kind)];
|
|
2738
|
+
const said2 = way ? await way.ask("verify", { proof }) : null;
|
|
2739
|
+
const who = said2 === null || isSilence(said2) || isWord(said2) ? null : verified(said2);
|
|
2740
|
+
if (!who) return { error: "refused" };
|
|
2741
|
+
const id = `user:${who.user}`;
|
|
2742
|
+
if (!this.standings[id] && (who.make === true || typeof who.make === "string")) {
|
|
2743
|
+
if (!/^[\w.-]{1,80}$/.test(who.user) || who.user === "desk") return { error: "a user is a word, and not desk" };
|
|
2744
|
+
if (await this.boot(typeof who.make === "string" ? who.make : "User", who.user, id) === null) return { error: "no such user" };
|
|
2745
|
+
await this.standings[id].ask("hello", { name: who.user });
|
|
2746
|
+
}
|
|
2747
|
+
const user = this.standings[id];
|
|
2748
|
+
if (!user) return { error: "no such user" };
|
|
2749
|
+
const inv = await user.ask("device", { client: who.client, ...who.wake === true ? { wake: true } : {}, ...who.reach === true ? { reach: true } : {}, ...who.mint === true ? { mint: true } : {}, ...who.revoke === true ? { revoke: true } : {} });
|
|
2750
|
+
if (isSilence(inv)) return { error: "silence" };
|
|
2751
|
+
if (isWord(inv)) return { error: wordOf(inv) };
|
|
2752
|
+
return inv;
|
|
2753
|
+
}
|
|
2754
|
+
};
|
|
2755
|
+
|
|
2756
|
+
// beings/avatar.ts
|
|
2757
|
+
var USER = "user";
|
|
2758
|
+
var PUSHER = "user:push";
|
|
2759
|
+
var Avatar = class extends Carrier {
|
|
2760
|
+
static cells = { inbox: [] };
|
|
2761
|
+
static asks = {
|
|
2762
|
+
notify: { description: "a push from the user being", input: { type: "object" }, for: (occ) => occ?.id === PUSHER }
|
|
2763
|
+
};
|
|
2764
|
+
// Her sides' ears. One identity is one avatar for life, and two sessions
|
|
2765
|
+
// of that identity are two sides on her at once, so a push goes to every
|
|
2766
|
+
// ear present; each side adds its own on open and removes it on close.
|
|
2767
|
+
// Dropped if there is none.
|
|
2768
|
+
ears = /* @__PURE__ */ new Set();
|
|
2769
|
+
// ---- what the user being may ask her
|
|
2770
|
+
notify(args) {
|
|
2771
|
+
this.cells.inbox.push(args);
|
|
2772
|
+
for (const ear of this.ears) ear(args);
|
|
2773
|
+
return { notified: true };
|
|
2774
|
+
}
|
|
2775
|
+
// ---- what her side calls. Not asks: nobody reaches these through a door.
|
|
2776
|
+
// The credential exchange's last step. `desk` is the desk's public
|
|
2777
|
+
// invitation, `proof` is what the route produced. She knocks the desk as a
|
|
2778
|
+
// stranger, gets an invitation the user being minted, and joins with it. A
|
|
2779
|
+
// knock on a public invitation births nothing, so she holds no standing at
|
|
2780
|
+
// the desk afterwards. If she already holds `user`, this is a reconnect and
|
|
2781
|
+
// nothing is minted.
|
|
2782
|
+
async enter(desk, proof) {
|
|
2783
|
+
if (this.standings[USER]) return this.tools();
|
|
2784
|
+
const inv = await this.knock(desk, "device", { proof });
|
|
2785
|
+
if (isSilence(inv)) return { error: "silence" };
|
|
2786
|
+
if (isWord(inv)) return { error: wordOf(inv) };
|
|
2787
|
+
if (!inv || typeof inv !== "object" || Array.isArray(inv) || "error" in inv) return inv ?? { error: "no invitation" };
|
|
2788
|
+
return this.join(inv);
|
|
2789
|
+
}
|
|
2790
|
+
// Knock with an invitation, hand the far being one back so she can push,
|
|
2791
|
+
// and take her as `user`. The only moment a standing is born here. A being
|
|
2792
|
+
// who answers the hello with an error object is a being who answered: she
|
|
2793
|
+
// is taken all the same, and the way back she did not take is dropped, so
|
|
2794
|
+
// any world's public being may admit a guest, hello or not.
|
|
2795
|
+
async join(invitation) {
|
|
2796
|
+
if (this.standings[USER]) {
|
|
2797
|
+
const had = await this.tools();
|
|
2798
|
+
if (!("error" in had) || had.error !== "removed") return had;
|
|
2799
|
+
this.standings.remove(USER);
|
|
2800
|
+
this.occupants.remove(PUSHER);
|
|
2801
|
+
}
|
|
2802
|
+
const mine2 = await this.invite(PUSHER);
|
|
2803
|
+
if (mine2 === null) return { error: "user is taken" };
|
|
2804
|
+
const out = await this.knock(invitation, "hello", { invitation: mine2 });
|
|
2805
|
+
if (isSilence(out) || isWord(out)) {
|
|
2806
|
+
this.occupants.remove(PUSHER);
|
|
2807
|
+
return { error: isSilence(out) ? "silence" : wordOf(out) };
|
|
2808
|
+
}
|
|
2809
|
+
if (out !== null && typeof out === "object" && !Array.isArray(out) && "error" in out) this.occupants.remove(PUSHER);
|
|
2810
|
+
await this.take(USER, invitation);
|
|
2811
|
+
return this.tools();
|
|
2812
|
+
}
|
|
2813
|
+
// Her describe, as the user being shows it to her. This is the tool list.
|
|
2814
|
+
async tools() {
|
|
2815
|
+
const st = this.standings[USER];
|
|
2816
|
+
if (!st) return { error: "not joined" };
|
|
2817
|
+
const bp = await st.ask();
|
|
2818
|
+
if (isSilence(bp) || isWord(bp)) return { error: isSilence(bp) ? "silence" : wordOf(bp) };
|
|
2819
|
+
return bp;
|
|
2820
|
+
}
|
|
2821
|
+
// One call from the side, one ask on the standing. The three words pass
|
|
2822
|
+
// through untouched; the side renders them.
|
|
2823
|
+
call(name, args = {}, wanted) {
|
|
2824
|
+
const st = this.standings[USER];
|
|
2825
|
+
return st ? st.ask(name, args, wanted) : Promise.resolve({ error: "not joined" });
|
|
2826
|
+
}
|
|
2827
|
+
};
|
|
2828
|
+
|
|
2829
|
+
// beings/presence.ts
|
|
2830
|
+
var Presence = class extends Avatar {
|
|
2831
|
+
// Her world's user being sees the box, and nobody else: the way back she
|
|
2832
|
+
// handed over at the join is the one occupant who may ask through her.
|
|
2833
|
+
static carries(occupant2, _asker) {
|
|
2834
|
+
return occupant2?.id === PUSHER;
|
|
2835
|
+
}
|
|
2836
|
+
// Her standing at the user being is hers to speak through and never to
|
|
2837
|
+
// carry: the user being carries her, so carrying it back would be a
|
|
2838
|
+
// loop, and it is the one standing that is not a faculty.
|
|
2839
|
+
static skips(id) {
|
|
2840
|
+
return id === USER;
|
|
2841
|
+
}
|
|
2842
|
+
// Which names the box lent her, by name, settled after birth.
|
|
2843
|
+
lent;
|
|
2844
|
+
constructor(stance) {
|
|
2845
|
+
super(stance);
|
|
2846
|
+
this.lent = Promise.all(Object.keys(FACULTIES).map(async (name) => [name, await join(this, name)])).then((all) => Object.fromEntries(all));
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
|
|
2850
|
+
// beings/courier.ts
|
|
2851
|
+
var MOVER = "mover";
|
|
2852
|
+
var HEX2 = /^(?:[0-9a-f]{2})+$/;
|
|
2853
|
+
var PK = /^[0-9a-f]{128}$/;
|
|
2854
|
+
var invited = (occ) => occ !== void 0;
|
|
2855
|
+
function isMoving(v) {
|
|
2856
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
|
|
2857
|
+
const m = v;
|
|
2858
|
+
const object = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
2859
|
+
return typeof m.world === "string" && PK.test(m.world) && typeof m.at === "string" && typeof m.name === "string" && typeof m.seed === "string" && HEX2.test(m.seed) && object(m.partition) && object(m.record);
|
|
2860
|
+
}
|
|
2861
|
+
var answered3 = (out) => {
|
|
2862
|
+
if (isSilence(out)) return { error: "silence" };
|
|
2863
|
+
if (isWord(out)) return { error: wordOf(out) };
|
|
2864
|
+
if (out === null || typeof out !== "object" || Array.isArray(out)) return { error: "the other device answered nothing" };
|
|
2865
|
+
return out;
|
|
2866
|
+
};
|
|
2867
|
+
var Courier = class extends Being {
|
|
2868
|
+
static asks = {
|
|
2869
|
+
hand: { description: "the ward this device is giving away: its world, where that world lives, and the ward itself", input: { type: "object" }, for: invited },
|
|
2870
|
+
done: { description: "the ward arrived on the other device: delete the copy here", input: { type: "object" }, for: invited }
|
|
2871
|
+
};
|
|
2872
|
+
// The shell holding the device that gives: what to hand over, and what to
|
|
2873
|
+
// do when the other end says it landed. Set while a move is offered and
|
|
2874
|
+
// gone after, so a courier nobody is holding gives nothing.
|
|
2875
|
+
giving;
|
|
2876
|
+
gone;
|
|
2877
|
+
// ---- what the device taking asks
|
|
2878
|
+
async hand() {
|
|
2879
|
+
if (!this.giving) return { error: "this device is giving nothing away" };
|
|
2880
|
+
return await this.giving();
|
|
2881
|
+
}
|
|
2882
|
+
async done() {
|
|
2883
|
+
if (!this.gone) return { error: "this device is giving nothing away" };
|
|
2884
|
+
return await this.gone();
|
|
2885
|
+
}
|
|
2886
|
+
// ---- what the shell taking calls. Not asks: nobody reaches these through a door.
|
|
2887
|
+
// Knock the far courier, take the ward she answers with, and hold her:
|
|
2888
|
+
// the standing is what the second ask goes through, and a knock is spent
|
|
2889
|
+
// the moment it is answered.
|
|
2890
|
+
async fetch(invitation) {
|
|
2891
|
+
const out = answered3(await this.knock(invitation, "hand", {}));
|
|
2892
|
+
if ("error" in out && typeof out.error === "string") return { error: out.error };
|
|
2893
|
+
if (!isMoving(out)) return { error: "the other device answered with something that is not a ward" };
|
|
2894
|
+
if (await this.take(MOVER, invitation) === null) return { error: "this device could not hold the other one" };
|
|
2895
|
+
return out;
|
|
2896
|
+
}
|
|
2897
|
+
// It is here: the other device may delete its copy.
|
|
2898
|
+
async landed() {
|
|
2899
|
+
const standing2 = this.standings[MOVER];
|
|
2900
|
+
if (!standing2) return { error: "this device is not holding the other one" };
|
|
2901
|
+
const out = answered3(await standing2.ask("done", {}));
|
|
2902
|
+
if (typeof out.deleted === "string") return { deleted: out.deleted };
|
|
2903
|
+
return { error: typeof out.error === "string" ? out.error : "the other device would not let go" };
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
|
|
2907
|
+
// beings/envoy.ts
|
|
2908
|
+
var FRESH = 5 * 6e4;
|
|
2909
|
+
|
|
2910
|
+
// beings/clock.ts
|
|
2911
|
+
var WOKE = `${TIMER}:wake`;
|
|
2912
|
+
var MINUTE2 = 6e4;
|
|
2913
|
+
var HOUR = 36e5;
|
|
2914
|
+
var SPAN2 = 23 * HOUR + 59 * MINUTE2;
|
|
2915
|
+
|
|
2916
|
+
// ../quo/dist/ward/ground.js
|
|
2917
|
+
var maker = (objects, ...registries) => (className, stance) => {
|
|
2918
|
+
const r = registries.find((reg) => reg !== void 0 && Object.hasOwn(reg, className));
|
|
2919
|
+
const C = r?.[className];
|
|
2920
|
+
if (!C)
|
|
2921
|
+
return null;
|
|
2922
|
+
const obj = new C(stance);
|
|
2923
|
+
objects.set(stance.cells, obj);
|
|
2924
|
+
return obj;
|
|
2925
|
+
};
|
|
2926
|
+
var entropy = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
2927
|
+
async function learnPk(w) {
|
|
2928
|
+
const notes = await w.ask();
|
|
2929
|
+
const pk = notes?.notes?.pk;
|
|
2930
|
+
if (typeof pk !== "string")
|
|
2931
|
+
throw new Error("the ward did not say its pk");
|
|
2932
|
+
return pk;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// ../quo/dist/harbor/reach.js
|
|
2936
|
+
var never = () => new Promise(() => {
|
|
2937
|
+
});
|
|
2938
|
+
function request(url) {
|
|
2939
|
+
const base = url.replace(/\/$/, "");
|
|
2940
|
+
return {
|
|
2941
|
+
close: () => {
|
|
2942
|
+
},
|
|
2943
|
+
carry: async (pk, bytes) => {
|
|
2944
|
+
let res;
|
|
2945
|
+
const body = new Uint8Array(bytes.length);
|
|
2946
|
+
body.set(bytes);
|
|
2947
|
+
try {
|
|
2948
|
+
res = await fetch(`${base}/${pk}`, { method: "POST", headers: { "content-type": "application/octet-stream", "quo-suite": String(SUITE) }, body });
|
|
2949
|
+
} catch {
|
|
2950
|
+
return void 0;
|
|
2951
|
+
}
|
|
2952
|
+
if (res.status === 404 || res.status >= 400 && res.status < 500 || res.status === 502 || res.status === 503 || res.status === 504)
|
|
2953
|
+
return void 0;
|
|
2954
|
+
if (!res.ok)
|
|
2955
|
+
return never();
|
|
2956
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
var ASK = 0;
|
|
2961
|
+
var REPLY = 1;
|
|
2962
|
+
var NONE = 2;
|
|
2963
|
+
var SUITE = 1;
|
|
2964
|
+
var REFUSED = 4001;
|
|
2965
|
+
var OPEN = 1;
|
|
2966
|
+
var PENDING = 4096;
|
|
2967
|
+
var Socket = class {
|
|
2968
|
+
line;
|
|
2969
|
+
pending = /* @__PURE__ */ new Map();
|
|
2970
|
+
far = [];
|
|
2971
|
+
// the pks the far side announced
|
|
2972
|
+
#id = 1;
|
|
2973
|
+
#open;
|
|
2974
|
+
constructor(line, deliver, onAnnounce, onClose) {
|
|
2975
|
+
this.line = line;
|
|
2976
|
+
line.binaryType = "arraybuffer";
|
|
2977
|
+
this.#open = line.readyState === OPEN ? Promise.resolve() : new Promise((ok, no) => {
|
|
2978
|
+
line.addEventListener("open", ok);
|
|
2979
|
+
line.addEventListener("error", no);
|
|
2980
|
+
line.addEventListener("close", () => no(new Error("closed before it opened")));
|
|
2981
|
+
});
|
|
2982
|
+
this.#open.catch(() => {
|
|
2983
|
+
});
|
|
2984
|
+
line.addEventListener("message", (e) => {
|
|
2985
|
+
if (typeof e.data === "string") {
|
|
2986
|
+
try {
|
|
2987
|
+
const m = JSON.parse(e.data);
|
|
2988
|
+
if (m.suite !== void 0 && m.suite !== SUITE) {
|
|
2989
|
+
this.far = [];
|
|
2990
|
+
line.close(REFUSED, "suite");
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
if (Array.isArray(m.announce)) {
|
|
2994
|
+
this.far = m.announce.filter((x) => typeof x === "string");
|
|
2995
|
+
onAnnounce(this.far);
|
|
2996
|
+
}
|
|
2997
|
+
} catch {
|
|
2998
|
+
}
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
const buf = e.data instanceof ArrayBuffer ? new Uint8Array(e.data) : ArrayBuffer.isView(e.data) ? new Uint8Array(e.data.buffer, e.data.byteOffset, e.data.byteLength) : void 0;
|
|
3002
|
+
if (!buf || buf.length < 5)
|
|
3003
|
+
return;
|
|
3004
|
+
const kind = buf[0], id = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(1);
|
|
3005
|
+
if (kind === ASK) {
|
|
3006
|
+
const pk = hex2(buf.subarray(5, 69));
|
|
3007
|
+
void deliver(pk, new Uint8Array(buf.subarray(69))).then((back) => back, () => void 0).then((back) => {
|
|
3008
|
+
const head = new Uint8Array(5);
|
|
3009
|
+
head[0] = back === void 0 ? NONE : REPLY;
|
|
3010
|
+
new DataView(head.buffer).setUint32(1, id);
|
|
3011
|
+
if (line.readyState === OPEN)
|
|
3012
|
+
line.send(back === void 0 ? head : concat([head, back]));
|
|
3013
|
+
});
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
const waiting = this.pending.get(id);
|
|
3017
|
+
if (!waiting)
|
|
3018
|
+
return;
|
|
3019
|
+
this.pending.delete(id);
|
|
3020
|
+
waiting(kind === REPLY ? new Uint8Array(buf.subarray(5)) : void 0);
|
|
3021
|
+
});
|
|
3022
|
+
line.addEventListener("error", () => {
|
|
3023
|
+
});
|
|
3024
|
+
line.addEventListener("close", (e) => {
|
|
3025
|
+
this.pending.clear();
|
|
3026
|
+
onClose({ code: e?.code, reason: e?.reason });
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
// Say which ward pks this side holds.
|
|
3030
|
+
announce(pks) {
|
|
3031
|
+
if (this.line.readyState === OPEN)
|
|
3032
|
+
this.line.send(JSON.stringify({ announce: pks, suite: SUITE }));
|
|
3033
|
+
}
|
|
3034
|
+
async carry(pk, bytes) {
|
|
3035
|
+
try {
|
|
3036
|
+
await this.#open;
|
|
3037
|
+
} catch {
|
|
3038
|
+
return void 0;
|
|
3039
|
+
}
|
|
3040
|
+
if (this.line.readyState !== OPEN)
|
|
3041
|
+
return void 0;
|
|
3042
|
+
const id = this.#id++;
|
|
3043
|
+
const head = new Uint8Array(69);
|
|
3044
|
+
head[0] = ASK;
|
|
3045
|
+
new DataView(head.buffer).setUint32(1, id);
|
|
3046
|
+
head.set(unhex(pk), 5);
|
|
3047
|
+
return new Promise((ok) => {
|
|
3048
|
+
if (this.pending.size >= PENDING)
|
|
3049
|
+
this.pending.delete(this.pending.keys().next().value);
|
|
3050
|
+
this.pending.set(id, ok);
|
|
3051
|
+
this.line.send(concat([head, bytes]));
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
close() {
|
|
3055
|
+
this.line.close();
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
|
|
3059
|
+
// ../quo/dist/harbor/store.js
|
|
3060
|
+
var values = (p) => JSON.parse(JSON.stringify(p));
|
|
3061
|
+
var rowsOf = (partition, rows) => rows.map((row) => {
|
|
3062
|
+
const value = rowOf(partition, row);
|
|
3063
|
+
return [row, value === void 0 ? void 0 : values(value)];
|
|
3064
|
+
});
|
|
3065
|
+
|
|
3066
|
+
// ../quo/dist/harbor/core.js
|
|
3067
|
+
var DEFAULT_CODE = "classes/index.ts";
|
|
3068
|
+
var PROBE = 1e4;
|
|
3069
|
+
var ANNOUNCE = 64;
|
|
3070
|
+
var PROVING = 8;
|
|
3071
|
+
var announces = /* @__PURE__ */ new WeakMap();
|
|
3072
|
+
var Harbor = class {
|
|
3073
|
+
store;
|
|
3074
|
+
loader;
|
|
3075
|
+
wards = /* @__PURE__ */ new Map();
|
|
3076
|
+
// name -> pointers
|
|
3077
|
+
doors = /* @__PURE__ */ new Map();
|
|
3078
|
+
// ward pk -> door
|
|
3079
|
+
reaches = /* @__PURE__ */ new Map();
|
|
3080
|
+
// the directory: foreign ward pk -> reach
|
|
3081
|
+
down = /* @__PURE__ */ new Set();
|
|
3082
|
+
// pks this harbor will not reach right now: weather, for a test
|
|
3083
|
+
refused = /* @__PURE__ */ new Map();
|
|
3084
|
+
// own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
|
|
3085
|
+
faults = /* @__PURE__ */ new Map();
|
|
3086
|
+
// ward name -> saves the store refused. what a terrain does with it is its own
|
|
3087
|
+
// Told every time a save is refused, with the ward's name and how many of
|
|
3088
|
+
// its saves the store has now refused. The refusal is already the answer of
|
|
3089
|
+
// the ask that caused it, so nobody is waiting on this; it is for the box's
|
|
3090
|
+
// own record, a journal line or a counter, and a terrain that keeps none
|
|
3091
|
+
// leaves it alone.
|
|
3092
|
+
onFault = () => {
|
|
3093
|
+
};
|
|
3094
|
+
unbooted = [];
|
|
3095
|
+
// the wards the store keeps that would not host this run, by name
|
|
3096
|
+
classes = {};
|
|
3097
|
+
// classes handed in-process, beside the loader's
|
|
3098
|
+
#saving = /* @__PURE__ */ new Map();
|
|
3099
|
+
// The dialers' sockets, in the order they were dialed: where a pk nobody
|
|
3100
|
+
// here knows is sent, because the listener a harbor dialed is a rendezvous
|
|
3101
|
+
// and may hold that pk for someone else. It is a list because a harbor may
|
|
3102
|
+
// dial more than one, and a rendezvous is a listener and nothing more, so
|
|
3103
|
+
// there is never only one of them. A pk is tried down the list until one
|
|
3104
|
+
// carries it: with a single slot, a harbor holding two lines answered
|
|
3105
|
+
// unreached for a pk the other line could have reached.
|
|
3106
|
+
fallbacks = [];
|
|
3107
|
+
// How a dialer says what this harbor holds now. An announce is a claim and
|
|
3108
|
+
// a claim is worth nothing until the door behind it answers a probe, so
|
|
3109
|
+
// saying it again costs a proof and buys nothing to an impostor. What is
|
|
3110
|
+
// not free is never saying it again: a ward booted, adopted or dropped
|
|
3111
|
+
// after a line opened would be announced only by the next line, and a line
|
|
3112
|
+
// that does not drop never opens again, so it would be unreachable through
|
|
3113
|
+
// that listener for as long as the socket stayed healthy.
|
|
3114
|
+
announcers = /* @__PURE__ */ new Set();
|
|
3115
|
+
#announce() {
|
|
3116
|
+
for (const say of this.announcers)
|
|
3117
|
+
say();
|
|
3118
|
+
}
|
|
3119
|
+
// What this device lends the beings of the wards it hosts, by name. One
|
|
3120
|
+
// for the harbor, and `lendFor` says which ward may ask it: every ward,
|
|
3121
|
+
// unless a terrain says a stranger's ward is lent nothing.
|
|
3122
|
+
// Set at birth or once the harbor's own ward is standing, which is the
|
|
3123
|
+
// usual order: a terrain puts that ward up first, then boots the rest on a
|
|
3124
|
+
// ground that can reach it.
|
|
3125
|
+
lend;
|
|
3126
|
+
constructor(store, loader = async () => ({}), lend) {
|
|
3127
|
+
this.store = store;
|
|
3128
|
+
this.loader = loader;
|
|
3129
|
+
this.lend = lend;
|
|
3130
|
+
}
|
|
3131
|
+
lendFor(_name, _record) {
|
|
3132
|
+
return this.lend;
|
|
3133
|
+
}
|
|
3134
|
+
// Boot every ward the store keeps, and learn the hints. One ward that will
|
|
3135
|
+
// not host, a partition this kit cannot read or a loader that throws, is
|
|
3136
|
+
// named in `unbooted` and takes nobody else down with it.
|
|
3137
|
+
async boot() {
|
|
3138
|
+
for (const name of await this.store.list()) {
|
|
3139
|
+
if (this.wards.has(name))
|
|
3140
|
+
continue;
|
|
3141
|
+
try {
|
|
3142
|
+
const kept2 = await this.store.load(name);
|
|
3143
|
+
if (kept2)
|
|
3144
|
+
await this.host(name, kept2);
|
|
3145
|
+
} catch {
|
|
3146
|
+
this.unbooted.push(name);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
for (const [pk, url] of Object.entries(await this.store.hints()))
|
|
3150
|
+
this.hint(pk, url);
|
|
3151
|
+
}
|
|
3152
|
+
// The object a ward writes into, by the name it is kept under: the
|
|
3153
|
+
// partition the harbor was handed at boot and keeps so it can save it.
|
|
3154
|
+
// Nothing in the harbor reads it, and it is not on `Hosted`, because a
|
|
3155
|
+
// side that holds a ward would then hold every seed in it and reach any
|
|
3156
|
+
// being past every door. It is here for one reader, the conformance
|
|
3157
|
+
// suite, whose census is ward state read straight and whose forgeries are
|
|
3158
|
+
// ward state written straight, the way `MemoryHarbor.partitions` is. A
|
|
3159
|
+
// side asks the ward.
|
|
3160
|
+
partitionOf(name) {
|
|
3161
|
+
return this.#saving.get(name)?.memory;
|
|
3162
|
+
}
|
|
3163
|
+
// ---- saving
|
|
3164
|
+
// The ward wrote one row. Its partition is saved once the line is free, and
|
|
3165
|
+
// once for every burst of writes, with every row named in that burst.
|
|
3166
|
+
#wrote(name, row) {
|
|
3167
|
+
const s = this.#saving.get(name);
|
|
3168
|
+
if (!s || s.gone)
|
|
3169
|
+
return;
|
|
3170
|
+
s.dirty.add(row);
|
|
3171
|
+
if (s.timer === void 0)
|
|
3172
|
+
s.timer = setTimeout(() => void this.#flush(name), 0);
|
|
3173
|
+
}
|
|
3174
|
+
// The rows a store refused, put back in the ward's memory to what the
|
|
3175
|
+
// store holds for them. A being whose row was refused would otherwise go
|
|
3176
|
+
// on answering out of a memory that runs ahead of the device: her next
|
|
3177
|
+
// write is a row past the same ceiling, refused again, and everything she
|
|
3178
|
+
// was told between now and the reload is gone with it. Standing her at
|
|
3179
|
+
// what is kept costs her the writes that were never kept, which is what
|
|
3180
|
+
// the asker was told, and leaves a small write after it one the store
|
|
3181
|
+
// takes.
|
|
3182
|
+
//
|
|
3183
|
+
// The shape is read where the shape lives. The harbor hands each row and
|
|
3184
|
+
// the copy it kept to the partition, which knows which part of itself a row
|
|
3185
|
+
// is; nothing here reads a being, a bind table or a head. And nothing here
|
|
3186
|
+
// asks the store: what it took is what this harbor handed it, which is
|
|
3187
|
+
// `kept`, and a row nobody has handed it since this ward was hosted is a
|
|
3188
|
+
// row the ward has not moved, so there is nothing to put back.
|
|
3189
|
+
// Every row together, and never the refused ones alone. A relation's state
|
|
3190
|
+
// is spread over rows, the head for the door's heirs and a being's own row
|
|
3191
|
+
// for the keys she speaks under: put one back and the door stands ahead of
|
|
3192
|
+
// the knocker and the relation never speaks again. So the ward is put back
|
|
3193
|
+
// to the whole point, which is what the store held at the last save taken
|
|
3194
|
+
// while no call was in flight. A row that point never knew is left where it
|
|
3195
|
+
// is and stays named: no save has ever taken it, so there is nothing to put
|
|
3196
|
+
// it back to. The rows that moved come back, so the store is put back with
|
|
3197
|
+
// them and a reload finds no half-done row.
|
|
3198
|
+
#standAtKept(s) {
|
|
3199
|
+
if (s.gone)
|
|
3200
|
+
return [];
|
|
3201
|
+
const moved = [];
|
|
3202
|
+
for (const row of s.kept.keys()) {
|
|
3203
|
+
const was = s.kept.get(row);
|
|
3204
|
+
const now = rowsOf(s.memory, [row])[0][1];
|
|
3205
|
+
if (JSON.stringify(was) === JSON.stringify(now))
|
|
3206
|
+
continue;
|
|
3207
|
+
restoreRow(s.memory, row, was);
|
|
3208
|
+
moved.push(row);
|
|
3209
|
+
}
|
|
3210
|
+
for (const row of moved)
|
|
3211
|
+
s.dirty.delete(row);
|
|
3212
|
+
return moved;
|
|
3213
|
+
}
|
|
3214
|
+
// The ward back to the point, and the store with it. The store is written
|
|
3215
|
+
// because a reload reads it and not the memory: a refusal that put only the
|
|
3216
|
+
// memory back would leave the rows the store did take standing ahead of the
|
|
3217
|
+
// ward until the next restart, and one half of a relation ahead of the
|
|
3218
|
+
// other. What goes out is what the store already took once, so it is a
|
|
3219
|
+
// write the store has no reason to refuse; one that refuses it anyway is
|
|
3220
|
+
// counted like any other, and there is nothing further to put back to.
|
|
3221
|
+
async #putBack(name, s) {
|
|
3222
|
+
const under2 = [...s.under.keys()];
|
|
3223
|
+
s.under.clear();
|
|
3224
|
+
const moved = this.#standAtKept(s);
|
|
3225
|
+
const rows = [.../* @__PURE__ */ new Set([...moved, ...under2])];
|
|
3226
|
+
if (rows.length === 0 || s.gone)
|
|
3227
|
+
return;
|
|
3228
|
+
try {
|
|
3229
|
+
await this.store.save(name, s.memory, rows);
|
|
3230
|
+
for (const row of rows)
|
|
3231
|
+
s.dirty.delete(row);
|
|
3232
|
+
} catch {
|
|
3233
|
+
this.#fault(name);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
// The call ended and the ward is whole, so the point catches up with it. A
|
|
3237
|
+
// row the store took under the call and nobody has touched since is where
|
|
3238
|
+
// the ward stands, so it joins the point as it is; a row that moved after
|
|
3239
|
+
// that save is named for one more, and that save is quiet and moves the
|
|
3240
|
+
// point itself. Nothing is written twice for a ward whose call left its
|
|
3241
|
+
// rows where the store already has them.
|
|
3242
|
+
#settle(name, s) {
|
|
3243
|
+
for (const [row, value] of s.under) {
|
|
3244
|
+
const now = rowsOf(s.memory, [row])[0][1];
|
|
3245
|
+
if (JSON.stringify(value) === JSON.stringify(now))
|
|
3246
|
+
s.kept.set(row, value);
|
|
3247
|
+
else
|
|
3248
|
+
s.dirty.add(row);
|
|
3249
|
+
}
|
|
3250
|
+
s.under.clear();
|
|
3251
|
+
if (s.dirty.size > 0)
|
|
3252
|
+
void this.#flush(name);
|
|
3253
|
+
}
|
|
3254
|
+
// One refusal, counted and told. What a terrain does with the count is its
|
|
3255
|
+
// own; the arrival that caused it is told by the answer, not by this.
|
|
3256
|
+
#fault(name) {
|
|
3257
|
+
const faults = (this.faults.get(name) ?? 0) + 1;
|
|
3258
|
+
this.faults.set(name, faults);
|
|
3259
|
+
try {
|
|
3260
|
+
this.onFault(name, faults);
|
|
3261
|
+
} catch {
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
// Answers whether everything it wrote was kept. False is a store that
|
|
3265
|
+
// refused, and it is the ward's answer to the arrival that caused it, not
|
|
3266
|
+
// a number in a map nobody reads. A flush that had nothing to write kept
|
|
3267
|
+
// everything it was given, which is true and is why a read is never
|
|
3268
|
+
// refused.
|
|
3269
|
+
async #flush(name) {
|
|
3270
|
+
const s = this.#saving.get(name);
|
|
3271
|
+
if (!s)
|
|
3272
|
+
return true;
|
|
3273
|
+
if (s.timer !== void 0) {
|
|
3274
|
+
clearTimeout(s.timer);
|
|
3275
|
+
s.timer = void 0;
|
|
3276
|
+
}
|
|
3277
|
+
if (s.running)
|
|
3278
|
+
return s.running;
|
|
3279
|
+
s.running = (async () => {
|
|
3280
|
+
if (s.back) {
|
|
3281
|
+
if (s.calls > 0)
|
|
3282
|
+
return false;
|
|
3283
|
+
s.back = false;
|
|
3284
|
+
await this.#putBack(name, s);
|
|
3285
|
+
return false;
|
|
3286
|
+
}
|
|
3287
|
+
while (s.dirty.size > 0 && !s.gone) {
|
|
3288
|
+
const rows = [...s.dirty];
|
|
3289
|
+
s.dirty.clear();
|
|
3290
|
+
const writing = rowsOf(s.memory, rows);
|
|
3291
|
+
try {
|
|
3292
|
+
const quiet = s.calls === 0;
|
|
3293
|
+
await this.store.save(name, s.memory, rows);
|
|
3294
|
+
for (const [row, value] of writing)
|
|
3295
|
+
(quiet ? s.kept : s.under).set(row, value);
|
|
3296
|
+
} catch {
|
|
3297
|
+
this.#fault(name);
|
|
3298
|
+
for (const row of rows)
|
|
3299
|
+
s.dirty.add(row);
|
|
3300
|
+
s.fault = true;
|
|
3301
|
+
if (s.calls > 0) {
|
|
3302
|
+
s.back = true;
|
|
3303
|
+
return false;
|
|
3304
|
+
}
|
|
3305
|
+
await this.#putBack(name, s);
|
|
3306
|
+
return false;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
return true;
|
|
3310
|
+
})();
|
|
3311
|
+
try {
|
|
3312
|
+
return await s.running;
|
|
3313
|
+
} finally {
|
|
3314
|
+
s.running = null;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
// ---- the directory
|
|
3318
|
+
// Bytes to one of this harbor's own doors, and the one bit the door says
|
|
3319
|
+
// beside its reply, counted.
|
|
3320
|
+
async #own(pk, door2, bytes) {
|
|
3321
|
+
const r = await door2(bytes);
|
|
3322
|
+
if (!r.heard)
|
|
3323
|
+
this.refused.set(pk, (this.refused.get(pk) ?? 0) + 1);
|
|
3324
|
+
return new Uint8Array(r.bytes);
|
|
3325
|
+
}
|
|
3326
|
+
async carry(pk, bytes) {
|
|
3327
|
+
if (this.down.has(pk))
|
|
3328
|
+
return void 0;
|
|
3329
|
+
const door2 = this.doors.get(pk);
|
|
3330
|
+
if (door2)
|
|
3331
|
+
return this.#own(pk, door2, bytes);
|
|
3332
|
+
const bound = this.reaches.get(pk);
|
|
3333
|
+
if (bound)
|
|
3334
|
+
return bound.reach.carry(pk, bytes);
|
|
3335
|
+
for (const reach of this.fallbacks) {
|
|
3336
|
+
const back = await reach.carry(pk, bytes);
|
|
3337
|
+
if (back !== void 0)
|
|
3338
|
+
return back;
|
|
3339
|
+
}
|
|
3340
|
+
return void 0;
|
|
3341
|
+
}
|
|
3342
|
+
async deliver(pk, bytes) {
|
|
3343
|
+
if (this.down.has(pk))
|
|
3344
|
+
return void 0;
|
|
3345
|
+
const door2 = this.doors.get(pk);
|
|
3346
|
+
if (door2)
|
|
3347
|
+
return this.#own(pk, door2, bytes);
|
|
3348
|
+
const bound = this.reaches.get(pk);
|
|
3349
|
+
return bound?.held ? bound.reach.carry(pk, bytes) : void 0;
|
|
3350
|
+
}
|
|
3351
|
+
hint(pk, url) {
|
|
3352
|
+
if (this.reaches.has(pk))
|
|
3353
|
+
return;
|
|
3354
|
+
this.reaches.set(pk, { reach: request(url), held: false });
|
|
3355
|
+
}
|
|
3356
|
+
async remember(pk, url) {
|
|
3357
|
+
this.hint(pk, url);
|
|
3358
|
+
await this.store.hint(pk, url);
|
|
3359
|
+
}
|
|
3360
|
+
// An announce is a claim, and a claim binds nothing until proven: anyone
|
|
3361
|
+
// who can reach a listener could otherwise name a pk that is not theirs and
|
|
3362
|
+
// take its reachability. The proof is the door itself, as it already is. A
|
|
3363
|
+
// lid this harbor minted and noise after it is a box that does not open,
|
|
3364
|
+
// and the door answers one such box with silence sealed to the lid and
|
|
3365
|
+
// signed by the ward key. Only the holder of that ward's seed can write
|
|
3366
|
+
// that reply, the lid is fresh so nothing replays, and a box that does not
|
|
3367
|
+
// open writes nothing at the ward. This is the one box a harbor ever opens,
|
|
3368
|
+
// the one it sealed itself, and it reads nothing from it but that it is
|
|
3369
|
+
// the silence a door owes such a box, signed by the claimed key.
|
|
3370
|
+
async prove(pk, reach) {
|
|
3371
|
+
if (!isWardPk(pk))
|
|
3372
|
+
return false;
|
|
3373
|
+
try {
|
|
3374
|
+
const lid2 = await sealingPair(entropy(KEY));
|
|
3375
|
+
const back = await within(PROBE, reach.carry(pk, concat([lid2.pk, entropy(KEY)])));
|
|
3376
|
+
if (back === LATE || back === void 0)
|
|
3377
|
+
return false;
|
|
3378
|
+
const reply = await openReply(back, lid2.secret, wardSignPk(pk));
|
|
3379
|
+
return reply !== null && "silence" in reply;
|
|
3380
|
+
} catch {
|
|
3381
|
+
return false;
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
// Bind what the far side claims, each pk proven at its door first. Both
|
|
3385
|
+
// sides bind this way: the listener the dialer's claims, the dialer the
|
|
3386
|
+
// listener's, since a claim is a claim whichever end made it. An announce
|
|
3387
|
+
// names at most this many pks, and they are proven a few at a time: a
|
|
3388
|
+
// side that announced a thousand would otherwise cost a thousand keys, a
|
|
3389
|
+
// thousand frames and a thousand waits at once, on its word alone.
|
|
3390
|
+
async bind(pks, reach, held3) {
|
|
3391
|
+
const said2 = (announces.get(reach) ?? 0) + 1;
|
|
3392
|
+
announces.set(reach, said2);
|
|
3393
|
+
for (const [pk, b] of this.reaches)
|
|
3394
|
+
if (b.reach === reach && !pks.includes(pk))
|
|
3395
|
+
this.reaches.delete(pk);
|
|
3396
|
+
const claimed = [...new Set(pks)].slice(0, ANNOUNCE);
|
|
3397
|
+
const prove = async (pk) => {
|
|
3398
|
+
if (this.doors.has(pk))
|
|
3399
|
+
return;
|
|
3400
|
+
const standing2 = this.reaches.get(pk);
|
|
3401
|
+
if (standing2?.held && standing2.reach !== reach)
|
|
3402
|
+
return;
|
|
3403
|
+
if (!await this.prove(pk, reach))
|
|
3404
|
+
return;
|
|
3405
|
+
if (announces.get(reach) !== said2)
|
|
3406
|
+
return;
|
|
3407
|
+
if (this.reaches.get(pk)?.held && this.reaches.get(pk).reach !== reach)
|
|
3408
|
+
return;
|
|
3409
|
+
this.reaches.set(pk, { reach, held: held3 });
|
|
3410
|
+
};
|
|
3411
|
+
for (let i = 0; i < claimed.length; i += PROVING)
|
|
3412
|
+
await Promise.all(claimed.slice(i, i + PROVING).map(prove));
|
|
3413
|
+
}
|
|
3414
|
+
unbind(reach) {
|
|
3415
|
+
for (const [pk, b] of this.reaches)
|
|
3416
|
+
if (b.reach === reach)
|
|
3417
|
+
this.reaches.delete(pk);
|
|
3418
|
+
}
|
|
3419
|
+
// ---- wards
|
|
3420
|
+
// Mint a seed and boot an empty ward under a name: a world born here.
|
|
3421
|
+
async create(name, user = "", code = DEFAULT_CODE) {
|
|
3422
|
+
const kept2 = { seed: entropy(KEY), partition: {}, record: { pk: "", code, user } };
|
|
3423
|
+
await this.store.put(name, kept2);
|
|
3424
|
+
const hosted = await this.host(name, kept2);
|
|
3425
|
+
await this.store.record(name, hosted.record);
|
|
3426
|
+
return hosted;
|
|
3427
|
+
}
|
|
3428
|
+
// Stop serving a ward and take it out of the store: what was kept comes
|
|
3429
|
+
// back, for another harbor to put. The partition is written first, and
|
|
3430
|
+
// never again under this name: whoever still holds the old pointers holds
|
|
3431
|
+
// a ward that saves nothing and answers its owner silence.
|
|
3432
|
+
async drop(name) {
|
|
3433
|
+
const h = this.wards.get(name);
|
|
3434
|
+
if (h) {
|
|
3435
|
+
await h.save();
|
|
3436
|
+
const s = this.#saving.get(name);
|
|
3437
|
+
if (s)
|
|
3438
|
+
s.gone = true;
|
|
3439
|
+
this.#saving.delete(name);
|
|
3440
|
+
this.wards.delete(name);
|
|
3441
|
+
this.doors.delete(h.pk);
|
|
3442
|
+
this.#announce();
|
|
3443
|
+
}
|
|
3444
|
+
return this.store.take(name);
|
|
3445
|
+
}
|
|
3446
|
+
// Put what another harbor dropped, and boot it: same seed, same pk. A ward
|
|
3447
|
+
// that will not boot is not adopted. This is the one path where a partition
|
|
3448
|
+
// written somewhere else arrives, so it is the one place the ward's own
|
|
3449
|
+
// shape check is met, and the name is refused for as long as it is kept:
|
|
3450
|
+
// a partition put and left would hold the name against every later try,
|
|
3451
|
+
// with a copy of it in a store that can never serve it. It goes back out,
|
|
3452
|
+
// and the only copy is the one the caller is still holding.
|
|
3453
|
+
async adopt(name, kept2) {
|
|
3454
|
+
await this.store.put(name, kept2);
|
|
3455
|
+
try {
|
|
3456
|
+
return await this.host(name, kept2);
|
|
3457
|
+
} catch (e) {
|
|
3458
|
+
await this.store.take(name);
|
|
3459
|
+
throw e;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
// A ward from what the store keeps: the ground, once, and two pointers.
|
|
3463
|
+
async host(name, kept2) {
|
|
3464
|
+
const { seed: seed2, partition: memory } = kept2;
|
|
3465
|
+
const classes = await this.loader(kept2.record);
|
|
3466
|
+
const objects = /* @__PURE__ */ new WeakMap();
|
|
3467
|
+
const ground = {
|
|
3468
|
+
seed: seed2,
|
|
3469
|
+
memory,
|
|
3470
|
+
// This ward's own classes first, then the harbor's: a ward that names a
|
|
3471
|
+
// class of its own is answered with hers.
|
|
3472
|
+
instantiate: maker(objects, classes, this.classes),
|
|
3473
|
+
carry: (pk2, bytes) => this.carry(pk2, new Uint8Array(bytes)),
|
|
3474
|
+
random: entropy,
|
|
3475
|
+
wrote: (row) => this.#wrote(name, row),
|
|
3476
|
+
// What the ward wrote is in the store when this resolves, and it says
|
|
3477
|
+
// whether the store took it. The ward calls it at the end of every
|
|
3478
|
+
// arrival, before it seals, so a call through a pointer has already
|
|
3479
|
+
// waited for the store when it comes back: a restart right after one
|
|
3480
|
+
// finds everything, and a refusal is that arrival's own answer rather
|
|
3481
|
+
// than a number counted behind it. A ward that wrote nothing has
|
|
3482
|
+
// nothing to save, so asking it what it holds is free, which is what
|
|
3483
|
+
// makes the ask pointer the way to find out rather than an expensive
|
|
3484
|
+
// one.
|
|
3485
|
+
//
|
|
3486
|
+
// A ward on its way out keeps nothing and refuses nothing: an arrival
|
|
3487
|
+
// racing an unhost is not a store saying no, and saying so would be a
|
|
3488
|
+
// lie in the one direction that matters.
|
|
3489
|
+
keep: async () => {
|
|
3490
|
+
const s = this.#saving.get(name);
|
|
3491
|
+
if (!s || s.gone)
|
|
3492
|
+
return true;
|
|
3493
|
+
const ok = await this.#flush(name);
|
|
3494
|
+
const no = !ok || s.fault;
|
|
3495
|
+
s.fault = false;
|
|
3496
|
+
return !no;
|
|
3497
|
+
},
|
|
3498
|
+
// The ward says a call is in flight, and says when it ends. The harbor
|
|
3499
|
+
// goes on writing under it; what it does not do is take a save made
|
|
3500
|
+
// there as the point a refused ward is put back to, and a refusal that
|
|
3501
|
+
// lands under a call waits here for the call to end before it puts the
|
|
3502
|
+
// ward back. The ward is the one that knows, because a being reaching
|
|
3503
|
+
// another being of her own ward goes to that ward's door directly and
|
|
3504
|
+
// the harbor never sees that call at all.
|
|
3505
|
+
calling: () => {
|
|
3506
|
+
const s = this.#saving.get(name);
|
|
3507
|
+
if (!s)
|
|
3508
|
+
return () => {
|
|
3509
|
+
};
|
|
3510
|
+
s.calls += 1;
|
|
3511
|
+
let lowered = false;
|
|
3512
|
+
return () => {
|
|
3513
|
+
if (lowered)
|
|
3514
|
+
return;
|
|
3515
|
+
lowered = true;
|
|
3516
|
+
s.calls -= 1;
|
|
3517
|
+
if (s.calls > 0 || s.gone)
|
|
3518
|
+
return;
|
|
3519
|
+
if (s.back)
|
|
3520
|
+
void this.#flush(name);
|
|
3521
|
+
else
|
|
3522
|
+
this.#settle(name, s);
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
};
|
|
3526
|
+
const lend = this.lendFor(name, kept2.record);
|
|
3527
|
+
if (lend)
|
|
3528
|
+
ground.lend = lend;
|
|
3529
|
+
const saving = { memory, dirty: /* @__PURE__ */ new Set(), kept: new Map(rowsOf(memory, rowsIn(memory))), running: null, timer: void 0, gone: false, calls: 0, fault: false, back: false, under: /* @__PURE__ */ new Map() };
|
|
3530
|
+
this.#saving.set(name, saving);
|
|
3531
|
+
let w, pk;
|
|
3532
|
+
try {
|
|
3533
|
+
w = await Ward(ground);
|
|
3534
|
+
pk = await learnPk(w);
|
|
3535
|
+
} catch (e) {
|
|
3536
|
+
saving.gone = true;
|
|
3537
|
+
this.#saving.delete(name);
|
|
3538
|
+
throw e;
|
|
3539
|
+
}
|
|
3540
|
+
const save = async () => void await ground.keep?.();
|
|
3541
|
+
const door2 = (bytes) => w.door(bytes);
|
|
3542
|
+
const ask = async (method, args) => {
|
|
3543
|
+
if (saving.gone)
|
|
3544
|
+
return silence;
|
|
3545
|
+
return w.ask(method, args);
|
|
3546
|
+
};
|
|
3547
|
+
const beings = memory.beings;
|
|
3548
|
+
const being = (key) => {
|
|
3549
|
+
const cells = beings && Object.hasOwn(beings, key) ? beings[key] : void 0;
|
|
3550
|
+
return cells ? objects.get(cells) : void 0;
|
|
3551
|
+
};
|
|
3552
|
+
const keys = () => Object.keys(beings ?? {}).filter((k) => being(k) !== void 0);
|
|
3553
|
+
const hosted = { door: door2, ask, pk, name, record: { ...kept2.record, pk }, being, keys, save };
|
|
3554
|
+
this.wards.set(name, hosted);
|
|
3555
|
+
this.doors.set(hosted.pk, door2);
|
|
3556
|
+
await save();
|
|
3557
|
+
this.#announce();
|
|
3558
|
+
return hosted;
|
|
3559
|
+
}
|
|
3560
|
+
};
|
|
3561
|
+
|
|
3562
|
+
// ../quo/dist/harbor/dial.js
|
|
3563
|
+
function dial(harbor, url) {
|
|
3564
|
+
const d = { url, socket: null, wake: () => {
|
|
3565
|
+
}, close: () => {
|
|
3566
|
+
} };
|
|
3567
|
+
let stopped = false, wait = 1e3, timer, line, held3;
|
|
3568
|
+
const connect = () => {
|
|
3569
|
+
timer = void 0;
|
|
3570
|
+
if (stopped)
|
|
3571
|
+
return;
|
|
3572
|
+
line = new WebSocket(url.replace(/\/$/, "").replace(/^http/, "ws"));
|
|
3573
|
+
const s = new Socket(line, (pk, bytes) => harbor.deliver(pk, bytes), (far) => {
|
|
3574
|
+
wait = 1e3;
|
|
3575
|
+
void harbor.bind(far, s, false);
|
|
3576
|
+
}, (why) => {
|
|
3577
|
+
shed();
|
|
3578
|
+
if (held3?.socket !== s)
|
|
3579
|
+
return;
|
|
3580
|
+
held3 = void 0;
|
|
3581
|
+
if (why.code === REFUSED)
|
|
3582
|
+
wait = 3e4;
|
|
3583
|
+
if (!stopped)
|
|
3584
|
+
timer = setTimeout(connect, wait);
|
|
3585
|
+
wait = Math.min(wait * 2, 3e4);
|
|
3586
|
+
});
|
|
3587
|
+
const shed = () => {
|
|
3588
|
+
harbor.announcers.delete(say);
|
|
3589
|
+
harbor.unbind(s);
|
|
3590
|
+
if (d.socket === s)
|
|
3591
|
+
d.socket = null;
|
|
3592
|
+
const at3 = harbor.fallbacks.indexOf(s);
|
|
3593
|
+
if (at3 !== -1)
|
|
3594
|
+
harbor.fallbacks.splice(at3, 1);
|
|
3595
|
+
};
|
|
3596
|
+
held3 = { socket: s, shed };
|
|
3597
|
+
harbor.fallbacks.push(s);
|
|
3598
|
+
const say = () => s.announce([...harbor.doors.keys()]);
|
|
3599
|
+
line.addEventListener("open", () => {
|
|
3600
|
+
if (stopped)
|
|
3601
|
+
return s.close();
|
|
3602
|
+
d.socket = s;
|
|
3603
|
+
harbor.announcers.add(say);
|
|
3604
|
+
say();
|
|
3605
|
+
});
|
|
3606
|
+
};
|
|
3607
|
+
d.wake = () => {
|
|
3608
|
+
if (stopped)
|
|
3609
|
+
return;
|
|
3610
|
+
clearTimeout(timer);
|
|
3611
|
+
timer = void 0;
|
|
3612
|
+
wait = 1e3;
|
|
3613
|
+
const old = held3;
|
|
3614
|
+
held3 = void 0;
|
|
3615
|
+
old?.shed();
|
|
3616
|
+
old?.socket.close();
|
|
3617
|
+
connect();
|
|
3618
|
+
};
|
|
3619
|
+
d.close = () => {
|
|
3620
|
+
stopped = true;
|
|
3621
|
+
clearTimeout(timer);
|
|
3622
|
+
line?.close();
|
|
3623
|
+
};
|
|
3624
|
+
connect();
|
|
3625
|
+
return d;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// harbor/idb.ts
|
|
3629
|
+
var WARDS2 = "wards";
|
|
3630
|
+
var ROWS = "rows";
|
|
3631
|
+
var HINTS = "hints";
|
|
3632
|
+
var at2 = (name, row) => `${name}\0${row}`;
|
|
3633
|
+
var under = (name) => IDBKeyRange.bound(at2(name, ""), at2(name, "\uFFFF"));
|
|
3634
|
+
function open2(name) {
|
|
3635
|
+
return new Promise((ok, no) => {
|
|
3636
|
+
const req = indexedDB.open(name, 2);
|
|
3637
|
+
req.onupgradeneeded = () => {
|
|
3638
|
+
const db = req.result;
|
|
3639
|
+
if (!db.objectStoreNames.contains(WARDS2)) db.createObjectStore(WARDS2);
|
|
3640
|
+
if (!db.objectStoreNames.contains(ROWS)) db.createObjectStore(ROWS);
|
|
3641
|
+
if (!db.objectStoreNames.contains(HINTS)) db.createObjectStore(HINTS);
|
|
3642
|
+
};
|
|
3643
|
+
req.onsuccess = () => ok(req.result);
|
|
3644
|
+
req.onerror = () => no(req.error ?? new Error("indexedDB would not open"));
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
function done(req) {
|
|
3648
|
+
return new Promise((ok, no) => {
|
|
3649
|
+
req.onsuccess = () => ok(req.result);
|
|
3650
|
+
req.onerror = () => no(req.error ?? new Error("indexedDB request failed"));
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
var Idb = class {
|
|
3654
|
+
name;
|
|
3655
|
+
#db;
|
|
3656
|
+
constructor(name = "quo") {
|
|
3657
|
+
this.name = name;
|
|
3658
|
+
}
|
|
3659
|
+
#open() {
|
|
3660
|
+
return this.#db ??= open2(this.name);
|
|
3661
|
+
}
|
|
3662
|
+
async #tx(store, mode) {
|
|
3663
|
+
return (await this.#open()).transaction(store, mode).objectStore(store);
|
|
3664
|
+
}
|
|
3665
|
+
async list() {
|
|
3666
|
+
return (await done((await this.#tx(WARDS2, "readonly")).getAllKeys())).map(String);
|
|
3667
|
+
}
|
|
3668
|
+
async load(name) {
|
|
3669
|
+
const row = await done((await this.#tx(WARDS2, "readonly")).get(name));
|
|
3670
|
+
if (!row) return void 0;
|
|
3671
|
+
const st = await this.#tx(ROWS, "readonly");
|
|
3672
|
+
const keys = (await done(st.getAllKeys(under(name)))).map(String);
|
|
3673
|
+
const vals = await done(st.getAll(under(name)));
|
|
3674
|
+
return { seed: new Uint8Array(row.seed), partition: fromRows(Object.fromEntries(keys.map((k, i) => [k.slice(name.length + 1), vals[i]]))), record: row.record };
|
|
3675
|
+
}
|
|
3676
|
+
async put(name, kept2) {
|
|
3677
|
+
const st = await this.#tx(WARDS2, "readwrite");
|
|
3678
|
+
if (await done(st.getKey(name)) !== void 0) throw new Error(`ward ${name} already exists in ${this.name}`);
|
|
3679
|
+
await done(st.put({ seed: kept2.seed, record: kept2.record }, name));
|
|
3680
|
+
await this.#keep(name, kept2.partition, rowsIn(kept2.partition));
|
|
3681
|
+
}
|
|
3682
|
+
async save(name, partition, rows) {
|
|
3683
|
+
if (await done((await this.#tx(WARDS2, "readonly")).getKey(name)) === void 0) return;
|
|
3684
|
+
await this.#keep(name, partition, rows);
|
|
3685
|
+
}
|
|
3686
|
+
// The rows named, written or deleted. A row whose being has left comes back
|
|
3687
|
+
// undefined and her record goes with her.
|
|
3688
|
+
async #keep(name, partition, rows) {
|
|
3689
|
+
const st = await this.#tx(ROWS, "readwrite");
|
|
3690
|
+
for (const [row, value] of rowsOf(partition, rows)) {
|
|
3691
|
+
if (value === void 0) await done(st.delete(at2(name, row)));
|
|
3692
|
+
else await done(st.put(value, at2(name, row)));
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
async record(name, record) {
|
|
3696
|
+
const st = await this.#tx(WARDS2, "readwrite");
|
|
3697
|
+
const row = await done(st.get(name));
|
|
3698
|
+
if (!row) return;
|
|
3699
|
+
await done(st.put({ ...row, record }, name));
|
|
3700
|
+
}
|
|
3701
|
+
async take(name) {
|
|
3702
|
+
const kept2 = await this.load(name);
|
|
3703
|
+
if (!kept2) return void 0;
|
|
3704
|
+
await done((await this.#tx(ROWS, "readwrite")).delete(under(name)));
|
|
3705
|
+
await done((await this.#tx(WARDS2, "readwrite")).delete(name));
|
|
3706
|
+
return kept2;
|
|
3707
|
+
}
|
|
3708
|
+
async hints() {
|
|
3709
|
+
const st = await this.#tx(HINTS, "readonly");
|
|
3710
|
+
const keys = (await done(st.getAllKeys())).map(String);
|
|
3711
|
+
const vals = await done(st.getAll());
|
|
3712
|
+
return Object.fromEntries(keys.map((k, i) => [k, vals[i]]));
|
|
3713
|
+
}
|
|
3714
|
+
async hint(pk, url) {
|
|
3715
|
+
await done((await this.#tx(HINTS, "readwrite")).put(url, pk));
|
|
3716
|
+
}
|
|
3717
|
+
close() {
|
|
3718
|
+
void this.#db?.then((db) => db.close());
|
|
3719
|
+
this.#db = void 0;
|
|
3720
|
+
}
|
|
3721
|
+
};
|
|
3722
|
+
|
|
3723
|
+
// harbor/browser.ts
|
|
3724
|
+
var BUILT_IN = { User, Desk, Avatar, Presence, Courier, ...FACULTY_CLASSES };
|
|
3725
|
+
var BrowserHarbor = class extends Harbor {
|
|
3726
|
+
name;
|
|
3727
|
+
dialers = [];
|
|
3728
|
+
#locked;
|
|
3729
|
+
#release;
|
|
3730
|
+
// `lend` is what this box lends the beings of the wards it hosts: a name
|
|
3731
|
+
// to an invitation on a being of its own ward. `stand` builds one; a bare
|
|
3732
|
+
// tab hands none and every name answers null.
|
|
3733
|
+
constructor(store = "quo", classes = {}, lend) {
|
|
3734
|
+
const s = typeof store === "string" ? new Idb(store) : store;
|
|
3735
|
+
super(s, async () => ({ ...BUILT_IN, ...classes }), lend);
|
|
3736
|
+
this.name = typeof store === "string" ? store : "quo";
|
|
3737
|
+
this.#locked = typeof store === "string";
|
|
3738
|
+
}
|
|
3739
|
+
// What this box lends, by name. A bare tab says nothing, stands no ward of
|
|
3740
|
+
// its own and lends nothing.
|
|
3741
|
+
lent;
|
|
3742
|
+
// What this box lends is said before it boots, and standing the ward is
|
|
3743
|
+
// part of booting: a being born in any other ward lends from her first
|
|
3744
|
+
// line, and a ground that could not reach it yet would leave her holding
|
|
3745
|
+
// nothing until her next birth.
|
|
3746
|
+
async stand(lent) {
|
|
3747
|
+
this.lent = lent;
|
|
3748
|
+
if (this.wards.size > 0) this.lend = await stand(this, lent);
|
|
3749
|
+
}
|
|
3750
|
+
// Take the lock and boot every ward kept here. A lock already held is
|
|
3751
|
+
// another tab's harbor over this seed, and this one refuses to boot.
|
|
3752
|
+
async boot() {
|
|
3753
|
+
if (this.#locked) await this.#lease();
|
|
3754
|
+
if (this.lent) this.lend = await stand(this, this.lent);
|
|
3755
|
+
await super.boot();
|
|
3756
|
+
await settled();
|
|
3757
|
+
}
|
|
3758
|
+
// Hold one socket to a world's quo. route.
|
|
3759
|
+
dial(url) {
|
|
3760
|
+
const d = dial(this, url);
|
|
3761
|
+
this.dialers.push(d);
|
|
3762
|
+
return d;
|
|
3763
|
+
}
|
|
3764
|
+
// The device is back: a phone out of a pocket, an app to the foreground.
|
|
3765
|
+
// Every socket is told, and one that died silently is dialed again.
|
|
3766
|
+
wake() {
|
|
3767
|
+
for (const d of this.dialers) d.wake();
|
|
3768
|
+
}
|
|
3769
|
+
async close() {
|
|
3770
|
+
for (const d of this.dialers.splice(0)) d.close();
|
|
3771
|
+
this.store.close?.();
|
|
3772
|
+
this.#release?.();
|
|
3773
|
+
this.#release = void 0;
|
|
3774
|
+
}
|
|
3775
|
+
async #lease() {
|
|
3776
|
+
const locks = globalThis.navigator?.locks;
|
|
3777
|
+
if (!locks) return;
|
|
3778
|
+
const granted = await new Promise((ok) => {
|
|
3779
|
+
void locks.request(`quo:${this.name}`, { ifAvailable: true }, (lock) => {
|
|
3780
|
+
if (!lock) {
|
|
3781
|
+
ok(false);
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
ok(true);
|
|
3785
|
+
return new Promise((release) => this.#release = release);
|
|
3786
|
+
});
|
|
3787
|
+
});
|
|
3788
|
+
if (!granted) throw new Error(`harbor ${this.name} is held by another tab`);
|
|
3789
|
+
}
|
|
3790
|
+
};
|
|
3791
|
+
|
|
3792
|
+
// beings/link.ts
|
|
3793
|
+
var KEY2 = "quo";
|
|
3794
|
+
var PROOF = "proof";
|
|
3795
|
+
var HEX3 = (n) => `[0-9a-f]{${n}}`;
|
|
3796
|
+
var FULL = new RegExp(`^(${HEX3(128)})\\.(${HEX3(64)})\\.(${HEX3(64)})$`);
|
|
3797
|
+
var PUBLIC2 = new RegExp(`^${HEX3(128)}$`);
|
|
3798
|
+
function format(inv) {
|
|
3799
|
+
return inv.heir && inv.secret ? `${inv.ward}.${inv.heir}.${inv.secret}` : inv.ward;
|
|
3800
|
+
}
|
|
3801
|
+
function parse(hash) {
|
|
3802
|
+
const m = /(?:^#|&)quo=([^&]+)/.exec(hash.startsWith("#") ? hash : `#${hash}`);
|
|
3803
|
+
if (!m) return null;
|
|
3804
|
+
const v = decodeURIComponent(m[1]);
|
|
3805
|
+
const full = FULL.exec(v);
|
|
3806
|
+
if (full) return { ward: full[1], heir: full[2], secret: full[3] };
|
|
3807
|
+
return PUBLIC2.test(v) ? { ward: v } : null;
|
|
3808
|
+
}
|
|
3809
|
+
function parseProof(hash) {
|
|
3810
|
+
const m = new RegExp(`(?:^#|&)${PROOF}=([^&]+)`).exec(hash.startsWith("#") ? hash : `#${hash}`);
|
|
3811
|
+
if (!m) return null;
|
|
3812
|
+
const v = decodeURIComponent(m[1]);
|
|
3813
|
+
const at3 = v.indexOf(".");
|
|
3814
|
+
if (at3 < 1 || at3 === v.length - 1) return null;
|
|
3815
|
+
const kind = v.slice(0, at3);
|
|
3816
|
+
return /^[\w-]{1,40}$/.test(kind) ? { kind, token: v.slice(at3 + 1) } : null;
|
|
3817
|
+
}
|
|
3818
|
+
function strip(hash) {
|
|
3819
|
+
const rest = (hash.startsWith("#") ? hash.slice(1) : hash).split("&").filter((p) => !p.startsWith(`${KEY2}=`) && !p.startsWith(`${PROOF}=`)).join("&");
|
|
3820
|
+
return rest ? `#${rest}` : "";
|
|
3821
|
+
}
|
|
3822
|
+
function link(page2, inv) {
|
|
3823
|
+
return `${page2}#${KEY2}=${format(inv)}`;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
// human/worlds.ts
|
|
3827
|
+
var localName = (pk) => `w-${pk.slice(0, 16)}`;
|
|
3828
|
+
async function world(harbor, pk) {
|
|
3829
|
+
const name = localName(pk);
|
|
3830
|
+
return harbor.wards.get(name) ?? await harbor.create(name, "me");
|
|
3831
|
+
}
|
|
3832
|
+
function relations(ward) {
|
|
3833
|
+
const out = [];
|
|
3834
|
+
for (const key of ward.keys()) {
|
|
3835
|
+
const a = ward.being(key);
|
|
3836
|
+
if (a instanceof Avatar && a.standings[USER]) out.push({ key, avatar: a });
|
|
3837
|
+
}
|
|
3838
|
+
return out;
|
|
3839
|
+
}
|
|
3840
|
+
async function fresh2(ward) {
|
|
3841
|
+
for (const key of ward.keys()) {
|
|
3842
|
+
const a = ward.being(key);
|
|
3843
|
+
if (a instanceof Avatar && !a.standings[USER]) return { key, avatar: a };
|
|
3844
|
+
}
|
|
3845
|
+
for (let n = 1; n <= 64; n++) {
|
|
3846
|
+
const key = `r${n}`;
|
|
3847
|
+
const out = await ward.ask("boot", { key, class: "Presence" });
|
|
3848
|
+
if (out.error) continue;
|
|
3849
|
+
await settled();
|
|
3850
|
+
return { key, avatar: ward.being(key) };
|
|
3851
|
+
}
|
|
3852
|
+
throw new Error("the tab could not boot an avatar: every key it tries is taken");
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// human/move.ts
|
|
3856
|
+
var { hex: hex3, unhex: unhex2 } = arithmetic_exports;
|
|
3857
|
+
var COURIER = "courier";
|
|
3858
|
+
var KEY3 = "courier";
|
|
3859
|
+
async function courier(harbor) {
|
|
3860
|
+
const ward = harbor.wards.get(COURIER) ?? await harbor.create(COURIER, "me");
|
|
3861
|
+
if (!ward.being(KEY3)) {
|
|
3862
|
+
const out = await ward.ask("boot", { key: KEY3, class: "Courier" });
|
|
3863
|
+
if (out.error) throw new Error(`no courier on this device: ${out.error}`);
|
|
3864
|
+
}
|
|
3865
|
+
return ward.being(KEY3);
|
|
3866
|
+
}
|
|
3867
|
+
var away = async (harbor) => {
|
|
3868
|
+
const c = harbor.wards.get(COURIER)?.being(KEY3);
|
|
3869
|
+
if (c) {
|
|
3870
|
+
c.giving = void 0;
|
|
3871
|
+
c.gone = void 0;
|
|
3872
|
+
}
|
|
3873
|
+
await harbor.drop(COURIER);
|
|
3874
|
+
};
|
|
3875
|
+
async function giveWorld(harbor, world2, at3) {
|
|
3876
|
+
const name = localName(world2.pk);
|
|
3877
|
+
if (!harbor.wards.has(name)) throw new Error("this device does not hold that world");
|
|
3878
|
+
const c = await courier(harbor);
|
|
3879
|
+
const invitation = await c.invite(MOVER);
|
|
3880
|
+
if (!invitation) {
|
|
3881
|
+
await away(harbor);
|
|
3882
|
+
throw new Error("this device is already offering a world");
|
|
3883
|
+
}
|
|
3884
|
+
let landed = () => {
|
|
3885
|
+
};
|
|
3886
|
+
const moved = new Promise((ok) => landed = ok);
|
|
3887
|
+
c.giving = async () => {
|
|
3888
|
+
const ward = harbor.wards.get(name);
|
|
3889
|
+
if (!ward) return { error: "this device no longer holds that world" };
|
|
3890
|
+
await ward.save();
|
|
3891
|
+
const kept2 = await harbor.store.load(name);
|
|
3892
|
+
if (!kept2) return { error: "this device no longer holds that world" };
|
|
3893
|
+
return { world: world2.pk, at: world2.at, name: world2.name, seed: hex3(kept2.seed), partition: kept2.partition, record: kept2.record };
|
|
3894
|
+
};
|
|
3895
|
+
c.gone = async () => {
|
|
3896
|
+
const gone = await harbor.drop(name);
|
|
3897
|
+
if (!gone) return { error: "this device no longer holds that world" };
|
|
3898
|
+
landed(world2.pk);
|
|
3899
|
+
return { deleted: world2.pk };
|
|
3900
|
+
};
|
|
3901
|
+
await harbor.wards.get(COURIER).save();
|
|
3902
|
+
return { offer: link(at3, invitation), invitation, moved, close: () => away(harbor) };
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// human/wake.ts
|
|
3906
|
+
var webToken = (endpoint, p256dh, auth) => ({ kind: "web", endpoint, ...p256dh && auth ? { p256dh, auth } : {} });
|
|
3907
|
+
async function subscribe(web, key, ask = false) {
|
|
3908
|
+
const workers = globalThis.navigator?.serviceWorker;
|
|
3909
|
+
if (!workers || !("PushManager" in globalThis) || !("Notification" in globalThis)) return null;
|
|
3910
|
+
try {
|
|
3911
|
+
const registration = await workers.register(`${web}/sw.js`, { type: "module" });
|
|
3912
|
+
await workers.ready;
|
|
3913
|
+
if (Notification.permission === "default" && ask) await Notification.requestPermission();
|
|
3914
|
+
if (Notification.permission !== "granted") return null;
|
|
3915
|
+
const held3 = await registration.pushManager.getSubscription();
|
|
3916
|
+
const sub = held3 ?? await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
|
|
3917
|
+
const keys = sub.toJSON().keys;
|
|
3918
|
+
return webToken(sub.endpoint, keys?.p256dh, keys?.auth);
|
|
3919
|
+
} catch {
|
|
3920
|
+
return null;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
async function drained(harbor, pks) {
|
|
3924
|
+
const out = [];
|
|
3925
|
+
for (const pk of pks) {
|
|
3926
|
+
const ward = harbor.wards.get(localName(pk));
|
|
3927
|
+
if (!ward) continue;
|
|
3928
|
+
for (const rel of relations(ward)) {
|
|
3929
|
+
const got = await rel.avatar.call("missed");
|
|
3930
|
+
const objects = got.missed;
|
|
3931
|
+
if (!Array.isArray(objects)) continue;
|
|
3932
|
+
for (const o of objects) {
|
|
3933
|
+
if (o === null || typeof o !== "object" || Array.isArray(o)) continue;
|
|
3934
|
+
rel.avatar.notify(o);
|
|
3935
|
+
out.push(o);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
await ward.save();
|
|
3939
|
+
}
|
|
3940
|
+
return out;
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
// beings/side.ts
|
|
3944
|
+
function word2(answer) {
|
|
3945
|
+
if (isSilence(answer)) return { word: "silence" };
|
|
3946
|
+
if (isUnreached(answer)) return { word: "unreached" };
|
|
3947
|
+
if (isWord(answer)) return { word: "quo", name: wordOf(answer) };
|
|
3948
|
+
if (answer !== null && typeof answer === "object" && !Array.isArray(answer) && "error" in answer) return { word: "error", value: answer };
|
|
3949
|
+
return { word: "object", value: answer };
|
|
3950
|
+
}
|
|
3951
|
+
var SILENCE_TEXT = "silence: the work may have happened, and no answer came back";
|
|
3952
|
+
var UNREACHED_TEXT = "unreached: nothing was delivered; asking again is safe";
|
|
3953
|
+
var WORD_TEXT = {
|
|
3954
|
+
unreached: UNREACHED_TEXT,
|
|
3955
|
+
late: "late: the wait ran out; the work may have happened",
|
|
3956
|
+
invitation: "invitation: that is not an invitation",
|
|
3957
|
+
dropped: "dropped: the standing is gone",
|
|
3958
|
+
removed: "removed: the far being removed you",
|
|
3959
|
+
absent: "absent: the far being did not come back this run",
|
|
3960
|
+
unannounced: "unannounced: the knock announced no key of its own",
|
|
3961
|
+
repeated: "repeated: that number was already honoured",
|
|
3962
|
+
threw: "threw: the far being threw"
|
|
3963
|
+
};
|
|
3964
|
+
var wordText = (name) => WORD_TEXT[name];
|
|
3965
|
+
|
|
3966
|
+
// human/tree.ts
|
|
3967
|
+
var ROLES = /* @__PURE__ */ new Set(["title", "lead", "body", "quiet", "label"]);
|
|
3968
|
+
var AS = /* @__PURE__ */ new Set(["auto", "text", "table", "cards", "list", "people", "feed"]);
|
|
3969
|
+
var LAYOUTS = /* @__PURE__ */ new Set(["list", "grid", "hero", "feed"]);
|
|
3970
|
+
var TEXT = 4e3;
|
|
3971
|
+
var DEPTH2 = 12;
|
|
3972
|
+
var WIDE = 200;
|
|
3973
|
+
var NAME2 = /^[\w.:-]{1,80}$/;
|
|
3974
|
+
var HREF = 2e3;
|
|
3975
|
+
function sanitiseTree(v, depth = 0) {
|
|
3976
|
+
if (depth > DEPTH2 || v === null || typeof v !== "object" || Array.isArray(v)) return null;
|
|
3977
|
+
const o = v;
|
|
3978
|
+
switch (o.kind) {
|
|
3979
|
+
case "stack":
|
|
3980
|
+
case "row": {
|
|
3981
|
+
const of = Array.isArray(o.of) ? o.of.slice(0, WIDE).map((c) => sanitiseTree(c, depth + 1)).filter((c) => c !== null) : [];
|
|
3982
|
+
if (o.kind === "row") return typeof o.as === "string" && o.as === "person" ? { kind: "row", of, as: "person" } : { kind: "row", of };
|
|
3983
|
+
return typeof o.as === "string" && LAYOUTS.has(o.as) ? { kind: "stack", of, as: o.as } : { kind: "stack", of };
|
|
3984
|
+
}
|
|
3985
|
+
case "text": {
|
|
3986
|
+
if (typeof o.text !== "string") return null;
|
|
3987
|
+
const n = { kind: "text", text: o.text.slice(0, TEXT) };
|
|
3988
|
+
if (typeof o.role === "string" && ROLES.has(o.role)) n.role = o.role;
|
|
3989
|
+
return n;
|
|
3990
|
+
}
|
|
3991
|
+
case "image": {
|
|
3992
|
+
if (typeof o.src !== "string" || !/^data:image\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+$/i.test(o.src)) return null;
|
|
3993
|
+
const n = { kind: "image", src: o.src };
|
|
3994
|
+
if (typeof o.alt === "string") n.alt = o.alt.slice(0, 200);
|
|
3995
|
+
return n;
|
|
3996
|
+
}
|
|
3997
|
+
case "link": {
|
|
3998
|
+
if (typeof o.href !== "string" || o.href.length > HREF) return null;
|
|
3999
|
+
let url;
|
|
4000
|
+
try {
|
|
4001
|
+
url = new URL(o.href);
|
|
4002
|
+
} catch {
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
4006
|
+
const n = { kind: "link", href: url.href };
|
|
4007
|
+
if (typeof o.text === "string" && o.text.trim()) n.text = o.text.slice(0, 200);
|
|
4008
|
+
return n;
|
|
4009
|
+
}
|
|
4010
|
+
case "form":
|
|
4011
|
+
return typeof o.ask === "string" && NAME2.test(o.ask) ? { kind: "form", ask: o.ask } : null;
|
|
4012
|
+
case "answer": {
|
|
4013
|
+
if (typeof o.ask !== "string" || !NAME2.test(o.ask)) return null;
|
|
4014
|
+
const n = { kind: "answer", ask: o.ask };
|
|
4015
|
+
if (typeof o.as === "string" && AS.has(o.as)) n.as = o.as;
|
|
4016
|
+
return n;
|
|
4017
|
+
}
|
|
4018
|
+
case "standing":
|
|
4019
|
+
if (typeof o.id !== "string" || !NAME2.test(o.id)) return null;
|
|
4020
|
+
return o.as === "door" ? { kind: "standing", id: o.id, as: "door" } : { kind: "standing", id: o.id };
|
|
4021
|
+
case "standings":
|
|
4022
|
+
return { kind: "standings" };
|
|
4023
|
+
default:
|
|
4024
|
+
return null;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
function answersIn(n, out = []) {
|
|
4028
|
+
if (n.kind === "answer") out.push(n.ask);
|
|
4029
|
+
if (n.kind === "stack" || n.kind === "row") for (const c of n.of) answersIn(c, out);
|
|
4030
|
+
return out;
|
|
4031
|
+
}
|
|
4032
|
+
var ROLE_TAG = { title: "h1", lead: "p", body: "p", quiet: "p", label: "p" };
|
|
4033
|
+
function titled(n, depth = 0) {
|
|
4034
|
+
if (!n || depth > 2) return false;
|
|
4035
|
+
if (n.kind === "text") return n.role === "title";
|
|
4036
|
+
if (n.kind === "stack" || n.kind === "row") return n.of.some((c) => titled(c, depth + 1));
|
|
4037
|
+
return false;
|
|
4038
|
+
}
|
|
4039
|
+
function paint(n, p) {
|
|
4040
|
+
switch (n.kind) {
|
|
4041
|
+
case "stack": {
|
|
4042
|
+
const inner = (wrap) => n.of.map((c) => `<div class="${wrap}">${paint(c, p)}</div>`).join("");
|
|
4043
|
+
if (n.as === "list") return `<div class="nv-list">${inner("nv-item")}</div>`;
|
|
4044
|
+
if (n.as === "grid") return `<div class="nv-cards">${inner("nv-card")}</div>`;
|
|
4045
|
+
if (n.as === "hero") return `<div class="nv-hero">${n.of.map((c) => paint(c, p)).join("")}</div>`;
|
|
4046
|
+
if (n.as === "feed") return `<div class="nv-feed">${n.of.map((c) => paint(c, p)).join("")}</div>`;
|
|
4047
|
+
return `<div class="nv-stack">${n.of.map((c) => paint(c, p)).join("")}</div>`;
|
|
4048
|
+
}
|
|
4049
|
+
case "row": {
|
|
4050
|
+
if (n.as === "person") {
|
|
4051
|
+
const face2 = n.of.find((c) => c.kind === "image");
|
|
4052
|
+
const rest = n.of.filter((c) => c !== face2).map((c) => paint(c, p)).join("");
|
|
4053
|
+
const words = n.of.find((c) => c.kind === "text");
|
|
4054
|
+
const initial = words && words.kind === "text" ? escape(words.text.trim().charAt(0).toUpperCase()) : "";
|
|
4055
|
+
return `<div class="nv-person"><span class="nv-avatar">${face2 ? paint(face2, p) : initial}</span><div class="nv-stack">${rest}</div></div>`;
|
|
4056
|
+
}
|
|
4057
|
+
return `<div class="nv-row" data-align="center">${n.of.map((c) => paint(c, p)).join("")}</div>`;
|
|
4058
|
+
}
|
|
4059
|
+
case "text": {
|
|
4060
|
+
const role = n.role ?? "body";
|
|
4061
|
+
return `<${ROLE_TAG[role]} class="nv-t-${role}">${escape(n.text)}</${ROLE_TAG[role]}>`;
|
|
4062
|
+
}
|
|
4063
|
+
case "image":
|
|
4064
|
+
return `<img class="nv-picture" alt="${escape(n.alt ?? "")}" src="${n.src}">`;
|
|
4065
|
+
case "link":
|
|
4066
|
+
return p.link(n.href, n.text ?? n.href);
|
|
4067
|
+
case "form":
|
|
4068
|
+
return p.form(n.ask);
|
|
4069
|
+
case "answer":
|
|
4070
|
+
return p.answer(n.ask, n.as ?? "auto");
|
|
4071
|
+
case "standing":
|
|
4072
|
+
return p.standing(n.id, n.as);
|
|
4073
|
+
case "standings":
|
|
4074
|
+
return p.standings();
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
var nobody = { link: () => "", form: () => "", answer: () => "", standing: () => "", standings: () => "" };
|
|
4078
|
+
|
|
4079
|
+
// human/html.ts
|
|
4080
|
+
var escape = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c);
|
|
4081
|
+
var props = (schema) => Object.entries(schema?.properties ?? {});
|
|
4082
|
+
var required = (schema) => new Set(schema?.required ?? []);
|
|
4083
|
+
var MOMENT = "moment";
|
|
4084
|
+
var mine = (ask, at3 = Date.now()) => Object.fromEntries(props(ask.input).filter(([, p]) => p.format === MOMENT).map(([name]) => [name, at3]));
|
|
4085
|
+
var asks = (ask) => [...required(ask.input)].filter((name) => !(name in mine(ask)));
|
|
4086
|
+
function field(name, p, must) {
|
|
4087
|
+
const id = escape(name);
|
|
4088
|
+
const label = `<label for="${id}">${id}${must ? " <b>*</b>" : ""}</label>`;
|
|
4089
|
+
const help = p.description ? `<p class="nv-hint">${escape(p.description)}</p>` : "";
|
|
4090
|
+
const one = (control) => `<div class="nv-field">${label}${control}${help}</div>`;
|
|
4091
|
+
const req = must ? " required" : "";
|
|
4092
|
+
if (Array.isArray(p.enum)) {
|
|
4093
|
+
const text3 = (v) => v !== null && typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
4094
|
+
const options = p.enum.map((v) => `<option value="${escape(text3(v))}">${escape(text3(v))}</option>`).join("");
|
|
4095
|
+
return one(`<select id="${id}" name="${id}"${req}>${must ? "" : '<option value=""></option>'}${options}</select>`);
|
|
4096
|
+
}
|
|
4097
|
+
if (p.type === "boolean") return `<div class="nv-field" data-inline><input type="checkbox" id="${id}" name="${id}" value="true">${label}${help}</div>`;
|
|
4098
|
+
if (p.type === "number" || p.type === "integer") return one(`<input type="number" id="${id}" name="${id}"${p.type === "integer" ? ' step="1"' : ' step="any"'}${req}>`);
|
|
4099
|
+
if (p.type === "string" || p.type === void 0) {
|
|
4100
|
+
const kind = p.format === "password" || p.format === "date" || p.format === "email" || p.format === "time" ? p.format : "text";
|
|
4101
|
+
return one(`<input type="${kind}" id="${id}" name="${id}"${req}>`);
|
|
4102
|
+
}
|
|
4103
|
+
return one(`<textarea id="${id}" name="${id}" placeholder="JSON"${req}></textarea>`);
|
|
4104
|
+
}
|
|
4105
|
+
function form(ask, h = {}) {
|
|
4106
|
+
const must = required(ask.input);
|
|
4107
|
+
const own = mine(ask);
|
|
4108
|
+
const fields = props(ask.input).filter(([name]) => !(name in own)).map(([name, p]) => field(name, p, must.has(name))).join("");
|
|
4109
|
+
const label = `${h.icon ? `${escape(h.icon)} ` : ""}${escape(h.title ?? ask.name)}`;
|
|
4110
|
+
const care = h.destructive ? ' data-confirm="true"' : "";
|
|
4111
|
+
const said2 = ask.description ? `<p class="nv-hint">${escape(ask.description)}</p>` : "";
|
|
4112
|
+
return `<form class="nv-stack" data-ask="${escape(ask.name)}"${care}><fieldset><legend>${label}</legend>${said2}${fields}<div class="nv-row" data-align="center"><button class="nv-button"${h.destructive ? ' data-variant="danger"' : ""}>${label}</button></div></fieldset></form>`;
|
|
4113
|
+
}
|
|
4114
|
+
function values2(ask, raw) {
|
|
4115
|
+
const args = mine(ask);
|
|
4116
|
+
const must = required(ask.input);
|
|
4117
|
+
for (const [name, p] of props(ask.input)) {
|
|
4118
|
+
if (name in args) continue;
|
|
4119
|
+
const s = raw[name];
|
|
4120
|
+
if (p.type === "boolean") {
|
|
4121
|
+
if (s === "true") args[name] = true;
|
|
4122
|
+
else if (must.has(name)) args[name] = false;
|
|
4123
|
+
continue;
|
|
4124
|
+
}
|
|
4125
|
+
if (s === void 0 || s === "" && !must.has(name)) continue;
|
|
4126
|
+
if (p.type === "number" || p.type === "integer") {
|
|
4127
|
+
const n = Number(s);
|
|
4128
|
+
if (s === "" || Number.isNaN(n)) return { error: `${name} is a number` };
|
|
4129
|
+
args[name] = n;
|
|
4130
|
+
} else if (p.type === "string" || p.type === void 0 || Array.isArray(p.enum)) {
|
|
4131
|
+
args[name] = s;
|
|
4132
|
+
} else {
|
|
4133
|
+
try {
|
|
4134
|
+
args[name] = JSON.parse(s);
|
|
4135
|
+
} catch {
|
|
4136
|
+
return { error: `${name} is not JSON` };
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
return { args };
|
|
4141
|
+
}
|
|
4142
|
+
function view(value, schema) {
|
|
4143
|
+
if (value === null) return "<i>nothing</i>";
|
|
4144
|
+
if (typeof value !== "object") return escape(String(value));
|
|
4145
|
+
if (Array.isArray(value)) {
|
|
4146
|
+
if (value.length && value.every((v) => v !== null && typeof v === "object" && !Array.isArray(v))) {
|
|
4147
|
+
const rows = value;
|
|
4148
|
+
const keys2 = [...new Set(rows.flatMap((r) => Object.keys(r)))];
|
|
4149
|
+
const head = keys2.map((k) => `<th>${escape(k)}</th>`).join("");
|
|
4150
|
+
const body = rows.map((r) => `<tr>${keys2.map((k) => `<td>${k in r ? view(r[k]) : ""}</td>`).join("")}</tr>`).join("");
|
|
4151
|
+
return `<div class="nv-table-scroll"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
|
|
4152
|
+
}
|
|
4153
|
+
return value.length ? `<ul class="nv-list" role="list">${value.map((v) => `<li class="nv-item">${view(v)}</li>`).join("")}</ul>` : '<p class="nv-empty">none</p>';
|
|
4154
|
+
}
|
|
4155
|
+
const declared = props(schema).map(([k]) => k);
|
|
4156
|
+
const keys = [...declared.filter((k) => k in value), ...Object.keys(value).filter((k) => !declared.includes(k))];
|
|
4157
|
+
if (!keys.length) return "<i>empty</i>";
|
|
4158
|
+
return `<div class="nv-table-scroll"><table>${keys.map((k) => `<tr><th>${escape(k)}</th><td>${view(value[k], props(schema).find(([n]) => n === k)?.[1])}</td></tr>`).join("")}</table></div>`;
|
|
4159
|
+
}
|
|
4160
|
+
function face(w, schema) {
|
|
4161
|
+
if (w.word === "object") return `<div class="nv-answer" data-word="object">${view(w.value, schema)}</div>`;
|
|
4162
|
+
if (w.word === "error") return `<div class="nv-answer" data-word="error" data-tone="danger">${view(w.value)}</div>`;
|
|
4163
|
+
if (w.word === "silence") return `<div class="nv-answer" data-word="silence" data-tone="warning">${escape(SILENCE_TEXT)}</div>`;
|
|
4164
|
+
if (w.word === "quo") return `<div class="nv-answer" data-word="quo" data-tone="warning">${escape(wordText(w.name))}</div>`;
|
|
4165
|
+
return `<div class="nv-answer" data-word="unreached" data-tone="warning">${escape(UNREACHED_TEXT)}</div>`;
|
|
4166
|
+
}
|
|
4167
|
+
var person = (v) => {
|
|
4168
|
+
if (v === null || typeof v !== "object" || Array.isArray(v) || typeof v.name !== "string") return view(v);
|
|
4169
|
+
const name = v.name;
|
|
4170
|
+
const line = typeof v.line === "string" ? `<p class="nv-t-quiet">${escape(v.line)}</p>` : "";
|
|
4171
|
+
const pic = typeof v.picture === "string" && /^data:image\//.test(v.picture) ? `<img alt="" src="${v.picture}">` : escape(name.trim().charAt(0).toUpperCase());
|
|
4172
|
+
return `<div class="nv-person"><span class="nv-avatar">${pic}</span><div class="nv-stack"><p class="nv-t-body">${escape(name)}</p>${line}</div></div>`;
|
|
4173
|
+
};
|
|
4174
|
+
function people(value) {
|
|
4175
|
+
if (!Array.isArray(value)) return view(value);
|
|
4176
|
+
if (!value.length) return '<p class="nv-empty">nobody yet</p>';
|
|
4177
|
+
return `<div class="nv-list">${value.map((v) => `<div class="nv-item">${person(v)}</div>`).join("")}</div>`;
|
|
4178
|
+
}
|
|
4179
|
+
var happened = (v) => {
|
|
4180
|
+
if (v === null || typeof v !== "object" || Array.isArray(v) || typeof v.text !== "string") return view(v);
|
|
4181
|
+
const at3 = typeof v.at === "string" || typeof v.at === "number" ? `<p class="nv-t-label">${escape(String(v.at))}</p>` : "";
|
|
4182
|
+
return `<div class="nv-stack">${at3}<p class="nv-t-body">${escape(v.text)}</p></div>`;
|
|
4183
|
+
};
|
|
4184
|
+
function feed(value) {
|
|
4185
|
+
if (!Array.isArray(value)) return view(value);
|
|
4186
|
+
if (!value.length) return '<p class="nv-empty">nothing yet</p>';
|
|
4187
|
+
return `<div class="nv-feed">${value.map(happened).join("")}</div>`;
|
|
4188
|
+
}
|
|
4189
|
+
var ENDED = /* @__PURE__ */ new Set(["removed", "dropped", "not joined"]);
|
|
4190
|
+
var AWAY = /* @__PURE__ */ new Set(["unreached", "silence", "late", "absent"]);
|
|
4191
|
+
function standing(error) {
|
|
4192
|
+
return ENDED.has(error) ? "ended" : AWAY.has(error) ? "away" : "error";
|
|
4193
|
+
}
|
|
4194
|
+
function stands(error) {
|
|
4195
|
+
switch (standing(error)) {
|
|
4196
|
+
case "ended":
|
|
4197
|
+
return error === "not joined" ? "you are not in this world" : "this world ended it: you are out, and nothing here reaches it";
|
|
4198
|
+
case "away":
|
|
4199
|
+
return `this world cannot be reached right now, and nothing was delivered (${error})`;
|
|
4200
|
+
default:
|
|
4201
|
+
return `not in: ${error}`;
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
function refused(answer) {
|
|
4205
|
+
const w = word2(answer);
|
|
4206
|
+
if (w.word === "silence") return SILENCE_TEXT;
|
|
4207
|
+
if (w.word === "unreached") return UNREACHED_TEXT;
|
|
4208
|
+
if (w.word === "quo") return wordText(w.name);
|
|
4209
|
+
const said2 = w.word === "error" && typeof w.value.error === "string" ? w.value.error : "";
|
|
4210
|
+
switch (said2) {
|
|
4211
|
+
case "id taken":
|
|
4212
|
+
return "a device of that name is already in this world. If it is one you have lost, take it out of the world from a device you still hold, or come back under another name.";
|
|
4213
|
+
case "no such user":
|
|
4214
|
+
return "this world knows nobody by that way in.";
|
|
4215
|
+
case "refused":
|
|
4216
|
+
return "that way in was refused: it may be spent, or it may never have been one.";
|
|
4217
|
+
default:
|
|
4218
|
+
return said2 ? `that way in was refused: ${said2}` : "that way in was refused.";
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
function title(bp, l = {}) {
|
|
4222
|
+
if (l.name) return l.name;
|
|
4223
|
+
const n = bp?.notes;
|
|
4224
|
+
return n !== null && typeof n === "object" && !Array.isArray(n) && typeof n.name === "string" && n.name ? n.name : "quo";
|
|
4225
|
+
}
|
|
4226
|
+
function look(l) {
|
|
4227
|
+
if (!l) return { style: "", head: "" };
|
|
4228
|
+
const vars = [];
|
|
4229
|
+
if (l.accent) vars.push(`--nv-color-accent:${l.accent}`, `--nv-color-accent-hover:${l.accent}`, `--nv-color-link:${l.accent}`);
|
|
4230
|
+
if (l.background) vars.push(`--nv-color-bg-raised:${l.background}`);
|
|
4231
|
+
if (l.foreground) vars.push(`--nv-color-fg:${l.foreground}`, `--nv-color-fg-body:${l.foreground}`);
|
|
4232
|
+
if (l.font) vars.push(`--nv-font-sans:${l.font}`, `--nv-font-display:${l.font}`);
|
|
4233
|
+
if (l.radius !== void 0) vars.push(`--nv-radius-lg:${l.radius}px`, `--nv-radius-xl:${l.radius}px`);
|
|
4234
|
+
const logo = l.logo ? `<img class="logo" alt="" src="${l.logo}">` : "";
|
|
4235
|
+
const name = l.name ? escape(l.name) : "";
|
|
4236
|
+
return { style: vars.length ? ` style="${vars.join(";")}"` : "", head: logo || name ? `<h2>${logo}${name}</h2>` : "" };
|
|
4237
|
+
}
|
|
4238
|
+
function ordered(all, l) {
|
|
4239
|
+
const want = l?.order ?? [];
|
|
4240
|
+
return [...want.map((n) => all.find((a) => a.name === n)).filter((a) => a !== void 0), ...all.filter((a) => !want.includes(a.name))];
|
|
4241
|
+
}
|
|
4242
|
+
function page(m) {
|
|
4243
|
+
const bp = m.blueprint;
|
|
4244
|
+
const pres = presentation(bp);
|
|
4245
|
+
const shows = (w, schema) => {
|
|
4246
|
+
if (w.word !== "object" || m.at === void 0 || !isInvitation(w.value)) return face(w, schema);
|
|
4247
|
+
const url = link(m.at, w.value);
|
|
4248
|
+
return `<div class="nv-answer" data-word="object" data-way><a class="nv-key" href="${escape(url)}">${escape(url)}</a></div>`;
|
|
4249
|
+
};
|
|
4250
|
+
const one = (l, prefix = "") => (a) => {
|
|
4251
|
+
const w = m.answers[a.name];
|
|
4252
|
+
const h = hint(l, a.name.startsWith(prefix) ? a.name.slice(prefix.length) : a.name);
|
|
4253
|
+
return `<section class="nv-stack">${form(a, h)}${w ? shows(w, a.output) : ""}</section>`;
|
|
4254
|
+
};
|
|
4255
|
+
const answer = (name, as) => {
|
|
4256
|
+
const a = bp?.asks.find((x) => x.name === name);
|
|
4257
|
+
const w = a ? m.answers[name] : void 0;
|
|
4258
|
+
if (!a || !w) return "";
|
|
4259
|
+
if (w.word === "object" && as === "text") return `<div class="nv-answer" data-word="object">${view(w.value === null || typeof w.value !== "object" ? w.value : JSON.stringify(w.value))}</div>`;
|
|
4260
|
+
if (w.word === "object" && as === "cards" && Array.isArray(w.value)) return `<div class="nv-cards">${w.value.map((v) => `<div class="nv-card">${view(v)}</div>`).join("")}</div>`;
|
|
4261
|
+
if (w.word === "object" && as === "list" && Array.isArray(w.value)) return `<ul class="nv-list" role="list">${w.value.map((v) => `<li class="nv-item">${view(v)}</li>`).join("")}</ul>`;
|
|
4262
|
+
if (w.word === "object" && as === "people") return people(w.value);
|
|
4263
|
+
if (w.word === "object" && as === "feed") return feed(w.value);
|
|
4264
|
+
return shows(w, a.output);
|
|
4265
|
+
};
|
|
4266
|
+
const originOf = (s) => {
|
|
4267
|
+
if (s === void 0) return null;
|
|
4268
|
+
try {
|
|
4269
|
+
return new URL(s).origin;
|
|
4270
|
+
} catch {
|
|
4271
|
+
return null;
|
|
4272
|
+
}
|
|
4273
|
+
};
|
|
4274
|
+
const here = originOf(m.at);
|
|
4275
|
+
const linkedBy = (l) => (href, text3) => {
|
|
4276
|
+
const origin = originOf(href);
|
|
4277
|
+
if (origin === null) return escape(href);
|
|
4278
|
+
const vouched = here !== null && origin === here || (l?.stands?.includes(origin) ?? false);
|
|
4279
|
+
if (!vouched) return `<span class="nv-key link away" title="${escape(href)}">${escape(href)}</span>`;
|
|
4280
|
+
const out = origin === here ? "" : " out";
|
|
4281
|
+
return `<a class="link${out}" href="${escape(href)}" rel="noopener noreferrer" data-origin="${escape(origin)}">${escape(text3)}</a>`;
|
|
4282
|
+
};
|
|
4283
|
+
const groups2 = groups(bp);
|
|
4284
|
+
const taken = new Set(Object.values(groups2).flatMap((g) => g.asks ?? []));
|
|
4285
|
+
const section = (id, as) => {
|
|
4286
|
+
const g = groups2[id];
|
|
4287
|
+
const inGroup = bp && g ? bp.asks.filter((a) => g.asks?.includes(a.name)) : [];
|
|
4288
|
+
if (!inGroup.length) return "";
|
|
4289
|
+
const kept2 = sanitise(g.look);
|
|
4290
|
+
const l = look(kept2);
|
|
4291
|
+
const prefix = `${id}-`;
|
|
4292
|
+
const tree = m.pages[id];
|
|
4293
|
+
const own = m.at !== void 0 && m.focus !== id ? `<p class="nv-t-quiet"><a class="open" href="${escape(`${m.at}/${encodeURIComponent(id)}`)}">open</a></p>` : "";
|
|
4294
|
+
const whole2 = m.focus === id ? " data-whole" : "";
|
|
4295
|
+
if (as === "door" && m.focus !== id) {
|
|
4296
|
+
const name = kept2.name ?? id;
|
|
4297
|
+
const count = inGroup.length;
|
|
4298
|
+
return `<section class="nv-standing" data-standing="${escape(id)}" data-door${l.style}><div class="nv-person"><span class="nv-avatar">${escape(name.charAt(0).toUpperCase())}</span><div class="nv-stack"><p class="nv-t-body">${escape(name)}</p><p class="nv-t-quiet">${count} ${count === 1 ? "thing" : "things"} to do here, on its own page</p></div></div>${own}</section>`;
|
|
4299
|
+
}
|
|
4300
|
+
if (tree) {
|
|
4301
|
+
const hers = {
|
|
4302
|
+
form: (name) => {
|
|
4303
|
+
const a = inGroup.find((x) => x.name === prefix + name);
|
|
4304
|
+
return a ? one(kept2, prefix)(a) : "";
|
|
4305
|
+
},
|
|
4306
|
+
answer: (name, as2) => inGroup.some((x) => x.name === prefix + name) ? answer(prefix + name, as2) : "",
|
|
4307
|
+
link: linkedBy(kept2),
|
|
4308
|
+
// a carried standing vouches by her own look and never her carrier's
|
|
4309
|
+
standing: () => "",
|
|
4310
|
+
standings: () => ""
|
|
4311
|
+
};
|
|
4312
|
+
return `<section class="nv-standing" data-standing="${escape(id)}"${whole2}${l.style}>${paint(tree, hers)}${own}</section>`;
|
|
4313
|
+
}
|
|
4314
|
+
const want = (kept2.order ?? []).map((n) => prefix + n);
|
|
4315
|
+
const inOrder = ordered(inGroup, { order: want });
|
|
4316
|
+
return `<section class="nv-standing" data-standing="${escape(id)}"${whole2}${l.style}>${l.head || `<h2>${escape(id)}</h2>`}${inOrder.map(one(kept2, prefix)).join("")}${own}</section>`;
|
|
4317
|
+
};
|
|
4318
|
+
const far = Object.keys(groups2).map((id) => section(id)).join("");
|
|
4319
|
+
const painter = {
|
|
4320
|
+
form: (name) => {
|
|
4321
|
+
const a = bp?.asks.find((x) => x.name === name && !taken.has(x.name) && !pres.has(x.name));
|
|
4322
|
+
return a ? one(m.look)(a) : "";
|
|
4323
|
+
},
|
|
4324
|
+
answer: (name, as) => pres.has(name) ? "" : answer(name, as),
|
|
4325
|
+
link: linkedBy(m.look),
|
|
4326
|
+
standing: section,
|
|
4327
|
+
standings: () => far
|
|
4328
|
+
};
|
|
4329
|
+
const mineAsks = bp ? ordered(bp.asks.filter((a) => !taken.has(a.name) && !pres.has(a.name)), m.look).map(one(m.look)).join("") : "";
|
|
4330
|
+
const focused = m.focus !== void 0;
|
|
4331
|
+
const body = focused ? section(m.focus) : m.tree ? paint(m.tree, painter) : `<div class="nv-stack">${mineAsks}${far}</div>`;
|
|
4332
|
+
const shown = bp && bp.notes !== null && typeof bp.notes === "object" && !Array.isArray(bp.notes) ? Object.fromEntries(Object.entries(bp.notes).filter(([k]) => k !== "standings" && k !== "name")) : bp?.notes;
|
|
4333
|
+
const notes = bp && !m.tree && shown !== null && shown !== void 0 && !(typeof shown === "object" && !Array.isArray(shown) && !Object.keys(shown).length) ? `<aside class="nv-answer notes">${view(shown)}</aside>` : "";
|
|
4334
|
+
const pushes = m.pushes.length ? `<section class="nv-stack pushes"><p class="nv-t-label">pushes</p><div class="nv-feed">${m.pushes.map((p) => `<div>${view(p)}</div>`).join("")}</div></section>` : "";
|
|
4335
|
+
const styled = look(m.look);
|
|
4336
|
+
const back = focused && m.at !== void 0 ? `<a class="back" href="${escape(m.at)}">${escape(title(bp, m.look))}</a>` : "";
|
|
4337
|
+
const said2 = focused ? m.pages[m.focus] ? titled(m.pages[m.focus]) : false : titled(m.tree);
|
|
4338
|
+
const head = focused ? back : m.tree && said2 ? "" : `${m.look.logo ? `<img class="logo" alt="" src="${m.look.logo}">` : ""}<h1>${escape(title(bp, m.look))}</h1>`;
|
|
4339
|
+
const tone = m.standing === "ended" || m.standing === "error" ? "danger" : m.standing === "away" ? "warning" : "success";
|
|
4340
|
+
return `<header class="nv-bar"${styled.style}>${head}<p class="nv-chip notice" data-standing="${m.standing}" data-tone="${tone}">${escape(m.notice)}</p></header>${focused ? "" : notes}<main data-page${focused ? ' class="focus"' : ""}${styled.style}>${body}</main>${pushes}`;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
// human/screen.ts
|
|
4344
|
+
async function screenSide(avatar, surface, options = {}) {
|
|
4345
|
+
const after = options.after ?? (async () => {
|
|
4346
|
+
});
|
|
4347
|
+
const notice = options.notice ?? "";
|
|
4348
|
+
const model = { blueprint: null, look: {}, tree: null, pages: {}, notice, standing: "in", answers: {}, pushes: [], ...options.focus !== void 0 ? { focus: options.focus } : {}, ...options.at !== void 0 ? { at: options.at } : {} };
|
|
4349
|
+
let seen2 = null;
|
|
4350
|
+
const show = () => surface.show(page(model));
|
|
4351
|
+
const refresh = async () => {
|
|
4352
|
+
const bp = await avatar.tools();
|
|
4353
|
+
if ("error" in bp && !("asks" in bp)) {
|
|
4354
|
+
model.standing = standing(bp.error);
|
|
4355
|
+
model.notice = stands(bp.error);
|
|
4356
|
+
if (model.standing === "ended") await options.ended?.();
|
|
4357
|
+
} else {
|
|
4358
|
+
model.standing = "in";
|
|
4359
|
+
const d = await digest(bp);
|
|
4360
|
+
const moved = d !== seen2;
|
|
4361
|
+
if (moved) {
|
|
4362
|
+
seen2 = d;
|
|
4363
|
+
model.blueprint = bp;
|
|
4364
|
+
for (const k of Object.keys(model.answers)) if (!bp.asks.some((a) => a.name === k)) delete model.answers[k];
|
|
4365
|
+
if (bp.asks.some((a) => a.name === "look")) {
|
|
4366
|
+
const l = await avatar.call("look");
|
|
4367
|
+
model.look = isSilence(l) || isWord(l) ? {} : sanitise(l);
|
|
4368
|
+
} else model.look = {};
|
|
4369
|
+
}
|
|
4370
|
+
const trees = () => [model.tree ? answersIn(model.tree) : [], ...Object.entries(model.pages).map(([id, t]) => t ? answersIn(t).map((n) => `${id}-${n}`) : [])].flat();
|
|
4371
|
+
const shows = new Set(trees());
|
|
4372
|
+
const tree = async (name) => {
|
|
4373
|
+
const t = await avatar.call(name);
|
|
4374
|
+
return isSilence(t) || isWord(t) ? null : sanitiseTree(t);
|
|
4375
|
+
};
|
|
4376
|
+
model.tree = bp.asks.some((a) => a.name === "page") ? await tree("page") : null;
|
|
4377
|
+
model.pages = {};
|
|
4378
|
+
for (const [id, g] of Object.entries(groups(bp))) if (typeof g.page === "string" && bp.asks.some((a) => a.name === g.page)) model.pages[id] = await tree(g.page);
|
|
4379
|
+
const wanted = new Set(trees());
|
|
4380
|
+
for (const n of shows) if (wanted.has(n)) delete model.answers[n];
|
|
4381
|
+
for (const a of bp.asks) {
|
|
4382
|
+
const needs = asks(a).length > 0;
|
|
4383
|
+
const run = wanted.has(a.name) || moved && hintFor(bp, model.look, a.name).readOnly;
|
|
4384
|
+
if (run && !needs && !(a.name in model.answers)) model.answers[a.name] = word2(await avatar.call(a.name, mine(a)));
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
await after();
|
|
4388
|
+
show();
|
|
4389
|
+
};
|
|
4390
|
+
surface.onSubmit = (name, raw) => void submit(name, raw);
|
|
4391
|
+
const submit = async (name, raw) => {
|
|
4392
|
+
const ask = model.blueprint?.asks.find((a) => a.name === name);
|
|
4393
|
+
if (!ask) return;
|
|
4394
|
+
const v = values2(ask, raw);
|
|
4395
|
+
if ("error" in v) {
|
|
4396
|
+
model.answers[name] = { word: "error", value: { error: v.error } };
|
|
4397
|
+
return show();
|
|
4398
|
+
}
|
|
4399
|
+
const out = await avatar.call(name, v.args);
|
|
4400
|
+
await after();
|
|
4401
|
+
if (options.admit && isInvitation(out)) return options.admit(out);
|
|
4402
|
+
model.answers[name] = word2(out);
|
|
4403
|
+
await refresh();
|
|
4404
|
+
};
|
|
4405
|
+
const ear = (object) => {
|
|
4406
|
+
model.pushes.push(object);
|
|
4407
|
+
show();
|
|
4408
|
+
};
|
|
4409
|
+
avatar.ears.add(ear);
|
|
4410
|
+
await refresh();
|
|
4411
|
+
return {
|
|
4412
|
+
model,
|
|
4413
|
+
refresh,
|
|
4414
|
+
close: async () => {
|
|
4415
|
+
avatar.ears.delete(ear);
|
|
4416
|
+
surface.onSubmit = void 0;
|
|
4417
|
+
}
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
// human/dom.ts
|
|
4422
|
+
function domSurface(root2) {
|
|
4423
|
+
const s = { show: (html) => root2.innerHTML = html, onSubmit: void 0 };
|
|
4424
|
+
root2.addEventListener("submit", (ev) => {
|
|
4425
|
+
const f = ev.target;
|
|
4426
|
+
const ask = f?.dataset.ask;
|
|
4427
|
+
if (!f || ask === void 0) return;
|
|
4428
|
+
ev.preventDefault();
|
|
4429
|
+
const raw = {};
|
|
4430
|
+
for (const [k, v] of new FormData(f)) raw[k] = typeof v === "string" ? v : "";
|
|
4431
|
+
s.onSubmit?.(ask, raw);
|
|
4432
|
+
});
|
|
4433
|
+
return s;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
// human/guest.ts
|
|
4437
|
+
function guest(avatar, ward) {
|
|
4438
|
+
const at3 = { ward };
|
|
4439
|
+
return {
|
|
4440
|
+
tools: async () => {
|
|
4441
|
+
const bp = await avatar.knock(at3);
|
|
4442
|
+
if (isSilence(bp)) return { error: "silence" };
|
|
4443
|
+
if (isWord(bp)) return { error: wordOf(bp) };
|
|
4444
|
+
if (bp === null || typeof bp !== "object" || Array.isArray(bp) || !Array.isArray(bp.asks)) return { error: "nobody is home" };
|
|
4445
|
+
return bp;
|
|
4446
|
+
},
|
|
4447
|
+
call: (name, args = {}, wanted) => avatar.knock(at3, name, args, wanted),
|
|
4448
|
+
ears: /* @__PURE__ */ new Set()
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
// human/mark.ts
|
|
4453
|
+
var RING = '<circle cx="32" cy="32" r="22" stroke-dasharray="110 30" transform="rotate(-60 32 32)"/><circle cx="32" cy="32" r="5" fill="currentColor" stroke="none"/>';
|
|
4454
|
+
var MARK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round">${RING}</svg>`;
|
|
4455
|
+
var MARK = "data:image/svg+xml;base64," + btoa(MARK_SVG);
|
|
4456
|
+
|
|
4457
|
+
// human/door.ts
|
|
4458
|
+
function doorTree(at3) {
|
|
4459
|
+
return {
|
|
4460
|
+
kind: "stack",
|
|
4461
|
+
as: "hero",
|
|
4462
|
+
of: [
|
|
4463
|
+
{ kind: "image", src: MARK, alt: "" },
|
|
4464
|
+
{ kind: "text", text: "quo", role: "title" },
|
|
4465
|
+
{ kind: "text", text: at3.world ? `${at3.world} at ${at3.host}` : at3.host, role: "label" },
|
|
4466
|
+
{ kind: "text", text: at3.world ? "This world is entered by invitation." : "The worlds here are entered by invitation.", role: "lead" },
|
|
4467
|
+
{ kind: "text", text: "An invitation is a link someone sends you. Open it here, and you are in: no account, no password, nothing to type. What you can do inside is what the world shows you, and it is yours to keep on this device." },
|
|
4468
|
+
{ kind: "text", text: "Nothing on this page asks anything of you. If you were sent here without a link, ask the person who sent you for one.", role: "quiet" }
|
|
4469
|
+
]
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
function door(at3) {
|
|
4473
|
+
return `<main data-door>${paint(doorTree(at3), nobody)}</main>`;
|
|
4474
|
+
}
|
|
4475
|
+
function heldTree(at3) {
|
|
4476
|
+
return {
|
|
4477
|
+
kind: "stack",
|
|
4478
|
+
as: "hero",
|
|
4479
|
+
of: [
|
|
4480
|
+
{ kind: "image", src: MARK, alt: "" },
|
|
4481
|
+
{ kind: "text", text: "Already open", role: "title" },
|
|
4482
|
+
{ kind: "text", text: at3.world ? `${at3.world} at ${at3.host}` : at3.host, role: "label" },
|
|
4483
|
+
{ kind: "text", text: "This world is open in another tab of this browser, and one browser is one device here. That tab holds the device; this one is only a page.", role: "lead" },
|
|
4484
|
+
{ kind: "text", text: "Go back to the tab that has it, or close that tab and reload this one. Nothing is lost either way: what this device is in every world is kept in the browser, not in a tab." }
|
|
4485
|
+
]
|
|
4486
|
+
};
|
|
4487
|
+
}
|
|
4488
|
+
function held2(at3) {
|
|
4489
|
+
return `<main data-door>${paint(heldTree(at3), nobody)}</main>`;
|
|
4490
|
+
}
|
|
4491
|
+
var ours = {
|
|
4492
|
+
...nobody,
|
|
4493
|
+
link: (href, text3) => `<a class="link" href="${escape(href)}">${escape(text3)}</a>`
|
|
4494
|
+
};
|
|
4495
|
+
|
|
4496
|
+
// human/local.ts
|
|
4497
|
+
function local(ward, key) {
|
|
4498
|
+
const one = (method, args = {}) => ward.ask("ask", method === void 0 ? { being: key } : { being: key, method, args });
|
|
4499
|
+
return {
|
|
4500
|
+
tools: async () => await one(),
|
|
4501
|
+
call: (name, args = {}) => one(name, args),
|
|
4502
|
+
ears: /* @__PURE__ */ new Set()
|
|
4503
|
+
};
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
// human/tab.ts
|
|
4507
|
+
var kept = (key, fallback) => {
|
|
4508
|
+
try {
|
|
4509
|
+
return JSON.parse(localStorage.getItem(key) ?? "null") ?? fallback;
|
|
4510
|
+
} catch {
|
|
4511
|
+
return fallback;
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
var keep2 = (key, value) => {
|
|
4515
|
+
try {
|
|
4516
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
4517
|
+
} catch {
|
|
4518
|
+
}
|
|
4519
|
+
};
|
|
4520
|
+
var WORLDS = "quo-worlds";
|
|
4521
|
+
var worlds = () => kept(WORLDS, {});
|
|
4522
|
+
var remember = (pk, url, name) => keep2(WORLDS, { ...worlds(), [pk]: { url, name } });
|
|
4523
|
+
var AT = (pk) => `quo-at:${pk}`;
|
|
4524
|
+
var NAMES = (pk) => `quo-names:${pk}`;
|
|
4525
|
+
var el = (tag, text3 = "", attrs = {}) => {
|
|
4526
|
+
const e = document.createElement(tag);
|
|
4527
|
+
if (text3) e.textContent = text3;
|
|
4528
|
+
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
|
|
4529
|
+
return e;
|
|
4530
|
+
};
|
|
4531
|
+
async function start(cfg, root2 = document.body) {
|
|
4532
|
+
const linked = parse(location.hash);
|
|
4533
|
+
const proved = parseProof(location.hash);
|
|
4534
|
+
if (linked || proved) history.replaceState(null, "", location.pathname + location.search + strip(location.hash));
|
|
4535
|
+
const nav = el("nav", "", { class: "worlds nv-strip" });
|
|
4536
|
+
const who = el("nav", "", { class: "relations nv-row" });
|
|
4537
|
+
const status = el("p", "booting", { class: "state nv-t-quiet" });
|
|
4538
|
+
const screen = el("div");
|
|
4539
|
+
root2.append(nav, who, status, screen);
|
|
4540
|
+
const say = (s) => status.textContent = s;
|
|
4541
|
+
const harbor = new BrowserHarbor("quo");
|
|
4542
|
+
const classes = cfg.beings ? await import(`${cfg.web}/beings.js`).then((mod) => {
|
|
4543
|
+
const out = {};
|
|
4544
|
+
for (const [name, v] of Object.entries(mod)) if (typeof v === "function" && "prototype" in v) out[name] = v;
|
|
4545
|
+
return out;
|
|
4546
|
+
}) : {};
|
|
4547
|
+
Object.assign(harbor.classes, classes);
|
|
4548
|
+
try {
|
|
4549
|
+
await harbor.boot();
|
|
4550
|
+
} catch (e) {
|
|
4551
|
+
status.remove();
|
|
4552
|
+
screen.innerHTML = held2({ world: cfg.ward, host: new URL(cfg.web).host });
|
|
4553
|
+
throw e;
|
|
4554
|
+
}
|
|
4555
|
+
harbor.dial(cfg.quo);
|
|
4556
|
+
const pks = Object.values(cfg.wards).map((w) => w.pk);
|
|
4557
|
+
let bell = cfg.bell ? await subscribe(cfg.web, cfg.bell) : null;
|
|
4558
|
+
globalThis.navigator?.serviceWorker?.addEventListener("message", (e) => {
|
|
4559
|
+
if (e.data?.quo !== "wake") return;
|
|
4560
|
+
const port = e.ports[0];
|
|
4561
|
+
void (async () => {
|
|
4562
|
+
harbor.wake();
|
|
4563
|
+
port?.postMessage({ missed: await drained(harbor, pks) });
|
|
4564
|
+
})();
|
|
4565
|
+
});
|
|
4566
|
+
const nameOf = (pk2) => Object.keys(cfg.wards).find((n) => cfg.wards[n].pk === pk2);
|
|
4567
|
+
const pk = linked?.ward ?? (cfg.ward ? cfg.wards[cfg.ward]?.pk : void 0);
|
|
4568
|
+
const wardName = pk ? nameOf(pk) : void 0;
|
|
4569
|
+
const pageOf = (name) => `${new URL(cfg.web).pathname.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
|
|
4570
|
+
const addressOf = (name) => location.origin + pageOf(name);
|
|
4571
|
+
const switcher = () => {
|
|
4572
|
+
nav.replaceChildren();
|
|
4573
|
+
const known = worlds();
|
|
4574
|
+
for (const [k, w] of Object.entries(known)) {
|
|
4575
|
+
const a = el("a", w.name || k.slice(0, 8), { href: w.url, class: "nv-chip" });
|
|
4576
|
+
if (k === pk) a.setAttribute("aria-current", "page");
|
|
4577
|
+
nav.append(a);
|
|
4578
|
+
}
|
|
4579
|
+
if (!nav.childElementCount) nav.remove();
|
|
4580
|
+
else if (!nav.isConnected) root2.insertBefore(nav, root2.firstChild);
|
|
4581
|
+
};
|
|
4582
|
+
switcher();
|
|
4583
|
+
if (!pk || !wardName) {
|
|
4584
|
+
say(linked ? "this link is for a world that does not live here" : "choose a world");
|
|
4585
|
+
return;
|
|
4586
|
+
}
|
|
4587
|
+
if (linked && location.pathname !== pageOf(wardName)) history.replaceState(null, "", pageOf(wardName) + location.search + location.hash);
|
|
4588
|
+
const ward = await world(harbor, pk);
|
|
4589
|
+
const at3 = { name: wardName, pk, public: cfg.wards[wardName].public };
|
|
4590
|
+
let side = null;
|
|
4591
|
+
let current = null;
|
|
4592
|
+
const names = () => kept(NAMES(pk), {});
|
|
4593
|
+
const people2 = () => {
|
|
4594
|
+
who.replaceChildren();
|
|
4595
|
+
const rels2 = relations(ward);
|
|
4596
|
+
const called = names();
|
|
4597
|
+
for (const r of rels2) {
|
|
4598
|
+
const b = el("button", called[r.key] ?? r.key, { type: "button", class: "nv-chip" });
|
|
4599
|
+
if (r.key === current?.key) b.setAttribute("aria-current", "true");
|
|
4600
|
+
b.onclick = () => void inside(r, `in, as ${called[r.key] ?? r.key}`, called[r.key] ?? r.key);
|
|
4601
|
+
who.append(b);
|
|
4602
|
+
}
|
|
4603
|
+
if (rels2.length) {
|
|
4604
|
+
const more = el("button", "another way in", { type: "button", class: "another nv-chip" });
|
|
4605
|
+
more.onclick = () => void atDoor();
|
|
4606
|
+
who.append(more);
|
|
4607
|
+
}
|
|
4608
|
+
moving.hidden = rels2.length === 0;
|
|
4609
|
+
};
|
|
4610
|
+
const locals = [];
|
|
4611
|
+
const boot = async (rel) => {
|
|
4612
|
+
for (const name of Object.keys(classes)) {
|
|
4613
|
+
const key = `${rel.key}-${name}`;
|
|
4614
|
+
if (!ward.being(key)) {
|
|
4615
|
+
const out = await ward.ask("boot", { key, class: name });
|
|
4616
|
+
if (out.error) continue;
|
|
4617
|
+
await ward.save();
|
|
4618
|
+
}
|
|
4619
|
+
if (!ward.being(key)) continue;
|
|
4620
|
+
const section = el("section", "", { class: "local", "data-being": key });
|
|
4621
|
+
screen.append(section);
|
|
4622
|
+
locals.push(await screenSide(local(ward, key), domSurface(section), { at: addressOf(at3.name) }));
|
|
4623
|
+
}
|
|
4624
|
+
};
|
|
4625
|
+
const inside = async (rel, notice, called) => {
|
|
4626
|
+
await side?.close();
|
|
4627
|
+
for (const l of locals.splice(0)) await l.close();
|
|
4628
|
+
status.remove();
|
|
4629
|
+
root2.querySelector("form.password")?.remove();
|
|
4630
|
+
screen.replaceChildren();
|
|
4631
|
+
current = rel;
|
|
4632
|
+
keep2(AT(pk), rel.key);
|
|
4633
|
+
const mine2 = el("div", "", { class: "far" });
|
|
4634
|
+
screen.append(mine2);
|
|
4635
|
+
const s = await screenSide(rel.avatar, domSurface(mine2), {
|
|
4636
|
+
after: () => ward.save(),
|
|
4637
|
+
notice,
|
|
4638
|
+
at: addressOf(at3.name),
|
|
4639
|
+
// A world that ended the relation is over, and the one act left is
|
|
4640
|
+
// leaving. The tab says so and puts the way out where she is looking;
|
|
4641
|
+
// it drops nothing, since one answer at one moment is never enough to
|
|
4642
|
+
// delete a ward and the far world can never be asked to confirm it.
|
|
4643
|
+
ended: async () => {
|
|
4644
|
+
leave.textContent = "leave this world";
|
|
4645
|
+
leave.classList.add("only");
|
|
4646
|
+
moving.scrollIntoView({ block: "nearest" });
|
|
4647
|
+
},
|
|
4648
|
+
...cfg.being ? { focus: cfg.being } : {}
|
|
4649
|
+
});
|
|
4650
|
+
side = s;
|
|
4651
|
+
const bp = s.model.blueprint;
|
|
4652
|
+
const notesName = typeof bp?.notes?.name === "string" ? bp.notes.name : "";
|
|
4653
|
+
remember(pk, addressOf(at3.name), s.model.look.name || notesName || at3.name);
|
|
4654
|
+
if (!names()[rel.key]) keep2(NAMES(pk), { ...names(), [rel.key]: called });
|
|
4655
|
+
if (bell) await rel.avatar.call("bell", { token: bell }).then(() => ward.save());
|
|
4656
|
+
await drained(harbor, [pk]);
|
|
4657
|
+
switcher();
|
|
4658
|
+
people2();
|
|
4659
|
+
if (!cfg.being) await boot(rel);
|
|
4660
|
+
};
|
|
4661
|
+
const admit = async (inv, notice, called) => {
|
|
4662
|
+
say("knocking");
|
|
4663
|
+
const rel = await fresh2(ward);
|
|
4664
|
+
const got = await rel.avatar.join(inv);
|
|
4665
|
+
await ward.save();
|
|
4666
|
+
if (!("asks" in got)) {
|
|
4667
|
+
say(`not in: ${got.error}`);
|
|
4668
|
+
return false;
|
|
4669
|
+
}
|
|
4670
|
+
await inside(rel, notice, called);
|
|
4671
|
+
return true;
|
|
4672
|
+
};
|
|
4673
|
+
const atDoor = async () => {
|
|
4674
|
+
await side?.close();
|
|
4675
|
+
for (const l of locals.splice(0)) await l.close();
|
|
4676
|
+
side = null;
|
|
4677
|
+
current = null;
|
|
4678
|
+
screen.replaceChildren();
|
|
4679
|
+
if (!status.isConnected) root2.insertBefore(status, screen);
|
|
4680
|
+
people2();
|
|
4681
|
+
if (!at3.public) {
|
|
4682
|
+
status.remove();
|
|
4683
|
+
screen.innerHTML = door({ world: at3.name, host: new URL(cfg.web).host });
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
say(`at the door of ${at3.name}`);
|
|
4687
|
+
const rel = await fresh2(ward);
|
|
4688
|
+
const gate = guest(rel.avatar, pk);
|
|
4689
|
+
const s = await screenSide(gate, domSurface(screen), {
|
|
4690
|
+
after: () => ward.save(),
|
|
4691
|
+
notice: `a guest at ${at3.name}: what she shows strangers`,
|
|
4692
|
+
at: addressOf(at3.name),
|
|
4693
|
+
admit: async (inv) => {
|
|
4694
|
+
await s.close();
|
|
4695
|
+
await admit(inv, `in, as a guest of ${at3.name}`, "guest");
|
|
4696
|
+
}
|
|
4697
|
+
});
|
|
4698
|
+
side = s;
|
|
4699
|
+
status.remove();
|
|
4700
|
+
};
|
|
4701
|
+
const forget = async () => {
|
|
4702
|
+
await side?.close();
|
|
4703
|
+
for (const l of locals.splice(0)) await l.close();
|
|
4704
|
+
side = null;
|
|
4705
|
+
keep2(
|
|
4706
|
+
WORLDS,
|
|
4707
|
+
Object.fromEntries(Object.entries(worlds()).filter(([k]) => k !== pk))
|
|
4708
|
+
);
|
|
4709
|
+
localStorage.removeItem(AT(pk));
|
|
4710
|
+
localStorage.removeItem(NAMES(pk));
|
|
4711
|
+
};
|
|
4712
|
+
const leave = el("button", "leave this world", { type: "button", class: "leave nv-button", "data-variant": "danger" });
|
|
4713
|
+
leave.onclick = () => void (async () => {
|
|
4714
|
+
await forget();
|
|
4715
|
+
await harbor.drop(localName(pk));
|
|
4716
|
+
location.reload();
|
|
4717
|
+
})();
|
|
4718
|
+
const moving = el("div", "", { class: "move" });
|
|
4719
|
+
const offer = el("p", "", { class: "invitation" });
|
|
4720
|
+
const said2 = el("p", "", { class: "quiet" });
|
|
4721
|
+
const give = el("button", "move this world to another device", { type: "button", class: "nv-button", "data-variant": "quiet" });
|
|
4722
|
+
const doorbell = el("button", "let this world wake me", { type: "button", class: "doorbell nv-button", "data-variant": "quiet" });
|
|
4723
|
+
doorbell.hidden = !cfg.bell || bell !== null;
|
|
4724
|
+
doorbell.onclick = () => void (async () => {
|
|
4725
|
+
bell = cfg.bell ? await subscribe(cfg.web, cfg.bell, true) : null;
|
|
4726
|
+
doorbell.hidden = bell !== null;
|
|
4727
|
+
if (bell && current) await current.avatar.call("bell", { token: bell }).then(() => ward.save());
|
|
4728
|
+
})();
|
|
4729
|
+
moving.append(give, leave, offer, said2, doorbell);
|
|
4730
|
+
let giving = null;
|
|
4731
|
+
const stopGiving = async () => {
|
|
4732
|
+
const g = giving;
|
|
4733
|
+
giving = null;
|
|
4734
|
+
offer.textContent = "";
|
|
4735
|
+
give.textContent = "move this world to another device";
|
|
4736
|
+
await g?.close();
|
|
4737
|
+
};
|
|
4738
|
+
give.onclick = () => void (async () => {
|
|
4739
|
+
if (giving) return stopGiving();
|
|
4740
|
+
said2.textContent = "";
|
|
4741
|
+
giving = await giveWorld(harbor, { pk, at: addressOf(at3.name), name: at3.name }, cfg.quo);
|
|
4742
|
+
offer.textContent = giving.offer;
|
|
4743
|
+
give.textContent = "stop offering it";
|
|
4744
|
+
void giving.moved.then(async () => {
|
|
4745
|
+
await forget();
|
|
4746
|
+
location.reload();
|
|
4747
|
+
});
|
|
4748
|
+
})().catch((e) => said2.textContent = String(e));
|
|
4749
|
+
root2.append(moving);
|
|
4750
|
+
if (proved && at3.public) {
|
|
4751
|
+
say("at the door");
|
|
4752
|
+
const rel = await fresh2(ward);
|
|
4753
|
+
const got = await rel.avatar.knock({ ward: pk }, "device", { proof: { kind: proved.kind, token: proved.token } });
|
|
4754
|
+
await ward.save();
|
|
4755
|
+
if (isInvitation(got) && await admit(got, "in, by the link", "link")) return;
|
|
4756
|
+
say(refused(got));
|
|
4757
|
+
}
|
|
4758
|
+
if (linked && await admit(linked, "in, by the link", "link")) return;
|
|
4759
|
+
const rels = relations(ward);
|
|
4760
|
+
const last = kept(AT(pk), null);
|
|
4761
|
+
const back = rels.find((r) => r.key === last) ?? rels[rels.length - 1];
|
|
4762
|
+
if (back) {
|
|
4763
|
+
const had = await back.avatar.tools();
|
|
4764
|
+
await ward.save();
|
|
4765
|
+
if ("asks" in had) return inside(back, linked ? `not in by the link; in, as before` : "in, as before", names()[back.key] ?? back.key);
|
|
4766
|
+
say(`not in (${had.error})`);
|
|
4767
|
+
}
|
|
4768
|
+
await atDoor();
|
|
4769
|
+
}
|
|
4770
|
+
var config = document.getElementById("quo");
|
|
4771
|
+
if (config?.textContent) void start(JSON.parse(config.textContent));
|
|
4772
|
+
export {
|
|
4773
|
+
USER,
|
|
4774
|
+
start
|
|
4775
|
+
};
|