@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.
Files changed (319) hide show
  1. package/CHANGELOG.md +788 -5
  2. package/README.md +62 -42
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1584 -1580
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +93 -71
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +59 -7
  52. package/dist/connectors/api-connector.js +106 -19
  53. package/dist/connectors/api.js +4 -1
  54. package/dist/connectors/guarded-fetch.d.ts +144 -0
  55. package/dist/connectors/guarded-fetch.js +84 -35
  56. package/dist/connectors/negotiation-cache.js +46 -0
  57. package/dist/connectors/option-shapes.js +27 -0
  58. package/dist/connectors/remote-mcp.d.ts +30 -4
  59. package/dist/connectors/remote-mcp.js +1774 -320
  60. package/dist/connectors/resource-uri.js +335 -0
  61. package/dist/credential-contract.d.ts +10 -0
  62. package/dist/credential-rules.js +1 -3
  63. package/dist/credential-url.js +108 -0
  64. package/dist/credentials.d.ts +6 -1
  65. package/dist/credentials.js +27 -18
  66. package/dist/d1.d.ts +130 -0
  67. package/dist/d1.js +290 -0
  68. package/dist/describe-config.d.ts +165 -0
  69. package/dist/describe-config.js +315 -0
  70. package/dist/described.js +106 -0
  71. package/dist/downstream-input-context.js +119 -0
  72. package/dist/downstream-input.js +323 -0
  73. package/dist/errors.d.ts +97 -20
  74. package/dist/errors.js +158 -80
  75. package/dist/execute.js +453 -305
  76. package/dist/executor-admission.d.ts +1 -0
  77. package/dist/executor-admission.js +11 -15
  78. package/dist/executor-contract.d.ts +20 -0
  79. package/dist/executor-contract.js +48 -0
  80. package/dist/executor-result.js +24 -9
  81. package/dist/executors/quickjs-child.js +3 -4
  82. package/dist/executors/quickjs-runtime.js +147 -59
  83. package/dist/executors/quickjs.js +29 -35
  84. package/dist/favicon.js +1 -2
  85. package/dist/fix-prompt.js +1 -3
  86. package/dist/guest-runtime.js +188 -0
  87. package/dist/guest-types.d.ts +176 -0
  88. package/dist/guest-types.js +1 -0
  89. package/dist/html-security.js +24 -0
  90. package/dist/identity.js +1 -3
  91. package/dist/inbound-credential.js +52 -0
  92. package/dist/index.d.ts +24 -256
  93. package/dist/index.js +65 -478
  94. package/dist/invocation-auth.js +69 -0
  95. package/dist/invocation.js +230 -73
  96. package/dist/json-schema.d.ts +1 -1
  97. package/dist/mcp-client-context.js +35 -0
  98. package/dist/mcp-result.js +11 -7
  99. package/dist/meta-output.js +76 -0
  100. package/dist/meta-tool-names.d.ts +2 -0
  101. package/dist/meta-tool-names.js +3 -0
  102. package/dist/meta-tools.js +285 -209
  103. package/dist/module-contracts.d.ts +8 -23
  104. package/dist/node.d.ts +0 -2
  105. package/dist/node.js +2 -2
  106. package/dist/oauth-callback-outcome.js +1 -3
  107. package/dist/oauth-handoff.js +117 -0
  108. package/dist/oauth-sealing.js +20 -0
  109. package/dist/operator-record.d.ts +137 -0
  110. package/dist/operator-record.js +420 -0
  111. package/dist/operator-ui/assets.js +31 -0
  112. package/dist/operator-ui/contract.d.ts +72 -0
  113. package/dist/operator-ui/contract.js +1 -0
  114. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  115. package/dist/operator-ui/generated.js +5 -4
  116. package/dist/operator-ui/model.d.ts +12 -87
  117. package/dist/operator-ui/model.js +2 -9
  118. package/dist/operator-ui/setup-commands.js +1 -3
  119. package/dist/operator-ui/view.d.ts +323 -0
  120. package/dist/operator-ui/view.js +62 -189
  121. package/dist/page-styles.js +2 -3
  122. package/dist/program-source.js +14 -2
  123. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  124. package/dist/provider.d.ts +142 -0
  125. package/dist/provider.js +187 -0
  126. package/dist/providers/_shared/google/result-size.js +57 -0
  127. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  128. package/dist/providers/_shared/google/service-account.js +500 -0
  129. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  130. package/dist/providers/_shared/google/workspace.js +518 -0
  131. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  132. package/dist/providers/basecamp/index.js +997 -0
  133. package/dist/providers/basecamp/skill.generated.js +9 -0
  134. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  135. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  136. package/dist/providers/breeze/skill.generated.js +9 -0
  137. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  138. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  139. package/dist/providers/ccb/skill.generated.js +9 -0
  140. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  141. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  142. package/dist/providers/cloudflare/skill.generated.js +10 -0
  143. package/dist/providers/docs/index.d.ts +54 -0
  144. package/dist/providers/docs/index.js +1237 -0
  145. package/dist/providers/docs/skill.generated.js +9 -0
  146. package/dist/providers/drive/index.d.ts +53 -0
  147. package/dist/providers/drive/index.js +1523 -0
  148. package/dist/providers/drive/skill.generated.js +9 -0
  149. package/dist/providers/forms/index.d.ts +54 -0
  150. package/dist/providers/forms/index.js +1217 -0
  151. package/dist/providers/forms/skill.generated.js +11 -0
  152. package/dist/providers/github/auth.d.ts +32 -0
  153. package/dist/providers/github/auth.js +361 -0
  154. package/dist/providers/github/hosted.js +218 -0
  155. package/dist/providers/github/index.d.ts +13 -0
  156. package/dist/providers/github/index.js +140 -0
  157. package/dist/providers/github/key.js +100 -0
  158. package/dist/providers/github/rest.js +321 -0
  159. package/dist/providers/github/scope.d.ts +29 -0
  160. package/dist/providers/github/scope.js +124 -0
  161. package/dist/providers/github/skill.generated.js +8 -0
  162. package/dist/providers/gmail/index.d.ts +51 -0
  163. package/dist/providers/gmail/index.js +1436 -0
  164. package/dist/providers/gmail/skill.generated.js +9 -0
  165. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  166. package/dist/providers/linear/index.js +352 -0
  167. package/dist/providers/linear/skill.generated.js +8 -0
  168. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  169. package/dist/providers/mixpanel/index.js +416 -0
  170. package/dist/providers/mixpanel/skill.generated.js +9 -0
  171. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  172. package/dist/providers/{notion.js → notion/index.js} +226 -239
  173. package/dist/providers/notion/skill.generated.js +10 -0
  174. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  175. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  176. package/dist/providers/overflow/skill.generated.js +8 -0
  177. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  178. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  179. package/dist/providers/planning-center/skill.generated.js +10 -0
  180. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  181. package/dist/providers/revenuecat/index.js +557 -0
  182. package/dist/providers/revenuecat/skill.generated.js +11 -0
  183. package/dist/providers/sheets/index.d.ts +54 -0
  184. package/dist/providers/sheets/index.js +1237 -0
  185. package/dist/providers/sheets/skill.generated.js +10 -0
  186. package/dist/providers/slides/index.d.ts +53 -0
  187. package/dist/providers/slides/index.js +2645 -0
  188. package/dist/providers/slides/skill.generated.js +9 -0
  189. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  190. package/dist/providers/stripe/index.js +266 -0
  191. package/dist/providers/stripe/skill.generated.js +11 -0
  192. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  193. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  194. package/dist/providers/tithely/skill.generated.js +8 -0
  195. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  196. package/dist/providers/vercel/index.js +1038 -0
  197. package/dist/providers/vercel/mcp-ownership.js +212 -0
  198. package/dist/providers/vercel/skill.generated.js +11 -0
  199. package/dist/registry.d.ts +108 -171
  200. package/dist/registry.js +513 -925
  201. package/dist/request-state.js +21 -0
  202. package/dist/resource-read.js +100 -0
  203. package/dist/result-shapes.js +17 -49
  204. package/dist/retired-toolkits.js +1 -1
  205. package/dist/routes/access-tokens.js +3 -6
  206. package/dist/routes/activity-context.js +21 -0
  207. package/dist/routes/activity-disclosure.js +40 -0
  208. package/dist/routes/activity.js +59 -21
  209. package/dist/routes/actor-labels.js +3 -50
  210. package/dist/routes/connect.js +221 -0
  211. package/dist/routes/credentials.js +15 -27
  212. package/dist/routes/mcp.js +356 -130
  213. package/dist/routes/oauth-client-metadata.js +15 -0
  214. package/dist/routes/oauth-management.js +45 -107
  215. package/dist/routes/oauth.js +110 -57
  216. package/dist/routes/operator.js +3 -3
  217. package/dist/routes/shared.d.ts +25 -64
  218. package/dist/routes/shared.js +160 -80
  219. package/dist/routes/skills.js +30 -0
  220. package/dist/routes/ui-config.js +225 -0
  221. package/dist/routes/ui-data.js +40 -35
  222. package/dist/routes/ui.js +66 -56
  223. package/dist/runtime/admission.js +4 -2
  224. package/dist/runtime/call-admission.js +3 -1
  225. package/dist/runtime/connector-scope.js +1 -4
  226. package/dist/runtime/run.js +10 -39
  227. package/dist/runtime/services.js +12 -117
  228. package/dist/runtime/shared-read.js +1 -3
  229. package/dist/runtime/storage.js +8 -1
  230. package/dist/sent-secrets.d.ts +52 -0
  231. package/dist/sent-secrets.js +474 -0
  232. package/dist/server.js +28 -71
  233. package/dist/skills.js +406 -174
  234. package/dist/sqlite.d.ts +48 -0
  235. package/dist/sqlite.js +166 -0
  236. package/dist/storage/keys.js +428 -0
  237. package/dist/storage/memory.js +7 -0
  238. package/dist/storage/sql.d.ts +66 -0
  239. package/dist/storage/sql.js +534 -0
  240. package/dist/timeout.js +4 -2
  241. package/dist/tool-name.js +17 -0
  242. package/dist/tool-safety.d.ts +9 -35
  243. package/dist/tool-safety.js +20 -40
  244. package/dist/types.d.ts +320 -54
  245. package/dist/types.js +0 -1
  246. package/dist/ui.d.ts +7 -19
  247. package/dist/ui.js +76 -90
  248. package/dist/url-safety.js +3 -6
  249. package/dist/usage-guide.js +137 -0
  250. package/dist/validate.d.ts +7 -0
  251. package/dist/validate.js +309 -47
  252. package/dist/version.d.ts +1 -7
  253. package/dist/version.js +3 -7
  254. package/dist/worker.d.ts +11 -0
  255. package/dist/worker.js +434 -0
  256. package/documentation/architecture.md +664 -231
  257. package/documentation/auth.md +936 -546
  258. package/documentation/code-mode.md +501 -312
  259. package/documentation/meta-tools.md +449 -200
  260. package/documentation/operator-tests.md +93 -0
  261. package/documentation/provider-migration-0.29.md +60 -0
  262. package/examples/worker/AGENTS.md +34 -12
  263. package/examples/worker/README.md +286 -255
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  266. package/examples/worker/src/connecta.config.ts +151 -0
  267. package/examples/worker/src/index.ts +14 -197
  268. package/examples/worker/wrangler.jsonc +15 -35
  269. package/package.json +120 -54
  270. package/templates/node/.dockerignore +1 -2
  271. package/templates/node/.env.example +20 -21
  272. package/templates/node/AGENTS.md +21 -17
  273. package/templates/node/Dockerfile +3 -5
  274. package/templates/node/README.md +133 -58
  275. package/templates/node/docker-compose.yml +11 -12
  276. package/templates/node/package.json +4 -2
  277. package/templates/node/src/connecta.config.ts +119 -0
  278. package/templates/node/src/index.ts +9 -154
  279. package/templates/node/src/provision-token.ts +17 -0
  280. package/AGENTS.md +0 -170
  281. package/dist/artifacts/connector.d.ts +0 -58
  282. package/dist/artifacts/connector.js +0 -730
  283. package/dist/artifacts/document.js +0 -86
  284. package/dist/artifacts/guide.js +0 -140
  285. package/dist/artifacts/html-scan.js +0 -329
  286. package/dist/artifacts/json.js +0 -78
  287. package/dist/artifacts/kv-store.d.ts +0 -20
  288. package/dist/artifacts/kv-store.js +0 -140
  289. package/dist/artifacts/markdown.js +0 -478
  290. package/dist/artifacts/module.d.ts +0 -37
  291. package/dist/artifacts/module.js +0 -66
  292. package/dist/artifacts/operations.d.ts +0 -197
  293. package/dist/artifacts/operations.js +0 -1002
  294. package/dist/artifacts/refresh.d.ts +0 -55
  295. package/dist/artifacts/refresh.js +0 -236
  296. package/dist/artifacts/routes.js +0 -206
  297. package/dist/artifacts/types.d.ts +0 -214
  298. package/dist/artifacts/types.js +0 -22
  299. package/dist/artifacts/validate.d.ts +0 -62
  300. package/dist/artifacts/validate.js +0 -651
  301. package/dist/artifacts.d.ts +0 -6
  302. package/dist/artifacts.js +0 -9
  303. package/dist/auth/bearer.d.ts +0 -11
  304. package/dist/auth/bearer.js +0 -50
  305. package/dist/providers/basecamp.js +0 -445
  306. package/dist/providers/linear.js +0 -217
  307. package/dist/providers/mixpanel.js +0 -247
  308. package/dist/providers/revenuecat.js +0 -317
  309. package/dist/providers/stripe.js +0 -253
  310. package/dist/providers/vercel.js +0 -1253
  311. package/dist/storage/file.d.ts +0 -15
  312. package/dist/storage/file.js +0 -455
  313. package/ethos.md +0 -93
  314. package/examples/worker/src/cloudflare-kv.ts +0 -43
  315. package/examples/worker/src/d1-activity-row.ts +0 -155
  316. package/examples/worker/src/d1-activity.ts +0 -147
  317. package/examples/worker/src/d1-storage.ts +0 -138
  318. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  319. 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
