cauthy-auth 0.15.0 → 0.17.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,7 +101,7 @@ 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. 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:
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, React Router or Remix, SvelteKit, Vite + React, 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
107
  npx cauthy-auth init
@@ -131,6 +131,13 @@ const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:
131
131
  app.use(auth.middleware()) // or auth.protect(handler) for Request/Response
132
132
  ```
133
133
 
134
+ When that server calls another of your servers for the person, it trades the agent's token for one for that server (token exchange, RFC 8693). Delegate the call and create the server's credentials in the dashboard first:
135
+
136
+ ```ts
137
+ const auth = mcpAuth({ resource, clientId: process.env.CAUTHY_MCP_CLIENT_ID, clientSecret: process.env.CAUTHY_MCP_CLIENT_SECRET })
138
+ const { accessToken } = await auth.exchange(req.agent, 'https://api.example.com', ['orders:read'])
139
+ ```
140
+
134
141
  The full guide is in Cauthy's documentation, under "Auth SDK".
135
142
 
136
143
  ## License
package/dist/cli.js CHANGED
@@ -57,33 +57,41 @@ var import_node_path = require("path");
57
57
  var FRAMEWORK_NAMES = {
58
58
  next: "Next.js",
59
59
  "tanstack-start": "TanStack Start",
60
+ "react-router": "React Router",
60
61
  sveltekit: "SvelteKit",
61
62
  express: "Express",
62
63
  hono: "Hono",
63
- fastify: "Fastify"
64
+ fastify: "Fastify",
65
+ "vite-react": "Vite + React"
64
66
  };
65
67
  function detect(pkg, has) {
66
68
  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
+ const remix = Boolean(deps["@remix-run/react"] || deps["@remix-run/node"]);
70
+ const framework = deps.next ? "next" : deps["@tanstack/react-start"] ? "tanstack-start" : deps["@react-router/dev"] || remix ? "react-router" : deps["@sveltejs/kit"] ? "sveltekit" : deps.vite && deps.react && !deps.express && !deps.fastify ? "vite-react" : deps.hono ? "hono" : deps.fastify ? "fastify" : deps.express ? "express" : null;
71
+ const port = framework === "sveltekit" || framework === "vite-react" || framework === "react-router" && !(remix && !deps.vite) ? 5173 : 3e3;
69
72
  let root = has("src") ? "src" : "";
70
73
  let nextPagesOnly = false;
71
74
  if (framework === "next") {
72
75
  root = has("src/app") ? "src" : has("app") ? "" : has("src/pages") ? "src" : "";
73
76
  nextPagesOnly = !has("src/app") && !has("app");
74
77
  }
75
- if (framework === "tanstack-start" || framework === "sveltekit") root = "src";
78
+ if (framework === "tanstack-start" || framework === "sveltekit" || framework === "vite-react") root = "src";
79
+ if (framework === "react-router") root = "app";
76
80
  const major = /(\d+)/.exec(deps.next ?? "");
81
+ const needs = ["cauthy-auth", ...framework === "vite-react" ? ["hono", "@hono/node-server"] : []];
77
82
  return {
78
83
  framework,
79
84
  ...nextPagesOnly ? { nextPagesOnly } : {},
80
85
  root,
86
+ ...framework === "react-router" && remix ? { remix } : {},
87
+ ...framework === "react-router" ? { fileRoutes: remix || Boolean(deps["@react-router/fs-routes"]) } : {},
81
88
  typescript: has("tsconfig.json") || Boolean(deps.typescript),
82
89
  esm: pkg.type === "module",
83
90
  ...framework === "next" && major ? { nextMajor: Number(major[1]) } : {},
84
91
  devUrl: `http://localhost:${port}`,
85
92
  system: systemName(pkg.name),
86
93
  installed: Boolean(deps["cauthy-auth"]),
94
+ toInstall: needs.filter((name) => !deps[name]),
87
95
  packageManager: has("pnpm-lock.yaml") ? "pnpm" : has("yarn.lock") ? "yarn" : has("bun.lockb") || has("bun.lock") ? "bun" : "npm"
