Skip to content

aidrecabrera/redact

Repository files navigation

@svene/redact

npm version types license

Tiny TypeScript wrapper for values that should not leak through logs, JSON, string coercion, or Node.js inspection.

Inspired by Dillon Mulroy's Redacted module and Effect's Redacted.

It helps prevent accidental exposure. It is not encryption, a secret manager, a sandbox, or a security boundary. Code that can call revealRedacted() can read the value.

install

npm install @svene/redact

basic use

import { redact, revealRedacted } from "@svene/redact";

const apiKey = redact("sk_live_123");

console.log(apiKey);       // <redacted>
String(apiKey);             // <redacted>
JSON.stringify({ apiKey }); // {"apiKey":"<redacted>"}

const raw = revealRedacted(apiKey); // sk_live_123

copy or install?

This package is small. If you only need it in one app, copying the code into your own utils is fine.

Install it if you want the tested version, the types, the examples, and the same behavior across projects.

why use it

Secrets leak through boring paths: debug logs, request dumps, JSON snapshots, test failures, console.log, and util.inspect.

@svene/redact makes the safe path the default. Raw access stays explicit.

The value is stored in an internal WeakMap, not on the wrapper object.

import { redact } from "@svene/redact";

const token = redact("secret");

Object.keys(token);                      // []
Reflect.ownKeys(token);                  // []
Object.getOwnPropertyDescriptors(token); // {}

type-safe secrets

Use Brand when two secrets share the same runtime type but must not be mixed.

import {
  createRedacted,
  revealRedacted,
  type Brand,
  type Redacted,
} from "@svene/redact";

type ApiKey = string & Brand<"ApiKey">;
type DbPassword = string & Brand<"DbPassword">;

const apiKey = "sk_live_123" as ApiKey;
const dbPassword = "password" as DbPassword;

const redactedApiKey: Redacted<ApiKey> = createRedacted(apiKey);
const redactedDbPassword: Redacted<DbPassword> = createRedacted(dbPassword);

const fetchUser = (key: Redacted<ApiKey>) => {
  const raw: ApiKey = revealRedacted(key);
  return raw;
};

fetchUser(redactedApiKey);     // ok
fetchUser(redactedDbPassword); // TypeScript error

disposal

Disposal removes the wrapper from the internal registry. After that, revealRedacted() throws.

import { disposeRedacted, redact, revealRedacted } from "@svene/redact";

const secret = redact("sensitive");

revealRedacted(secret); // sensitive
disposeRedacted(secret);
revealRedacted(secret); // throws TypeError

In runtimes where Symbol.dispose exists, wrappers also expose a runtime dispose hook. The public Redacted<T> type does not require ESNext.Disposable; ordinary TypeScript consumers can use disposeRedacted().

Disposal does not wipe memory. JavaScript does not provide reliable memory wiping for strings, and other references to the original value may still exist.

esm and commonjs

Both ESM and CommonJS entrypoints are published. In one JavaScript realm, wrappers created through import and require share the same registry, so either entrypoint can reveal or dispose wrappers created by the other.

api

redact(value)

Creates a Redacted<T> wrapper. Alias of createRedacted(value).

const secret = redact("value");

createRedacted(value)

Creates a Redacted<T> wrapper.

const secret = createRedacted("value");

revealRedacted(redacted)

Returns the wrapped value. Throws TypeError if the wrapper is invalid or disposed.

const raw = revealRedacted(secret);

isRedacted(value)

Returns true for live wrappers created by this module. Disposed wrappers return false.

if (isRedacted(value)) {
  revealRedacted(value);
}

disposeRedacted(redacted)

Removes the wrapper from the internal registry.

disposeRedacted(secret);

createEqRedacted(eq)

Builds equality for redacted values from equality for the wrapped value.

import { createEqRedacted, redact, type Brand } from "@svene/redact";

type ApiKey = string & Brand<"ApiKey">;

const eqApiKey = createEqRedacted<ApiKey>((a, b) => a === b);

const a = redact("x" as ApiKey);
const b = redact("x" as ApiKey);

eqApiKey(a, b); // true

types

type Redacted<T> // opaque wrapper for T
type Brand<Name extends string | symbol> // type-only nominal marker
type Eq<T> = (self: T, that: T) => boolean
type Unredact<T> // extracts T from Redacted<T>

output behavior

path output
String(secret) <redacted>
`${secret}` <redacted>
JSON.stringify(secret) "<redacted>"
JSON.stringify({ secret }) {"secret":"<redacted>"}
console.log(secret) in Node.js <redacted>
util.inspect(secret) in Node.js <redacted>
Object spread no secret
Reflect.ownKeys(secret) []

limits

This package does not protect against code that can reveal the value.

const secret = redact("sk_live_123");
revealRedacted(secret); // sk_live_123

It also does not replace logger-level redaction. Keep redaction rules for authorization headers, cookies, tokens, database URLs, and request bodies.

console.dir() in Node.js bypasses custom inspect hooks. The raw value still is not stored on the wrapper, but Node controls the output shape.

references

license

MIT

About

Redact sensitive values from logs, JSON, strings, and Node inspect.

Topics

Resources

License

Stars

Watchers

Forks

Contributors