cauthy-auth 0.8.0 → 0.9.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/dist/react.d.mts CHANGED
@@ -260,6 +260,15 @@ interface AccountSession {
260
260
  * The signed-in person's own account. What weakens it — the password, the
261
261
  * second step, its recovery codes — asks for the current password again.
262
262
  */
263
+ /** A provider the account can also sign in with. */
264
+ interface LinkedIdentity {
265
+ provider: string;
266
+ /** The address the provider has for the person, when it said. */
267
+ email: string;
268
+ name: string;
269
+ /** When it was linked, in UTC (ISO 8601). */
270
+ linkedAt: string;
271
+ }
263
272
  interface AccountClient {
264
273
  /** The account, or null when nobody is signed in. */
265
274
  get(): Promise<Account$1 | null>;
@@ -299,6 +308,34 @@ interface AccountClient {
299
308
  recoveryCodes: string[];
300
309
  }>>;
301
310
  };
311
+ /**
312
+ * Asks to sign in with another address. Nothing changes yet: the new address
313
+ * is mailed a link to confirm it, and once it is confirmed the old one is
314
+ * told, with a link that undoes it. Both open Cauthy's own page.
315
+ */
316
+ changeEmail(form: {
317
+ email: string;
318
+ password: string;
319
+ }): Promise<AuthResult<'sent', {
320
+ email: string;
321
+ }>>;
322
+ /** The providers (Google…) the account can also sign in with. */
323
+ identities(): Promise<LinkedIdentity[]>;
324
+ /**
325
+ * Sends the browser to the provider, to link it to the account; it comes back
326
+ * at `returnTo` with `?linked=<provider>`, or `?auth_error=<code>`.
327
+ */
328
+ link(provider: string, options?: {
329
+ returnTo?: string;
330
+ }): void;
331
+ /** Where `link` sends the browser, for a plain link. */
332
+ linkUrl(provider: string, options?: {
333
+ returnTo?: string;
334
+ }): string;
335
+ /** The account stops signing in with that provider. Refused when it would be left with no way to sign in. */
336
+ unlink(provider: string): Promise<AuthResult<'unlinked', {
337
+ provider: string;
338
+ }>>;
302
339
  /** Where the account is signed in. */
303
340
  sessions(): Promise<AccountSession[]>;
304
341
  /** Signs the account out everywhere, here too. */
@@ -448,6 +485,15 @@ interface AuthTexts {
448
485
  sessions: string;
449
486
  signOutEverywhere: string;
450
487
  unknownDevice: string;
488
+ emailAddress: string;
489
+ newEmail: string;
490
+ changeEmail: string;
491
+ emailSent: string;
492
+ linkedAccounts: string;
493
+ noLinkedAccounts: string;
494
+ linkWith: string;
495
+ unlink: string;
496
+ linked: string;
451
497
  /** By error code; one missing shows what the server said. */
452
498
  errors: Record<string, string>;
453
499
  }
@@ -576,4 +622,4 @@ declare function useSession(): SessionContext;
576
622
  */
577
623
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
578
624
 
579
- export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, type JsonValue, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, createAuthClient, definePolicy, useAuth, useSession };
625
+ export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, type JsonValue, type LinkedIdentity, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, createAuthClient, definePolicy, useAuth, useSession };
package/dist/react.d.ts CHANGED
@@ -260,6 +260,15 @@ interface AccountSession {
260
260
  * The signed-in person's own account. What weakens it — the password, the
261
261
  * second step, its recovery codes — asks for the current password again.
262
262
  */
263
+ /** A provider the account can also sign in with. */
264
+ interface LinkedIdentity {
265
+ provider: string;
266
+ /** The address the provider has for the person, when it said. */
267
+ email: string;
268
+ name: string;
269
+ /** When it was linked, in UTC (ISO 8601). */
270
+ linkedAt: string;
271
+ }
263
272
  interface AccountClient {
264
273
  /** The account, or null when nobody is signed in. */
265
274
  get(): Promise<Account$1 | null>;
@@ -299,6 +308,34 @@ interface AccountClient {
299
308
  recoveryCodes: string[];
300
309
  }>>;
301
310
  };
311
+ /**
312
+ * Asks to sign in with another address. Nothing changes yet: the new address
313
+ * is mailed a link to confirm it, and once it is confirmed the old one is
314
+ * told, with a link that undoes it. Both open Cauthy's own page.
315
+ */
316
+ changeEmail(form: {
317
+ email: string;
318
+ password: string;
319
+ }): Promise<AuthResult<'sent', {
320
+ email: string;
321
+ }>>;
322
+ /** The providers (Google…) the account can also sign in with. */
323
+ identities(): Promise<LinkedIdentity[]>;
324
+ /**
325
+ * Sends the browser to the provider, to link it to the account; it comes back
326
+ * at `returnTo` with `?linked=<provider>`, or `?auth_error=<code>`.
327
+ */
328
+ link(provider: string, options?: {
329
+ returnTo?: string;
330
+ }): void;
331
+ /** Where `link` sends the browser, for a plain link. */
332
+ linkUrl(provider: string, options?: {
333
+ returnTo?: string;
334
+ }): string;
335
+ /** The account stops signing in with that provider. Refused when it would be left with no way to sign in. */
336
+ unlink(provider: string): Promise<AuthResult<'unlinked', {
337
+ provider: string;
338
+ }>>;
302
339
  /** Where the account is signed in. */
303
340
  sessions(): Promise<AccountSession[]>;
304
341
  /** Signs the account out everywhere, here too. */
@@ -448,6 +485,15 @@ interface AuthTexts {
448
485
  sessions: string;
449
486
  signOutEverywhere: string;
450
487
  unknownDevice: string;
488
+ emailAddress: string;
489
+ newEmail: string;
490
+ changeEmail: string;
491
+ emailSent: string;
492
+ linkedAccounts: string;
493
+ noLinkedAccounts: string;
494
+ linkWith: string;
495
+ unlink: string;
496
+ linked: string;
451
497
  /** By error code; one missing shows what the server said. */
452
498
  errors: Record<string, string>;
453
499
  }
@@ -576,4 +622,4 @@ declare function useSession(): SessionContext;
576
622
  */
577
623
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
578
624
 
579
- export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, type JsonValue, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, createAuthClient, definePolicy, useAuth, useSession };
625
+ export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, type JsonValue, type LinkedIdentity, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, createAuthClient, definePolicy, useAuth, useSession };
package/dist/react.js CHANGED
@@ -165,6 +165,7 @@ function createAuthClient(options = {}) {
165
165
  return null;
166
166
  }
