@volter/teams 0.2.40 → 0.2.42

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
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/auth.mjs CHANGED
@@ -26,13 +26,19 @@ const LINEAGE_ROWS = `FROM lineage l JOIN credentials c ON c.id=l.id
26
26
  LEFT JOIN apps a ON a.id=i.app_id
27
27
  LEFT JOIN memberships installer ON installer.team_id=i.team_id AND installer.principal_id=i.installer_id
28
28
  LEFT JOIN memberships service ON service.team_id=i.team_id AND service.principal_id=i.principal_id`;
29
- export const APP_LINEAGE_REVOKED = `${APP_LINEAGE}, lineage_actors(team,id) AS (
29
+ // Materialize only this credential lineage's actors before checking their
30
+ // agency. When json_each was flattened, the hosted planner checked every
31
+ // agent in the team before matching actor.value (20,794 reads per event).
32
+ // CROSS JOIN keeps the small actor list on the left of the indexed agent
33
+ // lookup. Authority stays live: no predicate or result is cached.
34
+ export const APP_LINEAGE_REVOKED = `${APP_LINEAGE}, lineage_actors(team,id) AS MATERIALIZED (
30
35
  SELECT c.team_id,actor.value FROM lineage l JOIN credentials c ON c.id=l.id JOIN principals p ON p.id=c.principal_id
31
36
  JOIN app_credentials ac ON ac.credential_id=c.id, json_each(COALESCE(ac.actor_json,'[]')) actor WHERE actor.type='text')
32
37
  SELECT 1 FROM lineage l JOIN credentials c ON c.id=l.id JOIN principals p ON p.id=c.principal_id
33
38
  WHERE p.kind='agent' AND NOT EXISTS (${AGENT_ACTIVE.replace('VALUES(?,?)','SELECT c.team_id,c.principal_id')})
34
- UNION ALL SELECT 1 FROM lineage_actors x JOIN agents named ON named.team_id=x.team AND named.id=x.id
35
- WHERE NOT EXISTS (${AGENT_ACTIVE.replace('VALUES(?,?)','SELECT named.team_id,named.id')})
39
+ UNION ALL SELECT 1 FROM lineage_actors x CROSS JOIN agents named
40
+ WHERE named.team_id=x.team AND named.id=x.id
41
+ AND NOT EXISTS (${AGENT_ACTIVE.replace('VALUES(?,?)','SELECT named.team_id,named.id')})
36
42
  UNION ALL SELECT 1 ${LINEAGE_ROWS}
37
43
  WHERE EXISTS(SELECT 1 FROM agent_credentials d LEFT JOIN memberships dm ON dm.team_id=d.team_id AND dm.principal_id=c.principal_id WHERE d.parent_credential_id=c.id AND d.credential_id IN(SELECT id FROM lineage) AND COALESCE(dm.active,0)!=1)
38
44
  OR c.revoked_at IS NOT NULL OR (c.expires_at IS NOT NULL AND c.expires_at<=?)
package/server/http.mjs CHANGED
@@ -23,7 +23,21 @@ const parseJson=req=>parseRequestJson(req,fail);
23
23
  const result=(api,auth,data)=>({version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},data});
24
24
  function sideEffectStatements(api,res,status,body,now,targetId=null,journal=false){const statements=[];if(status<400&&res._idempotencyRecord){const record=res._idempotencyRecord,safe=redactSecretReplay(record.path,body);statements.push({sql:'INSERT INTO idempotency(team_id,principal_id,route,key,request_digest,response_status,response_json,expires_at) VALUES (?,?,?,?,?,?,?,?)',params:[record.teamId,record.principalId,record.route,record.key,record.requestDigest,status,journal?{$journalJsonValue:safe}:JSON.stringify(safe),now+86400000]});}if(res._auditRecord){const record=res._auditRecord;statements.push({sql:'INSERT INTO audit(team_id,id,actor_id,operation,target_id,outcome,revision,created_at,actor_json) VALUES (?,?,?,?,?,?,?,?,?)',params:[record.teamId,api.ids.next('audit'),record.actorId,record.operation,targetId,status<400?'allowed':'denied',journal?{$journalRevision:true}:null,now,JSON.stringify(record.act??[])]});}return statements;}