88
96
  };
89
97
  }
@@ -100,9 +108,9 @@ function importPath(from, to) {
100
108
  function scaffold(d) {
101
109
  const ext = d.typescript ? "ts" : "js";
102
110
  const jsx = d.typescript ? "tsx" : "jsx";
103
- const install = `${d.packageManager === "npm" ? "npm install" : `${d.packageManager} add`} cauthy-auth`;
111
+ const install = `${d.packageManager === "npm" ? "npm install" : `${d.packageManager} add`} ${d.toInstall.join(" ")}`;
104
112
  const dev = `${d.packageManager === "npm" ? "npm run" : d.packageManager} dev`;
105
- const steps = d.installed ? [] : [`Install the package: ${install}`];
113
+ const steps = d.toInstall.length === 0 ? [] : [`Install the ${d.toInstall.length === 1 ? "package" : "packages"}: ${install}`];
106
114
  const files = [];
107
115
  const signIn = `${d.devUrl}/sign-in`;
108
116
  const hosted = `${d.devUrl}/api/auth/login`;
@@ -159,6 +167,49 @@ export const Route = createFileRoute('/sign-in')({ component: SignInPage })
159
167
  steps.push(`Start the application: ${dev}`, `Open ${signIn}`);
160
168
  return { files, steps, envFile: ".env", open: signIn };
161
169
  }
170
+ case "react-router": {
171
+ const authFile = join(d.root, `auth.server.${ext}`);
172
+ files.push({ path: authFile, content: PLAIN_AUTH });
173
+ const route = join(d.root, "routes", `api.auth.$.${ext}`);
174
+ const param = d.typescript ? "{ request }: { request: Request }" : "{ request }";
175
+ files.push({ path: route, content: `// Serves cauthy-auth: sign-in, return, session, sign-out.
176
+ import { auth } from '${importPath(route, authFile)}'
177
+
178
+ export const loader = (${param}) => auth.handler(request)
179
+ export const action = (${param}) => auth.handler(request)
180
+ ` });
181
+ const page = join(d.root, "routes", `sign-in.${jsx}`);
182
+ files.push({ path: page, content: reactSignInPage("", "export default function SignInPage") });
183
+ if (!d.fileRoutes) {
184
+ const rel = (p) => import_node_path.posix.relative(d.root, p);
185
+ steps.push(`Add the two routes to ${join(d.root, `routes.${ext}`)}:
186
+
187
+ route('api/auth/*', '${rel(route)}'),
188
+ route('sign-in', '${rel(page)}'),`);
189
+ }
190
+ steps.push(`Start the application: ${dev}`, `Open ${signIn}`);
191
+ return { files, steps, envFile: ".env", open: signIn };
192
+ }
193
+ case "vite-react": {
194
+ const app = join("server", `app.${ext}`);
195
+ files.push({ path: join("server", `auth.${ext}`), content: PLAIN_AUTH });
196
+ files.push({ path: app, content: viteApp(d.typescript) });
197
+ files.push({ path: join("server", `index.${ext}`), content: VITE_SERVER });
198
+ files.push({ path: join("server", `vite.${ext}`), content: viteDev(d.typescript, `/${app}`) });
199
+ const page = join(d.root, `SignInPage.${jsx}`);
200
+ files.push({ path: page, content: reactSignInPage("", "export function SignInPage") });
201
+ const run = d.typescript ? "npx tsx server/index.ts" : "node server/index.js";
202
+ steps.push(
203
+ `Add the plugin that serves /api while developing to vite.config.${ext}:
204
+
205
+ import { cauthyDev } from './server/vite'
206
+ plugins: [react(), cauthyDev()]`,
207
+ `Start the application: ${dev}`,
208
+ `Open ${hosted}: Cauthy's sign-in page, and back. For a screen of your own, show <SignInPage /> from ${page} at /sign-in.`,
209
+ `In production, build the pages (vite build) and run ${run}: it serves them and /api.`
210
+ );
211
+ return { files, steps, envFile: ".env", open: hosted };
212
+ }
162
213
  case "sveltekit": {
163
214
  const authFile = join(d.root, "lib/server", `auth.${ext}`);
164
215
  files.push({ path: authFile, content: PLAIN_AUTH });
@@ -231,6 +282,62 @@ const { createCauthy } = require('cauthy-auth')
231
282
 
232
283
  exports.auth = createCauthy()
233
284
  `;
285
+ function viteApp(ts) {
286
+ return `// The application's server side: cauthy-auth's routes under /api/auth, and
287
+ // c.get('session') on every other /api request. While developing it runs
288
+ // inside Vite (server/vite); in production server/index serves it with the
289
+ // built pages.
290
+ import { Hono } from 'hono'
291
+ import { cauthyHono, requireSession${ts ? ", type CauthyVariables" : ""} } from 'cauthy-auth/hono'
292
+ import { auth } from './auth.js'
293
+
294
+ export const app = new Hono${ts ? "<{ Variables: CauthyVariables }>" : ""}()
295
+ app.use('/api/*', cauthyHono(auth))
296
+
297
+ // An API only a signed-in person may call: a 401 for anybody else.
298
+ app.get('/api/me', requireSession, (c) => c.json(c.get('session')${ts ? "!" : ""}.user))
299
+ `;
300
+ }
301
+ var VITE_SERVER = `// The application in production: the pages Vite built, and the API.
302
+ import { serve } from '@hono/node-server'
303
+ import { serveStatic } from '@hono/node-server/serve-static'
304
+ import { app } from './app.js'
305
+
306
+ app.use('/*', serveStatic({ root: './dist' }))
307
+ // Any other path is a page of the single-page app.
308
+ app.get('*', serveStatic({ path: './dist/index.html' }))
309
+
310
+ const port = Number(process.env.PORT ?? 3000)
311
+ serve({ fetch: app.fetch, port }, () => console.log(\`http://localhost:\${port}\`))
312
+ `;
313
+ function viteDev(ts, app) {
314
+ return `// While developing, Vite serves the pages and hands /api to the server's
315
+ // app, on the same address: no proxy, and Cauthy sends people back to the
316
+ // address the browser is on.
317
+ //
318
+ // import { cauthyDev } from './server/vite'
319
+ // plugins: [react(), cauthyDev()]
320
+ import { getRequestListener } from '@hono/node-server'
321
+ import { loadEnv${ts ? ", type Plugin" : ""} } from 'vite'
322
+
323
+ export function cauthyDev()${ts ? ": Plugin" : ""} {
324
+ return {
325
+ name: 'cauthy-dev',
326
+ configureServer(server) {
327
+ // The variables in the env file, which createCauthy() reads from the environment.
328
+ Object.assign(process.env, loadEnv(server.config.mode, server.config.envDir || server.config.root, 'CAUTHY_'))
329
+ server.middlewares.use((req, res, next) => {
330
+ if (!req.url?.startsWith('/api/')) return next()
331
+ // Loaded through Vite: a change to the server's files applies on the next request.
332
+ server.ssrLoadModule('${app}')
333
+ .then(({ app }) => getRequestListener(app.fetch)(req, res))
334
+ .catch(next)
335
+ })
336
+ },
337
+ }
338
+ }
339
+ `;
340
+ }
234
341
  function reactSignInPage(head, declaration, tail = "") {
235
342
  return `${head}// The application's own sign-in page. <SignIn /> is the ready-made form: the
236
343
  // workspace's providers, passkeys, the second step and a sign-in link, drawn
@@ -912,9 +1019,10 @@ var INIT_HELP = `cauthy-auth init [options]
912
1019
 
913
1020
  Connects an application to Cauthy and leaves signing in working. Run it in
914
1021
  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.
1022
+ with (Next.js, TanStack Start, React Router or Remix, SvelteKit, Vite + React,
1023
+ Express, Hono, Fastify), makes its client in Cauthy, writes the variables to
1024
+ its env file, and lays down the route and a sign-in page. A file that is
1025
+ already there is never written over.
918
1026
 
919
1027
  --system <name> The system's name: lowercase letters, digits, - or _.
920
1028
  Default: the package's name.
@@ -976,7 +1084,7 @@ ${INIT_HELP}`);
976
1084
  const system = given["--system"] ?? found?.system;
977
1085
  const appUrl = given["--app-url"] ?? found?.devUrl;
978
1086
  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]}.
1087
+ if (found?.framework) process.stdout.write(`Found ${found.remix ? "Remix" : FRAMEWORK_NAMES[found.framework]}.
980
1088
  `);
981
1089
  const target = given["--write"] ?? plan?.envFile;
982
1090
  let had = "";
package/dist/client.d.mts CHANGED
@@ -319,10 +319,19 @@ interface AuthClient {
319
319
  sentTo: string;
320
320
  channel: string;
321
321
  }>>;
322
- /** Mails a link that signs the person in. The answer is the same whether or not the address has an account. */
322
+ /**
323
+ * Mails a link (and a code) that signs the person in. The answer is the
324
+ * same whether or not the address has an account. `create`: an address
325
+ * with no account gets one when the link or code is used, if the
326
+ * workspace accepts new accounts — one screen signs people in and up.
327
+ * `returnTo` (a path of this site) travels in the link: the page it opens
328
+ * gets it as `?return_to=`, and `emailCode` answers it.
329
+ */
323
330
  magicLink(form: {
324
331
  email: string;
325
332
  captchaToken?: string;
333
+ create?: boolean;
334
+ returnTo?: string;
326
335
  }): Promise<AuthResult<'sent'>>;
327
336
  /** Signs in with the token the mailed link opened the page with (`ml_token`). */
328
337
  magicLinkVerify(form: {
@@ -335,6 +344,7 @@ interface AuthClient {
335
344
  code: string;
336
345
  }): Promise<AuthResult<'signed_in', {
337
346
  session: PublicSession;
347
+ returnTo?: string;
338
348
  }>>;
339
349
  /**
340
350
  * Signs in with a passkey, on this page: the browser asks the person for
package/dist/client.d.ts CHANGED
@@ -319,10 +319,19 @@ interface AuthClient {
319
319
  sentTo: string;
320
320
  channel: string;
321
321
  }>>;
322
- /** Mails a link that signs the person in. The answer is the same whether or not the address has an account. */
322
+ /**
323
+ * Mails a link (and a code) that signs the person in. The answer is the
324
+ * same whether or not the address has an account. `create`: an address
325
+ * with no account gets one when the link or code is used, if the
326
+ * workspace accepts new accounts — one screen signs people in and up.
327
+ * `returnTo` (a path of this site) travels in the link: the page it opens
328
+ * gets it as `?return_to=`, and `emailCode` answers it.
329
+ */
323
330
  magicLink(form: {
324
331
  email: string;
325
332
  captchaToken?: string;
333
+ create?: boolean;
334
+ returnTo?: string;
326
335
  }): Promise<AuthResult<'sent'>>;
327
336
  /** Signs in with the token the mailed link opened the page with (`ml_token`). */
328
337
  magicLinkVerify(form: {
@@ -335,6 +344,7 @@ interface AuthClient {
335
344
  code: string;
336
345
  }): Promise<AuthResult<'signed_in', {
337
346
  session: PublicSession;
347
+ returnTo?: string;
338
348
  }>>;
339
349
  /**
340
350
  * Signs in with a passkey, on this page: the browser asks the person for
package/dist/client.js CHANGED
@@ -379,7 +379,7 @@ function createAuthClient(options = {}) {
379
379
  mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
380
380
  mfaMethods: () => post("/sign-in/mfa/methods", {}, null),
381
381
  mfaSendCode: () => post("/sign-in/mfa/send", {}, null),
382
- magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email), f.captchaToken),
382
+ magicLink: (f) => post("/sign-in/magic-link", { email: f.email, ...f.create ? { create: true } : {}, ...f.returnTo ? { returnTo: f.returnTo } : {} }, checkEmail(f.email), f.captchaToken),
383
383
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
384
384
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
385
385
  passkeyUrl,