@greatwait/contracts 0.1.0-alpha.1

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 (241) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/authority.d.ts +217 -0
  4. package/dist/authority.js +261 -0
  5. package/dist/callback-ownership.d.ts +58 -0
  6. package/dist/callback-ownership.js +71 -0
  7. package/dist/callback-safety.d.ts +38 -0
  8. package/dist/callback-safety.js +403 -0
  9. package/dist/compatibility.d.ts +82 -0
  10. package/dist/compatibility.js +115 -0
  11. package/dist/delivery.d.ts +401 -0
  12. package/dist/delivery.js +311 -0
  13. package/dist/endpoints.d.ts +305 -0
  14. package/dist/endpoints.js +179 -0
  15. package/dist/events.d.ts +62 -0
  16. package/dist/events.js +38 -0
  17. package/dist/first-value-journey.d.ts +305 -0
  18. package/dist/first-value-journey.js +466 -0
  19. package/dist/forecast.d.ts +443 -0
  20. package/dist/forecast.js +729 -0
  21. package/dist/identifiers.d.ts +109 -0
  22. package/dist/identifiers.js +131 -0
  23. package/dist/identity.d.ts +196 -0
  24. package/dist/identity.js +182 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.js +27 -0
  27. package/dist/mcp.d.ts +51 -0
  28. package/dist/mcp.js +56 -0
  29. package/dist/oauth.d.ts +796 -0
  30. package/dist/oauth.js +708 -0
  31. package/dist/operations.d.ts +3985 -0
  32. package/dist/operations.js +1837 -0
  33. package/dist/partition.d.ts +771 -0
  34. package/dist/partition.js +206 -0
  35. package/dist/policy.d.ts +93 -0
  36. package/dist/policy.js +180 -0
  37. package/dist/problems.d.ts +158 -0
  38. package/dist/problems.js +104 -0
  39. package/dist/protocol.d.ts +308 -0
  40. package/dist/protocol.js +515 -0
  41. package/dist/scheduling-records.d.ts +395 -0
  42. package/dist/scheduling-records.js +284 -0
  43. package/dist/scheduling.d.ts +1098 -0
  44. package/dist/scheduling.js +1116 -0
  45. package/dist/secrets.d.ts +383 -0
  46. package/dist/secrets.js +272 -0
  47. package/dist/signatures.d.ts +704 -0
  48. package/dist/signatures.js +867 -0
  49. package/dist/tenancy.d.ts +178 -0
  50. package/dist/tenancy.js +145 -0
  51. package/dist/test-sink.d.ts +986 -0
  52. package/dist/test-sink.js +755 -0
  53. package/dist/time.d.ts +45 -0
  54. package/dist/time.js +128 -0
  55. package/dist/usage.d.ts +121 -0
  56. package/dist/usage.js +200 -0
  57. package/dist/webhooks.d.ts +116 -0
  58. package/dist/webhooks.js +161 -0
  59. package/dist/workspace.d.ts +92 -0
  60. package/dist/workspace.js +59 -0
  61. package/generated/v1/callback-delivery-signature.json +467 -0
  62. package/generated/v1/callback-endpoint-verification.json +59 -0
  63. package/generated/v1/callback-receiver.json +241 -0
  64. package/generated/v1/examples/attempts.list.response.json +24 -0
  65. package/generated/v1/examples/attempts.read.response.json +20 -0
  66. package/generated/v1/examples/credentials.create.request.json +5 -0
  67. package/generated/v1/examples/credentials.create.response.json +10 -0
  68. package/generated/v1/examples/credentials.list.response.json +17 -0
  69. package/generated/v1/examples/credentials.revoke.response.json +11 -0
  70. package/generated/v1/examples/credentials.rotate.request.json +3 -0
  71. package/generated/v1/examples/credentials.rotate.response.json +22 -0
  72. package/generated/v1/examples/endpoints.create.request.json +3 -0
  73. package/generated/v1/examples/endpoints.create.response.json +16 -0
  74. package/generated/v1/examples/endpoints.disable.response.json +15 -0
  75. package/generated/v1/examples/endpoints.list.response.json +21 -0
  76. package/generated/v1/examples/endpoints.read.response.json +14 -0
  77. package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
  78. package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
  79. package/generated/v1/examples/endpoints.update.request.json +3 -0
  80. package/generated/v1/examples/endpoints.update.response.json +13 -0
  81. package/generated/v1/examples/endpoints.verify.response.json +15 -0
  82. package/generated/v1/examples/forecast.read.response.json +28 -0
  83. package/generated/v1/examples/forecast.windows.response.json +21 -0
  84. package/generated/v1/examples/grants.list.response.json +17 -0
  85. package/generated/v1/examples/grants.revoke.response.json +16 -0
  86. package/generated/v1/examples/meta.scaffold.response.json +5 -0
  87. package/generated/v1/examples/meta.version.response.json +5 -0
  88. package/generated/v1/examples/occurrences.list.response.json +17 -0
  89. package/generated/v1/examples/occurrences.read.response.json +15 -0
  90. package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
  91. package/generated/v1/examples/projects.create.request.json +3 -0
  92. package/generated/v1/examples/projects.create.response.json +5 -0
  93. package/generated/v1/examples/projects.list.response.json +9 -0
  94. package/generated/v1/examples/schedules.archive.response.json +25 -0
  95. package/generated/v1/examples/schedules.cancel.response.json +24 -0
  96. package/generated/v1/examples/schedules.create.request.json +9 -0
  97. package/generated/v1/examples/schedules.create.response.json +25 -0
  98. package/generated/v1/examples/schedules.list.response.json +23 -0
  99. package/generated/v1/examples/schedules.pause.response.json +23 -0
  100. package/generated/v1/examples/schedules.read.response.json +25 -0
  101. package/generated/v1/examples/schedules.replace.request.json +8 -0
  102. package/generated/v1/examples/schedules.replace.response.json +25 -0
  103. package/generated/v1/examples/schedules.resume.response.json +23 -0
  104. package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
  105. package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
  106. package/generated/v1/examples/testsinks.configure.request.json +3 -0
  107. package/generated/v1/examples/testsinks.configure.response.json +13 -0
  108. package/generated/v1/examples/testsinks.create.request.json +8 -0
  109. package/generated/v1/examples/testsinks.create.response.json +13 -0
  110. package/generated/v1/examples/testsinks.delete.response.json +13 -0
  111. package/generated/v1/examples/testsinks.list.response.json +20 -0
  112. package/generated/v1/examples/testsinks.read.response.json +14 -0
  113. package/generated/v1/examples/usage.read.response.json +20 -0
  114. package/generated/v1/first-value-journey.json +436 -0
  115. package/generated/v1/manifest.json +1037 -0
  116. package/generated/v1/openapi.json +23665 -0
  117. package/generated/v1/policy-matrix.json +305 -0
  118. package/generated/v1/schemas/attempts.list.query.json +16 -0
  119. package/generated/v1/schemas/attempts.list.response.json +175 -0
  120. package/generated/v1/schemas/attempts.read.response.json +168 -0
  121. package/generated/v1/schemas/credentials.create.request.json +39 -0
  122. package/generated/v1/schemas/credentials.create.response.json +88 -0
  123. package/generated/v1/schemas/credentials.list.query.json +16 -0
  124. package/generated/v1/schemas/credentials.list.response.json +124 -0
  125. package/generated/v1/schemas/credentials.revoke.response.json +84 -0
  126. package/generated/v1/schemas/credentials.rotate.request.json +11 -0
  127. package/generated/v1/schemas/credentials.rotate.response.json +171 -0
  128. package/generated/v1/schemas/endpoints.create.request.json +12 -0
  129. package/generated/v1/schemas/endpoints.create.response.json +115 -0
  130. package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
  131. package/generated/v1/schemas/endpoints.disable.response.json +100 -0
  132. package/generated/v1/schemas/endpoints.list.query.json +16 -0
  133. package/generated/v1/schemas/endpoints.list.response.json +146 -0
  134. package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
  135. package/generated/v1/schemas/endpoints.read.response.json +100 -0
  136. package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
  137. package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
  138. package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
  139. package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
  140. package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
  141. package/generated/v1/schemas/endpoints.update.request.json +12 -0
  142. package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
  143. package/generated/v1/schemas/endpoints.update.response.json +100 -0
  144. package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
  145. package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
  146. package/generated/v1/schemas/endpoints.verify.response.json +100 -0
  147. package/generated/v1/schemas/forecast.read.query.json +26 -0
  148. package/generated/v1/schemas/forecast.read.response.json +100 -0
  149. package/generated/v1/schemas/forecast.windows.query.json +26 -0
  150. package/generated/v1/schemas/forecast.windows.response.json +94 -0
  151. package/generated/v1/schemas/grants.list.response.json +98 -0
  152. package/generated/v1/schemas/grants.revoke.response.json +95 -0
  153. package/generated/v1/schemas/meta.scaffold.response.json +36 -0
  154. package/generated/v1/schemas/meta.version.response.json +19 -0
  155. package/generated/v1/schemas/occurrences.list.query.json +16 -0
  156. package/generated/v1/schemas/occurrences.list.response.json +83 -0
  157. package/generated/v1/schemas/occurrences.read.response.json +76 -0
  158. package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
  159. package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
  160. package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
  161. package/generated/v1/schemas/projects.create.request.json +12 -0
  162. package/generated/v1/schemas/projects.create.response.json +25 -0
  163. package/generated/v1/schemas/projects.list.response.json +35 -0
  164. package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
  165. package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
  166. package/generated/v1/schemas/schedules.archive.response.json +189 -0
  167. package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
  168. package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
  169. package/generated/v1/schemas/schedules.cancel.response.json +189 -0
  170. package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
  171. package/generated/v1/schemas/schedules.create.request.json +99 -0
  172. package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
  173. package/generated/v1/schemas/schedules.create.response.json +189 -0
  174. package/generated/v1/schemas/schedules.list.query.json +16 -0
  175. package/generated/v1/schemas/schedules.list.response.json +170 -0
  176. package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
  177. package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
  178. package/generated/v1/schemas/schedules.pause.response.json +189 -0
  179. package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
  180. package/generated/v1/schemas/schedules.read.response.json +189 -0
  181. package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
  182. package/generated/v1/schemas/schedules.replace.request.json +99 -0
  183. package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
  184. package/generated/v1/schemas/schedules.replace.response.json +189 -0
  185. package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
  186. package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
  187. package/generated/v1/schemas/schedules.resume.response.json +189 -0
  188. package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
  189. package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
  190. package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
  191. package/generated/v1/schemas/testsinks.configure.request.json +66 -0
  192. package/generated/v1/schemas/testsinks.configure.response.json +115 -0
  193. package/generated/v1/schemas/testsinks.create.request.json +76 -0
  194. package/generated/v1/schemas/testsinks.create.response.json +115 -0
  195. package/generated/v1/schemas/testsinks.delete.response.json +115 -0
  196. package/generated/v1/schemas/testsinks.list.query.json +16 -0
  197. package/generated/v1/schemas/testsinks.list.response.json +161 -0
  198. package/generated/v1/schemas/testsinks.read.response.json +115 -0
  199. package/generated/v1/schemas/usage.read.query.json +15 -0
  200. package/generated/v1/schemas/usage.read.response.json +71 -0
  201. package/generated/v1/snippets/attempts.list.json +5 -0
  202. package/generated/v1/snippets/attempts.read.json +5 -0
  203. package/generated/v1/snippets/credentials.create.json +5 -0
  204. package/generated/v1/snippets/credentials.list.json +5 -0
  205. package/generated/v1/snippets/credentials.revoke.json +5 -0
  206. package/generated/v1/snippets/credentials.rotate.json +5 -0
  207. package/generated/v1/snippets/endpoints.create.json +5 -0
  208. package/generated/v1/snippets/endpoints.disable.json +5 -0
  209. package/generated/v1/snippets/endpoints.list.json +5 -0
  210. package/generated/v1/snippets/endpoints.read.json +5 -0
  211. package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
  212. package/generated/v1/snippets/endpoints.update.json +5 -0
  213. package/generated/v1/snippets/endpoints.verify.json +5 -0
  214. package/generated/v1/snippets/forecast.read.json +5 -0
  215. package/generated/v1/snippets/forecast.windows.json +5 -0
  216. package/generated/v1/snippets/grants.list.json +5 -0
  217. package/generated/v1/snippets/grants.revoke.json +5 -0
  218. package/generated/v1/snippets/meta.scaffold.json +5 -0
  219. package/generated/v1/snippets/meta.version.json +5 -0
  220. package/generated/v1/snippets/occurrences.list.json +5 -0
  221. package/generated/v1/snippets/occurrences.read.json +5 -0
  222. package/generated/v1/snippets/occurrences.redeliver.json +5 -0
  223. package/generated/v1/snippets/projects.create.json +5 -0
  224. package/generated/v1/snippets/projects.list.json +5 -0
  225. package/generated/v1/snippets/schedules.archive.json +5 -0
  226. package/generated/v1/snippets/schedules.cancel.json +5 -0
  227. package/generated/v1/snippets/schedules.create.json +5 -0
  228. package/generated/v1/snippets/schedules.list.json +5 -0
  229. package/generated/v1/snippets/schedules.pause.json +5 -0
  230. package/generated/v1/snippets/schedules.read.json +5 -0
  231. package/generated/v1/snippets/schedules.replace.json +5 -0
  232. package/generated/v1/snippets/schedules.resume.json +5 -0
  233. package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
  234. package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
  235. package/generated/v1/snippets/testsinks.configure.json +5 -0
  236. package/generated/v1/snippets/testsinks.create.json +5 -0
  237. package/generated/v1/snippets/testsinks.delete.json +5 -0
  238. package/generated/v1/snippets/testsinks.list.json +5 -0
  239. package/generated/v1/snippets/testsinks.read.json +5 -0
  240. package/generated/v1/snippets/usage.read.json +5 -0
  241. package/package.json +153 -0
