Skip to content

Email the owner when Etsy approves a pending app - #1440

Merged
helpcodeai merged 3 commits into
mainfrom
feat/etsy-approval-watcher
Nov 2, 2026
Merged

helpcodeai merged 3 commits into
mainfrom
feat/etsy-approval-watcher

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Summary

In the last 30 days, 449 Cloud workspaces got the Etsy setup error "Etsy does not accept these app keys yet". A new Etsy app stays Pending until Etsy reviews it. Only 34% of those workspaces later made a successful Etsy call. Etsy's approval email arrives days later, and nothing brought people back to finish the sign-in.

With this PR, the keys are kept and checked again. When Etsy accepts them, the connector's owner gets one email with a button to finish the sign-in and a first request to try in Claude.

Changes

  • Setup check (adapters.service.ts): for a watched adapter (APP_APPROVAL_ADAPTERS, only etsy for now), a 403 from checkBeforeAuthorization is flagged appPending: true. The check is split into pingAppKeys. The new public recheckAppKeys(slug, baseUrl, envVars) makes the same /openapi-ping call with x-api-key: keystring:shared_secret and returns accepted, refused or unknown.
  • Setup page (connectors/setup/[slug]): on appPending, the page saves the keys on a draft connector, using the existing save(). Drafts stay hidden from MCP until signed in. The page then shows an info box instead of the red error: "Etsy reviews new apps, usually within a few days. We'll e-mail you when it's approved; nothing else to do. Your keys are saved." It also shows Etsy's answer and a link to the connector. A retry stores the new values on that connector instead of installing a second one. If the save fails (for example, the plan limit), the page shows the error as before. Messages are in en, de and it.
  • AppApprovalWatcherService (new, src/adapters/): a sweep every 15 minutes (setInterval, the same pattern as ConnectorAuthStatusService).
    • Selection: catalog connectors with config.adapterSlug = 'etsy', OAuth2, created in the last 14 days, still needs_authorization with both keys stored. Per-user connectors are skipped.
    • Pending since: the earliest of these, from the same workspace, at most a day before the connector was created, and naming this connector or none:
      • an etsy_app_pending_seen event from the setup check
      • a setup_verify_failed event for Etsy with status 403, which covers setups made before this change
      • the watcher's own first refused check
    • Backoff: hourly during the first day after the first refusal, then every 6 hours. It stops 14 days after the connector was created or first refused. It also stops once the connector is signed in (doneAt), or once the keys are accepted without ever having been refused (notPending, no email).
    • State: kept in connectors.config.appApprovalWatch (pendingSince, lastCheckedAt, approvedAt, notifiedAt…). It is merged with a single jsonb update, so other config keys and concurrent edits are left alone. No schema change.
    • Idempotency: the email is claimed with a conditional write (… WHERE config->'appApprovalWatch'->>'notifiedAt' IS NULL), so only one replica wins. Before sending, the service also checks for an existing etsy_app_approved_notified event for the connector. At most one email per connector, across restarts and replicas. A failed send is not retried.
  • Email (appApprovedEmail, EmailService.sendAppApprovedEmail): transactional and not marketing. It is sent even with email_marketing_opt_out, because the user asked to connect Etsy, and at most once per connector.
    • Recipient: the connector's owner (connectors.user_id).
    • Subject: "Etsy approved your app".
    • One button, "Finish the Etsy sign-in", to /connectors/<id>. On Cloud the link goes through the user's social sign-in, like the other emails.
    • Then the line "Then ask Claude: “Show my last 10 Etsy orders”", linking to https://claude.ai/new?q=Show%20my%20last%2010%20Etsy%20orders.
    • Sent through the same transport as the connector sign-in email: the workspace SMTP if one is set, else the system SMTP (SMTP_FROM, i.e. AnythingMCP <[email protected]> on Cloud). No new SMTP config.
  • Product events (server-only):
    • etsy_app_pending_seen: via is setup (with connectorId when finishing an existing connector) or watcher.
    • etsy_app_approved_notified: carries connectorId, adapterSlug, and kind set to emailed or not_sent.

Cloud vs self-hosted

  • Cloud: as described above. Emails go out through the system SMTP (AnythingMCP <[email protected]>), or through the workspace's own SMTP when it has one, the same as the connector sign-in email.
  • Self-hosted: the same watcher and the same setup-page behaviour. With SMTP configured (instance or workspace), the same email goes out, linking to FRONTEND_URL. Without SMTP, nothing is sent: the outcome is logged (… accepted the app keys; no email sent) and recorded as etsy_app_approved_notified with kind: not_sent, and there are no retries.

Wording for the owner (not changed here)

The Etsy 403 hint lives in adapters/intl/etsy.json (verifyHints.403), which another branch is editing, so this PR does not touch it. The new pending wording is on the setup page instead. Suggested hint text if you want them aligned: "Etsy reviews new apps, usually within a few days. We'll e-mail you when it's approved; nothing else to do. If your app is already active, copy the Shared secret again from your app page: it must belong to this Keystring."

Type

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance improvement
  • Other (describe below)

Testing

  • Added/updated unit tests
  • Tested manually (describe steps below)
  • No tests needed (explain why)

New specs:

  • app-approval-watch.spec.ts: backoff and stop rules, matching of pending evidence, the claude.ai link.
  • app-approval-watcher.service.spec.ts: the service runs against an in-memory connectors table whose $executeRaw mock applies the same merge and claim as the SQL. It covers:
    • selection
    • skipping connectors that are signed in, have no keys, use per-user credentials, or are too old
    • the first-refusal record
    • one email across repeated sweeps, a restart, and two concurrent sweeps
    • the event-based guard
    • the no-SMTP path
    • provider-down
    • the email content
  • adapters.service.spec.ts: appPending only on a 403 of a watched adapter; recheckAppKeys returns accepted, refused or unknown.
  • adapters.controller.spec.ts: etsy_app_pending_seen is logged with connectorId.
  • The new email sample (app-approved-etsy) runs through the existing email-templates.spec.ts checks: hostile names, allowed hosts, no placeholders, and no price talk.
  • Playwright (connector-guided-setup.spec.ts): a Pending Etsy app keeps the keys (one import), shows the message and the connector link, does not send the user to Etsy, and a retry updates the kept connector instead of importing again.

Commands and results (run in this branch, after npm ci at the root and npx prisma generate in packages/backend):

Directory Command Result
packages/backend npx jest --maxWorkers=2 src/adapters/app-approval-watch src/adapters/adapters.service.spec.ts src/adapters/adapters.controller.spec.ts src/settings/email src/audit/product-event 7 suites, 1031 tests passed
packages/backend npx tsc --noEmit -p tsconfig.json exit 0
packages/backend npx eslint <the 13 changed backend files> (no --fix) exit 0
packages/frontend npx tsc --noEmit -p tsconfig.json exit 0
packages/frontend npx eslint src/app/connectors/setup src/lib/api.ts tests/e2e/connector-guided-setup.spec.ts exit 0
packages/frontend npx playwright test tests/e2e/connector-guided-setup.spec.ts --workers=1 11 passed
root node scripts/validate-adapters.mjs exit 0 (no adapter changed)
root node --test scripts/validate-adapters.test.mjs scripts/adapter-new.test.mjs scripts/generate-llms-txt.test.mjs 58 passed
root node scripts/adapter-count.mjs --check OK, 691 adapters
root node scripts/generate-llms-txt.mjs --check exit 0

The full backend suite was not run locally; CI runs it. During Playwright, next dev printed a react/react-dom version-mismatch warning for _document. It is unrelated to this change and did not affect the tests.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable): the user-facing text is the setup page message and the email. docs/tool-definition.md does not document checkBeforeAuthorization at all (existing gap, not changed here).
  • All existing tests pass (npm test): targeted suites only, see above
  • This PR has a descriptive title

Open points

  • Etsy answers 403 both to a Pending app and to a shared secret that does not belong to the keystring. A connector with a wrong secret is therefore watched for 14 days (about 60 cheap pings) and never emailed. The page keeps showing Etsy's hint, which says to copy the secret again if the app is already active.
  • The page now saves a draft connector on a Pending answer. That counts against the plan's connector limit, as "Save anyway" already did. When the limit blocks the save, nothing is watched and the page shows the error as before.
  • Users who hit the error before this deploy without saving their keys cannot be helped: their keys were never stored. Connectors saved with "Save anyway" in the last 14 days are picked up, because the old setup_verify_failed 403 events count as evidence.

Related Issues

Follows #881 and #936 (Etsy key check before the sign-in).

In 30 days 449 Cloud workspaces were told Etsy does not accept their new
app's keys yet (the app stays Pending until Etsy reviews it), and only a
third ever made an Etsy call: Etsy's approval mail comes days later and
nothing brought them back.

- The setup check flags a 403 of a watched adapter (Etsy) as appPending;
  the setup page then keeps the keys on a draft connector and says the
  owner will be emailed. Retries update that connector.
- AppApprovalWatcherService re-checks the stored keys with the same
  /openapi-ping call: hourly the first day, then every 6 hours, for 14
  days. State lives in connectors.config.appApprovalWatch (no schema
  change); the email is claimed with a conditional write and checked
  against the etsy_app_approved_notified event, so it is sent once per
  connector across restarts and replicas.
- New server-only product events etsy_app_pending_seen and
  etsy_app_approved_notified, and the appApprovedEmail template.
…tcher

# Conflicts:
#	packages/backend/src/audit/product-event.service.ts
#	packages/frontend/src/app/connectors/setup/[slug]/page.tsx
#	packages/frontend/src/lib/api.ts
@helpcodeai
helpcodeai enabled auto-merge (squash) November 2, 2026 10:50
@helpcodeai
helpcodeai merged commit 25a55fa into main Nov 2, 2026
14 checks passed
@helpcodeai
helpcodeai deleted the feat/etsy-approval-watcher branch November 2, 2026 11:20
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant