@nxgt/janus 0.25.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/dist/index.js CHANGED
@@ -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,66 +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
- collectExpiredTokens: optional(slots, "tokens", "deleteExpiredTokens", where),
798
- findUsers: optional(slots, "users", "findUsers", where)
799
- };
800
- }
801
- function optional(stores, slot, method, where) {
802
- const found = stores[slot][method];
803
- if (found !== undefined && typeof found !== "function") {
804
- throw new TypeError(`${where}: store.${slot}.${method} must be a function or absent`);
805
- }
806
- return found !== undefined;
807
- }
808
-
809
- // src/auth/devices/token.ts
810
- import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
811
-
812
753
  // src/auth/derived-keys.ts
813
754
  import { createHmac, hkdfSync } from "node:crypto";
814
755
  var derived = new WeakMap;
@@ -833,317 +774,69 @@ function keyedHash(key, parts) {
833
774
  return createHmac("sha256", key).update(parts.join("\x00")).digest();
834
775
  }
835
776
 
836
- // src/auth/devices/token.ts
837
- var PURPOSE = "janus/devices/v1";
838
- var TOKEN = /^d1\.([A-Za-z0-9_-]{1,64})\.([A-Za-z0-9_-]{22})\.([A-Za-z0-9_-]{43})$/;
839
- function macOf(key, userId, deviceId) {
840
- return keyedHash(key, [userId, deviceId]);
841
- }
842
- function signed(sealer, userId, deviceId) {
843
- const key = derivedKey(sealer, PURPOSE);
844
- const mac = macOf(key, userId, deviceId).toString("base64url");
845
- return `d1.${sealer.sealWith}.${deviceId}.${mac}`;
846
- }
847
- function mintDeviceToken(sealer, userId) {
848
- return signed(sealer, userId, randomBytes3(16).toString("base64url"));
849
- }
850
- function knownDeviceToken(sealer, userId, presented) {
851
- const [, keyId, deviceId, mac] = TOKEN.exec(presented) ?? [];
852
- if (keyId === undefined || deviceId === undefined || mac === undefined) {
853
- return null;
854
- }
855
- const key = derivedKey(sealer, PURPOSE, keyId);
856
- if (key === undefined)
857
- return null;
858
- const expected = macOf(key, userId, deviceId);
859
- const given = Buffer.from(mac, "base64url");
860
- if (given.length !== expected.length || !timingSafeEqual2(given, expected)) {
861
- return null;
862
- }
863
- 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);
864
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";
865
786
 
866
- // src/auth/devices/hint.ts
867
- var UNTRACKED = Object.freeze({
868
- newDevice: false,
869
- deviceToken: null
870
- });
871
- function deviceHint(context, options, where) {
872
- if (options !== undefined && (typeof options !== "object" || options === null)) {
873
- throw new TypeError(`${where}: options must be { device }, or left out — the token goes under device`);
874
- }
875
- const device = options?.device;
876
- if (device === undefined)
877
- return;
878
- if (device !== null && typeof device !== "string") {
879
- throw new TypeError(`${where}: options.device must be the device token the client holds, or null when it holds none`);
880
- }
881
- if (context.config.devices === null) {
882
- throw new TypeError(`${where}: a device was given, but janus() has no devices — pass devices: { keys }`);
883
- }
884
- return device;
885
- }
886
- function deviceOutcome(context, userId, hint, signUp) {
887
- const sealer = context.config.devices?.sealer;
888
- if (hint === undefined || sealer === undefined)
889
- return UNTRACKED;
890
- const known = hint === null || signUp ? null : knownDeviceToken(sealer, userId, hint);
891
- if (known !== null)
892
- return { newDevice: false, deviceToken: known };
893
- return {
894
- newDevice: !signUp,
895
- deviceToken: mintDeviceToken(sealer, userId)
896
- };
897
- }
898
- // src/auth/devices/report.ts
899
- async function reportNewDevice(context, result) {
900
- if (result.status !== "signedIn" || !result.newDevice)
901
- return;
902
- await emit(context, "user.newDeviceSignedIn", result.user, result.session.createdAt, { sessionId: result.session.id });
903
- }
904
787
  // src/auth/secrets.ts
905
- import { createHash, randomBytes as randomBytes4 } from "node:crypto";
788
+ import { createHash, randomBytes as randomBytes3 } from "node:crypto";
906
789
  function mintSecret() {
907
- return randomBytes4(32).toString("base64url");
790
+ return randomBytes3(32).toString("base64url");
908
791
  }