+ }
@@ -1,11 +1,22 @@
1
- import { createClerkClient } from "@clerk/backend";
1
+ import type { ClerkGateClient } from "./clerk-sdk/client.js";
2
2
  import type { InboundAuth } from "../types.js";
3
- type ClerkClient = ReturnType<typeof createClerkClient>;
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
- * `bearerToken` has no email to read and is admitted without a domain check.
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. Return false to reject an authenticated user. */
22
- gate?: (userId: string, clerk: ClerkClient) => boolean | Promise<boolean>;
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 {};
@@ -1,25 +1,17 @@
1
- // Clerk as the OAuth 2.1 authorization server; connecta is the resource server.
2
- // Single tenant, no tenant-tag requirement, optional allowedDomains/gate() with
3
- // ~60s identity caching.
4
- import { createClerkClient } from "@clerk/backend";
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, POST, OPTIONS",
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
- [user.firstName, user.lastName].filter(Boolean).join(" ");
106
- const primary = user.emailAddresses?.find((address) => address.id === user.primaryEmailAddressId &&
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
- * Bounded, escaped form of the denied domain for the operator log — the same
175
- * treatment server logs give other caller-controlled values. An email domain is
176
- * caller-influenced (anyone who controls a mailbox controls its domain): the
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 loggableDomain(domain) {
182
- const bounded = domain.slice(0, 100);
183
- const escaped = JSON.stringify(bounded).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
184
- return escaped + (bounded.length < domain.length ? " (truncated)" : "");
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 = createClerkClient({
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 scopes = opts.scopes ?? ["openid", "profile", "email"];
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 unauthorized = (baseUrl, tokenPresent, request) => {
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": `Bearer ${error}resource_metadata="${meta}"`,
351
+ "WWW-Authenticate": challenge(request, baseUrl),
304
352
  },
305
353
  });
306
354
  };
307
- const forbidden = () => new Response(JSON.stringify({ error: "forbidden" }), { status: 403, headers: { "Content-Type": "application/json" } });
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
- console.warn(`[connecta] clerk email lookup failed for ${userId}: ${error instanceof Error ? error.message : String(error)} — denying`);
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
- // One line for three cases (no primary email, unverified, or an address
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
- // The domain, never the address: this is an operator log, not a place to
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
- allowed =
363
- (await checkDomain(userId)) &&
364
- (opts.gate ? await opts.gate(userId, clerk) : true);
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
- if (!pathname.startsWith("/.well-known/"))
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 tokenPresent = Boolean(request.headers.get("authorization"));
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 state = await clerk.authenticateRequest(request, {
428
- // MCP clients use Clerk OAuth access tokens; the browser operator UI
429
- // uses the signed-in operator's short-lived Clerk session token.
430
- // authorizedParties must NOT be passed here: OAuth access tokens may
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
- // Reason (not the token) in the logs: bearer rejections are
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
- // Session JWTs carry `azp` (the origin they were minted for); pin it
450
- // to this connecta deployment so a sibling subdomain's cookie/token
451
- // cannot be replayed here. OAuth access tokens may have no azp.
452
- const typed = auth;
453
- if (typed.tokenType === "session_token") {
454
- const azp = typed.sessionClaims?.azp;
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
- console.warn(`[connecta] session token azp mismatch: azp=${azp} expected=${origin}`);
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 = typed.userId ?? undefined;
545
+ userId = auth.userId ?? undefined;
465
546
  }
466
- catch (error) {
467
- console.warn(`[connecta] clerk authenticateRequest threw: ${error instanceof Error ? error.message : String(error)} tokenShape=${tokenShape(request)}`);
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" && value.length > 0 ? value : undefined;
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(_request, _baseUrl, runtimeContext) {
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
- if (!identity) {
35
- // Access service-token policies authenticate the request and attach
36
- // ctx.access, but getIdentity() is a user-identity API and returns
37
- // undefined. Cloudflare strips the service-token headers before the
38
- // Worker, so the Access application audience is the only trusted,
39
- // stable service attribution available without parsing a JWT.
40
- return { ok: true, subjectId: access.aud };
41
- }
42
- const userId = identityString(identity, "user_uuid") ??
43
- identityString(identity, "email");
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
- serviceTokenId ||
48
- (!userId && commonName)) {
49
- const subjectId = serviceTokenId ?? commonName;
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;