---
title: "@playstack/crypto"
description: "Workers-compatible encryption, digests, signatures, verification, and secure token generation."
tags: ["package","foundation","crypto","security","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@playstack/crypto` provides Workers-compatible authenticated encryption, digests, signatures, verification, and secure token generation while keeping key ownership explicit.

## Install

```sh
npm install @playstack/core @playstack/crypto
```

## Compose it once

Resolve secrets at the deployment boundary and inject a clock and Web Crypto implementation. Capability packages receive the resulting suite or the smaller function they require.

```ts
import { systemClock } from '@playstack/core'
import { createCrypto } from '@playstack/crypto'

export const applicationCrypto = createCrypto({
  crypto: globalThis.crypto,
  clock: systemClock(),
  encryptionKeys: {
    keys: [
      { id: 'enc-2026-08', key: process.env.ENCRYPTION_KEY_CURRENT!, active: true },
      { id: 'enc-2026-05', key: process.env.ENCRYPTION_KEY_PREVIOUS! },
    ],
  },
  signingKeys: {
    keys: [
      { id: 'sig-2026-08', key: process.env.SIGNING_KEY_CURRENT!, active: true },
      { id: 'sig-2026-05', key: process.env.SIGNING_KEY_PREVIOUS! },
    ],
  },
  maximumTokenTtl: '15m',
  replayStore: singleUseTokenStore,
})

const token = await applicationCrypto.sign({ userId }, { purpose: 'email-verification', ttl: '10m' })
```

The key strings are canonical padded base64. Encryption keys must decode to exactly 32 bytes; signing keys must decode to at least 32 bytes. Keep old keys available for verification and decryption until their data has expired or been re-encrypted, but mark exactly one key in each keyset active.

## Node-only and browser declarations

The public digest, HMAC, and encryption capabilities use minimal DOM-independent interfaces rather than requiring ambient `SubtleCrypto`. Node-only consumers can use `lib: ["es2022"]`, Node types, and `skipLibCheck: false`; browser consumers can retain their DOM libraries. Inject the runtime's actual Web Crypto implementation. Enabling browser globals or disabling declaration checking is not required for server projects, and this type boundary does not supply a runtime crypto polyfill.

## Configuration reference

| `CryptoSuiteOptions` property | Type           | Required | Purpose                                                                           |
| ----------------------------- | -------------- | -------- | --------------------------------------------------------------------------------- |
| `crypto`                      | `WebCrypto`    | Yes      | Supplies `subtle` and `getRandomValues`; usually `globalThis.crypto`.             |
| `clock`                       | `Clock`        | Yes      | Controls issue, expiry, and verification time.                                    |
| `encryptionKeys`              | `KeysetConfig` | Yes      | AES-GCM keys with one active key.                                                 |
| `signingKeys`                 | `KeysetConfig` | Yes      | HMAC-SHA-256 keys with one active key.                                            |
| `replayStore`                 | `ReplayStore`  | No       | Atomically consumes token IDs for single-use verification.                        |
| `maximumTokenTtl`             | `string`       | No       | Maximum signed-token lifetime; defaults to `5m`. Supports `ms`, `s`, `m`, or `h`. |

Each `KeyConfig` has an `id`, a base64 `key`, and optional `active`. IDs are embedded in output envelopes so key rotation does not require trial decryption.

## Operations

| Method                                | Use                                                                                |
| ------------------------------------- | ---------------------------------------------------------------------------------- |
| `encrypt` / `decrypt` / `decryptText` | Authenticated AES-GCM envelopes for strings or bytes.                              |
| `needsReencryption` / `reencrypt`     | Detect and migrate values written by an older encryption key.                      |
| `randomToken`                         | Generate an opaque base64url token; defaults to 32 bytes and requires at least 16. |
| `digestSha256`                        | Produce a stable digest for storing opaque-token lookups.                          |
| `verifyHmacSha256`                    | Compare an expected HMAC without timing-sensitive string comparison.               |
| `sign` / `verify`                     | Create and verify purpose-bound, expiring structured tokens.                       |

## Drain legacy encrypted envelopes

`createSecretMigrationCodec()` composes the current suite with application-owned legacy decoders. A decoder must positively identify its format; ambiguous and unknown values fail closed, and a malformed current Playstack envelope never falls back to a legacy reader.

Successful legacy decryption returns a current replacement envelope but performs no write. The application persists that replacement with compare-and-swap after the surrounding domain operation succeeds, allowing retired formats and keys to be drained without making them permanent Playstack behavior.

## Replay-store bridge

Single-use verification calls one application-owned bridge:

```ts
interface ReplayStore {
  consume(jti: string, expiresAtSeconds: number): Promise<boolean>
}
```

`consume` must be atomic across all instances. Return `true` only for the first consumption; retain the marker until `expiresAtSeconds`. Redis `SET NX` or a database uniqueness constraint are suitable implementations. If `singleUse: true` is requested without this bridge, verification fails closed.

## Testing

`@playstack/crypto/testing` exports `DeterministicCrypto` and `MemoryReplayStore`. Use them with `FakeClock` from `@playstack/core/testing` to test rotation, expiry, and replay behavior without nondeterminism.

## Boundary

The package does not load environment variables, manage or derive keys, hash passwords, or provide persistent replay storage. The application owns its secret manager, rotation procedure, and bridge lifecycle.

{/* package-reference:start */}

## API entry points and requirements

Reference snapshot: `@playstack/crypto@0.1.0-beta.1`. Import only the entry point your runtime needs. Paths below are relative to the installed package; use **Go to Definition** in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

| Public entry point | Declaration file |
| --- | --- |
| `@playstack/crypto` | `./dist/index.d.ts` |
| `@playstack/crypto/primitives` | `./dist/primitives.d.ts` |
| `@playstack/crypto/errors` | `./dist/errors.d.ts` |
| `@playstack/crypto/testing` | `./dist/testing.d.ts` |
| `@playstack/crypto/playstack.integration.json` | No TypeScript declaration (asset or metadata export). |
| `@playstack/crypto/package.json` | No TypeScript declaration (asset or metadata export). |

Node.js engine requirement: `>=20`. This is not a claim that every entry point works in browsers or Workers.

### Peer dependencies

This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.

For a complete first program, start with [Getting started](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
