@fonderie/auth 7.11.1 → 7.12.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 +2 -2
- package/brain/signatures.md +4 -1
- package/dist/index.cjs +29 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +29 -6
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +11 -1
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +11 -1
- package/dist/middlewares/index.js.map +1 -1
- package/package.json +1 -1
package/brain/outcomes.md
CHANGED
|
@@ -138,8 +138,8 @@ Raw SQL ships in `node_modules/@fonderie/auth/dist/migrations/sql/` — read it
|
|
|
138
138
|
|
|
139
139
|
| Method | Path | Middleware chain (auth / validation / handler) |
|
|
140
140
|
|---|---|---|
|
|
141
|
-
| GET | `/_admin/users` | `async (ctx) => { const params = new URL(ctx.request.url).searchParams; const email = params.get('email')?.trim().toLowerCase(); if (email) { const user = await users.findByEmail(email); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); } const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const cursorParam = params.get('cursor'); const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null; if (cursorParam && !cursor) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor'); const page = await users.list({ limit, ...(cursor ? { cursor } : {}) }); return setApiResponse(HTTP.OK, 'USERS', 'Users', toAdminUserPageDTO(page)); }` |
|
|
142
|
-
| GET | `/_admin/users/:id` | `async (ctx) => { const user = await users.
|
|
141
|
+
| GET | `/_admin/users` | `async (ctx) => { const params = new URL(ctx.request.url).searchParams; const email = params.get('email')?.trim().toLowerCase(); if (email) { const user = await users.findByEmail(email); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); } const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const cursorParam = params.get('cursor'); const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null; if (cursorParam && !cursor) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor'); const deleted = params.get('deleted') === '1' || params.get('deleted') === 'true'; const page = await users.list({ limit, ...(cursor ? { cursor } : {}), ...(deleted ? { deleted } : {}), }); return setApiResponse(HTTP.OK, 'USERS', 'Users', toAdminUserPageDTO(page)); }` |
|
|
142
|
+
| GET | `/_admin/users/:id` | `async (ctx) => { // Read-only view: a soft-deleted account resolves (deletedAt set), so // an operator arriving from billing sees what happened to it. const user = await users.findByIdIncludingDeleted(idOf(ctx)); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); }` |
|
|
143
143
|
| GET | `/_admin/users/:id/login-history` | `async (ctx) => { if (!(await users.findById(idOf(ctx)))) return NOT_FOUND(); const params = new URL(ctx.request.url).searchParams; const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const cursorParam = params.get('cursor'); const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null; if (cursorParam && !cursor) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor'); const page = await events.listByUser({ userId: idOf(ctx), limit, ...(cursor ? { cursor } : {}), }); return setApiResponse( HTTP.OK, 'LOGIN_HISTORY', 'Login history', toLoginHistoryPageDTO(page), ); }` |
|
|
144
144
|
| DELETE | `/_admin/users/:id/sessions` | `async (ctx) => { if (!(await users.findById(idOf(ctx)))) return NOT_FOUND(); await sessions.deleteByUser(idOf(ctx)); return setApiResponse(HTTP.OK, 'SESSIONS_REVOKED', 'All sessions revoked'); }` |
|
|
145
145
|
| GET | `/_admin/users/:id/sessions` | `async (ctx) => { if (!(await users.findById(idOf(ctx)))) return NOT_FOUND(); const rows = await sessions.listLiveByUser(idOf(ctx)); return setApiResponse( HTTP.OK, 'SESSIONS', 'Live sessions', rows.map((r) => toSessionDTO(r, null)), ); }` |
|
package/brain/signatures.md
CHANGED
|
@@ -213,12 +213,15 @@ function normalizeEmailSafe(email: string): string | null
|
|
|
213
213
|
|
|
214
214
|
function importUser(store: IStoreAdapter, user: IImportUser): Promise<{ id: string; }>
|
|
215
215
|
|
|
216
|
-
function purgeSoftDeletedUsers(store: IStoreAdapter, { olderThanDays }: IPurgeOptions): Promise<number>
|
|
216
|
+
function purgeSoftDeletedUsers(store: IStoreAdapter, { olderThanDays, bus }: IPurgeOptions): Promise<number>
|
|
217
217
|
|
|
218
218
|
function startUserRetention(store: IStoreAdapter, options: IUserRetentionScheduleOptions): { stop: () => void; }
|
|
219
219
|
|
|
220
220
|
interface IPurgeOptions {
|
|
221
221
|
olderThanDays: number;
|
|
222
|
+
bus?: {
|
|
223
|
+
emit(type: string, payload: unknown): Promise<void>;
|
|
224
|
+
} | undefined;
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
interface IUserRetentionScheduleOptions extends IPurgeOptions {
|
package/dist/index.cjs
CHANGED
|
@@ -320,6 +320,8 @@ var MESSAGE_KEYS = {
|
|
|
320
320
|
var EVENT_KEYS = {
|
|
321
321
|
userRegistered: "fonderie.user.registered",
|
|
322
322
|
userDeleted: "fonderie.user.deleted",
|
|
323
|
+
// Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.
|
|
324
|
+
userPurged: "fonderie.user.purged",
|
|
323
325
|
emailVerified: "fonderie.user.email_verified",
|
|
324
326
|
passwordChanged: "fonderie.user.password_changed"
|
|
325
327
|
};
|
|
@@ -593,7 +595,7 @@ var UserModel = class {
|
|
|
593
595
|
async list(query = {}) {
|
|
594
596
|
const limit = Math.min(query.limit ?? 50, MAX_LIST_LIMIT);
|
|
595
597
|
const params = [];
|
|
596
|
-
const where = ["deleted_at IS NULL"];
|
|
598
|
+
const where = [query.deleted ? "deleted_at IS NOT NULL" : "deleted_at IS NULL"];
|
|
597
599
|
if (query.cursor) {
|
|
598
600
|
params.push(query.cursor.createdAt, query.cursor.id);
|
|
599
601
|
where.push(`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`);
|
|
@@ -609,6 +611,16 @@ var UserModel = class {
|
|
|
609
611
|
);
|
|
610
612
|
return { users: rows.slice(0, limit), hasMore: rows.length > limit };
|
|
611
613
|
}
|
|
614
|
+
// For operator views only: a soft-deleted account still resolves, with
|
|
615
|
+
// deletedAt set, so the console can say "deleted on …" instead of "no such
|
|
616
|
+
// user". Nothing that authenticates or acts on an account may use this.
|
|
617
|
+
async findByIdIncludingDeleted(id) {
|
|
618
|
+
const [row] = await this.store.query(
|
|
619
|
+
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1`,
|
|
620
|
+
[id]
|
|
621
|
+
);
|
|
622
|
+
return row ?? null;
|
|
623
|
+
}
|
|
612
624
|
async findById(id) {
|
|
613
625
|
const [row] = await this.store.query(
|
|
614
626
|
`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,
|
|
@@ -3426,7 +3438,12 @@ function adminRouteTable(store) {
|
|
|
3426
3438
|
const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
|
|
3427
3439
|
if (cursorParam && !cursor)
|
|
3428
3440
|
return (0, import_core12.setApiResponse)(import_core12.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
|
|
3429
|
-
const
|
|
3441
|
+
const deleted = params.get("deleted") === "1" || params.get("deleted") === "true";
|
|
3442
|
+
const page = await users.list({
|
|
3443
|
+
limit,
|
|
3444
|
+
...cursor ? { cursor } : {},
|
|
3445
|
+
...deleted ? { deleted } : {}
|
|
3446
|
+
});
|
|
3430
3447
|
return (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USERS", "Users", toAdminUserPageDTO(page));
|
|
3431
3448
|
}
|
|
3432
3449
|
],
|
|
@@ -3434,7 +3451,7 @@ function adminRouteTable(store) {
|
|
|
3434
3451
|
"GET",
|
|
3435
3452
|
"/_admin/users/:id",
|
|
3436
3453
|
async (ctx) => {
|
|
3437
|
-
const user = await users.
|
|
3454
|
+
const user = await users.findByIdIncludingDeleted(idOf(ctx));
|
|
3438
3455
|
return user ? (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
|
|
3439
3456
|
}
|
|
3440
3457
|
],
|
|
@@ -3513,7 +3530,7 @@ var AuthModule = class {
|
|
|
3513
3530
|
name = "@fonderie/auth";
|
|
3514
3531
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3515
3532
|
// say what is actually deployed rather than 'not reported'.
|
|
3516
|
-
version = "7.
|
|
3533
|
+
version = "7.12.0";
|
|
3517
3534
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3518
3535
|
describeAdmin() {
|
|
3519
3536
|
return { routes: describeAuthAdminRoutes(this.store) };
|
|
@@ -3744,7 +3761,7 @@ async function importUser(store, user) {
|
|
|
3744
3761
|
}
|
|
3745
3762
|
|
|
3746
3763
|
// src/services/retention.ts
|
|
3747
|
-
async function purgeSoftDeletedUsers(store, { olderThanDays }) {
|
|
3764
|
+
async function purgeSoftDeletedUsers(store, { olderThanDays, bus }) {
|
|
3748
3765
|
if (!Number.isFinite(olderThanDays) || olderThanDays < 0) {
|
|
3749
3766
|
throw new Error("[auth] purgeSoftDeletedUsers: olderThanDays must be a non-negative number");
|
|
3750
3767
|
}
|
|
@@ -3755,6 +3772,9 @@ async function purgeSoftDeletedUsers(store, { olderThanDays }) {
|
|
|
3755
3772
|
RETURNING id`,
|
|
3756
3773
|
[olderThanDays]
|
|
3757
3774
|
);
|
|
3775
|
+
if (bus) {
|
|
3776
|
+
for (const { id } of rows) await bus.emit("fonderie.user.purged", { userId: id });
|
|
3777
|
+
}
|
|
3758
3778
|
return rows.length;
|
|
3759
3779
|
}
|
|
3760
3780
|
function startUserRetention(store, options) {
|
|
@@ -3763,7 +3783,10 @@ function startUserRetention(store, options) {
|
|
|
3763
3783
|
const run = async () => {
|
|
3764
3784
|
if (stopped) return;
|
|
3765
3785
|
try {
|
|
3766
|
-
const deleted = await purgeSoftDeletedUsers(store, {
|
|
3786
|
+
const deleted = await purgeSoftDeletedUsers(store, {
|
|
3787
|
+
olderThanDays: options.olderThanDays,
|
|
3788
|
+
...options.bus ? { bus: options.bus } : {}
|
|
3789
|
+
});
|
|
3767
3790
|
options.onPurge?.(deleted);
|
|
3768
3791
|
} catch (err) {
|
|
3769
3792
|
console.error("[auth] scheduled user retention purge failed:", err);
|