@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.
- package/package.json +1 -1
- 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.
|
|
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
|
-
//
|
|
247
|
-
//
|
|
248
|
-
if(!machine)
|
|
249
|
-
if(machine){
|
|
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){
|