package/dist/oauth.js ADDED
@@ -0,0 +1,708 @@
1
+ import { z } from "zod";
2
+ import { SCOPES, environmentKindSchema, scopeSchema, } from "./authority.js";
3
+ import { oauthClientIdSchema, oauthGrantIdSchema, projectIdSchema, } from "./identifiers.js";
4
+ import { timestampSchema } from "./time.js";
5
+ /**
6
+ * The delegated-authorization vocabulary: OAuth 2.1 as this product speaks it.
7
+ *
8
+ * Two things make this file worth reading before the routes that use it.
9
+ *
10
+ * **OAuth's wire shapes are not this product's wire shapes.** Everything on
11
+ * `/v1` answers `application/problem+json` with a `code` from `PROBLEM_CODES`;
12
+ * an OAuth endpoint answers `{"error": "invalid_grant"}` because RFC 6749 §5.2
13
+ * says so and every client in existence parses that. The two taxonomies are kept
14
+ * apart deliberately rather than reconciled — a client library that has to
15
+ * branch on which envelope it got back is a client library that will get it
16
+ * wrong. `OAUTH_ERRORS` below is the OAuth half, and it is closed for the same
17
+ * reason `PROBLEM_CODES` is.
18
+ *
19
+ * **The scope vocabulary is shared, and that is the whole point.** An OAuth
20
+ * grant carries `Scope` values from `authority.ts` — the same nine an API
21
+ * credential carries, capped by the same `capScopesToRole`. There is no
22
+ * OAuth-only scope and no OAuth-only operation, so a token cannot reach anything
23
+ * a key cannot, which is what architecture section 8 means by "API key and Clerk
24
+ * role mappings share one policy package".
25
+ */
26
+ /**
27
+ * Where each OAuth endpoint lives.
28
+ *
29
+ * Stated once because two readers must agree: the router that serves them and
30
+ * the discovery documents that advertise them. A client fetches the metadata and
31
+ * then uses exactly the URLs it found there, so a path that drifts from its
32
+ * advertisement is not a 404 in one place — it is a client that cannot
33
+ * authenticate and cannot say why.
34
+ *
35
+ * Outside `/v1` on purpose, following the convention `CLERK_WEBHOOK_PATH` set.
36
+ * The `/v1` surface is the product's operation registry: every entry is
37
+ * projected into OpenAPI, the SDK, an MCP tool and the contract mock, and W1-07
38
+ * enforces parity across all of them. None of that is true of an OAuth endpoint,
39
+ * whose shape is fixed by a standard rather than by us, and one of which
40
+ * (`/oauth/token`) must never become an agent-callable tool.
41
+ */
42
+ export const OAUTH_ROUTES = {
43
+ /** RFC 8414 authorization server metadata. */
44
+ authorizationServerMetadata: "/.well-known/oauth-authorization-server",
45
+ /** The browser-facing start of the flow. */
46
+ authorize: "/oauth/authorize",
47
+ /** What the consent screen reads to render itself. Session-authenticated. */
48
+ authorizationRequest: "/oauth/authorization-requests/:authorizationRequestId",
49
+ /** Where the consent screen posts approve or deny. Session-authenticated. */
50
+ consent: "/oauth/authorization-requests/:authorizationRequestId/consent",
51
+ // **No `grants` here since W4-08.** A member's list of connected
52
+ // applications is in no RFC and this file's own comment above called the
53
+ // consent endpoints "this product's own API" — so being outside `/v1` bought
54
+ // nothing and cost the published document, the generated client and an MCP
55
+ // disposition. `grants.list` and `grants.revoke` are operations now.
56
+ /** RFC 9728 protected resource metadata, which is how MCP clients start. */
57
+ protectedResourceMetadata: "/.well-known/oauth-protected-resource",
58
+ /** RFC 7591 dynamic client registration. */
59
+ register: "/oauth/register",
60
+ /** RFC 7009 token revocation. */
61
+ revoke: "/oauth/revoke",
62
+ /** RFC 6749 token endpoint. */
63
+ token: "/oauth/token",
64
+ };
65
+ /**
66
+ * Every error this authorization server emits, and nothing else.
67
+ *
68
+ * Closed rather than open because an `error` value is what a client branches on,
69
+ * exactly like a problem `code`. RFC 6749 §4.1.2.1 and §5.2 define most of
70
+ * these; `invalid_target` is RFC 8707's, for a `resource` naming something this
71
+ * server does not protect.
72
+ *
73
+ * Note what is *not* here: there is no error meaning "your membership was
74
+ * downgraded" and none meaning "that grant was revoked". Both are `invalid_grant`
75
+ * at the token endpoint, because RFC 6749 gives one code for "the grant is no
76
+ * longer usable" and inventing a second would tell a client which of the two
77
+ * happened — which is information about the account, delivered to whoever holds
78
+ * a dead token.
79
+ */
80
+ export const OAUTH_ERRORS = [
81
+ "access_denied",
82
+ "invalid_client",
83
+ "invalid_client_metadata",
84
+ "invalid_grant",
85
+ "invalid_redirect_uri",
86
+ "invalid_request",
87
+ "invalid_scope",
88
+ "invalid_target",
89
+ "server_error",
90
+ "temporarily_unavailable",
91
+ "unauthorized_client",
92
+ "unsupported_grant_type",
93
+ "unsupported_response_type",
94
+ ];
95
+ export const oauthErrorCodeSchema = z.enum(OAUTH_ERRORS);
96
+ /**
97
+ * The error body, RFC 6749 §5.2.
98
+ *
99
+ * `error_description` is human-readable and must stay that way: it is displayed
100
+ * to developers and never parsed. FXO-1368 is the record of what happens when
101
+ * a caller does parse it — reconciliation that matched a provider's
102
+ * human-readable string broke when the wording changed. Anything a client needs
103
+ * to act on belongs in `error`.
104
+ */
105
+ export const oauthErrorSchema = z.object({
106
+ error: oauthErrorCodeSchema,
107
+ error_description: z.string().min(1).optional(),
108
+ error_uri: z.url().optional(),
109
+ });
110
+ /**
111
+ * HTTP status for each error, so a route cannot pair a code with the wrong one.
112
+ *
113
+ * `invalid_client` is 401 because RFC 6749 §5.2 says so, and the difference is
114
+ * load-bearing: a client that cannot authenticate must not be told to fix its
115
+ * request. Everything else on the token endpoint is 400 — including
116
+ * `invalid_grant`, which is the code a revoked, replayed or expired grant
117
+ * produces, and which is deliberately *not* 401. A 401 invites a client to
118
+ * re-present the same dead credential.
119
+ */
120
+ export const OAUTH_ERROR_STATUS = {
121
+ access_denied: 400,
122
+ invalid_client: 401,
123
+ // RFC 7591 §3.2.2's registration errors, which share this envelope and are
124
+ // 400 like every other request fault. Kept in one catalogue rather than a
125
+ // second so a route cannot reach for a code from the wrong standard.
126
+ invalid_client_metadata: 400,
127
+ invalid_grant: 400,
128
+ invalid_redirect_uri: 400,
129
+ invalid_request: 400,
130
+ invalid_scope: 400,
131
+ invalid_target: 400,
132
+ server_error: 500,
133
+ temporarily_unavailable: 503,
134
+ unauthorized_client: 400,
135
+ unsupported_grant_type: 400,
136
+ unsupported_response_type: 400,
137
+ };
138
+ /**
139
+ * Build an error body. The status comes from the table above rather than from
140
+ * the caller, so the two cannot contradict each other — the same argument
141
+ * `problem()` makes for `PROBLEM_CODES`.
142
+ */
143
+ export function oauthError(error, description) {
144
+ return {
145
+ body: oauthErrorSchema.parse({
146
+ error,
147
+ ...(description === undefined ? {} : { error_description: description }),
148
+ }),
149
+ status: OAUTH_ERROR_STATUS[error],
150
+ };
151
+ }
152
+ /**
153
+ * The only grant types this server issues against.
154
+ *
155
+ * No implicit grant, no resource owner password credentials, no client
156
+ * credentials. The first two are removed by OAuth 2.1; the third would be a
157
+ * client acting with no human behind it, which is what an API credential already
158
+ * is and what `capScopesToRole` has no role to cap against.
159
+ */
160
+ export const OAUTH_GRANT_TYPES = [
161
+ "authorization_code",
162
+ "refresh_token",
163
+ ];
164
+ export const oauthGrantTypeSchema = z.enum(OAUTH_GRANT_TYPES);
165
+ /** Authorization code is the only response type. See above. */
166
+ export const OAUTH_RESPONSE_TYPES = ["code"];
167
+ /**
168
+ * PKCE challenge methods, and `plain` is not one.
169
+ *
170
+ * OAuth 2.1 removes it, and the reason is worth stating where the constant is:
171
+ * `plain` sends the verifier as the challenge, so anyone who can read the
172
+ * authorization request can complete the exchange — which is the entire attack
173
+ * PKCE exists to stop. A client asking for `plain` is refused rather than
174
+ * downgraded to it.
175
+ */
176
+ export const PKCE_METHODS = ["S256"];
177
+ export const pkceMethodSchema = z.enum(PKCE_METHODS);
178
+ /**
179
+ * RFC 7636 §4.1: 43–128 characters from an unreserved alphabet. The floor is
180
+ * what makes the verifier unguessable, so it is enforced rather than assumed.
181
+ */
182
+ export const PKCE_VERIFIER_PATTERN = /^[A-Za-z0-9\-._~]{43,128}$/u;
183
+ /** The base64url SHA-256 of a verifier: 43 characters, no padding. */
184
+ export const PKCE_CHALLENGE_PATTERN = /^[A-Za-z0-9\-_]{43}$/u;
185
+ /**
186
+ * Client authentication methods this server accepts, which is one: none.
187
+ *
188
+ * **Every client here is a public client.** An MCP client is a desktop
189
+ * application, a CLI, or a browser extension; it cannot keep a secret, and RFC
190
+ * 7591 registration hands a `client_secret` to whoever asked for one without
191
+ * ever authenticating them. Issuing a secret to an unauthenticated registrant
192
+ * and then treating it as proof of identity is a confidence trick played on
193
+ * ourselves.
194
+ *
195
+ * What replaces it is PKCE, which is mandatory here, plus exact redirect
196
+ * matching — together those bind an authorization code to the software instance
197
+ * that started the flow, which is the property a client secret is usually
198
+ * pressed into providing. A confidential-client flow can be added later without
199
+ * a contract break; adding it now would be surface with no consumer.
200
+ */
201
+ export const TOKEN_ENDPOINT_AUTH_METHODS = ["none"];
202
+ /**
203
+ * How long a member has to finish consenting before the request expires.
204
+ *
205
+ * Short because a pending authorization request is a live intent to grant
206
+ * access, and a long-lived one is something an attacker can hope a member
207
+ * approves absent-mindedly later. Ten minutes is enough to sign in to Clerk from
208
+ * a cold browser and read the screen.
209
+ */
210
+ export const AUTHORIZATION_REQUEST_TTL_SECONDS = 600;
211
+ /**
212
+ * How long an issued authorization code may be exchanged.
213
+ *
214
+ * RFC 6749 §4.1.2 recommends a maximum of ten minutes; this is deliberately much
215
+ * shorter, because the code travels through the user's browser — a redirect
216
+ * chain, an address bar, a browser history entry, and whatever referrer policy
217
+ * the client's own page carries. Sixty seconds is more than a token exchange
218
+ * needs and less than a leak is useful for.
219
+ */
220
+ export const AUTHORIZATION_CODE_TTL_SECONDS = 60;
221
+ /**
222
+ * How long an access token is valid.
223
+ *
224
+ * The number that decides how stale a revocation may be *in the worst case where
225
+ * the token check is skipped* — which, on this server, is never: every request
226
+ * loads the grant and re-checks membership, so a revoked grant fails on its next
227
+ * use rather than at the end of this window. The lifetime is short anyway,
228
+ * because a token this cheap to refresh has no reason to be long-lived.
229
+ */
230
+ export const ACCESS_TOKEN_TTL_SECONDS = 3600;
231
+ /**
232
+ * How long a refresh token is valid, absent rotation.
233
+ *
234
+ * Rotation resets it, so an actively used grant survives indefinitely and an
235
+ * abandoned one stops working after thirty days. That is the behaviour a member
236
+ * expects from "apps I have connected": the ones they use keep working, and the
237
+ * ones they forgot about do not accumulate forever.
238
+ */
239
+ export const REFRESH_TOKEN_TTL_SECONDS = 2_592_000;
240
+ /**
241
+ * Token shapes.
242
+ *
243
+ * The same prefix-plus-secret construction API credentials use, for the same
244
+ * reason: a presented token is found by its non-secret leading segment before
245
+ * anything is compared, so verification never scans. The environment is in the
246
+ * prefix because a token that cannot be told apart from a live one at a glance
247
+ * is one somebody will paste into the wrong place.
248
+ *
249
+ * Three separate prefixes rather than one, because they are three different
250
+ * things with three different lifetimes, and a store that accepted a refresh
251
+ * token where an access token belonged would be one type confusion away from
252
+ * making the short lifetime meaningless.
253
+ */
254
+ /** `oat_live_` and friends: four characters, the environment, two separators. */
255
+ export const OAUTH_TOKEN_NAMESPACE_LENGTH = 9;
256
+ /**
257
+ * Random characters after the namespace. Forty-eight of a 32-symbol alphabet is
258
+ * 240 bits — deliberately more than a credential's 160, because these are minted
259
+ * by machines at machine rates and nobody ever has to type one.
260
+ */
261
+ export const OAUTH_TOKEN_SECRET_LENGTH = 48;
262
+ export const ACCESS_TOKEN_PATTERN = /^oat_(?:live|test)_[0-9a-z]{48}$/u;
263
+ export const REFRESH_TOKEN_PATTERN = /^ort_(?:live|test)_[0-9a-z]{48}$/u;
264
+ export const AUTHORIZATION_CODE_PATTERN = /^oac_(?:live|test)_[0-9a-z]{48}$/u;
265
+ /** Any of the three, for the one place that has to tell ours from anything else. */
266
+ export const OAUTH_SECRET_PATTERN = /^o(?:at|rt|ac)_(?:live|test)_[0-9a-z]{48}$/u;
267
+ /**
268
+ * A redirect URI, validated the way RFC 8252 says a public client's must be.
269
+ *
270
+ * Three shapes are accepted and everything else is refused:
271
+ *
272
+ * - `https://` anywhere. The ordinary case.
273
+ * - `http://` on a loopback host only — `127.0.0.1`, `[::1]`, `localhost`. A
274
+ * native client receives its code on a listener it started itself, and there
275
+ * is no TLS to have. Refusing these would refuse every desktop MCP client.
276
+ * - a private-use scheme containing a dot, such as `com.example.app:/callback`.
277
+ * RFC 8252 §7.1's reverse-domain form, which is what a mobile or editor client
278
+ * registers.
279
+ *
280
+ * A fragment is refused outright: RFC 6749 §3.1.2 forbids one, and the fragment
281
+ * is where the server appends nothing and the browser strips everything, so a
282
+ * client that registered one would silently never see its code.
283
+ *
284
+ * Userinfo is refused because `https://evil@good.example` is read as
285
+ * `good.example` by a URL parser and as `evil` by a human skimming a consent
286
+ * screen — and the consent screen is the one place a human is asked to make this
287
+ * exact judgement.
288
+ */
289
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "[::1]", "::1", "localhost"]);
290
+ export const redirectUriSchema = z
291
+ .string()
292
+ .min(1)
293
+ .max(2048)
294
+ .superRefine((value, context) => {
295
+ let url;
296
+ try {
297
+ url = new URL(value);
298
+ }
299
+ catch {
300
+ context.addIssue({
301
+ code: "custom",
302
+ message: "must be an absolute URI",
303
+ });
304
+ return;
305
+ }
306
+ if (url.hash.length > 0) {
307
+ context.addIssue({
308
+ code: "custom",
309
+ message: "must not contain a fragment",
310
+ });
311
+ }
312
+ if (url.username.length > 0 || url.password.length > 0) {
313
+ context.addIssue({
314
+ code: "custom",
315
+ message: "must not contain userinfo",
316
+ });
317
+ }
318
+ if (url.protocol === "https:") {
319
+ return;
320
+ }
321
+ if (url.protocol === "http:") {
322
+ if (!LOOPBACK_HOSTS.has(url.hostname)) {
323
+ context.addIssue({
324
+ code: "custom",
325
+ message: "http is permitted only on a loopback host",
326
+ });
327
+ }
328
+ return;
329
+ }
330
+ // A private-use scheme. The dot is RFC 8252's own test for one, and it is
331
+ // what keeps `myapp:/callback` — a scheme anybody could claim — out.
332
+ if (!url.protocol.slice(0, -1).includes(".")) {
333
+ context.addIssue({
334
+ code: "custom",
335
+ message: "must be https, http on a loopback host, or a private-use scheme in reverse-domain form",
336
+ });
337
+ }
338
+ });
339
+ /** Whether a redirect URI names a loopback listener. Exported for the store. */
340
+ export function isLoopbackRedirect(value) {
341
+ try {
342
+ const url = new URL(value);
343
+ return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
344
+ }
345
+ catch {
346
+ return false;
347
+ }
348
+ }
349
+ /** The most redirect URIs one client may register. */
350
+ export const MAX_REDIRECT_URIS = 8;
351
+ /**
352
+ * RFC 7591 dynamic client registration.
353
+ *
354
+ * Registration is open — no credential is required to call it — and that is a
355
+ * deliberate, bounded decision rather than an oversight. An MCP client
356
+ * discovers this server, registers itself, and only then asks a human to
357
+ * approve it; requiring a credential first would mean every client had to be
358
+ * provisioned by hand before anyone could try it, which is the friction dynamic
359
+ * registration exists to remove.
360
+ *
361
+ * What makes it safe is that **a registered client holds no authority at all**.
362
+ * It cannot read, write, or name anything until a signed-in member approves a
363
+ * grant, and that grant is capped by the member's own role. The worst a
364
+ * registration flood achieves is rows in a table, which is a rate-limiting
365
+ * problem — W7-03's, and named there rather than solved here with an unmeasured
366
+ * limit.
367
+ *
368
+ * Unknown fields are dropped rather than refused. RFC 7591 §2 lets a client send
369
+ * metadata a server does not implement, and says the server returns what it
370
+ * actually registered — so a client sending `logo_uri` gets a registration back
371
+ * without one, not a 400.
372
+ */
373
+ /**
374
+ * A URL the consent screen may put in front of a human.
375
+ *
376
+ * `https` and nothing else. The scheme check is the whole guard: `javascript:`
377
+ * and `data:` are the ones that execute, and `http:` is the one an intermediary
378
+ * can rewrite — on the page where what the human sees *is* the decision.
379
+ */
380
+ export const httpsUriSchema = z
381
+ .url()
382
+ .max(2048)
383
+ .refine((value) => value.toLowerCase().startsWith("https://"), {
384
+ message: "must be an https URL",
385
+ });
386
+ export const clientRegistrationRequestSchema = z.object({
387
+ client_name: z.string().min(1).max(120),
388
+ client_uri: z.url().max(2048).optional(),
389
+ grant_types: z.array(oauthGrantTypeSchema).min(1).optional(),
390
+ /**
391
+ * RFC 7591 §2's presentation metadata, carried because the consent screen is
392
+ * where a human decides whether to trust the asking application.
393
+ *
394
+ * **`https` only, and enforced rather than described.** Both review lenses
395
+ * found this claiming the restriction in a comment while validating with a
396
+ * bare `z.url()`, which accepts `http:`, `javascript:` and `data:` — on
397
+ * fields a stranger sets through open registration and a human then reads
398
+ * while deciding whether to grant access. Nothing rendered them yet, which is
399
+ * the only reason it was not live cross-site scripting; the next story to
400
+ * wire them into an `href` would have inherited it, trusting the comment.
401
+ */
402
+ logo_uri: httpsUriSchema.optional(),
403
+ policy_uri: httpsUriSchema.optional(),
404
+ redirect_uris: z.array(redirectUriSchema).min(1).max(MAX_REDIRECT_URIS),
405
+ response_types: z.array(z.enum(OAUTH_RESPONSE_TYPES)).min(1).optional(),
406
+ scope: z.string().max(1024).optional(),
407
+ token_endpoint_auth_method: z.enum(TOKEN_ENDPOINT_AUTH_METHODS).optional(),
408
+ tos_uri: httpsUriSchema.optional(),
409
+ });
410
+ /**
411
+ * What registration answers with, RFC 7591 §3.2.1.
412
+ *
413
+ * No `client_secret`, and no `client_secret_expires_at` either — RFC 7591 says
414
+ * the latter is required *when a secret is issued*, and emitting it as `0`
415
+ * alongside no secret is how a client concludes it has a non-expiring one.
416
+ */
417
+ export const clientRegistrationSchema = z.object({
418
+ client_id: oauthClientIdSchema,
419
+ client_id_issued_at: z.number().int().positive(),
420
+ client_name: z.string(),
421
+ client_uri: z.string().optional(),
422
+ grant_types: z.array(oauthGrantTypeSchema),
423
+ logo_uri: z.string().optional(),
424
+ policy_uri: z.string().optional(),
425
+ redirect_uris: z.array(z.string()),
426
+ response_types: z.array(z.enum(OAUTH_RESPONSE_TYPES)),
427
+ scope: z.string(),
428
+ token_endpoint_auth_method: z.enum(TOKEN_ENDPOINT_AUTH_METHODS),
429
+ tos_uri: z.string().optional(),
430
+ });
431
+ /**
432
+ * Scopes on the wire are one space-delimited string, RFC 6749 §3.3.
433
+ *
434
+ * Parsing drops anything not in `SCOPES` rather than refusing the request. The
435
+ * choice matters and follows `capScopesToRole`: a client asking for a scope this
436
+ * server does not know is asking for less than it thought, and the consent
437
+ * screen shows what it will actually get. Refusing outright would break every
438
+ * client that sends a scope some other server understood.
439
+ *
440
+ * A request whose scopes *all* fall away is a different case and is refused as
441
+ * `invalid_scope` by the caller — asking for nothing this server can grant is a
442
+ * mistake worth reporting.
443
+ */
444
+ export function parseScopeString(value) {
445
+ if (value === undefined) {
446
+ return [];
447
+ }
448
+ const requested = new Set(value.split(/\s+/u).filter((entry) => entry.length > 0));
449
+ return SCOPES.filter((scope) => requested.has(scope));
450
+ }
451
+ /** The inverse. Ordered by `SCOPES` so one grant has exactly one spelling. */
452
+ export function formatScopeString(scopes) {
453
+ const held = new Set(scopes);
454
+ return SCOPES.filter((scope) => held.has(scope)).join(" ");
455
+ }
456
+ /**
457
+ * The authorization request, as it arrives on the query string.
458
+ *
459
+ * Everything is a string because a query string has no other type, and the
460
+ * coercions happen here rather than in the route. `state` is opaque and echoed
461
+ * back untouched — it is the client's CSRF defence and this server has no
462
+ * business interpreting it.
463
+ *
464
+ * `resource` is RFC 8707's, and MCP's authorization specification requires
465
+ * clients to send it. It is validated against this server's own resource
466
+ * identifier rather than merely recorded: a token minted for one audience and
467
+ * accepted by another is the confused-deputy problem RFC 8707 exists to close.
468
+ */
469
+ export const authorizationParametersSchema = z.object({
470
+ client_id: oauthClientIdSchema,
471
+ code_challenge: z.string().regex(PKCE_CHALLENGE_PATTERN),
472
+ code_challenge_method: pkceMethodSchema,
473
+ redirect_uri: z.string().min(1).max(2048),
474
+ resource: z.string().max(2048).optional(),
475
+ response_type: z.enum(OAUTH_RESPONSE_TYPES),
476
+ scope: z.string().max(1024).optional(),
477
+ state: z.string().max(512).optional(),
478
+ });
479
+ /**
480
+ * What the consent screen is given to render.
481
+ *
482
+ * It names the client, the scopes, and the projects the member may grant
483
+ * within — architecture section 8 requires consent to name project, environment
484
+ * and scopes, and a screen that cannot show them cannot obtain that consent.
485
+ *
486
+ * `scopes` here is already capped to the member's current role, so the screen
487
+ * never displays a permission that would not actually be granted. A member who
488
+ * asked for `credentials:write` and holds `member` sees the reduced list, which
489
+ * is the honest thing to show and is also what will be recorded.
490
+ */
491
+ export const consentPromptSchema = z.object({
492
+ client: z.object({
493
+ id: oauthClientIdSchema,
494
+ /**
495
+ * What the screen shows so a human can tell *who* is asking.
496
+ *
497
+ * All optional, and all attacker-controlled: a client registers itself and
498
+ * chooses its own name and logo. The screen must present them as claims
499
+ * rather than as endorsements — the value that is not attacker-controlled is
500
+ * `redirectUri`, which is where the access would actually go.
501
+ */
502
+ logoUri: z.string().optional(),
503
+ name: z.string(),
504
+ policyUri: z.string().optional(),
505
+ termsUri: z.string().optional(),
506
+ uri: z.string().optional(),
507
+ }),
508
+ /**
509
+ * The environments a grant may name. Both, always: a project owns one of each
510
+ * by construction, and the choice is the member's rather than the client's.
511
+ */
512
+ environmentKinds: z.array(environmentKindSchema).min(1),
513
+ expiresAt: timestampSchema,
514
+ /** Where the code will be delivered, so a human can check it. */
515
+ redirectUri: z.string(),
516
+ /**
517
+ * Active projects the member may grant within. Empty means the organization
518
+ * has no project yet, which the screen has to say rather than presenting an
519
+ * empty picker.
520
+ *
521
+ * A display aid rather than an authorization control: whatever the screen
522
+ * shows, the decision is re-resolved through `resolveAuthority`, so a project
523
+ * this list omitted — or one it should have — is decided there.
524
+ */
525
+ projects: z
526
+ .array(z.object({ id: projectIdSchema, name: z.string() }))
527
+ .max(200),
528
+ /** Scopes that would be granted: requested, capped by the member's role. */
529
+ scopes: z.array(scopeSchema),
530
+ });
531
+ /**
532
+ * The member's decision.
533
+ *
534
+ * `scopes` is the member's own narrowing — a consent screen that only offered
535
+ * all-or-nothing would train people to click through. It is intersected with
536
+ * what was requested and with the role ceiling, so it can only ever reduce.
537
+ *
538
+ * `project` and `environmentKind` are the *authority* the grant will carry, and
539
+ * they are chosen here rather than requested by the client. A client that could
540
+ * name a project would be selecting a tenant, which the `/v1` conventions forbid
541
+ * outright and which is no less forbidden because the request arrived through a
542
+ * browser.
543
+ */
544
+ export const consentDecisionSchema = z.discriminatedUnion("decision", [
545
+ z.object({
546
+ decision: z.literal("approve"),
547
+ environmentKind: environmentKindSchema,
548
+ project: projectIdSchema,
549
+ scopes: z.array(scopeSchema).min(1),
550
+ }),
551
+ z.object({ decision: z.literal("deny") }),
552
+ ]);
553
+ /**
554
+ * Where to send the browser next.
555
+ *
556
+ * Returned as a body rather than served as a 302, because the caller is the
557
+ * consent screen's `fetch` rather than the browser itself — a redirect answered
558
+ * to `fetch` would be followed by the client, delivering the code to the
559
+ * consent screen's own JavaScript instead of to the client's redirect URI.
560
+ */
561
+ export const consentOutcomeSchema = z.object({
562
+ redirectTo: z.string(),
563
+ });
564
+ /** RFC 6749 §5.1. `expires_in` is seconds, and relative, as the standard says. */
565
+ export const tokenResponseSchema = z.object({
566
+ access_token: z.string(),
567
+ expires_in: z.number().int().positive(),
568
+ refresh_token: z.string(),
569
+ scope: z.string(),
570
+ token_type: z.literal("Bearer"),
571
+ });
572
+ /**
573
+ * The token request, form-encoded per RFC 6749 §4.1.3 and §6.
574
+ *
575
+ * A discriminated union on `grant_type` so the two exchanges cannot borrow each
576
+ * other's fields: an authorization-code request without a `code_verifier` is
577
+ * refused by the shape rather than by a check somebody has to remember, which is
578
+ * what makes "PKCE is mandatory" a property of the contract.
579
+ */
580
+ export const tokenRequestSchema = z.discriminatedUnion("grant_type", [
581
+ z.object({
582
+ client_id: oauthClientIdSchema,
583
+ code: z.string().min(1).max(256),
584
+ code_verifier: z.string().regex(PKCE_VERIFIER_PATTERN),
585
+ grant_type: z.literal("authorization_code"),
586
+ redirect_uri: z.string().min(1).max(2048),
587
+ resource: z.string().max(2048).optional(),
588
+ }),
589
+ z.object({
590
+ client_id: oauthClientIdSchema,
591
+ grant_type: z.literal("refresh_token"),
592
+ refresh_token: z.string().min(1).max(256),
593
+ resource: z.string().max(2048).optional(),
594
+ scope: z.string().max(1024).optional(),
595
+ }),
596
+ ]);
597
+ /** RFC 7009 §2.1. The hint is advisory and this server ignores it. */
598
+ export const revocationRequestSchema = z.object({
599
+ client_id: oauthClientIdSchema,
600
+ token: z.string().min(1).max(256),
601
+ token_type_hint: z.enum(["access_token", "refresh_token"]).optional(),
602
+ });
603
+ /**
604
+ * What became of a grant.
605
+ *
606
+ * `revoked` is terminal and covers every way a grant dies — the member revoked
607
+ * it, a code or refresh token was replayed, or the client revoked its own token.
608
+ * There is no `expired` state: a grant whose refresh token lapsed is still a
609
+ * grant the member may want to see and revoke, and collapsing it into `revoked`
610
+ * would say somebody decided something when nobody did.
611
+ */
612
+ export const OAUTH_GRANT_STATUSES = ["active", "revoked"];
613
+ export const oauthGrantStatusSchema = z.enum(OAUTH_GRANT_STATUSES);
614
+ /**
615
+ * Why a grant became terminal.
616
+ *
617
+ * Recorded because "my integration stopped working" is a support question, and
618
+ * the four answers are genuinely different: somebody revoked it deliberately,
619
+ * the client revoked its own token, or one of the two replay defences fired.
620
+ * The last two are security events and the first two are not, and an audit trail
621
+ * that cannot tell them apart cannot answer either question.
622
+ */
623
+ export const OAUTH_REVOCATION_REASONS = [
624
+ "authorization-code-replayed",
625
+ "client-revoked",
626
+ "member-revoked",
627
+ "refresh-token-replayed",
628
+ ];
629
+ export const oauthRevocationReasonSchema = z.enum(OAUTH_REVOCATION_REASONS);
630
+ /**
631
+ * A grant as the member sees it. No token material, present or past.
632
+ *
633
+ * This is the shape architecture section 8's "user-visible grant revocation"
634
+ * needs: enough to recognise which application this is and decide whether to
635
+ * keep it, and nothing that would let a reader of the list use it.
636
+ */
637
+ export const oauthGrantSchema = z.object({
638
+ client: z.object({ id: oauthClientIdSchema, name: z.string() }),
639
+ createdAt: timestampSchema,
640
+ environmentKind: environmentKindSchema,
641
+ id: oauthGrantIdSchema,
642
+ lastUsedAt: timestampSchema.optional(),
643
+ project: projectIdSchema,
644
+ revocationReason: oauthRevocationReasonSchema.optional(),
645
+ revokedAt: timestampSchema.optional(),
646
+ scopes: z.array(scopeSchema).min(1),
647
+ status: oauthGrantStatusSchema,
648
+ });
649
+ export const oauthGrantListSchema = z.object({
650
+ data: z.array(oauthGrantSchema),
651
+ });
652
+ /**
653
+ * What disconnecting answers with.
654
+ *
655
+ * The grant rather than an empty body, for the reason `credentialRevokedSchema`
656
+ * gives: a member who clicked disconnect wants to see *which* connection stopped
657
+ * and when, and a 204 makes them ask for that separately.
658
+ */
659
+ export const oauthGrantRevokedSchema = z.object({ grant: oauthGrantSchema });
660
+ /**
661
+ * RFC 8414 authorization server metadata.
662
+ *
663
+ * Only the fields a client uses are advertised. Two are worth noting for what
664
+ * they refuse rather than what they offer: `code_challenge_methods_supported` is
665
+ * `["S256"]`, which tells a conforming client that `plain` will be rejected
666
+ * before it tries; and `grant_types_supported` omits everything OAuth 2.1
667
+ * removed, so a client cannot conclude from silence that implicit is available.
668
+ */
669
+ export const authorizationServerMetadataSchema = z.object({
670
+ authorization_endpoint: z.url(),
671
+ code_challenge_methods_supported: z.array(z.enum(PKCE_METHODS)).min(1),
672
+ grant_types_supported: z.array(oauthGrantTypeSchema).min(1),
673
+ issuer: z.url(),
674
+ registration_endpoint: z.url(),
675
+ response_modes_supported: z.array(z.literal("query")).min(1),
676
+ response_types_supported: z.array(z.enum(OAUTH_RESPONSE_TYPES)).min(1),
677
+ revocation_endpoint: z.url(),
678
+ scopes_supported: z.array(scopeSchema).min(1),
679
+ token_endpoint: z.url(),
680
+ token_endpoint_auth_methods_supported: z
681
+ .array(z.enum(TOKEN_ENDPOINT_AUTH_METHODS))
682
+ .min(1),
683
+ });
684
+ /**
685
+ * RFC 9728 protected resource metadata.
686
+ *
687
+ * This is where an MCP client's discovery actually begins: it calls a protected
688
+ * endpoint, receives 401 with `WWW-Authenticate: Bearer resource_metadata=…`,
689
+ * fetches this document, and learns which authorization server to talk to. A
690
+ * server that serves the RFC 8414 document but not this one is discoverable only
691
+ * by clients that already knew where to look.
692
+ */
693
+ export const protectedResourceMetadataSchema = z.object({
694
+ authorization_servers: z.array(z.url()).min(1),
695
+ bearer_methods_supported: z.array(z.literal("header")).min(1),
696
+ resource: z.url(),
697
+ scopes_supported: z.array(scopeSchema).min(1),
698
+ });
699
+ /**
700
+ * The `WWW-Authenticate` value a protected endpoint returns on 401.
701
+ *
702
+ * RFC 9728 §5.1. The `resource_metadata` parameter is the whole mechanism —
703
+ * without it a client that receives a 401 knows only that it is unauthenticated,
704
+ * and has nowhere to go next.
705
+ */
706
+ export function bearerChallenge(resourceMetadataUrl) {
707
+ return `Bearer resource_metadata="${resourceMetadataUrl}"`;
708
+ }