@blamejs/core 0.6.6 → 0.6.11

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/lib/i18n.js CHANGED
@@ -38,16 +38,16 @@
38
38
  * - {var} interpolation; missing vars render as literal {var} unless
39
39
  * `interpolation.strict: true`.
40
40
  *
41
- * Validation tiers:
41
+ * Validation policy:
42
42
  *
43
- * - create() opts → Tier A (throw at boot)
44
- * - bad locale tag at any boundary → Tier A (throw)
45
- * - t(missingKey) → Tier C (return key + obs)
46
- * - t() with bad locale override → Tier A (programming bug)
47
- * - plural shape missing 'other' → Tier A (throw at load)
48
- * - interpolation missing var → Tier C (literal {var})
49
- * - format* bad input → Tier A
50
- * - middleware Accept-Language parse error → Tier C (defaultLocale)
43
+ * - create() opts → throw at boot
44
+ * - bad locale tag at any boundary → throw at call site
45
+ * - t(missingKey) → return key + observability event
46
+ * - t() with bad locale override → throw at call site (programming bug)
47
+ * - plural shape missing 'other' → throw at load time
48
+ * - interpolation missing var → render literal {var}
49
+ * - format* bad input → throw at call site
50
+ * - middleware Accept-Language parse error → fall back to defaultLocale
51
51
  *
52
52
  * Security stance: translation values come from operator-controlled
53
53
  * files, not user input. {var} interpolation does NOT html-escape;
@@ -97,7 +97,7 @@ var DEFAULTS = Object.freeze({
97
97
  RTL_LANGUAGES: RTL_LANGUAGES,
98
98
  });
99
99
 
100
- // ---- Tier-A validation ----
100
+ // ---- Call-site validation (throw on bad input) ----
101
101
 
102
102
  function _isValidBcp47(tag) {
103
103
  if (typeof tag !== "string" || tag.length === 0) return false;
@@ -476,7 +476,7 @@ function create(opts) {
476
476
  try {
477
477
  if (operatorObs) operatorObs.event(name, 1, labels || {});
478
478
  else observability().event(name, 1, labels || {});
479
- } catch (_e) { /* Tier B */ }
479
+ } catch (_e) { /* drop-silent observability sink must not crash i18n calls */ }
480
480
  }
481
481
 
482
482
  // Cardinal plural-rules instances per locale. `Intl.PluralRules` is
@@ -721,7 +721,7 @@ function create(opts) {
721
721
  // Permit setting a non-configured locale (operators may want to
722
722
  // experiment), but fall the chain through to the configured ones.
723
723
  // Don't throw — i18n.locale is set/observed in many flows; making
724
- // it Tier-A would force operators into try/catch around UI setters.
724
+ // this throw would force operators into try/catch around UI setters.
725
725
  _emitObs("i18n.miss.locale", { requested: newLocale, resolved: defaultLocale });
726
726
  }
727
727
  currentLocale = newLocale;
@@ -1041,8 +1041,40 @@ async function _parseJsonFromBuf(buf, opts) {
1041
1041
  return parsed;
1042
1042
  }
1043
1043
 
1044
+ // raw — convenience wrapper that returns a middleware which buffers
1045
+ // the request body as a Buffer regardless of Content-Type. Webhook
1046
+ // signature-verification routes use this — the HMAC is computed over
1047
+ // the literal body bytes, so JSON-parsing first would change them.
1048
+ //
1049
+ // router.post("/hooks/in", b.middleware.bodyParser.raw(), function (req, res) {
1050
+ // verifier.verify(req.headers["x-signature"], req.body); // req.body is a Buffer
1051
+ // });
1052
+ //
1053
+ // Accepts the same `raw`-section opts as create() (limit, contentTypes).
1054
+ // contentTypes default expands to `["*/*"]` so any Content-Type lands
1055
+ // as raw bytes.
1056
+ function raw(opts) {
1057
+ opts = opts || {};
1058
+ return create({
1059
+ json: false,
1060
+ urlencoded: false,
1061
+ text: false,
1062
+ multipart: false,
1063
+ raw: {
1064
+ limit: opts.limit != null ? opts.limit : DEFAULTS.raw.limit,
1065
+ contentTypes: opts.contentTypes || ["*/*"],
1066
+ },
1067
+ });
1068
+ }
1069
+
1070
+ // Attach raw onto create so b.middleware.bodyParser.raw() works
1071
+ // (middleware/index.js exports the create function as the namespace
1072
+ // itself, so static helpers hang off it).
1073
+ create.raw = raw;
1074
+
1044
1075
  module.exports = {
1045
1076
  create: create,
1077
+ raw: raw,
1046
1078
  BodyParserError: BodyParserError,
1047
1079
  // Internal helpers exposed for tests + the csrf-protect refactor.
1048
1080
  _contentType: _contentType,
@@ -191,7 +191,7 @@ function create(opts) {
191
191
  }
192
192
  }
