@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/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
- // A session the server revoked — today: a refresh token reused after its
288
- // grace (theft signal). { userId, sessionId, reason }.
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 id`,
1186
+ `DELETE FROM fonderie_sessions WHERE id = $1 AND user_id = $2 RETURNING sid`,
1181
1187
  [id, userId]
1182
1188
  );
1183
- return rows.length > 0;
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 id`,
1201
+ RETURNING sid`,
1192
1202
  [userId, keepSid]
1193
1203
  );
1194
- return rows.length;
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, sessionId: found.row.id, reason: "refresh-reuse" })
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.terminateById(ctx.user.id, id);
2508
- if (!removed) {
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 count = await sessions.terminateOthers(ctx.user.id, currentSid);
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.18.0";
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() {