909
792
  function hashSecret(secret) {
910
793
  return createHash("sha256").update(secret).digest("hex");
911
794
  }
912
795
 
913
- // src/auth/sessions/open-session.ts
914
- function toSession(record) {
915
- const { tokenHash: _, ...session } = record;
916
- return session;
917
- }
918
- async function openSession(context, type, user, device, signUp = false) {
919
- const now = context.clock.now();
920
- const token = mintSecret();
921
- const record = {
922
- id: mintId2(now.getTime()),
923
- tokenHash: hashSecret(token),
924
- userId: user.id,
925
- authenticatedAt: now,
926
- expiresAt: new Date(now.getTime() + type.lifespanMs),
927
- revokedAt: null,
928
- createdAt: now
929
- };
930
- await context.store.sessions.insertSession(record);
931
- return {
932
- status: "signedIn",
933
- user: toUser(user),
934
- session: toSession(record),
935
- token,
936
- ...deviceOutcome(context, user.id, device, signUp)
937
- };
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
+ }
938
802
  }
939
- // src/auth/sessions/presented-token.ts
940
- function presentedToken(request, cookieName) {
941
- const headers = headersOf(request);
942
- const read = (name) => {
943
- if (headers instanceof Headers)
944
- return headers.get(name);
945
- for (const [key, value] of Object.entries(headers)) {
946
- if (key.toLowerCase() !== name || value === undefined)
947
- continue;
948
- return typeof value === "string" ? value : value.join(", ");
949
- }
950
- return null;
951
- };
952
- const authorization = read("authorization");
953
- if (authorization !== null) {
954
- const match = /^bearer(?:\s+(.*))?$/i.exec(authorization.trim());
955
- if (match)
956
- 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
+ });
957
808
  }
958
- const header = read("x-session-token");
959
- if (header !== null)
960
- return header.trim();
961
- const cookie = read("cookie");
962
- if (cookie !== null) {
963
- for (const pair of cookie.split(";")) {
964
- const at = pair.indexOf("=");
965
- if (at !== -1 && pair.slice(0, at).trim() === cookieName) {
966
- return pair.slice(at + 1).trim();
967
- }
968
- }
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
+ });
969
816
  }
970
- return null;
971
817
  }
972
- function headersOf(request) {
973
- if (request instanceof Headers)
974
- return request;
975
- const inner = request.headers;
976
- if (inner instanceof Headers)
977
- return inner;
978
- if (typeof inner === "object" && inner !== null)
979
- return inner;
980
- return request;
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;
981
828
  }
982
- // src/auth/sessions/standing.ts
983
- async function standingSession(context, request) {
984
- const token = presentedToken(request, context.config.cookie.name);
985
- if (token === null)
986
- return null;
987
- const session = await context.store.sessions.findSessionByTokenHash(hashSecret(token));
988
- const now = context.clock.now().getTime();
989
- 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());
990
831
  }
991
832
 
