@volter/teams 0.1.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.1.4",
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",
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; a subject holding a live machine grant sees that machine (roots stay custodial).
230
- const custody=roots?'(m.custodian_id=? OR ?=1)':'(m.custodian_id=? OR ?=1 OR EXISTS (SELECT 1 FROM machine_grants g WHERE g.team_id=m.team_id AND g.machine_id=m.id AND g.expires_at>? AND g.subject_id IN (SELECT value FROM json_each(?))))',params=roots?[auth.team_id,auth.principal_id,isAdmin(auth)?1:0]:[auth.team_id,auth.principal_id,isAdmin(auth)?1:0,api.clock.nowMs(),JSON.stringify([auth.principal_id,...(auth.group_ids??[])])];
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
  }
@@ -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
- if (!rows.length) return null;
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: rows.map((row) => row.id), revisions: Object.fromEntries(rows.map((row) => [row.id, row.revision])) };
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. */