@anuma/sdk 1.0.0 → 1.1.1
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/README.md +54 -53
- package/dist/api/spec.cjs +30 -0
- package/dist/api/spec.d.mts +3 -0
- package/dist/api/spec.d.ts +3 -0
- package/dist/api/spec.mjs +12 -0
- package/dist/chunk-4ETHWSXJ.mjs +25 -0
- package/dist/client/index.cjs +2344 -0
- package/dist/client/index.d.mts +10368 -0
- package/dist/client/index.d.ts +10368 -0
- package/dist/client/index.mjs +2168 -0
- package/dist/clientConfig-WITO4T6L.mjs +12 -0
- package/dist/constants/index.cjs +30 -0
- package/dist/constants/index.d.mts +20 -0
- package/dist/constants/index.d.ts +20 -0
- package/dist/constants/index.mjs +5 -0
- package/dist/design/index.cjs +1523 -0
- package/dist/design/index.d.mts +561 -0
- package/dist/design/index.d.ts +561 -0
- package/dist/design/index.mjs +1468 -0
- package/dist/expo/chunk-3HZFU3PV.mjs +5283 -0
- package/dist/expo/chunk-DFVCS3QQ.mjs +246 -0
- package/dist/{react/chunk-MJJIYFAX.mjs → expo/chunk-GYKJOWEO.mjs} +13 -1
- package/dist/expo/chunk-Y7XT2VNQ.mjs +646 -0
- package/dist/expo/clientConfig-VPOGQRNP.mjs +12 -0
- package/dist/expo/consolidate-HEMQGOUD.mjs +131 -0
- package/dist/expo/index.cjs +16904 -6750
- package/dist/expo/index.d.mts +5755 -1474
- package/dist/expo/index.d.ts +5755 -1474
- package/dist/expo/index.mjs +9718 -6182
- package/dist/expo/recall-MCB3TVBW.mjs +8 -0
- package/dist/index.cjs +2992 -42
- package/dist/index.d.mts +7719 -1835
- package/dist/index.d.ts +7719 -1835
- package/dist/index.mjs +2859 -39
- package/dist/next/index.cjs +6 -2
- package/dist/next/index.d.mts +24 -1
- package/dist/next/index.d.ts +24 -1
- package/dist/next/index.mjs +6 -2
- package/dist/pii/transformers.cjs +126 -0
- package/dist/pii/transformers.d.mts +103 -0
- package/dist/pii/transformers.d.ts +103 -0
- package/dist/pii/transformers.mjs +90 -0
- package/dist/react/chunk-DFVCS3QQ.mjs +246 -0
- package/dist/react/chunk-ELDXIXRE.mjs +5656 -0
- package/dist/{expo/chunk-MJJIYFAX.mjs → react/chunk-GYKJOWEO.mjs} +13 -1
- package/dist/react/chunk-TP6CJZ25.mjs +645 -0
- package/dist/react/clientConfig-VPOGQRNP.mjs +12 -0
- package/dist/react/consolidate-IJH7LD2T.mjs +131 -0
- package/dist/react/index.cjs +25683 -11143
- package/dist/react/index.d.mts +7399 -1393
- package/dist/react/index.d.ts +7399 -1393
- package/dist/react/index.mjs +16709 -9196
- package/dist/react/recall-CSOMIT4H.mjs +8 -0
- package/dist/server/chunk-FMGZTVXQ.mjs +62 -0
- package/dist/server/chunk-G3DC7QBH.mjs +19457 -0
- package/dist/server/chunk-PXN5Q3MD.mjs +643 -0
- package/dist/server/chunk-RCCKRTBI.mjs +230 -0
- package/dist/server/clientConfig-GMADOBPK.mjs +12 -0
- package/dist/server/consolidate-A2ZIX5WF.mjs +131 -0
- package/dist/server/index.cjs +32161 -0
- package/dist/server/index.d.mts +7319 -0
- package/dist/server/index.d.ts +7319 -0
- package/dist/server/index.mjs +11217 -0
- package/dist/server/recall-LFYTTIIY.mjs +8 -0
- package/dist/tools/index.cjs +12682 -34
- package/dist/tools/index.d.mts +2733 -18
- package/dist/tools/index.d.ts +2733 -18
- package/dist/tools/index.mjs +12565 -33
- package/dist/utils/index.cjs +165 -0
- package/dist/utils/index.d.mts +14 -0
- package/dist/utils/index.d.ts +14 -0
- package/dist/utils/index.mjs +145 -0
- package/dist/vercel/index.d.mts +0 -3
- package/dist/vercel/index.d.ts +0 -3
- package/package.json +127 -16
- package/dist/expo/chunk-LJYAMK62.mjs +0 -25
- package/dist/expo/chunk-PJCZO4BQ.mjs +0 -25
- package/dist/expo/clientConfig-2MI4KULF.mjs +0 -10
- package/dist/expo/clientConfig-QDAXFL3W.mjs +0 -10
- package/dist/expo/clientConfig-R4IOW7I2.mjs +0 -10
- package/dist/react/chunk-LJYAMK62.mjs +0 -25
- package/dist/react/chunk-PJCZO4BQ.mjs +0 -25
- package/dist/react/clientConfig-2MI4KULF.mjs +0 -10
- package/dist/react/clientConfig-QDAXFL3W.mjs +0 -10
- package/dist/react/clientConfig-R4IOW7I2.mjs +0 -10
- package/dist/tools/chunk-KDFGY4SK.mjs +0 -13
- package/dist/tools/clientConfig-RMDOT5YM.mjs +0 -10
package/dist/next/index.cjs
CHANGED
|
@@ -27,8 +27,12 @@ var withAnuma = (nextConfig = {}) => {
|
|
|
27
27
|
return {
|
|
28
28
|
...nextConfig,
|
|
29
29
|
serverExternalPackages: [
|
|
30
|
-
...nextConfig.serverExternalPackages
|
|
31
|
-
"sharp"
|
|
30
|
+
...nextConfig.serverExternalPackages ?? [],
|
|
31
|
+
"sharp",
|
|
32
|
+
// exceljs pulls in unzipper → fstream which calls process.umask() at
|
|
33
|
+
// module init time, crashing Cloudflare Workers and other edge runtimes.
|
|
34
|
+
// Externalizing prevents the server bundler from including it in SSR.
|
|
35
|
+
"exceljs"
|
|
32
36
|
],
|
|
33
37
|
webpack: (config, options) => {
|
|
34
38
|
const { isServer } = options;
|
package/dist/next/index.d.mts
CHANGED
|
@@ -18,6 +18,29 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @module
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
/** Minimal Webpack configuration shape used by the SDK plugin. */
|
|
22
|
+
interface WebpackConfig {
|
|
23
|
+
resolve: {
|
|
24
|
+
alias: Record<string, unknown>;
|
|
25
|
+
fallback: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
module: {
|
|
28
|
+
rules: Array<Record<string, unknown>>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Webpack compiler options passed by Next.js. */
|
|
32
|
+
interface WebpackOptions {
|
|
33
|
+
isServer: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Next.js configuration object shape used by the SDK plugin. */
|
|
36
|
+
interface NextConfig {
|
|
37
|
+
serverExternalPackages?: string[];
|
|
38
|
+
webpack?: (config: WebpackConfig, options: WebpackOptions) => WebpackConfig;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
declare const withAnuma: (nextConfig?: NextConfig) => {
|
|
42
|
+
serverExternalPackages: string[];
|
|
43
|
+
webpack: (config: WebpackConfig, options: WebpackOptions) => WebpackConfig;
|
|
44
|
+
};
|
|
22
45
|
|
|
23
46
|
export { withAnuma };
|
package/dist/next/index.d.ts
CHANGED
|
@@ -18,6 +18,29 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @module
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
/** Minimal Webpack configuration shape used by the SDK plugin. */
|
|
22
|
+
interface WebpackConfig {
|
|
23
|
+
resolve: {
|
|
24
|
+
alias: Record<string, unknown>;
|
|
25
|
+
fallback: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
module: {
|
|
28
|
+
rules: Array<Record<string, unknown>>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Webpack compiler options passed by Next.js. */
|
|
32
|
+
interface WebpackOptions {
|
|
33
|
+
isServer: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Next.js configuration object shape used by the SDK plugin. */
|
|
36
|
+
interface NextConfig {
|
|
37
|
+
serverExternalPackages?: string[];
|
|
38
|
+
webpack?: (config: WebpackConfig, options: WebpackOptions) => WebpackConfig;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
declare const withAnuma: (nextConfig?: NextConfig) => {
|
|
42
|
+
serverExternalPackages: string[];
|
|
43
|
+
webpack: (config: WebpackConfig, options: WebpackOptions) => WebpackConfig;
|
|
44
|
+
};
|
|
22
45
|
|
|
23
46
|
export { withAnuma };
|
package/dist/next/index.mjs
CHANGED
|
@@ -3,8 +3,12 @@ var withAnuma = (nextConfig = {}) => {
|
|
|
3
3
|
return {
|
|
4
4
|
...nextConfig,
|
|
5
5
|
serverExternalPackages: [
|
|
6
|
-
...nextConfig.serverExternalPackages
|
|
7
|
-
"sharp"
|
|
6
|
+
...nextConfig.serverExternalPackages ?? [],
|
|
7
|
+
"sharp",
|
|
8
|
+
// exceljs pulls in unzipper → fstream which calls process.umask() at
|
|
9
|
+
// module init time, crashing Cloudflare Workers and other edge runtimes.
|
|
10
|
+
// Externalizing prevents the server bundler from including it in SSR.
|
|
11
|
+
"exceljs"
|
|
8
12
|
],
|
|
9
13
|
webpack: (config, options) => {
|
|
10
14
|
const { isServer } = options;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/lib/pii/detectors/transformers.ts
|
|
31
|
+
var transformers_exports = {};
|
|
32
|
+
__export(transformers_exports, {
|
|
33
|
+
aggregateTokens: () => aggregateTokens,
|
|
34
|
+
createTransformersNerDetector: () => createTransformersNerDetector
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(transformers_exports);
|
|
37
|
+
var DEFAULT_MODEL = "Xenova/bert-base-NER";
|
|
38
|
+
var DEFAULT_TAG_MAP = {
|
|
39
|
+
PER: "PERSON",
|
|
40
|
+
LOC: "LOCATION",
|
|
41
|
+
ORG: "ORG",
|
|
42
|
+
MISC: void 0
|
|
43
|
+
};
|
|
44
|
+
function placeTokens(tokens, text) {
|
|
45
|
+
const placed = [];
|
|
46
|
+
let cursor = 0;
|
|
47
|
+
for (const t of tokens) {
|
|
48
|
+
const surface = t.word.startsWith("##") ? t.word.slice(2) : t.word;
|
|
49
|
+
const start = text.indexOf(surface, cursor);
|
|
50
|
+
if (start < 0) {
|
|
51
|
+
placed.push({ ...t, start: -1, end: -1 });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const end = start + surface.length;
|
|
55
|
+
placed.push({ ...t, start, end });
|
|
56
|
+
cursor = end;
|
|
57
|
+
}
|
|
58
|
+
return placed;
|
|
59
|
+
}
|
|
60
|
+
function aggregateTokens(tokens, text, tagMap, minScore) {
|
|
61
|
+
const placed = placeTokens(tokens, text);
|
|
62
|
+
const spans = [];
|
|
63
|
+
let cur = null;
|
|
64
|
+
const flush = () => {
|
|
65
|
+
if (!cur) return;
|
|
66
|
+
const category = cur.valid ? tagMap[cur.base] : void 0;
|
|
67
|
+
if (category) {
|
|
68
|
+
const score = cur.scores.reduce((a, b) => a + b, 0) / cur.scores.length;
|
|
69
|
+
if (score >= minScore) spans.push({ start: cur.start, end: cur.end, category, score });
|
|
70
|
+
}
|
|
71
|
+
cur = null;
|
|
72
|
+
};
|
|
73
|
+
for (const t of placed) {
|
|
74
|
+
const dash = t.entity.indexOf("-");
|
|
75
|
+
const prefix = dash >= 0 ? t.entity.slice(0, dash) : t.entity;
|
|
76
|
+
const base = dash >= 0 ? t.entity.slice(dash + 1) : t.entity;
|
|
77
|
+
const isContinuation = t.word.startsWith("##");
|
|
78
|
+
const unplaceable = t.start < 0;
|
|
79
|
+
const continues = cur !== null && base === cur.base && (prefix === "I" || isContinuation) && (unplaceable || t.start - cur.end <= 1);
|
|
80
|
+
if (continues) {
|
|
81
|
+
if (unplaceable) cur.valid = false;
|
|
82
|
+
else {
|
|
83
|
+
cur.end = t.end;
|
|
84
|
+
cur.scores.push(t.score);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
flush();
|
|
88
|
+
cur = unplaceable ? null : { base, start: t.start, end: t.end, scores: [t.score], valid: true };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
flush();
|
|
92
|
+
return spans;
|
|
93
|
+
}
|
|
94
|
+
function createTransformersNerDetector(options = {}) {
|
|
95
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
96
|
+
const dtype = options.dtype ?? "q8";
|
|
97
|
+
const minScore = options.minScore ?? 0.5;
|
|
98
|
+
const tagMap = options.tagMap ?? DEFAULT_TAG_MAP;
|
|
99
|
+
let pipePromise = null;
|
|
100
|
+
const load = () => {
|
|
101
|
+
if (!pipePromise) {
|
|
102
|
+
const loading = (async () => {
|
|
103
|
+
const transformers = await import("@huggingface/transformers");
|
|
104
|
+
return transformers.pipeline("token-classification", model, { dtype });
|
|
105
|
+
})();
|
|
106
|
+
pipePromise = loading;
|
|
107
|
+
loading.catch(() => {
|
|
108
|
+
if (pipePromise === loading) pipePromise = null;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return pipePromise;
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
async detect(text) {
|
|
115
|
+
if (!text) return [];
|
|
116
|
+
const classify = await load();
|
|
117
|
+
const tokens = await classify(text);
|
|
118
|
+
return aggregateTokens(tokens, text, tagMap, minScore);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
123
|
+
0 && (module.exports = {
|
|
124
|
+
aggregateTokens,
|
|
125
|
+
createTransformersNerDetector
|
|
126
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII detection patterns for structured data types.
|
|
3
|
+
*
|
|
4
|
+
* Each pattern has a category (used for placeholder tags like [EMAIL_1]),
|
|
5
|
+
* a regex, and an optional validator for reducing false positives.
|
|
6
|
+
*/
|
|
7
|
+
type PiiCategory = "EMAIL" | "PHONE" | "SSN" | "CREDIT_CARD" | "IP_ADDRESS" | "API_KEY" | "US_ADDRESS" | "DATE_OF_BIRTH";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NER detector contract for PII redaction.
|
|
11
|
+
*
|
|
12
|
+
* The regex layer in {@link ./patterns} handles *structured* PII (email, phone,
|
|
13
|
+
* SSN, credit card, …). It cannot detect *unstructured* PII — person names,
|
|
14
|
+
* locations, organizations — which need a named-entity-recognition model.
|
|
15
|
+
*
|
|
16
|
+
* Rather than bundle a model (and a runtime) into the SDK, we define a small
|
|
17
|
+
* runtime-agnostic contract: a {@link NerDetector} takes text and returns the
|
|
18
|
+
* spans it found. Consumers supply an implementation appropriate to their
|
|
19
|
+
* platform — a Transformers.js model in the browser/Node (see
|
|
20
|
+
* `./detectors/transformers`), an `onnxruntime-react-native` model on mobile,
|
|
21
|
+
* a remote service, or a fake in tests. The SDK keeps detection *on-device* by
|
|
22
|
+
* never calling out itself; it only consumes the spans a detector returns and
|
|
23
|
+
* merges them into the existing placeholder map.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A character-offset span of detected PII. `start`/`end` are indices into the
|
|
28
|
+
* text passed to {@link NerDetector.detect} (`end` exclusive). `category`
|
|
29
|
+
* becomes the placeholder tag prefix (e.g. "PERSON" → `[PERSON_1]`); it may be
|
|
30
|
+
* any string, conventionally `PERSON` / `LOCATION` / `ORG` for NER entities.
|
|
31
|
+
*/
|
|
32
|
+
interface PiiSpan {
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
category: PiiCategory | (string & {});
|
|
36
|
+
/** Optional model confidence in [0,1]; informational (filter in the detector). */
|
|
37
|
+
score?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detects unstructured PII in text. Implementations run a model (or service)
|
|
41
|
+
* and return spans. The SDK defensively clamps spans to the text bounds and
|
|
42
|
+
* snaps them to whole-word boundaries before use, so a detector returning a
|
|
43
|
+
* sub-word span cannot corrupt the output — but a detector should still return
|
|
44
|
+
* its cleanest spans.
|
|
45
|
+
*
|
|
46
|
+
* `detect` should be deterministic (same text → same spans): the redactor uses
|
|
47
|
+
* the returned order as a placeholder-numbering tiebreak, so a nondeterministic
|
|
48
|
+
* detector can produce unstable placeholder numbers across runs.
|
|
49
|
+
*/
|
|
50
|
+
interface NerDetector {
|
|
51
|
+
detect(text: string): Promise<PiiSpan[]>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reference {@link NerDetector} backed by Transformers.js (`@huggingface/transformers`).
|
|
56
|
+
*
|
|
57
|
+
* Runs a quantized BERT NER model fully on-device (browser via the
|
|
58
|
+
* onnxruntime-web/WASM backend, or Node) — no network call, so PII never leaves
|
|
59
|
+
* the device. The model is loaded lazily on first `detect()` and cached, so
|
|
60
|
+
* importing this module is cheap and apps that never enable NER pay nothing.
|
|
61
|
+
*
|
|
62
|
+
* `@huggingface/transformers` is an OPTIONAL peer dependency: it is `import()`-ed
|
|
63
|
+
* dynamically (matching `lib/memory/reranker.ts`) so it is only required when a
|
|
64
|
+
* caller actually constructs this detector. React Native cannot use this
|
|
65
|
+
* detector (its WASM backend doesn't run under Hermes) — supply an
|
|
66
|
+
* `onnxruntime-react-native`-based {@link NerDetector} there instead.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
interface TransformersNerDetectorOptions {
|
|
70
|
+
/** Hugging Face model id. Default: `Xenova/bert-base-NER`. */
|
|
71
|
+
model?: string;
|
|
72
|
+
/** Weight precision passed to the pipeline. Default: `q8` (smallest). */
|
|
73
|
+
dtype?: "fp32" | "fp16" | "q8" | "int8" | "uint8" | "q4" | (string & {});
|
|
74
|
+
/** Drop entities below this confidence in [0,1]. Default: `0.5`. */
|
|
75
|
+
minScore?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Map raw NER tags (without the `B-`/`I-` prefix) to PII categories. A tag
|
|
78
|
+
* mapped to `undefined` is dropped. Default maps PER→PERSON, LOC→LOCATION,
|
|
79
|
+
* ORG→ORG and drops MISC (noisy and not reliably PII).
|
|
80
|
+
*/
|
|
81
|
+
tagMap?: Record<string, string | undefined>;
|
|
82
|
+
}
|
|
83
|
+
/** Raw per-token output of a Transformers.js `token-classification` pipeline. */
|
|
84
|
+
interface RawToken {
|
|
85
|
+
entity: string;
|
|
86
|
+
score: number;
|
|
87
|
+
index: number;
|
|
88
|
+
word: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Aggregate B-/I- subword tokens into whole-entity spans. A new entity starts
|
|
92
|
+
* on a `B-` tag or a tag change; contiguous `I-` tokens (and WordPiece "##"
|
|
93
|
+
* continuations) of the same base tag extend it. (The SDK additionally snaps
|
|
94
|
+
* spans to word boundaries, so dropped trailing subwords are recovered there.)
|
|
95
|
+
*/
|
|
96
|
+
declare function aggregateTokens(tokens: RawToken[], text: string, tagMap: Record<string, string | undefined>, minScore: number): PiiSpan[];
|
|
97
|
+
/**
|
|
98
|
+
* Create a Transformers.js-backed {@link NerDetector}. The returned detector
|
|
99
|
+
* loads the model lazily on first use and caches it for the process/page.
|
|
100
|
+
*/
|
|
101
|
+
declare function createTransformersNerDetector(options?: TransformersNerDetectorOptions): NerDetector;
|
|
102
|
+
|
|
103
|
+
export { type TransformersNerDetectorOptions, aggregateTokens, createTransformersNerDetector };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII detection patterns for structured data types.
|
|
3
|
+
*
|
|
4
|
+
* Each pattern has a category (used for placeholder tags like [EMAIL_1]),
|
|
5
|
+
* a regex, and an optional validator for reducing false positives.
|
|
6
|
+
*/
|
|
7
|
+
type PiiCategory = "EMAIL" | "PHONE" | "SSN" | "CREDIT_CARD" | "IP_ADDRESS" | "API_KEY" | "US_ADDRESS" | "DATE_OF_BIRTH";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NER detector contract for PII redaction.
|
|
11
|
+
*
|
|
12
|
+
* The regex layer in {@link ./patterns} handles *structured* PII (email, phone,
|
|
13
|
+
* SSN, credit card, …). It cannot detect *unstructured* PII — person names,
|
|
14
|
+
* locations, organizations — which need a named-entity-recognition model.
|
|
15
|
+
*
|
|
16
|
+
* Rather than bundle a model (and a runtime) into the SDK, we define a small
|
|
17
|
+
* runtime-agnostic contract: a {@link NerDetector} takes text and returns the
|
|
18
|
+
* spans it found. Consumers supply an implementation appropriate to their
|
|
19
|
+
* platform — a Transformers.js model in the browser/Node (see
|
|
20
|
+
* `./detectors/transformers`), an `onnxruntime-react-native` model on mobile,
|
|
21
|
+
* a remote service, or a fake in tests. The SDK keeps detection *on-device* by
|
|
22
|
+
* never calling out itself; it only consumes the spans a detector returns and
|
|
23
|
+
* merges them into the existing placeholder map.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A character-offset span of detected PII. `start`/`end` are indices into the
|
|
28
|
+
* text passed to {@link NerDetector.detect} (`end` exclusive). `category`
|
|
29
|
+
* becomes the placeholder tag prefix (e.g. "PERSON" → `[PERSON_1]`); it may be
|
|
30
|
+
* any string, conventionally `PERSON` / `LOCATION` / `ORG` for NER entities.
|
|
31
|
+
*/
|
|
32
|
+
interface PiiSpan {
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
category: PiiCategory | (string & {});
|
|
36
|
+
/** Optional model confidence in [0,1]; informational (filter in the detector). */
|
|
37
|
+
score?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detects unstructured PII in text. Implementations run a model (or service)
|
|
41
|
+
* and return spans. The SDK defensively clamps spans to the text bounds and
|
|
42
|
+
* snaps them to whole-word boundaries before use, so a detector returning a
|
|
43
|
+
* sub-word span cannot corrupt the output — but a detector should still return
|
|
44
|
+
* its cleanest spans.
|
|
45
|
+
*
|
|
46
|
+
* `detect` should be deterministic (same text → same spans): the redactor uses
|
|
47
|
+
* the returned order as a placeholder-numbering tiebreak, so a nondeterministic
|
|
48
|
+
* detector can produce unstable placeholder numbers across runs.
|
|
49
|
+
*/
|
|
50
|
+
interface NerDetector {
|
|
51
|
+
detect(text: string): Promise<PiiSpan[]>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reference {@link NerDetector} backed by Transformers.js (`@huggingface/transformers`).
|
|
56
|
+
*
|
|
57
|
+
* Runs a quantized BERT NER model fully on-device (browser via the
|
|
58
|
+
* onnxruntime-web/WASM backend, or Node) — no network call, so PII never leaves
|
|
59
|
+
* the device. The model is loaded lazily on first `detect()` and cached, so
|
|
60
|
+
* importing this module is cheap and apps that never enable NER pay nothing.
|
|
61
|
+
*
|
|
62
|
+
* `@huggingface/transformers` is an OPTIONAL peer dependency: it is `import()`-ed
|
|
63
|
+
* dynamically (matching `lib/memory/reranker.ts`) so it is only required when a
|
|
64
|
+
* caller actually constructs this detector. React Native cannot use this
|
|
65
|
+
* detector (its WASM backend doesn't run under Hermes) — supply an
|
|
66
|
+
* `onnxruntime-react-native`-based {@link NerDetector} there instead.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
interface TransformersNerDetectorOptions {
|
|
70
|
+
/** Hugging Face model id. Default: `Xenova/bert-base-NER`. */
|
|
71
|
+
model?: string;
|
|
72
|
+
/** Weight precision passed to the pipeline. Default: `q8` (smallest). */
|
|
73
|
+
dtype?: "fp32" | "fp16" | "q8" | "int8" | "uint8" | "q4" | (string & {});
|
|
74
|
+
/** Drop entities below this confidence in [0,1]. Default: `0.5`. */
|
|
75
|
+
minScore?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Map raw NER tags (without the `B-`/`I-` prefix) to PII categories. A tag
|
|
78
|
+
* mapped to `undefined` is dropped. Default maps PER→PERSON, LOC→LOCATION,
|
|
79
|
+
* ORG→ORG and drops MISC (noisy and not reliably PII).
|
|
80
|
+
*/
|
|
81
|
+
tagMap?: Record<string, string | undefined>;
|
|
82
|
+
}
|
|
83
|
+
/** Raw per-token output of a Transformers.js `token-classification` pipeline. */
|
|
84
|
+
interface RawToken {
|
|
85
|
+
entity: string;
|
|
86
|
+
score: number;
|
|
87
|
+
index: number;
|
|
88
|
+
word: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Aggregate B-/I- subword tokens into whole-entity spans. A new entity starts
|
|
92
|
+
* on a `B-` tag or a tag change; contiguous `I-` tokens (and WordPiece "##"
|
|
93
|
+
* continuations) of the same base tag extend it. (The SDK additionally snaps
|
|
94
|
+
* spans to word boundaries, so dropped trailing subwords are recovered there.)
|
|
95
|
+
*/
|
|
96
|
+
declare function aggregateTokens(tokens: RawToken[], text: string, tagMap: Record<string, string | undefined>, minScore: number): PiiSpan[];
|
|
97
|
+
/**
|
|
98
|
+
* Create a Transformers.js-backed {@link NerDetector}. The returned detector
|
|
99
|
+
* loads the model lazily on first use and caches it for the process/page.
|
|
100
|
+
*/
|
|
101
|
+
declare function createTransformersNerDetector(options?: TransformersNerDetectorOptions): NerDetector;
|
|
102
|
+
|
|
103
|
+
export { type TransformersNerDetectorOptions, aggregateTokens, createTransformersNerDetector };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/lib/pii/detectors/transformers.ts
|
|
2
|
+
var DEFAULT_MODEL = "Xenova/bert-base-NER";
|
|
3
|
+
var DEFAULT_TAG_MAP = {
|
|
4
|
+
PER: "PERSON",
|
|
5
|
+
LOC: "LOCATION",
|
|
6
|
+
ORG: "ORG",
|
|
7
|
+
MISC: void 0
|
|
8
|
+
};
|
|
9
|
+
function placeTokens(tokens, text) {
|
|
10
|
+
const placed = [];
|
|
11
|
+
let cursor = 0;
|
|
12
|
+
for (const t of tokens) {
|
|
13
|
+
const surface = t.word.startsWith("##") ? t.word.slice(2) : t.word;
|
|
14
|
+
const start = text.indexOf(surface, cursor);
|
|
15
|
+
if (start < 0) {
|
|
16
|
+
placed.push({ ...t, start: -1, end: -1 });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const end = start + surface.length;
|
|
20
|
+
placed.push({ ...t, start, end });
|
|
21
|
+
cursor = end;
|
|
22
|
+
}
|
|
23
|
+
return placed;
|
|
24
|
+
}
|
|
25
|
+
function aggregateTokens(tokens, text, tagMap, minScore) {
|
|
26
|
+
const placed = placeTokens(tokens, text);
|
|
27
|
+
const spans = [];
|
|
28
|
+
let cur = null;
|
|
29
|
+
const flush = () => {
|
|
30
|
+
if (!cur) return;
|
|
31
|
+
const category = cur.valid ? tagMap[cur.base] : void 0;
|
|
32
|
+
if (category) {
|
|
33
|
+
const score = cur.scores.reduce((a, b) => a + b, 0) / cur.scores.length;
|
|
34
|
+
if (score >= minScore) spans.push({ start: cur.start, end: cur.end, category, score });
|
|
35
|
+
}
|
|
36
|
+
cur = null;
|
|
37
|
+
};
|
|
38
|
+
for (const t of placed) {
|
|
39
|
+
const dash = t.entity.indexOf("-");
|
|
40
|
+
const prefix = dash >= 0 ? t.entity.slice(0, dash) : t.entity;
|
|
41
|
+
const base = dash >= 0 ? t.entity.slice(dash + 1) : t.entity;
|
|
42
|
+
const isContinuation = t.word.startsWith("##");
|
|
43
|
+
const unplaceable = t.start < 0;
|
|
44
|
+
const continues = cur !== null && base === cur.base && (prefix === "I" || isContinuation) && (unplaceable || t.start - cur.end <= 1);
|
|
45
|
+
if (continues) {
|
|
46
|
+
if (unplaceable) cur.valid = false;
|
|
47
|
+
else {
|
|
48
|
+
cur.end = t.end;
|
|
49
|
+
cur.scores.push(t.score);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
flush();
|
|
53
|
+
cur = unplaceable ? null : { base, start: t.start, end: t.end, scores: [t.score], valid: true };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
flush();
|
|
57
|
+
return spans;
|
|
58
|
+
}
|
|
59
|
+
function createTransformersNerDetector(options = {}) {
|
|
60
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
61
|
+
const dtype = options.dtype ?? "q8";
|
|
62
|
+
const minScore = options.minScore ?? 0.5;
|
|
63
|
+
const tagMap = options.tagMap ?? DEFAULT_TAG_MAP;
|
|
64
|
+
let pipePromise = null;
|
|
65
|
+
const load = () => {
|
|
66
|
+
if (!pipePromise) {
|
|
67
|
+
const loading = (async () => {
|
|
68
|
+
const transformers = await import("@huggingface/transformers");
|
|
69
|
+
return transformers.pipeline("token-classification", model, { dtype });
|
|
70
|
+
})();
|
|
71
|
+
pipePromise = loading;
|
|
72
|
+
loading.catch(() => {
|
|
73
|
+
if (pipePromise === loading) pipePromise = null;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return pipePromise;
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
async detect(text) {
|
|
80
|
+
if (!text) return [];
|
|
81
|
+
const classify = await load();
|
|
82
|
+
const tokens = await classify(text);
|
|
83
|
+
return aggregateTokens(tokens, text, tagMap, minScore);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
aggregateTokens,
|
|
89
|
+
createTransformersNerDetector
|
|
90
|
+
};
|