@zackbart/connecta 0.24.2 → 0.24.4

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 (97) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +205 -1
  3. package/README.md +5 -6
  4. package/dist/auth/bearer.js +2 -0
  5. package/dist/auth/downstream-oauth.d.ts +12 -1
  6. package/dist/auth/downstream-oauth.js +147 -35
  7. package/dist/branding.d.ts +31 -2
  8. package/dist/branding.js +116 -8
  9. package/dist/call-admission.d.ts +4 -0
  10. package/dist/call-admission.js +26 -0
  11. package/dist/catalog-drift.js +9 -4
  12. package/dist/catalog-service.d.ts +2 -0
  13. package/dist/catalog-service.js +25 -8
  14. package/dist/catalog.d.ts +2 -0
  15. package/dist/catalog.js +246 -121
  16. package/dist/connectors/api.d.ts +1 -1
  17. package/dist/connectors/api.js +21 -3
  18. package/dist/connectors/guarded-fetch.d.ts +6 -2
  19. package/dist/connectors/guarded-fetch.js +61 -24
  20. package/dist/connectors/remote-mcp.js +92 -57
  21. package/dist/errors.d.ts +28 -3
  22. package/dist/errors.js +60 -1
  23. package/dist/execute.js +85 -23
  24. package/dist/executor-result.js +3 -1
  25. package/dist/executors/quickjs-child.js +5 -1
  26. package/dist/executors/quickjs-protocol.d.ts +4 -0
  27. package/dist/executors/quickjs-runtime.d.ts +1 -1
  28. package/dist/executors/quickjs-runtime.js +38 -21
  29. package/dist/executors/quickjs.js +68 -27
  30. package/dist/index.d.ts +15 -1
  31. package/dist/index.js +36 -4
  32. package/dist/invocation.js +134 -93
  33. package/dist/mcp-result.js +3 -2
  34. package/dist/meta-tools.js +191 -36
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +38 -1
  37. package/dist/operator-ui/view.js +71 -0
  38. package/dist/providers/cloudflare.d.ts +14 -2
  39. package/dist/providers/cloudflare.js +107 -16
  40. package/dist/providers/linear.d.ts +26 -4
  41. package/dist/providers/linear.js +19 -4
  42. package/dist/providers/mixpanel.d.ts +16 -3
  43. package/dist/providers/mixpanel.js +13 -2
  44. package/dist/providers/notion.d.ts +8 -1
  45. package/dist/providers/notion.js +83 -10
  46. package/dist/providers/revenuecat.d.ts +30 -4
  47. package/dist/providers/revenuecat.js +42 -4
  48. package/dist/providers/stripe.d.ts +7 -1
  49. package/dist/providers/stripe.js +30 -4
  50. package/dist/providers/vercel.js +11 -1
  51. package/dist/registry.d.ts +22 -2
  52. package/dist/registry.js +101 -13
  53. package/dist/routes/mcp.d.ts +4 -1
  54. package/dist/routes/mcp.js +84 -13
  55. package/dist/routes/oauth.js +4 -0
  56. package/dist/routes/shared.d.ts +1 -0
  57. package/dist/routes/shared.js +4 -4
  58. package/dist/server.js +15 -3
  59. package/dist/skills.js +6 -5
  60. package/dist/storage/file.d.ts +6 -2
  61. package/dist/storage/file.js +312 -34
  62. package/dist/storage/memory.js +12 -1
  63. package/dist/types.d.ts +37 -0
  64. package/dist/ui.js +18 -10
  65. package/dist/validate.js +3 -3
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/documentation/architecture.md +193 -165
  69. package/documentation/auth.md +199 -145
  70. package/documentation/code-mode.md +433 -316
  71. package/documentation/meta-tools.md +363 -355
  72. package/examples/worker/AGENTS.md +2 -1
  73. package/examples/worker/README.md +12 -10
  74. package/examples/worker/src/index.ts +12 -15
  75. package/package.json +1 -2
  76. package/templates/node/.env.example +3 -3
  77. package/templates/node/AGENTS.md +5 -4
  78. package/templates/node/README.md +2 -1
  79. package/templates/node/package.json +1 -1
  80. package/templates/node/src/index.ts +23 -22
  81. package/documentation/call-admission.md +0 -142
  82. package/documentation/cloudflare.md +0 -471
  83. package/documentation/connector-guides.md +0 -176
  84. package/documentation/connectors.md +0 -389
  85. package/documentation/linear.md +0 -193
  86. package/documentation/mixpanel.md +0 -160
  87. package/documentation/notion.md +0 -308
  88. package/documentation/operations.md +0 -359
  89. package/documentation/operator-ui.md +0 -135
  90. package/documentation/optional-modules-upgrade.md +0 -243
  91. package/documentation/provider-conventions.md +0 -722
  92. package/documentation/request-admission.md +0 -170
  93. package/documentation/revenuecat.md +0 -305
  94. package/documentation/storage-and-credentials.md +0 -201
  95. package/documentation/stripe.md +0 -262
  96. package/documentation/upgrading.md +0 -754
  97. package/documentation/vercel.md +0 -241
