@frontmcp/auth 1.10.16 → 1.11.0-rc.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 (175) hide show
  1. package/browser/authorities/authorities.context.mjs +115 -0
  2. package/browser/authorities/authorities.engine.mjs +272 -0
  3. package/browser/authorities/authorities.errors.mjs +59 -0
  4. package/browser/authorities/authorities.evaluator.mjs +225 -0
  5. package/browser/authorities/authorities.metadata-augment.mjs +18 -0
  6. package/browser/authorities/authorities.registry.mjs +112 -0
  7. package/browser/authorities/authorities.schema.mjs +92 -0
  8. package/browser/authorities/authorities.scope-mapping.mjs +30 -0
  9. package/browser/authorities/authorities.validation.mjs +208 -0
  10. package/browser/authorities/index.mjs +43 -0
  11. package/browser/authorization/authorization.class.mjs +252 -0
  12. package/browser/authorization/authorization.types.mjs +64 -0
  13. package/browser/authorization/dcr-client.registry.mjs +222 -0
  14. package/browser/authorization/index.mjs +41 -0
  15. package/browser/authorization/orchestrated.accessor.mjs +86 -0
  16. package/browser/authorization/orchestrated.authorization.mjs +289 -0
  17. package/browser/authorization/public.authorization.mjs +124 -0
  18. package/browser/authorization/transparent.authorization.mjs +130 -0
  19. package/browser/cimd/cimd-redis.cache.mjs +231 -0
  20. package/browser/cimd/cimd.cache.mjs +215 -0
  21. package/browser/cimd/cimd.errors.mjs +142 -0
  22. package/browser/cimd/cimd.logger.mjs +15 -0
  23. package/browser/cimd/cimd.service.mjs +376 -0
  24. package/browser/cimd/cimd.types.mjs +228 -0
  25. package/browser/cimd/cimd.validator.mjs +217 -0
  26. package/browser/cimd/index.mjs +63 -0
  27. package/browser/common/auth-logger.interface.mjs +14 -0
  28. package/browser/common/index.mjs +6 -0
  29. package/browser/common/jwt.types.mjs +38 -0
  30. package/browser/common/session-user.types.mjs +0 -0
  31. package/browser/common/session.types.mjs +58 -0
  32. package/browser/common/zod-utils.mjs +0 -0
  33. package/browser/consent/consent.types.mjs +94 -0
  34. package/browser/consent/index.mjs +17 -0
  35. package/browser/context/frontmcp-auth-context.factory.mjs +29 -0
  36. package/browser/context/frontmcp-auth-context.impl.mjs +112 -0
  37. package/browser/context/index.mjs +7 -0
  38. package/browser/detection/auth-provider-detection.mjs +176 -0
  39. package/browser/detection/index.mjs +19 -0
  40. package/browser/errors/auth-internal.error.mjs +42 -0
  41. package/browser/errors/auth-internal.errors.mjs +150 -0
  42. package/browser/errors/index.mjs +53 -0
  43. package/browser/fetch/fetch-credential-middleware.mjs +91 -0
  44. package/browser/fetch/index.mjs +19 -0
  45. package/browser/index.mjs +573 -0
  46. package/browser/jwks/index.mjs +9 -0
  47. package/browser/jwks/jose.loader.mjs +28 -0
  48. package/browser/jwks/jwks.service.mjs +521 -0
  49. package/browser/jwks/jwks.utils.mjs +33 -0
  50. package/browser/options/app-auth.schema.mjs +18 -0
  51. package/browser/options/index.mjs +89 -0
  52. package/browser/options/orchestrated.schema.mjs +128 -0
  53. package/browser/options/public.schema.mjs +39 -0
  54. package/browser/options/schema.mjs +16 -0
  55. package/browser/options/shared.schemas.mjs +506 -0
  56. package/browser/options/static.schema.mjs +41 -0
  57. package/browser/options/transparent.schema.mjs +43 -0
  58. package/browser/options/utils.mjs +47 -0
  59. package/browser/people/index.mjs +15 -0
  60. package/browser/people/loopback-token.auth.mjs +115 -0
  61. package/browser/people/people-auth.options.mjs +37 -0
  62. package/browser/session/authorization-vault.mjs +279 -0
  63. package/browser/session/authorization.store.mjs +336 -0
  64. package/browser/session/consent.store.mjs +34 -0
  65. package/browser/session/credential-resume-link.mjs +43 -0
  66. package/browser/session/credentials-accessor.impl.mjs +54 -0
  67. package/browser/session/credentials-accessor.mjs +5 -0
  68. package/browser/session/encrypted-authorization-vault.mjs +405 -0
  69. package/browser/session/federated-auth.session.mjs +176 -0
  70. package/browser/session/incremental-auth-ticket.mjs +42 -0
  71. package/browser/session/index.mjs +266 -0
  72. package/browser/session/orchestrated-token.crypto.mjs +47 -0
  73. package/browser/session/orchestrated-token.store.mjs +286 -0
  74. package/browser/session/pending-login.mjs +37 -0
  75. package/browser/session/redis-session.store.mjs +255 -0
  76. package/browser/session/secure-store-accessor.impl.mjs +78 -0
  77. package/browser/session/secure-store-accessor.mjs +5 -0
  78. package/browser/session/secure-store-backends.mjs +111 -0
  79. package/browser/session/secure-store.factory.mjs +77 -0
  80. package/browser/session/session-credential-vault.factory.mjs +18 -0
  81. package/browser/session/session-credential-vault.mjs +243 -0
  82. package/browser/session/session-crypto.mjs +39 -0
  83. package/browser/session/session-rate-limiter.mjs +126 -0
  84. package/browser/session/session.transport.mjs +16 -0
  85. package/browser/session/signin-binding.mjs +30 -0
  86. package/browser/session/storage/in-memory-authorization-vault.mjs +28 -0
  87. package/browser/session/storage/index.mjs +14 -0
  88. package/browser/session/storage/storage-authorization-vault.mjs +301 -0
  89. package/browser/session/storage/storage-token-store.mjs +83 -0
  90. package/browser/session/storage-authorization.store.mjs +134 -0
  91. package/browser/session/storage-consent.store.mjs +33 -0
  92. package/browser/session/storage-federated-auth.session.mjs +55 -0
  93. package/browser/session/storage-orchestrated-token.store.mjs +197 -0
  94. package/browser/session/token-storage.factory.mjs +58 -0
  95. package/browser/session/token.refresh.mjs +36 -0
  96. package/browser/session/token.vault.mjs +61 -0
  97. package/browser/session/transport-session.types.mjs +108 -0
  98. package/browser/session/utils/auth-token.utils.mjs +58 -0
  99. package/browser/session/utils/index.mjs +23 -0
  100. package/browser/session/utils/session-crypto.utils.mjs +49 -0
  101. package/browser/session/utils/tiny-ttl-cache.mjs +46 -0
  102. package/browser/session/vault-encryption.mjs +187 -0
  103. package/browser/session/vercel-kv-session.store.mjs +230 -0
  104. package/browser/shared-server/index.mjs +17 -0
  105. package/browser/shared-server/shared-server-identity.mjs +55 -0
  106. package/browser/ui/base-layout.mjs +184 -0
  107. package/browser/ui/index.mjs +51 -0
  108. package/browser/ui/local-login.helper.mjs +46 -0
  109. package/browser/ui/templates.mjs +482 -0
  110. package/browser/utils/audience.validator.mjs +110 -0
  111. package/browser/utils/authorization-id.utils.mjs +10 -0
  112. package/browser/utils/index.mjs +45 -0
  113. package/browser/utils/redirect-uri.utils.mjs +22 -0
  114. package/browser/utils/scope-grant.utils.mjs +36 -0
  115. package/browser/utils/token-scopes.utils.mjs +12 -0
  116. package/browser/utils/www-authenticate.utils.mjs +164 -0
  117. package/browser/vault/auth-providers.accessor.impl.mjs +236 -0
  118. package/browser/vault/auth-providers.accessor.mjs +5 -0
  119. package/browser/vault/auth-providers.registry.mjs +140 -0
  120. package/browser/vault/auth-providers.types.mjs +47 -0
  121. package/browser/vault/auth-providers.vault.mjs +206 -0
  122. package/browser/vault/credential-cache.mjs +187 -0
  123. package/browser/vault/credential-helpers.mjs +15 -0
  124. package/browser/vault/credential-loaders/eager-loader.mjs +91 -0
  125. package/browser/vault/credential-loaders/lazy-loader.mjs +120 -0
  126. package/browser/vault/index.mjs +35 -0
  127. package/cimd/cimd.types.d.ts +1 -1
  128. package/cimd/cimd.types.d.ts.map +1 -1
  129. package/common/jwt.types.d.ts +1 -1
  130. package/common/jwt.types.d.ts.map +1 -1
  131. package/common/session.types.d.ts +9 -1
  132. package/common/session.types.d.ts.map +1 -1
  133. package/consent/consent.types.d.ts +1 -1
  134. package/consent/consent.types.d.ts.map +1 -1
  135. package/detection/auth-provider-detection.d.ts +1 -1
  136. package/detection/auth-provider-detection.d.ts.map +1 -1
  137. package/errors/auth-internal.errors.d.ts +6 -0
  138. package/errors/auth-internal.errors.d.ts.map +1 -1
  139. package/errors/index.d.ts +1 -1
  140. package/errors/index.d.ts.map +1 -1
  141. package/esm/index.mjs +2 -2
  142. package/esm/package.json +21 -5
  143. package/index.d.ts +5 -1
  144. package/index.d.ts.map +1 -1
  145. package/index.js +2 -2
  146. package/options/app-auth.schema.d.ts +19 -1
  147. package/options/app-auth.schema.d.ts.map +1 -1
  148. package/options/orchestrated.schema.d.ts +37 -1
  149. package/options/orchestrated.schema.d.ts.map +1 -1
  150. package/options/public.schema.d.ts +1 -1
  151. package/options/public.schema.d.ts.map +1 -1
  152. package/options/schema.d.ts +19 -1
  153. package/options/schema.d.ts.map +1 -1
  154. package/options/shared.schemas.d.ts +10 -14
  155. package/options/shared.schemas.d.ts.map +1 -1
  156. package/options/static.schema.d.ts +1 -1
  157. package/options/static.schema.d.ts.map +1 -1
  158. package/options/transparent.schema.d.ts +1 -1
  159. package/options/transparent.schema.d.ts.map +1 -1
  160. package/package.json +21 -5
  161. package/people/index.d.ts +3 -0
  162. package/people/index.d.ts.map +1 -0
  163. package/people/loopback-token.auth.d.ts +75 -0
  164. package/people/loopback-token.auth.d.ts.map +1 -0
  165. package/people/people-auth.options.d.ts +36 -0
  166. package/people/people-auth.options.d.ts.map +1 -0
  167. package/session/authorization-vault.d.ts +1 -1
  168. package/session/authorization-vault.d.ts.map +1 -1
  169. package/session/encrypted-authorization-vault.d.ts +1 -1
  170. package/session/encrypted-authorization-vault.d.ts.map +1 -1
  171. package/session/secure-store.factory.d.ts.map +1 -1
  172. package/shared-server/index.d.ts +3 -0
  173. package/shared-server/index.d.ts.map +1 -0
  174. package/shared-server/shared-server-identity.d.ts +26 -0
  175. package/shared-server/shared-server-identity.d.ts.map +1 -0
@@ -0,0 +1,376 @@
1
+ // libs/auth/src/cimd/cimd.service.ts
2
+ import { isRedirectResponse } from "@frontmcp/utils";
3
+ import { createCimdCache } from "./cimd.cache.mjs";
4
+ import {
5
+ CimdClientIdMismatchError,
6
+ CimdFetchError,
7
+ CimdResponseTooLargeError,
8
+ CimdSecurityError,
9
+ CimdValidationError,
10
+ RedirectUriMismatchError
11
+ } from "./cimd.errors.mjs";
12
+ import { noopLogger } from "./cimd.logger.mjs";
13
+ import {
14
+ cimdCacheConfigSchema,
15
+ cimdConfigSchema,
16
+ cimdNetworkConfigSchema,
17
+ cimdSecurityConfigSchema,
18
+ clientMetadataDocumentSchema
19
+ } from "./cimd.types.mjs";
20
+ import { assertHostNotSsrf, hasOnlyLocalhostRedirectUris, isCimdClientId, validateClientIdUrl } from "./cimd.validator.mjs";
21
+ var CimdService = class {
22
+ config;
23
+ cacheConfig;
24
+ securityConfig;
25
+ networkConfig;
26
+ cacheBackend;
27
+ logger;
28
+ /**
29
+ * Whether CIMD is enabled.
30
+ */
31
+ get enabled() {
32
+ return this.config.enabled;
33
+ }
34
+ /**
35
+ * Create a new CIMD service.
36
+ *
37
+ * @param logger - Optional logger. If not provided, logging is disabled.
38
+ * @param config - Optional configuration.
39
+ */
40
+ constructor(logger, config) {
41
+ this.logger = (logger ?? noopLogger).child("CimdService");
42
+ this.config = cimdConfigSchema.parse(config ?? {});
43
+ this.cacheConfig = cimdCacheConfigSchema.parse(this.config.cache ?? {});
44
+ this.securityConfig = cimdSecurityConfigSchema.parse(this.config.security ?? {});
45
+ this.networkConfig = cimdNetworkConfigSchema.parse(this.config.network ?? {});
46
+ this.logger.debug("CimdService initialized", {
47
+ enabled: this.config.enabled,
48
+ cacheDefaultTtlMs: this.cacheConfig.defaultTtlMs,
49
+ networkTimeoutMs: this.networkConfig.timeoutMs
50
+ });
51
+ if (this.securityConfig.allowInsecureForTesting) {
52
+ this.logger.warn(
53
+ "CIMD allowInsecureForTesting is enabled. HTTP is allowed for localhost CIMD URLs. This should NEVER be enabled in production!"
54
+ );
55
+ }
56
+ }
57
+ /**
58
+ * Create the cache now (connecting to Redis with `cache.type: 'redis'`), so a misconfigured or
59
+ * unreachable cache fails at startup rather than on the first CIMD client.
60
+ */
61
+ async initialize() {
62
+ await this.cache();
63
+ }
64
+ /**
65
+ * The cache `cache.type` selects, created once through the `createCimdCache` factory. A failed
66
+ * creation is retried on the next use.
67
+ */
68
+ cache() {
69
+ if (!this.cacheBackend) {
70
+ this.cacheBackend = createCimdCache(this.cacheConfig).catch((error) => {
71
+ this.cacheBackend = void 0;
72
+ throw error;
73
+ });
74
+ }
75
+ return this.cacheBackend;
76
+ }
77
+ /** Close the cache's connection (Redis with `cache.type: 'redis'`), if one was created. */
78
+ async dispose() {
79
+ const backend = this.cacheBackend;
80
+ this.cacheBackend = void 0;
81
+ if (!backend) return;
82
+ const createdBackend = await backend.catch(() => void 0);
83
+ await createdBackend?.close?.();
84
+ }
85
+ /**
86
+ * Check if a client_id is a CIMD URL.
87
+ *
88
+ * @param clientId - The client_id to check
89
+ * @returns true if this is a CIMD client_id (HTTPS URL with path, or HTTP for localhost when testing)
90
+ */
91
+ isCimdClientId(clientId) {
92
+ return isCimdClientId(clientId, this.securityConfig.allowInsecureForTesting);
93
+ }
94
+ /**
95
+ * Resolve a client_id to its metadata document.
96
+ *
97
+ * If the client_id is a CIMD URL, this fetches and validates the metadata document.
98
+ * Non-CIMD client IDs return a result with isCimdClient: false.
99
+ *
100
+ * @param clientId - The client_id to resolve
101
+ * @returns Resolution result with metadata if available
102
+ */
103
+ async resolveClientMetadata(clientId) {
104
+ if (!this.isCimdClientId(clientId)) {
105
+ return {
106
+ isCimdClient: false,
107
+ fromCache: false
108
+ };
109
+ }
110
+ validateClientIdUrl(clientId, this.securityConfig);
111
+ const cache = await this.cache();
112
+ const cached = await cache.get(clientId);
113
+ if (cached) {
114
+ this.logger.debug(`Cache hit for CIMD client: ${clientId}`);
115
+ return {
116
+ isCimdClient: true,
117
+ metadata: cached.document,
118
+ fromCache: true,
119
+ expiresAt: cached.expiresAt,
120
+ etag: cached.etag,
121
+ lastModified: cached.lastModified
122
+ };
123
+ }
124
+ this.logger.info(`Fetching CIMD document: ${clientId}`);
125
+ const { document, headers } = await this.fetchMetadataDocument(clientId);
126
+ this.validateDocument(clientId, document);
127
+ if (this.securityConfig.warnOnLocalhostRedirects && hasOnlyLocalhostRedirectUris(document.redirect_uris)) {
128
+ this.logger.warn(`CIMD client "${clientId}" has only localhost redirect URIs - this may be a development client`);
129
+ }
130
+ await cache.set(clientId, document, headers);
131
+ const entry = await cache.get(clientId);
132
+ return {
133
+ isCimdClient: true,
134
+ metadata: document,
135
+ fromCache: false,
136
+ expiresAt: entry?.expiresAt,
137
+ etag: entry?.etag,
138
+ lastModified: entry?.lastModified
139
+ };
140
+ }
141
+ /**
142
+ * Validate that a redirect_uri is registered for the client.
143
+ *
144
+ * @param redirectUri - The redirect_uri from the authorization request
145
+ * @param metadata - The client's metadata document
146
+ * @throws RedirectUriMismatchError if the redirect_uri is not registered
147
+ */
148
+ validateRedirectUri(redirectUri, metadata) {
149
+ const normalizedRequestUri = normalizeRedirectUri(redirectUri);
150
+ const normalizedAllowed = metadata.redirect_uris.map(normalizeRedirectUri);
151
+ if (!normalizedAllowed.includes(normalizedRequestUri)) {
152
+ throw new RedirectUriMismatchError(metadata.client_id, redirectUri, metadata.redirect_uris);
153
+ }
154
+ }
155
+ /**
156
+ * Clear the cache for a specific client or all clients.
157
+ *
158
+ * @param clientId - Optional client_id to clear; clears all if not provided
159
+ */
160
+ async clearCache(clientId) {
161
+ const cache = await this.cache();
162
+ if (clientId) {
163
+ await cache.delete(clientId);
164
+ this.logger.debug(`Cache cleared for: ${clientId}`);
165
+ } else {
166
+ await cache.clear();
167
+ this.logger.debug("Cache cleared");
168
+ }
169
+ }
170
+ /**
171
+ * Get cache statistics.
172
+ */
173
+ async getCacheStats() {
174
+ const cache = await this.cache();
175
+ return {
176
+ size: await cache.size()
177
+ };
178
+ }
179
+ /**
180
+ * Fetch a metadata document from a CIMD URL.
181
+ *
182
+ * Following the JwksService.fetchJson() pattern.
183
+ */
184
+ async fetchMetadataDocument(clientId) {
185
+ const controller = new AbortController();
186
+ const timer = setTimeout(() => controller.abort(), this.networkConfig.timeoutMs);
187
+ try {
188
+ const cache = await this.cache();
189
+ const conditionalHeaders = await cache.getConditionalHeaders(clientId);
190
+ const originalOrigin = new URL(clientId).origin;
191
+ const maxRedirects = this.networkConfig.maxRedirects;
192
+ let currentUrl = clientId;
193
+ let redirectCount = 0;
194
+ let isFirstRequest = true;
195
+ while (true) {
196
+ if (this.securityConfig.blockPrivateIPs && !this.securityConfig.allowInsecureForTesting) {
197
+ await assertHostNotSsrf(new URL(currentUrl).hostname, clientId);
198
+ }
199
+ const headers = {
200
+ Accept: "application/json"
201
+ };
202
+ if (isFirstRequest && conditionalHeaders) {
203
+ Object.assign(headers, conditionalHeaders);
204
+ }
205
+ const response = await fetch(currentUrl, {
206
+ method: "GET",
207
+ headers,
208
+ signal: controller.signal,
209
+ redirect: "manual"
210
+ });
211
+ if (response.status === 304) {
212
+ const staleEntry = await cache.getStale(clientId);
213
+ if (staleEntry) {
214
+ await cache.revalidate(clientId, response.headers);
215
+ this.logger.debug(`CIMD document not modified: ${clientId}`);
216
+ return {
217
+ document: staleEntry.document,
218
+ headers: response.headers
219
+ };
220
+ }
221
+ throw new CimdFetchError(clientId, "304 Not Modified but no cached entry", {
222
+ httpStatus: 304
223
+ });
224
+ }
225
+ if (isRedirectResponse(response)) {
226
+ const location = response.headers.get("location");
227
+ if (!location) {
228
+ throw new CimdFetchError(clientId, "Redirect response missing Location header", {
229
+ httpStatus: response.status
230
+ });
231
+ }
232
+ const nextUrl = new URL(location, currentUrl).toString();
233
+ const redirectPolicy = this.networkConfig.redirectPolicy;
234
+ if (redirectPolicy === "deny") {
235
+ throw new CimdFetchError(
236
+ clientId,
237
+ `CIMD fetch redirected to "${nextUrl}" but redirects are disabled. Host the metadata at the client_id URL or set auth.cimd.network.redirectPolicy to "same-origin" or "allow".`,
238
+ { httpStatus: response.status }
239
+ );
240
+ }
241
+ if (redirectPolicy === "same-origin") {
242
+ const nextOrigin = new URL(nextUrl).origin;
243
+ if (nextOrigin !== originalOrigin) {
244
+ throw new CimdFetchError(
245
+ clientId,
246
+ `CIMD fetch redirected to "${nextUrl}" which is not the same origin as "${originalOrigin}". Host the metadata at the client_id URL or set auth.cimd.network.redirectPolicy to "allow".`,
247
+ { httpStatus: response.status }
248
+ );
249
+ }
250
+ }
251
+ validateClientIdUrl(nextUrl, this.securityConfig);
252
+ redirectCount += 1;
253
+ if (redirectCount > maxRedirects) {
254
+ throw new CimdFetchError(
255
+ clientId,
256
+ `CIMD fetch exceeded max redirects (${maxRedirects}). Host the metadata at the client_id URL or increase auth.cimd.network.maxRedirects.`
257
+ );
258
+ }
259
+ this.logger.warn(`CIMD redirect ${redirectCount}/${maxRedirects}: ${currentUrl} -> ${nextUrl}`);
260
+ currentUrl = nextUrl;
261
+ isFirstRequest = false;
262
+ continue;
263
+ }
264
+ if (!response.ok) {
265
+ throw new CimdFetchError(clientId, `HTTP ${response.status} ${response.statusText}`, {
266
+ httpStatus: response.status
267
+ });
268
+ }
269
+ const contentLength = response.headers.get("content-length");
270
+ if (contentLength) {
271
+ const length = parseInt(contentLength, 10);
272
+ if (!isNaN(length) && length > this.networkConfig.maxResponseSizeBytes) {
273
+ throw new CimdResponseTooLargeError(clientId, this.networkConfig.maxResponseSizeBytes, length);
274
+ }
275
+ }
276
+ const text = await this.readResponseWithLimit(response, this.networkConfig.maxResponseSizeBytes, clientId);
277
+ let document;
278
+ try {
279
+ document = JSON.parse(text);
280
+ } catch (e) {
281
+ throw new CimdFetchError(clientId, "Invalid JSON response", {
282
+ originalError: e instanceof Error ? e : new Error(String(e))
283
+ });
284
+ }
285
+ return { document, headers: response.headers };
286
+ }
287
+ } catch (error) {
288
+ if (error instanceof CimdFetchError || error instanceof CimdResponseTooLargeError || error instanceof CimdSecurityError) {
289
+ throw error;
290
+ }
291
+ if (error instanceof Error && error.name === "AbortError") {
292
+ throw new CimdFetchError(clientId, "Request timeout", {
293
+ originalError: error
294
+ });
295
+ }
296
+ throw new CimdFetchError(clientId, error instanceof Error ? error.message : "Unknown error", {
297
+ originalError: error instanceof Error ? error : void 0
298
+ });
299
+ } finally {
300
+ clearTimeout(timer);
301
+ }
302
+ }
303
+ /**
304
+ * Read response body with size limit.
305
+ */
306
+ async readResponseWithLimit(response, maxBytes, clientId) {
307
+ const reader = response.body?.getReader();
308
+ if (!reader) {
309
+ const buffer = await response.arrayBuffer();
310
+ if (buffer.byteLength > maxBytes) {
311
+ throw new CimdResponseTooLargeError(clientId, maxBytes, buffer.byteLength);
312
+ }
313
+ return new TextDecoder().decode(buffer);
314
+ }
315
+ const chunks = [];
316
+ let totalBytes = 0;
317
+ try {
318
+ while (true) {
319
+ const { done, value } = await reader.read();
320
+ if (done) break;
321
+ totalBytes += value.length;
322
+ if (totalBytes > maxBytes) {
323
+ throw new CimdResponseTooLargeError(clientId, maxBytes, totalBytes);
324
+ }
325
+ chunks.push(value);
326
+ }
327
+ } finally {
328
+ reader.releaseLock();
329
+ }
330
+ const combined = new Uint8Array(totalBytes);
331
+ let offset = 0;
332
+ for (const chunk of chunks) {
333
+ combined.set(chunk, offset);
334
+ offset += chunk.length;
335
+ }
336
+ return new TextDecoder().decode(combined);
337
+ }
338
+ /**
339
+ * Validate a fetched document against the schema.
340
+ *
341
+ * @param clientId - The URL from which the document was fetched
342
+ * @param document - The document to validate
343
+ */
344
+ validateDocument(clientId, document) {
345
+ const result = clientMetadataDocumentSchema.safeParse(document);
346
+ if (!result.success) {
347
+ const errors = result.error.issues.map((issue) => {
348
+ const path = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
349
+ return `${path}${issue.message}`;
350
+ });
351
+ throw new CimdValidationError(clientId, errors);
352
+ }
353
+ if (result.data.client_id !== clientId) {
354
+ throw new CimdClientIdMismatchError(clientId, result.data.client_id);
355
+ }
356
+ }
357
+ };
358
+ var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
359
+ function isLoopbackHostname(hostname) {
360
+ return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
361
+ }
362
+ function normalizeRedirectUri(uri) {
363
+ try {
364
+ const url = new URL(uri);
365
+ const host = isLoopbackHostname(url.hostname) ? url.hostname.toLowerCase() : url.host.toLowerCase();
366
+ let normalized = `${url.protocol.toLowerCase()}//${host}`;
367
+ normalized += url.pathname.replace(/\/+$/, "") || "/";
368
+ if (url.search) normalized += url.search;
369
+ return normalized;
370
+ } catch {
371
+ return uri;
372
+ }
373
+ }
374
+ export {
375
+ CimdService
376
+ };
@@ -0,0 +1,228 @@
1
+ // libs/auth/src/cimd/cimd.types.ts
2
+ import { internalZ as z } from "@frontmcp/lazy-zod";
3
+ var clientMetadataDocumentSchema = z.object({
4
+ // REQUIRED per CIMD spec
5
+ /**
6
+ * Client identifier - MUST match the URL from which this document was fetched.
7
+ */
8
+ client_id: z.string().url(),
9
+ /**
10
+ * Human-readable name of the client.
11
+ */
12
+ client_name: z.string().min(1),
13
+ /**
14
+ * Array of redirect URIs for authorization responses.
15
+ * At least one is required.
16
+ */
17
+ redirect_uris: z.array(z.string().url()).min(1),
18
+ // OPTIONAL per RFC 7591
19
+ /**
20
+ * Token endpoint authentication method.
21
+ * @default 'none'
22
+ */
23
+ token_endpoint_auth_method: z.enum(["none", "client_secret_basic", "client_secret_post", "private_key_jwt"]).default("none"),
24
+ /**
25
+ * OAuth grant types the client can use.
26
+ * @default ['authorization_code']
27
+ */
28
+ grant_types: z.array(z.string()).default(["authorization_code"]),
29
+ /**
30
+ * OAuth response types the client can request.
31
+ * @default ['code']
32
+ */
33
+ response_types: z.array(z.string()).default(["code"]),
34
+ /**
35
+ * URL of the client's home page.
36
+ */
37
+ client_uri: z.string().url().optional(),
38
+ /**
39
+ * URL of the client's logo image.
40
+ */
41
+ logo_uri: z.string().url().optional(),
42
+ /**
43
+ * URL of the client's JWKS (for private_key_jwt).
44
+ */
45
+ jwks_uri: z.string().url().optional(),
46
+ /**
47
+ * Inline JWKS (for private_key_jwt).
48
+ */
49
+ jwks: z.object({
50
+ keys: z.array(z.record(z.string(), z.unknown()))
51
+ }).optional(),
52
+ /**
53
+ * URL of the client's terms of service.
54
+ */
55
+ tos_uri: z.string().url().optional(),
56
+ /**
57
+ * URL of the client's privacy policy.
58
+ */
59
+ policy_uri: z.string().url().optional(),
60
+ /**
61
+ * Requested OAuth scopes.
62
+ */
63
+ scope: z.string().optional(),
64
+ /**
65
+ * Array of contact emails for the client.
66
+ */
67
+ contacts: z.array(z.string().email()).optional(),
68
+ /**
69
+ * Software statement (signed JWT).
70
+ */
71
+ software_statement: z.string().optional(),
72
+ /**
73
+ * Unique identifier for the client software.
74
+ */
75
+ software_id: z.string().optional(),
76
+ /**
77
+ * Version of the client software.
78
+ */
79
+ software_version: z.string().optional()
80
+ });
81
+ var cimdRedisCacheConfigSchema = z.object({
82
+ /**
83
+ * Redis connection URL.
84
+ * e.g., "redis://user:pass@host:6379/0"
85
+ */
86
+ url: z.string().optional(),
87
+ /**
88
+ * Redis host.
89
+ */
90
+ host: z.string().optional(),
91
+ /**
92
+ * Redis port.
93
+ * @default 6379
94
+ */
95
+ port: z.number().optional(),
96
+ /**
97
+ * Redis password.
98
+ */
99
+ password: z.string().optional(),
100
+ /**
101
+ * Redis database number.
102
+ * @default 0
103
+ */
104
+ db: z.number().optional(),
105
+ /**
106
+ * Enable TLS for Redis connection.
107
+ * @default false
108
+ */
109
+ tls: z.boolean().optional(),
110
+ /**
111
+ * Key prefix for CIMD cache entries.
112
+ * @default 'cimd:'
113
+ */
114
+ keyPrefix: z.string().default("cimd:")
115
+ });
116
+ var cimdCacheConfigSchema = z.object({
117
+ /**
118
+ * Cache storage type.
119
+ * - 'memory': In-memory cache (default, suitable for dev/single-instance)
120
+ * - 'redis': Redis-backed cache (for production/distributed deployments)
121
+ * @default 'memory'
122
+ */
123
+ type: z.enum(["memory", "redis"]).default("memory"),
124
+ /**
125
+ * Default TTL for cached metadata documents.
126
+ * @default 3600000 (1 hour)
127
+ */
128
+ defaultTtlMs: z.number().min(0).default(36e5),
129
+ /**
130
+ * Maximum TTL (even if server suggests longer).
131
+ * @default 86400000 (24 hours)
132
+ */
133
+ maxTtlMs: z.number().min(0).default(864e5),
134
+ /**
135
+ * Minimum TTL (even if server suggests shorter).
136
+ * @default 60000 (1 minute)
137
+ */
138
+ minTtlMs: z.number().min(0).default(6e4),
139
+ /**
140
+ * Redis configuration (required when type is 'redis').
141
+ */
142
+ redis: cimdRedisCacheConfigSchema.optional()
143
+ });
144
+ var cimdSecurityConfigSchema = z.object({
145
+ /**
146
+ * Block fetching from private/internal IP addresses (SSRF protection).
147
+ * @default true
148
+ */
149
+ blockPrivateIPs: z.boolean().default(true),
150
+ /**
151
+ * Explicit list of allowed domains.
152
+ * If set, only these domains can host CIMD documents.
153
+ */
154
+ allowedDomains: z.array(z.string()).optional(),
155
+ /**
156
+ * Explicit list of blocked domains.
157
+ * These domains cannot host CIMD documents.
158
+ */
159
+ blockedDomains: z.array(z.string()).optional(),
160
+ /**
161
+ * Warn when a client has only localhost redirect URIs.
162
+ * @default true
163
+ */
164
+ warnOnLocalhostRedirects: z.boolean().default(true),
165
+ /**
166
+ * Allow HTTP (instead of HTTPS) for localhost CIMD URLs.
167
+ *
168
+ * **WARNING: This is for testing purposes only. Never enable in production!**
169
+ *
170
+ * When enabled, permits HTTP URLs for localhost CIMD client IDs during testing.
171
+ * The CIMD spec requires HTTPS, but e2e test servers typically use HTTP.
172
+ *
173
+ * @default false
174
+ */
175
+ allowInsecureForTesting: z.boolean().default(false)
176
+ });
177
+ var cimdNetworkConfigSchema = z.object({
178
+ /**
179
+ * Request timeout in milliseconds.
180
+ * @default 5000 (5 seconds)
181
+ */
182
+ timeoutMs: z.number().min(100).default(5e3),
183
+ /**
184
+ * Maximum response body size in bytes.
185
+ * @default 65536 (64KB)
186
+ */
187
+ maxResponseSizeBytes: z.number().min(1024).default(65536),
188
+ /**
189
+ * Redirect handling policy for CIMD fetches.
190
+ * - 'deny': reject redirects (default, safest)
191
+ * - 'same-origin': allow redirects only to the same origin
192
+ * - 'allow': allow redirects to any origin
193
+ * @default 'deny'
194
+ */
195
+ redirectPolicy: z.enum(["deny", "same-origin", "allow"]).default("deny"),
196
+ /**
197
+ * Maximum number of redirects to follow when redirects are allowed.
198
+ * @default 5
199
+ */
200
+ maxRedirects: z.number().int().min(0).default(5)
201
+ });
202
+ var cimdConfigSchema = z.object({
203
+ /**
204
+ * Enable CIMD support.
205
+ * @default true
206
+ */
207
+ enabled: z.boolean().default(true),
208
+ /**
209
+ * Cache configuration.
210
+ */
211
+ cache: cimdCacheConfigSchema.optional(),
212
+ /**
213
+ * Security configuration.
214
+ */
215
+ security: cimdSecurityConfigSchema.optional(),
216
+ /**
217
+ * Network configuration.
218
+ */
219
+ network: cimdNetworkConfigSchema.optional()
220
+ });
221
+ export {
222
+ cimdCacheConfigSchema,
223
+ cimdConfigSchema,
224
+ cimdNetworkConfigSchema,
225
+ cimdRedisCacheConfigSchema,
226
+ cimdSecurityConfigSchema,
227
+ clientMetadataDocumentSchema
228
+ };