@nxgt/janus 0.23.1 → 0.25.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 +119 -21
- package/dist/auth/email-flows/prove-first.d.ts +23 -1
- package/dist/auth/email-flows/prove-first.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/outage.d.ts +17 -6
- package/dist/auth/outage.d.ts.map +1 -1
- package/dist/auth/port/assert-stores.d.ts.map +1 -1
- package/dist/auth/port/memory/tokens.d.ts.map +1 -1
- package/dist/auth/port/types/stores.d.ts +2 -0
- package/dist/auth/port/types/stores.d.ts.map +1 -1
- package/dist/auth/port/types/tokens.d.ts +12 -0
- package/dist/auth/port/types/tokens.d.ts.map +1 -1
- package/dist/auth/sessions/collect-expired.d.ts +14 -0
- package/dist/auth/sessions/collect-expired.d.ts.map +1 -0
- package/dist/auth/sessions/shared-api.d.ts +1 -1
- package/dist/auth/sessions/shared-api.d.ts.map +1 -1
- package/dist/auth/types/email-flows.d.ts +13 -2
- package/dist/auth/types/email-flows.d.ts.map +1 -1
- package/dist/auth/types/find-or-create.d.ts +59 -0
- package/dist/auth/types/find-or-create.d.ts.map +1 -0
- package/dist/auth/types/index.d.ts +1 -0
- package/dist/auth/types/index.d.ts.map +1 -1
- package/dist/auth/types/janus.d.ts +6 -4
- package/dist/auth/types/janus.d.ts.map +1 -1
- package/dist/auth/users/any-type-api.d.ts +2 -2
- package/dist/auth/users/any-type-api.d.ts.map +1 -1
- package/dist/auth/users/find-or-create.d.ts +12 -0
- package/dist/auth/users/find-or-create.d.ts.map +1 -0
- package/dist/auth/users/insert.d.ts +8 -3
- package/dist/auth/users/insert.d.ts.map +1 -1
- package/dist/auth/users/type-api.d.ts +1 -1
- package/dist/auth/users/type-api.d.ts.map +1 -1
- package/dist/chunks/{index-6bjfv4xv.js → index-q3w3vr69.js} +13 -3
- package/dist/chunks/{index-6bjfv4xv.js.map → index-q3w3vr69.js.map} +3 -3
- package/dist/chunks/{index-cntabs9d.js → index-rpp0bw6e.js} +2 -2
- package/dist/chunks/{index-kppkcbn5.js → index-spwh8b9z.js} +2 -5
- package/dist/chunks/{index-kppkcbn5.js.map → index-spwh8b9z.js.map} +2 -2
- package/dist/chunks/{index-4khsm7mw.js → index-y9b327j8.js} +18 -18
- package/dist/chunks/{index-4khsm7mw.js.map → index-y9b327j8.js.map} +5 -5
- package/dist/conformance/cases/outage.d.ts.map +1 -1
- package/dist/conformance/cases/tokens/deletion.d.ts.map +1 -1
- package/dist/conformance/describe.d.ts +1 -0
- package/dist/conformance/describe.d.ts.map +1 -1
- package/dist/conformance/index.js +39 -5
- package/dist/conformance/index.js.map +5 -5
- package/dist/conformance/types.d.ts +1 -1
- package/dist/conformance/types.d.ts.map +1 -1
- package/dist/index.js +167 -44
- package/dist/index.js.map +11 -9
- package/dist/permissions/engine.d.ts.map +1 -1
- package/dist/permissions/index.d.ts +1 -1
- package/dist/permissions/index.d.ts.map +1 -1
- package/dist/permissions/index.js +52 -19
- package/dist/permissions/index.js.map +6 -5
- package/dist/permissions/input.d.ts +13 -1
- package/dist/permissions/input.d.ts.map +1 -1
- package/dist/permissions/model/grant.d.ts +14 -1
- package/dist/permissions/model/grant.d.ts.map +1 -1
- package/dist/permissions/model/permissions.d.ts +13 -1
- package/dist/permissions/model/permissions.d.ts.map +1 -1
- package/dist/permissions/port/types.d.ts +8 -2
- package/dist/permissions/port/types.d.ts.map +1 -1
- package/dist/permissions/replace.d.ts +25 -0
- package/dist/permissions/replace.d.ts.map +1 -0
- package/docs/README.md +2 -2
- package/docs/guide/adapters.md +40 -3
- package/docs/guide/email-flows.md +35 -5
- package/docs/guide/errors.md +2 -2
- package/docs/guide/passwords.md +7 -10
- package/docs/guide/permissions.md +42 -1
- package/docs/guide/sessions.md +21 -4
- package/docs/guide/users.md +44 -0
- package/docs/guide/vocabulary.md +1 -1
- package/docs/roadmap.md +33 -38
- package/docs/troubleshooting.md +50 -21
- package/package.json +1 -1
- /package/dist/chunks/{index-cntabs9d.js.map → index-rpp0bw6e.js.map} +0 -0
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-y9b327j8.js";
|
|
11
11
|
import {
|
|
12
12
|
mintId2,
|
|
13
13
|
isId2,
|
|
14
14
|
mintedAt2,
|
|
15
15
|
createMemoryStores2
|
|
16
|
-
} from "./chunks/index-
|
|
16
|
+
} from "./chunks/index-q3w3vr69.js";
|
|
17
17
|
import {
|
|
18
18
|
JanusError2,
|
|
19
19
|
UserInvalidError2,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
formatTuple2,
|
|
40
40
|
parseTuple2,
|
|
41
41
|
parseSubject2
|
|
42
|
-
} from "./chunks/index-
|
|
42
|
+
} from "./chunks/index-spwh8b9z.js";
|
|
43
43
|
|
|
44
44
|
// src/auth/hashers.ts
|
|
45
45
|
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
@@ -550,6 +550,15 @@ async function unlessVersionConflict(write) {
|
|
|
550
550
|
throw error;
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
|
+
async function unlessLoginTaken(write) {
|
|
554
|
+
try {
|
|
555
|
+
return await write;
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (error instanceof StoreConflict2 && error.on === "login")
|
|
558
|
+
return error;
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
553
562
|
|
|
554
563
|
// src/auth/context/password.ts
|
|
555
564
|
function passwordRule(type, where) {
|
|
@@ -785,6 +794,7 @@ function assertStores(stores, where) {
|
|
|
785
794
|
const slots = stores;
|
|
786
795
|
return {
|
|
787
796
|
collectExpired: optional(slots, "sessions", "deleteExpiredSessions", where),
|
|
797
|
+
collectExpiredTokens: optional(slots, "tokens", "deleteExpiredTokens", where),
|
|
788
798
|
findUsers: optional(slots, "users", "findUsers", where)
|
|
789
799
|
};
|
|
790
800
|
}
|
|
@@ -1014,6 +1024,19 @@ function renewalDue(type, session, now) {
|
|
|
1014
1024
|
return type.renewAfterMs !== null && type.lifespanMs - (session.expiresAt.getTime() - now) >= type.renewAfterMs;
|
|
1015
1025
|
}
|
|
1016
1026
|
|
|
1027
|
+
// src/auth/sessions/collect-expired.ts
|
|
1028
|
+
async function collectExpired(context) {
|
|
1029
|
+
const { store, clock, capabilities } = context;
|
|
1030
|
+
const sessions = capabilities.collectExpired ? store.sessions.deleteExpiredSessions : undefined;
|
|
1031
|
+
const tokens = capabilities.collectExpiredTokens ? store.tokens.deleteExpiredTokens : undefined;
|
|
1032
|
+
if (sessions === undefined && tokens === undefined) {
|
|
1033
|
+
throw new UnsupportedError2("collectExpired: store.sessions does not implement deleteExpiredSessions, nor store.tokens deleteExpiredTokens — their stores expire records on their own, or implement the methods", { slot: "sessions", operation: "deleteExpiredSessions" });
|
|
1034
|
+
}
|
|
1035
|
+
const before = clock.now();
|
|
1036
|
+
const collected = sessions === undefined ? 0 : await sessions(before);
|
|
1037
|
+
return collected + (tokens === undefined ? 0 : await tokens(before));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1017
1040
|
// src/auth/sessions/cookie.ts
|
|
1018
1041
|
function cookieOf(context) {
|
|
1019
1042
|
const { name, domain, path, sameSite, secure } = context.config.cookie;
|
|
@@ -1062,11 +1085,7 @@ function sharedApi(context) {
|
|
|
1062
1085
|
},
|
|
1063
1086
|
cookie: cookieOf(context),
|
|
1064
1087
|
async collectExpired() {
|
|
1065
|
-
|
|
1066
|
-
if (!context.capabilities.collectExpired || collect === undefined) {
|
|
1067
|
-
throw new UnsupportedError2("collectExpired: store.sessions does not implement deleteExpiredSessions — its store expires sessions on its own, or implement the method", { slot: "sessions", operation: "deleteExpiredSessions" });
|
|
1068
|
-
}
|
|
1069
|
-
return collect(clock.now());
|
|
1088
|
+
return collectExpired(context);
|
|
1070
1089
|
},
|
|
1071
1090
|
types: [...config.types.keys()]
|
|
1072
1091
|
};
|
|
@@ -1648,19 +1667,69 @@ async function endWhatTheSquatterOpened(context, userId) {
|
|
|
1648
1667
|
function proveFirstFlow(context, type, at) {
|
|
1649
1668
|
return async (user) => {
|
|
1650
1669
|
const where = at("verifyEmail.proveFirst");
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1670
|
+
const id = refId(user);
|
|
1671
|
+
const claimed = claimedEmail(type, user);
|
|
1672
|
+
const first = await readActive(context, type, id, where);
|
|
1673
|
+
assertClaimed(type, claimed, first, user, where);
|
|
1674
|
+
const answer = await unlessVersionConflict(proveOrAnswer(context, type, first, where));
|
|
1675
|
+
if (answer !== null)
|
|
1676
|
+
return answer;
|
|
1677
|
+
const again = await readActive(context, type, id, where);
|
|
1678
|
+
assertClaimed(type, claimed, again, user, where);
|
|
1679
|
+
if (!sameEmail(type, first, again)) {
|
|
1680
|
+
throw new StoreConflict2("version", `${where}: the e-mail changed since it was read`, {
|
|
1681
|
+
userId: first.id,
|
|
1682
|
+
expectedVersion: first.version,
|
|
1683
|
+
actualVersion: again.version,
|
|
1684
|
+
operation: where
|
|
1656
1685
|
});
|
|
1657
1686
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1687
|
+
return proveOrAnswer(context, type, again, where);
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
async function readActive(context, type, id, where) {
|
|
1691
|
+
const record = await getRecord(context, id, type.name, where);
|
|
1692
|
+
if (!record.active) {
|
|
1693
|
+
throw new UserInactiveError2(`${where}: the user is inactive`, {
|
|
1694
|
+
userId: record.id,
|
|
1695
|
+
userType: type.name
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
return record;
|
|
1699
|
+
}
|
|
1700
|
+
async function proveOrAnswer(context, type, record, where) {
|
|
1701
|
+
if (record.emailVerifiedAt !== null) {
|
|
1702
|
+
return { user: toUser(record), proved: false };
|
|
1703
|
+
}
|
|
1704
|
+
const proved = await proveFirstEmail(context, type, record, where);
|
|
1705
|
+
return { user: toUser(proved), proved: true };
|
|
1706
|
+
}
|
|
1707
|
+
function sameEmail(type, a, b) {
|
|
1708
|
+
const held = (record) => {
|
|
1709
|
+
const email = emailOf(type, record.fields);
|
|
1710
|
+
return email === null ? null : normalizeEmail(email);
|
|
1663
1711
|
};
|
|
1712
|
+
return held(a) === held(b);
|
|
1713
|
+
}
|
|
1714
|
+
function claimedEmail(type, user) {
|
|
1715
|
+
if (typeof user !== "object" || user === null)
|
|
1716
|
+
return null;
|
|
1717
|
+
const value = user[type.email];
|
|
1718
|
+
return typeof value === "string" ? normalizeEmail(value) : null;
|
|
1719
|
+
}
|
|
1720
|
+
function assertClaimed(type, claimed, record, user, where) {
|
|
1721
|
+
if (claimed === null)
|
|
1722
|
+
return;
|
|
1723
|
+
const email = emailOf(type, record.fields);
|
|
1724
|
+
if (email !== null && normalizeEmail(email) === claimed)
|
|
1725
|
+
return;
|
|
1726
|
+
const held = user.version;
|
|
1727
|
+
throw new StoreConflict2("version", `${where}: the e-mail changed since the user was read`, {
|
|
1728
|
+
userId: record.id,
|
|
1729
|
+
...typeof held === "number" ? { expectedVersion: held } : {},
|
|
1730
|
+
actualVersion: record.version,
|
|
1731
|
+
operation: where
|
|
1732
|
+
});
|
|
1664
1733
|
}
|
|
1665
1734
|
|
|
1666
1735
|
// src/auth/email-flows/verify-email.ts
|
|
@@ -2387,36 +2456,13 @@ function findByEmailFlows(context, type) {
|
|
|
2387
2456
|
// src/auth/users/by-login.ts
|
|
2388
2457
|
var byLogin = async (context, type, login) => isStorable(login) ? context.store.users.findUserByLogin(type.name, login) : null;
|
|
2389
2458
|
|
|
2390
|
-
// src/auth/users/change-password.ts
|
|
2391
|
-
async function changePassword(context, type, user, change, options, where) {
|
|
2392
|
-
passwordRule(type, where);
|
|
2393
|
-
checkPassword(type, change?.next, where);
|
|
2394
|
-
const hasher = requireHasher(context, where);
|
|
2395
|
-
const written = await writeUser(context, user, type, options, where, async (record, now) => {
|
|
2396
|
-
if (record.password === null || !await passwordMatches(context, record, change.current, where)) {
|
|
2397
|
-
throw new CredentialError2("CREDENTIALS_INVALID", `${where}: the current password does not match`, {
|
|
2398
|
-
reason: record.password === null ? "noPassword" : "wrongPassword",
|
|
2399
|
-
userId: record.id,
|
|
2400
|
-
userType: type.name
|
|
2401
|
-
});
|
|
2402
|
-
}
|
|
2403
|
-
return {
|
|
2404
|
-
password: {
|
|
2405
|
-
hash: await hasher.hash(change.next),
|
|
2406
|
-
updatedAt: now
|
|
2407
|
-
}
|
|
2408
|
-
};
|
|
2409
|
-
});
|
|
2410
|
-
await passwordChanged(context, written);
|
|
2411
|
-
return written;
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
2459
|
// src/auth/users/insert.ts
|
|
2415
|
-
async function insert(context, type, input, where) {
|
|
2460
|
+
async function insert(context, type, input, where, accept) {
|
|
2416
2461
|
const { store, clock } = context;
|
|
2417
2462
|
const { password, active, ...rest } = input ?? {};
|
|
2418
2463
|
const now = clock.now();
|
|
2419
2464
|
const fields = await validateFields(type, rest, where);
|
|
2465
|
+
accept?.(fields);
|
|
2420
2466
|
let hash = null;
|
|
2421
2467
|
if (password !== undefined) {
|
|
2422
2468
|
checkPassword(type, password, where);
|
|
@@ -2443,6 +2489,82 @@ async function insert(context, type, input, where) {
|
|
|
2443
2489
|
return inserted;
|
|
2444
2490
|
}
|
|
2445
2491
|
|
|
2492
|
+
// src/auth/users/find-or-create.ts
|
|
2493
|
+
async function holder(context, type, key) {
|
|
2494
|
+
if (type.password !== null) {
|
|
2495
|
+
return byLogin(context, type, type.password.normalize(key));
|
|
2496
|
+
}
|
|
2497
|
+
return key.trim() === "" ? null : holderOfEmail(context, type, key);
|
|
2498
|
+
}
|
|
2499
|
+
function refusal2(type, where, field) {
|
|
2500
|
+
return new UserInvalidError2(`${where}: the fields do not match the ${type.name} schema (1 issue, at ${field})`, {
|
|
2501
|
+
issues: [
|
|
2502
|
+
{ path: [field], message: "is not the key findOrCreate was given" }
|
|
2503
|
+
],
|
|
2504
|
+
userType: type.name
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
function mustNameKey(type, key, where) {
|
|
2508
|
+
return (fields) => {
|
|
2509
|
+
const field = type.password === null ? type.email : type.password.login;
|
|
2510
|
+
const value = fields[field];
|
|
2511
|
+
const named = typeof value !== "string" ? null : type.password === null ? normalizeEmail(value) : type.password.normalize(value);
|
|
2512
|
+
const wanted = type.password === null ? normalizeEmail(key) : type.password.normalize(key);
|
|
2513
|
+
if (named === null || named !== wanted) {
|
|
2514
|
+
throw refusal2(type, where, field);
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
function findOrCreateFlows(context, type, at) {
|
|
2519
|
+
return {
|
|
2520
|
+
async findOrCreate(key, input) {
|
|
2521
|
+
const where = at("findOrCreate");
|
|
2522
|
+
if (typeof key !== "string") {
|
|
2523
|
+
throw new UserInvalidError2(`${where}: the key must be a string`, {
|
|
2524
|
+
issues: [{ path: [], message: "is not a string" }],
|
|
2525
|
+
userType: type.name
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
const found = await holder(context, type, key);
|
|
2529
|
+
if (found !== null)
|
|
2530
|
+
return { user: toUser(found), created: false };
|
|
2531
|
+
const record = await input();
|
|
2532
|
+
const written = await unlessLoginTaken(insert(context, type, record, where, mustNameKey(type, key, where)));
|
|
2533
|
+
if (!(written instanceof StoreConflict2)) {
|
|
2534
|
+
return { user: toUser(written), created: true };
|
|
2535
|
+
}
|
|
2536
|
+
const winner = await holder(context, type, key);
|
|
2537
|
+
if (winner === null)
|
|
2538
|
+
throw written;
|
|
2539
|
+
return { user: toUser(winner), created: false };
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// src/auth/users/change-password.ts
|
|
2545
|
+
async function changePassword(context, type, user, change, options, where) {
|
|
2546
|
+
passwordRule(type, where);
|
|
2547
|
+
checkPassword(type, change?.next, where);
|
|
2548
|
+
const hasher = requireHasher(context, where);
|
|
2549
|
+
const written = await writeUser(context, user, type, options, where, async (record, now) => {
|
|
2550
|
+
if (record.password === null || !await passwordMatches(context, record, change.current, where)) {
|
|
2551
|
+
throw new CredentialError2("CREDENTIALS_INVALID", `${where}: the current password does not match`, {
|
|
2552
|
+
reason: record.password === null ? "noPassword" : "wrongPassword",
|
|
2553
|
+
userId: record.id,
|
|
2554
|
+
userType: type.name
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
return {
|
|
2558
|
+
password: {
|
|
2559
|
+
hash: await hasher.hash(change.next),
|
|
2560
|
+
updatedAt: now
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
});
|
|
2564
|
+
await passwordChanged(context, written);
|
|
2565
|
+
return written;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2446
2568
|
// src/auth/users/sign-in.ts
|
|
2447
2569
|
async function signIn(context, type, finish, input, call) {
|
|
2448
2570
|
const { where, device } = call;
|
|
@@ -2663,6 +2785,7 @@ function typeApi(context, type) {
|
|
|
2663
2785
|
magicLink: magicLinkFlows(context, type, at, secondFactor.finish),
|
|
2664
2786
|
stepUp: stepUpFlows(context, type, at),
|
|
2665
2787
|
...findByEmailFlows(context, type),
|
|
2788
|
+
...findOrCreateFlows(context, type, at),
|
|
2666
2789
|
...emailFlows(context, type, at)
|
|
2667
2790
|
};
|
|
2668
2791
|
}
|
|
@@ -2751,5 +2874,5 @@ export {
|
|
|
2751
2874
|
systemClock
|
|
2752
2875
|
};
|
|
2753
2876
|
|
|
2754
|
-
//# debugId=
|
|
2877
|
+
//# debugId=D10ED7160A09120864756E2164756E21
|
|
2755
2878
|
//# sourceMappingURL=index.js.map
|