cauthy-auth 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,10 +101,11 @@ import 'cauthy-auth/styles.css'
101
101
 
102
102
  Or draw them with your own `components` and `classNames`, and pass `unstyled`. The dashboard can start the look from your stylesheet (shadcn/ui and Tailwind, Stackmyth, or variables of your own); `npx cauthy-auth theme --css src/app/globals.css` reads it the same way from a terminal.
103
103
 
104
- Connect your application to Cauthy in one step, from Cauthy's dashboard ("Connect a System") or from a terminal. It makes the client, registers `https://<your app>/api/auth/callback`, and prints every variable to set, `CAUTHY_SECRET` included:
104
+ Connect your application to Cauthy in one step, from Cauthy's dashboard ("Connect a System") or from a terminal. In the application's folder, `init` reads its framework (Next.js, TanStack Start, SvelteKit, Express, Hono, Fastify), makes the client, writes every variable to its env file, `CAUTHY_SECRET` included, and lays down the route and a sign-in page, never over a file that is there:
105
105
 
106
106
  ```bash
107
- npx cauthy-auth init --system ledger --app-url https://app.example.com
107
+ npx cauthy-auth init
108
+ npx cauthy-auth init --system ledger --app-url https://app.example.com # production
108
109
  ```
109
110
 
110
111
  The same client is your application's own identity: it manages its organizations with no person's token.
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  // src/cli.ts
5
5
  var import_node_fs = require("fs");
6
+ var import_node_path2 = require("path");
6
7
 
7
8
  // src/connect.ts
8
9
  function sessionSecret() {
@@ -51,6 +52,206 @@ function envFile(env2) {
51
52
  return Object.entries(env2).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
52
53
  }
53
54
 
55
+ // src/scaffold.ts
56
+ var import_node_path = require("path");
57
+ var FRAMEWORK_NAMES = {
58
+ next: "Next.js",
59
+ "tanstack-start": "TanStack Start",
60
+ sveltekit: "SvelteKit",
61
+ express: "Express",
62
+ hono: "Hono",
63
+ fastify: "Fastify"
64
+ };
65
+ function detect(pkg, has) {
66
+ const deps = { ...pkg.devDependencies, ...pkg.dependencies };
67
+ const framework = deps.next ? "next" : deps["@tanstack/react-start"] ? "tanstack-start" : deps["@sveltejs/kit"] ? "sveltekit" : deps.hono ? "hono" : deps.fastify ? "fastify" : deps.express ? "express" : null;
68
+ const port = framework === "sveltekit" ? 5173 : 3e3;
69
+ let root = has("src") ? "src" : "";
70
+ let nextPagesOnly = false;
71
+ if (framework === "next") {
72
+ root = has("src/app") ? "src" : has("app") ? "" : has("src/pages") ? "src" : "";
73
+ nextPagesOnly = !has("src/app") && !has("app");
74
+ }
75
+ if (framework === "tanstack-start" || framework === "sveltekit") root = "src";
76
+ const major = /(\d+)/.exec(deps.next ?? "");
77
+ return {
78
+ framework,
79
+ ...nextPagesOnly ? { nextPagesOnly } : {},
80
+ root,
81
+ typescript: has("tsconfig.json") || Boolean(deps.typescript),
82
+ esm: pkg.type === "module",
83
+ ...framework === "next" && major ? { nextMajor: Number(major[1]) } : {},
84
+ devUrl: `http://localhost:${port}`,
85
+ system: systemName(pkg.name),
86
+ installed: Boolean(deps["cauthy-auth"]),
87
+ packageManager: has("pnpm-lock.yaml") ? "pnpm" : has("yarn.lock") ? "yarn" : has("bun.lockb") || has("bun.lock") ? "bun" : "npm"
88
+ };
89
+ }
90
+ function systemName(pkgName) {
91
+ const bare = (pkgName ?? "").replace(/^@[^/]+\//, "");
92
+ const name = bare.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
93
+ return name || "app";
94
+ }
95
+ var join = (...parts) => import_node_path.posix.join(...parts.filter(Boolean));
96
+ function importPath(from, to) {
97
+ const rel = import_node_path.posix.relative(import_node_path.posix.dirname(from), to).replace(/\.(ts|tsx|js|jsx)$/, "");
98
+ return rel.startsWith(".") ? rel : `./${rel}`;
99
+ }
100
+ function scaffold(d) {
101
+ const ext = d.typescript ? "ts" : "js";
102
+ const jsx = d.typescript ? "tsx" : "jsx";
103
+ const install = `${d.packageManager === "npm" ? "npm install" : `${d.packageManager} add`} cauthy-auth`;
104
+ const dev = `${d.packageManager === "npm" ? "npm run" : d.packageManager} dev`;
105
+ const steps = d.installed ? [] : [`Install the package: ${install}`];
106
+ const files = [];
107
+ const signIn = `${d.devUrl}/sign-in`;
108
+ const hosted = `${d.devUrl}/api/auth/login`;
109
+ switch (d.framework) {
110
+ case "next": {
111
+ const authFile = join(d.root, "lib", `auth.${ext}`);
112
+ files.push({ path: authFile, content: NEXT_AUTH });
113
+ if (d.nextPagesOnly) {
114
+ steps.push('The pages router is not written for you: serve auth.handler(request) under /api/auth/* (see the Auth SDK guide, "Mount the route").');
115
+ return { files, steps, envFile: ".env.local", open: hosted };
116
+ }
117
+ const route = join(d.root, "app/api/auth/[...cauthy]", `route.${ext}`);
118
+ files.push({ path: route, content: `// Serves cauthy-auth: sign-in, return, session, sign-out.
119
+ import { cauthy } from '${importPath(route, authFile)}'
120
+
121
+ export const { GET, POST } = cauthy
122
+ ` });
123
+ const before = join(d.root, `${(d.nextMajor ?? 15) >= 16 ? "proxy" : "middleware"}.${ext}`);
124
+ files.push({ path: before, content: `// Keeps the session renewed. To send anybody not signed in away from some
125
+ // pages: cauthy.middleware({ protect: (path) => path.startsWith('/app') }).
126
+ import { cauthy } from '${importPath(before, authFile)}'
127
+
128
+ export default cauthy.middleware()
129
+
130
+ export const config = { matcher: ['/((?!_next/|favicon.ico).*)'] }
131
+ ` });
132
+ files.push({ path: join(d.root, "app/sign-in", `page.${jsx}`), content: reactSignInPage(`'use client'
133
+
134
+ `, "export default function SignInPage") });
135
+ steps.push(`Start the application: ${dev}`, `Open ${signIn}`);
136
+ return { files, steps, envFile: ".env.local", open: signIn };
137
+ }
138
+ case "tanstack-start": {
139
+ const authFile = join(d.root, `auth.${ext}`);
140
+ files.push({ path: authFile, content: PLAIN_AUTH });
141
+ const route = join(d.root, "routes/api/auth", `$.${ext}`);
142
+ files.push({ path: route, content: `// Serves cauthy-auth: sign-in, return, session, sign-out.
143
+ import { createFileRoute } from '@tanstack/react-router'
144
+ import { auth } from '${importPath(route, authFile)}'
145
+
146
+ export const Route = createFileRoute('/api/auth/$')({
147
+ server: {
148
+ handlers: {
149
+ GET: ({ request }) => auth.handler(request),
150
+ POST: ({ request }) => auth.handler(request),
151
+ },
152
+ },
153
+ })
154
+ ` });
155
+ files.push({ path: join(d.root, "routes", `sign-in.${jsx}`), content: reactSignInPage(`import { createFileRoute } from '@tanstack/react-router'
156
+ `, "function SignInPage", `
157
+ export const Route = createFileRoute('/sign-in')({ component: SignInPage })
158
+ `) });
159
+ steps.push(`Start the application: ${dev}`, `Open ${signIn}`);
160
+ return { files, steps, envFile: ".env", open: signIn };
161
+ }
162
+ case "sveltekit": {
163
+ const authFile = join(d.root, "lib/server", `auth.${ext}`);
164
+ files.push({ path: authFile, content: PLAIN_AUTH });
165
+ const route = join(d.root, "routes/api/auth/[...cauthy]", `+server.${ext}`);
166
+ const param = d.typescript ? "{ request }: { request: Request }" : "{ request }";
167
+ files.push({ path: route, content: `// Serves cauthy-auth: sign-in, return, session, sign-out.
168
+ import { auth } from '$lib/server/auth'
169
+
170
+ export const GET = (${param}) => auth.handler(request)
171
+ export const POST = (${param}) => auth.handler(request)
172
+ ` });
173
+ steps.push(`Start the application: ${dev}`, `Open ${hosted}: Cauthy's sign-in page, and back. For screens of your own, see createCauthySession in cauthy-auth/svelte.`);
174
+ return { files, steps, envFile: ".env", open: hosted };
175
+ }
176
+ case "express":
177
+ case "hono":
178
+ case "fastify": {
179
+ const authFile = join(d.root, `auth.${ext}`);
180
+ const cjs = !d.typescript && !d.esm;
181
+ files.push({ path: authFile, content: cjs ? PLAIN_AUTH_CJS : PLAIN_AUTH });
182
+ steps.push(`Add to your server, where the app is made:
183
+
184
+ ${serverLines(d.framework, cjs)}`, `Start the application: ${dev}`, `Open ${hosted}`);
185
+ return { files, steps, envFile: ".env", open: hosted };
186
+ }
187
+ default:
188
+ steps.push('Serve auth.handler(request) under /api/auth/* (see the Auth SDK guide, "Mount the route").');
189
+ return { files, steps, envFile: ".env", open: hosted };
190
+ }
191
+ }
192
+ function serverLines(f, cjs) {
193
+ const from = "./auth";
194
+ if (f === "express") {
195
+ return cjs ? ` const { cauthyExpress } = require('cauthy-auth/express')
196
+ const { auth } = require('${from}')
197
+ app.use(cauthyExpress(auth))` : ` import { cauthyExpress } from 'cauthy-auth/express'
198
+ import { auth } from '${from}'
199
+ app.use(cauthyExpress(auth))`;
200
+ }
201
+ if (f === "hono") {
202
+ return cjs ? ` const { cauthyHono } = require('cauthy-auth/hono')
203
+ const { auth } = require('${from}')
204
+ app.use(cauthyHono(auth))` : ` import { cauthyHono } from 'cauthy-auth/hono'
205
+ import { auth } from '${from}'
206
+ app.use(cauthyHono(auth))`;
207
+ }
208
+ return cjs ? ` const { cauthyFastify } = require('cauthy-auth/fastify')
209
+ const { auth } = require('${from}')
210
+ await app.register(cauthyFastify, { auth })` : ` import { cauthyFastify } from 'cauthy-auth/fastify'
211
+ import { auth } from '${from}'
212
+ await app.register(cauthyFastify, { auth })`;
213
+ }
214
+ var NEXT_AUTH = `// createCauthy() reads CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_CLIENT_SECRET,
215
+ // CAUTHY_SECRET and CAUTHY_TENANT from the environment.
216
+ import { createCauthy } from 'cauthy-auth'
217
+ import { cauthyNext } from 'cauthy-auth/next'
218
+
219
+ export const auth = createCauthy()
220
+ export const cauthy = cauthyNext(auth)
221
+ `;
222
+ var PLAIN_AUTH = `// createCauthy() reads CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_CLIENT_SECRET,
223
+ // CAUTHY_SECRET and CAUTHY_TENANT from the environment.
224
+ import { createCauthy } from 'cauthy-auth'
225
+
226
+ export const auth = createCauthy()
227
+ `;
228
+ var PLAIN_AUTH_CJS = `// createCauthy() reads CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_CLIENT_SECRET,
229
+ // CAUTHY_SECRET and CAUTHY_TENANT from the environment.
230
+ const { createCauthy } = require('cauthy-auth')
231
+
232
+ exports.auth = createCauthy()
233
+ `;
234
+ function reactSignInPage(head, declaration, tail = "") {
235
+ return `${head}// The application's own sign-in page. <SignIn /> is the ready-made form: the
236
+ // workspace's providers, passkeys, the second step and a sign-in link, drawn
237
+ // with the look set in Cauthy's dashboard. useAuth() has the same functions
238
+ // for a form of your own. Move <CauthyProvider> to the application's root
239
+ // to read the session on every page.
240
+ import { CauthyProvider, SignIn } from 'cauthy-auth/react'
241
+ import 'cauthy-auth/styles.css'
242
+ ${tail}
243
+ ${declaration}() {
244
+ return (
245
+ <main style={{ maxWidth: 380, margin: '64px auto', padding: '0 16px' }}>
246
+ <CauthyProvider>
247
+ <SignIn returnTo="/" />
248
+ </CauthyProvider>
249
+ </main>
250
+ )
251
+ }
252
+ `;
253
+ }
254
+
54
255
  // src/permissions.ts
55
256
  var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
56
257
  function policySource(system, roles) {
@@ -707,14 +908,19 @@ function createSystem(given = {}) {
707
908
  }
708
909
 
709
910
  // src/cli.ts
710
- var INIT_HELP = `cauthy-auth init --system <name> --app-url <address> [options]
911
+ var INIT_HELP = `cauthy-auth init [options]
711
912
 
712
- Connects an application to Cauthy: makes its client, registers where its people
713
- come back to and where it hears events, and prints the variables to set.
913
+ Connects an application to Cauthy and leaves signing in working. Run it in
914
+ the application's folder: it reads which framework the application is made
915
+ with (Next.js, TanStack Start, SvelteKit, Express, Hono, Fastify), makes its
916
+ client in Cauthy, writes the variables to its env file, and lays down the
917
+ route and a sign-in page. A file that is already there is never written over.
714
918
 
715
919
  --system <name> The system's name: lowercase letters, digits, - or _.
920
+ Default: the package's name.
716
921
  --app-url <address> The application's public address: https://app.example.com,
717
- or http://localhost:3000 while developing.
922
+ or http://localhost:3000 while developing. Default: the
923
+ framework's address while developing.
718
924
  --name <name> Shown to people on Cauthy's pages. Default: the system's name.
719
925
  --callback-path <path> Where the application serves the return from a sign-in.
720
926
  Default: /api/auth/callback.
@@ -722,35 +928,78 @@ come back to and where it hears events, and prints the variables to set.
722
928
  Cauthy's events (verify them with auth.webhook).
723
929
  --jwks-uri <address> Where the application publishes its keys, to prove
724
930
  itself with a signature instead of its secret.
725
- --write <file> Append the variables to this file instead of printing
726
- them (they include the client's secret, shown once).
931
+ --write <file> The env file to add the variables to. Default: .env.local
932
+ for Next.js, .env otherwise.
933
+ --print Print the variables instead of writing them.
934
+ --no-files Connect only: write no route and no page.
935
+ --reconnect Connect again although the env file has Cauthy's
936
+ variables: a new client, with a new secret.
727
937
 
728
938
  Needs CAUTHY_ISSUER, CAUTHY_TENANT (the workspace's slug) and CAUTHY_TOKEN (a
729
939
  personal access token with systems:connect).
730
940
  `;
941
+ function readJson(file) {
942
+ try {
943
+ return JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
944
+ } catch {
945
+ return null;
946
+ }
947
+ }
948
+ function ignored(file) {
949
+ let lines;
950
+ try {
951
+ lines = (0, import_node_fs.readFileSync)(".gitignore", "utf8").split(/\r?\n/).map((l) => l.trim().replace(/^\//, ""));
952
+ } catch {
953
+ return false;
954
+ }
955
+ return lines.some((l) => l === file || l === ".env*" || l === "*.env" || file.endsWith(".local") && (l === "*.local" || l === ".env*.local"));
956
+ }
731
957
  async function init(argv) {
732
958
  if (argv.includes("--help") || argv.includes("-h")) {
733
959
  process.stdout.write(INIT_HELP);
734
960
  return;
735
961
  }
736
962
  const given = {};
737
- const flags = ["--system", "--app-url", "--name", "--callback-path", "--events-url", "--jwks-uri", "--write"];
963
+ const valued = ["--system", "--app-url", "--name", "--callback-path", "--events-url", "--jwks-uri", "--write"];
964
+ const switches = ["--print", "--no-files", "--reconnect"];
738
965
  for (let i = 0; i < argv.length; i++) {
739
966
  const arg = argv[i];
740
- if (!flags.includes(arg)) fail(`unknown option ${arg}
967
+ if (switches.includes(arg)) given[arg] = "yes";
968
+ else if (valued.includes(arg)) given[arg] = argv[++i] ?? fail(`${arg} needs a value`);
969
+ else fail(`unknown option ${arg}
741
970
 
742
971
  ${INIT_HELP}`);
743
- given[arg] = argv[++i] ?? fail(`${arg} needs a value`);
744
972
  }
745
- if (!given["--system"] || !given["--app-url"]) fail("give --system and --app-url\n\n" + INIT_HELP);
973
+ const pkg = readJson("package.json");
974
+ const found = pkg ? detect(pkg, import_node_fs.existsSync) : null;
975
+ const plan = found ? scaffold(found) : null;
976
+ const system = given["--system"] ?? found?.system;
977
+ const appUrl = given["--app-url"] ?? found?.devUrl;
978
+ if (!system || !appUrl) fail("no package.json here: give --system and --app-url\n\n" + INIT_HELP);
979
+ if (found?.framework) process.stdout.write(`Found ${FRAMEWORK_NAMES[found.framework]}.
980
+ `);
981
+ const target = given["--write"] ?? plan?.envFile;
982
+ let had = "";
983
+ try {
984
+ had = target ? (0, import_node_fs.readFileSync)(target, "utf8") : "";
985
+ } catch {
986
+ }
987
+ const already = /^CAUTHY_CLIENT_ID=/m.test(had);
988
+ if (already && !given["--reconnect"]) {
989
+ process.stdout.write(`Already connected: ${target} has Cauthy's variables. (--reconnect makes a new client.)
990
+ `);
991
+ return writeFiles(plan, given["--no-files"] !== void 0);
992
+ }
746
993
  const done = await connectSystem({
747
- system: given["--system"],
748
- appUrl: given["--app-url"],
994
+ system,
995
+ appUrl,
749
996
  name: given["--name"],
750
997
  callbackPath: given["--callback-path"],
751
998
  eventsUrl: given["--events-url"],
752
999
  jwksUri: given["--jwks-uri"]
753
1000
  });
1001
+ const tenant = process.env.CAUTHY_TENANT;
1002
+ if (tenant && !done.env.CAUTHY_TENANT) done.env.CAUTHY_TENANT = tenant;
754
1003
  const vars = envFile(done.env);
755
1004
  process.stdout.write(`Connected: the system ${done.system} is the client ${done.clientId}.
756
1005
  `);
@@ -758,25 +1007,55 @@ ${INIT_HELP}`);
758
1007
  `);
759
1008
  if (done.webhook) process.stdout.write(` events are sent to ${done.webhook.url}
760
1009
  `);
761
- if (given["--write"]) {
762
- (0, import_node_fs.appendFileSync)(given["--write"], vars, { mode: 384 });
763
- process.stdout.write(`
764
- The variables were added to ${given["--write"]}. Keep that file out of the repository.
1010
+ if (given["--print"] || !target || already) {
1011
+ if (already) process.stdout.write(`
1012
+ ${target} already has Cauthy's variables: replace them with these by hand.
765
1013
  `);
766
- } else {
767
1014
  process.stdout.write(`
768
1015
  Set these where the application runs. The secret is shown this once:
769
1016
 
770
1017
  ${vars}`);
1018
+ } else {
1019
+ (0, import_node_fs.appendFileSync)(target, (had && !had.endsWith("\n") ? "\n" : "") + vars, { mode: 384 });
1020
+ process.stdout.write(`
1021
+ The variables were added to ${target}.
1022
+ `);
1023
+ if (!ignored(target)) {
1024
+ (0, import_node_fs.appendFileSync)(".gitignore", `${(0, import_node_fs.existsSync)(".gitignore") ? "\n" : ""}${target}
1025
+ `);
1026
+ process.stdout.write(`${target} was added to .gitignore: it holds the client's secret.
1027
+ `);
1028
+ }
771
1029
  }
772
- process.stdout.write(`
1030
+ writeFiles(plan, given["--no-files"] !== void 0);
1031
+ }
1032
+ function writeFiles(plan, none) {
1033
+ if (!plan || none) {
1034
+ process.stdout.write(`
773
1035
  Then, in the application:
774
1036
 
775
1037
  import { createCauthy } from 'cauthy-auth'
776
1038
  export const auth = createCauthy()
777
1039
 
778
- and serve auth.handler(request) under ${(given["--callback-path"] ?? "/api/auth/callback").replace(/\/callback$/, "")}/*.
1040
+ and serve auth.handler(request) under /api/auth/*.
1041
+ `);
1042
+ return;
1043
+ }
1044
+ process.stdout.write("\n");
1045
+ for (const f of plan.files) {
1046
+ if ((0, import_node_fs.existsSync)(f.path)) {
1047
+ process.stdout.write(` left as it was ${f.path} (it is already there)
779
1048
  `);
1049
+ continue;
1050
+ }
1051
+ (0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(f.path) || ".", { recursive: true });
1052
+ (0, import_node_fs.writeFileSync)(f.path, f.content);
1053
+ process.stdout.write(` wrote ${f.path}
1054
+ `);
1055
+ }
1056
+ process.stdout.write("\nWhat is left:\n\n");
1057
+ plan.steps.forEach((step, i) => process.stdout.write(`${i + 1}. ${step}
1058
+ `));
780
1059
  }
781
1060
  var POLICY_HELP = `cauthy-auth policy --pull [--out <file.ts>] [--system <name>]
782
1061
  cauthy-auth policy --file <roles.json> [--system <name>] [--push]
@@ -869,7 +1148,7 @@ ${POLICY_HELP}`);
869
1148
  process.stdout.write("Not sent. Add --push to give it to Cauthy.\n");
870
1149
  }
871
1150
  }
872
- var HELP = `cauthy-auth init \u2026 connect an application to Cauthy (cauthy-auth init --help)
1151
+ var HELP = `cauthy-auth init \u2026 connect an application and leave signing in working (cauthy-auth init --help)
873
1152
  cauthy-auth policy \u2026 load a system's catalog of roles (cauthy-auth policy --help)
874
1153
  cauthy-auth theme [options]
875
1154
 
package/dist/client.d.mts CHANGED
@@ -6,6 +6,12 @@ import 'jose';
6
6
 
7
7
  /** Whether this browser can use a passkey at all. */
8
8
  declare function canUsePasskeys(): boolean;
9
+ /**
10
+ * Whether this browser offers passkeys in the autofill of a field marked
11
+ * `autocomplete="username webauthn"`, so the person picks one without
12
+ * looking for a button.
13
+ */
14
+ declare function canAutofillPasskeys(): Promise<boolean>;
9
15
 
10
16
  interface SessionState {
11
17
  /** The signed-in person and their organizations, or null. */
@@ -87,6 +93,13 @@ interface AuthMethods {
87
93
  * page that cannot, or a browser that refuses to here, sends the person.
88
94
  */
89
95
  passkeyOnCauthy: boolean;
96
+ /**
97
+ * The workspace's Cloudflare Turnstile site key, when it asks for the
98
+ * challenge: signing in, creating an account, and asking for a sign-in link
99
+ * or a new password then take its answer (`captchaToken`). The ready-made
100
+ * forms show it themselves.
101
+ */
102
+ captchaSiteKey?: string;
90
103
  }
91
104
  /** A passkey of the signed-in account. Moments are UTC. */
92
105
  interface Passkey {
@@ -128,6 +141,17 @@ interface LinkedIdentity {
128
141
  /** When it was linked, in UTC (ISO 8601). */
129
142
  linkedAt: string;
130
143
  }
144
+ /** How an account takes its second step. */
145
+ interface SecondStep {
146
+ totp: boolean;
147
+ /** The phone codes are sent to, masked (`+57 ••• ••• 4567`). */
148
+ phone: {
149
+ number: string;
150
+ channel: string;
151
+ } | null;
152
+ /** The ways the workspace sends codes to phones. */
153
+ phoneChannels: string[];
154
+ }
131
155
  interface AccountClient {
132
156
  /** The account, or null when nobody is signed in. */
133
157
  get(): Promise<Account | null>;
@@ -166,6 +190,30 @@ interface AccountClient {
166
190
  }): Promise<AuthResult<'renewed', {
167
191
  recoveryCodes: string[];
168
192
  }>>;
193
+ /**
194
+ * How the account takes its second step: an authenticator app, a phone
195
+ * (masked), and the ways the workspace sends codes to phones (`sms`,
196
+ * `whatsapp`; none when it has no Twilio account for it).
197
+ */
198
+ factors(): Promise<SecondStep | null>;
199
+ /** Adds a phone as the second step: a code is sent to it, for `verifyPhone`. */
200
+ addPhone(form: {
201
+ phone: string;
202
+ channel: 'sms' | 'whatsapp';
203
+ password: string;
204
+ }): Promise<AuthResult<'sent', {
205
+ sentTo: string;
206
+ channel: string;
207
+ }>>;
208
+ /** Proves the phone with the code it received. `recoveryCodes` come with a first second step, shown this once. */
209
+ verifyPhone(form: {
210
+ code: string;
211
+ }): Promise<AuthResult<'enabled', {
212
+ recoveryCodes: string[];
213
+ }>>;
214
+ removePhone(form: {
215
+ password: string;
216
+ }): Promise<AuthResult<'removed'>>;
169
217
  };
170
218
  /**
171
219
  * Asks to sign in with another address. Nothing changes yet: the new address
@@ -222,6 +270,7 @@ interface AuthClient {
222
270
  email(form: {
223
271
  email: string;
224
272
  password: string;
273
+ captchaToken?: string;
225
274
  }): Promise<AuthResult<'signed_in', {
226
275
  session: PublicSession;
227
276
  }> | AuthResult<'mfa_required'>>;
@@ -231,9 +280,23 @@ interface AuthClient {
231
280
  }): Promise<AuthResult<'signed_in', {
232
281
  session: PublicSession;
233
282
  }>>;
283
+ /** While the second step is asked for: whether the account has an authenticator app, and the phone (masked) a code can be sent to. */
284
+ mfaMethods(): Promise<AuthResult<'ok', {
285
+ totp: boolean;
286
+ phone: {
287
+ number: string;
288
+ channel: string;
289
+ } | null;
290
+ }>>;
291
+ /** Sends a code to the account's phone, by SMS or WhatsApp; `mfa` takes it. A few per sign-in. */
292
+ mfaSendCode(): Promise<AuthResult<'sent', {
293
+ sentTo: string;
294
+ channel: string;
295
+ }>>;
234
296
  /** Mails a link that signs the person in. The answer is the same whether or not the address has an account. */
235
297
  magicLink(form: {
236
298
  email: string;
299
+ captchaToken?: string;
237
300
  }): Promise<AuthResult<'sent'>>;
238
301
  /** Signs in with the token the mailed link opened the page with (`ml_token`). */
239
302
  magicLinkVerify(form: {
@@ -261,6 +324,34 @@ interface AuthClient {
261
324
  }): Promise<AuthResult<'signed_in', {
262
325
  session: PublicSession;
263
326
  }> | AuthResult<'redirected'>>;
327
+ /**
328
+ * Offers the person's passkeys in the browser's autofill of the email
329
+ * field, which must carry `autocomplete="username webauthn"`, and signs in
330
+ * with the one they pick. It waits until they pick one or `signal`
331
+ * aborts: abort it before calling `passkey` or sending the form.
332
+ * `cancelled`: aborted, or this browser or page cannot (then there is
333
+ * nothing to offer, and the passkey button still works).
334
+ */
335
+ passkeyAutofill(options?: {
336
+ signal?: AbortSignal;
337
+ }): Promise<AuthResult<'signed_in', {
338
+ session: PublicSession;
339
+ }>>;
340
+ /**
341
+ * Google One Tap: Google's prompt over the page offers the Google account
342
+ * the browser already has, and signs in with it in one tap, as the Google
343
+ * sign-in does (the same account). Needs the workspace's Google sign-in,
344
+ * and the page's address among the Google client's authorized JavaScript
345
+ * origins. Waits until the person picks an account or closes the prompt
346
+ * (`cancelled`), or `signal` aborts. `one_tap_unavailable`: the workspace
347
+ * has no Google sign-in.
348
+ */
349
+ googleOneTap(options?: {
350
+ signal?: AbortSignal;
351
+ context?: 'signin' | 'signup' | 'use';
352
+ }): Promise<AuthResult<'signed_in', {
353
+ session: PublicSession;
354
+ }>>;
264
355
  /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
265
356
  passkeyUrl(options?: {
266
357
  returnTo?: string;
@@ -281,6 +372,7 @@ interface AuthClient {
281
372
  email: string;
282
373
  password: string;
283
374
  name?: string;
375
+ captchaToken?: string;
284
376
  }): Promise<AuthResult<'signed_in', {
285
377
  session: PublicSession;
286
378
  }> | AuthResult<'verification_required', {
@@ -290,6 +382,7 @@ interface AuthClient {
290
382
  /** Mails a link to choose a new password. The answer is the same whether or not the address has an account. */
291
383
  forgotPassword(form: {
292
384
  email: string;
385
+ captchaToken?: string;
293
386
  }): Promise<AuthResult<'sent'>>;
294
387
  /** Sets the new password, with the token the mailed link opened the page with (`token`). */
295
388
  resetPassword(form: {
@@ -326,4 +419,4 @@ interface AuthClientOptions {
326
419
  }
327
420
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
328
421
 
329
- export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type LinkedIdentity, type Passkey, type SessionState, type SessionStore, type SessionStoreOptions, canUsePasskeys, createAuthClient, createSessionStore };
422
+ export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type LinkedIdentity, type Passkey, type SecondStep, type SessionState, type SessionStore, type SessionStoreOptions, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };