@blamejs/core 0.8.42 → 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.
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
@@ -0,0 +1,545 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.tenantQuota
4
+ * @nav Production
5
+ * @title Tenant Quota
6
+ *
7
+ * @intro
8
+ * Per-tenant rate / byte / row quotas with enforcement helpers and
9
+ * audit emission on breach. Multi-tenant deployments need three
10
+ * things the framework's DB layer doesn't natively provide:
11
+ *
12
+ * 1. Storage caps — refuse INSERT when a tenant has consumed
13
+ * more than its allowance (`defaultBytesCap`,
14
+ * or a `perTenantBytesCap[tenantId]` override).
15
+ * 2. Query budgets — refuse SELECT when a tenant exceeds its
16
+ * rolling-window QPS or rows-read totals.
17
+ * 3. Isolation — every row a query reads under a claimed
18
+ * tenantId MUST belong to that tenant.
19
+ * Cross-tenant rows surface as
20
+ * `db.tenant.crossover` audit events.
21
+ *
22
+ * Replaces the global `maxRowsPerQuery` knob for tenant-scoped
23
+ * scenarios — operators were previously forced to pick one global
24
+ * cap that would starve large tenants or under-cap small ones.
25
+ *
26
+ * Storage-cap accounting: `bytesUsed` is computed by walking every
27
+ * table whose schema declares the configured `tenantField` and
28
+ * summing the textual length of every column for matching rows.
29
+ * The framework caches the per-tenant total for `cacheTtlMs`
30
+ * (default 30s) so a hot path doesn't pay the scan on every assert.
31
+ *
32
+ * Query budget: sliding-window counter keyed `(tenantId, windowStart)`.
33
+ * Window defaults to 60s. `observe()` rejects when either the
34
+ * QPS-equivalent call count exceeds `perTenantQpsCap * window` or
35
+ * the rows-read total exceeds `perTenantTotalRowsRead`.
36
+ *
37
+ * Audit emissions:
38
+ * - `tenant.quota.exceeded` — `assert()` refused an insert/update
39
+ * - `tenant.budget.exceeded` — `observe()` refused a query
40
+ * - `db.tenant.crossover` — `instrumentQuery` saw rows belonging
41
+ * to the wrong tenant under the
42
+ * operator-claimed tenantId
43
+ *
44
+ * SOC 2 CC6.1 ("logical access controls") + ISO 27001 A.8.1.5
45
+ * ("classification of information") map directly onto this
46
+ * primitive — operators wire its emissions into the same audit
47
+ * chain auditors read.
48
+ *
49
+ * @card
50
+ * Per-tenant rate / byte / row quotas with enforcement helpers and audit emission on breach.
51
+ */
52
+
53
+ var C = require("./constants");
54
+ var lazyRequire = require("./lazy-require");
55
+ var validateOpts = require("./validate-opts");
56
+ var { defineClass } = require("./framework-error");
57
+
58
+ var TenantQuotaError = defineClass("TenantQuotaError", { alwaysPermanent: true });
59
+
60
+ var audit = lazyRequire(function () { return require("./audit"); });
61
+ var observability = lazyRequire(function () { return require("./observability"); });
62
+
63
+ var DEFAULT_CACHE_TTL_MS = C.TIME.seconds(30);
64
+ var DEFAULT_WINDOW_MS = C.TIME.minutes(1);
65
+ var DEFAULT_QPS_CAP = 100; // allow:raw-byte-literal — request count, not bytes
66
+ var DEFAULT_ROWS_READ = 50000; // allow:raw-byte-literal — row count, not bytes
67
+ var DEFAULT_BYTES_CAP = C.BYTES.gib(1);
68
+
69
+ // ---- Per-tenant storage cap (assert / snapshot / list) ----
70
+
71
+ /**
72
+ * @primitive b.tenantQuota.create
73
+ * @signature b.tenantQuota.create(opts)
74
+ * @since 0.7.0
75
+ * @compliance soc2, gdpr
76
+ * @related b.tenantQuota.budget, b.tenantQuota.instrumentQuery
77
+ *
78
+ * Build a per-tenant storage-cap enforcer. Returns an object exposing
79
+ * `assert(tenantId)` (throws `TenantQuotaError` on breach),
80
+ * `snapshot(tenantId)` (returns `{ tenantId, bytesUsed, bytesCap,
81
+ * percent }`), `list()` (snapshot every distinct tenant), and
82
+ * `invalidate(tenantId?)` (drop the per-tenant cache so the next
83
+ * assert recomputes). The cache TTL trades freshness for cost on
84
+ * the hot path; bump it down for stricter limits.
85
+ *
86
+ * @opts
87
+ * {
88
+ * db: object, // required, b.db namespace
89
+ * tenantField: string, // required, e.g. "tenantId"
90
+ * defaultBytesCap?: number, // default: 1 GiB (C.BYTES.gib(1))
91
+ * perTenantBytesCap?: { [tenantId: string]: number },
92
+ * tables?: string[], // override auto-detection
93
+ * audit?: boolean, // default: true
94
+ * cacheTtlMs?: number, // default: 30_000
95
+ * }
96
+ *
97
+ * @example
98
+ * var quota = b.tenantQuota.create({
99
+ * db: b.db,
100
+ * tenantField: "tenantId",
101
+ * defaultBytesCap: b.constants.BYTES.gib(1),
102
+ * perTenantBytesCap: { "tenant-vip": b.constants.BYTES.gib(10) },
103
+ * });
104
+ * await quota.assert("tenant-acme");
105
+ * // → { tenantId: "tenant-acme", bytesUsed: 12345, bytesCap: 1073741824, percent: 0.0000115 }
106
+ */
107
+ function create(opts) {
108
+ validateOpts.requireObject(opts, "tenantQuota.create", TenantQuotaError);
109
+ validateOpts(opts, [
110
+ "db", "tenantField", "defaultBytesCap", "perTenantBytesCap",
111
+ "tables", "audit", "cacheTtlMs",
112
+ ], "tenantQuota.create");
113
+
114
+ if (!opts.db || typeof opts.db.from !== "function" ||
115
+ typeof opts.db.getTableMetadata !== "function") {
116
+ throw new TenantQuotaError("tenantQuota/bad-db",
117
+ "tenantQuota.create: opts.db must be the framework's b.db namespace");
118
+ }
119
+ validateOpts.requireNonEmptyString(opts.tenantField,
120
+ "tenantQuota.create: tenantField", TenantQuotaError, "tenantQuota/bad-field");
121
+
122
+ var defaultBytesCap = (opts.defaultBytesCap == null)
123
+ ? DEFAULT_BYTES_CAP
124
+ : opts.defaultBytesCap;
125
+ if (typeof defaultBytesCap !== "number" || !isFinite(defaultBytesCap) || defaultBytesCap <= 0) {
126
+ throw new TenantQuotaError("tenantQuota/bad-cap",
127
+ "tenantQuota.create: defaultBytesCap must be a positive finite number");
128
+ }
129
+
130
+ var perTenantBytesCap = opts.perTenantBytesCap || {};
131
+ if (typeof perTenantBytesCap !== "object" || Array.isArray(perTenantBytesCap)) {
132
+ throw new TenantQuotaError("tenantQuota/bad-per-tenant",
133
+ "tenantQuota.create: perTenantBytesCap must be a plain object {tenantId: bytes}");
134
+ }
135
+ // Validate every per-tenant override at config time so a typo
136
+ // surfaces here rather than as a silent fall-through to default.
137
+ var ptKeys = Object.keys(perTenantBytesCap);
138
+ for (var pi = 0; pi < ptKeys.length; pi++) {
139
+ var v = perTenantBytesCap[ptKeys[pi]];
140
+ if (typeof v !== "number" || !isFinite(v) || v <= 0) {
141
+ throw new TenantQuotaError("tenantQuota/bad-per-tenant",
142
+ "tenantQuota.create: perTenantBytesCap['" + ptKeys[pi] +
143
+ "'] must be a positive finite number");
144
+ }
145
+ }
146
+
147
+ var auditOn = opts.audit !== false;
148
+ var cacheTtlMs = (opts.cacheTtlMs == null) ? DEFAULT_CACHE_TTL_MS : opts.cacheTtlMs;
149
+ if (typeof cacheTtlMs !== "number" || !isFinite(cacheTtlMs) || cacheTtlMs < 0) {
150
+ throw new TenantQuotaError("tenantQuota/bad-ttl",
151
+ "tenantQuota.create: cacheTtlMs must be a non-negative finite number");
152
+ }
153
+
154
+ var db = opts.db;
155
+ var tenantField = opts.tenantField;
156
+
157
+ // tables — operator-supplied table list (must include tenantField).
158
+ // When omitted we walk getTableMetadata() and pick every table whose
159
+ // schema declares the configured field.
160
+ var tablesOverride = Array.isArray(opts.tables) ? opts.tables.slice() : null;
161
+
162
+ function _resolveTables() {
163
+ if (tablesOverride) return tablesOverride;
164
+ var meta = db.getTableMetadata();
165
+ var out = [];
166
+ var keys = Object.keys(meta || {});
167
+ for (var i = 0; i < keys.length; i++) {
168
+ var t = meta[keys[i]];
169
+ if (t && t.columns && Object.prototype.hasOwnProperty.call(t.columns, tenantField)) {
170
+ out.push(keys[i]);
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+
176
+ // Per-tenant cached snapshot — { bytesUsed, takenAt }
177
+ var cache = new Map();
178
+
179
+ function _capFor(tenantId) {
180
+ if (Object.prototype.hasOwnProperty.call(perTenantBytesCap, tenantId)) {
181
+ return perTenantBytesCap[tenantId];
182
+ }
183
+ return defaultBytesCap;
184
+ }
185
+
186
+ function _emitAudit(action, outcome, metadata) {
187
+ if (!auditOn) return;
188
+ try {
189
+ audit().safeEmit({
190
+ action: action,
191
+ outcome: outcome,
192
+ metadata: metadata || {},
193
+ });
194
+ } catch (_e) { /* audit best-effort */ }
195
+ }
196
+
197
+ function _emitMetric(name, n) {
198
+ try { observability().safeEvent(name, n || 1, {}); }
199
+ catch (_e) { /* drop-silent */ }
200
+ }
201
+
202
+ async function _computeBytesUsed(tenantId) {
203
+ var tables = _resolveTables();
204
+ var total = 0;
205
+ for (var i = 0; i < tables.length; i++) {
206
+ // SUM(LENGTH(...)) across every column wins over a per-row
207
+ // serializer — SQLite computes it in one scan and the framework's
208
+ // sealed-column ciphertext is already on disk under this length.
209
+ // We sum the textual length of every column to approximate row
210
+ // bytes; a small under-count for INTEGER columns is acceptable
211
+ // when the cap is a soft limit operators raise long before
212
+ // hitting hard storage.
213
+ var rows = db.from(tables[i])
214
+ .where(tenantField, "=", tenantId)
215
+ .select(["*"])
216
+ .all();
217
+ for (var r = 0; r < rows.length; r++) {
218
+ var row = rows[r];
219
+ var keys = Object.keys(row);
220
+ for (var k = 0; k < keys.length; k++) {
221
+ var v = row[keys[k]];
222
+ if (v == null) continue;
223
+ if (Buffer.isBuffer(v)) total += v.length;
224
+ else total += String(v).length;
225
+ }
226
+ }
227
+ }
228
+ return total;
229
+ }
230
+
231
+ async function snapshot(tenantId) {
232
+ validateOpts.requireNonEmptyString(tenantId,
233
+ "tenantQuota.snapshot: tenantId", TenantQuotaError, "tenantQuota/bad-tenant");
234
+ var now = Date.now();
235
+ var cached = cache.get(tenantId);
236
+ var bytesUsed;
237
+ if (cached && (now - cached.takenAt) < cacheTtlMs) {
238
+ bytesUsed = cached.bytesUsed;
239
+ } else {
240
+ bytesUsed = await _computeBytesUsed(tenantId);
241
+ cache.set(tenantId, { bytesUsed: bytesUsed, takenAt: now });
242
+ }
243
+ var bytesCap = _capFor(tenantId);
244
+ return {
245
+ tenantId: tenantId,
246
+ bytesUsed: bytesUsed,
247
+ bytesCap: bytesCap,
248
+ percent: bytesCap === 0 ? 0 : bytesUsed / bytesCap,
249
+ };
250
+ }
251
+
252
+ async function assert(tenantId) {
253
+ var snap = await snapshot(tenantId);
254
+ if (snap.bytesUsed >= snap.bytesCap) {
255
+ _emitAudit("tenant.quota.exceeded", "denied", {
256
+ tenantId: tenantId,
257
+ bytesUsed: snap.bytesUsed,
258
+ bytesCap: snap.bytesCap,
259
+ });
260
+ _emitMetric("tenant.quota.exceeded", 1);
261
+ throw new TenantQuotaError("tenantQuota/exceeded",
262
+ "tenantQuota.assert: tenant '" + tenantId + "' is at " +
263
+ snap.bytesUsed + " of " + snap.bytesCap + " bytes; insert refused");
264
+ }
265
+ return snap;
266
+ }
267
+
268
+ async function list() {
269
+ // Walk every table, distinct tenantId values, and snapshot each.
270
+ var tables = _resolveTables();
271
+ var seen = Object.create(null);
272
+ for (var i = 0; i < tables.length; i++) {
273
+ var ids = db.from(tables[i])
274
+ .select([tenantField])
275
+ .all();
276
+ for (var j = 0; j < ids.length; j++) {
277
+ var v = ids[j] && ids[j][tenantField];
278
+ if (typeof v === "string" && v.length > 0) seen[v] = true;
279
+ }
280
+ }
281
+ var out = [];
282
+ var tenantIds = Object.keys(seen);
283
+ for (var t = 0; t < tenantIds.length; t++) {
284
+ out.push(await snapshot(tenantIds[t]));
285
+ }
286
+ return out;
287
+ }
288
+
289
+ function _invalidate(tenantId) {
290
+ if (tenantId === undefined) cache.clear();
291
+ else cache.delete(tenantId);
292
+ }
293
+
294
+ return {
295
+ assert: assert,
296
+ snapshot: snapshot,
297
+ list: list,
298
+ invalidate: _invalidate,
299
+ };
300
+ }
301
+
302
+ // ---- Per-tenant query budget (observe() — sliding window) ----
303
+
304
+ /**
305
+ * @primitive b.tenantQuota.budget
306
+ * @signature b.tenantQuota.budget(opts)
307
+ * @since 0.7.0
308
+ * @compliance soc2
309
+ * @related b.tenantQuota.create, b.tenantQuota.instrumentQuery
310
+ *
311
+ * Build a per-tenant query-budget enforcer. Returns an object exposing
312
+ * `observe(tenantId, info)` (throws `TenantQuotaError` on breach),
313
+ * `snapshot(tenantId)` (returns the current window's counters), and
314
+ * `reset(tenantId?)` (drop counters). Sliding-window: every breach
315
+ * past the configured QPS or rows-read total emits
316
+ * `tenant.budget.exceeded` and refuses the call.
317
+ *
318
+ * @opts
319
+ * {
320
+ * db: object, // required, b.db namespace
321
+ * tenantField: string, // required
322
+ * perTenantQpsCap?: number, // default: 100 calls/sec
323
+ * perTenantTotalRowsRead?: number, // default: 50_000 rows per window
324
+ * window?: number, // default: 60_000 ms (C.TIME.minutes(1))
325
+ * audit?: boolean, // default: true
326
+ * }
327
+ *
328
+ * @example
329
+ * var budget = b.tenantQuota.budget({
330
+ * db: b.db,
331
+ * tenantField: "tenantId",
332
+ * perTenantQpsCap: 100,
333
+ * perTenantTotalRowsRead: 50000,
334
+ * window: b.constants.TIME.minutes(1),
335
+ * });
336
+ * var snap = budget.observe("tenant-acme", { rowsRead: 12 });
337
+ * // → { calls: 1, rowsRead: 12, windowMs: 60000 }
338
+ */
339
+ function budget(opts) {
340
+ validateOpts.requireObject(opts, "tenantQuota.budget", TenantQuotaError);
341
+ validateOpts(opts, [
342
+ "db", "tenantField", "perTenantQpsCap", "perTenantTotalRowsRead",
343
+ "window", "audit",
344
+ ], "tenantQuota.budget");
345
+
346
+ validateOpts.requireNonEmptyString(opts.tenantField,
347
+ "tenantQuota.budget: tenantField", TenantQuotaError, "tenantQuota/bad-field");
348
+
349
+ var qpsCap = (opts.perTenantQpsCap == null) ? DEFAULT_QPS_CAP : opts.perTenantQpsCap;
350
+ if (typeof qpsCap !== "number" || !isFinite(qpsCap) || qpsCap <= 0) {
351
+ throw new TenantQuotaError("tenantQuota/bad-qps",
352
+ "tenantQuota.budget: perTenantQpsCap must be a positive finite number");
353
+ }
354
+ var rowsCap = (opts.perTenantTotalRowsRead == null) ? DEFAULT_ROWS_READ : opts.perTenantTotalRowsRead;
355
+ if (typeof rowsCap !== "number" || !isFinite(rowsCap) || rowsCap <= 0) {
356
+ throw new TenantQuotaError("tenantQuota/bad-rows",
357
+ "tenantQuota.budget: perTenantTotalRowsRead must be a positive finite number");
358
+ }
359
+ var windowMs = (opts.window == null) ? DEFAULT_WINDOW_MS : opts.window;
360
+ if (typeof windowMs !== "number" || !isFinite(windowMs) || windowMs <= 0) {
361
+ throw new TenantQuotaError("tenantQuota/bad-window",
362
+ "tenantQuota.budget: window must be a positive finite number");
363
+ }
364
+ var auditOn = opts.audit !== false;
365
+
366
+ // tenantId → { windowStart, calls, rowsRead }
367
+ var counters = new Map();
368
+
369
+ function _slot(tenantId, now) {
370
+ var c = counters.get(tenantId);
371
+ if (!c || (now - c.windowStart) >= windowMs) {
372
+ c = { windowStart: now, calls: 0, rowsRead: 0 };
373
+ counters.set(tenantId, c);
374
+ }
375
+ return c;
376
+ }
377
+
378
+ function _emitAudit(action, outcome, metadata) {
379
+ if (!auditOn) return;
380
+ try {
381
+ audit().safeEmit({
382
+ action: action,
383
+ outcome: outcome,
384
+ metadata: metadata || {},
385
+ });
386
+ } catch (_e) { /* audit best-effort */ }
387
+ }
388
+
389
+ function _emitMetric(name, n) {
390
+ try { observability().safeEvent(name, n || 1, {}); }
391
+ catch (_e) { /* drop-silent */ }
392
+ }
393
+
394
+ function observe(tenantId, info) {
395
+ validateOpts.requireNonEmptyString(tenantId,
396
+ "tenantQuota.budget.observe: tenantId", TenantQuotaError, "tenantQuota/bad-tenant");
397
+ info = info || {};
398
+ var rowsRead = (typeof info.rowsRead === "number" && info.rowsRead >= 0) ? info.rowsRead : 0;
399
+ var now = Date.now();
400
+ var c = _slot(tenantId, now);
401
+ c.calls += 1;
402
+ c.rowsRead += rowsRead;
403
+ var maxCalls = Math.max(1, Math.floor(qpsCap * (windowMs / C.TIME.seconds(1))));
404
+ if (c.calls > maxCalls || c.rowsRead > rowsCap) {
405
+ _emitAudit("tenant.budget.exceeded", "denied", {
406
+ tenantId: tenantId,
407
+ calls: c.calls,
408
+ rowsRead: c.rowsRead,
409
+ qpsCap: qpsCap,
410
+ rowsCap: rowsCap,
411
+ windowMs: windowMs,
412
+ });
413
+ _emitMetric("tenant.budget.exceeded", 1);
414
+ throw new TenantQuotaError("tenantQuota/budget-exceeded",
415
+ "tenantQuota.budget: tenant '" + tenantId + "' exceeded budget " +
416
+ "(calls=" + c.calls + "/" + maxCalls + ", rowsRead=" + c.rowsRead +
417
+ "/" + rowsCap + ", windowMs=" + windowMs + ")");
418
+ }
419
+ return { calls: c.calls, rowsRead: c.rowsRead, windowMs: windowMs };
420
+ }
421
+
422
+ function snapshot(tenantId) {
423
+ var now = Date.now();
424
+ var c = counters.get(tenantId);
425
+ if (!c || (now - c.windowStart) >= windowMs) {
426
+ return { tenantId: tenantId, calls: 0, rowsRead: 0, windowMs: windowMs };
427
+ }
428
+ return { tenantId: tenantId, calls: c.calls, rowsRead: c.rowsRead, windowMs: windowMs };
429
+ }
430
+
431
+ function reset(tenantId) {
432
+ if (tenantId === undefined) counters.clear();
433
+ else counters.delete(tenantId);
434
+ }
435
+
436
+ return {
437
+ observe: observe,
438
+ snapshot: snapshot,
439
+ reset: reset,
440
+ };
441
+ }
442
+
443
+ // ---- Tenant-isolation breach detection (instrumentQuery) ----
444
+ //
445
+ // instrumentQuery wraps a result set + the operator-claimed tenantId
446
+ // and emits db.tenant.crossover when any row's tenantField value
447
+ // disagrees with the claim. Used by the framework's _readQuery /
448
+ // query primitives at the seam where a query result lands.
449
+
450
+ /**
451
+ * @primitive b.tenantQuota.instrumentQuery
452
+ * @signature b.tenantQuota.instrumentQuery(opts)
453
+ * @since 0.7.0
454
+ * @compliance soc2, gdpr
455
+ * @related b.tenantQuota.create, b.tenantQuota.budget
456
+ *
457
+ * Walk a result set and detect rows whose `tenantField` value
458
+ * disagrees with the operator-claimed `tenantId` — a multi-tenant
459
+ * isolation breach. Returns `{ ok, crossover }` where `crossover` is
460
+ * the list of offending row indexes + their actual tenantId values.
461
+ * Audit emission `db.tenant.crossover` fires with a five-row sample
462
+ * when any breach is detected so the framework's chain-signed audit
463
+ * carries the forensic trail without dumping the whole result set.
464
+ *
465
+ * @opts
466
+ * {
467
+ * rows: object[], // required, the query result rows
468
+ * tenantField: string, // required, e.g. "tenantId"
469
+ * tenantId: string, // required, the operator-claimed tenant
470
+ * table?: string, // optional, recorded in the audit metadata
471
+ * audit?: boolean, // default: true
472
+ * }
473
+ *
474
+ * @example
475
+ * var rows = [
476
+ * { _id: 1, tenantId: "tenant-acme", name: "ok" },
477
+ * { _id: 2, tenantId: "tenant-other", name: "leak" },
478
+ * ];
479
+ * var result = b.tenantQuota.instrumentQuery({
480
+ * rows: rows,
481
+ * tenantField: "tenantId",
482
+ * tenantId: "tenant-acme",
483
+ * table: "orders",
484
+ * });
485
+ * // → { ok: false, crossover: [{ index: 1, actualTenantId: "tenant-other" }] }
486
+ */
487
+ function instrumentQuery(opts) {
488
+ if (!opts || typeof opts !== "object") {
489
+ throw new TenantQuotaError("tenantQuota/bad-instr",
490
+ "tenantQuota.instrumentQuery: opts object is required");
491
+ }
492
+ validateOpts(opts, [
493
+ "rows", "tenantField", "tenantId", "audit", "table",
494
+ ], "tenantQuota.instrumentQuery");
495
+
496
+ if (!Array.isArray(opts.rows)) {
497
+ throw new TenantQuotaError("tenantQuota/bad-rows",
498
+ "tenantQuota.instrumentQuery: rows must be an array");
499
+ }
500
+ validateOpts.requireNonEmptyString(opts.tenantField,
501
+ "tenantQuota.instrumentQuery: tenantField", TenantQuotaError, "tenantQuota/bad-field");
502
+ validateOpts.requireNonEmptyString(opts.tenantId,
503
+ "tenantQuota.instrumentQuery: tenantId", TenantQuotaError, "tenantQuota/bad-tenant");
504
+ var auditOn = opts.audit !== false;
505
+
506
+ var crossover = [];
507
+ for (var i = 0; i < opts.rows.length; i++) {
508
+ var row = opts.rows[i];
509
+ if (!row || typeof row !== "object") continue;
510
+ var actual = row[opts.tenantField];
511
+ if (actual !== undefined && actual !== null && actual !== opts.tenantId) {
512
+ crossover.push({ index: i, actualTenantId: String(actual) });
513
+ }
514
+ }
515
+ if (crossover.length > 0) {
516
+ if (auditOn) {
517
+ try {
518
+ audit().safeEmit({
519
+ action: "db.tenant.crossover",
520
+ outcome: "failure",
521
+ metadata: {
522
+ tenantField: opts.tenantField,
523
+ claimedTenant: opts.tenantId,
524
+ table: opts.table || null,
525
+ rowCount: crossover.length,
526
+ sample: crossover.slice(0, 5), // allow:raw-byte-literal — sample size, not bytes
527
+ },
528
+ });
529
+ } catch (_e) { /* audit best-effort */ }
530
+ }
531
+ try { observability().safeEvent("db.tenant.crossover", crossover.length, {}); }
532
+ catch (_e) { /* drop-silent */ }
533
+ }
534
+ return {
535
+ ok: crossover.length === 0,
536
+ crossover: crossover,
537
+ };
538
+ }
539
+
540
+ module.exports = {
541
+ create: create,
542
+ budget: budget,
543
+ instrumentQuery: instrumentQuery,
544
+ TenantQuotaError: TenantQuotaError,
545
+ };