UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

734 lines 34.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthFetch = void 0; // @ts-nocheck const Utils = __importStar(require("../../primitives/utils.js")); const Random_js_1 = __importDefault(require("../../primitives/Random.js")); const P2PKH_js_1 = __importDefault(require("../../script/templates/P2PKH.js")); const PublicKey_js_1 = __importDefault(require("../../primitives/PublicKey.js")); const createNonce_js_1 = require("../utils/createNonce.js"); const Peer_js_1 = require("../Peer.js"); const SimplifiedFetchTransport_js_1 = require("../transports/SimplifiedFetchTransport.js"); const SessionManager_js_1 = require("../SessionManager.js"); const index_js_1 = require("../utils/index.js"); const PAYMENT_VERSION = '1.0'; /** * AuthFetch provides a lightweight fetch client for interacting with servers * over a simplified HTTP transport mechanism. It integrates session management, peer communication, * and certificate handling to enable secure and mutually-authenticated requests. * * Additionally, it automatically handles 402 Payment Required responses by creating * and sending BSV payment transactions when necessary. */ class AuthFetch { /** * Constructs a new AuthFetch instance. * @param wallet - The wallet instance for signing and authentication. * @param requestedCertificates - Optional set of certificates to request from peers. */ constructor(wallet, requestedCertificates, sessionManager, originator) { this.callbacks = {}; this.certificatesReceived = []; this.peers = {}; this.wallet = wallet; this.requestedCertificates = requestedCertificates; this.sessionManager = sessionManager ?? new SessionManager_js_1.SessionManager(); this.originator = originator; } /** * Mutually authenticates and sends a HTTP request to a server. * * 1) Attempt the request. * 2) If 402 Payment Required, automatically create and send payment. * 3) Return the final response. * * @param url - The URL to send the request to. * @param config - Configuration options for the request, including method, headers, and body. * @returns A promise that resolves with the server's response, structured as a Response-like object. * * @throws Will throw an error if unsupported headers are used or other validation fails. */ async fetch(url, config = {}) { if (typeof config.retryCounter === 'number') { if (config.retryCounter <= 0) { throw new Error('Request failed after maximum number of retries.'); } config.retryCounter--; } const response = await new Promise((async (resolve, reject) => { try { // Apply defaults const { method = 'GET', headers = {}, body } = config; // Extract a base url const parsedUrl = new URL(url); const baseURL = parsedUrl.origin; // Create a new transport for this base url if needed let peerToUse; if (typeof this.peers[baseURL] === 'undefined') { // Create a peer for the request const newTransport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport(baseURL); peerToUse = { peer: new Peer_js_1.Peer(this.wallet, newTransport, this.requestedCertificates, this.sessionManager, undefined, this.originator), pendingCertificateRequests: [] }; this.peers[baseURL] = peerToUse; this.peers[baseURL].peer.listenForCertificatesReceived((senderPublicKey, certs) => { this.certificatesReceived.push(...certs); }); this.peers[baseURL].peer.listenForCertificatesRequested((async (verifier, requestedCertificates) => { try { this.peers[baseURL].pendingCertificateRequests.push(true); const certificatesToInclude = await (0, index_js_1.getVerifiableCertificates)(this.wallet, requestedCertificates, verifier, this.originator); await this.peers[baseURL].peer.sendCertificateResponse(verifier, certificatesToInclude); } finally { // Give the backend 500 ms to process the certificates we just sent, before releasing the queue entry await new Promise(resolve => setTimeout(resolve, 500)); this.peers[baseURL].pendingCertificateRequests.shift(); } })); } else { // Check if there's a session associated with this baseURL if (this.peers[baseURL].supportsMutualAuth === false) { // Use standard fetch if mutual authentication is not supported try { const response = await this.handleFetchAndValidate(url, config, this.peers[baseURL]); resolve(response); } catch (error) { reject(error); } return; } peerToUse = this.peers[baseURL]; } // Serialize the simplified fetch request. const requestNonce = (0, Random_js_1.default)(32); const requestNonceAsBase64 = Utils.toBase64(requestNonce); const writer = await this.serializeRequest(method, headers, body, parsedUrl, requestNonce); // Setup general message listener to resolve requests once a response is received this.callbacks[requestNonceAsBase64] = { resolve, reject }; const listenerId = peerToUse.peer.listenForGeneralMessages((senderPublicKey, payload) => { // Create a reader const responseReader = new Utils.Reader(payload); // Deserialize first 32 bytes of payload const responseNonceAsBase64 = Utils.toBase64(responseReader.read(32)); if (responseNonceAsBase64 !== requestNonceAsBase64) { return; } peerToUse.peer.stopListeningForGeneralMessages(listenerId); // Save the identity key for the peer for future requests, since we have it here. this.peers[baseURL].identityKey = senderPublicKey; this.peers[baseURL].supportsMutualAuth = true; // Status code const statusCode = responseReader.readVarIntNum(); // Headers const responseHeaders = {}; const nHeaders = responseReader.readVarIntNum(); if (nHeaders > 0) { for (let i = 0; i < nHeaders; i++) { const nHeaderKeyBytes = responseReader.readVarIntNum(); const headerKeyBytes = responseReader.read(nHeaderKeyBytes); const headerKey = Utils.toUTF8(headerKeyBytes); const nHeaderValueBytes = responseReader.readVarIntNum(); const headerValueBytes = responseReader.read(nHeaderValueBytes); const headerValue = Utils.toUTF8(headerValueBytes); responseHeaders[headerKey] = headerValue; } } // Add back the server identity key header responseHeaders['x-bsv-auth-identity-key'] = senderPublicKey; // Body let responseBody; const responseBodyBytes = responseReader.readVarIntNum(); if (responseBodyBytes > 0) { responseBody = responseReader.read(responseBodyBytes); } // Create the Response object const responseValue = new Response(responseBody ? new Uint8Array(responseBody) : null, { status: statusCode, statusText: `${statusCode}`, headers: new Headers(responseHeaders) }); // Resolve or reject the correct request with the response data this.callbacks[requestNonceAsBase64].resolve(responseValue); // Clean up delete this.callbacks[requestNonceAsBase64]; }); // Before sending general messages to the peer, ensure that no certificate requests are pending. // This way, the user would need to choose to either allow or reject the certificate request first. // If the server has a resource that requires certificates to be sent before access would be granted, // this makes sure the user has a chance to send the certificates before the resource is requested. if (peerToUse.pendingCertificateRequests.length > 0) { await new Promise(resolve => { setInterval(() => { if (peerToUse.pendingCertificateRequests.length === 0) { resolve(); } }, 100); // Check every 100 ms for the user to finish responding }); } // Send the request, now that all listeners are set up await peerToUse.peer.toPeer(writer.toArray(), peerToUse.identityKey).catch(async (error) => { if (error.message.includes('Session not found for nonce')) { delete this.peers[baseURL]; config.retryCounter ?? (config.retryCounter = 3); const response = await this.fetch(url, config); resolve(response); return; } if (error.message.includes('HTTP server failed to authenticate')) { try { const response = await this.handleFetchAndValidate(url, config, peerToUse); resolve(response); return; } catch (fetchError) { reject(fetchError); } } else { reject(error); } }); } catch (e) { reject(e); } })); // Check if server requires payment to access the requested route if (response.status === 402) { // Create and attach a payment, then retry return await this.handlePaymentAndRetry(url, config, response); } return response; } /** * Request Certificates from a Peer * @param baseUrl * @param certificatesToRequest */ async sendCertificateRequest(baseUrl, certificatesToRequest) { const parsedUrl = new URL(baseUrl); const baseURL = parsedUrl.origin; let peerToUse; if (typeof this.peers[baseURL] !== 'undefined') { peerToUse = { peer: this.peers[baseURL].peer }; } else { const newTransport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport(baseURL); peerToUse = { peer: new Peer_js_1.Peer(this.wallet, newTransport, this.requestedCertificates, this.sessionManager, this.originator) }; this.peers[baseURL] = peerToUse; } // Return a promise that resolves when certificates are received return await new Promise((async (resolve, reject) => { // Set up the listener before making the request const callbackId = peerToUse.peer.listenForCertificatesReceived((_senderPublicKey, certs) => { peerToUse.peer.stopListeningForCertificatesReceived(callbackId); this.certificatesReceived.push(...certs); resolve(certs); }); try { // Initiate the certificate request await peerToUse.peer.requestCertificates(certificatesToRequest, peerToUse.identityKey); } catch (err) { peerToUse.peer.stopListeningForCertificatesReceived(callbackId); reject(err); } })); } /** * Return any certificates we've collected thus far, then clear them out. */ consumeReceivedCertificates() { return this.certificatesReceived.splice(0); } /** * Serializes the HTTP request to be sent over the Transport. * * @param method - The HTTP method (e.g., 'GET', 'POST') for the request. * @param headers - A record of HTTP headers to include in the request. * @param body - The body of the request, if applicable (e.g., for POST/PUT requests). * @param parsedUrl - The parsed URL object containing the full request URL. * @param requestNonce - A unique random nonce to ensure request integrity. * @returns A promise that resolves to a `Writer` containing the serialized request. * * @throws Will throw an error if unsupported headers are used or serialization fails. */ async serializeRequest(method, headers, body, parsedUrl, requestNonce) { const writer = new Utils.Writer(); // Write request nonce writer.write(requestNonce); // Method length writer.writeVarIntNum(method.length); // Method writer.write(Utils.toArray(method)); // Handle pathname (e.g. /path/to/resource) if (parsedUrl.pathname.length > 0) { // Pathname length const pathnameAsArray = Utils.toArray(parsedUrl.pathname); writer.writeVarIntNum(pathnameAsArray.length); // Pathname writer.write(pathnameAsArray); } else { writer.writeVarIntNum(-1); } // Handle search params (e.g. ?q=hello) if (parsedUrl.search.length > 0) { // search length const searchAsArray = Utils.toArray(parsedUrl.search); writer.writeVarIntNum(searchAsArray.length); // search writer.write(searchAsArray); } else { writer.writeVarIntNum(-1); } // Construct headers to send / sign: // Ensures clients only provided supported HTTP request headers // - Include custom headers prefixed with x-bsv (excluding those starting with x-bsv-auth) // - Include a normalized version of the content-type header // - Include the authorization header const includedHeaders = []; for (let [k, v] of Object.entries(headers)) { k = k.toLowerCase(); // We will always sign lower-case header keys if (k.startsWith('x-bsv-') || k === 'authorization') { if (k.startsWith('x-bsv-auth')) { throw new Error('No BSV auth headers allowed here!'); } includedHeaders.push([k, v]); } else if (k.startsWith('content-type')) { // Normalize the Content-Type header by removing any parameters (e.g., "; charset=utf-8") v = v.split(';')[0].trim(); includedHeaders.push([k, v]); } else { throw new Error('Unsupported header in the simplified fetch implementation. Only content-type, authorization, and x-bsv-* headers are supported.'); } } // Sort the headers by key to ensure a consistent order for signing and verification. includedHeaders.sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); // nHeaders writer.writeVarIntNum(includedHeaders.length); for (let i = 0; i < includedHeaders.length; i++) { // headerKeyLength const headerKeyAsArray = Utils.toArray(includedHeaders[i][0], 'utf8'); writer.writeVarIntNum(headerKeyAsArray.length); // headerKey writer.write(headerKeyAsArray); // headerValueLength const headerValueAsArray = Utils.toArray(includedHeaders[i][1], 'utf8'); writer.writeVarIntNum(headerValueAsArray.length); // headerValue writer.write(headerValueAsArray); } // If method typically carries a body and body is undefined, default it // This prevents signature verification errors due to mismatch default body types with express const methodsThatTypicallyHaveBody = ['POST', 'PUT', 'PATCH', 'DELETE']; if (methodsThatTypicallyHaveBody.includes(method.toUpperCase()) && body === undefined) { // Check if content-type is application/json const contentTypeHeader = includedHeaders.find(([k]) => k === 'content-type'); if (contentTypeHeader && contentTypeHeader[1].includes('application/json')) { body = '{}'; } else { body = ''; } } // Handle body if (body) { const reqBody = await this.normalizeBodyToNumberArray(body); // Use the utility function writer.writeVarIntNum(reqBody.length); writer.write(reqBody); } else { writer.writeVarIntNum(-1); // No body } return writer; } /** * Handles a non-authenticated fetch requests and validates that the server is not claiming to be authenticated. */ async handleFetchAndValidate(url, config, peerToUse) { const response = await fetch(url, config); response.headers.forEach(header => { if (header.toLocaleLowerCase().startsWith('x-bsv')) { throw new Error('The server is trying to claim it has been authenticated when it has not!'); } }); if (response.ok) { peerToUse.supportsMutualAuth = false; return response; } else { throw new Error(`Request failed with status: ${response.status}`); } } /** * If we get 402 Payment Required, we build a transaction via wallet.createAction() * and re-attempt the request with an x-bsv-payment header. */ async handlePaymentAndRetry(url, config = {}, originalResponse) { const paymentVersion = originalResponse.headers.get('x-bsv-payment-version'); if (!paymentVersion || paymentVersion !== PAYMENT_VERSION) { throw new Error(`Unsupported x-bsv-payment-version response header. Client version: ${PAYMENT_VERSION}, Server version: ${paymentVersion}`); } const satoshisRequiredHeader = originalResponse.headers.get('x-bsv-payment-satoshis-required'); if (!satoshisRequiredHeader) { throw new Error('Missing x-bsv-payment-satoshis-required response header.'); } const satoshisRequired = parseInt(satoshisRequiredHeader); if (isNaN(satoshisRequired) || satoshisRequired <= 0) { throw new Error('Invalid x-bsv-payment-satoshis-required response header value.'); } const serverIdentityKey = originalResponse.headers.get('x-bsv-auth-identity-key'); if (typeof serverIdentityKey !== 'string') { throw new Error('Missing x-bsv-auth-identity-key response header.'); } const derivationPrefix = originalResponse.headers.get('x-bsv-payment-derivation-prefix'); if (typeof derivationPrefix !== 'string' || derivationPrefix.length < 1) { throw new Error('Missing x-bsv-payment-derivation-prefix response header.'); } let paymentContext = config.paymentContext; if (paymentContext != null) { const requirementsChanged = !this.isPaymentContextCompatible(paymentContext, satoshisRequired, serverIdentityKey, derivationPrefix); if (requirementsChanged) { this.logPaymentAttempt('warn', 'Server adjusted payment requirements; regenerating transaction', this.composePaymentLogDetails(url, paymentContext)); paymentContext = await this.createPaymentContext(url, config, satoshisRequired, serverIdentityKey, derivationPrefix); } } else { paymentContext = await this.createPaymentContext(url, config, satoshisRequired, serverIdentityKey, derivationPrefix); } if (paymentContext.attempts >= paymentContext.maxAttempts) { throw this.buildPaymentFailureError(url, paymentContext, new Error('Maximum payment attempts exceeded before retrying')); } const headersWithPayment = { ...(config.headers ?? {}) }; headersWithPayment['x-bsv-payment'] = JSON.stringify({ derivationPrefix: paymentContext.derivationPrefix, derivationSuffix: paymentContext.derivationSuffix, transaction: paymentContext.transactionBase64 }); const nextConfig = { ...config, headers: headersWithPayment, paymentContext }; if (typeof nextConfig.retryCounter !== 'number') { nextConfig.retryCounter = 3; } const attemptNumber = paymentContext.attempts + 1; const maxAttempts = paymentContext.maxAttempts; paymentContext.attempts = attemptNumber; const attemptDetails = this.composePaymentLogDetails(url, paymentContext); this.logPaymentAttempt('warn', `Attempting paid request (${attemptNumber}/${maxAttempts})`, attemptDetails); try { const response = await this.fetch(url, nextConfig); this.logPaymentAttempt('info', `Paid request attempt ${attemptNumber} succeeded`, attemptDetails); return response; } catch (error) { const errorEntry = this.createPaymentErrorEntry(paymentContext.attempts, error); paymentContext.errors.push(errorEntry); this.logPaymentAttempt('error', `Paid request attempt ${attemptNumber} failed`, { ...attemptDetails, error: { message: errorEntry.message, stack: errorEntry.stack } }); if (paymentContext.attempts >= paymentContext.maxAttempts) { throw this.buildPaymentFailureError(url, paymentContext, error); } const delayMs = this.getPaymentRetryDelay(paymentContext.attempts); await this.wait(delayMs); return this.handlePaymentAndRetry(url, nextConfig, originalResponse); } } isPaymentContextCompatible(context, satoshisRequired, serverIdentityKey, derivationPrefix) { return (context.satoshisRequired === satoshisRequired && context.serverIdentityKey === serverIdentityKey && context.derivationPrefix === derivationPrefix); } async createPaymentContext(url, config, satoshisRequired, serverIdentityKey, derivationPrefix) { const derivationSuffix = await (0, createNonce_js_1.createNonce)(this.wallet, undefined, this.originator); const { publicKey: derivedPublicKey } = await this.wallet.getPublicKey({ protocolID: [2, '3241645161d8'], keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: serverIdentityKey }, this.originator); const lockingScript = new P2PKH_js_1.default().lock(PublicKey_js_1.default.fromString(derivedPublicKey).toAddress()).toHex(); const { tx } = await this.wallet.createAction({ description: `Payment for request to ${new URL(url).origin}`, outputs: [{ satoshis: satoshisRequired, lockingScript, customInstructions: JSON.stringify({ derivationPrefix, derivationSuffix, payee: serverIdentityKey }), outputDescription: 'HTTP request payment' }], options: { randomizeOutputs: false } }, this.originator); const { publicKey: clientIdentityKey } = await this.wallet.getPublicKey({ identityKey: true }, this.originator); return { satoshisRequired, transactionBase64: Utils.toBase64(tx), derivationPrefix, derivationSuffix, serverIdentityKey, clientIdentityKey, attempts: 0, maxAttempts: this.getMaxPaymentAttempts(config), errors: [], requestSummary: this.buildPaymentRequestSummary(url, config) }; } getMaxPaymentAttempts(config) { const attempts = typeof config.paymentRetryAttempts === 'number' ? config.paymentRetryAttempts : undefined; if (typeof attempts === 'number' && attempts > 0) { return Math.floor(attempts); } return 3; } buildPaymentRequestSummary(url, config) { const headers = { ...(config.headers ?? {}) }; const method = typeof config.method === 'string' ? config.method.toUpperCase() : 'GET'; const bodySummary = this.describeRequestBodyForLogging(config.body); return { url, method, headers, bodyType: bodySummary.type, bodyByteLength: bodySummary.byteLength }; } describeRequestBodyForLogging(body) { if (body == null) { return { type: 'none', byteLength: 0 }; } if (typeof body === 'string') { return { type: 'string', byteLength: Utils.toArray(body, 'utf8').length }; } if (Array.isArray(body)) { if (body.every((item) => typeof item === 'number')) { return { type: 'number[]', byteLength: body.length }; } return { type: 'array', byteLength: body.length }; } if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) { return { type: 'ArrayBuffer', byteLength: body.byteLength }; } if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body)) { return { type: body.constructor != null ? body.constructor.name : 'TypedArray', byteLength: body.byteLength }; } if (typeof Blob !== 'undefined' && body instanceof Blob) { return { type: 'Blob', byteLength: body.size }; } if (typeof FormData !== 'undefined' && body instanceof FormData) { return { type: 'FormData', byteLength: 0 }; } if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) { const serialized = body.toString(); return { type: 'URLSearchParams', byteLength: Utils.toArray(serialized, 'utf8').length }; } if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) { return { type: 'ReadableStream', byteLength: 0 }; } try { const serialized = JSON.stringify(body); if (typeof serialized === 'string') { return { type: 'object', byteLength: Utils.toArray(serialized, 'utf8').length }; } } catch (_) { // Ignore JSON serialization issues for logging purposes. } return { type: typeof body, byteLength: 0 }; } composePaymentLogDetails(url, context) { return { url, request: context.requestSummary, payment: { satoshis: context.satoshisRequired, transactionBase64: context.transactionBase64, derivationPrefix: context.derivationPrefix, derivationSuffix: context.derivationSuffix, serverIdentityKey: context.serverIdentityKey, clientIdentityKey: context.clientIdentityKey }, attempts: { used: context.attempts, max: context.maxAttempts }, errors: context.errors }; } logPaymentAttempt(level, message, details) { const prefix = '[AuthFetch][Payment]'; if (level === 'error') { console.error(`${prefix} ${message}`, details); } else if (level === 'warn') { console.warn(`${prefix} ${message}`, details); } else { if (typeof console.info === 'function') { console.info(`${prefix} ${message}`, details); } else { console.log(`${prefix} ${message}`, details); } } } createPaymentErrorEntry(attempt, error) { const entry = { attempt, timestamp: new Date().toISOString(), message: '', stack: undefined }; if (error instanceof Error) { entry.message = error.message; entry.stack = error.stack ?? undefined; } else { entry.message = String(error); } return entry; } getPaymentRetryDelay(attempt) { const baseDelay = 250; const multiplier = Math.min(attempt, 5); return baseDelay * multiplier; } async wait(ms) { if (ms <= 0) { return; } await new Promise(resolve => setTimeout(resolve, ms)); } buildPaymentFailureError(url, context, lastError) { const message = `Paid request to ${url} failed after ${context.attempts}/${context.maxAttempts} attempts. Sent ${context.satoshisRequired} satoshis to ${context.serverIdentityKey}.`; const error = new Error(message); const failureDetails = { request: context.requestSummary, payment: { satoshis: context.satoshisRequired, transactionBase64: context.transactionBase64, derivationPrefix: context.derivationPrefix, derivationSuffix: context.derivationSuffix, serverIdentityKey: context.serverIdentityKey, clientIdentityKey: context.clientIdentityKey }, attempts: { used: context.attempts, max: context.maxAttempts }, errors: context.errors }; error.details = failureDetails; if (lastError instanceof Error) { ; error.cause = lastError; } return error; } async normalizeBodyToNumberArray(body) { // 0. Null / undefined if (body == null) { return []; } // 1. object if (typeof body === 'object') { return Utils.toArray(JSON.stringify(body, 'utf8')); } // 2. number[] if (Array.isArray(body) && body.every((item) => typeof item === 'number')) { return body; // Return the array as is } // 3. string if (typeof body === 'string') { return Utils.toArray(body, 'utf8'); } // 4. ArrayBuffer / TypedArrays if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { const typedArray = body instanceof ArrayBuffer ? new Uint8Array(body) : new Uint8Array(body.buffer); return Array.from(typedArray); } // 5. Blob if (body instanceof Blob) { const arrayBuffer = await body.arrayBuffer(); return Array.from(new Uint8Array(arrayBuffer)); } // 6. FormData if (body instanceof FormData) { const entries = []; body.forEach((value, key) => { entries.push([key, value.toString()]); }); const urlEncoded = new URLSearchParams(entries).toString(); return Utils.toArray(urlEncoded, 'utf8'); } // 7. URLSearchParams if (body instanceof URLSearchParams) { return Utils.toArray(body.toString(), 'utf8'); } // 8. ReadableStream if (body instanceof ReadableStream) { throw new Error('ReadableStream cannot be directly converted to number[].'); } // 9. Fallback throw new Error('Unsupported body type in this SimplifiedFetch implementation.'); } } exports.AuthFetch = AuthFetch; //# sourceMappingURL=AuthFetch.js.map