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 +6 -0
- package/dist/index.js +39 -5
- package/dist/steps/database.d.ts.map +1 -1
- package/dist/steps/database.js +56 -1
- package/dist/steps/deployment.d.ts.map +1 -1
- package/dist/steps/deployment.js +12 -1
- package/dist/utils/files.d.ts +2 -0
- package/dist/utils/files.d.ts.map +1 -1
- package/dist/utils/files.js +15 -0
- package/dist/utils/supabase-management-api.d.ts +28 -0
- package/dist/utils/supabase-management-api.d.ts.map +1 -0
- package/dist/utils/supabase-management-api.js +84 -0
- package/package.json +1 -1
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.
|
|
97
|
-
*
|
|
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
|
|
109
|
-
const
|
|
110
|
-
|
|
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":"
|
|
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"}
|
package/dist/steps/database.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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"}
|
package/dist/steps/deployment.js
CHANGED
|
@@ -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.");
|
package/dist/utils/files.d.ts
CHANGED
|
@@ -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"}
|
package/dist/utils/files.js
CHANGED
|
@@ -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.
|
|
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": [
|