@volter/teams 0.1.3 → 0.1.5
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 +2 -2
- package/server/http.mjs +3 -2
- package/server/machine-link.mjs +14 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"node": ">=22"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@volter/identity": "^0.3.
|
|
41
|
+
"@volter/identity": "^0.3.1",
|
|
42
42
|
"ws": "^8.21.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/server/http.mjs
CHANGED
|
@@ -226,8 +226,9 @@ async function enroll(api,req,res,auth,requestId){if(auth.role==='viewer')fail(4
|
|
|
226
226
|
if(machine){return send(res,200,result(api,auth,{machine:machineJson(machine,api.clock.nowMs()),source:sourceJson({id:machine.source_id,name:machine.source_name,kind:machine.source_kind,state:machine.source_state,machine_id:machine.id,custodian_id:machine.custodian_id,visibility:machine.visibility,revision:machine.source_revision,last_received_at:machine.last_received_at,decoder:machine.decoder,counters_json:machine.counters_json,last_error:machine.last_error}),credential:null}),requestId);}const mid=api.ids.next('machine'),sid=api.ids.next('source'),cid=api.ids.next('credential'),token=makeToken(cid),now=api.clock.nowMs(),data={machine:{id:mid,name:b.name,custodian_id:auth.principal_id,enrollment:'active',connection:'offline',last_seen_at:null,incarnation_id:null,revision:1},source:{id:sid,name:`${b.name} connector`,kind:'connector',state:'active',machine_id:mid,custodian_id:auth.principal_id,default_visibility:'restricted',revision:1,last_received_at:null,decoder:null},credential:{token,kind:'machine',expires_at:now+2592000000}},response=result(api,auth,data);await guardedMutation(api,res,auth,[...authGuards(auth,now),{sql:'SELECT 1 ok WHERE NOT EXISTS (SELECT 1 FROM sources WHERE team_id=? AND name=?)',params:[auth.team_id,`${b.name} connector`],code:'conflict',status:409,message:`a machine named ${b.name} is already enrolled in this team; enroll this one under another --name`}],[{sql:'INSERT INTO machines(team_id,id,name,custodian_id,enrollment_key) VALUES (?,?,?,?,?)',params:[auth.team_id,mid,b.name,auth.principal_id,b.enrollment_key]},{sql:"INSERT INTO sources(team_id,id,name,kind,machine_id,custodian_id,visibility,state) VALUES (?,?,?,?,?,?,?,'active')",params:[auth.team_id,sid,`${b.name} connector`,'connector',mid,auth.principal_id,'restricted']},{sql:'INSERT INTO credentials(id,token_hash,principal_id,kind,team_id,source_id,machine_id,expires_at,created_at) VALUES (?,?,?,?,?,?,?,?,?)',params:[cid,hashToken(token),auth.principal_id,'machine',auth.team_id,sid,mid,now+2592000000,now]}],{status:201,body:response,targetId:mid});send(res,201,response,requestId);}
|
|
227
227
|
const machineJson=(m,now)=>({id:m.id,name:m.name,custodian_id:m.custodian_id,enrollment:m.revoked_at==null?'active':'revoked',connection:m.last_seen_at!=null&&m.last_seen_at+45000>now?'online':'offline',last_seen_at:m.last_seen_at,incarnation_id:m.incarnation_id,host_key_id:m.host_key_id??null,revision:m.revision});
|
|
228
228
|
async function machineAdminRoute(api,req,res,auth,machineId,roots,requestId){
|
|
229
|
-
// Custodians and admins see their machines
|
|
230
|
-
|
|
229
|
+
// Custodians and admins see their machines, revoked ones included; every member sees the team's active machines,
|
|
230
|
+
// each of which they can message (the team's standing mail grant, machine-link.mjs). Roots stay custodial.
|
|
231
|
+
const custody=roots?'(m.custodian_id=? OR ?=1)':'(m.custodian_id=? OR ?=1 OR m.revoked_at IS NULL)',params=[auth.team_id,auth.principal_id,isAdmin(auth)?1:0];
|
|
231
232
|
if(machineId){const machine=await api.store.one(`SELECT m.* FROM machines m WHERE m.team_id=? AND ${custody} AND m.id=?`,[...params,machineId]);if(!machine)fail(404,'not_found','not found');if(roots){const rows=await api.store.query('SELECT value_json FROM machine_roots WHERE team_id=? AND machine_id=? ORDER BY id',[auth.team_id,machineId]);return send(res,200,{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},items:rows.map(row=>JSON.parse(row.value_json)),next_cursor:null},requestId);}return send(res,200,result(api,auth,machineJson(machine,api.clock.nowMs())),requestId,{ETag:`"${machine.revision}"`});}
|
|
232
233
|
const rows=await api.store.query(`SELECT m.* FROM machines m WHERE m.team_id=? AND ${custody} ORDER BY m.name,m.id`,params);send(res,200,{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},items:rows.map(row=>machineJson(row,api.clock.nowMs())),next_cursor:null},requestId);
|
|
233
234
|
}
|
package/server/machine-link.mjs
CHANGED
|
@@ -33,10 +33,20 @@ const channels = (api) => api.machineChannels ??= new Set();
|
|
|
33
33
|
const key = (teamId, machineId) => `${teamId}\0${machineId}`;
|
|
34
34
|
const grantJson = (row) => ({ id: row.id, machine_id: row.machine_id, subject_id: row.subject_id, caps: JSON.parse(row.caps_json), targets: JSON.parse(row.targets_json), issuer_id: row.issuer_id, created_at: row.created_at, expires_at: row.expires_at, revision: row.revision });
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Members of a team message each other's sessions without a grant each way, as Claude's own sessions reach one
|
|
38
|
+
* another: every machine carries the team's standing grant of `mail` on every session there, whose subject is the
|
|
39
|
+
* team itself, and every active member's claim names it. It grants nothing else, and the machine checks it from its
|
|
40
|
+
* own synced rows like any grant.
|
|
41
|
+
*/
|
|
42
|
+
export const TEAM_MAIL_GRANT = 'team-mail';
|
|
43
|
+
const teamSubject = (teamId) => `team:${teamId}`;
|
|
44
|
+
const teamMailGrant = (teamId, machineId) => ({ id: TEAM_MAIL_GRANT, machine_id: machineId, subject_id: teamSubject(teamId), caps: ['mail'], targets: ['mail:*'], issuer_id: null, created_at: 0, expires_at: Number.MAX_SAFE_INTEGER, revision: 1 });
|
|
45
|
+
|
|
36
46
|
/** The unexpired grants of one machine, as its configuration carries them to the machine. */
|
|
37
47
|
export async function machineGrantRows(api, teamId, machineId) {
|
|
38
48
|
const rows = await api.store.query('SELECT * FROM machine_grants WHERE team_id=? AND machine_id=? AND expires_at>? ORDER BY id', [teamId, machineId, api.clock.nowMs()]);
|
|
39
|
-
return rows.map(grantJson);
|
|
49
|
+
return [...rows.map(grantJson), teamMailGrant(teamId, machineId)];
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
/** What the caller may claim on the machine right now, or null (indistinguishable from no machine). */
|
|
@@ -53,9 +63,10 @@ async function claimFor(api, auth, machineId) {
|
|
|
53
63
|
if (machine.custodian_id === auth.principal_id) return { principal: auth.principal_id, groups, custodian: true };
|
|
54
64
|
const subjects = [auth.principal_id, ...groups];
|
|
55
65
|
const rows = await api.store.query(`SELECT id, revision FROM machine_grants WHERE team_id=? AND machine_id=? AND expires_at>? AND subject_id IN (${subjects.map(() => '?').join(',')}) ORDER BY id`, [auth.team_id, machineId, api.clock.nowMs(), ...subjects]);
|
|
56
|
-
|
|
66
|
+
// Every member holds the team's standing mail grant (TEAM_MAIL_GRANT) besides their own.
|
|
67
|
+
const held = [...rows, { id: TEAM_MAIL_GRANT, revision: 1 }];
|
|
57
68
|
// Revisions let the machine notice a grant it holds has changed (rewritten, narrowed) and resync.
|
|
58
|
-
return { principal: auth.principal_id, groups, grants:
|
|
69
|
+
return { principal: auth.principal_id, groups: [...groups, teamSubject(auth.team_id)], grants: held.map((row) => row.id), revisions: Object.fromEntries(held.map((row) => [row.id, row.revision])) };
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
/** Close every channel a member leaving the team, or a group losing a member or being deleted, leaves without its claimed authority. */
|