UNPKG

@taprsvp/types

Version:

TypeScript types and interfaces for the Transaction Authorization Protocol (TAP)

500 lines (418 loc) 21.8 kB
/** * @fileoverview TAP Message Validation using Zod v4 * * This module provides Zod schema definitions for validating TAP (Transaction Authorization Protocol) messages. * It ensures type safety and runtime validation of all TAP message types. * * @see {@link https://github.com/colinhacks/zod | Zod Documentation} * @see {@link https://github.com/TransactionAuthorizationProtocol/TAIPs | TAP Specifications} * @version 1.0.0 */ import { z } from 'zod'; import type { IsoCurrency } from './currencies'; import type { PurposeCode, CategoryPurposeCode } from './purpose_codes'; // ============================================================================ // FUNDAMENTAL VALIDATORS // ============================================================================ /** Decentralized Identifier (DID) validator */ export const DIDSchema = z.string().regex(/^did:[a-z0-9]+:[a-zA-Z0-9._%-]+$/); /** Internationalized Resource Identifier (IRI) validator */ export const IRISchema = z.string().regex(/^[a-zA-Z][a-zA-Z0-9+.-]*:.+$/); /** TAP Context URI validator */ export const TAPContextSchema = z.literal("https://tap.rsvp/schema/1.0"); /** TAP Type URI validator */ export const TAPTypeSchema = z.string().regex(/^https:\/\/tap\.rsvp\/schema\/1\.0#[A-Za-z]+$/); /** ISO 8601 DateTime validator */ export const ISO8601DateTimeSchema = z.string().datetime(); /** UUID v4 validator */ export const UUIDSchema = z.string().uuid(); /** CAIP-2 Chain ID validator (namespace:reference) */ export const CAIP2Schema = z.string().regex(/^[a-z0-9]+:[a-zA-Z0-9._%-]+$/); /** CAIP-10 Account Address validator (chain_id:account_address) */ export const CAIP10Schema = z.string().regex(/^[a-z0-9]+:[a-zA-Z0-9._%-]+:[a-zA-Z0-9._%-]+$/); /** CAIP-19 Asset ID validator (chain_id/token_namespace:token_reference) */ export const CAIP19Schema = z.string().regex(/^[a-z0-9]+:[a-zA-Z0-9._%-]+\/[a-z0-9]+:[a-zA-Z0-9._%-]+$/); /** PayTo URI validator for traditional banking */ export const PayToURISchema = z.string().regex(/^payto:\/\/[a-zA-Z0-9._%-]+\/[a-zA-Z0-9._%-]+/); /** Settlement Address (union of CAIP-10 and PayTo URI) */ export const SettlementAddressSchema = z.union([CAIP10Schema, PayToURISchema]); /** Amount as decimal string validator */ export const AmountSchema = z.string().regex(/^\d+(\.\d+)?$/); /** Valid ISO 4217 Currency Codes - extracted from currencies.ts */ const VALID_CURRENCY_CODES = [ "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CHF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VES", "VND", "VUV", "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWL" ] as const; /** Valid ISO 20022 Purpose Codes - extracted from purpose_codes.ts */ const VALID_PURPOSE_CODES = [ "ACCT", "ADCS", "ADMG", "ADVA", "AEMP", "AGRT", "AIRB", "ALLW", "ALMY", "AMEX", "ANNI", "ANTS", "AREN", "AUCO", "BBSC", "BCDM", "BCFG", "BECH", "BENE", "BEXP", "BFWD", "BKDF", "BKFE", "BKFM", "BKIP", "BKPP", "BLDM", "BNET", "BOCE", "BOND", "BONU", "BUSB", "CABD", "CAEQ", "CAFI", "CASH", "CBCR", "CBFF", "CBFR", "CBLK", "CBTV", "CCHD", "CCIR", "CCPC", "CCPM", "CCRD", "CCSM", "CDBL", "CDCB", "CDCD", "CDCS", "CDDP", "CDEP", "CDOC", "CDQC", "CFDI", "CFEE", "CGDD", "CHAR", "CLPR", "CMDT", "COLL", "COMC", "COMM", "COMP", "COMT", "CORT", "COST", "CPEN", "CPKC", "CPYR", "CRDS", "CRPR", "CRSP", "CRTL", "CSDB", "CSLP", "CVCF", "DBCR", "DBTC", "DCRD", "DEBT", "DEPD", "DEPT", "DERI", "DICL", "DIVD", "DMEQ", "DNTS", "DSMT", "DVPM", "ECPG", "ECPR", "ECPU", "EDUC", "EFTC", "EFTD", "ELEC", "ENRG", "EPAY", "EQPT", "EQTS", "EQUS", "ESTX", "ETUP", "EXPT", "EXTD", "FACT", "FAND", "FCOL", "FCPM", "FEES", "FERB", "FIXI", "FLCR", "FNET", "FORW", "FREX", "FUTR", "FWBC", "FWCC", "FWLV", "FWSB", "FWSC", "FXNT", "GAFA", "GAHO", "GAMB", "GASB", "GDDS", "GDSV", "GFRP", "GIFT", "GOVI", "GOVT", "GSCB", "GSTX", "GVEA", "GVEB", "GVEC", "GVED", "GWLT", "HEDG", "HLRP", "HLST", "HLTC", "HLTI", "HREC", "HSPC", "HSTX", "ICCP", "ICRF", "IDCP", "IHRP", "INPC", "INPR", "INSC", "INSM", "INSU", "INTC", "INTE", "INTP", "INTX", "INVS", "IPAY", "IPCA", "IPDO", "IPEA", "IPEC", "IPEW", "IPPS", "IPRT", "IPUW", "IVPT", "LBIN", "LBRI", "LCOL", "LFEE", "LICF", "LIFI", "LIMA", "LMEQ", "LMFI", "LMRK", "LOAN", "LOAR", "LOTT", "LREB", "LREV", "LSFL", "LTCF", "MAFC", "MARF", "MARG", "MBSB", "MBSC", "MCDM", "MCFG", "MDCS", "MGCC", "MGSC", "MOMA", "MSVC", "MTUP", "NETT", "NITX", "NOWS", "NWCH", "NWCM", "OCCC", "OCDM", "OCFG", "OFEE", "OPBC", "OPCC", "OPSB", "OPSC", "OPTN", "OTCD", "OTHR", "OTLC", "PADD", "PAYR", "PCOM", "PDEP", "PEFC", "PENO", "PENS", "PHON", "PLDS", "PLRF", "POPE", "PPTI", "PRCP", "PRME", "PTSP", "PTXP", "RAPI", "RCKE", "RCPT", "RDTX", "REBT", "REFU", "RELG", "RENT", "REOD", "REPO", "RETL", "RHBS", "RIMB", "RINP", "RLWY", "RMCO", "ROYA", "RPBC", "RPCC", "RPNT", "RPSB", "RPSC", "RRBN", "RRCT", "RRTP", "RVPM", "RVPO", "SALA", "SASW", "SAVG", "SBSC", "SCIE", "SCIR", "SCRP", "SCVE", "SECU", "SEPI", "SERV", "SHBC", "SHCC", "SHSL", "SLEB", "SLOA", "SLPI", "SPLT", "SPSP", "SSBE", "STDY", "SUBS", "SUPP", "SWBC", "SWCC", "SWFP", "SWPP", "SWPT", "SWRS", "SWSB", "SWSC", "SWUF", "TAXR", "TAXS", "TBAN", "TBAS", "TBBC", "TBCC", "TBIL", "TCSC", "TELI", "TLRF", "TLRR", "TMPG", "TPRI", "TPRP", "TRAD", "TRCP", "TREA", "TRFD", "TRNC", "TRPT", "TRVC", "UBIL", "UNIT", "VATX", "VIEW", "VOST", "WEBI", "WHLD", "WTER", "ZABA" ] as const; /** Valid ISO 20022 Category Purpose Codes - extracted from purpose_codes.ts */ const VALID_CATEGORY_PURPOSE_CODES = [ "BONU", "CASH", "CGWV", "CIPC", "CONC", "CORT", "CTDF", "DCRD", "DIVI", "DVPM", "EPAY", "FCDT", "FCIN", "FCOL", "GOVT", "HEDG", "ICCP", "IDCP", "INTC", "INTE", "LBOX", "LOAN", "OTHR", "PENS", "RPRE", "RRCT", "RVPM", "SALA", "SAVG", "SECU", "SSBE", "SUPP", "SWEP", "TAXS", "TOPG", "TRAD", "TREA", "VATX" ] as const; /** ISO 4217 Currency Code validator */ export const CurrencyCodeSchema = z.enum(VALID_CURRENCY_CODES); /** ISO 20022 Purpose Code validator */ export const PurposeCodeSchema = z.enum(VALID_PURPOSE_CODES); /** ISO 20022 Category Purpose Code validator */ export const CategoryPurposeCodeSchema = z.enum(VALID_CATEGORY_PURPOSE_CODES); /** DTI (Digital Token Identifier) validator */ export const DTISchema = z.string().min(1); /** Asset ID (union of CAIP-19, DTI, and ISO 4217 currency code) */ export const AssetSchema = z.union([CAIP19Schema, DTISchema, CurrencyCodeSchema]); /** Supported Asset Pricing object for Payment messages */ export const SupportedAssetPricingSchema = z.object({ asset: AssetSchema, amount: AmountSchema, expires: ISO8601DateTimeSchema.optional() }); // ============================================================================ // JSON-LD BASE VALIDATORS // ============================================================================ /** Base JSON-LD object validator */ export const JsonLdObjectSchema = z.object({ "@context": z.union([IRISchema, z.record(z.string(), z.string())]).optional(), "@type": z.string() }); /** TAP Message Object base validator */ export const TapMessageObjectSchema = JsonLdObjectSchema.merge(z.object({ "@context": z.union([TAPContextSchema, z.record(z.string(), IRISchema)]), "@type": z.string() })); // ============================================================================ // PARTICIPANT VALIDATORS // ============================================================================ /** Base Participant validator */ export const ParticipantSchema = z.object({ "@id": DIDSchema, "@type": z.string().optional(), name: z.string(), email: z.string().email().optional(), phone: z.string().optional(), url: z.string().url().optional() }); /** Natural Person validator */ export const PersonSchema = ParticipantSchema.merge(z.object({ "@type": z.literal("https://schema.org/Person").optional(), givenName: z.string().optional(), familyName: z.string().optional(), additionalName: z.string().optional(), birthDate: z.string().optional(), nationality: z.string().optional() })); /** Organization validator */ export const OrganizationSchema = ParticipantSchema.merge(z.object({ "@type": z.literal("https://schema.org/Organization").optional(), legalName: z.string().optional(), taxID: z.string().optional(), leiCode: z.string().optional(), address: z.object({ streetAddress: z.string().optional(), addressLocality: z.string().optional(), addressRegion: z.string().optional(), postalCode: z.string().optional(), addressCountry: z.string().optional() }).optional() })); /** Party (Person or Organization) validator */ export const PartySchema = z.union([PersonSchema, OrganizationSchema]); /** Agent validator */ export const AgentSchema = OrganizationSchema.partial().merge(z.object({ "@id": DIDSchema, for: DIDSchema, role: z.string().optional(), policies: z.array(z.record(z.string(), z.unknown())).optional() })); // ============================================================================ // DIDCOMM MESSAGE VALIDATORS // ============================================================================ /** DIDComm Message base validator */ export const DIDCommMessageSchema = z.object({ id: UUIDSchema, type: TAPTypeSchema, from: DIDSchema, to: z.array(DIDSchema), created_time: z.number(), expires_time: z.number().optional(), body: z.record(z.string(), z.unknown()) }); /** DIDComm Reply base validator */ export const DIDCommReplySchema = DIDCommMessageSchema.merge(z.object({ thid: UUIDSchema })); // ============================================================================ // TAP MESSAGE VALIDATORS // ============================================================================ /** Transfer message body validator */ export const TransferSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Transfer"), asset: CAIP19Schema, amount: AmountSchema, originator: PartySchema, beneficiary: PartySchema, agents: z.array(AgentSchema), settlementAddress: SettlementAddressSchema.optional(), purposeCode: PurposeCodeSchema.optional(), reference: z.string().optional() })); /** Payment message body validator */ export const PaymentSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Payment"), amount: AmountSchema, asset: AssetSchema.optional(), currency: CurrencyCodeSchema.optional(), supportedAssets: z.array(z.union([AssetSchema, SupportedAssetPricingSchema])).optional(), fallbackSettlementAddresses: z.array(SettlementAddressSchema).optional(), merchant: PartySchema, customer: PartySchema.optional(), agents: z.array(AgentSchema), expiry: ISO8601DateTimeSchema.optional(), purposeCode: PurposeCodeSchema.optional(), reference: z.string().optional(), invoiceId: z.string().optional() })).refine(data => data.asset || data.currency, { message: "Either asset or currency must be provided" }); /** Authorization message body validator */ export const AuthorizeSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Authorize"), decision: z.enum(["approve", "deny"]), reason: z.string().optional(), conditions: z.array(z.record(z.string(), z.unknown())).optional() })); /** Transaction Constraints validator */ export const TransactionConstraintsSchema = z.object({ purposes: z.array(PurposeCodeSchema).optional(), categoryPurposes: z.array(CategoryPurposeCodeSchema).optional(), limits: z.object({ per_transaction: AmountSchema.optional(), per_day: AmountSchema.optional(), per_week: AmountSchema.optional(), per_month: AmountSchema.optional(), per_year: AmountSchema.optional(), currency: CurrencyCodeSchema }).optional(), allowedBeneficiaries: z.array(PartySchema).optional(), allowedSettlementAddresses: z.array(CAIP10Schema).optional(), allowedAssets: z.array(CAIP19Schema).optional() }); /** Connect message body validator */ export const ConnectSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Connect"), requester: PartySchema, principal: PartySchema, agents: z.array(AgentSchema), constraints: TransactionConstraintsSchema, expiry: ISO8601DateTimeSchema.optional(), agreement: z.string().url().optional() })); /** Settle message body validator */ export const SettleSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Settle"), txHash: z.string(), blockHash: z.string().optional(), blockNumber: z.number().optional(), transactionIndex: z.number().optional(), gasUsed: z.string().optional(), effectiveGasPrice: z.string().optional() })); /** Reject message body validator */ export const RejectSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Reject"), reason: z.string(), code: z.string().optional() })); /** Cancel message body validator */ export const CancelSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Cancel"), reason: z.string().optional() })); /** Revert message body validator */ export const RevertSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Revert"), reason: z.string(), txHash: z.string().optional() })); /** RFQ message body validator */ export const RFQSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("RFQ"), fromAssets: z.array(AssetSchema).min(1), toAssets: z.array(AssetSchema).min(1), fromAmount: AmountSchema.optional(), toAmount: AmountSchema.optional(), requester: PartySchema, provider: PartySchema.optional(), agents: z.array(AgentSchema).min(1), policies: z.array(z.record(z.string(), z.unknown())).optional() })).refine(data => data.fromAmount || data.toAmount, { message: "Either fromAmount or toAmount must be provided" }); /** Quote message body validator */ export const QuoteSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Quote"), fromAsset: AssetSchema, toAsset: AssetSchema, fromAmount: AmountSchema, toAmount: AmountSchema, provider: PartySchema, agents: z.array(AgentSchema).min(1), expiresAt: ISO8601DateTimeSchema })); /** Lock message body validator */ export const LockSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Lock"), asset: CAIP19Schema.optional(), currency: CurrencyCodeSchema.optional(), amount: AmountSchema, originator: PartySchema, beneficiary: PartySchema, expiry: ISO8601DateTimeSchema, agreement: z.string().url().optional(), agents: z.array(AgentSchema).min(1) })).refine(data => data.asset || data.currency, { message: "Either asset or currency must be provided" }); /** Capture message body validator */ export const CaptureSchema = TapMessageObjectSchema.merge(z.object({ "@type": z.literal("Capture"), amount: AmountSchema.optional(), settlementAddress: SettlementAddressSchema.optional() })); // ============================================================================ // DIDCOMM WRAPPED MESSAGE VALIDATORS // ============================================================================ /** Transfer DIDComm message validator */ export const TransferMessageSchema = DIDCommMessageSchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Transfer"), body: TransferSchema })); /** Payment DIDComm message validator */ export const PaymentMessageSchema = DIDCommMessageSchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Payment"), body: PaymentSchema })); /** Authorization DIDComm reply validator */ export const AuthorizeMessageSchema = DIDCommReplySchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Authorize"), body: AuthorizeSchema })); /** Connect DIDComm message validator */ export const ConnectMessageSchema = DIDCommMessageSchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Connect"), body: ConnectSchema })); /** Settle DIDComm reply validator */ export const SettleMessageSchema = DIDCommReplySchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Settle"), body: SettleSchema })); /** Reject DIDComm reply validator */ export const RejectMessageSchema = DIDCommReplySchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Reject"), body: RejectSchema })); /** Cancel DIDComm reply validator */ export const CancelMessageSchema = DIDCommReplySchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Cancel"), body: CancelSchema })); /** Revert DIDComm reply validator */ export const RevertMessageSchema = DIDCommReplySchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Revert"), body: RevertSchema })); /** RFQ DIDComm message validator */ export const RFQMessageSchema = DIDCommMessageSchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#RFQ"), body: RFQSchema })); /** Quote DIDComm reply validator */ export const QuoteMessageSchema = DIDCommReplySchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Quote"), body: QuoteSchema })); /** Lock DIDComm message validator */ export const LockMessageSchema = DIDCommMessageSchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Lock"), body: LockSchema })); /** Capture DIDComm reply validator */ export const CaptureMessageSchema = DIDCommReplySchema.merge(z.object({ type: z.literal("https://tap.rsvp/schema/1.0#Capture"), body: CaptureSchema })); // ============================================================================ // UNION VALIDATOR FOR ALL TAP MESSAGES // ============================================================================ /** All TAP Message types validator */ export const TAPMessageSchema = z.discriminatedUnion("type", [ TransferMessageSchema, PaymentMessageSchema, RFQMessageSchema, QuoteMessageSchema, LockMessageSchema, CaptureMessageSchema, AuthorizeMessageSchema, ConnectMessageSchema, SettleMessageSchema, RejectMessageSchema, CancelMessageSchema, RevertMessageSchema ]); // ============================================================================ // VALIDATION FUNCTIONS // ============================================================================ /** * Validates a TAP message against the appropriate schema * @param message - The message to validate * @returns Validation result with parsed data or error details */ export function validateTAPMessage(message: unknown) { return TAPMessageSchema.safeParse(message); } /** * Validates and parses a TAP message, throwing on validation failure * @param message - The message to validate * @returns Parsed and validated message * @throws ZodError if validation fails */ export function parseTAPMessage(message: unknown) { return TAPMessageSchema.parse(message); } /** * Type guard to check if an object is a valid TAP message * @param message - The message to check * @returns True if the message is a valid TAP message */ export function isTAPMessage(message: unknown): boolean { return TAPMessageSchema.safeParse(message).success; } // ============================================================================ // SPECIFIC MESSAGE TYPE VALIDATORS // ============================================================================ /** Validates Transfer messages */ export const validateTransferMessage = (message: unknown) => TransferMessageSchema.safeParse(message); /** Validates Payment messages */ export const validatePaymentMessage = (message: unknown) => PaymentMessageSchema.safeParse(message); /** Validates Authorization messages */ export const validateAuthorizeMessage = (message: unknown) => AuthorizeMessageSchema.safeParse(message); /** Validates Connect messages */ export const validateConnectMessage = (message: unknown) => ConnectMessageSchema.safeParse(message); /** Validates Settlement messages */ export const validateSettleMessage = (message: unknown) => SettleMessageSchema.safeParse(message); /** Validates Reject messages */ export const validateRejectMessage = (message: unknown) => RejectMessageSchema.safeParse(message); /** Validates Cancel messages */ export const validateCancelMessage = (message: unknown) => CancelMessageSchema.safeParse(message); /** Validates Revert messages */ export const validateRevertMessage = (message: unknown) => RevertMessageSchema.safeParse(message); /** Validates RFQ messages */ export const validateRFQMessage = (message: unknown) => RFQMessageSchema.safeParse(message); /** Validates Quote messages */ export const validateQuoteMessage = (message: unknown) => QuoteMessageSchema.safeParse(message); /** Validates Lock messages */ export const validateLockMessage = (message: unknown) => LockMessageSchema.safeParse(message); /** Validates Capture messages */ export const validateCaptureMessage = (message: unknown) => CaptureMessageSchema.safeParse(message); /** Validates Transaction Constraints */ export const validateTransactionConstraints = (constraints: unknown) => TransactionConstraintsSchema.safeParse(constraints);