@frontmcp/auth 1.9.2 → 1.9.3
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.
- package/authorization/orchestrated.authorization.d.ts +10 -2
- package/authorization/orchestrated.authorization.d.ts.map +1 -1
- package/cimd/cimd.service.d.ts +13 -1
- package/cimd/cimd.service.d.ts.map +1 -1
- package/esm/index.mjs +457 -103
- package/esm/package.json +5 -5
- package/index.js +457 -108
- package/jwks/jwks.service.d.ts +15 -0
- package/jwks/jwks.service.d.ts.map +1 -1
- package/package.json +5 -5
- package/session/authorization.store.d.ts +6 -0
- package/session/authorization.store.d.ts.map +1 -1
- package/session/orchestrated-token.crypto.d.ts +11 -0
- package/session/orchestrated-token.crypto.d.ts.map +1 -1
- package/session/orchestrated-token.store.d.ts +16 -1
- package/session/orchestrated-token.store.d.ts.map +1 -1
- package/session/storage-orchestrated-token.store.d.ts +10 -0
- package/session/storage-orchestrated-token.store.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -25,6 +25,245 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
25
25
|
};
|
|
26
26
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
27
|
|
|
28
|
+
// libs/auth/src/cimd/cimd-redis.cache.ts
|
|
29
|
+
var cimd_redis_cache_exports = {};
|
|
30
|
+
__export(cimd_redis_cache_exports, {
|
|
31
|
+
RedisCimdCache: () => RedisCimdCache
|
|
32
|
+
});
|
|
33
|
+
var import_utils43, import_utils44, RedisCimdCache;
|
|
34
|
+
var init_cimd_redis_cache = __esm({
|
|
35
|
+
"libs/auth/src/cimd/cimd-redis.cache.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
import_utils43 = require("@frontmcp/utils");
|
|
38
|
+
import_utils44 = require("@frontmcp/utils");
|
|
39
|
+
init_cimd_cache();
|
|
40
|
+
RedisCimdCache = class {
|
|
41
|
+
redis;
|
|
42
|
+
keyPrefix;
|
|
43
|
+
config;
|
|
44
|
+
constructor(config) {
|
|
45
|
+
if (!config.redis) {
|
|
46
|
+
throw new Error("Redis configuration is required for RedisCimdCache");
|
|
47
|
+
}
|
|
48
|
+
this.config = {
|
|
49
|
+
defaultTtlMs: config.defaultTtlMs ?? 36e5,
|
|
50
|
+
maxTtlMs: config.maxTtlMs ?? 864e5,
|
|
51
|
+
minTtlMs: config.minTtlMs ?? 6e4
|
|
52
|
+
};
|
|
53
|
+
this.keyPrefix = config.redis.keyPrefix ?? "cimd:";
|
|
54
|
+
const redisOptions = {};
|
|
55
|
+
if (config.redis.url) {
|
|
56
|
+
redisOptions.url = config.redis.url;
|
|
57
|
+
} else if (config.redis.host) {
|
|
58
|
+
redisOptions.config = {
|
|
59
|
+
host: config.redis.host,
|
|
60
|
+
port: config.redis.port,
|
|
61
|
+
password: config.redis.password,
|
|
62
|
+
db: config.redis.db,
|
|
63
|
+
tls: config.redis.tls
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
this.redis = new import_utils43.RedisStorageAdapter(redisOptions);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Connect to Redis.
|
|
70
|
+
* Must be called before using any cache operations.
|
|
71
|
+
*/
|
|
72
|
+
async connect() {
|
|
73
|
+
await this.redis.connect();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Generate a Redis key for a client ID.
|
|
77
|
+
* Uses SHA-256 hash to handle URLs with special characters.
|
|
78
|
+
*/
|
|
79
|
+
cacheKey(clientId) {
|
|
80
|
+
const hash = (0, import_utils44.sha256Hex)(clientId);
|
|
81
|
+
return `${this.keyPrefix}${hash}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get a cached entry by client_id.
|
|
85
|
+
*
|
|
86
|
+
* @param clientId - The client_id URL
|
|
87
|
+
* @returns The cached entry if valid, or undefined
|
|
88
|
+
*/
|
|
89
|
+
async get(clientId) {
|
|
90
|
+
const key = this.cacheKey(clientId);
|
|
91
|
+
const data = await this.redis.get(key);
|
|
92
|
+
if (!data) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const entry = JSON.parse(data);
|
|
97
|
+
if (entry.expiresAt < Date.now()) {
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
return entry;
|
|
101
|
+
} catch {
|
|
102
|
+
await this.redis.delete(key);
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get a stale entry for conditional revalidation.
|
|
108
|
+
*
|
|
109
|
+
* @param clientId - The client_id URL
|
|
110
|
+
* @returns The stale entry (even if expired), or undefined if not cached
|
|
111
|
+
*/
|
|
112
|
+
async getStale(clientId) {
|
|
113
|
+
const key = this.cacheKey(clientId);
|
|
114
|
+
const data = await this.redis.get(key);
|
|
115
|
+
if (!data) {
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(data);
|
|
120
|
+
} catch {
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Store a document in the cache.
|
|
126
|
+
*
|
|
127
|
+
* @param clientId - The client_id URL
|
|
128
|
+
* @param document - The metadata document
|
|
129
|
+
* @param headers - HTTP response headers
|
|
130
|
+
*/
|
|
131
|
+
async set(clientId, document, headers) {
|
|
132
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
133
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const entry = {
|
|
136
|
+
document,
|
|
137
|
+
expiresAt: now + ttlMs,
|
|
138
|
+
etag,
|
|
139
|
+
lastModified,
|
|
140
|
+
cachedAt: now
|
|
141
|
+
};
|
|
142
|
+
const key = this.cacheKey(clientId);
|
|
143
|
+
const value = JSON.stringify(entry);
|
|
144
|
+
const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
|
|
145
|
+
await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Update an existing cache entry (after 304 Not Modified).
|
|
149
|
+
*
|
|
150
|
+
* @param clientId - The client_id URL
|
|
151
|
+
* @param headers - New HTTP headers with updated cache directives
|
|
152
|
+
*/
|
|
153
|
+
async revalidate(clientId, headers) {
|
|
154
|
+
const key = this.cacheKey(clientId);
|
|
155
|
+
const data = await this.redis.get(key);
|
|
156
|
+
if (!data) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const existing = JSON.parse(data);
|
|
161
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
162
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
163
|
+
existing.expiresAt = Date.now() + ttlMs;
|
|
164
|
+
if (etag) existing.etag = etag;
|
|
165
|
+
if (lastModified) existing.lastModified = lastModified;
|
|
166
|
+
const value = JSON.stringify(existing);
|
|
167
|
+
const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
|
|
168
|
+
await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Delete a cache entry.
|
|
176
|
+
*
|
|
177
|
+
* @param clientId - The client_id URL
|
|
178
|
+
* @returns true if an entry was deleted
|
|
179
|
+
*/
|
|
180
|
+
async delete(clientId) {
|
|
181
|
+
const key = this.cacheKey(clientId);
|
|
182
|
+
return this.redis.delete(key);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get conditional request headers for a cached entry.
|
|
186
|
+
*
|
|
187
|
+
* @param clientId - The client_id URL
|
|
188
|
+
* @returns Headers for conditional request, or undefined if not cached
|
|
189
|
+
*/
|
|
190
|
+
async getConditionalHeaders(clientId) {
|
|
191
|
+
const key = this.cacheKey(clientId);
|
|
192
|
+
const data = await this.redis.get(key);
|
|
193
|
+
if (!data) {
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const entry = JSON.parse(data);
|
|
198
|
+
const headers = {};
|
|
199
|
+
if (entry.etag) {
|
|
200
|
+
headers["If-None-Match"] = entry.etag;
|
|
201
|
+
}
|
|
202
|
+
if (entry.lastModified) {
|
|
203
|
+
headers["If-Modified-Since"] = entry.lastModified;
|
|
204
|
+
}
|
|
205
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
206
|
+
} catch {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Clear all cached entries.
|
|
212
|
+
* Uses Redis SCAN to find and delete all keys with our prefix.
|
|
213
|
+
*/
|
|
214
|
+
async clear() {
|
|
215
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
216
|
+
if (keys.length > 0) {
|
|
217
|
+
await this.redis.mdelete(keys);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get the number of cached entries.
|
|
222
|
+
* Uses Redis SCAN to count keys with our prefix.
|
|
223
|
+
*/
|
|
224
|
+
async size() {
|
|
225
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
226
|
+
return keys.length;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Remove expired entries.
|
|
230
|
+
*
|
|
231
|
+
* Note: Redis handles expiration automatically via TTL.
|
|
232
|
+
* This method is primarily for explicit cleanup of entries that are
|
|
233
|
+
* well past their HTTP cache expiration but still within Redis TTL.
|
|
234
|
+
*
|
|
235
|
+
* @returns Number of entries removed
|
|
236
|
+
*/
|
|
237
|
+
async cleanup() {
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
let removed = 0;
|
|
240
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
241
|
+
for (const key of keys) {
|
|
242
|
+
const data = await this.redis.get(key);
|
|
243
|
+
if (!data) continue;
|
|
244
|
+
try {
|
|
245
|
+
const entry = JSON.parse(data);
|
|
246
|
+
if (entry.expiresAt + this.config.maxTtlMs * 2 < now) {
|
|
247
|
+
await this.redis.delete(key);
|
|
248
|
+
removed++;
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
await this.redis.delete(key);
|
|
252
|
+
removed++;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return removed;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Close the Redis connection.
|
|
259
|
+
*/
|
|
260
|
+
async close() {
|
|
261
|
+
await this.redis.disconnect();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
28
267
|
// libs/auth/src/cimd/cimd.cache.ts
|
|
29
268
|
function extractCacheHeaders(headers) {
|
|
30
269
|
return {
|
|
@@ -90,6 +329,19 @@ function parseCacheControlHeader(value) {
|
|
|
90
329
|
}
|
|
91
330
|
return result;
|
|
92
331
|
}
|
|
332
|
+
async function createCimdCache(config) {
|
|
333
|
+
const cacheType = config?.type ?? "memory";
|
|
334
|
+
if (cacheType === "redis") {
|
|
335
|
+
if (!config?.redis) {
|
|
336
|
+
throw new Error('Redis configuration is required when cache type is "redis"');
|
|
337
|
+
}
|
|
338
|
+
const { RedisCimdCache: RedisCimdCache2 } = await Promise.resolve().then(() => (init_cimd_redis_cache(), cimd_redis_cache_exports));
|
|
339
|
+
const cache = new RedisCimdCache2(config);
|
|
340
|
+
await cache.connect();
|
|
341
|
+
return cache;
|
|
342
|
+
}
|
|
343
|
+
return new InMemoryCimdCache(config);
|
|
344
|
+
}
|
|
93
345
|
var InMemoryCimdCache;
|
|
94
346
|
var init_cimd_cache = __esm({
|
|
95
347
|
"libs/auth/src/cimd/cimd.cache.ts"() {
|
|
@@ -962,6 +1214,7 @@ var TRANSPARENT_JWT_ALGS = [
|
|
|
962
1214
|
];
|
|
963
1215
|
var MAX_JWKS_FETCH_BYTES = 1048576;
|
|
964
1216
|
var MAX_JWKS_FETCH_HOPS = 3;
|
|
1217
|
+
var UNKNOWN_KID_REFETCH_COOLDOWN_MS = 6e4;
|
|
965
1218
|
var MISSING_EXP_ERROR = 'missing required "exp" claim';
|
|
966
1219
|
function isMissingExpError(error) {
|
|
967
1220
|
if (typeof error !== "object" || error === null) return false;
|
|
@@ -982,6 +1235,8 @@ var JwksService = class {
|
|
|
982
1235
|
orchestratorKey;
|
|
983
1236
|
// Provider JWKS cache (providerId -> jwks + fetchedAt)
|
|
984
1237
|
providerJwks = /* @__PURE__ */ new Map();
|
|
1238
|
+
// The last key refetch an unknown `kid` started, per provider (see refetchForUnknownKid)
|
|
1239
|
+
unknownKidRefetches = /* @__PURE__ */ new Map();
|
|
985
1240
|
// Track if key has been initialized (for async loading)
|
|
986
1241
|
keyInitialized = false;
|
|
987
1242
|
// Promise guard to prevent concurrent key generation
|
|
@@ -1042,6 +1297,9 @@ var JwksService = class {
|
|
|
1042
1297
|
let jwks;
|
|
1043
1298
|
try {
|
|
1044
1299
|
jwks = await this.getJwksForProvider(p);
|
|
1300
|
+
if (kid && !p.jwks?.keys?.length && !jwks?.keys?.some((key) => key.kid === kid)) {
|
|
1301
|
+
jwks = await this.refetchForUnknownKid(p) ?? jwks;
|
|
1302
|
+
}
|
|
1045
1303
|
if (!jwks?.keys?.length) continue;
|
|
1046
1304
|
const JWKS = (0, import_jose.createLocalJWKSet)(jwks);
|
|
1047
1305
|
const issuerConstraint = this.issuerConstraintFor(p);
|
|
@@ -1224,6 +1482,35 @@ var JwksService = class {
|
|
|
1224
1482
|
if (cached && Date.now() - cached.fetchedAt < this.opts.providerJwksTtlMs) {
|
|
1225
1483
|
return cached.jwks;
|
|
1226
1484
|
}
|
|
1485
|
+
const fetched = await this.fetchProviderJwks(ref);
|
|
1486
|
+
if (fetched) return fetched;
|
|
1487
|
+
const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
|
|
1488
|
+
if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
|
|
1489
|
+
return cached.jwks;
|
|
1490
|
+
}
|
|
1491
|
+
return void 0;
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Fetch a provider's keys again for a token whose `kid` the cached keys don't have (a key the
|
|
1495
|
+
* provider started using since), ahead of the cache's TTL. At most one refetch per provider per
|
|
1496
|
+
* {@link UNKNOWN_KID_REFETCH_COOLDOWN_MS}: tokens that arrive meanwhile wait for the one in flight or
|
|
1497
|
+
* use its result, so a flood of tokens with made-up `kid`s can't hammer the provider. Only freshly
|
|
1498
|
+
* fetched keys are returned: a failed refetch never revives cached keys past the stale window.
|
|
1499
|
+
*/
|
|
1500
|
+
refetchForUnknownKid(ref) {
|
|
1501
|
+
let refetch = this.unknownKidRefetches.get(ref.id);
|
|
1502
|
+
if (!refetch || Date.now() - refetch.startedAt >= UNKNOWN_KID_REFETCH_COOLDOWN_MS) {
|
|
1503
|
+
refetch = { startedAt: Date.now(), done: this.fetchProviderJwks(ref) };
|
|
1504
|
+
this.unknownKidRefetches.set(ref.id, refetch);
|
|
1505
|
+
}
|
|
1506
|
+
return refetch.done;
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Fetch a provider's keys, and cache them: from `jwksUri`, else the conventional location, else
|
|
1510
|
+
* the `jwks_uri` of the issuer's OAuth or OpenID metadata. Every URL goes through the same scheme
|
|
1511
|
+
* and SSRF checks (`fetchJson`), `jwks_uri` included.
|
|
1512
|
+
*/
|
|
1513
|
+
async fetchProviderJwks(ref) {
|
|
1227
1514
|
if (ref.jwksUri) {
|
|
1228
1515
|
const fromUri = await this.tryFetchJwks(ref.id, ref.jwksUri);
|
|
1229
1516
|
if (fromUri?.keys?.length) return fromUri;
|
|
@@ -1239,10 +1526,6 @@ var JwksService = class {
|
|
|
1239
1526
|
if (fromMeta?.keys?.length) return fromMeta;
|
|
1240
1527
|
}
|
|
1241
1528
|
}
|
|
1242
|
-
const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
|
|
1243
|
-
if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
|
|
1244
|
-
return cached.jwks;
|
|
1245
|
-
}
|
|
1246
1529
|
return void 0;
|
|
1247
1530
|
}
|
|
1248
1531
|
// ===========================================================================
|
|
@@ -2209,7 +2492,8 @@ function buildRefreshTokenRecord(params) {
|
|
|
2209
2492
|
customClaims: params.customClaims,
|
|
2210
2493
|
federatedLoginUsed: params.federatedLoginUsed,
|
|
2211
2494
|
selectedProviderIds: params.selectedProviderIds,
|
|
2212
|
-
skippedProviderIds: params.skippedProviderIds
|
|
2495
|
+
skippedProviderIds: params.skippedProviderIds,
|
|
2496
|
+
providerTokensId: params.providerTokensId
|
|
2213
2497
|
};
|
|
2214
2498
|
}
|
|
2215
2499
|
function verifyPkce(codeVerifier, challenge) {
|
|
@@ -5200,7 +5484,49 @@ var VercelKvSessionStore = class {
|
|
|
5200
5484
|
};
|
|
5201
5485
|
|
|
5202
5486
|
// libs/auth/src/session/orchestrated-token.store.ts
|
|
5487
|
+
var import_utils30 = require("@frontmcp/utils");
|
|
5488
|
+
|
|
5489
|
+
// libs/auth/src/session/orchestrated-token.crypto.ts
|
|
5203
5490
|
var import_utils29 = require("@frontmcp/utils");
|
|
5491
|
+
function recordDropAt(record) {
|
|
5492
|
+
if (!record.refreshToken) return record.expiresAt;
|
|
5493
|
+
return Math.max(record.expiresAt ?? 0, record.updatedAt + REFRESH_TOKEN_TTL_MS);
|
|
5494
|
+
}
|
|
5495
|
+
function hasLiveAccessToken(record, refreshSkewMs = 0) {
|
|
5496
|
+
if (record.expiresAt === void 0) return true;
|
|
5497
|
+
return record.expiresAt - (record.refreshToken ? refreshSkewMs : 0) > Date.now();
|
|
5498
|
+
}
|
|
5499
|
+
var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
|
|
5500
|
+
function deriveKeyForRecord(masterKey, compositeKey, cache) {
|
|
5501
|
+
const cached = cache.get(compositeKey);
|
|
5502
|
+
if (cached) {
|
|
5503
|
+
return cached;
|
|
5504
|
+
}
|
|
5505
|
+
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
5506
|
+
const derived = (0, import_utils29.hkdfSha256)(masterKey, HKDF_SALT, info, 32);
|
|
5507
|
+
cache.set(compositeKey, derived);
|
|
5508
|
+
return derived;
|
|
5509
|
+
}
|
|
5510
|
+
function encryptRecord(key, record) {
|
|
5511
|
+
const plaintext = JSON.stringify(record);
|
|
5512
|
+
const iv = (0, import_utils29.randomBytes)(12);
|
|
5513
|
+
const { ciphertext, tag } = (0, import_utils29.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
|
|
5514
|
+
return JSON.stringify({
|
|
5515
|
+
iv: Buffer.from(iv).toString("base64url"),
|
|
5516
|
+
tag: Buffer.from(tag).toString("base64url"),
|
|
5517
|
+
data: Buffer.from(ciphertext).toString("base64url")
|
|
5518
|
+
});
|
|
5519
|
+
}
|
|
5520
|
+
function decryptRecord(key, encrypted) {
|
|
5521
|
+
const { iv, tag, data } = JSON.parse(encrypted);
|
|
5522
|
+
const ivBytes = Buffer.from(iv, "base64url");
|
|
5523
|
+
const tagBytes = Buffer.from(tag, "base64url");
|
|
5524
|
+
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
5525
|
+
const plaintext = (0, import_utils29.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
|
|
5526
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
// libs/auth/src/session/orchestrated-token.store.ts
|
|
5204
5530
|
var InMemoryOrchestratedTokenStore = class {
|
|
5205
5531
|
/** Token storage: Map<compositeKey, ProviderTokenRecord> */
|
|
5206
5532
|
tokens = /* @__PURE__ */ new Map();
|
|
@@ -5212,9 +5538,12 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5212
5538
|
cleanupTimer;
|
|
5213
5539
|
/** Default TTL for records */
|
|
5214
5540
|
defaultTtlMs;
|
|
5541
|
+
/** How long before its expiry a refreshable access token stops being returned */
|
|
5542
|
+
refreshSkewMs;
|
|
5215
5543
|
constructor(options = {}) {
|
|
5216
5544
|
this.encryptionKey = options.encryptionKey;
|
|
5217
5545
|
this.defaultTtlMs = options.defaultTtlMs;
|
|
5546
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 0;
|
|
5218
5547
|
const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
|
|
5219
5548
|
this.cleanupTimer = setInterval(() => {
|
|
5220
5549
|
void this.cleanup();
|
|
@@ -5242,7 +5571,7 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5242
5571
|
}
|
|
5243
5572
|
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
5244
5573
|
const salt = new TextEncoder().encode("frontmcp-token-store");
|
|
5245
|
-
const derivedKey = (0,
|
|
5574
|
+
const derivedKey = (0, import_utils30.hkdfSha256)(this.encryptionKey, salt, info, 32);
|
|
5246
5575
|
this.derivedKeys.set(compositeKey, derivedKey);
|
|
5247
5576
|
return derivedKey;
|
|
5248
5577
|
}
|
|
@@ -5252,8 +5581,8 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5252
5581
|
async encryptRecord(compositeKey, record) {
|
|
5253
5582
|
const key = await this.deriveKeyForRecord(compositeKey);
|
|
5254
5583
|
const plaintext = JSON.stringify(record);
|
|
5255
|
-
const iv = (0,
|
|
5256
|
-
const { ciphertext, tag } = (0,
|
|
5584
|
+
const iv = (0, import_utils30.randomBytes)(12);
|
|
5585
|
+
const { ciphertext, tag } = (0, import_utils30.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
|
|
5257
5586
|
return JSON.stringify({
|
|
5258
5587
|
iv: Buffer.from(iv).toString("base64url"),
|
|
5259
5588
|
tag: Buffer.from(tag).toString("base64url"),
|
|
@@ -5269,7 +5598,7 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5269
5598
|
const ivBytes = Buffer.from(iv, "base64url");
|
|
5270
5599
|
const tagBytes = Buffer.from(tag, "base64url");
|
|
5271
5600
|
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
5272
|
-
const plaintext = (0,
|
|
5601
|
+
const plaintext = (0, import_utils30.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
|
|
5273
5602
|
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
5274
5603
|
}
|
|
5275
5604
|
/**
|
|
@@ -5295,18 +5624,20 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5295
5624
|
} else {
|
|
5296
5625
|
record = stored;
|
|
5297
5626
|
}
|
|
5298
|
-
|
|
5627
|
+
const dropAt = recordDropAt(record);
|
|
5628
|
+
if (dropAt && dropAt < Date.now()) {
|
|
5299
5629
|
this.tokens.delete(key);
|
|
5300
5630
|
return null;
|
|
5301
5631
|
}
|
|
5302
5632
|
return record;
|
|
5303
5633
|
}
|
|
5304
5634
|
/**
|
|
5305
|
-
* Retrieve decrypted access token for a provider
|
|
5635
|
+
* Retrieve decrypted access token for a provider, or null once it has expired (a record with a
|
|
5636
|
+
* refresh token keeps that, to renew it)
|
|
5306
5637
|
*/
|
|
5307
5638
|
async getAccessToken(authorizationId, providerId) {
|
|
5308
5639
|
const record = await this.getRecord(authorizationId, providerId);
|
|
5309
|
-
return record
|
|
5640
|
+
return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
|
|
5310
5641
|
}
|
|
5311
5642
|
/**
|
|
5312
5643
|
* Retrieve decrypted refresh token for a provider
|
|
@@ -5399,7 +5730,8 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5399
5730
|
} else {
|
|
5400
5731
|
record = stored;
|
|
5401
5732
|
}
|
|
5402
|
-
|
|
5733
|
+
const dropAt = record ? recordDropAt(record) : void 0;
|
|
5734
|
+
if (dropAt && dropAt < now) {
|
|
5403
5735
|
keysToDelete.push(key);
|
|
5404
5736
|
}
|
|
5405
5737
|
}
|
|
@@ -5439,74 +5771,48 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5439
5771
|
* @param toAuthId - Target authorization ID (e.g., "def456")
|
|
5440
5772
|
*/
|
|
5441
5773
|
async migrateTokens(fromAuthId, toAuthId) {
|
|
5442
|
-
const
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
if (key.startsWith(prefix)) {
|
|
5446
|
-
keysToMigrate.push(key);
|
|
5447
|
-
}
|
|
5774
|
+
for (const oldKey of await this.copyRecords(fromAuthId, toAuthId)) {
|
|
5775
|
+
this.tokens.delete(oldKey);
|
|
5776
|
+
this.derivedKeys.delete(oldKey);
|
|
5448
5777
|
}
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5778
|
+
}
|
|
5779
|
+
/**
|
|
5780
|
+
* Copy tokens from one authorization ID to another, keeping them under the source ID too.
|
|
5781
|
+
*/
|
|
5782
|
+
async copyTokens(fromAuthId, toAuthId) {
|
|
5783
|
+
await this.copyRecords(fromAuthId, toAuthId);
|
|
5784
|
+
}
|
|
5785
|
+
/** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the keys copied. */
|
|
5786
|
+
async copyRecords(fromAuthId, toAuthId) {
|
|
5787
|
+
const prefix = `${fromAuthId}:`;
|
|
5788
|
+
const sourceKeys = [...this.tokens.keys()].filter((key) => key.startsWith(prefix));
|
|
5789
|
+
const copiedKeys = [];
|
|
5790
|
+
for (const oldKey of sourceKeys) {
|
|
5452
5791
|
const stored = this.tokens.get(oldKey);
|
|
5453
|
-
if (!stored)
|
|
5454
|
-
|
|
5455
|
-
}
|
|
5792
|
+
if (!stored) continue;
|
|
5793
|
+
const newKey = this.buildKey(toAuthId, oldKey.slice(prefix.length));
|
|
5456
5794
|
if (this.encryptionKey) {
|
|
5457
5795
|
try {
|
|
5458
5796
|
const record = await this.decryptRecord(oldKey, stored);
|
|
5459
|
-
|
|
5460
|
-
this.tokens.set(newKey, encrypted);
|
|
5797
|
+
this.tokens.set(newKey, await this.encryptRecord(newKey, record));
|
|
5461
5798
|
} catch {
|
|
5462
5799
|
continue;
|
|
5463
5800
|
}
|
|
5464
5801
|
} else {
|
|
5465
5802
|
this.tokens.set(newKey, stored);
|
|
5466
5803
|
}
|
|
5467
|
-
|
|
5468
|
-
this.derivedKeys.delete(oldKey);
|
|
5804
|
+
copiedKeys.push(oldKey);
|
|
5469
5805
|
}
|
|
5806
|
+
return copiedKeys;
|
|
5470
5807
|
}
|
|
5471
5808
|
};
|
|
5472
5809
|
|
|
5473
|
-
// libs/auth/src/session/orchestrated-token.crypto.ts
|
|
5474
|
-
var import_utils30 = require("@frontmcp/utils");
|
|
5475
|
-
var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
|
|
5476
|
-
function deriveKeyForRecord(masterKey, compositeKey, cache) {
|
|
5477
|
-
const cached = cache.get(compositeKey);
|
|
5478
|
-
if (cached) {
|
|
5479
|
-
return cached;
|
|
5480
|
-
}
|
|
5481
|
-
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
5482
|
-
const derived = (0, import_utils30.hkdfSha256)(masterKey, HKDF_SALT, info, 32);
|
|
5483
|
-
cache.set(compositeKey, derived);
|
|
5484
|
-
return derived;
|
|
5485
|
-
}
|
|
5486
|
-
function encryptRecord(key, record) {
|
|
5487
|
-
const plaintext = JSON.stringify(record);
|
|
5488
|
-
const iv = (0, import_utils30.randomBytes)(12);
|
|
5489
|
-
const { ciphertext, tag } = (0, import_utils30.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
|
|
5490
|
-
return JSON.stringify({
|
|
5491
|
-
iv: Buffer.from(iv).toString("base64url"),
|
|
5492
|
-
tag: Buffer.from(tag).toString("base64url"),
|
|
5493
|
-
data: Buffer.from(ciphertext).toString("base64url")
|
|
5494
|
-
});
|
|
5495
|
-
}
|
|
5496
|
-
function decryptRecord(key, encrypted) {
|
|
5497
|
-
const { iv, tag, data } = JSON.parse(encrypted);
|
|
5498
|
-
const ivBytes = Buffer.from(iv, "base64url");
|
|
5499
|
-
const tagBytes = Buffer.from(tag, "base64url");
|
|
5500
|
-
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
5501
|
-
const plaintext = (0, import_utils30.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
|
|
5502
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
5503
|
-
}
|
|
5504
|
-
|
|
5505
5810
|
// libs/auth/src/session/storage-orchestrated-token.store.ts
|
|
5506
5811
|
var StorageOrchestratedTokenStore = class {
|
|
5507
5812
|
storage;
|
|
5508
5813
|
encryptionKey;
|
|
5509
5814
|
defaultTtlMs;
|
|
5815
|
+
refreshSkewMs;
|
|
5510
5816
|
namespace;
|
|
5511
5817
|
storageIsNamespaced;
|
|
5512
5818
|
/** HKDF-derived subkey cache, keyed by composite key. */
|
|
@@ -5514,6 +5820,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5514
5820
|
constructor(storage, options = {}) {
|
|
5515
5821
|
this.encryptionKey = options.encryptionKey;
|
|
5516
5822
|
this.defaultTtlMs = options.defaultTtlMs;
|
|
5823
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 0;
|
|
5517
5824
|
this.namespace = options.namespace ?? "otok";
|
|
5518
5825
|
this.storageIsNamespaced = this.isNamespacedStorage(storage);
|
|
5519
5826
|
this.storage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
|
|
@@ -5523,7 +5830,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5523
5830
|
// ============================================
|
|
5524
5831
|
async getAccessToken(authorizationId, providerId) {
|
|
5525
5832
|
const record = await this.getRecord(authorizationId, providerId);
|
|
5526
|
-
return record
|
|
5833
|
+
return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
|
|
5527
5834
|
}
|
|
5528
5835
|
async getRefreshToken(authorizationId, providerId) {
|
|
5529
5836
|
const record = await this.getRecord(authorizationId, providerId);
|
|
@@ -5540,7 +5847,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5540
5847
|
updatedAt: now
|
|
5541
5848
|
};
|
|
5542
5849
|
const value = this.encryptionKey ? encryptRecord(this.keyFor(compositeKey), record) : JSON.stringify(record);
|
|
5543
|
-
await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(record
|
|
5850
|
+
await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(recordDropAt(record)));
|
|
5544
5851
|
}
|
|
5545
5852
|
async deleteTokens(authorizationId, providerId) {
|
|
5546
5853
|
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
@@ -5566,12 +5873,37 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5566
5873
|
return providerIds;
|
|
5567
5874
|
}
|
|
5568
5875
|
async migrateTokens(fromAuthId, toAuthId) {
|
|
5876
|
+
for (const oldComposite of await this.copyRecords(fromAuthId, toAuthId)) {
|
|
5877
|
+
await this.storage.delete(this.storageKey(oldComposite));
|
|
5878
|
+
this.derivedKeys.delete(oldComposite);
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
async copyTokens(fromAuthId, toAuthId) {
|
|
5882
|
+
await this.copyRecords(fromAuthId, toAuthId);
|
|
5883
|
+
}
|
|
5884
|
+
/**
|
|
5885
|
+
* Delete all tokens for an authorization id.
|
|
5886
|
+
*/
|
|
5887
|
+
async deleteAllForAuthorization(authorizationId) {
|
|
5888
|
+
const prefix = `${authorizationId}:`;
|
|
5889
|
+
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5890
|
+
for (const compositeKey of compositeKeys) {
|
|
5891
|
+
if (!compositeKey.startsWith(prefix)) continue;
|
|
5892
|
+
await this.storage.delete(this.storageKey(compositeKey));
|
|
5893
|
+
this.derivedKeys.delete(compositeKey);
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
// ============================================
|
|
5897
|
+
// Internals
|
|
5898
|
+
// ============================================
|
|
5899
|
+
/** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the composite keys copied. */
|
|
5900
|
+
async copyRecords(fromAuthId, toAuthId) {
|
|
5569
5901
|
const prefix = `${fromAuthId}:`;
|
|
5570
5902
|
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5903
|
+
const copiedKeys = [];
|
|
5571
5904
|
for (const oldComposite of compositeKeys) {
|
|
5572
5905
|
if (!oldComposite.startsWith(prefix)) continue;
|
|
5573
|
-
const
|
|
5574
|
-
const newComposite = this.compositeKey(toAuthId, providerId);
|
|
5906
|
+
const newComposite = this.compositeKey(toAuthId, oldComposite.slice(prefix.length));
|
|
5575
5907
|
const stored = await this.storage.get(this.storageKey(oldComposite));
|
|
5576
5908
|
if (stored === null) continue;
|
|
5577
5909
|
if (this.encryptionKey) {
|
|
@@ -5582,35 +5914,20 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5582
5914
|
continue;
|
|
5583
5915
|
}
|
|
5584
5916
|
const value = encryptRecord(this.keyFor(newComposite), record);
|
|
5585
|
-
await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(record
|
|
5917
|
+
await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(recordDropAt(record)));
|
|
5586
5918
|
} else {
|
|
5587
|
-
let
|
|
5919
|
+
let dropAt;
|
|
5588
5920
|
try {
|
|
5589
|
-
|
|
5921
|
+
dropAt = recordDropAt(JSON.parse(stored));
|
|
5590
5922
|
} catch {
|
|
5591
|
-
|
|
5923
|
+
dropAt = void 0;
|
|
5592
5924
|
}
|
|
5593
|
-
await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(
|
|
5925
|
+
await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(dropAt));
|
|
5594
5926
|
}
|
|
5595
|
-
|
|
5596
|
-
this.derivedKeys.delete(oldComposite);
|
|
5597
|
-
}
|
|
5598
|
-
}
|
|
5599
|
-
/**
|
|
5600
|
-
* Delete all tokens for an authorization id.
|
|
5601
|
-
*/
|
|
5602
|
-
async deleteAllForAuthorization(authorizationId) {
|
|
5603
|
-
const prefix = `${authorizationId}:`;
|
|
5604
|
-
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5605
|
-
for (const compositeKey of compositeKeys) {
|
|
5606
|
-
if (!compositeKey.startsWith(prefix)) continue;
|
|
5607
|
-
await this.storage.delete(this.storageKey(compositeKey));
|
|
5608
|
-
this.derivedKeys.delete(compositeKey);
|
|
5927
|
+
copiedKeys.push(oldComposite);
|
|
5609
5928
|
}
|
|
5929
|
+
return copiedKeys;
|
|
5610
5930
|
}
|
|
5611
|
-
// ============================================
|
|
5612
|
-
// Internals
|
|
5613
|
-
// ============================================
|
|
5614
5931
|
async getRecord(authorizationId, providerId) {
|
|
5615
5932
|
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
5616
5933
|
const stored = await this.storage.get(this.storageKey(compositeKey));
|
|
@@ -5633,7 +5950,8 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5633
5950
|
return null;
|
|
5634
5951
|
}
|
|
5635
5952
|
}
|
|
5636
|
-
|
|
5953
|
+
const dropAt = recordDropAt(record);
|
|
5954
|
+
if (dropAt && dropAt < Date.now()) {
|
|
5637
5955
|
await this.storage.delete(this.storageKey(compositeKey));
|
|
5638
5956
|
return null;
|
|
5639
5957
|
}
|
|
@@ -7085,7 +7403,7 @@ var OrchestratedAuthorization = class _OrchestratedAuthorization extends Authori
|
|
|
7085
7403
|
const result = await this.#onTokenRefresh(providerId, refreshToken);
|
|
7086
7404
|
await this.#tokenStore.storeTokens(this.id, providerId, {
|
|
7087
7405
|
accessToken: result.accessToken,
|
|
7088
|
-
refreshToken: result.refreshToken,
|
|
7406
|
+
refreshToken: result.refreshToken ?? refreshToken,
|
|
7089
7407
|
expiresAt: result.expiresIn ? Date.now() + result.expiresIn * 1e3 : void 0
|
|
7090
7408
|
});
|
|
7091
7409
|
const currentState = this.#providerStates.get(providerId);
|
|
@@ -7847,14 +8165,14 @@ var cimdConfigSchema = import_lazy_zod9.z.object({
|
|
|
7847
8165
|
init_cimd_cache();
|
|
7848
8166
|
|
|
7849
8167
|
// libs/auth/src/cimd/cimd.service.ts
|
|
7850
|
-
var
|
|
8168
|
+
var import_utils45 = require("@frontmcp/utils");
|
|
7851
8169
|
init_cimd_cache();
|
|
7852
8170
|
var CimdService = class {
|
|
7853
8171
|
config;
|
|
7854
8172
|
cacheConfig;
|
|
7855
8173
|
securityConfig;
|
|
7856
8174
|
networkConfig;
|
|
7857
|
-
|
|
8175
|
+
cacheBackend;
|
|
7858
8176
|
logger;
|
|
7859
8177
|
/**
|
|
7860
8178
|
* Whether CIMD is enabled.
|
|
@@ -7874,7 +8192,6 @@ var CimdService = class {
|
|
|
7874
8192
|
this.cacheConfig = cimdCacheConfigSchema.parse(this.config.cache ?? {});
|
|
7875
8193
|
this.securityConfig = cimdSecurityConfigSchema.parse(this.config.security ?? {});
|
|
7876
8194
|
this.networkConfig = cimdNetworkConfigSchema.parse(this.config.network ?? {});
|
|
7877
|
-
this.cache = new InMemoryCimdCache(this.cacheConfig);
|
|
7878
8195
|
this.logger.debug("CimdService initialized", {
|
|
7879
8196
|
enabled: this.config.enabled,
|
|
7880
8197
|
cacheDefaultTtlMs: this.cacheConfig.defaultTtlMs,
|
|
@@ -7886,6 +8203,34 @@ var CimdService = class {
|
|
|
7886
8203
|
);
|
|
7887
8204
|
}
|
|
7888
8205
|
}
|
|
8206
|
+
/**
|
|
8207
|
+
* Create the cache now (connecting to Redis with `cache.type: 'redis'`), so a misconfigured or
|
|
8208
|
+
* unreachable cache fails at startup rather than on the first CIMD client.
|
|
8209
|
+
*/
|
|
8210
|
+
async initialize() {
|
|
8211
|
+
await this.cache();
|
|
8212
|
+
}
|
|
8213
|
+
/**
|
|
8214
|
+
* The cache `cache.type` selects, created once through the `createCimdCache` factory. A failed
|
|
8215
|
+
* creation is retried on the next use.
|
|
8216
|
+
*/
|
|
8217
|
+
cache() {
|
|
8218
|
+
if (!this.cacheBackend) {
|
|
8219
|
+
this.cacheBackend = createCimdCache(this.cacheConfig).catch((error) => {
|
|
8220
|
+
this.cacheBackend = void 0;
|
|
8221
|
+
throw error;
|
|
8222
|
+
});
|
|
8223
|
+
}
|
|
8224
|
+
return this.cacheBackend;
|
|
8225
|
+
}
|
|
8226
|
+
/** Close the cache's connection (Redis with `cache.type: 'redis'`), if one was created. */
|
|
8227
|
+
async dispose() {
|
|
8228
|
+
const backend = this.cacheBackend;
|
|
8229
|
+
this.cacheBackend = void 0;
|
|
8230
|
+
if (!backend) return;
|
|
8231
|
+
const createdBackend = await backend.catch(() => void 0);
|
|
8232
|
+
await createdBackend?.close?.();
|
|
8233
|
+
}
|
|
7889
8234
|
/**
|
|
7890
8235
|
* Check if a client_id is a CIMD URL.
|
|
7891
8236
|
*
|
|
@@ -7912,7 +8257,8 @@ var CimdService = class {
|
|
|
7912
8257
|
};
|
|
7913
8258
|
}
|
|
7914
8259
|
validateClientIdUrl(clientId, this.securityConfig);
|
|
7915
|
-
const
|
|
8260
|
+
const cache = await this.cache();
|
|
8261
|
+
const cached = await cache.get(clientId);
|
|
7916
8262
|
if (cached) {
|
|
7917
8263
|
this.logger.debug(`Cache hit for CIMD client: ${clientId}`);
|
|
7918
8264
|
return {
|
|
@@ -7930,8 +8276,8 @@ var CimdService = class {
|
|
|
7930
8276
|
if (this.securityConfig.warnOnLocalhostRedirects && hasOnlyLocalhostRedirectUris(document.redirect_uris)) {
|
|
7931
8277
|
this.logger.warn(`CIMD client "${clientId}" has only localhost redirect URIs - this may be a development client`);
|
|
7932
8278
|
}
|
|
7933
|
-
await
|
|
7934
|
-
const entry = await
|
|
8279
|
+
await cache.set(clientId, document, headers);
|
|
8280
|
+
const entry = await cache.get(clientId);
|
|
7935
8281
|
return {
|
|
7936
8282
|
isCimdClient: true,
|
|
7937
8283
|
metadata: document,
|
|
@@ -7961,11 +8307,12 @@ var CimdService = class {
|
|
|
7961
8307
|
* @param clientId - Optional client_id to clear; clears all if not provided
|
|
7962
8308
|
*/
|
|
7963
8309
|
async clearCache(clientId) {
|
|
8310
|
+
const cache = await this.cache();
|
|
7964
8311
|
if (clientId) {
|
|
7965
|
-
await
|
|
8312
|
+
await cache.delete(clientId);
|
|
7966
8313
|
this.logger.debug(`Cache cleared for: ${clientId}`);
|
|
7967
8314
|
} else {
|
|
7968
|
-
await
|
|
8315
|
+
await cache.clear();
|
|
7969
8316
|
this.logger.debug("Cache cleared");
|
|
7970
8317
|
}
|
|
7971
8318
|
}
|
|
@@ -7973,8 +8320,9 @@ var CimdService = class {
|
|
|
7973
8320
|
* Get cache statistics.
|
|
7974
8321
|
*/
|
|
7975
8322
|
async getCacheStats() {
|
|
8323
|
+
const cache = await this.cache();
|
|
7976
8324
|
return {
|
|
7977
|
-
size: await
|
|
8325
|
+
size: await cache.size()
|
|
7978
8326
|
};
|
|
7979
8327
|
}
|
|
7980
8328
|
/**
|
|
@@ -7986,7 +8334,8 @@ var CimdService = class {
|
|
|
7986
8334
|
const controller = new AbortController();
|
|
7987
8335
|
const timer = setTimeout(() => controller.abort(), this.networkConfig.timeoutMs);
|
|
7988
8336
|
try {
|
|
7989
|
-
const
|
|
8337
|
+
const cache = await this.cache();
|
|
8338
|
+
const conditionalHeaders = await cache.getConditionalHeaders(clientId);
|
|
7990
8339
|
const originalOrigin = new URL(clientId).origin;
|
|
7991
8340
|
const maxRedirects = this.networkConfig.maxRedirects;
|
|
7992
8341
|
let currentUrl = clientId;
|
|
@@ -8009,9 +8358,9 @@ var CimdService = class {
|
|
|
8009
8358
|
redirect: "manual"
|
|
8010
8359
|
});
|
|
8011
8360
|
if (response.status === 304) {
|
|
8012
|
-
const staleEntry = await
|
|
8361
|
+
const staleEntry = await cache.getStale(clientId);
|
|
8013
8362
|
if (staleEntry) {
|
|
8014
|
-
await
|
|
8363
|
+
await cache.revalidate(clientId, response.headers);
|
|
8015
8364
|
this.logger.debug(`CIMD document not modified: ${clientId}`);
|
|
8016
8365
|
return {
|
|
8017
8366
|
document: staleEntry.document,
|
|
@@ -8022,7 +8371,7 @@ var CimdService = class {
|
|
|
8022
8371
|
httpStatus: 304
|
|
8023
8372
|
});
|
|
8024
8373
|
}
|
|
8025
|
-
if ((0,
|
|
8374
|
+
if ((0, import_utils45.isRedirectResponse)(response)) {
|
|
8026
8375
|
const location = response.headers.get("location");
|
|
8027
8376
|
if (!location) {
|
|
8028
8377
|
throw new CimdFetchError(clientId, "Redirect response missing Location header", {
|
|
@@ -8519,7 +8868,7 @@ var tokenRefreshConfigSchema = import_lazy_zod10.z.object({
|
|
|
8519
8868
|
* Refresh token before expiry by this many seconds
|
|
8520
8869
|
* @default 60
|
|
8521
8870
|
*/
|
|
8522
|
-
skewSeconds: import_lazy_zod10.z.number().default(60)
|
|
8871
|
+
skewSeconds: import_lazy_zod10.z.number().nonnegative().default(60)
|
|
8523
8872
|
});
|
|
8524
8873
|
var skippedAppBehaviorSchema = import_lazy_zod10.z.enum(["anonymous", "require-auth"]);
|
|
8525
8874
|
var consentConfigSchema = import_lazy_zod10.z.object({
|
|
@@ -8632,7 +8981,7 @@ var publicAuthOptionsSchema = import_lazy_zod11.z.object({
|
|
|
8632
8981
|
* Anonymous session TTL in seconds
|
|
8633
8982
|
* @default 3600 (1 hour)
|
|
8634
8983
|
*/
|
|
8635
|
-
sessionTtl: import_lazy_zod11.z.number().default(3600),
|
|
8984
|
+
sessionTtl: import_lazy_zod11.z.number().int().positive().default(3600),
|
|
8636
8985
|
/**
|
|
8637
8986
|
* Scopes granted to anonymous sessions
|
|
8638
8987
|
* @default ['anonymous']
|
|
@@ -9702,7 +10051,7 @@ var CredentialCache = class {
|
|
|
9702
10051
|
var AUTH_PROVIDERS_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:AUTH_PROVIDERS_ACCESSOR");
|
|
9703
10052
|
|
|
9704
10053
|
// libs/auth/src/vault/auth-providers.accessor.impl.ts
|
|
9705
|
-
var
|
|
10054
|
+
var import_utils48 = require("@frontmcp/utils");
|
|
9706
10055
|
var AuthProvidersAccessorImpl = class {
|
|
9707
10056
|
constructor(registry, vault, cache, loader, context, logger) {
|
|
9708
10057
|
this.registry = registry;
|
|
@@ -9917,7 +10266,7 @@ var AuthProvidersAccessorImpl = class {
|
|
|
9917
10266
|
return { [credential.headerName]: credential.key };
|
|
9918
10267
|
case "basic": {
|
|
9919
10268
|
const value = `${credential.username}:${credential.password}`;
|
|
9920
|
-
const encoded = credential.encodedValue ?? (0,
|
|
10269
|
+
const encoded = credential.encodedValue ?? (0, import_utils48.base64Encode)(new TextEncoder().encode(value));
|
|
9921
10270
|
return { Authorization: `Basic ${encoded}` };
|
|
9922
10271
|
}
|
|
9923
10272
|
case "custom":
|
|
@@ -10477,7 +10826,7 @@ var LazyCredentialLoader = class {
|
|
|
10477
10826
|
};
|
|
10478
10827
|
|
|
10479
10828
|
// libs/auth/src/authorities/authorities.context.ts
|
|
10480
|
-
var
|
|
10829
|
+
var import_utils49 = require("@frontmcp/utils");
|
|
10481
10830
|
function resolveDotPath(obj, path) {
|
|
10482
10831
|
if (path in obj) {
|
|
10483
10832
|
return obj[path];
|
|
@@ -10520,7 +10869,7 @@ function signedInSubject(rawSub, mappedSub) {
|
|
|
10520
10869
|
}
|
|
10521
10870
|
var runtimeEnv = new Proxy(
|
|
10522
10871
|
{},
|
|
10523
|
-
{ get: (_target, name) => typeof name === "string" ? (0,
|
|
10872
|
+
{ get: (_target, name) => typeof name === "string" ? (0, import_utils49.getEnv)(name) : void 0 }
|
|
10524
10873
|
);
|
|
10525
10874
|
var AuthoritiesContextBuilder = class {
|
|
10526
10875
|
claimsMapping;
|