bridge-index-background.mjs
122 lines 5.6 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 17 hours ago
1 /**
2 * Netlify Background Function: long-running re-index path for the bridge.
3 *
4 * The `-background` filename suffix is what tells Netlify to give this function
5 * the 15-minute timeout (vs 60 s for synchronous functions like `bridge.mjs`)
6 * and to return 202 to the caller within ~50 ms regardless of how long the
7 * actual work takes (docs.netlify.com/build/functions/background-functions).
8 *
9 * How a re-index reaches this function:
10 * 1. Browser → `POST /api/v1/index` (proxied to `netlify/functions/bridge.mjs`).
11 * 2. Sync handler runs preflight (`lib/bridge-index-preflight-estimate.mjs`)
12 * and decides this work won't fit in 60 s.
13 * 3. Sync handler `acquireJobLock` + HTTP-POSTs to THIS endpoint with HMAC
14 * headers (signed by `lib/bridge-internal-hmac.mjs`).
15 * 4. THIS function validates the HMAC, mounts the same Express app from
16 * `hub/bridge/server.mjs` (which contains the `POST /api/v1/index` route),
17 * sets `globalThis.__bridge_internal_request` so the route SKIPS the
18 * routing decision and runs inline, then invokes the route via
19 * `serverless-http`.
20 * 5. After the route finishes (success OR error), the route's own code
21 * releases the job lock and writes `setLastIndexedAt`.
22 *
23 * Auth (defense in depth):
24 * - HMAC over `(canisterUid, vaultId, jobId, ts)` signed with `SESSION_SECRET`
25 * proves the request came from the bridge sync handler.
26 * - JWT in `Authorization: Bearer …` is forwarded verbatim from the original
27 * browser request, so `requireBridgeAuth` still requires a real user.
28 * - This function refuses any path other than `POST /api/v1/index` so the
29 * blast radius of a future HMAC bypass would be limited to the same route
30 * a forged sync request could already hit.
31 */
32
33 import serverless from 'serverless-http';
34 import { connectLambda, getStore } from '@netlify/blobs';
35 import { app } from '../../hub/bridge/server.mjs';
36 import { verifyInternalRequest } from '../../lib/bridge-internal-hmac.mjs';
37
38 /**
39 * Reject anything other than `POST` to the index route. Defense-in-depth: the
40 * HMAC + JWT already gate the request, but this is one more belt so a future
41 * routing mistake cannot accidentally expose another endpoint.
42 */
43 function isAllowedRoute(httpMethod, path) {
44 if (httpMethod !== 'POST') return false;
45 if (typeof path !== 'string') return false;
46 // Netlify forwards the original request path, possibly prefixed with the
47 // function path. Accept any of: `/api/v1/index`, `/.netlify/functions/bridge-index-background`,
48 // `/.netlify/functions/bridge-index-background/api/v1/index`.
49 if (path === '/api/v1/index') return true;
50 if (path === '/.netlify/functions/bridge-index-background') return true;
51 if (path === '/.netlify/functions/bridge-index-background/') return true;
52 if (path === '/.netlify/functions/bridge-index-background/api/v1/index') return true;
53 return false;
54 }
55
56 export const handler = async (event, context) => {
57 const headers = (event && event.headers) || {};
58 const httpMethod = (event && event.httpMethod) || 'POST';
59 const eventPath = (event && (event.path || event.rawPath)) || '';
60
61 // 1. Route guard.
62 if (!isAllowedRoute(httpMethod, eventPath)) {
63 return {
64 statusCode: 404,
65 headers: { 'content-type': 'application/json' },
66 body: JSON.stringify({ error: 'Not found' }),
67 };
68 }
69
70 // 2. HMAC validation. The shared secret must match what the sync function
71 // (running with the SAME `SESSION_SECRET` env var) used to sign the request.
72 // `HUB_JWT_SECRET` is checked as a legacy fallback to match `userIdFromJwt`
73 // in `hub/bridge/server.mjs`.
74 const secret = process.env.SESSION_SECRET || process.env.HUB_JWT_SECRET || '';
75 const verified = verifyInternalRequest(secret, {
76 canisterUid: headers['x-bridge-internal-uid'],
77 vaultId: headers['x-bridge-internal-vault-id'],
78 jobId: headers['x-bridge-internal-job-id'],
79 ts: headers['x-bridge-internal-ts'],
80 sig: headers['x-bridge-internal-sig'],
81 });
82 if (!verified.ok) {
83 console.warn('[bridge-index-background] HMAC verification failed:', verified.reason);
84 return {
85 statusCode: 401,
86 headers: { 'content-type': 'application/json' },
87 body: JSON.stringify({ error: 'Unauthorized internal request', reason: verified.reason }),
88 };
89 }
90
91 // 3. Set up the Netlify Blob store the same way `bridge.mjs` does. Eventual
92 // consistency is fine for the background path since the lock + sidecar
93 // blobs are read by the next user-triggered request, not by the caller of
94 // this background function.
95 connectLambda(event);
96 const consistency =
97 String(process.env.NETLIFY_BLOBS_CONSISTENCY || '')
98 .trim()
99 .toLowerCase() === 'strong'
100 ? 'strong'
101 : 'eventual';
102 globalThis.__netlify_blob_store = getStore({ name: 'bridge-data', consistency });
103
104 // 4. Set the internal-request marker that `hub/bridge/server.mjs` reads via
105 // `req.bridgeInternalRequest`. This is what tells the index handler to
106 // SKIP its routing decision and execute inline regardless of size.
107 globalThis.__bridge_internal_request = {
108 canisterUid: verified.payload.canisterUid,
109 vaultId: verified.payload.vaultId,
110 jobId: verified.payload.jobId,
111 };
112
113 try {
114 // Force the path the express app sees to `/api/v1/index` so the route resolver
115 // hits the right handler regardless of how Netlify rewrote the URL.
116 const routedEvent = { ...event, path: '/api/v1/index', rawUrl: '/api/v1/index' };
117 return await serverless(app)(routedEvent, context);
118 } finally {
119 delete globalThis.__netlify_blob_store;
120 delete globalThis.__bridge_internal_request;
121 }
122 };
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 17 hours ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 23 hours ago