@zackbart/connecta 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +28 -15
- package/CHANGELOG.md +466 -0
- package/README.md +19 -4
- package/bin/connecta.mjs +44 -3
- package/dist/access-tokens.d.ts +0 -1
- package/dist/access-tokens.js +0 -1
- package/dist/activity.d.ts +50 -2
- package/dist/activity.js +34 -1
- package/dist/apps-shell.d.ts +0 -1
- package/dist/apps-shell.js +0 -1
- package/dist/auth/bearer.d.ts +0 -1
- package/dist/auth/bearer.js +0 -1
- package/dist/auth/clerk.d.ts +0 -1
- package/dist/auth/clerk.js +45 -9
- package/dist/auth/downstream-oauth.d.ts +0 -1
- package/dist/auth/downstream-oauth.js +0 -1
- package/dist/call-admission.d.ts +0 -1
- package/dist/call-admission.js +0 -1
- package/dist/catalog-drift.d.ts +99 -0
- package/dist/catalog-drift.js +251 -0
- package/dist/catalog-fingerprint.d.ts +0 -1
- package/dist/catalog-fingerprint.js +0 -1
- package/dist/catalog-limits.d.ts +0 -1
- package/dist/catalog-limits.js +0 -1
- package/dist/catalog-service.d.ts +0 -1
- package/dist/catalog-service.js +0 -1
- package/dist/catalog.d.ts +0 -1
- package/dist/catalog.js +0 -1
- package/dist/concurrency.d.ts +0 -1
- package/dist/concurrency.js +0 -1
- package/dist/connector-scope.d.ts +0 -1
- package/dist/connector-scope.js +0 -1
- package/dist/connectors/api.d.ts +29 -21
- package/dist/connectors/api.js +45 -19
- package/dist/connectors/guarded-fetch.d.ts +106 -0
- package/dist/connectors/guarded-fetch.js +289 -0
- package/dist/connectors/remote-mcp.d.ts +0 -1
- package/dist/connectors/remote-mcp.js +0 -1
- package/dist/credentials.d.ts +0 -1
- package/dist/credentials.js +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +0 -1
- package/dist/execute.d.ts +0 -1
- package/dist/execute.js +0 -1
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +28 -1
- package/dist/executor-result.d.ts +0 -1
- package/dist/executor-result.js +0 -1
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-child.js +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -1
- package/dist/executors/quickjs-protocol.js +0 -1
- package/dist/executors/quickjs-runtime.d.ts +0 -1
- package/dist/executors/quickjs-runtime.js +0 -1
- package/dist/executors/quickjs.d.ts +0 -1
- package/dist/executors/quickjs.js +2 -1
- package/dist/favicon.d.ts +0 -1
- package/dist/favicon.js +0 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +24 -7
- package/dist/invocation.d.ts +0 -1
- package/dist/invocation.js +0 -1
- package/dist/json-schema.d.ts +0 -1
- package/dist/json-schema.js +0 -1
- package/dist/mcp-result.d.ts +0 -1
- package/dist/mcp-result.js +0 -1
- package/dist/meta-tools.d.ts +0 -1
- package/dist/meta-tools.js +0 -1
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -1
- package/dist/operator-ui/generated.d.ts +2 -3
- package/dist/operator-ui/generated.js +3 -4
- package/dist/operator-ui/model.d.ts +10 -1
- package/dist/operator-ui/model.js +0 -1
- package/dist/operator-ui/view.d.ts +167 -0
- package/dist/operator-ui/view.js +270 -0
- package/dist/providers/cloudflare.d.ts +0 -1
- package/dist/providers/cloudflare.js +250 -276
- package/dist/providers/linear.d.ts +26 -5
- package/dist/providers/linear.js +32 -61
- package/dist/providers/mixpanel.d.ts +35 -4
- package/dist/providers/mixpanel.js +53 -79
- package/dist/providers/notion.d.ts +0 -1
- package/dist/providers/notion.js +87 -42
- package/dist/providers/stripe.d.ts +13 -1
- package/dist/providers/stripe.js +31 -60
- package/dist/registry.d.ts +34 -2
- package/dist/registry.js +85 -5
- package/dist/retired-toolkits.d.ts +0 -1
- package/dist/retired-toolkits.js +0 -1
- package/dist/routes/access-tokens.d.ts +0 -1
- package/dist/routes/access-tokens.js +0 -1
- package/dist/routes/activity.d.ts +0 -1
- package/dist/routes/activity.js +0 -1
- package/dist/routes/credentials.d.ts +0 -1
- package/dist/routes/credentials.js +0 -1
- package/dist/routes/mcp.d.ts +0 -1
- package/dist/routes/mcp.js +0 -1
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +0 -1
- package/dist/routes/shared.d.ts +2 -1
- package/dist/routes/shared.js +0 -1
- package/dist/routes/ui.d.ts +0 -1
- package/dist/routes/ui.js +0 -1
- package/dist/server.d.ts +0 -1
- package/dist/server.js +11 -1
- package/dist/skills.d.ts +0 -1
- package/dist/skills.js +0 -1
- package/dist/storage/file.d.ts +0 -1
- package/dist/storage/file.js +0 -1
- package/dist/storage/memory.d.ts +0 -1
- package/dist/storage/memory.js +0 -1
- package/dist/timeout.d.ts +0 -1
- package/dist/timeout.js +0 -1
- package/dist/tool-safety.d.ts +0 -1
- package/dist/tool-safety.js +0 -1
- package/dist/types.d.ts +45 -1
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +0 -1
- package/dist/ui.js +13 -137
- package/dist/validate.d.ts +13 -15
- package/dist/validate.js +24 -11
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -2
- package/documentation/architecture.md +186 -5
- package/documentation/auth.md +11 -0
- package/documentation/call-admission.md +142 -6
- package/documentation/cloudflare.md +58 -8
- package/documentation/code-mode.md +12 -12
- package/documentation/connector-guides.md +172 -5
- package/documentation/connectors.md +89 -0
- package/documentation/linear.md +20 -2
- package/documentation/mcp-ui-design.md +2 -2
- package/documentation/mixpanel.md +42 -9
- package/documentation/notion.md +16 -0
- package/documentation/operations.md +283 -5
- package/documentation/operator-ui.md +120 -5
- package/documentation/provider-audit.md +160 -0
- package/documentation/provider-conventions.md +631 -0
- package/documentation/request-admission.md +167 -5
- package/documentation/rich-output-design.md +1 -2
- package/documentation/stripe.md +15 -0
- package/ethos.md +13 -8
- package/examples/worker/README.md +96 -12
- package/examples/worker/src/index.ts +46 -1
- package/examples/worker/wrangler.jsonc +14 -1
- package/package.json +4 -5
- package/templates/node/.dockerignore +10 -0
- package/templates/node/.env.example +40 -3
- package/templates/node/AGENTS.md +11 -1
- package/templates/node/Dockerfile +49 -0
- package/templates/node/README.md +117 -2
- package/templates/node/docker-compose.yml +39 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +182 -0
- package/templates/node/src/index.ts +80 -4
- package/assets/connecta-clay-hero.png +0 -0
- package/dist/access-tokens.d.ts.map +0 -1
- package/dist/access-tokens.js.map +0 -1
- package/dist/activity.d.ts.map +0 -1
- package/dist/activity.js.map +0 -1
- package/dist/apps-shell.d.ts.map +0 -1
- package/dist/apps-shell.js.map +0 -1
- package/dist/auth/bearer.d.ts.map +0 -1
- package/dist/auth/bearer.js.map +0 -1
- package/dist/auth/clerk.d.ts.map +0 -1
- package/dist/auth/clerk.js.map +0 -1
- package/dist/auth/downstream-oauth.d.ts.map +0 -1
- package/dist/auth/downstream-oauth.js.map +0 -1
- package/dist/call-admission.d.ts.map +0 -1
- package/dist/call-admission.js.map +0 -1
- package/dist/catalog-fingerprint.d.ts.map +0 -1
- package/dist/catalog-fingerprint.js.map +0 -1
- package/dist/catalog-limits.d.ts.map +0 -1
- package/dist/catalog-limits.js.map +0 -1
- package/dist/catalog-service.d.ts.map +0 -1
- package/dist/catalog-service.js.map +0 -1
- package/dist/catalog.d.ts.map +0 -1
- package/dist/catalog.js.map +0 -1
- package/dist/concurrency.d.ts.map +0 -1
- package/dist/concurrency.js.map +0 -1
- package/dist/connector-scope.d.ts.map +0 -1
- package/dist/connector-scope.js.map +0 -1
- package/dist/connectors/api.d.ts.map +0 -1
- package/dist/connectors/api.js.map +0 -1
- package/dist/connectors/remote-mcp.d.ts.map +0 -1
- package/dist/connectors/remote-mcp.js.map +0 -1
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js.map +0 -1
- package/dist/executor-admission.d.ts.map +0 -1
- package/dist/executor-admission.js.map +0 -1
- package/dist/executor-result.d.ts.map +0 -1
- package/dist/executor-result.js.map +0 -1
- package/dist/executors/quickjs-child.d.ts.map +0 -1
- package/dist/executors/quickjs-child.js.map +0 -1
- package/dist/executors/quickjs-protocol.d.ts.map +0 -1
- package/dist/executors/quickjs-protocol.js.map +0 -1
- package/dist/executors/quickjs-runtime.d.ts.map +0 -1
- package/dist/executors/quickjs-runtime.js.map +0 -1
- package/dist/executors/quickjs.d.ts.map +0 -1
- package/dist/executors/quickjs.js.map +0 -1
- package/dist/favicon.d.ts.map +0 -1
- package/dist/favicon.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invocation.d.ts.map +0 -1
- package/dist/invocation.js.map +0 -1
- package/dist/json-schema.d.ts.map +0 -1
- package/dist/json-schema.js.map +0 -1
- package/dist/mcp-result.d.ts.map +0 -1
- package/dist/mcp-result.js.map +0 -1
- package/dist/meta-tools.d.ts.map +0 -1
- package/dist/meta-tools.js.map +0 -1
- package/dist/node.d.ts.map +0 -1
- package/dist/node.js.map +0 -1
- package/dist/operator-ui/generated.d.ts.map +0 -1
- package/dist/operator-ui/generated.js.map +0 -1
- package/dist/operator-ui/model.d.ts.map +0 -1
- package/dist/operator-ui/model.js.map +0 -1
- package/dist/providers/cloudflare.d.ts.map +0 -1
- package/dist/providers/cloudflare.js.map +0 -1
- package/dist/providers/linear.d.ts.map +0 -1
- package/dist/providers/linear.js.map +0 -1
- package/dist/providers/mixpanel.d.ts.map +0 -1
- package/dist/providers/mixpanel.js.map +0 -1
- package/dist/providers/notion.d.ts.map +0 -1
- package/dist/providers/notion.js.map +0 -1
- package/dist/providers/stripe.d.ts.map +0 -1
- package/dist/providers/stripe.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/retired-toolkits.d.ts.map +0 -1
- package/dist/retired-toolkits.js.map +0 -1
- package/dist/routes/access-tokens.d.ts.map +0 -1
- package/dist/routes/access-tokens.js.map +0 -1
- package/dist/routes/activity.d.ts.map +0 -1
- package/dist/routes/activity.js.map +0 -1
- package/dist/routes/credentials.d.ts.map +0 -1
- package/dist/routes/credentials.js.map +0 -1
- package/dist/routes/mcp.d.ts.map +0 -1
- package/dist/routes/mcp.js.map +0 -1
- package/dist/routes/oauth.d.ts.map +0 -1
- package/dist/routes/oauth.js.map +0 -1
- package/dist/routes/shared.d.ts.map +0 -1
- package/dist/routes/shared.js.map +0 -1
- package/dist/routes/ui.d.ts.map +0 -1
- package/dist/routes/ui.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/storage/file.d.ts.map +0 -1
- package/dist/storage/file.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/timeout.d.ts.map +0 -1
- package/dist/timeout.js.map +0 -1
- package/dist/tool-safety.d.ts.map +0 -1
- package/dist/tool-safety.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/examples/node/README.md +0 -52
- package/examples/node/src/index.ts +0 -71
- package/src/access-tokens.ts +0 -289
- package/src/activity.ts +0 -234
- package/src/apps-shell.ts +0 -370
- package/src/auth/bearer.ts +0 -61
- package/src/auth/clerk.ts +0 -553
- package/src/auth/downstream-oauth.ts +0 -575
- package/src/call-admission.ts +0 -521
- package/src/catalog-fingerprint.ts +0 -52
- package/src/catalog-limits.ts +0 -11
- package/src/catalog-service.ts +0 -1153
- package/src/catalog.ts +0 -790
- package/src/concurrency.ts +0 -39
- package/src/connector-scope.ts +0 -66
- package/src/connectors/api.ts +0 -159
- package/src/connectors/remote-mcp.ts +0 -1090
- package/src/credentials.ts +0 -428
- package/src/errors.ts +0 -364
- package/src/execute.ts +0 -1406
- package/src/executor-admission.ts +0 -328
- package/src/executor-result.ts +0 -107
- package/src/executors/quickjs-child.ts +0 -172
- package/src/executors/quickjs-protocol.ts +0 -99
- package/src/executors/quickjs-runtime.ts +0 -470
- package/src/executors/quickjs.ts +0 -776
- package/src/favicon.ts +0 -53
- package/src/index.ts +0 -666
- package/src/invocation.ts +0 -655
- package/src/json-schema.ts +0 -11
- package/src/mcp-result.ts +0 -43
- package/src/meta-tools.ts +0 -1607
- package/src/node.ts +0 -206
- package/src/operator-ui/browser.css +0 -423
- package/src/operator-ui/browser.ts +0 -1281
- package/src/operator-ui/generated.ts +0 -4
- package/src/operator-ui/model.ts +0 -111
- package/src/providers/cloudflare.ts +0 -3999
- package/src/providers/linear.ts +0 -302
- package/src/providers/mixpanel.ts +0 -229
- package/src/providers/notion.ts +0 -1879
- package/src/providers/stripe.ts +0 -307
- package/src/registry.ts +0 -1085
- package/src/retired-toolkits.ts +0 -14
- package/src/routes/access-tokens.ts +0 -115
- package/src/routes/activity.ts +0 -215
- package/src/routes/credentials.ts +0 -254
- package/src/routes/mcp.ts +0 -429
- package/src/routes/oauth.ts +0 -322
- package/src/routes/shared.ts +0 -247
- package/src/routes/ui.ts +0 -152
- package/src/server.ts +0 -195
- package/src/skills.ts +0 -271
- package/src/storage/file.ts +0 -123
- package/src/storage/memory.ts +0 -44
- package/src/timeout.ts +0 -84
- package/src/tool-safety.ts +0 -15
- package/src/types.ts +0 -523
- package/src/ui.ts +0 -744
- package/src/validate.ts +0 -387
- package/src/version.ts +0 -7
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Web-API only, like the rest of the core: a manifest comparison that ran on
|
|
2
|
+
// Node but not on Workers would leave half the deployments unable to tell a
|
|
3
|
+
// stale allowlist from a current one.
|
|
4
|
+
import { boundedEchoText } from "./errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* Byte budget for the one string a drift report carries. An ISO timestamp is
|
|
7
|
+
* 24 bytes; anything near this bound is a plugin sending something else.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_OBSERVED_AT_BYTES = 64;
|
|
10
|
+
/** A count, or 0 when the seam returned something that is not one. */
|
|
11
|
+
function boundedCount(value) {
|
|
12
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
13
|
+
? Math.max(0, Math.trunc(value))
|
|
14
|
+
: 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rebuild a drift report as four counts and a bounded timestamp.
|
|
18
|
+
*
|
|
19
|
+
* `Connector.catalogDrift()` sits on the open plugin seam, and what it returns
|
|
20
|
+
* lands in the body of unauthenticated `/health` and on connector status.
|
|
21
|
+
* TypeScript constrains neither an extra enumerable property nor the length of
|
|
22
|
+
* `observedAt` at runtime, so "counts and nothing else" is *made* true here —
|
|
23
|
+
* at the boundary where third-party output becomes a response — rather than
|
|
24
|
+
* trusted. The activity path reconstructs its five fields for the same reason.
|
|
25
|
+
*/
|
|
26
|
+
export function boundedCatalogDrift(report) {
|
|
27
|
+
if (!report || typeof report !== "object")
|
|
28
|
+
return undefined;
|
|
29
|
+
return {
|
|
30
|
+
observedAt: boundedEchoText(typeof report.observedAt === "string" ? report.observedAt : "", MAX_OBSERVED_AT_BYTES),
|
|
31
|
+
unclassifiedTools: boundedCount(report.unclassifiedTools),
|
|
32
|
+
unservedTools: boundedCount(report.unservedTools),
|
|
33
|
+
annotationConflicts: boundedCount(report.annotationConflicts),
|
|
34
|
+
schemaChanges: boundedCount(report.schemaChanges),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
/** Deterministic JSON: object keys sorted, so key order is not a schema change. */
|
|
39
|
+
function canonicalize(value) {
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.map(canonicalize);
|
|
42
|
+
if (value === null || typeof value !== "object")
|
|
43
|
+
return value;
|
|
44
|
+
const entries = Object.entries(value)
|
|
45
|
+
.filter(([, item]) => item !== undefined)
|
|
46
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
47
|
+
return Object.fromEntries(entries.map(([key, item]) => [key, canonicalize(item)]));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Digest the schemas of one downstream tool.
|
|
51
|
+
*
|
|
52
|
+
* Canonical rather than literal, unlike the catalog fingerprint in
|
|
53
|
+
* `src/catalog-fingerprint.ts`: that one is deliberately conservative because a
|
|
54
|
+
* spurious cache write is cheap, while a spurious drift finding spends a
|
|
55
|
+
* maintainer's attention on a downstream that reordered its JSON keys.
|
|
56
|
+
* Description and annotations are excluded — a reworded description is P1's
|
|
57
|
+
* business, and an annotation change is already its own drift category.
|
|
58
|
+
*/
|
|
59
|
+
export async function vettedSchemaDigest(tool) {
|
|
60
|
+
const bytes = encoder.encode(JSON.stringify(canonicalize({
|
|
61
|
+
inputSchema: tool.inputSchema ?? null,
|
|
62
|
+
outputSchema: tool.outputSchema ?? null,
|
|
63
|
+
})));
|
|
64
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
65
|
+
return `sha256:${[...digest]
|
|
66
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
67
|
+
.join("")}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build a provider's manifest from the lists it already maintains.
|
|
71
|
+
*
|
|
72
|
+
* Throws when a name is classified twice, because a tool that is both a read
|
|
73
|
+
* and a write is a review mistake that must not boot: the annotation the
|
|
74
|
+
* connector would apply depends on which branch runs first, and "safe by
|
|
75
|
+
* default" cannot be decided by ordering.
|
|
76
|
+
*/
|
|
77
|
+
export function vettedCatalog(input) {
|
|
78
|
+
const tools = new Map();
|
|
79
|
+
const digestFor = (name) => input.schemaDigests?.[name];
|
|
80
|
+
for (const name of input.reads) {
|
|
81
|
+
const digest = digestFor(name);
|
|
82
|
+
tools.set(name, {
|
|
83
|
+
verdict: "read-only",
|
|
84
|
+
...(digest !== undefined ? { schemaDigest: digest } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const [name, verdict] of input.writes) {
|
|
88
|
+
if (tools.has(name)) {
|
|
89
|
+
throw new Error(`vettedCatalog() classified "${name}" as both a read and a write.`);
|
|
90
|
+
}
|
|
91
|
+
const digest = digestFor(name);
|
|
92
|
+
tools.set(name, {
|
|
93
|
+
verdict,
|
|
94
|
+
...(digest !== undefined ? { schemaDigest: digest } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
for (const name of Object.keys(input.schemaDigests ?? {})) {
|
|
98
|
+
if (!tools.has(name)) {
|
|
99
|
+
throw new Error(`vettedCatalog() recorded a schema digest for unclassified tool "${name}".`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { version: 1, tools };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Fill in downstream silence; keep reviewed destructive tools fail-closed.
|
|
106
|
+
*
|
|
107
|
+
* Silence is what a vetted classification is for, and an explicit downstream
|
|
108
|
+
* annotation otherwise wins in both directions. `destructiveHint: true` or
|
|
109
|
+
* `readOnlyHint: false` on a classified read is the downstream telling us this
|
|
110
|
+
* release's allowlist is stale; `readOnlyHint: true` on a name no release has
|
|
111
|
+
* classified says the same thing from the other side. The single place a
|
|
112
|
+
* vetted verdict still overrides the downstream is a name this release
|
|
113
|
+
* reviewed and filed destructive: there connecta knows what the tool does, and
|
|
114
|
+
* a claim to the contrary is a downstream bug rather than news
|
|
115
|
+
* ([#310](https://github.com/zackbart/connecta/issues/310),
|
|
116
|
+
* [#315](https://github.com/zackbart/connecta/issues/315)).
|
|
117
|
+
*/
|
|
118
|
+
function applyVettedSafety(catalog, definition) {
|
|
119
|
+
const downstream = definition.annotations ?? {};
|
|
120
|
+
const record = catalog.tools.get(definition.name);
|
|
121
|
+
if (record?.verdict === "read-only") {
|
|
122
|
+
if (downstream.destructiveHint === true ||
|
|
123
|
+
downstream.readOnlyHint === false) {
|
|
124
|
+
return definition;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...definition,
|
|
128
|
+
annotations: {
|
|
129
|
+
...downstream,
|
|
130
|
+
readOnlyHint: true,
|
|
131
|
+
destructiveHint: downstream.destructiveHint ?? false,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (record?.verdict === "destructive") {
|
|
136
|
+
return {
|
|
137
|
+
...definition,
|
|
138
|
+
annotations: {
|
|
139
|
+
...downstream,
|
|
140
|
+
readOnlyHint: false,
|
|
141
|
+
destructiveHint: true,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Maintained additive creates and tools this release has never seen land
|
|
146
|
+
// here alike. Fill-in only: a silent tool is not read-only, so drift still
|
|
147
|
+
// fails closed onto `call_destructive_tool`, and neither population gets a
|
|
148
|
+
// `destructiveHint` it has not earned. A tool that arrives explicitly
|
|
149
|
+
// read-only keeps that annotation — on a name no release has reviewed, the
|
|
150
|
+
// downstream's own word is the only evidence there is, and rewriting it
|
|
151
|
+
// would be an overrule rather than a fill-in.
|
|
152
|
+
return {
|
|
153
|
+
...definition,
|
|
154
|
+
annotations: {
|
|
155
|
+
...downstream,
|
|
156
|
+
readOnlyHint: downstream.readOnlyHint ?? false,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Whether the downstream's own annotation contradicts what a release reviewed.
|
|
162
|
+
*
|
|
163
|
+
* Only an *explicit* contradiction counts. Silence is the ordinary case the
|
|
164
|
+
* classification exists to fill, and an unclassified tool is already counted
|
|
165
|
+
* as an addition rather than twice.
|
|
166
|
+
*/
|
|
167
|
+
function contradicts(record, definition) {
|
|
168
|
+
const downstream = definition.annotations ?? {};
|
|
169
|
+
if (record.verdict === "read-only") {
|
|
170
|
+
return (downstream.readOnlyHint === false || downstream.destructiveHint === true);
|
|
171
|
+
}
|
|
172
|
+
return downstream.readOnlyHint === true;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Compare a live catalog with the manifest and count what moved.
|
|
176
|
+
*
|
|
177
|
+
* Counts only, and by construction: there is nowhere here to put a tool name,
|
|
178
|
+
* a schema, or a downstream string, so no later surface has to remember to
|
|
179
|
+
* strip one. Reads nothing but the tools it was handed — the caller already
|
|
180
|
+
* fetched them to serve a request, and this function never fetches anything.
|
|
181
|
+
*/
|
|
182
|
+
export async function detectCatalogDrift(catalog, tools) {
|
|
183
|
+
let unclassifiedTools = 0;
|
|
184
|
+
let annotationConflicts = 0;
|
|
185
|
+
let schemaChanges = 0;
|
|
186
|
+
const served = new Set();
|
|
187
|
+
for (const definition of tools) {
|
|
188
|
+
served.add(definition.name);
|
|
189
|
+
const record = catalog.tools.get(definition.name);
|
|
190
|
+
if (!record) {
|
|
191
|
+
unclassifiedTools += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (contradicts(record, definition))
|
|
195
|
+
annotationConflicts += 1;
|
|
196
|
+
// A manifest that recorded no digest for this tool cannot have an opinion
|
|
197
|
+
// about its schema, so it does not pay for a hash either.
|
|
198
|
+
if (record.schemaDigest !== undefined &&
|
|
199
|
+
record.schemaDigest !== (await vettedSchemaDigest(definition))) {
|
|
200
|
+
schemaChanges += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let unservedTools = 0;
|
|
204
|
+
for (const name of catalog.tools.keys()) {
|
|
205
|
+
if (!served.has(name))
|
|
206
|
+
unservedTools += 1;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
unclassifiedTools,
|
|
210
|
+
unservedTools,
|
|
211
|
+
annotationConflicts,
|
|
212
|
+
schemaChanges,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Wrap a hosted-MCP connector in its vetted manifest: the classification the
|
|
217
|
+
* catalog is normalized with, and the drift check that rides the same listing.
|
|
218
|
+
*
|
|
219
|
+
* The check happens where the tools are already in hand and still unmodified —
|
|
220
|
+
* after the downstream answered, before the classification is applied. It adds
|
|
221
|
+
* no request of its own, which is the whole boundary: connecta watches a
|
|
222
|
+
* contract while it is serving a refresh the deployment asked for, and never
|
|
223
|
+
* initiates one to go looking ([#179](https://github.com/zackbart/connecta/issues/179),
|
|
224
|
+
* [#343](https://github.com/zackbart/connecta/issues/343)).
|
|
225
|
+
*/
|
|
226
|
+
export function withVettedCatalog(connector, catalog) {
|
|
227
|
+
let observed;
|
|
228
|
+
return {
|
|
229
|
+
...connector,
|
|
230
|
+
async listTools(ctx) {
|
|
231
|
+
const downstream = await connector.listTools(ctx);
|
|
232
|
+
try {
|
|
233
|
+
observed = {
|
|
234
|
+
observedAt: new Date().toISOString(),
|
|
235
|
+
...(await detectCatalogDrift(catalog, downstream)),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// A drift check is a report about a catalog, never a condition for
|
|
240
|
+
// serving one. Keep the last good observation rather than replacing it
|
|
241
|
+
// with a lie, and let the refresh through.
|
|
242
|
+
ctx.logger.warn(`[connecta] connector "${connector.id}" catalog drift check failed: ` +
|
|
243
|
+
(error instanceof Error ? error.message : String(error)));
|
|
244
|
+
}
|
|
245
|
+
return downstream.map((definition) => applyVettedSafety(catalog, definition));
|
|
246
|
+
},
|
|
247
|
+
catalogDrift() {
|
|
248
|
+
return observed;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -17,4 +17,3 @@ export declare function fingerprintSerializedCatalog(serializedTools: string): P
|
|
|
17
17
|
* any serialized field must change the digest.
|
|
18
18
|
*/
|
|
19
19
|
export declare function snapshotCatalog(tools: ToolDef[]): Promise<CatalogSnapshot>;
|
|
20
|
-
//# sourceMappingURL=catalog-fingerprint.d.ts.map
|
package/dist/catalog-limits.d.ts
CHANGED
|
@@ -7,4 +7,3 @@ export declare const MAX_CATALOG_TOOLS = 100000;
|
|
|
7
7
|
export declare const MAX_SERIALIZED_CATALOG_BYTES: number;
|
|
8
8
|
/** Persisted values stay comfortably below platform per-value limits. */
|
|
9
9
|
export declare const MAX_CATALOG_CHUNK_BYTES: number;
|
|
10
|
-
//# sourceMappingURL=catalog-limits.d.ts.map
|
package/dist/catalog-limits.js
CHANGED
|
@@ -7,4 +7,3 @@ export const MAX_CATALOG_TOOLS = 100_000;
|
|
|
7
7
|
export const MAX_SERIALIZED_CATALOG_BYTES = 32 * 1024 * 1024;
|
|
8
8
|
/** Persisted values stay comfortably below platform per-value limits. */
|
|
9
9
|
export const MAX_CATALOG_CHUNK_BYTES = 1024 * 1024;
|
|
10
|
-
//# sourceMappingURL=catalog-limits.js.map
|
package/dist/catalog-service.js
CHANGED
package/dist/catalog.d.ts
CHANGED
package/dist/catalog.js
CHANGED
package/dist/concurrency.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export declare function resolveDiscoveryConcurrency(value: number | undefined): number;
|
|
2
2
|
/** Run `fn` over `items` with at most `limit` operations in flight. */
|
|
3
3
|
export declare function mapSettledWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<PromiseSettledResult<R>[]>;
|
|
4
|
-
//# sourceMappingURL=concurrency.d.ts.map
|
package/dist/concurrency.js
CHANGED
|
@@ -13,4 +13,3 @@ export type DeferredWork = (promise: Promise<unknown>) => void;
|
|
|
13
13
|
* again after this returns.
|
|
14
14
|
*/
|
|
15
15
|
export declare function closeConnectorScope(connector: Connector, ctx: ConnectorContext, defer?: DeferredWork): Promise<void>;
|
|
16
|
-
//# sourceMappingURL=connector-scope.d.ts.map
|
package/dist/connector-scope.js
CHANGED
package/dist/connectors/api.d.ts
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorCredentialConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, CredentialTestResult, JsonSchema, ToolAnnotations } from "../types.js";
|
|
2
2
|
export interface ApiTool {
|
|
3
3
|
name: string;
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Required, non-empty. Discovery has nothing else to go on: a nameless
|
|
6
|
+
* capability costs the agent a guess, and a guess costs a wrong call.
|
|
7
|
+
*/
|
|
8
|
+
description: string;
|
|
9
|
+
/**
|
|
10
|
+
* A plain JSON Schema object describing the tool input. Optional, but what
|
|
11
|
+
* you supply must be a schema the validator can compile — `api()` refuses to
|
|
12
|
+
* construct otherwise.
|
|
13
|
+
*/
|
|
6
14
|
inputSchema?: JsonSchema;
|
|
7
15
|
/** A plain JSON Schema object describing the tool's structured output. */
|
|
8
16
|
outputSchema?: JsonSchema;
|
|
9
17
|
/**
|
|
10
|
-
* Standard MCP-style behavior hints
|
|
11
|
-
* admits the tool to call_tool and
|
|
18
|
+
* Standard MCP-style behavior hints, with an explicit `readOnlyHint`
|
|
19
|
+
* required: `true` declares a read and admits the tool to call_tool and
|
|
20
|
+
* execute_code, `false` declares work that must cross
|
|
21
|
+
* `call_destructive_tool` where a host can ask a human. Connecta never
|
|
22
|
+
* infers the classification from a name, a description, a schema, or the
|
|
23
|
+
* other annotations.
|
|
12
24
|
*/
|
|
13
|
-
annotations
|
|
25
|
+
annotations: ToolAnnotations & {
|
|
26
|
+
readOnlyHint: boolean;
|
|
27
|
+
};
|
|
14
28
|
handler: (args: any, ctx: ConnectorContext) => Promise<unknown> | unknown;
|
|
15
29
|
}
|
|
16
30
|
export interface ApiOptions {
|
|
@@ -47,16 +61,6 @@ export interface ApiOptions {
|
|
|
47
61
|
* on loose coercion.
|
|
48
62
|
*/
|
|
49
63
|
validateArgs?: boolean;
|
|
50
|
-
/**
|
|
51
|
-
* Fail-closed on a tool whose `inputSchema` the validator cannot evaluate
|
|
52
|
-
* (default false). The default surfaces such a schema as a one-time warning
|
|
53
|
-
* and then passes the raw arguments through, so a broken schema never breaks
|
|
54
|
-
* an otherwise working tool. Set true to instead reject those calls with a
|
|
55
|
-
* non-retryable `invalid_args` ConnectorCallError, so a schema that cannot be
|
|
56
|
-
* enforced never silently admits unvalidated input. Only consulted when
|
|
57
|
-
* `validateArgs` is not false.
|
|
58
|
-
*/
|
|
59
|
-
strictValidation?: boolean;
|
|
60
64
|
tools: ApiTool[];
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
@@ -64,11 +68,15 @@ export interface ApiOptions {
|
|
|
64
68
|
* Tool inputs are plain JSON Schema objects (bring your own zod-to-json-schema
|
|
65
69
|
* conversion if you prefer zod). call_tool JSON-wraps the handler's return.
|
|
66
70
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
71
|
+
* Every tool declares a description and an explicit `annotations.readOnlyHint`,
|
|
72
|
+
* and any `inputSchema` it carries must compile — a tool that fails the
|
|
73
|
+
* contract throws here rather than reaching a catalog. Arguments are then
|
|
74
|
+
* validated against `inputSchema` before the handler runs (disable with
|
|
75
|
+
* `validateArgs: false`, which opts out of enforcement, not out of the schema
|
|
76
|
+
* being real), and a schema that only reveals itself as unenforceable on first
|
|
77
|
+
* use — an unresolvable `$ref`, say — fails the call rather than passing raw
|
|
78
|
+
* arguments through. Remote MCP inputs are also validated, but in the shared
|
|
79
|
+
* invocation path against the request-local downstream catalog, where a
|
|
80
|
+
* downstream's schema is its own affair and stays fail-open.
|
|
72
81
|
*/
|
|
73
82
|
export declare function api(id: string, opts: ApiOptions): Connector;
|
|
74
|
-
//# sourceMappingURL=api.d.ts.map
|
package/dist/connectors/api.js
CHANGED
|
@@ -1,34 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compileValidator, validateToolInput } from "../validate.js";
|
|
2
|
+
/**
|
|
3
|
+
* Enforce the construction contract for one hand-written tool.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is something only the author can supply and no runtime can
|
|
6
|
+
* guess: what the tool does, and whether calling it needs a human's blessing.
|
|
7
|
+
* Guessing either one is how a deployment boots into the wrong shape, so this
|
|
8
|
+
* throws instead. Note what it does *not* do — it never reads a name, verb, or
|
|
9
|
+
* HTTP method to infer a safety class. An unclassified tool is a bug in the
|
|
10
|
+
* deployment, not a puzzle for connecta to solve.
|
|
11
|
+
*/
|
|
12
|
+
function checkToolContract(id, tool) {
|
|
13
|
+
const address = `${id}.${tool.name}`;
|
|
14
|
+
if (typeof tool.description !== "string" || tool.description.trim() === "") {
|
|
15
|
+
throw new Error(`api() tool "${address}" needs a non-empty description — it is what an ` +
|
|
16
|
+
"agent reads to choose the tool (convention: imperative one-liner, " +
|
|
17
|
+
'e.g. "Send an email via Resend").');
|
|
18
|
+
}
|
|
19
|
+
if (typeof tool.annotations?.readOnlyHint !== "boolean") {
|
|
20
|
+
throw new Error(`api() tool "${address}" needs an explicit annotations.readOnlyHint: ` +
|
|
21
|
+
"true for a read, false for work that must cross " +
|
|
22
|
+
"call_destructive_tool. Connecta never infers the classification " +
|
|
23
|
+
"from a tool name, description, schema, or other annotations.");
|
|
24
|
+
}
|
|
25
|
+
if (tool.inputSchema)
|
|
26
|
+
compileValidator(tool.inputSchema, { address });
|
|
27
|
+
}
|
|
2
28
|
/**
|
|
3
29
|
* A connector defined entirely in code: static tool defs + fetch handlers.
|
|
4
30
|
* Tool inputs are plain JSON Schema objects (bring your own zod-to-json-schema
|
|
5
31
|
* conversion if you prefer zod). call_tool JSON-wraps the handler's return.
|
|
6
32
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
33
|
+
* Every tool declares a description and an explicit `annotations.readOnlyHint`,
|
|
34
|
+
* and any `inputSchema` it carries must compile — a tool that fails the
|
|
35
|
+
* contract throws here rather than reaching a catalog. Arguments are then
|
|
36
|
+
* validated against `inputSchema` before the handler runs (disable with
|
|
37
|
+
* `validateArgs: false`, which opts out of enforcement, not out of the schema
|
|
38
|
+
* being real), and a schema that only reveals itself as unenforceable on first
|
|
39
|
+
* use — an unresolvable `$ref`, say — fails the call rather than passing raw
|
|
40
|
+
* arguments through. Remote MCP inputs are also validated, but in the shared
|
|
41
|
+
* invocation path against the request-local downstream catalog, where a
|
|
42
|
+
* downstream's schema is its own affair and stays fail-open.
|
|
12
43
|
*/
|
|
13
44
|
export function api(id, opts) {
|
|
45
|
+
for (const t of opts.tools)
|
|
46
|
+
checkToolContract(id, t);
|
|
14
47
|
const defs = opts.tools.map((t) => ({
|
|
15
48
|
name: t.name,
|
|
16
|
-
|
|
49
|
+
description: t.description,
|
|
17
50
|
...(t.inputSchema !== undefined ? { inputSchema: t.inputSchema } : {}),
|
|
18
51
|
...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
|
|
19
|
-
|
|
52
|
+
annotations: t.annotations,
|
|
20
53
|
}));
|
|
21
54
|
const byName = new Map(opts.tools.map((t) => [t.name, t]));
|
|
22
55
|
const validateArgs = opts.validateArgs ?? true;
|
|
23
|
-
const strictValidation = opts.strictValidation ?? false;
|
|
24
|
-
if (validateArgs) {
|
|
25
|
-
// Compile each schema now so a validator-hostile inputSchema surfaces once
|
|
26
|
-
// here rather than silently on its first call. Warning-only; never throws.
|
|
27
|
-
for (const t of opts.tools) {
|
|
28
|
-
if (t.inputSchema)
|
|
29
|
-
precompileValidator(t.inputSchema, { address: `${id}.${t.name}` });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
56
|
return {
|
|
33
57
|
id,
|
|
34
58
|
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
@@ -64,7 +88,10 @@ export function api(id, opts) {
|
|
|
64
88
|
const invalid = validateToolInput(tool.inputSchema, input, {
|
|
65
89
|
address: `${id}.${name}`,
|
|
66
90
|
logger: ctx.logger,
|
|
67
|
-
|
|
91
|
+
// Always: the schema compiled at construction, so anything that
|
|
92
|
+
// fails here is a schema that cannot be enforced, and a surface we
|
|
93
|
+
// wrote ourselves does not get to admit unvalidated input quietly.
|
|
94
|
+
failClosed: true,
|
|
68
95
|
});
|
|
69
96
|
if (invalid)
|
|
70
97
|
throw invalid;
|
|
@@ -77,4 +104,3 @@ export function api(id, opts) {
|
|
|
77
104
|
},
|
|
78
105
|
};
|
|
79
106
|
}
|
|
80
|
-
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ConnectorContext } from "../types.js";
|
|
2
|
+
/** The methods a hand-written provider surface actually uses. */
|
|
3
|
+
type GuardedMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
export interface GuardedRequest {
|
|
5
|
+
method: GuardedMethod;
|
|
6
|
+
/**
|
|
7
|
+
* Provider-relative path beginning with one `/`, resolved against the base
|
|
8
|
+
* URL's own path. Never an absolute URL, and never carrying `?` or `#` —
|
|
9
|
+
* query parameters go in `query`, where they are encoded rather than parsed.
|
|
10
|
+
*/
|
|
11
|
+
path: string;
|
|
12
|
+
/** Search parameters; `undefined` values are dropped, others stringified. */
|
|
13
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* Per-request headers. `undefined` values are dropped, and a header that
|
|
16
|
+
* collides with an authentication header is refused rather than allowed to
|
|
17
|
+
* shadow it.
|
|
18
|
+
*/
|
|
19
|
+
headers?: Record<string, string | undefined>;
|
|
20
|
+
/** JSON request body. Serialized here, with the `Content-Type` to match. */
|
|
21
|
+
body?: unknown;
|
|
22
|
+
/**
|
|
23
|
+
* A pre-framed body — `FormData`, text, bytes. Mutually exclusive with
|
|
24
|
+
* `body`, and no `Content-Type` is supplied: multipart needs `fetch` to pick
|
|
25
|
+
* the boundary, and anything else knows its own type.
|
|
26
|
+
*/
|
|
27
|
+
rawBody?: BodyInit;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One downstream response, read under the connector's byte ceiling.
|
|
31
|
+
*
|
|
32
|
+
* The accessors are the whole read surface — there is no escape to the
|
|
33
|
+
* underlying `Response`, because an unbounded `.text()` on it is exactly the
|
|
34
|
+
* mistake this type exists to remove. `status`, `ok`, and `headers` are the
|
|
35
|
+
* raw facts a provider mapper needs to make its own classification.
|
|
36
|
+
*/
|
|
37
|
+
interface GuardedResponse {
|
|
38
|
+
readonly status: number;
|
|
39
|
+
readonly ok: boolean;
|
|
40
|
+
readonly headers: Headers;
|
|
41
|
+
/** The body as bytes. */
|
|
42
|
+
bytes(): Promise<Uint8Array>;
|
|
43
|
+
/** The body decoded as UTF-8. */
|
|
44
|
+
text(): Promise<string>;
|
|
45
|
+
/** The body parsed as JSON; `undefined` for an empty body, throws on junk. */
|
|
46
|
+
json(): Promise<unknown>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Turn one response into the provider's own result, or throw the provider's
|
|
50
|
+
* own typed failure. This is where status codes acquire meaning, and it is
|
|
51
|
+
* always the provider's code.
|
|
52
|
+
*/
|
|
53
|
+
type GuardedResponseMapper<T> = (response: GuardedResponse, ctx: ConnectorContext) => T | Promise<T>;
|
|
54
|
+
export interface GuardedFetchOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Provider name as it appears in normalized failure prose — "Cloudflare",
|
|
57
|
+
* "Notion". Used for messages only; it is never parsed.
|
|
58
|
+
*/
|
|
59
|
+
provider: string;
|
|
60
|
+
/**
|
|
61
|
+
* Absolute base URL. Its origin and path prefix are the confinement: every
|
|
62
|
+
* request resolves beneath it or is refused.
|
|
63
|
+
*/
|
|
64
|
+
baseUrl: string;
|
|
65
|
+
/**
|
|
66
|
+
* Headers proving the caller's identity, resolved once per request. Throwing
|
|
67
|
+
* a typed `auth_required` from here is the documented way to report a
|
|
68
|
+
* missing credential — the helper has no opinion about what a credential is.
|
|
69
|
+
*/
|
|
70
|
+
authenticate: (ctx: ConnectorContext) => Record<string, string> | Promise<Record<string, string>>;
|
|
71
|
+
/** Constant headers sent with every request — `Accept`, an API version. */
|
|
72
|
+
headers?: Record<string, string>;
|
|
73
|
+
/**
|
|
74
|
+
* Ceiling on the response body, in bytes. Required, because a default here
|
|
75
|
+
* would be the helper guessing on a provider's behalf: what counts as an
|
|
76
|
+
* absurd response is a fact about the API, not about HTTP.
|
|
77
|
+
*/
|
|
78
|
+
maxResponseBytes: number;
|
|
79
|
+
}
|
|
80
|
+
/** Send one guarded request and map its response with provider knowledge. */
|
|
81
|
+
export type GuardedTransport = <T>(request: GuardedRequest, ctx: ConnectorContext, map: GuardedResponseMapper<T>) => Promise<T>;
|
|
82
|
+
/**
|
|
83
|
+
* Build the guarded transport one hand-written connector sends every request
|
|
84
|
+
* through.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* const send = guardedFetch({
|
|
88
|
+
* provider: "Billing",
|
|
89
|
+
* baseUrl: "https://billing.internal.example/v1",
|
|
90
|
+
* maxResponseBytes: 4 * 1024 * 1024,
|
|
91
|
+
* headers: { Accept: "application/json" },
|
|
92
|
+
* authenticate: async (ctx) => {
|
|
93
|
+
* const token = await ctx.credential?.get();
|
|
94
|
+
* if (!token) throw new ConnectorCallError("auth_required", "…");
|
|
95
|
+
* return { Authorization: `Bearer ${token}` };
|
|
96
|
+
* },
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* const invoice = await send({ method: "GET", path: `/invoices/${id}` }, ctx, (response) => {
|
|
100
|
+
* if (!response.ok) throw billingFailure(response.status);
|
|
101
|
+
* return response.json();
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare function guardedFetch(options: GuardedFetchOptions): GuardedTransport;
|
|
106
|
+
export {};
|