create-kontrolia-auth 2.3.0 → 2.4.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
@@ -17,11 +17,13 @@ Eso hace, en orden:
17
17
  3. **Base de datos** — dos caminos independientes:
18
18
  - _Ya tengo Supabase_ (Cloud o self-hosted): pegas URL + keys, sin Docker.
19
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.
20
+ automáticamente** los tres ajustes que solo viven en su Dashboard —
21
+ exponer el schema `kontrolia_auth` en la API, activar el Custom Access
22
+ Token Hook, y activar el servidor OAuth 2.1 (para que apps de otros
23
+ dominios puedan iniciar sesión) vía la Management API de Supabase
24
+ (pide un Personal Access Token de tu cuenta, distinto a las keys del
25
+ proyecto; no se guarda). Si lo saltas, muestra las instrucciones
26
+ manuales exactas para lo que falte.
25
27
  - _Crear uno nuevo self-hosted_: genera `docker/.env`, **levanta los
26
28
  contenedores por ti** (`docker compose up -d`) y aplica las migraciones
27
29
  en cuanto Postgres está listo.
@@ -49,11 +51,20 @@ pnpm --filter create-kontrolia-auth dev
49
51
  | `npx create-kontrolia-auth update` | Descarga el código nuevo (`git pull`) sobre una copia ya instalada, instala dependencias, y aplica las migraciones nuevas. |
50
52
  | `npx create-kontrolia-auth deploy` | Va directo al paso de despliegue (URLs, cliente OAuth, `.env.local`, creación automática en Vercel) sin repetir las preguntas de base de datos ni de aplicación — para cuando ya tienes todo corriendo y solo quieres (re)conectar auth-server o admin-panel a un nuevo destino. |
51
53
  | `npx create-kontrolia-auth migrate` | Aplica/re-aplica las migraciones contra una base existente. |
54
+ | `npx create-kontrolia-auth grant-admin <email>` | Otorga platform admin a un usuario ya registrado, directo contra la base de datos (connection string, sin pasar por login). Es el camino de recuperación cuando no hay ningún platform admin — por ejemplo si instalaste sobre un proyecto Supabase que ya tenía usuarios de otra app, y por eso el primer registro no se ascendió automáticamente. |
52
55
  | `npx create-kontrolia-auth doctor` | Solo revisa que tengas Node, pnpm, git y Docker. |
53
56
 
54
57
  Las migraciones son **idempotentes** (solo tocan el schema `kontrolia_auth`), así
55
58
  que `migrate` es seguro de correr las veces que necesites.
56
59
 
60
+ `grant-admin` usa el mismo modelo de confianza que `migrate`: quien tiene la
61
+ connection string de Postgres ya puede hacer cualquier cosa en esa base de
62
+ datos, así que no pasa por el login ni por el check de `is_platform_admin` de
63
+ la app — por diseño, es la única vía que funciona cuando no hay ningún
64
+ platform admin (la página "Platform admins" del admin-panel requiere ya serlo
65
+ para poder usarla). Una vez que tengas al menos uno, agregar más se hace desde
66
+ ahí, sin volver a tocar la terminal.
67
+
57
68
  `update` es para quien instaló con `npx create-kontrolia-auth mi-app` y ya
58
69
  tiene esa carpeta corriendo — no descarta cambios locales (se detiene si hay
59
70
  algo sin guardar) y solo avanza si puede hacer fast-forward sobre `origin/main`.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import * as p from "@clack/prompts";
3
- import { migrate } from "@kontrolia/db";
3
+ import { grantPlatformAdmin, migrate } from "@kontrolia/db";
4
4
  import { askApplicationStep } from "./steps/application.js";
5
5
  import { askDatabaseStep } from "./steps/database.js";
6
6
  import { askDeploymentStep } from "./steps/deployment.js";
@@ -42,6 +42,51 @@ async function runMigrateCommand() {
42
42
  process.exitCode = 1;
43
43
  p.outro(ok ? "Base de datos al día." : "Revisa la connection string y vuelve a intentar.");
44
44
  }
