@everystack/auth 0.2.5 → 0.2.6

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/src/sql.ts CHANGED
@@ -32,6 +32,7 @@ CREATE OR REPLACE FUNCTION auth.hash_password(p_password TEXT)
32
32
  RETURNS TEXT
33
33
  LANGUAGE plpgsql
34
34
  SECURITY DEFINER
35
+ SET search_path = auth, public, pg_temp
35
36
  AS $$
36
37
  BEGIN
37
38
  RETURN crypt(p_password, gen_salt('bf', 10));
@@ -43,6 +44,7 @@ CREATE OR REPLACE FUNCTION auth.verify_password(p_password TEXT, p_hash TEXT)
43
44
  RETURNS BOOLEAN
44
45
  LANGUAGE plpgsql
45
46
  SECURITY DEFINER
47
+ SET search_path = auth, public, pg_temp
46
48
  AS $$
47
49
  DECLARE
48
50
  v_normalized TEXT;
@@ -59,6 +61,7 @@ CREATE OR REPLACE FUNCTION auth.sign_jwt(p_payload JSONB)
59
61
  RETURNS TEXT
60
62
  LANGUAGE plpgsql
61
63
  SECURITY DEFINER
64
+ SET search_path = auth, public, pg_temp
62
65
  AS $$
63
66
  DECLARE
64
67
  v_header TEXT;
@@ -102,6 +105,7 @@ CREATE OR REPLACE FUNCTION auth.verify_jwt(p_token TEXT)
102
105
  RETURNS JSONB
103
106
  LANGUAGE plpgsql
104
107
  SECURITY DEFINER
108
+ SET search_path = auth, public, pg_temp
105
109
  AS $$
106
110
  DECLARE
107
111
  v_parts TEXT[];
@@ -499,6 +503,118 @@ $$;
499
503
 
500
504
  REVOKE EXECUTE ON FUNCTION auth.verify_one_time_token FROM PUBLIC;`;
501
505
 
506
+ // =============================================================================
507
+ // TOKEN-GATED ACTION WRAPPERS
508
+ //
509
+ // These are the anon-safe entry points for the password-reset and email-confirm
510
+ // flows. They are SECURITY DEFINER (they update auth.users — a locked table), but the
511
+ // CALLER must already hold a valid one-time token, so they are credential-gated the
512
+ // same way auth.sign_in is password-gated: there is nothing to abuse without the token.
513
+ // The token-MINTING function (auth.create_one_time_token) is never granted to anon and
514
+ // runs only on the privileged worker path. These wrappers touch ONLY auth.users — auth
515
+ // columns. Public profile data lives in its own table and is never mixed in here.
516
+ // =============================================================================
517
+
518
+ const CONFIRM_EMAIL = `\
519
+ CREATE OR REPLACE FUNCTION auth.confirm_email(p_token TEXT)
520
+ RETURNS JSONB
521
+ LANGUAGE plpgsql
522
+ SECURITY DEFINER
523
+ SET search_path = auth, public, pg_temp
524
+ AS $$
525
+ DECLARE
526
+ v_result JSONB;
527
+ v_user_id UUID;
528
+ BEGIN
529
+ -- Raises on an invalid/expired/consumed token (fails closed). One-time use.
530
+ v_result := auth.verify_one_time_token(p_token, 'confirmation');
531
+ v_user_id := (v_result->>'userId')::uuid;
532
+
533
+ UPDATE auth.users
534
+ SET email_confirmed_at = now(), updated_at = now()
535
+ WHERE id = v_user_id;
536
+
537
+ RETURN jsonb_build_object('confirmed', true, 'userId', v_user_id);
538
+ END;
539
+ $$;
540
+
541
+ REVOKE EXECUTE ON FUNCTION auth.confirm_email FROM PUBLIC;`;
542
+
543
+ const RESET_PASSWORD = `\
544
+ CREATE OR REPLACE FUNCTION auth.reset_password(p_token TEXT, p_new_password TEXT)
545
+ RETURNS JSONB
546
+ LANGUAGE plpgsql
547
+ SECURITY DEFINER
548
+ SET search_path = auth, public, pg_temp
549
+ AS $$
550
+ DECLARE
551
+ v_result JSONB;
552
+ v_user_id UUID;
553
+ BEGIN
554
+ -- Raises on an invalid/expired/consumed token (fails closed). One-time use.
555
+ v_result := auth.verify_one_time_token(p_token, 'recovery');
556
+ v_user_id := (v_result->>'userId')::uuid;
557
+
558
+ UPDATE auth.users
559
+ SET encrypted_password = auth.hash_password(p_new_password), updated_at = now()
560
+ WHERE id = v_user_id;
561
+
562
+ -- A password reset invalidates every existing session — force re-login everywhere.
563
+ PERFORM auth.revoke_refresh_tokens(v_user_id);
564
+
565
+ RETURN jsonb_build_object('reset', true, 'userId', v_user_id);
566
+ END;
567
+ $$;
568
+
569
+ REVOKE EXECUTE ON FUNCTION auth.reset_password FROM PUBLIC;`;
570
+
571
+ // auth.change_password — the authenticated self-service password change. Not token-gated
572
+ // but credential-gated: the caller must be authenticated (auth.uid() from the verified
573
+ // JWT) AND prove the current password before the new one is set. SECURITY DEFINER because
574
+ // verifying the current password requires reading encrypted_password, which authenticated
575
+ // must never be granted. Granted to authenticated (not anon); called via withRole.
576
+ const CHANGE_PASSWORD = `\
577
+ CREATE OR REPLACE FUNCTION auth.change_password(p_current_password TEXT, p_new_password TEXT)
578
+ RETURNS JSONB
579
+ LANGUAGE plpgsql
580
+ SECURITY DEFINER
581
+ SET search_path = auth, public, pg_temp
582
+ AS $$
583
+ DECLARE
584
+ v_user_id UUID;
585
+ v_hash TEXT;
586
+ BEGIN
587
+ -- The caller is authenticated; their verified subject claim identifies them.
588
+ -- Read the claim inline so this wrapper has no dependency on the accessor functions.
589
+ v_user_id := (nullif(current_setting('request.jwt.claims', true), '')::jsonb->>'sub')::uuid;
590
+ IF v_user_id IS NULL THEN
591
+ RAISE EXCEPTION 'not_authenticated' USING ERRCODE = 'P0001';
592
+ END IF;
593
+
594
+ SELECT encrypted_password INTO v_hash FROM auth.users
595
+ WHERE id = v_user_id AND deleted_at IS NULL;
596
+ IF NOT FOUND OR v_hash IS NULL THEN
597
+ RAISE EXCEPTION 'invalid_credentials' USING ERRCODE = 'P0001';
598
+ END IF;
599
+
600
+ -- Verify the current password before changing it (the gate).
601
+ IF NOT auth.verify_password(p_current_password, v_hash) THEN
602
+ RAISE EXCEPTION 'invalid_credentials' USING ERRCODE = 'P0001';
603
+ END IF;
604
+
605
+ UPDATE auth.users
606
+ SET encrypted_password = auth.hash_password(p_new_password), updated_at = now()
607
+ WHERE id = v_user_id;
608
+
609
+ -- A password change invalidates every existing session — force re-login everywhere.
610
+ PERFORM auth.revoke_refresh_tokens(v_user_id);
611
+
612
+ RETURN jsonb_build_object('changed', true, 'userId', v_user_id);
613
+ END;
614
+ $$;
615
+
616
+ REVOKE EXECUTE ON FUNCTION auth.change_password FROM PUBLIC;`;
617
+
502
618
  // =============================================================================
503
619
  // DEFAULT AUTH FUNCTIONS (work against default auth.users schema)
504
620
  // =============================================================================
@@ -811,6 +927,76 @@ $$;
811
927
 
812
928
  REVOKE EXECUTE ON FUNCTION auth.oauth_sign_in_jwt FROM PUBLIC;`;
