apply-note-provenance.mjs
76 lines 2.8 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Hosted gateway: align note POST bodies with hub/server.mjs provenance rules before proxying to the canister.
3 *
4 * IMPORTANT: `frontmatter` must remain a JSON **object** on the wire (Express will stringify the whole body once).
5 * If we set `frontmatter` to JSON.stringify(merged), the POST body contains `"frontmatter":"{\"k\":...}"` and Motoko's
6 * extractJsonString copies `\\"` as two characters instead of unescaping — stored text is invalid JSON and the
7 * Hub shows `{}`. Sending a nested object lets Motoko use extractJsonObjectSlice and persist valid JSON text.
8 */
9
10 import { mergeProvenanceFrontmatter } from '../../lib/hub-provenance.mjs';
11
12 /**
13 * Merge hosted provenance fields (and optional AIR attestation id) into a note POST/PUT body
14 * before forwarding to the canister.
15 *
16 * `frontmatter` must remain a JSON **object** on the wire so Motoko's `extractJsonObjectSlice`
17 * persists valid JSON. Passing a pre-stringified value causes double-escaping.
18 *
19 * @param {unknown} body Parsed POST/PUT body (Express json())
20 * @param {string | null} userId X-User-Id value (e.g. google:123)
21 * @param {string | null} [airId] Attestation ID from attestBeforeWrite; injected when non-null
22 * @returns {unknown}
23 */
24 export function mergeHostedNoteBodyForCanister(body, userId, airId = null) {
25 if (!body || typeof body !== 'object' || Array.isArray(body)) return body;
26 /** @type {Record<string, unknown>} */
27 const out = { ...body };
28 let clientFm = out.frontmatter;
29 if (typeof clientFm === 'string') {
30 try {
31 clientFm = clientFm.trim() ? JSON.parse(clientFm) : {};
32 } catch {
33 clientFm = {};
34 }
35 } else if (!clientFm || typeof clientFm !== 'object' || Array.isArray(clientFm)) {
36 clientFm = {};
37 }
38 const merged = mergeProvenanceFrontmatter(
39 /** @type {Record<string, unknown>} */ (clientFm),
40 {
41 sub: userId || null,
42 kind: 'human',
43 }
44 );
45 // Improvement B: inject attestation id when present
46 if (airId) {
47 merged.air_id = airId;
48 }
49 out.frontmatter = merged;
50 return out;
51 }
52
53 /**
54 * @param {string} method
55 * @param {string} pathPart URL path without query (e.g. /api/v1/notes)
56 */
57 export function isPostApiV1Notes(method, pathPart) {
58 if (method !== 'POST') return false;
59 const p = (pathPart || '').replace(/\/+$/, '') || '/';
60 return p === '/api/v1/notes';
61 }
62
63 /**
64 * Returns true for note-write requests: POST /api/v1/notes (create) or
65 * PUT /api/v1/notes/:path (update). Used to gate AIR attestation on the gateway.
66 * @param {string} method
67 * @param {string} pathPart URL path without query
68 */
69 export function isNoteWriteRequest(method, pathPart) {
70 const p = (pathPart || '').replace(/\/+$/, '') || '/';
71 if (method === 'POST' && p === '/api/v1/notes') return true;
72 if (method === 'PUT' && p.startsWith('/api/v1/notes/')) return true;
73 return false;
74 }
75
76 export { pathPartNoQuery } from './request-path.mjs';
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago