@volter/teams 0.1.84 → 0.1.86
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/browser-channel.d.ts +1 -1
- package/browser-channel.mjs +5 -2
- package/package.json +1 -1
- package/server/apps.mjs +16 -5
- package/server/http.mjs +9 -3
package/browser-channel.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export declare class TurnDeliveryUnconfirmedError extends Error {
|
|
|
59
59
|
* does not undo delivery. After acceptance, a lost, invalid or cancelled receipt
|
|
60
60
|
* rejects with TurnDeliveryUnconfirmedError. Reuse the same key when retrying it.
|
|
61
61
|
*/
|
|
62
|
-
export function sendBrowserTurn(channel: BrowserChannel, text: string, options?: { turnKey?: string; signal?: AbortSignal }): Promise<BrowserTurnAnswer>;
|
|
62
|
+
export function sendBrowserTurn(channel: BrowserChannel, text: string, options?: { turnKey?: string; target?: { harness: string; session: string }; signal?: AbortSignal }): Promise<BrowserTurnAnswer>;
|
|
63
63
|
|
|
64
64
|
export interface MailHistoryEntry {
|
|
65
65
|
id: string; from: string; fromName: string; to: string; text: string; at: number;
|
package/browser-channel.mjs
CHANGED
|
@@ -178,12 +178,15 @@ export class TurnDeliveryUnconfirmedError extends Error {
|
|
|
178
178
|
* is free), or `{ code, text }` when it was not delivered; rejects when the machine refuses the stream.
|
|
179
179
|
* `turnKey`: one key per line the person typed, reused when they retry it, so a line the machine took after its
|
|
180
180
|
* first send was given up on is not typed twice (a machine of door vocabulary 3 or later keeps each key once).
|
|
181
|
+
* `target`: on a machine channel, the session that takes the line (`{ harness, session }`, the session's native
|
|
182
|
+
* id); a session channel's grant names its session, and needs none.
|
|
181
183
|
* Receipt waits are bounded to thirty seconds and 256 KB. `signal` cancels the
|
|
182
184
|
* wait; neither cancellation nor timeout undoes a turn already accepted.
|
|
183
185
|
*/
|
|
184
|
-
export async function sendBrowserTurn(channel, text, { turnKey, signal } = {}) {
|
|
186
|
+
export async function sendBrowserTurn(channel, text, { turnKey, target, signal } = {}) {
|
|
185
187
|
signal?.throwIfAborted();
|
|
186
|
-
const
|
|
188
|
+
const named = target ? { harness: String(target.harness), session: String(target.session) } : {};
|
|
189
|
+
const stream = await channel.open('mail', { request: { op: 'turn', text, ...named, ...(turnKey ? { turn_key: String(turnKey) } : {}) } });
|
|
187
190
|
return new Promise((resolve, reject) => {
|
|
188
191
|
let answer = '', bytes = 0, settled = false;
|
|
189
192
|
const cleanup = () => { clearTimeout(timer); stream.off('data', data); stream.off('close', closed); signal?.removeEventListener('abort', cancelled); };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.86",
|
|
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/apps.mjs
CHANGED
|
@@ -153,6 +153,12 @@ export async function installedIssuer(api, issuer, action, teamId = null) {
|
|
|
153
153
|
WHERE a.issuer=? AND a.state='active' AND i.state='active' AND m.active=1 AND installer.active=1 AND installer.role IN ('owner','admin') AND (? IS NULL OR i.team_id=?) AND EXISTS (SELECT 1 FROM json_each(i.actions_json) WHERE value=?) LIMIT 1`, [issuer, teamId, teamId, action]);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/** Every issuer some team trusts for sign-in (installedIssuer's trust, any team): what a client may offer. */
|
|
157
|
+
export async function identityIssuers(api) {
|
|
158
|
+
return (await api.store.query(`SELECT DISTINCT a.issuer FROM app_installations i JOIN apps a ON a.id=i.app_id JOIN memberships m ON m.team_id=i.team_id AND m.principal_id=i.principal_id JOIN memberships installer ON installer.team_id=i.team_id AND installer.principal_id=i.installer_id
|
|
159
|
+
WHERE a.issuer IS NOT NULL AND a.state='active' AND i.state='active' AND m.active=1 AND installer.active=1 AND installer.role IN ('owner','admin') AND EXISTS (SELECT 1 FROM json_each(i.actions_json) WHERE value='identity.login') ORDER BY a.issuer`, [])).map(row => row.issuer);
|
|
160
|
+
}
|
|
161
|
+
|
|
156
162
|
/** Fan-out is durable and transactionally committed with the source mutation. C2 adds resource events. */
|
|
157
163
|
export async function appEventStatements(api, teamId, kind, data, addedDestination = null) {
|
|
158
164
|
const now = api.clock.nowMs(), eventId = api.ids.next('event');
|
|
@@ -252,14 +258,18 @@ export async function appRoute(ctx) {
|
|
|
252
258
|
const app=await api.store.one("SELECT * FROM apps WHERE id=? AND team_id=? AND state='active'",[match[1],auth.team_id]);
|
|
253
259
|
if(!app)error(404,'not_found','app not found');
|
|
254
260
|
const now=api.clock.nowMs();
|
|
255
|
-
|
|
256
|
-
await
|
|
261
|
+
// Grants its installations issued, and grants made to their principals.
|
|
262
|
+
const grants=await api.store.query('SELECT g.id,g.team_id,g.machine_id FROM machine_grants g JOIN app_installations i ON i.id=g.app_installation_id OR (i.team_id=g.team_id AND i.principal_id=g.subject_id) WHERE i.app_id=?',[app.id]);
|
|
263
|
+
await guardedMutation(api,res,auth,[...adminGuards(auth,now),guard("SELECT 1 FROM apps WHERE id=? AND team_id=? AND state='active'",[app.id,auth.team_id]),guard('SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM machine_grants g JOIN app_installations i ON i.id=g.app_installation_id OR (i.team_id=g.team_id AND i.principal_id=g.subject_id) WHERE i.app_id=? AND g.id NOT IN (SELECT value FROM json_each(?)))',[app.id,JSON.stringify(grants.map(g=>g.id))])],[
|
|
257
264
|
statement("UPDATE apps SET state='retired',secret_hash='' WHERE id=?",app.id),
|
|
258
265
|
statement("UPDATE app_installations SET state='uninstalled',uninstalled_at=?,signing_secret='' WHERE app_id=?",now,app.id),
|
|
259
266
|
statement('UPDATE credentials SET revoked_at=? WHERE id IN (SELECT ac.credential_id FROM app_credentials ac JOIN app_installations i ON i.id=ac.installation_id WHERE i.app_id=? UNION SELECT c.credential_id FROM identity_app_credentials c JOIN app_installations i ON i.id=c.installation_id WHERE i.app_id=?)',now,app.id,app.id),
|
|
260
267
|
statement('UPDATE memberships SET active=0,revision=revision+1 WHERE (team_id,principal_id) IN (SELECT team_id,principal_id FROM app_installations WHERE app_id=?)',app.id),
|
|
261
268
|
statement('DELETE FROM app_install_codes WHERE installation_id IN (SELECT id FROM app_installations WHERE app_id=?)',app.id),
|
|
262
|
-
statement('DELETE FROM machine_grants WHERE app_installation_id IN (SELECT id FROM app_installations WHERE app_id=?)',app.id),
|
|
269
|
+
statement('DELETE FROM machine_grants WHERE app_installation_id IN (SELECT id FROM app_installations WHERE app_id=?) OR (team_id,subject_id) IN (SELECT team_id,principal_id FROM app_installations WHERE app_id=?)',app.id,app.id),
|
|
270
|
+
// Its principals leave their groups too, so a group's grants do not reach a reinstalled app.
|
|
271
|
+
statement('UPDATE groups_table SET revision=revision+1 WHERE (team_id,id) IN (SELECT team_id,group_id FROM group_members WHERE (team_id,principal_id) IN (SELECT team_id,principal_id FROM app_installations WHERE app_id=?))',app.id),
|
|
272
|
+
statement('DELETE FROM group_members WHERE (team_id,principal_id) IN (SELECT team_id,principal_id FROM app_installations WHERE app_id=?)',app.id),
|
|
263
273
|
statement("UPDATE app_webhook_deliveries SET state='cancelled',lease_until=NULL WHERE state='pending' AND installation_id IN (SELECT id FROM app_installations WHERE app_id=?)",app.id),
|
|
264
274
|
statement('UPDATE teams SET auth_epoch=auth_epoch+1 WHERE id IN (SELECT team_id FROM app_installations WHERE app_id=?)',app.id),
|
|
265
275
|
],{status:204,body:null,targetId:app.id});
|
|
@@ -320,8 +330,9 @@ export async function appRoute(ctx) {
|
|
|
320
330
|
if (!install) error(404, 'not_found', 'installation not found');
|
|
321
331
|
if (req.method === 'GET') { send(res, 200, result(api, auth, installJson(install)), requestId); return true; }
|
|
322
332
|
const now = api.clock.nowMs();
|
|
323
|
-
|
|
324
|
-
await
|
|
333
|
+
// The grants it issued and the grants made to its principal both end: a reinstall must not bring old access back.
|
|
334
|
+
const grants=await api.store.query('SELECT id,machine_id FROM machine_grants WHERE app_installation_id=? OR (team_id=? AND subject_id=?)',[install.id,install.team_id,install.principal_id]);
|
|
335
|
+
await guardedMutation(api, res, auth, [...adminGuards(auth, now),guard('SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM machine_grants WHERE (app_installation_id=? OR (team_id=? AND subject_id=?)) AND id NOT IN (SELECT value FROM json_each(?)))',[install.id,install.team_id,install.principal_id,JSON.stringify(grants.map(g=>g.id))])], [statement("UPDATE app_installations SET state='uninstalled',uninstalled_at=?,signing_secret='' WHERE id=?", now, install.id), statement('UPDATE credentials SET revoked_at=? WHERE id IN (SELECT credential_id FROM app_credentials WHERE installation_id=? UNION SELECT credential_id FROM identity_app_credentials WHERE installation_id=?)', now, install.id, install.id), statement('UPDATE memberships SET active=0,revision=revision+1 WHERE team_id=? AND principal_id=?', auth.team_id, install.principal_id), statement('DELETE FROM app_install_codes WHERE installation_id=?', install.id), statement('DELETE FROM machine_grants WHERE app_installation_id=? OR (team_id=? AND subject_id=?)',install.id,install.team_id,install.principal_id), statement('UPDATE groups_table SET revision=revision+1 WHERE (team_id,id) IN (SELECT team_id,group_id FROM group_members WHERE team_id=? AND principal_id=?)',install.team_id,install.principal_id), statement('DELETE FROM group_members WHERE team_id=? AND principal_id=?',install.team_id,install.principal_id), statement("UPDATE app_webhook_deliveries SET state='cancelled',lease_until=NULL WHERE installation_id=? AND state='pending'", install.id), statement('UPDATE teams SET auth_epoch=auth_epoch+1 WHERE id=?', auth.team_id), ...await appEventStatements(api, auth.team_id, 'app.uninstalled', installJson({ ...install, state: 'uninstalled', uninstalled_at: now }))], { status: 204, body: null, targetId: install.id });
|
|
325
336
|
closeAppChannels(api,install.id,'app_uninstalled');
|
|
326
337
|
for(const grant of grants)closeMachineChannels(api,auth.team_id,grant.machine_id,{grantId:grant.id,reason:'app_uninstalled'});
|
|
327
338
|
api.events.publish(auth.team_id, { type: 'authorization_changed', installation_id:install.id });
|
package/server/http.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appRoute, appPreAuth, appAuthentication, enforceAppScope, appAuthGuards, installedIssuer } from './apps.mjs';
|
|
1
|
+
import { appRoute, appPreAuth, appAuthentication, enforceAppScope, appAuthGuards, installedIssuer, identityIssuers } from './apps.mjs';
|
|
2
2
|
import { machineEventStream } from './machine-events.mjs';
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
4
|
import { bearer, hashToken, canManage, isAdmin, makeToken } from './auth.mjs';
|
|
@@ -40,13 +40,16 @@ export function createHttpHandler(api){return async(req,res)=>{const requestId=a
|
|
|
40
40
|
if(req.method==='POST'&&path==='/api/v1/invitations/accept'){checkRate(api,`login:${req.socket.remoteAddress}`,10,5);const body=await parseJson(req);return await acceptInvite(api,res,body,requestId);}
|
|
41
41
|
if(req.method==='POST'&&path==='/api/v1/login/volter'){checkRate(api,`login:${req.socket.remoteAddress}`,10,5);const body=await parseJson(req);return await identityLogin(api,res,body,requestId);}
|
|
42
42
|
if(req.method==='POST'&&path==='/api/v1/login/volter/teams'){checkRate(api,`login-teams:${req.socket.remoteAddress}`,10,5);const body=await parseJson(req);return await identityTeams(api,res,body,requestId);}
|
|
43
|
+
// How to sign in is public, as a client needs it before it has a credential: the modes this server takes and, for
|
|
44
|
+
// identity, the issuers some team trusts and the doors that exchange an issuer's token (identity.mjs verifies any
|
|
45
|
+
// OIDC issuer), so a client follows what is advertised rather than naming a provider.
|
|
46
|
+
if(req.method==='GET'&&path==='/api/v1/capabilities'){checkRate(api,`capabilities:${req.socket.remoteAddress}`,60,30);const issuers=api.publicOrigin?await identityIssuers(api):[];return send(res,200,{version:1,server_id:api.serverId,protocol:'teams.v1',operations:['catalog','search','read','follow','access','ingest'],auth_modes:['token','invitation','installed-app',...(issuers.length?['identity']:[])],app_token_exchange:'/api/v1/apps/token',...(issuers.length?{identity:{issuers,audience:api.publicOrigin,login:'/api/v1/login/volter',teams:'/api/v1/login/volter/teams'}}:{})},requestId);}
|
|
43
47
|
if(!auth)fail(401,'authentication_required','authentication required');auth.now=api.clock.nowMs();auth.clock=api.clock;
|
|
44
48
|
const bulk=/\/captures\/[^/]+\/parts\/\d+$/.test(path)||/\/uploads\/[^/]+\/chunks\/\d+$/.test(path);// A machine's own credential syncs its live sessions as they change (a capture is a begin and a commit), so it
|
|
45
49
|
// gets the bulk allowance for ordinary requests too; a person's or a service's stays at 120 a minute.
|
|
46
50
|
checkRate(api,`credential:${auth.credential_id}:${bulk?'bulk':'ordinary'}`,bulk||auth.kind==='machine'?600:120,bulk||auth.kind==='machine'?60:30);
|
|
47
51
|
if(req.method==='GET'&&path==='/api/v1/me'){if(!['user','service'].includes(auth.kind))fail(403,'forbidden','credential kind cannot use this operation');const teams=await api.store.query('SELECT t.id,t.name,t.revision,m.role FROM memberships m JOIN teams t ON t.id=m.team_id WHERE m.principal_id=? AND m.active=1 AND (? IS NULL OR m.team_id=?)',[auth.principal_id,auth.team_id,auth.team_id]);return send(res,200,{server_id:api.serverId,principal:{id:auth.principal_id,kind:auth.principal_kind,display_name:auth.display_name},teams:teams.map(x=>({team:{id:x.id,name:x.name,revision:x.revision},role:x.role}))},requestId);}
|
|
48
52
|
if(req.method==='POST'&&path==='/api/v1/me/identities')return await linkIdentity(api,req,res,auth,requestId);
|
|
49
|
-
if(req.method==='GET'&&path==='/api/v1/capabilities')return send(res,200,{version:1,server_id:api.serverId,protocol:'teams.v1',operations:['catalog','search','read','follow','access','ingest'],auth_modes:['token','invitation','installed-app'],app_token_exchange:'/api/v1/apps/token'},requestId);
|
|
50
53
|
if(req.method==='GET'&&path==='/api/v1/teams')return await listTeams(api,res,auth,url,requestId);
|
|
51
54
|
if(req.method==='POST'&&path==='/api/v1/teams')return await createTeam(api,req,res,auth,path,requestId);
|
|
52
55
|
const match=/^\/api\/v1\/teams\/([^/]+)(\/.*)?$/.exec(path);if(!match)fail(404,'not_found','not found');const teamId=match[1],sub=match[2]??'';
|
|
@@ -292,7 +295,10 @@ async function revokeMachine(api,req,res,auth,machineId,requestId){
|
|
|
292
295
|
async function watchedFor(api,teamId,machineId){const ids=[...(api.watchers?.keys()??[])].filter(key=>key.startsWith(`${teamId}:`)).map(key=>key.slice(teamId.length+1)).slice(0,90);if(!ids.length)return [];return (await api.store.query(`SELECT o.harness,o.native_id FROM origins o JOIN sources s ON s.team_id=o.team_id AND s.id=o.source_id WHERE o.team_id=? AND s.machine_id=? AND o.session_id IN (${ids.map(()=>'?').join(',')})`,[teamId,machineId,...ids])).map(({harness,native_id})=>({harness,native_id}));}
|
|
293
296
|
async function heartbeat(api,req,res,auth,machineId,requestId){if(auth.kind!=='machine'||auth.machine_id!==machineId)fail(403,'forbidden','exact machine credential required');const machine=await api.store.one('SELECT * FROM machines WHERE team_id=? AND id=?',[auth.team_id,machineId]);if(!machine||machine.revoked_at!=null)fail(403,'forbidden','machine enrollment is not active');const b=await parseJson(req);exact(b,['incarnation_id','sequence','bindings','roots','host_key_id','activity']);if(b.activity!==undefined&&(!Array.isArray(b.activity)||b.activity.length>4096))fail(400,'invalid_request','invalid activity');if(b.host_key_id!==undefined&&(typeof b.host_key_id!=='string'||!/^[A-Za-z0-9_-]{16,128}$/.test(b.host_key_id)))fail(400,'invalid_request','invalid host_key_id');if(typeof b.incarnation_id!=='string'||!Number.isSafeInteger(b.sequence)||b.sequence<1||!Array.isArray(b.bindings)||!Array.isArray(b.roots))fail(400,'invalid_request','invalid heartbeat');if(machine.incarnation_id===b.incarnation_id&&b.sequence<=machine.heartbeat_sequence)return send(res,200,result(api,auth,{...machineJson(machine,api.clock.nowMs(),api),watched:await watchedFor(api,auth.team_id,machineId)}),requestId);const now=api.clock.nowMs(),newIncarnation=machine.incarnation_id!==null&&machine.incarnation_id!==b.incarnation_id,statements=[{sql:'UPDATE machines SET incarnation_id=?,heartbeat_sequence=?,last_seen_at=?,host_key_id=COALESCE(?,host_key_id),revision=revision+1 WHERE team_id=? AND id=?',params:[b.incarnation_id,b.sequence,now,b.host_key_id??null,auth.team_id,machineId]},{sql:'DELETE FROM machine_roots WHERE team_id=? AND machine_id=?',params:[auth.team_id,machineId]}];if(newIncarnation)statements.push({sql:"UPDATE attachments SET state='closed',closed_at=? WHERE team_id=? AND machine_id=? AND state IN ('pending','active')",params:[now,auth.team_id,machineId]},{sql:"UPDATE operations SET state='unknown' WHERE team_id=? AND id IN (SELECT operation_id FROM native_effects WHERE team_id=? AND machine_id=? AND state IN ('pending','running'))",params:[auth.team_id,auth.team_id,machineId]},{sql:"UPDATE native_effects SET state='unknown',result_json=? WHERE team_id=? AND machine_id=? AND state IN ('pending','running')",params:[JSON.stringify({state:'unknown',error:null}),auth.team_id,machineId]});statements.push({sql:'DELETE FROM bindings WHERE team_id=? AND machine_id=? AND id NOT IN (SELECT binding_id FROM attachments WHERE team_id=? AND machine_id=?) AND id NOT IN (SELECT binding_id FROM native_effects WHERE team_id=? AND machine_id=? AND binding_id IS NOT NULL)',params:[auth.team_id,machineId,auth.team_id,machineId,auth.team_id,machineId]});// What each synced session's harness is doing now: this heartbeat's report replaces the source's last one.
|
|
294
297
|
if(Array.isArray(b.activity)){const source=await api.store.one("SELECT id FROM sources WHERE team_id=? AND machine_id=? AND kind='connector'",[auth.team_id,machineId]);if(source){statements.push({sql:'DELETE FROM session_activity WHERE team_id=? AND source_id=?',params:[auth.team_id,source.id]});for(const item of b.activity){exact(item,['harness','native_id','turn','since'],'activity');if(typeof item.harness!=='string'||!item.harness||item.harness.length>64||typeof item.native_id!=='string'||!item.native_id||item.native_id.length>256||typeof item.turn!=='string'||!item.turn||item.turn.length>32||(item.since!=null&&!Number.isSafeInteger(item.since)))fail(400,'invalid_request','invalid activity');statements.push({sql:'INSERT OR REPLACE INTO session_activity(team_id,source_id,harness,native_id,turn,since,observed_at) VALUES (?,?,?,?,?,?,?)',params:[auth.team_id,source.id,item.harness,item.native_id,item.turn,item.since??null,now]});}}}
|
|
295
|
-
for(const root of b.roots){exact(root,['id','machine_id','name','harnesses'],'root');if(root.machine_id!==machineId)fail(400,'invalid_request','root machine mismatch');statements.push({sql:'INSERT INTO machine_roots(team_id,machine_id,id,value_json) VALUES (?,?,?,?)',params:[auth.team_id,machineId,root.id,JSON.stringify(root)]});}
|
|
298
|
+
for(const root of b.roots){exact(root,['id','machine_id','name','harnesses'],'root');if(root.machine_id!==machineId)fail(400,'invalid_request','root machine mismatch');statements.push({sql:'INSERT INTO machine_roots(team_id,machine_id,id,value_json) VALUES (?,?,?,?)',params:[auth.team_id,machineId,root.id,JSON.stringify(root)]});}// A binding is stored against the session its native id names; one naming no session this source published is
|
|
299
|
+
// said back (unbound_bindings), never dropped in silence.
|
|
300
|
+
const unbound=[];for(const binding of b.bindings){const origin=await api.store.one('SELECT 1 ok FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND deleted_at IS NULL',[auth.team_id,auth.source_id,binding.harness,binding.native_session_id]);if(!origin)unbound.push({id:binding.id,harness:binding.harness,native_session_id:binding.native_session_id,reason:'no_session'});}
|
|
301
|
+
for(const binding of b.bindings)statements.push({sql:'INSERT INTO bindings(team_id,id,machine_id,session_id,incarnation_id,descriptor_json,observed_at) SELECT ?,?,?,o.session_id,?,?,? FROM origins o WHERE o.team_id=? AND o.source_id=? AND o.harness=? AND o.native_id=? AND o.deleted_at IS NULL ON CONFLICT(team_id,id) DO UPDATE SET machine_id=excluded.machine_id,session_id=excluded.session_id,incarnation_id=excluded.incarnation_id,descriptor_json=excluded.descriptor_json,observed_at=excluded.observed_at',params:[auth.team_id,binding.id,machineId,b.incarnation_id,JSON.stringify(binding),now,auth.team_id,auth.source_id,binding.harness,binding.native_session_id]});const updated={...machine,incarnation_id:b.incarnation_id,heartbeat_sequence:b.sequence,last_seen_at:now,revision:machine.revision+1},response=result(api,auth,machineJson(updated,now,api)),guards=[{sql:"SELECT 1 ok FROM credentials c JOIN machines m ON m.team_id=c.team_id AND m.id=c.machine_id JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id JOIN memberships mem ON mem.team_id=m.team_id AND mem.principal_id=m.custodian_id WHERE c.id=? AND c.kind='machine' AND c.machine_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND m.revoked_at IS NULL AND mem.active=1 AND s.state!='revoked' AND (m.incarnation_id IS NULL OR m.incarnation_id!=? OR m.heartbeat_sequence<?)",params:[auth.credential_id,machineId,now,b.incarnation_id,b.sequence],code:'authentication_required',status:401,message:'machine authorization or heartbeat sequence changed'}];await guardedMutation(api,res,auth,guards,statements,{status:200,body:response,targetId:machineId});send(res,200,response?.data?{...response,data:{...response.data,watched:await watchedFor(api,auth.team_id,machineId),...(unbound.length?{unbound_bindings:unbound}:{})}}:response,requestId);}
|
|
296
302
|
async function machineConfiguration(api,res,auth,machineId,requestId){
|
|
297
303
|
if(auth.kind!=='machine'||auth.machine_id!==machineId)fail(auth.kind==='machine'?404:403,auth.kind==='machine'?'not_found':'forbidden','not found');
|
|
298
304
|
const [machine,rules]=await api.store.snapshot([
|