@zackbart/connecta 0.26.0 → 0.26.2
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 +141 -0
- package/README.md +11 -0
- package/dist/access-tokens.d.ts +38 -0
- package/dist/access-tokens.js +294 -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/identity.js +2 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +13 -2
- package/dist/module-contracts.d.ts +5 -0
- package/dist/oauth-callback-outcome.js +43 -18
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +1 -0
- package/dist/operator-ui/view.js +304 -33
- package/dist/page-styles.js +7 -0
- package/dist/registry.js +16 -13
- package/dist/routes/access-tokens.js +110 -0
- 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/shared.d.ts +3 -1
- package/dist/routes/shared.js +6 -1
- package/dist/routes/ui.js +9 -1
- package/dist/server.js +8 -5
- package/dist/skills.js +6 -6
- package/dist/types.d.ts +5 -0
- package/dist/ui.d.ts +6 -1
- package/dist/ui.js +27 -64
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +19 -9
- package/documentation/auth.md +166 -2
- package/documentation/code-mode.md +15 -15
- package/documentation/meta-tools.md +12 -5
- package/ethos.md +1 -1
- package/package.json +5 -1
- package/templates/node/README.md +11 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +5 -1
package/dist/skills.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { boundedEchoText } from "./errors.js";
|
|
2
|
-
const ROUTE = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar
|
|
2
|
+
const ROUTE = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar read results with a small sample before proceeding.";
|
|
3
3
|
const RECOVERY = 'After auth_required use authorize_connector. After a truncated direct result use get_result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
|
|
4
4
|
/**
|
|
5
5
|
* The MCP instructions of a deployment whose programs pause at writes. The
|
|
@@ -8,7 +8,7 @@ const RECOVERY = 'After auth_required use authorize_connector. After a truncated
|
|
|
8
8
|
* only that programs *may* write reads each write after a lookup as a known
|
|
9
9
|
* one and sends them one prompt at a time.
|
|
10
10
|
*/
|
|
11
|
-
export const CONNECTA_INSTRUCTIONS = `Choose a route before discovery.
|
|
11
|
+
export const CONNECTA_INSTRUCTIONS = `Choose a route before discovery. One known read: call_tool; one known write: call_destructive_tool. Everything else starts with execute_code to discover, call, and return the answer: unknown addresses, reduction, multiple or dependent calls, and writes that follow reads. Keep authorized batches in one program. Sample unfamiliar reads. Reduce a one-time write's full result there or page a direct call with get_result; never repeat it to recover output. Non-exempt program writes pause unsent; resume_execution with the returned token, address, and args approves the call and continues the program. Approval "tool" covers later same-address calls in this run; other approval-required writes need their own approval. ${RECOVERY}`;
|
|
12
12
|
/**
|
|
13
13
|
* The instructions of a deployment without resumable writes, whose programs
|
|
14
14
|
* refuse every write: the routing it always had.
|
|
@@ -35,7 +35,7 @@ The minimum guest API is:
|
|
|
35
35
|
|
|
36
36
|
## Discover and select
|
|
37
37
|
|
|
38
|
-
Search and call in one run when schemas suffice.
|
|
38
|
+
Search and call in one run when schemas suffice. Sample unfamiliar reads. Reduce a one-time write's full result here or page a direct call with get_result; never repeat the write to recover output. Search distinct operations separately with 2–4 distinctive action/object terms.
|
|
39
39
|
|
|
40
40
|
For top-level catalog inspection or approval-required discovery, omit \`limit\` initially (the default is 8), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and available output shapes. An \`outputSchemaSource: "observed"\` shape is a hint, not a contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
|
|
41
41
|
|
|
@@ -43,12 +43,12 @@ For top-level catalog inspection or approval-required discovery, omit \`limit\`
|
|
|
43
43
|
- Use \`includeSchemas: "json"\` for programmatic schema inspection; compact schemas are text, not objects with \`.properties\`. Check connectorTitle for the account/environment, then address, purpose, annotations, inputs, and outputs. Never select only because a result ranks first or has fewer required inputs.
|
|
44
44
|
- Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
|
|
45
45
|
- Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
|
|
46
|
-
- Reduce through available output keys.
|
|
46
|
+
- Reduce through available output keys. An observed key is a hint; later results may differ. Do not guess collection roots such as \`items\` or \`results\`. If a match is missing, re-search or describe.
|
|
47
47
|
- Match provider identifiers and names exactly after resolving them from source data or a connector guide. A broad regular expression that merely finds a plausible value is not identity resolution.
|
|
48
48
|
- Preserve the schema's JSON types exactly: a numeric id is a number, not a numeric-looking string.
|
|
49
49
|
- Validate tabular headers, row arrays, and row widths before mapping them. Never let a header or partial row become data.
|
|
50
50
|
|
|
51
|
-
Only
|
|
51
|
+
Only explicitly \`readOnlyHint: true\` or config-exempt calls run unasked. With resumable writes, non-exempt approval-required calls pause unsent; \`resume_execution\` repeats the pending address, args, and token. Without them, use \`call_destructive_tool\` for writes. Keep each authorized batch in one resumable program, final post included. \`approval: "call"\` covers one write; \`approval: "tool"\` covers later same-address calls in this run. Other approval-required writes need approval. Replay requires the same call order. Sandbox code cannot widen host gates.
|
|
52
52
|
|
|
53
53
|
## Errors and repair
|
|
54
54
|
|
|
@@ -59,7 +59,7 @@ Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`detail
|
|
|
59
59
|
- \`write_outcome_unknown\`: sent but unanswered, never re-sent; check the target.
|
|
60
60
|
- \`execution_expired\`, \`_diverged\`, \`_interrupted\`: check \`writes\`, then rerun.
|
|
61
61
|
- \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
|
|
62
|
-
-
|
|
62
|
+
- Truncated direct call: page with \`get_result\`, never repeat a write. A program result has no page handle; reduce a read in a new program. After a write, check the target or report the gap.
|
|
63
63
|
- Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
|
|
64
64
|
|
|
65
65
|
For a direct call, \`resultMode: "value"\` unwraps the result. \`timeoutMs\` sets its deadline. Every call makes one attempt; use the returned error classification and retry hint to decide whether to reissue. \`diagnostics: true\` adds timing.
|
package/dist/types.d.ts
CHANGED
|
@@ -217,6 +217,11 @@ export interface ConnectorStatus {
|
|
|
217
217
|
state: ConnectorStatusState;
|
|
218
218
|
/** When state === "auth_required", the URL the operator should open. */
|
|
219
219
|
authorizationUrl?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Set by a non-forced `startAuth` that handed back a still-recent pending
|
|
222
|
+
* authorization URL instead of starting a new flow. Absent otherwise.
|
|
223
|
+
*/
|
|
224
|
+
authorizationReused?: boolean;
|
|
220
225
|
message?: string;
|
|
221
226
|
/**
|
|
222
227
|
* Drift observed the last time this connector served a catalog refresh *in
|
package/dist/ui.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { RegistryView } from "./registry.js";
|
|
|
7
7
|
import type { ConnectaBranding, Connector, ConnectorStatus, ToolDef, UiAuthConfig } from "./types.js";
|
|
8
8
|
export { filterUiConnectors, type CredentialManagementCapability, type UiConnector, type UiData, } from "./operator-ui/model.js";
|
|
9
9
|
export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
|
|
10
|
-
export type OperatorPage = "connections" | "activity" | "artifacts" | "artifact";
|
|
10
|
+
export type OperatorPage = "tokens" | "connections" | "activity" | "artifacts" | "artifact";
|
|
11
11
|
export declare function operatorPageForPath(path: string): OperatorPage | undefined;
|
|
12
12
|
export declare function operatorPageTitle(page: OperatorPage, configuredTitle: string): string;
|
|
13
13
|
/**
|
|
@@ -52,6 +52,11 @@ export declare function buildUiData(registry: RegistryView, baseUrl: string, ser
|
|
|
52
52
|
export declare function renderUiHtml(uiAuth?: UiAuthConfig, mcpUrl?: string, branding?: ConnectaBranding, nonce?: string, page?: OperatorPage, options?: {
|
|
53
53
|
/** Where the Connections page lives, when this shell is on another origin. */
|
|
54
54
|
homeUrl?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The origin that serves `/favicon.*`, when this shell is on another one:
|
|
57
|
+
* an artifact host answers those paths with 404. See `PageLayout.iconOrigin`.
|
|
58
|
+
*/
|
|
59
|
+
iconOrigin?: string | undefined;
|
|
55
60
|
}): string;
|
|
56
61
|
/** Mount the connection UI without enabling any storage or activity module. */
|
|
57
62
|
export declare function operatorUi(options?: {
|
package/dist/ui.js
CHANGED
|
@@ -6,7 +6,7 @@ import { runEdge } from "./runtime/run.js";
|
|
|
6
6
|
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
7
7
|
import { OPERATOR_UI_CSS, OPERATOR_UI_SCRIPT, } from "./operator-ui/generated.js";
|
|
8
8
|
export { filterUiConnectors, } from "./operator-ui/model.js";
|
|
9
|
-
import {
|
|
9
|
+
import { escapeHtml, isSafeHttpsUrl, notFoundResponse, renderPage, resolveBranding, } from "./branding.js";
|
|
10
10
|
export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
|
|
11
11
|
/**
|
|
12
12
|
* A JS string literal safe to inline in a script element. Escaping `/` keeps
|
|
@@ -17,6 +17,7 @@ function stringForInlineScript(value) {
|
|
|
17
17
|
}
|
|
18
18
|
const OPERATOR_PAGE_LABELS = {
|
|
19
19
|
connections: "Connections",
|
|
20
|
+
tokens: "Access tokens",
|
|
20
21
|
activity: "Activity",
|
|
21
22
|
artifacts: "Artifacts",
|
|
22
23
|
artifact: "Artifact",
|
|
@@ -24,6 +25,8 @@ const OPERATOR_PAGE_LABELS = {
|
|
|
24
25
|
/** `/artifacts/<id>` and its snapshots, `/artifacts/<id>/v/<version>`. */
|
|
25
26
|
const ARTIFACT_PAGE = /^\/artifacts\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?(?:\/v\/\d{1,9})?$/;
|
|
26
27
|
export function operatorPageForPath(path) {
|
|
28
|
+
if (path === "/tokens")
|
|
29
|
+
return "tokens";
|
|
27
30
|
if (path === "/")
|
|
28
31
|
return "connections";
|
|
29
32
|
if (path === "/activity")
|
|
@@ -98,13 +101,6 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
98
101
|
...detailOptions,
|
|
99
102
|
}));
|
|
100
103
|
}
|
|
101
|
-
function escapeHtmlAttr(value) {
|
|
102
|
-
return value
|
|
103
|
-
.replaceAll("&", "&")
|
|
104
|
-
.replaceAll('"', """)
|
|
105
|
-
.replaceAll("<", "<")
|
|
106
|
-
.replaceAll(">", ">");
|
|
107
|
-
}
|
|
108
104
|
function jsonForInlineScript(value) {
|
|
109
105
|
return JSON.stringify(value)
|
|
110
106
|
.replaceAll("<", "\\u003c")
|
|
@@ -145,71 +141,36 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
|
|
|
145
141
|
}
|
|
146
142
|
: (uiAuth ?? { kind: "bearer" });
|
|
147
143
|
const brand = resolveBranding(branding);
|
|
148
|
-
const title = operatorPageTitle(page, brand.pageTitle);
|
|
149
144
|
// When an operator shell ships a nonce-based CSP, every script it emits must
|
|
150
145
|
// carry that nonce to run; without a nonce the markup is unchanged.
|
|
151
146
|
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
|
|
152
|
-
// Top-left corner. With an owner set it reads "<owner> <product>"; without
|
|
153
|
-
// one the product label stands alone. Either half links out when the
|
|
154
|
-
// matching URL is configured.
|
|
155
|
-
const owner = brand.ownerName
|
|
156
|
-
? brand.ownerUrl
|
|
157
|
-
? `<a class="brand navlink" href="${escapeHtmlAttr(brand.ownerUrl)}">${escapeHtmlAttr(brand.ownerName)}</a>`
|
|
158
|
-
: `<span class="brand">${escapeHtmlAttr(brand.ownerName)}</span>`
|
|
159
|
-
: brand.productUrl
|
|
160
|
-
? `<a class="brand navlink" href="${escapeHtmlAttr(brand.productUrl)}">${escapeHtmlAttr(brand.productName)}</a>`
|
|
161
|
-
: `<span class="brand">${escapeHtmlAttr(brand.productName)}</span>`;
|
|
162
|
-
const product = brand.ownerName
|
|
163
|
-
? brand.productUrl
|
|
164
|
-
? `<a class="product navlink" href="${escapeHtmlAttr(brand.productUrl)}">${escapeHtmlAttr(brand.productName)}</a>`
|
|
165
|
-
: `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
|
|
166
|
-
: "";
|
|
167
147
|
const clerkScript = clerk && clerkScriptOrigin
|
|
168
|
-
? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${
|
|
148
|
+
? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${escapeHtml(clerk.publishableKey)}" src="${escapeHtml(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
|
|
169
149
|
: "";
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
<meta name="description" content="${escapeHtmlAttr(brand.description)}">
|
|
183
|
-
<link rel="icon" href="${escapeHtmlAttr(brand.faviconHref)}" type="image/svg+xml">
|
|
184
|
-
<link rel="shortcut icon" href="/favicon.ico">
|
|
185
|
-
<title>${escapeHtmlAttr(title)}</title>
|
|
186
|
-
${clerkScript}
|
|
187
|
-
<style>${OPERATOR_UI_CSS}${themeCss(brand.theme)}</style>
|
|
188
|
-
</head>
|
|
189
|
-
<body>
|
|
190
|
-
<a class="skip-link" href="#operatorContent">Skip to operator page</a>
|
|
191
|
-
<header class="masthead shell">
|
|
192
|
-
<div class="masthead-inner">
|
|
193
|
-
<div class="mast-nav">
|
|
194
|
-
${owner}
|
|
195
|
-
${product}
|
|
196
|
-
</div>
|
|
197
|
-
<div id="operatorNav"></div>
|
|
198
|
-
</div>
|
|
199
|
-
</header>
|
|
200
|
-
|
|
150
|
+
// The shared layout owns the head, scheme, theme, and masthead; the shell
|
|
151
|
+
// adds its own bundle (which already imports the shared tokens), the nav
|
|
152
|
+
// mount, and the app.
|
|
153
|
+
return renderPage(branding, {
|
|
154
|
+
title: operatorPageTitle(page, brand.pageTitle),
|
|
155
|
+
uiMounted: true,
|
|
156
|
+
iconOrigin: options.iconOrigin,
|
|
157
|
+
styles: OPERATOR_UI_CSS,
|
|
158
|
+
head: clerkScript,
|
|
159
|
+
skipTo: { id: "operatorContent", label: "Skip to operator page" },
|
|
160
|
+
mastheadEnd: '<div id="operatorNav"></div>',
|
|
161
|
+
body: `
|
|
201
162
|
<main id="operatorContent" class="page shell" tabindex="-1">
|
|
202
163
|
<div class="lead">
|
|
203
164
|
<h1>${OPERATOR_PAGE_LABELS[page]}</h1>
|
|
204
165
|
<div class="lead-copy">
|
|
205
|
-
<p>${
|
|
166
|
+
<p>${escapeHtml(brand.description)}</p>
|
|
206
167
|
<noscript><p class="msg">The operator pages need JavaScript. Nothing else here
|
|
207
168
|
does — agents reach this deployment through <span class="mono">/mcp</span>.</p></noscript>
|
|
208
169
|
</div>
|
|
209
170
|
</div>
|
|
210
171
|
</main>
|
|
211
|
-
|
|
212
|
-
|
|
172
|
+
`,
|
|
173
|
+
tail: `<script${nonceAttr}>
|
|
213
174
|
const AUTH = ${jsonForInlineScript(auth)};
|
|
214
175
|
const MCP_URL = ${jsonForInlineScript(mcpUrl)};
|
|
215
176
|
const INITIAL_PAGE = ${jsonForInlineScript(page)};
|
|
@@ -218,12 +179,11 @@ const TITLE_SUFFIX = ${jsonForInlineScript(brand.pageTitle)};
|
|
|
218
179
|
const PRODUCT_NAME = ${stringForInlineScript(brand.productName)};
|
|
219
180
|
const PRODUCT_DESCRIPTION = ${stringForInlineScript(brand.description)};
|
|
220
181
|
const PRODUCT_OPERATOR_LABEL = ${stringForInlineScript(brand.productName + " operator")};
|
|
221
|
-
${OPERATOR_UI_SCRIPT}</script
|
|
222
|
-
|
|
223
|
-
</html>`;
|
|
182
|
+
${OPERATOR_UI_SCRIPT}</script>`,
|
|
183
|
+
});
|
|
224
184
|
}
|
|
225
185
|
function ownsOperatorPath(reserved, path) {
|
|
226
|
-
if (path === "/activity")
|
|
186
|
+
if (path === "/activity" || path === "/tokens")
|
|
227
187
|
return true;
|
|
228
188
|
return reserved.some((pattern) => pattern.endsWith("/*")
|
|
229
189
|
? path.startsWith(pattern.slice(0, -1))
|
|
@@ -252,8 +212,11 @@ export function operatorUi(options = {}) {
|
|
|
252
212
|
// The pages' JSON API and the sandboxed frame; the module owns both,
|
|
253
213
|
// and this bundle imports none of it.
|
|
254
214
|
return (await artifacts?.handle(context)) ??
|
|
255
|
-
|
|
215
|
+
notFoundResponse(context.request, context.opts);
|
|
256
216
|
}
|
|
217
|
+
const tokenResponse = await context.opts.accessTokens?.handle(context);
|
|
218
|
+
if (tokenResponse)
|
|
219
|
+
return tokenResponse;
|
|
257
220
|
const routes = [
|
|
258
221
|
...(context.opts.credentialVault ? [routeCredentials] : []),
|
|
259
222
|
routeOAuthManagement,
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -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
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
@@ -604,6 +614,6 @@ compiling and configuring the real thing.
|
|
|
604
614
|
connector limiters, then the executor, then the Connecta's runtime; Node's `listen()` calls it on
|
|
605
615
|
SIGTERM/SIGINT.
|
|
606
616
|
- **Structural mistakes throw at construction.** A duplicate connector id, an
|
|
607
|
-
invalid admission rule, the
|
|
617
|
+
invalid admission rule, the old boolean `accessTokens` option, a missing executor:
|
|
608
618
|
all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
|
|
609
619
|
in the wrong shape is worse than not starting.
|
package/documentation/auth.md
CHANGED
|
@@ -7,7 +7,7 @@ static bearers are checked first, then other providers in configuration order.
|
|
|
7
7
|
An `InboundAuth` provider's `authorize(request, baseUrl, runtimeContext)`
|
|
8
8
|
returns either `{ ok: true, userId?, subjectId?, principal? }` or a refusal
|
|
9
9
|
carrying its own `Response`, so the provider owns its challenge. Connecta
|
|
10
|
-
issues
|
|
10
|
+
issues managed client tokens only when the optional `accessTokens` module is configured.
|
|
11
11
|
|
|
12
12
|
The bearer adapter challenges with `WWW-Authenticate: Bearer` and deliberately
|
|
13
13
|
omits `resource_metadata`: its credential is configured out of band, so it has no
|
|
@@ -16,6 +16,67 @@ or the edge own OAuth discovery. An open deployment with any connector warns at
|
|
|
16
16
|
construction — including API connectors carrying static auth headers, and with
|
|
17
17
|
sharper wording for credential and OAuth connectors.
|
|
18
18
|
|
|
19
|
+
## Managed client tokens and upgrading from v0.23
|
|
20
|
+
|
|
21
|
+
Import `accessTokens` from `@zackbart/connecta/auth/access-tokens` and pass its
|
|
22
|
+
module to `createConnecta`. It installs its inbound adapter and, beside `ui`,
|
|
23
|
+
the Access tokens page and `/ui/access-tokens` lifecycle routes. Omitting the
|
|
24
|
+
module loads none of its implementation and serves none of those routes.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { accessTokens } from "@zackbart/connecta/auth/access-tokens";
|
|
28
|
+
|
|
29
|
+
createConnecta({
|
|
30
|
+
storage,
|
|
31
|
+
accessTokens: accessTokens(storage),
|
|
32
|
+
auth: clerkAuth({ publishableKey, secretKey }),
|
|
33
|
+
ui: operatorUi(),
|
|
34
|
+
identity: {
|
|
35
|
+
connectorAccess, // Keep the existing principal and token-id grant rules.
|
|
36
|
+
accessTokenManagement: ({ principal }) =>
|
|
37
|
+
principal?.namespace === "clerk:your-existing-namespace" &&
|
|
38
|
+
principal.id === "your-operator-id",
|
|
39
|
+
},
|
|
40
|
+
connectors,
|
|
41
|
+
executor,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For a v0.23 deployment, replace the old `accessTokens: true` or options object
|
|
46
|
+
with `accessTokens: accessTokens(storage)`, using **the same storage and key
|
|
47
|
+
namespace**. Keep the existing `access-token:v1:record:*` and
|
|
48
|
+
`access-token:v1:lookup:*` records. Their unrevoked `cta_…` secrets keep working;
|
|
49
|
+
no secret recovery, rewrite, or client rotation is needed. Preserve the existing
|
|
50
|
+
identity namespaces and connector/pool grant callbacks too. Storage compatibility
|
|
51
|
+
does not translate deployment configuration or invent replacement grants.
|
|
52
|
+
|
|
53
|
+
The adapter preserves `access_token` actor ids, the
|
|
54
|
+
`connecta:access-tokens:v1` activity namespace, friendly names, and any stored
|
|
55
|
+
principal. A token without a principal stays unbound. Every request evaluates
|
|
56
|
+
current identity/tool/pool grants; a token never becomes an interactive operator.
|
|
57
|
+
Names are labels, never permissions. New UI-issued tokens belong to the issuing
|
|
58
|
+
human's principal, and any explicitly permitted token manager can list, rename,
|
|
59
|
+
or revoke deployment tokens. Static and managed bearers cannot manage tokens or
|
|
60
|
+
connection credentials. Operators need an interactive auth provider.
|
|
61
|
+
|
|
62
|
+
Storage must implement `list`. Existing-token verification, rename, and revoke
|
|
63
|
+
also work on older adapters without `compareAndSet`; **new issuance requires
|
|
64
|
+
atomic `compareAndSet`**, because counting records before writing admits too many
|
|
65
|
+
concurrent creates. Active capacity defaults to 100, configurable with
|
|
66
|
+
`accessTokens(storage, { maxActive: 200 })`, up to 1,000. A durable reservation
|
|
67
|
+
counts before a secret is written. An interrupted create can consume capacity
|
|
68
|
+
without returning a token; it is never automatically retried or released after
|
|
69
|
+
an uncertain write. Avoid creating new tokens through old-version instances once
|
|
70
|
+
new-version issuance has started, since those instances do not honor reservations.
|
|
71
|
+
|
|
72
|
+
Secrets contain 256 random bits and only their SHA-256 digests persist. Creation
|
|
73
|
+
returns the secret once; list and rename never return it. Revocation removes its
|
|
74
|
+
lookup before updating metadata, and authorization has no token cache. A strongly
|
|
75
|
+
consistent store makes revocation effective on the next authorization; an
|
|
76
|
+
eventually consistent backend retains its own propagation delay. Requests already
|
|
77
|
+
admitted are not recalled. Management writes require an exact same-origin Origin,
|
|
78
|
+
and responses are private and non-cacheable.
|
|
79
|
+
|
|
19
80
|
## Origins
|
|
20
81
|
|
|
21
82
|
`allowedOrigins?: readonly string[] | "*"` bounds which browsers may speak to
|
|
@@ -250,6 +311,87 @@ means authorizing again. A vault without the optional `seal`/`open` members
|
|
|
250
311
|
keeps these values plaintext and draws a startup warning. Without any vault,
|
|
251
312
|
nothing changes: the state is plaintext, as it always was.
|
|
252
313
|
|
|
314
|
+
## Starting, restarting, and retiring an OAuth epoch
|
|
315
|
+
|
|
316
|
+
Every downstream OAuth value lives under an **epoch**, the value of
|
|
317
|
+
`oauth:generation`. A restart or disconnect publishes a new one before it
|
|
318
|
+
deletes anything, and a write from a flow that captured an older epoch lands
|
|
319
|
+
in that epoch's own key namespace, where no reader looks. The fence is what
|
|
320
|
+
keeps a retired grant out of use; deleting the old keys is hygiene on top.
|
|
321
|
+
|
|
322
|
+
The operator's `POST /ui/oauth/<id>` takes a `mode` query parameter:
|
|
323
|
+
|
|
324
|
+
| Request | What it does |
|
|
325
|
+
| --- | --- |
|
|
326
|
+
| `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. |
|
|
327
|
+
| `?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. |
|
|
328
|
+
| `DELETE /ui/oauth/<id>` | Disconnect: a disconnected epoch that passive reads never turn back into a consent flow. |
|
|
329
|
+
|
|
330
|
+
Any other `mode`, or more than one, is a 400 before anything starts; the
|
|
331
|
+
management permission, visibility, and principal checks are the same for
|
|
332
|
+
both modes, and a personal connector continues only the caller's own flow.
|
|
333
|
+
A 200 answers `{ state, authorizationUrl?, reused? }`, with `reused` present
|
|
334
|
+
beside every URL: `true` when an earlier start's URL came back unchanged. A
|
|
335
|
+
continue that reused a URL or found the connection healthy changed nothing,
|
|
336
|
+
so it leaves the cached catalog alone; every other start invalidates it. The
|
|
337
|
+
ten minutes are `PENDING_AUTHORIZATION_MAX_AGE_MS`. Connecta never expires
|
|
338
|
+
its own half of a URL, but authorization servers expire theirs, and a fresh
|
|
339
|
+
start that keeps the registration costs one authorization request. A URL
|
|
340
|
+
stored without a write time — by an earlier release, or by a connector never
|
|
341
|
+
reset since epochs arrived — is stale by definition. `authorize_connector`
|
|
342
|
+
without `force` follows the same rule.
|
|
343
|
+
|
|
344
|
+
Continue trusts the stored registration, which carries the `redirect_uris`
|
|
345
|
+
it was registered with. After a `publicUrl` change the authorization server
|
|
346
|
+
refuses that client's new callback at consent. Restart registers a client
|
|
347
|
+
for the current URL and recovers.
|
|
348
|
+
|
|
349
|
+
A restart cannot know which retired epochs a late write reached, so it
|
|
350
|
+
publishes a **cleanup lineage** under the new epoch before activating it:
|
|
351
|
+
the retired epochs, as the same plain list of names every release reads, and
|
|
352
|
+
beside it a record of when each retired. A published lineage is never
|
|
353
|
+
rewritten. A stale writer whose own cleanup fails appends to the live
|
|
354
|
+
lineage, and a restart that rewrote the list it had read could drop that
|
|
355
|
+
append. An epoch leaves the lineage only by being left out of its
|
|
356
|
+
successor's.
|
|
357
|
+
|
|
358
|
+
A restart does the same storage work however many came before it. After the
|
|
359
|
+
fence it deletes the one epoch it retired: six values, then that epoch's own
|
|
360
|
+
lineage records. An epoch's manifest outlives its values only when their
|
|
361
|
+
deletion failed, so the restart also reads the manifests of the eight most
|
|
362
|
+
recently retired epochs (`RETRY_PROBES`) and cleans up again any that still
|
|
363
|
+
have one. A Disconnect or Restart that reported a failed cleanup therefore
|
|
364
|
+
deletes the old grant when the operator retries it. Before publishing, it
|
|
365
|
+
sweeps up to 16 epochs retired more than `CLEANUP_GRACE_MS` (24 hours) ago,
|
|
366
|
+
oldest first, and leaves out only those whose values and records were all
|
|
367
|
+
deleted. A failed sweep is carried and tried again later, and never fails
|
|
368
|
+
the restart. Any other epoch inside its grace is not deleted again. A late write into it is unreadable behind the fence,
|
|
369
|
+
and the late writer deletes it itself. If that delete fails, the writer
|
|
370
|
+
records the epoch again as retired at that moment, appending it or, when it
|
|
371
|
+
is already listed, moving only its time. A restart that swept it re-reads the
|
|
372
|
+
lineage before publishing and keeps it. Whatever remains is swept once the
|
|
373
|
+
grace has passed. Two such writers racing can lose one time update, which
|
|
374
|
+
leaves the earlier time; a time missing altogether reads as retired at the
|
|
375
|
+
moment of the restart that reads it, which only lengthens a grace.
|
|
376
|
+
|
|
377
|
+
**The assumption:** no request holds a retired epoch for a day. The writers
|
|
378
|
+
that can land late are OAuth flows, refreshes, and readers whose Workers KV
|
|
379
|
+
replica still serves the old generation for a minute or more; none of them
|
|
380
|
+
run that long. If it fails, only a writer that dies between its write and
|
|
381
|
+
its own cleanup leaves residue no restart tracks, and that residue is never
|
|
382
|
+
readable.
|
|
383
|
+
|
|
384
|
+
The lineage holds at most 5,000 epochs, the ones retired within the last day
|
|
385
|
+
plus any the sweep has not reached. A restart that would exceed that is
|
|
386
|
+
refused before the fence moves, which takes more than 4,000 restarts of one
|
|
387
|
+
connector in a day on top of the 1,000 an earlier release allowed. A
|
|
388
|
+
connector that reached that release's 1,000 wall restarts again the day it
|
|
389
|
+
upgrades, and its old entries drain 16 per restart a day later. An older
|
|
390
|
+
release ignores the times, so a rollback still restarts unless a lineage has
|
|
391
|
+
grown past its own 1,000 cap. Deletes run six at a time, the Workers limit
|
|
392
|
+
on simultaneous connections, and catalog invalidation deletes its chunks
|
|
393
|
+
concurrently under the registry's chunk I/O bound.
|
|
394
|
+
|
|
253
395
|
## Refresh failures
|
|
254
396
|
|
|
255
397
|
A failed token refresh means one of two things, and connecta decides which
|
|
@@ -328,7 +470,7 @@ from caller input.
|
|
|
328
470
|
interactive human, the one default here that is open, because a single-operator
|
|
329
471
|
deployment would otherwise be locked out of its own event stream. Team
|
|
330
472
|
deployments should set it. There is no general administrator role and no
|
|
331
|
-
token-management authority.
|
|
473
|
+
implicit token-management authority. `identity.accessTokenManagement` is a separate boolean permission, false by default, evaluated only for interactive humans. Lifecycle routes also require a stable principal.
|
|
332
474
|
|
|
333
475
|
```ts
|
|
334
476
|
createConnecta({
|
|
@@ -429,6 +571,28 @@ instead of a base64 stack on every route.
|
|
|
429
571
|
|
|
430
572
|
## Human authentication management
|
|
431
573
|
|
|
574
|
+
An operator's OAuth Connect or Restart request gives the start and its handoff
|
|
575
|
+
30 seconds. The request's abort signal and that deadline reach downstream OAuth
|
|
576
|
+
fetches, including metadata discovery and dynamic registration. If the deadline
|
|
577
|
+
expires, the route returns `504` with `OAuth authorization start timed out` after
|
|
578
|
+
an already-started reset finishes. Storage has no cancellation contract: its
|
|
579
|
+
generation write must drain before a response, or it could publish an old epoch
|
|
580
|
+
over a newer flow. Catalog invalidation and connector scope close also finish
|
|
581
|
+
outside the deadline. Those storage and cleanup waits can extend the click past
|
|
582
|
+
30 seconds; the bound is on downstream work, where a hung authorization server
|
|
583
|
+
was the failure. The OAuth provider checks its signal before writing and removes
|
|
584
|
+
a cancelled value write that finishes late. An operator Disconnect keeps its
|
|
585
|
+
existing commit-through behavior even if the browser leaves.
|
|
586
|
+
|
|
587
|
+
Restart retains a dynamically registered client only when its stored issuer and
|
|
588
|
+
the connector's URL, redirect URI, client metadata, auth scope, transport settings,
|
|
589
|
+
and owner partition still match. It drops the grant and one-shot flow state,
|
|
590
|
+
selects the authorization server again, and re-seals the retained client under
|
|
591
|
+
the replacement epoch key. A different issuer registers a new client. Disconnect
|
|
592
|
+
and issuer-mismatch recovery discard it. The SDK has no client-validation
|
|
593
|
+
exchange while constructing a fresh consent URL; a provider that revokes a
|
|
594
|
+
retained client may refuse it only at consent or callback, after this request.
|
|
595
|
+
|
|
432
596
|
Credential and OAuth mutation require an admitted interactive human, connector
|
|
433
597
|
visibility, the appropriate shared or personal permission, and an exact
|
|
434
598
|
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
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
532
|
-
|
|
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).**
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
73
|
-
|
|
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.
|
|
85
|
-
|
|
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/ethos.md
CHANGED
|
@@ -40,7 +40,7 @@ carries them.
|
|
|
40
40
|
| Optional deployment modules | accepted | typed slots select UI, activity, vault, and inbound auth; core keeps discovery, execution, invocation, and enforcement |
|
|
41
41
|
| Artifacts module | accepted | a built-in connector for team-only sandboxed pages over stored JSON; immutable versions let writes skip approval. Supersedes [#287](https://github.com/zackbart/connecta/issues/287) |
|
|
42
42
|
| 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)) |
|
|
43
|
-
| Connecta-issued access tokens |
|
|
43
|
+
| Connecta-issued access tokens | accepted | optional /auth/access-tokens preserves v0.23 secrets; config owns grants, interactive management needs explicit permission ([#619](https://github.com/zackbart/connecta/issues/619)) |
|
|
44
44
|
| Expanded Notion page create/update options | refused | different workflows, not missing fields; use `api()` ([#408](https://github.com/zackbart/connecta/issues/408)) |
|
|
45
45
|
| Resources, prompts, and downstream MCP Apps templates | refused | tools only; clients own presentation ([#266](https://github.com/zackbart/connecta/issues/266)) |
|
|
46
46
|
| Protocol sessions, server push, elicitation passthrough | refused | stateless per request; elicitation has no route |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
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.",
|
|
@@ -114,6 +114,10 @@
|
|
|
114
114
|
"./artifacts": {
|
|
115
115
|
"types": "./dist/artifacts.d.ts",
|
|
116
116
|
"import": "./dist/artifacts.js"
|
|
117
|
+
},
|
|
118
|
+
"./auth/access-tokens": {
|
|
119
|
+
"types": "./dist/access-tokens.d.ts",
|
|
120
|
+
"import": "./dist/access-tokens.js"
|
|
117
121
|
}
|
|
118
122
|
},
|
|
119
123
|
"scripts": {
|