@blamejs/core 0.7.4 → 0.7.18

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.
Files changed (180) hide show
  1. package/CHANGELOG.md +423 -395
  2. package/README.md +150 -149
  3. package/bin/blamejs.js +0 -0
  4. package/index.js +308 -284
  5. package/lib/api-key.js +660 -663
  6. package/lib/api-snapshot.js +338 -338
  7. package/lib/app-shutdown.js +385 -385
  8. package/lib/app.js +365 -365
  9. package/lib/archive.js +250 -250
  10. package/lib/atomic-file.js +544 -544
  11. package/lib/audit-chain.js +177 -177
  12. package/lib/audit-sign.js +344 -344
  13. package/lib/audit-tools.js +677 -677
  14. package/lib/audit.js +766 -766
  15. package/lib/auth/jwt.js +311 -311
  16. package/lib/auth/lockout.js +436 -436
  17. package/lib/auth/oauth.js +721 -721
  18. package/lib/auth/passkey.js +181 -181
  19. package/lib/auth/password.js +594 -594
  20. package/lib/backup/bundle.js +217 -217
  21. package/lib/backup/crypto.js +176 -176
  22. package/lib/backup/index.js +515 -515
  23. package/lib/backup/manifest.js +282 -282
  24. package/lib/break-glass.js +1338 -1338
  25. package/lib/bundler.js +441 -441
  26. package/lib/cache-redis.js +256 -256
  27. package/lib/cache.js +1206 -1206
  28. package/lib/canonical-json.js +115 -115
  29. package/lib/chain-writer.js +234 -234
  30. package/lib/cli-helpers.js +206 -206
  31. package/lib/cli.js +2334 -2334
  32. package/lib/cluster-provider-db.js +317 -317
  33. package/lib/cluster-storage.js +226 -226
  34. package/lib/cluster.js +703 -703
  35. package/lib/codepoint-class.js +196 -0
  36. package/lib/config-drift.js +301 -301
  37. package/lib/consent.js +222 -222
  38. package/lib/constants.js +191 -191
  39. package/lib/cookies.js +315 -315
  40. package/lib/credential-hash.js +322 -322
  41. package/lib/crypto.js +266 -266
  42. package/lib/csv.js +275 -286
  43. package/lib/db-declare-row-policy.js +267 -267
  44. package/lib/db-declare-view.js +420 -421
  45. package/lib/db-query.js +406 -406
  46. package/lib/db-schema.js +319 -319
  47. package/lib/db.js +1288 -1288
  48. package/lib/deprecate.js +222 -222
  49. package/lib/dev.js +335 -335
  50. package/lib/dual-control.js +473 -473
  51. package/lib/error-page.js +420 -420
  52. package/lib/external-db-migrate.js +441 -441
  53. package/lib/external-db.js +1061 -1061
  54. package/lib/file-type.js +273 -273
  55. package/lib/file-upload.js +213 -10
  56. package/lib/forms.js +422 -422
  57. package/lib/framework-error.js +293 -215
  58. package/lib/framework-schema.js +717 -717
  59. package/lib/gate-contract.js +971 -0
  60. package/lib/guard-all.js +405 -0
  61. package/lib/guard-archive.js +739 -0
  62. package/lib/guard-csv.js +816 -0
  63. package/lib/guard-email.js +744 -0
  64. package/lib/guard-filename.js +724 -0
  65. package/lib/guard-html.js +976 -0
  66. package/lib/guard-json.js +729 -0
  67. package/lib/guard-markdown.js +586 -0
  68. package/lib/guard-svg.js +976 -0
  69. package/lib/guard-xml.js +405 -0
  70. package/lib/guard-yaml.js +529 -0
  71. package/lib/handlers.js +350 -350
  72. package/lib/http-client-cookie-jar.js +508 -508
  73. package/lib/http-client.js +1195 -1195
  74. package/lib/i18n.js +878 -878
  75. package/lib/jobs.js +185 -185
  76. package/lib/log-stream-cloudwatch.js +369 -369
  77. package/lib/log-stream-local.js +146 -146
  78. package/lib/log-stream-otlp-grpc.js +410 -410
  79. package/lib/log-stream-otlp.js +286 -286
  80. package/lib/log-stream-syslog.js +302 -302
  81. package/lib/log-stream-webhook.js +199 -199
  82. package/lib/log-stream.js +330 -330
  83. package/lib/log.js +500 -500
  84. package/lib/mail-bounce.js +528 -528
  85. package/lib/mail-dkim.js +369 -362
  86. package/lib/mail.js +981 -962
  87. package/lib/metrics.js +683 -683
  88. package/lib/middleware/api-encrypt.js +936 -936
  89. package/lib/middleware/attach-user.js +157 -157
  90. package/lib/middleware/body-parser.js +1170 -1091
  91. package/lib/middleware/bot-guard.js +178 -178
  92. package/lib/middleware/compression.js +452 -452
  93. package/lib/middleware/cors.js +314 -314
  94. package/lib/middleware/csp-nonce.js +348 -348
  95. package/lib/middleware/csrf-protect.js +316 -316
  96. package/lib/middleware/db-role-for.js +264 -264
  97. package/lib/middleware/health.js +392 -392
  98. package/lib/middleware/index.js +79 -79
  99. package/lib/middleware/rate-limit.js +358 -358
  100. package/lib/middleware/request-id.js +61 -61
  101. package/lib/middleware/request-log.js +168 -168
  102. package/lib/middleware/require-auth.js +104 -104
  103. package/lib/middleware/security-headers.js +116 -116
  104. package/lib/middleware/sse.js +166 -166
  105. package/lib/migrations.js +383 -383
  106. package/lib/mtls-ca.js +518 -518
  107. package/lib/mtls-engine-default.js +481 -481
  108. package/lib/network-dns.js +632 -632
  109. package/lib/network-heartbeat.js +290 -290
  110. package/lib/network-nts.js +574 -574
  111. package/lib/network-proxy.js +265 -265
  112. package/lib/network-tls.js +328 -328
  113. package/lib/network.js +233 -233
  114. package/lib/notify.js +612 -612
  115. package/lib/ntp-check.js +229 -229
  116. package/lib/numeric-bounds.js +111 -91
  117. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  118. package/lib/object-store/azure-blob.js +488 -488
  119. package/lib/object-store/gcs-bucket-ops.js +351 -351
  120. package/lib/object-store/gcs.js +519 -519
  121. package/lib/object-store/http-put.js +153 -153
  122. package/lib/object-store/index.js +197 -197
  123. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  124. package/lib/object-store/sigv4.js +903 -903
  125. package/lib/observability.js +151 -151
  126. package/lib/otel-export.js +269 -269
  127. package/lib/pagination.js +464 -464
  128. package/lib/parsers/index.js +80 -80
  129. package/lib/parsers/safe-env.js +642 -642
  130. package/lib/parsers/safe-ini.js +292 -292
  131. package/lib/parsers/safe-toml.js +784 -784
  132. package/lib/parsers/safe-xml.js +390 -390
  133. package/lib/parsers/safe-yaml.js +1015 -1015
  134. package/lib/permissions.js +708 -708
  135. package/lib/pqc-agent.js +87 -87
  136. package/lib/pqc-gate.js +279 -279
  137. package/lib/protobuf-encoder.js +190 -190
  138. package/lib/protocol-dispatcher.js +161 -161
  139. package/lib/pubsub-redis.js +167 -167
  140. package/lib/pubsub.js +429 -429
  141. package/lib/queue-local.js +476 -476
  142. package/lib/queue-redis.js +745 -745
  143. package/lib/queue-sqs.js +319 -319
  144. package/lib/queue.js +695 -695
  145. package/lib/redis-client.js +519 -519
  146. package/lib/request-helpers.js +340 -340
  147. package/lib/restore-bundle.js +237 -237
  148. package/lib/restore-rollback.js +259 -259
  149. package/lib/restore.js +409 -409
  150. package/lib/retry.js +376 -376
  151. package/lib/router.js +748 -748
  152. package/lib/safe-async.js +735 -735
  153. package/lib/safe-buffer.js +237 -237
  154. package/lib/safe-json.js +541 -541
  155. package/lib/safe-schema.js +1266 -1266
  156. package/lib/safe-url.js +159 -159
  157. package/lib/scheduler.js +706 -706
  158. package/lib/security-assert.js +373 -373
  159. package/lib/seeders.js +618 -618
  160. package/lib/session.js +478 -478
  161. package/lib/slug.js +269 -269
  162. package/lib/ssrf-guard.js +401 -401
  163. package/lib/static.js +184 -4
  164. package/lib/storage.js +471 -471
  165. package/lib/subject.js +281 -281
  166. package/lib/template.js +791 -791
  167. package/lib/testing.js +798 -798
  168. package/lib/time.js +310 -310
  169. package/lib/totp.js +302 -302
  170. package/lib/tracing.js +494 -494
  171. package/lib/uuid.js +132 -132
  172. package/lib/validate-opts.js +340 -319
  173. package/lib/vault/index.js +308 -308
  174. package/lib/vault/rotate.js +784 -784
  175. package/lib/vault/wrap.js +296 -296
  176. package/lib/vendor/noble-ciphers.cjs +9 -9
  177. package/lib/webhook.js +595 -595
  178. package/lib/websocket.js +1048 -1048
  179. package/package.json +77 -77
  180. package/sbom.cyclonedx.json +7 -7
