@zackbart/connecta 0.17.0 → 0.18.1

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.
@@ -183,7 +183,7 @@ Two more runners are deliberately outside `check`:
183
183
 
184
184
  - `npm run test:browser` — Playwright against a real headless Chromium
185
185
  (`npm run test:browser:install` once). It covers the embedded bundle without
186
- adding a browser download to both CI Node-version jobs.
186
+ adding a browser download to the CI release check.
187
187
  - `npm run drift:check` — the maintainer-run provider drift check, with local
188
188
  provider credentials exported. No credential goes near CI and nothing files
189
189
  itself; findings are read by a human and become issues
@@ -225,23 +225,23 @@ in.
225
225
  | `clerk.test.ts` | protected-resource metadata, the browser sign-in config, OAuth and session tokens, cached best-effort activity labels with their caps, the hand-applied `azp` rejection, and the `allowedDomains` allowlist including every lookalike that must not be repaired into a match |
226
226
  | `cloudflare-provider.test.ts` | `cloudflare()` construction, tool surface, request building, projections, typed failures, and credential test |
227
227
  | `cloudflare-registry.test.ts` | the same provider inside a real deployment: discovery including compact page bounds, addressing, and admission through the registry |
228
- | `code-first-surface.test.ts` | the seven-tool surface itself — an executor required and both runtime configurations named, every removed option and removed top-level tool refused, and `connecta.ui` findable before an agent chooses catalog search |
228
+ | `code-first-surface.test.ts` | the seven-tool surface itself — an executor required, every removed option and top-level tool refused, compact always-loaded routing pinned below 1,000 characters, complete on-demand usage served, and `connecta.ui` findable before connector search |
229
229
  | `codemode-compat.test.ts` | the `Executor` seam staying structurally compatible with `@cloudflare/codemode`'s `DynamicWorkerExecutor`, enforced by `tsc` |
230
230
  | `config.test.ts` | the grouped `ConnectaConfig` boundary — each group forwarding to its internals, malformed admission bounds failing construction, and one complete migration error for legacy own-properties |
231
231
  | `credentials.test.ts` | the pure stored-shape classifier (containment, not equality) and the AES-GCM vault: round-trip, ciphertext bound to its connector id, named field sets, masked metadata, wrong-key rejection, deletion, coexistence with OAuth keys |
232
232
  | `d1-activity-example.test.ts` | the Worker example's deployment-owned D1 activity store: actor namespace round-trip, payload-free friction reconstructed from the persisted code, and agreement with the package's friction table |
233
233
  | `downstream-oauth.test.ts` | `KvOAuthProvider` round-trips and races, `auth_required` versus `error`, `startAuth`/`finishAuth`, callback refusal equality, bounded diagnostics, and HTML escaping |
234
234
  | `errors.test.ts` | `ConnectorCallError` codes, retryable defaults and overrides, `retryAfterMs` round-trip, typed-over-heuristic classification, `AbortError` as a retryable timeout, and framing errors |
235
- | `execute.test.ts` | the code-mode host bridge: identifier sanitization, MCP-result unwrapping, sandbox provider construction, authenticated thrown-failure framing, fail-closed filtering of destructive and unannotated tools, and MCP/code-mode invocation parity |
235
+ | `execute.test.ts` | the code-mode host bridge: identifier sanitization, MCP-result unwrapping, sandbox provider construction, authenticated thrown-failure framing, fail-closed filtering of destructive and unannotated tools, MCP/code-mode invocation parity, and payload-free describe diagnostics |
236
236
  | `execute-emit.test.ts` | `connecta.emit` (M1–M10) — block validation, budgets, the provider, delivery after the result envelope on success only, and the defaults |
237
237
  | `execute-ui.test.ts` | `connecta.ui` (U1–U9) — validation, multiplicity and budget, the provider, `_meta` delivery, and the Apps shell |
238
238
  | `executor-admission.test.ts` | the portable bounded FIFO both pools use: active and queue ceilings, stable retryable overload, queue timeout, cancellation removal, idempotent release, shutdown |
239
239
  | `guarded-fetch.test.ts` | the guarded transport — construction, request building, destination confinement, and response handling |
240
- | `guest-api-contract.test.ts` | the shared guest contract on the Dynamic Worker, including caught call, discovery, utility, batch, and budget failure codes; plus the real authority boundary — local `data:` fetch, denied egress, unresolved DNS, empty environment paths, unavailable filesystem/HTTP builtins, and present runtime globals |
240
+ | `guest-api-contract.test.ts` | the shared guest contract on the Dynamic Worker, including caught call, typed inline describe recovery, discovery, utility, batch, and budget failure codes; plus the real authority boundary — local `data:` fetch, denied egress, unresolved DNS, empty environment paths, unavailable filesystem/HTTP builtins, and present runtime globals |
241
241
  | `linear-provider.test.ts` / `linear-registry.test.ts` | the Linear proxy's construction, classification, and guide; then the same connector inside a real deployment |
242
- | `meta-tools.test.ts` | the registry-backed meta-tools: bounded discovery with page and address maxima, concise and full descriptions, compact and JSON schemas with numeric and string constraints, structured errors, `skills` and connector-guide selection including paragraph-aware summaries and configured-summary construction bounds, stored-credential drift, catalog-lookup health accounting, `fields` selection including total and partial misses below nested arrays, truncation and `get_result` offset validation and character alignment, per-connector `maxResultBytes`, probe timeouts, and empty-query browse of an unavailable or unconfigured catalog |
242
+ | `meta-tools.test.ts` | the registry-backed meta-tools: bounded discovery with page and address maxima, compact descriptions and the complete on-demand usage skill, compact and JSON schemas with constraints, typed describe recovery and suggestions, structured errors, connector-guide selection and summary bounds, stored-credential drift, catalog health, field projection, truncation and `get_result`, per-connector result bounds, probe timeouts, and unavailable or unknown browse recovery |
243
243
  | `mixpanel-provider.test.ts` / `mixpanel-registry.test.ts` | the Mixpanel proxy, its conditional-input guide and complete reviewed schema-digest manifest, then the same connector inside a real deployment |