992
- // src/auth/sessions/authenticate.ts
993
- async function authenticate(context, request, options) {
994
- const { store, clock, config } = context;
995
- const presented = await standingSession(context, request);
996
- if (presented === null)
997
- return null;
998
- const { token, session } = presented;
999
- const now = clock.now().getTime();
1000
- const user = await store.users.findUser(session.userId);
1001
- if (user === null || !user.active || options?.type !== undefined && user.type !== options.type) {
1002
- return null;
1003
- }
1004
- const type = config.types.get(user.type);
1005
- if (type === undefined || !renewalDue(type, session, now)) {
1006
- return {
1007
- user: toUser(user),
1008
- session: toSession(session),
1009
- token,
1010
- renewed: false
1011
- };
1012
- }
1013
- const extended = await store.sessions.extendSession(session.id, new Date(now + type.lifespanMs));
1014
- if (extended === null)
1015
- return null;
1016
- return {
1017
- user: toUser(user),
1018
- session: toSession(extended),
1019
- token,
1020
- renewed: true
1021
- };
1022
- }
1023
- function renewalDue(type, session, now) {
1024
- return type.renewAfterMs !== null && type.lifespanMs - (session.expiresAt.getTime() - now) >= type.renewAfterMs;
1025
- }
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
-
1040
- // src/auth/sessions/cookie.ts
1041
- function cookieOf(context) {
1042
- const { name, domain, path, sameSite, secure } = context.config.cookie;
1043
- const attributes = [
1044
- `Path=${path}`,
1045
- ...domain === null ? [] : [`Domain=${domain}`],
1046
- "HttpOnly",
1047
- `SameSite=${sameSite[0]?.toUpperCase()}${sameSite.slice(1)}`,
1048
- ...secure ? ["Secure"] : []
1049
- ].join("; ");
1050
- return {
1051
- name,
1052
- serialize: (token, session) => `${name}=${token}; Expires=${session.expiresAt.toUTCString()}; ${attributes}`,
1053
- clear: () => `${name}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; ${attributes}`
1054
- };
1055
- }
1056
-
1057
- // src/auth/sessions/shared-api.ts
1058
- function sharedApi(context) {
1059
- const { store, clock, config } = context;
1060
- return {
1061
- async authenticate(request, options) {
1062
- return authenticate(context, request, options);
1063
- },
1064
- async signOut(request) {
1065
- const token = presentedToken(request, config.cookie.name);
1066
- if (token === null)
1067
- return false;
1068
- const session = await store.sessions.findSessionByTokenHash(hashSecret(token));
1069
- if (session === null)
1070
- return false;
1071
- return store.sessions.revokeSession(session.id, clock.now());
1072
- },
1073
- async signOutEverywhere(user, options) {
1074
- const id = idOf(user);
1075
- if (!isId2(id))
1076
- return 0;
1077
- return options?.except === undefined || !isId2(options.except) ? store.sessions.revokeUserSessions(id, clock.now()) : store.sessions.revokeUserSessions(id, clock.now(), options.except);
1078
- },
1079
- async findUser(id) {
1080
- const record = await findRecord(context, id, null);
1081
- return record === null ? null : toUser(record);
1082
- },
1083
- async getUser(id) {
1084
- return toUser(await getRecord(context, id, null, "getUser"));
1085
- },
1086
- cookie: cookieOf(context),
1087
- async collectExpired() {
1088
- return collectExpired(context);
1089
- },
1090
- types: [...config.types.keys()]
1091
- };
1092
- }
1093
- // src/auth/counter-key.ts
1094
- function counterKey(context, purpose) {
1095
- const sealer = context.config.secondFactor?.sealer;
1096
- return sealer === undefined ? Buffer.from(purpose) : derivedKey(sealer, purpose);
1097
- }
1098
- var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
1099
-
1100
- // src/auth/one-time/codes.ts
1101
- import { timingSafeEqual as timingSafeEqual3 } from "node:crypto";
1102
-
1103
- // src/auth/one-time/refusals.ts
1104
- function refuseStale(type, user, token, where, noun) {
1105
- const email = emailOf(type, user.fields);
1106
- if (email === null || normalizeEmail(email) !== normalizeEmail(token.address)) {
1107
- 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 });
1108
- }
1109
- }
1110
- function refuseUnusable(token, now, where, noun) {
1111
- if (token === null) {
1112
- throw new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
1113
- operation: where
1114
- });
1115
- }
1116
- if (token.spentAt !== null) {
1117
- throw new TokenError2("TOKEN_SPENT", `${where}: the ${noun} was already used`, { operation: where });
1118
- }
1119
- if (token.expiresAt.getTime() <= now.getTime()) {
1120
- throw new TokenError2("TOKEN_EXPIRED", `${where}: the ${noun} has expired`, {
1121
- operation: where
1122
- });
1123
- }
1124
- }
1125
- var unknownOneTime = (where, noun) => new TokenError2("TOKEN_UNKNOWN", `${where}: no such ${noun}`, {
1126
- operation: where
1127
- });
1128
-
1129
- // src/auth/one-time/spend.ts
1130
- async function spendOneTime(context, secret, kind, where, noun) {
1131
- const now = context.clock.now();
1132
- const token = await context.store.tokens.consumeToken(hashSecret(secret), kind, now);
1133
- refuseUnusable(token, now, where, noun);
1134
- return token;
1135
- }
1136
- async function burnOneTime(context, secret, kind) {
1137
- await context.store.tokens.consumeToken(hashSecret(secret), kind, context.clock.now());
1138
- }
1139
-
1140
- // src/auth/one-time/codes.ts
1141
- var CODE_ATTEMPTS = 5;
1142
- async function countCodeAttempt(context, secret, kind, where, userType) {
1143
- const token = await context.store.tokens.countAttempt(hashSecret(secret), kind);
1144
- refuseUnusable(token, context.clock.now(), where, "challenge");
1145
- if (token.attempts > CODE_ATTEMPTS) {
1146
- 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);
1147
840
  }
