cauthy-auth 0.10.0 → 0.12.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
@@ -4,6 +4,15 @@ import { A as AuthField } from './validate-B_iC-4y8.mjs';
4
4
  export { F as FieldProblem, M as MIN_PASSWORD, c as checkCode, a as checkEmail, b as checkName, d as checkNewPassword, e as checkPassword } from './validate-B_iC-4y8.mjs';
5
5
  import 'jose';
6
6
 
7
+ /** Whether this browser can use a passkey at all. */
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>;
15
+
7
16
  interface SessionState {
8
17
  /** The signed-in person and their organizations, or null. */
9
18
  session: PublicSession | null;
@@ -70,8 +79,29 @@ interface AuthMethods {
70
79
  /** Whether people may create an account themselves. */
71
80
  signUp: boolean;
72
81
  magicLink: boolean;
73
- /** Whether to offer a passkey. It is asked for on Cauthy's page: the person goes there for that step and comes back signed in. */
82
+ /** Whether to offer a passkey. */
74
83
  passkey: boolean;
84
+ /**
85
+ * Whether the passkey is used on this page itself. False: `signIn.passkey`
86
+ * sends the person to Cauthy's page for that step alone, and they come back
87
+ * signed in. That happens only when this page is outside the domain the
88
+ * workspace's passkeys belong to.
89
+ */
90
+ passkeyHere: boolean;
91
+ /**
92
+ * Whether Cauthy's page can use the passkey, for that step alone: where a
93
+ * page that cannot, or a browser that refuses to here, sends the person.
94
+ */
95
+ passkeyOnCauthy: boolean;
96
+ }
97
+ /** A passkey of the signed-in account. Moments are UTC. */
98
+ interface Passkey {
99
+ id: string;
100
+ /** What the person called it ("My laptop"), or "". */
101
+ name: string;
102
+ createdAt: string;
103
+ /** When it last signed the person in, or null. */
104
+ lastUsedAt: string | null;
75
105
  }
76
106
  /** A person's own account, as their account screen shows it. */
77
107
  interface Account {
@@ -175,6 +205,22 @@ interface AccountClient {
175
205
  sessions(): Promise<AccountSession[]>;
176
206
  /** Signs the account out everywhere, here too. */
177
207
  signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
208
+ /** The account's passkeys. */
209
+ passkeys(): Promise<Passkey[]>;
210
+ /**
211
+ * Makes a passkey on this device, on this page: the browser asks the person
212
+ * for their fingerprint, face or screen lock. An account with a password
213
+ * proves it again first. `cancelled`: the person closed the browser's prompt.
214
+ */
215
+ addPasskey(form?: {
216
+ password?: string;
217
+ name?: string;
218
+ }): Promise<AuthResult<'added', {
219
+ passkey: Passkey;
220
+ }>>;
221
+ removePasskey(id: string): Promise<AuthResult<'removed', {
222
+ id: string;
223
+ }>>;
178
224
  }
179
225
  interface AuthClient {
180
226
  signIn: {
@@ -207,12 +253,49 @@ interface AuthClient {
207
253
  }): Promise<AuthResult<'signed_in', {
208
254
  session: PublicSession;
209
255
  }>>;
210
- /** Sends the browser to Cauthy's page to use a passkey; it comes back signed in, at `returnTo`. */
256
+ /**
257
+ * Signs in with a passkey, on this page: the browser asks the person for
258
+ * their fingerprint, face or screen lock. When this page cannot use the
259
+ * workspace's passkeys (`methods().passkeyHere` is false), the browser goes
260
+ * to Cauthy's page for that step alone and comes back signed in, at
261
+ * `returnTo`: then the answer is `redirected`. `cancelled`: the person
262
+ * closed the browser's prompt.
263
+ */
211
264
  passkey(options?: {
212
265
  returnTo?: string;
213
266
  locale?: string;
214
- }): void;
215
- /** Where `passkey` sends the browser, for a plain link. */
267
+ }): Promise<AuthResult<'signed_in', {
268
+ session: PublicSession;
269
+ }> | AuthResult<'redirected'>>;
270
+ /**
271
+ * Offers the person's passkeys in the browser's autofill of the email
272
+ * field, which must carry `autocomplete="username webauthn"`, and signs in
273
+ * with the one they pick. It waits until they pick one or `signal`
274
+ * aborts: abort it before calling `passkey` or sending the form.
275
+ * `cancelled`: aborted, or this browser or page cannot (then there is
276
+ * nothing to offer, and the passkey button still works).
277
+ */
278
+ passkeyAutofill(options?: {
279
+ signal?: AbortSignal;
280
+ }): Promise<AuthResult<'signed_in', {
281
+ session: PublicSession;
282
+ }>>;
283
+ /**
284
+ * Google One Tap: Google's prompt over the page offers the Google account
285
+ * the browser already has, and signs in with it in one tap, as the Google
286
+ * sign-in does (the same account). Needs the workspace's Google sign-in,
287
+ * and the page's address among the Google client's authorized JavaScript
288
+ * origins. Waits until the person picks an account or closes the prompt
289
+ * (`cancelled`), or `signal` aborts. `one_tap_unavailable`: the workspace
290
+ * has no Google sign-in.
291
+ */
292
+ googleOneTap(options?: {
293
+ signal?: AbortSignal;
294
+ context?: 'signin' | 'signup' | 'use';
295
+ }): Promise<AuthResult<'signed_in', {
296
+ session: PublicSession;
297
+ }>>;
298
+ /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
216
299
  passkeyUrl(options?: {
217
300
  returnTo?: string;
218
301
  locale?: string;
@@ -277,4 +360,4 @@ interface AuthClientOptions {
277
360
  }
278
361
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
279
362
 
280
- export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type LinkedIdentity, type SessionState, type SessionStore, type SessionStoreOptions, createAuthClient, createSessionStore };
363
+ 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, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };
package/dist/client.d.ts CHANGED
@@ -4,6 +4,15 @@ import { A as AuthField } from './validate-B_iC-4y8.js';
4
4
  export { F as FieldProblem, M as MIN_PASSWORD, c as checkCode, a as checkEmail, b as checkName, d as checkNewPassword, e as checkPassword } from './validate-B_iC-4y8.js';
5
5
  import 'jose';
6
6
 
7
+ /** Whether this browser can use a passkey at all. */
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>;
15
+
7
16
  interface SessionState {
8
17
  /** The signed-in person and their organizations, or null. */
9
18
  session: PublicSession | null;
@@ -70,8 +79,29 @@ interface AuthMethods {
70
79
  /** Whether people may create an account themselves. */
71
80
  signUp: boolean;
72
81
  magicLink: boolean;
73
- /** Whether to offer a passkey. It is asked for on Cauthy's page: the person goes there for that step and comes back signed in. */
82
+ /** Whether to offer a passkey. */
74
83
  passkey: boolean;
84
+ /**
85
+ * Whether the passkey is used on this page itself. False: `signIn.passkey`
86
+ * sends the person to Cauthy's page for that step alone, and they come back
87
+ * signed in. That happens only when this page is outside the domain the
88
+ * workspace's passkeys belong to.
89
+ */
90
+ passkeyHere: boolean;
91
+ /**
92
+ * Whether Cauthy's page can use the passkey, for that step alone: where a
93
+ * page that cannot, or a browser that refuses to here, sends the person.
94
+ */
95
+ passkeyOnCauthy: boolean;
96
+ }
97
+ /** A passkey of the signed-in account. Moments are UTC. */
98
+ interface Passkey {
99
+ id: string;
100
+ /** What the person called it ("My laptop"), or "". */
101
+ name: string;
102
+ createdAt: string;
103
+ /** When it last signed the person in, or null. */
104
+ lastUsedAt: string | null;
75
105
  }
76
106
  /** A person's own account, as their account screen shows it. */
77
107
  interface Account {
@@ -175,6 +205,22 @@ interface AccountClient {
175
205
  sessions(): Promise<AccountSession[]>;
176
206
  /** Signs the account out everywhere, here too. */
177
207
  signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
208
+ /** The account's passkeys. */
209
+ passkeys(): Promise<Passkey[]>;
210
+ /**
211
+ * Makes a passkey on this device, on this page: the browser asks the person
212
+ * for their fingerprint, face or screen lock. An account with a password
213
+ * proves it again first. `cancelled`: the person closed the browser's prompt.
214
+ */
215
+ addPasskey(form?: {
216
+ password?: string;
217
+ name?: string;
218
+ }): Promise<AuthResult<'added', {
219
+ passkey: Passkey;
220
+ }>>;
221
+ removePasskey(id: string): Promise<AuthResult<'removed', {
222
+ id: string;
223
+ }>>;
178
224
  }
179
225
  interface AuthClient {
180
226
  signIn: {
@@ -207,12 +253,49 @@ interface AuthClient {
207
253
  }): Promise<AuthResult<'signed_in', {
208
254
  session: PublicSession;
209
255
  }>>;
210
- /** Sends the browser to Cauthy's page to use a passkey; it comes back signed in, at `returnTo`. */
256
+ /**
257
+ * Signs in with a passkey, on this page: the browser asks the person for
258
+ * their fingerprint, face or screen lock. When this page cannot use the
259
+ * workspace's passkeys (`methods().passkeyHere` is false), the browser goes
260
+ * to Cauthy's page for that step alone and comes back signed in, at
261
+ * `returnTo`: then the answer is `redirected`. `cancelled`: the person
262
+ * closed the browser's prompt.
263
+ */
211
264
  passkey(options?: {
212
265
  returnTo?: string;
213
266
  locale?: string;
214
- }): void;
215
- /** Where `passkey` sends the browser, for a plain link. */
267
+ }): Promise<AuthResult<'signed_in', {
268
+ session: PublicSession;
269
+ }> | AuthResult<'redirected'>>;
270
+ /**
271
+ * Offers the person's passkeys in the browser's autofill of the email
272
+ * field, which must carry `autocomplete="username webauthn"`, and signs in
273
+ * with the one they pick. It waits until they pick one or `signal`
274
+ * aborts: abort it before calling `passkey` or sending the form.
275
+ * `cancelled`: aborted, or this browser or page cannot (then there is
276
+ * nothing to offer, and the passkey button still works).
277
+ */
278
+ passkeyAutofill(options?: {
279
+ signal?: AbortSignal;
280
+ }): Promise<AuthResult<'signed_in', {
281
+ session: PublicSession;
282
+ }>>;
283
+ /**
284
+ * Google One Tap: Google's prompt over the page offers the Google account
285
+ * the browser already has, and signs in with it in one tap, as the Google
286
+ * sign-in does (the same account). Needs the workspace's Google sign-in,
287
+ * and the page's address among the Google client's authorized JavaScript
288
+ * origins. Waits until the person picks an account or closes the prompt
289
+ * (`cancelled`), or `signal` aborts. `one_tap_unavailable`: the workspace
290
+ * has no Google sign-in.
291
+ */
292
+ googleOneTap(options?: {
293
+ signal?: AbortSignal;
294
+ context?: 'signin' | 'signup' | 'use';
295
+ }): Promise<AuthResult<'signed_in', {
296
+ session: PublicSession;
297
+ }>>;
298
+ /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
216
299
  passkeyUrl(options?: {
217
300
  returnTo?: string;
218
301
  locale?: string;
@@ -277,4 +360,4 @@ interface AuthClientOptions {
277
360
  }
278
361
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
279
362
 
280
- export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type LinkedIdentity, type SessionState, type SessionStore, type SessionStoreOptions, createAuthClient, createSessionStore };
363
+ 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, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };