@zackbart/connecta 0.26.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -287,14 +287,24 @@ answers only `{ ok }`), log its text, and the page picks a sentence by outcome
287
287
  without ever rendering what the server sent. Credential handoff URLs exist only while the UI is
288
288
  mounted; OAuth callbacks never need it.
289
289
 
290
- Its appearance is one token layer. `src/operator-ui/browser.css` resolves every
291
- color, radius, and font through a custom property and mixes the rest from those
292
- with `color-mix`, so `branding.theme` only has to append a `:root` block after
293
- that stylesheet. The five tokens it accepts are gated in `src/branding.ts`, each
294
- by a narrow syntactic check: deployment config reaches a `<style>` element here,
295
- and an unvalidated value would be CSS injection. The dark palette is the same
296
- tokens under `prefers-color-scheme`; `colorScheme` pins one with a `data-scheme`
297
- attribute on the page.
290
+ Its appearance is one token layer, and it is not the UI's alone: every page
291
+ connecta shows a person — the operator shell, the OAuth callback, a browser's
292
+ 404, the artifact frame and Markdown pages — reads it. `src/operator-ui/tokens.css`
293
+ resolves every color, radius, and font through a custom property and mixes the
294
+ rest from those with `color-mix`; `page.css` holds the base typography and the
295
+ primitives (shell, masthead, buttons, badges, messages, the one-message status
296
+ page). The UI's `browser.css` imports both, and the build writes the same CSS,
297
+ minified, into `src/page-styles.ts`, a module of two strings that core imports
298
+ without the UI bundle, so an OAuth callback still renders with the UI omitted.
299
+ `renderPage` in `src/branding.ts` is the one layout: head, favicon (the default
300
+ is linked only when the UI serves it), `data-scheme`, the tokens with the theme
301
+ after them, and the masthead. So `branding.theme` only has to append a `:root`
302
+ block after the stylesheet. The five tokens it accepts are gated in
303
+ `src/branding.ts`, each by a narrow syntactic check: deployment config reaches a
304
+ `<style>` element here, and an unvalidated value would be CSS injection. The
305
+ dark palette is the same tokens under `prefers-color-scheme`; `colorScheme` pins
306
+ one with a `data-scheme` attribute on the page, and the artifact viewer passes
307
+ the same attribute into a Markdown page, never into authored HTML.
298
308
 
299
309
  The artifacts module — `artifacts()` from `/artifacts`, implemented in
300
310
  `src/artifacts/` — is the one module that contributes a connector. Core
@@ -250,6 +250,87 @@ means authorizing again. A vault without the optional `seal`/`open` members
250
250
  keeps these values plaintext and draws a startup warning. Without any vault,
251
251
  nothing changes: the state is plaintext, as it always was.
252
252
 
