@fonderie/auth 7.18.0 → 7.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brain/outcomes.md +1 -1
- package/brain/signatures.md +2 -1
- package/dist/index.cjs +45 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +43 -15
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +12 -4
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +12 -4
- package/dist/middlewares/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IUser } from './types.cjs';
|
|
2
2
|
export { IMfaChallenge, ISession } from './types.cjs';
|
|
3
|
-
import { IFonderieModule, IAdminDescription, IReadinessProblem, IFonderieApp, IDefaultTemplate, IAdminRoute } from '@fonderie/core';
|
|
3
|
+
import { IFonderieModule, IAdminDescription, IEventCatalogEntry, IReadinessProblem, IFonderieApp, IDefaultTemplate, IAdminRoute } from '@fonderie/core';
|
|
4
4
|
import { IStoreAdapter } from '@fonderie/store';
|
|
5
5
|
import { EventBus } from '@fonderie/events';
|
|
6
6
|
import { I as IAuthConfig, a as IRequestLocation } from './session-B-IgXFu8.cjs';
|
|
@@ -19,6 +19,7 @@ declare class AuthModule implements IFonderieModule {
|
|
|
19
19
|
readonly version: string;
|
|
20
20
|
constructor(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus | undefined);
|
|
21
21
|
describeAdmin(): IAdminDescription;
|
|
22
|
+
describeEvents(): IEventCatalogEntry[];
|
|
22
23
|
checkReadiness(): IReadinessProblem[];
|
|
23
24
|
install(app: IFonderieApp): void;
|
|
24
25
|
}
|
|
@@ -203,6 +204,6 @@ interface IAdminUserPageDTO {
|
|
|
203
204
|
nextCursor: string | null;
|
|
204
205
|
}
|
|
205
206
|
declare function toAdminUserPageDTO(page: IUserPage): IAdminUserPageDTO;
|
|
206
|
-
declare function describeAuthAdminRoutes(store: IStoreAdapter): IAdminRoute[];
|
|
207
|
+
declare function describeAuthAdminRoutes(store: IStoreAdapter, bus?: EventBus): IAdminRoute[];
|
|
207
208
|
|
|
208
209
|
export { AuthModule, type ChangePasswordInput, DEFAULT_TEMPLATES, type IAdminUserDTO, IAuthConfig, type IImportUser, type ILoginEventDTO, type ILoginHistoryPageDTO, type IPurgeOptions, IRequestLocation, type ISessionDTO, IUser, IUserDTO, type IUserRetentionScheduleOptions, type LoginInput, type RegisterInput, type ResetPasswordInput, describeAuthAdminRoutes, importUser, normalizeEmail, normalizeEmailSafe, purgeSoftDeletedUsers, schemas, startUserRetention, toAdminUserDTO, toAdminUserPageDTO, validateAuthConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IUser } from './types.js';
|
|
2
2
|
export { IMfaChallenge, ISession } from './types.js';
|
|
3
|
-
import { IFonderieModule, IAdminDescription, IReadinessProblem, IFonderieApp, IDefaultTemplate, IAdminRoute } from '@fonderie/core';
|
|
3
|
+
import { IFonderieModule, IAdminDescription, IEventCatalogEntry, IReadinessProblem, IFonderieApp, IDefaultTemplate, IAdminRoute } from '@fonderie/core';
|
|
4
4
|
import { IStoreAdapter } from '@fonderie/store';
|
|
5
5
|
import { EventBus } from '@fonderie/events';
|
|
6
6
|
import { I as IAuthConfig, a as IRequestLocation } from './session-B-IgXFu8.js';
|
|
@@ -19,6 +19,7 @@ declare class AuthModule implements IFonderieModule {
|
|
|
19
19
|
readonly version: string;
|
|
20
20
|
constructor(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus | undefined);
|
|
21
21
|
describeAdmin(): IAdminDescription;
|
|
22
|
+
describeEvents(): IEventCatalogEntry[];
|
|
22
23
|
checkReadiness(): IReadinessProblem[];
|
|
23
24
|
install(app: IFonderieApp): void;
|
|
24
25
|
}
|
|
@@ -203,6 +204,6 @@ interface IAdminUserPageDTO {
|
|
|
203
204
|
nextCursor: string | null;
|
|
204
205
|
}
|
|
205
206
|
declare function toAdminUserPageDTO(page: IUserPage): IAdminUserPageDTO;
|
|
206
|
-
declare function describeAuthAdminRoutes(store: IStoreAdapter): IAdminRoute[];
|
|
207
|
+
declare function describeAuthAdminRoutes(store: IStoreAdapter, bus?: EventBus): IAdminRoute[];
|
|
207
208
|
|
|
208
209
|
export { AuthModule, type ChangePasswordInput, DEFAULT_TEMPLATES, type IAdminUserDTO, IAuthConfig, type IImportUser, type ILoginEventDTO, type ILoginHistoryPageDTO, type IPurgeOptions, IRequestLocation, type ISessionDTO, IUser, IUserDTO, type IUserRetentionScheduleOptions, type LoginInput, type RegisterInput, type ResetPasswordInput, describeAuthAdminRoutes, importUser, normalizeEmail, normalizeEmailSafe, purgeSoftDeletedUsers, schemas, startUserRetention, toAdminUserDTO, toAdminUserPageDTO, validateAuthConfig };
|
package/dist/index.js
CHANGED
|
@@ -284,8 +284,10 @@ var EVENT_KEYS = {
|
|
|
284
284
|
userPurged: "fonderie.user.purged",
|
|
285
285
|
emailVerified: "fonderie.user.email_verified",
|
|
286
286
|
passwordChanged: "fonderie.user.password_changed",
|
|
287
|
-
//
|
|
288
|
-
//
|
|
287
|
+
// Sessions the server revoked: { userId, sids (null = all), reason }. Reaches
|
|
288
|
+
// the user's devices live (@fonderie/sse) so a revoked device signs out at
|
|
289
|
+
// once. Reasons: 'terminated' (signed out from the devices list),
|
|
290
|
+
// 'password-changed', 'admin', 'refresh-reuse' (theft signal).
|
|
289
291
|
sessionRevoked: "fonderie.session.revoked"
|
|
290
292
|
};
|
|
291
293
|
|
|
@@ -1176,22 +1178,30 @@ var SessionModel = class {
|
|
|
1176
1178
|
// Terminate one session by row id, scoped to its owner (a user can never
|
|
1177
1179
|
// delete another user's session). Returns whether a row was removed.
|
|
1178
1180
|
async terminateById(userId, id) {
|
|
1181
|
+
return await this.terminateByIdReturningSid(userId, id) !== void 0;
|
|
1182
|
+
}
|
|
1183
|
+
/** Terminate one of the user's sessions; its sid (null if unbound), or undefined if none was removed. */
|
|
1184
|
+
async terminateByIdReturningSid(userId, id) {
|
|
1179
1185
|
const rows = await this.store.query(
|
|
1180
|
-
`DELETE FROM fonderie_sessions WHERE id = $1 AND user_id = $2 RETURNING
|
|
1186
|
+
`DELETE FROM fonderie_sessions WHERE id = $1 AND user_id = $2 RETURNING sid`,
|
|
1181
1187
|
[id, userId]
|
|
1182
1188
|
);
|
|
1183
|
-
return rows.length
|
|
1189
|
+
return rows.length ? rows[0]?.sid ?? null : void 0;
|
|
1184
1190
|
}
|
|
1185
1191
|
// Terminate all of a user's sessions except the current one (by sid).
|
|
1186
1192
|
// Returns the number of sessions removed.
|
|
1187
1193
|
async terminateOthers(userId, keepSid) {
|
|
1194
|
+
return (await this.terminateOthersReturningSids(userId, keepSid)).length;
|
|
1195
|
+
}
|
|
1196
|
+
/** Terminate all of the user's sessions but `keepSid`; the sids removed. */
|
|
1197
|
+
async terminateOthersReturningSids(userId, keepSid) {
|
|
1188
1198
|
const rows = await this.store.query(
|
|
1189
1199
|
`DELETE FROM fonderie_sessions
|
|
1190
1200
|
WHERE user_id = $1 AND (sid IS DISTINCT FROM $2)
|
|
1191
|
-
RETURNING
|
|
1201
|
+
RETURNING sid`,
|
|
1192
1202
|
[userId, keepSid]
|
|
1193
1203
|
);
|
|
1194
|
-
return rows.
|
|
1204
|
+
return rows.map((r) => r.sid);
|
|
1195
1205
|
}
|
|
1196
1206
|
async exists(token) {
|
|
1197
1207
|
const rows = await this.store.query(
|
|
@@ -2066,7 +2076,7 @@ function authController(store, config, bus, locales = defineLocales()) {
|
|
|
2066
2076
|
if (found.kind === "reused") {
|
|
2067
2077
|
await sessions.revokeById(found.row.id);
|
|
2068
2078
|
await background3(
|
|
2069
|
-
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId,
|
|
2079
|
+
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sids: [found.row.sid], reason: "refresh-reuse" })
|
|
2070
2080
|
);
|
|
2071
2081
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2072
2082
|
}
|
|
@@ -2504,10 +2514,11 @@ function userController(store, config, bus) {
|
|
|
2504
2514
|
if (!id) {
|
|
2505
2515
|
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2506
2516
|
}
|
|
2507
|
-
const removed = await sessions.
|
|
2508
|
-
if (
|
|
2517
|
+
const removed = await sessions.terminateByIdReturningSid(ctx.user.id, id);
|
|
2518
|
+
if (removed === void 0) {
|
|
2509
2519
|
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Session not found");
|
|
2510
2520
|
}
|
|
2521
|
+
await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: [removed], reason: "terminated" }));
|
|
2511
2522
|
return setApiResponse4(HTTP4.OK, "SESSION_TERMINATED", "Session terminated.", { id });
|
|
2512
2523
|
},
|
|
2513
2524
|
terminateOtherSessions: async (ctx) => {
|
|
@@ -2519,7 +2530,9 @@ function userController(store, config, bus) {
|
|
|
2519
2530
|
"Cannot terminate others without a current session context"
|
|
2520
2531
|
);
|
|
2521
2532
|
}
|
|
2522
|
-
const
|
|
2533
|
+
const sids = await sessions.terminateOthersReturningSids(ctx.user.id, currentSid);
|
|
2534
|
+
const count = sids.length;
|
|
2535
|
+
if (count) await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids, reason: "terminated" }));
|
|
2523
2536
|
return setApiResponse4(HTTP4.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
|
|
2524
2537
|
},
|
|
2525
2538
|
updateProfile: async (ctx) => {
|
|
@@ -2681,6 +2694,7 @@ function userController(store, config, bus) {
|
|
|
2681
2694
|
const hash = await hashPassword2(newPassword);
|
|
2682
2695
|
await users.updatePassword(ctx.user.id, hash);
|
|
2683
2696
|
await sessions.deleteByUser(ctx.user.id);
|
|
2697
|
+
await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: null, reason: "password-changed" }));
|
|
2684
2698
|
return setApiResponse4(HTTP4.OK, "PASSWORD_CHANGED", "Password updated successfully.");
|
|
2685
2699
|
},
|
|
2686
2700
|
/**
|
|
@@ -3575,6 +3589,7 @@ function extractToken(request) {
|
|
|
3575
3589
|
|
|
3576
3590
|
// src/admin.ts
|
|
3577
3591
|
import { HTTP as HTTP7, encodeKeysetCursor as encodeKeysetCursor2, setApiResponse as setApiResponse7 } from "@fonderie/core";
|
|
3592
|
+
import { background as background6 } from "@fonderie/core";
|
|
3578
3593
|
function toAdminUserDTO(user) {
|
|
3579
3594
|
return {
|
|
3580
3595
|
...toUserDTO(user),
|
|
@@ -3591,7 +3606,7 @@ function toAdminUserPageDTO(page) {
|
|
|
3591
3606
|
var ADMIN_PREFIX = "/_admin";
|
|
3592
3607
|
var idOf = (ctx) => ctx.meta.params?.["id"] ?? "";
|
|
3593
3608
|
var NOT_FOUND = () => setApiResponse7(HTTP7.NOT_FOUND, "NOT_FOUND", "No such user");
|
|
3594
|
-
function adminRouteTable(store) {
|
|
3609
|
+
function adminRouteTable(store, bus) {
|
|
3595
3610
|
const users = new UserModel(store);
|
|
3596
3611
|
const sessions = new SessionModel(store);
|
|
3597
3612
|
const events = new LoginEventModel(store);
|
|
@@ -3664,6 +3679,7 @@ function adminRouteTable(store) {
|
|
|
3664
3679
|
async (ctx) => {
|
|
3665
3680
|
if (!await users.findById(idOf(ctx))) return NOT_FOUND();
|
|
3666
3681
|
await sessions.deleteByUser(idOf(ctx));
|
|
3682
|
+
await background6(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: idOf(ctx), sids: null, reason: "admin" }));
|
|
3667
3683
|
return setApiResponse7(HTTP7.OK, "SESSIONS_REVOKED", "All sessions revoked");
|
|
3668
3684
|
}
|
|
3669
3685
|
],
|
|
@@ -3696,8 +3712,8 @@ function adminRouteTable(store) {
|
|
|
3696
3712
|
["POST", "/_admin/users/:id/unsuspend", setSuspended(false)]
|
|
3697
3713
|
];
|
|
3698
3714
|
}
|
|
3699
|
-
function describeAuthAdminRoutes(store) {
|
|
3700
|
-
return adminRouteTable(store).map(([method, path, h]) => ({
|
|
3715
|
+
function describeAuthAdminRoutes(store, bus) {
|
|
3716
|
+
return adminRouteTable(store, bus).map(([method, path, h]) => ({
|
|
3701
3717
|
method,
|
|
3702
3718
|
path: path.slice(ADMIN_PREFIX.length),
|
|
3703
3719
|
handlers: [h]
|
|
@@ -3718,10 +3734,22 @@ var AuthModule = class {
|
|
|
3718
3734
|
name = "@fonderie/auth";
|
|
3719
3735
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3720
3736
|
// say what is actually deployed rather than 'not reported'.
|
|
3721
|
-
version = "7.
|
|
3737
|
+
version = "7.19.0";
|
|
3722
3738
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3723
3739
|
describeAdmin() {
|
|
3724
|
-
return { routes: describeAuthAdminRoutes(this.store) };
|
|
3740
|
+
return { routes: describeAuthAdminRoutes(this.store, this.bus) };
|
|
3741
|
+
}
|
|
3742
|
+
// Live sign-out (docs/SESSION-DESIGN.md, Phase 5): a revoked device hears it
|
|
3743
|
+
// at once over @fonderie/sse — only the user it is about, ids only.
|
|
3744
|
+
describeEvents() {
|
|
3745
|
+
const revoked = {
|
|
3746
|
+
type: EVENT_KEYS.sessionRevoked,
|
|
3747
|
+
description: "Some of your sessions were signed out \u2014 sign out if this device is one of them",
|
|
3748
|
+
audience: "user",
|
|
3749
|
+
scope: (p) => ({ userId: p.userId }),
|
|
3750
|
+
project: (p) => ({ sids: p.sids, reason: p.reason })
|
|
3751
|
+
};
|
|
3752
|
+
return [revoked];
|
|
3725
3753
|
}
|
|
3726
3754
|
// Report config problems for app.checkProductionReadiness() (data, not throw).
|
|
3727
3755
|
checkReadiness() {
|