@volter/supercode-teams 0.2.25 → 0.2.26
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/args.mjs +1 -0
- package/bin/workspace.mjs +10 -1
- package/package.json +1 -1
- package/server/event-routes.mjs +1 -1
- package/server/harness-routes.mjs +38 -2
- package/server/store-core.mjs +20 -8
package/bin/workspace/args.mjs
CHANGED
|
@@ -11,6 +11,7 @@ const VALUES = new Set([
|
|
|
11
11
|
'out', 'ttl', 'type', 'name', 'visibility', 'backfill', 'format', 'to',
|
|
12
12
|
'grant', 'binding', 'caps', 'fidelity', 'cwd', 'root', 'workspace-from', 'since', 'until', 'session',
|
|
13
13
|
'on', 'timeout', 'plan', 'tmux-socket', 'volter-issuer', 'delegated-issuer', 'workplace', 'organization', 'key', 'room',
|
|
14
|
+
'conversation', 'rh2',
|
|
14
15
|
]);
|
|
15
16
|
|
|
16
17
|
export function parseArgs(argv) {
|
package/bin/workspace.mjs
CHANGED
|
@@ -299,6 +299,15 @@ async function sessionsCommand(sub, command) {
|
|
|
299
299
|
if (sub === 'export') return exportSession(session, command);
|
|
300
300
|
if (sub === 'continue') return continueSession(session, command);
|
|
301
301
|
if (sub === 'url') { only(flags, ['json', 'help', 'context']); requireArgs(rest, 1, 1, 'sessions url REF'); const descriptor = await session.descriptor(); return output({ version: 1, context: scope(context), data: { url: `${context.origin}/teams/${context.team_id}/sessions/${descriptor.data.id}` } }); }
|
|
302
|
+
// A session view (RH2 ADR 0008): its owner lets a workplace Room's Conversation read this session with the tokens that
|
|
303
|
+
// workplace signs. `bind` records it and prints what the workplace's own bind door takes; `unbind` removes it.
|
|
304
|
+
if (sub === 'bind' || sub === 'unbind') {
|
|
305
|
+
only(flags, ['json', 'help', 'context', 'room', 'conversation', 'rh2']); requireArgs(rest, 1, 1, `sessions ${sub} REF --room ROOM --conversation CONVERSATION --rh2 ORIGIN`);
|
|
306
|
+
if (!flags.room || !flags.conversation || !flags.rh2) usage(`sessions ${sub} needs --room, --conversation and --rh2 (the workplace's origin)`);
|
|
307
|
+
const body = { issuer: new URL(String(flags.rh2)).origin, room: String(flags.room), conversation: String(flags.conversation) };
|
|
308
|
+
const bound = await command.client.request(sub === 'bind' ? 'POST' : 'DELETE', `${session.path}/view-bindings`, { body });
|
|
309
|
+
return output({ version: 1, context: scope(context), data: { teamsUrl: new URL(context.origin).origin, team: context.team_id, sessionId: bound.data.session_id, harness: bound.data.harness, principalId: context.principal_id, bindings: bound.data.bindings } });
|
|
310
|
+
}
|
|
302
311
|
if (sub === 'rename') { only(flags, ['json', 'help', 'context']); requireArgs(rest, 2, 2, 'sessions rename REF TITLE'); const descriptor = await session.descriptor(); return output(await command.client.request('PATCH', session.path, { body: { title: rest[1] }, ifMatch: descriptor.data.revision })); }
|
|
303
312
|
if (sub === 'delete') { only(flags, ['json', 'help', 'context']); requireArgs(rest, 1, 1, 'sessions delete REF'); const descriptor = await session.descriptor(); await command.client.request('DELETE', session.path, { ifMatch: descriptor.data.revision }); return output({ version: 1, context: scope(context), data: null }); }
|
|
304
313
|
unsupported(`sessions ${sub} is reserved for a later arc`);
|
|
@@ -860,7 +869,7 @@ function printHelp() {
|
|
|
860
869
|
sources create|list|status|pause|revoke|credentials|config
|
|
861
870
|
machines list|roots|access|authorize-continuation
|
|
862
871
|
sync add|list|show|pause|resume|remove
|
|
863
|
-
sessions list|search|show|follow|access|rename|delete|url|attach|export|continue
|
|
872
|
+
sessions list|search|show|follow|access|rename|delete|url|attach|export|continue|bind|unbind
|
|
864
873
|
Use --help with a command for its required arguments and options.
|
|
865
874
|
`);
|
|
866
875
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
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
|
@@ -63,7 +63,7 @@ function streamQueue(stream, work) {
|
|
|
63
63
|
|
|
64
64
|
const sessionArgs = (api, auth, ref) => ({
|
|
65
65
|
teamId: auth.team_id, principalId: auth.principal_id,
|
|
66
|
-
credentialId: auth.credential_id, ref, now: api.clock.nowMs(),
|
|
66
|
+
credentialId: auth.credential_id, ref, now: api.clock.nowMs(), bound: auth.bound,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
async function readForEvent(api, auth, ref, tail) {
|
|
@@ -12,6 +12,7 @@ import { normalizeSearch, textFromContent, cursorHash, cursorToken } from './cat
|
|
|
12
12
|
import { captureDigest, captureMessages, handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
|
|
13
13
|
import { handleCatalogEvents, handleSessionEvents } from './event-routes.mjs';
|
|
14
14
|
import { exact } from '@volter/teams/server/request-json';
|
|
15
|
+
import { delegatedSessionOf, looksDelegated } from '@volter/teams/server/delegated';
|
|
15
16
|
import { renderSourceCollectorConfig } from './otlp/collector-config.mjs';
|
|
16
17
|
import { attachmentActionRoute, createAttachment, liveGrantRoute, machineEffectRoute } from './effects.mjs';
|
|
17
18
|
import { exportSessionRoute, operationRoute } from './exports.mjs';
|
|
@@ -21,6 +22,20 @@ import { fail, parseJson, result, send, guardedMutation, requireRevision, checkR
|
|
|
21
22
|
|
|
22
23
|
async function preAuth({ api, req, res, path, requestId }) {
|
|
23
24
|
let early;
|
|
25
|
+
// A session view (RH2 ADR 0008): a workplace page reads one session with a token its workplace signed for a Room and
|
|
26
|
+
// Conversation, which reaches only a session its owner bound to them (session_view_bindings). A member's own
|
|
27
|
+
// credential takes the ordinary routes below.
|
|
28
|
+
if(req.method==='GET'&&(early=/^\/api\/v1\/teams\/([^/]+)\/sessions\/([^/]+)(\/messages|\/events)?$/.exec(path))&&looksDelegated(bearer(req.headers.authorization))){
|
|
29
|
+
const access=await delegatedSessionOf(api,bearer(req.headers.authorization));
|
|
30
|
+
const ref=decodeURIComponent(early[2]);
|
|
31
|
+
if(!access||access.teamId!==early[1]||access.sessionRef!==ref)fail(401,'authentication_required','authentication required');
|
|
32
|
+
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}};
|
|
33
|
+
const url=new URL(req.url,'http://local');
|
|
34
|
+
if(!early[3])await sessionDescriptor(api,res,auth,ref,requestId);
|
|
35
|
+
else if(early[3]==='/messages')await readSession(api,res,auth,ref,url,requestId);
|
|
36
|
+
else await handleSessionEvents({api,req,res,auth,id:ref,url,requestId,fail});
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
24
39
|
if(req.method==='GET'&&(early=/^\/api\/v1\/teams\/([^/]+)\/attachments\/([^/]+)\/stream$/.exec(path))){const token=bearer(req.headers.authorization);checkRate(api,`attachment:${hashToken(token??'')}`,120,30);await attachmentStreamRoute({api,req,res,teamId:early[1],attachmentId:early[2],token,fail});return true;}
|
|
25
40
|
return false;
|
|
26
41
|
}
|
|
@@ -52,6 +67,7 @@ async function team({ api, req, res, auth, url, path, sub, requestId }) {
|
|
|
52
67
|
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;}
|
|
53
68
|
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;}
|
|
54
69
|
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;}
|
|
70
|
+
if(['GET','POST','DELETE'].includes(req.method)&&(m=/^\/sessions\/([^/]+)\/view-bindings$/.exec(sub))){await viewBindingRoute(api,req,res,auth,m[1],requestId);return true;}
|
|
55
71
|
if((m=/^\/sessions\/([^/]+)\/access(?:\/([^/]+))?$/.exec(sub))){await accessRoute(api,req,res,auth,m[1],m[2],requestId);return true;}
|
|
56
72
|
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;}
|
|
57
73
|
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;}
|
|
@@ -146,7 +162,7 @@ async function listSessions(api,res,auth,url,requestId,search){
|
|
|
146
162
|
send(res,200,body,requestId);
|
|
147
163
|
}
|
|
148
164
|
async function sessionDescriptor(api,res,auth,id,requestId){
|
|
149
|
-
const row=await api.store.authorizedSession({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,now:api.clock.nowMs()});
|
|
165
|
+
const row=await api.store.authorizedSession({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,now:api.clock.nowMs(),bound:auth.bound});
|
|
150
166
|
send(res,200,result(api,auth,JSON.parse(row.descriptor_json)),requestId,{ETag:`"${row.revision}"`});
|
|
151
167
|
}
|
|
152
168
|
async function correlateRoute(api,req,res,auth,path,requestId){
|
|
@@ -168,7 +184,7 @@ async function readSession(api,res,auth,id,url,requestId){
|
|
|
168
184
|
const tail=query.tail??100;
|
|
169
185
|
if(query.before&&query.revision!==undefined)fail(400,'invalid_request','revision cannot be combined with before');
|
|
170
186
|
const nextCursor=cursorToken();
|
|
171
|
-
const snapshot=await api.store.readSessionSnapshot({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,now:api.clock.nowMs(),beforeHash:query.before?cursorHash(query.before):null,requestedRevision:query.revision,tail,nextCursorHash:cursorHash(nextCursor),nextCursorToken:nextCursor});
|
|
187
|
+
const snapshot=await api.store.readSessionSnapshot({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,now:api.clock.nowMs(),bound:auth.bound,beforeHash:query.before?cursorHash(query.before):null,requestedRevision:query.revision,tail,nextCursorHash:cursorHash(nextCursor),nextCursorToken:nextCursor});
|
|
172
188
|
const {rows,beforeCursor,revision}=snapshot;
|
|
173
189
|
const messages=rows.map(row=>({id:row.id,source_id:row.source_id,native_key:row.native_key,recorded_at:row.recorded_at,value:JSON.parse(row.value_json)}));
|
|
174
190
|
send(res,200,{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},descriptor:JSON.parse(snapshot.descriptor_json),messages,before_cursor:beforeCursor,event_cursor:snapshot.eventCursor,revision,display:{returned:messages.length,total:snapshot.total,truncated_fields:0}},requestId);
|
|
@@ -246,3 +262,23 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
|
|
|
246
262
|
statements.push({sql:'INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,{$journalJsonValue:descriptor},JSON.stringify({source_id:sourceId}),normalizeSearch(commit.title??''),sortAt,boundaryNow]});let ordinal=0;for(const m of messages){let mid=(await api.store.one('SELECT id FROM message_ids WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND native_key=?',[auth.team_id,sourceId,cap.harness,cap.native_id,m.native_key]))?.id;if(!mid){mid=api.ids.next('message');statements.push({sql:'INSERT INTO message_ids(team_id,source_id,harness,native_id,native_key,id) VALUES (?,?,?,?,?,?)',params:[auth.team_id,sourceId,cap.harness,cap.native_id,m.native_key,mid]});}statements.push({sql:'INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text) VALUES (?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,ordinal++,mid,sourceId,m.native_key,m.recorded_at,JSON.stringify(m.value),normalizeSearch(textFromContent(m.value.content))]});}
|
|
247
263
|
const bundle=commit.native_bundle??null,legacyBundle=commit.artifacts.length?captureId:null;for(let index=0;index<commit.artifacts.length;index++){const a=commit.artifacts[index],file=bundle?.files.find(x=>x.upload_id===a.upload_id);statements.push({sql:'UPDATE uploads SET manifest_json=?,committed=1 WHERE team_id=? AND id=?',params:[JSON.stringify(a),auth.team_id,a.upload_id]},{sql:'INSERT INTO session_artifacts(team_id,session_id,revision,bundle_id,upload_id,format,provenance,file_role,fidelity,residue_json) VALUES (?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,bundle?.id??legacyBundle,a.upload_id,bundle?.harness??cap.harness,'capture',file?.role??(index===0?'primary':'bundle'),bundle?.fidelity??'semantic',JSON.stringify(bundle?.residue??['native_bundle_metadata_unavailable'])]});}const storedReceipt={source_id:sourceId,capture_id:captureId,session_id:sid,generation:cap.generation,sequence:cap.sequence,digest:commit.digest,revision:'__JOURNAL_REVISION__',duplicate:false};statements.push({sql:"UPDATE captures SET state='committed',commit_json=?,receipt_json=? WHERE team_id=? AND id=?",params:[JSON.stringify(commit),{$journalJsonValue:storedReceipt},auth.team_id,captureId]},{sql:'UPDATE sources SET last_received_at=? WHERE team_id=? AND id=?',params:[boundaryNow,auth.team_id,sourceId]},{sql:'UPDATE teams SET journal_revision=? WHERE id=?',params:[revisionParam,auth.team_id]},{sql:'INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',params:[auth.team_id,revisionParam,sid,'capture',boundaryNow]});const guards=[{sql:'SELECT 1 ok FROM credentials WHERE id=? AND source_id=? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)',params:[auth.credential_id,sourceId,boundaryNow],code:'authentication_required',status:401,message:'source credential expired or revoked'},...lineageGuards,{sql:"SELECT 1 ok FROM captures c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id LEFT JOIN machines m ON m.team_id=s.team_id AND m.id=s.machine_id LEFT JOIN sync_rules r ON r.team_id=c.team_id AND r.id=json_extract(c.begin_json,'$.rule_id') WHERE c.team_id=? AND c.id=? AND c.source_id=? AND c.state='staging' AND s.state='active' AND (s.kind!='connector' OR (m.revoked_at IS NULL AND r.machine_id=m.id AND r.source_id=s.id AND r.state='active' AND r.revision=json_extract(c.begin_json,'$.rule_revision') AND r.visibility=json_extract(c.begin_json,'$.visibility')))",params:[auth.team_id,captureId,sourceId],code:'conflict',status:409,message:'capture authorization changed'}],storedResponse=result(api,auth,storedReceipt),{revision}=await guardedMutation(api,res,auth,guards,statements,{status:200,body:storedResponse,targetId:captureId,journal:true}),receipt={...storedReceipt,revision};api.events.publish(auth.team_id,{type:'session_changed',cursor:String(revision),revision,session_id:sid,reason:'capture',read_required:true});send(res,200,result(api,auth,receipt),requestId);
|
|
248
264
|
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* A session's views (RH2 ADR 0008): its owner lets one workplace Room's Conversation read it, and that workplace's
|
|
268
|
+
* signed tokens then reach this session and nothing else (preAuth). GET lists them; POST adds one; DELETE removes one.
|
|
269
|
+
*/
|
|
270
|
+
async function viewBindingRoute(api,req,res,auth,ref,requestId){
|
|
271
|
+
if(!['user','service'].includes(auth.kind))fail(403,'forbidden','credential cannot bind sessions');
|
|
272
|
+
const session=await api.store.authorizedSession({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref,now:api.clock.nowMs()});
|
|
273
|
+
const list=async()=>api.store.query('SELECT issuer,room,conversation,bound_by,created_at FROM session_view_bindings WHERE team_id=? AND session_id=? ORDER BY created_at',[auth.team_id,session.id]);
|
|
274
|
+
if(req.method==='GET'){send(res,200,result(api,auth,{session_id:session.id,harness:session.harness,bindings:await list()}),requestId);return;}
|
|
275
|
+
if(session.custodian_id!==auth.principal_id)fail(403,'forbidden',"only the session's owner binds it to a view");
|
|
276
|
+
const body=await parseJson(req);exact(body,['issuer','room','conversation']);
|
|
277
|
+
if(typeof body.room!=='string'||!body.room||typeof body.conversation!=='string'||!body.conversation)fail(400,'invalid_request','room and conversation are required');
|
|
278
|
+
if(!api.delegatedIssuer||body.issuer!==api.delegatedIssuer)fail(400,'invalid_request',`this server accepts views signed by ${api.delegatedIssuer??'no workplace'}`);
|
|
279
|
+
const key=[auth.team_id,session.id,body.issuer,body.room,body.conversation];
|
|
280
|
+
await api.store.transaction(req.method==='POST'
|
|
281
|
+
?[{sql:'INSERT OR IGNORE INTO session_view_bindings(team_id,session_id,issuer,room,conversation,bound_by,created_at) VALUES (?,?,?,?,?,?,?)',params:[...key,auth.principal_id,api.clock.nowMs()]}]
|
|
282
|
+
:[{sql:'DELETE FROM session_view_bindings WHERE team_id=? AND session_id=? AND issuer=? AND room=? AND conversation=?',params:key}]);
|
|
283
|
+
send(res,200,result(api,auth,{session_id:session.id,harness:session.harness,bindings:await list()}),requestId);
|
|
284
|
+
}
|
package/server/store-core.mjs
CHANGED
|
@@ -22,20 +22,30 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
22
22
|
const journalParam=(value,revision)=>value&&typeof value==='object'&&value.$journalRevision===true?revision:value&&typeof value==='object'&&Object.hasOwn(value,'$journalJson')?JSON.stringify({...value.$journalJson,revision}):value&&typeof value==='object'&&Object.hasOwn(value,'$journalJsonValue')?JSON.stringify(journalValue(value.$journalJsonValue,revision)):value;
|
|
23
23
|
const domainError=(status,code,message)=>{const error=new Error(message);error.status=status;error.code=code;throw error;};
|
|
24
24
|
const authorizedSession=(args)=>{
|
|
25
|
-
const {teamId,principalId,credentialId,ref,now}=args;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const {teamId,principalId,credentialId,ref,now,bound}=args;
|
|
26
|
+
// `bound`: a workplace's session-view token ({issuer, room, conversation, expiresAt}), which reads only a
|
|
27
|
+
// session its owner bound to that Room and Conversation; it holds no credential and is no member.
|
|
28
|
+
let membership=null;
|
|
29
|
+
if(bound){
|
|
30
|
+
if(!(bound.expiresAt>now))domainError(401,'authentication_required','authentication required');
|
|
31
|
+
}else{
|
|
32
|
+
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]);
|
|
33
|
+
membership=get('SELECT role FROM memberships WHERE team_id=? AND principal_id=? AND active=1',[teamId,principalId]);
|
|
34
|
+
if(!credential||!membership)domainError(401,'authentication_required','authentication required');
|
|
35
|
+
}
|
|
29
36
|
if(typeof ref!=='string'||ref.length<8)domainError(404,'not_found','not found');
|
|
30
37
|
const groups=all('SELECT group_id FROM group_members WHERE team_id=? AND principal_id=?',[teamId,principalId]).map(row=>row.group_id);
|
|
31
|
-
|
|
38
|
+
// A session is named by its id (or a prefix of it) or by the harness's own id for it (a native
|
|
39
|
+
// session id such as a Claude Code session's uuid), which its origin records.
|
|
40
|
+
let candidates=all('SELECT * FROM sessions WHERE team_id=? AND id LIKE ? AND deleted_at IS NULL',[teamId,`${ref}%`]);
|
|
41
|
+
if(!candidates.length)candidates=all('SELECT DISTINCT s.* FROM sessions s JOIN origins o ON o.team_id=s.team_id AND o.session_id=s.id WHERE s.team_id=? AND o.native_id=? AND o.deleted_at IS NULL AND s.deleted_at IS NULL',[teamId,ref]);
|
|
32
42
|
const visible=new Map();
|
|
33
43
|
for(const candidate of candidates){
|
|
34
44
|
let root=candidate;const seen=new Set();
|
|
35
45
|
while(root.canonical_id){if(seen.has(root.id))domainError(409,'conflict','correlation cycle');seen.add(root.id);root=get('SELECT * FROM sessions WHERE team_id=? AND id=? AND deleted_at IS NULL',[teamId,root.canonical_id]);if(!root)break;}
|
|
36
46
|
if(!root)continue;
|
|
37
47
|
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]);
|
|
38
|
-
const readable=family.every(session=>{
|
|
48
|
+
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=>{
|
|
39
49
|
if(session.deleted_at!=null)return false;
|
|
40
50
|
if(session.visibility==='team'||session.custodian_id===principalId)return true;
|
|
41
51
|
const grants=all('SELECT subject_kind,subject_id,expires_at FROM session_grants WHERE team_id=? AND session_id=?',[teamId,session.id]);
|
|
@@ -57,8 +67,10 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
57
67
|
return {session:{...session,descriptor_json:JSON.stringify(projection)},membership};
|
|
58
68
|
};
|
|
59
69
|
const readSessionSnapshot=args=>tx(()=>{
|
|
60
|
-
const {teamId,
|
|
61
|
-
const {session}=authorizedSession(args);
|
|
70
|
+
const {teamId,ref,now,beforeHash,requestedRevision,tail,nextCursorHash,nextCursorToken}=args;
|
|
71
|
+
const {session}=authorizedSession(args);
|
|
72
|
+
// A bound view's history cursors are kept under the session's owner, the member it reads for.
|
|
73
|
+
const principalId=args.bound?session.custodian_id:args.principalId;const team=get('SELECT auth_epoch,journal_revision FROM teams WHERE id=?',[teamId]);
|
|
62
74
|
let revision,ordinals;
|
|
63
75
|
if(beforeHash){
|
|
64
76
|
const cursor=get("SELECT * FROM cursors WHERE id=? AND team_id=? AND principal_id=? AND kind='history'",[beforeHash,teamId,principalId]);
|