@blamejs/core 0.4.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/CHANGELOG.md +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- package/package.json +39 -0
|
@@ -0,0 +1,430 @@
|
|
|
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 tiers:
|
|
35
|
+
*
|
|
36
|
+
* - create() role table / scope formats → Tier A (throw at app init)
|
|
37
|
+
* - require(scope) registration arg → Tier A (throw at route declaration)
|
|
38
|
+
* - check(actor, scope) bad actor → Tier C (return false)
|
|
39
|
+
* - resolver returns null in middleware → 401 (missingActorStatus)
|
|
40
|
+
* - actor lacks scope in middleware → 403 (denyStatus)
|
|
41
|
+
* - audit/observability emit failures → Tier B (drop silent)
|
|
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 lazyRequire = require("./lazy-require");
|
|
49
|
+
var requestHelpers = require("./request-helpers");
|
|
50
|
+
var validateOpts = require("./validate-opts");
|
|
51
|
+
var { PermissionsError } = require("./framework-error");
|
|
52
|
+
|
|
53
|
+
var _err = PermissionsError.factory;
|
|
54
|
+
|
|
55
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
56
|
+
|
|
57
|
+
function _emitEvent(name, value, labels) {
|
|
58
|
+
try { observability().event(name, value, labels || {}); }
|
|
59
|
+
catch (_e) { /* Tier B: hot-path observability sink */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Lowercase tokens, digits, dash, underscore, and `*` allowed per
|
|
63
|
+
// segment. Scope format is segments separated by `:`.
|
|
64
|
+
var SCOPE_RE = /^[a-z0-9_*-]+(:[a-z0-9_*-]+)*$/;
|
|
65
|
+
|
|
66
|
+
// Audit defaults: BOTH success and failure default ON for permissions.
|
|
67
|
+
// Unlike api-key.verify (which is gate-keeping for a downstream action
|
|
68
|
+
// the application separately audits), a permissions.check IS the
|
|
69
|
+
// authorization decision — there's no further-downstream audit event.
|
|
70
|
+
// "user X granted users:delete at time T" is exactly what compliance
|
|
71
|
+
// auditors ask for. Operators with extreme volume opt out via
|
|
72
|
+
// auditSuccess: false; failures remain on regardless.
|
|
73
|
+
var DEFAULTS = Object.freeze({
|
|
74
|
+
auditFailures: true,
|
|
75
|
+
auditSuccess: true,
|
|
76
|
+
denyStatus: 403,
|
|
77
|
+
missingActorStatus: 401,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ---- Wildcard matcher ----
|
|
81
|
+
|
|
82
|
+
function match(granted, required) {
|
|
83
|
+
if (typeof granted !== "string" || typeof required !== "string") return false;
|
|
84
|
+
if (granted.length === 0 || required.length === 0) return false;
|
|
85
|
+
var gParts = granted.split(":");
|
|
86
|
+
var rParts = required.split(":");
|
|
87
|
+
for (var i = 0; i < gParts.length; i++) {
|
|
88
|
+
var g = gParts[i];
|
|
89
|
+
if (g === "*") {
|
|
90
|
+
// Trailing * is greedy — matches the rest of required.
|
|
91
|
+
if (i === gParts.length - 1) return true;
|
|
92
|
+
// Per-segment * — matches THIS segment of required (any value),
|
|
93
|
+
// continue to next segment. Required must have a segment here.
|
|
94
|
+
if (i >= rParts.length) return false;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (i >= rParts.length) return false; // granted is more specific than required
|
|
98
|
+
if (g !== rParts[i]) return false;
|
|
99
|
+
}
|
|
100
|
+
// Reached end of granted without wildcard. Lengths must match exactly
|
|
101
|
+
// (no implicit sub-resource grant).
|
|
102
|
+
return rParts.length === gParts.length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- Role table validation + expansion ----
|
|
106
|
+
|
|
107
|
+
function _validateScopePattern(scope, ctx) {
|
|
108
|
+
if (typeof scope !== "string" || scope.length === 0) {
|
|
109
|
+
throw _err("BAD_SCOPE", ctx + ": scope must be a non-empty string, got " + typeof scope);
|
|
110
|
+
}
|
|
111
|
+
if (!SCOPE_RE.test(scope)) {
|
|
112
|
+
throw _err("BAD_SCOPE", ctx + ": scope '" + scope +
|
|
113
|
+
"' must match " + SCOPE_RE + " (lowercase tokens with optional `*`)");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _normalizeRoleEntry(name, entry) {
|
|
118
|
+
if (Array.isArray(entry)) {
|
|
119
|
+
return { extends: [], permissions: entry.slice() };
|
|
120
|
+
}
|
|
121
|
+
if (entry && typeof entry === "object") {
|
|
122
|
+
var ext = entry.extends || [];
|
|
123
|
+
var perms = entry.permissions || [];
|
|
124
|
+
if (!Array.isArray(ext)) {
|
|
125
|
+
throw _err("BAD_ROLE", "role '" + name + "': extends must be an array of role names");
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(perms)) {
|
|
128
|
+
throw _err("BAD_ROLE", "role '" + name + "': permissions must be an array of scope strings");
|
|
129
|
+
}
|
|
130
|
+
return { extends: ext.slice(), permissions: perms.slice() };
|
|
131
|
+
}
|
|
132
|
+
throw _err("BAD_ROLE", "role '" + name + "' must be an array of scopes or { extends?, permissions }");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _validateRoles(roles) {
|
|
136
|
+
if (!roles || typeof roles !== "object" || Array.isArray(roles)) {
|
|
137
|
+
throw _err("BAD_OPT", "permissions.create: roles must be an object map of name → spec");
|
|
138
|
+
}
|
|
139
|
+
var names = Object.keys(roles);
|
|
140
|
+
if (names.length === 0) {
|
|
141
|
+
throw _err("BAD_OPT", "permissions.create: roles map must have at least one role");
|
|
142
|
+
}
|
|
143
|
+
var normalized = {};
|
|
144
|
+
for (var i = 0; i < names.length; i++) {
|
|
145
|
+
var name = names[i];
|
|
146
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
147
|
+
throw _err("BAD_ROLE", "role name must be a non-empty string");
|
|
148
|
+
}
|
|
149
|
+
var spec = _normalizeRoleEntry(name, roles[name]);
|
|
150
|
+
for (var j = 0; j < spec.permissions.length; j++) {
|
|
151
|
+
_validateScopePattern(spec.permissions[j], "role '" + name + "'");
|
|
152
|
+
}
|
|
153
|
+
for (var k = 0; k < spec.extends.length; k++) {
|
|
154
|
+
if (typeof spec.extends[k] !== "string" || spec.extends[k].length === 0) {
|
|
155
|
+
throw _err("BAD_ROLE", "role '" + name + "': extends entry must be a non-empty string");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
normalized[name] = spec;
|
|
159
|
+
}
|
|
160
|
+
// Check extends references resolve to known roles
|
|
161
|
+
for (var n = 0; n < names.length; n++) {
|
|
162
|
+
var spec2 = normalized[names[n]];
|
|
163
|
+
for (var m = 0; m < spec2.extends.length; m++) {
|
|
164
|
+
if (!Object.prototype.hasOwnProperty.call(normalized, spec2.extends[m])) {
|
|
165
|
+
throw _err("UNKNOWN_ROLE", "role '" + names[n] + "': extends references unknown role '" +
|
|
166
|
+
spec2.extends[m] + "'");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Cycle detection via DFS
|
|
171
|
+
for (var p = 0; p < names.length; p++) {
|
|
172
|
+
_detectCycle(names[p], normalized, []);
|
|
173
|
+
}
|
|
174
|
+
return normalized;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _detectCycle(roleName, table, stack) {
|
|
178
|
+
if (stack.indexOf(roleName) !== -1) {
|
|
179
|
+
throw _err("CYCLE", "permissions.create: cycle in extends chain: " +
|
|
180
|
+
stack.concat([roleName]).join(" → "));
|
|
181
|
+
}
|
|
182
|
+
var spec = table[roleName];
|
|
183
|
+
for (var i = 0; i < spec.extends.length; i++) {
|
|
184
|
+
_detectCycle(spec.extends[i], table, stack.concat([roleName]));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function _expandOne(roleName, table, visited, out) {
|
|
189
|
+
if (visited.has(roleName)) return;
|
|
190
|
+
visited.add(roleName);
|
|
191
|
+
var spec = table[roleName];
|
|
192
|
+
if (!spec) return;
|
|
193
|
+
for (var i = 0; i < spec.extends.length; i++) {
|
|
194
|
+
_expandOne(spec.extends[i], table, visited, out);
|
|
195
|
+
}
|
|
196
|
+
for (var j = 0; j < spec.permissions.length; j++) {
|
|
197
|
+
if (out.indexOf(spec.permissions[j]) === -1) out.push(spec.permissions[j]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- Default resolver ----
|
|
202
|
+
|
|
203
|
+
function _defaultResolver(req) {
|
|
204
|
+
if (!req || typeof req !== "object") return null;
|
|
205
|
+
if (req.apiKey && Array.isArray(req.apiKey.scopes)) return { scopes: req.apiKey.scopes };
|
|
206
|
+
if (req.user && Array.isArray(req.user.scopes)) return { scopes: req.user.scopes };
|
|
207
|
+
if (req.user && Array.isArray(req.user.roles)) return { roles: req.user.roles };
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- Validation: create opts ----
|
|
212
|
+
|
|
213
|
+
function _validateCreateOpts(opts) {
|
|
214
|
+
if (!opts || typeof opts !== "object") {
|
|
215
|
+
throw _err("BAD_OPT", "permissions.create: opts must be an object");
|
|
216
|
+
}
|
|
217
|
+
if (opts.resolver !== undefined && typeof opts.resolver !== "function") {
|
|
218
|
+
throw _err("BAD_OPT", "permissions.create: resolver must be a function");
|
|
219
|
+
}
|
|
220
|
+
if (opts.audit !== undefined && opts.audit !== null) {
|
|
221
|
+
if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
|
|
222
|
+
throw _err("BAD_OPT", "permissions.create: audit must be a b.audit-shaped object (safeEmit fn)");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
|
|
226
|
+
throw _err("BAD_OPT", "permissions.create: auditFailures must be a boolean");
|
|
227
|
+
}
|
|
228
|
+
if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
|
|
229
|
+
throw _err("BAD_OPT", "permissions.create: auditSuccess must be a boolean");
|
|
230
|
+
}
|
|
231
|
+
if (opts.denyStatus !== undefined &&
|
|
232
|
+
(typeof opts.denyStatus !== "number" || !isFinite(opts.denyStatus) || opts.denyStatus < 100 || opts.denyStatus > 599)) {
|
|
233
|
+
throw _err("BAD_OPT", "permissions.create: denyStatus must be an HTTP status code (100-599)");
|
|
234
|
+
}
|
|
235
|
+
if (opts.missingActorStatus !== undefined &&
|
|
236
|
+
(typeof opts.missingActorStatus !== "number" || !isFinite(opts.missingActorStatus) ||
|
|
237
|
+
opts.missingActorStatus < 100 || opts.missingActorStatus > 599)) {
|
|
238
|
+
throw _err("BAD_OPT", "permissions.create: missingActorStatus must be an HTTP status code (100-599)");
|
|
239
|
+
}
|
|
240
|
+
if (opts.responder !== undefined && typeof opts.responder !== "function") {
|
|
241
|
+
throw _err("BAD_OPT", "permissions.create: responder must be a function");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---- Registry ----
|
|
246
|
+
|
|
247
|
+
function create(opts) {
|
|
248
|
+
opts = opts || {};
|
|
249
|
+
validateOpts(opts, [
|
|
250
|
+
"roles", "resolver", "audit", "auditFailures", "auditSuccess",
|
|
251
|
+
"denyStatus", "missingActorStatus", "responder",
|
|
252
|
+
], "permissions");
|
|
253
|
+
_validateCreateOpts(opts);
|
|
254
|
+
var roleTable = _validateRoles(opts.roles);
|
|
255
|
+
var resolver = opts.resolver || _defaultResolver;
|
|
256
|
+
var audit = opts.audit || null;
|
|
257
|
+
var auditFailures = (opts.auditFailures === undefined) ? DEFAULTS.auditFailures : opts.auditFailures;
|
|
258
|
+
var auditSuccess = (opts.auditSuccess === undefined) ? DEFAULTS.auditSuccess : opts.auditSuccess;
|
|
259
|
+
var denyStatus = opts.denyStatus || DEFAULTS.denyStatus;
|
|
260
|
+
var missingActorStatus = opts.missingActorStatus || DEFAULTS.missingActorStatus;
|
|
261
|
+
var responder = opts.responder || _defaultResponder;
|
|
262
|
+
|
|
263
|
+
function _auditEmit(action, info) {
|
|
264
|
+
if (!audit) return;
|
|
265
|
+
if (info && info.outcome === "success" && !auditSuccess) return;
|
|
266
|
+
if (info && info.outcome !== "success" && !auditFailures) return;
|
|
267
|
+
try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
|
|
268
|
+
catch (_e) { /* audit best-effort */ }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function expand(roleNames) {
|
|
272
|
+
if (!Array.isArray(roleNames)) return [];
|
|
273
|
+
var visited = new Set();
|
|
274
|
+
var out = [];
|
|
275
|
+
for (var i = 0; i < roleNames.length; i++) {
|
|
276
|
+
if (typeof roleNames[i] === "string" && Object.prototype.hasOwnProperty.call(roleTable, roleNames[i])) {
|
|
277
|
+
_expandOne(roleNames[i], roleTable, visited, out);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function _actorScopes(actor) {
|
|
284
|
+
if (!actor || typeof actor !== "object") return [];
|
|
285
|
+
if (Array.isArray(actor.scopes)) return actor.scopes;
|
|
286
|
+
if (Array.isArray(actor.roles)) return expand(actor.roles);
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function check(actor, requiredScope) {
|
|
291
|
+
var scopes = _actorScopes(actor);
|
|
292
|
+
for (var i = 0; i < scopes.length; i++) {
|
|
293
|
+
if (typeof scopes[i] === "string" && match(scopes[i], requiredScope)) return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function checkAll(actor, requiredScopes) {
|
|
299
|
+
if (!Array.isArray(requiredScopes)) return false;
|
|
300
|
+
for (var i = 0; i < requiredScopes.length; i++) {
|
|
301
|
+
if (!check(actor, requiredScopes[i])) return false;
|
|
302
|
+
}
|
|
303
|
+
return requiredScopes.length > 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function checkAny(actor, requiredScopes) {
|
|
307
|
+
if (!Array.isArray(requiredScopes)) return false;
|
|
308
|
+
for (var i = 0; i < requiredScopes.length; i++) {
|
|
309
|
+
if (check(actor, requiredScopes[i])) return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Middleware factory. `mode` is "single" | "all" | "any"; `requested`
|
|
315
|
+
// is the scope or scope list. Tier A on registration arg.
|
|
316
|
+
function _middleware(mode, requested) {
|
|
317
|
+
if (mode === "single") {
|
|
318
|
+
_validateScopePattern(requested, "permissions.require");
|
|
319
|
+
} else {
|
|
320
|
+
if (!Array.isArray(requested) || requested.length === 0) {
|
|
321
|
+
throw _err("BAD_OPT", "permissions." + (mode === "all" ? "requireAll" : "requireAny") +
|
|
322
|
+
": scopes must be a non-empty array");
|
|
323
|
+
}
|
|
324
|
+
for (var i = 0; i < requested.length; i++) {
|
|
325
|
+
_validateScopePattern(requested[i], "permissions." + (mode === "all" ? "requireAll" : "requireAny"));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return function permissionsMiddleware(req, res, next) {
|
|
330
|
+
var actor = resolver(req);
|
|
331
|
+
if (!actor) {
|
|
332
|
+
// Diagnostic: the most common cause of a null actor is that
|
|
333
|
+
// attachUser/auth wasn't mounted before this middleware, so
|
|
334
|
+
// req.user / req.apiKey are still undefined. Emit a hint —
|
|
335
|
+
// operators tracing a 401 here see exactly what to check first.
|
|
336
|
+
var hint = (req && (req.user || req.apiKey))
|
|
337
|
+
? "actor present on req but resolver returned null — check resolver implementation"
|
|
338
|
+
: "no req.user or req.apiKey — confirm attachUser / apiKey-verify middleware is mounted before perms.require()";
|
|
339
|
+
_emitEvent("permissions.missing_actor", 1,
|
|
340
|
+
{ requested: _labelize(requested) });
|
|
341
|
+
_auditEmit("permissions.missing_actor", {
|
|
342
|
+
actor: _actorAuditShape(null, req),
|
|
343
|
+
resource: { kind: "permission", id: _labelize(requested) },
|
|
344
|
+
outcome: "failure",
|
|
345
|
+
reason: "no-actor",
|
|
346
|
+
metadata: { hint: hint },
|
|
347
|
+
});
|
|
348
|
+
return responder(req, res, missingActorStatus, {
|
|
349
|
+
error: "missing_actor",
|
|
350
|
+
status: missingActorStatus,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
var ok;
|
|
355
|
+
if (mode === "single") ok = check(actor, requested);
|
|
356
|
+
else if (mode === "all") ok = checkAll(actor, requested);
|
|
357
|
+
else ok = checkAny(actor, requested);
|
|
358
|
+
|
|
359
|
+
if (!ok) {
|
|
360
|
+
_emitEvent("permissions.check", 1,
|
|
361
|
+
{ outcome: "deny", requested: _labelize(requested), mode: mode });
|
|
362
|
+
_auditEmit("permissions.check.deny", {
|
|
363
|
+
actor: _actorAuditShape(actor, req),
|
|
364
|
+
resource: { kind: "permission", id: _labelize(requested) },
|
|
365
|
+
outcome: "failure",
|
|
366
|
+
reason: "forbidden",
|
|
367
|
+
metadata: { mode: mode },
|
|
368
|
+
});
|
|
369
|
+
return responder(req, res, denyStatus, {
|
|
370
|
+
error: "forbidden",
|
|
371
|
+
status: denyStatus,
|
|
372
|
+
requested: _labelize(requested),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_emitEvent("permissions.check", 1,
|
|
377
|
+
{ outcome: "success", mode: mode });
|
|
378
|
+
_auditEmit("permissions.check.success", {
|
|
379
|
+
actor: _actorAuditShape(actor, req),
|
|
380
|
+
resource: { kind: "permission", id: _labelize(requested) },
|
|
381
|
+
outcome: "success",
|
|
382
|
+
metadata: { mode: mode },
|
|
383
|
+
});
|
|
384
|
+
next();
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
require: function (scope) { return _middleware("single", scope); },
|
|
390
|
+
requireAll: function (scopes) { return _middleware("all", scopes); },
|
|
391
|
+
requireAny: function (scopes) { return _middleware("any", scopes); },
|
|
392
|
+
check: check,
|
|
393
|
+
checkAll: checkAll,
|
|
394
|
+
checkAny: checkAny,
|
|
395
|
+
expand: expand,
|
|
396
|
+
has: function (name) { return Object.prototype.hasOwnProperty.call(roleTable, name); },
|
|
397
|
+
roles: Object.freeze(Object.keys(roleTable)),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---- Helpers ----
|
|
402
|
+
|
|
403
|
+
function _labelize(requested) {
|
|
404
|
+
return Array.isArray(requested) ? requested.join(",") : String(requested);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function _actorAuditShape(actor, req) {
|
|
408
|
+
// Pull the 5 W's (WHO/WHERE/HOW) from the request, then layer the
|
|
409
|
+
// resolver-supplied actor identity on top so userId/roles/scopes
|
|
410
|
+
// aren't lost when the request itself doesn't carry them.
|
|
411
|
+
var base = requestHelpers.extractActorContext(req);
|
|
412
|
+
if (actor) {
|
|
413
|
+
if (actor.userId) base.userId = actor.userId;
|
|
414
|
+
if (Array.isArray(actor.roles)) base.roles = actor.roles.slice();
|
|
415
|
+
if (Array.isArray(actor.scopes)) base.scopes = actor.scopes.slice();
|
|
416
|
+
}
|
|
417
|
+
return base;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function _defaultResponder(req, res, status, info) {
|
|
421
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
422
|
+
res.end(JSON.stringify(info));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = {
|
|
426
|
+
create: create,
|
|
427
|
+
match: match,
|
|
428
|
+
PermissionsError: PermissionsError,
|
|
429
|
+
DEFAULTS: DEFAULTS,
|
|
430
|
+
};
|
package/lib/pqc-agent.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* pqc-agent — outbound HTTPS agent locked to PQC group preference.
|
|
4
|
+
*
|
|
5
|
+
* The framework's posture is "all outbound TLS is PQC-only". This is
|
|
6
|
+
* the single primitive that defines what that means at the agent
|
|
7
|
+
* level: TLSv1.3 minimum, ecdhCurve set to the framework's PQC hybrid
|
|
8
|
+
* preference (constants.TLS_GROUP_CURVE_STR), keep-alive on.
|
|
9
|
+
*
|
|
10
|
+
* Two surfaces:
|
|
11
|
+
*
|
|
12
|
+
* 1. b.pqcAgent.agent — a process-wide default agent, lazy-built on
|
|
13
|
+
* first access. Use this for one-off outbound calls that go
|
|
14
|
+
* through node:https directly:
|
|
15
|
+
*
|
|
16
|
+
* https.request(url, { agent: b.pqcAgent.agent }, ...);
|
|
17
|
+
*
|
|
18
|
+
* 2. b.pqcAgent.create(opts) — build a fresh agent with custom
|
|
19
|
+
* pool / timeout opts. ecdhCurve and minVersion CANNOT be
|
|
20
|
+
* weakened via opts; operator-supplied values for those are
|
|
21
|
+
* ignored and the framework's defaults win. Operators who need
|
|
22
|
+
* a non-PQC agent for a deliberate one-off integration with a
|
|
23
|
+
* non-PQC server construct their own new https.Agent() directly,
|
|
24
|
+
* outside this primitive.
|
|
25
|
+
*
|
|
26
|
+
* lib/http-client.js's transport cache uses pqcAgent.create() under
|
|
27
|
+
* the hood, so the framework's bundled HTTP client and any operator-
|
|
28
|
+
* direct https.request calls converge on the same agent posture.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
var https = require("node:https");
|
|
32
|
+
var http = require("node:http");
|
|
33
|
+
var C = require("./constants");
|
|
34
|
+
|
|
35
|
+
// Defaults for connection pooling. These ARE overridable via opts —
|
|
36
|
+
// only the cryptographic posture (ecdhCurve / minVersion) is locked.
|
|
37
|
+
var DEFAULT_OPTS = {
|
|
38
|
+
keepAlive: true,
|
|
39
|
+
keepAliveMsecs: 30000,
|
|
40
|
+
maxSockets: 50,
|
|
41
|
+
maxFreeSockets: 16,
|
|
42
|
+
scheduling: "lifo",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function _buildAgentOpts(opts) {
|
|
46
|
+
opts = opts || {};
|
|
47
|
+
var merged = Object.assign({}, DEFAULT_OPTS, opts);
|
|
48
|
+
// Cryptographic posture cannot be relaxed via opts. Even if the
|
|
49
|
+
// operator passes ecdhCurve: 'P-256' or minVersion: 'TLSv1.2', the
|
|
50
|
+
// framework defaults win. This is deliberate: the primitive's whole
|
|
51
|
+
// value is that you can't accidentally ship a downgraded agent.
|
|
52
|
+
merged.ecdhCurve = C.TLS_GROUP_CURVE_STR;
|
|
53
|
+
merged.minVersion = "TLSv1.3";
|
|
54
|
+
return merged;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function create(opts) {
|
|
58
|
+
return new https.Agent(_buildAgentOpts(opts));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// http (cleartext) variant — same pool defaults but obviously no TLS
|
|
62
|
+
// posture to enforce. Operator-side, almost no caller wants this; it
|
|
63
|
+
// exists so http-client's h1 transport for cleartext origins (h2c
|
|
64
|
+
// fixtures, internal services) shares the pool tuning.
|
|
65
|
+
function createHttp(opts) {
|
|
66
|
+
return new http.Agent(Object.assign({}, DEFAULT_OPTS, opts || {}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Lazy-built default agent. Most callers use this; the lazy build
|
|
70
|
+
// avoids creating an https.Agent at require time for processes that
|
|
71
|
+
// never make an outbound HTTPS call.
|
|
72
|
+
var _defaultAgent = null;
|
|
73
|
+
function _getDefaultAgent() {
|
|
74
|
+
if (!_defaultAgent) _defaultAgent = create();
|
|
75
|
+
return _defaultAgent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
// Read property — getter so the agent is built on first access.
|
|
80
|
+
get agent() { return _getDefaultAgent(); },
|
|
81
|
+
create: create,
|
|
82
|
+
createHttp: createHttp,
|
|
83
|
+
DEFAULT_OPTS: DEFAULT_OPTS,
|
|
84
|
+
enforced: true,
|
|
85
|
+
};
|