@pramen/auth 0.0.37 → 0.0.38
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.ts +19 -6
- package/dist/index.js +63 -11
- package/package.json +2 -2
- package/src/index.ts +66 -11
package/dist/index.d.ts
CHANGED
|
@@ -37,8 +37,8 @@ export declare function verifyPassword(password: string, stored: string): Promis
|
|
|
37
37
|
export declare function signToken(claims: Record<string, unknown>, secret: string, opts?: {
|
|
38
38
|
ttlSeconds?: number;
|
|
39
39
|
}): Promise<string>;
|
|
40
|
-
/** signup / login / me. Roles are assigned server-side (default `["user"]`)
|
|
41
|
-
* client never picks its own roles. Spread into your handler map. */
|
|
40
|
+
/** signup / login / me / refreshSession. Roles are assigned server-side (default `["user"]`)
|
|
41
|
+
* — the client never picks its own roles. Spread into your handler map. */
|
|
42
42
|
export declare const authHandlers: {
|
|
43
43
|
signup: import("@pramen/server").Handler<{
|
|
44
44
|
username: string;
|
|
@@ -63,6 +63,13 @@ export declare const authHandlers: {
|
|
|
63
63
|
};
|
|
64
64
|
}>;
|
|
65
65
|
me: import("@pramen/server").Handler<unknown, import("@pramen/server").Identity | null>;
|
|
66
|
+
refreshSession: import("@pramen/server").Handler<unknown, {
|
|
67
|
+
token: string;
|
|
68
|
+
user: {
|
|
69
|
+
username: string;
|
|
70
|
+
roles: string[];
|
|
71
|
+
};
|
|
72
|
+
}>;
|
|
66
73
|
};
|
|
67
74
|
export declare const magicLinkSchema: {
|
|
68
75
|
auth_magic_links: import("@pramen/server").EntityDef<{
|
|
@@ -180,8 +187,11 @@ export declare function createUserHandlers(opts?: {
|
|
|
180
187
|
username: string;
|
|
181
188
|
roles: string[];
|
|
182
189
|
}, Record<string, unknown>>;
|
|
183
|
-
/** Admin: activate / deactivate a user. Deactivating blocks future logins
|
|
184
|
-
*
|
|
190
|
+
/** Admin: activate / deactivate a user. Deactivating blocks future logins AND
|
|
191
|
+
* refreshSession, AND revokes OUTSTANDING tokens immediately via the KV denylist (the
|
|
192
|
+
* Worker fails them closed) — so revocation no longer waits out the token TTL. The
|
|
193
|
+
* denylist entry self-expires at the session TTL. Reactivating LIFTS the entry (it is
|
|
194
|
+
* username-scoped, so a stale entry would otherwise lock out even a fresh login). */
|
|
185
195
|
setUserActive: import("@pramen/server").Handler<{
|
|
186
196
|
username: string;
|
|
187
197
|
active: boolean;
|
|
@@ -230,8 +240,11 @@ export declare const userHandlers: {
|
|
|
230
240
|
username: string;
|
|
231
241
|
roles: string[];
|
|
232
242
|
}, Record<string, unknown>>;
|
|
233
|
-
/** Admin: activate / deactivate a user. Deactivating blocks future logins
|
|
234
|
-
*
|
|
243
|
+
/** Admin: activate / deactivate a user. Deactivating blocks future logins AND
|
|
244
|
+
* refreshSession, AND revokes OUTSTANDING tokens immediately via the KV denylist (the
|
|
245
|
+
* Worker fails them closed) — so revocation no longer waits out the token TTL. The
|
|
246
|
+
* denylist entry self-expires at the session TTL. Reactivating LIFTS the entry (it is
|
|
247
|
+
* username-scoped, so a stale entry would otherwise lock out even a fresh login). */
|
|
235
248
|
setUserActive: import("@pramen/server").Handler<{
|
|
236
249
|
username: string;
|
|
237
250
|
active: boolean;
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Passwordless magic-link login is also available via createMagicLinkAuth (spread
|
|
16
16
|
// magicLinkSchema too). It is transport-agnostic: you supply sendEmail; pramen owns
|
|
17
17
|
// the token lifecycle. See createMagicLinkAuth below.
|
|
18
|
-
import { Entity, mutation, query, defaultTo, unique, hidden, policy, allow, $identity, BadRequest, Unauthorized } from "@pramen/server";
|
|
18
|
+
import { Entity, mutation, query, defaultTo, unique, hidden, policy, allow, $identity, BadRequest, Unauthorized, denySession, allowSession } from "@pramen/server";
|
|
19
19
|
// --- schema fragment: spread into your defineSchema so the table is migrated ---
|
|
20
20
|
export const authSchema = {
|
|
21
21
|
auth_users: Entity((t) => ({
|
|
@@ -146,8 +146,35 @@ function parseCreds(raw) {
|
|
|
146
146
|
}
|
|
147
147
|
return { username: o.username, password: o.password, email };
|
|
148
148
|
}
|
|
149
|
-
/**
|
|
150
|
-
*
|
|
149
|
+
/** Build the `refreshSession` handler: an AUTHENTICATED mutation that re-reads the caller's
|
|
150
|
+
* `roles` + `active` from the users table (keyed on `username` = the JWT `sub`) and reissues a
|
|
151
|
+
* fresh token with the configured session TTL — the same `{ token, user }` shape as `login`.
|
|
152
|
+
* Throws Unauthorized if the row is gone or the account is deactivated (the `isActive` helper).
|
|
153
|
+
*
|
|
154
|
+
* Why it exists: the core is stateless verify-only, so roles/active are baked into a token at
|
|
155
|
+
* login. refreshSession lets a client (1) silently refresh at ~half-TTL, so AUTH_SESSION_TTL_SECONDS
|
|
156
|
+
* can be kept SHORT (bounded revocation lag) without logging the user out; and (2) pick up a role
|
|
157
|
+
* GRANT immediately — e.g. right after a subscription checkout flips the role — with no re-login.
|
|
158
|
+
* Shared by password users (`authHandlers`) and magic-link users (`createMagicLinkAuth`): both
|
|
159
|
+
* store the row in the same authSchema-shaped table keyed on the immutable `username`. `ttlOf`
|
|
160
|
+
* supplies each factory's own configured TTL. */
|
|
161
|
+
function buildRefreshSession(ttlOf, table = "auth_users") {
|
|
162
|
+
return mutation(async (ctx) => {
|
|
163
|
+
const userId = requireUserId(ctx);
|
|
164
|
+
const rows = await ctx.db.exec(`SELECT username, roles, active FROM ${table} WHERE username = ? LIMIT 1`, userId);
|
|
165
|
+
const u = rows[0];
|
|
166
|
+
// Gone or deactivated ⇒ no fresh token (mirrors login). The Worker denylist already
|
|
167
|
+
// fails a deactivated user's outstanding token closed; this ensures refresh can't
|
|
168
|
+
// launder a revoked session into a new, longer-lived one either.
|
|
169
|
+
if (!u || !isActive(u.active))
|
|
170
|
+
throw new Unauthorized("session is no longer valid");
|
|
171
|
+
const roles = JSON.parse(String(u.roles));
|
|
172
|
+
const token = await signToken({ sub: String(u.username), roles }, secretOf(ctx), { ttlSeconds: ttlOf(ctx) });
|
|
173
|
+
return { token, user: { username: String(u.username), roles } };
|
|
174
|
+
}, { auth: "authenticated" });
|
|
175
|
+
}
|
|
176
|
+
/** signup / login / me / refreshSession. Roles are assigned server-side (default `["user"]`)
|
|
177
|
+
* — the client never picks its own roles. Spread into your handler map. */
|
|
151
178
|
export const authHandlers = {
|
|
152
179
|
// NOTE on username enumeration: signup returns a distinct "username is taken" error,
|
|
153
180
|
// which is an enumeration oracle. This is INHERENT to systems where the username is a
|
|
@@ -203,6 +230,10 @@ export const authHandlers = {
|
|
|
203
230
|
return { token, user: { username: String(u.username), roles } };
|
|
204
231
|
}, { input: parseCreds }),
|
|
205
232
|
me: query((ctx) => ctx.identity),
|
|
233
|
+
// Re-read roles/active for the caller and reissue a token at the env-configured session
|
|
234
|
+
// TTL (AUTH_SESSION_TTL_SECONDS). Lets a short TTL bound revocation lag without logging
|
|
235
|
+
// the user out, and picks up role grants without re-login. See buildRefreshSession.
|
|
236
|
+
refreshSession: buildRefreshSession(sessionTtlOf),
|
|
206
237
|
};
|
|
207
238
|
// --- magic link (passwordless) login ---------------------------------------
|
|
208
239
|
//
|
|
@@ -291,6 +322,10 @@ export function createMagicLinkAuth(opts) {
|
|
|
291
322
|
const sessionTtl = opts.sessionTtlSeconds ?? TOKEN_TTL_SECONDS;
|
|
292
323
|
const defaultRoles = opts.defaultRoles ?? DEFAULT_ROLES;
|
|
293
324
|
const handlers = {
|
|
325
|
+
// Silent token refresh for magic-link users (same table, keyed on username). Reissues
|
|
326
|
+
// at this factory's configured session TTL. Shared implementation with authHandlers —
|
|
327
|
+
// when both are spread into one app, either definition serves either user.
|
|
328
|
+
refreshSession: buildRefreshSession(() => sessionTtl),
|
|
294
329
|
/** Admin-only: create a passwordless user with the given roles (defaults if omitted)
|
|
295
330
|
* and email them a fresh magic link. Idempotent — inviting an existing user just
|
|
296
331
|
* resends the link and leaves their roles alone (admin uses setUserRoles for changes).
|
|
@@ -381,12 +416,19 @@ export function createMagicLinkAuth(opts) {
|
|
|
381
416
|
// roles (admin manages everyone; the authenticated user manages only itself). Because
|
|
382
417
|
// the admin read policy restricts `fields`, `passwordHash` is never projected back.
|
|
383
418
|
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
419
|
+
// Roles are baked into the JWT at login, so the core is stateless verify-only. Two
|
|
420
|
+
// mechanisms close the gap that leaves, without a session store:
|
|
421
|
+
// - `refreshSession` (authHandlers / createMagicLinkAuth): an authenticated caller
|
|
422
|
+
// re-reads roles/active and gets a fresh token. A client refreshing at ~half-TTL lets
|
|
423
|
+
// AUTH_SESSION_TTL_SECONDS (default 3600) stay short — bounding how long a stale
|
|
424
|
+
// setUserRoles/setUserActive lingers — and picks up a role GRANT immediately (no re-login).
|
|
425
|
+
// - KV denylist (HARD revocation, independent of TTL): setUserActive(false) and deleteUser
|
|
426
|
+
// write an `authDenied:<username>` entry via `denySession(ctx.kv, …)`; the core Worker
|
|
427
|
+
// checks `isSessionDenied` right after resolving identity and fails a revoked token closed
|
|
428
|
+
// (401) — so a deactivate/delete takes effect on the NEXT request, not the next login. The
|
|
429
|
+
// entry self-expires at the session TTL (the list never grows); reactivation lifts it
|
|
430
|
+
// (`allowSession`). `denySession`/`allowSession`/`isSessionDenied` are exported from
|
|
431
|
+
// @pramen/server so an app can revoke on its own compromise signals too.
|
|
390
432
|
/** SQLite has no bool: `active` is stored 0/1 (NULL on a pre-column row = active). */
|
|
391
433
|
function isActive(v) {
|
|
392
434
|
return v == null || Number(v) !== 0;
|
|
@@ -435,8 +477,11 @@ export function createUserHandlers(opts = {}) {
|
|
|
435
477
|
throw new BadRequest("user not found"); // (or out of the caller's update scope)
|
|
436
478
|
return updated;
|
|
437
479
|
}),
|
|
438
|
-
/** Admin: activate / deactivate a user. Deactivating blocks future logins
|
|
439
|
-
*
|
|
480
|
+
/** Admin: activate / deactivate a user. Deactivating blocks future logins AND
|
|
481
|
+
* refreshSession, AND revokes OUTSTANDING tokens immediately via the KV denylist (the
|
|
482
|
+
* Worker fails them closed) — so revocation no longer waits out the token TTL. The
|
|
483
|
+
* denylist entry self-expires at the session TTL. Reactivating LIFTS the entry (it is
|
|
484
|
+
* username-scoped, so a stale entry would otherwise lock out even a fresh login). */
|
|
440
485
|
setUserActive: mutation(async (ctx, input) => {
|
|
441
486
|
if (typeof input?.username !== "string" || input.username.length === 0)
|
|
442
487
|
throw new BadRequest("username is required");
|
|
@@ -448,6 +493,11 @@ export function createUserHandlers(opts = {}) {
|
|
|
448
493
|
const updated = await usersDb(ctx).update(table, input.username, { active: input.active });
|
|
449
494
|
if (!updated)
|
|
450
495
|
throw new BadRequest("user not found");
|
|
496
|
+
// KV is not part of the mutation's transaction — do it after the update succeeds.
|
|
497
|
+
if (input.active === false)
|
|
498
|
+
await denySession(ctx.kv, input.username, sessionTtlOf(ctx));
|
|
499
|
+
else
|
|
500
|
+
await allowSession(ctx.kv, input.username);
|
|
451
501
|
return updated;
|
|
452
502
|
}),
|
|
453
503
|
/** Admin: permanently delete a user. ACL-gated by the admin delete policy; a caller
|
|
@@ -460,6 +510,8 @@ export function createUserHandlers(opts = {}) {
|
|
|
460
510
|
const deleted = await usersDb(ctx).delete(table, input.username);
|
|
461
511
|
if (!deleted)
|
|
462
512
|
throw new BadRequest("user not found");
|
|
513
|
+
// Revoke any outstanding tokens for the now-deleted user (self-expires at the TTL).
|
|
514
|
+
await denySession(ctx.kv, input.username, sessionTtlOf(ctx));
|
|
463
515
|
return { ok: true };
|
|
464
516
|
}),
|
|
465
517
|
/** Self-service: change the caller's contact email. The ACL self policy scopes the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"description": "Optional credential→JWT login for pramen — signup/login/me + PBKDF2 hashing, issuing HS256 tokens the pramen verifier accepts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pramen/server": "0.0.
|
|
37
|
+
"@pramen/server": "0.0.38"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// magicLinkSchema too). It is transport-agnostic: you supply sendEmail; pramen owns
|
|
17
17
|
// the token lifecycle. See createMagicLinkAuth below.
|
|
18
18
|
|
|
19
|
-
import { Entity, mutation, query, defaultTo, unique, hidden, policy, allow, $identity, BadRequest, Unauthorized } from "@pramen/server";
|
|
19
|
+
import { Entity, mutation, query, defaultTo, unique, hidden, policy, allow, $identity, BadRequest, Unauthorized, denySession, allowSession } from "@pramen/server";
|
|
20
20
|
import type { AppTaskMap, HandlerContext, HandlerMap, Policy } from "@pramen/server";
|
|
21
21
|
|
|
22
22
|
// --- schema fragment: spread into your defineSchema so the table is migrated ---
|
|
@@ -169,8 +169,38 @@ function parseCreds(raw: unknown): { username: string; password: string; email?:
|
|
|
169
169
|
return { username: o.username, password: o.password, email };
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
/**
|
|
173
|
-
*
|
|
172
|
+
/** Build the `refreshSession` handler: an AUTHENTICATED mutation that re-reads the caller's
|
|
173
|
+
* `roles` + `active` from the users table (keyed on `username` = the JWT `sub`) and reissues a
|
|
174
|
+
* fresh token with the configured session TTL — the same `{ token, user }` shape as `login`.
|
|
175
|
+
* Throws Unauthorized if the row is gone or the account is deactivated (the `isActive` helper).
|
|
176
|
+
*
|
|
177
|
+
* Why it exists: the core is stateless verify-only, so roles/active are baked into a token at
|
|
178
|
+
* login. refreshSession lets a client (1) silently refresh at ~half-TTL, so AUTH_SESSION_TTL_SECONDS
|
|
179
|
+
* can be kept SHORT (bounded revocation lag) without logging the user out; and (2) pick up a role
|
|
180
|
+
* GRANT immediately — e.g. right after a subscription checkout flips the role — with no re-login.
|
|
181
|
+
* Shared by password users (`authHandlers`) and magic-link users (`createMagicLinkAuth`): both
|
|
182
|
+
* store the row in the same authSchema-shaped table keyed on the immutable `username`. `ttlOf`
|
|
183
|
+
* supplies each factory's own configured TTL. */
|
|
184
|
+
function buildRefreshSession(ttlOf: (ctx: HandlerContext) => number, table = "auth_users") {
|
|
185
|
+
return mutation(
|
|
186
|
+
async (ctx) => {
|
|
187
|
+
const userId = requireUserId(ctx);
|
|
188
|
+
const rows = await ctx.db.exec(`SELECT username, roles, active FROM ${table} WHERE username = ? LIMIT 1`, userId);
|
|
189
|
+
const u = rows[0];
|
|
190
|
+
// Gone or deactivated ⇒ no fresh token (mirrors login). The Worker denylist already
|
|
191
|
+
// fails a deactivated user's outstanding token closed; this ensures refresh can't
|
|
192
|
+
// launder a revoked session into a new, longer-lived one either.
|
|
193
|
+
if (!u || !isActive(u.active)) throw new Unauthorized("session is no longer valid");
|
|
194
|
+
const roles = JSON.parse(String(u.roles)) as string[];
|
|
195
|
+
const token = await signToken({ sub: String(u.username), roles }, secretOf(ctx), { ttlSeconds: ttlOf(ctx) });
|
|
196
|
+
return { token, user: { username: String(u.username), roles } };
|
|
197
|
+
},
|
|
198
|
+
{ auth: "authenticated" },
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** signup / login / me / refreshSession. Roles are assigned server-side (default `["user"]`)
|
|
203
|
+
* — the client never picks its own roles. Spread into your handler map. */
|
|
174
204
|
export const authHandlers = {
|
|
175
205
|
// NOTE on username enumeration: signup returns a distinct "username is taken" error,
|
|
176
206
|
// which is an enumeration oracle. This is INHERENT to systems where the username is a
|
|
@@ -242,6 +272,11 @@ export const authHandlers = {
|
|
|
242
272
|
),
|
|
243
273
|
|
|
244
274
|
me: query((ctx) => ctx.identity),
|
|
275
|
+
|
|
276
|
+
// Re-read roles/active for the caller and reissue a token at the env-configured session
|
|
277
|
+
// TTL (AUTH_SESSION_TTL_SECONDS). Lets a short TTL bound revocation lag without logging
|
|
278
|
+
// the user out, and picks up role grants without re-login. See buildRefreshSession.
|
|
279
|
+
refreshSession: buildRefreshSession(sessionTtlOf),
|
|
245
280
|
};
|
|
246
281
|
|
|
247
282
|
// --- magic link (passwordless) login ---------------------------------------
|
|
@@ -357,6 +392,11 @@ export function createMagicLinkAuth(opts: MagicLinkOptions): { handlers: Handler
|
|
|
357
392
|
const defaultRoles = opts.defaultRoles ?? DEFAULT_ROLES;
|
|
358
393
|
|
|
359
394
|
const handlers: HandlerMap = {
|
|
395
|
+
// Silent token refresh for magic-link users (same table, keyed on username). Reissues
|
|
396
|
+
// at this factory's configured session TTL. Shared implementation with authHandlers —
|
|
397
|
+
// when both are spread into one app, either definition serves either user.
|
|
398
|
+
refreshSession: buildRefreshSession(() => sessionTtl),
|
|
399
|
+
|
|
360
400
|
/** Admin-only: create a passwordless user with the given roles (defaults if omitted)
|
|
361
401
|
* and email them a fresh magic link. Idempotent — inviting an existing user just
|
|
362
402
|
* resends the link and leaves their roles alone (admin uses setUserRoles for changes).
|
|
@@ -489,12 +529,19 @@ export function createMagicLinkAuth(opts: MagicLinkOptions): { handlers: Handler
|
|
|
489
529
|
// roles (admin manages everyone; the authenticated user manages only itself). Because
|
|
490
530
|
// the admin read policy restricts `fields`, `passwordHash` is never projected back.
|
|
491
531
|
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
532
|
+
// Roles are baked into the JWT at login, so the core is stateless verify-only. Two
|
|
533
|
+
// mechanisms close the gap that leaves, without a session store:
|
|
534
|
+
// - `refreshSession` (authHandlers / createMagicLinkAuth): an authenticated caller
|
|
535
|
+
// re-reads roles/active and gets a fresh token. A client refreshing at ~half-TTL lets
|
|
536
|
+
// AUTH_SESSION_TTL_SECONDS (default 3600) stay short — bounding how long a stale
|
|
537
|
+
// setUserRoles/setUserActive lingers — and picks up a role GRANT immediately (no re-login).
|
|
538
|
+
// - KV denylist (HARD revocation, independent of TTL): setUserActive(false) and deleteUser
|
|
539
|
+
// write an `authDenied:<username>` entry via `denySession(ctx.kv, …)`; the core Worker
|
|
540
|
+
// checks `isSessionDenied` right after resolving identity and fails a revoked token closed
|
|
541
|
+
// (401) — so a deactivate/delete takes effect on the NEXT request, not the next login. The
|
|
542
|
+
// entry self-expires at the session TTL (the list never grows); reactivation lifts it
|
|
543
|
+
// (`allowSession`). `denySession`/`allowSession`/`isSessionDenied` are exported from
|
|
544
|
+
// @pramen/server so an app can revoke on its own compromise signals too.
|
|
498
545
|
|
|
499
546
|
/** SQLite has no bool: `active` is stored 0/1 (NULL on a pre-column row = active). */
|
|
500
547
|
function isActive(v: unknown): boolean {
|
|
@@ -553,8 +600,11 @@ export function createUserHandlers(opts: { table?: string } = {}) {
|
|
|
553
600
|
return updated;
|
|
554
601
|
}),
|
|
555
602
|
|
|
556
|
-
/** Admin: activate / deactivate a user. Deactivating blocks future logins
|
|
557
|
-
*
|
|
603
|
+
/** Admin: activate / deactivate a user. Deactivating blocks future logins AND
|
|
604
|
+
* refreshSession, AND revokes OUTSTANDING tokens immediately via the KV denylist (the
|
|
605
|
+
* Worker fails them closed) — so revocation no longer waits out the token TTL. The
|
|
606
|
+
* denylist entry self-expires at the session TTL. Reactivating LIFTS the entry (it is
|
|
607
|
+
* username-scoped, so a stale entry would otherwise lock out even a fresh login). */
|
|
558
608
|
setUserActive: mutation(async (ctx, input: { username: string; active: boolean }) => {
|
|
559
609
|
if (typeof input?.username !== "string" || input.username.length === 0) throw new BadRequest("username is required");
|
|
560
610
|
if (typeof input?.active !== "boolean") throw new BadRequest("active must be a boolean");
|
|
@@ -563,6 +613,9 @@ export function createUserHandlers(opts: { table?: string } = {}) {
|
|
|
563
613
|
}
|
|
564
614
|
const updated = await usersDb(ctx).update(table, input.username, { active: input.active });
|
|
565
615
|
if (!updated) throw new BadRequest("user not found");
|
|
616
|
+
// KV is not part of the mutation's transaction — do it after the update succeeds.
|
|
617
|
+
if (input.active === false) await denySession(ctx.kv, input.username, sessionTtlOf(ctx));
|
|
618
|
+
else await allowSession(ctx.kv, input.username);
|
|
566
619
|
return updated;
|
|
567
620
|
}),
|
|
568
621
|
|
|
@@ -573,6 +626,8 @@ export function createUserHandlers(opts: { table?: string } = {}) {
|
|
|
573
626
|
if (input.username === ctx.identity?.userId) throw new BadRequest("cannot delete your own account");
|
|
574
627
|
const deleted = await usersDb(ctx).delete(table, input.username);
|
|
575
628
|
if (!deleted) throw new BadRequest("user not found");
|
|
629
|
+
// Revoke any outstanding tokens for the now-deleted user (self-expires at the TTL).
|
|
630
|
+
await denySession(ctx.kv, input.username, sessionTtlOf(ctx));
|
|
576
631
|
return { ok: true };
|
|
577
632
|
}),
|
|
578
633
|
|