{"id":50443814,"url":"https://github.com/ericrihm/sc-cpe","last_synced_at":"2026-05-31T20:02:35.251Z","repository":{"id":351633585,"uuid":"1210762167","full_name":"ericrihm/sc-cpe","owner":"ericrihm","description":"Auto-issues CPE/CEU certificates to Simply Cyber Daily Threat Briefing YouTube livestream attendees. PAdES-T signed PDFs, hash-chained append-only audit log, Cloudflare-native (Pages Functions + D1 + R2 + Workers).","archived":false,"fork":false,"pushed_at":"2026-05-11T14:37:19.000Z","size":1719,"stargazers_count":0,"open_issues_count":33,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-11T15:28:21.621Z","etag":null,"topics":["audit-log","cloudflare-d1","cloudflare-workers","continuing-education","cpe","cybersecurity","pades","simplycyber"],"latest_commit_sha":null,"homepage":"https://sc-cpe-web.pages.dev","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ericrihm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-14T18:23:11.000Z","updated_at":"2026-05-11T14:46:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ericrihm/sc-cpe","commit_stats":null,"previous_names":["ericrihm/sc-cpe"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ericrihm/sc-cpe","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fsc-cpe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fsc-cpe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fsc-cpe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fsc-cpe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ericrihm","download_url":"https://codeload.github.com/ericrihm/sc-cpe/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fsc-cpe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33746514,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audit-log","cloudflare-d1","cloudflare-workers","continuing-education","cpe","cybersecurity","pades","simplycyber"],"created_at":"2026-05-31T20:02:34.987Z","updated_at":"2026-05-31T20:02:35.245Z","avatar_url":"https://github.com/ericrihm.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/assets/sample-cert.png\" alt=\"SC-CPE Certificate Sample\" width=\"520\"\u003e\n  \u003cbr/\u003e\n  \u003cstrong\u003eSC-CPE\u003c/strong\u003e — Simply Cyber CPE Certificates\n  \u003cbr/\u003e\n  \u003cem\u003eAutomatic, cryptographically verifiable continuing-education certificates\u003cbr/\u003efor everyone who shows up to the Daily Threat Briefing.\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/ericrihm/sc-cpe/actions/workflows/deploy-prod.yml\"\u003e\u003cimg src=\"https://github.com/ericrihm/sc-cpe/actions/workflows/deploy-prod.yml/badge.svg?branch=main\" alt=\"Deploy\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/ericrihm/sc-cpe/actions/workflows/smoke.yml\"\u003e\u003cimg src=\"https://github.com/ericrihm/sc-cpe/actions/workflows/smoke.yml/badge.svg\" alt=\"Smoke\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/ericrihm/sc-cpe/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/ericrihm/sc-cpe/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://sc-cpe-web.pages.dev/status.html\"\u003e\u003cimg src=\"https://img.shields.io/badge/status-live-brightgreen?style=flat\" alt=\"Status: Live\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://sc-cpe-web.pages.dev/verify.html\"\u003e\u003cimg src=\"https://img.shields.io/badge/certs-PAdES--T%20signed-blue?style=flat\" alt=\"PAdES-T Signed\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-MIT-blue?style=flat\" alt=\"MIT License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n**[Verify a certificate](https://sc-cpe-web.pages.dev/verify.html)** · **[Leaderboard](https://sc-cpe-web.pages.dev/leaderboard.html)** · **[Show links](https://sc-cpe-web.pages.dev/links.html)** · **[Public profiles](https://sc-cpe-web.pages.dev/profile.html)** · **[Contribute](CONTRIBUTING.md)**\n\n# SC-CPE\n\nSecurity professionals need CPE/CEU credits to keep their certifications active, but tracking attendance is manual, records are inconsistent, and there's no way for an auditor to independently verify a claim. SC-CPE fixes this — it watches the Simply Cyber Daily Threat Briefing YouTube live chat, matches per-user verification codes, and issues PAdES-T signed PDF certificates that are cryptographically verifiable offline, years later, without contacting the issuer.\n\n---\n\n## What \u0026 Why\n\nSC-CPE watches the [Simply Cyber Daily Threat Briefing](https://www.youtube.com/@simplycyber) YouTube live chat, matches per-user verification codes, and issues **signed PDF certificates** worth **0.5 CPE / CEU per session**. Every certificate is PAdES-T signed with an RFC-3161 timestamp and anchored to an append-only, hash-chained audit log — verifiable offline, years later, without contacting the issuer.\n\nCPE tracking is a pain point for security professionals — manual logs, inconsistent records, and no way to independently verify attendance. Unlike traditional self-reported CPE systems, SC-CPE solves this by automating the entire pipeline from livestream attendance through signed certificate delivery, giving holders a credential that any auditor can check without trusting the issuer.\n\n\u003e [!TIP]\n\u003e 20 weekday briefings/month = **10 CPE**. Enough to cover a significant chunk of most annual renewal requirements.\n\n---\n\n## Supported Programs\n\n| Program | Credit | Per Session | Submission Format |\n|:--------|:-------|:------------|:------------------|\n| **CompTIA** (Security+, CySA+, CASP+, PenTest+, Network+ ...) | CEU | 0.5 CEU | Proof-of-attendance: name, date(s), hours, provider, signature |\n| **ISC2** (CISSP, SSCP, CCSP ...) | CPE | 0.5 CPE (Group B) | Same fields — upload under \"Education\" |\n| **ISACA** (CISM, CISA, CRISC, CGEIT, CDPSE ...) | CPE | 0.5 CPE | All 7 ISACA audit-evidence fields present |\n\nAcceptance is ultimately the certification body's decision — see [Terms §5](https://sc-cpe-web.pages.dev/terms.html#5).\n\n\u003e [!IMPORTANT]\n\u003e **ISACA 2027 update:** Starting January 2027, ISACA splits CPE into *certification-aligned* (90 CPE min) and *professional-aligned* (30 CPE max). The Daily Threat Briefing covers threats, risk management, security operations, and governance — all certification-aligned domains. SC-CPE certificates already include the activity description field needed for domain-relevance verification.\n\n---\n\n## Quickstart\n\n```\n1. Register    →  sc-cpe-web.pages.dev  (email + legal name + Turnstile)\n2. Get code    →  check your email for SC-CPE{XXXX-XXXX}\n3. Post code   →  paste it in YouTube live chat during the briefing\n4. Get credit  →  shows on your dashboard within ~60 seconds\n5. Get cert    →  per-session (~2h) or monthly bundle — your pick\n```\n\nYour dashboard link arrives by email from `certs@signalplane.co`. Lost it? Just visit [`/dashboard`](https://sc-cpe-web.pages.dev/dashboard.html) — an inline login form emails you a fresh link. You can also opt-in to \"remember this device\" so your dashboard loads without the URL token.\n\n---\n\n## How It Works\n\n\u003cp align=\"center\"\u003e\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/assets/diagram-how-it-works-dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"docs/assets/diagram-how-it-works-light.svg\"\u003e\n  \u003cimg alt=\"How it works — registration through cert delivery\" src=\"docs/assets/diagram-how-it-works-light.svg\" width=\"280\"\u003e\n\u003c/picture\u003e\n\u003c/p\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eDetailed data flow\u003c/strong\u003e\u003c/summary\u003e\n\n```\n 1.  Register at sc-cpe-web.pages.dev\n     → email + Turnstile. Dashboard link + SC-CPE{XXXX-XXXX} code arrive\n       by email. The HTTP response never contains them — email possession\n       is the activation gate.\n\n 2.  Post your code in YouTube live chat during the stream.\n     → The poller (every minute, 08:00-11:00 ET Mon-Fri) ingests chat,\n       matches your code, and credits 0.5 CPE. Pre-stream chat and\n       replays don't count. Dashboard tells you if you posted too early.\n\n 3.  Pick cert style in the dashboard: per-session, bundled, or both.\n     → Per-session certs arrive within ~2h. Bundled certs ship monthly.\n       Both are PAdES-T signed.\n\n 4.  Submit to your CE portal.\n     → Upload the PDF under \"webinars/seminars/training.\" The cert is\n       the proof document.\n\n 5.  Verify any cert at /verify.html\n     → Drop the PDF on the page. SHA-256 is recomputed client-side and\n       compared to the registered hash. Anyone — including auditors —\n       can check without contacting us.\n```\n\n\u003c/details\u003e\n\n---\n\n## Architecture\n\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/assets/diagram-architecture-dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"docs/assets/diagram-architecture-light.svg\"\u003e\n  \u003cimg alt=\"Architecture — Cloudflare Edge, External services, GitHub Actions\" src=\"docs/assets/diagram-architecture-light.svg\" width=\"100%\"\u003e\n\u003c/picture\u003e\n\n| Component | Tech | What it does |\n|:----------|:-----|:-------------|\n| **Pages Functions** | Cloudflare Pages | Registration, dashboard, verify, profiles, admin API, analytics, CSV export, links archive |\n| **Poller** | CF Worker · `* * * * *` | Polls YouTube live chat (OAuth + API-key fallback), matches codes, credits attendance, updates streaks, extracts show links |\n| **Email Sender** | CF Worker · `*/2 * * * *` | Drains `email_outbox` via Resend |\n| **Purge** | CF Worker · `0 9 * * *` | Daily R2 chat GC, security digest, weekly digest, cert nudge, renewal nudges, link enrichment, monthly digest, Discord webhooks |\n| **Cert Signer** | Python 3.11 (GH Actions) | WeasyPrint render + `endesive` PAdES-T with RFC-3161 |\n| **Chat Rescan** | Python (GH Actions) · daily 16:00 UTC | Recovers missed attendance from chat replays |\n| **D1** | Cloudflare SQLite | Single source of truth — schema in `db/schema.sql` |\n| **R2** | Cloudflare Object Storage | Raw chat JSONL (purges daily) + signed PDF certs + weekly backups |\n| **KV** | Cloudflare KV | Rate-limit counters + circuit breaker state |\n\n---\n\n## Tech Stack\n\n| Layer | Tech |\n|:------|:-----|\n| Frontend | Cloudflare Pages (static HTML + JS, CSP `script-src 'self'`) |\n| API | Cloudflare Pages Functions (V8 isolates) |\n| Workers | Cloudflare Workers (poller, purge, email-sender) |\n| Database | Cloudflare D1 (SQLite) |\n| Storage | Cloudflare R2 (certs, chat, backups) |\n| Caching | Cloudflare KV (rate limits, circuit breakers) |\n| Email | Resend (DKIM + SPF; DMARC DNS pending) |\n| Certs | Python + WeasyPrint + endesive (PAdES-T) |\n| CI/CD | GitHub Actions (13 workflows) |\n\n---\n\n## Certificate Integrity\n\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/assets/diagram-cert-integrity-dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"docs/assets/diagram-cert-integrity-light.svg\"\u003e\n  \u003cimg alt=\"Certificate integrity — four layers of verification\" src=\"docs/assets/diagram-cert-integrity-light.svg\" width=\"100%\"\u003e\n\u003c/picture\u003e\n\n\u003e [!NOTE]\n\u003e Anyone can generate a PDF that says \"attended.\" SC-CPE certificates are different because each one is anchored to four independent, durable pieces of evidence.\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd width=\"50%\"\u003e\n\n**1. Time-gated attendance** — The poller only credits messages whose YouTube `publishedAt` falls inside the live window. Pre-stream chat and replays don't count. Rejected attempts are logged and surfaced on your dashboard.\n\n**2. Hash-chained audit log** — Every state transition is recorded in an append-only, SHA-256 chained table with a `UNIQUE INDEX` on `prev_hash`. Chain forks fail at insert time. `verify_audit_chain.py` replays the full chain.\n\n\u003c/td\u003e\n\u003ctd width=\"50%\"\u003e\n\n**3. PAdES-T + RFC-3161** — Certs are signed with a dedicated CA-rooted key and bound to a trusted timestamp authority. The signature outlives the key's validity period. The signing cert fingerprint is on the face of every PDF.\n\n**4. Public verify URL** — Each cert carries a `/verify.html?t=...` link anyone can open — no login required. Drop the PDF on the page and the browser recomputes its SHA-256 client-side against the registered hash.\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n```\nuser_registered → code_matched → attendance_credited → cert_issued → email_sent\n       ▲                                                      ▲\n       └──────── prev_hash = sha256(canonicalAuditRow(tip)) ──┘\n```\n\n---\n\n## Features\n\n### Cert Delivery Options\n\n| Option | Best for | Delivery |\n|:-------|:---------|:---------|\n| **Per-session** | CompTIA (1 activity per submission) | On demand, ~2h after request |\n| **Monthly bundle** | ISC2 / ISACA (annual rollup) | Auto-generated end of month |\n| **Both** | Multiple certifications | Per-session + monthly |\n\nChange your preference anytime from the dashboard.\n\n### Community \u0026 Engagement\n\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/assets/diagram-features-dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"docs/assets/diagram-features-light.svg\"\u003e\n  \u003cimg alt=\"Features — Dashboard, Community, Admin, and Communications\" src=\"docs/assets/diagram-features-light.svg\" width=\"100%\"\u003e\n\u003c/picture\u003e\n\n**User dashboard** — Attendance calendar, streak tracking (current + longest, weekday-aware), renewal countdown, annual summary, bulk cert download, appeal flow, inline sign-in, and device-memory opt-in.\n\n**Community** — Opt-in leaderboard with streak column, public profiles (privacy-safe: first name + last initial), shareable SVG badges, show links archive with RSS feed.\n\n**Admin \u0026 ops** — Analytics dashboard (growth, engagement, certs, system charts), CSV export (users / attendance / certs), appeal resolution queue, feature toggles, cert reissue/revoke/resend, manual attendance grants.\n\n**Communications** — Monthly digest, weekly digest, cert nudges, renewal milestone emails (50% / 75% / 90% + 30-day warning), Discord webhooks (cert announcements + weekly leaderboard top-5).\n\n---\n\n## Observability\n\n| Signal | Source | Cadence |\n|:-------|:-------|:--------|\n| Poller heartbeat | D1 `heartbeats` | Every minute (during stream window) |\n| Purge / security / digest / cert nudge / renewal nudge | D1 `heartbeats` | Daily / weekly / monthly |\n| Email sender | D1 `heartbeats` | Every 2 minutes |\n| Synthetic canary | GH Actions `smoke.yml` | Hourly — pings prod, writes canary heartbeat |\n| Watchdog | GH Actions `watchdog.yml` | Every 15 min — `/api/health` poll, Discord alerts |\n| Audit chain | GH Actions `audit-chain-weekly.yml` | Weekly full chain walk |\n| Schema drift | GH Actions `schema-drift.yml` | Weekly D1-vs-`schema.sql` diff |\n| Chat rescan | GH Actions `rescan-chat.yml` | Daily 16:00 UTC — recovers missed attendance |\n| D1 backup | GH Actions `backup.yml` | Weekly Sun 06:00 UTC → R2 + GitHub Artifact |\n| CodeQL | GH Actions `codeql.yml` | Weekly + on push |\n\nLive status: [`/status.html`](https://sc-cpe-web.pages.dev/status.html) (auto-refreshes every 30s)\n\n---\n\n## CI/CD Pipeline\n\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/assets/diagram-cicd-dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"docs/assets/diagram-cicd-light.svg\"\u003e\n  \u003cimg alt=\"CI/CD pipeline — push to live in ~2 min\" src=\"docs/assets/diagram-cicd-light.svg\" width=\"100%\"\u003e\n\u003c/picture\u003e\n\nBranch protection on `main`: PRs required, `Node test suite` + `Secret scan (gitleaks)` must pass, no force-push. Auto-merge enabled — `gh pr merge --auto --squash` lands the PR the moment checks go green.\n\nD1 migrations in `db/migrations/` are applied automatically during deploy — the pipeline tracks applied files in `_applied_migrations` and only runs new ones.\n\n**PR previews** — Every pull request gets an isolated preview deployment with its own D1, KV, and R2 bindings. The [`deploy-preview.yml`](.github/workflows/deploy-preview.yml) workflow applies migrations, seeds test data, deploys, and comments the preview URL on the PR.\n\n---\n\n## API\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e49 endpoints — expand for full table\u003c/strong\u003e\u003c/summary\u003e\n\n### Public\n\n| Path | Auth | Purpose |\n|:-----|:-----|:--------|\n| `POST /api/register` | Turnstile | Sign up |\n| `POST /api/recover` | Turnstile | Recover dashboard link via email |\n| `GET /api/health` | public | External watchdog poll |\n| `GET /api/verify/{token}` | public | Cert verification data |\n| `GET /api/crl.json` | public | Certificate revocation list |\n| `GET /api/leaderboard` | public | Community leaderboard (top 20) |\n| `GET /api/links` | public | Show links archive |\n| `GET /api/links/rss` | public | Show links RSS feed |\n| `GET /api/profile/{token}` | public | Public profile (privacy-safe stats) |\n| `GET /api/badge/{token}` | public | SVG achievement badge |\n| `GET /api/download/{token}` | public | Cert PDF download |\n| `GET /api/preflight/channel` | public | YouTube channel pre-check |\n| `POST /api/csp-report` | public | CSP violation reports |\n\n### User (dashboard-token + CSRF)\n\n| Path | Auth | Purpose |\n|:-----|:-----|:--------|\n| `GET /api/me/{token}` | dashboard-token | User dashboard data |\n| `POST /api/me/{token}/prefs` | + CSRF | Set cert style, nudge opt-out, leaderboard opt-in |\n| `POST /api/me/{token}/cert-per-session/{stream_id}` | + CSRF | Request single-session cert |\n| `POST /api/me/{token}/cert-feedback` | + CSRF | Report cert typo/error |\n| `POST /api/me/{token}/resend-code` | + CSRF | Get a fresh verification code |\n| `POST /api/me/{token}/appeal` | + CSRF | Appeal missed attendance credit |\n| `POST /api/me/{token}/delete` | + CSRF | Account deletion (GDPR) |\n| `POST /api/me/{token}/rotate` | + CSRF | Rotate dashboard token |\n| `GET /api/me/{token}/annual-summary` | dashboard-token | Year-at-a-glance stats |\n\n### Admin (bearer token)\n\n| Path | Auth | Purpose |\n|:-----|:-----|:--------|\n| `GET /api/admin/heartbeat-status` | bearer | Per-source staleness |\n| `GET /api/admin/audit-chain-verify` | bearer | Full chain walk |\n| `GET /api/admin/ops-stats` | bearer | Dashboard counts + warnings |\n| `GET /api/admin/cert-feedback` | bearer | Non-ok feedback inbox |\n| `GET /api/admin/users` | bearer | User search |\n| `GET /api/admin/user/{id}/certs` | bearer | Certs for a specific user |\n| `GET /api/admin/attendance` | bearer | Attendance records |\n| `GET /api/admin/appeals` | bearer | Pending appeals |\n| `POST /api/admin/appeals/{id}/resolve` | bearer | Resolve appeal |\n| `POST /api/admin/cert/{id}/reissue` | bearer | Queue cert regeneration |\n| `POST /api/admin/cert/{token}/resend` | bearer | Resend cert email |\n| `POST /api/admin/revoke` | bearer | Revoke a certificate |\n| `GET /api/admin/export` | bearer | CSV export (users, attendance, certs) |\n| `GET /api/admin/security-events` | bearer | Security event log |\n| `GET /api/admin/streams` | bearer | Recent streams with attendance counts |\n| `POST /api/admin/suspend` | bearer | Suspend / unsuspend a user |\n| `GET/DELETE /api/admin/email-suppression` | bearer | Email suppression list management |\n| `GET /api/admin/analytics/growth` | bearer | User growth time series |\n| `GET /api/admin/analytics/engagement` | bearer | Attendance engagement metrics |\n| `GET /api/admin/analytics/certs` | bearer | Certificate issuance stats |\n| `GET /api/admin/analytics/system` | bearer | System health metrics |\n| `POST /api/admin/canary-beat` | bearer | Hourly smoke heartbeat |\n| `GET/POST /api/admin/toggles` | bearer | Feature toggles |\n| `GET /api/admin/auth/login` | bearer | Admin OAuth login |\n| `GET /api/admin/auth/callback` | bearer | Admin OAuth callback |\n| `GET /api/admin/auth/logout` | bearer | Admin logout |\n| `GET /api/watchdog-state` | bearer | Watchdog health state |\n\n\u003c/details\u003e\n\n---\n\n## Design Decisions\n\n- **Hash-chained audit log over simple event table** — append-only with SHA-256 `prev_hash` ensures tampering is detectable years later without a trusted third party. A `UNIQUE INDEX` on `prev_hash` serialises concurrent writers at the database level.\n\n- **Dashboard token over password auth** — a single bearer URL means no password resets, no session management, no cookie consent banners. Trade-off: URL sharing leaks access, mitigated by a separate `badge_token` for public-facing URLs.\n\n- **PAdES-T over simple PDF signing** — RFC-3161 timestamps make certificates verifiable even after the signing key expires. More complexity, but the cert is the product — it must stand on its own.\n\n- **Email-queue pattern over direct send** — decouples email delivery from the request path, enabling retry with idempotency, backpressure visibility via the outbox table, and a clean cursor-advance-on-success contract.\n\n---\n\n## Who Runs This\n\n**Simply Cyber LLC** (United States). Fully open-source at [`github.com/ericrihm/sc-cpe`](https://github.com/ericrihm/sc-cpe) — every line that decides who gets credit, every policy doc, every deploy workflow. Branch protection + required CI + auto-deploy means the deployed code is the exact SHA on `main`.\n\n| Domain | Purpose |\n|:-------|:--------|\n| `sc-cpe-web.pages.dev` | Web + API (canonical origin) |\n| `cpe.simplycyber.io` | Reserved — future DNS wiring |\n| `signalplane.co` | Email domain (Resend DKIM + SPF; DMARC DNS pending) |\n\nSecurity disclosure: [`security.txt`](https://sc-cpe-web.pages.dev/.well-known/security.txt) or email `certs@signalplane.co` with `[SECURITY]` in the subject.\n\n---\n\n## Testing \u0026 Development\n\n```bash\nscripts/install_hooks.sh                 # pre-push hook → runs test suite\nbash scripts/test.sh                     # pure-logic tests (388 tests)\nscripts/check_schema.sh                  # diff live D1 schema vs repo\nADMIN_TOKEN=... ORIGIN=https://sc-cpe-web.pages.dev \\\n  scripts/smoke_hardening.sh             # read-only probe of deployed origin\n```\n\n## Deploying\n\nAuto-deploy on every merge to `main` via [`deploy-prod.yml`](.github/workflows/deploy-prod.yml):\ntests → D1 migrations → Pages → Workers (parallel) → post-deploy smoke. ~2 min on a warm runner.\n\n```bash\ngit checkout -b fix/whatever\ngit commit -m \"fix(scope): description\"\ngit push -u origin fix/whatever\ngh pr create --fill \u0026\u0026 gh pr merge --auto --squash\n```\n\n\u003e [!CAUTION]\n\u003e **Break-glass only:** `enforce_admins: false` allows admin direct-push to `main`. The push trigger still fires `deploy-prod` with full test suite. Re-engage protection immediately after.\n\n---\n\n## Repo Map\n\n```\npages/                 Cloudflare Pages Functions — public web surface\n  functions/api/       JSON API (register, dashboard, admin, analytics, verify, links, profiles)\n  _lib.js              Shared helpers (audit, rate-limit, email, crypto)\n  _heartbeat.js        Staleness predicates for heartbeat monitoring\n  _middleware.js        Security headers (CSP, HSTS, COOP, CORP)\n  *.html / *.js / *.css  15 pages, extracted JS/CSS (CSP-safe, no inline scripts)\nworkers/\n  poller/              Per-minute livestream chat poller (OAuth + API-key fallback)\n  purge/               Daily R2 GC + digests + nudges + link enrichment + Discord webhooks\n  email-sender/        Drains email_outbox via Resend\nservices/certs/        Python PDF issuer (PAdES-T + RFC-3161)\ndb/\n  schema.sql           Authoritative schema\n  migrations/          Append-only numbered migrations (auto-applied on deploy)\n  seed-preview.sql     Test data for PR preview environments\nscripts/               Smoke, schema check, audit verifier, tests, backups, chat rescan\n.github/workflows/     13 workflows: CI, deploy (prod + preview), smoke, watchdog,\n                       cert crons, schema drift, backups, chat rescan, CodeQL\ndocs/\n  DESIGN.md            Architecture decisions\n  RUNBOOK.md           Operator procedures\n  LTV.md               Legal/compliance reasoning (GDPR Art. 17(3)(e))\n  VERIFIER_GUIDE.md    Third-party cert verification guide\n  PITCH.md             Simply Cyber team pitch\n```\n\n## Contributing\n\nBuilt for the [Simply Cyber](https://www.youtube.com/@SimplyCyber) community. Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).\n\nFound a bug or have feedback? [Open an issue](https://github.com/ericrihm/sc-cpe/issues) or email certs@signalplane.co.\n\n## License\n\nMIT — see [LICENSE](LICENSE) for details. Cert artefacts are retained under GDPR Art. 17(3)(e) as evidentiary records.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericrihm%2Fsc-cpe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fericrihm%2Fsc-cpe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericrihm%2Fsc-cpe/lists"}