@blamejs/core 0.8.76 → 0.8.78

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.
@@ -537,6 +537,162 @@ function deployerChecklist(assessment) {
537
537
  return items;
538
538
  }
539
539
 
540
+ /**
541
+ * @primitive b.complianceAiAct.fundamentalRightsImpactAssessment
542
+ * @signature b.complianceAiAct.fundamentalRightsImpactAssessment(opts)
543
+ * @since 0.8.77
544
+ *
545
+ * EU AI Act Article 27 — Fundamental Rights Impact Assessment (FRIA).
546
+ * Mandatory for deployers of high-risk AI systems listed in Annex III
547
+ * §5 (creditworthiness scoring, life/health insurance risk), §6
548
+ * (law enforcement), §7 (migration/asylum), §8 (justice admin), public
549
+ * authorities, and any private body providing public services. Must
550
+ * be completed BEFORE the first use of the high-risk system, kept
551
+ * updated, and notified to the national market-surveillance authority.
552
+ *
553
+ * Returns the structured FRIA document scaffold — operator fills in
554
+ * the per-deployment specifics; the framework auto-populates the
555
+ * fields it can derive (system identification, GPAI classification
556
+ * if applicable, Annex IV reference, deployment-context audit hooks).
557
+ *
558
+ * @opts
559
+ * {
560
+ * systemId: string, // operator's high-risk system identifier
561
+ * systemDescription: { ... }, // forwarded to classify() for risk-tier verdict
562
+ * deploymentContext: { purpose, sector, geography, scale },
563
+ * affectedPersons: { categories: string[], estimatedCount: number },
564
+ * risksToFundamentalRights: string[], // operator-identified risks
565
+ * mitigations: string[], // mitigations + monitoring per risk
566
+ * humanOversight: { roles: string[], escalationPath: string },
567
+ * residualRisks: string[],
568
+ * reviewCadence: string, // e.g. "quarterly"
569
+ * }
570
+ *
571
+ * @example
572
+ * var fria = b.complianceAiAct.fundamentalRightsImpactAssessment({
573
+ * systemId: "credit-scoring-v3",
574
+ * deploymentContext: { purpose: "loan approval", sector: "financial",
575
+ * geography: "EU", scale: "1M decisions/year" },
576
+ * affectedPersons: { categories: ["EU consumers"], estimatedCount: 1000000 },
577
+ * risksToFundamentalRights: ["discriminatory denial", "right to explanation"],
578
+ * mitigations: ["bias audit every 6 months", "human review threshold"],
579
+ * humanOversight: { roles: ["credit officer"], escalationPath: "ombudsman" },
580
+ * reviewCadence: "semi-annual",
581
+ * });
582
+ */
583
+ function fundamentalRightsImpactAssessment(opts) {
584
+ if (!opts || typeof opts !== "object") {
585
+ throw new Error("fundamentalRightsImpactAssessment: opts required");
586
+ }
587
+ validateOpts.requireNonEmptyString(opts.systemId, "systemId",
588
+ Error, "compliance-ai-act/no-system-id");
589
+ return {
590
+ "$schema": "https://blamejs.com/schema/ai-act-fria-v1.json",
591
+ regulation: "EU Regulation 2024/1689 — AI Act",
592
+ article: "Article 27 (Fundamental Rights Impact Assessment)",
593
+ generatedAt: new Date().toISOString(),
594
+ systemId: opts.systemId,
595
+ classification: opts.systemDescription ? classify(opts.systemDescription) : null,
596
+ deploymentContext: opts.deploymentContext || {},
597
+ affectedPersons: opts.affectedPersons || { categories: [], estimatedCount: null },
598
+ risks: opts.risksToFundamentalRights || [],
599
+ mitigations: opts.mitigations || [],
600
+ humanOversight: opts.humanOversight || { roles: [], escalationPath: null },
601
+ residualRisks: opts.residualRisks || [],
602
+ reviewCadence: opts.reviewCadence || "annual",
603
+ notificationStatus: "operator-must-notify",
604
+ note: "Notify national market-surveillance authority before first use (Art 27(3))",
605
+ auditHook: "b.audit emission action='aiact.fria.completed' recommended",
606
+ annexIVReference: "see b.complianceAiAct.annexIVScaffold for technical documentation",
607
+ };
608
+ }
609
+
610
+ /**
611
+ * @primitive b.complianceAiAct.gpai.trainingDataSummary
612
+ * @signature b.complianceAiAct.gpai.trainingDataSummary(opts)
613
+ * @since 0.8.77
614
+ *
615
+ * EU AI Act Article 53(1)(d) — GPAI training-data summary template
616
+ * compliant with the AI Office's template format (published in 2024,
617
+ * mandatory from 2026-08-02). The template requires categories of
618
+ * data, modalities, source provenance, copyright + licensing status,
619
+ * dataset sizes, dates of collection, and steps taken to identify +
620
+ * mitigate biases.
621
+ *
622
+ * Returns the JSON document operators publish under their `/.well-known/
623
+ * ai-training-data-summary` endpoint or attach to model cards.
624
+ *
625
+ * @opts
626
+ * modelId: string, // required
627
+ * modelVersion: string, // optional
628
+ * provider: object, // { name, address, contact }
629
+ * dataCategories: string[], // ["web-crawl", "books", "code", "synthetic", ...]
630
+ * modalities: string[], // ["text", "image", "audio", "video"]
631
+ * sources: object[], // { identifier, url, type, licenseStatus, size, collectedFrom, collectedTo }
632
+ * copyrightStatus: object, // { respectsRightReservations, machineReadableSignalsObserved, tdmExceptionUsed }
633
+ * biasMitigation: object, // { methodsApplied, auditCadence, remediations }
634
+ * contentProvenance: object, // { synthIdEmbed, c2paManifestEmbed, watermarkProvider }
635
+ *
636
+ * @example
637
+ * var summary = b.complianceAiAct.gpai.trainingDataSummary({
638
+ * modelId: "acme-llm-7b",
639
+ * modelVersion: "1.0",
640
+ * provider: { name: "Acme AI", address: "1 St", contact: "ai@acme.example" },
641
+ * dataCategories: ["web-crawl", "books", "code"],
642
+ * modalities: ["text"],
643
+ * sources: [
644
+ * { identifier: "CommonCrawl-2024", type: "web-crawl", licenseStatus: "permitted" },
645
+ * ],
646
+ * biasMitigation: { methodsApplied: ["demographic-balance"], auditCadence: "quarterly" },
647
+ * });
648
+ */
649
+ function trainingDataSummary(opts) {
650
+ if (!opts || typeof opts !== "object") {
651
+ throw new Error("trainingDataSummary: opts required");
652
+ }
653
+ validateOpts.requireNonEmptyString(opts.modelId, "modelId",
654
+ Error, "compliance-ai-act/no-model-id");
655
+ return {
656
+ "$schema": "https://blamejs.com/schema/ai-act-gpai-training-summary-v1.json",
657
+ regulation: "EU Regulation 2024/1689 — AI Act",
658
+ article: "Article 53(1)(d) (GPAI training data summary)",
659
+ template: "AI Office GPAI Training Data Summary Template",
660
+ generatedAt: new Date().toISOString(),
661
+ modelId: opts.modelId,
662
+ modelVersion: opts.modelVersion || null,
663
+ provider: opts.provider || { name: null, address: null, contact: null },
664
+ dataCategories: opts.dataCategories || [], // ["web-crawl", "books", "code", "synthetic", ...]
665
+ modalities: opts.modalities || [], // ["text", "image", "audio", "video"]
666
+ sources: (opts.sources || []).map(function (s) {
667
+ return {
668
+ identifier: s.identifier,
669
+ url: s.url || null,
670
+ type: s.type || "unknown",
671
+ licenseStatus: s.licenseStatus || "unknown",
672
+ size: s.size || null,
673
+ collectedFrom: s.collectedFrom || null,
674
+ collectedTo: s.collectedTo || null,
675
+ };
676
+ }),
677
+ copyrightStatus: opts.copyrightStatus || {
678
+ respectsRightReservations: null,
679
+ machineReadableSignalsObserved: null,
680
+ tdmExceptionUsed: null,
681
+ },
682
+ biasMitigation: opts.biasMitigation || {
683
+ methodsApplied: [],
684
+ auditCadence: "annual",
685
+ remediations: [],
686
+ },
687
+ contentProvenance: opts.contentProvenance || {
688
+ synthIdEmbed: false,
689
+ c2paManifestEmbed: false,
690
+ watermarkProvider: null,
691
+ },
692
+ note: "Publish at /.well-known/ai-training-data-summary or model card per AI Office template (mandatory 2026-08-02)",
693
+ };
694
+ }
695
+
540
696
  module.exports = {
541
697
  classify: classify,
542
698
  deployerChecklist: deployerChecklist,
@@ -545,9 +701,10 @@ module.exports = {
545
701
  transparency: transparency,
546
702
  logging: logging,
547
703
  gpai: {
548
- classify: gpaiClassify,
549
- listObligations: listGpaiObligations,
550
- OBLIGATIONS: GPAI_OBLIGATIONS,
704
+ classify: gpaiClassify,
705
+ listObligations: listGpaiObligations,
706
+ trainingDataSummary: trainingDataSummary,
707
+ OBLIGATIONS: GPAI_OBLIGATIONS,
551
708
  },
552
709
  articleObligations: articleObligations,
553
710
  listArticles: listArticles,
@@ -555,4 +712,5 @@ module.exports = {
555
712
  DEADLINES: DEADLINES,
556
713
  emitClassificationAudit: emitClassificationAudit,
557
714
  annexIVScaffold: annexIVScaffold,
715
+ fundamentalRightsImpactAssessment: fundamentalRightsImpactAssessment,
558
716
  };
package/lib/compliance.js CHANGED
@@ -110,6 +110,32 @@ var KNOWN_POSTURES = Object.freeze([
110
110
  "nydfs-500", // NYDFS 23 NYCRR 500 Amendment 2 — financial cybersecurity (multi-factor + asset inventory + governance)
111
111
  "hipaa-2026", // HHS HIPAA Security Rule 2026-Q4 final — extends hipaa with mandatory MFA + asset inventory + 72h restoration testing
112
112
  "quebec-25", // Quebec Law 25 final phase (effective 2026-09-22) — DPIA + automated-decision opt-out
113
+ // ---- v0.8.77 expansion — US state consumer-privacy postures ----
114
+ // Each posture carries per-state cure-period, profiling opt-out
115
+ // and minor-consent metadata via b.dsr.stateRules(state). The
116
+ // generic DSR primitive (b.dsr.submit) covers ~80% of the surface;
117
+ // these postures fill in the per-state drift.
118
+ "vcdpa", // Virginia Consumer Data Protection Act
119
+ "co-cpa", // Colorado Privacy Act
120
+ "ctdpa", // Connecticut Data Privacy Act
121
+ "ucpa", // Utah Consumer Privacy Act
122
+ "tdpsa", // Texas Data Privacy and Security Act
123
+ "or-cpa", // Oregon Consumer Privacy Act
124
+ "mt-cdpa", // Montana Consumer Data Privacy Act
125
+ "ia-icdpa", // Iowa Consumer Data Protection Act
126
+ "in-indpa", // Indiana Consumer Data Protection Act
127
+ "de-dpdpa", // Delaware Personal Data Privacy Act
128
+ "nh-nhpa", // New Hampshire SB 255 Consumer Privacy Act
129
+ "nj-njdpa", // New Jersey Data Privacy Act
130
+ "ky-kcdpa", // Kentucky Consumer Data Protection Act
131
+ "tn-tipa", // Tennessee Information Protection Act
132
+ "mn-mncdpa", // Minnesota Consumer Data Privacy Act
133
+ "ri-ricpa", // Rhode Island Consumer Privacy Act
134
+ "ne-dpa", // Nebraska Data Privacy Act
135
+ "nv-sb370", // Nevada SB 370 Consumer Health Data Privacy
136
+ "ca-aadc", // California Age-Appropriate Design Code (partial preliminary injunction; track for re-enforcement)
137
+ "ct-sb3", // Connecticut SB 3 Consumer Health Data
138
+ "tx-cubi", // Texas Capture or Use of Biometric Identifier
113
139
  ]);
114
140
 
115
141
  var STATE = { posture: null, setAt: null };
@@ -493,6 +519,28 @@ var REGIME_MAP = Object.freeze({
493
519
  jurisdiction: "CA-QC",
494
520
  domain: "privacy",
495
521
  },
522
+ // v0.8.77 — US state consumer-privacy postures
523
+ "vcdpa": { name: "Virginia Consumer Data Protection Act", citation: "Va. Code §59.1-575 et seq. (effective 2023-01-01)", jurisdiction: "US-VA", domain: "privacy" },
524
+ "co-cpa": { name: "Colorado Privacy Act", citation: "C.R.S. §6-1-1301 et seq. (effective 2023-07-01)", jurisdiction: "US-CO", domain: "privacy" },
525
+ "ctdpa": { name: "Connecticut Data Privacy Act", citation: "Conn. Gen. Stat. §42-515 et seq. (effective 2023-07-01)", jurisdiction: "US-CT", domain: "privacy" },
526
+ "ucpa": { name: "Utah Consumer Privacy Act", citation: "Utah Code §13-61-101 et seq. (effective 2023-12-31)", jurisdiction: "US-UT", domain: "privacy" },
527
+ "tdpsa": { name: "Texas Data Privacy and Security Act", citation: "Tex. Bus. & Com. Code §541.001 et seq. (effective 2024-07-01)", jurisdiction: "US-TX", domain: "privacy" },
528
+ "or-cpa": { name: "Oregon Consumer Privacy Act", citation: "Or. Rev. Stat. §646A.570 et seq. (effective 2024-07-01)", jurisdiction: "US-OR", domain: "privacy" },
529
+ "mt-cdpa": { name: "Montana Consumer Data Privacy Act", citation: "Mont. Code §30-14-2801 et seq. (effective 2024-10-01)", jurisdiction: "US-MT", domain: "privacy" },
530
+ "ia-icdpa": { name: "Iowa Consumer Data Protection Act", citation: "Iowa Code §715D (effective 2025-01-01)", jurisdiction: "US-IA", domain: "privacy" },
531
+ "in-indpa": { name: "Indiana Consumer Data Protection Act", citation: "Ind. Code §24-15 (effective 2026-01-01)", jurisdiction: "US-IN", domain: "privacy" },
532
+ "de-dpdpa": { name: "Delaware Personal Data Privacy Act", citation: "6 Del. Code Ch. 12D (effective 2026-01-01)", jurisdiction: "US-DE", domain: "privacy" },
533
+ "nh-nhpa": { name: "New Hampshire SB 255 Consumer Privacy Act", citation: "NH RSA Chapter 507-H (effective 2026-01-01)", jurisdiction: "US-NH", domain: "privacy" },
534
+ "nj-njdpa": { name: "New Jersey Data Privacy Act", citation: "N.J. Rev. Stat. §56:8-166.4 et seq. (effective 2026-01-15)", jurisdiction: "US-NJ", domain: "privacy" },
535
+ "ky-kcdpa": { name: "Kentucky Consumer Data Protection Act", citation: "Ky. Rev. Stat. §367.3611 et seq. (effective 2026-01-01)", jurisdiction: "US-KY", domain: "privacy" },
536
+ "tn-tipa": { name: "Tennessee Information Protection Act", citation: "Tenn. Code §47-18-3201 et seq. (effective 2025-07-01)", jurisdiction: "US-TN", domain: "privacy" },
537
+ "mn-mncdpa": { name: "Minnesota Consumer Data Privacy Act", citation: "Minn. Stat. §325O (effective 2026-07-31)", jurisdiction: "US-MN", domain: "privacy" },
538
+ "ri-ricpa": { name: "Rhode Island Consumer Privacy Act", citation: "R.I. Gen. Laws §6-48.1 (effective 2026-01-01)", jurisdiction: "US-RI", domain: "privacy" },
539
+ "ne-dpa": { name: "Nebraska Data Privacy Act", citation: "Neb. Rev. Stat. §87-1101 et seq. (effective 2025-01-01)", jurisdiction: "US-NE", domain: "privacy" },
540
+ "nv-sb370": { name: "Nevada SB 370 Consumer Health Data Privacy", citation: "Nev. Rev. Stat. §603A (consumer-health amendments, effective 2024-03-31)", jurisdiction: "US-NV", domain: "health" },
541
+ "ca-aadc": { name: "California Age-Appropriate Design Code Act", citation: "Cal. Civ. Code §1798.99.28 et seq. (partial preliminary injunction NetChoice v. Bonta)", jurisdiction: "US-CA", domain: "privacy" },
542
+ "ct-sb3": { name: "Connecticut SB 3 Consumer Health Data", citation: "Conn. P.A. 23-56 (effective 2023-07-01)", jurisdiction: "US-CT", domain: "health" },
543
+ "tx-cubi": { name: "Texas Capture or Use of Biometric Identifier", citation: "Tex. Bus. & Com. Code §503.001 (effective 2009-09-01)", jurisdiction: "US-TX", domain: "biometric" },
496
544
  });
497
545
 
498
546
  /**
package/lib/config.js CHANGED
@@ -185,14 +185,25 @@ function create(opts) {
185
185
  return value;
186
186
  }
187
187
 
188
- return {
189
- value: value,
188
+ // `.value` is a getter, not a captured property. Without this,
189
+ // `cfg.value.X` reads from the object that was current at create()
190
+ // and never reflects subsequent reload() updates — operators looking
191
+ // at `cfg.value.FEATURE_X` would see stale values forever, while
192
+ // `cfg.get("FEATURE_X")` saw fresh ones. The @primitive docs
193
+ // (loadDbBacked example) promise `cfg.value.X` always works, so the
194
+ // getter is the contract.
195
+ var handle = {
190
196
  get: function (key) { return value[key]; },
191
197
  has: function (key) { return Object.prototype.hasOwnProperty.call(value, key); },
192
198
  redacted: redactedView,
193
199
  subscribe: subscribe,
194
200
  reload: reload,
195
201
  };
202
+ Object.defineProperty(handle, "value", {
203
+ get: function () { return value; },
204
+ enumerable: true,
205
+ });
206
+ return handle;
196
207
  }
197
208
 
198
209
  /**
@@ -209,16 +220,45 @@ function create(opts) {
209
220
  * via the underlying handle's `reload`, and re-validates. Reload
210
221
  * failures emit a `config.reload.failed` audit row but DO NOT
211
222
  * clobber the previous value — the running app stays on the
212
- * last-good config. The returned handle is the same shape as
213
- * `create()` plus a `.stop()` method that halts the poller.
223
+ * last-good config.
224
+ *
225
+ * Returns immediately with a synchronous handle, but kicks off one
226
+ * immediate hydration tick on construction so the first DB read
227
+ * happens at t=0 rather than t=intervalMs. Callers that need to wait
228
+ * for first-data-applied can `await handle.hydrated` before the app
229
+ * starts serving traffic; the Promise resolves after the first tick
230
+ * settles (success OR audit-on-failure path) and never rejects, so
231
+ * the boot path never deadlocks on a temporarily-unreachable DB.
232
+ *
233
+ * The returned handle is the same shape as `create()` plus:
234
+ * - `.hydrated` — Promise<void> for the first tick
235
+ * - `.refresh()`— run one tick on demand (save-triggered reload);
236
+ * returns Promise<void> that never rejects
237
+ * - `.stop()` — halts the poller
238
+ *
239
+ * Three tiers of precedence (highest wins): the DB-row overlay
240
+ * resolved at each `_tick` > the `opts.env` baseline > defaults
241
+ * declared on the schema (`s.string().default(...)` and friends).
242
+ * The `.subscribe(fn)` callback registered through `create()` fires
243
+ * synchronously inside every successful reload — operators reach for
244
+ * it to invalidate caches, recompute derived state, or hot-rebuild
245
+ * middleware that closed over the previous config value.
214
246
  *
215
247
  * @opts
216
- * schema: b.safeSchema instance (required),
217
- * env: object (env baseline; default process.env),
218
- * redactKeys: Array<string>,
219
- * fetchRows: async () => Array<{ key: string, value: string }> (required),
220
- * intervalMs: number (positive finite poll interval),
221
- * audit: boolean (default true; reserved for future per-poll audit),
248
+ * schema: b.safeSchema instance (required),
249
+ * env: object (env baseline; default process.env),
250
+ * redactKeys: Array<string>,
251
+ * fetchRows: async () => Array<{ key: string, value: string }> (required),
252
+ * intervalMs: number (positive finite poll interval),
253
+ * transformValue: (row) => string | Promise<string> (optional per-row
254
+ * transform — receives `{ key, value, ...rest }` so the
255
+ * row can carry envelope metadata; returns the value
256
+ * that flows into the schema. Common shape: unseal a
257
+ * `b.vault`-sealed ciphertext column before validation.
258
+ * Rows whose transform throws or returns a non-string
259
+ * are skipped with a `config.reload.failed` audit so a
260
+ * single bad row never crashes the poller),
261
+ * audit: boolean (default true; reserved for future per-poll audit),
222
262
  *
223
263
  * @example
224
264
  * var s = b.safeSchema;
@@ -234,10 +274,42 @@ function create(opts) {
234
274
  * });
235
275
  * cfg.value.FEATURE_X; // → false (until first poll tick lands)
236
276
  * cfg.stop(); // halt the poller on shutdown
277
+ *
278
+ * @example
279
+ * // Sealed values — column stores `b.vault.seal(plain)` ciphertext.
280
+ * var cfg = b.config.loadDbBacked({
281
+ * schema: s.object({ STRIPE_SECRET: s.string() }),
282
+ * fetchRows: async function () {
283
+ * return await db.all("SELECT key, sealed FROM _config WHERE sealed IS NOT NULL");
284
+ * },
285
+ * transformValue: function (row) {
286
+ * return b.vault.unseal(row.sealed).toString("utf8");
287
+ * },
288
+ * intervalMs: 30 * 1000,
289
+ * });
290
+ *
291
+ * @example
292
+ * // Save-triggered reload — admin UI writes a row, fires refresh()
293
+ * // so the new value is active immediately without waiting for
294
+ * // intervalMs. cfg.subscribe(...) sees the change inline.
295
+ * var cfg = b.config.loadDbBacked({
296
+ * schema: s.object({ FEATURE_X: b.config.coerce.boolean().default(false) }),
297
+ * fetchRows: async function () { return await db.all("SELECT key, value FROM _config"); },
298
+ * intervalMs: 5 * 60 * 1000, // safety-net interval
299
+ * });
300
+ * await cfg.hydrated; // boot path waits
301
+ * cfg.subscribe(function (next) { cache.invalidate(); });
302
+ *
303
+ * adminApp.post("/settings", async function (req, res) {
304
+ * await db.run("INSERT OR REPLACE INTO _config(key,value) VALUES (?,?)",
305
+ * req.body.key, req.body.value);
306
+ * await cfg.refresh(); // active immediately
307
+ * res.json({ ok: true });
308
+ * });
237
309
  */
238
310
  function loadDbBacked(opts) {
239
311
  opts = opts || {};
240
- validateOpts(opts, ["schema", "env", "redactKeys", "fetchRows", "intervalMs", "audit"],
312
+ validateOpts(opts, ["schema", "env", "redactKeys", "fetchRows", "intervalMs", "transformValue", "audit"],
241
313
  "config.loadDbBacked");
242
314
  if (typeof opts.fetchRows !== "function") {
243
315
  throw new ConfigError("config/bad-fetch-rows",
@@ -247,6 +319,9 @@ function loadDbBacked(opts) {
247
319
  throw new ConfigError("config/bad-interval",
248
320
  "loadDbBacked: opts.intervalMs must be a positive finite number");
249
321
  }
322
+ var transformValue = validateOpts.optionalFunction(
323
+ opts.transformValue, "loadDbBacked: opts.transformValue",
324
+ ConfigError, "config/bad-transform-value") || null;
250
325
  var cfg = create({ schema: opts.schema, env: opts.env, redactKeys: opts.redactKeys });
251
326
  var stopped = false;
252
327
  async function _tick() {
@@ -265,9 +340,32 @@ function loadDbBacked(opts) {
265
340
  if (!Array.isArray(rows)) return;
266
341
  var overlay = {};
267
342
  for (var i = 0; i < rows.length; i++) {
268
- if (rows[i] && typeof rows[i].key === "string") {
269
- overlay[rows[i].key] = rows[i].value;
343
+ var row = rows[i];
344
+ if (!row || typeof row.key !== "string") continue;
345
+ var value = row.value;
346
+ if (transformValue) {
347
+ try {
348
+ value = await transformValue(row);
349
+ } catch (e) {
350
+ try {
351
+ lazyAudit().safeEmit({
352
+ action: "config.reload.failed", outcome: "failure",
353
+ metadata: { phase: "transform", key: row.key, reason: e && e.message },
354
+ });
355
+ } catch (_e) { /* audit best-effort */ }
356
+ continue;
357
+ }
358
+ if (typeof value !== "string") {
359
+ try {
360
+ lazyAudit().safeEmit({
361
+ action: "config.reload.failed", outcome: "failure",
362
+ metadata: { phase: "transform", key: row.key, reason: "transformValue did not return a string" },
363
+ });
364
+ } catch (_e) { /* audit best-effort */ }
365
+ continue;
366
+ }
270
367
  }
368
+ overlay[row.key] = value;
271
369
  }
272
370
  try { cfg.reload(overlay); }
273
371
  catch (e) {
@@ -279,7 +377,25 @@ function loadDbBacked(opts) {
279
377
  } catch (_e) { /* audit best-effort */ }
280
378
  }
281
379
  }
380
+ // Fire one immediate hydration before the interval kicks in so
381
+ // callers can `await cfg.hydrated` and not get an empty config window
382
+ // (env defaults only) for the first intervalMs of process lifetime.
383
+ // The interval still fires every intervalMs afterwards for ongoing
384
+ // drift detection. The hydration Promise NEVER rejects — _tick
385
+ // swallows fetch / transform / validate failures via audit, matching
386
+ // the established "last-good config stays in place" contract.
387
+ cfg.hydrated = _tick();
282
388
  var handle = safeAsync.repeating(_tick, opts.intervalMs, { name: "config-db-reload" });
389
+ // Save-triggered reload — admin save handlers / settings-management
390
+ // UIs invoke cfg.refresh() right after writing a row to drop the
391
+ // intervalMs-worth of staleness latency between save and active.
392
+ // Returns the same Promise<void> shape as cfg.hydrated: resolves
393
+ // after the tick settles (success OR audit-on-failure), never
394
+ // rejects so the save handler never deadlocks on a flaky DB.
395
+ // Subscribers fire synchronously inside cfg.reload() within the
396
+ // tick, matching the save-then-invalidate-cache pattern operators
397
+ // expect when an admin flips a feature flag.
398
+ cfg.refresh = function () { return _tick(); };
283
399
  cfg.stop = function () { stopped = true; if (handle) { handle.stop(); handle = null; } };
284
400
  return cfg;
285
401
  }