@zackbart/connecta 0.24.4 → 0.25.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.
- package/AGENTS.md +40 -10
- package/CHANGELOG.md +253 -0
- package/dist/advertised-schema.js +53 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +141 -105
- package/dist/catalog.js +258 -33
- package/dist/concurrency.js +24 -23
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +317 -241
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/execute.js +363 -257
- package/dist/executor-admission.js +67 -50
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +251 -210
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +51 -20
- package/dist/invocation.js +324 -285
- package/dist/meta-tools.js +206 -92
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +46 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +38 -2
- package/dist/program-source.js +205 -0
- package/dist/providers/notion.js +2 -2
- package/dist/providers/revenuecat.js +32 -5
- package/dist/providers/stripe.d.ts +3 -2
- package/dist/providers/vercel.js +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +305 -260
- package/dist/routes/activity.js +120 -137
- package/dist/routes/credentials.js +174 -163
- package/dist/routes/mcp.js +129 -70
- package/dist/routes/oauth-management.js +60 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +82 -0
- package/dist/routes/shared.d.ts +2 -0
- package/dist/routes/ui-data.js +210 -0
- package/dist/routes/ui.js +64 -32
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +111 -89
- package/dist/skills.js +1 -1
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +17 -47
- package/dist/types.d.ts +27 -7
- package/dist/ui.d.ts +16 -2
- package/dist/ui.js +50 -166
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +184 -10
- package/documentation/auth.md +80 -0
- package/documentation/code-mode.md +69 -24
- package/documentation/meta-tools.md +164 -27
- package/ethos.md +44 -47
- package/examples/worker/README.md +51 -5
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +6 -6
- package/examples/worker/wrangler.jsonc +16 -4
- package/package.json +11 -3
- package/templates/node/package.json +1 -1
- package/dist/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -96
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -188
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- package/dist/timeout.d.ts +0 -21
- package/dist/tool-safety.d.ts +0 -9
|
@@ -12,24 +12,29 @@ credential administration belong to [auth](./auth.md).
|
|
|
12
12
|
## The seven tools
|
|
13
13
|
|
|
14
14
|
Every deployment requires an executor, so `tools/list` is exactly seven. No
|
|
15
|
-
configuration adds an eighth or removes one.
|
|
15
|
+
configuration adds an eighth or removes one. Resumable writes plan an
|
|
16
|
+
unconditional eighth, the destructive-annotated `resume_execution`
|
|
17
|
+
([ethos](../ethos.md#decisions)); `execute_code` keeps its read-only hint,
|
|
18
|
+
because writes will run only there or for config-exempt tools.
|
|
16
19
|
|
|
17
20
|
| Tool | Arguments | Returns |
|
|
18
21
|
| --- | --- | --- |
|
|
19
22
|
| `execute_code` | `code`, `diagnostics?` | the program's reduced return value, plus a `diagnostics` block when asked |
|
|
20
|
-
| `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json"` | `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
|
|
23
|
+
| `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
|
|
21
24
|
| `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
|
|
22
25
|
| `call_destructive_tool` | the same, plus `reason?` | the same |
|
|
23
26
|
| `authorize_connector` | `connector`, `force?` | the class-specific handoff in [authorization recovery](#authorization-recovery) |
|
|
24
|
-
| `get_result` | `id`, `offset?`, `maxBytes?` | `{ offset,
|
|
27
|
+
| `get_result` | `id`, `offset?`, `maxBytes?` | a one-line JSON header `{ resultId, offset, bytes, totalBytes, hasMore, nextAction? }`, a newline, then the page as raw text ([paging](#paging-with-get_result)) |
|
|
25
28
|
| `skills` | `name?` | the listing when `name` is absent, that skill's markdown when it is present |
|
|
26
29
|
|
|
27
30
|
`limit` defaults to 8 and is capped at 100, as is one `connecta.describe` batch.
|
|
28
31
|
`get_result.offset` is a whole number of bytes ≥ 0 defaulting to 0 and
|
|
29
|
-
`maxBytes` a whole number ≥ 1
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
`maxBytes` a whole number ≥ 1 that defaults to, and is clamped to, the inline
|
|
33
|
+
cap of the call that stashed the result — 24,000 bytes unless
|
|
34
|
+
`calls.maxResultBytes` or a per-connector override says otherwise
|
|
35
|
+
([why 24,000](#truncated-direct-call-results)). A value outside those domains is
|
|
36
|
+
an input error; a valid `maxBytes` above the cap is an upper bound, not an
|
|
37
|
+
error ([why](#paging-with-get_result)). `reason` is at most 500 characters of context for the host's human
|
|
33
38
|
approval view; Connecta neither treats it as authority nor sends it downstream,
|
|
34
39
|
and an empty or whitespace-only one reads as no reason rather than as grounds to
|
|
35
40
|
refuse a consequential call.
|
|
@@ -106,7 +111,7 @@ discovery. Both take the same arguments.
|
|
|
106
111
|
| `connector` | scopes to one id, loading that catalog alone instead of fanning out across every configured connector. Set it when the integration is obvious, omit it when the right one is genuinely ambiguous |
|
|
107
112
|
| `safety` | `"readOnly"` for what generated code may call, `"approvalRequired"` for the complementary set that must cross `call_destructive_tool`, omitted or `"all"` for the complete configured catalog |
|
|
108
113
|
| `limit` / `offset` | page the ranked results; omit `limit` initially so the default eight-result page stays small |
|
|
109
|
-
| `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema |
|
|
114
|
+
| `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema, `"typescript"` for a function signature ([below](#typescript-signatures)) |
|
|
110
115
|
| `fullDescriptions` | unabridged tool purposes, at the obvious cost |
|
|
111
116
|
|
|
112
117
|
Neither `connector` nor `safety` grants authority or changes invocation
|
|
@@ -141,6 +146,43 @@ act on is the flag: any cap sets `inputSchemaTruncated` or
|
|
|
141
146
|
`outputSchemaTruncated`, and that means repeat with `includeSchemas: "json"`, or
|
|
142
147
|
describe, when the exact constraints matter.
|
|
143
148
|
|
|
149
|
+
### TypeScript signatures
|
|
150
|
+
|
|
151
|
+
`includeSchemas: "typescript"` replaces both schema fields with one
|
|
152
|
+
`signature`: the function `connecta.call(address, args)` resolves to, written
|
|
153
|
+
as a TypeScript type because agents write code against types more reliably
|
|
154
|
+
than against JSON Schema ([executor](https://github.com/UsefulSoftwareCo/executor)'s
|
|
155
|
+
discovery made the same bet).
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
(args: { team: string; limit?: number /* <= 250 */ }) => Promise<unknown>
|
|
159
|
+
(args?: {}) => Promise<{ accounts: { stripe_context: string; livemode: boolean; name?: string }[] }>
|
|
160
|
+
(args: { team: string }) => Promise</* observed, not declared */ { cursor?: string; issues?: { id?: string }[] }>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
It is something to read, not something that runs: programs stay JavaScript,
|
|
164
|
+
and the refusal of erasable TypeScript in `execute_code` stands
|
|
165
|
+
([ethos](../ethos.md#decisions)). The output half is the provider's declared
|
|
166
|
+
schema, else an `S9` observation that opens with the
|
|
167
|
+
`/* observed, not declared */` marker and keeps the `outputSchemaSource`
|
|
168
|
+
field, else `unknown` — runtime evidence never reads as a contract, even when
|
|
169
|
+
the signature is lifted out of its entry. A tool without parameters takes
|
|
170
|
+
`args?: {}`.
|
|
171
|
+
|
|
172
|
+
The walk, byte budgets, and truncation flags are compact's, each half
|
|
173
|
+
separately, and search, describe, and every discovery route share them, so the
|
|
174
|
+
same tool renders the same signature from `search_tools` and `connecta.search`.
|
|
175
|
+
What changes is the dialect: `number` for `integer`, `;` between members,
|
|
176
|
+
quoted non-identifier keys, `T[]` over a parenthesized union, `| null` for
|
|
177
|
+
OpenAPI `nullable` and type lists, index signatures for schema-valued
|
|
178
|
+
`additionalProperties`, `Record<string, unknown>` for an open object with no
|
|
179
|
+
declared properties, and JSDoc for property prose where describe keeps it.
|
|
180
|
+
Where compact would print a bare definition name or raw JSON, the signature
|
|
181
|
+
prints `unknown` with a marker and sets the flag: `/* recursive */` for a
|
|
182
|
+
`$ref` cycle, `/* unresolved */` for a missing target, `/* truncated */` past
|
|
183
|
+
depth four or an exhausted budget. `inputKeys` and friends come from the
|
|
184
|
+
declared schema exactly as they do for compact.
|
|
185
|
+
|
|
144
186
|
## Connector guides and skills
|
|
145
187
|
|
|
146
188
|
A connector may attach a deployment-owned guide as markdown, keeping the
|
|
@@ -202,7 +244,8 @@ text block carrying its compact JSON and then applies the same content size
|
|
|
202
244
|
guard, which preserves structured-only results including `null`, arrays, and
|
|
203
245
|
scalars. An existing text mirror stays unchanged; Connecta adds no second copy.
|
|
204
246
|
Newly stashed JSON and downstream content envelopes use compact serialization,
|
|
205
|
-
|
|
247
|
+
and a lone text block stashes as its own text, so `get_result` offsets and
|
|
248
|
+
totals describe exactly that text.
|
|
206
249
|
|
|
207
250
|
| Bound | Value |
|
|
208
251
|
| --- | --- |
|
|
@@ -244,30 +287,124 @@ original UTF-8 text, not the envelope. A supplied offset inside a character
|
|
|
244
287
|
moves back to its start; page ends also align to character boundaries, and a
|
|
245
288
|
page smaller than one character widens just enough to make progress.
|
|
246
289
|
|
|
247
|
-
A successfully stashed `call_tool` truncation notice carries both the historical
|
|
248
|
-
`resultId` and an exact
|
|
249
|
-
`nextAction: { tool: "get_result", arguments: { id, offset: 0 } }`, so the handle
|
|
250
|
-
is actionable without copying an identifier out of prose. Program results and
|
|
251
|
-
oversized discovery responses carry no such route: paging a program's return
|
|
252
|
-
value is a refused shape, because a program can shrink anything before it
|
|
253
|
-
returns.
|
|
254
|
-
|
|
255
|
-
A refused or failed stash write cannot undo a downstream success. Both call tools
|
|
256
|
-
then return a truncated preview where one is usable, with a paging-unavailable
|
|
257
|
-
notice and no `resultId` or paging action; an envelope carrying non-text blocks
|
|
258
|
-
gets the notice alone, because the head of a half-written base64 image helps
|
|
259
|
-
nobody. Activity records success, and the operator logger receives a fixed
|
|
260
|
-
warning naming the connector and tool without storage error prose. For read-only
|
|
261
|
-
work, reduce the result inside `execute_code` — repeating an approved write is
|
|
262
|
-
not a way to recover its output. Other result-processing failures use a fixed
|
|
263
|
-
`result_processing_failed` message and are never retryable.
|
|
264
|
-
|
|
265
290
|
A per-call `timeoutMs` covers catalog resolution, admission, and connector
|
|
266
291
|
execution under one deadline. The admission queue's own timeout may expire
|
|
267
292
|
sooner but cannot extend the call deadline. Result processing happens after that
|
|
268
293
|
deadline ends, because a completed downstream call must not turn into a
|
|
269
294
|
retryable timeout while Connecta prepares its response.
|
|
270
295
|
|
|
296
|
+
### Truncated direct-call results
|
|
297
|
+
|
|
298
|
+
A `call_tool` or `call_destructive_tool` result over its cap — the connector's
|
|
299
|
+
`maxResultBytes`, else `calls.maxResultBytes`, else 24,000 bytes — comes back
|
|
300
|
+
as one text block. Its first line is the truncation notice, one line of compact
|
|
301
|
+
JSON; everything after the first newline is the preview:
|
|
302
|
+
|
|
303
|
+
```text
|
|
304
|
+
{"truncated":true,"resultId":"…","totalBytes":161420,"hint":"This write already ran: do not call it again to see its result. Bytes 0-24000 of 161420 follow; page the rest with get_result using nextAction.","nextAction":{"tool":"get_result","arguments":{"id":"…","offset":24000}}}
|
|
305
|
+
{"ts":"2026-09-16T21:31:11.759Z","actor":"sam.ortiz@example.com",…
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The notice leads because clients cut oversized results from the end, and a
|
|
309
|
+
notice at the tail is the part they drop. Claude Code (measured on 2.1.280, from
|
|
310
|
+
its bundled source and with `claude -p` against a probe server) passes an MCP
|
|
311
|
+
result through untouched while its text blocks total at most 50,000 characters.
|
|
312
|
+
One character more and it replaces the whole result with a pointer to a spill
|
|
313
|
+
file plus the first 2,000 characters of `JSON.stringify(content, null, 2)`,
|
|
314
|
+
whatever `MAX_MCP_OUTPUT_TOKENS` says. Once the characters exceed twice
|
|
315
|
+
`MAX_MCP_OUTPUT_TOKENS` (25,000 by default, so again 50,000) it also counts real
|
|
316
|
+
tokens against that limit, and a result over it arrives as an error carrying no
|
|
317
|
+
content at all — no preview, no notice. The old layout — a
|
|
318
|
+
50,000-byte preview, then the notice — crossed the first line every time, so an
|
|
319
|
+
agent without file tools never saw the handle; in the eval's write task two of
|
|
320
|
+
three models re-ran a billable export three times trying to read its result.
|
|
321
|
+
|
|
322
|
+
The default cap sits under both lines with room to spare. Preview plus notice
|
|
323
|
+
stays under 25,000 bytes, which is under 50,000 characters for any text, because
|
|
324
|
+
a character is at least one UTF-8 byte, and under 25,000 tokens for any text,
|
|
325
|
+
because a token covers at least one byte. A `get_result` page obeys the same
|
|
326
|
+
cap, and 24,000 matches the program result boundary in
|
|
327
|
+
[code mode](./code-mode.md). A deployment whose
|
|
328
|
+
clients take more can raise the cap, and one whose clients take less can lower
|
|
329
|
+
it, per connector if need be; a result between 24,000 and 50,000 bytes that
|
|
330
|
+
used to arrive whole now pages. A client that spills rather than rejects keeps
|
|
331
|
+
the notice whatever its threshold, since the notice is the first few hundred
|
|
332
|
+
characters. One that rejects outright — Claude Code with
|
|
333
|
+
`MAX_MCP_OUTPUT_TOKENS` set below 12,500 — needs a cap under twice its limit.
|
|
334
|
+
|
|
335
|
+
The preview is the head of what `get_result` pages, cut on a character
|
|
336
|
+
boundary, so `nextAction.arguments.offset` continues exactly where it stops. A
|
|
337
|
+
lone text block — including the one synthesized from `structuredContent` — is
|
|
338
|
+
measured, stashed, previewed, and paged as its text: wrapping it in a content
|
|
339
|
+
envelope added nothing but a second layer of JSON escaping, so a JSON payload
|
|
340
|
+
used to preview as `[{"type":"text","text":"{\"ts\":…` and every page carried
|
|
341
|
+
the escaping twice. Several text blocks keep the serialized envelope as the one
|
|
342
|
+
string measured, stashed, and paged, because their boundaries live there; they
|
|
343
|
+
preview as their text joined by newlines, and their next action starts at
|
|
344
|
+
offset 0. A content array carrying non-text blocks returns the notice alone,
|
|
345
|
+
because the head of a half-written base64 image helps nobody. In value mode the
|
|
346
|
+
notice is `data` itself, with no preview and a next action at offset 0.
|
|
347
|
+
|
|
348
|
+
The hint says what to do next. For a call not explicitly annotated read-only it
|
|
349
|
+
opens with “This write already ran: do not call it again to see its result.”
|
|
350
|
+
The approved write happened; repeating it to look again is how one export
|
|
351
|
+
becomes three. For an explicitly read-only call, repeating is the better route,
|
|
352
|
+
so the hint offers it first: to find something specific, repeat the read inside
|
|
353
|
+
`execute_code` with `connecta.call` and filter or search the result there; to
|
|
354
|
+
read it in full, page with `get_result`. Inside a program the inline cap does
|
|
355
|
+
not apply to a host call's result, only to what the program returns (`L6` in
|
|
356
|
+
[code mode](./code-mode.md) bounds a host call far higher), so the reduction
|
|
357
|
+
sees the whole result at once. When the hint named paging alone, the eval's
|
|
358
|
+
weakest model read a 185 KB CI log in 24,000-byte pages, skipped the range
|
|
359
|
+
holding the real failure, and named the flaky test near the top; before the
|
|
360
|
+
notice was visible, it had reduced the log in a program and found the failure.
|
|
361
|
+
`nextAction` stays the page handle for either kind of call, because paging is
|
|
362
|
+
the one next step connecta can spell out exactly, while a reduction is a
|
|
363
|
+
program the agent has to write. `resultId` stays beside the exact `nextAction`,
|
|
364
|
+
so the handle is actionable without copying an identifier out of prose.
|
|
365
|
+
Program results and oversized discovery responses carry no such route: paging a
|
|
366
|
+
program's return value is a refused shape, because a program can shrink
|
|
367
|
+
anything before it returns.
|
|
368
|
+
|
|
369
|
+
### Paging with get_result
|
|
370
|
+
|
|
371
|
+
A page is the truncated result's shape again: one text block whose first line
|
|
372
|
+
is a JSON header and whose remainder is the page, raw.
|
|
373
|
+
|
|
374
|
+
```text
|
|
375
|
+
{"resultId":"…","offset":24000,"bytes":24000,"totalBytes":161420,"hasMore":true,"nextAction":{"tool":"get_result","arguments":{"id":"…","offset":48000}}}
|
|
376
|
+
{"ts":"2026-09-17T23:42:55.357Z","actor":"noor.haddad@example.com",…
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`bytes` is what this page returned; `hasMore` is false on the last page, which
|
|
380
|
+
carries no `nextAction`. The page is the stashed text as-is — a lone text
|
|
381
|
+
block's own text, a value's compact JSON, several blocks' serialized content
|
|
382
|
+
array — and never a JSON string holding it. Pages used to arrive as
|
|
383
|
+
`{ offset, nextOffset?, totalBytes, text }`, which escaped every quote and
|
|
384
|
+
newline in the page: a second layer over JSON payloads that made them hard to
|
|
385
|
+
read and swelled a 20,000-byte page of JSON lines to 25,000 characters or more.
|
|
386
|
+
|
|
387
|
+
`maxBytes` is an upper bound clamped to the inline cap of the call that stashed
|
|
388
|
+
the result — the connector's override when it had one, recorded in the stash
|
|
389
|
+
entry — and a clamped request is answered, not refused, with `bytes` saying how
|
|
390
|
+
much came back. Clients cut an oversized page exactly as they cut the original
|
|
391
|
+
result. In the eval, agents that could finally see the handle asked for pages of
|
|
392
|
+
50,000 bytes and more, and Claude Code rejected every one of those answers
|
|
393
|
+
outright, leaving an agent that had just been told not to repeat a write with
|
|
394
|
+
nothing to read. So no response to a truncated call or to a page request
|
|
395
|
+
exceeds the cap plus a header of a few hundred bytes. Value mode's unpageable
|
|
396
|
+
preview is cut short enough that its JSON escaping still fits. Entries stashed
|
|
397
|
+
before the cap was recorded page at the deployment cap.
|
|
398
|
+
|
|
399
|
+
A refused or failed stash write cannot undo a downstream success. Both call tools
|
|
400
|
+
then return the same layout with a paging-unavailable notice and no `resultId`
|
|
401
|
+
or paging action — the notice first, then the preview where one is usable, and
|
|
402
|
+
for a write the same warning not to repeat it. Activity records success, and
|
|
403
|
+
the operator logger receives a fixed warning naming the connector and tool
|
|
404
|
+
without storage error prose. For read-only work, reduce the result inside
|
|
405
|
+
`execute_code` — repeating an approved write is not a way to recover its output. Other result-processing failures use a fixed
|
|
406
|
+
`result_processing_failed` message and are never retryable.
|
|
407
|
+
|
|
271
408
|
## Lexical discovery
|
|
272
409
|
|
|
273
410
|
`search_tools` tokenizes tool names and descriptions at punctuation and
|
package/ethos.md
CHANGED
|
@@ -4,92 +4,89 @@ Connecta’s scope, refusals, and invariants. Contradictions require a design de
|
|
|
4
4
|
|
|
5
5
|
## What this is
|
|
6
6
|
|
|
7
|
-
- **One MCP endpoint.** Agents reach configured connectors through JavaScript
|
|
8
|
-
and seven meta-tools
|
|
9
|
-
- **Config-as-code.** One tenant and connector set; identities
|
|
10
|
-
config-derived views. Maintained providers, `remoteMcp()`, and `api()`
|
|
11
|
-
|
|
12
|
-
- **Safe by default.**
|
|
13
|
-
|
|
14
|
-
- **One fetch-native core.** Web APIs
|
|
15
|
-
|
|
7
|
+
- **One MCP endpoint.** Agents reach configured connectors through JavaScript,
|
|
8
|
+
a required executor, and seven meta-tools, eight planned.
|
|
9
|
+
- **Config-as-code.** One tenant and connector set; identities get
|
|
10
|
+
config-derived views. Maintained providers, `remoteMcp()`, and `api()` share
|
|
11
|
+
one connector contract.
|
|
12
|
+
- **Safe by default.** Programs run only explicitly read-only tools unasked;
|
|
13
|
+
others cross `call_destructive_tool` or, planned, pause. The host owns approval.
|
|
14
|
+
- **One fetch-native core.** Web APIs on Node and Workers; platform code and
|
|
15
|
+
optional features behind explicit subpaths; Effect inside, Promises outside.
|
|
16
16
|
- **Human auth management.** Optional pages show status and payload-free history.
|
|
17
17
|
Explicit permissions allow credential changes and OAuth. Capabilities stay in code.
|
|
18
18
|
|
|
19
19
|
## What this isn't
|
|
20
20
|
|
|
21
|
-
No runtime registration, admin-editable capabilities, policy engine
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
acceptable; version numbers signal change, not stability.
|
|
21
|
+
No runtime registration, admin-editable capabilities, or policy engine; config
|
|
22
|
+
may only exempt a tool from asking. No schema ingestion, accounts, or groups;
|
|
23
|
+
personal state remains within one tenant. Breaking changes remain acceptable;
|
|
24
|
+
version numbers signal change, not stability.
|
|
26
25
|
|
|
27
26
|
## Decisions
|
|
28
27
|
|
|
29
28
|
Revisiting a verdict requires a new argument. Accepted designs live in
|
|
30
|
-
subsystem guides and the CHANGELOG
|
|
29
|
+
subsystem guides and the CHANGELOG; `planned` ones bind review before either
|
|
30
|
+
carries them.
|
|
31
31
|
|
|
32
32
|
| Decision | Verdict | Why |
|
|
33
33
|
| --- | --- | --- |
|
|
34
|
-
| OpenAPI / GraphQL ingestion | refused | the disease is a tool nobody chose
|
|
34
|
+
| OpenAPI / GraphQL ingestion | refused | the disease is a document-authored tool nobody chose; hand-written literals, even through a shared factory, are still authorship |
|
|
35
35
|
| Multi-tenancy / account model | refused | one deployment per tenant; inbound auth owns identity |
|
|
36
|
-
|
|
|
36
|
+
| Approvals and pauses | planned | a program pauses host-side at its first unapproved write until the destructive-annotated `resume_execution` repeats that exact call, the host's prompt approving it or its tool; an expiring journal replays the run, never re-sending unknown outcomes |
|
|
37
37
|
| Runtime connector registration | refused | config-as-code is the security model |
|
|
38
|
-
| Optional deployment modules | accepted |
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
38
|
+
| Optional deployment modules | accepted | typed slots select UI, activity, vault, and inbound auth; core keeps discovery, execution, invocation, and enforcement |
|
|
39
|
+
| Artifacts module | planned | a built-in connector, not a meta-tool: team-only sandboxed pages over stored JSON, never calling tools. Immutable versions let writes skip approval. Supersedes [#287](https://github.com/zackbart/connecta/issues/287) on executor's artifacts |
|
|
40
|
+
| Plugin lifecycle, provider registry, or marketplace | refused | modules are deployment code, not runtime installs; prebuilt connections are imports, discovered in docs ([#297](https://github.com/zackbart/connecta/issues/297)) |
|
|
41
|
+
| Connecta-issued access tokens | removed | inbound providers authenticate clients; bearer auth stays an optional adapter |
|
|
42
42
|
| Expanded Notion page create/update options | refused | different workflows, not missing fields; use `api()` ([#408](https://github.com/zackbart/connecta/issues/408)) |
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
| Elicitation passthrough | refused | no route through a stateless aggregator |
|
|
43
|
+
| Resources, prompts, and downstream MCP Apps templates | refused | tools only; clients own presentation ([#266](https://github.com/zackbart/connecta/issues/266)) |
|
|
44
|
+
| Protocol sessions, server push, elicitation passthrough | refused | stateless per request; elicitation has no route |
|
|
46
45
|
| Repository formatter | refused | style is authored, not enforced |
|
|
47
|
-
| Host-side projection of program results | refused | a program projects
|
|
48
|
-
| `get_result` paging for program results | refused | paging rewards the unprojected return code mode exists to remove ([#223](https://github.com/zackbart/connecta/issues/223)) |
|
|
46
|
+
| Host-side projection or paging of program results | refused | a program projects: heuristics drop fields invisibly, paging rewards unprojected returns ([#223](https://github.com/zackbart/connecta/issues/223)) |
|
|
49
47
|
| Native Tasks for oversized results | refused | tasks solve duration, `get_result` solves size ([#176](https://github.com/zackbart/connecta/issues/176)) |
|
|
50
48
|
| Widening the `Executor` result contract | refused | `{ result, error?, logs? }` is the `@cloudflare/codemode` parity guarantee ([#267](https://github.com/zackbart/connecta/issues/267)) |
|
|
51
|
-
| Erasable TypeScript in `execute_code` | refused | 24 MB dependency
|
|
52
|
-
| Guest-minted `resource` / `resource_link` blocks | refused |
|
|
49
|
+
| Erasable TypeScript in `execute_code` | refused | a 24 MB core dependency, no measured agent benefit ([#419](https://github.com/zackbart/connecta/issues/419)) |
|
|
50
|
+
| Guest-minted `resource` / `resource_link` blocks | refused | no program mints a URI a client may dereference ([#266](https://github.com/zackbart/connecta/issues/266)) |
|
|
53
51
|
| Provenance tracking for emitted content | refused | everything a program emits is program output ([#267](https://github.com/zackbart/connecta/issues/267)) |
|
|
54
|
-
| Result sampling on the catalog surface | refused | sampling is execution
|
|
52
|
+
| Result sampling on the catalog surface | refused | sampling is execution, not a catalog read ([#282](https://github.com/zackbart/connecta/issues/282)) |
|
|
55
53
|
| Legacy embedded `UIResource` delivery | refused | superseded upstream, rendered by no client we face ([#266](https://github.com/zackbart/connecta/issues/266)) |
|
|
56
|
-
| Effect as the core effect system |
|
|
57
|
-
| Shared bounded queue under both admission controllers | refused | built
|
|
54
|
+
| Effect as the core effect system | accepted | lifetimes held the bugs: the rewrite closed leaked permits, hung waits, and cross-request races. Overturns [#470](https://github.com/zackbart/connecta/issues/470) |
|
|
55
|
+
| Shared bounded queue under both admission controllers | refused | built: −17 lines bought a hook-parameterised abstraction ([#453](https://github.com/zackbart/connecta/issues/453)) |
|
|
58
56
|
| MCP Apps rendering and `connecta.ui` | removed | clients render returned data |
|
|
59
57
|
| Connector shortcut globals | removed | canonical addresses need no sanitization |
|
|
60
58
|
| `connecta.batch` | removed | JavaScript promises suffice |
|
|
61
59
|
| Automatic direct-call retries | removed | callers own retry timing |
|
|
62
|
-
| Connector HTTP routes | removed | deployments own custom routes |
|
|
63
|
-
| Caller-selected toolkits | removed | config derives every view
|
|
60
|
+
| Connector HTTP routes | removed | deployments own custom routes; artifact viewing is the planned exception |
|
|
61
|
+
| Caller-selected toolkits | removed | config derives every view, grant-gated `/mcp/<pool>` pools included ([#178](https://github.com/zackbart/connecta/issues/178), [#531](https://github.com/zackbart/connecta/issues/531)) |
|
|
64
62
|
| Proactive credential liveness | removed | fail-at-use is enough ([#179](https://github.com/zackbart/connecta/issues/179)) |
|
|
65
63
|
| Classic (executor-free) surface | removed | an executor is mandatory ([#273](https://github.com/zackbart/connecta/issues/273)) |
|
|
66
|
-
| Per-result lexical query coverage | removed |
|
|
67
|
-
| Stabilized workflows |
|
|
64
|
+
| Per-result lexical query coverage | removed | failed a precommitted response-bytes gate ([#323](https://github.com/zackbart/connecta/issues/323)) |
|
|
65
|
+
| Stabilized workflows | planned | only as artifact refresh: scheduled read-only programs, shared credentials ([#225](https://github.com/zackbart/connecta/issues/225)) |
|
|
68
66
|
| Semantic tool search | gated | keyword search has not been shown to fail ([#27](https://github.com/zackbart/connecta/issues/27)) |
|
|
69
67
|
| MRTR / `input_required` passthrough | gated | relayable statelessly; no host or downstream emits it yet ([#176](https://github.com/zackbart/connecta/issues/176)) |
|
|
70
68
|
| Downstream `ttlMs` cache hints | gated | needs refresh-churn evidence ([#206](https://github.com/zackbart/connecta/issues/206)) |
|
|
71
|
-
| Downstream MCP Apps template passthrough | refused | clients own presentation; Connecta serves tools and data |
|
|
72
69
|
| Worker Access inbound auth | provisional | Managed OAuth and Clerk migration need production evidence ([#506](https://github.com/zackbart/connecta/issues/506)) |
|
|
73
70
|
| Program UI tool calls | removed | duplicated calls without improving retrieval ([#287](https://github.com/zackbart/connecta/issues/287), [#484](https://github.com/zackbart/connecta/issues/484)) |
|
|
74
71
|
|
|
75
72
|
## Invariants
|
|
76
73
|
|
|
77
|
-
Tests beside subsystem documentation enforce these
|
|
78
|
-
|
|
74
|
+
Tests beside subsystem documentation enforce these; breaking one requires a
|
|
75
|
+
design decision.
|
|
79
76
|
|
|
80
77
|
- **Fail-closed read-only.** A missing, false, or contradictory annotation never gets the benefit of the doubt.
|
|
81
78
|
- **Generated code cannot mint capabilities.** Admission, credentials, and classification are enforced below the sandbox.
|
|
82
|
-
- **Only explicitly read-only work runs
|
|
83
|
-
- **Nothing request-bound survives a request.** No transport, stream, signal, or awaited promise outlives it.
|
|
84
|
-
- **A downstream catalog is complete or it is a failure.** A partial
|
|
79
|
+
- **Only explicitly read-only work runs unasked in the sandbox.** Everything else crosses `call_destructive_tool` or, planned, `resume_execution` or a config exemption.
|
|
80
|
+
- **Nothing request-bound survives a request.** No transport, stream, signal, or awaited promise outlives it; a paused journal is data, not a request.
|
|
81
|
+
- **A downstream catalog is complete or it is a failure.** A partial one is never cached, persisted, or served.
|
|
85
82
|
- **Activity is payload-free by construction.** The event type has nowhere to put arguments, results, code, or raw errors.
|
|
86
83
|
- **An observed shape is never a declaration.** Names and broad types only, labeled, and gone behind any declared schema.
|
|
87
|
-
- **Credentials never leave the host.** Encrypted at rest, readable only by the owning connector and, for personal auth,
|
|
88
|
-
- **Import-graph purity.** Nothing reachable from the root entry imports a `node:` builtin
|
|
89
|
-
- **The published surface is a boundary.** Heavyweight or platform-bound code goes behind an optional-peer subpath.
|
|
90
|
-
- **Human routes manage auth, never capability.** Visibility grants use, not
|
|
91
|
-
- **Omitted modules do no work.** Core imports no UI bundle,
|
|
92
|
-
- **Status reads do not start authorization.**
|
|
84
|
+
- **Credentials never leave the host.** Encrypted at rest, readable only by the owning connector (and principal, for personal auth), rendered by nothing.
|
|
85
|
+
- **Import-graph purity.** Nothing reachable from the root entry imports a `node:` builtin or `effect/testing`.
|
|
86
|
+
- **The published surface is a boundary.** Heavyweight or platform-bound code goes behind an optional-peer subpath; Effect is the one hard dependency, named by no published type.
|
|
87
|
+
- **Human routes manage auth, never capability.** Visibility grants use, not administration. Shared and personal auth mutations need separate config-derived permissions, both denied by default, and activity reads their own. Planned: viewing artifacts.
|
|
88
|
+
- **Omitted modules do no work.** Core imports no UI bundle, vault, activity, or bearer implementation. OAuth callbacks work without UI.
|
|
89
|
+
- **Status reads do not start authorization.** Only an explicit authorized action starts OAuth, never a page load.
|
|
93
90
|
- **Structural mistakes throw at construction.** Booting into the wrong shape is worse than not booting.
|
|
94
91
|
|
|
95
92
|
Connecta simplifies [executor](https://github.com/UsefulSoftwareCo/executor).
|
|
@@ -16,15 +16,17 @@ migrations, and secrets.
|
|
|
16
16
|
| --- | --- |
|
|
17
17
|
| `src/index.ts` | the Worker entrypoint — connector and auth configuration |
|
|
18
18
|
| `src/cloudflare-kv.ts` | `KVStorage` over Workers KV (deployment-owned, not a package export) |
|
|
19
|
+
| `src/d1-storage.ts` | `KVStorage` over D1 with atomic compare-and-set (optional; see below) |
|
|
19
20
|
| `src/d1-activity.ts` | `ActivityStore` over D1 (deployment-owned; see below) |
|
|
20
21
|
| `src/d1-activity-row.ts` | the row ↔ event mapping `d1-activity.ts` uses, including friction derived from `error_code` for rows written before that column existed |
|
|
21
22
|
| `wrangler.jsonc` | Worker name, vars, bindings, `compatibility_flags` |
|
|
22
23
|
|
|
23
|
-
`cloudflare-kv.ts` and `d1-activity.ts` deliberately live
|
|
24
|
-
the package: storage backends are deployment-owned, so the
|
|
25
|
-
the generic `KVStorage` and `ActivityStore` contracts.
|
|
26
|
-
consistent across locations; use a strongly consistent
|
|
27
|
-
|
|
24
|
+
`cloudflare-kv.ts`, `d1-storage.ts`, and `d1-activity.ts` deliberately live
|
|
25
|
+
here rather than in the package: storage backends are deployment-owned, so the
|
|
26
|
+
package ships only the generic `KVStorage` and `ActivityStore` contracts.
|
|
27
|
+
Workers KV is eventually consistent across locations; use a strongly consistent
|
|
28
|
+
`KVStorage` adapter, such as [`d1-storage.ts`](#strongly-consistent-storage-optional),
|
|
29
|
+
when OAuth disconnect or credential rotation must become globally visible
|
|
28
30
|
immediately.
|
|
29
31
|
|
|
30
32
|
## Deploy
|
|
@@ -214,6 +216,50 @@ serves the seven-tool surface. Do not add `bindings`, `modules`, or
|
|
|
214
216
|
owns the package install — see
|
|
215
217
|
[copied into its own repository](#copied-into-its-own-repository).
|
|
216
218
|
|
|
219
|
+
## Strongly consistent storage (optional)
|
|
220
|
+
|
|
221
|
+
`KVStorage` has an optional atomic `compareAndSet`, for a subsystem that must
|
|
222
|
+
claim a key exactly once. **Workers KV cannot provide it.** It is eventually
|
|
223
|
+
consistent: two locations can each read a key as absent and both write, so
|
|
224
|
+
`cloudflare-kv.ts` declares no `compareAndSet` rather than fake one with a read
|
|
225
|
+
followed by a write. `src/d1-storage.ts` is a complete `KVStorage` over D1
|
|
226
|
+
that does provide it, one SQL statement per claim, with TTLs honored at read
|
|
227
|
+
time. It replaces the KV namespace rather than sitting beside it: every piece
|
|
228
|
+
of connecta state lives in one store.
|
|
229
|
+
|
|
230
|
+
1. Create the database and uncomment the `STORAGE_DB` entry of the
|
|
231
|
+
`d1_databases` binding in `wrangler.jsonc`, pasting in the id it prints:
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
wrangler d1 create connecta-storage
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
2. Apply the schema (keep it in a deployment-owned `migrations/` directory):
|
|
238
|
+
|
|
239
|
+
```sql
|
|
240
|
+
CREATE TABLE IF NOT EXISTS connecta_kv (
|
|
241
|
+
key TEXT PRIMARY KEY,
|
|
242
|
+
value TEXT NOT NULL,
|
|
243
|
+
expires_at_ms INTEGER
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
CREATE INDEX IF NOT EXISTS connecta_kv_expiry
|
|
247
|
+
ON connecta_kv (expires_at_ms);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
3. In `src/index.ts`, add `STORAGE_DB: D1Database` to `Env` and build the store
|
|
251
|
+
from it:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { d1Storage } from "./d1-storage.js";
|
|
255
|
+
|
|
256
|
+
const storage = d1Storage(env.STORAGE_DB);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Switching an existing deployment starts from empty state: downstream OAuth
|
|
260
|
+
connections must be re-authorized and vault credentials re-entered, because
|
|
261
|
+
nothing copies them out of the KV namespace.
|
|
262
|
+
|
|
217
263
|
## Activity history (optional)
|
|
218
264
|
|
|
219
265
|
`src/d1-activity.ts` is a complete `ActivityStore` over D1 — keyset paging on
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { KVStorage } from "@zackbart/connecta";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KVStorage over a D1 database, with the atomic `compareAndSet` that Workers
|
|
5
|
+
* KV cannot offer. Apply the `connecta_kv` schema in README.md § "Strongly
|
|
6
|
+
* consistent storage" before first use.
|
|
7
|
+
*
|
|
8
|
+
* Every compare-and-set is a single SQL statement. D1 runs one database's
|
|
9
|
+
* statements one at a time against its primary, so the comparison and the
|
|
10
|
+
* write cannot interleave with another request's: of fifty concurrent claims
|
|
11
|
+
* on an absent key, exactly one changes a row.
|
|
12
|
+
*
|
|
13
|
+
* Expiry is judged by the Worker's clock at each call, the same `Date.now()`
|
|
14
|
+
* the core uses. An expired row reads as absent immediately and is removed
|
|
15
|
+
* physically by later writes, a bounded batch at a time, so no request starts
|
|
16
|
+
* a background job and no cron is required.
|
|
17
|
+
*/
|
|
18
|
+
export function d1Storage(db: D1Database): KVStorage {
|
|
19
|
+
// Every statement that tests liveness binds the current time as ?2.
|
|
20
|
+
const live = "(expires_at_ms IS NULL OR expires_at_ms > ?2)";
|
|
21
|
+
const expiresAt = (now: number, ttlSeconds?: number) =>
|
|
22
|
+
ttlSeconds ? now + ttlSeconds * 1000 : null;
|
|
23
|
+
const changed = (result: D1Result) => result.meta.changes > 0;
|
|
24
|
+
return {
|
|
25
|
+
async get(key) {
|
|
26
|
+
const row = await db
|
|
27
|
+
.prepare(`SELECT value FROM connecta_kv WHERE key = ?1 AND ${live}`)
|
|
28
|
+
.bind(key, Date.now())
|
|
29
|
+
.first<{ value: string }>();
|
|
30
|
+
return row?.value ?? null;
|
|
31
|
+
},
|
|
32
|
+
async set(key, value, opts) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
await db.batch([
|
|
35
|
+
db
|
|
36
|
+
.prepare(
|
|
37
|
+
`DELETE FROM connecta_kv WHERE key IN (
|
|
38
|
+
SELECT key FROM connecta_kv WHERE expires_at_ms <= ?1 LIMIT 16
|
|
39
|
+
)`,
|
|
40
|
+
)
|
|
41
|
+
.bind(now),
|
|
42
|
+
db
|
|
43
|
+
.prepare(
|
|
44
|
+
`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
45
|
+
VALUES (?1, ?2, ?3)
|
|
46
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
47
|
+
value = excluded.value, expires_at_ms = excluded.expires_at_ms`,
|
|
48
|
+
)
|
|
49
|
+
.bind(key, value, expiresAt(now, opts?.ttlSeconds)),
|
|
50
|
+
]);
|
|
51
|
+
},
|
|
52
|
+
async delete(key) {
|
|
53
|
+
await db.prepare("DELETE FROM connecta_kv WHERE key = ?1").bind(key).run();
|
|
54
|
+
},
|
|
55
|
+
async list(prefix) {
|
|
56
|
+
const { results } = await db
|
|
57
|
+
.prepare(
|
|
58
|
+
`SELECT key FROM connecta_kv
|
|
59
|
+
WHERE key >= ?1 AND substr(key, 1, length(?1)) = ?1 AND ${live}`,
|
|
60
|
+
)
|
|
61
|
+
.bind(prefix, Date.now())
|
|
62
|
+
.all<{ key: string }>();
|
|
63
|
+
// Sort in JavaScript: SQLite orders UTF-8 bytes, the contract orders
|
|
64
|
+
// the UTF-16 code units every other adapter sorts by.
|
|
65
|
+
return results.map((row) => row.key).sort();
|
|
66
|
+
},
|
|
67
|
+
async compareAndSet(key, expected, next, opts) {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (next === null && expected === null) {
|
|
70
|
+
// Nothing to write; the claim holds exactly when no live row exists.
|
|
71
|
+
const row = await db
|
|
72
|
+
.prepare(`SELECT 1 AS present FROM connecta_kv WHERE key = ?1 AND ${live}`)
|
|
73
|
+
.bind(key, now)
|
|
74
|
+
.first();
|
|
75
|
+
return row === null;
|
|
76
|
+
}
|
|
77
|
+
if (next === null) {
|
|
78
|
+
return changed(
|
|
79
|
+
await db
|
|
80
|
+
.prepare(
|
|
81
|
+
`DELETE FROM connecta_kv WHERE key = ?1 AND ${live} AND value = ?3`,
|
|
82
|
+
)
|
|
83
|
+
.bind(key, now, expected)
|
|
84
|
+
.run(),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const expiry = expiresAt(now, opts?.ttlSeconds);
|
|
88
|
+
if (expected === null) {
|
|
89
|
+
// Insert, or take over a row that has expired. A live row makes the
|
|
90
|
+
// upsert's WHERE false, so nothing changes and the claim is refused.
|
|
91
|
+
return changed(
|
|
92
|
+
await db
|
|
93
|
+
.prepare(
|
|
94
|
+
`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
95
|
+
VALUES (?1, ?3, ?4)
|
|
96
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
97
|
+
value = excluded.value, expires_at_ms = excluded.expires_at_ms
|
|
98
|
+
WHERE connecta_kv.expires_at_ms IS NOT NULL
|
|
99
|
+
AND connecta_kv.expires_at_ms <= ?2`,
|
|
100
|
+
)
|
|
101
|
+
.bind(key, now, next, expiry)
|
|
102
|
+
.run(),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return changed(
|
|
106
|
+
await db
|
|
107
|
+
.prepare(
|
|
108
|
+
`UPDATE connecta_kv SET value = ?4, expires_at_ms = ?5
|
|
109
|
+
WHERE key = ?1 AND ${live} AND value = ?3`,
|
|
110
|
+
)
|
|
111
|
+
.bind(key, now, expected, next, expiry)
|
|
112
|
+
.run(),
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -50,8 +50,8 @@ interface Env {
|
|
|
50
50
|
CONNECTA_KV: KVNamespace;
|
|
51
51
|
/**
|
|
52
52
|
* Base64 32-byte AES key encrypting operator-managed credentials in KV.
|
|
53
|
-
* Unset means no vault:
|
|
54
|
-
*
|
|
53
|
+
* Unset means no vault: credential management is unavailable in the operator
|
|
54
|
+
* UI. Never put it in KV, since it is what protects KV.
|
|
55
55
|
*/
|
|
56
56
|
CREDENTIAL_ENCRYPTION_KEY: string;
|
|
57
57
|
DOWNSTREAM_TOKEN: string;
|
|
@@ -121,8 +121,8 @@ function build(env: Env) {
|
|
|
121
121
|
type: "headers",
|
|
122
122
|
headers: { Authorization: `Bearer ${env.DOWNSTREAM_TOKEN}` },
|
|
123
123
|
// The vault-backed alternative for a downstream that authenticates
|
|
124
|
-
// with a static key: the operator
|
|
125
|
-
//
|
|
124
|
+
// with a static key: the operator manages it inside its connection
|
|
125
|
+
// in the operator UI, so no Worker secret holds it.
|
|
126
126
|
// type: "credential",
|
|
127
127
|
// credential: { label: "Notion internal integration token" },
|
|
128
128
|
},
|
|
@@ -132,8 +132,8 @@ function build(env: Env) {
|
|
|
132
132
|
}),
|
|
133
133
|
api("echo", {
|
|
134
134
|
description: "Echo — text transforms",
|
|
135
|
-
// What a vault-backed connector adds — an operator edits this slot
|
|
136
|
-
//
|
|
135
|
+
// What a vault-backed connector adds — an operator edits this slot
|
|
136
|
+
// inside its connection in the operator UI and the handler reads it with
|
|
137
137
|
// `await ctx.credential?.get()`, so the secret never lives in source
|
|
138
138
|
// or in a Worker variable:
|
|
139
139
|
// credential: { label: "API token" },
|