@volter/teams 0.1.60 → 0.1.61

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server/http.mjs +25 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
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
@@ -243,10 +243,31 @@ async function groupRoute(api,req,res,auth,groupId,principalId,url,requestId){
243
243
  statements.push({sql:'UPDATE teams SET auth_epoch=auth_epoch+1 WHERE id=?',params:[auth.team_id]});await guardedMutation(api,res,auth,guards,statements,{status,body:response,targetId:target});api.events.publish(auth.team_id,{type:'authorization_changed',group_id:groupId});if(req.method==='DELETE')closeChannelsFor(api,auth.team_id,{groupId,principalId:principalId??null,reason:'group_changed'});send(res,status,response,requestId);
244
244
  }
245
245
  async function enroll(api,req,res,auth,requestId){if(auth.role==='viewer')fail(403,'forbidden','viewers cannot enroll machines');const b=await parseJson(req);exact(b,['name','enrollment_key']);if(!b.name||!b.enrollment_key)fail(400,'invalid_request','name and enrollment_key required');const byKey=()=>api.store.one('SELECT m.*,s.id source_id,s.name source_name,s.kind source_kind,s.state source_state,s.visibility,s.revision source_revision,s.last_received_at,s.decoder,s.counters_json,s.last_error FROM machines m JOIN sources s ON s.team_id=m.team_id AND s.machine_id=m.id WHERE m.team_id=? AND m.custodian_id=? AND m.enrollment_key=?',[auth.team_id,auth.principal_id,b.enrollment_key]);let machine=await byKey();
246
- // The same person enrolling a machine of the same name again (a reinstall, a lost home) takes that machine back; the
247
- // caller then rotates its credential, as for a machine it already knew.
248
- if(!machine){const own=await api.store.one('SELECT id FROM machines WHERE team_id=? AND custodian_id=? AND name=? AND revoked_at IS NULL',[auth.team_id,auth.principal_id,b.name]);if(own){await api.store.transaction([{sql:'UPDATE machines SET enrollment_key=? WHERE team_id=? AND id=?',params:[b.enrollment_key,auth.team_id,own.id]}]);machine=await byKey();}}
249
- if(machine){return send(res,200,result(api,auth,{machine:machineJson(machine,api.clock.nowMs(),api),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);}
246
+ // Enrollment is a fresh, authenticated custodial act. A disconnect revoked the
247
+ // old credentials, but must not permanently reserve this custodian's name.
248
+ if(!machine) machine=await api.store.one('SELECT m.*,s.id source_id,s.name source_name,s.kind source_kind,s.state source_state,s.visibility,s.revision source_revision,s.last_received_at,s.decoder,s.counters_json,s.last_error FROM machines m JOIN sources s ON s.team_id=m.team_id AND s.machine_id=m.id WHERE m.team_id=? AND m.custodian_id=? AND m.name=?',[auth.team_id,auth.principal_id,b.name]);
249
+ if(machine){
250
+ const now=api.clock.nowMs(),reviving=machine.revoked_at!=null;
251
+ const changed=reviving||machine.enrollment_key!==b.enrollment_key;
252
+ const cid=reviving?api.ids.next('credential'):null,token=reviving?makeToken(cid):null;
253
+ const updated=changed?{...machine,enrollment_key:b.enrollment_key,revision:machine.revision+1,...(reviving?{revoked_at:null,last_seen_at:null,incarnation_id:null,heartbeat_sequence:0}: {})}:machine;
254
+ const source=sourceJson({id:machine.source_id,name:machine.source_name,kind:machine.source_kind,state:reviving?'active':machine.source_state,machine_id:machine.id,custodian_id:machine.custodian_id,visibility:machine.visibility,revision:machine.source_revision+(reviving?1:0),last_received_at:machine.last_received_at,decoder:machine.decoder,counters_json:machine.counters_json,last_error:machine.last_error});
255
+ const response=result(api,auth,{machine:machineJson(updated,now,api),source,credential:reviving?{token,kind:'machine',expires_at:now+2592000000}:null});
256
+ if(changed){
257
+ const guards=[...authGuards(auth,now),{sql:'SELECT 1 ok FROM machines m JOIN sources s ON s.team_id=m.team_id AND s.machine_id=m.id WHERE m.team_id=? AND m.id=? AND m.custodian_id=? AND m.revision=? AND s.id=? AND s.revision=?',params:[auth.team_id,machine.id,auth.principal_id,machine.revision,machine.source_id,machine.source_revision],code:'revision_mismatch',status:409,message:'machine enrollment changed; retry connect'}];
258
+ const statements=[{sql:'UPDATE machines SET enrollment_key=?,revision=revision+1 WHERE team_id=? AND id=?',params:[b.enrollment_key,auth.team_id,machine.id]}];
259
+ if(reviving)statements.push(
260
+ {sql:'UPDATE machines SET revoked_at=NULL,last_seen_at=NULL,incarnation_id=NULL,heartbeat_sequence=0 WHERE team_id=? AND id=?',params:[auth.team_id,machine.id]},
261
+ {sql:"UPDATE sources SET state='active',revision=revision+1 WHERE team_id=? AND id=?",params:[auth.team_id,machine.source_id]},
262
+ {sql:'UPDATE credentials SET revoked_at=? WHERE team_id=? AND machine_id=? AND revoked_at IS NULL',params:[now,auth.team_id,machine.id]},
263
+ {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,machine.source_id,machine.id,now+2592000000,now]},
264
+ {sql:'UPDATE teams SET auth_epoch=auth_epoch+1 WHERE id=?',params:[auth.team_id]});
265
+ await guardedMutation(api,res,auth,guards,statements,{status:200,body:response,targetId:machine.id});
266
+ if(reviving)api.events.publish(auth.team_id,{type:'authorization_changed',machine_id:machine.id});
267
+ }
268
+ return send(res,200,response,requestId);
269
+ }
270
+ 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);}
250
271
  // `online` is a machine that heartbeats and holds its link, so callers can reach it; `unlinked` heartbeats without one.
251
272
  const machineJson=(m,now,api)=>({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?(!api||api.machineLinks?.has(`${m.team_id}\0${m.id}`)?'online':'unlinked'):'offline',last_seen_at:m.last_seen_at,incarnation_id:m.incarnation_id,host_key_id:m.host_key_id??null,revision:m.revision});
252
273
  async function machineAdminRoute(api,req,res,auth,machineId,roots,requestId){