813
929
 
930
+ // =============================================================================
931
+ // SESSION WRAPPERS — the anon-safe login/signup entry points for a least-priv API
932
+ //
933
+ // Each is credential-gated (sign_in raises on a bad password; sign_up raises on a dup
934
+ // email), so the caller proves the credential before anything is minted. They verify or
935
+ // create the user AND issue the refresh token internally (as the definer/owner), so the
936
+ // raw minters — issue_refresh_token, sign_jwt — never need a grant to anon and stay
937
+ // REVOKE'd. They return only the caller's OWN identity plus the refresh token; the ACCESS
938
+ // JWT is minted by the handler in JS (so app-level enrichClaims, e.g. the `aud` claim,
939
+ // is preserved). They touch only auth.users; the public profile is created by a trigger.
940
+ // =============================================================================
941
+
942
+ const SIGN_IN_SESSION = `\
943
+ CREATE OR REPLACE FUNCTION auth.sign_in_session(
944
+ p_email TEXT,
945
+ p_password TEXT,
946
+ p_ip_address TEXT DEFAULT NULL,
947
+ p_user_agent TEXT DEFAULT NULL,
948
+ p_device_id UUID DEFAULT NULL
949
+ )
950
+ RETURNS JSONB
951
+ LANGUAGE plpgsql
952
+ SECURITY DEFINER
953
+ SET search_path = auth, public, pg_temp
954
+ AS $$
955
+ DECLARE
956
+ v_user JSONB;
957
+ v_refresh JSONB;
958
+ BEGIN
959
+ -- Raises invalid_credentials (timing-safe) on a bad email/password — fails closed.
960
+ v_user := auth.sign_in(p_email, p_password);
961
+ v_refresh := auth.issue_refresh_token(
962
+ (v_user->>'sub')::uuid, NULL, p_ip_address, p_user_agent, p_device_id
963
+ );
964
+ RETURN v_user || jsonb_build_object('refresh_token', v_refresh->>'token');
965
+ END;
966
+ $$;
967
+
968
+ REVOKE EXECUTE ON FUNCTION auth.sign_in_session FROM PUBLIC;`;
969
+
970
+ const SIGN_UP_SESSION = `\
971
+ CREATE OR REPLACE FUNCTION auth.sign_up_session(
972
+ p_email TEXT,
973
+ p_password TEXT,
974
+ p_user_meta_data JSONB DEFAULT '{}',
975
+ p_ip_address TEXT DEFAULT NULL,
976
+ p_user_agent TEXT DEFAULT NULL,
977
+ p_device_id UUID DEFAULT NULL
978
+ )
979
+ RETURNS JSONB
980
+ LANGUAGE plpgsql
981
+ SECURITY DEFINER
982
+ SET search_path = auth, public, pg_temp
983
+ AS $$
984
+ DECLARE
985
+ v_user JSONB;
986
+ v_refresh JSONB;
987
+ BEGIN
988
+ -- Raises email_exists on a duplicate. Creates auth.users only; the caller creates
989
+ -- the public profile as the authenticated user (invoker INSERT under RLS).
990
+ v_user := auth.sign_up(p_email, p_password, p_user_meta_data);
991
+ v_refresh := auth.issue_refresh_token(
992
+ (v_user->>'sub')::uuid, NULL, p_ip_address, p_user_agent, p_device_id
993
+ );
994
+ RETURN v_user || jsonb_build_object('refresh_token', v_refresh->>'token');
995
+ END;
996
+ $$;
997
+
998
+ REVOKE EXECUTE ON FUNCTION auth.sign_up_session FROM PUBLIC;`;
999
+
814
1000
  // =============================================================================
