# Copy this file to wrangler.toml and fill in the IDs. # wrangler.toml is gitignored (it holds your real binding IDs). # BEGIN:must-edit-banner # Generated by `npm run config:build` from scripts/config-registry.ts — do not edit by hand. # # ── START HERE ──────────────────────────────────────────────────────── # # Most of this file is optional flags with sane defaults. These are the # 4 values that ship as placeholders and must be replaced before you # deploy — everything else can wait: # # ALLOWED_ORIGINS — comma-separated origins allowed to embed and call /api/* # ADMIN_EMAILS — comma-separated emails that get auto-admin on OAuth signup # PUBLIC_BASE_URL — public URL of this Worker; used in permalinks and email bodies # OAUTH_CALLBACK_BASE — must match the redirect URI registered with each provider # # Plus the [[routes]] block below, if you want a custom subdomain rather # than *.workers.dev. # # `npm run setup` walks you through the rest (D1, KV, secrets) and # reprints this list when it finishes. # # ────────────────────────────────────────────────────────────────────── # END:must-edit-banner name = "garrul" main = "src/index.ts" compatibility_date = "2025-11-01" compatibility_flags = ["nodejs_compat"] # Recommended: point at your custom subdomain (e.g. comments.example.com). # Falling back to .workers.dev works but causes third-party-cookie # friction in some browsers — see docs. # routes = [ # { pattern = "comments.example.com", custom_domain = true } # ] # Used for build-time embed.js inlining of the API base URL. [vars] ENV = "production" EDIT_WINDOW_MINUTES = "15" ALLOWED_ORIGINS = "https://yourblog.example.com" ADMIN_EMAILS = "you@example.com" EMAIL_PROVIDER = "resend" # From address for digest emails. Must be on a domain verified in Resend. EMAIL_FROM = "Garrul " # Optional. Global ceiling on outbound subscription-confirmation email, counted # in D1. Also editable at runtime from /admin/settings. Global, not per-reader: # a spent window turns away new subscribers until it rolls (confirmed # subscribers and their digests are unaffected). Watch `wrangler tail` for # "confirmation email budget exhausted" before raising. The daily default sits # above Resend's free-tier 100/day on purpose — lower it if you'd rather Garrul # stop before your provider does. See docs/ANTISPAM.md. # CONFIRM_SEND_BURST_MAX = "20" # sends per 60s window # CONFIRM_SEND_DAILY_MAX = "200" # sends per 24h window # Public base URL of this worker, used for /c/:id permalinks inside digest emails # and for unsubscribe links. Same value as OAUTH_CALLBACK_BASE in most setups. PUBLIC_BASE_URL = "https://comments.example.com" # Optional. The public URL of this instance (e.g. "https://comments.example.com"). # Only set this if the Worker sits behind a proxy or custom domain where the # inbound `Host` header doesn't match your canonical address. Used by the # /AGENTS.md route to produce ready-to-paste embed snippets for AI assistants. # CANONICAL_URL = "https://comments.example.com" # Optional. Vulnerability-disclosure contact published at # /.well-known/security.txt (RFC 9116): an email address, or an https:// or # mailto: URI. Unset, the route answers 404. Editable at runtime from the # admin Settings page (a saved setting there overrides whatever is set here). # SECURITY_CONTACT = "security@example.com" # OAuth callback base — must match the redirect URI you registered with # each provider (e.g. https://comments.example.com/api/v1/auth/github/callback). # Leave unset in dev; the worker falls back to the request origin. OAUTH_CALLBACK_BASE = "https://comments.example.com" # Optional. Set "1"/"true" to hide the "Powered by Garrul" line under the # comment list. Unset = attribution shown. # BRANDING_HIDDEN = "false" # Feature flags. These are env-var DEFAULTS — an operator can override any of # them at runtime from the admin Settings page (a DB row wins over the env # var). Set "0"/"false" to disable. Comment-level features default ON; the # page-level features default OFF so an upgrade doesn't surface new UI on an # instance that didn't opt in. # # Comment voting (downvotes are implicitly off when voting itself is off): # VOTING_ENABLED = "true" # DOWNVOTES_ENABLED = "true" # Accepting new comments: # COMMENTS_ENABLED = "true" # Per-comment emoji reactions: # REACTIONS_ENABLED = "true" # Page-level engagement — react / vote on the article itself (default off): # PAGE_REACTIONS_ENABLED = "false" # PAGE_VOTES_ENABLED = "false" # Keep deleted comments in the thread as a placeholder ("[deleted]" / # "[removed by a moderator]") instead of pruning leaf deletions (default off): # SHOW_DELETED_PLACEHOLDERS = "false" # Email you when comments land in the moderation queue or get reported # (default off — one debounced digest per cron tick, never one mail per # comment). Needs the email block above; silently does nothing without it. # Recipients default to ADMIN_EMAILS; set the second var only if the alerts # belong somewhere else, such as a shared moderation@ alias. # MODERATOR_EMAIL_ENABLED = "false" # MODERATOR_NOTIFY_EMAILS = "moderation@example.com" # Require a Turnstile challenge on every comment instead of only anonymous # ones (default off — a signed-in author posts without one). Does nothing # unless TURNSTILE_SITE_KEY/TURNSTILE_SECRET are set. Also editable at # runtime from the admin Settings page: # TURNSTILE_ALWAYS = "false" # Display & pagination (all optional; clamped server-side). Also editable at # runtime from the admin Settings page. Top-level comments per initial load / # "Load older" click (default 25, range 1-200 — set 100 for pre-v1.10 behavior): # COMMENTS_PER_PAGE = "25" # Replies shown per parent before a "Show N more replies" button (0 = all): # REPLIES_PER_THREAD = "3" # Replies at this nesting depth or deeper start collapsed (0 = never): # AUTO_COLLAPSE_DEPTH = "3" # Language for the widget, the Atom feed and notification emails. "auto" (the # default) follows the host page's ; an explicit tag overrides it. # Comment text itself is never translated. # DEFAULT_LOCALE = "auto" # Order top-level comments load in when the embed doesn't ask: "new" (newest # first, the default), "old" (oldest first — chronological) or "top" (highest # score, needs VOTING_ENABLED). Readers can still switch order in the widget. # DEFAULT_SORT = "new" # Thread lifecycle — auto-close threads to new comments (existing comments, # reactions and votes stay live). Evaluated lazily at read/write time, no cron. # Both default 0 = disabled. Close a thread N days after its article was # published (host passes data-published; falls back to first-comment time): # AUTO_CLOSE_DAYS = "0" # Hard sunset: close ALL threads at this epoch-ms timestamp (admin Settings uses # a date picker that writes the epoch): # AUTO_CLOSE_AT = "0" # Community auto-collapse — reader-side, reversible fold of low-scored comments # (requires DOWNVOTES_ENABLED). Minimum total votes before the ratio applies — # the brigading floor (default 5): # COMMUNITY_MIN_VOTES = "5" # Percent of downvotes/total that triggers collapse (0 = off, range 0-100): # COMMUNITY_COLLAPSE_RATIO = "0" # # Privacy retention — clear comments.ip_hash + user_agent and # reports.reporter_ip_hash once a row is N days old (swept by the cron above). # 0 = off (default): hashes are kept for the life of the row. IRREVERSIBLE, and # the sweep refuses to run below 7 days. Anonymous ghost users.provider_id is # never swept — that column IS the identity. See docs/ip-hashing.md. # IP_HASH_RETENTION_DAYS = "0" # # Delete audit_log rows once they are N days old (swept by the same cron). # 0 = off (default): moderation history is kept indefinitely. IRREVERSIBLE, and # the sweep refuses to run below 30 days — a higher floor than the IP one # because a moderation record stays useful for months. Whole rows go rather than # being redacted. See docs/compliance/data-inventory.md. # AUDIT_LOG_RETENTION_DAYS = "0" # Optional: Cloudflare account ID — paired with the CF_API_TOKEN secret to # enable the /admin/usage analytics page. # CF_ACCOUNT_ID = "0123abcd..." # Telegram operator bot (optional; the feature is OFF when TELEGRAM_BOT_TOKEN # is unset). See docs/telegram.md for BotFather setup + the setWebhook call. # The bot token and webhook secret are SECRETS (wrangler secret put, below); # only the @username is a plain var. When set, the /admin/telegram page renders # a one-tap t.me/?start= deep link instead of manual /start steps. # TELEGRAM_BOT_USERNAME = "YourGarrulBot" # Anti-spam (all optional; OFF by default). See docs/ANTISPAM.md. # Pluggable content classifier — "akismet" or "workers-ai". Leave unset for none. # SPAM_PROVIDER = "akismet" # SPAM_PROVIDER = "workers-ai" # # Lightweight in-core heuristics. Unset = off. Tripped signals flag the # comment to status='pending' (admin queue) — never a silent drop. # SPAM_LINK_THRESHOLD = "3" # flag if > N URLs in body # SPAM_HONEYPOT_MIN_MS = "1500" # flag if form submitted faster than N ms # SPAM_FIRST_COMMENT_MODERATE = "true" # new author → pending until one approved # # Muted words, one term per line. This is only the default a fresh deploy # starts with — the list is normally maintained on /admin/settings, and a # saved setting there overrides whatever is set here. Terms match whole # words (`ass` does not flag "class"); wrap in `*` to match anywhere, # trail one for a prefix. Not a regex: `.` and `(` are literal. # SPAM_BLOCKLIST = """ # casino # *viagra* # t.me/* # """ # # For SPAM_PROVIDER = "workers-ai", also add this binding: # [ai] # binding = "AI" # Optional: atomic, cross-colo-accurate rate limiting via a Durable Object. # # Without this the limiter runs on the edge Cache API, which cannot do a # compare-and-swap and keeps counters per datacenter. See docs/ANTISPAM.md # § "Rate-limit accuracy" for exactly what that costs and what this fixes — # including what it does NOT fix. # # CAREFUL WITH THE MIGRATION TAG. [[migrations]] is an ordered sequence for # the whole Worker, not per-class. `tag = "v1"` is correct only if you have no # other Durable Object; if you do, use the next unused tag and keep the # existing blocks in place. Removing the binding later is a clean no-op — the # shard holds no persistent state — so leave the class exported rather than # writing a deleted_classes migration. # # [[durable_objects.bindings]] # name = "RATE_LIMIT_DO" # class_name = "RateLimitShard" # # [[migrations]] # tag = "v1" # new_sqlite_classes = ["RateLimitShard"] # NOT new_classes — the Workers free # # plan only offers SQLite-backed # # Durable Object classes. # Notification digest job runs every 15 minutes; tune to taste. # Comments newer than 5 minutes are debounced so a reply burst coalesces # into a single email per subscriber. [triggers] crons = ["*/15 * * * *"] # BEGIN:secrets-pointer # Generated by `npm run config:build` — do not edit by hand. # # Secrets are NOT set in this file. Two ways to set them: # # 1. In bulk (recommended): # cp secrets.example.env secrets.env # then edit # npx wrangler secret bulk secrets.env && rm secrets.env # 2. One at a time: `npx wrangler secret put ` # (`./scripts/setup.sh` prompts for these on a first install.) # # For local dev, use .dev.vars (gitignored) — see .dev.vars.example. # # Always required: JWT_SECRET, IP_HASH_SECRET, TURNSTILE_SITE_KEY, TURNSTILE_SECRET # Generated for you by setup.sh: JWT_SECRET, IP_HASH_SECRET # Optional (per feature): # GH_CLIENT_ID — from github.com/settings/developers # GH_CLIENT_SECRET — from github.com/settings/developers # GOOGLE_CLIENT_ID — from console.cloud.google.com → OAuth credentials # GOOGLE_CLIENT_SECRET — from console.cloud.google.com → OAuth credentials # FACEBOOK_CLIENT_ID — from developers.facebook.com → Facebook Login # FACEBOOK_CLIENT_SECRET — from developers.facebook.com → Facebook Login # TWITTER_CLIENT_ID — from developer.x.com → OAuth 2.0 (returns no email) # TWITTER_CLIENT_SECRET — from developer.x.com → OAuth 2.0 # DISCORD_CLIENT_ID — from discord.com/developers → OAuth2 # DISCORD_CLIENT_SECRET — from discord.com/developers → OAuth2 # RESEND_API_KEY — from resend.com/api-keys # WEBHOOK_URL — legacy single-URL webhook; prefer /admin/webhooks endpoints # TELEGRAM_BOT_TOKEN — BotFather token; see docs/telegram.md # TELEGRAM_WEBHOOK_SECRET — shared secret for setWebhook; required for inbound commands # AKISMET_API_KEY — required when SPAM_PROVIDER = "akismet" # AKISMET_SITE_URL — public site URL registered with Akismet # SPAM_FORM_TS_SECRET — HMAC key for signed form-timestamp tokens # CF_API_TOKEN — Analytics-read token; scopes in AGENTS-OPERATE §5 # GITHUB_TOKEN — no-permission token; raises the 60 req/hr cap # END:secrets-pointer # BEGIN:secrets-required # Generated by `npm run config:build` — do not edit by hand. # # Commented out on purpose — uncomment ONLY in a deploy-only config. # # What it buys you: a deploy that would leave one of these unset fails # instead of shipping a Worker that 500s on its first request. # # What it costs you: declaring [secrets] makes `wrangler dev` bind ONLY # the names listed below (plus [vars]) out of .dev.vars — every other # secret in that file is dropped silently, with no warning. If you run # `wrangler dev` against this file, leave these two lines commented or # local OAuth, Resend, Akismet and Telegram will read as unconfigured. # # Adding the optional per-provider credentials to the list does not help: # wrangler then fails the deploy on every install that deliberately # doesn't set them. # [secrets] # required = ["JWT_SECRET", "IP_HASH_SECRET", "TURNSTILE_SITE_KEY", "TURNSTILE_SECRET"] # END:secrets-required # BEGIN:d1-databases # Generated by `npm run config:build` from the Bindings type in src/index.ts. Do not edit by hand. # Ids are filled in by ./scripts/setup.sh, matched on binding name. [[d1_databases]] binding = "DB" database_name = "garrul-db" database_id = "PASTE_FROM_WRANGLER_D1_CREATE" # END:d1-databases # BEGIN:kv-namespaces # Generated by `npm run config:build` from the Bindings type in src/index.ts. Do not edit by hand. # Ids are filled in by ./scripts/setup.sh, matched on binding name. [[kv_namespaces]] binding = "RATE_LIMITS" id = "PASTE_FROM_WRANGLER_KV_CREATE" [[kv_namespaces]] binding = "OAUTH_STATE" id = "PASTE_FROM_WRANGLER_KV_CREATE" [[kv_namespaces]] binding = "SESSIONS" id = "PASTE_FROM_WRANGLER_KV_CREATE" [[kv_namespaces]] binding = "TREE_CACHE" id = "PASTE_FROM_WRANGLER_KV_CREATE" # END:kv-namespaces # Workers Analytics Engine — write-side metrics (comment-posted, rate-limit-hit, etc.). # Free tier covers small operators easily. # BEGIN:analytics-datasets # Generated by `npm run config:build` from the Bindings type in src/index.ts. Do not edit by hand. [[analytics_engine_datasets]] binding = "ANALYTICS" dataset = "garrul_events" # END:analytics-datasets