253
+ ## Starting, restarting, and retiring an OAuth epoch
254
+
255
+ Every downstream OAuth value lives under an **epoch**, the value of
256
+ `oauth:generation`. A restart or disconnect publishes a new one before it
257
+ deletes anything, and a write from a flow that captured an older epoch lands
258
+ in that epoch's own key namespace, where no reader looks. The fence is what
259
+ keeps a retired grant out of use; deleting the old keys is hygiene on top.
260
+
261
+ The operator's `POST /ui/oauth/<id>` takes a `mode` query parameter:
262
+
263
+ | Request | What it does |
264
+ | --- | --- |
265
+ | `POST /ui/oauth/<id>` or `?mode=restart` | `startAuth(ctx, { force: true })`: a new epoch, with tokens, the registered client, discovery, and any pending flow wiped, so the next flow registers a client again. |
266
+ | `?mode=continue` | `startAuth(ctx, { force: false })`: hand back the pending authorization URL if it was written in the last ten minutes; otherwise start a flow in the current epoch, reusing the stored client registration. A disconnected connector still gets its new epoch first. |
267
+ | `DELETE /ui/oauth/<id>` | Disconnect: a disconnected epoch that passive reads never turn back into a consent flow. |
268
+
269
+ Any other `mode`, or more than one, is a 400 before anything starts; the
270
+ management permission, visibility, and principal checks are the same for
271
+ both modes, and a personal connector continues only the caller's own flow.
272
+ A 200 answers `{ state, authorizationUrl?, reused? }`, with `reused` present
273
+ beside every URL: `true` when an earlier start's URL came back unchanged. A
274
+ continue that reused a URL or found the connection healthy changed nothing,
275
+ so it leaves the cached catalog alone; every other start invalidates it. The
276
+ ten minutes are `PENDING_AUTHORIZATION_MAX_AGE_MS`. Connecta never expires
277
+ its own half of a URL, but authorization servers expire theirs, and a fresh
278
+ start that keeps the registration costs one authorization request. A URL
279
+ stored without a write time — by an earlier release, or by a connector never
280
+ reset since epochs arrived — is stale by definition. `authorize_connector`
281
+ without `force` follows the same rule.
282
+
283
+ Continue trusts the stored registration, which carries the `redirect_uris`
284
+ it was registered with. After a `publicUrl` change the authorization server
285
+ refuses that client's new callback at consent. Restart registers a client
286
+ for the current URL and recovers.
287
+
288
+ A restart cannot know which retired epochs a late write reached, so it
289
+ publishes a **cleanup lineage** under the new epoch before activating it:
290
+ the retired epochs, as the same plain list of names every release reads, and
291
+ beside it a record of when each retired. A published lineage is never
292
+ rewritten. A stale writer whose own cleanup fails appends to the live
293
+ lineage, and a restart that rewrote the list it had read could drop that
294
+ append. An epoch leaves the lineage only by being left out of its
295
+ successor's.
296
+
297
+ A restart does the same storage work however many came before it. After the
298
+ fence it deletes the one epoch it retired: six values, then that epoch's own
299
+ lineage records. An epoch's manifest outlives its values only when their
300
+ deletion failed, so the restart also reads the manifests of the eight most
301
+ recently retired epochs (`RETRY_PROBES`) and cleans up again any that still
302
+ have one. A Disconnect or Restart that reported a failed cleanup therefore
303
+ deletes the old grant when the operator retries it. Before publishing, it
304
+ sweeps up to 16 epochs retired more than `CLEANUP_GRACE_MS` (24 hours) ago,
305
+ oldest first, and leaves out only those whose values and records were all
306
+ deleted. A failed sweep is carried and tried again later, and never fails
307
+ the restart. Any other epoch inside its grace is not deleted again. A late write into it is unreadable behind the fence,
308
+ and the late writer deletes it itself. If that delete fails, the writer
309
+ records the epoch again as retired at that moment, appending it or, when it
310
+ is already listed, moving only its time. A restart that swept it re-reads the
311
+ lineage before publishing and keeps it. Whatever remains is swept once the
312
+ grace has passed. Two such writers racing can lose one time update, which
313
+ leaves the earlier time; a time missing altogether reads as retired at the
314
+ moment of the restart that reads it, which only lengthens a grace.
315
+
316
+ **The assumption:** no request holds a retired epoch for a day. The writers
317
+ that can land late are OAuth flows, refreshes, and readers whose Workers KV
318
+ replica still serves the old generation for a minute or more; none of them
319
+ run that long. If it fails, only a writer that dies between its write and
320
+ its own cleanup leaves residue no restart tracks, and that residue is never
321
+ readable.
322
+
323
+ The lineage holds at most 5,000 epochs, the ones retired within the last day
324
+ plus any the sweep has not reached. A restart that would exceed that is
325
+ refused before the fence moves, which takes more than 4,000 restarts of one
326
+ connector in a day on top of the 1,000 an earlier release allowed. A
327
+ connector that reached that release's 1,000 wall restarts again the day it
328
+ upgrades, and its old entries drain 16 per restart a day later. An older
329
+ release ignores the times, so a rollback still restarts unless a lineage has
330
+ grown past its own 1,000 cap. Deletes run six at a time, the Workers limit
331
+ on simultaneous connections, and catalog invalidation deletes its chunks
332
+ concurrently under the registry's chunk I/O bound.
333
+
253
334
  ## Refresh failures