25
25
  async function persistSideEffects(api,res,status,body){const statements=sideEffectStatements(api,res,status,body,api.clock.nowMs());if(statements.length)await api.store.transaction(statements);}
26
- function send(res,status,body,requestId,headers={}){const deliver=()=>{res.writeHead(status,{'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store','X-Request-ID':requestId,...headers});res.end(body===null?'':JSON.stringify(body));};if(!res._sideEffectsCommitted&&res._api&&(res._idempotencyRecord||res._auditRecord))return persistSideEffects(res._api,res,status,body).then(deliver,error=>{if(/UNIQUE constraint failed: idempotency\./.test(error?.message??''))return deliver();if(res.headersSent)return;res.writeHead(500,{'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store','X-Request-ID':requestId});res.end(JSON.stringify({error:{code:'internal',message:'The request was not recorded.'}}));});deliver();}
26
+ function send(res,status,body,requestId,headers={}){
27
+ const deliver=(actualStatus=status,actualBody=body,actualHeaders=headers)=>{
28
+ // The host observes the response actually issued, including a refusal
29
+ // whose audit write failed. It decides which diagnostic fields are safe.
30
+ res._workResponse?.(actualStatus,actualBody);
31
+ res.writeHead(actualStatus,{'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store','X-Request-ID':requestId,...actualHeaders});
32
+ res.end(actualBody===null?'':JSON.stringify(actualBody));
33
+ };
34
+ if(!res._sideEffectsCommitted&&res._api&&(res._idempotencyRecord||res._auditRecord))return persistSideEffects(res._api,res,status,body).then(()=>deliver(),error=>{
35
+ if(/UNIQUE constraint failed: idempotency\./.test(error?.message??''))return deliver();
36
+ if(res.headersSent)return;
37
+ deliver(500,{error:{code:'internal',message:'The request was not recorded.'}},{});
38
+ });
39
+ deliver();
40
+ }
27
41
  async function guardedMutation(api,res,auth,guards,statements,{status,body,targetId=null,journal=false}){const now=api.clock.nowMs(),all=[...statements,...sideEffectStatements(api,res,status,body,now,targetId,journal)],result=journal?await api.store.journalTransaction(auth.team_id,[...guards,...appAuthGuards(auth,now)],all):await api.store.guardedTransaction([...guards,...appAuthGuards(auth,now)],all);res._sideEffectsCommitted=true;api.events.publish(auth.team_id,{type:'events_available'});return result;}
28
42
  const requireRevision=(req,current,label='resource')=>{const expected=Number(String(req.headers['if-match']??'').replaceAll('"',''));if(!Number.isSafeInteger(expected))fail(400,'invalid_request','If-Match is required');if(expected!==current)fail(409,'revision_mismatch',`${label} revision changed`);return expected;};
29
43
  const CORS={'Access-Control-Allow-Origin':'*','Access-Control-Expose-Headers':'ETag, X-Request-ID, Content-Range'};
@@ -11,6 +11,7 @@ const stmt = (sql, ...params) => ({sql, params});
11
11
  // A Durable Object binds at most 100 parameters to a statement, so id lists go in batches.
12
12
  const batches = (list, size = 90) => Array.from({length: Math.ceil(list.length / size)}, (_, at) => list.slice(at * size, (at + 1) * size));
13
13
  const holes = list => list.map(() => '?').join(',');
14
+ const EVENT_REPLAY_BATCH = 32;
14
15
  const fail = (status,code,message) => {throw Object.assign(new Error(message),{status,code});};
15
16
  const text = (value,label) => {if(typeof value!=='string'||!value||value.length>256||/[\x00-\x1f]/.test(value))fail(400,'invalid_request',`${label} must be a nonempty string of at most 256 characters`);return value;};
16
17
  const eventOf = row => ({id:row.id,org:row.team_id,at:row.created_at,actor:JSON.parse(row.actor_json),kind:row.kind,resource:{kind:row.resource_kind,id:row.resource_id},data:JSON.parse(row.data_json)});
@@ -179,8 +180,17 @@ async function eventStream({api,req,res,auth,url,requestId}){
179
180
  let closed=false,running=null,dirty=false;
180
181
  const close=()=>{if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);release();res.end();};
