@blamejs/core 0.7.107 → 0.8.4

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 (100) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/NOTICE +17 -1
  3. package/README.md +4 -3
  4. package/index.js +15 -0
  5. package/lib/asyncapi-bindings.js +160 -0
  6. package/lib/asyncapi-traits.js +143 -0
  7. package/lib/asyncapi.js +531 -0
  8. package/lib/audit-sign.js +1 -1
  9. package/lib/audit.js +68 -2
  10. package/lib/auth/acr-vocabulary.js +265 -0
  11. package/lib/auth/auth-time-tracker.js +111 -0
  12. package/lib/auth/elevation-grant.js +306 -0
  13. package/lib/auth/jwt.js +13 -0
  14. package/lib/auth/lockout.js +16 -3
  15. package/lib/auth/oauth.js +15 -1
  16. package/lib/auth/password.js +22 -2
  17. package/lib/auth/sd-jwt-vc-issuer.js +2 -2
  18. package/lib/auth/sd-jwt-vc.js +7 -2
  19. package/lib/auth/step-up-policy.js +335 -0
  20. package/lib/auth/step-up.js +445 -0
  21. package/lib/break-glass.js +53 -14
  22. package/lib/cache-redis.js +1 -1
  23. package/lib/cache.js +6 -1
  24. package/lib/cli.js +3 -3
  25. package/lib/cluster.js +24 -1
  26. package/lib/compliance-ai-act-logging.js +190 -0
  27. package/lib/compliance-ai-act-prohibited.js +205 -0
  28. package/lib/compliance-ai-act-risk.js +189 -0
  29. package/lib/compliance-ai-act-transparency.js +200 -0
  30. package/lib/compliance-ai-act.js +558 -0
  31. package/lib/compliance.js +12 -2
  32. package/lib/config-drift.js +2 -2
  33. package/lib/crypto-field.js +21 -1
  34. package/lib/crypto.js +114 -1
  35. package/lib/db.js +35 -4
  36. package/lib/dev.js +30 -3
  37. package/lib/dual-control.js +19 -1
  38. package/lib/external-db.js +10 -0
  39. package/lib/file-upload.js +30 -3
  40. package/lib/flag-cache.js +136 -0
  41. package/lib/flag-evaluation-context.js +135 -0
  42. package/lib/flag-providers.js +279 -0
  43. package/lib/flag-targeting.js +210 -0
  44. package/lib/flag.js +284 -0
  45. package/lib/guard-all.js +33 -16
  46. package/lib/guard-csv.js +16 -2
  47. package/lib/guard-html.js +35 -0
  48. package/lib/guard-svg.js +20 -0
  49. package/lib/http-client.js +57 -11
  50. package/lib/inbox.js +391 -0
  51. package/lib/log-stream-syslog.js +8 -0
  52. package/lib/log-stream.js +1 -1
  53. package/lib/mail-arc-sign.js +372 -0
  54. package/lib/mail-auth.js +2 -0
  55. package/lib/mail.js +40 -0
  56. package/lib/middleware/ai-act-disclosure.js +166 -0
  57. package/lib/middleware/asyncapi-serve.js +136 -0
  58. package/lib/middleware/attach-user.js +25 -2
  59. package/lib/middleware/bearer-auth.js +71 -6
  60. package/lib/middleware/body-parser.js +13 -0
  61. package/lib/middleware/cors.js +10 -0
  62. package/lib/middleware/csrf-protect.js +34 -3
  63. package/lib/middleware/dpop.js +3 -3
  64. package/lib/middleware/flag-context.js +76 -0
  65. package/lib/middleware/host-allowlist.js +1 -1
  66. package/lib/middleware/index.js +15 -0
  67. package/lib/middleware/openapi-serve.js +143 -0
  68. package/lib/middleware/require-aal.js +2 -2
  69. package/lib/middleware/require-step-up.js +186 -0
  70. package/lib/middleware/trace-propagate.js +1 -1
  71. package/lib/mtls-ca.js +23 -29
  72. package/lib/mtls-engine-default.js +21 -1
  73. package/lib/network-tls.js +21 -6
  74. package/lib/object-store/sigv4-bucket-ops.js +41 -0
  75. package/lib/observability-otlp-exporter.js +35 -2
  76. package/lib/openapi-paths-builder.js +248 -0
  77. package/lib/openapi-schema-walk.js +192 -0
  78. package/lib/openapi-security.js +169 -0
  79. package/lib/openapi-yaml.js +154 -0
  80. package/lib/openapi.js +443 -0
  81. package/lib/outbox.js +3 -3
  82. package/lib/permissions.js +10 -1
  83. package/lib/pqc-agent.js +22 -1
  84. package/lib/pqc-software.js +195 -0
  85. package/lib/pubsub.js +8 -4
  86. package/lib/redact.js +26 -1
  87. package/lib/retention.js +26 -0
  88. package/lib/router.js +1 -0
  89. package/lib/scheduler.js +57 -1
  90. package/lib/session.js +3 -3
  91. package/lib/ssrf-guard.js +19 -4
  92. package/lib/static.js +12 -0
  93. package/lib/totp.js +16 -0
  94. package/lib/vault/index.js +3 -0
  95. package/lib/vault-aad.js +259 -0
  96. package/lib/vendor/MANIFEST.json +29 -0
  97. package/lib/vendor/noble-post-quantum.cjs +18 -0
  98. package/lib/ws-client.js +978 -0
  99. package/package.json +1 -1
  100. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ /**
3
+ * OpenAPI 3.1 — minimal YAML 1.2 emitter for the document JSON.
4
+ *
5
+ * Why a hand-rolled emitter: the framework is zero-runtime-dep and the
6
+ * vendored yaml-min already handles most cases. This module bridges
7
+ * the OpenAPI document into a YAML rendering operators can paste into
8
+ * Swagger-UI / docs-as-code pipelines that want yaml.
9
+ *
10
+ * Output is pure ASCII, uses 2-space indentation, quotes strings that
11
+ * could be parsed as numbers / booleans / null / yaml-tags / contain
12
+ * special characters. Operators with strict yaml-emitter requirements
13
+ * pipe the document JSON through their own yaml stack instead.
14
+ */
15
+
16
+ var { defineClass } = require("./framework-error");
17
+ var OpenApiError = defineClass("OpenApiError", { alwaysPermanent: true });
18
+
19
+ var SPECIAL_KEYS = ["true", "false", "null", "True", "False", "Null",
20
+ "TRUE", "FALSE", "NULL", "yes", "no", "on", "off",
21
+ "Yes", "No", "On", "Off", "YES", "NO", "ON", "OFF",
22
+ "~"];
23
+
24
+ var QUOTE_PATTERN = /[:#&*!|>'"%@`{}[\],?]|^\s|\s$|^-\s|\t/;
25
+ var NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
26
+ var INT_PATTERN = /^-?\d+$/;
27
+ var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}/;
28
+
29
+ function _needsQuoting(str) {
30
+ if (str.length === 0) return true;
31
+ if (SPECIAL_KEYS.indexOf(str) !== -1) return true;
32
+ if (NUMBER_PATTERN.test(str)) return true;
33
+ if (INT_PATTERN.test(str)) return true;
34
+ if (DATE_PATTERN.test(str)) return true;
35
+ if (QUOTE_PATTERN.test(str)) return true;
36
+ return false;
37
+ }
38
+
39
+ function _quoteString(str) {
40
+ // Use double quotes; escape backslashes + quotes + control chars.
41
+ var out = '"';
42
+ for (var i = 0; i < str.length; i += 1) {
43
+ var ch = str.charAt(i);
44
+ var code = str.charCodeAt(i);
45
+ if (ch === "\\") out += "\\\\";
46
+ else if (ch === '"') out += '\\"';
47
+ else if (code === 0x0a) out += "\\n";
48
+ else if (code === 0x0d) out += "\\r";
49
+ else if (code === 0x09) out += "\\t";
50
+ else if (code < 0x20) out += "\\u" + code.toString(16).padStart(4, "0"); // allow:raw-byte-literal — codepoint hex padding
51
+ else out += ch;
52
+ }
53
+ out += '"';
54
+ return out;
55
+ }
56
+
57
+ function _encodeScalar(value) {
58
+ if (value === null) return "null";
59
+ if (value === undefined) return "null";
60
+ if (typeof value === "boolean") return value ? "true" : "false";
61
+ if (typeof value === "number") {
62
+ if (!isFinite(value)) return "null";
63
+ return String(value);
64
+ }
65
+ if (typeof value === "string") {
66
+ if (_needsQuoting(value)) return _quoteString(value);
67
+ return value;
68
+ }
69
+ if (typeof value === "bigint") return String(value);
70
+ return _quoteString(String(value));
71
+ }
72
+
73
+ function _encodeKey(key) {
74
+ if (typeof key !== "string") return _quoteString(String(key));
75
+ if (_needsQuoting(key)) return _quoteString(key);
76
+ return key;
77
+ }
78
+
79
+ function _isScalar(v) {
80
+ return v === null || v === undefined ||
81
+ typeof v === "boolean" || typeof v === "number" ||
82
+ typeof v === "string" || typeof v === "bigint";
83
+ }
84
+
85
+ function _isPlainObject(v) {
86
+ return !!v && typeof v === "object" && !Array.isArray(v);
87
+ }
88
+
89
+ function emit(value, indent) {
90
+ indent = indent || 0;
91
+ var pad = _pad(indent);
92
+
93
+ if (_isScalar(value)) {
94
+ return _encodeScalar(value);
95
+ }
96
+ if (Array.isArray(value)) {
97
+ if (value.length === 0) return "[]";
98
+ var arrLines = [];
99
+ for (var i = 0; i < value.length; i += 1) {
100
+ var entry = value[i];
101
+ if (_isScalar(entry)) {
102
+ arrLines.push(pad + "- " + _encodeScalar(entry));
103
+ } else if (Array.isArray(entry)) {
104
+ arrLines.push(pad + "- " + emit(entry, indent + 1).replace(/^\s+/, ""));
105
+ } else {
106
+ arrLines.push(pad + "-");
107
+ arrLines.push(emit(entry, indent + 1));
108
+ }
109
+ }
110
+ return arrLines.join("\n");
111
+ }
112
+ if (_isPlainObject(value)) {
113
+ var keys = Object.keys(value);
114
+ if (keys.length === 0) return "{}";
115
+ var objLines = [];
116
+ for (var k = 0; k < keys.length; k += 1) {
117
+ var keyStr = keys[k];
118
+ var v = value[keyStr];
119
+ var encodedKey = _encodeKey(keyStr);
120
+ if (_isScalar(v)) {
121
+ objLines.push(pad + encodedKey + ": " + _encodeScalar(v));
122
+ } else if (Array.isArray(v) && v.length === 0) {
123
+ objLines.push(pad + encodedKey + ": []");
124
+ } else if (_isPlainObject(v) && Object.keys(v).length === 0) {
125
+ objLines.push(pad + encodedKey + ": {}");
126
+ } else {
127
+ objLines.push(pad + encodedKey + ":");
128
+ objLines.push(emit(v, indent + 1));
129
+ }
130
+ }
131
+ return objLines.join("\n");
132
+ }
133
+ return _encodeScalar(value);
134
+ }
135
+
136
+ function _pad(indent) {
137
+ var s = "";
138
+ for (var i = 0; i < indent; i += 1) s += " ";
139
+ return s;
140
+ }
141
+
142
+ function toYaml(doc) {
143
+ if (doc == null || typeof doc !== "object") {
144
+ throw new OpenApiError("openapi/bad-yaml-input",
145
+ "openapi.toYaml: input must be a non-null object");
146
+ }
147
+ return emit(doc, 0) + "\n";
148
+ }
149
+
150
+ module.exports = {
151
+ toYaml: toYaml,
152
+ emit: emit,
153
+ OpenApiError: OpenApiError,
154
+ };
package/lib/openapi.js ADDED
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ /**
3
+ * b.openapi — OpenAPI 3.1 schema-document builder.
4
+ *
5
+ * Operators describe their public HTTP surface as an OpenAPI 3.1
6
+ * document the framework can serve at /openapi.json (or any path of
7
+ * their choice) for downstream tooling: API consumers, Postman, code-
8
+ * generators, contract-testing rigs.
9
+ *
10
+ * The builder is FRAMEWORK-FACING: it produces a valid OpenAPI 3.1
11
+ * document, but the operator's hand-written contract is the source of
12
+ * truth — it does NOT auto-walk b.router routes (operators frequently
13
+ * want a smaller / different surface published than what the router
14
+ * exposes internally). Future patch may add `fromRouter()` once the
15
+ * route-shape is stable.
16
+ *
17
+ * Public surface:
18
+ *
19
+ * b.openapi.create({ info, servers, externalDocs, tags })
20
+ * -> builder
21
+ *
22
+ * builder.path(method, urlPattern, opts) // add operation
23
+ * builder.schema(name, schema) // reusable component schema
24
+ * builder.response(name, response) // reusable response
25
+ * builder.parameter(name, parameter) // reusable parameter
26
+ * builder.security.add(name, scheme) // add security scheme
27
+ * builder.security.require(requirement) // doc-level security
28
+ * builder.tag({ name, description }) // tag group
29
+ * builder.server({ url, description, variables }) // server URL
30
+ *
31
+ * builder.toJson() -> OpenAPI 3.1 JSON document
32
+ * builder.toYaml() -> YAML serialisation (if vendored YAML present)
33
+ * builder.middleware(opts) -> request-time middleware that serves the doc
34
+ *
35
+ * b.openapi.security.{bearer,basic,apiKey,oauth2,openIdConnect,mtls,dpop}
36
+ * -> security-scheme builders (delegated from openapi-security.js)
37
+ *
38
+ * b.openapi.schemaWalk(input)
39
+ * -> safeSchema -> JSON Schema utility (delegated)
40
+ */
41
+
42
+ var validateOpts = require("./validate-opts");
43
+ var lazyRequire = require("./lazy-require");
44
+ var schemaWalk = require("./openapi-schema-walk");
45
+ var pathsBuilderMod = require("./openapi-paths-builder");
46
+ var openapiSecurity = require("./openapi-security");
47
+ var openapiYaml = require("./openapi-yaml");
48
+ var { defineClass } = require("./framework-error");
49
+ var audit = lazyRequire(function () { return require("./audit"); });
50
+
51
+ var OpenApiError = defineClass("OpenApiError", { alwaysPermanent: true });
52
+
53
+ var OPENAPI_VERSION = "3.1.0";
54
+
55
+ function create(opts) {
56
+ opts = opts || {};
57
+ validateOpts(opts, [
58
+ "info", "servers", "externalDocs", "tags", "security",
59
+ ], "openapi.create");
60
+ if (!opts.info || typeof opts.info !== "object") {
61
+ throw new OpenApiError("openapi/bad-info",
62
+ "openapi.create: info object is required (title + version)");
63
+ }
64
+ validateOpts.requireNonEmptyString(opts.info.title,
65
+ "openapi.create: info.title", OpenApiError, "openapi/bad-info");
66
+ validateOpts.requireNonEmptyString(opts.info.version,
67
+ "openapi.create: info.version", OpenApiError, "openapi/bad-info");
68
+
69
+ var paths = new pathsBuilderMod.PathsBuilder();
70
+ var components = {
71
+ schemas: {},
72
+ responses: {},
73
+ parameters: {},
74
+ securitySchemes: {},
75
+ requestBodies: {},
76
+ headers: {},
77
+ examples: {},
78
+ };
79
+ var docTags = Array.isArray(opts.tags) ? opts.tags.slice() : [];
80
+ var docServers = Array.isArray(opts.servers) ? opts.servers.slice() : [];
81
+ var docSecurity = Array.isArray(opts.security) ? opts.security.slice() : [];
82
+ var externalDocs = opts.externalDocs || null;
83
+
84
+ function _validateServerEntry(entry, label) {
85
+ if (!entry || typeof entry !== "object") {
86
+ throw new OpenApiError("openapi/bad-server",
87
+ label + ": server must be an object");
88
+ }
89
+ validateOpts.requireNonEmptyString(entry.url,
90
+ label + ": url", OpenApiError, "openapi/bad-server");
91
+ }
92
+ for (var s = 0; s < docServers.length; s += 1) {
93
+ _validateServerEntry(docServers[s], "openapi.create: servers[" + s + "]");
94
+ }
95
+
96
+ var builder = {
97
+ info: Object.assign({}, opts.info),
98
+
99
+ path: function (method, urlPattern, pathOpts) {
100
+ paths.add(method, urlPattern, pathOpts || {});
101
+ return builder;
102
+ },
103
+
104
+ schema: function (name, schemaSpec) {
105
+ validateOpts.requireNonEmptyString(name, "schema: name",
106
+ OpenApiError, "openapi/bad-component");
107
+ if (Object.prototype.hasOwnProperty.call(components.schemas, name)) {
108
+ throw new OpenApiError("openapi/duplicate-component",
109
+ "schema: component schemas." + name + " already registered");
110
+ }
111
+ components.schemas[name] = schemaWalk.walk(schemaSpec);
112
+ return builder;
113
+ },
114
+
115
+ response: function (name, responseSpec) {
116
+ validateOpts.requireNonEmptyString(name, "response: name",
117
+ OpenApiError, "openapi/bad-component");
118
+ if (!responseSpec || typeof responseSpec !== "object") {
119
+ throw new OpenApiError("openapi/bad-response",
120
+ "response: responseSpec must be an object");
121
+ }
122
+ if (typeof responseSpec.description !== "string") {
123
+ throw new OpenApiError("openapi/missing-response-description",
124
+ "response: description is required");
125
+ }
126
+ components.responses[name] = responseSpec;
127
+ return builder;
128
+ },
129
+
130
+ parameter: function (name, paramSpec) {
131
+ validateOpts.requireNonEmptyString(name, "parameter: name",
132
+ OpenApiError, "openapi/bad-component");
133
+ if (!paramSpec || typeof paramSpec !== "object") {
134
+ throw new OpenApiError("openapi/bad-parameter",
135
+ "parameter: paramSpec must be an object");
136
+ }
137
+ var p = Object.assign({}, paramSpec);
138
+ if (p.schema != null) p.schema = schemaWalk.walk(p.schema);
139
+ components.parameters[name] = p;
140
+ return builder;
141
+ },
142
+
143
+ requestBody: function (name, bodySpec) {
144
+ validateOpts.requireNonEmptyString(name, "requestBody: name",
145
+ OpenApiError, "openapi/bad-component");
146
+ if (!bodySpec || typeof bodySpec !== "object") {
147
+ throw new OpenApiError("openapi/bad-request-body",
148
+ "requestBody: bodySpec must be an object");
149
+ }
150
+ components.requestBodies[name] = bodySpec;
151
+ return builder;
152
+ },
153
+
154
+ header: function (name, headerSpec) {
155
+ validateOpts.requireNonEmptyString(name, "header: name",
156
+ OpenApiError, "openapi/bad-component");
157
+ components.headers[name] = headerSpec;
158
+ return builder;
159
+ },
160
+
161
+ example: function (name, exampleSpec) {
162
+ validateOpts.requireNonEmptyString(name, "example: name",
163
+ OpenApiError, "openapi/bad-component");
164
+ components.examples[name] = exampleSpec;
165
+ return builder;
166
+ },
167
+
168
+ security: {
169
+ add: function (name, scheme) {
170
+ validateOpts.requireNonEmptyString(name, "security.add: name",
171
+ OpenApiError, "openapi/bad-security");
172
+ if (!scheme || typeof scheme !== "object" || typeof scheme.type !== "string") {
173
+ throw new OpenApiError("openapi/bad-security",
174
+ "security.add: scheme must be a securityScheme object with a type");
175
+ }
176
+ components.securitySchemes[name] = scheme;
177
+ return builder;
178
+ },
179
+ require: function (requirement) {
180
+ if (!requirement || typeof requirement !== "object") {
181
+ throw new OpenApiError("openapi/bad-security",
182
+ "security.require: requirement must be an object like { schemeName: ['scope'] }");
183
+ }
184
+ docSecurity.push(requirement);
185
+ return builder;
186
+ },
187
+ },
188
+
189
+ tag: function (tagSpec) {
190
+ if (!tagSpec || typeof tagSpec !== "object" ||
191
+ typeof tagSpec.name !== "string" || tagSpec.name.length === 0) {
192
+ throw new OpenApiError("openapi/bad-tag",
193
+ "tag: tagSpec.name is required");
194
+ }
195
+ docTags.push(tagSpec);
196
+ return builder;
197
+ },
198
+
199
+ server: function (serverSpec) {
200
+ _validateServerEntry(serverSpec, "server");
201
+ docServers.push(serverSpec);
202
+ return builder;
203
+ },
204
+
205
+ toJson: function () {
206
+ var doc = {
207
+ openapi: OPENAPI_VERSION,
208
+ info: builder.info,
209
+ };
210
+ if (docServers.length > 0) doc.servers = docServers.slice();
211
+ doc.paths = paths.toMap();
212
+ var anyComponent = false;
213
+ var componentsOut = {};
214
+ var keys = ["schemas", "responses", "parameters", "requestBodies",
215
+ "headers", "examples", "securitySchemes"];
216
+ for (var k = 0; k < keys.length; k += 1) {
217
+ var key = keys[k];
218
+ if (Object.keys(components[key]).length > 0) {
219
+ componentsOut[key] = components[key];
220
+ anyComponent = true;
221
+ }
222
+ }
223
+ if (anyComponent) doc.components = componentsOut;
224
+ if (docSecurity.length > 0) doc.security = docSecurity.slice();
225
+ if (docTags.length > 0) doc.tags = docTags.slice();
226
+ if (externalDocs) doc.externalDocs = externalDocs;
227
+ // Validate security references — every requirement key must be a
228
+ // registered security scheme.
229
+ for (var r = 0; r < docSecurity.length; r += 1) {
230
+ for (var schemeName in docSecurity[r]) {
231
+ if (!Object.prototype.hasOwnProperty.call(docSecurity[r], schemeName)) continue;
232
+ if (!components.securitySchemes[schemeName]) {
233
+ throw new OpenApiError("openapi/dangling-security",
234
+ "toJson: doc-level security references undefined scheme " +
235
+ JSON.stringify(schemeName));
236
+ }
237
+ }
238
+ }
239
+ // Same check on per-operation security.
240
+ for (var pathKey in doc.paths) {
241
+ if (!Object.prototype.hasOwnProperty.call(doc.paths, pathKey)) continue;
242
+ var pathItem = doc.paths[pathKey];
243
+ for (var methodKey in pathItem) {
244
+ if (!Object.prototype.hasOwnProperty.call(pathItem, methodKey)) continue;
245
+ var op = pathItem[methodKey];
246
+ if (Array.isArray(op.security)) {
247
+ for (var os = 0; os < op.security.length; os += 1) {
248
+ for (var sn in op.security[os]) {
249
+ if (!Object.prototype.hasOwnProperty.call(op.security[os], sn)) continue;
250
+ if (!components.securitySchemes[sn]) {
251
+ throw new OpenApiError("openapi/dangling-security",
252
+ "toJson: " + methodKey.toUpperCase() + " " + pathKey +
253
+ " references undefined security scheme " + JSON.stringify(sn));
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ try {
261
+ audit().safeEmit({
262
+ action: "openapi.document.built",
263
+ outcome: "success",
264
+ actor: null,
265
+ metadata: {
266
+ title: builder.info.title,
267
+ version: builder.info.version,
268
+ pathCount: Object.keys(doc.paths).length,
269
+ schemaCount: Object.keys(components.schemas).length,
270
+ securityCount: Object.keys(components.securitySchemes).length,
271
+ },
272
+ });
273
+ } catch (_e) { /* drop-silent */ }
274
+ return doc;
275
+ },
276
+
277
+ toJsonString: function (indent) {
278
+ return JSON.stringify(builder.toJson(), null, indent || 2);
279
+ },
280
+
281
+ toYaml: function () {
282
+ return openapiYaml.toYaml(builder.toJson());
283
+ },
284
+
285
+ middleware: function (mwOpts) {
286
+ mwOpts = mwOpts || {};
287
+ validateOpts(mwOpts, ["pretty", "cacheControl"], "openapi.builder.middleware");
288
+ var pretty = mwOpts.pretty === true ? 2 : 0;
289
+ var cacheControl = (typeof mwOpts.cacheControl === "string" && mwOpts.cacheControl.length > 0)
290
+ ? mwOpts.cacheControl
291
+ : "public, max-age=300";
292
+ // Memoize the JSON between calls; re-build if the operator
293
+ // calls forceRebuild().
294
+ var cached = null;
295
+ var cachedString = null;
296
+ function _rebuild() {
297
+ cached = builder.toJson();
298
+ cachedString = JSON.stringify(cached, null, pretty);
299
+ }
300
+ _rebuild();
301
+ var mw = function (req, res, next) {
302
+ if (typeof res.writeHead !== "function") return next();
303
+ var body = cachedString;
304
+ res.writeHead(200, { // allow:raw-byte-literal — HTTP 200 status
305
+ "Content-Type": "application/json; charset=utf-8",
306
+ "Content-Length": Buffer.byteLength(body),
307
+ "Cache-Control": cacheControl,
308
+ });
309
+ res.end(body);
310
+ };
311
+ mw.forceRebuild = _rebuild;
312
+ return mw;
313
+ },
314
+ };
315
+
316
+ return builder;
317
+ }
318
+
319
+ // Parse + validate an external OpenAPI 3.1 JSON document. Operators
320
+ // hand a doc that arrived from a downstream integration (consumer
321
+ // hand-edited, contract-test fixture, third-party publish) and want
322
+ // the framework's gate to enforce the same shape rules `toJson` does
323
+ // on builder output.
324
+ //
325
+ // Returns `{ doc, errors[] }`. `doc` is the parsed object (whether
326
+ // valid or not, so the operator can inspect what they got);
327
+ // `errors` is an array of strings — empty on a valid document.
328
+ //
329
+ // Throws (config-time entry-point) on invalid JSON / wrong type.
330
+ function parse(jsonStringOrObject) {
331
+ var doc;
332
+ if (typeof jsonStringOrObject === "string") {
333
+ try { doc = JSON.parse(jsonStringOrObject); } // allow:bare-json-parse — operator-supplied OpenAPI doc; size-bounded by caller
334
+ catch (e) {
335
+ throw new OpenApiError("openapi/bad-json",
336
+ "openapi.parse: invalid JSON — " + e.message);
337
+ }
338
+ } else if (jsonStringOrObject != null && typeof jsonStringOrObject === "object") {
339
+ doc = jsonStringOrObject;
340
+ } else {
341
+ throw new OpenApiError("openapi/bad-input",
342
+ "openapi.parse: input must be a JSON string or a plain object");
343
+ }
344
+ var errors = [];
345
+ if (typeof doc.openapi !== "string") {
346
+ errors.push("missing or non-string `openapi` version field (must be 3.1.x)");
347
+ } else if (doc.openapi.indexOf("3.1") !== 0) {
348
+ errors.push("`openapi` version must be 3.1.x — got " + JSON.stringify(doc.openapi));
349
+ }
350
+ if (!doc.info || typeof doc.info !== "object") {
351
+ errors.push("missing or non-object `info`");
352
+ } else {
353
+ if (typeof doc.info.title !== "string" || doc.info.title.length === 0) {
354
+ errors.push("info.title must be a non-empty string");
355
+ }
356
+ if (typeof doc.info.version !== "string" || doc.info.version.length === 0) {
357
+ errors.push("info.version must be a non-empty string");
358
+ }
359
+ }
360
+ if (doc.paths != null && typeof doc.paths !== "object") {
361
+ errors.push("`paths` must be an object when present");
362
+ } else if (doc.paths) {
363
+ for (var pathKey in doc.paths) {
364
+ if (!Object.prototype.hasOwnProperty.call(doc.paths, pathKey)) continue;
365
+ if (pathKey.charAt(0) !== "/") {
366
+ errors.push("path " + JSON.stringify(pathKey) + " must start with '/'");
367
+ }
368
+ var pathItem = doc.paths[pathKey];
369
+ if (!pathItem || typeof pathItem !== "object") {
370
+ errors.push("paths[" + JSON.stringify(pathKey) + "] must be an object");
371
+ continue;
372
+ }
373
+ var validMethods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
374
+ for (var methodKey in pathItem) {
375
+ if (!Object.prototype.hasOwnProperty.call(pathItem, methodKey)) continue;
376
+ if (validMethods.indexOf(methodKey) === -1) continue; // allow non-method fields like 'parameters', 'summary', '$ref'
377
+ var op = pathItem[methodKey];
378
+ if (!op || typeof op !== "object") {
379
+ errors.push(methodKey.toUpperCase() + " " + pathKey + ": operation must be an object");
380
+ continue;
381
+ }
382
+ if (!op.responses || typeof op.responses !== "object" ||
383
+ Object.keys(op.responses).length === 0) {
384
+ errors.push(methodKey.toUpperCase() + " " + pathKey +
385
+ ": responses object required (per OpenAPI 3.1 §4.8.5)");
386
+ } else {
387
+ for (var statusKey in op.responses) {
388
+ if (!Object.prototype.hasOwnProperty.call(op.responses, statusKey)) continue;
389
+ var resp = op.responses[statusKey];
390
+ if (!resp || typeof resp !== "object") {
391
+ errors.push(methodKey.toUpperCase() + " " + pathKey +
392
+ " response " + statusKey + ": must be an object");
393
+ continue;
394
+ }
395
+ if (resp["$ref"]) continue; // $ref short-circuit
396
+ if (typeof resp.description !== "string" || resp.description.length === 0) {
397
+ errors.push(methodKey.toUpperCase() + " " + pathKey +
398
+ " response " + statusKey +
399
+ ": description is required (per OpenAPI 3.1 §4.8.16)");
400
+ }
401
+ }
402
+ }
403
+ if (Array.isArray(op.parameters)) {
404
+ for (var pi = 0; pi < op.parameters.length; pi += 1) {
405
+ var p = op.parameters[pi];
406
+ if (!p || typeof p !== "object") continue;
407
+ if (p["$ref"]) continue;
408
+ if (p.in === "path" && p.required !== true) {
409
+ errors.push(methodKey.toUpperCase() + " " + pathKey +
410
+ " parameters[" + pi + "]: path parameter " +
411
+ JSON.stringify(p.name) + " must have required=true");
412
+ }
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+ // Dangling security references — every requirement key must resolve
419
+ // to a registered security scheme.
420
+ var securitySchemes = (doc.components && doc.components.securitySchemes) || {};
421
+ if (Array.isArray(doc.security)) {
422
+ for (var s = 0; s < doc.security.length; s += 1) {
423
+ for (var schemeName in doc.security[s]) {
424
+ if (!Object.prototype.hasOwnProperty.call(doc.security[s], schemeName)) continue;
425
+ if (!securitySchemes[schemeName]) {
426
+ errors.push("doc-level security references undefined scheme " +
427
+ JSON.stringify(schemeName));
428
+ }
429
+ }
430
+ }
431
+ }
432
+ return { doc: doc, errors: errors, valid: errors.length === 0 };
433
+ }
434
+
435
+ module.exports = {
436
+ create: create,
437
+ parse: parse,
438
+ schemaWalk: schemaWalk.walk,
439
+ security: openapiSecurity,
440
+ toYaml: openapiYaml.toYaml,
441
+ VERSION: OPENAPI_VERSION,
442
+ OpenApiError: OpenApiError,
443
+ };
package/lib/outbox.js CHANGED
@@ -308,7 +308,7 @@ function create(opts) {
308
308
  " SET status = 'dead', attempts = $1, last_error = $2 WHERE id = $3",
309
309
  [attempts + 1, String(errMsg).slice(0, 1024), id] // allow:raw-byte-literal — error-message char cap
310
310
  );
311
- _emitAudit("system.outbox.deadletter", "fail", { id: id, attempts: attempts + 1 });
311
+ _emitAudit("system.outbox.deadletter", "failure", { id: id, attempts: attempts + 1 });
312
312
  _emitMetric("dead-letter", 1);
313
313
  }
314
314
 
@@ -353,7 +353,7 @@ function create(opts) {
353
353
  .catch(function () { /* drop-silent — see _processOnce */ })
354
354
  .finally(function () { inFlight = null; });
355
355
  }, pollIntervalMs, { name: name + "-publisher" });
356
- _emitAudit("system.outbox.started", "ok", { name: name });
356
+ _emitAudit("system.outbox.started", "success", { name: name });
357
357
  }
358
358
 
359
359
  async function stop() {
@@ -365,7 +365,7 @@ function create(opts) {
365
365
  if (inFlight) {
366
366
  try { await inFlight; } catch (_e) { /* drop-silent */ }
367
367
  }
368
- _emitAudit("system.outbox.stopped", "ok", { name: name });
368
+ _emitAudit("system.outbox.stopped", "success", { name: name });
369
369
  }
370
370
 
371
371
  async function pendingCount() {
@@ -491,8 +491,17 @@ function create(opts) {
491
491
  }
492
492
 
493
493
  if (enforceMfa) {
494
+ // Window floor — when neither route nor role supplies an
495
+ // explicit mfaWindowMs, default to 15 minutes. Without this
496
+ // floor, a stolen long-lived cookie carrying an old `mfaAt`
497
+ // walks past every requireMfa: true gate. Operators who want
498
+ // an explicit no-window pass-through must say so via
499
+ // mfaWindowMs: Infinity (audited reason).
500
+ if (enforceWindowMs === null) {
501
+ enforceWindowMs = C.TIME.minutes(15);
502
+ }
494
503
  var mfaOk = actor.mfaAuthenticated === true;
495
- if (mfaOk && enforceWindowMs !== null) {
504
+ if (mfaOk && enforceWindowMs !== null && enforceWindowMs !== Infinity) {
496
505
  var mfaAt = typeof actor.mfaAt === "number" ? actor.mfaAt : 0;
497
506
  if (Date.now() - mfaAt > enforceWindowMs) {
498
507
  mfaOk = false;
package/lib/pqc-agent.js CHANGED
@@ -48,7 +48,28 @@ var DEFAULT_OPTS = {
48
48
  function _buildAgentOpts(opts) {
49
49
  opts = opts || {};
50
50
  var merged = Object.assign({}, DEFAULT_OPTS, opts);
51
- merged.ecdhCurve = C.TLS_GROUP_CURVE_STR;
51
+ // Caller may narrow the framework's curve preference list (drop a
52
+ // group, keep the remaining ones in framework-preferred order) but
53
+ // cannot widen it. A caller-supplied `ecdhCurve` string is parsed
54
+ // into groups and every group must appear in TLS_GROUP_PREFERENCE,
55
+ // otherwise the agent build refuses. The empty narrowing is a
56
+ // misconfig — TLS won't negotiate a key share — so reject too.
57
+ if (typeof opts.ecdhCurve === "string" && opts.ecdhCurve.length > 0) {
58
+ var requested = opts.ecdhCurve.split(":");
59
+ for (var rgi = 0; rgi < requested.length; rgi++) {
60
+ if (C.TLS_GROUP_PREFERENCE.indexOf(requested[rgi]) === -1) {
61
+ throw new TypeError(
62
+ "pqc-agent: opts.ecdhCurve='" + opts.ecdhCurve + "' includes '" +
63
+ requested[rgi] + "' which is not in the framework PQC-hybrid " +
64
+ "preference (" + C.TLS_GROUP_CURVE_STR + "); construct an " +
65
+ "https.Agent directly to negotiate weaker groups."
66
+ );
67
+ }
68
+ }
69
+ merged.ecdhCurve = requested.join(":");
70
+ } else {
71
+ merged.ecdhCurve = C.TLS_GROUP_CURVE_STR;
72
+ }
52
73
  merged.minVersion = "TLSv1.3";
53
74
  if (networkTls && typeof networkTls.applyToContext === "function") {
54
75
  merged = networkTls.applyToContext({ base: merged });