244
- | `notion-provider.test.ts` / `notion-registry.test.ts` | Notion's tool surface, request construction, lean projections, both pagination conventions, error mapping, and writes; then the connector in a real deployment |
244
+ | `notion-provider.test.ts` / `notion-registry.test.ts` | Notion's deliberate tool surface, including declined expanded page inputs, request construction, lean projections, both pagination conventions, error mapping, and writes; then the connector in a real deployment |
245
245
  | `operator-boundary.test.ts` | the operator row of the decisions table, after every mutation route: authentication material managed without moving a declared structure, and the one honest exception — a credential write making a remote catalog appear, which is discovery arriving, not an operator editing the deployment |
246
246
  | `operator-store.test.ts` | `src/operator-ui/app/store.ts` against a fake browser: the Clerk listener, `gate()`, the generation fence, and the request path |
247
247
  | `provider-conventions.test.ts` | the conventions a test can hold: hand-written providers refusing schemas they cannot enforce (H5), their compact discovery schemas staying complete (H7), Cloudflare stating its second pagination convention in the schema (H10), and Notion saying it has no escape hatch (H14) |
@@ -249,10 +249,11 @@ in.
249
249
  | `remote-mcp.test.ts` | `remoteMcp()` against an in-process server through the `_transportFactory` seam: passthrough, downstream `isError`, Workers-safe output-schema validation, request-scoped client reuse and at-most-once scope close; plus the real transport's manual redirect policy, destination guard, credential containment, and downstream session termination |
250
250
  | `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
251
251
  | `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
252
- | `server.test.ts` | end-to-end `/mcp` (401 initialize instructions seven tools usage skill `call_tool`), the open routes, Clerk `.well-known` metadata with no network, an end-to-end code-mode run, and `waitUntil` reaching agent catalog reads through both `search_tools` and `execute_code` |
252
+ | `revenuecat-provider.test.ts` / `revenuecat-registry.test.ts` | the RevenueCat proxy's per-project key scoping and account-wide OAuth guides, its purpose-bearing summary, the argued borderline verdicts in its digest-free manifest, and the deliberately unclassified `render-paywall-screenshot`; then two project-scoped keys as two connectors in a real deployment |
253
+ | `server.test.ts` | end-to-end `/mcp` (401 → compact initialize instructions → seven compact definitions with bounded connector inventory → complete usage skill → `call_tool`), conditional guide pointers, open routes, Clerk `.well-known` metadata without network, code mode, and deferred catalog reads through both discovery surfaces |
253
254
  | `server-route-contracts.test.ts` | the route contracts `server.ts` must keep byte-identical: every built-in answered ahead of connector routes inside the security wrapper, open data-free shells with framing denied, per-route auth and same-origin requirements with exact 401/403/405 bodies, and OAuth `verifyState`-before-`finishAuth` ordering |
254
255
  | `startup-warnings.test.ts` | every construction-time `logger.warn` and, as importantly, the conditions that must *not* trigger one: open mode with a credential or OAuth connector, `publicUrl` unset beside OAuth, dropped branding and `uiAuth` URLs, a missing `verifyState`, a credential test-hook mismatch, and an unusable `calls.maxResultBytes` |
255
- | `stripe-provider.test.ts` / `stripe-registry.test.ts` | the Stripe proxy's endpoint modes, admission, multi-account OAuth guidance, and no-guess account selection; then the connector in a real deployment |
256
+ | `stripe-provider.test.ts` / `stripe-registry.test.ts` | the Stripe proxy's mixed-mode OAuth and fixed-mode header contracts, admission, exact account selectors, and no-guess rule; then fixed credentials in a real deployment |
256
257
  | `ui.test.ts` | the server shell and `/ui/*` routes and the app's pure state rules from `view.ts` — filtering, page routing and capability states, credential management, gated `/ui/data` with broken-connector isolation and registry-owned catalog-observation containment, and the URL safety gates |
257
258
  | `validate.test.ts` | `validateToolInput()` — a returned (not thrown) `invalid_args` naming the path, `additionalProperties: false` enforcement, per-schema validator caching, and an unusable schema passed through with one warning |
258
259
 
@@ -263,12 +264,12 @@ justification for *not* re-running it in workerd, so "it was easier" is not one.
263
264
 
264
265
  | Suite | Covers | Why Node |
265
266
  | --- | --- | --- |
266
- | `deployment-shapes.test.ts` | the Worker as the only example with a loader-only sandbox, one Node template that is also its own container, the same source running locally and in the container, the full operator surface in both, a template that cannot start on its own `.env.example`, a Worker README naming every optional peer its entrypoint imports, and the initializer's `.gitignore` staying in step | walks the template and example trees with Node filesystem APIs |
267
+ | `deployment-shapes.test.ts` | the Worker as the only example with a loader-only sandbox, one Node template that is also its own container, the same source running locally and in the container, the Node template's pinned esbuild install-script approval, the full operator surface in both, a template that cannot start on its own `.env.example`, a Worker README naming every optional peer its entrypoint imports, and the initializer's `.gitignore` staying in step | walks the template and example trees with Node filesystem APIs |
267
268
  | `doc-links.test.ts` | the documentation checker itself — local file and fragment resolution, repository URLs resolved back to the checkout, duplicate heading slugs, fenced-code exclusion, and useful failures | spawns the Node checker against filesystem fixtures |
268
269
  | `doctor-cli.test.ts` | `connecta doctor`'s executor line end to end — the sandbox the deployment reports is the one named, an unidentifiable executor gets an executor-neutral line, and a hostile name is bounded and stripped before it reaches a terminal | spawns the CLI against a Node HTTP deployment over real sockets |
269
270
  | `drift-check.test.ts` | the maintainer drift checker — hosted-provider credential framing, recorded touched endpoints, a quiet revision bump, clear failures for an unavailable spec/manifest/credential, `$ref` traversal, and one well-formed row per endpoint | spawns the Node checker against filesystem fixtures |
270
271
  | `file-storage.test.ts` | `fileStorage()` across instances, logical TTL plus physical pruning without clobbering a newer value, and corrupt-file quarantine | exercises the Node filesystem storage adapter |
271
- | `guest-api-contract-quickjs.test.ts` | the shared guest-contract cases on the real QuickJS executor, including identical caught failure codes, its exact absent globals, and blocked runtime imports | runs the contract cases on the Node QuickJS executor |
272
+ | `guest-api-contract-quickjs.test.ts` | the shared guest-contract cases on the real QuickJS executor, including identical caught failure codes and inline describe recovery, its exact absent globals, and blocked runtime imports | runs the contract cases on the Node QuickJS executor |
272
273
  | `node.test.ts` | the `listen()` adapter propagating an HTTP client disconnect through the Web `Request` and the MCP handler into a program's connector call, releasing both admission permits | exercises the Node HTTP adapter over real TCP sockets |
273
274
  | `packed-links.test.ts` | the packed-link gate itself — shipped targets and repository URLs accepted, relative links into unshipped paths and directories rejected with the citation to write instead, reference definitions seen, fenced examples ignored, the changelog exempt | spawns the Node packed-link gate against filesystem fixtures |
274
275
  | `package-surface.test.ts` | the published boundary — built output shipped, the `exports` map carrying exactly the documented subpaths plus `./package.json`, only generic factories, platform storage kept in examples, Clerk and QuickJS behind optional subpaths, every provider independently importable, and the Cloudflare provider free of bare specifiers | walks the package tree with Node filesystem APIs |
@@ -1,7 +1,7 @@
1
1
  # Provider audit
2
2
 
3
3
  [`provider-conventions.md`](./provider-conventions.md) wrote the bar down. This
4
- document runs it against the five maintained prebuilt connections and returns a
4
+ document runs it against the six maintained prebuilt connections and returns a
5
5
  verdict for every applicable convention: **meets**, **misses** (with the fix),
6
6
  or **n/a** (with the reason). A convention is never quietly skipped, and an
7
7
  accepted miss is recorded as a provider-specific exception with its argument
@@ -81,6 +81,14 @@ deliberate surface.
81
81
  | H13 guide | meets | structured, declared summary, `required: true` with a stated reason — the database→data-source lookup is a sequence no complete schema can express |
82
82
  | H14 hatch | **missed → fixed** | Notion has no guarded raw-REST tool, which H14 explicitly permits for a finite surface — provided it says so. It did not. The guide now names the absence, so an agent does not spend a search proving there is no `notion_api_get` |
83
83
 
