@polderlabs/bizar 6.3.0 → 7.0.0

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 (245) hide show
  1. package/.claude/hooks/sessionend-recall.mjs +3 -3
  2. package/.claude/hooks/worker-suggest.mjs +110 -0
  3. package/.claude/settings.json +9 -0
  4. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
  5. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
  6. package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
  7. package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
  8. package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
  9. package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
  10. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
  11. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
  12. package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
  13. package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
  14. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
  15. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
  16. package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
  17. package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
  18. package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
  19. package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
  20. package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
  21. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
  22. package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
  23. package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
  24. package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
  25. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
  26. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
  27. package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
  28. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
  29. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
  30. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
  31. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
  32. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
  33. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
  34. package/bizar-dash/dist/index.html +12 -10
  35. package/bizar-dash/dist/mobile.html +7 -4
  36. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  37. package/bizar-dash/src/server/api.mjs +11 -0
  38. package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
  39. package/bizar-dash/src/server/memory-distillation.mjs +296 -0
  40. package/bizar-dash/src/server/routes/distill.mjs +129 -0
  41. package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
  42. package/bizar-dash/src/web/App.tsx +4 -0
  43. package/bizar-dash/src/web/components/Tag.tsx +18 -5
  44. package/bizar-dash/src/web/components/Topbar.tsx +3 -0
  45. package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
  46. package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
  47. package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
  48. package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
  49. package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
  50. package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
  51. package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
  52. package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
  53. package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
  54. package/bizar-dash/src/web/lib/types.ts +25 -0
  55. package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
  56. package/bizar-dash/src/web/styles/main.css +251 -0
  57. package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
  58. package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
  59. package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
  60. package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
  61. package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
  62. package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
  63. package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
  64. package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
  65. package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
  66. package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
  67. package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
  68. package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
  69. package/bizar-dash/src/web/ui/controls/controls.css +454 -0
  70. package/bizar-dash/src/web/ui/controls/index.ts +33 -0
  71. package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
  72. package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
  73. package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
  74. package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
  75. package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
  76. package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
  77. package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
  78. package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
  79. package/bizar-dash/src/web/ui/data/data.css +535 -0
  80. package/bizar-dash/src/web/ui/data/index.ts +31 -0
  81. package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
  82. package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
  83. package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
  84. package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
  85. package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
  86. package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
  87. package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
  88. package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
  89. package/bizar-dash/src/web/ui/index.ts +56 -0
  90. package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
  91. package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
  92. package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
  93. package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
  94. package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
  95. package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
  96. package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
  97. package/bizar-dash/src/web/ui/layout/index.ts +33 -0
  98. package/bizar-dash/src/web/ui/layout/layout.css +413 -0
  99. package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
  100. package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
  101. package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
  102. package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
  103. package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
  104. package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
  105. package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
  106. package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
  107. package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
  108. package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
  109. package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
  110. package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
  111. package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
  112. package/bizar-dash/src/web/ui/styles/globals.css +86 -0
  113. package/bizar-dash/src/web/ui/styles/reset.css +125 -0
  114. package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
  115. package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
  116. package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
  117. package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
  118. package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
  119. package/bizar-dash/src/web/views/Agents.tsx +506 -266
  120. package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
  121. package/bizar-dash/src/web/views/Overview.tsx +996 -448
  122. package/bizar-dash/src/web/views/Tasks.tsx +277 -273
  123. package/bizar-dash/tests/communication-log.test.tsx +67 -0
  124. package/bizar-dash/tests/dep-graph.test.tsx +40 -0
  125. package/bizar-dash/tests/event-log.test.tsx +61 -0
  126. package/bizar-dash/tests/goal-input.test.tsx +72 -0
  127. package/bizar-dash/tests/goap-planner.test.ts +68 -0
  128. package/bizar-dash/tests/quality-gates.test.tsx +59 -0
  129. package/bizar-dash/tests/setup.ts +31 -1
  130. package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
  131. package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
  132. package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
  133. package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
  134. package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
  135. package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
  136. package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
  137. package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
  138. package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
  139. package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
  140. package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
  141. package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
  142. package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
  143. package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
  144. package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
  145. package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
  146. package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
  147. package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
  148. package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
  149. package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
  150. package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
  151. package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
  152. package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
  153. package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
  154. package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
  155. package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
  156. package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
  157. package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
  158. package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
  159. package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
  160. package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
  161. package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
  162. package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
  163. package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
  164. package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
  165. package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
  166. package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
  167. package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
  168. package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
  169. package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
  170. package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
  171. package/bizar-dash/tests/views/Agents.test.tsx +359 -0
  172. package/bizar-dash/tests/views/Overview.test.tsx +413 -0
  173. package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
  174. package/bizar-dash/vitest.config.ts +3 -1
  175. package/cli/__tests__/cost-gate.test.mjs +301 -0
  176. package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
  177. package/cli/bin.mjs +38 -0
  178. package/cli/commands/claim.mjs +282 -0
  179. package/cli/commands/cost.mjs +279 -0
  180. package/cli/cost-gate.mjs +501 -0
  181. package/cli/feature-list-bridge.mjs +400 -0
  182. package/cli/memory-constants.mjs +17 -0
  183. package/cli/worker-dispatcher.mjs +259 -0
  184. package/cli/worker-dispatcher.test.mjs +204 -0
  185. package/config/trigger-patterns.json +186 -0
  186. package/package.json +2 -2
  187. package/packages/sdk/package.json +1 -1
  188. package/packages/sdk/src/agent-registry.ts +304 -0
  189. package/packages/sdk/src/consensus/byzantine.ts +587 -0
  190. package/packages/sdk/src/consensus/index.ts +147 -0
  191. package/packages/sdk/src/consensus/queen.ts +173 -0
  192. package/packages/sdk/src/consensus/types.ts +199 -0
  193. package/packages/sdk/src/federation/audit.ts +156 -0
  194. package/packages/sdk/src/federation/budget.ts +269 -0
  195. package/packages/sdk/src/federation/envelope.ts +144 -0
  196. package/packages/sdk/src/federation/hmac.ts +160 -0
  197. package/packages/sdk/src/federation/index.ts +443 -0
  198. package/packages/sdk/src/federation/pii.ts +241 -0
  199. package/packages/sdk/src/federation/policy.ts +112 -0
  200. package/packages/sdk/src/federation/trust.ts +146 -0
  201. package/packages/sdk/src/index.ts +67 -0
  202. package/packages/sdk/src/mcp/server.ts +351 -1
  203. package/packages/sdk/src/router/codemod-intent.ts +125 -0
  204. package/packages/sdk/src/router/index.ts +247 -0
  205. package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
  206. package/packages/sdk/src/router/memory-distillation.ts +249 -0
  207. package/packages/sdk/src/router/model-router.ts +235 -0
  208. package/packages/sdk/src/router/q-learning-router.ts +304 -0
  209. package/packages/sdk/src/swarm-topology.ts +279 -0
  210. package/packages/sdk/tests/agent-registry.test.ts +285 -0
  211. package/packages/sdk/tests/consensus.test.ts +376 -0
  212. package/packages/sdk/tests/federation/audit.test.ts +164 -0
  213. package/packages/sdk/tests/federation/budget.test.ts +189 -0
  214. package/packages/sdk/tests/federation/envelope.test.ts +120 -0
  215. package/packages/sdk/tests/federation/hmac.test.ts +194 -0
  216. package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
  217. package/packages/sdk/tests/federation/pii.test.ts +106 -0
  218. package/packages/sdk/tests/federation/policy.test.ts +140 -0
  219. package/packages/sdk/tests/federation/trust.test.ts +124 -0
  220. package/packages/sdk/tests/mcp-tools.test.ts +204 -0
  221. package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
  222. package/packages/sdk/tests/model-router.test.mjs +108 -0
  223. package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
  224. package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
  225. package/packages/sdk/tests/router.test.mjs +91 -0
  226. package/packages/sdk/tests/sdk.test.mjs +26 -2
  227. package/packages/sdk/tests/swarm-topology.test.ts +258 -0
  228. package/scripts/bh-full-e2e.mjs +11 -15
  229. package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
  230. package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
  231. package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
  232. package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
  233. package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
  234. package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
  235. package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
  236. package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
  237. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
  238. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
  239. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
  240. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
  241. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
  242. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
  243. package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
  244. package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
  245. package/packages/sdk/package-lock.json +0 -32
@@ -0,0 +1,443 @@
1
+ /**
2
+ * federation/index.ts — top-level orchestrator + public exports.
3
+ *
4
+ * F-038 — composes the eight federation primitives into a single
5
+ * `createFederation(opts)` factory. The orchestrator wires:
6
+ *
7
+ * sign — produce a signed FederationEnvelope (with fresh nonce +
8
+ * PII redaction + audit log entry).
9
+ * verify — verify an inbound envelope (HMAC + nonce window +
10
+ * trust + policy + audit log entry).
11
+ * send — high-level helper: sign + persist outgoing audit.
12
+ * receive — high-level helper: verify + accept-or-reject + persist
13
+ * inbound audit.
14
+ * status — snapshot for the `federation_status` MCP tool.
15
+ *
16
+ * No native deps. Pure TS over Node `crypto` + `fs`.
17
+ */
18
+
19
+ import { randomUUID } from "node:crypto";
20
+
21
+ import {
22
+ canonicalSignablePayload,
23
+ emptyScanResult,
24
+ type EnvelopeBudget,
25
+ type FederationEnvelope,
26
+ type FederationMessageType,
27
+ } from "./envelope.js";
28
+ import { freshNonce, NonceCache, signEnvelope, verifySignature } from "./hmac.js";
29
+ import { apply, isPiiMode, type PiiMode, type PiiResult } from "./pii.js";
30
+ import { TrustEvaluator, type PeerState } from "./trust.js";
31
+ import { PolicyEngine, type PolicyDecision } from "./policy.js";
32
+ import { AuditService } from "./audit.js";
33
+ import { FederationBudget, validateBudgetInput, type PerPeerBudget } from "./budget.js";
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Public re-exports
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export {
40
+ FEDERATION_MESSAGE_TYPES,
41
+ TRUST_AFFECTING_MESSAGE_TYPES,
42
+ isFederationMessageType,
43
+ canonicalSignablePayload,
44
+ envelopeAgeMs,
45
+ emptyScanResult,
46
+ type EnvelopeBudget,
47
+ type FederationEnvelope,
48
+ type FederationMessageType,
49
+ type PiiScanAction,
50
+ type PiiScanDetection,
51
+ type PiiScanResult,
52
+ } from "./envelope.js";
53
+
54
+ export {
55
+ DEFAULT_NONCE_WINDOW_MS,
56
+ NonceCache,
57
+ freshNonce,
58
+ signEnvelope,
59
+ verifySignature,
60
+ type VerifyOptions,
61
+ type VerifyOutcome,
62
+ } from "./hmac.js";
63
+
64
+ export {
65
+ PII_MODES,
66
+ isPiiMode,
67
+ PiiPipeline,
68
+ apply,
69
+ type ApplyOptions,
70
+ type PiiAction,
71
+ type PiiCategory,
72
+ type PiiDetection,
73
+ type PiiMode,
74
+ type PiiResult,
75
+ } from "./pii.js";
76
+
77
+ export {
78
+ MAX_TRUST_AGE_MS,
79
+ MIN_TRUST_SCORE,
80
+ TrustEvaluator,
81
+ type PeerState,
82
+ type TrustDecision,
83
+ } from "./trust.js";
84
+
85
+ export {
86
+ DEFAULT_MAX_HOPS,
87
+ PolicyEngine,
88
+ type PolicyConfig,
89
+ type PolicyDecision,
90
+ } from "./policy.js";
91
+
92
+ export {
93
+ DEFAULT_AUDIT_PATH,
94
+ AuditService,
95
+ type AuditDecision,
96
+ type AuditEntry,
97
+ type AuditServiceOpts,
98
+ } from "./audit.js";
99
+
100
+ export {
101
+ DEFAULT_BUDGET_PATH,
102
+ MAX_TOKENS_CEILING,
103
+ MAX_USD_CEILING,
104
+ FederationBudget,
105
+ validateBudgetInput,
106
+ type FederationBudgetOpts,
107
+ type PerPeerBudget,
108
+ type ReserveResult,
109
+ } from "./budget.js";
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Orchestrator
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export interface FederationStatusSnapshot {
116
+ readonly nodeId: string;
117
+ readonly nonceCacheSize: number;
118
+ readonly auditSizeBytes: number;
119
+ readonly auditPath: string;
120
+ readonly budget: {
121
+ readonly path: string;
122
+ readonly perPeer: readonly PerPeerBudget[];
123
+ readonly outstandingReservations: number;
124
+ };
125
+ readonly peers: readonly PeerState[];
126
+ }
127
+
128
+ export interface CreateFederationOpts {
129
+ /** Local node id stamped into audit + status payloads. */
130
+ readonly nodeId: string;
131
+ /** Shared HMAC secret. Sender + receiver MUST match. */
132
+ readonly secret: string;
133
+ /** PII redaction mode applied on `sign()`. */
134
+ readonly piiMode?: PiiMode;
135
+ /** Override audit log path. */
136
+ readonly auditPath?: string;
137
+ /** Override budget JSON path. */
138
+ readonly budgetPath?: string;
139
+ /** Optional explicit PolicyEngine (for custom allowlists/blocklists). */
140
+ readonly policy?: PolicyEngine;
141
+ /** Optional explicit TrustEvaluator (custom allowlist). */
142
+ readonly trust?: TrustEvaluator;
143
+ /** Optional explicit AuditService (test-only override). */
144
+ readonly audit?: AuditService;
145
+ /** Optional explicit FederationBudget (test-only override). */
146
+ readonly budget?: FederationBudget;
147
+ }
148
+
149
+ export interface FederationHandle {
150
+ readonly nodeId: string;
151
+ /** PII redaction mode currently in effect. */
152
+ getPiiMode(): PiiMode;
153
+ setPiiMode(mode: PiiMode): void;
154
+ /** Sign + redact + audit. The resulting envelope is ready to
155
+ * transmit to the peer. */
156
+ sign<T>(input: SignInput<T>): FederationEnvelope<T>;
157
+ /** Verify + admit-or-reject + audit. The caller decides what
158
+ * to do with the result. */
159
+ receive<T>(envelope: FederationEnvelope<T>): ReceiveResult;
160
+ /** Snapshot for the `federation_status` MCP tool. */
161
+ status(): FederationStatusSnapshot;
162
+ /** Direct access to the underlying services — for advanced
163
+ * consumers + tests. */
164
+ readonly services: {
165
+ readonly trust: TrustEvaluator;
166
+ readonly policy: PolicyEngine;
167
+ readonly audit: AuditService;
168
+ readonly budget: FederationBudget;
169
+ readonly nonces: NonceCache;
170
+ };
171
+ }
172
+
173
+ export interface SignInput<T> {
174
+ readonly targetNodeId: string;
175
+ readonly sessionId: string;
176
+ readonly messageType: FederationMessageType;
177
+ readonly payload: T;
178
+ readonly budget?: EnvelopeBudget;
179
+ /** If true, skip PII redaction. Defaults to false. */
180
+ readonly skipPii?: boolean;
181
+ }
182
+
183
+ export interface ReceiveResult {
184
+ readonly trusted: boolean;
185
+ readonly policyAllowed: boolean;
186
+ readonly hmacValid: boolean;
187
+ readonly pii: PiiResult;
188
+ readonly envelope: FederationEnvelope;
189
+ readonly rejectionReason?: string;
190
+ }
191
+
192
+ /** Build the orchestrator handle. */
193
+ export function createFederation(opts: CreateFederationOpts): FederationHandle {
194
+ if (!opts || typeof opts.nodeId !== "string" || opts.nodeId.length === 0) {
195
+ throw new Error("createFederation: nodeId is required");
196
+ }
197
+ if (typeof opts.secret !== "string" || opts.secret.length === 0) {
198
+ throw new Error("createFederation: secret is required");
199
+ }
200
+ const modeRef: { value: PiiMode } = { value: isPiiMode(opts.piiMode) ? opts.piiMode : "soc2" };
201
+
202
+ const trust = opts.trust ?? new TrustEvaluator({ allowlist: [] });
203
+ const policy = opts.policy ?? new PolicyEngine();
204
+ const audit = opts.audit ?? new AuditService({ nodeId: opts.nodeId, path: opts.auditPath });
205
+ const budget = opts.budget ?? new FederationBudget({ path: opts.budgetPath });
206
+ const nonces = new NonceCache();
207
+
208
+ function getPiiMode(): PiiMode { return modeRef.value; }
209
+ function setPiiMode(m: PiiMode): void {
210
+ if (!isPiiMode(m)) throw new Error(`unknown PII mode: ${m}`);
211
+ modeRef.value = m;
212
+ }
213
+
214
+ function sign<T>(input: SignInput<T>): FederationEnvelope<T> {
215
+ const validated = input.budget
216
+ ? validateBudgetInput(input.budget)
217
+ : { ok: true as const, maxTokens: Number.POSITIVE_INFINITY, maxUsd: Number.POSITIVE_INFINITY, maxHops: 8 };
218
+ if (!validated.ok) throw new Error(`sign: bad budget: ${validated.error}`);
219
+
220
+ const env: FederationEnvelope<T> = {
221
+ envelopeId: randomUUID(),
222
+ sourceNodeId: opts.nodeId,
223
+ targetNodeId: input.targetNodeId,
224
+ sessionId: input.sessionId,
225
+ messageType: input.messageType,
226
+ payload: input.payload,
227
+ timestamp: new Date().toISOString(),
228
+ nonce: freshNonce(),
229
+ hmacSignature: "", // filled in below
230
+ piiScanResult: emptyScanResult(),
231
+ budget: {
232
+ maxTokens: validated.maxTokens,
233
+ maxUsd: validated.maxUsd,
234
+ maxHops: validated.maxHops,
235
+ },
236
+ hopCount: 0,
237
+ hopsRemaining: validated.maxHops,
238
+ };
239
+
240
+ // PII redaction over the stringified payload — keep the result
241
+ // in `piiScanResult` so the receiver can audit what was stripped.
242
+ let pii: PiiResult = { transformed: "", detections: [], actionsApplied: [], blocked: false, mode: modeRef.value };
243
+ if (!input.skipPii && typeof input.payload === "string") {
244
+ pii = apply(input.payload, modeRef.value);
245
+ if (pii.blocked) {
246
+ audit.record({
247
+ ts: new Date().toISOString(),
248
+ envelopeId: env.envelopeId,
249
+ sourceNodeId: env.sourceNodeId,
250
+ targetNodeId: env.targetNodeId,
251
+ messageType: env.messageType,
252
+ nonce: env.nonce,
253
+ allowed: false,
254
+ reason: `pii_blocked (mode=${modeRef.value})`,
255
+ layer: "budget",
256
+ });
257
+ }
258
+ }
259
+
260
+ const finalEnv: FederationEnvelope<T> = {
261
+ ...env,
262
+ payload: (pii.transformed ? (pii.transformed as unknown as T) : input.payload),
263
+ piiScanResult: pii.detections.length > 0
264
+ ? {
265
+ scanned: true,
266
+ piiFound: true,
267
+ detections: pii.detections.map((d) => ({
268
+ type: d.category,
269
+ action: "redact",
270
+ confidence: d.confidence,
271
+ count: 1,
272
+ })),
273
+ actionsApplied: pii.actionsApplied.map((a) => a.action),
274
+ scanDurationMs: 0,
275
+ }
276
+ : emptyScanResult(),
277
+ };
278
+
279
+ const signature = signEnvelope(finalEnv, opts.secret);
280
+ const signed: FederationEnvelope<T> = { ...finalEnv, hmacSignature: signature };
281
+
282
+ audit.record({
283
+ ts: signed.timestamp,
284
+ envelopeId: signed.envelopeId,
285
+ sourceNodeId: signed.sourceNodeId,
286
+ targetNodeId: signed.targetNodeId,
287
+ messageType: signed.messageType,
288
+ nonce: signed.nonce,
289
+ allowed: true,
290
+ reason: `signed (pii_mode=${modeRef.value})`,
291
+ layer: "sent",
292
+ });
293
+ return signed;
294
+ }
295
+
296
+ function receive<T>(envelope: FederationEnvelope<T>): ReceiveResult {
297
+ // 1. HMAC + freshness + nonce replay
298
+ const verifyOut = verifySignature(envelope, opts.secret, nonces as unknown as ReadonlySet<string>);
299
+ if (verifyOut.ok) {
300
+ nonces.check(envelope.nonce);
301
+ }
302
+
303
+ const piiScanResult = envelope.piiScanResult ?? emptyScanResult();
304
+ const pii: PiiResult = {
305
+ transformed: typeof envelope.payload === "string" ? envelope.payload : "",
306
+ detections: piiScanResult.detections.map((d) => ({
307
+ category: d.type as never,
308
+ value: "",
309
+ offset: 0,
310
+ confidence: d.confidence,
311
+ })),
312
+ actionsApplied: piiScanResult.actionsApplied.map((a) => ({ category: "email", action: a })),
313
+ blocked: false,
314
+ mode: modeRef.value,
315
+ };
316
+
317
+ let trusted = false;
318
+ let policyAllowed = false;
319
+ let rejectionReason: string | undefined;
320
+
321
+ if (!verifyOut.ok) {
322
+ rejectionReason = `hmac:${verifyOut.reason}`;
323
+ audit.record({
324
+ ts: new Date().toISOString(),
325
+ envelopeId: envelope.envelopeId,
326
+ sourceNodeId: envelope.sourceNodeId,
327
+ targetNodeId: envelope.targetNodeId,
328
+ messageType: envelope.messageType,
329
+ nonce: envelope.nonce,
330
+ allowed: false,
331
+ reason: rejectionReason,
332
+ layer: "hmac",
333
+ });
334
+ } else {
335
+ // 2. Trust
336
+ const peer: PeerState = trust.getPeerState(envelope.sourceNodeId);
337
+ const trustOut = trust.evaluate(envelope, peer);
338
+ trusted = trustOut.allowed;
339
+ if (!trusted) {
340
+ rejectionReason = `trust:${trustOut.reason}`;
341
+ trust.recordOutcome(envelope.sourceNodeId, false);
342
+ audit.record({
343
+ ts: new Date().toISOString(),
344
+ envelopeId: envelope.envelopeId,
345
+ sourceNodeId: envelope.sourceNodeId,
346
+ targetNodeId: envelope.targetNodeId,
347
+ messageType: envelope.messageType,
348
+ nonce: envelope.nonce,
349
+ allowed: false,
350
+ reason: rejectionReason,
351
+ layer: "trust",
352
+ });
353
+ } else {
354
+ trust.recordOutcome(envelope.sourceNodeId, true);
355
+ // 3. Policy (hops + blocklist + message allowlist)
356
+ const polOut: PolicyDecision = policy.enforce(envelope, envelope.budget);
357
+ policyAllowed = polOut.allowed;
358
+ if (!polOut.allowed) {
359
+ rejectionReason = `policy:${polOut.denialReason}`;
360
+ audit.record({
361
+ ts: new Date().toISOString(),
362
+ envelopeId: envelope.envelopeId,
363
+ sourceNodeId: envelope.sourceNodeId,
364
+ targetNodeId: envelope.targetNodeId,
365
+ messageType: envelope.messageType,
366
+ nonce: envelope.nonce,
367
+ allowed: false,
368
+ reason: rejectionReason,
369
+ layer: "policy",
370
+ });
371
+ } else {
372
+ audit.record({
373
+ ts: new Date().toISOString(),
374
+ envelopeId: envelope.envelopeId,
375
+ sourceNodeId: envelope.sourceNodeId,
376
+ targetNodeId: envelope.targetNodeId,
377
+ messageType: envelope.messageType,
378
+ nonce: envelope.nonce,
379
+ allowed: true,
380
+ reason: `accepted (score=${trustOut.score.toFixed(3)})`,
381
+ layer: "received",
382
+ });
383
+ }
384
+ }
385
+ }
386
+
387
+ return {
388
+ trusted,
389
+ policyAllowed,
390
+ hmacValid: verifyOut.ok,
391
+ pii,
392
+ envelope,
393
+ rejectionReason,
394
+ };
395
+ }
396
+
397
+ function status(): FederationStatusSnapshot {
398
+ // Synthesize a peer list from audit history + TrustEvaluator internal map.
399
+ const peerIds = new Set<string>();
400
+ const tail = audit.tail(1000);
401
+ for (const entry of tail) peerIds.add(entry.sourceNodeId);
402
+ const peerSnapshots: PeerState[] = [];
403
+ for (const id of peerIds) peerSnapshots.push(trust.getPeerState(id));
404
+
405
+ return {
406
+ nodeId: opts.nodeId,
407
+ nonceCacheSize: nonces.size(),
408
+ auditSizeBytes: audit.size(),
409
+ auditPath: audit.getPath(),
410
+ budget: {
411
+ path: opts.budgetPath ?? ".harness/federation-budget.json",
412
+ perPeer: budget.snapshot(),
413
+ outstandingReservations: budget.outstanding().length,
414
+ },
415
+ peers: peerSnapshots,
416
+ };
417
+ }
418
+
419
+ return {
420
+ nodeId: opts.nodeId,
421
+ getPiiMode,
422
+ setPiiMode,
423
+ sign,
424
+ receive,
425
+ status,
426
+ services: { trust, policy, audit, budget, nonces },
427
+ };
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // Convenience helper — re-export `canonicalSignablePayload` so callers
432
+ // that want to compute their own signature can import it from the top.
433
+ // ---------------------------------------------------------------------------
434
+
435
+ export { canonicalSignablePayload as buildSignablePayload };
436
+
437
+ // ---------------------------------------------------------------------------
438
+ // Helper for tests — produce a fresh envelope-id.
439
+ // ---------------------------------------------------------------------------
440
+
441
+ export function freshEnvelopeId(): string {
442
+ return randomUUID();
443
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * federation/pii.ts — PII redaction pipeline with 4 compliance modes.
3
+ *
4
+ * F-038 — ported from ruflo
5
+ * `v3/@claude-flow/plugin-agent-federation/src/domain/services/pii-pipeline-service.ts`
6
+ * but simplified per spec ("For v1, focus on redact (regex-based:
7
+ * emails, SSNs, credit cards, IPs)") while keeping the same
8
+ * 4-mode surface (`soc2 | gdpr | hipaa | permissive`).
9
+ *
10
+ * Compliance modes map to the detection + action matrix:
11
+ *
12
+ * soc2 — strict; redact emails, IPs, SSNs, credit cards.
13
+ * gdpr — strict; redact emails, IPs, names, addresses.
14
+ * hipaa — strictest; redact everything we recognise (incl. SSN).
15
+ * permissive — only block the most damaging (credit cards, JWTs,
16
+ * AWS keys, private keys, GitHub tokens); pass through
17
+ * emails / IPs / phone numbers.
18
+ *
19
+ * The transforms are PURE: no I/O, no clocks (other than explicit
20
+ * `now` injection). `apply()` returns a transformed string + the
21
+ * list of detections + a `blocked` flag (true if a hard-blocked
22
+ * PII was found — the caller decides whether to drop the message).
23
+ *
24
+ * No native deps.
25
+ */
26
+
27
+ /** 4 supported compliance modes for the skeleton. */
28
+ export type PiiMode = "soc2" | "gdpr" | "hipaa" | "permissive";
29
+
30
+ export const PII_MODES: readonly PiiMode[] = ["soc2", "gdpr", "hipaa", "permissive"] as const;
31
+
32
+ export function isPiiMode(m: unknown): m is PiiMode {
33
+ return typeof m === "string" && (PII_MODES as readonly string[]).includes(m);
34
+ }
35
+
36
+ export type PiiAction = "redact" | "hash" | "pass" | "block";
37
+
38
+ /** Internal PII categories — finer-grained than the modes. */
39
+ export type PiiCategory =
40
+ | "email"
41
+ | "ssn"
42
+ | "credit_card"
43
+ | "ip_address"
44
+ | "phone"
45
+ | "name"
46
+ | "address"
47
+ | "jwt"
48
+ | "aws_key"
49
+ | "private_key"
50
+ | "github_token";
51
+
52
+ export interface PiiDetection {
53
+ readonly category: PiiCategory;
54
+ readonly value: string;
55
+ readonly offset: number;
56
+ readonly confidence: number;
57
+ }
58
+
59
+ export interface PiiResult {
60
+ readonly transformed: string;
61
+ readonly detections: readonly PiiDetection[];
62
+ readonly actionsApplied: readonly { category: PiiCategory; action: PiiAction }[];
63
+ readonly blocked: boolean;
64
+ readonly mode: PiiMode;
65
+ }
66
+
67
+ interface RegexEntry {
68
+ readonly category: PiiCategory;
69
+ readonly pattern: RegExp;
70
+ readonly confidence: number;
71
+ }
72
+
73
+ /** Detection table — order matters: more-specific patterns come first
74
+ * so we don't double-match. */
75
+ const PATTERNS: readonly RegexEntry[] = [
76
+ // secrets/keys — block in every mode except permissive (which still blocks them)
77
+ { category: "aws_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g, confidence: 0.99 },
78
+ { category: "private_key", pattern: /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+)?PRIVATE\s+KEY-----/g, confidence: 0.99 },
79
+ { category: "github_token", pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}\b/g, confidence: 0.97 },
80
+ { category: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, confidence: 0.95 },
81
+ { category: "credit_card", pattern: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g, confidence: 0.94 },
82
+ { category: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/g, confidence: 0.93 },
83
+ { category: "email", pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, confidence: 0.9 },
84
+ { category: "ip_address", pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, confidence: 0.85 },
85
+ { category: "phone", pattern: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, confidence: 0.8 },
86
+ { category: "name", pattern: /\b(?:Mr\.|Mrs\.|Ms\.|Dr\.)\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b/g, confidence: 0.7 },
87
+ { category: "address", pattern: /\b\d{1,5}\s+(?:[A-Z][a-z]+\s+){1,3}(?:St|Ave|Blvd|Dr|Ln|Rd|Way|Ct)\b/g, confidence: 0.7 },
88
+ ];
89
+
90
+ /** Per-mode action policy. `block` means the envelope is rejected
91
+ * outright (caller drops it); `redact` replaces with
92
+ * `[REDACTED:<category>]`; `hash` replaces with
93
+ * `[HASH:<8-char-prefix>]`; `pass` lets it through unchanged. */
94
+ const MODE_POLICY: Record<PiiMode, Record<PiiCategory, PiiAction>> = {
95
+ soc2: {
96
+ email: "redact", ssn: "redact", credit_card: "block", ip_address: "redact",
97
+ phone: "redact", name: "pass", address: "pass",
98
+ jwt: "block", aws_key: "block", private_key: "block", github_token: "block",
99
+ },
100
+ gdpr: {
101
+ email: "redact", ssn: "redact", credit_card: "block", ip_address: "redact",
102
+ phone: "redact", name: "redact", address: "redact",
103
+ jwt: "block", aws_key: "block", private_key: "block", github_token: "block",
104
+ },
105
+ hipaa: {
106
+ email: "redact", ssn: "redact", credit_card: "block", ip_address: "redact",
107
+ phone: "redact", name: "redact", address: "redact",
108
+ jwt: "block", aws_key: "block", private_key: "block", github_token: "block",
109
+ },
110
+ permissive: {
111
+ email: "pass", ssn: "redact", credit_card: "block", ip_address: "pass",
112
+ phone: "pass", name: "pass", address: "pass",
113
+ jwt: "block", aws_key: "block", private_key: "block", github_token: "block",
114
+ },
115
+ };
116
+
117
+ const REDACT_TOKEN = (cat: PiiCategory) => `[REDACTED:${cat}]`;
118
+ const HASH_PREFIX = 8;
119
+
120
+ function deterministicHash(value: string, salt: string): string {
121
+ // Lightweight, no-native-dep hash: FNV-1a 32-bit, hex-encoded, then
122
+ // salted + first-N chars. The skeleton doesn't need crypto-grade
123
+ // hashing here — the HMAC envelope signature already provides
124
+ // integrity. This is purely for irreversible string replacement.
125
+ let h = 0x811c9dc5;
126
+ for (let i = 0; i < value.length; i++) {
127
+ h ^= value.charCodeAt(i);
128
+ h = Math.imul(h, 0x01000193) >>> 0;
129
+ }
130
+ const salted = `${salt}:${h.toString(16).padStart(8, "0")}`;
131
+ let h2 = 0x811c9dc5;
132
+ for (let i = 0; i < salted.length; i++) {
133
+ h2 ^= salted.charCodeAt(i);
134
+ h2 = Math.imul(h2, 0x01000193) >>> 0;
135
+ }
136
+ return h2.toString(16).padStart(8, "0");
137
+ }
138
+
139
+ export interface ApplyOptions {
140
+ /** Salt for hash() actions. Defaults to "bizar". */
141
+ readonly salt?: string;
142
+ }
143
+
144
+ /** Pure-function PII pipeline. Walks the regex table, decides an
145
+ * action per category under the chosen mode, applies the
146
+ * replacement. Returns enough metadata for the audit service to
147
+ * log what was stripped. */
148
+ export function apply(
149
+ text: string,
150
+ mode: PiiMode,
151
+ opts: ApplyOptions = {},
152
+ ): PiiResult {
153
+ if (!isPiiMode(mode)) {
154
+ throw new Error(`apply: unknown mode '${String(mode)}'`);
155
+ }
156
+ const salt = opts.salt ?? "bizar";
157
+ const policy = MODE_POLICY[mode];
158
+ const detections: PiiDetection[] = [];
159
+ const actionsApplied: { category: PiiCategory; action: PiiAction }[] = [];
160
+ let blocked = false;
161
+ let transformed = text;
162
+
163
+ // Two-phase: (1) collect ALL detections across ALL categories first,
164
+ // (2) apply per-category actions in reverse-offset order so a
165
+ // `block` on one category doesn't hide detections in later ones.
166
+ for (const entry of PATTERNS) {
167
+ const regex = new RegExp(entry.pattern.source, entry.pattern.flags);
168
+ let m: RegExpExecArray | null;
169
+ while ((m = regex.exec(transformed)) !== null) {
170
+ detections.push({
171
+ category: entry.category,
172
+ value: m[0],
173
+ offset: m.index,
174
+ confidence: entry.confidence,
175
+ });
176
+ }
177
+ }
178
+
179
+ // Now compute actions per category. `block` short-circuits the
180
+ // whole transform but we still report every detection.
181
+ for (const entry of PATTERNS) {
182
+ const inCategory = detections.filter((d) => d.category === entry.category);
183
+ if (inCategory.length === 0) continue;
184
+
185
+ const action = policy[entry.category];
186
+ actionsApplied.push({ category: entry.category, action });
187
+ if (action === "block") {
188
+ blocked = true;
189
+ continue;
190
+ }
191
+ if (action === "pass") continue;
192
+
193
+ // Apply right-to-left so earlier offsets remain valid.
194
+ for (const hit of inCategory.reverse()) {
195
+ const replacement =
196
+ action === "redact"
197
+ ? REDACT_TOKEN(entry.category)
198
+ : `[HASH:${deterministicHash(hit.value, salt).slice(0, HASH_PREFIX)}]`;
199
+ transformed =
200
+ transformed.slice(0, hit.offset) +
201
+ replacement +
202
+ transformed.slice(hit.offset + hit.value.length);
203
+ }
204
+ }
205
+
206
+ if (blocked) transformed = "";
207
+
208
+ return {
209
+ transformed,
210
+ detections,
211
+ actionsApplied,
212
+ blocked,
213
+ mode,
214
+ };
215
+ }
216
+
217
+ /** PiiPipeline — class wrapper for parity with ruflo's
218
+ * PIIPipelineService shape. Holds a default mode + salt, exposes
219
+ * `process(text)` and `setMode(mode)`. */
220
+ export class PiiPipeline {
221
+ private mode: PiiMode;
222
+ private readonly salt: string;
223
+
224
+ constructor(opts: { mode?: PiiMode; salt?: string } = {}) {
225
+ this.mode = opts.mode ?? "soc2";
226
+ this.salt = opts.salt ?? "bizar";
227
+ }
228
+
229
+ setMode(mode: PiiMode): void {
230
+ if (!isPiiMode(mode)) throw new Error(`PiiPipeline: unknown mode '${mode}'`);
231
+ this.mode = mode;
232
+ }
233
+
234
+ getMode(): PiiMode {
235
+ return this.mode;
236
+ }
237
+
238
+ process(text: string): PiiResult {
239
+ return apply(text, this.mode, { salt: this.salt });
240
+ }
241
+ }