@volter/supercode-teams 0.2.164 → 0.2.166
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/bin/workspace.mjs +2 -2
- package/package.json +1 -1
- package/server/event-routes.mjs +2 -1
- package/server/harness-routes.mjs +36 -22
- package/server/store-core.mjs +15 -3
- package/workspace-client.d.ts +5 -3
- package/workspace-client.mjs +1 -0
package/bin/workspace.mjs
CHANGED
|
@@ -81,14 +81,14 @@ async function appsCommand(verb,sub,{flags,rest,output,team}) {
|
|
|
81
81
|
const body=()=>{if(!flags.body_file)usage('--body-file is required (use - for stdin)');return JSON.parse(readFileSync(flags.body_file==='-'?0:flags.body_file,'utf8'));};
|
|
82
82
|
if(verb==='apps') {
|
|
83
83
|
if(sub==='catalogue'||sub==='list'){requireArgs(rest,0,0,'teams apps catalogue|list');return output(await team.apps[sub]());}
|
|
84
|
-
if(
|
|
84
|
+
if(['get','rotate-secret','deregister'].includes(sub)){requireArgs(rest,1,1,'teams apps get|rotate-secret|deregister APP');return output(await team.apps[sub==='rotate-secret'?'rotateSecret':sub](rest[0]));}
|
|
85
85
|
if(sub==='register'){requireArgs(rest,0,0,'teams apps register --body-file FILE');return output(await team.apps.register(body()));}
|
|
86
86
|
if(sub==='install'){requireArgs(rest,1,1,'teams apps install APP --body-file FILE');return output(await team.apps.install(rest[0],body()));}
|
|
87
87
|
} else {
|
|
88
88
|
if(sub==='list'){requireArgs(rest,0,0,'teams app-installations list');return output(await team.appInstallations.list());}
|
|
89
89
|
if(['get','uninstall','delegate'].includes(sub)){requireArgs(rest,1,1,'teams app-installations get|uninstall|delegate INSTALLATION');return output(await team.appInstallations[sub](rest[0],...(sub==='delegate'?[body()]:[])));}
|
|
90
90
|
}
|
|
91
|
-
usage('teams apps catalogue|list|get|register|install|rotate-secret; app-installations list|get|uninstall|delegate');
|
|
91
|
+
usage('teams apps catalogue|list|get|register|install|rotate-secret|deregister; app-installations list|get|uninstall|delegate');
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
async function teamsCommand(verb, sub, { flags, rest, output, client }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.166",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|
package/server/event-routes.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appGrantAllows } from '@volter/teams/server/auth';
|
|
1
2
|
import { appAuthentication } from '@volter/teams/server/apps';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { validateListQuery } from '../contracts/index.mjs';
|
|
@@ -172,7 +173,7 @@ async function catalogWindow(api, auth, query) {
|
|
|
172
173
|
list.push(grant); grantsBySession.set(grant.session_id, list);
|
|
173
174
|
}
|
|
174
175
|
const readable = (row, seen = new Set()) => {
|
|
175
|
-
if (!row || seen.has(row.id) || row.deleted_at != null || !row.descriptor_json) return false;
|
|
176
|
+
if (!row || seen.has(row.id) || row.deleted_at != null || !row.descriptor_json || !appGrantAllows(auth,'session',row.id,'sessions.read')) return false;
|
|
176
177
|
seen.add(row.id);
|
|
177
178
|
const own = row.visibility === 'team' || row.custodian_id === auth.principal_id || (grantsBySession.get(row.id) ?? []).some(grant =>
|
|
178
179
|
(grant.expires_at == null || grant.expires_at > now) &&
|
|
@@ -28,11 +28,17 @@ import { fail, parseJson, result, send, guardedMutation, requireRevision, checkR
|
|
|
28
28
|
/** The `sub` a token says it is for, unverified: for a log line only, never for a decision. */
|
|
29
29
|
function unverifiedSubject(token){try{return String(JSON.parse(Buffer.from(String(token).split('.')[1]??'','base64url').toString('utf8')).sub??'').slice(0,80)||'-';}catch{return '-';}}
|
|
30
30
|
|
|
31
|
+
// URL encoding belongs to the HTTP boundary; native references retain their literal bytes in the store.
|
|
32
|
+
function sessionRef(value) {
|
|
33
|
+
try { return decodeURIComponent(value); }
|
|
34
|
+
catch { fail(400, 'invalid_request', 'session reference has invalid URL encoding'); }
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
async function preAuth({ api, req, res, path, requestId }) {
|
|
32
38
|
let early;
|
|
33
39
|
if(req.method==='POST'&&(early=/^\/api\/v1\/teams\/([^/]+)\/sessions\/([^/]+)\/room-notices$/.exec(path))){
|
|
34
40
|
const access=await delegatedRoomNoticeOf(api,bearer(req.headers.authorization));
|
|
35
|
-
if(!access || access.teamId!==early[1] || access.sessionRef!==
|
|
41
|
+
if(!access || access.teamId!==early[1] || access.sessionRef!==sessionRef(early[2])) fail(401,'authentication_required','authentication required');
|
|
36
42
|
checkRate(api,`room-notices:${access.issuer}:${access.teamId}`,120,10);
|
|
37
43
|
const receipt=await deliverSessionRoomNotice(api,access);
|
|
38
44
|
send(res,200,{data:receipt},requestId);return true;
|
|
@@ -46,14 +52,14 @@ async function preAuth({ api, req, res, path, requestId }) {
|
|
|
46
52
|
if(req.method==='POST'&&(early=/^\/api\/v1\/teams\/([^/]+)\/sessions\/([^/]+)\/viewer-errors$/.exec(path))){
|
|
47
53
|
const presented=bearer(req.headers.authorization)??'';checkRate(api,`viewer-errors:${hashToken(presented)}`,30,10);
|
|
48
54
|
const access=looksDelegated(presented)?await delegatedSessionOf(api,presented):null,body=await parseJson(req).catch(()=>({})),text=(value,limit)=>typeof value==='string'?value.replace(/\s+/g,' ').slice(0,limit):null;
|
|
49
|
-
console.log(`viewer-error team=${early[1]} session=${
|
|
55
|
+
console.log(`viewer-error team=${early[1]} session=${sessionRef(early[2]).slice(0,120)} subject=${access?access.subject:`unverified:${text(unverifiedSubject(presented),80)}`} scope=${access?.scope??'-'} room=${access?.room??'-'} stage=${text(body?.stage,40)} status=${Number.isSafeInteger(body?.status)?body.status:'-'} message=${JSON.stringify(text(body?.message,400))}`);
|
|
50
56
|
send(res,204,null,requestId);return true;
|
|
51
57
|
}
|
|
52
58
|
if(req.method==='GET'&&(early=/^\/api\/v1\/teams\/([^/]+)\/sessions\/([^/]+)(\/messages|\/events)?$/.exec(path))&&looksDelegated(bearer(req.headers.authorization))){
|
|
53
59
|
const access=await delegatedSessionOf(api,bearer(req.headers.authorization));
|
|
54
|
-
const ref=
|
|
60
|
+
const ref=sessionRef(early[2]);
|
|
55
61
|
if(!access||access.teamId!==early[1]||access.sessionRef!==ref){console.log(`session-view refused team=${early[1]} session=${ref.slice(0,120)} subject=${access?access.subject:`unverified:${unverifiedSubject(bearer(req.headers.authorization))}`} reason=${!access?'token not admitted':'token names another session'}`);fail(401,'authentication_required','authentication required');}
|
|
56
|
-
const auth={kind:'session_view',team_id:early[1],principal_id:null,credential_id:null,bound:{issuer:access.issuer,room:access.room,conversation:access.conversation,expiresAt:access.expiresAt}};
|
|
62
|
+
const auth={kind:'session_view',team_id:early[1],principal_id:null,credential_id:null,bound:{installationId:access.installationId,installedAt:access.installedAt,issuer:access.issuer,room:access.room,conversation:access.conversation,expiresAt:access.expiresAt}};
|
|
57
63
|
const url=new URL(req.url,'http://local');
|
|
58
64
|
if(!early[3])await sessionDescriptor(api,res,auth,ref,requestId);
|
|
59
65
|
else if(early[3]==='/messages')await readSession(api,res,auth,ref,url,requestId);
|
|
@@ -85,18 +91,18 @@ async function team({ api, req, res, auth, url, path, sub, requestId }) {
|
|
|
85
91
|
if(req.method==='GET'&&sub==='/sessions/search'){await listSessions(api,res,auth,url,requestId,true);return true;}
|
|
86
92
|
if(req.method==='GET'&&sub==='/sessions/events'){await handleCatalogEvents({api,req,res,auth,url,requestId,qobj,canReadMerged,readerProjection,fail});return true;}
|
|
87
93
|
if(req.method==='POST'&&sub==='/correlations'){await correlateRoute(api,req,res,auth,path,requestId);return true;}
|
|
88
|
-
if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)$/.exec(sub))){await sessionDescriptor(api,res,auth,m[1],requestId);return true;}
|
|
89
|
-
if((req.method==='PATCH'||req.method==='DELETE')&&(m=/^\/sessions\/([^/]+)$/.exec(sub))){await mutateSession(api,req,res,auth,m[1],requestId);return true;}
|
|
90
|
-
if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/messages$/.exec(sub))){await readSession(api,res,auth,m[1],url,requestId);return true;}
|
|
91
|
-
if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/artifacts\/([^/]+)$/.exec(sub))){await readArtifact(api,req,res,auth,m[1],m[2],requestId);return true;}
|
|
92
|
-
if(req.method==='POST'&&(m=/^\/sessions\/([^/]+)\/exports$/.exec(sub))){await exportSessionRoute({api,req,res,auth,sessionId:m[1],requestId,parseJson,exact,fail,send,result,guardedMutation});return true;}
|
|
93
|
-
if(req.method==='POST'&&(m=/^\/sessions\/([^/]+)\/continuations$/.exec(sub))){await continuationRoute({api,req,res,auth,sessionId:m[1],requestId,parseJson,exact,fail,send,result,guardedMutation});return true;}
|
|
94
|
-
if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/events$/.exec(sub))){await handleSessionEvents({api,req,res,auth,id:m[1],url,requestId,resolve,canReadMerged,readerProjection,fail});return true;}
|
|
95
|
-
if(['GET','POST','DELETE'].includes(req.method)&&(m=/^\/sessions\/([^/]+)\/view-bindings$/.exec(sub))){await viewBindingRoute(api,req,res,auth,m[1],requestId);return true;}
|
|
96
|
-
if((m=/^\/sessions\/([^/]+)\/access(?:\/([^/]+))?$/.exec(sub))){await accessRoute(api,req,res,auth,m[1],m[2],requestId);return true;}
|
|
94
|
+
if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)$/.exec(sub))){await sessionDescriptor(api,res,auth,sessionRef(m[1]),requestId);return true;}
|
|
95
|
+
if((req.method==='PATCH'||req.method==='DELETE')&&(m=/^\/sessions\/([^/]+)$/.exec(sub))){await mutateSession(api,req,res,auth,sessionRef(m[1]),requestId);return true;}
|
|
96
|
+
if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/messages$/.exec(sub))){await readSession(api,res,auth,sessionRef(m[1]),url,requestId);return true;}
|
|
97
|
+
if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/artifacts\/([^/]+)$/.exec(sub))){await readArtifact(api,req,res,auth,sessionRef(m[1]),m[2],requestId);return true;}
|
|
98
|
+
if(req.method==='POST'&&(m=/^\/sessions\/([^/]+)\/exports$/.exec(sub))){await exportSessionRoute({api,req,res,auth,sessionId:sessionRef(m[1]),requestId,parseJson,exact,fail,send,result,guardedMutation});return true;}
|
|
99
|
+
if(req.method==='POST'&&(m=/^\/sessions\/([^/]+)\/continuations$/.exec(sub))){await continuationRoute({api,req,res,auth,sessionId:sessionRef(m[1]),requestId,parseJson,exact,fail,send,result,guardedMutation});return true;}
|
|
100
|
+
if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/events$/.exec(sub))){await handleSessionEvents({api,req,res,auth,id:sessionRef(m[1]),url,requestId,resolve,canReadMerged,readerProjection,fail});return true;}
|
|
101
|
+
if(['GET','POST','DELETE'].includes(req.method)&&(m=/^\/sessions\/([^/]+)\/view-bindings$/.exec(sub))){await viewBindingRoute(api,req,res,auth,sessionRef(m[1]),requestId);return true;}
|
|
102
|
+
if((m=/^\/sessions\/([^/]+)\/access(?:\/([^/]+))?$/.exec(sub))){await accessRoute(api,req,res,auth,sessionRef(m[1]),m[2],requestId);return true;}
|
|
97
103
|
if((m=/^\/machines\/([^/]+)\/grants(?:\/([^/]+))?$/.exec(sub))){await liveGrantRoute({api,req,res,auth,machineId:m[1],grantId:m[2],url,requestId,parseJson,exact,fail,send,result,guardedMutation,requireRevision});return true;}
|
|
98
104
|
if((m=/^\/machines\/([^/]+)\/effects(?:\/([^/]+)(?:\/(claim|result))?)?$/.exec(sub))){await machineEffectRoute({api,req,res,auth,machineId:m[1],effectId:m[2],action:m[3],url,requestId,parseJson,exact,fail,send,result,guardedMutation});return true;}
|
|
99
|
-
if(req.method==='POST'&&(m=/^\/sessions\/([^/]+)\/attachments$/.exec(sub))){await createAttachment({api,req,res,auth,sessionId:m[1],requestId,parseJson,exact,fail,send,result,guardedMutation,resolve});return true;}
|
|
105
|
+
if(req.method==='POST'&&(m=/^\/sessions\/([^/]+)\/attachments$/.exec(sub))){await createAttachment({api,req,res,auth,sessionId:sessionRef(m[1]),requestId,parseJson,exact,fail,send,result,guardedMutation,resolve});return true;}
|
|
100
106
|
if(req.method==='POST'&&(m=/^\/attachments\/([^/]+)\/actions$/.exec(sub))){await attachmentActionRoute({api,req,res,auth,attachmentId:m[1],requestId,parseJson,exact,fail,send,result,guardedMutation,resolve});return true;}
|
|
101
107
|
if(req.method==='DELETE'&&(m=/^\/attachments\/([^/]+)$/.exec(sub))){await detachAttachmentRoute({api,res,auth,attachmentId:m[1],requestId,fail,send,guardedMutation});return true;}
|
|
102
108
|
if(req.method==='GET'&&(m=/^\/operations\/([^/]+)$/.exec(sub))){await operationRoute({api,res,auth,operationId:m[1],requestId,fail,send,result,guardedMutation});return true;}
|
|
@@ -159,12 +165,8 @@ async function listSessions(api,res,auth,url,requestId,search){
|
|
|
159
165
|
{kind:'query',sql:'SELECT * FROM session_grants WHERE team_id=?',params:[auth.team_id]}
|
|
160
166
|
);
|
|
161
167
|
if(cursorKey)statements.push({kind:'query',sql:`SELECT s.*,v.revision version_revision,v.descriptor_json,v.sort_at FROM ${cursorVersions} JOIN session_versions v ON v.team_id=? AND v.session_id=json_extract(j.value,'$[0]') AND v.revision=json_extract(j.value,'$[1]') JOIN sessions s ON s.team_id=v.team_id AND s.id=v.session_id WHERE s.deleted_at IS NULL AND v.revision<>s.revision`,params:[...cursorKey,auth.team_id]});
|
|
162
|
-
if(search){
|
|
163
|
-
statements.push({kind:'query',sql:'SELECT m.id,m.session_id,s.revision revision,m.recorded_at,m.search_text FROM sessions s JOIN messages m ON m.team_id=s.team_id AND m.session_id=s.id AND m.revision<=s.revision AND s.revision<COALESCE(m.until_revision,m.revision+1) WHERE s.team_id=? AND s.deleted_at IS NULL',params:[auth.team_id]});
|
|
164
|
-
if(cursorKey)statements.push({kind:'query',sql:`SELECT m.id,m.session_id,json_extract(j.value,'$[1]') revision,m.recorded_at,m.search_text FROM ${cursorVersions} JOIN messages m ON m.team_id=? AND m.session_id=json_extract(j.value,'$[0]') AND m.revision<=json_extract(j.value,'$[1]') AND json_extract(j.value,'$[1]')<COALESCE(m.until_revision,m.revision+1) JOIN sessions s ON s.team_id=m.team_id AND s.id=m.session_id WHERE s.deleted_at IS NULL AND json_extract(j.value,'$[1]')<>s.revision`,params:[...cursorKey,auth.team_id]});
|
|
165
|
-
}
|
|
166
168
|
const values=await api.store.snapshot(statements),offset=cursorKey?1:0,cursor=cursorKey?values[0]:null,team=values[offset],credential=values[offset+1],membership=values[offset+2],currentRows=values[offset+3],allGrants=values[offset+4];
|
|
167
|
-
|
|
169
|
+
const olderRows=cursorKey?values[offset+5]:[];
|
|
168
170
|
const rows=[...currentRows,...olderRows],currentById=new Map(currentRows.map(row=>[row.id,row]));
|
|
169
171
|
const boundaryNow=api.clock.nowMs();
|
|
170
172
|
if(!membership?.active||credential?.revoked_at!=null||(credential?.expires_at!=null&&credential.expires_at<=boundaryNow))fail(401,'authentication_required','authentication required');
|
|
@@ -173,9 +175,8 @@ async function listSessions(api,res,auth,url,requestId,search){
|
|
|
173
175
|
if(cursor.kind!==(search?'search':'list')||cursor.expires_at<=boundaryNow||cursor.auth_epoch!==team.auth_epoch||canonicalJson(JSON.parse(cursor.query_json))!==canonicalJson(normalizedQuery))fail(409,'resync_required','cursor expired, mismatched, or authorization changed');
|
|
174
176
|
tuples=JSON.parse(cursor.snapshot_json);
|
|
175
177
|
}else tuples=rows.filter(row=>row.revision===row.version_revision).map(row=>[row.id,row.version_revision]);
|
|
176
|
-
const rowByVersion=new Map(rows.map(row=>[`${row.id}:${row.version_revision}`,row])),grantsBySession=new Map()
|
|
178
|
+
const rowByVersion=new Map(rows.map(row=>[`${row.id}:${row.version_revision}`,row])),grantsBySession=new Map();
|
|
177
179
|
for(const grant of allGrants){const list=grantsBySession.get(grant.session_id)??[];list.push(grant);grantsBySession.set(grant.session_id,list);}
|
|
178
|
-
for(const message of allMessages){const key=`${message.session_id}:${message.revision}`,list=messagesByVersion.get(key)??[];list.push(message);messagesByVersion.set(key,list);}
|
|
179
180
|
const hits=[],hitTuples=[];
|
|
180
181
|
for(const [id,revision]of tuples){
|
|
181
182
|
const row=rowByVersion.get(`${id}:${revision}`),current=currentById.get(id);
|
|
@@ -190,7 +191,20 @@ async function listSessions(api,res,auth,url,requestId,search){
|
|
|
190
191
|
if(query.until!==undefined&&row.sort_at>=query.until)continue;
|
|
191
192
|
if(query.include_children===false&&view.parent_id!==null)continue;
|
|
192
193
|
let hit=view;
|
|
193
|
-
if(search){
|
|
194
|
+
if(search){
|
|
195
|
+
const needle=normalizeSearch(query.query.trim()),matches=[],title=normalizeSearch(view.title??'');
|
|
196
|
+
if(title.includes(needle))matches.push({message_id:null,field:'title',excerpt:title,offset:[...title.slice(0,title.indexOf(needle))].length});
|
|
197
|
+
// search_text is normalized on ingestion. SQLite matches the pinned version and returns only three bounded
|
|
198
|
+
// excerpts; neither nonmatching/private transcripts nor the org's retained text enter the Worker's heap.
|
|
199
|
+
const found=await api.store.query(`SELECT m.id,instr(m.search_text,?)-1 offset,
|
|
200
|
+
substr(m.search_text,max(1,instr(m.search_text,?)-120),length(?)+240) excerpt
|
|
201
|
+
FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT('m')}
|
|
202
|
+
AND instr(m.search_text,?)>0 ORDER BY m.ordinal,m.id LIMIT ?`,[needle,needle,needle,auth.team_id,id,revision,revision,needle,3-matches.length]);
|
|
203
|
+
matches.push(...found.map(message=>({message_id:message.id,field:'content',excerpt:message.excerpt,offset:message.offset})));
|
|
204
|
+
if(!matches.length)continue;
|
|
205
|
+
const latest=await api.store.one(`SELECT max(recorded_at) at FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT('m')}`,[auth.team_id,id,revision,revision]);
|
|
206
|
+
hit={session:view,matches,match_at:Math.max(latest?.at??0,row.sort_at),indexed_revision:revision};
|
|
207
|
+
}
|
|
194
208
|
hits.push(hit);hitTuples.push([id,revision]);
|
|
195
209
|
}
|
|
196
210
|
const limit=query.limit??50,page=hits.slice(0,limit);let next=null;
|
package/server/store-core.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { ingestOtlpBatch, correlateSessions } from './arc2-transactions.mjs';
|
|
|
7
7
|
import { claimEffect, getEffect, listEffects, reportEffect } from './effects-transactions.mjs';
|
|
8
8
|
import { applyCaptureMessages } from './capture-transactions.mjs';
|
|
9
9
|
import { MIGRATIONS, MESSAGE_AT } from '@volter/teams/server/migrations';
|
|
10
|
+
import { APP_LINEAGE_REVOKED, appGrantAllows } from '@volter/teams/server/auth';
|
|
10
11
|
|
|
11
12
|
/** Bring a database to the newest schema. `version()`/`setVersion(n)` name where the host records it. */
|
|
12
13
|
export function migrate({ exec, tx, version, setVersion }) {
|
|
@@ -26,9 +27,10 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
26
27
|
const {teamId,principalId,credentialId,ref,now,bound}=args;
|
|
27
28
|
// `bound`: a workplace's session-view token ({issuer, room, conversation, expiresAt}), which reads only a
|
|
28
29
|
// session its owner bound to that Room and Conversation; it holds no credential and is no member.
|
|
29
|
-
let membership=null;
|
|
30
|
+
let membership=null,appFence=null,grantSource=null;
|
|
31
|
+
if((args.delegationDepth??0)>16)domainError(401,'authentication_required','delegation lineage is too deep');
|
|
30
32
|
if(bound){
|
|
31
|
-
if(!get(`SELECT 1 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 WHERE i.team_id=? AND a.issuer=? AND i.state='active' AND a.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='delegation.sessions')`,[teamId,bound.issuer]))domainError(401,'authentication_required','integration was uninstalled');
|
|
33
|
+
if(!get(`SELECT 1 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 WHERE i.team_id=? AND a.issuer=? AND i.id=? AND i.created_at=? AND i.state='active' AND a.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='delegation.sessions')`,[teamId,bound.issuer,bound.installationId,bound.installedAt]))domainError(401,'authentication_required','integration was uninstalled');
|
|
32
34
|
if(!(bound.expiresAt>now))domainError(401,'authentication_required','authentication required');
|
|
33
35
|
}else{
|
|
34
36
|
const credential=get("SELECT id FROM credentials WHERE id=? AND principal_id=? AND kind IN ('user','service') AND (team_id IS NULL OR team_id=?) AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)",[credentialId,principalId,teamId,now]);
|
|
@@ -36,7 +38,12 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
36
38
|
const identity=get(`SELECT i.state,a.state app_state,m.active,m.role FROM identity_app_credentials c JOIN app_installations i ON i.id=c.installation_id JOIN apps a ON a.id=i.app_id JOIN memberships m ON m.team_id=i.team_id AND m.principal_id=i.installer_id WHERE c.credential_id=?`,[credentialId]);
|
|
37
39
|
if(identity&&(identity.state!=='active'||identity.app_state!=='active'||!identity.active||!['owner','admin'].includes(identity.role)))domainError(401,'authentication_required','identity integration revoked');
|
|
38
40
|
const app=get('SELECT ac.*,i.state,a.state app_state,installer.active,installer.role FROM app_credentials ac JOIN app_installations i ON i.id=ac.installation_id JOIN apps a ON a.id=i.app_id JOIN memberships installer ON installer.team_id=i.team_id AND installer.principal_id=i.installer_id WHERE ac.credential_id=?',[credentialId]);
|
|
39
|
-
if(app){
|
|
41
|
+
if(app){
|
|
42
|
+
if(app.state!=='active'||app.app_state!=='active'||!app.active||!['owner','admin'].includes(app.role)||!JSON.parse(app.actions_json).includes('sessions.read'))domainError(403,'forbidden','app session read authorization changed');
|
|
43
|
+
if(get(APP_LINEAGE_REVOKED,[credentialId,now]))domainError(401,'authentication_required','delegation authority revoked');
|
|
44
|
+
appFence=JSON.parse(app.grants_json);
|
|
45
|
+
if(appFence!==null)grantSource=get('SELECT id,principal_id FROM credentials WHERE id=?',[app.subject_credential_id??app.parent_id]);
|
|
46
|
+
}
|
|
40
47
|
if(!credential||!membership)domainError(401,'authentication_required','authentication required');
|
|
41
48
|
}
|
|
42
49
|
if(typeof ref!=='string'||ref.length<8)domainError(404,'not_found','not found');
|
|
@@ -57,6 +64,11 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
57
64
|
const family=all('WITH RECURSIVE family(id) AS (SELECT ? UNION SELECT s.id FROM sessions s JOIN family f ON s.canonical_id=f.id WHERE s.team_id=?) SELECT s.* FROM sessions s JOIN family f ON f.id=s.id WHERE s.team_id=?',[root.id,teamId,teamId]);
|
|
58
65
|
const readable=bound?Boolean(get('SELECT 1 ok FROM session_view_bindings WHERE team_id=? AND session_id=? AND issuer=? AND room=? AND conversation=?',[teamId,root.id,bound.issuer,bound.room,bound.conversation]))&&family.every(session=>session.deleted_at==null):family.every(session=>{
|
|
59
66
|
if(session.deleted_at!=null)return false;
|
|
67
|
+
if(!appGrantAllows({app_grants:appFence},'session',session.id,'sessions.read'))return false;
|
|
68
|
+
if(appFence!==null){
|
|
69
|
+
if(!grantSource)return false;
|
|
70
|
+
try{authorizedSession({teamId,principalId:grantSource.principal_id,credentialId:grantSource.id,ref:session.id,now,delegationDepth:(args.delegationDepth??0)+1,projectionDepth:1});}catch{return false;}
|
|
71
|
+
}
|
|
60
72
|
if(session.visibility==='team'||session.custodian_id===principalId)return true;
|
|
61
73
|
const grants=all('SELECT subject_kind,subject_id,expires_at FROM session_grants WHERE team_id=? AND session_id=?',[teamId,session.id]);
|
|
62
74
|
return grants.some(grant=>(grant.subject_kind==='principal'&&grant.subject_id===principalId||grant.subject_kind==='group'&&groups.includes(grant.subject_id))&&(grant.expires_at==null||grant.expires_at>now));
|
package/workspace-client.d.ts
CHANGED
|
@@ -49,11 +49,13 @@ export interface AppInstallation {
|
|
|
49
49
|
id:string; app_id:string; team_id:string; principal_id:string; installer_id:string;
|
|
50
50
|
actions:string[]; state:'active'|'uninstalled'; created_at:number; uninstalled_at:number|null;
|
|
51
51
|
}
|
|
52
|
-
export interface
|
|
52
|
+
export interface AppResourceGrant { kind:'session'; id:string; actions:('sessions.read'|'sessions.write'|'sessions.access')[] }
|
|
53
|
+
export interface AppDelegation { actions:string[]; actor:string; expires_in_ms?:number; principal_id?:string; grants?:AppResourceGrant[]; subject_token?:string }
|
|
54
|
+
export interface AppToken { grants?:AppResourceGrant[]|null; token:string; kind:'service'; expires_at:number; actions:string[]; subject:string; act:string[] }
|
|
53
55
|
export interface AppCodeExchange { version:1; installation:AppInstallation; credential:AppToken; webhook_secret:string }
|
|
54
56
|
export interface TeamClient {
|
|
55
|
-
apps:{catalogue(options?:RequestOptions):Promise<Result<{actions:string[];event_kinds:string[]}>>;list(options?:RequestOptions):Promise<Page<TeamsApp>>;get(id:string,options?:RequestOptions):Promise<Result<TeamsApp>>;register(body:RegisterTeamsApp,options?:RequestOptions):Promise<Result<TeamsApp & {secret?:string}>>;rotateSecret(id:string,options?:RequestOptions):Promise<Result<{id:string;secret?:string}>>;install(id:string,body:{redirect_url:string;state:string;actions?:string[]},options?:RequestOptions):Promise<Result<{installation:AppInstallation;code?:string;state:string;redirect_url:string;expires_at:number}>>};
|
|
56
|
-
appInstallations:{list(options?:RequestOptions):Promise<Page<AppInstallation>>;get(id:string,options?:RequestOptions):Promise<Result<AppInstallation>>;uninstall(id:string,options?:RequestOptions):Promise<null>;delegate(id:string,body:
|
|
57
|
+
apps:{deregister(id:string,options?:RequestOptions):Promise<null>;catalogue(options?:RequestOptions):Promise<Result<{actions:string[];event_kinds:string[]}>>;list(options?:RequestOptions):Promise<Page<TeamsApp>>;get(id:string,options?:RequestOptions):Promise<Result<TeamsApp>>;register(body:RegisterTeamsApp,options?:RequestOptions):Promise<Result<TeamsApp & {secret?:string}>>;rotateSecret(id:string,options?:RequestOptions):Promise<Result<{id:string;secret?:string}>>;install(id:string,body:{redirect_url:string;state:string;actions?:string[]},options?:RequestOptions):Promise<Result<{installation:AppInstallation;code?:string;state:string;redirect_url:string;expires_at:number}>>};
|
|
58
|
+
appInstallations:{list(options?:RequestOptions):Promise<Page<AppInstallation>>;get(id:string,options?:RequestOptions):Promise<Result<AppInstallation>>;uninstall(id:string,options?:RequestOptions):Promise<null>;delegate(id:string,body:AppDelegation,options?:RequestOptions):Promise<Result<AppToken|null>>};
|
|
57
59
|
sessions:{list(query?:ListQuery,options?:RequestOptions):Promise<Page<Descriptor>>;search(query:SearchQuery,options?:RequestOptions):Promise<SearchPage>;watch(query?:ListQuery,options?:StreamOptions):AsyncIterable<Event>;get(id:string):SessionClient};
|
|
58
60
|
members:{list(query?:Record<string,unknown>,options?:RequestOptions):Promise<Page<Membership>>;update(id:string,body:{role:Membership['role']},options?:RequestOptions):Promise<Result<Membership>>;remove(id:string,options?:RequestOptions):Promise<null>};
|
|
59
61
|
groups:{list(query?:Record<string,unknown>,options?:RequestOptions):Promise<Page<Group>>;create(body:{name:string},options?:RequestOptions):Promise<Result<Group>>;remove(id:string,options?:RequestOptions):Promise<null>;addMember(groupId:string,memberId:string,options?:RequestOptions):Promise<null>;removeMember(groupId:string,memberId:string,options?:RequestOptions):Promise<null>};
|
package/workspace-client.mjs
CHANGED
|
@@ -33,6 +33,7 @@ class TeamClient {
|
|
|
33
33
|
catalogue: o => get('/apps/catalogue', o),
|
|
34
34
|
list: o => get('/apps', o), get: (id, o) => get(resource('/apps', id), o),
|
|
35
35
|
register: (body, o) => post('/apps', body, o),
|
|
36
|
+
deregister: (id, o) => remove(resource('/apps', id), o),
|
|
36
37
|
rotateSecret: (id, o) => post(resource('/apps', id) + '/secret', {}, o),
|
|
37
38
|
install: (id, body, o) => post(resource('/apps', id) + '/installations', body, o),
|
|
38
39
|
};
|