@ankhorage/infra 0.1.0 → 0.2.0
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 +6 -0
- package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -1
- package/dist/adapters/minikube/auth/supabase/index.js +198 -5
- package/dist/adapters/minikube/auth/supabase/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/minikube/auth/supabase/index.test.ts +93 -0
- package/src/adapters/minikube/auth/supabase/index.ts +242 -5
package/CHANGELOG.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/minikube/auth/supabase/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,wBAAwB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/minikube/auth/supabase/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,wBAAwB,CA6E3B"}
|
|
@@ -3,10 +3,12 @@ export function generateSupabaseAuthArtifacts(args) {
|
|
|
3
3
|
const root = 'infra/minikube/k8s/auth/supabase';
|
|
4
4
|
const resourceRoot = 'auth/supabase';
|
|
5
5
|
const docsRoot = 'infra/minikube/auth';
|
|
6
|
+
const migrationRoot = 'infra/minikube/supabase/migrations';
|
|
6
7
|
const scope = manifest.auth?.scope ?? 'global';
|
|
7
8
|
const authzEngine = manifest.auth?.authorization.engine ?? 'native';
|
|
8
9
|
const authzKind = manifest.auth?.authorization.kind ?? 'RBAC';
|
|
9
10
|
const authFieldModel = resolveAuthFieldModel(manifest);
|
|
11
|
+
const profileModel = resolveProfileModel(manifest);
|
|
10
12
|
const warnings = [];
|
|
11
13
|
if (scope !== 'global') {
|
|
12
14
|
warnings.push(`Auth scope "${scope}" is not fully modeled yet for Supabase. Baseline resources assume global auth.`);
|
|
@@ -21,6 +23,7 @@ export function generateSupabaseAuthArtifacts(args) {
|
|
|
21
23
|
authzEngine,
|
|
22
24
|
authzKind,
|
|
23
25
|
authFieldModel,
|
|
26
|
+
profileModel,
|
|
24
27
|
}),
|
|
25
28
|
},
|
|
26
29
|
{
|
|
@@ -35,11 +38,20 @@ export function generateSupabaseAuthArtifacts(args) {
|
|
|
35
38
|
authzEngine,
|
|
36
39
|
authzKind,
|
|
37
40
|
authFieldModel,
|
|
41
|
+
profileModel,
|
|
38
42
|
}),
|
|
39
43
|
},
|
|
44
|
+
...(profileModel.enabled
|
|
45
|
+
? [
|
|
46
|
+
{
|
|
47
|
+
path: `${migrationRoot}/0001_auth_profiles.sql`,
|
|
48
|
+
content: getSupabaseProfileMigration(profileModel),
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
: []),
|
|
40
52
|
{
|
|
41
53
|
path: `${docsRoot}/supabase-runtime-wiring.md`,
|
|
42
|
-
content: getSupabaseRuntimeWiringGuide(),
|
|
54
|
+
content: getSupabaseRuntimeWiringGuide(profileModel),
|
|
43
55
|
},
|
|
44
56
|
],
|
|
45
57
|
resources: [
|
|
@@ -60,7 +72,7 @@ export function generateSupabaseAuthArtifacts(args) {
|
|
|
60
72
|
};
|
|
61
73
|
}
|
|
62
74
|
function getSupabaseConfigMap(args) {
|
|
63
|
-
const { namespace, scope, authzEngine, authzKind, authFieldModel } = args;
|
|
75
|
+
const { namespace, scope, authzEngine, authzKind, authFieldModel, profileModel } = args;
|
|
64
76
|
return `apiVersion: v1
|
|
65
77
|
kind: ConfigMap
|
|
66
78
|
metadata:
|
|
@@ -76,6 +88,10 @@ data:
|
|
|
76
88
|
AUTH_SIGN_UP_OPTIONAL_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}"
|
|
77
89
|
AUTH_SIGN_UP_POLICY: "${authFieldModel.signUpPolicy}"
|
|
78
90
|
AUTH_PROFILE_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}"
|
|
91
|
+
AUTH_PROFILE_TABLE: "${escapeYamlDoubleQuoted(profileModel.table)}"
|
|
92
|
+
AUTH_PROFILE_PRIMARY_KEY: "${profileModel.primaryKey}"
|
|
93
|
+
AUTH_PROFILE_CREATE_STRATEGY: "${profileModel.createStrategy}"
|
|
94
|
+
AUTH_PROFILE_UPDATE_STRATEGY: "${profileModel.updateStrategy}"
|
|
79
95
|
`;
|
|
80
96
|
}
|
|
81
97
|
function getSupabaseSecret(namespace) {
|
|
@@ -95,7 +111,7 @@ stringData:
|
|
|
95
111
|
`;
|
|
96
112
|
}
|
|
97
113
|
function getRuntimeAuthEnvConfigMap(args) {
|
|
98
|
-
const { namespace, scope, authzEngine, authzKind, authFieldModel } = args;
|
|
114
|
+
const { namespace, scope, authzEngine, authzKind, authFieldModel, profileModel } = args;
|
|
99
115
|
return `apiVersion: v1
|
|
100
116
|
kind: ConfigMap
|
|
101
117
|
metadata:
|
|
@@ -112,11 +128,31 @@ data:
|
|
|
112
128
|
AUTH_SIGN_UP_OPTIONAL_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}"
|
|
113
129
|
AUTH_SIGN_UP_POLICY: "${authFieldModel.signUpPolicy}"
|
|
114
130
|
AUTH_PROFILE_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}"
|
|
131
|
+
AUTH_PROFILE_TABLE: "${escapeYamlDoubleQuoted(profileModel.table)}"
|
|
132
|
+
AUTH_PROFILE_PRIMARY_KEY: "${profileModel.primaryKey}"
|
|
133
|
+
AUTH_PROFILE_CREATE_STRATEGY: "${profileModel.createStrategy}"
|
|
134
|
+
AUTH_PROFILE_UPDATE_STRATEGY: "${profileModel.updateStrategy}"
|
|
115
135
|
AUTH_TOKEN_HEADER: "Authorization"
|
|
116
136
|
AUTH_TOKEN_PREFIX: "Bearer"
|
|
117
137
|
`;
|
|
118
138
|
}
|
|
119
|
-
function getSupabaseRuntimeWiringGuide() {
|
|
139
|
+
function getSupabaseRuntimeWiringGuide(profileModel) {
|
|
140
|
+
const profileSection = profileModel.enabled
|
|
141
|
+
? `
|
|
142
|
+
## App Profile Table
|
|
143
|
+
|
|
144
|
+
Identity remains provider-owned in \`auth.users\`. App-facing profile data is generated in
|
|
145
|
+
\`public.${profileModel.table}\` and linked through \`${profileModel.table}.id -> auth.users.id\`.
|
|
146
|
+
|
|
147
|
+
The generated migration:
|
|
148
|
+
|
|
149
|
+
- creates the profile table
|
|
150
|
+
- enables row-level security
|
|
151
|
+
- allows signed-in users to read and update their own profile row
|
|
152
|
+
- protects the generated \`role\` column from self-service profile updates
|
|
153
|
+
- creates a trigger for new auth users when \`AUTH_PROFILE_CREATE_STRATEGY=trigger\`
|
|
154
|
+
`
|
|
155
|
+
: '';
|
|
120
156
|
return `# Supabase Runtime Wiring
|
|
121
157
|
|
|
122
158
|
This file documents the runtime conventions generated by the minikube Supabase adapter.
|
|
@@ -159,6 +195,10 @@ with \`optional: true\` for auth sources.
|
|
|
159
195
|
- \`AUTH_SIGN_UP_OPTIONAL_FIELDS\`
|
|
160
196
|
- \`AUTH_SIGN_UP_POLICY\`
|
|
161
197
|
- \`AUTH_PROFILE_FIELDS\`
|
|
198
|
+
- \`AUTH_PROFILE_TABLE\`
|
|
199
|
+
- \`AUTH_PROFILE_PRIMARY_KEY\`
|
|
200
|
+
- \`AUTH_PROFILE_CREATE_STRATEGY\`
|
|
201
|
+
- \`AUTH_PROFILE_UPDATE_STRATEGY\`
|
|
162
202
|
- \`SUPABASE_SECRET_SYNC_ENABLED\`
|
|
163
203
|
- \`SUPABASE_URL\`
|
|
164
204
|
- \`SUPABASE_ANON_KEY\`
|
|
@@ -166,7 +206,7 @@ with \`optional: true\` for auth sources.
|
|
|
166
206
|
- \`SUPABASE_JWT_SECRET\`
|
|
167
207
|
- \`EXPO_PUBLIC_SUPABASE_URL\`
|
|
168
208
|
- \`EXPO_PUBLIC_SUPABASE_ANON_KEY\`
|
|
169
|
-
|
|
209
|
+
${profileSection}
|
|
170
210
|
## Secret Sync
|
|
171
211
|
|
|
172
212
|
When \`SUPABASE_SECRET_SYNC_ENABLED=true\`, \`scripts/up.sh\` will create/update
|
|
@@ -178,6 +218,9 @@ const DEFAULT_SIGN_UP_REQUIRED_FIELDS = ['email', 'password'];
|
|
|
178
218
|
const DEFAULT_SIGN_UP_OPTIONAL_FIELDS = ['firstName', 'lastName'];
|
|
179
219
|
const DEFAULT_SIGN_UP_POLICY = 'autoSignIn';
|
|
180
220
|
const DEFAULT_PROFILE_FIELDS = ['email', 'firstName', 'lastName'];
|
|
221
|
+
const DEFAULT_PROFILE_PRIMARY_KEY = 'authUserId';
|
|
222
|
+
const DEFAULT_PROFILE_CREATE_STRATEGY = 'trigger';
|
|
223
|
+
const DEFAULT_PROFILE_UPDATE_STRATEGY = 'api';
|
|
181
224
|
function resolveAuthFieldModel(manifest) {
|
|
182
225
|
const signInIdentifiers = normalizeFieldList(manifest.auth?.signIn?.identifiers, DEFAULT_SIGN_IN_IDENTIFIERS);
|
|
183
226
|
const signUpRequiredFields = normalizeFieldList(manifest.auth?.signUp?.requiredFields, DEFAULT_SIGN_UP_REQUIRED_FIELDS);
|
|
@@ -192,6 +235,144 @@ function resolveAuthFieldModel(manifest) {
|
|
|
192
235
|
profileFieldsCsv: profileFields.join(','),
|
|
193
236
|
};
|
|
194
237
|
}
|
|
238
|
+
function resolveProfileModel(manifest) {
|
|
239
|
+
const table = normalizeIdentifier(manifest.auth?.profile?.table ?? '');
|
|
240
|
+
const fields = normalizeFieldList(manifest.auth?.profile?.fields, DEFAULT_PROFILE_FIELDS);
|
|
241
|
+
return {
|
|
242
|
+
enabled: table.length > 0,
|
|
243
|
+
table,
|
|
244
|
+
primaryKey: manifest.auth?.profile?.primaryKey ?? DEFAULT_PROFILE_PRIMARY_KEY,
|
|
245
|
+
createStrategy: manifest.auth?.profile?.createStrategy ?? DEFAULT_PROFILE_CREATE_STRATEGY,
|
|
246
|
+
updateStrategy: manifest.auth?.profile?.updateStrategy ?? DEFAULT_PROFILE_UPDATE_STRATEGY,
|
|
247
|
+
columns: resolveProfileColumns(fields),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function getSupabaseProfileMigration(profileModel) {
|
|
251
|
+
const table = quoteIdentifier(profileModel.table);
|
|
252
|
+
const functionName = quoteIdentifier(`handle_new_${profileModel.table}_user`);
|
|
253
|
+
const triggerName = quoteIdentifier(`on_auth_user_created_${profileModel.table}`);
|
|
254
|
+
const columnDefinitions = profileModel.columns
|
|
255
|
+
.map((column) => ` ${quoteIdentifier(column.column)} ${column.sqlType}`)
|
|
256
|
+
.join(',\n');
|
|
257
|
+
const insertColumns = ['id', ...profileModel.columns.map((column) => column.column)]
|
|
258
|
+
.map(quoteIdentifier)
|
|
259
|
+
.join(', ');
|
|
260
|
+
const insertValues = ['new.id', ...profileModel.columns.map((column) => column.fromNewUser)].join(', ');
|
|
261
|
+
const updateAssignments = profileModel.columns
|
|
262
|
+
.filter((column) => column.field === 'email')
|
|
263
|
+
.map((column) => `${quoteIdentifier(column.column)} = excluded.${quoteIdentifier(column.column)}`);
|
|
264
|
+
const conflictUpdate = [...updateAssignments, 'updated_at = now()'].join(',\n ');
|
|
265
|
+
const updateGrantColumns = profileModel.columns.map((column) => quoteIdentifier(column.column));
|
|
266
|
+
const updateGrantSql = updateGrantColumns.length > 0
|
|
267
|
+
? `grant update (${updateGrantColumns.join(', ')}) on table public.${table} to authenticated;\n`
|
|
268
|
+
: '';
|
|
269
|
+
const triggerSql = profileModel.createStrategy === 'trigger'
|
|
270
|
+
? `
|
|
271
|
+
create or replace function public.${functionName}()
|
|
272
|
+
returns trigger
|
|
273
|
+
language plpgsql
|
|
274
|
+
security definer
|
|
275
|
+
set search_path = public
|
|
276
|
+
as $$
|
|
277
|
+
begin
|
|
278
|
+
insert into public.${table} (${insertColumns})
|
|
279
|
+
values (${insertValues})
|
|
280
|
+
on conflict (id) do update set
|
|
281
|
+
${conflictUpdate};
|
|
282
|
+
|
|
283
|
+
return new;
|
|
284
|
+
end;
|
|
285
|
+
$$;
|
|
286
|
+
|
|
287
|
+
drop trigger if exists ${triggerName} on auth.users;
|
|
288
|
+
create trigger ${triggerName}
|
|
289
|
+
after insert on auth.users
|
|
290
|
+
for each row execute function public.${functionName}();
|
|
291
|
+
`
|
|
292
|
+
: '';
|
|
293
|
+
return `-- Generated by @ankhorage/infra from manifest.infra.auth.profile.
|
|
294
|
+
-- Supabase Auth owns identity in auth.users. App-facing profile data lives in public.${profileModel.table}.
|
|
295
|
+
|
|
296
|
+
create table if not exists public.${table} (
|
|
297
|
+
id uuid primary key references auth.users(id) on delete cascade${columnDefinitions ? `,\n${columnDefinitions}` : ''},
|
|
298
|
+
role text not null default 'user',
|
|
299
|
+
created_at timestamptz not null default now(),
|
|
300
|
+
updated_at timestamptz not null default now()
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
alter table public.${table} enable row level security;
|
|
304
|
+
|
|
305
|
+
revoke all on table public.${table} from anon, authenticated;
|
|
306
|
+
grant select on table public.${table} to authenticated;
|
|
307
|
+
${updateGrantSql}
|
|
308
|
+
create policy ${quoteIdentifier(`${profileModel.table}_select_own`)}
|
|
309
|
+
on public.${table}
|
|
310
|
+
for select
|
|
311
|
+
using (auth.uid() = id);
|
|
312
|
+
|
|
313
|
+
create policy ${quoteIdentifier(`${profileModel.table}_update_own`)}
|
|
314
|
+
on public.${table}
|
|
315
|
+
for update
|
|
316
|
+
using (auth.uid() = id)
|
|
317
|
+
with check (auth.uid() = id);
|
|
318
|
+
${triggerSql}`;
|
|
319
|
+
}
|
|
320
|
+
function resolveProfileColumns(fields) {
|
|
321
|
+
const columns = new Map();
|
|
322
|
+
for (const field of fields) {
|
|
323
|
+
const column = mapProfileFieldToColumn(field);
|
|
324
|
+
if (!column || columns.has(column.column)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
columns.set(column.column, column);
|
|
328
|
+
}
|
|
329
|
+
return [...columns.values()];
|
|
330
|
+
}
|
|
331
|
+
function mapProfileFieldToColumn(field) {
|
|
332
|
+
switch (field) {
|
|
333
|
+
case 'email':
|
|
334
|
+
return { field, column: 'email', sqlType: 'text', fromNewUser: 'new.email' };
|
|
335
|
+
case 'displayName':
|
|
336
|
+
return {
|
|
337
|
+
field,
|
|
338
|
+
column: 'display_name',
|
|
339
|
+
sqlType: 'text',
|
|
340
|
+
fromNewUser: "coalesce(new.raw_user_meta_data->>'displayName', new.raw_user_meta_data->>'display_name', split_part(new.email, '@', 1))",
|
|
341
|
+
};
|
|
342
|
+
case 'firstName':
|
|
343
|
+
return {
|
|
344
|
+
field,
|
|
345
|
+
column: 'first_name',
|
|
346
|
+
sqlType: 'text',
|
|
347
|
+
fromNewUser: "coalesce(new.raw_user_meta_data->>'firstName', new.raw_user_meta_data->>'first_name')",
|
|
348
|
+
};
|
|
349
|
+
case 'lastName':
|
|
350
|
+
return {
|
|
351
|
+
field,
|
|
352
|
+
column: 'last_name',
|
|
353
|
+
sqlType: 'text',
|
|
354
|
+
fromNewUser: "coalesce(new.raw_user_meta_data->>'lastName', new.raw_user_meta_data->>'last_name')",
|
|
355
|
+
};
|
|
356
|
+
case 'avatarUrl':
|
|
357
|
+
return {
|
|
358
|
+
field,
|
|
359
|
+
column: 'avatar_url',
|
|
360
|
+
sqlType: 'text',
|
|
361
|
+
fromNewUser: "coalesce(new.raw_user_meta_data->>'avatarUrl', new.raw_user_meta_data->>'avatar_url')",
|
|
362
|
+
};
|
|
363
|
+
case 'username':
|
|
364
|
+
return {
|
|
365
|
+
field,
|
|
366
|
+
column: 'username',
|
|
367
|
+
sqlType: 'text',
|
|
368
|
+
fromNewUser: "new.raw_user_meta_data->>'username'",
|
|
369
|
+
};
|
|
370
|
+
case 'phone':
|
|
371
|
+
return { field, column: 'phone', sqlType: 'text', fromNewUser: 'new.phone' };
|
|
372
|
+
default:
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
195
376
|
function normalizeFieldList(values, fallback) {
|
|
196
377
|
const source = values ?? fallback;
|
|
197
378
|
const next = [];
|
|
@@ -207,6 +388,18 @@ function normalizeFieldList(values, fallback) {
|
|
|
207
388
|
}
|
|
208
389
|
return [...fallback];
|
|
209
390
|
}
|
|
391
|
+
function normalizeIdentifier(value) {
|
|
392
|
+
const trimmed = value.trim();
|
|
393
|
+
if (!trimmed)
|
|
394
|
+
return '';
|
|
395
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(trimmed)) {
|
|
396
|
+
throw new Error(`Invalid Supabase profile table identifier "${trimmed}". Use snake_case letters, numbers, and underscores.`);
|
|
397
|
+
}
|
|
398
|
+
return trimmed;
|
|
399
|
+
}
|
|
400
|
+
function quoteIdentifier(value) {
|
|
401
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
402
|
+
}
|
|
210
403
|
function escapeYamlDoubleQuoted(value) {
|
|
211
404
|
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
212
405
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/adapters/minikube/auth/supabase/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { InfraManifestInput } from '../../../../types';\nimport type { MinikubeAdapterArtifacts } from '../../contracts';\n\nexport function generateSupabaseAuthArtifacts(args: {\n manifest: InfraManifestInput;\n namespace: string;\n}): MinikubeAdapterArtifacts {\n const { manifest, namespace } = args;\n\n const root = 'infra/minikube/k8s/auth/supabase';\n const resourceRoot = 'auth/supabase';\n const docsRoot = 'infra/minikube/auth';\n const scope = manifest.auth?.scope ?? 'global';\n const authzEngine = manifest.auth?.authorization.engine ?? 'native';\n const authzKind = manifest.auth?.authorization.kind ?? 'RBAC';\n const authFieldModel = resolveAuthFieldModel(manifest);\n\n const warnings: string[] = [];\n if (scope !== 'global') {\n warnings.push(\n `Auth scope \"${scope}\" is not fully modeled yet for Supabase. Baseline resources assume global auth.`,\n );\n }\n\n return {\n files: [\n {\n path: `${root}/supabase-auth.configmap.yaml`,\n content: getSupabaseConfigMap({\n namespace,\n scope,\n authzEngine,\n authzKind,\n authFieldModel,\n }),\n },\n {\n path: `${root}/supabase-auth.secret.yaml`,\n content: getSupabaseSecret(namespace),\n },\n {\n path: `${root}/app-runtime-auth.env.configmap.yaml`,\n content: getRuntimeAuthEnvConfigMap({\n namespace,\n scope,\n authzEngine,\n authzKind,\n authFieldModel,\n }),\n },\n {\n path: `${docsRoot}/supabase-runtime-wiring.md`,\n content: getSupabaseRuntimeWiringGuide(),\n },\n ],\n resources: [\n `${resourceRoot}/supabase-auth.configmap.yaml`,\n `${resourceRoot}/supabase-auth.secret.yaml`,\n `${resourceRoot}/app-runtime-auth.env.configmap.yaml`,\n ],\n envEntries: [\n 'SUPABASE_SECRET_SYNC_ENABLED=true',\n 'SUPABASE_URL=',\n 'SUPABASE_ANON_KEY=',\n 'SUPABASE_SERVICE_ROLE_KEY=',\n 'SUPABASE_JWT_SECRET=',\n 'EXPO_PUBLIC_SUPABASE_URL=',\n 'EXPO_PUBLIC_SUPABASE_ANON_KEY=',\n ],\n warnings,\n };\n}\n\nfunction getSupabaseConfigMap(args: {\n namespace: string;\n scope: string;\n authzEngine: string;\n authzKind: string;\n authFieldModel: ResolvedAuthFieldModel;\n}) {\n const { namespace, scope, authzEngine, authzKind, authFieldModel } = args;\n\n return `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: supabase-auth-config\n namespace: ${namespace}\ndata:\n AUTH_PROVIDER: \"supabase\"\n AUTH_SCOPE: \"${scope}\"\n AUTHZ_ENGINE: \"${authzEngine}\"\n AUTHZ_KIND: \"${authzKind}\"\n AUTH_SIGN_IN_IDENTIFIERS: \"${escapeYamlDoubleQuoted(authFieldModel.signInIdentifiersCsv)}\"\n AUTH_SIGN_UP_REQUIRED_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.signUpRequiredFieldsCsv)}\"\n AUTH_SIGN_UP_OPTIONAL_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}\"\n AUTH_SIGN_UP_POLICY: \"${authFieldModel.signUpPolicy}\"\n AUTH_PROFILE_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}\"\n`;\n}\n\nfunction getSupabaseSecret(namespace: string) {\n return `apiVersion: v1\nkind: Secret\nmetadata:\n name: supabase-auth-secrets\n namespace: ${namespace}\ntype: Opaque\nstringData:\n SUPABASE_URL: \"\"\n SUPABASE_ANON_KEY: \"\"\n SUPABASE_SERVICE_ROLE_KEY: \"\"\n SUPABASE_JWT_SECRET: \"\"\n EXPO_PUBLIC_SUPABASE_URL: \"\"\n EXPO_PUBLIC_SUPABASE_ANON_KEY: \"\"\n`;\n}\n\nfunction getRuntimeAuthEnvConfigMap(args: {\n namespace: string;\n scope: string;\n authzEngine: string;\n authzKind: string;\n authFieldModel: ResolvedAuthFieldModel;\n}) {\n const { namespace, scope, authzEngine, authzKind, authFieldModel } = args;\n\n return `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app-runtime-auth-env\n namespace: ${namespace}\ndata:\n AUTH_REQUIRED: \"true\"\n AUTH_PROVIDER: \"supabase\"\n AUTH_SCOPE: \"${scope}\"\n AUTHZ_ENGINE: \"${authzEngine}\"\n AUTHZ_KIND: \"${authzKind}\"\n AUTH_SIGN_IN_IDENTIFIERS: \"${escapeYamlDoubleQuoted(authFieldModel.signInIdentifiersCsv)}\"\n AUTH_SIGN_UP_REQUIRED_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.signUpRequiredFieldsCsv)}\"\n AUTH_SIGN_UP_OPTIONAL_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}\"\n AUTH_SIGN_UP_POLICY: \"${authFieldModel.signUpPolicy}\"\n AUTH_PROFILE_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}\"\n AUTH_TOKEN_HEADER: \"Authorization\"\n AUTH_TOKEN_PREFIX: \"Bearer\"\n`;\n}\n\nfunction getSupabaseRuntimeWiringGuide() {\n return `# Supabase Runtime Wiring\n\nThis file documents the runtime conventions generated by the minikube Supabase adapter.\n\n## Generated Kubernetes Objects\n\n- \\`ConfigMap/supabase-auth-config\\`: high-level auth metadata.\n- \\`Secret/supabase-auth-secrets\\`: Supabase keys and public runtime keys.\n- \\`ConfigMap/app-runtime-auth-env\\`: auth runtime feature flags and authz metadata.\n\n## App Runtime Convention\n\nYour app runtime deployment should import both:\n\n- \\`app-runtime-auth-env\\` (configMap)\n- \\`supabase-auth-secrets\\` (secret)\n\nExample deployment fragment:\n\n\\`\\`\\`yaml\nenvFrom:\n - configMapRef:\n name: app-runtime-auth-env\n - secretRef:\n name: supabase-auth-secrets\n\\`\\`\\`\n\nThe generated base deployment at \\`k8s/app/deployment.yaml\\` already includes this wiring\nwith \\`optional: true\\` for auth sources.\n\n## Expected Environment Variables\n\n- \\`AUTH_REQUIRED\\`\n- \\`AUTH_PROVIDER\\`\n- \\`AUTH_SCOPE\\`\n- \\`AUTHZ_ENGINE\\`\n- \\`AUTHZ_KIND\\`\n- \\`AUTH_SIGN_IN_IDENTIFIERS\\`\n- \\`AUTH_SIGN_UP_REQUIRED_FIELDS\\`\n- \\`AUTH_SIGN_UP_OPTIONAL_FIELDS\\`\n- \\`AUTH_SIGN_UP_POLICY\\`\n- \\`AUTH_PROFILE_FIELDS\\`\n- \\`SUPABASE_SECRET_SYNC_ENABLED\\`\n- \\`SUPABASE_URL\\`\n- \\`SUPABASE_ANON_KEY\\`\n- \\`SUPABASE_SERVICE_ROLE_KEY\\`\n- \\`SUPABASE_JWT_SECRET\\`\n- \\`EXPO_PUBLIC_SUPABASE_URL\\`\n- \\`EXPO_PUBLIC_SUPABASE_ANON_KEY\\`\n\n## Secret Sync\n\nWhen \\`SUPABASE_SECRET_SYNC_ENABLED=true\\`, \\`scripts/up.sh\\` will create/update\n\\`Secret/supabase-auth-secrets\\` from the loaded environment values.\n`;\n}\n\nconst DEFAULT_SIGN_IN_IDENTIFIERS = ['email'];\nconst DEFAULT_SIGN_UP_REQUIRED_FIELDS = ['email', 'password'];\nconst DEFAULT_SIGN_UP_OPTIONAL_FIELDS = ['firstName', 'lastName'];\nconst DEFAULT_SIGN_UP_POLICY = 'autoSignIn';\nconst DEFAULT_PROFILE_FIELDS = ['email', 'firstName', 'lastName'];\n\ninterface ResolvedAuthFieldModel {\n signInIdentifiersCsv: string;\n signUpRequiredFieldsCsv: string;\n signUpOptionalFieldsCsv: string;\n signUpPolicy: string;\n profileFieldsCsv: string;\n}\n\nfunction resolveAuthFieldModel(manifest: InfraManifestInput): ResolvedAuthFieldModel {\n const signInIdentifiers = normalizeFieldList(\n manifest.auth?.signIn?.identifiers,\n DEFAULT_SIGN_IN_IDENTIFIERS,\n );\n const signUpRequiredFields = normalizeFieldList(\n manifest.auth?.signUp?.requiredFields,\n DEFAULT_SIGN_UP_REQUIRED_FIELDS,\n );\n const signUpOptionalFields = normalizeFieldList(\n manifest.auth?.signUp?.optionalFields,\n DEFAULT_SIGN_UP_OPTIONAL_FIELDS,\n ).filter((field) => !signUpRequiredFields.includes(field));\n const signUpPolicy = manifest.auth?.signUp?.signUpPolicy ?? DEFAULT_SIGN_UP_POLICY;\n const profileFields = normalizeFieldList(manifest.auth?.profile?.fields, DEFAULT_PROFILE_FIELDS);\n\n return {\n signInIdentifiersCsv: signInIdentifiers.join(','),\n signUpRequiredFieldsCsv: signUpRequiredFields.join(','),\n signUpOptionalFieldsCsv: signUpOptionalFields.join(','),\n signUpPolicy,\n profileFieldsCsv: profileFields.join(','),\n };\n}\n\nfunction normalizeFieldList(\n values: readonly string[] | undefined,\n fallback: readonly string[],\n): string[] {\n const source = values ?? fallback;\n const next: string[] = [];\n\n for (const value of source) {\n const normalized = value.trim();\n if (!normalized || next.includes(normalized)) {\n continue;\n }\n next.push(normalized);\n }\n\n if (next.length > 0) {\n return next;\n }\n\n return [...fallback];\n}\n\nfunction escapeYamlDoubleQuoted(value: string): string {\n return value.replaceAll('\\\\', '\\\\\\\\').replaceAll('\"', '\\\\\"');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/adapters/minikube/auth/supabase/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { InfraManifestInput } from '../../../../types';\nimport type { MinikubeAdapterArtifacts } from '../../contracts';\n\nexport function generateSupabaseAuthArtifacts(args: {\n manifest: InfraManifestInput;\n namespace: string;\n}): MinikubeAdapterArtifacts {\n const { manifest, namespace } = args;\n\n const root = 'infra/minikube/k8s/auth/supabase';\n const resourceRoot = 'auth/supabase';\n const docsRoot = 'infra/minikube/auth';\n const migrationRoot = 'infra/minikube/supabase/migrations';\n const scope = manifest.auth?.scope ?? 'global';\n const authzEngine = manifest.auth?.authorization.engine ?? 'native';\n const authzKind = manifest.auth?.authorization.kind ?? 'RBAC';\n const authFieldModel = resolveAuthFieldModel(manifest);\n const profileModel = resolveProfileModel(manifest);\n\n const warnings: string[] = [];\n if (scope !== 'global') {\n warnings.push(\n `Auth scope \"${scope}\" is not fully modeled yet for Supabase. Baseline resources assume global auth.`,\n );\n }\n\n return {\n files: [\n {\n path: `${root}/supabase-auth.configmap.yaml`,\n content: getSupabaseConfigMap({\n namespace,\n scope,\n authzEngine,\n authzKind,\n authFieldModel,\n profileModel,\n }),\n },\n {\n path: `${root}/supabase-auth.secret.yaml`,\n content: getSupabaseSecret(namespace),\n },\n {\n path: `${root}/app-runtime-auth.env.configmap.yaml`,\n content: getRuntimeAuthEnvConfigMap({\n namespace,\n scope,\n authzEngine,\n authzKind,\n authFieldModel,\n profileModel,\n }),\n },\n ...(profileModel.enabled\n ? [\n {\n path: `${migrationRoot}/0001_auth_profiles.sql`,\n content: getSupabaseProfileMigration(profileModel),\n },\n ]\n : []),\n {\n path: `${docsRoot}/supabase-runtime-wiring.md`,\n content: getSupabaseRuntimeWiringGuide(profileModel),\n },\n ],\n resources: [\n `${resourceRoot}/supabase-auth.configmap.yaml`,\n `${resourceRoot}/supabase-auth.secret.yaml`,\n `${resourceRoot}/app-runtime-auth.env.configmap.yaml`,\n ],\n envEntries: [\n 'SUPABASE_SECRET_SYNC_ENABLED=true',\n 'SUPABASE_URL=',\n 'SUPABASE_ANON_KEY=',\n 'SUPABASE_SERVICE_ROLE_KEY=',\n 'SUPABASE_JWT_SECRET=',\n 'EXPO_PUBLIC_SUPABASE_URL=',\n 'EXPO_PUBLIC_SUPABASE_ANON_KEY=',\n ],\n warnings,\n };\n}\n\nfunction getSupabaseConfigMap(args: {\n namespace: string;\n scope: string;\n authzEngine: string;\n authzKind: string;\n authFieldModel: ResolvedAuthFieldModel;\n profileModel: ResolvedProfileModel;\n}) {\n const { namespace, scope, authzEngine, authzKind, authFieldModel, profileModel } = args;\n\n return `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: supabase-auth-config\n namespace: ${namespace}\ndata:\n AUTH_PROVIDER: \"supabase\"\n AUTH_SCOPE: \"${scope}\"\n AUTHZ_ENGINE: \"${authzEngine}\"\n AUTHZ_KIND: \"${authzKind}\"\n AUTH_SIGN_IN_IDENTIFIERS: \"${escapeYamlDoubleQuoted(authFieldModel.signInIdentifiersCsv)}\"\n AUTH_SIGN_UP_REQUIRED_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.signUpRequiredFieldsCsv)}\"\n AUTH_SIGN_UP_OPTIONAL_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}\"\n AUTH_SIGN_UP_POLICY: \"${authFieldModel.signUpPolicy}\"\n AUTH_PROFILE_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}\"\n AUTH_PROFILE_TABLE: \"${escapeYamlDoubleQuoted(profileModel.table)}\"\n AUTH_PROFILE_PRIMARY_KEY: \"${profileModel.primaryKey}\"\n AUTH_PROFILE_CREATE_STRATEGY: \"${profileModel.createStrategy}\"\n AUTH_PROFILE_UPDATE_STRATEGY: \"${profileModel.updateStrategy}\"\n`;\n}\n\nfunction getSupabaseSecret(namespace: string) {\n return `apiVersion: v1\nkind: Secret\nmetadata:\n name: supabase-auth-secrets\n namespace: ${namespace}\ntype: Opaque\nstringData:\n SUPABASE_URL: \"\"\n SUPABASE_ANON_KEY: \"\"\n SUPABASE_SERVICE_ROLE_KEY: \"\"\n SUPABASE_JWT_SECRET: \"\"\n EXPO_PUBLIC_SUPABASE_URL: \"\"\n EXPO_PUBLIC_SUPABASE_ANON_KEY: \"\"\n`;\n}\n\nfunction getRuntimeAuthEnvConfigMap(args: {\n namespace: string;\n scope: string;\n authzEngine: string;\n authzKind: string;\n authFieldModel: ResolvedAuthFieldModel;\n profileModel: ResolvedProfileModel;\n}) {\n const { namespace, scope, authzEngine, authzKind, authFieldModel, profileModel } = args;\n\n return `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app-runtime-auth-env\n namespace: ${namespace}\ndata:\n AUTH_REQUIRED: \"true\"\n AUTH_PROVIDER: \"supabase\"\n AUTH_SCOPE: \"${scope}\"\n AUTHZ_ENGINE: \"${authzEngine}\"\n AUTHZ_KIND: \"${authzKind}\"\n AUTH_SIGN_IN_IDENTIFIERS: \"${escapeYamlDoubleQuoted(authFieldModel.signInIdentifiersCsv)}\"\n AUTH_SIGN_UP_REQUIRED_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.signUpRequiredFieldsCsv)}\"\n AUTH_SIGN_UP_OPTIONAL_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}\"\n AUTH_SIGN_UP_POLICY: \"${authFieldModel.signUpPolicy}\"\n AUTH_PROFILE_FIELDS: \"${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}\"\n AUTH_PROFILE_TABLE: \"${escapeYamlDoubleQuoted(profileModel.table)}\"\n AUTH_PROFILE_PRIMARY_KEY: \"${profileModel.primaryKey}\"\n AUTH_PROFILE_CREATE_STRATEGY: \"${profileModel.createStrategy}\"\n AUTH_PROFILE_UPDATE_STRATEGY: \"${profileModel.updateStrategy}\"\n AUTH_TOKEN_HEADER: \"Authorization\"\n AUTH_TOKEN_PREFIX: \"Bearer\"\n`;\n}\n\nfunction getSupabaseRuntimeWiringGuide(profileModel: ResolvedProfileModel) {\n const profileSection = profileModel.enabled\n ? `\n## App Profile Table\n\nIdentity remains provider-owned in \\`auth.users\\`. App-facing profile data is generated in\n\\`public.${profileModel.table}\\` and linked through \\`${profileModel.table}.id -> auth.users.id\\`.\n\nThe generated migration:\n\n- creates the profile table\n- enables row-level security\n- allows signed-in users to read and update their own profile row\n- protects the generated \\`role\\` column from self-service profile updates\n- creates a trigger for new auth users when \\`AUTH_PROFILE_CREATE_STRATEGY=trigger\\`\n`\n : '';\n\n return `# Supabase Runtime Wiring\n\nThis file documents the runtime conventions generated by the minikube Supabase adapter.\n\n## Generated Kubernetes Objects\n\n- \\`ConfigMap/supabase-auth-config\\`: high-level auth metadata.\n- \\`Secret/supabase-auth-secrets\\`: Supabase keys and public runtime keys.\n- \\`ConfigMap/app-runtime-auth-env\\`: auth runtime feature flags and authz metadata.\n\n## App Runtime Convention\n\nYour app runtime deployment should import both:\n\n- \\`app-runtime-auth-env\\` (configMap)\n- \\`supabase-auth-secrets\\` (secret)\n\nExample deployment fragment:\n\n\\`\\`\\`yaml\nenvFrom:\n - configMapRef:\n name: app-runtime-auth-env\n - secretRef:\n name: supabase-auth-secrets\n\\`\\`\\`\n\nThe generated base deployment at \\`k8s/app/deployment.yaml\\` already includes this wiring\nwith \\`optional: true\\` for auth sources.\n\n## Expected Environment Variables\n\n- \\`AUTH_REQUIRED\\`\n- \\`AUTH_PROVIDER\\`\n- \\`AUTH_SCOPE\\`\n- \\`AUTHZ_ENGINE\\`\n- \\`AUTHZ_KIND\\`\n- \\`AUTH_SIGN_IN_IDENTIFIERS\\`\n- \\`AUTH_SIGN_UP_REQUIRED_FIELDS\\`\n- \\`AUTH_SIGN_UP_OPTIONAL_FIELDS\\`\n- \\`AUTH_SIGN_UP_POLICY\\`\n- \\`AUTH_PROFILE_FIELDS\\`\n- \\`AUTH_PROFILE_TABLE\\`\n- \\`AUTH_PROFILE_PRIMARY_KEY\\`\n- \\`AUTH_PROFILE_CREATE_STRATEGY\\`\n- \\`AUTH_PROFILE_UPDATE_STRATEGY\\`\n- \\`SUPABASE_SECRET_SYNC_ENABLED\\`\n- \\`SUPABASE_URL\\`\n- \\`SUPABASE_ANON_KEY\\`\n- \\`SUPABASE_SERVICE_ROLE_KEY\\`\n- \\`SUPABASE_JWT_SECRET\\`\n- \\`EXPO_PUBLIC_SUPABASE_URL\\`\n- \\`EXPO_PUBLIC_SUPABASE_ANON_KEY\\`\n${profileSection}\n## Secret Sync\n\nWhen \\`SUPABASE_SECRET_SYNC_ENABLED=true\\`, \\`scripts/up.sh\\` will create/update\n\\`Secret/supabase-auth-secrets\\` from the loaded environment values.\n`;\n}\n\nconst DEFAULT_SIGN_IN_IDENTIFIERS = ['email'];\nconst DEFAULT_SIGN_UP_REQUIRED_FIELDS = ['email', 'password'];\nconst DEFAULT_SIGN_UP_OPTIONAL_FIELDS = ['firstName', 'lastName'];\nconst DEFAULT_SIGN_UP_POLICY = 'autoSignIn';\nconst DEFAULT_PROFILE_FIELDS = ['email', 'firstName', 'lastName'];\nconst DEFAULT_PROFILE_PRIMARY_KEY = 'authUserId';\nconst DEFAULT_PROFILE_CREATE_STRATEGY = 'trigger';\nconst DEFAULT_PROFILE_UPDATE_STRATEGY = 'api';\n\ninterface ResolvedAuthFieldModel {\n signInIdentifiersCsv: string;\n signUpRequiredFieldsCsv: string;\n signUpOptionalFieldsCsv: string;\n signUpPolicy: string;\n profileFieldsCsv: string;\n}\n\ninterface ProfileColumnSpec {\n readonly field: string;\n readonly column: string;\n readonly sqlType: string;\n readonly fromNewUser: string;\n}\n\ninterface ResolvedProfileModel {\n readonly enabled: boolean;\n readonly table: string;\n readonly primaryKey: string;\n readonly createStrategy: string;\n readonly updateStrategy: string;\n readonly columns: readonly ProfileColumnSpec[];\n}\n\nfunction resolveAuthFieldModel(manifest: InfraManifestInput): ResolvedAuthFieldModel {\n const signInIdentifiers = normalizeFieldList(\n manifest.auth?.signIn?.identifiers,\n DEFAULT_SIGN_IN_IDENTIFIERS,\n );\n const signUpRequiredFields = normalizeFieldList(\n manifest.auth?.signUp?.requiredFields,\n DEFAULT_SIGN_UP_REQUIRED_FIELDS,\n );\n const signUpOptionalFields = normalizeFieldList(\n manifest.auth?.signUp?.optionalFields,\n DEFAULT_SIGN_UP_OPTIONAL_FIELDS,\n ).filter((field) => !signUpRequiredFields.includes(field));\n const signUpPolicy = manifest.auth?.signUp?.signUpPolicy ?? DEFAULT_SIGN_UP_POLICY;\n const profileFields = normalizeFieldList(manifest.auth?.profile?.fields, DEFAULT_PROFILE_FIELDS);\n\n return {\n signInIdentifiersCsv: signInIdentifiers.join(','),\n signUpRequiredFieldsCsv: signUpRequiredFields.join(','),\n signUpOptionalFieldsCsv: signUpOptionalFields.join(','),\n signUpPolicy,\n profileFieldsCsv: profileFields.join(','),\n };\n}\n\nfunction resolveProfileModel(manifest: InfraManifestInput): ResolvedProfileModel {\n const table = normalizeIdentifier(manifest.auth?.profile?.table ?? '');\n const fields = normalizeFieldList(manifest.auth?.profile?.fields, DEFAULT_PROFILE_FIELDS);\n\n return {\n enabled: table.length > 0,\n table,\n primaryKey: manifest.auth?.profile?.primaryKey ?? DEFAULT_PROFILE_PRIMARY_KEY,\n createStrategy: manifest.auth?.profile?.createStrategy ?? DEFAULT_PROFILE_CREATE_STRATEGY,\n updateStrategy: manifest.auth?.profile?.updateStrategy ?? DEFAULT_PROFILE_UPDATE_STRATEGY,\n columns: resolveProfileColumns(fields),\n };\n}\n\nfunction getSupabaseProfileMigration(profileModel: ResolvedProfileModel): string {\n const table = quoteIdentifier(profileModel.table);\n const functionName = quoteIdentifier(`handle_new_${profileModel.table}_user`);\n const triggerName = quoteIdentifier(`on_auth_user_created_${profileModel.table}`);\n const columnDefinitions = profileModel.columns\n .map((column) => ` ${quoteIdentifier(column.column)} ${column.sqlType}`)\n .join(',\\n');\n const insertColumns = ['id', ...profileModel.columns.map((column) => column.column)]\n .map(quoteIdentifier)\n .join(', ');\n const insertValues = ['new.id', ...profileModel.columns.map((column) => column.fromNewUser)].join(\n ', ',\n );\n const updateAssignments = profileModel.columns\n .filter((column) => column.field === 'email')\n .map(\n (column) => `${quoteIdentifier(column.column)} = excluded.${quoteIdentifier(column.column)}`,\n );\n const conflictUpdate = [...updateAssignments, 'updated_at = now()'].join(',\\n ');\n const updateGrantColumns = profileModel.columns.map((column) => quoteIdentifier(column.column));\n const updateGrantSql =\n updateGrantColumns.length > 0\n ? `grant update (${updateGrantColumns.join(', ')}) on table public.${table} to authenticated;\\n`\n : '';\n const triggerSql =\n profileModel.createStrategy === 'trigger'\n ? `\ncreate or replace function public.${functionName}()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\nbegin\n insert into public.${table} (${insertColumns})\n values (${insertValues})\n on conflict (id) do update set\n ${conflictUpdate};\n\n return new;\nend;\n$$;\n\ndrop trigger if exists ${triggerName} on auth.users;\ncreate trigger ${triggerName}\n after insert on auth.users\n for each row execute function public.${functionName}();\n`\n : '';\n\n return `-- Generated by @ankhorage/infra from manifest.infra.auth.profile.\n-- Supabase Auth owns identity in auth.users. App-facing profile data lives in public.${profileModel.table}.\n\ncreate table if not exists public.${table} (\n id uuid primary key references auth.users(id) on delete cascade${\n columnDefinitions ? `,\\n${columnDefinitions}` : ''\n },\n role text not null default 'user',\n created_at timestamptz not null default now(),\n updated_at timestamptz not null default now()\n);\n\nalter table public.${table} enable row level security;\n\nrevoke all on table public.${table} from anon, authenticated;\ngrant select on table public.${table} to authenticated;\n${updateGrantSql}\ncreate policy ${quoteIdentifier(`${profileModel.table}_select_own`)}\n on public.${table}\n for select\n using (auth.uid() = id);\n\ncreate policy ${quoteIdentifier(`${profileModel.table}_update_own`)}\n on public.${table}\n for update\n using (auth.uid() = id)\n with check (auth.uid() = id);\n${triggerSql}`;\n}\n\nfunction resolveProfileColumns(fields: readonly string[]): ProfileColumnSpec[] {\n const columns = new Map<string, ProfileColumnSpec>();\n\n for (const field of fields) {\n const column = mapProfileFieldToColumn(field);\n if (!column || columns.has(column.column)) {\n continue;\n }\n columns.set(column.column, column);\n }\n\n return [...columns.values()];\n}\n\nfunction mapProfileFieldToColumn(field: string): ProfileColumnSpec | null {\n switch (field) {\n case 'email':\n return { field, column: 'email', sqlType: 'text', fromNewUser: 'new.email' };\n case 'displayName':\n return {\n field,\n column: 'display_name',\n sqlType: 'text',\n fromNewUser:\n \"coalesce(new.raw_user_meta_data->>'displayName', new.raw_user_meta_data->>'display_name', split_part(new.email, '@', 1))\",\n };\n case 'firstName':\n return {\n field,\n column: 'first_name',\n sqlType: 'text',\n fromNewUser:\n \"coalesce(new.raw_user_meta_data->>'firstName', new.raw_user_meta_data->>'first_name')\",\n };\n case 'lastName':\n return {\n field,\n column: 'last_name',\n sqlType: 'text',\n fromNewUser:\n \"coalesce(new.raw_user_meta_data->>'lastName', new.raw_user_meta_data->>'last_name')\",\n };\n case 'avatarUrl':\n return {\n field,\n column: 'avatar_url',\n sqlType: 'text',\n fromNewUser:\n \"coalesce(new.raw_user_meta_data->>'avatarUrl', new.raw_user_meta_data->>'avatar_url')\",\n };\n case 'username':\n return {\n field,\n column: 'username',\n sqlType: 'text',\n fromNewUser: \"new.raw_user_meta_data->>'username'\",\n };\n case 'phone':\n return { field, column: 'phone', sqlType: 'text', fromNewUser: 'new.phone' };\n default:\n return null;\n }\n}\n\nfunction normalizeFieldList(\n values: readonly string[] | undefined,\n fallback: readonly string[],\n): string[] {\n const source = values ?? fallback;\n const next: string[] = [];\n\n for (const value of source) {\n const normalized = value.trim();\n if (!normalized || next.includes(normalized)) {\n continue;\n }\n next.push(normalized);\n }\n\n if (next.length > 0) {\n return next;\n }\n\n return [...fallback];\n}\n\nfunction normalizeIdentifier(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return '';\n\n if (!/^[a-z_][a-z0-9_]*$/.test(trimmed)) {\n throw new Error(\n `Invalid Supabase profile table identifier \"${trimmed}\". Use snake_case letters, numbers, and underscores.`,\n );\n }\n\n return trimmed;\n}\n\nfunction quoteIdentifier(value: string): string {\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\nfunction escapeYamlDoubleQuoted(value: string): string {\n return value.replaceAll('\\\\', '\\\\\\\\').replaceAll('\"', '\\\\\"');\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/infra",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Provider-neutral infrastructure generation: turns app manifests, datasets, auth, state, storage, and deployment config into generated infra artifacts, migrations, seeds, APIs, and runtime dependencies.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/infra#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"openapi"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ankhorage/contracts": "^1.
|
|
36
|
+
"@ankhorage/contracts": "^1.17.0"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"dist",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { InfraManifest } from '@ankhorage/contracts';
|
|
2
|
+
import { describe, expect, test } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
import { generateSupabaseAuthArtifacts } from './index';
|
|
5
|
+
|
|
6
|
+
describe('generateSupabaseAuthArtifacts profile tables', () => {
|
|
7
|
+
test('generates profile metadata env vars and SQL migration when profile table is declared', () => {
|
|
8
|
+
const artifacts = generateSupabaseAuthArtifacts({
|
|
9
|
+
namespace: 'scan-app',
|
|
10
|
+
manifest: createManifest({
|
|
11
|
+
profile: {
|
|
12
|
+
fields: ['email', 'displayName', 'avatarUrl'],
|
|
13
|
+
table: 'profiles',
|
|
14
|
+
primaryKey: 'authUserId',
|
|
15
|
+
createStrategy: 'trigger',
|
|
16
|
+
updateStrategy: 'api',
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const runtimeConfig = artifacts.files.find(
|
|
22
|
+
(file) =>
|
|
23
|
+
file.path === 'infra/minikube/k8s/auth/supabase/app-runtime-auth.env.configmap.yaml',
|
|
24
|
+
);
|
|
25
|
+
expect(runtimeConfig?.content).toContain('AUTH_PROFILE_TABLE: "profiles"');
|
|
26
|
+
expect(runtimeConfig?.content).toContain('AUTH_PROFILE_PRIMARY_KEY: "authUserId"');
|
|
27
|
+
expect(runtimeConfig?.content).toContain('AUTH_PROFILE_CREATE_STRATEGY: "trigger"');
|
|
28
|
+
expect(runtimeConfig?.content).toContain('AUTH_PROFILE_UPDATE_STRATEGY: "api"');
|
|
29
|
+
|
|
30
|
+
const migration = artifacts.files.find(
|
|
31
|
+
(file) => file.path === 'infra/minikube/supabase/migrations/0001_auth_profiles.sql',
|
|
32
|
+
);
|
|
33
|
+
expect(migration?.content).toContain('create table if not exists public."profiles"');
|
|
34
|
+
expect(migration?.content).toContain(
|
|
35
|
+
'id uuid primary key references auth.users(id) on delete cascade',
|
|
36
|
+
);
|
|
37
|
+
expect(migration?.content).toContain('"display_name" text');
|
|
38
|
+
expect(migration?.content).toContain('"avatar_url" text');
|
|
39
|
+
expect(migration?.content).toContain('alter table public."profiles" enable row level security');
|
|
40
|
+
expect(migration?.content).toContain(
|
|
41
|
+
'revoke all on table public."profiles" from anon, authenticated',
|
|
42
|
+
);
|
|
43
|
+
expect(migration?.content).toContain(
|
|
44
|
+
'grant select on table public."profiles" to authenticated',
|
|
45
|
+
);
|
|
46
|
+
expect(migration?.content).toContain(
|
|
47
|
+
'grant update ("email", "display_name", "avatar_url") on table public."profiles" to authenticated',
|
|
48
|
+
);
|
|
49
|
+
expect(migration?.content).not.toContain('grant update ("role")');
|
|
50
|
+
expect(migration?.content).toContain('create policy "profiles_select_own"');
|
|
51
|
+
expect(migration?.content).toContain('create policy "profiles_update_own"');
|
|
52
|
+
expect(migration?.content).toContain('using (auth.uid() = id)');
|
|
53
|
+
expect(migration?.content).toContain('create trigger "on_auth_user_created_profiles"');
|
|
54
|
+
expect(migration?.content).toContain('after insert on auth.users');
|
|
55
|
+
expect(migration?.content).toContain('execute function public."handle_new_profiles_user"()');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('does not generate a migration when profile table is omitted', () => {
|
|
59
|
+
const artifacts = generateSupabaseAuthArtifacts({
|
|
60
|
+
namespace: 'scan-app',
|
|
61
|
+
manifest: createManifest({
|
|
62
|
+
profile: {
|
|
63
|
+
fields: ['email', 'displayName'],
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(artifacts.files.some((file) => file.path.includes('supabase/migrations'))).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function createManifest(auth: Pick<NonNullable<InfraManifest['auth']>, 'profile'>): InfraManifest {
|
|
73
|
+
return {
|
|
74
|
+
deployment: {
|
|
75
|
+
target: 'minikube',
|
|
76
|
+
monitoring: false,
|
|
77
|
+
},
|
|
78
|
+
auth: {
|
|
79
|
+
scope: 'global',
|
|
80
|
+
provider: 'supabase',
|
|
81
|
+
authorization: {
|
|
82
|
+
kind: 'RBAC',
|
|
83
|
+
engine: 'native',
|
|
84
|
+
},
|
|
85
|
+
...auth,
|
|
86
|
+
},
|
|
87
|
+
database: {
|
|
88
|
+
provider: 'supabase',
|
|
89
|
+
tier: 'dev',
|
|
90
|
+
},
|
|
91
|
+
plugins: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -10,10 +10,12 @@ export function generateSupabaseAuthArtifacts(args: {
|
|
|
10
10
|
const root = 'infra/minikube/k8s/auth/supabase';
|
|
11
11
|
const resourceRoot = 'auth/supabase';
|
|
12
12
|
const docsRoot = 'infra/minikube/auth';
|
|
13
|
+
const migrationRoot = 'infra/minikube/supabase/migrations';
|
|
13
14
|
const scope = manifest.auth?.scope ?? 'global';
|
|
14
15
|
const authzEngine = manifest.auth?.authorization.engine ?? 'native';
|
|
15
16
|
const authzKind = manifest.auth?.authorization.kind ?? 'RBAC';
|
|
16
17
|
const authFieldModel = resolveAuthFieldModel(manifest);
|
|
18
|
+
const profileModel = resolveProfileModel(manifest);
|
|
17
19
|
|
|
18
20
|
const warnings: string[] = [];
|
|
19
21
|
if (scope !== 'global') {
|
|
@@ -32,6 +34,7 @@ export function generateSupabaseAuthArtifacts(args: {
|
|
|
32
34
|
authzEngine,
|
|
33
35
|
authzKind,
|
|
34
36
|
authFieldModel,
|
|
37
|
+
profileModel,
|
|
35
38
|
}),
|
|
36
39
|
},
|
|
37
40
|
{
|
|
@@ -46,11 +49,20 @@ export function generateSupabaseAuthArtifacts(args: {
|
|
|
46
49
|
authzEngine,
|
|
47
50
|
authzKind,
|
|
48
51
|
authFieldModel,
|
|
52
|
+
profileModel,
|
|
49
53
|
}),
|
|
50
54
|
},
|
|
55
|
+
...(profileModel.enabled
|
|
56
|
+
? [
|
|
57
|
+
{
|
|
58
|
+
path: `${migrationRoot}/0001_auth_profiles.sql`,
|
|
59
|
+
content: getSupabaseProfileMigration(profileModel),
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
: []),
|
|
51
63
|
{
|
|
52
64
|
path: `${docsRoot}/supabase-runtime-wiring.md`,
|
|
53
|
-
content: getSupabaseRuntimeWiringGuide(),
|
|
65
|
+
content: getSupabaseRuntimeWiringGuide(profileModel),
|
|
54
66
|
},
|
|
55
67
|
],
|
|
56
68
|
resources: [
|
|
@@ -77,8 +89,9 @@ function getSupabaseConfigMap(args: {
|
|
|
77
89
|
authzEngine: string;
|
|
78
90
|
authzKind: string;
|
|
79
91
|
authFieldModel: ResolvedAuthFieldModel;
|
|
92
|
+
profileModel: ResolvedProfileModel;
|
|
80
93
|
}) {
|
|
81
|
-
const { namespace, scope, authzEngine, authzKind, authFieldModel } = args;
|
|
94
|
+
const { namespace, scope, authzEngine, authzKind, authFieldModel, profileModel } = args;
|
|
82
95
|
|
|
83
96
|
return `apiVersion: v1
|
|
84
97
|
kind: ConfigMap
|
|
@@ -95,6 +108,10 @@ data:
|
|
|
95
108
|
AUTH_SIGN_UP_OPTIONAL_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}"
|
|
96
109
|
AUTH_SIGN_UP_POLICY: "${authFieldModel.signUpPolicy}"
|
|
97
110
|
AUTH_PROFILE_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}"
|
|
111
|
+
AUTH_PROFILE_TABLE: "${escapeYamlDoubleQuoted(profileModel.table)}"
|
|
112
|
+
AUTH_PROFILE_PRIMARY_KEY: "${profileModel.primaryKey}"
|
|
113
|
+
AUTH_PROFILE_CREATE_STRATEGY: "${profileModel.createStrategy}"
|
|
114
|
+
AUTH_PROFILE_UPDATE_STRATEGY: "${profileModel.updateStrategy}"
|
|
98
115
|
`;
|
|
99
116
|
}
|
|
100
117
|
|
|
@@ -121,8 +138,9 @@ function getRuntimeAuthEnvConfigMap(args: {
|
|
|
121
138
|
authzEngine: string;
|
|
122
139
|
authzKind: string;
|
|
123
140
|
authFieldModel: ResolvedAuthFieldModel;
|
|
141
|
+
profileModel: ResolvedProfileModel;
|
|
124
142
|
}) {
|
|
125
|
-
const { namespace, scope, authzEngine, authzKind, authFieldModel } = args;
|
|
143
|
+
const { namespace, scope, authzEngine, authzKind, authFieldModel, profileModel } = args;
|
|
126
144
|
|
|
127
145
|
return `apiVersion: v1
|
|
128
146
|
kind: ConfigMap
|
|
@@ -140,12 +158,33 @@ data:
|
|
|
140
158
|
AUTH_SIGN_UP_OPTIONAL_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}"
|
|
141
159
|
AUTH_SIGN_UP_POLICY: "${authFieldModel.signUpPolicy}"
|
|
142
160
|
AUTH_PROFILE_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}"
|
|
161
|
+
AUTH_PROFILE_TABLE: "${escapeYamlDoubleQuoted(profileModel.table)}"
|
|
162
|
+
AUTH_PROFILE_PRIMARY_KEY: "${profileModel.primaryKey}"
|
|
163
|
+
AUTH_PROFILE_CREATE_STRATEGY: "${profileModel.createStrategy}"
|
|
164
|
+
AUTH_PROFILE_UPDATE_STRATEGY: "${profileModel.updateStrategy}"
|
|
143
165
|
AUTH_TOKEN_HEADER: "Authorization"
|
|
144
166
|
AUTH_TOKEN_PREFIX: "Bearer"
|
|
145
167
|
`;
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
function getSupabaseRuntimeWiringGuide() {
|
|
170
|
+
function getSupabaseRuntimeWiringGuide(profileModel: ResolvedProfileModel) {
|
|
171
|
+
const profileSection = profileModel.enabled
|
|
172
|
+
? `
|
|
173
|
+
## App Profile Table
|
|
174
|
+
|
|
175
|
+
Identity remains provider-owned in \`auth.users\`. App-facing profile data is generated in
|
|
176
|
+
\`public.${profileModel.table}\` and linked through \`${profileModel.table}.id -> auth.users.id\`.
|
|
177
|
+
|
|
178
|
+
The generated migration:
|
|
179
|
+
|
|
180
|
+
- creates the profile table
|
|
181
|
+
- enables row-level security
|
|
182
|
+
- allows signed-in users to read and update their own profile row
|
|
183
|
+
- protects the generated \`role\` column from self-service profile updates
|
|
184
|
+
- creates a trigger for new auth users when \`AUTH_PROFILE_CREATE_STRATEGY=trigger\`
|
|
185
|
+
`
|
|
186
|
+
: '';
|
|
187
|
+
|
|
149
188
|
return `# Supabase Runtime Wiring
|
|
150
189
|
|
|
151
190
|
This file documents the runtime conventions generated by the minikube Supabase adapter.
|
|
@@ -188,6 +227,10 @@ with \`optional: true\` for auth sources.
|
|
|
188
227
|
- \`AUTH_SIGN_UP_OPTIONAL_FIELDS\`
|
|
189
228
|
- \`AUTH_SIGN_UP_POLICY\`
|
|
190
229
|
- \`AUTH_PROFILE_FIELDS\`
|
|
230
|
+
- \`AUTH_PROFILE_TABLE\`
|
|
231
|
+
- \`AUTH_PROFILE_PRIMARY_KEY\`
|
|
232
|
+
- \`AUTH_PROFILE_CREATE_STRATEGY\`
|
|
233
|
+
- \`AUTH_PROFILE_UPDATE_STRATEGY\`
|
|
191
234
|
- \`SUPABASE_SECRET_SYNC_ENABLED\`
|
|
192
235
|
- \`SUPABASE_URL\`
|
|
193
236
|
- \`SUPABASE_ANON_KEY\`
|
|
@@ -195,7 +238,7 @@ with \`optional: true\` for auth sources.
|
|
|
195
238
|
- \`SUPABASE_JWT_SECRET\`
|
|
196
239
|
- \`EXPO_PUBLIC_SUPABASE_URL\`
|
|
197
240
|
- \`EXPO_PUBLIC_SUPABASE_ANON_KEY\`
|
|
198
|
-
|
|
241
|
+
${profileSection}
|
|
199
242
|
## Secret Sync
|
|
200
243
|
|
|
201
244
|
When \`SUPABASE_SECRET_SYNC_ENABLED=true\`, \`scripts/up.sh\` will create/update
|
|
@@ -208,6 +251,9 @@ const DEFAULT_SIGN_UP_REQUIRED_FIELDS = ['email', 'password'];
|
|
|
208
251
|
const DEFAULT_SIGN_UP_OPTIONAL_FIELDS = ['firstName', 'lastName'];
|
|
209
252
|
const DEFAULT_SIGN_UP_POLICY = 'autoSignIn';
|
|
210
253
|
const DEFAULT_PROFILE_FIELDS = ['email', 'firstName', 'lastName'];
|
|
254
|
+
const DEFAULT_PROFILE_PRIMARY_KEY = 'authUserId';
|
|
255
|
+
const DEFAULT_PROFILE_CREATE_STRATEGY = 'trigger';
|
|
256
|
+
const DEFAULT_PROFILE_UPDATE_STRATEGY = 'api';
|
|
211
257
|
|
|
212
258
|
interface ResolvedAuthFieldModel {
|
|
213
259
|
signInIdentifiersCsv: string;
|
|
@@ -217,6 +263,22 @@ interface ResolvedAuthFieldModel {
|
|
|
217
263
|
profileFieldsCsv: string;
|
|
218
264
|
}
|
|
219
265
|
|
|
266
|
+
interface ProfileColumnSpec {
|
|
267
|
+
readonly field: string;
|
|
268
|
+
readonly column: string;
|
|
269
|
+
readonly sqlType: string;
|
|
270
|
+
readonly fromNewUser: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface ResolvedProfileModel {
|
|
274
|
+
readonly enabled: boolean;
|
|
275
|
+
readonly table: string;
|
|
276
|
+
readonly primaryKey: string;
|
|
277
|
+
readonly createStrategy: string;
|
|
278
|
+
readonly updateStrategy: string;
|
|
279
|
+
readonly columns: readonly ProfileColumnSpec[];
|
|
280
|
+
}
|
|
281
|
+
|
|
220
282
|
function resolveAuthFieldModel(manifest: InfraManifestInput): ResolvedAuthFieldModel {
|
|
221
283
|
const signInIdentifiers = normalizeFieldList(
|
|
222
284
|
manifest.auth?.signIn?.identifiers,
|
|
@@ -242,6 +304,164 @@ function resolveAuthFieldModel(manifest: InfraManifestInput): ResolvedAuthFieldM
|
|
|
242
304
|
};
|
|
243
305
|
}
|
|
244
306
|
|
|
307
|
+
function resolveProfileModel(manifest: InfraManifestInput): ResolvedProfileModel {
|
|
308
|
+
const table = normalizeIdentifier(manifest.auth?.profile?.table ?? '');
|
|
309
|
+
const fields = normalizeFieldList(manifest.auth?.profile?.fields, DEFAULT_PROFILE_FIELDS);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
enabled: table.length > 0,
|
|
313
|
+
table,
|
|
314
|
+
primaryKey: manifest.auth?.profile?.primaryKey ?? DEFAULT_PROFILE_PRIMARY_KEY,
|
|
315
|
+
createStrategy: manifest.auth?.profile?.createStrategy ?? DEFAULT_PROFILE_CREATE_STRATEGY,
|
|
316
|
+
updateStrategy: manifest.auth?.profile?.updateStrategy ?? DEFAULT_PROFILE_UPDATE_STRATEGY,
|
|
317
|
+
columns: resolveProfileColumns(fields),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getSupabaseProfileMigration(profileModel: ResolvedProfileModel): string {
|
|
322
|
+
const table = quoteIdentifier(profileModel.table);
|
|
323
|
+
const functionName = quoteIdentifier(`handle_new_${profileModel.table}_user`);
|
|
324
|
+
const triggerName = quoteIdentifier(`on_auth_user_created_${profileModel.table}`);
|
|
325
|
+
const columnDefinitions = profileModel.columns
|
|
326
|
+
.map((column) => ` ${quoteIdentifier(column.column)} ${column.sqlType}`)
|
|
327
|
+
.join(',\n');
|
|
328
|
+
const insertColumns = ['id', ...profileModel.columns.map((column) => column.column)]
|
|
329
|
+
.map(quoteIdentifier)
|
|
330
|
+
.join(', ');
|
|
331
|
+
const insertValues = ['new.id', ...profileModel.columns.map((column) => column.fromNewUser)].join(
|
|
332
|
+
', ',
|
|
333
|
+
);
|
|
334
|
+
const updateAssignments = profileModel.columns
|
|
335
|
+
.filter((column) => column.field === 'email')
|
|
336
|
+
.map(
|
|
337
|
+
(column) => `${quoteIdentifier(column.column)} = excluded.${quoteIdentifier(column.column)}`,
|
|
338
|
+
);
|
|
339
|
+
const conflictUpdate = [...updateAssignments, 'updated_at = now()'].join(',\n ');
|
|
340
|
+
const updateGrantColumns = profileModel.columns.map((column) => quoteIdentifier(column.column));
|
|
341
|
+
const updateGrantSql =
|
|
342
|
+
updateGrantColumns.length > 0
|
|
343
|
+
? `grant update (${updateGrantColumns.join(', ')}) on table public.${table} to authenticated;\n`
|
|
344
|
+
: '';
|
|
345
|
+
const triggerSql =
|
|
346
|
+
profileModel.createStrategy === 'trigger'
|
|
347
|
+
? `
|
|
348
|
+
create or replace function public.${functionName}()
|
|
349
|
+
returns trigger
|
|
350
|
+
language plpgsql
|
|
351
|
+
security definer
|
|
352
|
+
set search_path = public
|
|
353
|
+
as $$
|
|
354
|
+
begin
|
|
355
|
+
insert into public.${table} (${insertColumns})
|
|
356
|
+
values (${insertValues})
|
|
357
|
+
on conflict (id) do update set
|
|
358
|
+
${conflictUpdate};
|
|
359
|
+
|
|
360
|
+
return new;
|
|
361
|
+
end;
|
|
362
|
+
$$;
|
|
363
|
+
|
|
364
|
+
drop trigger if exists ${triggerName} on auth.users;
|
|
365
|
+
create trigger ${triggerName}
|
|
366
|
+
after insert on auth.users
|
|
367
|
+
for each row execute function public.${functionName}();
|
|
368
|
+
`
|
|
369
|
+
: '';
|
|
370
|
+
|
|
371
|
+
return `-- Generated by @ankhorage/infra from manifest.infra.auth.profile.
|
|
372
|
+
-- Supabase Auth owns identity in auth.users. App-facing profile data lives in public.${profileModel.table}.
|
|
373
|
+
|
|
374
|
+
create table if not exists public.${table} (
|
|
375
|
+
id uuid primary key references auth.users(id) on delete cascade${
|
|
376
|
+
columnDefinitions ? `,\n${columnDefinitions}` : ''
|
|
377
|
+
},
|
|
378
|
+
role text not null default 'user',
|
|
379
|
+
created_at timestamptz not null default now(),
|
|
380
|
+
updated_at timestamptz not null default now()
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
alter table public.${table} enable row level security;
|
|
384
|
+
|
|
385
|
+
revoke all on table public.${table} from anon, authenticated;
|
|
386
|
+
grant select on table public.${table} to authenticated;
|
|
387
|
+
${updateGrantSql}
|
|
388
|
+
create policy ${quoteIdentifier(`${profileModel.table}_select_own`)}
|
|
389
|
+
on public.${table}
|
|
390
|
+
for select
|
|
391
|
+
using (auth.uid() = id);
|
|
392
|
+
|
|
393
|
+
create policy ${quoteIdentifier(`${profileModel.table}_update_own`)}
|
|
394
|
+
on public.${table}
|
|
395
|
+
for update
|
|
396
|
+
using (auth.uid() = id)
|
|
397
|
+
with check (auth.uid() = id);
|
|
398
|
+
${triggerSql}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function resolveProfileColumns(fields: readonly string[]): ProfileColumnSpec[] {
|
|
402
|
+
const columns = new Map<string, ProfileColumnSpec>();
|
|
403
|
+
|
|
404
|
+
for (const field of fields) {
|
|
405
|
+
const column = mapProfileFieldToColumn(field);
|
|
406
|
+
if (!column || columns.has(column.column)) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
columns.set(column.column, column);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return [...columns.values()];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function mapProfileFieldToColumn(field: string): ProfileColumnSpec | null {
|
|
416
|
+
switch (field) {
|
|
417
|
+
case 'email':
|
|
418
|
+
return { field, column: 'email', sqlType: 'text', fromNewUser: 'new.email' };
|
|
419
|
+
case 'displayName':
|
|
420
|
+
return {
|
|
421
|
+
field,
|
|
422
|
+
column: 'display_name',
|
|
423
|
+
sqlType: 'text',
|
|
424
|
+
fromNewUser:
|
|
425
|
+
"coalesce(new.raw_user_meta_data->>'displayName', new.raw_user_meta_data->>'display_name', split_part(new.email, '@', 1))",
|
|
426
|
+
};
|
|
427
|
+
case 'firstName':
|
|
428
|
+
return {
|
|
429
|
+
field,
|
|
430
|
+
column: 'first_name',
|
|
431
|
+
sqlType: 'text',
|
|
432
|
+
fromNewUser:
|
|
433
|
+
"coalesce(new.raw_user_meta_data->>'firstName', new.raw_user_meta_data->>'first_name')",
|
|
434
|
+
};
|
|
435
|
+
case 'lastName':
|
|
436
|
+
return {
|
|
437
|
+
field,
|
|
438
|
+
column: 'last_name',
|
|
439
|
+
sqlType: 'text',
|
|
440
|
+
fromNewUser:
|
|
441
|
+
"coalesce(new.raw_user_meta_data->>'lastName', new.raw_user_meta_data->>'last_name')",
|
|
442
|
+
};
|
|
443
|
+
case 'avatarUrl':
|
|
444
|
+
return {
|
|
445
|
+
field,
|
|
446
|
+
column: 'avatar_url',
|
|
447
|
+
sqlType: 'text',
|
|
448
|
+
fromNewUser:
|
|
449
|
+
"coalesce(new.raw_user_meta_data->>'avatarUrl', new.raw_user_meta_data->>'avatar_url')",
|
|
450
|
+
};
|
|
451
|
+
case 'username':
|
|
452
|
+
return {
|
|
453
|
+
field,
|
|
454
|
+
column: 'username',
|
|
455
|
+
sqlType: 'text',
|
|
456
|
+
fromNewUser: "new.raw_user_meta_data->>'username'",
|
|
457
|
+
};
|
|
458
|
+
case 'phone':
|
|
459
|
+
return { field, column: 'phone', sqlType: 'text', fromNewUser: 'new.phone' };
|
|
460
|
+
default:
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
245
465
|
function normalizeFieldList(
|
|
246
466
|
values: readonly string[] | undefined,
|
|
247
467
|
fallback: readonly string[],
|
|
@@ -264,6 +484,23 @@ function normalizeFieldList(
|
|
|
264
484
|
return [...fallback];
|
|
265
485
|
}
|
|
266
486
|
|
|
487
|
+
function normalizeIdentifier(value: string): string {
|
|
488
|
+
const trimmed = value.trim();
|
|
489
|
+
if (!trimmed) return '';
|
|
490
|
+
|
|
491
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(trimmed)) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`Invalid Supabase profile table identifier "${trimmed}". Use snake_case letters, numbers, and underscores.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return trimmed;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function quoteIdentifier(value: string): string {
|
|
501
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
502
|
+
}
|
|
503
|
+
|
|
267
504
|
function escapeYamlDoubleQuoted(value: string): string {
|
|
268
505
|
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
269
506
|
}
|