815
1001
  // COMPOSABLE GENERATORS
816
1002
  // =============================================================================
@@ -843,6 +1029,17 @@ export function generateOneTimeTokenSQL(): string {
843
1029
  .trimEnd() + '\n';
844
1030
  }
845
1031
 
1032
+ /**
1033
+ * auth.confirm_email, auth.reset_password — token-gated action wrappers. The anon-safe
1034
+ * entry points for the email-confirm and password-reset flows (the caller holds a valid
1035
+ * one-time token; the token-minting function stays owner-only on the worker path).
1036
+ */
1037
+ export function generateTokenActionsSQL(): string {
1038
+ return [CONFIRM_EMAIL, '', RESET_PASSWORD, '', CHANGE_PASSWORD]
1039
+ .join('\n')
1040
+ .trimEnd() + '\n';
1041
+ }
1042
+
846
1043
  /** auth.sign_in, auth.sign_up, auth.oauth_sign_in (work against default schema) */
847
1044
  export function generateDefaultAuthSQL(): string {
848
1045
  return [SIGN_UP, '', SIGN_IN, '', OAUTH_SIGN_IN]
@@ -857,6 +1054,18 @@ export function generateJwtWrappersSQL(): string {
857
1054
  .trimEnd() + '\n';
858
1055
  }
859
1056
 
1057
+ /**
1058
+ * auth.sign_in_session, auth.sign_up_session — the anon-safe, credential-gated login/signup
1059
+ * entry points for a least-privilege API. They verify/create the user and issue the refresh
1060
+ * token internally (the raw minters stay owner-only); the handler mints the access JWT in JS
1061
+ * so enrichClaims is preserved.
1062
+ */
1063
+ export function generateSessionWrappersSQL(): string {
1064
+ return [SIGN_IN_SESSION, '', SIGN_UP_SESSION]
1065
+ .join('\n')
1066
+ .trimEnd() + '\n';
1067
+ }
1068
+
860
1069
  // =============================================================================
861
1070
  // MAIN ENTRY POINT
862
1071
  // =============================================================================
@@ -866,6 +1075,10 @@ export interface AuthSQLOptions {
866
1075
  accessors?: boolean;
867
1076
  /** Include one-time token functions (default: true) */
868
1077
  oneTimeTokens?: boolean;
1078
+ /** Include the token-gated action wrappers confirm_email/reset_password (default: true) */
1079
+ tokenActions?: boolean;
1080
+ /** Include the anon-safe session wrappers sign_in_session/sign_up_session (default: true) */
1081
+ sessionWrappers?: boolean;
869
1082
  /** Include default sign_in/sign_up/oauth_sign_in functions (default: true) */
870
1083
  defaultAuthFunctions?: boolean;
871
1084
  /** Include JWT convenience wrappers (default: false) */
@@ -892,6 +1105,12 @@ export function generateAuthSQL(options?: AuthSQLOptions): string {
892
1105
  if (options?.oneTimeTokens !== false) {
893
1106
  parts.push('', generateOneTimeTokenSQL());
894
1107
  }
1108
+ if (options?.tokenActions !== false) {
1109
+ parts.push('', generateTokenActionsSQL());
1110
+ }
1111
+ if (options?.sessionWrappers !== false) {
1112
+ parts.push('', generateSessionWrappersSQL());
1113
+ }
895
1114
  if (options?.defaultAuthFunctions !== false) {
896
1115
  parts.push('', generateDefaultAuthSQL());
897
1116
  }