@zackbart/connecta 0.18.0 → 0.18.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.
@@ -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
 
@@ -405,33 +412,49 @@ retries.
405
412
 
406
413
  OAuth per connector instance, stored in connector-scoped storage, is the
407
414
  default. The provider's own headless credential — a personal API key, a
408
- restricted key, a service account — is supported through explicit `headers`
409
- auth, documented as a secret rather than configuration, and paired with the
410
- narrowest mode the deployment can use. `requireHttps` is set. Recovery from an
411
- expired authorization is the ordinary `auth_required` → `authorize_connector`
412
- route.
415
+ restricted key, a service account — is supported two ways: explicit `headers`
416
+ auth, documented as a secret rather than configuration, and `{ type:
417
+ "credential" }`, which declares an operator slot and takes the same secret from
418
+ `/credentials` instead. Either way it is paired with the narrowest mode the
419
+ deployment can use, and the framing matches the provider's *published* contract
420
+ for the MCP endpoint — not a convention borrowed from that provider's other
421
+ APIs, and not this repository's earlier example, which is the same claim wearing
422
+ a circle. `requireHttps` is set. Recovery from an expired authorization is the
423
+ ordinary `auth_required` → `authorize_connector` route, which returns the
424
+ consent URL for OAuth and the `/credentials` handoff for a declared slot.
413
425
 
414
426
  *Why:* one route back from an expired credential is what keeps a failed call
415
427
  from becoming an abandoned task. *Cost:* wrong-tool selection.
416
428
 
417
- ### P10 — There is no credential test; the equivalent check happens at construction
429
+ ### P10 — Nothing probes a credential unasked; a declared slot may be tested on request
418
430
 
