@zackbart/connecta 0.40.0 → 0.42.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 +148 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +32 -5
- package/dist/activity.js +32 -6
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +33 -44
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +15 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +77 -39
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +352 -43
- package/dist/catalog-freshness.js +25 -0
- package/dist/catalog-service.js +408 -78
- package/dist/catalog.js +216 -62
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +53 -21
- package/dist/config.js +56 -17
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +113 -22
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +14 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +4 -2
- package/dist/describe-config.js +1 -0
- package/dist/discovery-health.js +26 -11
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +60 -4
- package/dist/errors.js +88 -3
- package/dist/execute.js +491 -202
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +3 -3
- package/dist/executors/quickjs-child.js +14 -5
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +37 -20
- package/dist/executors/quickjs.js +24 -7
- package/dist/guarded-transport.js +2 -0
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +14 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +11 -3
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +167 -64
- package/dist/membership.d.ts +1 -1
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +286 -73
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +177 -55
- package/dist/node.d.ts +7 -0
- package/dist/node.js +85 -4
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +104 -0
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +9 -1
- package/dist/operator-record.js +16 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +1 -14
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +2 -1
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +8 -3
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +2 -1
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +3 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +12 -6
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +3 -6
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +2 -1
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +2 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +3 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +142 -30
- package/dist/resource-read.js +21 -2
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +53 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +83 -35
- package/dist/routes/shared.d.ts +6 -0
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +38 -2
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +213 -57
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +16 -7
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +36 -0
- package/dist/url-safety.js +38 -20
- package/dist/usage-guide.js +14 -13
- package/dist/utility-recovery.js +33 -0
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +60 -15
- package/documentation/architecture.md +152 -43
- package/documentation/auth.md +160 -53
- package/documentation/code-mode.md +396 -119
- package/documentation/deploying.md +35 -10
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +761 -157
- package/documentation/operating.md +91 -4
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +34 -20
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +10 -8
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/http/hatch.js
CHANGED
|
@@ -25,6 +25,8 @@ import { Integer, isMaxChars, isMinChars } from "./json-schema.js";
|
|
|
25
25
|
import { boundCursor, StandardPage } from "./paging.js";
|
|
26
26
|
import { Classification, Dispatches, IdempotencyKey, RequestSecrets, Signal } from "./services.js";
|
|
27
27
|
import { read, write } from "./tools.js";
|
|
28
|
+
/** A page that reports more data it cannot continue must never read as the complete answer. */
|
|
29
|
+
const CONTINUATION_WITHHELD = "This result is incomplete: more data exists and continuation was withheld.";
|
|
28
30
|
const indexedOperations = new WeakMap();
|
|
29
31
|
function operationsOf(index) {
|
|
30
32
|
let operations = indexedOperations.get(index);
|
|
@@ -204,7 +206,7 @@ export function hatch(options) {
|
|
|
204
206
|
}
|
|
205
207
|
return fieldsInput(fieldsNote(first ?? preferred));
|
|
206
208
|
};
|
|
207
|
-
const fieldsNote = (example) => fieldsDescription("data", example, `Copy declared paths from ${name}_api_details response.fields.
|
|
209
|
+
const fieldsNote = (example) => fieldsDescription("data", example, `Copy declared paths from ${name}_api_details response.fields. A withheld value reads "[redacted]".`);
|
|
208
210
|
const verdictOf = (op) => verdicts.get(`${op.method} ${op.path}`) ?? "refuse";
|
|
209
211
|
const refusalOf = (op) => connection.safety.refusal?.({ method: op.method, template: op.path }) ?? options.binding?.refusal?.(op);
|
|
210
212
|
const discoveryRefusals = new Map();
|
|
@@ -259,7 +261,7 @@ export function hatch(options) {
|
|
|
259
261
|
}
|
|
260
262
|
: {}),
|
|
261
263
|
warnings: Schema.optionalKey(Schema.Array(Schema.String).annotate({
|
|
262
|
-
description: `${options.binding?.warnings ? "Reviewed warnings preserved after fields selection, and " : ""}fields entries that matched nothing in this nonempty response, with the properties declared where matching stopped. At most 10 warnings of 512 characters, then an omitted count.`,
|
|
264
|
+
description: `${options.binding?.warnings ? "Reviewed warnings preserved after fields selection, and " : ""}fields entries that matched nothing in this nonempty response, with the properties declared where matching stopped${pages.size ? ", and a notice when continuation was withheld" : ""}. At most 10 warnings of 512 characters, then an omitted count.`,
|
|
263
265
|
})),
|
|
264
266
|
});
|
|
265
267
|
const hasTotals = [...pages.values()].some((adapters) => adapters.some((adapter) => adapter.total !== undefined));
|
|
@@ -268,11 +270,14 @@ export function hatch(options) {
|
|
|
268
270
|
...callEnvelope.fields,
|
|
269
271
|
page: Schema.optionalKey(Schema.Struct({
|
|
270
272
|
...StandardPage.fields,
|
|
273
|
+
hasMore: Schema.Boolean.annotate({
|
|
274
|
+
description: "True without nextCursor means this data is incomplete and continuation was withheld.",
|
|
275
|
+
}),
|
|
271
276
|
...(hasTotals ? { total: Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(0))) } : {}),
|
|
272
277
|
param: Schema.String.annotate({
|
|
273
278
|
description: options.pageCursor === "argument"
|
|
274
279
|
? "Vendor parameter restored from cursor; pass nextCursor as the top-level cursor."
|
|
275
|
-
: "Put nextCursor in
|
|
280
|
+
: "Put nextCursor unchanged in query[param] or body[param], as page.in says, and keep every other argument.",
|
|
276
281
|
}),
|
|
277
282
|
in: Schema.Literals(["query", "body"]),
|
|
278
283
|
})),
|
|
@@ -1171,6 +1176,8 @@ export function hatch(options) {
|
|
|
1171
1176
|
const send = options.binding
|
|
1172
1177
|
? options.binding.send(call(), sendOptions)
|
|
1173
1178
|
: connection.sendPath(request, sendOptions);
|
|
1179
|
+
// Both send paths share the binding result contract. Keep its optional
|
|
1180
|
+
// metadata typed even when the plain HTTP branch omits those properties.
|
|
1174
1181
|
const answer = yield* send.pipe(Effect.catch((failure) => Effect.gen(function* () {
|
|
1175
1182
|
if (!key || args.idempotencyKey)
|
|
1176
1183
|
return yield* Effect.fail(failure);
|
|
@@ -1221,6 +1228,7 @@ export function hatch(options) {
|
|
|
1221
1228
|
// Roots were checked before dispatch; the pinned index cannot say whether a
|
|
1222
1229
|
// deeper level is closed, so a nested miss is reported, never refused.
|
|
1223
1230
|
const warnings = boundWarnings([
|
|
1231
|
+
...(page?.hasMore && page.nextCursor === undefined ? [CONTINUATION_WITHHELD] : []),
|
|
1224
1232
|
...(options.binding?.warnings && "warnings" in answer ? (answer.warnings ?? []) : []),
|
|
1225
1233
|
...fieldsMisses(answered, args.fields, index
|
|
1226
1234
|
.fields(op)
|
|
@@ -1,9 +1,68 @@
|
|
|
1
1
|
// Node-only: owned undici dispatcher and Node stream adapters, reached via /node.
|
|
2
|
+
import { Resolver } from "node:dns/promises";
|
|
3
|
+
import { learnedDestination } from "../guarded-transport.js";
|
|
4
|
+
import { learnedUrlRefusal } from "../url-safety.js";
|
|
5
|
+
import { OAuthRequestNotSentError } from "../auth/oauth-request-not-sent.js";
|
|
2
6
|
import { Readable } from "node:stream";
|
|
3
7
|
import { isIP } from "node:net";
|
|
4
8
|
import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
|
|
5
9
|
import { Client, ProxyAgent } from "undici";
|
|
6
10
|
import { ConnectorCallError } from "../errors.js";
|
|
11
|
+
/** Each lookup owns its DNS channels; cancellation closes them before returning. */
|
|
12
|
+
async function resolveAddresses(hostname, signal) {
|
|
13
|
+
const resolver = new Resolver();
|
|
14
|
+
const cancel = () => resolver.cancel();
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
17
|
+
try {
|
|
18
|
+
const results = await Promise.allSettled([resolver.resolve4(hostname), resolver.resolve6(hostname)]);
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
for (const result of results) {
|
|
21
|
+
if (result.status === "rejected" && !["ENODATA", "ENOTFOUND"].includes(result.reason?.code))
|
|
22
|
+
throw result.reason;
|
|
23
|
+
}
|
|
24
|
+
return results.flatMap((result, index) => result.status === "fulfilled" ? result.value.map((address) => ({ address, family: index === 0 ? 4 : 6 })) : []);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
signal.removeEventListener("abort", cancel);
|
|
28
|
+
resolver.cancel();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Observe even a misbehaving injected resolver, without holding the request open. */
|
|
32
|
+
async function boundedLookup(lookup, hostname, requestSignal) {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const abort = () => controller.abort(requestSignal?.reason);
|
|
35
|
+
const timer = setTimeout(() => controller.abort(new Error("DNS resolution timed out.")), 10_000);
|
|
36
|
+
requestSignal?.addEventListener("abort", abort, { once: true });
|
|
37
|
+
let onAbort;
|
|
38
|
+
let resolution;
|
|
39
|
+
try {
|
|
40
|
+
if (requestSignal?.aborted)
|
|
41
|
+
abort();
|
|
42
|
+
controller.signal.throwIfAborted();
|
|
43
|
+
resolution = lookup(hostname, controller.signal);
|
|
44
|
+
return await Promise.race([
|
|
45
|
+
resolution,
|
|
46
|
+
new Promise((_, reject) => {
|
|
47
|
+
onAbort = () => reject(controller.signal.reason);
|
|
48
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
49
|
+
if (controller.signal.aborted)
|
|
50
|
+
onAbort();
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
requestSignal?.removeEventListener("abort", abort);
|
|
57
|
+
if (onAbort)
|
|
58
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
59
|
+
controller.abort();
|
|
60
|
+
// Drain the owned resolver after cancelling its channels (INV-7). Test
|
|
61
|
+
// injections may ignore cancellation, but cannot hold send() open.
|
|
62
|
+
if (lookup === resolveAddresses)
|
|
63
|
+
await resolution?.catch(() => { });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
7
66
|
/** Pull-based bridge: Node 22's Readable.toWeb can enqueue after Web cancellation. */
|
|
8
67
|
function responseBody(body, signal) {
|
|
9
68
|
const chunks = body[Symbol.asyncIterator]();
|
|
@@ -43,7 +102,7 @@ function responseBody(body, signal) {
|
|
|
43
102
|
* HTTP/2 REFUSED_STREAM and pipeline recovery paths, for every method.
|
|
44
103
|
* The response/abort owns the client until the body ends or is cancelled.
|
|
45
104
|
*/
|
|
46
|
-
export function createNodeGuardedTransport(proxy) {
|
|
105
|
+
export function createNodeGuardedTransport(proxy, lookup = resolveAddresses) {
|
|
47
106
|
let proxyUri;
|
|
48
107
|
let proxyToken;
|
|
49
108
|
if (proxy !== undefined) {
|
|
@@ -68,7 +127,41 @@ export function createNodeGuardedTransport(proxy) {
|
|
|
68
127
|
return {
|
|
69
128
|
delaysDispatch: true,
|
|
70
129
|
async send(url, init, beforeDispatch = () => { }) {
|
|
130
|
+
const configured = init[learnedDestination];
|
|
131
|
+
let pinned;
|
|
132
|
+
if (configured && url.origin !== configured.origin) {
|
|
133
|
+
const refuse = () => new OAuthRequestNotSentError("connector_call_failed", "The learned URL resolved to an unsafe destination; nothing was sent.", { retryable: false });
|
|
134
|
+
if (learnedUrlRefusal(configured, url))
|
|
135
|
+
throw refuse();
|
|
136
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
137
|
+
let addresses;
|
|
138
|
+
try {
|
|
139
|
+
addresses = isIP(hostname)
|
|
140
|
+
? [{ address: hostname, family: isIP(hostname) }]
|
|
141
|
+
: await boundedLookup(lookup, hostname, init.signal);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
init.signal?.throwIfAborted();
|
|
145
|
+
throw refuse();
|
|
146
|
+
}
|
|
147
|
+
init.signal?.throwIfAborted();
|
|
148
|
+
if (addresses.length === 0 ||
|
|
149
|
+
addresses.some(({ address, family }) => {
|
|
150
|
+
if (!isIP(address) || isIP(address) !== family)
|
|
151
|
+
return true;
|
|
152
|
+
const resolved = new URL(url);
|
|
153
|
+
resolved.hostname = family === 6 ? `[${address}]` : address;
|
|
154
|
+
return learnedUrlRefusal(configured, resolved) !== undefined;
|
|
155
|
+
}))
|
|
156
|
+
throw refuse();
|
|
157
|
+
pinned = addresses[0];
|
|
158
|
+
}
|
|
159
|
+
const destination = new URL(url);
|
|
160
|
+
if (pinned && proxyUri)
|
|
161
|
+
destination.hostname = pinned.family === 6 ? `[${pinned.address}]` : pinned.address;
|
|
71
162
|
const framed = new Request(url, init);
|
|
163
|
+
if (pinned)
|
|
164
|
+
framed.headers.set("host", url.host);
|
|
72
165
|
if (!framed.headers.has("user-agent"))
|
|
73
166
|
framed.headers.set("user-agent", "connecta");
|
|
74
167
|
let dispatched = false;
|
|
@@ -86,6 +179,7 @@ export function createNodeGuardedTransport(proxy) {
|
|
|
86
179
|
uri: proxyUri,
|
|
87
180
|
...(proxyToken ? { token: proxyToken } : {}),
|
|
88
181
|
proxyTunnel: true,
|
|
182
|
+
...(pinned ? { requestTls: { servername: isIP(url.hostname) ? "" : url.hostname } } : {}),
|
|
89
183
|
...clientOptions,
|
|
90
184
|
interceptors: { Agent: [], Client: [] },
|
|
91
185
|
// Neither side of CONNECT uses a Pool, interceptor or global dispatcher.
|
|
@@ -99,7 +193,7 @@ export function createNodeGuardedTransport(proxy) {
|
|
|
99
193
|
// ProxyAgent 6 explicitly sets SNI to the proxy hostname.
|
|
100
194
|
// Node 26 rejects IP SNI. Omit it for literal IPs; tls.connect
|
|
101
195
|
// still checks the certificate against its connection host.
|
|
102
|
-
connect({ ...parameters, ...(name
|
|
196
|
+
connect({ ...parameters, ...(name ? { servername: isIP(name.replace(/^\[|\]$/g, "")) ? "" : name } : {}) }, callback);
|
|
103
197
|
},
|
|
104
198
|
}));
|
|
105
199
|
},
|
|
@@ -135,7 +229,19 @@ export function createNodeGuardedTransport(proxy) {
|
|
|
135
229
|
});
|
|
136
230
|
},
|
|
137
231
|
})
|
|
138
|
-
: new Client(url.origin,
|
|
232
|
+
: new Client(url.origin, {
|
|
233
|
+
...clientOptions,
|
|
234
|
+
...(pinned
|
|
235
|
+
? {
|
|
236
|
+
connect: {
|
|
237
|
+
// No second resolver can replace the checked address. The Client
|
|
238
|
+
// retains the URL hostname for SNI and certificate verification.
|
|
239
|
+
lookup: (_hostname, _options, callback) => callback(null, pinned.address, pinned.family),
|
|
240
|
+
autoSelectFamily: false,
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
244
|
+
});
|
|
139
245
|
const destroy = () => Promise.allSettled([tunnelClient?.destroy(), client.destroy()]);
|
|
140
246
|
const abort = () => {
|
|
141
247
|
void destroy();
|
|
@@ -146,7 +252,7 @@ export function createNodeGuardedTransport(proxy) {
|
|
|
146
252
|
if (!proxyUri)
|
|
147
253
|
dispatch();
|
|
148
254
|
const answer = await client.request({
|
|
149
|
-
origin:
|
|
255
|
+
origin: destination.origin,
|
|
150
256
|
path: `${url.pathname}${url.search}`,
|
|
151
257
|
method: framed.method,
|
|
152
258
|
headers: Object.fromEntries(framed.headers),
|
package/dist/http/oauth.js
CHANGED
|
@@ -30,7 +30,7 @@ import * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
|
30
30
|
import { readRequest, renewedAfterUnauthorized } from "../provider-call.js";
|
|
31
31
|
import { renewableGrantFor } from "../auth/renewable-grant.js";
|
|
32
32
|
import { ConnectorCallError } from "../errors.js";
|
|
33
|
-
import { responseRequest } from "./client.js";
|
|
33
|
+
import { failedResponseRequest, responseRequest } from "./client.js";
|
|
34
34
|
import { Refusal, RequestRefused } from "./errors.js";
|
|
35
35
|
import { Dispatches, Signal, duringCall } from "./services.js";
|
|
36
36
|
/** The calling owner's renewable token, as the operations a transport needs from it. */
|
|
@@ -53,6 +53,9 @@ export function grantFromOAuth(oauth) {
|
|
|
53
53
|
return {
|
|
54
54
|
token: use(grant ? () => grant.token() : missing),
|
|
55
55
|
refresh: (stale) => use(grant ? () => grant.refresh(stale) : missing),
|
|
56
|
+
...(grant?.observe
|
|
57
|
+
? { observe: (token, outcome) => use(() => grant.observe(token, outcome)) }
|
|
58
|
+
: {}),
|
|
56
59
|
rejected: use(grant ? () => grant.rejected() : missing),
|
|
57
60
|
};
|
|
58
61
|
}
|
|
@@ -80,12 +83,21 @@ export function refreshOnce(guarded, options) {
|
|
|
80
83
|
return yield* Effect.fail(refusal);
|
|
81
84
|
});
|
|
82
85
|
const execute = (request) => Effect.gen(function* () {
|
|
83
|
-
const
|
|
86
|
+
const grant = yield* Grant;
|
|
87
|
+
const observeOversized = (failure) => {
|
|
88
|
+
const sent = failedResponseRequest(failure);
|
|
89
|
+
return responseRequest(failure)?.status === 401 && sent && grant.observe
|
|
90
|
+
? grant.observe(sentToken(sent), "refused")
|
|
91
|
+
: Effect.void;
|
|
92
|
+
};
|
|
93
|
+
const response = yield* inner.execute(request).pipe(Effect.tapErrorTag("ResponseTooLarge", observeOversized), Effect.catchTag("ResponseTooLarge", (failure) => {
|
|
84
94
|
const entry = responseRequest(failure);
|
|
85
95
|
return options?.challengeMaxBytes !== undefined && entry?.status === 401
|
|
86
96
|
? challengeFailure(entry)
|
|
87
97
|
: Effect.fail(failure);
|
|
88
98
|
}));
|
|
99
|
+
if (grant.observe && (response.status === 401 || (response.status >= 200 && response.status < 300)))
|
|
100
|
+
yield* grant.observe(sentToken(response.request), response.status === 401 ? "refused" : "accepted");
|
|
89
101
|
if (response.status !== 401)
|
|
90
102
|
return response;
|
|
91
103
|
// Read under the ceiling now, so nothing is left open while renewing.
|
|
@@ -94,7 +106,6 @@ export function refreshOnce(guarded, options) {
|
|
|
94
106
|
.pipe(Effect.catchTag("ResponseTooLarge", (failure) => options?.challengeMaxBytes !== undefined
|
|
95
107
|
? challengeFailure(responseRequest(response))
|
|
96
108
|
: Effect.fail(failure)));
|
|
97
|
-
const grant = yield* Grant;
|
|
98
109
|
const stale = sentToken(response.request);
|
|
99
110
|
const entry = responseRequest(response);
|
|
100
111
|
if (!readRequest(entry ?? {}) && options?.writeRenewal === "discard") {
|
|
@@ -135,11 +146,11 @@ export function refreshOnce(guarded, options) {
|
|
|
135
146
|
return yield* Effect.fail(new RequestRefused({ message: "This call has ended; no renewal work may continue." }));
|
|
136
147
|
renewedAfterUnauthorized(entry, stale, fresh);
|
|
137
148
|
// `authenticate` runs again on the replay and reads the renewed token.
|
|
138
|
-
const replay = yield* inner
|
|
139
|
-
.execute(request)
|
|
140
|
-
.pipe(Effect.catchTag("ResponseTooLarge", (failure) => options?.challengeMaxBytes !== undefined && responseRequest(failure)?.status === 401
|
|
149
|
+
const replay = yield* inner.execute(request).pipe(Effect.tapErrorTag("ResponseTooLarge", observeOversized), Effect.catchTag("ResponseTooLarge", (failure) => options?.challengeMaxBytes !== undefined && responseRequest(failure)?.status === 401
|
|
141
150
|
? grant.rejected
|
|
142
151
|
: Effect.fail(failure)));
|
|
152
|
+
if (grant.observe && (replay.status === 401 || (replay.status >= 200 && replay.status < 300)))
|
|
153
|
+
yield* grant.observe(sentToken(replay.request), replay.status === 401 ? "refused" : "accepted");
|
|
143
154
|
if (replay.status !== 401)
|
|
144
155
|
return replay;
|
|
145
156
|
if (options?.replayRejection === "answer") {
|
package/dist/http/paging.js
CHANGED
|
@@ -20,7 +20,7 @@ export const StandardPage = Schema.Struct({
|
|
|
20
20
|
});
|
|
21
21
|
/** The input field that carries a cursor back. */
|
|
22
22
|
export const Cursor = Schema.String.check(Schema.isMinLength(1)).annotate({
|
|
23
|
-
description: "Opaque nextCursor from the previous page. Pass it back unchanged.",
|
|
23
|
+
description: "Opaque page.nextCursor from the previous page. Pass it back unchanged with the same arguments.",
|
|
24
24
|
});
|
|
25
25
|
/**
|
|
26
26
|
* Lookahead paging, for a vendor that returns no `has_more`: ask for one row
|
package/dist/http/provider.js
CHANGED
|
@@ -46,7 +46,7 @@ const missingGrant = {
|
|
|
46
46
|
};
|
|
47
47
|
/** An ordinary Promise connector factory; Effect remains inside the kit. */
|
|
48
48
|
export function defineApiProvider(definition) {
|
|
49
|
-
const { displayTitle, description, authScope, vendor, baseUrl, maxResponseBytes, timeoutMs, headers, admit, transport, defaultCallAdmission, pageSize: declaredPageSize, idempotencyKey, credential, oauth, oauthWrite401, grant, grantRenewal, authenticate, errors, safety, result, inBandRefusals, probe, guide, transformGuide, hatch: declaredHatch, tools, tagConnector, searchSynonyms, ...metadata } = definition;
|
|
49
|
+
const { displayTitle, description, authScope, vendor, baseUrl, maxResponseBytes, timeoutMs, headers, admit, transport, defaultCallAdmission, pageSize: declaredPageSize, idempotencyKey, credential, oauth, oauthWrite401, grant, grantRenewal, authenticate, publicIdentifierHeaders, errors, safety, result, inBandRefusals, probe, guide, transformGuide, hatch: declaredHatch, tools, tagConnector, searchSynonyms, ...metadata } = definition;
|
|
50
50
|
if (searchSynonyms !== undefined &&
|
|
51
51
|
!(Array.isArray(searchSynonyms) &&
|
|
52
52
|
searchSynonyms.length > 0 &&
|
|
@@ -95,18 +95,13 @@ export function defineApiProvider(definition) {
|
|
|
95
95
|
kind: "api",
|
|
96
96
|
kit: {
|
|
97
97
|
operations: operations(vendor),
|
|
98
|
-
...(pageSize ? { pageSize } : {}),
|
|
99
98
|
...(hatchOptions
|
|
100
99
|
? {
|
|
101
100
|
hatch: {
|
|
102
101
|
name: hatchOptions.name,
|
|
103
102
|
verdicts: hatchOptions.verdicts,
|
|
104
103
|
...(hatchOptions.fieldsRoot ? { fieldsRoot: hatchOptions.fieldsRoot } : {}),
|
|
105
|
-
...(hatchOptions.pageCursor && Object.keys(hatchOptions.pages ?? {}).length
|
|
106
|
-
? { paging: hatchOptions.pageCursor }
|
|
107
|
-
: {}),
|
|
108
104
|
},
|
|
109
|
-
refusesWithheld: true,
|
|
110
105
|
}
|
|
111
106
|
: {}),
|
|
112
107
|
...(meanings ? { errors: meanings } : {}),
|
|
@@ -134,6 +129,7 @@ export function defineApiProvider(definition) {
|
|
|
134
129
|
...(headers === undefined ? {} : { headers }),
|
|
135
130
|
...(admit === undefined ? {} : { admit }),
|
|
136
131
|
authenticate: typeof authenticate === "function" ? authenticate(options) : authenticate,
|
|
132
|
+
...(publicIdentifierHeaders === undefined ? {} : { publicIdentifierHeaders }),
|
|
137
133
|
...configured,
|
|
138
134
|
});
|
|
139
135
|
const connected = connect(vendor, oauth || machineGrant
|
package/dist/http/vendor.js
CHANGED
|
@@ -342,6 +342,7 @@ export function connect(vendor, guarded, options) {
|
|
|
342
342
|
method: declared.method,
|
|
343
343
|
template: declared.path,
|
|
344
344
|
...(sendOptions?.flags ? { flags: sendOptions.flags } : {}),
|
|
345
|
+
...(sendOptions?.context ? { context: sendOptions.context } : {}),
|
|
345
346
|
};
|
|
346
347
|
return Effect.flatMap(admitAddress({
|
|
347
348
|
params: request.params,
|
|
@@ -370,7 +371,11 @@ export function connect(vendor, guarded, options) {
|
|
|
370
371
|
request.form !== undefined ||
|
|
371
372
|
request.emptyForm))
|
|
372
373
|
return Effect.fail(new RequestRefused({ message: "A raw body cannot accompany a bulk item or another body encoding." }));
|
|
373
|
-
return Effect.flatMap(admitAddress({ path, query: request.query }), () => Effect.flatMap(options.safety.resolve(method, path), (operation) => settle({
|
|
374
|
+
return Effect.flatMap(admitAddress({ path, query: request.query }), () => Effect.flatMap(options.safety.resolve(method, path), (operation) => settle({
|
|
375
|
+
...operation,
|
|
376
|
+
...(sendOptions?.flags ? { flags: sendOptions.flags } : {}),
|
|
377
|
+
...(sendOptions?.context ? { context: sendOptions.context } : {}),
|
|
378
|
+
}, Effect.suspend(() => {
|
|
374
379
|
let built = HttpClientRequest.make(method)(path, request.query ? { urlParams: parameterPairs(request.query) } : {});
|
|
375
380
|
if (request.headers)
|
|
376
381
|
built = HttpClientRequest.setHeaders(built, request.headers);
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,6 @@ export type { Registry } from "./registry.js";
|
|
|
41
41
|
export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRequestAuth, RemoteMcpRedirectPolicy, } from "./connectors/remote-mcp.js";
|
|
42
42
|
export type { AgentRequestPrincipal, HumanRequestPrincipal, PrincipalForwarding, PrincipalRequestToken, RequestPrincipal, RequestPrincipalPolicy, RequestToken, RequestTokenInput, } from "./request-token.js";
|
|
43
43
|
export type { ApiHandlerContext, ApiOAuthAccess, ApiOAuthClientAuthentication, ApiOAuthConfig, ApiOptions, ApiTool, } from "./connectors/api.js";
|
|
44
|
-
export type { ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, AgentRouteNames, ConnectorContext, ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, ConnectorUsageGuide, ConnectorSkill, ConnectorSkillResourceContents, DescribedEndpoint, ConnectorStatus, CredentialFailureClass, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
|
|
44
|
+
export type { ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCatalogPeek, ConnectorCredentialValues, AgentRouteNames, ConnectorContext, ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, ConnectorUsageGuide, ConnectorSkill, ConnectorSkillResourceContents, DescribedEndpoint, ConnectorStatus, CredentialFailureClass, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
|
|
45
45
|
export type { ActivityActor, ActivityCatalogChange, ActivityCallSource, ActivityOutcome, ActivityPage, ActivityReadActor, ActivityReadEvent, ActivityReader, ActivityReadGate, ActivityReadPage, ActivitySink, ActivityStore, AgentFriction, ToolCallActivityEvent, } from "./activity.js";
|
|
46
46
|
export { META_TOOL_NAMES } from "./meta-tool-names.js";
|
package/dist/index.js
CHANGED
|
@@ -92,14 +92,16 @@ function warnInsecureConfig(config) {
|
|
|
92
92
|
: "") +
|
|
93
93
|
"Configure Clerk, Cloudflare Access on Workers, or accessTokens(storage) to gate access.");
|
|
94
94
|
}
|
|
95
|
-
// Unset publicUrl
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
95
|
+
// Unset publicUrl: everything built from the deployment's own URL follows the
|
|
96
|
+
// inbound request, whose Host (and X-Forwarded-Proto on Node) the caller
|
|
97
|
+
// chooses. Clerk without any publicUrl has already refused construction.
|
|
98
|
+
if (!config.publicUrl && (oauthConnectors.length > 0 || inboundAuth.length > 0)) {
|
|
99
|
+
logger.warn("[connecta] publicUrl is unset: the URLs this deployment hands out, including " +
|
|
100
|
+
"protected-resource metadata, authorization handoffs and the downstream OAuth " +
|
|
101
|
+
"redirect_uri, are derived per-request from the inbound Host header (and " +
|
|
102
|
+
"X-Forwarded-Proto on Node). A caller who controls those can point them at " +
|
|
103
|
+
"their own host and, with OAuth connectors, capture the authorization code. " +
|
|
104
|
+
"Set `publicUrl` to a fixed https origin.");
|
|
103
105
|
}
|
|
104
106
|
// Branding URLs that failed their scheme gate. Rendering silently falls back
|
|
105
107
|
// (a bad URL must not take the page down), so this warning is the only way an
|
|
@@ -156,19 +158,6 @@ function warnInsecureConfig(config) {
|
|
|
156
158
|
`action and POST /ui/credentials/${connector.id}/test answers 400 ` +
|
|
157
159
|
"until the matching hook is implemented.");
|
|
158
160
|
}
|
|
159
|
-
// A vault written before seal/open existed encrypts credentials but cannot
|
|
160
|
-
// seal downstream OAuth state, so tokens stay plaintext at rest beside them.
|
|
161
|
-
const vault = config.vault;
|
|
162
|
-
if (vault && (typeof vault.seal !== "function" || typeof vault.open !== "function")) {
|
|
163
|
-
for (const connector of config.connectors) {
|
|
164
|
-
if (!connector.startAuth)
|
|
165
|
-
continue;
|
|
166
|
-
logger.warn(`[connecta] connector "${connector.id}" stores its downstream OAuth ` +
|
|
167
|
-
"tokens, client registration, and PKCE verifier as plaintext: the " +
|
|
168
|
-
"configured vault implements no `seal`/`open`. Use " +
|
|
169
|
-
"encryptedCredentialVault(...) or add both methods to seal them.");
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
161
|
// OAuth connectors whose callback cannot perform a state/CSRF check. The
|
|
173
162
|
// public route refuses every callback for these connectors rather than hand
|
|
174
163
|
// an unverified code to finishAuth, so this warning explains why auth cannot
|
|
@@ -222,6 +211,11 @@ export function createConnecta(config) {
|
|
|
222
211
|
const requestAdmission = new AdmissionController(resolved.admission.requests);
|
|
223
212
|
let codeAdmission;
|
|
224
213
|
let executor = resolved.executor;
|
|
214
|
+
if (executor.deadlineMs !== undefined && executor.deadlineMs >= resolved.execute.watchdogMs) {
|
|
215
|
+
logger.warn(`[connecta] The executor's ${executor.deadlineMs}ms deadline does not end before execute.watchdogMs ` +
|
|
216
|
+
`(${resolved.execute.watchdogMs}ms), so programs end at the watchdog. Raise execute.watchdogMs ` +
|
|
217
|
+
"above it if your MCP clients allow requests that long.");
|
|
218
|
+
}
|
|
225
219
|
// Read the identity off the configured executor, before any wrapper hides
|
|
226
220
|
// it behind an anonymous object literal.
|
|
227
221
|
const configuredExecutorName = executorName(executor);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { keepErrorName } from "./redaction-identity.js";
|
|
2
|
+
export class InvocationFailure extends Error {
|
|
3
|
+
details;
|
|
4
|
+
code;
|
|
5
|
+
retryable;
|
|
6
|
+
retryAfterMs;
|
|
7
|
+
connector;
|
|
8
|
+
operation;
|
|
9
|
+
recovery;
|
|
10
|
+
nextAction;
|
|
11
|
+
retry;
|
|
12
|
+
data;
|
|
13
|
+
constructor(details) {
|
|
14
|
+
super(details.message);
|
|
15
|
+
this.details = details;
|
|
16
|
+
this.name = "InvocationFailure";
|
|
17
|
+
this.code = details.code;
|
|
18
|
+
this.retryable = details.retryable;
|
|
19
|
+
this.retryAfterMs = details.retryAfterMs;
|
|
20
|
+
this.connector = details.connector;
|
|
21
|
+
this.operation = details.operation;
|
|
22
|
+
this.recovery = details.recovery;
|
|
23
|
+
this.nextAction = details.nextAction;
|
|
24
|
+
this.retry = details.retry;
|
|
25
|
+
this.data = details;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
keepErrorName(InvocationFailure, "InvocationFailure");
|