diff options
| author | srdusr <[email protected]> | 2025-12-15 20:44:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-12-15 20:44:00 +0200 |
| commit | 1740327557074df0c8b99635ee949e2540ac94d0 (patch) | |
| tree | eb88af6056350798fa3f53ff4349b1e947c71d6e /scripts | |
| parent | 3dbebfbc9345d2603908f32c0dabebc0ff21feb3 (diff) | |
| download | typerpunk-1740327557074df0c8b99635ee949e2540ac94d0.tar.gz typerpunk-1740327557074df0c8b99635ee949e2540ac94d0.zip | |
Harden for production: dependencies, headers, admin roles, docs
Dependencies
- The server build carried 37 known advisories, including RUSTSEC-2024-0363
in sqlx 0.7, which is the database layer. sqlx moved to 0.8 with
default-features off, which also drops the MySQL and SQLite drivers and
with them rsa and RUSTSEC-2023-0071. reqwest moved to 0.12, which brings
hyper 1.x and was the sole source of every remaining advisory: h2 0.3,
rustls-webpki 0.101, rustls-pemfile 1.0 and idna 0.3.
- The server build now reports no known vulnerabilities against OSV. cargo
audit itself would not compile, so the check queries OSV with the crate
versions cargo tree reports for the server binary.
- Cargo.lock is committed. This workspace produces binaries, so the lockfile
is what makes a deployed build reproducible and the audit above meaningful.
Headers
- The application sent no security headers at all. The static server now
sends a Content-Security-Policy, nosniff, frame options, a referrer policy
and a permissions policy; the API sends a policy of its own, since it
serves JSON and should load and frame nothing.
- The one inline script in index.html moved to a file so script-src needs no
unsafe-inline. WebAssembly needs wasm-unsafe-eval, without which nothing
types at all, so that is present and explained.
- Five style attributes moved to the CSSOM rather than adding unsafe-inline
for styles. A style attribute in markup is refused by the policy; the same
property set through element.style is not.
Production configuration
- With TYPERPUNK_ENV=production the server refuses to start if COOKIE_SECURE
is off, if DATABASE_URL is still the development default, or if
FRONTEND_ORIGIN is http on a non-local host. These were warnings, and a
warning in a log nobody reads is not a safeguard.
Administration
- Moderators were appointed with psql. There is now an admin role,
bootstrapped from TYPERPUNK_ADMIN_USERNAME at startup, and a UI to appoint
and remove moderators. An administrator's own role cannot be changed
through the API, so a mistake cannot lock everyone out of moderation.
Corpus
- scripts/export_approved.js writes approved submissions back into
data/packs/community-*.json. Approved passages are served from the database
and merged at startup, so without this the repository dataset and the live
corpus drift apart, and a fresh checkout or the TUI sees only what shipped.
Documentation
- README rewritten for the repository: what it does, how to run it, the pack
format, the server variables, deployment, and what the security posture
actually is. Plain English, no em dashes, no emoji.
Checked and found already correct: every private endpoint refuses anonymous
callers, session cookies are HttpOnly and SameSite=Lax, CORS names a single
origin, internal errors are logged rather than returned, and every query is
parameterised.
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/export_approved.js | 61 |
1 files changed, 61 insertions, 0 deletions
diff --git a/scripts/export_approved.js b/scripts/export_approved.js new file mode 100755 index 0000000..b2cb556 --- /dev/null +++ b/scripts/export_approved.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +/* + Pulls approved community submissions out of a running server and writes them + into data/packs/community-<category>.json, so the repository dataset and the + live corpus do not drift apart. + + Approved passages are served from the database, and the client merges them on + top of the bundled packs at startup. That is what makes them appear without a + release. It also means a fresh checkout, an offline run, or the TUI (which + reads texts.json directly) sees only what shipped. Running this and committing + the result folds the live corpus back into the repository. + + Usage: + node scripts/export_approved.js [http://localhost:8787] + node scripts/merge_packs.js +*/ +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const PACKS_DIR = path.join(ROOT, 'data', 'packs'); +const base = (process.argv[2] || process.env.TYPERPUNK_API || 'http://localhost:8787').replace(/\/$/, ''); + +async function main() { + const res = await fetch(`${base}/api/texts`); + if (!res.ok) throw new Error(`${base}/api/texts returned ${res.status}`); + const items = await res.json(); + if (!Array.isArray(items) || items.length === 0) { + console.log('no approved submissions to export'); + return; + } + + // Grouped by category so an export lands in the same shape as a hand + // written pack, and a reviewer can read the diff. + const byCategory = new Map(); + for (const item of items) { + if (!item.content || !item.category) continue; + const entry = { category: item.category, content: item.content }; + if (item.attribution) entry.attribution = item.attribution; + if (item.language) entry.language = item.language; + if (!byCategory.has(item.category)) byCategory.set(item.category, []); + byCategory.get(item.category).push(entry); + } + + let written = 0; + for (const [category, entries] of byCategory) { + // Sorted by content so re-exporting produces the same file rather than + // a reordered one, which would make every diff unreadable. + entries.sort((a, b) => a.content.localeCompare(b.content)); + const file = path.join(PACKS_DIR, `community-${category}.json`); + fs.writeFileSync(file, JSON.stringify(entries, null, 2) + '\n'); + console.log(` ${path.relative(ROOT, file)}: ${entries.length}`); + written += entries.length; + } + console.log(`exported ${written} approved passages; run scripts/merge_packs.js next`); +} + +main().catch(err => { + console.error('export failed:', err.message); + process.exit(1); +}); |