Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to the ValidPay React Native SDK will be documented in this
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.0] - 2026-07-02

### Added

- **End-Cell issuance — `createEndCellIntent()`** (recommended). KeyHalve's
blind-rail flow: encrypts the payload locally and XOR-splits the AES key into
**ShareA** (returned as `key`, rides the QR) plus one share per holder
(`holders` defaults to `['keyhalve', 'platform']` → a 3-of-3 split). No
single party — not the platform, not KeyHalve — can read or reassemble the
key. Requires the API deployment to have End-Cell issuance enabled.
- **Rail verify.** `verifyIntent` now detects End-Cell intents and fetches the
independent KeyHalve rail share alongside the platform share(s), verifying
the rail's Ed25519 signature against a **pinned** key (fail-closed) before
recombining in memory and decrypting. New client options `railBaseUrl` /
`railPublicKeySpki` override the rail endpoint and pinned key for staging.

### Changed

- **Canonical verify origin is now `https://verify.keyhalve.com`.**
`buildVerifyUrl` emits verify links on the canonical KeyHalve verifier
origin by default (base64url key in the `#` fragment).
- README leads with End-Cell (recommended): End-Cell-first quick start, an
"End-Cell (recommended)" API section, and an explicit warning that this
SDK's `createIntent` is the legacy FULL-single-key flow (no key split at
all — unlike Node/Python, where `createIntent` defaults to 2-share
split-key).

## [1.3.0] - 2026-06-19

