docs-routes.mjs
254 lines 9.1 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago
1 /**
2 * Hosted bridge REST routes for docs connectors (KN-DOCS-SYNC-b).
3 *
4 * DOCS_OAUTH_GOOGLE_AUTHORIZED flipped true 2026-08-17 (Tier 3).
5 * DOCS_NOTION_HUB_KEY_AUTHORIZED flipped true 2026-08-22 (Tier 3).
6 * Production never passes authorizedOverride.
7 * Blob hydrate/persist uses strong consistency for pending OAuth state (D17).
8 *
9 * @see docs/KN-DOCS-SYNC-FREEZE.md
10 * @see hub/bridge/calendar-blob-store.mjs (pattern sibling)
11 */
12
13 import path from 'path';
14 import fs from 'fs';
15 import { createProposal } from '../proposals-store.mjs';
16 import {
17 createProductionGoogleDriveClient,
18 createProductionNotionClient,
19 handleBeginDocsProvider,
20 handleDocsConnectorAction,
21 handleDocsConnectorCallbackUnified,
22 handleListAllDocsConnectors,
23 } from '../../lib/docs/docs-api.mjs';
24 import { withDocsBlobSync } from './docs-blob-store.mjs';
25
26 /**
27 * Resolve a writable vault path for docs import proposals on the bridge.
28 * Hosted lambdas use DATA_DIR/vaults/{vaultId}; self-tests may inject vaultPath.
29 *
30 * @param {string} dataDir
31 * @param {string} vaultId
32 * @returns {string}
33 */
34 export function bridgeDocsVaultPath(dataDir, vaultId) {
35 const safe = typeof vaultId === 'string' && vaultId.trim() ? vaultId.trim() : 'default';
36 const dir = path.join(dataDir, 'vaults', safe);
37 fs.mkdirSync(dir, { recursive: true });
38 return dir;
39 }
40
41 /**
42 * @param {import('express').Express} app
43 * @param {{
44 * dataDir: string,
45 * requireBridgeAuth: import('express').RequestHandler,
46 * requireBridgeEditorOrAdmin: import('express').RequestHandler,
47 * resolveHostedBridgeContext: (req: import('express').Request, actorUid: string) => Promise<{
48 * ok: boolean,
49 * status?: number,
50 * error?: string,
51 * code?: string,
52 * vaultId?: string,
53 * }>,
54 * resolveHostedBridgeSettingsContext: (req: import('express').Request, actorUid: string) => {
55 * allowedVaultIds: string[],
56 * },
57 * sanitizeVaultId: (raw: unknown) => string,
58 * }} deps
59 */
60 export function registerBridgeDocsRoutes(app, deps) {
61 const {
62 dataDir,
63 requireBridgeAuth,
64 requireBridgeEditorOrAdmin,
65 resolveHostedBridgeContext,
66 resolveHostedBridgeSettingsContext,
67 sanitizeVaultId,
68 } = deps;
69
70 app.get('/api/v1/docs/connectors/callback', async (req, res) => {
71 try {
72 const result = await withDocsBlobSync({
73 blobStore: req.blobStore,
74 dataDir,
75 run: async () => {
76 const googleClient = createProductionGoogleDriveClient();
77 return handleDocsConnectorCallbackUnified({
78 dataDir,
79 query: req.query,
80 googleClient,
81 env: process.env,
82 });
83 },
84 });
85 if (result.redirect) {
86 return res.redirect(result.status, result.redirect);
87 }
88 return res.status(result.status).json({ code: result.code });
89 } catch {
90 return res.status(500).json({ error: 'Callback failed', code: 'RUNTIME_ERROR' });
91 }
92 });
93
94 app.post('/api/v1/docs/connectors', requireBridgeAuth, requireBridgeEditorOrAdmin, async (req, res) => {
95 const hctx = await resolveHostedBridgeContext(req, req.uid);
96 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
97 const result = await withDocsBlobSync({
98 blobStore: req.blobStore,
99 dataDir,
100 run: () =>
101 handleBeginDocsProvider({
102 dataDir,
103 vaultId: hctx.vaultId,
104 body: req.body,
105 env: process.env,
106 }),
107 });
108 if (!result.ok) {
109 return res.status(result.status).json({ error: result.error ?? 'Not authorized', code: result.code });
110 }
111 return res.status(result.status).json(result.payload);
112 });
113
114 app.get('/api/v1/docs/connectors', requireBridgeAuth, async (req, res) => {
115 const hctx = await resolveHostedBridgeSettingsContext(req, req.uid);
116 const vaultId = sanitizeVaultId(req.headers['x-vault-id']);
117 if (!hctx.allowedVaultIds.includes(vaultId)) {
118 return res.status(403).json({ error: 'Access to this vault is not allowed.', code: 'FORBIDDEN' });
119 }
120 const result = await withDocsBlobSync({
121 blobStore: req.blobStore,
122 dataDir,
123 persist: false,
124 run: () =>
125 handleListAllDocsConnectors({
126 dataDir,
127 vaultId,
128 }),
129 });
130 if (!result.ok) {
131 return res.status(result.status).json({ error: result.error ?? 'Not authorized', code: result.code });
132 }
133 return res.json(result.payload);
134 });
135
136 app.get('/api/v1/docs/connectors/:id/files', requireBridgeAuth, async (req, res) => {
137 const hctx = await resolveHostedBridgeSettingsContext(req, req.uid);
138 const vaultId = sanitizeVaultId(req.headers['x-vault-id']);
139 if (!hctx.allowedVaultIds.includes(vaultId)) {
140 return res.status(403).json({ error: 'Access to this vault is not allowed.', code: 'FORBIDDEN' });
141 }
142 const connectorId = typeof req.params.id === 'string' ? decodeURIComponent(req.params.id).trim() : '';
143 try {
144 const result = await withDocsBlobSync({
145 blobStore: req.blobStore,
146 dataDir,
147 run: async () =>
148 handleDocsConnectorAction('list', {
149 dataDir,
150 vaultId,
151 connectorId,
152 query: req.query,
153 env: process.env,
154 googleClient: createProductionGoogleDriveClient(),
155 notionClient: createProductionNotionClient(),
156 }),
157 });
158 if (!result.ok) {
159 return res.status(result.status).json({ code: result.code });
160 }
161 return res.status(result.status).json(result.payload);
162 } catch (e) {
163 return res.status(500).json({ error: e.message, code: 'RUNTIME_ERROR' });
164 }
165 });
166
167 app.post('/api/v1/docs/connectors/:id/import', requireBridgeAuth, requireBridgeEditorOrAdmin, async (req, res) => {
168 const hctx = await resolveHostedBridgeContext(req, req.uid);
169 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
170 const connectorId = typeof req.params.id === 'string' ? decodeURIComponent(req.params.id).trim() : '';
171 const vaultPath = bridgeDocsVaultPath(dataDir, hctx.vaultId);
172 try {
173 const result = await withDocsBlobSync({
174 blobStore: req.blobStore,
175 dataDir,
176 run: async () =>
177 handleDocsConnectorAction('import', {
178 dataDir,
179 vaultPath,
180 vaultId: hctx.vaultId,
181 connectorId,
182 body: req.body,
183 env: process.env,
184 googleClient: createProductionGoogleDriveClient(),
185 notionClient: createProductionNotionClient(),
186 createProposalFn: createProposal,
187 }),
188 });
189 if (!result.ok) {
190 return res.status(result.status).json({ code: result.code });
191 }
192 return res.status(result.status).json(result.payload);
193 } catch (e) {
194 return res.status(500).json({ error: e.message, code: 'RUNTIME_ERROR' });
195 }
196 });
197
198 app.post('/api/v1/docs/connectors/:id/sync', requireBridgeAuth, requireBridgeEditorOrAdmin, async (req, res) => {
199 const hctx = await resolveHostedBridgeContext(req, req.uid);
200 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
201 const connectorId = typeof req.params.id === 'string' ? decodeURIComponent(req.params.id).trim() : '';
202 const vaultPath = bridgeDocsVaultPath(dataDir, hctx.vaultId);
203 try {
204 const result = await withDocsBlobSync({
205 blobStore: req.blobStore,
206 dataDir,
207 run: async () =>
208 handleDocsConnectorAction('sync', {
209 dataDir,
210 vaultPath,
211 vaultId: hctx.vaultId,
212 connectorId,
213 env: process.env,
214 googleClient: createProductionGoogleDriveClient(),
215 notionClient: createProductionNotionClient(),
216 createProposalFn: createProposal,
217 }),
218 });
219 if (!result.ok) {
220 return res.status(result.status).json({ code: result.code });
221 }
222 return res.status(result.status).json(result.payload);
223 } catch (e) {
224 return res.status(500).json({ error: e.message, code: 'RUNTIME_ERROR' });
225 }
226 });
227
228 app.delete('/api/v1/docs/connectors/:id', requireBridgeAuth, requireBridgeEditorOrAdmin, async (req, res) => {
229 const hctx = await resolveHostedBridgeContext(req, req.uid);
230 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
231 const connectorId = typeof req.params.id === 'string' ? decodeURIComponent(req.params.id).trim() : '';
232 try {
233 const result = await withDocsBlobSync({
234 blobStore: req.blobStore,
235 dataDir,
236 run: async () =>
237 handleDocsConnectorAction('revoke', {
238 dataDir,
239 vaultId: hctx.vaultId,
240 connectorId,
241 env: process.env,
242 googleClient: createProductionGoogleDriveClient(),
243 notionClient: createProductionNotionClient(),
244 }),
245 });
246 if (!result.ok) {
247 return res.status(result.status).json({ code: result.code });
248 }
249 return res.status(result.status).json(result.payload);
250 } catch (e) {
251 return res.status(500).json({ error: e.message, code: 'RUNTIME_ERROR' });
252 }
253 });
254 }
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago