create-kontrolia-auth 1.0.0 → 1.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/steps/deployment.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAYpD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC3F"}
1
+ {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/steps/deployment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAYpD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAiI3F"}
@@ -1,5 +1,6 @@
1
1
  import * as p from "@clack/prompts";
2
- import { writeEnvFile } from "../utils/files.js";
2
+ import { updateEnvValue, writeEnvFile } from "../utils/files.js";
3
+ import { registerOAuthClient } from "../utils/oauth-client.js";
3
4
  const NEXT_STEPS = {
4
5
  docker: "Se despliegan como parte del mismo docker/docker-compose.yml (servicios auth-server y admin-panel).",
5
6
  vercel: "vercel deploy en apps/auth-server y apps/admin-panel por separado, configurando las mismas variables en el dashboard de Vercel.",
@@ -27,6 +28,63 @@ export async function askDeploymentStep(repoRoot, db) {
27
28
  p.cancel("Instalación cancelada.");
28
29
  process.exit(0);
29
30
  }
31
+ const authServerUrl = await p.text({
32
+ message: "¿En qué URL va a vivir auth-server? (admin-panel la usa para enviar ahí a quien no tenga sesión)",
33
+ placeholder: "http://localhost:3000",
34
+ defaultValue: "http://localhost:3000",
35
+ });
36
+ if (p.isCancel(authServerUrl)) {
37
+ p.cancel("Instalación cancelada.");
38
+ process.exit(0);
39
+ }
40
+ const adminPanelUrl = await p.text({
41
+ 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)",
42
+ placeholder: "http://localhost:3001",
43
+ defaultValue: "http://localhost:3001",
44
+ });
45
+ if (p.isCancel(adminPanelUrl)) {
46
+ p.cancel("Instalación cancelada.");
47
+ process.exit(0);
48
+ }
49
+ const cookieDomain = await p.text({
50
+ message: "¿auth-server y admin-panel van a vivir en subdominios del mismo dominio (ej. auth.tuempresa.com y admin.tuempresa.com)? " +
51
+ "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.",
52
+ placeholder: "",
53
+ defaultValue: "",
54
+ });
55
+ if (p.isCancel(cookieDomain)) {
56
+ p.cancel("Instalación cancelada.");
57
+ process.exit(0);
58
+ }
59
+ // Registers admin-panel as a first-party OAuth 2.1 client of this
60
+ // Supabase project so the dashboard can redirect straight through the
61
+ // authorize/consent/token flow instead of relying on a shared cookie
62
+ // domain — the only way SSO works when auth-server and admin-panel end up
63
+ // on genuinely different domains, not just subdomains.
64
+ const oauthSpinner = p.spinner();
65
+ oauthSpinner.start("Registrando admin-panel como cliente OAuth 2.1");
66
+ let oauthClientId = null;
67
+ try {
68
+ oauthClientId = await registerOAuthClient({
69
+ supabaseUrl: db.supabaseUrl,
70
+ serviceRoleKey: db.serviceRoleKey,
71
+ clientName: "admin-panel",
72
+ redirectUris: [`${adminPanelUrl}/oauth/callback`],
73
+ });
74
+ }
75
+ catch {
76
+ oauthClientId = null;
77
+ }
78
+ if (oauthClientId) {
79
+ oauthSpinner.stop("admin-panel registrado como cliente OAuth 2.1");
80
+ }
81
+ else {
82
+ oauthSpinner.stop("No se pudo registrar el cliente OAuth 2.1 (se omite)");
83
+ p.note("Tu proyecto Supabase no tiene habilitado el servidor OAuth 2.1 de GoTrue (GOTRUE_OAUTH_SERVER_ENABLED). " +
84
+ "admin-panel seguirá funcionando con el enlace de login normal, pero eso solo mantiene la sesión si " +
85
+ "auth-server y admin-panel comparten dominio (o subdominios con NEXT_PUBLIC_COOKIE_DOMAIN). " +
86
+ "Si en el futuro necesitas dominios completamente distintos, habilita esa opción en tu proyecto y vuelve a correr el instalador.", "Paso manual pendiente");
87
+ }
30
88
  const s = p.spinner();
31
89
  s.start("Generando apps/auth-server/.env.local y apps/admin-panel/.env.local");
32
90
  await writeEnvFile(`${repoRoot}/apps/auth-server/.env.local`, {
@@ -34,11 +92,35 @@ export async function askDeploymentStep(repoRoot, db) {
34
92
  NEXT_PUBLIC_SUPABASE_ANON_KEY: db.anonKey,
35
93
  SUPABASE_URL: db.supabaseUrl,
36
94
  SUPABASE_SERVICE_ROLE_KEY: db.serviceRoleKey,
95
+ NEXT_PUBLIC_ADMIN_PANEL_URL: adminPanelUrl,
96
+ NEXT_PUBLIC_COOKIE_DOMAIN: cookieDomain,
37
97
  });
38
98
  await writeEnvFile(`${repoRoot}/apps/admin-panel/.env.local`, {
39
99
  NEXT_PUBLIC_SUPABASE_URL: db.supabaseUrl,
40
100
  NEXT_PUBLIC_SUPABASE_ANON_KEY: db.anonKey,
101
+ NEXT_PUBLIC_AUTH_SERVER_URL: authServerUrl,
102
+ NEXT_PUBLIC_COOKIE_DOMAIN: cookieDomain,
103
+ NEXT_PUBLIC_OAUTH_CLIENT_ID: oauthClientId ?? "",
41
104
  });
105
+ if (db.mode === "new-self-hosted") {
106
+ // database.ts wrote SITE_URL="http://localhost:3000" as a fixed default
107
+ // *before* this step ever asked where auth-server actually lives —
108
+ // if authServerUrl isn't that default, GoTrue would otherwise keep
109
+ // building its own /oauth/consent redirects (and email links) against
110
+ // the wrong host forever.
111
+ await updateEnvValue(`${repoRoot}/docker/.env`, "SITE_URL", authServerUrl);
112
+ await updateEnvValue(`${repoRoot}/docker/.env`, "ADMIN_PANEL_URL", adminPanelUrl);
113
+ if (cookieDomain) {
114
+ await updateEnvValue(`${repoRoot}/docker/.env`, "COOKIE_DOMAIN", cookieDomain);
115
+ }
116
+ if (oauthClientId) {
117
+ await updateEnvValue(`${repoRoot}/docker/.env`, "OAUTH_CLIENT_ID", oauthClientId);
118
+ }
119
+ }
42
120
  s.stop(".env.local generados en auth-server y admin-panel");
121
+ if (db.mode === "new-self-hosted") {
122
+ p.note("docker/.env se actualizó con las URLs reales — como el contenedor de auth (GoTrue) ya estaba arriba con los " +
123
+ "valores por defecto, corre `docker compose -f docker/docker-compose.yml up -d` una vez más para que los tome.", "Reinicia el docker-compose");
124
+ }
43
125
  p.note(NEXT_STEPS[target], `Despliegue: ${target}`);
44
126
  }
@@ -2,5 +2,11 @@ export interface EnvValues {
2
2
  [key: string]: string;
3
3
  }
4
4
  export declare function writeEnvFile(path: string, values: EnvValues): Promise<void>;
5
+ /**
6
+ * Updates (or appends) a single key in an existing .env file without
7
+ * touching the rest — for docker/.env, which database.ts already wrote a
8
+ * full set of values into before deployment.ts runs.
9
+ */
10
+ export declare function updateEnvValue(path: string, key: string, value: string): Promise<void>;
5
11
  export declare function fileExists(path: string): boolean;
6
12
  //# 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,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"}
@@ -1,11 +1,25 @@
1
1
  import { existsSync } from "node:fs";
2
- import { writeFile } from "node:fs/promises";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
3
  export async function writeEnvFile(path, values) {
4
4
  const contents = Object.entries(values)
5
5
  .map(([key, value]) => `${key}=${value}`)
6
6
  .join("\n");
7
7
  await writeFile(path, `${contents}\n`, "utf8");
8
8
  }
9
+ /**
10
+ * Updates (or appends) a single key in an existing .env file without
11
+ * touching the rest — for docker/.env, which database.ts already wrote a
12
+ * full set of values into before deployment.ts runs.
13
+ */
14
+ export async function updateEnvValue(path, key, value) {
15
+ const current = existsSync(path) ? await readFile(path, "utf8") : "";
16
+ const line = `${key}=${value}`;
17
+ const pattern = new RegExp(`^${key}=.*$`, "m");
18
+ const updated = pattern.test(current)
19
+ ? current.replace(pattern, line)
20
+ : `${current.replace(/\n+$/, "")}\n${line}\n`;
21
+ await writeFile(path, updated.replace(/^\n/, ""), "utf8");
22
+ }
9
23
  export function fileExists(path) {
10
24
  return existsSync(path);
11
25
  }
@@ -0,0 +1,21 @@
1
+ export interface RegisterOAuthClientOptions {
2
+ supabaseUrl: string;
3
+ serviceRoleKey: string;
4
+ clientName: string;
5
+ redirectUris: string[];
6
+ }
7
+ /**
8
+ * Registers a first-party OAuth client (admin-panel) against this Supabase
9
+ * project's own OAuth 2.1 server, via the service-role admin API — the only
10
+ * way SSO between auth-server and admin-panel survives them living on
11
+ * genuinely different domains (not just subdomains), since a shared cookie
12
+ * simply can't cross that boundary.
13
+ *
14
+ * Returns null (rather than throwing) when the project doesn't have OAuth
15
+ * server mode enabled — an existing Supabase project the installer doesn't
16
+ * control the config of, same "last mile manual step" as the Custom Access
17
+ * Token Hook. The caller is expected to fall back gracefully and tell the
18
+ * user how to enable it themselves.
19
+ */
20
+ export declare function registerOAuthClient(options: RegisterOAuthClientOptions): Promise<string | null>;
21
+ //# sourceMappingURL=oauth-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-client.d.ts","sourceRoot":"","sources":["../../src/utils/oauth-client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmBrG"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Registers a first-party OAuth client (admin-panel) against this Supabase
3
+ * project's own OAuth 2.1 server, via the service-role admin API — the only
4
+ * way SSO between auth-server and admin-panel survives them living on
5
+ * genuinely different domains (not just subdomains), since a shared cookie
6
+ * simply can't cross that boundary.
7
+ *
8
+ * Returns null (rather than throwing) when the project doesn't have OAuth
9
+ * server mode enabled — an existing Supabase project the installer doesn't
10
+ * control the config of, same "last mile manual step" as the Custom Access
11
+ * Token Hook. The caller is expected to fall back gracefully and tell the
12
+ * user how to enable it themselves.
13
+ */
14
+ export async function registerOAuthClient(options) {
15
+ const response = await fetch(`${options.supabaseUrl.replace(/\/$/, "")}/auth/v1/admin/oauth/clients`, {
16
+ method: "POST",
17
+ headers: {
18
+ Authorization: `Bearer ${options.serviceRoleKey}`,
19
+ apikey: options.serviceRoleKey,
20
+ "Content-Type": "application/json",
21
+ },
22
+ body: JSON.stringify({
23
+ client_name: options.clientName,
24
+ redirect_uris: options.redirectUris,
25
+ client_type: "public",
26
+ token_endpoint_auth_method: "none",
27
+ }),
28
+ });
29
+ if (!response.ok)
30
+ return null;
31
+ const data = (await response.json());
32
+ return data.client_id;
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kontrolia-auth",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "license": "MIT",
5
5
  "description": "Installer wizard for KontrolIA Auth. Two independent questions: (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, Coolify, Railway).",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "@clack/prompts": "^0.9.1",
34
34
  "jose": "^5.9.6",
35
- "@kontrolia/db": "1.0.0"
35
+ "@kontrolia/db": "1.0.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.10.2",