254
335
 
255
336
  A failed token refresh means one of two things, and connecta decides which
@@ -429,6 +510,28 @@ instead of a base64 stack on every route.
429
510
 
430
511
  ## Human authentication management
431
512
 
513
+ An operator's OAuth Connect or Restart request gives the start and its handoff
514
+ 30 seconds. The request's abort signal and that deadline reach downstream OAuth
515
+ fetches, including metadata discovery and dynamic registration. If the deadline
516
+ expires, the route returns `504` with `OAuth authorization start timed out` after
517
+ an already-started reset finishes. Storage has no cancellation contract: its
518
+ generation write must drain before a response, or it could publish an old epoch
519
+ over a newer flow. Catalog invalidation and connector scope close also finish
520
+ outside the deadline. Those storage and cleanup waits can extend the click past
521
+ 30 seconds; the bound is on downstream work, where a hung authorization server
522
+ was the failure. The OAuth provider checks its signal before writing and removes
523
+ a cancelled value write that finishes late. An operator Disconnect keeps its
524
+ existing commit-through behavior even if the browser leaves.
525
+
526
+ Restart retains a dynamically registered client only when its stored issuer and
527
+ the connector's URL, redirect URI, client metadata, auth scope, transport settings,
528
+ and owner partition still match. It drops the grant and one-shot flow state,
529
+ selects the authorization server again, and re-seals the retained client under
530
+ the replacement epoch key. A different issuer registers a new client. Disconnect
531
+ and issuer-mismatch recovery discard it. The SDK has no client-validation
532
+ exchange while constructing a fresh consent URL; a provider that revokes a
533
+ retained client may refuse it only at consent or callback, after this request.
534
+
432
535
  Credential and OAuth mutation require an admitted interactive human, connector
433
536
  visibility, the appropriate shared or personal permission, and an exact
434
537
  same-origin `Origin` for browser requests. A configured MCP bearer never becomes
@@ -504,13 +504,11 @@ provider prose remains `connector_call_failed`.
504
504
 
505
505
  ## Results and projection
506
506
 