45
+ /**
46
+ * `create-kontrolia-auth grant-admin <email>` — the break-glass recovery
47
+ * path for platform admins. The normal way to grant one (admin-panel's
48
+ * "Platform admins" page) requires already being a platform admin to call
49
+ * its API — which is exactly the problem when the automatic first-admin
50
+ * bootstrap didn't fire (e.g. installing onto an existing Supabase project
51
+ * that already had unrelated `auth.users` rows, so the "first user ever"
52
+ * check never matched) or a project ends up with zero admins some other
53
+ * way. This bypasses the app's authorization layer entirely, same trust
54
+ * model as `migrate`: whoever has the Postgres connection string can
55
+ * already do anything in that database.
56
+ */
57
+ async function runGrantAdminCommand(email) {
58
+ console.clear();
59
+ p.intro("KontrolIA Auth — otorgar platform admin");
60
+ if (!email) {
61
+ p.log.error("Uso: npx create-kontrolia-auth grant-admin <email>");
62
+ p.outro("Falta el correo.");
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const url = await p.text({
67
+ message: "Connection string de Postgres",
68
+ placeholder: "postgres://postgres:...@host:5432/postgres",
69
+ });
70
+ if (p.isCancel(url) || !url) {
71
+ p.cancel("Cancelado.");
72
+ process.exit(0);
73
+ }
74
+ const s = p.spinner();
75
+ s.start(`Buscando a ${email} y otorgando platform admin`);
76
+ try {
77
+ const result = await grantPlatformAdmin({ connectionString: url, email });
78
+ s.stop(result.alreadyGranted
79
+ ? `${result.email} ya era platform admin — nada que hacer.`
80
+ : `${result.email} ahora es platform admin.`);
81
+ p.outro("Listo. Si tenía una sesión abierta, debe cerrarla y volver a entrar para que el token nuevo lleve el claim.");
82
+ }
83
+ catch (error) {
84
+ s.stop("Falló");
85
+ p.log.error(error.message);
86
+ process.exitCode = 1;
87
+ p.outro("Revisa el correo y la connection string, y vuelve a intentar.");
88
+ }
89
+ }
45
90
  /** `create-kontrolia-auth doctor` — just the requirements check. */
46
91
  async function runDoctor() {
47
92
  console.clear();
@@ -207,6 +252,8 @@ async function main() {
207
252
  return runUpdateCommand();
208
253
  if (first === "deploy")
209
254
  return runDeployCommand();
255
+ if (first === "grant-admin")
256
+ return runGrantAdminCommand(args[1]);
210
257
  // `create <dir>` / `install <dir>` / `<dir>` / (nothing) all reach install.
211
258
  const dir = first === "create" || first === "install" ? args[1] : first;
212
259
  return runInstall(dir);
@@ -1 +1 @@
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"}
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;AA2DD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAwF/E"}
@@ -3,29 +3,30 @@ 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";
6
+ import { addExposedSchema, enableCustomAccessTokenHook, enableOAuthServer, extractProjectRef } from "../utils/supabase-management-api.js";
7
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.
8
+ * Offers to configure the three Supabase Cloud dashboard-only settings
9
+ * (exposed schemas, Custom Access Token Hook, OAuth 2.1 server) via the
10
+ * Management API instead of leaving them as manual steps — mirrors the
11
+ * Vercel deploy automation: a separate, more powerful "Personal Access
12
+ * Token" (account level, not the project's anon/service keys), captured
13
+ * with masked input and never persisted. Only offered for Supabase Cloud
14
+ * URLs (self-hosted projects and custom domains have no Management API to
15
+ * call). Returns which parts actually succeeded so the caller can fall back
16
+ * to a manual note for whichever one didn't.
17
17
  */
18
18
  async function tryAutoConfigureSupabaseCloud(supabaseUrl) {
19
19
  const projectRef = extractProjectRef(supabaseUrl);
20
20
  if (!projectRef)
21
- return { schemaDone: false, hookDone: false };
21
+ return { schemaDone: false, hookDone: false, oauthServerDone: false };
22
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? " +
23
+ message: "¿Quieres que configure automáticamente el schema expuesto, el Custom Access Token Hook, y el servidor OAuth 2.1 " +
24
+ "(necesario para que otras apps de dominios distintos inicien sesión) en tu proyecto Supabase? " +
24
25
  "Necesito un Personal Access Token de tu cuenta Supabase (distinto a las keys del proyecto que ya diste).",
25
26
  initialValue: true,
26
27
  });
27
28
  if (p.isCancel(auto) || !auto)
28
- return { schemaDone: false, hookDone: false };
29
+ return { schemaDone: false, hookDone: false, oauthServerDone: false };
29
30
  p.note("1. Entra a supabase.com/dashboard/account/tokens\n" +
30
31
  '2. Dale click a "Generate new token"\n' +
31
32
  "3. Ponle un nombre, confirma y cópialo (solo se ve una vez)\n" +
@@ -35,7 +36,7 @@ async function tryAutoConfigureSupabaseCloud(supabaseUrl) {
35
36
  validate: (value) => (value.trim() ? undefined : "Requerido"),
36
37
  });
37
38
  if (p.isCancel(managementToken))
38
- return { schemaDone: false, hookDone: false };
39
+ return { schemaDone: false, hookDone: false, oauthServerDone: false };
39
40
  const schemaSpinner = p.spinner();
40
41
  schemaSpinner.start("Exponiendo el schema kontrolia_auth en la API de datos");
41
42
  const schemaResult = await addExposedSchema(managementToken, projectRef, "kontrolia_auth");
@@ -44,7 +45,11 @@ async function tryAutoConfigureSupabaseCloud(supabaseUrl) {
44
45
  hookSpinner.start("Activando el Custom Access Token Hook");
45
46
  const hookResult = await enableCustomAccessTokenHook(managementToken, projectRef);
46
47
  hookSpinner.stop(hookResult.ok ? "Hook activado" : `Falló: ${hookResult.error}`);
47
- return { schemaDone: schemaResult.ok, hookDone: hookResult.ok };
48
+ const oauthSpinner = p.spinner();
49
+ oauthSpinner.start("Activando el servidor OAuth 2.1");
50
+ const oauthResult = await enableOAuthServer(managementToken, projectRef);
51
+ oauthSpinner.stop(oauthResult.ok ? "Servidor OAuth 2.1 activado" : `Falló: ${oauthResult.error}`);
52
+ return { schemaDone: schemaResult.ok, hookDone: hookResult.ok, oauthServerDone: oauthResult.ok };
48
53
  }
49
54
  /**
50
55
  * Question A from the architecture plan: where does Postgres + GoTrue come
@@ -75,8 +80,8 @@ export async function askDatabaseStep(repoRoot) {
75
80
  const anonKey = await textOrExit("Anon/public key");
76
81
  const serviceRoleKey = await textOrExit("Service role key (server-only, nunca la expongas al navegador)");
77
82
  const databaseUrl = await textOrExit("Connection string de Postgres", "postgres://postgres:...@db.tu-proyecto.supabase.co:5432/postgres");
78
- const { schemaDone, hookDone } = await tryAutoConfigureSupabaseCloud(supabaseUrl);
79
- if (!schemaDone || !hookDone) {
83
+ const { schemaDone, hookDone, oauthServerDone } = await tryAutoConfigureSupabaseCloud(supabaseUrl);
84
+ if (!schemaDone || !hookDone || !oauthServerDone) {
80
85
  const pending = [];
81
86
  if (!hookDone)
82
87
  pending.push("Authentication → Hooks → activa kontrolia_auth.custom_access_token_hook.");
@@ -84,6 +89,11 @@ export async function askDatabaseStep(repoRoot) {
84
89
  pending.push("Project Settings → Data API → en \"Exposed schemas\" agrega kontrolia_auth (queda como: public, graphql_public, kontrolia_auth). " +
85
90
  "Sin esto, crear una organización u otras operaciones fallan con \"Invalid schema: kontrolia_auth\".");
86
91
  }
92
+ if (!oauthServerDone) {
93
+ pending.push("Servidor OAuth 2.1 (necesario para que otras apps de dominios distintos inicien sesión): actívalo desde el Dashboard si ya " +
94
+ "tiene el toggle disponible (función en beta) — sin él, el registro de clientes OAuth y el login desde otros dominios fallan " +
95
+ 'con "OAuth server is disabled".');
96
+ }
87
97
  p.note("Si tu proyecto es Supabase Cloud (o self-hosted fuera de nuestro docker-compose), estos ajustes solo se pueden hacer " +
88
98
  "desde el Dashboard — la API de Supabase no los expone del todo:\n\n" +
89
99
  pending.map((line, i) => `${i + 1}. ${line}`).join("\n"), pending.length > 1 ? "Pasos manuales pendientes" : "Paso manual pendiente");
@@ -25,4 +25,16 @@ export declare function addExposedSchema(token: string, projectRef: string, sche
25
25
  * is always named "postgres", so the pg-functions:// URI is fixed.
26
26
  */
27
27
  export declare function enableCustomAccessTokenHook(token: string, projectRef: string): Promise<ManagementApiResult>;
28
+ /**
29
+ * Enables GoTrue's OAuth 2.1 authorization server — what makes cross-domain
30
+ * SSO possible (admin-panel exchanging a PKCE code for its own session
31
+ * instead of relying on a shared cookie). Without this, registering an OAuth
32
+ * client during install/deploy silently fails with
33
+ * `{"error_code":"feature_disabled","msg":"OAuth server is disabled"}`, and
34
+ * any third-party app trying to log in via the OAuth flow hits the same
35
+ * error. The API rejects `oauth_server_enabled` unless
36
+ * `oauth_server_authorization_path` is sent in the same request, even though
37
+ * conceptually it's just an on/off toggle.
38
+ */
39
+ export declare function enableOAuthServer(token: string, projectRef: string): Promise<ManagementApiResult>;
28
40
  //# sourceMappingURL=supabase-management-api.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"supabase-management-api.d.ts","sourceRoot":"","sources":["../../src/utils/supabase-management-api.ts"],"names":[],"mappings":"AAIA,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;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAgBvG"}
@@ -1,4 +1,6 @@
1
1
  const CUSTOM_ACCESS_TOKEN_HOOK_URI = "pg-functions://postgres/kontrolia_auth/custom_access_token_hook";
2
+ /** GoTrue's internal route for the OAuth 2.1 authorization endpoint — the API rejects oauth_server_enabled without it. */
3
+ const OAUTH_SERVER_AUTHORIZATION_PATH = "/oauth/authorize";
2
4
  /**
3
5
  * Extracts the project ref (subdomain) from a Supabase Cloud URL, e.g.
4
6
  * "https://abcxyz.supabase.co" -> "abcxyz". Returns null for anything else
@@ -82,3 +84,33 @@ export async function enableCustomAccessTokenHook(token, projectRef) {
82
84
  const body = (await res.json().catch(() => null));
83
85
  return { ok: false, error: body?.message ?? `HTTP ${res.status}` };
84
86
  }
87
+ /**
88
+ * Enables GoTrue's OAuth 2.1 authorization server — what makes cross-domain
89
+ * SSO possible (admin-panel exchanging a PKCE code for its own session
90
+ * instead of relying on a shared cookie). Without this, registering an OAuth
91
+ * client during install/deploy silently fails with
92
+ * `{"error_code":"feature_disabled","msg":"OAuth server is disabled"}`, and
93
+ * any third-party app trying to log in via the OAuth flow hits the same
94
+ * error. The API rejects `oauth_server_enabled` unless
95
+ * `oauth_server_authorization_path` is sent in the same request, even though
96
+ * conceptually it's just an on/off toggle.
97
+ */
98
+ export async function enableOAuthServer(token, projectRef) {
99
+ let res;
100
+ try {
101
+ res = await managementApiFetch(`/projects/${projectRef}/config/auth`, token, {
102
+ method: "PATCH",
103
+ body: JSON.stringify({
104
+ oauth_server_enabled: true,
105
+ oauth_server_authorization_path: OAUTH_SERVER_AUTHORIZATION_PATH,
106
+ }),
107
+ });
108
+ }
109
+ catch (error) {
110
+ return { ok: false, error: error.message };
111
+ }
112
+ if (res.ok)
113
+ return { ok: true };
114
+ const body = (await res.json().catch(() => null));
115
+ return { ok: false, error: body?.message ?? `HTTP ${res.status}` };
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kontrolia-auth",
3
- "version": "2.3.0",
3
+ "version": "2.4.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": [
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "@clack/prompts": "^0.9.1",
37
37
  "jose": "^5.9.6",
38
- "@kontrolia/db": "2.0.0"
38
+ "@kontrolia/db": "2.1.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.10.2",