@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/esm/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
2
|
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
3
|
// __frontmcpCreateRequire
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
6
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
7
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -16,6 +17,249 @@ var __esm = (fn, res, err) => function __init() {
|
|
|
16
17
|
throw err = [e], e;
|
|
17
18
|
}
|
|
18
19
|
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// libs/auth/src/cimd/cimd-redis.cache.ts
|
|
26
|
+
var cimd_redis_cache_exports = {};
|
|
27
|
+
__export(cimd_redis_cache_exports, {
|
|
28
|
+
RedisCimdCache: () => RedisCimdCache
|
|
29
|
+
});
|
|
30
|
+
import { RedisStorageAdapter as RedisStorageAdapter3 } from "@frontmcp/utils";
|
|
31
|
+
import { sha256Hex as sha256Hex5 } from "@frontmcp/utils";
|
|
32
|
+
var RedisCimdCache;
|
|
33
|
+
var init_cimd_redis_cache = __esm({
|
|
34
|
+
"libs/auth/src/cimd/cimd-redis.cache.ts"() {
|
|
35
|
+
"use strict";
|
|
36
|
+
init_cimd_cache();
|
|
37
|
+
RedisCimdCache = class {
|
|
38
|
+
redis;
|
|
39
|
+
keyPrefix;
|
|
40
|
+
config;
|
|
41
|
+
constructor(config) {
|
|
42
|
+
if (!config.redis) {
|
|
43
|
+
throw new Error("Redis configuration is required for RedisCimdCache");
|
|
44
|
+
}
|
|
45
|
+
this.config = {
|
|
46
|
+
defaultTtlMs: config.defaultTtlMs ?? 36e5,
|
|
47
|
+
maxTtlMs: config.maxTtlMs ?? 864e5,
|
|
48
|
+
minTtlMs: config.minTtlMs ?? 6e4
|
|
49
|
+
};
|
|
50
|
+
this.keyPrefix = config.redis.keyPrefix ?? "cimd:";
|
|
51
|
+
const redisOptions = {};
|
|
52
|
+
if (config.redis.url) {
|
|
53
|
+
redisOptions.url = config.redis.url;
|
|
54
|
+
} else if (config.redis.host) {
|
|
55
|
+
redisOptions.config = {
|
|
56
|
+
host: config.redis.host,
|
|
57
|
+
port: config.redis.port,
|
|
58
|
+
password: config.redis.password,
|
|
59
|
+
db: config.redis.db,
|
|
60
|
+
tls: config.redis.tls
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
this.redis = new RedisStorageAdapter3(redisOptions);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Connect to Redis.
|
|
67
|
+
* Must be called before using any cache operations.
|
|
68
|
+
*/
|
|
69
|
+
async connect() {
|
|
70
|
+
await this.redis.connect();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate a Redis key for a client ID.
|
|
74
|
+
* Uses SHA-256 hash to handle URLs with special characters.
|
|
75
|
+
*/
|
|
76
|
+
cacheKey(clientId) {
|
|
77
|
+
const hash = sha256Hex5(clientId);
|
|
78
|
+
return `${this.keyPrefix}${hash}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get a cached entry by client_id.
|
|
82
|
+
*
|
|
83
|
+
* @param clientId - The client_id URL
|
|
84
|
+
* @returns The cached entry if valid, or undefined
|
|
85
|
+
*/
|
|
86
|
+
async get(clientId) {
|
|
87
|
+
const key = this.cacheKey(clientId);
|
|
88
|
+
const data = await this.redis.get(key);
|
|
89
|
+
if (!data) {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const entry = JSON.parse(data);
|
|
94
|
+
if (entry.expiresAt < Date.now()) {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
return entry;
|
|
98
|
+
} catch {
|
|
99
|
+
await this.redis.delete(key);
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get a stale entry for conditional revalidation.
|
|
105
|
+
*
|
|
106
|
+
* @param clientId - The client_id URL
|
|
107
|
+
* @returns The stale entry (even if expired), or undefined if not cached
|
|
108
|
+
*/
|
|
109
|
+
async getStale(clientId) {
|
|
110
|
+
const key = this.cacheKey(clientId);
|
|
111
|
+
const data = await this.redis.get(key);
|
|
112
|
+
if (!data) {
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(data);
|
|
117
|
+
} catch {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Store a document in the cache.
|
|
123
|
+
*
|
|
124
|
+
* @param clientId - The client_id URL
|
|
125
|
+
* @param document - The metadata document
|
|
126
|
+
* @param headers - HTTP response headers
|
|
127
|
+
*/
|
|
128
|
+
async set(clientId, document, headers) {
|
|
129
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
130
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const entry = {
|
|
133
|
+
document,
|
|
134
|
+
expiresAt: now + ttlMs,
|
|
135
|
+
etag,
|
|
136
|
+
lastModified,
|
|
137
|
+
cachedAt: now
|
|
138
|
+
};
|
|
139
|
+
const key = this.cacheKey(clientId);
|
|
140
|
+
const value = JSON.stringify(entry);
|
|
141
|
+
const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
|
|
142
|
+
await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Update an existing cache entry (after 304 Not Modified).
|
|
146
|
+
*
|
|
147
|
+
* @param clientId - The client_id URL
|
|
148
|
+
* @param headers - New HTTP headers with updated cache directives
|
|
149
|
+
*/
|
|
150
|
+
async revalidate(clientId, headers) {
|
|
151
|
+
const key = this.cacheKey(clientId);
|
|
152
|
+
const data = await this.redis.get(key);
|
|
153
|
+
if (!data) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const existing = JSON.parse(data);
|
|
158
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
159
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
160
|
+
existing.expiresAt = Date.now() + ttlMs;
|
|
161
|
+
if (etag) existing.etag = etag;
|
|
162
|
+
if (lastModified) existing.lastModified = lastModified;
|
|
163
|
+
const value = JSON.stringify(existing);
|
|
164
|
+
const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
|
|
165
|
+
await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Delete a cache entry.
|
|
173
|
+
*
|
|
174
|
+
* @param clientId - The client_id URL
|
|
175
|
+
* @returns true if an entry was deleted
|
|
176
|
+
*/
|
|
177
|
+
async delete(clientId) {
|
|
178
|
+
const key = this.cacheKey(clientId);
|
|
179
|
+
return this.redis.delete(key);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get conditional request headers for a cached entry.
|
|
183
|
+
*
|
|
184
|
+
* @param clientId - The client_id URL
|
|
185
|
+
* @returns Headers for conditional request, or undefined if not cached
|
|
186
|
+
*/
|
|
187
|
+
async getConditionalHeaders(clientId) {
|
|
188
|
+
const key = this.cacheKey(clientId);
|
|
189
|
+
const data = await this.redis.get(key);
|
|
190
|
+
if (!data) {
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const entry = JSON.parse(data);
|
|
195
|
+
const headers = {};
|
|
196
|
+
if (entry.etag) {
|
|
197
|
+
headers["If-None-Match"] = entry.etag;
|
|
198
|
+
}
|
|
199
|
+
if (entry.lastModified) {
|
|
200
|
+
headers["If-Modified-Since"] = entry.lastModified;
|
|
201
|
+
}
|
|
202
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
203
|
+
} catch {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Clear all cached entries.
|
|
209
|
+
* Uses Redis SCAN to find and delete all keys with our prefix.
|
|
210
|
+
*/
|
|
211
|
+
async clear() {
|
|
212
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
213
|
+
if (keys.length > 0) {
|
|
214
|
+
await this.redis.mdelete(keys);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get the number of cached entries.
|
|
219
|
+
* Uses Redis SCAN to count keys with our prefix.
|
|
220
|
+
*/
|
|
221
|
+
async size() {
|
|
222
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
223
|
+
return keys.length;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Remove expired entries.
|
|
227
|
+
*
|
|
228
|
+
* Note: Redis handles expiration automatically via TTL.
|
|
229
|
+
* This method is primarily for explicit cleanup of entries that are
|
|
230
|
+
* well past their HTTP cache expiration but still within Redis TTL.
|
|
231
|
+
*
|
|
232
|
+
* @returns Number of entries removed
|
|
233
|
+
*/
|
|
234
|
+
async cleanup() {
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
let removed = 0;
|
|
237
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
238
|
+
for (const key of keys) {
|
|
239
|
+
const data = await this.redis.get(key);
|
|
240
|
+
if (!data) continue;
|
|
241
|
+
try {
|
|
242
|
+
const entry = JSON.parse(data);
|
|
243
|
+
if (entry.expiresAt + this.config.maxTtlMs * 2 < now) {
|
|
244
|
+
await this.redis.delete(key);
|
|
245
|
+
removed++;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
await this.redis.delete(key);
|
|
249
|
+
removed++;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return removed;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Close the Redis connection.
|
|
256
|
+
*/
|
|
257
|
+
async close() {
|
|
258
|
+
await this.redis.disconnect();
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
});
|
|
19
263
|
|
|
20
264
|
// libs/auth/src/cimd/cimd.cache.ts
|
|
21
265
|
function extractCacheHeaders(headers) {
|
|
@@ -82,6 +326,19 @@ function parseCacheControlHeader(value) {
|
|
|
82
326
|
}
|
|
83
327
|
return result;
|
|
84
328
|
}
|
|
329
|
+
async function createCimdCache(config) {
|
|
330
|
+
const cacheType = config?.type ?? "memory";
|
|
331
|
+
if (cacheType === "redis") {
|
|
332
|
+
if (!config?.redis) {
|
|
333
|
+
throw new Error('Redis configuration is required when cache type is "redis"');
|
|
334
|
+
}
|
|
335
|
+
const { RedisCimdCache: RedisCimdCache2 } = await Promise.resolve().then(() => (init_cimd_redis_cache(), cimd_redis_cache_exports));
|
|
336
|
+
const cache = new RedisCimdCache2(config);
|
|
337
|
+
await cache.connect();
|
|
338
|
+
return cache;
|
|
339
|
+
}
|
|
340
|
+
return new InMemoryCimdCache(config);
|
|
341
|
+
}
|
|
85
342
|
var InMemoryCimdCache;
|
|
86
343
|
var init_cimd_cache = __esm({
|
|
87
344
|
"libs/auth/src/cimd/cimd.cache.ts"() {
|
|
@@ -620,6 +877,7 @@ var TRANSPARENT_JWT_ALGS = [
|
|
|
620
877
|
];
|
|
621
878
|
var MAX_JWKS_FETCH_BYTES = 1048576;
|
|
622
879
|
var MAX_JWKS_FETCH_HOPS = 3;
|
|
880
|
+
var UNKNOWN_KID_REFETCH_COOLDOWN_MS = 6e4;
|
|
623
881
|
var MISSING_EXP_ERROR = 'missing required "exp" claim';
|
|
624
882
|
function isMissingExpError(error) {
|
|
625
883
|
if (typeof error !== "object" || error === null) return false;
|
|
@@ -640,6 +898,8 @@ var JwksService = class {
|
|
|
640
898
|
orchestratorKey;
|
|
641
899
|
// Provider JWKS cache (providerId -> jwks + fetchedAt)
|
|
642
900
|
providerJwks = /* @__PURE__ */ new Map();
|
|
901
|
+
// The last key refetch an unknown `kid` started, per provider (see refetchForUnknownKid)
|
|
902
|
+
unknownKidRefetches = /* @__PURE__ */ new Map();
|
|
643
903
|
// Track if key has been initialized (for async loading)
|
|
644
904
|
keyInitialized = false;
|
|
645
905
|
// Promise guard to prevent concurrent key generation
|
|
@@ -700,6 +960,9 @@ var JwksService = class {
|
|
|
700
960
|
let jwks;
|
|
701
961
|
try {
|
|
702
962
|
jwks = await this.getJwksForProvider(p);
|
|
963
|
+
if (kid && !p.jwks?.keys?.length && !jwks?.keys?.some((key) => key.kid === kid)) {
|
|
964
|
+
jwks = await this.refetchForUnknownKid(p) ?? jwks;
|
|
965
|
+
}
|
|
703
966
|
if (!jwks?.keys?.length) continue;
|
|
704
967
|
const JWKS = createLocalJWKSet(jwks);
|
|
705
968
|
const issuerConstraint = this.issuerConstraintFor(p);
|
|
@@ -882,6 +1145,35 @@ var JwksService = class {
|
|
|
882
1145
|
if (cached && Date.now() - cached.fetchedAt < this.opts.providerJwksTtlMs) {
|
|
883
1146
|
return cached.jwks;
|
|
884
1147
|
}
|
|
1148
|
+
const fetched = await this.fetchProviderJwks(ref);
|
|
1149
|
+
if (fetched) return fetched;
|
|
1150
|
+
const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
|
|
1151
|
+
if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
|
|
1152
|
+
return cached.jwks;
|
|
1153
|
+
}
|
|
1154
|
+
return void 0;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Fetch a provider's keys again for a token whose `kid` the cached keys don't have (a key the
|
|
1158
|
+
* provider started using since), ahead of the cache's TTL. At most one refetch per provider per
|
|
1159
|
+
* {@link UNKNOWN_KID_REFETCH_COOLDOWN_MS}: tokens that arrive meanwhile wait for the one in flight or
|
|
1160
|
+
* use its result, so a flood of tokens with made-up `kid`s can't hammer the provider. Only freshly
|
|
1161
|
+
* fetched keys are returned: a failed refetch never revives cached keys past the stale window.
|
|
1162
|
+
*/
|
|
1163
|
+
refetchForUnknownKid(ref) {
|
|
1164
|
+
let refetch = this.unknownKidRefetches.get(ref.id);
|
|
1165
|
+
if (!refetch || Date.now() - refetch.startedAt >= UNKNOWN_KID_REFETCH_COOLDOWN_MS) {
|
|
1166
|
+
refetch = { startedAt: Date.now(), done: this.fetchProviderJwks(ref) };
|
|
1167
|
+
this.unknownKidRefetches.set(ref.id, refetch);
|
|
1168
|
+
}
|
|
1169
|
+
return refetch.done;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Fetch a provider's keys, and cache them: from `jwksUri`, else the conventional location, else
|
|
1173
|
+
* the `jwks_uri` of the issuer's OAuth or OpenID metadata. Every URL goes through the same scheme
|
|
1174
|
+
* and SSRF checks (`fetchJson`), `jwks_uri` included.
|
|
1175
|
+
*/
|
|
1176
|
+
async fetchProviderJwks(ref) {
|
|
885
1177
|
if (ref.jwksUri) {
|
|
886
1178
|
const fromUri = await this.tryFetchJwks(ref.id, ref.jwksUri);
|
|
887
1179
|
if (fromUri?.keys?.length) return fromUri;
|
|
@@ -897,10 +1189,6 @@ var JwksService = class {
|
|
|
897
1189
|
if (fromMeta?.keys?.length) return fromMeta;
|
|
898
1190
|
}
|
|
899
1191
|
}
|
|
900
|
-
const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
|
|
901
|
-
if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
|
|
902
|
-
return cached.jwks;
|
|
903
|
-
}
|
|
904
1192
|
return void 0;
|
|
905
1193
|
}
|
|
906
1194
|
// ===========================================================================
|
|
@@ -1867,7 +2155,8 @@ function buildRefreshTokenRecord(params) {
|
|
|
1867
2155
|
customClaims: params.customClaims,
|
|
1868
2156
|
federatedLoginUsed: params.federatedLoginUsed,
|
|
1869
2157
|
selectedProviderIds: params.selectedProviderIds,
|
|
1870
|
-
skippedProviderIds: params.skippedProviderIds
|
|
2158
|
+
skippedProviderIds: params.skippedProviderIds,
|
|
2159
|
+
providerTokensId: params.providerTokensId
|
|
1871
2160
|
};
|
|
1872
2161
|
}
|
|
1873
2162
|
function verifyPkce(codeVerifier, challenge) {
|
|
@@ -4885,7 +5174,49 @@ var VercelKvSessionStore = class {
|
|
|
4885
5174
|
};
|
|
4886
5175
|
|
|
4887
5176
|
// libs/auth/src/session/orchestrated-token.store.ts
|
|
4888
|
-
import {
|
|
5177
|
+
import { decryptAesGcm as decryptAesGcm5, encryptAesGcm as encryptAesGcm5, hkdfSha256 as hkdfSha2564, randomBytes as randomBytes8 } from "@frontmcp/utils";
|
|
5178
|
+
|
|
5179
|
+
// libs/auth/src/session/orchestrated-token.crypto.ts
|
|
5180
|
+
import { decryptAesGcm as decryptAesGcm4, encryptAesGcm as encryptAesGcm4, hkdfSha256 as hkdfSha2563, randomBytes as randomBytes7 } from "@frontmcp/utils";
|
|
5181
|
+
function recordDropAt(record) {
|
|
5182
|
+
if (!record.refreshToken) return record.expiresAt;
|
|
5183
|
+
return Math.max(record.expiresAt ?? 0, record.updatedAt + REFRESH_TOKEN_TTL_MS);
|
|
5184
|
+
}
|
|
5185
|
+
function hasLiveAccessToken(record, refreshSkewMs = 0) {
|
|
5186
|
+
if (record.expiresAt === void 0) return true;
|
|
5187
|
+
return record.expiresAt - (record.refreshToken ? refreshSkewMs : 0) > Date.now();
|
|
5188
|
+
}
|
|
5189
|
+
var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
|
|
5190
|
+
function deriveKeyForRecord(masterKey, compositeKey, cache) {
|
|
5191
|
+
const cached = cache.get(compositeKey);
|
|
5192
|
+
if (cached) {
|
|
5193
|
+
return cached;
|
|
5194
|
+
}
|
|
5195
|
+
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
5196
|
+
const derived = hkdfSha2563(masterKey, HKDF_SALT, info, 32);
|
|
5197
|
+
cache.set(compositeKey, derived);
|
|
5198
|
+
return derived;
|
|
5199
|
+
}
|
|
5200
|
+
function encryptRecord(key, record) {
|
|
5201
|
+
const plaintext = JSON.stringify(record);
|
|
5202
|
+
const iv = randomBytes7(12);
|
|
5203
|
+
const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
|
|
5204
|
+
return JSON.stringify({
|
|
5205
|
+
iv: Buffer.from(iv).toString("base64url"),
|
|
5206
|
+
tag: Buffer.from(tag).toString("base64url"),
|
|
5207
|
+
data: Buffer.from(ciphertext).toString("base64url")
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
function decryptRecord(key, encrypted) {
|
|
5211
|
+
const { iv, tag, data } = JSON.parse(encrypted);
|
|
5212
|
+
const ivBytes = Buffer.from(iv, "base64url");
|
|
5213
|
+
const tagBytes = Buffer.from(tag, "base64url");
|
|
5214
|
+
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
5215
|
+
const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
|
|
5216
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
5217
|
+
}
|
|
5218
|
+
|
|
5219
|
+
// libs/auth/src/session/orchestrated-token.store.ts
|
|
4889
5220
|
var InMemoryOrchestratedTokenStore = class {
|
|
4890
5221
|
/** Token storage: Map<compositeKey, ProviderTokenRecord> */
|
|
4891
5222
|
tokens = /* @__PURE__ */ new Map();
|
|
@@ -4897,9 +5228,12 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4897
5228
|
cleanupTimer;
|
|
4898
5229
|
/** Default TTL for records */
|
|
4899
5230
|
defaultTtlMs;
|
|
5231
|
+
/** How long before its expiry a refreshable access token stops being returned */
|
|
5232
|
+
refreshSkewMs;
|
|
4900
5233
|
constructor(options = {}) {
|
|
4901
5234
|
this.encryptionKey = options.encryptionKey;
|
|
4902
5235
|
this.defaultTtlMs = options.defaultTtlMs;
|
|
5236
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 0;
|
|
4903
5237
|
const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
|
|
4904
5238
|
this.cleanupTimer = setInterval(() => {
|
|
4905
5239
|
void this.cleanup();
|
|
@@ -4927,7 +5261,7 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4927
5261
|
}
|
|
4928
5262
|
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
4929
5263
|
const salt = new TextEncoder().encode("frontmcp-token-store");
|
|
4930
|
-
const derivedKey =
|
|
5264
|
+
const derivedKey = hkdfSha2564(this.encryptionKey, salt, info, 32);
|
|
4931
5265
|
this.derivedKeys.set(compositeKey, derivedKey);
|
|
4932
5266
|
return derivedKey;
|
|
4933
5267
|
}
|
|
@@ -4937,8 +5271,8 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4937
5271
|
async encryptRecord(compositeKey, record) {
|
|
4938
5272
|
const key = await this.deriveKeyForRecord(compositeKey);
|
|
4939
5273
|
const plaintext = JSON.stringify(record);
|
|
4940
|
-
const iv =
|
|
4941
|
-
const { ciphertext, tag } =
|
|
5274
|
+
const iv = randomBytes8(12);
|
|
5275
|
+
const { ciphertext, tag } = encryptAesGcm5(key, new TextEncoder().encode(plaintext), iv);
|
|
4942
5276
|
return JSON.stringify({
|
|
4943
5277
|
iv: Buffer.from(iv).toString("base64url"),
|
|
4944
5278
|
tag: Buffer.from(tag).toString("base64url"),
|
|
@@ -4954,7 +5288,7 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4954
5288
|
const ivBytes = Buffer.from(iv, "base64url");
|
|
4955
5289
|
const tagBytes = Buffer.from(tag, "base64url");
|
|
4956
5290
|
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
4957
|
-
const plaintext =
|
|
5291
|
+
const plaintext = decryptAesGcm5(key, ciphertextBytes, ivBytes, tagBytes);
|
|
4958
5292
|
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
4959
5293
|
}
|
|
4960
5294
|
/**
|
|
@@ -4980,18 +5314,20 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4980
5314
|
} else {
|
|
4981
5315
|
record = stored;
|
|
4982
5316
|
}
|
|
4983
|
-
|
|
5317
|
+
const dropAt = recordDropAt(record);
|
|
5318
|
+
if (dropAt && dropAt < Date.now()) {
|
|
4984
5319
|
this.tokens.delete(key);
|
|
4985
5320
|
return null;
|
|
4986
5321
|
}
|
|
4987
5322
|
return record;
|
|
4988
5323
|
}
|
|
4989
5324
|
/**
|
|
4990
|
-
* Retrieve decrypted access token for a provider
|
|
5325
|
+
* Retrieve decrypted access token for a provider, or null once it has expired (a record with a
|
|
5326
|
+
* refresh token keeps that, to renew it)
|
|
4991
5327
|
*/
|
|
4992
5328
|
async getAccessToken(authorizationId, providerId) {
|
|
4993
5329
|
const record = await this.getRecord(authorizationId, providerId);
|
|
4994
|
-
return record
|
|
5330
|
+
return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
|
|
4995
5331
|
}
|
|
4996
5332
|
/**
|
|
4997
5333
|
* Retrieve decrypted refresh token for a provider
|
|
@@ -5084,7 +5420,8 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5084
5420
|
} else {
|
|
5085
5421
|
record = stored;
|
|
5086
5422
|
}
|
|
5087
|
-
|
|
5423
|
+
const dropAt = record ? recordDropAt(record) : void 0;
|
|
5424
|
+
if (dropAt && dropAt < now) {
|
|
5088
5425
|
keysToDelete.push(key);
|
|
5089
5426
|
}
|
|
5090
5427
|
}
|
|
@@ -5124,74 +5461,48 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5124
5461
|
* @param toAuthId - Target authorization ID (e.g., "def456")
|
|
5125
5462
|
*/
|
|
5126
5463
|
async migrateTokens(fromAuthId, toAuthId) {
|
|
5127
|
-
const
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
if (key.startsWith(prefix)) {
|
|
5131
|
-
keysToMigrate.push(key);
|
|
5132
|
-
}
|
|
5464
|
+
for (const oldKey of await this.copyRecords(fromAuthId, toAuthId)) {
|
|
5465
|
+
this.tokens.delete(oldKey);
|
|
5466
|
+
this.derivedKeys.delete(oldKey);
|
|
5133
5467
|
}
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5468
|
+
}
|
|
5469
|
+
/**
|
|
5470
|
+
* Copy tokens from one authorization ID to another, keeping them under the source ID too.
|
|
5471
|
+
*/
|
|
5472
|
+
async copyTokens(fromAuthId, toAuthId) {
|
|
5473
|
+
await this.copyRecords(fromAuthId, toAuthId);
|
|
5474
|
+
}
|
|
5475
|
+
/** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the keys copied. */
|
|
5476
|
+
async copyRecords(fromAuthId, toAuthId) {
|
|
5477
|
+
const prefix = `${fromAuthId}:`;
|
|
5478
|
+
const sourceKeys = [...this.tokens.keys()].filter((key) => key.startsWith(prefix));
|
|
5479
|
+
const copiedKeys = [];
|
|
5480
|
+
for (const oldKey of sourceKeys) {
|
|
5137
5481
|
const stored = this.tokens.get(oldKey);
|
|
5138
|
-
if (!stored)
|
|
5139
|
-
|
|
5140
|
-
}
|
|
5482
|
+
if (!stored) continue;
|
|
5483
|
+
const newKey = this.buildKey(toAuthId, oldKey.slice(prefix.length));
|
|
5141
5484
|
if (this.encryptionKey) {
|
|
5142
5485
|
try {
|
|
5143
5486
|
const record = await this.decryptRecord(oldKey, stored);
|
|
5144
|
-
|
|
5145
|
-
this.tokens.set(newKey, encrypted);
|
|
5487
|
+
this.tokens.set(newKey, await this.encryptRecord(newKey, record));
|
|
5146
5488
|
} catch {
|
|
5147
5489
|
continue;
|
|
5148
5490
|
}
|
|
5149
5491
|
} else {
|
|
5150
5492
|
this.tokens.set(newKey, stored);
|
|
5151
5493
|
}
|
|
5152
|
-
|
|
5153
|
-
this.derivedKeys.delete(oldKey);
|
|
5494
|
+
copiedKeys.push(oldKey);
|
|
5154
5495
|
}
|
|
5496
|
+
return copiedKeys;
|
|
5155
5497
|
}
|
|
5156
5498
|
};
|
|
5157
5499
|
|
|
5158
|
-
// libs/auth/src/session/orchestrated-token.crypto.ts
|
|
5159
|
-
import { decryptAesGcm as decryptAesGcm5, encryptAesGcm as encryptAesGcm5, hkdfSha256 as hkdfSha2564, randomBytes as randomBytes8 } from "@frontmcp/utils";
|
|
5160
|
-
var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
|
|
5161
|
-
function deriveKeyForRecord(masterKey, compositeKey, cache) {
|
|
5162
|
-
const cached = cache.get(compositeKey);
|
|
5163
|
-
if (cached) {
|
|
5164
|
-
return cached;
|
|
5165
|
-
}
|
|
5166
|
-
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
5167
|
-
const derived = hkdfSha2564(masterKey, HKDF_SALT, info, 32);
|
|
5168
|
-
cache.set(compositeKey, derived);
|
|
5169
|
-
return derived;
|
|
5170
|
-
}
|
|
5171
|
-
function encryptRecord(key, record) {
|
|
5172
|
-
const plaintext = JSON.stringify(record);
|
|
5173
|
-
const iv = randomBytes8(12);
|
|
5174
|
-
const { ciphertext, tag } = encryptAesGcm5(key, new TextEncoder().encode(plaintext), iv);
|
|
5175
|
-
return JSON.stringify({
|
|
5176
|
-
iv: Buffer.from(iv).toString("base64url"),
|
|
5177
|
-
tag: Buffer.from(tag).toString("base64url"),
|
|
5178
|
-
data: Buffer.from(ciphertext).toString("base64url")
|
|
5179
|
-
});
|
|
5180
|
-
}
|
|
5181
|
-
function decryptRecord(key, encrypted) {
|
|
5182
|
-
const { iv, tag, data } = JSON.parse(encrypted);
|
|
5183
|
-
const ivBytes = Buffer.from(iv, "base64url");
|
|
5184
|
-
const tagBytes = Buffer.from(tag, "base64url");
|
|
5185
|
-
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
5186
|
-
const plaintext = decryptAesGcm5(key, ciphertextBytes, ivBytes, tagBytes);
|
|
5187
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
5188
|
-
}
|
|
5189
|
-
|
|
5190
5500
|
// libs/auth/src/session/storage-orchestrated-token.store.ts
|
|
5191
5501
|
var StorageOrchestratedTokenStore = class {
|
|
5192
5502
|
storage;
|
|
5193
5503
|
encryptionKey;
|
|
5194
5504
|
defaultTtlMs;
|
|
5505
|
+
refreshSkewMs;
|
|
5195
5506
|
namespace;
|
|
5196
5507
|
storageIsNamespaced;
|
|
5197
5508
|
/** HKDF-derived subkey cache, keyed by composite key. */
|
|
@@ -5199,6 +5510,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5199
5510
|
constructor(storage, options = {}) {
|
|
5200
5511
|
this.encryptionKey = options.encryptionKey;
|
|
5201
5512
|
this.defaultTtlMs = options.defaultTtlMs;
|
|
5513
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 0;
|
|
5202
5514
|
this.namespace = options.namespace ?? "otok";
|
|
5203
5515
|
this.storageIsNamespaced = this.isNamespacedStorage(storage);
|
|
5204
5516
|
this.storage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
|
|
@@ -5208,7 +5520,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5208
5520
|
// ============================================
|
|
5209
5521
|
async getAccessToken(authorizationId, providerId) {
|
|
5210
5522
|
const record = await this.getRecord(authorizationId, providerId);
|
|
5211
|
-
return record
|
|
5523
|
+
return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
|
|
5212
5524
|
}
|
|
5213
5525
|
async getRefreshToken(authorizationId, providerId) {
|
|
5214
5526
|
const record = await this.getRecord(authorizationId, providerId);
|
|
@@ -5225,7 +5537,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5225
5537
|
updatedAt: now
|
|
5226
5538
|
};
|
|
5227
5539
|
const value = this.encryptionKey ? encryptRecord(this.keyFor(compositeKey), record) : JSON.stringify(record);
|
|
5228
|
-
await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(record
|
|
5540
|
+
await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(recordDropAt(record)));
|
|
5229
5541
|
}
|
|
5230
5542
|
async deleteTokens(authorizationId, providerId) {
|
|
5231
5543
|
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
@@ -5251,12 +5563,37 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5251
5563
|
return providerIds;
|
|
5252
5564
|
}
|
|
5253
5565
|
async migrateTokens(fromAuthId, toAuthId) {
|
|
5566
|
+
for (const oldComposite of await this.copyRecords(fromAuthId, toAuthId)) {
|
|
5567
|
+
await this.storage.delete(this.storageKey(oldComposite));
|
|
5568
|
+
this.derivedKeys.delete(oldComposite);
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
async copyTokens(fromAuthId, toAuthId) {
|
|
5572
|
+
await this.copyRecords(fromAuthId, toAuthId);
|
|
5573
|
+
}
|
|
5574
|
+
/**
|
|
5575
|
+
* Delete all tokens for an authorization id.
|
|
5576
|
+
*/
|
|
5577
|
+
async deleteAllForAuthorization(authorizationId) {
|
|
5578
|
+
const prefix = `${authorizationId}:`;
|
|
5579
|
+
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5580
|
+
for (const compositeKey of compositeKeys) {
|
|
5581
|
+
if (!compositeKey.startsWith(prefix)) continue;
|
|
5582
|
+
await this.storage.delete(this.storageKey(compositeKey));
|
|
5583
|
+
this.derivedKeys.delete(compositeKey);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
// ============================================
|
|
5587
|
+
// Internals
|
|
5588
|
+
// ============================================
|
|
5589
|
+
/** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the composite keys copied. */
|
|
5590
|
+
async copyRecords(fromAuthId, toAuthId) {
|
|
5254
5591
|
const prefix = `${fromAuthId}:`;
|
|
5255
5592
|
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5593
|
+
const copiedKeys = [];
|
|
5256
5594
|
for (const oldComposite of compositeKeys) {
|
|
5257
5595
|
if (!oldComposite.startsWith(prefix)) continue;
|
|
5258
|
-
const
|
|
5259
|
-
const newComposite = this.compositeKey(toAuthId, providerId);
|
|
5596
|
+
const newComposite = this.compositeKey(toAuthId, oldComposite.slice(prefix.length));
|
|
5260
5597
|
const stored = await this.storage.get(this.storageKey(oldComposite));
|
|
5261
5598
|
if (stored === null) continue;
|
|
5262
5599
|
if (this.encryptionKey) {
|
|
@@ -5267,35 +5604,20 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5267
5604
|
continue;
|
|
5268
5605
|
}
|
|
5269
5606
|
const value = encryptRecord(this.keyFor(newComposite), record);
|
|
5270
|
-
await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(record
|
|
5607
|
+
await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(recordDropAt(record)));
|
|
5271
5608
|
} else {
|
|
5272
|
-
let
|
|
5609
|
+
let dropAt;
|
|
5273
5610
|
try {
|
|
5274
|
-
|
|
5611
|
+
dropAt = recordDropAt(JSON.parse(stored));
|
|
5275
5612
|
} catch {
|
|
5276
|
-
|
|
5613
|
+
dropAt = void 0;
|
|
5277
5614
|
}
|
|
5278
|
-
await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(
|
|
5615
|
+
await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(dropAt));
|
|
5279
5616
|
}
|
|
5280
|
-
|
|
5281
|
-
this.derivedKeys.delete(oldComposite);
|
|
5617
|
+
copiedKeys.push(oldComposite);
|
|
5282
5618
|
}
|
|
5619
|
+
return copiedKeys;
|
|
5283
5620
|
}
|
|
5284
|
-
/**
|
|
5285
|
-
* Delete all tokens for an authorization id.
|
|
5286
|
-
*/
|
|
5287
|
-
async deleteAllForAuthorization(authorizationId) {
|
|
5288
|
-
const prefix = `${authorizationId}:`;
|
|
5289
|
-
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5290
|
-
for (const compositeKey of compositeKeys) {
|
|
5291
|
-
if (!compositeKey.startsWith(prefix)) continue;
|
|
5292
|
-
await this.storage.delete(this.storageKey(compositeKey));
|
|
5293
|
-
this.derivedKeys.delete(compositeKey);
|
|
5294
|
-
}
|
|
5295
|
-
}
|
|
5296
|
-
// ============================================
|
|
5297
|
-
// Internals
|
|
5298
|
-
// ============================================
|
|
5299
5621
|
async getRecord(authorizationId, providerId) {
|
|
5300
5622
|
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
5301
5623
|
const stored = await this.storage.get(this.storageKey(compositeKey));
|
|
@@ -5318,7 +5640,8 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5318
5640
|
return null;
|
|
5319
5641
|
}
|
|
5320
5642
|
}
|
|
5321
|
-
|
|
5643
|
+
const dropAt = recordDropAt(record);
|
|
5644
|
+
if (dropAt && dropAt < Date.now()) {
|
|
5322
5645
|
await this.storage.delete(this.storageKey(compositeKey));
|
|
5323
5646
|
return null;
|
|
5324
5647
|
}
|
|
@@ -6770,7 +7093,7 @@ var OrchestratedAuthorization = class _OrchestratedAuthorization extends Authori
|
|
|
6770
7093
|
const result = await this.#onTokenRefresh(providerId, refreshToken);
|
|
6771
7094
|
await this.#tokenStore.storeTokens(this.id, providerId, {
|
|
6772
7095
|
accessToken: result.accessToken,
|
|
6773
|
-
refreshToken: result.refreshToken,
|
|
7096
|
+
refreshToken: result.refreshToken ?? refreshToken,
|
|
6774
7097
|
expiresAt: result.expiresIn ? Date.now() + result.expiresIn * 1e3 : void 0
|
|
6775
7098
|
});
|
|
6776
7099
|
const currentState = this.#providerStates.get(providerId);
|
|
@@ -7539,7 +7862,7 @@ var CimdService = class {
|
|
|
7539
7862
|
cacheConfig;
|
|
7540
7863
|
securityConfig;
|
|
7541
7864
|
networkConfig;
|
|
7542
|
-
|
|
7865
|
+
cacheBackend;
|
|
7543
7866
|
logger;
|
|
7544
7867
|
/**
|
|
7545
7868
|
* Whether CIMD is enabled.
|
|
@@ -7559,7 +7882,6 @@ var CimdService = class {
|
|
|
7559
7882
|
this.cacheConfig = cimdCacheConfigSchema.parse(this.config.cache ?? {});
|
|
7560
7883
|
this.securityConfig = cimdSecurityConfigSchema.parse(this.config.security ?? {});
|
|
7561
7884
|
this.networkConfig = cimdNetworkConfigSchema.parse(this.config.network ?? {});
|
|
7562
|
-
this.cache = new InMemoryCimdCache(this.cacheConfig);
|
|
7563
7885
|
this.logger.debug("CimdService initialized", {
|
|
7564
7886
|
enabled: this.config.enabled,
|
|
7565
7887
|
cacheDefaultTtlMs: this.cacheConfig.defaultTtlMs,
|
|
@@ -7571,6 +7893,34 @@ var CimdService = class {
|
|
|
7571
7893
|
);
|
|
7572
7894
|
}
|
|
7573
7895
|
}
|
|
7896
|
+
/**
|
|
7897
|
+
* Create the cache now (connecting to Redis with `cache.type: 'redis'`), so a misconfigured or
|
|
7898
|
+
* unreachable cache fails at startup rather than on the first CIMD client.
|
|
7899
|
+
*/
|
|
7900
|
+
async initialize() {
|
|
7901
|
+
await this.cache();
|
|
7902
|
+
}
|
|
7903
|
+
/**
|
|
7904
|
+
* The cache `cache.type` selects, created once through the `createCimdCache` factory. A failed
|
|
7905
|
+
* creation is retried on the next use.
|
|
7906
|
+
*/
|
|
7907
|
+
cache() {
|
|
7908
|
+
if (!this.cacheBackend) {
|
|
7909
|
+
this.cacheBackend = createCimdCache(this.cacheConfig).catch((error) => {
|
|
7910
|
+
this.cacheBackend = void 0;
|
|
7911
|
+
throw error;
|
|
7912
|
+
});
|
|
7913
|
+
}
|
|
7914
|
+
return this.cacheBackend;
|
|
7915
|
+
}
|
|
7916
|
+
/** Close the cache's connection (Redis with `cache.type: 'redis'`), if one was created. */
|
|
7917
|
+
async dispose() {
|
|
7918
|
+
const backend = this.cacheBackend;
|
|
7919
|
+
this.cacheBackend = void 0;
|
|
7920
|
+
if (!backend) return;
|
|
7921
|
+
const createdBackend = await backend.catch(() => void 0);
|
|
7922
|
+
await createdBackend?.close?.();
|
|
7923
|
+
}
|
|
7574
7924
|
/**
|
|
7575
7925
|
* Check if a client_id is a CIMD URL.
|
|
7576
7926
|
*
|
|
@@ -7597,7 +7947,8 @@ var CimdService = class {
|
|
|
7597
7947
|
};
|
|
7598
7948
|
}
|
|
7599
7949
|
validateClientIdUrl(clientId, this.securityConfig);
|
|
7600
|
-
const
|
|
7950
|
+
const cache = await this.cache();
|
|
7951
|
+
const cached = await cache.get(clientId);
|
|
7601
7952
|
if (cached) {
|
|
7602
7953
|
this.logger.debug(`Cache hit for CIMD client: ${clientId}`);
|
|
7603
7954
|
return {
|
|
@@ -7615,8 +7966,8 @@ var CimdService = class {
|
|
|
7615
7966
|
if (this.securityConfig.warnOnLocalhostRedirects && hasOnlyLocalhostRedirectUris(document.redirect_uris)) {
|
|
7616
7967
|
this.logger.warn(`CIMD client "${clientId}" has only localhost redirect URIs - this may be a development client`);
|
|
7617
7968
|
}
|
|
7618
|
-
await
|
|
7619
|
-
const entry = await
|
|
7969
|
+
await cache.set(clientId, document, headers);
|
|
7970
|
+
const entry = await cache.get(clientId);
|
|
7620
7971
|
return {
|
|
7621
7972
|
isCimdClient: true,
|
|
7622
7973
|
metadata: document,
|
|
@@ -7646,11 +7997,12 @@ var CimdService = class {
|
|
|
7646
7997
|
* @param clientId - Optional client_id to clear; clears all if not provided
|
|
7647
7998
|
*/
|
|
7648
7999
|
async clearCache(clientId) {
|
|
8000
|
+
const cache = await this.cache();
|
|
7649
8001
|
if (clientId) {
|
|
7650
|
-
await
|
|
8002
|
+
await cache.delete(clientId);
|
|
7651
8003
|
this.logger.debug(`Cache cleared for: ${clientId}`);
|
|
7652
8004
|
} else {
|
|
7653
|
-
await
|
|
8005
|
+
await cache.clear();
|
|
7654
8006
|
this.logger.debug("Cache cleared");
|
|
7655
8007
|
}
|
|
7656
8008
|
}
|
|
@@ -7658,8 +8010,9 @@ var CimdService = class {
|
|
|
7658
8010
|
* Get cache statistics.
|
|
7659
8011
|
*/
|
|
7660
8012
|
async getCacheStats() {
|
|
8013
|
+
const cache = await this.cache();
|
|
7661
8014
|
return {
|
|
7662
|
-
size: await
|
|
8015
|
+
size: await cache.size()
|
|
7663
8016
|
};
|
|
7664
8017
|
}
|
|
7665
8018
|
/**
|
|
@@ -7671,7 +8024,8 @@ var CimdService = class {
|
|
|
7671
8024
|
const controller = new AbortController();
|
|
7672
8025
|
const timer = setTimeout(() => controller.abort(), this.networkConfig.timeoutMs);
|
|
7673
8026
|
try {
|
|
7674
|
-
const
|
|
8027
|
+
const cache = await this.cache();
|
|
8028
|
+
const conditionalHeaders = await cache.getConditionalHeaders(clientId);
|
|
7675
8029
|
const originalOrigin = new URL(clientId).origin;
|
|
7676
8030
|
const maxRedirects = this.networkConfig.maxRedirects;
|
|
7677
8031
|
let currentUrl = clientId;
|
|
@@ -7694,9 +8048,9 @@ var CimdService = class {
|
|
|
7694
8048
|
redirect: "manual"
|
|
7695
8049
|
});
|
|
7696
8050
|
if (response.status === 304) {
|
|
7697
|
-
const staleEntry = await
|
|
8051
|
+
const staleEntry = await cache.getStale(clientId);
|
|
7698
8052
|
if (staleEntry) {
|
|
7699
|
-
await
|
|
8053
|
+
await cache.revalidate(clientId, response.headers);
|
|
7700
8054
|
this.logger.debug(`CIMD document not modified: ${clientId}`);
|
|
7701
8055
|
return {
|
|
7702
8056
|
document: staleEntry.document,
|
|
@@ -8204,7 +8558,7 @@ var tokenRefreshConfigSchema = z10.object({
|
|
|
8204
8558
|
* Refresh token before expiry by this many seconds
|
|
8205
8559
|
* @default 60
|
|
8206
8560
|
*/
|
|
8207
|
-
skewSeconds: z10.number().default(60)
|
|
8561
|
+
skewSeconds: z10.number().nonnegative().default(60)
|
|
8208
8562
|
});
|
|
8209
8563
|
var skippedAppBehaviorSchema = z10.enum(["anonymous", "require-auth"]);
|
|
8210
8564
|
var consentConfigSchema = z10.object({
|
|
@@ -8317,7 +8671,7 @@ var publicAuthOptionsSchema = z11.object({
|
|
|
8317
8671
|
* Anonymous session TTL in seconds
|
|
8318
8672
|
* @default 3600 (1 hour)
|
|
8319
8673
|
*/
|
|
8320
|
-
sessionTtl: z11.number().default(3600),
|
|
8674
|
+
sessionTtl: z11.number().int().positive().default(3600),
|
|
8321
8675
|
/**
|
|
8322
8676
|
* Scopes granted to anonymous sessions
|
|
8323
8677
|
* @default ['anonymous']
|