@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/LICENSE +681 -0
- package/README.md +5 -1
- package/package.json +19 -9
- package/src/index.ts +23 -6
- package/src/postgres.ts +458 -151
- package/src/sql.ts +219 -0
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
|
}
|