
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@uekichinos/stash
Advanced tools
Lightweight localStorage/sessionStorage wrapper with TTL expiry, TypeScript generics, namespace isolation, and version-based auto-wipe. Zero dependencies.
Lightweight localStorage/sessionStorage wrapper with TTL expiry, TypeScript generics, and namespace isolation. Zero dependencies.
stash.set('token', 'abc123', { ttl: '1h' })
stash.get('token') // 'abc123' | null (null after 1 hour)
'30s', '1h', '7d')getOrSet, async remember (with concurrent de-dupe)subscribe to a key, including cross-tab changesnpm install @uekichinos/stash
import { stash } from '@uekichinos/stash'
// Store a value
stash.set('user', { name: 'John' })
// Retrieve it
stash.get('user') // { name: 'John' }
// With TTL — auto-expires after 30 minutes
stash.set('session', data, { ttl: '30m' })
// Namespace — isolate keys per module
const auth = stash.namespace('auth')
auth.set('token', 'xyz', { ttl: '1h' })
stash.set(key, value, options?)Stores a value in localStorage (default) or sessionStorage.
stash.set('key', value)
stash.set('key', value, { ttl: '1h' })
stash.set('key', value, { ttl: 5000 }) // ms also accepted
stash.set('key', value, { storage: 'session' })
stash.get<T>(key, options?)Returns the stored value, or null if missing or expired. Expired keys are removed automatically on read.
stash.get('key') // unknown | null
stash.get<User>('user') // User | null
stash.get('key', { storage: 'session' })
stash.has(key, options?)Returns true if the key exists and has not expired.
stash.has('token') // boolean
stash.remove(key, options?)Deletes a key immediately.
stash.remove('token')
stash.ttl(key, options?)Returns remaining TTL in milliseconds, null if the key has no TTL or doesn't exist.
stash.ttl('token') // e.g. 1800000 (30 minutes remaining)
stash.keys(options?)Returns all non-expired keys managed by stash. Expired keys are removed automatically.
stash.set('a', 1)
stash.set('b', 2, { ttl: '1h' })
stash.keys() // ['a', 'b']
const auth = stash.namespace('auth')
auth.set('token', 'xyz')
auth.keys() // ['token'] (not 'auth:token' — prefix is stripped)
stash.clear(options?)Removes all keys managed by stash. Does not touch keys set outside of stash.
stash.clear()
stash.clear({ storage: 'session' })
stash.purge(options?)Removes only expired keys. Useful for manual cleanup.
stash.purge()
stash.namespace(prefix, options?)Returns a namespaced stash instance. Keys are stored as stash:{prefix}:{key}.
const auth = stash.namespace('auth')
auth.set('token', 'abc') // stored as 'stash:auth:token'
auth.get('token')
auth.clear() // only clears 'stash:auth:*' keys
With version — when version changes, all keys from older versions are wiped automatically:
// Old code (version 1) — keys are stored with v:1
const v1 = stash.namespace('app', { version: 1 })
v1.set('config', oldData)
// New code (version 2) — old v:1 keys are wiped on init
const v2 = stash.namespace('app', { version: 2 })
v2.get('config') // null — old data gone
stash.getOrSet<T>(key, factory, options?)Return the cached value if present; otherwise call factory(), store its result, and return it. A stored null counts as a hit (the factory won't re-run).
const config = stash.getOrSet('config', () => computeExpensiveConfig(), { ttl: '10m' })
stash.remember<T>(key, factory, options?)Async getOrSet. Awaits factory(), caches the resolved value, and returns a Promise. Concurrent calls for the same key share one in-flight promise, and a rejected factory is never cached.
const rates = await stash.remember(
'fx-rates',
() => fetch('/api/fx').then((r) => r.json()),
{ ttl: '1h' },
)
stash.update<T>(key, updater, options?)Read-modify-write in a single call. The updater receives the current value (or null if missing/expired). The entry's existing TTL is preserved unless you pass a new ttl.
stash.update('cart', (items = []) => [...items, newItem]) // TTL kept
stash.update('views', (n = 0) => n + 1)
stash.touch(key, ttl, options?)Extend or replace a key's TTL without rewriting its value. Returns false if the key is missing or already expired.
stash.touch('session', '30m') // keep the session alive
stash.subscribe<T>(key, callback, options?)Run callback(value) whenever the key changes — via a local set / update / remove, or a change in another tab (through the storage event). Returns an unsubscribe function. value is null when the key is removed or expires.
const off = stash.subscribe('auth:token', (token) => {
if (token === null) redirectToLogin() // logged out in another tab
})
// later
off()
Cross-tab notifications require
localStorage(the browser only firesstorageevents for it). Same-tab changes always notify.
When set hits a QuotaExceededError, stash evicts its own entries closest to expiring (already-expired ones first) and retries. Entries without a TTL are never evicted.
stash.configure({
onEvict: (keys) => console.warn('stash evicted to make room:', keys),
})
If localStorage / sessionStorage throws or is absent (SSR, private browsing, sandboxed iframe, cookies disabled), stash transparently switches to an in-memory store for the lifetime of the page — set / get keep working instead of silently doing nothing.
stash.isPersistent() // false when the in-memory fallback is active
stash.isPersistent({ storage: 'session' })
stash.getAll<T>(options?) / stash.entries<T>(options?)Snapshot every non-expired entry in the current scope (respects namespaces). Expired entries are dropped as a side effect.
const auth = stash.namespace('auth')
auth.getAll() // { token: '…', refresh: '…' }
auth.entries() // [['token', '…'], ['refresh', '…']]
stash.getStale<T>(key, options?)Like get, but returns expired values too — flagged, and left in storage — so you can show stale data instantly while refreshing in the background.
const { value, expired, exists } = stash.getStale('dashboard')
if (exists) render(value)
if (expired) refreshInBackground()
| Format | Duration |
|---|---|
'30s' | 30 seconds |
'5m' | 5 minutes |
'2h' | 2 hours |
'7d' | 7 days |
5000 | 5000 milliseconds |
All methods are fully typed. Use generics with get for typed retrieval:
interface User {
id: number
name: string
}
stash.set<User>('user', { id: 1, name: 'John' })
const user = stash.get<User>('user') // User | null
<script> tag (no bundler)<script src="https://unpkg.com/@uekichinos/stash/dist/index.global.js"></script>
<script>
Stash.stash.set('key', 'value', { ttl: '1h' })
Stash.stash.get('key')
</script>
| Option | Backend |
|---|---|
'local' (default) | localStorage — persists across sessions |
'session' | sessionStorage — cleared when tab closes |
stash.set('draft', form, { storage: 'session' })
stash.get('draft', { storage: 'session' })
MIT © uekichinos
FAQs
Lightweight localStorage/sessionStorage wrapper with TTL expiry, TypeScript generics, namespace isolation, and version-based auto-wipe. Zero dependencies.
We found that @uekichinos/stash demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.