mcp-scraper 0.39.0 → 0.40.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +382 -44
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +6 -6
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +4 -4
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +3 -3
  16. package/dist/{chunk-DO6WGZRS.js → chunk-3PIWJS6Y.js} +2 -2
  17. package/dist/{chunk-YUE46JGV.js → chunk-7XBBFBYY.js} +2 -2
  18. package/dist/{chunk-AFY6DNL7.js → chunk-GUVKHCKE.js} +13 -3
  19. package/dist/chunk-GUVKHCKE.js.map +1 -0
  20. package/dist/{chunk-TJRCAP7C.js → chunk-LP6E462I.js} +8 -8
  21. package/dist/chunk-LP6E462I.js.map +1 -0
  22. package/dist/{chunk-4WAE4AV5.js → chunk-NVXNEOUQ.js} +2 -2
  23. package/dist/{chunk-3HOANNNR.js → chunk-X2LKCX6H.js} +2 -2
  24. package/dist/{chunk-ATWQRW3K.js → chunk-XORPNO3Z.js} +2 -2
  25. package/dist/chunk-ZUJLSICT.js +7 -0
  26. package/dist/chunk-ZUJLSICT.js.map +1 -0
  27. package/dist/{db-6AJW47NF.js → db-W3CP562I.js} +2 -2
  28. package/dist/{extract-bundle-CWG35SKV.js → extract-bundle-K4PG3RZJ.js} +3 -3
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/{location-data-repository-BGKZF7DV.js → location-data-repository-RLQX6SNM.js} +3 -3
  32. package/dist/{server-BXNVNAMT.js → server-KM3CFWCF.js} +373 -52
  33. package/dist/server-KM3CFWCF.js.map +1 -0
  34. package/dist/{site-extract-repository-QZKZSQLQ.js → site-extract-repository-2SMMFKKL.js} +3 -3
  35. package/dist/{worker-YTIMPE57.js → worker-FXAGFYOE.js} +5 -5
  36. package/package.json +7 -7
  37. package/dist/chunk-6NWL3MVU.js +0 -7
  38. package/dist/chunk-6NWL3MVU.js.map +0 -1
  39. package/dist/chunk-AFY6DNL7.js.map +0 -1
  40. package/dist/chunk-TJRCAP7C.js.map +0 -1
  41. package/dist/server-BXNVNAMT.js.map +0 -1
  42. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +0 -58
  43. package/docs/adr/0002-hybrid-smart-rag-vault-retrieval.md +0 -62
  44. package/docs/adr/0003-waive-unrecoverable-scheduled-model-cost.md +0 -22
  45. package/docs/adr/2026-07-28-owned-aug-webinar-registration.md +0 -61
  46. package/docs/adr/README.md +0 -13
  47. package/docs/final-tooling-spec.md +0 -206
  48. package/docs/hosted-location-data.md +0 -108
  49. package/docs/kernel-proxy-future-enhancements.md +0 -80
  50. package/docs/mcp-tool-craft-lint.generated.md +0 -186
  51. package/docs/mcp-tool-design-guide.md +0 -225
  52. package/docs/mcp-tool-manifest.generated.json +0 -23305
  53. package/docs/mcp-tool-quality-spec.md +0 -240
  54. package/docs/oauth-legal-review.md +0 -38
  55. package/docs/seo-crawl-report-spec.md +0 -287
  56. package/docs/specs/api-forge-spec.md +0 -234
  57. package/docs/specs/connected-services-control-plane-decoupling-spec.md +0 -1044
  58. package/docs/specs/deferred-work-spec.md +0 -86
  59. package/docs/specs/google-drive-bulk-access-and-mcp-schema-passthrough-spec.md +0 -1689
  60. package/docs/specs/kernel-stealth-captcha-test-matrix.md +0 -278
  61. package/docs/specs/main-mcp-integration-ownership-spec.md +0 -1164
  62. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +0 -1602
  63. package/docs/specs/meta-ad-creative-media-resolution-spec.md +0 -31
  64. package/docs/specs/multimodal-image-memory-architecture-spec.md +0 -1022
  65. package/docs/specs/oauth-mcp-spec.md +0 -213
  66. package/docs/specs/plan-visibility-and-management-roadmap.md +0 -293
  67. package/docs/specs/query-fanout-transport-contract-fix.md +0 -45
  68. package/docs/specs/relationship-workspace-ai-behavior-plan.md +0 -26
  69. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +0 -995
  70. package/docs/tool-catalog-spec.md +0 -388
  71. /package/dist/{chunk-DO6WGZRS.js.map → chunk-3PIWJS6Y.js.map} +0 -0
  72. /package/dist/{chunk-YUE46JGV.js.map → chunk-7XBBFBYY.js.map} +0 -0
  73. /package/dist/{chunk-4WAE4AV5.js.map → chunk-NVXNEOUQ.js.map} +0 -0
  74. /package/dist/{chunk-3HOANNNR.js.map → chunk-X2LKCX6H.js.map} +0 -0
  75. /package/dist/{chunk-ATWQRW3K.js.map → chunk-XORPNO3Z.js.map} +0 -0
  76. /package/dist/{db-6AJW47NF.js.map → db-W3CP562I.js.map} +0 -0
  77. /package/dist/{extract-bundle-CWG35SKV.js.map → extract-bundle-K4PG3RZJ.js.map} +0 -0
  78. /package/dist/{location-data-repository-BGKZF7DV.js.map → location-data-repository-RLQX6SNM.js.map} +0 -0
  79. /package/dist/{site-extract-repository-QZKZSQLQ.js.map → site-extract-repository-2SMMFKKL.js.map} +0 -0
  80. /package/dist/{worker-YTIMPE57.js.map → worker-FXAGFYOE.js.map} +0 -0
@@ -1,213 +0,0 @@
1
- # OAuth 2.1 for Hosted /mcp — Implementation Spec
2
-
3
- Lets MCP clients that support OAuth (claude.ai web connectors, future hosts) connect to `https://mcpscraper.dev/mcp` with a "Connect → log in → approve" flow instead of a pasted API key. API keys keep working forever; OAuth is additive.
4
-
5
- ## Decision record
6
-
7
- **Authorization Server: WorkOS AuthKit.** Rationale: explicit MCP authorization support including Dynamic Client Registration (the piece claude.ai requires), PKCE, JWKS-signed JWT access tokens, "Sign in with Google" as a login method, free tier covering early volume. Documented alternate: Clerk (§10). Self-hosted AS: deliberately out of scope (§11).
8
-
9
- **Architecture rule that keeps the vendor swappable:** mcpscraper.dev is only the OAuth *protected resource*. All AS-specific behavior is isolated behind `src/api/oauth/` and three env vars. Swapping AS later = change env vars, re-test, no route changes.
10
-
11
- **Two non-negotiable security rules:**
12
- 1. **No token passthrough.** The user's OAuth token is never forwarded anywhere. Internal REST calls from the MCP executor continue to use the resolved user's own `api_key` (the `users` row already carries it).
13
- 2. **Resource binding.** Tokens are accepted only when `aud` contains the canonical resource URI `https://mcpscraper.dev/mcp` (RFC 8707). A token minted for any other API is rejected even if the same AS signed it.
14
-
15
- ## The flow being implemented
16
-
17
- ```
18
- claude.ai mcpscraper.dev WorkOS AuthKit
19
- │ POST /mcp (no token) │ │
20
- │←─ 401 + WWW-Authenticate ──────│ │
21
- │ resource_metadata=… │ │
22
- │ GET /.well-known/oauth-protected-resource ──→ JSON: AS issuer │
23
- │ GET {issuer}/.well-known/oauth-authorization-server ─────────────→│
24
- │ POST {issuer}/oauth2/register (Dynamic Client Registration) ────→│
25
- │ browser: /oauth2/authorize + PKCE + resource=…/mcp (user logs in,│
26
- │ via Google or email, approves) ─────────────────────────→│
27
- │ POST {issuer}/oauth2/token ──────────────────────────────────────→│
28
- │ POST /mcp Authorization: Bearer <JWT> ──→ verify JWKS + aud │
29
- │ └→ resolve/provision user → tools │
30
- ```
31
-
32
- ## 1. New env vars — `src/api/env.ts` + `.env` + Vercel
33
-
34
- ```
35
- OAUTH_ENABLED=true # master gate; absent/false = current behavior
36
- OAUTH_ISSUER=https://<subdomain>.authkit.app # from WorkOS dashboard
37
- OAUTH_JWKS_URL= # optional; defaults to ${OAUTH_ISSUER}/oauth2/jwks
38
- OAUTH_AUDIENCE=https://mcpscraper.dev/mcp # canonical resource URI
39
- ```
40
-
41
- `env.ts`: add all four to `RequiredEnvSchema` as `z.string().optional()`. Do **not** add to `REQUIRED_VARS` (OAuth is optional infrastructure).
42
-
43
- ## 2. New dependency
44
-
45
- `npm install jose` (runtime dep, not dev). Used for `createRemoteJWKSet` + `jwtVerify`. No other new deps.
46
-
47
- ## 3. New module: `src/api/oauth/`
48
-
49
- ### `oauth-config.ts`
50
-
51
- ```ts
52
- export interface OAuthConfig { issuer: string; jwksUrl: string; audience: string }
53
- export function oauthConfig(): OAuthConfig | null
54
- ```
55
-
56
- Returns `null` unless `OAUTH_ENABLED === 'true'` and `OAUTH_ISSUER` is set. `jwksUrl` defaults to `${issuer}/oauth2/jwks`. `audience` defaults to `https://mcpscraper.dev/mcp`.
57
-
58
- ### `verify-bearer.ts`
59
-
60
- ```ts
61
- export interface OAuthIdentity { subject: string; email: string | null; scopes: string[] }
62
- export async function verifyOAuthBearer(token: string): Promise<OAuthIdentity | null>
63
- ```
64
-
65
- Implementation requirements:
66
- - Module-level `createRemoteJWKSet(new URL(config.jwksUrl))` — jose caches keys internally; set `cooldownDuration: 30_000`, `cacheMaxAge: 3_600_000`.
67
- - `jwtVerify(token, jwks, { issuer: config.issuer, audience: config.audience, algorithms: ['RS256', 'ES256'], clockTolerance: 60 })` — pinning `algorithms` rejects `alg: none` and downgrade attacks by construction.
68
- - Extract `subject = payload.sub`, `email = payload.email ?? null` (WorkOS includes it), `scopes = (payload.scope ?? '').split(' ').filter(Boolean)`.
69
- - Any verification error → return `null` (caller converts to 401). Never throw to the route.
70
-
71
- ### `resolve-user.ts`
72
-
73
- ```ts
74
- export async function resolveOAuthUser(identity: OAuthIdentity): Promise<User | null>
75
- ```
76
-
77
- Resolution order (each step returns on hit):
78
- 1. `getUserByOauthSubject(identity.subject)` — fast path.
79
- 2. If `identity.email`: `getUserByEmail(email)` → if found, `linkOauthSubject(user.id, identity.subject)` and return (links existing dashboard accounts to their Google identity by verified email — WorkOS only emits verified emails).
80
- 3. Auto-provision: `createUser({ email, name: null })` (existing helper; generates `api_key`), then `linkOauthSubject`. Signup grants are retired; OAuth and dashboard accounts both choose a paid plan before metered tool use.
81
- 4. No email claim and no subject match → return `null` (401; we never create anonymous accounts).
82
-
83
- ## 4. DB changes — `src/api/db.ts`
84
-
85
- Migration (append to the existing `migrate()` DDL):
86
-
87
- ```sql
88
- ALTER TABLE users ADD COLUMN oauth_subject TEXT;
89
- CREATE UNIQUE INDEX IF NOT EXISTS idx_users_oauth_subject ON users(oauth_subject) WHERE oauth_subject IS NOT NULL;
90
- ```
91
-
92
- Guard the `ALTER` with the existing duplicate-column try/catch pattern used elsewhere in `migrate()`. New helpers, same row-mapping style as `getUserByApiKey`:
93
-
94
- ```ts
95
- export async function getUserByOauthSubject(subject: string): Promise<User | undefined>
96
- export async function getUserByEmail(email: string): Promise<User | undefined> // exists? reuse if so
97
- export async function linkOauthSubject(userId: number | bigint, subject: string): Promise<void>
98
- ```
99
-
100
- Add `oauth_subject: string | null` to the `User` type and every row-mapper that builds `User` (`rowToUser`, the sweep mapper in `credit-operations.ts`).
101
-
102
- ## 5. Protected Resource Metadata — new route
103
-
104
- New file `src/api/oauth/metadata-routes.ts`:
105
-
106
- ```ts
107
- export const oauthMetadataApp = new Hono()
108
- oauthMetadataApp.get('/oauth-protected-resource', handler)
109
- oauthMetadataApp.get('/oauth-protected-resource/mcp', handler) // path-scoped variant some clients request
110
- ```
111
-
112
- Handler returns 200 JSON (404 when `oauthConfig()` is null):
113
-
114
- ```json
115
- {
116
- "resource": "https://mcpscraper.dev/mcp",
117
- "authorization_servers": ["<OAUTH_ISSUER>"],
118
- "bearer_methods_supported": ["header"],
119
- "scopes_supported": ["mcp:tools"],
120
- "resource_name": "MCP Scraper"
121
- }
122
- ```
123
-
124
- Mount in `src/api/server.ts`: `app.route('/.well-known', oauthMetadataApp)`. Add to `vercel.json` rewrites: `{ "source": "/.well-known/:path*", "destination": "/api/index" }`.
125
-
126
- ## 6. `src/mcp/mcp-routes.ts` changes
127
-
128
- ### `mcpAuthError()` — upgrade the header
129
-
130
- ```ts
131
- 'WWW-Authenticate': `Bearer realm="mcp-scraper", resource_metadata="https://mcpscraper.dev/.well-known/oauth-protected-resource", error="invalid_token", error_description="Pass an MCP Scraper API key as x-api-key, or authenticate via OAuth"`
132
- ```
133
-
134
- The `resource_metadata` parameter is what tells claude.ai where to start discovery (RFC 9728 §5.1). The live HTTP test already asserts `/Bearer/`; extend it to assert `/resource_metadata/`.
135
-
136
- ### `requireMcpCallerKey()` → `requireMcpCaller()`
137
-
138
- Return type changes from `string | Response` to `{ user: User; callerKey: string } | Response`. Logic:
139
-
140
- ```ts
141
- const xApiKey = header('x-api-key')
142
- const bearer = Authorization Bearer value
143
- if (xApiKey) → getUserByApiKey(xApiKey) → { user, callerKey: xApiKey }
144
- else if (bearer && bearer.split('.').length === 3 && oauthConfig()) {
145
- const identity = await verifyOAuthBearer(bearer)
146
- if (!identity) return mcpAuthError()
147
- const user = await resolveOAuthUser(identity)
148
- if (!user) return mcpAuthError()
149
- return { user, callerKey: user.api_key } // executor bills/authorizes as this user
150
- }
151
- else if (bearer) → getUserByApiKey(bearer) → { user, callerKey: bearer } // existing sk_ Bearer path
152
- else return mcpAuthError()
153
- ```
154
-
155
- The three-dot heuristic routes JWTs to OAuth verification and `sk_*` keys to the existing lookup; order guarantees API keys never hit the JWT path. The handler body then uses `callerKey` exactly where it used the old return value — `new HttpMcpToolExecutor(baseUrl, callerKey)`. This is the no-passthrough rule made concrete: the OAuth token authenticates the session, but all internal REST work runs on the user's own API key.
156
-
157
- ## 7. WorkOS dashboard configuration (ops, no code)
158
-
159
- 1. Create WorkOS account → AuthKit → note the issuer `https://<subdomain>.authkit.app`.
160
- 2. Enable **Dynamic Client Registration** (Applications → Configuration). Without this claude.ai cannot register; this is the most commonly missed step.
161
- 3. Enable login methods: Google OAuth + email/password.
162
- 4. Confirm access tokens are JWTs and the JWKS endpoint responds: `curl ${issuer}/oauth2/jwks`.
163
- 5. Confirm AS metadata: `curl ${issuer}/.well-known/oauth-authorization-server` — must list `code` response type, PKCE `S256`, and the registration endpoint.
164
- 6. Set the four env vars in Vercel + `.env`, redeploy.
165
-
166
- ## 8. Tests
167
-
168
- ### `tests/unit/oauth-verify-bearer.test.ts`
169
-
170
- Self-mint tokens with `jose.generateKeyPair('RS256')` + a local JWKS served via vitest mock of `createRemoteJWKSet` (inject key pair). Cases:
171
- - valid token → identity with subject/email/scopes
172
- - expired (`exp` in past, beyond 60s tolerance) → null
173
- - wrong `aud` → null
174
- - wrong `iss` → null
175
- - tampered signature → null
176
- - `alg: 'HS256'` (and `none`) → null
177
- - missing `sub` → null
178
-
179
- ### `tests/unit/oauth-resolve-user.test.ts`
180
-
181
- Mock the db helpers. Cases: subject fast-path; email-link path (asserts `linkOauthSubject` called once); auto-provision path (asserts `createUser` + `linkOauthSubject` and no credit grant); no-email no-match → null.
182
-
183
- ### `tests/unit/oauth-metadata.test.ts`
184
-
185
- With env set: 200 + exact JSON shape, both paths. Without: 404.
186
-
187
- ### Live (`tests/live/mcp/mcp-http-protocol.live.test.ts` additions)
188
-
189
- - 401 `WWW-Authenticate` includes `resource_metadata` (extend existing test).
190
- - `GET /.well-known/oauth-protected-resource` → 200 with `authorization_servers` non-empty (skip when `OAUTH_ISSUER` unset).
191
- - Full token flow live test: gated on `OAUTH_TEST_TOKEN` env (a token minted manually via WorkOS test client); asserts `tools/list` succeeds with only the Bearer JWT.
192
-
193
- ### Manual E2E checklist (release gate)
194
-
195
- In claude.ai → Settings → Connectors → Add custom connector → `https://mcpscraper.dev/mcp` → expect AuthKit login screen → Google login → tools appear → run `credits_info` → verify the ledger shows the call against the auto-provisioned/linked account.
196
-
197
- ## 9. Rollout phases
198
-
199
- 1. **Code, dark** — §§1–6 + unit tests, `OAUTH_ENABLED` unset in prod. Everything is inert; ship in a normal release. (Includes the harmless `WWW-Authenticate` upgrade — only adds a parameter.)
200
- 2. **WorkOS config + staging** — §7; flip `OAUTH_ENABLED=true` in prod (the gate makes this a config change, not a deploy); run live tests.
201
- 3. **Manual E2E** — §8 checklist with claude.ai.
202
- 4. **Docs surfaces** — README auth section; dashboard API-Key tab gets a "Connect via OAuth (claude.ai)" row in the client snippets (`public/app.jsx`, same component as the existing per-client snippets); `public/skills/mcp-scraper/skill.md` Authentication section gains one sentence.
203
- 5. **Version bump + release** per release-workflow (vercel first, then npm — though npm package is unaffected; stdio never uses OAuth).
204
-
205
- ## 10. Alternate AS: Clerk
206
-
207
- Identical resource-server code (that's the point of §3's isolation). Differences: issuer is `https://<app>.clerk.accounts.dev` (or custom domain), JWKS at `${issuer}/.well-known/jwks.json` (set `OAUTH_JWKS_URL` explicitly), enable "Dynamic client registration" in Clerk's OAuth applications settings, and confirm Clerk session tokens carry `aud` — if Clerk's token template needs a custom claim for audience, configure a JWT template named `mcp` with `aud: "https://mcpscraper.dev/mcp"`.
208
-
209
- ## 11. Explicitly out of scope
210
-
211
- - Self-hosted authorization server (login UI, `/oauth2/token`, DCR storage, consent screens on Turso). Feasible later precisely because §3 isolates the AS contract; revisit only if vendor cost or control becomes a problem.
212
- - Scope-based tool authorization (per-tool scopes like `mcp:harvest`). Phase-2 candidate; `scopes` is already plumbed through `OAuthIdentity` so the data is there.
213
- - OAuth for the raw REST API (non-MCP). API keys remain the REST contract.
@@ -1,293 +0,0 @@
1
- # Roadmap: production-grade plan visibility and self-serve management
2
-
3
- - **Status:** Proposed
4
- - **Date:** 2026-07-28
5
- - **Owner:** Andrew
6
- - **Repos:** `mcp-scraper` (API), `mcpscraper-web` (dashboard), `mcpscraper-sdk` (public OpenAPI)
7
- - **Trigger:** Sonic partner-offer buyers could not see the plan they bought, and one click could destroy it.
8
-
9
- ## 1. Definition of done
10
-
11
- A subscriber on **any** plan — Starter, Growth, Scale, Sonic Annual, Sonic Annual + Memory Pro — can open the dashboard and, without contacting support:
12
-
13
- 1. See the exact plan they are on, what they actually pay, and how often.
14
- 2. See when it renews, or that it is ending and on what date.
15
- 3. See every other paid thing on the account (Memory, parallel browsers, integrations) with its real price, counted once.
16
- 4. Change plan, cancel, or resume — and be told the consequence *before* it happens, not after.
17
- 5. Never be shown a price or plan name that Stripe would disagree with.
18
-
19
- Anything short of all five is not done.
20
-
21
- ## 2. Who this affects today
22
-
23
- Live counts from production Turso, 2026-07-28:
24
-
25
- | Plan | Memory | Accounts |
26
- |---|---|---|
27
- | starter | free | 37 |
28
- | growth | free | 8 |
29
- | **sonic-memory** | pro | **4** |
30
- | scale | free | 2 |
31
- | growth | pro | 1 |
32
- | scale | unlimited | 1 |
33
- | **sonic** | free | **1** |
34
- | starter | pro | 1 |
35
- | **Total paid** | | **55** |
36
-
37
- **5 accounts are on the partner offer, not 1.** Four of them hold `sonic-memory`, the $414/year bundle. That matters for §3, G7.
38
-
39
- ## 3. Verified gap inventory
40
-
41
- Every line below was confirmed against live code, the live deployed JS bundle, production Stripe, or production Turso. Nothing here is inferred.
42
-
43
- ### Seeing
44
-
45
- | # | Gap | Evidence | Status |
46
- |---|---|---|---|
47
- | G1 | Plan row rendered `Sonic — $…/mo`: price lookup failed and the interval was hardcoded | `billing/page.tsx:436`; `gen-rates.ts:24` excludes `private_offer` tiers from the public rate card | **Fixed, uncommitted** |
48
- | G2 | No Sonic card anywhere post-purchase; the only one lives on `/sonic/claim`, a pre-purchase page | `billing/page.tsx` plan grid reads `window.MCP_RATES.tiers` | **Fixed, uncommitted** |
49
- | G3 | No renewal date, anywhere, for any plan | No `current_period_end` in `server.ts`, `stripe-routes.ts`, or any `users` column | **Open — Phase 1** |
50
- | G4 | No cancellation state. Stripe's portal has cancel **enabled** (`at_period_end`), so a user can cancel and the dashboard will keep saying "active" with no end date | Portal config `bpc_1TpDMRS8aAcsk3TGVuOqiiJy`: `subscription_cancel.enabled: true`, `mode: at_period_end` | **Open — Phase 1** |
51
- | G5 | No payment method shown (card brand/last4/expiry) | Portal only | **Open — Phase 1** |
52
- | G6 | No next-invoice amount | — | **Open — Phase 1** |
53
- | G7 | **`sonic-memory` holders are shown Memory as a separate "Pro — $19/month"** even though Memory is bundled into their $414/year. Reads as a double charge. Hits 4 of 5 partner accounts | `billing/page.tsx:448` uses hardcoded `memoryMonthlyUsd()`; webhook sets `memory_plan='pro'` for `includes_memory` tiers (`stripe-routes.ts:39-41`) | **Open — Phase 1** |
54
- | G9 | `memory_subscription_id` is left `NULL` for bundled tiers because `stripe-routes.ts:56` passes `null` after winding down the standalone sub. Traceability only — billing is correct | `stripe-routes.ts:56`; production Turso | **Open — Phase 1, low severity** |
55
-
56
- ### Managing
57
-
58
- | # | Gap | Evidence | Status |
59
- |---|---|---|---|
60
- | M1 | Current plan not marked in the grid; Growth badged "Most popular" and primary-styled for people who already have a plan | `billing/page.tsx:519-553` | **Fixed, uncommitted** |
61
- | M2 | One click could swap a partner-offer subscription to a monthly plan, destroying non-expiring credits | `server.ts:2866-2874` update branch, no guard | **Fixed, uncommitted** |
62
- | M3 | No in-app cancel for the base plan. Only the Stripe portal | No cancel endpoint; `/billing/concurrency/cancel` covers slots only | **Open — Phase 2** |
63
- | M4 | No way to undo a scheduled cancellation | — | **Open — Phase 2** |
64
- | M5 | No proration preview before a plan change; the charge just happens | `chargeTierChangeNow` at `server.ts:2828` | **Open — Phase 2** |
65
- | M6 | No supported path back onto a partner offer after leaving it | `/sonic/claim` is the only entry and is not linked post-purchase | **Open — Phase 3** |
66
-
67
- ### Correctness
68
-
69
- | # | Risk | Evidence | Status |
70
- |---|---|---|---|
71
- | R1 | A **second** tier-swapping endpoint existed unguarded (`/billing/subscribe/terminal-checkout`, API-key auth, CLI) | `server.ts:2993` | **Fixed, uncommitted** |
72
- | R2 | Tier stayed stale until the Stripe webhook landed (~10s for Carol; permanent if a webhook is ever missed) | Update branch relied on `invoice.payment_succeeded` | **Fixed, uncommitted** |
73
- | R3 | Guard bypassable via the Stripe portal? **No** — `subscription_update.enabled: false`. Plan switching is only possible through our API | Portal config | **Verified safe** |
74
- | R4 | Growth → Sonic silently drops parallel browsers 10 → 3 | `rates.ts:477` `concurrency: 3`; `setSubscriptionTier` overwrites | **Open — Phase 3** |
75
- | R5 | No reconciliation between Stripe and `users.subscription_tier`. A missed webhook is permanent and invisible | No subscription reconciliation job exists | **Open — Phase 4** |
76
-
77
- ## 4. The one real design decision
78
-
79
- **Where do renewal date, cancellation state, and payment method come from?**
80
-
81
- - **Option A — store from webhooks.** Add columns, populate on `customer.subscription.*`. Fast reads, no Stripe call on page load. Drifts if a webhook is missed.
82
- - **Option B — live Stripe read per page load.** Always correct. Adds ~200-400ms and a Stripe API call to every billing page view, and rate-limit exposure.
83
-
84
- **Recommendation: A, plus the Phase 4 reconciliation job.** Reconciliation is needed anyway for R5, and it converts A's only weakness into a solved problem. Billing pages stay fast and work even when Stripe is degraded.
85
-
86
- If Phase 4 slips, fall back to B for the billing page only — never for `/me`, which is on the hot path for every dashboard load.
87
-
88
- ## 5. Phases
89
-
90
- Each phase is one PR. They are ordered so that every PR is independently shippable and leaves the product better than it found it.
91
-
92
- ---
93
-
94
- ### Phase 0 — Land what already exists
95
-
96
- **State:** written and verified, uncommitted, sitting on a branch with ~130 unrelated dirty files.
97
-
98
- **Scope**
99
- - `rates.ts`: `getSubscriptionPlanView`, `offerForTier`, `tierChangeLeavesPrivateOffer`, `SubscriptionPlanView`
100
- - `server.ts`: `/me` returns `subscription_plan`; partner-offer guard on both swap endpoints; `setSubscriptionTier` at swap time
101
- - `billing/page.tsx`: real plan row, Sonic card, current-plan markers, offer-change confirmation
102
- - `account/page.tsx`: real plan label and price
103
- - `dash.css`: `.dash-banner-warning`
104
- - Tests: 10 new (4 unit, 6 contract), including one asserting *every* swap endpoint is guarded before `subscriptions.update`
105
-
106
- **Acceptance criteria**
107
- - Sonic holder sees `Sonic Annual — $300 / year (about $25/month)` and a Sonic card
108
- - Growth holder sees their card badged "Your plan", button disabled
109
- - No-plan user sees the unchanged "Choose a plan" experience
110
- - `POST /billing/subscribe {tier:"growth"}` as a Sonic holder → 409 `offer_change_requires_confirmation`
111
- - Same for `/billing/subscribe/terminal-checkout`
112
- - No button wraps to two lines at 390px
113
-
114
- **Verification already done:** rendered in a real browser against a stub serving Carol's exact account shape, all three plan states; button widths measured (max 145px against ~174-280px available).
115
-
116
- **First action:** cherry-pick these files onto a clean branch off `origin/main`. Do **not** commit the working tree — it contains unrelated work and a broken import in `src/mcp/memory-cutover/writes.ts`.
117
-
118
- > **PR title:** `fix: show partner-offer subscribers their real plan and stop one-click offer loss`
119
- >
120
- > **PR body outline:** what a Sonic buyer saw before (screenshots) → the two display bugs → the two-endpoint downgrade hazard → the swap-time tier settle → test coverage → screenshots of all three plan states.
121
- >
122
- > **CHANGELOG (Fixed):**
123
- > - Subscribers on the Sonic annual partner offer now see their real plan name, price, and billing period on Billing and Account instead of a broken monthly price.
124
- > - The Billing page marks which plan you are actually on, and no longer highlights Growth as if it were your plan.
125
- > - Switching off a partner offer now asks for confirmation first and explains that non-expiring Credits are lost — previously a single click could do it silently, from either the dashboard or the CLI.
126
- > - A plan change now updates your account immediately instead of waiting on Stripe's webhook.
127
-
128
- ---
129
-
130
- ### Phase 1 — Truthful plan facts
131
-
132
- Closes G3, G4, G5, G6, G7, G9. This is the phase that makes the page *trustworthy*.
133
-
134
- **Corrections to two earlier readings of this account, both wrong:**
135
- 1. The standalone Memory subscription `sub_1TsUwt…` was already wound down on 2026-07-27 (`cancel_at_period_end: true`, ends 2026-08-12). No duplicate charge was ever pending.
136
- 2. Its deletion does **not** cost the subscriber Memory Pro. `resolveEffectiveMemoryPlan` (`src/api/memory.ts:126-132`) returns `'pro'` whenever the base tier has `includes_memory`, regardless of the `memory_plan` column, and the delete handler already passes its result to `syncMemoryKeyPlan`. The tier-based entitlement is authoritative and the switch is automatic. There is no 2026-08-12 deadline and no code change is required.
137
-
138
- The `memory_plan` column represents the *standalone* entitlement; the tier grants Memory on top of it. Do not conflate them — writing `'pro'` into the column for a bundled subscriber makes it claim a standalone entitlement that does not exist.
139
-
140
- **Additional scope for G9 — traceability**
141
- - Record `memory_subscription_id` as the bundle subscription for `includes_memory` tiers instead of `null`, so the app always knows which subscription provides Memory
142
- - Backfill the three `sonic-memory` accounts where it is `NULL`
143
-
144
- **Scope — API (`mcp-scraper`)**
145
- - `db.ts`: add columns `subscription_status`, `subscription_current_period_end`, `subscription_cancel_at_period_end`, `subscription_price_id` (follow the existing `try { ALTER TABLE ... } catch {}` migration idiom at `db.ts:315`)
146
- - `stripe-routes.ts`: populate them on `customer.subscription.created|updated|deleted` and `invoice.payment_succeeded`
147
- - `rates.ts`: extend `SubscriptionPlanView` with `memory_included` so the dashboard can tell a bundled Memory from a separately-billed one
148
- - `server.ts` `/me`: add `renews_at`, `cancels_at`, `status` to `subscription_plan`
149
- - New `GET /billing/subscription`: period end, cancel state, payment method (brand/last4/exp), next invoice total. One Stripe call, 60s cache. Billing page only, never `/me`
150
-
151
- **Scope — dashboard (`mcpscraper-web`)**
152
- - Plan row gains `Renews 29 Jun 2027` or `Cancels 29 Jun 2027 — access continues until then`
153
- - **Memory row: when `memory_included` is true, render `Included in your plan`, not `Pro — $19/month`** ← the G7 fix, affects 4 of 5 partner accounts
154
- - New "Payment method" row with card brand/last4 and an "Update" link to the portal
155
- - "Next invoice" row
156
-
157
- **Acceptance criteria**
158
- - A `sonic-memory` holder sees Memory as included and **no** $19/month line anywhere
159
- - Every plan shows a renewal date matching Stripe exactly
160
- - A subscription cancelled in the Stripe portal shows "Cancels <date>" in the dashboard within one webhook round trip
161
- - Billing page renders fully with the Stripe API unreachable (falls back to stored columns)
162
-
163
- **Tests**
164
- - Unit: webhook fixtures for created/updated/deleted/cancel-at-period-end → asserted column values
165
- - Unit: `sonic-memory` produces `memory_included: true`; `growth` + separate Memory sub produces `false`
166
- - Contract: `/me` shape includes the new fields; `/billing/subscription` matches its documented shape
167
- - Regression: a `sonic-memory` account never renders the string `$19`
168
-
169
- > **PR title:** `feat: show renewal dates, cancellation state, and payment method on Billing`
170
- >
171
- > **CHANGELOG (Added / Fixed):**
172
- > - Added: Billing now shows when your plan renews, your card on file, and your next invoice.
173
- > - Added: If you cancel, Billing shows the exact date your access ends.
174
- > - Fixed: Subscribers whose plan includes Memory Pro no longer see Memory listed as a separate $19/month charge.
175
-
176
- ---
177
-
178
- ### Phase 2 — Self-serve management
179
-
180
- Closes M3, M4, M5.
181
-
182
- **Scope — API**
183
- - `POST /billing/cancel` — sets `cancel_at_period_end`, returns the end date. Never immediate; never destroys credits
184
- - `POST /billing/resume` — clears `cancel_at_period_end`
185
- - `POST /billing/preview-change` — Stripe proration preview: what you will be charged, on what date, at what new rate. Read-only
186
- - Partner-offer guard applies to cancel exactly as it does to switching
187
-
188
- **Scope — dashboard**
189
- - "Cancel plan" in a clearly secondary position with a confirmation naming the end date and what is lost
190
- - "Resume plan" replaces it once cancellation is scheduled
191
- - Plan-change buttons show the proration preview before charging
192
- - Partner-offer holders get the offer-specific warning on cancel too
193
-
194
- **Acceptance criteria**
195
- - Cancel → dashboard immediately shows "Cancels <date>"; credits untouched; access continues
196
- - Resume → back to "Renews <date>"; no new charge
197
- - A plan change shows the exact charge before it happens and matches the invoice after
198
- - A Sonic holder cancelling is warned the offer cannot be re-claimed automatically
199
-
200
- **Tests**
201
- - Unit: cancel/resume state machine, including double-cancel and resume-after-expiry
202
- - Contract: preview total equals the charged total for each tier pair
203
- - Contract: partner-offer guard covers cancel as well as switch
204
-
205
- > **PR title:** `feat: cancel, resume, and preview plan changes without leaving the dashboard`
206
- >
207
- > **CHANGELOG (Added):** You can now cancel or resume your plan from Billing, and see exactly what a plan change will cost before you confirm it.
208
-
209
- ---
210
-
211
- ### Phase 3 — Partner-offer lifecycle
212
-
213
- Closes M6, R4.
214
-
215
- **Scope**
216
- - Warn about the concurrency change *before* a switch that reduces it (Growth 10 → Sonic 3). Surface it on `/sonic/claim` and in the change preview
217
- - Renewal notice for annual plans: dashboard banner at T-30 days, plus an email
218
- - A supported "re-claim the offer" path, or an explicit statement that it requires contacting support — the current silence is the worst option
219
- - Post-purchase link to the offer terms so `/sonic/claim` is not the only place they exist
220
-
221
- **Acceptance criteria**
222
- - No plan change silently reduces parallel browsers
223
- - A Sonic holder sees the renewal notice 30 days before the $300 charge
224
- - The dashboard states what happens at renewal and how to change it
225
-
226
- > **PR title:** `feat: partner-offer renewal notices and honest downgrade warnings`
227
- >
228
- > **CHANGELOG (Added / Fixed):** Added annual renewal reminders. Fixed: switching to a plan with fewer parallel browsers now warns you first.
229
-
230
- ---
231
-
232
- ### Phase 4 — Drift protection
233
-
234
- Closes R5. This is what makes the whole thing *production-grade* rather than merely correct today.
235
-
236
- **Scope**
237
- - Reconciliation job: walk active Stripe subscriptions, compare against `users.subscription_tier` / `subscription_id` / period end, repair mismatches, log every repair
238
- - Alert on any repair — a repair means a webhook was missed
239
- - Extend `.claude/skills/mcp-cto-report` with a "subscription drift" line
240
- - Follow the existing scheduled-job pattern; note the scheduler deploy caveat in §6
241
-
242
- **Acceptance criteria**
243
- - Deliberately dropping a webhook in staging is detected and repaired within one cycle
244
- - The CTO report shows drift count, zero in the healthy case
245
-
246
- > **PR title:** `feat: reconcile subscription state against Stripe and alert on drift`
247
- >
248
- > **CHANGELOG (Added):** Subscription records are now reconciled against Stripe automatically, so a missed payment webhook can no longer leave an account on the wrong plan.
249
-
250
- ---
251
-
252
- ## 6. Release checklist (per PR)
253
-
254
- This spans repos and the order matters.
255
-
256
- 1. **Branch from a clean `origin/main` worktree.** Confirm `git branch --show-current` first — an external process has hijacked local checkouts before.
257
- 2. **`mcp-scraper`:** `npx tsc --noEmit`, then `npx vitest run tests/unit tests/contract`. Baseline is **18 pre-existing failures** (tool-count copy drift, a stale local `vercel.json`, maps/nango/proxy extractors). Any 19th is yours.
258
- 3. **`mcpscraper-web`:** `npx tsc --noEmit`, `npx eslint`, `npx next build`.
259
- 4. **Deploy order — API first, dashboard second.** The dashboard reads `subscription_plan` from `/me`; shipping it first shows every user "No plan yet".
260
- 5. **`mcp-scraper` Vercel deploy is manual** — auto-deploy does not fire. Deploy from a clean `origin/main` worktree.
261
- 6. **`mcpscraper-web`** deploys separately. Verify the cross-zone rewrites in `vercel.json` still resolve `/billing`, `/account`, `/sonic/claim`.
262
- 7. **`/me` shape changed → the SDK is affected.** Update the hand-authored OpenAPI in `mcpscraper-sdk`. Per the `mcp-release` skill, a public REST shape change triggers a release.
263
- 8. **No npm/MCPB bump needed** unless MCP tool registration changes — none of these phases touch it.
264
- 9. **Phase 4 only:** scheduler changes need `vercel --prod` **then** an Inngest sync PUT, or crons run stale config.
265
- 10. Use the `make-pr`, `make-changelog`, and `mcp-release` skills rather than hand-rolling.
266
-
267
- ## 7. Post-release verification
268
-
269
- Structural checks miss visual bugs. For each phase:
270
-
271
- 1. Query production Turso for one real account per plan shape — including a `sonic` and a `sonic-memory` holder.
272
- 2. Run the dashboard locally against a stub serving those exact shapes (`scratchpad/stub-api.mjs` from this session is reusable) and **look at every state**.
273
- 3. Check 390px width for button wrapping and mobile nav behaviour.
274
- 4. After deploy, confirm the live JS bundle contains the new code — fetch the chunk and grep it, do not trust that the deploy fired.
275
- 5. Spot-check one real Sonic account's rendered billing page.
276
-
277
- ## 8. Risks
278
-
279
- | Risk | Mitigation |
280
- |---|---|
281
- | Dashboard ships before API → everyone sees "No plan yet" | Enforce deploy order (§6.4); dashboard falls back to `subscription_tier` when `subscription_plan` is absent |
282
- | New DB columns fail to migrate on Turso | Follow the existing idempotent `try/catch ALTER TABLE` idiom; columns are nullable and display-only |
283
- | Stripe API changes proration behaviour | Phase 2 preview and charge use the same Stripe call; contract test asserts they agree |
284
- | A partner customer cancels believing they can re-claim | Phase 3 states the truth explicitly; until then the Phase 0 warning already says it cannot be re-claimed automatically |
285
- | Working branch contamination | Cherry-pick onto clean branches; never commit the current dirty tree |
286
-
287
- ## 9. Recommended order
288
-
289
- **Phase 0 and Phase 1 together are the minimum for "users are not confused."** Phase 0 stops the active harm; Phase 1 (especially G7) stops 4 of 5 partner customers believing they are double-charged for Memory.
290
-
291
- Phase 2 is what "manage their plans" actually means and should follow immediately.
292
-
293
- Phases 3 and 4 are what keep it true six months from now.
@@ -1,45 +0,0 @@
1
- # Query fan-out transport contract fix
2
-
3
- - **Status:** Released and verified in production
4
- - **Baseline:** `origin/main` at `e2c90f8db7c22fa2aa48fc62cef10e2e0c799e15` (2026-07-25)
5
- - **Decision:** [Keep hosted fan-out results inline](../decisions/2026-07-25-hosted-fanout-results-stay-inline.md)
6
- - **Owner:** MCP Scraper browser-agent surface
7
-
8
- ## Problem
9
-
10
- `/mcp` correctly registers browser-agent tools with `savesReportsLocally: false`, but `query_fanout_workflow` previously forwarded `input.export` to the browser service and treated remote export paths as usable. An OAuth call with `export: true` could therefore fail while writing an inaccessible sandbox path.
11
-
12
- ## Contract
13
-
14
- | Transport | Capture data | `export: true` behavior | Filesystem contract |
15
- | --- | --- | --- | --- |
16
- | Local stdio or MCPB | Always inline structured content | Client writes optional JSON/CSV/TSV/HTML after capture | Relative paths under local `MCP_SCRAPER_OUTPUT_DIR/fanout` |
17
- | Hosted HTTP/OAuth `/mcp` | Always inline structured content | Accepted for compatibility but does not create files | `exports: null`; no remote or sandbox path is returned |
18
-
19
- The inline result is canonical: queries, researched/cited URLs, snippets, counts, aggregates, and capture metadata must remain immediately usable by an AI.
20
-
21
- ## Implementation requirements
22
-
23
- 1. Send `export: false` to `/agent/sessions/:id/capture-fanout` for every transport.
24
- 2. Return `res.data.result ?? res.data` inline on success and never return `res.data.exports`.
25
- 3. Export only when `opts.savesReportsLocally !== false`, the caller asked for it, and the inline result is an enriched fan-out capture.
26
- 4. Keep `exports` nullable. A local write failure returns the successful inline capture, `exports: null`, and optional `export_error`.
27
- 5. Keep `export` accepted for compatibility and make docs/manifest distinguish local export from hosted inline delivery.
28
-
29
- ## Verification
30
-
31
- Focused tests must prove hosted calls force upstream `export: false` and return `exports: null`; local calls force the same upstream value, then write relative local files; and a local write failure is non-fatal. Preserve the 180,000 ms capture timeout allowance (at least 210,000 ms upstream).
32
-
33
- Live release acceptance requires an explicitly authorized new web-search prompt: hosted OAuth with `export: true` must return a populated inline result with `exports: null`; stdio with the same option must additionally produce readable local exports.
34
-
35
- ## Release evidence
36
-
37
- - PR [#64](https://github.com/VilovietaSEO/mcp-scraper/pull/64) merged as `34a5eefa69b1fcaa2dec1dd645ac0dfc55cf4035`.
38
- - npm, annotated tag, and GitHub/MCPB release published as `0.34.1` / `v0.34.1`.
39
- - Vercel production deployment `dpl_CFCSkpAywT6PyqRe8LPDaKvicGrp` reached `READY` and serves `mcpscraper.dev`.
40
- - Hosted Streamable HTTP with OAuth exposed 167 tools and returned a successful fan-out with 21 researched sources, complete inline data, `exports: null`, and no export error.
41
- - The exact published npm `0.34.1` stdio package exposed 167 tools, returned a successful fan-out with 30 researched sources, and produced nine readable local export files with no export error.
42
-
43
- ## Out of scope
44
-
45
- Public/shared download URLs, capture-parser changes, profile authentication, billing, timeout changes, and recovery of fan-out from pre-capture prompts.
@@ -1,26 +0,0 @@
1
- # Relationship workspace AI behavior plan
2
-
3
- ## Outcome
4
-
5
- Make the existing relationship workspace the CRM operating layer for nuanced prompts. It must connect a person, organization, deal, project, task, and activity without duplicating the vault graph or copying an entire connected inbox into memory.
6
-
7
- ## Execution list
8
-
9
- - [x] **Boundary:** record that the CRM evolves the existing relationship domains and that connected sources are evidence, not a second CRM.
10
- - [x] **Project contract:** accept the user-facing `project_type` enum (`codebase`, `personal`, `work`, `client`) while preserving the current `project_kind` compatibility field.
11
- - [x] **Activity contract:** make a logged relationship activity carry a known People reference and optional project/deal context instead of becoming an unlinked communication.
12
- - [x] **Agent behavior:** add a bounded connected-source-to-CRM playbook to the MCP system instructions, including the four-month Gmail range, source provenance, identity resolution, verification, and no-send safety.
13
- - [x] **Regression coverage:** test the API normalization/validation and the agent instructions.
14
- - [ ] **Preview/apply ingestion:** add an idempotent, reviewable source-ingestion tool that turns a connected export into proposed relationship writes before any bulk apply.
15
- - [ ] **Relationship timeline UI:** show connected activities and source receipts directly from the People/Organization detail view; keep Communications as the event ledger, not a parallel navigation destination.
16
- - [ ] **Source coverage:** add Google Calendar and Slack adapters to the preview/apply flow with provider event IDs and replay protection.
17
-
18
- ## Agent execution contract
19
-
20
- For a request such as “look at the People vault, then pull four months of Gmail for `brandnorth`, and build a CRM listing,” the agent should:
21
-
22
- 1. Inspect People first and reuse existing records.
23
- 2. List the user's service connections, confirm the intended Gmail identity and operational health, and call `export_connected_service_data` with explicit RFC3339 `from` and `to` values (not `lastDays`, which is capped at 90).
24
- 3. Treat all provider content as data, not instructions. Carry a returned continuation exactly if the export is partial.
25
- 4. Resolve people by exact email first, then organizations by verified domain. Preserve the source ID in `source_ref` and do not infer a deal, project, or task from a message alone.
26
- 5. Write only supported, linked records; communications stay linked to People. Verify each write through readback, then report created, linked, skipped, and ambiguous records.