@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.
- package/.claude/hooks/sessionend-recall.mjs +3 -3
- package/.claude/hooks/worker-suggest.mjs +110 -0
- package/.claude/settings.json +9 -0
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
- package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
- package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
- package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
- package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
- package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
- package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
- package/bizar-dash/dist/index.html +12 -10
- package/bizar-dash/dist/mobile.html +7 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +11 -0
- package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
- package/bizar-dash/src/server/memory-distillation.mjs +296 -0
- package/bizar-dash/src/server/routes/distill.mjs +129 -0
- package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
- package/bizar-dash/src/web/App.tsx +4 -0
- package/bizar-dash/src/web/components/Tag.tsx +18 -5
- package/bizar-dash/src/web/components/Topbar.tsx +3 -0
- package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
- package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
- package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
- package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
- package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
- package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
- package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
- package/bizar-dash/src/web/lib/types.ts +25 -0
- package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
- package/bizar-dash/src/web/styles/main.css +251 -0
- package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
- package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
- package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
- package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
- package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
- package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
- package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
- package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
- package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
- package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
- package/bizar-dash/src/web/ui/controls/controls.css +454 -0
- package/bizar-dash/src/web/ui/controls/index.ts +33 -0
- package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
- package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
- package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
- package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
- package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
- package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
- package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
- package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
- package/bizar-dash/src/web/ui/data/data.css +535 -0
- package/bizar-dash/src/web/ui/data/index.ts +31 -0
- package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
- package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
- package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
- package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
- package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
- package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
- package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
- package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
- package/bizar-dash/src/web/ui/index.ts +56 -0
- package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
- package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
- package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
- package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
- package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
- package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
- package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
- package/bizar-dash/src/web/ui/layout/index.ts +33 -0
- package/bizar-dash/src/web/ui/layout/layout.css +413 -0
- package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
- package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
- package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
- package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
- package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
- package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
- package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
- package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
- package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
- package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
- package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
- package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
- package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
- package/bizar-dash/src/web/ui/styles/globals.css +86 -0
- package/bizar-dash/src/web/ui/styles/reset.css +125 -0
- package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
- package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
- package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
- package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
- package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
- package/bizar-dash/src/web/views/Agents.tsx +506 -266
- package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
- package/bizar-dash/src/web/views/Overview.tsx +996 -448
- package/bizar-dash/src/web/views/Tasks.tsx +277 -273
- package/bizar-dash/tests/communication-log.test.tsx +67 -0
- package/bizar-dash/tests/dep-graph.test.tsx +40 -0
- package/bizar-dash/tests/event-log.test.tsx +61 -0
- package/bizar-dash/tests/goal-input.test.tsx +72 -0
- package/bizar-dash/tests/goap-planner.test.ts +68 -0
- package/bizar-dash/tests/quality-gates.test.tsx +59 -0
- package/bizar-dash/tests/setup.ts +31 -1
- package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
- package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
- package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
- package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
- package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
- package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
- package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
- package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
- package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
- package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
- package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
- package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
- package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
- package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
- package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
- package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
- package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
- package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
- package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
- package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
- package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
- package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
- package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
- package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
- package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
- package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
- package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
- package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
- package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
- package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
- package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
- package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
- package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
- package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
- package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
- package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
- package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
- package/bizar-dash/tests/views/Agents.test.tsx +359 -0
- package/bizar-dash/tests/views/Overview.test.tsx +413 -0
- package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
- package/bizar-dash/vitest.config.ts +3 -1
- package/cli/__tests__/cost-gate.test.mjs +301 -0
- package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
- package/cli/bin.mjs +38 -0
- package/cli/commands/claim.mjs +282 -0
- package/cli/commands/cost.mjs +279 -0
- package/cli/cost-gate.mjs +501 -0
- package/cli/feature-list-bridge.mjs +400 -0
- package/cli/memory-constants.mjs +17 -0
- package/cli/worker-dispatcher.mjs +259 -0
- package/cli/worker-dispatcher.test.mjs +204 -0
- package/config/trigger-patterns.json +186 -0
- package/package.json +2 -2
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/agent-registry.ts +304 -0
- package/packages/sdk/src/consensus/byzantine.ts +587 -0
- package/packages/sdk/src/consensus/index.ts +147 -0
- package/packages/sdk/src/consensus/queen.ts +173 -0
- package/packages/sdk/src/consensus/types.ts +199 -0
- package/packages/sdk/src/federation/audit.ts +156 -0
- package/packages/sdk/src/federation/budget.ts +269 -0
- package/packages/sdk/src/federation/envelope.ts +144 -0
- package/packages/sdk/src/federation/hmac.ts +160 -0
- package/packages/sdk/src/federation/index.ts +443 -0
- package/packages/sdk/src/federation/pii.ts +241 -0
- package/packages/sdk/src/federation/policy.ts +112 -0
- package/packages/sdk/src/federation/trust.ts +146 -0
- package/packages/sdk/src/index.ts +67 -0
- package/packages/sdk/src/mcp/server.ts +351 -1
- package/packages/sdk/src/router/codemod-intent.ts +125 -0
- package/packages/sdk/src/router/index.ts +247 -0
- package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
- package/packages/sdk/src/router/memory-distillation.ts +249 -0
- package/packages/sdk/src/router/model-router.ts +235 -0
- package/packages/sdk/src/router/q-learning-router.ts +304 -0
- package/packages/sdk/src/swarm-topology.ts +279 -0
- package/packages/sdk/tests/agent-registry.test.ts +285 -0
- package/packages/sdk/tests/consensus.test.ts +376 -0
- package/packages/sdk/tests/federation/audit.test.ts +164 -0
- package/packages/sdk/tests/federation/budget.test.ts +189 -0
- package/packages/sdk/tests/federation/envelope.test.ts +120 -0
- package/packages/sdk/tests/federation/hmac.test.ts +194 -0
- package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
- package/packages/sdk/tests/federation/pii.test.ts +106 -0
- package/packages/sdk/tests/federation/policy.test.ts +140 -0
- package/packages/sdk/tests/federation/trust.test.ts +124 -0
- package/packages/sdk/tests/mcp-tools.test.ts +204 -0
- package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
- package/packages/sdk/tests/model-router.test.mjs +108 -0
- package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
- package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
- package/packages/sdk/tests/router.test.mjs +91 -0
- package/packages/sdk/tests/sdk.test.mjs +26 -2
- package/packages/sdk/tests/swarm-topology.test.ts +258 -0
- package/scripts/bh-full-e2e.mjs +11 -15
- package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
- package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
- package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
- package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
- package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
- package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
- package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
- package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
- package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
- 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
|
+
}
|