@everystack/auth 0.4.7 → 0.4.8

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/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to `@everystack/auth` are documented here.
4
4
 
5
5
  Format based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [0.4.8] - 2026-09-26
8
+
9
+ ### Fixed
10
+
11
+ - revoke sessions when users are blocked
12
+
13
+ ## [0.4.7] - 2026-09-26
14
+
15
+ ### Fixed
16
+
17
+ - enforce bans on refresh and signin, check password before ban
18
+ - make the access token lifetime configurable
19
+ - signout revokes the presented refresh token
20
+ - refuse banned users on OAuth sign-in; signout never fails on revoke
21
+
22
+ ### Documentation
23
+
24
+ - document signout revocation, accessTokenTtlSec and ban enforcement
25
+
7
26
  ## [0.4.6] - 2026-09-22
8
27
 
9
28
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/auth",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "JWT authentication handlers, password hashing, and React auth context",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -39,8 +39,9 @@
39
39
  * hand-patch a body here.
40
40
  */
41
41
 
42
- import { defineFunction, arg, can, sql } from '@everystack/model';
42
+ import { defineFunction, defineTrigger, arg, can, sql } from '@everystack/model';
43
43
  import { DEFAULT_ACCESS_TOKEN_TTL_SEC, resolveAccessTokenTtl } from '../sql';
44
+ import { User } from './user';
44
45
 
