@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.
- package/CHANGELOG.md +116 -0
- package/dist/artifacts/document.js +6 -2
- package/dist/artifacts/markdown.js +43 -14
- package/dist/artifacts/routes.js +27 -4
- package/dist/artifacts/validate.js +1 -0
- package/dist/auth/downstream-oauth.js +397 -48
- package/dist/auth/oauth-start-reset.js +20 -0
- package/dist/branding.d.ts +87 -0
- package/dist/branding.js +170 -1
- package/dist/connectors/remote-mcp.js +32 -7
- package/dist/execute.js +7 -8
- package/dist/oauth-callback-outcome.js +43 -18
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.js +288 -33
- package/dist/page-styles.js +7 -0
- package/dist/registry.js +16 -13
- package/dist/routes/mcp.js +5 -1
- package/dist/routes/oauth-management.js +66 -12
- package/dist/routes/oauth.js +54 -87
- package/dist/routes/ui.js +7 -1
- package/dist/server.js +7 -5
- package/dist/skills.js +6 -6
- package/dist/types.d.ts +5 -0
- package/dist/ui.d.ts +5 -0
- package/dist/ui.js +20 -63
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +18 -8
- package/documentation/auth.md +103 -0
- package/documentation/code-mode.md +15 -15
- package/documentation/meta-tools.md +12 -5
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,122 @@ All notable changes to this package are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.26.1 — 2026-09-26
|
|
8
|
+
|
|
9
|
+
This patch fixes slow OAuth restarts, unbounded downstream authorization waits,
|
|
10
|
+
and operator pages that signed people out after a temporary load failure.
|
|
11
|
+
Connect now opens consent in one click, and browser-facing pages share the
|
|
12
|
+
configured theme. Agent guidance preserves one-time write results and keeps
|
|
13
|
+
an authorized batch in one resumable program. Approval defaults and the eight
|
|
14
|
+
MCP tools are unchanged. No configuration or storage migration is required;
|
|
15
|
+
deployments without the operator UI can ignore the page changes. OAuth starts
|
|
16
|
+
abort downstream work after 30 seconds, but uncancellable storage resets and
|
|
17
|
+
cleanup must finish before the response. Restart reuses a matching client
|
|
18
|
+
registration; a revoked client may still fail at consent or callback and need
|
|
19
|
+
another restart. The Node template now pins 0.26.1.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Continue or restart an operator OAuth start.** `POST /ui/oauth/<id>`
|
|
24
|
+
takes `?mode=continue` or `?mode=restart`. `restart`, still the default
|
|
25
|
+
when `mode` is absent, creates a new epoch and clears the grant and
|
|
26
|
+
discovery while retaining a matching issuer-bound client registration. `continue` hands back the pending
|
|
27
|
+
authorization URL when it was written in the last ten minutes. Otherwise it
|
|
28
|
+
starts a flow in the current epoch, reusing a stored registration when one
|
|
29
|
+
exists. A first connection still registers dynamically. A disconnected
|
|
30
|
+
connector resets first. A response with a URL now carries `reused`. A continue that reused a
|
|
31
|
+
URL or found the connection healthy leaves the cached catalog alone. Any
|
|
32
|
+
other `mode` is a 400. After a `publicUrl` change, continue keeps a client
|
|
33
|
+
registered for the old callback, which the authorization server refuses;
|
|
34
|
+
restart recovers. `ConnectorStatus` gains the optional
|
|
35
|
+
`authorizationReused`, set by `remoteMcp()`'s `startAuth`.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **Pending authorization URLs expire for reuse.** A non-forced `startAuth`,
|
|
40
|
+
including `authorize_connector` without `force`, reissues a pending URL only
|
|
41
|
+
when it is under ten minutes old. The pending URL's envelope now records its
|
|
42
|
+
write time, which older readers ignore. A URL written before this release,
|
|
43
|
+
or under a pre-epoch generation, starts a fresh flow instead.
|
|
44
|
+
- **OAuth cleanup deletes run concurrently.** A restart's epoch cleanup,
|
|
45
|
+
`clearPending`, and `invalidateCredentials("all")` keep up to six deletes
|
|
46
|
+
in flight, the Workers limit on simultaneous connections. Every delete is
|
|
47
|
+
still attempted, and a falsy rejection now counts as a failure. A retired
|
|
48
|
+
epoch's manifest is still removed only after all of its values. Catalog
|
|
49
|
+
invalidation deletes the root and its chunks together under the registry's
|
|
50
|
+
chunk I/O bound, so a failed root delete no longer skips the chunks.
|
|
51
|
+
|
|
52
|
+
- Every page connecta renders for a person now shares one token layer and one
|
|
53
|
+
layout: the operator shell, the OAuth callback, a browser's 404, and the
|
|
54
|
+
artifact frame follow `branding.theme` and light or dark. The OAuth callback
|
|
55
|
+
names the outcome in its heading with a status mark, names the connector by
|
|
56
|
+
title only after the state check, uses `branding.productName` in its copy,
|
|
57
|
+
and folds the agent fix prompt under "Details for the operator". Refusals
|
|
58
|
+
stay byte-identical across connectors and paths.
|
|
59
|
+
- A request whose `Accept` names `text/html` gets a themed 404 page; every
|
|
60
|
+
other client keeps the plain `Not Found` body.
|
|
61
|
+
- Markdown artifacts take the deployment's resolved scheme and tokens instead
|
|
62
|
+
of the OS palette, and drop an opening `# Heading` that only repeats the
|
|
63
|
+
title the viewer already shows. The frame waits on the viewer's surface with
|
|
64
|
+
a loading line, and says so if the page never arrives.
|
|
65
|
+
- Without the operator UI, callback and 404 pages no longer link the default
|
|
66
|
+
`/favicon.svg`, which only the UI serves.
|
|
67
|
+
- The operator UI starts OAuth in one click. Connect opens the provider's
|
|
68
|
+
page in a new tab straight away and asks the server to continue a pending
|
|
69
|
+
authorization rather than restart it; only Reconnect on a healthy
|
|
70
|
+
connection restarts, behind an in-page confirm. A tab returning to the page
|
|
71
|
+
quietly re-reads the status of connectors waiting on authorization.
|
|
72
|
+
- Operator UI notices appear in the card that caused them, in fixed
|
|
73
|
+
sentences rather than the route's error text; problems fixed by authorizing
|
|
74
|
+
or adding a credential are warnings rather than errors, with one primary
|
|
75
|
+
action per row. Activity and artifact states, the confirm dialogs, the
|
|
76
|
+
mobile masthead, and the vocabulary shown to people (outcomes, actors,
|
|
77
|
+
timestamps) now read the same across pages and use `branding.productName`.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- Operator OAuth starts now abort downstream discovery and registration after
|
|
82
|
+
30 seconds or browser cancellation, with a fixed timeout response. Every
|
|
83
|
+
reset already started by the request, including issuer-mismatch recovery,
|
|
84
|
+
drains before catalog invalidation and response so a delayed generation
|
|
85
|
+
write cannot replace a later flow. These uncancellable storage waits can
|
|
86
|
+
exceed the deadline. Disconnect still finishes after browser cancellation.
|
|
87
|
+
- Forced OAuth restarts reuse registrations bound to the issuer, redirect URI,
|
|
88
|
+
client metadata, connector settings, and owner partition. Credentials are
|
|
89
|
+
re-sealed for the replacement epoch, and fresh discovery detects issuer
|
|
90
|
+
changes. Disconnect and issuer-mismatch recovery discard the registration.
|
|
91
|
+
The SDK cannot detect a revoked client while constructing a consent URL;
|
|
92
|
+
a callback refusal clears it so the next restart can register again.
|
|
93
|
+
- Agent sampling advice now applies to reads. For a one-time write, agents
|
|
94
|
+
reduce the full result in the program or page a direct-call result through
|
|
95
|
+
`get_result`, without repeating the write to recover discarded output.
|
|
96
|
+
- Authorized batches stay in one resumable program. Guidance explains how
|
|
97
|
+
tool-scoped approval covers repeated calls to one address while other
|
|
98
|
+
approval-required writes retain their own approvals. The call-scoped
|
|
99
|
+
default and host enforcement are unchanged.
|
|
100
|
+
- **A restart's cleanup no longer grows with every earlier restart.** Each
|
|
101
|
+
OAuth restart re-deleted every epoch the connector had ever retired, one key
|
|
102
|
+
at a time, and restart 1,001 failed forever with a full cleanup backlog. A
|
|
103
|
+
restart now deletes the epoch it retires, retries any of the eight most
|
|
104
|
+
recent epochs whose cleanup failed (their manifest outlives their values),
|
|
105
|
+
and sweeps at most 16 epochs retired more than 24 hours ago, dropping each
|
|
106
|
+
from the lineage only when all of its keys are gone. A Disconnect that
|
|
107
|
+
reported a failed cleanup still deletes the old grant when retried. The
|
|
108
|
+
cleanup work stays bounded independently of earlier resets. Residue a late
|
|
109
|
+
writer leaves in a younger epoch stays unreadable behind the fence until the
|
|
110
|
+
sweep reaches it. A late writer whose cleanup fails in an epoch already
|
|
111
|
+
listed now restarts that epoch's grace. The accepted assumption is that no
|
|
112
|
+
request holds a retired epoch for a day. The lineage records retirement
|
|
113
|
+
times in a sibling record older releases ignore, and its cap rises to 5,000
|
|
114
|
+
epochs. A connector stuck at the old 1,000 wall restarts the day it
|
|
115
|
+
upgrades. Rolling back is clean unless a lineage has grown past 1,000. See
|
|
116
|
+
[auth](./documentation/auth.md#starting-restarting-and-retiring-an-oauth-epoch).
|
|
117
|
+
- A failed or unreachable `/ui/data` no longer signs the operator out. Only a
|
|
118
|
+
401 or 403 on that read returns to the token gate; anything else keeps the
|
|
119
|
+
page and offers Retry. A connector whose details fail to load says whether
|
|
120
|
+
the session, the browser's connection, or the downstream service is at
|
|
121
|
+
fault, and only the last offers the fix prompt.
|
|
122
|
+
|
|
7
123
|
## 0.26.0 — 2026-09-25
|
|
8
124
|
|
|
9
125
|
Programs can now write with host approval. When a program reaches a tool that
|
|
@@ -42,19 +42,23 @@ const DEFINE_GLOBAL = '(()=>{const f=(o)=>{if(o!==null&&typeof o==="object"){for
|
|
|
42
42
|
/**
|
|
43
43
|
* The complete document a viewer or render check loads. `data` maps document
|
|
44
44
|
* names to their stored, script-safe JSON text, which is spliced in as-is.
|
|
45
|
+
*
|
|
46
|
+
* `theme` reaches a Markdown page only, whose styling is connecta's own. An
|
|
47
|
+
* HTML page is the author's, down to its colors, and is never restyled; the
|
|
48
|
+
* frame around it is what follows the deployment's scheme.
|
|
45
49
|
*/
|
|
46
50
|
export function buildFrameDocument(input) {
|
|
47
51
|
let page = input.source;
|
|
48
52
|
if (input.kind === "markdown") {
|
|
49
53
|
try {
|
|
50
|
-
page = markdownPage(input.source, input.global.title);
|
|
54
|
+
page = markdownPage(input.source, input.global.title, input.theme);
|
|
51
55
|
}
|
|
52
56
|
catch (error) {
|
|
53
57
|
if (!(error instanceof MarkdownNestingError))
|
|
54
58
|
throw error;
|
|
55
59
|
// Old or externally populated stores may hold a page that today's
|
|
56
60
|
// validator would reject. Render fixed text instead of throwing here.
|
|
57
|
-
page = markdownPage("This page has too many nested block quotes or lists to display.", input.global.title);
|
|
61
|
+
page = markdownPage("This page has too many nested block quotes or lists to display.", input.global.title, input.theme);
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
const meta = scriptSafeJson(input.global);
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// of text, a delimiter that found no closer is never searched for again, and
|
|
14
14
|
// nesting stops at a fixed depth. A megabyte of `[[[[` or `**` renders in
|
|
15
15
|
// linear time, because this runs on every page view.
|
|
16
|
+
import { resolveTheme, schemeAttribute, themeCss, } from "../branding.js";
|
|
17
|
+
import { TOKENS_CSS } from "../page-styles.js";
|
|
16
18
|
const escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'");
|
|
17
19
|
const PUNCTUATION = /^[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]$/;
|
|
18
20
|
const MAX_INLINE_DEPTH = 8;
|
|
@@ -430,20 +432,47 @@ export function renderMarkdown(source) {
|
|
|
430
432
|
.map((line) => line.replace(/^\t+/, (tabs) => " ".repeat(tabs.length)));
|
|
431
433
|
return blocks(lines);
|
|
432
434
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Every color, font, and radius comes from the shared tokens, so a Markdown
|
|
437
|
+
* page reads as part of the viewer around it: the deployment's accent and
|
|
438
|
+
* fonts, and the palette of the scheme the page was given rather than whatever
|
|
439
|
+
* the OS prefers. The background is the frame's `--surface`, so the page sits
|
|
440
|
+
* flush in its card instead of painting a second one.
|
|
441
|
+
*/
|
|
442
|
+
const MARKDOWN_STYLE = "body{margin:0;font:16px/1.6 var(--sans);color:var(--text);background:var(--surface)}" +
|
|
443
|
+
"main{max-width:820px;margin:0 auto;padding:32px 24px}main>:first-child{margin-top:0}" +
|
|
435
444
|
"h1,h2,h3,h4{line-height:1.25;margin:1.6em 0 .6em}h1{font-size:2em}h2{font-size:1.5em}" +
|
|
436
|
-
"a{color
|
|
437
|
-
"pre{overflow:auto;padding:16px;background
|
|
438
|
-
"blockquote{margin:0;padding:0 1em;color
|
|
439
|
-
"table{border-collapse:collapse;display:block;overflow:auto}th,td{padding:6px 12px;border:1px solid
|
|
440
|
-
"img{max-width:100%}hr{border:0;border-top:1px solid
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
445
|
+
"a{color:var(--link)}code{font:.9em var(--mono);background:var(--surface-2);padding:.1em .3em;border-radius:4px}" +
|
|
446
|
+
"pre{overflow:auto;padding:16px;background:var(--surface-2);border-radius:calc(var(--radius) - 2px)}pre code{background:none;padding:0}" +
|
|
447
|
+
"blockquote{margin:0;padding:0 1em;color:var(--muted);border-left:4px solid var(--border-strong)}" +
|
|
448
|
+
"table{border-collapse:collapse;display:block;overflow:auto}th,td{padding:6px 12px;border:1px solid var(--border-strong)}" +
|
|
449
|
+
"th{background:var(--surface-2)}img{max-width:100%}hr{border:0;border-top:1px solid var(--border)}";
|
|
450
|
+
const collapse = (text) => text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
451
|
+
/**
|
|
452
|
+
* The source without a leading `# Heading` that only repeats the title. The
|
|
453
|
+
* viewer shows the title as the page's heading directly above the frame, so a
|
|
454
|
+
* page opening with the same words would say them twice. Any other opening
|
|
455
|
+
* heading is the author's and stays.
|
|
456
|
+
*/
|
|
457
|
+
function withoutRepeatedTitle(source, title) {
|
|
458
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
459
|
+
const first = lines.findIndex((line) => line.trim() !== "");
|
|
460
|
+
const heading = first < 0 ? undefined : HEADING.exec(lines[first] ?? "");
|
|
461
|
+
if (heading?.[1] !== "#" || !collapse(title) || collapse(heading[2] ?? "") !== collapse(title)) {
|
|
462
|
+
return source;
|
|
463
|
+
}
|
|
464
|
+
return lines.slice(first + 1).join("\n");
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* A complete page for a Markdown artifact: the rendered body in
|
|
468
|
+
* `<main id="artifact-root">`. `theme` is the deployment's resolved theme; the
|
|
469
|
+
* viewer passes it so a pinned scheme and the operator's tokens reach the
|
|
470
|
+
* frame. Without one the page follows the OS scheme with default tokens.
|
|
471
|
+
*/
|
|
472
|
+
export function markdownPage(source, title, theme) {
|
|
473
|
+
const resolved = theme ?? resolveTheme();
|
|
474
|
+
return (`<!doctype html><html lang="en"${schemeAttribute(resolved)}><head><meta charset="utf-8">` +
|
|
446
475
|
`<meta name="viewport" content="width=device-width,initial-scale=1">` +
|
|
447
|
-
`<title>${escapeHtml(title)}</title><style>${MARKDOWN_STYLE}</style></head>` +
|
|
448
|
-
`<body><main id="artifact-root">\n${renderMarkdown(source)}\n</main></body></html>`);
|
|
476
|
+
`<title>${escapeHtml(title)}</title><style>${TOKENS_CSS}${themeCss(resolved)}${MARKDOWN_STYLE}</style></head>` +
|
|
477
|
+
`<body><main id="artifact-root">\n${renderMarkdown(withoutRepeatedTitle(source, title))}\n</main></body></html>`);
|
|
449
478
|
}
|
package/dist/artifacts/routes.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// could call — and the fetch directives leave it no network.
|
|
12
12
|
import { resolveActorLabels, actorKey } from "../routes/actor-labels.js";
|
|
13
13
|
import { authorize, mayViewArtifacts, privateJson, validateAuthPermissions, } from "../routes/shared.js";
|
|
14
|
+
import { resolveBranding, schemeAttribute, themeCss } from "../branding.js";
|
|
15
|
+
import { TOKENS_CSS } from "../page-styles.js";
|
|
14
16
|
import { buildFrameDocument, frameCsp } from "./document.js";
|
|
15
17
|
import { scriptSafeJson } from "./json.js";
|
|
16
18
|
import { freshnessOf } from "./refresh.js";
|
|
@@ -20,19 +22,38 @@ const MAX_QUERY_CHARS = 200;
|
|
|
20
22
|
const MAX_PINS = 32;
|
|
21
23
|
const PIN = /^([A-Za-z_][A-Za-z0-9_]{0,63}):(\d{1,9})$/;
|
|
22
24
|
const notFound = () => privateJson({ error: "not found" }, { status: 404 });
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
+
/** How long the frame waits for its document before saying it did not come. */
|
|
26
|
+
const FRAME_STALL_MS = 15_000;
|
|
27
|
+
/**
|
|
28
|
+
* The frame's bootstrap: wait for exactly one document from the parent, then
|
|
29
|
+
* become it.
|
|
30
|
+
*
|
|
31
|
+
* Until the document arrives the frame shows one quiet line on the viewer's
|
|
32
|
+
* own surface, in the deployment's scheme and tokens, instead of a blank white
|
|
33
|
+
* box; if the handshake never completes, the line says so. None of it is page
|
|
34
|
+
* data — the scheme and tokens are deployment configuration — and all of it is
|
|
35
|
+
* gone the moment `document.open` replaces this document with the page.
|
|
36
|
+
*/
|
|
37
|
+
function frameBootstrap(expectedOrigin, theme) {
|
|
25
38
|
return `<!doctype html>
|
|
26
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="referrer" content="no-referrer"
|
|
39
|
+
<html lang="en"${schemeAttribute(theme)}><head><meta charset="utf-8"><meta name="referrer" content="no-referrer">
|
|
40
|
+
<style>${TOKENS_CSS}${themeCss(theme)}html{background:var(--surface)}body{color:var(--muted);font:.875rem/1.5 var(--sans);margin:0;padding:1.25rem 1.5rem}</style>
|
|
41
|
+
</head><body>
|
|
42
|
+
<p id="frame-status" role="status">Loading page…</p>
|
|
27
43
|
<script>
|
|
28
44
|
(() => {
|
|
29
45
|
const expected = ${scriptSafeJson(expectedOrigin)};
|
|
30
46
|
if (window.parent === window) return;
|
|
47
|
+
const status = document.getElementById("frame-status");
|
|
48
|
+
const stalled = setTimeout(() => {
|
|
49
|
+
status.textContent = "This page did not load. Reload to try again.";
|
|
50
|
+
}, ${FRAME_STALL_MS});
|
|
31
51
|
const receive = (event) => {
|
|
32
52
|
if (event.source !== window.parent || event.origin !== expected) return;
|
|
33
53
|
const message = event.data;
|
|
34
54
|
if (!message || message.type !== "document" || typeof message.html !== "string") return;
|
|
35
55
|
window.removeEventListener("message", receive);
|
|
56
|
+
clearTimeout(stalled);
|
|
36
57
|
document.open();
|
|
37
58
|
document.write(message.html);
|
|
38
59
|
document.close();
|
|
@@ -152,6 +173,7 @@ export function artifactRoutes(options) {
|
|
|
152
173
|
document: buildFrameDocument({
|
|
153
174
|
kind: page.head.kind,
|
|
154
175
|
source: page.source,
|
|
176
|
+
theme: resolveBranding(context.opts.branding).theme,
|
|
155
177
|
global: {
|
|
156
178
|
id,
|
|
157
179
|
title: page.head.title,
|
|
@@ -169,7 +191,8 @@ export function artifactRoutes(options) {
|
|
|
169
191
|
if (path === "/artifacts/_frame") {
|
|
170
192
|
if (!read)
|
|
171
193
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
172
|
-
|
|
194
|
+
const bootstrap = frameBootstrap(new URL(context.baseUrl).origin, resolveBranding(context.opts.branding).theme);
|
|
195
|
+
return new Response(request.method === "HEAD" ? null : bootstrap, {
|
|
173
196
|
headers: {
|
|
174
197
|
"Content-Type": "text/html; charset=utf-8",
|
|
175
198
|
"Content-Security-Policy": csp,
|
|
@@ -215,6 +215,7 @@ export function checkView(input, context) {
|
|
|
215
215
|
if (kind === "markdown") {
|
|
216
216
|
let rendered;
|
|
217
217
|
try {
|
|
218
|
+
// The served page also carries themeCss (≤~0.5 KB), which FRAME_OVERHEAD_BYTES absorbs.
|
|
218
219
|
rendered = utf8Bytes(markdownPage(source, input.title ?? "")) + input.dataBytes + FRAME_OVERHEAD_BYTES;
|
|
219
220
|
}
|
|
220
221
|
catch (error) {
|