@frontmcp/auth 1.12.0-rc.1 → 1.12.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/authorities/authorities.context.d.ts +7 -0
- package/authorities/authorities.context.d.ts.map +1 -1
- package/authorities/authorities.engine.d.ts +10 -4
- package/authorities/authorities.engine.d.ts.map +1 -1
- package/authorities/authorities.evaluator.d.ts +2 -0
- package/authorities/authorities.evaluator.d.ts.map +1 -1
- package/authorities/authorities.validation.d.ts.map +1 -1
- package/authorization/dcr-client.registry.d.ts.map +1 -1
- package/authorization/public.authorization.d.ts +3 -2
- package/authorization/public.authorization.d.ts.map +1 -1
- package/browser/authorities/authorities.context.mjs +5 -0
- package/browser/authorities/authorities.engine.mjs +45 -9
- package/browser/authorities/authorities.evaluator.mjs +36 -10
- package/browser/authorities/authorities.validation.mjs +3 -1
- package/browser/authorization/authorization.class.mjs +3 -3
- package/browser/authorization/dcr-client.registry.mjs +8 -0
- package/browser/authorization/public.authorization.mjs +10 -4
- package/browser/cimd/cimd-redis.cache.mjs +2 -0
- package/browser/cimd/cimd.service.mjs +16 -7
- package/browser/cimd/cimd.validator.mjs +6 -2
- package/browser/detection/auth-provider-detection.mjs +15 -0
- package/browser/jwks/jwks.service.mjs +9 -9
- package/browser/people/loopback-token.auth.mjs +10 -3
- package/browser/session/authorization.store.mjs +56 -2
- package/browser/session/consent.store.mjs +2 -1
- package/browser/session/credentials-accessor.impl.mjs +4 -1
- package/browser/session/incremental-auth-ticket.mjs +2 -0
- package/browser/session/redis-session.store.mjs +6 -4
- package/browser/session/secure-store-backends.mjs +2 -1
- package/browser/session/secure-store.factory.mjs +7 -1
- package/browser/session/storage/storage-authorization-vault.mjs +8 -2
- package/browser/session/storage-authorization.store.mjs +49 -3
- package/browser/session/storage-consent.store.mjs +3 -1
- package/browser/session/token-storage.factory.mjs +5 -2
- package/browser/session/transport-session.types.mjs +6 -0
- package/browser/session/utils/session-crypto.utils.mjs +14 -1
- package/browser/session/vercel-kv-session.store.mjs +6 -4
- package/cimd/cimd-redis.cache.d.ts.map +1 -1
- package/cimd/cimd.service.d.ts +1 -0
- package/cimd/cimd.service.d.ts.map +1 -1
- package/cimd/cimd.validator.d.ts +2 -0
- package/cimd/cimd.validator.d.ts.map +1 -1
- package/detection/auth-provider-detection.d.ts.map +1 -1
- package/esm/index.mjs +5 -5
- package/esm/package.json +5 -5
- package/index.js +5 -5
- package/jwks/jwks.service.d.ts.map +1 -1
- package/package.json +5 -5
- package/people/loopback-token.auth.d.ts +5 -1
- package/people/loopback-token.auth.d.ts.map +1 -1
- package/session/authorization.store.d.ts +23 -0
- package/session/authorization.store.d.ts.map +1 -1
- package/session/consent.store.d.ts +3 -1
- package/session/consent.store.d.ts.map +1 -1
- package/session/credentials-accessor.impl.d.ts.map +1 -1
- package/session/incremental-auth-ticket.d.ts +4 -0
- package/session/incremental-auth-ticket.d.ts.map +1 -1
- package/session/redis-session.store.d.ts.map +1 -1
- package/session/secure-store-backends.d.ts +3 -4
- package/session/secure-store-backends.d.ts.map +1 -1
- package/session/secure-store.factory.d.ts +2 -1
- package/session/secure-store.factory.d.ts.map +1 -1
- package/session/storage/storage-authorization-vault.d.ts.map +1 -1
- package/session/storage-authorization.store.d.ts +8 -0
- package/session/storage-authorization.store.d.ts.map +1 -1
- package/session/storage-consent.store.d.ts.map +1 -1
- package/session/token-storage.factory.d.ts +2 -1
- package/session/token-storage.factory.d.ts.map +1 -1
- package/session/transport-session.types.d.ts +5 -0
- package/session/transport-session.types.d.ts.map +1 -1
- package/session/utils/session-crypto.utils.d.ts +2 -1
- package/session/utils/session-crypto.utils.d.ts.map +1 -1
- package/session/vercel-kv-session.store.d.ts.map +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// libs/auth/src/session/authorization.store.ts
|
|
2
2
|
import { internalZ as z } from "@frontmcp/lazy-zod";
|
|
3
3
|
import { randomUUID, sha256Base64url } from "@frontmcp/utils";
|
|
4
|
+
import { AuthFlowError } from "../errors/auth-internal.errors.mjs";
|
|
4
5
|
var pkceChallengeSchema = z.object({
|
|
5
6
|
challenge: z.string().min(43).max(128),
|
|
6
7
|
method: z.literal("S256")
|
|
@@ -116,6 +117,25 @@ function buildRefreshTokenRecord(params) {
|
|
|
116
117
|
providerTokensId: params.providerTokensId
|
|
117
118
|
};
|
|
118
119
|
}
|
|
120
|
+
function isRedeemableCode(record) {
|
|
121
|
+
return !!record && !record.used && Date.now() <= record.expiresAt;
|
|
122
|
+
}
|
|
123
|
+
function isRotatableRefreshToken(record) {
|
|
124
|
+
return !!record && !record.revoked && Date.now() <= record.expiresAt;
|
|
125
|
+
}
|
|
126
|
+
function authorizationCodeRedeemedError() {
|
|
127
|
+
return new AuthFlowError(
|
|
128
|
+
"This authorization code was already redeemed by another token request, or it expired. Start a new authorization to get a new code.",
|
|
129
|
+
"AUTHORIZATION_CODE_REDEEMED"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function refreshTokenRedeemedError() {
|
|
133
|
+
return new AuthFlowError(
|
|
134
|
+
"This refresh token was already rotated by another token request, or it was revoked or expired. Sign in again to get a new refresh token.",
|
|
135
|
+
"REFRESH_TOKEN_REDEEMED"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
var COMPARE_AND_DELETE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0`;
|
|
119
139
|
function verifyPkce(codeVerifier, challenge) {
|
|
120
140
|
if (challenge.method !== "S256") {
|
|
121
141
|
return false;
|
|
@@ -151,6 +171,7 @@ var InMemoryAuthorizationStore = class {
|
|
|
151
171
|
}
|
|
152
172
|
async markCodeUsed(code, issuedRefreshToken) {
|
|
153
173
|
const record = this.codes.get(code);
|
|
174
|
+
if (issuedRefreshToken === void 0 && !isRedeemableCode(record)) throw authorizationCodeRedeemedError();
|
|
154
175
|
if (record) {
|
|
155
176
|
record.used = true;
|
|
156
177
|
if (issuedRefreshToken) record.issuedRefreshToken = issuedRefreshToken;
|
|
@@ -192,7 +213,9 @@ var InMemoryAuthorizationStore = class {
|
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
async rotateRefreshToken(oldToken, newRecord) {
|
|
195
|
-
|
|
216
|
+
const record = this.refreshTokens.get(oldToken);
|
|
217
|
+
if (!isRotatableRefreshToken(record)) throw refreshTokenRedeemedError();
|
|
218
|
+
record.revoked = true;
|
|
196
219
|
newRecord.previousToken = oldToken;
|
|
197
220
|
await this.storeRefreshToken(newRecord);
|
|
198
221
|
}
|
|
@@ -268,6 +291,14 @@ var RedisAuthorizationStore = class {
|
|
|
268
291
|
return JSON.parse(data);
|
|
269
292
|
}
|
|
270
293
|
async markCodeUsed(code, issuedRefreshToken) {
|
|
294
|
+
if (issuedRefreshToken === void 0) {
|
|
295
|
+
const claimed = await this.claim("code", code, isRedeemableCode, (record2) => ({
|
|
296
|
+
...record2,
|
|
297
|
+
used: true
|
|
298
|
+
}));
|
|
299
|
+
if (!claimed) throw authorizationCodeRedeemedError();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
271
302
|
const record = await this.getAuthorizationCode(code);
|
|
272
303
|
if (record) {
|
|
273
304
|
record.used = true;
|
|
@@ -311,12 +342,31 @@ var RedisAuthorizationStore = class {
|
|
|
311
342
|
}
|
|
312
343
|
}
|
|
313
344
|
async rotateRefreshToken(oldToken, newRecord) {
|
|
314
|
-
await this.
|
|
345
|
+
const claimed = await this.claim("refresh", oldToken, isRotatableRefreshToken, (record) => ({
|
|
346
|
+
...record,
|
|
347
|
+
revoked: true
|
|
348
|
+
}));
|
|
349
|
+
if (!claimed) throw refreshTokenRedeemedError();
|
|
315
350
|
newRecord.previousToken = oldToken;
|
|
316
351
|
await this.storeRefreshToken(newRecord);
|
|
317
352
|
}
|
|
318
353
|
async cleanup() {
|
|
319
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Replace the record under `type:id` with `update(record)` while `isLive(record)`. The compare-and-delete lets
|
|
357
|
+
* one request win: false when the record is missing or not live, or another request changed it first.
|
|
358
|
+
*/
|
|
359
|
+
async claim(type, id, isLive, update) {
|
|
360
|
+
const key = this.key(type, id);
|
|
361
|
+
const raw = await this.redis.get(key);
|
|
362
|
+
if (!raw) return false;
|
|
363
|
+
const record = JSON.parse(raw);
|
|
364
|
+
if (!isLive(record)) return false;
|
|
365
|
+
if (await this.redis.eval(COMPARE_AND_DELETE_SCRIPT, 1, key, raw) !== 1) return false;
|
|
366
|
+
const ttl = Math.ceil((record.expiresAt - Date.now()) / 1e3);
|
|
367
|
+
await this.redis.set(key, JSON.stringify(update(record)), "EX", Math.max(ttl, 1));
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
320
370
|
};
|
|
321
371
|
export {
|
|
322
372
|
AUTH_CODE_TTL_MS,
|
|
@@ -325,12 +375,16 @@ export {
|
|
|
325
375
|
REFRESH_TOKEN_TTL_MS,
|
|
326
376
|
RedisAuthorizationStore,
|
|
327
377
|
authorizationCodeRecordSchema,
|
|
378
|
+
authorizationCodeRedeemedError,
|
|
328
379
|
buildCodeRecord,
|
|
329
380
|
buildPendingRecord,
|
|
330
381
|
buildRefreshTokenRecord,
|
|
331
382
|
generateAuthorizationCode,
|
|
332
383
|
generatePkceChallenge,
|
|
333
384
|
generateRefreshTokenValue,
|
|
385
|
+
isRedeemableCode,
|
|
386
|
+
isRotatableRefreshToken,
|
|
334
387
|
pkceChallengeSchema,
|
|
388
|
+
refreshTokenRedeemedError,
|
|
335
389
|
verifyPkce
|
|
336
390
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// libs/auth/src/session/consent.store.ts
|
|
2
2
|
function consentRecordKey(userSub, clientId) {
|
|
3
|
-
|
|
3
|
+
const escapedSub = userSub.replace(/%/g, "%25").replace(/:/g, "%3A");
|
|
4
|
+
return `consent:${escapedSub}:${clientId}`;
|
|
4
5
|
}
|
|
5
6
|
var InMemoryConsentStore = class {
|
|
6
7
|
records = /* @__PURE__ */ new Map();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// libs/auth/src/session/credentials-accessor.impl.ts
|
|
2
|
+
import { escapeControlCharacters } from "@frontmcp/utils";
|
|
2
3
|
import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
|
|
3
4
|
import { buildCredentialResumeUrl, signCredentialResumeToken } from "./credential-resume-link.mjs";
|
|
4
5
|
var CredentialsAccessorImpl = class {
|
|
@@ -44,7 +45,9 @@ var CredentialsAccessorImpl = class {
|
|
|
44
45
|
}
|
|
45
46
|
const token = signCredentialResumeToken({ sub, key, context, ttlMs: this.resumeTtlMs }, this.signingSecret);
|
|
46
47
|
const resumeUrl = buildCredentialResumeUrl(this.basePath, token);
|
|
47
|
-
this.logger.debug(
|
|
48
|
+
this.logger.debug(
|
|
49
|
+
`CredentialsAccessor: credential "${escapeControlCharacters(key)}" not connected; issued resume link`
|
|
50
|
+
);
|
|
48
51
|
return {
|
|
49
52
|
connected: false,
|
|
50
53
|
key,
|
|
@@ -23,6 +23,7 @@ function signIncrementalAuthTicket(input, secret) {
|
|
|
23
23
|
name: input.name
|
|
24
24
|
})
|
|
25
25
|
} : {},
|
|
26
|
+
...input.audience !== void 0 ? { aud: input.audience } : {},
|
|
26
27
|
jti,
|
|
27
28
|
exp: Date.now() + (input.ttlMs ?? DEFAULT_INCREMENTAL_TICKET_TTL_MS)
|
|
28
29
|
};
|
|
@@ -44,6 +45,7 @@ function verifyIncrementalAuthTicket(token, secret, now = Date.now()) {
|
|
|
44
45
|
if (!Number.isFinite(payload.exp)) return null;
|
|
45
46
|
if (payload.toolId !== void 0 && typeof payload.toolId !== "string") return null;
|
|
46
47
|
if (payload.profile !== void 0 && typeof payload.profile !== "string") return null;
|
|
48
|
+
if (payload.aud !== void 0 && typeof payload.aud !== "string") return null;
|
|
47
49
|
if (payload.priorAppIds !== void 0 && (!Array.isArray(payload.priorAppIds) || payload.priorAppIds.some((id) => typeof id !== "string"))) {
|
|
48
50
|
return null;
|
|
49
51
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// libs/auth/src/session/redis-session.store.ts
|
|
2
2
|
import { randomUUID, RedisStorageAdapter } from "@frontmcp/utils";
|
|
3
3
|
import { SessionIdEmptyError } from "../errors/auth-internal.errors.mjs";
|
|
4
|
-
import { signSession,
|
|
4
|
+
import { signSession, verifySession } from "./session-crypto.mjs";
|
|
5
5
|
import { SessionRateLimiter } from "./session-rate-limiter.mjs";
|
|
6
6
|
import {
|
|
7
|
+
sessionMaxLifetimeAt,
|
|
7
8
|
storedSessionSchema
|
|
8
9
|
} from "./transport-session.types.mjs";
|
|
9
10
|
var RedisSessionStore = class {
|
|
@@ -110,7 +111,7 @@ var RedisSessionStore = class {
|
|
|
110
111
|
try {
|
|
111
112
|
let parsed;
|
|
112
113
|
if (this.security.enableSigning) {
|
|
113
|
-
parsed =
|
|
114
|
+
parsed = verifySession(raw, { secret: this.security.signingSecret });
|
|
114
115
|
if (!parsed) {
|
|
115
116
|
this.logger?.warn("[RedisSessionStore] Session signature verification failed", {
|
|
116
117
|
sessionId: sessionId.slice(0, 20)
|
|
@@ -131,10 +132,11 @@ var RedisSessionStore = class {
|
|
|
131
132
|
return null;
|
|
132
133
|
}
|
|
133
134
|
const session = result.data;
|
|
134
|
-
|
|
135
|
+
const maxLifetimeAt = sessionMaxLifetimeAt(session, this.security.maxLifetimeMs);
|
|
136
|
+
if (maxLifetimeAt && maxLifetimeAt < Date.now()) {
|
|
135
137
|
this.logger?.info("[RedisSessionStore] Session exceeded max lifetime", {
|
|
136
138
|
sessionId: sessionId.slice(0, 20),
|
|
137
|
-
maxLifetimeAt
|
|
139
|
+
maxLifetimeAt
|
|
138
140
|
});
|
|
139
141
|
await this.delete(sessionId);
|
|
140
142
|
return null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// libs/auth/src/session/secure-store-backends.ts
|
|
2
|
+
import { base64urlEncode, randomBytes } from "@frontmcp/utils";
|
|
2
3
|
import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
|
|
3
4
|
import { VaultEncryption } from "./vault-encryption.mjs";
|
|
4
5
|
var DEFAULT_KEY_PREFIX = "mcp:secret:";
|
|
@@ -14,7 +15,7 @@ var EncryptedStorageSecureStoreBackend = class {
|
|
|
14
15
|
this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
15
16
|
this.logger = options.logger ?? noopAuthLogger;
|
|
16
17
|
this.encryption = new VaultEncryption({
|
|
17
|
-
pepper: options.pepper,
|
|
18
|
+
pepper: options.pepper?.trim() ? options.pepper : base64urlEncode(randomBytes(32)),
|
|
18
19
|
hkdfInfo: "frontmcp-secure-store-v1"
|
|
19
20
|
});
|
|
20
21
|
}
|
|
@@ -56,6 +56,12 @@ async function createSecureStore(options) {
|
|
|
56
56
|
return { backend: config.backend, scope, ttlMs, kind: "custom" };
|
|
57
57
|
}
|
|
58
58
|
const kind = isSqliteConfig(config) ? "sqlite" : isRedisConfig(config) ? "redis" : "memory";
|
|
59
|
+
const effectivePepper = readPepper(config) ?? pepper;
|
|
60
|
+
if (kind !== "memory" && !effectivePepper?.trim()) {
|
|
61
|
+
throw new SecureStoreConfigError(
|
|
62
|
+
`the ${kind} backing encrypts the secrets with a pepper, and none was given. Set VAULT_SECRET or JWT_SECRET (or pass pepper to createSecureStore()), or set secureStore.encryption.pepper.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
59
65
|
let adapter;
|
|
60
66
|
if (storage) {
|
|
61
67
|
adapter = storage;
|
|
@@ -67,7 +73,7 @@ async function createSecureStore(options) {
|
|
|
67
73
|
}
|
|
68
74
|
const backend = new EncryptedStorageSecureStoreBackend({
|
|
69
75
|
storage: adapter,
|
|
70
|
-
pepper:
|
|
76
|
+
pepper: effectivePepper,
|
|
71
77
|
logger
|
|
72
78
|
});
|
|
73
79
|
return { backend, scope, ttlMs, kind, ...storage ? {} : { close: () => adapter.disconnect() } };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// libs/auth/src/session/storage/storage-authorization-vault.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
escapeControlCharacters,
|
|
4
|
+
randomUUID,
|
|
5
|
+
TypedStorage
|
|
6
|
+
} from "@frontmcp/utils";
|
|
3
7
|
import {
|
|
4
8
|
authorizationVaultEntrySchema
|
|
5
9
|
} from "../authorization-vault.mjs";
|
|
@@ -45,7 +49,9 @@ var StorageAuthorizationVault = class {
|
|
|
45
49
|
async update(id, updates) {
|
|
46
50
|
const entry = await this.get(id);
|
|
47
51
|
if (!entry) {
|
|
48
|
-
console.warn(
|
|
52
|
+
console.warn(
|
|
53
|
+
`[StorageAuthorizationVault] Update failed: vault entry not found for id ${escapeControlCharacters(id)}`
|
|
54
|
+
);
|
|
49
55
|
return;
|
|
50
56
|
}
|
|
51
57
|
const updated = {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// libs/auth/src/session/storage-authorization.store.ts
|
|
2
|
-
import { TypedStorage } from "@frontmcp/utils";
|
|
2
|
+
import { StorageNotSupportedError, TypedStorage } from "@frontmcp/utils";
|
|
3
3
|
import {
|
|
4
|
+
authorizationCodeRedeemedError,
|
|
4
5
|
buildCodeRecord,
|
|
5
6
|
buildPendingRecord,
|
|
6
7
|
buildRefreshTokenRecord,
|
|
7
8
|
generateAuthorizationCode,
|
|
8
|
-
generateRefreshTokenValue
|
|
9
|
+
generateRefreshTokenValue,
|
|
10
|
+
isRedeemableCode,
|
|
11
|
+
isRotatableRefreshToken,
|
|
12
|
+
refreshTokenRedeemedError
|
|
9
13
|
} from "./authorization.store.mjs";
|
|
10
14
|
var StorageAuthorizationStore = class {
|
|
11
15
|
codeStore;
|
|
@@ -13,10 +17,12 @@ var StorageAuthorizationStore = class {
|
|
|
13
17
|
refreshStore;
|
|
14
18
|
namespace;
|
|
15
19
|
storageIsNamespaced;
|
|
20
|
+
base;
|
|
16
21
|
constructor(storage, options = {}) {
|
|
17
22
|
this.namespace = options.namespace ?? "oauth";
|
|
18
23
|
this.storageIsNamespaced = this.isNamespacedStorage(storage);
|
|
19
24
|
const base = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
|
|
25
|
+
this.base = base;
|
|
20
26
|
this.codeStore = new TypedStorage(base);
|
|
21
27
|
this.pendingStore = new TypedStorage(base);
|
|
22
28
|
this.refreshStore = new TypedStorage(base);
|
|
@@ -55,6 +61,14 @@ var StorageAuthorizationStore = class {
|
|
|
55
61
|
return record;
|
|
56
62
|
}
|
|
57
63
|
async markCodeUsed(code, issuedRefreshToken) {
|
|
64
|
+
if (issuedRefreshToken === void 0) {
|
|
65
|
+
const claimed = await this.claim(this.codeStore, this.key("code", code), isRedeemableCode, (record2) => ({
|
|
66
|
+
...record2,
|
|
67
|
+
used: true
|
|
68
|
+
}));
|
|
69
|
+
if (!claimed) throw authorizationCodeRedeemedError();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
58
72
|
const record = await this.getAuthorizationCode(code);
|
|
59
73
|
if (record) {
|
|
60
74
|
record.used = true;
|
|
@@ -105,7 +119,16 @@ var StorageAuthorizationStore = class {
|
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
async rotateRefreshToken(oldToken, newRecord) {
|
|
108
|
-
await this.
|
|
122
|
+
const claimed = await this.claim(
|
|
123
|
+
this.refreshStore,
|
|
124
|
+
this.key("refresh", oldToken),
|
|
125
|
+
isRotatableRefreshToken,
|
|
126
|
+
(record) => ({
|
|
127
|
+
...record,
|
|
128
|
+
revoked: true
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
if (!claimed) throw refreshTokenRedeemedError();
|
|
109
132
|
newRecord.previousToken = oldToken;
|
|
110
133
|
await this.storeRefreshToken(newRecord);
|
|
111
134
|
}
|
|
@@ -117,6 +140,29 @@ var StorageAuthorizationStore = class {
|
|
|
117
140
|
// ============================================
|
|
118
141
|
// Internals
|
|
119
142
|
// ============================================
|
|
143
|
+
/**
|
|
144
|
+
* Replace the record under `key` with `update(record)` while `isLive(record)`. The compare-and-delete lets one
|
|
145
|
+
* request win: false when the record is missing or not live, or another request changed it first.
|
|
146
|
+
*/
|
|
147
|
+
async claim(store, key, isLive, update) {
|
|
148
|
+
const raw = await this.base.get(key);
|
|
149
|
+
if (raw === null) return false;
|
|
150
|
+
const record = JSON.parse(raw);
|
|
151
|
+
if (!isLive(record) || !await this.deleteIfEquals(key, raw)) return false;
|
|
152
|
+
await store.set(key, update(record), this.ttl(record.expiresAt));
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
/** Delete `key` only while it holds `expectedValue`, on an adapter that can do that in one step. */
|
|
156
|
+
deleteIfEquals(key, expectedValue) {
|
|
157
|
+
if (!this.base.deleteIfEquals) {
|
|
158
|
+
throw new StorageNotSupportedError(
|
|
159
|
+
"deleteIfEquals",
|
|
160
|
+
"this storage adapter",
|
|
161
|
+
"Use the memory, Redis, Upstash, Vercel KV or SQLite adapter, so an authorization code or refresh token is redeemed once."
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return this.base.deleteIfEquals(key, expectedValue);
|
|
165
|
+
}
|
|
120
166
|
ttl(expiresAtMs) {
|
|
121
167
|
const ttl = Math.ceil((expiresAtMs - Date.now()) / 1e3);
|
|
122
168
|
return { ttlSeconds: ttl > 0 ? ttl : 1 };
|
|
@@ -12,7 +12,9 @@ var StorageConsentStore = class {
|
|
|
12
12
|
this.storage = new TypedStorage(namespacedStorage);
|
|
13
13
|
}
|
|
14
14
|
async get(userSub, clientId) {
|
|
15
|
-
|
|
15
|
+
const record = await this.storage.get(this.key(userSub, clientId));
|
|
16
|
+
if (!record || record.userSub !== userSub || record.clientId !== clientId) return null;
|
|
17
|
+
return record;
|
|
16
18
|
}
|
|
17
19
|
async set(record) {
|
|
18
20
|
await this.storage.set(this.key(record.userSub, record.clientId), record);
|
|
@@ -9,7 +9,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
// libs/auth/src/session/token-storage.factory.ts
|
|
12
|
-
import { MemoryStorageAdapter, RedisStorageAdapter } from "@frontmcp/utils";
|
|
12
|
+
import { MemoryStorageAdapter, RedisStorageAdapter, requireOptionalModule } from "@frontmcp/utils";
|
|
13
13
|
function isRedisTokenStorage(config) {
|
|
14
14
|
return typeof config === "object" && config !== null && "redis" in config && !!config.redis;
|
|
15
15
|
}
|
|
@@ -26,7 +26,10 @@ async function createTokenStorageAdapter(config) {
|
|
|
26
26
|
return adapter;
|
|
27
27
|
}
|
|
28
28
|
if (isSqliteTokenStorage(config)) {
|
|
29
|
-
const { SqliteStorageAdapter } =
|
|
29
|
+
const { SqliteStorageAdapter } = requireOptionalModule(
|
|
30
|
+
"@frontmcp/storage-sqlite",
|
|
31
|
+
() => __require("@frontmcp/storage-sqlite")
|
|
32
|
+
);
|
|
30
33
|
const adapter = new SqliteStorageAdapter({
|
|
31
34
|
path: config.sqlite.path,
|
|
32
35
|
encryption: config.sqlite.encryption,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// libs/auth/src/session/transport-session.types.ts
|
|
2
2
|
import { internalZ as z } from "@frontmcp/lazy-zod";
|
|
3
|
+
function sessionMaxLifetimeAt(session, maxLifetimeMs) {
|
|
4
|
+
const configured = maxLifetimeMs && maxLifetimeMs > 0 ? session.createdAt + maxLifetimeMs : void 0;
|
|
5
|
+
const deadlines = [session.maxLifetimeAt, configured].filter((deadline) => !!deadline);
|
|
6
|
+
return deadlines.length > 0 ? Math.min(...deadlines) : void 0;
|
|
7
|
+
}
|
|
3
8
|
var transportProtocolSchema = z.enum([
|
|
4
9
|
"legacy-sse",
|
|
5
10
|
"sse",
|
|
@@ -97,6 +102,7 @@ export {
|
|
|
97
102
|
legacySseTransportStateSchema,
|
|
98
103
|
redisConfigSchema,
|
|
99
104
|
sessionJwtPayloadSchema,
|
|
105
|
+
sessionMaxLifetimeAt,
|
|
100
106
|
sseTransportStateSchema,
|
|
101
107
|
statefulHttpTransportStateSchema,
|
|
102
108
|
statelessHttpTransportStateSchema,
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// libs/auth/src/session/utils/session-crypto.utils.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
decryptValue,
|
|
4
|
+
encryptValue,
|
|
5
|
+
getEnv,
|
|
6
|
+
getMachineId,
|
|
7
|
+
getRuntimeContext,
|
|
8
|
+
isProduction,
|
|
9
|
+
sha256
|
|
10
|
+
} from "@frontmcp/utils";
|
|
3
11
|
import { SessionSecretRequiredError } from "../../errors/auth-internal.errors.mjs";
|
|
4
12
|
var cachedKey = null;
|
|
5
13
|
function getKey() {
|
|
@@ -9,6 +17,11 @@ function getKey() {
|
|
|
9
17
|
if (isProduction()) {
|
|
10
18
|
throw new SessionSecretRequiredError("session ID encryption");
|
|
11
19
|
}
|
|
20
|
+
if (getRuntimeContext().deployment === "distributed") {
|
|
21
|
+
throw new SessionSecretRequiredError(
|
|
22
|
+
"session ID encryption in a distributed deployment, whose responses carry the machine id: set MCP_SESSION_SECRET (openssl rand -hex 32)"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
12
25
|
console.warn(
|
|
13
26
|
"[SessionIdUtils] Using machine ID as session encryption secret - NOT SECURE FOR PRODUCTION. Set MCP_SESSION_SECRET environment variable for secure session encryption."
|
|
14
27
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// libs/auth/src/session/vercel-kv-session.store.ts
|
|
2
2
|
import { randomUUID, VercelKvStorageAdapter } from "@frontmcp/utils";
|
|
3
3
|
import { SessionIdEmptyError } from "../errors/auth-internal.errors.mjs";
|
|
4
|
-
import { signSession,
|
|
4
|
+
import { signSession, verifySession } from "./session-crypto.mjs";
|
|
5
5
|
import { SessionRateLimiter } from "./session-rate-limiter.mjs";
|
|
6
6
|
import {
|
|
7
|
+
sessionMaxLifetimeAt,
|
|
7
8
|
storedSessionSchema
|
|
8
9
|
} from "./transport-session.types.mjs";
|
|
9
10
|
var VercelKvSessionStore = class {
|
|
@@ -92,7 +93,7 @@ var VercelKvSessionStore = class {
|
|
|
92
93
|
try {
|
|
93
94
|
let parsed;
|
|
94
95
|
if (this.security.enableSigning) {
|
|
95
|
-
parsed =
|
|
96
|
+
parsed = verifySession(raw, { secret: this.security.signingSecret });
|
|
96
97
|
if (!parsed) {
|
|
97
98
|
this.logger?.warn("[VercelKvSessionStore] Session signature verification failed", {
|
|
98
99
|
sessionId: sessionId.slice(0, 20)
|
|
@@ -113,10 +114,11 @@ var VercelKvSessionStore = class {
|
|
|
113
114
|
return null;
|
|
114
115
|
}
|
|
115
116
|
const session = result.data;
|
|
116
|
-
|
|
117
|
+
const maxLifetimeAt = sessionMaxLifetimeAt(session, this.security.maxLifetimeMs);
|
|
118
|
+
if (maxLifetimeAt && maxLifetimeAt < Date.now()) {
|
|
117
119
|
this.logger?.info("[VercelKvSessionStore] Session exceeded max lifetime", {
|
|
118
120
|
sessionId: sessionId.slice(0, 20),
|
|
119
|
-
maxLifetimeAt
|
|
121
|
+
maxLifetimeAt
|
|
120
122
|
});
|
|
121
123
|
await this.delete(sessionId);
|
|
122
124
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cimd-redis.cache.d.ts","sourceRoot":"","sources":["../../src/cimd/cimd-redis.cache.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAa5E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,cAAe,YAAW,gBAAgB;IACrD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;gBAElC,MAAM,EAAE,eAAe;
|
|
1
|
+
{"version":3,"file":"cimd-redis.cache.d.ts","sourceRoot":"","sources":["../../src/cimd/cimd-redis.cache.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAa5E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,cAAe,YAAW,gBAAgB;IACrD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;gBAElC,MAAM,EAAE,eAAe;IAiCnC;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAKhB;;;;;OAKG;IACG,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IA0BhE;;;;;OAKG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAerE;;;;;;OAMG;IACG,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB9F;;;;;OAKG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IA4BtE;;;;;OAKG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKhD;;;;;OAKG;IACG,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IA0B1F;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAK7B;;;;;;;;OAQG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;IA2BhC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
package/cimd/cimd.service.d.ts
CHANGED
|
@@ -122,6 +122,7 @@ export declare class CimdService {
|
|
|
122
122
|
*
|
|
123
123
|
* @param clientId - The URL from which the document was fetched
|
|
124
124
|
* @param document - The document to validate
|
|
125
|
+
* @returns The parsed document, with the schema's defaults for the fields it leaves out
|
|
125
126
|
*/
|
|
126
127
|
private validateDocument;
|
|
127
128
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cimd.service.d.ts","sourceRoot":"","sources":["../../src/cimd/cimd.service.ts"],"names":[],"mappings":"AAWA,OAAO,EAAyB,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAY3F,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EASL,KAAK,eAAe,EAEpB,KAAK,oBAAoB,EAEzB,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"cimd.service.d.ts","sourceRoot":"","sources":["../../src/cimd/cimd.service.ts"],"names":[],"mappings":"AAWA,OAAO,EAAyB,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAY3F,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EASL,KAAK,eAAe,EAEpB,KAAK,oBAAoB,EAEzB,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAyBtB;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkB;IAC9C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,YAAY,CAAC,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IAEpC;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;;OAKG;gBACS,MAAM,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC;IAyBlE;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;OAGG;IACH,OAAO,CAAC,KAAK;IAUb,2FAA2F;IACrF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B;;;;;OAKG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIzC;;;;;;;;OAQG;IACG,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAqD5E;;;;;;OAMG;IACH,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG,IAAI;IAUhF;;;;;;;;OAQG;IACH,qBAAqB,CAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI;IAmB7D;;;;;;;OAOG;IACG,qBAAqB,CAAC,QAAQ,EAAE,sBAAsB,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQzG;;;;;OAKG;YACW,UAAU;IA2BxB,mHAAmH;IACnH,OAAO,CAAC,iBAAiB;IAezB;;;;OAIG;IACG,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAOhD;;OAEG;YACW,qBAAqB;IAenC;;;;;OAKG;YACW,SAAS;IAsJvB;;OAEG;YACW,qBAAqB;IAyCnC;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;CAmBzB"}
|
package/cimd/cimd.validator.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type { CimdSecurityConfig } from './cimd.types';
|
|
|
11
11
|
* @returns true if this is a CIMD client_id
|
|
12
12
|
*/
|
|
13
13
|
export declare function isCimdClientId(clientId: string, allowInsecure?: boolean): boolean;
|
|
14
|
+
/** Whether private addresses are refused for this host: with blockPrivateIPs, for every host but a localhost one under allowInsecureForTesting. */
|
|
15
|
+
export declare function blocksPrivateAddressesFor(hostname: string, securityConfig?: Partial<CimdSecurityConfig>): boolean;
|
|
14
16
|
/**
|
|
15
17
|
* Validate a client_id URL for CIMD usage.
|
|
16
18
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cimd.validator.d.ts","sourceRoot":"","sources":["../../src/cimd/cimd.validator.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,UAAQ,GAAG,OAAO,CA4B/E;AAUD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,GAAG,
|
|
1
|
+
{"version":3,"file":"cimd.validator.d.ts","sourceRoot":"","sources":["../../src/cimd/cimd.validator.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,UAAQ,GAAG,OAAO,CA4B/E;AAUD,mJAAmJ;AACnJ,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAGjH;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,GAAG,CA4DvG;AAED;;GAEG;AACH,UAAU,eAAe;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAqBrE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAgDxF;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzF;AAyGD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAgB5E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-provider-detection.d.ts","sourceRoot":"","sources":["../../src/detection/auth-provider-detection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,SAAS,IAAI,CAAC,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAcrD,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;gCAOrC,CAAC;AAEH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;gCAQ5C,CAAC;AAMH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAE5F,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAiB7D;AAyBD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,WAAW,GAAG,SAAS,EACnC,IAAI,EAAE,WAAW,EAAE,GAClB,2BAA2B,
|
|
1
|
+
{"version":3,"file":"auth-provider-detection.d.ts","sourceRoot":"","sources":["../../src/detection/auth-provider-detection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,SAAS,IAAI,CAAC,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAcrD,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;gCAOrC,CAAC;AAEH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;gCAQ5C,CAAC;AAMH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAE5F,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAiB7D;AAyBD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,WAAW,GAAG,SAAS,EACnC,IAAI,EAAE,WAAW,EAAE,GAClB,2BAA2B,CAyG7B;AAyCD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,UAAU,EAAE,WAAW,GAAG,SAAS,GAClC,OAAO,CAaT;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,2BAA2B,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAGtG;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,2BAA2B,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAGpG"}
|