cauthy-auth 0.11.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
@@ -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. */
@@ -261,6 +267,34 @@ interface AuthClient {
261
267
  }): Promise<AuthResult<'signed_in', {
262
268
  session: PublicSession;
263
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
+ }>>;
264
298
  /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
265
299
  passkeyUrl(options?: {
266
300
  returnTo?: string;
@@ -326,4 +360,4 @@ interface AuthClientOptions {
326
360
  }
327
361
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
328
362
 
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 };
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
@@ -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. */
@@ -261,6 +267,34 @@ interface AuthClient {
261
267
  }): Promise<AuthResult<'signed_in', {
262
268
  session: PublicSession;
263
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
+ }>>;
264
298
  /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
265
299
  passkeyUrl(options?: {
266
300
  returnTo?: string;
@@ -326,4 +360,4 @@ interface AuthClientOptions {
326
360
  }
327
361
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
328
362
 
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 };
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.js CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var client_exports = {};
22
22
  __export(client_exports, {
23
23
  MIN_PASSWORD: () => MIN_PASSWORD,
24
+ canAutofillPasskeys: () => canAutofillPasskeys,
24
25
  canUsePasskeys: () => canUsePasskeys,
25
26
  checkCode: () => checkCode,
26
27
  checkEmail: () => checkEmail,
@@ -36,6 +37,15 @@ module.exports = __toCommonJS(client_exports);
36
37
  function canUsePasskeys() {
37
38
  return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
38
39
  }
40
+ async function canAutofillPasskeys() {
41
+ if (!canUsePasskeys()) return false;
42
+ const pkc = window.PublicKeyCredential;
43
+ try {
44
+ return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
39
49
  function bytes(text2) {
40
50
  const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
41
51
  const bin = atob(b64);
@@ -51,9 +61,9 @@ function text(buf) {
51
61
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
52
62
  }
53
63
  var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
54
- async function getPasskey(options) {
64
+ async function getPasskey(options, how = {}) {
55
65
  const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
56
- const cred = await navigator.credentials.get({ publicKey });
66
+ const cred = await navigator.credentials.get({ publicKey, ...how });
57
67
  if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
58
68
  const r = cred.response;
59
69
  return {
@@ -281,6 +291,24 @@ function createSessionStore(options = {}) {
281
291
  }
282
292
 
283
293
  // src/client.ts
294
+ var googleScript = null;
295
+ function loadGoogleIdentity() {
296
+ const ready = () => window.google?.accounts?.id;
297
+ if (ready()) return Promise.resolve(ready());
298
+ googleScript ??= new Promise((resolve, reject) => {
299
+ const script = document.createElement("script");
300
+ script.src = "https://accounts.google.com/gsi/client";
301
+ script.async = true;
302
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
303
+ script.onerror = () => {
304
+ googleScript = null;
305
+ reject(new Error("google identity services did not load"));
306
+ };
307
+ document.head.appendChild(script);
308
+ });
309
+ return googleScript;
310
+ }
311
+ var PASSKEY_SECONDS = 300;
284
312
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
285
313
  function createAuthClient(options = {}) {
286
314
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -364,6 +392,66 @@ function createAuthClient(options = {}) {
364
392
  window.location.assign(passkeyUrl(o));
365
393
  return { ok: true, status: "redirected" };
366
394
  },
395
+ async passkeyAutofill(o = {}) {
396
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
397
+ const m = await methods();
398
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
399
+ for (; ; ) {
400
+ if (o.signal?.aborted) return cancelled;
401
+ const begun = await post("/sign-in/passkey/begin", {}, null);
402
+ if (!begun.ok) return begun;
403
+ const renew = new AbortController();
404
+ const stop = () => renew.abort();
405
+ o.signal?.addEventListener("abort", stop, { once: true });
406
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
407
+ let credential;
408
+ try {
409
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
410
+ } catch (e) {
411
+ if (o.signal?.aborted) return cancelled;
412
+ if (renew.signal.aborted) continue;
413
+ return { ok: false, error: ceremonyError(e) };
414
+ } finally {
415
+ clearTimeout(timer);
416
+ o.signal?.removeEventListener("abort", stop);
417
+ }
418
+ return post("/sign-in/passkey/finish", { credential }, null);
419
+ }
420
+ },
421
+ async googleOneTap(o = {}) {
422
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
423
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
424
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
425
+ if (!begun.ok) return begun;
426
+ let gis;
427
+ try {
428
+ gis = await loadGoogleIdentity();
429
+ } catch {
430
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
431
+ }
432
+ const credential = await new Promise((done) => {
433
+ o.signal?.addEventListener("abort", () => {
434
+ gis.cancel();
435
+ done(null);
436
+ }, { once: true });
437
+ gis.initialize({
438
+ client_id: begun.clientId,
439
+ nonce: begun.nonce,
440
+ context: o.context ?? "signin",
441
+ callback: (r) => done(r.credential ?? null),
442
+ auto_select: false,
443
+ cancel_on_tap_outside: true,
444
+ itp_support: true,
445
+ use_fedcm_for_prompt: true
446
+ });
447
+ gis.prompt((moment) => {
448
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
449
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
450
+ });
451
+ });
452
+ if (credential === null) return cancelled;
453
+ return post("/sign-in/google-one-tap", { credential }, null);
454
+ },
367
455
  socialUrl,
368
456
  social(provider, o) {
369
457
  window.location.assign(socialUrl(provider, o));
@@ -436,6 +524,7 @@ function createAuthClient(options = {}) {
436
524
  // Annotate the CommonJS export names for ESM import in node:
437
525
  0 && (module.exports = {
438
526
  MIN_PASSWORD,
527
+ canAutofillPasskeys,
439
528
  canUsePasskeys,
440
529
  checkCode,
441
530
  checkEmail,