hub-provenance.mjs
73 lines 2.3 KB
Raw
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2 feat(auth): persistent login system + C7 session introspection Human minor ⚠ breaking 16 days ago
1 /**
2 * Hub-mediated note writes: server-controlled frontmatter keys for accountability.
3 * Clients cannot forge reserved keys — merge always applies server values last.
4 */
5
6 const RESERVED = new Set([
7 'knowtation_editor',
8 'knowtation_edited_at',
9 'author_kind',
10 'knowtation_proposed_by',
11 'knowtation_approved_by',
12 ]);
13
14 /**
15 * Remove reserved keys from a frontmatter object (e.g. untrusted client input).
16 * @param {Record<string, unknown> | null | undefined} fm
17 * @returns {Record<string, string>}
18 */
19 export function stripReservedFrontmatterKeys(fm) {
20 if (!fm || typeof fm !== 'object' || Array.isArray(fm)) return {};
21 /** @type {Record<string, string>} */
22 const out = {};
23 for (const [k, v] of Object.entries(fm)) {
24 if (RESERVED.has(k)) continue;
25 if (v === undefined || v === null) continue;
26 out[k] = typeof v === 'string' ? v : String(v);
27 }
28 return out;
29 }
30
31 /**
32 * Merge client/body frontmatter with server provenance. Reserved keys always come from the server.
33 *
34 * @param {Record<string, unknown> | null | undefined} clientFrontmatter
35 * @param {{
36 * sub?: string | null,
37 * kind: 'human' | 'webhook' | 'agent' | 'import',
38 * now?: string,
39 * proposedBy?: string | null,
40 * approvedBy?: string | null,
41 * }} opts
42 * @returns {Record<string, string>}
43 */
44 function parseClientFrontmatterInput(raw) {
45 if (raw == null) return null;
46 if (typeof raw === 'object' && !Array.isArray(raw)) return raw;
47 if (typeof raw === 'string') {
48 const t = raw.replace(/^\uFEFF/, '').trim();
49 if (!t) return {};
50 try {
51 const parsed = JSON.parse(t);
52 return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
53 } catch {
54 return {};
55 }
56 }
57 return null;
58 }
59
60 export function mergeProvenanceFrontmatter(clientFrontmatter, opts) {
61 const now = opts.now ?? new Date().toISOString();
62 const coerced = parseClientFrontmatterInput(clientFrontmatter);
63 const base = stripReservedFrontmatterKeys(coerced ?? {});
64 /** @type {Record<string, string>} */
65 const prov = {
66 author_kind: opts.kind,
67 knowtation_edited_at: now,
68 };
69 if (opts.sub) prov.knowtation_editor = String(opts.sub);
70 if (opts.proposedBy) prov.knowtation_proposed_by = String(opts.proposedBy);
71 if (opts.approvedBy) prov.knowtation_approved_by = String(opts.approvedBy);
72 return { ...base, ...prov };
73 }
File History 2 commits
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2 feat(auth): persistent login system + C7 session introspection Human minor 16 days ago