1148
841
  return { token, attemptsLeft: CODE_ATTEMPTS - token.attempts };
1149
842
  }
@@ -1167,7 +860,7 @@ function codeMatches(token, challenge, code) {
1167
860
  return false;
1168
861
  const given = Buffer.from(hashCode(challenge, code));
1169
862
  const held = Buffer.from(token.codeHash);
1170
- return given.length === held.length && timingSafeEqual3(given, held);
863
+ return given.length === held.length && timingSafeEqual2(given, held);
1171
864
  }
1172
865
  // src/auth/one-time/issue.ts
1173
866
  import { randomInt } from "node:crypto";
@@ -1237,174 +930,492 @@ async function countRestarting(links) {
1237
930
  absent = Number.POSITIVE_INFINITY;
1238
931
  }
1239
932
  }
1240
- throw links.counting.vanished();
1241
- }
1242
- async function countLink(links, tokenHash) {
1243
- const tokens = links.context.store.tokens;
1244
- const counted = await tokens.countAttempt(tokenHash, "secondFactor");
1245
- return counted ?? insertLink(links, tokenHash);
1246
- }
1247
- async function insertLink(links, tokenHash) {
1248
- const tokens = links.context.store.tokens;
1249
- await tokens.insertToken({
1250
- tokenHash,
1251
- kind: "secondFactor",
1252
- userId: links.counting.userId,
1253
- address: "",
1254
- codeHash: null,
1255
- attempts: 0,
1256
- expiresAt: new Date(links.expiresAt),
1257
- spentAt: null,
1258
- createdAt: new Date(links.now)
1259
- });
1260
- const inserted = await tokens.countAttempt(tokenHash, "secondFactor");
1261
- if (inserted !== null)
1262
- return inserted;
1263
- throw links.counting.vanished();
1264
- }
1265
- // src/auth/mail-requests.ts
1266
- var PURPOSE2 = "janus/mail/requests/v1";
1267
- async function countMailByAddress(context, type, flow, email, where, userId) {
1268
- await countMail(context, type, flow, {
1269
- address: normalizeEmail(email),
1270
- ...userId === undefined ? {} : { userId }
1271
- }, where);
933
+ throw links.counting.vanished();
934
+ }
935
+ async function countLink(links, tokenHash) {
936
+ const tokens = links.context.store.tokens;
937
+ const counted = await tokens.countAttempt(tokenHash, "secondFactor");
938
+ return counted ?? insertLink(links, tokenHash);
939
+ }
940
+ async function insertLink(links, tokenHash) {
941
+ const tokens = links.context.store.tokens;
942
+ await tokens.insertToken({
943
+ tokenHash,
944
+ kind: "secondFactor",
945
+ userId: links.counting.userId,
946
+ address: "",
947
+ codeHash: null,
948
+ attempts: 0,
949
+ expiresAt: new Date(links.expiresAt),
950
+ spentAt: null,
951
+ createdAt: new Date(links.now)
952
+ });
953
+ const inserted = await tokens.countAttempt(tokenHash, "secondFactor");
954
+ if (inserted !== null)
955
+ return inserted;
956
+ throw links.counting.vanished();
957
+ }
958
+ // src/auth/mail-requests.ts
959
+ var PURPOSE = "janus/mail/requests/v1";
960
+ async function countMailByAddress(context, type, flow, email, where, userId) {
961
+ await countMail(context, type, flow, {
962
+ address: normalizeEmail(email),
963
+ ...userId === undefined ? {} : { userId }
964
+ }, where);
965
+ }
966
+ async function countMailByUser(context, type, flow, userId, where) {
967
+ await countMail(context, type, flow, { userId }, where);
968
+ }
969
+ async function countMail(context, type, flow, counted, where) {
970
+ const throttle = context.config.mailThrottle;
971
+ if (throttle === null)
972
+ return;
973
+ const key = counterKey(context, PURPOSE);
974
+ const subject = "address" in counted ? keyed(key, [flow, "address", counted.address]) : keyed(key, [flow, "user", counted.userId]);
975
+ const { attempts, endsAt } = await countInWindow(context, {
976
+ windowMs: throttle.windowMs,
977
+ spent: "carry",
978
+ userId: subject,
979
+ linkHash: (window, link) => hashSecret(keyed(key, [subject, String(window), String(link)])),
980
+ vanished: () => new StoreFailure2(`${where}: the store dropped the requests it had just counted`, { slot: "tokens", operation: "countAttempt" })
981
+ });
982
+ if (attempts <= throttle.attempts)
983
+ return;
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 });
1238
+ }
1239
+ // src/auth/sessions/open-session.ts
1240
+ function toSession(record) {
1241
+ const { tokenHash: _, ...session } = record;
1242
+ return session;
1243
+ }
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);
1257
+ return {
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;
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;
1272
1297
  }
