@frontmcp/auth 1.10.16 → 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,243 @@
|
|
|
1
|
+
// libs/auth/src/session/session-credential-vault.ts
|
|
2
|
+
import { getEnv, randomUUID } from "@frontmcp/utils";
|
|
3
|
+
import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
|
|
4
|
+
import { VaultEncryption } from "./vault-encryption.mjs";
|
|
5
|
+
var DEFAULT_KEY_PREFIX = "mcp:cred:";
|
|
6
|
+
var DEFAULT_PENDING_TTL_MS = 6e5;
|
|
7
|
+
var SessionCredentialVault = class {
|
|
8
|
+
storage;
|
|
9
|
+
encryption;
|
|
10
|
+
keyPrefix;
|
|
11
|
+
ttlMs;
|
|
12
|
+
pendingTtlMs;
|
|
13
|
+
logger;
|
|
14
|
+
/** Cache of derived AES keys, keyed by `${sub}:${vaultId}`. */
|
|
15
|
+
keyCache = /* @__PURE__ */ new Map();
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.storage = options.storage;
|
|
18
|
+
this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
19
|
+
this.ttlMs = options.ttlMs;
|
|
20
|
+
this.pendingTtlMs = options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
21
|
+
this.logger = options.logger ?? noopAuthLogger;
|
|
22
|
+
const pepper = options.pepper ?? resolveVaultPepper(this.logger);
|
|
23
|
+
this.encryption = new VaultEncryption({ pepper, hkdfInfo: "frontmcp-session-credential-vault-v1" });
|
|
24
|
+
}
|
|
25
|
+
// ============================================
|
|
26
|
+
// Storage-key helpers
|
|
27
|
+
// ============================================
|
|
28
|
+
/** `cred:idx:<sub>` \u2192 current vaultId (the sub \u2192 vaultId indirection). */
|
|
29
|
+
indexKey(sub) {
|
|
30
|
+
return `${this.keyPrefix}idx:${sub}`;
|
|
31
|
+
}
|
|
32
|
+
/** `cred:keys:<sub>:<vaultId>` \u2192 JSON array of credential key names (the SET). */
|
|
33
|
+
keysKey(sub, vaultId) {
|
|
34
|
+
return `${this.keyPrefix}keys:${sub}:${vaultId}`;
|
|
35
|
+
}
|
|
36
|
+
/** `cred:data:<sub>:<vaultId>:<key>` \u2192 encrypted credential record. */
|
|
37
|
+
dataKey(sub, vaultId, key) {
|
|
38
|
+
return `${this.keyPrefix}data:${sub}:${vaultId}:${key}`;
|
|
39
|
+
}
|
|
40
|
+
/** `cred:pending:<pendingId>` \u2192 encrypted pending-authorize accumulator. */
|
|
41
|
+
pendingKey(pendingId) {
|
|
42
|
+
return `${this.keyPrefix}pending:${pendingId}`;
|
|
43
|
+
}
|
|
44
|
+
/** Convert the configured `ttlMs` into `SetOptions` (storage uses `ttlSeconds`). */
|
|
45
|
+
get credentialSetOpts() {
|
|
46
|
+
return this.ttlMs ? { ttlSeconds: Math.ceil(this.ttlMs / 1e3) } : void 0;
|
|
47
|
+
}
|
|
48
|
+
// ============================================
|
|
49
|
+
// Key derivation
|
|
50
|
+
// ============================================
|
|
51
|
+
/**
|
|
52
|
+
* Derive (and cache) the AES-256 key for a `(sub, vaultId)` pair. The vaultId
|
|
53
|
+
* acts as the HKDF salt, so rotating it makes prior ciphertext undecryptable.
|
|
54
|
+
*/
|
|
55
|
+
deriveKey(sub, vaultId) {
|
|
56
|
+
const cacheKey = `${sub}:${vaultId}`;
|
|
57
|
+
const cached = this.keyCache.get(cacheKey);
|
|
58
|
+
if (cached) return cached;
|
|
59
|
+
const promise = this.encryption.deriveKey({ jti: vaultId, vaultKey: vaultId, sub, iat: 0 });
|
|
60
|
+
this.keyCache.set(cacheKey, promise);
|
|
61
|
+
return promise;
|
|
62
|
+
}
|
|
63
|
+
// ============================================
|
|
64
|
+
// vaultId indirection
|
|
65
|
+
// ============================================
|
|
66
|
+
/**
|
|
67
|
+
* Read the current vaultId for a subject, or undefined when the subject has no
|
|
68
|
+
* live vault (never authorized, or rotated away).
|
|
69
|
+
*/
|
|
70
|
+
async getVaultId(sub) {
|
|
71
|
+
const raw = await this.storage.get(this.indexKey(sub));
|
|
72
|
+
return raw ?? void 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Mint a FRESH vaultId for a subject and point the indirection at it. Called
|
|
76
|
+
* at the start of each authorize so reconnect = empty vault. Returns the new
|
|
77
|
+
* vaultId. The old vaultId's ciphertext is left in storage but is
|
|
78
|
+
* unreachable (no index pointer) and undecryptable (different derived key);
|
|
79
|
+
* it expires via TTL when configured.
|
|
80
|
+
*/
|
|
81
|
+
async rotateVault(sub) {
|
|
82
|
+
const vaultId = randomUUID();
|
|
83
|
+
await this.storage.set(this.indexKey(sub), vaultId, this.credentialSetOpts);
|
|
84
|
+
return vaultId;
|
|
85
|
+
}
|
|
86
|
+
// ============================================
|
|
87
|
+
// Credential CRUD (committed vault)
|
|
88
|
+
// ============================================
|
|
89
|
+
/**
|
|
90
|
+
* Encrypt + store a single credential into the vault identified by
|
|
91
|
+
* `(sub, vaultId)` and add its key to the key set. Used both to commit
|
|
92
|
+
* authorize-time credentials and to additively add a credential mid-session.
|
|
93
|
+
*/
|
|
94
|
+
async store(sub, vaultId, key, value) {
|
|
95
|
+
const aesKey = await this.deriveKey(sub, vaultId);
|
|
96
|
+
const enc = await this.encryption.encryptObject(this.normalize(value), aesKey);
|
|
97
|
+
const record = { enc };
|
|
98
|
+
await this.storage.set(this.dataKey(sub, vaultId, key), JSON.stringify(record), this.credentialSetOpts);
|
|
99
|
+
await this.addKeyToSet(sub, vaultId, key);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Decrypt and return a credential by key for a subject's CURRENT vault.
|
|
103
|
+
* Returns undefined when there is no live vault or no such key, or when
|
|
104
|
+
* decryption fails (e.g. a stale pointer to rotated-away ciphertext).
|
|
105
|
+
*/
|
|
106
|
+
async get(sub, key) {
|
|
107
|
+
const vaultId = await this.getVaultId(sub);
|
|
108
|
+
if (!vaultId) return void 0;
|
|
109
|
+
const raw = await this.storage.get(this.dataKey(sub, vaultId, key));
|
|
110
|
+
if (!raw) return void 0;
|
|
111
|
+
try {
|
|
112
|
+
const record = JSON.parse(raw);
|
|
113
|
+
const aesKey = await this.deriveKey(sub, vaultId);
|
|
114
|
+
return await this.encryption.decryptObject(record.enc, aesKey);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
this.logger.warn(`SessionCredentialVault: failed to read credential "${key}": ${errMsg(err)}`);
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* List the credential keys available in a subject's current vault. Returns an
|
|
122
|
+
* empty array when there is no live vault (e.g. after rotation on reconnect).
|
|
123
|
+
*/
|
|
124
|
+
async list(sub) {
|
|
125
|
+
const vaultId = await this.getVaultId(sub);
|
|
126
|
+
if (!vaultId) return [];
|
|
127
|
+
return this.readKeySet(sub, vaultId);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Remove a credential by key from a subject's current vault (deletes the
|
|
131
|
+
* ciphertext and drops the key from the set). No-op when absent.
|
|
132
|
+
*/
|
|
133
|
+
async remove(sub, key) {
|
|
134
|
+
const vaultId = await this.getVaultId(sub);
|
|
135
|
+
if (!vaultId) return;
|
|
136
|
+
await this.storage.delete(this.dataKey(sub, vaultId, key));
|
|
137
|
+
const keys = await this.readKeySet(sub, vaultId);
|
|
138
|
+
const next = keys.filter((k) => k !== key);
|
|
139
|
+
if (next.length !== keys.length) {
|
|
140
|
+
await this.writeKeySet(sub, vaultId, next);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ============================================
|
|
144
|
+
// Authorize-time accumulation + commit
|
|
145
|
+
// ============================================
|
|
146
|
+
/**
|
|
147
|
+
* Stage credentials returned by `authenticate()` under a pending id, encrypted
|
|
148
|
+
* against a freshly-rotated vaultId for `sub`. They are NOT visible to tools
|
|
149
|
+
* until {@link commit} flushes them \u2014 this lets the authorize flow accumulate
|
|
150
|
+
* credentials before the subject's identity is final.
|
|
151
|
+
*
|
|
152
|
+
* @returns the vaultId the credentials were staged against.
|
|
153
|
+
*/
|
|
154
|
+
async stage(pendingId, sub, credentials) {
|
|
155
|
+
const vaultId = await this.rotateVault(sub);
|
|
156
|
+
const creds = {};
|
|
157
|
+
for (const cred of credentials) {
|
|
158
|
+
const aesKey = await this.deriveKey(sub, vaultId);
|
|
159
|
+
const { key, ...value } = cred;
|
|
160
|
+
creds[key] = { enc: await this.encryption.encryptObject(this.normalize(value), aesKey) };
|
|
161
|
+
}
|
|
162
|
+
const batch = { sub, vaultId, creds };
|
|
163
|
+
await this.storage.set(this.pendingKey(pendingId), JSON.stringify(batch), {
|
|
164
|
+
ttlSeconds: Math.ceil(this.pendingTtlMs / 1e3)
|
|
165
|
+
});
|
|
166
|
+
return vaultId;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Flush a staged batch into the live vault, binding it to `sub`. The staged
|
|
170
|
+
* batch's `sub` must equal the supplied `sub` (the one baked into the minted
|
|
171
|
+
* token) \u2014 a mismatch is refused so credentials can never be committed under a
|
|
172
|
+
* different identity than the one that will read them.
|
|
173
|
+
*
|
|
174
|
+
* @returns true when a batch was committed, false when none was staged.
|
|
175
|
+
*/
|
|
176
|
+
async commit(pendingId, sub) {
|
|
177
|
+
const raw = await this.storage.get(this.pendingKey(pendingId));
|
|
178
|
+
if (!raw) return false;
|
|
179
|
+
let batch;
|
|
180
|
+
try {
|
|
181
|
+
batch = JSON.parse(raw);
|
|
182
|
+
} catch {
|
|
183
|
+
await this.storage.delete(this.pendingKey(pendingId));
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (batch.sub !== sub) {
|
|
187
|
+
this.logger.warn("SessionCredentialVault: pending batch subject mismatch on commit; discarding");
|
|
188
|
+
await this.storage.delete(this.pendingKey(pendingId));
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
await this.storage.set(this.indexKey(sub), batch.vaultId, this.credentialSetOpts);
|
|
192
|
+
const keys = [];
|
|
193
|
+
for (const [key, record] of Object.entries(batch.creds)) {
|
|
194
|
+
await this.storage.set(this.dataKey(sub, batch.vaultId, key), JSON.stringify(record), this.credentialSetOpts);
|
|
195
|
+
keys.push(key);
|
|
196
|
+
}
|
|
197
|
+
await this.writeKeySet(sub, batch.vaultId, keys);
|
|
198
|
+
await this.storage.delete(this.pendingKey(pendingId));
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
// ============================================
|
|
202
|
+
// Key-set helpers
|
|
203
|
+
// ============================================
|
|
204
|
+
async readKeySet(sub, vaultId) {
|
|
205
|
+
const raw = await this.storage.get(this.keysKey(sub, vaultId));
|
|
206
|
+
if (!raw) return [];
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(raw);
|
|
209
|
+
return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
|
|
210
|
+
} catch {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async writeKeySet(sub, vaultId, keys) {
|
|
215
|
+
const unique = [...new Set(keys)];
|
|
216
|
+
await this.storage.set(this.keysKey(sub, vaultId), JSON.stringify(unique), this.credentialSetOpts);
|
|
217
|
+
}
|
|
218
|
+
async addKeyToSet(sub, vaultId, key) {
|
|
219
|
+
const keys = await this.readKeySet(sub, vaultId);
|
|
220
|
+
if (!keys.includes(key)) {
|
|
221
|
+
keys.push(key);
|
|
222
|
+
await this.writeKeySet(sub, vaultId, keys);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Strip undefined metadata so the encrypted JSON stays minimal. */
|
|
226
|
+
normalize(value) {
|
|
227
|
+
return value.metadata === void 0 ? { secret: value.secret } : { secret: value.secret, metadata: value.metadata };
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function resolveVaultPepper(logger) {
|
|
231
|
+
const fromEnv = getEnv("VAULT_SECRET") ?? getEnv("JWT_SECRET");
|
|
232
|
+
if (fromEnv && fromEnv.trim().length > 0) return fromEnv;
|
|
233
|
+
logger.warn(
|
|
234
|
+
"SessionCredentialVault: neither VAULT_SECRET nor JWT_SECRET is set; using a random per-process pepper. Stored credentials will NOT survive a restart and cannot be shared across instances. Set VAULT_SECRET (or JWT_SECRET) for persistent, multi-instance credential storage."
|
|
235
|
+
);
|
|
236
|
+
return randomUUID() + randomUUID();
|
|
237
|
+
}
|
|
238
|
+
function errMsg(err) {
|
|
239
|
+
return err instanceof Error ? err.message : String(err);
|
|
240
|
+
}
|
|
241
|
+
export {
|
|
242
|
+
SessionCredentialVault
|
|
243
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// libs/auth/src/session/session-crypto.ts
|
|
2
|
+
import {
|
|
3
|
+
signData,
|
|
4
|
+
verifyData,
|
|
5
|
+
isSignedData,
|
|
6
|
+
verifyOrParseData,
|
|
7
|
+
getEnv,
|
|
8
|
+
isProduction
|
|
9
|
+
} from "@frontmcp/utils";
|
|
10
|
+
import { SessionSecretRequiredError } from "../errors/auth-internal.errors.mjs";
|
|
11
|
+
function getSigningSecret(config) {
|
|
12
|
+
const secret = config?.secret || getEnv("MCP_SESSION_SECRET");
|
|
13
|
+
if (!secret) {
|
|
14
|
+
if (isProduction()) {
|
|
15
|
+
throw new SessionSecretRequiredError("session signing");
|
|
16
|
+
}
|
|
17
|
+
console.warn("[SessionCrypto] MCP_SESSION_SECRET not set. Using insecure default for development only.");
|
|
18
|
+
return "insecure-dev-secret-do-not-use-in-production";
|
|
19
|
+
}
|
|
20
|
+
return secret;
|
|
21
|
+
}
|
|
22
|
+
function resolveConfig(config) {
|
|
23
|
+
return { secret: getSigningSecret(config) };
|
|
24
|
+
}
|
|
25
|
+
function signSession(session, config) {
|
|
26
|
+
return signData(session, resolveConfig(config));
|
|
27
|
+
}
|
|
28
|
+
function verifySession(signedData, config) {
|
|
29
|
+
return verifyData(signedData, resolveConfig(config));
|
|
30
|
+
}
|
|
31
|
+
function verifyOrParseSession(data, config) {
|
|
32
|
+
return verifyOrParseData(data, resolveConfig(config));
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
isSignedData as isSignedSession,
|
|
36
|
+
signSession,
|
|
37
|
+
verifyOrParseSession,
|
|
38
|
+
verifySession
|
|
39
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// libs/auth/src/session/session-rate-limiter.ts
|
|
2
|
+
var SessionRateLimiter = class {
|
|
3
|
+
windowMs;
|
|
4
|
+
maxRequests;
|
|
5
|
+
requests = /* @__PURE__ */ new Map();
|
|
6
|
+
cleanupIntervalMs;
|
|
7
|
+
cleanupTimer = null;
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.windowMs = config.windowMs ?? 6e4;
|
|
10
|
+
this.maxRequests = config.maxRequests ?? 100;
|
|
11
|
+
this.cleanupIntervalMs = config.cleanupIntervalMs ?? 6e4;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Start the periodic cleanup timer on first use (idempotent). Deferred out of
|
|
15
|
+
* the constructor so constructing an instance at module scope has no side
|
|
16
|
+
* effects \u2014 see the constructor note on V8-isolate global-scope restrictions.
|
|
17
|
+
*/
|
|
18
|
+
ensureCleanupTimer() {
|
|
19
|
+
if (this.cleanupTimer || this.cleanupIntervalMs <= 0) return;
|
|
20
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), this.cleanupIntervalMs);
|
|
21
|
+
if (typeof this.cleanupTimer.unref === "function") this.cleanupTimer.unref();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if a request is allowed for the given key.
|
|
25
|
+
*
|
|
26
|
+
* @param key - Identifier for rate limiting (e.g., client IP, session ID prefix)
|
|
27
|
+
* @returns Rate limit result with allowed status and metadata
|
|
28
|
+
*/
|
|
29
|
+
check(key) {
|
|
30
|
+
this.ensureCleanupTimer();
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const windowStart = now - this.windowMs;
|
|
33
|
+
let timestamps = this.requests.get(key);
|
|
34
|
+
if (!timestamps) {
|
|
35
|
+
timestamps = [];
|
|
36
|
+
this.requests.set(key, timestamps);
|
|
37
|
+
}
|
|
38
|
+
const validTimestamps = timestamps.filter((t) => t > windowStart);
|
|
39
|
+
const oldestInWindow = validTimestamps[0] ?? now;
|
|
40
|
+
const resetAt = oldestInWindow + this.windowMs;
|
|
41
|
+
if (validTimestamps.length >= this.maxRequests) {
|
|
42
|
+
return {
|
|
43
|
+
allowed: false,
|
|
44
|
+
remaining: 0,
|
|
45
|
+
resetAt,
|
|
46
|
+
retryAfterMs: resetAt - now
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
validTimestamps.push(now);
|
|
50
|
+
this.requests.set(key, validTimestamps);
|
|
51
|
+
return {
|
|
52
|
+
allowed: true,
|
|
53
|
+
remaining: this.maxRequests - validTimestamps.length,
|
|
54
|
+
resetAt
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a request would be allowed without recording it.
|
|
59
|
+
* Useful for pre-checking without consuming quota.
|
|
60
|
+
*
|
|
61
|
+
* @param key - Identifier for rate limiting
|
|
62
|
+
* @returns true if request would be allowed
|
|
63
|
+
*/
|
|
64
|
+
wouldAllow(key) {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const windowStart = now - this.windowMs;
|
|
67
|
+
const timestamps = this.requests.get(key);
|
|
68
|
+
if (!timestamps) return true;
|
|
69
|
+
const validCount = timestamps.filter((t) => t > windowStart).length;
|
|
70
|
+
return validCount < this.maxRequests;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reset rate limit for a specific key.
|
|
74
|
+
* Useful for testing or after successful authentication.
|
|
75
|
+
*
|
|
76
|
+
* @param key - Identifier to reset
|
|
77
|
+
*/
|
|
78
|
+
reset(key) {
|
|
79
|
+
this.requests.delete(key);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clean up expired entries from all keys.
|
|
83
|
+
* Called automatically on configured interval, but can be called manually.
|
|
84
|
+
*/
|
|
85
|
+
cleanup() {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
const windowStart = now - this.windowMs;
|
|
88
|
+
for (const [key, timestamps] of this.requests.entries()) {
|
|
89
|
+
const valid = timestamps.filter((t) => t > windowStart);
|
|
90
|
+
if (valid.length === 0) {
|
|
91
|
+
this.requests.delete(key);
|
|
92
|
+
} else if (valid.length < timestamps.length) {
|
|
93
|
+
this.requests.set(key, valid);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get current statistics for monitoring.
|
|
99
|
+
*/
|
|
100
|
+
getStats() {
|
|
101
|
+
let totalRequests = 0;
|
|
102
|
+
for (const timestamps of this.requests.values()) {
|
|
103
|
+
totalRequests += timestamps.length;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
totalKeys: this.requests.size,
|
|
107
|
+
totalRequests
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Stop the automatic cleanup timer.
|
|
112
|
+
* Call this when disposing of the rate limiter.
|
|
113
|
+
*/
|
|
114
|
+
dispose() {
|
|
115
|
+
if (this.cleanupTimer) {
|
|
116
|
+
clearInterval(this.cleanupTimer);
|
|
117
|
+
this.cleanupTimer = null;
|
|
118
|
+
}
|
|
119
|
+
this.requests.clear();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var defaultSessionRateLimiter = new SessionRateLimiter();
|
|
123
|
+
export {
|
|
124
|
+
SessionRateLimiter,
|
|
125
|
+
defaultSessionRateLimiter
|
|
126
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// libs/auth/src/session/session.transport.ts
|
|
2
|
+
import { randomUUID } from "@frontmcp/utils";
|
|
3
|
+
var TransportIdGenerator = class {
|
|
4
|
+
/**
|
|
5
|
+
* Create a transport session ID.
|
|
6
|
+
* Generates JWT-style IDs for distributed session support.
|
|
7
|
+
*
|
|
8
|
+
* @returns A transport session ID (UUID without dashes)
|
|
9
|
+
*/
|
|
10
|
+
static createId() {
|
|
11
|
+
return randomUUID().replace(/-/g, "");
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
TransportIdGenerator
|
|
16
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// libs/auth/src/session/signin-binding.ts
|
|
2
|
+
import { base64urlEncode, getCookie, randomBytes, sha256Hex, timingSafeEqual } from "@frontmcp/utils";
|
|
3
|
+
var SIGNIN_BINDING_COOKIE_PREFIX = "frontmcp_signin_";
|
|
4
|
+
var SIGNIN_BINDING_SECURE_COOKIE_PREFIX = `__Host-${SIGNIN_BINDING_COOKIE_PREFIX}`;
|
|
5
|
+
var SIGNIN_BINDING_MAX_AGE_SECONDS = 30 * 60;
|
|
6
|
+
function signinBindingCookieName(pendingAuthId, options = {}) {
|
|
7
|
+
const prefix = options.secure ? SIGNIN_BINDING_SECURE_COOKIE_PREFIX : SIGNIN_BINDING_COOKIE_PREFIX;
|
|
8
|
+
return `${prefix}${sha256Hex(`signin:${pendingAuthId}`).slice(0, 16)}`;
|
|
9
|
+
}
|
|
10
|
+
function createSigninBinding(pendingAuthId, options = {}) {
|
|
11
|
+
const value = base64urlEncode(randomBytes(32));
|
|
12
|
+
return { cookieName: signinBindingCookieName(pendingAuthId, options), value, hash: sha256Hex(value) };
|
|
13
|
+
}
|
|
14
|
+
function signinBindingMatches(cookieHeader, pendingAuthId, expectedHash, options = {}) {
|
|
15
|
+
if (!expectedHash) return false;
|
|
16
|
+
const value = getCookie(cookieHeader, signinBindingCookieName(pendingAuthId, options));
|
|
17
|
+
if (!value) return false;
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
const actual = encoder.encode(sha256Hex(value));
|
|
20
|
+
const expected = encoder.encode(expectedHash);
|
|
21
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
SIGNIN_BINDING_COOKIE_PREFIX,
|
|
25
|
+
SIGNIN_BINDING_MAX_AGE_SECONDS,
|
|
26
|
+
SIGNIN_BINDING_SECURE_COOKIE_PREFIX,
|
|
27
|
+
createSigninBinding,
|
|
28
|
+
signinBindingCookieName,
|
|
29
|
+
signinBindingMatches
|
|
30
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// libs/auth/src/session/storage/in-memory-authorization-vault.ts
|
|
2
|
+
import { MemoryStorageAdapter } from "@frontmcp/utils";
|
|
3
|
+
import { StorageAuthorizationVault } from "./storage-authorization-vault.mjs";
|
|
4
|
+
var InMemoryAuthorizationVault = class extends StorageAuthorizationVault {
|
|
5
|
+
memoryAdapter;
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
const memoryAdapter = new MemoryStorageAdapter();
|
|
8
|
+
super(memoryAdapter, {
|
|
9
|
+
namespace: options.namespace ?? "vault",
|
|
10
|
+
pendingAuthTtlMs: options.pendingAuthTtlMs
|
|
11
|
+
});
|
|
12
|
+
this.memoryAdapter = memoryAdapter;
|
|
13
|
+
void this.memoryAdapter.connect();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Clear all stored data.
|
|
17
|
+
* Useful for testing.
|
|
18
|
+
*/
|
|
19
|
+
async clear() {
|
|
20
|
+
const keys = await this.memoryAdapter.keys("*");
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
await this.memoryAdapter.delete(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export {
|
|
27
|
+
InMemoryAuthorizationVault
|
|
28
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// libs/auth/src/session/storage/index.ts
|
|
2
|
+
import { TypedStorage } from "@frontmcp/utils";
|
|
3
|
+
import { EncryptedTypedStorage, EncryptedStorageError } from "@frontmcp/utils";
|
|
4
|
+
import { StorageTokenStore } from "./storage-token-store.mjs";
|
|
5
|
+
import { StorageAuthorizationVault } from "./storage-authorization-vault.mjs";
|
|
6
|
+
import { InMemoryAuthorizationVault } from "./in-memory-authorization-vault.mjs";
|
|
7
|
+
export {
|
|
8
|
+
EncryptedStorageError,
|
|
9
|
+
EncryptedTypedStorage,
|
|
10
|
+
InMemoryAuthorizationVault,
|
|
11
|
+
StorageAuthorizationVault,
|
|
12
|
+
StorageTokenStore,
|
|
13
|
+
TypedStorage
|
|
14
|
+
};
|