@zackbart/connecta 0.28.1 → 0.30.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 +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/argument-redaction.js +230 -0
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +288 -0
- package/dist/authorization-handoff.js +111 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +409 -225
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +633 -0
- package/dist/config.js +508 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +87 -38
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +262 -79
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +194 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -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/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +365 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +336 -56
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +146 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +126 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +140 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { ConnectaBranding
|
|
2
|
-
import type { ActivityStore, ActivityReadGate, recordToolActivity,
|
|
1
|
+
import type { ConnectaBranding } from "./types.js";
|
|
2
|
+
import type { ActivityStore, ActivityReadGate, recordToolActivity, recordCatalogChangeActivity } from "./activity.js";
|
|
3
3
|
import type { RouteContext } from "./routes/shared.js";
|
|
4
|
-
import type { ArtifactRefreshRuntime } from "./artifacts/refresh.js";
|
|
5
4
|
/** Construction-time UI contract. Core never imports the implementation. */
|
|
6
5
|
export interface OperatorSurface {
|
|
7
6
|
readonly branding?: ConnectaBranding;
|
|
@@ -16,29 +15,15 @@ export interface ActivityModule {
|
|
|
16
15
|
readonly readGate?: ActivityReadGate;
|
|
17
16
|
handle(context: RouteContext): Promise<Response | null>;
|
|
18
17
|
readonly recordTool: typeof recordToolActivity;
|
|
19
|
-
readonly
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* The artifacts module, created by `artifacts()` from /artifacts. Core appends
|
|
23
|
-
* its one prebuilt connector to the configured set — the same catalog,
|
|
24
|
-
* invocation, admission, and activity paths as any other — and imports none
|
|
25
|
-
* of its implementation.
|
|
26
|
-
*/
|
|
27
|
-
export interface ArtifactsModule {
|
|
28
|
-
/** The built-in `artifacts` connector. */
|
|
29
|
-
readonly connector: Connector;
|
|
30
|
-
/**
|
|
31
|
-
* Serve the pages' JSON API (`/artifacts/_api/*`) and the sandboxed frame
|
|
32
|
-
* (`/artifacts/_frame`); null for any other path. The operator UI delegates
|
|
33
|
-
* here and serves the page shells itself, so without `ui` there are no
|
|
34
|
-
* artifact routes at all.
|
|
35
|
-
*/
|
|
36
|
-
handle(context: RouteContext): Promise<Response | null>;
|
|
37
|
-
/** Bind the optional refresh runner after core has built its registry and executor. */
|
|
38
|
-
bindRefresh?(runtime: ArtifactRefreshRuntime): void;
|
|
18
|
+
readonly recordChange: typeof recordCatalogChangeActivity;
|
|
39
19
|
}
|
|
40
20
|
/** Optional client-token authentication and operator lifecycle. */
|
|
41
21
|
export interface AccessTokensModule {
|
|
42
22
|
readonly auth: import("./types.js").InboundAuth;
|
|
43
23
|
handle(context: RouteContext): Promise<Response | null>;
|
|
24
|
+
/** Token policy for `describeConfig()`; never token material. */
|
|
25
|
+
describe?(): {
|
|
26
|
+
maxActive: number;
|
|
27
|
+
optionSources?: Readonly<Record<string, "default" | "config">>;
|
|
28
|
+
};
|
|
44
29
|
}
|
package/dist/node.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { Server } from "node:http";
|
|
2
2
|
import type { Connecta } from "./index.js";
|
|
3
|
-
export { fileStorage } from "./storage/file.js";
|
|
4
|
-
export type { FileStorageOptions } from "./storage/file.js";
|
|
5
3
|
export interface ListenOptions {
|
|
6
4
|
port: number;
|
|
7
5
|
/** Interface to bind. Defaults to Node's own (all interfaces). */
|
package/dist/node.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { Readable } from "node:stream";
|
|
3
3
|
import { pipeline } from "node:stream/promises";
|
|
4
|
-
|
|
4
|
+
import { failureRecord, logFailure } from "./operator-record.js";
|
|
5
5
|
/** 10 MiB. Tool arguments are JSON; nothing legitimate approaches this. */
|
|
6
6
|
const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
7
7
|
const BODY_TOO_LARGE = Symbol("body-too-large");
|
|
@@ -113,7 +113,7 @@ export function listen(connecta, portOrOptions) {
|
|
|
113
113
|
if (controller.signal.aborted)
|
|
114
114
|
return;
|
|
115
115
|
// A headless deployment has nothing else to go on; never swallow this.
|
|
116
|
-
console
|
|
116
|
+
logFailure(console, "request failed", failureRecord({}, error), "error");
|
|
117
117
|
if (res.headersSent) {
|
|
118
118
|
// Mid-stream failure: appending an error string here would corrupt
|
|
119
119
|
// the partial body the client has already begun reading. Cutting
|
|
@@ -107,9 +107,7 @@ export function oauthCallbackOutcome(reason, connector, productName = "Connecta"
|
|
|
107
107
|
tone: outcome.tone,
|
|
108
108
|
heading: outcome.heading.replace("{name}", () => name),
|
|
109
109
|
message: outcome.message.replace("{product}", () => productName),
|
|
110
|
-
...(outcome.fix
|
|
111
|
-
? { fixPrompt: renderFixPrompt(outcome.fix, named?.id) }
|
|
112
|
-
: {}),
|
|
110
|
+
...(outcome.fix ? { fixPrompt: renderFixPrompt(outcome.fix, named?.id) } : {}),
|
|
113
111
|
};
|
|
114
112
|
}
|
|
115
113
|
/** A connector's title when it has a usable one, otherwise its id. */
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { oauthConnectKeys } from "./storage/keys.js";
|
|
2
|
+
const HANDOFF_TTL_MS = 15 * 60_000;
|
|
3
|
+
const INTERACTIVE_OAUTH_REQUIRED = "An interactive provider (Clerk or Cloudflare Access) is required to connect OAuth connectors.";
|
|
4
|
+
// Length-prefixed fields avoid JSON keys and escaping without reserving any
|
|
5
|
+
// character in an identity. This is an opaque wire encoding, not encryption.
|
|
6
|
+
function encodeHandoff(h) {
|
|
7
|
+
return [
|
|
8
|
+
h.connector,
|
|
9
|
+
h.owner,
|
|
10
|
+
h.principal,
|
|
11
|
+
h.origin,
|
|
12
|
+
h.expiresAt.toString(36),
|
|
13
|
+
h.nonce,
|
|
14
|
+
h.force ? "1" : "0",
|
|
15
|
+
h.after ?? "",
|
|
16
|
+
]
|
|
17
|
+
.map((field) => `${field.length}:${field}`)
|
|
18
|
+
.join("");
|
|
19
|
+
}
|
|
20
|
+
function decodeHandoff(value) {
|
|
21
|
+
const fields = [];
|
|
22
|
+
let offset = 0;
|
|
23
|
+
while (offset < value.length && fields.length < 8) {
|
|
24
|
+
const colon = value.indexOf(":", offset);
|
|
25
|
+
const lengthText = value.slice(offset, colon);
|
|
26
|
+
if (colon < offset || !/^(0|[1-9][0-9]*)$/.test(lengthText))
|
|
27
|
+
throw new Error("Invalid handoff field");
|
|
28
|
+
const length = Number(lengthText);
|
|
29
|
+
offset = colon + 1;
|
|
30
|
+
if (!Number.isSafeInteger(length) || length > value.length - offset)
|
|
31
|
+
throw new Error("Invalid handoff length");
|
|
32
|
+
fields.push(value.slice(offset, offset + length));
|
|
33
|
+
offset += length;
|
|
34
|
+
}
|
|
35
|
+
const [connector, owner, principal, origin, expiry, nonce, force, after] = fields;
|
|
36
|
+
if (fields.length !== 8 || offset !== value.length || !/^[0-9a-z]+$/.test(expiry) || !/^[01]$/.test(force))
|
|
37
|
+
throw new Error("Invalid handoff fields");
|
|
38
|
+
return {
|
|
39
|
+
connector: connector,
|
|
40
|
+
owner: owner,
|
|
41
|
+
principal: principal,
|
|
42
|
+
origin: origin,
|
|
43
|
+
expiresAt: parseInt(expiry, 36),
|
|
44
|
+
nonce: nonce,
|
|
45
|
+
force: force === "1",
|
|
46
|
+
...(after ? { after } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function encodeSegment(value) {
|
|
50
|
+
let binary = "";
|
|
51
|
+
for (const byte of new TextEncoder().encode(value))
|
|
52
|
+
binary += String.fromCharCode(byte);
|
|
53
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
54
|
+
}
|
|
55
|
+
function decodeSegment(value) {
|
|
56
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value))
|
|
57
|
+
throw new Error("Invalid handoff encoding");
|
|
58
|
+
const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
|
|
59
|
+
const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
|
|
60
|
+
if (encodeSegment(decoded) !== value)
|
|
61
|
+
throw new Error("Noncanonical handoff encoding");
|
|
62
|
+
return decoded;
|
|
63
|
+
}
|
|
64
|
+
export function oauthConnectUnavailable(opts) {
|
|
65
|
+
if (!opts.config.auth.some((provider) => provider.interactiveOperator &&
|
|
66
|
+
(provider.uiAuth?.kind === "clerk" || provider.uiAuth?.kind === "cloudflare-access"))) {
|
|
67
|
+
return INTERACTIVE_OAUTH_REQUIRED;
|
|
68
|
+
}
|
|
69
|
+
if (!opts.config.vault?.signOAuthHandoff || !opts.config.vault.verifyOAuthHandoff) {
|
|
70
|
+
return "Connecting OAuth connectors requires a credential vault with a handoff signing key. Configure encryptedCredentialVault(storage, key).";
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
export async function oauthConnectUrl(opts, baseUrl, connectorId, principal, force = false, opaque = false) {
|
|
75
|
+
return (await oauthConnectLink(opts, baseUrl, connectorId, principal, force, opaque)).url;
|
|
76
|
+
}
|
|
77
|
+
export async function oauthConnectLink(opts, baseUrl, connectorId, principal, force = false, opaque = false, after) {
|
|
78
|
+
const unavailable = oauthConnectUnavailable(opts);
|
|
79
|
+
if (unavailable)
|
|
80
|
+
throw new Error(unavailable);
|
|
81
|
+
const connector = opts.registry.getConnector(connectorId);
|
|
82
|
+
if (!connector || !principal)
|
|
83
|
+
throw new Error("OAuth connection requires an initiating user identity.");
|
|
84
|
+
const handoff = {
|
|
85
|
+
connector: connectorId,
|
|
86
|
+
owner: connector.authScope === "personal" ? principal : "shared",
|
|
87
|
+
principal,
|
|
88
|
+
origin: new URL(baseUrl).origin,
|
|
89
|
+
expiresAt: Date.now() + HANDOFF_TTL_MS,
|
|
90
|
+
nonce: crypto.randomUUID(),
|
|
91
|
+
force,
|
|
92
|
+
...(after ? { after } : {}),
|
|
93
|
+
};
|
|
94
|
+
const vault = opts.config.vault;
|
|
95
|
+
if (opaque && (!vault.seal || !vault.open))
|
|
96
|
+
throw new Error("URL elicitation requires a sealing credential vault.");
|
|
97
|
+
const compact = encodeHandoff(handoff);
|
|
98
|
+
const payload = opaque
|
|
99
|
+
? `v4.${encodeSegment(await vault.seal(connectorId, "connecta:connect-link:v4", compact))}`
|
|
100
|
+
: `v3.${encodeSegment(compact)}`;
|
|
101
|
+
// Preserve the vault's entire signature and its representation, including
|
|
102
|
+
// custom vaults, while keeping every URL segment unpadded base64url.
|
|
103
|
+
const signature = encodeSegment(await vault.signOAuthHandoff(payload));
|
|
104
|
+
const url = new URL(`/connect/${connectorId}`, baseUrl);
|
|
105
|
+
url.searchParams.set("h", `${payload}.${signature}`);
|
|
106
|
+
return { url: url.toString(), nonce: handoff.nonce };
|
|
107
|
+
}
|
|
108
|
+
/** A claim precedes OAuth reset; only a completed start can prove recovery. */
|
|
109
|
+
export async function oauthConnectLinkProgress(opts, baseUrl, id, nonce) {
|
|
110
|
+
const stage = await opts.registry.contextFor(id, baseUrl).storage.get(oauthConnectKeys.used(nonce));
|
|
111
|
+
return stage === "used" ? "claimed" : stage === "started" || stage === "failed" ? stage : undefined;
|
|
112
|
+
}
|
|
113
|
+
/** Make outstanding flow links single-use refusals before a completion check. */
|
|
114
|
+
export async function closeOAuthConnectLinks(opts, baseUrl, id, nonces) {
|
|
115
|
+
const storage = opts.registry.contextFor(id, baseUrl).storage;
|
|
116
|
+
for (const nonce of nonces) {
|
|
117
|
+
await storage.compareAndSet(oauthConnectKeys.used(nonce), null, "closed", { ttlSeconds: HANDOFF_TTL_MS / 1000 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Consuming a round prevents replay from creating sibling retry lineages. */
|
|
121
|
+
export async function claimAuthRetry(opts, baseUrl, id, nonce) {
|
|
122
|
+
return opts.registry
|
|
123
|
+
.contextFor(id, baseUrl)
|
|
124
|
+
.storage.compareAndSet(oauthConnectKeys.retry(nonce), null, "used", { ttlSeconds: HANDOFF_TTL_MS / 1000 });
|
|
125
|
+
}
|
|
126
|
+
export async function verifyOAuthHandoff(opts, baseUrl, connectorId, token) {
|
|
127
|
+
if (!token || token.length > 4096)
|
|
128
|
+
return null;
|
|
129
|
+
try {
|
|
130
|
+
const parts = token.split(".");
|
|
131
|
+
let h;
|
|
132
|
+
if (parts.length === 3 && (parts[0] === "v3" || parts[0] === "v4")) {
|
|
133
|
+
const [version, body, signature] = parts;
|
|
134
|
+
const payload = `${version}.${body}`;
|
|
135
|
+
if (!(await opts.config.vault?.verifyOAuthHandoff?.(payload, decodeSegment(signature))))
|
|
136
|
+
return null;
|
|
137
|
+
const decoded = decodeSegment(body);
|
|
138
|
+
h = decodeHandoff(version === "v4" ? await opts.config.vault.open(connectorId, "connecta:connect-link:v4", decoded) : decoded);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Keep pre-upgrade JSON and sealed v2 links until their original expiry.
|
|
142
|
+
// Authenticate their exact original text; never normalize before verifying.
|
|
143
|
+
const [payload, signature, ...rest] = parts;
|
|
144
|
+
if (!payload || !signature || rest.length)
|
|
145
|
+
return null;
|
|
146
|
+
if (!(await opts.config.vault?.verifyOAuthHandoff?.(payload, signature)))
|
|
147
|
+
return null;
|
|
148
|
+
h = JSON.parse(payload.startsWith("v2:")
|
|
149
|
+
? await opts.config.vault.open(connectorId, "connecta:connect-link:v2", atob(payload.slice(3)))
|
|
150
|
+
: atob(payload));
|
|
151
|
+
}
|
|
152
|
+
const connector = opts.registry.getConnector(connectorId);
|
|
153
|
+
if (!connector ||
|
|
154
|
+
h.connector !== connectorId ||
|
|
155
|
+
typeof h.principal !== "string" ||
|
|
156
|
+
!h.principal ||
|
|
157
|
+
h.owner !== (connector.authScope === "personal" ? h.principal : "shared") ||
|
|
158
|
+
h.origin !== new URL(baseUrl).origin ||
|
|
159
|
+
!Number.isSafeInteger(h.expiresAt) ||
|
|
160
|
+
h.expiresAt <= Date.now() ||
|
|
161
|
+
h.expiresAt > Date.now() + HANDOFF_TTL_MS ||
|
|
162
|
+
typeof h.nonce !== "string" ||
|
|
163
|
+
!h.nonce ||
|
|
164
|
+
typeof h.force !== "boolean")
|
|
165
|
+
return null;
|
|
166
|
+
if (h.after !== undefined &&
|
|
167
|
+
(typeof h.after !== "string" || !/^[a-f0-9-]{36}$/.test(h.after) || h.after === h.nonce || h.force))
|
|
168
|
+
return null;
|
|
169
|
+
if (await opts.registry.contextFor(connectorId, baseUrl).storage.get(oauthConnectKeys.used(h.nonce)))
|
|
170
|
+
return null;
|
|
171
|
+
return h;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Claim only after browser identity and permissions pass, before starting OAuth. */
|
|
178
|
+
export async function consumeOAuthConnectLink(opts, handoff) {
|
|
179
|
+
if (handoff.expiresAt <= Date.now())
|
|
180
|
+
return false;
|
|
181
|
+
const storage = opts.registry.contextFor(handoff.connector, handoff.origin).storage;
|
|
182
|
+
if (handoff.after && (await storage.get(oauthConnectKeys.used(handoff.after))) !== "started")
|
|
183
|
+
return false;
|
|
184
|
+
const expiry = { ttlSeconds: Math.max(1, Math.ceil((handoff.expiresAt - Date.now()) / 1000)) };
|
|
185
|
+
return storage.compareAndSet(oauthConnectKeys.used(handoff.nonce), null, "used", expiry);
|
|
186
|
+
}
|
|
187
|
+
/** Terminal marker only after the start and every tracked reset have settled. */
|
|
188
|
+
export async function finishOAuthConnectStart(opts, handoff, outcome) {
|
|
189
|
+
if (handoff.expiresAt <= Date.now())
|
|
190
|
+
return false;
|
|
191
|
+
const storage = opts.registry.contextFor(handoff.connector, handoff.origin).storage;
|
|
192
|
+
const expiry = { ttlSeconds: Math.max(1, Math.ceil((handoff.expiresAt - Date.now()) / 1000)) };
|
|
193
|
+
return storage.compareAndSet(oauthConnectKeys.used(handoff.nonce), "used", outcome, expiry);
|
|
194
|
+
}
|
package/dist/oauth-sealing.js
CHANGED
|
@@ -27,3 +27,23 @@ export function oauthSealerFor(ctx) {
|
|
|
27
27
|
export function inheritOAuthSealer(from, to) {
|
|
28
28
|
return attachOAuthSealer(to, sealers.get(from));
|
|
29
29
|
}
|
|
30
|
+
/** A separate HMAC key for browser handoffs, derived from the credential key. */
|
|
31
|
+
export async function deriveOAuthHandoffKey(raw) {
|
|
32
|
+
const material = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveKey"]);
|
|
33
|
+
return crypto.subtle.deriveKey({
|
|
34
|
+
name: "HKDF",
|
|
35
|
+
hash: "SHA-256",
|
|
36
|
+
salt: new TextEncoder().encode("connecta:oauth-handoff:v1"),
|
|
37
|
+
info: new TextEncoder().encode("browser-connect"),
|
|
38
|
+
}, material, { name: "HMAC", hash: "SHA-256", length: 256 }, false, ["sign", "verify"]);
|
|
39
|
+
}
|
|
40
|
+
/** A purpose-specific MCP retry key, available only to the host's codec. */
|
|
41
|
+
export async function deriveRequestStateKey(raw) {
|
|
42
|
+
const material = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveBits"]);
|
|
43
|
+
return new Uint8Array(await crypto.subtle.deriveBits({
|
|
44
|
+
name: "HKDF",
|
|
45
|
+
hash: "SHA-256",
|
|
46
|
+
salt: new TextEncoder().encode("connecta:request-state:v1"),
|
|
47
|
+
info: new TextEncoder().encode("auth-elicitation"),
|
|
48
|
+
}, material, 256));
|
|
49
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { type CallErrorDetails } from "./errors.js";
|
|
2
|
+
import type { ConnectorStatus, Logger } from "./types.js";
|
|
3
|
+
/** Where in connecta's work a failure happened. Closed, so it carries no prose. */
|
|
4
|
+
type FailureStep = "MCP handshake" | "tools/list" | "tools/call" | "resources/read" | "OAuth discovery" | "OAuth client registration" | "OAuth token request" | "OAuth flow" | "handler";
|
|
5
|
+
/**
|
|
6
|
+
* Registered OAuth `error` codes: the pinned SDK's `OAuthErrorCode` values
|
|
7
|
+
* (RFC 6749, 6750, 7591, 8707, 9449) and RFC 7591's software-statement pair.
|
|
8
|
+
*/
|
|
9
|
+
export declare const OAUTH_ERROR_CODES: ReadonlySet<string>;
|
|
10
|
+
/** Inbound denial facts, never identity text or provider diagnostics. */
|
|
11
|
+
export type AuthDenialReason = "authentication_failed" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "pool_not_declared" | "pool_grant_denied" | "pool_grant_threw";
|
|
12
|
+
/**
|
|
13
|
+
* Name `ctor`'s instances `label` in records and fixed text. Call once, at
|
|
14
|
+
* module scope, beside the class. An error whose prototype chain reaches no
|
|
15
|
+
* labelled class has no label; its `name` is never consulted.
|
|
16
|
+
*/
|
|
17
|
+
export declare function labelErrorClass(ctor: {
|
|
18
|
+
readonly prototype: object;
|
|
19
|
+
}, label: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* The label of the nearest labelled class on `failure`'s prototype chain.
|
|
22
|
+
* A genuine DOMException is told as `AbortError` or `TimeoutError` when its
|
|
23
|
+
* name is exactly one of those, else `DOMException`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function errorLabel(failure: unknown): string | undefined;
|
|
26
|
+
/** Facts connecta derived about a failure where it saw it. */
|
|
27
|
+
export interface FailureFacts {
|
|
28
|
+
step?: FailureStep;
|
|
29
|
+
/** Scheme, host, and port of the request that failed. */
|
|
30
|
+
origin?: string;
|
|
31
|
+
/** The HTTP status the downstream answered with. */
|
|
32
|
+
httpStatus?: number;
|
|
33
|
+
/** A registered OAuth `error` code the server named. */
|
|
34
|
+
oauthError?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Note what connecta knows about `failure` for any record made of it later. */
|
|
37
|
+
export declare function attachFailureFacts<T>(failure: T, facts: FailureFacts): T;
|
|
38
|
+
/** The facts and class label of `from`, kept for `to` (its classified form, say). */
|
|
39
|
+
export declare function carryFailureFacts<T>(from: unknown, to: T): T;
|
|
40
|
+
/** What a failure record may be about. Only these fields are read. */
|
|
41
|
+
export interface FailureSubject {
|
|
42
|
+
reason?: AuthDenialReason;
|
|
43
|
+
/** A registered connector's id. */
|
|
44
|
+
connector?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The catalog entry the call resolved to, never a name alone: a tool is
|
|
47
|
+
* named only when connecta found it in that connector's catalog.
|
|
48
|
+
*/
|
|
49
|
+
tool?: {
|
|
50
|
+
readonly name: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
source?: string;
|
|
53
|
+
/** A status state, for a status record. */
|
|
54
|
+
state?: ConnectorStatus["state"] | "failed";
|
|
55
|
+
/** Whether an OAuth start continued or restarted authorization. */
|
|
56
|
+
mode?: "continue" | "restart";
|
|
57
|
+
attempts?: number;
|
|
58
|
+
durationMs?: number;
|
|
59
|
+
}
|
|
60
|
+
declare const recordBrand: unique symbol;
|
|
61
|
+
/** One failure as an operator may read it. Only `failureRecord` makes one. */
|
|
62
|
+
export interface FailureRecord {
|
|
63
|
+
readonly [recordBrand]: true;
|
|
64
|
+
readonly reason?: AuthDenialReason;
|
|
65
|
+
readonly connector?: string;
|
|
66
|
+
readonly tool?: string;
|
|
67
|
+
readonly source?: string;
|
|
68
|
+
readonly state?: string;
|
|
69
|
+
readonly mode?: string;
|
|
70
|
+
readonly attempts?: number;
|
|
71
|
+
readonly durationMs?: number;
|
|
72
|
+
readonly step?: string;
|
|
73
|
+
readonly origin?: string;
|
|
74
|
+
readonly httpStatus?: number;
|
|
75
|
+
readonly oauthError?: string;
|
|
76
|
+
/** Classification code, such as `unavailable` or `connector_call_failed`. */
|
|
77
|
+
readonly code?: string;
|
|
78
|
+
readonly retryable?: boolean;
|
|
79
|
+
/** The error's class label, by identity, such as `TypeError`. */
|
|
80
|
+
readonly errorClass?: string;
|
|
81
|
+
/** A network errno or `timeout`. */
|
|
82
|
+
readonly errno?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* How an operator record names a catalog entry: its name when it fits MCP's
|
|
86
|
+
* tool-name grammar, else `<withheld>`. Activity rows and the operator page's
|
|
87
|
+
* catalog use it too, so a withheld name appears nowhere an operator reads.
|
|
88
|
+
*/
|
|
89
|
+
export declare function recordedToolName(entry: {
|
|
90
|
+
readonly name: string;
|
|
91
|
+
}): string;
|
|
92
|
+
/** Vouch for `details` as a classification connecta itself computed. */
|
|
93
|
+
export declare function classifiedFailure<T extends CallErrorDetails>(details: T): T;
|
|
94
|
+
/**
|
|
95
|
+
* The record of a failure: the subject's listed fields, each checked; the
|
|
96
|
+
* facts attached to the failure; its class label; and the typed
|
|
97
|
+
* classification it carries (a `ConnectorCallError`'s code, retryability,
|
|
98
|
+
* origin, and errno, or those of a `CallErrorDetails` registered with
|
|
99
|
+
* `classifiedFailure`). Never its
|
|
100
|
+
* text, and nothing else from `subject`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function failureRecord(subject: FailureSubject, failure?: unknown): FailureRecord;
|
|
103
|
+
/**
|
|
104
|
+
* The events an operator log reports failures as. Closed: a new one is a
|
|
105
|
+
* reviewed line here, never a string assembled at the call site.
|
|
106
|
+
*/
|
|
107
|
+
export type FailureEvent = "negotiation cache read failed" | "negotiation cache write failed" | "connectorAccess grant is unreachable" | "call failed" | "input schema unusable; arguments are not validated" | "catalog read failed" | "catalog persistence failed" | "catalog invalidation failed" | "catalog refresh observation failed" | "catalog refresh failed; serving stale catalog" | "deferred catalog refresh failed" | "deferred catalog refresh could not attach to the runtime" | "catalog drift check failed" | "schema digest check failed; serving digested reviews as writes" | "credential shape read failed" | "credential test failed" | "credential test threw" | "session termination refused or failed; the downstream session may remain until its provider timeout" | "activity record failed" | "catalog drift record failed" | "activity read failed" | "OAuth start failed" | "OAuth disconnect failed" | "OAuth callback verifyState threw; no authorization code was exchanged" | "OAuth callback handoff could not be consumed; no authorization code was exchanged" | "MCP pool request denied" | "OAuth error callback consent could not be consumed; no authorization code was exchanged" | "MCP handler error" | "operator status" | "result paging unavailable" | "request failed" | "Clerk request denied" | "Clerk authentication failed";
|
|
108
|
+
/**
|
|
109
|
+
* Log one failure as `[connecta] <event>` and its record. A record
|
|
110
|
+
* `failureRecord` did not build is replaced, whatever its shape, by a fixed
|
|
111
|
+
* `{ record: "<rejected>" }`. Never throws: it runs on failure paths, and a
|
|
112
|
+
* logger that throws is contained here.
|
|
113
|
+
*/
|
|
114
|
+
export declare function logFailure(logger: Pick<Logger, "info" | "warn" | "error">, event: FailureEvent, record: FailureRecord, level?: "info" | "warn" | "error"): void;
|
|
115
|
+
/**
|
|
116
|
+
* A failure in connecta's words, from its record alone:
|
|
117
|
+
* `Connector "<id>" <step> with <origin> failed with HTTP <status> (<class>, <code>, <errno>).`
|
|
118
|
+
* Used for status messages, and for agent-facing text where the error's own
|
|
119
|
+
* may not pass.
|
|
120
|
+
*/
|
|
121
|
+
export declare function describeFailure(connectorId: string, failure: unknown): string;
|
|
122
|
+
/** A status whose message connecta wrote, such as an auth_required notice. */
|
|
123
|
+
export declare function ownStatus<T extends ConnectorStatus>(status: T): T;
|
|
124
|
+
/** An error status described from `failure`'s record, remembering the failure. */
|
|
125
|
+
export declare function failureStatus(connectorId: string, failure: unknown): ConnectorStatus;
|
|
126
|
+
/**
|
|
127
|
+
* `status` rebuilt from what connecta approved. A status connecta wrote
|
|
128
|
+
* (`ownStatus`, `failureStatus`) is rebuilt from the snapshot taken then,
|
|
129
|
+
* whatever has since been assigned to it. Any other status contributes its
|
|
130
|
+
* typed state alone: a plugin `status()` seam returns its author's text,
|
|
131
|
+
* which can quote a downstream, and connecta cannot vouch for it whatever it
|
|
132
|
+
* looks like.
|
|
133
|
+
*/
|
|
134
|
+
export declare function boundedStatus(status: ConnectorStatus): ConnectorStatus;
|
|
135
|
+
/** The failure connecta saw behind a status it described, if any. */
|
|
136
|
+
export declare function statusFailure(status: ConnectorStatus): unknown;
|
|
137
|
+
export {};
|