@redact-secret/core 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +202 -0
- package/dist/adapters/node-stream.d.ts +51 -0
- package/dist/adapters/node-stream.js +79 -0
- package/dist/adapters/shared.d.ts +33 -0
- package/dist/adapters/shared.js +81 -0
- package/dist/adapters/web-stream.d.ts +57 -0
- package/dist/adapters/web-stream.js +115 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.js +82 -0
- package/dist/formatters.d.ts +23 -0
- package/dist/formatters.js +22 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +62 -0
- package/dist/native.d.ts +80 -0
- package/dist/native.js +21 -0
- package/dist/runtime/browser.d.ts +83 -0
- package/dist/runtime/browser.js +143 -0
- package/dist/runtime/node.d.ts +58 -0
- package/dist/runtime/node.js +122 -0
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.js +256 -0
- package/dist/session.d.ts +13 -0
- package/dist/session.js +15 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.js +14 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +8 -0
- package/package.json +66 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single sanitized error type this package throws.
|
|
3
|
+
*
|
|
4
|
+
* Every code and message below is fixed and input-free: no error carries the
|
|
5
|
+
* scanned input, a matched value, a placeholder, or a failing callback's own
|
|
6
|
+
* message (`decision-define-runtime-bindings`). Sixteen codes come from the
|
|
7
|
+
* Rust core; `NOT_INITIALIZED` and `INITIALIZATION_FAILED` are produced by the
|
|
8
|
+
* binding layer, `INVALID_CHUNK` and `INVALID_UTF8` by the stream adapters,
|
|
9
|
+
* `UNPAIRED_SURROGATE` by this package's own runtime-neutral input check, and
|
|
10
|
+
* `INCREMENTAL_UNAVAILABLE` by any binding that does not implement
|
|
11
|
+
* incremental sanitization — the browser binding always (`bindings/wasm` has
|
|
12
|
+
* no such export by design) and the Node binding until `bindings/node` gains
|
|
13
|
+
* one; and this package normalizes all of them into the same class so
|
|
14
|
+
* `instanceof SecretScanError` holds on every runtime and every subpath.
|
|
15
|
+
*
|
|
16
|
+
* `UNPAIRED_SURROGATE` resolves `B/F-10`
|
|
17
|
+
* (`docs/audits/deferred-quality-backlog.md`): a JavaScript string may hold a
|
|
18
|
+
* lone UTF-16 surrogate, which has no UTF-8 representation, so it cannot
|
|
19
|
+
* cross into the Rust core's `&str` without either a silent `U+FFFD`
|
|
20
|
+
* substitution (which would make `redact`'s output a transcoded copy, not the
|
|
21
|
+
* caller's input with only findings replaced) or a stable rejection. This
|
|
22
|
+
* package rejects, once, in `runtime.ts`'s `requireString`, before the string
|
|
23
|
+
* reaches either binding — so the error and this check are identical on the
|
|
24
|
+
* Node and browser runtimes by construction rather than by parallel
|
|
25
|
+
* implementation.
|
|
26
|
+
*/
|
|
27
|
+
/** The fixed message for every code, mirroring the core's own strings. */
|
|
28
|
+
const ERROR_MESSAGES = {
|
|
29
|
+
INVALID_INPUT: "Secret scan input must be a string.",
|
|
30
|
+
INVALID_OPTIONS: "Secret scan options are invalid.",
|
|
31
|
+
INVALID_DETECTOR: "Invalid detector registration.",
|
|
32
|
+
DETECTOR_FAILURE: "A secret detector failed.",
|
|
33
|
+
INVALID_CANDIDATE: "A secret detector returned an invalid candidate.",
|
|
34
|
+
POLICY_FAILURE: "The secret policy failed.",
|
|
35
|
+
INVALID_POLICY_ACTION: "The secret policy returned an invalid action.",
|
|
36
|
+
INVALID_FINDINGS: "Redaction findings are invalid.",
|
|
37
|
+
PLACEHOLDER_FAILURE: "The placeholder formatter failed.",
|
|
38
|
+
INVALID_PLACEHOLDER: "The placeholder formatter returned an invalid value.",
|
|
39
|
+
INVALID_LIMITS: "Incremental sanitizer limits are invalid.",
|
|
40
|
+
INPUT_LIMIT_EXCEEDED: "Incremental sanitizer input limit exceeded.",
|
|
41
|
+
BUFFER_LIMIT_EXCEEDED: "Incremental sanitizer buffer limit exceeded.",
|
|
42
|
+
TOKEN_LIMIT_EXCEEDED: "Incremental sanitizer token limit exceeded.",
|
|
43
|
+
MULTILINE_LIMIT_EXCEEDED: "Incremental sanitizer multiline limit exceeded.",
|
|
44
|
+
INVALID_STATE: "The incremental sanitizer is no longer accepting input.",
|
|
45
|
+
NOT_INITIALIZED: "redact-secret is not initialized; await initialize() before this call.",
|
|
46
|
+
INITIALIZATION_FAILED: "redact-secret failed to initialize.",
|
|
47
|
+
INVALID_CHUNK: "Stream sanitizer input must contain bytes.",
|
|
48
|
+
INVALID_UTF8: "Stream sanitizer input is not valid UTF-8.",
|
|
49
|
+
UNPAIRED_SURROGATE: "Secret scan input contains an unpaired UTF-16 surrogate.",
|
|
50
|
+
INCREMENTAL_UNAVAILABLE: "Incremental sanitization is not available on this runtime.",
|
|
51
|
+
};
|
|
52
|
+
const ERROR_CODES = new Set(Object.keys(ERROR_MESSAGES));
|
|
53
|
+
/** A sanitized failure. It carries nothing but its fixed code and message. */
|
|
54
|
+
export class SecretScanError extends Error {
|
|
55
|
+
code;
|
|
56
|
+
constructor(code) {
|
|
57
|
+
super(ERROR_MESSAGES[code]);
|
|
58
|
+
this.name = "SecretScanError";
|
|
59
|
+
this.code = code;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function nativeErrorCode(value) {
|
|
63
|
+
if (typeof value !== "object" || value === null)
|
|
64
|
+
return undefined;
|
|
65
|
+
const { code } = value;
|
|
66
|
+
return typeof code === "string" && ERROR_CODES.has(code)
|
|
67
|
+
? code
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Rewrites whatever a binding threw as a {@link SecretScanError}.
|
|
72
|
+
*
|
|
73
|
+
* The Node addon throws an N-API error whose `code` is the core's fixed code;
|
|
74
|
+
* the WebAssembly binding throws a `js_sys::Error` with the same property. A
|
|
75
|
+
* value that carries no recognized code is replaced by `fallback` rather than
|
|
76
|
+
* surfaced, so a host-specific message can never reach a caller.
|
|
77
|
+
*/
|
|
78
|
+
export function toSecretScanError(thrown, fallback) {
|
|
79
|
+
if (thrown instanceof SecretScanError)
|
|
80
|
+
return thrown;
|
|
81
|
+
return new SecretScanError(nativeErrorCode(thrown) ?? fallback);
|
|
82
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The supported placeholder formatter helpers.
|
|
3
|
+
*
|
|
4
|
+
* Both are pure functions of safe finding metadata: they never see the input
|
|
5
|
+
* or the matched value, and the binding validates whatever they return. Pass
|
|
6
|
+
* {@link defaultPlaceholderFormatter} to name the built-in behavior
|
|
7
|
+
* explicitly — the runtime recognizes it and lets the Rust core apply its own
|
|
8
|
+
* formatter rather than calling back into JavaScript per placeholder.
|
|
9
|
+
*
|
|
10
|
+
* There is deliberately no exported default *policy*: the built-in policy's
|
|
11
|
+
* type table lives in Rust, and re-declaring it here would create a second
|
|
12
|
+
* source of truth (`decision-govern-cross-language-conformance`). Omit
|
|
13
|
+
* `policy` to use it.
|
|
14
|
+
*/
|
|
15
|
+
import type { PlaceholderFormatter } from "./types.js";
|
|
16
|
+
/** Formats `<SECRET_1>`, `<SECRET_2>`, ... in replacement order. */
|
|
17
|
+
export declare const defaultPlaceholderFormatter: PlaceholderFormatter;
|
|
18
|
+
/**
|
|
19
|
+
* Formats `<JWT_1>`, `<AWS_ACCESS_KEY_ID_2>`, ... naming the finding type,
|
|
20
|
+
* upper-cased with `.` and `-` mapped to `_`, matching the core's own typed
|
|
21
|
+
* formatter.
|
|
22
|
+
*/
|
|
23
|
+
export declare const typedPlaceholderFormatter: PlaceholderFormatter;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The supported placeholder formatter helpers.
|
|
3
|
+
*
|
|
4
|
+
* Both are pure functions of safe finding metadata: they never see the input
|
|
5
|
+
* or the matched value, and the binding validates whatever they return. Pass
|
|
6
|
+
* {@link defaultPlaceholderFormatter} to name the built-in behavior
|
|
7
|
+
* explicitly — the runtime recognizes it and lets the Rust core apply its own
|
|
8
|
+
* formatter rather than calling back into JavaScript per placeholder.
|
|
9
|
+
*
|
|
10
|
+
* There is deliberately no exported default *policy*: the built-in policy's
|
|
11
|
+
* type table lives in Rust, and re-declaring it here would create a second
|
|
12
|
+
* source of truth (`decision-govern-cross-language-conformance`). Omit
|
|
13
|
+
* `policy` to use it.
|
|
14
|
+
*/
|
|
15
|
+
/** Formats `<SECRET_1>`, `<SECRET_2>`, ... in replacement order. */
|
|
16
|
+
export const defaultPlaceholderFormatter = (_finding, context) => `<SECRET_${context.placeholderIndex}>`;
|
|
17
|
+
/**
|
|
18
|
+
* Formats `<JWT_1>`, `<AWS_ACCESS_KEY_ID_2>`, ... naming the finding type,
|
|
19
|
+
* upper-cased with `.` and `-` mapped to `_`, matching the core's own typed
|
|
20
|
+
* formatter.
|
|
21
|
+
*/
|
|
22
|
+
export const typedPlaceholderFormatter = (finding, context) => `<${finding.type.toUpperCase().replace(/[.-]/g, "_")}_${context.placeholderIndex}>`;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@redact-secret/core`: deterministic secret detection and redaction, one
|
|
3
|
+
* API across Node.js and the browser.
|
|
4
|
+
*
|
|
5
|
+
* The package's `exports` map selects the N-API addon on Node and the
|
|
6
|
+
* WebAssembly build in the browser (`decision-define-runtime-bindings`).
|
|
7
|
+
* Every runtime uses the same contract:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { initialize, scanAndRedact } from "@redact-secret/core";
|
|
11
|
+
*
|
|
12
|
+
* await initialize();
|
|
13
|
+
* const { text, findings } = scanAndRedact(input);
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `await initialize()` must succeed exactly once before any synchronous
|
|
17
|
+
* operation; calling it again is free. Node's own loading has nothing to
|
|
18
|
+
* await, but the call stays part of the contract so the usage model does not
|
|
19
|
+
* vary by runtime.
|
|
20
|
+
*
|
|
21
|
+
* Every range this module reports is a `[start, end)` pair of UTF-16
|
|
22
|
+
* code-unit offsets ({@link RANGE_UNIT}), and every finding it returns is
|
|
23
|
+
* frozen. Every failure is a {@link SecretScanError} carrying nothing but a
|
|
24
|
+
* fixed code and message.
|
|
25
|
+
*
|
|
26
|
+
* Built-in detectors all run in Rust; there is no custom detector callback in
|
|
27
|
+
* this API, and no internal module of this package is reachable through its
|
|
28
|
+
* `exports` map.
|
|
29
|
+
*
|
|
30
|
+
* Byte streams are served by the two adapter subpaths,
|
|
31
|
+
* `@redact-secret/core/node-stream` and
|
|
32
|
+
* `@redact-secret/core/web-stream`, each of which drives one incremental
|
|
33
|
+
* session per stream. This root module never resolves a `node:` module.
|
|
34
|
+
*/
|
|
35
|
+
import type { RangeUnit } from "./types.js";
|
|
36
|
+
/**
|
|
37
|
+
* Loads this runtime's binding and prepares it for use.
|
|
38
|
+
*
|
|
39
|
+
* Idempotent: the artifact is loaded at most once no matter how many callers
|
|
40
|
+
* await it. A rejected attempt is not cached, so a caller may retry. Rejects
|
|
41
|
+
* with `INITIALIZATION_FAILED` when the artifact is missing, unusable, or
|
|
42
|
+
* built from a different product version than this package.
|
|
43
|
+
*/
|
|
44
|
+
export declare const initialize: () => Promise<void>;
|
|
45
|
+
/** Scans `input` and returns every finding, in input order. */
|
|
46
|
+
export declare const scan: (input: string, options?: import("./types.js").ScanOptions) => readonly import("./types.js").SecretFinding[];
|
|
47
|
+
/**
|
|
48
|
+
* Replaces the `redact` and `block` findings in `input` with placeholders,
|
|
49
|
+
* leaving `warn` and `allow` findings untouched.
|
|
50
|
+
*
|
|
51
|
+
* `findings` must be the findings {@link scan} returned for this same input.
|
|
52
|
+
*/
|
|
53
|
+
export declare const redact: (input: string, findings: readonly import("./types.js").SecretFinding[], options?: import("./types.js").RedactOptions) => string;
|
|
54
|
+
/** Scans and redacts in one call, so text and findings cannot disagree. */
|
|
55
|
+
export declare const scanAndRedact: (input: string, options?: import("./types.js").ScanAndRedactOptions) => import("./types.js").ScanResult;
|
|
56
|
+
/** Opens a bounded incremental session over text supplied in chunks. */
|
|
57
|
+
export declare const createIncrementalSanitizer: (options: import("./types.js").IncrementalSanitizerOptions) => import("./types.js").IncrementalSanitizer;
|
|
58
|
+
export { defaultPlaceholderFormatter, typedPlaceholderFormatter, } from "./formatters.js";
|
|
59
|
+
export { SecretScanError } from "./errors.js";
|
|
60
|
+
export type { SecretScanErrorCode } from "./errors.js";
|
|
61
|
+
export { VERSION } from "./version.js";
|
|
62
|
+
/** The string-index unit of every range this package reports. */
|
|
63
|
+
export declare const RANGE_UNIT: RangeUnit;
|
|
64
|
+
export type { DetectedSecretFinding, IncrementalLimits, IncrementalPolicyContext, IncrementalSanitizer, IncrementalSanitizerOptions, IncrementalSanitizerResult, IncrementalSanitizerState, IncrementalSecretPolicy, PlaceholderContext, PlaceholderFormatter, PolicyContext, RangeUnit, RedactOptions, ScanAndRedactOptions, ScanOptions, ScanResult, SecretAction, SecretConfidence, SecretFinding, SecretPolicy, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@redact-secret/core`: deterministic secret detection and redaction, one
|
|
3
|
+
* API across Node.js and the browser.
|
|
4
|
+
*
|
|
5
|
+
* The package's `exports` map selects the N-API addon on Node and the
|
|
6
|
+
* WebAssembly build in the browser (`decision-define-runtime-bindings`).
|
|
7
|
+
* Every runtime uses the same contract:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { initialize, scanAndRedact } from "@redact-secret/core";
|
|
11
|
+
*
|
|
12
|
+
* await initialize();
|
|
13
|
+
* const { text, findings } = scanAndRedact(input);
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `await initialize()` must succeed exactly once before any synchronous
|
|
17
|
+
* operation; calling it again is free. Node's own loading has nothing to
|
|
18
|
+
* await, but the call stays part of the contract so the usage model does not
|
|
19
|
+
* vary by runtime.
|
|
20
|
+
*
|
|
21
|
+
* Every range this module reports is a `[start, end)` pair of UTF-16
|
|
22
|
+
* code-unit offsets ({@link RANGE_UNIT}), and every finding it returns is
|
|
23
|
+
* frozen. Every failure is a {@link SecretScanError} carrying nothing but a
|
|
24
|
+
* fixed code and message.
|
|
25
|
+
*
|
|
26
|
+
* Built-in detectors all run in Rust; there is no custom detector callback in
|
|
27
|
+
* this API, and no internal module of this package is reachable through its
|
|
28
|
+
* `exports` map.
|
|
29
|
+
*
|
|
30
|
+
* Byte streams are served by the two adapter subpaths,
|
|
31
|
+
* `@redact-secret/core/node-stream` and
|
|
32
|
+
* `@redact-secret/core/web-stream`, each of which drives one incremental
|
|
33
|
+
* session per stream. This root module never resolves a `node:` module.
|
|
34
|
+
*/
|
|
35
|
+
import { runtime } from "./session.js";
|
|
36
|
+
/**
|
|
37
|
+
* Loads this runtime's binding and prepares it for use.
|
|
38
|
+
*
|
|
39
|
+
* Idempotent: the artifact is loaded at most once no matter how many callers
|
|
40
|
+
* await it. A rejected attempt is not cached, so a caller may retry. Rejects
|
|
41
|
+
* with `INITIALIZATION_FAILED` when the artifact is missing, unusable, or
|
|
42
|
+
* built from a different product version than this package.
|
|
43
|
+
*/
|
|
44
|
+
export const initialize = runtime.initialize;
|
|
45
|
+
/** Scans `input` and returns every finding, in input order. */
|
|
46
|
+
export const scan = runtime.scan;
|
|
47
|
+
/**
|
|
48
|
+
* Replaces the `redact` and `block` findings in `input` with placeholders,
|
|
49
|
+
* leaving `warn` and `allow` findings untouched.
|
|
50
|
+
*
|
|
51
|
+
* `findings` must be the findings {@link scan} returned for this same input.
|
|
52
|
+
*/
|
|
53
|
+
export const redact = runtime.redact;
|
|
54
|
+
/** Scans and redacts in one call, so text and findings cannot disagree. */
|
|
55
|
+
export const scanAndRedact = runtime.scanAndRedact;
|
|
56
|
+
/** Opens a bounded incremental session over text supplied in chunks. */
|
|
57
|
+
export const createIncrementalSanitizer = runtime.createIncrementalSanitizer;
|
|
58
|
+
export { defaultPlaceholderFormatter, typedPlaceholderFormatter, } from "./formatters.js";
|
|
59
|
+
export { SecretScanError } from "./errors.js";
|
|
60
|
+
export { VERSION } from "./version.js";
|
|
61
|
+
/** The string-index unit of every range this package reports. */
|
|
62
|
+
export const RANGE_UNIT = "utf16-code-units";
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The internal contract every runtime adapter must satisfy.
|
|
3
|
+
*
|
|
4
|
+
* `runtime/node.ts` builds it from the N-API addon and `runtime/browser.ts`
|
|
5
|
+
* from the WebAssembly build; nothing here is part of the published API, and
|
|
6
|
+
* the package's `exports` map makes this module unreachable from outside
|
|
7
|
+
* (`decision-define-runtime-bindings`).
|
|
8
|
+
*
|
|
9
|
+
* Every offset crossing this boundary is already a UTF-16 code-unit offset:
|
|
10
|
+
* each binding converts from the core's UTF-8 byte offsets on its own side,
|
|
11
|
+
* without changing the selected span.
|
|
12
|
+
*/
|
|
13
|
+
import type { IncrementalPolicyContext, IncrementalSanitizerState, PlaceholderContext, PolicyContext } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Carries a binding-private handle alongside a public finding.
|
|
16
|
+
*
|
|
17
|
+
* The WebAssembly binding returns opaque `Finding` objects that its `redact`
|
|
18
|
+
* must receive back unchanged; the property is a symbol and non-enumerable, so
|
|
19
|
+
* it never appears in `Object.keys`, `JSON.stringify`, or a structural
|
|
20
|
+
* comparison of a public finding.
|
|
21
|
+
*/
|
|
22
|
+
export declare const NATIVE_HANDLE: unique symbol;
|
|
23
|
+
export interface NativeFinding {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly type: string;
|
|
26
|
+
readonly detector: string;
|
|
27
|
+
readonly confidence: string;
|
|
28
|
+
readonly action: string;
|
|
29
|
+
readonly start: number;
|
|
30
|
+
readonly end: number;
|
|
31
|
+
readonly [NATIVE_HANDLE]?: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface NativeDetectedFinding {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly type: string;
|
|
36
|
+
readonly detector: string;
|
|
37
|
+
readonly confidence: string;
|
|
38
|
+
readonly start: number;
|
|
39
|
+
readonly end: number;
|
|
40
|
+
}
|
|
41
|
+
export type NativePolicyCallback = (finding: NativeDetectedFinding, context: PolicyContext) => string;
|
|
42
|
+
export type NativeIncrementalPolicyCallback = (finding: NativeDetectedFinding, context: IncrementalPolicyContext) => string;
|
|
43
|
+
export type NativeFormatterCallback = (finding: NativeFinding, context: PlaceholderContext) => string;
|
|
44
|
+
export interface NativeScanAndRedactResult {
|
|
45
|
+
readonly text: string;
|
|
46
|
+
readonly findings: readonly NativeFinding[];
|
|
47
|
+
}
|
|
48
|
+
export interface NativeIncrementalLimits {
|
|
49
|
+
readonly maxInputCodeUnits: number;
|
|
50
|
+
readonly maxBufferedCodeUnits: number;
|
|
51
|
+
readonly maxTokenCodeUnits: number;
|
|
52
|
+
readonly maxMultilineCodeUnits: number;
|
|
53
|
+
}
|
|
54
|
+
export interface NativeIncrementalOptions {
|
|
55
|
+
readonly limits: NativeIncrementalLimits;
|
|
56
|
+
readonly policy?: NativeIncrementalPolicyCallback;
|
|
57
|
+
readonly formatter?: NativeFormatterCallback;
|
|
58
|
+
}
|
|
59
|
+
export interface NativeIncrementalResult {
|
|
60
|
+
readonly text: string;
|
|
61
|
+
readonly findings: readonly NativeFinding[];
|
|
62
|
+
}
|
|
63
|
+
export interface NativeIncrementalSanitizer {
|
|
64
|
+
readonly state: IncrementalSanitizerState;
|
|
65
|
+
append(chunk: string): NativeIncrementalResult;
|
|
66
|
+
finalize(): NativeIncrementalResult;
|
|
67
|
+
abort(): void;
|
|
68
|
+
}
|
|
69
|
+
export interface NativeBinding {
|
|
70
|
+
/** The shared product version this artifact was built from. */
|
|
71
|
+
version(): string;
|
|
72
|
+
/** Idempotent native setup. May be a no-op, as it is on Node. */
|
|
73
|
+
initialize(): void;
|
|
74
|
+
scan(input: string, policy: NativePolicyCallback | undefined): readonly NativeFinding[];
|
|
75
|
+
redact(input: string, findings: readonly NativeFinding[], formatter: NativeFormatterCallback | undefined): string;
|
|
76
|
+
scanAndRedact(input: string, policy: NativePolicyCallback | undefined, formatter: NativeFormatterCallback | undefined): NativeScanAndRedactResult;
|
|
77
|
+
createIncrementalSanitizer(options: NativeIncrementalOptions): NativeIncrementalSanitizer;
|
|
78
|
+
}
|
|
79
|
+
/** Loads and prepares this runtime's binding. Called at most once. */
|
|
80
|
+
export type NativeBindingLoader = () => Promise<NativeBinding>;
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The internal contract every runtime adapter must satisfy.
|
|
3
|
+
*
|
|
4
|
+
* `runtime/node.ts` builds it from the N-API addon and `runtime/browser.ts`
|
|
5
|
+
* from the WebAssembly build; nothing here is part of the published API, and
|
|
6
|
+
* the package's `exports` map makes this module unreachable from outside
|
|
7
|
+
* (`decision-define-runtime-bindings`).
|
|
8
|
+
*
|
|
9
|
+
* Every offset crossing this boundary is already a UTF-16 code-unit offset:
|
|
10
|
+
* each binding converts from the core's UTF-8 byte offsets on its own side,
|
|
11
|
+
* without changing the selected span.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Carries a binding-private handle alongside a public finding.
|
|
15
|
+
*
|
|
16
|
+
* The WebAssembly binding returns opaque `Finding` objects that its `redact`
|
|
17
|
+
* must receive back unchanged; the property is a symbol and non-enumerable, so
|
|
18
|
+
* it never appears in `Object.keys`, `JSON.stringify`, or a structural
|
|
19
|
+
* comparison of a public finding.
|
|
20
|
+
*/
|
|
21
|
+
export const NATIVE_HANDLE = Symbol.for("@redact-secret/core.native-handle");
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser adapter: the `wasm-bindgen` build, normalized to the internal
|
|
3
|
+
* binding contract (`decision-define-runtime-bindings`).
|
|
4
|
+
*
|
|
5
|
+
* The package's `imports` map reaches this module under the `browser`
|
|
6
|
+
* condition and by default. It imports nothing from `node:` and touches no
|
|
7
|
+
* Node global, so a bundle produced for the browser resolves only this file
|
|
8
|
+
* and the WebAssembly artifact it loads.
|
|
9
|
+
*
|
|
10
|
+
* There are two distinct setup steps behind one `await initialize()`: the
|
|
11
|
+
* generated `init()` that fetches and instantiates the `.wasm` binary, and the
|
|
12
|
+
* binding's own idempotent `initialize()` that builds the detector registry.
|
|
13
|
+
* Wrapping both is exactly this adapter's job.
|
|
14
|
+
*
|
|
15
|
+
* `bindings/wasm` does not export a `createIncrementalSanitizer` (see its
|
|
16
|
+
* `README.md`): incremental sanitization is deliberately unavailable on this
|
|
17
|
+
* runtime, so this adapter's `createIncrementalSanitizer` always rejects with
|
|
18
|
+
* the fixed `INCREMENTAL_UNAVAILABLE` code. `./web-stream` rejects the same
|
|
19
|
+
* way, through this same binding, and `initialize()` itself still resolves.
|
|
20
|
+
*/
|
|
21
|
+
import { type NativeBinding } from "../native.js";
|
|
22
|
+
import type { PlaceholderContext, PolicyContext } from "../types.js";
|
|
23
|
+
/** One opaque finding handle, as `scan`/`redact`/`scanAndRedact` return it. */
|
|
24
|
+
export interface WasmFinding {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly type: string;
|
|
27
|
+
readonly detector: string;
|
|
28
|
+
readonly confidence: string;
|
|
29
|
+
readonly action: string;
|
|
30
|
+
readonly range: {
|
|
31
|
+
readonly start: number;
|
|
32
|
+
readonly end: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The safe metadata a `policy` callback is actually invoked with
|
|
37
|
+
* (`bindings/wasm/src/metadata.rs`'s `policy_finding`): the same fields as
|
|
38
|
+
* {@link WasmFinding} minus `action`, with the range still nested rather than
|
|
39
|
+
* flattened onto the object.
|
|
40
|
+
*/
|
|
41
|
+
export interface WasmDetectedFindingMetadata {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly type: string;
|
|
44
|
+
readonly detector: string;
|
|
45
|
+
readonly confidence: string;
|
|
46
|
+
readonly range: {
|
|
47
|
+
readonly start: number;
|
|
48
|
+
readonly end: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The safe metadata a `formatter` callback is actually invoked with
|
|
53
|
+
* (`metadata.rs`'s `formatter_finding`): adds the `action` the policy already
|
|
54
|
+
* chose.
|
|
55
|
+
*/
|
|
56
|
+
export interface WasmFindingMetadata extends WasmDetectedFindingMetadata {
|
|
57
|
+
readonly action: string;
|
|
58
|
+
}
|
|
59
|
+
type WasmPolicyCallback = (finding: WasmDetectedFindingMetadata, context: PolicyContext) => string;
|
|
60
|
+
type WasmFormatterCallback = (finding: WasmFindingMetadata, context: PlaceholderContext) => string;
|
|
61
|
+
export interface WasmModule {
|
|
62
|
+
default(): Promise<unknown>;
|
|
63
|
+
version(): string;
|
|
64
|
+
initialize(): void;
|
|
65
|
+
scan(input: string, policy?: WasmPolicyCallback): readonly WasmFinding[];
|
|
66
|
+
redact(input: string, findings: readonly WasmFinding[], formatter?: WasmFormatterCallback): string;
|
|
67
|
+
scanAndRedact(input: string, policy?: WasmPolicyCallback, formatter?: WasmFormatterCallback): {
|
|
68
|
+
readonly text: string;
|
|
69
|
+
readonly findings: readonly WasmFinding[];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Builds the internal binding contract from an already-loaded WebAssembly
|
|
74
|
+
* module.
|
|
75
|
+
*
|
|
76
|
+
* Exported so a test double can exercise this exact normalization — the
|
|
77
|
+
* nested-metadata flattening above and the fixed `INCREMENTAL_UNAVAILABLE`
|
|
78
|
+
* rejection below — against a fake module shaped like the real artifact,
|
|
79
|
+
* without loading the artifact itself.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createBindingFromWasmModule(wasm: WasmModule): NativeBinding;
|
|
82
|
+
export declare const loadNativeBinding: () => Promise<NativeBinding>;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser adapter: the `wasm-bindgen` build, normalized to the internal
|
|
3
|
+
* binding contract (`decision-define-runtime-bindings`).
|
|
4
|
+
*
|
|
5
|
+
* The package's `imports` map reaches this module under the `browser`
|
|
6
|
+
* condition and by default. It imports nothing from `node:` and touches no
|
|
7
|
+
* Node global, so a bundle produced for the browser resolves only this file
|
|
8
|
+
* and the WebAssembly artifact it loads.
|
|
9
|
+
*
|
|
10
|
+
* There are two distinct setup steps behind one `await initialize()`: the
|
|
11
|
+
* generated `init()` that fetches and instantiates the `.wasm` binary, and the
|
|
12
|
+
* binding's own idempotent `initialize()` that builds the detector registry.
|
|
13
|
+
* Wrapping both is exactly this adapter's job.
|
|
14
|
+
*
|
|
15
|
+
* `bindings/wasm` does not export a `createIncrementalSanitizer` (see its
|
|
16
|
+
* `README.md`): incremental sanitization is deliberately unavailable on this
|
|
17
|
+
* runtime, so this adapter's `createIncrementalSanitizer` always rejects with
|
|
18
|
+
* the fixed `INCREMENTAL_UNAVAILABLE` code. `./web-stream` rejects the same
|
|
19
|
+
* way, through this same binding, and `initialize()` itself still resolves.
|
|
20
|
+
*/
|
|
21
|
+
import { SecretScanError } from "../errors.js";
|
|
22
|
+
import { NATIVE_HANDLE, } from "../native.js";
|
|
23
|
+
/**
|
|
24
|
+
* Flattens an opaque handle into the contract's shape while keeping the handle
|
|
25
|
+
* itself, because the WebAssembly `redact` only accepts the objects its own
|
|
26
|
+
* `scan` returned.
|
|
27
|
+
*/
|
|
28
|
+
function toNativeFinding(finding) {
|
|
29
|
+
const { start, end } = finding.range;
|
|
30
|
+
return {
|
|
31
|
+
id: finding.id,
|
|
32
|
+
type: finding.type,
|
|
33
|
+
detector: finding.detector,
|
|
34
|
+
confidence: finding.confidence,
|
|
35
|
+
action: finding.action,
|
|
36
|
+
start,
|
|
37
|
+
end,
|
|
38
|
+
[NATIVE_HANDLE]: finding,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Recovers the handle `scan` produced, refusing a foreign finding. */
|
|
42
|
+
function toWasmFinding(finding) {
|
|
43
|
+
const handle = finding[NATIVE_HANDLE];
|
|
44
|
+
if (handle === undefined)
|
|
45
|
+
throw new SecretScanError("INVALID_FINDINGS");
|
|
46
|
+
return handle;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Flattens and freezes the nested-`range` metadata a `policy` callback is
|
|
50
|
+
* actually invoked with, so a callback crossing this boundary sees the same
|
|
51
|
+
* numeric `start`/`end` fields it sees on Node, matching `types.ts`.
|
|
52
|
+
*/
|
|
53
|
+
function toNativeDetectedFinding(finding) {
|
|
54
|
+
const { start, end } = finding.range;
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
id: finding.id,
|
|
57
|
+
type: finding.type,
|
|
58
|
+
detector: finding.detector,
|
|
59
|
+
confidence: finding.confidence,
|
|
60
|
+
start,
|
|
61
|
+
end,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** As {@link toNativeDetectedFinding}, plus the `action` a formatter sees. */
|
|
65
|
+
function toNativeFormatterMetadata(finding) {
|
|
66
|
+
const { start, end } = finding.range;
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
id: finding.id,
|
|
69
|
+
type: finding.type,
|
|
70
|
+
detector: finding.detector,
|
|
71
|
+
confidence: finding.confidence,
|
|
72
|
+
action: finding.action,
|
|
73
|
+
start,
|
|
74
|
+
end,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function toWasmPolicyCallback(policy) {
|
|
78
|
+
if (policy === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
return (finding, context) => policy(toNativeDetectedFinding(finding), context);
|
|
81
|
+
}
|
|
82
|
+
function toWasmFormatterCallback(formatter) {
|
|
83
|
+
if (formatter === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
return (finding, context) => formatter(toNativeFormatterMetadata(finding), context);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Loads the WebAssembly artifact published in lockstep with this package.
|
|
89
|
+
*
|
|
90
|
+
* The specifier is a literal so a bundler can resolve and include the glue and
|
|
91
|
+
* the `.wasm` binary it references, and the import is dynamic so nothing is
|
|
92
|
+
* fetched until a caller awaits `initialize()`.
|
|
93
|
+
*/
|
|
94
|
+
async function loadWasmModule() {
|
|
95
|
+
const module = (await import("@redact-secret/wasm"));
|
|
96
|
+
for (const name of [
|
|
97
|
+
"default",
|
|
98
|
+
"version",
|
|
99
|
+
"initialize",
|
|
100
|
+
"scan",
|
|
101
|
+
"redact",
|
|
102
|
+
"scanAndRedact",
|
|
103
|
+
]) {
|
|
104
|
+
if (typeof module[name] !== "function") {
|
|
105
|
+
throw new SecretScanError("INITIALIZATION_FAILED");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return module;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Builds the internal binding contract from an already-loaded WebAssembly
|
|
112
|
+
* module.
|
|
113
|
+
*
|
|
114
|
+
* Exported so a test double can exercise this exact normalization — the
|
|
115
|
+
* nested-metadata flattening above and the fixed `INCREMENTAL_UNAVAILABLE`
|
|
116
|
+
* rejection below — against a fake module shaped like the real artifact,
|
|
117
|
+
* without loading the artifact itself.
|
|
118
|
+
*/
|
|
119
|
+
export function createBindingFromWasmModule(wasm) {
|
|
120
|
+
return {
|
|
121
|
+
version: () => wasm.version(),
|
|
122
|
+
initialize: () => {
|
|
123
|
+
wasm.initialize();
|
|
124
|
+
},
|
|
125
|
+
scan: (input, policy) => wasm.scan(input, toWasmPolicyCallback(policy)).map(toNativeFinding),
|
|
126
|
+
redact: (input, findings, formatter) => wasm.redact(input, findings.map(toWasmFinding), toWasmFormatterCallback(formatter)),
|
|
127
|
+
scanAndRedact: (input, policy, formatter) => {
|
|
128
|
+
const result = wasm.scanAndRedact(input, toWasmPolicyCallback(policy), toWasmFormatterCallback(formatter));
|
|
129
|
+
return {
|
|
130
|
+
text: result.text,
|
|
131
|
+
findings: result.findings.map(toNativeFinding),
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
createIncrementalSanitizer: () => {
|
|
135
|
+
throw new SecretScanError("INCREMENTAL_UNAVAILABLE");
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export const loadNativeBinding = async () => {
|
|
140
|
+
const wasm = await loadWasmModule();
|
|
141
|
+
await wasm.default();
|
|
142
|
+
return createBindingFromWasmModule(wasm);
|
|
143
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node.js adapter: the N-API native addon, normalized to the internal
|
|
3
|
+
* binding contract (`decision-define-runtime-bindings`).
|
|
4
|
+
*
|
|
5
|
+
* The package's `imports` map reaches this module only under the `node`
|
|
6
|
+
* condition, so a browser build never resolves it and never pulls in the
|
|
7
|
+
* `node:` module below.
|
|
8
|
+
*
|
|
9
|
+
* Node's own loading has nothing to await, so `initialize()` here is a fast
|
|
10
|
+
* idempotent no-op in the addon. It stays part of the contract regardless, so
|
|
11
|
+
* the usage model does not vary by runtime.
|
|
12
|
+
*/
|
|
13
|
+
import type { NativeBinding, NativeFinding, NativeFormatterCallback, NativeIncrementalOptions, NativeIncrementalSanitizer, NativePolicyCallback } from "../native.js";
|
|
14
|
+
/**
|
|
15
|
+
* The addon's own exported shape. `scanAndRedact` names its text `redacted`,
|
|
16
|
+
* which this adapter renames to the contract's `text`; everything else is
|
|
17
|
+
* already the documented UTF-16 shape.
|
|
18
|
+
*
|
|
19
|
+
* `createIncrementalSanitizer` is optional: `bindings/node` does not implement
|
|
20
|
+
* it yet (unlike `bindings/python`, which wraps the same core
|
|
21
|
+
* `IncrementalSanitizer`), so this adapter treats its absence the same way
|
|
22
|
+
* `runtime/browser.ts` treats `bindings/wasm`'s documented non-support —
|
|
23
|
+
* `INCREMENTAL_UNAVAILABLE` at call time, not a load-time failure.
|
|
24
|
+
*/
|
|
25
|
+
interface NodeAddon {
|
|
26
|
+
version(): string;
|
|
27
|
+
initialize(): void;
|
|
28
|
+
scan(input: string, policy?: NativePolicyCallback): readonly NativeFinding[];
|
|
29
|
+
redact(input: string, findings: readonly NativeFinding[], formatter?: NativeFormatterCallback): string;
|
|
30
|
+
scanAndRedact(input: string, policy?: NativePolicyCallback, formatter?: NativeFormatterCallback): {
|
|
31
|
+
readonly findings: readonly NativeFinding[];
|
|
32
|
+
readonly redacted: string;
|
|
33
|
+
};
|
|
34
|
+
createIncrementalSanitizer?(options: NativeIncrementalOptions): NativeIncrementalSanitizer;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The addon package this host should have installed, or `undefined` on a
|
|
38
|
+
* platform/architecture this package ships no addon for at all — the
|
|
39
|
+
* runtime fallback that keeps an unsupported host's failure identical to a
|
|
40
|
+
* supported host whose optional dependency did not install: both reach
|
|
41
|
+
* `loadAddon`'s own `INITIALIZATION_FAILED`, never a raw `require` error.
|
|
42
|
+
*
|
|
43
|
+
* Exported so `scripts/qualify-node-addon.mjs` and
|
|
44
|
+
* `scripts/qualify-package-consumer.mjs` compute the same host-to-package
|
|
45
|
+
* mapping this module actually loads from, instead of restating it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveAddonSpecifier(): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Builds the internal binding contract from an already-loaded addon.
|
|
50
|
+
*
|
|
51
|
+
* Exported so a test double can exercise this exact normalization —
|
|
52
|
+
* including the `createIncrementalSanitizer` fallback — without loading the
|
|
53
|
+
* real addon, the way `runtime/browser.ts`'s
|
|
54
|
+
* `createBindingFromWasmModule` does for the WebAssembly artifact.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createBindingFromAddon(addon: NodeAddon): NativeBinding;
|
|
57
|
+
export declare const loadNativeBinding: () => Promise<NativeBinding>;
|
|
58
|
+
export {};
|