193
193
 
194
- // Tier A validation on opts.siteOrigin — must parse as http(s) URL.
194
+ // Throw at create() on bad opts.siteOrigin — must parse as http(s) URL.
195
195
  // Accept string OR array of strings.
196
196
  var siteOrigins = [];
197
197
  if (opts.siteOrigin !== undefined && opts.siteOrigin !== null) {
@@ -34,7 +34,7 @@
34
34
  * (e.g. req.session.data.csrfToken). Issuance + req.csrfToken
35
35
  * exposure are the operator's responsibility in this mode.
36
36
  *
37
- * If neither is supplied, the middleware throws at create() — Tier A
37
+ * If neither is supplied, the middleware throws at create() —
38
38
  * config-time validation, no silent passthrough.
39
39
  *
40
40
  * Submitted-token sources tried in order on state-changing requests:
@@ -177,7 +177,7 @@ function create(opts) {
177
177
  ? opts.trustProxy : false;
178
178
  var _isHttps = _isHttpsFor(trustProxy);
179
179
 
180
- // Tier A — exactly one issuance source.
180
+ // Throw at create() — exactly one issuance source allowed.
181
181
  var hasCookie = opts.cookie != null && opts.cookie !== false;
182
182
  var hasLookup = typeof opts.tokenLookup === "function";
183
183
  if (hasCookie && hasLookup) {
@@ -62,15 +62,23 @@
62
62
  * Observability event: db.role.bound { value: 1, labels: { role, source } }
63
63
  * source ∈ "resolver" | "permissions" | "default"
64
64
  *
65
- * Audit emission of `db.role.switched` (the cross-request transition
66
- * record) lands in v0.6.7 this middleware focuses on binding the role.
65
+ * Audit emission: db.role.switched is recorded once per request when a
66
+ * role binds. The audit row carries the actor 5 W's via
67
+ * requestHelpers.extractActorContext and metadata { previousRole,
68
+ * newRole, source }. Defaults align with the framework's "the
69
+ * authorization decision IS the audit-worthy event" stance — both
70
+ * auditFailures and auditSuccess default true. The audit sink can be
71
+ * pinned via opts.audit (any object exposing safeEmit), defaults to
72
+ * the framework's b.audit.
67
73
  */
68
74
  var dbRoleContext = require("../db-role-context");
69
75
  var lazyRequire = require("../lazy-require");
76
+ var requestHelpers = require("../request-helpers");
70
77
  var safeSql = require("../safe-sql");
71
78
  var validateOpts = require("../validate-opts");
72
79
  var { defineClass } = require("../framework-error");
73
80
 
81
+ var audit = lazyRequire(function () { return require("../audit"); });
74
82
  var observability = lazyRequire(function () { return require("../observability"); });
75
83
 
76
84
  var DbRoleForError = defineClass("DbRoleForError", { alwaysPermanent: true });
@@ -79,6 +87,7 @@ var _err = function (code, message) { return new DbRoleForError(code, message);
79
87
  var ALLOWED_OPTS = [
80
88
  "resolve", "permissions", "defaultRole",
81
89
  "requireRole", "missingRoleStatus", "responder",
90
+ "audit", "auditFailures", "auditSuccess",
82
91
  ];
83
92
 
84
93
  function _emitEvent(name, value, labels) {
@@ -140,6 +149,20 @@ function create(opts) {
140
149
  "middleware.dbRoleFor: missingRoleStatus must be an HTTP status code (100-599)");
141
150
  }
142
151
  }
152
+ if (opts.audit !== undefined && opts.audit !== null) {
153
+ if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
154
+ throw _err("db-role-for/bad-opt",
155
+ "middleware.dbRoleFor: audit must be a b.audit-shaped object (safeEmit fn)");
156
+ }
157
+ }
158
+ if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
159
+ throw _err("db-role-for/bad-opt",
160
+ "middleware.dbRoleFor: auditFailures must be a boolean");
161
+ }
162
+ if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
163
+ throw _err("db-role-for/bad-opt",
164
+ "middleware.dbRoleFor: auditSuccess must be a boolean");
165
+ }
143
166
 
144
167
  var resolveFn = opts.resolve || null;
145
168
  var perms = opts.permissions || null;
@@ -147,6 +170,14 @@ function create(opts) {
147
170
  var requireRole = !!opts.requireRole;
148
171
  var missingRoleStatus = opts.missingRoleStatus || 401;
149
172
  var responder = opts.responder || _defaultResponder;
173
+ // Audit defaults match permissions: the role-binding decision IS the
174
+ // audit-worthy act. Operators with extreme volume opt out via
175
+ // auditSuccess: false; failures stay on regardless. The audit sink
176
+ // defaults to the framework's b.audit; operators with multiple audit
177
+ // chains pass their own (matches captureAudit's shape).
178
+ var auditSink = opts.audit || null;
179
+ var auditFailures = (opts.auditFailures === undefined) ? true : opts.auditFailures;
180
+ var auditSuccess = (opts.auditSuccess === undefined) ? true : opts.auditSuccess;
150
181
 
151
182
  return function dbRoleForMiddleware(req, res, next) {
152
183
  var role = null;
@@ -182,6 +213,15 @@ function create(opts) {
182
213
  if (!role) {
183
214
  if (requireRole) {
184
215
  _emitEvent("db.role.missing", 1, {});
216
+ if (auditFailures) {
217
+ _auditSwitch(auditSink, req, {
218
+ previousRole: dbRoleContext.getRole(),
219
+ newRole: null,
220
+ source: "middleware",
221
+ outcome: "failure",
222
+ reason: "no-role",
223
+ });
224
+ }
185
225
  return responder(req, res, missingRoleStatus, {
186
226
  error: "missing_db_role",
187
227
  status: missingRoleStatus,
@@ -206,12 +246,46 @@ function create(opts) {
206
246
  return next(e);
207
247
  }
208
248
 
249
+ var previousRole = dbRoleContext.getRole();
209
250
  req.dbRole = role;
210
251
  _emitEvent("db.role.bound", 1, { role: role, source: source });
252
+ if (auditSuccess) {
253
+ _auditSwitch(auditSink, req, {
254
+ previousRole: previousRole,
255
+ newRole: role,
256
+ source: "middleware",
257
+ outcome: "success",
258
+ });
259
+ }
211
260
  dbRoleContext.runWithRole(role, function () { next(); });
212
261
  };
213
262
  }
214
263
 
264
+ // Emit the db.role.switched audit row. Fire-and-forget — the audit
265
+ // handler's own try/catch keeps a momentary outage from breaking the
266
+ // request. The actor 5 W's come from extractActorContext (req-driven);
267
+ // metadata carries the previous + new role + binding source so a
268
+ // forensic walker can reconstruct "which role read which row when."
269
+ // The sink defaults to the framework's b.audit when the operator
270
+ // didn't pass an explicit instance.
271
+ function _auditSwitch(sink, req, info) {
272
+ try {
273
+ var emitter = sink || audit();
274
+ emitter.safeEmit({
275
+ action: "db.role.switched",
276
+ actor: requestHelpers.extractActorContext(req),
277
+ resource: { kind: "db.role", id: info.newRole || "(none)" },
278
+ outcome: info.outcome || "success",
279
+ reason: info.reason || null,
280
+ metadata: {
281
+ previousRole: info.previousRole || null,
282
+ newRole: info.newRole || null,
283
+ source: info.source,
284
+ },
285
+ });
286
+ } catch (_e) { /* audit best-effort */ }
287
+ }
288
+
215
289
  module.exports = {
216
290
  create: create,
217
291
  DbRoleForError: DbRoleForError,
@@ -256,8 +256,8 @@ function create(opts) {
256
256
  var bodyOnLimit = opts.bodyOnLimit !== undefined ? opts.bodyOnLimit : "Too Many Requests";
257
257
  var emitHeaders = opts.header !== false;
258
258
  var skipPaths = opts.skipPaths || [];
259
- // Tier-A: each entry must be a string prefix or a RegExp. Anything
260
- // else would crash _shouldSkip with TypeError on the first request.
259
+ // Throw at create(): each entry must be a string prefix or a RegExp.
260
+ // Anything else would crash _shouldSkip with TypeError on the first request.
261
261
  for (var sp = 0; sp < skipPaths.length; sp++) {
262
262
  if (typeof skipPaths[sp] !== "string" && !(skipPaths[sp] instanceof RegExp)) {
263
263
  throw new Error("middleware.rateLimit: skipPaths[" + sp +
package/lib/notify.js CHANGED
@@ -74,7 +74,7 @@ var DEFAULTS = Object.freeze({
74
74
  // we get the framework's policy without forking the constants.
75
75
  });
76
76
 
77
- // ---- Tier-A validation ----
77
+ // ---- Call-site validation (throw on bad input) ----
78
78
 
79
79
  function _isFiniteNonNegative(n) {
80
80
  return typeof n === "number" && isFinite(n) && n >= 0;
@@ -360,7 +360,7 @@ function create(opts) {
360
360
 
361
361
  function _emitObs(name, labels) {
362
362
  try { observability().event(name, 1, labels || {}); }
363
- catch (_e) { /* Tier B */ }
363
+ catch (_e) { /* drop-silent observability sink must not crash send() */ }
364
364
  }
365
365
 
366
366
  function _emitAudit(action, info) {
@@ -44,13 +44,14 @@
44
44
  * maxAgeSeconds: 3600,
45
45
  * }]);
46
46
  *
47
- * Validation is Tier-A: every input shape is rejected at the call
48
- * site rather than producing a server-side 400. Errors surface as
49
- * ObjectStoreError with codes (BUCKET_INVALID_NAME, INVALID_LIFECYCLE,
50
- * INVALID_CORS_RULE, BUCKET_ALREADY_OWNED, BUCKET_NOT_EMPTY, etc.).
47
+ * Validation rejects every bad input shape at the call site rather
48
+ * than producing a server-side 400. Errors surface as ObjectStoreError
49
+ * with codes (BUCKET_INVALID_NAME, INVALID_LIFECYCLE, INVALID_CORS_RULE,
50
+ * BUCKET_ALREADY_OWNED, BUCKET_NOT_EMPTY, etc.).
51
51
  */
52
52
  var { URL } = require("url");
53
53
  var nodeCrypto = require("crypto");
54
+ var C = require("../constants");
54
55
  var sigv4 = require("./sigv4");
55
56
  var safeXml = require("../parsers/safe-xml");
56
57
  var safeUrl = require("../safe-url");
@@ -261,7 +262,7 @@ function _buildCorsXml(rules) {
261
262
  body += "</CORSRule>";
262
263
  }
263
264
  body += "</CORSConfiguration>";
264
- if (Buffer.byteLength(body, "utf8") > 64 * 1024) {
265
+ if (Buffer.byteLength(body, "utf8") > C.BYTES.kib(64)) {
265
266
  throw _err("INVALID_CORS_RULE",
266
267
  "CORS configuration exceeds 64 KB (S3 spec)", true);
267
268
  }
@@ -214,17 +214,17 @@ function _request(method, url, headers, body, opts) {
214
214
  // CompleteMultipartUpload with EntityTooSmall. The framework refuses
215
215
  // configurations below this floor at create() time so operators don't
216
216
  // see surprising failures only on large uploads.
217
- var MIN_PART_SIZE_BYTES = 5 * 1024 * 1024;
217
+ var MIN_PART_SIZE_BYTES = C.BYTES.mib(5);
218
218
  // S3 spec ceiling on part count. CompleteMultipartUpload rejects
219
219
  // uploads with more than 10000 parts.
220
220
  var MAX_PARTS = 10000;
221
221
  // Auto-multipart trigger: buffered bodies under this stay single-PUT.
222
222
  // Streams always go multipart since size isn't known up-front.
223
- var DEFAULT_MULTIPART_THRESHOLD_BYTES = 64 * 1024 * 1024;
223
+ var DEFAULT_MULTIPART_THRESHOLD_BYTES = C.BYTES.mib(64);
224
224
  // Conservative default part size — large enough to keep round-trip
225
225
  // overhead small relative to payload, small enough to fit comfortably
226
226
  // in a 4-way-concurrent upload's memory footprint.
227
- var DEFAULT_PART_SIZE_BYTES = 16 * 1024 * 1024;
227
+ var DEFAULT_PART_SIZE_BYTES = C.BYTES.mib(16);
228
228
  var DEFAULT_PART_CONCURRENCY = 4;
229
229
 
230
230
  // ---- SSE option handling ----
@@ -59,18 +59,43 @@ var lazyRequire = require("./lazy-require");
59
59
  var tracing = lazyRequire(function () { return require("./tracing"); });
60
60
  var metrics = lazyRequire(function () { return require("./metrics"); });
61
61
 
62
+ // Operator-installed tap handler — wired via setTap(). When non-null,
63
+ // every observability event/tap dispatch routes here in addition to
64
+ // the framework's metrics module. Used by b.otelExport.create() so an
65
+ // OTLP/HTTP exporter receives the same hot-path counters the framework
66
+ // emits internally.
67
+ var _externalTap = null;
68
+
62
69
  function _safeMetricsTap(name, value, labels) {
63
70
  try { metrics().tap(name, value, labels); }
64
71
  catch (_e) { /* boot-order tolerance — metrics may not be loaded */ }
72
+ if (_externalTap !== null) {
73
+ try { _externalTap(name, value, labels); }
74
+ catch (_e) { /* operator-installed handler — drop-silent on its throws */ }
75
+ }
76
+ }
77
+
78
+ // setTap — install an external tap handler. Operators wire this from
79
+ // `b.otelExport.create({...}).tapHandler` so every framework counter
80
+ // also lands in the operator's metrics pipeline.
81
+ //
82
+ // The handler signature mirrors metrics.tap: (name, value, labels).
83
+ // Pass null to remove the previously-installed handler.
84
+ function setTap(handler) {
85
+ if (handler !== null && typeof handler !== "function") {
86
+ throw new TypeError("observability.setTap: handler must be a function or null, got " +
87
+ typeof handler);
88
+ }
89
+ _externalTap = handler;
65
90
  }
66
91
 
67
92
  function tap(name, attrs, fn) {
68
93
  if (typeof attrs === "function") { fn = attrs; attrs = null; }
69
- // Tier A validation: tap is called from many call sites and a typo
70
- // in the name (e.g. variable holding undefined) silently corrupts both
71
- // the span tree AND the metrics counter route, with no obvious symptom
72
- // until somebody opens a dashboard. Throw at first call so the
73
- // operator catches it.
94
+ // Throw on bad input: tap is called from many call sites and a typo
95
+ // in the name (e.g. variable holding undefined) silently corrupts
96
+ // both the span tree AND the metrics counter route, with no obvious
97
+ // symptom until somebody opens a dashboard. Throw at first call so
98
+ // the operator catches it.
74
99
  if (typeof name !== "string" || name.length === 0) {
75
100
  throw new TypeError("observability.tap: name must be a non-empty string, got " +
76
101
  (typeof name) + " " + JSON.stringify(name));
@@ -97,18 +122,19 @@ function tap(name, attrs, fn) {
97
122
  });
98
123
  }
99
124
 
100
- // Tier B (drop-silent): event is the fire-and-forget shape called from
101
- // hot paths where throwing would crash the request that triggered it.
102
- // Bad input is silently dropped — operators with a misnamed event see
103
- // the missing counter, not a 500. metrics.tap performs its own label-
104
- // name regex validation; an invalid call surfaces in the metrics module
105
- // log, not via a thrown exception.
125
+ // Drop-silent on bad input by design: event is the fire-and-forget
126
+ // shape called from hot paths where throwing would crash the request
127
+ // that triggered it. Operators with a misnamed event see the missing
128
+ // counter, not a 500. metrics.tap performs its own label-name regex
129
+ // validation; an invalid call surfaces in the metrics module log, not
130
+ // via a thrown exception.
106
131
  function event(name, value, labels) {
107
132
  if (typeof name !== "string" || name.length === 0) return;
108
133
  _safeMetricsTap(name, value, labels);
109
134
  }
110
135
 
111
136
  module.exports = {
112
- tap: tap,
113
- event: event,
137
+ tap: tap,
138
+ event: event,
139
+ setTap: setTap,
114
140
  };
@@ -27,8 +27,8 @@
27
27
  *
28
28
  * Wiring with `b.observability`:
29
29
  *
30
- * // Option A: replace the metrics tap entirely
31
- * b.observability._setTap(otel.tapHandler);
30
+ * // Option A: install as an external tap on b.observability
31
+ * b.observability.setTap(otel.tapHandler);
32
32
  *
33
33
  * // Option B: alongside b.metrics — operators write their own
34
34
  * // multi-tap fan-out (or pick one or the other for v1).
package/lib/pagination.js CHANGED
@@ -215,10 +215,10 @@ async function cursor(query, opts) {
215
215
  }
216
216
  var limit = _resolveLimit(opts);
217
217
  var orderBy = typeof opts.orderBy === "string" && opts.orderBy.length > 0 ? opts.orderBy : "_id";
218
- // Tier-A on orderBy — the value is interpolated into a raw SQL fragment
219
- // for the keyset where-clause. Restrict to identifier-safe characters
220
- // so a careless caller piping `req.query.orderBy` through doesn't
221
- // create an SQL-injection vector.
218
+ // Throw at call site on bad orderBy — the value is interpolated into
219
+ // a raw SQL fragment for the keyset where-clause. Restrict to
220
+ // identifier-safe characters so a careless caller piping
221
+ // `req.query.orderBy` through doesn't create an SQL-injection vector.
222
222
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(orderBy)) {
223
223
  throw new PaginationError("pagination/bad-orderby",
224
224
  "cursor: orderBy must match /^[A-Za-z_][A-Za-z0-9_]*$/ (identifier-safe), got " +
@@ -342,8 +342,8 @@ async function offset(query, opts) {
342
342
  var page = parseInt(opts.page, 10);
343
343
  if (isNaN(page) || page < 1) page = 1;
344
344
  var orderBy = typeof opts.orderBy === "string" && opts.orderBy.length > 0 ? opts.orderBy : "_id";
345
- // Same Tier-A on offset() as cursor() — orderBy passes through to the
346
- // db Query; identifier-safe-only to prevent SQL injection.
345
+ // Same identifier-only check on offset() as cursor() — orderBy passes
346
+ // through to the db Query; throw at call site to prevent SQL injection.
347
347
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(orderBy)) {
348
348
  throw new PaginationError("pagination/bad-orderby",
349
349
  "offset: orderBy must match /^[A-Za-z_][A-Za-z0-9_]*$/ (identifier-safe), got " +
@@ -31,14 +31,14 @@
31
31
  * "users:*:read" matches "users:foo:read" (per-segment *)
32
32
  * "users:read" matches "users:read" only — no implicit sub-resource grant
33
33
  *
34
- * Validation tiers:
34
+ * Validation policy:
35
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)
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
39
  * - resolver returns null in middleware → 401 (missingActorStatus)
40
40
  * - actor lacks scope in middleware → 403 (denyStatus)
41
- * - audit/observability emit failures → Tier B (drop silent)
41
+ * - audit/observability emit failures → drop silent (hot-path sink)
42
42
  *
43
43
  * Audit defaults follow the framework's security-defaults stance
44
44
  * default: `auditFailures: true`
@@ -57,7 +57,7 @@ var observability = lazyRequire(function () { return require("./observability");
57
57
 
58
58
  function _emitEvent(name, value, labels) {
59
59
  try { observability().event(name, value, labels || {}); }
60
- catch (_e) { /* Tier B: hot-path observability sink */ }
60
+ catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
61
61
  }
62
62
 
63
63
  // Lowercase tokens, digits, dash, underscore, and `*` allowed per
@@ -331,7 +331,7 @@ function create(opts) {
331
331
  }
332
332
 
333
333
  // Middleware factory. `mode` is "single" | "all" | "any"; `requested`
334
- // is the scope or scope list. Tier A on registration arg.
334
+ // is the scope or scope list. Throw at registration time on bad shape.
335
335
  function _middleware(mode, requested) {
336
336
  if (mode === "single") {
337
337
  _validateScopePattern(requested, "permissions.require");
@@ -51,8 +51,8 @@ var { defineClass } = require("./framework-error");
51
51
  // supply their own get this generic shape with the same code namespace.
52
52
  var ProtocolDispatcherError = defineClass("ProtocolDispatcherError", { withStatusCode: true });
53
53
 
54
- // Tier A validation — called at create-time, throw on bad input so
55
- // operators catch typos at app boot, not first request.
54
+ // Throw at create-time on bad input so operators catch typos at app
55
+ // boot, not first request.
56
56
  function _validateConfig(opts) {
57
57
  if (!opts || typeof opts !== "object") {
58
58
  throw new Error("protocolDispatcher.create: opts is required");
package/lib/queue.js CHANGED
@@ -505,7 +505,7 @@ function _resetForTest() {
505
505
  // ],
506
506
  // });
507
507
  //
508
- // Cycle detection runs at registration (Tier-A throw). Each child enters
508
+ // Cycle detection runs at registration (throws at call site). Each child enters
509
509
  // the queue with availableAt = MAX_SAFE_INTEGER until parent completion
510
510
  // bumps it. Returns { flowId, jobs: [{ name, jobId }, ...] }.
511
511
  function enqueueFlow(spec) {
@@ -175,9 +175,9 @@ function requestProtocol(req, opts) {
175
175
  // parseListHeader(undefined) → []
176
176
  // parseListHeader("") → []
177
177
  //
178
- // Tier-C: tolerant of non-string input (returns []) — these are read
179
- // from request headers that the network might omit. Callers needing
180
- // stricter checks layer their own validation on the result.
178
+ // Tolerant read: non-string input returns [] — these are read from
179
+ // request headers that the network might omit. Callers needing stricter
180
+ // checks layer their own validation on the result.
181
181
  function parseListHeader(value, opts) {
182
182
  if (value == null) return [];
183
183
  opts = opts || {};
package/lib/retry.js CHANGED
@@ -18,14 +18,14 @@
18
18
  * compose freely with `safeAsync.withTimeout`, AbortSignals, and any
19
19
  * caller-side instrumentation.
20
20
  *
21
- * Validation tiers:
21
+ * Validation policy:
22
22
  *
23
- * - withRetry opts at first call → Tier A (throw)
24
- * - CircuitBreaker constructor opts → Tier A (throw)
25
- * - backoffDelay(attempt) attempt argument → Tier A (throw)
26
- * - isRetryable(err) defensive read → Tier C (return defaults)
27
- * - onRetry callback throw → Tier B (drop silent)
28
- * - breaker internal _onSuccess/_onFailure → Tier B (drop silent)
23
+ * - withRetry opts at first call → throw at call site
24
+ * - CircuitBreaker constructor opts → throw at call site
25
+ * - backoffDelay(attempt) attempt argument → throw at call site
26
+ * - isRetryable(err) defensive read → tolerant (return defaults)
27
+ * - onRetry callback throw → drop silent (hot-path sink)
28
+ * - breaker internal _onSuccess/_onFailure → drop silent (hot-path sink)
29
29
  *
30
30
  * HTTP-client auto-retry is intentionally NOT provided here. Callers
31
31
  * wrap their own outbound calls in `b.retry.withRetry(...)` to keep
@@ -40,13 +40,13 @@ var lazyRequire = require("./lazy-require");
40
40
  // are fully loaded.
41
41
  var safeAsync = lazyRequire(function () { return require("./safe-async"); });
42
42
  // observability is also lazy-required because the metrics + tracing
43
- // registry boots after this file loads. event() is Tier B any
44
- // throw inside the metrics sink is swallowed by observability itself.
43
+ // registry boots after this file loads. event() is fire-and-forget
44
+ // any throw inside the metrics sink is swallowed by observability itself.
45
45
  var observability = lazyRequire(function () { return require("./observability"); });
46
46
 
47
47
  function _emitEvent(name, value, labels) {
48
48
  try { observability().event(name, value, labels || {}); }
49
- catch (_e) { /* Tier B: hot-path observability sink */ }
49
+ catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
50
50
  }
51
51
 
52
52
  // ---- Defaults ----
@@ -80,7 +80,7 @@ var DEFAULT_BREAKER = Object.freeze({
80
80
  successThreshold: 2, // consecutive HALF probes that close it
81
81
  });
82
82
 
83
- // ---- Tier-A validation helpers ----
83
+ // ---- Call-site validation helpers (throw on bad input) ----
84
84
 
85
85
  function _isPositiveInt(n) {
86
86
  return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
@@ -147,7 +147,7 @@ function _validateBreakerOpts(name, opts) {
147
147
 
148
148
  // ---- Public surface ----
149
149
 
150
- // Tier C: defensive read of err shape; missing fields → false.
150
+ // Tolerant read of err shape; missing fields → false.
151
151
  function isRetryable(err) {
152
152
  if (!err) return false;
153
153
  if (err.isObjectStoreError && err.permanent) return false;
@@ -162,11 +162,12 @@ function isRetryable(err) {
162
162
  return false; // default: not retryable (avoid masking bugs)
163
163
  }
164
164
 
165
- // Tier A: attempt must be a positive int; opts (when supplied) must
166
- // have non-neg-finite baseDelayMs/maxDelayMs and finite jitterFactor in [0,1].
167
- // We don't full-validate opts here every call (hot path) — defaults are
168
- // frozen, so the only way a bad opts reaches here is via withRetry which
169
- // already validated, OR a caller using backoffDelay directly. For that
165
+ // Throw on bad input: attempt must be a positive int; opts (when supplied)
166
+ // must have non-neg-finite baseDelayMs/maxDelayMs and finite jitterFactor
167
+ // in [0,1]. We don't full-validate opts here every call (hot path) —
168
+ // defaults are frozen, so the only way a bad opts reaches here is via
169
+ // withRetry which already validated, OR a caller using backoffDelay
170
+ // directly. For that
170
171
  // direct case we still validate the attempt arg loudly.
171
172
  function backoffDelay(attempt, opts) {
172
173
  if (!_isPositiveInt(attempt)) {
@@ -212,8 +213,8 @@ async function withRetry(fn, opts) {
212
213
  var delay = backoffDelay(attempt, opts);
213
214
  _emitEvent("retry.attempt", 1, { attempt: attempt });
214
215
  if (typeof opts.onRetry === "function") {
215
- // Tier B: hot-path observability sink. A thrown observer must
216
- // not crash the retry loop — by design.
216
+ // Hot-path observability sink drops silent on observer throw
217
+ // so a thrown observer can't crash the retry loop.
217
218
  try { opts.onRetry({ attempt: attempt, delay: delay, error: err }); } catch (_e) {}
218
219
  }
219
220
  // Honor opts.signal during the backoff sleep — a caller who aborts