@volter/teams 0.1.52 → 0.1.53
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/package.json +1 -1
- package/server/delegated.mjs +16 -0
- package/server/machine-link.mjs +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.53",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
|
|
6
6
|
"license": "MIT",
|
package/server/delegated.mjs
CHANGED
|
@@ -106,6 +106,22 @@ export async function delegatedSessionOf(api, token) {
|
|
|
106
106
|
return { roomContext, conversation: claims.conversation, expiresAt: claims.exp * 1000, issuer: claims.iss, name, room: claims.room, scope: claims.scope, sessionRef: session.id, subject: claims.sub, teamId: session.team };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** A signed Room lifecycle notice. This scope grants no transcript read or user-turn control. */
|
|
110
|
+
export async function delegatedRoomNoticeOf(api, token) {
|
|
111
|
+
const c = await verifiedClaims(api, token);
|
|
112
|
+
if (!c || c.scope !== 'room-notice' || !c.session || c.sub !== c.session.agent) return null;
|
|
113
|
+
const n = c.notice;
|
|
114
|
+
if (![c.room, c.conversation, c.session.team, c.session.id, c.session.harness].every(v => typeof v === 'string' && v.length > 0 && v.length <= 256)) return null;
|
|
115
|
+
if (!n || !['joined', 'member-joined', 'member-left', 'left'].includes(n.kind)
|
|
116
|
+
|| typeof n.id !== 'string' || !/^[a-zA-Z0-9:_-]{1,200}$/.test(n.id)
|
|
117
|
+
|| typeof n.text !== 'string' || !n.text.trim() || n.text.length > 16000) return null;
|
|
118
|
+
const now = Math.floor(api.clock.nowMs() / 1000);
|
|
119
|
+
if (!Number.isFinite(c.iat) || c.exp - c.iat > SESSION_TOKEN_MAX_S || c.exp - now > SESSION_TOKEN_MAX_S) return null;
|
|
120
|
+
return { conversation: c.conversation, expiresAt: c.exp * 1000, issuer: c.iss, room: c.room,
|
|
121
|
+
roomName: typeof c.roomName === 'string' ? c.roomName.slice(0, 120) : c.room,
|
|
122
|
+
sessionRef: c.session.id, harness: c.session.harness, subject: c.sub, teamId: c.session.team, notice: n };
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
/** A token's claims when it is one this server trusts: signed by the issuer's key, for this server, in its lifetime. */
|
|
110
126
|
async function verifiedClaims(api, token) {
|
|
111
127
|
if (!api.delegatedIssuer || !api.publicOrigin || !looksDelegated(token)) return null;
|
package/server/machine-link.mjs
CHANGED
|
@@ -372,3 +372,40 @@ async function acceptChannel(api, auth, machineId, duplex, closeSocket) {
|
|
|
372
372
|
upstream.on('close', (reason) => { open.delete(pair); if (!stream.closed) stream.close(reason); });
|
|
373
373
|
});
|
|
374
374
|
}
|
|
375
|
+
|
|
376
|
+
/** Deliver only signed lifecycle text to an exact owner-bound session, through durable native mail.
|
|
377
|
+
* The native --id receipt survives reconnects and server restarts; retry never invents a new id.
|
|
378
|
+
*/
|
|
379
|
+
export async function deliverSessionRoomNotice(api, access) {
|
|
380
|
+
const target = await boundSessionTarget(api, access.teamId, access);
|
|
381
|
+
if (!target || target.harness !== access.harness) throw Object.assign(new Error('session not found'), { status: 404, code: 'not_found' });
|
|
382
|
+
const machine = await api.store.one('SELECT name FROM machines WHERE team_id=? AND id=? AND revoked_at IS NULL', [access.teamId, target.machineId]);
|
|
383
|
+
if (!machine) throw Object.assign(new Error('machine not found'), { status: 404, code: 'not_found' });
|
|
384
|
+
const machineName = String(machine.name).split('.')[0].toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
385
|
+
const to = `sc:${machineName}:${target.harness}:${target.nativeId}`;
|
|
386
|
+
const identity = JSON.stringify([access.issuer, access.room, access.conversation, target.sessionId, access.notice.id]);
|
|
387
|
+
const digest = [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(identity)))].map(v => v.toString(16).padStart(2, '0')).join('');
|
|
388
|
+
const pending = api.roomNoticeDeliveries ??= new Map();
|
|
389
|
+
if (pending.has(digest)) return pending.get(digest);
|
|
390
|
+
const delivery = (async () => {
|
|
391
|
+
const id = api.ids.next('grant'), principal = delegatedPrincipal(access), now = api.clock.nowMs();
|
|
392
|
+
await api.store.transaction([{ sql: 'INSERT INTO machine_grants(team_id,id,machine_id,subject_id,caps_json,targets_json,issuer_id,created_at,expires_at) VALUES (?,?,?,?,?,?,?,?,?)',
|
|
393
|
+
params: [access.teamId, id, target.machineId, principal, JSON.stringify(['mail']), JSON.stringify([`mail:${to}`]), `delegated:room-notice:${digest}`, now, access.expiresAt] }]);
|
|
394
|
+
try {
|
|
395
|
+
const link = await linkWithin(api, key(access.teamId, target.machineId), LINK_WAIT_MS);
|
|
396
|
+
if (!link) throw new Error('machine offline; Room notice remains pending');
|
|
397
|
+
const stream = await link.channel.open('mail', { _admitted: { principal, groups: [], grants: [id], revisions: { [id]: 1 } },
|
|
398
|
+
request: { op: 'send', from: `sc:${machineName}:operator:room-${digest.slice(0, 24)}`, from_name: `Room ${access.roomName}`,
|
|
399
|
+
to, body: access.notice.text, id: `room-notice:${digest}`, queue: false } });
|
|
400
|
+
return await new Promise((resolve, reject) => {
|
|
401
|
+
let text = '';
|
|
402
|
+
const timeout = setTimeout(() => { stream.close('timeout'); reject(new Error('Room notice delivery not confirmed')); }, 15000);
|
|
403
|
+
stream.on('data', bytes => { text += new TextDecoder().decode(bytes); });
|
|
404
|
+
stream.on('close', () => { clearTimeout(timeout); try { const result = JSON.parse(text); if (result.code !== 0) throw new Error(result.text ?? 'Room notice refused'); resolve({ delivered: true, noticeId: access.notice.id }); } catch (error) { reject(error); } });
|
|
405
|
+
stream.on('error', error => { clearTimeout(timeout); reject(error); });
|
|
406
|
+
});
|
|
407
|
+
} finally { await api.store.transaction([{ sql: 'DELETE FROM machine_grants WHERE team_id=? AND id=?', params: [access.teamId, id] }]); }
|
|
408
|
+
})();
|
|
409
|
+
pending.set(digest, delivery);
|
|
410
|
+
try { return await delivery; } finally { pending.delete(digest); }
|
|
411
|
+
}
|