@volter/supercode-teams 0.2.20

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.
Files changed (79) hide show
  1. package/README.md +119 -0
  2. package/attention.mjs +191 -0
  3. package/bin/fleet.mjs +151 -0
  4. package/bin/teams.mjs +291 -0
  5. package/bin/workspace/args.mjs +66 -0
  6. package/bin/workspace/files.mjs +110 -0
  7. package/bin/workspace/lifecycle.mjs +113 -0
  8. package/bin/workspace/volter-device.mjs +42 -0
  9. package/bin/workspace/volter-login.mjs +185 -0
  10. package/bin/workspace.mjs +726 -0
  11. package/browser-attach.mjs +90 -0
  12. package/client.mjs +212 -0
  13. package/connector/effects.mjs +95 -0
  14. package/connector/index.d.ts +69 -0
  15. package/connector/index.mjs +174 -0
  16. package/connector/native-continuation.d.ts +8 -0
  17. package/connector/native-continuation.mjs +50 -0
  18. package/connector/native-effects.mjs +164 -0
  19. package/context.mjs +34 -0
  20. package/contracts/adapter-map.json +16 -0
  21. package/contracts/index.d.ts +299 -0
  22. package/contracts/index.mjs +37 -0
  23. package/execution.mjs +206 -0
  24. package/fleet.mjs +78 -0
  25. package/index.mjs +12 -0
  26. package/machine-door.mjs +36 -0
  27. package/machine.mjs +1398 -0
  28. package/package.json +80 -0
  29. package/porthole-hook.cjs +137 -0
  30. package/remote/job-exec.py +146 -0
  31. package/remote/wire.py +23 -0
  32. package/server/arc2-transactions.mjs +125 -0
  33. package/server/attachment-relay.mjs +149 -0
  34. package/server/catalog.mjs +7 -0
  35. package/server/continuations.mjs +56 -0
  36. package/server/correlation.d.ts +4 -0
  37. package/server/correlation.mjs +17 -0
  38. package/server/effects-transactions.mjs +119 -0
  39. package/server/effects.mjs +97 -0
  40. package/server/event-routes.mjs +208 -0
  41. package/server/exports.mjs +46 -0
  42. package/server/harness-routes.mjs +248 -0
  43. package/server/index.d.ts +31 -0
  44. package/server/index.mjs +52 -0
  45. package/server/ingest.mjs +24 -0
  46. package/server/node-upgrade.mjs +39 -0
  47. package/server/otlp/collector-config.d.ts +2 -0
  48. package/server/otlp/collector-config.mjs +26 -0
  49. package/server/otlp/decoders/index.d.ts +63 -0
  50. package/server/otlp/decoders/index.mjs +311 -0
  51. package/server/otlp/index.d.ts +4 -0
  52. package/server/otlp/index.mjs +4 -0
  53. package/server/otlp/projection.d.ts +5 -0
  54. package/server/otlp/projection.mjs +17 -0
  55. package/server/otlp/receiver.d.ts +3 -0
  56. package/server/otlp/receiver.mjs +36 -0
  57. package/server/otlp/schema/google/protobuf/any.proto +162 -0
  58. package/server/otlp/schema/google/rpc/status.proto +48 -0
  59. package/server/otlp/schema/manifest.json +22 -0
  60. package/server/otlp/schema/opentelemetry/proto/collector/logs/v1/logs_service.proto +77 -0
  61. package/server/otlp/schema/opentelemetry/proto/collector/metrics/v1/metrics_service.proto +77 -0
  62. package/server/otlp/schema/opentelemetry/proto/collector/trace/v1/trace_service.proto +77 -0
  63. package/server/otlp/schema/opentelemetry/proto/common/v1/common.proto +154 -0
  64. package/server/otlp/schema/opentelemetry/proto/logs/v1/logs.proto +226 -0
  65. package/server/otlp/schema/opentelemetry/proto/metrics/v1/metrics.proto +735 -0
  66. package/server/otlp/schema/opentelemetry/proto/resource/v1/resource.proto +45 -0
  67. package/server/otlp/schema/opentelemetry/proto/trace/v1/trace.proto +359 -0
  68. package/server/otlp/transport.d.ts +10 -0
  69. package/server/otlp/transport.mjs +424 -0
  70. package/server/otlp/wire-codec.mjs +153 -0
  71. package/server/store-core.mjs +139 -0
  72. package/server/store-worker.mjs +23 -0
  73. package/worker/fetch-adapter.mjs +49 -0
  74. package/worker/index.mjs +127 -0
  75. package/worker/websocket-duplex.mjs +29 -0
  76. package/worker/wrangler.toml +38 -0
  77. package/workspace-client.d.ts +75 -0
  78. package/workspace-client.mjs +171 -0
  79. package/workspace-move.mjs +38 -0
@@ -0,0 +1,208 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { validateListQuery } from '../contracts/index.mjs';
3
+ import { cursorHash, cursorToken } from './catalog.mjs';
4
+ import { writeSse } from '@volter/teams/server/events';
5
+
6
+ function openStream(api, req, res, auth, requestId, fail) {
7
+ const count = api.openStreams.get(auth.credential_id) ?? 0;
8
+ if (count >= 10) fail(429, 'rate_limited', 'too many open event streams', { retry_after_seconds: 1 });
9
+ api.openStreams.set(auth.credential_id, count + 1);
10
+ let closed = false;
11
+ const cleanups = [];
12
+ const close = () => {
13
+ if (closed) return;
14
+ closed = true;
15
+ for (const cleanup of cleanups) cleanup();
16
+ const next = (api.openStreams.get(auth.credential_id) ?? 1) - 1;
17
+ if (next > 0) api.openStreams.set(auth.credential_id, next);
18
+ else api.openStreams.delete(auth.credential_id);
19
+ res.end();
20
+ };
21
+ req.once('close', close);
22
+ api.events.once('shutdown', close);
23
+ cleanups.push(() => api.events.off('shutdown', close));
24
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store', 'X-Request-ID': requestId });
25
+ return {
26
+ get closed() { return closed; },
27
+ cleanup(fn) { if (closed) fn(); else cleanups.push(fn); },
28
+ close,
29
+ terminal(type, reason) {
30
+ if (!closed) writeSse(res, { type, reason, terminal: true });
31
+ close();
32
+ },
33
+ write(event) {
34
+ if (closed) return false;
35
+ if (res.writableLength + Buffer.byteLength(JSON.stringify(event)) > 1048576) {
36
+ this.terminal('resync_required', 'slow_reader');
37
+ return false;
38
+ }
39
+ writeSse(res, event);
40
+ return true;
41
+ },
42
+ };
43
+ }
44
+
45
+ function streamQueue(stream, work) {
46
+ let running = null, dirty = false;
47
+ return () => {
48
+ dirty = true;
49
+ if (running) return running;
50
+ // A busy stream needs one subsequent replay/reset, not a promise per incoming event.
51
+ running = (async () => {
52
+ while (dirty && !stream.closed) {
53
+ dirty = false;
54
+ await work();
55
+ }
56
+ })().catch(error => {
57
+ if ([401, 403, 404].includes(error.status)) stream.terminal('access_revoked', 'authorization');
58
+ else stream.terminal('resync_required', 'stream_unavailable');
59
+ }).finally(() => { running = null; });
60
+ return running;
61
+ };
62
+ }
63
+
64
+ const sessionArgs = (api, auth, ref) => ({
65
+ teamId: auth.team_id, principalId: auth.principal_id,
66
+ credentialId: auth.credential_id, ref, now: api.clock.nowMs(),
67
+ });
68
+
69
+ async function readForEvent(api, auth, ref, tail) {
70
+ const nextCursorToken = cursorToken();
71
+ const snapshot = await api.store.readSessionSnapshot({
72
+ ...sessionArgs(api, auth, ref), tail,
73
+ nextCursorToken, nextCursorHash: cursorHash(nextCursorToken),
74
+ });
75
+ const messages = snapshot.rows.map(item => ({
76
+ id: item.id, source_id: item.source_id, native_key: item.native_key,
77
+ recorded_at: item.recorded_at, value: JSON.parse(item.value_json),
78
+ }));
79
+ return {
80
+ version: 1,
81
+ context: { server_id: api.serverId, team_id: auth.team_id, principal_id: auth.principal_id },
82
+ descriptor: JSON.parse(snapshot.descriptor_json), messages,
83
+ before_cursor: snapshot.beforeCursor, event_cursor: snapshot.eventCursor,
84
+ revision: snapshot.revision,
85
+ display: { returned: messages.length, total: snapshot.total, truncated_fields: 0 },
86
+ };
87
+ }
88
+
89
+ export async function handleSessionEvents({ api, req, res, auth, id, url, requestId, fail }) {
90
+ const tail = url.searchParams.has('tail') ? Number(url.searchParams.get('tail')) : 100;
91
+ if ([...url.searchParams.keys()].some(key => !['after', 'tail'].includes(key)) || !Number.isSafeInteger(tail) || tail < 1 || tail > 500) fail(400, 'invalid_request', 'invalid follow query');
92
+ const after = url.searchParams.get('after') ?? req.headers['last-event-id'] ?? null;
93
+ if (url.searchParams.get('after') && req.headers['last-event-id'] && url.searchParams.get('after') !== req.headers['last-event-id']) fail(400, 'invalid_request', 'after and Last-Event-ID differ');
94
+ const initial = await readForEvent(api, auth, id, tail);
95
+ const sessionId = initial.descriptor.id;
96
+ let delivered = after === null ? Number(initial.event_cursor) : Number(after);
97
+ if (!Number.isSafeInteger(delivered) || delivered < 0) fail(409, 'resync_required', 'event cursor is invalid');
98
+ if (after !== null) {
99
+ const [team, retained] = await api.store.snapshot([
100
+ { kind: 'one', sql: 'SELECT journal_revision FROM teams WHERE id=?', params: [auth.team_id] },
101
+ { kind: 'one', sql: 'SELECT min(revision) first_revision FROM journal WHERE team_id=?', params: [auth.team_id] },
102
+ ]);
103
+ if (delivered > team.journal_revision || delivered < team.journal_revision && (retained.first_revision === null || delivered < retained.first_revision - 1)) fail(409, 'resync_required', 'event cursor is outside retained journal');
104
+ }
105
+ const stream = openStream(api, req, res, auth, requestId, fail);
106
+ if (after === null) stream.write({ type: 'snapshot', cursor: initial.event_cursor, revision: initial.revision, data: initial });
107
+ const enqueue = streamQueue(stream, async () => {
108
+ // This worker operation reads current credential, membership, groups and every constituent ACL together.
109
+ await api.store.authorizedSession(sessionArgs(api, auth, sessionId));
110
+ const replay = await api.store.query('SELECT revision,kind FROM journal WHERE team_id=? AND session_id=? AND revision>? ORDER BY revision', [auth.team_id, sessionId, delivered]);
111
+ for (const event of replay) {
112
+ if (!stream.write(event.kind === 'delete'
113
+ ? { type: 'session_deleted', cursor: String(event.revision), revision: event.revision, session_id: sessionId }
114
+ : { type: 'session_changed', cursor: String(event.revision), revision: event.revision, session_id: sessionId, reason: event.kind === 'metadata' ? 'metadata' : 'capture', read_required: true })) break;
115
+ delivered = event.revision;
116
+ }
117
+ });
118
+ stream.cleanup(api.events.subscribe(`session:${auth.team_id}:${sessionId}`, enqueue));
119
+ stream.cleanup(api.events.subscribe(`team:${auth.team_id}`, event => { if (event.type === 'authorization_changed') enqueue(); }));
120
+ // The snapshot pins its journal cursor atomically. Replay closes the snapshot/subscription gap.
121
+ await enqueue();
122
+ const heartbeat = setInterval(() => { void enqueue().then(() => { if (!stream.closed) res.write(': heartbeat\n\n'); }); }, 15000);
123
+ heartbeat.unref?.();
124
+ stream.cleanup(() => clearInterval(heartbeat));
125
+ }
126
+
127
+ async function catalogWindow(api, auth, query) {
128
+ const [team, credential, membership, groups, rows, grants] = await api.store.snapshot([
129
+ { kind: 'one', sql: 'SELECT journal_revision,auth_epoch FROM teams WHERE id=?', params: [auth.team_id] },
130
+ { kind: 'one', sql: 'SELECT * FROM credentials WHERE id=?', params: [auth.credential_id] },
131
+ { kind: 'one', sql: 'SELECT role,active FROM memberships WHERE team_id=? AND principal_id=?', params: [auth.team_id, auth.principal_id] },
132
+ { kind: 'query', sql: 'SELECT group_id FROM group_members WHERE team_id=? AND principal_id=?', params: [auth.team_id, auth.principal_id] },
133
+ { kind: 'query', sql: 'SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s LEFT JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? ORDER BY v.sort_at DESC,s.id ASC', params: [auth.team_id] },
134
+ { kind: 'query', sql: 'SELECT * FROM session_grants WHERE team_id=?', params: [auth.team_id] },
135
+ ]);
136
+ const now = api.clock.nowMs();
137
+ if (!membership?.active || !credential || credential.revoked_at != null || credential.expires_at != null && credential.expires_at <= now) throw Object.assign(new Error('authorization changed'), { status: 401 });
138
+ const byId = new Map(rows.map(row => [row.id, row]));
139
+ const groupIds = new Set(groups.map(group => group.group_id));
140
+ const children = new Map();
141
+ const grantsBySession = new Map();
142
+ for (const row of rows) if (row.canonical_id) {
143
+ const list = children.get(row.canonical_id) ?? [];
144
+ list.push(row); children.set(row.canonical_id, list);
145
+ }
146
+ for (const grant of grants) {
147
+ const list = grantsBySession.get(grant.session_id) ?? [];
148
+ list.push(grant); grantsBySession.set(grant.session_id, list);
149
+ }
150
+ const readable = (row, seen = new Set()) => {
151
+ if (!row || seen.has(row.id) || row.deleted_at != null || !row.descriptor_json) return false;
152
+ seen.add(row.id);
153
+ const own = row.visibility === 'team' || row.custodian_id === auth.principal_id || (grantsBySession.get(row.id) ?? []).some(grant =>
154
+ (grant.expires_at == null || grant.expires_at > now) &&
155
+ (grant.subject_kind === 'principal' && grant.subject_id === auth.principal_id || grant.subject_kind === 'group' && groupIds.has(grant.subject_id)));
156
+ return own && (children.get(row.id) ?? []).every(child => readable(child, seen));
157
+ };
158
+ const readableReference = id => {
159
+ let row = byId.get(id); const seen = new Set();
160
+ while (row?.canonical_id) {
161
+ if (seen.has(row.id)) return false;
162
+ seen.add(row.id); row = byId.get(row.canonical_id);
163
+ }
164
+ return readable(row);
165
+ };
166
+ const items = [];
167
+ for (const row of rows) {
168
+ if (row.canonical_id || !readable(row)) continue;
169
+ const descriptor = JSON.parse(row.descriptor_json);
170
+ for (const field of ['parent_id', 'continued_from']) if (descriptor[field] && !readableReference(descriptor[field])) descriptor[field] = null;
171
+ if (query.user && (query.user === 'me' ? auth.principal_id : query.user) !== descriptor.user?.principal_id) continue;
172
+ if (query.repo && query.repo !== descriptor.repository?.key) continue;
173
+ if (query.harness && descriptor.harness !== query.harness) continue;
174
+ if (query.source && !descriptor.source_ids.includes(query.source)) continue;
175
+ if (query.machine && !descriptor.machine_ids.includes(query.machine)) continue;
176
+ if (query.since !== undefined && row.sort_at < query.since) continue;
177
+ if (query.until !== undefined && row.sort_at >= query.until) continue;
178
+ if (query.include_children === false && descriptor.parent_id !== null) continue;
179
+ items.push(descriptor);
180
+ if (items.length >= (query.limit ?? 50)) break;
181
+ }
182
+ return {
183
+ revision: team.journal_revision, authEpoch: team.auth_epoch,
184
+ data: { version: 1, context: { server_id: api.serverId, team_id: auth.team_id, principal_id: auth.principal_id }, items, next_cursor: null },
185
+ };
186
+ }
187
+
188
+ export async function handleCatalogEvents({ api, req, res, auth, url, requestId, qobj, fail }) {
189
+ if (url.searchParams.get('after') && req.headers['last-event-id'] && url.searchParams.get('after') !== req.headers['last-event-id']) fail(400, 'invalid_request', 'after and Last-Event-ID differ');
190
+ const query = qobj(url); delete query.after;
191
+ validateListQuery(query);
192
+ if (query.cursor) fail(400, 'invalid_request', 'catalog streams use current windows, not page cursors');
193
+ const stream = openStream(api, req, res, auth, requestId, fail);
194
+ let lastCursor = null;
195
+ const enqueue = streamQueue(stream, async () => {
196
+ const page = await catalogWindow(api, auth, query);
197
+ // Authorization and expiry can change a window without creating a session revision.
198
+ const digest = createHash('sha256').update(JSON.stringify(page.data)).digest('hex');
199
+ const cursor = `${page.revision}:${page.authEpoch}:${digest}`;
200
+ if (cursor !== lastCursor && stream.write({ type: 'catalog_reset', cursor, revision: page.revision, data: page.data })) lastCursor = cursor;
201
+ });
202
+ // Subscribe before taking the first snapshot, and serialize resets so older windows cannot arrive last.
203
+ stream.cleanup(api.events.subscribe(`team:${auth.team_id}`, enqueue));
204
+ await enqueue();
205
+ const heartbeat = setInterval(() => { void enqueue().then(() => { if (!stream.closed) res.write(': heartbeat\n\n'); }); }, 15000);
206
+ heartbeat.unref?.();
207
+ stream.cleanup(() => clearInterval(heartbeat));
208
+ }
@@ -0,0 +1,46 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson } from '../contracts/index.mjs';
3
+
4
+ const sha256=value=>createHash('sha256').update(value).digest('hex');
5
+ const textOf=value=>typeof value?.content==='string'?value.content:Array.isArray(value?.content)?value.content.map(part=>typeof part==='string'?part:part?.text??'').join(''):'';
6
+ const escapeHtml=value=>value.replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
7
+
8
+ export async function exportSessionRoute({api,req,res,auth,sessionId,requestId,parseJson,exact,fail,send,result,guardedMutation}){
9
+ const body=await parseJson(req);exact(body,['format','fidelity','revision']);
10
+ if(typeof body.format!=='string'||!Number.isSafeInteger(body.revision)||body.revision<0)fail(400,'invalid_request','invalid export request');
11
+ if(body.fidelity!==undefined&&!['byte_lossless','value_lossless','semantic'].includes(body.fidelity))fail(400,'invalid_request','invalid requested fidelity');
12
+ const snapshot=await api.store.readSessionSnapshot({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:sessionId,now:api.clock.nowMs(),requestedRevision:body.revision,tail:100000,nextCursorHash:'unused',nextCursorToken:'unused'});
13
+ if(snapshot.total!==snapshot.rows.length)fail(413,'request_too_large','session is too large to export');
14
+ const descriptor=JSON.parse(snapshot.descriptor_json),coverage=descriptor.coverage,files=[];
15
+ let fidelity,residue=[];
16
+ if(body.format==='text'||body.format==='html'){
17
+ if(body.fidelity!==undefined&&body.fidelity!=='semantic')fail(422,'fidelity_unavailable','text and HTML exports provide semantic fidelity');
18
+ const lines=snapshot.rows.map(row=>{const value=JSON.parse(row.value_json);return `${value.role??'unknown'}: ${textOf(value)}`;});
19
+ const plain=lines.join('\n\n')+'\n',bytes=Buffer.from(body.format==='html'?`<!doctype html><meta charset="utf-8"><pre>${escapeHtml(plain)}</pre>`:plain),uploadId=api.ids.next('upload'),relativeName=`session.${body.format==='html'?'html':'txt'}`,mediaType=body.format==='html'?'text/html; charset=utf-8':'text/plain; charset=utf-8';
20
+ files.push({upload_id:uploadId,relative_name:relativeName,media_type:mediaType,byte_length:bytes.length,sha256:sha256(bytes),role:'primary',_bytes:bytes,_source:descriptor.source_ids[0]});fidelity='semantic';
21
+ }else{
22
+ if(body.format!==descriptor.harness)fail(422,'fidelity_unavailable','cross-harness native export is not available through the captured artifact');
23
+ const bundles=await api.store.query(`SELECT a.bundle_id,a.format,a.fidelity,a.residue_json,count(*) file_count,sum(CASE WHEN a.file_role='primary' THEN 1 ELSE 0 END) primary_count
24
+ FROM session_artifacts a WHERE a.team_id=? AND a.session_id=? AND a.revision=? AND a.provenance='capture' AND a.format=? GROUP BY a.bundle_id,a.format,a.fidelity,a.residue_json ORDER BY a.bundle_id`,[auth.team_id,descriptor.id,body.revision,body.format]);
25
+ const bundle=bundles.find(item=>item.primary_count===1&&(!body.fidelity||item.fidelity===body.fidelity));if(!bundle)fail(422,'fidelity_unavailable','no complete native bundle exists for this revision and fidelity');
26
+ const rows=await api.store.query(`SELECT a.file_role,u.id,u.manifest_json FROM session_artifacts a JOIN uploads u ON u.team_id=a.team_id AND u.id=a.upload_id
27
+ WHERE a.team_id=? AND a.session_id=? AND a.revision=? AND a.provenance='capture' AND a.bundle_id=? AND u.committed=1 ORDER BY a.file_role,u.id`,[auth.team_id,descriptor.id,body.revision,bundle.bundle_id]);
28
+ if(rows.length!==bundle.file_count)fail(422,'fidelity_unavailable','native bundle is incomplete');
29
+ for(const row of rows){const manifest=JSON.parse(row.manifest_json);files.push({upload_id:row.id,relative_name:manifest.relative_name,media_type:manifest.media_type,byte_length:manifest.byte_length,sha256:manifest.sha256,role:row.file_role});}
30
+ fidelity=bundle.fidelity;residue=JSON.parse(bundle.residue_json);
31
+ if(body.fidelity!==undefined&&body.fidelity!==fidelity)fail(422,'fidelity_unavailable','captured native artifact does not match requested fidelity');
32
+ }
33
+ const operationId=api.ids.next('operation'),exportResult={kind:'export',source_session_id:descriptor.id,source_revision:body.revision,format:body.format,fidelity,residue,coverage,files:files.map(({_bytes,_source,...file})=>file)},operation={id:operationId,state:'succeeded',result:exportResult,error:null},response=result(api,auth,operation),now=api.clock.nowMs();
34
+ const statements=[];
35
+ for(const file of files)if(file._bytes){const manifest={upload_id:file.upload_id,relative_name:file.relative_name,media_type:file.media_type,role:'native',byte_length:file.byte_length,sha256:file.sha256,chunks:1};statements.push({sql:'INSERT INTO uploads(team_id,id,source_id,manifest_json,created_at,committed) VALUES (?,?,?,?,?,1)',params:[auth.team_id,file.upload_id,file._source,JSON.stringify(manifest),now]},{sql:'INSERT INTO artifact_chunks(team_id,upload_id,chunk_index,sha256,bytes) VALUES (?,?,?,?,?)',params:[auth.team_id,file.upload_id,0,file.sha256,file._bytes]},{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,descriptor.id,body.revision,operationId,file.upload_id,body.format,'export','primary','semantic','[]']});}
36
+ statements.push({sql:'INSERT INTO operations(team_id,id,requester_id,session_id,state,value_json,created_at) VALUES (?,?,?,?,?,?,?)',params:[auth.team_id,operationId,auth.principal_id,descriptor.id,'succeeded',JSON.stringify({result:exportResult,error:null}),now]});
37
+ await guardedMutation(api,res,auth,[{sql:'SELECT 1 ok FROM credentials c JOIN memberships m ON m.team_id=? AND m.principal_id=c.principal_id WHERE c.id=? AND c.principal_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND m.active=1',params:[auth.team_id,auth.credential_id,auth.principal_id,now],code:'authentication_required',status:401,message:'credential changed'},{sql:'SELECT 1 ok FROM session_versions v JOIN sessions s ON s.team_id=v.team_id AND s.id=v.session_id WHERE v.team_id=? AND v.session_id=? AND v.revision=? AND s.deleted_at IS NULL',params:[auth.team_id,descriptor.id,body.revision],code:'not_found',status:404,message:'session revision is unavailable'}],statements,{status:202,body:response,targetId:operationId});
38
+ send(res,202,response,requestId);
39
+ }
40
+
41
+ export async function operationRoute({api,res,auth,operationId,requestId,fail,send,result}){
42
+ const row=await api.store.one('SELECT * FROM operations WHERE team_id=? AND id=? AND requester_id=?',[auth.team_id,operationId,auth.principal_id]);
43
+ if(!row)fail(404,'not_found','operation not found');
44
+ await api.store.authorizedSession({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:row.session_id,now:api.clock.nowMs()});
45
+ const value=JSON.parse(row.value_json);send(res,200,result(api,auth,{id:row.id,state:row.state,result:value.result??null,error:value.error??null}),requestId);
46
+ }
@@ -0,0 +1,248 @@
1
+ // The harness's routes on a Teams server (company RFC 0008 §6): the session catalog,
2
+ // search, reads, access and correlations; exports, continuations, effects and the
3
+ // attachment relay; sync rules; OTLP ingest, captures and uploads. The neutral handler
4
+ // (http.mjs) owns identity, teams, members, groups, credentials, invitations, machines,
5
+ // enrollment and machine grants, and calls these hooks at the points the harness's
6
+ // routes have always run: before authentication, once authenticated, before the
7
+ // idempotency check, and last among a team's routes. Each hook returns true when it
8
+ // answered the request.
9
+ import { bearer, hashToken, canRead, canManage, isAdmin } from '@volter/teams/server/auth';
10
+ import { validateListQuery, validateSearchQuery, validateReadQuery, validateCaptureBegin, validateCapturePart, canonicalJson } from '../contracts/index.mjs';
11
+ import { normalizeSearch, textFromContent, cursorHash, cursorToken } from './catalog.mjs';
12
+ import { captureDigest, captureMessages, handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
13
+ import { handleCatalogEvents, handleSessionEvents } from './event-routes.mjs';
14
+ import { exact } from '@volter/teams/server/request-json';
15
+ import { renderSourceCollectorConfig } from './otlp/collector-config.mjs';
16
+ import { attachmentActionRoute, createAttachment, liveGrantRoute, machineEffectRoute } from './effects.mjs';
17
+ import { exportSessionRoute, operationRoute } from './exports.mjs';
18
+ import { attachmentStreamRoute, attachmentFramesRoute, closeMachineAttachmentRoute, detachAttachmentRoute } from './attachment-relay.mjs';
19
+ import { continuationRoute, remoteContinuationArtifactRoute, remoteContinuationRoute } from './continuations.mjs';
20
+ import { fail, parseJson, result, send, guardedMutation, requireRevision, checkRate, grants, syncRuleJson, authGuards } from '@volter/teams/server/http';
21
+
22
+ async function preAuth({ api, req, res, path, requestId }) {
23
+ let early;
24
+ 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
+ return false;
26
+ }
27
+
28
+ async function authed({ api, req, res, auth, path, requestId }) {
29
+ const otlpSignal=req.method==='POST'?({'/v1/logs':'logs','/v1/traces':'traces','/v1/metrics':'metrics'}[path]):null;if(otlpSignal){await handleOtlpRoute(api,req,res,auth,otlpSignal);return true;}
30
+ return false;
31
+ }
32
+
33
+ async function beforeIdempotency({ api, req, res, auth, url, path, sub, requestId }) {
34
+ let m;
35
+ if(req.method==='POST'&&(m=/^\/machines\/([^/]+)\/attachments\/([^/]+)\/frames$/.exec(sub))){await attachmentFramesRoute({api,req,res,auth,machineId:m[1],attachmentId:m[2],requestId,fail,send,result,guardedMutation});return true;}
36
+ if(req.method==='DELETE'&&(m=/^\/machines\/([^/]+)\/attachments\/([^/]+)$/.exec(sub))){await closeMachineAttachmentRoute({api,req,res,auth,machineId:m[1],attachmentId:m[2],requestId,fail,send});return true;}
37
+ if(req.method==='GET'&&(m=/^\/machines\/([^/]+)\/effects\/([^/]+)\/continuation$/.exec(sub))){await remoteContinuationRoute({api,req,res,auth,machineId:m[1],effectId:m[2],requestId,fail,send,result});return true;}
38
+ if(req.method==='GET'&&(m=/^\/machines\/([^/]+)\/effects\/([^/]+)\/artifacts\/([^/]+)$/.exec(sub))){await remoteContinuationArtifactRoute({api,req,res,auth,machineId:m[1],effectId:m[2],uploadId:m[3],requestId,fail});return true;}
39
+ return false;
40
+ }
41
+
42
+ async function team({ api, req, res, auth, url, path, sub, requestId }) {
43
+ let m;
44
+ if(req.method==='GET'&&sub==='/sessions'){await listSessions(api,res,auth,url,requestId,false);return true;}
45
+ if(req.method==='GET'&&sub==='/sessions/search'){await listSessions(api,res,auth,url,requestId,true);return true;}
46
+ if(req.method==='GET'&&sub==='/sessions/events'){await handleCatalogEvents({api,req,res,auth,url,requestId,qobj,canReadMerged,readerProjection,fail});return true;}
47
+ if(req.method==='POST'&&sub==='/correlations'){await correlateRoute(api,req,res,auth,path,requestId);return true;}
48
+ if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)$/.exec(sub))){await sessionDescriptor(api,res,auth,m[1],requestId);return true;}
49
+ if((req.method==='PATCH'||req.method==='DELETE')&&(m=/^\/sessions\/([^/]+)$/.exec(sub))){await mutateSession(api,req,res,auth,m[1],requestId);return true;}
50
+ if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/messages$/.exec(sub))){await readSession(api,res,auth,m[1],url,requestId);return true;}
51
+ if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/artifacts\/([^/]+)$/.exec(sub))){await readArtifact(api,req,res,auth,m[1],m[2],requestId);return true;}
52
+ 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
+ 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
+ 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;}
55
+ if((m=/^\/sessions\/([^/]+)\/access(?:\/([^/]+))?$/.exec(sub))){await accessRoute(api,req,res,auth,m[1],m[2],requestId);return true;}
56
+ 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
+ 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;}
58
+ 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;}
59
+ 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;}
60
+ if(req.method==='DELETE'&&(m=/^\/attachments\/([^/]+)$/.exec(sub))){await detachAttachmentRoute({api,res,auth,attachmentId:m[1],requestId,fail,send,guardedMutation});return true;}
61
+ if(req.method==='GET'&&(m=/^\/operations\/([^/]+)$/.exec(sub))){await operationRoute({api,res,auth,operationId:m[1],requestId,fail,send,result,guardedMutation});return true;}
62
+ if((m=/^\/continuations\/([^/]+)(?:\/(authorize-destination|launch|complete-local))?$/.exec(sub))){await continuationRoute({api,req,res,auth,id:m[1],action:m[2],requestId,parseJson,exact,fail,send,result,guardedMutation});return true;}
63
+ if(req.method==='GET'&&(m=/^\/sources\/([^/]+)\/config$/.exec(sub))){await sourceConfig(api,res,auth,m[1],url,requestId);return true;}
64
+ if(req.method==='POST'&&sub==='/sync-rules'){await createSyncRule(api,req,res,auth,requestId);return true;}
65
+ if(req.method==='GET'&&sub==='/sync-rules'){await listSyncRules(api,res,auth,url,requestId);return true;}
66
+ if(req.method==='GET'&&(m=/^\/sync-rules\/([^/]+)$/.exec(sub))){await getSyncRule(api,res,auth,m[1],requestId);return true;}
67
+ if(req.method==='PATCH'&&(m=/^\/sync-rules\/([^/]+)$/.exec(sub))){await updateSyncRule(api,req,res,auth,m[1],requestId);return true;}
68
+ if(req.method==='DELETE'&&(m=/^\/sync-rules\/([^/]+)$/.exec(sub))){await removeSyncRule(api,req,res,auth,m[1],requestId);return true;}
69
+ if(req.method==='POST'&&(m=/^\/sources\/([^/]+)\/captures$/.exec(sub))){await captureBegin(api,req,res,auth,m[1],requestId);return true;}
70
+ if(req.method==='PUT'&&(m=/^\/sources\/([^/]+)\/captures\/([^/]+)\/parts\/(\d+)$/.exec(sub))){await capturePart(api,req,res,auth,m[1],m[2],Number(m[3]),requestId);return true;}
71
+ if(req.method==='POST'&&(m=/^\/sources\/([^/]+)\/captures\/([^/]+)\/commit$/.exec(sub))){await captureCommit(api,req,res,auth,m[1],m[2],requestId);return true;}
72
+ if(req.method==='PUT'&&(m=/^\/sources\/([^/]+)\/uploads\/([^/]+)\/chunks\/(\d+)$/.exec(sub))){await artifactChunk(api,req,res,auth,m[1],m[2],Number(m[3]),requestId);return true;}
73
+ return false;
74
+ }
75
+
76
+ /** The hooks a Teams server serving the harness passes as `api.harnessRoutes`. */
77
+ export const harnessRoutes = Object.freeze({ preAuth, authed, beforeIdempotency, team });
78
+
79
+ async function canonicalSession(api,team,row){const seen=new Set();while(row?.canonical_id){if(seen.has(row.id))fail(409,'conflict','correlation cycle');seen.add(row.id);row=await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[team,row.canonical_id]);}return row;}
80
+ async function constituents(api,team,row){const root=await canonicalSession(api,team,row);return api.store.query('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,team,team]);}
81
+ async function canReadMerged(api,auth,row){for(const member of await constituents(api,auth.team_id,row))if(!canRead(auth,member,await grants(api,auth.team_id,member.id)))return false;return true;}
82
+ const managerGuard=(auth,sessionId,now,{governance=false}={})=>({
83
+ sql:`SELECT 1 ok FROM sessions s JOIN memberships m ON m.team_id=s.team_id AND m.principal_id=? WHERE s.team_id=? AND s.id=? AND s.deleted_at IS NULL AND m.active=1 AND m.role!='viewer' AND (?=1 AND m.role IN ('owner','admin') OR s.custodian_id=? OR EXISTS (SELECT 1 FROM session_grants g WHERE g.team_id=s.team_id AND g.session_id=s.id AND g.role='manager' AND (g.expires_at IS NULL OR g.expires_at>?) AND (g.subject_kind='principal' AND g.subject_id=? OR g.subject_kind='group' AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.team_id=s.team_id AND gm.group_id=g.subject_id AND gm.principal_id=?))))`,
84
+ params:[auth.principal_id,auth.team_id,sessionId,governance?1:0,auth.principal_id,now,auth.principal_id,auth.principal_id],
85
+ code:'not_found',status:404,message:'not found'
86
+ });
87
+ async function resolve(api,auth,ref){let rows;if(ref.length>=8)rows=await api.store.query('SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND s.id LIKE ?',[auth.team_id,`${ref}%`]);else rows=[];const visible=[];for(let r of rows){r=await canonicalSession(api,auth.team_id,r);r=await api.store.one('SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND s.id=?',[auth.team_id,r.id]);if(r&&await canReadMerged(api,auth,r)&&!visible.some(x=>x.id===r.id))visible.push(r);}if(visible.length>1)fail(400,'ambiguous_reference','ambiguous session reference');if(!visible.length)fail(404,'not_found','not found');return visible[0];}
88
+ function qobj(url){return Object.fromEntries([...url.searchParams].map(([k,v])=>[k,['limit','since','until','tail','revision'].includes(k)?Number(v):k==='include_children'?v==='true':v]));}
89
+ function projection(row){return JSON.parse(row.descriptor_json);}
90
+ async function readerProjection(api,auth,row){
91
+ const view=projection(row);
92
+ for(const field of ['parent_id','continued_from']){
93
+ const reference=view[field];
94
+ if(!reference)continue;
95
+ const target=await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=? AND deleted_at IS NULL',[auth.team_id,reference]);
96
+ if(!target||!await canReadMerged(api,{...auth,now:api.clock.nowMs()},target))view[field]=null;
97
+ }
98
+ return view;
99
+ }
100
+ async function listSessions(api,res,auth,url,requestId,search){
101
+ const query=qobj(url);
102
+ search?validateSearchQuery(query):validateListQuery(query);
103
+ const normalizedQuery=Object.fromEntries(Object.entries(query).filter(([key])=>key!=='cursor'));
104
+ const statements=[];
105
+ if(query.cursor)statements.push({kind:'one',sql:'SELECT * FROM cursors WHERE id=? AND team_id=? AND principal_id=?',params:[cursorHash(query.cursor),auth.team_id,auth.principal_id]});
106
+ statements.push(
107
+ {kind:'one',sql:'SELECT auth_epoch,journal_revision FROM teams WHERE id=?',params:[auth.team_id]},
108
+ {kind:'one',sql:'SELECT revoked_at,expires_at FROM credentials WHERE id=?',params:[auth.credential_id]},
109
+ {kind:'one',sql:'SELECT role,active FROM memberships WHERE team_id=? AND principal_id=?',params:[auth.team_id,auth.principal_id]},
110
+ {kind:'query',sql:'SELECT s.*,v.revision version_revision,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id WHERE s.team_id=? AND s.deleted_at IS NULL ORDER BY v.sort_at DESC,s.id ASC',params:[auth.team_id]},
111
+ {kind:'query',sql:'SELECT * FROM session_grants WHERE team_id=?',params:[auth.team_id]},
112
+ {kind:'query',sql:'SELECT id,session_id,revision,recorded_at,search_text FROM messages WHERE team_id=?',params:[auth.team_id]}
113
+ );
114
+ const values=await api.store.snapshot(statements),offset=query.cursor?1:0,cursor=query.cursor?values[0]:null,team=values[offset],credential=values[offset+1],membership=values[offset+2],rows=values[offset+3],allGrants=values[offset+4],allMessages=values[offset+5];
115
+ const boundaryNow=api.clock.nowMs();
116
+ if(!membership?.active||credential?.revoked_at!=null||(credential?.expires_at!=null&&credential.expires_at<=boundaryNow))fail(401,'authentication_required','authentication required');
117
+ let tuples;
118
+ if(cursor){
119
+ 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');
120
+ tuples=JSON.parse(cursor.snapshot_json);
121
+ }else tuples=rows.filter(row=>row.revision===row.version_revision).map(row=>[row.id,row.version_revision]);
122
+ const rowByVersion=new Map(rows.map(row=>[`${row.id}:${row.version_revision}`,row])),grantsBySession=new Map(),messagesByVersion=new Map();
123
+ for(const grant of allGrants){const list=grantsBySession.get(grant.session_id)??[];list.push(grant);grantsBySession.set(grant.session_id,list);}
124
+ for(const message of allMessages){const key=`${message.session_id}:${message.revision}`,list=messagesByVersion.get(key)??[];list.push(message);messagesByVersion.set(key,list);}
125
+ const hits=[],hitTuples=[];
126
+ for(const [id,revision]of tuples){
127
+ const row=rowByVersion.get(`${id}:${revision}`),current=rows.find(candidate=>candidate.id===id&&candidate.revision===candidate.version_revision);
128
+ if(!row||!current||current.canonical_id||!await canReadMerged(api,{...auth,now:boundaryNow},current))continue;
129
+ const view=await readerProjection(api,auth,row);
130
+ if(query.user&&(query.user==='me'?auth.principal_id:query.user)!==view.user?.principal_id)continue;
131
+ if(query.repo&&query.repo!==view.repository?.key)continue;
132
+ if(query.harness&&query.harness!==view.harness)continue;
133
+ if(query.machine&&!view.machine_ids.includes(query.machine))continue;
134
+ if(query.source&&!view.source_ids.includes(query.source))continue;
135
+ if(query.since!==undefined&&row.sort_at<query.since)continue;
136
+ if(query.until!==undefined&&row.sort_at>=query.until)continue;
137
+ if(query.include_children===false&&view.parent_id!==null)continue;
138
+ let hit=view;
139
+ if(search){const needle=normalizeSearch(query.query.trim()),messages=messagesByVersion.get(`${id}:${revision}`)??[],matches=[],title=normalizeSearch(view.title??'');if(title.includes(needle))matches.push({message_id:null,field:'title',excerpt:title,offset:title.indexOf(needle)});for(const message of messages){const text=normalizeSearch(message.search_text),at=text.indexOf(needle);if(at>=0&&matches.length<3)matches.push({message_id:message.id,field:'content',excerpt:[...text].slice(Math.max(0,at-120),at+needle.length+120).join(''),offset:at});}if(!matches.length)continue;hit={session:view,matches,match_at:Math.max(...messages.map(message=>message.recorded_at??0),row.sort_at),indexed_revision:revision};}
140
+ hits.push(hit);hitTuples.push([id,revision]);
141
+ }
142
+ const limit=query.limit??50,page=hits.slice(0,limit);let next=null;
143
+ if(hits.length>limit){next=cursorToken();await api.store.exec('INSERT INTO cursors(id,team_id,principal_id,kind,query_json,auth_epoch,revision,snapshot_json,expires_at) VALUES (?,?,?,?,?,?,?,?,?)',[cursorHash(next),auth.team_id,auth.principal_id,search?'search':'list',JSON.stringify(normalizedQuery),team.auth_epoch,team.journal_revision,JSON.stringify(hitTuples.slice(limit)),boundaryNow+900000]);}
144
+ const body={version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},items:page,next_cursor:next};
145
+ if(search)body.coverage={scope:'retained_text',indexed_at:boundaryNow,indexed_revision:team.journal_revision};
146
+ send(res,200,body,requestId);
147
+ }
148
+ 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()});
150
+ send(res,200,result(api,auth,JSON.parse(row.descriptor_json)),requestId,{ETag:`"${row.revision}"`});
151
+ }
152
+ async function correlateRoute(api,req,res,auth,path,requestId){
153
+ if(!['user','service'].includes(auth.kind)||auth.role==='viewer')fail(403,'forbidden','credential cannot correlate sessions');
154
+ const body=await parseJson(req);exact(body,['left_source_id','left_native_id','right_source_id','right_native_id','left_revision','right_revision','harness','evidence']);
155
+ if(body.evidence!=='custodian_attestation'||typeof body.harness!=='string'||typeof body.left_source_id!=='string'||typeof body.right_source_id!=='string'||typeof body.left_native_id!=='string'||typeof body.right_native_id!=='string'||!Number.isSafeInteger(body.left_revision)||!Number.isSafeInteger(body.right_revision))fail(400,'invalid_request','invalid correlation attestation');
156
+ const correlationId=api.ids.next('correlation'),record=res._idempotencyRecord;
157
+ const outcome=await api.store.correlateSessions({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,now:api.clock.nowMs(),correlationId,auditId:api.ids.next('audit'),operation:`POST ${path}`,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},idempotency:record?{route:record.route,key:record.key,requestDigest:record.requestDigest}:null,harness:body.harness,evidence:body,left:{sourceId:body.left_source_id,nativeId:body.left_native_id,revision:body.left_revision},right:{sourceId:body.right_source_id,nativeId:body.right_native_id,revision:body.right_revision}});
158
+ if(outcome.tombstoned){res._sideEffectsCommitted=true;api.events.publish(auth.team_id,{type:'session_deleted',cursor:String(outcome.revision),revision:outcome.revision,session_id:outcome.canonicalId});fail(410,'session_deleted','a correlated constituent was deleted');}
159
+ res._sideEffectsCommitted=true;api.events.publish(auth.team_id,{type:'session_changed',cursor:String(outcome.revision),revision:outcome.revision,session_id:outcome.canonicalId,reason:'metadata',read_required:true});send(res,200,outcome.response,requestId,{ETag:`"${outcome.revision}"`});
160
+ }
161
+ async function mutateSession(api,req,res,auth,id,requestId){const seed=await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[auth.team_id,id]),root=await canonicalSession(api,auth.team_id,seed),canonicalId=root?.id??id;const row=await api.store.one('SELECT s.*,v.descriptor_json,v.source_projection_json,v.search_title,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND s.id=?',[auth.team_id,canonicalId]);if(!row)fail(404,'not_found','not found');const members=await constituents(api,auth.team_id,row);for(const member of members)if(!isAdmin(auth)&&!canManage(auth,member,await grants(api,auth.team_id,member.id)))fail(404,'not_found','not found');const expected=Number(String(req.headers['if-match']??'').replaceAll('"',''));if(!Number.isSafeInteger(expected))fail(400,'invalid_request','If-Match is required');if(expected!==row.revision)fail(409,'revision_mismatch','session revision changed');const boundaryNow=api.clock.nowMs(),revisionParam={$journalRevision:true},guards=[...authGuards(auth,boundaryNow),{sql:'SELECT 1 ok FROM sessions WHERE team_id=? AND id=? AND revision=? AND deleted_at IS NULL',params:[auth.team_id,row.id,expected],code:'revision_mismatch',status:409,message:'session revision changed'}];
162
+ for(const member of members)guards.push(managerGuard(auth,member.id,boundaryNow,{governance:true}));
163
+ if(req.method==='DELETE'){const deletion=[];for(const member of members)deletion.push({sql:'UPDATE sessions SET deleted_at=?,revision=? WHERE team_id=? AND id=?',params:[boundaryNow,revisionParam,auth.team_id,member.id]},{sql:'UPDATE origins SET deleted_at=? WHERE team_id=? AND session_id=?',params:[boundaryNow,auth.team_id,member.id]},{sql:'DELETE FROM session_versions WHERE team_id=? AND session_id=?',params:[auth.team_id,member.id]});deletion.push({sql:'UPDATE teams SET journal_revision=?,auth_epoch=auth_epoch+1 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,row.id,'delete',boundaryNow]});const {revision}=await guardedMutation(api,res,auth,guards,deletion,{status:204,body:null,targetId:row.id,journal:true});api.events.publish(auth.team_id,{type:'session_deleted',cursor:String(revision),revision,session_id:row.id});return send(res,204,null,requestId);}
164
+ const body=await parseJson(req);exact(body,['title']);if(body.title!==null&&typeof body.title!=='string')fail(400,'invalid_request','title must be string or null');const descriptor={...projection(row),title:body.title};delete descriptor.revision;const oldMessages=await api.store.query('SELECT * FROM messages WHERE team_id=? AND session_id=? AND revision=? ORDER BY ordinal',[auth.team_id,row.id,row.revision]);const statements=[{sql:'UPDATE sessions SET revision=? WHERE team_id=? AND id=? AND revision=?',params:[revisionParam,auth.team_id,row.id,expected]},{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,row.id,revisionParam,{$journalJson:descriptor},row.source_projection_json,normalizeSearch(body.title??''),row.sort_at,boundaryNow]}];for(const x of oldMessages)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,row.id,revisionParam,x.ordinal,x.id,x.source_id,x.native_key,x.recorded_at,x.value_json,x.search_text]});statements.push({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,row.id,'metadata',boundaryNow]});const storedResponse=result(api,auth,{...descriptor,revision:'__JOURNAL_REVISION__'}),{revision}=await guardedMutation(api,res,auth,guards,statements,{status:200,body:storedResponse,targetId:row.id,journal:true}),responseDescriptor={...descriptor,revision};api.events.publish(auth.team_id,{type:'session_changed',cursor:String(revision),revision,session_id:row.id,reason:'metadata',read_required:true});send(res,200,result(api,auth,responseDescriptor),requestId,{ETag:`"${revision}"`});}
165
+ async function readSession(api,res,auth,id,url,requestId){
166
+ const query=qobj(url);
167
+ validateReadQuery(query);
168
+ const tail=query.tail??100;
169
+ if(query.before&&query.revision!==undefined)fail(400,'invalid_request','revision cannot be combined with before');
170
+ 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});
172
+ const {rows,beforeCursor,revision}=snapshot;
173
+ 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
+ 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);
175
+ }
176
+ async function readArtifact(api,req,res,auth,id,uploadId,requestId){
177
+ const artifact=await api.store.readArtifact({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,uploadId,now:api.clock.nowMs()}),bytes=Buffer.from(artifact.bytes),range=req.headers.range;
178
+ let start=0,end=bytes.length-1,status=200;
179
+ if(range){const match=/^bytes=(\d*)-(\d*)$/.exec(range);if(!match)fail(416,'invalid_request','invalid byte range');if(match[1])start=Number(match[1]);if(match[2])end=Number(match[2]);if(!match[1]&&match[2]){const suffix=Number(match[2]);start=Math.max(0,bytes.length-suffix);end=bytes.length-1;}if(!Number.isSafeInteger(start)||!Number.isSafeInteger(end)||start<0||end<start||start>=bytes.length)fail(416,'invalid_request','byte range is outside artifact');end=Math.min(end,bytes.length-1);status=206;}
180
+ const body=bytes.subarray(start,end+1);res.writeHead(status,{'Content-Type':'application/octet-stream','Content-Length':String(body.length),'Accept-Ranges':'bytes','Cache-Control':'no-store','X-Request-ID':requestId,...(status===206?{'Content-Range':`bytes ${start}-${end}/${bytes.length}`}:{})});res.end(body);
181
+ }
182
+
183
+ async function accessRoute(api,req,res,auth,id,grantId,requestId){
184
+ const seed=await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[auth.team_id,id]);if(!seed)fail(404,'not_found','not found');
185
+ const row=await canonicalSession(api,auth.team_id,seed);
186
+ const members=await constituents(api,auth.team_id,row),memberGrants=new Map();
187
+ for(const member of members){const gs=await grants(api,auth.team_id,member.id);memberGrants.set(member.id,gs);if(!canManage(auth,member,gs)&&!isAdmin(auth))fail(404,'not_found','not found');}
188
+ let gs=memberGrants.get(row.id)??[];
189
+ if(req.method==='GET')return send(res,200,result(api,auth,{revision:row.access_revision,visibility:row.visibility,custodian_id:row.custodian_id,grants:gs.map(grantJson)}),requestId,{ETag:`"${row.access_revision}"`});
190
+ const expected=requireRevision(req,row.access_revision,'access'),now=api.clock.nowMs(),next=expected+1,guards=[...authGuards(auth,now),{sql:'SELECT 1 ok FROM sessions WHERE team_id=? AND id=? AND access_revision=? AND deleted_at IS NULL',params:[auth.team_id,row.id,expected],code:'revision_mismatch',status:409,message:'access revision changed'}],statements=[];for(const member of members)guards.push(managerGuard(auth,member.id,now,{governance:true}));let status=200,response;
191
+ if(req.method==='PATCH'){
192
+ const body=await parseJson(req);exact(body,['visibility']);if(!['team','restricted'].includes(body.visibility))fail(400,'invalid_request','invalid visibility');for(const member of members)statements.push({sql:'UPDATE sessions SET visibility=?,access_revision=access_revision+1 WHERE team_id=? AND id=?',params:[body.visibility,auth.team_id,member.id]});response=result(api,auth,{revision:next,visibility:body.visibility,custodian_id:row.custodian_id,grants:gs.map(grantJson)});
193
+ }else if(req.method==='POST'){
194
+ const body=await parseJson(req);exact(body,['subject','role','expires_at']);if(!body.subject||!['principal','group'].includes(body.subject.kind)||typeof body.subject.id!=='string'||!['reader','manager'].includes(body.role))fail(400,'invalid_request','invalid grant');const subjectTable=body.subject.kind==='principal'?'memberships':'groups_table',subjectColumn=body.subject.kind==='principal'?'principal_id':'id';guards.push({sql:`SELECT 1 ok FROM ${subjectTable} WHERE team_id=? AND ${subjectColumn}=?${body.subject.kind==='principal'?' AND active=1':''}${body.role==='manager'&&body.subject.kind==='principal'?" AND role!='viewer'":''}`,params:[auth.team_id,body.subject.id],code:'invalid_request',status:400,message:'grant subject is not eligible'});let canonicalGrant;for(const member of members){const gid=api.ids.next('grant');if(member.id===row.id)canonicalGrant=gid;statements.push({sql:'INSERT INTO session_grants(team_id,id,session_id,subject_kind,subject_id,role,expires_at) VALUES (?,?,?,?,?,?,?)',params:[auth.team_id,gid,member.id,body.subject.kind,body.subject.id,body.role,body.expires_at]},{sql:'UPDATE sessions SET access_revision=access_revision+1 WHERE team_id=? AND id=?',params:[auth.team_id,member.id]});}gs=[...gs,{id:canonicalGrant,subject_kind:body.subject.kind,subject_id:body.subject.id,role:body.role,expires_at:body.expires_at}];status=201;response=result(api,auth,{revision:next,visibility:row.visibility,custodian_id:row.custodian_id,grants:gs.map(grantJson)});
195
+ }else if(req.method==='DELETE'&&grantId){const target=gs.find(g=>g.id===grantId);if(!target)fail(404,'not_found','grant not found');for(const member of members){statements.push({sql:'DELETE FROM session_grants WHERE team_id=? AND session_id=? AND subject_kind=? AND subject_id=? AND role=?',params:[auth.team_id,member.id,target.subject_kind,target.subject_id,target.role]},{sql:'UPDATE sessions SET access_revision=access_revision+1 WHERE team_id=? AND id=?',params:[auth.team_id,member.id]});}gs=gs.filter(g=>g.id!==grantId);response=result(api,auth,{revision:next,visibility:row.visibility,custodian_id:row.custodian_id,grants:gs.map(grantJson)});
196
+ }else fail(405,'not_found','method not allowed');
197
+ statements.push({sql:'UPDATE teams SET auth_epoch=auth_epoch+1 WHERE id=?',params:[auth.team_id]});await guardedMutation(api,res,auth,guards,statements,{status,body:response,targetId:row.id});api.events.publish(auth.team_id,{type:'authorization_changed',session_id:row.id});send(res,status,response,requestId,{ETag:`"${next}"`});
198
+ }
199
+ const grantJson=g=>({id:g.id,subject:{kind:g.subject_kind,id:g.subject_id},role:g.role,expires_at:g.expires_at});
200
+ async function createSyncRule(api,req,res,auth,requestId){if(!['user','service'].includes(auth.kind)||auth.role==='viewer')fail(403,'forbidden','forbidden');const b=await parseJson(req);exact(b,['machine_id','source_id','selector','visibility','backfill']);if(!['team','restricted'].includes(b.visibility)||!['none','since','all'].includes(b.backfill?.mode)||b.backfill.mode==='since'&&!Number.isSafeInteger(b.backfill.since))fail(400,'invalid_request','invalid sync rule');const id=api.ids.next('rule'),captureAfter=api.clock.nowMs(),data={id,machine_id:b.machine_id,source_id:b.source_id,selector:b.selector,visibility:b.visibility,backfill:b.backfill,state:'active',revision:1,capture_after:captureAfter},response=result(api,auth,data),guards=[...authGuards(auth,captureAfter),{sql:"SELECT 1 ok FROM machines m JOIN sources s ON s.team_id=m.team_id AND s.machine_id=m.id JOIN machine_roots r ON r.team_id=m.team_id AND r.machine_id=m.id WHERE m.team_id=? AND m.id=? AND m.custodian_id=? AND m.revoked_at IS NULL AND s.id=? AND s.custodian_id=? AND s.kind='connector' AND s.state!='revoked' AND r.id=?",params:[auth.team_id,b.machine_id,auth.principal_id,b.source_id,auth.principal_id,b.selector?.root_id],code:'forbidden',status:403,message:'machine, source, or root authorization changed'}];await guardedMutation(api,res,auth,guards,[{sql:"INSERT INTO sync_rules(team_id,id,source_id,machine_id,selector_json,visibility,backfill_json,state,capture_after) VALUES (?,?,?,?,?,?,?,'active',?)",params:[auth.team_id,id,b.source_id,b.machine_id,JSON.stringify(b.selector),b.visibility,JSON.stringify(b.backfill),captureAfter]}],{status:201,body:response,targetId:id});send(res,201,response,requestId);}
201
+ async function listSyncRules(api,res,auth,url,requestId){if(!['user','service'].includes(auth.kind))fail(403,'forbidden','forbidden');const machine=url.searchParams.get('machine');let rows;if(isAdmin(auth))rows=await api.store.query('SELECT * FROM sync_rules WHERE team_id=? AND (? IS NULL OR machine_id=?) ORDER BY id',[auth.team_id,machine,machine]);else rows=await api.store.query('SELECT r.* FROM sync_rules r JOIN machines m ON m.team_id=r.team_id AND m.id=r.machine_id WHERE r.team_id=? AND m.custodian_id=? AND (? IS NULL OR r.machine_id=?) ORDER BY r.id',[auth.team_id,auth.principal_id,machine,machine]);send(res,200,{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},items:rows.map(syncRuleJson),next_cursor:null},requestId);}
202
+ async function getSyncRule(api,res,auth,id,requestId){const row=await api.store.one('SELECT r.* FROM sync_rules r JOIN machines m ON m.team_id=r.team_id AND m.id=r.machine_id WHERE r.team_id=? AND r.id=? AND (m.custodian_id=? OR ?=1)',[auth.team_id,id,auth.principal_id,isAdmin(auth)?1:0]);if(!row)fail(404,'not_found','not found');send(res,200,result(api,auth,syncRuleJson(row)),requestId,{ETag:`"${row.revision}"`});}
203
+ async function updateSyncRule(api,req,res,auth,id,requestId){const row=await api.store.one('SELECT r.* FROM sync_rules r JOIN machines m ON m.team_id=r.team_id AND m.id=r.machine_id WHERE r.team_id=? AND r.id=? AND m.custodian_id=?',[auth.team_id,id,auth.principal_id]);if(!row)fail(404,'not_found','not found');requireRevision(req,row.revision,'sync rule');const body=await parseJson(req);exact(body,['state','backfill']);if(!['active','paused'].includes(body.state)||body.state==='active'&&!body.backfill||body.backfill&&!['none','since','all'].includes(body.backfill.mode))fail(400,'invalid_request','resume requires an explicit valid backfill policy');const now=api.clock.nowMs(),next=row.revision+1,response=result(api,auth,syncRuleJson({...row,state:body.state,backfill_json:JSON.stringify(body.backfill??JSON.parse(row.backfill_json)),capture_after:body.state==='active'?now:row.capture_after,revision:next})),guards=[...authGuards(auth,now),{sql:'SELECT 1 ok FROM sync_rules r JOIN machines m ON m.team_id=r.team_id AND m.id=r.machine_id WHERE r.team_id=? AND r.id=? AND r.revision=? AND m.custodian_id=? AND m.revoked_at IS NULL',params:[auth.team_id,id,row.revision,auth.principal_id],code:'revision_mismatch',status:409,message:'sync rule changed'}];await guardedMutation(api,res,auth,guards,[{sql:'UPDATE sync_rules SET state=?,backfill_json=?,capture_after=?,revision=revision+1 WHERE team_id=? AND id=?',params:[body.state,JSON.stringify(body.backfill??JSON.parse(row.backfill_json)),body.state==='active'?now:row.capture_after,auth.team_id,id]}],{status:200,body:response,targetId:id});send(res,200,response,requestId,{ETag:`"${next}"`});}
204
+ async function removeSyncRule(api,req,res,auth,id,requestId){const row=await api.store.one('SELECT r.* FROM sync_rules r JOIN machines m ON m.team_id=r.team_id AND m.id=r.machine_id WHERE r.team_id=? AND r.id=? AND m.custodian_id=?',[auth.team_id,id,auth.principal_id]);if(!row)fail(404,'not_found','not found');requireRevision(req,row.revision,'sync rule');const now=api.clock.nowMs(),guards=[...authGuards(auth,now),{sql:'SELECT 1 ok FROM sync_rules r JOIN machines m ON m.team_id=r.team_id AND m.id=r.machine_id WHERE r.team_id=? AND r.id=? AND r.revision=? AND m.custodian_id=?',params:[auth.team_id,id,row.revision,auth.principal_id],code:'revision_mismatch',status:409,message:'sync rule changed'}];await guardedMutation(api,res,auth,guards,[{sql:"UPDATE sync_rules SET state='removed',revision=revision+1 WHERE team_id=? AND id=?",params:[auth.team_id,id]}],{status:204,body:null,targetId:id});send(res,204,null,requestId);}
205
+ async function sourceConfig(api,res,auth,id,url,requestId){if(url.searchParams.get('format')!=='otel-collector'||[...url.searchParams.keys()].some(key=>key!=='format'))fail(400,'invalid_request','format=otel-collector is required');const row=await api.store.one("SELECT * FROM sources WHERE team_id=? AND id=? AND kind='otlp' AND (custodian_id=? OR ?=1)",[auth.team_id,id,auth.principal_id,isAdmin(auth)?1:0]);if(!row)fail(404,'not_found','not found');send(res,200,result(api,auth,renderSourceCollectorConfig({sourceId:id,endpoint:api.publicOrigin??api.localOrigin})),requestId);}
206
+ async function sourceAuth(api,auth,id){if(!auth||!['machine','ingest'].includes(auth.kind)||auth.source_id!==id)fail(403,'forbidden','source credential required');const s=await api.store.one('SELECT * FROM sources WHERE team_id=? AND id=?',[auth.team_id,id]);if(!s)fail(404,'not_found','not found');if(s.state==='revoked')fail(403,'source_revoked','source revoked');if(s.state==='paused')fail(409,'source_paused','source paused');if(auth.kind==='machine'){const machine=await api.store.one('SELECT 1 ok FROM machines WHERE team_id=? AND id=? AND revoked_at IS NULL',[auth.team_id,auth.machine_id]);if(!machine||s.machine_id!==auth.machine_id)fail(403,'forbidden','machine enrollment is not active');}return s;}
207
+ async function expireStaging(api,teamId,sourceId,id,table,row){if(!row||row.state&&row.state!=='staging'||row.committed===1||row.created_at+86400000>api.clock.nowMs())return false;await api.store.transaction([{sql:'INSERT OR REPLACE INTO capture_expiries(team_id,source_id,id,expired_at) VALUES (?,?,?,?)',params:[teamId,sourceId,id,api.clock.nowMs()]},{sql:`DELETE FROM ${table} WHERE team_id=? AND id=? AND source_id=?`,params:[teamId,id,sourceId]}]);return true;}
208
+ async function captureBegin(api,req,res,auth,id,requestId){const source=await sourceAuth(api,auth,id);const b=await parseJson(req);validateCaptureBegin(b);if(source.kind==='connector'){if(b.rule_id===null)fail(403,'forbidden','connector capture requires an active sync rule');const rule=await api.store.one('SELECT * FROM sync_rules WHERE team_id=? AND id=? AND source_id=? AND machine_id=?',[auth.team_id,b.rule_id,id,auth.machine_id]);if(!rule||rule.state!=='active'||rule.revision!==b.rule_revision)fail(403,'forbidden','sync rule is absent, inactive, or stale');if(b.visibility!==rule.visibility)fail(400,'invalid_request','capture visibility differs from sync rule');}else if(b.visibility!==source.visibility)fail(400,'invalid_request','capture visibility differs from source policy');const expired=await api.store.one('SELECT id FROM capture_expiries WHERE team_id=? AND source_id=? AND id=?',[auth.team_id,id,b.capture_id]);if(expired)fail(409,'resync_required','capture reservation expired');const existing=await api.store.one('SELECT * FROM captures WHERE team_id=? AND id=?',[auth.team_id,b.capture_id]);if(await expireStaging(api,auth.team_id,id,b.capture_id,'captures',existing))fail(409,'resync_required','capture reservation expired');if(existing){if(existing.begin_json!==canonicalJson(b))fail(409,'capture_conflict','capture id reused with different request');if(existing.state==='committed')return send(res,200,result(api,auth,{state:'committed',receipt:JSON.parse(existing.receipt_json)}),requestId);const next=(await api.store.one('SELECT count(*) n FROM capture_parts WHERE team_id=? AND capture_id=?',[auth.team_id,b.capture_id])).n;return send(res,200,result(api,auth,{state:'staging',capture_id:b.capture_id,next_part:next}),requestId);}
209
+ const origin=await api.store.one('SELECT * FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?',[auth.team_id,id,b.harness,b.native_session_id]);if(origin?.deleted_at!=null)fail(410,'session_deleted','session deleted');const expected=origin?{generation:origin.generation,sequence:origin.sequence+1,digest:origin.digest}:{generation:1,sequence:1,digest:null};if(b.generation!==expected.generation||b.sequence!==expected.sequence||b.previous_digest!==expected.digest)fail(409,'batch_gap','capture is not the next source revision',expected);const now=api.clock.nowMs(),response=result(api,auth,{state:'staging',capture_id:b.capture_id,next_part:0}),guards=[{sql:"SELECT 1 ok FROM credentials 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=c.machine_id LEFT JOIN sync_rules r ON r.team_id=s.team_id AND r.id=? WHERE c.id=? AND c.team_id=? AND c.source_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND s.state='active' AND ((s.kind!='connector' AND s.visibility=?) OR (s.kind='connector' AND c.kind='machine' AND m.id=c.machine_id AND m.revoked_at IS NULL AND r.source_id=s.id AND r.machine_id=m.id AND r.state='active' AND r.revision=? AND r.visibility=?))",params:[b.rule_id,auth.credential_id,auth.team_id,id,now,b.visibility,b.rule_revision,b.visibility],code:'forbidden',status:403,message:'source authorization changed'},{sql:"SELECT 1 ok WHERE NOT EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND deleted_at IS NOT NULL) AND ((?=1 AND ?=1 AND ? IS NULL AND NOT EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?)) OR EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND generation=? AND sequence=?-1 AND digest IS ?))",params:[auth.team_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest,auth.team_id,id,b.harness,b.native_session_id,auth.team_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest],code:'batch_gap',status:409,message:'capture is not the next source revision'}];await guardedMutation(api,res,auth,guards,[{sql:"INSERT INTO captures(team_id,id,source_id,harness,native_id,generation,sequence,previous_digest,state,begin_json,created_at) VALUES (?,?,?,?,?,?,?,?, 'staging',?,?)",params:[auth.team_id,b.capture_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest,canonicalJson(b),now]}],{status:201,body:response,targetId:b.capture_id});send(res,201,response,requestId);}
210
+ async function capturePart(api,req,res,auth,sourceId,captureId,index,requestId){await sourceAuth(api,auth,sourceId);const b=await parseJson(req);validateCapturePart(b);if(b.capture_id!==captureId||b.index!==index)fail(400,'invalid_request','capture path/body mismatch');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||cap.state!=='staging')fail(404,'not_found','capture not staging');const payload=canonicalJson(b),digest=sha256(payload),old=await api.store.one('SELECT * FROM capture_parts WHERE team_id=? AND capture_id=? AND part_index=?',[auth.team_id,captureId,index]);if(old){if(old.digest!==digest)fail(409,'capture_conflict','part replay differs');return send(res,200,result(api,auth,{next_part:index+1}),requestId);}const count=(await api.store.one('SELECT count(*) n FROM capture_parts WHERE team_id=? AND capture_id=?',[auth.team_id,captureId])).n;if(index!==count)fail(409,'batch_gap','capture parts must be contiguous');const now=api.clock.nowMs(),response=result(api,auth,{next_part:index+1}),guards=[{sql:"SELECT 1 ok FROM credentials c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id JOIN captures cap ON cap.team_id=s.team_id AND cap.source_id=s.id WHERE c.id=? AND c.source_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND s.state='active' AND cap.id=? AND cap.state='staging' AND cap.created_at+86400000>? AND (SELECT count(*) FROM capture_parts WHERE team_id=cap.team_id AND capture_id=cap.id)=?",params:[auth.credential_id,sourceId,now,captureId,now,index],code:'batch_gap',status:409,message:'capture authorization or checkpoint changed'}];await guardedMutation(api,res,auth,guards,[{sql:'INSERT INTO capture_parts(team_id,capture_id,part_index,digest,payload_json) VALUES (?,?,?,?,?)',params:[auth.team_id,captureId,index,digest,payload]}],{status:200,body:response,targetId:captureId});send(res,200,response,requestId);}
211
+ async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId){
212
+ await sourceAuth(api,auth,sourceId);
213
+ const b=await parseJson(req);
214
+ exact(b,['upload_id','index','sha256','bytes_base64']);
215
+ if(b.upload_id!==uploadId||b.index!==index)fail(400,'invalid_request','upload path/body mismatch');
216
+ let bytes;
217
+ try{bytes=Buffer.from(b.bytes_base64,'base64');}catch{fail(400,'invalid_request','invalid base64');}
218
+ if(bytes.length>1048576)fail(413,'request_too_large','artifact chunk exceeds 1 MiB');
219
+ if(sha256(bytes)!==b.sha256)fail(409,'capture_conflict','artifact chunk digest differs');
220
+ const expired=await api.store.one('SELECT id FROM capture_expiries WHERE team_id=? AND source_id=? AND id=?',[auth.team_id,sourceId,uploadId]);
221
+ if(expired)fail(409,'resync_required','upload reservation expired');
222
+ const upload=await api.store.one('SELECT * FROM uploads WHERE team_id=? AND id=?',[auth.team_id,uploadId]);
223
+ if(await expireStaging(api,auth.team_id,sourceId,uploadId,'uploads',upload))fail(409,'resync_required','upload reservation expired');
224
+ if(upload&&upload.source_id!==sourceId)fail(409,'capture_conflict','upload belongs to another source');
225
+ const old=await api.store.one('SELECT sha256 FROM artifact_chunks WHERE team_id=? AND upload_id=? AND chunk_index=?',[auth.team_id,uploadId,index]);
226
+ if(old){if(old.sha256!==b.sha256)fail(409,'capture_conflict','artifact chunk replay differs');return send(res,200,result(api,auth,{index,sha256:b.sha256}),requestId);}
227
+ const now=api.clock.nowMs(),response=result(api,auth,{index,sha256:b.sha256});
228
+ const guards=[
229
+ {sql:"SELECT 1 ok WHERE EXISTS (SELECT 1 FROM credentials c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id WHERE c.id=? AND c.source_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND s.state='active') AND NOT EXISTS (SELECT 1 FROM capture_expiries WHERE team_id=? AND source_id=? AND id=?) AND NOT EXISTS (SELECT 1 FROM uploads WHERE team_id=? AND id=? AND source_id<>?) AND COALESCE((SELECT committed FROM uploads WHERE team_id=? AND id=?),0)=0 AND COALESCE((SELECT created_at+86400000>? FROM uploads WHERE team_id=? AND id=?),1) AND (SELECT count(*) FROM artifact_chunks WHERE team_id=? AND upload_id=?)=?",params:[auth.credential_id,sourceId,now,auth.team_id,sourceId,uploadId,auth.team_id,uploadId,sourceId,auth.team_id,uploadId,now,auth.team_id,uploadId,auth.team_id,uploadId,index],code:'batch_gap',status:409,message:'upload authorization, expiry, or sequence changed'},
230
+ {sql:'SELECT 1 ok WHERE COALESCE((SELECT sum(length(bytes)) FROM artifact_chunks WHERE team_id=? AND upload_id=?),0)+?<=?',params:[auth.team_id,uploadId,bytes.length,api.uploadLimitBytes],code:'request_too_large',status:413,message:'artifact exceeds configured upload cap'}
231
+ ];
232
+ await guardedMutation(api,res,auth,guards,[
233
+ {sql:'INSERT OR IGNORE INTO uploads(team_id,id,source_id,created_at) VALUES (?,?,?,?)',params:[auth.team_id,uploadId,sourceId,now]},
234
+ {sql:'INSERT INTO artifact_chunks(team_id,upload_id,chunk_index,sha256,bytes) VALUES (?,?,?,?,?)',params:[auth.team_id,uploadId,index,b.sha256,bytes]},
235
+ ],{status:200,body:response,targetId:uploadId});
236
+ send(res,200,response,requestId);
237
+ }
238
+ 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 rows=await api.store.query('SELECT * FROM capture_parts WHERE team_id=? AND capture_id=? ORDER BY part_index',[auth.team_id,captureId]);if(rows.length!==commit.parts)fail(409,'batch_gap','capture parts incomplete');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');}const parts=rows.map(x=>JSON.parse(x.payload_json));if(captureDigest(begin,parts,commit)!==commit.digest)fail(409,'capture_conflict','capture digest differs');const messages=captureMessages(parts);if(messages.length>100000)fail(413,'request_too_large','capture contains too many messages');
239
+ for(const manifest of commit.artifacts){validateArtifactName(manifest.relative_name);const upload=await api.store.one('SELECT source_id FROM uploads WHERE team_id=? AND id=?',[auth.team_id,manifest.upload_id]);if(!upload||upload.source_id!==sourceId)fail(409,'capture_conflict','artifact upload belongs to another source');const chunks=await api.store.query('SELECT c.* FROM artifact_chunks c JOIN uploads u ON u.team_id=c.team_id AND u.id=c.upload_id WHERE c.team_id=? AND c.upload_id=? AND u.source_id=? ORDER BY c.chunk_index',[auth.team_id,manifest.upload_id,sourceId]);const bytes=Buffer.concat(chunks.map(x=>x.bytes));if(chunks.length!==manifest.chunks||bytes.length!==manifest.byte_length||sha256(bytes)!==manifest.sha256)fail(409,'capture_conflict','artifact manifest does not match staged chunks');}
240
+ if(commit.native_bundle!=null){const bundle=commit.native_bundle;exact(bundle,['id','harness','fidelity','residue','files'],'native_bundle');if(typeof bundle.id!=='string'||bundle.harness!==cap.harness||!['byte_lossless','value_lossless','semantic'].includes(bundle.fidelity)||!Array.isArray(bundle.residue)||!bundle.residue.every(x=>typeof x==='string')||!Array.isArray(bundle.files))fail(400,'invalid_request','invalid native bundle');const artifactIds=new Set(commit.artifacts.map(x=>x.upload_id)),seen=new Set();for(const file of bundle.files){exact(file,['upload_id','role'],'native_bundle file');if(!artifactIds.has(file.upload_id)||seen.has(file.upload_id)||!['primary','subagent','bundle','source_recovery'].includes(file.role))fail(400,'invalid_request','native bundle files must uniquely reference captured artifacts');seen.add(file.upload_id);}if(seen.size!==artifactIds.size||bundle.files.filter(x=>x.role==='primary').length!==1||new Set(commit.artifacts.map(x=>x.relative_name)).size!==commit.artifacts.length)fail(400,'invalid_request','native bundle must describe every unique artifact and one primary');}
241
+ let origin=await api.store.one('SELECT * FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?',[auth.team_id,sourceId,cap.harness,cap.native_id]);
242
+ let lineage=null,lineageValue=null,lineageMembers=[],lineageGuards=[];if(!origin&&source.kind==='connector'&&source.machine_id){const candidates=await api.store.query("SELECT * FROM continuations WHERE team_id=? AND state='running' AND json_extract(value_json,'$.new_session_id') IS NULL AND json_extract(value_json,'$.harness')=? AND json_extract(value_json,'$.destination.machine_id')=? AND json_extract(value_json,'$.remote_result.state')='succeeded' AND json_extract(value_json,'$.remote_result.receipt.native_session_id')=?",[auth.team_id,cap.harness,source.machine_id,cap.native_id]);if(candidates.length===1){const candidate=candidates[0],membership=await api.store.one('SELECT role,active FROM memberships WHERE team_id=? AND principal_id=?',[auth.team_id,candidate.requester_id]),reader={kind:'user',team_id:auth.team_id,principal_id:candidate.requester_id,role:membership?.role,active:membership?.active===1,now:api.clock.nowMs(),group_ids:(await api.store.query('SELECT group_id FROM group_members WHERE team_id=? AND principal_id=?',[auth.team_id,candidate.requester_id])).map(x=>x.group_id)};if(await canReadMerged(api,reader,await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[auth.team_id,candidate.source_session_id]))){lineage=candidate;lineageValue=JSON.parse(candidate.value_json);lineageMembers=await constituents(api,auth.team_id,await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[auth.team_id,candidate.source_session_id]));}}if(lineageValue){const lineageNow=api.clock.nowMs();lineageGuards=[{sql:'SELECT 1 ok FROM memberships WHERE team_id=? AND principal_id=? AND active=1',params:[auth.team_id,lineage.requester_id],code:'conflict',status:409,message:'continuation requester membership changed'},{sql:'SELECT 1 ok FROM credentials WHERE id=? AND principal_id=? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)',params:[lineageValue.issuer_credential_id,lineage.requester_id,lineageNow],code:'conflict',status:409,message:'continuation issuer credential changed'},...lineageMembers.map(member=>({sql:`SELECT 1 ok FROM sessions s WHERE s.team_id=? AND s.id=? AND s.deleted_at IS NULL AND (?=0 OR s.access_revision=?) AND (s.visibility='team' OR s.custodian_id=? OR EXISTS (SELECT 1 FROM session_grants g WHERE g.team_id=s.team_id AND g.session_id=s.id AND (g.expires_at IS NULL OR g.expires_at>?) AND (g.subject_kind='principal' AND g.subject_id=? OR g.subject_kind='group' AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.team_id=s.team_id AND gm.group_id=g.subject_id AND gm.principal_id=?))))`,params:[auth.team_id,member.id,member.id===lineage.source_session_id?1:0,lineageValue.access_revision,lineage.requester_id,lineageNow,lineage.requester_id,lineage.requester_id],code:'conflict',status:409,message:'continuation source authorization changed'}))];}}
243
+ const sid=origin?.session_id??api.ids.next('session'),parentOrigin=commit.parent_native_id?await api.store.one('SELECT session_id FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND deleted_at IS NULL',[auth.team_id,sourceId,cap.harness,commit.parent_native_id]):null,revisionParam={$journalRevision:true},recorded=commit.recorded_activity_at,boundaryNow=api.clock.nowMs(),sortAt=recorded??boundaryNow,descriptor={id:sid,revision:'__JOURNAL_REVISION__',title:commit.title,harness:cap.harness,user:{principal_id:source.kind==='connector'?source.custodian_id:null,reported_user:commit.reported_user,status:source.kind==='connector'?'verified':commit.reported_user?'reported':'unknown',mapping_id:null},repository:commit.repository,parent_id:parentOrigin?.session_id??null,continued_from:lineage?.source_session_id??null,recorded_activity_at:recorded,received_at:boundaryNow,sort_at:sortAt,activity_basis:recorded==null?'received':'recorded',source_ids:[sourceId],machine_ids:source.machine_id?[source.machine_id]:[],coverage:commit.coverage,bindings:[],actions:{}},statements=[];
244
+ if(!origin)statements.push({sql:'INSERT INTO sessions(team_id,id,harness,custodian_id,visibility,revision,created_at) VALUES (?,?,?,?,?,?,?)',params:[auth.team_id,sid,cap.harness,source.custodian_id,lineage?'restricted':begin.visibility,revisionParam,boundaryNow]},{sql:'INSERT INTO origins(team_id,source_id,harness,native_id,session_id,generation,sequence,digest) VALUES (?,?,?,?,?,?,?,?)',params:[auth.team_id,sourceId,cap.harness,cap.native_id,sid,cap.generation,cap.sequence,commit.digest]});else statements.push({sql:'UPDATE sessions SET revision=? WHERE team_id=? AND id=?',params:[revisionParam,auth.team_id,sid]},{sql:'UPDATE origins SET generation=?,sequence=?,digest=? WHERE team_id=? AND source_id=? AND harness=? AND native_id=?',params:[cap.generation,cap.sequence,commit.digest,auth.team_id,sourceId,cap.harness,cap.native_id]});
245
+ if(lineage){lineageValue.new_session_id=sid;const operationRow=await api.store.one('SELECT value_json FROM operations WHERE team_id=? AND id=?',[auth.team_id,lineageValue.operation_id]),operationValue=JSON.parse(operationRow.value_json);operationValue.result={...operationValue.result,new_session_id:sid};statements.push({sql:"UPDATE continuations SET value_json=? WHERE team_id=? AND id=? AND state='running' AND value_json=?",params:[JSON.stringify(lineageValue),auth.team_id,lineage.id,lineage.value_json]},{sql:'UPDATE operations SET value_json=? WHERE team_id=? AND id=?',params:[JSON.stringify(operationValue),auth.team_id,lineageValue.operation_id]});lineageGuards.push({sql:"SELECT 1 ok FROM continuations WHERE team_id=? AND id=? AND state='running' AND value_json=?",params:[auth.team_id,lineage.id,lineage.value_json],code:'conflict',status:409,message:'continuation lineage changed'});}
246
+ 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
+ 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
+ }