@voiceflow/redacted 0.0.0-stage → 1.1.1-6c6187dc5.88
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 +69 -2
- package/build/expose/zod.d.mts +4 -0
- package/build/expose/zod.d.mts.map +1 -0
- package/build/expose/zod.mjs +12 -0
- package/build/expose/zod.mjs.map +1 -0
- package/build/expose.d.mts +3 -0
- package/build/expose.d.mts.map +1 -0
- package/build/expose.mjs +9 -0
- package/build/expose.mjs.map +1 -0
- package/build/index.d.mts +4 -0
- package/build/index.d.mts.map +1 -0
- package/build/index.mjs +1 -0
- package/build/redacted-ChfYPjlA.mjs +71 -0
- package/build/redacted-ChfYPjlA.mjs.map +1 -0
- package/build/redacted-ChiwQxek.d.mts +24 -0
- package/build/redacted-ChiwQxek.d.mts.map +1 -0
- package/build/testing.d.mts +7 -0
- package/build/testing.d.mts.map +1 -0
- package/build/testing.mjs +39 -0
- package/build/testing.mjs.map +1 -0
- package/build/zod.d.mts +5 -0
- package/build/zod.d.mts.map +1 -0
- package/build/zod.mjs +8 -0
- package/build/zod.mjs.map +1 -0
- package/package.json +55 -4
package/README.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
#
|
|
1
|
+
# redacted
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A secret wrapper that renders as `[secret:<id>]` everywhere, and only gives up its value through an explicit `expose()`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
yarn add --exact @voiceflow/redacted
|
|
9
|
+
|
|
10
|
+
# for the /zod and /expose/zod subpaths
|
|
11
|
+
yarn add --exact zod
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Rules
|
|
15
|
+
|
|
16
|
+
1. A secret is a `Redacted`. Secret sources never return `string`.
|
|
17
|
+
2. Log, trace, throw or stringify freely; the output is `[secret:<id>]`. Spreading or `structuredClone` drops the wrapper and leaves only `{ id }`.
|
|
18
|
+
3. Transform only in a boundary file: `Redacted.make(transform(expose(secret)), secret.id)`. Never call `JSON.parse` on a secret anywhere else, and catch and rethrow its failures without the original error: V8's `SyntaxError` message quotes the first characters of the input.
|
|
19
|
+
4. Call `expose()` once per credential, in a boundary file, as close as possible to the SDK constructor or header.
|
|
20
|
+
5. Across the wire, use the codec: decode on receipt, encode in a boundary file.
|
|
21
|
+
6. Compare with `Redacted.equals`. Tests register the equality tester.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { Redacted } from '@voiceflow/redacted';
|
|
27
|
+
import { expose } from '@voiceflow/redacted/expose';
|
|
28
|
+
|
|
29
|
+
const apiKey = Redacted.make(process.env.ELEVEN_LABS_API_KEY!, 'ELEVEN_LABS_API_KEY');
|
|
30
|
+
logger.info({ apiKey }, 'creating client'); // {"apiKey":"[secret:ELEVEN_LABS_API_KEY]"}
|
|
31
|
+
|
|
32
|
+
// google-auth.boundary.ts
|
|
33
|
+
const account = Redacted.make(ServiceAccountDTO.parse(JSON.parse(expose(serviceAccountJson))), serviceAccountJson.id);
|
|
34
|
+
|
|
35
|
+
// eleven-labs.boundary.ts
|
|
36
|
+
const client = new ElevenLabsClient({ apiKey: expose(apiKey) });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`Redacted` has no `Symbol.toPrimitive` or `valueOf`, so passing one where a `string` is expected (`Headers`, `Buffer.from`, an SDK option, `===`) is a compile error. A template literal or `String()` still compiles and silently produces the placeholder, which is safe in a log line and wrong in an `Authorization` header: that is what rule 4 is for.
|
|
40
|
+
|
|
41
|
+
`Redacted.make` throws a `TypeError` on a placeholder-shaped string (`[secret]` or `[secret:<id>]`), so a forgotten boundary encode fails loudly instead of persisting `[secret:<id>]`; `zRedactedFromString` and the codec report the same input as a zod issue.
|
|
42
|
+
|
|
43
|
+
`Redacted.make(redacted, id)` returns the same instance for the same or no id, and a re-keyed copy for a different id, so a value decoded from the wire (which has no id) gets its id without passing through `expose()`. `expose()` throws on a prototype copy such as `Object.create(secret)` or a `Proxy`, which keep the brand but not the value.
|
|
44
|
+
|
|
45
|
+
`Redacted.equals` is a best-effort constant-time comparison: it compares every UTF-16 code unit of the longer value without exiting early, but JavaScript engines give no strict timing guarantee. It is string-only and throws a `TypeError` otherwise.
|
|
46
|
+
|
|
47
|
+
## Where `expose()` is allowed
|
|
48
|
+
|
|
49
|
+
`@voiceflow/redacted/expose`, `@voiceflow/redacted/expose/zod` and `@voiceflow/redacted/testing` are the only paths to a secret's plaintext, so `rg 'redacted/(expose|testing)'` lists every file that can hold plaintext. In this repo, `no-restricted-imports` in `oxlint.config.mjs` blocks all three subpaths everywhere except the allowlist override and test files; a new boundary file is added to that override. Keep each boundary file small and keep the exposed value inside it: hand the SDK client or the outgoing request to the rest of the code, never the string.
|
|
50
|
+
|
|
51
|
+
A shared vitest setup file that registers the equality tester is not a test file by glob and needs an allowlist entry.
|
|
52
|
+
|
|
53
|
+
## Subpaths
|
|
54
|
+
|
|
55
|
+
| Import | Exports |
|
|
56
|
+
| -------------------------------- | --------------------------------------------------- |
|
|
57
|
+
| `@voiceflow/redacted` | `Redacted`, `toSecretPlaceholder` |
|
|
58
|
+
| `@voiceflow/redacted/expose` | `expose` |
|
|
59
|
+
| `@voiceflow/redacted/expose/zod` | `redactedStringCodec` (requires `zod`) |
|
|
60
|
+
| `@voiceflow/redacted/zod` | `zRedacted`, `zRedactedFromString` (requires `zod`) |
|
|
61
|
+
| `@voiceflow/redacted/testing` | `redactedEqualityTester`, `assertNoSecretLeak` |
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// vitest
|
|
65
|
+
import { assertNoSecretLeak, redactedEqualityTester } from '@voiceflow/redacted/testing';
|
|
66
|
+
|
|
67
|
+
// compares id and the exposed values with vitest's own deep equality
|
|
68
|
+
expect.addEqualityTesters([redactedEqualityTester]);
|
|
69
|
+
assertNoSecretLeak(capturedLogs, ['sk_live_canary_value']);
|
|
70
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.d.mts","names":[],"sources":["../../src/expose.zod.ts"],"mappings":";;qBAUa,qBAAmB,EAAA,SAAA,EAAA,WAAA,EAAA,UAAA,kBAAA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { n as Redacted, r as isSecretPlaceholder } from "../redacted-ChfYPjlA.mjs";
|
|
2
|
+
import { expose } from "../expose.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
// src/expose.zod.ts
|
|
5
|
+
const zRedactedString = z.custom((value) => Redacted.isRedacted(value), { error: "Expected Redacted" });
|
|
6
|
+
const redactedStringCodec = z.codec(z.string().refine((value) => !isSecretPlaceholder(value), { error: "Secret placeholder is not a secret" }), zRedactedString, {
|
|
7
|
+
decode: (value) => Redacted.make(value),
|
|
8
|
+
encode: (redacted) => expose(redacted)
|
|
9
|
+
});
|
|
10
|
+
export { redactedStringCodec };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=zod.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.mjs","names":[],"sources":["../../src/expose.zod.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { expose } from './expose';\nimport { Redacted } from './redacted';\nimport { isSecretPlaceholder } from './secret-placeholder.util';\n\nconst zRedactedString = z.custom<Redacted<string>>((value) => Redacted.isRedacted(value), {\n error: 'Expected Redacted',\n});\n\nexport const redactedStringCodec = z.codec(\n z.string().refine((value) => !isSecretPlaceholder(value), { error: 'Secret placeholder is not a secret' }),\n zRedactedString,\n {\n decode: (value) => Redacted.make(value),\n encode: (redacted) => expose(redacted),\n }\n);\n"],"mappings":";;;;AAMA,MAAM,kBAAkB,EAAE,QAA0B,UAAU,SAAS,WAAW,KAAK,GAAG,EACxF,OAAO,oBACT,CAAC;AAED,MAAa,sBAAsB,EAAE,MACnC,EAAE,OAAO,CAAC,CAAC,QAAQ,UAAU,CAAC,oBAAoB,KAAK,GAAG,EAAE,OAAO,qCAAqC,CAAC,GACzG,iBACA;CACE,SAAS,UAAU,SAAS,KAAK,KAAK;CACtC,SAAS,aAAa,OAAO,QAAQ;AACvC,CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expose.d.mts","names":[],"sources":["../src/expose.ts"],"mappings":";wBAEgB,OAAO,GAAG,GAAG,SAAS,KAAK"}
|
package/build/expose.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { n as Redacted, t as EXPOSE } from "./redacted-ChfYPjlA.mjs";
|
|
2
|
+
// src/expose.ts
|
|
3
|
+
function expose(r) {
|
|
4
|
+
if (!Redacted.isRedacted(r)) throw new TypeError("expose() expects a Redacted value");
|
|
5
|
+
return r[EXPOSE]();
|
|
6
|
+
}
|
|
7
|
+
export { expose };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=expose.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expose.mjs","names":[],"sources":["../src/expose.ts"],"sourcesContent":["import { EXPOSE, Redacted } from './redacted';\n\nexport function expose<T>(r: Redacted<T>): T {\n if (!Redacted.isRedacted(r)) throw new TypeError('expose() expects a Redacted value');\n\n return r[EXPOSE]();\n}\n"],"mappings":";;AAEA,SAAgB,OAAU,GAAmB;CAC3C,IAAI,CAAC,SAAS,WAAW,CAAC,GAAG,MAAM,IAAI,UAAU,mCAAmC;CAEpF,OAAO,EAAE,OAAO,CAAC;AACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/secret-placeholder.util.ts"],"mappings":";qBAEa,sBAAuB"}
|
package/build/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as Redacted, i as toSecretPlaceholder } from "./redacted-ChfYPjlA.mjs";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/secret-placeholder.util.ts
|
|
2
|
+
const SECRET_PLACEHOLDER = /^\[secret(?::.*)?\]$/s;
|
|
3
|
+
const toSecretPlaceholder = (id) => id === void 0 ? "[secret]" : `[secret:${id}]`;
|
|
4
|
+
const isSecretPlaceholder = (value) => SECRET_PLACEHOLDER.test(value);
|
|
5
|
+
// src/redacted.ts
|
|
6
|
+
const BRAND = Symbol.for("@voiceflow/redacted");
|
|
7
|
+
const INSPECT = Symbol.for("nodejs.util.inspect.custom");
|
|
8
|
+
const EXPOSE = Symbol.for("@voiceflow/redacted/expose");
|
|
9
|
+
const VALUES = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
const assertId = (id) => {
|
|
11
|
+
if (id !== void 0 && (typeof id !== "string" || id.length === 0)) throw new TypeError("Redacted id must be a non-empty string");
|
|
12
|
+
};
|
|
13
|
+
var Redacted = class Redacted {
|
|
14
|
+
static make(value, id) {
|
|
15
|
+
assertId(id);
|
|
16
|
+
if (Redacted.isRedacted(value)) return id === void 0 || id === value.id ? value : new Redacted(Redacted.#read(value), id);
|
|
17
|
+
if (typeof value === "string" && isSecretPlaceholder(value)) throw new TypeError(`Redacted value${id === void 0 ? "" : ` for ${id}`} must not be a secret placeholder`);
|
|
18
|
+
return new Redacted(value, id);
|
|
19
|
+
}
|
|
20
|
+
static isRedacted(u) {
|
|
21
|
+
return typeof u === "object" && u !== null && u[BRAND] === true;
|
|
22
|
+
}
|
|
23
|
+
static fromRecord(record) {
|
|
24
|
+
return Object.freeze(Object.fromEntries(Object.entries(record).map(([key, value]) => [key, Redacted.make(value, key)])));
|
|
25
|
+
}
|
|
26
|
+
/** Best-effort constant-time over UTF-16 code units; JavaScript engines give no strict timing guarantee. */
|
|
27
|
+
static equals(a, b) {
|
|
28
|
+
const left = Redacted.#read(a);
|
|
29
|
+
const right = Redacted.#read(b);
|
|
30
|
+
if (typeof left !== "string" || typeof right !== "string") throw new TypeError("Redacted.equals compares string secrets only");
|
|
31
|
+
const length = Math.max(left.length, right.length);
|
|
32
|
+
let diff = left.length ^ right.length;
|
|
33
|
+
for (let index = 0; index < length; index++) diff |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
|
|
34
|
+
return diff === 0;
|
|
35
|
+
}
|
|
36
|
+
static #read(r) {
|
|
37
|
+
if (!Redacted.isRedacted(r)) throw new TypeError("Expected a Redacted value");
|
|
38
|
+
return r[EXPOSE]();
|
|
39
|
+
}
|
|
40
|
+
id;
|
|
41
|
+
constructor(value, id) {
|
|
42
|
+
VALUES.set(this, value);
|
|
43
|
+
this.id = id;
|
|
44
|
+
Object.freeze(this);
|
|
45
|
+
}
|
|
46
|
+
get [Symbol.toStringTag]() {
|
|
47
|
+
return "Redacted";
|
|
48
|
+
}
|
|
49
|
+
get [BRAND]() {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
toString() {
|
|
53
|
+
return toSecretPlaceholder(this.id);
|
|
54
|
+
}
|
|
55
|
+
toJSON() {
|
|
56
|
+
return this.toString();
|
|
57
|
+
}
|
|
58
|
+
toBSON() {
|
|
59
|
+
return this.toString();
|
|
60
|
+
}
|
|
61
|
+
[INSPECT]() {
|
|
62
|
+
return this.toString();
|
|
63
|
+
}
|
|
64
|
+
[EXPOSE]() {
|
|
65
|
+
if (!VALUES.has(this)) throw new TypeError("Expected a Redacted instance, not a prototype copy");
|
|
66
|
+
return VALUES.get(this);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
export { toSecretPlaceholder as i, Redacted as n, isSecretPlaceholder as r, EXPOSE as t };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=redacted-ChfYPjlA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redacted-ChfYPjlA.mjs","names":[],"sources":["../src/secret-placeholder.util.ts","../src/redacted.ts"],"sourcesContent":["const SECRET_PLACEHOLDER = /^\\[secret(?::.*)?\\]$/s;\n\nexport const toSecretPlaceholder = (id?: string): string => (id === undefined ? '[secret]' : `[secret:${id}]`);\n\nexport const isSecretPlaceholder = (value: string): boolean => SECRET_PLACEHOLDER.test(value);\n","import { isSecretPlaceholder, toSecretPlaceholder } from './secret-placeholder.util';\n\nconst BRAND: unique symbol = Symbol.for('@voiceflow/redacted');\nconst INSPECT: unique symbol = Symbol.for('nodejs.util.inspect.custom');\nexport const EXPOSE: unique symbol = Symbol.for('@voiceflow/redacted/expose');\n\n// A WeakMap rather than a #private field, which the inspector protocol lists as a property of the instance.\nconst VALUES = new WeakMap<object, unknown>();\n\nconst assertId = (id: unknown): void => {\n if (id !== undefined && (typeof id !== 'string' || id.length === 0)) {\n throw new TypeError('Redacted id must be a non-empty string');\n }\n};\n\nexport class Redacted<out T = string> {\n /** Returns the same instance, or a re-keyed copy when a different id is given. */\n static make<T>(value: Redacted<T>, id?: string): Redacted<T>;\n\n static make<T>(value: T, id?: string): Redacted<T>;\n\n static make(value: unknown, id?: string): Redacted<unknown> {\n assertId(id);\n\n if (Redacted.isRedacted(value)) {\n return id === undefined || id === value.id ? value : new Redacted(Redacted.#read(value), id);\n }\n\n if (typeof value === 'string' && isSecretPlaceholder(value)) {\n throw new TypeError(`Redacted value${id === undefined ? '' : ` for ${id}`} must not be a secret placeholder`);\n }\n\n return new Redacted(value, id);\n }\n\n static isRedacted(u: unknown): u is Redacted<unknown> {\n return typeof u === 'object' && u !== null && (u as Partial<Record<typeof BRAND, unknown>>)[BRAND] === true;\n }\n\n static fromRecord(record: Readonly<Record<string, string>>): Readonly<Record<string, Redacted<string>>> {\n return Object.freeze(\n Object.fromEntries(Object.entries(record).map(([key, value]) => [key, Redacted.make(value, key)]))\n );\n }\n\n /** Best-effort constant-time over UTF-16 code units; JavaScript engines give no strict timing guarantee. */\n static equals(a: Redacted<string>, b: Redacted<string>): boolean {\n const left: unknown = Redacted.#read(a);\n const right: unknown = Redacted.#read(b);\n\n if (typeof left !== 'string' || typeof right !== 'string') {\n throw new TypeError('Redacted.equals compares string secrets only');\n }\n\n const length = Math.max(left.length, right.length);\n\n let diff = left.length ^ right.length;\n\n for (let index = 0; index < length; index++) {\n diff |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);\n }\n\n return diff === 0;\n }\n\n // Reads through the instance's own method so instances from another copy of this package still work.\n static #read<T>(r: Redacted<T>): T {\n if (!Redacted.isRedacted(r)) throw new TypeError('Expected a Redacted value');\n\n return r[EXPOSE]();\n }\n\n readonly id: string | undefined;\n\n private constructor(value: T, id: string | undefined) {\n VALUES.set(this, value);\n this.id = id;\n\n Object.freeze(this);\n }\n\n get [Symbol.toStringTag](): string {\n return 'Redacted';\n }\n\n get [BRAND](): true {\n return true;\n }\n\n toString(): string {\n return toSecretPlaceholder(this.id);\n }\n\n toJSON(): string {\n return this.toString();\n }\n\n toBSON(): string {\n return this.toString();\n }\n\n [INSPECT](): string {\n return this.toString();\n }\n\n [EXPOSE](): T {\n if (!VALUES.has(this)) throw new TypeError('Expected a Redacted instance, not a prototype copy');\n\n return VALUES.get(this) as T;\n }\n}\n"],"mappings":";AAAA,MAAM,qBAAqB;AAE3B,MAAa,uBAAuB,OAAyB,OAAO,KAAA,IAAY,aAAa,WAAW,GAAG;AAE3G,MAAa,uBAAuB,UAA2B,mBAAmB,KAAK,KAAK;;ACF5F,MAAM,QAAuB,OAAO,IAAI,qBAAqB;AAC7D,MAAM,UAAyB,OAAO,IAAI,4BAA4B;AACtE,MAAa,SAAwB,OAAO,IAAI,4BAA4B;AAG5E,MAAM,yBAAS,IAAI,QAAyB;AAE5C,MAAM,YAAY,OAAsB;CACtC,IAAI,OAAO,KAAA,MAAc,OAAO,OAAO,YAAY,GAAG,WAAW,IAC/D,MAAM,IAAI,UAAU,wCAAwC;AAEhE;AAEA,IAAa,WAAb,MAAa,SAAyB;CAMpC,OAAO,KAAK,OAAgB,IAAgC;EAC1D,SAAS,EAAE;EAEX,IAAI,SAAS,WAAW,KAAK,GAC3B,OAAO,OAAO,KAAA,KAAa,OAAO,MAAM,KAAK,QAAQ,IAAI,SAAS,SAAS,MAAM,KAAK,GAAG,EAAE;EAG7F,IAAI,OAAO,UAAU,YAAY,oBAAoB,KAAK,GACxD,MAAM,IAAI,UAAU,iBAAiB,OAAO,KAAA,IAAY,KAAK,QAAQ,KAAK,kCAAkC;EAG9G,OAAO,IAAI,SAAS,OAAO,EAAE;CAC/B;CAEA,OAAO,WAAW,GAAoC;EACpD,OAAO,OAAO,MAAM,YAAY,MAAM,QAAS,EAA6C,WAAW;CACzG;CAEA,OAAO,WAAW,QAAsF;EACtG,OAAO,OAAO,OACZ,OAAO,YAAY,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,SAAS,KAAK,OAAO,GAAG,CAAC,CAAC,CAAC,CACnG;CACF;;CAGA,OAAO,OAAO,GAAqB,GAA8B;EAC/D,MAAM,OAAgB,SAAS,MAAM,CAAC;EACtC,MAAM,QAAiB,SAAS,MAAM,CAAC;EAEvC,IAAI,OAAO,SAAS,YAAY,OAAO,UAAU,UAC/C,MAAM,IAAI,UAAU,8CAA8C;EAGpE,MAAM,SAAS,KAAK,IAAI,KAAK,QAAQ,MAAM,MAAM;EAEjD,IAAI,OAAO,KAAK,SAAS,MAAM;EAE/B,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,SAClC,SAAS,KAAK,WAAW,KAAK,KAAK,MAAM,MAAM,WAAW,KAAK,KAAK;EAGtE,OAAO,SAAS;CAClB;CAGA,OAAO,MAAS,GAAmB;EACjC,IAAI,CAAC,SAAS,WAAW,CAAC,GAAG,MAAM,IAAI,UAAU,2BAA2B;EAE5E,OAAO,EAAE,OAAO,CAAC;CACnB;CAEA;CAEA,YAAoB,OAAU,IAAwB;EACpD,OAAO,IAAI,MAAM,KAAK;EACtB,KAAK,KAAK;EAEV,OAAO,OAAO,IAAI;CACpB;CAEA,KAAK,OAAO,eAAuB;EACjC,OAAO;CACT;CAEA,KAAK,SAAe;EAClB,OAAO;CACT;CAEA,WAAmB;EACjB,OAAO,oBAAoB,KAAK,EAAE;CACpC;CAEA,SAAiB;EACf,OAAO,KAAK,SAAS;CACvB;CAEA,SAAiB;EACf,OAAO,KAAK,SAAS;CACvB;CAEA,CAAC,WAAmB;EAClB,OAAO,KAAK,SAAS;CACvB;CAEA,CAAC,UAAa;EACZ,IAAI,CAAC,OAAO,IAAI,IAAI,GAAG,MAAM,IAAI,UAAU,oDAAoD;EAE/F,OAAO,OAAO,IAAI,IAAI;CACxB;AACF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare const BRAND: unique symbol;
|
|
2
|
+
declare const INSPECT: unique symbol;
|
|
3
|
+
declare const EXPOSE: unique symbol;
|
|
4
|
+
declare class Redacted<out T = string> {
|
|
5
|
+
#private;
|
|
6
|
+
/** Returns the same instance, or a re-keyed copy when a different id is given. */
|
|
7
|
+
static make<T>(value: Redacted<T>, id?: string): Redacted<T>;
|
|
8
|
+
static make<T>(value: T, id?: string): Redacted<T>;
|
|
9
|
+
static isRedacted(u: unknown): u is Redacted<unknown>;
|
|
10
|
+
static fromRecord(record: Readonly<Record<string, string>>): Readonly<Record<string, Redacted<string>>>;
|
|
11
|
+
/** Best-effort constant-time over UTF-16 code units; JavaScript engines give no strict timing guarantee. */
|
|
12
|
+
static equals(a: Redacted<string>, b: Redacted<string>): boolean;
|
|
13
|
+
readonly id: string | undefined;
|
|
14
|
+
private constructor();
|
|
15
|
+
get [Symbol.toStringTag](): string;
|
|
16
|
+
get [BRAND](): true;
|
|
17
|
+
toString(): string;
|
|
18
|
+
toJSON(): string;
|
|
19
|
+
toBSON(): string;
|
|
20
|
+
[INSPECT](): string;
|
|
21
|
+
[EXPOSE](): T;
|
|
22
|
+
}
|
|
23
|
+
export { Redacted as t };
|
|
24
|
+
//# sourceMappingURL=redacted-ChiwQxek.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redacted-ChiwQxek.d.mts","names":[],"sources":["../src/redacted.ts"],"mappings":"A,cAEM;cACA;cACO;cAWA,aAAa;;;SAEjB,KAAK,GAAG,OAAO,SAAS,IAAI,cAAc,SAAS;SAEnD,KAAK,GAAG,OAAO,GAAG,cAAc,SAAS;SAgBzC,WAAW,aAAa,KAAK;SAI7B,WAAW,QAAQ,SAAS,0BAA0B,SAAS,eAAe;;SAO9E,OAAO,GAAG,kBAAkB,GAAG;WA0B7B;UAEF;OAOF,OAAO;OAIP;EAIL;EAIA;EAIA;GAIC;GAIA,WAAW"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface EqualityTesterContext {
|
|
2
|
+
equals(a: unknown, b: unknown, customTesters?: readonly unknown[]): boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare function redactedEqualityTester(this: EqualityTesterContext, a: unknown, b: unknown, customTesters: readonly unknown[]): boolean | undefined;
|
|
5
|
+
export declare function assertNoSecretLeak(haystack: string, canaries: readonly string[]): void;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=testing.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.mts","names":[],"sources":["../src/testing.ts"],"mappings":"A,UAMU;EACR,OAAO,YAAY,YAAY;;wBAGjB,uBACd,MAAM,uBACN,YACA,YACA;wBAiCc,mBAAmB,kBAAkB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { n as Redacted } from "./redacted-ChfYPjlA.mjs";
|
|
2
|
+
import { expose } from "./expose.mjs";
|
|
3
|
+
// src/testing.ts
|
|
4
|
+
const ENCODER = new TextEncoder();
|
|
5
|
+
function redactedEqualityTester(a, b, customTesters) {
|
|
6
|
+
if (!Redacted.isRedacted(a) || !Redacted.isRedacted(b)) return void 0;
|
|
7
|
+
return a.id === b.id && this.equals(expose(a), expose(b), customTesters);
|
|
8
|
+
}
|
|
9
|
+
const toBase64 = (canary) => {
|
|
10
|
+
let binary = "";
|
|
11
|
+
for (const byte of ENCODER.encode(canary)) binary += String.fromCharCode(byte);
|
|
12
|
+
return btoa(binary);
|
|
13
|
+
};
|
|
14
|
+
const toURLEncoded = (canary) => {
|
|
15
|
+
try {
|
|
16
|
+
return encodeURIComponent(canary);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const LEAK_FORMS = [
|
|
22
|
+
["raw", (canary) => canary],
|
|
23
|
+
["prefix", (canary) => canary.length >= 12 ? canary.slice(0, 10) : null],
|
|
24
|
+
["base64", toBase64],
|
|
25
|
+
["url-encoded", toURLEncoded],
|
|
26
|
+
["json-escaped", (canary) => JSON.stringify(canary).slice(1, -1)]
|
|
27
|
+
];
|
|
28
|
+
function assertNoSecretLeak(haystack, canaries) {
|
|
29
|
+
canaries.forEach((canary, index) => {
|
|
30
|
+
if (canary.length === 0) throw new TypeError(`Secret canary ${index} is empty`);
|
|
31
|
+
for (const [form, encode] of LEAK_FORMS) {
|
|
32
|
+
const needle = encode(canary);
|
|
33
|
+
if (needle && haystack.includes(needle)) throw new Error(`Secret canary ${index} leaked in ${form} form`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export { assertNoSecretLeak, redactedEqualityTester };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import { expose } from './expose';\nimport { Redacted } from './redacted';\n\nconst ENCODER = new TextEncoder();\n\n// Structural stand-in for vitest's TesterContext, which vitest 4 does not export.\ninterface EqualityTesterContext {\n equals(a: unknown, b: unknown, customTesters?: readonly unknown[]): boolean;\n}\n\nexport function redactedEqualityTester(\n this: EqualityTesterContext,\n a: unknown,\n b: unknown,\n customTesters: readonly unknown[]\n): boolean | undefined {\n if (!Redacted.isRedacted(a) || !Redacted.isRedacted(b)) return undefined;\n\n // oxlint-disable-next-line oxc/no-this-in-exported-function -- vitest calls testers with its context as `this`\n return a.id === b.id && this.equals(expose(a), expose(b), customTesters);\n}\n\nconst toBase64 = (canary: string): string => {\n let binary = '';\n\n for (const byte of ENCODER.encode(canary)) binary += String.fromCharCode(byte);\n\n return btoa(binary);\n};\n\n// encodeURIComponent throws URIError on an unpaired surrogate; a canary that cannot be URL-encoded cannot leak in that form.\nconst toURLEncoded = (canary: string): string | null => {\n try {\n return encodeURIComponent(canary);\n } catch {\n return null;\n }\n};\n\nconst LEAK_FORMS: ReadonlyArray<readonly [form: string, encode: (canary: string) => string | null]> = [\n ['raw', (canary) => canary],\n ['prefix', (canary) => (canary.length >= 12 ? canary.slice(0, 10) : null)],\n ['base64', toBase64],\n ['url-encoded', toURLEncoded],\n ['json-escaped', (canary) => JSON.stringify(canary).slice(1, -1)],\n];\n\nexport function assertNoSecretLeak(haystack: string, canaries: readonly string[]): void {\n canaries.forEach((canary, index) => {\n if (canary.length === 0) throw new TypeError(`Secret canary ${index} is empty`);\n\n for (const [form, encode] of LEAK_FORMS) {\n const needle = encode(canary);\n\n if (needle && haystack.includes(needle)) {\n throw new Error(`Secret canary ${index} leaked in ${form} form`);\n }\n }\n });\n}\n"],"mappings":";;;AAGA,MAAM,UAAU,IAAI,YAAY;AAOhC,SAAgB,uBAEd,GACA,GACA,eACqB;CACrB,IAAI,CAAC,SAAS,WAAW,CAAC,KAAK,CAAC,SAAS,WAAW,CAAC,GAAG,OAAO,KAAA;CAG/D,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,OAAO,CAAC,GAAG,OAAO,CAAC,GAAG,aAAa;AACzE;AAEA,MAAM,YAAY,WAA2B;CAC3C,IAAI,SAAS;CAEb,KAAK,MAAM,QAAQ,QAAQ,OAAO,MAAM,GAAG,UAAU,OAAO,aAAa,IAAI;CAE7E,OAAO,KAAK,MAAM;AACpB;AAGA,MAAM,gBAAgB,WAAkC;CACtD,IAAI;EACF,OAAO,mBAAmB,MAAM;CAClC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,aAAgG;CACpG,CAAC,QAAQ,WAAW,MAAM;CAC1B,CAAC,WAAW,WAAY,OAAO,UAAU,KAAK,OAAO,MAAM,GAAG,EAAE,IAAI,IAAK;CACzE,CAAC,UAAU,QAAQ;CACnB,CAAC,eAAe,YAAY;CAC5B,CAAC,iBAAiB,WAAW,KAAK,UAAU,MAAM,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC;AAClE;AAEA,SAAgB,mBAAmB,UAAkB,UAAmC;CACtF,SAAS,SAAS,QAAQ,UAAU;EAClC,IAAI,OAAO,WAAW,GAAG,MAAM,IAAI,UAAU,iBAAiB,MAAM,UAAU;EAE9E,KAAK,MAAM,CAAC,MAAM,WAAW,YAAY;GACvC,MAAM,SAAS,OAAO,MAAM;GAE5B,IAAI,UAAU,SAAS,SAAS,MAAM,GACpC,MAAM,IAAI,MAAM,iBAAiB,MAAM,aAAa,KAAK,MAAM;EAEnE;CACF,CAAC;AACH"}
|
package/build/zod.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.d.mts","names":[],"sources":["../src/zod.ts"],"mappings":";;qBAKa,WAAW,EAAE,QAAQ;qBAKrB,qBAAqB,EAAE,QAAQ"}
|
package/build/zod.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { n as Redacted, r as isSecretPlaceholder } from "./redacted-ChfYPjlA.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// src/zod.ts
|
|
4
|
+
const zRedacted = z.custom((value) => Redacted.isRedacted(value), { error: "Expected Redacted" });
|
|
5
|
+
const zRedactedFromString = z.string().refine((value) => !isSecretPlaceholder(value), { error: "Secret placeholder is not a secret" }).transform((value) => Redacted.make(value));
|
|
6
|
+
export { zRedacted, zRedactedFromString };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=zod.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.mjs","names":[],"sources":["../src/zod.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { Redacted } from './redacted';\nimport { isSecretPlaceholder } from './secret-placeholder.util';\n\nexport const zRedacted: z.ZodType<Redacted<unknown>> = z.custom<Redacted<unknown>>(\n (value) => Redacted.isRedacted(value),\n { error: 'Expected Redacted' }\n);\n\nexport const zRedactedFromString: z.ZodType<Redacted<string>, string> = z\n .string()\n .refine((value) => !isSecretPlaceholder(value), { error: 'Secret placeholder is not a secret' })\n .transform((value) => Redacted.make(value));\n"],"mappings":";;;AAKA,MAAa,YAA0C,EAAE,QACtD,UAAU,SAAS,WAAW,KAAK,GACpC,EAAE,OAAO,oBAAoB,CAC/B;AAEA,MAAa,sBAA2D,EACrE,OAAO,CAAC,CACR,QAAQ,UAAU,CAAC,oBAAoB,KAAK,GAAG,EAAE,OAAO,qCAAqC,CAAC,CAAC,CAC/F,WAAW,UAAU,SAAS,KAAK,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voiceflow/redacted",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.1-6c6187dc5.88+6c6187dc5",
|
|
4
|
+
"description": "Secret wrapper that renders as a placeholder everywhere except an explicit expose()",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"files": [
|
|
7
|
+
"build"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./build/index.mjs",
|
|
12
|
+
"./expose": "./build/expose.mjs",
|
|
13
|
+
"./expose/zod": "./build/expose/zod.mjs",
|
|
14
|
+
"./testing": "./build/testing.mjs",
|
|
15
|
+
"./zod": "./build/zod.mjs"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "yarn g:turbo run build:cmd --filter=@voiceflow/redacted...",
|
|
22
|
+
"build:cmd": "yarn g:tsdown",
|
|
23
|
+
"clean": "yarn g:rimraf build",
|
|
24
|
+
"lint": "yarn g:run-p -c lint:oxlint lint:oxfmt",
|
|
25
|
+
"lint:oxlint": "yarn g:oxlint .",
|
|
26
|
+
"lint:fix": "yarn g:run-p -c \"g:oxlint . --fix\" \"g:oxfmt .\"",
|
|
27
|
+
"lint:oxfmt": "yarn g:oxfmt --check .",
|
|
28
|
+
"tdd": "yarn g:vitest",
|
|
29
|
+
"test": "yarn g:run-p -c test:types test:unit test:cycles test:unused",
|
|
30
|
+
"test:cycles": "yarn g:dpdm src/main.ts src/expose.ts src/expose.zod.ts src/zod.ts src/testing.ts",
|
|
31
|
+
"test:types": "yarn g:tsc --noEmit",
|
|
32
|
+
"test:unit": "yarn g:vitest run --coverage",
|
|
33
|
+
"test:unused": "yarn g:knip --workspace @voiceflow/redacted"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/lodash": "4.14.191",
|
|
37
|
+
"bson": "7.3.3",
|
|
38
|
+
"lodash": "4.18.1",
|
|
39
|
+
"pino": "8.17.2",
|
|
40
|
+
"zod": "4.4.3"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"zod": "^4.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"zod": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": "26"
|
|
52
|
+
},
|
|
53
|
+
"volta": {
|
|
54
|
+
"extends": "../../package.json"
|
|
55
|
+
},
|
|
56
|
+
"gitHead": "6c6187dc5bf5a37eb5e25c90d62ef066daa0a923"
|
|
57
|
+
}
|