@supatype/cli 0.1.5 → 0.1.7
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +143 -93
- package/.turbo/turbo-typecheck.log +1 -1
- package/deno.d.ts +52 -0
- package/dist/augmentation-generator.d.ts +22 -0
- package/dist/augmentation-generator.d.ts.map +1 -1
- package/dist/augmentation-generator.js +43 -7
- package/dist/augmentation-generator.js.map +1 -1
- package/dist/binary-cache.d.ts +14 -0
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +33 -13
- package/dist/binary-cache.js.map +1 -1
- package/dist/cache-clean.d.ts +15 -0
- package/dist/cache-clean.d.ts.map +1 -0
- package/dist/cache-clean.js +76 -0
- package/dist/cache-clean.js.map +1 -0
- package/dist/cache-pins.d.ts +51 -0
- package/dist/cache-pins.d.ts.map +1 -0
- package/dist/cache-pins.js +184 -0
- package/dist/cache-pins.js.map +1 -0
- package/dist/cli-install-method.d.ts +19 -0
- package/dist/cli-install-method.d.ts.map +1 -0
- package/dist/cli-install-method.js +60 -0
- package/dist/cli-install-method.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-credentials.d.ts +34 -0
- package/dist/cloud-credentials.d.ts.map +1 -0
- package/dist/cloud-credentials.js +139 -0
- package/dist/cloud-credentials.js.map +1 -0
- package/dist/commands/admin.d.ts +11 -0
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +131 -22
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/cache.d.ts.map +1 -1
- package/dist/commands/cache.js +67 -71
- package/dist/commands/cache.js.map +1 -1
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +32 -8
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/db.d.ts +1 -0
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +147 -1
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +50 -10
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +28 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +60 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/functions.d.ts +7 -0
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +40 -6
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +5 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/hooks.d.ts +12 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +118 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +41 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/internal.d.ts +6 -0
- package/dist/commands/internal.d.ts.map +1 -0
- package/dist/commands/internal.js +22 -0
- package/dist/commands/internal.js.map +1 -0
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +2 -0
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/link-helpers.d.ts.map +1 -1
- package/dist/commands/link-helpers.js +8 -2
- package/dist/commands/link-helpers.js.map +1 -1
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +110 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +110 -7
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/self-update.d.ts +7 -0
- package/dist/commands/self-update.d.ts.map +1 -1
- package/dist/commands/self-update.js +18 -11
- package/dist/commands/self-update.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -3
- package/dist/config.js.map +1 -1
- package/dist/db-preflight.d.ts +78 -0
- package/dist/db-preflight.d.ts.map +1 -0
- package/dist/db-preflight.js +452 -0
- package/dist/db-preflight.js.map +1 -0
- package/dist/dev-compose.d.ts +1 -1
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +129 -72
- package/dist/dev-compose.js.map +1 -1
- package/dist/env-file.d.ts +40 -2
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +132 -11
- package/dist/env-file.js.map +1 -1
- package/dist/field-masking-tier.d.ts +50 -0
- package/dist/field-masking-tier.d.ts.map +1 -0
- package/dist/field-masking-tier.js +91 -0
- package/dist/field-masking-tier.js.map +1 -0
- package/dist/functions-deno-types.d.ts +24 -0
- package/dist/functions-deno-types.d.ts.map +1 -0
- package/dist/functions-deno-types.js +155 -0
- package/dist/functions-deno-types.js.map +1 -0
- package/dist/hook-upload.d.ts +30 -0
- package/dist/hook-upload.d.ts.map +1 -0
- package/dist/hook-upload.js +86 -0
- package/dist/hook-upload.js.map +1 -0
- package/dist/hooks-generator.d.ts +8 -0
- package/dist/hooks-generator.d.ts.map +1 -0
- package/dist/hooks-generator.js +332 -0
- package/dist/hooks-generator.js.map +1 -0
- package/dist/link.d.ts +4 -0
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +2 -0
- package/dist/link.js.map +1 -1
- package/dist/local-secrets.d.ts +71 -0
- package/dist/local-secrets.d.ts.map +1 -0
- package/dist/local-secrets.js +100 -0
- package/dist/local-secrets.js.map +1 -0
- package/dist/model-hooks.d.ts +73 -0
- package/dist/model-hooks.d.ts.map +1 -0
- package/dist/model-hooks.js +211 -0
- package/dist/model-hooks.js.map +1 -0
- package/dist/postgres-ctl.d.ts +11 -0
- package/dist/postgres-ctl.d.ts.map +1 -1
- package/dist/postgres-ctl.js +25 -2
- package/dist/postgres-ctl.js.map +1 -1
- package/dist/project-config.d.ts +121 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +150 -1
- package/dist/project-config.js.map +1 -1
- package/dist/resolve-target.d.ts +4 -0
- package/dist/resolve-target.d.ts.map +1 -1
- package/dist/resolve-target.js +69 -73
- package/dist/resolve-target.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +2 -1
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +4 -2
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/schema-push-lock.d.ts.map +1 -1
- package/dist/schema-push-lock.js +43 -16
- package/dist/schema-push-lock.js.map +1 -1
- package/dist/self-host-compose.d.ts +24 -0
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +301 -69
- package/dist/self-host-compose.js.map +1 -1
- package/dist/service-role-check.d.ts +21 -0
- package/dist/service-role-check.d.ts.map +1 -0
- package/dist/service-role-check.js +85 -0
- package/dist/service-role-check.js.map +1 -0
- package/dist/target-client.d.ts +17 -0
- package/dist/target-client.d.ts.map +1 -1
- package/dist/target-client.js +69 -17
- package/dist/target-client.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +736 -32
- package/dist/type-extractor.js.map +1 -1
- package/dist/ui/runtime/command-chrome.js +1 -1
- package/dist/ui/runtime/command-chrome.js.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.js +3 -1
- package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
- package/package.json +4 -1
- package/scripts/generate-homebrew-formula.mjs +121 -0
- package/src/augmentation-generator.ts +46 -9
- package/src/binary-cache.ts +47 -15
- package/src/cache-clean.ts +81 -0
- package/src/cache-pins.ts +243 -0
- package/src/cli-install-method.ts +67 -0
- package/src/cli.ts +6 -0
- package/src/cloud-credentials.ts +183 -0
- package/src/commands/admin.ts +153 -28
- package/src/commands/cache.ts +90 -75
- package/src/commands/cloud.ts +32 -8
- package/src/commands/db.ts +162 -1
- package/src/commands/dev.ts +57 -8
- package/src/commands/doctor.ts +69 -3
- package/src/commands/functions.ts +42 -5
- package/src/commands/generate.ts +5 -1
- package/src/commands/hooks.ts +138 -0
- package/src/commands/init.ts +42 -8
- package/src/commands/internal.ts +23 -0
- package/src/commands/keys.ts +2 -0
- package/src/commands/link-helpers.ts +9 -2
- package/src/commands/login.ts +127 -0
- package/src/commands/push.ts +124 -7
- package/src/commands/self-update.ts +85 -13
- package/src/config.ts +22 -3
- package/src/db-preflight.ts +575 -0
- package/src/dev-compose.ts +144 -73
- package/src/env-file.ts +157 -11
- package/src/field-masking-tier.ts +107 -0
- package/src/functions-deno-types.ts +188 -0
- package/src/hook-upload.ts +107 -0
- package/src/hooks-generator.ts +352 -0
- package/src/link.ts +7 -0
- package/src/local-secrets.ts +104 -0
- package/src/model-hooks.ts +260 -0
- package/src/postgres-ctl.ts +29 -2
- package/src/project-config.ts +236 -2
- package/src/resolve-target.ts +113 -68
- package/src/schema-ast-v2.ts +5 -2
- package/src/schema-push-lock.ts +56 -17
- package/src/self-host-compose.ts +344 -68
- package/src/service-role-check.ts +111 -0
- package/src/target-client.ts +105 -18
- package/src/type-extractor.ts +883 -28
- package/src/ui/runtime/command-chrome.ts +1 -1
- package/src/ui/runtime/stdin-after-ink.ts +3 -1
- package/tests/access-composition.test.ts +667 -0
- package/tests/admin-studio-membership.test.ts +121 -0
- package/tests/api-schemas.test.ts +83 -0
- package/tests/archive-name-alias.test.ts +42 -0
- package/tests/augmentation-generator.test.ts +72 -4
- package/tests/cache-clean.test.ts +206 -0
- package/tests/cli-help.test.ts +3 -0
- package/tests/cli-install-method.test.ts +66 -0
- package/tests/command-chrome-coverage.test.ts +31 -26
- package/tests/command-chrome-wrap.test.ts +1 -0
- package/tests/db-preflight.test.ts +72 -0
- package/tests/doctor-render.test.ts +62 -0
- package/tests/doctor-service-role.test.ts +55 -0
- package/tests/env-file.test.ts +121 -0
- package/tests/external-database-compose.test.ts +184 -0
- package/tests/external-database-config.test.ts +154 -0
- package/tests/field-masking-tier.test.ts +184 -0
- package/tests/functions-deno-types.test.ts +197 -0
- package/tests/homebrew-formula.test.ts +58 -0
- package/tests/hook-upload.test.ts +125 -0
- package/tests/hooks-generator.test.ts +212 -0
- package/tests/init.test.ts +3 -0
- package/tests/local-secrets.test.ts +92 -0
- package/tests/model-hooks.test.ts +322 -0
- package/tests/runtime-contract.test.ts +113 -1
- package/tests/service-role-check.test.ts +109 -0
- package/tests/service-role-routes.test.ts +51 -0
- package/tests/target-client-refresh.test.ts +94 -0
- package/tests/type-extractor.test.ts +187 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tests/engine-distribution.test.ts +0 -418
package/src/commands/doctor.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Command } from "commander"
|
|
2
2
|
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
3
3
|
import { info, plain } from "../ui/messages.js"
|
|
4
|
-
import { schemaPathFromProject } from "../project-config.js"
|
|
4
|
+
import { hooksPathFromProject, schemaPathFromProject, serviceRoleRoutes } from "../project-config.js"
|
|
5
5
|
import { resolveTarget, targetSchemaDoctor, schemaPgSchema } from "../resolve-target.js"
|
|
6
6
|
import { loadProjectLink } from "../link.js"
|
|
7
7
|
import { resolveHostEngineDatabaseUrl } from "../dev-compose.js"
|
|
8
|
+
import { hooksReport, type HooksReport } from "../model-hooks.js"
|
|
9
|
+
import { checkServiceRoleRoutes, type ServiceRoleProblems } from "../service-role-check.js"
|
|
8
10
|
|
|
9
11
|
interface DoctorItem {
|
|
10
12
|
kind: string
|
|
@@ -20,12 +22,15 @@ interface DoctorReport {
|
|
|
20
22
|
unmanagedDrift: DoctorItem[]
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
/** Exported for tests: the label form is easy to get subtly wrong per item kind. */
|
|
26
|
+
export function printSection(title: string, items: DoctorItem[]): void {
|
|
24
27
|
if (items.length === 0) return
|
|
25
28
|
plain(`\n${title} (${items.length}):\n`)
|
|
26
29
|
for (const item of items) {
|
|
27
30
|
const fields = item.fields.length > 0 ? ` (${item.fields.join(", ")})` : ""
|
|
28
|
-
|
|
31
|
+
// A table's `name` *is* its table, so the usual `table.name` form renders "widget.widget".
|
|
32
|
+
const label = item.table === item.name ? item.name : `${item.table}.${item.name}`
|
|
33
|
+
plain(` • ${label}${fields}`)
|
|
29
34
|
plain(` ${item.message}`)
|
|
30
35
|
}
|
|
31
36
|
}
|
|
@@ -82,6 +87,9 @@ export function registerDoctor(program: Command): void {
|
|
|
82
87
|
})) as DoctorReport
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
printHooks(hooksReport(cwd, hooksPathFromProject(config, cwd), ast))
|
|
91
|
+
printServiceRoleGrants(checkServiceRoleRoutes(config, cwd), serviceRoleRoutes(config))
|
|
92
|
+
|
|
85
93
|
printSection("Missing (in AST, not in DB)", report.missing ?? [])
|
|
86
94
|
printSection("Stale managed (stamped, not in AST)", report.staleManaged ?? [])
|
|
87
95
|
printSection("Unmanaged drift (manual decision)", report.unmanagedDrift ?? [])
|
|
@@ -101,3 +109,61 @@ export function registerDoctor(program: Command): void {
|
|
|
101
109
|
}
|
|
102
110
|
})
|
|
103
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether declared hooks can actually run.
|
|
115
|
+
*
|
|
116
|
+
* Worth its own section because every failure here is silent: a hook whose function is missing, or a
|
|
117
|
+
* stack with functions switched off, produces no error anywhere — the write just succeeds
|
|
118
|
+
* unvalidated. Drift you cannot see is the thing doctor exists for.
|
|
119
|
+
*/
|
|
120
|
+
export function printHooks(report: HooksReport): void {
|
|
121
|
+
if (report.declared.length === 0) return
|
|
122
|
+
|
|
123
|
+
plain(`\nHooks (${report.declared.length}):\n`)
|
|
124
|
+
for (const hook of report.declared) {
|
|
125
|
+
const broken = report.missing.some(
|
|
126
|
+
(m) => m.model === hook.model && m.event === hook.event,
|
|
127
|
+
)
|
|
128
|
+
plain(` ${broken ? "✗" : "•"} ${hook.model}.${hook.event} → ${hook.function}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (report.missing.length > 0) {
|
|
132
|
+
plain("\n Those marked ✗ name a function that does not exist, so they never fire.")
|
|
133
|
+
plain(" Create it with: supatype hooks new <name>")
|
|
134
|
+
}
|
|
135
|
+
if (report.functionsDisabled) {
|
|
136
|
+
plain("\n functions_enabled is false in .supatype/manifest.json — every hook is inert.")
|
|
137
|
+
plain(" Regenerate the stack config with: supatype self-host compose")
|
|
138
|
+
}
|
|
139
|
+
if (report.mapMissing) {
|
|
140
|
+
plain("\n .supatype/manifest.json carries no hook map, so the server has nothing to call.")
|
|
141
|
+
plain(" Run: supatype push")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Report which functions may see the service-role key, and which grants do nothing.
|
|
147
|
+
*
|
|
148
|
+
* Worth printing even when everything resolves: this is the list of functions that can read and write
|
|
149
|
+
* past every access rule in the schema, and "which ones are those again?" should be answerable without
|
|
150
|
+
* opening the config.
|
|
151
|
+
*/
|
|
152
|
+
export function printServiceRoleGrants(
|
|
153
|
+
problems: ServiceRoleProblems,
|
|
154
|
+
declared: readonly string[],
|
|
155
|
+
): void {
|
|
156
|
+
if (declared.length === 0) return
|
|
157
|
+
|
|
158
|
+
plain(`\nService-role grants (${declared.length}):\n`)
|
|
159
|
+
const broken = new Set(problems.missing)
|
|
160
|
+
for (const name of declared) {
|
|
161
|
+
plain(` ${broken.has(name) ? "✗" : "•"} ${name}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (broken.size > 0) {
|
|
165
|
+
plain("\n Those marked ✗ match no function, so they grant nothing — the function reads no key.")
|
|
166
|
+
}
|
|
167
|
+
for (const warning of problems.warnings) plain(warning)
|
|
168
|
+
plain("\n These functions bypass every access rule in the schema. Anything not listed cannot.")
|
|
169
|
+
}
|
|
@@ -16,6 +16,7 @@ import { ensureBinary } from "../ensure-binary.js"
|
|
|
16
16
|
import {
|
|
17
17
|
functionsPathCandidatesFromProject,
|
|
18
18
|
preferredFunctionsPathFromProject,
|
|
19
|
+
serviceRoleRoutes,
|
|
19
20
|
} from "../project-config.js"
|
|
20
21
|
import {
|
|
21
22
|
discoverTsFunctionsInDir,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
import { loadProjectLink } from "../link.js"
|
|
25
26
|
import { resolveTarget } from "../resolve-target.js"
|
|
26
27
|
import { targetFetch } from "../target-client.js"
|
|
28
|
+
import { ensureFunctionsDenoTypes } from "../functions-deno-types.js"
|
|
27
29
|
import { error, info, plain } from "../ui/messages.js"
|
|
28
30
|
import { nextSteps } from "../ui/next-steps.js"
|
|
29
31
|
|
|
@@ -140,18 +142,19 @@ function scaffoldFunction(cwd: string, name: string): void {
|
|
|
140
142
|
// Docs: https://supatype.com/docs/edge-functions
|
|
141
143
|
|
|
142
144
|
export default async function handler(req: Request): Promise<Response> {
|
|
143
|
-
const { method
|
|
145
|
+
const { method } = req
|
|
146
|
+
const url = Deno.env.get("SUPATYPE_URL")
|
|
144
147
|
|
|
145
148
|
// Example: read request body for POST requests
|
|
146
149
|
if (method === "POST") {
|
|
147
150
|
const body = await req.json()
|
|
148
|
-
return new Response(JSON.stringify({ message: "Hello from ${name}!", received: body }), {
|
|
151
|
+
return new Response(JSON.stringify({ message: "Hello from ${name}!", received: body, url }), {
|
|
149
152
|
status: 200,
|
|
150
153
|
headers: { "Content-Type": "application/json" },
|
|
151
154
|
})
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
return new Response(JSON.stringify({ message: "Hello from ${name}!" }), {
|
|
157
|
+
return new Response(JSON.stringify({ message: "Hello from ${name}!", url }), {
|
|
155
158
|
status: 200,
|
|
156
159
|
headers: { "Content-Type": "application/json" },
|
|
157
160
|
})
|
|
@@ -181,13 +184,27 @@ export default async function handler(req: Request): Promise<Response> {
|
|
|
181
184
|
)
|
|
182
185
|
}
|
|
183
186
|
|
|
187
|
+
const denoTypes = ensureFunctionsDenoTypes(cwd, functionsDir)
|
|
188
|
+
|
|
184
189
|
const functionsDirLabel = relativeFunctionsDir(cwd, functionsDir)
|
|
185
190
|
info(`Created function: ${functionsDirLabel}/${name}/index.ts`)
|
|
186
|
-
|
|
191
|
+
if (denoTypes.wroteDenoDts) {
|
|
192
|
+
info(`Added Deno IDE types: ${functionsDirLabel}/deno.d.ts`)
|
|
193
|
+
}
|
|
194
|
+
if (denoTypes.wroteTsconfig) {
|
|
195
|
+
info(`Added functions tsconfig: ${functionsDirLabel}/tsconfig.json`)
|
|
196
|
+
}
|
|
197
|
+
if (denoTypes.rootExclude === "updated") {
|
|
198
|
+
info('Updated root tsconfig.json to exclude "functions"')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const steps = [
|
|
187
202
|
"Local dev: npx supatype functions serve",
|
|
188
203
|
`Invoke: npx supatype functions invoke ${name}`,
|
|
189
204
|
"Deploy: npx supatype functions deploy",
|
|
190
|
-
|
|
205
|
+
...denoTypes.hints,
|
|
206
|
+
]
|
|
207
|
+
nextSteps("Next steps:", steps)
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
// ─── Discover functions ──────────────────────────────────────────────────────
|
|
@@ -422,6 +439,9 @@ async function deployViaTarget(
|
|
|
422
439
|
source,
|
|
423
440
|
entrypoint: `${fn.name}/index.ts`,
|
|
424
441
|
}],
|
|
442
|
+
// Sent every deploy, and replaced wholesale by the platform: the config file is the whole
|
|
443
|
+
// truth, so a name removed from it must stop being entitled to the service-role key.
|
|
444
|
+
serviceRole: serviceRoleRoutesFor(cwd),
|
|
425
445
|
},
|
|
426
446
|
token: target.token!,
|
|
427
447
|
orgId: target.orgId,
|
|
@@ -479,6 +499,9 @@ async function deployCloud(cwd: string, fns: DiscoveredFunction[], env?: string)
|
|
|
479
499
|
source,
|
|
480
500
|
entrypoint: `${fn.name}/index.ts`,
|
|
481
501
|
}],
|
|
502
|
+
// Same grants as the target path: both reach the same endpoint, and a project deployed one
|
|
503
|
+
// way must not end up with a different credential surface than the other.
|
|
504
|
+
serviceRole: serviceRoleRoutesFor(cwd),
|
|
482
505
|
}),
|
|
483
506
|
signal: AbortSignal.timeout(60_000),
|
|
484
507
|
})
|
|
@@ -500,6 +523,20 @@ async function deployCloud(cwd: string, fns: DiscoveredFunction[], env?: string)
|
|
|
500
523
|
info(`Invoke: https://${linked.ref}.supatype.dev/functions/v1/<name>`)
|
|
501
524
|
}
|
|
502
525
|
|
|
526
|
+
/**
|
|
527
|
+
* The project's `functions.serviceRole` list, or undefined when the config cannot be read.
|
|
528
|
+
*
|
|
529
|
+
* Undefined rather than `[]`: the platform replaces the stored list wholesale when the field is present,
|
|
530
|
+
* so sending an empty array because a config failed to load would revoke every grant the project has.
|
|
531
|
+
*/
|
|
532
|
+
export function serviceRoleRoutesFor(cwd: string): string[] | undefined {
|
|
533
|
+
try {
|
|
534
|
+
return serviceRoleRoutes(loadConfig(cwd))
|
|
535
|
+
} catch {
|
|
536
|
+
return undefined
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
503
540
|
function readFunctionSource(fn: DiscoveredFunction): string {
|
|
504
541
|
const stat = statSync(fn.absPath)
|
|
505
542
|
if (stat.isFile()) {
|
package/src/commands/generate.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { Command } from "commander"
|
|
|
2
2
|
import { mkdirSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { resolve, dirname } from "node:path"
|
|
4
4
|
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
5
|
-
import { schemaPathFromProject } from "../project-config.js"
|
|
5
|
+
import { hooksPathFromProject, schemaPathFromProject } from "../project-config.js"
|
|
6
6
|
import { ensureEngine, engineRequest } from "../engine-client.js"
|
|
7
7
|
import { generateClientAugmentation } from "../augmentation-generator.js"
|
|
8
|
+
import { writeHooksModule } from "../model-hooks.js"
|
|
8
9
|
import { error, info } from "../ui/messages.js"
|
|
9
10
|
|
|
10
11
|
export function registerGenerate(program: Command): void {
|
|
@@ -41,5 +42,8 @@ export function registerGenerate(program: Command): void {
|
|
|
41
42
|
mkdirSync(dirname(augmentationOutPath), { recursive: true })
|
|
42
43
|
writeFileSync(augmentationOutPath, augmentationCode, "utf8")
|
|
43
44
|
info(`Client augmentation written to ${outputClientPath}`)
|
|
45
|
+
|
|
46
|
+
const hooksPath = writeHooksModule(cwd, hooksPathFromProject(config, cwd), ast)
|
|
47
|
+
if (hooksPath !== null) info(`Hook handler types written to ${hooksPath}`)
|
|
44
48
|
})
|
|
45
49
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `supatype hooks` — scaffold and inspect model hooks.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `supatype functions` because the two are different things wearing the same runtime.
|
|
5
|
+
* A function is a **public endpoint**: anyone holding the anon key can invoke it. A hook is
|
|
6
|
+
* **procedural** — only the API server calls it, around a write, and the gateway refuses the route
|
|
7
|
+
* from outside. Keeping them in separate directories is what makes that boundary structural instead
|
|
8
|
+
* of a list somebody has to maintain.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from "commander"
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
12
|
+
import { join, relative } from "node:path"
|
|
13
|
+
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
14
|
+
import { hooksPathFromProject, schemaPathFromProject } from "../project-config.js"
|
|
15
|
+
import { declaredHooks, hooksReport } from "../model-hooks.js"
|
|
16
|
+
import { error, info, plain } from "../ui/messages.js"
|
|
17
|
+
|
|
18
|
+
export function registerHooks(program: Command): void {
|
|
19
|
+
const hooksCmd = program
|
|
20
|
+
.command("hooks")
|
|
21
|
+
.description("Manage model hooks (procedural handlers the API runs around a write)")
|
|
22
|
+
|
|
23
|
+
hooksCmd
|
|
24
|
+
.command("new <name>")
|
|
25
|
+
.description("Scaffold a hook handler")
|
|
26
|
+
.action((name: string) => {
|
|
27
|
+
scaffoldHook(process.cwd(), name)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
hooksCmd
|
|
31
|
+
.command("list")
|
|
32
|
+
.description("Show the hooks the schema declares and whether each one can run")
|
|
33
|
+
.action(() => {
|
|
34
|
+
const cwd = process.cwd()
|
|
35
|
+
const config = loadConfig(cwd)
|
|
36
|
+
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
37
|
+
const report = hooksReport(cwd, hooksPathFromProject(config, cwd), ast)
|
|
38
|
+
|
|
39
|
+
if (report.declared.length === 0) {
|
|
40
|
+
info("No model declares a hook.")
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
for (const hook of report.declared) {
|
|
44
|
+
const broken = report.missing.some((m) => m.model === hook.model && m.event === hook.event)
|
|
45
|
+
plain(` ${broken ? "✗" : "•"} ${hook.model}.${hook.event} → ${hook.function}`)
|
|
46
|
+
}
|
|
47
|
+
if (report.missing.length > 0) {
|
|
48
|
+
plain("\n ✗ names a handler that does not exist, so that hook never fires.")
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scaffoldHook(cwd: string, name: string): void {
|
|
54
|
+
const config = loadConfig(cwd)
|
|
55
|
+
const hooksDir = hooksPathFromProject(config, cwd)
|
|
56
|
+
const dir = join(hooksDir, name)
|
|
57
|
+
|
|
58
|
+
if (existsSync(dir)) {
|
|
59
|
+
error(`Hook "${name}" already exists at ${relative(cwd, dir)}`)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
64
|
+
const declared = declaredHooks(ast).filter((hook) => hook.function === name)
|
|
65
|
+
|
|
66
|
+
mkdirSync(dir, { recursive: true })
|
|
67
|
+
writeFileSync(join(dir, "index.ts"), handlerTemplate(name, declared), "utf8")
|
|
68
|
+
|
|
69
|
+
info(`Created hook: ${relative(cwd, join(dir, "index.ts"))}`)
|
|
70
|
+
if (declared.length === 0) {
|
|
71
|
+
// Scaffolding a handler nothing calls is easy to do and impossible to notice, since a hook that
|
|
72
|
+
// is never referenced simply never runs.
|
|
73
|
+
plain("")
|
|
74
|
+
plain(" No model references it yet. Add it to a model's meta:")
|
|
75
|
+
plain(` hooks: { beforeChange: "${name}" }`)
|
|
76
|
+
plain(" then run `supatype push` to generate its types and register it.")
|
|
77
|
+
} else {
|
|
78
|
+
plain("")
|
|
79
|
+
for (const hook of declared) {
|
|
80
|
+
plain(` Wired to ${hook.model}.${hook.event}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The scaffold deliberately imports from `../_supatype/hooks.ts`, which `push` generates.
|
|
87
|
+
*
|
|
88
|
+
* Before the first push that file does not exist, so the handler will not compile — which is the
|
|
89
|
+
* honest state: the types come from the schema, and a handler typed against a model that has not been
|
|
90
|
+
* pushed is a handler typed against a guess.
|
|
91
|
+
*/
|
|
92
|
+
function handlerTemplate(name: string, declared: { model: string; event: string }[]): string {
|
|
93
|
+
const event = declared[0]?.event ?? "beforeChange"
|
|
94
|
+
const model = declared[0]?.model ?? "YourModel"
|
|
95
|
+
|
|
96
|
+
if (event.startsWith("after")) {
|
|
97
|
+
return `// ${name} — model hook (${event})
|
|
98
|
+
//
|
|
99
|
+
// Runs after the write has succeeded. It cannot change or undo it: a rejection here would be a
|
|
100
|
+
// rejection of something that already happened. Failures are logged, not surfaced to the caller.
|
|
101
|
+
|
|
102
|
+
import { hook, type AfterChange } from "../_supatype/hooks.ts"
|
|
103
|
+
|
|
104
|
+
const handler: AfterChange<"your_table"> = async (ctx) => {
|
|
105
|
+
console.log(\`\${ctx.table} \${ctx.operation} by \${ctx.user?.sub ?? "anon"}\`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default hook(handler)
|
|
109
|
+
`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `// ${name} — model hook (${event}) for ${model}
|
|
113
|
+
//
|
|
114
|
+
// Runs before the write reaches Postgres, so it can reject it or rewrite the body.
|
|
115
|
+
//
|
|
116
|
+
// Not a security boundary: hooks fire for writes through the API, and direct SQL, seeds and anything
|
|
117
|
+
// holding the service role bypass them. Invariants belong in the model's access rules or a CHECK.
|
|
118
|
+
|
|
119
|
+
import { hook, type BeforeChange } from "../_supatype/hooks.ts"
|
|
120
|
+
|
|
121
|
+
const handler: BeforeChange<"your_table"> = async (ctx) => {
|
|
122
|
+
if (ctx.operation === "insert") {
|
|
123
|
+
// \`ctx.rows\` is the submitted rows, typed from the model.
|
|
124
|
+
return { rows: ctx.rows }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// \`ctx.patch\` is the submitted partial; \`ctx.previous()\` reads the rows about to change.
|
|
128
|
+
// Those come back as stored — unmasked — so do not echo them into a rejection message.
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
// Reject with a message the caller sees, and a status of your choosing:
|
|
132
|
+
// return { reject: "A post needs a title" }
|
|
133
|
+
// return { reject: { message: "Already exists", status: 409, code: "duplicate" } }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default hook(handler)
|
|
137
|
+
`
|
|
138
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Command } from "commander"
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { resolve, join, basename } from "node:path"
|
|
4
4
|
import { spawnSync } from "node:child_process"
|
|
5
|
+
import { randomBytes } from "node:crypto"
|
|
5
6
|
import { p, runClackFlow } from "../ui/clack.js"
|
|
6
7
|
import { ensureNotCancelled } from "../ui/prompts.js"
|
|
7
8
|
import { generateAndWriteKeys } from "./keys.js"
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
ensureComponentBinaries,
|
|
14
15
|
reportComponentBinaryFailures,
|
|
15
16
|
} from "../ensure-component-binaries.js"
|
|
17
|
+
import { ensureFunctionsDenoTypes } from "../functions-deno-types.js"
|
|
16
18
|
import { mergeSupatypePackageJson } from "../init-package-json.js"
|
|
17
19
|
import { cliPackageVersion } from "../cli-package-version.js"
|
|
18
20
|
import {
|
|
@@ -792,6 +794,12 @@ function scaffoldHelloFunction(
|
|
|
792
794
|
if (!existsSync(join(dir, "functions/.env.local"))) {
|
|
793
795
|
write("functions/.env.local", functionsEnvLocalTemplate())
|
|
794
796
|
}
|
|
797
|
+
const denoTypes = ensureFunctionsDenoTypes(dir, join(dir, "functions"))
|
|
798
|
+
if (denoTypes.wroteDenoDts) file("created", "functions/deno.d.ts")
|
|
799
|
+
if (denoTypes.wroteTsconfig) file("created", "functions/tsconfig.json")
|
|
800
|
+
if (denoTypes.rootExclude === "updated") {
|
|
801
|
+
file("updated", 'tsconfig.json (exclude "functions")')
|
|
802
|
+
}
|
|
795
803
|
}
|
|
796
804
|
|
|
797
805
|
// ─── Templates ───────────────────────────────────────────────────────────────
|
|
@@ -1182,13 +1190,28 @@ export type SiteSettings = Model<{
|
|
|
1182
1190
|
|
|
1183
1191
|
function envTemplate(opts: ScaffoldOptions): string {
|
|
1184
1192
|
const sections: string[] = []
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1193
|
+
// Generated, not constant. These reach a server: the previous defaults meant a self-host
|
|
1194
|
+
// deployment ran with a password of `postgres` and a JWT secret published in this repo —
|
|
1195
|
+
// and anyone holding that secret can mint a token naming any Postgres role. Safe to generate
|
|
1196
|
+
// only because `supatype dev` no longer overwrites them on every run (see local-secrets.ts).
|
|
1197
|
+
const postgresPassword = randomBytes(24).toString("base64url")
|
|
1189
1198
|
|
|
1190
|
-
sections.push(
|
|
1191
|
-
|
|
1199
|
+
sections.push(`DATABASE_URL=postgresql://supatype_admin:${postgresPassword}@localhost:5432/${opts.projectName}
|
|
1200
|
+
POSTGRES_USER=supatype_admin
|
|
1201
|
+
POSTGRES_PASSWORD=${postgresPassword}
|
|
1202
|
+
POSTGRES_DB=${opts.projectName}
|
|
1203
|
+
|
|
1204
|
+
# PostgREST's own database credential. It connects as \`authenticator\`, a role that holds no
|
|
1205
|
+
# privileges and can only switch to anon/authenticated/service_role — so a request whose JWT
|
|
1206
|
+
# names any other role is refused by Postgres rather than honoured.
|
|
1207
|
+
#
|
|
1208
|
+
# Deliberately NOT the same as POSTGRES_PASSWORD: that one is yours, for direct SQL access, and
|
|
1209
|
+
# rotating it should never take the REST API down with it.
|
|
1210
|
+
AUTHENTICATOR_PASSWORD=${randomBytes(24).toString("base64url")}`)
|
|
1211
|
+
|
|
1212
|
+
sections.push(`# JWT — ANON_KEY and SERVICE_ROLE_KEY are derived from this secret.
|
|
1213
|
+
# Changing it invalidates every issued token; re-run \`supatype keys\` afterwards.
|
|
1214
|
+
JWT_SECRET=${randomBytes(48).toString("base64url")}
|
|
1192
1215
|
ANON_KEY=
|
|
1193
1216
|
SERVICE_ROLE_KEY=`)
|
|
1194
1217
|
|
|
@@ -1340,16 +1363,17 @@ function helloFunctionTemplate(): string {
|
|
|
1340
1363
|
|
|
1341
1364
|
export default async function handler(req: Request): Promise<Response> {
|
|
1342
1365
|
const { method } = req
|
|
1366
|
+
const url = Deno.env.get("SUPATYPE_URL")
|
|
1343
1367
|
|
|
1344
1368
|
if (method === "POST") {
|
|
1345
1369
|
const body = await req.json()
|
|
1346
|
-
return new Response(JSON.stringify({ message: "Hello from hello!", received: body }), {
|
|
1370
|
+
return new Response(JSON.stringify({ message: "Hello from hello!", received: body, url }), {
|
|
1347
1371
|
status: 200,
|
|
1348
1372
|
headers: { "Content-Type": "application/json" },
|
|
1349
1373
|
})
|
|
1350
1374
|
}
|
|
1351
1375
|
|
|
1352
|
-
return new Response(JSON.stringify({ message: "Hello from hello!" }), {
|
|
1376
|
+
return new Response(JSON.stringify({ message: "Hello from hello!", url }), {
|
|
1353
1377
|
status: 200,
|
|
1354
1378
|
headers: { "Content-Type": "application/json" },
|
|
1355
1379
|
})
|
|
@@ -1435,6 +1459,16 @@ function printNextSteps(args: {
|
|
|
1435
1459
|
info(`Supatype project ready${name ? ` in ${name}/` : ""}.`)
|
|
1436
1460
|
nextSteps(setupComplete ? "Next steps:" : "Finish setup:", steps)
|
|
1437
1461
|
|
|
1462
|
+
// Shown once, at creation. These are generated per project rather than shipped as
|
|
1463
|
+
// constants, so this is the moment to say they exist and where they live — nothing prints
|
|
1464
|
+
// them again, and the JWT secret is only echoed as a fingerprint from here on.
|
|
1465
|
+
nextSteps("Secrets — generated for this project, kept in .env:", [
|
|
1466
|
+
"POSTGRES_PASSWORD direct SQL access as the superuser",
|
|
1467
|
+
"AUTHENTICATOR_PASSWORD PostgREST's own login; rotating the one above does not affect it",
|
|
1468
|
+
"JWT_SECRET signs ANON_KEY and SERVICE_ROLE_KEY; changing it invalidates every token",
|
|
1469
|
+
"Keep .env out of version control, and use different values in production.",
|
|
1470
|
+
])
|
|
1471
|
+
|
|
1438
1472
|
if (result.app.mode === "none") {
|
|
1439
1473
|
nextSteps("Static frontend (self-host):", [
|
|
1440
1474
|
"supatype app add --static ./public",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hidden internal commands (Homebrew post_install, etc.).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Command } from "commander"
|
|
6
|
+
import { downloadAll } from "../binary-cache.js"
|
|
7
|
+
import { error, info } from "../ui/messages.js"
|
|
8
|
+
|
|
9
|
+
export function registerInternalCommands(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command("_postinstall", { hidden: true })
|
|
12
|
+
.description("Prefetch CDN component binaries after install (Homebrew post_install)")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
info("Prefetching Supatype component binaries for this platform...")
|
|
16
|
+
await downloadAll(undefined, true)
|
|
17
|
+
info("Component prefetch complete.")
|
|
18
|
+
} catch (err) {
|
|
19
|
+
error(err instanceof Error ? err.message : String(err))
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
package/src/commands/keys.ts
CHANGED
|
@@ -72,9 +72,11 @@ export function generateAndWriteKeys(
|
|
|
72
72
|
content = upsertEnvVar(content, "SERVICE_ROLE_KEY", serviceKey)
|
|
73
73
|
content = upsertEnvVar(content, "VITE_SUPATYPE_ANON_KEY", anonKey)
|
|
74
74
|
content = upsertEnvVar(content, "PUBLIC_SUPATYPE_ANON_KEY", anonKey)
|
|
75
|
+
content = upsertEnvVar(content, "EXPO_PUBLIC_SUPATYPE_ANON_KEY", anonKey)
|
|
75
76
|
const apiUrl = readEnvVar(content, "PUBLIC_SUPATYPE_URL") ?? readEnvVar(content, "API_EXTERNAL_URL")
|
|
76
77
|
if (apiUrl) {
|
|
77
78
|
content = upsertEnvVar(content, "VITE_SUPATYPE_URL", apiUrl)
|
|
79
|
+
content = upsertEnvVar(content, "EXPO_PUBLIC_SUPATYPE_URL", apiUrl)
|
|
78
80
|
}
|
|
79
81
|
writeFileSync(envPath, content, "utf8")
|
|
80
82
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander"
|
|
2
2
|
import { loadConfig } from "../config.js"
|
|
3
|
+
import { loadCloudCredentials } from "../cloud-credentials.js"
|
|
3
4
|
import {
|
|
4
5
|
createCloudLink,
|
|
5
6
|
createSelfHostLink,
|
|
@@ -23,7 +24,8 @@ function resolveLinkToken(opts: {
|
|
|
23
24
|
opts.serviceRoleKey ??
|
|
24
25
|
process.env["SUPATYPE_ACCESS_TOKEN"] ??
|
|
25
26
|
process.env["SUPATYPE_TOKEN"] ??
|
|
26
|
-
process.env["SERVICE_ROLE_KEY"]
|
|
27
|
+
process.env["SERVICE_ROLE_KEY"] ??
|
|
28
|
+
loadCloudCredentials()?.accessToken
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -211,11 +213,15 @@ export async function runLinkAction(opts: {
|
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
if (!token) {
|
|
214
|
-
error("Authentication required.
|
|
216
|
+
error("Authentication required. Run: supatype login (or set SUPATYPE_ACCESS_TOKEN / pass --token).")
|
|
215
217
|
process.exit(1)
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
const cloudApiUrl = opts.apiUrl.replace(/\/$/, "")
|
|
221
|
+
const creds = loadCloudCredentials()
|
|
222
|
+
const refreshToken =
|
|
223
|
+
(creds?.apiUrl.replace(/\/$/, "") === cloudApiUrl ? creds.refreshToken : undefined) ??
|
|
224
|
+
existing?.refreshToken
|
|
219
225
|
|
|
220
226
|
if (project) {
|
|
221
227
|
const one = await targetFetch<{ slug: string; orgId: string }>(cloudApiUrl, "/api/v1", {
|
|
@@ -250,6 +256,7 @@ export async function runLinkAction(opts: {
|
|
|
250
256
|
orgId: one.orgId,
|
|
251
257
|
environments,
|
|
252
258
|
existing,
|
|
259
|
+
...(refreshToken !== undefined ? { refreshToken } : {}),
|
|
253
260
|
})
|
|
254
261
|
saveProjectLink(cwd, link)
|
|
255
262
|
info(`Linked to cloud project: ${project}`)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud session login / logout for CLI control-plane auth.
|
|
3
|
+
* Self-host uses SERVICE_ROLE_KEY via `supatype link --url` — not this command.
|
|
4
|
+
*/
|
|
5
|
+
import type { Command } from "commander"
|
|
6
|
+
import {
|
|
7
|
+
clearCloudCredentials,
|
|
8
|
+
cloudPasswordLogin,
|
|
9
|
+
loadCloudCredentials,
|
|
10
|
+
persistCloudSession,
|
|
11
|
+
} from "../cloud-credentials.js"
|
|
12
|
+
import { loadProjectLink, saveProjectLink } from "../link.js"
|
|
13
|
+
import { isInteractive } from "../ui/interactive.js"
|
|
14
|
+
import { error, info, plain } from "../ui/messages.js"
|
|
15
|
+
import { ensureNotCancelled, p, printLogo, runClackFlow } from "../ui/prompts.js"
|
|
16
|
+
|
|
17
|
+
function defaultApiUrl(cwd: string, flag?: string): string {
|
|
18
|
+
if (flag?.trim()) return flag.trim().replace(/\/$/, "")
|
|
19
|
+
const link = loadProjectLink(cwd)
|
|
20
|
+
if (link?.kind === "cloud" && link.cloudApiUrl) {
|
|
21
|
+
return link.cloudApiUrl.replace(/\/$/, "")
|
|
22
|
+
}
|
|
23
|
+
return (process.env["SUPATYPE_API_URL"] ?? "https://api.supatype.com").replace(/\/$/, "")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerLogin(program: Command): void {
|
|
27
|
+
program
|
|
28
|
+
.command("login")
|
|
29
|
+
.description("Sign in to Supatype Cloud (stores refreshable session for push/deploy)")
|
|
30
|
+
.option("--api-url <url>", "Cloud control plane API URL")
|
|
31
|
+
.option("--email <email>", "Account email (non-interactive)")
|
|
32
|
+
.option("--password <password>", "Account password (prefer SUPATYPE_PASSWORD)")
|
|
33
|
+
.action(async (opts: { apiUrl?: string; email?: string; password?: string }) => {
|
|
34
|
+
const cwd = process.cwd()
|
|
35
|
+
const apiUrl = defaultApiUrl(cwd, opts.apiUrl)
|
|
36
|
+
|
|
37
|
+
let email = opts.email?.trim() ?? process.env["SUPATYPE_EMAIL"]?.trim()
|
|
38
|
+
let password =
|
|
39
|
+
opts.password ?? process.env["SUPATYPE_PASSWORD"] ?? process.env["SUPATYPE_LOGIN_PASSWORD"]
|
|
40
|
+
|
|
41
|
+
if ((!email || !password) && isInteractive()) {
|
|
42
|
+
await runClackFlow(async () => {
|
|
43
|
+
printLogo()
|
|
44
|
+
p.intro("Supatype Cloud login")
|
|
45
|
+
if (!email) {
|
|
46
|
+
email = ensureNotCancelled(
|
|
47
|
+
await p.text({
|
|
48
|
+
message: "Email",
|
|
49
|
+
placeholder: "you@example.com",
|
|
50
|
+
}),
|
|
51
|
+
).trim()
|
|
52
|
+
}
|
|
53
|
+
if (!password) {
|
|
54
|
+
password = ensureNotCancelled(await p.password({ message: "Password" }))
|
|
55
|
+
}
|
|
56
|
+
p.outro("Signing in...")
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!email || !password) {
|
|
61
|
+
error(
|
|
62
|
+
"Email and password required. Pass --email / --password, set SUPATYPE_EMAIL + SUPATYPE_PASSWORD, or run interactively.",
|
|
63
|
+
)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const session = await cloudPasswordLogin(apiUrl, email, password)
|
|
69
|
+
persistCloudSession(cwd, {
|
|
70
|
+
apiUrl,
|
|
71
|
+
accessToken: session.accessToken,
|
|
72
|
+
...(session.refreshToken !== undefined ? { refreshToken: session.refreshToken } : {}),
|
|
73
|
+
...(session.email !== undefined ? { email: session.email } : { email }),
|
|
74
|
+
})
|
|
75
|
+
const link = loadProjectLink(cwd)
|
|
76
|
+
info(`Logged in to ${apiUrl}${session.email ? ` as ${session.email}` : ""}`)
|
|
77
|
+
if (link?.kind === "cloud") {
|
|
78
|
+
info(`Updated cloud session for linked project ${link.projectRef}`)
|
|
79
|
+
} else if (!session.refreshToken) {
|
|
80
|
+
plain("Warning: no refresh_token returned — session may expire in ~1h. Update control plane.")
|
|
81
|
+
} else {
|
|
82
|
+
plain("Tip: link a project with: supatype link --project <slug>")
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
error((err as Error).message)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command("logout")
|
|
92
|
+
.description("Clear stored Supatype Cloud credentials")
|
|
93
|
+
.action(() => {
|
|
94
|
+
const cwd = process.cwd()
|
|
95
|
+
clearCloudCredentials()
|
|
96
|
+
const link = loadProjectLink(cwd)
|
|
97
|
+
if (link?.kind === "cloud") {
|
|
98
|
+
delete link.token
|
|
99
|
+
delete link.refreshToken
|
|
100
|
+
saveProjectLink(cwd, link)
|
|
101
|
+
}
|
|
102
|
+
info("Logged out of Supatype Cloud.")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command("whoami")
|
|
107
|
+
.description("Show the current Supatype Cloud session")
|
|
108
|
+
.action(() => {
|
|
109
|
+
const creds = loadCloudCredentials()
|
|
110
|
+
const cwd = process.cwd()
|
|
111
|
+
const link = loadProjectLink(cwd)
|
|
112
|
+
if (!creds && !(link?.kind === "cloud" && link.token)) {
|
|
113
|
+
info("Not logged in. Run: supatype login")
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
if (creds) {
|
|
117
|
+
plain(`API: ${creds.apiUrl}`)
|
|
118
|
+
if (creds.email) plain(`Email: ${creds.email}`)
|
|
119
|
+
plain(`Refresh token: ${creds.refreshToken ? "yes" : "no"}`)
|
|
120
|
+
plain(`Updated: ${creds.updatedAt}`)
|
|
121
|
+
}
|
|
122
|
+
if (link?.kind === "cloud") {
|
|
123
|
+
plain(`Linked project: ${link.projectRef}`)
|
|
124
|
+
plain(`Link has refresh token: ${link.refreshToken ? "yes" : "no"}`)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|