84
+ The 0.17.0 drift review also considered Notion's expanded create and update
85
+ contracts. Workspace-private creation, templates, placement, richer media,
86
+ locking, and irreversible content erasure stay outside the maintained surface.
87
+ They are separate ownership, ordering, asynchronous, coordination, file, or
88
+ deletion workflows rather than missing fields on the five existing writes
89
+ ([#408](https://github.com/zackbart/connecta/issues/408),
90
+ [#409](https://github.com/zackbart/connecta/issues/409)).
91
+
84
92
  ## Linear — hosted-MCP proxy
85
93
 
86
94
  | Convention | Verdict | Notes |
@@ -105,16 +113,16 @@ deliberate surface.
105
113
  | --- | --- | --- |
106
114
  | P1 add, never rewrite | meets | annotations only |
107
115
  | P2 identity | meets | required `purpose`, `instructions` appended, classification untouchable from there; purpose states deployment routing intent and the guide says it is not proof of authenticated account identity |
108
- | P3 routing fact | meets | production versus sandbox appears in the title, the description, and the guide's first line |
109
- | P4 endpoint default | meets | exemplary, and the model for the second clause: one published endpoint, `mode` required with no default, and construction throws when a recognizable key prefix contradicts the declared mode without reading anything it cannot classify |
116
+ | P3 routing fact | meets | OAuth metadata states mixed account scope and the guide resolves mode from `livemode`; fixed header credentials retain their mode in every routing surface |
117
+ | P4 endpoint default | meets | OAuth has no connector-wide mode to default; static headers require one, and construction throws when a recognizable key prefix contradicts it |
110
118
  | P5 classification | meets | including the two verdicts that needed an argument — `stripe_api_read` is a read because the tool is the boundary, `create_refund` is destructive despite its name |
111
119
  | P6 catalog varies | **missed → fixed** | the doc already knew this (`get_balance_summary` is Treasury and gated; a `create_customer` example survives in Stripe's prose but not its tool table), but the *guide* did not say it, and the guide is what reaches the agent. Added |
112
- | P7 reduction advice | **missed → fixed** | bare string; the derived summary was "Mode: production. Account purpose: …", spending the 120-character budget on the operator's prose. Now a declared, mode-shaped summary. `required` unset: the four generic tools are the routing decision and the mode warning already rides the title and description |
113
- | P8 identity resolution | **missed → fixed** | Stripe's writes take ids and the guide never said where they come from. Added: the typed prefixes (`cus_`, `sub_`, `ch_`, `pi_`, `in_`, `acct_`), the rule that a plausible-looking one belongs to a different object or to nobody, and the read tools that produce a real one. The guide now also accounts for OAuth sessions tied to several organization accounts: connector metadata is not identity proof, the exact selector must come from the live tool schema, and an ambiguous target or selection mechanism stops rather than becoming a guessed argument or header ([#404](https://github.com/zackbart/connecta/issues/404)) |
120
+ | P7 reduction advice | **missed → fixed** | OAuth has a mixed-scope summary; fixed credentials keep mode-shaped summaries. `required` stays unset because the four generic tools remain the routing decision |
121
+ | P8 identity resolution | **missed → fixed** | The guide names typed object ids and their read sources. For OAuth it requires `list_available_accounts_or_orgs`, then carries the returned `stripe_context` and `livemode` unchanged; ambiguity stops ([#404](https://github.com/zackbart/connecta/issues/404), [#414](https://github.com/zackbart/connecta/issues/414)) |
114
122
  | P9 authentication | meets | OAuth default, `requireHttps`, restricted key documented as a secret and paired with the narrowest scope. The guide distinguishes organization accounts within an OAuth session from Connect connected accounts, whose calls reject OAuth and use a deployment-configured restricted key plus `Stripe-Account`. The `auth_required` → `authorize_connector` route was added alongside P8, since a proxy's only recovery instruction lives there |
115
123
  | P10 no credential test | meets | no credential slot; the mode/key contradiction throws at construction instead, which is where P10 says the H12 guarantee gets paid |
116
124
  | P11 transport vs tool error | meets | inherited from `remoteMcp()`; the guide now also says that a rejected argument or plan restriction arrives in Stripe's own words and is not an authorization problem |
117
- | P12 admission budget | meets | a citable documented number (100/s live, 25/s sandbox), transcribed per mode, with `maxConcurrency` labeled as Connecta's own conservative choice |
125
+ | P12 admission budget | meets | fixed credentials use their documented mode rate; mixed OAuth uses the stricter 25/s sandbox rate and concurrency bound |
118
126
  | P13 drift visible | meets | both lists are module-level constants in one file, and are the manifest the refresh-time drift check compares against ([#343](https://github.com/zackbart/connecta/issues/343)) |
119
127
 
120
128
  ## Mixpanel — hosted-MCP proxy
@@ -139,6 +147,28 @@ Linear's reasoning.
139
147
  | P12 admission budget | **missed → fixed** | the connection hardcoded a 600-call hourly budget transcribed from a limit Mixpanel meters **per user**. P12 names this case exactly: a per-runtime counter cannot approximate a per-user quota in either direction — one runtime serving several users under-counts, several isolates sharing one credential each admit a full budget. The default is removed; `callAdmission` is now an operator option with a documented example, matching Linear |
140
148
  | P13 drift visible | meets | both lists are module-level constants in one file, and are the manifest the refresh-time drift check compares against ([#343](https://github.com/zackbart/connecta/issues/343)) |
141
149
 
150
+ ## RevenueCat — hosted-MCP proxy
151
+
152
+ Written after the conventions existed, so it has no misses to record — only two
153
+ places where the honest answer departs from the obvious one, both argued below.
154
+ Ninety-five documented tools, ninety-four classified, one deliberately not.
155
+
156
+ | Convention | Verdict | Notes |
157
+ | --- | --- | --- |
158
+ | P1 add, never rewrite | meets | `listTools` maps annotations and returns every other field untouched |
159
+ | P2 identity | meets | required `purpose` (blank throws), `instructions` appended under `## Project instructions`, and appended text cannot reach the classification |
160
+ | P3 routing fact | meets, with the fact split in two | the routing fact is scope, and it has two halves. The *shape* — one project versus every project the account can reach — is knowable at construction and rides the default title (`RevenueCat (single project)` versus `RevenueCat`). *Which* project a key opens is not knowable without calling something, which P10 forbids, so it rides the guide's first line and the declared summary, built from the operator's `purpose`. That makes this the one maintained proxy with a purpose-bearing summary rather than a static one, and the reason is P3's own cost: two `sk_` connectors share a title, an endpoint, and a catalog, so a static summary would leave them indistinguishable in the only field search returns |
161
+ | P4 endpoint default | n/a — one endpoint, and the scope rides the credential | RevenueCat publishes a single MCP endpoint, so there is nothing to select between. The scope difference comes from the credential shape itself, which the constructor reads rather than asks for: `auth.type === "headers"` *is* the single-project declaration. There is no mode to default and no mode to contradict, so the P4 machinery Stripe needs has nothing to do here |
162
+ | P5 classification | meets | 50 reads, 15 additive writes, 29 destructive writes named; `render-paywall-screenshot` is on neither list because RevenueCat's reference gives it no access column, and it fails closed. Nine borderline verdicts are argued beside the rows they decide, and asserted in the suite so a silent flip fails |
163
+ | P6 catalog varies | meets | the guide names paywall AI editing, benchmarks, experiments, virtual currencies, and account billing as the plan-, platform-, and beta-gated areas where absence is expected, and separately names the unclassified tool so its approval prompt does not read as a bug |
164
+ | P7 reduction advice | meets | structured guide, declared summary, cursor-then-reduce advice aimed at the two objects that are actually large here (customers and their event history). `required` stays unset: the project-resolution sequence is worth reading before a run, not before every call |
165
+ | P8 identity resolution | meets | the guide names the whole chain — `list-projects` for the `project_id` every project-scoped call takes, then `list-apps`, `list-products`, `list-entitlements`, `list-offerings`, `list-paywalls`, `list-audiences`, and `list-customers` for the ids their `get-`, `update-`, `archive-`, and `delete-` counterparts expect — and says a plausible-looking id belongs to another project or to nobody. For OAuth it also says to stop and ask when more than one project fits |
166
+ | P9 authentication | meets | OAuth default, `requireHttps`, the API v2 secret key documented as a secret and paired with the narrowest scope RevenueCat offers (one project). The guide names the `auth_required` → `authorize_connector` route, and separately says that a read-only key's refusal is RevenueCat's own words rather than an authorization gap connecta can repair |
167
+ | P10 no credential test | meets | no `credential`, `testCredential`, or `testCredentials`. This is also where the constructor's most tempting option was refused: a `project?: string` checked against `list-projects` at construction is a credential test wearing a configuration hat, so the operator's stated purpose carries the claim and the agent confirms it on first use. There is no recognizable-credential contradiction to throw on either — an `sk_` key encodes no project — so the construction-time half of P10 has nothing to check here, exactly as it has nothing to check for Mixpanel's region |
168
+ | P11 transport vs tool error | meets | inherited whole from `remoteMcp()`; the wrapper adds no error handling and reads no downstream prose. The guide says a rejected argument, a permission gap, and a plan restriction all arrive in RevenueCat's own words |
169
+ | P12 admission budget | meets, by declining a number that exists | RevenueCat does publish limits, which is why this row needed an argument rather than a shrug. It meters per domain — 480/min for customer information, virtual currencies, and refunds; 60 for project configuration and audiences; 25 for charts and metrics — and a `ConnectorCallAdmissionPolicy` carries exactly one rule. Picking 25 throttles a customer read loop to a nineteenth of its allowance; picking 480 leaves a chart sweep unprotected; neither is the provider's limit. The metering scope repeats the point: developer-level keys are metered per developer, which a per-runtime counter cannot approximate. So the guide states RevenueCat's own numbers and the `429` / `Retry-After` / `backoff_ms` signals, and `callAdmission` stays an operator option with a documented example |
170
+ | P13 drift visible | meets | both lists are module-level constants in one file and *are* the manifest the wrapper classifies from, compared against the live catalog on every refresh ([#343](https://github.com/zackbart/connecta/issues/343)). The maintainer-run check accepts `revenuecat` with `CONNECTA_DRIFT_REVENUECAT_KEY`. No schema digests are recorded, and the manifest says so rather than shipping invented ones |
171
+
142
172
  ## Scoreboard
143
173
 
144
174
  | Provider | Meets | Missed and fixed | Recorded exception | Open |
@@ -148,8 +178,9 @@ Linear's reasoning.
148
178
  | Linear | 11 | 2 | P4 departs from the letter | — |
149
179
  | Stripe | 10 | 3 | — | — |
150
180
  | Mixpanel | 7 | 5 | P10 half n/a | — |
181
+ | RevenueCat | 12 | 0 | P4 n/a (one endpoint); P3 met with a purpose-bearing summary | — |
151
182
 
152
- Nineteen misses, nineteen fixes, four recorded exceptions, one judgment left to
183
+ Nineteen misses, nineteen fixes, six recorded exceptions, one judgment left to
153
184
  the issue that owns it. The pattern in the misses is worth naming: sixteen of
154
185
  the nineteen are a guide, a title, or a schema description failing to *say*
155
186
  something the implementation already did correctly. Only three changed what a
@@ -158,3 +189,10 @@ access declaration, Mixpanel dropping a budget it could not honestly compute.
158
189
  The conventions are mostly not asking for different behavior. They are asking
159
190
  for the behavior to reach the agent, which is a different problem and, on this
160
191
  evidence, the one the providers were losing.
192
+
193
+ RevenueCat is the first connection written *after* the conventions and adds no
194
+ misses to those nineteen, which is the least interesting thing about its row.
195
+ The interesting part is that two conventions came out somewhere other than
196
+ their obvious reading — P4 has no endpoint to select and P12 declines a number
197
+ the provider actually publishes — and both had to be argued rather than
198
+ skipped. A convention that only ever returns "meets" is not being applied.
@@ -1,6 +1,6 @@
1
1
  # Provider conventions
2
2
 
3
- The five maintained prebuilt connections grew one at a time, and until now
3
+ The six maintained prebuilt connections grew one at a time, and until now
4
4
  "excellent provider" meant whatever the last author thought. This document
5
5
  writes the judgment down so it can be argued with, audited, and reused.
6
6
 
@@ -11,7 +11,7 @@ cannot honestly cover both:
11
11
  tool name, schema, projection, and error. Today: Cloudflare, Notion.
12
12
  - **Hosted-MCP proxies** — `remoteMcp()` wrappers around a server somebody else
13
13
  operates, where the names, schemas, results, and error prose arrive as they
14
- are. Today: Linear, Stripe, Mixpanel.
14
+ are. Today: Linear, Stripe, Mixpanel, RevenueCat.
15
15
 
16
16
  The governing principle for every convention below is the same: **keep the
17
17
  model that interacts with connecta as efficient as possible.** A convention
@@ -386,11 +386,18 @@ complete spends calls proving it is not. *Cost:* wrong-tool selection.
386
386
 
387
387
  A proxy cannot project a downstream result, so the guide tells the agent to
388
388
  page with the cursor rather than raising the page size, and to reduce inside
389
- `execute_code` before returning anything. Structured form, explicit `summary`,
390
- `required: true` only for a genuine cross-tool sequence or a generic wrapper.
391
-
392
- *Why:* the only projection available is the one the program writes. *Cost:*
393
- result size.
389
+ `execute_code` before returning anything and, where a value's rendering is
390
+ the provider's rule rather than the schema's, what that value means: Mixpanel
391
+ renders an absent boolean property as `false` in a breakdown, so the guide
392
+ says to confirm presence before reading `false` as a signal
393
+ ([#430](https://github.com/zackbart/connecta/issues/430)). Structured form,
394
+ explicit `summary`, `required: true` only for a genuine cross-tool sequence or
395
+ a generic wrapper.
396
+
397
+ *Why:* the only projection available is the one the program writes, and a
398
+ value the schema types correctly can still mislead without the provider's
399
+ rendering rule beside it — the agent then re-queries to explain a signal that
400
+ was never there. *Cost:* result size.
394
401
 
395
402
  ### P8 — Identity resolution comes before action
396
403
 
@@ -494,8 +501,8 @@ is the shape that does not become it.
494
501
  **What a manifest holds.** Every tool name a release reviewed, the verdict it
495
502
  reviewed it as (`read-only`, `additive`, `destructive`), and — where a release
496
503
  actually read them — a digest of that tool's input and output schemas. Today
497
- the three proxies ship names and verdicts and no digests, because no release
498
- has read a live schema and written it down, and an invented digest reports a
504
+ three of the four proxies ship names and verdicts and no digests, because no
505
+ release has read a live schema and written it down, and an invented digest reports a
499
506
  change that never happened. `npm run drift:check -- --record` reads them from a
500
507
  live catalog and prints the block a release pastes in; until a release does,
501
508
  a manifest without digests counts no schema changes, which is the honest answer
@@ -567,9 +574,10 @@ compares its own totals against `detectCatalogDrift()`: two readings of one
567
574
  manifest that disagree mean one of them is lying, which is worth failing over.
568
575
  One credential per provider comes from the environment —
569
576
  `CONNECTA_DRIFT_LINEAR_KEY`, `CONNECTA_DRIFT_STRIPE_KEY`,
570
- `CONNECTA_DRIFT_MIXPANEL_KEY` — and a missing or dead one stops the run with a
571
- message naming it rather than reporting an empty catalog as mass removal.
572
- Linear and bare Stripe values use their documented bearer or Basic framing.
577
+ `CONNECTA_DRIFT_MIXPANEL_KEY`, `CONNECTA_DRIFT_REVENUECAT_KEY` — and a missing
578
+ or dead one stops the run with a message naming it rather than reporting an
579
+ empty catalog as mass removal. Linear, bare Stripe, and RevenueCat `sk_` values
580
+ use their documented bearer or Basic framing.
573
581
  Mixpanel's beta service-account form is provider-specific:
574
582
  `user:secret` becomes `Bearer Basic <base64(user:secret)>`, exactly as its MCP
575
583
  documentation requires. A value that already includes whitespace is treated
@@ -623,11 +631,11 @@ evidence and nothing else: no tool is generated from one, which is the
623
631
  ## What the audit checks
624
632
 
625
633
  The provider audit ([#342](https://github.com/zackbart/connecta/issues/342))
626
- runs this document against each of the five providers and returns a verdict per
634
+ runs this document against each of the six providers and returns a verdict per
627
635
  convention: **meets**, **misses** (with the fix), or **not applicable** (with
628
636
  the reason). A convention is never quietly skipped, and an accepted miss is
629
637
  recorded as a provider-specific exception with its argument, not left blank.
630
- Its five reports live in [provider-audit.md](./provider-audit.md), and the
638
+ Its six reports live in [provider-audit.md](./provider-audit.md), and the
631
639
  mechanically checkable half of the hand-written bar runs on every test run in
632
640
  [`test/provider-conventions.test.ts`](https://github.com/zackbart/connecta/blob/main/test/provider-conventions.test.ts) —
633
641
  so a convention that was met once stays met, or fails loudly.
@@ -675,6 +683,7 @@ their place, so they are this audit's work, not a second removal argument.
675
683
 
676
684
  Each provider's own guide ([Cloudflare](./cloudflare.md),
677
685
  [Linear](./linear.md), [Mixpanel](./mixpanel.md), [Notion](./notion.md),
678
- [Stripe](./stripe.md)) is part of the audited surface: documentation moves with
686
+ [RevenueCat](./revenuecat.md), [Stripe](./stripe.md)) is part of the audited
687
+ surface: documentation moves with
679
688
  the work, and a guide describing a surface that shipped differently is itself a
680
689
  miss.
@@ -0,0 +1,279 @@
1
+ # RevenueCat prebuilt connection
2
+
3
+ Import `revenuecat()` independently from
4
+ `@zackbart/connecta/providers/revenuecat`. It wraps
5
+ [RevenueCat's hosted MCP server](https://www.revenuecat.com/docs/tools/mcp/setup)
6
+ with OAuth by default, project-scoping guidance that differs by credential
7
+ shape, a task-oriented usage guide, and a vetted safety classification. It adds
8
+ no provider dependency and is not reachable from Connecta's root entry.
9
+
10
+ ```ts
11
+ import { revenuecat } from "@zackbart/connecta/providers/revenuecat";
12
+
13
+ const subscriptions = revenuecat("revenuecat", {
14
+ purpose: "Subscription state, entitlements, and revenue across our projects",
15
+ instructions: "Never grant a promotional entitlement without a support ticket.",
16
+ });
17
+ ```
18
+
19
+ The endpoint is `https://mcp.revenuecat.ai/mcp` over streamable HTTP.
20
+
21
+ `purpose` is required, and it does more work here than in any other maintained
22
+ connection. RevenueCat's own tools do not report which project a static key
23
+ reaches until you call one, and Connecta runs no credential test at construction
24
+ (P10), so `purpose` is the only place the deployment's intent is written down.
25
+ It opens the guide and it *is* the guide summary, which is the field search
26
+ returns. Project `instructions` are appended to the maintained guide and cannot
27
+ change the connector's safety classification.
28
+
29
+ ## The scoping fact this connection exists to get right
30
+
31
+ RevenueCat has two credential shapes with two different scopes, and the guide
32
+ you get depends on which one you configured.
33
+
34
+ **A secret API key is project-wide.** RevenueCat's own words:
35
+ "Secret API keys are project-wide and can be created and revoked by project
36
+ Admins" ([authentication](https://www.revenuecat.com/docs/projects/authentication)).
37
+ `list-projects` "lists all RevenueCat projects accessible with the provided API
38
+ key" — with an `sk_` key that is exactly one project. So a `headers`-auth
39
+ connector reaches one project and nothing outside it. Its title is
40
+ `RevenueCat (single project)` and its guide opens by naming the project the
41
+ operator said the key is for.
42
+
43
+ **OAuth is account-scoped.** One session reaches every project the account can
44
+ see, and each project-scoped tool takes a `project_id`. Its title is
45
+ `RevenueCat` and its guide opens with the resolution discipline: call
46
+ `list-projects` first, carry the exact `project_id` it returned into every
47
+ project-scoped call, and stop and ask when more than one project fits.
48
+ Connecta does not pick a project, and the connector id, title, and purpose are
49
+ routing hints rather than proof of where a call will land.
50
+
51
+ The constructor deliberately has no `project` option. Declaring a project that
52
+ Connecta then checked against `list-projects` at construction would be a
53
+ credential test, which P10 forbids — a proxy makes no unasked-for downstream
54
+ call. The operator's stated purpose carries the claim; the agent confirms it
55
+ with `list-projects` on first use.
56
+
57
+ ## Several projects
58
+
59
+ One key, one project, one connector. A deployment that needs two projects
60
+ declares two connectors, each with its own key and its own id:
61
+
62
+ ```ts
63
+ import { revenuecat } from "@zackbart/connecta/providers/revenuecat";
64
+
65
+ connectors: [
66
+ revenuecat("bepresent_ios", {
67
+ purpose: "Subscription state for the BePresent iOS project",
68
+ auth: {
69
+ type: "headers",
70
+ headers: { Authorization: `Bearer ${env.REVENUECAT_BEPRESENT_KEY}` },
71
+ },
72
+ }),
73
+ revenuecat("biblescroll", {
74
+ purpose: "Subscription state for the BibleScroll project",
75
+ auth: {
76
+ type: "headers",
77
+ headers: { Authorization: `Bearer ${env.REVENUECAT_BIBLESCROLL_KEY}` },
78
+ },
79
+ }),
80
+ ]
81
+ ```
82
+
83
+ That is config-as-code doing what an account model would otherwise do: one
84
+ credential per connector, each with its own catalog, storage namespace, health,
85
+ and admission counters. The two share a title, because Connecta cannot know
86
+ which project a key opens — so the guide summary is what tells them apart, and
87
+ it is built from `purpose`. Write a purpose that names the project, not one
88
+ that names RevenueCat.
89
+
90
+ If the deployment genuinely needs to move between projects in one session, use
91
+ OAuth instead and let the agent resolve `project_id`. Do not point a
92
+ project-scoped key's `project_id` argument at a project it cannot reach; the
93
+ call fails at RevenueCat, which is the correct outcome but a wasted round trip.
94
+
95
+ ## Authentication
96
+
97
+ OAuth is the default and the option RevenueCat recommends: "OAuth provides a
98
+ seamless authentication experience: log in to your RevenueCat account and grant
99
+ access to the MCP server, with no API keys to manage." Each connector instance
100
+ keeps its own flow and tokens in connector-scoped storage.
101
+
102
+ RevenueCat also accepts an API v2 secret key as a bearer token for headless
103
+ agents:
104
+
105
+ ```ts
106
+ revenuecat("bepresent_ios", {
107
+ purpose: "Subscription state for the BePresent iOS project",
108
+ auth: {
109
+ type: "headers",
110
+ headers: { Authorization: `Bearer ${env.REVENUECAT_KEY}` },
111
+ },
112
+ });
113
+ ```
114
+
115
+ Keys are prefixed `sk_`, are issued read-only or write-enabled, and can be
116
+ revoked at any time by a project Admin. RevenueCat's setup guidance is to "use
117
+ a write-enabled key if you plan to create/modify resources"; "a read-only key
118
+ works if you only need to view data". Keep the key in the runtime's secret
119
+ store, never in the deployment file.
120
+
121
+ **Connecta does not filter writes for a read-only key.** It has no way to tell
122
+ which kind a key is without spending a call, so every write in the catalog is
123
+ offered, reaches RevenueCat, and fails there in RevenueCat's own words. The
124
+ guide says so, so an agent reads that refusal as "this key cannot write" rather
125
+ than as a bad argument and repairs it by routing to a write-enabled connector
126
+ instead of retrying.
127
+
128
+ An expired or revoked credential surfaces as `auth_required`, and the guide
129
+ names the `authorize_connector` recovery. A permission gap, a plan restriction,
130
+ or a rejected argument arrives as RevenueCat wrote it and is not an
131
+ authorization problem.
132
+
133
+ ## The ninety-five tools, and what they are classified as
134
+
135
+ RevenueCat's
136
+ [tool reference](https://www.revenuecat.com/docs/tools/mcp/tools-reference),
137
+ read on **2026-08-18**, documents ninety-five tools in a Read/Write table.
138
+ Ninety-four carry an access column and are classified here: **50 read-only, 15
139
+ additive writes, 29 destructive writes.**
140
+
141
+ Reads are every `Read` row, verbatim — the nine project and app reads, the four
142
+ product reads, the entitlement, offering, targeting, paywall, customer, virtual
143
+ currency, chart, webhook, and SDK reads, and `get-paywall-ai-task`.
144
+
145
+ Writes follow the verb where the verb is honest: `archive-*` and `unarchive-*`
146
+ flip an existing object's active state, `update-*`, `delete-*`, `publish-*`,
147
+ `unpublish-*`, and `detach-*` change or remove something that already exists,
148
+ and a plain `create-*` brings a new object into being beside the old ones.
149
+ `set-product-store-state` is an upsert and `submit-products-to-store` sends
150
+ products to Apple for review, so both are destructive.
151
+ `assign-customer-offering` and `grant-customer-entitlement` change a real
152
+ customer's access, so both are destructive too.
153
+
154
+ Nine verdicts are not decided by the verb, and each is argued in the source
155
+ beside the row:
156
+
157
+ | Tool | Verdict | Why |
158
+ | --- | --- | --- |
159
+ | `create-product-prices` | destructive | named `create-`, described "Configure prices for a product". The price set already exists and configuring it replaces what is there. Money-facing and overwriting |
160
+ | `equalize-subscription-prices` | additive | "Fills **missing** App Store subscription territory prices" — by RevenueCat's own word it writes only where nothing is set |
161
+ | `validate-app-credentials` | additive | RevenueCat files it `Write`, so it does not reach the read path, but it leaves the saved credentials alone and only records the outcome of a check |
162
+ | `upload-product-store-state-screenshot` | additive | "Reserves an App Store Connect review screenshot slot" — a new slot appears; nothing existing is replaced |
163
+ | `attach-products-to-entitlement` | additive | attach adds membership and removes nothing; `detach-products-from-entitlement` is the destructive half. Filing both destructive would make the pair read identically in the approval copy a human sees |
164
+ | `attach-products-to-package` | additive | the same argument one level down |
165
+ | `duplicate-paywall` | additive | "Duplicates an existing paywall's current draft" — the original is untouched |
166
+ | `create-paywall-ai` | additive | starts an async task that creates a paywall; every existing one is left alone |
167
+ | `edit-paywall-ai` | destructive | starts an async task that rewrites a draft that already exists |
168
+
169
+ `create-webhook-integration` deserves a sentence too. No existing integration
170
+ changes, so the verb reads additive — but with filters omitted the new one
171
+ "starts delivering" every customer event in the project to a URL the caller
172
+ typed. Customer data leaving the account is the `create_refund` argument again:
173
+ filed destructive on consequence, so the approval copy says what is at stake.
174
+
175
+ **`render-paywall-screenshot` is deliberately unclassified.** RevenueCat's
176
+ reference gives it no access column at all, so no release has reviewed what it
177
+ does. It fails closed onto `call_destructive_tool` rather than being guessed
178
+ into the read path because its name sounds harmless (P5). The guide names it,
179
+ so an agent does not read the approval prompt as a bug.
180
+
181
+ That classification fills in downstream silence and otherwise preserves explicit
182
+ annotations. A tool on the read allowlist arriving with `destructiveHint: true`
183
+ or `readOnlyHint: false` keeps exactly what the downstream said and stays behind
184
+ `call_destructive_tool`. A tool on neither maintained list arriving with
185
+ `readOnlyHint: true` keeps that too. Both are the downstream telling you this
186
+ release's allowlist is stale. The one fail-closed exception applies to a name
187
+ this release reviewed and filed destructive: a `grant-customer-entitlement`
188
+ claiming `readOnlyHint: true` is a downstream bug rather than news, and stays on
189
+ the approval path.
190
+
191
+ The tool list is not a fixed set, and the guide says so. RevenueCat gates parts
192
+ of its catalog by plan, platform, and beta enrollment — paywall AI editing,
193
+ benchmarks, experiments, virtual currencies, and the account-billing tools are
194
+ the usual absentees — so search this connector for what it actually exposes
195
+ rather than assuming a documented tool is here.
196
+
197
+ **No schema digests are recorded.** No release has read RevenueCat's live
198
+ schemas and written them down; that needs a live project and a maintainer's own
199
+ key. The manifest therefore ships names and verdicts only, and the drift check
200
+ honestly counts zero schema changes rather than reporting an invented one.
201
+ `npm run drift:check -- --record` reads them from a live catalog and prints the
202
+ block a release pastes in
203
+ ([#351](https://github.com/zackbart/connecta/issues/351)).
204
+
205
+ ## Rate limits
206
+
207
+ RevenueCat documents numbers, and this connection still declares no budget.
208
+
209
+ API v2 meters per minute and **per domain**
210
+ ([rate limits](https://www.revenuecat.com/docs/api-v2#tag/Rate-Limit), read
211
+ 2026-08-18):
212
+
213
+ | Domain | Requests per minute |
214
+ | --- | --- |
215
+ | Customer Information | 480 |
216
+ | Virtual Currencies | 480 |
217
+ | Subscription Transactions Refunds | 480 |
218
+ | Audiences | 60 |
219
+ | Project Configuration | 60 |
220
+ | Charts & Metrics | 25 |
221
+
222
+ A `ConnectorCallAdmissionPolicy` carries exactly one rule, so a connector-wide
223
+ budget has to pick one of those six numbers for all ninety-five tools.
224
+ Transcribing 25 would throttle a customer read loop to a nineteenth of its
225
+ documented allowance; transcribing 480 would leave a chart sweep unprotected.
226
+ Neither is the provider's limit, and both would look like RevenueCat being
227
+ flaky. The metering scope says the same thing again: the limit applies per API
228
+ key for app-level keys and **per developer** for developer-level keys, so an
229
+ OAuth session shares one budget with everything else that developer does, which
230
+ a per-runtime counter cannot approximate in either direction.
231
+
232
+ So the number stays with the operator who knows the account (P12), and the
233
+ guide states RevenueCat's own limits instead, along with the `429`,
234
+ `Retry-After`, and `backoff_ms` signals to back off on. Supply one like this:
235
+
236
+ ```ts
237
+ revenuecat("revenuecat", {
238
+ purpose: "Revenue charts and cohort reporting",
239
+ callAdmission: {
240
+ rules: [
241
+ {
242
+ maxConcurrency: 4,
243
+ queueTimeoutMs: 5_000,
244
+ retryAfterMs: 2_000,
245
+ // The Charts & Metrics ceiling, because this connector is used for
246
+ // charts. A customer-lookup connector would declare 480.
247
+ budget: { kind: "rolling-window", maxCalls: 25, windowMs: 60_000 },
248
+ },
249
+ ],
250
+ },
251
+ });
252
+ ```
253
+
254
+ As with every connector policy this is a **best-effort approximation** of the
255
+ provider's limit, not an enforcement of it. Each runtime keeps its own counter,
256
+ so N Worker isolates or Node processes serving one deployment can each admit up
257
+ to the stated rate. Discovery traffic is outside connector call admission and
258
+ still needs restrained use.
259
+
260
+ ## What is not verified
261
+
262
+ - **Every tool name is transcribed from RevenueCat's published reference on
263
+ 2026-08-18, not read from a live catalog.** No maintainer ran this against a
264
+ project with a real key before it shipped. A name the reference lists and the
265
+ server does not serve costs nothing; a tool the server serves and the
266
+ reference omits fails closed. The maintainer-run drift check with
267
+ `CONNECTA_DRIFT_REVENUECAT_KEY` set is what turns that into a finding with a
268
+ name attached.
269
+ - **No input or output schema has been read**, which is why the manifest
270
+ carries no digests.
271
+ - **Whether `render-paywall-screenshot` mutates anything.** It has no access
272
+ column, and guessing is exactly what P5 exists to prevent.
273
+
274
+ ## Conventions
275
+
276
+ This connection is audited against
277
+ [the provider conventions](./provider-conventions.md). Its verdict per
278
+ convention is the RevenueCat section of
279
+ [the provider audit](./provider-audit.md).