507
- **R1 (verdict: projection stays explicit).** A program's return value reaches
508
- the model unchanged except for the size guard in `R2`. Connecta does not
509
- summarize, reshape, or field-select it, and there is no automatic projection
510
- mode: the measured byte win came from *program-authored* projection, while a
511
- host heuristic would drop deliberately returned fields invisibly. Helpers earn
512
- their way in only if [#222](https://github.com/zackbart/connecta/issues/222)
513
- shows programs failing to project on their own.
507
+ **R1 (verdict: projection stays explicit).** A program's return reaches the model
508
+ unchanged except for `R2`'s size guard. Connecta does not summarize or select
509
+ fields. Program-authored projection saved bytes; host heuristics could drop
510
+ deliberately returned fields. Helpers need [#222](https://github.com/zackbart/connecta/issues/222)
511
+ evidence that programs fail to project.
514
512
 
515
513
  **R2.** The boundary is 24,000 serialized characters (~6k tokens). A value over
516
514
  it is replaced by exactly one envelope:
@@ -528,15 +526,16 @@ The envelope is itself bounded as serialized, so `totalChars` is always the true
528
526
  size of what the program returned and truncation happens exactly once no matter
529
527
  how many hops the value takes.
530
528
 
531
- **R3.** Truncation is a *successful* result, not an error: the program ran, and
532
- the honest report is that its answer was too large. The fix is a program that
533
- returns less, which is why the envelope says so.
529
+ **R3.** Truncation is a *successful* result: the program ran but returned too
530
+ much. Run a program that returns less; that is why the envelope says so.
534
531
 
535
- **R4 (verdict: no result paging for programs).** A truncated program result
536
- carries no `get_result` handle, unlike `call_tool`. `get_result` exists so a
537
- model can page a *downstream payload* it could not shrink; a program can shrink
538
- anything, so paging its result would reward the behavior code mode exists to
539
- remove.
532
+ **R4 (verdict: no result paging for programs).** Program results have no
533
+ `get_result` handle. A program can shrink its return; paging would reward
534
+ unprojected data.
535
+
536
+ For non-repeatable writes, inspect and reduce the full result before return,
537
+ or page a direct `call_destructive_tool` result with `get_result`. Sampling a
538
+ program result may discard the only copy.
540
539
 
541
540
  **R5.** `console.log`, `console.warn`, and `console.error` are captured in call
542
541
  order and returned as one `logs` string, capped at 4,000 characters with a
@@ -833,6 +832,7 @@ live. `emit` is not journaled; only the completing play delivers.
833
832
  **W7.** `approval: "call"`, the default, covers that one write; `"tool"` covers
834
833
  every later call to that canonical address for the run. Approvals live in the
835
834
  header, a retry's replacing one that sent nothing; nothing else grants one.
835
+ Keep an authorized batch in one program; use tool scope for repeated calls to that address, while other addresses still need their own approval.
836
836
 
837
837
  **W8.** A replay fails `execution_diverged`, never replayed again, when (a) a
838
838
  call has no record before the approved write is repeated, (b) the program
@@ -69,10 +69,14 @@ an earlier result, or several operations. A program keeps discovery, calls, and
69
69
  reduction together when the schemas and result shapes suffice, and gives each
70
70
  distinct operation its own short `connecta.search` query. That is cheaper than it
71
71
  looks: discovery inside the program returns no candidate schema to the model and
72
- costs no round trip. One exception — an unfamiliar provider result may come back
73
- as a small sample for inspection before continuing in another call, which avoids
72
+ costs no round trip. One exception — an unfamiliar read result may come back as
73
+ a small sample for inspection before continuing in another call, which avoids
74
74
  repeated guesses at text formats and collection roots without restoring a
75
- mandatory discovery-only round trip.
75
+ mandatory discovery-only round trip. A one-time write can return the only copy
76
+ of its result. Inspect and reduce the full value before a program returns, or
77
+ use a direct `call_destructive_tool` result and page it with `get_result` after
78
+ the write runs once. A program return has no page handle; sampling or slicing
79
+ a write's output there can discard the answer.
76
80
 
77
81
  Writes take the same route. A program that reaches a tool not explicitly
78
82
  annotated read-only stops before sending it — unless the deployment's config
@@ -81,8 +85,11 @@ token; `resume_execution` repeating it is the approval, and the program
81
85
  replays from a journal to send it and carry on — to its answer, or to its next
82
86
  write. So "close the stale issues and post a summary" is one program and a few
83
87
  approvals, not one program and thirty-one top-level calls, and the program's
84
- reasoning survives between them. `approval: "tool"` covers the rest of the
85
- run's calls to that tool, so thirty closes cost one prompt. One known write
88
+ reasoning survives between them. Keep the entire authorized batch in that
89
+ program, including its final post. The default `approval: "call"` covers only
90
+ the pending write. Choosing `approval: "tool"` when the batch repeats an
91
+ address covers later calls to that tool in this run, so thirty closes cost one
92
+ prompt; the summary post still needs its own approval. One known write
86
93
  still goes straight to `call_destructive_tool`, and top-level discovery stays
87
94
  for catalog inspection. On a deployment without resumable writes (storage
88
95
  without `compareAndSet`), a program cannot write at all: the call fails
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of eight meta-tools.",
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@zackbart/connecta": "0.26.0",
18
+ "@zackbart/connecta": "0.26.1",
19
19
  "quickjs-emscripten": "0.32.0"
20
20
  },
21
21
  "devDependencies": {