@@ -1,170 +0,0 @@
1
- # Request admission
2
-
3
- Connecta bounds work at the Web-standard request boundary — before inbound
4
- auth, before the MCP server exists, before any catalog is touched. A burst
5
- therefore meets an explicit active count and an explicit queue instead of
6
- asking traffic shape and the runtime allocator to pick the process high-water
7
- mark.
8
-
9
- There are two pools here and a third elsewhere. This guide covers the first
10
- two: the deployment-wide `/mcp` pool and the fallback code pool. Per-connector
11
- downstream bounds are [call admission](./call-admission.md), which is a
12
- different question — request admission bounds the whole MCP envelope, call
13
- admission bounds the individual `Connector.callTool` attempts fanned out inside
14
- it.
15
-
16
- ## The pools
17
-
18
- Every non-preflight `/mcp` request takes one permit from a deployment-wide FIFO
19
- pool. Initialization, discovery, ordinary calls, and `execute_code` all pay it.
20
- A program then takes a *second* permit from the deliberately smaller code pool,
21
- so one request cannot trade ordinary capacity for an unbounded number of
22
- sandboxes.
23
-
24
- ```ts
25
- const connecta = createConnecta({
26
- admission: {
27
- requests: { concurrency: 16, maxQueueSize: 32, queueTimeoutMs: 5_000, retryAfterMs: 1_000 },
28
- code: { concurrency: 2, maxQueueSize: 8, queueTimeoutMs: 5_000, retryAfterMs: 1_000 },
29
- },
30
- // …
31
- });
32
- ```
33
-
34
- Those values are the defaults, and both pools are the same
35
- `AdmissionController` (`src/executor-admission.ts`) with different numbers.
36
- `maxQueueSize: 0` is the fail-fast shape. Every value is a finite whole number;
37
- concurrency and the queue timeout must be positive, while queue size and the
38
- retry hint may be zero. Invalid bounds throw at construction rather than
39
- silently removing the deployment's memory boundary — a pool that quietly became
40
- unbounded is worse than a deployment that refuses to boot.
41
-
42
- `admission.code` is a *fallback*. An executor that implements `acquire()` is an
43
- `AdmittingExecutor` and already owns a bounded pool, so its own settings win and
44
- connecta warns that `admission.code` was ignored. `quickJsExecutor()` is one of
45
- those: it defaults to one active execution and 32 queued callers, configured on
46
- the executor rather than here. Cloudflare's `DynamicWorkerExecutor` is not, so
47
- a Worker deployment gets the fallback pool wrapped around it at construction —
48
- which is also why `/health` always has a code-admission shape to report.
49
-
50
- The request pool is global FIFO across identities. It is a capacity boundary,
51
- not tenant fairness: one busy caller can occupy it. Per-tenant fairness needs a
52
- policy above connecta, and one deployment still serves one tenant even when
53
- identity rules give its principals different connector views
54
- ([`ethos.md`](../ethos.md)), so a global queue is not pretending to supply
55
- something it does not.
56
-
57
- ## Admission before auth
58
-
59
- `/mcp` acquires its permit *before* running the auth gate. This looks backwards
60
- until you price it: authenticating first means an unauthenticated flood buys a
61
- Clerk network lookup per request, so the cheapest possible attack becomes the
62
- most expensive request the server can serve. Admitting first means it buys a
63
- queue slot and a 503.
64
-
65
- The permit is released with the response *body*, not when the handler returns.
66
- A slow client draining a large result still counts as active work, because its
67
- bytes and its socket still exist. `releaseAdmissionWithResponse` re-wraps the
68
- response stream to do this, absorbs a rejecting `cancel()` rather than leaking
69
- an unhandled rejection, and releases exactly once — release is idempotent, and
70
- `test/request-admission.test.ts` pins both the stream-cancel path and the
71
- double-release case.
72
-
73
- Every other route bypasses the pool entirely. `/health` and the operator
74
- surface stay responsive while MCP is saturated, which is the whole point: an
75
- operator diagnosing an overload must not have to queue behind it. `/health`
76
- names the exempt routes in `admission.reservedRoutes` so the claim is checkable
77
- from outside.
78
-
79
- ## Overload, cancellation, shutdown
80
-
81
- A full queue or an expired queue deadline answers HTTP 503 with `Retry-After`,
82
- CORS headers, and a stable JSON-RPC error:
83
-
84
- ```json
85
- {
86
- "jsonrpc": "2.0",
87
- "id": null,
88
- "error": {
89
- "code": -32001,
90
- "message": "Server capacity is exhausted. Retry later.",
91
- "data": { "code": "server_overloaded", "retryable": true, "retryAfterMs": 1000 }
92
- }
93
- }
94
- ```
95
-
96
- `Retry-After` is that hint rounded up to at least one whole second. It is
97
- advice, not a reservation. Shutdown uses `-32002` / `server_shutting_down` and
98
- is not retryable. Code-pool overload never reaches this layer: it surfaces as
99
- an ordinary MCP tool error with `executor_overloaded`, `retryable: true`, and
100
- the executor's own `retryAfterMs`.
101
-
102
- A cancelled queued request is removed immediately and never receives a later
103
- permit — cancelling and then being admitted would hold capacity for a caller
104
- that is gone. An admitted request keeps its permit until its body completes,
105
- errors, or is cancelled.
106
-
107
- `connecta.close()` closes both queues before releasing executor resources:
108
- queued and future MCP work is rejected with `server_shutting_down` while
109
- admitted work drains. Node's `listen()` calls it on SIGTERM or SIGINT, stops
110
- accepting connections, drains, and enforces `shutdownTimeoutMs` (10 s default)
111
- — SIGTERM arrives on every `docker compose up` recreate, and Node's default
112
- response to it is to die mid-request.
113
-
114
- ## What admission is not
115
-
116
- The Node adapter's `maxBodyBytes` (10 MiB default) is a separate ingress guard.
117
- It caps the HTTP body while constructing the Web `Request`, which happens
118
- *before* the portable `/mcp` boundary can run. Admission bounds MCP, auth,
119
- catalog, and response work; it is not a byte budget for many simultaneous slow
120
- or near-limit uploads. Hostile public traffic wants an ingress proxy with a
121
- body-rate limit in front, and `maxBodyBytes` set to the smallest value the
122
- deployment actually needs.
123
-
124
- The rejection warning is rate-limited to one per second, and each line reports
125
- how many were suppressed since the last one. The `/health` totals count every
126
- rejection, so the log is a sample and the counters are the record. Queue waits
127
- log at debug level. Nothing on this path records or exposes request bodies,
128
- tool arguments, identities, or results.
129
-
130
- ## Observations
131
-
132
- `/health` exposes payload-free snapshots under `admission.requests`,
133
- `admission.code`, and `admission.downstreamCalls`. The first two carry
134
- configured bounds, current active and queued counts, cumulative
135
- admitted/queued/rejected/cancelled/closed totals, and queue-wait count, total,
136
- and maximum. The request policy is labelled `global-fifo`; downstream policy is
137
- labelled `connector-partitioned-per-runtime`. An executor that owns its own
138
- pool and exposes no snapshot reports `{ managedByExecutor: true }`.
139
-
140
- With `execute_code({ diagnostics: true })`, a caller sees the same split from
141
- the inside: `admissionMs` is time spent waiting for a permit and `connectorMs`
142
- is the admitted attempt.
143
-
144
- ## Measuring capacity
145
-
146
- `npm run load:admission` builds the package, then starts server and generator
147
- in separate processes over real loopback TCP, warms a 10,000-tool catalog,
148
- verifies every returned value, and records throughput, p50/p95/p99, server-only
149
- peak RSS, RSS after forced GC, and live heap after GC. Each matrix cell gets a
150
- fresh server so an earlier allocator high-water mark cannot contaminate the
151
- next baseline; the three-round soak deliberately reuses one, because allocator
152
- high-water retention and a live-object climb look identical in a single run and
153
- different across three.
154
-
155
- `CONNECTA_LOAD_CATALOG_SIZE`, `CONNECTA_LOAD_CONCURRENCY`, and
156
- `CONNECTA_LOAD_MAX_QUEUE_SIZE` change catalog size, server concurrency, and
157
- queue depth. The script prints its own numbers; no baseline is checked in,
158
- deliberately. A laptop matrix is an example capacity profile, not a portable
159
- SLO, and downstream payload size moves it more than any setting here does — pin
160
- a runner before enforcing a regression ratio, and measure the connector mix the
161
- deployment actually runs.
162
-
163
- ## Tests that enforce this
164
-
165
- | Invariant | Suite |
166
- | --- | --- |
167
- | FIFO bounds, active and queue ceilings, stable retryable overload, queue timeout, cancellation removal, idempotent release, shutdown | `test/executor-admission.test.ts` (Node + Workers) |
168
- | `/mcp` bounded before auth, stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, the separate fallback code pool | `test/request-admission.test.ts` |
169
- | A client disconnect propagating through the Web `Request` into a program's connector call, releasing both permits | `test/node.test.ts` |
170
- | The `/health` admission payload alongside the drift counts | `test/catalog-drift.test.ts`, `test/server.test.ts` |
@@ -1,305 +0,0 @@
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 the connection UI at `/`:
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-six 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-30**, documents ninety-six tools. Ninety-five carry an access
160
- column and are classified here: **51 read-only, 15 additive writes, 29
161
- 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, `get-paywall-ai-task`, and
166
- `get-refund-request-preferences`.
167
-
168
- Writes follow the verb where the verb is honest: `archive-*` and `unarchive-*`
169
- flip an existing object's active state, `update-*`, `delete-*`, `publish-*`,
170
- `unpublish-*`, and `detach-*` change or remove something that already exists,
171
- and a plain `create-*` brings a new object into being beside the old ones.
172
- `set-product-store-state` is an upsert and `submit-products-to-store` sends
173
- products to Apple for review, so both are destructive.
174
- `assign-customer-offering` and `grant-customer-entitlement` change a real
175
- customer's access, so both are destructive too.
176
-
177
- Nine verdicts are not decided by the verb, and each is argued in the source
178
- beside the row:
179
-
180
- | Tool | Verdict | Why |
181
- | --- | --- | --- |
182
- | `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 |
183
- | `equalize-subscription-prices` | additive | "Fills **missing** App Store subscription territory prices" — by RevenueCat's own word it writes only where nothing is set |
184
- | `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 |
185
- | `upload-product-store-state-screenshot` | additive | "Reserves an App Store Connect review screenshot slot" — a new slot appears; nothing existing is replaced |
186
- | `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 |
187
- | `attach-products-to-package` | additive | the same argument one level down |
188
- | `duplicate-paywall` | additive | "Duplicates an existing paywall's current draft" — the original is untouched |
189
- | `create-paywall-ai` | additive | starts an async task that creates a paywall; every existing one is left alone |
190
- | `edit-paywall-ai` | destructive | starts an async task that rewrites a draft that already exists |
191
-
192
- `create-webhook-integration` deserves a sentence too. No existing integration
193
- changes, so the verb reads additive — but with filters omitted the new one
194
- "starts delivering" every customer event in the project to a URL the caller
195
- typed. Customer data leaving the account makes it destructive on consequence,
196
- so the approval copy says what is at stake.
197
-
198
- **`render-paywall-screenshot` is deliberately unclassified.** RevenueCat's
199
- reference gives it no access column. The current live server explicitly marks
200
- it read-only, so that catalog keeps it callable from `execute_code`; if a later
201
- catalog omits the annotation, it fails closed onto `call_destructive_tool`.
202
- Connecta preserves the provider's current annotation without inventing a
203
- release classification from the tool's harmless-sounding name (P5).
204
-
205
- That classification fills in downstream silence and otherwise preserves explicit
206
- annotations. A tool on the read allowlist arriving with `destructiveHint: true`
207
- or `readOnlyHint: false` keeps exactly what the downstream said and stays behind
208
- `call_destructive_tool`. A tool on neither maintained list arriving with
209
- `readOnlyHint: true` keeps that too. Both are the downstream telling you this
210
- release's allowlist is stale. The one fail-closed exception applies to a name
211
- this release reviewed and filed destructive: a `grant-customer-entitlement`
212
- claiming `readOnlyHint: true` is a downstream bug rather than news, and stays on
213
- the approval path.
214
-
215
- The tool list is not a fixed set, and the guide says so. RevenueCat gates parts
216
- of its catalog by plan, platform, and beta enrollment — paywall AI editing,
217
- benchmarks, experiments, virtual currencies, and the account-billing tools are
218
- the usual absentees — so search this connector for what it actually exposes
219
- rather than assuming a documented tool is here.
220
-
221
- **No schemas are vendored.** The manifest ships names and safety verdicts only.
222
- The live `tools/list` response remains the schema agents receive; Connecta does
223
- not replace it with a snapshot or require a maintainer credential to validate
224
- one.
225
-
226
- ## Rate limits
227
-
228
- RevenueCat documents numbers, and this connection still declares no budget.
229
-
230
- API v2 meters per minute and **per domain**
231
- ([rate limits](https://www.revenuecat.com/docs/api-v2#tag/Rate-Limit), read
232
- 2026-08-18):
233
-
234
- | Domain | Requests per minute |
235
- | --- | --- |
236
- | Customer Information | 480 |
237
- | Virtual Currencies | 480 |
238
- | Subscription Transactions Refunds | 480 |
239
- | Audiences | 60 |
240
- | Project Configuration | 60 |
241
- | Charts & Metrics | 25 |
242
-
243
- A `ConnectorCallAdmissionPolicy` carries exactly one rule, so a connector-wide
244
- budget has to pick one of those six numbers for all ninety-six tools.
245
- Transcribing 25 would throttle a customer read loop to a nineteenth of its
246
- documented allowance; transcribing 480 would leave a chart sweep unprotected.
247
- Neither is the provider's limit, and both would look like RevenueCat being
248
- flaky. The metering scope says the same thing again: the limit applies per API
249
- key for app-level keys and **per developer** for developer-level keys, so an
250
- OAuth session shares one budget with everything else that developer does, which
251
- a per-runtime counter cannot approximate in either direction.
252
-
253
- So the number stays with the operator who knows the account (P12), and the
254
- guide states RevenueCat's own limits instead, along with the `429`,
255
- `Retry-After`, and `backoff_ms` signals to back off on. Supply one like this:
256
-
257
- ```ts
258
- revenuecat("revenuecat", {
259
- purpose: "Revenue charts and cohort reporting",
260
- callAdmission: {
261
- rules: [
262
- {
263
- maxConcurrency: 4,
264
- queueTimeoutMs: 5_000,
265
- retryAfterMs: 2_000,
266
- // The Charts & Metrics ceiling, because this connector is used for
267
- // charts. A customer-lookup connector would declare 480.
268
- budget: { kind: "rolling-window", maxCalls: 25, windowMs: 60_000 },
269
- },
270
- ],
271
- },
272
- });
273
- ```
274
-
275
- As with every connector policy this is a **best-effort approximation** of the
276
- provider's limit, not an enforcement of it. Each runtime keeps its own counter,
277
- so N Worker isolates or Node processes serving one deployment can each admit up
278
- to the stated rate. Discovery traffic is outside connector call admission and
279
- still needs restrained use.
280
-
281
- ## What is not verified
282
-
283
- - **The 2026-08-30 live review used a project-scoped catalog.** It proves the
284
- additions that catalog serves, including `get-refund-request-preferences`,
285
- but cannot prove a globally documented tool was removed. The manifest stays
286
- a superset because plan, platform, and credential scope hide tools.
287
- - **No complete schema set is vendored.** The live review read the new schemas,
288
- but its scoped catalog omitted many classified writes. Runtime schemas still
289
- come from the server.
290
- - **Whether `render-paywall-screenshot` mutates anything.** It has no access
291
- column, and guessing is exactly what P5 exists to prevent.
292
-
293
- `npm run drift:check -- --docs --provider revenuecat` checks the official setup
294
- page and its 105-row tool reference without a credential. It compares the
295
- documented names with the release-reviewed classifications. The screenshot
296
- tool's blank Access column is a manually reviewed exception: the checker
297
- reports it separately and Connecta keeps it fail-closed. The check reads names,
298
- not live schemas or machine-interpreted access verdicts.
299
-
300
- ## Conventions
301
-
302
- This connection is audited against
303
- [the provider conventions](./provider-conventions.md). Its verdict per
304
- convention is the RevenueCat section of
305
- [the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).
@@ -1,201 +0,0 @@
1
- # Storage and credentials
2
-
3
- The core `KVStorage` contract supports `get`, `set`, and `delete`; adapters may
4
- also implement `list(prefix)`. Core uses storage for connector state, catalogs,
5
- and result paging. The vault is an explicit independent option:
6
-
7
- ```ts
8
- import { encryptedCredentialVault } from "@zackbart/connecta/credentials";
9
-
10
- createConnecta({
11
- connectors,
12
- executor,
13
- storage,
14
- vault: encryptedCredentialVault(storage, encryptionKey),
15
- });
16
- ```
17
-
18
- `encryptionKey` is a base64 32-byte AES key. The factory implements the
19
- root-exported `CredentialVault` contract. Core depends on the contract without
20
- importing the encrypted implementation; deployments may supply their own.
21
- A replacement must isolate both connector id and optional owner, preserve
22
- metadata-only reads for the UI, and keep plaintext confined to host credential
23
- resolution. The built-in AES-GCM implementation binds connector and owner into
24
- authenticated encryption context. Sharing a backend is not permission to share
25
- a principal's credentials.
26
-
27
- Connectors declare a human-managed `credential` slot in code. The vault exposes
28
- read access only through that connector's `ctx.credential`. Values, masked
29
- values, call arguments, and raw errors never enter model recovery or activity.
30
- Omitting the vault leaves literal deployment-provided secrets and downstream
31
- OAuth available, but declared vault slots cannot be managed.
32
-
33
- Credentials fail at use. Proactive liveness probing remains removed by
34
- [the 0.9 decision](../ethos.md). Operator-triggered tests and local credential
35
- shape checks remain. `authorize_connector` returns a human credential handoff
36
- only when both a vault and the UI are configured. Without either, recovery is
37
- `unavailable` and explains the missing configuration.
38
-
39
- A static bearer may invoke a connector but never mutate its auth as a browser
40
- user. Interactive users need visibility plus `credentialAdministration` for
41
- shared auth or `personalConnection` for their own personal auth. Both management
42
- permissions default to none. Saving, testing, replacing, or removing a value
43
- never returns it. The vault is read for each call, so a saved replacement takes
44
- effect without restarting the deployment.
45
-
46
- ## Storage continuity
47
-
48
- This module extraction changes no encrypted record keys, owner partitions, or
49
- OAuth storage format. Reuse the existing storage and encryption key when
50
- constructing `encryptedCredentialVault`. Keep connector ids, principal ids, and
51
- identity namespaces unchanged. Losing the encryption key makes old vault
52
- values unreadable; changing identity context selects another personal partition.
53
-
54
- Removed Connecta-issued token records remain inert in storage. This upgrade
55
- does not delete them, convert them to another credential, or migrate their
56
- clients automatically.
57
-
58
- ## Shared and personal auth
59
-
60
- Connector auth defaults to `authScope: "shared"`. Its credential, OAuth state,
61
- tokens, catalog cache, and connector storage belong to the deployment. Set
62
- `authScope: "personal"` when every human principal needs a separate downstream
63
- account:
64
-
65
- ```ts
66
- remoteMcp("linear", {
67
- url: "https://mcp.linear.app/mcp",
68
- authScope: "personal",
69
- auth: { type: "oauth" },
70
- });
71
- ```
72
-
73
- Personal connectors disappear from a request that has no stable human
74
- principal. For a principal that can see one, connecta partitions connector
75
- storage, encrypted vault records, catalog caches, OAuth generations, and
76
- observed result shapes under an opaque SHA-256 identity key. Results used by
77
- `get_result` are partitioned by the authenticated subject, so one authenticated subject cannot page another subject's call.
78
-
79
- Literal `auth: { type: "headers" }` cannot be personal because its secret lives
80
- in deployment code. `remoteMcp()` refuses that combination at construction.
81
- Use operator-managed credential auth or OAuth instead.
82
-
83
- ## A remote MCP connector's static credential
84
-
85
- `remoteMcp()` accepts a third auth shape beside OAuth and literal headers:
86
-
87
- ```ts
88
- remoteMcp("revenuecat_bepresent", {
89
- url: "https://mcp.revenuecat.ai/mcp",
90
- auth: { type: "credential", credential: { label: "API v2 secret key" } },
91
- });
92
- ```
93
-
94
- The connector, its endpoint, and the credential *slot* stay declared in code;
95
- only the secret arrives through the connection UI at `/`. That is the same boundary
96
- `api()` has always had, and the reason a project-wide key no longer has to be a
97
- Worker secret or an environment variable
98
- ([#439](https://github.com/zackbart/connecta/issues/439)).
99
-
100
- `header` defaults to `Authorization` and `scheme` to `Bearer`. `scheme: null`
101
- sends the stored value verbatim, which is what Linear's personal API keys
102
- expect. A scheme whose last token is `Basic` declares HTTP Basic credentials, so
103
- the stored `user:secret` is base64-encoded first — `"Basic"` produces
104
- `Basic <base64>`, and Mixpanel's documented `"Bearer Basic"` produces
105
- `Bearer Basic <base64>`. There is one reserved `value` field and no multi-field
106
- header composition: named `credential.fields` are refused at construction.
107
-
108
- A stored value is checked before anything frames it: a line break or other
109
- control character — what a key pasted across two lines leaves behind — is
110
- refused as `auth_required` with a message naming the problem and never the
111
- value. That check exists because the runtime that rejects such a header quotes
112
- the whole offending value back in its `TypeError`, and that message would
113
- otherwise reach the agent, the operator page, and the activity log. Behind it,
114
- any error whose message quotes the credential or the header it became is
115
- discarded whole and replaced; nothing is masked or truncated, because a
116
- redaction that keeps part of a secret is still a leak.
117
-
118
- An empty slot is not a boot failure and not a silently absent connector. The
119
- connector is present, its status reads `auth_required`, calls fail with the same
120
- typed error a missing OAuth grant produces, and `authorize_connector` returns
121
- a connection UI handoff at `/` when UI and vault are configured. Without either,
122
- it returns `unavailable` and explains what is missing.
123
-
124
- The vault is read before any cached downstream client is trusted, so a rotation
125
- lands on the next call rather than the next deploy. Connecta compares a SHA-256
126
- digest of the value the cached client connected with; a different digest closes
127
- that client and reconnects. The plaintext lives in the connect attempt's local
128
- scope, never on connector state, never in a log, and never in a status or error
129
- message. A cleartext `http://` destination warns at construction here exactly as
130
- it does for literal headers — who owns the secret changed, not what the wire
131
- carries.
132
-
133
- The connection UI Test action connects with the stored value and reports how many
134
- tools the downstream served. That is the whole honest check for a proxy: which
135
- account, project, or mode the key reaches is the provider's answer, not
136
- Connecta's.
137
-
138
- ## Downstream OAuth
139
-
140
- `remoteMcp()` stores dynamic client registration, tokens, PKCE material, state,
141
- and the pending authorization URL in the connector's storage namespace.
142
- Registration and token envelopes are bound to the validated authorization
143
- server `issuer`. An unbound pre-0.9 envelope is upgraded in place on its first
144
- issuer-aware read, preserving the existing grant.
145
-
146
- For personal OAuth, the authorization handoff also stores a 15-minute mapping
147
- from a SHA-256 digest of `state` to the principal partition. The public callback
148
- uses that mapping before it verifies state or exchanges the code. Neither the
149
- browser nor a callback parameter can select a principal. The callback deletes
150
- the mapping before it exchanges the code, so a second callback cannot replay
151
- the principal handoff in strongly consistent storage. Cloudflare KV deletion
152
- is eventually consistent, so handoff consumption there is best-effort across
153
- PoPs; the downstream authorization code remains single-use. If the callback
154
- request also carries an interactive identity, Connecta refuses it when that
155
- principal did not start the flow.
156
-
157
- If later discovery resolves a different issuer, Connecta does not send the old
158
- client identifier or tokens to it. The provider publishes a new generation
159
- epoch, makes every older credential namespace unreadable, cleans up the retired
160
- values, and lets the SDK begin registration and authorization again. The same
161
- epoch fence prevents an older isolate or late token exchange from resurrecting
162
- the retired grant.
163
-
164
- The OAuth callback verifies the one-shot `state` first, then hands the complete
165
- query string—including RFC 9207 `iss`—to the SDK transport. One-shot state,
166
- verifier, and pending URL are cleared only after a successful exchange.
167
-
168
- Within one `remoteMcp()` runtime, one request scope owns refresh-token
169
- redemption for an OAuth generation. Concurrent scopes wait for the owner's
170
- token save or bounded failure, then either read storage again or receive that
171
- failure. A scope that had already read the retired refresh token reuses the
172
- newly stored rotating token locally instead of sending the retired value
173
- upstream. Force reauthorization retires the old generation's gate, and a
174
- failed flow releases ownership for a later attempt. The coordinator retains
175
- only a completion signal and one temporary owner-abort listener until that
176
- exact flight settles, never the token response or downstream transport. A
177
- follower may stop waiting when its own request is cancelled without cancelling
178
- the owner or poisoning the generation for later callers. If the owner's
179
- credential mutation fails, joined callers receive that same bounded failure
180
- instead of waking to redeem the unchanged token; a later independent call may
181
- retry. Non-success and malformed token responses settle current waiters at the
182
- fetch boundary, before any later authorization callback can itself fail.
183
- Cancelling the owner aborts its fetch and fails current joiners rather than
184
- promoting one: once a request reaches the authorization server, repeating its
185
- old refresh token is not known to be safe. If that cancellation lands while
186
- the valid response's credential write is already running, a same-generation
187
- attempt receives `temporarily_unavailable` until the exact write succeeds or
188
- fails. This mutation marker contains no retained promise; force
189
- reauthorization removes it when the old generation becomes unreadable.
190
- An additional opaque success identity lets a request recognize a refresh that
191
- completed after its issuer-aware token read even when the authorization server
192
- returned byte-identical credentials. The identity is generation-scoped and is
193
- discarded with the retired generation. Every authoritative storage-generation
194
- read also retires coordinator state from other epochs, so an externally
195
- advanced generation cannot be overwritten in runtime state by late old work.
196
-
197
- This guarantee is runtime-local. `KVStorage` has no atomic lock or
198
- compare-and-set operation, so separate processes or Worker isolates can still
199
- redeem the same refresh token concurrently. Generation envelopes continue to
200
- fence their writes, but Connecta does not claim cross-isolate exactly-once
201
- refresh.