@fonderie/auth 7.9.4 → 7.10.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 +31 -0
- package/brain/outcomes.md +2 -0
- package/brain/signatures.md +34 -0
- package/dist/index.cjs +132 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +130 -20
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +93 -4
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js +93 -4
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/sql/018_login_event_location.sql +6 -0
- package/dist/migrations/sql/019_session_location.sql +5 -0
- package/dist/{session-Bv4qu9xV.d.cts → session-DU9QTf7f.d.cts} +37 -1
- package/dist/{session-Bv4qu9xV.d.ts → session-DU9QTf7f.d.ts} +37 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,37 @@ import { withSession, requireAuth } from '@fonderie/auth';
|
|
|
37
37
|
Also exports `toUserDTO`, `normalizeEmail`, and the full type surface
|
|
38
38
|
(`IUser`, `ISession`, `IMfaChallenge`, …).
|
|
39
39
|
|
|
40
|
+
## Where did that request come from? (optional)
|
|
41
|
+
|
|
42
|
+
Pass a `location` resolver and auth stamps a location on every event it
|
|
43
|
+
records — each login attempt, each registration, and each new session:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { geoFromHeaders } from '@fonderie/geo';
|
|
47
|
+
|
|
48
|
+
new AuthModule(store, {
|
|
49
|
+
providers: ['email'],
|
|
50
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
51
|
+
// Vercel/Cloudflare edge headers — zero infrastructure. Trust comes from
|
|
52
|
+
// the deployment, never the request.
|
|
53
|
+
location: ({ headers }) =>
|
|
54
|
+
geoFromHeaders(headers, { trust: process.env.VERCEL ? 'vercel' : undefined }),
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Login history then includes `registration` rows alongside sign-ins, and both
|
|
59
|
+
history events and active sessions carry `location: { country, subdivision,
|
|
60
|
+
city, timeZone, … } | null` (plus `isp`/`asn`/`proxy`/`hosting` if your
|
|
61
|
+
resolver knows them). Registration matters most when verification is not
|
|
62
|
+
enforced: the account is live from that request, so it is the first record of
|
|
63
|
+
where the user came from.
|
|
64
|
+
|
|
65
|
+
Run your migrations: `018_login_event_location.sql` and
|
|
66
|
+
`019_session_location.sql` add the columns. The resolver runs at most once per
|
|
67
|
+
request; its output is sanitized and bounded (coordinates ~1 km); if it throws or takes over 500 ms the row is written without a location
|
|
68
|
+
and the request is unaffected. Country is reliable; region and city are
|
|
69
|
+
approximate.
|
|
70
|
+
|
|
40
71
|
## Why this exists
|
|
41
72
|
|
|
42
73
|
You've shipped this plumbing before — auth, teams, billing, messaging —
|
package/brain/outcomes.md
CHANGED
|
@@ -38,6 +38,7 @@ failure_reason TEXT
|
|
|
38
38
|
ip_address TEXT
|
|
39
39
|
user_agent TEXT
|
|
40
40
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
41
|
+
location JSONB
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
### `fonderie_mfa_backup_codes`
|
|
@@ -93,6 +94,7 @@ ip_address TEXT
|
|
|
93
94
|
expires_at TIMESTAMPTZ NOT NULL
|
|
94
95
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
95
96
|
sid UUID
|
|
97
|
+
location JSONB
|
|
96
98
|
-- INDEX idx_fonderie_sessions_expires_at (expires_at)
|
|
97
99
|
-- INDEX idx_fonderie_sessions_sid (sid)
|
|
98
100
|
```
|
package/brain/signatures.md
CHANGED
|
@@ -56,6 +56,7 @@ new AuthModule(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus | undef
|
|
|
56
56
|
.install(app: IFonderieApp): void
|
|
57
57
|
|
|
58
58
|
interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
|
|
59
|
+
location?: LocationResolver;
|
|
59
60
|
secureCookies?: boolean;
|
|
60
61
|
rateLimit?: IAuthRateLimitConfig | false;
|
|
61
62
|
accessTokenDuration?: string;
|
|
@@ -139,6 +140,7 @@ interface ILoginEventDTO {
|
|
|
139
140
|
failureReason: string | null;
|
|
140
141
|
ipAddress: string | null;
|
|
141
142
|
userAgent: string | null;
|
|
143
|
+
location: IRequestLocation | null;
|
|
142
144
|
createdAt: string;
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -152,10 +154,42 @@ interface ISessionDTO {
|
|
|
152
154
|
current: boolean;
|
|
153
155
|
ipAddress: string | null;
|
|
154
156
|
userAgent: string | null;
|
|
157
|
+
location: IRequestLocation | null;
|
|
155
158
|
createdAt: string;
|
|
156
159
|
expiresAt: string;
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
function sanitizeLocation(input: unknown): IRequestLocation | null
|
|
163
|
+
|
|
164
|
+
function resolveLocation(resolver: LocationResolver | undefined, req: ILocationRequest, timeoutMs?: number): Promise<IRequestLocation | null>
|
|
165
|
+
|
|
166
|
+
interface IRequestLocation {
|
|
167
|
+
country?: string | null;
|
|
168
|
+
countryName?: string | null;
|
|
169
|
+
subdivision?: string | null;
|
|
170
|
+
subdivisionName?: string | null;
|
|
171
|
+
city?: string | null;
|
|
172
|
+
postalCode?: string | null;
|
|
173
|
+
continent?: string | null;
|
|
174
|
+
timeZone?: string | null;
|
|
175
|
+
latitude?: number | null;
|
|
176
|
+
longitude?: number | null;
|
|
177
|
+
accuracyRadius?: number | null;
|
|
178
|
+
isp?: string | null;
|
|
179
|
+
org?: string | null;
|
|
180
|
+
asn?: string | null;
|
|
181
|
+
mobile?: boolean | null;
|
|
182
|
+
proxy?: boolean | null;
|
|
183
|
+
hosting?: boolean | null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface ILocationRequest {
|
|
187
|
+
ip: string | null;
|
|
188
|
+
headers: Headers;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type LocationResolver = (req: ILocationRequest) => IRequestLocation | null | undefined | Promise<IRequestLocation | null | undefined>;
|
|
192
|
+
|
|
159
193
|
function validate(schema: IRequestSchema): Middleware
|
|
160
194
|
|
|
161
195
|
namespace schemas — exports: ChangePasswordInput, LoginInput, RegisterInput, ResetPasswordInput, appleNativeSchema, changePasswordSchema, forgotPasswordSchema, loginSchema, mfaTokenSchema, refreshSchema, registerSchema, resetPasswordSchema, updateEmailSchema, updatePhoneSchema, updatePreferencesSchema, updateProfileSchema, verifySchema
|
package/dist/index.cjs
CHANGED
|
@@ -80,6 +80,8 @@ __export(index_exports, {
|
|
|
80
80
|
normalizeEmailSafe: () => normalizeEmailSafe,
|
|
81
81
|
purgeSoftDeletedUsers: () => purgeSoftDeletedUsers,
|
|
82
82
|
requireAuth: () => import_middlewares3.requireAuth,
|
|
83
|
+
resolveLocation: () => resolveLocation,
|
|
84
|
+
sanitizeLocation: () => sanitizeLocation,
|
|
83
85
|
schemas: () => schemas_exports,
|
|
84
86
|
startUserRetention: () => startUserRetention,
|
|
85
87
|
toAdminUserDTO: () => toAdminUserDTO,
|
|
@@ -881,18 +883,106 @@ var UserModel = class {
|
|
|
881
883
|
};
|
|
882
884
|
var MAX_LIST_LIMIT = 200;
|
|
883
885
|
|
|
886
|
+
// src/services/request-location.ts
|
|
887
|
+
var LOCATION_TIMEOUT_MS = 500;
|
|
888
|
+
var TEXT_MAX = 128;
|
|
889
|
+
function str(v, re) {
|
|
890
|
+
if (typeof v !== "string") return null;
|
|
891
|
+
const t = v.trim();
|
|
892
|
+
if (t.length === 0 || t.length > TEXT_MAX) return null;
|
|
893
|
+
return re && !re.test(t) ? null : t;
|
|
894
|
+
}
|
|
895
|
+
function upperCode(v, re) {
|
|
896
|
+
const t = str(v, re);
|
|
897
|
+
return t ? t.toUpperCase() : null;
|
|
898
|
+
}
|
|
899
|
+
function bool(v) {
|
|
900
|
+
return typeof v === "boolean" ? v : null;
|
|
901
|
+
}
|
|
902
|
+
function coord(v, limit) {
|
|
903
|
+
if (typeof v !== "number" || !Number.isFinite(v) || Math.abs(v) > limit) return null;
|
|
904
|
+
return Math.round(v * 100) / 100;
|
|
905
|
+
}
|
|
906
|
+
function radius(v) {
|
|
907
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0 || v > 2e4) return null;
|
|
908
|
+
return Math.round(v);
|
|
909
|
+
}
|
|
910
|
+
function sanitizeLocation(input) {
|
|
911
|
+
if (!input || typeof input !== "object") return null;
|
|
912
|
+
const i = input;
|
|
913
|
+
const out = {
|
|
914
|
+
country: upperCode(i["country"], /^[A-Za-z]{2}$/),
|
|
915
|
+
countryName: str(i["countryName"]),
|
|
916
|
+
subdivision: upperCode(i["subdivision"], /^[A-Za-z0-9]{1,3}$/),
|
|
917
|
+
subdivisionName: str(i["subdivisionName"]),
|
|
918
|
+
city: str(i["city"]),
|
|
919
|
+
postalCode: upperCode(i["postalCode"], /^[A-Za-z0-9][A-Za-z0-9 -]{0,11}$/),
|
|
920
|
+
continent: upperCode(i["continent"], /^[A-Za-z]{2}$/),
|
|
921
|
+
timeZone: str(i["timeZone"], /^(UTC|[A-Za-z_]+(\/[A-Za-z0-9_+-]+)+)$/),
|
|
922
|
+
latitude: coord(i["latitude"], 90),
|
|
923
|
+
longitude: coord(i["longitude"], 180),
|
|
924
|
+
accuracyRadius: radius(i["accuracyRadius"]),
|
|
925
|
+
isp: str(i["isp"]),
|
|
926
|
+
org: str(i["org"]),
|
|
927
|
+
asn: upperCode(i["asn"], /^AS\d{1,10}$/i),
|
|
928
|
+
mobile: bool(i["mobile"]),
|
|
929
|
+
proxy: bool(i["proxy"]),
|
|
930
|
+
hosting: bool(i["hosting"])
|
|
931
|
+
};
|
|
932
|
+
const known = Object.fromEntries(Object.entries(out).filter(([, v]) => v !== null));
|
|
933
|
+
return Object.keys(known).length > 0 ? known : null;
|
|
934
|
+
}
|
|
935
|
+
var perRequest = /* @__PURE__ */ new WeakMap();
|
|
936
|
+
function resolveLocation(resolver, req, timeoutMs = LOCATION_TIMEOUT_MS) {
|
|
937
|
+
if (!resolver) return Promise.resolve(null);
|
|
938
|
+
const cached = perRequest.get(req.headers);
|
|
939
|
+
if (cached && cached.resolver === resolver) return cached.result;
|
|
940
|
+
const result = resolveOnce(resolver, req, timeoutMs);
|
|
941
|
+
perRequest.set(req.headers, { resolver, result });
|
|
942
|
+
return result;
|
|
943
|
+
}
|
|
944
|
+
async function resolveOnce(resolver, req, timeoutMs) {
|
|
945
|
+
let timer;
|
|
946
|
+
try {
|
|
947
|
+
const result = await Promise.race([
|
|
948
|
+
Promise.resolve().then(() => resolver(req)),
|
|
949
|
+
new Promise((resolve2) => {
|
|
950
|
+
timer = setTimeout(() => resolve2(null), timeoutMs);
|
|
951
|
+
})
|
|
952
|
+
]);
|
|
953
|
+
return sanitizeLocation(result);
|
|
954
|
+
} catch (err) {
|
|
955
|
+
console.warn("[auth] location resolver failed; recording without a location:", err);
|
|
956
|
+
return null;
|
|
957
|
+
} finally {
|
|
958
|
+
if (timer) clearTimeout(timer);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
884
962
|
// src/models/session.model.ts
|
|
885
963
|
var SessionModel = class {
|
|
886
|
-
|
|
964
|
+
// `locate` is IAuthConfig.location. Absent ⇒ sessions carry no location.
|
|
965
|
+
constructor(store, locate) {
|
|
887
966
|
this.store = store;
|
|
967
|
+
this.locate = locate;
|
|
888
968
|
}
|
|
889
969
|
store;
|
|
970
|
+
locate;
|
|
890
971
|
async create(userId, token, expiresAt, sid, meta) {
|
|
972
|
+
const location = this.locate && meta?.headers ? await resolveLocation(this.locate, { ip: meta.ipAddress, headers: meta.headers }) : null;
|
|
891
973
|
await this.store.query(
|
|
892
|
-
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address)
|
|
893
|
-
VALUES ($1, $2, $3, $4, $5, $6)
|
|
974
|
+
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location)
|
|
975
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
894
976
|
ON CONFLICT (token) DO NOTHING`,
|
|
895
|
-
[
|
|
977
|
+
[
|
|
978
|
+
userId,
|
|
979
|
+
token,
|
|
980
|
+
expiresAt,
|
|
981
|
+
sid ?? null,
|
|
982
|
+
meta?.userAgent ?? null,
|
|
983
|
+
meta?.ipAddress ?? null,
|
|
984
|
+
location ? JSON.stringify(location) : null
|
|
985
|
+
]
|
|
896
986
|
);
|
|
897
987
|
}
|
|
898
988
|
async delete(token) {
|
|
@@ -931,6 +1021,7 @@ var SessionModel = class {
|
|
|
931
1021
|
`SELECT id, sid,
|
|
932
1022
|
user_agent AS "userAgent",
|
|
933
1023
|
ip_address AS "ipAddress",
|
|
1024
|
+
location,
|
|
934
1025
|
created_at AS "createdAt",
|
|
935
1026
|
expires_at AS "expiresAt"
|
|
936
1027
|
FROM fonderie_sessions
|
|
@@ -1014,15 +1105,20 @@ var BackupCodeModel = class {
|
|
|
1014
1105
|
// src/models/login-event.model.ts
|
|
1015
1106
|
var import_core4 = require("@fonderie/core");
|
|
1016
1107
|
var LoginEventModel = class {
|
|
1017
|
-
|
|
1108
|
+
// `locate` is IAuthConfig.location. Absent ⇒ rows carry no location,
|
|
1109
|
+
// exactly as before.
|
|
1110
|
+
constructor(store, locate) {
|
|
1018
1111
|
this.store = store;
|
|
1112
|
+
this.locate = locate;
|
|
1019
1113
|
}
|
|
1020
1114
|
store;
|
|
1115
|
+
locate;
|
|
1021
1116
|
async record(e) {
|
|
1117
|
+
const location = this.locate ? await resolveLocation(this.locate, { ip: e.ipAddress, headers: e.headers ?? new Headers() }) : null;
|
|
1022
1118
|
await this.store.query(
|
|
1023
1119
|
`INSERT INTO fonderie_login_events
|
|
1024
|
-
(user_id, email_attempted, method, outcome, failure_reason, ip_address, user_agent)
|
|
1025
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
1120
|
+
(user_id, email_attempted, method, outcome, failure_reason, ip_address, user_agent, location)
|
|
1121
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
1026
1122
|
[
|
|
1027
1123
|
e.userId,
|
|
1028
1124
|
e.emailAttempted ?? null,
|
|
@@ -1030,7 +1126,8 @@ var LoginEventModel = class {
|
|
|
1030
1126
|
e.outcome,
|
|
1031
1127
|
e.failureReason ?? null,
|
|
1032
1128
|
e.ipAddress,
|
|
1033
|
-
e.userAgent
|
|
1129
|
+
e.userAgent,
|
|
1130
|
+
location ? JSON.stringify(location) : null
|
|
1034
1131
|
]
|
|
1035
1132
|
);
|
|
1036
1133
|
}
|
|
@@ -1069,7 +1166,7 @@ var LoginEventModel = class {
|
|
|
1069
1166
|
params.push(limit + 1);
|
|
1070
1167
|
const rows = await this.store.query(
|
|
1071
1168
|
`SELECT id, method, outcome, failure_reason AS "failureReason",
|
|
1072
|
-
ip_address AS "ipAddress", user_agent AS "userAgent",
|
|
1169
|
+
ip_address AS "ipAddress", user_agent AS "userAgent", location,
|
|
1073
1170
|
created_at AS "createdAt", created_at::text AS "createdAtRaw"
|
|
1074
1171
|
FROM fonderie_login_events
|
|
1075
1172
|
WHERE ${where.join(" AND ")}
|
|
@@ -1097,7 +1194,8 @@ function requestMeta(ctx) {
|
|
|
1097
1194
|
const ua = ctx.request.headers.get("user-agent");
|
|
1098
1195
|
const meta = {
|
|
1099
1196
|
ipAddress: typeof ip === "string" && ip.length > 0 ? ip : null,
|
|
1100
|
-
userAgent: ua && ua.length > 0 ? ua.slice(0, UA_MAX) : null
|
|
1197
|
+
userAgent: ua && ua.length > 0 ? ua.slice(0, UA_MAX) : null,
|
|
1198
|
+
headers: ctx.request.headers
|
|
1101
1199
|
};
|
|
1102
1200
|
if (meta.ipAddress === null && meta.userAgent === null) warnIfIdentityless(ctx);
|
|
1103
1201
|
return meta;
|
|
@@ -1106,9 +1204,9 @@ function requestMeta(ctx) {
|
|
|
1106
1204
|
// src/controllers/mfa.controller.ts
|
|
1107
1205
|
function mfaController(store, config, issuer, bus) {
|
|
1108
1206
|
const users = new UserModel(store);
|
|
1109
|
-
const sessions = new SessionModel(store);
|
|
1207
|
+
const sessions = new SessionModel(store, config.location);
|
|
1110
1208
|
const backupCodes = new BackupCodeModel(store);
|
|
1111
|
-
const loginEvents = new LoginEventModel(store);
|
|
1209
|
+
const loginEvents = new LoginEventModel(store, config.location);
|
|
1112
1210
|
const mfaCipher = makeMfaCipher(config.mfaSecretKey);
|
|
1113
1211
|
return {
|
|
1114
1212
|
// ── 1. Setup ───────────────────────────────────────────────
|
|
@@ -1506,8 +1604,8 @@ function extractRefreshToken(ctx) {
|
|
|
1506
1604
|
}
|
|
1507
1605
|
function authController(store, config, bus) {
|
|
1508
1606
|
const users = new UserModel(store);
|
|
1509
|
-
const sessions = new SessionModel(store);
|
|
1510
|
-
const loginEvents = new LoginEventModel(store);
|
|
1607
|
+
const sessions = new SessionModel(store, config.location);
|
|
1608
|
+
const loginEvents = new LoginEventModel(store, config.location);
|
|
1511
1609
|
const passwordReset = new PasswordResetModel(store);
|
|
1512
1610
|
const emailVerif = new EmailVerificationModel(store);
|
|
1513
1611
|
const phoneVerif = new PhoneVerificationModel(store);
|
|
@@ -1575,7 +1673,15 @@ function authController(store, config, bus) {
|
|
|
1575
1673
|
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1576
1674
|
loginMethod: "email"
|
|
1577
1675
|
});
|
|
1578
|
-
|
|
1676
|
+
const registerMeta = requestMeta(ctx);
|
|
1677
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, registerMeta);
|
|
1678
|
+
await loginEvents.recordSafe({
|
|
1679
|
+
userId: user.id,
|
|
1680
|
+
emailAttempted: normalizedEmail,
|
|
1681
|
+
method: "registration",
|
|
1682
|
+
outcome: "success",
|
|
1683
|
+
...registerMeta
|
|
1684
|
+
});
|
|
1579
1685
|
const resolvedRegister = { ...config, ...config.resolve?.(ctx) };
|
|
1580
1686
|
const requiresVerification = !!resolvedRegister.requireVerification && !user.emailVerifiedAt;
|
|
1581
1687
|
return Response.json(
|
|
@@ -2134,6 +2240,9 @@ function toLoginEventDTO(row) {
|
|
|
2134
2240
|
failureReason: row.failureReason,
|
|
2135
2241
|
ipAddress: row.ipAddress,
|
|
2136
2242
|
userAgent: row.userAgent,
|
|
2243
|
+
// Re-sanitized on read: the column is JSONB and could have been written
|
|
2244
|
+
// by anything with database access.
|
|
2245
|
+
location: sanitizeLocation(row.location),
|
|
2137
2246
|
createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt)
|
|
2138
2247
|
};
|
|
2139
2248
|
}
|
|
@@ -2149,6 +2258,7 @@ function toSessionDTO(row, currentSid) {
|
|
|
2149
2258
|
current: currentSid !== null && row.sid === currentSid,
|
|
2150
2259
|
ipAddress: row.ipAddress,
|
|
2151
2260
|
userAgent: row.userAgent,
|
|
2261
|
+
location: sanitizeLocation(row.location),
|
|
2152
2262
|
createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt),
|
|
2153
2263
|
expiresAt: row.expiresAt instanceof Date ? row.expiresAt.toISOString() : String(row.expiresAt)
|
|
2154
2264
|
};
|
|
@@ -2163,8 +2273,8 @@ function isValidPhone2(phone2) {
|
|
|
2163
2273
|
}
|
|
2164
2274
|
function userController(store, config, bus) {
|
|
2165
2275
|
const users = new UserModel(store);
|
|
2166
|
-
const sessions = new SessionModel(store);
|
|
2167
|
-
const loginEvents = new LoginEventModel(store);
|
|
2276
|
+
const sessions = new SessionModel(store, config.location);
|
|
2277
|
+
const loginEvents = new LoginEventModel(store, config.location);
|
|
2168
2278
|
const emailVerif = new EmailVerificationModel(store);
|
|
2169
2279
|
const phoneVerif = new PhoneVerificationModel(store);
|
|
2170
2280
|
return {
|
|
@@ -2608,8 +2718,8 @@ async function verifyAppleIdToken(idToken, opts) {
|
|
|
2608
2718
|
var appleEmailVerified = (c) => c.email_verified === true || c.email_verified === "true";
|
|
2609
2719
|
function oauthController(store, config, bus) {
|
|
2610
2720
|
const users = new UserModel(store);
|
|
2611
|
-
const sessions = new SessionModel(store);
|
|
2612
|
-
const loginEvents = new LoginEventModel(store);
|
|
2721
|
+
const sessions = new SessionModel(store, config.location);
|
|
2722
|
+
const loginEvents = new LoginEventModel(store, config.location);
|
|
2613
2723
|
const consumedTokens = new ConsumedTokenModel(store);
|
|
2614
2724
|
const announceOAuthUpsert = async (ctx, upserted, provider, user) => {
|
|
2615
2725
|
if (!bus) return;
|
|
@@ -3398,7 +3508,7 @@ var AuthModule = class {
|
|
|
3398
3508
|
name = "@fonderie/auth";
|
|
3399
3509
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3400
3510
|
// say what is actually deployed rather than 'not reported'.
|
|
3401
|
-
version = "7.
|
|
3511
|
+
version = "7.10.0";
|
|
3402
3512
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3403
3513
|
describeAdmin() {
|
|
3404
3514
|
return { routes: describeAuthAdminRoutes(this.store) };
|
|
@@ -3676,6 +3786,8 @@ function startUserRetention(store, options) {
|
|
|
3676
3786
|
normalizeEmailSafe,
|
|
3677
3787
|
purgeSoftDeletedUsers,
|
|
3678
3788
|
requireAuth,
|
|
3789
|
+
resolveLocation,
|
|
3790
|
+
sanitizeLocation,
|
|
3679
3791
|
schemas,
|
|
3680
3792
|
startUserRetention,
|
|
3681
3793
|
toAdminUserDTO,
|