@edcalderon/auth 1.2.1 → 1.2.2
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
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.2] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added `packages/auth/supabase/` SQL templates for a vendor-independent `public.users` table and optional Supabase Auth sync trigger.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Hardened the OIDC upsert migration so identity writes require a trusted server-side caller instead of the `anon` role.
|
|
12
|
+
- Preserved existing user profile fields when optional claims are omitted during upserts or Supabase sync updates.
|
|
13
|
+
|
|
3
14
|
## [1.2.1] - 2026-03-02
|
|
4
15
|
|
|
5
16
|
### Changed
|
package/README.md
CHANGED
|
@@ -11,12 +11,16 @@ Swap between Supabase, Firebase, Hybrid, or any custom provider without changing
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
## 📋 Latest Changes (v1.2.
|
|
14
|
+
## 📋 Latest Changes (v1.2.2)
|
|
15
15
|
|
|
16
|
-
###
|
|
16
|
+
### Added
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
|
|
18
|
+
- Added `packages/auth/supabase/` SQL templates for a vendor-independent `public.users` table and optional Supabase Auth sync trigger.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Hardened the OIDC upsert migration so identity writes require a trusted server-side caller instead of the `anon` role.
|
|
23
|
+
- Preserved existing user profile fields when optional claims are omitted during upserts or Supabase sync updates.
|
|
20
24
|
|
|
21
25
|
For full version history, see [CHANGELOG.md](./CHANGELOG.md) and [GitHub releases](https://github.com/edcalderon/my-second-brain/releases)
|
|
22
26
|
|
|
@@ -68,6 +72,13 @@ pnpm add firebase
|
|
|
68
72
|
pnpm add react-native
|
|
69
73
|
```
|
|
70
74
|
|
|
75
|
+
### Supabase SQL Templates
|
|
76
|
+
|
|
77
|
+
If you want an application-owned user table instead of coupling your identity model to `auth.users`, copy the reference SQL templates in `packages/auth/supabase/` into your Supabase project and apply them with `supabase db push`.
|
|
78
|
+
|
|
79
|
+
- `001_create_app_users.sql`: vendor-independent `public.users` table plus secure server-side OIDC upsert RPC
|
|
80
|
+
- `002_sync_auth_users_to_app_users.sql`: optional trigger and backfill for projects using Supabase Auth
|
|
81
|
+
|
|
71
82
|
---
|
|
72
83
|
|
|
73
84
|
## Subpath Exports (Crucial for RN/Next.js compatibility)
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @edcalderon/auth Supabase SQL Templates
|
|
2
|
+
|
|
3
|
+
Copy these files into your own Supabase project. They are reference migrations for projects using @edcalderon/auth, not runtime imports.
|
|
4
|
+
|
|
5
|
+
## Quick Setups
|
|
6
|
+
|
|
7
|
+
| Use case | Apply | Result |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Supabase Auth quick setup | `001` + `002` | Vendor-independent `public.users` plus automatic `auth.users -> public.users` sync |
|
|
10
|
+
| External OIDC only | `001` | Vendor-independent `public.users` plus a secure server-side upsert RPC |
|
|
11
|
+
|
|
12
|
+
## Files
|
|
13
|
+
|
|
14
|
+
| File | Purpose |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `migrations/001_create_app_users.sql` | Creates `public.users`, timestamps trigger, and `upsert_oidc_user()` |
|
|
17
|
+
| `migrations/002_sync_auth_users_to_app_users.sql` | Optional trigger/backfill for projects that use Supabase Auth |
|
|
18
|
+
|
|
19
|
+
## What You Get
|
|
20
|
+
|
|
21
|
+
- A vendor-independent `public.users` table keyed by `(sub, iss)`.
|
|
22
|
+
- A clean migration path away from Supabase Auth later.
|
|
23
|
+
- An optional trigger that mirrors Supabase Auth users automatically.
|
|
24
|
+
- Safe default behavior for optional profile fields so missing claims do not erase stored data.
|
|
25
|
+
|
|
26
|
+
## Security Model
|
|
27
|
+
|
|
28
|
+
`upsert_oidc_user()` is intentionally not callable by the `anon` role.
|
|
29
|
+
|
|
30
|
+
- Supabase Auth users are synced automatically by migration `002`.
|
|
31
|
+
- External OIDC users should be written by a trusted server or Edge Function after token verification.
|
|
32
|
+
- Optional fields such as `email`, `name`, `picture`, and `provider` only overwrite stored values when a non-null value is provided.
|
|
33
|
+
|
|
34
|
+
## Apply
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Copy into your project
|
|
38
|
+
cp packages/auth/supabase/migrations/*.sql your-project/supabase/migrations/
|
|
39
|
+
|
|
40
|
+
# Apply them with the Supabase CLI
|
|
41
|
+
supabase db push
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If you only want the vendor-independent table and are not using Supabase Auth as a provider, apply only `001_create_app_users.sql`.
|
|
45
|
+
|
|
46
|
+
## Suggested External OIDC Flow
|
|
47
|
+
|
|
48
|
+
1. Verify the provider token in your server or Edge Function.
|
|
49
|
+
2. Extract `sub`, `iss`, and optional claims.
|
|
50
|
+
3. Call `upsert_oidc_user()` with the service-role key.
|
|
51
|
+
4. Use the returned `public.users.id` as your application user identifier.
|
|
52
|
+
|
|
53
|
+
## Why `(sub, iss)`
|
|
54
|
+
|
|
55
|
+
The `(sub, iss)` pair follows the OIDC identity model and allows multiple providers to coexist safely:
|
|
56
|
+
|
|
57
|
+
| Provider | `sub` | `iss` |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| Supabase Auth | `auth.users.id` | `supabase` |
|
|
60
|
+
| Authentik | provider UUID | issuer URL |
|
|
61
|
+
| Auth0 | provider user id | issuer URL |
|
|
62
|
+
| Firebase | provider uid | issuer URL |
|
|
63
|
+
| Custom OIDC | JWT `sub` | JWT `iss` |
|
|
64
|
+
|
|
65
|
+
That gives you a stable application-owned identity layer even if you later replace the auth provider.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 001_create_app_users.sql
|
|
3
|
+
-- Vendor-independent user registry for projects using @edcalderon/auth.
|
|
4
|
+
--
|
|
5
|
+
-- This creates an application-owned public.users table keyed by (sub, iss)
|
|
6
|
+
-- and a secure upsert RPC intended for trusted server-side use.
|
|
7
|
+
-- ============================================================================
|
|
8
|
+
|
|
9
|
+
create extension if not exists pgcrypto;
|
|
10
|
+
|
|
11
|
+
create table if not exists public.users (
|
|
12
|
+
id uuid primary key default gen_random_uuid(),
|
|
13
|
+
sub text not null,
|
|
14
|
+
iss text not null,
|
|
15
|
+
email text,
|
|
16
|
+
email_verified boolean not null default false,
|
|
17
|
+
name text,
|
|
18
|
+
picture text,
|
|
19
|
+
provider text,
|
|
20
|
+
raw_claims jsonb not null default '{}'::jsonb,
|
|
21
|
+
created_at timestamptz not null default timezone('utc', now()),
|
|
22
|
+
updated_at timestamptz not null default timezone('utc', now()),
|
|
23
|
+
|
|
24
|
+
constraint users_sub_iss_uq unique (sub, iss),
|
|
25
|
+
constraint users_sub_len_chk check (char_length(sub) <= 256),
|
|
26
|
+
constraint users_iss_len_chk check (char_length(iss) <= 512),
|
|
27
|
+
constraint users_email_len_chk check (
|
|
28
|
+
email is null or char_length(email) <= 320
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
create index if not exists users_email_idx
|
|
33
|
+
on public.users (lower(email))
|
|
34
|
+
where email is not null;
|
|
35
|
+
|
|
36
|
+
create index if not exists users_provider_idx
|
|
37
|
+
on public.users (provider)
|
|
38
|
+
where provider is not null;
|
|
39
|
+
|
|
40
|
+
create or replace function public.users_set_updated_at()
|
|
41
|
+
returns trigger
|
|
42
|
+
language plpgsql
|
|
43
|
+
as $$
|
|
44
|
+
begin
|
|
45
|
+
new.updated_at = timezone('utc', now());
|
|
46
|
+
return new;
|
|
47
|
+
end;
|
|
48
|
+
$$;
|
|
49
|
+
|
|
50
|
+
drop trigger if exists trg_users_set_updated_at on public.users;
|
|
51
|
+
|
|
52
|
+
create trigger trg_users_set_updated_at
|
|
53
|
+
before update on public.users
|
|
54
|
+
for each row
|
|
55
|
+
execute function public.users_set_updated_at();
|
|
56
|
+
|
|
57
|
+
create or replace function public.upsert_oidc_user(
|
|
58
|
+
p_sub text,
|
|
59
|
+
p_iss text,
|
|
60
|
+
p_email text default null,
|
|
61
|
+
p_email_verified boolean default false,
|
|
62
|
+
p_name text default null,
|
|
63
|
+
p_picture text default null,
|
|
64
|
+
p_provider text default null,
|
|
65
|
+
p_raw_claims jsonb default '{}'::jsonb
|
|
66
|
+
)
|
|
67
|
+
returns public.users
|
|
68
|
+
language plpgsql
|
|
69
|
+
security definer
|
|
70
|
+
set search_path = public
|
|
71
|
+
as $$
|
|
72
|
+
declare
|
|
73
|
+
v_claims jsonb := coalesce(
|
|
74
|
+
nullif(current_setting('request.jwt.claims', true), ''),
|
|
75
|
+
'{}'
|
|
76
|
+
)::jsonb;
|
|
77
|
+
v_role text := coalesce(nullif(v_claims ->> 'role', ''), 'unknown');
|
|
78
|
+
v_user public.users;
|
|
79
|
+
begin
|
|
80
|
+
if v_role <> 'service_role' then
|
|
81
|
+
raise exception 'upsert_oidc_user() requires a trusted server-side caller';
|
|
82
|
+
end if;
|
|
83
|
+
|
|
84
|
+
if nullif(trim(p_sub), '') is null or nullif(trim(p_iss), '') is null then
|
|
85
|
+
raise exception 'upsert_oidc_user() requires non-empty p_sub and p_iss';
|
|
86
|
+
end if;
|
|
87
|
+
|
|
88
|
+
insert into public.users (
|
|
89
|
+
sub,
|
|
90
|
+
iss,
|
|
91
|
+
email,
|
|
92
|
+
email_verified,
|
|
93
|
+
name,
|
|
94
|
+
picture,
|
|
95
|
+
provider,
|
|
96
|
+
raw_claims
|
|
97
|
+
)
|
|
98
|
+
values (
|
|
99
|
+
trim(p_sub),
|
|
100
|
+
trim(p_iss),
|
|
101
|
+
nullif(trim(p_email), ''),
|
|
102
|
+
p_email_verified,
|
|
103
|
+
nullif(trim(p_name), ''),
|
|
104
|
+
nullif(trim(p_picture), ''),
|
|
105
|
+
nullif(trim(p_provider), ''),
|
|
106
|
+
coalesce(p_raw_claims, '{}'::jsonb)
|
|
107
|
+
)
|
|
108
|
+
on conflict (sub, iss) do update
|
|
109
|
+
set email = coalesce(excluded.email, public.users.email),
|
|
110
|
+
email_verified = public.users.email_verified or excluded.email_verified,
|
|
111
|
+
name = coalesce(excluded.name, public.users.name),
|
|
112
|
+
picture = coalesce(excluded.picture, public.users.picture),
|
|
113
|
+
provider = coalesce(excluded.provider, public.users.provider),
|
|
114
|
+
raw_claims = case
|
|
115
|
+
when excluded.raw_claims = '{}'::jsonb then public.users.raw_claims
|
|
116
|
+
else public.users.raw_claims || excluded.raw_claims
|
|
117
|
+
end,
|
|
118
|
+
updated_at = timezone('utc', now())
|
|
119
|
+
returning * into v_user;
|
|
120
|
+
|
|
121
|
+
return v_user;
|
|
122
|
+
end;
|
|
123
|
+
$$;
|
|
124
|
+
|
|
125
|
+
revoke all on function public.upsert_oidc_user(
|
|
126
|
+
text,
|
|
127
|
+
text,
|
|
128
|
+
text,
|
|
129
|
+
boolean,
|
|
130
|
+
text,
|
|
131
|
+
text,
|
|
132
|
+
text,
|
|
133
|
+
jsonb
|
|
134
|
+
) from public, anon, authenticated;
|
|
135
|
+
|
|
136
|
+
grant execute on function public.upsert_oidc_user(
|
|
137
|
+
text,
|
|
138
|
+
text,
|
|
139
|
+
text,
|
|
140
|
+
boolean,
|
|
141
|
+
text,
|
|
142
|
+
text,
|
|
143
|
+
text,
|
|
144
|
+
jsonb
|
|
145
|
+
) to service_role;
|
|
146
|
+
|
|
147
|
+
alter table public.users enable row level security;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 002_sync_auth_users_to_app_users.sql
|
|
3
|
+
-- Optional trigger for projects that use Supabase Auth.
|
|
4
|
+
-- Keeps auth.users mirrored into public.users and backfills existing users.
|
|
5
|
+
-- ============================================================================
|
|
6
|
+
|
|
7
|
+
create or replace function public.sync_auth_user_to_app_users()
|
|
8
|
+
returns trigger
|
|
9
|
+
language plpgsql
|
|
10
|
+
security definer
|
|
11
|
+
set search_path = public
|
|
12
|
+
as $$
|
|
13
|
+
begin
|
|
14
|
+
if new.is_anonymous then
|
|
15
|
+
return new;
|
|
16
|
+
end if;
|
|
17
|
+
|
|
18
|
+
insert into public.users (
|
|
19
|
+
sub,
|
|
20
|
+
iss,
|
|
21
|
+
email,
|
|
22
|
+
email_verified,
|
|
23
|
+
name,
|
|
24
|
+
picture,
|
|
25
|
+
provider,
|
|
26
|
+
raw_claims
|
|
27
|
+
)
|
|
28
|
+
values (
|
|
29
|
+
new.id::text,
|
|
30
|
+
'supabase',
|
|
31
|
+
new.email,
|
|
32
|
+
new.email_confirmed_at is not null,
|
|
33
|
+
coalesce(
|
|
34
|
+
new.raw_user_meta_data ->> 'name',
|
|
35
|
+
new.raw_user_meta_data ->> 'full_name',
|
|
36
|
+
split_part(coalesce(new.email, ''), '@', 1)
|
|
37
|
+
),
|
|
38
|
+
new.raw_user_meta_data ->> 'avatar_url',
|
|
39
|
+
coalesce(new.raw_app_meta_data ->> 'provider', 'email'),
|
|
40
|
+
coalesce(new.raw_user_meta_data, '{}'::jsonb)
|
|
41
|
+
)
|
|
42
|
+
on conflict (sub, iss) do update
|
|
43
|
+
set email = coalesce(excluded.email, public.users.email),
|
|
44
|
+
email_verified = public.users.email_verified or excluded.email_verified,
|
|
45
|
+
name = coalesce(excluded.name, public.users.name),
|
|
46
|
+
picture = coalesce(excluded.picture, public.users.picture),
|
|
47
|
+
provider = coalesce(excluded.provider, public.users.provider),
|
|
48
|
+
raw_claims = case
|
|
49
|
+
when excluded.raw_claims = '{}'::jsonb then public.users.raw_claims
|
|
50
|
+
else public.users.raw_claims || excluded.raw_claims
|
|
51
|
+
end,
|
|
52
|
+
updated_at = timezone('utc', now());
|
|
53
|
+
|
|
54
|
+
return new;
|
|
55
|
+
end;
|
|
56
|
+
$$;
|
|
57
|
+
|
|
58
|
+
drop trigger if exists trg_auth_users_sync_to_app_users on auth.users;
|
|
59
|
+
|
|
60
|
+
create trigger trg_auth_users_sync_to_app_users
|
|
61
|
+
after insert or update of email_confirmed_at, email, raw_user_meta_data, raw_app_meta_data
|
|
62
|
+
on auth.users
|
|
63
|
+
for each row
|
|
64
|
+
execute function public.sync_auth_user_to_app_users();
|
|
65
|
+
|
|
66
|
+
insert into public.users (
|
|
67
|
+
sub,
|
|
68
|
+
iss,
|
|
69
|
+
email,
|
|
70
|
+
email_verified,
|
|
71
|
+
name,
|
|
72
|
+
picture,
|
|
73
|
+
provider,
|
|
74
|
+
raw_claims
|
|
75
|
+
)
|
|
76
|
+
select
|
|
77
|
+
u.id::text,
|
|
78
|
+
'supabase',
|
|
79
|
+
u.email,
|
|
80
|
+
u.email_confirmed_at is not null,
|
|
81
|
+
coalesce(
|
|
82
|
+
u.raw_user_meta_data ->> 'name',
|
|
83
|
+
u.raw_user_meta_data ->> 'full_name',
|
|
84
|
+
split_part(coalesce(u.email, ''), '@', 1)
|
|
85
|
+
),
|
|
86
|
+
u.raw_user_meta_data ->> 'avatar_url',
|
|
87
|
+
coalesce(u.raw_app_meta_data ->> 'provider', 'email'),
|
|
88
|
+
coalesce(u.raw_user_meta_data, '{}'::jsonb)
|
|
89
|
+
from auth.users u
|
|
90
|
+
where not u.is_anonymous
|
|
91
|
+
on conflict (sub, iss) do update
|
|
92
|
+
set email = coalesce(excluded.email, public.users.email),
|
|
93
|
+
email_verified = public.users.email_verified or excluded.email_verified,
|
|
94
|
+
name = coalesce(excluded.name, public.users.name),
|
|
95
|
+
picture = coalesce(excluded.picture, public.users.picture),
|
|
96
|
+
provider = coalesce(excluded.provider, public.users.provider),
|
|
97
|
+
raw_claims = case
|
|
98
|
+
when excluded.raw_claims = '{}'::jsonb then public.users.raw_claims
|
|
99
|
+
else public.users.raw_claims || excluded.raw_claims
|
|
100
|
+
end,
|
|
101
|
+
updated_at = timezone('utc', now());
|