UNPKG

@bsv/payment-express-middleware

Version:
1 lines 16.6 kB
{"version":3,"file":"index.cjs","names":["Utils","PublicKey","Beef"],"sources":["../../src/index.ts"],"sourcesContent":["import { Beef, createNonce, PublicKey, Utils, verifyNonce, type AtomicBEEF } from '@bsv/sdk'\nimport type { RequestHandler, Response } from 'express'\nimport type {\n BSVPayment,\n PaymentMiddlewareOptions,\n PaymentReplayStore,\n PaymentRequest\n} from './types.js'\n\nconst PAYMENT_VERSION = '1.0'\nconst DEFAULT_MAX_PAYMENT_HEADER_BYTES = 64 * 1024\nconst DEFAULT_REPLAY_CAPACITY = 100_000\nconst MAX_NONCE_LENGTH = 512\n\ninterface ParsedPayment {\n payment: BSVPayment\n transaction: AtomicBEEF\n transactionId: string\n satoshis: number\n}\n\ninterface InternalizeResult {\n accepted?: boolean\n isMerge?: boolean\n}\n\nexport class InMemoryPaymentReplayStore implements PaymentReplayStore {\n private readonly claimed = new Set<string>()\n\n constructor(private readonly maxEntries: number = DEFAULT_REPLAY_CAPACITY) {\n if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {\n throw new RangeError('Replay-store capacity must be a positive safe integer.')\n }\n }\n\n claim(transactionId: string): boolean {\n if (this.claimed.has(transactionId)) return false\n if (this.claimed.size >= this.maxEntries) {\n throw new Error('Payment replay store capacity exceeded.')\n }\n this.claimed.add(transactionId)\n return true\n }\n}\n\nfunction isPositiveSafeInteger(value: number): boolean {\n return Number.isSafeInteger(value) && value > 0\n}\n\nfunction isCanonicalBase64(value: string): boolean {\n if (\n value.length === 0 ||\n !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)\n ) {\n return false\n }\n try {\n return Utils.toBase64(Utils.toArray(value, 'base64')) === value\n } catch {\n return false\n }\n}\n\nfunction isCompressedPublicKey(value: string): boolean {\n if (!/^(02|03)[0-9a-fA-F]{64}$/.test(value)) return false\n try {\n return PublicKey.fromString(value).toString() === value.toLowerCase()\n } catch {\n return false\n }\n}\n\nfunction paymentHeader(req: PaymentRequest): string | null | undefined {\n const value = req.headers['x-bsv-payment']\n if (value === undefined) return undefined\n return typeof value === 'string' ? value : null\n}\n\nfunction parsePaymentHeader(raw: string, maxBytes: number): BSVPayment | undefined {\n if (Buffer.byteLength(raw, 'utf8') > maxBytes) return undefined\n let value: unknown\n try {\n value = JSON.parse(raw)\n } catch {\n return undefined\n }\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined\n const record = value as Record<string, unknown>\n if (\n typeof record.derivationPrefix !== 'string' ||\n typeof record.derivationSuffix !== 'string' ||\n typeof record.transaction !== 'string' ||\n record.derivationPrefix.length > MAX_NONCE_LENGTH ||\n record.derivationSuffix.length > MAX_NONCE_LENGTH ||\n !isCanonicalBase64(record.derivationPrefix) ||\n !isCanonicalBase64(record.derivationSuffix) ||\n !isCanonicalBase64(record.transaction)\n ) {\n return undefined\n }\n return {\n derivationPrefix: record.derivationPrefix,\n derivationSuffix: record.derivationSuffix,\n transaction: record.transaction\n }\n}\n\nfunction parseAtomicPayment(\n payment: BSVPayment,\n requiredSatoshis: number\n): ParsedPayment | undefined {\n try {\n const transaction = Utils.toArray(payment.transaction, 'base64') as AtomicBEEF\n const beef = Beef.fromBinary(transaction)\n const transactionId = beef.atomicTxid\n if (typeof transactionId !== 'string') return undefined\n const atomicTransaction = beef.findTxid(transactionId)?.tx\n const satoshis = atomicTransaction?.outputs[0]?.satoshis\n if (typeof satoshis !== 'number' || satoshis < requiredSatoshis) return undefined\n return { payment, transaction, transactionId, satoshis }\n } catch {\n return undefined\n }\n}\n\nfunction sendError(\n res: Response,\n status: number,\n code: string,\n description: string,\n details: Record<string, unknown> = {}\n): void {\n res.status(status).json({\n status: 'error',\n code,\n ...details,\n description\n })\n}\n\nfunction safeErrorContext(error: unknown): Record<string, unknown> {\n return error instanceof Error ? { errorName: error.name } : { errorType: typeof error }\n}\n\nfunction isPaymentLogger(value: unknown): boolean {\n if (value === undefined) return true\n if (value === null || typeof value !== 'object') return false\n const logger = value as Record<string, unknown>\n return (\n (logger.error === undefined || typeof logger.error === 'function') &&\n (logger.warn === undefined || typeof logger.warn === 'function')\n )\n}\n\nasync function issuePaymentChallenge(\n wallet: PaymentMiddlewareOptions['wallet'],\n res: Response,\n requestPrice: number,\n logger: PaymentMiddlewareOptions['logger']\n): Promise<void> {\n try {\n const derivationPrefix = await createNonce(wallet)\n res\n .status(402)\n .set({\n 'x-bsv-payment-version': PAYMENT_VERSION,\n 'x-bsv-payment-satoshis-required': String(requestPrice),\n 'x-bsv-payment-derivation-prefix': derivationPrefix\n })\n .json({\n status: 'error',\n code: 'ERR_PAYMENT_REQUIRED',\n satoshisRequired: requestPrice,\n description: 'A BSV payment is required. Provide the X-BSV-Payment header.'\n })\n } catch (error) {\n logger?.error?.('Failed to create a payment challenge.', safeErrorContext(error))\n sendError(res, 503, 'ERR_PAYMENT_UNAVAILABLE', 'Payment processing is temporarily unavailable.')\n }\n}\n\n/**\n * Creates middleware that enforces a BRC-29 wallet payment after BRC-103 auth.\n */\nexport function createPaymentMiddleware(options: PaymentMiddlewareOptions): RequestHandler {\n if (options === null || typeof options !== 'object') {\n throw new TypeError('Payment middleware options are required.')\n }\n\n const {\n calculateRequestPrice = () => 100,\n wallet,\n replayStore = new InMemoryPaymentReplayStore(),\n maxPaymentHeaderBytes = DEFAULT_MAX_PAYMENT_HEADER_BYTES,\n logger\n } = options\n\n if (typeof calculateRequestPrice !== 'function') {\n throw new TypeError('The calculateRequestPrice option must be a function.')\n }\n if (\n wallet === null ||\n typeof wallet !== 'object' ||\n typeof wallet.internalizeAction !== 'function'\n ) {\n throw new TypeError('A valid wallet instance must be supplied to the payment middleware.')\n }\n if (replayStore === null || typeof replayStore.claim !== 'function') {\n throw new TypeError('A replay store with an atomic claim method is required.')\n }\n if (!Number.isSafeInteger(maxPaymentHeaderBytes) || maxPaymentHeaderBytes < 1) {\n throw new RangeError('maxPaymentHeaderBytes must be a positive safe integer.')\n }\n if (!isPaymentLogger(logger)) {\n throw new TypeError('logger error and warn properties must be functions when provided.')\n }\n\n return async (req, res, next): Promise<void> => {\n const paymentRequest: PaymentRequest = req\n const identityKey = paymentRequest.auth?.identityKey\n if (typeof identityKey !== 'string' || !isCompressedPublicKey(identityKey)) {\n sendError(\n res,\n 500,\n 'ERR_SERVER_MISCONFIGURED',\n 'The payment middleware must run after successful Auth middleware.'\n )\n return\n }\n\n let requestPrice: number\n try {\n requestPrice = await calculateRequestPrice(paymentRequest)\n } catch (error) {\n logger?.error?.('Payment pricing failed.', safeErrorContext(error))\n sendError(\n res,\n 500,\n 'ERR_PAYMENT_INTERNAL',\n 'An internal error occurred while determining the payment required for this request.'\n )\n return\n }\n\n if (requestPrice === 0) {\n paymentRequest.payment = { satoshisPaid: 0, accepted: true, tx: '', txid: '' }\n next()\n return\n }\n if (!isPositiveSafeInteger(requestPrice)) {\n logger?.error?.('Payment pricing returned an invalid value.', { requestPrice })\n sendError(res, 500, 'ERR_PAYMENT_INTERNAL', 'The configured payment price is invalid.')\n return\n }\n\n const rawPayment = paymentHeader(paymentRequest)\n if (rawPayment === undefined) {\n await issuePaymentChallenge(wallet, res, requestPrice, logger)\n return\n }\n\n if (rawPayment === null) {\n sendError(res, 400, 'ERR_MALFORMED_PAYMENT', 'The X-BSV-Payment header is malformed.')\n return\n }\n\n const payment = parsePaymentHeader(rawPayment, maxPaymentHeaderBytes)\n if (payment === undefined) {\n sendError(res, 400, 'ERR_MALFORMED_PAYMENT', 'The X-BSV-Payment header is malformed.')\n return\n }\n\n let validPrefix = false\n try {\n validPrefix = await verifyNonce(payment.derivationPrefix, wallet)\n } catch (error) {\n logger?.warn?.('Payment derivation-prefix verification failed.', safeErrorContext(error))\n }\n if (!validPrefix) {\n sendError(\n res,\n 400,\n 'ERR_INVALID_DERIVATION_PREFIX',\n 'The payment derivation prefix is invalid.'\n )\n return\n }\n\n const parsed = parseAtomicPayment(payment, requestPrice)\n if (parsed === undefined) {\n sendError(\n res,\n 400,\n 'ERR_INVALID_PAYMENT',\n 'The payment transaction is invalid or does not cover the required amount.'\n )\n return\n }\n\n let claimed: boolean\n try {\n const claimResult: unknown = await replayStore.claim(parsed.transactionId)\n if (typeof claimResult !== 'boolean') {\n throw new TypeError('The replay store returned an invalid claim result.')\n }\n claimed = claimResult\n } catch (error) {\n logger?.error?.('Payment replay claim failed.', safeErrorContext(error))\n sendError(\n res,\n 503,\n 'ERR_PAYMENT_UNAVAILABLE',\n 'Payment processing is temporarily unavailable.'\n )\n return\n }\n if (!claimed) {\n sendError(res, 409, 'ERR_PAYMENT_REPLAYED', 'This payment was already used.')\n return\n }\n\n try {\n const result = (await wallet.internalizeAction({\n tx: parsed.transaction,\n outputs: [\n {\n paymentRemittance: {\n derivationPrefix: payment.derivationPrefix,\n derivationSuffix: payment.derivationSuffix,\n senderIdentityKey: identityKey\n },\n outputIndex: 0,\n protocol: 'wallet payment'\n }\n ],\n description: 'Payment for request'\n })) as InternalizeResult\n\n if (result.accepted !== true || result.isMerge === true) {\n sendError(res, 409, 'ERR_PAYMENT_REPLAYED', 'This payment was not newly accepted.')\n return\n }\n\n paymentRequest.payment = {\n satoshisPaid: parsed.satoshis,\n accepted: true,\n tx: payment.transaction,\n txid: parsed.transactionId\n }\n res.set({\n 'x-bsv-payment-satoshis-paid': String(parsed.satoshis)\n })\n next()\n } catch (error) {\n logger?.warn?.('Payment internalization failed.', safeErrorContext(error))\n sendError(res, 400, 'ERR_PAYMENT_FAILED', 'The payment could not be accepted.')\n }\n }\n}\n"],"mappings":"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"}