create-kontrolia-auth 2.2.1 → 2.3.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/README.md CHANGED
@@ -16,6 +16,12 @@ Eso hace, en orden:
16
16
  2. **Descarga** el repositorio en `./mi-app` e instala dependencias.
17
17
  3. **Base de datos** — dos caminos independientes:
18
18
  - _Ya tengo Supabase_ (Cloud o self-hosted): pegas URL + keys, sin Docker.
19
+ Si es un proyecto Supabase Cloud, además ofrece **configurar
20
+ automáticamente** los dos ajustes que solo viven en su Dashboard —
21
+ exponer el schema `kontrolia_auth` en la API y activar el Custom Access
22
+ Token Hook — vía la Management API de Supabase (pide un Personal Access
23
+ Token de tu cuenta, distinto a las keys del proyecto; no se guarda). Si
24
+ lo saltas, muestra las instrucciones manuales exactas.
19
25
  - _Crear uno nuevo self-hosted_: genera `docker/.env`, **levanta los
20
26
  contenedores por ti** (`docker compose up -d`) y aplica las migraciones
21
27
  en cuanto Postgres está listo.
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { askApplicationStep } from "./steps/application.js";
5
5
  import { askDatabaseStep } from "./steps/database.js";
6
6
  import { askDeploymentStep } from "./steps/deployment.js";
7
7
  import { bringUpAndMigrate } from "./utils/docker.js";
8
+ import { readEnvFile } from "./utils/files.js";
8
9
  import { runPreflight } from "./utils/preflight.js";
9
10
  import { textOrExit } from "./utils/prompts.js";
10
11
  import { ensureRepo, isInsideRepo } from "./utils/scaffold.js";
@@ -93,8 +94,10 @@ async function runUpdateCommand() {
93
94
  * offer) without repeating the database/application questions. For anyone
94
95
  * who already has everything running and just wants to (re)connect a
95
96
  * deploy target — e.g. adding admin-panel after auth-server was already
96
- * deployed. Doesn't persist Supabase credentials anywhere between runs, so
97
- * it still has to ask for them once here.
97
+ * deployed. A previous install/deploy already wrote these same Supabase
98
+ * values into apps/auth-server/.env.local, so this reuses them instead of
99
+ * asking again every time — only falls back to asking when that file is
100
+ * missing or incomplete (e.g. first run, or the file got deleted).
98
101
  */
99
102
  async function runDeployCommand() {
100
103
  console.clear();
@@ -105,9 +108,40 @@ async function runDeployCommand() {
105
108
  process.exitCode = 1;
106
109
  return;
107
110
  }
108
- const supabaseUrl = await textOrExit("URL de tu proyecto Supabase", "https://tu-proyecto.supabase.co");
109
- const anonKey = await textOrExit("Anon/public key");
110
- const serviceRoleKey = await textOrExit("Service role key (server-only, nunca la expongas al navegador)");
111
+ const existingEnv = await readEnvFile(`${process.cwd()}/apps/auth-server/.env.local`);
112
+ const found = existingEnv?.SUPABASE_URL && existingEnv?.NEXT_PUBLIC_SUPABASE_ANON_KEY && existingEnv?.SUPABASE_SERVICE_ROLE_KEY
113
+ ? {
114
+ supabaseUrl: existingEnv.SUPABASE_URL,
115
+ anonKey: existingEnv.NEXT_PUBLIC_SUPABASE_ANON_KEY,
116
+ serviceRoleKey: existingEnv.SUPABASE_SERVICE_ROLE_KEY,
117
+ }
118
+ : null;
119
+ let supabaseUrl;
120
+ let anonKey;
121
+ let serviceRoleKey;
122
+ if (found) {
123
+ const reuse = await p.confirm({
124
+ message: `Ya tengo las credenciales de Supabase que usaste antes (${found.supabaseUrl}) — ¿las uso, o quieres capturar otras?`,
125
+ initialValue: true,
126
+ });
127
+ if (p.isCancel(reuse)) {
128
+ p.cancel("Cancelado.");
129
+ process.exit(0);
130
+ }
131
+ if (reuse) {
132
+ ({ supabaseUrl, anonKey, serviceRoleKey } = found);
133
+ }
134
+ else {
135
+ supabaseUrl = await textOrExit("URL de tu proyecto Supabase", "https://tu-proyecto.supabase.co");
136
+ anonKey = await textOrExit("Anon/public key");
137
+ serviceRoleKey = await textOrExit("Service role key (server-only, nunca la expongas al navegador)");
138
+ }
139
+ }
140
+ else {
141
+ supabaseUrl = await textOrExit("URL de tu proyecto Supabase", "https://tu-proyecto.supabase.co");
142
+ anonKey = await textOrExit("Anon/public key");
143
+ serviceRoleKey = await textOrExit("Service role key (server-only, nunca la expongas al navegador)");
144
+ }
111
145
  const db = { mode: "existing", databaseUrl: "", supabaseUrl, anonKey, serviceRoleKey };
112
146
  await askDeploymentStep(process.cwd(), db);
113
147
  p.outro("Listo.");
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/steps/database.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAmE/E"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/steps/database.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;CACxB;AAmDD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAiF/E"}
@@ -3,6 +3,49 @@ import { generateJwtSecret } from "../utils/secrets.js";
3
3
  import { generateSupabaseKeys } from "../utils/supabase-keys.js";
4
4
  import { writeEnvFile } from "../utils/files.js";
5
5
  import { textOrExit } from "../utils/prompts.js";
6
+ import { addExposedSchema, enableCustomAccessTokenHook, extractProjectRef } from "../utils/supabase-management-api.js";
7
+ /**
8
+ * Offers to configure the two Supabase Cloud dashboard-only settings
9
+ * (exposed schemas + Custom Access Token Hook) via the Management API
10
+ * instead of leaving them as manual steps — mirrors the Vercel deploy
11
+ * automation: a separate, more powerful "Personal Access Token" (account
12
+ * level, not the project's anon/service keys), captured with masked input
13
+ * and never persisted. Only offered for Supabase Cloud URLs (self-hosted
14
+ * projects and custom domains have no Management API to call). Returns
15
+ * which parts actually succeeded so the caller can fall back to a manual
16
+ * note for whichever one didn't.
17
+ */
18
+ async function tryAutoConfigureSupabaseCloud(supabaseUrl) {
19
+ const projectRef = extractProjectRef(supabaseUrl);
20
+ if (!projectRef)
21
+ return { schemaDone: false, hookDone: false };
22
+ const auto = await p.confirm({
23
+ message: "¿Quieres que configure automáticamente el schema expuesto y el Custom Access Token Hook en tu proyecto Supabase? " +
24
+ "Necesito un Personal Access Token de tu cuenta Supabase (distinto a las keys del proyecto que ya diste).",
25
+ initialValue: true,
26
+ });
27
+ if (p.isCancel(auto) || !auto)
28
+ return { schemaDone: false, hookDone: false };
29
+ p.note("1. Entra a supabase.com/dashboard/account/tokens\n" +
30
+ '2. Dale click a "Generate new token"\n' +
31
+ "3. Ponle un nombre, confirma y cópialo (solo se ve una vez)\n" +
32
+ "4. Pégalo aquí abajo", "Cómo conseguir el Personal Access Token de Supabase");
33
+ const managementToken = await p.password({
34
+ message: "Personal Access Token de Supabase (no se guarda, solo se usa ahora)",
35
+ validate: (value) => (value.trim() ? undefined : "Requerido"),
36
+ });
37
+ if (p.isCancel(managementToken))
38
+ return { schemaDone: false, hookDone: false };
39
+ const schemaSpinner = p.spinner();
40
+ schemaSpinner.start("Exponiendo el schema kontrolia_auth en la API de datos");
41
+ const schemaResult = await addExposedSchema(managementToken, projectRef, "kontrolia_auth");
42
+ schemaSpinner.stop(schemaResult.ok ? "Schema kontrolia_auth expuesto" : `Falló: ${schemaResult.error}`);
43
+ const hookSpinner = p.spinner();
44
+ hookSpinner.start("Activando el Custom Access Token Hook");
45
+ const hookResult = await enableCustomAccessTokenHook(managementToken, projectRef);
46
+ hookSpinner.stop(hookResult.ok ? "Hook activado" : `Falló: ${hookResult.error}`);
47
+ return { schemaDone: schemaResult.ok, hookDone: hookResult.ok };
48
+ }
6
49
  /**
7
50
  * Question A from the architecture plan: where does Postgres + GoTrue come
8
51
  * from? Two fully independent paths — neither assumes Docker.
@@ -32,7 +75,19 @@ export async function askDatabaseStep(repoRoot) {
32
75
  const anonKey = await textOrExit("Anon/public key");
33
76
  const serviceRoleKey = await textOrExit("Service role key (server-only, nunca la expongas al navegador)");
34
77
  const databaseUrl = await textOrExit("Connection string de Postgres", "postgres://postgres:...@db.tu-proyecto.supabase.co:5432/postgres");
35
- p.note("Si tu proyecto es Supabase Cloud, activa el Custom Access Token Hook manualmente en Dashboard → Authentication → Hooks → selecciona kontrolia_auth.custom_access_token_hook. No se puede automatizar por API.", "Paso manual pendiente");
78
+ const { schemaDone, hookDone } = await tryAutoConfigureSupabaseCloud(supabaseUrl);
79
+ if (!schemaDone || !hookDone) {
80
+ const pending = [];
81
+ if (!hookDone)
82
+ pending.push("Authentication → Hooks → activa kontrolia_auth.custom_access_token_hook.");
83
+ if (!schemaDone) {
84
+ pending.push("Project Settings → Data API → en \"Exposed schemas\" agrega kontrolia_auth (queda como: public, graphql_public, kontrolia_auth). " +
85
+ "Sin esto, crear una organización u otras operaciones fallan con \"Invalid schema: kontrolia_auth\".");
86
+ }
87
+ p.note("Si tu proyecto es Supabase Cloud (o self-hosted fuera de nuestro docker-compose), estos ajustes solo se pueden hacer " +
88
+ "desde el Dashboard — la API de Supabase no los expone del todo:\n\n" +
89
+ pending.map((line, i) => `${i + 1}. ${line}`).join("\n"), pending.length > 1 ? "Pasos manuales pendientes" : "Paso manual pendiente");
90
+ }
36
91
  return { mode, databaseUrl, supabaseUrl, anonKey, serviceRoleKey };
37
92
  }
38
93
  const jwtSecret = generateJwtSecret();
@@ -1 +1 @@
1
- {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/steps/deployment.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AA8JpD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAwI3F"}
1
+ {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/steps/deployment.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AA8JpD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAoJ3F"}
@@ -1,5 +1,5 @@
1
1
  import * as p from "@clack/prompts";
2
- import { updateEnvValue, writeEnvFile } from "../utils/files.js";
2
+ import { readEnvFile, updateEnvValue, writeEnvFile } from "../utils/files.js";
3
3
  import { registerOAuthClient } from "../utils/oauth-client.js";
4
4
  import { createVercelProject, detectCurrentBranch, detectGitHubRepo, triggerVercelDeployment } from "../utils/vercel-api.js";
5
5
  const NEXT_STEPS = {
@@ -128,6 +128,14 @@ async function tryAutoCreateVercelProjects(repoRoot, authServerEnv, adminPanelEn
128
128
  * ever generates env files, it never touches the database connection.
129
129
  */
130
130
  export async function askDeploymentStep(repoRoot, db) {
131
+ // A previous install/deploy already wrote these into .env.local — reuse
132
+ // them as editable defaults instead of making the user retype the same
133
+ // URLs every time they run `deploy` again.
134
+ const prevAuthServerEnv = await readEnvFile(`${repoRoot}/apps/auth-server/.env.local`);
135
+ const prevAdminPanelEnv = await readEnvFile(`${repoRoot}/apps/admin-panel/.env.local`);
136
+ const prevAuthServerUrl = prevAdminPanelEnv?.NEXT_PUBLIC_AUTH_SERVER_URL;
137
+ const prevAdminPanelUrl = prevAuthServerEnv?.NEXT_PUBLIC_ADMIN_PANEL_URL;
138
+ const prevCookieDomain = prevAuthServerEnv?.NEXT_PUBLIC_COOKIE_DOMAIN ?? prevAdminPanelEnv?.NEXT_PUBLIC_COOKIE_DOMAIN;
131
139
  const target = await p.select({
132
140
  message: "¿Dónde vas a desplegar auth-server / admin-panel?",
133
141
  options: [
@@ -147,6 +155,7 @@ export async function askDeploymentStep(repoRoot, db) {
147
155
  message: "¿En qué URL va a vivir auth-server? (admin-panel la usa para enviar ahí a quien no tenga sesión)",
148
156
  placeholder: "http://localhost:3000",
149
157
  defaultValue: "http://localhost:3000",
158
+ initialValue: prevAuthServerUrl,
150
159
  });
151
160
  if (p.isCancel(authServerUrl)) {
152
161
  p.cancel("Instalación cancelada.");
@@ -156,6 +165,7 @@ export async function askDeploymentStep(repoRoot, db) {
156
165
  message: "¿Y en qué URL va a vivir admin-panel? (auth-server la usa para regresar ahí después de iniciar sesión, en vez de mandarte a su propia pantalla de inicio)",
157
166
  placeholder: "http://localhost:3001",
158
167
  defaultValue: "http://localhost:3001",
168
+ initialValue: prevAdminPanelUrl,
159
169
  });
160
170
  if (p.isCancel(adminPanelUrl)) {
161
171
  p.cancel("Instalación cancelada.");
@@ -166,6 +176,7 @@ export async function askDeploymentStep(repoRoot, db) {
166
176
  "Si es así, escribe el dominio compartido empezando con un punto (ej. .tuempresa.com). Si comparten el mismo host, o no lo sabes todavía, déjalo vacío.",
167
177
  placeholder: "",
168
178
  defaultValue: "",
179
+ initialValue: prevCookieDomain,
169
180
  });
170
181
  if (p.isCancel(cookieDomain)) {
171
182
  p.cancel("Instalación cancelada.");
@@ -9,4 +9,6 @@ export declare function writeEnvFile(path: string, values: EnvValues): Promise<v
9
9
  */
10
10
  export declare function updateEnvValue(path: string, key: string, value: string): Promise<void>;
11
11
  export declare function fileExists(path: string): boolean;
12
+ /** Parses a simple `KEY=value` .env file (no quoting/escaping) — null if it doesn't exist. */
13
+ export declare function readEnvFile(path: string): Promise<EnvValues | null>;
12
14
  //# sourceMappingURL=files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/utils/files.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjF;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ5F;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD"}
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/utils/files.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjF;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ5F;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,8FAA8F;AAC9F,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAYzE"}
@@ -23,3 +23,18 @@ export async function updateEnvValue(path, key, value) {
23
23
  export function fileExists(path) {
24
24
  return existsSync(path);
25
25
  }
26
+ /** Parses a simple `KEY=value` .env file (no quoting/escaping) — null if it doesn't exist. */
27
+ export async function readEnvFile(path) {
28
+ if (!existsSync(path))
29
+ return null;
30
+ const contents = await readFile(path, "utf8");
31
+ const values = {};
32
+ for (const line of contents.split("\n")) {
33
+ const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
34
+ if (match) {
35
+ const [, key, value] = match;
36
+ values[key] = value.trim();
37
+ }
38
+ }
39
+ return values;
40
+ }
@@ -0,0 +1,28 @@
1
+ export interface ManagementApiResult {
2
+ ok: boolean;
3
+ /** Only present when ok is false. */
4
+ error?: string;
5
+ }
6
+ /**
7
+ * Extracts the project ref (subdomain) from a Supabase Cloud URL, e.g.
8
+ * "https://abcxyz.supabase.co" -> "abcxyz". Returns null for anything else
9
+ * (self-hosted, a custom domain) — those aren't reachable through Supabase's
10
+ * account-level Management API, which only knows about Cloud projects.
11
+ */
12
+ export declare function extractProjectRef(supabaseUrl: string): string | null;
13
+ /**
14
+ * Adds a schema to the project's exposed-schemas list (PostgREST's
15
+ * `db_schema`) if it isn't there already — idempotent, and preserves
16
+ * whatever schemas were already exposed instead of overwriting them, since
17
+ * `db_schema` is a single string representing the *entire* list, not a
18
+ * per-schema toggle.
19
+ */
20
+ export declare function addExposedSchema(token: string, projectRef: string, schema: string): Promise<ManagementApiResult>;
21
+ /**
22
+ * Enables the Custom Access Token Hook against
23
+ * kontrolia_auth.custom_access_token_hook — the function that injects
24
+ * organization_id/roles/permissions into the JWT. Cloud projects' database
25
+ * is always named "postgres", so the pg-functions:// URI is fixed.
26
+ */
27
+ export declare function enableCustomAccessTokenHook(token: string, projectRef: string): Promise<ManagementApiResult>;
28
+ //# sourceMappingURL=supabase-management-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supabase-management-api.d.ts","sourceRoot":"","sources":["../../src/utils/supabase-management-api.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGpE;AASD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+BtH;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAgBjH"}
@@ -0,0 +1,84 @@
1
+ const CUSTOM_ACCESS_TOKEN_HOOK_URI = "pg-functions://postgres/kontrolia_auth/custom_access_token_hook";
2
+ /**
3
+ * Extracts the project ref (subdomain) from a Supabase Cloud URL, e.g.
4
+ * "https://abcxyz.supabase.co" -> "abcxyz". Returns null for anything else
5
+ * (self-hosted, a custom domain) — those aren't reachable through Supabase's
6
+ * account-level Management API, which only knows about Cloud projects.
7
+ */
8
+ export function extractProjectRef(supabaseUrl) {
9
+ const match = supabaseUrl.match(/^https:\/\/([a-z0-9-]+)\.supabase\.co\/?$/);
10
+ return match ? match[1] : null;
11
+ }
12
+ async function managementApiFetch(path, token, init) {
13
+ return fetch(`https://api.supabase.com/v1${path}`, {
14
+ ...init,
15
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init?.headers },
16
+ });
17
+ }
18
+ /**
19
+ * Adds a schema to the project's exposed-schemas list (PostgREST's
20
+ * `db_schema`) if it isn't there already — idempotent, and preserves
21
+ * whatever schemas were already exposed instead of overwriting them, since
22
+ * `db_schema` is a single string representing the *entire* list, not a
23
+ * per-schema toggle.
24
+ */
25
+ export async function addExposedSchema(token, projectRef, schema) {
26
+ let getRes;
27
+ try {
28
+ getRes = await managementApiFetch(`/projects/${projectRef}/postgrest`, token);
29
+ }
30
+ catch (error) {
31
+ return { ok: false, error: error.message };
32
+ }
33
+ if (!getRes.ok) {
34
+ const body = (await getRes.json().catch(() => null));
35
+ return { ok: false, error: body?.message ?? `HTTP ${getRes.status} leyendo la config actual` };
36
+ }
37
+ const current = (await getRes.json());
38
+ const schemas = (current.db_schema ?? "public")
39
+ .split(",")
40
+ .map((s) => s.trim())
41
+ .filter(Boolean);
42
+ if (schemas.includes(schema))
43
+ return { ok: true };
44
+ schemas.push(schema);
45
+ let patchRes;
46
+ try {
47
+ patchRes = await managementApiFetch(`/projects/${projectRef}/postgrest`, token, {
48
+ method: "PATCH",
49
+ body: JSON.stringify({ db_schema: schemas.join(",") }),
50
+ });
51
+ }
52
+ catch (error) {
53
+ return { ok: false, error: error.message };
54
+ }
55
+ if (patchRes.ok)
56
+ return { ok: true };
57
+ const body = (await patchRes.json().catch(() => null));
58
+ return { ok: false, error: body?.message ?? `HTTP ${patchRes.status}` };
59
+ }
60
+ /**
61
+ * Enables the Custom Access Token Hook against
62
+ * kontrolia_auth.custom_access_token_hook — the function that injects
63
+ * organization_id/roles/permissions into the JWT. Cloud projects' database
64
+ * is always named "postgres", so the pg-functions:// URI is fixed.
65
+ */
66
+ export async function enableCustomAccessTokenHook(token, projectRef) {
67
+ let res;
68
+ try {
69
+ res = await managementApiFetch(`/projects/${projectRef}/config/auth`, token, {
70
+ method: "PATCH",
71
+ body: JSON.stringify({
72
+ hook_custom_access_token_enabled: true,
73
+ hook_custom_access_token_uri: CUSTOM_ACCESS_TOKEN_HOOK_URI,
74
+ }),
75
+ });
76
+ }
77
+ catch (error) {
78
+ return { ok: false, error: error.message };
79
+ }
80
+ if (res.ok)
81
+ return { ok: true };
82
+ const body = (await res.json().catch(() => null));
83
+ return { ok: false, error: body?.message ?? `HTTP ${res.status}` };
84
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kontrolia-auth",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "license": "MIT",
5
5
  "description": "Turnkey installer for KontrolIA Auth. `npx create-kontrolia-auth mi-app` checks requirements, downloads the repo, installs deps, and runs a wizard: (1) connect to an existing Supabase project or create a new self-hosted one via Docker; (2) where to deploy auth-server/admin-panel (Docker, Vercel, Railway, Render, Coolify). Also exposes `update`, `migrate` and `doctor` subcommands.",
6
6
  "keywords": [