From d343c27d6e581b973b9d99b73e5e436bc88c41fe Mon Sep 17 00:00:00 2001 From: feruzm Date: Tue, 1 Sep 2026 18:57:56 +0000 Subject: [PATCH] Pass ENOTIFY_INTERNAL_TOKEN through to vapi Gap in my own change. ecency/vision-api#91 reads ENOTIFY_INTERNAL_TOKEN to unlock a user's own complete notification feed from enotify, but nothing ever set it: the variable was never added to the compose files or forwarded by the deploy workflows, so vapi would have read an empty string forever. Wired exactly like SSR_INTERNAL_SECRET, which is the most recent secret of the same shape: declared on the vapi service in both compose files, and forwarded in all three places each deploy job needs it (the env map, the ssh envs list and the export before the stack deploy), across both regions in master.yml and staging. Only the vapi service gets it. The web service does not talk to enotify. Setting it by hand on the running service would not have worked either: a stack deploy from this repo resets the full service spec of every service in the stack, so it has to live here. Requires the ENOTIFY_INTERNAL_TOKEN repository secret, matching [APP] INTERNAL_TOKEN in enotify's config.ini. enotify fails closed, so until both sides carry the same value every user quietly loses favorites, bookmarks, Points transfers and the aggregates from their own notification list. --- .github/workflows/master.yml | 8 ++++++-- .github/workflows/staging.yml | 4 +++- apps/web/docker-compose.production.yml | 5 +++++ apps/web/docker-compose.yml | 5 +++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 9ee3156f05..b7093bd8f9 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -143,12 +143,13 @@ jobs: SEO_CRON_SECRET: ${{secrets.SEO_CRON_SECRET}} TURNSTILE_SECRET: ${{secrets.TURNSTILE_SECRET}} SSR_INTERNAL_SECRET: ${{secrets.SSR_INTERNAL_SECRET}} + ENOTIFY_INTERNAL_TOKEN: ${{secrets.ENOTIFY_INTERNAL_TOKEN}} with: host: ${{ secrets.SSH_HOST_EU }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} port: ${{ secrets.SSH_PORT }} - envs: USE_PRIVATE,PRIVATE_API_ADDR,PRIVATE_API_AUTH,HIVESIGNER_SECRET,SEARCH_API_ADDR,SEARCH_API_SECRET,STRIPE_INTERNAL_SECRET,HOSTING_INTERNAL_SECRET,HIVESEARCHER_ORIGIN_IP,PLAUSIBLE_API_KEY,BLOCKSTREAM_CLIENT_ID,BLOCKSTREAM_CLIENT_SECRET,MATTERMOST_TEAM_ID,MATTERMOST_ADMIN_TOKEN,MATTERMOST_BASE_URL,MATTERMOST_WS_ALLOWED_ORIGINS,THREESPEAK_EMBED_API_KEY,SEO_CRON_SECRET,TURNSTILE_SECRET,NEWSLETTER_API_URL,NEWSLETTER_SERVICE_TOKEN,SSR_INTERNAL_SECRET + envs: USE_PRIVATE,PRIVATE_API_ADDR,PRIVATE_API_AUTH,HIVESIGNER_SECRET,SEARCH_API_ADDR,SEARCH_API_SECRET,STRIPE_INTERNAL_SECRET,HOSTING_INTERNAL_SECRET,HIVESEARCHER_ORIGIN_IP,PLAUSIBLE_API_KEY,BLOCKSTREAM_CLIENT_ID,BLOCKSTREAM_CLIENT_SECRET,MATTERMOST_TEAM_ID,MATTERMOST_ADMIN_TOKEN,MATTERMOST_BASE_URL,MATTERMOST_WS_ALLOWED_ORIGINS,THREESPEAK_EMBED_API_KEY,SEO_CRON_SECRET,TURNSTILE_SECRET,NEWSLETTER_API_URL,NEWSLETTER_SERVICE_TOKEN,SSR_INTERNAL_SECRET,ENOTIFY_INTERNAL_TOKEN script: | set -eo pipefail export USE_PRIVATE=$USE_PRIVATE @@ -173,6 +174,7 @@ jobs: export SEO_CRON_SECRET=$SEO_CRON_SECRET export TURNSTILE_SECRET=$TURNSTILE_SECRET export SSR_INTERNAL_SECRET=$SSR_INTERNAL_SECRET + export ENOTIFY_INTERNAL_TOKEN=$ENOTIFY_INTERNAL_TOKEN # The web sizing knobs default in compose, so `docker-compose config` # would otherwise read whatever the HOST happens to have exported and # this region's memory profile could change with no commit behind it. @@ -248,6 +250,7 @@ jobs: SEO_CRON_SECRET: ${{secrets.SEO_CRON_SECRET}} TURNSTILE_SECRET: ${{secrets.TURNSTILE_SECRET}} SSR_INTERNAL_SECRET: ${{secrets.SSR_INTERNAL_SECRET}} + ENOTIFY_INTERNAL_TOKEN: ${{secrets.ENOTIFY_INTERNAL_TOKEN}} # Newsletter service lives on the EU box; the US tier reaches it through the TLS relay # on the EU origin (api.blogs.ecency.com/newsletter-relay, IP-allowlisted), so this # region's URL is its own secret. Same token. @@ -281,7 +284,7 @@ jobs: username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} port: ${{ secrets.SSH_PORT }} - envs: USE_PRIVATE,PRIVATE_API_ADDR,PRIVATE_API_AUTH,HIVESIGNER_SECRET,SEARCH_API_ADDR,SEARCH_API_SECRET,STRIPE_INTERNAL_SECRET,HOSTING_INTERNAL_SECRET,HIVESEARCHER_ORIGIN_IP,PLAUSIBLE_API_KEY,BLOCKSTREAM_CLIENT_ID,BLOCKSTREAM_CLIENT_SECRET,MATTERMOST_TEAM_ID,MATTERMOST_ADMIN_TOKEN,MATTERMOST_BASE_URL,MATTERMOST_WS_ALLOWED_ORIGINS,THREESPEAK_EMBED_API_KEY,SEO_CRON_SECRET,TURNSTILE_SECRET,NEWSLETTER_API_URL,NEWSLETTER_SERVICE_TOKEN,WEB_REPLICAS,WEB_HEAP_MB,WEB_MEM_LIMIT,WEB_UPDATE_ORDER,SSR_INTERNAL_SECRET + envs: USE_PRIVATE,PRIVATE_API_ADDR,PRIVATE_API_AUTH,HIVESIGNER_SECRET,SEARCH_API_ADDR,SEARCH_API_SECRET,STRIPE_INTERNAL_SECRET,HOSTING_INTERNAL_SECRET,HIVESEARCHER_ORIGIN_IP,PLAUSIBLE_API_KEY,BLOCKSTREAM_CLIENT_ID,BLOCKSTREAM_CLIENT_SECRET,MATTERMOST_TEAM_ID,MATTERMOST_ADMIN_TOKEN,MATTERMOST_BASE_URL,MATTERMOST_WS_ALLOWED_ORIGINS,THREESPEAK_EMBED_API_KEY,SEO_CRON_SECRET,TURNSTILE_SECRET,NEWSLETTER_API_URL,NEWSLETTER_SERVICE_TOKEN,WEB_REPLICAS,WEB_HEAP_MB,WEB_MEM_LIMIT,WEB_UPDATE_ORDER,SSR_INTERNAL_SECRET,ENOTIFY_INTERNAL_TOKEN script: | set -eo pipefail export USE_PRIVATE=$USE_PRIVATE @@ -300,6 +303,7 @@ jobs: export SEO_CRON_SECRET=$SEO_CRON_SECRET export TURNSTILE_SECRET=$TURNSTILE_SECRET export SSR_INTERNAL_SECRET=$SSR_INTERNAL_SECRET + export ENOTIFY_INTERNAL_TOKEN=$ENOTIFY_INTERNAL_TOKEN # The web tier decides per request whether the newsletter service is configured, so a # missing secret would ship a silent no-show. Fail loudly instead. : "${NEWSLETTER_API_URL:?NEWSLETTER_API_URL is required for the US deploy}" diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index c8a5860127..e9e05d8bb4 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -142,6 +142,7 @@ jobs: SEO_CRON_SECRET: ${{secrets.SEO_CRON_SECRET}} TURNSTILE_SECRET: ${{secrets.TURNSTILE_SECRET}} SSR_INTERNAL_SECRET: ${{secrets.SSR_INTERNAL_SECRET}} + ENOTIFY_INTERNAL_TOKEN: ${{secrets.ENOTIFY_INTERNAL_TOKEN}} # Newsletter service (ecency/news) lives on the EU production box; staging reaches # it through the IP-allowlisted TLS relay on the EU origin, same as US, with its own # URL secret. There is one service and one database, so subscriptions made on alpha @@ -153,7 +154,7 @@ jobs: username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} port: ${{ secrets.SSH_PORT }} - envs: USE_PRIVATE,PRIVATE_API_ADDR,PRIVATE_API_AUTH,HIVESIGNER_SECRET,SEARCH_API_ADDR,SEARCH_API_SECRET,STRIPE_INTERNAL_SECRET,HOSTING_INTERNAL_SECRET,HIVESEARCHER_ORIGIN_IP,PLAUSIBLE_API_KEY,BLOCKSTREAM_CLIENT_ID,BLOCKSTREAM_CLIENT_SECRET,MATTERMOST_TEAM_ID,MATTERMOST_ADMIN_TOKEN,MATTERMOST_BASE_URL,MATTERMOST_WS_ALLOWED_ORIGINS,THREESPEAK_EMBED_API_KEY,SEO_CRON_SECRET,TURNSTILE_SECRET,NEWSLETTER_API_URL,NEWSLETTER_SERVICE_TOKEN,SSR_INTERNAL_SECRET + envs: USE_PRIVATE,PRIVATE_API_ADDR,PRIVATE_API_AUTH,HIVESIGNER_SECRET,SEARCH_API_ADDR,SEARCH_API_SECRET,STRIPE_INTERNAL_SECRET,HOSTING_INTERNAL_SECRET,HIVESEARCHER_ORIGIN_IP,PLAUSIBLE_API_KEY,BLOCKSTREAM_CLIENT_ID,BLOCKSTREAM_CLIENT_SECRET,MATTERMOST_TEAM_ID,MATTERMOST_ADMIN_TOKEN,MATTERMOST_BASE_URL,MATTERMOST_WS_ALLOWED_ORIGINS,THREESPEAK_EMBED_API_KEY,SEO_CRON_SECRET,TURNSTILE_SECRET,NEWSLETTER_API_URL,NEWSLETTER_SERVICE_TOKEN,SSR_INTERNAL_SECRET,ENOTIFY_INTERNAL_TOKEN script: | export USE_PRIVATE=$USE_PRIVATE export PRIVATE_API_ADDR=$PRIVATE_API_ADDR @@ -171,6 +172,7 @@ jobs: export SEO_CRON_SECRET=$SEO_CRON_SECRET export TURNSTILE_SECRET=$TURNSTILE_SECRET export SSR_INTERNAL_SECRET=$SSR_INTERNAL_SECRET + export ENOTIFY_INTERNAL_TOKEN=$ENOTIFY_INTERNAL_TOKEN # Unlike production this does not fail without the newsletter settings: staging may run # without the feature. It says so, because the web hides the controls silently otherwise. if [ -z "$NEWSLETTER_API_URL" ] || [ -z "$NEWSLETTER_SERVICE_TOKEN" ]; then diff --git a/apps/web/docker-compose.production.yml b/apps/web/docker-compose.production.yml index 4148f66734..af5043f782 100644 --- a/apps/web/docker-compose.production.yml +++ b/apps/web/docker-compose.production.yml @@ -20,6 +20,11 @@ services: # Shared secret for the internal SSR RPC cache (/private-api/ssr/*); the # web service sends it on every proxied read. Unset = those routes are off. - SSR_INTERNAL_SECRET + # Shared secret vapi presents to enotify to unlock a user's OWN complete + # notification feed. enotify defaults to chain-derived activity only and + # fails closed, so unset means every user silently loses favorites, + # bookmarks, Points transfers and the aggregates from their own list. + - ENOTIFY_INTERNAL_TOKEN # Upstream fills the SSR RPC cache may run at once, and how many may wait # for a slot. An origin with ~90 misses/s against a 200-900ms node path # needs 45-85 fills in flight; the defaults (64/256) queued fills past the diff --git a/apps/web/docker-compose.yml b/apps/web/docker-compose.yml index 956b44238c..e82e27bd00 100644 --- a/apps/web/docker-compose.yml +++ b/apps/web/docker-compose.yml @@ -20,6 +20,11 @@ services: # Shared secret for the internal SSR RPC cache (/private-api/ssr/*); the # web service sends it on every proxied read. Unset = those routes are off. - SSR_INTERNAL_SECRET + # Shared secret vapi presents to enotify to unlock a user's OWN complete + # notification feed. enotify defaults to chain-derived activity only and + # fails closed, so unset means every user silently loses favorites, + # bookmarks, Points transfers and the aggregates from their own list. + - ENOTIFY_INTERNAL_TOKEN # Upstream fills the SSR RPC cache may run at once, and how many may wait # for a slot. An origin with ~90 misses/s against a 200-900ms node path # needs 45-85 fills in flight; the defaults (64/256) queued fills past the