@chidchanun/bcp 0.2.4 → 0.2.6

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.
@@ -0,0 +1,368 @@
1
+ export type AuthorizationMatch =
2
+ | "any"
3
+ | "all";
4
+
5
+ export type PermissionRequirement =
6
+ | string
7
+ | readonly string[];
8
+
9
+ export interface PermissionCheckOptions {
10
+ field?: string;
11
+ match?: AuthorizationMatch;
12
+ }
13
+
14
+ export interface AuthorizationContext<
15
+ TUser extends object = Record<string, unknown>,
16
+ TResource = unknown,
17
+ TData = unknown
18
+ > {
19
+ user: TUser;
20
+ resource?: TResource;
21
+ data?: TData;
22
+ }
23
+
24
+ export type AuthorizationPolicy<
25
+ TUser extends object = Record<string, unknown>,
26
+ TResource = unknown,
27
+ TData = unknown
28
+ > = (
29
+ context: AuthorizationContext<
30
+ TUser,
31
+ TResource,
32
+ TData
33
+ >
34
+ ) => boolean | Promise<boolean>;
35
+
36
+ export class AuthorizationError
37
+ extends Error {
38
+ readonly status = 403;
39
+ readonly code = "FORBIDDEN";
40
+
41
+ constructor(
42
+ message = "Forbidden"
43
+ ) {
44
+ super(message);
45
+ this.name =
46
+ "AuthorizationError";
47
+ }
48
+ }
49
+
50
+ export function getUserPermissions(
51
+ user: object,
52
+ field = "permissions"
53
+ ): string[] {
54
+ const normalizedField =
55
+ normalizeField(
56
+ field
57
+ );
58
+ const value =
59
+ (
60
+ user as
61
+ Record<string, unknown>
62
+ )[normalizedField];
63
+
64
+ if (
65
+ typeof value === "string"
66
+ ) {
67
+ const permission =
68
+ value.trim();
69
+
70
+ return permission
71
+ ? [permission]
72
+ : [];
73
+ }
74
+
75
+ if (
76
+ !Array.isArray(
77
+ value
78
+ )
79
+ ) {
80
+ return [];
81
+ }
82
+
83
+ return Array.from(
84
+ new Set(
85
+ value
86
+ .filter(
87
+ (
88
+ permission
89
+ ): permission is string =>
90
+ typeof permission ===
91
+ "string"
92
+ )
93
+ .map(
94
+ (permission) =>
95
+ permission.trim()
96
+ )
97
+ .filter(Boolean)
98
+ )
99
+ );
100
+ }
101
+
102
+ export function hasPermission(
103
+ user: object,
104
+ required:
105
+ PermissionRequirement,
106
+ options:
107
+ PermissionCheckOptions = {}
108
+ ): boolean {
109
+ const requiredPermissions =
110
+ normalizePermissions(
111
+ required
112
+ );
113
+ const assignedPermissions =
114
+ getUserPermissions(
115
+ user,
116
+ options.field
117
+ );
118
+ const match =
119
+ options.match ??
120
+ "any";
121
+
122
+ assertAuthorizationMatch(
123
+ match
124
+ );
125
+
126
+ return match === "all"
127
+ ? requiredPermissions.every(
128
+ (permission) =>
129
+ assignedPermissions.includes(
130
+ permission
131
+ )
132
+ )
133
+ : requiredPermissions.some(
134
+ (permission) =>
135
+ assignedPermissions.includes(
136
+ permission
137
+ )
138
+ );
139
+ }
140
+
141
+ export function assertPermission(
142
+ user: object,
143
+ required:
144
+ PermissionRequirement,
145
+ options:
146
+ PermissionCheckOptions = {}
147
+ ): void {
148
+ if (
149
+ !hasPermission(
150
+ user,
151
+ required,
152
+ options
153
+ )
154
+ ) {
155
+ throw new AuthorizationError();
156
+ }
157
+ }
158
+
159
+ export function defineAuthorizationPolicy<
160
+ TUser extends object,
161
+ TResource = unknown,
162
+ TData = unknown
163
+ >(
164
+ policy:
165
+ AuthorizationPolicy<
166
+ TUser,
167
+ TResource,
168
+ TData
169
+ >
170
+ ): AuthorizationPolicy<
171
+ TUser,
172
+ TResource,
173
+ TData
174
+ > {
175
+ if (
176
+ typeof policy !==
177
+ "function"
178
+ ) {
179
+ throw new TypeError(
180
+ "BCP Authorization: policy must be a function."
181
+ );
182
+ }
183
+
184
+ return policy;
185
+ }
186
+
187
+ export async function can<
188
+ TUser extends object,
189
+ TResource = unknown,
190
+ TData = unknown
191
+ >(
192
+ policy:
193
+ AuthorizationPolicy<
194
+ TUser,
195
+ TResource,
196
+ TData
197
+ >,
198
+ context:
199
+ AuthorizationContext<
200
+ TUser,
201
+ TResource,
202
+ TData
203
+ >
204
+ ): Promise<boolean> {
205
+ assertAuthorizationContext(
206
+ context
207
+ );
208
+
209
+ return Boolean(
210
+ await policy(
211
+ context
212
+ )
213
+ );
214
+ }
215
+
216
+ export async function cannot<
217
+ TUser extends object,
218
+ TResource = unknown,
219
+ TData = unknown
220
+ >(
221
+ policy:
222
+ AuthorizationPolicy<
223
+ TUser,
224
+ TResource,
225
+ TData
226
+ >,
227
+ context:
228
+ AuthorizationContext<
229
+ TUser,
230
+ TResource,
231
+ TData
232
+ >
233
+ ): Promise<boolean> {
234
+ return !await can(
235
+ policy,
236
+ context
237
+ );
238
+ }
239
+
240
+ export async function authorize<
241
+ TUser extends object,
242
+ TResource = unknown,
243
+ TData = unknown
244
+ >(
245
+ policy:
246
+ AuthorizationPolicy<
247
+ TUser,
248
+ TResource,
249
+ TData
250
+ >,
251
+ context:
252
+ AuthorizationContext<
253
+ TUser,
254
+ TResource,
255
+ TData
256
+ >,
257
+ message = "Forbidden"
258
+ ): Promise<void> {
259
+ if (
260
+ !await can(
261
+ policy,
262
+ context
263
+ )
264
+ ) {
265
+ throw new AuthorizationError(
266
+ message
267
+ );
268
+ }
269
+ }
270
+
271
+ function normalizePermissions(
272
+ value:
273
+ PermissionRequirement
274
+ ): string[] {
275
+ const permissions =
276
+ typeof value === "string"
277
+ ? [value]
278
+ : [
279
+ ...value,
280
+ ];
281
+ const normalized =
282
+ permissions.map(
283
+ (permission) =>
284
+ permission.trim()
285
+ );
286
+
287
+ if (
288
+ normalized.length === 0 ||
289
+ normalized.some(
290
+ (permission) =>
291
+ permission.length === 0
292
+ )
293
+ ) {
294
+ throw new TypeError(
295
+ "BCP Authorization: required permissions must contain non-empty strings."
296
+ );
297
+ }
298
+
299
+ return Array.from(
300
+ new Set(
301
+ normalized
302
+ )
303
+ );
304
+ }
305
+
306
+ function normalizeField(
307
+ value: string
308
+ ): string {
309
+ const field =
310
+ value.trim();
311
+
312
+ if (!field) {
313
+ throw new TypeError(
314
+ "BCP Authorization: permission field must be a non-empty string."
315
+ );
316
+ }
317
+
318
+ return field;
319
+ }
320
+
321
+ function assertAuthorizationMatch(
322
+ value: string
323
+ ): asserts value is AuthorizationMatch {
324
+ if (
325
+ value !== "any" &&
326
+ value !== "all"
327
+ ) {
328
+ throw new TypeError(
329
+ "BCP Authorization: match must be either \"any\" or \"all\"."
330
+ );
331
+ }
332
+ }
333
+
334
+ function assertAuthorizationContext(
335
+ value: unknown
336
+ ): asserts value is AuthorizationContext<object> {
337
+ if (
338
+ !value ||
339
+ typeof value !== "object" ||
340
+ Array.isArray(
341
+ value
342
+ ) ||
343
+ !(
344
+ "user" in value
345
+ ) ||
346
+ !(
347
+ value as {
348
+ user?: unknown;
349
+ }
350
+ ).user ||
351
+ typeof (
352
+ value as {
353
+ user: unknown;
354
+ }
355
+ ).user !== "object" ||
356
+ Array.isArray(
357
+ (
358
+ value as {
359
+ user: unknown;
360
+ }
361
+ ).user
362
+ )
363
+ ) {
364
+ throw new TypeError(
365
+ "BCP Authorization: context must contain a user object."
366
+ );
367
+ }
368
+ }