@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,187 @@
1
+ // libs/auth/src/session/vault-encryption.ts
2
+ import { z } from "@frontmcp/lazy-zod";
3
+ import {
4
+ base64urlDecode,
5
+ base64urlEncode,
6
+ decryptAesGcm,
7
+ encryptAesGcm,
8
+ hkdfSha256,
9
+ randomBytes
10
+ } from "@frontmcp/utils";
11
+ var encryptedDataSchema = z.object({
12
+ /** Version for future algorithm changes */
13
+ v: z.literal(1),
14
+ /** Algorithm identifier */
15
+ alg: z.literal("aes-256-gcm"),
16
+ /** Initialization vector (base64) */
17
+ iv: z.string(),
18
+ /** Ciphertext (base64) */
19
+ ct: z.string(),
20
+ /** Authentication tag (base64) */
21
+ tag: z.string()
22
+ });
23
+ var VaultEncryption = class {
24
+ pepper;
25
+ hkdfInfo;
26
+ constructor(config = {}) {
27
+ this.pepper = new TextEncoder().encode(config.pepper ?? "");
28
+ this.hkdfInfo = config.hkdfInfo ?? "frontmcp-vault-v1";
29
+ }
30
+ /**
31
+ * Derive an encryption key from JWT claims
32
+ *
33
+ * The key derivation uses HKDF:
34
+ * 1. Combine jti + vaultKey + sub + iat + pepper as IKM
35
+ * 2. Apply HKDF-SHA256 to derive a 256-bit key
36
+ *
37
+ * @param claims - JWT claims containing key material
38
+ * @returns 32-byte encryption key as Uint8Array
39
+ */
40
+ async deriveKey(claims) {
41
+ const ikmParts = [
42
+ claims.jti,
43
+ claims.vaultKey ?? "",
44
+ claims.sub,
45
+ claims.iat.toString(),
46
+ new TextDecoder().decode(this.pepper)
47
+ ];
48
+ const ikm = new TextEncoder().encode(ikmParts.join(""));
49
+ const infoBytes = new TextEncoder().encode(this.hkdfInfo);
50
+ const key = await hkdfSha256(ikm, new Uint8Array(0), infoBytes, 32);
51
+ return key;
52
+ }
53
+ /**
54
+ * Derive a key directly from the raw JWT token string
55
+ *
56
+ * This is useful when you want to derive the key from the token
57
+ * before or without fully parsing the claims. Uses the token's
58
+ * signature portion as additional entropy.
59
+ *
60
+ * @param token - The raw JWT token string
61
+ * @param claims - Parsed JWT claims
62
+ * @returns 32-byte encryption key as Uint8Array
63
+ */
64
+ async deriveKeyFromToken(token, claims) {
65
+ const parts = token.split(".");
66
+ const signature = parts[2] ?? "";
67
+ const ikmParts = [
68
+ claims.jti,
69
+ claims.vaultKey ?? "",
70
+ claims.sub,
71
+ claims.iat.toString(),
72
+ signature,
73
+ new TextDecoder().decode(this.pepper)
74
+ ];
75
+ const ikm = new TextEncoder().encode(ikmParts.join(""));
76
+ const infoBytes = new TextEncoder().encode(this.hkdfInfo);
77
+ const key = await hkdfSha256(ikm, new Uint8Array(0), infoBytes, 32);
78
+ return key;
79
+ }
80
+ /**
81
+ * Encrypt plaintext data using AES-256-GCM
82
+ *
83
+ * @param plaintext - Data to encrypt (typically JSON string)
84
+ * @param key - 32-byte encryption key from deriveKey()
85
+ * @returns Encrypted data object (safe to store in Redis)
86
+ */
87
+ async encrypt(plaintext, key) {
88
+ if (key.length !== 32) {
89
+ throw new Error("Encryption key must be 32 bytes");
90
+ }
91
+ const iv = randomBytes(12);
92
+ const { ciphertext, tag } = await encryptAesGcm(key, new TextEncoder().encode(plaintext), iv);
93
+ return {
94
+ v: 1,
95
+ alg: "aes-256-gcm",
96
+ iv: base64urlEncode(iv),
97
+ ct: base64urlEncode(ciphertext),
98
+ tag: base64urlEncode(tag)
99
+ };
100
+ }
101
+ /**
102
+ * Decrypt encrypted data using AES-256-GCM
103
+ *
104
+ * @param encrypted - Encrypted data object from encrypt()
105
+ * @param key - 32-byte encryption key from deriveKey()
106
+ * @returns Decrypted plaintext
107
+ * @throws Error if decryption fails (wrong key, tampered data, etc.)
108
+ */
109
+ async decrypt(encrypted, key) {
110
+ if (key.length !== 32) {
111
+ throw new Error("Encryption key must be 32 bytes");
112
+ }
113
+ const parsed = encryptedDataSchema.safeParse(encrypted);
114
+ if (!parsed.success) {
115
+ throw new Error("Invalid encrypted data format");
116
+ }
117
+ const { iv, ct, tag } = parsed.data;
118
+ const ivBuffer = base64urlDecode(iv);
119
+ const ciphertext = base64urlDecode(ct);
120
+ const tagBuffer = base64urlDecode(tag);
121
+ try {
122
+ const plaintext = await decryptAesGcm(key, ciphertext, ivBuffer, tagBuffer);
123
+ return new TextDecoder().decode(plaintext);
124
+ } catch (_error) {
125
+ throw new Error("Decryption failed: invalid key or corrupted data");
126
+ }
127
+ }
128
+ /**
129
+ * Encrypt a JavaScript object (serializes to JSON first)
130
+ *
131
+ * @param data - Object to encrypt
132
+ * @param key - Encryption key
133
+ * @returns Encrypted data
134
+ */
135
+ async encryptObject(data, key) {
136
+ return this.encrypt(JSON.stringify(data), key);
137
+ }
138
+ /**
139
+ * Decrypt and parse a JavaScript object
140
+ *
141
+ * @param encrypted - Encrypted data
142
+ * @param key - Encryption key
143
+ * @returns Decrypted and parsed object
144
+ */
145
+ async decryptObject(encrypted, key) {
146
+ const plaintext = await this.decrypt(encrypted, key);
147
+ return JSON.parse(plaintext);
148
+ }
149
+ /**
150
+ * Check if data is in encrypted format
151
+ *
152
+ * @param data - Data to check
153
+ * @returns True if data appears to be encrypted
154
+ */
155
+ isEncrypted(data) {
156
+ return encryptedDataSchema.safeParse(data).success;
157
+ }
158
+ };
159
+ var encryptedVaultEntrySchema = z.object({
160
+ /** Vault ID (maps to JWT jti claim) */
161
+ id: z.string(),
162
+ /** User subject identifier */
163
+ userSub: z.string(),
164
+ /** User email (unencrypted for display) */
165
+ userEmail: z.string().optional(),
166
+ /** User name (unencrypted for display) */
167
+ userName: z.string().optional(),
168
+ /** Client ID that created this session */
169
+ clientId: z.string(),
170
+ /** Creation timestamp */
171
+ createdAt: z.number(),
172
+ /** Last access timestamp */
173
+ lastAccessAt: z.number(),
174
+ /** Encrypted sensitive data (provider tokens, credentials, consent) */
175
+ encryptedData: encryptedDataSchema,
176
+ /** Apps that are fully authorized (unencrypted for quick lookup) */
177
+ authorizedAppIds: z.array(z.string()),
178
+ /** Apps that were skipped (unencrypted for quick lookup) */
179
+ skippedAppIds: z.array(z.string()),
180
+ /** Pending auth IDs (unencrypted for lookup, actual URLs encrypted) */
181
+ pendingAuthIds: z.array(z.string()).default([])
182
+ });
183
+ export {
184
+ VaultEncryption,
185
+ encryptedDataSchema,
186
+ encryptedVaultEntrySchema
187
+ };
@@ -0,0 +1,230 @@
1
+ // libs/auth/src/session/vercel-kv-session.store.ts
2
+ import { randomUUID, VercelKvStorageAdapter } from "@frontmcp/utils";
3
+ import { SessionIdEmptyError } from "../errors/auth-internal.errors.mjs";
4
+ import { signSession, verifyOrParseSession } from "./session-crypto.mjs";
5
+ import { SessionRateLimiter } from "./session-rate-limiter.mjs";
6
+ import {
7
+ storedSessionSchema
8
+ } from "./transport-session.types.mjs";
9
+ var VercelKvSessionStore = class {
10
+ storage;
11
+ keyPrefix;
12
+ defaultTtlMs;
13
+ logger;
14
+ // Security features
15
+ security;
16
+ rateLimiter;
17
+ constructor(config, logger) {
18
+ this.keyPrefix = config.keyPrefix ?? "mcp:session:";
19
+ this.defaultTtlMs = config.defaultTtlMs ?? 36e5;
20
+ this.logger = logger;
21
+ this.security = ("security" in config ? config.security : void 0) ?? {};
22
+ if (this.security.enableRateLimiting) {
23
+ this.rateLimiter = new SessionRateLimiter({
24
+ windowMs: this.security.rateLimiting?.windowMs,
25
+ maxRequests: this.security.rateLimiting?.maxRequests
26
+ });
27
+ }
28
+ this.storage = new VercelKvStorageAdapter({
29
+ url: config.url,
30
+ token: config.token,
31
+ keyPrefix: this.keyPrefix
32
+ });
33
+ }
34
+ /**
35
+ * Validate session ID
36
+ * @throws Error if sessionId is empty
37
+ */
38
+ validateSessionId(sessionId) {
39
+ if (!sessionId || sessionId.trim() === "") {
40
+ throw new SessionIdEmptyError("VercelKvSessionStore");
41
+ }
42
+ }
43
+ /**
44
+ * Connect to Vercel KV
45
+ * Thread-safe: concurrent calls will share the same connection via adapter.
46
+ */
47
+ async connect() {
48
+ await this.storage.connect();
49
+ }
50
+ /**
51
+ * Ensure the storage adapter is connected
52
+ */
53
+ async ensureConnected() {
54
+ await this.storage.connect();
55
+ }
56
+ /**
57
+ * Get a stored session by ID
58
+ *
59
+ * Note: Vercel KV doesn't support GETEX, so we use GET + EXPIRE separately.
60
+ * This is slightly less atomic than Redis GETEX but sufficient for most use cases.
61
+ *
62
+ * @param sessionId - The session ID to look up
63
+ * @param options - Optional parameters for rate limiting
64
+ * @param options.clientIdentifier - Client identifier (e.g., IP address) for rate limiting.
65
+ * When provided, rate limiting is applied per-client to prevent session enumeration.
66
+ * If not provided, falls back to sessionId which provides DoS protection per-session.
67
+ */
68
+ async get(sessionId, options) {
69
+ this.validateSessionId(sessionId);
70
+ if (this.rateLimiter) {
71
+ const rateLimitKey = options?.clientIdentifier || sessionId;
72
+ const rateLimitResult = this.rateLimiter.check(rateLimitKey);
73
+ if (!rateLimitResult.allowed) {
74
+ this.logger?.warn("[VercelKvSessionStore] Rate limit exceeded for session lookup", {
75
+ sessionId: sessionId.slice(0, 20),
76
+ clientIdentifier: options?.clientIdentifier ? options.clientIdentifier.slice(0, 20) : void 0,
77
+ retryAfterMs: rateLimitResult.retryAfterMs
78
+ });
79
+ return null;
80
+ }
81
+ }
82
+ await this.ensureConnected();
83
+ const raw = await this.storage.get(sessionId);
84
+ if (!raw) return null;
85
+ const ttlSeconds = Math.ceil(this.defaultTtlMs / 1e3);
86
+ this.storage.expire(sessionId, ttlSeconds).catch((err) => {
87
+ this.logger?.warn("[VercelKvSessionStore] TTL extension failed", {
88
+ sessionId: sessionId.slice(0, 20),
89
+ error: err.message
90
+ });
91
+ });
92
+ try {
93
+ let parsed;
94
+ if (this.security.enableSigning) {
95
+ parsed = verifyOrParseSession(raw, { secret: this.security.signingSecret });
96
+ if (!parsed) {
97
+ this.logger?.warn("[VercelKvSessionStore] Session signature verification failed", {
98
+ sessionId: sessionId.slice(0, 20)
99
+ });
100
+ this.delete(sessionId).catch(() => void 0);
101
+ return null;
102
+ }
103
+ } else {
104
+ parsed = JSON.parse(raw);
105
+ }
106
+ const result = storedSessionSchema.safeParse(parsed);
107
+ if (!result.success) {
108
+ this.logger?.warn("[VercelKvSessionStore] Invalid session format", {
109
+ sessionId: sessionId.slice(0, 20),
110
+ errors: result.error.issues.slice(0, 3).map((i) => ({ path: i.path, message: i.message }))
111
+ });
112
+ this.delete(sessionId).catch(() => void 0);
113
+ return null;
114
+ }
115
+ const session = result.data;
116
+ if (session.maxLifetimeAt && session.maxLifetimeAt < Date.now()) {
117
+ this.logger?.info("[VercelKvSessionStore] Session exceeded max lifetime", {
118
+ sessionId: sessionId.slice(0, 20),
119
+ maxLifetimeAt: session.maxLifetimeAt
120
+ });
121
+ await this.delete(sessionId);
122
+ return null;
123
+ }
124
+ if (session.session.expiresAt && session.session.expiresAt < Date.now()) {
125
+ await this.delete(sessionId);
126
+ return null;
127
+ }
128
+ if (session.session.expiresAt) {
129
+ const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());
130
+ if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {
131
+ const boundedTtlSeconds = Math.ceil(ttlMs / 1e3);
132
+ this.storage.expire(sessionId, boundedTtlSeconds).catch((err) => {
133
+ this.logger?.warn("[VercelKvSessionStore] TTL bound extension failed", {
134
+ sessionId: sessionId.slice(0, 20),
135
+ error: err.message
136
+ });
137
+ });
138
+ }
139
+ }
140
+ const updatedSession = {
141
+ ...session,
142
+ lastAccessedAt: Date.now()
143
+ };
144
+ return updatedSession;
145
+ } catch (error) {
146
+ this.logger?.warn("[VercelKvSessionStore] Failed to parse session", {
147
+ sessionId: sessionId.slice(0, 20),
148
+ error: error.message
149
+ });
150
+ this.delete(sessionId).catch(() => void 0);
151
+ return null;
152
+ }
153
+ }
154
+ /**
155
+ * Store a session with optional TTL
156
+ */
157
+ async set(sessionId, session, ttlMs) {
158
+ this.validateSessionId(sessionId);
159
+ await this.ensureConnected();
160
+ let value;
161
+ if (this.security.enableSigning) {
162
+ value = signSession(session, { secret: this.security.signingSecret });
163
+ } else {
164
+ value = JSON.stringify(session);
165
+ }
166
+ if (ttlMs && ttlMs > 0) {
167
+ const ttlSeconds = Math.ceil(ttlMs / 1e3);
168
+ await this.storage.set(sessionId, value, { ttlSeconds });
169
+ } else if (session.session.expiresAt) {
170
+ const ttl = session.session.expiresAt - Date.now();
171
+ if (ttl > 0) {
172
+ const ttlSeconds = Math.ceil(ttl / 1e3);
173
+ await this.storage.set(sessionId, value, { ttlSeconds });
174
+ } else {
175
+ await this.storage.set(sessionId, value);
176
+ }
177
+ } else {
178
+ await this.storage.set(sessionId, value);
179
+ }
180
+ }
181
+ /**
182
+ * Delete a session
183
+ */
184
+ async delete(sessionId) {
185
+ this.validateSessionId(sessionId);
186
+ await this.ensureConnected();
187
+ await this.storage.delete(sessionId);
188
+ }
189
+ /**
190
+ * Check if a session exists
191
+ */
192
+ async exists(sessionId) {
193
+ this.validateSessionId(sessionId);
194
+ await this.ensureConnected();
195
+ return this.storage.exists(sessionId);
196
+ }
197
+ /**
198
+ * Allocate a new session ID
199
+ */
200
+ allocId() {
201
+ return randomUUID();
202
+ }
203
+ /**
204
+ * Disconnect from Vercel KV
205
+ * Vercel KV uses REST API, so this just clears internal state
206
+ */
207
+ async disconnect() {
208
+ await this.storage.disconnect();
209
+ }
210
+ /**
211
+ * Test Vercel KV connection by checking if we can access the API.
212
+ * Useful for validating connection on startup.
213
+ *
214
+ * @returns true if connection is healthy, false otherwise
215
+ */
216
+ async ping() {
217
+ try {
218
+ await this.ensureConnected();
219
+ return this.storage.ping();
220
+ } catch (error) {
221
+ this.logger?.error("[VercelKvSessionStore] Connection failed", {
222
+ error: error.message
223
+ });
224
+ return false;
225
+ }
226
+ }
227
+ };
228
+ export {
229
+ VercelKvSessionStore
230
+ };
@@ -0,0 +1,17 @@
1
+ // libs/auth/src/shared-server/index.ts
2
+ import {
3
+ createSharedServerNonce,
4
+ createSharedServerSecret,
5
+ deriveSharedServerKeys,
6
+ isSharedServerNonce,
7
+ proveSharedServerIdentity,
8
+ verifySharedServerIdentity
9
+ } from "./shared-server-identity.mjs";
10
+ export {
11
+ createSharedServerNonce,
12
+ createSharedServerSecret,
13
+ deriveSharedServerKeys,
14
+ isSharedServerNonce,
15
+ proveSharedServerIdentity,
16
+ verifySharedServerIdentity
17
+ };
@@ -0,0 +1,55 @@
1
+ // libs/auth/src/shared-server/shared-server-identity.ts
2
+ import {
3
+ base64urlDecode,
4
+ base64urlEncode,
5
+ hkdfSha256,
6
+ hmacSha256,
7
+ randomBytes,
8
+ timingSafeEqual
9
+ } from "@frontmcp/utils";
10
+ var SECRET_BYTES = 32;
11
+ var BASE64URL_32_BYTES = /^[A-Za-z0-9_-]{43}$/;
12
+ var KEY_SALT = "frontmcp shared server";
13
+ var PROOF_LABEL = "frontmcp shared server identity v1";
14
+ var encoder = new TextEncoder();
15
+ function createSharedServerSecret() {
16
+ return base64urlEncode(randomBytes(SECRET_BYTES));
17
+ }
18
+ function createSharedServerNonce() {
19
+ return base64urlEncode(randomBytes(SECRET_BYTES));
20
+ }
21
+ function isBase64url32Bytes(value) {
22
+ return typeof value === "string" && BASE64URL_32_BYTES.test(value);
23
+ }
24
+ function isSharedServerNonce(value) {
25
+ return isBase64url32Bytes(value);
26
+ }
27
+ function deriveSharedServerKeys(secret) {
28
+ if (!isBase64url32Bytes(secret)) throw new TypeError("a shared server secret is 32 bytes of base64url");
29
+ const bytes = base64urlDecode(secret);
30
+ const salt = encoder.encode(KEY_SALT);
31
+ return {
32
+ proofKey: hkdfSha256(bytes, salt, encoder.encode("proof"), 32),
33
+ token: base64urlEncode(hkdfSha256(bytes, salt, encoder.encode("token"), 32))
34
+ };
35
+ }
36
+ function proofBytes(proofKey, { endpoint, nonce, version }) {
37
+ return hmacSha256(proofKey, encoder.encode(JSON.stringify([PROOF_LABEL, endpoint, nonce, version])));
38
+ }
39
+ function proveSharedServerIdentity(proofKey, statement) {
40
+ return base64urlEncode(proofBytes(proofKey, statement));
41
+ }
42
+ function verifySharedServerIdentity(proofKey, statement, proof) {
43
+ if (!isBase64url32Bytes(proof)) return false;
44
+ const expected = proofBytes(proofKey, statement);
45
+ const presented = base64urlDecode(proof);
46
+ return presented.length === expected.length && timingSafeEqual(presented, expected);
47
+ }
48
+ export {
49
+ createSharedServerNonce,
50
+ createSharedServerSecret,
51
+ deriveSharedServerKeys,
52
+ isSharedServerNonce,
53
+ proveSharedServerIdentity,
54
+ verifySharedServerIdentity
55
+ };
@@ -0,0 +1,184 @@
1
+ // libs/auth/src/ui/base-layout.ts
2
+ import { escapeHtml } from "@frontmcp/utils";
3
+ import { escapeHtml as escapeHtml2 } from "@frontmcp/utils";
4
+ var CDN = {
5
+ /** Tailwind CSS v4 Browser CDN - generates styles on-the-fly with @theme support */
6
+ tailwind: "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4",
7
+ /** Google Fonts - Inter for modern UI */
8
+ fonts: {
9
+ preconnect: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
10
+ stylesheet: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
11
+ }
12
+ };
13
+ var BUILT_IN_AUTH_PAGE_CSP = [
14
+ "default-src 'self'",
15
+ "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
16
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
17
+ "font-src 'self' https://fonts.gstatic.com",
18
+ "img-src 'self' data: https:",
19
+ "connect-src 'self'",
20
+ "object-src 'none'",
21
+ "base-uri 'none'",
22
+ "frame-ancestors 'none'"
23
+ ].join("; ");
24
+ function builtInAuthPageHeaders(options = {}) {
25
+ return {
26
+ ...options.csp === false ? {} : { "Content-Security-Policy": BUILT_IN_AUTH_PAGE_CSP },
27
+ "X-Frame-Options": "DENY",
28
+ "X-Content-Type-Options": "nosniff",
29
+ "Referrer-Policy": "same-origin",
30
+ "Cache-Control": "no-store"
31
+ };
32
+ }
33
+ var DEFAULT_THEME = {
34
+ colors: {
35
+ primary: "#3b82f6",
36
+ // blue-500
37
+ "primary-dark": "#2563eb",
38
+ // blue-600
39
+ secondary: "#8b5cf6",
40
+ // violet-500
41
+ accent: "#06b6d4",
42
+ // cyan-500
43
+ success: "#22c55e",
44
+ // green-500
45
+ warning: "#f59e0b",
46
+ // amber-500
47
+ danger: "#ef4444"
48
+ // red-500
49
+ },
50
+ fonts: {
51
+ sans: 'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
52
+ }
53
+ };
54
+ function buildThemeCss(theme) {
55
+ const lines = [];
56
+ if (theme.colors) {
57
+ for (const [key, value] of Object.entries(theme.colors)) {
58
+ if (value) {
59
+ lines.push(`--color-${key}: ${value};`);
60
+ }
61
+ }
62
+ }
63
+ if (theme.fonts) {
64
+ for (const [key, value] of Object.entries(theme.fonts)) {
65
+ if (value) {
66
+ lines.push(`--font-${key}: ${value};`);
67
+ }
68
+ }
69
+ }
70
+ if (theme.customVars) {
71
+ lines.push(theme.customVars);
72
+ }
73
+ return lines.join("\n ");
74
+ }
75
+ function baseLayout(content, options) {
76
+ const {
77
+ title,
78
+ description,
79
+ includeTailwind = true,
80
+ includeFonts = true,
81
+ headExtra = "",
82
+ bodyClass = "bg-gray-50 min-h-screen font-sans antialiased",
83
+ theme
84
+ } = options;
85
+ const mergedTheme = {
86
+ colors: { ...DEFAULT_THEME.colors, ...theme?.colors },
87
+ fonts: { ...DEFAULT_THEME.fonts, ...theme?.fonts },
88
+ customVars: theme?.customVars,
89
+ customCss: theme?.customCss
90
+ };
91
+ const fontPreconnect = includeFonts ? CDN.fonts.preconnect.map((url, i) => `<link rel="preconnect" href="${url}"${i > 0 ? " crossorigin" : ""}>`).join("\n ") : "";
92
+ const fontStylesheet = includeFonts ? `<link href="${CDN.fonts.stylesheet}" rel="stylesheet">` : "";
93
+ const themeCss = buildThemeCss(mergedTheme);
94
+ const customCss = mergedTheme.customCss || "";
95
+ const tailwindBlock = includeTailwind ? `<script src="${CDN.tailwind}"></script>
96
+ <style type="text/tailwindcss">
97
+ @theme {
98
+ ${themeCss}
99
+ }
100
+ ${customCss}
101
+ </style>` : "";
102
+ const metaDescription = description ? `<meta name="description" content="${escapeHtml(description)}">` : "";
103
+ return `<!DOCTYPE html>
104
+ <html lang="en">
105
+ <head>
106
+ <meta charset="UTF-8">
107
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
108
+ <title>${escapeHtml(title)} - FrontMCP</title>
109
+ ${metaDescription}
110
+
111
+ <!-- Google Fonts CDN - Inter (modern UI font) -->
112
+ ${fontPreconnect}
113
+ ${fontStylesheet}
114
+
115
+ <!-- Tailwind CSS v4 Browser CDN with @theme support -->
116
+ ${tailwindBlock}
117
+ ${headExtra}
118
+ </head>
119
+ <body class="${escapeHtml(bodyClass)}">
120
+ ${content}
121
+ </body>
122
+ </html>`;
123
+ }
124
+ function createLayout(defaultOptions) {
125
+ return (content, options) => {
126
+ const mergedTheme = defaultOptions.theme || options.theme ? {
127
+ colors: { ...defaultOptions.theme?.colors, ...options.theme?.colors },
128
+ fonts: { ...defaultOptions.theme?.fonts, ...options.theme?.fonts },
129
+ customVars: options.theme?.customVars ?? defaultOptions.theme?.customVars,
130
+ customCss: options.theme?.customCss ?? defaultOptions.theme?.customCss
131
+ } : void 0;
132
+ return baseLayout(content, {
133
+ ...defaultOptions,
134
+ ...options,
135
+ theme: mergedTheme
136
+ });
137
+ };
138
+ }
139
+ var authLayout = createLayout({
140
+ bodyClass: "bg-gray-50 min-h-screen font-sans antialiased"
141
+ });
142
+ function centeredCardLayout(content, options) {
143
+ const wrappedContent = `
144
+ <div class="min-h-screen flex items-center justify-center p-4">
145
+ <div class="w-full max-w-md">
146
+ ${content}
147
+ </div>
148
+ </div>`;
149
+ return baseLayout(wrappedContent, {
150
+ ...options,
151
+ bodyClass: "bg-gradient-to-br from-primary to-secondary min-h-screen font-sans antialiased"
152
+ });
153
+ }
154
+ function wideLayout(content, options) {
155
+ const wrappedContent = `
156
+ <div class="min-h-screen py-8 px-4">
157
+ <div class="max-w-2xl mx-auto">
158
+ ${content}
159
+ </div>
160
+ </div>`;
161
+ return baseLayout(wrappedContent, options);
162
+ }
163
+ function extraWideLayout(content, options) {
164
+ const wrappedContent = `
165
+ <div class="min-h-screen py-8 px-4">
166
+ <div class="max-w-3xl mx-auto">
167
+ ${content}
168
+ </div>
169
+ </div>`;
170
+ return baseLayout(wrappedContent, options);
171
+ }
172
+ export {
173
+ BUILT_IN_AUTH_PAGE_CSP,
174
+ CDN,
175
+ DEFAULT_THEME,
176
+ authLayout,
177
+ baseLayout,
178
+ builtInAuthPageHeaders,
179
+ centeredCardLayout,
180
+ createLayout,
181
+ escapeHtml2 as escapeHtml,
182
+ extraWideLayout,
183
+ wideLayout
184
+ };