@blamejs/core 0.8.43 → 0.8.50

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 (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/pagination.js CHANGED
@@ -1,98 +1,49 @@
1
1
  "use strict";
2
2
  /**
3
- * pagination — cursor + offset helpers.
3
+ * @module b.pagination
4
+ * @nav Tools
5
+ * @title Pagination
4
6
  *
5
- * Every CRUD list endpoint reinvents pagination, usually wrong.
6
- * The two failure modes:
7
+ * @intro
8
+ * Cursor-based pagination opaque tokens that encode the last-row
9
+ * sort key + direction, resilient to inserts and deletes between
10
+ * pages.
7
11
  *
8
- * - Offset pagination at depth: `LIMIT n OFFSET 50000` makes the DB
9
- * scan-and-skip 50,000 rows. O(n). With concurrent writes, rows
10
- * are also missed/duplicated as new inserts shift the offset.
11
- * - Cursor pagination without a tie-breaker: `WHERE createdAt > ?`
12
- * skips or duplicates rows when two records share createdAt.
12
+ * Every CRUD list endpoint reinvents pagination, usually wrong. The
13
+ * two failure modes: offset pagination at depth (`LIMIT n OFFSET
14
+ * 50000` scan-and-skips 50,000 rows; concurrent writes shift the
15
+ * offset and rows get missed or duplicated), and cursor pagination
16
+ * without a tie-breaker (`WHERE createdAt > ?` skips or duplicates
17
+ * rows when two records share `createdAt`).
13
18
  *
14
- * This module ships both, done correctly, plus the encode/decode
15
- * primitives operators reach for when their SQL doesn't fit the
16
- * Query-builder shape.
19
+ * This module ships both done correctly. `cursor()` uses composite
20
+ * `(orderBy, _id)` ordering `_id` is the implicit tie-breaker, so
21
+ * two rows with identical `orderByVal` are still totally ordered.
22
+ * Forward navigation: `WHERE (orderByVal > ?) OR (orderByVal = ? AND
23
+ * _id > ?)`. Backward: same with `<`, then reverse the result set.
17
24
  *
18
- * Public API:
25
+ * Cursors are HMAC-tagged with operator-supplied `secret`. A tampered
26
+ * cursor is detected at decode time and rejected with
27
+ * `PaginationError`. Cursor format: `<base64url state>.<base64url
28
+ * tag>`, state is canonical JSON of `{ v, orderKey, vals, forward }`,
29
+ * tag is `SHA3-512(secret || stateJson).slice(0, 16)`. Direction is
30
+ * part of the cursor — operators don't round-trip it via query
31
+ * string. Multi-column ordering accepted: a string, an array of
32
+ * strings, or `[{ column, direction }, ...]`; `_id` is appended as a
33
+ * tiebreaker if not already in the chain.
19
34
  *
20
- * var p = b.pagination;
35
+ * `offset()` is the legacy-client tool, not the recommended path.
36
+ * It returns `total` (from `COUNT(*)`) and computes `totalPages` so
37
+ * legacy clients can render numbered nav.
21
38
  *
22
- * // Cursor: O(1) at any depth. Composite (orderBy, _id) ordering
23
- * // so ties on the orderBy column are broken by _id and rows are
24
- * // never skipped.
25
- * var page = await p.cursor(b.db.from("users"), {
26
- * cursor: req.query.cursor,
27
- * limit: req.query.limit,
28
- * max: 100,
29
- * default: 25,
30
- * orderBy: "_id", // default; use "createdAt" etc.
31
- * direction: "asc", // "asc" | "desc"
32
- * secret: pageSecret, // Buffer or string; HMAC-tag the cursor
33
- * });
34
- * // → { items: [...], nextCursor, prevCursor, limit, hasMore }
35
- *
36
- * // Offset: page-numbered. Ergonomic for legacy clients.
37
- * var off = await p.offset(b.db.from("users"), {
38
- * page: req.query.page,
39
- * perPage: req.query.perPage,
40
- * max: 100,
41
- * default: 25,
42
- * });
43
- * // → { items, total, page, perPage, totalPages, hasMore }
44
- *
45
- * // Low-level — for raw SQL or custom row sources.
46
- * var token = p.encodeCursor({ orderByVal: 12345, id: "abc" }, secret);
47
- * var state = p.decodeCursor(token, secret);
48
- *
49
- * Cursor design:
50
- * - Composite ordering: (orderBy column, _id). _id is the implicit
51
- * tie-breaker, so two rows with identical orderByVal are still
52
- * totally ordered. Forward navigation: WHERE
53
- * (orderByVal > cur.orderByVal) OR
54
- * (orderByVal = cur.orderByVal AND _id > cur.id)
55
- * Backward: same with `<`, then reverse the result set.
56
- * - Cursors are HMAC-tagged. A tampered cursor is detected at decode
57
- * time and rejected with PaginationError. Operators MUST pass
58
- * `secret` (Buffer or string) — there's no auto-derivation, since
59
- * framework-derived secrets would produce surprises across deploys.
60
- * - Cursor format: `<base64url state>.<base64url tag>`. State is
61
- * canonical JSON of `{ v, dir, orderBy, orderByVal, id }`. Tag is
62
- * SHA3-512(secret || stateJson).slice(0, 16).
63
- * - direction is part of the cursor — operators don't need to round-
64
- * trip it via query string. The cursor itself encodes whether it's
65
- * a "next" or "prev" position so navigation stays consistent.
66
- *
67
- * Limit semantics:
68
- * - Operator passes `default` and `max`. The effective limit is
69
- * min(max, requestedLimit || default). Negative or non-integer
70
- * limits are coerced to default.
71
- * - The page query fetches limit+1 to detect hasMore without a
72
- * second COUNT(*) trip.
39
+ * Cursor TTL / expiry is operator-side: embed a timestamp in your own
40
+ * state and check at decode-time before passing to `.cursor()`. The
41
+ * framework's HMAC tag carries no notion of time. Search / filter
42
+ * integration composes chain `.where()` on the Query before handing
43
+ * to `.cursor()`.
73
44
  *
74
- * Offset is the legacy-client tool, not the recommended path. The
75
- * module's offset() returns a `total` (from COUNT(*)) and computes
76
- * `totalPages` so legacy clients can render numbered nav.
77
- *
78
- * Multi-column ordering:
79
- * - orderBy accepts a string (single column), an array of strings
80
- * (multiple columns, all using opts.direction), or an array of
81
- * { column, direction } objects (mixed directions per column).
82
- * The keyset WHERE expands to the standard OR cascade
83
- * (col0 [op0] ? OR (col0 = ? AND col1 [op1] ?) OR ...)
84
- * so successive pages can't repeat or skip rows when ties on the
85
- * leading columns are broken by trailing ones. _id is appended as
86
- * a tiebreaker if not already in the orderBy chain.
87
- *
88
- * Out of scope (with structural reasons documented):
89
- * - Cursor TTL / expiry. Operators who want time-limited cursors
90
- * embed a timestamp in their own state and check at decode-time
91
- * before passing to .cursor(). The framework's HMAC tag carries
92
- * no notion of time.
93
- * - Search / filter integration. Operators chain .where() on the
94
- * Query before handing to .cursor() — pagination composes with
95
- * whatever filtering the operator's already applied.
45
+ * @card
46
+ * Cursor-based pagination opaque tokens that encode the last-row sort key + direction, resilient to inserts and deletes between pages.
96
47
  */
97
48
 
98
49
  var nodeCrypto = require("node:crypto");
@@ -151,6 +102,29 @@ function _tag(secretBuf, stateJson) {
151
102
  return h.digest().slice(0, TAG_BYTES);
152
103
  }
153
104
 
105
+ /**
106
+ * @primitive b.pagination.encodeCursor
107
+ * @signature b.pagination.encodeCursor(state, secret)
108
+ * @since 0.6.20
109
+ * @related b.pagination.decodeCursor, b.pagination.cursor
110
+ *
111
+ * Low-level cursor encoder for raw-SQL or custom row-source paths.
112
+ * Wraps `state` with the framework version field, canonicalises via
113
+ * the shared canonical-JSON walker, then computes the SHA3-512 HMAC
114
+ * tag and emits `<base64url(stateJson)>.<base64url(tag)>`. State is
115
+ * any plain-data object — `Buffer` / `Map` / `Set` / `RegExp` /
116
+ * functions / circular references are rejected loudly. `secret` is a
117
+ * `Buffer` or non-empty string; an empty secret throws.
118
+ *
119
+ * @example
120
+ * var token = b.pagination.encodeCursor(
121
+ * { orderKey: ["createdAt:asc", "_id:asc"], vals: [1700000000000, "u-42"], forward: true },
122
+ * "page-secret"
123
+ * );
124
+ * // token is `<base64url state>.<base64url tag>`, ready to round-trip via query string.
125
+ * var state = b.pagination.decodeCursor(token, "page-secret");
126
+ * state.forward; // → true
127
+ */
154
128
  function encodeCursor(state, secret) {
155
129
  if (!state || typeof state !== "object") {
156
130
  throw new PaginationError("pagination/bad-state",
@@ -166,6 +140,32 @@ function encodeCursor(state, secret) {
166
140
  return _b64urlEncode(json) + "." + _b64urlEncode(tag);
167
141
  }
168
142
 
143
+ /**
144
+ * @primitive b.pagination.decodeCursor
145
+ * @signature b.pagination.decodeCursor(token, secret)
146
+ * @since 0.6.20
147
+ * @related b.pagination.encodeCursor, b.pagination.cursor
148
+ *
149
+ * Inverse of `encodeCursor`. Splits on the `.` separator, base64url-
150
+ * decodes both halves, recomputes the HMAC tag against `secret` and
151
+ * compares with `b.crypto.timingSafeEqual`. On mismatch (tamper or
152
+ * wrong secret) throws `PaginationError("pagination/cursor-tag-
153
+ * mismatch")`. State JSON is parsed via `b.safeJson.parse` with a
154
+ * 8-KiB byte cap. The framework version field (`v`) must match the
155
+ * current `CURSOR_VERSION`; older cursors throw `pagination/cursor-
156
+ * version` so operators can detect rolling-deploy mismatches.
157
+ *
158
+ * @example
159
+ * try {
160
+ * var state = b.pagination.decodeCursor(req.query.cursor, "page-secret");
161
+ * state.vals; // → [1700000000000, "u-42"]
162
+ * state.forward; // → true
163
+ * } catch (e) {
164
+ * // PaginationError — tamper, wrong secret, or stale cursor version.
165
+ * res.statusCode = 400;
166
+ * res.end("invalid cursor");
167
+ * }
168
+ */
169
169
  function decodeCursor(token, secret) {
170
170
  if (typeof token !== "string" || token.length === 0) {
171
171
  throw new PaginationError("pagination/bad-cursor", "cursor must be a non-empty string");
@@ -300,6 +300,54 @@ function _buildKeysetWhere(orderEntries, cursorVals, forward) {
300
300
  return { sql: clauses.join(" OR "), params: params };
301
301
  }
302
302
 
303
+ /**
304
+ * @primitive b.pagination.cursor
305
+ * @signature b.pagination.cursor(query, opts)
306
+ * @since 0.6.20
307
+ * @related b.pagination.offset, b.pagination.encodeCursor
308
+ *
309
+ * Cursor pagination over a `b.db.from(...)` Query. O(1) at any depth.
310
+ * Builds the keyset `WHERE` from the previous page's column values,
311
+ * applies the operator's `orderBy` chain (with `_id` appended as
312
+ * tiebreaker), fetches `limit + 1` rows to detect `hasMore` without a
313
+ * second `COUNT(*)`, and returns `{ items, nextCursor, prevCursor,
314
+ * limit, hasMore }`. Cursors round-trip via opaque base64url strings
315
+ * — operators don't pick apart the encoded state.
316
+ *
317
+ * Operators MUST pass `opts.secret` (Buffer or non-empty string) for
318
+ * HMAC tagging. There's no auto-derivation — framework-derived
319
+ * secrets would surprise across deploys.
320
+ *
321
+ * @opts
322
+ * cursor: string, // opaque token from a previous response (omit for first page)
323
+ * limit: number, // requested page size; clamped to opts.max, defaults to opts.default
324
+ * max: number, // hard cap on limit (defaults to 100)
325
+ * default: number, // limit when none requested (defaults to 25)
326
+ * orderBy: string|array, // column name, ["a","b"], or [{column,direction}]
327
+ * direction: "asc"|"desc", // default direction applied to string/array forms
328
+ * secret: Buffer|string, // REQUIRED — HMAC key for cursor tag
329
+ * forward: boolean, // override cursor's encoded direction (rare)
330
+ *
331
+ * @example
332
+ * var page = await b.pagination.cursor(b.db.from("users"), {
333
+ * cursor: req.query.cursor,
334
+ * limit: parseInt(req.query.limit, 10),
335
+ * max: 100,
336
+ * default: 25,
337
+ * orderBy: "createdAt",
338
+ * direction: "desc",
339
+ * secret: "page-secret",
340
+ * });
341
+ * page.items; // → array of rows (length <= limit)
342
+ * page.nextCursor; // → string token, or null when there's no next page
343
+ * page.hasMore; // → true when more rows exist beyond this page
344
+ *
345
+ * // Multi-column ordering with mixed directions:
346
+ * var mixed = await b.pagination.cursor(b.db.from("orders"), {
347
+ * orderBy: [{ column: "priority", direction: "desc" }, { column: "createdAt", direction: "asc" }],
348
+ * secret: "page-secret",
349
+ * });
350
+ */
303
351
  async function cursor(query, opts) {
304
352
  if (!query || typeof query.where !== "function" || typeof query.orderBy !== "function" ||
305
353
  typeof query.limit !== "function" || typeof query.all !== "function") {
@@ -405,6 +453,40 @@ async function cursor(query, opts) {
405
453
 
406
454
  // ---- Offset pagination ----
407
455
 
456
+ /**
457
+ * @primitive b.pagination.offset
458
+ * @signature b.pagination.offset(query, opts)
459
+ * @since 0.6.20
460
+ * @related b.pagination.cursor
461
+ *
462
+ * Offset pagination — page-numbered, ergonomic for legacy clients
463
+ * that render numbered nav. Issues `COUNT(*)` to compute `total` and
464
+ * `totalPages`. Use `cursor()` for new endpoints; `offset()` is only
465
+ * the right shape when the consumer's UI already binds to page
466
+ * numbers. Re-applies the operator's `where()` chain unmodified, then
467
+ * adds `ORDER BY orderBy direction LIMIT perPage OFFSET (page-1)*perPage`.
468
+ *
469
+ * @opts
470
+ * page: number, // 1-based page number (defaults to 1; non-integer coerces to 1)
471
+ * perPage: number, // rows per page (clamped to opts.max, defaults to opts.default)
472
+ * max: number, // hard cap on perPage (defaults to 100)
473
+ * default: number, // perPage when none requested (defaults to 25)
474
+ * orderBy: string, // column name (defaults to "_id"); identifier-validated against safeSql
475
+ * direction: "asc"|"desc", // sort direction (defaults to "asc")
476
+ *
477
+ * @example
478
+ * var page = await b.pagination.offset(b.db.from("users"), {
479
+ * page: parseInt(req.query.page, 10),
480
+ * perPage: parseInt(req.query.perPage, 10),
481
+ * max: 100,
482
+ * default: 25,
483
+ * orderBy: "createdAt",
484
+ * direction: "desc",
485
+ * });
486
+ * page.total; // → e.g. 1284
487
+ * page.totalPages; // → e.g. 52 (when perPage=25)
488
+ * page.hasMore; // → true when page < totalPages
489
+ */
408
490
  async function offset(query, opts) {
409
491
  if (!query || typeof query.limit !== "function" || typeof query.offset !== "function" ||
410
492
  typeof query.all !== "function" || typeof query.count !== "function") {
@@ -70,11 +70,22 @@ var safeIni = require("./safe-ini");
70
70
  var safeToml = require("./safe-toml");
71
71
  var safeXml = require("./safe-xml");
72
72
  var safeYaml = require("./safe-yaml");
73
+ var bodyParser = require("../middleware/body-parser");
73
74
 
75
+ // Standalone async parsers for request bodies. Same parsing pipeline the
76
+ // b.middleware.bodyParser uses — handlers that lazy-parse (route-shape
77
+ // dispatch, streaming endpoints that bypass middleware) call these inline:
78
+ //
79
+ // var body = await b.parsers.json(req, { maxBytes: C.BYTES.mib(2) });
80
+ // var parts = await b.parsers.multipart(req, { maxBytes: C.BYTES.mib(50), maxFiles: 5 });
81
+ //
82
+ // The middleware composes these — no parallel parser to drift.
74
83
  module.exports = {
75
- xml: safeXml,
76
- toml: safeToml,
77
- yaml: safeYaml,
78
- env: safeEnv,
79
- ini: safeIni,
84
+ xml: safeXml,
85
+ toml: safeToml,
86
+ yaml: safeYaml,
87
+ env: safeEnv,
88
+ ini: safeIni,
89
+ json: bodyParser.parseJson,
90
+ multipart: bodyParser.parseMultipart,
80
91
  };
@@ -1,48 +1,27 @@
1
1
  "use strict";
2
2
  /**
3
- * b.permissions — RBAC primitive.
3
+ * @module b.permissions
4
+ * @nav Identity
5
+ * @title Permissions
4
6
  *
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);
7
+ * @intro
8
+ * RBAC / ABAC / scope-based access control — declare roles, resolve
9
+ * user permissions, gate routes via middleware. Composes with
10
+ * b.apiKey (scopes flow from req.apiKey) and b.middleware.attachUser
11
+ * (roles flow from req.user). Wildcard scope syntax mirrors the
12
+ * project-wide convention: `"users:*"` (trailing-greedy),
13
+ * `"*:read"` (per-segment), `"*"` (root-greedy).
18
14
  *
19
- * The default resolver chain reads the actor from the request:
15
+ * The middleware factory runs three layers per request: RBAC scope
16
+ * match, optional MFA freshness gate (per-route or per-role), and
17
+ * ABAC predicate evaluation when a policy is registered for the
18
+ * requested scope. Failures emit `permissions.check.deny` /
19
+ * `permissions.mfa.required` / `permissions.policy.deny` audit
20
+ * events with the actor 5 W's so a compliance reviewer can
21
+ * reconstruct exactly which layer refused the request.
20
22
  *
21
- * req.apiKey.scopes → { scopes: [...] } (b.apiKey.verify output)
22
- * req.user.scopes → { scopes: [...] } (operator-set)
23
- * req.user.roles → { roles: [...] } (operator-set)
24
- *
25
- * Operators with non-default request shapes pass `resolver` to create().
26
- *
27
- * Wildcard semantics (b.permissions.match):
28
- * "*" matches any scope (greedy)
29
- * "users:*" matches "users:read", "users:read:detail", etc. (trailing * is greedy)
30
- * "*:read" matches "users:read", "posts:read"
31
- * "users:*:read" matches "users:foo:read" (per-segment *)
32
- * "users:read" matches "users:read" only — no implicit sub-resource grant
33
- *
34
- * Validation policy:
35
- *
36
- * - create() role table / scope formats → throw at app init
37
- * - require(scope) registration arg → throw at route declaration
38
- * - check(actor, scope) bad actor → return false (tolerant read)
39
- * - resolver returns null in middleware → 401 (missingActorStatus)
40
- * - actor lacks scope in middleware → 403 (denyStatus)
41
- * - audit/observability emit failures → drop silent (hot-path sink)
42
- *
43
- * Audit defaults follow the framework's security-defaults stance
44
- * default: `auditFailures: true`
45
- * (deny is a security signal), `auditSuccess: false` (per-request noise).
23
+ * @card
24
+ * RBAC / ABAC / scope-based access control — declare roles, resolve user permissions, gate routes via middleware.
46
25
  */
47
26
 
48
27
  var C = require("./constants");
@@ -82,6 +61,27 @@ var DEFAULTS = Object.freeze({
82
61
 
83
62
  // ---- Wildcard matcher ----
84
63
 
64
+ /**
65
+ * @primitive b.permissions.match
66
+ * @signature b.permissions.match(granted, required)
67
+ * @since 0.4.9
68
+ * @status stable
69
+ * @related b.permissions.create
70
+ *
71
+ * Project-wide scope wildcard matcher. Returns `true` when `granted`
72
+ * (a scope held by the actor, possibly containing `*`) covers
73
+ * `required` (the scope a route demands, always concrete). Trailing
74
+ * `*` is greedy across remaining segments; mid-string `*` matches a
75
+ * single segment.
76
+ *
77
+ * @example
78
+ * b.permissions.match("users:*", "users:read"); // → true
79
+ * b.permissions.match("users:*", "users:read:detail"); // → true
80
+ * b.permissions.match("*:read", "users:read"); // → true
81
+ * b.permissions.match("users:*:read", "users:42:read"); // → true
82
+ * b.permissions.match("users:read", "users:write"); // → false
83
+ * b.permissions.match("users:read", "users:read:audit"); // → false (no implicit sub-resource grant)
84
+ */
85
85
  function match(granted, required) {
86
86
  if (typeof granted !== "string" || typeof required !== "string") return false;
87
87
  if (granted.length === 0 || required.length === 0) return false;
@@ -265,6 +265,59 @@ function _validateCreateOpts(opts) {
265
265
 
266
266
  // ---- Registry ----
267
267
 
268
+ /**
269
+ * @primitive b.permissions.create
270
+ * @signature b.permissions.create(opts)
271
+ * @since 0.4.9
272
+ * @status stable
273
+ * @compliance hipaa, pci-dss, gdpr, soc2
274
+ * @related b.permissions.match, b.apiKey.create, b.middleware.attachUser
275
+ *
276
+ * Build a permissions registry from a role table. Returns a handle
277
+ * exposing `require(scope, mwOpts?)` / `requireAll(scopes, mwOpts?)`
278
+ * / `requireAny(scopes, mwOpts?)` middleware factories,
279
+ * `check(actor, scope)` / `checkAll` / `checkAny` synchronous
280
+ * predicates, `policy(scope, predicate)` for ABAC layering,
281
+ * `expand(roleNames)` to resolve inherited permissions, and
282
+ * `dbRoleFor(reqOrActor)` for declarative DB role binding. Role
283
+ * tables are validated at create-time — unknown extends targets,
284
+ * cycles, and bad scope shapes throw before the first request.
285
+ *
286
+ * @opts
287
+ * roles: object, // role-name → spec (required, ≥1 entry)
288
+ * resolver: function, // req → actor; default reads req.apiKey / req.user
289
+ * audit: b.audit, // optional audit sink
290
+ * auditFailures: boolean, // default true
291
+ * auditSuccess: boolean, // default true (compliance trail)
292
+ * denyStatus: number, // default 403
293
+ * missingActorStatus: number, // default 401
294
+ * responder: function, // (req, res, status, info) — custom error responder
295
+ *
296
+ * @example
297
+ * var perms = b.permissions.create({
298
+ * roles: {
299
+ * admin: { extends: ["editor"], permissions: ["users:delete"] },
300
+ * editor: ["users:read", "users:write", "posts:*"],
301
+ * viewer: ["*:read"],
302
+ * },
303
+ * audit: b.audit,
304
+ * });
305
+ *
306
+ * // Synchronous check (e.g. inside a handler that already has the actor):
307
+ * perms.check({ roles: ["editor"] }, "posts:write"); // → true
308
+ * perms.check({ scopes: ["users:read"] }, "users:delete"); // → false
309
+ *
310
+ * // Middleware on a route — actor is read from req.user / req.apiKey:
311
+ * router.delete("/users/:id",
312
+ * attachUser,
313
+ * perms.require("users:delete", { requireMfa: true, mfaWindowMs: b.constants.TIME.minutes(5) }),
314
+ * deleteHandler);
315
+ *
316
+ * // ABAC predicate stacked on top of RBAC:
317
+ * perms.policy("orders:write", async function (actor, ctx) {
318
+ * return ctx.order.tenantId === actor.tenantId;
319
+ * });
320
+ */
268
321
  function create(opts) {
269
322
  opts = opts || {};
270
323
  validateOpts(opts, [
package/lib/pqc-agent.js CHANGED
@@ -31,7 +31,47 @@
31
31
  var https = require("node:https");
32
32
  var http = require("node:http");
33
33
  var C = require("./constants");
34
+ var lazyRequire = require("./lazy-require");
34
35
  var networkTls = require("./network-tls");
36
+ var safeBuffer = require("./safe-buffer");
37
+
38
+ // audit imports crypto/handlers transitively — lazy to avoid load
39
+ // cycles when pqc-agent is required during framework bootstrap.
40
+ var audit = lazyRequire(function () { return require("./audit"); });
41
+
42
+ // IANA TLS Supported Groups Registry — every named-group identifier
43
+ // the framework knows by name. Operators with `allowOperatorGroups:
44
+ // true` may pass any entry from this registry; entries outside it
45
+ // still throw (catches typos / smuggled-separator attempts).
46
+ var KNOWN_TLS_GROUPS = Object.freeze([
47
+ // PQC hybrids
48
+ "SecP384r1MLKEM1024", // draft-kwiatkowski-tls-ecdhe-mlkem 0x11ED
49
+ "X25519MLKEM768", // RFC 9794 0x11EC
50
+ "SecP256r1MLKEM768", // RFC 9794 0x11EB
51
+ // Classical groups (operator opt-in only)
52
+ "X25519",
53
+ "secp256r1", // allow:raw-byte-literal — IANA TLS group name (P-256), not bytes
54
+ "secp384r1", // allow:raw-byte-literal — IANA TLS group name (P-384), not bytes
55
+ "secp521r1", // allow:raw-byte-literal — IANA TLS group name (P-521), not bytes
56
+ "X448",
57
+ ]);
58
+
59
+ function _validateGroupName(name) {
60
+ // Same shape as network-tls._validateKeyShare: alphanumeric +
61
+ // underscore, bounded length. Refuses `:` so an operator can't
62
+ // smuggle a second group through one slot.
63
+ if (typeof name !== "string" || name.length === 0 || name.length > 64) { // allow:raw-byte-literal — string-length cap, not bytes
64
+ throw new TypeError(
65
+ "pqc-agent: ecdhCurve group entries must be non-empty strings up to 64 chars"
66
+ );
67
+ }
68
+ if (!safeBuffer.BASE64URL_RE.test(name)) {
69
+ throw new TypeError(
70
+ "pqc-agent: ecdhCurve group '" + name + "' has illegal characters " +
71
+ "(must match [A-Za-z0-9_-]+)"
72
+ );
73
+ }
74
+ }
35
75
 
36
76
  // Defaults for connection pooling. These ARE overridable via opts —
37
77
  // only the cryptographic posture (ecdhCurve / minVersion) is locked.
@@ -47,24 +87,59 @@ var DEFAULT_OPTS = {
47
87
 
48
88
  function _buildAgentOpts(opts) {
49
89
  opts = opts || {};
90
+ // allowOperatorGroups gates operator-supplied groups outside the
91
+ // framework PQC preference. Default false: caller may only narrow
92
+ // (subset of) TLS_GROUP_PREFERENCE. Set true and any KNOWN_TLS_GROUPS
93
+ // entry is permitted, including classical groups (operator's call
94
+ // — security-defaults posture says default no, opt-in yes). Each
95
+ // accepted operator group emits an audit event so the choice is
96
+ // visible in the audit log.
97
+ var allowOperatorGroups = opts.allowOperatorGroups === true;
50
98
  var merged = Object.assign({}, DEFAULT_OPTS, opts);
99
+ delete merged.allowOperatorGroups;
51
100
  // Caller may narrow the framework's curve preference list (drop a
52
101
  // 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.
102
+ // cannot widen it unless allowOperatorGroups: true is set. A
103
+ // caller-supplied `ecdhCurve` string is parsed into groups and each
104
+ // group is validated; the empty narrowing is a misconfig — TLS
105
+ // won't negotiate a key share — so reject too.
57
106
  if (typeof opts.ecdhCurve === "string" && opts.ecdhCurve.length > 0) {
58
107
  var requested = opts.ecdhCurve.split(":");
108
+ if (requested.length === 0) {
109
+ throw new TypeError(
110
+ "pqc-agent: opts.ecdhCurve must contain at least one group"
111
+ );
112
+ }
59
113
  for (var rgi = 0; rgi < requested.length; rgi++) {
60
- if (C.TLS_GROUP_PREFERENCE.indexOf(requested[rgi]) === -1) {
114
+ var group = requested[rgi];
115
+ _validateGroupName(group);
116
+ if (C.TLS_GROUP_PREFERENCE.indexOf(group) !== -1) continue;
117
+ if (!allowOperatorGroups) {
61
118
  throw new TypeError(
62
119
  "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."
120
+ group + "' which is not in the framework PQC-hybrid " +
121
+ "preference (" + C.TLS_GROUP_CURVE_STR + "); pass " +
122
+ "{ allowOperatorGroups: true } to accept operator-supplied " +
123
+ "groups, or construct an https.Agent directly."
124
+ );
125
+ }
126
+ if (KNOWN_TLS_GROUPS.indexOf(group) === -1) {
127
+ throw new TypeError(
128
+ "pqc-agent: opts.ecdhCurve group '" + group + "' is not a " +
129
+ "known IANA TLS Supported Group identifier"
66
130
  );
67
131
  }
132
+ // Operator-supplied group accepted — audit-emit so the
133
+ // framework-default deviation is visible to operators reading
134
+ // the audit log. safeEmit is drop-silent on error (audit bus
135
+ // failures must not break TLS agent construction).
136
+ try {
137
+ audit().safeEmit({
138
+ action: "pqcagent.operator_group.accepted",
139
+ outcome: "success",
140
+ metadata: { group: group, ecdhCurve: opts.ecdhCurve },
141
+ });
142
+ } catch (_e) { /* drop-silent — audit is best-effort here */ }
68
143
  }
69
144
  merged.ecdhCurve = requested.join(":");
70
145
  } else {
@@ -104,5 +179,6 @@ module.exports = {
104
179
  create: create,
105
180
  createHttp: createHttp,
106
181
  DEFAULT_OPTS: DEFAULT_OPTS,
182
+ KNOWN_TLS_GROUPS: KNOWN_TLS_GROUPS,
107
183
  enforced: true,
108
184
  };