@frontmcp/auth 1.10.15 → 1.11.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/authorities/authorities.context.mjs +115 -0
- package/browser/authorities/authorities.engine.mjs +272 -0
- package/browser/authorities/authorities.errors.mjs +59 -0
- package/browser/authorities/authorities.evaluator.mjs +225 -0
- package/browser/authorities/authorities.metadata-augment.mjs +18 -0
- package/browser/authorities/authorities.registry.mjs +112 -0
- package/browser/authorities/authorities.schema.mjs +92 -0
- package/browser/authorities/authorities.scope-mapping.mjs +30 -0
- package/browser/authorities/authorities.validation.mjs +208 -0
- package/browser/authorities/index.mjs +43 -0
- package/browser/authorization/authorization.class.mjs +252 -0
- package/browser/authorization/authorization.types.mjs +64 -0
- package/browser/authorization/dcr-client.registry.mjs +222 -0
- package/browser/authorization/index.mjs +41 -0
- package/browser/authorization/orchestrated.accessor.mjs +86 -0
- package/browser/authorization/orchestrated.authorization.mjs +289 -0
- package/browser/authorization/public.authorization.mjs +124 -0
- package/browser/authorization/transparent.authorization.mjs +130 -0
- package/browser/cimd/cimd-redis.cache.mjs +231 -0
- package/browser/cimd/cimd.cache.mjs +215 -0
- package/browser/cimd/cimd.errors.mjs +142 -0
- package/browser/cimd/cimd.logger.mjs +15 -0
- package/browser/cimd/cimd.service.mjs +376 -0
- package/browser/cimd/cimd.types.mjs +228 -0
- package/browser/cimd/cimd.validator.mjs +217 -0
- package/browser/cimd/index.mjs +63 -0
- package/browser/common/auth-logger.interface.mjs +14 -0
- package/browser/common/index.mjs +6 -0
- package/browser/common/jwt.types.mjs +38 -0
- package/browser/common/session-user.types.mjs +0 -0
- package/browser/common/session.types.mjs +58 -0
- package/browser/common/zod-utils.mjs +0 -0
- package/browser/consent/consent.types.mjs +94 -0
- package/browser/consent/index.mjs +17 -0
- package/browser/context/frontmcp-auth-context.factory.mjs +29 -0
- package/browser/context/frontmcp-auth-context.impl.mjs +112 -0
- package/browser/context/index.mjs +7 -0
- package/browser/detection/auth-provider-detection.mjs +176 -0
- package/browser/detection/index.mjs +19 -0
- package/browser/errors/auth-internal.error.mjs +42 -0
- package/browser/errors/auth-internal.errors.mjs +150 -0
- package/browser/errors/index.mjs +53 -0
- package/browser/fetch/fetch-credential-middleware.mjs +91 -0
- package/browser/fetch/index.mjs +19 -0
- package/browser/index.mjs +573 -0
- package/browser/jwks/index.mjs +9 -0
- package/browser/jwks/jose.loader.mjs +28 -0
- package/browser/jwks/jwks.service.mjs +521 -0
- package/browser/jwks/jwks.utils.mjs +33 -0
- package/browser/options/app-auth.schema.mjs +18 -0
- package/browser/options/index.mjs +89 -0
- package/browser/options/orchestrated.schema.mjs +128 -0
- package/browser/options/public.schema.mjs +39 -0
- package/browser/options/schema.mjs +16 -0
- package/browser/options/shared.schemas.mjs +506 -0
- package/browser/options/static.schema.mjs +41 -0
- package/browser/options/transparent.schema.mjs +43 -0
- package/browser/options/utils.mjs +47 -0
- package/browser/people/index.mjs +15 -0
- package/browser/people/loopback-token.auth.mjs +115 -0
- package/browser/people/people-auth.options.mjs +37 -0
- package/browser/session/authorization-vault.mjs +279 -0
- package/browser/session/authorization.store.mjs +336 -0
- package/browser/session/consent.store.mjs +34 -0
- package/browser/session/credential-resume-link.mjs +43 -0
- package/browser/session/credentials-accessor.impl.mjs +54 -0
- package/browser/session/credentials-accessor.mjs +5 -0
- package/browser/session/encrypted-authorization-vault.mjs +405 -0
- package/browser/session/federated-auth.session.mjs +176 -0
- package/browser/session/incremental-auth-ticket.mjs +42 -0
- package/browser/session/index.mjs +266 -0
- package/browser/session/orchestrated-token.crypto.mjs +47 -0
- package/browser/session/orchestrated-token.store.mjs +286 -0
- package/browser/session/pending-login.mjs +37 -0
- package/browser/session/redis-session.store.mjs +255 -0
- package/browser/session/secure-store-accessor.impl.mjs +78 -0
- package/browser/session/secure-store-accessor.mjs +5 -0
- package/browser/session/secure-store-backends.mjs +111 -0
- package/browser/session/secure-store.factory.mjs +77 -0
- package/browser/session/session-credential-vault.factory.mjs +18 -0
- package/browser/session/session-credential-vault.mjs +243 -0
- package/browser/session/session-crypto.mjs +39 -0
- package/browser/session/session-rate-limiter.mjs +126 -0
- package/browser/session/session.transport.mjs +16 -0
- package/browser/session/signin-binding.mjs +30 -0
- package/browser/session/storage/in-memory-authorization-vault.mjs +28 -0
- package/browser/session/storage/index.mjs +14 -0
- package/browser/session/storage/storage-authorization-vault.mjs +301 -0
- package/browser/session/storage/storage-token-store.mjs +83 -0
- package/browser/session/storage-authorization.store.mjs +134 -0
- package/browser/session/storage-consent.store.mjs +33 -0
- package/browser/session/storage-federated-auth.session.mjs +55 -0
- package/browser/session/storage-orchestrated-token.store.mjs +197 -0
- package/browser/session/token-storage.factory.mjs +58 -0
- package/browser/session/token.refresh.mjs +36 -0
- package/browser/session/token.vault.mjs +61 -0
- package/browser/session/transport-session.types.mjs +108 -0
- package/browser/session/utils/auth-token.utils.mjs +58 -0
- package/browser/session/utils/index.mjs +23 -0
- package/browser/session/utils/session-crypto.utils.mjs +49 -0
- package/browser/session/utils/tiny-ttl-cache.mjs +46 -0
- package/browser/session/vault-encryption.mjs +187 -0
- package/browser/session/vercel-kv-session.store.mjs +230 -0
- package/browser/shared-server/index.mjs +17 -0
- package/browser/shared-server/shared-server-identity.mjs +55 -0
- package/browser/ui/base-layout.mjs +184 -0
- package/browser/ui/index.mjs +51 -0
- package/browser/ui/local-login.helper.mjs +46 -0
- package/browser/ui/templates.mjs +482 -0
- package/browser/utils/audience.validator.mjs +110 -0
- package/browser/utils/authorization-id.utils.mjs +10 -0
- package/browser/utils/index.mjs +45 -0
- package/browser/utils/redirect-uri.utils.mjs +22 -0
- package/browser/utils/scope-grant.utils.mjs +36 -0
- package/browser/utils/token-scopes.utils.mjs +12 -0
- package/browser/utils/www-authenticate.utils.mjs +164 -0
- package/browser/vault/auth-providers.accessor.impl.mjs +236 -0
- package/browser/vault/auth-providers.accessor.mjs +5 -0
- package/browser/vault/auth-providers.registry.mjs +140 -0
- package/browser/vault/auth-providers.types.mjs +47 -0
- package/browser/vault/auth-providers.vault.mjs +206 -0
- package/browser/vault/credential-cache.mjs +187 -0
- package/browser/vault/credential-helpers.mjs +15 -0
- package/browser/vault/credential-loaders/eager-loader.mjs +91 -0
- package/browser/vault/credential-loaders/lazy-loader.mjs +120 -0
- package/browser/vault/index.mjs +35 -0
- package/cimd/cimd.types.d.ts +1 -1
- package/cimd/cimd.types.d.ts.map +1 -1
- package/common/jwt.types.d.ts +1 -1
- package/common/jwt.types.d.ts.map +1 -1
- package/common/session.types.d.ts +9 -1
- package/common/session.types.d.ts.map +1 -1
- package/consent/consent.types.d.ts +1 -1
- package/consent/consent.types.d.ts.map +1 -1
- package/detection/auth-provider-detection.d.ts +1 -1
- package/detection/auth-provider-detection.d.ts.map +1 -1
- package/errors/auth-internal.errors.d.ts +6 -0
- package/errors/auth-internal.errors.d.ts.map +1 -1
- package/errors/index.d.ts +1 -1
- package/errors/index.d.ts.map +1 -1
- package/esm/index.mjs +2 -2
- package/esm/package.json +21 -5
- package/index.d.ts +5 -1
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/options/app-auth.schema.d.ts +19 -1
- package/options/app-auth.schema.d.ts.map +1 -1
- package/options/orchestrated.schema.d.ts +37 -1
- package/options/orchestrated.schema.d.ts.map +1 -1
- package/options/public.schema.d.ts +1 -1
- package/options/public.schema.d.ts.map +1 -1
- package/options/schema.d.ts +19 -1
- package/options/schema.d.ts.map +1 -1
- package/options/shared.schemas.d.ts +10 -14
- package/options/shared.schemas.d.ts.map +1 -1
- package/options/static.schema.d.ts +1 -1
- package/options/static.schema.d.ts.map +1 -1
- package/options/transparent.schema.d.ts +1 -1
- package/options/transparent.schema.d.ts.map +1 -1
- package/package.json +21 -5
- package/people/index.d.ts +3 -0
- package/people/index.d.ts.map +1 -0
- package/people/loopback-token.auth.d.ts +75 -0
- package/people/loopback-token.auth.d.ts.map +1 -0
- package/people/people-auth.options.d.ts +36 -0
- package/people/people-auth.options.d.ts.map +1 -0
- package/session/authorization-vault.d.ts +1 -1
- package/session/authorization-vault.d.ts.map +1 -1
- package/session/encrypted-authorization-vault.d.ts +1 -1
- package/session/encrypted-authorization-vault.d.ts.map +1 -1
- package/session/secure-store.factory.d.ts.map +1 -1
- package/shared-server/index.d.ts +3 -0
- package/shared-server/index.d.ts.map +1 -0
- package/shared-server/shared-server-identity.d.ts +26 -0
- package/shared-server/shared-server-identity.d.ts.map +1 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// libs/auth/src/cimd/cimd-redis.cache.ts
|
|
2
|
+
import { RedisStorageAdapter } from "@frontmcp/utils";
|
|
3
|
+
import { sha256Hex } from "@frontmcp/utils";
|
|
4
|
+
import { extractCacheHeaders, parseCacheHeaders } from "./cimd.cache.mjs";
|
|
5
|
+
var RedisCimdCache = class {
|
|
6
|
+
redis;
|
|
7
|
+
keyPrefix;
|
|
8
|
+
config;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
if (!config.redis) {
|
|
11
|
+
throw new Error("Redis configuration is required for RedisCimdCache");
|
|
12
|
+
}
|
|
13
|
+
this.config = {
|
|
14
|
+
defaultTtlMs: config.defaultTtlMs ?? 36e5,
|
|
15
|
+
maxTtlMs: config.maxTtlMs ?? 864e5,
|
|
16
|
+
minTtlMs: config.minTtlMs ?? 6e4
|
|
17
|
+
};
|
|
18
|
+
this.keyPrefix = config.redis.keyPrefix ?? "cimd:";
|
|
19
|
+
const redisOptions = {};
|
|
20
|
+
if (config.redis.url) {
|
|
21
|
+
redisOptions.url = config.redis.url;
|
|
22
|
+
} else if (config.redis.host) {
|
|
23
|
+
redisOptions.config = {
|
|
24
|
+
host: config.redis.host,
|
|
25
|
+
port: config.redis.port,
|
|
26
|
+
password: config.redis.password,
|
|
27
|
+
db: config.redis.db,
|
|
28
|
+
tls: config.redis.tls
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
this.redis = new RedisStorageAdapter(redisOptions);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Connect to Redis.
|
|
35
|
+
* Must be called before using any cache operations.
|
|
36
|
+
*/
|
|
37
|
+
async connect() {
|
|
38
|
+
await this.redis.connect();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate a Redis key for a client ID.
|
|
42
|
+
* Uses SHA-256 hash to handle URLs with special characters.
|
|
43
|
+
*/
|
|
44
|
+
cacheKey(clientId) {
|
|
45
|
+
const hash = sha256Hex(clientId);
|
|
46
|
+
return `${this.keyPrefix}${hash}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get a cached entry by client_id.
|
|
50
|
+
*
|
|
51
|
+
* @param clientId - The client_id URL
|
|
52
|
+
* @returns The cached entry if valid, or undefined
|
|
53
|
+
*/
|
|
54
|
+
async get(clientId) {
|
|
55
|
+
const key = this.cacheKey(clientId);
|
|
56
|
+
const data = await this.redis.get(key);
|
|
57
|
+
if (!data) {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const entry = JSON.parse(data);
|
|
62
|
+
if (entry.expiresAt < Date.now()) {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
return entry;
|
|
66
|
+
} catch {
|
|
67
|
+
await this.redis.delete(key);
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get a stale entry for conditional revalidation.
|
|
73
|
+
*
|
|
74
|
+
* @param clientId - The client_id URL
|
|
75
|
+
* @returns The stale entry (even if expired), or undefined if not cached
|
|
76
|
+
*/
|
|
77
|
+
async getStale(clientId) {
|
|
78
|
+
const key = this.cacheKey(clientId);
|
|
79
|
+
const data = await this.redis.get(key);
|
|
80
|
+
if (!data) {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(data);
|
|
85
|
+
} catch {
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Store a document in the cache.
|
|
91
|
+
*
|
|
92
|
+
* @param clientId - The client_id URL
|
|
93
|
+
* @param document - The metadata document
|
|
94
|
+
* @param headers - HTTP response headers
|
|
95
|
+
*/
|
|
96
|
+
async set(clientId, document, headers) {
|
|
97
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
98
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const entry = {
|
|
101
|
+
document,
|
|
102
|
+
expiresAt: now + ttlMs,
|
|
103
|
+
etag,
|
|
104
|
+
lastModified,
|
|
105
|
+
cachedAt: now
|
|
106
|
+
};
|
|
107
|
+
const key = this.cacheKey(clientId);
|
|
108
|
+
const value = JSON.stringify(entry);
|
|
109
|
+
const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
|
|
110
|
+
await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Update an existing cache entry (after 304 Not Modified).
|
|
114
|
+
*
|
|
115
|
+
* @param clientId - The client_id URL
|
|
116
|
+
* @param headers - New HTTP headers with updated cache directives
|
|
117
|
+
*/
|
|
118
|
+
async revalidate(clientId, headers) {
|
|
119
|
+
const key = this.cacheKey(clientId);
|
|
120
|
+
const data = await this.redis.get(key);
|
|
121
|
+
if (!data) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const existing = JSON.parse(data);
|
|
126
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
127
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
128
|
+
existing.expiresAt = Date.now() + ttlMs;
|
|
129
|
+
if (etag) existing.etag = etag;
|
|
130
|
+
if (lastModified) existing.lastModified = lastModified;
|
|
131
|
+
const value = JSON.stringify(existing);
|
|
132
|
+
const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
|
|
133
|
+
await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
|
|
134
|
+
return true;
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Delete a cache entry.
|
|
141
|
+
*
|
|
142
|
+
* @param clientId - The client_id URL
|
|
143
|
+
* @returns true if an entry was deleted
|
|
144
|
+
*/
|
|
145
|
+
async delete(clientId) {
|
|
146
|
+
const key = this.cacheKey(clientId);
|
|
147
|
+
return this.redis.delete(key);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get conditional request headers for a cached entry.
|
|
151
|
+
*
|
|
152
|
+
* @param clientId - The client_id URL
|
|
153
|
+
* @returns Headers for conditional request, or undefined if not cached
|
|
154
|
+
*/
|
|
155
|
+
async getConditionalHeaders(clientId) {
|
|
156
|
+
const key = this.cacheKey(clientId);
|
|
157
|
+
const data = await this.redis.get(key);
|
|
158
|
+
if (!data) {
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const entry = JSON.parse(data);
|
|
163
|
+
const headers = {};
|
|
164
|
+
if (entry.etag) {
|
|
165
|
+
headers["If-None-Match"] = entry.etag;
|
|
166
|
+
}
|
|
167
|
+
if (entry.lastModified) {
|
|
168
|
+
headers["If-Modified-Since"] = entry.lastModified;
|
|
169
|
+
}
|
|
170
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
171
|
+
} catch {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Clear all cached entries.
|
|
177
|
+
* Uses Redis SCAN to find and delete all keys with our prefix.
|
|
178
|
+
*/
|
|
179
|
+
async clear() {
|
|
180
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
181
|
+
if (keys.length > 0) {
|
|
182
|
+
await this.redis.mdelete(keys);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get the number of cached entries.
|
|
187
|
+
* Uses Redis SCAN to count keys with our prefix.
|
|
188
|
+
*/
|
|
189
|
+
async size() {
|
|
190
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
191
|
+
return keys.length;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Remove expired entries.
|
|
195
|
+
*
|
|
196
|
+
* Note: Redis handles expiration automatically via TTL.
|
|
197
|
+
* This method is primarily for explicit cleanup of entries that are
|
|
198
|
+
* well past their HTTP cache expiration but still within Redis TTL.
|
|
199
|
+
*
|
|
200
|
+
* @returns Number of entries removed
|
|
201
|
+
*/
|
|
202
|
+
async cleanup() {
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
let removed = 0;
|
|
205
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
206
|
+
for (const key of keys) {
|
|
207
|
+
const data = await this.redis.get(key);
|
|
208
|
+
if (!data) continue;
|
|
209
|
+
try {
|
|
210
|
+
const entry = JSON.parse(data);
|
|
211
|
+
if (entry.expiresAt + this.config.maxTtlMs * 2 < now) {
|
|
212
|
+
await this.redis.delete(key);
|
|
213
|
+
removed++;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
await this.redis.delete(key);
|
|
217
|
+
removed++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return removed;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Close the Redis connection.
|
|
224
|
+
*/
|
|
225
|
+
async close() {
|
|
226
|
+
await this.redis.disconnect();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
export {
|
|
230
|
+
RedisCimdCache
|
|
231
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// libs/auth/src/cimd/cimd.cache.ts
|
|
2
|
+
function extractCacheHeaders(headers) {
|
|
3
|
+
return {
|
|
4
|
+
"cache-control": headers.get("cache-control") ?? void 0,
|
|
5
|
+
expires: headers.get("expires") ?? void 0,
|
|
6
|
+
etag: headers.get("etag") ?? void 0,
|
|
7
|
+
"last-modified": headers.get("last-modified") ?? void 0,
|
|
8
|
+
age: headers.get("age") ?? void 0
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function parseCacheHeaders(headers, config) {
|
|
12
|
+
let ttlMs = config.defaultTtlMs;
|
|
13
|
+
if (headers["cache-control"]) {
|
|
14
|
+
const cacheControl = parseCacheControlHeader(headers["cache-control"]);
|
|
15
|
+
if (cacheControl["no-store"] || cacheControl["no-cache"]) {
|
|
16
|
+
ttlMs = config.minTtlMs;
|
|
17
|
+
} else if (typeof cacheControl["max-age"] === "number") {
|
|
18
|
+
let maxAgeSecs = cacheControl["max-age"];
|
|
19
|
+
if (headers.age) {
|
|
20
|
+
const ageSeconds = parseInt(headers.age, 10);
|
|
21
|
+
if (!isNaN(ageSeconds)) {
|
|
22
|
+
maxAgeSecs = Math.max(0, maxAgeSecs - ageSeconds);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
ttlMs = maxAgeSecs * 1e3;
|
|
26
|
+
}
|
|
27
|
+
if (typeof cacheControl["s-maxage"] === "number") {
|
|
28
|
+
let sMaxAgeSecs = cacheControl["s-maxage"];
|
|
29
|
+
if (headers.age) {
|
|
30
|
+
const ageSeconds = parseInt(headers.age, 10);
|
|
31
|
+
if (!isNaN(ageSeconds)) {
|
|
32
|
+
sMaxAgeSecs = Math.max(0, sMaxAgeSecs - ageSeconds);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
ttlMs = sMaxAgeSecs * 1e3;
|
|
36
|
+
}
|
|
37
|
+
} else if (headers.expires) {
|
|
38
|
+
const expiresDate = new Date(headers.expires);
|
|
39
|
+
if (!isNaN(expiresDate.getTime())) {
|
|
40
|
+
ttlMs = Math.max(0, expiresDate.getTime() - Date.now());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
ttlMs = Math.max(config.minTtlMs, Math.min(config.maxTtlMs, ttlMs));
|
|
44
|
+
return {
|
|
45
|
+
ttlMs,
|
|
46
|
+
etag: headers.etag,
|
|
47
|
+
lastModified: headers["last-modified"]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function parseCacheControlHeader(value) {
|
|
51
|
+
const result = {};
|
|
52
|
+
const directives = value.toLowerCase().split(",").map((d) => d.trim());
|
|
53
|
+
for (const directive of directives) {
|
|
54
|
+
const [key, val] = directive.split("=").map((s) => s.trim());
|
|
55
|
+
if (val !== void 0) {
|
|
56
|
+
const numVal = parseInt(val, 10);
|
|
57
|
+
if (!isNaN(numVal)) {
|
|
58
|
+
result[key] = numVal;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
result[key] = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
var InMemoryCimdCache = class {
|
|
67
|
+
cache = /* @__PURE__ */ new Map();
|
|
68
|
+
config;
|
|
69
|
+
constructor(config) {
|
|
70
|
+
this.config = {
|
|
71
|
+
defaultTtlMs: config?.defaultTtlMs ?? 36e5,
|
|
72
|
+
maxTtlMs: config?.maxTtlMs ?? 864e5,
|
|
73
|
+
minTtlMs: config?.minTtlMs ?? 6e4
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get a cached entry by client_id.
|
|
78
|
+
*
|
|
79
|
+
* @param clientId - The client_id URL
|
|
80
|
+
* @returns The cached entry if valid, or undefined
|
|
81
|
+
*/
|
|
82
|
+
async get(clientId) {
|
|
83
|
+
const entry = this.cache.get(clientId);
|
|
84
|
+
if (!entry) {
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
if (entry.expiresAt < Date.now()) {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get a stale entry for conditional revalidation.
|
|
94
|
+
*
|
|
95
|
+
* @param clientId - The client_id URL
|
|
96
|
+
* @returns The stale entry (even if expired), or undefined if not cached
|
|
97
|
+
*/
|
|
98
|
+
async getStale(clientId) {
|
|
99
|
+
return this.cache.get(clientId);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Store a document in the cache.
|
|
103
|
+
*
|
|
104
|
+
* @param clientId - The client_id URL
|
|
105
|
+
* @param document - The metadata document
|
|
106
|
+
* @param headers - HTTP response headers
|
|
107
|
+
*/
|
|
108
|
+
async set(clientId, document, headers) {
|
|
109
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
110
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const entry = {
|
|
113
|
+
document,
|
|
114
|
+
expiresAt: now + ttlMs,
|
|
115
|
+
etag,
|
|
116
|
+
lastModified,
|
|
117
|
+
cachedAt: now
|
|
118
|
+
};
|
|
119
|
+
this.cache.set(clientId, entry);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Update an existing cache entry (after 304 Not Modified).
|
|
123
|
+
*
|
|
124
|
+
* @param clientId - The client_id URL
|
|
125
|
+
* @param headers - New HTTP headers with updated cache directives
|
|
126
|
+
*/
|
|
127
|
+
async revalidate(clientId, headers) {
|
|
128
|
+
const existing = this.cache.get(clientId);
|
|
129
|
+
if (!existing) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
133
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
134
|
+
existing.expiresAt = Date.now() + ttlMs;
|
|
135
|
+
if (etag) existing.etag = etag;
|
|
136
|
+
if (lastModified) existing.lastModified = lastModified;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Delete a cache entry.
|
|
141
|
+
*
|
|
142
|
+
* @param clientId - The client_id URL
|
|
143
|
+
* @returns true if an entry was deleted
|
|
144
|
+
*/
|
|
145
|
+
async delete(clientId) {
|
|
146
|
+
return this.cache.delete(clientId);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get conditional request headers for a cached entry.
|
|
150
|
+
*
|
|
151
|
+
* @param clientId - The client_id URL
|
|
152
|
+
* @returns Headers for conditional request, or undefined if not cached
|
|
153
|
+
*/
|
|
154
|
+
async getConditionalHeaders(clientId) {
|
|
155
|
+
const entry = this.cache.get(clientId);
|
|
156
|
+
if (!entry) {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
const headers = {};
|
|
160
|
+
if (entry.etag) {
|
|
161
|
+
headers["If-None-Match"] = entry.etag;
|
|
162
|
+
}
|
|
163
|
+
if (entry.lastModified) {
|
|
164
|
+
headers["If-Modified-Since"] = entry.lastModified;
|
|
165
|
+
}
|
|
166
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Clear all cached entries.
|
|
170
|
+
*/
|
|
171
|
+
async clear() {
|
|
172
|
+
this.cache.clear();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get the number of cached entries.
|
|
176
|
+
*/
|
|
177
|
+
async size() {
|
|
178
|
+
return this.cache.size;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Remove expired entries.
|
|
182
|
+
*
|
|
183
|
+
* @returns Number of entries removed
|
|
184
|
+
*/
|
|
185
|
+
async cleanup() {
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
let removed = 0;
|
|
188
|
+
for (const [clientId, entry] of this.cache) {
|
|
189
|
+
if (entry.expiresAt + this.config.maxTtlMs * 2 < now) {
|
|
190
|
+
this.cache.delete(clientId);
|
|
191
|
+
removed++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return removed;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
async function createCimdCache(config) {
|
|
198
|
+
const cacheType = config?.type ?? "memory";
|
|
199
|
+
if (cacheType === "redis") {
|
|
200
|
+
if (!config?.redis) {
|
|
201
|
+
throw new Error('Redis configuration is required when cache type is "redis"');
|
|
202
|
+
}
|
|
203
|
+
const { RedisCimdCache } = await import("./cimd-redis.cache.mjs");
|
|
204
|
+
const cache = new RedisCimdCache(config);
|
|
205
|
+
await cache.connect();
|
|
206
|
+
return cache;
|
|
207
|
+
}
|
|
208
|
+
return new InMemoryCimdCache(config);
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
InMemoryCimdCache,
|
|
212
|
+
createCimdCache,
|
|
213
|
+
extractCacheHeaders,
|
|
214
|
+
parseCacheHeaders
|
|
215
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// libs/auth/src/cimd/cimd.errors.ts
|
|
2
|
+
var CimdError = class extends Error {
|
|
3
|
+
/**
|
|
4
|
+
* Error code for machine-readable identification.
|
|
5
|
+
*/
|
|
6
|
+
code;
|
|
7
|
+
/**
|
|
8
|
+
* HTTP status code to return when this error occurs.
|
|
9
|
+
*/
|
|
10
|
+
statusCode;
|
|
11
|
+
/**
|
|
12
|
+
* The client_id URL that caused the error.
|
|
13
|
+
*/
|
|
14
|
+
clientIdUrl;
|
|
15
|
+
constructor(message, code, statusCode, clientIdUrl) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = this.constructor.name;
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.clientIdUrl = clientIdUrl;
|
|
21
|
+
if (Error.captureStackTrace) {
|
|
22
|
+
Error.captureStackTrace(this, this.constructor);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get a message safe to return to clients.
|
|
27
|
+
* Override in subclasses to provide user-friendly messages.
|
|
28
|
+
*/
|
|
29
|
+
getPublicMessage() {
|
|
30
|
+
return this.message;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var InvalidClientIdUrlError = class extends CimdError {
|
|
34
|
+
reason;
|
|
35
|
+
constructor(clientIdUrl, reason) {
|
|
36
|
+
super(`Invalid CIMD client_id URL: ${reason}`, "INVALID_CLIENT_ID_URL", 400, clientIdUrl);
|
|
37
|
+
this.reason = reason;
|
|
38
|
+
}
|
|
39
|
+
getPublicMessage() {
|
|
40
|
+
return `Invalid client_id URL: ${this.reason}`;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var CimdFetchError = class extends CimdError {
|
|
44
|
+
httpStatus;
|
|
45
|
+
originalError;
|
|
46
|
+
constructor(clientIdUrl, message, options) {
|
|
47
|
+
super(`Failed to fetch CIMD document from ${clientIdUrl}: ${message}`, "CIMD_FETCH_ERROR", 502, clientIdUrl);
|
|
48
|
+
this.httpStatus = options?.httpStatus;
|
|
49
|
+
this.originalError = options?.originalError;
|
|
50
|
+
}
|
|
51
|
+
getPublicMessage() {
|
|
52
|
+
if (this.httpStatus) {
|
|
53
|
+
return `Failed to fetch client metadata: HTTP ${this.httpStatus}`;
|
|
54
|
+
}
|
|
55
|
+
return "Failed to fetch client metadata document";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var CimdValidationError = class extends CimdError {
|
|
59
|
+
validationErrors;
|
|
60
|
+
constructor(clientIdUrl, errors) {
|
|
61
|
+
super(`CIMD document validation failed: ${errors.join("; ")}`, "CIMD_VALIDATION_ERROR", 400, clientIdUrl);
|
|
62
|
+
this.validationErrors = errors;
|
|
63
|
+
}
|
|
64
|
+
getPublicMessage() {
|
|
65
|
+
return `Client metadata document validation failed: ${this.validationErrors.join("; ")}`;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var CimdClientIdMismatchError = class extends CimdError {
|
|
69
|
+
documentClientId;
|
|
70
|
+
constructor(urlClientId, documentClientId) {
|
|
71
|
+
super(
|
|
72
|
+
`CIMD client_id mismatch: URL is "${urlClientId}" but document contains "${documentClientId}"`,
|
|
73
|
+
"CIMD_CLIENT_ID_MISMATCH",
|
|
74
|
+
400,
|
|
75
|
+
urlClientId
|
|
76
|
+
);
|
|
77
|
+
this.documentClientId = documentClientId;
|
|
78
|
+
}
|
|
79
|
+
getPublicMessage() {
|
|
80
|
+
return "Client ID in metadata document does not match the request";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var CimdSecurityError = class extends CimdError {
|
|
84
|
+
securityReason;
|
|
85
|
+
constructor(clientIdUrl, reason) {
|
|
86
|
+
super(`CIMD security check failed for ${clientIdUrl}: ${reason}`, "CIMD_SECURITY_ERROR", 403, clientIdUrl);
|
|
87
|
+
this.securityReason = reason;
|
|
88
|
+
}
|
|
89
|
+
getPublicMessage() {
|
|
90
|
+
return "Client ID URL is not allowed by security policy";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var RedirectUriMismatchError = class extends CimdError {
|
|
94
|
+
requestedRedirectUri;
|
|
95
|
+
allowedRedirectUris;
|
|
96
|
+
constructor(clientIdUrl, requestedRedirectUri, allowedRedirectUris) {
|
|
97
|
+
super(
|
|
98
|
+
`Redirect URI "${requestedRedirectUri}" is not registered for client "${clientIdUrl}"`,
|
|
99
|
+
"REDIRECT_URI_MISMATCH",
|
|
100
|
+
400,
|
|
101
|
+
clientIdUrl
|
|
102
|
+
);
|
|
103
|
+
this.requestedRedirectUri = requestedRedirectUri;
|
|
104
|
+
this.allowedRedirectUris = allowedRedirectUris;
|
|
105
|
+
}
|
|
106
|
+
getPublicMessage() {
|
|
107
|
+
return "The redirect_uri is not registered for this client";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var CimdResponseTooLargeError = class extends CimdError {
|
|
111
|
+
maxBytes;
|
|
112
|
+
actualBytes;
|
|
113
|
+
constructor(clientIdUrl, maxBytes, actualBytes) {
|
|
114
|
+
super(
|
|
115
|
+
`CIMD response from ${clientIdUrl} exceeds maximum size of ${maxBytes} bytes${actualBytes ? ` (received ${actualBytes} bytes)` : ""}`,
|
|
116
|
+
"CIMD_RESPONSE_TOO_LARGE",
|
|
117
|
+
502,
|
|
118
|
+
clientIdUrl
|
|
119
|
+
);
|
|
120
|
+
this.maxBytes = maxBytes;
|
|
121
|
+
this.actualBytes = actualBytes;
|
|
122
|
+
}
|
|
123
|
+
getPublicMessage() {
|
|
124
|
+
return "Client metadata document is too large";
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var CimdDisabledError = class extends CimdError {
|
|
128
|
+
constructor() {
|
|
129
|
+
super("CIMD (Client ID Metadata Documents) is disabled on this server", "CIMD_DISABLED", 400);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
export {
|
|
133
|
+
CimdClientIdMismatchError,
|
|
134
|
+
CimdDisabledError,
|
|
135
|
+
CimdError,
|
|
136
|
+
CimdFetchError,
|
|
137
|
+
CimdResponseTooLargeError,
|
|
138
|
+
CimdSecurityError,
|
|
139
|
+
CimdValidationError,
|
|
140
|
+
InvalidClientIdUrlError,
|
|
141
|
+
RedirectUriMismatchError
|
|
142
|
+
};
|