diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b795e37..339c45b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,9 +5,9 @@ on:
workflow_dispatch:
inputs:
version:
- description: 'Package version (e.g. 0.2.0)'
+ description: 'Package version (e.g. 0.4.0)'
required: true
- default: '0.2.0'
+ default: '0.4.0'
permissions:
contents: write # create the GitHub Release
@@ -39,12 +39,13 @@ jobs:
- run: dotnet test -c Release --no-build
- name: Pack
- run: >
- dotnet pack src/RedactWire/RedactWire.csproj
- -c Release --no-build
- -p:Version=${{ steps.ver.outputs.version }}
- -p:ContinuousIntegrationBuild=true
- -o artifacts
+ run: |
+ for proj in src/RedactWire/RedactWire.csproj src/RedactWire.Structured/RedactWire.Structured.csproj; do
+ dotnet pack "$proj" -c Release --no-build \
+ -p:Version=${{ steps.ver.outputs.version }} \
+ -p:ContinuousIntegrationBuild=true \
+ -o artifacts
+ done
- name: NuGet login (Trusted Publishing, OIDC)
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..48f6955
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,36 @@
+# Changelog
+
+All notable changes to RedactWire. This project is pre-1.0 — breaking changes can land in
+minor versions.
+
+## 0.4.0
+
+### ⚠️ Breaking
+
+- **Structured scanning (JSON / XML / object) moved out of core** into a new opt-in package
+ **`RedactWire.Structured`**. The core `RedactWire` package no longer depends on
+ `System.Text.Json` — string-only consumers carry one less dependency.
+- **`Redactor.DetectJson` / `DetectXml` / `DetectObject` removed.** Reference the
+ `RedactWire.Structured` package and call them on a detector instead:
+ ```csharp
+ // before
+ Redactor.DetectJson(json);
+ // after (after `dotnet add package RedactWire.Structured`)
+ Redactor.Default.DetectJson(json); // shared static detector
+ detector.DetectJson(json, culture); // or your own builder/detector
+ ```
+ `DetectJson/DetectXml/DetectObject` are extension methods on `PiiDetector` (namespace
+ `RedactWire`), so they light up automatically once the add-on is referenced. Method
+ signatures and detection behavior are unchanged — this is a packaging move only.
+
+### Added
+
+- **Secret detection** — `AddSecretDetection()` enables provider-prefixed API keys/tokens
+ (OpenAI, AWS access-key-id, GitHub, Stripe, Slack, Google, SendGrid, npm), JWT and PEM
+ private keys, as `PiiType.Secret` (provider in `Subtype`). On by default in `Redactor`.
+- **Remove / replace built-in rules** — `RemoveRule(type[, subtype])`, `RemoveRules(predicate)`,
+ `ReplaceInvariantRule(rule)`, `ReplaceRule(culture, rule)`. `IPiiRule` now exposes `Subtype`.
+- **Region-based culture resolution** — a country's languages share one pack
+ (`en-IN`/`hi-IN`, `en-CA`/`fr-CA`, Singapore's four languages); same language across
+ countries stays distinct (`zh-CN`/`zh-TW`/`zh-HK`/`zh-SG`).
+- More country packs (now 50+), incl. Malaysia (MyKad) and Australia Medicare.
diff --git a/CLAUDE.md b/CLAUDE.md
index 365c635..9231bea 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -22,14 +22,15 @@
namespace). Use `Redactor.Detect/HasPii/Redact` for zero-config calls.
## Architecture
-- Core library (`src/RedactWire`) multi-targets **net8.0;netstandard2.0**. Dependencies:
- `Microsoft.Extensions.DependencyInjection.Abstractions` (contracts-only, both TFMs) so
- the DI bootstrap ships in core; `System.Text.Json` only on netstandard2.0 (built into
- net8). We deliberately keep the package count low — no separate extension NuGets.
-- Structured scanning lives in `Structured/`: `DetectJson` (System.Text.Json),
- `DetectXml` (System.Xml, **XXE-safe**: DtdProcessing.Prohibit + null resolver),
- `DetectObject` (reflection; cycle detection, depth cap, collections, skips framework
- types). All are extension methods on `PiiDetector`; string values only.
+- Core library (`src/RedactWire`) targets **netstandard2.0**; its only dependency is
+ `Microsoft.Extensions.DependencyInjection.Abstractions` (contracts-only) so the DI
+ bootstrap ships in core. **No `System.Text.Json`** — keep it that way.
+- Structured scanning is a separate opt-in package, **`src/RedactWire.Structured`**
+ (depends on core + `System.Text.Json`): `DetectJson` (System.Text.Json), `DetectXml`
+ (System.Xml, **XXE-safe**: DtdProcessing.Prohibit + null resolver), `DetectObject`
+ (reflection; cycle detection, depth cap, collections, skips framework types). All are
+ extension methods on `PiiDetector` in namespace `RedactWire`; string values only. The
+ static `Redactor` has NO structured forwarders — call `Redactor.Default.DetectJson(...)`.
- Three ways to use it:
1. Static `Redactor` facade — zero bootstrap.
2. `PiiDetectorBuilder` — manual configuration.
diff --git a/README.md b/README.md
index 1d1171c..5441b93 100644
--- a/README.md
+++ b/README.md
@@ -37,9 +37,9 @@ that drives overlap resolution.
- **Severity model** (`Critical > High > Medium > Low`): the primary key for overlap
resolution — a higher-severity match wins over an overlapping lower-severity one,
even at lower confidence.
-- **Structured scanning:** detect PII inside **JSON**, **XML**, and **object graphs**,
- with each hit located by JSONPath / XPath / property path. Standard libraries only
- (`System.Text.Json`, `System.Xml`, reflection); XML is parsed safely (no XXE).
+- **Structured scanning** (opt-in **`RedactWire.Structured`** add-on): detect PII inside
+ **JSON**, **XML**, and **object graphs**, each hit located by JSONPath / XPath / property
+ path. Kept out of core so plain-string users don't pull `System.Text.Json`.
- **Redaction:** mask (length-preserving), remove, type-label, or a custom replacement.
- **Honest "no PII":** a requested culture with no rule pack is flagged
`Supported = false` instead of silently "passing".
@@ -180,17 +180,26 @@ strings need entropy/context and are a later phase. See [`docs/rules/secrets.md`
## Structured scanning (JSON / XML / objects)
-Scan structured data and get each match with its location. Available on `PiiDetector`
-(and the static `Redactor`).
+Add-on package — keeps `System.Text.Json` out of the core:
+
+```bash
+dotnet add package RedactWire.Structured
+```
+
+Scan structured data and get each match with its location. `DetectJson/DetectXml/DetectObject`
+are extension methods on `PiiDetector`, so they light up once the add-on is referenced
+(use `Redactor.Default.DetectJson(...)` for the shared static detector).
```csharp
-foreach (var h in Redactor.DetectJson("""{"user":{"email":"a@b.com"},"ssn":"123-45-6789"}"""))
+var detector = PiiDetectorBuilder.CreateDefault().AddCulture(new CultureInfo("en-US")).Build();
+
+foreach (var h in detector.DetectJson("""{"user":{"email":"a@b.com"},"ssn":"123-45-6789"}"""))
Console.WriteLine($"{h.Path}: {h.Match.Type}");
// $.user.email: Email
// $.ssn: SocialSecurity
-Redactor.DetectXml(""); // -> /u/@email
-Redactor.DetectObject(myPoco); // -> User.Contacts[0].Phone
+detector.DetectXml(""); // -> /u/@email
+detector.DetectObject(myPoco); // -> User.Contacts[0].Phone
```
Notes:
@@ -198,8 +207,8 @@ Notes:
- XML is parsed with DTD processing prohibited and no external resolver — **XXE-safe**.
- Object scanning walks public properties/fields, handles collections/dictionaries,
detects cycles, caps depth, and does not recurse into framework types.
-- `System.Text.Json` is built into modern .NET; on `netstandard2.0`/.NET Framework it
- comes as a NuGet dependency (the library multi-targets so net8+ stays clean).
+- The add-on uses `System.Text.Json` (JSON) and BCL `System.Xml` / reflection (XML, objects).
+ This is the only reason `System.Text.Json` is split out of the core package.
## Extending
@@ -284,7 +293,8 @@ yield return new RuleHit(value, start, length, 0.9,
| Path | What |
|---|---|
-| `src/RedactWire` | the library (+ DI bootstrap) |
+| `src/RedactWire` | the core library (detection / validation / redaction + DI bootstrap) |
+| `src/RedactWire.Structured` | JSON/XML/object scanning add-on (`System.Text.Json`) |
| `src/RedactWire.Cli` | command-line scanner / redactor |
| `samples/RedactWire.Sample.Web` | ASP.NET Core Razor Pages PII tester |
| `tests/RedactWire.Tests` | xUnit tests |
diff --git a/RedactWire.slnx b/RedactWire.slnx
index 4b22299..96dbe58 100644
--- a/RedactWire.slnx
+++ b/RedactWire.slnx
@@ -4,9 +4,11 @@
+
+
diff --git a/samples/RedactWire.Sample.Web/Pages/Shared/_Layout.cshtml b/samples/RedactWire.Sample.Web/Pages/Shared/_Layout.cshtml
index 2ff9ff9..bd1ba3b 100644
--- a/samples/RedactWire.Sample.Web/Pages/Shared/_Layout.cshtml
+++ b/samples/RedactWire.Sample.Web/Pages/Shared/_Layout.cshtml
@@ -22,6 +22,12 @@