@blamejs/blamejs-shop 0.0.122 → 0.0.124

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.12.28",
4
- "createdAt": "2026-05-24T15:47:42.211Z",
3
+ "frameworkVersion": "0.12.29",
4
+ "createdAt": "2026-05-24T17:07:11.597Z",
5
5
  "exports": {
6
6
  "a2a": {
7
7
  "type": "object",
@@ -1548,6 +1548,31 @@
1548
1548
  }
1549
1549
  }
1550
1550
  },
1551
+ "dp": {
1552
+ "type": "object",
1553
+ "members": {
1554
+ "ACCOUNTINGS": {
1555
+ "type": "instance",
1556
+ "ctorName": "Array"
1557
+ },
1558
+ "AiDpError": {
1559
+ "type": "function",
1560
+ "arity": 4
1561
+ },
1562
+ "MECHANISMS": {
1563
+ "type": "instance",
1564
+ "ctorName": "Array"
1565
+ },
1566
+ "budget": {
1567
+ "type": "function",
1568
+ "arity": 1
1569
+ },
1570
+ "mechanism": {
1571
+ "type": "function",
1572
+ "arity": 1
1573
+ }
1574
+ }
1575
+ },
1551
1576
  "input": {
1552
1577
  "type": "object",
1553
1578
  "members": {
@@ -445,6 +445,7 @@ module.exports = {
445
445
  disclosure: require("./lib/ai-disclosure"),
446
446
  quota: require("./lib/ai-quota"),
447
447
  capability: require("./lib/ai-capability"),
448
+ dp: require("./lib/ai-dp"),
448
449
  },
449
450
  promisePool: require("./lib/promise-pool"),
450
451
  sdNotify: require("./lib/sd-notify"),
@@ -0,0 +1,539 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.ai.dp
4
+ * @nav Compliance
5
+ * @title Differential privacy
6
+ *
7
+ * @intro
8
+ * Float-safe differential-privacy mechanisms with per-scope privacy
9
+ * budgeting. Differential privacy adds calibrated noise to an
10
+ * aggregate so the output is provably insensitive to any single
11
+ * record — but the guarantee is fragile: Mironov (2012) showed that
12
+ * a Laplace mechanism implemented with naive double-precision
13
+ * sampling lets an attacker distinguish neighbouring datasets with
14
+ * &gt; 35% probability from a <em>single</em> output, silently
15
+ * destroying the promise. This module ships only mechanisms whose
16
+ * sampling is hardened against that class of attack:
17
+ *
18
+ * - <strong>Laplace via the snapping mechanism</strong> (Mironov
19
+ * 2012): clamp to a bound, draw a CSPRNG sign + full-mantissa
20
+ * uniform, then round to a power-of-two grid — the rounding
21
+ * removes the exploitable low-order mantissa bits. Pure
22
+ * ε-differential privacy.
23
+ * - <strong>Discrete Gaussian</strong> (Canonne–Kamath–Steinke
24
+ * 2020): integer-exact rejection sampling built from
25
+ * Bernoulli(exp(−γ)) over exact rationals — no floating-point
26
+ * noise at all. (ε, δ)-differential privacy, integer-valued.
27
+ *
28
+ * All randomness comes from <code>b.crypto.generateBytes</code>
29
+ * (SHAKE256 over the OS CSPRNG), never <code>Math.random</code>.
30
+ *
31
+ * <code>b.ai.dp.budget({ scope, epsilon, delta })</code> tracks a
32
+ * privacy budget per scope (per-user / per-tenant / per-query-class)
33
+ * and refuses a <code>consume</code> that would exceed it.
34
+ * Composition is accounted two ways:
35
+ *
36
+ * - <code>"basic"</code> (default) — sum the per-release ε and δ.
37
+ * Always valid; conservative.
38
+ * - <code>"rdp"</code> — a Rényi DP accountant (Mironov 2017) tracks
39
+ * RDP across a grid of orders and converts to (ε, δ) at the
40
+ * scope's δ, giving a much tighter bound under repeated Gaussian
41
+ * releases. Requires <code>delta &gt; 0</code>.
42
+ *
43
+ * NIST SP 800-226 (2025) is the evaluation standard for these
44
+ * guarantees; Dwork &amp; Roth, "The Algorithmic Foundations of
45
+ * Differential Privacy", is the canonical reference.
46
+ *
47
+ * The exponential and sparse-vector mechanisms are
48
+ * deferred-with-condition: their float-safe constructions (the
49
+ * base-2 / permute-and-flip exponential mechanism, Ilvento 2019; a
50
+ * snapped sparse-vector) are a distinct effort, and shipping them
51
+ * float-<em>unsafe</em> would defeat the module's purpose. They
52
+ * re-open on operator demand with the named construction.
53
+ *
54
+ * @card
55
+ * Float-safe differential privacy — snapping-mechanism Laplace
56
+ * (Mironov 2012) + discrete Gaussian (CKS20), CSPRNG noise, per-
57
+ * scope ε/δ budgets with basic + Rényi-DP accounting.
58
+ */
59
+
60
+ var bCrypto = require("./crypto");
61
+ var validateOpts = require("./validate-opts");
62
+ var lazyRequire = require("./lazy-require");
63
+ var { defineClass } = require("./framework-error");
64
+
65
+ var AiDpError = defineClass("AiDpError", { alwaysPermanent: true });
66
+
67
+ var audit = lazyRequire(function () { return require("./audit"); });
68
+
69
+ var MECHANISMS = ["laplace", "gaussian"];
70
+ var ACCOUNTINGS = ["basic", "rdp"];
71
+
72
+ // Rational approximation precision for a real-valued σ² fed to the
73
+ // integer-exact discrete-Gaussian sampler. 2^32 keeps the deviation
74
+ // from the target σ² below 2^-32 — far under the noise scale — while
75
+ // keeping the BigInt denominators bounded.
76
+ var SIGMA2_RATIONAL_DEN = 4294967296; // allow:raw-byte-literal — 2^32 rational-approx denominator, not a byte size
77
+
78
+ // ---- Minimal exact rational (BigInt num / den, den > 0) ----
79
+
80
+ function _gcd(a, b) {
81
+ a = a < 0n ? -a : a;
82
+ b = b < 0n ? -b : b;
83
+ while (b) { var t = a % b; a = b; b = t; }
84
+ return a;
85
+ }
86
+ function _fr(num, den) {
87
+ if (den < 0n) { num = -num; den = -den; }
88
+ var g = _gcd(num, den) || 1n;
89
+ return { num: num / g, den: den / g };
90
+ }
91
+ function _frFromFloat(x, den) {
92
+ // den is a Number power-of-two-ish denominator; round(x*den)/den.
93
+ return _fr(BigInt(Math.round(x * den)), BigInt(den));
94
+ }
95
+ function _frMul(a, b) { return _fr(a.num * b.num, a.den * b.den); }
96
+ function _frSub(a, b) { return _fr(a.num * b.den - b.num * a.den, a.den * b.den); }
97
+ function _frLte(a, b) { return a.num * b.den <= b.num * a.den; } // a <= b
98
+ function _frGt(a, b) { return a.num * b.den > b.num * a.den; } // a > b
99
+
100
+ // ---- CSPRNG primitives (all noise routes through b.crypto) ----
101
+
102
+ // Uniform BigInt in [0, m) via rejection sampling on CSPRNG bytes —
103
+ // no modulo bias.
104
+ function _uniformBelow(m) {
105
+ if (m <= 0n) throw new AiDpError("aiDp/internal", "ai.dp: _uniformBelow needs m > 0");
106
+ if (m === 1n) return 0n;
107
+ var bits = m.toString(2).length;
108
+ var bytes = Math.ceil(bits / 8); // allow:raw-byte-literal — bits-per-byte divisor, not a size
109
+ var mask = (1n << BigInt(bits)) - 1n;
110
+ for (;;) {
111
+ var buf = bCrypto.generateBytes(bytes);
112
+ var x = 0n;
113
+ for (var i = 0; i < bytes; i++) x = (x << 8n) | BigInt(buf[i]);
114
+ x = x & mask;
115
+ if (x < m) return x;
116
+ }
117
+ }
118
+
119
+ // Uniform double in (0, 1] with full 53-bit mantissa entropy — the
120
+ // snapping mechanism's noise source. A 53-bit integer is drawn via
121
+ // the BigInt rejection sampler (accumulating 53 bits in a JS Number
122
+ // would overflow the 2^53 safe-integer range and skew the draw), then
123
+ // mapped (val + 1) / 2^53 → (0, 1].
124
+ var TWO_POW_53 = 9007199254740992; // allow:raw-byte-literal — 2^53 mantissa range, not a byte size
125
+ function _uniformOpen() {
126
+ var v = Number(_uniformBelow(9007199254740992n)); // [0, 2^53) exact
127
+ return (v + 1) / TWO_POW_53; // (0, 1]
128
+ }
129
+
130
+ function _randomSign() {
131
+ return (bCrypto.generateBytes(1)[0] & 1) === 1 ? 1 : -1;
132
+ }
133
+
134
+ // ---- Canonne–Kamath–Steinke 2020 integer-exact samplers ----
135
+ // Ported verbatim from the reference implementation
136
+ // (github.com/IBM/discrete-gaussian-differential-privacy). All
137
+ // arithmetic is exact (BigInt rationals); no floating-point noise.
138
+
139
+ function _bernoulli(p) { // p rational in [0,1]
140
+ return _uniformBelow(p.den) < p.num ? 1 : 0;
141
+ }
142
+ function _bernoulliExp1(x) { // x rational in [0,1]
143
+ var k = 1n;
144
+ for (;;) {
145
+ if (_bernoulli(_fr(x.num, x.den * k)) === 1) k = k + 1n;
146
+ else break;
147
+ }
148
+ return Number(k % 2n);
149
+ }
150
+ function _bernoulliExp(x) { // x rational >= 0
151
+ while (_frGt(x, _fr(1n, 1n))) {
152
+ if (_bernoulliExp1(_fr(1n, 1n)) === 1) x = _frSub(x, _fr(1n, 1n));
153
+ else return 0;
154
+ }
155
+ return _bernoulliExp1(x);
156
+ }
157
+ function _geometricExpSlow(x) { // x rational >= 0
158
+ var k = 0n;
159
+ for (;;) {
160
+ if (_bernoulliExp(x) === 1) k = k + 1n;
161
+ else return k;
162
+ }
163
+ }
164
+ function _geometricExpFast(x) { // x rational > 0; returns BigInt
165
+ if (x.num === 0n) return 0n;
166
+ var t = x.den;
167
+ var u;
168
+ for (;;) {
169
+ u = _uniformBelow(t);
170
+ if (_bernoulliExp(_fr(u, t)) === 1) break;
171
+ }
172
+ var v = _geometricExpSlow(_fr(1n, 1n));
173
+ var value = v * t + u;
174
+ return value / x.num; // integer division
175
+ }
176
+ function _sampleDLaplace(scaleNum, scaleDen) { // Lap_Z(scale); returns BigInt
177
+ var invScale = _fr(scaleDen, scaleNum); // 1 / scale
178
+ for (;;) {
179
+ var sign = _bernoulli(_fr(1n, 2n));
180
+ var magnitude = _geometricExpFast(invScale);
181
+ if (sign === 1 && magnitude === 0n) continue;
182
+ return magnitude * BigInt(1 - 2 * sign);
183
+ }
184
+ }
185
+ function _floorSqrtFrac(fr) { // floor(sqrt(rational)); returns BigInt
186
+ var num = fr.num, den = fr.den;
187
+ var a = 0n, b = 1n;
188
+ while (b * b * den <= num) b = 2n * b;
189
+ while (a + 1n < b) {
190
+ var c = (a + b) / 2n;
191
+ if (c * c * den <= num) a = c; else b = c;
192
+ }
193
+ return a;
194
+ }
195
+ function _sampleDGauss(sigma2) { // sigma2 rational > 0; returns BigInt
196
+ var t = _floorSqrtFrac(sigma2) + 1n;
197
+ var two_sigma2 = _fr(2n * sigma2.num, sigma2.den); // 2 * sigma2
198
+ var sigma2_over_t = _fr(sigma2.num, sigma2.den * t); // sigma2 / t
199
+ for (;;) {
200
+ var candidate = _sampleDLaplace(t, 1n);
201
+ var absC = candidate < 0n ? -candidate : candidate;
202
+ var diff = _frSub(_fr(absC, 1n), sigma2_over_t); // |candidate| - sigma2/t
203
+ // bias = diff^2 / (2 sigma2) — multiply diff^2 by the reciprocal of 2σ².
204
+ var diff2 = _fr(diff.num * diff.num, diff.den * diff.den);
205
+ var bias = _frMul(diff2, _fr(two_sigma2.den, two_sigma2.num));
206
+ if (_bernoulliExp(bias) === 1) return candidate;
207
+ }
208
+ }
209
+
210
+ // ---- Snapping-mechanism Laplace (Mironov 2012), float-safe ----
211
+
212
+ function _clamp(x, bound) {
213
+ if (x < -bound) return -bound;
214
+ if (x > bound) return bound;
215
+ return x;
216
+ }
217
+ function _snappingLaplace(value, scale, bound) {
218
+ // scale = sensitivity / epsilon (Laplace b). bound B clamps the
219
+ // input + output; the privacy guarantee depends on it. Lambda is
220
+ // the smallest power of two >= scale, so inner / Lambda and
221
+ // Lambda * round(...) are exact float ops — that is what removes
222
+ // the attackable low-order bits the naive sampler leaks.
223
+ var xc = _clamp(value, bound);
224
+ var S = _randomSign();
225
+ var U = _uniformOpen(); // (0, 1]
226
+ var lambdaPow = Math.pow(2, Math.ceil(Math.log2(scale)));
227
+ var inner = xc + S * scale * Math.log(U);
228
+ var rounded = lambdaPow * Math.round(inner / lambdaPow);
229
+ return _clamp(rounded, bound);
230
+ }
231
+
232
+ // ---- Rényi-DP costs (Mironov 2017) ----
233
+
234
+ var RDP_ORDERS = [1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5, 6, 8, 12, 16, 24, 32, 48, 64, 128, 256]; // allow:raw-byte-literal — Rényi DP orders (α), not byte sizes
235
+
236
+ // Gaussian mechanism with noise-to-sensitivity z = sigma / sensitivity:
237
+ // RDP(alpha) = alpha / (2 z^2).
238
+ function _rdpGaussian(alpha, sigma, sensitivity) {
239
+ var z = sigma / sensitivity;
240
+ return alpha / (2 * z * z);
241
+ }
242
+ // Laplace mechanism with pure-DP parameter eps0 (= sensitivity / scale):
243
+ // RDP(alpha) = (1/(alpha-1)) * ln( (alpha/(2alpha-1)) e^{(alpha-1)eps0}
244
+ // + ((alpha-1)/(2alpha-1)) e^{-alpha eps0} ).
245
+ function _rdpLaplace(alpha, eps0) {
246
+ var a = alpha;
247
+ var num1 = a / (2 * a - 1);
248
+ var num2 = (a - 1) / (2 * a - 1);
249
+ var term = num1 * Math.exp((a - 1) * eps0) + num2 * Math.exp(-a * eps0);
250
+ return Math.log(term) / (a - 1);
251
+ }
252
+ // Convert an RDP curve (rdp[order]) to (eps, delta): the standard
253
+ // RDP -> DP bound eps(delta) = min_alpha ( rdp(alpha) + ln(1/delta)/(alpha-1) ).
254
+ function _rdpToEpsilon(rdpByOrder, delta) {
255
+ var best = Infinity;
256
+ for (var i = 0; i < RDP_ORDERS.length; i++) {
257
+ var a = RDP_ORDERS[i];
258
+ var e = rdpByOrder[i] + Math.log(1 / delta) / (a - 1);
259
+ if (e < best) best = e;
260
+ }
261
+ return best;
262
+ }
263
+
264
+ // ---- mechanism descriptor ----
265
+
266
+ /**
267
+ * @primitive b.ai.dp.mechanism
268
+ * @signature b.ai.dp.mechanism(opts)
269
+ * @since 0.12.29
270
+ * @status stable
271
+ * @compliance gdpr, soc2
272
+ * @related b.ai.dp.budget, b.ai.quota.create
273
+ *
274
+ * Build a float-safe DP noise mechanism. <code>type: "laplace"</code>
275
+ * is the snapping mechanism (pure ε-DP, real-valued, needs a
276
+ * <code>bound</code>); <code>type: "gaussian"</code> is the discrete
277
+ * Gaussian (integer-valued, (ε, δ)-DP, needs <code>delta</code>).
278
+ * Pass the result to <code>budget.consume(mechanism, value)</code>.
279
+ *
280
+ * @opts
281
+ * {
282
+ * type: string, // "laplace" | "gaussian"
283
+ * sensitivity: number, // required, > 0 (L1 for laplace, L1/integer for gaussian)
284
+ * epsilon: number, // required, > 0 (per-release ε; ε ≤ 1 for the
285
+ * // classic Gaussian calibration)
286
+ * delta?: number, // gaussian only, required, 0 < δ < 1
287
+ * bound?: number, // laplace only, required, > 0 — clamp bound B
288
+ * }
289
+ *
290
+ * @example
291
+ * var lap = b.ai.dp.mechanism({ type: "laplace", sensitivity: 1, epsilon: 0.5, bound: 1000 });
292
+ * var gss = b.ai.dp.mechanism({ type: "gaussian", sensitivity: 1, epsilon: 0.5, delta: 1e-6 });
293
+ */
294
+ function mechanism(opts) {
295
+ validateOpts.requireObject(opts, "ai.dp.mechanism", AiDpError);
296
+ validateOpts(opts, ["type", "sensitivity", "epsilon", "delta", "bound"], "ai.dp.mechanism");
297
+
298
+ if (MECHANISMS.indexOf(opts.type) === -1) {
299
+ throw new AiDpError("aiDp/bad-mechanism",
300
+ "ai.dp.mechanism: type must be one of " + MECHANISMS.join(" / ") +
301
+ " (exponential / sparse-vector are deferred — their float-safe constructions " +
302
+ "re-open on demand)");
303
+ }
304
+ if (typeof opts.sensitivity !== "number" || !isFinite(opts.sensitivity) || opts.sensitivity <= 0) {
305
+ throw new AiDpError("aiDp/bad-sensitivity",
306
+ "ai.dp.mechanism: sensitivity must be a positive finite number");
307
+ }
308
+ if (typeof opts.epsilon !== "number" || !isFinite(opts.epsilon) || opts.epsilon <= 0) {
309
+ throw new AiDpError("aiDp/bad-epsilon",
310
+ "ai.dp.mechanism: epsilon must be a positive finite number");
311
+ }
312
+
313
+ if (opts.type === "laplace") {
314
+ if (typeof opts.bound !== "number" || !isFinite(opts.bound) || opts.bound <= 0) {
315
+ throw new AiDpError("aiDp/bad-bound",
316
+ "ai.dp.mechanism: laplace requires bound > 0 (the snapping clamp; the " +
317
+ "privacy guarantee depends on it)");
318
+ }
319
+ var scale = opts.sensitivity / opts.epsilon;
320
+ return Object.freeze({
321
+ type: "laplace", sensitivity: opts.sensitivity, epsilon: opts.epsilon,
322
+ delta: 0, scale: scale, bound: opts.bound,
323
+ });
324
+ }
325
+
326
+ // gaussian
327
+ if (typeof opts.delta !== "number" || !isFinite(opts.delta) || opts.delta <= 0 || opts.delta >= 1) {
328
+ throw new AiDpError("aiDp/bad-delta",
329
+ "ai.dp.mechanism: gaussian requires 0 < delta < 1");
330
+ }
331
+ if (opts.epsilon > 1) {
332
+ throw new AiDpError("aiDp/epsilon-too-large",
333
+ "ai.dp.mechanism: the classic Gaussian calibration is proven for epsilon <= 1; " +
334
+ "split into multiple releases under an rdp budget, or the analytic Gaussian " +
335
+ "mechanism (Balle-Wang 2018) re-opens this path on demand");
336
+ }
337
+ // Classic Gaussian calibration (Dwork & Roth Thm 3.22), valid for ε ≤ 1.
338
+ var sigma = Math.sqrt(2 * Math.log(1.25 / opts.delta)) * opts.sensitivity / opts.epsilon;
339
+ return Object.freeze({
340
+ type: "gaussian", sensitivity: opts.sensitivity, epsilon: opts.epsilon,
341
+ delta: opts.delta, sigma: sigma, sigma2: sigma * sigma,
342
+ });
343
+ }
344
+
345
+ // Apply a mechanism's noise to a numeric value (no accounting — the
346
+ // budget wraps this).
347
+ function _applyMechanism(m, value) {
348
+ if (typeof value !== "number" || !isFinite(value)) {
349
+ throw new AiDpError("aiDp/bad-value", "ai.dp: value must be a finite number");
350
+ }
351
+ if (m.type === "laplace") {
352
+ return _snappingLaplace(value, m.scale, m.bound);
353
+ }
354
+ // gaussian — discrete, integer noise added to the (rounded) value.
355
+ var sigma2Frac = _frFromFloat(m.sigma2, SIGMA2_RATIONAL_DEN);
356
+ var noise = _sampleDGauss(sigma2Frac);
357
+ return Math.round(value) + Number(noise);
358
+ }
359
+
360
+ function _mechRdp(m, orderIndex) {
361
+ var alpha = RDP_ORDERS[orderIndex];
362
+ if (m.type === "gaussian") return _rdpGaussian(alpha, m.sigma, m.sensitivity);
363
+ return _rdpLaplace(alpha, m.epsilon);
364
+ }
365
+
366
+ // ---- per-scope budget ----
367
+
368
+ /**
369
+ * @primitive b.ai.dp.budget
370
+ * @signature b.ai.dp.budget(opts)
371
+ * @since 0.12.29
372
+ * @status stable
373
+ * @compliance gdpr, soc2
374
+ * @related b.ai.dp.mechanism, b.ai.quota.create
375
+ *
376
+ * Track a differential-privacy budget for one scope (per-user /
377
+ * per-tenant / per-query-class) and refuse a release that would
378
+ * exceed it. Returns <code>{ consume, remaining, spent, reset }</code>.
379
+ * <code>consume(mechanism, value)</code> adds the mechanism's noise,
380
+ * charges the accountant, and throws <code>aiDp/budget-exhausted</code>
381
+ * if the release would push the scope past its (ε, δ). With
382
+ * <code>accounting: "rdp"</code> the charge is accounted via Rényi DP
383
+ * for a tight composition bound (requires <code>delta &gt; 0</code>);
384
+ * <code>"basic"</code> (default) sums per-release ε and δ.
385
+ *
386
+ * @opts
387
+ * {
388
+ * scope: string, // required, the budget scope id
389
+ * epsilon: number, // required, total ε budget (> 0)
390
+ * delta?: number, // total δ budget (>= 0; required > 0 for rdp / gaussian)
391
+ * accounting?: string, // "basic" (default) | "rdp"
392
+ * audit?: boolean, // default: true
393
+ * }
394
+ *
395
+ * @example
396
+ * var b1 = b.ai.dp.budget({ scope: "tenant-acme:daily", epsilon: 3, delta: 1e-6, accounting: "rdp" });
397
+ * var m = b.ai.dp.mechanism({ type: "gaussian", sensitivity: 1, epsilon: 0.5, delta: 1e-6 });
398
+ * var out = b1.consume(m, trueCount);
399
+ * // → { value: <noised>, cost: { epsilon: 0.5, delta: 1e-6 }, remaining: { epsilon, delta } }
400
+ */
401
+ function budget(opts) {
402
+ validateOpts.requireObject(opts, "ai.dp.budget", AiDpError);
403
+ validateOpts(opts, ["scope", "epsilon", "delta", "accounting", "audit"], "ai.dp.budget");
404
+
405
+ validateOpts.requireNonEmptyString(opts.scope,
406
+ "ai.dp.budget: scope", AiDpError, "aiDp/bad-scope");
407
+ if (typeof opts.epsilon !== "number" || !isFinite(opts.epsilon) || opts.epsilon <= 0) {
408
+ throw new AiDpError("aiDp/bad-epsilon", "ai.dp.budget: epsilon must be a positive finite number");
409
+ }
410
+ var totalEpsilon = opts.epsilon;
411
+ var totalDelta = (opts.delta == null) ? 0 : opts.delta;
412
+ if (typeof totalDelta !== "number" || !isFinite(totalDelta) || totalDelta < 0 || totalDelta >= 1) {
413
+ throw new AiDpError("aiDp/bad-delta", "ai.dp.budget: delta must be in [0, 1)");
414
+ }
415
+ var accounting = (opts.accounting == null) ? "basic" : opts.accounting;
416
+ if (ACCOUNTINGS.indexOf(accounting) === -1) {
417
+ throw new AiDpError("aiDp/bad-accounting",
418
+ "ai.dp.budget: accounting must be one of " + ACCOUNTINGS.join(" / "));
419
+ }
420
+ if (accounting === "rdp" && totalDelta <= 0) {
421
+ throw new AiDpError("aiDp/bad-accounting",
422
+ "ai.dp.budget: rdp accounting requires delta > 0 (the RDP→(ε,δ) conversion is " +
423
+ "undefined at delta = 0; use basic accounting for pure-ε budgets)");
424
+ }
425
+ var auditOn = opts.audit !== false;
426
+
427
+ var scope = opts.scope;
428
+ var spentEpsilon = 0; // basic accounting
429
+ var spentDelta = 0;
430
+ var rdp = RDP_ORDERS.map(function () { return 0; }); // rdp accounting
431
+
432
+ function _emitAudit(action, outcome, metadata) {
433
+ if (!auditOn) return;
434
+ try { audit().safeEmit({ action: action, outcome: outcome, metadata: metadata || {} }); }
435
+ catch (_e) { /* drop-silent */ }
436
+ }
437
+
438
+ function _currentEpsilon(rdpCurve) {
439
+ if (accounting === "basic") return spentEpsilon;
440
+ return _rdpToEpsilon(rdpCurve, totalDelta);
441
+ }
442
+
443
+ function remaining() {
444
+ if (accounting === "basic") {
445
+ return {
446
+ epsilon: Math.max(0, totalEpsilon - spentEpsilon),
447
+ delta: Math.max(0, totalDelta - spentDelta),
448
+ };
449
+ }
450
+ return { epsilon: Math.max(0, totalEpsilon - _rdpToEpsilon(rdp, totalDelta)), delta: totalDelta };
451
+ }
452
+
453
+ function spent() {
454
+ if (accounting === "basic") return { epsilon: spentEpsilon, delta: spentDelta };
455
+ return { epsilon: _rdpToEpsilon(rdp, totalDelta), delta: totalDelta };
456
+ }
457
+
458
+ function consume(m, value) {
459
+ if (!m || typeof m !== "object" || MECHANISMS.indexOf(m.type) === -1) {
460
+ throw new AiDpError("aiDp/bad-mechanism",
461
+ "ai.dp.budget.consume: first argument must be a b.ai.dp.mechanism");
462
+ }
463
+ if (m.type === "gaussian" && totalDelta <= 0) {
464
+ throw new AiDpError("aiDp/bad-delta",
465
+ "ai.dp.budget.consume: a gaussian mechanism needs a scope delta > 0");
466
+ }
467
+
468
+ // Prospective accounting: would this release fit under the budget?
469
+ var cost;
470
+ if (accounting === "basic") {
471
+ if (spentEpsilon + m.epsilon > totalEpsilon + 1e-12 ||
472
+ spentDelta + m.delta > totalDelta + 1e-12) {
473
+ _emitAudit("dp/budget-exhausted", "denied", {
474
+ scope: scope, accounting: accounting, mechanism: m.type,
475
+ requestEpsilon: m.epsilon, requestDelta: m.delta,
476
+ spentEpsilon: spentEpsilon, totalEpsilon: totalEpsilon,
477
+ });
478
+ throw new AiDpError("aiDp/budget-exhausted",
479
+ "ai.dp.budget.consume: scope '" + scope + "' would spend ε=" +
480
+ (spentEpsilon + m.epsilon) + "/" + totalEpsilon + ", δ=" +
481
+ (spentDelta + m.delta) + "/" + totalDelta + "; refused");
482
+ }
483
+ cost = { epsilon: m.epsilon, delta: m.delta };
484
+ } else {
485
+ var trial = rdp.map(function (r, i) { return r + _mechRdp(m, i); });
486
+ var trialEps = _rdpToEpsilon(trial, totalDelta);
487
+ if (trialEps > totalEpsilon + 1e-12) {
488
+ _emitAudit("dp/budget-exhausted", "denied", {
489
+ scope: scope, accounting: accounting, mechanism: m.type,
490
+ projectedEpsilon: trialEps, totalEpsilon: totalEpsilon,
491
+ });
492
+ throw new AiDpError("aiDp/budget-exhausted",
493
+ "ai.dp.budget.consume: scope '" + scope + "' would reach ε=" +
494
+ trialEps.toFixed(4) + " of " + totalEpsilon + " at δ=" + totalDelta + "; refused");
495
+ }
496
+ var before = _rdpToEpsilon(rdp, totalDelta);
497
+ cost = { epsilon: trialEps - before, delta: 0 };
498
+ }
499
+
500
+ // Charge, then sample. (Sampling never fails; charging first keeps
501
+ // the budget monotone even if a caller ignores the throw path.)
502
+ var noised = _applyMechanism(m, value);
503
+ if (accounting === "basic") {
504
+ spentEpsilon += m.epsilon;
505
+ spentDelta += m.delta;
506
+ } else {
507
+ rdp = rdp.map(function (r, i) { return r + _mechRdp(m, i); });
508
+ }
509
+
510
+ _emitAudit("dp/budget-consumed", "allowed", {
511
+ scope: scope, accounting: accounting, mechanism: m.type,
512
+ epsilon: m.epsilon, delta: m.delta,
513
+ });
514
+ return { value: noised, cost: cost, remaining: remaining() };
515
+ }
516
+
517
+ function reset() {
518
+ spentEpsilon = 0;
519
+ spentDelta = 0;
520
+ rdp = RDP_ORDERS.map(function () { return 0; });
521
+ }
522
+
523
+ return {
524
+ consume: consume,
525
+ remaining: remaining,
526
+ spent: spent,
527
+ reset: reset,
528
+ scope: scope,
529
+ accounting: accounting,
530
+ };
531
+ }
532
+
533
+ module.exports = {
534
+ mechanism: mechanism,
535
+ budget: budget,
536
+ MECHANISMS: MECHANISMS,
537
+ ACCOUNTINGS: ACCOUNTINGS,
538
+ AiDpError: AiDpError,
539
+ };
@@ -387,9 +387,16 @@ function random(byteLength) {
387
387
  // when callers requested more. SHAKE256 is also already the
388
388
  // framework's KDF / browser-side derivation primitive, so the same
389
389
  // hash family does double duty.
390
- return nodeCrypto.createHash("shake256", { outputLength: n })
391
- .update(nodeCrypto.randomBytes(n))
390
+ //
391
+ // Node's SHAKE256 XOF is non-uniform at outputLength 1 (the byte
392
+ // values 0x00 and 0xff never occur and the low bit skews to ~0.54);
393
+ // outputLength >= 2 is uniform. Draw at least 2 bytes and slice so a
394
+ // 1-byte request still returns a uniform byte.
395
+ var drawN = n < 2 ? 2 : n;
396
+ var out = nodeCrypto.createHash("shake256", { outputLength: drawN })
397
+ .update(nodeCrypto.randomBytes(drawN))
392
398
  .digest();
399
+ return drawN === n ? out : out.subarray(0, n);
393
400
  }
394
401
 
395
402
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.28",
3
+ "version": "0.12.29",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.12.29",
4
+ "date": "2026-05-24",
5
+ "headline": "`b.ai.dp` — float-safe differential privacy: snapping-mechanism Laplace + discrete Gaussian + Rényi-DP budgets",
6
+ "summary": "Differential privacy adds calibrated noise so an aggregate is provably insensitive to any single record — but the guarantee is fragile: Mironov (2012) showed that a Laplace mechanism sampled with naive double-precision floats lets an attacker distinguish neighbouring datasets with > 35% probability from a single output, silently destroying the promise. `b.ai.dp` ships only mechanisms whose sampling is hardened against that attack class: Laplace via the snapping mechanism (clamp + CSPRNG sign + full-mantissa uniform + power-of-two-grid rounding) and the discrete Gaussian (Canonne–Kamath–Steinke 2020) via integer-exact rejection sampling built from Bernoulli(exp(−γ)) over exact rationals — no floating-point noise at all. All randomness comes from `b.crypto.generateBytes` (SHAKE256 over the OS CSPRNG), never `Math.random`. `b.ai.dp.budget({ scope, epsilon, delta })` tracks a privacy budget per scope and refuses a `consume` that would exceed it, accounting composition either by basic summation (default) or a Rényi-DP accountant (Mironov 2017) for a much tighter bound under repeated Gaussian releases. NIST SP 800-226 (2025) is the evaluation standard; Dwork & Roth is the canonical reference. The exponential and sparse-vector mechanisms are deferred-with-condition — their float-safe constructions (base-2 / permute-and-flip; snapped SVT) re-open on operator demand, since shipping them float-unsafe would defeat the module's purpose.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "`b.ai.dp.mechanism({ type, sensitivity, epsilon, ... })` — float-safe noise mechanisms",
13
+ "body": "`type: \"laplace\"` is the snapping mechanism (pure ε-DP, real-valued, requires a clamp `bound` the guarantee depends on); `type: \"gaussian\"` is the discrete Gaussian (integer-valued, (ε, δ)-DP, requires `delta`). The Gaussian uses the classic calibration σ = √(2 ln(1.25/δ))·Δ/ε, proven for ε ≤ 1 — larger ε is refused with a pointer to splitting the release under an rdp budget. Descriptors are validated + frozen at construction so a malformed parameter fails fast."
14
+ },
15
+ {
16
+ "title": "`b.ai.dp.budget({ scope, epsilon, delta, accounting })` — per-scope privacy budget",
17
+ "body": "Returns `{ consume, remaining, spent, reset }`. `consume(mechanism, value)` adds the mechanism's noise, charges the accountant, and throws `aiDp/budget-exhausted` if the release would push the scope past its (ε, δ). `accounting: \"basic\"` (default) sums per-release ε and δ; `accounting: \"rdp\"` runs a Rényi-DP accountant across a grid of orders and converts to (ε, δ) at the scope's δ for a tight composition bound under repeated Gaussian releases (requires `delta > 0`). The scope budget is enforced on both ε and δ independently."
18
+ }
19
+ ]
20
+ },
21
+ {
22
+ "heading": "Security",
23
+ "items": [
24
+ {
25
+ "title": "`b.crypto.generateBytes` uniformity fix at 1-byte length",
26
+ "body": "Node's SHAKE256 XOF is non-uniform at `outputLength: 1` — the byte values 0x00 and 0xff never occur and the low bit skews to ~0.54. `b.crypto.generateBytes(1)` (and the underlying `random(1)`) now draws at least 2 bytes and slices, so a single-byte CSPRNG request is uniform. Surfaced by `b.ai.dp` per-byte noise sampling; any per-byte consumer of `generateBytes` inherits the fix. A regression test asserts 0x00 / 0xff occur and the low bit is balanced."
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ }