Skip to content

ekaone/shielded

Repository files navigation

@ekaone/shielded

Typescript data safety primitive that prevents sensitive state from leaking through logging, devtools, and serialization.


The Problem Space

Modern JavaScript applications handle sensitive data constantly — auth tokens, payment details, PII, session credentials. The risk isn't always a malicious attacker. Most leaks happen from your own team, during normal development and operations.

Console logging during debugging

// Developer debugging an auth issue
console.log("current store:", store)
// → { user: "Eka", token: "eyJhbGciOiJIUzI1NiIsInR..." } ❌

A single console.log in a code review, a debug session, or a staging environment can expose tokens that were never meant to be visible.

Error tracking capturing full state

Sentry.captureException(err, { extra: store })
// → token, cardToken, ssn all appear in your Sentry dashboard ❌

Tools like Sentry, Datadog, and LogRocket serialize everything they receive. If your store is in scope when an error occurs, every key goes to the cloud.

JSON serialization leaking to the client

// Server-side rendering — state serialized into HTML
const html = `<script>window.__STATE__ = ${JSON.stringify(store)}</script>`
// → sensitive fields visible in page source ❌

SSR hydration is a common vector for leaking server-side secrets to the browser.

Devtools broadcasting everything

Redux DevTools, Zustand devtools, and React Query devtools display the full store in real time. Any sensitive value in state is immediately visible to anyone with devtools open — including in production if devtools extensions are installed.

The core issue

No existing state manager addresses this at the primitive level. The safe path requires manually sanitizing every log call, every error boundary, every analytics event, and every serialization point. @ekaone/shielded makes the safe path the default.


How It Works

sealed() wraps a value in an opaque container that is invisible to all standard serialization and logging mechanisms. The value is stored in a native private class field (#value) — unreachable via Object.keys, Object.entries, or property enumeration. It only surfaces when you explicitly call .unwrap().

const token = sealed("eyJhbG...")

console.log(token)              // [Sealed]
JSON.stringify({ token })       // '{}'
`${token}`                      // "[Sealed]"
Object.keys({ token })          // []  (no enumerable properties)

token.unwrap()                  // "eyJhbG..."  ← explicit opt-in only

Installation

npm install @ekaone/shielded
pnpm install @ekaone/shielded
yarn install @ekaone/shielded

API

sealed(value)

Wraps any value so it is hidden from serialization and logging. Use .unwrap() to explicitly read it.

import { sealed } from "@ekaone/shielded"

const token = sealed("eyJhbG...")

// All of these hide the value
console.log(token)                    // [Sealed]
JSON.stringify({ token })             // '{}'
String(token)                         // "[Sealed]"
`Bearer ${token}`                     // "Bearer [Sealed]"

// Explicit read
token.unwrap()                        // "eyJhbG..."
token.isSealed                        // true
token.isExpired                       // false

Works with any type:

sealed(42).unwrap()           // 42
sealed({ a: 1 }).unwrap()     // { a: 1 }
sealed(true).unwrap()         // true
sealed(null).unwrap()         // null

createStore(state)

Creates a store with full sealed value awareness. Plain values behave normally. Sealed values stay sealed until explicitly unwrapped.

import { createStore, sealed } from "@ekaone/shielded"

const store = createStore({
  user: "Eka",
  token: sealed("eyJhbG..."),
  cardToken: sealed("tok_visa_..."),
})

// Read plain value
store.get("user")                     // "Eka"

// Read sealed value — still sealed
store.get("token")                    // SealedValue<string>
store.get("token").unwrap()           // "eyJhbG..."

// Write
store.set("user", "John")
store.set("token", sealed("newToken..."))

// Snapshot — sealed keys omitted entirely
store.snapshot()                      // { user: "Eka" }

// Safe to log or send to error tracking
console.log(store.snapshot())         // { user: "Eka" } ✅
Sentry.captureException(err, {
  extra: store.snapshot()             // token and cardToken never appear ✅
})

store.subscribe(fn)

Subscribe to state changes. Returns an unsubscribe function. Sealed values remain sealed inside the subscriber.

const unsubscribe = store.subscribe((state) => {
  console.log(state.user)             // "Eka"
  console.log(state.token)            // [Sealed]  ← never leaks
})

store.set("user", "John")             // subscriber fires

// Stop listening
unsubscribe()

withTTL(value, ms)

Wraps a sealed value with a time-to-live in milliseconds. After expiry, .unwrap() throws. Useful for session tokens, OTP values, and temporary credentials.

import { sealed, withTTL } from "@ekaone/shielded"

const token = withTTL(sealed("eyJhbG..."), 5 * 60 * 1000) // 5 minutes

token.unwrap()       // "eyJhbG..."  (within TTL)
token.isExpired      // false

// After 5 minutes:
token.unwrap()       // throws "[shielded] sealed value has expired"
token.isExpired      // true

isSealedValue(value)

Type guard — returns true if a value is a SealedValue. Useful for conditional unwrapping.

import { isSealedValue } from "@ekaone/shielded"

const val = store.get("token")

if (isSealedValue(val)) {
  const raw = val.unwrap()  // TypeScript knows this is safe
}

isSealedValue(sealed("x"))  // true
isSealedValue("x")          // false
isSealedValue({ isSealed: true })  // false  ← spoofed objects rejected

Real World Use Cases

Auth tokens

const authStore = createStore({
  user: { name: "Eka", role: "admin" },
  accessToken: sealed("eyJhbG..."),
  refreshToken: sealed("dGhpcyBp..."),
})

// Use in API calls
await fetch("/api/data", {
  headers: {
    Authorization: `Bearer ${authStore.get("accessToken").unwrap()}`
  }
})

// Safe to log the store state
console.log(authStore.snapshot())
// { user: { name: "Eka", role: "admin" } }  ← tokens never appear

Payment flow

const checkoutStore = createStore({
  cartTotal: 9900,
  currency: "USD",
  cardLast4: "4242",
  stripeToken: sealed("tok_visa_..."),
})

// Sentry captures a crash during checkout
Sentry.captureException(err, {
  extra: checkoutStore.snapshot()
  // { cartTotal: 9900, currency: "USD", cardLast4: "4242" }
  // stripeToken is never in the report ✅
})

Healthcare / PII

const patientStore = createStore({
  name: "John Doe",
  dob: sealed("1985-03-12"),
  ssn: sealed("***-**-1234"),
  appointmentDate: "2026-04-01",
})

// Safe analytics event
analytics.track("appointment_viewed", patientStore.snapshot())
// { name: "John Doe", appointmentDate: "2026-04-01" }  ← no PII ✅

Session with expiry

const sessionStore = createStore({
  userId: "usr_abc123",
  sessionToken: withTTL(sealed("sess_xyz..."), 30 * 60 * 1000), // 30 min
})

// Works within session window
sessionStore.get("sessionToken").unwrap()   // "sess_xyz..."

// After 30 minutes — automatic protection
sessionStore.get("sessionToken").unwrap()   // throws: sealed value has expired

Composing with Existing State Managers

sealed() works as a standalone primitive. You can drop it into Zustand, Jotai, or any other state manager without replacing anything.

With Zustand

import { create } from "zustand"
import { sealed, isSealedValue } from "@ekaone/shielded"

const useStore = create(() => ({
  user: "Eka",
  token: sealed("eyJhbG..."),
}))

// In a component
const token = useStore((s) => s.token)
token.unwrap()   // "eyJhbG..."

// Safe devtools snapshot
const safeState = Object.fromEntries(
  Object.entries(useStore.getState()).filter(([, v]) => !isSealedValue(v))
)

With Jotai

import { atom } from "jotai"
import { sealed } from "@ekaone/shielded"

const tokenAtom = atom(sealed("eyJhbG..."))

// In a component
const [token] = useAtom(tokenAtom)
token.unwrap()   // "eyJhbG..."

TypeScript

Fully typed. sealed() preserves the inner type through unwrap().

import type { SealedValue, Store } from "@ekaone/shielded"

const token: SealedValue<string> = sealed("eyJhbG...")
const num: SealedValue<number> = sealed(42)

token.unwrap()   // string
num.unwrap()     // number

Performance

sealed() adds no asymptotic overhead. All operations remain O(1).

Operation Complexity vs baseline
sealed(value) O(1) same
.unwrap() O(1) +1 function call
JSON.stringify O(k) same or faster — sealed keys skipped
snapshot() O(k) O(k) key scan
subscribe notify O(n) same — n subscribers

The constant overhead of .unwrap() is a single property access — nanoseconds in practice, unmeasurable in any real application.


License

MIT © Eka Prasetia

Links


⭐ If this library helps you, please consider giving it a star on GitHub!

About

TypeScript primitive that prevents sensitive state from leaking through logging, devtools, and serialization

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors