@zackbart/connecta 0.29.0 → 0.31.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/CHANGELOG.md +99 -0
- package/README.md +126 -134
- package/bin/connecta.mjs +13 -2
- package/bin/version.mjs +1 -1
- package/dist/argument-redaction.js +230 -0
- package/dist/auth-elicitation.js +14 -9
- package/dist/authorization-handoff.js +111 -0
- package/dist/call-recovery.js +25 -0
- package/dist/catalog-cache.js +2 -2
- package/dist/catalog-service.js +103 -63
- package/dist/config-schema.d.ts +14 -7
- package/dist/config-schema.js +29 -10
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -7
- package/dist/connectors/guarded-fetch.d.ts +9 -1
- package/dist/connectors/guarded-fetch.js +17 -5
- package/dist/connectors/negotiation-cache.js +1 -1
- package/dist/downstream-input-context.js +9 -0
- package/dist/downstream-input.js +13 -11
- package/dist/errors.d.ts +38 -12
- package/dist/errors.js +19 -9
- package/dist/execute.js +375 -43
- package/dist/executor-result.js +11 -9
- package/dist/executors/quickjs-child.js +12 -5
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +6 -1
- package/dist/executors/quickjs.js +20 -4
- package/dist/guest-runtime.js +52 -2
- package/dist/guest-types.d.ts +47 -5
- package/dist/index.d.ts +1 -1
- package/dist/invocation.js +72 -14
- package/dist/meta-output.js +0 -15
- package/dist/meta-tools.js +166 -95
- package/dist/oauth-handoff.js +88 -11
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +9 -0
- package/dist/operator-ui/view.js +11 -0
- package/dist/program-result.js +5 -0
- package/dist/program-writes.js +7 -48
- package/dist/provider.d.ts +8 -6
- package/dist/provider.js +3 -4
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/_shared/rest/dispatch.js +41 -0
- package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
- package/dist/providers/_shared/rest/operation-index.js +527 -0
- package/dist/providers/_shared/rest/tools.d.ts +141 -0
- package/dist/providers/_shared/rest/tools.js +569 -0
- package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
- package/dist/providers/_shared/rest/value-safety.js +580 -0
- package/dist/providers/cloudflare/graphql.js +385 -0
- package/dist/providers/cloudflare/index.d.ts +47 -51
- package/dist/providers/cloudflare/index.js +188 -1997
- package/dist/providers/cloudflare/openapi.generated.js +3347 -0
- package/dist/providers/cloudflare/rest.d.ts +37 -0
- package/dist/providers/cloudflare/rest.js +1056 -0
- package/dist/providers/cloudflare/skill.generated.js +7 -4
- package/dist/providers/cloudflare/value-safety.js +1247 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/providers/notion/index.d.ts +35 -31
- package/dist/providers/notion/index.js +60 -1499
- package/dist/providers/notion/openapi.generated.js +75 -0
- package/dist/providers/notion/rest.d.ts +24 -0
- package/dist/providers/notion/rest.js +1067 -0
- package/dist/providers/notion/skill.generated.js +5 -4
- package/dist/providers/notion/value-safety.js +31 -0
- package/dist/providers/stripe/index.d.ts +26 -20
- package/dist/providers/stripe/index.js +103 -130
- package/dist/providers/stripe/openapi.generated.js +649 -0
- package/dist/providers/stripe/rest.d.ts +14 -0
- package/dist/providers/stripe/rest.js +395 -0
- package/dist/providers/stripe/skill.generated.js +7 -5
- package/dist/providers/stripe/value-safety.js +705 -0
- package/dist/providers/vercel/index.d.ts +31 -22
- package/dist/providers/vercel/index.js +96 -853
- package/dist/providers/vercel/openapi.generated.js +456 -0
- package/dist/providers/vercel/rest.d.ts +20 -0
- package/dist/providers/vercel/rest.js +1002 -0
- package/dist/providers/vercel/skill.generated.js +4 -5
- package/dist/providers/vercel/value-safety.js +328 -0
- package/dist/registry.d.ts +8 -6
- package/dist/registry.js +134 -61
- package/dist/resource-read.js +4 -1
- package/dist/routes/mcp.js +11 -2
- package/dist/sent-secrets.d.ts +54 -13
- package/dist/sent-secrets.js +770 -231
- package/dist/skills.js +26 -16
- package/dist/sqlite.js +21 -6
- package/dist/storage/keys.js +20 -5
- package/dist/storage/memory.js +9 -4
- package/dist/storage/sql.js +18 -9
- package/dist/types.d.ts +17 -3
- package/dist/usage-guide.js +49 -40
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +2 -1
- package/dist/write-outcome.js +47 -0
- package/documentation/README.md +64 -0
- package/documentation/architecture.md +241 -97
- package/documentation/auth.md +138 -5
- package/documentation/code-mode.md +224 -107
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +170 -0
- package/documentation/meta-tools.md +142 -30
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +4 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-auth-migration.md +307 -0
- package/documentation/provider-migration-0.29.md +22 -5
- package/examples/worker/README.md +9 -2
- package/package.json +9 -2
- package/templates/node/README.md +9 -2
- package/templates/node/package.json +1 -1
- package/dist/providers/vercel/mcp-ownership.js +0 -212
|
@@ -3,6 +3,15 @@ import type { CatalogDriftReport } from "../types.js";
|
|
|
3
3
|
import type { CredentialManagementCapability, UiConnector, UiData, UiProblem } from "./model.js";
|
|
4
4
|
import type { FixPromptKind } from "./fix-prompts.js";
|
|
5
5
|
export declare function registrationPathLabel(path: NonNullable<UiConnector["registrationPath"]>): string;
|
|
6
|
+
/**
|
|
7
|
+
* A connector's source column: the maintained provider with the
|
|
8
|
+
* implementation it uses, since a dual provider's `auth` selects either the
|
|
9
|
+
* vendor's hosted MCP server or Connecta's REST connector.
|
|
10
|
+
*/
|
|
11
|
+
export declare function connectorSourceLabel(source: {
|
|
12
|
+
kind: string;
|
|
13
|
+
provider?: string;
|
|
14
|
+
}): string;
|
|
6
15
|
export type OperatorPage = "overview" | "tools" | "access" | "config" | "connector" | "tokens" | "connections" | "activity";
|
|
7
16
|
/** Pages the nav lists. */
|
|
8
17
|
export declare const OPERATOR_PAGES: readonly OperatorPage[];
|
package/dist/operator-ui/view.js
CHANGED
|
@@ -14,6 +14,17 @@ const REGISTRATION_LABELS = {
|
|
|
14
14
|
export function registrationPathLabel(path) {
|
|
15
15
|
return REGISTRATION_LABELS[path];
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* A connector's source column: the maintained provider with the
|
|
19
|
+
* implementation it uses, since a dual provider's `auth` selects either the
|
|
20
|
+
* vendor's hosted MCP server or Connecta's REST connector.
|
|
21
|
+
*/
|
|
22
|
+
export function connectorSourceLabel(source) {
|
|
23
|
+
if (!source.provider)
|
|
24
|
+
return source.kind;
|
|
25
|
+
const implementation = source.kind === "remote-mcp" ? "hosted MCP" : source.kind === "api" ? "REST" : source.kind;
|
|
26
|
+
return `${source.provider} · ${implementation}`;
|
|
27
|
+
}
|
|
17
28
|
/** Pages the nav lists. */
|
|
18
29
|
export const OPERATOR_PAGES = [
|
|
19
30
|
"overview",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Shared transport policy, independent of any optional executor implementation. */
|
|
2
|
+
export const PROGRAM_RESULT_INLINE_BYTES = 1024 * 1024;
|
|
3
|
+
/** Leave room for the bridge envelope and six-character JSON escapes per byte. */
|
|
4
|
+
export const PROGRAM_RESULT_PAGE_BYTES = 32 * 1024;
|
|
5
|
+
export const PROGRAM_RESULT_WIRE_BYTES = 240 * 1024;
|
package/dist/program-writes.js
CHANGED
|
@@ -3,55 +3,10 @@
|
|
|
3
3
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
4
4
|
import { Effect } from "effect";
|
|
5
5
|
import { jsonResult } from "./meta-tools.js";
|
|
6
|
+
import { classifyWriteOutcome } from "./write-outcome.js";
|
|
7
|
+
export { classifyWriteOutcome } from "./write-outcome.js";
|
|
6
8
|
/** Writes one program may send (`execute.maxWrites`), unless configured. */
|
|
7
9
|
export const DEFAULT_MAX_WRITES = CONFIG_DEFAULTS.execute.maxWrites;
|
|
8
|
-
/**
|
|
9
|
-
* Whether a write landed, as far as anyone can know.
|
|
10
|
-
*
|
|
11
|
-
* `unknown` is the outcome a result must never paper over: the call left
|
|
12
|
-
* connecta and no answer came back, so sending it again could do it twice and
|
|
13
|
-
* not sending it could leave it undone. That is a dispatched call that timed
|
|
14
|
-
* out, was cancelled, found the service unavailable, or failed any way that
|
|
15
|
-
* is not a verdict. A refusal code (`REFUSALS`) or a downstream tool's own
|
|
16
|
-
* `isError` is an answer, so it is `failed`. Response text does not decide
|
|
17
|
-
* whether an outcome is unknown. A call that was never dispatched is
|
|
18
|
-
* `failed`: nothing was sent.
|
|
19
|
-
* `result_processing_failed` means the downstream call completed.
|
|
20
|
-
*/
|
|
21
|
-
export function classifyWriteOutcome(outcome) {
|
|
22
|
-
if (outcome.ok)
|
|
23
|
-
return "ok";
|
|
24
|
-
if (!outcome.dispatched)
|
|
25
|
-
return "failed";
|
|
26
|
-
const code = outcome.error?.code;
|
|
27
|
-
if (code === "result_processing_failed")
|
|
28
|
-
return "ok";
|
|
29
|
-
if (code === "timeout" || code === "cancelled" || code === "unavailable") {
|
|
30
|
-
return "unknown";
|
|
31
|
-
}
|
|
32
|
-
if (code !== undefined && REFUSALS.has(code))
|
|
33
|
-
return "failed";
|
|
34
|
-
return outcome.answered === true ? "failed" : "unknown";
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Codes that say the other side refused the call rather than acted on it:
|
|
38
|
-
* the credential, the arguments, the resource, the rate, or a base version
|
|
39
|
-
* someone else already moved past (`conflict`). Anything else a
|
|
40
|
-
* connector reports after dispatch — `connector_call_failed` from a response
|
|
41
|
-
* too large to read, a redirect it would not follow, a body it could not
|
|
42
|
-
* parse, a 5xx — may come after the write landed, so only a downstream
|
|
43
|
-
* tool's own `isError` answer makes it a known failure.
|
|
44
|
-
*/
|
|
45
|
-
const REFUSALS = new Set([
|
|
46
|
-
"auth_required",
|
|
47
|
-
"downstream_oauth_required",
|
|
48
|
-
"provider_permission_denied",
|
|
49
|
-
"invalid_args",
|
|
50
|
-
"not_found",
|
|
51
|
-
"rate_limited",
|
|
52
|
-
"input_required_unsupported",
|
|
53
|
-
"conflict",
|
|
54
|
-
]);
|
|
55
10
|
/** What a dispatched write's outcome says about whether it landed. */
|
|
56
11
|
export function writeStateOf(outcome) {
|
|
57
12
|
const state = classifyWriteOutcome(outcome.ok
|
|
@@ -104,6 +59,10 @@ export class ProgramWrites {
|
|
|
104
59
|
get isClosed() {
|
|
105
60
|
return this.closed;
|
|
106
61
|
}
|
|
62
|
+
/** Whether a dispatched write completed successfully in this run. */
|
|
63
|
+
get hasSucceeded() {
|
|
64
|
+
return this.states.includes("ok");
|
|
65
|
+
}
|
|
107
66
|
close() {
|
|
108
67
|
this.closed = true;
|
|
109
68
|
}
|
|
@@ -151,7 +110,7 @@ export class ProgramWrites {
|
|
|
151
110
|
if (writes.unknown > 0) {
|
|
152
111
|
return errorEnvelope({
|
|
153
112
|
code: "write_outcome_unknown",
|
|
154
|
-
message: "A write this program sent has no known outcome, so that is the result rather than what the program returned. It will not be sent again. Check its target before doing anything that depends on it.",
|
|
113
|
+
message: "A write this program sent has no known outcome, so that is the result rather than what the program returned. It will not be sent again. Check its target before doing anything that depends on it. Argument echoes may be partial or absent; use the original arguments if reconciliation requires another call.",
|
|
155
114
|
retryable: false,
|
|
156
115
|
...(this.deadlines.length > 0 ? { details: this.deadlines[0] } : {}),
|
|
157
116
|
...(this.deadlines.length > 1 ? { timeouts: this.deadlines } : {}),
|
package/dist/provider.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { type Field } from "./config-schema.js";
|
|
|
2
2
|
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorUsageGuide, ToolClassification } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* How a provider reaches its vendor: the vendor's hosted MCP server, an
|
|
5
|
-
* `api()` connector over the vendor's HTTP API,
|
|
6
|
-
*
|
|
5
|
+
* `api()` connector over the vendor's HTTP API, one connector composing both
|
|
6
|
+
* with an explicit operation ownership map, or `"dual"`: the configured
|
|
7
|
+
* `auth.type` selects the implementation, OAuth reaching the hosted MCP server
|
|
8
|
+
* and a key reaching Connecta's REST connector.
|
|
7
9
|
*/
|
|
8
|
-
export type ProviderKind = "mcp" | "api" | "composed";
|
|
10
|
+
export type ProviderKind = "mcp" | "api" | "composed" | "dual";
|
|
9
11
|
/** Options every provider accepts, validated before `create` runs. */
|
|
10
12
|
export interface ProviderOptions {
|
|
11
13
|
/** What this connection is for and which decisions it answers. Required. */
|
|
@@ -35,11 +37,11 @@ export declare const PROVIDER_COMMON: {
|
|
|
35
37
|
maxPartitions: Field<unknown, unknown, true>;
|
|
36
38
|
};
|
|
37
39
|
};
|
|
40
|
+
instructions: Field<unknown, unknown, true>;
|
|
38
41
|
title: Field<unknown, unknown, true>;
|
|
39
42
|
authScope: Field<unknown, unknown, true>;
|
|
40
43
|
maxResultBytes: Field<unknown, unknown, true>;
|
|
41
44
|
purpose: Field<unknown, unknown, true>;
|
|
42
|
-
instructions: Field<unknown, unknown, true>;
|
|
43
45
|
};
|
|
44
46
|
/** The provider's maintained, connection-independent usage guide. */
|
|
45
47
|
export interface ProviderSkill {
|
|
@@ -105,8 +107,8 @@ export interface ProviderDefinition<O extends ProviderOptions> {
|
|
|
105
107
|
/** Closed factory options, declared with `optionsOf<O>()` for key parity. */
|
|
106
108
|
options: Field<unknown, unknown>;
|
|
107
109
|
/**
|
|
108
|
-
* Reviewed classification of the vendor's hosted MCP catalog. Hosted
|
|
109
|
-
* composed providers pass it to `remoteMcp({ classify })` through
|
|
110
|
+
* Reviewed classification of the vendor's hosted MCP catalog. Hosted,
|
|
111
|
+
* composed, and dual providers pass it to `remoteMcp({ classify })` through
|
|
110
112
|
* `ProviderContext.classify`, so the verdicts that classify live tools and
|
|
111
113
|
* the record a drift check reads cannot disagree. `api()` providers annotate
|
|
112
114
|
* each tool they author and must omit it.
|
package/dist/provider.js
CHANGED
|
@@ -20,7 +20,7 @@ export const PROVIDER_COMMON = {
|
|
|
20
20
|
callAdmission: CALL_ADMISSION,
|
|
21
21
|
};
|
|
22
22
|
const PROVIDER_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
23
|
-
const KINDS = new Set(["mcp", "api", "composed"]);
|
|
23
|
+
const KINDS = new Set(["mcp", "api", "composed", "dual"]);
|
|
24
24
|
function nonEmpty(value) {
|
|
25
25
|
return typeof value === "string" && value.trim() !== "";
|
|
26
26
|
}
|
|
@@ -40,7 +40,7 @@ export function defineProvider(definition) {
|
|
|
40
40
|
throw new Error(`${label} requires a non-empty title.`);
|
|
41
41
|
}
|
|
42
42
|
if (!KINDS.has(definition.kind)) {
|
|
43
|
-
throw new Error(`${label} kind must be "mcp", "api", or "
|
|
43
|
+
throw new Error(`${label} kind must be "mcp", "api", "composed", or "dual".`);
|
|
44
44
|
}
|
|
45
45
|
if (!nonEmpty(definition.skill?.content) || !nonEmpty(definition.skill.instructionsHeading)) {
|
|
46
46
|
throw new Error(`${label} skill requires non-empty content and instructionsHeading.`);
|
|
@@ -180,8 +180,7 @@ function providerConstructionError(provider, id, error) {
|
|
|
180
180
|
.replace(/^requires\s+/, "")
|
|
181
181
|
.replace(/^must be /, "")
|
|
182
182
|
.replace(/^(.+) must be /, "$1 to be ")
|
|
183
|
-
.replace(/^declares /, "a consistent declaration of ")
|
|
184
|
-
.replace(/^with headers or credential auth requires /, "headers or credential auth to declare ");
|
|
183
|
+
.replace(/^declares /, "a consistent declaration of ");
|
|
185
184
|
error.message = `${at} requires ${detail}`;
|
|
186
185
|
return error;
|
|
187
186
|
}
|
|
@@ -109,12 +109,12 @@ export declare const GOOGLE_WORKSPACE_OPTIONS: import("../../../config-schema.js
|
|
|
109
109
|
maxPartitions: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
110
110
|
};
|
|
111
111
|
};
|
|
112
|
+
instructions: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
112
113
|
title: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
113
114
|
maxResultBytes: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
114
115
|
baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
115
116
|
subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
116
117
|
purpose: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
117
|
-
instructions: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
118
118
|
};
|
|
119
119
|
};
|
|
120
120
|
/** The checked, construction-time half of {@link GoogleWorkspaceOptions}. */
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** In-memory tokens and credential-bound login flights shared only inside one request. */
|
|
2
|
+
import { ConnectorCallError } from "../../errors.js";
|
|
3
|
+
const LOGIN_TIMEOUT_MS = 15_000;
|
|
4
|
+
const MAX_CACHED_TOKENS = 512;
|
|
5
|
+
/** Each provider instance owns its cache, so no credential crosses an API origin. */
|
|
6
|
+
export function requestTokenCache(source) {
|
|
7
|
+
const tokens = new Map();
|
|
8
|
+
const requests = new WeakMap();
|
|
9
|
+
function start(auth, ctx) {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const timer = setTimeout(() => controller.abort(new ConnectorCallError("unavailable", "Machine identity login timed out.")), LOGIN_TIMEOUT_MS);
|
|
12
|
+
const flight = { controller, waiters: 0, settled: false, promise: Promise.resolve("") };
|
|
13
|
+
flight.promise = auth
|
|
14
|
+
.mint({ ...ctx, signal: controller.signal })
|
|
15
|
+
.then((minted) => {
|
|
16
|
+
// An abandoned login must not seed a token after cancellation.
|
|
17
|
+
controller.signal.throwIfAborted();
|
|
18
|
+
tokens.delete(auth.key);
|
|
19
|
+
tokens.set(auth.key, minted);
|
|
20
|
+
while (tokens.size > MAX_CACHED_TOKENS)
|
|
21
|
+
tokens.delete(tokens.keys().next().value);
|
|
22
|
+
return minted.accessToken;
|
|
23
|
+
})
|
|
24
|
+
.finally(() => {
|
|
25
|
+
flight.settled = true;
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
});
|
|
28
|
+
// All waiters may cancel before the transport observes its abort.
|
|
29
|
+
void flight.promise.catch(() => { });
|
|
30
|
+
return flight;
|
|
31
|
+
}
|
|
32
|
+
async function follow(flight, signal) {
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
flight.waiters++;
|
|
35
|
+
let onAbort;
|
|
36
|
+
try {
|
|
37
|
+
const aborted = new Promise((_, reject) => {
|
|
38
|
+
if (!signal)
|
|
39
|
+
return;
|
|
40
|
+
onAbort = () => reject(signal.reason);
|
|
41
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
42
|
+
});
|
|
43
|
+
return await Promise.race([flight.promise, aborted]);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
if (onAbort)
|
|
47
|
+
signal?.removeEventListener("abort", onAbort);
|
|
48
|
+
flight.waiters--;
|
|
49
|
+
if (!flight.settled && flight.waiters === 0)
|
|
50
|
+
flight.controller.abort();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
async token(ctx, rejected) {
|
|
55
|
+
ctx.signal?.throwIfAborted();
|
|
56
|
+
const auth = await source(ctx);
|
|
57
|
+
ctx.signal?.throwIfAborted();
|
|
58
|
+
const cached = tokens.get(auth.key);
|
|
59
|
+
if (cached && cached.accessToken !== rejected && cached.expiresAt > Date.now())
|
|
60
|
+
return cached.accessToken;
|
|
61
|
+
const scope = ctx.requestScope ?? ctx;
|
|
62
|
+
let perCredential = requests.get(scope);
|
|
63
|
+
if (!perCredential) {
|
|
64
|
+
perCredential = new Map();
|
|
65
|
+
requests.set(scope, perCredential);
|
|
66
|
+
}
|
|
67
|
+
let request = perCredential.get(auth.key);
|
|
68
|
+
if (!request) {
|
|
69
|
+
request = {};
|
|
70
|
+
perCredential.set(auth.key, request);
|
|
71
|
+
}
|
|
72
|
+
if (rejected !== undefined) {
|
|
73
|
+
// Keep the recovery outcome for the request, including failures. Later
|
|
74
|
+
// 401s can never turn this into a fresh-login loop.
|
|
75
|
+
request.recovery ??= start(auth, ctx);
|
|
76
|
+
return await follow(request.recovery, ctx.signal);
|
|
77
|
+
}
|
|
78
|
+
if (!request.initial || request.initial.settled || request.initial.controller.signal.aborted)
|
|
79
|
+
request.initial = start(auth, ctx);
|
|
80
|
+
return await follow(request.initial, ctx.signal);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Auth selects the implementation (decisions/0005): a `"dual"` provider sends
|
|
2
|
+
// OAuth to the vendor's hosted MCP server and a key to Connecta's own REST
|
|
3
|
+
// connector. The choice is made once, at construction, from `auth.type`; one
|
|
4
|
+
// connector id is one implementation, and a failure on one is never replayed
|
|
5
|
+
// against the other (INV-9).
|
|
6
|
+
import { remoteMcp } from "../../../connectors/remote-mcp.js";
|
|
7
|
+
/**
|
|
8
|
+
* A provider `create` that dispatches on `options.auth.type`. Pair it with
|
|
9
|
+
* `variants(["auth", "type"], …)` options, which refuse a missing or unknown
|
|
10
|
+
* type, and options the selected case does not accept, before this runs.
|
|
11
|
+
*/
|
|
12
|
+
export function byAuth(cases) {
|
|
13
|
+
return (id, options, provider) => {
|
|
14
|
+
const type = options.auth?.type;
|
|
15
|
+
const create = typeof type === "string" && Object.hasOwn(cases, type) ? cases[type] : undefined;
|
|
16
|
+
if (!create) {
|
|
17
|
+
throw new Error(`auth.type must be one of ${Object.keys(cases)
|
|
18
|
+
.map((name) => JSON.stringify(name))
|
|
19
|
+
.join(", ")}.`);
|
|
20
|
+
}
|
|
21
|
+
return create(id, options, provider);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The vendor's hosted MCP server, OAuth only: no bearer token, header, or
|
|
26
|
+
* operator credential reaches a hosted endpoint. The provider's reviewed
|
|
27
|
+
* classification classifies its catalog.
|
|
28
|
+
*/
|
|
29
|
+
export function hostedOAuth(id, provider, options) {
|
|
30
|
+
return remoteMcp(id, {
|
|
31
|
+
url: options.url,
|
|
32
|
+
...provider.connectorOptions,
|
|
33
|
+
title: options.title,
|
|
34
|
+
description: options.description,
|
|
35
|
+
auth: { type: "oauth" },
|
|
36
|
+
requireHttps: true,
|
|
37
|
+
classify: provider.classify,
|
|
38
|
+
...(options.callAdmission ? { callAdmission: options.callAdmission } : {}),
|
|
39
|
+
usageGuide: options.usageGuide,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { JsonSchema } from "../../../types.js";
|
|
2
|
+
export type RestMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
3
|
+
/**
|
|
4
|
+
* One request-side schema, compacted: `t` type, `e` enum, `f` format (only
|
|
5
|
+
* `binary`), `n` nullable, `d` description, `a` anyOf/oneOf, `p` properties,
|
|
6
|
+
* `r` required, `i` items, `m` additionalProperties, `x` truncated at the
|
|
7
|
+
* generation depth (unvalidated below), `$` a shared subtree.
|
|
8
|
+
*/
|
|
9
|
+
export interface SchemaNode {
|
|
10
|
+
readonly t?: string | readonly string[];
|
|
11
|
+
readonly e?: readonly unknown[];
|
|
12
|
+
readonly f?: string;
|
|
13
|
+
readonly n?: 1;
|
|
14
|
+
readonly d?: string;
|
|
15
|
+
readonly a?: readonly SchemaNode[];
|
|
16
|
+
readonly p?: Readonly<Record<string, SchemaNode>>;
|
|
17
|
+
readonly r?: readonly string[];
|
|
18
|
+
readonly i?: SchemaNode;
|
|
19
|
+
readonly m?: SchemaNode;
|
|
20
|
+
readonly x?: 1;
|
|
21
|
+
readonly $?: number;
|
|
22
|
+
}
|
|
23
|
+
/** `[method, path template, operationId, summary, tag index, server index?]` */
|
|
24
|
+
type OperationRow = readonly [string, string, string, string, number, number?];
|
|
25
|
+
/** What `scripts/generate-openapi.mjs` writes to `openapi.generated.ts`. */
|
|
26
|
+
export interface OpenApiData {
|
|
27
|
+
/** The pinned document's URL. */
|
|
28
|
+
readonly source: string;
|
|
29
|
+
/** The pin: a tag, commit, or content hash. */
|
|
30
|
+
readonly revision: string;
|
|
31
|
+
/** `sha256:` digest of the pinned document's bytes. */
|
|
32
|
+
readonly digest: string;
|
|
33
|
+
/** The document's `info.version`; Stripe sends it as `Stripe-Version`. */
|
|
34
|
+
readonly version: string;
|
|
35
|
+
/** `[0]` is the default origin; later entries are per-operation servers. */
|
|
36
|
+
readonly servers: readonly string[];
|
|
37
|
+
readonly tags: readonly string[];
|
|
38
|
+
readonly ops: readonly OperationRow[];
|
|
39
|
+
/** JSON: `{ d: SchemaNode[], o: (0 | [params, body])[] }`, aligned with `ops`. */
|
|
40
|
+
readonly details: string;
|
|
41
|
+
}
|
|
42
|
+
export interface Operation {
|
|
43
|
+
readonly method: RestMethod;
|
|
44
|
+
/** The path template, e.g. `/v1/customers/{customer}`. */
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly operationId: string;
|
|
47
|
+
readonly summary: string;
|
|
48
|
+
readonly tag: string;
|
|
49
|
+
/** A non-default origin this operation is served from, if any. */
|
|
50
|
+
readonly server: string | undefined;
|
|
51
|
+
/** Position in `OpenApiData.ops`. */
|
|
52
|
+
readonly row: number;
|
|
53
|
+
}
|
|
54
|
+
/** A concrete call matched to its operation. */
|
|
55
|
+
export interface OperationMatch {
|
|
56
|
+
readonly op: Operation;
|
|
57
|
+
/** Path parameter values, exactly as the caller sent them. */
|
|
58
|
+
readonly params: Readonly<Record<string, string>>;
|
|
59
|
+
}
|
|
60
|
+
/** One operation's request contract, expanded to JSON Schema for an agent. */
|
|
61
|
+
export interface OperationContract {
|
|
62
|
+
method: RestMethod;
|
|
63
|
+
path: string;
|
|
64
|
+
operationId: string;
|
|
65
|
+
summary: string;
|
|
66
|
+
server?: string;
|
|
67
|
+
parameters: {
|
|
68
|
+
name: string;
|
|
69
|
+
in: "path" | "query";
|
|
70
|
+
required: boolean;
|
|
71
|
+
description?: string;
|
|
72
|
+
schema: JsonSchema;
|
|
73
|
+
}[];
|
|
74
|
+
body?: {
|
|
75
|
+
contentType: string;
|
|
76
|
+
required?: true;
|
|
77
|
+
schema: JsonSchema;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** The read side of one vendor's pinned index. */
|
|
81
|
+
export declare class OperationIndex {
|
|
82
|
+
#private;
|
|
83
|
+
readonly version: string;
|
|
84
|
+
readonly revision: string;
|
|
85
|
+
readonly ops: readonly Operation[];
|
|
86
|
+
/** The methods any operation uses, in a stable order. */
|
|
87
|
+
readonly methods: readonly RestMethod[];
|
|
88
|
+
/**
|
|
89
|
+
* `vendor` is the tool-name prefix; `title` the display name in refusals.
|
|
90
|
+
* `slashParams` names path parameters whose values may carry an encoded
|
|
91
|
+
* "/" (`%2F`), such as an object key, and only as the path's last segment.
|
|
92
|
+
*/
|
|
93
|
+
constructor(data: OpenApiData, names: {
|
|
94
|
+
vendor: string;
|
|
95
|
+
title: string;
|
|
96
|
+
slashParams?: readonly string[];
|
|
97
|
+
});
|
|
98
|
+
/** The operation with this exact method and path template, if any. */
|
|
99
|
+
operation(method: string, template: string): Operation | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Match a concrete call to its operation. Literal segments outrank
|
|
102
|
+
* parameters, as OpenAPI requires (`/v1/customers/search` before
|
|
103
|
+
* `/v1/customers/{customer}`); the most specific template that fits decides,
|
|
104
|
+
* and a method it does not define is refused rather than retried against a
|
|
105
|
+
* looser template.
|
|
106
|
+
*/
|
|
107
|
+
resolve(method: string, path: string): OperationMatch;
|
|
108
|
+
/** Up to five operations whose paths most resemble a missed one. */
|
|
109
|
+
nearest(method: string, segments: readonly string[]): string[];
|
|
110
|
+
/**
|
|
111
|
+
* Rank operations for a free-text query: every query word must appear in
|
|
112
|
+
* the path, summary, operationId, or tag (plural-insensitive, prefixes of
|
|
113
|
+
* three letters count), and path matches weigh most. A query that starts
|
|
114
|
+
* with "/" also matches the templates that fit it.
|
|
115
|
+
*/
|
|
116
|
+
search(query: string, options: {
|
|
117
|
+
method?: RestMethod;
|
|
118
|
+
limit: number;
|
|
119
|
+
}): Operation[];
|
|
120
|
+
/** Whether the operation takes a body, and in which framing. */
|
|
121
|
+
bodyType(op: Operation): string | undefined;
|
|
122
|
+
/** The operation's request contract as JSON Schema, optionally one parameter only. */
|
|
123
|
+
contract(op: Operation, only?: string): OperationContract;
|
|
124
|
+
/**
|
|
125
|
+
* Check query and body names, required members, enums, and object/array
|
|
126
|
+
* shapes against the operation, to the depth the index carries. Scalars are
|
|
127
|
+
* not type-policed beyond that: vendors coerce form strings, and a wrong
|
|
128
|
+
* number is the vendor's to explain. `null` and `""` pass anywhere, since
|
|
129
|
+
* they are how form APIs unset a field.
|
|
130
|
+
*/
|
|
131
|
+
check(op: Operation, query: Readonly<Record<string, unknown>>, body: unknown): void;
|
|
132
|
+
}
|
|
133
|
+
export {};
|