@@ -1,708 +1,708 @@
1
- "use strict";
2
- /**
3
- * b.permissions — RBAC primitive.
4
- *
5
- * var perms = b.permissions.create({
6
- * roles: {
7
- * admin: { extends: ["editor"], permissions: ["users:delete"] },
8
- * editor: ["users:read", "users:write", "posts:*"],
9
- * viewer: ["*:read"],
10
- * },
11
- * audit: b.audit, // optional
12
- * });
13
- *
14
- * router.delete("/users/:id",
15
- * authMiddleware, // populates req.user / req.apiKey
16
- * perms.require("users:delete"),
17
- * deleteUserHandler);
18
- *
19
- * The default resolver chain reads the actor from the request:
20
- *
21
- * req.apiKey.scopes → { scopes: [...] } (b.apiKey.verify output)
22
- * req.user.scopes → { scopes: [...] } (operator-set)
23
- * req.user.roles → { roles: [...] } (operator-set)
24
- *
25
- * Operators with non-default request shapes pass `resolver` to create().
26
- *
27
- * Wildcard semantics (b.permissions.match):
28
- * "*" matches any scope (greedy)
29
- * "users:*" matches "users:read", "users:read:detail", etc. (trailing * is greedy)
30
- * "*:read" matches "users:read", "posts:read"
31
- * "users:*:read" matches "users:foo:read" (per-segment *)
32
- * "users:read" matches "users:read" only — no implicit sub-resource grant
33
- *
34
- * Validation policy:
35
- *
36
- * - create() role table / scope formats → throw at app init
37
- * - require(scope) registration arg → throw at route declaration
38
- * - check(actor, scope) bad actor → return false (tolerant read)
39
- * - resolver returns null in middleware → 401 (missingActorStatus)
40
- * - actor lacks scope in middleware → 403 (denyStatus)
41
- * - audit/observability emit failures → drop silent (hot-path sink)
42
- *
43
- * Audit defaults follow the framework's security-defaults stance
44
- * default: `auditFailures: true`
45
- * (deny is a security signal), `auditSuccess: false` (per-request noise).
46
- */
47
-
48
- var C = require("./constants");
49
- var lazyRequire = require("./lazy-require");
50
- var requestHelpers = require("./request-helpers");
51
- var safeSql = require("./safe-sql");
52
- var validateOpts = require("./validate-opts");
53
- var { PermissionsError } = require("./framework-error");
54
-
55
- var _err = PermissionsError.factory;
56
-
57
- var observability = lazyRequire(function () { return require("./observability"); });
58
-
59
- function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
60
-
61
- // Lowercase tokens, digits, dash, underscore, and `*` allowed per
62
- // segment. Scope format is segments separated by `:`.
63
- var SCOPE_RE = /^[a-z0-9_*-]+(:[a-z0-9_*-]+)*$/;
64
- // Bound the regex engine on operator-supplied scope strings. 256 chars
65
- // holds any realistic real-world scope (typical scopes run 8-32 chars);
66
- // rejecting longer keeps the regex linear regardless of input shape.
67
- var SCOPE_MAX_LENGTH = C.BYTES.bytes(256);
68
-
69
- // Audit defaults: BOTH success and failure default ON for permissions.
70
- // Unlike api-key.verify (which is gate-keeping for a downstream action
71
- // the application separately audits), a permissions.check IS the
72
- // authorization decision — there's no further-downstream audit event.
73
- // "user X granted users:delete at time T" is exactly what compliance
74
- // auditors ask for. Operators with extreme volume opt out via
75
- // auditSuccess: false; failures remain on regardless.
76
- var DEFAULTS = Object.freeze({
77
- auditFailures: true,
78
- auditSuccess: true,
79
- denyStatus: 403,
80
- missingActorStatus: 401,
81
- });
82
-
83
- // ---- Wildcard matcher ----
84
-
85
- function match(granted, required) {
86
- if (typeof granted !== "string" || typeof required !== "string") return false;
87
- if (granted.length === 0 || required.length === 0) return false;
88
- var gParts = granted.split(":");
89
- var rParts = required.split(":");
90
- for (var i = 0; i < gParts.length; i++) {
91
- var g = gParts[i];
92
- if (g === "*") {
93
- // Trailing * is greedy — matches the rest of required.
94
- if (i === gParts.length - 1) return true;
95
- // Per-segment * — matches THIS segment of required (any value),
96
- // continue to next segment. Required must have a segment here.
97
- if (i >= rParts.length) return false;
98
- continue;
99
- }
100
- if (i >= rParts.length) return false; // granted is more specific than required
101
- if (g !== rParts[i]) return false;
102
- }
103
- // Reached end of granted without wildcard. Lengths must match exactly
104
- // (no implicit sub-resource grant).
105
- return rParts.length === gParts.length;
106
- }
107
-
108
- // ---- Role table validation + expansion ----
109
-
110
- function _validateScopePattern(scope, ctx) {
111
- if (typeof scope !== "string" || scope.length === 0) {
112
- throw _err("BAD_SCOPE", ctx + ": scope must be a non-empty string, got " + typeof scope);
113
- }
114
- // Length cap before the regex test — bound the engine on hostile
115
- // input lengths even though SCOPE_RE is anchored.
116
- if (scope.length > SCOPE_MAX_LENGTH || !SCOPE_RE.test(scope)) {
117
- throw _err("BAD_SCOPE", ctx + ": scope '" + scope +
118
- "' is empty, too long, or doesn't match " + SCOPE_RE +
119
- " (lowercase tokens with optional `*`)");
120
- }
121
- }
122
-
123
- function _normalizeRoleEntry(name, entry) {
124
- if (Array.isArray(entry)) {
125
- return { extends: [], permissions: entry.slice(), dbRole: null,
126
- requireMfa: false, mfaWindowMs: null };
127
- }
128
- if (entry && typeof entry === "object") {
129
- var ext = entry.extends || [];
130
- var perms = entry.permissions || [];
131
- if (!Array.isArray(ext)) {
132
- throw _err("BAD_ROLE", "role '" + name + "': extends must be an array of role names");
133
- }
134
- if (!Array.isArray(perms)) {
135
- throw _err("BAD_ROLE", "role '" + name + "': permissions must be an array of scope strings");
136
- }
137
- var dbRole = null;
138
- if (entry.dbRole !== undefined && entry.dbRole !== null) {
139
- if (typeof entry.dbRole !== "string" || entry.dbRole.length === 0) {
140
- throw _err("BAD_ROLE",
141
- "role '" + name + "': dbRole must be a non-empty string");
142
- }
143
- // dbRole feeds straight into externalDb backend pick + the
144
- // dbRoleFor middleware's identifier check; validate at create()
145
- // time so a typo surfaces at boot, not on the first request.
146
- try {
147
- safeSql.validateIdentifier(entry.dbRole, { allowReserved: false });
148
- } catch (e) {
149
- throw _err("BAD_ROLE",
150
- "role '" + name + "': dbRole '" + entry.dbRole +
151
- "' is not a valid SQL identifier: " + ((e && e.message) || String(e)));
152
- }
153
- dbRole = entry.dbRole;
154
- }
155
- var requireMfa = entry.requireMfa === true;
156
- var mfaWindowMs = null;
157
- if (entry.mfaWindowMs !== undefined && entry.mfaWindowMs !== null) {
158
- if (typeof entry.mfaWindowMs !== "number" || !isFinite(entry.mfaWindowMs) || entry.mfaWindowMs <= 0) {
159
- throw _err("BAD_ROLE",
160
- "role '" + name + "': mfaWindowMs must be a positive finite number");
161
- }
162
- mfaWindowMs = entry.mfaWindowMs;
163
- }
164
- return { extends: ext.slice(), permissions: perms.slice(), dbRole: dbRole,
165
- requireMfa: requireMfa, mfaWindowMs: mfaWindowMs };
166
- }
167
- throw _err("BAD_ROLE", "role '" + name + "' must be an array of scopes or { extends?, permissions, dbRole?, requireMfa?, mfaWindowMs? }");
168
- }
169
-
170
- function _validateRoles(roles) {
171
- if (!roles || typeof roles !== "object" || Array.isArray(roles)) {
172
- throw _err("BAD_OPT", "permissions.create: roles must be an object map of name → spec");
173
- }
174
- var names = Object.keys(roles);
175
- if (names.length === 0) {
176
- throw _err("BAD_OPT", "permissions.create: roles map must have at least one role");
177
- }
178
- var normalized = {};
179
- for (var i = 0; i < names.length; i++) {
180
- var name = names[i];
181
- if (typeof name !== "string" || name.length === 0) {
182
- throw _err("BAD_ROLE", "role name must be a non-empty string");
183
- }
184
- var spec = _normalizeRoleEntry(name, roles[name]);
185
- for (var j = 0; j < spec.permissions.length; j++) {
186
- _validateScopePattern(spec.permissions[j], "role '" + name + "'");
187
- }
188
- for (var k = 0; k < spec.extends.length; k++) {
189
- if (typeof spec.extends[k] !== "string" || spec.extends[k].length === 0) {
190
- throw _err("BAD_ROLE", "role '" + name + "': extends entry must be a non-empty string");
191
- }
192
- }
193
- normalized[name] = spec;
194
- }
195
- // Check extends references resolve to known roles
196
- for (var n = 0; n < names.length; n++) {
197
- var spec2 = normalized[names[n]];
198
- for (var m = 0; m < spec2.extends.length; m++) {
199
- if (!Object.prototype.hasOwnProperty.call(normalized, spec2.extends[m])) {
200
- throw _err("UNKNOWN_ROLE", "role '" + names[n] + "': extends references unknown role '" +
201
- spec2.extends[m] + "'");
202
- }
203
- }
204
- }
205
- // Cycle detection via DFS
206
- for (var p = 0; p < names.length; p++) {
207
- _detectCycle(names[p], normalized, []);
208
- }
209
- return normalized;
210
- }
211
-
212
- function _detectCycle(roleName, table, stack) {
213
- if (stack.indexOf(roleName) !== -1) {
214
- throw _err("CYCLE", "permissions.create: cycle in extends chain: " +
215
- stack.concat([roleName]).join(" → "));
216
- }
217
- var spec = table[roleName];
218
- for (var i = 0; i < spec.extends.length; i++) {
219
- _detectCycle(spec.extends[i], table, stack.concat([roleName]));
220
- }
221
- }
222
-
223
- function _expandOne(roleName, table, visited, out) {
224
- if (visited.has(roleName)) return;
225
- visited.add(roleName);
226
- var spec = table[roleName];
227
- if (!spec) return;
228
- for (var i = 0; i < spec.extends.length; i++) {
229
- _expandOne(spec.extends[i], table, visited, out);
230
- }
231
- for (var j = 0; j < spec.permissions.length; j++) {
232
- if (out.indexOf(spec.permissions[j]) === -1) out.push(spec.permissions[j]);
233
- }
234
- }
235
-
236
- // ---- Default resolver ----
237
-
238
- function _defaultResolver(req) {
239
- if (!req || typeof req !== "object") return null;
240
- if (req.apiKey && Array.isArray(req.apiKey.scopes)) return { scopes: req.apiKey.scopes };
241
- if (req.user && Array.isArray(req.user.scopes)) return { scopes: req.user.scopes };
242
- if (req.user && Array.isArray(req.user.roles)) return { roles: req.user.roles };
243
- return null;
244
- }
245
-
246
- // ---- Validation: create opts ----
247
-
248
- function _validateCreateOpts(opts) {
249
- validateOpts.requireObject(opts, "permissions.create", PermissionsError);
250
- validateOpts.optionalFunction(opts.resolver, "permissions.create: resolver", PermissionsError);
251
- validateOpts.auditShape(opts.audit, "permissions.create", PermissionsError);
252
- validateOpts.optionalBoolean(opts.auditFailures, "permissions.create: auditFailures", PermissionsError);
253
- validateOpts.optionalBoolean(opts.auditSuccess, "permissions.create: auditSuccess", PermissionsError);
254
- if (opts.denyStatus !== undefined &&
255
- (typeof opts.denyStatus !== "number" || !isFinite(opts.denyStatus) || opts.denyStatus < 100 || opts.denyStatus > 599)) {
256
- throw _err("BAD_OPT", "permissions.create: denyStatus must be an HTTP status code (100-599)");
257
- }
258
- if (opts.missingActorStatus !== undefined &&
259
- (typeof opts.missingActorStatus !== "number" || !isFinite(opts.missingActorStatus) ||
260
- opts.missingActorStatus < 100 || opts.missingActorStatus > 599)) {
261
- throw _err("BAD_OPT", "permissions.create: missingActorStatus must be an HTTP status code (100-599)");
262
- }
263
- validateOpts.optionalFunction(opts.responder, "permissions.create: responder", PermissionsError);
264
- }
265
-
266
- // ---- Registry ----
267
-
268
- function create(opts) {
269
- opts = opts || {};
270
- validateOpts(opts, [
271
- "roles", "resolver", "audit", "auditFailures", "auditSuccess",
272
- "denyStatus", "missingActorStatus", "responder",
273
- ], "permissions");
274
- _validateCreateOpts(opts);
275
- var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
276
- var roleTable = _validateRoles(opts.roles);
277
- var resolver = opts.resolver || _defaultResolver;
278
- var audit = opts.audit || null;
279
- var auditFailures = cfg.auditFailures;
280
- var auditSuccess = cfg.auditSuccess;
281
- var denyStatus = cfg.denyStatus;
282
- var missingActorStatus = cfg.missingActorStatus;
283
- var responder = opts.responder || _defaultResponder;
284
-
285
- // ABAC predicate registry. Each entry: scope-string → async predicate
286
- // function (actor, context) → boolean. The middleware evaluates the
287
- // predicate AFTER the RBAC scope check passes — so a route protected
288
- // by `perms.require("orders.read")` first checks the actor has the
289
- // orders:read scope, then (if the scope has a policy registered)
290
- // evaluates the predicate with the actor + a per-request context
291
- // built by the route's `context` middleware opt. ABAC + RBAC stack
292
- // — a route needs to pass BOTH layers when both are configured.
293
- var policies = {};
294
-
295
- function policy(scope, predicate) {
296
- _validateScopePattern(scope, "permissions.policy");
297
- if (typeof predicate !== "function") {
298
- throw _err("BAD_OPT", "permissions.policy: predicate must be a function (actor, context) -> bool");
299
- }
300
- if (policies[scope]) {
301
- throw _err("DUPLICATE_POLICY", "permissions.policy: '" + scope + "' is already registered");
302
- }
303
- policies[scope] = predicate;
304
- }
305
-
306
- function _findPolicy(requestedScope) {
307
- // Exact match wins; no wildcard expansion (a wildcard policy
308
- // gating arbitrary scopes is too easy to misconfigure).
309
- return policies[requestedScope] || null;
310
- }
311
-
312
- var _emitRaw = validateOpts.makeAuditEmitter(audit);
313
- function _auditEmit(action, info) {
314
- if (info && info.outcome === "success" && !auditSuccess) return;
315
- if (info && info.outcome !== "success" && !auditFailures) return;
316
- _emitRaw(action, info);
317
- }
318
-
319
- function expand(roleNames) {
320
- if (!Array.isArray(roleNames)) return [];
321
- var visited = new Set();
322
- var out = [];
323
- for (var i = 0; i < roleNames.length; i++) {
324
- if (typeof roleNames[i] === "string" && Object.prototype.hasOwnProperty.call(roleTable, roleNames[i])) {
325
- _expandOne(roleNames[i], roleTable, visited, out);
326
- }
327
- }
328
- return out;
329
- }
330
-
331
- function _actorScopes(actor) {
332
- if (!actor || typeof actor !== "object") return [];
333
- if (Array.isArray(actor.scopes)) return actor.scopes;
334
- if (Array.isArray(actor.roles)) return expand(actor.roles);
335
- return [];
336
- }
337
-
338
- function check(actor, requiredScope) {
339
- var scopes = _actorScopes(actor);
340
- for (var i = 0; i < scopes.length; i++) {
341
- if (typeof scopes[i] === "string" && match(scopes[i], requiredScope)) return true;
342
- }
343
- return false;
344
- }
345
-
346
- function checkAll(actor, requiredScopes) {
347
- if (!Array.isArray(requiredScopes)) return false;
348
- for (var i = 0; i < requiredScopes.length; i++) {
349
- if (!check(actor, requiredScopes[i])) return false;
350
- }
351
- return requiredScopes.length > 0;
352
- }
353
-
354
- function checkAny(actor, requiredScopes) {
355
- if (!Array.isArray(requiredScopes)) return false;
356
- for (var i = 0; i < requiredScopes.length; i++) {
357
- if (check(actor, requiredScopes[i])) return true;
358
- }
359
- return false;
360
- }
361
-
362
- // Middleware factory. `mode` is "single" | "all" | "any"; `requested`
363
- // is the scope or scope list. Throw at registration time on bad shape.
364
- function _middleware(mode, requested, mwOpts) {
365
- if (mode === "single") {
366
- _validateScopePattern(requested, "permissions.require");
367
- } else {
368
- if (!Array.isArray(requested) || requested.length === 0) {
369
- throw _err("BAD_OPT", "permissions." + (mode === "all" ? "requireAll" : "requireAny") +
370
- ": scopes must be a non-empty array");
371
- }
372
- for (var i = 0; i < requested.length; i++) {
373
- _validateScopePattern(requested[i], "permissions." + (mode === "all" ? "requireAll" : "requireAny"));
374
- }
375
- }
376
-
377
- // Per-route MFA enforcement opts: { requireMfa, mfaWindowMs }.
378
- // When set, the middleware blocks unless the actor's mfaAuthenticated
379
- // flag is truthy AND (when mfaWindowMs is set) actor.mfaAt is fresher
380
- // than (now - mfaWindowMs). The actor signal is operator-set: after
381
- // a successful TOTP / passkey step-up, the route handler stamps
382
- // req.user.mfaAuthenticated = true and req.user.mfaAt = Date.now().
383
- mwOpts = mwOpts || {};
384
- var routeRequireMfa = mwOpts.requireMfa === true;
385
- var routeMfaWindowMs = null;
386
- if (mwOpts.mfaWindowMs !== undefined && mwOpts.mfaWindowMs !== null) {
387
- if (typeof mwOpts.mfaWindowMs !== "number" || !isFinite(mwOpts.mfaWindowMs) || mwOpts.mfaWindowMs <= 0) {
388
- throw _err("BAD_OPT", "permissions middleware: mfaWindowMs must be a positive finite number");
389
- }
390
- routeMfaWindowMs = mwOpts.mfaWindowMs;
391
- }
392
- // ABAC context provider — operator-supplied function (req)→object.
393
- // The function runs once per request, AFTER scope/MFA pass, BEFORE
394
- // the policy predicate. Whatever it returns is passed to the
395
- // policy as `context`. Async functions are awaited.
396
- var contextProvider = mwOpts.context;
397
- if (contextProvider !== undefined && typeof contextProvider !== "function") {
398
- throw _err("BAD_OPT", "permissions middleware: context must be a function (req) -> object");
399
- }
400
-
401
- return async function permissionsMiddleware(req, res, next) {
402
- var actor = resolver(req);
403
- if (!actor) {
404
- // Diagnostic: the most common cause of a null actor is that
405
- // attachUser/auth wasn't mounted before this middleware, so
406
- // req.user / req.apiKey are still undefined. Emit a hint —
407
- // operators tracing a 401 here see exactly what to check first.
408
- var hint = (req && (req.user || req.apiKey))
409
- ? "actor present on req but resolver returned null — check resolver implementation"
410
- : "no req.user or req.apiKey — confirm attachUser / apiKey-verify middleware is mounted before perms.require()";
411
- _emitEvent("permissions.missing_actor", 1,
412
- { requested: _labelize(requested) });
413
- _auditEmit("permissions.missing_actor", {
414
- actor: _actorAuditShape(null, req),
415
- resource: { kind: "permission", id: _labelize(requested) },
416
- outcome: "failure",
417
- reason: "no-actor",
418
- metadata: { hint: hint },
419
- });
420
- return responder(req, res, missingActorStatus, {
421
- error: "missing_actor",
422
- status: missingActorStatus,
423
- });
424
- }
425
-
426
- var ok;
427
- if (mode === "single") ok = check(actor, requested);
428
- else if (mode === "all") ok = checkAll(actor, requested);
429
- else ok = checkAny(actor, requested);
430
-
431
- if (!ok) {
432
- _emitEvent("permissions.check", 1,
433
- { outcome: "deny", requested: _labelize(requested), mode: mode });
434
- _auditEmit("permissions.check.deny", {
435
- actor: _actorAuditShape(actor, req),
436
- resource: { kind: "permission", id: _labelize(requested) },
437
- outcome: "failure",
438
- reason: "forbidden",
439
- metadata: { mode: mode },
440
- });
441
- return responder(req, res, denyStatus, {
442
- error: "forbidden",
443
- status: denyStatus,
444
- requested: _labelize(requested),
445
- });
446
- }
447
-
448
- // MFA enforcement gate. Two sources of "this needs MFA":
449
- // 1. Per-route opt: perms.require("scope", { requireMfa: true })
450
- // 2. Per-role flag: a role spec with requireMfa:true that
451
- // contributes to satisfying the requested scope
452
- // Either source enabling MFA forces the gate. mfaWindowMs (per-route
453
- // OR per-role, route wins on conflict) bounds freshness — without
454
- // it, ANY past MFA stamp counts (which is too permissive for high-
455
- // value routes; operators set a window like C.TIME.minutes(15)).
456
- var enforceMfa = routeRequireMfa;
457
- var enforceWindowMs = routeMfaWindowMs;
458
- if (!enforceMfa) {
459
- // Walk the actor's roles and check whether any role with
460
- // requireMfa=true contributes a permission that matches the
461
- // requested scope. If so, MFA is required regardless of the
462
- // route-level opt.
463
- var actorRoles = Array.isArray(actor.roles) ? actor.roles : [];
464
- for (var ri = 0; ri < actorRoles.length; ri++) {
465
- var rname = actorRoles[ri];
466
- if (typeof rname !== "string") continue;
467
- var rspec = roleTable[rname];
468
- if (!rspec || !rspec.requireMfa) continue;
469
- // Cheap match: if the role grants any scope that satisfies the
470
- // requested scope (single mode) or any of the requested
471
- // (all/any modes), MFA is required for this route.
472
- var visited = new Set();
473
- var roleScopes = [];
474
- _expandOne(rname, roleTable, visited, roleScopes);
475
- var roleMatches = false;
476
- var requestedList = mode === "single" ? [requested] : requested;
477
- outer: for (var rj = 0; rj < roleScopes.length; rj++) {
478
- for (var rk = 0; rk < requestedList.length; rk++) {
479
- if (match(roleScopes[rj], requestedList[rk])) {
480
- roleMatches = true; break outer;
481
- }
482
- }
483
- }
484
- if (roleMatches) {
485
- enforceMfa = true;
486
- if (enforceWindowMs === null && rspec.mfaWindowMs !== null) {
487
- enforceWindowMs = rspec.mfaWindowMs;
488
- }
489
- }
490
- }
491
- }
492
-
493
- if (enforceMfa) {
494
- var mfaOk = actor.mfaAuthenticated === true;
495
- if (mfaOk && enforceWindowMs !== null) {
496
- var mfaAt = typeof actor.mfaAt === "number" ? actor.mfaAt : 0;
497
- if (Date.now() - mfaAt > enforceWindowMs) {
498
- mfaOk = false;
499
- }
500
- }
501
- if (!mfaOk) {
502
- _emitEvent("permissions.mfa_required", 1,
503
- { requested: _labelize(requested), mode: mode });
504
- _auditEmit("permissions.mfa.required", {
505
- actor: _actorAuditShape(actor, req),
506
- resource: { kind: "permission", id: _labelize(requested) },
507
- outcome: "denied",
508
- reason: "mfa-required",
509
- metadata: { mode: mode, windowMs: enforceWindowMs },
510
- });
511
- return responder(req, res, denyStatus, {
512
- error: "mfa_required",
513
- status: denyStatus,
514
- requested: _labelize(requested),
515
- });
516
- }
517
- }
518
-
519
- // ABAC layer fires for every requested scope that has a
520
- // registered policy predicate. Single-mode evaluates the one
521
- // scope; requireAll evaluates each scope's policy (every must
522
- // pass); requireAny evaluates only the policies on scopes the
523
- // actor's RBAC layer satisfied (so a failing policy on a scope
524
- // the actor doesn't even hold doesn't leak the policy's
525
- // existence). Each predicate failure short-circuits with a
526
- // policy.deny audit row naming the failing scope.
527
- var policyTargets = [];
528
- if (mode === "single" && _findPolicy(requested)) {
529
- policyTargets.push(requested);
530
- } else if (mode === "all" || mode === "any") {
531
- for (var pi = 0; pi < requested.length; pi++) {
532
- if (_findPolicy(requested[pi])) {
533
- if (mode === "any" && !check(actor, requested[pi])) continue;
534
- policyTargets.push(requested[pi]);
535
- }
536
- }
537
- }
538
- if (policyTargets.length > 0) {
539
- var policyContext = null;
540
- if (contextProvider) {
541
- try {
542
- policyContext = await contextProvider(req);
543
- } catch (e) {
544
- _emitEvent("permissions.policy_context_error", 1,
545
- { requested: _labelize(requested) });
546
- _auditEmit("permissions.policy.error", {
547
- actor: _actorAuditShape(actor, req),
548
- resource: { kind: "permission", id: _labelize(requested) },
549
- outcome: "failure",
550
- reason: "context-provider-threw",
551
- metadata: { error: (e && e.message) || String(e), mode: mode },
552
- });
553
- return responder(req, res, denyStatus, {
554
- error: "policy_context_error",
555
- status: denyStatus,
556
- requested: _labelize(requested),
557
- });
558
- }
559
- }
560
- for (var pti = 0; pti < policyTargets.length; pti++) {
561
- var thisScope = policyTargets[pti];
562
- var pred = _findPolicy(thisScope);
563
- var verdict;
564
- try {
565
- verdict = await pred(actor, policyContext);
566
- } catch (e2) {
567
- _emitEvent("permissions.policy_error", 1, { requested: thisScope });
568
- _auditEmit("permissions.policy.error", {
569
- actor: _actorAuditShape(actor, req),
570
- resource: { kind: "permission", id: thisScope },
571
- outcome: "failure",
572
- reason: "predicate-threw",
573
- metadata: { error: (e2 && e2.message) || String(e2), mode: mode },
574
- });
575
- return responder(req, res, denyStatus, {
576
- error: "policy_error",
577
- status: denyStatus,
578
- requested: thisScope,
579
- });
580
- }
581
- if (verdict !== true) {
582
- _emitEvent("permissions.policy_denied", 1, { requested: thisScope });
583
- _auditEmit("permissions.policy.deny", {
584
- actor: _actorAuditShape(actor, req),
585
- resource: { kind: "permission", id: thisScope },
586
- outcome: "failure",
587
- reason: "policy-predicate-returned-falsy",
588
- metadata: { mode: mode, scopeIndex: pti },
589
- });
590
- return responder(req, res, denyStatus, {
591
- error: "policy_denied",
592
- status: denyStatus,
593
- requested: thisScope,
594
- });
595
- }
596
- }
597
- }
598
-
599
- _emitEvent("permissions.check", 1,
600
- { outcome: "success", mode: mode });
601
- _auditEmit("permissions.check.success", {
602
- actor: _actorAuditShape(actor, req),
603
- resource: { kind: "permission", id: _labelize(requested) },
604
- outcome: "success",
605
- metadata: { mode: mode, mfaEnforced: enforceMfa },
606
- });
607
- next();
608
- };
609
- }
610
-
611
- // dbRoleFor — walk the actor's roles in order and return the first
612
- // declared dbRole. Composes with b.middleware.dbRoleFor so a single
613
- // RBAC table drives both authorization scopes and request-time DB
614
- // role binding.
615
- //
616
- // The arg can be the request (default resolver pulls actor from
617
- // req.user / req.apiKey) OR an actor object directly. Returns null if
618
- // no actor is found OR the actor's roles don't include any with a
619
- // declared dbRole.
620
- //
621
- // Lookup order: extends are walked depth-first so a child role that
622
- // overrides dbRole takes precedence over its parent. When multiple
623
- // top-level roles are listed, the first wins (operators wanting a
624
- // priority order should list more-specific roles first).
625
- function dbRoleFor(reqOrActor) {
626
- var actor = reqOrActor;
627
- // Heuristic: a request shape carries headers / url; resolve through
628
- // the configured resolver. An actor shape has roles / scopes
629
- // directly.
630
- if (actor && (actor.headers || actor.url || actor.method)) {
631
- actor = resolver(actor);
632
- }
633
- if (!actor || typeof actor !== "object") return null;
634
- var roleNames = Array.isArray(actor.roles) ? actor.roles : null;
635
- if (!roleNames || roleNames.length === 0) return null;
636
- // Walk the same DFS order expand() uses so the first-seen dbRole
637
- // is consistent with how scopes are inherited.
638
- var visited = new Set();
639
- for (var i = 0; i < roleNames.length; i++) {
640
- var name = roleNames[i];
641
- if (typeof name !== "string") continue;
642
- if (!Object.prototype.hasOwnProperty.call(roleTable, name)) continue;
643
- var found = _findDbRole(name, roleTable, visited);
644
- if (found) return found;
645
- }
646
- return null;
647
- }
648
-
649
- return {
650
- require: function (scope, mwOpts) { return _middleware("single", scope, mwOpts); },
651
- requireAll: function (scopes, mwOpts) { return _middleware("all", scopes, mwOpts); },
652
- requireAny: function (scopes, mwOpts) { return _middleware("any", scopes, mwOpts); },
653
- policy: policy,
654
- check: check,
655
- checkAll: checkAll,
656
- checkAny: checkAny,
657
- expand: expand,
658
- dbRoleFor: dbRoleFor,
659
- has: function (name) { return Object.prototype.hasOwnProperty.call(roleTable, name); },
660
- roles: Object.freeze(Object.keys(roleTable)),
661
- };
662
- }
663
-
664
- function _findDbRole(roleName, table, visited) {
665
- if (visited.has(roleName)) return null;
666
- visited.add(roleName);
667
- var spec = table[roleName];
668
- if (!spec) return null;
669
- // Child overrides parent — check this role's own dbRole first, then
670
- // recurse into extends.
671
- if (spec.dbRole) return spec.dbRole;
672
- for (var i = 0; i < spec.extends.length; i++) {
673
- var found = _findDbRole(spec.extends[i], table, visited);
674
- if (found) return found;
675
- }
676
- return null;
677
- }
678
-
679
- // ---- Helpers ----
680
-
681
- function _labelize(requested) {
682
- return Array.isArray(requested) ? requested.join(",") : String(requested);
683
- }
684
-
685
- function _actorAuditShape(actor, req) {
686
- // Pull the 5 W's (WHO/WHERE/HOW) from the request, then layer the
687
- // resolver-supplied actor identity on top so userId/roles/scopes
688
- // aren't lost when the request itself doesn't carry them.
689
- var base = requestHelpers.extractActorContext(req);
690
- if (actor) {
691
- if (actor.userId) base.userId = actor.userId;
692
- if (Array.isArray(actor.roles)) base.roles = actor.roles.slice();
693
- if (Array.isArray(actor.scopes)) base.scopes = actor.scopes.slice();
694
- }
695
- return base;
696
- }
697
-
698
- function _defaultResponder(req, res, status, info) {
699
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
700
- res.end(JSON.stringify(info));
701
- }
702
-
703
- module.exports = {
704
- create: create,
705
- match: match,
706
- PermissionsError: PermissionsError,
707
- DEFAULTS: DEFAULTS,
708
- };
1
+ "use strict";
2
+ /**
3
+ * b.permissions — RBAC primitive.
4
+ *
5
+ * var perms = b.permissions.create({
6
+ * roles: {
7
+ * admin: { extends: ["editor"], permissions: ["users:delete"] },
8
+ * editor: ["users:read", "users:write", "posts:*"],
9
+ * viewer: ["*:read"],
10
+ * },
11
+ * audit: b.audit, // optional
12
+ * });
13
+ *
14
+ * router.delete("/users/:id",
15
+ * authMiddleware, // populates req.user / req.apiKey
16
+ * perms.require("users:delete"),
17
+ * deleteUserHandler);
18
+ *
19
+ * The default resolver chain reads the actor from the request:
20
+ *
21
+ * req.apiKey.scopes → { scopes: [...] } (b.apiKey.verify output)
22
+ * req.user.scopes → { scopes: [...] } (operator-set)
23
+ * req.user.roles → { roles: [...] } (operator-set)
24
+ *
25
+ * Operators with non-default request shapes pass `resolver` to create().
26
+ *
27
+ * Wildcard semantics (b.permissions.match):
28
+ * "*" matches any scope (greedy)
29
+ * "users:*" matches "users:read", "users:read:detail", etc. (trailing * is greedy)
30
+ * "*:read" matches "users:read", "posts:read"
31
+ * "users:*:read" matches "users:foo:read" (per-segment *)
32
+ * "users:read" matches "users:read" only — no implicit sub-resource grant
33
+ *
34
+ * Validation policy:
35
+ *
36
+ * - create() role table / scope formats → throw at app init
37
+ * - require(scope) registration arg → throw at route declaration
38
+ * - check(actor, scope) bad actor → return false (tolerant read)
39
+ * - resolver returns null in middleware → 401 (missingActorStatus)
40
+ * - actor lacks scope in middleware → 403 (denyStatus)
41
+ * - audit/observability emit failures → drop silent (hot-path sink)
42
+ *
43
+ * Audit defaults follow the framework's security-defaults stance
44
+ * default: `auditFailures: true`
45
+ * (deny is a security signal), `auditSuccess: false` (per-request noise).
46
+ */
47
+
48
+ var C = require("./constants");
49
+ var lazyRequire = require("./lazy-require");
50
+ var requestHelpers = require("./request-helpers");
51
+ var safeSql = require("./safe-sql");
52
+ var validateOpts = require("./validate-opts");
53
+ var { PermissionsError } = require("./framework-error");
54
+
55
+ var _err = PermissionsError.factory;
56
+
57
+ var observability = lazyRequire(function () { return require("./observability"); });
58
+
59
+ function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
60
+
61
+ // Lowercase tokens, digits, dash, underscore, and `*` allowed per
62
+ // segment. Scope format is segments separated by `:`.
63
+ var SCOPE_RE = /^[a-z0-9_*-]+(:[a-z0-9_*-]+)*$/;
64
+ // Bound the regex engine on operator-supplied scope strings. 256 chars
65
+ // holds any realistic real-world scope (typical scopes run 8-32 chars);
66
+ // rejecting longer keeps the regex linear regardless of input shape.
67
+ var SCOPE_MAX_LENGTH = C.BYTES.bytes(256);
68
+
69
+ // Audit defaults: BOTH success and failure default ON for permissions.
70
+ // Unlike api-key.verify (which is gate-keeping for a downstream action
71
+ // the application separately audits), a permissions.check IS the
72
+ // authorization decision — there's no further-downstream audit event.
73
+ // "user X granted users:delete at time T" is exactly what compliance
74
+ // auditors ask for. Operators with extreme volume opt out via
75
+ // auditSuccess: false; failures remain on regardless.
76
+ var DEFAULTS = Object.freeze({
77
+ auditFailures: true,
78
+ auditSuccess: true,
79
+ denyStatus: 403,
80
+ missingActorStatus: 401,
81
+ });
82
+
83
+ // ---- Wildcard matcher ----
84
+
85
+ function match(granted, required) {
86
+ if (typeof granted !== "string" || typeof required !== "string") return false;
87
+ if (granted.length === 0 || required.length === 0) return false;
88
+ var gParts = granted.split(":");
89
+ var rParts = required.split(":");
90
+ for (var i = 0; i < gParts.length; i++) {
91
+ var g = gParts[i];
92
+ if (g === "*") {
93
+ // Trailing * is greedy — matches the rest of required.
94
+ if (i === gParts.length - 1) return true;
95
+ // Per-segment * — matches THIS segment of required (any value),
96
+ // continue to next segment. Required must have a segment here.
97
+ if (i >= rParts.length) return false;
98
+ continue;
99
+ }
100
+ if (i >= rParts.length) return false; // granted is more specific than required
101
+ if (g !== rParts[i]) return false;
102
+ }
103
+ // Reached end of granted without wildcard. Lengths must match exactly
104
+ // (no implicit sub-resource grant).
105
+ return rParts.length === gParts.length;
106
+ }
107
+
108
+ // ---- Role table validation + expansion ----
109
+
110
+ function _validateScopePattern(scope, ctx) {
111
+ if (typeof scope !== "string" || scope.length === 0) {
112
+ throw _err("BAD_SCOPE", ctx + ": scope must be a non-empty string, got " + typeof scope);
113
+ }
114
+ // Length cap before the regex test — bound the engine on hostile
115
+ // input lengths even though SCOPE_RE is anchored.
116
+ if (scope.length > SCOPE_MAX_LENGTH || !SCOPE_RE.test(scope)) {
117
+ throw _err("BAD_SCOPE", ctx + ": scope '" + scope +
118
+ "' is empty, too long, or doesn't match " + SCOPE_RE +
119
+ " (lowercase tokens with optional `*`)");
120
+ }
121
+ }
122
+
123
+ function _normalizeRoleEntry(name, entry) {
124
+ if (Array.isArray(entry)) {
125
+ return { extends: [], permissions: entry.slice(), dbRole: null,
126
+ requireMfa: false, mfaWindowMs: null };
127
+ }
128
+ if (entry && typeof entry === "object") {
129
+ var ext = entry.extends || [];
130
+ var perms = entry.permissions || [];
131
+ if (!Array.isArray(ext)) {
132
+ throw _err("BAD_ROLE", "role '" + name + "': extends must be an array of role names");
133
+ }
134
+ if (!Array.isArray(perms)) {
135
+ throw _err("BAD_ROLE", "role '" + name + "': permissions must be an array of scope strings");
136
+ }
137
+ var dbRole = null;
138
+ if (entry.dbRole !== undefined && entry.dbRole !== null) {
139
+ if (typeof entry.dbRole !== "string" || entry.dbRole.length === 0) {
140
+ throw _err("BAD_ROLE",
141
+ "role '" + name + "': dbRole must be a non-empty string");
142
+ }
143
+ // dbRole feeds straight into externalDb backend pick + the
144
+ // dbRoleFor middleware's identifier check; validate at create()
145
+ // time so a typo surfaces at boot, not on the first request.
146
+ try {
147
+ safeSql.validateIdentifier(entry.dbRole, { allowReserved: false });
148
+ } catch (e) {
149
+ throw _err("BAD_ROLE",
150
+ "role '" + name + "': dbRole '" + entry.dbRole +
151
+ "' is not a valid SQL identifier: " + ((e && e.message) || String(e)));
152
+ }
153
+ dbRole = entry.dbRole;
154
+ }
155
+ var requireMfa = entry.requireMfa === true;
156
+ var mfaWindowMs = null;
157
+ if (entry.mfaWindowMs !== undefined && entry.mfaWindowMs !== null) {
158
+ if (typeof entry.mfaWindowMs !== "number" || !isFinite(entry.mfaWindowMs) || entry.mfaWindowMs <= 0) {
159
+ throw _err("BAD_ROLE",
160
+ "role '" + name + "': mfaWindowMs must be a positive finite number");
161
+ }
162
+ mfaWindowMs = entry.mfaWindowMs;
163
+ }
164
+ return { extends: ext.slice(), permissions: perms.slice(), dbRole: dbRole,
165
+ requireMfa: requireMfa, mfaWindowMs: mfaWindowMs };
166
+ }
167
+ throw _err("BAD_ROLE", "role '" + name + "' must be an array of scopes or { extends?, permissions, dbRole?, requireMfa?, mfaWindowMs? }");
168
+ }
169
+
170
+ function _validateRoles(roles) {
171
+ if (!roles || typeof roles !== "object" || Array.isArray(roles)) {
172
+ throw _err("BAD_OPT", "permissions.create: roles must be an object map of name → spec");
173
+ }
174
+ var names = Object.keys(roles);
175
+ if (names.length === 0) {
176
+ throw _err("BAD_OPT", "permissions.create: roles map must have at least one role");
177
+ }
178
+ var normalized = {};
179
+ for (var i = 0; i < names.length; i++) {
180
+ var name = names[i];
181
+ if (typeof name !== "string" || name.length === 0) {
182
+ throw _err("BAD_ROLE", "role name must be a non-empty string");
183
+ }
184
+ var spec = _normalizeRoleEntry(name, roles[name]);
185
+ for (var j = 0; j < spec.permissions.length; j++) {
186
+ _validateScopePattern(spec.permissions[j], "role '" + name + "'");
187
+ }
188
+ for (var k = 0; k < spec.extends.length; k++) {
189
+ if (typeof spec.extends[k] !== "string" || spec.extends[k].length === 0) {
190
+ throw _err("BAD_ROLE", "role '" + name + "': extends entry must be a non-empty string");
191
+ }
192
+ }
193
+ normalized[name] = spec;
194
+ }
195
+ // Check extends references resolve to known roles
196
+ for (var n = 0; n < names.length; n++) {
197
+ var spec2 = normalized[names[n]];
198
+ for (var m = 0; m < spec2.extends.length; m++) {
199
+ if (!Object.prototype.hasOwnProperty.call(normalized, spec2.extends[m])) {
200
+ throw _err("UNKNOWN_ROLE", "role '" + names[n] + "': extends references unknown role '" +
201
+ spec2.extends[m] + "'");
202
+ }
203
+ }
204
+ }
205
+ // Cycle detection via DFS
206
+ for (var p = 0; p < names.length; p++) {
207
+ _detectCycle(names[p], normalized, []);
208
+ }
209
+ return normalized;
210
+ }
211
+
212
+ function _detectCycle(roleName, table, stack) {
213
+ if (stack.indexOf(roleName) !== -1) {
214
+ throw _err("CYCLE", "permissions.create: cycle in extends chain: " +
215
+ stack.concat([roleName]).join(" → "));
216
+ }
217
+ var spec = table[roleName];
218
+ for (var i = 0; i < spec.extends.length; i++) {
219
+ _detectCycle(spec.extends[i], table, stack.concat([roleName]));
220
+ }
221
+ }
222
+
223
+ function _expandOne(roleName, table, visited, out) {
224
+ if (visited.has(roleName)) return;
225
+ visited.add(roleName);
226
+ var spec = table[roleName];
227
+ if (!spec) return;
228
+ for (var i = 0; i < spec.extends.length; i++) {
229
+ _expandOne(spec.extends[i], table, visited, out);
230
+ }
231
+ for (var j = 0; j < spec.permissions.length; j++) {
232
+ if (out.indexOf(spec.permissions[j]) === -1) out.push(spec.permissions[j]);
233
+ }
234
+ }
235
+
236
+ // ---- Default resolver ----
237
+
238
+ function _defaultResolver(req) {
239
+ if (!req || typeof req !== "object") return null;
240
+ if (req.apiKey && Array.isArray(req.apiKey.scopes)) return { scopes: req.apiKey.scopes };
241
+ if (req.user && Array.isArray(req.user.scopes)) return { scopes: req.user.scopes };
242
+ if (req.user && Array.isArray(req.user.roles)) return { roles: req.user.roles };
243
+ return null;
244
+ }
245
+
246
+ // ---- Validation: create opts ----
247
+
248
+ function _validateCreateOpts(opts) {
249
+ validateOpts.requireObject(opts, "permissions.create", PermissionsError);
250
+ validateOpts.optionalFunction(opts.resolver, "permissions.create: resolver", PermissionsError);
251
+ validateOpts.auditShape(opts.audit, "permissions.create", PermissionsError);
252
+ validateOpts.optionalBoolean(opts.auditFailures, "permissions.create: auditFailures", PermissionsError);
253
+ validateOpts.optionalBoolean(opts.auditSuccess, "permissions.create: auditSuccess", PermissionsError);
254
+ if (opts.denyStatus !== undefined &&
255
+ (typeof opts.denyStatus !== "number" || !isFinite(opts.denyStatus) || opts.denyStatus < 100 || opts.denyStatus > 599)) {
256
+ throw _err("BAD_OPT", "permissions.create: denyStatus must be an HTTP status code (100-599)");
257
+ }
258
+ if (opts.missingActorStatus !== undefined &&
259
+ (typeof opts.missingActorStatus !== "number" || !isFinite(opts.missingActorStatus) ||
260
+ opts.missingActorStatus < 100 || opts.missingActorStatus > 599)) {
261
+ throw _err("BAD_OPT", "permissions.create: missingActorStatus must be an HTTP status code (100-599)");
262
+ }
263
+ validateOpts.optionalFunction(opts.responder, "permissions.create: responder", PermissionsError);
264
+ }
265
+
266
+ // ---- Registry ----
267
+
268
+ function create(opts) {
269
+ opts = opts || {};
270
+ validateOpts(opts, [
271
+ "roles", "resolver", "audit", "auditFailures", "auditSuccess",
272
+ "denyStatus", "missingActorStatus", "responder",
273
+ ], "permissions");
274
+ _validateCreateOpts(opts);
275
+ var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
276
+ var roleTable = _validateRoles(opts.roles);
277
+ var resolver = opts.resolver || _defaultResolver;
278
+ var audit = opts.audit || null;
279
+ var auditFailures = cfg.auditFailures;
280
+ var auditSuccess = cfg.auditSuccess;
281
+ var denyStatus = cfg.denyStatus;
282
+ var missingActorStatus = cfg.missingActorStatus;
283
+ var responder = opts.responder || _defaultResponder;
284
+
285
+ // ABAC predicate registry. Each entry: scope-string → async predicate
286
+ // function (actor, context) → boolean. The middleware evaluates the
287
+ // predicate AFTER the RBAC scope check passes — so a route protected
288
+ // by `perms.require("orders.read")` first checks the actor has the
289
+ // orders:read scope, then (if the scope has a policy registered)
290
+ // evaluates the predicate with the actor + a per-request context
291
+ // built by the route's `context` middleware opt. ABAC + RBAC stack
292
+ // — a route needs to pass BOTH layers when both are configured.
293
+ var policies = {};
294
+
295
+ function policy(scope, predicate) {
296
+ _validateScopePattern(scope, "permissions.policy");
297
+ if (typeof predicate !== "function") {
298
+ throw _err("BAD_OPT", "permissions.policy: predicate must be a function (actor, context) -> bool");
299
+ }
300
+ if (policies[scope]) {
301
+ throw _err("DUPLICATE_POLICY", "permissions.policy: '" + scope + "' is already registered");
302
+ }
303
+ policies[scope] = predicate;
304
+ }
305
+
306
+ function _findPolicy(requestedScope) {
307
+ // Exact match wins; no wildcard expansion (a wildcard policy
308
+ // gating arbitrary scopes is too easy to misconfigure).
309
+ return policies[requestedScope] || null;
310
+ }
311
+
312
+ var _emitRaw = validateOpts.makeAuditEmitter(audit);
313
+ function _auditEmit(action, info) {
314
+ if (info && info.outcome === "success" && !auditSuccess) return;
315
+ if (info && info.outcome !== "success" && !auditFailures) return;
316
+ _emitRaw(action, info);
317
+ }
318
+
319
+ function expand(roleNames) {
320
+ if (!Array.isArray(roleNames)) return [];
321
+ var visited = new Set();
322
+ var out = [];
323
+ for (var i = 0; i < roleNames.length; i++) {
324
+ if (typeof roleNames[i] === "string" && Object.prototype.hasOwnProperty.call(roleTable, roleNames[i])) {
325
+ _expandOne(roleNames[i], roleTable, visited, out);
326
+ }
327
+ }
328
+ return out;
329
+ }
330
+
331
+ function _actorScopes(actor) {
332
+ if (!actor || typeof actor !== "object") return [];
333
+ if (Array.isArray(actor.scopes)) return actor.scopes;
334
+ if (Array.isArray(actor.roles)) return expand(actor.roles);
335
+ return [];
336
+ }
337
+
338
+ function check(actor, requiredScope) {
339
+ var scopes = _actorScopes(actor);
340
+ for (var i = 0; i < scopes.length; i++) {
341
+ if (typeof scopes[i] === "string" && match(scopes[i], requiredScope)) return true;
342
+ }
343
+ return false;
344
+ }
345
+
346
+ function checkAll(actor, requiredScopes) {
347
+ if (!Array.isArray(requiredScopes)) return false;
348
+ for (var i = 0; i < requiredScopes.length; i++) {
349
+ if (!check(actor, requiredScopes[i])) return false;
350
+ }
351
+ return requiredScopes.length > 0;
352
+ }
353
+
354
+ function checkAny(actor, requiredScopes) {
355
+ if (!Array.isArray(requiredScopes)) return false;
356
+ for (var i = 0; i < requiredScopes.length; i++) {
357
+ if (check(actor, requiredScopes[i])) return true;
358
+ }
359
+ return false;
360
+ }
361
+
362
+ // Middleware factory. `mode` is "single" | "all" | "any"; `requested`
363
+ // is the scope or scope list. Throw at registration time on bad shape.
364
+ function _middleware(mode, requested, mwOpts) {
365
+ if (mode === "single") {
366
+ _validateScopePattern(requested, "permissions.require");
367
+ } else {
368
+ if (!Array.isArray(requested) || requested.length === 0) {
369
+ throw _err("BAD_OPT", "permissions." + (mode === "all" ? "requireAll" : "requireAny") +
370
+ ": scopes must be a non-empty array");
371
+ }
372
+ for (var i = 0; i < requested.length; i++) {
373
+ _validateScopePattern(requested[i], "permissions." + (mode === "all" ? "requireAll" : "requireAny"));
374
+ }
375
+ }
376
+
377
+ // Per-route MFA enforcement opts: { requireMfa, mfaWindowMs }.
378
+ // When set, the middleware blocks unless the actor's mfaAuthenticated
379
+ // flag is truthy AND (when mfaWindowMs is set) actor.mfaAt is fresher
380
+ // than (now - mfaWindowMs). The actor signal is operator-set: after
381
+ // a successful TOTP / passkey step-up, the route handler stamps
382
+ // req.user.mfaAuthenticated = true and req.user.mfaAt = Date.now().
383
+ mwOpts = mwOpts || {};
384
+ var routeRequireMfa = mwOpts.requireMfa === true;
385
+ var routeMfaWindowMs = null;
386
+ if (mwOpts.mfaWindowMs !== undefined && mwOpts.mfaWindowMs !== null) {
387
+ if (typeof mwOpts.mfaWindowMs !== "number" || !isFinite(mwOpts.mfaWindowMs) || mwOpts.mfaWindowMs <= 0) {
388
+ throw _err("BAD_OPT", "permissions middleware: mfaWindowMs must be a positive finite number");
389
+ }
390
+ routeMfaWindowMs = mwOpts.mfaWindowMs;
391
+ }
392
+ // ABAC context provider — operator-supplied function (req)→object.
393
+ // The function runs once per request, AFTER scope/MFA pass, BEFORE
394
+ // the policy predicate. Whatever it returns is passed to the
395
+ // policy as `context`. Async functions are awaited.
396
+ var contextProvider = mwOpts.context;
397
+ if (contextProvider !== undefined && typeof contextProvider !== "function") {
398
+ throw _err("BAD_OPT", "permissions middleware: context must be a function (req) -> object");
399
+ }
400
+
401
+ return async function permissionsMiddleware(req, res, next) {
402
+ var actor = resolver(req);
403
+ if (!actor) {
404
+ // Diagnostic: the most common cause of a null actor is that
405
+ // attachUser/auth wasn't mounted before this middleware, so
406
+ // req.user / req.apiKey are still undefined. Emit a hint —
407
+ // operators tracing a 401 here see exactly what to check first.
408
+ var hint = (req && (req.user || req.apiKey))
409
+ ? "actor present on req but resolver returned null — check resolver implementation"
410
+ : "no req.user or req.apiKey — confirm attachUser / apiKey-verify middleware is mounted before perms.require()";
411
+ _emitEvent("permissions.missing_actor", 1,
412
+ { requested: _labelize(requested) });
413
+ _auditEmit("permissions.missing_actor", {
414
+ actor: _actorAuditShape(null, req),
415
+ resource: { kind: "permission", id: _labelize(requested) },
416
+ outcome: "failure",
417
+ reason: "no-actor",
418
+ metadata: { hint: hint },
419
+ });
420
+ return responder(req, res, missingActorStatus, {
421
+ error: "missing_actor",
422
+ status: missingActorStatus,
423
+ });
424
+ }
425
+
426
+ var ok;
427
+ if (mode === "single") ok = check(actor, requested);
428
+ else if (mode === "all") ok = checkAll(actor, requested);
429
+ else ok = checkAny(actor, requested);
430
+
431
+ if (!ok) {
432
+ _emitEvent("permissions.check", 1,
433
+ { outcome: "deny", requested: _labelize(requested), mode: mode });
434
+ _auditEmit("permissions.check.deny", {
435
+ actor: _actorAuditShape(actor, req),
436
+ resource: { kind: "permission", id: _labelize(requested) },
437
+ outcome: "failure",
438
+ reason: "forbidden",
439
+ metadata: { mode: mode },
440
+ });
441
+ return responder(req, res, denyStatus, {
442
+ error: "forbidden",
443
+ status: denyStatus,
444
+ requested: _labelize(requested),
445
+ });
446
+ }
447
+
448
+ // MFA enforcement gate. Two sources of "this needs MFA":
449
+ // 1. Per-route opt: perms.require("scope", { requireMfa: true })
450
+ // 2. Per-role flag: a role spec with requireMfa:true that
451
+ // contributes to satisfying the requested scope
452
+ // Either source enabling MFA forces the gate. mfaWindowMs (per-route
453
+ // OR per-role, route wins on conflict) bounds freshness — without
454
+ // it, ANY past MFA stamp counts (which is too permissive for high-
455
+ // value routes; operators set a window like C.TIME.minutes(15)).
456
+ var enforceMfa = routeRequireMfa;
457
+ var enforceWindowMs = routeMfaWindowMs;
458
+ if (!enforceMfa) {
459
+ // Walk the actor's roles and check whether any role with
460
+ // requireMfa=true contributes a permission that matches the
461
+ // requested scope. If so, MFA is required regardless of the
462
+ // route-level opt.
463
+ var actorRoles = Array.isArray(actor.roles) ? actor.roles : [];
464
+ for (var ri = 0; ri < actorRoles.length; ri++) {
465
+ var rname = actorRoles[ri];
466
+ if (typeof rname !== "string") continue;
467
+ var rspec = roleTable[rname];
468
+ if (!rspec || !rspec.requireMfa) continue;
469
+ // Cheap match: if the role grants any scope that satisfies the
470
+ // requested scope (single mode) or any of the requested
471
+ // (all/any modes), MFA is required for this route.
472
+ var visited = new Set();
473
+ var roleScopes = [];
474
+ _expandOne(rname, roleTable, visited, roleScopes);
475
+ var roleMatches = false;
476
+ var requestedList = mode === "single" ? [requested] : requested;
477
+ outer: for (var rj = 0; rj < roleScopes.length; rj++) {
478
+ for (var rk = 0; rk < requestedList.length; rk++) {
479
+ if (match(roleScopes[rj], requestedList[rk])) {
480
+ roleMatches = true; break outer;
481
+ }
482
+ }
483
+ }
484
+ if (roleMatches) {
485
+ enforceMfa = true;
486
+ if (enforceWindowMs === null && rspec.mfaWindowMs !== null) {
487
+ enforceWindowMs = rspec.mfaWindowMs;
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ if (enforceMfa) {
494
+ var mfaOk = actor.mfaAuthenticated === true;
495
+ if (mfaOk && enforceWindowMs !== null) {
496
+ var mfaAt = typeof actor.mfaAt === "number" ? actor.mfaAt : 0;
497
+ if (Date.now() - mfaAt > enforceWindowMs) {
498
+ mfaOk = false;
499
+ }
500
+ }
501
+ if (!mfaOk) {
502
+ _emitEvent("permissions.mfa_required", 1,
503
+ { requested: _labelize(requested), mode: mode });
504
+ _auditEmit("permissions.mfa.required", {
505
+ actor: _actorAuditShape(actor, req),
506
+ resource: { kind: "permission", id: _labelize(requested) },
507
+ outcome: "denied",
508
+ reason: "mfa-required",
509
+ metadata: { mode: mode, windowMs: enforceWindowMs },
510
+ });
511
+ return responder(req, res, denyStatus, {
512
+ error: "mfa_required",
513
+ status: denyStatus,
514
+ requested: _labelize(requested),
515
+ });
516
+ }
517
+ }
518
+
519
+ // ABAC layer fires for every requested scope that has a
520
+ // registered policy predicate. Single-mode evaluates the one
521
+ // scope; requireAll evaluates each scope's policy (every must
522
+ // pass); requireAny evaluates only the policies on scopes the
523
+ // actor's RBAC layer satisfied (so a failing policy on a scope
524
+ // the actor doesn't even hold doesn't leak the policy's
525
+ // existence). Each predicate failure short-circuits with a
526
+ // policy.deny audit row naming the failing scope.
527
+ var policyTargets = [];
528
+ if (mode === "single" && _findPolicy(requested)) {
529
+ policyTargets.push(requested);
530
+ } else if (mode === "all" || mode === "any") {
531
+ for (var pi = 0; pi < requested.length; pi++) {
532
+ if (_findPolicy(requested[pi])) {
533
+ if (mode === "any" && !check(actor, requested[pi])) continue;
534
+ policyTargets.push(requested[pi]);
535
+ }
536
+ }
537
+ }
538
+ if (policyTargets.length > 0) {
539
+ var policyContext = null;
540
+ if (contextProvider) {
541
+ try {
542
+ policyContext = await contextProvider(req);
543
+ } catch (e) {
544
+ _emitEvent("permissions.policy_context_error", 1,
545
+ { requested: _labelize(requested) });
546
+ _auditEmit("permissions.policy.error", {
547
+ actor: _actorAuditShape(actor, req),
548
+ resource: { kind: "permission", id: _labelize(requested) },
549
+ outcome: "failure",
550
+ reason: "context-provider-threw",
551
+ metadata: { error: (e && e.message) || String(e), mode: mode },
552
+ });
553
+ return responder(req, res, denyStatus, {
554
+ error: "policy_context_error",
555
+ status: denyStatus,
556
+ requested: _labelize(requested),
557
+ });
558
+ }
559
+ }
560
+ for (var pti = 0; pti < policyTargets.length; pti++) {
561
+ var thisScope = policyTargets[pti];
562
+ var pred = _findPolicy(thisScope);
563
+ var verdict;
564
+ try {
565
+ verdict = await pred(actor, policyContext);
566
+ } catch (e2) {
567
+ _emitEvent("permissions.policy_error", 1, { requested: thisScope });
568
+ _auditEmit("permissions.policy.error", {
569
+ actor: _actorAuditShape(actor, req),
570
+ resource: { kind: "permission", id: thisScope },
571
+ outcome: "failure",
572
+ reason: "predicate-threw",
573
+ metadata: { error: (e2 && e2.message) || String(e2), mode: mode },
574
+ });
575
+ return responder(req, res, denyStatus, {
576
+ error: "policy_error",
577
+ status: denyStatus,
578
+ requested: thisScope,
579
+ });
580
+ }
581
+ if (verdict !== true) {
582
+ _emitEvent("permissions.policy_denied", 1, { requested: thisScope });
583
+ _auditEmit("permissions.policy.deny", {
584
+ actor: _actorAuditShape(actor, req),
585
+ resource: { kind: "permission", id: thisScope },
586
+ outcome: "failure",
587
+ reason: "policy-predicate-returned-falsy",
588
+ metadata: { mode: mode, scopeIndex: pti },
589
+ });
590
+ return responder(req, res, denyStatus, {
591
+ error: "policy_denied",
592
+ status: denyStatus,
593
+ requested: thisScope,
594
+ });
595
+ }
596
+ }
597
+ }
598
+
599
+ _emitEvent("permissions.check", 1,
600
+ { outcome: "success", mode: mode });
601
+ _auditEmit("permissions.check.success", {
602
+ actor: _actorAuditShape(actor, req),
603
+ resource: { kind: "permission", id: _labelize(requested) },
604
+ outcome: "success",
605
+ metadata: { mode: mode, mfaEnforced: enforceMfa },
606
+ });
607
+ next();
608
+ };
609
+ }
610
+
611
+ // dbRoleFor — walk the actor's roles in order and return the first
612
+ // declared dbRole. Composes with b.middleware.dbRoleFor so a single
613
+ // RBAC table drives both authorization scopes and request-time DB
614
+ // role binding.
615
+ //
616
+ // The arg can be the request (default resolver pulls actor from
617
+ // req.user / req.apiKey) OR an actor object directly. Returns null if
618
+ // no actor is found OR the actor's roles don't include any with a
619
+ // declared dbRole.
620
+ //
621
+ // Lookup order: extends are walked depth-first so a child role that
622
+ // overrides dbRole takes precedence over its parent. When multiple
623
+ // top-level roles are listed, the first wins (operators wanting a
624
+ // priority order should list more-specific roles first).
625
+ function dbRoleFor(reqOrActor) {
626
+ var actor = reqOrActor;
627
+ // Heuristic: a request shape carries headers / url; resolve through
628
+ // the configured resolver. An actor shape has roles / scopes
629
+ // directly.
630
+ if (actor && (actor.headers || actor.url || actor.method)) {
631
+ actor = resolver(actor);
632
+ }
633
+ if (!actor || typeof actor !== "object") return null;
634
+ var roleNames = Array.isArray(actor.roles) ? actor.roles : null;
635
+ if (!roleNames || roleNames.length === 0) return null;
636
+ // Walk the same DFS order expand() uses so the first-seen dbRole
637
+ // is consistent with how scopes are inherited.
638
+ var visited = new Set();
639
+ for (var i = 0; i < roleNames.length; i++) {
640
+ var name = roleNames[i];
641
+ if (typeof name !== "string") continue;
642
+ if (!Object.prototype.hasOwnProperty.call(roleTable, name)) continue;
643
+ var found = _findDbRole(name, roleTable, visited);
644
+ if (found) return found;
645
+ }
646
+ return null;
647
+ }
648
+
649
+ return {
650
+ require: function (scope, mwOpts) { return _middleware("single", scope, mwOpts); },
651
+ requireAll: function (scopes, mwOpts) { return _middleware("all", scopes, mwOpts); },
652
+ requireAny: function (scopes, mwOpts) { return _middleware("any", scopes, mwOpts); },
653
+ policy: policy,
654
+ check: check,
655
+ checkAll: checkAll,
656
+ checkAny: checkAny,
657
+ expand: expand,
658
+ dbRoleFor: dbRoleFor,
659
+ has: function (name) { return Object.prototype.hasOwnProperty.call(roleTable, name); },
660
+ roles: Object.freeze(Object.keys(roleTable)),
661
+ };
662
+ }
663
+
664
+ function _findDbRole(roleName, table, visited) {
665
+ if (visited.has(roleName)) return null;
666
+ visited.add(roleName);
667
+ var spec = table[roleName];
668
+ if (!spec) return null;
669
+ // Child overrides parent — check this role's own dbRole first, then
670
+ // recurse into extends.
671
+ if (spec.dbRole) return spec.dbRole;
672
+ for (var i = 0; i < spec.extends.length; i++) {
673
+ var found = _findDbRole(spec.extends[i], table, visited);
674
+ if (found) return found;
675
+ }
676
+ return null;
677
+ }
678
+
679
+ // ---- Helpers ----
680
+
681
+ function _labelize(requested) {
682
+ return Array.isArray(requested) ? requested.join(",") : String(requested);
683
+ }
684
+
685
+ function _actorAuditShape(actor, req) {
686
+ // Pull the 5 W's (WHO/WHERE/HOW) from the request, then layer the
687
+ // resolver-supplied actor identity on top so userId/roles/scopes
688
+ // aren't lost when the request itself doesn't carry them.
689
+ var base = requestHelpers.extractActorContext(req);
690
+ if (actor) {
691
+ if (actor.userId) base.userId = actor.userId;
692
+ if (Array.isArray(actor.roles)) base.roles = actor.roles.slice();
693
+ if (Array.isArray(actor.scopes)) base.scopes = actor.scopes.slice();
694
+ }
695
+ return base;
696
+ }
697
+
698
+ function _defaultResponder(req, res, status, info) {
699
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
700
+ res.end(JSON.stringify(info));
701
+ }
702
+
703
+ module.exports = {
704
+ create: create,
705
+ match: match,
706
+ PermissionsError: PermissionsError,
707
+ DEFAULTS: DEFAULTS,
708
+ };