hub-provenance.mjs
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
sha256:6a102aafafdfe7e70a24f4e59740200f0ee713ce7915f1b53e9d4ba5ee8b4410
Initial Muse snapshot
Human
48 days ago