UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

259 lines 13 kB
import * as Utils from '../../primitives/utils.js'; // Only bind window.fetch in the browser const defaultFetch = typeof window !== 'undefined' ? fetch.bind(window) : fetch; /** * Implements an HTTP-specific transport for handling Peer mutual authentication messages. * This class integrates with fetch to send and receive authenticated messages between peers. */ export class SimplifiedFetchTransport { onDataCallback; fetchClient; baseUrl; /** * Constructs a new instance of SimplifiedFetchTransport. * @param baseUrl - The base URL for all HTTP requests made by this transport. * @param fetchClient - A fetch implementation to use for HTTP requests (default: global fetch). */ constructor(baseUrl, fetchClient = defaultFetch) { this.fetchClient = fetchClient; this.baseUrl = baseUrl; } /** * Sends a message to an HTTP server using the transport mechanism. * Handles both general and authenticated message types. For general messages, * the payload is deserialized and sent as an HTTP request. For other message types, * the message is sent as a POST request to the `/auth` endpoint. * * @param message - The AuthMessage to send. * @returns A promise that resolves when the message is successfully sent. * * @throws Will throw an error if no listener has been registered via `onData`. */ async send(message) { if (this.onDataCallback == null) { throw new Error('Listen before you start speaking. God gave you two ears and one mouth for a reason.'); } if (message.messageType !== 'general') { return await new Promise((resolve, reject) => { void (async () => { try { const responsePromise = this.fetchClient(`${this.baseUrl}/.well-known/auth`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(message) }); // For initialRequest message, mark connection as established and start pool. if (message.messageType !== 'initialRequest') { resolve(); } const response = await responsePromise; // Handle the response if data is received and callback is set if (response.ok && (this.onDataCallback != null)) { const responseMessage = await response.json(); this.onDataCallback(responseMessage); } else { // Server may be a non authenticated server throw new Error('HTTP server failed to authenticate'); } if (message.messageType === 'initialRequest') { resolve(); } } catch (e) { reject(e); } })(); }); } else { // Parse message payload const httpRequest = this.deserializeRequestPayload(message.payload); // Send the byte array as the HTTP payload const url = `${this.baseUrl}${httpRequest.urlPostfix}`; const httpRequestWithAuthHeaders = httpRequest; if (typeof httpRequest.headers !== 'object') { httpRequestWithAuthHeaders.headers = {}; } // Append auth headers in request to server httpRequestWithAuthHeaders.headers['x-bsv-auth-version'] = message.version; httpRequestWithAuthHeaders.headers['x-bsv-auth-identity-key'] = message.identityKey; httpRequestWithAuthHeaders.headers['x-bsv-auth-nonce'] = message.nonce; httpRequestWithAuthHeaders.headers['x-bsv-auth-your-nonce'] = message.yourNonce; httpRequestWithAuthHeaders.headers['x-bsv-auth-signature'] = Utils.toHex(message.signature); httpRequestWithAuthHeaders.headers['x-bsv-auth-request-id'] = httpRequest.requestId; // Ensure Content-Type is set for requests with a body if (httpRequestWithAuthHeaders.body != null) { const headers = httpRequestWithAuthHeaders.headers; if (headers['content-type'] == null) { throw new Error('Content-Type header is required for requests with a body.'); } const contentType = String(headers['content-type'] ?? ''); // Transform body based on Content-Type if (contentType.includes('application/json')) { // Convert byte array to JSON string httpRequestWithAuthHeaders.body = Utils.toUTF8(httpRequestWithAuthHeaders.body); } else if (contentType.includes('application/x-www-form-urlencoded')) { // Convert byte array to URL-encoded string httpRequestWithAuthHeaders.body = Utils.toUTF8(httpRequestWithAuthHeaders.body); } else if (contentType.includes('text/plain')) { // Convert byte array to plain UTF-8 string httpRequestWithAuthHeaders.body = Utils.toUTF8(httpRequestWithAuthHeaders.body); } else { // For all other content types, treat as binary data httpRequestWithAuthHeaders.body = new Uint8Array(httpRequestWithAuthHeaders.body); } } // Send the actual fetch request to the server const response = await this.fetchClient(url, { method: httpRequestWithAuthHeaders.method, headers: httpRequestWithAuthHeaders.headers, body: httpRequestWithAuthHeaders.body }); // Check for an acceptable status if (response.status === 500 && (response.headers.get('x-bsv-auth-request-id') == null && response.headers.get('x-bsv-auth-requested-certificates') == null)) { // Try parsing JSON error const errorInfo = await response.json(); // Otherwise just throw whatever we got throw new Error(`HTTP ${response.status} - ${JSON.stringify(errorInfo)}`); } const parsedBody = await response.arrayBuffer(); const payloadWriter = new Utils.Writer(); if (response.headers.get('x-bsv-auth-request-id') != null) { payloadWriter.write(Utils.toArray(response.headers.get('x-bsv-auth-request-id'), 'base64')); } payloadWriter.writeVarIntNum(response.status); // PARSE RESPONSE HEADERS FROM SERVER -------------------------------- // Parse response headers from the server and include only the signed headers: // - Include custom headers prefixed with x-bsv (excluding those starting with x-bsv-auth) // - Include the authorization header const includedHeaders = []; response.headers.forEach((value, key) => { const lowerKey = key.toLowerCase(); if ((lowerKey.startsWith('x-bsv-') || lowerKey === 'authorization') && !lowerKey.startsWith('x-bsv-auth')) { includedHeaders.push([lowerKey, value]); } }); // Sort the headers by key to ensure a consistent order for signing and verification. includedHeaders.sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); // nHeaders payloadWriter.writeVarIntNum(includedHeaders.length); for (let i = 0; i < includedHeaders.length; i++) { // headerKeyLength const headerKeyAsArray = Utils.toArray(includedHeaders[i][0], 'utf8'); payloadWriter.writeVarIntNum(headerKeyAsArray.length); // headerKey payloadWriter.write(headerKeyAsArray); // headerValueLength const headerValueAsArray = Utils.toArray(includedHeaders[i][1], 'utf8'); payloadWriter.writeVarIntNum(headerValueAsArray.length); // headerValue payloadWriter.write(headerValueAsArray); } // Handle body if (parsedBody != null) { const bodyAsArray = Array.from(new Uint8Array(parsedBody)); payloadWriter.writeVarIntNum(bodyAsArray.length); payloadWriter.write(bodyAsArray); } else { payloadWriter.writeVarIntNum(-1); } // Build the correct AuthMessage for the response const responseMessage = { version: response.headers.get('x-bsv-auth-version'), messageType: response.headers.get('x-bsv-auth-message-type') === 'certificateRequest' ? 'certificateRequest' : 'general', identityKey: response.headers.get('x-bsv-auth-identity-key'), nonce: response.headers.get('x-bsv-auth-nonce'), yourNonce: response.headers.get('x-bsv-auth-your-nonce'), requestedCertificates: JSON.parse(response.headers.get('x-bsv-auth-requested-certificates')), payload: payloadWriter.toArray(), signature: Utils.toArray(response.headers.get('x-bsv-auth-signature'), 'hex') }; // If the server didn't provide the correct authentication headers, throw an error if (responseMessage.version == null) { throw new Error('HTTP server failed to authenticate'); } // Handle the response if data is received and callback is set this.onDataCallback(responseMessage); } } /** * Registers a callback to handle incoming messages. * This must be called before sending any messages to ensure responses can be processed. * * @param callback - A function to invoke when an incoming AuthMessage is received. * @returns A promise that resolves once the callback is set. */ async onData(callback) { this.onDataCallback = (m) => { void callback(m); }; } /** * Deserializes a request payload from a byte array into an HTTP request-like structure. * * @param payload - The serialized payload to deserialize. * @returns An object representing the deserialized request, including the method, * URL postfix (path and query string), headers, body, and request ID. */ deserializeRequestPayload(payload) { // Create a reader const requestReader = new Utils.Reader(payload); // The first 32 bytes is the requestId const requestId = Utils.toBase64(requestReader.read(32)); // Method const methodLength = requestReader.readVarIntNum(); let method = 'GET'; if (methodLength > 0) { method = Utils.toUTF8(requestReader.read(methodLength)); } // Path const pathLength = requestReader.readVarIntNum(); let path = ''; if (pathLength > 0) { path = Utils.toUTF8(requestReader.read(pathLength)); } // Search const searchLength = requestReader.readVarIntNum(); let search = ''; if (searchLength > 0) { search = Utils.toUTF8(requestReader.read(searchLength)); } // Read headers const requestHeaders = {}; const nHeaders = requestReader.readVarIntNum(); if (nHeaders > 0) { for (let i = 0; i < nHeaders; i++) { const nHeaderKeyBytes = requestReader.readVarIntNum(); const headerKeyBytes = requestReader.read(nHeaderKeyBytes); const headerKey = Utils.toUTF8(headerKeyBytes); const nHeaderValueBytes = requestReader.readVarIntNum(); const headerValueBytes = requestReader.read(nHeaderValueBytes); const headerValue = Utils.toUTF8(headerValueBytes); requestHeaders[headerKey] = headerValue; } } // Read body let requestBody; const requestBodyBytes = requestReader.readVarIntNum(); if (requestBodyBytes > 0) { requestBody = requestReader.read(requestBodyBytes); } // Return the deserialized RequestInit return { urlPostfix: path + search, method, headers: requestHeaders, body: requestBody, requestId }; } } //# sourceMappingURL=SimplifiedFetchTransport.js.map