1273
- async function countMailByUser(context, type, flow, userId, where) {
1274
- await countMail(context, type, flow, { userId }, where);
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;
1275
1307
  }
1276
- async function countMail(context, type, flow, counted, where) {
1277
- const throttle = context.config.mailThrottle;
1278
- if (throttle === null)
1279
- return;
1280
- const key = counterKey(context, PURPOSE2);
1281
- const subject = "address" in counted ? keyed(key, [flow, "address", counted.address]) : keyed(key, [flow, "user", counted.userId]);
1282
- const { attempts, endsAt } = await countInWindow(context, {
1283
- windowMs: throttle.windowMs,
1284
- spent: "carry",
1285
- userId: subject,
1286
- linkHash: (window, link) => hashSecret(keyed(key, [subject, String(window), String(link)])),
1287
- vanished: () => new StoreFailure2(`${where}: the store dropped the requests it had just counted`, { slot: "tokens", operation: "countAttempt" })
1288
- });
1289
- if (attempts <= throttle.attempts)
1290
- return;
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));
1291
1314
  const now = context.clock.now().getTime();
1292
- 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`, {
1293
- userType: type.name,
1294
- ...counted.userId === undefined ? {} : { userId: counted.userId },
1295
- retryAfter: Math.max(1, Math.ceil((endsAt - now) / 1000))
1296
- });
1297
- }
1298
-
1299
- // src/auth/users/fields-patch.ts
1300
- var normalized = (email) => email === null ? null : normalizeEmail(email);
1301
- function emailChanged(type, before, after) {
1302
- return normalized(emailOf(type, before)) !== normalized(emailOf(type, after));
1303
- }
1304
- async function fieldsPatch(type, record, patch, where) {
1305
- const fields = await validateFields(type, { ...record.fields, ...patch }, where);
1306
- return {
1307
- fields,
1308
- logins: loginsOf(type, fields, where),
1309
- schemaVersion: type.schemaVersion,
1310
- ...emailChanged(type, record.fields, fields) ? { emailVerifiedAt: null } : {}
1311
- };
1315
+ return session === null || session.revokedAt !== null || session.expiresAt.getTime() <= now ? null : { token, session };
1312
1316
  }
1313
1317
 
1314
- // src/auth/email-flows/change-email-token.ts
1315
- var formerHash = (secret, former) => hashSecret(`janus/changeEmail/former\x00${secret}\x00${normalizeEmail(former ?? "")}`);
1316
- async function newAddressOf(type, user, email, where) {
1317
- const fields = await validateFields(type, { ...user.fields, [type.email]: email }, where);
1318
- const address = emailOf(type, fields);
1319
- if (address === null) {
1320
- 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`);
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;
1321
1329
  }
