@protobase/cli 0.66.8 → 0.66.10
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/dist/cli/package.js +1 -1
- package/dist/dev/api-forward.js +3 -2
- package/dist/dev/app-files.js +11 -0
- package/dist/dev/check-remote-api.js +18 -0
- package/dist/dev/remote-api.js +58 -0
- package/dist/dev/run-ui.js +34 -0
- package/dist/dev/run.js +3 -7
- package/dist/main.js +23 -12
- package/package.json +7 -7
- package/skills/protobase/references/features/build-and-run.md +1 -0
- package/skills/protobase/references/features/sign-in-and-accounts.md +1 -1
- package/skills/protobase/references/reference/auth.md +7 -4
- package/skills/protobase/references/reference/cli.md +12 -0
package/dist/cli/package.js
CHANGED
package/dist/dev/api-forward.js
CHANGED
|
@@ -9,7 +9,8 @@ import { request } from "node:http";
|
|
|
9
9
|
* discovery documents MCP clients read (`oauthWellKnownPaths`), below which a resource's own one is, and the push worker
|
|
10
10
|
* (`withDevServiceWorker`). Vite serves the rest.
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
12
|
+
const apiRequestPattern = /^(\/api(\/|\?|$)|\/\.well-known\/webauthn(\?|$)|\/\.well-known\/oauth-(authorization-server|protected-resource)(\/|\?|$)|\/protobase\.sw\.js(\?|$))/;
|
|
13
|
+
const isApiRequest = (url) => apiRequestPattern.test(url);
|
|
13
14
|
const hopByHop = /* @__PURE__ */ new Set([
|
|
14
15
|
"connection",
|
|
15
16
|
"keep-alive",
|
|
@@ -67,4 +68,4 @@ const apiForward = (port) => ({
|
|
|
67
68
|
}
|
|
68
69
|
});
|
|
69
70
|
//#endregion
|
|
70
|
-
export { apiForward, isApiRequest };
|
|
71
|
+
export { apiForward, apiRequestPattern, isApiRequest };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { adminAppMain } from "../project/admin-app.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
//#region src/dev/app-files.ts
|
|
5
|
+
/** Fails, naming them, when the admin app in `appDir` lacks the files `protobase dev` serves it from. */
|
|
6
|
+
const requireAppFiles = (appDir) => {
|
|
7
|
+
const missing = ["index.html", adminAppMain].filter((file) => !existsSync(path.join(appDir, file)));
|
|
8
|
+
if (missing.length > 0) throw new Error(`The admin app is missing ${missing.join(", ")} in ${appDir}; \`protobase dev\` serves the app from there`);
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { requireAppFiles };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/dev/check-remote-api.ts
|
|
2
|
+
/**
|
|
3
|
+
* Fails, before the dev server starts, when the instance at `origin` cannot be reached or does not answer `/api/meta`
|
|
4
|
+
* as a Protobase API does, with the X-Meta-Version header.
|
|
5
|
+
*/
|
|
6
|
+
const checkRemoteApi = async (origin) => {
|
|
7
|
+
const meta = `${origin}/api/meta`;
|
|
8
|
+
const answer = await fetch(meta, {
|
|
9
|
+
signal: AbortSignal.timeout(1e4),
|
|
10
|
+
redirect: "manual"
|
|
11
|
+
}).catch((error) => {
|
|
12
|
+
throw new Error(`Cannot reach the API at ${meta}: ${error.cause?.message ?? error.message}`);
|
|
13
|
+
});
|
|
14
|
+
await answer.body?.cancel();
|
|
15
|
+
if (!answer.headers.has("x-meta-version")) throw new Error(`${origin} answered ${meta} with ${answer.status} and no X-Meta-Version header; is it a Protobase app?`);
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { checkRemoteApi };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isAllowedHost } from "./allowed-host.js";
|
|
2
|
+
import { apiRequestPattern } from "./api-forward.js";
|
|
3
|
+
//#region src/dev/remote-api.ts
|
|
4
|
+
/**
|
|
5
|
+
* The origin of the Protobase instance `protobase dev --api` hands the API to, from the URL given: http or https, and
|
|
6
|
+
* no path, since an instance serves its API at /api of its origin.
|
|
7
|
+
*/
|
|
8
|
+
const remoteApiOrigin = (value) => {
|
|
9
|
+
const url = URL.parse(value);
|
|
10
|
+
if (!url || url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`--api expects an http or https URL, such as https://app.example.com, not "${value}"`);
|
|
11
|
+
if (url.pathname !== "/" || url.search) throw new Error(`--api expects the instance's origin, such as ${url.origin}, without a path`);
|
|
12
|
+
return url.origin;
|
|
13
|
+
};
|
|
14
|
+
const ofDevServer = (value, request) => {
|
|
15
|
+
const url = value === void 0 ? null : URL.parse(value);
|
|
16
|
+
return url?.host === request.headers.host ? url : void 0;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Hands the requests the API process answers in `protobase dev` (`isApiRequest`), and the API's WebSockets, to the
|
|
20
|
+
* instance at `origin` through Vite's proxy. The instance sees its own Host, and its own origin in the Origin and Referer
|
|
21
|
+
* of a page of this dev server, so its sign-in and its checks of a request's origin take them; a request from any other
|
|
22
|
+
* page keeps the origin it came with. Its cookies come back without their Domain, for localhost to keep, and a
|
|
23
|
+
* redirect to its own address comes back as a path on this server. A socket whose Host fails Vite's host check is
|
|
24
|
+
* refused, as Vite refuses such a request.
|
|
25
|
+
*/
|
|
26
|
+
const remoteApi = (origin) => {
|
|
27
|
+
let allowedHosts = [];
|
|
28
|
+
return {
|
|
29
|
+
name: "protobase-remote-api",
|
|
30
|
+
config: () => ({ server: { proxy: { [apiRequestPattern.source]: {
|
|
31
|
+
target: origin,
|
|
32
|
+
changeOrigin: true,
|
|
33
|
+
ws: true,
|
|
34
|
+
rewriteWsOrigin: true,
|
|
35
|
+
cookieDomainRewrite: { "*": "" },
|
|
36
|
+
bypass: (request, response) => {
|
|
37
|
+
if (!(response === void 0)) return void 0;
|
|
38
|
+
return isAllowedHost(request.headers.host, allowedHosts) ? void 0 : false;
|
|
39
|
+
},
|
|
40
|
+
configure: (proxy) => {
|
|
41
|
+
proxy.on("proxyReq", (upstream, request) => {
|
|
42
|
+
if (ofDevServer(request.headers.origin, request)) upstream.setHeader("origin", origin);
|
|
43
|
+
const referer = ofDevServer(request.headers.referer, request);
|
|
44
|
+
if (referer) upstream.setHeader("referer", `${origin}${referer.pathname}${referer.search}`);
|
|
45
|
+
});
|
|
46
|
+
proxy.on("proxyRes", (answer) => {
|
|
47
|
+
const location = answer.headers.location;
|
|
48
|
+
if (location?.startsWith(`${origin}/`)) answer.headers.location = location.slice(origin.length);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} } } }),
|
|
52
|
+
configResolved: (config) => {
|
|
53
|
+
allowedHosts = config.server.allowedHosts;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
export { remoteApi, remoteApiOrigin };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { adminAppDir } from "../project/admin-app.js";
|
|
2
|
+
import { alsoOnIpv6, checkPortFree } from "./loopback.js";
|
|
3
|
+
import { remoteApi } from "./remote-api.js";
|
|
4
|
+
import { requireAppFiles } from "./app-files.js";
|
|
5
|
+
import { checkRemoteApi } from "./check-remote-api.js";
|
|
6
|
+
//#region src/dev/run-ui.ts
|
|
7
|
+
/**
|
|
8
|
+
* `protobase dev --api`: the admin app with the project's UI, theme and icons, as `runDev` serves it, with its API
|
|
9
|
+
* requests handed to a running instance instead of an API process of its own; no database and no project config are read.
|
|
10
|
+
*/
|
|
11
|
+
const runUiDev = async (options, out) => {
|
|
12
|
+
const appDir = options.appDir ?? adminAppDir;
|
|
13
|
+
requireAppFiles(appDir);
|
|
14
|
+
await checkRemoteApi(options.api);
|
|
15
|
+
if (options.port !== 0) await checkPortFree(options.port);
|
|
16
|
+
const { createAppServer } = await import("./app-server.js");
|
|
17
|
+
const server = await createAppServer({
|
|
18
|
+
...options,
|
|
19
|
+
appDir,
|
|
20
|
+
extensions: options.extensions ?? []
|
|
21
|
+
}, [remoteApi(options.api)]);
|
|
22
|
+
const closeAndThrow = async (error) => {
|
|
23
|
+
await server.close();
|
|
24
|
+
throw error;
|
|
25
|
+
};
|
|
26
|
+
await server.listen().catch(closeAndThrow);
|
|
27
|
+
const address = server.httpServer?.address();
|
|
28
|
+
const port = typeof address === "object" && address ? address.port : options.port;
|
|
29
|
+
if (server.httpServer) await alsoOnIpv6(server.httpServer, port).catch(closeAndThrow);
|
|
30
|
+
out(`protobase dev ready at http://localhost:${port}\n project ${options.projectDir} (UI only)\n api ${options.api}/api/v1, through http://localhost:${port}/api/v1\n login sign in as a user of ${options.api}\n`);
|
|
31
|
+
return server;
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { runUiDev };
|
package/dist/dev/run.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { loadNearestEnvFile } from "./env.js";
|
|
2
|
-
import { adminAppDir
|
|
2
|
+
import { adminAppDir } from "../project/admin-app.js";
|
|
3
3
|
import { alsoOnIpv6, checkPortFree } from "./loopback.js";
|
|
4
|
+
import { apiForward } from "./api-forward.js";
|
|
5
|
+
import { requireAppFiles } from "./app-files.js";
|
|
4
6
|
import { checkDatabase } from "./check-database.js";
|
|
5
7
|
import { authDriftLines } from "./auth-drift-lines.js";
|
|
6
8
|
import { loginLine } from "./login-line.js";
|
|
7
|
-
import { apiForward } from "./api-forward.js";
|
|
8
9
|
import { forkApiChild } from "./api-child.js";
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
//#region src/dev/run.ts
|
|
12
|
-
const requireAppFiles = (appDir) => {
|
|
13
|
-
const missing = ["index.html", adminAppMain].filter((file) => !existsSync(path.join(appDir, file)));
|
|
14
|
-
if (missing.length > 0) throw new Error(`The admin app is missing ${missing.join(", ")} in ${appDir}; \`protobase dev\` serves the app from there`);
|
|
15
|
-
};
|
|
16
12
|
const runDev = async (options, out) => {
|
|
17
13
|
loadNearestEnvFile(options.projectDir);
|
|
18
14
|
const url = process.env[options.envVar];
|
package/dist/main.js
CHANGED
|
@@ -60,23 +60,34 @@ program.command("doctor").description("Check a config folder against the databas
|
|
|
60
60
|
}, out);
|
|
61
61
|
if (errors > 0) process.exitCode = 1;
|
|
62
62
|
});
|
|
63
|
-
|
|
64
|
-
const [{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
const runUiDevAgainst = async (api, options) => {
|
|
64
|
+
const [{ runUiDev }, { remoteApiOrigin }] = await Promise.all([import("./dev/run-ui.js"), import("./dev/remote-api.js")]);
|
|
65
|
+
return runUiDev({
|
|
66
|
+
...options,
|
|
67
|
+
api: remoteApiOrigin(api)
|
|
68
|
+
}, out);
|
|
69
|
+
};
|
|
70
|
+
const runFullDev = async (envVar, jobs, options) => {
|
|
71
|
+
const { runDev } = await import("./dev/run.js");
|
|
70
72
|
const server = await runDev({
|
|
73
|
+
...options,
|
|
74
|
+
envVar,
|
|
75
|
+
jobs
|
|
76
|
+
}, out);
|
|
77
|
+
warn(placeholderSecretWarning(process.env));
|
|
78
|
+
return server;
|
|
79
|
+
};
|
|
80
|
+
program.command("dev").description("Serve the admin app and API for the project in the current directory").option("--port <port>", "port for the app and API", "5173").option("--env <variable>", "environment variable holding the database URL", "DATABASE_URL").option("--allowed-hosts <hosts>", "extra Host headers to accept, comma separated; a leading dot matches subdomains (.trycloudflare.com)").option("--cache-dir <dir>", "Vite's dependency cache, so parallel dev servers do not share one").option("--extend <dir>", "merge the protobase.config.ts and protobase.ui.tsx in this folder before the project's, repeatable", collect, []).option("--no-jobs", "run none of the app's jobs and deliver none of its events, such as against its hosted database").option("--api <url>", "serve the admin app alone, with the API of the running Protobase instance at this URL; no database or API process").action(async (opts) => {
|
|
81
|
+
const [{ resolveExtensions }, { writeProjectTypes }] = await Promise.all([import("./project/extensions.js"), import("./project/project-types.js")]);
|
|
82
|
+
await writeProjectTypes(process.cwd());
|
|
83
|
+
const shared = {
|
|
71
84
|
extensions: resolveExtensions(opts.extend),
|
|
72
85
|
port: Number(opts.port),
|
|
73
|
-
envVar: opts.env,
|
|
74
86
|
projectDir: process.cwd(),
|
|
75
87
|
cacheDir: opts.cacheDir && path.resolve(opts.cacheDir),
|
|
76
|
-
allowedHosts: parseHosts(opts.allowedHosts)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
warn(placeholderSecretWarning(process.env));
|
|
88
|
+
allowedHosts: parseHosts(opts.allowedHosts)
|
|
89
|
+
};
|
|
90
|
+
const server = opts.api ? await runUiDevAgainst(opts.api, shared) : await runFullDev(opts.env, opts.jobs, shared);
|
|
80
91
|
process.once("SIGINT", () => void server.close().then(() => process.exit(0)));
|
|
81
92
|
process.once("SIGTERM", () => void server.close().then(() => process.exit(0)));
|
|
82
93
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.10",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@better-auth/core": "^1.7.12",
|
|
45
45
|
"@hono/node-server": "^2.1.3",
|
|
46
|
-
"@protobase/config": "0.66.
|
|
47
|
-
"@protobase/layout": "0.66.
|
|
48
|
-
"@protobase/query": "0.66.
|
|
49
|
-
"@protobase/schema": "0.66.
|
|
50
|
-
"@protobase/server": "0.66.
|
|
51
|
-
"@protobase/ui": "0.66.
|
|
46
|
+
"@protobase/config": "0.66.10",
|
|
47
|
+
"@protobase/layout": "0.66.10",
|
|
48
|
+
"@protobase/query": "0.66.10",
|
|
49
|
+
"@protobase/schema": "0.66.10",
|
|
50
|
+
"@protobase/server": "0.66.10",
|
|
51
|
+
"@protobase/ui": "0.66.10",
|
|
52
52
|
"@react-email/render": "^2.1.0",
|
|
53
53
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
54
54
|
"@sentry/vite-plugin": "^11.10.0",
|
|
@@ -7,6 +7,7 @@ Develop, build and run an app, on servers, in containers and on phones.
|
|
|
7
7
|
- [Node and Bun](../reference/cli.md#how-it-runs): The `protobase` command runs on Node 22.12 or later or on Bun, and a bundle runs on both unless it is built for Bun only.
|
|
8
8
|
- [Dev server](../reference/cli.md#dev): The admin app and the API on one port, with changes shown without a restart.
|
|
9
9
|
- [Dev server without jobs](../reference/cli.md#dev): `protobase dev --no-jobs` runs no jobs and delivers no events, webhooks or pushes, to develop against a hosted app's database without doing its work twice.
|
|
10
|
+
- [UI-only dev server](../reference/cli.md#ui-only) (beta): `protobase dev --api <url>` serves the app's UI from the project with the API of an app already running, such as its staging deploy, with no database.
|
|
10
11
|
- [Editor types](../reference/cli.md#types): `protobase types` gives the editor and `tsc` the types of the CLI that builds the app, with nothing installed.
|
|
11
12
|
- [Project scripts](../reference/cli.md#run): `protobase run` runs a seed or a migration script on the CLI's packages.
|
|
12
13
|
- [Migrate command](../reference/cli.md#migrate): `protobase migrate` applies the project's SQL migrations in one transaction, with nothing installed.
|
|
@@ -22,7 +22,7 @@ Sign-in methods, users, roles, organizations and tokens.
|
|
|
22
22
|
- [Passkeys](../reference/auth.md#passkeys): People sign in with their device's face or fingerprint unlock, or with a security key.
|
|
23
23
|
- [Two-factor authentication](../reference/auth.md#two-factor-authentication): A second step with an authenticator app, backup codes or emailed codes.
|
|
24
24
|
- [Emailed confirmation code](../reference/auth.md#emailed-confirmation-code): Someone without a passkey or a second step confirms it's them with a code emailed on request, where they are, such as before a secret is shown.
|
|
25
|
-
- [Confirming an account change](../reference/auth.md#confirming-an-account-change): Adding a passkey, unlinking a provider or deleting the account from an older session asks to confirm it's them in place, never to sign out and sign in again.
|
|
25
|
+
- [Confirming an account change](../reference/auth.md#confirming-an-account-change): Adding a passkey, turning on a second step without a password, unlinking a provider or deleting the account from an older session asks to confirm it's them in place, never to sign out and sign in again; a first passkey takes the password too.
|
|
26
26
|
- [Sign-in policy](../reference/auth.md#sign-in-policy): Admins choose which sign-in methods are on, off or required, and the server refuses a policy that would lock people out.
|
|
27
27
|
- [GitHub and social sign-in](../reference/auth.md#sign-in-with-github): People sign in with GitHub or another social provider.
|
|
28
28
|
- [Sign-in links](../reference/auth.md#sign-in-links-and-connected-accounts): A link signs someone in with a sign-in provider at once, without the sign-in page.
|
|
@@ -272,7 +272,7 @@ Signing out a device stops that device's API token at its next request (see [Whe
|
|
|
272
272
|
|
|
273
273
|
When the app has mail, the account's address is emailed that its password changed, with a link to choose a new one if it was not them: after a change, a first password, a [reset](#password-reset), an admin setting it, and a new password through the [endpoints for supabase-js](#supabase-js-and-supabasessr).
|
|
274
274
|
|
|
275
|
-
Checks of the signed-in account's current password are limited to 5 a minute for each account, on top of the limit for each client: changing the password, deleting the account, turning [two-factor authentication](#two-factor-authentication) on or off, its authenticator link and new backup codes,
|
|
275
|
+
Checks of the signed-in account's current password are limited to 5 a minute for each account, on top of the limit for each client: changing the password, deleting the account, turning [two-factor authentication](#two-factor-authentication) on or off, its authenticator link and new backup codes, `POST /api/auth/verify-password`, and [confirming a first passkey](#confirming-an-account-change) with the password. Every check counts, a right one too; one over the limit is refused with `429` (`TOO_MANY_PASSWORD_CHECKS`, with `X-Retry-After` in seconds), so a stolen session cannot guess the password from many addresses. <gap>The current-password limit cannot be shared between server instances, each of which counts on its own, because a shared store for it is not implemented.</gap>
|
|
276
276
|
|
|
277
277
|
On the [account page](#account-page), the `Password` block does both: with a password, it asks for the current one and the new one twice, and says the other devices are signed out; without one, it sets the first, and nobody is signed out. While the sign-in policy turns passwords off, or in a staff session, it only says so.
|
|
278
278
|
|
|
@@ -369,7 +369,7 @@ People add passkeys (Face ID, Touch ID, Windows Hello, a phone or a security key
|
|
|
369
369
|
- On an app with several origins, every passkey works on each of them through [Related Origin Requests](https://passkeys.dev/docs/advanced/related-origins/) (Chrome and Edge 128, Safari 18, Firefox 152; at most 5 registrable domains).
|
|
370
370
|
- A browser asks for the passkeys of one RP ID at a time. When people have passkeys for more than one of the app's hosts, `GET /api/auth/status` lists them as `passkeyDomains`, the passkey domain first, and the sign-in page adds "Use a passkey made for {host}" for each of the others. `GET /api/auth/passkey/generate-authenticate-options?rpId=<host>` asks for one of them.
|
|
371
371
|
- A passkey sign-in asks for no second step: the passkey is something you have and something you are or know.
|
|
372
|
-
-
|
|
372
|
+
- Adding a passkey from a session that signed in more than a day ago asks to [confirm it's them](#confirming-an-account-change) first, and the account page asks for it in place; for a first passkey, on an account without a [second step](#two-factor-authentication), the account's password does too.
|
|
373
373
|
- Passkeys can be renamed and removed on the account page; removing one here leaves it on the device.
|
|
374
374
|
|
|
375
375
|
## Two-factor authentication
|
|
@@ -379,7 +379,7 @@ On their account page, people turn on a second step after a password or an email
|
|
|
379
379
|
- **Authenticator app:** the page shows a QR code (and the key, to type in), the person confirms it with a code from the app, and gets ten backup codes that each work once instead of a code. New backup codes replace the old ones.
|
|
380
380
|
- **Emailed code:** with mail configured, a 6-digit code mailed at each sign-in, valid for 10 minutes. An account with an app can also choose an emailed code at sign-in. Backup codes come with the app only.
|
|
381
381
|
|
|
382
|
-
Turning it on, off, or making new backup codes asks for the account's password when it has one. At sign-in, a correct password or code answers `{ "twoFactorRedirect": true, "twoFactorMethods": ["totp", "otp"] }` instead of a session, and `POST /api/auth/two-factor/verify-totp`, `verify-otp` or `verify-backup-code` with `{ code, trustDevice? }` finishes it; `trustDevice` skips the step in that browser for 30 days. Ten wrong codes in a row lock the account's second step for 15 minutes.
|
|
382
|
+
Turning it on, off, or making new backup codes asks for the account's password when it has one. Without one, turning it on from a session that signed in more than a day ago, on an account without a passkey, asks to [confirm it's them](#confirming-an-account-change) first. At sign-in, a correct password or code answers `{ "twoFactorRedirect": true, "twoFactorMethods": ["totp", "otp"] }` instead of a session, and `POST /api/auth/two-factor/verify-totp`, `verify-otp` or `verify-backup-code` with `{ code, trustDevice? }` finishes it; `trustDevice` skips the step in that browser for 30 days. Ten wrong codes in a row lock the account's second step for 15 minutes.
|
|
383
383
|
|
|
384
384
|
Sign-in with GitHub or another [provider](#sign-in-with-github) is not asked for the second step.
|
|
385
385
|
|
|
@@ -405,10 +405,13 @@ An account with neither a passkey nor a second step has nothing stronger to conf
|
|
|
405
405
|
|
|
406
406
|
Better Auth's own check of a fresh session, which could only be met by signing out and in again, is off (`freshAge: 0`). In its place, a session that signed in more than a day ago confirms it's them in place, as above, before a change that needs it, else the change is `403 STRONG_SIGN_IN_REQUIRED`; the confirmation counts for 15 minutes:
|
|
407
407
|
|
|
408
|
-
- adding a passkey
|
|
408
|
+
- adding a passkey;
|
|
409
|
+
- turning on [two-factor authentication](#two-factor-authentication) on an account without a password, a passkey or a second step (an account with a password gives it instead);
|
|
409
410
|
- unlinking a sign-in provider (`/unlink-account`);
|
|
410
411
|
- [deleting the account](#deleting-the-account) without a password.
|
|
411
412
|
|
|
413
|
+
An account with neither a passkey nor a second step has no strong sign-in to confirm with but an [emailed code](#emailed-confirmation-code), so for its first passkey it may give its password instead, where the account page asks: `POST /api/auth/account/confirm-password` with `{ password }`, `session.account.confirmWithPassword(password)` in the client. It counts for adding that passkey only, within 15 minutes, and is no strong sign-in: a [strong sign-in field](data-config.md#recent-strong-sign-in) still asks for one. A wrong password is `INVALID_PASSWORD`, and every try counts toward the account's [password checks](#name-password-and-devices); an account with a passkey or a second step is refused (`403 PASSWORD_CONFIRMATION_NOT_OFFERED`), and so is a [staff session](#staff-sign-in).
|
|
414
|
+
|
|
412
415
|
Listing the account's devices (`/list-sessions`) needs no confirmation.
|
|
413
416
|
|
|
414
417
|
## Sign-in policy
|
|
@@ -134,6 +134,7 @@ Exit code is 1 when there are errors. Run it after the project's migrations and
|
|
|
134
134
|
|
|
135
135
|
```sh
|
|
136
136
|
protobase dev [--port 5173] [--env DATABASE_URL] [--cache-dir <dir>] [--allowed-hosts <hosts>] [--extend <dir>] [--no-jobs] # run inside a project, e.g. bun run --cwd tests/apps/erp dev
|
|
137
|
+
protobase dev --api <url> [--port 5173] [--cache-dir <dir>] [--allowed-hosts <hosts>] [--extend <dir>] # the UI alone, on the API at <url>
|
|
137
138
|
```
|
|
138
139
|
|
|
139
140
|
One port, two processes: Vite serves the admin app from `@protobase/ui`'s `src/app/` (`dist/app/` when installed; `index.html` and `main.tsx`, or `main.js`; the command fails with a clear message when they are missing) and hands `/api`, supabase-js's `/api/sb` included, to the Hono app from `createAdmin`, which runs in a process of its own; the CLI's process never loads `@protobase/server`. Open `http://localhost:<port>`; the API reference is at `/api/docs`.
|
|
@@ -154,6 +155,17 @@ One port, two processes: Vite serves the admin app from `@protobase/ui`'s `src/a
|
|
|
154
155
|
- **Extensions:** `--extend <dir>` merges an extension in, its UI built into the dev page beside the project's, to try one out; its files reload like the project's.
|
|
155
156
|
- **Hot reload:** server code, data configs, `ui.ts` views, layouts and the config are loaded through Vite's SSR module graph, and the config is read again with each rebuild. A change inside the project (or, in this repository, in `packages/server`, `packages/query`, `packages/schema`) rebuilds the API on the next request without a restart; `/meta` and `X-Meta-Version` change, so open tabs refetch. The database pool survives reloads. Newly added `ui.ts` files appear after the next change inside the project.
|
|
156
157
|
|
|
158
|
+
### UI only
|
|
159
|
+
|
|
160
|
+
:::caution[Beta]
|
|
161
|
+
Sign-in with a passkey or an outside provider, such as Google, belongs to the instance's own address and does not return to the dev server; sign in with an emailed code or a password. Safari keeps no cookie of an `https` instance on `http://localhost`; use Chrome or Firefox.
|
|
162
|
+
:::
|
|
163
|
+
|
|
164
|
+
`--api <url>` serves the admin app with the project's UI, theme, icons and `--extend` folders, hot reloaded as above, and hands every request the API process would answer (`/api`, `/.well-known/webauthn`, the OAuth discovery documents and `/protobase.sw.js`), its WebSockets included, to the Protobase app already running at `<url>`, such as `https://staging.example.com` or another `protobase dev`. Nothing reads a database or the project's config: no `DATABASE_URL`, no `PROTOBASE_SECRET`, no migration checks and no jobs, and resources, roles and data are the instance's. Change the UI here and the server code where the instance runs.
|
|
165
|
+
|
|
166
|
+
- **Checked at start:** the instance must answer `/api/meta` with an `X-Meta-Version` header, as every Protobase app does; otherwise the command names the URL and what came back. `<url>` is an origin, without a path.
|
|
167
|
+
- **Sign-in:** sign in as a user of the instance. Requests from the dev server's own pages reach it with its own Host, Origin and Referer, so its origin checks pass; a request from any other site keeps its own origin, which the instance refuses. Its cookies come back without their `Domain`, for `localhost` to keep, and a redirect to its own address becomes a path on the dev server.
|
|
168
|
+
|
|
157
169
|
### Extending a config
|
|
158
170
|
|
|
159
171
|
`--extend <dir>` (repeatable) merges an extension's folder into the app before the project: its config and the code its paths find, the same as the project's. How each part merges, and what a name defined twice does, is in [Extensions](config.md#extensions).
|