@zackbart/connecta 0.28.1 → 0.29.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 +788 -5
- package/README.md +62 -42
- package/bin/connecta.mjs +110 -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/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 +283 -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 +368 -224
- 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 +631 -0
- package/dist/config.js +503 -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 +84 -35
- 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 +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- 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 +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -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 +230 -73
- 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 +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- 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 +117 -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-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- 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/{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/{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 +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -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 +356 -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 +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -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 +320 -54
- 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 +137 -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 +434 -0
- package/documentation/architecture.md +664 -231
- package/documentation/auth.md +936 -546
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -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 +120 -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 +133 -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,19 @@
|
|
|
1
|
+
import { createClerkClient as createIsolatedClerkClient } from "./clerk-sdk/client.js";
|
|
2
|
+
import { byteReadResponse } from "../byte-read-response.js";
|
|
3
|
+
import { failureRecord, logFailure } from "../operator-record.js";
|
|
4
|
+
/** All SDK network paths, including fresh opaque-verification clients. */
|
|
5
|
+
export function createByteReadingClerkClient(options) {
|
|
6
|
+
const client = createIsolatedClerkClient({
|
|
7
|
+
...options,
|
|
8
|
+
telemetry: { disabled: true },
|
|
9
|
+
fetch: async (input, init) => byteReadResponse(await fetch(input, init)),
|
|
10
|
+
});
|
|
11
|
+
// The upstream disabled flag does not cover recordLog or debug output.
|
|
12
|
+
client.telemetry.record = () => { };
|
|
13
|
+
client.telemetry.recordLog = () => { };
|
|
14
|
+
return client;
|
|
15
|
+
}
|
|
16
|
+
/** The generated SDK never forwards its diagnostic text to a console. */
|
|
17
|
+
export function clerkSdkFailure(failure) {
|
|
18
|
+
logFailure(console, "Clerk authentication failed", failureRecord({}, failure));
|
|
19
|
+
}
|
package/dist/auth/clerk.d.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ClerkGateClient } from "./clerk-sdk/client.js";
|
|
2
2
|
import type { InboundAuth } from "../types.js";
|
|
3
|
-
type
|
|
3
|
+
export type { ClerkGateClient, ClerkUser } from "./clerk-sdk/client.js";
|
|
4
4
|
export interface ClerkAuthOptions {
|
|
5
5
|
publishableKey: string;
|
|
6
6
|
secretKey: string;
|
|
7
7
|
/** Public base URL of this deployment. Defaults to the request origin. */
|
|
8
8
|
publicUrl?: string;
|
|
9
|
+
/**
|
|
10
|
+
* OAuth client IDs dedicated to this deployment, used only when a verified
|
|
11
|
+
* access token has no audience/resource claim. Omitted or `[]` requires
|
|
12
|
+
* resource-bound tokens, both JWT and opaque. Enable Clerk's
|
|
13
|
+
* `aud_claim_enabled` setting and request the endpoint URL as `resource`.
|
|
14
|
+
* A present audience/resource must match
|
|
15
|
+
* the canonical `/mcp` or `/mcp/<pool>` URL even for an allowlisted client.
|
|
16
|
+
* Never share these clients with another resource server. Pool grants still
|
|
17
|
+
* narrow access; the fallback client list applies to every pool.
|
|
18
|
+
*/
|
|
19
|
+
allowedOAuthClientIds?: readonly string[];
|
|
9
20
|
/**
|
|
10
21
|
* Email domains this deployment admits, e.g. `["acme.com"]`. An
|
|
11
22
|
* authenticated user whose verified primary email is not on one of them is
|
|
@@ -14,12 +25,12 @@ export interface ClerkAuthOptions {
|
|
|
14
25
|
* `mail.acme.com` — spell a subdomain out to allow it. Entries must be ASCII
|
|
15
26
|
* (punycode for an internationalized domain) and are validated at
|
|
16
27
|
* construction. Absent ⇒ every authenticated user passes this check, as
|
|
17
|
-
* before the option existed. Governs Clerk sign-in only: a co-configured
|
|
18
|
-
*
|
|
28
|
+
* before the option existed. Governs Clerk sign-in only: a co-configured machine
|
|
29
|
+
* access token has no email to read and is admitted without a domain check.
|
|
19
30
|
*/
|
|
20
31
|
allowedDomains?: readonly string[];
|
|
21
|
-
/** Optional allow-list hook
|
|
22
|
-
gate?: (userId: string, clerk:
|
|
32
|
+
/** Optional allow-list hook using Connecta's bundled user lookup client. */
|
|
33
|
+
gate?: (userId: string, clerk: ClerkGateClient) => boolean | Promise<boolean>;
|
|
23
34
|
/** Advertised scopes in protected-resource metadata. */
|
|
24
35
|
scopes?: string[];
|
|
25
36
|
/** Optional hosted Account Portal sign-in URL for operator pages. Absolute https only. */
|
|
@@ -28,4 +39,3 @@ export interface ClerkAuthOptions {
|
|
|
28
39
|
signUpUrl?: string;
|
|
29
40
|
}
|
|
30
41
|
export declare function clerkAuth(opts: ClerkAuthOptions): InboundAuth;
|
|
31
|
-
export {};
|
package/dist/auth/clerk.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
1
|
+
import { authorizationCredential, authorizationRequest, isMachineCredential } from "../inbound-credential.js";
|
|
2
|
+
import { createByteReadingClerkClient } from "./clerk-transport.js";
|
|
3
|
+
import { decodeJwt } from "@clerk/backend/jwt";
|
|
4
|
+
import { byteReadResponse } from "../byte-read-response.js";
|
|
5
|
+
import { failureRecord, logFailure } from "../operator-record.js";
|
|
5
6
|
import { assertNoRetiredToolkitOptions } from "../retired-toolkits.js";
|
|
7
|
+
function logDenial(reason, failure) {
|
|
8
|
+
logFailure(console, "Clerk request denied", failureRecord({ reason }, failure));
|
|
9
|
+
}
|
|
6
10
|
const CORS_HEADERS = {
|
|
7
11
|
"Access-Control-Allow-Origin": "*",
|
|
8
|
-
"Access-Control-Allow-Methods": "GET,
|
|
12
|
+
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
|
9
13
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, mcp-protocol-version",
|
|
10
14
|
};
|
|
11
|
-
/** Coarse bearer shape for diagnostics — never the token itself. */
|
|
12
|
-
function tokenShape(request) {
|
|
13
|
-
const header = request.headers.get("authorization");
|
|
14
|
-
if (!header)
|
|
15
|
-
return "none";
|
|
16
|
-
const token = header.replace(/^Bearer\s+/i, "");
|
|
17
|
-
if (token.startsWith("oat_"))
|
|
18
|
-
return "oauth-opaque";
|
|
19
|
-
if (token.startsWith("eyJ"))
|
|
20
|
-
return "jwt";
|
|
21
|
-
return "other";
|
|
22
|
-
}
|
|
23
15
|
/**
|
|
24
16
|
* Clerk Frontend API origin, derived from pk_(test|live)_<b64 domain>.
|
|
25
17
|
*
|
|
@@ -55,8 +47,7 @@ function fapiUrl(publishableKey) {
|
|
|
55
47
|
// Clerk terminates the encoded domain with `$`; everything else is the host.
|
|
56
48
|
const domain = decoded.replace(/\$$/, "");
|
|
57
49
|
if (!isDomain(domain)) {
|
|
58
|
-
throw new Error("clerkAuth: `publishableKey` does not decode to a Frontend API domain." +
|
|
59
|
-
shape);
|
|
50
|
+
throw new Error("clerkAuth: `publishableKey` does not decode to a Frontend API domain." + shape);
|
|
60
51
|
}
|
|
61
52
|
return `https://${domain}`;
|
|
62
53
|
}
|
|
@@ -101,13 +92,9 @@ function cleanActivityLabel(value) {
|
|
|
101
92
|
}
|
|
102
93
|
/** Prefer a person's name, then a verified primary email, then username. */
|
|
103
94
|
function activityLabelForUser(user) {
|
|
104
|
-
const fullName = user.fullName ??
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
address.verification?.status === "verified");
|
|
108
|
-
return (cleanActivityLabel(fullName) ??
|
|
109
|
-
cleanActivityLabel(primary?.emailAddress) ??
|
|
110
|
-
cleanActivityLabel(user.username));
|
|
95
|
+
const fullName = user.fullName ?? [user.firstName, user.lastName].filter(Boolean).join(" ");
|
|
96
|
+
const primary = user.emailAddresses?.find((address) => address.id === user.primaryEmailAddressId && address.verification?.status === "verified");
|
|
97
|
+
return cleanActivityLabel(fullName) ?? cleanActivityLabel(primary?.emailAddress) ?? cleanActivityLabel(user.username);
|
|
111
98
|
}
|
|
112
99
|
/**
|
|
113
100
|
* One label of a domain: ASCII letters/digits, interior hyphens only, 63
|
|
@@ -160,9 +147,7 @@ function normalizeAllowedDomains(value) {
|
|
|
160
147
|
// operator can see in the throw. Nothing is forgiven on the email side.
|
|
161
148
|
const domain = entry.trim();
|
|
162
149
|
if (!isDomain(domain)) {
|
|
163
|
-
const hint = domain.includes("@")
|
|
164
|
-
? " Write the domain alone, with no `@` and no local part."
|
|
165
|
-
: "";
|
|
150
|
+
const hint = domain.includes("@") ? " Write the domain alone, with no `@` and no local part." : "";
|
|
166
151
|
throw new Error(`clerkAuth: \`allowedDomains\` entry ${JSON.stringify(entry)} is not a ` +
|
|
167
152
|
`domain (expected something like "acme.com").${hint}`);
|
|
168
153
|
}
|
|
@@ -170,18 +155,72 @@ function normalizeAllowedDomains(value) {
|
|
|
170
155
|
}
|
|
171
156
|
return domains;
|
|
172
157
|
}
|
|
158
|
+
/** No wildcard or implicit all-clients mode for unbound OAuth tokens. */
|
|
159
|
+
function normalizeOAuthClientIds(value) {
|
|
160
|
+
if (value === undefined)
|
|
161
|
+
return new Set();
|
|
162
|
+
if (!Array.isArray(value)) {
|
|
163
|
+
throw new Error("clerkAuth: `allowedOAuthClientIds` must be an array of OAuth client IDs. " +
|
|
164
|
+
"Omit it or use [] to require resource-bound OAuth tokens.");
|
|
165
|
+
}
|
|
166
|
+
for (const id of value) {
|
|
167
|
+
if (typeof id !== "string" || !/^[\x21-\x7e]+$/.test(id) || id === "*") {
|
|
168
|
+
throw new Error("clerkAuth: `allowedOAuthClientIds` entries must be nonempty, " + "non-wildcard client IDs without whitespace.");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return new Set(value);
|
|
172
|
+
}
|
|
173
|
+
/** Only verified claims reach this comparison; resource URLs match exactly. */
|
|
174
|
+
function oauthBindingRejection(claims, resource, clientId, allowedClientIds) {
|
|
175
|
+
const bindings = ["aud", "resource"].filter((key) => Object.prototype.hasOwnProperty.call(claims, key));
|
|
176
|
+
if (bindings.length === 0) {
|
|
177
|
+
return allowedClientIds.has(clientId) ? null : "oauth_client_not_allowed";
|
|
178
|
+
}
|
|
179
|
+
const matches = bindings.every((key) => {
|
|
180
|
+
const value = claims[key];
|
|
181
|
+
const audiences = Array.isArray(value) ? value : [value];
|
|
182
|
+
return (audiences.length > 0 &&
|
|
183
|
+
audiences.every((audience) => typeof audience === "string" && audience.length > 0) &&
|
|
184
|
+
audiences.includes(resource));
|
|
185
|
+
});
|
|
186
|
+
return matches ? null : "oauth_binding_mismatch";
|
|
187
|
+
}
|
|
173
188
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* bound is what a 253-byte domain needs, and the escaping — JSON.stringify plus
|
|
178
|
-
* the hand-rolled U+2028/U+2029 pass it leaves raw — is defense in depth behind
|
|
179
|
-
* `isDomain`, which has already ruled out the newline that would forge a line.
|
|
189
|
+
* Clerk 3.12's OAuth deserializer drops `aud`. Read the same verification
|
|
190
|
+
* endpoint directly after SDK authentication, without a shared response cache
|
|
191
|
+
* or SDK patch. Its subject/client and active verdict must still agree.
|
|
180
192
|
*/
|
|
181
|
-
function
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
193
|
+
async function opaqueOAuthClaims(token, secretKey, userId, clientId, signal) {
|
|
194
|
+
const response = await fetch("https://api.clerk.com/v1/oauth_applications/access_tokens/verify", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
Authorization: `Bearer ${secretKey}`,
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"Clerk-API-Version": "2026-05-12",
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({ access_token: token }),
|
|
202
|
+
redirect: "error",
|
|
203
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok)
|
|
206
|
+
throw new Error("Opaque OAuth verification failed");
|
|
207
|
+
const claims = await byteReadResponse(response).json();
|
|
208
|
+
if (!claims ||
|
|
209
|
+
typeof claims !== "object" ||
|
|
210
|
+
Array.isArray(claims) ||
|
|
211
|
+
!("object" in claims) ||
|
|
212
|
+
claims.object !== "clerk_idp_oauth_access_token" ||
|
|
213
|
+
!("subject" in claims) ||
|
|
214
|
+
claims.subject !== userId ||
|
|
215
|
+
!("client_id" in claims) ||
|
|
216
|
+
claims.client_id !== clientId ||
|
|
217
|
+
!("revoked" in claims) ||
|
|
218
|
+
claims.revoked !== false ||
|
|
219
|
+
!("expired" in claims) ||
|
|
220
|
+
claims.expired !== false) {
|
|
221
|
+
throw new Error("Opaque OAuth verification response invalid");
|
|
222
|
+
}
|
|
223
|
+
return claims;
|
|
185
224
|
}
|
|
186
225
|
/**
|
|
187
226
|
* The domain of an email address, lowercased for comparison, or null when the
|
|
@@ -217,12 +256,18 @@ export function clerkAuth(opts) {
|
|
|
217
256
|
// Before the Clerk client, so a malformed key fails as a connecta
|
|
218
257
|
// configuration error rather than however the SDK happens to treat it.
|
|
219
258
|
const frontendApiUrl = fapiUrl(opts.publishableKey);
|
|
220
|
-
const clerk =
|
|
259
|
+
const clerk = createByteReadingClerkClient({
|
|
221
260
|
secretKey: opts.secretKey,
|
|
222
261
|
publishableKey: opts.publishableKey,
|
|
223
262
|
});
|
|
224
263
|
const allowedDomains = normalizeAllowedDomains(opts.allowedDomains);
|
|
225
|
-
const
|
|
264
|
+
const allowedOAuthClientIds = normalizeOAuthClientIds(opts.allowedOAuthClientIds);
|
|
265
|
+
const requestedScopes = opts.scopes ?? ["openid", "profile", "email"];
|
|
266
|
+
if (!Array.isArray(requestedScopes) ||
|
|
267
|
+
requestedScopes.some((scope) => typeof scope !== "string" || !/^[\x21\x23-\x5b\x5d-\x7e]+$/.test(scope))) {
|
|
268
|
+
throw new Error("clerkAuth: `scopes` must contain OAuth scope tokens without whitespace, quotes, or backslashes.");
|
|
269
|
+
}
|
|
270
|
+
const scopes = [...requestedScopes];
|
|
226
271
|
const gateCache = new Map();
|
|
227
272
|
const activityLabelCache = new Map();
|
|
228
273
|
const pendingActivityLabels = new Map();
|
|
@@ -232,8 +277,7 @@ export function clerkAuth(opts) {
|
|
|
232
277
|
const cacheActivityLabel = (userId, label) => {
|
|
233
278
|
writeIdentityCache(activityLabelCache, userId, {
|
|
234
279
|
...(label ? { label } : {}),
|
|
235
|
-
exp: Date.now() +
|
|
236
|
-
(label ? ACTIVITY_LABEL_TTL_MS : ACTIVITY_LABEL_MISS_TTL_MS),
|
|
280
|
+
exp: Date.now() + (label ? ACTIVITY_LABEL_TTL_MS : ACTIVITY_LABEL_MISS_TTL_MS),
|
|
237
281
|
});
|
|
238
282
|
};
|
|
239
283
|
const resolveActivityLabel = async (userId) => {
|
|
@@ -247,8 +291,7 @@ export function clerkAuth(opts) {
|
|
|
247
291
|
// concurrency bounded even when requests hang forever: do not queue more
|
|
248
292
|
// identities in memory, and do not start a duplicate for an id whose raw
|
|
249
293
|
// lookup outlived its caller-facing deadline.
|
|
250
|
-
if (inFlightActivityLabelIds.has(userId) ||
|
|
251
|
-
activeActivityLabelLookups >= ACTIVITY_LABEL_MAX_IN_FLIGHT) {
|
|
294
|
+
if (inFlightActivityLabelIds.has(userId) || activeActivityLabelLookups >= ACTIVITY_LABEL_MAX_IN_FLIGHT) {
|
|
252
295
|
cacheActivityLabel(userId, undefined);
|
|
253
296
|
return undefined;
|
|
254
297
|
}
|
|
@@ -289,22 +332,30 @@ export function clerkAuth(opts) {
|
|
|
289
332
|
pendingActivityLabels.set(userId, lookup);
|
|
290
333
|
return lookup;
|
|
291
334
|
};
|
|
292
|
-
const
|
|
335
|
+
const challenge = (request, baseUrl) => {
|
|
336
|
+
const tokenPresent = authorizationCredential(request).kind !== "absent";
|
|
293
337
|
const error = tokenPresent ? `error="invalid_token", ` : "";
|
|
294
338
|
// A pool endpoint is its own protected resource: the challenge names the
|
|
295
339
|
// metadata document whose `resource` matches the URL the client used, or
|
|
296
340
|
// RFC 9728 tells it to reject the mismatch.
|
|
297
341
|
const pool = mcpPoolSuffix(new URL(request.url).pathname);
|
|
298
342
|
const meta = `${resolveBase(baseUrl)}/.well-known/oauth-protected-resource${pool ? `/mcp${pool}` : ""}`;
|
|
343
|
+
const quote = (value) => value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
344
|
+
return `Bearer ${error}resource_metadata="${quote(meta)}", scope="${scopes.join(" ")}"`;
|
|
345
|
+
};
|
|
346
|
+
const unauthorized = (baseUrl, _tokenPresent, request) => {
|
|
299
347
|
return new Response(JSON.stringify({ error: "unauthorized" }), {
|
|
300
348
|
status: 401,
|
|
301
349
|
headers: {
|
|
302
350
|
"Content-Type": "application/json",
|
|
303
|
-
"WWW-Authenticate":
|
|
351
|
+
"WWW-Authenticate": challenge(request, baseUrl),
|
|
304
352
|
},
|
|
305
353
|
});
|
|
306
354
|
};
|
|
307
|
-
const forbidden = () => new Response(JSON.stringify({ error: "forbidden" }), {
|
|
355
|
+
const forbidden = () => new Response(JSON.stringify({ error: "forbidden" }), {
|
|
356
|
+
status: 403,
|
|
357
|
+
headers: { "Content-Type": "application/json" },
|
|
358
|
+
});
|
|
308
359
|
/**
|
|
309
360
|
* The domain half of admission. Fails CLOSED on every uncertainty — no
|
|
310
361
|
* primary email, an unverified one, a malformed address, or the lookup
|
|
@@ -323,24 +374,16 @@ export function clerkAuth(opts) {
|
|
|
323
374
|
}
|
|
324
375
|
}
|
|
325
376
|
catch (error) {
|
|
326
|
-
|
|
377
|
+
logDenial("email_lookup_failed", error);
|
|
327
378
|
return false;
|
|
328
379
|
}
|
|
329
380
|
const domain = email ? emailDomain(email) : null;
|
|
330
381
|
if (!domain) {
|
|
331
|
-
|
|
332
|
-
// with no readable domain) because the caller must not be able to tell
|
|
333
|
-
// them apart — but it must not claim the email is missing when it is
|
|
334
|
-
// there and malformed.
|
|
335
|
-
console.warn(`[connecta] clerk user ${userId} has no verified primary email with a ` +
|
|
336
|
-
"well-formed domain — denying");
|
|
382
|
+
logDenial("verified_email_invalid");
|
|
337
383
|
return false;
|
|
338
384
|
}
|
|
339
385
|
if (!allowedDomains.has(domain)) {
|
|
340
|
-
|
|
341
|
-
// spill the local part of someone's email on every denied request.
|
|
342
|
-
console.warn(`[connecta] clerk user ${userId} denied: email domain ` +
|
|
343
|
-
`${loggableDomain(domain)} is not on allowedDomains`);
|
|
386
|
+
logDenial("email_domain_denied");
|
|
344
387
|
return false;
|
|
345
388
|
}
|
|
346
389
|
return true;
|
|
@@ -359,23 +402,31 @@ export function clerkAuth(opts) {
|
|
|
359
402
|
return hit.allowed;
|
|
360
403
|
let allowed = false;
|
|
361
404
|
try {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
405
|
+
const domainAllowed = await checkDomain(userId);
|
|
406
|
+
allowed = domainAllowed && (opts.gate ? await opts.gate(userId, clerk) : true);
|
|
407
|
+
if (domainAllowed && !allowed)
|
|
408
|
+
logDenial("gate_denied");
|
|
365
409
|
}
|
|
366
410
|
catch {
|
|
411
|
+
logDenial("gate_failed");
|
|
367
412
|
allowed = false;
|
|
368
413
|
}
|
|
369
414
|
writeIdentityCache(gateCache, userId, {
|
|
370
415
|
allowed,
|
|
371
|
-
exp: Date.now() +
|
|
372
|
-
(allowed ? GATE_ALLOWED_TTL_MS : GATE_FORBIDDEN_TTL_MS),
|
|
416
|
+
exp: Date.now() + (allowed ? GATE_ALLOWED_TTL_MS : GATE_FORBIDDEN_TTL_MS),
|
|
373
417
|
});
|
|
374
418
|
return allowed;
|
|
375
419
|
};
|
|
376
420
|
return {
|
|
377
421
|
kind: "clerk",
|
|
378
422
|
interactiveOperator: true,
|
|
423
|
+
recognizesCredential: (request) => {
|
|
424
|
+
if (authorizationCredential(request).kind !== "absent")
|
|
425
|
+
return !isMachineCredential(request);
|
|
426
|
+
return (/(?:^|;\s*)__session(?:_[^=;]+)?=/.test(request.headers.get("cookie") ?? "") ||
|
|
427
|
+
new URL(request.url).searchParams.has("__clerk_synced"));
|
|
428
|
+
},
|
|
429
|
+
challenge,
|
|
379
430
|
activityActorNamespace: frontendApiUrl,
|
|
380
431
|
activityActorLabel: resolveActivityLabel,
|
|
381
432
|
uiAuth: {
|
|
@@ -387,8 +438,14 @@ export function clerkAuth(opts) {
|
|
|
387
438
|
},
|
|
388
439
|
async handleMetadata(request, baseUrl) {
|
|
389
440
|
const { pathname } = new URL(request.url);
|
|
390
|
-
|
|
441
|
+
const protectedResource = pathname === "/.well-known/oauth-protected-resource" ||
|
|
442
|
+
pathname === "/.well-known/oauth-protected-resource/mcp" ||
|
|
443
|
+
/^\/\.well-known\/oauth-protected-resource\/mcp\/[a-z0-9_-]+$/.test(pathname);
|
|
444
|
+
if (!protectedResource)
|
|
391
445
|
return null;
|
|
446
|
+
if (request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS") {
|
|
447
|
+
return new Response(null, { status: 405, headers: { ...CORS_HEADERS, Allow: "GET, HEAD, OPTIONS" } });
|
|
448
|
+
}
|
|
392
449
|
if (request.method === "OPTIONS") {
|
|
393
450
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
394
451
|
}
|
|
@@ -406,74 +463,99 @@ export function clerkAuth(opts) {
|
|
|
406
463
|
scopes_supported: scopes,
|
|
407
464
|
}, { headers: CORS_HEADERS });
|
|
408
465
|
}
|
|
409
|
-
if (pathname === "/.well-known/oauth-authorization-server") {
|
|
410
|
-
try {
|
|
411
|
-
const upstream = await fetch(`${frontendApiUrl}/.well-known/oauth-authorization-server`);
|
|
412
|
-
if (!upstream.ok) {
|
|
413
|
-
return Response.json({ error: "upstream authorization server metadata unavailable" }, { status: 502, headers: CORS_HEADERS });
|
|
414
|
-
}
|
|
415
|
-
return Response.json(await upstream.json(), { headers: CORS_HEADERS });
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
return Response.json({ error: "upstream authorization server metadata unavailable" }, { status: 502, headers: CORS_HEADERS });
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
466
|
return null;
|
|
422
467
|
},
|
|
423
468
|
async authorize(request, baseUrl) {
|
|
424
|
-
const
|
|
469
|
+
const credential = authorizationCredential(request);
|
|
470
|
+
const tokenPresent = credential.kind !== "absent";
|
|
471
|
+
if (credential.kind === "invalid" || isMachineCredential(request)) {
|
|
472
|
+
logDenial("authorization_header_invalid");
|
|
473
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
474
|
+
}
|
|
475
|
+
const browserOAuthRoute = /^\/(?:connect|oauth\/callback)\//.test(new URL(request.url).pathname);
|
|
425
476
|
let userId;
|
|
477
|
+
let sessionCookies;
|
|
426
478
|
try {
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// be JWTs without an azp claim, and Clerk rejects azp=undefined when
|
|
432
|
-
// that option is set. The sibling-subdomain pin it provided is
|
|
433
|
-
// enforced below, only for session tokens that actually carry azp.
|
|
434
|
-
acceptsToken: ["oauth_token", "session_token"],
|
|
479
|
+
const pathname = new URL(request.url).pathname;
|
|
480
|
+
const isMcp = pathname === "/mcp" || pathname.startsWith("/mcp/");
|
|
481
|
+
const state = await clerk.authenticateRequest(authorizationRequest(request), {
|
|
482
|
+
acceptsToken: isMcp ? "oauth_token" : "session_token",
|
|
435
483
|
});
|
|
484
|
+
const browserOAuth = !tokenPresent && browserOAuthRoute;
|
|
485
|
+
// Consent can outlast Clerk's session JWT. Preserve the SDK's browser
|
|
486
|
+
// handshake, which returns here with a refreshed, verified session.
|
|
487
|
+
if (browserOAuth && state.status === "handshake" && state.headers.has("location")) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
response: new Response(null, {
|
|
491
|
+
status: 307,
|
|
492
|
+
headers: state.headers,
|
|
493
|
+
}),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (browserOAuth)
|
|
497
|
+
sessionCookies = state.headers?.getSetCookie();
|
|
436
498
|
const auth = state.toAuth();
|
|
437
499
|
if (!auth?.isAuthenticated) {
|
|
438
|
-
|
|
439
|
-
// otherwise indistinguishable 401s in `wrangler tail`.
|
|
440
|
-
const detail = state;
|
|
441
|
-
console.warn(`[connecta] clerk rejected request: status=${state.status}` +
|
|
442
|
-
` reason=${detail.reason ?? "?"} message=${detail.message ?? ""}` +
|
|
443
|
-
` tokenShape=${tokenShape(request)}`);
|
|
500
|
+
logDenial("authentication_failed");
|
|
444
501
|
return {
|
|
445
502
|
ok: false,
|
|
446
503
|
response: unauthorized(baseUrl, tokenPresent, request),
|
|
447
504
|
};
|
|
448
505
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
506
|
+
if (isMcp) {
|
|
507
|
+
const resource = `${resolveBase(baseUrl)}/mcp${mcpPoolSuffix(pathname) ?? ""}`;
|
|
508
|
+
if (auth.tokenType !== "oauth_token") {
|
|
509
|
+
logDenial("token_type_mismatch");
|
|
510
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
511
|
+
}
|
|
512
|
+
const token = await auth.getToken();
|
|
513
|
+
let claims;
|
|
514
|
+
if (token.startsWith("oat_")) {
|
|
515
|
+
try {
|
|
516
|
+
claims = await opaqueOAuthClaims(token, opts.secretKey, auth.userId, auth.clientId, request.signal);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
logDenial("oauth_verification_failed");
|
|
520
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
claims = decodeJwt(token).payload;
|
|
525
|
+
}
|
|
526
|
+
const rejection = oauthBindingRejection(claims, resource, auth.clientId, allowedOAuthClientIds);
|
|
527
|
+
if (rejection) {
|
|
528
|
+
logDenial(rejection);
|
|
529
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
if (auth.tokenType !== "session_token") {
|
|
534
|
+
logDenial("token_type_mismatch");
|
|
535
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
536
|
+
}
|
|
537
|
+
// Browser session origins retain their existing deployment pin.
|
|
538
|
+
const azp = auth.sessionClaims?.azp;
|
|
455
539
|
const origin = new URL(resolveBase(baseUrl)).origin;
|
|
456
540
|
if (azp && azp !== origin) {
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
ok: false,
|
|
460
|
-
response: unauthorized(baseUrl, tokenPresent, request),
|
|
461
|
-
};
|
|
541
|
+
logDenial("session_origin_mismatch");
|
|
542
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
462
543
|
}
|
|
463
544
|
}
|
|
464
|
-
userId =
|
|
545
|
+
userId = auth.userId ?? undefined;
|
|
465
546
|
}
|
|
466
|
-
catch
|
|
467
|
-
|
|
547
|
+
catch {
|
|
548
|
+
logDenial("authentication_failed");
|
|
468
549
|
return { ok: false, response: unauthorized(baseUrl, true, request) };
|
|
469
550
|
}
|
|
470
551
|
if (!userId) {
|
|
552
|
+
logDenial("user_missing");
|
|
471
553
|
return { ok: false, response: unauthorized(baseUrl, true, request) };
|
|
472
554
|
}
|
|
473
555
|
if (!(await checkGate(userId))) {
|
|
474
556
|
return { ok: false, response: forbidden() };
|
|
475
557
|
}
|
|
476
|
-
return { ok: true, userId };
|
|
558
|
+
return { ok: true, userId, ...(sessionCookies?.length ? { sessionCookies } : {}) };
|
|
477
559
|
},
|
|
478
560
|
};
|
|
479
561
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { authorizationCredential } from "../inbound-credential.js";
|
|
2
|
+
import { validIdentityReference } from "../identity.js";
|
|
1
3
|
function identityString(identity, field) {
|
|
2
4
|
const value = identity[field];
|
|
3
|
-
return typeof value === "string" &&
|
|
5
|
+
return typeof value === "string" && validIdentityReference({ namespace: "cloudflare-access", id: value })
|
|
6
|
+
? value
|
|
7
|
+
: undefined;
|
|
4
8
|
}
|
|
5
9
|
function unauthorized() {
|
|
6
10
|
return {
|
|
7
11
|
ok: false,
|
|
8
|
-
response: Response.json({ error: "Cloudflare Access authentication required" }, { status: 401 }),
|
|
12
|
+
response: Response.json({ error: "Cloudflare Access authentication required" }, { status: 401, headers: { "WWW-Authenticate": 'Bearer scope="openid email"' } }),
|
|
9
13
|
};
|
|
10
14
|
}
|
|
11
15
|
/**
|
|
@@ -18,11 +22,15 @@ export function cloudflareAccessAuth() {
|
|
|
18
22
|
return {
|
|
19
23
|
kind: "cloudflare-access",
|
|
20
24
|
interactiveOperator: true,
|
|
25
|
+
recognizesCredential: (request, context) => authorizationCredential(request).kind === "absent" && Boolean(context?.access),
|
|
26
|
+
challenge: () => 'Bearer scope="openid email"',
|
|
21
27
|
activityActorNamespace: "cloudflare-access",
|
|
22
28
|
uiAuth: { kind: "cloudflare-access" },
|
|
23
|
-
async authorize(
|
|
29
|
+
async authorize(request, _baseUrl, runtimeContext) {
|
|
30
|
+
if (authorizationCredential(request).kind !== "absent")
|
|
31
|
+
return unauthorized();
|
|
24
32
|
const access = runtimeContext?.access;
|
|
25
|
-
if (!access)
|
|
33
|
+
if (!access || typeof access.aud !== "string" || !/^[\x21-\x7e]{1,256}$/.test(access.aud))
|
|
26
34
|
return unauthorized();
|
|
27
35
|
let identity;
|
|
28
36
|
try {
|
|
@@ -31,29 +39,20 @@ export function cloudflareAccessAuth() {
|
|
|
31
39
|
catch {
|
|
32
40
|
return unauthorized();
|
|
33
41
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const commonName = identityString(identity, "common_name");
|
|
45
|
-
const serviceTokenId = identityString(identity, "service_token_id");
|
|
42
|
+
// Access binds ctx.access.aud to its Worker application at the edge.
|
|
43
|
+
// Service credentials can pass that edge, but connecta machines must
|
|
44
|
+
// present a cta_ token to the machine provider instead.
|
|
45
|
+
const forbidden = () => ({
|
|
46
|
+
ok: false,
|
|
47
|
+
response: Response.json({ error: "Cloudflare Access human identity required" }, { status: 403 }),
|
|
48
|
+
});
|
|
49
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity))
|
|
50
|
+
return forbidden();
|
|
51
|
+
const userId = identityString(identity, "user_uuid") ?? identityString(identity, "email");
|
|
46
52
|
if (identity.service_token_status === true ||
|
|
47
|
-
|
|
48
|
-
(
|
|
49
|
-
|
|
50
|
-
return subjectId
|
|
51
|
-
? { ok: true, subjectId }
|
|
52
|
-
: {
|
|
53
|
-
ok: false,
|
|
54
|
-
response: Response.json({ error: "Cloudflare Access service identity required" }, { status: 403 }),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
53
|
+
identityString(identity, "service_token_id") ||
|
|
54
|
+
identityString(identity, "common_name"))
|
|
55
|
+
return forbidden();
|
|
57
56
|
if (!userId) {
|
|
58
57
|
return {
|
|
59
58
|
ok: false,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OAuthClientMetadata } from "@modelcontextprotocol/client";
|
|
2
|
+
import type { Connector } from "../types.js";
|
|
3
|
+
import type { SentSecrets } from "../sent-secrets.js";
|
|
4
|
+
/** A deployment-owned identity, bound to one authorization server. */
|
|
5
|
+
export interface RemoteOAuthClient {
|
|
6
|
+
issuer: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
clientSecret?: string;
|
|
9
|
+
tokenEndpointAuthMethod?: "none" | "client_secret_basic" | "client_secret_post";
|
|
10
|
+
}
|
|
11
|
+
/** Normalize the deployment-selected default once, including revocation. */
|
|
12
|
+
export declare function remoteClientAuthMethod(client: RemoteOAuthClient): "none" | "client_secret_basic" | "client_secret_post";
|
|
13
|
+
/** Register raw OAuth client credentials from the final request before dispatch.
|
|
14
|
+
* Basic components use RFC 6749 form encoding, unlike ordinary Basic auth.
|
|
15
|
+
* Public client IDs remain visible; confidential IDs join their client secret. */
|
|
16
|
+
export declare function trackRemoteClientRequest(secrets: SentSecrets, input: RequestInfo | URL, init?: RequestInit): void;
|
|
17
|
+
export declare function assertRemoteOAuthClient(id: string, client: RemoteOAuthClient | undefined): void;
|
|
18
|
+
/** The one metadata definition used for DCR, binding, and the public document. */
|
|
19
|
+
export declare function downstreamClientMetadata(redirectUri: string, scope?: string, clientName?: string, method?: string): OAuthClientMetadata;
|
|
20
|
+
/** Only configured public HTTPS deployments can publish a self-hosted client ID. */
|
|
21
|
+
export declare function selfHostedClientUrl(publicUrl: string | undefined, id: string): string | undefined;
|
|
22
|
+
/** One canonical callback for the served document and the SDK's whole flow. */
|
|
23
|
+
export declare function downstreamRedirectUri(baseUrl: string, publicUrl: string | undefined, id: string): string;
|
|
24
|
+
export declare function declareSelfHostedClient(connector: Connector, scope?: string): void;
|
|
25
|
+
export declare function selfHostedClientDocument(connector: Connector, publicUrl: string | undefined, clientName: string): (OAuthClientMetadata & {
|
|
26
|
+
client_id: string;
|
|
27
|
+
}) | undefined;
|
|
28
|
+
/** RFC 6749 client authentication; deployment-selected methods never downgrade. */
|
|
29
|
+
export declare function authenticateRemoteClient(client: RemoteOAuthClient, headers: Headers, params: URLSearchParams): void;
|