From 201d4e5ced37ee3b2ed5a5cf3d73685e35b95661 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 19 Jun 2026 04:41:42 -0500 Subject: [PATCH] feat: platform delegation - onBehalfOf on seal + delegated badge on verify (1.3.0) RN parity for platform delegation (Fork B), matching node 0.8.0 / python 1.5.0 and the live API. onBehalfOf {ref,name} on createIntent / createFileIntent / createSelectiveIntent / createIntentBatch; verifyIntent surfaces verificationLevel + delegatedBy. README + CHANGELOG. 94 tests (2 new). Requires the ValidPay API with delegation (live 2026-06-19). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 18 ++++++++++++++++ README.md | 25 +++++++++++++++++++++++ __tests__/client.test.ts | 30 +++++++++++++++++++++++++++ package.json | 2 +- src/client.ts | 44 +++++++++++++++++++++++++++++++++++++++- src/types.ts | 4 ++++ 6 files changed, 121 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d2476..262d75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ 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.3.0] - 2026-06-19 + +### Added + +- **Platform delegation — `onBehalfOf`** (Fork B). Platforms that seal on behalf + of the businesses they serve can name the business per seal: pass + `onBehalfOf: { ref, name }` in the options to `createIntent`, + `createFileIntent`, `createSelectiveIntent`, or per-item in + `createIntentBatch`. `ref` is your own id for the business (the dedupe key — + same `ref` rolls up); `name` is who the verifier sees. The verifier sees that + business as the issuer, attributed *through* your platform, at the `delegated` + trust rung. ValidPay stays blind to the document contents — identity only. +- `verifyIntent` now surfaces `verificationLevel` (`none` < `delegated` < + `domain` < `business`) and `delegatedBy` (`{ platform, platformLevel }` or + `null`). + +> Requires the ValidPay API with platform-delegation support (live 2026-06-19). + ## [1.0.1] - 2026-06-08 ### Changed diff --git a/README.md b/README.md index b957479..6358f47 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,31 @@ client.verifySplitKeyIntent(retrievalId, shareA): Promise document as you would the regular key. Share B stays on the server and is fetched at verify time. +### Platform delegation — `onBehalfOf` + +If you integrate as a **platform** and seal on behalf of the businesses you +serve, name the business on each seal. The verifier sees that business as the +issuer ("who"), attributed *through* your platform ("through whom"), at the +`delegated` trust rung. Those businesses never touch ValidPay, and ValidPay +stays blind to the document contents. + +```ts +const { retrievalId, key } = await client.createIntent( + 'lease', + { unit: '4B', term: '12mo' }, + { onBehalfOf: { ref: 'landlord_8675309', name: 'Smith Properties LLC' } }, +); + +const result = await client.verifyIntent(retrievalId, key); +result.issuer; // 'Smith Properties LLC' +result.verificationLevel; // 'delegated' +result.delegatedBy; // { platform: 'Your Platform', platformLevel: 'domain' } +``` + +Same `ref` ⇒ same tracked business (its documents and verification counts roll +up). A sub-issuer surfaces as `delegated` only once **your** platform account is +domain-verified. + ### Selective disclosure (Patent E) ```ts diff --git a/__tests__/client.test.ts b/__tests__/client.test.ts index e6d3567..2463c2f 100644 --- a/__tests__/client.test.ts +++ b/__tests__/client.test.ts @@ -101,6 +101,16 @@ describe('createIntent', () => { expect(JSON.parse(decrypted)).toEqual({ payee: 'Alice', amount: 1500 }); }); + test('sends on_behalf_of for platform delegation', async () => { + pushResponse({ retrieval_id: 'vp_d' }, 201); + const c = newClient(); + await c.createIntent('lease', { a: 1 }, { + onBehalfOf: { ref: 'cust_42', name: 'Smith Properties' }, + }); + const body = calls[0].body as Record; + expect(body.on_behalf_of).toEqual({ ref: 'cust_42', name: 'Smith Properties' }); + }); + test('passes through valid_from / valid_until', async () => { pushResponse({ retrieval_id: 'vp_a' }, 201); const c = newClient(); @@ -226,6 +236,26 @@ describe('verifyIntent', () => { expect(auth).toBeUndefined(); }); + test('surfaces verificationLevel + delegatedBy for a delegated issuer', async () => { + const { key, encrypted_payload, commitment_hash } = await buildActiveIntent({ a: 1 }); + pushResponse({ + intent_id: 'vp_d', + encrypted_payload, + commitment_hash, + commitment_version: 2, + issuer: 'Smith Properties', + issuer_verified: false, + registered_at: '2026-05-01T00:00:00Z', + status: 'active', + verification_level: 'delegated', + delegated_by: { platform: 'Acme Platform', platform_level: 'domain' }, + }); + const c = newClient(); + const result = await c.verifyIntent('vp_d', key); + expect(result.verificationLevel).toBe('delegated'); + expect(result.delegatedBy).toEqual({ platform: 'Acme Platform', platformLevel: 'domain' }); + }); + test('throws intent_revoked when status=revoked', async () => { pushResponse({ intent_id: 'vp_1', diff --git a/package.json b/package.json index 4766342..30281ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@validpay/react-native-sdk", - "version": "1.2.0", + "version": "1.3.0", "description": "ValidPay verification SDK for React Native — 8 patented protections for document authenticity", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/client.ts b/src/client.ts index 3a9e4d1..bbfbeac 100644 --- a/src/client.ts +++ b/src/client.ts @@ -28,9 +28,22 @@ export interface ValidPayClientOptions { timeoutMs?: number; } +/** + * Platform delegation (Fork B). Seal on behalf of one of the businesses your + * platform serves: `ref` is your own id for that business (the dedupe key — + * same ref rolls up), `name` is who the verifier sees. The verifier sees that + * business as the issuer, through your platform, at the `delegated` rung. + */ +export interface OnBehalfOf { + ref: string; + name: string; +} + export interface TimeLockOptions { validFrom?: string | null; validUntil?: string | null; + /** Seal on behalf of one of your businesses (platform delegation). */ + onBehalfOf?: OnBehalfOf; } export interface CreateBoundIntentOptions { @@ -47,6 +60,8 @@ export interface BatchIntentInput { payload: unknown; validFrom?: string | null; validUntil?: string | null; + /** Seal on behalf of one of your businesses (platform delegation). */ + onBehalfOf?: OnBehalfOf; } interface IntentApiResponse { @@ -73,6 +88,8 @@ interface IntentApiResponse { valid_until?: string | null; revoked_at?: string | null; revocation_reason?: string | null; + verification_level?: 'none' | 'delegated' | 'domain' | 'business'; + delegated_by?: { platform: string; platform_level: 'domain' | 'business' } | null; } /** @@ -126,6 +143,7 @@ export class ValidPayClient { }; if (opts.validFrom != null) body.valid_from = opts.validFrom; if (opts.validUntil != null) body.valid_until = opts.validUntil; + if (opts.onBehalfOf != null) body.on_behalf_of = opts.onBehalfOf; const data = await this.request<{ retrieval_id?: string }>( 'POST', @@ -187,6 +205,7 @@ export class ValidPayClient { }; if (item.validFrom != null) reqItem.valid_from = item.validFrom; if (item.validUntil != null) reqItem.valid_until = item.validUntil; + if (item.onBehalfOf != null) reqItem.on_behalf_of = item.onBehalfOf; requestItems.push(reqItem); } @@ -279,6 +298,13 @@ export class ValidPayClient { validFrom: data.valid_from ?? null, validUntil: data.valid_until ?? null, timeLockStatus: computeTimeLockStatus(data.valid_from, data.valid_until), + verificationLevel: data.verification_level, + delegatedBy: data.delegated_by + ? { + platform: data.delegated_by.platform, + platformLevel: data.delegated_by.platform_level, + } + : null, }; } @@ -315,6 +341,7 @@ export class ValidPayClient { }; if (opts.validFrom != null) body.valid_from = opts.validFrom; if (opts.validUntil != null) body.valid_until = opts.validUntil; + if (opts.onBehalfOf != null) body.on_behalf_of = opts.onBehalfOf; const data = await this.request<{ retrieval_id?: string }>( 'POST', @@ -396,6 +423,13 @@ export class ValidPayClient { validFrom: data.valid_from ?? null, validUntil: data.valid_until ?? null, timeLockStatus: computeTimeLockStatus(data.valid_from, data.valid_until), + verificationLevel: data.verification_level, + delegatedBy: data.delegated_by + ? { + platform: data.delegated_by.platform, + platformLevel: data.delegated_by.platform_level, + } + : null, }; } @@ -407,7 +441,7 @@ export class ValidPayClient { documentType: string, payload: Record, disclosurePolicy: Record, - opts: { splitKey?: boolean; validFrom?: string | null; validUntil?: string | null } = {}, + opts: { splitKey?: boolean; validFrom?: string | null; validUntil?: string | null; onBehalfOf?: OnBehalfOf } = {}, ): Promise { if (!documentType) { throw new ValidPayError('invalid_argument', 'documentType is required'); @@ -470,6 +504,7 @@ export class ValidPayClient { if (keyFragmentB != null) body.key_fragment_b = keyFragmentB; if (opts.validFrom != null) body.valid_from = opts.validFrom; if (opts.validUntil != null) body.valid_until = opts.validUntil; + if (opts.onBehalfOf != null) body.on_behalf_of = opts.onBehalfOf; const data = await this.request<{ retrieval_id?: string }>( 'POST', @@ -579,6 +614,13 @@ export class ValidPayClient { validFrom: data.valid_from ?? null, validUntil: data.valid_until ?? null, timeLockStatus: computeTimeLockStatus(data.valid_from, data.valid_until), + verificationLevel: data.verification_level, + delegatedBy: data.delegated_by + ? { + platform: data.delegated_by.platform, + platformLevel: data.delegated_by.platform_level, + } + : null, }; } diff --git a/src/types.ts b/src/types.ts index 8c9100a..c37462d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,10 @@ export interface VerifyIntentResult { validFrom: string | null; validUntil: string | null; timeLockStatus: 'valid' | 'not_yet_valid' | 'expired' | null; + /** Graded trust rung: 'none' < 'delegated' < 'domain' < 'business'. */ + verificationLevel?: 'none' | 'delegated' | 'domain' | 'business'; + /** Set when sealed on behalf of, via a platform (delegation); else null. */ + delegatedBy?: { platform: string; platformLevel: 'domain' | 'business' } | null; } /**