endpoint-permissions-kit 0.1.0 → 0.2.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.
package/dist/esm/index.js CHANGED
@@ -1,424 +1,591 @@
1
1
  // src/constants.ts
2
- var METHODS = ["find", "update", "create", "remove"];
3
- var GENERAL_ROLE = "general";
4
- var GLOBAL_HOOK_OWNER = "*";
5
- var ALL_FIELDS = "*";
6
- var MODULE_SEPARATOR = ".";
7
- var PERMISSION_ID_SEPARATOR = "::";
2
+ var constants = {
3
+ METHODS: Object.freeze(["find", "update", "create", "remove"]),
4
+ GENERAL_ROLE: "general",
5
+ GLOBAL_HOOK_OWNER: "*",
6
+ ALL_FIELDS: "*",
7
+ MODULE_SEPARATOR: ".",
8
+ PERMISSION_ID_SEPARATOR: "::"
9
+ };
10
+ var constants_default = constants;
8
11
 
9
- // src/state.ts
10
- function getOrCreateState() {
11
- const stateKey = Symbol.for("endpoint-permissions-kit");
12
- const stateHost = globalThis;
13
- if (stateHost[stateKey] === undefined) {
14
- stateHost[stateKey] = { roles: new Set([GENERAL_ROLE]), modules: new Map, snapshot: null };
12
+ // src/errors.ts
13
+ var errors = {
14
+ create(code, message) {
15
+ return Object.assign(new Error(message), { name: "PkitError", code });
16
+ },
17
+ describe(value) {
18
+ try {
19
+ return String(value);
20
+ } catch {
21
+ return typeof value;
22
+ }
15
23
  }
16
- return stateHost[stateKey];
17
- }
24
+ };
25
+ var errors_default = errors;
18
26
 
19
- // src/errors.ts
20
- function pkitError(code, message) {
21
- return Object.assign(new Error(message), { name: "PkitError", code });
27
+ // src/identifiers.ts
28
+ var identifiers = {
29
+ build(role, action, name) {
30
+ return `${role}${constants_default.PERMISSION_ID_SEPARATOR}${action}${constants_default.PERMISSION_ID_SEPARATOR}${name}`;
31
+ },
32
+ parse(value, code, subject) {
33
+ if (typeof value !== "string")
34
+ throw invalidIdentifier(value, code, subject);
35
+ const parts = value.split(constants_default.PERMISSION_ID_SEPARATOR);
36
+ const [role, action, name] = parts;
37
+ if (parts.length !== 3 || role === undefined || action === undefined || name === undefined) {
38
+ throw invalidIdentifier(value, code, subject);
39
+ }
40
+ const isValidRole = isCleanSegment(role) && role !== constants_default.GLOBAL_HOOK_OWNER;
41
+ const isValidName = isCleanSegment(name) && name !== constants_default.GLOBAL_HOOK_OWNER;
42
+ const isValidAction = action.split(constants_default.MODULE_SEPARATOR).every(isCleanSegment);
43
+ if (!isValidRole || !isValidName || !isValidAction)
44
+ throw invalidIdentifier(value, code, subject);
45
+ return { role, action, name };
46
+ },
47
+ checkRole(role) {
48
+ if (typeof role === "string" && isCleanSegment(role) && role !== constants_default.GLOBAL_HOOK_OWNER)
49
+ return;
50
+ throw errors_default.create("INVALID_DEFINITION", `invalid role: "${errors_default.describe(role)}" (non-empty string, no ":" or surrounding whitespace; "${constants_default.GLOBAL_HOOK_OWNER}" is reserved for global hooks)`);
51
+ },
52
+ checkModuleSegment(segment) {
53
+ if (typeof segment === "string" && isCleanSegment(segment) && !segment.includes(constants_default.MODULE_SEPARATOR))
54
+ return;
55
+ throw errors_default.create("INVALID_DEFINITION", `invalid module name: "${errors_default.describe(segment)}" (non-empty string, no dots, no ":" or surrounding whitespace)`);
56
+ },
57
+ checkName(name) {
58
+ if (typeof name === "string" && isCleanSegment(name) && name !== constants_default.GLOBAL_HOOK_OWNER)
59
+ return;
60
+ throw errors_default.create("INVALID_DEFINITION", `invalid permission name: "${errors_default.describe(name)}" (non-empty string, no ":" or surrounding whitespace; "*" is reserved)`);
61
+ }
62
+ };
63
+ function isCleanSegment(value) {
64
+ return value.length > 0 && value.trim() === value && !value.includes(":");
65
+ }
66
+ function invalidIdentifier(value, code, subject) {
67
+ return errors_default.create(code, `${subject}: "${errors_default.describe(value)}" (format role::module::name)`);
22
68
  }
69
+ var identifiers_default = identifiers;
23
70
 
24
- // src/resolve.ts
25
- var DIRECT_AUTHORIZATION = Object.freeze({ direct: true, grantedBy: Object.freeze([]) });
26
- var DISABLED_ACCESS = Object.freeze({ status: "disabled" });
27
- var UNASSIGNED_ACCESS = Object.freeze({ status: "unassigned" });
28
- function permissionIdOf(role, action, name) {
29
- return [role, action, name].join(PERMISSION_ID_SEPARATOR);
30
- }
31
- function resolveAccess(nameEntry, permissionId, role, method, assigned) {
32
- if (assigned.has(permissionId))
33
- return resolveDirectAccess(nameEntry, role, method);
34
- return resolveGrantedAccess(nameEntry, method, assigned);
35
- }
36
- function resolveDirectAccess(nameEntry, role, method) {
37
- const definition = nameEntry.actions.get(role)?.[method];
38
- if (!definition?.enabled)
39
- return DISABLED_ACCESS;
40
- return { status: "granted", properties: definition.properties, authorization: DIRECT_AUTHORIZATION };
41
- }
42
- function resolveGrantedAccess(nameEntry, method, assigned) {
43
- const grantedBy = [];
44
- const properties = new Set;
45
- let reachable = false;
46
- for (const [enablingId, grant] of nameEntry.grants) {
47
- if (!assigned.has(enablingId))
71
+ // src/properties.ts
72
+ var PRUNED = Symbol("pruned");
73
+ var MAX_DEPTH = 1000;
74
+ var properties = {
75
+ checkDeclaredPath(property, permissionPath) {
76
+ if (property.includes("[") || property.includes("]")) {
77
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: array indexes are not allowed in properties: "${property}"`);
78
+ }
79
+ const segments = segmentsOf(property);
80
+ for (const segment of segments) {
81
+ if (segment.length === 0)
82
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: empty segment in property path: "${property}"`);
83
+ }
84
+ if (segments[0] === constants_default.ALL_FIELDS) {
85
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: a property path cannot start with "${constants_default.ALL_FIELDS}": "${property}"`);
86
+ }
87
+ },
88
+ resolve(data, allowed, reserved, cropper) {
89
+ if (allowed === constants_default.ALL_FIELDS)
90
+ return data;
91
+ const patterns = [];
92
+ for (const pattern of allowed.concat(reserved))
93
+ patterns.push(segmentsOf(pattern));
94
+ const walk = { patterns, segments: [], ancestors: new Set([data]) };
95
+ if (cropper)
96
+ return cropData(data, walk);
97
+ rejectForbiddenPaths(data, walk);
98
+ return data;
99
+ }
100
+ };
101
+ function segmentsOf(path) {
102
+ const segments = [];
103
+ let segment = "";
104
+ let index = 0;
105
+ while (index < path.length) {
106
+ const character = path[index];
107
+ if (character === "\\") {
108
+ segment += path[index + 1] ?? "";
109
+ index += 2;
48
110
  continue;
49
- reachable = true;
50
- const definition = grant.actions[method];
51
- if (!definition)
111
+ }
112
+ if (character === ".") {
113
+ segments.push(segment);
114
+ segment = "";
115
+ index += 1;
52
116
  continue;
53
- grantedBy.push(enablingId);
54
- for (const field of definition.properties)
55
- properties.add(field);
56
- }
57
- if (grantedBy.length === 0)
58
- return reachable ? DISABLED_ACCESS : UNASSIGNED_ACCESS;
59
- const authorization = Object.freeze({ direct: false, grantedBy: Object.freeze(grantedBy.sort()) });
60
- return { status: "granted", properties: Object.freeze([...properties]), authorization };
61
- }
62
-
63
- // src/validators.ts
64
- function validateObject(value, failure) {
65
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
66
- throw pkitError(failure.code, failure.message);
117
+ }
118
+ segment += character;
119
+ index += 1;
67
120
  }
121
+ segments.push(segment);
122
+ return segments;
68
123
  }
69
- function validateStrings(value, rules) {
70
- if (!Array.isArray(value))
71
- throw pkitError(rules.code, rules.message);
72
- for (const field of value) {
73
- if (typeof field !== "string" || field.length < rules.minimumLength) {
74
- throw pkitError(rules.code, rules.message);
124
+ function isAllowedPath(segments, patterns) {
125
+ for (const pattern of patterns) {
126
+ if (pattern.length !== segments.length)
127
+ continue;
128
+ let isMatch = true;
129
+ for (let index = 0;index < segments.length; index += 1) {
130
+ const patternSegment = pattern[index];
131
+ if (patternSegment === constants_default.ALL_FIELDS)
132
+ continue;
133
+ if (patternSegment === segments[index])
134
+ continue;
135
+ isMatch = false;
136
+ break;
75
137
  }
138
+ if (isMatch)
139
+ return true;
76
140
  }
141
+ return false;
77
142
  }
78
- function validateContextKey(key) {
79
- if (key !== "roles")
80
- throw pkitError("INVALID_DEFINITION", `unknown context key: "${String(key)}"`);
81
- }
82
- function validateOpenRegistry(state) {
83
- if (state.snapshot)
84
- throw pkitError("SEALED", "pkit.seal() was already called: no more registrations allowed");
143
+ function isPlainObject(value) {
144
+ if (value === null || typeof value !== "object" || Array.isArray(value))
145
+ return false;
146
+ const prototype = Object.getPrototypeOf(value);
147
+ return prototype === Object.prototype || prototype === null;
85
148
  }
86
- function validateSnapshot(snapshot) {
87
- if (snapshot === null) {
88
- throw pkitError("NOT_SEALED", "pkit.seal() has not been called: call it after importing every permission file");
89
- }
149
+ function requireDepth(walk) {
150
+ if (walk.ancestors.size < MAX_DEPTH)
151
+ return;
152
+ throw new RangeError(`data is nested deeper than ${MAX_DEPTH} levels`);
90
153
  }
91
- function validateRole(role, roles, code) {
92
- if (typeof role === "string" && roles.has(role))
154
+ function setField(target, key, value) {
155
+ if (key === "__proto__") {
156
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
93
157
  return;
94
- if (code === "ROLE_NOT_DECLARED") {
95
- throw pkitError(code, `role "${String(role)}" is not declared. Available roles: ${[...roles].join(", ")}.
96
- ` + "Is pkit.context.set('roles', [...]) missing from pkit.config.js, or was it imported after this file?");
97
158
  }
98
- throw pkitError(code, `role "${String(role)}" is not declared`);
99
- }
100
- function validateRoleCatalog(roles, state) {
101
- validateOpenRegistry(state);
102
- if (state.modules.size) {
103
- throw pkitError("INVALID_DEFINITION", "roles must be declared before registering permissions: import pkit.config.js first");
159
+ target[key] = value;
160
+ }
161
+ function cropValue(value, walk) {
162
+ if (Array.isArray(value)) {
163
+ if (value.length === 0)
164
+ return isAllowedPath(walk.segments, walk.patterns) ? [] : PRUNED;
165
+ if (walk.ancestors.has(value))
166
+ throw new RangeError("data contains a circular reference");
167
+ requireDepth(walk);
168
+ walk.ancestors.add(value);
169
+ const items = [];
170
+ for (const item of value) {
171
+ const croppedItem = cropValue(item, walk);
172
+ if (croppedItem === PRUNED)
173
+ continue;
174
+ items.push(croppedItem);
175
+ }
176
+ walk.ancestors.delete(value);
177
+ return items.length === 0 ? PRUNED : items;
104
178
  }
105
- validateStrings(roles, {
106
- code: "INVALID_DEFINITION",
107
- message: "roles must be an array of non-empty strings",
108
- minimumLength: 1
109
- });
110
- if (roles.length === 0)
111
- throw pkitError("INVALID_DEFINITION", "roles must declare at least one role");
112
- for (const role of roles)
113
- validateRoleSegment(role);
114
- }
115
- function isCleanSegment(value) {
116
- return value.length > 0 && value.trim() === value && !value.includes(":");
179
+ if (isPlainObject(value)) {
180
+ const container = value;
181
+ const keys = Object.keys(container);
182
+ if (keys.length === 0)
183
+ return isAllowedPath(walk.segments, walk.patterns) ? {} : PRUNED;
184
+ if (walk.ancestors.has(container))
185
+ throw new RangeError("data contains a circular reference");
186
+ requireDepth(walk);
187
+ walk.ancestors.add(container);
188
+ const cropped = {};
189
+ let keptCount = 0;
190
+ for (const key of keys) {
191
+ walk.segments.push(key);
192
+ const croppedValue = cropValue(container[key], walk);
193
+ walk.segments.pop();
194
+ if (croppedValue === PRUNED)
195
+ continue;
196
+ setField(cropped, key, croppedValue);
197
+ keptCount += 1;
198
+ }
199
+ walk.ancestors.delete(container);
200
+ return keptCount === 0 ? PRUNED : cropped;
201
+ }
202
+ return isAllowedPath(walk.segments, walk.patterns) ? value : PRUNED;
203
+ }
204
+ function cropData(data, walk) {
205
+ const cropped = {};
206
+ for (const key of Object.keys(data)) {
207
+ walk.segments.push(key);
208
+ const croppedValue = cropValue(data[key], walk);
209
+ walk.segments.pop();
210
+ if (croppedValue === PRUNED)
211
+ continue;
212
+ setField(cropped, key, croppedValue);
213
+ }
214
+ return cropped;
117
215
  }
118
- function validateRoleSegment(role) {
119
- if (typeof role === "string" && isCleanSegment(role) && role !== GLOBAL_HOOK_OWNER)
216
+ function collectForbiddenPaths(value, walk, forbidden) {
217
+ if (Array.isArray(value)) {
218
+ if (value.length === 0 || walk.ancestors.has(value)) {
219
+ reportPath(walk, forbidden);
220
+ return;
221
+ }
222
+ requireDepth(walk);
223
+ walk.ancestors.add(value);
224
+ for (const item of value)
225
+ collectForbiddenPaths(item, walk, forbidden);
226
+ walk.ancestors.delete(value);
120
227
  return;
121
- throw pkitError("INVALID_DEFINITION", `invalid role: "${String(role)}" (non-empty string, no ":" or surrounding whitespace; "${GLOBAL_HOOK_OWNER}" is reserved for global hooks)`);
122
- }
123
- function validateModuleName(moduleName) {
124
- if (typeof moduleName === "string" && isCleanSegment(moduleName) && !moduleName.includes(MODULE_SEPARATOR))
228
+ }
229
+ if (isPlainObject(value)) {
230
+ const container = value;
231
+ const keys = Object.keys(container);
232
+ if (keys.length === 0 || walk.ancestors.has(container)) {
233
+ reportPath(walk, forbidden);
234
+ return;
235
+ }
236
+ requireDepth(walk);
237
+ walk.ancestors.add(container);
238
+ for (const key of keys) {
239
+ walk.segments.push(key);
240
+ collectForbiddenPaths(container[key], walk, forbidden);
241
+ walk.segments.pop();
242
+ }
243
+ walk.ancestors.delete(container);
125
244
  return;
126
- throw pkitError("INVALID_DEFINITION", `invalid module name: "${String(moduleName)}" (non-empty string, no dots, no ":" or surrounding whitespace)`);
245
+ }
246
+ reportPath(walk, forbidden);
127
247
  }
128
- function validatePermissionName(name) {
129
- if (typeof name === "string" && isCleanSegment(name) && name !== GLOBAL_HOOK_OWNER)
248
+ function reportPath(walk, forbidden) {
249
+ if (isAllowedPath(walk.segments, walk.patterns))
130
250
  return;
131
- throw pkitError("INVALID_DEFINITION", `invalid permission name: "${String(name)}" (non-empty string, no ":" or surrounding whitespace; "*" is reserved)`);
132
- }
133
- function parsePermissionId(value, failure) {
134
- if (typeof value !== "string")
135
- throw pkitError(failure.code, failure.message);
136
- const parts = value.split(PERMISSION_ID_SEPARATOR);
137
- const [role, action, name] = parts;
138
- if (parts.length !== 3 || role === undefined || action === undefined || name === undefined) {
139
- throw pkitError(failure.code, failure.message);
140
- }
141
- const isValidRole = isCleanSegment(role) && role !== GLOBAL_HOOK_OWNER;
142
- const isValidName = isCleanSegment(name) && name !== GLOBAL_HOOK_OWNER;
143
- const isValidAction = action.split(MODULE_SEPARATOR).every(isCleanSegment);
144
- if (!isValidRole || !isValidName || !isValidAction)
145
- throw pkitError(failure.code, failure.message);
146
- return { role, action, name };
147
- }
148
- function validateMethod(method, failure) {
149
- if (METHODS.includes(method))
251
+ forbidden.add(walk.segments.join("."));
252
+ }
253
+ function rejectForbiddenPaths(data, walk) {
254
+ const forbidden = new Set;
255
+ for (const key of Object.keys(data)) {
256
+ walk.segments.push(key);
257
+ collectForbiddenPaths(data[key], walk, forbidden);
258
+ walk.segments.pop();
259
+ }
260
+ if (forbidden.size === 0)
150
261
  return;
151
- throw pkitError(failure.code, `${failure.message}: method "${String(method)}" does not exist. Methods: ${METHODS.join(", ")}`);
262
+ const fields = [...forbidden];
263
+ const error = errors_default.create("PROPERTIES_NOT_ALLOWED", `fields not allowed: ${fields.join(", ")}`);
264
+ throw Object.assign(error, { fields });
152
265
  }
153
- function readActionDefinition(definition, permissionPath) {
154
- validateObject(definition, { code: "INVALID_DEFINITION", message: `${permissionPath}: must be an object` });
155
- if (typeof definition.enabled !== "boolean") {
156
- throw pkitError("INVALID_DEFINITION", `${permissionPath}: enabled must be a boolean`);
157
- }
158
- const { properties } = definition;
159
- if (properties === ALL_FIELDS)
160
- return Object.freeze({ enabled: definition.enabled, properties });
161
- validateStrings(properties, {
162
- code: "INVALID_DEFINITION",
163
- message: `${permissionPath}: properties must be string[] or '*'`,
164
- minimumLength: 1
165
- });
166
- if (properties.includes(ALL_FIELDS)) {
167
- throw pkitError("INVALID_DEFINITION", `${permissionPath}: '${ALL_FIELDS}' is only allowed as the whole properties value`);
168
- }
169
- if (new Set(properties).size !== properties.length) {
170
- throw pkitError("INVALID_DEFINITION", `${permissionPath}: properties contains duplicate fields`);
171
- }
172
- return Object.freeze({ enabled: definition.enabled, properties: Object.freeze([...properties]) });
173
- }
174
- function readActionDefinitions(actions, permissionPath) {
175
- validateObject(actions, { code: "INVALID_DEFINITION", message: `${permissionPath}: registerActions expects an object` });
176
- const registeredActions = Object.create(null);
177
- for (const [method, definition] of Object.entries(actions)) {
178
- validateMethod(method, { code: "INVALID_DEFINITION", message: permissionPath });
179
- registeredActions[method] = readActionDefinition(definition, `${permissionPath}.${method}`);
180
- }
181
- return Object.freeze(registeredActions);
182
- }
183
- function validateActions(actions, registration, state) {
184
- const { action, name, role } = registration;
185
- validateOpenRegistry(state);
186
- validateRole(role, state.roles, "ROLE_NOT_DECLARED");
187
- if (state.modules.get(action)?.names.get(name)?.actions.has(role)) {
188
- throw pkitError("DUPLICATE_REGISTRATION", `"${action}::${name}" already has actions registered for role "${role}"`);
189
- }
190
- return readActionDefinitions(actions, `${action}::${name} [${role}]`);
191
- }
192
- function validateGrantActions(actions, registration, state) {
193
- const { action, name, permissionId } = registration;
194
- validateOpenRegistry(state);
195
- const source = parsePermissionId(permissionId, {
196
- code: "INVALID_DEFINITION",
197
- message: `"${action}::${name}": invalid grantTo identifier: "${String(permissionId)}" (format role::module::name)`
198
- });
199
- validateRole(source.role, state.roles, "ROLE_NOT_DECLARED");
200
- if (source.action === action && source.name === name) {
201
- throw pkitError("INVALID_DEFINITION", `"${permissionId}" cannot grant to itself`);
202
- }
203
- if (state.modules.get(action)?.names.get(name)?.grants.has(permissionId)) {
204
- throw pkitError("DUPLICATE_REGISTRATION", `"${action}::${name}" already has a grant for "${permissionId}"`);
266
+ var properties_default = properties;
267
+
268
+ // src/state.ts
269
+ var STATE_KEY = Symbol.for("endpoint-permissions-kit");
270
+ var state = {
271
+ getOrCreate() {
272
+ const stateHost = globalThis;
273
+ if (stateHost[STATE_KEY] === undefined) {
274
+ stateHost[STATE_KEY] = {
275
+ roles: new Set([constants_default.GENERAL_ROLE]),
276
+ cropper: false,
277
+ reservedFields: [],
278
+ modules: new Map,
279
+ snapshot: null
280
+ };
281
+ }
282
+ return stateHost[STATE_KEY];
283
+ },
284
+ requireOpen(currentState) {
285
+ if (currentState.snapshot)
286
+ throw errors_default.create("SEALED", "pkit.seal() was already called: no more registrations allowed");
287
+ },
288
+ requireSnapshot(currentState) {
289
+ if (currentState.snapshot === null) {
290
+ throw errors_default.create("NOT_SEALED", "pkit.seal() has not been called: call it after importing every permission file");
291
+ }
292
+ return currentState.snapshot;
205
293
  }
206
- const permissionPath = `${action}::${name} [grantTo ${permissionId}]`;
207
- const registeredActions = readActionDefinitions(actions, permissionPath);
208
- for (const [method, definition] of Object.entries(registeredActions)) {
209
- if (definition.enabled !== true) {
210
- throw pkitError("INVALID_DEFINITION", `${permissionPath}.${method}: a grant does not allow enabled: false`);
294
+ };
295
+ var state_default = state;
296
+
297
+ // src/context.ts
298
+ var context = Object.freeze({
299
+ set(key, value) {
300
+ const contextKey = checkKey(key);
301
+ const currentState = state_default.getOrCreate();
302
+ state_default.requireOpen(currentState);
303
+ if (contextKey === "cropper") {
304
+ if (typeof value !== "boolean")
305
+ throw errors_default.create("INVALID_DEFINITION", "cropper must be a boolean");
306
+ currentState.cropper = value;
307
+ return;
211
308
  }
212
- if (definition.properties === ALL_FIELDS) {
213
- throw pkitError("INVALID_DEFINITION", `${permissionPath}.${method}: a grant requires an explicit properties list`);
309
+ if (contextKey === "reservedFields") {
310
+ currentState.reservedFields = Object.freeze(readReservedFields(value));
311
+ return;
214
312
  }
313
+ currentState.roles = new Set(readRoleCatalog(value, currentState.modules.size));
314
+ },
315
+ get(key) {
316
+ const contextKey = checkKey(key);
317
+ const currentState = state_default.getOrCreate();
318
+ if (contextKey === "cropper")
319
+ return currentState.cropper;
320
+ const paths = contextKey === "roles" ? [...currentState.roles] : [...currentState.reservedFields];
321
+ return paths;
215
322
  }
216
- return { source, actions: registeredActions };
217
- }
218
- function validateHook(hook, registration, state) {
219
- const { action, name, role, method } = registration;
220
- validateOpenRegistry(state);
221
- if (role !== undefined && role !== GLOBAL_HOOK_OWNER)
222
- validateRole(role, state.roles, "ROLE_NOT_DECLARED");
223
- const permissionPath = name === undefined ? action : `${action}::${name}`;
224
- validateMethod(method, { code: "INVALID_DEFINITION", message: permissionPath });
225
- if (typeof hook !== "function")
226
- throw pkitError("INVALID_DEFINITION", `${permissionPath}: hook("${method}") expects a function`);
227
- }
228
- function declaresMethod(nameEntry, method) {
229
- for (const actions of nameEntry.actions.values()) {
230
- if (Object.hasOwn(actions, method))
231
- return true;
323
+ });
324
+ function checkKey(key) {
325
+ if (key === "roles" || key === "cropper" || key === "reservedFields")
326
+ return key;
327
+ throw errors_default.create("INVALID_DEFINITION", `unknown context key: "${errors_default.describe(key)}"`);
328
+ }
329
+ function readRoleCatalog(value, registeredModuleCount) {
330
+ if (registeredModuleCount > 0) {
331
+ throw errors_default.create("INVALID_DEFINITION", "roles must be declared before registering permissions: import pkit.config.js first");
232
332
  }
233
- return false;
234
- }
235
- function hasAccessPath(nameEntry, role, method) {
236
- if (nameEntry.actions.get(role)?.[method])
237
- return true;
238
- for (const grant of nameEntry.grants.values()) {
239
- if (grant.source.role === role && grant.actions[method])
240
- return true;
333
+ if (!Array.isArray(value))
334
+ throw errors_default.create("INVALID_DEFINITION", "roles must be an array of non-empty strings");
335
+ for (const role of value) {
336
+ if (typeof role !== "string" || role.length === 0) {
337
+ throw errors_default.create("INVALID_DEFINITION", "roles must be an array of non-empty strings");
338
+ }
241
339
  }
242
- return false;
340
+ const catalog = value;
341
+ if (catalog.length === 0)
342
+ throw errors_default.create("INVALID_DEFINITION", "roles must declare at least one role");
343
+ for (const role of catalog)
344
+ identifiers_default.checkRole(role);
345
+ return catalog;
346
+ }
347
+ function readReservedFields(value) {
348
+ if (!Array.isArray(value))
349
+ throw errors_default.create("INVALID_DEFINITION", "reservedFields must be an array of property paths");
350
+ const fields = [];
351
+ for (const field of value) {
352
+ if (typeof field !== "string")
353
+ throw errors_default.create("INVALID_DEFINITION", "reservedFields must be an array of property paths");
354
+ properties_default.checkDeclaredPath(field, "reservedFields");
355
+ fields.push(field);
356
+ }
357
+ return fields;
243
358
  }
244
- function validateGrantReferences(permissionPath, nameEntry, modules) {
245
- for (const [permissionId, grant] of nameEntry.grants) {
246
- const sourceEntry = modules.get(grant.source.action)?.names.get(grant.source.name);
247
- if (!sourceEntry?.actions.has(grant.source.role)) {
248
- throw pkitError("INVALID_DEFINITION", `"${permissionPath}": grantTo "${permissionId}" references a permission with no registered actions`);
359
+ var context_default = context;
360
+
361
+ // src/resolve.ts
362
+ var DISABLED_ACCESS = Object.freeze({ status: "disabled" });
363
+ var UNASSIGNED_ACCESS = Object.freeze({ status: "unassigned" });
364
+ var resolve = {
365
+ identity(assignments, snapshot, roles) {
366
+ if (assignments === null || typeof assignments !== "object")
367
+ throw errors_default.create("INVALID_INPUT", "assignments must be an object");
368
+ const { role, permissions } = assignments;
369
+ if (typeof role !== "string")
370
+ throw errors_default.create("INVALID_INPUT", "role is required");
371
+ if (!roles.has(role))
372
+ throw errors_default.create("UNKNOWN_ROLE", `role "${role}" is not declared`);
373
+ if (!Array.isArray(permissions))
374
+ throw errors_default.create("INVALID_INPUT", "permissions must be an array of strings");
375
+ const assigned = new Set;
376
+ const names = new Map;
377
+ for (const permissionId of permissions) {
378
+ const reference = identifiers_default.parse(permissionId, "INVALID_INPUT", "invalid permission identifier");
379
+ if (reference.role !== role) {
380
+ throw errors_default.create("PERMISSION_ROLE_MISMATCH", `"${permissionId}" belongs to role "${reference.role}", not the authenticated role "${role}"`);
381
+ }
382
+ if (!snapshot.assignable.has(permissionId)) {
383
+ throw errors_default.create("UNKNOWN_PERMISSION", `"${permissionId}" is not an assignable permission`);
384
+ }
385
+ const assignedName = names.get(reference.action);
386
+ if (assignedName !== undefined && assignedName !== reference.name) {
387
+ throw errors_default.create("AMBIGUOUS_PERMISSION", `"${permissionId}" and "${identifiers_default.build(role, reference.action, assignedName)}" are both assigned: a request names no permission name, so module "${reference.action}" cannot be resolved`);
388
+ }
389
+ assigned.add(permissionId);
390
+ names.set(reference.action, reference.name);
249
391
  }
250
- for (const method of Object.keys(grant.actions)) {
251
- if (declaresMethod(nameEntry, method))
392
+ return { role, assigned, names };
393
+ },
394
+ permission(registeredModule, action, identity) {
395
+ const assignedName = identity.names.get(action);
396
+ if (assignedName !== undefined) {
397
+ const assignedEntry = registeredModule.names.get(assignedName);
398
+ if (assignedEntry === undefined)
399
+ return;
400
+ return { permissionId: identifiers_default.build(identity.role, action, assignedName), entry: assignedEntry };
401
+ }
402
+ let chosen;
403
+ for (const [name, nameEntry] of registeredModule.names) {
404
+ const permissionId = identifiers_default.build(identity.role, action, name);
405
+ if (resolve.methodAccess(nameEntry, permissionId, identity.role, identity.assigned) === undefined)
406
+ continue;
407
+ if (chosen !== undefined) {
408
+ throw errors_default.create("AMBIGUOUS_PERMISSION", `grants reach "${chosen.permissionId}" and "${permissionId}": a request names no permission name, so module "${action}" cannot be resolved`);
409
+ }
410
+ chosen = { permissionId, entry: nameEntry };
411
+ }
412
+ return chosen;
413
+ },
414
+ access(nameEntry, permissionId, role, method, assigned) {
415
+ if (!assigned.has(permissionId))
416
+ return grantedAccess(nameEntry, method, assigned);
417
+ const definition = nameEntry.actions.get(role)?.[method];
418
+ if (!definition?.enabled)
419
+ return DISABLED_ACCESS;
420
+ return { status: "granted", properties: definition.properties };
421
+ },
422
+ methodAccess(nameEntry, permissionId, role, assigned) {
423
+ const methodAccess = Object.create(null);
424
+ if (assigned.has(permissionId)) {
425
+ const actions = nameEntry.actions.get(role);
426
+ for (const method of constants_default.METHODS)
427
+ methodAccess[method] = actions?.[method]?.enabled === true;
428
+ return Object.freeze(methodAccess);
429
+ }
430
+ const grantedMethods = new Set;
431
+ let isReachable = false;
432
+ for (const [enablingId, grant] of nameEntry.grants) {
433
+ if (!assigned.has(enablingId))
252
434
  continue;
253
- throw pkitError("INVALID_DEFINITION", `"${permissionPath}": grantTo "${permissionId}" grants "${method}", which no role declares on that permission`);
435
+ isReachable = true;
436
+ for (const method of Object.keys(grant.actions))
437
+ grantedMethods.add(method);
254
438
  }
439
+ if (!isReachable)
440
+ return;
441
+ for (const method of constants_default.METHODS)
442
+ methodAccess[method] = grantedMethods.has(method);
443
+ return Object.freeze(methodAccess);
255
444
  }
256
- }
257
- function validateRoleHooks(permissionPath, nameEntry) {
258
- for (const [hookRole, methodHooks] of nameEntry.hooks) {
259
- if (hookRole === GLOBAL_HOOK_OWNER)
445
+ };
446
+ function grantedAccess(nameEntry, method, assigned) {
447
+ const fields = new Set;
448
+ let isGranted = false;
449
+ let isReachable = false;
450
+ for (const [enablingId, grant] of nameEntry.grants) {
451
+ if (!assigned.has(enablingId))
260
452
  continue;
261
- for (const method of methodHooks.keys()) {
262
- if (hasAccessPath(nameEntry, hookRole, method))
453
+ isReachable = true;
454
+ const definition = grant.actions[method];
455
+ if (!definition)
456
+ continue;
457
+ isGranted = true;
458
+ for (const field of definition.properties)
459
+ fields.add(field);
460
+ }
461
+ if (!isGranted)
462
+ return isReachable ? DISABLED_ACCESS : UNASSIGNED_ACCESS;
463
+ return { status: "granted", properties: Object.freeze([...fields]) };
464
+ }
465
+ var resolve_default = resolve;
466
+
467
+ // src/permissions.ts
468
+ var permissions = Object.freeze({
469
+ forUser(assignments) {
470
+ const currentState = state_default.getOrCreate();
471
+ const snapshot = state_default.requireSnapshot(currentState);
472
+ const identity = resolve_default.identity(assignments, snapshot, currentState.roles);
473
+ const access = Object.create(null);
474
+ for (const [action, registeredModule] of currentState.modules) {
475
+ const chosen = resolve_default.permission(registeredModule, action, identity);
476
+ if (!chosen)
263
477
  continue;
264
- throw pkitError("INVALID_DEFINITION", `"${permissionPath}": hook for "${hookRole}" on "${method}" has no registered actions or grant for that role`);
478
+ const methodAccess = resolve_default.methodAccess(chosen.entry, chosen.permissionId, identity.role, identity.assigned);
479
+ if (methodAccess)
480
+ access[chosen.permissionId] = methodAccess;
265
481
  }
482
+ return Object.freeze(access);
483
+ },
484
+ get named() {
485
+ return state_default.requireSnapshot(state_default.getOrCreate()).named;
266
486
  }
267
- }
268
- function validateSealedRegistry(modules) {
269
- for (const [action, registeredModule] of modules) {
270
- if (registeredModule.names.size === 0) {
271
- throw pkitError("INVALID_DEFINITION", `"${action}" has hooks but no name with registered actions`);
487
+ });
488
+ var permissions_default = permissions;
489
+
490
+ // src/definitions.ts
491
+ var METHODS = new Set(constants_default.METHODS);
492
+ var definitions = {
493
+ checkMethod(method, code, subject) {
494
+ if (typeof method === "string" && METHODS.has(method))
495
+ return method;
496
+ throw errors_default.create(code, `${subject}: method "${errors_default.describe(method)}" does not exist. Methods: ${constants_default.METHODS.join(", ")}`);
497
+ },
498
+ readActions(actions, permissionPath) {
499
+ const declared = requireObject(actions, `${permissionPath}: registerActions expects an object`);
500
+ const registeredActions = Object.create(null);
501
+ for (const [method, definition] of Object.entries(declared)) {
502
+ definitions.checkMethod(method, "INVALID_DEFINITION", permissionPath);
503
+ registeredActions[method] = readActionDefinition(definition, `${permissionPath}.${method}`);
272
504
  }
273
- for (const [name, nameEntry] of registeredModule.names) {
274
- const permissionPath = `${action}::${name}`;
275
- if (nameEntry.actions.size === 0) {
276
- throw pkitError("INVALID_DEFINITION", `"${permissionPath}" has no registered actions for any role`);
505
+ return Object.freeze(registeredActions);
506
+ },
507
+ readGrant(actions, permissionPath) {
508
+ const registeredActions = definitions.readActions(actions, permissionPath);
509
+ for (const [method, definition] of Object.entries(registeredActions)) {
510
+ if (definition.enabled !== true) {
511
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}.${method}: a grant does not allow enabled: false`);
512
+ }
513
+ if (definition.properties === constants_default.ALL_FIELDS) {
514
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}.${method}: a grant requires an explicit properties list`);
277
515
  }
278
- validateGrantReferences(permissionPath, nameEntry, modules);
279
- validateRoleHooks(permissionPath, nameEntry);
280
516
  }
517
+ return registeredActions;
281
518
  }
519
+ };
520
+ function requireObject(value, message) {
521
+ if (value === null || typeof value !== "object" || Array.isArray(value))
522
+ throw errors_default.create("INVALID_DEFINITION", message);
523
+ return value;
282
524
  }
283
- function validateAssignments(permissions, role, assignable) {
284
- validateStrings(permissions, { code: "INVALID_INPUT", message: "permissions must be an array of strings", minimumLength: 1 });
285
- const assigned = new Set;
286
- for (const permissionId of permissions) {
287
- const reference = parsePermissionId(permissionId, {
288
- code: "INVALID_INPUT",
289
- message: `invalid permission identifier: "${permissionId}" (format role::module::name)`
290
- });
291
- if (reference.role !== role) {
292
- throw pkitError("PERMISSION_ROLE_MISMATCH", `"${permissionId}" belongs to role "${reference.role}", not the authenticated role "${role}"`);
293
- }
294
- if (!assignable.has(permissionId)) {
295
- throw pkitError("UNKNOWN_PERMISSION", `"${permissionId}" is not an assignable permission`);
296
- }
297
- assigned.add(permissionId);
525
+ function readActionDefinition(definition, permissionPath) {
526
+ const action = requireObject(definition, `${permissionPath}: must be an object`);
527
+ if (typeof action.enabled !== "boolean") {
528
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: enabled must be a boolean`);
298
529
  }
299
- return assigned;
300
- }
301
- function validateIdentity(assignments, state) {
302
- validateSnapshot(state.snapshot);
303
- const { role, permissions } = assignments;
304
- if (typeof role !== "string")
305
- throw pkitError("INVALID_INPUT", "role is required");
306
- validateRole(role, state.roles, "UNKNOWN_ROLE");
307
- return { role, assigned: validateAssignments(permissions, role, state.snapshot.assignable) };
308
- }
309
- function trimSelection(select, properties) {
310
- validateStrings(select, { code: "INVALID_INPUT", message: "select must be an array of strings", minimumLength: 0 });
311
- const selectedFields = [];
312
- for (const field of select) {
313
- if (properties.includes(field))
314
- selectedFields.push(field);
530
+ if (action.properties === constants_default.ALL_FIELDS) {
531
+ return Object.freeze({ enabled: action.enabled, properties: constants_default.ALL_FIELDS });
315
532
  }
316
- return selectedFields;
533
+ return Object.freeze({ enabled: action.enabled, properties: readDeclaredProperties(action.properties, permissionPath) });
317
534
  }
318
- function rejectForbiddenFields(data, properties, permissionPath) {
319
- const forbiddenFields = [];
320
- for (const field of Object.keys(data)) {
321
- if (!properties.includes(field))
322
- forbiddenFields.push(field);
535
+ function readDeclaredProperties(value, permissionPath) {
536
+ if (!Array.isArray(value)) {
537
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: properties must be string[] or '${constants_default.ALL_FIELDS}'`);
323
538
  }
324
- if (forbiddenFields.length === 0)
325
- return;
326
- throw Object.assign(pkitError("PROPERTIES_NOT_ALLOWED", `fields not allowed in "${permissionPath}": ${forbiddenFields.join(", ")}`), { fields: forbiddenFields });
327
- }
328
- function validateRequest(input, state) {
329
- validateSnapshot(state.snapshot);
330
- const { action, name, method } = input;
331
- validateMethod(method, { code: "INVALID_INPUT", message: "method is required" });
332
- if (method !== "find" && Object.hasOwn(input, "select"))
333
- throw pkitError("INVALID_INPUT", "select only applies to find");
334
- if (typeof action !== "string" || typeof name !== "string")
335
- throw pkitError("INVALID_INPUT", "action and name are required");
336
- const { role, assigned } = validateIdentity(input, state);
337
- const registeredModule = state.modules.get(action);
338
- if (!registeredModule)
339
- throw pkitError("UNKNOWN_ACTION", `module "${action}" is not registered`);
340
- const nameEntry = registeredModule.names.get(name);
341
- if (!nameEntry)
342
- throw pkitError("UNKNOWN_PERMISSION", `permission "${action}::${name}" is not registered`);
343
- const permissionId = permissionIdOf(role, action, name);
344
- const access = resolveAccess(nameEntry, permissionId, role, method, assigned);
345
- if (access.status === "unassigned") {
346
- throw pkitError("PERMISSION_NOT_ASSIGNED", `"${permissionId}" is not assigned or granted by the user permissions`);
539
+ for (const property of value) {
540
+ if (typeof property !== "string" || property.length === 0) {
541
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: properties must be string[] or '${constants_default.ALL_FIELDS}'`);
542
+ }
347
543
  }
348
- if (access.status === "disabled")
349
- throw pkitError("METHOD_DISABLED", `"${permissionId}.${method}" is not enabled`);
350
- const { data, context } = input;
351
- if (context !== undefined)
352
- validateObject(context, { code: "INVALID_INPUT", message: "context must be an object" });
353
- const { properties, authorization } = access;
354
- const permission = Object.freeze({ role, action, name, permissionId, method, enabled: true, properties, authorization });
355
- const request = { registeredModule, nameEntry, permission, data, context };
356
- if (method === "find") {
357
- if (data !== undefined)
358
- validateObject(data, { code: "INVALID_INPUT", message: "data must be an object" });
359
- const { select } = input;
360
- if (select === undefined)
361
- return { ...request, result: properties };
362
- if (properties === ALL_FIELDS) {
363
- validateStrings(select, { code: "INVALID_INPUT", message: "select must be an array of strings", minimumLength: 0 });
364
- return { ...request, result: select };
365
- }
366
- return { ...request, result: trimSelection(select, properties) };
367
- }
368
- validateObject(data, { code: "INVALID_INPUT", message: "data must be an object" });
369
- if (properties !== ALL_FIELDS)
370
- rejectForbiddenFields(data, properties, `${permissionId}.${method}`);
371
- return { ...request, result: data };
372
- }
373
-
374
- // src/context.ts
375
- function setRoles(key, roles) {
376
- validateContextKey(key);
377
- const state = getOrCreateState();
378
- validateRoleCatalog(roles, state);
379
- state.roles = new Set(roles);
380
- }
381
- function getRoles(key) {
382
- validateContextKey(key);
383
- return [...getOrCreateState().roles];
544
+ const declared = value;
545
+ if (declared.includes(constants_default.ALL_FIELDS)) {
546
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: '${constants_default.ALL_FIELDS}' is only allowed as the whole properties value`);
547
+ }
548
+ if (new Set(declared).size !== declared.length) {
549
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: properties contains duplicate fields`);
550
+ }
551
+ for (const property of declared)
552
+ properties_default.checkDeclaredPath(property, permissionPath);
553
+ return Object.freeze([...declared]);
384
554
  }
385
- var context = Object.freeze({ set: setRoles, get: getRoles });
555
+ var definitions_default = definitions;
386
556
 
387
557
  // src/registry.ts
388
- function appendHook(methodHooks, method, hook) {
389
- const hooks = methodHooks.get(method) ?? [];
390
- methodHooks.set(method, hooks);
391
- hooks.push(hook);
392
- }
393
- function registerActions(scope, actions) {
394
- const state = getOrCreateState();
395
- const registeredActions = validateActions(actions, scope, state);
396
- ensureName(state, scope.action, scope.name).actions.set(scope.role, registeredActions);
397
- return scope.builder;
398
- }
399
- function registerGrant(scope, actions) {
400
- const state = getOrCreateState();
401
- const grant = validateGrantActions(actions, scope, state);
402
- ensureName(state, scope.action, scope.name).grants.set(scope.permissionId, grant);
403
- return scope.builder;
558
+ var registry = {
559
+ defineModule(segment) {
560
+ identifiers_default.checkModuleSegment(segment);
561
+ return createModuleBuilder(segment);
562
+ }
563
+ };
564
+ function createModuleBuilder(action) {
565
+ const builder = {};
566
+ const scope = { action, builder };
567
+ return Object.assign(builder, {
568
+ module: appendModule.bind(null, action),
569
+ name: createNameBuilder.bind(null, action),
570
+ hook: registerModuleHook.bind(null, scope)
571
+ });
404
572
  }
405
- function registerModuleHook(scope, method, hook) {
406
- const state = getOrCreateState();
407
- validateHook(hook, { action: scope.action, method }, state);
408
- appendHook(ensureModule(state, scope.action).hooks, method, hook);
409
- return scope.builder;
573
+ function appendModule(parentAction, segment) {
574
+ identifiers_default.checkModuleSegment(segment);
575
+ return createModuleBuilder(`${parentAction}${constants_default.MODULE_SEPARATOR}${segment}`);
410
576
  }
411
- function registerNameHook(scope, method, hook) {
412
- const state = getOrCreateState();
413
- validateHook(hook, { ...scope, method }, state);
414
- const nameEntry = ensureName(state, scope.action, scope.name);
415
- const roleHooks = nameEntry.hooks.get(scope.role) ?? new Map;
416
- nameEntry.hooks.set(scope.role, roleHooks);
417
- appendHook(roleHooks, method, hook);
418
- return scope.builder;
577
+ function createNameBuilder(action, name) {
578
+ identifiers_default.checkName(name);
579
+ const builder = {};
580
+ const scope = { action, name, role: constants_default.GLOBAL_HOOK_OWNER, builder };
581
+ return Object.assign(builder, {
582
+ role: createRoleBuilder.bind(null, action, name),
583
+ grantTo: createGrantBuilder.bind(null, action, name),
584
+ hook: registerNameHook.bind(null, scope)
585
+ });
419
586
  }
420
587
  function createRoleBuilder(action, name, role) {
421
- validateRoleSegment(role);
588
+ identifiers_default.checkRole(role);
422
589
  const builder = {};
423
590
  const scope = { action, name, role, builder };
424
591
  return Object.assign(builder, {
@@ -431,43 +598,72 @@ function createGrantBuilder(action, name, permissionId) {
431
598
  const scope = { action, name, permissionId, builder };
432
599
  return Object.assign(builder, { registerActions: registerGrant.bind(null, scope) });
433
600
  }
434
- function createNameBuilder(action, name) {
435
- validatePermissionName(name);
436
- const builder = {};
437
- const scope = { action, name, role: GLOBAL_HOOK_OWNER, builder };
438
- return Object.assign(builder, {
439
- role: createRoleBuilder.bind(null, action, name),
440
- grantTo: createGrantBuilder.bind(null, action, name),
441
- hook: registerNameHook.bind(null, scope)
442
- });
601
+ function registerActions(scope, actions) {
602
+ const currentState = state_default.getOrCreate();
603
+ state_default.requireOpen(currentState);
604
+ requireDeclaredRole(scope.role, currentState.roles);
605
+ if (currentState.modules.get(scope.action)?.names.get(scope.name)?.actions.has(scope.role)) {
606
+ throw errors_default.create("DUPLICATE_REGISTRATION", `"${scope.action}::${scope.name}" already has actions registered for role "${scope.role}"`);
607
+ }
608
+ const registeredActions = definitions_default.readActions(actions, `${scope.action}::${scope.name} [${scope.role}]`);
609
+ ensureName(currentState, scope.action, scope.name).actions.set(scope.role, registeredActions);
610
+ return scope.builder;
443
611
  }
444
- function createModuleBuilder(action) {
445
- const builder = {};
446
- const scope = { action, builder };
447
- return Object.assign(builder, {
448
- module: appendModule.bind(null, action),
449
- name: createNameBuilder.bind(null, action),
450
- hook: registerModuleHook.bind(null, scope)
451
- });
612
+ function registerGrant(scope, actions) {
613
+ const currentState = state_default.getOrCreate();
614
+ state_default.requireOpen(currentState);
615
+ const source = identifiers_default.parse(scope.permissionId, "INVALID_DEFINITION", `"${scope.action}::${scope.name}": invalid grantTo identifier`);
616
+ requireDeclaredRole(source.role, currentState.roles);
617
+ if (source.action === scope.action && source.name === scope.name) {
618
+ throw errors_default.create("INVALID_DEFINITION", `"${scope.permissionId}" cannot grant to itself`);
619
+ }
620
+ if (currentState.modules.get(scope.action)?.names.get(scope.name)?.grants.has(scope.permissionId)) {
621
+ throw errors_default.create("DUPLICATE_REGISTRATION", `"${scope.action}::${scope.name}" already has a grant for "${scope.permissionId}"`);
622
+ }
623
+ const grantActions = definitions_default.readGrant(actions, `${scope.action}::${scope.name} [grantTo ${scope.permissionId}]`);
624
+ ensureName(currentState, scope.action, scope.name).grants.set(scope.permissionId, { source, actions: grantActions });
625
+ return scope.builder;
452
626
  }
453
- function appendModule(parentAction, segment) {
454
- validateModuleName(segment);
455
- return createModuleBuilder(`${parentAction}${MODULE_SEPARATOR}${segment}`);
627
+ function registerModuleHook(scope, method, hook) {
628
+ const currentState = state_default.getOrCreate();
629
+ state_default.requireOpen(currentState);
630
+ checkHook(hook, method, scope.action);
631
+ appendHook(ensureModule(currentState, scope.action).hooks, method, hook);
632
+ return scope.builder;
456
633
  }
457
- function defineModule(segment) {
458
- validateModuleName(segment);
459
- return createModuleBuilder(segment);
634
+ function registerNameHook(scope, method, hook) {
635
+ const currentState = state_default.getOrCreate();
636
+ state_default.requireOpen(currentState);
637
+ if (scope.role !== constants_default.GLOBAL_HOOK_OWNER)
638
+ requireDeclaredRole(scope.role, currentState.roles);
639
+ checkHook(hook, method, `${scope.action}::${scope.name}`);
640
+ const nameEntry = ensureName(currentState, scope.action, scope.name);
641
+ const roleHooks = nameEntry.hooks.get(scope.role) ?? new Map;
642
+ nameEntry.hooks.set(scope.role, roleHooks);
643
+ appendHook(roleHooks, method, hook);
644
+ return scope.builder;
460
645
  }
461
- function ensureModule(state, action) {
462
- const registeredModule = state.modules.get(action);
646
+ function checkHook(hook, method, permissionPath) {
647
+ definitions_default.checkMethod(method, "INVALID_DEFINITION", permissionPath);
648
+ if (typeof hook !== "function")
649
+ throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: hook("${method}") expects a function`);
650
+ }
651
+ function requireDeclaredRole(role, roles) {
652
+ if (roles.has(role))
653
+ return;
654
+ throw errors_default.create("ROLE_NOT_DECLARED", `role "${role}" is not declared. Available roles: ${[...roles].join(", ")}.
655
+ ` + "Is pkit.context.set('roles', [...]) missing from pkit.config.js, or was it imported after this file?");
656
+ }
657
+ function ensureModule(currentState, action) {
658
+ const registeredModule = currentState.modules.get(action);
463
659
  if (registeredModule)
464
660
  return registeredModule;
465
661
  const newModule = { names: new Map, hooks: new Map };
466
- state.modules.set(action, newModule);
662
+ currentState.modules.set(action, newModule);
467
663
  return newModule;
468
664
  }
469
- function ensureName(state, action, name) {
470
- const registeredModule = ensureModule(state, action);
665
+ function ensureName(currentState, action, name) {
666
+ const registeredModule = ensureModule(currentState, action);
471
667
  const registeredName = registeredModule.names.get(name);
472
668
  if (registeredName)
473
669
  return registeredName;
@@ -475,60 +671,158 @@ function ensureName(state, action, name) {
475
671
  registeredModule.names.set(name, newName);
476
672
  return newName;
477
673
  }
674
+ function appendHook(methodHooks, method, hook) {
675
+ const hooks = methodHooks.get(method) ?? [];
676
+ methodHooks.set(method, hooks);
677
+ hooks.push(hook);
678
+ }
679
+ var registry_default = registry;
478
680
 
479
681
  // src/seal.ts
480
- function seal() {
481
- const state = getOrCreateState();
482
- if (state.snapshot)
483
- return;
484
- validateSealedRegistry(state.modules);
485
- const named = Object.create(null);
486
- for (const [action, registeredModule] of state.modules) {
682
+ var sealer = {
683
+ seal() {
684
+ const currentState = state_default.getOrCreate();
685
+ if (currentState.snapshot)
686
+ return;
687
+ checkRegistry(currentState.modules);
688
+ const named = Object.create(null);
689
+ for (const [action, registeredModule] of currentState.modules) {
690
+ for (const [name, nameEntry] of registeredModule.names) {
691
+ for (const [role, actions] of nameEntry.actions) {
692
+ named[identifiers_default.build(role, action, name)] = actions;
693
+ }
694
+ }
695
+ }
696
+ currentState.snapshot = { named: Object.freeze(named), assignable: new Set(Object.keys(named)) };
697
+ }
698
+ };
699
+ function checkRegistry(modules) {
700
+ for (const [action, registeredModule] of modules) {
701
+ if (registeredModule.names.size === 0) {
702
+ throw errors_default.create("INVALID_DEFINITION", `"${action}" has hooks but no name with registered actions`);
703
+ }
487
704
  for (const [name, nameEntry] of registeredModule.names) {
488
- for (const [role, actions] of nameEntry.actions) {
489
- named[permissionIdOf(role, action, name)] = actions;
705
+ const permissionPath = `${action}::${name}`;
706
+ if (nameEntry.actions.size === 0) {
707
+ throw errors_default.create("INVALID_DEFINITION", `"${permissionPath}" has no registered actions for any role`);
490
708
  }
709
+ checkGrantReferences(permissionPath, nameEntry, modules);
710
+ checkRoleHooks(permissionPath, nameEntry);
491
711
  }
492
712
  }
493
- state.snapshot = { named: Object.freeze(named), assignable: new Set(Object.keys(named)) };
494
713
  }
495
-
496
- // src/permissions.ts
497
- function getNamedCatalog() {
498
- const { snapshot } = getOrCreateState();
499
- validateSnapshot(snapshot);
500
- return snapshot.named;
501
- }
502
- function resolveMethodAccess(nameEntry, permissionId, role, assigned) {
503
- const methodAccess = Object.create(null);
504
- let reachable = false;
505
- for (const method of METHODS) {
506
- const access = resolveAccess(nameEntry, permissionId, role, method, assigned);
507
- methodAccess[method] = access.status === "granted";
508
- if (access.status !== "unassigned")
509
- reachable = true;
510
- }
511
- return reachable ? Object.freeze(methodAccess) : undefined;
512
- }
513
- function forUser(assignments) {
514
- const state = getOrCreateState();
515
- const { role, assigned } = validateIdentity(assignments, state);
516
- const access = Object.create(null);
517
- for (const [action, registeredModule] of state.modules) {
518
- for (const [name, nameEntry] of registeredModule.names) {
519
- const permissionId = permissionIdOf(role, action, name);
520
- const methodAccess = resolveMethodAccess(nameEntry, permissionId, role, assigned);
521
- if (methodAccess)
522
- access[permissionId] = methodAccess;
714
+ function checkGrantReferences(permissionPath, nameEntry, modules) {
715
+ for (const [permissionId, grant] of nameEntry.grants) {
716
+ const sourceEntry = modules.get(grant.source.action)?.names.get(grant.source.name);
717
+ if (!sourceEntry?.actions.has(grant.source.role)) {
718
+ throw errors_default.create("INVALID_DEFINITION", `"${permissionPath}": grantTo "${permissionId}" references a permission with no registered actions`);
523
719
  }
720
+ for (const method of Object.keys(grant.actions)) {
721
+ if (declaresMethod(nameEntry, method))
722
+ continue;
723
+ throw errors_default.create("INVALID_DEFINITION", `"${permissionPath}": grantTo "${permissionId}" grants "${method}", which no role declares on that permission`);
724
+ }
725
+ }
726
+ }
727
+ function declaresMethod(nameEntry, method) {
728
+ for (const actions of nameEntry.actions.values()) {
729
+ if (Object.hasOwn(actions, method))
730
+ return true;
524
731
  }
525
- return Object.freeze(access);
732
+ return false;
526
733
  }
527
- var permissions = Object.freeze(Object.defineProperty({ forUser }, "named", { get: getNamedCatalog, enumerable: true, configurable: false }));
734
+ function checkRoleHooks(permissionPath, nameEntry) {
735
+ for (const [hookRole, methodHooks] of nameEntry.hooks) {
736
+ if (hookRole === constants_default.GLOBAL_HOOK_OWNER)
737
+ continue;
738
+ for (const method of methodHooks.keys()) {
739
+ if (hasAccessPath(nameEntry, hookRole, method))
740
+ continue;
741
+ throw errors_default.create("INVALID_DEFINITION", `"${permissionPath}": hook for "${hookRole}" on "${method}" has no registered actions or grant for that role`);
742
+ }
743
+ }
744
+ }
745
+ function hasAccessPath(nameEntry, role, method) {
746
+ if (nameEntry.actions.get(role)?.[method])
747
+ return true;
748
+ for (const grant of nameEntry.grants.values()) {
749
+ if (grant.source.role === role && grant.actions[method])
750
+ return true;
751
+ }
752
+ return false;
753
+ }
754
+ var seal_default = sealer;
528
755
 
529
756
  // src/validate.ts
757
+ var validator = {
758
+ async validate(input) {
759
+ try {
760
+ const request = prepareRequest(input, state_default.getOrCreate());
761
+ const hookCalls = [];
762
+ for (const hook of request.hooks)
763
+ hookCalls.push(invokeHook(hook, request));
764
+ const hookResults = await Promise.allSettled(hookCalls);
765
+ const hookErrors = [];
766
+ for (const hookResult of hookResults) {
767
+ if (hookResult.status === "fulfilled")
768
+ continue;
769
+ const hookCause = hookResult.reason;
770
+ hookErrors.push({ code: "HOOK_ERROR", message: describeHookFailure(hookCause), cause: hookCause });
771
+ }
772
+ if (hookErrors.length)
773
+ return { result: null, errors: Object.freeze(hookErrors) };
774
+ return { result: { data: request.data }, errors: Object.freeze([]) };
775
+ } catch (cause) {
776
+ return { result: null, errors: Object.freeze([translateFailure(cause)]) };
777
+ }
778
+ }
779
+ };
780
+ function prepareRequest(input, currentState) {
781
+ const snapshot = state_default.requireSnapshot(currentState);
782
+ const method = definitions_default.checkMethod(input.method, "INVALID_INPUT", "method is required");
783
+ const { action, permissions } = input;
784
+ if (typeof action !== "string")
785
+ throw errors_default.create("INVALID_INPUT", "action is required");
786
+ const data = readOptionalObject(input.data, "data must be an object");
787
+ const context = readOptionalObject(input.context, "context must be an object");
788
+ const identity = resolve_default.identity(input, snapshot, currentState.roles);
789
+ const { role, assigned } = identity;
790
+ const registeredModule = currentState.modules.get(action);
791
+ if (!registeredModule)
792
+ throw errors_default.create("UNKNOWN_ACTION", `module "${action}" is not registered`);
793
+ const chosen = resolve_default.permission(registeredModule, action, identity);
794
+ if (!chosen) {
795
+ throw errors_default.create("PERMISSION_NOT_ASSIGNED", `no name of module "${action}" is assigned or granted to role "${role}" by the user permissions`);
796
+ }
797
+ const { permissionId, entry: nameEntry } = chosen;
798
+ const access = resolve_default.access(nameEntry, permissionId, role, method, assigned);
799
+ if (access.status === "unassigned") {
800
+ throw errors_default.create("PERMISSION_NOT_ASSIGNED", `"${permissionId}" is not assigned or granted by the user permissions`);
801
+ }
802
+ if (access.status === "disabled")
803
+ throw errors_default.create("METHOD_DISABLED", `"${permissionId}.${method}" is not enabled`);
804
+ const hookGroups = [
805
+ registeredModule.hooks.get(method),
806
+ nameEntry.hooks.get(constants_default.GLOBAL_HOOK_OWNER)?.get(method),
807
+ nameEntry.hooks.get(role)?.get(method)
808
+ ];
809
+ const hooks = [];
810
+ for (const group of hookGroups) {
811
+ if (group)
812
+ hooks.push(...group);
813
+ }
814
+ const allowedData = properties_default.resolve(data, access.properties, currentState.reservedFields, currentState.cropper);
815
+ return { hooks, context, permissions, data: allowedData };
816
+ }
817
+ function readOptionalObject(value, message) {
818
+ if (value === undefined)
819
+ return {};
820
+ if (value === null || typeof value !== "object" || Array.isArray(value))
821
+ throw errors_default.create("INVALID_INPUT", message);
822
+ return value;
823
+ }
530
824
  async function invokeHook(hook, request) {
531
- return hook(request.data, request.context, request.permission);
825
+ return hook(request.data, request.context, request.permissions);
532
826
  }
533
827
  function describeHookFailure(reason) {
534
828
  try {
@@ -537,58 +831,34 @@ function describeHookFailure(reason) {
537
831
  return "the hook threw a value that cannot be described";
538
832
  }
539
833
  }
540
- async function validate(input) {
541
- try {
542
- const request = validateRequest(input, getOrCreateState());
543
- const { registeredModule, nameEntry, permission, result } = request;
544
- const { method, role } = permission;
545
- const hookGroups = [
546
- registeredModule.hooks.get(method),
547
- nameEntry.hooks.get(GLOBAL_HOOK_OWNER)?.get(method),
548
- nameEntry.hooks.get(role)?.get(method)
549
- ];
550
- const hookCalls = [];
551
- for (const hooks of hookGroups) {
552
- if (!hooks)
553
- continue;
554
- for (const hook of hooks)
555
- hookCalls.push(invokeHook(hook, request));
556
- }
557
- const hookResults = await Promise.allSettled(hookCalls);
558
- const errors = [];
559
- for (const hookResult of hookResults) {
560
- if (hookResult.status === "fulfilled")
561
- continue;
562
- const hookCause = hookResult.reason;
563
- errors.push({ code: "HOOK_ERROR", message: describeHookFailure(hookCause), cause: hookCause });
564
- }
565
- if (errors.length)
566
- return { result: null, errors: Object.freeze(errors) };
567
- return { result, errors: Object.freeze([]) };
568
- } catch (cause) {
569
- if (cause instanceof Error && cause.name === "PkitError") {
570
- const permissionError = cause;
571
- const error = permissionError.code === "PROPERTIES_NOT_ALLOWED" ? { code: permissionError.code, message: permissionError.message, fields: permissionError.fields } : { code: permissionError.code, message: permissionError.message };
572
- return { result: null, errors: Object.freeze([error]) };
573
- }
574
- return {
575
- result: null,
576
- errors: Object.freeze([{ code: "VALIDATION_ERROR", message: "permission could not be validated", cause }])
577
- };
834
+ function translateFailure(cause) {
835
+ if (!(cause instanceof Error) || cause.name !== "PkitError") {
836
+ return { code: "VALIDATION_ERROR", message: "permission could not be validated", cause };
578
837
  }
838
+ const permissionError = cause;
839
+ if (permissionError.code === "PROPERTIES_NOT_ALLOWED") {
840
+ return { code: permissionError.code, message: permissionError.message, fields: permissionError.fields };
841
+ }
842
+ return { code: permissionError.code, message: permissionError.message };
579
843
  }
844
+ var validate_default = validator;
845
+
580
846
  // src/index.ts
581
- var pkit = Object.freeze({ context, module: defineModule, seal, permissions, validate });
847
+ var METHODS2 = constants_default.METHODS;
848
+ var seal = seal_default.seal;
849
+ var validate = validate_default.validate;
850
+ var defineModule = registry_default.defineModule;
851
+ var pkit = Object.freeze({ context: context_default, module: defineModule, seal, permissions: permissions_default, validate });
582
852
  var src_default = pkit;
583
853
  export {
584
- METHODS,
585
- context,
854
+ METHODS2 as METHODS,
855
+ context_default as context,
586
856
  src_default as default,
587
857
  defineModule as module,
588
- permissions,
858
+ permissions_default as permissions,
589
859
  pkit,
590
860
  seal,
591
861
  validate
592
862
  };
593
863
 
594
- //# debugId=60F9871502AF5BAC64756E2164756E21
864
+ //# debugId=659F54F36460059464756E2164756E21