@nxgt/janus 0.24.0 → 0.26.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 +39 -12
- package/dist/auth/config/reserved.d.ts +1 -1
- package/dist/auth/config/reserved.d.ts.map +1 -1
- package/dist/auth/email-flows/any-type-change-email.d.ts +18 -0
- package/dist/auth/email-flows/any-type-change-email.d.ts.map +1 -0
- package/dist/auth/email-flows/change-email-token.d.ts +6 -1
- package/dist/auth/email-flows/change-email-token.d.ts.map +1 -1
- package/dist/auth/email-flows/change-email.d.ts +6 -0
- package/dist/auth/email-flows/change-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/outage.d.ts +2 -2
- 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/change-email.d.ts +31 -0
- package/dist/auth/types/change-email.d.ts.map +1 -1
- package/dist/auth/types/index.d.ts +1 -1
- package/dist/auth/types/index.d.ts.map +1 -1
- package/dist/auth/types/janus.d.ts +13 -5
- package/dist/auth/types/janus.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 +478 -453
- package/dist/index.js.map +21 -19
- package/dist/permissions/index.js +3 -3
- package/docs/guide/adapters.md +40 -3
- package/docs/guide/email-flows.md +16 -2
- package/docs/guide/errors.md +1 -1
- package/docs/guide/passwords.md +7 -10
- package/docs/guide/sessions.md +15 -4
- package/docs/guide/users.md +1 -1
- package/docs/roadmap.md +14 -20
- package/docs/troubleshooting.md +12 -10
- 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";
|
|
@@ -155,7 +155,8 @@ var RESERVED_TYPES = [
|
|
|
155
155
|
"getUser",
|
|
156
156
|
"cookie",
|
|
157
157
|
"collectExpired",
|
|
158
|
-
"types"
|
|
158
|
+
"types",
|
|
159
|
+
"changeEmail"
|
|
159
160
|
];
|
|
160
161
|
// src/time/duration.ts
|
|
161
162
|
var UNITS = {
|
|
@@ -749,65 +750,6 @@ async function emit(context, type, user, occurredAt, extras = {}) {
|
|
|
749
750
|
}
|
|
750
751
|
}
|
|
751
752
|
|
|
752
|
-
// src/auth/port/assert-stores.ts
|
|
753
|
-
var REQUIRED = {
|
|
754
|
-
users: [
|
|
755
|
-
"insertUser",
|
|
756
|
-
"findUser",
|
|
757
|
-
"findUserByLogin",
|
|
758
|
-
"listUsers",
|
|
759
|
-
"updateUser",
|
|
760
|
-
"deleteUser"
|
|
761
|
-
],
|
|
762
|
-
sessions: [
|
|
763
|
-
"insertSession",
|
|
764
|
-
"findSessionByTokenHash",
|
|
765
|
-
"extendSession",
|
|
766
|
-
"reauthenticateSession",
|
|
767
|
-
"revokeSession",
|
|
768
|
-
"revokeUserSessions",
|
|
769
|
-
"deleteUserSessions"
|
|
770
|
-
],
|
|
771
|
-
tokens: [
|
|
772
|
-
"insertToken",
|
|
773
|
-
"consumeToken",
|
|
774
|
-
"countAttempt",
|
|
775
|
-
"spendUserTokens",
|
|
776
|
-
"deleteUserTokens"
|
|
777
|
-
]
|
|
778
|
-
};
|
|
779
|
-
function assertStores(stores, where) {
|
|
780
|
-
if (typeof stores !== "object" || stores === null) {
|
|
781
|
-
throw new TypeError(`${where}: store must be an object with users, sessions and tokens`);
|
|
782
|
-
}
|
|
783
|
-
for (const slot of Object.keys(REQUIRED)) {
|
|
784
|
-
const store = stores[slot];
|
|
785
|
-
if (typeof store !== "object" || store === null) {
|
|
786
|
-
throw new TypeError(`${where}: store.${slot} is missing`);
|
|
787
|
-
}
|
|
788
|
-
for (const method of REQUIRED[slot]) {
|
|
789
|
-
if (typeof store[method] !== "function") {
|
|
790
|
-
throw new TypeError(`${where}: store.${slot} has no method ${method}, which the port requires`);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
const slots = stores;
|
|
795
|
-
return {
|
|
796
|
-
collectExpired: optional(slots, "sessions", "deleteExpiredSessions", where),
|
|
797
|
-
findUsers: optional(slots, "users", "findUsers", where)
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
function optional(stores, slot, method, where) {
|
|
801
|
-
const found = stores[slot][method];
|
|
802
|
-
if (found !== undefined && typeof found !== "function") {
|
|
803
|
-
throw new TypeError(`${where}: store.${slot}.${method} must be a function or absent`);
|
|
804
|
-
}
|
|
805
|
-
return found !== undefined;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// src/auth/devices/token.ts
|
|
809
|
-
import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
810
|
-
|
|
811
753
|
// src/auth/derived-keys.ts
|
|
812
754
|
import { createHmac, hkdfSync } from "node:crypto";
|
|
813
755
|
var derived = new WeakMap;
|
|
@@ -832,308 +774,69 @@ function keyedHash(key, parts) {
|
|
|
832
774
|
return createHmac("sha256", key).update(parts.join("\x00")).digest();
|
|
833
775
|
}
|
|
834
776
|
|
|
835
|
-
// src/auth/
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
return keyedHash(key, [userId, deviceId]);
|
|
840
|
-
}
|
|
841
|
-
function signed(sealer, userId, deviceId) {
|
|
842
|
-
const key = derivedKey(sealer, PURPOSE);
|
|
843
|
-
const mac = macOf(key, userId, deviceId).toString("base64url");
|
|
844
|
-
return `d1.${sealer.sealWith}.${deviceId}.${mac}`;
|
|
845
|
-
}
|
|
846
|
-
function mintDeviceToken(sealer, userId) {
|
|
847
|
-
return signed(sealer, userId, randomBytes3(16).toString("base64url"));
|
|
848
|
-
}
|
|
849
|
-
function knownDeviceToken(sealer, userId, presented) {
|
|
850
|
-
const [, keyId, deviceId, mac] = TOKEN.exec(presented) ?? [];
|
|
851
|
-
if (keyId === undefined || deviceId === undefined || mac === undefined) {
|
|
852
|
-
return null;
|
|
853
|
-
}
|
|
854
|
-
const key = derivedKey(sealer, PURPOSE, keyId);
|
|
855
|
-
if (key === undefined)
|
|
856
|
-
return null;
|
|
857
|
-
const expected = macOf(key, userId, deviceId);
|
|
858
|
-
const given = Buffer.from(mac, "base64url");
|
|
859
|
-
if (given.length !== expected.length || !timingSafeEqual2(given, expected)) {
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
return keyId === sealer.sealWith ? presented : signed(sealer, userId, deviceId);
|
|
777
|
+
// src/auth/counter-key.ts
|
|
778
|
+
function counterKey(context, purpose) {
|
|
779
|
+
const sealer = context.config.secondFactor?.sealer;
|
|
780
|
+
return sealer === undefined ? Buffer.from(purpose) : derivedKey(sealer, purpose);
|
|
863
781
|
}
|
|
782
|
+
var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
|
|
783
|
+
|
|
784
|
+
// src/auth/one-time/codes.ts
|
|
785
|
+
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
864
786
|
|
|
865
|
-
// src/auth/devices/hint.ts
|
|
866
|
-
var UNTRACKED = Object.freeze({
|
|
867
|
-
newDevice: false,
|
|
868
|
-
deviceToken: null
|
|
869
|
-
});
|
|
870
|
-
function deviceHint(context, options, where) {
|
|
871
|
-
if (options !== undefined && (typeof options !== "object" || options === null)) {
|
|
872
|
-
throw new TypeError(`${where}: options must be { device }, or left out — the token goes under device`);
|
|
873
|
-
}
|
|
874
|
-
const device = options?.device;
|
|
875
|
-
if (device === undefined)
|
|
876
|
-
return;
|
|
877
|
-
if (device !== null && typeof device !== "string") {
|
|
878
|
-
throw new TypeError(`${where}: options.device must be the device token the client holds, or null when it holds none`);
|
|
879
|
-
}
|
|
880
|
-
if (context.config.devices === null) {
|
|
881
|
-
throw new TypeError(`${where}: a device was given, but janus() has no devices — pass devices: { keys }`);
|
|
882
|
-
}
|
|
883
|
-
return device;
|
|
884
|
-
}
|
|
885
|
-
function deviceOutcome(context, userId, hint, signUp) {
|
|
886
|
-
const sealer = context.config.devices?.sealer;
|
|
887
|
-
if (hint === undefined || sealer === undefined)
|
|
888
|
-
return UNTRACKED;
|
|
889
|
-
const known = hint === null || signUp ? null : knownDeviceToken(sealer, userId, hint);
|
|
890
|
-
if (known !== null)
|
|
891
|
-
return { newDevice: false, deviceToken: known };
|
|
892
|
-
return {
|
|
893
|
-
newDevice: !signUp,
|
|
894
|
-
deviceToken: mintDeviceToken(sealer, userId)
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
// src/auth/devices/report.ts
|
|
898
|
-
async function reportNewDevice(context, result) {
|
|
899
|
-
if (result.status !== "signedIn" || !result.newDevice)
|
|
900
|
-
return;
|
|
901
|
-
await emit(context, "user.newDeviceSignedIn", result.user, result.session.createdAt, { sessionId: result.session.id });
|
|
902
|
-
}
|
|
903
787
|
// src/auth/secrets.ts
|
|
904
|
-
import { createHash, randomBytes as
|
|
788
|
+
import { createHash, randomBytes as randomBytes3 } from "node:crypto";
|
|
905
789
|
function mintSecret() {
|
|
906
|
-
return
|
|
790
|
+
return randomBytes3(32).toString("base64url");
|
|
907
791
|
}
|
|
908
792
|
function hashSecret(secret) {
|
|
909
793
|
return createHash("sha256").update(secret).digest("hex");
|
|
910
794
|
}
|
|
911
795
|
|
|
912
|
-
// src/auth/
|
|
913
|
-
function
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const now = context.clock.now();
|
|
919
|
-
const token = mintSecret();
|
|
920
|
-
const record = {
|
|
921
|
-
id: mintId2(now.getTime()),
|
|
922
|
-
tokenHash: hashSecret(token),
|
|
923
|
-
userId: user.id,
|
|
924
|
-
authenticatedAt: now,
|
|
925
|
-
expiresAt: new Date(now.getTime() + type.lifespanMs),
|
|
926
|
-
revokedAt: null,
|
|
927
|
-
createdAt: now
|
|
928
|
-
};
|
|
929
|
-
await context.store.sessions.insertSession(record);
|
|
930
|
-
return {
|
|
931
|
-
status: "signedIn",
|
|
932
|
-
user: toUser(user),
|
|
933
|
-
session: toSession(record),
|
|
934
|
-
token,
|
|
935
|
-
...deviceOutcome(context, user.id, device, signUp)
|
|
936
|
-
};
|
|
796
|
+
// src/auth/one-time/refusals.ts
|
|
797
|
+
function refuseStale(type, user, token, where, noun) {
|
|
798
|
+
const email = emailOf(type, user.fields);
|
|
799
|
+
if (email === null || normalizeEmail(email) !== normalizeEmail(token.address)) {
|
|
800
|
+
throw new TokenError2("TOKEN_STALE", `${where}: the ${noun} was sent to an e-mail the user no longer has`, { operation: where, userId: user.id, userType: type.name });
|
|
801
|
+
}
|
|
937
802
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
return headers.get(name);
|
|
944
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
945
|
-
if (key.toLowerCase() !== name || value === undefined)
|
|
946
|
-
continue;
|
|
947
|
-
return typeof value === "string" ? value : value.join(", ");
|
|
948
|
-
}
|
|
949
|
-
return null;
|
|
950
|
-
};
|
|
951
|
-
const authorization = read("authorization");
|
|
952
|
-
if (authorization !== null) {
|
|
953
|
-
const match = /^bearer(?:\s+(.*))?$/i.exec(authorization.trim());
|
|
954
|
-
if (match)
|
|
955
|
-
return match[1]?.trim() ?? "";
|
|
803
|
+
function refuseUnusable(token, now, where, noun) {
|
|
804
|
+
if (token === null) {
|
|
805
|
+
throw new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
|
|
806
|
+
operation: where
|
|
807
|
+
});
|
|
956
808
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (at !== -1 && pair.slice(0, at).trim() === cookieName) {
|
|
965
|
-
return pair.slice(at + 1).trim();
|
|
966
|
-
}
|
|
967
|
-
}
|
|
809
|
+
if (token.spentAt !== null) {
|
|
810
|
+
throw new TokenError2("TOKEN_SPENT", `${where}: the ${noun} was already used`, { operation: where });
|
|
811
|
+
}
|
|
812
|
+
if (token.expiresAt.getTime() <= now.getTime()) {
|
|
813
|
+
throw new TokenError2("TOKEN_EXPIRED", `${where}: the ${noun} has expired`, {
|
|
814
|
+
operation: where
|
|
815
|
+
});
|
|
968
816
|
}
|
|
969
|
-
return null;
|
|
970
817
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
818
|
+
var unknownOneTime = (where, noun) => new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
|
|
819
|
+
operation: where
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// src/auth/one-time/spend.ts
|
|
823
|
+
async function spendOneTime(context, secret, kind, where, noun) {
|
|
824
|
+
const now = context.clock.now();
|
|
825
|
+
const token = await context.store.tokens.consumeToken(hashSecret(secret), kind, now);
|
|
826
|
+
refuseUnusable(token, now, where, noun);
|
|
827
|
+
return token;
|
|
980
828
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const token = presentedToken(request, context.config.cookie.name);
|
|
984
|
-
if (token === null)
|
|
985
|
-
return null;
|
|
986
|
-
const session = await context.store.sessions.findSessionByTokenHash(hashSecret(token));
|
|
987
|
-
const now = context.clock.now().getTime();
|
|
988
|
-
return session === null || session.revokedAt !== null || session.expiresAt.getTime() <= now ? null : { token, session };
|
|
829
|
+
async function burnOneTime(context, secret, kind) {
|
|
830
|
+
await context.store.tokens.consumeToken(hashSecret(secret), kind, context.clock.now());
|
|
989
831
|
}
|
|
990
832
|
|
|
991
|
-
// src/auth/
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const now = clock.now().getTime();
|
|
999
|
-
const user = await store.users.findUser(session.userId);
|
|
1000
|
-
if (user === null || !user.active || options?.type !== undefined && user.type !== options.type) {
|
|
1001
|
-
return null;
|
|
1002
|
-
}
|
|
1003
|
-
const type = config.types.get(user.type);
|
|
1004
|
-
if (type === undefined || !renewalDue(type, session, now)) {
|
|
1005
|
-
return {
|
|
1006
|
-
user: toUser(user),
|
|
1007
|
-
session: toSession(session),
|
|
1008
|
-
token,
|
|
1009
|
-
renewed: false
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
const extended = await store.sessions.extendSession(session.id, new Date(now + type.lifespanMs));
|
|
1013
|
-
if (extended === null)
|
|
1014
|
-
return null;
|
|
1015
|
-
return {
|
|
1016
|
-
user: toUser(user),
|
|
1017
|
-
session: toSession(extended),
|
|
1018
|
-
token,
|
|
1019
|
-
renewed: true
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
function renewalDue(type, session, now) {
|
|
1023
|
-
return type.renewAfterMs !== null && type.lifespanMs - (session.expiresAt.getTime() - now) >= type.renewAfterMs;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// src/auth/sessions/cookie.ts
|
|
1027
|
-
function cookieOf(context) {
|
|
1028
|
-
const { name, domain, path, sameSite, secure } = context.config.cookie;
|
|
1029
|
-
const attributes = [
|
|
1030
|
-
`Path=${path}`,
|
|
1031
|
-
...domain === null ? [] : [`Domain=${domain}`],
|
|
1032
|
-
"HttpOnly",
|
|
1033
|
-
`SameSite=${sameSite[0]?.toUpperCase()}${sameSite.slice(1)}`,
|
|
1034
|
-
...secure ? ["Secure"] : []
|
|
1035
|
-
].join("; ");
|
|
1036
|
-
return {
|
|
1037
|
-
name,
|
|
1038
|
-
serialize: (token, session) => `${name}=${token}; Expires=${session.expiresAt.toUTCString()}; ${attributes}`,
|
|
1039
|
-
clear: () => `${name}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; ${attributes}`
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// src/auth/sessions/shared-api.ts
|
|
1044
|
-
function sharedApi(context) {
|
|
1045
|
-
const { store, clock, config } = context;
|
|
1046
|
-
return {
|
|
1047
|
-
async authenticate(request, options) {
|
|
1048
|
-
return authenticate(context, request, options);
|
|
1049
|
-
},
|
|
1050
|
-
async signOut(request) {
|
|
1051
|
-
const token = presentedToken(request, config.cookie.name);
|
|
1052
|
-
if (token === null)
|
|
1053
|
-
return false;
|
|
1054
|
-
const session = await store.sessions.findSessionByTokenHash(hashSecret(token));
|
|
1055
|
-
if (session === null)
|
|
1056
|
-
return false;
|
|
1057
|
-
return store.sessions.revokeSession(session.id, clock.now());
|
|
1058
|
-
},
|
|
1059
|
-
async signOutEverywhere(user, options) {
|
|
1060
|
-
const id = idOf(user);
|
|
1061
|
-
if (!isId2(id))
|
|
1062
|
-
return 0;
|
|
1063
|
-
return options?.except === undefined || !isId2(options.except) ? store.sessions.revokeUserSessions(id, clock.now()) : store.sessions.revokeUserSessions(id, clock.now(), options.except);
|
|
1064
|
-
},
|
|
1065
|
-
async findUser(id) {
|
|
1066
|
-
const record = await findRecord(context, id, null);
|
|
1067
|
-
return record === null ? null : toUser(record);
|
|
1068
|
-
},
|
|
1069
|
-
async getUser(id) {
|
|
1070
|
-
return toUser(await getRecord(context, id, null, "getUser"));
|
|
1071
|
-
},
|
|
1072
|
-
cookie: cookieOf(context),
|
|
1073
|
-
async collectExpired() {
|
|
1074
|
-
const collect = store.sessions.deleteExpiredSessions;
|
|
1075
|
-
if (!context.capabilities.collectExpired || collect === undefined) {
|
|
1076
|
-
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" });
|
|
1077
|
-
}
|
|
1078
|
-
return collect(clock.now());
|
|
1079
|
-
},
|
|
1080
|
-
types: [...config.types.keys()]
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
// src/auth/counter-key.ts
|
|
1084
|
-
function counterKey(context, purpose) {
|
|
1085
|
-
const sealer = context.config.secondFactor?.sealer;
|
|
1086
|
-
return sealer === undefined ? Buffer.from(purpose) : derivedKey(sealer, purpose);
|
|
1087
|
-
}
|
|
1088
|
-
var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
|
|
1089
|
-
|
|
1090
|
-
// src/auth/one-time/codes.ts
|
|
1091
|
-
import { timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
1092
|
-
|
|
1093
|
-
// src/auth/one-time/refusals.ts
|
|
1094
|
-
function refuseStale(type, user, token, where, noun) {
|
|
1095
|
-
const email = emailOf(type, user.fields);
|
|
1096
|
-
if (email === null || normalizeEmail(email) !== normalizeEmail(token.address)) {
|
|
1097
|
-
throw new TokenError2("TOKEN_STALE", `${where}: the ${noun} was sent to an e-mail the user no longer has`, { operation: where, userId: user.id, userType: type.name });
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
function refuseUnusable(token, now, where, noun) {
|
|
1101
|
-
if (token === null) {
|
|
1102
|
-
throw new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
|
|
1103
|
-
operation: where
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
if (token.spentAt !== null) {
|
|
1107
|
-
throw new TokenError2("TOKEN_SPENT", `${where}: the ${noun} was already used`, { operation: where });
|
|
1108
|
-
}
|
|
1109
|
-
if (token.expiresAt.getTime() <= now.getTime()) {
|
|
1110
|
-
throw new TokenError2("TOKEN_EXPIRED", `${where}: the ${noun} has expired`, {
|
|
1111
|
-
operation: where
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
var unknownOneTime = (where, noun) => new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
|
|
1116
|
-
operation: where
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
// src/auth/one-time/spend.ts
|
|
1120
|
-
async function spendOneTime(context, secret, kind, where, noun) {
|
|
1121
|
-
const now = context.clock.now();
|
|
1122
|
-
const token = await context.store.tokens.consumeToken(hashSecret(secret), kind, now);
|
|
1123
|
-
refuseUnusable(token, now, where, noun);
|
|
1124
|
-
return token;
|
|
1125
|
-
}
|
|
1126
|
-
async function burnOneTime(context, secret, kind) {
|
|
1127
|
-
await context.store.tokens.consumeToken(hashSecret(secret), kind, context.clock.now());
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// src/auth/one-time/codes.ts
|
|
1131
|
-
var CODE_ATTEMPTS = 5;
|
|
1132
|
-
async function countCodeAttempt(context, secret, kind, where, userType) {
|
|
1133
|
-
const token = await context.store.tokens.countAttempt(hashSecret(secret), kind);
|
|
1134
|
-
refuseUnusable(token, context.clock.now(), where, "challenge");
|
|
1135
|
-
if (token.attempts > CODE_ATTEMPTS) {
|
|
1136
|
-
throw codeInvalid(where, token.userId, userType, 0);
|
|
833
|
+
// src/auth/one-time/codes.ts
|
|
834
|
+
var CODE_ATTEMPTS = 5;
|
|
835
|
+
async function countCodeAttempt(context, secret, kind, where, userType) {
|
|
836
|
+
const token = await context.store.tokens.countAttempt(hashSecret(secret), kind);
|
|
837
|
+
refuseUnusable(token, context.clock.now(), where, "challenge");
|
|
838
|
+
if (token.attempts > CODE_ATTEMPTS) {
|
|
839
|
+
throw codeInvalid(where, token.userId, userType, 0);
|
|
1137
840
|
}
|
|
1138
841
|
return { token, attemptsLeft: CODE_ATTEMPTS - token.attempts };
|
|
1139
842
|
}
|
|
@@ -1157,7 +860,7 @@ function codeMatches(token, challenge, code) {
|
|
|
1157
860
|
return false;
|
|
1158
861
|
const given = Buffer.from(hashCode(challenge, code));
|
|
1159
862
|
const held = Buffer.from(token.codeHash);
|
|
1160
|
-
return given.length === held.length &&
|
|
863
|
+
return given.length === held.length && timingSafeEqual2(given, held);
|
|
1161
864
|
}
|
|
1162
865
|
// src/auth/one-time/issue.ts
|
|
1163
866
|
import { randomInt } from "node:crypto";
|
|
@@ -1253,7 +956,7 @@ async function insertLink(links, tokenHash) {
|
|
|
1253
956
|
throw links.counting.vanished();
|
|
1254
957
|
}
|
|
1255
958
|
// src/auth/mail-requests.ts
|
|
1256
|
-
var
|
|
959
|
+
var PURPOSE = "janus/mail/requests/v1";
|
|
1257
960
|
async function countMailByAddress(context, type, flow, email, where, userId) {
|
|
1258
961
|
await countMail(context, type, flow, {
|
|
1259
962
|
address: normalizeEmail(email),
|
|
@@ -1267,7 +970,7 @@ async function countMail(context, type, flow, counted, where) {
|
|
|
1267
970
|
const throttle = context.config.mailThrottle;
|
|
1268
971
|
if (throttle === null)
|
|
1269
972
|
return;
|
|
1270
|
-
const key = counterKey(context,
|
|
973
|
+
const key = counterKey(context, PURPOSE);
|
|
1271
974
|
const subject = "address" in counted ? keyed(key, [flow, "address", counted.address]) : keyed(key, [flow, "user", counted.userId]);
|
|
1272
975
|
const { attempts, endsAt } = await countInWindow(context, {
|
|
1273
976
|
windowMs: throttle.windowMs,
|
|
@@ -1278,123 +981,441 @@ async function countMail(context, type, flow, counted, where) {
|
|
|
1278
981
|
});
|
|
1279
982
|
if (attempts <= throttle.attempts)
|
|
1280
983
|
return;
|
|
1281
|
-
const now = context.clock.now().getTime();
|
|
1282
|
-
throw new MailThrottledError2(`${where}: too many e-mails asked for this ${"address" in counted ? "address" : "user"} — the last one sent still works; use it, or wait for the next window`, {
|
|
1283
|
-
userType: type.name,
|
|
1284
|
-
...counted.userId === undefined ? {} : { userId: counted.userId },
|
|
1285
|
-
retryAfter: Math.max(1, Math.ceil((endsAt - now) / 1000))
|
|
1286
|
-
});
|
|
984
|
+
const now = context.clock.now().getTime();
|
|
985
|
+
throw new MailThrottledError2(`${where}: too many e-mails asked for this ${"address" in counted ? "address" : "user"} — the last one sent still works; use it, or wait for the next window`, {
|
|
986
|
+
userType: type.name,
|
|
987
|
+
...counted.userId === undefined ? {} : { userId: counted.userId },
|
|
988
|
+
retryAfter: Math.max(1, Math.ceil((endsAt - now) / 1000))
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// src/auth/users/fields-patch.ts
|
|
993
|
+
var normalized = (email) => email === null ? null : normalizeEmail(email);
|
|
994
|
+
function emailChanged(type, before, after) {
|
|
995
|
+
return normalized(emailOf(type, before)) !== normalized(emailOf(type, after));
|
|
996
|
+
}
|
|
997
|
+
async function fieldsPatch(type, record, patch, where) {
|
|
998
|
+
const fields = await validateFields(type, { ...record.fields, ...patch }, where);
|
|
999
|
+
return {
|
|
1000
|
+
fields,
|
|
1001
|
+
logins: loginsOf(type, fields, where),
|
|
1002
|
+
schemaVersion: type.schemaVersion,
|
|
1003
|
+
...emailChanged(type, record.fields, fields) ? { emailVerifiedAt: null } : {}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/auth/email-flows/change-email-token.ts
|
|
1008
|
+
var formerHash = (secret, former) => hashSecret(`janus/changeEmail/former\x00${secret}\x00${normalizeEmail(former ?? "")}`);
|
|
1009
|
+
async function newAddressOf(type, user, email, where) {
|
|
1010
|
+
const fields = await validateFields(type, { ...user.fields, [type.email]: email }, where);
|
|
1011
|
+
const address = emailOf(type, fields);
|
|
1012
|
+
if (address === null) {
|
|
1013
|
+
throw new TypeError(`janus: email "${type.email}" did not name a string in validated ${type.name} fields — the schema must keep the e-mail field a required string`);
|
|
1014
|
+
}
|
|
1015
|
+
if (normalizeEmail(address) === normalizeEmail(emailOf(type, user.fields) ?? "")) {
|
|
1016
|
+
throw new UserInvalidError2(`${where}: the new e-mail is the user's current one — a change moves to another address; verifyEmail proves the current one`, {
|
|
1017
|
+
issues: [
|
|
1018
|
+
{ path: [type.email], message: "is the current e-mail of the user" }
|
|
1019
|
+
],
|
|
1020
|
+
userType: type.name
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return address;
|
|
1024
|
+
}
|
|
1025
|
+
async function issueChangeEmailToken(context, type, user, address) {
|
|
1026
|
+
const secret = mintSecret();
|
|
1027
|
+
const { expiresAt } = await issueOneTime(context, {
|
|
1028
|
+
kind: "changeEmail",
|
|
1029
|
+
userId: user.id,
|
|
1030
|
+
address,
|
|
1031
|
+
ttlMs: context.config.tokenTtlMs.changeEmail,
|
|
1032
|
+
secret,
|
|
1033
|
+
boundHash: formerHash(secret, emailOf(type, user.fields))
|
|
1034
|
+
});
|
|
1035
|
+
await context.store.tokens.spendUserTokens(user.id, "changeEmail", context.clock.now(), hashSecret(secret));
|
|
1036
|
+
return { token: secret, email: address, expiresAt };
|
|
1037
|
+
}
|
|
1038
|
+
function refuseMoved(type, user, token, secret, where) {
|
|
1039
|
+
if (token.codeHash !== formerHash(secret, emailOf(type, user.fields))) {
|
|
1040
|
+
throw new TokenError2("TOKEN_STALE", `${where}: the token was asked from an e-mail the user no longer has`, { operation: where, userId: user.id, userType: type.name });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async function redeemChangeEmailToken(context, type, secret, where) {
|
|
1044
|
+
const token = await spendOneTime(context, secret, "changeEmail", where, "token");
|
|
1045
|
+
const user = await findRecord(context, token.userId, type?.name ?? null);
|
|
1046
|
+
const owner = type ?? context.config.types.get(user?.type ?? "");
|
|
1047
|
+
if (user === null || owner === undefined)
|
|
1048
|
+
throw unknownOneTime(where, "token");
|
|
1049
|
+
if (emailOf(owner, user.fields) === null)
|
|
1050
|
+
throw unknownOneTime(where, "token");
|
|
1051
|
+
refuseMoved(owner, user, token, secret, where);
|
|
1052
|
+
return { token, user, type: owner };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/auth/email-flows/change-email.ts
|
|
1056
|
+
var inactive = (type, user, where) => new UserInactiveError2(`${where}: the user is inactive`, {
|
|
1057
|
+
userId: user.id,
|
|
1058
|
+
userType: type.name
|
|
1059
|
+
});
|
|
1060
|
+
function changeEmailFlows(context, type, at) {
|
|
1061
|
+
return {
|
|
1062
|
+
async send(user, email) {
|
|
1063
|
+
const where = at("changeEmail.send");
|
|
1064
|
+
const record = await getRecord(context, idOf(user), type.name, where);
|
|
1065
|
+
if (!record.active)
|
|
1066
|
+
throw inactive(type, record, where);
|
|
1067
|
+
const address = await newAddressOf(type, record, email, where);
|
|
1068
|
+
await countMailByUser(context, type, "changeEmail", record.id, where);
|
|
1069
|
+
await countMailByAddress(context, type, "changeEmail", address, where, record.id);
|
|
1070
|
+
return issueChangeEmailToken(context, type, record, address);
|
|
1071
|
+
},
|
|
1072
|
+
async confirm(secret) {
|
|
1073
|
+
return confirmChange(context, type, secret, at("changeEmail.confirm"));
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
async function confirmChange(context, asked, secret, where) {
|
|
1078
|
+
const { token, user, type } = await redeemChangeEmailToken(context, asked, secret, where);
|
|
1079
|
+
if (!user.active)
|
|
1080
|
+
throw inactive(type, user, where);
|
|
1081
|
+
const seen = { replaced: null };
|
|
1082
|
+
const written = await writeUser(context, user.id, type, undefined, where, async (record, now) => {
|
|
1083
|
+
if (!record.active)
|
|
1084
|
+
throw inactive(type, record, where);
|
|
1085
|
+
refuseMoved(type, record, token, secret, where);
|
|
1086
|
+
seen.replaced = record;
|
|
1087
|
+
const patch = await fieldsPatch(type, record, { [type.email]: token.address }, where);
|
|
1088
|
+
const proved = record.emailVerifiedAt === null || emailChanged(type, record.fields, patch.fields);
|
|
1089
|
+
return proved ? { ...patch, emailVerifiedAt: now } : patch;
|
|
1090
|
+
});
|
|
1091
|
+
const { replaced } = seen;
|
|
1092
|
+
const moved = replaced !== null && emailChanged(type, replaced.fields, written.fields);
|
|
1093
|
+
if (replaced !== null && moved) {
|
|
1094
|
+
await emit(context, "user.emailChanged", written, written.updatedAt, {
|
|
1095
|
+
formerEmail: emailOf(type, replaced.fields)
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (moved || replaced?.emailVerifiedAt === null) {
|
|
1099
|
+
await emit(context, "user.emailVerified", written, written.updatedAt);
|
|
1100
|
+
}
|
|
1101
|
+
return toUser(written);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/auth/email-flows/any-type-change-email.ts
|
|
1105
|
+
function anyTypeChangeEmail(context) {
|
|
1106
|
+
return {
|
|
1107
|
+
changeEmail: {
|
|
1108
|
+
confirm: (secret) => confirmChange(context, null, secret, "changeEmail.confirm")
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/auth/port/assert-stores.ts
|
|
1114
|
+
var REQUIRED = {
|
|
1115
|
+
users: [
|
|
1116
|
+
"insertUser",
|
|
1117
|
+
"findUser",
|
|
1118
|
+
"findUserByLogin",
|
|
1119
|
+
"listUsers",
|
|
1120
|
+
"updateUser",
|
|
1121
|
+
"deleteUser"
|
|
1122
|
+
],
|
|
1123
|
+
sessions: [
|
|
1124
|
+
"insertSession",
|
|
1125
|
+
"findSessionByTokenHash",
|
|
1126
|
+
"extendSession",
|
|
1127
|
+
"reauthenticateSession",
|
|
1128
|
+
"revokeSession",
|
|
1129
|
+
"revokeUserSessions",
|
|
1130
|
+
"deleteUserSessions"
|
|
1131
|
+
],
|
|
1132
|
+
tokens: [
|
|
1133
|
+
"insertToken",
|
|
1134
|
+
"consumeToken",
|
|
1135
|
+
"countAttempt",
|
|
1136
|
+
"spendUserTokens",
|
|
1137
|
+
"deleteUserTokens"
|
|
1138
|
+
]
|
|
1139
|
+
};
|
|
1140
|
+
function assertStores(stores, where) {
|
|
1141
|
+
if (typeof stores !== "object" || stores === null) {
|
|
1142
|
+
throw new TypeError(`${where}: store must be an object with users, sessions and tokens`);
|
|
1143
|
+
}
|
|
1144
|
+
for (const slot of Object.keys(REQUIRED)) {
|
|
1145
|
+
const store = stores[slot];
|
|
1146
|
+
if (typeof store !== "object" || store === null) {
|
|
1147
|
+
throw new TypeError(`${where}: store.${slot} is missing`);
|
|
1148
|
+
}
|
|
1149
|
+
for (const method of REQUIRED[slot]) {
|
|
1150
|
+
if (typeof store[method] !== "function") {
|
|
1151
|
+
throw new TypeError(`${where}: store.${slot} has no method ${method}, which the port requires`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const slots = stores;
|
|
1156
|
+
return {
|
|
1157
|
+
collectExpired: optional(slots, "sessions", "deleteExpiredSessions", where),
|
|
1158
|
+
collectExpiredTokens: optional(slots, "tokens", "deleteExpiredTokens", where),
|
|
1159
|
+
findUsers: optional(slots, "users", "findUsers", where)
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function optional(stores, slot, method, where) {
|
|
1163
|
+
const found = stores[slot][method];
|
|
1164
|
+
if (found !== undefined && typeof found !== "function") {
|
|
1165
|
+
throw new TypeError(`${where}: store.${slot}.${method} must be a function or absent`);
|
|
1166
|
+
}
|
|
1167
|
+
return found !== undefined;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/auth/devices/token.ts
|
|
1171
|
+
import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
1172
|
+
var PURPOSE2 = "janus/devices/v1";
|
|
1173
|
+
var TOKEN = /^d1\.([A-Za-z0-9_-]{1,64})\.([A-Za-z0-9_-]{22})\.([A-Za-z0-9_-]{43})$/;
|
|
1174
|
+
function macOf(key, userId, deviceId) {
|
|
1175
|
+
return keyedHash(key, [userId, deviceId]);
|
|
1176
|
+
}
|
|
1177
|
+
function signed(sealer, userId, deviceId) {
|
|
1178
|
+
const key = derivedKey(sealer, PURPOSE2);
|
|
1179
|
+
const mac = macOf(key, userId, deviceId).toString("base64url");
|
|
1180
|
+
return `d1.${sealer.sealWith}.${deviceId}.${mac}`;
|
|
1181
|
+
}
|
|
1182
|
+
function mintDeviceToken(sealer, userId) {
|
|
1183
|
+
return signed(sealer, userId, randomBytes4(16).toString("base64url"));
|
|
1184
|
+
}
|
|
1185
|
+
function knownDeviceToken(sealer, userId, presented) {
|
|
1186
|
+
const [, keyId, deviceId, mac] = TOKEN.exec(presented) ?? [];
|
|
1187
|
+
if (keyId === undefined || deviceId === undefined || mac === undefined) {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
const key = derivedKey(sealer, PURPOSE2, keyId);
|
|
1191
|
+
if (key === undefined)
|
|
1192
|
+
return null;
|
|
1193
|
+
const expected = macOf(key, userId, deviceId);
|
|
1194
|
+
const given = Buffer.from(mac, "base64url");
|
|
1195
|
+
if (given.length !== expected.length || !timingSafeEqual3(given, expected)) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
return keyId === sealer.sealWith ? presented : signed(sealer, userId, deviceId);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/auth/devices/hint.ts
|
|
1202
|
+
var UNTRACKED = Object.freeze({
|
|
1203
|
+
newDevice: false,
|
|
1204
|
+
deviceToken: null
|
|
1205
|
+
});
|
|
1206
|
+
function deviceHint(context, options, where) {
|
|
1207
|
+
if (options !== undefined && (typeof options !== "object" || options === null)) {
|
|
1208
|
+
throw new TypeError(`${where}: options must be { device }, or left out — the token goes under device`);
|
|
1209
|
+
}
|
|
1210
|
+
const device = options?.device;
|
|
1211
|
+
if (device === undefined)
|
|
1212
|
+
return;
|
|
1213
|
+
if (device !== null && typeof device !== "string") {
|
|
1214
|
+
throw new TypeError(`${where}: options.device must be the device token the client holds, or null when it holds none`);
|
|
1215
|
+
}
|
|
1216
|
+
if (context.config.devices === null) {
|
|
1217
|
+
throw new TypeError(`${where}: a device was given, but janus() has no devices — pass devices: { keys }`);
|
|
1218
|
+
}
|
|
1219
|
+
return device;
|
|
1220
|
+
}
|
|
1221
|
+
function deviceOutcome(context, userId, hint, signUp) {
|
|
1222
|
+
const sealer = context.config.devices?.sealer;
|
|
1223
|
+
if (hint === undefined || sealer === undefined)
|
|
1224
|
+
return UNTRACKED;
|
|
1225
|
+
const known = hint === null || signUp ? null : knownDeviceToken(sealer, userId, hint);
|
|
1226
|
+
if (known !== null)
|
|
1227
|
+
return { newDevice: false, deviceToken: known };
|
|
1228
|
+
return {
|
|
1229
|
+
newDevice: !signUp,
|
|
1230
|
+
deviceToken: mintDeviceToken(sealer, userId)
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
// src/auth/devices/report.ts
|
|
1234
|
+
async function reportNewDevice(context, result) {
|
|
1235
|
+
if (result.status !== "signedIn" || !result.newDevice)
|
|
1236
|
+
return;
|
|
1237
|
+
await emit(context, "user.newDeviceSignedIn", result.user, result.session.createdAt, { sessionId: result.session.id });
|
|
1287
1238
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
return normalized(emailOf(type, before)) !== normalized(emailOf(type, after));
|
|
1239
|
+
// src/auth/sessions/open-session.ts
|
|
1240
|
+
function toSession(record) {
|
|
1241
|
+
const { tokenHash: _, ...session } = record;
|
|
1242
|
+
return session;
|
|
1293
1243
|
}
|
|
1294
|
-
async function
|
|
1295
|
-
const
|
|
1244
|
+
async function openSession(context, type, user, device, signUp = false) {
|
|
1245
|
+
const now = context.clock.now();
|
|
1246
|
+
const token = mintSecret();
|
|
1247
|
+
const record = {
|
|
1248
|
+
id: mintId2(now.getTime()),
|
|
1249
|
+
tokenHash: hashSecret(token),
|
|
1250
|
+
userId: user.id,
|
|
1251
|
+
authenticatedAt: now,
|
|
1252
|
+
expiresAt: new Date(now.getTime() + type.lifespanMs),
|
|
1253
|
+
revokedAt: null,
|
|
1254
|
+
createdAt: now
|
|
1255
|
+
};
|
|
1256
|
+
await context.store.sessions.insertSession(record);
|
|
1296
1257
|
return {
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1258
|
+
status: "signedIn",
|
|
1259
|
+
user: toUser(user),
|
|
1260
|
+
session: toSession(record),
|
|
1261
|
+
token,
|
|
1262
|
+
...deviceOutcome(context, user.id, device, signUp)
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
// src/auth/sessions/presented-token.ts
|
|
1266
|
+
function presentedToken(request, cookieName) {
|
|
1267
|
+
const headers = headersOf(request);
|
|
1268
|
+
const read = (name) => {
|
|
1269
|
+
if (headers instanceof Headers)
|
|
1270
|
+
return headers.get(name);
|
|
1271
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1272
|
+
if (key.toLowerCase() !== name || value === undefined)
|
|
1273
|
+
continue;
|
|
1274
|
+
return typeof value === "string" ? value : value.join(", ");
|
|
1275
|
+
}
|
|
1276
|
+
return null;
|
|
1301
1277
|
};
|
|
1278
|
+
const authorization = read("authorization");
|
|
1279
|
+
if (authorization !== null) {
|
|
1280
|
+
const match = /^bearer(?:\s+(.*))?$/i.exec(authorization.trim());
|
|
1281
|
+
if (match)
|
|
1282
|
+
return match[1]?.trim() ?? "";
|
|
1283
|
+
}
|
|
1284
|
+
const header = read("x-session-token");
|
|
1285
|
+
if (header !== null)
|
|
1286
|
+
return header.trim();
|
|
1287
|
+
const cookie = read("cookie");
|
|
1288
|
+
if (cookie !== null) {
|
|
1289
|
+
for (const pair of cookie.split(";")) {
|
|
1290
|
+
const at = pair.indexOf("=");
|
|
1291
|
+
if (at !== -1 && pair.slice(0, at).trim() === cookieName) {
|
|
1292
|
+
return pair.slice(at + 1).trim();
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
function headersOf(request) {
|
|
1299
|
+
if (request instanceof Headers)
|
|
1300
|
+
return request;
|
|
1301
|
+
const inner = request.headers;
|
|
1302
|
+
if (inner instanceof Headers)
|
|
1303
|
+
return inner;
|
|
1304
|
+
if (typeof inner === "object" && inner !== null)
|
|
1305
|
+
return inner;
|
|
1306
|
+
return request;
|
|
1307
|
+
}
|
|
1308
|
+
// src/auth/sessions/standing.ts
|
|
1309
|
+
async function standingSession(context, request) {
|
|
1310
|
+
const token = presentedToken(request, context.config.cookie.name);
|
|
1311
|
+
if (token === null)
|
|
1312
|
+
return null;
|
|
1313
|
+
const session = await context.store.sessions.findSessionByTokenHash(hashSecret(token));
|
|
1314
|
+
const now = context.clock.now().getTime();
|
|
1315
|
+
return session === null || session.revokedAt !== null || session.expiresAt.getTime() <= now ? null : { token, session };
|
|
1302
1316
|
}
|
|
1303
1317
|
|
|
1304
|
-
// src/auth/
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1318
|
+
// src/auth/sessions/authenticate.ts
|
|
1319
|
+
async function authenticate(context, request, options) {
|
|
1320
|
+
const { store, clock, config } = context;
|
|
1321
|
+
const presented = await standingSession(context, request);
|
|
1322
|
+
if (presented === null)
|
|
1323
|
+
return null;
|
|
1324
|
+
const { token, session } = presented;
|
|
1325
|
+
const now = clock.now().getTime();
|
|
1326
|
+
const user = await store.users.findUser(session.userId);
|
|
1327
|
+
if (user === null || !user.active || options?.type !== undefined && user.type !== options.type) {
|
|
1328
|
+
return null;
|
|
1311
1329
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1330
|
+
const type = config.types.get(user.type);
|
|
1331
|
+
if (type === undefined || !renewalDue(type, session, now)) {
|
|
1332
|
+
return {
|
|
1333
|
+
user: toUser(user),
|
|
1334
|
+
session: toSession(session),
|
|
1335
|
+
token,
|
|
1336
|
+
renewed: false
|
|
1337
|
+
};
|
|
1319
1338
|
}
|
|
1320
|
-
|
|
1339
|
+
const extended = await store.sessions.extendSession(session.id, new Date(now + type.lifespanMs));
|
|
1340
|
+
if (extended === null)
|
|
1341
|
+
return null;
|
|
1342
|
+
return {
|
|
1343
|
+
user: toUser(user),
|
|
1344
|
+
session: toSession(extended),
|
|
1345
|
+
token,
|
|
1346
|
+
renewed: true
|
|
1347
|
+
};
|
|
1321
1348
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
const { expiresAt } = await issueOneTime(context, {
|
|
1325
|
-
kind: "changeEmail",
|
|
1326
|
-
userId: user.id,
|
|
1327
|
-
address,
|
|
1328
|
-
ttlMs: context.config.tokenTtlMs.changeEmail,
|
|
1329
|
-
secret,
|
|
1330
|
-
boundHash: formerHash(secret, emailOf(type, user.fields))
|
|
1331
|
-
});
|
|
1332
|
-
await context.store.tokens.spendUserTokens(user.id, "changeEmail", context.clock.now(), hashSecret(secret));
|
|
1333
|
-
return { token: secret, email: address, expiresAt };
|
|
1349
|
+
function renewalDue(type, session, now) {
|
|
1350
|
+
return type.renewAfterMs !== null && type.lifespanMs - (session.expiresAt.getTime() - now) >= type.renewAfterMs;
|
|
1334
1351
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1352
|
+
|
|
1353
|
+
// src/auth/sessions/collect-expired.ts
|
|
1354
|
+
async function collectExpired(context) {
|
|
1355
|
+
const { store, clock, capabilities } = context;
|
|
1356
|
+
const sessions = capabilities.collectExpired ? store.sessions.deleteExpiredSessions : undefined;
|
|
1357
|
+
const tokens = capabilities.collectExpiredTokens ? store.tokens.deleteExpiredTokens : undefined;
|
|
1358
|
+
if (sessions === undefined && tokens === undefined) {
|
|
1359
|
+
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" });
|
|
1338
1360
|
}
|
|
1361
|
+
const before = clock.now();
|
|
1362
|
+
const collected = sessions === undefined ? 0 : await sessions(before);
|
|
1363
|
+
return collected + (tokens === undefined ? 0 : await tokens(before));
|
|
1339
1364
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1365
|
+
|
|
1366
|
+
// src/auth/sessions/cookie.ts
|
|
1367
|
+
function cookieOf(context) {
|
|
1368
|
+
const { name, domain, path, sameSite, secure } = context.config.cookie;
|
|
1369
|
+
const attributes = [
|
|
1370
|
+
`Path=${path}`,
|
|
1371
|
+
...domain === null ? [] : [`Domain=${domain}`],
|
|
1372
|
+
"HttpOnly",
|
|
1373
|
+
`SameSite=${sameSite[0]?.toUpperCase()}${sameSite.slice(1)}`,
|
|
1374
|
+
...secure ? ["Secure"] : []
|
|
1375
|
+
].join("; ");
|
|
1376
|
+
return {
|
|
1377
|
+
name,
|
|
1378
|
+
serialize: (token, session) => `${name}=${token}; Expires=${session.expiresAt.toUTCString()}; ${attributes}`,
|
|
1379
|
+
clear: () => `${name}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; ${attributes}`
|
|
1380
|
+
};
|
|
1347
1381
|
}
|
|
1348
1382
|
|
|
1349
|
-
// src/auth/
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
userType: type.name
|
|
1353
|
-
});
|
|
1354
|
-
function changeEmailFlows(context, type, at) {
|
|
1383
|
+
// src/auth/sessions/shared-api.ts
|
|
1384
|
+
function sharedApi(context) {
|
|
1385
|
+
const { store, clock, config } = context;
|
|
1355
1386
|
return {
|
|
1356
|
-
async
|
|
1357
|
-
|
|
1358
|
-
const record = await getRecord(context, idOf(user), type.name, where);
|
|
1359
|
-
if (!record.active)
|
|
1360
|
-
throw inactive(type, record, where);
|
|
1361
|
-
const address = await newAddressOf(type, record, email, where);
|
|
1362
|
-
await countMailByUser(context, type, "changeEmail", record.id, where);
|
|
1363
|
-
await countMailByAddress(context, type, "changeEmail", address, where, record.id);
|
|
1364
|
-
return issueChangeEmailToken(context, type, record, address);
|
|
1387
|
+
async authenticate(request, options) {
|
|
1388
|
+
return authenticate(context, request, options);
|
|
1365
1389
|
},
|
|
1366
|
-
async
|
|
1367
|
-
|
|
1368
|
-
|
|
1390
|
+
async signOut(request) {
|
|
1391
|
+
const token = presentedToken(request, config.cookie.name);
|
|
1392
|
+
if (token === null)
|
|
1393
|
+
return false;
|
|
1394
|
+
const session = await store.sessions.findSessionByTokenHash(hashSecret(token));
|
|
1395
|
+
if (session === null)
|
|
1396
|
+
return false;
|
|
1397
|
+
return store.sessions.revokeSession(session.id, clock.now());
|
|
1398
|
+
},
|
|
1399
|
+
async signOutEverywhere(user, options) {
|
|
1400
|
+
const id = idOf(user);
|
|
1401
|
+
if (!isId2(id))
|
|
1402
|
+
return 0;
|
|
1403
|
+
return options?.except === undefined || !isId2(options.except) ? store.sessions.revokeUserSessions(id, clock.now()) : store.sessions.revokeUserSessions(id, clock.now(), options.except);
|
|
1404
|
+
},
|
|
1405
|
+
async findUser(id) {
|
|
1406
|
+
const record = await findRecord(context, id, null);
|
|
1407
|
+
return record === null ? null : toUser(record);
|
|
1408
|
+
},
|
|
1409
|
+
async getUser(id) {
|
|
1410
|
+
return toUser(await getRecord(context, id, null, "getUser"));
|
|
1411
|
+
},
|
|
1412
|
+
cookie: cookieOf(context),
|
|
1413
|
+
async collectExpired() {
|
|
1414
|
+
return collectExpired(context);
|
|
1415
|
+
},
|
|
1416
|
+
types: [...config.types.keys()]
|
|
1369
1417
|
};
|
|
1370
1418
|
}
|
|
1371
|
-
async function confirmChange(context, type, secret, where) {
|
|
1372
|
-
const { token, user } = await redeemChangeEmailToken(context, type, secret, where);
|
|
1373
|
-
if (!user.active)
|
|
1374
|
-
throw inactive(type, user, where);
|
|
1375
|
-
const seen = { replaced: null };
|
|
1376
|
-
const written = await writeUser(context, user.id, type, undefined, where, async (record, now) => {
|
|
1377
|
-
if (!record.active)
|
|
1378
|
-
throw inactive(type, record, where);
|
|
1379
|
-
refuseMoved(type, record, token, secret, where);
|
|
1380
|
-
seen.replaced = record;
|
|
1381
|
-
const patch = await fieldsPatch(type, record, { [type.email]: token.address }, where);
|
|
1382
|
-
const proved = record.emailVerifiedAt === null || emailChanged(type, record.fields, patch.fields);
|
|
1383
|
-
return proved ? { ...patch, emailVerifiedAt: now } : patch;
|
|
1384
|
-
});
|
|
1385
|
-
const { replaced } = seen;
|
|
1386
|
-
const moved = replaced !== null && emailChanged(type, replaced.fields, written.fields);
|
|
1387
|
-
if (replaced !== null && moved) {
|
|
1388
|
-
await emit(context, "user.emailChanged", written, written.updatedAt, {
|
|
1389
|
-
formerEmail: emailOf(type, replaced.fields)
|
|
1390
|
-
});
|
|
1391
|
-
}
|
|
1392
|
-
if (moved || replaced?.emailVerifiedAt === null) {
|
|
1393
|
-
await emit(context, "user.emailVerified", written, written.updatedAt);
|
|
1394
|
-
}
|
|
1395
|
-
return toUser(written);
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
1419
|
// src/auth/password-written.ts
|
|
1399
1420
|
async function endWhatThePasswordOpened(context, userId) {
|
|
1400
1421
|
const now = context.clock.now();
|
|
@@ -2810,7 +2831,11 @@ function janus(config) {
|
|
|
2810
2831
|
type.name,
|
|
2811
2832
|
typeApi(context, type)
|
|
2812
2833
|
]));
|
|
2813
|
-
const surface = resolved.single ? { ...shared, ...types["user"] } : {
|
|
2834
|
+
const surface = resolved.single ? { ...shared, ...types["user"] } : {
|
|
2835
|
+
...shared,
|
|
2836
|
+
...types,
|
|
2837
|
+
...anyTypeChangeEmail(context)
|
|
2838
|
+
};
|
|
2814
2839
|
return surface;
|
|
2815
2840
|
}
|
|
2816
2841
|
// src/auth/sessions/fresh.ts
|
|
@@ -2864,5 +2889,5 @@ export {
|
|
|
2864
2889
|
systemClock
|
|
2865
2890
|
};
|
|
2866
2891
|
|
|
2867
|
-
//# debugId=
|
|
2892
|
+
//# debugId=48ED60BDDE6B1B3264756E2164756E21
|
|
2868
2893
|
//# sourceMappingURL=index.js.map
|