1322
- if (normalizeEmail(address) === normalizeEmail(emailOf(type, user.fields) ?? "")) {
1323
- 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`, {
1324
- issues: [
1325
- { path: [type.email], message: "is the current e-mail of the user" }
1326
- ],
1327
- userType: type.name
1328
- });
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
+ };
1329
1338
  }
1330
- return address;
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
+ };
1331
1348
  }
1332
- async function issueChangeEmailToken(context, type, user, address) {
1333
- const secret = mintSecret();
1334
- const { expiresAt } = await issueOneTime(context, {
1335
- kind: "changeEmail",
1336
- userId: user.id,
1337
- address,
1338
- ttlMs: context.config.tokenTtlMs.changeEmail,
1339
- secret,
1340
- boundHash: formerHash(secret, emailOf(type, user.fields))
1341
- });
1342
- await context.store.tokens.spendUserTokens(user.id, "changeEmail", context.clock.now(), hashSecret(secret));
1343
- 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;
1344
1351
  }
1345
- function refuseMoved(type, user, token, secret, where) {
1346
- if (token.codeHash !== formerHash(secret, emailOf(type, user.fields))) {
1347
- 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 });
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" });
1348
1360
  }
1361
+ const before = clock.now();
1362
+ const collected = sessions === undefined ? 0 : await sessions(before);
1363
+ return collected + (tokens === undefined ? 0 : await tokens(before));
1349
1364
  }
1350
- async function redeemChangeEmailToken(context, type, secret, where) {
1351
- const token = await spendOneTime(context, secret, "changeEmail", where, "token");
1352
- const user = await findRecord(context, token.userId, type.name);
1353
- if (user === null)
1354
- throw unknownOneTime(where, "token");
1355
- refuseMoved(type, user, token, secret, where);
1356
- return { token, user };
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
+ };
1357
1381
  }
1358
1382
 
1359
- // src/auth/email-flows/change-email.ts
1360
- var inactive = (type, user, where) => new UserInactiveError2(`${where}: the user is inactive`, {
1361
- userId: user.id,
1362
- userType: type.name
1363
- });
1364
- function changeEmailFlows(context, type, at) {
1383
+ // src/auth/sessions/shared-api.ts
1384
+ function sharedApi(context) {
1385
+ const { store, clock, config } = context;
1365
1386
  return {
1366
- async send(user, email) {
1367
- const where = at("changeEmail.send");
1368
- const record = await getRecord(context, idOf(user), type.name, where);
1369
- if (!record.active)
1370
- throw inactive(type, record, where);
1371
- const address = await newAddressOf(type, record, email, where);
1372
- await countMailByUser(context, type, "changeEmail", record.id, where);
1373
- await countMailByAddress(context, type, "changeEmail", address, where, record.id);
1374
- return issueChangeEmailToken(context, type, record, address);
1387
+ async authenticate(request, options) {
1388
+ return authenticate(context, request, options);
1375
1389
  },
1376
- async confirm(secret) {
1377
- return confirmChange(context, type, secret, at("changeEmail.confirm"));
1378
- }
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()]
1379
1417
  };
1380
1418
  }
1381
- async function confirmChange(context, type, secret, where) {
1382
- const { token, user } = await redeemChangeEmailToken(context, type, secret, where);
1383
- if (!user.active)
1384
- throw inactive(type, user, where);
1385
- const seen = { replaced: null };
1386
- const written = await writeUser(context, user.id, type, undefined, where, async (record, now) => {
1387
- if (!record.active)
1388
- throw inactive(type, record, where);
1389
- refuseMoved(type, record, token, secret, where);
1390
- seen.replaced = record;
1391
- const patch = await fieldsPatch(type, record, { [type.email]: token.address }, where);
1392
- const proved = record.emailVerifiedAt === null || emailChanged(type, record.fields, patch.fields);
1393
- return proved ? { ...patch, emailVerifiedAt: now } : patch;
1394
- });
1395
- const { replaced } = seen;
1396
- const moved = replaced !== null && emailChanged(type, replaced.fields, written.fields);
1397
- if (replaced !== null && moved) {
1398
- await emit(context, "user.emailChanged", written, written.updatedAt, {
1399
- formerEmail: emailOf(type, replaced.fields)
1400
- });
1401
- }
1402
- if (moved || replaced?.emailVerifiedAt === null) {
1403
- await emit(context, "user.emailVerified", written, written.updatedAt);
1404
- }
1405
- return toUser(written);
1406
- }
1407
-
1408
1419
  // src/auth/password-written.ts
1409
1420
  async function endWhatThePasswordOpened(context, userId) {
1410
1421
  const now = context.clock.now();
@@ -2820,7 +2831,11 @@ function janus(config) {
2820
2831
  type.name,
2821
2832
  typeApi(context, type)
2822
2833
  ]));
2823
- const surface = resolved.single ? { ...shared, ...types["user"] } : { ...shared, ...types };
2834
+ const surface = resolved.single ? { ...shared, ...types["user"] } : {
2835
+ ...shared,
2836
+ ...types,
2837
+ ...anyTypeChangeEmail(context)
2838
+ };
2824
2839
  return surface;
2825
2840
  }
2826
2841
  // src/auth/sessions/fresh.ts
@@ -2874,5 +2889,5 @@ export {
2874
2889
  systemClock
2875
2890
  };
2876
2891
 
2877
- //# debugId=D10ED7160A09120864756E2164756E21
2892
+ //# debugId=48ED60BDDE6B1B3264756E2164756E21
2878
2893
  //# sourceMappingURL=index.js.map