diff --git a/CHANGELOG.md b/CHANGELOG.md index 262d75f..01fe21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 6358f47..0cf04c0 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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= +// 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 | | ------ | ----------------------------- | --------------------------------------------------- | @@ -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 +// 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 @@ -118,6 +184,16 @@ client.createIntentBatch(intents): Promise // ≤100 ite client.verifyIntent(retrievalId, key): Promise ``` +> ⚠️ **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 diff --git a/package.json b/package.json index 378985d..0e4c5f6 100644 --- a/package.json +++ b/package.json @@ -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",