@blamejs/core 0.8.43 → 0.8.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/pagination.js
CHANGED
|
@@ -1,98 +1,49 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.pagination
|
|
4
|
+
* @nav Tools
|
|
5
|
+
* @title Pagination
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
75
|
-
*
|
|
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") {
|
package/lib/parsers/index.js
CHANGED
|
@@ -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:
|
|
76
|
-
toml:
|
|
77
|
-
yaml:
|
|
78
|
-
env:
|
|
79
|
-
ini:
|
|
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
|
};
|
package/lib/permissions.js
CHANGED
|
@@ -1,48 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.permissions
|
|
3
|
+
* @module b.permissions
|
|
4
|
+
* @nav Identity
|
|
5
|
+
* @title Permissions
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
"preference (" + C.TLS_GROUP_CURVE_STR + ");
|
|
65
|
-
"
|
|
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
|
};
|