@weaveprotocol/core 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -8
- package/dist/doors/doors.d.ts +139 -0
- package/dist/doors/doors.d.ts.map +1 -0
- package/dist/doors/doors.js +210 -0
- package/dist/doors/doors.js.map +1 -0
- package/dist/doors/index.d.ts +9 -0
- package/dist/doors/index.d.ts.map +1 -0
- package/dist/doors/index.js +7 -0
- package/dist/doors/index.js.map +1 -0
- package/dist/identity/contact-key.d.ts +26 -0
- package/dist/identity/contact-key.d.ts.map +1 -1
- package/dist/identity/contact-key.js +52 -0
- package/dist/identity/contact-key.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/network/index.d.ts +2 -0
- package/dist/network/index.d.ts.map +1 -1
- package/dist/network/index.js +1 -0
- package/dist/network/index.js.map +1 -1
- package/dist/network/mailbox.d.ts +49 -0
- package/dist/network/mailbox.d.ts.map +1 -0
- package/dist/network/mailbox.js +125 -0
- package/dist/network/mailbox.js.map +1 -0
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +320 -2
- package/dist/node/node.js.map +1 -1
- package/dist/node/types.d.ts +107 -1
- package/dist/node/types.d.ts.map +1 -1
- package/dist/schemas/contacts.d.ts +137 -0
- package/dist/schemas/contacts.d.ts.map +1 -1
- package/dist/schemas/contacts.js +63 -0
- package/dist/schemas/contacts.js.map +1 -1
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/session/auth.d.ts.map +1 -1
- package/dist/session/auth.js +2 -1
- package/dist/session/auth.js.map +1 -1
- package/dist/session/hosting.d.ts +1 -1
- package/dist/space/notify.d.ts +1 -1
- package/package.json +6 -2
package/dist/node/node.js
CHANGED
|
@@ -30,8 +30,10 @@ import { DEFAULT_ICE_SERVERS } from '../network/rtc-transport.js';
|
|
|
30
30
|
import { deriveInviteKey } from '../space/space-access.js';
|
|
31
31
|
import { base64UrlDecode, base64UrlEncode } from '../utils/encoding.js';
|
|
32
32
|
import { onePerKey } from '../records/rules.js';
|
|
33
|
-
import { contactKeyPair, deriveMemberKeyBytes, openSealed, sealFor } from '../identity/contact-key.js';
|
|
34
|
-
import { contact as contactSchema, contactRequest as contactRequestSchema } from '../schemas/contacts.js';
|
|
33
|
+
import { contactKeyPair, contactPublicKey, deriveDoorKeyBytes, deriveDoorSignKeyBytes, deriveMemberKeyBytes, openSealed, sealFor } from '../identity/contact-key.js';
|
|
34
|
+
import { contact as contactSchema, contactRequest as contactRequestSchema, door as doorSchema, knock as knockSchema, knockAnswer as knockAnswerSchema, } from '../schemas/contacts.js';
|
|
35
|
+
import { checkAnswer, clip, doorTopic, encodeDoorCode, KNOCK_TTL_SECONDS, knockId, MAX_DOOR_RELAYS, openKnock, parseDoorCode, sealKnock, signAnswer, signPurge, } from '../doors/doors.js';
|
|
36
|
+
import { createMailboxClient } from '../network/mailbox.js';
|
|
35
37
|
import { team } from '../space/presets.js';
|
|
36
38
|
import { CARRIER_COLLECTION, deriveAccountRegistry, deriveContactsSpace, MEMBERSHIP_COLLECTION, PROFILE_COLLECTION, PROFILE_KEY, } from '../space/account-registry.js';
|
|
37
39
|
import { CARRY_CLOSED_KEY, makePass, PASS_COLLECTION, passKey } from '../space/pass.js';
|
|
@@ -1137,11 +1139,14 @@ export async function createNode(config) {
|
|
|
1137
1139
|
const name = contact.name.trim().slice(0, 200);
|
|
1138
1140
|
if (!name)
|
|
1139
1141
|
throw new Error('A contact needs a name');
|
|
1142
|
+
// An answer to their knock still to be written stays to be written.
|
|
1143
|
+
const pending = (await contactRecords()).find((record) => record.body.did === contact.did)?.body?.door;
|
|
1140
1144
|
return writeContact({
|
|
1141
1145
|
did: contact.did,
|
|
1142
1146
|
name,
|
|
1143
1147
|
...(contact.space ? { space: contact.space } : {}),
|
|
1144
1148
|
...(contact.note ? { note: contact.note.slice(0, 2000) } : {}),
|
|
1149
|
+
...(pending ? { door: pending } : {}),
|
|
1145
1150
|
});
|
|
1146
1151
|
},
|
|
1147
1152
|
async remove(did) {
|
|
@@ -1227,6 +1232,306 @@ export async function createNode(config) {
|
|
|
1227
1232
|
return [...seen].filter((account) => account !== config.signer.did && account !== did).sort();
|
|
1228
1233
|
},
|
|
1229
1234
|
});
|
|
1235
|
+
// ─── Doors ─────────────────────────────────────────────────────────
|
|
1236
|
+
//
|
|
1237
|
+
// A door is a `std.door` in the contacts space; its keys are derived from
|
|
1238
|
+
// the contact key and its id, so every device with the contact key opens the
|
|
1239
|
+
// same doors. Knocks wait in the mailboxes of the relays a door names.
|
|
1240
|
+
//
|
|
1241
|
+
// A knock you left is a `std.knock` until the person behind the door
|
|
1242
|
+
// answers in the space for two — a `std.knock-answer` signed with the
|
|
1243
|
+
// door's signing key — when it becomes a contact and the invite is closed.
|
|
1244
|
+
// Accepting a knock writes that answer as soon as this account's membership
|
|
1245
|
+
// there has landed; until then the contact carries the door's id.
|
|
1246
|
+
// See `src/doors/doors.ts`.
|
|
1247
|
+
const mailbox = config.mailbox ?? createMailboxClient();
|
|
1248
|
+
/** Knocks seen at this node's doors, by id: opened (null when they didn't check out), and until when to keep them */
|
|
1249
|
+
const knockCache = new Map();
|
|
1250
|
+
const DAY_MS = 24 * 3600 * 1000;
|
|
1251
|
+
function requireContactKey(what) {
|
|
1252
|
+
// Agents never open doors or knock, even one a home gave the contact key.
|
|
1253
|
+
if (agentSession)
|
|
1254
|
+
throw new Error(`An agent can't use doors. ${what} is the person's to do.`);
|
|
1255
|
+
if (!config.contactKey)
|
|
1256
|
+
throw new Error(`${what} needs your contact key. Connect to your account home again, and allow contacts.`);
|
|
1257
|
+
return config.contactKey;
|
|
1258
|
+
}
|
|
1259
|
+
/** A door's keys, from the contact key and its id */
|
|
1260
|
+
async function doorKeys(id) {
|
|
1261
|
+
const contactKey = requireContactKey('A door');
|
|
1262
|
+
const key = await deriveDoorKeyBytes(contactKey, id);
|
|
1263
|
+
const sign = await deriveDoorSignKeyBytes(contactKey, id);
|
|
1264
|
+
const signPublic = contactPublicKey(sign);
|
|
1265
|
+
return { key, sign, publicKey: contactPublicKey(key), signPublic, topic: await doorTopic(signPublic) };
|
|
1266
|
+
}
|
|
1267
|
+
async function doorRecords() {
|
|
1268
|
+
if (!contactsSpaceId)
|
|
1269
|
+
return [];
|
|
1270
|
+
return (await (await contactsRuntime()).list({ collection: doorSchema.name })).filter((record) => record.verified && record.root === config.signer.did && typeof record.body?.id === 'string' && Array.isArray(record.body.relays));
|
|
1271
|
+
}
|
|
1272
|
+
async function doorView(record) {
|
|
1273
|
+
const body = record.body;
|
|
1274
|
+
const keys = await doorKeys(body.id);
|
|
1275
|
+
return Object.freeze({
|
|
1276
|
+
id: body.id,
|
|
1277
|
+
...(body.label ? { label: body.label } : {}),
|
|
1278
|
+
...(body.name ? { name: body.name } : {}),
|
|
1279
|
+
key: keys.publicKey,
|
|
1280
|
+
sign: keys.signPublic,
|
|
1281
|
+
relays: Object.freeze([...body.relays]),
|
|
1282
|
+
code: encodeDoorCode({ key: keys.publicKey, sign: keys.signPublic, relays: body.relays, ...(body.name ? { name: body.name } : {}) }),
|
|
1283
|
+
createdAt: record.createdAt,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
/** Clears knocks from every relay a door names, as its owner; how many relays did */
|
|
1287
|
+
async function purgeDoor(door, ids) {
|
|
1288
|
+
const keys = await doorKeys(door.id);
|
|
1289
|
+
const done = await Promise.allSettled(door.relays.map((relay) => mailbox.purge(relay, keys.topic, keys.signPublic, ids, (nonce) => signPurge(keys.sign, keys.topic, nonce, ids))));
|
|
1290
|
+
return done.filter((result) => result.status === 'fulfilled').length;
|
|
1291
|
+
}
|
|
1292
|
+
async function sentRecords() {
|
|
1293
|
+
if (!contactsSpaceId)
|
|
1294
|
+
return [];
|
|
1295
|
+
return (await (await contactsRuntime()).list({ collection: knockSchema.name })).filter((record) => record.verified &&
|
|
1296
|
+
record.root === config.signer.did &&
|
|
1297
|
+
typeof record.body?.space === 'string' &&
|
|
1298
|
+
typeof record.body.sign === 'string' &&
|
|
1299
|
+
typeof record.body.invite === 'string');
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Knocks of yours: answered ones become contacts, and the invite is closed;
|
|
1303
|
+
* ones nobody answered within the knock's life are let go, space and all.
|
|
1304
|
+
*/
|
|
1305
|
+
async function settleSent() {
|
|
1306
|
+
for (const record of await sentRecords()) {
|
|
1307
|
+
const { space, name, sign, invite } = record.body;
|
|
1308
|
+
if (!(await registry.get(space))) {
|
|
1309
|
+
await (await contactsRuntime()).remove(record.key);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const open = await runtime(space);
|
|
1313
|
+
const { members } = await open.access();
|
|
1314
|
+
let answered = null;
|
|
1315
|
+
for (const answer of await open.list({ collection: knockAnswerSchema.name })) {
|
|
1316
|
+
const did = answer.root;
|
|
1317
|
+
if (!answer.verified || !did || did === config.signer.did || answer.createdBy !== did || typeof answer.body?.sig !== 'string')
|
|
1318
|
+
continue;
|
|
1319
|
+
if (!members.some((member) => member.did === did))
|
|
1320
|
+
continue;
|
|
1321
|
+
if (await checkAnswer(sign, space, did, answer.body.sig)) {
|
|
1322
|
+
answered = did;
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (answered) {
|
|
1327
|
+
// Whoever else the invite reached, nobody more joins with it.
|
|
1328
|
+
await spaces.closeInvite(space, invite).catch(() => { });
|
|
1329
|
+
const given = (await open.profiles()).find((profile) => profile.did === answered)?.name;
|
|
1330
|
+
await writeContact({ did: answered, name: given ?? name, space });
|
|
1331
|
+
await (await contactsRuntime()).remove(record.key);
|
|
1332
|
+
}
|
|
1333
|
+
else if (Date.now() - Date.parse(record.createdAt) > KNOCK_TTL_SECONDS * 1000 + DAY_MS) {
|
|
1334
|
+
await spaces.leave(space).catch(() => { });
|
|
1335
|
+
await (await contactsRuntime()).remove(record.key);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
/** Writes the answer to knocks this account accepted, once it can write in the space for two */
|
|
1340
|
+
async function answerAccepted() {
|
|
1341
|
+
for (const record of await contactRecords()) {
|
|
1342
|
+
const { did, space, door } = record.body;
|
|
1343
|
+
if (typeof door !== 'string' || typeof space !== 'string')
|
|
1344
|
+
continue;
|
|
1345
|
+
if (!(await registry.get(space)))
|
|
1346
|
+
continue;
|
|
1347
|
+
const open = await runtime(space);
|
|
1348
|
+
if ((await open.access()).role === null)
|
|
1349
|
+
continue;
|
|
1350
|
+
const { sign } = await doorKeys(door);
|
|
1351
|
+
try {
|
|
1352
|
+
await open.put(knockAnswerSchema.name, { sig: await signAnswer(sign, space, config.signer.did) });
|
|
1353
|
+
}
|
|
1354
|
+
catch {
|
|
1355
|
+
continue; // The space's definition of answers hasn't arrived yet.
|
|
1356
|
+
}
|
|
1357
|
+
const { door: _answered, ...rest } = record.body;
|
|
1358
|
+
await writeContact({ ...rest, did });
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
/** What the mailboxes of this node's doors hold now, opened and cached */
|
|
1362
|
+
async function readDoors() {
|
|
1363
|
+
const now = Date.now();
|
|
1364
|
+
for (const [id, cached] of knockCache)
|
|
1365
|
+
if (cached.until <= now)
|
|
1366
|
+
knockCache.delete(id);
|
|
1367
|
+
const found = [];
|
|
1368
|
+
for (const record of await doorRecords()) {
|
|
1369
|
+
const { id: doorId, relays } = record.body;
|
|
1370
|
+
const keys = await doorKeys(doorId);
|
|
1371
|
+
// Every relay, in full: a door holds 64 knocks at most, and a knock
|
|
1372
|
+
// cleared from another device should go from here too. What was opened
|
|
1373
|
+
// once is not opened again.
|
|
1374
|
+
const held = new Map();
|
|
1375
|
+
for (const result of await Promise.allSettled(relays.map((relay) => mailbox.fetch(relay, keys.topic)))) {
|
|
1376
|
+
if (result.status !== 'fulfilled')
|
|
1377
|
+
continue;
|
|
1378
|
+
for (const item of result.value) {
|
|
1379
|
+
// A relay's id is only its word: file the knock under the hash of what it is.
|
|
1380
|
+
const id = await knockId(item.blob);
|
|
1381
|
+
const seen = held.get(id);
|
|
1382
|
+
if (!seen || item.at < seen.at)
|
|
1383
|
+
held.set(id, { blob: item.blob, at: item.at });
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
for (const [id, item] of held) {
|
|
1387
|
+
let cached = knockCache.get(id);
|
|
1388
|
+
if (!cached) {
|
|
1389
|
+
cached = { opened: await openKnock(keys.key, item.blob, item.at, provider), door: doorId, until: item.at + KNOCK_TTL_SECONDS * 1000 };
|
|
1390
|
+
knockCache.set(id, cached);
|
|
1391
|
+
}
|
|
1392
|
+
if (cached.opened)
|
|
1393
|
+
found.push({ id, door: doorId, opened: cached.opened });
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return found;
|
|
1397
|
+
}
|
|
1398
|
+
const doors = Object.freeze({
|
|
1399
|
+
async list() {
|
|
1400
|
+
if (!config.contactKey || agentSession)
|
|
1401
|
+
return [];
|
|
1402
|
+
return Promise.all((await doorRecords()).map(doorView));
|
|
1403
|
+
},
|
|
1404
|
+
async open(options = {}) {
|
|
1405
|
+
requireContactKey('Opening a door');
|
|
1406
|
+
if (!contactsSpaceId)
|
|
1407
|
+
await contactsRuntime();
|
|
1408
|
+
const relays = [...(options.relays ?? (config.network?.relays ?? []).filter((url) => checkRelays([url]) === null))].slice(0, MAX_DOOR_RELAYS);
|
|
1409
|
+
if (relays.length === 0)
|
|
1410
|
+
throw new Error('A door needs a relay to hold its knocks, and this node has none.');
|
|
1411
|
+
const name = clip((options.name ?? (await ownName()) ?? '').trim(), 64);
|
|
1412
|
+
const label = options.label ? clip(options.label.trim(), 64) : '';
|
|
1413
|
+
const id = base64UrlEncode(globalThis.crypto.getRandomValues(new Uint8Array(16)));
|
|
1414
|
+
// Checks the relays and name the way a knocker will.
|
|
1415
|
+
const keys = await doorKeys(id);
|
|
1416
|
+
encodeDoorCode({ key: keys.publicKey, sign: keys.signPublic, relays, ...(name ? { name } : {}) });
|
|
1417
|
+
const open = await contactsRuntime();
|
|
1418
|
+
await ensureDefined(open, doorSchema);
|
|
1419
|
+
return doorView(await open.put(doorSchema.name, { id, relays, ...(name ? { name } : {}), ...(label ? { label } : {}) }));
|
|
1420
|
+
},
|
|
1421
|
+
async close(id) {
|
|
1422
|
+
const found = (await doorRecords()).find((record) => record.body.id === id);
|
|
1423
|
+
if (found)
|
|
1424
|
+
await (await contactsRuntime()).remove(found.key);
|
|
1425
|
+
for (const [knock, cached] of knockCache)
|
|
1426
|
+
if (cached.door === id)
|
|
1427
|
+
knockCache.delete(knock);
|
|
1428
|
+
},
|
|
1429
|
+
async clear(id) {
|
|
1430
|
+
requireContactKey('Clearing a door');
|
|
1431
|
+
const found = (await doorRecords()).find((record) => record.body.id === id);
|
|
1432
|
+
if (!found)
|
|
1433
|
+
throw new Error('There is no such door');
|
|
1434
|
+
if ((await purgeDoor(found.body, null)) === 0)
|
|
1435
|
+
throw new Error("None of the door's relays could be reached to clear it.");
|
|
1436
|
+
for (const [knock, cached] of knockCache)
|
|
1437
|
+
if (cached.door === id)
|
|
1438
|
+
knockCache.delete(knock);
|
|
1439
|
+
},
|
|
1440
|
+
async knock(code, options = {}) {
|
|
1441
|
+
requireContactKey('Knocking on a door');
|
|
1442
|
+
requireEverywhere('Knocking on a door');
|
|
1443
|
+
const door = parseDoorCode(code);
|
|
1444
|
+
for (const mine of await doors.list())
|
|
1445
|
+
if (mine.key === door.key)
|
|
1446
|
+
throw new Error('That is one of your own doors');
|
|
1447
|
+
if (!contactsSpaceId)
|
|
1448
|
+
await contactsRuntime();
|
|
1449
|
+
const mine = (await ownName()) ?? 'Someone';
|
|
1450
|
+
const theirs = door.name ?? 'Contact';
|
|
1451
|
+
const pair = await spaces.create({ name: `${clip(mine, 64)} & ${theirs}`, visibility: 'private', ...team });
|
|
1452
|
+
let invite;
|
|
1453
|
+
try {
|
|
1454
|
+
// Defined now, by the one who may: the answer is written by someone who joins as an editor.
|
|
1455
|
+
await ensureDefined(await runtime(pair.id), knockAnswerSchema);
|
|
1456
|
+
invite = await spaces.invite(pair.id, { role: 'editor' });
|
|
1457
|
+
const blob = await sealKnock(door.key, { from: config.signer.did, name: mine, invite, ...(options.note ? { note: options.note } : {}) }, { did: session.did, key: session.key, proof: session.proof() }, provider);
|
|
1458
|
+
const topic = await doorTopic(door.sign);
|
|
1459
|
+
const left = await Promise.allSettled(door.relays.map((relay) => mailbox.drop(relay, topic, blob, KNOCK_TTL_SECONDS)));
|
|
1460
|
+
if (!left.some((result) => result.status === 'fulfilled')) {
|
|
1461
|
+
const reasons = left.map((result) => (result.status === 'rejected' ? String(result.reason?.message ?? result.reason) : '')).filter(Boolean);
|
|
1462
|
+
throw new Error(`None of their door's relays took the knock. ${reasons.join('; ')}`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
catch (error) {
|
|
1466
|
+
await spaces.leave(pair.id).catch(() => { });
|
|
1467
|
+
throw error;
|
|
1468
|
+
}
|
|
1469
|
+
const open = await contactsRuntime();
|
|
1470
|
+
await ensureDefined(open, knockSchema);
|
|
1471
|
+
await open.put(knockSchema.name, { space: pair.id, name: theirs, door: door.key, sign: door.sign, invite });
|
|
1472
|
+
return { space: pair.id };
|
|
1473
|
+
},
|
|
1474
|
+
async knocks() {
|
|
1475
|
+
requireContactKey('Reading knocks');
|
|
1476
|
+
await settleSent().catch(() => { });
|
|
1477
|
+
await answerAccepted().catch(() => { });
|
|
1478
|
+
const blocked = new Set((await contactRecords()).filter((record) => record.body.blocked === true).map((record) => record.body.did));
|
|
1479
|
+
const found = [];
|
|
1480
|
+
for (const { id, door, opened } of await readDoors()) {
|
|
1481
|
+
if (opened.from === config.signer.did || blocked.has(opened.from))
|
|
1482
|
+
continue;
|
|
1483
|
+
// Already accepted, here or on another device.
|
|
1484
|
+
if (await registry.get(opened.pairSpace))
|
|
1485
|
+
continue;
|
|
1486
|
+
found.push(Object.freeze({
|
|
1487
|
+
id,
|
|
1488
|
+
door,
|
|
1489
|
+
from: opened.from,
|
|
1490
|
+
name: opened.name,
|
|
1491
|
+
...(opened.note ? { note: opened.note } : {}),
|
|
1492
|
+
pairSpace: opened.pairSpace,
|
|
1493
|
+
at: new Date(opened.at).toISOString(),
|
|
1494
|
+
}));
|
|
1495
|
+
}
|
|
1496
|
+
return found.sort((a, b) => b.at.localeCompare(a.at));
|
|
1497
|
+
},
|
|
1498
|
+
async sent() {
|
|
1499
|
+
await settleSent().catch(() => { });
|
|
1500
|
+
return (await sentRecords()).map((record) => Object.freeze({ space: record.body.space, name: record.body.name, at: record.createdAt }));
|
|
1501
|
+
},
|
|
1502
|
+
async accept(id) {
|
|
1503
|
+
requireContactKey('Opening the door to someone');
|
|
1504
|
+
requireEverywhere('Opening the door to someone');
|
|
1505
|
+
if (!knockCache.get(id)?.opened)
|
|
1506
|
+
await readDoors();
|
|
1507
|
+
const cached = knockCache.get(id);
|
|
1508
|
+
const knocked = cached?.opened;
|
|
1509
|
+
if (!cached || !knocked)
|
|
1510
|
+
throw new Error('That knock is gone: it expired, or the door was closed or cleared.');
|
|
1511
|
+
await spaces.join(knocked.invite);
|
|
1512
|
+
const known = await contacts.get(knocked.from);
|
|
1513
|
+
const contact = await writeContact({ did: knocked.from, name: known?.name ?? knocked.name, space: knocked.pairSpace, door: cached.door });
|
|
1514
|
+
await answerAccepted().catch(() => { });
|
|
1515
|
+
// Done with: nobody needs it in the mailboxes any more.
|
|
1516
|
+
const door = (await doorRecords()).find((record) => record.body.id === cached.door);
|
|
1517
|
+
if (door)
|
|
1518
|
+
await purgeDoor(door.body, [id]).catch(() => 0);
|
|
1519
|
+
knockCache.delete(id);
|
|
1520
|
+
return contact;
|
|
1521
|
+
},
|
|
1522
|
+
async dismiss(id) {
|
|
1523
|
+
requireContactKey('Dismissing a knock');
|
|
1524
|
+
if (!knockCache.has(id))
|
|
1525
|
+
await readDoors();
|
|
1526
|
+
const cached = knockCache.get(id);
|
|
1527
|
+
if (!cached)
|
|
1528
|
+
return;
|
|
1529
|
+
const door = (await doorRecords()).find((record) => record.body.id === cached.door);
|
|
1530
|
+
if (door)
|
|
1531
|
+
await purgeDoor(door.body, [id]);
|
|
1532
|
+
knockCache.delete(id);
|
|
1533
|
+
},
|
|
1534
|
+
});
|
|
1230
1535
|
const collections = Object.freeze({
|
|
1231
1536
|
async list(spaceId) {
|
|
1232
1537
|
return (await runtime(spaceId)).collections();
|
|
@@ -1460,6 +1765,18 @@ export async function createNode(config) {
|
|
|
1460
1765
|
accept: person('accept contact requests'),
|
|
1461
1766
|
others: person('look inside a contact\'s space'),
|
|
1462
1767
|
}),
|
|
1768
|
+
// Doors are the person's: an agent has none and knocks on none.
|
|
1769
|
+
doors: Object.freeze({
|
|
1770
|
+
list: async () => [],
|
|
1771
|
+
open: person('open a door'),
|
|
1772
|
+
close: person('close a door'),
|
|
1773
|
+
knock: person('knock on anyone\'s door'),
|
|
1774
|
+
clear: person('clear a door'),
|
|
1775
|
+
knocks: person('read knocks'),
|
|
1776
|
+
sent: person('look at knocks'),
|
|
1777
|
+
accept: person('open the door to anyone'),
|
|
1778
|
+
dismiss: person('dismiss a knock'),
|
|
1779
|
+
}),
|
|
1463
1780
|
delegation: () => note,
|
|
1464
1781
|
iceServers: node.iceServers,
|
|
1465
1782
|
delegate: person('pass its access on'),
|
|
@@ -1483,6 +1800,7 @@ export async function createNode(config) {
|
|
|
1483
1800
|
hosting,
|
|
1484
1801
|
notifications,
|
|
1485
1802
|
contacts,
|
|
1803
|
+
doors,
|
|
1486
1804
|
asAgent,
|
|
1487
1805
|
delegation: () => current,
|
|
1488
1806
|
iceServers: async () => (mesh ? mesh.iceServers() : (config.network?.iceServers ?? DEFAULT_ICE_SERVERS)),
|