@gopherium/react-auth 0.10.0 → 0.11.1

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.
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { ValidationError } from '../api.js';
2
3
  export declare const usersQueryKey: readonly ["users"];
3
4
  declare const userSchema: z.ZodObject<{
4
5
  id: z.ZodString;
@@ -49,6 +50,21 @@ export declare class SelfRoleError extends Error {
49
50
  */
50
51
  export declare class LastPrivilegedError extends Error {
51
52
  }
53
+ /**
54
+ * RoleUnknownError is thrown when a write names a role nobody declared.
55
+ */
56
+ export declare class RoleUnknownError extends ValidationError {
57
+ }
58
+ /**
59
+ * RoleBeyondReachError is thrown when the actor may not hand out the role or change the account.
60
+ */
61
+ export declare class RoleBeyondReachError extends Error {
62
+ }
63
+ /**
64
+ * RoleChangedError is thrown when the account's role changed before the write landed.
65
+ */
66
+ export declare class RoleChangedError extends Error {
67
+ }
52
68
  /**
53
69
  * Returns every user account.
54
70
  * @param signal - Aborts the in-flight request.
@@ -39,21 +39,79 @@ export class SelfRoleError extends Error {
39
39
  */
40
40
  export class LastPrivilegedError extends Error {
41
41
  }
42
+ /**
43
+ * RoleUnknownError is thrown when a write names a role nobody declared.
44
+ */
45
+ export class RoleUnknownError extends ValidationError {
46
+ }
47
+ /**
48
+ * RoleBeyondReachError is thrown when the actor may not hand out the role or change the account.
49
+ */
50
+ export class RoleBeyondReachError extends Error {
51
+ }
52
+ /**
53
+ * RoleChangedError is thrown when the account's role changed before the write landed.
54
+ */
55
+ export class RoleChangedError extends Error {
56
+ }
42
57
  const errorSchema = z.object({ error: z.string(), code: z.string().optional() });
58
+ const roleErrors = new Map([
59
+ ['role_unknown', RoleUnknownError],
60
+ ['role_beyond_reach', RoleBeyondReachError],
61
+ ]);
62
+ const accountErrors = new Map([
63
+ ...roleErrors,
64
+ ['role_changed', RoleChangedError],
65
+ ]);
66
+ const roleChangeErrors = new Map([
67
+ ...accountErrors,
68
+ ['role_insufficient', RoleRefusedError],
69
+ ['self_role_refused', SelfRoleError],
70
+ ['last_privileged_refused', LastPrivilegedError],
71
+ ]);
43
72
  /**
44
- * Extracts the backend's explanation from a failed response body.
45
- * @param response - The failed HTTP response.
73
+ * Returns the code and message a refused response carries.
74
+ * @param response - The refused HTTP response.
46
75
  * @param fallback - The message to use when the body is unreadable.
47
- * @returns The message explaining the failure.
76
+ * @returns The error code, if any, and its message.
48
77
  */
49
- async function errorMessage(response, fallback) {
78
+ async function errorOf(response, fallback) {
50
79
  try {
51
- return errorSchema.parse(await response.json()).error;
80
+ const held = errorSchema.parse(await response.json());
81
+ return { code: held.code, message: held.error };
52
82
  }
53
83
  catch {
54
- return fallback;
84
+ return { message: fallback };
55
85
  }
56
86
  }
87
+ /**
88
+ * Returns the typed error a refused response's code names.
89
+ * @param refused - The code and message the response carries.
90
+ * @param errors - The error class each known code maps to.
91
+ * @returns The typed error, or undefined for any other code.
92
+ */
93
+ function typedError(refused, errors = roleErrors) {
94
+ const typed = errors.get(refused.code ?? '');
95
+ return typed && new typed(refused.message);
96
+ }
97
+ /**
98
+ * Returns the error a refused account write throws.
99
+ * @param response - The refused HTTP response.
100
+ * @param fallback - The message to use when the body is unreadable.
101
+ * @param write - Names the write in the error for any other status.
102
+ * @returns The error to throw.
103
+ */
104
+ async function writeError(response, fallback, write) {
105
+ const refused = await errorOf(response, fallback);
106
+ const typed = typedError(refused);
107
+ if (typed) {
108
+ return typed;
109
+ }
110
+ if (response.status === 422) {
111
+ return new ValidationError(refused.message);
112
+ }
113
+ return new Error(`${write} failed with status ${response.status}`);
114
+ }
57
115
  /**
58
116
  * Lists the accounts over REST.
59
117
  * @param signal - Aborts the in-flight request.
@@ -86,11 +144,8 @@ async function restCreateUser(input) {
86
144
  if (response.status === 409) {
87
145
  throw new EmailTakenError('email already in use');
88
146
  }
89
- if (response.status === 422) {
90
- throw new ValidationError(await errorMessage(response, __('invalid user details', DOMAIN)));
91
- }
92
147
  if (!response.ok) {
93
- throw new Error(`creating user failed with status ${response.status}`);
148
+ throw await writeError(response, __('invalid user details', DOMAIN), 'creating user');
94
149
  }
95
150
  return userSchema.parse(await response.json());
96
151
  }
@@ -109,14 +164,11 @@ async function restInvite(input) {
109
164
  if (response.status === 401) {
110
165
  throw new UnauthorizedError('session expired');
111
166
  }
112
- if (response.status === 422) {
113
- throw new ValidationError(await errorMessage(response, __('invalid invitation details', DOMAIN)));
114
- }
115
167
  if (response.status === 429) {
116
168
  throw new RateLimitedError('too many attempts');
117
169
  }
118
170
  if (!response.ok) {
119
- throw new Error(`inviting failed with status ${response.status}`);
171
+ throw await writeError(response, __('invalid invitation details', DOMAIN), 'inviting');
120
172
  }
121
173
  return invitationSchema.parse(await response.json());
122
174
  }
@@ -135,7 +187,8 @@ async function restSetUserDisabled(id, disabled) {
135
187
  throw new UnauthorizedError('session expired');
136
188
  }
137
189
  if (!response.ok) {
138
- throw new Error(`updating user failed with status ${response.status}`);
190
+ const refused = await errorOf(response, __('the account could not be updated', DOMAIN));
191
+ throw typedError(refused, accountErrors) ?? new Error(`updating user failed with status ${response.status}`);
139
192
  }
140
193
  }
141
194
  /**
@@ -155,31 +208,8 @@ async function restSetUserRole(id, role) {
155
208
  if (response.status === 401) {
156
209
  throw new UnauthorizedError('session expired');
157
210
  }
158
- const failed = await errorOf(response);
159
- if (failed.code === 'role_insufficient') {
160
- throw new RoleRefusedError(failed.message);
161
- }
162
- if (failed.code === 'self_role_refused') {
163
- throw new SelfRoleError(failed.message);
164
- }
165
- if (failed.code === 'last_privileged_refused') {
166
- throw new LastPrivilegedError(failed.message);
167
- }
168
- throw new Error(`updating role failed with status ${response.status}`);
169
- }
170
- /**
171
- * Returns the code and message a refused response carries.
172
- * @param response - The refused HTTP response.
173
- * @returns The error code, if any, and its message.
174
- */
175
- async function errorOf(response) {
176
- try {
177
- const held = errorSchema.parse(await response.json());
178
- return { code: held.code, message: held.error };
179
- }
180
- catch {
181
- return { message: __('the account could not be updated', DOMAIN) };
182
- }
211
+ const refused = await errorOf(response, __('the account could not be updated', DOMAIN));
212
+ throw typedError(refused, roleChangeErrors) ?? new Error(`updating role failed with status ${response.status}`);
183
213
  }
184
214
  /**
185
215
  * Returns every user account.
package/dist/api.d.ts CHANGED
@@ -4,6 +4,8 @@ declare const userSchema: z.ZodObject<{
4
4
  email: z.ZodString;
5
5
  name: z.ZodString;
6
6
  role: z.ZodDefault<z.ZodString>;
7
+ capabilities: z.ZodOptional<z.ZodArray<z.ZodString>>;
8
+ grantable: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
9
  }, z.core.$strip>;
8
10
  export type User = z.infer<typeof userSchema>;
9
11
  /**
package/dist/api.js CHANGED
@@ -8,6 +8,8 @@ const userSchema = z.object({
8
8
  email: z.string(),
9
9
  name: z.string(),
10
10
  role: z.string().default(''),
11
+ capabilities: z.array(z.string()).optional(),
12
+ grantable: z.array(z.string()).optional(),
11
13
  });
12
14
  /**
13
15
  * InvalidCredentialsError is thrown when the backend rejects a login.
@@ -0,0 +1,8 @@
1
+ import type { User } from './api.js';
2
+ /**
3
+ * Reports whether the user holds the capability, a missing user holding none.
4
+ * @param user - The signed-in user, if any.
5
+ * @param capability - The capability asked for.
6
+ * @returns Whether the user holds the capability.
7
+ */
8
+ export declare function can(user: User | null | undefined, capability: string): boolean;
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Reports whether the user holds the capability, a missing user holding none.
4
+ * @param user - The signed-in user, if any.
5
+ * @param capability - The capability asked for.
6
+ * @returns Whether the user holds the capability.
7
+ */
8
+ export function can(user, capability) {
9
+ return user?.capabilities?.includes(capability) ?? false;
10
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { InvalidCredentialsError, InvalidTokenError, RateLimitedError, UnauthorizedError, ValidationError, acceptInvite, fetchSession, login, logout, requestPasswordReset, resetPassword, } from './api.js';
2
2
  export type { User } from './api.js';
3
3
  export { AuthGate } from './AuthGate.js';
4
+ export { can } from './capabilities.js';
4
5
  export { DOMAIN } from './domain.js';
5
6
  export { catalogFor } from './languages.js';
6
7
  export type { Catalog } from './languages.js';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  export { InvalidCredentialsError, InvalidTokenError, RateLimitedError, UnauthorizedError, ValidationError, acceptInvite, fetchSession, login, logout, requestPasswordReset, resetPassword, } from './api.js';
3
3
  export { AuthGate } from './AuthGate.js';
4
+ export { can } from './capabilities.js';
4
5
  export { DOMAIN } from './domain.js';
5
6
  export { catalogFor } from './languages.js';
6
7
  export { isSessionRevoked } from './probe.js';
package/dist/session.d.ts CHANGED
@@ -13,6 +13,8 @@ export declare function useSession(): import("@tanstack/react-query").UseQueryRe
13
13
  email: string;
14
14
  name: string;
15
15
  role: string;
16
+ capabilities?: string[] | undefined;
17
+ grantable?: string[] | undefined;
16
18
  } | null>, Error>;
17
19
  /**
18
20
  * Ends the current session and drops all cached data belonging to the
@@ -17,6 +17,14 @@ export declare const defaultUser: User;
17
17
  * @returns The account.
18
18
  */
19
19
  export declare function userWithRole(role: string): User;
20
+ /**
21
+ * Returns a canned account holding the role, its capabilities, and the roles it may hand out.
22
+ * @param role - The role the account holds.
23
+ * @param capabilities - The capabilities the role carries.
24
+ * @param grantable - The roles the account may hand out.
25
+ * @returns The account.
26
+ */
27
+ export declare function userWithCapabilities(role: string, capabilities: string[], grantable?: string[]): User;
20
28
  /**
21
29
  * Answers an invitation as mailed.
22
30
  * @returns The msw handler.
@@ -46,6 +46,16 @@ export function userWithRole(role) {
46
46
  role,
47
47
  };
48
48
  }
49
+ /**
50
+ * Returns a canned account holding the role, its capabilities, and the roles it may hand out.
51
+ * @param role - The role the account holds.
52
+ * @param capabilities - The capabilities the role carries.
53
+ * @param grantable - The roles the account may hand out.
54
+ * @returns The account.
55
+ */
56
+ export function userWithCapabilities(role, capabilities, grantable = []) {
57
+ return { ...userWithRole(role), capabilities: [...capabilities], grantable: [...grantable] };
58
+ }
49
59
  /**
50
60
  * Answers an invitation as mailed.
51
61
  * @returns The msw handler.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherium/react-auth",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Session authentication for React apps backed by gouncer's authkit.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -55,7 +55,7 @@
55
55
  "@wordpress/ui": ">=0.23.0 <0.24.0",
56
56
  "msw": "^2.15.0",
57
57
  "react": "^19.2.7",
58
- "vitest": "^4.1.10"
58
+ "vitest": "^4.1.10 || ^5.0.0"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "@testing-library/jest-dom": {
@@ -85,7 +85,7 @@
85
85
  "@types/gettext-parser": "9.0.0",
86
86
  "@types/react": "^19.2.17",
87
87
  "@vitejs/plugin-react": "^6.0.3",
88
- "@vitest/coverage-v8": "^4.1.10",
88
+ "@vitest/coverage-v8": "^5.0.3",
89
89
  "@wordpress/i18n": "6.29.0",
90
90
  "@wordpress/theme": "2.2.0",
91
91
  "@wordpress/ui": "0.23.0",
@@ -95,7 +95,7 @@
95
95
  "eslint-plugin-tsdoc": "^0.4.0",
96
96
  "gettext-extractor": "4.0.6",
97
97
  "gettext-parser": "9.1.1",
98
- "jsdom": "^29.1.1",
98
+ "jsdom": "^30.1.2",
99
99
  "msw": "^2.15.0",
100
100
  "oxlint": "^1.71.0",
101
101
  "react": "^19.2.7",
@@ -103,6 +103,6 @@
103
103
  "typescript": "~6.0.2",
104
104
  "typescript-eslint": "^8.46.4",
105
105
  "vite": "^8.1.4",
106
- "vitest": "^4.1.10"
106
+ "vitest": "^5.0.3"
107
107
  }
108
108
  }