181
182
  const terminal=(type,reason)=>{if(!closed)writeSse(res,{type,reason,terminal:true});close();};
182
- const write=async event=>{if(closed)return false;const room=await sseRoom(res,Buffer.byteLength(JSON.stringify(event)));if(closed||room==='closed')return false;if(room==='slow'){terminal('resync_required','slow_reader');return false;}writeSse(res,event);return true;};
183
+ const detail=values=>api.workDetail?.(values);
184
+ // SQLite on the object resolves as microtasks. An await per event does not
185
+ // let another request or the response transport run. The hosted meter yields
186
+ // at the end of each page; the Node host needs the same timer boundary.
187
+ const page=async(name,work,values={})=>{
188
+ if(api.work)return api.work(`publishedEvents.${name}`,work,values);
189
+ try{return await work();}finally{await new Promise(resolve=>setTimeout(resolve,0));}
190
+ };
191
+ const write=async event=>{if(closed)return false;detail({stage:'write',queued_bytes:res.writableLength??0});const room=await sseRoom(res,Buffer.byteLength(JSON.stringify(event)));if(closed||room==='closed')return false;if(room==='slow'){terminal('resync_required','slow_reader');return false;}writeSse(res,event);return true;};
183
192
  const readable=async event=>{
193
+ detail({stage:'authority'});
184
194
  const current=await liveAuth(api,auth);
185
195
  if(current.epoch!==auth.epoch)fail(409,'resync_required','authorization changed during event delivery');
186
196
  auth=current;return mayReadEvent(api,auth,event);
@@ -196,25 +206,52 @@ async function eventStream({api,req,res,auth,url,requestId}){
196
206
  if(cursor!==null&&(cursor<bounds.floor||cursor>Math.max(bounds.last,bounds.floor))){terminal('resync_required','retention');return;}
197
207
  if(cursor===null){
198
208
  // Subscribe before reading the atomic snapshot. Replay after this watermark repairs all concurrent writes.
199
- const [head,resources]=await api.store.snapshot([
209
+ const [head,resources]=await page('snapshot',()=>api.store.snapshot([
200
210
  {kind:'one',sql:'SELECT COALESCE((SELECT max(sequence) FROM org_events WHERE team_id=?),(SELECT pruned_through FROM event_retention WHERE team_id=?),0) last',params:[auth.team_id,auth.team_id]},
201
211
  {kind:'query',sql:'SELECT r.sequence FROM event_resources r JOIN org_events e ON e.sequence=r.sequence WHERE r.team_id=? AND (? IS NULL OR r.source_id=?) AND (? IS NULL OR r.resource_kind IN (SELECT value FROM json_each(?)) OR e.kind IN (SELECT value FROM json_each(?))) ORDER BY r.source_id,r.resource_kind,r.resource_id',params:[auth.team_id,query.source,query.source,filterResources,filterResources,filterKinds]},
202
- ]);
212
+ ]),{stage:'query'});
203
213
  cursor=head.last;if(!await write({type:'snapshot_begin'}))return;
204
- for(let at=0;at<resources.length;at+=32){
205
- const sequences=resources.slice(at,at+32).map(row=>row.sequence);
206
- const rows=await api.store.query('SELECT * FROM org_events WHERE team_id=? AND sequence IN (SELECT value FROM json_each(?)) ORDER BY sequence',[auth.team_id,JSON.stringify(sequences)]);
207
- if(rows.length!==sequences.length){terminal('resync_required','snapshot_retention');return;}
208
- for(const row of rows){const event=eventOf(row);if(await readable(event))if(!await write({type:'snapshot_item',sequence:row.sequence,data:event}))return;}
214
+ for(let at=0;at<resources.length&&!closed;at+=EVENT_REPLAY_BATCH){
215
+ const sequences=resources.slice(at,at+EVENT_REPLAY_BATCH).map(row=>row.sequence);
216
+ const complete=await page('snapshotPage',async()=>{
217
+ const rows=await api.store.query('SELECT * FROM org_events WHERE team_id=? AND sequence IN (SELECT value FROM json_each(?)) ORDER BY sequence',[auth.team_id,JSON.stringify(sequences)]);
218
+ if(rows.length!==sequences.length){terminal('resync_required','snapshot_retention');return false;}
219
+ let considered=0,delivered=0;
220
+ for(const row of rows){
221
+ if(closed)return false;
222
+ const event=eventOf(row);considered++;detail({events_considered:considered});
223
+ if(await readable(event)){if(!await write({type:'snapshot_item',sequence:row.sequence,data:event}))return false;delivered++;detail({events_delivered:delivered});}
224
+ }
225
+ detail({stage:'complete'});return true;
226
+ },{stage:'query',events_considered:0,events_delivered:0,snapshot_resources:resources.length,page_offset:at});
227
+ if(!complete)return;
209
228
  }
210
229
  if(!await write({type:'snapshot_end',cursor:codec.encode(cursor)}))return;
211
230
  }
212
- for(;;){
213
- const rows=await api.store.query('SELECT * FROM org_events WHERE team_id=? AND sequence>? AND (? IS NULL OR source_id=?) AND (? IS NULL OR kind IN (SELECT value FROM json_each(?))) ORDER BY sequence LIMIT 128',[auth.team_id,cursor,query.source,query.source,filterKinds,filterKinds]);
214
- if(!rows.length){cursor=Math.max(cursor,bounds.last);await write({type:'checkpoint',cursor:codec.encode(cursor)});break;}
215
- for(const row of rows){if(closed)return;cursor=row.sequence;const event=eventOf(row);if(await readable(event)&&!await write({type:'event',sequence:row.sequence,cursor:codec.encode(cursor),data:event}))return;}
216
- // Advance even when the reader was not authorized for a resource; never leak its data.
217
- if(!await write({type:'checkpoint',cursor:codec.encode(cursor)}))return;if(rows.length<128)break;
231
+ // Finish one observed watermark rather than chasing concurrent publishers
232
+ // forever. The subscribed dirty flag schedules the next watermark.
233
+ const through=Math.max(bounds.last,cursor);
234
+ while(!closed){
235
+ const state=await page('replayPage',async()=>{
236
+ // Retention can run during the preceding page's timer yield. Read its
237
+ // floor and this page in one snapshot before advancing the cursor,
238
+ // including when filters leave an empty page.
239
+ const [retention,rows]=await api.store.snapshot([
240
+ {kind:'one',sql:'SELECT COALESCE((SELECT pruned_through FROM event_retention WHERE team_id=?),0) floor',params:[auth.team_id]},
241
+ {kind:'query',sql:`SELECT * FROM org_events WHERE team_id=? AND sequence>? AND sequence<=? AND (? IS NULL OR source_id=?) AND (? IS NULL OR kind IN (SELECT value FROM json_each(?))) ORDER BY sequence LIMIT ${EVENT_REPLAY_BATCH}`,params:[auth.team_id,cursor,through,query.source,query.source,filterKinds,filterKinds]},
242
+ ]);
243
+ if(cursor<retention.floor){terminal('resync_required','retention');return 'closed';}
244
+ if(!rows.length){cursor=through;await write({type:'checkpoint',cursor:codec.encode(cursor)});detail({stage:'complete'});return 'done';}
245
+ let considered=0,delivered=0;
246
+ for(const row of rows){
247
+ if(closed)return 'closed';cursor=row.sequence;const event=eventOf(row);considered++;detail({events_considered:considered});
248
+ if(await readable(event)){if(!await write({type:'event',sequence:row.sequence,cursor:codec.encode(cursor),data:event}))return 'closed';delivered++;detail({events_delivered:delivered});}
249
+ }
250
+ // Advance even when the reader was not authorized for a resource; never leak its data.
251
+ if(!await write({type:'checkpoint',cursor:codec.encode(cursor)}))return 'closed';
252
+ detail({stage:'complete'});return rows.length<EVENT_REPLAY_BATCH?'done':'more';
253
+ },{stage:'query',events_considered:0,events_delivered:0});
254
+ if(state!=='more')break;
218
255
  }
219
256
  };
220
257
  const enqueue=()=>{dirty=true;if(running)return running;running=(async()=>{while(dirty&&!closed){dirty=false;await drain();}})().catch(error=>terminal([401,403].includes(error.status)?'access_revoked':'resync_required',error.code??'unavailable')).finally(()=>{running=null;});return running;};