alepha 0.20.7 → 0.21.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 (230) hide show
  1. package/README.md +30 -19
  2. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  3. package/dist/api/audits/index.d.ts +2 -2
  4. package/dist/api/audits/index.d.ts.map +1 -1
  5. package/dist/api/files/index.browser.js +25 -0
  6. package/dist/api/files/index.browser.js.map +1 -1
  7. package/dist/api/files/index.d.ts +72 -2
  8. package/dist/api/files/index.d.ts.map +1 -1
  9. package/dist/api/files/index.js +82 -7
  10. package/dist/api/files/index.js.map +1 -1
  11. package/dist/api/jobs/index.d.ts +7 -5
  12. package/dist/api/jobs/index.d.ts.map +1 -1
  13. package/dist/api/jobs/index.js +4 -2
  14. package/dist/api/jobs/index.js.map +1 -1
  15. package/dist/api/keys/index.d.ts +3 -0
  16. package/dist/api/keys/index.d.ts.map +1 -1
  17. package/dist/api/keys/index.js +2 -0
  18. package/dist/api/keys/index.js.map +1 -1
  19. package/dist/api/oauth/index.d.ts +287 -0
  20. package/dist/api/oauth/index.d.ts.map +1 -0
  21. package/dist/api/oauth/index.js +578 -0
  22. package/dist/api/oauth/index.js.map +1 -0
  23. package/dist/api/organizations/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.d.ts +2 -2
  25. package/dist/api/parameters/index.d.ts.map +1 -1
  26. package/dist/api/parameters/index.js +2 -2
  27. package/dist/api/parameters/index.js.map +1 -1
  28. package/dist/api/payments/index.d.ts +2 -0
  29. package/dist/api/payments/index.d.ts.map +1 -1
  30. package/dist/api/payments/index.js +5 -4
  31. package/dist/api/payments/index.js.map +1 -1
  32. package/dist/api/subscriptions/index.d.ts.map +1 -1
  33. package/dist/api/users/index.browser.js +8 -0
  34. package/dist/api/users/index.browser.js.map +1 -1
  35. package/dist/api/users/index.d.ts +286 -5039
  36. package/dist/api/users/index.d.ts.map +1 -1
  37. package/dist/api/users/index.js +59 -8
  38. package/dist/api/users/index.js.map +1 -1
  39. package/dist/api/verifications/index.d.ts +2 -0
  40. package/dist/api/verifications/index.d.ts.map +1 -1
  41. package/dist/api/verifications/index.js +4 -2
  42. package/dist/api/verifications/index.js.map +1 -1
  43. package/dist/batch/index.d.ts +2 -0
  44. package/dist/batch/index.d.ts.map +1 -1
  45. package/dist/batch/index.js +3 -1
  46. package/dist/batch/index.js.map +1 -1
  47. package/dist/bucket/index.d.ts +5 -0
  48. package/dist/bucket/index.d.ts.map +1 -1
  49. package/dist/bucket/index.js +9 -5
  50. package/dist/bucket/index.js.map +1 -1
  51. package/dist/bucket/index.workerd.js +5 -3
  52. package/dist/bucket/index.workerd.js.map +1 -1
  53. package/dist/cli/core/index.d.ts +157 -18
  54. package/dist/cli/core/index.d.ts.map +1 -1
  55. package/dist/cli/core/index.js +309 -44
  56. package/dist/cli/core/index.js.map +1 -1
  57. package/dist/cli/devtools/index.d.ts.map +1 -1
  58. package/dist/cli/devtools/index.js +10 -6
  59. package/dist/cli/devtools/index.js.map +1 -1
  60. package/dist/cli/i18n/index.d.ts +136 -0
  61. package/dist/cli/i18n/index.d.ts.map +1 -0
  62. package/dist/cli/i18n/index.js +245 -0
  63. package/dist/cli/i18n/index.js.map +1 -0
  64. package/dist/cli/platform/index.d.ts +39 -1
  65. package/dist/cli/platform/index.d.ts.map +1 -1
  66. package/dist/cli/platform/index.js +123 -5
  67. package/dist/cli/platform/index.js.map +1 -1
  68. package/dist/cli/vendor/index.d.ts +34 -9
  69. package/dist/cli/vendor/index.d.ts.map +1 -1
  70. package/dist/cli/vendor/index.js +55 -28
  71. package/dist/cli/vendor/index.js.map +1 -1
  72. package/dist/core/index.d.ts +0 -5
  73. package/dist/core/index.d.ts.map +1 -1
  74. package/dist/core/index.js +0 -22
  75. package/dist/core/index.js.map +1 -1
  76. package/dist/crypto/index.browser.js +73 -0
  77. package/dist/crypto/index.browser.js.map +1 -1
  78. package/dist/crypto/index.d.ts +10 -0
  79. package/dist/crypto/index.d.ts.map +1 -1
  80. package/dist/crypto/index.js +60 -0
  81. package/dist/crypto/index.js.map +1 -1
  82. package/dist/mcp/index.d.ts +40 -0
  83. package/dist/mcp/index.d.ts.map +1 -1
  84. package/dist/mcp/index.js +33 -2
  85. package/dist/mcp/index.js.map +1 -1
  86. package/dist/orm/core/index.bun.js +1254 -1070
  87. package/dist/orm/core/index.bun.js.map +1 -1
  88. package/dist/orm/core/index.d.ts +1494 -6228
  89. package/dist/orm/core/index.d.ts.map +1 -1
  90. package/dist/orm/core/index.js +2174 -2011
  91. package/dist/orm/core/index.js.map +1 -1
  92. package/dist/orm/postgres/index.bun.js +13 -20
  93. package/dist/orm/postgres/index.bun.js.map +1 -1
  94. package/dist/react/form/index.d.ts +0 -28
  95. package/dist/react/form/index.d.ts.map +1 -1
  96. package/dist/react/form/index.js +18 -4
  97. package/dist/react/form/index.js.map +1 -1
  98. package/dist/react/head/index.browser.js +3 -0
  99. package/dist/react/head/index.browser.js.map +1 -1
  100. package/dist/react/head/index.d.ts +13 -0
  101. package/dist/react/head/index.d.ts.map +1 -1
  102. package/dist/react/head/index.js +3 -0
  103. package/dist/react/head/index.js.map +1 -1
  104. package/dist/react/router/index.d.ts.map +1 -1
  105. package/dist/react/router/index.js +3 -0
  106. package/dist/react/router/index.js.map +1 -1
  107. package/dist/react/testing/index.js +3 -3
  108. package/dist/react/testing/index.js.map +1 -1
  109. package/dist/redis/index.bun.js +2 -6
  110. package/dist/redis/index.bun.js.map +1 -1
  111. package/dist/security/index.d.ts +14 -1
  112. package/dist/security/index.d.ts.map +1 -1
  113. package/dist/security/index.js +11 -7
  114. package/dist/security/index.js.map +1 -1
  115. package/dist/server/core/index.d.ts +2 -1
  116. package/dist/server/core/index.d.ts.map +1 -1
  117. package/dist/server/core/index.js +3 -3
  118. package/dist/server/core/index.js.map +1 -1
  119. package/dist/server/links/index.browser.js +18 -2
  120. package/dist/server/links/index.browser.js.map +1 -1
  121. package/dist/server/links/index.d.ts +1 -0
  122. package/dist/server/links/index.d.ts.map +1 -1
  123. package/dist/server/links/index.js +18 -2
  124. package/dist/server/links/index.js.map +1 -1
  125. package/dist/system/index.d.ts +159 -128
  126. package/dist/system/index.d.ts.map +1 -1
  127. package/dist/system/index.js +249 -181
  128. package/dist/system/index.js.map +1 -1
  129. package/package.json +36 -15
  130. package/src/api/files/__tests__/FileAccessProvider.spec.ts +87 -0
  131. package/src/api/files/__tests__/FileController.spec.ts +4 -0
  132. package/src/api/files/controllers/FileController.ts +14 -4
  133. package/src/api/files/entities/files.ts +25 -0
  134. package/src/api/files/index.ts +9 -1
  135. package/src/api/files/providers/FileAccessProvider.ts +52 -0
  136. package/src/api/files/services/FileService.ts +2 -0
  137. package/src/api/jobs/providers/JobProvider.ts +4 -2
  138. package/src/api/keys/controllers/ApiKeyController.ts +1 -0
  139. package/src/api/keys/schemas/listApiKeyResponseSchema.ts +1 -0
  140. package/src/api/oauth/__tests__/OAuthClientService.spec.ts +123 -0
  141. package/src/api/oauth/__tests__/OAuthController.spec.ts +233 -0
  142. package/src/api/oauth/controllers/OAuthController.ts +275 -0
  143. package/src/api/oauth/entities/oauthClientEntity.ts +31 -0
  144. package/src/api/oauth/helpers/consentPage.ts +65 -0
  145. package/src/api/oauth/helpers/oauthMetadata.ts +29 -0
  146. package/src/api/oauth/index.ts +38 -0
  147. package/src/api/oauth/schemas/authorizeDecisionBodySchema.ts +18 -0
  148. package/src/api/oauth/schemas/authorizeQuerySchema.ts +15 -0
  149. package/src/api/oauth/schemas/registerClientBodySchema.ts +16 -0
  150. package/src/api/oauth/schemas/tokenRequestBodySchema.ts +16 -0
  151. package/src/api/oauth/services/OAuthClientService.ts +267 -0
  152. package/src/api/parameters/services/ParameterProvider.ts +2 -2
  153. package/src/api/payments/providers/MemoryPaymentProvider.ts +6 -4
  154. package/src/api/users/__tests__/ApiKeys.spec.ts +30 -0
  155. package/src/api/users/__tests__/realmOauth.spec.ts +52 -0
  156. package/src/api/users/atoms/realmAuthSettingsAtom.ts +7 -0
  157. package/src/api/users/entities/sessions.ts +8 -0
  158. package/src/api/users/primitives/$realm.ts +83 -2
  159. package/src/api/users/services/CredentialService.ts +1 -2
  160. package/src/api/users/services/RegistrationService.ts +8 -7
  161. package/src/api/users/services/SessionService.ts +2 -0
  162. package/src/api/verifications/services/VerificationService.ts +4 -2
  163. package/src/batch/providers/BatchProvider.ts +3 -1
  164. package/src/bucket/providers/CloudflareR2Provider.ts +3 -1
  165. package/src/bucket/providers/LocalFileStorageProvider.ts +3 -2
  166. package/src/bucket/providers/MemoryFileStorageProvider.ts +3 -2
  167. package/src/bucket/providers/NodeS3BucketProvider.ts +3 -1
  168. package/src/cli/core/__tests__/BuildDockerTask.spec.ts +306 -0
  169. package/src/cli/core/__tests__/init.spec.ts +28 -9
  170. package/src/cli/core/atoms/buildOptions.ts +45 -0
  171. package/src/cli/core/commands/build.ts +27 -0
  172. package/src/cli/core/commands/db.ts +4 -1
  173. package/src/cli/core/commands/init.ts +0 -3
  174. package/src/cli/core/commands/lint.ts +4 -8
  175. package/src/cli/core/commands/test.ts +21 -10
  176. package/src/cli/core/commands/typecheck.ts +4 -10
  177. package/src/cli/core/commands/verify.ts +4 -3
  178. package/src/cli/core/services/AlephaCliUtils.ts +57 -1
  179. package/src/cli/core/services/PackageManagerUtils.ts +8 -24
  180. package/src/cli/core/services/ProjectScaffolder.ts +41 -5
  181. package/src/cli/core/tasks/BuildDockerTask.ts +191 -14
  182. package/src/cli/core/templates/agentMd.ts +7 -0
  183. package/src/cli/core/templates/alephaConfigTs.ts +14 -0
  184. package/src/cli/core/templates/dummySpecTs.ts +15 -3
  185. package/src/cli/core/templates/vscodeSettingsJson.ts +21 -0
  186. package/src/cli/devtools/index.ts +10 -6
  187. package/src/cli/i18n/__tests__/I18nCheckService.spec.ts +128 -0
  188. package/src/cli/i18n/atoms/i18nOptions.ts +51 -0
  189. package/src/cli/i18n/commands/I18nCommand.ts +81 -0
  190. package/src/cli/i18n/index.ts +52 -0
  191. package/src/cli/i18n/services/I18nCheckService.ts +144 -0
  192. package/src/cli/platform/adapters/CloudflareAdapter.ts +123 -31
  193. package/src/cli/platform/commands/SecretsCommand.ts +8 -4
  194. package/src/cli/platform/schemas/cloudflare.ts +24 -0
  195. package/src/cli/platform/services/CloudflareApi.ts +44 -0
  196. package/src/cli/vendor/__tests__/VendorService.spec.ts +32 -14
  197. package/src/cli/vendor/atoms/vendorOptions.ts +11 -1
  198. package/src/cli/vendor/commands/VendorCommand.ts +15 -3
  199. package/src/cli/vendor/services/VendorService.ts +55 -19
  200. package/src/core/index.ts +0 -32
  201. package/src/core/interfaces/Run.ts +0 -5
  202. package/src/crypto/__tests__/BrowserCryptoProvider.browser.spec.ts +58 -0
  203. package/src/crypto/providers/BrowserCryptoProvider.ts +115 -0
  204. package/src/crypto/providers/CryptoProvider.ts +99 -0
  205. package/src/mcp/__tests__/StreamableHttpMcpTransport.spec.ts +117 -0
  206. package/src/mcp/transports/StreamableHttpMcpTransport.ts +48 -0
  207. package/src/orm/__tests__/$sequence.spec.ts +87 -2
  208. package/src/orm/core/entities/alephaSequences.ts +42 -0
  209. package/src/orm/core/index.bun.ts +21 -20
  210. package/src/orm/core/index.shared-server.ts +2 -0
  211. package/src/orm/core/index.ts +2 -0
  212. package/src/orm/core/primitives/$sequence.ts +68 -27
  213. package/src/orm/core/providers/SequenceProvider.ts +103 -0
  214. package/src/orm/core/providers/drivers/CloudflareD1Provider.ts +7 -2
  215. package/src/orm/core/services/Repository.ts +22 -1
  216. package/src/orm/postgres/index.bun.ts +5 -12
  217. package/src/react/form/__tests__/useForm.browser.spec.tsx +434 -0
  218. package/src/react/form/hooks/useForm.ts +32 -5
  219. package/src/react/head/interfaces/Head.ts +13 -0
  220. package/src/react/head/providers/BrowserHeadProvider.ts +9 -0
  221. package/src/react/router/providers/ReactServerTemplateProvider.ts +4 -0
  222. package/src/redis/index.bun.ts +2 -6
  223. package/src/security/primitives/$issuer.ts +14 -0
  224. package/src/security/providers/ServerSecurityProvider.ts +20 -19
  225. package/src/server/core/__tests__/BunHttpServerProvider.bun.spec.ts +6 -6
  226. package/src/server/core/providers/ServerRouterProvider.ts +5 -2
  227. package/src/server/links/services/BatchCollector.ts +36 -2
  228. package/src/system/__tests__/BunShellProvider.bun.spec.ts +74 -0
  229. package/src/system/index.ts +13 -4
  230. package/src/system/providers/BunShellProvider.ts +81 -0
