@zackbart/connecta 0.6.0 → 0.7.0

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 (72) hide show
  1. package/CHANGELOG.md +403 -0
  2. package/README.md +163 -308
  3. package/dist/auth/bearer.d.ts +4 -3
  4. package/dist/auth/bearer.d.ts.map +1 -1
  5. package/dist/auth/bearer.js +10 -8
  6. package/dist/auth/bearer.js.map +1 -1
  7. package/dist/auth/clerk.d.ts +8 -7
  8. package/dist/auth/clerk.d.ts.map +1 -1
  9. package/dist/auth/clerk.js +27 -8
  10. package/dist/auth/clerk.js.map +1 -1
  11. package/dist/connector-scope.d.ts +13 -0
  12. package/dist/connector-scope.d.ts.map +1 -0
  13. package/dist/connector-scope.js +35 -0
  14. package/dist/connector-scope.js.map +1 -0
  15. package/dist/connectors/api.d.ts +5 -5
  16. package/dist/connectors/api.d.ts.map +1 -1
  17. package/dist/connectors/remote-mcp.d.ts +3 -3
  18. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  19. package/dist/connectors/remote-mcp.js +309 -10
  20. package/dist/connectors/remote-mcp.js.map +1 -1
  21. package/dist/credential-health.d.ts +20 -9
  22. package/dist/credential-health.d.ts.map +1 -1
  23. package/dist/credential-health.js +127 -63
  24. package/dist/credential-health.js.map +1 -1
  25. package/dist/credentials.d.ts +84 -1
  26. package/dist/credentials.d.ts.map +1 -1
  27. package/dist/credentials.js +109 -2
  28. package/dist/credentials.js.map +1 -1
  29. package/dist/index.d.ts +83 -82
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +101 -31
  32. package/dist/index.js.map +1 -1
  33. package/dist/meta-tools.d.ts +3 -3
  34. package/dist/meta-tools.d.ts.map +1 -1
  35. package/dist/meta-tools.js +16 -7
  36. package/dist/meta-tools.js.map +1 -1
  37. package/dist/registry.d.ts +3 -2
  38. package/dist/registry.d.ts.map +1 -1
  39. package/dist/registry.js +4 -3
  40. package/dist/registry.js.map +1 -1
  41. package/dist/server.d.ts +1 -1
  42. package/dist/server.d.ts.map +1 -1
  43. package/dist/server.js +154 -52
  44. package/dist/server.js.map +1 -1
  45. package/dist/skills.js +2 -2
  46. package/dist/skills.js.map +1 -1
  47. package/dist/toolkits.js +1 -1
  48. package/dist/types.d.ts +51 -26
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/ui.d.ts +52 -21
  51. package/dist/ui.d.ts.map +1 -1
  52. package/dist/ui.js +665 -196
  53. package/dist/ui.js.map +1 -1
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +3 -2
  57. package/src/auth/bearer.ts +10 -8
  58. package/src/auth/clerk.ts +28 -9
  59. package/src/connector-scope.ts +41 -0
  60. package/src/connectors/api.ts +5 -5
  61. package/src/connectors/remote-mcp.ts +348 -25
  62. package/src/credential-health.ts +151 -71
  63. package/src/credentials.ts +166 -3
  64. package/src/index.ts +202 -113
  65. package/src/meta-tools.ts +22 -7
  66. package/src/registry.ts +4 -3
  67. package/src/server.ts +197 -71
  68. package/src/skills.ts +2 -2
  69. package/src/toolkits.ts +1 -1
  70. package/src/types.ts +51 -26
  71. package/src/ui.ts +703 -195
  72. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,409 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.7.0 — 2026-07-27
6
+
7
+ 0.7.0 is the surface settlement: one release that finishes moving connecta's
8
+ public shape so that it can stop moving. It arrives from two directions at once.
9
+ A controlled qualification pass against 0.6.1 (issue #86) measured four ways a
10
+ deployment could be worn down — an unbounded cache, an enumerable callback, a
11
+ session leak, and a credential that reports itself usable and then isn't — and
12
+ those are fixed here. Meanwhile the config surface had accumulated eleven flat
13
+ tuning fields, the reference manual had reached 2,600 lines, and the operator UI
14
+ was one page pretending to be three. Those are settled here too, and settling
15
+ them breaks things, which is the argument for doing it in one release instead of
16
+ spreading it across three.
17
+
18
+ **What breaks.** Three things, and each is a compile error or a loud refusal
19
+ rather than a behavior change you have to notice on your own. Every flat 0.6.x
20
+ tuning option is removed in favour of four grouped ones. A connector that
21
+ implements `finishAuth` without `verifyState` can no longer complete OAuth. And
22
+ `/`, `/credentials`, and `/activity` are now core-owned routes, so a connector
23
+ `handleRequest` that served any of them is shadowed. The first is mechanical:
24
+ `createConnecta` throws once, before it reads anything else, naming every legacy
25
+ path it found and its replacement.
26
+
27
+ **What a deployment can ignore.** The defaults and runtime behavior of every
28
+ config option are unchanged — only the paths moved. `/ui` bookmarks keep working
29
+ through a permanent redirect, and all 17 documentation anchors still resolve
30
+ even though the manual is now nine files. If you configure connecta with a
31
+ literal object, do not paginate a downstream MCP server, and write your own
32
+ connectors against `remoteMcp`/`api` rather than hand-rolling `finishAuth`, the
33
+ whole migration is a mechanical rewrite of one config block.
34
+
35
+ ### Breaking
36
+
37
+ - **`ConnectaConfig` tuning is grouped by subsystem for 0.7.0** (issue #28).
38
+ The flat 0.6.x options are removed rather than deprecated: TypeScript rejects
39
+ them, and JavaScript callers get one fail-fast error from `createConnecta`
40
+ listing every legacy own property it finds and its replacement. Supplying
41
+ both an old and a new path is still an error; there is no precedence rule or
42
+ compatibility alias. Migrate configuration as follows:
43
+
44
+ | 0.6.x | 0.7.0 |
45
+ |---|---|
46
+ | `activity` | `activity.store` |
47
+ | `activityReadGate` | `activity.readGate` |
48
+ | `activityDeploymentId` | `activity.deploymentId` |
49
+ | `credentialEncryptionKey` | `credentials.encryptionKey` |
50
+ | `credentialHealth` | `credentials.health` |
51
+ | `toolCacheTtlSeconds` | `discovery.catalogTtlSeconds` |
52
+ | `persistToolCatalog` | `discovery.persistCatalog` |
53
+ | `toolCatalogStaleSeconds` | `discovery.staleCatalogSeconds` |
54
+ | `probeTimeoutMs` | `discovery.probeTimeoutMs` |
55
+ | `defaultToolTimeoutMs` | `calls.defaultTimeoutMs` |
56
+ | `maxResultBytes` | `calls.maxResultBytes` |
57
+
58
+ For example:
59
+
60
+ ```ts
61
+ // 0.6.x
62
+ createConnecta({
63
+ connectors,
64
+ storage,
65
+ activity,
66
+ credentialEncryptionKey: env.CONNECTA_CREDENTIAL_KEY,
67
+ toolCacheTtlSeconds: 300,
68
+ defaultToolTimeoutMs: 30_000,
69
+ maxResultBytes: 50_000,
70
+ });
71
+
72
+ // 0.7.0
73
+ createConnecta({
74
+ connectors,
75
+ storage,
76
+ activity: { store: activity },
77
+ credentials: { encryptionKey: env.CONNECTA_CREDENTIAL_KEY },
78
+ discovery: { catalogTtlSeconds: 300 },
79
+ calls: { defaultTimeoutMs: 30_000, maxResultBytes: 50_000 },
80
+ });
81
+ ```
82
+
83
+ The defaults and runtime behavior of each option are unchanged. Connector
84
+ definitions keep their per-connector `maxResultBytes`, and structural seams
85
+ such as `storage`, `auth`, `connectors`, `toolkits`, and `executor` remain at
86
+ the top level.
87
+ - **A connector implementing `finishAuth` without `verifyState` can no longer
88
+ complete OAuth** (issue #62). Such a connector previously exchanged the
89
+ authorization code with no CSRF guard at all — connecta had no way to
90
+ establish that it had started the flow being completed. The callback now
91
+ refuses with the same opaque 400 as every other refusal, exchanges no code,
92
+ and logs one operator-grade line naming the connector and the missing hook.
93
+ `verifyState` stays optional in the type system and is required in practice
94
+ wherever `finishAuth` is present. The shipped `remoteMcp` OAuth provider has
95
+ always implemented it, so this reaches hand-written connectors only.
96
+ - **`/`, `/credentials`, and `/activity` are now core-owned routes** (issue
97
+ #57). They previously fell through to connector `handleRequest` and then to a
98
+ 404, so a connector serving any of the three is now shadowed without warning.
99
+ `GET /` returns the operator shell where 0.6.1 returned 404, and non-GET on
100
+ those routes and on `/ui` returns 405 rather than falling through.
101
+
102
+ ### Added
103
+
104
+ - **`Connector.closeScope?(ctx)`** (issue #66) — an optional, best-effort seam
105
+ for releasing whatever a connector opened for one scope. Called at most once,
106
+ inside a fixed 100 ms completion window: a hook that is absent, throws, or
107
+ never settles cannot replace, corrupt, or delay the result of the operation
108
+ that triggered it. `remoteMcp` implements it, tearing down cached and
109
+ half-open sessions and fencing late connection races. Connectors that do not
110
+ implement it are unaffected.
111
+
112
+ ### Changed
113
+
114
+ - **The operator UI is now a direct-linkable three-page set** (issue #57):
115
+ Connections at `/`, Credentials at `/credentials`, and Activity at
116
+ `/activity`. `GET /ui` remains as a permanent `308` compatibility redirect to
117
+ `/`. The pages use semantic navigation, path-specific titles, direct
118
+ load/refresh/bookmark and Back/Forward behavior, and the same restrained
119
+ responsive connector-ledger design. They are three routes over one
120
+ no-build-step client implementation, not copied applications.
121
+ - **Every canonical operator route serves the same open, data-free shell.** The
122
+ nonce CSP, framing denial, referrer policy, HSTS behavior, content-type
123
+ protection, URL gates, and escaping guarantees formerly attached to `/ui`
124
+ apply equally to `/`, `/credentials`, and `/activity`. Deployment data remains
125
+ behind the unchanged private APIs: `/ui/data`, `/ui/activity`, and
126
+ `/ui/credentials/<connectorId>[/test]`.
127
+ - **Vault controls moved out of connector cards and into Credentials.**
128
+ Connections remains read-only status; Credentials preserves Add/Replace/Test/
129
+ Remove, masked metadata, stored-shape recovery, and live feedback without ever
130
+ returning a secret. Its explicit capability states cover eligible Clerk
131
+ operators, bearer-only sessions, missing vaults, and deployments with no
132
+ credential slots without broadening the existing Clerk/user-id/toolkit/
133
+ same-origin mutation boundary. Activity likewise renders an explicit
134
+ not-configured state when `activity.store.list` is absent. OAuth result pages
135
+ now return to `/`.
136
+ - **The reference manual is nine documents instead of one** (issue #61).
137
+ `docs/documentation.md` had reached 2,600 lines; it is now a compatibility
138
+ index that preserves all 17 `#N-…` anchors, so every existing deep link from
139
+ the README, the examples, and source comments still resolves. The prose moved
140
+ to `architecture`, `meta-tools`, `connectors`, `auth`,
141
+ `storage-and-credentials`, `operations`, `code-mode`, `operator-ui`, and
142
+ `toolkits`. A new `npm run check:docs` validates local links, fragments,
143
+ heading slugs, and the legacy-anchor manifest, and runs first in
144
+ `npm run check`. Contributor-facing only: the published package is unchanged
145
+ and docs still ship in no tarball.
146
+
147
+ ### Fixed
148
+
149
+ - **`clerkAuth`'s identity-decision cache is bounded at 1,024 identities per
150
+ instance** (issue #70). It never evicted: every distinct identity that reached
151
+ the gate was retained for the lifetime of the isolate. `allowedDomains` made
152
+ that attacker-reachable on an open-signup Clerk instance, since an
153
+ authenticated-but-denied identity is still a cache entry — 100,000 denied
154
+ identities measured at roughly 11.96 MiB retained, with the oldest entry still
155
+ present. Eviction is LRU, so a small active set stays hot, and the only thing
156
+ evicting an entry can cause is a fresh check against Clerk: it can never turn
157
+ a denial into an admission, and it cannot extend an allow past its TTL, since
158
+ a cache hit re-inserts the same record rather than restamping it. The bound is
159
+ deliberately fixed rather than an operator knob, and the ~60 s allow / ~30 s
160
+ deny windows are unchanged.
161
+ - **`/oauth/callback/<id>` no longer lets an unauthenticated caller enumerate
162
+ configured connector ids** (issue #62). 0.6.1 answered `404 Unknown connector
163
+ "<id>"` for an id it did not recognise and a distinct 400 for a state
164
+ mismatch, which made the callback a free directory of every connector a
165
+ deployment had configured. Unknown ids, non-OAuth connectors, missing or
166
+ mismatched state, an absent verifier, and a throwing verifier now return one
167
+ byte-identical 400 — same status, same body, same headers. They also cost the
168
+ same single storage read, so the clock cannot sort the ids the body refuses to
169
+ name. That is cost equalisation rather than constant time, and the
170
+ documentation says so and enumerates what stays distinguishable. The precise
171
+ diagnosis moved to the operator log, bounded and escaped.
172
+ - **Probe-opened downstream MCP sessions are closed instead of abandoned**
173
+ (issue #66). Credential-health sweeps, `list_connectors({ probe: true })`, and
174
+ `/ui/data` each opened downstream sessions and left them for the provider to
175
+ age out — measured at 200 probes opening 200 sessions and explicitly closing
176
+ zero, made continuous by the periodic sweep. Closing now ends the session
177
+ rather than merely dropping the local client: `remoteMcp` issues the
178
+ specification's `DELETE` carrying `Mcp-Session-Id` *before* the close that
179
+ would otherwise abort it, feature-detected and best-effort, while a stateless
180
+ downstream issues none. A downstream that refuses, errors, or never answers is
181
+ closed anyway. One consequence worth knowing: a probing `list_connectors` now
182
+ runs on its own scope rather than the request's — it has to, since a request
183
+ scope cannot be closed — so a request that probes *and* then calls the same
184
+ connector opens two downstream sessions where it opened one. Per-request
185
+ `/mcp` scopes are unchanged.
186
+ - **A credential stored under an older declaration no longer reports itself
187
+ usable and then fails** (issue #69). `/ui`, the credential Test route, and
188
+ credential health each interpreted the stored shape slightly differently, so a
189
+ credential could render as configured and testable and then answer 409 with a
190
+ misleading configure-first message. One pure classifier now answers for all
191
+ three, and it asks about *containment* rather than equality: a stored set is
192
+ fine as long as it holds every field the declaration currently names. A
193
+ missing declared field is drift — which covers every case issue #69 reported,
194
+ including a rename and a swap between the single-value and named shapes in
195
+ either direction — and drift is never auto-migrated. Credentials keeps Replace
196
+ and Remove and hides Test, the test route answers 409 without calling a hook,
197
+ and health records an explicit error rather than a fabricated verdict, checked
198
+ before the freshness gate so a stale `ok` cannot mask a redeploy. Leftover
199
+ keys are explicitly *not* drift: dropping a field from a declaration leaves
200
+ its secret in the vault, but every accessor keeps returning the right value
201
+ and every call keeps working, so calling that drift would tell an operator to
202
+ re-enter a credential that works and that many providers will not reissue in
203
+ readable form. Credentials prints one non-blocking line naming the leftovers
204
+ instead. A repeated drift verdict is charged to the freshness budget rather
205
+ than rewritten on every sweep in every isolate.
206
+ - **A paginated downstream MCP server no longer loses everything after its
207
+ first `tools/list` page** (issue #77). `tools/list` is cursor-paginated in the
208
+ MCP specification and the SDK's `Client.listTools()` returns one page,
209
+ `nextCursor` included, without following it. `remoteMcp()` called it once, so
210
+ a downstream that paged its catalog was split in half without complaint:
211
+ page-one tools were counted, searchable, describable and callable, and every
212
+ tool after them appeared not to exist — missing from `list_connectors` counts,
213
+ `search_tools`, `describe_tools`, and address resolution alike, with nothing
214
+ in any log saying a page had been left behind. That is the wrong failure for
215
+ a product whose premise is progressive discovery over large tool catalogs,
216
+ since large catalogs are exactly the ones that paginate. A catalog refresh now
217
+ walks the whole cursor chain on the same request-scoped client before the
218
+ result is indexed. Cursors are handed back byte-for-byte and never parsed,
219
+ rewritten, or persisted; an empty-string `nextCursor` is treated as *present*
220
+ (pagination ends on an absent one, and a truthiness check there would truncate
221
+ the catalog silently). A downstream that advertises no `nextCursor` is
222
+ unchanged in both directions: one request, sent with no `cursor` param, and
223
+ byte-identical tool definitions. Tools are deduplicated by name, first page
224
+ wins, so an unstable cursor that overlaps pages cannot inflate `toolCount`,
225
+ double a `search_tools` row, or churn the registry into a persistence write
226
+ per refresh. A `nextCursor: null` — a common JSON idiom for end-of-pagination
227
+ that the MCP result schema does not accept — is now reported as a named
228
+ nonconformance instead of a raw Zod dump. Any page that fails fails the whole
229
+ refresh rather than publishing a prefix, so the registry keeps serving the
230
+ last complete catalog through its existing stale fallback.
231
+
232
+ The walk is bounded on **tools**, not pages. A page ceiling is the wrong
233
+ dimension — the server picks the page size, so N pages is N × a number
234
+ connecta cannot observe, and the common conformant idiom of "advertise a
235
+ cursor whenever the page came back full, then serve one empty page to
236
+ terminate" means a well-behaved 10,000-tool server paging at 100 spends 101
237
+ requests. So a refresh now fails immediately on a cursor handed back twice,
238
+ fails on two consecutive pages that add nothing while still promising more
239
+ (one is legal — that empty terminator), caps what it accumulates at
240
+ `MAX_TOOLS` (100,000, the top of the catalog envelope issue #82 benchmarks),
241
+ and keeps `MAX_TOOL_PAGES` (raised to 10,000) only as an unreachable runaway
242
+ backstop. These bounds make an unterminating walk finite and report it as a
243
+ connector error; they do not cancel one that a probe deadline has already
244
+ abandoned, because `withTimeout` bounds the caller's wait rather than the
245
+ work.
246
+
247
+ - **Downstream output-schema validation now covers every page of a paginated
248
+ catalog, not just the last one** (issue #77). The MCP SDK's
249
+ `Client.listTools()` rebuilds its output-schema validators and task-support
250
+ sets from each page it receives, clearing them first — so walking the cursor
251
+ chain left the request-scoped client validating the final page alone. A
252
+ `call_tool` against any earlier-page tool then found no validator and
253
+ silently skipped both the "declared an `outputSchema` but returned no
254
+ `structuredContent`" check and the structured-content validation, and lost
255
+ the required-task guard with them. Enforcement depended on which page a tool
256
+ happened to land on. A completed walk now re-primes that cache once from the
257
+ full aggregated catalog; a test asserts the pinned SDK still provides the
258
+ method it reaches for, so a future bump fails CI rather than quietly
259
+ restoring the gap.
260
+
261
+ ## 0.6.1 — 2026-07-26
262
+
263
+ A patch release: three bug fixes and a documentation overhaul. No new
264
+ configuration, no new exported types, no change to the published API surface —
265
+ `ConnectaConfig` and every type are byte-identical to 0.6.0. Nothing here is
266
+ breaking. But two of the fixes *withdraw* behavior a deployment could have been
267
+ relying on by accident, and in both cases withdrawing it is what the fix is, not
268
+ a decision taken alongside it:
269
+
270
+ - **A `uiAuth.signInUrl` or `signUpUrl` that is not an absolute `https:` URL is
271
+ now dropped** rather than handed to `Clerk.load` (issue #56). Relative paths,
272
+ `http:`, protocol-relative, `javascript:` and `data:` values all fall together,
273
+ under the same gate `frontendApiUrl` has always passed. A rejected value reaches no part of the rendered page: the key is
274
+ simply absent from `/ui`'s inline `AUTH` object, so Clerk falls back to its own
275
+ default exactly as it does for an unset field, and `/ui` still renders and
276
+ still signs operators in. Construction logs one warning naming each dropped
277
+ field. What belongs in these fields is a hosted Account Portal address
278
+ (`https://accounts.<domain>`, `https://<slug>.accounts.dev`), so a deployment
279
+ that noticed this at all was pointing operators at a sign-in page connecta does
280
+ not host.
281
+ - **The declared credential shape now picks the credential test hook, on every
282
+ surface** (issue #55). Two of the four shape/hook combinations change behavior.
283
+ A **single-value `credential` declaring both hooks** now runs
284
+ `testCredential(value)` with the raw string, where the test route previously
285
+ preferred `testCredentials` and handed it the vault's reserved `{ value }` map
286
+ — the single-value hook now receives the string it was written to expect
287
+ instead of a one-entry map named after a storage detail. A **single-value
288
+ `credential` declaring only `testCredentials`** is no longer tested or probed
289
+ at all: /ui renders no Test button, a direct POST answers 400 naming the
290
+ mismatch, `createConnecta` warns at construction, and the liveness sweep added
291
+ in 0.6.0 reports it `not_checkable` instead of calling a hook with a shape its
292
+ connector never declared. Both of those are the fallback order that *was* the
293
+ bug, so there is no version of this fix that preserves them. Named `fields`
294
+ with `testCredentials`, or with both hooks, behave exactly as they did.
295
+
296
+ One textual delta reaches every deployment and no configuration turns it off, so
297
+ anything snapshotting agent-facing error text will diff: `resolveSkill`'s two
298
+ `Available:` branches now say `Available skills:` like the other three (issue
299
+ #50).
300
+
301
+ The docs are substantially reorganized, and one of the changes ships to npm:
302
+ readers of the package page get a completely different README. See Changed. The
303
+ next intentional breaking release stays reserved for issue #28.
304
+
305
+ ### Changed
306
+
307
+ - **The README is rewritten around the problem connecta solves** (345 lines to
308
+ 196). It had become a condensed reference manual — roughly 60% code blocks and
309
+ option tables, every one of them duplicated in `docs/documentation.md` or
310
+ `examples/` — with a single paragraph explaining why connecta exists. npm
311
+ readers see only this file, so it was the worst place for the duplication and
312
+ the best place for the argument. It now runs as prose: the context-window and
313
+ per-client configuration costs of connecting an agent to N MCP servers, what
314
+ connecta does about them, why it is shaped the way it is (config as code, a
315
+ server-side credential vault with liveness checks, fail-closed read-only,
316
+ toolkits and who is admitted to them, payload-free activity, a read-only
317
+ operator dashboard), when *not* to use it, one minimal Node quickstart, and
318
+ links out. Removed from it and unchanged in `docs/` and `examples/`: the
319
+ nine-row signature-level meta-tool table (the nine names now appear in prose
320
+ with their purposes), the Worker quickstart, and the toolkits, code-mode,
321
+ credentials, activity and branding blocks with their option semantics.
322
+ **Links out of the README are now absolute GitHub URLs**, because `docs/` does
323
+ not ship in the package and relative links are dead on npmjs.com. This is the
324
+ only change in the release that reaches npm as content rather than code.
325
+ - **`docs/design.md` is retired into a new `docs/decisions.md`.** design.md was
326
+ mostly a worse copy of documentation.md, and it had gone stale in at least one
327
+ load-bearing way: it claimed the Clerk adapter passes `authorizedParties:
328
+ [connectaOrigin]` to `authenticateRequest`. It does not, and must not — OAuth
329
+ access tokens may be JWTs with no `azp` claim, and Clerk rejects `azp ===
330
+ undefined` when that option is set, so passing it would reject every MCP
331
+ client; the `azp` pin is applied by hand after verification, for session tokens
332
+ only. decisions.md records that as a **rejected alternative** rather than
333
+ repeating the wrong version. It answers two questions documentation.md does
334
+ not: "may I build X?" (non-goals, rejected alternatives) and "must my change
335
+ preserve Y?" (invariants) — collecting the invariants documentation.md states
336
+ but never gathers as pointers rather than duplicated prose, alongside the four
337
+ that lived only in design.md: no runtime admin, nothing request-bound surviving
338
+ a request, single tenant, and fail-closed read-only. Two facts moved into
339
+ documentation.md §13 instead, next to the code they explain. Inbound references
340
+ in the README, documentation.md and the Docker example follow; **any external
341
+ link to `docs/design.md` is now dead**, and the 0.x CHANGELOG mentions of it
342
+ stay as historical record. The credential-test invariant added by issue #55
343
+ also gained the qualifier §17 already carried: a connector whose shape and hook
344
+ mismatch carries no verdict *from a credential hook*, but is still probed
345
+ through `status()` if it declares one, since that question never involves the
346
+ shape.
347
+ - **CLAUDE.md is an agent brief rather than a policy stub.** It now states what
348
+ gates "done" (`npm run check`, and what that runs), the two-document map
349
+ (documentation.md as reference manual, decisions.md as non-goals and
350
+ invariants) with the warning that its section numbers are linked from source
351
+ comments, the two CI-enforced boundaries that are invisible from inside a
352
+ single file (import-graph purity, the published surface) as where-new-code-goes
353
+ guidance, the `WORKERS_SUITES` allowlist trap that silently skips a portable
354
+ suite left out of it, and the commit, CHANGELOG and release conventions. The
355
+ roadmap-lives-in-GitHub-issues policy is unchanged, verbatim.
356
+
357
+ ### Fixed
358
+
359
+ - **One rule decides how a credential is tested, and three copies of it no
360
+ longer disagree** (issue #55). `/ui` offered a Test button from the mere
361
+ presence of a hook (`testCredential || testCredentials`); the test route made
362
+ its own different choice — prefer `testCredentials`, else `testCredential` on
363
+ the vault's reserved `value` field; and 0.6.0's credential-health prober added
364
+ a third copy of that same preference order. They disagreed in both mismatch
365
+ shapes, and the visible bug was the ugliest one: a connector declaring named
366
+ `credential.fields` with only `testCredential` answered **409 "configure the
367
+ credential before testing it" on a fully configured credential**, blaming the
368
+ operator for connecta's own hook selection. `credentialTestRule` in
369
+ `src/credentials.ts` is now the single source of truth — `buildUiData`'s
370
+ `testable` flag, the test route's hook selection, the credential-health
371
+ prober's `testHookFor`/`isCheckableConnector`, and a new construction-time
372
+ warning all read it, so they cannot drift apart again. **The declared shape
373
+ picks the hook and the other one is never substituted**: named fields are
374
+ tested by `testCredentials` with the whole set, a single value by
375
+ `testCredential` with the raw string. A connector implementing only the
376
+ mismatched hook is not testable rather than testable-by-accident — no button, a
377
+ 400 sharing its wording with the boot warning, and `not_checkable` to the
378
+ prober, which carries no verdict rather than an invented one (handing
379
+ `testCredential` a `value` that named fields never wrote would test the empty
380
+ string and record a confident `auth_required` about a credential nothing
381
+ examined). `status()` is unaffected: a mismatched connector that implements it
382
+ is still probed through it. The two behavior deltas this produces are stated
383
+ above. Recorded as an invariant in `docs/decisions.md`, and stated in
384
+ documentation.md §4, §7, §14 and §17.
385
+ - **`uiAuth.signInUrl` and `signUpUrl` are gated like every other URL that
386
+ reaches the browser** (issue #56). They were the residual — operator config
387
+ that arrives in the page as a *navigation target* rather than an attribute, so
388
+ no gate covered them and §14's URL-position invariant had to be read with an
389
+ exception beside it. Both are serialized into `/ui`'s inline `AUTH` object and
390
+ handed to `Clerk.load`, which navigates to them when an operator signs in. They
391
+ now pass the same gate `frontendApiUrl` does, so `isSafeScriptSrcUrl` — named
392
+ for the one position it used to guard — becomes `isSafeHttpsUrl`, one predicate
393
+ for all three `uiAuth` URLs. Absolute `https:` only, for the typed fields as
394
+ for the derived one: the loose carve-outs buy nothing real, since `http:` would
395
+ carry a sign-in over cleartext and a path relative to this origin is
396
+ meaningless because connecta hosts no sign-in page of its own. The drop warning
397
+ decides "did the operator mean to supply this?" through the same `isSetUrlValue`
398
+ helper `branding` uses, so a blank-string or falsy non-string value cannot warn
399
+ on one path and stay silent on the other. **§14 now states the invariant over
400
+ every URL position — attribute and navigation target — with no carve-out left.**
401
+ - **`resolveSkill` enumerates its list under one label** (issue #50). Three
402
+ branches said `Available skills:` and two said `Available:`, so which one an
403
+ agent saw depended on the branch it hit rather than on any difference in
404
+ meaning. Agent-facing error text is interface, and two labels read as two
405
+ concepts where there is one. Both stragglers now say `Available skills:`, and a
406
+ test walks all five error branches so reverting any one of them fails.
407
+
5
408
  ## 0.6.0 — 2026-07-26
6
409
 
7
410
  A feature release that makes two of 0.5.0's mechanisms protective rather than