45
46
  export const changePasswordFn = defineFunction('auth.change_password', {
46
47
  args: [arg('p_current_password', 'text'), arg('p_new_password', 'text')],
@@ -849,6 +850,36 @@ END;
849
850
  `,
850
851
  });
851
852
 
853
+ /** Removes every session as soon as an account becomes unavailable. */
854
+ export const revokeRefreshTokensOnUserBlockFn = defineFunction('auth.revoke_refresh_tokens_on_user_block', {
855
+ returns: 'trigger',
856
+ language: 'plpgsql',
857
+ security: 'definer',
858
+ searchPath: ['auth', 'public', 'pg_temp'],
859
+ body: sql`
860
+ BEGIN
861
+ DELETE FROM auth.refresh_tokens WHERE user_id = NEW.id;
862
+ RETURN NEW;
863
+ END;`,
864
+ });
865
+
866
+ /** The blocking transition is the durable revocation point; validation keeps its raise contract. */
867
+ export const revokeRefreshTokensOnUserBlockTrigger = defineTrigger('users_revoke_refresh_tokens_on_block', {
868
+ on: User,
869
+ timing: 'after',
870
+ events: ['update'],
871
+ of: ['banned_until', 'deleted_at'],
872
+ forEach: 'row',
873
+ condition: sql`
874
+ (NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL)
875
+ OR (
876
+ NEW.banned_until > now()
877
+ AND (OLD.banned_until IS NULL OR OLD.banned_until <= now())
878
+ )
879
+ `,
880
+ execute: revokeRefreshTokensOnUserBlockFn,
881
+ });
882
+
852
883
  export const verifyJwtFn = defineFunction('auth.verify_jwt', {
853
884
  args: [arg('p_token', 'text')],
854
885
  returns: 'jsonb',
@@ -979,6 +1010,7 @@ export const coreFunctions = [hashPasswordFn, verifyPasswordFn, signJwtFn, verif
979
1010
  /** The refresh-token family (RFC 6819 rotation). Always on. */
980
1011
  export const refreshTokenFunctions = [
981
1012
  issueRefreshTokenFn, validateRefreshTokenFn, revokeRefreshTokensFn, signOutFn, cleanupRefreshTokensFn,
1013
+ revokeRefreshTokensOnUserBlockFn,
982
1014
  ];
983
1015
 
984
1016
  /** `auth.uid()` / `auth.role()` / `auth.jwt()` — called FROM RLS policies, so granted to the
@@ -1041,3 +1073,8 @@ export function authFunctions(opts: AuthFunctionOptions = {}) {
1041
1073
  ...(opts.jwtWrappers === true ? jwtWrapperFunctionsFor(opts.accessTokenTtlSec) : []),
1042
1074
  ];
1043
1075
  }
1076
+
1077
+ /** The auth module's complete derived layer: functions plus lifecycle bindings. */
1078
+ export function authDerived(opts: AuthFunctionOptions = {}) {
1079
+ return [...authFunctions(opts), revokeRefreshTokensOnUserBlockTrigger];
1080
+ }
@@ -25,7 +25,7 @@ import { Device } from './device';
25
25
  import { MfaFactor } from './mfa-factor';
26
26
  import { MfaChallenge } from './mfa-challenge';
27
27
  import { OneTimeToken } from './one-time-token';
28
- import { authFunctions, type AuthFunctionOptions } from './functions';
28
+ import { authDerived, type AuthFunctionOptions } from './functions';
29
29
 
30
30
  export * from './functions';
31
31
 
@@ -51,7 +51,7 @@ export function defineAuthModule(opts: AuthFunctionOptions = {}): Module {
51
51
  return defineModule({
52
52
  models: authModels,
53
53
  extensions: ['pgcrypto'],
54
- derived: authFunctions(opts),
54
+ derived: authDerived(opts),
55
55
  });
56
56
  }
57
57
 
package/src/sql.ts CHANGED
@@ -358,6 +358,33 @@ $$;
358
358
 
359
359
  REVOKE EXECUTE ON FUNCTION auth.validate_refresh_token FROM PUBLIC;`;
360
360
 
361
+ const REVOKE_REFRESH_TOKENS_ON_USER_BLOCK = `\
362
+ CREATE OR REPLACE FUNCTION auth.revoke_refresh_tokens_on_user_block()
363
+ RETURNS TRIGGER
364
+ LANGUAGE plpgsql
365
+ SECURITY DEFINER
366
+ SET search_path = auth, public, pg_temp
367
+ AS $$
368
+ BEGIN
369
+ DELETE FROM auth.refresh_tokens WHERE user_id = NEW.id;
370
+ RETURN NEW;
371
+ END;
372
+ $$;
373
+
374
+ REVOKE EXECUTE ON FUNCTION auth.revoke_refresh_tokens_on_user_block FROM PUBLIC;
375
+
376
+ CREATE TRIGGER users_revoke_refresh_tokens_on_block
377
+ AFTER UPDATE OF banned_until, deleted_at ON auth.users
378
+ FOR EACH ROW
379
+ WHEN (
380
+ (NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL)
381
+ OR (
382
+ NEW.banned_until > now()
383
+ AND (OLD.banned_until IS NULL OR OLD.banned_until <= now())
384
+ )
385
+ )
386
+ EXECUTE FUNCTION auth.revoke_refresh_tokens_on_user_block();`;
387
+
361
388
  const REVOKE_REFRESH_TOKENS = `\
362
389
  CREATE OR REPLACE FUNCTION auth.revoke_refresh_tokens(
363
390
  p_user_id UUID,
@@ -1091,7 +1118,10 @@ export function generateCoreSQL(): string {
1091
1118
 
1092
1119
  /** issue + validate + revoke + sign_out + cleanup refresh tokens (RFC 6819) */
1093
1120
  export function generateRefreshTokenSQL(): string {
1094
- return [ISSUE_REFRESH_TOKEN, '', VALIDATE_REFRESH_TOKEN, '', REVOKE_REFRESH_TOKENS, '', SIGN_OUT, '', CLEANUP_REFRESH_TOKENS]
1121
+ return [
1122
+ ISSUE_REFRESH_TOKEN, '', VALIDATE_REFRESH_TOKEN, '', REVOKE_REFRESH_TOKENS_ON_USER_BLOCK, '',
1123
+ REVOKE_REFRESH_TOKENS, '', SIGN_OUT, '', CLEANUP_REFRESH_TOKENS,
1124
+ ]
1095
1125
  .join('\n')
1096
1126
  .trimEnd() + '\n';
1097
1127
  }