Spaces:
Sleeping
Sleeping
File size: 3,111 Bytes
99a44ac e5e233a 9b49194 e5e233a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
const ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
export function newId(len = 10) {
const bytes = crypto.randomBytes(len)
let out = ''
for (let i = 0; i < len; i++) out += ID_ALPHABET[bytes[i] % ID_ALPHABET.length]
return out
}
export function atomicWriteFile(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
const tmp = path.join(path.dirname(filePath), `.tmp-${newId(6)}`)
fs.writeFileSync(tmp, data)
fs.renameSync(tmp, filePath)
}
export function readJSON(filePath, fallback) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
} catch {
return fallback
}
}
export function writeJSON(filePath, value) {
atomicWriteFile(filePath, JSON.stringify(value, null, 2))
}
export function b64encode(uint8) {
return Buffer.from(uint8).toString('base64url')
}
export function b64decode(str) {
return new Uint8Array(Buffer.from(str, 'base64url'))
}
// --- signed session cookies ---
export function signSession(payload, secret) {
const body = Buffer.from(JSON.stringify(payload)).toString('base64url')
const mac = crypto.createHmac('sha256', secret).update(body).digest('base64url')
return `${body}.${mac}`
}
export function verifySession(value, secret) {
if (!value || !value.includes('.')) return null
const [body, mac] = value.split('.')
const expected = crypto.createHmac('sha256', secret).update(body).digest('base64url')
const a = Buffer.from(mac)
const b = Buffer.from(expected)
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null
try {
return JSON.parse(Buffer.from(body, 'base64url').toString('utf8'))
} catch {
return null
}
}
export function parseCookies(header) {
const out = {}
if (!header) return out
for (const part of header.split(';')) {
const idx = part.indexOf('=')
if (idx === -1) continue
out[part.slice(0, idx).trim()] = decodeURIComponent(part.slice(idx + 1).trim())
}
return out
}
export const HANDLE_RE = /^[a-z0-9][a-z0-9_.-]{1,38}$/
export const DOC_ID_RE = /^[A-Za-z0-9_-]{1,64}$/
// --- multi-page projects ---
// A project (former "doc") holds named page fragments inside one Yjs document.
// API code still uses `${projectId}::${slug}` as a convenient logical page name;
// the collaboration socket and persisted state use the bare project id.
export const PAGE_SLUG_RE = /^(_structure|[a-z0-9][a-z0-9-]{0,39})$/
export const DOC_NAME_RE = /^[A-Za-z0-9_-]{1,64}(::(_structure|[a-z0-9][a-z0-9-]{0,39}))?$/
export function projectIdOf(docName) {
return String(docName).split('::')[0]
}
export function pageSlugOf(docName) {
const parts = String(docName).split('::')
return parts[1] || 'home'
}
export function docNameFor(projectId, slug) {
return !slug || slug === 'home' ? projectId : `${projectId}::${slug}`
}
export function slugify(title) {
const s = String(title || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 40)
return s || 'page'
}
|