419
- A proxy declares no operator credential slot and implements neither
420
- `testCredential` nor `testCredentials`. `remoteMcp()` has no `credential`
421
- option, and neither shape of proxy credential is vault-managed: OAuth lives in
431
+ A proxy declares an operator credential slot exactly when its auth is `{ type:
432
+ "credential" }`, and then it inherits H12 whole
433
+ ([#439](https://github.com/zackbart/connecta/issues/439)). The other two shapes
434
+ declare no slot and hold nothing for the credentials page: OAuth lives in
422
435
  connector-scoped storage and is exercised by the authorization flow itself,
423
- while a headless key arrives as deployment configuration in `headers`, so there
424
- is nothing for the operator credentials page to hold or test. H12's guarantee is
425
- still owed, and a proxy pays it in two other places: construction throws when a
426
- recognizable credential contradicts the declared mode (P4), and a dead or
427
- revoked credential fails loudly at use as `auth_required` with the
428
- `authorize_connector` route attached (P9). Connecta never probes a downstream to
429
- see whether a credential is still alive — that shape is `removed` in the ethos
430
- ([#179](https://github.com/zackbart/connecta/issues/179)). A provider that later
431
- does take a vault-managed secret inherits H12 whole.
436
+ while a `headers` key arrives as deployment configuration. H12 is owed in every
437
+ shape, and a proxy pays it in two places that do not depend on a slot:
438
+ construction throws when a recognizable credential contradicts the declared mode
439
+ (P4) a check a vault-managed key cannot get, because there is nothing in the
440
+ deployment file to read and a dead, revoked, or absent credential fails loudly
441
+ at use as `auth_required` with the `authorize_connector` route attached (P9).
442
+
443
+ `testCredential` exists only behind the operator-pressed Test action on
444
+ `/credentials`, and only for a declared slot. It connects with the stored value
445
+ and reports how many tools the downstream served, which is the whole honest
446
+ check for a proxy: which account, project, or mode a key reaches is the
447
+ provider's answer, not Connecta's. That is not the shape
448
+ [#179](https://github.com/zackbart/connecta/issues/179) removed. What was
449
+ removed is the *unasked* probe — a liveness call every deployment pays on a
450
+ schedule or at startup to answer a question only a misconfigured one has. A
451
+ human clicking Test has asked, `api()` has had that button since the vault
452
+ existed, and nothing here probes on its own: no timer, no warmup, no check on
453
+ the read path.
432
454
 
433
455
  *Why:* an unasked-for liveness probe spends a call on every deployment to answer
434
- a question only a misconfigured one has. *Cost:* result size.
456
+ a question only a misconfigured one has; a requested one spends a call the
457
+ person requesting it chose. *Cost:* result size.
435
458
 
436
459
  ### P11 — Connecta classifies the transport; the downstream owns the tool error
437
460
 
@@ -494,8 +517,8 @@ is the shape that does not become it.
494
517
  **What a manifest holds.** Every tool name a release reviewed, the verdict it
495
518
  reviewed it as (`read-only`, `additive`, `destructive`), and — where a release
496
519
  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
520
+ three of the four proxies ship names and verdicts and no digests, because no
521
+ release has read a live schema and written it down, and an invented digest reports a
499
522
  change that never happened. `npm run drift:check -- --record` reads them from a
500
523
  live catalog and prints the block a release pastes in; until a release does,
501
524
  a manifest without digests counts no schema changes, which is the honest answer
@@ -567,9 +590,10 @@ compares its own totals against `detectCatalogDrift()`: two readings of one
567
590
  manifest that disagree mean one of them is lying, which is worth failing over.
568
591
  One credential per provider comes from the environment —
569
592
  `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.
593
+ `CONNECTA_DRIFT_MIXPANEL_KEY`, `CONNECTA_DRIFT_REVENUECAT_KEY` — and a missing
594
+ or dead one stops the run with a message naming it rather than reporting an
595
+ empty catalog as mass removal. Linear, bare Stripe, and RevenueCat `sk_` values
596
+ use their documented bearer or Basic framing.
573
597
  Mixpanel's beta service-account form is provider-specific:
574
598
  `user:secret` becomes `Bearer Basic <base64(user:secret)>`, exactly as its MCP
575
599
  documentation requires. A value that already includes whitespace is treated
@@ -623,11 +647,11 @@ evidence and nothing else: no tool is generated from one, which is the
623
647
  ## What the audit checks
624
648
 
625
649
  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
650
+ runs this document against each of the six providers and returns a verdict per
627
651
  convention: **meets**, **misses** (with the fix), or **not applicable** (with
628
652
  the reason). A convention is never quietly skipped, and an accepted miss is
629
653
  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
654
+ Its six reports live in [provider-audit.md](./provider-audit.md), and the
631
655
  mechanically checkable half of the hand-written bar runs on every test run in
632
656
  [`test/provider-conventions.test.ts`](https://github.com/zackbart/connecta/blob/main/test/provider-conventions.test.ts) —
633
657
  so a convention that was met once stays met, or fails loudly.
@@ -658,8 +682,8 @@ than by reading:
658
682
  | P4 | endpoint or mode option exists, with the documented default (or no default, where none is safe) |
659
683
  | P5 | reads and writes are named lists; an unlisted tool resolves to not-read-only; a reviewed destructive name beats a contradictory `readOnlyHint: true` |
660
684
  | P6, P8 | the guide contains the catalog-varies note and the id-resolution rule |
661
- | P9 | `auth` defaults to OAuth and `requireHttps` is set |
662
- | P10 | no `credential`, `testCredential`, or `testCredentials` on the wrapper; the mode/key contradiction throws at construction instead |
685
+ | P9 | `auth` defaults to OAuth and `requireHttps` is set; a credential-auth shape frames the key the way the provider's MCP documentation does |
686
+ | P10 | a `credential` slot exactly when auth is `{ type: "credential" }`; `testCredential` runs only from the operator's Test action, never on a timer or a read path; the mode/key contradiction still throws at construction |
663
687
  | P11 | an authorization failure surfaces as `auth_required`; a downstream tool error is returned unchanged, with no code chosen from its prose |
664
688
  | P12 | a declared budget matches a citable documented limit, or the absence is justified in the guide |
665
689
  | P13 | classification lists are maintained in one place per provider and built into the manifest the wrapper classifies from, so the drift check compares against the same fact the caller is served |
@@ -675,6 +699,7 @@ their place, so they are this audit's work, not a second removal argument.
675
699
 
676
700
  Each provider's own guide ([Cloudflare](./cloudflare.md),
677
701
  [Linear](./linear.md), [Mixpanel](./mixpanel.md), [Notion](./notion.md),
678
- [Stripe](./stripe.md)) is part of the audited surface: documentation moves with
702
+ [RevenueCat](./revenuecat.md), [Stripe](./stripe.md)) is part of the audited
703
+ surface: documentation moves with
679
704
  the work, and a guide describing a surface that shipped differently is itself a
680
705
  miss.
@@ -0,0 +1,301 @@
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
+ Neither key has to be a runtime secret. Declare the slot instead and each
84
+ connector's key is pasted, tested, and rotated on `/credentials`:
85
+
86
+ ```ts
87
+ connectors: [
88
+ revenuecat("bepresent_ios", {
89
+ purpose: "Subscription state for the BePresent iOS project",
90
+ auth: { type: "credential", credential: { label: "API v2 secret key" } },
91
+ }),
92
+ revenuecat("biblescroll", {
93
+ purpose: "Subscription state for the BibleScroll project",
94
+ auth: { type: "credential", credential: { label: "API v2 secret key" } },
95
+ }),
96
+ ]
97
+ ```
98
+
99
+ Two ids, two slots, two single-project catalogs — the `credential` option is
100
+ optional, and omitting it gives the same "API v2 secret key" label. See
101
+ [storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
102
+
103
+ That is config-as-code doing what an account model would otherwise do: one
104
+ credential per connector, each with its own catalog, storage namespace, health,
105
+ and admission counters. The two share a title, because Connecta cannot know
106
+ which project a key opens — so the guide summary is what tells them apart, and
107
+ it is built from `purpose`. Write a purpose that names the project, not one
108
+ that names RevenueCat.
109
+
110
+ If the deployment genuinely needs to move between projects in one session, use
111
+ OAuth instead and let the agent resolve `project_id`. Do not point a
112
+ project-scoped key's `project_id` argument at a project it cannot reach; the
113
+ call fails at RevenueCat, which is the correct outcome but a wasted round trip.
114
+
115
+ ## Authentication
116
+
117
+ OAuth is the default and the option RevenueCat recommends: "OAuth provides a
118
+ seamless authentication experience: log in to your RevenueCat account and grant
119
+ access to the MCP server, with no API keys to manage." Each connector instance
120
+ keeps its own flow and tokens in connector-scoped storage.
121
+
122
+ RevenueCat also accepts an API v2 secret key as a bearer token for headless
123
+ agents:
124
+
125
+ ```ts
126
+ revenuecat("bepresent_ios", {
127
+ purpose: "Subscription state for the BePresent iOS project",
128
+ auth: {
129
+ type: "headers",
130
+ headers: { Authorization: `Bearer ${env.REVENUECAT_KEY}` },
131
+ },
132
+ });
133
+ ```
134
+
135
+ Keys are prefixed `sk_`, are issued read-only or write-enabled, and can be
136
+ revoked at any time by a project Admin. RevenueCat's setup guidance is to "use
137
+ a write-enabled key if you plan to create/modify resources"; "a read-only key
138
+ works if you only need to view data". Keep the key in the runtime's secret
139
+ store, never in the deployment file — or declare
140
+ `auth: { type: "credential" }` and let the operator hold it in the vault
141
+ instead, which is the shape the two-project example above uses.
142
+
143
+ **Connecta does not filter writes for a read-only key.** It has no way to tell
144
+ which kind a key is without spending a call, so every write in the catalog is
145
+ offered, reaches RevenueCat, and fails there in RevenueCat's own words. The
146
+ guide says so, so an agent reads that refusal as "this key cannot write" rather
147
+ than as a bad argument and repairs it by routing to a write-enabled connector
148
+ instead of retrying.
149
+
150
+ An expired or revoked credential surfaces as `auth_required`, and the guide
151
+ names the `authorize_connector` recovery. A permission gap, a plan restriction,
152
+ or a rejected argument arrives as RevenueCat wrote it and is not an
153
+ authorization problem.
154
+
155
+ ## The ninety-five tools, and what they are classified as
156
+
157
+ RevenueCat's
158
+ [tool reference](https://www.revenuecat.com/docs/tools/mcp/tools-reference),
159
+ read on **2026-08-18**, documents ninety-five tools in a Read/Write table.
160
+ Ninety-four carry an access column and are classified here: **50 read-only, 15
161
+ additive writes, 29 destructive writes.**
162
+
163
+ Reads are every `Read` row, verbatim — the nine project and app reads, the four
164
+ product reads, the entitlement, offering, targeting, paywall, customer, virtual
165
+ currency, chart, webhook, and SDK reads, and `get-paywall-ai-task`.
166
+
167
+ Writes follow the verb where the verb is honest: `archive-*` and `unarchive-*`
168
+ flip an existing object's active state, `update-*`, `delete-*`, `publish-*`,
169
+ `unpublish-*`, and `detach-*` change or remove something that already exists,
170
+ and a plain `create-*` brings a new object into being beside the old ones.
171
+ `set-product-store-state` is an upsert and `submit-products-to-store` sends
172
+ products to Apple for review, so both are destructive.
173
+ `assign-customer-offering` and `grant-customer-entitlement` change a real
174
+ customer's access, so both are destructive too.
175
+
176
+ Nine verdicts are not decided by the verb, and each is argued in the source
177
+ beside the row:
178
+
179
+ | Tool | Verdict | Why |
180
+ | --- | --- | --- |
181
+ | `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 |
182
+ | `equalize-subscription-prices` | additive | "Fills **missing** App Store subscription territory prices" — by RevenueCat's own word it writes only where nothing is set |
183
+ | `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 |
184
+ | `upload-product-store-state-screenshot` | additive | "Reserves an App Store Connect review screenshot slot" — a new slot appears; nothing existing is replaced |
185
+ | `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 |
186
+ | `attach-products-to-package` | additive | the same argument one level down |
187
+ | `duplicate-paywall` | additive | "Duplicates an existing paywall's current draft" — the original is untouched |
188
+ | `create-paywall-ai` | additive | starts an async task that creates a paywall; every existing one is left alone |
189
+ | `edit-paywall-ai` | destructive | starts an async task that rewrites a draft that already exists |
190
+
191
+ `create-webhook-integration` deserves a sentence too. No existing integration
192
+ changes, so the verb reads additive — but with filters omitted the new one
193
+ "starts delivering" every customer event in the project to a URL the caller
194
+ typed. Customer data leaving the account is the `create_refund` argument again:
195
+ filed destructive on consequence, so the approval copy says what is at stake.
196
+
197
+ **`render-paywall-screenshot` is deliberately unclassified.** RevenueCat's
198
+ reference gives it no access column at all, so no release has reviewed what it
199
+ does. It fails closed onto `call_destructive_tool` rather than being guessed
200
+ into the read path because its name sounds harmless (P5). The guide names it,
201
+ so an agent does not read the approval prompt as a bug.
202
+
203
+ That classification fills in downstream silence and otherwise preserves explicit
204
+ annotations. A tool on the read allowlist arriving with `destructiveHint: true`
205
+ or `readOnlyHint: false` keeps exactly what the downstream said and stays behind
206
+ `call_destructive_tool`. A tool on neither maintained list arriving with
207
+ `readOnlyHint: true` keeps that too. Both are the downstream telling you this
208
+ release's allowlist is stale. The one fail-closed exception applies to a name
209
+ this release reviewed and filed destructive: a `grant-customer-entitlement`
210
+ claiming `readOnlyHint: true` is a downstream bug rather than news, and stays on
211
+ the approval path.
212
+
213
+ The tool list is not a fixed set, and the guide says so. RevenueCat gates parts
214
+ of its catalog by plan, platform, and beta enrollment — paywall AI editing,
215
+ benchmarks, experiments, virtual currencies, and the account-billing tools are
216
+ the usual absentees — so search this connector for what it actually exposes
217
+ rather than assuming a documented tool is here.
218
+
219
+ **No schema digests are recorded.** No release has read RevenueCat's live
220
+ schemas and written them down; that needs a live project and a maintainer's own
221
+ key. The manifest therefore ships names and verdicts only, and the drift check
222
+ honestly counts zero schema changes rather than reporting an invented one.
223
+ `npm run drift:check -- --record` reads them from a live catalog and prints the
224
+ block a release pastes in
225
+ ([#351](https://github.com/zackbart/connecta/issues/351)).
226
+
227
+ ## Rate limits
228
+
229
+ RevenueCat documents numbers, and this connection still declares no budget.
230
+
231
+ API v2 meters per minute and **per domain**
232
+ ([rate limits](https://www.revenuecat.com/docs/api-v2#tag/Rate-Limit), read
233
+ 2026-08-18):
234
+
235
+ | Domain | Requests per minute |
236
+ | --- | --- |
237
+ | Customer Information | 480 |
238
+ | Virtual Currencies | 480 |
239
+ | Subscription Transactions Refunds | 480 |
240
+ | Audiences | 60 |
241
+ | Project Configuration | 60 |
242
+ | Charts & Metrics | 25 |
243
+
244
+ A `ConnectorCallAdmissionPolicy` carries exactly one rule, so a connector-wide
245
+ budget has to pick one of those six numbers for all ninety-five tools.
246
+ Transcribing 25 would throttle a customer read loop to a nineteenth of its
247
+ documented allowance; transcribing 480 would leave a chart sweep unprotected.
248
+ Neither is the provider's limit, and both would look like RevenueCat being
249
+ flaky. The metering scope says the same thing again: the limit applies per API
250
+ key for app-level keys and **per developer** for developer-level keys, so an
251
+ OAuth session shares one budget with everything else that developer does, which
252
+ a per-runtime counter cannot approximate in either direction.
253
+
254
+ So the number stays with the operator who knows the account (P12), and the
255
+ guide states RevenueCat's own limits instead, along with the `429`,
256
+ `Retry-After`, and `backoff_ms` signals to back off on. Supply one like this:
257
+
258
+ ```ts
259
+ revenuecat("revenuecat", {
260
+ purpose: "Revenue charts and cohort reporting",
261
+ callAdmission: {
262
+ rules: [
263
+ {
264
+ maxConcurrency: 4,
265
+ queueTimeoutMs: 5_000,
266
+ retryAfterMs: 2_000,
267
+ // The Charts & Metrics ceiling, because this connector is used for
268
+ // charts. A customer-lookup connector would declare 480.
269
+ budget: { kind: "rolling-window", maxCalls: 25, windowMs: 60_000 },
270
+ },
271
+ ],
272
+ },
273
+ });
274
+ ```
275
+
276
+ As with every connector policy this is a **best-effort approximation** of the
277
+ provider's limit, not an enforcement of it. Each runtime keeps its own counter,
278
+ so N Worker isolates or Node processes serving one deployment can each admit up
279
+ to the stated rate. Discovery traffic is outside connector call admission and
280
+ still needs restrained use.
281
+
282
+ ## What is not verified
283
+
284
+ - **Every tool name is transcribed from RevenueCat's published reference on
285
+ 2026-08-18, not read from a live catalog.** No maintainer ran this against a
286
+ project with a real key before it shipped. A name the reference lists and the
287
+ server does not serve costs nothing; a tool the server serves and the
288
+ reference omits fails closed. The maintainer-run drift check with
289
+ `CONNECTA_DRIFT_REVENUECAT_KEY` set is what turns that into a finding with a
290
+ name attached.
291
+ - **No input or output schema has been read**, which is why the manifest
292
+ carries no digests.
293
+ - **Whether `render-paywall-screenshot` mutates anything.** It has no access
294
+ column, and guessing is exactly what P5 exists to prevent.
295
+
296
+ ## Conventions
297
+
298
+ This connection is audited against
299
+ [the provider conventions](./provider-conventions.md). Its verdict per
300
+ convention is the RevenueCat section of
301
+ [the provider audit](./provider-audit.md).
@@ -34,6 +34,61 @@ Credential mutation is intentionally narrower than MCP access:
34
34
  The vault is read for each call. Once an operator saves a replacement,
35
35
  the agent can retry immediately without restarting or redeploying Connecta.
36
36
 
37
+ ## A remote MCP connector's static credential
38
+
39
+ `remoteMcp()` accepts a third auth shape beside OAuth and literal headers:
40
+
41
+ ```ts
42
+ remoteMcp("revenuecat_bepresent", {
43
+ url: "https://mcp.revenuecat.ai/mcp",
44
+ auth: { type: "credential", credential: { label: "API v2 secret key" } },
45
+ });
46
+ ```
47
+
48
+ The connector, its endpoint, and the credential *slot* stay declared in code;
49
+ only the secret arrives through `/credentials`. That is the same boundary
50
+ `api()` has always had, and the reason a project-wide key no longer has to be a
51
+ Worker secret or an environment variable
52
+ ([#439](https://github.com/zackbart/connecta/issues/439)).
53
+
54
+ `header` defaults to `Authorization` and `scheme` to `Bearer`. `scheme: null`
55
+ sends the stored value verbatim, which is what Linear's personal API keys
56
+ expect. A scheme whose last token is `Basic` declares HTTP Basic credentials, so
57
+ the stored `user:secret` is base64-encoded first — `"Basic"` produces
58
+ `Basic <base64>`, and Mixpanel's documented `"Bearer Basic"` produces
59
+ `Bearer Basic <base64>`. There is one reserved `value` field and no multi-field
60
+ header composition: named `credential.fields` are refused at construction.
61
+
62
+ A stored value is checked before anything frames it: a line break or other
63
+ control character — what a key pasted across two lines leaves behind — is
64
+ refused as `auth_required` with a message naming the problem and never the
65
+ value. That check exists because the runtime that rejects such a header quotes
66
+ the whole offending value back in its `TypeError`, and that message would
67
+ otherwise reach the agent, the operator page, and the activity log. Behind it,
68
+ any error whose message quotes the credential or the header it became is
69
+ discarded whole and replaced; nothing is masked or truncated, because a
70
+ redaction that keeps part of a secret is still a leak.
71
+
72
+ An empty slot is not a boot failure and not a silently absent connector. The
73
+ connector is present, its status reads `auth_required`, calls fail with the same
74
+ typed error a missing OAuth grant produces, and `authorize_connector` returns
75
+ the `/credentials` handoff. With no vault configured at all, the failure names
76
+ `credentials.encryptionKey`, and Connecta already warned at startup.
77
+
78
+ The vault is read before any cached downstream client is trusted, so a rotation
79
+ lands on the next call rather than the next deploy. Connecta compares a SHA-256
80
+ digest of the value the cached client connected with; a different digest closes
81
+ that client and reconnects. The plaintext lives in the connect attempt's local
82
+ scope, never on connector state, never in a log, and never in a status or error
83
+ message. A cleartext `http://` destination warns at construction here exactly as
84
+ it does for literal headers — who owns the secret changed, not what the wire
85
+ carries.
86
+
87
+ `/credentials`' Test action connects with the stored value and reports how many
88
+ tools the downstream served. That is the whole honest check for a proxy: which
89
+ account, project, or mode the key reaches is the provider's answer, not
90
+ Connecta's.
91
+
37
92
  ## Downstream OAuth
38
93
 
39
94
  `remoteMcp()` stores dynamic client registration, tokens, PKCE material, state,
@@ -95,12 +95,32 @@ Use a restricted key, not a secret key, and scope it to the operations the
95
95
  agent actually needs; Stripe's own guidance is to "limit your agent's access to
96
96
  exactly the functionality it requires". Keep it in the runtime's secret store.
97
97
 
98
+ The same key can come from `/credentials` instead:
99
+
100
+ ```ts
101
+ stripe("stripe_sandbox", {
102
+ mode: "sandbox",
103
+ purpose: "Automated billing rehearsal",
104
+ auth: { type: "credential" },
105
+ });
106
+ ```
107
+
108
+ `mode` is required either way — a static key answers for exactly one
109
+ environment and cannot report which. The literal-header form is checked against
110
+ the key's `_live_`/`_test_` prefix at construction; an operator-managed key is
111
+ not in the deployment file to read, so the declared mode stands alone and a key
112
+ pointed at the other environment fails at Stripe. Declare the mode carefully:
113
+ that check is the one guard Connecta can offer, and this shape does not get it.
114
+ See
115
+ [storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
116
+
98
117
  Organization accounts in one OAuth session are not Stripe Connect connected
99
118
  accounts. Connect platforms can act as a connected account with
100
119
  `connectedAccount`, which adds Stripe's documented `Stripe-Account` header at
101
120
  connector construction. Stripe does not support OAuth for connected-account
102
- calls, so this requires a restricted key through `headers` auth and throws
103
- otherwise:
121
+ calls, and `Stripe-Account` is a second header beside the credential's own,
122
+ which the operator-managed shape does not assemble — so this requires a
123
+ restricted key through `headers` auth and throws otherwise:
104
124
 
105
125
  ```ts
106
126
  stripe("merchant_42", {
@@ -177,6 +197,16 @@ organization-account selection separate from the restricted-key-only Connect
177
197
  path, so an agent cannot repair uncertainty by fabricating `Stripe-Account` as
178
198
  a tool argument.
179
199
 
200
+ The guide also carries the reduction advice the generic schemas cannot (P7):
201
+ a list or search read that returns full objects belongs inside `execute_code`,
202
+ projected to the fields the question needs before `return`, because an
203
+ unprojected list truncates and a projected one keeps customer PII out of the
204
+ transcript. It names Stripe search's per-resource field set — charges search
205
+ has no `payment_intent` field, so the path is the PaymentIntent's
206
+ `latest_charge` — and the account → search → details → read sequence as one
207
+ program rather than four turns, and it names `outcome`, `failure_code`, and
208
+ `failure_message` on the charge as the answer to "why did this payment fail".
209
+
180
210
  Stripe publishes no stability or deprecation policy for this tool set and
181
211
  invites tool requests by email, so treat the list as unversioned. `get_balance_summary`
182
212
  is Treasury, which Stripe labels public preview and gates behind an access
@@ -57,7 +57,7 @@ exist so far:
57
57
  | --- | --- | --- |
58
58
  | **pre-template** | before 0.10.2 | no `connecta init` existed; hand-written, or copied from the retired `examples/node` |
59
59
  | **A** | 0.10.2 – 0.15.1 | `.env.example`, `.gitignore`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `package.json`, `src/index.ts`, `tsconfig.json` |
60
- | **B** | 0.16.0 – 0.18.0 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
60
+ | **B** | 0.16.0 – 0.18.2 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
61
61
 
62
62
  Generation A is a decade in template years and identifying it precisely does
63
63
  not matter, because you are about to reconstruct it exactly rather than guess
@@ -106,7 +106,7 @@ know what to preserve, once to know what to re-verify at the end.
106
106
  ### Bump the pin and install
107
107
 
108
108
  ```sh
109
- npm pkg set dependencies.@zackbart/connecta=0.18.0
109
+ npm pkg set dependencies.@zackbart/connecta=0.18.2
110
110
  npm install
111
111
  ```
112
112
 
@@ -130,7 +130,7 @@ Generate the *current* template beside the base you already made, into the same
130
130
  `$SCRATCH`:
131
131
 
132
132
  ```sh
133
- (cd "$SCRATCH" && npx @zackbart/connecta@0.18.0 init current)
133
+ (cd "$SCRATCH" && npx @zackbart/connecta@0.18.2 init current)
134
134
  ```
135
135
 
136
136
  You now have a three-way merge with a real base: `$SCRATCH/base` is what this
@@ -186,7 +186,7 @@ A deployment older than 0.10.2 has no base to diff against. Do not try to
186
186
  manufacture one. Instead:
187
187
 
188
188
  1. `SCRATCH=$(mktemp -d)`, then
189
- `(cd "$SCRATCH" && npx @zackbart/connecta@0.18.0 init current)` — there is no
189
+ `(cd "$SCRATCH" && npx @zackbart/connecta@0.18.2 init current)` — there is no
190
190
  `base` leg here, only the current template to read from.
191
191
  2. Copy `$SCRATCH/current` into the deployment file by file, **skipping
192
192
  `src/index.ts`**.
@@ -207,6 +207,33 @@ first, so cross them bottom-up: start at the oldest one still above this
207
207
  deployment's pin and work back up the page, because each boundary assumes the
208
208
  older ones are already done.
209
209
 
210
+ ### 0.18.1 → 0.18.2
211
+
212
+ Nothing throws for an existing deployment, and the version bump alone crosses
213
+ it. The release adds a third `auth` shape to `remoteMcp()` and every maintained
214
+ hosted connection — `{ type: "credential" }` — under which the connector
215
+ declares an operator slot on `/credentials` and reads the pasted value on each
216
+ request. A deployment carrying a static key as a runtime secret
217
+ (`auth: { type: "headers", headers: { Authorization: env.KEY } }`) keeps
218
+ working unchanged; moving it behind `/credentials` is an edit to the connector's
219
+ `auth` and one paste on the operator page, and needs `credentials.encryptionKey`
220
+ configured — a deployment without a vault gets a startup warning and
221
+ `recovery: "unavailable"` at use for that connector, not a boot failure. Two
222
+ Linear notes: the `headers` example in `documentation/linear.md` now shows
223
+ `Bearer ${key}` (Linear's MCP server documents that framing), and the credential
224
+ shape sends `Bearer` by default; a `headers` connector already sending a bare
225
+ key is untouched.
226
+
227
+ ### 0.18.0 → 0.18.1
228
+
229
+ Nothing throws, no option moves, and every deployment crosses this on the
230
+ version bump alone. The release adds one provider subpath,
231
+ `@zackbart/connecta/providers/revenuecat`, and rewrites guide text inside the
232
+ `mixpanel()` and `stripe()` connections; a deployment that constructs neither
233
+ sees no change, and one that does gets better first-line advice for the same
234
+ constructor calls. Clients that cache connector guides should refresh them
235
+ after upgrading.
236
+
210
237
  ### 0.17.0 → 0.18.0
211
238
 
212
239
  One floor moves and one always-loaded surface shrinks; neither changes a