@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,255 @@
1
+ // libs/auth/src/session/redis-session.store.ts
2
+ import { randomUUID, RedisStorageAdapter } from "@frontmcp/utils";
3
+ import {
4
+ storedSessionSchema
5
+ } from "./transport-session.types.mjs";
6
+ import { signSession, verifyOrParseSession } from "./session-crypto.mjs";
7
+ import { SessionRateLimiter } from "./session-rate-limiter.mjs";
8
+ import { SessionIdEmptyError } from "../errors/auth-internal.errors.mjs";
9
+ var RedisSessionStore = class {
10
+ storage;
11
+ keyPrefix;
12
+ defaultTtlMs;
13
+ logger;
14
+ externalInstance = false;
15
+ // Security features
16
+ security;
17
+ rateLimiter;
18
+ constructor(config, logger) {
19
+ this.defaultTtlMs = ("defaultTtlMs" in config ? config.defaultTtlMs : void 0) ?? 36e5;
20
+ this.logger = logger;
21
+ this.keyPrefix = ("keyPrefix" in config ? config.keyPrefix : void 0) ?? "mcp:session:";
22
+ this.security = ("security" in config ? config.security : void 0) ?? {};
23
+ if (this.security.enableRateLimiting) {
24
+ this.rateLimiter = new SessionRateLimiter({
25
+ windowMs: this.security.rateLimiting?.windowMs,
26
+ maxRequests: this.security.rateLimiting?.maxRequests
27
+ });
28
+ }
29
+ if ("redis" in config && config.redis) {
30
+ this.storage = new RedisStorageAdapter({
31
+ client: config.redis,
32
+ keyPrefix: this.keyPrefix
33
+ });
34
+ this.externalInstance = true;
35
+ } else {
36
+ const redisConfig = config;
37
+ this.storage = new RedisStorageAdapter({
38
+ config: {
39
+ host: redisConfig.host,
40
+ port: redisConfig.port ?? 6379,
41
+ password: redisConfig.password,
42
+ db: redisConfig.db ?? 0,
43
+ tls: redisConfig.tls
44
+ },
45
+ keyPrefix: this.keyPrefix
46
+ });
47
+ }
48
+ }
49
+ /**
50
+ * Get the full key for a session ID (without prefix, adapter handles it)
51
+ * @throws Error if sessionId is empty
52
+ */
53
+ validateSessionId(sessionId) {
54
+ if (!sessionId || sessionId.trim() === "") {
55
+ throw new SessionIdEmptyError("RedisSessionStore");
56
+ }
57
+ }
58
+ /**
59
+ * Ensure the storage adapter is connected
60
+ */
61
+ async ensureConnected() {
62
+ await this.storage.connect();
63
+ }
64
+ /**
65
+ * Get a stored session by ID
66
+ *
67
+ * Note: Uses atomic GETEX to extend TTL while reading, preventing race conditions
68
+ * where concurrent readers might resurrect expired sessions.
69
+ *
70
+ * @param sessionId - The session ID to look up
71
+ * @param options - Optional parameters for rate limiting
72
+ * @param options.clientIdentifier - Client identifier (e.g., IP address) for rate limiting.
73
+ * When provided, rate limiting is applied per-client to prevent session enumeration.
74
+ * If not provided, falls back to sessionId which provides DoS protection per-session.
75
+ */
76
+ async get(sessionId, options) {
77
+ this.validateSessionId(sessionId);
78
+ if (this.rateLimiter) {
79
+ const rateLimitKey = options?.clientIdentifier || sessionId;
80
+ const rateLimitResult = this.rateLimiter.check(rateLimitKey);
81
+ if (!rateLimitResult.allowed) {
82
+ this.logger?.warn("[RedisSessionStore] Rate limit exceeded for session lookup", {
83
+ sessionId: sessionId.slice(0, 20),
84
+ clientIdentifier: options?.clientIdentifier ? options.clientIdentifier.slice(0, 20) : void 0,
85
+ retryAfterMs: rateLimitResult.retryAfterMs
86
+ });
87
+ return null;
88
+ }
89
+ }
90
+ await this.ensureConnected();
91
+ let raw;
92
+ const redis = this.storage.getClient();
93
+ const ttlSeconds = Math.ceil(this.defaultTtlMs / 1e3);
94
+ if (redis) {
95
+ try {
96
+ raw = await redis.getex(this.keyPrefix + sessionId, "EX", ttlSeconds);
97
+ } catch {
98
+ raw = await this.storage.get(sessionId);
99
+ if (raw) {
100
+ await this.storage.expire(sessionId, ttlSeconds);
101
+ }
102
+ }
103
+ } else {
104
+ raw = await this.storage.get(sessionId);
105
+ if (raw) {
106
+ await this.storage.expire(sessionId, ttlSeconds);
107
+ }
108
+ }
109
+ if (!raw) return null;
110
+ try {
111
+ let parsed;
112
+ if (this.security.enableSigning) {
113
+ parsed = verifyOrParseSession(raw, { secret: this.security.signingSecret });
114
+ if (!parsed) {
115
+ this.logger?.warn("[RedisSessionStore] Session signature verification failed", {
116
+ sessionId: sessionId.slice(0, 20)
117
+ });
118
+ this.delete(sessionId).catch(() => void 0);
119
+ return null;
120
+ }
121
+ } else {
122
+ parsed = JSON.parse(raw);
123
+ }
124
+ const result = storedSessionSchema.safeParse(parsed);
125
+ if (!result.success) {
126
+ this.logger?.warn("[RedisSessionStore] Invalid session format", {
127
+ sessionId: sessionId.slice(0, 20),
128
+ errors: result.error.issues.slice(0, 3).map((i) => ({ path: i.path, message: i.message }))
129
+ });
130
+ this.delete(sessionId).catch(() => void 0);
131
+ return null;
132
+ }
133
+ const session = result.data;
134
+ if (session.maxLifetimeAt && session.maxLifetimeAt < Date.now()) {
135
+ this.logger?.info("[RedisSessionStore] Session exceeded max lifetime", {
136
+ sessionId: sessionId.slice(0, 20),
137
+ maxLifetimeAt: session.maxLifetimeAt
138
+ });
139
+ await this.delete(sessionId);
140
+ return null;
141
+ }
142
+ if (session.session.expiresAt && session.session.expiresAt < Date.now()) {
143
+ await this.delete(sessionId);
144
+ return null;
145
+ }
146
+ if (session.session.expiresAt) {
147
+ const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());
148
+ if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {
149
+ const boundedTtlSeconds = Math.ceil(ttlMs / 1e3);
150
+ this.storage.expire(sessionId, boundedTtlSeconds).catch((err) => {
151
+ this.logger?.warn("[RedisSessionStore] TTL extension failed", {
152
+ sessionId: sessionId.slice(0, 20),
153
+ error: err.message
154
+ });
155
+ });
156
+ }
157
+ }
158
+ const updatedSession = {
159
+ ...session,
160
+ lastAccessedAt: Date.now()
161
+ };
162
+ return updatedSession;
163
+ } catch (error) {
164
+ this.logger?.warn("[RedisSessionStore] Failed to parse session", {
165
+ sessionId: sessionId.slice(0, 20),
166
+ error: error.message
167
+ });
168
+ this.delete(sessionId).catch(() => void 0);
169
+ return null;
170
+ }
171
+ }
172
+ /**
173
+ * Store a session with optional TTL
174
+ */
175
+ async set(sessionId, session, ttlMs) {
176
+ this.validateSessionId(sessionId);
177
+ await this.ensureConnected();
178
+ let value;
179
+ if (this.security.enableSigning) {
180
+ value = signSession(session, { secret: this.security.signingSecret });
181
+ } else {
182
+ value = JSON.stringify(session);
183
+ }
184
+ if (ttlMs && ttlMs > 0) {
185
+ const ttlSeconds = Math.ceil(ttlMs / 1e3);
186
+ await this.storage.set(sessionId, value, { ttlSeconds });
187
+ } else if (session.session.expiresAt) {
188
+ const ttl = session.session.expiresAt - Date.now();
189
+ if (ttl > 0) {
190
+ const ttlSeconds = Math.ceil(ttl / 1e3);
191
+ await this.storage.set(sessionId, value, { ttlSeconds });
192
+ } else {
193
+ await this.storage.set(sessionId, value);
194
+ }
195
+ } else {
196
+ await this.storage.set(sessionId, value);
197
+ }
198
+ }
199
+ /**
200
+ * Delete a session
201
+ */
202
+ async delete(sessionId) {
203
+ this.validateSessionId(sessionId);
204
+ await this.ensureConnected();
205
+ await this.storage.delete(sessionId);
206
+ }
207
+ /**
208
+ * Check if a session exists
209
+ */
210
+ async exists(sessionId) {
211
+ this.validateSessionId(sessionId);
212
+ await this.ensureConnected();
213
+ return this.storage.exists(sessionId);
214
+ }
215
+ /**
216
+ * Allocate a new session ID
217
+ */
218
+ allocId() {
219
+ return randomUUID();
220
+ }
221
+ /**
222
+ * Disconnect from Redis (only if we created the connection)
223
+ */
224
+ async disconnect() {
225
+ if (!this.externalInstance) {
226
+ await this.storage.disconnect();
227
+ }
228
+ }
229
+ /**
230
+ * Get the underlying Redis client (for advanced use cases)
231
+ */
232
+ getRedisClient() {
233
+ return this.storage.getClient();
234
+ }
235
+ /**
236
+ * Test Redis connection by sending a PING command.
237
+ * Useful for validating connection on startup.
238
+ *
239
+ * @returns true if connection is healthy, false otherwise
240
+ */
241
+ async ping() {
242
+ try {
243
+ await this.ensureConnected();
244
+ return this.storage.ping();
245
+ } catch (error) {
246
+ this.logger?.error("[RedisSessionStore] Connection failed", {
247
+ error: error.message
248
+ });
249
+ return false;
250
+ }
251
+ }
252
+ };
253
+ export {
254
+ RedisSessionStore
255
+ };
@@ -0,0 +1,78 @@
1
+ // libs/auth/src/session/secure-store-accessor.impl.ts
2
+ import { sha256Hex } from "@frontmcp/utils";
3
+ import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
4
+ var GLOBAL_NAMESPACE = "global";
5
+ var SecureStoreAccessorImpl = class {
6
+ backend;
7
+ scope;
8
+ resolveSub;
9
+ resolveSessionId;
10
+ ttlMs;
11
+ logger;
12
+ constructor(deps) {
13
+ this.backend = deps.backend;
14
+ this.scope = deps.scope ?? "user";
15
+ this.resolveSub = deps.resolveSub;
16
+ this.resolveSessionId = deps.resolveSessionId;
17
+ this.ttlMs = deps.ttlMs;
18
+ this.logger = deps.logger ?? noopAuthLogger;
19
+ }
20
+ /**
21
+ * Resolve the namespace for the current request from the configured scope.
22
+ * Returns undefined when the scope's identity is unavailable (anonymous user
23
+ * under `user` scope, or no session under `session` scope), in which case
24
+ * reads return empty and writes are skipped.
25
+ */
26
+ resolveNamespace() {
27
+ switch (this.scope) {
28
+ case "global":
29
+ return GLOBAL_NAMESPACE;
30
+ case "session": {
31
+ const sid = this.resolveSessionId();
32
+ return sid ? `s:${sha256Hex(sid).slice(0, 32)}` : void 0;
33
+ }
34
+ case "user":
35
+ default: {
36
+ const sub = this.resolveSub();
37
+ return sub ? `u:${sha256Hex(sub).slice(0, 32)}` : void 0;
38
+ }
39
+ }
40
+ }
41
+ async get(key) {
42
+ const namespace = this.resolveNamespace();
43
+ if (!namespace) return void 0;
44
+ const raw = await this.backend.get(namespace, key);
45
+ if (raw === null) return void 0;
46
+ try {
47
+ return JSON.parse(raw);
48
+ } catch (err) {
49
+ this.logger.warn(`SecureStore: failed to parse secret "${key}": ${errMsg(err)}`);
50
+ return void 0;
51
+ }
52
+ }
53
+ async set(key, value, options) {
54
+ const namespace = this.resolveNamespace();
55
+ if (!namespace) {
56
+ this.logger.debug(`SecureStore: no ${this.scope} scope for request; skipping set("${key}")`);
57
+ return;
58
+ }
59
+ const ttlMs = options?.ttlMs ?? this.ttlMs;
60
+ await this.backend.set(namespace, key, JSON.stringify(value), ttlMs);
61
+ }
62
+ async delete(key) {
63
+ const namespace = this.resolveNamespace();
64
+ if (!namespace) return false;
65
+ return this.backend.delete(namespace, key);
66
+ }
67
+ async list() {
68
+ const namespace = this.resolveNamespace();
69
+ if (!namespace) return [];
70
+ return this.backend.list(namespace);
71
+ }
72
+ };
73
+ function errMsg(err) {
74
+ return err instanceof Error ? err.message : String(err);
75
+ }
76
+ export {
77
+ SecureStoreAccessorImpl
78
+ };
@@ -0,0 +1,5 @@
1
+ // libs/auth/src/session/secure-store-accessor.ts
2
+ var SECURE_STORE_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:SECURE_STORE_ACCESSOR");
3
+ export {
4
+ SECURE_STORE_ACCESSOR
5
+ };
@@ -0,0 +1,111 @@
1
+ // libs/auth/src/session/secure-store-backends.ts
2
+ import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
3
+ import { VaultEncryption } from "./vault-encryption.mjs";
4
+ var DEFAULT_KEY_PREFIX = "mcp:secret:";
5
+ var EncryptedStorageSecureStoreBackend = class {
6
+ storage;
7
+ encryption;
8
+ keyPrefix;
9
+ logger;
10
+ /** Cache of derived AES keys, keyed by namespace. */
11
+ keyCache = /* @__PURE__ */ new Map();
12
+ constructor(options) {
13
+ this.storage = options.storage;
14
+ this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;
15
+ this.logger = options.logger ?? noopAuthLogger;
16
+ this.encryption = new VaultEncryption({
17
+ pepper: options.pepper,
18
+ hkdfInfo: "frontmcp-secure-store-v1"
19
+ });
20
+ }
21
+ // ============================================
22
+ // Storage-key helpers
23
+ // ============================================
24
+ /** `secret:keys:<namespace>` \u2192 JSON array of key names. */
25
+ keysKey(namespace) {
26
+ return `${this.keyPrefix}keys:${namespace}`;
27
+ }
28
+ /** `secret:data:<namespace>:<key>` \u2192 encrypted record. */
29
+ dataKey(namespace, key) {
30
+ return `${this.keyPrefix}data:${namespace}:${key}`;
31
+ }
32
+ /**
33
+ * Derive (and cache) the AES-256 key for a namespace. The namespace acts as
34
+ * the HKDF salt material, so values are cryptographically isolated per scope.
35
+ */
36
+ deriveKey(namespace) {
37
+ const cached = this.keyCache.get(namespace);
38
+ if (cached) return cached;
39
+ const promise = this.encryption.deriveKey({ jti: namespace, vaultKey: namespace, sub: namespace, iat: 0 });
40
+ this.keyCache.set(namespace, promise);
41
+ return promise;
42
+ }
43
+ setOpts(ttlMs) {
44
+ return ttlMs && ttlMs > 0 ? { ttlSeconds: Math.ceil(ttlMs / 1e3) } : void 0;
45
+ }
46
+ // ============================================
47
+ // SecureStoreBackend
48
+ // ============================================
49
+ async get(namespace, key) {
50
+ const raw = await this.storage.get(this.dataKey(namespace, key));
51
+ if (!raw) return null;
52
+ try {
53
+ const record = JSON.parse(raw);
54
+ const aesKey = await this.deriveKey(namespace);
55
+ return await this.encryption.decrypt(record.enc, aesKey);
56
+ } catch (err) {
57
+ this.logger.warn(`SecureStore: failed to read secret "${key}": ${errMsg(err)}`);
58
+ return null;
59
+ }
60
+ }
61
+ async set(namespace, key, value, ttlMs) {
62
+ const aesKey = await this.deriveKey(namespace);
63
+ const enc = await this.encryption.encrypt(value, aesKey);
64
+ const record = { enc };
65
+ const opts = this.setOpts(ttlMs);
66
+ await this.storage.set(this.dataKey(namespace, key), JSON.stringify(record), opts);
67
+ await this.addKeyToSet(namespace, key, opts);
68
+ }
69
+ async delete(namespace, key) {
70
+ const existed = await this.storage.delete(this.dataKey(namespace, key));
71
+ const keys = await this.readKeySet(namespace);
72
+ const next = keys.filter((k) => k !== key);
73
+ if (next.length !== keys.length) {
74
+ await this.writeKeySet(namespace, next);
75
+ }
76
+ return existed;
77
+ }
78
+ async list(namespace) {
79
+ return this.readKeySet(namespace);
80
+ }
81
+ // ============================================
82
+ // Key-set helpers (the namespace's key SET)
83
+ // ============================================
84
+ async readKeySet(namespace) {
85
+ const raw = await this.storage.get(this.keysKey(namespace));
86
+ if (!raw) return [];
87
+ try {
88
+ const parsed = JSON.parse(raw);
89
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
90
+ } catch {
91
+ return [];
92
+ }
93
+ }
94
+ async writeKeySet(namespace, keys, opts) {
95
+ const unique = [...new Set(keys)];
96
+ await this.storage.set(this.keysKey(namespace), JSON.stringify(unique), opts);
97
+ }
98
+ async addKeyToSet(namespace, key, opts) {
99
+ const keys = await this.readKeySet(namespace);
100
+ if (!keys.includes(key)) {
101
+ keys.push(key);
102
+ await this.writeKeySet(namespace, keys, opts);
103
+ }
104
+ }
105
+ };
106
+ function errMsg(err) {
107
+ return err instanceof Error ? err.message : String(err);
108
+ }
109
+ export {
110
+ EncryptedStorageSecureStoreBackend
111
+ };
@@ -0,0 +1,77 @@
1
+ // libs/auth/src/session/secure-store.factory.ts
2
+ import { MemoryStorageAdapter } from "@frontmcp/utils";
3
+ import { SecureStoreConfigError } from "../errors/auth-internal.errors.mjs";
4
+ import { secureStoreConfigSchema } from "../options/shared.schemas.mjs";
5
+ import { EncryptedStorageSecureStoreBackend } from "./secure-store-backends.mjs";
6
+ import { createTokenStorageAdapter } from "./token-storage.factory.mjs";
7
+ function isCustomBackendConfig(config) {
8
+ return typeof config === "object" && config !== null && "backend" in config && !!config.backend;
9
+ }
10
+ function isSqliteConfig(config) {
11
+ return typeof config === "object" && config !== null && "sqlite" in config && !!config.sqlite;
12
+ }
13
+ function isRedisConfig(config) {
14
+ return typeof config === "object" && config !== null && "redis" in config && !!config.redis;
15
+ }
16
+ function readScope(config) {
17
+ if (typeof config === "object" && config !== null && "scope" in config && config.scope) {
18
+ return config.scope;
19
+ }
20
+ return "user";
21
+ }
22
+ function readTtlMs(config) {
23
+ if (typeof config === "object" && config !== null && "ttlMs" in config) {
24
+ return config.ttlMs;
25
+ }
26
+ return void 0;
27
+ }
28
+ function readPepper(config) {
29
+ if (typeof config === "object" && config !== null && "encryption" in config && config.encryption) {
30
+ return config.encryption.pepper;
31
+ }
32
+ return void 0;
33
+ }
34
+ function toTokenStorageConfig(config) {
35
+ if (isSqliteConfig(config)) {
36
+ return { sqlite: config.sqlite };
37
+ }
38
+ if (isRedisConfig(config)) {
39
+ return { redis: config.redis };
40
+ }
41
+ return "memory";
42
+ }
43
+ function refuseUnusableConfig(config) {
44
+ if (config === void 0) return;
45
+ const issue = secureStoreConfigSchema.safeParse(config).error?.issues[0];
46
+ if (!issue) return;
47
+ const where = issue.path.length > 0 ? `secureStore.${issue.path.map(String).join(".")}: ` : "";
48
+ throw new SecureStoreConfigError(`${where}${issue.message}`);
49
+ }
50
+ async function createSecureStore(options) {
51
+ const { config, pepper, storage, logger } = options;
52
+ refuseUnusableConfig(config);
53
+ const scope = readScope(config);
54
+ const ttlMs = readTtlMs(config);
55
+ if (isCustomBackendConfig(config)) {
56
+ return { backend: config.backend, scope, ttlMs, kind: "custom" };
57
+ }
58
+ const kind = isSqliteConfig(config) ? "sqlite" : isRedisConfig(config) ? "redis" : "memory";
59
+ let adapter;
60
+ if (storage) {
61
+ adapter = storage;
62
+ } else if (kind === "memory") {
63
+ adapter = new MemoryStorageAdapter();
64
+ await adapter.connect();
65
+ } else {
66
+ adapter = await createTokenStorageAdapter(toTokenStorageConfig(config));
67
+ }
68
+ const backend = new EncryptedStorageSecureStoreBackend({
69
+ storage: adapter,
70
+ pepper: readPepper(config) ?? pepper,
71
+ logger
72
+ });
73
+ return { backend, scope, ttlMs, kind, ...storage ? {} : { close: () => adapter.disconnect() } };
74
+ }
75
+ export {
76
+ createSecureStore
77
+ };
@@ -0,0 +1,18 @@
1
+ // libs/auth/src/session/session-credential-vault.factory.ts
2
+ import { SessionCredentialVault } from "./session-credential-vault.mjs";
3
+ import { createTokenStorageAdapter } from "./token-storage.factory.mjs";
4
+ async function createSessionCredentialVault(options) {
5
+ const storage = options.storage ?? await createTokenStorageAdapter(options.tokenStorage);
6
+ const vault = new SessionCredentialVault({
7
+ storage,
8
+ pepper: options.pepper,
9
+ keyPrefix: options.keyPrefix,
10
+ ttlMs: options.ttlMs,
11
+ logger: options.logger
12
+ });
13
+ const type = options.tokenStorage === void 0 || options.tokenStorage === "memory" ? "memory" : "sqlite" in options.tokenStorage ? "sqlite" : "redis";
14
+ return { vault, type };
15
+ }
16
+ export {
17
+ createSessionCredentialVault
18
+ };