@frontmcp/auth 1.10.16 → 1.11.0-rc.2

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,405 @@
1
+ // libs/auth/src/session/encrypted-authorization-vault.ts
2
+ import { internalZ as z } from "@frontmcp/lazy-zod";
3
+ import { AsyncLocalStorage, randomUUID } from "@frontmcp/utils";
4
+ import { EncryptionContextNotSetError, VaultLoadError, VaultNotFoundError } from "../errors/auth-internal.errors.mjs";
5
+ import { encryptedDataSchema, VaultEncryption } from "./vault-encryption.mjs";
6
+ var redisVaultEntrySchema = z.object({
7
+ /** Vault ID */
8
+ id: z.string(),
9
+ /** User sub (for lookup) */
10
+ userSub: z.string(),
11
+ /** User email (optional, for display) */
12
+ userEmail: z.string().optional(),
13
+ /** User name (optional, for display) */
14
+ userName: z.string().optional(),
15
+ /** Client ID */
16
+ clientId: z.string(),
17
+ /** Creation timestamp */
18
+ createdAt: z.number(),
19
+ /** Last access timestamp */
20
+ lastAccessAt: z.number(),
21
+ /** Authorized app IDs (unencrypted for quick auth checks) */
22
+ authorizedAppIds: z.array(z.string()),
23
+ /** Skipped app IDs (unencrypted for quick checks) */
24
+ skippedAppIds: z.array(z.string()),
25
+ /** Pending auth request IDs (unencrypted for lookup) */
26
+ pendingAuthIds: z.array(z.string()),
27
+ /** Encrypted sensitive data blob */
28
+ encrypted: encryptedDataSchema
29
+ });
30
+ var encryptionContextStorage = new AsyncLocalStorage();
31
+ var EncryptedRedisVault = class {
32
+ constructor(redis, encryption, namespace = "vault:") {
33
+ this.redis = redis;
34
+ this.encryption = encryption;
35
+ this.namespace = namespace;
36
+ }
37
+ redis;
38
+ encryption;
39
+ namespace;
40
+ /**
41
+ * Run a callback with encryption context set for the current async scope.
42
+ * This is the recommended way to set encryption context as it is safe for
43
+ * concurrent requests (each request gets its own isolated context).
44
+ *
45
+ * @param context - Encryption context with key and vaultId
46
+ * @param fn - Async function to run with the context
47
+ * @returns The result of the callback
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const result = await vault.runWithContext({ key, vaultId }, async () => {
52
+ * await vault.get(id);
53
+ * await vault.update(id, data);
54
+ * return 'done';
55
+ * });
56
+ * ```
57
+ */
58
+ runWithContext(context, fn) {
59
+ return encryptionContextStorage.run(context, fn);
60
+ }
61
+ /**
62
+ * Get current encryption key from AsyncLocalStorage.
63
+ */
64
+ getKey() {
65
+ const asyncContext = encryptionContextStorage.getStore();
66
+ if (asyncContext) {
67
+ return asyncContext.key;
68
+ }
69
+ throw new EncryptionContextNotSetError();
70
+ }
71
+ /**
72
+ * Create Redis key from vault ID
73
+ */
74
+ redisKey(id) {
75
+ return `${this.namespace}${id}`;
76
+ }
77
+ /**
78
+ * Create credential key from appId and providerId
79
+ */
80
+ credentialKey(appId, providerId) {
81
+ return `${appId}:${providerId}`;
82
+ }
83
+ /**
84
+ * Encrypt sensitive data
85
+ */
86
+ async encryptSensitive(data) {
87
+ return this.encryption.encryptObject(data, this.getKey());
88
+ }
89
+ /**
90
+ * Decrypt sensitive data
91
+ */
92
+ async decryptSensitive(encrypted) {
93
+ return this.encryption.decryptObject(encrypted, this.getKey());
94
+ }
95
+ /**
96
+ * Convert Redis entry to full vault entry (decrypts sensitive data)
97
+ */
98
+ async toVaultEntry(redisEntry) {
99
+ const sensitive = await this.decryptSensitive(redisEntry.encrypted);
100
+ return {
101
+ id: redisEntry.id,
102
+ userSub: redisEntry.userSub,
103
+ userEmail: redisEntry.userEmail,
104
+ userName: redisEntry.userName,
105
+ clientId: redisEntry.clientId,
106
+ createdAt: redisEntry.createdAt,
107
+ lastAccessAt: redisEntry.lastAccessAt,
108
+ appCredentials: sensitive.appCredentials,
109
+ consent: sensitive.consent,
110
+ federated: sensitive.federated,
111
+ pendingAuths: sensitive.pendingAuths,
112
+ authorizedAppIds: redisEntry.authorizedAppIds,
113
+ skippedAppIds: redisEntry.skippedAppIds
114
+ };
115
+ }
116
+ /**
117
+ * Convert vault entry to Redis entry (encrypts sensitive data)
118
+ */
119
+ async toRedisEntry(entry) {
120
+ const sensitive = {
121
+ appCredentials: entry.appCredentials,
122
+ consent: entry.consent,
123
+ federated: entry.federated,
124
+ pendingAuths: entry.pendingAuths
125
+ };
126
+ return {
127
+ id: entry.id,
128
+ userSub: entry.userSub,
129
+ userEmail: entry.userEmail,
130
+ userName: entry.userName,
131
+ clientId: entry.clientId,
132
+ createdAt: entry.createdAt,
133
+ lastAccessAt: entry.lastAccessAt,
134
+ authorizedAppIds: entry.authorizedAppIds,
135
+ skippedAppIds: entry.skippedAppIds,
136
+ pendingAuthIds: entry.pendingAuths.map((p) => p.id),
137
+ encrypted: await this.encryptSensitive(sensitive)
138
+ };
139
+ }
140
+ /**
141
+ * Save entry to Redis
142
+ */
143
+ async saveEntry(entry) {
144
+ const redisEntry = await this.toRedisEntry(entry);
145
+ await this.redis.set(this.redisKey(entry.id), JSON.stringify(redisEntry));
146
+ }
147
+ /**
148
+ * Load entry from Redis
149
+ */
150
+ async loadEntry(id) {
151
+ const data = await this.redis.get(this.redisKey(id));
152
+ if (!data) return null;
153
+ try {
154
+ const redisEntry = redisVaultEntrySchema.parse(JSON.parse(data));
155
+ return await this.toVaultEntry(redisEntry);
156
+ } catch (error) {
157
+ throw new VaultLoadError(id, error instanceof Error ? error : void 0);
158
+ }
159
+ }
160
+ // ============================================
161
+ // AuthorizationVault Interface Implementation
162
+ // ============================================
163
+ async create(params) {
164
+ const now = Date.now();
165
+ const entry = {
166
+ id: randomUUID(),
167
+ userSub: params.userSub,
168
+ userEmail: params.userEmail,
169
+ userName: params.userName,
170
+ clientId: params.clientId,
171
+ createdAt: now,
172
+ lastAccessAt: now,
173
+ appCredentials: {},
174
+ consent: params.consent,
175
+ federated: params.federated,
176
+ pendingAuths: [],
177
+ authorizedAppIds: params.authorizedAppIds ?? [],
178
+ skippedAppIds: params.skippedAppIds ?? []
179
+ };
180
+ await this.saveEntry(entry);
181
+ return entry;
182
+ }
183
+ async get(id) {
184
+ const entry = await this.loadEntry(id);
185
+ if (!entry) return null;
186
+ entry.lastAccessAt = Date.now();
187
+ await this.saveEntry(entry);
188
+ return entry;
189
+ }
190
+ async update(id, updates) {
191
+ const entry = await this.loadEntry(id);
192
+ if (!entry) {
193
+ throw new VaultNotFoundError("Vault entry", id);
194
+ }
195
+ Object.assign(entry, updates, { lastAccessAt: Date.now() });
196
+ await this.saveEntry(entry);
197
+ }
198
+ async delete(id) {
199
+ await this.redis.del(this.redisKey(id));
200
+ }
201
+ async updateConsent(vaultId, consent) {
202
+ const entry = await this.loadEntry(vaultId);
203
+ if (!entry) return;
204
+ entry.consent = consent;
205
+ entry.lastAccessAt = Date.now();
206
+ await this.saveEntry(entry);
207
+ }
208
+ async authorizeApp(vaultId, appId) {
209
+ const entry = await this.loadEntry(vaultId);
210
+ if (!entry) return;
211
+ entry.skippedAppIds = entry.skippedAppIds.filter((id) => id !== appId);
212
+ if (!entry.authorizedAppIds.includes(appId)) {
213
+ entry.authorizedAppIds.push(appId);
214
+ }
215
+ entry.lastAccessAt = Date.now();
216
+ await this.saveEntry(entry);
217
+ }
218
+ async createPendingAuth(vaultId, params) {
219
+ const entry = await this.loadEntry(vaultId);
220
+ if (!entry) {
221
+ throw new VaultNotFoundError("Vault", vaultId);
222
+ }
223
+ const now = Date.now();
224
+ const pendingAuth = {
225
+ id: randomUUID(),
226
+ appId: params.appId,
227
+ toolId: params.toolId,
228
+ authUrl: params.authUrl,
229
+ requiredScopes: params.requiredScopes,
230
+ elicitId: params.elicitId,
231
+ createdAt: now,
232
+ expiresAt: now + (params.ttlMs ?? 10 * 60 * 1e3),
233
+ status: "pending"
234
+ };
235
+ entry.pendingAuths.push(pendingAuth);
236
+ entry.lastAccessAt = now;
237
+ await this.saveEntry(entry);
238
+ return pendingAuth;
239
+ }
240
+ async getPendingAuth(vaultId, pendingAuthId) {
241
+ const entry = await this.loadEntry(vaultId);
242
+ if (!entry) return null;
243
+ const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
244
+ if (!pendingAuth) return null;
245
+ if (Date.now() > pendingAuth.expiresAt && pendingAuth.status === "pending") {
246
+ pendingAuth.status = "expired";
247
+ await this.saveEntry(entry);
248
+ }
249
+ return pendingAuth;
250
+ }
251
+ async completePendingAuth(vaultId, pendingAuthId) {
252
+ const entry = await this.loadEntry(vaultId);
253
+ if (!entry) return;
254
+ const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
255
+ if (pendingAuth) {
256
+ pendingAuth.status = "completed";
257
+ entry.skippedAppIds = entry.skippedAppIds.filter((id) => id !== pendingAuth.appId);
258
+ if (!entry.authorizedAppIds.includes(pendingAuth.appId)) {
259
+ entry.authorizedAppIds.push(pendingAuth.appId);
260
+ }
261
+ entry.lastAccessAt = Date.now();
262
+ await this.saveEntry(entry);
263
+ }
264
+ }
265
+ async cancelPendingAuth(vaultId, pendingAuthId) {
266
+ const entry = await this.loadEntry(vaultId);
267
+ if (!entry) return;
268
+ const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
269
+ if (pendingAuth) {
270
+ pendingAuth.status = "cancelled";
271
+ await this.saveEntry(entry);
272
+ }
273
+ }
274
+ async isAppAuthorized(vaultId, appId) {
275
+ const data = await this.redis.get(this.redisKey(vaultId));
276
+ if (!data) return false;
277
+ try {
278
+ const parsed = JSON.parse(data);
279
+ return Array.isArray(parsed.authorizedAppIds) && parsed.authorizedAppIds.includes(appId);
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+ async getPendingAuths(vaultId) {
285
+ const entry = await this.loadEntry(vaultId);
286
+ if (!entry) return [];
287
+ const now = Date.now();
288
+ let updated = false;
289
+ const pending = entry.pendingAuths.filter((p) => {
290
+ if (now > p.expiresAt && p.status === "pending") {
291
+ p.status = "expired";
292
+ updated = true;
293
+ }
294
+ return p.status === "pending";
295
+ });
296
+ if (updated) {
297
+ await this.saveEntry(entry);
298
+ }
299
+ return pending;
300
+ }
301
+ // ============================================
302
+ // App Credential Methods
303
+ // ============================================
304
+ async addAppCredential(vaultId, credential) {
305
+ const entry = await this.loadEntry(vaultId);
306
+ if (!entry) return;
307
+ const shouldStore = await this.shouldStoreCredential(vaultId, credential.appId);
308
+ if (!shouldStore) return;
309
+ const key = this.credentialKey(credential.appId, credential.providerId);
310
+ entry.appCredentials[key] = credential;
311
+ entry.lastAccessAt = Date.now();
312
+ await this.saveEntry(entry);
313
+ }
314
+ async removeAppCredential(vaultId, appId, providerId) {
315
+ const entry = await this.loadEntry(vaultId);
316
+ if (!entry) return;
317
+ const key = this.credentialKey(appId, providerId);
318
+ delete entry.appCredentials[key];
319
+ entry.lastAccessAt = Date.now();
320
+ await this.saveEntry(entry);
321
+ }
322
+ async getAppCredentials(vaultId, appId) {
323
+ const entry = await this.loadEntry(vaultId);
324
+ if (!entry) return [];
325
+ const prefix = `${appId}:`;
326
+ return Object.entries(entry.appCredentials).filter(([key]) => key.startsWith(prefix)).map(([, cred]) => cred);
327
+ }
328
+ async getCredential(vaultId, appId, providerId) {
329
+ const entry = await this.loadEntry(vaultId);
330
+ if (!entry) return null;
331
+ const key = this.credentialKey(appId, providerId);
332
+ return entry.appCredentials[key] ?? null;
333
+ }
334
+ async getAllCredentials(vaultId, filterByConsent = false) {
335
+ const entry = await this.loadEntry(vaultId);
336
+ if (!entry) return [];
337
+ const allCredentials = Object.values(entry.appCredentials);
338
+ if (!filterByConsent || !entry.consent?.enabled) {
339
+ return allCredentials;
340
+ }
341
+ const consentedToolIds = new Set(entry.consent.selectedToolIds);
342
+ return allCredentials.filter((cred) => {
343
+ return Array.from(consentedToolIds).some((toolId) => toolId.startsWith(`${cred.appId}:`));
344
+ });
345
+ }
346
+ async updateCredential(vaultId, appId, providerId, updates) {
347
+ const entry = await this.loadEntry(vaultId);
348
+ if (!entry) return;
349
+ const key = this.credentialKey(appId, providerId);
350
+ const credential = entry.appCredentials[key];
351
+ if (!credential) return;
352
+ Object.assign(credential, updates);
353
+ entry.lastAccessAt = Date.now();
354
+ await this.saveEntry(entry);
355
+ }
356
+ async shouldStoreCredential(vaultId, appId, toolIds) {
357
+ const entry = await this.loadEntry(vaultId);
358
+ if (!entry) return false;
359
+ if (!entry.consent?.enabled) {
360
+ return true;
361
+ }
362
+ if (toolIds && toolIds.length > 0) {
363
+ return toolIds.some((toolId) => entry.consent.selectedToolIds.includes(toolId));
364
+ }
365
+ const consentedToolIds = entry.consent.selectedToolIds;
366
+ return consentedToolIds.some((toolId) => toolId.startsWith(`${appId}:`));
367
+ }
368
+ async invalidateCredential(vaultId, appId, providerId, reason) {
369
+ await this.updateCredential(vaultId, appId, providerId, {
370
+ isValid: false,
371
+ invalidReason: reason
372
+ });
373
+ }
374
+ async refreshOAuthCredential(vaultId, appId, providerId, tokens) {
375
+ const entry = await this.loadEntry(vaultId);
376
+ if (!entry) return;
377
+ const key = this.credentialKey(appId, providerId);
378
+ const credential = entry.appCredentials[key];
379
+ if (!credential || credential.credential.type !== "oauth") return;
380
+ credential.credential.accessToken = tokens.accessToken;
381
+ if (tokens.refreshToken !== void 0) {
382
+ credential.credential.refreshToken = tokens.refreshToken;
383
+ }
384
+ if (tokens.expiresAt !== void 0) {
385
+ credential.credential.expiresAt = tokens.expiresAt;
386
+ credential.expiresAt = tokens.expiresAt;
387
+ }
388
+ credential.isValid = true;
389
+ credential.invalidReason = void 0;
390
+ entry.lastAccessAt = Date.now();
391
+ await this.saveEntry(entry);
392
+ }
393
+ async cleanup() {
394
+ }
395
+ };
396
+ function createEncryptedVault(redis, config = {}) {
397
+ const encryption = new VaultEncryption({ pepper: config.pepper });
398
+ const vault = new EncryptedRedisVault(redis, encryption, config.namespace);
399
+ return { vault, encryption };
400
+ }
401
+ export {
402
+ EncryptedRedisVault,
403
+ createEncryptedVault,
404
+ redisVaultEntrySchema
405
+ };
@@ -0,0 +1,176 @@
1
+ // libs/auth/src/session/federated-auth.session.ts
2
+ import { randomUUID } from "@frontmcp/utils";
3
+ import { AuthFlowError } from "../errors/auth-internal.errors.mjs";
4
+ function toSessionRecord(session) {
5
+ return {
6
+ ...session,
7
+ completedProviders: Array.from(session.completedProviders.entries())
8
+ };
9
+ }
10
+ function fromSessionRecord(record) {
11
+ return {
12
+ ...record,
13
+ completedProviders: new Map(record.completedProviders)
14
+ };
15
+ }
16
+ var InMemoryFederatedAuthSessionStore = class {
17
+ sessions = /* @__PURE__ */ new Map();
18
+ /** Default TTL for sessions (15 minutes) */
19
+ sessionTtlMs = 15 * 60 * 1e3;
20
+ /** Cleanup interval timer */
21
+ cleanupTimer;
22
+ constructor() {
23
+ this.cleanupTimer = setInterval(() => {
24
+ void this.cleanup();
25
+ }, 6e4);
26
+ if (this.cleanupTimer.unref) {
27
+ this.cleanupTimer.unref();
28
+ }
29
+ }
30
+ async store(session) {
31
+ const record = toSessionRecord(session);
32
+ this.sessions.set(session.id, record);
33
+ }
34
+ async get(id) {
35
+ const record = this.sessions.get(id);
36
+ if (!record) {
37
+ return null;
38
+ }
39
+ if (Date.now() > record.expiresAt) {
40
+ this.sessions.delete(id);
41
+ return null;
42
+ }
43
+ return fromSessionRecord(record);
44
+ }
45
+ async delete(id) {
46
+ this.sessions.delete(id);
47
+ }
48
+ async update(session) {
49
+ const record = toSessionRecord(session);
50
+ this.sessions.set(session.id, record);
51
+ }
52
+ /**
53
+ * Clean up expired sessions
54
+ */
55
+ async cleanup() {
56
+ const now = Date.now();
57
+ for (const [id, record] of this.sessions) {
58
+ if (now > record.expiresAt) {
59
+ this.sessions.delete(id);
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * Stop the cleanup timer
65
+ */
66
+ dispose() {
67
+ if (this.cleanupTimer) {
68
+ clearInterval(this.cleanupTimer);
69
+ this.cleanupTimer = void 0;
70
+ }
71
+ }
72
+ /**
73
+ * Create a new federated auth session
74
+ */
75
+ createSession(params) {
76
+ const now = Date.now();
77
+ return {
78
+ id: randomUUID(),
79
+ pendingAuthId: params.pendingAuthId,
80
+ clientId: params.clientId,
81
+ redirectUri: params.redirectUri,
82
+ scopes: params.scopes,
83
+ state: params.state,
84
+ resource: params.resource,
85
+ userInfo: params.userInfo,
86
+ frontmcpPkce: params.frontmcpPkce,
87
+ providerQueue: [...params.providerIds],
88
+ completedProviders: /* @__PURE__ */ new Map(),
89
+ skippedProviders: [],
90
+ signinBinding: params.signinBinding,
91
+ createdAt: now,
92
+ expiresAt: now + this.sessionTtlMs
93
+ };
94
+ }
95
+ /**
96
+ * Get count (for testing/monitoring)
97
+ */
98
+ get size() {
99
+ return this.sessions.size;
100
+ }
101
+ /**
102
+ * Clear all sessions (for testing)
103
+ */
104
+ clear() {
105
+ this.sessions.clear();
106
+ }
107
+ };
108
+ function createFederatedAuthSession(params, ttlMs = 15 * 60 * 1e3) {
109
+ const now = Date.now();
110
+ return {
111
+ id: randomUUID(),
112
+ pendingAuthId: params.pendingAuthId,
113
+ clientId: params.clientId,
114
+ redirectUri: params.redirectUri,
115
+ scopes: params.scopes,
116
+ state: params.state,
117
+ resource: params.resource,
118
+ userInfo: params.userInfo,
119
+ frontmcpPkce: params.frontmcpPkce,
120
+ providerQueue: [...params.providerIds],
121
+ completedProviders: /* @__PURE__ */ new Map(),
122
+ skippedProviders: [],
123
+ signinBinding: params.signinBinding,
124
+ createdAt: now,
125
+ expiresAt: now + ttlMs
126
+ };
127
+ }
128
+ function isSessionComplete(session) {
129
+ return session.providerQueue.length === 0 && !session.currentProviderId;
130
+ }
131
+ function getNextProvider(session) {
132
+ if (session.currentProviderId) {
133
+ return session.currentProviderId;
134
+ }
135
+ return session.providerQueue[0];
136
+ }
137
+ function completeCurrentProvider(session, tokens, userInfo) {
138
+ if (!session.currentProviderId) {
139
+ throw new AuthFlowError("No current provider to complete", "AUTH_FLOW_ERROR");
140
+ }
141
+ session.completedProviders.set(session.currentProviderId, {
142
+ providerId: session.currentProviderId,
143
+ tokens,
144
+ userInfo,
145
+ completedAt: Date.now()
146
+ });
147
+ session.currentProviderId = void 0;
148
+ session.currentProviderPkce = void 0;
149
+ session.currentProviderState = void 0;
150
+ }
151
+ function startNextProvider(session, pkce, state) {
152
+ if (session.currentProviderId) {
153
+ throw new AuthFlowError("Cannot start next provider while current is in progress", "AUTH_FLOW_ERROR");
154
+ }
155
+ if (session.providerQueue.length === 0) {
156
+ throw new AuthFlowError("No more providers in queue", "AUTH_FLOW_ERROR");
157
+ }
158
+ const providerId = session.providerQueue.shift();
159
+ if (!providerId) {
160
+ throw new AuthFlowError("No more providers in queue", "AUTH_FLOW_ERROR");
161
+ }
162
+ session.currentProviderId = providerId;
163
+ session.currentProviderPkce = pkce;
164
+ session.currentProviderState = state;
165
+ return providerId;
166
+ }
167
+ export {
168
+ InMemoryFederatedAuthSessionStore,
169
+ completeCurrentProvider,
170
+ createFederatedAuthSession,
171
+ fromSessionRecord,
172
+ getNextProvider,
173
+ isSessionComplete,
174
+ startNextProvider,
175
+ toSessionRecord
176
+ };
@@ -0,0 +1,42 @@
1
+ // libs/auth/src/session/incremental-auth-ticket.ts
2
+ import { randomUUID, signData, verifyData } from "@frontmcp/utils";
3
+ var DEFAULT_INCREMENTAL_TICKET_TTL_MS = 3e5;
4
+ function signIncrementalAuthTicket(input, secret) {
5
+ const payload = {
6
+ sub: input.sub,
7
+ appId: input.appId,
8
+ ...input.toolId !== void 0 ? { toolId: input.toolId } : {},
9
+ ...input.priorAppIds !== void 0 ? { priorAppIds: input.priorAppIds } : {},
10
+ jti: randomUUID(),
11
+ exp: Date.now() + (input.ttlMs ?? DEFAULT_INCREMENTAL_TICKET_TTL_MS)
12
+ };
13
+ const signedJson = signData(payload, { secret });
14
+ return Buffer.from(signedJson, "utf8").toString("base64url");
15
+ }
16
+ function verifyIncrementalAuthTicket(token, secret, now = Date.now()) {
17
+ let signedJson;
18
+ try {
19
+ signedJson = Buffer.from(token, "base64url").toString("utf8");
20
+ } catch {
21
+ return null;
22
+ }
23
+ const payload = verifyData(signedJson, { secret });
24
+ if (!payload) return null;
25
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) return null;
26
+ if (typeof payload.appId !== "string" || payload.appId.length === 0) return null;
27
+ if (typeof payload.jti !== "string" || payload.jti.length === 0) return null;
28
+ if (!Number.isFinite(payload.exp)) return null;
29
+ if (payload.toolId !== void 0 && typeof payload.toolId !== "string") return null;
30
+ if (payload.priorAppIds !== void 0 && (!Array.isArray(payload.priorAppIds) || payload.priorAppIds.some((id) => typeof id !== "string"))) {
31
+ return null;
32
+ }
33
+ if (now >= payload.exp) {
34
+ return null;
35
+ }
36
+ return payload;
37
+ }
38
+ export {
39
+ DEFAULT_INCREMENTAL_TICKET_TTL_MS,
40
+ signIncrementalAuthTicket,
41
+ verifyIncrementalAuthTicket
42
+ };