cauthy-auth 0.12.0 → 0.14.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
@@ -123,6 +123,14 @@ const who = await verifyToken(request) // throws CauthyTokenError: answer 401
123
123
  who.userId, who.tenantId, who.roles
124
124
  ```
125
125
 
126
+ An MCP server (or an API) that AI agents use as one of your people: Claude, Cursor and others find Cauthy from it, the person signs in and allows the agent, and the token works on that server alone.
127
+
128
+ ```ts
129
+ import { mcpAuth } from 'cauthy-auth/mcp'
130
+ const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:read'] })
131
+ app.use(auth.middleware()) // or auth.protect(handler) for Request/Response
132
+ ```
133
+
126
134
  The full guide is in Cauthy's documentation, under "Auth SDK".
127
135
 
128
136
  ## License
package/dist/client.d.mts CHANGED
@@ -93,6 +93,13 @@ interface AuthMethods {
93
93
  * page that cannot, or a browser that refuses to here, sends the person.
94
94
  */
95
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;
96
103
  }
97
104
  /** A passkey of the signed-in account. Moments are UTC. */
98
105
  interface Passkey {
@@ -114,6 +121,26 @@ interface Account {
114
121
  mfaEnabled: boolean;
115
122
  }
116
123
  /** Somewhere the person is signed in. Moments are UTC. */
124
+ /** An AI agent the person allowed to act for them on one of the workspace's servers. */
125
+ interface ConnectedAgent {
126
+ id: string;
127
+ /** The name the agent gave itself (Claude, Cursor…): not checked by anyone. */
128
+ name: string;
129
+ /** Where the agent receives its access: the one thing about it that is sure. */
130
+ redirectHost: string;
131
+ resource: {
132
+ name: string;
133
+ uri: string;
134
+ };
135
+ /** What the person allowed it, in their words. */
136
+ scopes: {
137
+ name: string;
138
+ description: string;
139
+ }[];
140
+ /** UTC, ISO 8601. */
141
+ connectedAt: string;
142
+ lastUsedAt: string;
143
+ }
117
144
  interface AccountSession {
118
145
  /** "Mac · Chrome", or "" when the browser did not say. */
119
146
  device: string;
@@ -134,6 +161,17 @@ interface LinkedIdentity {
134
161
  /** When it was linked, in UTC (ISO 8601). */
135
162
  linkedAt: string;
136
163
  }
164
+ /** How an account takes its second step. */
165
+ interface SecondStep {
166
+ totp: boolean;
167
+ /** The phone codes are sent to, masked (`+57 ••• ••• 4567`). */
168
+ phone: {
169
+ number: string;
170
+ channel: string;
171
+ } | null;
172
+ /** The ways the workspace sends codes to phones. */
173
+ phoneChannels: string[];
174
+ }
137
175
  interface AccountClient {
138
176
  /** The account, or null when nobody is signed in. */
139
177
  get(): Promise<Account | null>;
@@ -172,6 +210,30 @@ interface AccountClient {
172
210
  }): Promise<AuthResult<'renewed', {
173
211
  recoveryCodes: string[];
174
212
  }>>;
213
+ /**
214
+ * How the account takes its second step: an authenticator app, a phone
215
+ * (masked), and the ways the workspace sends codes to phones (`sms`,
216
+ * `whatsapp`; none when it has no Twilio account for it).
217
+ */
218
+ factors(): Promise<SecondStep | null>;
219
+ /** Adds a phone as the second step: a code is sent to it, for `verifyPhone`. */
220
+ addPhone(form: {
221
+ phone: string;
222
+ channel: 'sms' | 'whatsapp';
223
+ password: string;
224
+ }): Promise<AuthResult<'sent', {
225
+ sentTo: string;
226
+ channel: string;
227
+ }>>;
228
+ /** Proves the phone with the code it received. `recoveryCodes` come with a first second step, shown this once. */
229
+ verifyPhone(form: {
230
+ code: string;
231
+ }): Promise<AuthResult<'enabled', {
232
+ recoveryCodes: string[];
233
+ }>>;
234
+ removePhone(form: {
235
+ password: string;
236
+ }): Promise<AuthResult<'removed'>>;
175
237
  };
176
238
  /**
177
239
  * Asks to sign in with another address. Nothing changes yet: the new address
@@ -205,6 +267,12 @@ interface AccountClient {
205
267
  sessions(): Promise<AccountSession[]>;
206
268
  /** Signs the account out everywhere, here too. */
207
269
  signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
270
+ /** The AI agents the person connected. */
271
+ agents(): Promise<ConnectedAgent[]>;
272
+ /** Disconnects an agent: it loses its access within minutes. */
273
+ disconnectAgent(id: string): Promise<AuthResult<'disconnected', {
274
+ id: string;
275
+ }>>;
208
276
  /** The account's passkeys. */
209
277
  passkeys(): Promise<Passkey[]>;
210
278
  /**
@@ -228,6 +296,7 @@ interface AuthClient {
228
296
  email(form: {
229
297
  email: string;
230
298
  password: string;
299
+ captchaToken?: string;
231
300
  }): Promise<AuthResult<'signed_in', {
232
301
  session: PublicSession;
233
302
  }> | AuthResult<'mfa_required'>>;
@@ -237,9 +306,23 @@ interface AuthClient {
237
306
  }): Promise<AuthResult<'signed_in', {
238
307
  session: PublicSession;
239
308
  }>>;
309
+ /** While the second step is asked for: whether the account has an authenticator app, and the phone (masked) a code can be sent to. */
310
+ mfaMethods(): Promise<AuthResult<'ok', {
311
+ totp: boolean;
312
+ phone: {
313
+ number: string;
314
+ channel: string;
315
+ } | null;
316
+ }>>;
317
+ /** Sends a code to the account's phone, by SMS or WhatsApp; `mfa` takes it. A few per sign-in. */
318
+ mfaSendCode(): Promise<AuthResult<'sent', {
319
+ sentTo: string;
320
+ channel: string;
321
+ }>>;
240
322
  /** Mails a link that signs the person in. The answer is the same whether or not the address has an account. */
241
323
  magicLink(form: {
242
324
  email: string;
325
+ captchaToken?: string;
243
326
  }): Promise<AuthResult<'sent'>>;
244
327
  /** Signs in with the token the mailed link opened the page with (`ml_token`). */
245
328
  magicLinkVerify(form: {
@@ -315,6 +398,7 @@ interface AuthClient {
315
398
  email: string;
316
399
  password: string;
317
400
  name?: string;
401
+ captchaToken?: string;
318
402
  }): Promise<AuthResult<'signed_in', {
319
403
  session: PublicSession;
320
404
  }> | AuthResult<'verification_required', {
@@ -324,6 +408,7 @@ interface AuthClient {
324
408
  /** Mails a link to choose a new password. The answer is the same whether or not the address has an account. */
325
409
  forgotPassword(form: {
326
410
  email: string;
411
+ captchaToken?: string;
327
412
  }): Promise<AuthResult<'sent'>>;
328
413
  /** Sets the new password, with the token the mailed link opened the page with (`token`). */
329
414
  resetPassword(form: {
@@ -360,4 +445,4 @@ interface AuthClientOptions {
360
445
  }
361
446
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
362
447
 
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 };
448
+ export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type ConnectedAgent, type LinkedIdentity, type Passkey, type SecondStep, type SessionState, type SessionStore, type SessionStoreOptions, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };
package/dist/client.d.ts CHANGED
@@ -93,6 +93,13 @@ interface AuthMethods {
93
93
  * page that cannot, or a browser that refuses to here, sends the person.
94
94
  */
95
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;
96
103
  }
97
104
  /** A passkey of the signed-in account. Moments are UTC. */
98
105
  interface Passkey {
@@ -114,6 +121,26 @@ interface Account {
114
121
  mfaEnabled: boolean;
115
122
  }
116
123
  /** Somewhere the person is signed in. Moments are UTC. */
124
+ /** An AI agent the person allowed to act for them on one of the workspace's servers. */
125
+ interface ConnectedAgent {
126
+ id: string;
127
+ /** The name the agent gave itself (Claude, Cursor…): not checked by anyone. */
128
+ name: string;
129
+ /** Where the agent receives its access: the one thing about it that is sure. */
130
+ redirectHost: string;
131
+ resource: {
132
+ name: string;
133
+ uri: string;
134
+ };
135
+ /** What the person allowed it, in their words. */
136
+ scopes: {
137
+ name: string;
138
+ description: string;
139
+ }[];
140
+ /** UTC, ISO 8601. */
141
+ connectedAt: string;
142
+ lastUsedAt: string;
143
+ }
117
144
  interface AccountSession {
118
145
  /** "Mac · Chrome", or "" when the browser did not say. */
119
146
  device: string;
@@ -134,6 +161,17 @@ interface LinkedIdentity {
134
161
  /** When it was linked, in UTC (ISO 8601). */
135
162
  linkedAt: string;
136
163
  }
164
+ /** How an account takes its second step. */
165
+ interface SecondStep {
166
+ totp: boolean;
167
+ /** The phone codes are sent to, masked (`+57 ••• ••• 4567`). */
168
+ phone: {
169
+ number: string;
170
+ channel: string;
171
+ } | null;
172
+ /** The ways the workspace sends codes to phones. */
173
+ phoneChannels: string[];
174
+ }
137
175
  interface AccountClient {
138
176
  /** The account, or null when nobody is signed in. */
139
177
  get(): Promise<Account | null>;
@@ -172,6 +210,30 @@ interface AccountClient {
172
210
  }): Promise<AuthResult<'renewed', {
173
211
  recoveryCodes: string[];
174
212
  }>>;
213
+ /**
214
+ * How the account takes its second step: an authenticator app, a phone
215
+ * (masked), and the ways the workspace sends codes to phones (`sms`,
216
+ * `whatsapp`; none when it has no Twilio account for it).
217
+ */
218
+ factors(): Promise<SecondStep | null>;
219
+ /** Adds a phone as the second step: a code is sent to it, for `verifyPhone`. */
220
+ addPhone(form: {
221
+ phone: string;
222
+ channel: 'sms' | 'whatsapp';
223
+ password: string;
224
+ }): Promise<AuthResult<'sent', {
225
+ sentTo: string;
226
+ channel: string;
227
+ }>>;
228
+ /** Proves the phone with the code it received. `recoveryCodes` come with a first second step, shown this once. */
229
+ verifyPhone(form: {
230
+ code: string;
231
+ }): Promise<AuthResult<'enabled', {
232
+ recoveryCodes: string[];
233
+ }>>;
234
+ removePhone(form: {
235
+ password: string;
236
+ }): Promise<AuthResult<'removed'>>;
175
237
  };
176
238
  /**
177
239
  * Asks to sign in with another address. Nothing changes yet: the new address
@@ -205,6 +267,12 @@ interface AccountClient {
205
267
  sessions(): Promise<AccountSession[]>;
206
268
  /** Signs the account out everywhere, here too. */
207
269
  signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
270
+ /** The AI agents the person connected. */
271
+ agents(): Promise<ConnectedAgent[]>;
272
+ /** Disconnects an agent: it loses its access within minutes. */
273
+ disconnectAgent(id: string): Promise<AuthResult<'disconnected', {
274
+ id: string;
275
+ }>>;
208
276
  /** The account's passkeys. */
209
277
  passkeys(): Promise<Passkey[]>;
210
278
  /**
@@ -228,6 +296,7 @@ interface AuthClient {
228
296
  email(form: {
229
297
  email: string;
230
298
  password: string;
299
+ captchaToken?: string;
231
300
  }): Promise<AuthResult<'signed_in', {
232
301
  session: PublicSession;
233
302
  }> | AuthResult<'mfa_required'>>;
@@ -237,9 +306,23 @@ interface AuthClient {
237
306
  }): Promise<AuthResult<'signed_in', {
238
307
  session: PublicSession;
239
308
  }>>;
309
+ /** While the second step is asked for: whether the account has an authenticator app, and the phone (masked) a code can be sent to. */
310
+ mfaMethods(): Promise<AuthResult<'ok', {
311
+ totp: boolean;
312
+ phone: {
313
+ number: string;
314
+ channel: string;
315
+ } | null;
316
+ }>>;
317
+ /** Sends a code to the account's phone, by SMS or WhatsApp; `mfa` takes it. A few per sign-in. */
318
+ mfaSendCode(): Promise<AuthResult<'sent', {
319
+ sentTo: string;
320
+ channel: string;
321
+ }>>;
240
322
  /** Mails a link that signs the person in. The answer is the same whether or not the address has an account. */
241
323
  magicLink(form: {
242
324
  email: string;
325
+ captchaToken?: string;
243
326
  }): Promise<AuthResult<'sent'>>;
244
327
  /** Signs in with the token the mailed link opened the page with (`ml_token`). */
245
328
  magicLinkVerify(form: {
@@ -315,6 +398,7 @@ interface AuthClient {
315
398
  email: string;
316
399
  password: string;
317
400
  name?: string;
401
+ captchaToken?: string;
318
402
  }): Promise<AuthResult<'signed_in', {
319
403
  session: PublicSession;
320
404
  }> | AuthResult<'verification_required', {
@@ -324,6 +408,7 @@ interface AuthClient {
324
408
  /** Mails a link to choose a new password. The answer is the same whether or not the address has an account. */
325
409
  forgotPassword(form: {
326
410
  email: string;
411
+ captchaToken?: string;
327
412
  }): Promise<AuthResult<'sent'>>;
328
413
  /** Sets the new password, with the token the mailed link opened the page with (`token`). */
329
414
  resetPassword(form: {
@@ -360,4 +445,4 @@ interface AuthClientOptions {
360
445
  }
361
446
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
362
447
 
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 };
448
+ export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type ConnectedAgent, type LinkedIdentity, type Passkey, type SecondStep, type SessionState, type SessionStore, type SessionStoreOptions, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };
package/dist/client.js CHANGED
@@ -317,13 +317,13 @@ function createAuthClient(options = {}) {
317
317
  const said = typeof options.locale === "function" ? options.locale() : options.locale;
318
318
  return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
319
319
  };
320
- async function post(path, form, bad) {
320
+ async function post(path, form, bad, captchaToken) {
321
321
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
322
322
  try {
323
323
  const res = await doFetch(`${base}${path}`, {
324
324
  method: "POST",
325
325
  credentials: "same-origin",
326
- headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
326
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {}, ...captchaToken ? { "x-cauthy-captcha": captchaToken } : {} },
327
327
  body: JSON.stringify(form)
328
328
  });
329
329
  const body = await res.json().catch(() => null);
@@ -375,9 +375,11 @@ function createAuthClient(options = {}) {
375
375
  }
376
376
  return {
377
377
  signIn: {
378
- email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
378
+ email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password)), f.captchaToken),
379
379
  mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
380
- magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email)),
380
+ mfaMethods: () => post("/sign-in/mfa/methods", {}, null),
381
+ mfaSendCode: () => post("/sign-in/mfa/send", {}, null),
382
+ magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email), f.captchaToken),
381
383
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
382
384
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
383
385
  passkeyUrl,
@@ -458,9 +460,9 @@ function createAuthClient(options = {}) {
458
460
  }
459
461
  },
460
462
  signUp: {
461
- email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)))
463
+ email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)), f.captchaToken)
462
464
  },
463
- forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email)),
465
+ forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email), f.captchaToken),
464
466
  resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
465
467
  verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
466
468
  signOut: () => post("/sign-out", {}, null),
@@ -478,7 +480,14 @@ function createAuthClient(options = {}) {
478
480
  setup: (f) => post("/me/mfa/setup", { password: f.password }, null),
479
481
  verify: (f) => post("/me/mfa/verify", { code: f.code }, checkCode(f.code)),
480
482
  disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
481
- recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
483
+ recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null),
484
+ async factors() {
485
+ const body = await read("/me/mfa/factors");
486
+ return body && typeof body.totp === "boolean" ? body : null;
487
+ },
488
+ addPhone: (f) => post("/me/mfa/phone", { phone: f.phone, channel: f.channel, password: f.password }, null),
489
+ verifyPhone: (f) => post("/me/mfa/phone/verify", { code: f.code }, checkCode(f.code)),
490
+ removePhone: (f) => post("/me/mfa/phone/remove", { password: f.password }, null)
482
491
  },
483
492
  changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
484
493
  async identities() {
@@ -495,6 +504,11 @@ function createAuthClient(options = {}) {
495
504
  return body && Array.isArray(body.sessions) ? body.sessions : [];
496
505
  },
497
506
  signOutEverywhere: () => post("/me/sessions/end", {}, null),
507
+ async agents() {
508
+ const body = await read("/me/agents");
509
+ return body && Array.isArray(body.agents) ? body.agents : [];
510
+ },
511
+ disconnectAgent: (id) => post("/me/agents/disconnect", { id }, null),
498
512
  async passkeys() {
499
513
  const body = await read("/me/passkeys");
500
514
  return body && Array.isArray(body.passkeys) ? body.passkeys : [];