@@ -0,0 +1,578 @@
1
+ import { $atom, $inject, $module, $state, Alepha, AlephaError, t } from "alepha";
2
+ import { $logger } from "alepha/logger";
3
+ import { $route } from "alepha/server";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { DateTimeProvider } from "alepha/datetime";
6
+ import { $entity, $repository, db } from "alepha/orm";
7
+ import { JwtProvider } from "alepha/security";
8
+ //#region ../../src/api/oauth/helpers/consentPage.ts
9
+ const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({
10
+ "&": "&amp;",
11
+ "<": "&lt;",
12
+ ">": "&gt;",
13
+ "\"": "&quot;",
14
+ "'": "&#39;"
15
+ })[c]);
16
+ const renderConsentPage = (options) => {
17
+ const hidden = Object.entries(options.hidden).map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}" />`).join("");
18
+ const scopes = options.scopes.length ? options.scopes.map((s) => `<li>${escapeHtml(s)}</li>`).join("") : "<li>Basic access</li>";
19
+ return `<!doctype html>
20
+ <html lang="en"><head><meta charset="utf-8" />
21
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
22
+ <title>Authorize ${escapeHtml(options.clientName)}</title>
23
+ <style>
24
+ body{font-family:system-ui,sans-serif;background:#0b0b0f;color:#e5e5e5;
25
+ display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}
26
+ .card{background:#16161d;border:1px solid #2a2a35;border-radius:12px;
27
+ padding:32px;max-width:380px;width:100%}
28
+ h1{font-size:18px;margin:0 0 4px}p{color:#9a9aa5;font-size:14px;margin:4px 0 16px}
29
+ ul{font-size:14px;padding-left:18px}
30
+ .row{display:flex;gap:8px;margin-top:20px}
31
+ button{flex:1;padding:10px;border-radius:8px;border:0;font-size:14px;cursor:pointer}
32
+ .allow{background:#6d5cf0;color:#fff}.deny{background:#2a2a35;color:#e5e5e5}
33
+ </style></head><body>
34
+ <div class="card">
35
+ <h1>${escapeHtml(options.clientName)} wants to connect</h1>
36
+ <p>Signed in as ${escapeHtml(options.userName)}</p>
37
+ <p>It will be able to:</p>
38
+ <ul>${scopes}</ul>
39
+ <form method="POST" action="/oauth/authorize">${hidden}
40
+ <div class="row">
41
+ <button class="deny" type="submit" name="decision" value="deny">Deny</button>
42
+ <button class="allow" type="submit" name="decision" value="allow">Allow</button>
43
+ </div></form></div></body></html>`;
44
+ };
45
+ //#endregion
46
+ //#region ../../src/api/oauth/helpers/oauthMetadata.ts
47
+ /**
48
+ * RFC 8414 — OAuth 2.0 Authorization Server Metadata.
49
+ * `baseUrl` is the absolute origin of the deployment (e.g. https://app.com).
50
+ */
51
+ const buildAuthorizationServerMetadata = (baseUrl) => ({
52
+ issuer: baseUrl,
53
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
54
+ token_endpoint: `${baseUrl}/oauth/token`,
55
+ registration_endpoint: `${baseUrl}/oauth/register`,
56
+ response_types_supported: ["code"],
57
+ grant_types_supported: ["authorization_code", "refresh_token"],
58
+ code_challenge_methods_supported: ["S256"],
59
+ token_endpoint_auth_methods_supported: ["none"],
60
+ scopes_supported: ["mcp"]
61
+ });
62
+ /**
63
+ * RFC 9728 — OAuth 2.0 Protected Resource Metadata.
64
+ * `resource` is the absolute URL of the MCP endpoint being protected.
65
+ */
66
+ const buildProtectedResourceMetadata = (baseUrl, resource) => ({
67
+ resource,
68
+ authorization_servers: [baseUrl],
69
+ scopes_supported: ["mcp"],
70
+ bearer_methods_supported: ["header"]
71
+ });
72
+ //#endregion
73
+ //#region ../../src/api/oauth/schemas/authorizeDecisionBodySchema.ts
74
+ /**
75
+ * Body posted by the consent screen. All authorization-request parameters
76
+ * are round-tripped through hidden form fields so the POST handler can
77
+ * re-validate them without server-side session state.
78
+ */
79
+ const authorizeDecisionBodySchema = t.object({
80
+ decision: t.text(),
81
+ response_type: t.text(),
82
+ client_id: t.text(),
83
+ redirect_uri: t.text({ maxLength: 2048 }),
84
+ code_challenge: t.text(),
85
+ code_challenge_method: t.text(),
86
+ scope: t.optional(t.text({ maxLength: 1024 })),
87
+ state: t.optional(t.text({ maxLength: 512 })),
88
+ resource: t.optional(t.text({ maxLength: 2048 }))
89
+ });
90
+ //#endregion
91
+ //#region ../../src/api/oauth/schemas/authorizeQuerySchema.ts
92
+ /**
93
+ * OAuth 2.1 authorization request query parameters (GET /oauth/authorize).
94
+ */
95
+ const authorizeQuerySchema = t.object({
96
+ response_type: t.text(),
97
+ client_id: t.text(),
98
+ redirect_uri: t.text({ maxLength: 2048 }),
99
+ code_challenge: t.text(),
100
+ code_challenge_method: t.text(),
101
+ scope: t.optional(t.text({ maxLength: 1024 })),
102
+ state: t.optional(t.text({ maxLength: 512 })),
103
+ resource: t.optional(t.text({ maxLength: 2048 }))
104
+ });
105
+ //#endregion
106
+ //#region ../../src/api/oauth/schemas/registerClientBodySchema.ts
107
+ /**
108
+ * RFC 7591 Dynamic Client Registration request body.
109
+ * Only the fields Alepha consumes are typed; unknown fields are ignored.
110
+ */
111
+ const registerClientBodySchema = t.object({
112
+ client_name: t.optional(t.text({ maxLength: 200 })),
113
+ redirect_uris: t.array(t.text({ maxLength: 2048 }), { minItems: 1 }),
114
+ scope: t.optional(t.text({ maxLength: 1024 })),
115
+ grant_types: t.optional(t.array(t.text())),
116
+ token_endpoint_auth_method: t.optional(t.text())
117
+ }, { additionalProperties: true });
118
+ //#endregion
119
+ //#region ../../src/api/oauth/schemas/tokenRequestBodySchema.ts
120
+ /**
121
+ * Body of a POST /oauth/token request. OAuth 2.1 mandates
122
+ * application/x-www-form-urlencoded encoding (section 3.2.2). All fields are
123
+ * optional at the schema level; the handler enforces the grant-specific
124
+ * requirements and returns the appropriate OAuth error responses.
125
+ */
126
+ const tokenRequestBodySchema = t.object({
127
+ grant_type: t.optional(t.text()),
128
+ code: t.optional(t.text({ maxLength: 4096 })),
129
+ client_id: t.optional(t.text()),
130
+ redirect_uri: t.optional(t.text({ maxLength: 2048 })),
131
+ code_verifier: t.optional(t.text({ maxLength: 256 })),
132
+ refresh_token: t.optional(t.text({ maxLength: 4096 }))
133
+ });
134
+ //#endregion
135
+ //#region ../../src/api/oauth/entities/oauthClientEntity.ts
136
+ /**
137
+ * A registered OAuth 2.1 client application.
138
+ *
139
+ * Rows are created by Dynamic Client Registration (RFC 7591) when an MCP
140
+ * client (e.g. Claude) first connects. `source` records who created the
141
+ * client; for DCR it is always `"dcr"` and `createdByUserId` is null until
142
+ * a user completes an authorization.
143
+ */
144
+ const oauthClientEntity = $entity({
145
+ name: "oauth_clients",
146
+ schema: t.object({
147
+ id: db.primaryKey(t.uuid()),
148
+ createdAt: db.createdAt(),
149
+ updatedAt: db.updatedAt(),
150
+ clientId: t.text({ maxLength: 64 }),
151
+ clientName: t.text({ maxLength: 200 }),
152
+ redirectUris: db.default(t.array(t.text({ maxLength: 2048 })), []),
153
+ scopes: db.default(t.array(t.text({ maxLength: 64 })), []),
154
+ realm: t.text({ maxLength: 100 }),
155
+ source: db.default(t.text({ maxLength: 16 }), "dcr"),
156
+ createdByUserId: t.optional(t.uuid()),
157
+ lastUsedAt: t.optional(t.datetime()),
158
+ revokedAt: t.optional(t.datetime())
159
+ }),
160
+ indexes: [{
161
+ columns: ["clientId"],
162
+ unique: true
163
+ }]
164
+ });
165
+ //#endregion
166
+ //#region ../../src/api/oauth/services/OAuthClientService.ts
167
+ /**
168
+ * Core OAuth 2.1 service backing the authorization server.
169
+ *
170
+ * Responsibilities:
171
+ * - Client registration (RFC 7591 Dynamic Client Registration) and lookup,
172
+ * with exact-match redirect_uri validation.
173
+ * - Stateless PKCE authorization codes: minting short-lived signed JWTs that
174
+ * carry the grant, and verifying/consuming them (replay, expiry, client and
175
+ * redirect_uri checks, S256 PKCE).
176
+ * - Realm issuer registry: realms register an issuer + user loader so the
177
+ * token endpoint can mint access tokens without depending on realm wiring.
178
+ */
179
+ var OAuthClientService = class {
180
+ alepha = $inject(Alepha);
181
+ dateTime = $inject(DateTimeProvider);
182
+ log = $logger();
183
+ repo = $repository(oauthClientEntity);
184
+ jwt = $inject(JwtProvider);
185
+ /**
186
+ * Codes already redeemed in this process. Single-use enforcement only
187
+ * needs to cover the ~60s code lifetime, so a bounded in-memory set is
188
+ * sufficient even on serverless — an expired code fails JWT verification
189
+ * regardless.
190
+ */
191
+ usedCodes = /* @__PURE__ */ new Set();
192
+ /**
193
+ * Registry of realm issuers used to mint access tokens. Populated by
194
+ * `$realm` (via `registerIssuer`) so the OAuth module does not depend on the
195
+ * realm wiring directly.
196
+ */
197
+ issuers = /* @__PURE__ */ new Map();
198
+ /**
199
+ * Register a realm issuer and a user loader. Called by `$realm` so the
200
+ * OAuth token endpoint can mint access tokens for that realm.
201
+ */
202
+ registerIssuer(realm, issuer, loadUser) {
203
+ this.issuers.set(realm, {
204
+ issuer,
205
+ loadUser
206
+ });
207
+ }
208
+ /**
209
+ * Mint an access token for a consumed authorization-code grant, using the
210
+ * issuer registered for `realm`. Throws if the realm has no issuer.
211
+ */
212
+ async issueAccessToken(realm, grant) {
213
+ const entry = this.issuers.get(realm);
214
+ if (!entry) throw new AlephaError(`No issuer registered for realm '${realm}'`);
215
+ const user = await entry.loadUser(grant.userId);
216
+ const tokens = await entry.issuer.createToken(user, void 0, { clientId: grant.clientId });
217
+ return {
218
+ access_token: tokens.access_token,
219
+ expires_in: tokens.expires_in,
220
+ refresh_token: tokens.refresh_token
221
+ };
222
+ }
223
+ /**
224
+ * Exchange a refresh token for a fresh access token (OAuth 2.1
225
+ * `refresh_token` grant), using the issuer registered for `realm`. Lets an
226
+ * MCP client stay connected for the refresh token's full lifetime without
227
+ * re-running the authorization flow. Throws if the realm has no issuer or
228
+ * the refresh token is invalid/expired.
229
+ */
230
+ async refreshAccessToken(realm, refreshToken) {
231
+ const entry = this.issuers.get(realm);
232
+ if (!entry) throw new AlephaError(`No issuer registered for realm '${realm}'`);
233
+ const { tokens } = await entry.issuer.refreshToken(refreshToken);
234
+ return {
235
+ access_token: tokens.access_token,
236
+ expires_in: tokens.expires_in,
237
+ refresh_token: tokens.refresh_token
238
+ };
239
+ }
240
+ /**
241
+ * Register a new OAuth client. Used by the RFC 7591 DCR endpoint and,
242
+ * later, by user/admin UIs (via the `source` field).
243
+ */
244
+ async register(options) {
245
+ if (options.redirectUris.length === 0) throw new AlephaError("At least one redirect_uri is required");
246
+ for (const uri of options.redirectUris) if (!uri.startsWith("https://") && !uri.startsWith("http://localhost")) throw new AlephaError(`Invalid redirect_uri: ${uri}`);
247
+ const clientId = `mcp_${randomUUID().replace(/-/g, "")}`;
248
+ const client = await this.repo.create({
249
+ clientId,
250
+ clientName: options.clientName || "MCP Client",
251
+ redirectUris: options.redirectUris,
252
+ scopes: options.scopes,
253
+ realm: options.realm,
254
+ source: options.source ?? "dcr",
255
+ createdByUserId: options.createdByUserId
256
+ });
257
+ this.log.info("OAuth client registered", {
258
+ clientId,
259
+ source: client.source
260
+ });
261
+ return client;
262
+ }
263
+ /**
264
+ * Look up a client by its public `clientId`. Returns null if unknown.
265
+ */
266
+ async findByClientId(clientId) {
267
+ return await this.repo.findOne({ where: { clientId: { eq: clientId } } }) ?? null;
268
+ }
269
+ /**
270
+ * Exact-match redirect_uri check. OAuth 2.1 forbids substring/prefix
271
+ * matching — the value must equal a registered URI byte-for-byte.
272
+ */
273
+ isRedirectUriAllowed(client, redirectUri) {
274
+ return client.redirectUris.includes(redirectUri);
275
+ }
276
+ /**
277
+ * Mint a stateless authorization code: a short-lived signed JWT
278
+ * (`typ: "oauth_code"`) carrying the grant. No server-side code storage.
279
+ */
280
+ async createAuthorizationCode(realm, grant) {
281
+ const iat = this.dateTime.now().unix();
282
+ return this.jwt.create({
283
+ sub: grant.userId,
284
+ client_id: grant.clientId,
285
+ redirect_uri: grant.redirectUri,
286
+ code_challenge: grant.codeChallenge,
287
+ scopes: grant.scopes,
288
+ resource: grant.resource,
289
+ iat,
290
+ exp: iat + 60,
291
+ jti: randomUUID()
292
+ }, realm, { header: { typ: "oauth_code" } });
293
+ }
294
+ /**
295
+ * Verify and atomically consume an authorization code. Throws on expiry,
296
+ * replay, client/redirect mismatch, or PKCE failure.
297
+ */
298
+ async consumeAuthorizationCode(realm, code, check) {
299
+ const { result } = await this.jwt.parse(code, realm, { typ: "oauth_code" });
300
+ const payload = result.payload;
301
+ const jti = payload.jti;
302
+ if (this.usedCodes.has(jti)) throw new AlephaError("Authorization code already used");
303
+ if (payload.client_id !== check.clientId) throw new AlephaError("client_id mismatch");
304
+ if (payload.redirect_uri !== check.redirectUri) throw new AlephaError("redirect_uri mismatch");
305
+ if (createHash("sha256").update(check.codeVerifier).digest("base64url") !== payload.code_challenge) throw new AlephaError("PKCE verification failed");
306
+ this.usedCodes.add(jti);
307
+ return {
308
+ userId: payload.sub,
309
+ scopes: payload.scopes ?? [],
310
+ resource: payload.resource
311
+ };
312
+ }
313
+ };
314
+ //#endregion
315
+ //#region ../../src/api/oauth/controllers/OAuthController.ts
316
+ /**
317
+ * Configuration for the OAuth authorization server.
318
+ * `realm` is the issuer realm whose JWTs are minted as access tokens;
319
+ * `resource` is the path of the protected MCP endpoint;
320
+ * `loginPath` is the app-level login page unauthenticated users are
321
+ * redirected to from the authorize endpoint.
322
+ */
323
+ const oauthOptions = $atom({
324
+ name: "alepha.api.oauth.options",
325
+ description: "Configuration for the OAuth authorization server.",
326
+ schema: t.object({
327
+ realm: t.text({ default: "users" }),
328
+ resource: t.text({ default: "/mcp" }),
329
+ loginPath: t.text({ default: "/login" })
330
+ }),
331
+ default: {
332
+ realm: "users",
333
+ resource: "/mcp",
334
+ loginPath: "/login"
335
+ }
336
+ });
337
+ /**
338
+ * OAuth 2.1 authorization server endpoints: discovery metadata and
339
+ * RFC 7591 dynamic client registration. Authorize/token routes are added
340
+ * separately.
341
+ */
342
+ var OAuthController = class {
343
+ log = $logger();
344
+ options = $state(oauthOptions);
345
+ clients = $inject(OAuthClientService);
346
+ /**
347
+ * Absolute origin of the current request, e.g. https://app.com.
348
+ */
349
+ baseUrl(url) {
350
+ return `${url.protocol}//${url.host}`;
351
+ }
352
+ metadata = $route({
353
+ method: "GET",
354
+ path: "/.well-known/oauth-authorization-server",
355
+ handler: ({ url, reply }) => {
356
+ reply.headers["content-type"] = "application/json";
357
+ reply.body = JSON.stringify(buildAuthorizationServerMetadata(this.baseUrl(url)));
358
+ }
359
+ });
360
+ protectedResource = $route({
361
+ method: "GET",
362
+ path: "/.well-known/oauth-protected-resource",
363
+ handler: ({ url, reply }) => {
364
+ const base = this.baseUrl(url);
365
+ reply.headers["content-type"] = "application/json";
366
+ reply.body = JSON.stringify(buildProtectedResourceMetadata(base, `${base}${this.options.resource}`));
367
+ }
368
+ });
369
+ register = $route({
370
+ method: "POST",
371
+ path: "/oauth/register",
372
+ schema: { body: registerClientBodySchema },
373
+ handler: async ({ body, reply }) => {
374
+ const client = await this.clients.register({
375
+ realm: this.options.realm,
376
+ clientName: body.client_name ?? "MCP Client",
377
+ redirectUris: body.redirect_uris,
378
+ scopes: body.scope ? body.scope.split(" ") : ["mcp"],
379
+ source: "dcr"
380
+ });
381
+ reply.status = 201;
382
+ reply.headers["content-type"] = "application/json";
383
+ reply.body = JSON.stringify({
384
+ client_id: client.clientId,
385
+ client_id_issued_at: Math.floor(new Date(client.createdAt).getTime() / 1e3),
386
+ client_name: client.clientName,
387
+ redirect_uris: client.redirectUris,
388
+ grant_types: ["authorization_code"],
389
+ token_endpoint_auth_method: "none"
390
+ });
391
+ }
392
+ });
393
+ /**
394
+ * GET /oauth/authorize — OAuth 2.1 authorization request. If the user
395
+ * has no session, redirect to the realm login page with a return URL.
396
+ * If authenticated, render the consent screen.
397
+ */
398
+ authorize = $route({
399
+ method: "GET",
400
+ path: "/oauth/authorize",
401
+ schema: { query: authorizeQuerySchema },
402
+ use: [],
403
+ handler: async ({ query, user, url, reply }) => {
404
+ if (query.response_type !== "code") {
405
+ reply.status = 400;
406
+ reply.body = "unsupported response_type";
407
+ return;
408
+ }
409
+ if (query.code_challenge_method !== "S256") {
410
+ reply.status = 400;
411
+ reply.body = "code_challenge_method must be S256";
412
+ return;
413
+ }
414
+ const client = await this.clients.findByClientId(query.client_id);
415
+ if (!client || client.revokedAt) {
416
+ reply.status = 400;
417
+ reply.body = "unknown client_id";
418
+ return;
419
+ }
420
+ if (!this.clients.isRedirectUriAllowed(client, query.redirect_uri)) {
421
+ reply.status = 400;
422
+ reply.body = "redirect_uri not registered";
423
+ return;
424
+ }
425
+ if (!user) {
426
+ const returnTo = encodeURIComponent(url.pathname + url.search);
427
+ reply.redirect(`${this.options.loginPath}?redirect_uri=${returnTo}`, 302);
428
+ return;
429
+ }
430
+ reply.headers["content-type"] = "text/html; charset=utf-8";
431
+ reply.body = renderConsentPage({
432
+ clientName: client.clientName,
433
+ userName: user.name ?? user.email ?? "your account",
434
+ scopes: query.scope ? query.scope.split(" ") : client.scopes,
435
+ hidden: {
436
+ response_type: query.response_type,
437
+ client_id: query.client_id,
438
+ redirect_uri: query.redirect_uri,
439
+ code_challenge: query.code_challenge,
440
+ code_challenge_method: query.code_challenge_method,
441
+ scope: query.scope ?? "",
442
+ state: query.state ?? "",
443
+ resource: query.resource ?? ""
444
+ }
445
+ });
446
+ }
447
+ });
448
+ /**
449
+ * POST /oauth/authorize — consent decision. On "allow", mint an
450
+ * authorization code and redirect back to the client's redirect_uri.
451
+ *
452
+ * CSRF: this route carries no CSRF token and relies solely on the session
453
+ * cookie to identify the user. This is a deliberate MVP/MCP tradeoff — a
454
+ * forged consent submit can still only issue an authorization code to an
455
+ * already-registered client, and that code is bound by PKCE, so the
456
+ * attacker cannot redeem it without the matching code_verifier.
457
+ */
458
+ authorizeDecision = $route({
459
+ method: "POST",
460
+ path: "/oauth/authorize",
461
+ schema: { body: authorizeDecisionBodySchema },
462
+ use: [],
463
+ handler: async ({ body, user, reply }) => {
464
+ if (!user) {
465
+ reply.status = 401;
466
+ reply.body = "authentication required";
467
+ return;
468
+ }
469
+ const client = await this.clients.findByClientId(body.client_id);
470
+ if (!client || client.revokedAt || !this.clients.isRedirectUriAllowed(client, body.redirect_uri)) {
471
+ reply.status = 400;
472
+ reply.body = "invalid client";
473
+ return;
474
+ }
475
+ const redirect = new URL(body.redirect_uri);
476
+ if (body.decision !== "allow") {
477
+ redirect.searchParams.set("error", "access_denied");
478
+ if (body.state) redirect.searchParams.set("state", body.state);
479
+ reply.redirect(redirect.toString(), 302);
480
+ return;
481
+ }
482
+ const code = await this.clients.createAuthorizationCode(this.options.realm, {
483
+ userId: user.id,
484
+ clientId: body.client_id,
485
+ redirectUri: body.redirect_uri,
486
+ codeChallenge: body.code_challenge,
487
+ scopes: body.scope ? body.scope.split(" ") : client.scopes,
488
+ resource: body.resource || void 0
489
+ });
490
+ redirect.searchParams.set("code", code);
491
+ if (body.state) redirect.searchParams.set("state", body.state);
492
+ reply.redirect(redirect.toString(), 302);
493
+ }
494
+ });
495
+ /**
496
+ * POST /oauth/token — supports the `authorization_code` grant (verifies
497
+ * PKCE, mints an access token via the realm issuer) and the
498
+ * `refresh_token` grant (exchanges a refresh token for a fresh access
499
+ * token, so a client stays connected without re-running the flow).
500
+ */
501
+ token = $route({
502
+ method: "POST",
503
+ path: "/oauth/token",
504
+ schema: { body: tokenRequestBodySchema },
505
+ use: [],
506
+ handler: async ({ body, reply }) => {
507
+ reply.headers["content-type"] = "application/json";
508
+ try {
509
+ if (body.grant_type === "authorization_code") {
510
+ const grant = await this.clients.consumeAuthorizationCode(this.options.realm, body.code ?? "", {
511
+ clientId: body.client_id ?? "",
512
+ redirectUri: body.redirect_uri ?? "",
513
+ codeVerifier: body.code_verifier ?? ""
514
+ });
515
+ const tokens = await this.clients.issueAccessToken(this.options.realm, {
516
+ ...grant,
517
+ clientId: body.client_id ?? ""
518
+ });
519
+ reply.body = JSON.stringify({
520
+ access_token: tokens.access_token,
521
+ token_type: "Bearer",
522
+ expires_in: tokens.expires_in,
523
+ refresh_token: tokens.refresh_token,
524
+ scope: grant.scopes.join(" ")
525
+ });
526
+ return;
527
+ }
528
+ if (body.grant_type === "refresh_token") {
529
+ const tokens = await this.clients.refreshAccessToken(this.options.realm, body.refresh_token ?? "");
530
+ reply.body = JSON.stringify({
531
+ access_token: tokens.access_token,
532
+ token_type: "Bearer",
533
+ expires_in: tokens.expires_in,
534
+ refresh_token: tokens.refresh_token
535
+ });
536
+ return;
537
+ }
538
+ reply.status = 400;
539
+ reply.body = JSON.stringify({ error: "unsupported_grant_type" });
540
+ } catch (e) {
541
+ this.log.warn("OAuth token exchange failed", e);
542
+ reply.status = 400;
543
+ reply.body = JSON.stringify({ error: "invalid_grant" });
544
+ }
545
+ }
546
+ });
547
+ };
548
+ //#endregion
549
+ //#region ../../src/api/oauth/index.ts
550
+ /**
551
+ * OAuth 2.1 authorization server module for MCP.
552
+ *
553
+ * **Features:**
554
+ * - OAuth 2.1 authorization code flow with PKCE (RFC 7636)
555
+ * - Dynamic Client Registration (RFC 7591)
556
+ * - Authorization server metadata discovery (RFC 8414)
557
+ * - Stateless authorization codes (short-lived signed JWTs)
558
+ * - Single-use code enforcement
559
+ *
560
+ * **Integration:**
561
+ * Register the module and configure the realm + protected resource path:
562
+ *
563
+ * ```ts
564
+ * const app = Alepha.create()
565
+ * .with(AlephaOAuth)
566
+ * .set(oauthOptions, { realm: "users", resource: "/mcp" });
567
+ * ```
568
+ *
569
+ * @module alepha.api.oauth
570
+ */
571
+ const AlephaOAuth = $module({
572
+ name: "alepha.api.oauth",
573
+ services: [OAuthClientService, OAuthController]
574
+ });
575
+ //#endregion
576
+ export { AlephaOAuth, OAuthClientService, OAuthController, oauthClientEntity, oauthOptions };
577
+
578
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/api/oauth/helpers/consentPage.ts","../../../src/api/oauth/helpers/oauthMetadata.ts","../../../src/api/oauth/schemas/authorizeDecisionBodySchema.ts","../../../src/api/oauth/schemas/authorizeQuerySchema.ts","../../../src/api/oauth/schemas/registerClientBodySchema.ts","../../../src/api/oauth/schemas/tokenRequestBodySchema.ts","../../../src/api/oauth/entities/oauthClientEntity.ts","../../../src/api/oauth/services/OAuthClientService.ts","../../../src/api/oauth/controllers/OAuthController.ts","../../../src/api/oauth/index.ts"],"sourcesContent":["/**\n * Renders the OAuth consent screen as a self-contained HTML document.\n * Pure server-rendered HTML — no client framework, no @alepha/ui dependency.\n * All authorization parameters are emitted as hidden inputs so the POST to\n * /oauth/authorize is stateless.\n */\nexport interface ConsentPageOptions {\n clientName: string;\n userName: string;\n scopes: string[];\n /**\n * Hidden field name -> value; round-trips the authorization request.\n */\n hidden: Record<string, string>;\n}\n\nconst escapeHtml = (s: string): string =>\n s.replace(\n /[&<>\"']/g,\n (c) =>\n ({\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#39;\",\n })[c] as string,\n );\n\nexport const renderConsentPage = (options: ConsentPageOptions): string => {\n const hidden = Object.entries(options.hidden)\n .map(\n ([k, v]) =>\n `<input type=\"hidden\" name=\"${escapeHtml(k)}\" value=\"${escapeHtml(v)}\" />`,\n )\n .join(\"\");\n const scopes = options.scopes.length\n ? options.scopes.map((s) => `<li>${escapeHtml(s)}</li>`).join(\"\")\n : \"<li>Basic access</li>\";\n return `<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<title>Authorize ${escapeHtml(options.clientName)}</title>\n<style>\nbody{font-family:system-ui,sans-serif;background:#0b0b0f;color:#e5e5e5;\ndisplay:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}\n.card{background:#16161d;border:1px solid #2a2a35;border-radius:12px;\npadding:32px;max-width:380px;width:100%}\nh1{font-size:18px;margin:0 0 4px}p{color:#9a9aa5;font-size:14px;margin:4px 0 16px}\nul{font-size:14px;padding-left:18px}\n.row{display:flex;gap:8px;margin-top:20px}\nbutton{flex:1;padding:10px;border-radius:8px;border:0;font-size:14px;cursor:pointer}\n.allow{background:#6d5cf0;color:#fff}.deny{background:#2a2a35;color:#e5e5e5}\n</style></head><body>\n<div class=\"card\">\n<h1>${escapeHtml(options.clientName)} wants to connect</h1>\n<p>Signed in as ${escapeHtml(options.userName)}</p>\n<p>It will be able to:</p>\n<ul>${scopes}</ul>\n<form method=\"POST\" action=\"/oauth/authorize\">${hidden}\n<div class=\"row\">\n<button class=\"deny\" type=\"submit\" name=\"decision\" value=\"deny\">Deny</button>\n<button class=\"allow\" type=\"submit\" name=\"decision\" value=\"allow\">Allow</button>\n</div></form></div></body></html>`;\n};\n","/**\n * RFC 8414 — OAuth 2.0 Authorization Server Metadata.\n * `baseUrl` is the absolute origin of the deployment (e.g. https://app.com).\n */\nexport const buildAuthorizationServerMetadata = (baseUrl: string) => ({\n issuer: baseUrl,\n authorization_endpoint: `${baseUrl}/oauth/authorize`,\n token_endpoint: `${baseUrl}/oauth/token`,\n registration_endpoint: `${baseUrl}/oauth/register`,\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"],\n scopes_supported: [\"mcp\"],\n});\n\n/**\n * RFC 9728 — OAuth 2.0 Protected Resource Metadata.\n * `resource` is the absolute URL of the MCP endpoint being protected.\n */\nexport const buildProtectedResourceMetadata = (\n baseUrl: string,\n resource: string,\n) => ({\n resource,\n authorization_servers: [baseUrl],\n scopes_supported: [\"mcp\"],\n bearer_methods_supported: [\"header\"],\n});\n","import { t } from \"alepha\";\n\n/**\n * Body posted by the consent screen. All authorization-request parameters\n * are round-tripped through hidden form fields so the POST handler can\n * re-validate them without server-side session state.\n */\nexport const authorizeDecisionBodySchema = t.object({\n decision: t.text(),\n response_type: t.text(),\n client_id: t.text(),\n redirect_uri: t.text({ maxLength: 2048 }),\n code_challenge: t.text(),\n code_challenge_method: t.text(),\n scope: t.optional(t.text({ maxLength: 1024 })),\n state: t.optional(t.text({ maxLength: 512 })),\n resource: t.optional(t.text({ maxLength: 2048 })),\n});\n","import { t } from \"alepha\";\n\n/**\n * OAuth 2.1 authorization request query parameters (GET /oauth/authorize).\n */\nexport const authorizeQuerySchema = t.object({\n response_type: t.text(),\n client_id: t.text(),\n redirect_uri: t.text({ maxLength: 2048 }),\n code_challenge: t.text(),\n code_challenge_method: t.text(),\n scope: t.optional(t.text({ maxLength: 1024 })),\n state: t.optional(t.text({ maxLength: 512 })),\n resource: t.optional(t.text({ maxLength: 2048 })),\n});\n","import { t } from \"alepha\";\n\n/**\n * RFC 7591 Dynamic Client Registration request body.\n * Only the fields Alepha consumes are typed; unknown fields are ignored.\n */\nexport const registerClientBodySchema = t.object(\n {\n client_name: t.optional(t.text({ maxLength: 200 })),\n redirect_uris: t.array(t.text({ maxLength: 2048 }), { minItems: 1 }),\n scope: t.optional(t.text({ maxLength: 1024 })),\n grant_types: t.optional(t.array(t.text())),\n token_endpoint_auth_method: t.optional(t.text()),\n },\n { additionalProperties: true },\n);\n","import { t } from \"alepha\";\n\n/**\n * Body of a POST /oauth/token request. OAuth 2.1 mandates\n * application/x-www-form-urlencoded encoding (section 3.2.2). All fields are\n * optional at the schema level; the handler enforces the grant-specific\n * requirements and returns the appropriate OAuth error responses.\n */\nexport const tokenRequestBodySchema = t.object({\n grant_type: t.optional(t.text()),\n code: t.optional(t.text({ maxLength: 4096 })),\n client_id: t.optional(t.text()),\n redirect_uri: t.optional(t.text({ maxLength: 2048 })),\n code_verifier: t.optional(t.text({ maxLength: 256 })),\n refresh_token: t.optional(t.text({ maxLength: 4096 })),\n});\n","import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\n/**\n * A registered OAuth 2.1 client application.\n *\n * Rows are created by Dynamic Client Registration (RFC 7591) when an MCP\n * client (e.g. Claude) first connects. `source` records who created the\n * client; for DCR it is always `\"dcr\"` and `createdByUserId` is null until\n * a user completes an authorization.\n */\nexport const oauthClientEntity = $entity({\n name: \"oauth_clients\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n createdAt: db.createdAt(),\n updatedAt: db.updatedAt(),\n clientId: t.text({ maxLength: 64 }),\n clientName: t.text({ maxLength: 200 }),\n redirectUris: db.default(t.array(t.text({ maxLength: 2048 })), []),\n scopes: db.default(t.array(t.text({ maxLength: 64 })), []),\n realm: t.text({ maxLength: 100 }),\n source: db.default(t.text({ maxLength: 16 }), \"dcr\"),\n createdByUserId: t.optional(t.uuid()),\n lastUsedAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n }),\n indexes: [{ columns: [\"clientId\"], unique: true }],\n});\n\nexport type OAuthClientEntity = Static<typeof oauthClientEntity.schema>;\n","import { createHash, randomUUID } from \"node:crypto\";\nimport { $inject, Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport {\n type IssuerPrimitive,\n JwtProvider,\n type UserAccount,\n} from \"alepha/security\";\nimport {\n type OAuthClientEntity,\n oauthClientEntity,\n} from \"../entities/oauthClientEntity.ts\";\n\nexport interface RegisterClientOptions {\n realm: string;\n clientName: string;\n redirectUris: string[];\n scopes: string[];\n source?: \"dcr\" | \"user\" | \"admin\";\n createdByUserId?: string;\n}\n\n/**\n * Core OAuth 2.1 service backing the authorization server.\n *\n * Responsibilities:\n * - Client registration (RFC 7591 Dynamic Client Registration) and lookup,\n * with exact-match redirect_uri validation.\n * - Stateless PKCE authorization codes: minting short-lived signed JWTs that\n * carry the grant, and verifying/consuming them (replay, expiry, client and\n * redirect_uri checks, S256 PKCE).\n * - Realm issuer registry: realms register an issuer + user loader so the\n * token endpoint can mint access tokens without depending on realm wiring.\n */\nexport class OAuthClientService {\n protected readonly alepha = $inject(Alepha);\n protected readonly dateTime = $inject(DateTimeProvider);\n protected readonly log = $logger();\n protected readonly repo = $repository(oauthClientEntity);\n protected readonly jwt = $inject(JwtProvider);\n\n /**\n * Codes already redeemed in this process. Single-use enforcement only\n * needs to cover the ~60s code lifetime, so a bounded in-memory set is\n * sufficient even on serverless — an expired code fails JWT verification\n * regardless.\n */\n protected readonly usedCodes = new Set<string>();\n\n /**\n * Registry of realm issuers used to mint access tokens. Populated by\n * `$realm` (via `registerIssuer`) so the OAuth module does not depend on the\n * realm wiring directly.\n */\n protected readonly issuers = new Map<\n string,\n {\n issuer: IssuerPrimitive;\n loadUser: (userId: string) => Promise<UserAccount>;\n }\n >();\n\n /**\n * Register a realm issuer and a user loader. Called by `$realm` so the\n * OAuth token endpoint can mint access tokens for that realm.\n */\n public registerIssuer(\n realm: string,\n issuer: IssuerPrimitive,\n loadUser: (userId: string) => Promise<UserAccount>,\n ): void {\n this.issuers.set(realm, { issuer, loadUser });\n }\n\n /**\n * Mint an access token for a consumed authorization-code grant, using the\n * issuer registered for `realm`. Throws if the realm has no issuer.\n */\n public async issueAccessToken(\n realm: string,\n grant: {\n userId: string;\n scopes: string[];\n resource?: string;\n clientId?: string;\n },\n ): Promise<{\n access_token: string;\n expires_in?: number;\n refresh_token?: string;\n }> {\n const entry = this.issuers.get(realm);\n if (!entry) {\n throw new AlephaError(`No issuer registered for realm '${realm}'`);\n }\n const user = await entry.loadUser(grant.userId);\n // Tag the session the issuer creates with the OAuth client, so it can\n // later be surfaced as a \"connected app\" and revoked individually.\n const tokens = await entry.issuer.createToken(user, undefined, {\n clientId: grant.clientId,\n });\n return {\n access_token: tokens.access_token,\n expires_in: tokens.expires_in,\n refresh_token: tokens.refresh_token,\n };\n }\n\n /**\n * Exchange a refresh token for a fresh access token (OAuth 2.1\n * `refresh_token` grant), using the issuer registered for `realm`. Lets an\n * MCP client stay connected for the refresh token's full lifetime without\n * re-running the authorization flow. Throws if the realm has no issuer or\n * the refresh token is invalid/expired.\n */\n public async refreshAccessToken(\n realm: string,\n refreshToken: string,\n ): Promise<{\n access_token: string;\n expires_in?: number;\n refresh_token?: string;\n }> {\n const entry = this.issuers.get(realm);\n if (!entry) {\n throw new AlephaError(`No issuer registered for realm '${realm}'`);\n }\n const { tokens } = await entry.issuer.refreshToken(refreshToken);\n return {\n access_token: tokens.access_token,\n expires_in: tokens.expires_in,\n refresh_token: tokens.refresh_token,\n };\n }\n\n /**\n * Register a new OAuth client. Used by the RFC 7591 DCR endpoint and,\n * later, by user/admin UIs (via the `source` field).\n */\n public async register(\n options: RegisterClientOptions,\n ): Promise<OAuthClientEntity> {\n if (options.redirectUris.length === 0) {\n throw new AlephaError(\"At least one redirect_uri is required\");\n }\n for (const uri of options.redirectUris) {\n if (!uri.startsWith(\"https://\") && !uri.startsWith(\"http://localhost\")) {\n throw new AlephaError(`Invalid redirect_uri: ${uri}`);\n }\n }\n\n const clientId = `mcp_${randomUUID().replace(/-/g, \"\")}`;\n const client = await this.repo.create({\n clientId,\n clientName: options.clientName || \"MCP Client\",\n redirectUris: options.redirectUris,\n scopes: options.scopes,\n realm: options.realm,\n source: options.source ?? \"dcr\",\n createdByUserId: options.createdByUserId,\n });\n\n this.log.info(\"OAuth client registered\", {\n clientId,\n source: client.source,\n });\n return client;\n }\n\n /**\n * Look up a client by its public `clientId`. Returns null if unknown.\n */\n public async findByClientId(\n clientId: string,\n ): Promise<OAuthClientEntity | null> {\n return (\n (await this.repo.findOne({ where: { clientId: { eq: clientId } } })) ??\n null\n );\n }\n\n /**\n * Exact-match redirect_uri check. OAuth 2.1 forbids substring/prefix\n * matching — the value must equal a registered URI byte-for-byte.\n */\n public isRedirectUriAllowed(\n client: OAuthClientEntity,\n redirectUri: string,\n ): boolean {\n return client.redirectUris.includes(redirectUri);\n }\n\n /**\n * Mint a stateless authorization code: a short-lived signed JWT\n * (`typ: \"oauth_code\"`) carrying the grant. No server-side code storage.\n */\n public async createAuthorizationCode(\n realm: string,\n grant: {\n userId: string;\n clientId: string;\n redirectUri: string;\n codeChallenge: string;\n scopes: string[];\n resource?: string;\n },\n ): Promise<string> {\n const iat = this.dateTime.now().unix();\n return this.jwt.create(\n {\n sub: grant.userId,\n client_id: grant.clientId,\n redirect_uri: grant.redirectUri,\n code_challenge: grant.codeChallenge,\n scopes: grant.scopes,\n resource: grant.resource,\n iat,\n exp: iat + 60,\n jti: randomUUID(),\n },\n realm,\n { header: { typ: \"oauth_code\" } },\n );\n }\n\n /**\n * Verify and atomically consume an authorization code. Throws on expiry,\n * replay, client/redirect mismatch, or PKCE failure.\n */\n public async consumeAuthorizationCode(\n realm: string,\n code: string,\n check: { clientId: string; redirectUri: string; codeVerifier: string },\n ): Promise<{ userId: string; scopes: string[]; resource?: string }> {\n const { result } = await this.jwt.parse(code, realm, {\n typ: \"oauth_code\",\n });\n const payload = result.payload as Record<string, unknown>;\n\n const jti = payload.jti as string;\n if (this.usedCodes.has(jti)) {\n throw new AlephaError(\"Authorization code already used\");\n }\n if (payload.client_id !== check.clientId) {\n throw new AlephaError(\"client_id mismatch\");\n }\n if (payload.redirect_uri !== check.redirectUri) {\n throw new AlephaError(\"redirect_uri mismatch\");\n }\n\n const computed = createHash(\"sha256\")\n .update(check.codeVerifier)\n .digest(\"base64url\");\n if (computed !== payload.code_challenge) {\n throw new AlephaError(\"PKCE verification failed\");\n }\n\n this.usedCodes.add(jti);\n return {\n userId: payload.sub as string,\n scopes: (payload.scopes as string[]) ?? [],\n resource: payload.resource as string | undefined,\n };\n }\n}\n","import { $atom, $inject, $state, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { $route } from \"alepha/server\";\nimport { renderConsentPage } from \"../helpers/consentPage.ts\";\nimport {\n buildAuthorizationServerMetadata,\n buildProtectedResourceMetadata,\n} from \"../helpers/oauthMetadata.ts\";\nimport { authorizeDecisionBodySchema } from \"../schemas/authorizeDecisionBodySchema.ts\";\nimport { authorizeQuerySchema } from \"../schemas/authorizeQuerySchema.ts\";\nimport { registerClientBodySchema } from \"../schemas/registerClientBodySchema.ts\";\nimport { tokenRequestBodySchema } from \"../schemas/tokenRequestBodySchema.ts\";\nimport { OAuthClientService } from \"../services/OAuthClientService.ts\";\n\n/**\n * Configuration for the OAuth authorization server.\n * `realm` is the issuer realm whose JWTs are minted as access tokens;\n * `resource` is the path of the protected MCP endpoint;\n * `loginPath` is the app-level login page unauthenticated users are\n * redirected to from the authorize endpoint.\n */\nexport const oauthOptions = $atom({\n name: \"alepha.api.oauth.options\",\n description: \"Configuration for the OAuth authorization server.\",\n schema: t.object({\n realm: t.text({ default: \"users\" }),\n resource: t.text({ default: \"/mcp\" }),\n loginPath: t.text({ default: \"/login\" }),\n }),\n default: { realm: \"users\", resource: \"/mcp\", loginPath: \"/login\" },\n});\n\n/**\n * OAuth 2.1 authorization server endpoints: discovery metadata and\n * RFC 7591 dynamic client registration. Authorize/token routes are added\n * separately.\n */\nexport class OAuthController {\n protected readonly log = $logger();\n protected readonly options = $state(oauthOptions);\n protected readonly clients = $inject(OAuthClientService);\n\n /**\n * Absolute origin of the current request, e.g. https://app.com.\n */\n protected baseUrl(url: URL): string {\n return `${url.protocol}//${url.host}`;\n }\n\n metadata = $route({\n method: \"GET\",\n path: \"/.well-known/oauth-authorization-server\",\n handler: ({ url, reply }) => {\n reply.headers[\"content-type\"] = \"application/json\";\n reply.body = JSON.stringify(\n buildAuthorizationServerMetadata(this.baseUrl(url)),\n );\n },\n });\n\n protectedResource = $route({\n method: \"GET\",\n path: \"/.well-known/oauth-protected-resource\",\n handler: ({ url, reply }) => {\n const base = this.baseUrl(url);\n reply.headers[\"content-type\"] = \"application/json\";\n reply.body = JSON.stringify(\n buildProtectedResourceMetadata(base, `${base}${this.options.resource}`),\n );\n },\n });\n\n register = $route({\n method: \"POST\",\n path: \"/oauth/register\",\n schema: { body: registerClientBodySchema },\n handler: async ({ body, reply }) => {\n const client = await this.clients.register({\n realm: this.options.realm,\n clientName: body.client_name ?? \"MCP Client\",\n redirectUris: body.redirect_uris,\n scopes: body.scope ? body.scope.split(\" \") : [\"mcp\"],\n source: \"dcr\",\n });\n reply.status = 201;\n reply.headers[\"content-type\"] = \"application/json\";\n reply.body = JSON.stringify({\n client_id: client.clientId,\n client_id_issued_at: Math.floor(\n new Date(client.createdAt).getTime() / 1000,\n ),\n client_name: client.clientName,\n redirect_uris: client.redirectUris,\n grant_types: [\"authorization_code\"],\n token_endpoint_auth_method: \"none\",\n });\n },\n });\n\n /**\n * GET /oauth/authorize — OAuth 2.1 authorization request. If the user\n * has no session, redirect to the realm login page with a return URL.\n * If authenticated, render the consent screen.\n */\n authorize = $route({\n method: \"GET\",\n path: \"/oauth/authorize\",\n schema: { query: authorizeQuerySchema },\n use: [],\n handler: async ({ query, user, url, reply }) => {\n if (query.response_type !== \"code\") {\n reply.status = 400;\n reply.body = \"unsupported response_type\";\n return;\n }\n if (query.code_challenge_method !== \"S256\") {\n reply.status = 400;\n reply.body = \"code_challenge_method must be S256\";\n return;\n }\n const client = await this.clients.findByClientId(query.client_id);\n if (!client || client.revokedAt) {\n reply.status = 400;\n reply.body = \"unknown client_id\";\n return;\n }\n if (!this.clients.isRedirectUriAllowed(client, query.redirect_uri)) {\n reply.status = 400;\n reply.body = \"redirect_uri not registered\";\n return;\n }\n if (!user) {\n const returnTo = encodeURIComponent(url.pathname + url.search);\n reply.redirect(\n `${this.options.loginPath}?redirect_uri=${returnTo}`,\n 302,\n );\n return;\n }\n reply.headers[\"content-type\"] = \"text/html; charset=utf-8\";\n reply.body = renderConsentPage({\n clientName: client.clientName,\n userName: user.name ?? user.email ?? \"your account\",\n scopes: query.scope ? query.scope.split(\" \") : client.scopes,\n hidden: {\n response_type: query.response_type,\n client_id: query.client_id,\n redirect_uri: query.redirect_uri,\n code_challenge: query.code_challenge,\n code_challenge_method: query.code_challenge_method,\n scope: query.scope ?? \"\",\n state: query.state ?? \"\",\n resource: query.resource ?? \"\",\n },\n });\n },\n });\n\n /**\n * POST /oauth/authorize — consent decision. On \"allow\", mint an\n * authorization code and redirect back to the client's redirect_uri.\n *\n * CSRF: this route carries no CSRF token and relies solely on the session\n * cookie to identify the user. This is a deliberate MVP/MCP tradeoff — a\n * forged consent submit can still only issue an authorization code to an\n * already-registered client, and that code is bound by PKCE, so the\n * attacker cannot redeem it without the matching code_verifier.\n */\n authorizeDecision = $route({\n method: \"POST\",\n path: \"/oauth/authorize\",\n schema: { body: authorizeDecisionBodySchema },\n use: [],\n handler: async ({ body, user, reply }) => {\n if (!user) {\n reply.status = 401;\n reply.body = \"authentication required\";\n return;\n }\n const client = await this.clients.findByClientId(body.client_id);\n if (\n !client ||\n client.revokedAt ||\n !this.clients.isRedirectUriAllowed(client, body.redirect_uri)\n ) {\n reply.status = 400;\n reply.body = \"invalid client\";\n return;\n }\n const redirect = new URL(body.redirect_uri);\n if (body.decision !== \"allow\") {\n redirect.searchParams.set(\"error\", \"access_denied\");\n if (body.state) redirect.searchParams.set(\"state\", body.state);\n reply.redirect(redirect.toString(), 302);\n return;\n }\n const code = await this.clients.createAuthorizationCode(\n this.options.realm,\n {\n userId: user.id,\n clientId: body.client_id,\n redirectUri: body.redirect_uri,\n codeChallenge: body.code_challenge,\n scopes: body.scope ? body.scope.split(\" \") : client.scopes,\n resource: body.resource || undefined,\n },\n );\n redirect.searchParams.set(\"code\", code);\n if (body.state) redirect.searchParams.set(\"state\", body.state);\n reply.redirect(redirect.toString(), 302);\n },\n });\n\n /**\n * POST /oauth/token — supports the `authorization_code` grant (verifies\n * PKCE, mints an access token via the realm issuer) and the\n * `refresh_token` grant (exchanges a refresh token for a fresh access\n * token, so a client stays connected without re-running the flow).\n */\n token = $route({\n method: \"POST\",\n path: \"/oauth/token\",\n schema: { body: tokenRequestBodySchema },\n use: [],\n handler: async ({ body, reply }) => {\n reply.headers[\"content-type\"] = \"application/json\";\n try {\n if (body.grant_type === \"authorization_code\") {\n const grant = await this.clients.consumeAuthorizationCode(\n this.options.realm,\n body.code ?? \"\",\n {\n clientId: body.client_id ?? \"\",\n redirectUri: body.redirect_uri ?? \"\",\n codeVerifier: body.code_verifier ?? \"\",\n },\n );\n const tokens = await this.clients.issueAccessToken(\n this.options.realm,\n { ...grant, clientId: body.client_id ?? \"\" },\n );\n reply.body = JSON.stringify({\n access_token: tokens.access_token,\n token_type: \"Bearer\",\n expires_in: tokens.expires_in,\n refresh_token: tokens.refresh_token,\n scope: grant.scopes.join(\" \"),\n });\n return;\n }\n\n if (body.grant_type === \"refresh_token\") {\n const tokens = await this.clients.refreshAccessToken(\n this.options.realm,\n body.refresh_token ?? \"\",\n );\n reply.body = JSON.stringify({\n access_token: tokens.access_token,\n token_type: \"Bearer\",\n expires_in: tokens.expires_in,\n refresh_token: tokens.refresh_token,\n });\n return;\n }\n\n reply.status = 400;\n reply.body = JSON.stringify({ error: \"unsupported_grant_type\" });\n } catch (e) {\n this.log.warn(\"OAuth token exchange failed\", e);\n reply.status = 400;\n reply.body = JSON.stringify({ error: \"invalid_grant\" });\n }\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { OAuthController } from \"./controllers/OAuthController.ts\";\nimport { OAuthClientService } from \"./services/OAuthClientService.ts\";\n\nexport {\n OAuthController,\n oauthOptions,\n} from \"./controllers/OAuthController.ts\";\nexport type { OAuthClientEntity } from \"./entities/oauthClientEntity.ts\";\nexport { oauthClientEntity } from \"./entities/oauthClientEntity.ts\";\nexport type { RegisterClientOptions } from \"./services/OAuthClientService.ts\";\nexport { OAuthClientService } from \"./services/OAuthClientService.ts\";\n\n/**\n * OAuth 2.1 authorization server module for MCP.\n *\n * **Features:**\n * - OAuth 2.1 authorization code flow with PKCE (RFC 7636)\n * - Dynamic Client Registration (RFC 7591)\n * - Authorization server metadata discovery (RFC 8414)\n * - Stateless authorization codes (short-lived signed JWTs)\n * - Single-use code enforcement\n *\n * **Integration:**\n * Register the module and configure the realm + protected resource path:\n *\n * ```ts\n * const app = Alepha.create()\n * .with(AlephaOAuth)\n * .set(oauthOptions, { realm: \"users\", resource: \"/mcp\" });\n * ```\n *\n * @module alepha.api.oauth\n */\nexport const AlephaOAuth = $module({\n name: \"alepha.api.oauth\",\n services: [OAuthClientService, OAuthController],\n});\n"],"mappings":"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"}