@friggframework/core 2.0.0--canary.643.45b1fbc.0 → 2.0.0--canary.636.7dc8dca.0
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/handlers/routers/user.js +39 -98
- package/index.js +0 -6
- package/logs/index.js +3 -11
- package/logs/logger.js +3 -133
- package/modules/module.js +20 -0
- package/modules/requester/oauth-2.js +147 -3
- package/modules/requester/requester.js +114 -7
- package/package.json +5 -5
- package/types/module-plugin/index.d.ts +3 -0
- package/handlers/rate-limiter.js +0 -90
- package/handlers/routers/user-router.js +0 -226
- package/user/use-cases/login-with-api-key.js +0 -325
- package/user/use-cases/validate-api-key-auth-mode.js +0 -103
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
const Boom = require('@hapi/boom');
|
|
2
|
-
const { Module } = require('../../modules/module');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Maximum accepted length of a submitted API key. Capped before any provider
|
|
6
|
-
* work so the endpoint cannot be used to smuggle arbitrarily large payloads or
|
|
7
|
-
* amplify an oracle attack. Generous enough for JWT-shaped or concatenated keys.
|
|
8
|
-
* @constant {number}
|
|
9
|
-
*/
|
|
10
|
-
const DEFAULT_MAX_API_KEY_LENGTH = 8192;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Classify an error thrown by the module's Requester while validating or
|
|
14
|
-
* identifying a key, per ADR-034 §Security requirement 6 ("Outage ≠ invalid").
|
|
15
|
-
*
|
|
16
|
-
* A definitive provider rejection (401/403, or any other non-5xx client error)
|
|
17
|
-
* means the key is bad. A 5xx, a network failure, or a timeout means the
|
|
18
|
-
* provider is unavailable and MUST NOT be reported as an invalid key nor allowed
|
|
19
|
-
* to mint a session.
|
|
20
|
-
*
|
|
21
|
-
* @param {*} err - The thrown error.
|
|
22
|
-
* @returns {'invalid'|'unavailable'} classification
|
|
23
|
-
*/
|
|
24
|
-
function classifyProviderError(err) {
|
|
25
|
-
const status =
|
|
26
|
-
err?.statusCode ?? err?.response?.status ?? err?.status ?? undefined;
|
|
27
|
-
|
|
28
|
-
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
29
|
-
// 401/403 and any other definitive 4xx from the provider → bad key.
|
|
30
|
-
return 'invalid';
|
|
31
|
-
}
|
|
32
|
-
// 5xx, or no status at all (timeout / DNS / socket error) → outage.
|
|
33
|
-
return 'unavailable';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Generic invalid-credentials error. Deliberately identical for every failure
|
|
38
|
-
* reason on the bad-key path so the endpoint never enumerates users or keys
|
|
39
|
-
* (ADR-034 §Security requirement 3).
|
|
40
|
-
* @returns {Boom} 401
|
|
41
|
-
*/
|
|
42
|
-
function invalidCredentials() {
|
|
43
|
-
return Boom.unauthorized('Invalid credentials');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Provider-unavailable error, distinct from invalid credentials. No session is
|
|
48
|
-
* created and no cookie is cleared when this is thrown (ADR-034 §6).
|
|
49
|
-
* @returns {Boom} 503
|
|
50
|
-
*/
|
|
51
|
-
function providerUnavailable() {
|
|
52
|
-
return Boom.serverUnavailable('Identity provider unavailable');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Use case implementing the ADR-034 `apiKey` auth mode: log a browser end user
|
|
57
|
-
* in with their own product API key, validated *through the api-module itself*.
|
|
58
|
-
*
|
|
59
|
-
* Flow (see ADR-034):
|
|
60
|
-
* 1. Validate + identify the key via the configured identity module's
|
|
61
|
-
* Requester (`testAuthRequest`, then `getEntityDetails`).
|
|
62
|
-
* 2. Derive a provider-authoritative tenant identity — NEVER from client input.
|
|
63
|
-
* 3. Find-or-create the Frigg user from that identity (ordinary app user).
|
|
64
|
-
* 4. Create the Credential + Entity via `ProcessAuthorizationCallback`.
|
|
65
|
-
* 5. Mint a short-lived Frigg session token and return it.
|
|
66
|
-
*
|
|
67
|
-
* @class LoginWithApiKey
|
|
68
|
-
*/
|
|
69
|
-
class LoginWithApiKey {
|
|
70
|
-
/**
|
|
71
|
-
* @param {Object} params
|
|
72
|
-
* @param {Object} params.userConfig - App-definition `user` config (reads `authModes.apiKey`).
|
|
73
|
-
* @param {Array<Object>} params.moduleDefinitions - Module definitions available to the app.
|
|
74
|
-
* @param {import('./get-user-from-x-frigg-headers').GetUserFromXFriggHeaders} params.getUserFromXFriggHeaders - Reused find-or-create path.
|
|
75
|
-
* @param {import('../../modules/use-cases/process-authorization-callback').ProcessAuthorizationCallback} params.processAuthorizationCallback - Reused credential/entity creation.
|
|
76
|
-
* @param {import('./create-token-for-user-id').CreateTokenForUserId} params.createTokenForUserId - Reused session-token minting.
|
|
77
|
-
* @param {number} [params.tokenExpiryMinutes=120] - Access token TTL. Short by design (ADR-034 §5): revocation latency is bounded by this.
|
|
78
|
-
* @param {number} [params.maxApiKeyLength=8192] - Length cap enforced before any provider work.
|
|
79
|
-
* @param {typeof Module} [params.ModuleClass=Module] - Injectable Module class (for testing without a real Requester).
|
|
80
|
-
*/
|
|
81
|
-
constructor({
|
|
82
|
-
userConfig,
|
|
83
|
-
moduleDefinitions,
|
|
84
|
-
getUserFromXFriggHeaders,
|
|
85
|
-
processAuthorizationCallback,
|
|
86
|
-
createTokenForUserId,
|
|
87
|
-
tokenExpiryMinutes = 120,
|
|
88
|
-
maxApiKeyLength = DEFAULT_MAX_API_KEY_LENGTH,
|
|
89
|
-
ModuleClass = Module,
|
|
90
|
-
}) {
|
|
91
|
-
this.userConfig = userConfig || {};
|
|
92
|
-
this.moduleDefinitions = moduleDefinitions || [];
|
|
93
|
-
this.getUserFromXFriggHeaders = getUserFromXFriggHeaders;
|
|
94
|
-
this.processAuthorizationCallback = processAuthorizationCallback;
|
|
95
|
-
this.createTokenForUserId = createTokenForUserId;
|
|
96
|
-
this.tokenExpiryMinutes = tokenExpiryMinutes;
|
|
97
|
-
this.maxApiKeyLength = maxApiKeyLength;
|
|
98
|
-
this.ModuleClass = ModuleClass;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Resolve which identity module a request may use.
|
|
103
|
-
*
|
|
104
|
-
* The module is fixed by config (`authModes.apiKey.module`). A multi-identity
|
|
105
|
-
* app MAY configure an allowlist (`authModes.apiKey.modules`) and let the
|
|
106
|
-
* client pick one via the request body — but only from that allowlist. A
|
|
107
|
-
* client-supplied module that is not allowlisted is rejected generically, so
|
|
108
|
-
* the endpoint cannot be steered at an arbitrary module.
|
|
109
|
-
*
|
|
110
|
-
* @param {string} [requestedModule] - Optional module name from the request body.
|
|
111
|
-
* @returns {string} The resolved module name.
|
|
112
|
-
* @throws {Boom} 401 generic if apiKey mode is unconfigured or the request names a non-allowlisted module.
|
|
113
|
-
*/
|
|
114
|
-
resolveModuleName(requestedModule) {
|
|
115
|
-
const config = this.userConfig.authModes?.apiKey;
|
|
116
|
-
if (!config) {
|
|
117
|
-
// apiKey mode not enabled for this app.
|
|
118
|
-
throw invalidCredentials();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Allowlist = the UNION of the explicit `modules` array and the single
|
|
122
|
-
// `module` (matching validateApiKeyAuthMode's own union). This keeps the
|
|
123
|
-
// resolver and the wiring-time validator in agreement: a config like
|
|
124
|
-
// `{ modules: [], module: 'reevo' }` passes validation AND resolves to
|
|
125
|
-
// ['reevo'] here, rather than validation passing while the resolver saw
|
|
126
|
-
// an empty list and rejected every login. Never an open set.
|
|
127
|
-
const allowlist = [
|
|
128
|
-
...(Array.isArray(config.modules) ? config.modules : []),
|
|
129
|
-
...(config.module ? [config.module] : []),
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
if (allowlist.length === 0) {
|
|
133
|
-
throw invalidCredentials();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (requestedModule) {
|
|
137
|
-
if (!allowlist.includes(requestedModule)) {
|
|
138
|
-
throw invalidCredentials();
|
|
139
|
-
}
|
|
140
|
-
return requestedModule;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// No client-specified module: only unambiguous when exactly one is configured.
|
|
144
|
-
if (allowlist.length === 1) {
|
|
145
|
-
return allowlist[0];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Multi-identity app but the client did not name a module.
|
|
149
|
-
throw invalidCredentials();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Validate the key against the provider and derive a provider-authoritative
|
|
154
|
-
* tenant identity. Reused by login (and available for refresh re-validation,
|
|
155
|
-
* ADR-034 §5): a revoked key stops validating here.
|
|
156
|
-
*
|
|
157
|
-
* @param {string} apiKey
|
|
158
|
-
* @param {string} moduleName
|
|
159
|
-
* @returns {Promise<{ externalId: string, moduleDefinition: Object }>}
|
|
160
|
-
* @throws {Boom} 401 invalid credentials on a bad/rejected key or missing identifier; 503 on provider outage.
|
|
161
|
-
*/
|
|
162
|
-
async validateAndIdentify(apiKey, moduleName) {
|
|
163
|
-
const moduleDefinition = this.moduleDefinitions.find(
|
|
164
|
-
(def) => def.moduleName === moduleName
|
|
165
|
-
);
|
|
166
|
-
if (!moduleDefinition) {
|
|
167
|
-
// Should be caught at config-validation time; treat a runtime miss
|
|
168
|
-
// as a server misconfiguration rather than leaking specifics.
|
|
169
|
-
throw Boom.badImplementation(
|
|
170
|
-
`apiKey identity module '${moduleName}' is not registered`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const module = new this.ModuleClass({ definition: moduleDefinition });
|
|
175
|
-
|
|
176
|
-
// Seed the api client with the key without persisting anything.
|
|
177
|
-
const setAuthParams =
|
|
178
|
-
moduleDefinition.requiredAuthMethods?.setAuthParams;
|
|
179
|
-
if (typeof setAuthParams === 'function') {
|
|
180
|
-
await setAuthParams(module.api, { api_key: apiKey });
|
|
181
|
-
} else if (typeof module.api?.setApiKey === 'function') {
|
|
182
|
-
module.api.setApiKey(apiKey);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 1) Validity. Call testAuthRequest directly (NOT module.testAuth, which
|
|
186
|
-
// swallows the error and would collapse 401 and 503 into one `false`).
|
|
187
|
-
let isValid;
|
|
188
|
-
try {
|
|
189
|
-
isValid =
|
|
190
|
-
await moduleDefinition.requiredAuthMethods.testAuthRequest(
|
|
191
|
-
module.api
|
|
192
|
-
);
|
|
193
|
-
} catch (err) {
|
|
194
|
-
throw classifyProviderError(err) === 'unavailable'
|
|
195
|
-
? providerUnavailable()
|
|
196
|
-
: invalidCredentials();
|
|
197
|
-
}
|
|
198
|
-
// Require a STRICT boolean pass. A module that returns a truthy value on
|
|
199
|
-
// a bad key (e.g. an error object, a non-empty string, a response body)
|
|
200
|
-
// must NOT clear the validity gate — only an explicit `true` does. The
|
|
201
|
-
// login-path contract for testAuthRequest is: throw, or return `true`.
|
|
202
|
-
if (isValid !== true) {
|
|
203
|
-
throw invalidCredentials();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 2) Identity. Derive the tenant id from the provider response only.
|
|
207
|
-
let entityDetails;
|
|
208
|
-
try {
|
|
209
|
-
entityDetails =
|
|
210
|
-
await moduleDefinition.requiredAuthMethods.getEntityDetails(
|
|
211
|
-
module.api,
|
|
212
|
-
{ api_key: apiKey },
|
|
213
|
-
undefined,
|
|
214
|
-
undefined
|
|
215
|
-
);
|
|
216
|
-
} catch (err) {
|
|
217
|
-
throw classifyProviderError(err) === 'unavailable'
|
|
218
|
-
? providerUnavailable()
|
|
219
|
-
: invalidCredentials();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const externalId = entityDetails?.identifiers?.externalId;
|
|
223
|
-
// Require a scalar, string-or-number identifier. Anything else (an
|
|
224
|
-
// object, array, boolean, null/undefined) is NOT a stable identifier and
|
|
225
|
-
// must be rejected rather than String()-coerced into a bogus one like
|
|
226
|
-
// "[object Object]" (ADR-034 §Security requirement 1).
|
|
227
|
-
if (
|
|
228
|
-
(typeof externalId !== 'string' &&
|
|
229
|
-
typeof externalId !== 'number') ||
|
|
230
|
-
String(externalId).trim() === ''
|
|
231
|
-
) {
|
|
232
|
-
throw invalidCredentials();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return { externalId: String(externalId), moduleDefinition };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Execute the api-key login.
|
|
240
|
-
*
|
|
241
|
-
* @param {Object} input
|
|
242
|
-
* @param {string} input.apiKey - The submitted product API key.
|
|
243
|
-
* @param {string} [input.module] - Optional module name (multi-identity apps only; allowlisted).
|
|
244
|
-
* @returns {Promise<{ token: string, userId: string, module: string }>} The minted session token and principal.
|
|
245
|
-
* @throws {Boom} 401 generic on invalid key / missing identifier / unconfigured mode; 503 on provider outage.
|
|
246
|
-
*/
|
|
247
|
-
async execute({ apiKey, module: requestedModule } = {}) {
|
|
248
|
-
// Cap length and shape BEFORE any provider work (ADR-034 §3).
|
|
249
|
-
if (typeof apiKey !== 'string' || apiKey.length === 0) {
|
|
250
|
-
throw invalidCredentials();
|
|
251
|
-
}
|
|
252
|
-
if (apiKey.length > this.maxApiKeyLength) {
|
|
253
|
-
throw invalidCredentials();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const moduleName = this.resolveModuleName(requestedModule);
|
|
257
|
-
|
|
258
|
-
const { externalId } = await this.validateAndIdentify(
|
|
259
|
-
apiKey,
|
|
260
|
-
moduleName
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
// Namespace the provider identity by the RESOLVED module. In a
|
|
264
|
-
// multi-module allowlist two different providers can legitimately return
|
|
265
|
-
// the SAME externalId (e.g. both use the numeric account id "42"); a bare
|
|
266
|
-
// externalId would then collapse those two distinct tenants onto one
|
|
267
|
-
// Frigg user. Prefixing with the module keeps them separate for BOTH the
|
|
268
|
-
// org-user and individual-user identity.
|
|
269
|
-
const identity = `${moduleName}:${externalId}`;
|
|
270
|
-
|
|
271
|
-
// Find-or-create the Frigg user from the PROVIDER-DERIVED identity only.
|
|
272
|
-
// A client-supplied appOrgId/appUserId is never read here — the caller
|
|
273
|
-
// passes nothing but the key and (optionally) the allowlisted module.
|
|
274
|
-
const useOrg = this.userConfig.organizationUserRequired === true;
|
|
275
|
-
const appOrgId = useOrg ? identity : undefined;
|
|
276
|
-
const appUserId = useOrg ? undefined : identity;
|
|
277
|
-
|
|
278
|
-
// NOTE (accepted cleanup debt): the user is found-or-created before the
|
|
279
|
-
// credential is provisioned below. If ProcessAuthorizationCallback fails,
|
|
280
|
-
// a user with no credential/entity is left behind. Reordering to create
|
|
281
|
-
// the credential first is intentionally out of scope here; orphaned users
|
|
282
|
-
// on partial failure are tolerated and cleaned up out of band.
|
|
283
|
-
const user = await this.getUserFromXFriggHeaders.execute(
|
|
284
|
-
appUserId,
|
|
285
|
-
appOrgId
|
|
286
|
-
);
|
|
287
|
-
const userId = user.getId();
|
|
288
|
-
if (!userId) {
|
|
289
|
-
throw invalidCredentials();
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Create/refresh the Credential + Entity through the same path
|
|
293
|
-
// /api/authorize uses. The key is persisted only as the encrypted
|
|
294
|
-
// Credential — never returned, never logged, never a JWT claim.
|
|
295
|
-
const callbackResult = await this.processAuthorizationCallback.execute(
|
|
296
|
-
userId,
|
|
297
|
-
moduleName,
|
|
298
|
-
{ api_key: apiKey }
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
// Defense-in-depth: never mint a session unless the credential was
|
|
302
|
-
// actually persisted. A callback that returns without a credential id
|
|
303
|
-
// means the key was not connected; minting anyway would hand out a
|
|
304
|
-
// session over a half-provisioned tenant. Fail 500-class, not 401.
|
|
305
|
-
if (!callbackResult || !callbackResult.credential_id) {
|
|
306
|
-
throw Boom.badImplementation(
|
|
307
|
-
'Login failed to create a credential for the api-key identity'
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Mint an ordinary, short-lived app-user session token (never admin).
|
|
312
|
-
const token = await this.createTokenForUserId.execute(
|
|
313
|
-
userId,
|
|
314
|
-
this.tokenExpiryMinutes
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
return { token, userId, module: moduleName };
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
module.exports = {
|
|
322
|
-
LoginWithApiKey,
|
|
323
|
-
classifyProviderError,
|
|
324
|
-
DEFAULT_MAX_API_KEY_LENGTH,
|
|
325
|
-
};
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validate the `user.authModes.apiKey` block of an app definition against the
|
|
3
|
-
* app's registered module definitions (ADR-034 §Config).
|
|
4
|
-
*
|
|
5
|
-
* Default-off: when `authModes.apiKey` is absent this is a no-op, so apps that
|
|
6
|
-
* do not opt in are completely unaffected. When present, every named identity
|
|
7
|
-
* module MUST exist in the app's modules, or the app is misconfigured and we
|
|
8
|
-
* fail fast at wiring time rather than at first login.
|
|
9
|
-
*
|
|
10
|
-
* Accepts either:
|
|
11
|
-
* - `authModes.apiKey.module` — a single identity module (the common case), or
|
|
12
|
-
* - `authModes.apiKey.modules` — an allowlist of identity modules (multi-identity apps).
|
|
13
|
-
*
|
|
14
|
-
* @param {Object} userConfig - The app definition's `user` config (may be null).
|
|
15
|
-
* @param {Array<Object>} moduleDefinitions - Registered module definitions (each with `moduleName`).
|
|
16
|
-
* @throws {Error} If apiKey mode is declared but names no module, or names a module the app does not register.
|
|
17
|
-
* @returns {void}
|
|
18
|
-
*/
|
|
19
|
-
function validateApiKeyAuthMode(userConfig, moduleDefinitions = []) {
|
|
20
|
-
const config = userConfig?.authModes?.apiKey;
|
|
21
|
-
if (!config) {
|
|
22
|
-
return; // apiKey mode not enabled — nothing to validate.
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const named = [];
|
|
26
|
-
if (Array.isArray(config.modules)) {
|
|
27
|
-
named.push(...config.modules);
|
|
28
|
-
}
|
|
29
|
-
if (config.module) {
|
|
30
|
-
named.push(config.module);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (named.length === 0) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
'Invalid app definition: user.authModes.apiKey is enabled but names no identity module. ' +
|
|
36
|
-
"Set authModes.apiKey.module = '<moduleName>' (or authModes.apiKey.modules = ['<moduleName>', ...])."
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const known = new Set(
|
|
41
|
-
(moduleDefinitions || [])
|
|
42
|
-
.map((def) => def && def.moduleName)
|
|
43
|
-
.filter(Boolean)
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
for (const moduleName of named) {
|
|
47
|
-
if (typeof moduleName !== 'string' || moduleName.trim() === '') {
|
|
48
|
-
throw new Error(
|
|
49
|
-
'Invalid app definition: user.authModes.apiKey names a non-string module.'
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
if (!known.has(moduleName)) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Invalid app definition: user.authModes.apiKey.module '${moduleName}' is not a registered module. ` +
|
|
55
|
-
`Registered modules: ${[...known].join(', ') || '(none)'}.`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// allowedOrigins, when present, MUST be an array. A bare string would be
|
|
61
|
-
// iterated character-by-character by the Origin/Referer allowlist check
|
|
62
|
-
// (`allowed.includes(candidate)` on a string tests substrings), silently
|
|
63
|
-
// widening or breaking CSRF enforcement.
|
|
64
|
-
if (
|
|
65
|
-
config.allowedOrigins !== undefined &&
|
|
66
|
-
!Array.isArray(config.allowedOrigins)
|
|
67
|
-
) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
'Invalid app definition: user.authModes.apiKey.allowedOrigins must be an array of origin strings.'
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// rateLimit, when present, must be an object whose numeric knobs are positive
|
|
74
|
-
// finite numbers. A `0`, negative, or NaN would disable or corrupt the
|
|
75
|
-
// limiter (e.g. maxPerKey:0 rejects every request; windowMs:NaN never rolls),
|
|
76
|
-
// so fail fast at wiring time rather than shipping a broken oracle guard.
|
|
77
|
-
if (config.rateLimit !== undefined) {
|
|
78
|
-
if (
|
|
79
|
-
typeof config.rateLimit !== 'object' ||
|
|
80
|
-
config.rateLimit === null ||
|
|
81
|
-
Array.isArray(config.rateLimit)
|
|
82
|
-
) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
'Invalid app definition: user.authModes.apiKey.rateLimit must be an object.'
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
for (const field of ['maxPerKey', 'maxGlobal', 'windowMs']) {
|
|
88
|
-
const value = config.rateLimit[field];
|
|
89
|
-
if (
|
|
90
|
-
value !== undefined &&
|
|
91
|
-
(typeof value !== 'number' ||
|
|
92
|
-
!Number.isFinite(value) ||
|
|
93
|
-
value <= 0)
|
|
94
|
-
) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Invalid app definition: user.authModes.apiKey.rateLimit.${field} must be a positive finite number.`
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
module.exports = { validateApiKeyAuthMode };
|