import type { Client } from "../../client.ts"; import type { ProxyFetchRequest, ProxyFetchResponse, } from "../../plugos/proxy_fetch.ts"; import { fsEndpoint } from "../../spaces/constants.ts"; import { LuaNativeJSFunction, LuaTable } from "../runtime.ts"; export const netApi = new LuaTable({ proxyFetch: new LuaNativeJSFunction({ callback: async ( url: string, options: ProxyFetchRequest = {}, ): Promise => { // JSONify any non-serializable body if ( options?.body && typeof options.body !== "string" && !(options.body instanceof Uint8Array) ) { options.body = JSON.stringify(options.body); } const fetchOptions = options ? { method: options.method, headers: {} as Record, body: options.body, } : {}; fetchOptions.headers = buildProxyHeaders(options.headers); const resp = await client.httpSpacePrimitives.authenticatedFetch( buildProxyUrl(client, url), fetchOptions, ); if (resp.status !== 200) { return { ok: false, status: resp.status, headers: extractProxyHeaders(resp.headers), body: await resp.text(), }; } // Do sensible things with the body based on the content type // Read as ArrayBuffer first to safely handle empty responses (e.g. // PUT/DELETE returning 204 with Content-Type: application/json). // resp.arrayBuffer() never throws on an empty body, whereas // resp.json() would throw a SyntaxError. const rawBytes = new Uint8Array(await resp.arrayBuffer()); let body: any; const contentTypeHeader = options.responseEncoding || resp.headers.get("x-proxy-header-content-type"); const statusCode = +(resp.headers.get("x-proxy-status-code") || "200"); if (rawBytes.length === 0) { body = null; } else if (contentTypeHeader?.startsWith("application/json")) { body = JSON.parse(new TextDecoder().decode(rawBytes)); } else if ( contentTypeHeader?.startsWith("application/xml") || contentTypeHeader?.startsWith("text/") ) { body = new TextDecoder().decode(rawBytes); } else { body = rawBytes; } return { ok: resp.ok, status: statusCode, headers: extractProxyHeaders(resp.headers), body: body, }; }, description: "Performs an HTTP request through the SilverBullet server to avoid browser CORS restrictions.", signatures: ["net.proxyFetch(url, options?)"], parameters: [ { name: "url", type: "string", description: "URL to request." }, { name: "options", type: "table", description: "Optional method, headers, body, and responseEncoding values.", optional: true, }, ], returns: [ { type: "table", description: "Response status, headers, decoded body, and ok flag.", }, ], see: "API/net", }), readURI: new LuaNativeJSFunction({ callback: ( uri: string, options: { uri?: string; encoding?: string } = {}, ) => { options.uri = uri; return client.clientSystem.serviceRegistry.invokeBestMatch( `net.readURI:${uri}`, options, ); }, description: "Reads content from a URI using the best matching service.", signatures: ["net.readURI(uri, options?)"], parameters: [ { name: "uri", type: "string", description: "URI to read." }, { name: "options", type: "table", description: "Optional service-specific values such as encoding.", optional: true, }, ], returns: [{ description: "Content returned by the matching service." }], see: "API/net", }), writeURI: new LuaNativeJSFunction({ callback: (uri: string, content: string | Uint8Array) => { return client.clientSystem.serviceRegistry.invokeBestMatch( `net.writeURI:${uri}`, { uri, content }, ); }, description: "Writes content to a URI using the best matching service.", signatures: ["net.writeURI(uri, content)"], parameters: [ { name: "uri", type: "string", description: "URI to write." }, { name: "content", type: "string|userdata", description: "Text or binary content to write.", }, ], returns: [{ description: "Result returned by the matching service." }], see: "API/net", }), }); // Utility functions function buildProxyUrl(client: Client, url: string) { url = url.replace(/^https?:\/\//, ""); // Strip off the /.fs and replace with /.proxy return ( client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length) + "/.proxy/" + url ); } function buildProxyHeaders(headers?: Record): Record { const newHeaders: Record = { "X-Proxy-Request": "true" }; if (!headers) { return newHeaders; } for (const [key, value] of Object.entries(headers)) { newHeaders[`X-Proxy-Header-${key}`] = value; } return newHeaders; } function extractProxyHeaders(headers: Headers): Record { const newHeaders: Record = {}; for (const [key, value] of headers.entries()) { if (key.toLowerCase().startsWith("x-proxy-header-")) { newHeaders[key.slice("x-proxy-header-".length)] = value; } } return newHeaders; }