### Added
Expand Down
102 changes: 89 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Official React Native SDK for the [ValidPay](https://validpay.com) document
verification API. AES-256-GCM client-side encryption, eight patented
protections, and zero runtime dependencies.
verification API. AES-256-GCM client-side encryption, **End-Cell issuance
(blind rail)** — a 3-of-3 XOR key split across the QR, the platform, and the
independent KeyHalve rail, so no single party can read or reassemble the key —
eight patented protections, and zero runtime dependencies.

The wire format is byte-compatible with the [Python SDK][python-sdk] and the
[Node SDK][node-sdk] — an intent created with any ValidPay SDK can be
Expand Down Expand Up @@ -70,24 +72,61 @@ import { ValidPayClient } from '@validpay/react-native-sdk';

const client = new ValidPayClient({ apiKey: 'vp_live_…' });

// Issue
const result = await client.createIntent('check', {
// 1. Issuer side — seal with End-Cell (recommended). The AES key is split
// THREE ways: `key` is ShareA (rides the QR); one share goes to the
// platform; one goes to the independent KeyHalve rail. No single party —
// not the platform, not KeyHalve — can read or reassemble the key.
const result = await client.createEndCellIntent('check', {
payee: 'Alice',
amount: 1500,
});
console.log(result.retrievalId, result.key);
// → vp_abc123… (embed in QR)
// → base64 AES key — print on document, never store
// → vp_abc123… (public — embed in the QR)
// → ShareA (base64) — deliver ONLY to the intended verifier, out-of-band

// Verify (no API key required)
// 2. Verifier side — fetch and decrypt (no API key needed). For End-Cell
// intents, verifyIntent fetches the platform share + the rail share (the
// rail's Ed25519 signature is verified against a PINNED key, fail-closed),
// recombines in memory, decrypts locally, and re-checks the commitment hash.
const verified = await client.verifyIntent(result.retrievalId, result.key);
console.log(verified.payload); // { payee: 'Alice', amount: 1500 }
console.log(verified.integrityVerified); // true
```

> **Simpler 2-share option:** `createSplitKeyIntent()` splits the key between
> the document and the platform only — no independent rail share, so the
> platform alone could reconstruct. Prefer **End-Cell** above when independence
> from the platform matters.
>
> ⚠️ **`createIntent()` is the legacy FULL-single-key flow** — unlike the Node
> and Python SDKs, it performs **no key split at all**: the returned `key` is
> the complete AES key, so anyone holding the QR alone can decrypt. Use it only
> for backward compatibility with pre-split documents.

### Building a verification URL

The `retrievalId` is public; the `key` is secret. `buildVerifyUrl` stamps them
into a URL fragment (the `#` part — fragments are never sent to the server) on
the canonical verifier origin, base64url-encoding the key so phone QR scanners
and share-sheets don't mangle `+`, `/`, or `=`:

```ts
import { buildVerifyUrl } from '@validpay/react-native-sdk';

const url = buildVerifyUrl(result.retrievalId, result.key);
// → https://verify.keyhalve.com/verify/vp_abc123…#key=<base64url ShareA>
// Encode in a QR, paste in an email, scan with a phone camera.
// The /verify page reads the fragment client-side and decrypts locally.
```

## Features

Eight patented protections, all live in this SDK:
**End-Cell issuance (blind rail)** — `createEndCellIntent` / `verifyIntent` —
leads the SDK: a 3-of-3 XOR key split across ShareA (QR) + platform + the
independent KeyHalve rail, with the rail share Ed25519-signature-verified
against a pinned key at verify time (fail-closed).

Plus eight patented protections, all live in this SDK:

| Patent | Feature | Method |
| ------ | ----------------------------- | --------------------------------------------------- |
Expand All @@ -104,11 +143,38 @@ Eight patented protections, all live in this SDK:

### `new ValidPayClient(options)`

| Option | Type | Default | Notes |
| ----------- | -------- | ----------------------------- | ---------------------------------------- |
| `apiKey` | `string` | required | Bearer key from the issuer dashboard. |
| `baseUrl` | `string` | `https://api.validpay.com` | Override for staging. |
| `timeoutMs` | `number` | `10000` | Per-request timeout (AbortController). |
| Option | Type | Default | Notes |
| ------------------ | -------- | ----------------------------- | -------------------------------------------------- |
| `apiKey` | `string` | required | Bearer key from the issuer dashboard. |
| `baseUrl` | `string` | `https://api.validpay.com` | Override for staging. |
| `timeoutMs` | `number` | `10000` | Per-request timeout (AbortController). |
| `railBaseUrl` | `string` | `https://rail.keyhalve.com` | KeyHalve rail (End-Cell share custody). |
| `railPublicKeySpki`| `string` | pinned production key | Ed25519 SPKI (base64) the rail share is verified against. |

### End-Cell (recommended)

```ts
client.createEndCellIntent(documentType, payload, opts?): Promise<CreateIntentResult>
// opts: { holders?, validFrom?, validUntil?, onBehalfOf? }
```

KeyHalve's blind-rail flow. Generates a key, encrypts the payload locally, and
XOR-splits the key into **ShareA** (returned as `key`, for the QR) plus one
share per holder. `holders` defaults to `['keyhalve', 'platform']` → a
**3-of-3** split: the independent KeyHalve rail share + the platform share +
ShareA. No single party can read or reassemble the key. Verify with
`verifyIntent` (below), which fetches the platform + rail shares, verifies the
rail's Ed25519 signature against a **pinned** key (fail-closed), recombines in
memory, and decrypts. **The full key never exists on any single system.**
Requires the API deployment to have End-Cell issuance enabled.

```ts
const result = await client.createEndCellIntent('check', { payee: 'Alice', amount: 1500 });
const verified = await client.verifyIntent(result.retrievalId, result.key);
```

The rail endpoint and pinned key default to the production KeyHalve rail;
`railBaseUrl` / `railPublicKeySpki` client options exist for staging.

### Core methods

Expand All @@ -118,6 +184,16 @@ client.createIntentBatch(intents): Promise<CreateIntentResult[]> // ≤100 ite
client.verifyIntent(retrievalId, key): Promise<VerifyIntentResult>
```

> ⚠️ **Legacy:** in this SDK `createIntent` is the FULL-single-key flow — it
> performs **no key split at all** (unlike `createIntent` in the Node/Python
> SDKs, which default to 2-share split-key). The returned `key` is the complete
> AES key: whoever holds the QR alone can decrypt. Prefer
> `createEndCellIntent` (3-share, above) or `createSplitKeyIntent` (2-share,
> below) for new documents.

`verifyIntent` handles single-key and End-Cell intents; for a split-key intent
it throws `split_key_required` — use `verifySplitKeyIntent` instead.

### Split-key (Patent C)

```ts
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@validpay/react-native-sdk",
"version": "1.3.0",
"version": "1.4.0",
"description": "ValidPay verification SDK for React Native — 8 patented protections for document authenticity",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
Loading