167
167
  }
168
+ const linkUrl = (provider, o = {}) => `${base}/me/identities/link/${encodeURIComponent(provider)}${o.returnTo ? `?return_to=${encodeURIComponent(o.returnTo)}` : ""}`;
168
169
  const socialUrl = (provider, o = {}) => `${base}/sign-in/social/${encodeURIComponent(provider)}${o.returnTo ? `?return_to=${encodeURIComponent(o.returnTo)}` : ""}`;
169
170
  const passkeyUrl = (o = {}) => {
170
171
  const q = new URLSearchParams({ method: "passkey" });
@@ -211,6 +212,16 @@ function createAuthClient(options = {}) {
211
212
  disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
212
213
  recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
213
214
  },
215
+ changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
216
+ async identities() {
217
+ const body = await read("/me/identities");
218
+ return body && Array.isArray(body.identities) ? body.identities : [];
219
+ },
220
+ linkUrl,
221
+ link(provider, o) {
222
+ window.location.assign(linkUrl(provider, o));
223
+ },
224
+ unlink: (provider) => post("/me/identities/unlink", { provider }, null),
214
225
  async sessions() {
215
226
  const body = await read("/me/sessions");
216
227
  return body && Array.isArray(body.sessions) ? body.sessions : [];
@@ -347,6 +358,15 @@ var TEXTS = {
347
358
  sessions: "Where you are signed in",
348
359
  signOutEverywhere: "Sign out everywhere",
349
360
  unknownDevice: "Unknown device",
361
+ emailAddress: "Email address",
362
+ newEmail: "New email address",
363
+ changeEmail: "Change email",
364
+ emailSent: "We sent a link to {email}. Your address changes once you open it.",
365
+ linkedAccounts: "Linked accounts",
366
+ noLinkedAccounts: "No account is linked.",
367
+ linkWith: "Link",
368
+ unlink: "Unlink",
369
+ linked: "The account was linked.",
350
370
  errors: {
351
371
  required: "This is required.",
352
372
  invalid_email: "That does not look like an email address.",
@@ -355,6 +375,11 @@ var TEXTS = {
355
375
  unavailable: "Sign-in is unavailable right now. Try again in a moment.",
356
376
  state_mismatch: "That sign-in could not be completed. Try again.",
357
377
  cancelled: "The sign-in was cancelled.",
378
+ email_taken: "That address already has an account.",
379
+ email_unchanged: "That is already your address.",
380
+ last_sign_in_method: "Add a password or link another account first: it is your only way to sign in.",
381
+ identity_already_linked: "That account is linked to someone else.",
382
+ manual_linking_disabled: "Linking accounts is turned off for this workspace.",
358
383
  invalid_token: "This link is not valid, or it has expired. Ask for a new one.",
359
384
  invalid_code: "That code is not right, or it has expired.",
360
385
  wrong_password: "The current password is not right.",
@@ -412,6 +437,15 @@ var TEXTS = {
412
437
  sessions: "D\xF3nde tienes la sesi\xF3n abierta",
413
438
  signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
414
439
  unknownDevice: "Dispositivo desconocido",
440
+ emailAddress: "Correo",
441
+ newEmail: "Correo nuevo",
442
+ changeEmail: "Cambiar el correo",
443
+ emailSent: "Enviamos un enlace a {email}. Tu correo cambia cuando lo abras.",
444
+ linkedAccounts: "Cuentas vinculadas",
445
+ noLinkedAccounts: "No hay cuentas vinculadas.",
446
+ linkWith: "Vincular",
447
+ unlink: "Desvincular",
448
+ linked: "La cuenta qued\xF3 vinculada.",
415
449
  errors: {
416
450
  required: "Este dato es obligatorio.",
417
451
  invalid_email: "Eso no parece un correo.",
@@ -420,6 +454,11 @@ var TEXTS = {
420
454
  unavailable: "No se puede entrar en este momento. Intenta de nuevo en un momento.",
421
455
  state_mismatch: "No se pudo completar el ingreso. Intenta de nuevo.",
422
456
  cancelled: "El ingreso se cancel\xF3.",
457
+ email_taken: "Ese correo ya tiene una cuenta.",
458
+ email_unchanged: "Ese ya es tu correo.",
459
+ last_sign_in_method: "Primero agrega una contrase\xF1a o vincula otra cuenta: es tu \xFAnica forma de entrar.",
460
+ identity_already_linked: "Esa cuenta est\xE1 vinculada a otra persona.",
461
+ manual_linking_disabled: "Vincular cuentas est\xE1 desactivado en este workspace.",
423
462
  invalid_token: "Este enlace no es v\xE1lido o ya venci\xF3. Pide uno nuevo.",
424
463
  invalid_code: "Ese c\xF3digo no es correcto o ya venci\xF3.",
425
464
  wrong_password: "La contrase\xF1a actual no es correcta.",
@@ -779,7 +818,9 @@ function Account(props) {
779
818
  const when = props.formatDate ?? ((iso) => new Date(iso).toLocaleString());
780
819
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cx("account"), ...root, children: [
781
820
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProfilePart, { props, account, onSaved: setAccount }),
821
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmailPart, { props, account }),
782
822
  account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordPart, { props }),
823
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedPart, { props }),
783
824
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
784
825
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
785
826
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.passkeys }),
@@ -818,6 +859,65 @@ function ProfilePart({ props, account, onSaved }) {
818
859
  done && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: f.cx("notice"), children: done })
819
860
  ] });
820
861
  }
862
+ function EmailPart({ props, account }) {
863
+ const auth = useAuth();
864
+ const { f, done, setDone, run, general } = useSection(props);
865
+ const [email, setEmail] = (0, import_react.useState)("");
866
+ const [password, setPassword] = (0, import_react.useState)("");
867
+ const submit = async (e) => {
868
+ e.preventDefault();
869
+ const r = await run(() => auth.account.changeEmail({ email, password }));
870
+ if (!r.ok) return;
871
+ setPassword("");
872
+ setDone(f.t.emailSent.replace("{email}", r.email));
873
+ };
874
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: submit, noValidate: true, children: [
875
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.emailAddress }),
876
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: account.email }),
877
+ general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
878
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.newEmail, value: email, autoComplete: "email", required: true, onChange: setEmail }),
879
+ account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword }),
880
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.changeEmail }),
881
+ done && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: f.cx("notice"), children: done })
882
+ ] });
883
+ }
884
+ function LinkedPart({ props }) {
885
+ const auth = useAuth();
886
+ const { f, run, general } = useSection(props);
887
+ const [linked, setLinked] = (0, import_react.useState)(null);
888
+ const [offered, setOffered] = (0, import_react.useState)([]);
889
+ const [came, setCame] = (0, import_react.useState)(null);
890
+ (0, import_react.useEffect)(() => {
891
+ void auth.account.identities().then(setLinked);
892
+ void auth.methods().then((m) => setOffered(m.providers));
893
+ const back = typeof window === "undefined" ? null : new URLSearchParams(window.location.search);
894
+ if (back?.get("linked")) setCame(back.get("linked"));
895
+ else if (back?.get("auth_error")) f.setError({ code: back.get("auth_error"), message: "That account could not be linked. Try again." });
896
+ }, []);
897
+ if (linked === null) return null;
898
+ const name = (id) => offered.find((p) => p.id === id)?.name ?? id;
899
+ const unlink = async (provider) => {
900
+ const r = await run(() => auth.account.unlink(provider));
901
+ if (r.ok) setLinked(linked.filter((x) => x.provider !== provider));
902
+ };
903
+ const here = typeof window === "undefined" ? void 0 : window.location.pathname;
904
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: f.cx("section"), children: [
905
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.linkedAccounts }),
906
+ came && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: f.cx("notice"), children: f.t.linked }),
907
+ general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
908
+ linked.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.noLinkedAccounts }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: f.cx("sessions"), children: linked.map((x) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: f.cx("session"), children: [
909
+ name(x.provider),
910
+ x.email ? ` \xB7 ${x.email}` : "",
911
+ " ",
912
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void unlink(x.provider), children: f.t.unlink })
913
+ ] }, x.provider)) }),
914
+ offered.filter((p) => !linked.some((x) => x.provider === p.id)).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () => auth.account.link(p.id, { returnTo: here }), children: [
915
+ f.t.linkWith,
916
+ " ",
917
+ p.name
918
+ ] }, p.id))
919
+ ] });
920
+ }
821
921
  function PasswordPart({ props }) {
822
922
  const auth = useAuth();
823
923
  const { f, done, setDone, run, general } = useSection(props);