@nxgt/janus 0.17.2 → 0.19.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/README.md +107 -27
- package/dist/auth/config/index.d.ts +1 -1
- package/dist/auth/config/index.d.ts.map +1 -1
- package/dist/auth/config/janus-config.d.ts +31 -0
- package/dist/auth/config/janus-config.d.ts.map +1 -1
- package/dist/auth/config/resolve-config.d.ts.map +1 -1
- package/dist/auth/config/resolve-throttle.d.ts +30 -0
- package/dist/auth/config/resolve-throttle.d.ts.map +1 -0
- package/dist/auth/config/resolved-config.d.ts +5 -0
- package/dist/auth/config/resolved-config.d.ts.map +1 -1
- package/dist/auth/config/warn-throttle-window.d.ts +14 -0
- package/dist/auth/config/warn-throttle-window.d.ts.map +1 -0
- package/dist/auth/counter-key.d.ts +19 -0
- package/dist/auth/counter-key.d.ts.map +1 -0
- package/dist/auth/email-flows/reset-password.d.ts.map +1 -1
- package/dist/auth/email-flows/verify-email.d.ts.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/janus.d.ts.map +1 -1
- package/dist/auth/magic-link.d.ts.map +1 -1
- package/dist/auth/mail-requests.d.ts +51 -0
- package/dist/auth/mail-requests.d.ts.map +1 -0
- package/dist/auth/port/assert-stores.d.ts.map +1 -1
- package/dist/auth/port/memory/users.d.ts.map +1 -1
- package/dist/auth/port/types/stores.d.ts +5 -0
- package/dist/auth/port/types/stores.d.ts.map +1 -1
- package/dist/auth/port/types/users.d.ts +17 -0
- package/dist/auth/port/types/users.d.ts.map +1 -1
- package/dist/auth/sign-in-attempts.d.ts +3 -6
- package/dist/auth/sign-in-attempts.d.ts.map +1 -1
- package/dist/auth/sign-in-code.d.ts +3 -2
- package/dist/auth/sign-in-code.d.ts.map +1 -1
- package/dist/auth/step-up/request.d.ts.map +1 -1
- package/dist/auth/types/email-flows.d.ts +16 -6
- package/dist/auth/types/email-flows.d.ts.map +1 -1
- package/dist/auth/types/magic-link.d.ts +4 -2
- package/dist/auth/types/magic-link.d.ts.map +1 -1
- package/dist/auth/types/step-up.d.ts +4 -1
- package/dist/auth/types/step-up.d.ts.map +1 -1
- package/dist/auth/types/user-type.d.ts +15 -0
- package/dist/auth/types/user-type.d.ts.map +1 -1
- package/dist/auth/users/find-many.d.ts +29 -0
- package/dist/auth/users/find-many.d.ts.map +1 -0
- package/dist/auth/users/record-flows.d.ts.map +1 -1
- package/dist/chunks/{index-6fxq0kzf.js → index-4khsm7mw.js} +2 -2
- package/dist/chunks/{index-7xe5x3eg.js → index-6bjfv4xv.js} +9 -3
- package/dist/chunks/{index-7xe5x3eg.js.map → index-6bjfv4xv.js.map} +3 -3
- package/dist/chunks/{index-k136102s.js → index-cntabs9d.js} +2 -2
- package/dist/chunks/{index-ztng6018.js → index-kppkcbn5.js} +10 -3
- package/dist/chunks/{index-ztng6018.js.map → index-kppkcbn5.js.map} +4 -4
- package/dist/conformance/cases/outage.d.ts.map +1 -1
- package/dist/conformance/cases/users/batch.d.ts +8 -0
- package/dist/conformance/cases/users/batch.d.ts.map +1 -0
- package/dist/conformance/cases/users/index.d.ts.map +1 -1
- package/dist/conformance/describe.d.ts +2 -1
- package/dist/conformance/describe.d.ts.map +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +106 -46
- package/dist/conformance/index.js.map +7 -6
- package/dist/conformance/types.d.ts +10 -2
- package/dist/conformance/types.d.ts.map +1 -1
- package/dist/errors/janus-error/codes.d.ts +10 -0
- package/dist/errors/janus-error/codes.d.ts.map +1 -1
- package/dist/errors/janus-error/index.d.ts +1 -1
- package/dist/errors/janus-error/index.d.ts.map +1 -1
- package/dist/errors/janus-error/options.d.ts +4 -3
- package/dist/errors/janus-error/options.d.ts.map +1 -1
- package/dist/errors/janus-error/refusals.d.ts +8 -0
- package/dist/errors/janus-error/refusals.d.ts.map +1 -1
- package/dist/errors/janus-error/status.d.ts +1 -1
- package/dist/errors/janus-error/status.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +171 -30
- package/dist/index.js.map +18 -14
- package/dist/permissions/index.js +3 -3
- package/docs/README.md +1 -1
- package/docs/guide/adapters.md +44 -4
- package/docs/guide/email-flows.md +164 -4
- package/docs/guide/errors.md +12 -3
- package/docs/guide/magic-link.md +41 -5
- package/docs/guide/passwords.md +2 -1
- package/docs/guide/sign-in-code.md +46 -7
- package/docs/guide/step-up.md +38 -7
- package/docs/guide/users.md +49 -0
- package/docs/roadmap.md +26 -15
- package/docs/troubleshooting.md +127 -5
- package/package.json +1 -1
- package/dist/auth/config/resolve-sign-in.d.ts +0 -5
- package/dist/auth/config/resolve-sign-in.d.ts.map +0 -1
- /package/dist/chunks/{index-6fxq0kzf.js.map → index-4khsm7mw.js.map} +0 -0
- /package/dist/chunks/{index-k136102s.js.map → index-cntabs9d.js.map} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `@nxgt/janus/conformance` fails an adapter that breaks it.
|
|
22
22
|
*/
|
|
23
23
|
export * from './auth/index';
|
|
24
|
-
export { CredentialError, type CredentialRefusal, InvalidCursorError, type Issue, JanusError, type JanusErrorCode, type JanusErrorOptions, type JanusErrorStatus, NotFoundError, PermissionDepthError, SecondFactorError, StepUpRequiredError, StoreConflict, StoreFailure, statusOf, TokenError, UnsupportedError, UserInactiveError, UserInvalidError, } from './errors/janus-error';
|
|
24
|
+
export { CredentialError, type CredentialRefusal, InvalidCursorError, type Issue, JanusError, type JanusErrorCode, type JanusErrorOptions, type JanusErrorStatus, MailThrottledError, NotFoundError, PermissionDepthError, SecondFactorError, StepUpRequiredError, StoreConflict, StoreFailure, statusOf, TokenError, UnsupportedError, UserInactiveError, UserInvalidError, } from './errors/janus-error';
|
|
25
25
|
export { type Id, isId, mintedAt, mintId, } from './ids/id';
|
|
26
26
|
export { type CursorPage, DEFAULT_PAGE_SIZE, invalidCursor, MAX_PAGE_SIZE, pageLimit, } from './pagination/cursor-page';
|
|
27
27
|
export { formatEntity, formatSubject, formatTuple, parseSubject, parseTuple, } from './subjects/notation';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,cAAc,cAAc,CAAC;AAC7B,OAAO,EACN,eAAe,EACf,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,KAAK,KAAK,EACV,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,KAAK,EAAE,EACP,IAAI,EACJ,QAAQ,EACR,MAAM,GACN,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,KAAK,UAAU,EACf,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,SAAS,GACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,YAAY,EACZ,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,GACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACN,KAAK,MAAM,EACX,OAAO,EACP,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,EACL,SAAS,GACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,KAAK,QAAQ,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,cAAc,cAAc,CAAC;AAC7B,OAAO,EACN,eAAe,EACf,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,KAAK,KAAK,EACV,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,KAAK,EAAE,EACP,IAAI,EACJ,QAAQ,EACR,MAAM,GACN,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,KAAK,UAAU,EACf,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,SAAS,GACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,YAAY,EACZ,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,GACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACN,KAAK,MAAM,EACX,OAAO,EACP,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,EACL,SAAS,GACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,KAAK,QAAQ,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
MAX_PAGE_SIZE2,
|
|
8
8
|
pageLimit2,
|
|
9
9
|
invalidCursor2
|
|
10
|
-
} from "./chunks/index-
|
|
10
|
+
} from "./chunks/index-4khsm7mw.js";
|
|
11
11
|
import {
|
|
12
12
|
mintId2,
|
|
13
13
|
isId2,
|
|
14
14
|
mintedAt2,
|
|
15
15
|
createMemoryStores2
|
|
16
|
-
} from "./chunks/index-
|
|
16
|
+
} from "./chunks/index-6bjfv4xv.js";
|
|
17
17
|
import {
|
|
18
18
|
JanusError2,
|
|
19
19
|
UserInvalidError2,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
TokenError2,
|
|
23
23
|
SecondFactorError2,
|
|
24
24
|
StepUpRequiredError2,
|
|
25
|
+
MailThrottledError2,
|
|
25
26
|
InvalidCursorError2,
|
|
26
27
|
UnsupportedError2,
|
|
27
28
|
PermissionDepthError2,
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
formatTuple2,
|
|
39
40
|
parseTuple2,
|
|
40
41
|
parseSubject2
|
|
41
|
-
} from "./chunks/index-
|
|
42
|
+
} from "./chunks/index-kppkcbn5.js";
|
|
42
43
|
|
|
43
44
|
// src/auth/hashers.ts
|
|
44
45
|
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
@@ -303,25 +304,32 @@ function resolveSecondFactor(config, where) {
|
|
|
303
304
|
};
|
|
304
305
|
}
|
|
305
306
|
|
|
306
|
-
// src/auth/config/resolve-
|
|
307
|
+
// src/auth/config/resolve-throttle.ts
|
|
307
308
|
function resolveSignInThrottle(config, where) {
|
|
309
|
+
return resolveThrottle(config, { option: "signIn", attempts: 10, window: "15m" }, where);
|
|
310
|
+
}
|
|
311
|
+
function resolveMailThrottle(config, where) {
|
|
312
|
+
return resolveThrottle(config, { option: "mail", attempts: 5, window: "10m" }, where);
|
|
313
|
+
}
|
|
314
|
+
function resolveThrottle(config, defaults, where) {
|
|
315
|
+
const { option } = defaults;
|
|
308
316
|
if (config !== undefined && (typeof config !== "object" || config === null)) {
|
|
309
|
-
throw new TypeError(`${where}:
|
|
317
|
+
throw new TypeError(`${where}: ${option} must be an object — { throttle }`);
|
|
310
318
|
}
|
|
311
319
|
const throttle = config?.throttle ?? {};
|
|
312
320
|
if (throttle === false)
|
|
313
321
|
return null;
|
|
314
|
-
const at = `${where}:
|
|
322
|
+
const at = `${where}: ${option}.throttle`;
|
|
315
323
|
if (typeof throttle !== "object" || throttle === null) {
|
|
316
324
|
throw new TypeError(`${at} must be { attempts, window }, or false to count nothing`);
|
|
317
325
|
}
|
|
318
|
-
const attempts = throttle.attempts ??
|
|
326
|
+
const attempts = throttle.attempts ?? defaults.attempts;
|
|
319
327
|
if (!Number.isSafeInteger(attempts) || attempts < 1) {
|
|
320
328
|
throw new TypeError(`${at}.attempts must be a whole number above zero`);
|
|
321
329
|
}
|
|
322
330
|
return {
|
|
323
331
|
attempts,
|
|
324
|
-
windowMs: parseDuration(throttle.window ??
|
|
332
|
+
windowMs: parseDuration(throttle.window ?? defaults.window, `${at}.window`)
|
|
325
333
|
};
|
|
326
334
|
}
|
|
327
335
|
|
|
@@ -372,6 +380,7 @@ function resolveConfig(config, where) {
|
|
|
372
380
|
tokenTtlMs: resolveTokenTtls(config, where),
|
|
373
381
|
cookie: resolveCookie(config.cookie ?? {}, where),
|
|
374
382
|
signInThrottle: resolveSignInThrottle(config.signIn, where),
|
|
383
|
+
mailThrottle: resolveMailThrottle(config.mail, where),
|
|
375
384
|
secondFactor: resolveSecondFactor(config.secondFactor, where),
|
|
376
385
|
devices: resolveDevices(config.devices, where)
|
|
377
386
|
};
|
|
@@ -419,6 +428,46 @@ function resolveTokenTtls(config, where) {
|
|
|
419
428
|
stepUp: parseDuration(config.tokens?.stepUp ?? "10m", `${where}: tokens.stepUp`)
|
|
420
429
|
};
|
|
421
430
|
}
|
|
431
|
+
// src/auth/config/warn-throttle-window.ts
|
|
432
|
+
var FLOWS = [
|
|
433
|
+
"magicLink",
|
|
434
|
+
"signInCode",
|
|
435
|
+
"resetPassword",
|
|
436
|
+
"verifyEmail",
|
|
437
|
+
"stepUp"
|
|
438
|
+
];
|
|
439
|
+
var UNITS2 = [
|
|
440
|
+
["d", 86400000],
|
|
441
|
+
["h", 3600000],
|
|
442
|
+
["m", 60000],
|
|
443
|
+
["s", 1000]
|
|
444
|
+
];
|
|
445
|
+
function show(ms) {
|
|
446
|
+
const unit = UNITS2.find(([, size]) => ms % size === 0);
|
|
447
|
+
return unit === undefined ? `${ms}ms` : `${ms / unit[1]}${unit[0]}`;
|
|
448
|
+
}
|
|
449
|
+
function throttleWindowMessage(resolved, where) {
|
|
450
|
+
const throttle = resolved.mailThrottle;
|
|
451
|
+
if (throttle === null)
|
|
452
|
+
return null;
|
|
453
|
+
const window = show(throttle.windowMs);
|
|
454
|
+
const shorter = FLOWS.filter((flow) => resolved.tokenTtlMs[flow] < throttle.windowMs);
|
|
455
|
+
if (shorter.length === 0)
|
|
456
|
+
return null;
|
|
457
|
+
const shortest = Math.min(...shorter.map((flow) => resolved.tokenTtlMs[flow]));
|
|
458
|
+
const parts = shorter.map((flow) => `tokens.${flow} is ${show(resolved.tokenTtlMs[flow])}, shorter than mail.throttle.window, ${window}`);
|
|
459
|
+
return `${where}: ${parts.join("; ")} — past the limit the last ${shorter.length === 1 ? "token" : "tokens"} sent can expire before the refusal ends. Set mail.throttle.window to ${show(shortest)} or less, or raise ${shorter.map((flow) => `tokens.${flow}`).join(", ")} to ${window} or more.`;
|
|
460
|
+
}
|
|
461
|
+
function warnThrottleWindow(resolved, where) {
|
|
462
|
+
const message = throttleWindowMessage(resolved, where);
|
|
463
|
+
if (message === null)
|
|
464
|
+
return;
|
|
465
|
+
if (typeof process === "undefined" || typeof process.emitWarning !== "function") {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
process.emitWarning(message, { code: "JANUS_THROTTLE_WINDOW" });
|
|
469
|
+
}
|
|
470
|
+
|
|
422
471
|
// src/auth/context/create-context.ts
|
|
423
472
|
function createContext(config, store, relations, capabilities, clock, hasher, verifiers, events = null) {
|
|
424
473
|
let dummy = null;
|
|
@@ -725,12 +774,18 @@ function assertStores(stores, where) {
|
|
|
725
774
|
}
|
|
726
775
|
}
|
|
727
776
|
}
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
777
|
+
const slots = stores;
|
|
778
|
+
return {
|
|
779
|
+
collectExpired: optional(slots, "sessions", "deleteExpiredSessions", where),
|
|
780
|
+
findUsers: optional(slots, "users", "findUsers", where)
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function optional(stores, slot, method, where) {
|
|
784
|
+
const found = stores[slot][method];
|
|
785
|
+
if (found !== undefined && typeof found !== "function") {
|
|
786
|
+
throw new TypeError(`${where}: store.${slot}.${method} must be a function or absent`);
|
|
732
787
|
}
|
|
733
|
-
return
|
|
788
|
+
return found !== undefined;
|
|
734
789
|
}
|
|
735
790
|
|
|
736
791
|
// src/auth/devices/token.ts
|
|
@@ -1008,6 +1063,13 @@ function sharedApi(context) {
|
|
|
1008
1063
|
types: [...config.types.keys()]
|
|
1009
1064
|
};
|
|
1010
1065
|
}
|
|
1066
|
+
// src/auth/counter-key.ts
|
|
1067
|
+
function counterKey(context, purpose) {
|
|
1068
|
+
const sealer = context.config.secondFactor?.sealer;
|
|
1069
|
+
return sealer === undefined ? Buffer.from(purpose) : derivedKey(sealer, purpose);
|
|
1070
|
+
}
|
|
1071
|
+
var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
|
|
1072
|
+
|
|
1011
1073
|
// src/auth/one-time/codes.ts
|
|
1012
1074
|
import { timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
1013
1075
|
|
|
@@ -1173,6 +1235,38 @@ async function insertLink(links, tokenHash) {
|
|
|
1173
1235
|
return inserted;
|
|
1174
1236
|
throw links.counting.vanished();
|
|
1175
1237
|
}
|
|
1238
|
+
// src/auth/mail-requests.ts
|
|
1239
|
+
var PURPOSE2 = "janus/mail/requests/v1";
|
|
1240
|
+
async function countMailByAddress(context, type, flow, email, where) {
|
|
1241
|
+
await countMail(context, type, flow, { address: normalizeEmail(email) }, where);
|
|
1242
|
+
}
|
|
1243
|
+
async function countMailByUser(context, type, flow, userId, where) {
|
|
1244
|
+
await countMail(context, type, flow, { userId }, where);
|
|
1245
|
+
}
|
|
1246
|
+
async function countMail(context, type, flow, counted, where) {
|
|
1247
|
+
const throttle = context.config.mailThrottle;
|
|
1248
|
+
if (throttle === null)
|
|
1249
|
+
return;
|
|
1250
|
+
const key = counterKey(context, PURPOSE2);
|
|
1251
|
+
const subject = "address" in counted ? keyed(key, [flow, "address", counted.address]) : keyed(key, [flow, "user", counted.userId]);
|
|
1252
|
+
const { attempts, endsAt } = await countInWindow(context, {
|
|
1253
|
+
windowMs: throttle.windowMs,
|
|
1254
|
+
spent: "carry",
|
|
1255
|
+
userId: subject,
|
|
1256
|
+
linkHash: (window, link) => hashSecret(keyed(key, [subject, String(window), String(link)])),
|
|
1257
|
+
vanished: () => new StoreFailure2(`${where}: the store dropped the requests it had just counted`, { slot: "tokens", operation: "countAttempt" })
|
|
1258
|
+
});
|
|
1259
|
+
if (attempts <= throttle.attempts)
|
|
1260
|
+
return;
|
|
1261
|
+
const now = context.clock.now().getTime();
|
|
1262
|
+
const byUser = "userId" in counted;
|
|
1263
|
+
throw new MailThrottledError2(`${where}: too many e-mails asked for this ${byUser ? "user" : "address"} — the last one sent still works; use it, or wait for the next window`, {
|
|
1264
|
+
userType: type.name,
|
|
1265
|
+
...byUser ? { userId: counted.userId } : {},
|
|
1266
|
+
retryAfter: Math.max(1, Math.ceil((endsAt - now) / 1000))
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1176
1270
|
// src/auth/password-written.ts
|
|
1177
1271
|
async function endWhatThePasswordOpened(context, userId) {
|
|
1178
1272
|
const now = context.clock.now();
|
|
@@ -1234,6 +1328,7 @@ function resetPasswordFlows(context, type, at) {
|
|
|
1234
1328
|
async request(email) {
|
|
1235
1329
|
const where = at("resetPassword.request");
|
|
1236
1330
|
passwordRule(type, where);
|
|
1331
|
+
await countMailByAddress(context, type, "resetPassword", String(email), where);
|
|
1237
1332
|
const record = await holderOfEmail(context, type, String(email));
|
|
1238
1333
|
if (record === null)
|
|
1239
1334
|
return null;
|
|
@@ -1278,6 +1373,7 @@ function verifyEmailFlows(context, type, at) {
|
|
|
1278
1373
|
async send(user) {
|
|
1279
1374
|
const where = at("verifyEmail.send");
|
|
1280
1375
|
const record = await getRecord(context, idOf(user), type.name, where);
|
|
1376
|
+
await countMailByUser(context, type, "verifyEmail", record.id, where);
|
|
1281
1377
|
return issueEmailToken(context, type, "verifyEmail", record, where);
|
|
1282
1378
|
},
|
|
1283
1379
|
async confirm(secret) {
|
|
@@ -1465,7 +1561,7 @@ async function finishEmailSignIn(context, type, finish, signIn) {
|
|
|
1465
1561
|
function magicLinkFlows(context, type, at, finish) {
|
|
1466
1562
|
return {
|
|
1467
1563
|
async request(email) {
|
|
1468
|
-
return requestLink(context, type, String(email));
|
|
1564
|
+
return requestLink(context, type, String(email), at("magicLink.request"));
|
|
1469
1565
|
},
|
|
1470
1566
|
async confirm(token, options) {
|
|
1471
1567
|
const where = at("magicLink.confirm");
|
|
@@ -1474,7 +1570,8 @@ function magicLinkFlows(context, type, at, finish) {
|
|
|
1474
1570
|
}
|
|
1475
1571
|
};
|
|
1476
1572
|
}
|
|
1477
|
-
async function requestLink(context, type, email) {
|
|
1573
|
+
async function requestLink(context, type, email, where) {
|
|
1574
|
+
await countMailByAddress(context, type, "magicLink", email, where);
|
|
1478
1575
|
const record = await signInHolder(context, type, email);
|
|
1479
1576
|
if (record === null)
|
|
1480
1577
|
return null;
|
|
@@ -1500,18 +1597,12 @@ async function confirmLink(context, type, finish, secret, where, device) {
|
|
|
1500
1597
|
}
|
|
1501
1598
|
|
|
1502
1599
|
// src/auth/sign-in-attempts.ts
|
|
1503
|
-
var
|
|
1504
|
-
var UNSEALED = Buffer.from(PURPOSE2);
|
|
1505
|
-
function counterKey(context) {
|
|
1506
|
-
const sealer = context.config.secondFactor?.sealer;
|
|
1507
|
-
return sealer === undefined ? UNSEALED : derivedKey(sealer, PURPOSE2);
|
|
1508
|
-
}
|
|
1509
|
-
var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
|
|
1600
|
+
var PURPOSE3 = "janus/sign-in/attempts/v1";
|
|
1510
1601
|
async function countLogin(context, type, login, where) {
|
|
1511
1602
|
const throttle = context.config.signInThrottle;
|
|
1512
1603
|
if (throttle === null)
|
|
1513
1604
|
return null;
|
|
1514
|
-
const key = counterKey(context);
|
|
1605
|
+
const key = counterKey(context, PURPOSE3);
|
|
1515
1606
|
const loginKey = keyed(key, [type.name, login]);
|
|
1516
1607
|
return countInWindow(context, {
|
|
1517
1608
|
windowMs: throttle.windowMs,
|
|
@@ -1634,7 +1725,7 @@ var RECOVERY_CODES = 10;
|
|
|
1634
1725
|
var ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
1635
1726
|
var LENGTH = 10;
|
|
1636
1727
|
var SHAPE = /^[0-9a-hjkmnp-tv-z]{10}$/;
|
|
1637
|
-
var
|
|
1728
|
+
var PURPOSE4 = "janus/second-factor/recovery-codes/v1";
|
|
1638
1729
|
function mintRecoveryCodes() {
|
|
1639
1730
|
return Array.from({ length: RECOVERY_CODES }, () => {
|
|
1640
1731
|
let code = "";
|
|
@@ -1648,7 +1739,7 @@ function readRecoveryCode(typed) {
|
|
|
1648
1739
|
const code = typed.toLowerCase().replace(/[\s-]/g, "").replace(/o/g, "0").replace(/[il]/g, "1");
|
|
1649
1740
|
return SHAPE.test(code) ? code : null;
|
|
1650
1741
|
}
|
|
1651
|
-
var macKey = (sealer, keyId) => derivedKey(sealer,
|
|
1742
|
+
var macKey = (sealer, keyId) => derivedKey(sealer, PURPOSE4, keyId);
|
|
1652
1743
|
var mac = (key, userId, code) => keyedHash(key, [userId, code]);
|
|
1653
1744
|
function hashRecoveryCode(sealer, userId, code) {
|
|
1654
1745
|
const read = readRecoveryCode(code) ?? code;
|
|
@@ -1759,7 +1850,7 @@ function accountOf(type, record, where) {
|
|
|
1759
1850
|
// src/auth/second-factor/app-code-attempts.ts
|
|
1760
1851
|
var APP_CODE_WINDOW_MS = 15 * 60000;
|
|
1761
1852
|
var WINDOW_MINUTES = APP_CODE_WINDOW_MS / 60000;
|
|
1762
|
-
var
|
|
1853
|
+
var PURPOSE5 = "janus/second-factor/regenerate-attempts/v1";
|
|
1763
1854
|
function linkHash(sealer, record, window, link) {
|
|
1764
1855
|
const parts = [
|
|
1765
1856
|
record.id,
|
|
@@ -1767,7 +1858,7 @@ function linkHash(sealer, record, window, link) {
|
|
|
1767
1858
|
String(record.secondFactor?.lastStep ?? ""),
|
|
1768
1859
|
String(link)
|
|
1769
1860
|
];
|
|
1770
|
-
const key = derivedKey(sealer,
|
|
1861
|
+
const key = derivedKey(sealer, PURPOSE5);
|
|
1771
1862
|
return hashSecret(keyedHash(key, parts).toString("base64url"));
|
|
1772
1863
|
}
|
|
1773
1864
|
async function countAppCodeAttempt(context, sealer, record, where) {
|
|
@@ -1883,14 +1974,15 @@ function secondFactorFlows(context, type, at) {
|
|
|
1883
1974
|
function signInCodeFlows(context, type, at, finish) {
|
|
1884
1975
|
return {
|
|
1885
1976
|
async request(email) {
|
|
1886
|
-
return requestCode(context, type, String(email));
|
|
1977
|
+
return requestCode(context, type, String(email), at("signInCode.request"));
|
|
1887
1978
|
},
|
|
1888
1979
|
async confirm(challenge, code, options) {
|
|
1889
1980
|
return confirmCode(context, type, finish, { challenge, code, options }, at);
|
|
1890
1981
|
}
|
|
1891
1982
|
};
|
|
1892
1983
|
}
|
|
1893
|
-
async function requestCode(context, type, email) {
|
|
1984
|
+
async function requestCode(context, type, email, where) {
|
|
1985
|
+
await countMailByAddress(context, type, "signInCode", email, where);
|
|
1894
1986
|
const record = await signInHolder(context, type, email);
|
|
1895
1987
|
if (record === null)
|
|
1896
1988
|
return null;
|
|
@@ -2028,6 +2120,7 @@ async function byEmail(context, type, record, where) {
|
|
|
2028
2120
|
operation: where
|
|
2029
2121
|
});
|
|
2030
2122
|
}
|
|
2123
|
+
await countMailByUser(context, type, "stepUp", record.id, where);
|
|
2031
2124
|
const { secret, code, expiresAt } = await issueCode(context, {
|
|
2032
2125
|
kind: "stepUp",
|
|
2033
2126
|
userId: record.id,
|
|
@@ -2230,6 +2323,48 @@ async function deleteUser(context, type, user) {
|
|
|
2230
2323
|
return deleted;
|
|
2231
2324
|
}
|
|
2232
2325
|
|
|
2326
|
+
// src/auth/users/find-many.ts
|
|
2327
|
+
var FIND_MANY_BATCH = 100;
|
|
2328
|
+
var FIND_MANY_CONCURRENCY = 10;
|
|
2329
|
+
async function findManyRecords(context, ids, type, where) {
|
|
2330
|
+
if (!Array.isArray(ids)) {
|
|
2331
|
+
throw new TypeError(`${where}: ids must be an array of user ids`);
|
|
2332
|
+
}
|
|
2333
|
+
const wanted = [
|
|
2334
|
+
...new Set(ids.filter((id) => typeof id === "string" && isId2(id)))
|
|
2335
|
+
];
|
|
2336
|
+
const found = new Map;
|
|
2337
|
+
for (let start = 0;start < wanted.length; start += FIND_MANY_BATCH) {
|
|
2338
|
+
const batch = wanted.slice(start, start + FIND_MANY_BATCH);
|
|
2339
|
+
for (const record of await readBatch(context, batch)) {
|
|
2340
|
+
if (record.type === type)
|
|
2341
|
+
found.set(record.id, record);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return wanted.flatMap((id) => {
|
|
2345
|
+
const record = found.get(id);
|
|
2346
|
+
return record === undefined ? [] : [record];
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
async function readBatch(context, ids) {
|
|
2350
|
+
const { users } = context.store;
|
|
2351
|
+
if (context.capabilities.findUsers && users.findUsers !== undefined) {
|
|
2352
|
+
const answer = await users.findUsers(ids);
|
|
2353
|
+
if (!Array.isArray(answer)) {
|
|
2354
|
+
throw new StoreFailure2("users.findUsers answered no list: an absence is a shorter list, so this store forgot to answer", { slot: "users", operation: "findUsers" });
|
|
2355
|
+
}
|
|
2356
|
+
return answer;
|
|
2357
|
+
}
|
|
2358
|
+
const records = [];
|
|
2359
|
+
for (let start = 0;start < ids.length; start += FIND_MANY_CONCURRENCY) {
|
|
2360
|
+
const answers = await Promise.all(ids.slice(start, start + FIND_MANY_CONCURRENCY).map((id) => users.findUser(id)));
|
|
2361
|
+
for (const record of answers)
|
|
2362
|
+
if (record !== null)
|
|
2363
|
+
records.push(record);
|
|
2364
|
+
}
|
|
2365
|
+
return records;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2233
2368
|
// src/auth/users/fields-patch.ts
|
|
2234
2369
|
var normalized = (email) => email === null ? null : normalizeEmail(email);
|
|
2235
2370
|
function emailChanged(type, before, after) {
|
|
@@ -2272,6 +2407,10 @@ function recordFlows(context, type, at) {
|
|
|
2272
2407
|
const record = await findRecord(context, id, type.name);
|
|
2273
2408
|
return record === null ? null : toUser(record);
|
|
2274
2409
|
},
|
|
2410
|
+
async findMany(ids) {
|
|
2411
|
+
const records = await findManyRecords(context, ids, type.name, at("findMany"));
|
|
2412
|
+
return records.map(toUser);
|
|
2413
|
+
},
|
|
2275
2414
|
async get(id) {
|
|
2276
2415
|
return toUser(await getRecord(context, id, type.name, at("get")));
|
|
2277
2416
|
},
|
|
@@ -2320,6 +2459,7 @@ function typeApi(context, type) {
|
|
|
2320
2459
|
function janus(config) {
|
|
2321
2460
|
const where = "janus";
|
|
2322
2461
|
const resolved = resolveConfig(config, where);
|
|
2462
|
+
warnThrottleWindow(resolved, where);
|
|
2323
2463
|
const capabilities = assertStores(config.store, where);
|
|
2324
2464
|
const hasher = config.hasher ?? null;
|
|
2325
2465
|
const verifiers = [
|
|
@@ -2363,6 +2503,7 @@ export {
|
|
|
2363
2503
|
InvalidCursorError2 as InvalidCursorError,
|
|
2364
2504
|
JanusError2 as JanusError,
|
|
2365
2505
|
MAX_PAGE_SIZE2 as MAX_PAGE_SIZE,
|
|
2506
|
+
MailThrottledError2 as MailThrottledError,
|
|
2366
2507
|
NotFoundError2 as NotFoundError,
|
|
2367
2508
|
PermissionDepthError2 as PermissionDepthError,
|
|
2368
2509
|
SecondFactorError2 as SecondFactorError,
|
|
@@ -2399,5 +2540,5 @@ export {
|
|
|
2399
2540
|
systemClock
|
|
2400
2541
|
};
|
|
2401
2542
|
|
|
2402
|
-
//# debugId=
|
|
2543
|
+
//# debugId=22ACDB89AAF4B56564756E2164756E21
|
|
2403
2544
|
//# sourceMappingURL=index.js.map
|