@orkestrapay/cse
v1.0.8
Published
Orkestra client-side encryption (CSE) browser SDK — encrypts card data into a JWE Compact (RSA-OAEP-256 + A256GCM) using only the Web Crypto API.
Maintainers
Readme
@orkestrapay/cse
Encrypt card data in the browser, before it ever leaves your page.
@orkestrapay/cse turns the card number, expiry and CVV into a single encrypted string
(a JWE Compact token) using only the browser's built-in Web Crypto API. Your server — and
anyone between it and the browser — only ever sees ciphertext. Only Orkestra can decrypt it.
- Zero dependencies, ~4.5KB gzipped, no polyfills.
- Works with any framework, or with a plain
<script>tag. RSA-OAEP-256+A256GCM, Web Crypto only.
CSE runs on top of HTTPS, not instead of it. Every request is still TLS.
1. Install
With a bundler (npm / yarn / pnpm):
npm install @orkestrapay/cseimport { initialize, encryptCard } from "@orkestrapay/cse"Without a bundler (script tag):
<script src="https://<cdn-host>/cse-runtime/1.0.7/runtime.min.js"
integrity="sha384-…"
crossorigin="anonymous"></script>This exposes one global: window.OrkestraCSE, with the same functions.
Two rules for production:
- Pin the exact version — no
^, no~, and never a/latestCDN URL. This is a security component; upgrades should be deliberate.- Always use the
integrity(SRI) hash for the script tag. Orkestra provides it with each release. Details indocs/sri-pci.md.
2. Use it
Three steps: initialize once, encrypt at submit, send only the ciphertext.
<script src="https://<cdn-host>/cse-runtime/1.0.7/runtime.min.js"
integrity="sha384-…" crossorigin="anonymous"></script>
<script>
// 1. Once, when the checkout page loads. Name the environment — nothing else.
await OrkestraCSE.initialize({
environment: "production", // or "qa" / "sandbox"
audience: "core",
})
// 2. When the customer submits, read the inputs and encrypt.
const jwe = await OrkestraCSE.encryptCard({
pan: "4111111111111111",
expMonth: "03",
expYear: "2030",
cvv: "123",
holderName: "Ada Lovelace", // optional
})
// 3. Send ONLY the ciphertext to your backend, then clear the inputs.
await fetch("/api/pay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ crypto: { blob: jwe } }),
})
</script>Your backend forwards { crypto: { blob: jwe } } to Orkestra unchanged. It never needs to
read, parse or store the card data.
Complete, copy-pasteable examples: examples/vanilla (plain JS) and
examples/react (React / Next.js).
The rules that keep this safe
- Render card fields in the DOM as usual.
- At submit, read the values directly from the inputs — never put them in app state,
Redux,
localStorage, logs or analytics. await encryptCard(...).- Clear the inputs (
form.reset()), even when encryption fails. - POST only the JWE. Nothing else about the card.
3. API
initialize(options): Promise<void> // call once, before encrypting
encryptCard(card, ctx?): Promise<string> // returns the JWE Compact string
refreshKey(signal?): Promise<void> // force a new public key (see key rotation)
getVersion(): string // SDK version
createCseClient(): CseClient // independent instance (multi-tenant / tests)initialize is idempotent for the same configuration: repeated and concurrent calls are
coalesced into one key fetch. Re-initializing with a different environment throws
INVALID_CONFIG rather than silently keeping the first one — if you genuinely need two
environments at once, use createCseClient(). If initialization fails, just call it again to retry.
initialize(options)
| Option | Required | Default | What it does |
|---|---|---|---|
| environment | yes | — | "production", "qa" or "sandbox". The public-key URL for that environment ships inside the SDK — you never configure a URL. |
| audience | no | — | Written into every payload as aud. Usually "core". |
| timeoutMs | no | 8000 | Timeout for fetching the public key. |
| cacheTtlMs | no | 3600000 (1h) | How long the key is cached in memory. Capped by the endpoint's Cache-Control. |
| keyFetchRetries | no | 1 | Retries for the key fetch only. encryptCard is never retried silently. |
| signal | no | — | AbortSignal to cancel initialization. |
| keyEndpoint | no | — | Restricted override, for local development and Orkestra-internal staging only. Accepted solely for localhost or an orkestrapay.io host; anything else throws INVALID_CONFIG. Integrations should not use it. |
Why there is no URL to configure. The public key is the trust anchor of the whole scheme. If the endpoint could be set to an arbitrary host, one wrong value in a
.envwould make the SDK encrypt cards to someone else's public key — and nothing downstream would notice, because the JWE would still be perfectly valid. So the URLs ship inside the SDK and you only name the environment.
encryptCard(card, ctx?)
| Field | Required | Format |
|---|---|---|
| pan | yes | 12–19 digits. Spaces and dashes are stripped for you. |
| expMonth | yes | 1–12; "3" and "03" both work. |
| expYear | yes | 4 digits, e.g. "2030". Two-digit years are rejected. |
| cvv | yes | 3–4 digits. |
| holderName | no | Cardholder name. |
Optional second argument ctx: { audience?, operationId?, signal? } — overrides the
audience for a single call, attaches your own correlation id, or aborts a pending key fetch.
Returns the JWE Compact string. That string is the only thing you send.
Using multiple configurations
The module-level functions share one client. If you need more than one (multi-tenant apps, tests, two environments side by side), create independent instances:
import { createCseClient } from "@orkestrapay/cse"
const cse = createCseClient()
await cse.initialize({ environment: "production", audience: "core" })
const jwe = await cse.encryptCard({ pan, expMonth, expYear, cvv })4. Handling errors
Every failure throws a CseError with a stable .code. Branch on code, never on
message (messages are human-readable and may change). Errors never contain the PAN, CVV,
payload or key.
import { CseError } from "@orkestrapay/cse"
try {
const jwe = await encryptCard(card)
} catch (err) {
if (CseError.is(err)) {
switch (err.code) {
case "INVALID_PAYLOAD": return showFieldError("Check the card details.")
case "KEY_FETCH_FAILED":
case "TIMEOUT": return showRetry("Try again in a moment.")
default: return showGeneric()
}
}
throw err
}| Code | Meaning / what to do |
|---|---|
| SDK_NOT_INITIALIZED | encryptCard was called before initialize. |
| INVALID_CONFIG | Bad initialize options — missing/unknown environment, or a keyEndpoint override pointing at a host that is not Orkestra's. |
| INSECURE_CONTEXT | The page is not served over HTTPS. Fix the page, not the SDK. |
| WEBCRYPTO_UNAVAILABLE | The browser has no crypto.subtle. Unsupported browser. |
| KEY_FETCH_FAILED | Could not reach the key endpoint. Retryable. |
| KEY_INVALID / KEY_EXPIRED | The served key is unusable. Contact Orkestra support. |
| UNSUPPORTED_ALGORITHM | The browser cannot perform RSA-OAEP-256 / A256GCM, or the served key's algorithm is outside the strict allowlist (downgrade attempt). Usually an old browser, not an attack. |
| INVALID_PAYLOAD | A card field failed format validation. Show a form error. |
| ENCRYPTION_FAILED | The crypto operation failed. Retryable once. |
| TIMEOUT | The key fetch timed out or was aborted. Retryable. |
Key rotation mid-checkout
Orkestra rotates the encryption key periodically. If your backend reports that the kid has
expired, refresh and retry once — the SDK never re-encrypts silently:
await refreshKey()
const jwe = await encryptCard(card)5. Browser support
Any browser with the Web Crypto API and crypto.subtle over HTTPS — the last two major
versions of Chrome, Edge, Firefox, Safari and iOS Safari. There is no polyfill fallback: on
an unsupported browser initialize fails fast with WEBCRYPTO_UNAVAILABLE.
6. Security
The private key never leaves Orkestra's Vault; plaintext exists only transiently inside the platform. What this protects against — and, just as importantly, what it does not (JS memory, malware in the page before encryption, TLS, PCI/SAQ scope) — is documented honestly in:
docs/security.md— security model and limitsdocs/sri-pci.md— SRI, CDN and PCI considerationssecurity/threat-model.md— threat model
Further reading
| Document | What's in it |
|---|---|
| docs/integration.md | Longer integration guide |
| docs/wire-contract.md | The exact JWE format (frozen) |
| docs/versioning.md | SemVer policy |
| CHANGELOG.md | Release history |
| docs/maintaining.md | Building and releasing the SDK (maintainers) |
Need help? Contact Orkestra support.
