@volter/supercode-teams 0.2.162 → 0.2.163
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/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.163",
|
|
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",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { MESSAGE_AT, OPEN_REVISION } from '@volter/teams/server/migrations';
|
|
3
|
+
import { normalizeSearch, textFromContent } from './catalog.mjs';
|
|
4
|
+
|
|
5
|
+
export function applyCaptureMessages({ get, run }, capture, revision) {
|
|
6
|
+
const { teamId, sessionId, sourceId, captureId, harness, nativeId, currentRevision, carriedCount, parts, messageNamespace } = capture;
|
|
7
|
+
let ordinal = carriedCount;
|
|
8
|
+
for (let index = 0; index < parts; index++) {
|
|
9
|
+
const heldPart = get('SELECT payload_json FROM capture_parts WHERE team_id=? AND capture_id=? AND part_index=?', [teamId, captureId, index]);
|
|
10
|
+
if (!heldPart) throw Object.assign(new Error('capture parts incomplete'), { status: 409, code: 'batch_gap' });
|
|
11
|
+
const part = JSON.parse(heldPart.payload_json);
|
|
12
|
+
for (const message of part.messages) {
|
|
13
|
+
const value = JSON.stringify(message.value);
|
|
14
|
+
const old = currentRevision == null ? null : get(`SELECT revision,source_id,native_key,recorded_at,value_json,until_revision FROM messages m WHERE m.team_id=? AND m.session_id=? AND m.ordinal=? AND ${MESSAGE_AT()} LIMIT 1`, [teamId, sessionId, ordinal, currentRevision, currentRevision]);
|
|
15
|
+
if (old && old.source_id === sourceId && old.native_key === message.native_key && old.recorded_at === message.recorded_at && old.value_json === value) {
|
|
16
|
+
if (old.until_revision !== OPEN_REVISION) run('UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND revision=? AND ordinal=?', [OPEN_REVISION, teamId, sessionId, old.revision, ordinal]);
|
|
17
|
+
ordinal++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (old?.until_revision === OPEN_REVISION) run('UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND revision=? AND ordinal=?', [revision, teamId, sessionId, old.revision, ordinal]);
|
|
21
|
+
let messageId = get('SELECT id FROM message_ids WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND native_key=?', [teamId, sourceId, harness, nativeId, message.native_key])?.id;
|
|
22
|
+
if (!messageId) {
|
|
23
|
+
messageId = `msg_${createHash('sha256').update(`${messageNamespace}\0${message.native_key}`).digest('hex').slice(0, 32)}`;
|
|
24
|
+
run('INSERT INTO message_ids(team_id,source_id,harness,native_id,native_key,id) VALUES (?,?,?,?,?,?)', [teamId, sourceId, harness, nativeId, message.native_key, messageId]);
|
|
25
|
+
}
|
|
26
|
+
run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text,until_revision) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [teamId, sessionId, revision, ordinal++, messageId, sourceId, message.native_key, message.recorded_at, value, normalizeSearch(textFromContent(message.value.content)), OPEN_REVISION]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (currentRevision != null) run('UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND ordinal>=? AND revision<=? AND until_revision=?', [revision, teamId, sessionId, ordinal, currentRevision, OPEN_REVISION]);
|
|
30
|
+
return { messages: ordinal - carriedCount };
|
|
31
|
+
}
|
|
@@ -10,8 +10,8 @@ import { createHash } from 'node:crypto';
|
|
|
10
10
|
import { bearer, hashToken, canRead, canManage, isAdmin } from '@volter/teams/server/auth';
|
|
11
11
|
import { storageReport } from './storage-report.mjs';
|
|
12
12
|
import { validateListQuery, validateSearchQuery, validateReadQuery, validateCaptureBegin, validateCapturePart, canonicalJson } from '../contracts/index.mjs';
|
|
13
|
-
import { normalizeSearch,
|
|
14
|
-
import {
|
|
13
|
+
import { normalizeSearch, cursorHash, cursorToken } from './catalog.mjs';
|
|
14
|
+
import { handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
|
|
15
15
|
import { handleCatalogEvents, handleSessionEvents, sessionNames } from './event-routes.mjs';
|
|
16
16
|
import { exact } from '@volter/teams/server/request-json';
|
|
17
17
|
import { MESSAGE_AT, OPEN_REVISION } from '@volter/teams/server/migrations';
|
|
@@ -328,7 +328,26 @@ async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId)
|
|
|
328
328
|
],{status:200,body:response,targetId:uploadId});
|
|
329
329
|
send(res,200,response,requestId);
|
|
330
330
|
}
|
|
331
|
-
async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){const source=await sourceAuth(api,auth,sourceId);const commit=await parseJson(req);exact(commit,['capture_id','parts','digest','title','repository','reported_user','recorded_activity_at','parent_native_id','coverage','artifacts','native_bundle']);if(commit.capture_id!==captureId||!Number.isSafeInteger(commit.parts)||commit.parts<1)fail(400,'invalid_request','invalid commit');const cap=await api.store.one('SELECT * FROM captures WHERE team_id=? AND id=? AND source_id=?',[auth.team_id,captureId,sourceId]);if(await expireStaging(api,auth.team_id,sourceId,captureId,'captures',cap))fail(409,'resync_required','capture reservation expired');if(!cap)fail(404,'not_found','capture not found');if(cap.state==='committed'){const receipt=JSON.parse(cap.receipt_json);if(receipt.digest!==commit.digest)fail(409,'capture_conflict','commit replay differs');receipt.duplicate=true;return send(res,200,result(api,auth,receipt),requestId);}const
|
|
331
|
+
async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){const source=await sourceAuth(api,auth,sourceId);const commit=await parseJson(req);exact(commit,['capture_id','parts','digest','title','repository','reported_user','recorded_activity_at','parent_native_id','coverage','artifacts','native_bundle']);if(commit.capture_id!==captureId||!Number.isSafeInteger(commit.parts)||commit.parts<1)fail(400,'invalid_request','invalid commit');const cap=await api.store.one('SELECT * FROM captures WHERE team_id=? AND id=? AND source_id=?',[auth.team_id,captureId,sourceId]);if(await expireStaging(api,auth.team_id,sourceId,captureId,'captures',cap))fail(409,'resync_required','capture reservation expired');if(!cap)fail(404,'not_found','capture not found');if(cap.state==='committed'){const receipt=JSON.parse(cap.receipt_json);if(receipt.digest!==commit.digest)fail(409,'capture_conflict','commit replay differs');receipt.duplicate=true;return send(res,200,result(api,auth,receipt),requestId);}const begin=JSON.parse(cap.begin_json);if(source.kind==='connector'){const rule=await api.store.one('SELECT * FROM sync_rules WHERE team_id=? AND id=? AND source_id=? AND machine_id=?',[auth.team_id,begin.rule_id,sourceId,auth.machine_id]);if(!rule||rule.state!=='active'||rule.revision!==begin.rule_revision||rule.visibility!==begin.visibility)fail(rule?.state==='paused'?409:403,rule?.state==='paused'?'source_paused':'forbidden','sync rule changed before commit');}
|
|
332
|
+
const heldParts = await api.store.one('SELECT COUNT(*) count,MIN(part_index) first,MAX(part_index) last FROM capture_parts WHERE team_id=? AND capture_id=?', [auth.team_id, captureId]);
|
|
333
|
+
if (heldParts.count !== commit.parts || heldParts.first !== 0 || heldParts.last !== commit.parts - 1) fail(409, 'batch_gap', 'capture parts incomplete');
|
|
334
|
+
const unsignedCommit = { ...commit };
|
|
335
|
+
delete unsignedCommit.digest;
|
|
336
|
+
const captureHash = createHash('sha256').update(`{"begin":${canonicalJson(begin)},"commit":${canonicalJson(unsignedCommit)},"parts":[`);
|
|
337
|
+
const sentKeys = new Set();
|
|
338
|
+
let messageCount = 0;
|
|
339
|
+
for (let index = 0; index < commit.parts; index++) {
|
|
340
|
+
const part = await api.store.one('SELECT payload_json FROM capture_parts WHERE team_id=? AND capture_id=? AND part_index=?', [auth.team_id, captureId, index]);
|
|
341
|
+
if (!part) fail(409, 'batch_gap', 'capture parts incomplete');
|
|
342
|
+
if (index) captureHash.update(',');
|
|
343
|
+
captureHash.update(part.payload_json);
|
|
344
|
+
for (const message of JSON.parse(part.payload_json).messages) {
|
|
345
|
+
if (sentKeys.has(message.native_key)) fail(409, 'capture_conflict', 'duplicate native message key');
|
|
346
|
+
sentKeys.add(message.native_key);
|
|
347
|
+
if (++messageCount > 100000) fail(413, 'request_too_large', 'capture contains too many messages');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (captureHash.update(']}').digest('hex') !== commit.digest) fail(409, 'capture_conflict', 'capture digest differs');
|
|
332
351
|
// Native transcripts can be larger than the Worker's entire heap. Validate
|
|
333
352
|
// the retained bytes one chunk at a time; never materialize a whole artifact.
|
|
334
353
|
for (const manifest of commit.artifacts) {
|
|
@@ -358,9 +377,9 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
|
|
|
358
377
|
if (begin.carry) {
|
|
359
378
|
const { count, digest } = begin.carry;
|
|
360
379
|
const session = origin ? await api.store.one('SELECT revision FROM sessions WHERE team_id=? AND id=?', [auth.team_id, origin.session_id]) : null;
|
|
361
|
-
if (!session || count +
|
|
380
|
+
if (!session || count + messageCount > 100000) fail(409, 'carry_mismatch', 'the carried prefix is unavailable or too large');
|
|
362
381
|
carriedRevision = session.revision;
|
|
363
|
-
const hash = createHash('sha256').update('[')
|
|
382
|
+
const hash = createHash('sha256').update('[');
|
|
364
383
|
while (carriedCount < count) {
|
|
365
384
|
const page = await api.store.query(`SELECT ordinal,source_id,native_key,recorded_at,value_json FROM messages m
|
|
366
385
|
WHERE m.team_id=? AND m.session_id=? AND m.ordinal>=? AND m.ordinal<? AND ${MESSAGE_AT()} ORDER BY m.ordinal LIMIT 16`,
|
|
@@ -368,7 +387,7 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
|
|
|
368
387
|
if (!page.length) fail(409, 'carry_mismatch', 'the carried messages are not what this session holds; send the capture whole');
|
|
369
388
|
for (const row of page) {
|
|
370
389
|
if (row.ordinal !== carriedCount || row.source_id !== sourceId) fail(409, 'carry_mismatch', 'the carried messages are not what this session holds; send the capture whole');
|
|
371
|
-
if (
|
|
390
|
+
if (sentKeys.has(row.native_key)) fail(409, 'capture_conflict', 'duplicate native message key');
|
|
372
391
|
if (carriedCount) hash.update(',');
|
|
373
392
|
hash.update(canonicalJson({native_key:row.native_key, recorded_at:row.recorded_at ?? null, value:JSON.parse(row.value_json)}));
|
|
374
393
|
carriedCount++;
|
|
@@ -385,15 +404,11 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
|
|
|
385
404
|
// session that gained one message writes one row, not the whole session again.
|
|
386
405
|
const carryGuards=[],currentRevision=origin?(await api.store.one('SELECT revision FROM sessions WHERE team_id=? AND id=?',[auth.team_id,sid]))?.revision??null:null;
|
|
387
406
|
if (carriedRevision != null && currentRevision !== carriedRevision) fail(409, 'revision_mismatch', 'session changed during prefix verification; retry');
|
|
388
|
-
|
|
407
|
+
sentKeys.clear();
|
|
408
|
+
carryGuards.push({ sql: 'SELECT 1 ok WHERE (SELECT COUNT(*) FROM capture_parts WHERE team_id=? AND capture_id=?)=?', params: [auth.team_id, captureId, commit.parts], status: 409, code: 'batch_gap', message: 'capture parts changed before commit' });
|
|
389
409
|
// Another commit to this session between this read and the write would make the carried rows wrong: refused, retried.
|
|
390
410
|
if(currentRevision!=null)carryGuards.push({sql:'SELECT 1 ok FROM sessions WHERE team_id=? AND id=? AND revision=?',params:[auth.team_id,sid,currentRevision],code:'revision_mismatch',status:409,message:'session changed during capture; retry'});
|
|
391
|
-
|
|
392
|
-
let ordinal=carriedCount;for(const m of messages){const value=JSON.stringify(m.value),old=held.get(ordinal);held.delete(ordinal);
|
|
393
|
-
if(old&&old.source_id===sourceId&&old.native_key===m.native_key&&old.recorded_at===m.recorded_at&&old.value_json===value){if(old.until_revision!==OPEN_REVISION)statements.push({sql:'UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND revision=? AND ordinal=?',params:[OPEN_REVISION,auth.team_id,sid,old.revision,old.ordinal]});ordinal++;continue;}
|
|
394
|
-
if(old)close(old);
|
|
395
|
-
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,until_revision) VALUES (?,?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,ordinal++,mid,sourceId,m.native_key,m.recorded_at,value,normalizeSearch(textFromContent(m.value.content)),OPEN_REVISION]});}
|
|
396
|
-
for(const row of held.values())close(row);
|
|
411
|
+
statements.push({ kind: 'capture_messages', capture: { teamId: auth.team_id, sessionId: sid, sourceId, captureId, harness: cap.harness, nativeId: cap.native_id, currentRevision, carriedCount, parts: commit.parts, messageNamespace: api.ids.next('message') } });
|
|
397
412
|
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:'DELETE FROM capture_parts WHERE team_id=? AND capture_id=?',params:[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,...carryGuards,{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);
|
|
398
413
|
}
|
|
399
414
|
|
package/server/store-core.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// them on the object's own SQLite.
|
|
6
6
|
import { ingestOtlpBatch, correlateSessions } from './arc2-transactions.mjs';
|
|
7
7
|
import { claimEffect, getEffect, listEffects, reportEffect } from './effects-transactions.mjs';
|
|
8
|
+
import { applyCaptureMessages } from './capture-transactions.mjs';
|
|
8
9
|
import { MIGRATIONS, MESSAGE_AT } from '@volter/teams/server/migrations';
|
|
9
10
|
|
|
10
11
|
/** Bring a database to the newest schema. `version()`/`setVersion(n)` name where the host records it. */
|
|
@@ -119,7 +120,7 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
119
120
|
transaction: ({statements}) => tx(() => statements.map(s => s.kind === 'query' ? all(s.sql,s.params) : s.kind === 'one' ? get(s.sql,s.params) : run(s.sql,s.params))),
|
|
120
121
|
snapshot: ({statements}) => tx(() => statements.map(s => s.kind === 'one' ? get(s.sql,s.params) : all(s.sql,s.params))),
|
|
121
122
|
guardedTransaction: ({guards=[],statements}) => tx(() => { for(const guard of guards){const row=get(guard.sql,guard.params);if(!row){const error=new Error(guard.message??'transaction predicate changed');error.code=guard.code??'conflict';error.status=guard.status??409;throw error;}}return statements.map(s=>s.kind==='query'?all(s.sql,s.params):s.kind==='one'?get(s.sql,s.params):run(s.sql,s.params));}),
|
|
122
|
-
journalTransaction: ({teamId,guards=[],statements}) => tx(() => { for(const guard of guards){const row=get(guard.sql,guard.params);if(!row){const error=new Error(guard.message??'transaction predicate changed');error.code=guard.code??'conflict';error.status=guard.status??409;throw error;}}const team=get('SELECT journal_revision FROM teams WHERE id=?',[teamId]);if(!team)throw new Error('team not found');const revision=team.journal_revision+1;const results=statements.map(s=>{const params=(s.params??[]).map(value=>journalParam(value,revision));return s.kind==='query'?all(s.sql,params):s.kind==='one'?get(s.sql,params):run(s.sql,params);});return{revision,results};}),
|
|
123
|
+
journalTransaction: ({teamId,guards=[],statements}) => tx(() => { for(const guard of guards){const row=get(guard.sql,guard.params);if(!row){const error=new Error(guard.message??'transaction predicate changed');error.code=guard.code??'conflict';error.status=guard.status??409;throw error;}}const team=get('SELECT journal_revision FROM teams WHERE id=?',[teamId]);if(!team)throw new Error('team not found');const revision=team.journal_revision+1;const results=statements.map(s=>{if(s.kind==='capture_messages')return applyCaptureMessages({get,run},s.capture,revision);const params=(s.params??[]).map(value=>journalParam(value,revision));return s.kind==='query'?all(s.sql,params):s.kind==='one'?get(s.sql,params):run(s.sql,params);});return{revision,results};}),
|
|
123
124
|
ingestOtlpBatch: args => tx(() => ingestOtlpBatch({get,all,run},args)),
|
|
124
125
|
correlateSessions: args => tx(() => correlateSessions({get,all,run},args)),
|
|
125
126
|
listEffects: args => tx(() => listEffects({get,all,run},args)),
|