feat(page-data): add private and public dataset API (phase 1-3)

Extract UserDataSpaceService for shared SQLite access, wire logged-in Page
Data routes, and add public insert plus password-token read/update/delete with
policy storage, rate limits, and regression tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-08 12:44:23 +08:00
parent 1c8e0db256
commit 8d01553469
17 changed files with 2587 additions and 189 deletions
+52
View File
@@ -0,0 +1,52 @@
import fs from 'node:fs';
import path from 'node:path';
import { normalizePageAccessPolicy } from './page-access-policy.mjs';
export function resolvePageDataPolicyDir(workspaceRoot) {
return path.join(workspaceRoot, '.mindspace', 'page-data-policies');
}
export function resolvePageDataPolicyPath(workspaceRoot, pageId) {
const safePageId = String(pageId ?? '').trim();
if (!safePageId || /[\\/]/.test(safePageId)) {
throw Object.assign(new Error('pageId 无效'), { code: 'invalid_page_id' });
}
return path.join(resolvePageDataPolicyDir(workspaceRoot), `${safePageId}.json`);
}
export function readPageAccessPolicy(workspaceRoot, pageId) {
const policyPath = resolvePageDataPolicyPath(workspaceRoot, pageId);
if (!fs.existsSync(policyPath)) return null;
const raw = JSON.parse(fs.readFileSync(policyPath, 'utf8'));
return normalizePageAccessPolicy(raw, { fallbackPageId: pageId });
}
export function writePageAccessPolicy(workspaceRoot, policyInput) {
const policy = normalizePageAccessPolicy(policyInput);
const policyDir = resolvePageDataPolicyDir(workspaceRoot);
fs.mkdirSync(policyDir, { recursive: true });
const policyPath = resolvePageDataPolicyPath(workspaceRoot, policy.pageId);
fs.writeFileSync(
policyPath,
`${JSON.stringify({ ...policy, updatedAt: new Date().toISOString() }, null, 2)}\n`,
'utf8',
);
return readPageAccessPolicy(workspaceRoot, policy.pageId);
}
export function deletePageAccessPolicy(workspaceRoot, pageId) {
const policyPath = resolvePageDataPolicyPath(workspaceRoot, pageId);
if (!fs.existsSync(policyPath)) return false;
fs.unlinkSync(policyPath);
return true;
}
export function listPageAccessPolicies(workspaceRoot) {
const policyDir = resolvePageDataPolicyDir(workspaceRoot);
if (!fs.existsSync(policyDir)) return [];
return fs
.readdirSync(policyDir)
.filter((name) => name.endsWith('.json'))
.map((name) => readPageAccessPolicy(workspaceRoot, name.replace(/\.json$/, '')))
.filter(Boolean);
}