From 51bf050470545ccd381162e97f935792585bc4ef Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Mon, 19 Oct 2026 14:32:29 -0400 Subject: [PATCH 1/3] Request the moderation permissions the bot checks for in the install link The homepage install link asked only for View Channels, Send Messages and Read Message History (68608). The bot refuses delete, timeout, kick and ban unless it holds Manage Messages, Moderate Members, Kick Members or Ban Members, and warnings in threads need Send Messages in Threads. New servers therefore installed TypeCheck without the permissions its configured actions require. Request 1374389611526 instead; Discord still lets the installer untick any permission, and Administrator is not requested. Co-Authored-By: Claude Opus 5.5 --- SHA256SUMS | 4 ++-- activity/src/main.ts | 2 +- docs/SETUP.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SHA256SUMS b/SHA256SUMS index 8ecbb70..3b4ee2f 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -21,7 +21,7 @@ ecd8294e24a1cd55cc1aa0b3c5d6cde68c956524cfb1d7707766b4f6287b5e3e activity/index 6c8b74d4d57bff38a393ab038418c8f410627dc66721bc416905289110f151ac activity/legal/privacy.html 297c926eb462285e52e196af2d23f2ac70cb280324bfd30aeff74836e02f159c activity/legal/terms.html 92481a623badff677f19c6eb71a34038931ce059a1b5f6c4e652cb745e590b35 activity/src/connection.ts -3d47f4f00cb34228a30376b4e8ea68ce174f22800d5f2d1728817d4437cd6f32 activity/src/main.ts +8e1c4a409e52f6677d2fb51c3afabae5de8de05be606b83a44271c6d31f35992 activity/src/main.ts 8010da4164c86394863cb645eaf4567f066822114c40edf1d2762c1a5da3806a activity/src/onboarding.ts 66988416b7ab216ad9d3947ed528bfe9f2e45f74943971d506a686ff0b044508 activity/style.css 3fb468ea85cd082f252d85b34bb3da8b62a10ae4aa984b48ef93f0ecacc7017f data/scams/README.md @@ -84,7 +84,7 @@ e23f9dd58ab8dfeda5db3ed1395fb33aad2ef91918b316c91c13545b56ea8927 docs/PACK_VALI 8fbd438d3abf7737814680b335c7250c35b8504eea13abbced98d1240dfb6e32 docs/PERMISSIONS.md 29cba5147c78a1c67d3619b9665dfabb57aaf450651d00ebfbe306aac5fb90bb docs/RESEARCH.md 548ec927e1d97352156b824b228b722e32f7e3554e1894d6d402c28c81987975 docs/SECURITY.md -31eb57212ea71f93d717356df2ebe084e7db371fec5036a4e8761586dc63ec09 docs/SETUP.md +a41b3b3295d8f742c58aa931da366847e1942f21789e0ff45568cae0cb918f6c docs/SETUP.md 8e68110bbe703f2e44fe68d382a7f3d46ea0e5215406ee4d1ae72e7815a3a885 docs/TEXT_MODERATION.md f618ca2b276495eecdf4e556dd622792c36f44f5e1d694873b863bfd92d69a99 docs/TEXT_STARTERS.md 821210b0d31fce09ccfb03dedf521df28465327d021991dd76a92954b8109a18 docs/USER_SPECIFICATION.md diff --git a/activity/src/main.ts b/activity/src/main.ts index a218d5c..97950f4 100644 --- a/activity/src/main.ts +++ b/activity/src/main.ts @@ -719,7 +719,7 @@ async function start(autoConnect = true) { const inDiscord = new URLSearchParams(location.search).has('frame_id'); const showInstall = !preview && !inDiscord; const installUrl = new URL('https://discord.com/oauth2/authorize'); - installUrl.search = new URLSearchParams({ client_id: config.clientId, scope: 'bot applications.commands', permissions: '68608', integration_type: '0' }).toString(); + installUrl.search = new URLSearchParams({ client_id: config.clientId, scope: 'bot applications.commands', permissions: '1374389611526', integration_type: '0' }).toString(); app.append(h('main', { class: 'login' }, h('div', { class: 'login-brand' }, h('span', { class: 'brand-mark' }, 'J'), h('p', { class: 'eyebrow' }, 'JEV / TYPECHECK')), badge(preview ? 'OFFLINE PREVIEW' : 'EXPERIMENTAL MODERATION', 'amber'), h('h1', {}, 'Text + image moderation.\nUnder your control.'), h('p', { class: 'login-copy' }, 'Manage text policies, reference images, permissions, and credentials inside Discord. The backend bot handles continuous monitoring.'), h('div', { class: 'actions login-actions' }, showInstall ? h('a', { class: 'button primary', href: installUrl.href, target: '_blank', rel: 'noopener noreferrer', 'aria-describedby': 'install-help' }, 'Add to your server') : null, diff --git a/docs/SETUP.md b/docs/SETUP.md index 0a983b9..9d0ef13 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -24,9 +24,9 @@ The server serves `/terms` and `/privacy` from `activity/legal/`. Set the Develo The member who installs the app can immediately connect using their existing **Manage Server** permission; Discord owners and Administrators also have automatic management access. No extra TypeCheck role or role-creation permission is required. Other members still need a role grant under Permissions. Management authority is rechecked from Discord on every request. -The public homepage offers **Add to your server**, opening Discord's server installation flow in a new tab. It uses the configured application ID, `integration_type=0`, scopes `bot applications.commands`, and permission value `68608` (View Channels, Send Messages, Read Message History). It does not request Administrator or sanction permissions. The link is hidden in the offline preview and embedded Activity; the existing Activity connection flow remains available. +The public homepage offers **Add to your server**, opening Discord's server installation flow in a new tab. It uses the configured application ID, `integration_type=0`, scopes `bot applications.commands`, and permission value `1374389611526`: View Channels, Send Messages, Send Messages in Threads and Read Message History for monitoring, warnings and logs, plus Manage Messages, Moderate Members, Kick Members and Ban Members for the delete, timeout, kick and ban actions the bot checks before acting. It does not request Administrator. Discord lets the installing member untick any permission they do not want; actions whose permission is missing are refused before they run (`bot_missing_manage_messages`, `bot_missing_moderation_permission`). The link is hidden in the offline preview and embedded Activity; the existing Activity connection flow remains available. -Before public installation, enable **Public Bot** in the Developer Portal's Bot tab and keep **Requires OAuth2 Code Grant** disabled for this install flow. Under Installation, enable Guild Install and configure the Discord Provided Link with the same scopes and baseline permissions. Public Bot cannot be changed through the documented Edit Current Application bot API. The homepage supplies explicit scopes and permissions, so it does not depend on default install parameters. +Before public installation, enable **Public Bot** in the Developer Portal's Bot tab and keep **Requires OAuth2 Code Grant** disabled for this install flow. Under Installation, enable Guild Install and configure the install link with the same scopes and permission value. Public Bot cannot be changed through the documented Edit Current Application bot API. The homepage supplies explicit scopes and permissions, so it does not depend on default install parameters. A member with Manage Server can install the bot, but TypeCheck configuration requires the server owner or an administrator (or explicitly delegated capabilities). After installation, launch the Activity, choose monitored channels, add the server's Jev API key, and configure rules. Grant additional bot permissions below only for selected moderation features. Installation does not enable automatic enforcement. Verify installation and Activity launch with a non-owner application user in a separate test server before treating public onboarding as accepted. From 0603501fdc29bc6a68bf6c06b6908293f8cc8049 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Mon, 19 Oct 2026 14:50:37 -0400 Subject: [PATCH 2/3] Deploy production automatically when Validate succeeds on main 5e66945 limited Deploy production to manual dispatch, so merged changes stopped reaching GoDaddy unless someone dispatched them. Restore the workflow_run trigger: a successful Validate run for a push to main in this repository deploys that exact commit. Pull request runs, other branches and failed or cancelled runs still never deploy, and the existing guard still ends out-of-order runs as superseded. Manual dispatch remains for redeploys and rollbacks. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/deploy.yml | 15 ++++++++++++--- SHA256SUMS | 6 +++--- docs/DEPLOYMENT.md | 4 ++-- docs/OPERATIONS.md | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 56cb5c7..59bb23a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,12 +1,16 @@ name: Deploy production -# Explicitly dispatch only after separate production authorization. Validate on main is still required. Pull request +# Deploys a commit whose Validate run on a push to main succeeded, or an explicitly dispatched main commit. Pull request # runs, other branches and failed or cancelled runs never reach the deploy job. -# Publication requires explicit workflow_dispatch. No pull request code is +# workflow_run is used only to observe Validate's verdict for main. No pull request code is # checked out or executed here; the candidate is a main commit that already passed CI. # Database credentials exist only inside the GoDaddy runtime, so CI never touches the # database. A release admitted with an expand-only migration backs the database up to its # persistent volume and then migrates, once, at startup under the migration lock. on: # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: [Validate] + types: [completed] + branches: [main] workflow_dispatch: inputs: commit: @@ -28,7 +32,12 @@ concurrency: cancel-in-progress: false jobs: deploy: - if: github.event_name == 'workflow_dispatch' + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.head_repository.full_name == github.repository) runs-on: ubuntu-24.04 # Worst case: ship <=20, verify <=19 (incl. PREVIEW restart retries), rollback ship+verify <=39 minutes. timeout-minutes: 80 diff --git a/SHA256SUMS b/SHA256SUMS index 3b4ee2f..2b7feca 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,7 +1,7 @@ b67a5f8294f917779e210dc5104c4b08851876dbcfd91419d691e27e03e1f7e2 .dockerignore a8ee0dfbeba5f217873d77b9cd189879749da3028d864955c6e11ada8f9bda15 .env.example 70f3d6cd37568f51207435dbf859cf61542ab30860f26e3dc857e99648e22ca3 .github/workflows/ci.yml -8e4fce08f34b76ba6e44ff1611e812d92adc13fa1c2b943a62b3db5453c19a86 .github/workflows/deploy.yml +4b0d23a649c9ec6bf890d72b757f89fa584e59c1346c0406182c5497a6f90774 .github/workflows/deploy.yml 59b480ee3911da82044810c7d67df3416ee7968f2a399b5dfbae74f8f3a75967 .github/workflows/inspect-hosting.yml 51374f3516824cb7a88648dddda77fef8fa383d5e0bb118a6b4fa92444005df6 .gitignore 3a951f66e7e0f3e2dd1832473a871357c4a507b51100636b73bcf6ff0dd78b6c .npmrc @@ -75,11 +75,11 @@ e77b36946c2cd3e87400a8ef30821a9e620775034537e5a61b1d00fa408d0f78 docs/API.md 24861f458f5bba54f223f5903aefe9300793c154cd25958b7ac4f46072a8647a docs/CONTENT_ANALYSIS.md 9c81d1916b565043cc438787fc43b2e5719ff7aac78846f5d32ec7035fdb9d7d docs/COSTS.md 9ade67158bf9c08e1bde20d12401658a23c08df186871f869f32fa476dbd800f docs/DEPENDENCIES.md -9aae43e617e681d49076da868ff96ad6eb915490f67c498ca5a3466a3ae51083 docs/DEPLOYMENT.md +48ccecccc69b49b8ac75e32f8db169e864aa411fd73c89b5e44ab236ce0a5b0e docs/DEPLOYMENT.md e15373ed5926a2afe6e8069f0c832178ec2ffdcdd4d288525d0146c585271866 docs/HISTORICAL_SCANS.md b2f2313e1b6cba9454ad457cb6f02850cba1396174a1edc568db6022714dddde docs/MATCHING.md 41cb490223ca17c8953febc5524bc3c48e5338786c0317a483e93687e13a8543 docs/MODERATION_PACKS.md -af79129af04ddd9d5895dba21327f736e00fedca755f7f722395c62803bbb693 docs/OPERATIONS.md +4bee7aa5e6d7ad8035b726ba0647d08918ee5ac1c0424172b32ff9c478cf33ad docs/OPERATIONS.md e23f9dd58ab8dfeda5db3ed1395fb33aad2ef91918b316c91c13545b56ea8927 docs/PACK_VALIDATION.md 8fbd438d3abf7737814680b335c7250c35b8504eea13abbced98d1240dfb6e32 docs/PERMISSIONS.md 29cba5147c78a1c67d3619b9665dfabb57aaf450651d00ebfbe306aac5fb90bb docs/RESEARCH.md diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index ee68649..e69cdf9 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -2,7 +2,7 @@ Production runs on GoDaddy Node.js Hosting. A release reaches it only through `.github/workflows/deploy.yml`: -PR → `Validate` and review → merge to `main` → `Validate` succeeds on the push to `main` → separate explicit production authorization and workflow dispatch → `Deploy production` deploys that exact commit → post-deployment verification. +PR → `Validate` and review → merge to `main` → `Validate` succeeds on the push to `main` → `Deploy production` (started automatically by that successful run, or by manual dispatch) deploys that exact commit → post-deployment verification. ## Architecture (release source updated 2026-10-17) @@ -19,7 +19,7 @@ PR → `Validate` and review → merge to `main` → `Validate` succeeds on the ### Why deployment cannot bypass CI -- Production publication runs only by explicit authorized `workflow_dispatch`. Merging a PR or completing CI no longer automatically deploys production. Successful Validate on the exact main commit remains mandatory. +- Production publication runs automatically when `Validate` succeeds on a push to `main`: merging a PR deploys it once CI passes. Pull request runs, other branches and failed or cancelled runs never deploy. Manual `workflow_dispatch` remains available for redeploys and rollbacks. - Manual dispatch must name a full SHA that is on `main` and has a successful `Validate` push run. The workflow checks both. - Nothing else can deploy. GoDaddy has no push webhook, and the GoDaddy PAT exists only in the `production` environment, which is restricted to `main`. - Review remains a process requirement: the repository is public, but main currently has no branch protection or ruleset. Direct pushes still require successful main CI before this deployment workflow runs. The deployment job has `contents: write` only to create release artifact branches; its code never writes main. It uses the short-lived workflow token, not a new credential. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 5596f65..f8485aa 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -74,7 +74,7 @@ coverage exclusions, migration 003, privacy and validation instructions. ## Native text moderation (October 17, 2026) -Inspect native text status/blocked reasons and request correlation without logging message content. text_empty_or_unavailable requires intent/4014 and channel-access checks, never a safe fallback. Pause enforcement when diagnosing uncertainty; preserve action slots/job identity. Recovery refetches IDs/digests; stale edits requeue current content; deleted sources cannot punish. Historical scans remain image-only. Starter deletion is permanent. Before older-code downgrade, follow [JSON compatibility procedure](TEXT_MODERATION.md). Production publication requires separately authorized manual workflow dispatch after main CI. +Inspect native text status/blocked reasons and request correlation without logging message content. text_empty_or_unavailable requires intent/4014 and channel-access checks, never a safe fallback. Pause enforcement when diagnosing uncertainty; preserve action slots/job identity. Recovery refetches IDs/digests; stale edits requeue current content; deleted sources cannot punish. Historical scans remain image-only. Starter deletion is permanent. Before older-code downgrade, follow [JSON compatibility procedure](TEXT_MODERATION.md). Production publication runs automatically once Validate succeeds on main; see [deployment](DEPLOYMENT.md). ## Pack persistence and recovery From 0fd042b01fa237218b4017fa52dc693e8e116534 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Mon, 19 Oct 2026 14:55:31 -0400 Subject: [PATCH 3/3] Wait for a new production leader to become ready before failing verification Deploy run 39061306019 failed although the release was serving: right after the PREVIEW restart handed control to PUBLISH, /api/ready returned 503 not_ready because the new leader's Gateway had not connected yet, and the verifier read readiness once. Poll readiness for up to 90 seconds (within the overall deadline) before recording the result. Co-Authored-By: Claude Opus 5.5 --- SHA256SUMS | 2 +- scripts/verify-production.mjs | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/SHA256SUMS b/SHA256SUMS index 2b7feca..83f0e45 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -390,7 +390,7 @@ ced5890442d0278819e6afe2db64bae8282a49df4c3e158b6552be4a915435c1 scripts/test-p 5a4244a117b328bb01b54768151e2c881ff18cf90c0d365e645d89d613a0dd78 scripts/test-text-persistence.mjs d5dd84f2a02c9199b8a971d31f66abf605e1c755f63456802e6207987bfd5a54 scripts/test-threshold-persistence.mjs 6281e428e99049a2cf6a04c9abac7e14f25b4162b8d077cd104f41833332b037 scripts/validate-text-dataset.mjs -eb55d9520146a83841b6d5da870c7556dc8f11dd392128ec2e5a40874c7b510e scripts/verify-production.mjs +029b29c40c468211013982847d99051f171c93fa6a77a6884d6463b872c5cba9 scripts/verify-production.mjs a44e797125685b375a99f8ea8311972906255b05cdc4df91ea9beb0c8860e185 scripts/verify-provider-source.mjs 466a783814d37d2fc5a729c47705fc229fe397ab398a440b66fecc49602b3a69 server/backup.ts 15dec3c372b73a5932161f5adabacd86af6ed0d73f942b78482b7145559804ed server/config.ts diff --git a/scripts/verify-production.mjs b/scripts/verify-production.mjs index b659894..e86a2a8 100644 --- a/scripts/verify-production.mjs +++ b/scripts/verify-production.mjs @@ -33,6 +33,16 @@ async function waitForRelease() { throw new Error(`release not serving before timeout: ${last}`); } +// A newly elected leader reports 503 until its Gateway connects; poll briefly instead of failing on the first read. +async function waitForReady() { + const until = Math.min(deadline, Date.now() + 90000); + for (;;) { + const ready = await get(`${origin}/api/ready`); + if ((ready.status === 200 && json(ready.body)?.status === 'ready') || Date.now() >= until) return ready; + await new Promise(r => setTimeout(r, 5000)); + } +} + let health; try { health = await waitForRelease(); } catch (e) { check('release serving', false, e.message); } @@ -42,7 +52,7 @@ if (health) try { check('controller state recognized', ['leader', 'standby'].includes(health.status), health.status); check('object storage persisted', !storage || health.storage === storage, `${health.storage} (expected ${storage ?? 'new marker'})`); check('object storage writable', !expected || /^[0-9a-f]{32}$/.test(health.storage ?? ''), health.storage); - const ready = await get(`${origin}/api/ready`), r = json(ready.body); + const ready = await waitForReady(), r = json(ready.body); check('readiness (database + controller)', ready.status === 200 && r?.status === 'ready', `${ready.status} ${ready.body.slice(0, 100)}`); if (health.status === 'leader') check('leader remains ready', r?.controller === 'leader', r?.controller); check('api responses not cached', /no-store/.test(ready.headers.get('cache-control') ?? '') && ready.headers.get('cf-cache-status') !== 'HIT', ready.headers.get('cache-control'));