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/README.md +28 -20
- package/bin/pkit.mjs +2 -2
- package/dist/cjs/index.cjs +744 -474
- package/dist/cjs/index.cjs.map +16 -14
- package/dist/esm/cli/child.js +329 -352
- package/dist/esm/cli/child.js.map +11 -11
- package/dist/esm/cli/generate.js +78 -420
- package/dist/esm/cli/generate.js.map +5 -9
- package/dist/esm/index.js +746 -476
- package/dist/esm/index.js.map +16 -14
- package/dist/types/cli/generate.d.ts +6 -4
- package/dist/types/cli/protocol.d.ts +10 -7
- package/dist/types/constants.d.ts +9 -6
- package/dist/types/context.d.ts +5 -6
- package/dist/types/definitions.d.ts +15 -0
- package/dist/types/errors.d.ts +12 -11
- package/dist/types/identifiers.d.ts +21 -0
- package/dist/types/index.d.ts +16 -17
- package/dist/types/permissions.d.ts +10 -5
- package/dist/types/properties.d.ts +6 -0
- package/dist/types/registry.d.ts +4 -1
- package/dist/types/resolve.d.ts +50 -6
- package/dist/types/seal.d.ts +8 -1
- package/dist/types/state.d.ts +11 -9
- package/dist/types/types.d.ts +26 -34
- package/dist/types/validate.d.ts +5 -3
- package/package.json +1 -1
- package/dist/types/validators.d.ts +0 -53
package/dist/esm/cli/child.js
CHANGED
|
@@ -2,407 +2,384 @@
|
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
|
|
5
|
-
// src/constants.ts
|
|
6
|
-
var METHODS = ["find", "update", "create", "remove"];
|
|
7
|
-
var GENERAL_ROLE = "general";
|
|
8
|
-
var GLOBAL_HOOK_OWNER = "*";
|
|
9
|
-
var ALL_FIELDS = "*";
|
|
10
|
-
var MODULE_SEPARATOR = ".";
|
|
11
|
-
var PERMISSION_ID_SEPARATOR = "::";
|
|
12
|
-
|
|
13
|
-
// src/state.ts
|
|
14
|
-
function getOrCreateState() {
|
|
15
|
-
const stateKey = Symbol.for("endpoint-permissions-kit");
|
|
16
|
-
const stateHost = globalThis;
|
|
17
|
-
if (stateHost[stateKey] === undefined) {
|
|
18
|
-
stateHost[stateKey] = { roles: new Set([GENERAL_ROLE]), modules: new Map, snapshot: null };
|
|
19
|
-
}
|
|
20
|
-
return stateHost[stateKey];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
5
|
// src/errors.ts
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
function resolveAccess(nameEntry, permissionId, role, method, assigned) {
|
|
36
|
-
if (assigned.has(permissionId))
|
|
37
|
-
return resolveDirectAccess(nameEntry, role, method);
|
|
38
|
-
return resolveGrantedAccess(nameEntry, method, assigned);
|
|
39
|
-
}
|
|
40
|
-
function resolveDirectAccess(nameEntry, role, method) {
|
|
41
|
-
const definition = nameEntry.actions.get(role)?.[method];
|
|
42
|
-
if (!definition?.enabled)
|
|
43
|
-
return DISABLED_ACCESS;
|
|
44
|
-
return { status: "granted", properties: definition.properties, authorization: DIRECT_AUTHORIZATION };
|
|
45
|
-
}
|
|
46
|
-
function resolveGrantedAccess(nameEntry, method, assigned) {
|
|
47
|
-
const grantedBy = [];
|
|
48
|
-
const properties = new Set;
|
|
49
|
-
let reachable = false;
|
|
50
|
-
for (const [enablingId, grant] of nameEntry.grants) {
|
|
51
|
-
if (!assigned.has(enablingId))
|
|
52
|
-
continue;
|
|
53
|
-
reachable = true;
|
|
54
|
-
const definition = grant.actions[method];
|
|
55
|
-
if (!definition)
|
|
56
|
-
continue;
|
|
57
|
-
grantedBy.push(enablingId);
|
|
58
|
-
for (const field of definition.properties)
|
|
59
|
-
properties.add(field);
|
|
6
|
+
var errors = {
|
|
7
|
+
create(code, message) {
|
|
8
|
+
return Object.assign(new Error(message), { name: "PkitError", code });
|
|
9
|
+
},
|
|
10
|
+
describe(value) {
|
|
11
|
+
try {
|
|
12
|
+
return String(value);
|
|
13
|
+
} catch {
|
|
14
|
+
return typeof value;
|
|
15
|
+
}
|
|
60
16
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const authorization = Object.freeze({ direct: false, grantedBy: Object.freeze(grantedBy.sort()) });
|
|
64
|
-
return { status: "granted", properties: Object.freeze([...properties]), authorization };
|
|
65
|
-
}
|
|
17
|
+
};
|
|
18
|
+
var errors_default = errors;
|
|
66
19
|
|
|
67
|
-
// src/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
20
|
+
// src/constants.ts
|
|
21
|
+
var constants = {
|
|
22
|
+
METHODS: Object.freeze(["find", "update", "create", "remove"]),
|
|
23
|
+
GENERAL_ROLE: "general",
|
|
24
|
+
GLOBAL_HOOK_OWNER: "*",
|
|
25
|
+
ALL_FIELDS: "*",
|
|
26
|
+
MODULE_SEPARATOR: ".",
|
|
27
|
+
PERMISSION_ID_SEPARATOR: "::"
|
|
28
|
+
};
|
|
29
|
+
var constants_default = constants;
|
|
30
|
+
|
|
31
|
+
// src/identifiers.ts
|
|
32
|
+
var identifiers = {
|
|
33
|
+
build(role, action, name) {
|
|
34
|
+
return `${role}${constants_default.PERMISSION_ID_SEPARATOR}${action}${constants_default.PERMISSION_ID_SEPARATOR}${name}`;
|
|
35
|
+
},
|
|
36
|
+
parse(value, code, subject) {
|
|
37
|
+
if (typeof value !== "string")
|
|
38
|
+
throw invalidIdentifier(value, code, subject);
|
|
39
|
+
const parts = value.split(constants_default.PERMISSION_ID_SEPARATOR);
|
|
40
|
+
const [role, action, name] = parts;
|
|
41
|
+
if (parts.length !== 3 || role === undefined || action === undefined || name === undefined) {
|
|
42
|
+
throw invalidIdentifier(value, code, subject);
|
|
79
43
|
}
|
|
44
|
+
const isValidRole = isCleanSegment(role) && role !== constants_default.GLOBAL_HOOK_OWNER;
|
|
45
|
+
const isValidName = isCleanSegment(name) && name !== constants_default.GLOBAL_HOOK_OWNER;
|
|
46
|
+
const isValidAction = action.split(constants_default.MODULE_SEPARATOR).every(isCleanSegment);
|
|
47
|
+
if (!isValidRole || !isValidName || !isValidAction)
|
|
48
|
+
throw invalidIdentifier(value, code, subject);
|
|
49
|
+
return { role, action, name };
|
|
50
|
+
},
|
|
51
|
+
checkRole(role) {
|
|
52
|
+
if (typeof role === "string" && isCleanSegment(role) && role !== constants_default.GLOBAL_HOOK_OWNER)
|
|
53
|
+
return;
|
|
54
|
+
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)`);
|
|
55
|
+
},
|
|
56
|
+
checkModuleSegment(segment) {
|
|
57
|
+
if (typeof segment === "string" && isCleanSegment(segment) && !segment.includes(constants_default.MODULE_SEPARATOR))
|
|
58
|
+
return;
|
|
59
|
+
throw errors_default.create("INVALID_DEFINITION", `invalid module name: "${errors_default.describe(segment)}" (non-empty string, no dots, no ":" or surrounding whitespace)`);
|
|
60
|
+
},
|
|
61
|
+
checkName(name) {
|
|
62
|
+
if (typeof name === "string" && isCleanSegment(name) && name !== constants_default.GLOBAL_HOOK_OWNER)
|
|
63
|
+
return;
|
|
64
|
+
throw errors_default.create("INVALID_DEFINITION", `invalid permission name: "${errors_default.describe(name)}" (non-empty string, no ":" or surrounding whitespace; "*" is reserved)`);
|
|
80
65
|
}
|
|
81
|
-
}
|
|
82
|
-
function validateContextKey(key) {
|
|
83
|
-
if (key !== "roles")
|
|
84
|
-
throw pkitError("INVALID_DEFINITION", `unknown context key: "${String(key)}"`);
|
|
85
|
-
}
|
|
86
|
-
function validateOpenRegistry(state) {
|
|
87
|
-
if (state.snapshot)
|
|
88
|
-
throw pkitError("SEALED", "pkit.seal() was already called: no more registrations allowed");
|
|
89
|
-
}
|
|
90
|
-
function validateSnapshot(snapshot) {
|
|
91
|
-
if (snapshot === null) {
|
|
92
|
-
throw pkitError("NOT_SEALED", "pkit.seal() has not been called: call it after importing every permission file");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function validateRole(role, roles, code) {
|
|
96
|
-
if (typeof role === "string" && roles.has(role))
|
|
97
|
-
return;
|
|
98
|
-
if (code === "ROLE_NOT_DECLARED") {
|
|
99
|
-
throw pkitError(code, `role "${String(role)}" is not declared. Available roles: ${[...roles].join(", ")}.
|
|
100
|
-
` + "Is pkit.context.set('roles', [...]) missing from pkit.config.js, or was it imported after this file?");
|
|
101
|
-
}
|
|
102
|
-
throw pkitError(code, `role "${String(role)}" is not declared`);
|
|
103
|
-
}
|
|
104
|
-
function validateRoleCatalog(roles, state) {
|
|
105
|
-
validateOpenRegistry(state);
|
|
106
|
-
if (state.modules.size) {
|
|
107
|
-
throw pkitError("INVALID_DEFINITION", "roles must be declared before registering permissions: import pkit.config.js first");
|
|
108
|
-
}
|
|
109
|
-
validateStrings(roles, {
|
|
110
|
-
code: "INVALID_DEFINITION",
|
|
111
|
-
message: "roles must be an array of non-empty strings",
|
|
112
|
-
minimumLength: 1
|
|
113
|
-
});
|
|
114
|
-
if (roles.length === 0)
|
|
115
|
-
throw pkitError("INVALID_DEFINITION", "roles must declare at least one role");
|
|
116
|
-
for (const role of roles)
|
|
117
|
-
validateRoleSegment(role);
|
|
118
|
-
}
|
|
66
|
+
};
|
|
119
67
|
function isCleanSegment(value) {
|
|
120
68
|
return value.length > 0 && value.trim() === value && !value.includes(":");
|
|
121
69
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
return;
|
|
125
|
-
throw pkitError("INVALID_DEFINITION", `invalid role: "${String(role)}" (non-empty string, no ":" or surrounding whitespace; "${GLOBAL_HOOK_OWNER}" is reserved for global hooks)`);
|
|
126
|
-
}
|
|
127
|
-
function validateModuleName(moduleName) {
|
|
128
|
-
if (typeof moduleName === "string" && isCleanSegment(moduleName) && !moduleName.includes(MODULE_SEPARATOR))
|
|
129
|
-
return;
|
|
130
|
-
throw pkitError("INVALID_DEFINITION", `invalid module name: "${String(moduleName)}" (non-empty string, no dots, no ":" or surrounding whitespace)`);
|
|
131
|
-
}
|
|
132
|
-
function validatePermissionName(name) {
|
|
133
|
-
if (typeof name === "string" && isCleanSegment(name) && name !== GLOBAL_HOOK_OWNER)
|
|
134
|
-
return;
|
|
135
|
-
throw pkitError("INVALID_DEFINITION", `invalid permission name: "${String(name)}" (non-empty string, no ":" or surrounding whitespace; "*" is reserved)`);
|
|
136
|
-
}
|
|
137
|
-
function parsePermissionId(value, failure) {
|
|
138
|
-
if (typeof value !== "string")
|
|
139
|
-
throw pkitError(failure.code, failure.message);
|
|
140
|
-
const parts = value.split(PERMISSION_ID_SEPARATOR);
|
|
141
|
-
const [role, action, name] = parts;
|
|
142
|
-
if (parts.length !== 3 || role === undefined || action === undefined || name === undefined) {
|
|
143
|
-
throw pkitError(failure.code, failure.message);
|
|
144
|
-
}
|
|
145
|
-
const isValidRole = isCleanSegment(role) && role !== GLOBAL_HOOK_OWNER;
|
|
146
|
-
const isValidName = isCleanSegment(name) && name !== GLOBAL_HOOK_OWNER;
|
|
147
|
-
const isValidAction = action.split(MODULE_SEPARATOR).every(isCleanSegment);
|
|
148
|
-
if (!isValidRole || !isValidName || !isValidAction)
|
|
149
|
-
throw pkitError(failure.code, failure.message);
|
|
150
|
-
return { role, action, name };
|
|
151
|
-
}
|
|
152
|
-
function validateMethod(method, failure) {
|
|
153
|
-
if (METHODS.includes(method))
|
|
154
|
-
return;
|
|
155
|
-
throw pkitError(failure.code, `${failure.message}: method "${String(method)}" does not exist. Methods: ${METHODS.join(", ")}`);
|
|
156
|
-
}
|
|
157
|
-
function readActionDefinition(definition, permissionPath) {
|
|
158
|
-
validateObject(definition, { code: "INVALID_DEFINITION", message: `${permissionPath}: must be an object` });
|
|
159
|
-
if (typeof definition.enabled !== "boolean") {
|
|
160
|
-
throw pkitError("INVALID_DEFINITION", `${permissionPath}: enabled must be a boolean`);
|
|
161
|
-
}
|
|
162
|
-
const { properties } = definition;
|
|
163
|
-
if (properties === ALL_FIELDS)
|
|
164
|
-
return Object.freeze({ enabled: definition.enabled, properties });
|
|
165
|
-
validateStrings(properties, {
|
|
166
|
-
code: "INVALID_DEFINITION",
|
|
167
|
-
message: `${permissionPath}: properties must be string[] or '*'`,
|
|
168
|
-
minimumLength: 1
|
|
169
|
-
});
|
|
170
|
-
if (properties.includes(ALL_FIELDS)) {
|
|
171
|
-
throw pkitError("INVALID_DEFINITION", `${permissionPath}: '${ALL_FIELDS}' is only allowed as the whole properties value`);
|
|
172
|
-
}
|
|
173
|
-
if (new Set(properties).size !== properties.length) {
|
|
174
|
-
throw pkitError("INVALID_DEFINITION", `${permissionPath}: properties contains duplicate fields`);
|
|
175
|
-
}
|
|
176
|
-
return Object.freeze({ enabled: definition.enabled, properties: Object.freeze([...properties]) });
|
|
177
|
-
}
|
|
178
|
-
function readActionDefinitions(actions, permissionPath) {
|
|
179
|
-
validateObject(actions, { code: "INVALID_DEFINITION", message: `${permissionPath}: registerActions expects an object` });
|
|
180
|
-
const registeredActions = Object.create(null);
|
|
181
|
-
for (const [method, definition] of Object.entries(actions)) {
|
|
182
|
-
validateMethod(method, { code: "INVALID_DEFINITION", message: permissionPath });
|
|
183
|
-
registeredActions[method] = readActionDefinition(definition, `${permissionPath}.${method}`);
|
|
184
|
-
}
|
|
185
|
-
return Object.freeze(registeredActions);
|
|
186
|
-
}
|
|
187
|
-
function validateActions(actions, registration, state) {
|
|
188
|
-
const { action, name, role } = registration;
|
|
189
|
-
validateOpenRegistry(state);
|
|
190
|
-
validateRole(role, state.roles, "ROLE_NOT_DECLARED");
|
|
191
|
-
if (state.modules.get(action)?.names.get(name)?.actions.has(role)) {
|
|
192
|
-
throw pkitError("DUPLICATE_REGISTRATION", `"${action}::${name}" already has actions registered for role "${role}"`);
|
|
193
|
-
}
|
|
194
|
-
return readActionDefinitions(actions, `${action}::${name} [${role}]`);
|
|
70
|
+
function invalidIdentifier(value, code, subject) {
|
|
71
|
+
return errors_default.create(code, `${subject}: "${errors_default.describe(value)}" (format role::module::name)`);
|
|
195
72
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
73
|
+
var identifiers_default = identifiers;
|
|
74
|
+
|
|
75
|
+
// src/properties.ts
|
|
76
|
+
var PRUNED = Symbol("pruned");
|
|
77
|
+
var MAX_DEPTH = 1000;
|
|
78
|
+
var properties = {
|
|
79
|
+
checkDeclaredPath(property, permissionPath) {
|
|
80
|
+
if (property.includes("[") || property.includes("]")) {
|
|
81
|
+
throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: array indexes are not allowed in properties: "${property}"`);
|
|
82
|
+
}
|
|
83
|
+
const segments = segmentsOf(property);
|
|
84
|
+
for (const segment of segments) {
|
|
85
|
+
if (segment.length === 0)
|
|
86
|
+
throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: empty segment in property path: "${property}"`);
|
|
87
|
+
}
|
|
88
|
+
if (segments[0] === constants_default.ALL_FIELDS) {
|
|
89
|
+
throw errors_default.create("INVALID_DEFINITION", `${permissionPath}: a property path cannot start with "${constants_default.ALL_FIELDS}": "${property}"`);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
resolve(data, allowed, reserved, cropper) {
|
|
93
|
+
if (allowed === constants_default.ALL_FIELDS)
|
|
94
|
+
return data;
|
|
95
|
+
const patterns = [];
|
|
96
|
+
for (const pattern of allowed.concat(reserved))
|
|
97
|
+
patterns.push(segmentsOf(pattern));
|
|
98
|
+
const walk = { patterns, segments: [], ancestors: new Set([data]) };
|
|
99
|
+
if (cropper)
|
|
100
|
+
return cropData(data, walk);
|
|
101
|
+
rejectForbiddenPaths(data, walk);
|
|
102
|
+
return data;
|
|
209
103
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
104
|
+
};
|
|
105
|
+
function segmentsOf(path) {
|
|
106
|
+
const segments = [];
|
|
107
|
+
let segment = "";
|
|
108
|
+
let index = 0;
|
|
109
|
+
while (index < path.length) {
|
|
110
|
+
const character = path[index];
|
|
111
|
+
if (character === "\\") {
|
|
112
|
+
segment += path[index + 1] ?? "";
|
|
113
|
+
index += 2;
|
|
114
|
+
continue;
|
|
215
115
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
116
|
+
if (character === ".") {
|
|
117
|
+
segments.push(segment);
|
|
118
|
+
segment = "";
|
|
119
|
+
index += 1;
|
|
120
|
+
continue;
|
|
218
121
|
}
|
|
122
|
+
segment += character;
|
|
123
|
+
index += 1;
|
|
219
124
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
function validateHook(hook, registration, state) {
|
|
223
|
-
const { action, name, role, method } = registration;
|
|
224
|
-
validateOpenRegistry(state);
|
|
225
|
-
if (role !== undefined && role !== GLOBAL_HOOK_OWNER)
|
|
226
|
-
validateRole(role, state.roles, "ROLE_NOT_DECLARED");
|
|
227
|
-
const permissionPath = name === undefined ? action : `${action}::${name}`;
|
|
228
|
-
validateMethod(method, { code: "INVALID_DEFINITION", message: permissionPath });
|
|
229
|
-
if (typeof hook !== "function")
|
|
230
|
-
throw pkitError("INVALID_DEFINITION", `${permissionPath}: hook("${method}") expects a function`);
|
|
125
|
+
segments.push(segment);
|
|
126
|
+
return segments;
|
|
231
127
|
}
|
|
232
|
-
function
|
|
233
|
-
for (const
|
|
234
|
-
if (
|
|
128
|
+
function isAllowedPath(segments, patterns) {
|
|
129
|
+
for (const pattern of patterns) {
|
|
130
|
+
if (pattern.length !== segments.length)
|
|
131
|
+
continue;
|
|
132
|
+
let isMatch = true;
|
|
133
|
+
for (let index = 0;index < segments.length; index += 1) {
|
|
134
|
+
const patternSegment = pattern[index];
|
|
135
|
+
if (patternSegment === constants_default.ALL_FIELDS)
|
|
136
|
+
continue;
|
|
137
|
+
if (patternSegment === segments[index])
|
|
138
|
+
continue;
|
|
139
|
+
isMatch = false;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (isMatch)
|
|
235
143
|
return true;
|
|
236
144
|
}
|
|
237
145
|
return false;
|
|
238
146
|
}
|
|
239
|
-
function
|
|
240
|
-
if (
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
return false;
|
|
147
|
+
function isPlainObject(value) {
|
|
148
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
149
|
+
return false;
|
|
150
|
+
const prototype = Object.getPrototypeOf(value);
|
|
151
|
+
return prototype === Object.prototype || prototype === null;
|
|
247
152
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
153
|
+
function requireDepth(walk) {
|
|
154
|
+
if (walk.ancestors.size < MAX_DEPTH)
|
|
155
|
+
return;
|
|
156
|
+
throw new RangeError(`data is nested deeper than ${MAX_DEPTH} levels`);
|
|
157
|
+
}
|
|
158
|
+
function setField(target, key, value) {
|
|
159
|
+
if (key === "__proto__") {
|
|
160
|
+
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
target[key] = value;
|
|
164
|
+
}
|
|
165
|
+
function cropValue(value, walk) {
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
if (value.length === 0)
|
|
168
|
+
return isAllowedPath(walk.segments, walk.patterns) ? [] : PRUNED;
|
|
169
|
+
if (walk.ancestors.has(value))
|
|
170
|
+
throw new RangeError("data contains a circular reference");
|
|
171
|
+
requireDepth(walk);
|
|
172
|
+
walk.ancestors.add(value);
|
|
173
|
+
const items = [];
|
|
174
|
+
for (const item of value) {
|
|
175
|
+
const croppedItem = cropValue(item, walk);
|
|
176
|
+
if (croppedItem === PRUNED)
|
|
256
177
|
continue;
|
|
257
|
-
|
|
178
|
+
items.push(croppedItem);
|
|
258
179
|
}
|
|
180
|
+
walk.ancestors.delete(value);
|
|
181
|
+
return items.length === 0 ? PRUNED : items;
|
|
259
182
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
183
|
+
if (isPlainObject(value)) {
|
|
184
|
+
const container = value;
|
|
185
|
+
const keys = Object.keys(container);
|
|
186
|
+
if (keys.length === 0)
|
|
187
|
+
return isAllowedPath(walk.segments, walk.patterns) ? {} : PRUNED;
|
|
188
|
+
if (walk.ancestors.has(container))
|
|
189
|
+
throw new RangeError("data contains a circular reference");
|
|
190
|
+
requireDepth(walk);
|
|
191
|
+
walk.ancestors.add(container);
|
|
192
|
+
const cropped = {};
|
|
193
|
+
let keptCount = 0;
|
|
194
|
+
for (const key of keys) {
|
|
195
|
+
walk.segments.push(key);
|
|
196
|
+
const croppedValue = cropValue(container[key], walk);
|
|
197
|
+
walk.segments.pop();
|
|
198
|
+
if (croppedValue === PRUNED)
|
|
267
199
|
continue;
|
|
268
|
-
|
|
200
|
+
setField(cropped, key, croppedValue);
|
|
201
|
+
keptCount += 1;
|
|
269
202
|
}
|
|
203
|
+
walk.ancestors.delete(container);
|
|
204
|
+
return keptCount === 0 ? PRUNED : cropped;
|
|
205
|
+
}
|
|
206
|
+
return isAllowedPath(walk.segments, walk.patterns) ? value : PRUNED;
|
|
207
|
+
}
|
|
208
|
+
function cropData(data, walk) {
|
|
209
|
+
const cropped = {};
|
|
210
|
+
for (const key of Object.keys(data)) {
|
|
211
|
+
walk.segments.push(key);
|
|
212
|
+
const croppedValue = cropValue(data[key], walk);
|
|
213
|
+
walk.segments.pop();
|
|
214
|
+
if (croppedValue === PRUNED)
|
|
215
|
+
continue;
|
|
216
|
+
setField(cropped, key, croppedValue);
|
|
270
217
|
}
|
|
218
|
+
return cropped;
|
|
271
219
|
}
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
for (const [name, nameEntry] of registeredModule.names) {
|
|
278
|
-
const permissionPath = `${action}::${name}`;
|
|
279
|
-
if (nameEntry.actions.size === 0) {
|
|
280
|
-
throw pkitError("INVALID_DEFINITION", `"${permissionPath}" has no registered actions for any role`);
|
|
281
|
-
}
|
|
282
|
-
validateGrantReferences(permissionPath, nameEntry, modules);
|
|
283
|
-
validateRoleHooks(permissionPath, nameEntry);
|
|
220
|
+
function collectForbiddenPaths(value, walk, forbidden) {
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
if (value.length === 0 || walk.ancestors.has(value)) {
|
|
223
|
+
reportPath(walk, forbidden);
|
|
224
|
+
return;
|
|
284
225
|
}
|
|
226
|
+
requireDepth(walk);
|
|
227
|
+
walk.ancestors.add(value);
|
|
228
|
+
for (const item of value)
|
|
229
|
+
collectForbiddenPaths(item, walk, forbidden);
|
|
230
|
+
walk.ancestors.delete(value);
|
|
231
|
+
return;
|
|
285
232
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
code: "INVALID_INPUT",
|
|
293
|
-
message: `invalid permission identifier: "${permissionId}" (format role::module::name)`
|
|
294
|
-
});
|
|
295
|
-
if (reference.role !== role) {
|
|
296
|
-
throw pkitError("PERMISSION_ROLE_MISMATCH", `"${permissionId}" belongs to role "${reference.role}", not the authenticated role "${role}"`);
|
|
233
|
+
if (isPlainObject(value)) {
|
|
234
|
+
const container = value;
|
|
235
|
+
const keys = Object.keys(container);
|
|
236
|
+
if (keys.length === 0 || walk.ancestors.has(container)) {
|
|
237
|
+
reportPath(walk, forbidden);
|
|
238
|
+
return;
|
|
297
239
|
}
|
|
298
|
-
|
|
299
|
-
|
|
240
|
+
requireDepth(walk);
|
|
241
|
+
walk.ancestors.add(container);
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
walk.segments.push(key);
|
|
244
|
+
collectForbiddenPaths(container[key], walk, forbidden);
|
|
245
|
+
walk.segments.pop();
|
|
300
246
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return assigned;
|
|
304
|
-
}
|
|
305
|
-
function validateIdentity(assignments, state) {
|
|
306
|
-
validateSnapshot(state.snapshot);
|
|
307
|
-
const { role, permissions } = assignments;
|
|
308
|
-
if (typeof role !== "string")
|
|
309
|
-
throw pkitError("INVALID_INPUT", "role is required");
|
|
310
|
-
validateRole(role, state.roles, "UNKNOWN_ROLE");
|
|
311
|
-
return { role, assigned: validateAssignments(permissions, role, state.snapshot.assignable) };
|
|
312
|
-
}
|
|
313
|
-
function trimSelection(select, properties) {
|
|
314
|
-
validateStrings(select, { code: "INVALID_INPUT", message: "select must be an array of strings", minimumLength: 0 });
|
|
315
|
-
const selectedFields = [];
|
|
316
|
-
for (const field of select) {
|
|
317
|
-
if (properties.includes(field))
|
|
318
|
-
selectedFields.push(field);
|
|
247
|
+
walk.ancestors.delete(container);
|
|
248
|
+
return;
|
|
319
249
|
}
|
|
320
|
-
|
|
250
|
+
reportPath(walk, forbidden);
|
|
321
251
|
}
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
252
|
+
function reportPath(walk, forbidden) {
|
|
253
|
+
if (isAllowedPath(walk.segments, walk.patterns))
|
|
254
|
+
return;
|
|
255
|
+
forbidden.add(walk.segments.join("."));
|
|
256
|
+
}
|
|
257
|
+
function rejectForbiddenPaths(data, walk) {
|
|
258
|
+
const forbidden = new Set;
|
|
259
|
+
for (const key of Object.keys(data)) {
|
|
260
|
+
walk.segments.push(key);
|
|
261
|
+
collectForbiddenPaths(data[key], walk, forbidden);
|
|
262
|
+
walk.segments.pop();
|
|
327
263
|
}
|
|
328
|
-
if (
|
|
264
|
+
if (forbidden.size === 0)
|
|
329
265
|
return;
|
|
330
|
-
|
|
266
|
+
const fields = [...forbidden];
|
|
267
|
+
const error = errors_default.create("PROPERTIES_NOT_ALLOWED", `fields not allowed: ${fields.join(", ")}`);
|
|
268
|
+
throw Object.assign(error, { fields });
|
|
331
269
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const { properties, authorization } = access;
|
|
358
|
-
const permission = Object.freeze({ role, action, name, permissionId, method, enabled: true, properties, authorization });
|
|
359
|
-
const request = { registeredModule, nameEntry, permission, data, context };
|
|
360
|
-
if (method === "find") {
|
|
361
|
-
if (data !== undefined)
|
|
362
|
-
validateObject(data, { code: "INVALID_INPUT", message: "data must be an object" });
|
|
363
|
-
const { select } = input;
|
|
364
|
-
if (select === undefined)
|
|
365
|
-
return { ...request, result: properties };
|
|
366
|
-
if (properties === ALL_FIELDS) {
|
|
367
|
-
validateStrings(select, { code: "INVALID_INPUT", message: "select must be an array of strings", minimumLength: 0 });
|
|
368
|
-
return { ...request, result: select };
|
|
270
|
+
var properties_default = properties;
|
|
271
|
+
|
|
272
|
+
// src/state.ts
|
|
273
|
+
var STATE_KEY = Symbol.for("endpoint-permissions-kit");
|
|
274
|
+
var state = {
|
|
275
|
+
getOrCreate() {
|
|
276
|
+
const stateHost = globalThis;
|
|
277
|
+
if (stateHost[STATE_KEY] === undefined) {
|
|
278
|
+
stateHost[STATE_KEY] = {
|
|
279
|
+
roles: new Set([constants_default.GENERAL_ROLE]),
|
|
280
|
+
cropper: false,
|
|
281
|
+
reservedFields: [],
|
|
282
|
+
modules: new Map,
|
|
283
|
+
snapshot: null
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return stateHost[STATE_KEY];
|
|
287
|
+
},
|
|
288
|
+
requireOpen(currentState) {
|
|
289
|
+
if (currentState.snapshot)
|
|
290
|
+
throw errors_default.create("SEALED", "pkit.seal() was already called: no more registrations allowed");
|
|
291
|
+
},
|
|
292
|
+
requireSnapshot(currentState) {
|
|
293
|
+
if (currentState.snapshot === null) {
|
|
294
|
+
throw errors_default.create("NOT_SEALED", "pkit.seal() has not been called: call it after importing every permission file");
|
|
369
295
|
}
|
|
370
|
-
return
|
|
296
|
+
return currentState.snapshot;
|
|
371
297
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
rejectForbiddenFields(data, properties, `${permissionId}.${method}`);
|
|
375
|
-
return { ...request, result: data };
|
|
376
|
-
}
|
|
298
|
+
};
|
|
299
|
+
var state_default = state;
|
|
377
300
|
|
|
378
301
|
// src/context.ts
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
302
|
+
var context = Object.freeze({
|
|
303
|
+
set(key, value) {
|
|
304
|
+
const contextKey = checkKey(key);
|
|
305
|
+
const currentState = state_default.getOrCreate();
|
|
306
|
+
state_default.requireOpen(currentState);
|
|
307
|
+
if (contextKey === "cropper") {
|
|
308
|
+
if (typeof value !== "boolean")
|
|
309
|
+
throw errors_default.create("INVALID_DEFINITION", "cropper must be a boolean");
|
|
310
|
+
currentState.cropper = value;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (contextKey === "reservedFields") {
|
|
314
|
+
currentState.reservedFields = Object.freeze(readReservedFields(value));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
currentState.roles = new Set(readRoleCatalog(value, currentState.modules.size));
|
|
318
|
+
},
|
|
319
|
+
get(key) {
|
|
320
|
+
const contextKey = checkKey(key);
|
|
321
|
+
const currentState = state_default.getOrCreate();
|
|
322
|
+
if (contextKey === "cropper")
|
|
323
|
+
return currentState.cropper;
|
|
324
|
+
const paths = contextKey === "roles" ? [...currentState.roles] : [...currentState.reservedFields];
|
|
325
|
+
return paths;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
function checkKey(key) {
|
|
329
|
+
if (key === "roles" || key === "cropper" || key === "reservedFields")
|
|
330
|
+
return key;
|
|
331
|
+
throw errors_default.create("INVALID_DEFINITION", `unknown context key: "${errors_default.describe(key)}"`);
|
|
332
|
+
}
|
|
333
|
+
function readRoleCatalog(value, registeredModuleCount) {
|
|
334
|
+
if (registeredModuleCount > 0) {
|
|
335
|
+
throw errors_default.create("INVALID_DEFINITION", "roles must be declared before registering permissions: import pkit.config.js first");
|
|
336
|
+
}
|
|
337
|
+
if (!Array.isArray(value))
|
|
338
|
+
throw errors_default.create("INVALID_DEFINITION", "roles must be an array of non-empty strings");
|
|
339
|
+
for (const role of value) {
|
|
340
|
+
if (typeof role !== "string" || role.length === 0) {
|
|
341
|
+
throw errors_default.create("INVALID_DEFINITION", "roles must be an array of non-empty strings");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const catalog = value;
|
|
345
|
+
if (catalog.length === 0)
|
|
346
|
+
throw errors_default.create("INVALID_DEFINITION", "roles must declare at least one role");
|
|
347
|
+
for (const role of catalog)
|
|
348
|
+
identifiers_default.checkRole(role);
|
|
349
|
+
return catalog;
|
|
350
|
+
}
|
|
351
|
+
function readReservedFields(value) {
|
|
352
|
+
if (!Array.isArray(value))
|
|
353
|
+
throw errors_default.create("INVALID_DEFINITION", "reservedFields must be an array of property paths");
|
|
354
|
+
const fields = [];
|
|
355
|
+
for (const field of value) {
|
|
356
|
+
if (typeof field !== "string")
|
|
357
|
+
throw errors_default.create("INVALID_DEFINITION", "reservedFields must be an array of property paths");
|
|
358
|
+
properties_default.checkDeclaredPath(field, "reservedFields");
|
|
359
|
+
fields.push(field);
|
|
360
|
+
}
|
|
361
|
+
return fields;
|
|
388
362
|
}
|
|
389
|
-
var
|
|
363
|
+
var context_default = context;
|
|
390
364
|
|
|
391
365
|
// src/cli/protocol.ts
|
|
392
|
-
var
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
366
|
+
var protocol = {
|
|
367
|
+
EXIT_CODE: { success: 0, failure: 1, usage: 2 },
|
|
368
|
+
CATALOG_MARKER: "__PKIT_CATALOG__",
|
|
369
|
+
CHILD_TIMEOUT_MS: 30000,
|
|
370
|
+
CHILD_MAX_BUFFER_BYTES: 1048576
|
|
371
|
+
};
|
|
372
|
+
var protocol_default = protocol;
|
|
396
373
|
|
|
397
374
|
// src/cli/child.ts
|
|
398
375
|
var configPath = process.argv[2];
|
|
399
376
|
if (configPath === undefined) {
|
|
400
377
|
console.error("usage: child <pkit.config.js>");
|
|
401
|
-
process.exit(EXIT_CODE.usage);
|
|
378
|
+
process.exit(protocol_default.EXIT_CODE.usage);
|
|
402
379
|
}
|
|
403
380
|
await import(pathToFileURL(resolve(configPath)).href);
|
|
404
381
|
process.stdout.write(`
|
|
405
|
-
${CATALOG_MARKER}${JSON.stringify({ roles:
|
|
382
|
+
${protocol_default.CATALOG_MARKER}${JSON.stringify({ roles: context_default.get("roles") })}
|
|
406
383
|
`);
|
|
407
384
|
|
|
408
|
-
//# debugId=
|
|
385
|
+
//# debugId=D66DB9FE1DACA1BA64756E2164756E21
|