6b0c633a75
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index, Turnstile captcha, browser client SDK, publish-panel dataset binding, acceptance tests, and usage documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
2.9 KiB
JavaScript
111 lines
2.9 KiB
JavaScript
import crypto from 'node:crypto';
|
|
|
|
export function createPageDataSessionStore(options = {}) {
|
|
const ttlMs = Number(options.ttlMs ?? 30 * 60 * 1000);
|
|
const sessions = new Map();
|
|
|
|
function hashToken(token) {
|
|
return crypto.createHash('sha256').update(String(token)).digest('hex');
|
|
}
|
|
|
|
function issue({ pageId, ownerUserId, publicationId, accessMode }) {
|
|
const token = crypto.randomBytes(32).toString('base64url');
|
|
const tokenHash = hashToken(token);
|
|
const expiresAt = Date.now() + ttlMs;
|
|
sessions.set(tokenHash, {
|
|
pageId,
|
|
ownerUserId,
|
|
publicationId,
|
|
accessMode,
|
|
sessionId: crypto.randomUUID(),
|
|
expiresAt,
|
|
});
|
|
return {
|
|
token,
|
|
expiresAt,
|
|
sessionId: sessions.get(tokenHash).sessionId,
|
|
};
|
|
}
|
|
|
|
function verify(token) {
|
|
const session = sessions.get(hashToken(token));
|
|
if (!session) return null;
|
|
if (session.expiresAt <= Date.now()) {
|
|
sessions.delete(hashToken(token));
|
|
return null;
|
|
}
|
|
return session;
|
|
}
|
|
|
|
function revoke(token) {
|
|
return sessions.delete(hashToken(token));
|
|
}
|
|
|
|
function revokeByPageId(pageId) {
|
|
let count = 0;
|
|
for (const [key, session] of sessions.entries()) {
|
|
if (session.pageId === pageId) {
|
|
sessions.delete(key);
|
|
count += 1;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function listByPageId(pageId) {
|
|
const now = Date.now();
|
|
const active = [];
|
|
for (const session of sessions.values()) {
|
|
if (session.pageId !== pageId) continue;
|
|
if (session.expiresAt <= now) continue;
|
|
active.push({
|
|
sessionId: session.sessionId,
|
|
pageId: session.pageId,
|
|
ownerUserId: session.ownerUserId,
|
|
accessMode: session.accessMode,
|
|
expiresAt: session.expiresAt,
|
|
});
|
|
}
|
|
return active;
|
|
}
|
|
|
|
function clearExpired() {
|
|
const now = Date.now();
|
|
for (const [key, session] of sessions.entries()) {
|
|
if (session.expiresAt <= now) sessions.delete(key);
|
|
}
|
|
}
|
|
|
|
return { issue, verify, revoke, revokeByPageId, listByPageId, clearExpired };
|
|
}
|
|
|
|
export function createPageDataRateLimiter(options = {}) {
|
|
const windowMs = Number(options.windowMs ?? 60_000);
|
|
const maxRequests = Number(options.maxRequests ?? 30);
|
|
const buckets = new Map();
|
|
|
|
function check(key) {
|
|
const now = Date.now();
|
|
const bucket = buckets.get(key) ?? { count: 0, resetAt: now + windowMs };
|
|
if (now >= bucket.resetAt) {
|
|
bucket.count = 0;
|
|
bucket.resetAt = now + windowMs;
|
|
}
|
|
bucket.count += 1;
|
|
buckets.set(key, bucket);
|
|
if (bucket.count > maxRequests) {
|
|
throw Object.assign(new Error('请求过于频繁,请稍后再试'), {
|
|
code: 'rate_limited',
|
|
status: 429,
|
|
});
|
|
}
|
|
return { remaining: Math.max(0, maxRequests - bucket.count), resetAt: bucket.resetAt };
|
|
}
|
|
|
|
return { check };
|
|
}
|
|
|
|
export function hashClientMeta(value) {
|
|
return crypto.createHash('sha256').update(String(value ?? '')).digest('hex').slice(0, 16);
|
|
}
|