Files
memind/page-access-policy.mjs
T
john 8d01553469 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>
2026-07-08 12:44:23 +08:00

122 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { assertSafeSqlIdentifier, normalizeDatasetConfig } from './user-data-space-service.mjs';
export const PAGE_DATA_ACCESS_MODES = new Set(['public', 'password', 'login_required']);
export function normalizePolicyDataset(name, raw) {
const datasetName = assertSafeSqlIdentifier(name, 'dataset 名称');
if (!raw || typeof raw !== 'object') {
throw Object.assign(new Error(`dataset 策略无效:${datasetName}`), { code: 'invalid_policy' });
}
const columns = raw.columns && typeof raw.columns === 'object' ? raw.columns : {};
const normalizedColumns = {};
for (const [action, fields] of Object.entries(columns)) {
if (!Array.isArray(fields)) continue;
normalizedColumns[action] = fields.map((field) => assertSafeSqlIdentifier(field, `${action} 字段`));
}
return {
name: datasetName,
read: Boolean(raw.read),
insert: Boolean(raw.insert),
update: Boolean(raw.update),
softDelete: Boolean(raw.softDelete ?? raw.soft_delete),
hardDelete: Boolean(raw.hardDelete ?? raw.hard_delete),
columns: normalizedColumns,
limits:
raw.limits && typeof raw.limits === 'object'
? {
maxRowsPerRead: Number(raw.limits.maxRowsPerRead ?? 100),
maxInsertBytes: Number(raw.limits.maxInsertBytes ?? 8192),
}
: undefined,
};
}
export function normalizePageAccessPolicy(raw, { fallbackPageId = null, fallbackOwnerUserId = null } = {}) {
if (!raw || typeof raw !== 'object') {
throw Object.assign(new Error('Page Access Policy 无效'), { code: 'invalid_policy' });
}
const pageId = String(raw.pageId ?? fallbackPageId ?? '').trim();
const ownerUserId = String(raw.ownerUserId ?? fallbackOwnerUserId ?? '').trim();
if (!pageId) throw Object.assign(new Error('缺少 pageId'), { code: 'invalid_policy' });
if (!ownerUserId) throw Object.assign(new Error('缺少 ownerUserId'), { code: 'invalid_policy' });
const accessMode = String(raw.accessMode ?? raw.access_mode ?? 'public').trim();
if (!PAGE_DATA_ACCESS_MODES.has(accessMode)) {
throw Object.assign(new Error(`不支持的 accessMode${accessMode}`), { code: 'invalid_policy' });
}
const datasetsInput = raw.datasets && typeof raw.datasets === 'object' ? raw.datasets : {};
const datasets = {};
for (const [name, config] of Object.entries(datasetsInput)) {
datasets[assertSafeSqlIdentifier(name, 'dataset 名称')] = normalizePolicyDataset(name, config);
}
return {
pageId,
ownerUserId,
workspaceRef: String(raw.workspaceRef ?? raw.workspace_ref ?? '').trim() || null,
accessMode,
datasets,
updatedAt: raw.updatedAt ?? new Date().toISOString(),
};
}
export function buildEffectiveDataset(registryDataset, policyDataset) {
if (!registryDataset || !policyDataset) return null;
const actions = [];
if (policyDataset.read) actions.push('read');
if (policyDataset.insert) actions.push('insert');
if (policyDataset.update) actions.push('update');
if (policyDataset.softDelete) actions.push('soft_delete');
if (policyDataset.hardDelete) actions.push('hard_delete');
const columns = {
read: policyDataset.columns?.read ?? registryDataset.columns?.read ?? [],
insert: policyDataset.columns?.insert ?? registryDataset.columns?.insert ?? [],
update: policyDataset.columns?.update ?? registryDataset.columns?.update ?? [],
soft_delete: policyDataset.columns?.soft_delete ?? registryDataset.columns?.soft_delete ?? ['id'],
};
return normalizeDatasetConfig({
name: registryDataset.name,
table: registryDataset.table,
description: registryDataset.description,
actions,
columns,
limits: policyDataset.limits ?? registryDataset.limits,
});
}
export function defaultDatasetsForAccessMode(accessMode, datasets = {}) {
const normalized = {};
for (const [name, config] of Object.entries(datasets)) {
const base = normalizePolicyDataset(name, config);
if (accessMode === 'public') {
normalized[name] = {
...base,
read: false,
update: false,
softDelete: false,
hardDelete: false,
};
} else if (accessMode === 'password') {
normalized[name] = {
...base,
hardDelete: false,
};
} else {
normalized[name] = {
...base,
hardDelete: false,
};
}
}
return normalized;
}
export function policyAllowsAction(policy, datasetName, action) {
const dataset = policy?.datasets?.[datasetName];
if (!dataset) return false;
if (action === 'read') return dataset.read;
if (action === 'insert') return dataset.insert;
if (action === 'update') return dataset.update;
if (action === 'soft_delete') return dataset.softDelete;
if (action === 'hard_delete') return dataset.hardDelete;
return false;
}