@gopherium/react-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
@@ -10,7 +10,10 @@ Session authentication for React apps backed by gouncer's authkit.
10
10
  - `@gopherium/react-auth/admin` is the user administration API client.
11
11
  - `@gopherium/react-auth/wpds` renders the WordPress Design System UI:
12
12
  `LoginScreen` (brand prop), `AccountPanel`, `UsersScreen`,
13
- `NewUserScreen`, and `usersNavItem`. Import
13
+ `NewUserScreen`, `usersNavItem`, `SetupCodeStep`, which asks for
14
+ the one-time setup code as Jenkins' "Unlock Jenkins" does, and
15
+ `SetupAccountStep`, the account rows of WordPress's installer with
16
+ its password strength meter. Import
14
17
  `@gopherium/react-auth/wpds/style.css` alongside it.
15
18
  - `@gopherium/react-auth/testing` is the msw harness: `server`,
16
19
  `installTestEnvironment`, `seedSession`, `defaultUser`, and canned
@@ -28,6 +31,38 @@ const client = createAuthQueryClient()
28
31
  </QueryClientProvider>
29
32
  ```
30
33
 
34
+ ## Setup wizard rows
35
+
36
+ `SetupAccountStep` shows the wizard's own rows in `before`, above
37
+ Name, and in `after`, below Your Email. They line up with the step's
38
+ rows only when they use the same hooks:
39
+
40
+ - `className="gopherium-setup__row"` on each row.
41
+ - `render={<div />}` on `Field.Root`, so the row adds no gap of its
42
+ own.
43
+ - `className="gopherium-setup__label"` on the row's label.
44
+ - `className="gopherium-setup__checkbox"` on a `CheckboxControl`.
45
+ - `className="gopherium-setup__help"` on a help line under the field.
46
+
47
+ A checkbox row is a plain `<div className="gopherium-setup__row">`
48
+ with a `Field.VisualLabel`, as the step's Confirm Password row is.
49
+
50
+ ```tsx
51
+ <SetupAccountStep
52
+ code={code}
53
+ submitLabel="Install MyApp"
54
+ onCreated={onCreated}
55
+ before={
56
+ <Field.Root className="gopherium-setup__row" render={<div />}>
57
+ <Field.Label className="gopherium-setup__label">Site Title</Field.Label>
58
+ <div>
59
+ <Input value={title} onChange={(event) => setTitle(event.target.value)} />
60
+ </div>
61
+ </Field.Root>
62
+ }
63
+ />
64
+ ```
65
+
31
66
  ## Consumer requirements
32
67
 
33
68
  - `react` and `@tanstack/react-query` are peer dependencies; the app
@@ -37,6 +72,9 @@ const client = createAuthQueryClient()
37
72
  development, proxy `/api` to your backend so the `__Host-` session
38
73
  cookie is first-party.
39
74
  - `/wpds` needs the `@wordpress/ui` peer.
75
+ - The account step loads zxcvbn, about 400 kB gzipped, as its own chunk
76
+ the first time it renders. Vite reports that chunk as larger than
77
+ 500 kB.
40
78
 
41
79
  ## License
42
80
 
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ /** The account the server answers for a listed or created user. */
3
+ export declare const accountSchema: z.ZodObject<{
4
+ id: z.ZodString;
5
+ email: z.ZodString;
6
+ name: z.ZodString;
7
+ disabled: z.ZodBoolean;
8
+ role: z.ZodDefault<z.ZodString>;
9
+ created_at: z.ZodCoercedDate<unknown>;
10
+ }, z.core.$strip>;
11
+ /** Account is a user account as the server answers it. */
12
+ export type Account = z.infer<typeof accountSchema>;
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { z } from 'zod';
3
+ /** The account the server answers for a listed or created user. */
4
+ export const accountSchema = z.object({
5
+ id: z.string(),
6
+ email: z.string(),
7
+ name: z.string(),
8
+ disabled: z.boolean(),
9
+ role: z.string().default(''),
10
+ created_at: z.coerce.date(),
11
+ });
@@ -1,15 +1,8 @@
1
1
  import { z } from 'zod';
2
+ import type { Account as User } from '../account.js';
2
3
  import { ValidationError } from '../api.js';
3
4
  export declare const usersQueryKey: readonly ["users"];
4
- declare const userSchema: z.ZodObject<{
5
- id: z.ZodString;
6
- email: z.ZodString;
7
- name: z.ZodString;
8
- disabled: z.ZodBoolean;
9
- role: z.ZodDefault<z.ZodString>;
10
- created_at: z.ZodCoercedDate<unknown>;
11
- }, z.core.$strip>;
12
- export type User = z.infer<typeof userSchema>;
5
+ export type { Account as User } from '../account.js';
13
6
  export interface NewUser {
14
7
  email: string;
15
8
  name: string;
@@ -1,18 +1,11 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { __ } from '@wordpress/i18n';
3
3
  import { z } from 'zod';
4
+ import { accountSchema } from '../account.js';
4
5
  import { RateLimitedError, UnauthorizedError, ValidationError } from '../api.js';
5
6
  import { DOMAIN } from '../domain.js';
6
7
  import { resolveTransport } from '../transport.js';
7
8
  export const usersQueryKey = ['users'];
8
- const userSchema = z.object({
9
- id: z.string(),
10
- email: z.string(),
11
- name: z.string(),
12
- disabled: z.boolean(),
13
- role: z.string().default(''),
14
- created_at: z.coerce.date(),
15
- });
16
9
  const invitationSchema = z.discriminatedUnion('delivered', [
17
10
  z.object({ delivered: z.literal(true) }),
18
11
  z.object({ delivered: z.literal(false), activation_link: z.string().trim().min(1) }),
@@ -125,7 +118,7 @@ async function restFetchUsers(signal) {
125
118
  if (!response.ok) {
126
119
  throw new Error(`listing users failed with status ${response.status}`);
127
120
  }
128
- return z.array(userSchema).parse(await response.json());
121
+ return z.array(accountSchema).parse(await response.json());
129
122
  }
130
123
  /**
131
124
  * Creates an account over REST.
@@ -147,7 +140,7 @@ async function restCreateUser(input) {
147
140
  if (!response.ok) {
148
141
  throw await writeError(response, __('invalid user details', DOMAIN), 'creating user');
149
142
  }
150
- return userSchema.parse(await response.json());
143
+ return accountSchema.parse(await response.json());
151
144
  }
152
145
  /**
153
146
  * Sends an invitation over REST, answering the same shape whatever the
package/dist/api.d.ts CHANGED
@@ -31,12 +31,31 @@ export declare class RateLimitedError extends Error {
31
31
  */
32
32
  export declare class InvalidTokenError extends Error {
33
33
  }
34
- /**
35
- * ValidationError is thrown when the backend rejects submitted details;
36
- * its message is the backend's human-readable explanation.
37
- */
34
+ /** ValidationError is thrown when the backend rejects submitted details, with its message, code and meta. */
38
35
  export declare class ValidationError extends Error {
36
+ readonly code?: string;
37
+ readonly meta?: Record<string, unknown>;
38
+ /**
39
+ * Creates the error from the backend's refusal.
40
+ * @param message - The backend's human-readable explanation.
41
+ * @param refusal - The refusal's code and meta, when the backend sent them.
42
+ */
43
+ constructor(message: string, refusal?: {
44
+ code?: string;
45
+ meta?: Record<string, unknown>;
46
+ });
39
47
  }
48
+ /**
49
+ * Reads the backend's refusal from a failed response body.
50
+ * @param response - The failed HTTP response.
51
+ * @param fallback - The message to use when the body is unreadable.
52
+ * @returns The refusal message with its optional code and meta.
53
+ */
54
+ export declare function refusalOf(response: Response, fallback: string): Promise<{
55
+ message: string;
56
+ code?: string;
57
+ meta?: Record<string, unknown>;
58
+ }>;
40
59
  /**
41
60
  * Returns the logged-in user, or null when no session is active.
42
61
  * @param signal - Aborts the in-flight request.
package/dist/api.js CHANGED
@@ -34,23 +34,36 @@ export class RateLimitedError extends Error {
34
34
  */
35
35
  export class InvalidTokenError extends Error {
36
36
  }
37
- /**
38
- * ValidationError is thrown when the backend rejects submitted details;
39
- * its message is the backend's human-readable explanation.
40
- */
37
+ /** ValidationError is thrown when the backend rejects submitted details, with its message, code and meta. */
41
38
  export class ValidationError extends Error {
39
+ code;
40
+ meta;
41
+ /**
42
+ * Creates the error from the backend's refusal.
43
+ * @param message - The backend's human-readable explanation.
44
+ * @param refusal - The refusal's code and meta, when the backend sent them.
45
+ */
46
+ constructor(message, refusal = {}) {
47
+ super(message);
48
+ this.code = refusal.code;
49
+ this.meta = refusal.meta;
50
+ }
42
51
  }
43
- const refusalSchema = z.object({ error: z.string(), code: z.string().optional() });
52
+ const refusalSchema = z.object({
53
+ error: z.string(),
54
+ code: z.string().optional(),
55
+ meta: z.record(z.string(), z.unknown()).optional(),
56
+ });
44
57
  /**
45
58
  * Reads the backend's refusal from a failed response body.
46
59
  * @param response - The failed HTTP response.
47
60
  * @param fallback - The message to use when the body is unreadable.
48
- * @returns The refusal message and optional code.
61
+ * @returns The refusal message with its optional code and meta.
49
62
  */
50
- async function refusalOf(response, fallback) {
63
+ export async function refusalOf(response, fallback) {
51
64
  try {
52
65
  const held = refusalSchema.parse(await response.json());
53
- return { message: held.error, code: held.code };
66
+ return { message: held.error, code: held.code, meta: held.meta };
54
67
  }
55
68
  catch {
56
69
  return { message: fallback };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export type { Account } from './account.js';
1
2
  export { InvalidCredentialsError, InvalidTokenError, RateLimitedError, UnauthorizedError, ValidationError, acceptInvite, fetchSession, login, logout, requestPasswordReset, resetPassword, } from './api.js';
2
3
  export type { User } from './api.js';
3
4
  export { AuthGate } from './AuthGate.js';
@@ -8,5 +9,7 @@ export type { Catalog } from './languages.js';
8
9
  export { isSessionRevoked } from './probe.js';
9
10
  export { createAuthQueryClient } from './queryClient.js';
10
11
  export { adoptSession, sessionQueryKey, useLogout, useSession } from './session.js';
12
+ export { SetupClosedError, SetupCodeError, SiteStartingError, checkSetupCode, createFirstAccount, isSetupOpen, } from './setup.js';
13
+ export type { FirstAccount } from './setup.js';
11
14
  export { configureAuthTransport, resetAuthTransport } from './transport.js';
12
15
  export type { AuthTransport } from './transport.js';
package/dist/index.js CHANGED
@@ -7,4 +7,5 @@ export { catalogFor } from './languages.js';
7
7
  export { isSessionRevoked } from './probe.js';
8
8
  export { createAuthQueryClient } from './queryClient.js';
9
9
  export { adoptSession, sessionQueryKey, useLogout, useSession } from './session.js';
10
+ export { SetupClosedError, SetupCodeError, SiteStartingError, checkSetupCode, createFirstAccount, isSetupOpen, } from './setup.js';
10
11
  export { configureAuthTransport, resetAuthTransport } from './transport.js';
@@ -1 +1 @@
1
- { "": { "lang": "es-ES", "plural-forms": "nplurals=2; plural=(n != 1);" }, "Actions": ["Acciones"], "Active": ["Activo"], "Back to login": ["Volver al inicio de sesión"], "Disable": ["Desactivar"], "Disable %s": ["Desactivar a %s"], "Disabled": ["Desactivado"], "Done": ["Hecho"], "Enable": ["Activar"], "Enable %s": ["Activar a %s"], "Forgot your password?": ["¿Has olvidado tu contraseña?"], "Go to login": ["Ir al inicio de sesión"], "If that address has an account, a reset link is on its way.": ["Si esa dirección tiene una cuenta, le llegará un enlace para restablecerla."], "Invalid email or password.": ["Correo o contraseña no válidos."], "Loading users…": ["Cargando usuarios…"], "Loading…": ["Cargando…"], "Log in": ["Iniciar sesión"], "Log out": ["Cerrar sesión"], "Login failed, please try again.": ["No se pudo iniciar sesión, inténtalo de nuevo."], "Logout failed, please try again.": ["No se pudo cerrar sesión, inténtalo de nuevo."], "No mail server is configured. Deliver the activation link by hand.": ["No hay servidor de correo configurado. Entrega el enlace de activación a mano."], "Password": ["Contraseña"], "Send invitation": ["Enviar invitación"], "Send reset link": ["Enviar enlace de restablecimiento"], "Set password": ["Guardar contraseña"], "Something went wrong.": ["Algo ha ido mal."], "The invitation could not be sent.": ["No se pudo enviar la invitación."], "The password could not be set, please try again.": ["No se pudo guardar la contraseña, inténtalo de nuevo."], "The request failed, please try again.": ["No se pudo completar la petición, inténtalo de nuevo."], "This link is no longer valid. Ask for a new one.": ["Este enlace ya no es válido. Pide uno nuevo."], "This link is no longer valid. Ask the person who invited you for a new one.": ["Este enlace ya no es válido. Pide uno nuevo a quien te invitó."], "Too many attempts. Please wait a minute and try again.": ["Demasiados intentos. Espera un minuto e inténtalo de nuevo."], "Try again": ["Inténtalo de nuevo"], "Update failed.": ["No se pudo actualizar."], "Users could not be loaded.": ["No se pudieron cargar los usuarios."], "Your password is set. Sign in with it.": ["Tu contraseña está guardada. Inicia sesión con ella."], "admin section\u0004Users": ["Usuarios"], "button\u0004New user": ["Nuevo usuario"], "column\u0004Email": ["Correo"], "column\u0004Name": ["Nombre"], "column\u0004Status": ["Estado"], "field label\u0004Activation link": ["Enlace de activación"], "field label\u0004Email": ["Correo"], "field label\u0004Name": ["Nombre"], "invalid invitation details": ["datos de invitación no válidos"], "invalid password details": ["datos de contraseña no válidos"], "invalid user details": ["datos de usuario no válidos"], "page heading\u0004New user": ["Nuevo usuario"], "page heading\u0004Users": ["Usuarios"], "the account could not be updated": ["no se ha podido actualizar la cuenta"] }
1
+ { "": { "lang": "es-ES", "plural-forms": "nplurals=3; plural=((n == 1) ? 0 : ((n != 0 && n % 1000000 == 0) ? 1 : 2));" }, "Actions": ["Acciones"], "Active": ["Activo"], "Back to login": ["Volver al inicio de sesión"], "Caps lock is on.": ["El bloqueo de mayúsculas está activado."], "Confirm Password": ["Confirma la contraseña"], "Confirm use of weak password": ["Confirma el uso de una contraseña débil."], "Continue": ["Continuar"], "Disable": ["Desactivar"], "Disable %s": ["Desactivar a %s"], "Disabled": ["Desactivado"], "Done": ["Hecho"], "Double-check your email address before continuing.": ["Comprueba bien tu dirección de correo electrónico antes de continuar."], "Enable": ["Activar"], "Enable %s": ["Activar a %s"], "Go to login": ["Ir al inicio de sesión"], "Hide": ["Ocultar"], "Hide password": ["Ocultar la contraseña"], "Hint: The password should be at least %d character long. To make it stronger, use upper and lower case letters, numbers, and symbols like ! \" ? $ %% ^ & ).": ["Sugerencia: La contraseña debe ser de al menos %d carácter. Para hacerla más fuerte usa mayúsculas y minúsculas, números y símbolos como ! \" ? $ %% ^ y ).", "Sugerencia: La contraseña debe ser de al menos %d de caracteres. Para hacerla más fuerte usa mayúsculas y minúsculas, números y símbolos como ! \" ? $ %% ^ y ).", "Sugerencia: La contraseña debe ser de al menos %d caracteres. Para hacerla más fuerte usa mayúsculas y minúsculas, números y símbolos como ! \" ? $ %% ^ y )."], "If that address has an account, a reset link is on its way.": ["Si esa dirección tiene una cuenta, le llegará un enlace para restablecerla."], "Important:": ["Importante:"], "Invalid email or password.": ["Correo o contraseña no válidos."], "Loading users…": ["Cargando usuarios…"], "Loading…": ["Cargando…"], "Log in": ["Iniciar sesión"], "Log out": ["Cerrar sesión"], "Login failed, please try again.": ["No se pudo iniciar sesión, inténtalo de nuevo."], "Logout failed, please try again.": ["No se pudo cerrar sesión, inténtalo de nuevo."], "Lost your password?": ["¿Has olvidado tu contraseña?"], "No mail server is configured. Deliver the activation link by hand.": ["No hay servidor de correo configurado. Entrega el enlace de activación a mano."], "Password": ["Contraseña"], "Please copy the setup code from the file and paste it below.": ["Por favor, copia el código de configuración del archivo y pégalo abajo."], "Please provide a name.": ["Por favor, facilita un nombre."], "Send invitation": ["Enviar invitación"], "Send reset link": ["Enviar enlace de restablecimiento"], "Set password": ["Guardar contraseña"], "Setup code": ["Código de configuración"], "Show": ["Mostrar"], "Show password": ["Mostrar la contraseña"], "Something went wrong.": ["Algo ha ido mal."], "Sorry, that is not a valid email address. Email addresses look like <code>username@example.com</code>.": ["Lo siento, esa no es una dirección de correo electrónico válida. Las direcciones de correo electrónico deben parecerse a <code>usuario@example.com</code>."], "The account could not be created, please try again.": ["No se pudo crear la cuenta, inténtalo de nuevo."], "The invitation could not be sent.": ["No se pudo enviar la invitación."], "The password could not be set, please try again.": ["No se pudo guardar la contraseña, inténtalo de nuevo."], "The request failed, please try again.": ["No se pudo completar la petición, inténtalo de nuevo."], "The setup code file cannot be used. Please check it and try again.": ["No se puede usar el archivo del código de configuración. Por favor, revísalo e inténtalo de nuevo."], "The site is starting. Please wait a moment and try again.": ["El sitio se está iniciando. Por favor, espera un momento e inténtalo de nuevo."], "This link is no longer valid. Ask for a new one.": ["Este enlace ya no es válido. Pide uno nuevo."], "This link is no longer valid. Ask the person who invited you for a new one.": ["Este enlace ya no es válido. Pide uno nuevo a quien te invitó."], "This name is too long.": ["Este nombre es demasiado largo."], "This password is too long.": ["Esta contraseña es demasiado larga."], "To ensure this site is securely set up by its owner, a setup code has been written to a file on the server.": ["Para garantizar que la persona propietaria configure este sitio de forma segura, se ha escrito un código de configuración en un archivo del servidor."], "Too many attempts. Please try again later.": ["Demasiados intentos. Por favor, inténtalo de nuevo más tarde."], "Too many attempts. Please wait a minute and try again.": ["Demasiados intentos. Espera un minuto e inténtalo de nuevo."], "Try again": ["Inténtalo de nuevo"], "Unlock %s": ["Desbloquear %s"], "Update failed.": ["No se pudo actualizar."], "Users could not be loaded.": ["No se pudieron cargar los usuarios."], "You appear to have already set up this site.": ["Parece que ya has configurado este sitio."], "You will need this password to log in. Please store it in a secure location.": ["Necesitas esta contraseña para acceder. Por favor, guárdala en un lugar seguro."], "Your Email": ["Tu correo electrónico"], "Your password is set. Sign in with it.": ["Tu contraseña está guardada. Inicia sesión con ella."], "Your setup code appears to be invalid. Please copy it again from the setup code file.": ["Tu código de configuración parece que no es válido. Por favor, cópialo de nuevo del archivo del código de configuración."], "Your setup code has expired. A new one is waiting in the setup code file.": ["Tu código de configuración ha caducado. Hay uno nuevo esperando en el archivo del código de configuración."], "admin section\u0004Users": ["Usuarios"], "button\u0004New user": ["Nuevo usuario"], "column\u0004Email": ["Correo"], "column\u0004Name": ["Nombre"], "column\u0004Status": ["Estado"], "field label\u0004Activation link": ["Enlace de activación"], "field label\u0004Email": ["Correo"], "field label\u0004Name": ["Nombre"], "invalid invitation details": ["datos de invitación no válidos"], "invalid password details": ["datos de contraseña no válidos"], "invalid user details": ["datos de usuario no válidos"], "page heading\u0004New user": ["Nuevo usuario"], "page heading\u0004Users": ["Usuarios"], "password strength\u0004Medium": ["Medio"], "password strength\u0004Password strength unknown": ["Fortaleza de la contraseña desconocida"], "password strength\u0004Strong": ["Fuerte"], "password strength\u0004Very weak": ["Muy débil"], "password strength\u0004Weak": ["Débil"], "the account could not be updated": ["no se ha podido actualizar la cuenta"] }
@@ -1 +1 @@
1
- {"":{"lang":"es-ES","plural-forms":"nplurals=2; plural=(n != 1);"},"Actions":["Acciones"],"Active":["Activo"],"Back to login":["Volver al inicio de sesión"],"Disable":["Desactivar"],"Disable %s":["Desactivar a %s"],"Disabled":["Desactivado"],"Done":["Hecho"],"Enable":["Activar"],"Enable %s":["Activar a %s"],"Forgot your password?":["¿Has olvidado tu contraseña?"],"Go to login":["Ir al inicio de sesión"],"If that address has an account, a reset link is on its way.":["Si esa dirección tiene una cuenta, le llegará un enlace para restablecerla."],"Invalid email or password.":["Correo o contraseña no válidos."],"Loading users…":["Cargando usuarios…"],"Loading…":["Cargando…"],"Log in":["Iniciar sesión"],"Log out":["Cerrar sesión"],"Login failed, please try again.":["No se pudo iniciar sesión, inténtalo de nuevo."],"Logout failed, please try again.":["No se pudo cerrar sesión, inténtalo de nuevo."],"No mail server is configured. Deliver the activation link by hand.":["No hay servidor de correo configurado. Entrega el enlace de activación a mano."],"Password":["Contraseña"],"Send invitation":["Enviar invitación"],"Send reset link":["Enviar enlace de restablecimiento"],"Set password":["Guardar contraseña"],"Something went wrong.":["Algo ha ido mal."],"The invitation could not be sent.":["No se pudo enviar la invitación."],"The password could not be set, please try again.":["No se pudo guardar la contraseña, inténtalo de nuevo."],"The request failed, please try again.":["No se pudo completar la petición, inténtalo de nuevo."],"This link is no longer valid. Ask for a new one.":["Este enlace ya no es válido. Pide uno nuevo."],"This link is no longer valid. Ask the person who invited you for a new one.":["Este enlace ya no es válido. Pide uno nuevo a quien te invitó."],"Too many attempts. Please wait a minute and try again.":["Demasiados intentos. Espera un minuto e inténtalo de nuevo."],"Try again":["Inténtalo de nuevo"],"Update failed.":["No se pudo actualizar."],"Users could not be loaded.":["No se pudieron cargar los usuarios."],"Your password is set. Sign in with it.":["Tu contraseña está guardada. Inicia sesión con ella."],"admin section\u0004Users":["Usuarios"],"button\u0004New user":["Nuevo usuario"],"column\u0004Email":["Correo"],"column\u0004Name":["Nombre"],"column\u0004Status":["Estado"],"field label\u0004Activation link":["Enlace de activación"],"field label\u0004Email":["Correo"],"field label\u0004Name":["Nombre"],"invalid invitation details":["datos de invitación no válidos"],"invalid password details":["datos de contraseña no válidos"],"invalid user details":["datos de usuario no válidos"],"page heading\u0004New user":["Nuevo usuario"],"page heading\u0004Users":["Usuarios"],"the account could not be updated":["no se ha podido actualizar la cuenta"]}
1
+ {"":{"lang":"es-ES","plural-forms":"nplurals=3; plural=((n == 1) ? 0 : ((n != 0 && n % 1000000 == 0) ? 1 : 2));"},"Actions":["Acciones"],"Active":["Activo"],"Back to login":["Volver al inicio de sesión"],"Caps lock is on.":["El bloqueo de mayúsculas está activado."],"Confirm Password":["Confirma la contraseña"],"Confirm use of weak password":["Confirma el uso de una contraseña débil."],"Continue":["Continuar"],"Disable":["Desactivar"],"Disable %s":["Desactivar a %s"],"Disabled":["Desactivado"],"Done":["Hecho"],"Double-check your email address before continuing.":["Comprueba bien tu dirección de correo electrónico antes de continuar."],"Enable":["Activar"],"Enable %s":["Activar a %s"],"Go to login":["Ir al inicio de sesión"],"Hide":["Ocultar"],"Hide password":["Ocultar la contraseña"],"Hint: The password should be at least %d character long. To make it stronger, use upper and lower case letters, numbers, and symbols like ! \" ? $ %% ^ & ).":["Sugerencia: La contraseña debe ser de al menos %d carácter. Para hacerla más fuerte usa mayúsculas y minúsculas, números y símbolos como ! \" ? $ %% ^ y ).","Sugerencia: La contraseña debe ser de al menos %d de caracteres. Para hacerla más fuerte usa mayúsculas y minúsculas, números y símbolos como ! \" ? $ %% ^ y ).","Sugerencia: La contraseña debe ser de al menos %d caracteres. Para hacerla más fuerte usa mayúsculas y minúsculas, números y símbolos como ! \" ? $ %% ^ y )."],"If that address has an account, a reset link is on its way.":["Si esa dirección tiene una cuenta, le llegará un enlace para restablecerla."],"Important:":["Importante:"],"Invalid email or password.":["Correo o contraseña no válidos."],"Loading users…":["Cargando usuarios…"],"Loading…":["Cargando…"],"Log in":["Iniciar sesión"],"Log out":["Cerrar sesión"],"Login failed, please try again.":["No se pudo iniciar sesión, inténtalo de nuevo."],"Logout failed, please try again.":["No se pudo cerrar sesión, inténtalo de nuevo."],"Lost your password?":["¿Has olvidado tu contraseña?"],"No mail server is configured. Deliver the activation link by hand.":["No hay servidor de correo configurado. Entrega el enlace de activación a mano."],"Password":["Contraseña"],"Please copy the setup code from the file and paste it below.":["Por favor, copia el código de configuración del archivo y pégalo abajo."],"Please provide a name.":["Por favor, facilita un nombre."],"Send invitation":["Enviar invitación"],"Send reset link":["Enviar enlace de restablecimiento"],"Set password":["Guardar contraseña"],"Setup code":["Código de configuración"],"Show":["Mostrar"],"Show password":["Mostrar la contraseña"],"Something went wrong.":["Algo ha ido mal."],"Sorry, that is not a valid email address. Email addresses look like <code>username@example.com</code>.":["Lo siento, esa no es una dirección de correo electrónico válida. Las direcciones de correo electrónico deben parecerse a <code>usuario@example.com</code>."],"The account could not be created, please try again.":["No se pudo crear la cuenta, inténtalo de nuevo."],"The invitation could not be sent.":["No se pudo enviar la invitación."],"The password could not be set, please try again.":["No se pudo guardar la contraseña, inténtalo de nuevo."],"The request failed, please try again.":["No se pudo completar la petición, inténtalo de nuevo."],"The setup code file cannot be used. Please check it and try again.":["No se puede usar el archivo del código de configuración. Por favor, revísalo e inténtalo de nuevo."],"The site is starting. Please wait a moment and try again.":["El sitio se está iniciando. Por favor, espera un momento e inténtalo de nuevo."],"This link is no longer valid. Ask for a new one.":["Este enlace ya no es válido. Pide uno nuevo."],"This link is no longer valid. Ask the person who invited you for a new one.":["Este enlace ya no es válido. Pide uno nuevo a quien te invitó."],"This name is too long.":["Este nombre es demasiado largo."],"This password is too long.":["Esta contraseña es demasiado larga."],"To ensure this site is securely set up by its owner, a setup code has been written to a file on the server.":["Para garantizar que la persona propietaria configure este sitio de forma segura, se ha escrito un código de configuración en un archivo del servidor."],"Too many attempts. Please try again later.":["Demasiados intentos. Por favor, inténtalo de nuevo más tarde."],"Too many attempts. Please wait a minute and try again.":["Demasiados intentos. Espera un minuto e inténtalo de nuevo."],"Try again":["Inténtalo de nuevo"],"Unlock %s":["Desbloquear %s"],"Update failed.":["No se pudo actualizar."],"Users could not be loaded.":["No se pudieron cargar los usuarios."],"You appear to have already set up this site.":["Parece que ya has configurado este sitio."],"You will need this password to log in. Please store it in a secure location.":["Necesitas esta contraseña para acceder. Por favor, guárdala en un lugar seguro."],"Your Email":["Tu correo electrónico"],"Your password is set. Sign in with it.":["Tu contraseña está guardada. Inicia sesión con ella."],"Your setup code appears to be invalid. Please copy it again from the setup code file.":["Tu código de configuración parece que no es válido. Por favor, cópialo de nuevo del archivo del código de configuración."],"Your setup code has expired. A new one is waiting in the setup code file.":["Tu código de configuración ha caducado. Hay uno nuevo esperando en el archivo del código de configuración."],"admin section\u0004Users":["Usuarios"],"button\u0004New user":["Nuevo usuario"],"column\u0004Email":["Correo"],"column\u0004Name":["Nombre"],"column\u0004Status":["Estado"],"field label\u0004Activation link":["Enlace de activación"],"field label\u0004Email":["Correo"],"field label\u0004Name":["Nombre"],"invalid invitation details":["datos de invitación no válidos"],"invalid password details":["datos de contraseña no válidos"],"invalid user details":["datos de usuario no válidos"],"page heading\u0004New user":["Nuevo usuario"],"page heading\u0004Users":["Usuarios"],"password strength\u0004Medium":["Medio"],"password strength\u0004Password strength unknown":["Fortaleza de la contraseña desconocida"],"password strength\u0004Strong":["Fuerte"],"password strength\u0004Very weak":["Muy débil"],"password strength\u0004Weak":["Débil"],"the account could not be updated":["no se ha podido actualizar la cuenta"]}
@@ -0,0 +1,46 @@
1
+ import type { Account } from './account.js';
2
+ /** FirstAccount is what the setup account step sends. */
3
+ export interface FirstAccount {
4
+ code: string;
5
+ email: string;
6
+ name: string;
7
+ password: string;
8
+ }
9
+ /** SetupCodeError is thrown when the setup code is wrong, has expired, or its file cannot be used. */
10
+ export declare class SetupCodeError extends Error {
11
+ readonly reason: 'invalid' | 'expired' | 'file';
12
+ /**
13
+ * Creates the error for the reason the site gave.
14
+ * @param reason - Why the site refused the code.
15
+ */
16
+ constructor(reason: 'invalid' | 'expired' | 'file');
17
+ }
18
+ /** SetupClosedError is thrown when the site already has its first account. */
19
+ export declare class SetupClosedError extends Error {
20
+ }
21
+ /** SiteStartingError is thrown when the site answers 503 while it starts. */
22
+ export declare class SiteStartingError extends Error {
23
+ readonly retryAfter?: number;
24
+ /**
25
+ * Creates the error with the wait the site asked for.
26
+ * @param retryAfter - The seconds the site asked to wait, if it said.
27
+ */
28
+ constructor(retryAfter?: number);
29
+ }
30
+ /**
31
+ * Reports whether the site still waits for its first account.
32
+ * @param signal - Aborts the in-flight request.
33
+ * @returns True while the site has no account.
34
+ */
35
+ export declare function isSetupOpen(signal?: AbortSignal): Promise<boolean>;
36
+ /**
37
+ * Resolves when the site accepts the one-time setup code, spending nothing.
38
+ * @param code - The setup code as typed.
39
+ */
40
+ export declare function checkSetupCode(code: string): Promise<void>;
41
+ /**
42
+ * Creates the site's first account with the one-time setup code.
43
+ * @param input - The setup code and the account's details.
44
+ * @returns The created account.
45
+ */
46
+ export declare function createFirstAccount(input: FirstAccount): Promise<Account>;
package/dist/setup.js ADDED
@@ -0,0 +1,132 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { z } from 'zod';
3
+ import { accountSchema } from './account.js';
4
+ import { RateLimitedError, ValidationError, refusalOf } from './api.js';
5
+ import { resolveTransport } from './transport.js';
6
+ /** SetupCodeError is thrown when the setup code is wrong, has expired, or its file cannot be used. */
7
+ export class SetupCodeError extends Error {
8
+ reason;
9
+ /**
10
+ * Creates the error for the reason the site gave.
11
+ * @param reason - Why the site refused the code.
12
+ */
13
+ constructor(reason) {
14
+ super(`the setup code was refused as ${reason}`);
15
+ this.reason = reason;
16
+ }
17
+ }
18
+ /** SetupClosedError is thrown when the site already has its first account. */
19
+ export class SetupClosedError extends Error {
20
+ }
21
+ /** SiteStartingError is thrown when the site answers 503 while it starts. */
22
+ export class SiteStartingError extends Error {
23
+ retryAfter;
24
+ /**
25
+ * Creates the error with the wait the site asked for.
26
+ * @param retryAfter - The seconds the site asked to wait, if it said.
27
+ */
28
+ constructor(retryAfter) {
29
+ super('the site is starting');
30
+ this.retryAfter = retryAfter;
31
+ }
32
+ }
33
+ /** The setup flag the site answers. */
34
+ const statusSchema = z.object({ open: z.boolean() });
35
+ /** The form a Retry-After header takes when it names seconds. */
36
+ const SECONDS = /^\d+$/;
37
+ const setupRefusals = new Map([
38
+ [401, (refusal) => new SetupCodeError(refusal.code === 'setup_code_expired' ? 'expired' : 'invalid')],
39
+ [409, () => new SetupClosedError('the site already has its first account')],
40
+ [422, (refusal) => new ValidationError(refusal.message, refusal)],
41
+ [429, () => new RateLimitedError('too many attempts')],
42
+ [500, (refusal) => (refusal.code === 'setup_code_file' ? new SetupCodeError('file') : undefined)],
43
+ [503, (_, response) => new SiteStartingError(retryAfterOf(response))],
44
+ ]);
45
+ /**
46
+ * Returns the seconds a refused response asks to wait.
47
+ * @param response - The refused HTTP response.
48
+ * @returns The seconds, or undefined when its Retry-After header names none.
49
+ */
50
+ function retryAfterOf(response) {
51
+ const held = response.headers.get('Retry-After')?.trim() ?? '';
52
+ return SECONDS.test(held) ? Number(held) : undefined;
53
+ }
54
+ /**
55
+ * Returns the error a refused setup request throws.
56
+ * @param response - The refused HTTP response.
57
+ * @returns The error to throw.
58
+ */
59
+ async function setupRefusal(response) {
60
+ const refusal = await refusalOf(response, 'the setup request was refused');
61
+ return (setupRefusals.get(response.status)?.(refusal, response) ??
62
+ new Error(`the setup request failed with status ${response.status}`));
63
+ }
64
+ /**
65
+ * Posts a setup request over REST.
66
+ * @param path - The setup route.
67
+ * @param body - The fields the route reads.
68
+ * @returns The accepted response.
69
+ */
70
+ async function postSetup(path, body) {
71
+ const response = await fetch(path, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify(body),
75
+ });
76
+ if (!response.ok) {
77
+ throw await setupRefusal(response);
78
+ }
79
+ return response;
80
+ }
81
+ /**
82
+ * Reads the setup flag over REST.
83
+ * @param signal - Aborts the in-flight request.
84
+ * @returns True while the site has no account.
85
+ */
86
+ async function restIsSetupOpen(signal) {
87
+ const response = await fetch('/api/auth/setup', { signal });
88
+ if (!response.ok) {
89
+ throw new Error(`loading the setup state failed with status ${response.status}`);
90
+ }
91
+ return statusSchema.parse(await response.json()).open;
92
+ }
93
+ /**
94
+ * Checks the setup code over REST.
95
+ * @param code - The setup code as typed.
96
+ */
97
+ async function restCheckSetupCode(code) {
98
+ await postSetup('/api/auth/setup/code', { code });
99
+ }
100
+ /**
101
+ * Creates the first account over REST.
102
+ * @param input - The setup code and the account's details.
103
+ * @returns The created account.
104
+ */
105
+ async function restCreateFirstAccount(input) {
106
+ const { code, email, name, password } = input;
107
+ const response = await postSetup('/api/auth/setup', { code, email, name, password });
108
+ return accountSchema.parse(await response.json());
109
+ }
110
+ /**
111
+ * Reports whether the site still waits for its first account.
112
+ * @param signal - Aborts the in-flight request.
113
+ * @returns True while the site has no account.
114
+ */
115
+ export async function isSetupOpen(signal) {
116
+ return resolveTransport('isSetupOpen', restIsSetupOpen)(signal);
117
+ }
118
+ /**
119
+ * Resolves when the site accepts the one-time setup code, spending nothing.
120
+ * @param code - The setup code as typed.
121
+ */
122
+ export async function checkSetupCode(code) {
123
+ await resolveTransport('checkSetupCode', restCheckSetupCode)(code);
124
+ }
125
+ /**
126
+ * Creates the site's first account with the one-time setup code.
127
+ * @param input - The setup code and the account's details.
128
+ * @returns The created account.
129
+ */
130
+ export async function createFirstAccount(input) {
131
+ return resolveTransport('createFirstAccount', restCreateFirstAccount)(input);
132
+ }
@@ -2,6 +2,7 @@ import '@testing-library/jest-dom/vitest';
2
2
  import type { QueryClient } from '@tanstack/react-query';
3
3
  import type { User } from '../api.js';
4
4
  export { http, HttpResponse } from 'msw';
5
+ export { createdAccount, firstAccountClosed, firstAccountCodeRefused, firstAccountOk, firstAccountRateLimited, firstAccountStarting, setupCodeClosed, setupCodeOk, setupCodeRateLimited, setupCodeRefused, setupCodeStarting, setupStatus, } from './setup.js';
5
6
  export declare const server: import("msw/node").SetupServer;
6
7
  /**
7
8
  * Installs the msw server lifecycle and DOM cleanup as vitest hooks.
@@ -6,6 +6,7 @@ import { setupServer } from 'msw/node';
6
6
  import { afterAll, afterEach, beforeAll } from 'vitest';
7
7
  import { sessionQueryKey } from '../session.js';
8
8
  export { http, HttpResponse } from 'msw';
9
+ export { createdAccount, firstAccountClosed, firstAccountCodeRefused, firstAccountOk, firstAccountRateLimited, firstAccountStarting, setupCodeClosed, setupCodeOk, setupCodeRateLimited, setupCodeRefused, setupCodeStarting, setupStatus, } from './setup.js';
9
10
  export const server = setupServer();
10
11
  /**
11
12
  * Installs the msw server lifecycle and DOM cleanup as vitest hooks.
@@ -0,0 +1,63 @@
1
+ import type { Account } from '../account.js';
2
+ import type { SetupCodeError } from '../setup.js';
3
+ /** createdAccount is the canned first account a setup creates. */
4
+ export declare const createdAccount: Account;
5
+ /**
6
+ * Answers the setup state.
7
+ * @param open - Whether the site still waits for its first account.
8
+ * @returns The msw handler.
9
+ */
10
+ export declare function setupStatus(open: boolean): import("msw").HttpHandler;
11
+ /**
12
+ * Accepts a setup code check.
13
+ * @returns The msw handler.
14
+ */
15
+ export declare function setupCodeOk(): import("msw").HttpHandler;
16
+ /**
17
+ * Refuses a setup code check for the given reason.
18
+ * @param reason - Why the site refuses the code.
19
+ * @returns The msw handler.
20
+ */
21
+ export declare function setupCodeRefused(reason: SetupCodeError['reason']): import("msw").HttpHandler;
22
+ /**
23
+ * Refuses a setup code check as already set up.
24
+ * @returns The msw handler.
25
+ */
26
+ export declare function setupCodeClosed(): import("msw").HttpHandler;
27
+ /**
28
+ * Refuses a setup code check as rate limited.
29
+ * @returns The msw handler.
30
+ */
31
+ export declare function setupCodeRateLimited(): import("msw").HttpHandler;
32
+ /**
33
+ * Answers a setup code check as a site that is still starting.
34
+ * @returns The msw handler.
35
+ */
36
+ export declare function setupCodeStarting(): import("msw").HttpHandler;
37
+ /**
38
+ * Creates the first account as the given account.
39
+ * @param account - The account the site answers.
40
+ * @returns The msw handler.
41
+ */
42
+ export declare function firstAccountOk(account?: Account): import("msw").HttpHandler;
43
+ /**
44
+ * Refuses the first account's setup code for the given reason.
45
+ * @param reason - Why the site refuses the code.
46
+ * @returns The msw handler.
47
+ */
48
+ export declare function firstAccountCodeRefused(reason: SetupCodeError['reason']): import("msw").HttpHandler;
49
+ /**
50
+ * Refuses the first account as already set up.
51
+ * @returns The msw handler.
52
+ */
53
+ export declare function firstAccountClosed(): import("msw").HttpHandler;
54
+ /**
55
+ * Refuses the first account as rate limited.
56
+ * @returns The msw handler.
57
+ */
58
+ export declare function firstAccountRateLimited(): import("msw").HttpHandler;
59
+ /**
60
+ * Answers the first account as a site that is still starting.
61
+ * @returns The msw handler.
62
+ */
63
+ export declare function firstAccountStarting(): import("msw").HttpHandler;