212ff3ff80
Introduce UMS ingest/snapshot pipeline, Context Planner with multi-source recall, runtime context injection, canonical user mapping, and session snapshot loading on auth/me. Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
2.9 KiB
JavaScript
105 lines
2.9 KiB
JavaScript
import crypto from 'node:crypto';
|
|
|
|
function newId() {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
function nowMs() {
|
|
return Date.now();
|
|
}
|
|
|
|
function validateEnvelope(item) {
|
|
const required = [
|
|
'evidence_id',
|
|
'user_id',
|
|
'source_type',
|
|
'source_ref',
|
|
'occurred_at',
|
|
'evidence_type',
|
|
'payload',
|
|
'content_hash',
|
|
'schema_version',
|
|
];
|
|
for (const key of required) {
|
|
if (item[key] === undefined || item[key] === null || item[key] === '') {
|
|
return { ok: false, reason: `missing ${key}` };
|
|
}
|
|
}
|
|
if (item.schema_version !== 1) return { ok: false, reason: 'unsupported schema_version' };
|
|
if (item.privacy_level === 'secure_skip') return { ok: false, reason: 'secure_skip' };
|
|
return { ok: true };
|
|
}
|
|
|
|
/**
|
|
* @param {import('mysql2/promise').Pool} pool
|
|
* @param {{ items: object[], dry_run?: boolean }} input
|
|
*/
|
|
export async function ingestEvidenceBatch(pool, { items, dry_run = false }) {
|
|
const accepted = [];
|
|
const duplicates = [];
|
|
const rejected = [];
|
|
|
|
for (const item of items ?? []) {
|
|
const check = validateEnvelope(item);
|
|
if (!check.ok) {
|
|
rejected.push({ evidence_id: item.evidence_id ?? null, reason: check.reason });
|
|
continue;
|
|
}
|
|
|
|
const [existing] = await pool.query(
|
|
`SELECT evidence_id, content_hash FROM um_evidence WHERE user_id = ? AND content_hash = ? LIMIT 1`,
|
|
[item.user_id, item.content_hash],
|
|
);
|
|
if (existing[0]) {
|
|
duplicates.push(existing[0].evidence_id);
|
|
continue;
|
|
}
|
|
|
|
const [byId] = await pool.query(`SELECT content_hash FROM um_evidence WHERE evidence_id = ? LIMIT 1`, [
|
|
item.evidence_id,
|
|
]);
|
|
if (byId[0] && byId[0].content_hash !== item.content_hash) {
|
|
rejected.push({ evidence_id: item.evidence_id, reason: 'evidence_conflict' });
|
|
continue;
|
|
}
|
|
|
|
if (dry_run) {
|
|
accepted.push(item.evidence_id);
|
|
continue;
|
|
}
|
|
|
|
await pool.query(
|
|
`INSERT INTO um_evidence
|
|
(evidence_id, user_id, source_type, source_ref, evidence_type, occurred_at, received_at,
|
|
content_hash, schema_version, privacy_level, payload_json)
|
|
VALUES (?, ?, ?, ?, ?, ?, NOW(3), ?, ?, ?, ?)`,
|
|
[
|
|
item.evidence_id,
|
|
item.user_id,
|
|
item.source_type,
|
|
item.source_ref,
|
|
item.evidence_type,
|
|
item.occurred_at.replace('T', ' ').replace('Z', '').slice(0, 23),
|
|
item.content_hash,
|
|
item.schema_version,
|
|
item.privacy_level ?? 'normal',
|
|
JSON.stringify(item.payload),
|
|
],
|
|
);
|
|
accepted.push(item.evidence_id);
|
|
}
|
|
|
|
return { accepted, duplicates, rejected };
|
|
}
|
|
|
|
export async function updateIngestCursor(pool, userId, sourceType, cursorValue) {
|
|
await pool.query(
|
|
`INSERT INTO um_ingest_cursors (user_id, source_type, cursor_value, updated_at)
|
|
VALUES (?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE cursor_value = VALUES(cursor_value), updated_at = VALUES(updated_at)`,
|
|
[userId, sourceType, cursorValue, nowMs()],
|
|
);
|
|
}
|
|
|
|
export { validateEnvelope };
|