@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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { selfHostedClientDocument } from "../auth/downstream-client-metadata.js";
|
|
2
|
+
/** Public OAuth client metadata, without auth, storage, discovery, or request-host input. */
|
|
3
|
+
export function routeOAuthClientMetadata(context) {
|
|
4
|
+
if (!context.path.startsWith("/oauth/client-metadata/"))
|
|
5
|
+
return null;
|
|
6
|
+
const id = context.path.slice("/oauth/client-metadata/".length);
|
|
7
|
+
const connector = context.opts.registry.getConnector(id);
|
|
8
|
+
const document = connector &&
|
|
9
|
+
selfHostedClientDocument(connector, context.opts.config.publicUrl, context.opts.config.serverInfo.name);
|
|
10
|
+
if (!document)
|
|
11
|
+
return new Response(null, { status: 404 });
|
|
12
|
+
if (context.request.method !== "GET")
|
|
13
|
+
return new Response(null, { status: 405, headers: { Allow: "GET" } });
|
|
14
|
+
return Response.json(document, { headers: { "Cache-Control": "public, max-age=300" } });
|
|
15
|
+
}
|
|
@@ -1,28 +1,17 @@
|
|
|
1
|
+
import { OAuthRevocationError } from "../auth/downstream-revocation.js";
|
|
1
2
|
import { Effect, Result } from "effect";
|
|
2
|
-
import { drainOAuthStartResets } from "../auth/oauth-start-reset.js";
|
|
3
3
|
import { closeScope } from "../runtime/connector-scope.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { mayManageConnector, isSameOrigin,
|
|
8
|
-
/**
|
|
9
|
-
const OAUTH_START_TIMEOUT_MS = 30_000;
|
|
10
|
-
/**
|
|
11
|
-
* How a POST starts authorization, from its `mode` query parameter:
|
|
12
|
-
* `restart` (the default) resets the connector to a fresh epoch — wiping the
|
|
13
|
-
* grant, the client registration, and discovery — before starting over;
|
|
14
|
-
* `continue` hands back a recent pending authorization URL when one exists
|
|
15
|
-
* and otherwise starts a flow in the current epoch, keeping the stored
|
|
16
|
-
* registration. `null` is a malformed request.
|
|
17
|
-
*/
|
|
4
|
+
import { oauthConnectUrl, oauthConnectUnavailable } from "../oauth-handoff.js";
|
|
5
|
+
import { authorizedPerson, refuse, serveOperator, visibleRegistry, Answer } from "./operator.js";
|
|
6
|
+
import { failureRecord, logFailure } from "../operator-record.js";
|
|
7
|
+
import { mayManageConnector, isSameOrigin, privateJson } from "./shared.js";
|
|
8
|
+
/** A UI action signs its requested mode; the verified /connect visit applies it. */
|
|
18
9
|
function startMode(url) {
|
|
19
10
|
const modes = url.searchParams.getAll("mode");
|
|
20
11
|
if (modes.length === 0)
|
|
21
12
|
return "restart";
|
|
22
13
|
const [mode] = modes;
|
|
23
|
-
return modes.length === 1 && (mode === "continue" || mode === "restart")
|
|
24
|
-
? mode
|
|
25
|
-
: null;
|
|
14
|
+
return modes.length === 1 && (mode === "continue" || mode === "restart") ? mode : null;
|
|
26
15
|
}
|
|
27
16
|
function oauthManagementRequest(context, connectorId) {
|
|
28
17
|
const { request, baseUrl, defer, opts } = context;
|
|
@@ -52,101 +41,50 @@ function oauthManagementRequest(context, connectorId) {
|
|
|
52
41
|
if (mode === null) {
|
|
53
42
|
return yield* refuse('mode must be "continue" or "restart"', 400);
|
|
54
43
|
}
|
|
44
|
+
if (!disconnecting) {
|
|
45
|
+
const unavailable = oauthConnectUnavailable(opts);
|
|
46
|
+
if (unavailable)
|
|
47
|
+
return yield* refuse(unavailable, 403);
|
|
48
|
+
const authorizationUrl = yield* Effect.tryPromise({
|
|
49
|
+
try: () => oauthConnectUrl(opts, baseUrl, connectorId, authz.principalKey, mode === "restart"),
|
|
50
|
+
catch: () => new Answer(privateJson({ error: "OAuth connection link could not be created" }, { status: 502 })),
|
|
51
|
+
});
|
|
52
|
+
// This POST is already an explicit connect action; the browser visit
|
|
53
|
+
// verifies the signed initiator and applies it without another UI hand-off.
|
|
54
|
+
const target = new URL(authorizationUrl);
|
|
55
|
+
if (opts.config.ui)
|
|
56
|
+
target.searchParams.set("start", "1");
|
|
57
|
+
return privateJson({ state: "auth_required", authorizationUrl: target.href });
|
|
58
|
+
}
|
|
55
59
|
let ctx;
|
|
56
60
|
// The connector scope this request opened ends with it, however it ends.
|
|
57
|
-
yield* Effect.addFinalizer(() => ctx ? closeScope(connector, ctx, defer) : Effect.void);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await disconnectAuth(ctx);
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
const started = await startAuth(ctx, { force: mode === "restart" });
|
|
67
|
-
if (signal.aborted)
|
|
68
|
-
throw signal.reason;
|
|
69
|
-
if (started.authorizationUrl) {
|
|
70
|
-
await registry.bindOAuthHandoff(connectorId, started.authorizationUrl);
|
|
71
|
-
}
|
|
72
|
-
return started;
|
|
73
|
-
},
|
|
61
|
+
yield* Effect.addFinalizer(() => (ctx ? closeScope(connector, ctx, defer) : Effect.void));
|
|
62
|
+
// A disconnect commits through invalidation even if its hook rejects or
|
|
63
|
+
// the browser leaves. It has no start deadline or request signal.
|
|
64
|
+
ctx = registry.contextFor(connectorId, baseUrl);
|
|
65
|
+
const operation = yield* Effect.result(Effect.tryPromise({
|
|
66
|
+
try: () => disconnectAuth(ctx),
|
|
74
67
|
catch: (error) => error,
|
|
75
|
-
}), {
|
|
76
|
-
// A disconnect deliberately ignores both browser cancellation and
|
|
77
|
-
// this start deadline so its reset reaches invalidation.
|
|
78
|
-
...(disconnecting ? {} : { timeoutMs: OAUTH_START_TIMEOUT_MS }),
|
|
79
|
-
...(disconnecting ? {} : { signal: request.signal }),
|
|
80
|
-
timeoutError,
|
|
81
68
|
}));
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const started = Result.isSuccess(operation) ? operation.success : undefined;
|
|
96
|
-
const reused = mode === "continue" && started?.authorizationReused === true;
|
|
97
|
-
const unchanged = mode === "continue" && (reused || started?.state === "ok");
|
|
98
|
-
const invalidated = unchanged
|
|
99
|
-
? Result.succeed(undefined)
|
|
100
|
-
: yield* Effect.result(Effect.tryPromise({
|
|
101
|
-
try: () => registry.invalidateStored(connectorId),
|
|
102
|
-
catch: (error) => error,
|
|
103
|
-
}));
|
|
104
|
-
// Every failure below answers in the route's own fixed words. A hook's
|
|
105
|
-
// rejection and a status message can quote a token endpoint's error body
|
|
106
|
-
// or a provider's refusal, either of which can quote the secret it was
|
|
107
|
-
// sent, so that text goes to the deployment's log and nowhere else.
|
|
108
|
-
const failed = (detail, fixed, status) => {
|
|
109
|
-
opts.logger.warn(`[connecta] connector "${connectorId}" OAuth ${disconnecting ? "disconnect" : mode} failed: ${detail}`);
|
|
110
|
-
return refuse(fixed, status);
|
|
111
|
-
};
|
|
112
|
-
const couldNot = disconnecting
|
|
113
|
-
? "OAuth disconnect failed"
|
|
114
|
-
: "OAuth authorization could not start";
|
|
115
|
-
// A Result, not a caught value, decides whether it failed: a hook that
|
|
116
|
-
// rejects with no reason at all still failed.
|
|
117
|
-
if (Result.isFailure(operation)) {
|
|
118
|
-
if (operation.failure === timeoutError) {
|
|
119
|
-
return yield* failed(`start exceeded ${OAUTH_START_TIMEOUT_MS} ms`, "OAuth authorization start timed out", 504);
|
|
69
|
+
const invalidated = yield* Effect.result(Effect.tryPromise({
|
|
70
|
+
try: () => registry.invalidateStored(connectorId),
|
|
71
|
+
catch: (error) => error,
|
|
72
|
+
}));
|
|
73
|
+
if (Result.isFailure(operation) || Result.isFailure(invalidated)) {
|
|
74
|
+
const error = Result.isFailure(operation)
|
|
75
|
+
? operation.failure
|
|
76
|
+
: Result.isFailure(invalidated)
|
|
77
|
+
? invalidated.failure
|
|
78
|
+
: undefined;
|
|
79
|
+
logFailure(opts.config.logger, "OAuth disconnect failed", failureRecord({ connector: connectorId }, error));
|
|
80
|
+
if (error instanceof OAuthRevocationError && !Result.isFailure(invalidated)) {
|
|
81
|
+
return privateJson({ state: "auth_required", code: "oauth_revocation_failed" });
|
|
120
82
|
}
|
|
121
|
-
return yield*
|
|
122
|
-
}
|
|
123
|
-
if (Result.isFailure(invalidated)) {
|
|
124
|
-
return yield* failed(`stored-state invalidation: ${msg(invalidated.failure)}`, couldNot, 400);
|
|
125
|
-
}
|
|
126
|
-
const result = operation.success;
|
|
127
|
-
if (!result) {
|
|
128
|
-
return new Response(null, {
|
|
129
|
-
status: 204,
|
|
130
|
-
headers: {
|
|
131
|
-
"Cache-Control": "no-store",
|
|
132
|
-
"Referrer-Policy": "no-referrer",
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
const authorizationUrl = isSafeHttpUrl(result.authorizationUrl)
|
|
137
|
-
? result.authorizationUrl
|
|
138
|
-
: undefined;
|
|
139
|
-
if (result.state === "error") {
|
|
140
|
-
return yield* failed(result.message || "the connector reported an error state", couldNot, 502);
|
|
141
|
-
}
|
|
142
|
-
if (result.state === "auth_required" && !authorizationUrl) {
|
|
143
|
-
return yield* failed(result.message || "consent is required but no safe authorization URL was returned", "OAuth authorization requires consent but no safe URL is available", 502);
|
|
83
|
+
return yield* refuse("OAuth disconnect failed", 400);
|
|
144
84
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
state: result.state,
|
|
149
|
-
...(authorizationUrl ? { authorizationUrl, reused } : {}),
|
|
85
|
+
return new Response(null, {
|
|
86
|
+
status: 204,
|
|
87
|
+
headers: { "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" },
|
|
150
88
|
});
|
|
151
89
|
}).pipe(Effect.scoped);
|
|
152
90
|
}
|
package/dist/routes/oauth.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
+
import { htmlSecurityHeaders } from "../html-security.js";
|
|
3
|
+
import { authorizationCredential } from "../inbound-credential.js";
|
|
2
4
|
import { closeConnectorScope } from "../connector-scope.js";
|
|
3
|
-
import {
|
|
5
|
+
import { OAuthCallbackClaimedError, oauthStateDigest } from "../auth/downstream-oauth.js";
|
|
6
|
+
import { failureRecord, logFailure } from "../operator-record.js";
|
|
7
|
+
import { oauthFlowKeys } from "../storage/keys.js";
|
|
4
8
|
import { escapeHtml, renderPage, resolveBranding, STATUS_ICONS } from "../branding.js";
|
|
5
|
-
import { oauthCallbackOutcome, providerErrorReason
|
|
6
|
-
import { authorizeUiIdentity, mayManageConnector, validateAuthPermissions, loggableValue,
|
|
9
|
+
import { oauthCallbackOutcome, providerErrorReason } from "../oauth-callback-outcome.js";
|
|
10
|
+
import { authorizeUiIdentity, withSessionCookies, mayManageConnector, validateAuthPermissions, loggableValue, } from "./shared.js";
|
|
7
11
|
const TONE_MARKS = {
|
|
8
12
|
ok: { className: " ok", icon: STATUS_ICONS.ok, label: "Connected" },
|
|
9
13
|
declined: { className: "", icon: STATUS_ICONS.declined, label: "Not connected" },
|
|
@@ -24,10 +28,10 @@ const TONE_MARKS = {
|
|
|
24
28
|
* under a quiet disclosure rather than in the message.
|
|
25
29
|
*/
|
|
26
30
|
function html(reason, opts, connector) {
|
|
27
|
-
const brand = resolveBranding(opts.branding);
|
|
31
|
+
const brand = resolveBranding(opts.config.ui?.branding);
|
|
28
32
|
const outcome = oauthCallbackOutcome(reason, connector, brand.productName);
|
|
29
33
|
const mark = TONE_MARKS[outcome.tone];
|
|
30
|
-
const uiMounted = Boolean(opts.ui);
|
|
34
|
+
const uiMounted = Boolean(opts.config.ui);
|
|
31
35
|
const home = uiMounted
|
|
32
36
|
? `<div class="status-actions"><a class="btn${outcome.tone === "ok" ? "" : " primary"}" href="/">Return to ${escapeHtml(brand.productName)}</a></div>`
|
|
33
37
|
: "";
|
|
@@ -50,23 +54,23 @@ function html(reason, opts, connector) {
|
|
|
50
54
|
${details}
|
|
51
55
|
</section>
|
|
52
56
|
</main>`;
|
|
53
|
-
return new Response(renderPage(opts.branding, {
|
|
57
|
+
return new Response(renderPage(opts.config.ui?.branding, {
|
|
54
58
|
title: `${outcome.heading} — ${brand.pageTitle}`,
|
|
55
59
|
uiMounted,
|
|
56
60
|
body,
|
|
57
|
-
}), { status: outcome.status, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
61
|
+
}), { status: outcome.status, headers: htmlSecurityHeaders({ "Content-Type": "text/html; charset=utf-8" }) });
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
60
64
|
* Pay the storage read a real downstream-OAuth refusal pays, on the refusal
|
|
61
65
|
* paths that would otherwise pay nothing.
|
|
62
66
|
*
|
|
63
67
|
* Identical bodies do not hide a connector id if the clock still sorts them.
|
|
64
|
-
* `KvOAuthProvider.verifyState` reads
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* `KvOAuthProvider.verifyState` reads the consent the state names before it
|
|
69
|
+
* can reject it, so a configured id costs a storage round trip on the
|
|
70
|
+
* ordinary path while an id naming nothing used to touch no I/O. That gap is
|
|
71
|
+
* an oracle: sample the two and a wordlist recovers the connector list the
|
|
72
|
+
* flat 400 was meant to withhold. So zero-I/O refusals read the same key in
|
|
73
|
+
* the same `conn:<id>:` namespace, where an unconfigured id gets a miss.
|
|
70
74
|
*
|
|
71
75
|
* This is deliberately *not* a constant-time claim: a hit and a miss are not
|
|
72
76
|
* identical in a KV store, and a connector shipping its own `verifyState` may
|
|
@@ -78,10 +82,9 @@ function html(reason, opts, connector) {
|
|
|
78
82
|
* turning it into a 500 would hand back exactly the distinguishable response
|
|
79
83
|
* this whole path exists to deny.
|
|
80
84
|
*/
|
|
81
|
-
async function equalizeRefusalCost(context) {
|
|
85
|
+
async function equalizeRefusalCost(context, state) {
|
|
82
86
|
try {
|
|
83
|
-
|
|
84
|
-
await context.storage.get(oauthValueStorageKey("oauth:state", generation));
|
|
87
|
+
await context.storage.get(oauthFlowKeys.flow(await oauthStateDigest(state ?? "")));
|
|
85
88
|
}
|
|
86
89
|
catch {
|
|
87
90
|
// Deliberately ignored — see above.
|
|
@@ -100,6 +103,12 @@ async function equalizeRefusalCost(context) {
|
|
|
100
103
|
export function routeOAuthCallback(context) {
|
|
101
104
|
if (!context.path.startsWith("/oauth/callback/"))
|
|
102
105
|
return Effect.succeed(null);
|
|
106
|
+
// Downstream OAuth uses query-mode redirects; form_post is not supported.
|
|
107
|
+
if (context.request.method !== "GET")
|
|
108
|
+
return Effect.succeed(new Response(null, {
|
|
109
|
+
status: 405,
|
|
110
|
+
headers: { Allow: "GET" },
|
|
111
|
+
}));
|
|
103
112
|
return Effect.uninterruptible(Effect.promise(() => finishOAuthCallback(context)));
|
|
104
113
|
}
|
|
105
114
|
/**
|
|
@@ -122,18 +131,22 @@ const TOKEN_ERROR_CODES = new Set([
|
|
|
122
131
|
/** ` with OAuth error <code>` for a known code; nothing for anything else. */
|
|
123
132
|
function exchangeErrorCode(err) {
|
|
124
133
|
const code = err?.code;
|
|
125
|
-
return typeof code === "string" && TOKEN_ERROR_CODES.has(code)
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
return typeof code === "string" && TOKEN_ERROR_CODES.has(code) ? ` with OAuth error ${code}` : "";
|
|
135
|
+
}
|
|
136
|
+
/** The provider's own refusal of a duplicate callback, wherever it is wrapped. */
|
|
137
|
+
function claimedByAnotherCallback(err) {
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
for (let current = err; current instanceof Error && !seen.has(current); current = current.cause) {
|
|
140
|
+
if (current instanceof OAuthCallbackClaimedError)
|
|
141
|
+
return true;
|
|
142
|
+
seen.add(current);
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
128
145
|
}
|
|
129
146
|
async function finishOAuthCallback(context) {
|
|
130
147
|
const { path, url, baseUrl, opts } = context;
|
|
131
148
|
const error = url.searchParams.get("error");
|
|
132
|
-
if (error)
|
|
133
|
-
return html(providerErrorReason(error), opts);
|
|
134
149
|
const code = url.searchParams.get("code");
|
|
135
|
-
if (!code)
|
|
136
|
-
return html("invalid_callback", opts);
|
|
137
150
|
const id = path.slice("/oauth/callback/".length);
|
|
138
151
|
const state = url.searchParams.get("state");
|
|
139
152
|
const callbackTarget = await opts.registry.oauthCallbackView(id, state);
|
|
@@ -145,38 +158,40 @@ async function finishOAuthCallback(context) {
|
|
|
145
158
|
// nor touches storage for an unknown id, which is what lets the refusals
|
|
146
159
|
// below borrow it to equalize their cost.
|
|
147
160
|
const connectorContext = callbackRegistry
|
|
148
|
-
? callbackRegistry.contextFor(id, baseUrl)
|
|
149
|
-
: opts.registry.contextFor(id, baseUrl);
|
|
161
|
+
? callbackRegistry.contextFor(id, baseUrl, {}, context.defer ? { defer: context.defer } : {})
|
|
162
|
+
: opts.registry.contextFor(id, baseUrl, {}, context.defer ? { defer: context.defer } : {});
|
|
150
163
|
const refused = () => html("invalid_callback", opts);
|
|
151
164
|
if (!connector || !connector.finishAuth) {
|
|
152
|
-
await equalizeRefusalCost(connectorContext);
|
|
165
|
+
await equalizeRefusalCost(connectorContext, state);
|
|
153
166
|
return refused();
|
|
154
167
|
}
|
|
155
168
|
try {
|
|
156
169
|
const expectedPrincipalKey = callbackTarget?.principalKey;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
const browserIdentity = await authorizeUiIdentity(context.request, baseUrl, opts.config.auth, "OAuth callback", context.runtimeContext, opts.config.identity);
|
|
171
|
+
if (!browserIdentity.ok) {
|
|
172
|
+
if (browserIdentity.response.status === 401 && authorizationCredential(context.request).kind !== "absent")
|
|
173
|
+
return browserIdentity.response;
|
|
174
|
+
// A Clerk browser handshake refreshes its session and returns to this
|
|
175
|
+
// exact callback. It grants no identity and exchanges no code yet.
|
|
176
|
+
if (browserIdentity.response.status === 307 && browserIdentity.response.headers.has("location"))
|
|
177
|
+
return browserIdentity.response;
|
|
178
|
+
return refused();
|
|
179
|
+
}
|
|
180
|
+
if (!expectedPrincipalKey || browserIdentity.principalKey !== expectedPrincipalKey)
|
|
181
|
+
return refused();
|
|
182
|
+
try {
|
|
183
|
+
validateAuthPermissions(browserIdentity, opts.registry);
|
|
171
184
|
}
|
|
172
|
-
|
|
185
|
+
catch {
|
|
173
186
|
return refused();
|
|
174
187
|
}
|
|
188
|
+
if (!mayManageConnector(browserIdentity, connector))
|
|
189
|
+
return refused();
|
|
175
190
|
// CSRF / login-fixation guard: this route is intentionally public, so verify
|
|
176
191
|
// the `state` matches the flow connecta started BEFORE exchanging the code.
|
|
177
192
|
if (!connector.verifyState) {
|
|
178
|
-
await equalizeRefusalCost(connectorContext);
|
|
179
|
-
opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
|
|
193
|
+
await equalizeRefusalCost(connectorContext, state);
|
|
194
|
+
opts.config.logger.warn(`[connecta] refused an OAuth callback for connector ` +
|
|
180
195
|
`${loggableValue(id)} with 400: it implements finishAuth but no ` +
|
|
181
196
|
"verifyState, so connecta cannot establish that it started this flow. " +
|
|
182
197
|
"No authorization code was exchanged. Implement verifyState before " +
|
|
@@ -188,15 +203,11 @@ async function finishOAuthCallback(context) {
|
|
|
188
203
|
stateMatches = await connector.verifyState(state, connectorContext);
|
|
189
204
|
}
|
|
190
205
|
catch (err) {
|
|
191
|
-
opts.logger
|
|
192
|
-
`${loggableValue(id)} with 400: verifyState threw ` +
|
|
193
|
-
`${loggableValue(msg(err))}. No authorization code was exchanged. ` +
|
|
194
|
-
"Re-run authorization from connecta and check the verifier if it " +
|
|
195
|
-
"fails again.");
|
|
206
|
+
logFailure(opts.config.logger, "OAuth callback verifyState threw; no authorization code was exchanged", failureRecord({ connector: id }, err));
|
|
196
207
|
return refused();
|
|
197
208
|
}
|
|
198
209
|
if (!stateMatches) {
|
|
199
|
-
opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
|
|
210
|
+
opts.config.logger.warn(`[connecta] refused an OAuth callback for connector ` +
|
|
200
211
|
`${loggableValue(id)} with 400: ` +
|
|
201
212
|
(state === null
|
|
202
213
|
? "the state parameter was missing"
|
|
@@ -205,30 +216,72 @@ async function finishOAuthCallback(context) {
|
|
|
205
216
|
"connecta and try again.");
|
|
206
217
|
return refused();
|
|
207
218
|
}
|
|
208
|
-
|
|
219
|
+
// Error responses are callbacks too: neither their code nor their copy is
|
|
220
|
+
// interpreted until the state and the consent's issuer have been validated.
|
|
221
|
+
if (["state", "iss", "code", "error"].some((name) => url.searchParams.getAll(name).length > 1))
|
|
222
|
+
return refused();
|
|
223
|
+
try {
|
|
224
|
+
const issuer = url.searchParams.get("iss");
|
|
225
|
+
if (connector.verifyCallbackIssuer) {
|
|
226
|
+
if (!(await connector.verifyCallbackIssuer(issuer, connectorContext)))
|
|
227
|
+
return refused();
|
|
228
|
+
}
|
|
229
|
+
else if (issuer !== null) {
|
|
230
|
+
// A custom connector with no issuer validator cannot vouch for this response.
|
|
231
|
+
return refused();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return refused();
|
|
236
|
+
}
|
|
237
|
+
if (error === null && !code)
|
|
238
|
+
return refused();
|
|
239
|
+
if (error !== null && !connector.consumeAuthError)
|
|
240
|
+
return refused();
|
|
241
|
+
{
|
|
209
242
|
try {
|
|
210
|
-
await opts.registry.
|
|
243
|
+
if (!(await opts.registry.consumeOAuthHandoff(id, state, expectedPrincipalKey)))
|
|
244
|
+
return refused();
|
|
211
245
|
}
|
|
212
246
|
catch (err) {
|
|
213
|
-
opts.logger
|
|
214
|
-
`${loggableValue(id)} with 500: its principal handoff could not be ` +
|
|
215
|
-
`consumed (${loggableValue(msg(err))}). No authorization code was exchanged.`);
|
|
247
|
+
logFailure(opts.config.logger, "OAuth callback handoff could not be consumed; no authorization code was exchanged", failureRecord({ connector: id }, err));
|
|
216
248
|
return html("handoff_failed", opts, connector);
|
|
217
249
|
}
|
|
218
250
|
}
|
|
251
|
+
if (error !== null) {
|
|
252
|
+
try {
|
|
253
|
+
await connector.consumeAuthError(connectorContext);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
if (claimedByAnotherCallback(err))
|
|
257
|
+
return refused();
|
|
258
|
+
logFailure(opts.config.logger, "OAuth error callback consent could not be consumed; no authorization code was exchanged", failureRecord({ connector: id }, err));
|
|
259
|
+
return withSessionCookies(html("handoff_failed", opts, connector), browserIdentity.sessionCookies);
|
|
260
|
+
}
|
|
261
|
+
return withSessionCookies(html(providerErrorReason(error), opts), browserIdentity.sessionCookies);
|
|
262
|
+
}
|
|
219
263
|
try {
|
|
220
264
|
await connector.finishAuth(code, connectorContext, url.searchParams);
|
|
221
265
|
await callbackRegistry.invalidateStored(id);
|
|
222
|
-
return html("connected", opts, connector);
|
|
266
|
+
return withSessionCookies(html("connected", opts, connector), browserIdentity.sessionCookies);
|
|
223
267
|
}
|
|
224
268
|
catch (err) {
|
|
269
|
+
// A duplicate of a callback that already claimed this consent sent
|
|
270
|
+
// nothing: it is the already-used link the flat refusal describes, not
|
|
271
|
+
// an exchange the provider rejected.
|
|
272
|
+
if (claimedByAnotherCallback(err)) {
|
|
273
|
+
opts.config.logger.warn(`[connecta] refused an OAuth callback for connector ` +
|
|
274
|
+
`${loggableValue(id)} with 400: another callback had already ` +
|
|
275
|
+
"claimed its state. No authorization code was exchanged.");
|
|
276
|
+
return refused();
|
|
277
|
+
}
|
|
225
278
|
// Neither the page nor the log repeats what the exchange threw: the SDK
|
|
226
279
|
// quotes the token endpoint's error_description or raw body, and a
|
|
227
280
|
// provider echoing a client_secret_post request puts the secret there.
|
|
228
|
-
opts.logger.warn(`[connecta] OAuth callback for connector ${loggableValue(id)} failed ` +
|
|
281
|
+
opts.config.logger.warn(`[connecta] OAuth callback for connector ${loggableValue(id)} failed ` +
|
|
229
282
|
`with 500: the authorization code exchange failed${exchangeErrorCode(err)}. ` +
|
|
230
283
|
"Check the connector's client configuration and re-run authorization.");
|
|
231
|
-
return html("exchange_failed", opts, connector);
|
|
284
|
+
return withSessionCookies(html("exchange_failed", opts, connector), browserIdentity.sessionCookies);
|
|
232
285
|
}
|
|
233
286
|
}
|
|
234
287
|
finally {
|
package/dist/routes/operator.js
CHANGED
|
@@ -36,15 +36,15 @@ export function refuse(error, status) {
|
|
|
36
36
|
return Effect.fail(new Answer(privateJson({ error }, { status })));
|
|
37
37
|
}
|
|
38
38
|
function admitted(result) {
|
|
39
|
-
return Effect.flatMap(Effect.promise(() => result), (authz) => authz.ok ? Effect.succeed(authz) : Effect.fail(new Answer(authz.response)));
|
|
39
|
+
return Effect.flatMap(Effect.promise(() => result), (authz) => (authz.ok ? Effect.succeed(authz) : Effect.fail(new Answer(authz.response))));
|
|
40
40
|
}
|
|
41
41
|
/** Any configured caller. The gate's refusal, challenge headers and all, is the answer. */
|
|
42
42
|
export function authorized({ request, baseUrl, opts, runtimeContext }, partitionIdentity = true) {
|
|
43
|
-
return Effect.suspend(() => admitted(authorize(request, baseUrl, opts.auth, runtimeContext, opts.identity, partitionIdentity)));
|
|
43
|
+
return Effect.suspend(() => admitted(authorize(request, baseUrl, opts.config.auth, runtimeContext, opts.config.identity, partitionIdentity)));
|
|
44
44
|
}
|
|
45
45
|
/** A signed-in person, for a route that changes how a connector authenticates. */
|
|
46
46
|
export function authorizedPerson({ request, baseUrl, opts, runtimeContext }, purpose) {
|
|
47
|
-
return Effect.suspend(() => admitted(authorizeUiIdentity(request, baseUrl, opts.auth, purpose, runtimeContext, opts.identity)));
|
|
47
|
+
return Effect.suspend(() => admitted(authorizeUiIdentity(request, baseUrl, opts.config.auth, purpose, runtimeContext, opts.config.identity)));
|
|
48
48
|
}
|
|
49
49
|
/** The partition and tool grants an identity carries into a registry view. */
|
|
50
50
|
export function scopeFor(authz, connectorIds = authz.connectorIds) {
|
package/dist/routes/shared.d.ts
CHANGED
|
@@ -1,66 +1,30 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { AccessTokensModule, ActivityModule, ArtifactsModule, OperatorSurface } from "../module-contracts.js";
|
|
3
|
-
import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity.js";
|
|
4
|
-
import type { CredentialVault } from "../credential-contract.js";
|
|
1
|
+
import type { ActivityActor } from "../activity.js";
|
|
5
2
|
import type { DeferredWork } from "../connector-scope.js";
|
|
6
3
|
import type { AdmissionController } from "../executor-admission.js";
|
|
7
4
|
import type { Registry, ToolAccess } from "../registry.js";
|
|
8
5
|
import type { ResolvedPool } from "../connector-access.js";
|
|
9
|
-
import type { AuthenticatedIdentity,
|
|
10
|
-
import type {
|
|
11
|
-
import type { ConnectorPermission, ConnectaIdentityConfig } from "../index.js";
|
|
6
|
+
import type { AuthenticatedIdentity, Executor, InboundAuth, InboundAuthRuntimeContext } from "../types.js";
|
|
7
|
+
import type { ConnectorPermission, ConnectaIdentityConfig, ResolvedConfig } from "../config.js";
|
|
12
8
|
export { msg } from "../errors.js";
|
|
13
9
|
export interface ServerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Everything createConnecta resolved from configuration: auth, identity,
|
|
12
|
+
* URLs, modules, logger, and every limit with its default applied. Routes
|
|
13
|
+
* read it here rather than from fields copied out of it one by one.
|
|
14
|
+
*/
|
|
15
|
+
config: ResolvedConfig;
|
|
16
|
+
/** Construction-time allowlisted snapshot, frozen by describeConfig. */
|
|
17
|
+
configDescription: import("../describe-config.js").ConnectaConfigDescription;
|
|
18
|
+
configValueSources?: Readonly<Record<string, "default" | "config">>;
|
|
14
19
|
registry: Registry;
|
|
15
|
-
auth: InboundAuth[];
|
|
16
|
-
identity?: ConnectaIdentityConfig | undefined;
|
|
17
20
|
/** Validated named pools served at `/mcp/<name>`; empty when none declared. */
|
|
18
|
-
pools
|
|
19
|
-
|
|
20
|
-
artifactOrigin?: string | undefined;
|
|
21
|
-
allowedOrigins?: readonly string[] | "*" | undefined;
|
|
22
|
-
serverInfo: Implementation;
|
|
23
|
-
logger: Logger;
|
|
24
|
-
activity?: ActivityStore | undefined;
|
|
25
|
-
activityReadGate?: ActivityReadGate | undefined;
|
|
26
|
-
activityDeploymentId?: string | undefined;
|
|
27
|
-
deploymentInfo?: Record<string, unknown> | undefined;
|
|
28
|
-
/** Deadline for call_tool/call_destructive_tool calls that pass no timeoutMs. Off when unset. */
|
|
29
|
-
defaultToolTimeoutMs?: number | undefined;
|
|
30
|
-
/** Per-connector deadline for the search/describe probe fan-out. Default 30_000. */
|
|
31
|
-
probeTimeoutMs?: number | undefined;
|
|
32
|
-
/** Maximum simultaneous connector discovery operations. Default 4. */
|
|
33
|
-
discoveryConcurrency?: number | undefined;
|
|
34
|
-
/** Aggregate serialized-byte budget for connecta.emit per run. Default 4_000_000. */
|
|
35
|
-
maxEmittedBytes?: number | undefined;
|
|
36
|
-
/** Block-count budget for connecta.emit per run. Default 32. */
|
|
37
|
-
maxEmittedBlocks?: number | undefined;
|
|
38
|
-
/** Host calls one execute_code program may make. Default 20. */
|
|
39
|
-
maxHostCalls?: number | undefined;
|
|
40
|
-
/** Deadline per execute_code host call. Default 15_000. */
|
|
41
|
-
hostCallTimeoutMs?: number | undefined;
|
|
42
|
-
/** Hard ceiling on one execute_code run, outside the sandbox. Default 120_000. */
|
|
43
|
-
watchdogMs?: number | undefined;
|
|
44
|
-
/** Config approval exemptions (`execute.approval`), resolved at construction. */
|
|
45
|
-
approval?: ApprovalPolicy | undefined;
|
|
46
|
-
/** Exempt writes one program may send (`execute.maxWrites`). */
|
|
47
|
-
maxWrites?: number | undefined;
|
|
48
|
-
/** Required sandbox backing the execute_code meta-tool. */
|
|
21
|
+
pools: ReadonlyMap<string, ResolvedPool>;
|
|
22
|
+
/** Required sandbox backing execute_code, wrapped in fallback admission if needed. */
|
|
49
23
|
executor: Executor;
|
|
50
24
|
/** Sanitized identity of the configured sandbox, when it has one. */
|
|
51
25
|
executorName?: string | undefined;
|
|
52
26
|
/** Global FIFO boundary for all non-preflight `/mcp` requests. */
|
|
53
27
|
requestAdmission: AdmissionController;
|
|
54
|
-
/** Encrypted connector-credential storage backing the Credentials page. */
|
|
55
|
-
credentialVault?: CredentialVault | undefined;
|
|
56
|
-
/** Optional browser routes, with no implementation import in core. */
|
|
57
|
-
ui?: OperatorSurface | undefined;
|
|
58
|
-
activityModule?: ActivityModule | undefined;
|
|
59
|
-
/** Optional team pages; their routes mount only beside `ui`. */
|
|
60
|
-
artifactsModule?: ArtifactsModule | undefined;
|
|
61
|
-
accessTokens?: AccessTokensModule | undefined;
|
|
62
|
-
/** Optional browser UI and OAuth result-page labels. */
|
|
63
|
-
branding?: ConnectaBranding | undefined;
|
|
64
28
|
}
|
|
65
29
|
export interface RuntimeExecutionContext extends InboundAuthRuntimeContext {
|
|
66
30
|
waitUntil(promise: Promise<unknown>): void;
|
|
@@ -86,10 +50,16 @@ export declare function privateJson(body: unknown, init?: ResponseInit): Respons
|
|
|
86
50
|
*/
|
|
87
51
|
export declare function loggableValue(requested: string): string;
|
|
88
52
|
export declare function activityActorNamespace(provider: InboundAuth): string | undefined;
|
|
89
|
-
|
|
53
|
+
/** Metadata routing and challenge selection use the same first actual answer. */
|
|
54
|
+
export declare function authMetadata(request: Request, baseUrl: string, auth: readonly InboundAuth[]): Promise<{
|
|
55
|
+
provider: InboundAuth;
|
|
56
|
+
response: Response;
|
|
57
|
+
} | null>;
|
|
58
|
+
export declare function authorize(request: Request, baseUrl: string, auth: readonly InboundAuth[], runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig, partitionIdentity?: boolean, interactiveOnly?: boolean): Promise<{
|
|
90
59
|
ok: true;
|
|
91
60
|
actor: ActivityActor;
|
|
92
61
|
identity: AuthenticatedIdentity;
|
|
62
|
+
sessionCookies?: readonly string[];
|
|
93
63
|
subjectKey?: string;
|
|
94
64
|
principalKey?: string;
|
|
95
65
|
connectorIds: "all" | readonly string[];
|
|
@@ -107,12 +77,8 @@ export declare function authorize(request: Request, baseUrl: string, auth: Inbou
|
|
|
107
77
|
ok: false;
|
|
108
78
|
response: Response;
|
|
109
79
|
}>;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}> | {
|
|
113
|
-
ok: false;
|
|
114
|
-
response: Response;
|
|
115
|
-
}>;
|
|
80
|
+
/** Human routes reject machine credentials before any verification or storage I/O. */
|
|
81
|
+
export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: readonly InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<Awaited<ReturnType<typeof authorize>>>;
|
|
116
82
|
export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
|
|
117
83
|
export declare function withSecurityHeaders(response: Response, requestUrl: URL, _path: string): Response;
|
|
118
84
|
type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
|
|
@@ -120,14 +86,9 @@ type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
|
|
|
120
86
|
}>;
|
|
121
87
|
/** Unknown configured ids refuse the complete view, including management rights. */
|
|
122
88
|
export declare function validateAuthPermissions(authz: AuthorizedIdentity, registry: Registry): void;
|
|
123
|
-
/**
|
|
124
|
-
* Whether this identity may open artifact pages: exactly whoever may read the
|
|
125
|
-
* `artifacts` connector — it is visible to them and `get_artifact` is among
|
|
126
|
-
* its tools they were granted. Viewing grants nothing a program could not
|
|
127
|
-
* already read (ethos, "Human routes manage auth, never capability").
|
|
128
|
-
*/
|
|
129
|
-
export declare function mayViewArtifacts(authz: AuthorizedIdentity, registry: Registry): boolean;
|
|
130
89
|
export declare function mayManageConnector(authz: AuthorizedIdentity, connector: {
|
|
131
90
|
id: string;
|
|
132
91
|
authScope?: "shared" | "personal";
|
|
133
92
|
}): boolean;
|
|
93
|
+
/** Preserve refreshed browser cookies on the redirect or completion page. */
|
|
94
|
+
export declare function withSessionCookies(response: Response, cookies?: readonly string[]): Response;
|