note-state-id.mjs
85 lines 2.8 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 2 days ago
1 /**
2 * Canonical optimistic-concurrency id for vault notes (Hub proposals approve check).
3 * Format: kn1_<16 hex chars> = FNV-1a 64-bit over UTF-8 bytes (documented in docs/PROPOSAL-LIFECYCLE.md).
4 * Absent note: hash of single byte 0x00 (so new-file proposals can require "still absent").
5 */
6
7 /**
8 * @param {Buffer} buf
9 * @returns {string} 16 lowercase hex chars
10 */
11 export function fnv1a64Hex(buf) {
12 let h = 0xcbf29ce484222325n;
13 const prime = 0x100000001b3n;
14 for (let i = 0; i < buf.length; i++) {
15 h ^= BigInt(buf[i]);
16 h = (h * prime) & 0xffffffffffffffffn;
17 }
18 return h.toString(16).padStart(16, '0');
19 }
20
21 /**
22 * Deterministic JSON stringify with sorted object keys (no arrays in frontmatter required for v1).
23 * @param {unknown} value
24 * @returns {string}
25 */
26 export function stableStringify(value) {
27 if (value === null || value === undefined) return 'null';
28 const t = typeof value;
29 if (t === 'number' || t === 'boolean') return JSON.stringify(value);
30 if (t === 'string') return JSON.stringify(value);
31 if (Array.isArray(value)) {
32 return '[' + value.map((x) => stableStringify(x)).join(',') + ']';
33 }
34 if (t === 'object') {
35 const keys = Object.keys(value).sort();
36 return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(value[k])).join(',') + '}';
37 }
38 return JSON.stringify(value);
39 }
40
41 /**
42 * State id when the note path has no file (create flow).
43 * @returns {string}
44 */
45 export function absentNoteStateId() {
46 return 'kn1_' + fnv1a64Hex(Buffer.from([0x00]));
47 }
48
49 /**
50 * State id from parsed frontmatter object + body (matches Hub readNote semantics).
51 * @param {Record<string, unknown>} frontmatter
52 * @param {string} body
53 * @returns {string}
54 */
55 export function noteStateIdFromParts(frontmatter, body) {
56 const fm = stableStringify(frontmatter && typeof frontmatter === 'object' ? frontmatter : {});
57 const payload = `${fm}\0${body ?? ''}`;
58 return 'kn1_' + fnv1a64Hex(Buffer.from(payload, 'utf8'));
59 }
60
61 /**
62 * Hash exact frontmatter JSON text + body (hosted canister / string responses).
63 * @param {string} frontmatterJsonText
64 * @param {string} body
65 * @returns {string}
66 */
67 export function noteStateIdFromRawStrings(frontmatterJsonText, body) {
68 const fm = typeof frontmatterJsonText === 'string' ? frontmatterJsonText : '';
69 const payload = `${fm}\0${body ?? ''}`;
70 return 'kn1_' + fnv1a64Hex(Buffer.from(payload, 'utf8'));
71 }
72
73 /**
74 * Derive kn1_ id from a Hub GET /notes/:path JSON payload (object or string frontmatter).
75 * @param {{ frontmatter?: unknown, body?: string }} data
76 * @returns {string}
77 */
78 export function noteStateIdFromHubNoteJson(data) {
79 const body = data?.body ?? '';
80 const fm = data?.frontmatter;
81 if (typeof fm === 'string') {
82 return noteStateIdFromRawStrings(fm, body);
83 }
84 return noteStateIdFromParts(fm && typeof fm === 'object' ? fm : {}, body);
85 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 2 days ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 2 days ago