From aa6553b1968c591423f824774014973036a25b18 Mon Sep 17 00:00:00 2001 From: adityapk00 <31996805+adityapk00@users.noreply.github.com> Date: Fri, 22 Sep 2023 06:09:11 -0700 Subject: [PATCH] [wip] feat: Enable HTTP API Server (#1405) --- .changeset/brave-coins-yell.md | 5 + README.md | 1 + apps/hubble/package.json | 8 +- apps/hubble/scripts/httpapidocs.js | 191 +++ apps/hubble/scripts/linter.cjs | 6 +- apps/hubble/src/cli.ts | 6 +- apps/hubble/src/hubble.ts | 12 +- apps/hubble/src/network/sync/merkleTrie.ts | 40 +- apps/hubble/src/rpc/httpServer.ts | 265 ++-- apps/hubble/src/rpc/server.ts | 9 +- apps/hubble/src/rpc/test/bulkService.test.ts | 5 +- apps/hubble/src/rpc/test/concurrency.test.ts | 6 +- apps/hubble/src/rpc/test/httpServer.test.ts | 148 ++- apps/hubble/src/rpc/test/linkService.test.ts | 5 +- .../src/rpc/test/reactionService.test.ts | 5 +- apps/hubble/src/rpc/test/rpcAuth.test.ts | 9 +- apps/hubble/src/rpc/test/server.test.ts | 5 +- .../hubble/src/rpc/test/signerService.test.ts | 5 +- .../hubble/src/rpc/test/submitService.test.ts | 5 +- apps/hubble/src/rpc/test/syncService.test.ts | 5 +- .../src/rpc/test/userDataService.test.ts | 5 +- .../src/rpc/test/verificationService.test.ts | 5 +- apps/hubble/www/docs/.vitepress/config.ts | 1 + apps/hubble/www/docs/docs/cli.md | 5 +- apps/hubble/www/docs/docs/httpapi.md | 1109 +++++++++++++++++ packages/hub-nodejs/docs/README.md | 3 +- yarn.lock | 585 +++++++++ 27 files changed, 2238 insertions(+), 216 deletions(-) create mode 100644 .changeset/brave-coins-yell.md create mode 100644 apps/hubble/scripts/httpapidocs.js create mode 100644 apps/hubble/www/docs/docs/httpapi.md diff --git a/.changeset/brave-coins-yell.md b/.changeset/brave-coins-yell.md new file mode 100644 index 0000000000..8ab1aabb05 --- /dev/null +++ b/.changeset/brave-coins-yell.md @@ -0,0 +1,5 @@ +--- +"@farcaster/hubble": patch +--- + +feat: Enable HTTP API server diff --git a/README.md b/README.md index f2e930d81c..651d7b47c1 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This monorepo contains Hubble, an official Farcaster Hub implementation, and oth 1. To run Hubble, see the [Hubble docs](https://www.thehubble.xyz/). 1. To use Hubble, see the [hub-nodejs docs](./packages/hub-nodejs/docs/README.md). +1. To use the HTTP API to read Hubble data, see the [HTTP API docs](https://www.thehubble.xyz/docs/httpapi.html) ## Packages diff --git a/apps/hubble/package.json b/apps/hubble/package.json index 9d02cb2ddc..ef329507f2 100644 --- a/apps/hubble/package.json +++ b/apps/hubble/package.json @@ -57,7 +57,11 @@ "prettier-config-custom": "*", "progress": "~2.0.3", "ts-mockito": "~2.6.1", - "tsx": "~3.12.5" + "tsx": "~3.12.5", + "remark": "^15.0.1", + "remark-gfm": "^4.0.0", + "remark-parse": "^11.0.0", + "unified": "^11.0.3" }, "dependencies": { "@aws-sdk/client-s3": "^3.400.0", @@ -82,9 +86,9 @@ "axios": "^1.4.0", "cli-progress": "^3.12.0", "commander": "~10.0.0", - "libp2p": "0.43.4", "fastify": "^4.22.0", "hot-shots": "^10.0.0", + "libp2p": "0.43.4", "neverthrow": "~6.0.0", "node-cron": "~3.0.2", "pino": "~8.11.0", diff --git a/apps/hubble/scripts/httpapidocs.js b/apps/hubble/scripts/httpapidocs.js new file mode 100644 index 0000000000..831cef9780 --- /dev/null +++ b/apps/hubble/scripts/httpapidocs.js @@ -0,0 +1,191 @@ +import { readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { remark } from "remark"; +import remarkParse from "remark-parse"; +import remarkGfm from "remark-gfm"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * This linter checks that each of the HTTP API server endpoints is documented properly. + * This will check: + * 1. That all endpoints have a "@doc-tag:" comment in the httpServer.ts file + * 2. Make sure that all endpoints have a corresponding section in the HTTP API docs + * 3. Make sure that all parameters for every endpoint are documented in the HTTP API docs + * under the corresponding section + * 4. Make sure that all parameters that are documented are specified in the @docs-tag comment + * for that endpoint in the httpServer.ts file + */ +export function httpapidocs() { + function extractUniqueEndpoints(fileContent) { + const endpointSet = new Set(); + const regex = /\"\/v\d+\/([a-zA-Z0-9_]+)([a-zA-Z0-9_\/:]*)\"/g; + let match; + + // biome-ignore lint/suspicious/noAssignInExpressions: + while ((match = regex.exec(fileContent)) !== null) { + endpointSet.add(match[1]); + } + + return [...endpointSet]; + } + + function findMissingEndpointsInDocs(endpoints, docsContent) { + const missingEndpoints = endpoints.filter((endpoint) => !docsContent.includes(`### ${endpoint}`)); + + return missingEndpoints; + } + + function extractDocTags(fileContent) { + const endpointMap = {}; + const regex = /\/\/ @doc-tag:? \/([a-zA-Z0-9_]+)\??([^ \n]*)/; + const lines = fileContent.split("\n"); + + lines.forEach((line, index) => { + const match = line.match(regex); + if (match) { + const endpoint = match[1]; + const queryParams = match[2] + .split("&") + .filter(Boolean) + .map((param) => param.split("=")[0]); + + if (!endpointMap[endpoint]) { + endpointMap[endpoint] = { params: new Set(), lineNumbers: [] }; + } + + queryParams.forEach((param) => endpointMap[endpoint].params.add(param)); + endpointMap[endpoint].lineNumbers.push(index + 1); + } + }); + + // Convert sets to arrays for the final result + for (const endpoint in endpointMap) { + endpointMap[endpoint].params = [...endpointMap[endpoint].params]; + } + + return endpointMap; + } + + function findMissingDocTags(endpointMap, endpoints) { + const missingDocTags = []; + + for (const endpoint of endpoints) { + if (!endpointMap[endpoint]) { + missingDocTags.push(endpoint); + } + } + + return missingDocTags; + } + + function parseMarkdown(markdownContent) { + const processor = remark().use(remarkParse).use(remarkGfm); + const tree = processor.parse(markdownContent); + + return tree; + } + + function getParametersForEndpoint(endpoint, tree) { + let foundEndpoint = false; + let parameters = []; + let line = 0; + + tree.children.forEach((node, index) => { + if (node.type === "heading" && node.children[0].value === endpoint) { + foundEndpoint = true; + } + + if (foundEndpoint && node.type === "table") { + parameters = node.children + .slice(1) + .map((row) => row.children[0].children[0]?.value) + .filter((p) => p !== undefined); + line = node.position.start.line; + foundEndpoint = false; // Reset to stop looking after finding the table + } + }); + + return { parameters, line }; + } + + function checkParametersInDocs(docTags, tree) { + // For each endpoint, check if the parameters in the doc tags are present in the docs + let anyError = false; + for (const endpoint in docTags) { + const { parameters, line } = getParametersForEndpoint(endpoint, tree); + + for (const param of docTags[endpoint].params) { + if (!parameters.includes(param)) { + anyError = true; + console.error( + `Parameter "${param}" specified in the @doc-tag (on httpServer.ts: line ${docTags[ + endpoint + ].lineNumbers.join( + ", ", + )}) is missing documentation in the parameters table (on httpapi.md: line ${line}) for endpoint "${endpoint}"`, + ); + } + } + + // Check the other way. No excess params + for (const param of parameters) { + if (!docTags[endpoint].params.includes(param)) { + anyError = true; + console.error( + `Parameter "${param}" is documented in the parameters table (on httpapi.md: line ${line}) for endpoint "${endpoint}" but is not specified in the @doc-tag (on httpServer.ts: line ${docTags[ + endpoint + ].lineNumbers.join(", ")})`, + ); + } + } + } + + return anyError; + } + + const apiFilePath = join(__dirname, "../src/rpc/httpServer.ts"); + const contents = readFileSync(apiFilePath, "utf-8"); + + const endpoints = extractUniqueEndpoints(contents); + const docTags = extractDocTags(contents); + + const docFilePath = join(__dirname, "../www/docs/docs/httpapi.md"); + const docsContent = readFileSync(docFilePath, "utf-8"); + const tree = parseMarkdown(docsContent); + + // console.log(getParametersForEndpoint("castsByParent", tree)); + + // First, get all endPoints that are not documented in the docs + const missingEndpoints = findMissingEndpointsInDocs(endpoints, docsContent); + + // Next, get all endpoints that are documented but are missing doc tags + const missingDocTags = findMissingDocTags(docTags, endpoints); + + // console.log(docTags); + // Last, check for parameters + let anyError = checkParametersInDocs(docTags, tree); + + if (missingEndpoints.length > 0) { + console.error( + "The following endpoints specified in httpServer.ts are missing from the HTTP API docs in httpapi.md:", + ); + console.error(missingEndpoints); + anyError = true; + } + + if (missingDocTags.length > 0) { + console.error("The following endpoints specified in httpServer.ts are missing doc tags:"); + console.error(missingDocTags); + anyError = true; + } + + if (anyError) { + console.log("❌ HTTP API docs are not up to date"); + process.exit(1); + } else { + console.log("✨ HTTP API docs are up to date"); + } +} diff --git a/apps/hubble/scripts/linter.cjs b/apps/hubble/scripts/linter.cjs index a0f2ae606a..1ba079483b 100644 --- a/apps/hubble/scripts/linter.cjs +++ b/apps/hubble/scripts/linter.cjs @@ -11,10 +11,10 @@ async function executeAll() { const grafana = require("./grafanadash.cjs"); - await grafana(); - const clidocs = require("./clidocs.cjs"); - await clidocs(); + const { httpapidocs } = await import("./httpapidocs.js"); + + await Promise.all([grafana(), clidocs(), httpapidocs()]); } executeAll(); diff --git a/apps/hubble/src/cli.ts b/apps/hubble/src/cli.ts index a67da89a46..aab7b11ffd 100644 --- a/apps/hubble/src/cli.ts +++ b/apps/hubble/src/cli.ts @@ -76,8 +76,6 @@ app .option("--hub-operator-fid ", "The FID of the hub operator") .option("-c, --config ", "Path to the config file.") .option("--db-name ", "The name of the RocksDB instance. (default: rocks.hub._default)") - .option("--admin-server-enabled", "Enable the admin server. (default: disabled)") - .option("--admin-server-host ", "The host the admin server should listen on. (default: '127.0.0.1')") .option("--process-file-prefix ", 'Prefix for file to which hub process number is written. (default: "")') // Ethereum Options @@ -127,6 +125,9 @@ app "RPC rate limit for peers specified in rpm. Set to -1 for none. (default: 20k/min)", ) .option("--rpc-subscribe-per-ip-limit ", "Maximum RPC subscriptions per IP address. (default: 4)") + .option("--admin-server-enabled", "Enable the admin server. (default: disabled)") + .option("--admin-server-host ", "The host the admin server should listen on. (default: '127.0.0.1')") + .option("--http-server-disabled", "Disable the HTTP server. (default: enabled)") // Snapshots .option("--enable-snapshot-to-s3", "Enable daily snapshots to be uploaded to S3. (default: disabled)") @@ -512,6 +513,7 @@ app commitLockTimeout: cliOptions.commitLockTimeout ?? hubConfig.commitLockTimeout, commitLockMaxPending: cliOptions.commitLockMaxPending ?? hubConfig.commitLockMaxPending, adminServerEnabled: cliOptions.adminServerEnabled ?? hubConfig.adminServerEnabled, + httpServerDisabled: cliOptions.httpServerDisabled ?? hubConfig.httpServerDisabled ?? false, adminServerHost: cliOptions.adminServerHost ?? hubConfig.adminServerHost, testUsers: testUsers, directPeers, diff --git a/apps/hubble/src/hubble.ts b/apps/hubble/src/hubble.ts index cc231f4e28..afdd322359 100644 --- a/apps/hubble/src/hubble.ts +++ b/apps/hubble/src/hubble.ts @@ -213,6 +213,9 @@ export interface HubOptions { /** Commit lock queue size */ commitLockMaxPending?: number; + /** Http server disabled? */ + httpServerDisabled?: boolean; + /** Enables the Admin Server */ adminServerEnabled?: boolean; @@ -575,6 +578,11 @@ export class Hub implements HubInterface { // Start the RPC server await this.rpcServer.start(this.options.rpcServerHost, this.options.rpcPort ?? 0); + if (!this.options.httpServerDisabled) { + await this.httpApiServer.start(this.options.rpcServerHost, this.options.httpApiPort ?? 0); + } else { + log.info("HTTP API server disabled"); + } if (this.options.adminServerEnabled) { await this.adminServer.start(this.options.adminServerHost ?? "127.0.0.1"); } @@ -799,7 +807,9 @@ export class Hub implements HubInterface { clearInterval(this.contactTimer); // First, stop the RPC/Gossip server so we don't get any more messages - + if (!this.options.httpServerDisabled) { + await this.httpApiServer.stop(); + } await this.rpcServer.stop(true); // Force shutdown until we have a graceful way of ending active streams // Stop admin, gossip and sync engine diff --git a/apps/hubble/src/network/sync/merkleTrie.ts b/apps/hubble/src/network/sync/merkleTrie.ts index 8fdf1644bc..2b0a5d5c83 100644 --- a/apps/hubble/src/network/sync/merkleTrie.ts +++ b/apps/hubble/src/network/sync/merkleTrie.ts @@ -112,9 +112,16 @@ class MerkleTrie { this._worker.addListener("message", async (event) => { // console.log("Received message from worker thread", event); if (event.dbGetCallId) { - const value = await ResultAsync.fromPromise(this._db.get(Buffer.from(event.key)), (e) => e as Error); - if (value.isErr()) { - log.warn({ key: event.key, error: value.error }, "Error getting value from DB"); + // This can happen sometimes in tests when the DB is closed before the worker thread + let value = undefined; + if (this._db.status === "closed") { + log.warn("DB is closed. Ignoring DB read request from merkle trie worker thread"); + } else { + value = await ResultAsync.fromPromise(this._db.get(Buffer.from(event.key)), (e) => e as Error); + } + + if (!value || value.isErr()) { + log.warn({ key: event.key, error: value?.error }, "Error getting value from DB"); this._worker.postMessage({ dbGetCallId: event.dbGetCallId, value: undefined, @@ -126,19 +133,24 @@ class MerkleTrie { }); } } else if (event.dbKeyValuesCallId) { - const keyValues = event.dbKeyValues as MerkleTrieKV[]; - const txn = this._db.transaction(); - - // Collect all the pending DB updates into a single transaction batch - for (const { key, value } of keyValues) { - if (value && value.length > 0) { - txn.put(Buffer.from(key), Buffer.from(value)); - } else { - txn.del(Buffer.from(key)); + // This can happen sometimes in tests when the DB is closed before the worker thread + if (this._db.status === "closed") { + log.warn("DB is closed. Ignoring DB write request from merkle trie worker thread"); + } else { + const keyValues = event.dbKeyValues as MerkleTrieKV[]; + const txn = this._db.transaction(); + + // Collect all the pending DB updates into a single transaction batch + for (const { key, value } of keyValues) { + if (value && value.length > 0) { + txn.put(Buffer.from(key), Buffer.from(value)); + } else { + txn.del(Buffer.from(key)); + } } - } - await this._db.commit(txn); + await this._db.commit(txn); + } this._worker.postMessage({ dbKeyValuesCallId: event.dbKeyValuesCallId, }); diff --git a/apps/hubble/src/rpc/httpServer.ts b/apps/hubble/src/rpc/httpServer.ts index 9d3a6f5111..5bf3432d3e 100644 --- a/apps/hubble/src/rpc/httpServer.ts +++ b/apps/hubble/src/rpc/httpServer.ts @@ -18,7 +18,7 @@ import { userDataTypeFromJSON, utf8StringToBytes, } from "@farcaster/hub-nodejs"; -import { ServerUnaryCall } from "@grpc/grpc-js"; +import { Metadata, ServerUnaryCall } from "@grpc/grpc-js"; import fastify from "fastify"; import { Result, err, ok } from "neverthrow"; import { logger } from "../utils/logger.js"; @@ -53,9 +53,11 @@ function getCallObject( _method: M, params: DeepPartial>, request: fastify.FastifyRequest, + metadata?: Metadata, ): CallTypeForMethod { return { request: params, + metadata: metadata ?? new Metadata(), getPeer: () => request.ip, } as CallTypeForMethod; } @@ -64,7 +66,7 @@ function getCallObject( function handleResponse(reply: fastify.FastifyReply, obj: StaticEncodable): sendUnaryData { return (err, response) => { if (err) { - reply.code(400).send(JSON.stringify(err)); + reply.code(400).type("application/json").send(JSON.stringify(err)); } else { if (response) { // Convert the protobuf object to JSON @@ -103,23 +105,29 @@ function transformHash(obj: any): any { } // These are the target keys that are base64 encoded, which should be converted to hex - const targetKeys = [ + const toHexKeys = [ "hash", "address", "signer", "blockHash", "transactionHash", "key", + "owner", "to", "from", "recoveryAddress", ]; + // Convert these target keys to strings + const toStringKeys = ["name"]; + for (const key in obj) { // biome-ignore lint/suspicious/noPrototypeBuiltins: if (obj.hasOwnProperty(key)) { - if (targetKeys.includes(key) && typeof obj[key] === "string") { + if (toHexKeys.includes(key) && typeof obj[key] === "string") { obj[key] = convertB64ToHex(obj[key]); + } else if (toStringKeys.includes(key) && typeof obj[key] === "string") { + obj[key] = Buffer.from(obj[key], "base64").toString("utf-8"); } else if (typeof obj[key] === "object") { transformHash(obj[key]); } @@ -175,8 +183,6 @@ export class HttpAPIServer { this.grpcImpl = grpcImpl; this.engine = engine; - this.initHandlers(); - // Handle binary data this.app.addContentTypeParser("application/octet-stream", { parseAs: "buffer" }, function (req, body, done) { done(null, body); @@ -186,84 +192,84 @@ export class HttpAPIServer { log.error({ err: error, errMsg: error.message, request }, "Error in http request"); reply.code(500).send({ error: error.message }); }); + + this.initHandlers(); + } + + getMetadataFromAuthString(authString: string | undefined): Metadata { + const metadata = new Metadata(); + if (authString) { + metadata.add("authorization", authString); + } + + return metadata; } initHandlers() { //================Casts================ - // /cast/:fid/:hash - this.app.get<{ Params: { fid: string; hash: string } }>("/v1/cast/:fid/:hash", (request, reply) => { - const { fid, hash } = request.params; + // @doc-tag: /castById?fid=...&hash=... + this.app.get<{ Querystring: { fid: string; hash: string } }>("/v1/castById", (request, reply) => { + const { fid, hash } = request.query; const call = getCallObject("getCast", { fid: parseInt(fid), hash: hexStringToBytes(hash).unwrapOr([]) }, request); this.grpcImpl.getCast(call, handleResponse(reply, Message)); }); - // /casts/:fid?type=... - this.app.get<{ Params: { fid: string }; Querystring: QueryPageParams }>("/v1/casts/:fid", (request, reply) => { - const { fid } = request.params; + // @doc-tag: /castsByFid?fid=... + this.app.get<{ Querystring: QueryPageParams & { fid: string } }>("/v1/castsByFid", (request, reply) => { + const { fid } = request.query; const pageOptions = getPageOptions(request.query); const call = getCallObject("getAllCastMessagesByFid", { fid: parseInt(fid), ...pageOptions }, request); this.grpcImpl.getAllCastMessagesByFid(call, handleResponse(reply, MessagesResponse)); }); - // /casts/parent/:fid/:hash - this.app.get<{ Params: { fid: string; hash: string }; Querystring: QueryPageParams }>( - "/v1/casts/parent/:fid/:hash", + // @doc-tag: /castsByParent?fid=...&hash=... + // @doc-tag: /castsByParent?url=... + this.app.get<{ Querystring: QueryPageParams & { fid: string; hash: string; url: string } }>( + "/v1/castsByParent", (request, reply) => { - const { fid, hash } = request.params; + const { fid, hash, url } = request.query; + const decodedUrl = decodeURIComponent(url); const pageOptions = getPageOptions(request.query); + let parentCastId = undefined; + if (fid && hash) { + parentCastId = { fid: parseInt(fid), hash: hexStringToBytes(hash).unwrapOr([]) }; + } + const call = getCallObject( "getCastsByParent", - { - parentCastId: { fid: parseInt(fid), hash: hexStringToBytes(hash).unwrapOr([]) }, - ...pageOptions, - }, + { parentUrl: decodedUrl, parentCastId, ...pageOptions }, request, ); - this.grpcImpl.getCastsByParent(call, handleResponse(reply, MessagesResponse)); }, ); - // /casts/parent?url=... - this.app.get<{ Querystring: QueryPageParams & { url: string } }>("/v1/casts/parent", (request, reply) => { - const { url } = request.query; + // @doc-tag: /castsByMention?fid=... + this.app.get<{ Querystring: QueryPageParams & { fid: string } }>("/v1/castsByMention", (request, reply) => { + const { fid } = request.query; const pageOptions = getPageOptions(request.query); - const decodedUrl = decodeURIComponent(url); - const call = getCallObject("getCastsByParent", { parentUrl: decodedUrl, ...pageOptions }, request); - this.grpcImpl.getCastsByParent(call, handleResponse(reply, MessagesResponse)); + const call = getCallObject("getCastsByMention", { fid: parseInt(fid), ...pageOptions }, request); + this.grpcImpl.getCastsByMention(call, handleResponse(reply, MessagesResponse)); }); - // /casts/mention/:fid - this.app.get<{ Params: { fid: string }; Querystring: QueryPageParams }>( - "/v1/casts/mention/:fid", - (request, reply) => { - const { fid } = request.params; - const pageOptions = getPageOptions(request.query); - - const call = getCallObject("getCastsByMention", { fid: parseInt(fid), ...pageOptions }, request); - this.grpcImpl.getCastsByMention(call, handleResponse(reply, MessagesResponse)); - }, - ); - //=================Reactions================= - // /reactions/:fid/:target_fid/:target_hash?type=... + // @doc-tag: /reactionById?fid=...&target_fid=...&target_hash=...&reaction_type=... this.app.get<{ - Params: { fid: string; target_fid: string; target_hash: string }; - Querystring: { reactionType: string }; - }>("/v1/reaction/:fid/:target_fid/:target_hash", (request, reply) => { - const { fid, target_fid, target_hash } = request.params; + Querystring: { reaction_type: string; fid: string; target_fid: string; target_hash: string }; + }>("/v1/reactionById", (request, reply) => { + const { fid, target_fid, target_hash } = request.query; const call = getCallObject( "getReaction", { fid: parseInt(fid), targetCastId: { fid: parseInt(target_fid), hash: hexStringToBytes(target_hash).unwrapOr([]) }, - reactionType: getProtobufType(request.query.reactionType, reactionTypeFromJSON) ?? 0, + reactionType: getProtobufType(request.query.reaction_type, reactionTypeFromJSON) ?? 0, }, request, ); @@ -271,18 +277,18 @@ export class HttpAPIServer { this.grpcImpl.getReaction(call, handleResponse(reply, Message)); }); - // /reactions/:fid?type=... - this.app.get<{ Params: { fid: string }; Querystring: { reactionType: string } & QueryPageParams }>( - "/v1/reactions/:fid", + // @doc-tag: /reactionsByFid?fid=...&reaction_type=... + this.app.get<{ Querystring: { reaction_type: string; fid: string } & QueryPageParams }>( + "/v1/reactionsByFid", (request, reply) => { - const { fid } = request.params; + const { fid } = request.query; const pageOptions = getPageOptions(request.query); const call = getCallObject( "getReactionsByFid", { fid: parseInt(fid), - reactionType: getProtobufType(request.query.reactionType, reactionTypeFromJSON), + reactionType: getProtobufType(request.query.reaction_type, reactionTypeFromJSON), ...pageOptions, }, request, @@ -292,19 +298,18 @@ export class HttpAPIServer { }, ); - // /reactions/target/:target_fid/:target_hash?type=... + // @doc-tag: /reactionsByCast?target_fid=...&target_hash=...&reaction_type=... this.app.get<{ - Params: { target_fid: string; target_hash: string }; - Querystring: { reactionType: string } & QueryPageParams; - }>("/v1/reactions/target/:target_fid/:target_hash", (request, reply) => { - const { target_fid, target_hash } = request.params; + Querystring: { target_fid: string; target_hash: string; reaction_type: string } & QueryPageParams; + }>("/v1/reactionsByCast", (request, reply) => { + const { target_fid, target_hash } = request.query; const pageOptions = getPageOptions(request.query); const call = getCallObject( "getReactionsByCast", { targetCastId: { fid: parseInt(target_fid), hash: hexStringToBytes(target_hash).unwrapOr([]) }, - reactionType: getProtobufType(request.query.reactionType, reactionTypeFromJSON), + reactionType: getProtobufType(request.query.reaction_type, reactionTypeFromJSON), ...pageOptions, }, request, @@ -313,9 +318,9 @@ export class HttpAPIServer { this.grpcImpl.getReactionsByCast(call, handleResponse(reply, MessagesResponse)); }); - // /reactions/target?url=...&type=... - this.app.get<{ Querystring: { url: string; reactionType: string } & QueryPageParams }>( - "/v1/reactions/target", + // @doc-tag: /reactionsByTarget?url=...&reaction_type=... + this.app.get<{ Querystring: { url: string; reaction_type: string } & QueryPageParams }>( + "/v1/reactionsByTarget", (request, reply) => { const { url } = request.query; const pageOptions = getPageOptions(request.query); @@ -325,7 +330,7 @@ export class HttpAPIServer { "getReactionsByTarget", { targetUrl: decodedUrl, - reactionType: getProtobufType(request.query.reactionType, reactionTypeFromJSON), + reactionType: getProtobufType(request.query.reaction_type, reactionTypeFromJSON), ...pageOptions, }, request, @@ -336,18 +341,18 @@ export class HttpAPIServer { ); //=================Links================= - // /links/:fid/:target_fid?type=... - this.app.get<{ Params: { fid: string; target_fid: string }; Querystring: { type: string } }>( - "/v1/link/:fid/:target_fid", + // @doc-tag: /linkById?fid=...&target_fid=...&link_type=... + this.app.get<{ Querystring: { link_type: string; fid: string; target_fid: string } }>( + "/v1/linkById", (request, reply) => { - const { fid, target_fid } = request.params; + const { fid, target_fid } = request.query; const call = getCallObject( "getLink", { fid: parseInt(fid), targetFid: parseInt(target_fid), - linkType: request.query.type, + linkType: request.query.link_type, }, request, ); @@ -356,16 +361,16 @@ export class HttpAPIServer { }, ); - // /links/:fid?type=... - this.app.get<{ Params: { fid: string }; Querystring: { linkType: string } & QueryPageParams }>( - "/v1/links/:fid", + // @doc-tag: /linksByFid?fid=...&link_type=... + this.app.get<{ Querystring: { link_type: string; fid: string } & QueryPageParams }>( + "/v1/linksByFid", (request, reply) => { - const { fid } = request.params; + const { fid } = request.query; const pageOptions = getPageOptions(request.query); const call = getCallObject( "getLinksByFid", - { fid: parseInt(fid), linkType: request.query.linkType, ...pageOptions }, + { fid: parseInt(fid), linkType: request.query.link_type, ...pageOptions }, request, ); @@ -373,16 +378,16 @@ export class HttpAPIServer { }, ); - // /links/target/:target_fid?type=... - this.app.get<{ Params: { target_fid: string }; Querystring: { linkType: string } & QueryPageParams }>( - "/v1/links/target/:target_fid", + // @doc-tag: /linksByTargetFid?target_fid=...&link_type=... + this.app.get<{ Params: {}; Querystring: { link_type: string; target_fid: string } & QueryPageParams }>( + "/v1/linksByTargetFid", (request, reply) => { - const { target_fid } = request.params; + const { target_fid } = request.query; const pageOptions = getPageOptions(request.query); const call = getCallObject( "getLinksByTarget", - { targetFid: parseInt(target_fid), linkType: request.query.linkType, ...pageOptions }, + { targetFid: parseInt(target_fid), linkType: request.query.link_type, ...pageOptions }, request, ); @@ -391,13 +396,13 @@ export class HttpAPIServer { ); //==============User Data================ - // /userdata/:fid?type=... - this.app.get<{ Params: { fid: string }; Querystring: { type: string } & QueryPageParams }>( - "/v1/userdata/:fid", + // @doc-tag: /userDataByFid?fid=...&user_data_type=... + this.app.get<{ Querystring: { fid: string; user_data_type: string } & QueryPageParams }>( + "/v1/userDataByFid", (request, reply) => { - const { fid } = request.params; + const { fid } = request.query; const pageOptions = getPageOptions(request.query); - const userDataType = getProtobufType(request.query.type, userDataTypeFromJSON); + const userDataType = getProtobufType(request.query.user_data_type, userDataTypeFromJSON); if (userDataType) { const call = getCallObject("getUserData", { fid: parseInt(fid), userDataType, ...pageOptions }, request); @@ -410,18 +415,18 @@ export class HttpAPIServer { ); //=================Storage API================ - // /storagelimits/:fid - this.app.get<{ Params: { fid: string } }>("/v1/storagelimits/:fid", (request, reply) => { - const { fid } = request.params; + // @doc-tag: /storageLimitsByFid?fid=... + this.app.get<{ Querystring: { fid: string } }>("/v1/storageLimitsByFid", (request, reply) => { + const { fid } = request.query; const call = getCallObject("getCurrentStorageLimitsByFid", { fid: parseInt(fid) }, request); this.grpcImpl.getCurrentStorageLimitsByFid(call, handleResponse(reply, StorageLimitsResponse)); }); //===============Username Proofs================= - // /usernameproof/:name - this.app.get<{ Params: { name: string } }>("/v1/usernameproof/:name", (request, reply) => { - const { name } = request.params; + // @doc-tag: /userNameProofByName?name=... + this.app.get<{ Querystring: { name: string } }>("/v1/userNameProofByName", (request, reply) => { + const { name } = request.query; const fnameBytes = utf8StringToBytes(name).unwrapOr(new Uint8Array()); @@ -429,43 +434,38 @@ export class HttpAPIServer { this.grpcImpl.getUsernameProof(call, handleResponse(reply, UserNameProof)); }); - // /usernameproofs/:fid - this.app.get<{ Params: { fid: string } }>("/v1/usernameproofs/:fid", (request, reply) => { - const { fid } = request.params; + // @doc-tag: /usernameproofsByFid?fid=... + this.app.get<{ Querystring: { fid: string } }>("/v1/usernameproofsByFid", (request, reply) => { + const { fid } = request.query; const call = getCallObject("getUserNameProofsByFid", { fid: parseInt(fid) }, request); this.grpcImpl.getUserNameProofsByFid(call, handleResponse(reply, UsernameProofsResponse)); }); //=============Verifications================ - // /verifications/:fid?address=... - this.app.get<{ Params: { fid: string }; Querystring: { address: string } }>( - "/v1/verifications/:fid", - (request, reply) => { - const { fid } = request.params; - const { address } = request.query; + // @doc-tag: /verificationsByFid?fid=...&address=... + this.app.get<{ Querystring: { fid: string; address: string } }>("/v1/verificationsByFid", (request, reply) => { + const { fid, address } = request.query; - if (address) { - const call = getCallObject( - "getVerification", - { fid: parseInt(fid), address: hexStringToBytes(address).unwrapOr(new Uint8Array()) }, - request, - ); - this.grpcImpl.getVerification(call, handleResponse(reply, Message)); - } else { - const call = getCallObject("getVerificationsByFid", { fid: parseInt(fid) }, request); - this.grpcImpl.getVerificationsByFid(call, handleResponse(reply, MessagesResponse)); - } - }, - ); + if (address) { + const call = getCallObject( + "getVerification", + { fid: parseInt(fid), address: hexStringToBytes(address).unwrapOr(new Uint8Array()) }, + request, + ); + this.grpcImpl.getVerification(call, handleResponse(reply, Message)); + } else { + const call = getCallObject("getVerificationsByFid", { fid: parseInt(fid) }, request); + this.grpcImpl.getVerificationsByFid(call, handleResponse(reply, MessagesResponse)); + } + }); //================On Chain Events================ - // /onchain/signer/:fid?signer=... - this.app.get<{ Params: { fid: string }; Querystring: { signer: string } }>( - "/v1/onchain/signers/:fid", + // @doc-tag: /onChainSignersByFid?fid=...&signer=... + this.app.get<{ Querystring: { signer: string; fid: string } & QueryPageParams }>( + "/v1/onChainSignersByFid", (request, reply) => { - const { fid } = request.params; - const { signer } = request.query; + const { fid, signer } = request.query; if (signer) { const call = getCallObject( @@ -475,28 +475,29 @@ export class HttpAPIServer { ); this.grpcImpl.getOnChainSigner(call, handleResponse(reply, OnChainEvent)); } else { - const call = getCallObject("getOnChainSignersByFid", { fid: parseInt(fid) }, request); + const pageOptions = getPageOptions(request.query); + const call = getCallObject("getOnChainSignersByFid", { fid: parseInt(fid), ...pageOptions }, request); this.grpcImpl.getOnChainSignersByFid(call, handleResponse(reply, OnChainEventResponse)); } }, ); - // /onchain/events/:fid?type=... - this.app.get<{ Params: { fid: string }; Querystring: { type: string } & QueryPageParams }>( - "/v1/onchain/events/:fid", + // @doc-tag /onChainEventsByFid?fid=...&event_type=... + this.app.get<{ Querystring: { fid: string; event_type: string } & QueryPageParams }>( + "/v1/onChainEventsByFid", (request, reply) => { - const { fid } = request.params; + const { fid } = request.query; const pageOptions = getPageOptions(request.query); - const eventType = getProtobufType(request.query.type, onChainEventTypeFromJSON) ?? 0; + const eventType = getProtobufType(request.query.event_type, onChainEventTypeFromJSON) ?? 0; const call = getCallObject("getOnChainEvents", { fid: parseInt(fid), eventType, ...pageOptions }, request); this.grpcImpl.getOnChainEvents(call, handleResponse(reply, OnChainEventResponse)); }, ); - // /onchain/idregistryevent/address - this.app.get<{ Params: { address: string } }>("/v1/onchain/idregistryevent/:address", (request, reply) => { - const { address } = request.params; + // @doc-tag /onChainIdRegistryEventByAddress?address=... + this.app.get<{ Querystring: { address: string } }>("/v1/onChainIdRegistryEventByAddress", (request, reply) => { + const { address } = request.query; const call = getCallObject( "getIdRegistryOnChainEventByAddress", @@ -507,7 +508,7 @@ export class HttpAPIServer { }); //==================Submit Message================== - // POST /v1/submitMessage + // @doc-tag: /submitMessage this.app.post<{ Body: Buffer }>("/v1/submitMessage", (request, reply) => { // Get the Body content-type const contentType = request.headers["content-type"] as string; @@ -538,24 +539,26 @@ export class HttpAPIServer { return; } - const call = getCallObject("submitMessage", message, request); + // Grab and forward any authorization headers + const metadata = this.getMetadataFromAuthString(request?.headers?.authorization); + const call = getCallObject("submitMessage", message, request, metadata); this.grpcImpl.submitMessage(call, handleResponse(reply, Message)); }); //==================Events================== - // /event/:id - this.app.get<{ Params: { id: string } }>("/v1/event/:id", (request, reply) => { - const { id } = request.params; + // @doc-tag: /eventById?event_id=... + this.app.get<{ Querystring: { event_id: string } }>("/v1/eventById", (request, reply) => { + const { event_id: id } = request.query; const call = getCallObject("getEvent", { id: parseInt(id) }, request); this.grpcImpl.getEvent(call, handleResponse(reply, HubEvent)); }); - // /events?fromId=... - this.app.get<{ Querystring: { fromEventId: string } }>("/v1/events", (request, reply) => { - const { fromEventId } = request.query; + // @doc-tag /events?from_event_id=... + this.app.get<{ Querystring: { from_event_id: string } }>("/v1/events", (request, reply) => { + const { from_event_id } = request.query; - this.engine.getEvents(parseInt(fromEventId)).then((resp) => { + this.engine.getEvents(parseInt(from_event_id)).then((resp) => { if (resp.isErr()) { reply.code(400).send({ error: resp.error.message }); } else { diff --git a/apps/hubble/src/rpc/server.ts b/apps/hubble/src/rpc/server.ts index baee7fd5cb..f1352e7b68 100644 --- a/apps/hubble/src/rpc/server.ts +++ b/apps/hubble/src/rpc/server.ts @@ -37,6 +37,7 @@ import { OnChainEventResponse, SignerOnChainEvent, OnChainEvent, + HubResult, } from "@farcaster/hub-nodejs"; import { err, ok, Result } from "neverthrow"; import { APP_NICKNAME, APP_VERSION, HubInterface } from "../hubble.js"; @@ -68,7 +69,7 @@ export type RpcUsers = Map; const log = logger.child({ component: "rpcServer" }); // Check if the user is authenticated via the metadata -export const authenticateUser = async (metadata: Metadata, rpcUsers: RpcUsers): HubAsyncResult => { +export const authenticateUser = (metadata: Metadata, rpcUsers: RpcUsers): HubResult => { // If there is no auth user/pass, we don't need to authenticate if (rpcUsers.size === 0) { return ok(true); @@ -557,9 +558,9 @@ export default class Server { } // Authentication - const authResult = await authenticateUser(call.metadata, this.rpcUsers); + const authResult = authenticateUser(call.metadata, this.rpcUsers); if (authResult.isErr()) { - logger.warn({ errMsg: authResult.error.message }, "submitMessage failed"); + logger.warn({ errMsg: authResult.error.message }, "gRPC submitMessage failed"); callback( toServiceError(new HubError("unauthenticated", `gRPC authentication failed: ${authResult.error.message}`)), ); @@ -1111,7 +1112,7 @@ export default class Server { // regardless of rate limits. let authorized = false; if (this.rpcUsers.size > 0) { - authorized = (await authenticateUser(stream.metadata, this.rpcUsers)).unwrapOr(false); + authorized = authenticateUser(stream.metadata, this.rpcUsers).unwrapOr(false); } const allowed = this.subscribeIpLimiter.addConnection(peer); diff --git a/apps/hubble/src/rpc/test/bulkService.test.ts b/apps/hubble/src/rpc/test/bulkService.test.ts index b9b57aa7b6..444c0e11e7 100644 --- a/apps/hubble/src/rpc/test/bulkService.test.ts +++ b/apps/hubble/src/rpc/test/bulkService.test.ts @@ -28,17 +28,20 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/src/rpc/test/concurrency.test.ts b/apps/hubble/src/rpc/test/concurrency.test.ts index 4665e2a74f..a72cc0afb7 100644 --- a/apps/hubble/src/rpc/test/concurrency.test.ts +++ b/apps/hubble/src/rpc/test/concurrency.test.ts @@ -19,13 +19,15 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client1: HubRpcClient; let client2: HubRpcClient; let client3: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client1 = getInsecureHubRpcClient(`127.0.0.1:${port}`); client2 = getInsecureHubRpcClient(`127.0.0.1:${port}`); @@ -36,8 +38,10 @@ afterAll(async () => { client1.close(); client2.close(); client3.close(); + await server.stop(); await engine.stop(); + await syncEngine.stop(); }); const fid = Factories.Fid.build(); diff --git a/apps/hubble/src/rpc/test/httpServer.test.ts b/apps/hubble/src/rpc/test/httpServer.test.ts index 2836b461f5..be078a8954 100644 --- a/apps/hubble/src/rpc/test/httpServer.test.ts +++ b/apps/hubble/src/rpc/test/httpServer.test.ts @@ -38,6 +38,8 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network, undefined, publicClient); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; +let server: Server; let httpServer: HttpAPIServer; let httpServerAddress: string; @@ -46,14 +48,17 @@ function getFullUrl(path: string) { } beforeAll(async () => { - const server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); httpServer = new HttpAPIServer(server.getImpl(), engine); httpServerAddress = (await httpServer.start())._unsafeUnwrap(); }); afterAll(async () => { + await syncEngine.stop(); await httpServer.stop(); await engine.stop(); + await server.stop(); }); const fid = Factories.Fid.build(); @@ -110,6 +115,56 @@ describe("httpServer", () => { } expect(errored).toBeTruthy(); }); + + test("submit with auth", async () => { + const rpcAuth = "username:password"; + const authGrpcServer = new Server(hub, engine, syncEngine, undefined, rpcAuth); + const authServer = new HttpAPIServer(authGrpcServer.getImpl(), engine); + const addr = (await authServer.start())._unsafeUnwrap(); + + const postConfig = { + headers: { "Content-Type": "application/octet-stream" }, + auth: { username: "username", password: "password" }, + }; + + const url = `${addr}/v1/submitMessage`; + // Encode the message into a Buffer (of bytes) + const messageBytes = Buffer.from(Message.encode(castAdd).finish()); + + // Doesn't work if you don't pass auth + let errored = false; + try { + await axios.post(url, messageBytes, { headers: { "Content-Type": "application/octet-stream" } }); + } catch (e) { + errored = true; + // biome-ignore lint/suspicious/noExplicitAny: + const response = (e as any).response; + + expect(response.status).toBe(400); + expect(response.data.errCode).toEqual("unauthenticated"); + expect(response.data.details).toContain("Authorization header is empty"); + } + + // Doesn't work with a bad password + errored = false; + try { + await axios.post(url, messageBytes, { ...postConfig, auth: { username: "username", password: "badpassword" } }); + } catch (e) { + errored = true; + + // biome-ignore lint/suspicious/noExplicitAny: + const response = (e as any).response; + + expect(response.status).toBe(400); + expect(response.data.errCode).toEqual("unauthenticated"); + } + + // Right password works + const response = await axios.post(url, messageBytes, postConfig); + expect(response.status).toBe(200); + + await authServer.stop(); + }); }); describe("HubEvents APIs", () => { @@ -123,7 +178,7 @@ describe("httpServer", () => { expect((await engine.mergeMessage(castAdd)).isOk()).toBeTruthy(); // Get a http client for port 2181 - const url = getFullUrl("/v1/events?fromEventId=0"); + const url = getFullUrl("/v1/events?from_event_id=0"); const response = await axiosGet(url); expect(response.status).toBe(200); @@ -134,14 +189,14 @@ describe("httpServer", () => { const castAddEventId = response.data.events[3].id; // Get the castAdd event directly by ID - const url0 = getFullUrl(`/v1/event/${castAddEventId}`); + const url0 = getFullUrl(`/v1/eventById?event_id=${castAddEventId}`); const response0 = await axiosGet(url0); expect(response0.status).toBe(200); expect(response0.data.mergeMessageBody.message).toEqual(protoToJSON(castAdd, Message)); // Get the events starting after the signerAdd but before the castAdd - const url1 = getFullUrl(`/v1/events?fromEventId=${signerAddEventId + 1}`); + const url1 = getFullUrl(`/v1/events?from_event_id=${signerAddEventId + 1}`); const response1 = await axiosGet(url1); expect(response1.status).toBe(200); @@ -149,7 +204,7 @@ describe("httpServer", () => { expect(response1.data.events[0].mergeMessageBody.message).toEqual(protoToJSON(castAdd, Message)); // Now, get the events starting at the last eventID - const url2 = getFullUrl(`/v1/events?fromEventId=${castAddEventId}`); + const url2 = getFullUrl(`/v1/events?from_event_id=${castAddEventId}`); const response2 = await axiosGet(url2); expect(response2.status).toBe(200); @@ -157,7 +212,7 @@ describe("httpServer", () => { expect(response2.data.events[0].mergeMessageBody.message).toEqual(protoToJSON(castAdd, Message)); // Getthe events starting at the nextEventId should return nothing - const url3 = getFullUrl(`/v1/events?fromEventId=${response2.data.nextPageEventId}`); + const url3 = getFullUrl(`/v1/events?from_event_id=${response2.data.nextPageEventId}`); const response3 = await axiosGet(url3); expect(response3.status).toBe(200); @@ -185,7 +240,7 @@ describe("httpServer", () => { // Get a http client for port 2181 const hashHex = bytesToHexString(castAdd.hash)._unsafeUnwrap(); - const url = getFullUrl(`/v1/cast/${fid}/${hashHex}`); + const url = getFullUrl(`/v1/castById?fid=${fid}&hash=${hashHex}`); const response = await axiosGet(url); expect(response.status).toBe(200); @@ -200,21 +255,21 @@ describe("httpServer", () => { expect((await engine.mergeMessage(newCast)).isOk()).toBeTruthy(); // Get the new cast as a part of getAllCasts - const url2 = getFullUrl(`/v1/casts/${fid}`); + const url2 = getFullUrl(`/v1/castsByFid?fid=${fid}`); const response2 = await axiosGet(url2); expect(response2.status).toBe(200); expect(response2.data.messages).toEqual([protoToJSON(castAdd, Message), protoToJSON(newCast, Message)]); // Make sure paging works - const url4 = getFullUrl(`/v1/casts/${fid}?pageSize=1`); + const url4 = getFullUrl(`/v1/castsByFid?fid=${fid}&pageSize=1`); const response4 = await axiosGet(url4); expect(response4.status).toBe(200); expect(response4.data.messages).toEqual([protoToJSON(castAdd, Message)]); // get the next page - const url5 = getFullUrl(`/v1/casts/${fid}?pageToken=${response4.data.nextPageToken}`); + const url5 = getFullUrl(`/v1/castsByFid?fid=${fid}&pageToken=${response4.data.nextPageToken}`); const response5 = await axiosGet(url5); expect(response5.status).toBe(200); @@ -222,7 +277,7 @@ describe("httpServer", () => { expect(response5.data.nextPageToken).toBe(""); // Make sure reverse works - const url3 = getFullUrl(`/v1/casts/${fid}?reverse=true`); + const url3 = getFullUrl(`/v1/castsByFid?fid=${fid}&reverse=true`); const response3 = await axiosGet(url3); expect(response3.status).toBe(200); @@ -235,7 +290,7 @@ describe("httpServer", () => { // Get a http client for port 2181 const parentFid = castAdd.data.castAddBody.parentCastId?.fid; const hashHex = bytesToHexString(castAdd.data.castAddBody.parentCastId?.hash ?? new Uint8Array())._unsafeUnwrap(); - const url = getFullUrl(`/v1/casts/parent/${parentFid}/${hashHex}`); + const url = getFullUrl(`/v1/castsByParent?fid=${parentFid}&hash=${hashHex}`); const response = await axiosGet(url); expect(response.status).toBe(200); @@ -248,7 +303,7 @@ describe("httpServer", () => { expect((await engine.mergeMessage(castAdd2)).isOk()).toBeTruthy(); const encoded = encodeURIComponent(castAdd2.data.castAddBody.parentUrl ?? ""); - const url2 = getFullUrl(`/v1/casts/parent?url=${encoded}`); + const url2 = getFullUrl(`/v1/castsByParent?url=${encoded}`); const response2 = await axiosGet(url2); expect(response2.status).toBe(200); @@ -260,7 +315,7 @@ describe("httpServer", () => { // Get a http client for port 2181 for (let i = 0; i < castAdd.data.castAddBody.mentions.length; i++) { - const url = getFullUrl(`/v1/casts/mention/${castAdd.data.castAddBody.mentions[i]}`); + const url = getFullUrl(`/v1/castsByMention?fid=${castAdd.data.castAddBody.mentions[i]}`); const response = await axiosGet(url); expect(response.status).toBe(200); @@ -295,7 +350,9 @@ describe("httpServer", () => { // Get a http client for port 2181 const castHashHex = bytesToHexString(castAdd.hash)._unsafeUnwrap(); const url = getFullUrl( - `/v1/reaction/${fid}/${castAdd.data.fid}/${castHashHex}?reactionType=${reaction.data?.reactionBody?.type || 0}`, + `/v1/reactionById?fid=${fid}&target_fid=${castAdd.data.fid}&target_hash=${castHashHex}&reaction_type=${ + reaction.data?.reactionBody?.type || 0 + }`, ); const response = await axiosGet(url); @@ -304,9 +361,9 @@ describe("httpServer", () => { // Make sure it also works with the string reaction type const url2 = getFullUrl( - `/v1/reaction/${fid}/${castAdd.data.fid}/${castHashHex}?reactionType=${reactionTypeToJSON( - reaction.data?.reactionBody?.type || 0, - )}`, + `/v1/reactionById?fid=${fid}&target_fid=${ + castAdd.data.fid + }&target_hash=${castHashHex}&reaction_type=${reactionTypeToJSON(reaction.data?.reactionBody?.type || 0)}`, ); const response2 = await axiosGet(url2); @@ -314,7 +371,7 @@ describe("httpServer", () => { expect(response2.data).toEqual(protoToJSON(reaction, Message)); // Get the reaction by creator's fid - const url3 = getFullUrl(`/v1/reactions/${fid}?reactionType=${reaction.data?.reactionBody?.type || 0}`); + const url3 = getFullUrl(`/v1/reactionsByFid?fid=${fid}&reaction_type=${reaction.data?.reactionBody?.type || 0}`); const response3 = await axiosGet(url3); expect(response3.status).toBe(200); @@ -322,7 +379,7 @@ describe("httpServer", () => { // Get it by target cast const url4 = getFullUrl( - `/v1/reactions/target/${castAdd.data.fid}/${castHashHex}?reactionType=${ + `/v1/reactionsByCast?target_fid=${castAdd.data.fid}&target_hash=${castHashHex}&reaction_type=${ reaction.data?.reactionBody?.type || 0 }`, ); @@ -348,28 +405,28 @@ describe("httpServer", () => { // Get a http client for port 2181 const encoded = encodeURIComponent(targetUrl); - const url = getFullUrl(`/v1/reactions/target?url=${encoded}`); + const url = getFullUrl(`/v1/reactionsByTarget?url=${encoded}`); const response = await axiosGet(url); expect(response.status).toBe(200); expect(response.data.messages).toEqual([protoToJSON(reaction1, Message), protoToJSON(reaction2, Message)]); // Make sure paging works - const url4 = getFullUrl(`/v1/reactions/target?url=${encoded}&pageSize=1`); + const url4 = getFullUrl(`/v1/reactionsByTarget?url=${encoded}&pageSize=1`); const response4 = await axiosGet(url4); expect(response4.status).toBe(200); expect(response4.data.messages).toEqual([protoToJSON(reaction1, Message)]); // get the next page - const url5 = getFullUrl(`/v1/reactions/target?url=${encoded}&pageToken=${response4.data.nextPageToken}`); + const url5 = getFullUrl(`/v1/reactionsByTarget?url=${encoded}&pageToken=${response4.data.nextPageToken}`); const response5 = await axiosGet(url5); expect(response5.status).toBe(200); expect(response5.data.messages).toEqual([protoToJSON(reaction2, Message)]); // Make sure reverse works - const url3 = getFullUrl(`/v1/reactions/target?url=${encoded}&reverse=true`); + const url3 = getFullUrl(`/v1/reactionsByTarget?url=${encoded}&reverse=true`); const response3 = await axiosGet(url3); expect(response3.status).toBe(200); @@ -392,21 +449,23 @@ describe("httpServer", () => { expect((await engine.mergeMessage(linkAdd)).isOk()).toBeTruthy(); // Get a http client for port 2181 - const url = getFullUrl(`/v1/link/${fid}/${targetFid}?type=${linkAdd.data?.linkBody?.type}`); + const url = getFullUrl( + `/v1/linkById?fid=${fid}&target_fid=${targetFid}&link_type=${linkAdd.data?.linkBody?.type}`, + ); const response = await axiosGet(url); expect(response.status).toBe(200); expect(response.data).toEqual(protoToJSON(linkAdd, Message)); // Get it from the fid - const url1 = getFullUrl(`/v1/links/${fid}`); + const url1 = getFullUrl(`/v1/linksByFid?fid=${fid}`); const response1 = await axiosGet(url1); expect(response1.status).toBe(200); expect(response1.data.messages).toEqual([protoToJSON(linkAdd, Message)]); // Get it by target fid - const url2 = getFullUrl(`/v1/links/target/${targetFid}`); + const url2 = getFullUrl(`/v1/linksByTargetFid?target_fid=${targetFid}`); const response2 = await axiosGet(url2); expect(response2.status).toBe(200); @@ -434,21 +493,21 @@ describe("httpServer", () => { expect((await engine.mergeMessage(addBio)).isOk()).toBeTruthy(); // Get it all - const url = getFullUrl(`/v1/userdata/${fid}`); + const url = getFullUrl(`/v1/userDataByFid?fid=${fid}`); const response = await axiosGet(url); expect(response.status).toBe(200); expect(response.data.messages).toEqual([protoToJSON(addPfp, Message), protoToJSON(addBio, Message)]); // Get it by type (pfp) - const url2 = getFullUrl(`/v1/userdata/${fid}?type=${UserDataType.PFP}`); + const url2 = getFullUrl(`/v1/userDataByFid?fid=${fid}&user_data_type=${UserDataType.PFP}`); const response2 = await axiosGet(url2); expect(response2.status).toBe(200); expect(response2.data).toEqual(protoToJSON(addPfp, Message)); // Get it by type (bio) - const url3 = getFullUrl(`/v1/userdata/${fid}?type=${UserDataType.BIO}`); + const url3 = getFullUrl(`/v1/userDataByFid?fid=${fid}&user_data_type=${UserDataType.BIO}`); const response3 = await axiosGet(url3); expect(response3.status).toBe(200); @@ -458,7 +517,7 @@ describe("httpServer", () => { describe("Storage APIs", () => { test("getStorageLimits", async () => { - const url = getFullUrl(`/v1/storagelimits/${fid}`); + const url = getFullUrl(`/v1/storageLimitsByFid?fid=${fid}`); const response = await axiosGet(url); expect(response.status).toBe(200); @@ -498,20 +557,21 @@ describe("httpServer", () => { test("getUsernameProof", async () => { expect((await engine.mergeMessage(proof)).isOk()).toBeTruthy(); - const url = getFullUrl(`/v1/usernameproof/${fname}`); + const url = getFullUrl(`/v1/userNameProofByName?name=${fname}`); const response = await axiosGet(url); expect(response.status).toBe(200); - // biome-ignore lint/suspicious/noExplicitAny: - expect(response.data).toEqual((Message.toJSON(proof) as any).data.usernameProofBody); + + expect(response.data).toEqual((protoToJSON(proof, Message) as UsernameProofMessage).data.usernameProofBody); // Get via fid - const url2 = getFullUrl(`/v1/usernameproofs/${fid}`); + const url2 = getFullUrl(`/v1/usernameproofsByFid?fid=${fid}`); const response2 = await axiosGet(url2); expect(response2.status).toBe(200); - // biome-ignore lint/suspicious/noExplicitAny: - expect(response2.data.proofs).toEqual([(Message.toJSON(proof) as any).data.usernameProofBody]); + expect(response2.data.proofs).toEqual([ + (protoToJSON(proof, Message) as UsernameProofMessage).data.usernameProofBody, + ]); }); }); @@ -529,7 +589,7 @@ describe("httpServer", () => { expect((await engine.mergeMessage(verificationAdd)).isOk()).toBeTruthy(); const address = verificationAdd.data.verificationAddEthAddressBody.address; - const url = getFullUrl(`/v1/verifications/${fid}?address=${bytesToHexString(address)._unsafeUnwrap()}`); + const url = getFullUrl(`/v1/verificationsByFid?fid=${fid}&address=${bytesToHexString(address)._unsafeUnwrap()}`); const response = await axiosGet(url); expect(response.status).toBe(200); @@ -542,7 +602,7 @@ describe("httpServer", () => { ); // Get via fid - const url2 = getFullUrl(`/v1/verifications/${fid}`); + const url2 = getFullUrl(`/v1/verificationsByFid?fid=${fid}`); const response2 = await axiosGet(url2); expect(response2.status).toBe(200); @@ -560,28 +620,28 @@ describe("httpServer", () => { expect(await engine.mergeOnChainEvent(onChainEvent)).toBeTruthy(); - const url = getFullUrl(`/v1/onchain/signers/${fid}?signer=${signer}`); + const url = getFullUrl(`/v1/onChainSignersByFid?fid=${fid}&signer=${signer}`); const response = await axiosGet(url); expect(response.status).toBe(200); expect(response.data).toEqual(protoToJSON(onChainEvent, OnChainEvent)); // Get via fid - const url2 = getFullUrl(`/v1/onchain/signers/${fid}`); + const url2 = getFullUrl(`/v1/onChainSignersByFid?fid=${fid}`); const response2 = await axiosGet(url2); expect(response2.status).toBe(200); expect(response2.data.events).toEqual([protoToJSON(onChainEvent, OnChainEvent)]); // Get by type - const url3 = getFullUrl(`/v1/onchain/events/${fid}?type=${eventType}`); + const url3 = getFullUrl(`/v1/onChainEventsByFid?fid=${fid}&event_type=${eventType}`); const response3 = await axiosGet(url3); expect(response3.status).toBe(200); expect(response3.data.events).toEqual([protoToJSON(onChainEvent, OnChainEvent)]); // Get by type name - const url4 = getFullUrl(`/v1/onchain/events/${fid}?type=${onChainEventTypeToJSON(eventType)}`); + const url4 = getFullUrl(`/v1/onChainEventsByFid?fid=${fid}&event_type=${onChainEventTypeToJSON(eventType)}`); const response4 = await axiosGet(url4); expect(response4.status).toBe(200); @@ -591,7 +651,7 @@ describe("httpServer", () => { const idRegistryEvent = Factories.IdRegistryOnChainEvent.build({ fid }); expect(await engine.mergeOnChainEvent(idRegistryEvent)).toBeTruthy(); - const url5 = getFullUrl(`/v1/onchain/idregistryevent/${idRegistryEvent.idRegisterEventBody.to}`); + const url5 = getFullUrl(`/v1/onChainIdRegistryEventByAddress?address=${idRegistryEvent.idRegisterEventBody.to}`); const response5 = await axiosGet(url5); expect(response5.status).toBe(200); diff --git a/apps/hubble/src/rpc/test/linkService.test.ts b/apps/hubble/src/rpc/test/linkService.test.ts index 2c7f28e54a..0a4e71229a 100644 --- a/apps/hubble/src/rpc/test/linkService.test.ts +++ b/apps/hubble/src/rpc/test/linkService.test.ts @@ -23,17 +23,20 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/src/rpc/test/reactionService.test.ts b/apps/hubble/src/rpc/test/reactionService.test.ts index 97338a8e2e..2022030fb1 100644 --- a/apps/hubble/src/rpc/test/reactionService.test.ts +++ b/apps/hubble/src/rpc/test/reactionService.test.ts @@ -25,17 +25,20 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/src/rpc/test/rpcAuth.test.ts b/apps/hubble/src/rpc/test/rpcAuth.test.ts index d564b44f30..784d5fee08 100644 --- a/apps/hubble/src/rpc/test/rpcAuth.test.ts +++ b/apps/hubble/src/rpc/test/rpcAuth.test.ts @@ -43,7 +43,8 @@ afterAll(async () => { describe("auth tests", () => { test("fails with invalid password", async () => { - const authServer = new Server(hub, engine, new SyncEngine(hub, db), undefined, "admin:password"); + const syncEngine = new SyncEngine(hub, db); + const authServer = new Server(hub, engine, syncEngine, undefined, "admin:password"); const port = await authServer.start(); const authClient = getInsecureHubRpcClient(`127.0.0.1:${port}`); @@ -83,12 +84,15 @@ describe("auth tests", () => { const result5 = await authClient.getInfo(HubInfoRequest.create()); expect(result5.isOk()).toBeTruthy(); + await syncEngine.stop(); await authServer.stop(); + authClient.close(); }); test("all submit methods require auth", async () => { - const authServer = new Server(hub, engine, new SyncEngine(hub, db), undefined, "admin:password"); + const syncEngine = new SyncEngine(hub, db); + const authServer = new Server(hub, engine, syncEngine, undefined, "admin:password"); const port = await authServer.start(); const authClient = getInsecureHubRpcClient(`127.0.0.1:${port}`); @@ -109,6 +113,7 @@ describe("auth tests", () => { const result2 = await authClient.submitMessage(castAdd, metadata); expect(result2.isOk()).toBeTruthy(); + await syncEngine.stop(); await authServer.stop(); authClient.close(); }); diff --git a/apps/hubble/src/rpc/test/server.test.ts b/apps/hubble/src/rpc/test/server.test.ts index e58d0b11ca..2fb404b3f9 100644 --- a/apps/hubble/src/rpc/test/server.test.ts +++ b/apps/hubble/src/rpc/test/server.test.ts @@ -26,17 +26,20 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/src/rpc/test/signerService.test.ts b/apps/hubble/src/rpc/test/signerService.test.ts index b968c807a4..b443b2595c 100644 --- a/apps/hubble/src/rpc/test/signerService.test.ts +++ b/apps/hubble/src/rpc/test/signerService.test.ts @@ -26,17 +26,20 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/src/rpc/test/submitService.test.ts b/apps/hubble/src/rpc/test/submitService.test.ts index 9c737fe7eb..59348396ad 100644 --- a/apps/hubble/src/rpc/test/submitService.test.ts +++ b/apps/hubble/src/rpc/test/submitService.test.ts @@ -21,17 +21,20 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/src/rpc/test/syncService.test.ts b/apps/hubble/src/rpc/test/syncService.test.ts index fb38cbb3be..6b8bf6a2f6 100644 --- a/apps/hubble/src/rpc/test/syncService.test.ts +++ b/apps/hubble/src/rpc/test/syncService.test.ts @@ -23,17 +23,20 @@ const mockGossipNode = { const engine = new Engine(db, network); const hub = new MockHub(db, engine, mockGossipNode); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db), mockGossipNode); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine, mockGossipNode); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/src/rpc/test/userDataService.test.ts b/apps/hubble/src/rpc/test/userDataService.test.ts index 85dd8db008..d1f188494c 100644 --- a/apps/hubble/src/rpc/test/userDataService.test.ts +++ b/apps/hubble/src/rpc/test/userDataService.test.ts @@ -32,17 +32,20 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network, undefined, publicClient); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/src/rpc/test/verificationService.test.ts b/apps/hubble/src/rpc/test/verificationService.test.ts index 4d05265fea..5e6d7be22e 100644 --- a/apps/hubble/src/rpc/test/verificationService.test.ts +++ b/apps/hubble/src/rpc/test/verificationService.test.ts @@ -21,17 +21,20 @@ const network = FarcasterNetwork.TESTNET; const engine = new Engine(db, network); const hub = new MockHub(db, engine); +let syncEngine: SyncEngine; let server: Server; let client: HubRpcClient; beforeAll(async () => { - server = new Server(hub, engine, new SyncEngine(hub, db)); + syncEngine = new SyncEngine(hub, db); + server = new Server(hub, engine, syncEngine); const port = await server.start(); client = getInsecureHubRpcClient(`127.0.0.1:${port}`); }); afterAll(async () => { client.close(); + await syncEngine.stop(); await server.stop(); await engine.stop(); }); diff --git a/apps/hubble/www/docs/.vitepress/config.ts b/apps/hubble/www/docs/.vitepress/config.ts index 58b94b09d0..d8d8b75dc4 100644 --- a/apps/hubble/www/docs/.vitepress/config.ts +++ b/apps/hubble/www/docs/.vitepress/config.ts @@ -26,6 +26,7 @@ export default defineConfig({ items: [ { text: "CLI", link: "/docs/cli" }, { text: "APIs", link: "/docs/api" }, + { text: "HTTP APIs", link: "/docs/httpapi" }, { text: "Messages", link: "/docs/messages" }, { text: "OnChainEvents", link: "/docs/onchain_events" }, { text: "Events", link: "/docs/events" }, diff --git a/apps/hubble/www/docs/docs/cli.md b/apps/hubble/www/docs/docs/cli.md index 775dc257a9..b37c4b70cd 100644 --- a/apps/hubble/www/docs/docs/cli.md +++ b/apps/hubble/www/docs/docs/cli.md @@ -33,8 +33,6 @@ Hubble Options: --hub-operator-fid The FID of the hub operator. Optional. -c, --config Path to the config file. --db-name The name of the RocksDB instance. (default: rocks.hub._default) - --admin-server-enabled Enable the admin server. (default: disabled) - --admin-server-host The host the admin server should listen on. (default: '127.0.0.1') --process-file-prefix Prefix for file to which hub process number is written. (default: "") Ethereum Options: @@ -70,6 +68,9 @@ Networking Options: --ip IP address to listen on (default: "127.0.0.1") --announce-ip Public IP address announced to peers (default: fetched with external service) --announce-server-name Server name announced to peers, useful if SSL/TLS enabled. (default: "none") + --admin-server-enabled Enable the admin server. (default: disabled) + --admin-server-host The host the admin server should listen on. (default: '127.0.0.1') + --http-server-disabled Set this flag to disable the HTTP server (default: enabled) --direct-peers A list of peers for libp2p to directly peer with (default: []) --denied-peers Do not peer with specific peer ids. (default: no peers denied) --rpc-rate-limit RPC rate limit for peers specified in rpm. Set to -1 for none. (default: 20k/min) diff --git a/apps/hubble/www/docs/docs/httpapi.md b/apps/hubble/www/docs/docs/httpapi.md new file mode 100644 index 0000000000..c5b34a4582 --- /dev/null +++ b/apps/hubble/www/docs/docs/httpapi.md @@ -0,0 +1,1109 @@ +# HTTP API +Hubble serves a HTTP API on port 2281 by default. This API is current in beta, and can change anytime. + +## Using the API +The API can be called from any programing language or browser by making a normal HTTP request. + +**View the API responses in a browser** + +Simply open the URL in a browser +```url +http://127.0.0.1:2281/v1/castsByFid?fid=2 +``` + +**Call the API using curl** +```bash +curl http://127.0.0.1:2281/v1/castsByFid?fid=2 +``` + +**Call the API via Javascript, using the axios library** +```Javascript +import axios from "axios"; + +const fid = 2; +const server = "http://127.0.0.1:2281"; + +try { + const response = await axios.get(`${server}/v1/castsByFid?fid=${fid}`); + + console.log(`API Returned HTTP status ${response.status}`); + console.log(`First Cast's text is ${response.messages[0].data.castAddBody.text}`); +} catch (e) { + // Handle errors + console.log(response); +} +``` + +### Response encoding +Responses from the API are encoded as `application/json`, and can be parsed as normal JSON objects. + +1. Hashes, ETH addresses, signers etc... are all encoded as hex strings starting with `0x` +2. Signatures and other binary fields are encoded in base64 +3. Constants are encoded as their string types. For example, the `hashScheme` is encoded as `HASH_SCHEME_BLAKE3` which is equivalent to the `HASH_SCHEME_BLAKE3 = 1` from the protobuf schema. + +### Timestamps +Messages contain a timestamp, which is the _Farcaster Epoch Timestamp_ (and not the Unix Epoch). + +### Paging +Most endpoints support paging to get a large number of responses. + +**Pagination Query Parameters** + +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| pageSize | Maximum number of messages to return in a single response | `pageSize=100` | +| reverse | Reverse the sort order, returning latest messages first | `reverse=1` | +| pageToken | The page token returned by the previous query, to fetch the next page. If this parameters is empty, fetch the first page | `pageToken=AuzO1V0Dta...fStlynsGWT` | + +The returned `nextPageToken` is empty if there are no more pages to return. + +Pagination query parameters can be combined with other query parameters supported by the endpoint. For example, `/v1/casts?fid=2&pageSize=3`. + +**Example** + +Fetch all casts by FID `2`, fetching upto 3 casts per Page + +```bash +# Fetch first page +http://127.0.0.1:2281/v1/castsByFid?fid=2&pageSize=3 + +# Fetch next page. The pageToken is from the previous response(`response.nextPageToken`) +http://127.0.0.1:2281/v1/castsByFid?fid=2&pageSize=3&pageToken=AuzO1V0DtaItCwwa10X6YsfStlynsGWT +``` + +**Javascript Example** +```Javascript +import axios from "axios"; + +const fid = 2; +const server = "http://127.0.0.1:2281"; + +let nextPageToken = ""; +do { + const response = await axios.get(`${server}/v1/castsByFid?fid=${fid}&pageSize=100&nextPageToken=${nextPageToken}`); + // Process response.... + nextPageToken = response.nextPageToken; +} while (nextPageToken !== "") +``` + +### Handling Errors +If there's an API error, the HTTP status code is set to `400` or `500` as appropriate. The response is a JSON object with `detail`, `errCode` and `metadata` fields set to identify and debug the errors. + +**Example** +```bash +$ curl "http://127.0.0.1:2281/v1/castById?fid=invalid" +{ + "errCode": "bad_request.validation_failure", + "presentable": false, + "name": "HubError", + "code": 3, + "details": "fid must be an integer", + "metadata": { + "errcode": [ + "bad_request.validation_failure", + ], + }, +} +``` + +## Casts API + +### castById +Get a cast by its FID and Hash. + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID of the cast's creator | `fid=6833` | +| hash | The cast's hash | `hash=0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/castById?id=2&hash=0xd2b1ddc6c88e865a33cb1a565e0058d757042974 +``` + + +**Response** +```json +{ + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 2, + "timestamp": 48994466, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [], + "parentCastId": { + "fid": 226, + "hash": "0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9" + }, + "text": "Cast Text", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0xd2b1ddc6c88e865a33cb1a565e0058d757042974", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "3msLXzxB4eEYe...dHrY1vkxcPAA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a...58c" +} +``` + +### castsByFid +Fetch all casts for authored by an FID. + + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID of the cast's creator | `fid=6833` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/castsByFid?fid=2 +``` + +**Response** + +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 2, + "timestamp": 48994466, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": {... }, + "text": "Cast Text", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0xd2b1ddc6c88e865a33cb1a565e0058d757042974", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "3msLXzxB4eEYeF0Le...dHrY1vkxcPAA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a768cf1...2eca647b6d62558c" + } + ] + "nextPageToken": "" +} +``` + +### castsByParent +Fetch all casts by parent cast's FID and Hash OR by the parent's URL + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID of the parent cast | `fid=6833` | +| hash | The parent cast's hash | `hash=0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9` | +| url | The URL of the parent cast | `url=chain://eip155:1/erc721:0x39d89b649ffa044383333d297e325d42d31329b2` | + +**Note** +You can use either `?fid=...&hash=...` OR `?url=...` to query this endpoint + +**Example** +```bash +curl http://127.0.0.1:2281/v1/castsByParent?fid=226&hash=0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9 +``` + + +**Response** +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 226, + "timestamp": 48989255, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [], + "parentCastId": { + "fid": 226, + "hash": "0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9" + }, + "text": "Cast's Text", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0x0e501b359f88dcbcddac50a8f189260a9d02ad34", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "MjKnOQCTW42K8+A...tRbJfia2JJBg==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x6f1e8758...7f04a3b500ba" + }, + ], + "nextPageToken": "" +} +``` + + +### castsByMention +Fetch all casts that mention an FID + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID that is mentioned in a cast | `fid=6833` | + +**Note** +Use the `mentionsPositions` to extract the offset in the cast text where the FID was mentioned + +**Example** +```bash +curl http://127.0.0.1:2281/v1/castsByMention?fid=6833 +``` + + +**Response** +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 2, + "timestamp": 62298143, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [15, 6833], + "parentCastId": { + "fid": 2, + "hash": "0xd5540928cd3daf2758e501a61663427e41dcc09a" + }, + "text": "cc and ", + "mentionsPositions": [3, 8], + "embeds": [] + } + }, + "hash": "0xc6d4607835197a8ee225e9218d41e38aafb12076", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "TOaWrSTmz+cyzPMFGvF...OeUznB0Ag==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a768c...647b6d62558c" + }, + ], + "nextPageToken": "" +} +``` + +## Reactions API + +The Reactions API will accept the following values (either the string representation or the numerical value) for the `reaction_type` field. + +| String | Numerical value | Description | +| ------ | --------------- | ----------- | +| REACTION_TYPE_LIKE | 1 | Like the target cast | +| REACTION_TYPE_RECAST | 2 | Share target cast to the user's audience | + +### reactionById +Get a reaction by its created FID and target Cast. + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID of the reaction's creator | `fid=6833` | +| target_fid | The FID of the cast's creator | `target_fid=2` | +| target_hash | The cast's hash | `target_hash=0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9` | +| reaction_type | The type of reaction, either as a numerical enum value or string representation | `reaction_type=1` OR `reaction_type=REACTION_TYPE_LIKE` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/reactionById?fid=2&reaction_type=1&target_fid=1795&target_hash=0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0 +``` + + +**Response** +```json +{ + "data": { + "type": "MESSAGE_TYPE_REACTION_ADD", + "fid": 2, + "timestamp": 72752656, + "network": "FARCASTER_NETWORK_MAINNET", + "reactionBody": { + "type": "REACTION_TYPE_LIKE", + "targetCastId": { + "fid": 1795, + "hash": "0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0" + } + } + }, + "hash": "0x9fc9c51f6ea3acb84184efa88ba4f02e7d161766", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "F2OzKsn6Wj...gtyORbyCQ==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a7...647b6d62558c" +} +``` + + +### reactionsByFid +Get all reactions by an FID + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID of the reaction's creator | `fid=6833` | +| reaction_type | The type of reaction, either as a numerical enum value or string representation | `reaction_type=1` OR `reaction_type=REACTION_TYPE_LIKE` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/reactionsByFid?fid=2&reaction_type=1 +``` + + +**Response** +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_REACTION_ADD", + "fid": 2, + "timestamp": 72752656, + "network": "FARCASTER_NETWORK_MAINNET", + "reactionBody": { + "type": "REACTION_TYPE_LIKE", + "targetCastId": { + "fid": 1795, + "hash": "0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0" + } + } + }, + "hash": "0x9fc9c51f6ea3acb84184efa88ba4f02e7d161766", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "F2OzKsn6WjP8MTw...hqUbrAvp6mggtyORbyCQ==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a768...62558c" + }, + ], + "nextPageToken": "" +} +``` + + +### reactionsByCast +Get all reactions to a cast + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| target_fid | The FID of the cast's creator | `fid=6833` | +| target_hash | The hash of the cast | `target_hash=`0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0` | +| reaction_type | The type of reaction, either as a numerical enum value or string representation | `reaction_type=1` OR `reaction_type=REACTION_TYPE_LIKE` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/reactionsByCast?fid=2&reaction_type=1 +``` + + +**Response** +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_REACTION_ADD", + "fid": 426, + "timestamp": 72750141, + "network": "FARCASTER_NETWORK_MAINNET", + "reactionBody": { + "type": "REACTION_TYPE_LIKE", + "targetCastId": { + "fid": 1795, + "hash": "0x7363f449bfb0e7f01c5a1cc0054768ed5146abc0" + } + } + }, + "hash": "0x7662fba1be3166fc75acc0914a7b0e53468d5e7a", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "tmAUEYlt/+...R7IO3CA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x13dd2...204e57bc2a" + }, + ], + "nextPageToken": "" +} +``` + + +### reactionsByTarget +Get all reactions to cast's target URL + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| url | The URL of the parent cast | url=chain://eip155:1/erc721:0x39d89b649ffa044383333d297e325d42d31329b2 | +| reaction_type | The type of reaction, either as a numerical enum value or string representation | `reaction_type=1` OR `reaction_type=REACTION_TYPE_LIKE` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/reactionsByTarget?url=chain://eip155:1/erc721:0x39d89b649ffa044383333d297e325d42d31329b2 +``` + + +**Response** +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_REACTION_ADD", + "fid": 1134, + "timestamp": 79752856, + "network": "FARCASTER_NETWORK_MAINNET", + "reactionBody": { + "type": "REACTION_TYPE_LIKE", + "targetUrl": "chain://eip155:1/erc721:0x39d89b649ffa044383333d297e325d42d31329b2" + } + }, + "hash": "0x94a0309cf11a07b95ace71c62837a8e61f17adfd", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "+f/+M...0Uqzd0Ag==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0xf6...3769198d4c" + }, + ], + "nextPageToken": "" +} +``` + + + +## Links API + +The Links API will accept the following values for the `link_type` field. + +| String | Description | +| ------ | ----------- | +| follow | Follow from FID to Target FID | + +### linkById +Get a link by its FID and target FID. + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID of the link's originator | `fid=6833` | +| target_fid | The FID of the target of the link | `target_fid=2` | +| link_type | The type of link, as a string value| `link_type=follow` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/linkById?fid=6833&target_fid=2&link_type=follow +``` + + +**Response** +```json +{ + "data": { + "type": "MESSAGE_TYPE_LINK_ADD", + "fid": 6833, + "timestamp": 61144470, + "network": "FARCASTER_NETWORK_MAINNET", + "linkBody": { + "type": "follow", + "targetFid": 2 + } + }, + "hash": "0x58c23eaf4f6e597bf3af44303a041afe9732971b", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "sMypYEMqSyY...nfCA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x0852c07b56...06e999cdd" +} +``` + + +### linksByFid +Get all links from a source FID + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID of the reaction's creator | `fid=6833` | +| link_type | The type of link, as a string value| `link_type=follow` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/linksByFid?fid=6833 +``` + + +**Response** +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_LINK_ADD", + "fid": 6833, + "timestamp": 61144470, + "network": "FARCASTER_NETWORK_MAINNET", + "linkBody": { + "type": "follow", + "targetFid": 83 + } + }, + "hash": "0x094e35891519c0e04791a6ba4d2eb63d17462f02", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "qYsfX08mS...McYq6IYMl+ECw==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x0852c0...a06e999cdd" + }, + ], + "nextPageToken": "" +} +``` + +### linksByTargetFid +Get all links to a target FID + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| target_fid | The FID of the reaction's creator | `fid=6833` | +| link_type | The type of link, as a string value| `link_type=follow` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/linksByTargetFid?target_fid=6833 +``` + + +**Response** +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_LINK_ADD", + "fid": 302, + "timestamp": 61144668, + "network": "FARCASTER_NETWORK_MAINNET", + "linkBody": { + "type": "follow", + "targetFid": 6833 + } + }, + "hash": "0x78c62531d96088f640ffe7e62088b49749efe286", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "frIZJGIizv...qQd9QJyCg==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x59a04...6860ddfab" + }, + ], + "nextPageToken": "" +} +``` + + + +## UserData API + +The UserData API will accept the following values for the `user_data_type` field. + +| String | Numerical value | Description | +| ------ | --------------- | ----------- | +| USER_DATA_TYPE_PFP | 1 | Profile Picture for the user | +| USER_DATA_TYPE_DISPLAY | 2 | Display Name for the user | +| USER_DATA_TYPE_BIO | 3 | Bio for the user | +| USER_DATA_TYPE_URL | 5 | URL of the user | +| USER_DATA_TYPE_USERNAME | 6 | Preferred Name for the user | + + +### userDataByFid +Get UserData for a FID. + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID that's being requested | `fid=6833` | +| user_data_type | The type of user data, either as a numerical value or type string. If this is ommited, all user data for the FID is returned| `user_data_type=1` OR `user_data_type=USER_DATA_TYPE_DISPLAY` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/userDataByFid?fid=6833&user_data_type=1 +``` + + +**Response** +```json +{ + "data": { + "type": "MESSAGE_TYPE_USER_DATA_ADD", + "fid": 6833, + "timestamp": 83433831, + "network": "FARCASTER_NETWORK_MAINNET", + "userDataBody": { + "type": "USER_DATA_TYPE_PFP", + "value": "https://i.imgur.com/HG54Hq6.png" + } + }, + "hash": "0x327b8f47218c369ae01cc453cc23efc79f10181f", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "XITQZD7q...LdAlJ9Cg==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x0852...6e999cdd" +} +``` + +## Storage Limits API + + +### storageLimitsByFid +Get an FID's storage limits. + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID that's being requested | `fid=6833` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/storageLimitsByFid?fid=6833 +``` + + +**Response** +```json +{ + "limits": [ + { + "storeType": "STORE_TYPE_CASTS", + "limit": 10000 + }, + { + "storeType": "STORE_TYPE_LINKS", + "limit": 5000 + }, + { + "storeType": "STORE_TYPE_REACTIONS", + "limit": 5000 + }, + { + "storeType": "STORE_TYPE_USER_DATA", + "limit": 100 + }, + { + "storeType": "STORE_TYPE_USERNAME_PROOFS", + "limit": 10 + }, + { + "storeType": "STORE_TYPE_VERIFICATIONS", + "limit": 50 + } + ] +} +``` + + + +## Username Proofs API + + +### userNameProofByName +Get an proof for a username by the Farcaster username + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| name | The Farcaster username or ENS address | `name=adityapk` OR `name=dwr.eth` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/userNameProofByName?name=adityapk +``` + + +**Response** +```json +{ + "timestamp": 1670603245, + "name": "adityapk", + "owner": "Oi7uUaECifDm+larm+rzl3qQhcM=", + "signature": "fo5OhBP/ud...3IoJdhs=", + "fid": 6833, + "type": "USERNAME_TYPE_FNAME" +} +``` + + +### usernameproofsByFid +Get a list of proofs provided by an FID + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID being requested | `fid=2` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/usernameproofsByFid?fid=2 +``` + + +**Response** +```json +{ + "proofs": [ + { + "timestamp": 1623910393, + "name": "v", + "owner": "0x4114e33eb831858649ea3702e1c9a2db3f626446", + "signature": "bANBae+Ub...kr3Bik4xs=", + "fid": 2, + "type": "USERNAME_TYPE_FNAME" + }, + { + "timestamp": 1690329118, + "name": "varunsrin.eth", + "owner": "0x182327170fc284caaa5b1bc3e3878233f529d741", + "signature": "zCEszPt...zqxTiFqVBs=", + "fid": 2, + "type": "USERNAME_TYPE_ENS_L1" + } + ] +} +``` + + +## Verifications API + + +### verificationsByFid +Get a list of verifications provided by an FID + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID being requested | `fid=2` | +| address | The optional ETH address to filter by | `address=0x91031dcfdea024b4d51e775486111d2b2a715871` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/verificationsByFid?fid=2 +``` + + +**Response** +```json +{ + "messages": [ + { + "data": { + "type": "MESSAGE_TYPE_VERIFICATION_ADD_ETH_ADDRESS", + "fid": 2, + "timestamp": 73244540, + "network": "FARCASTER_NETWORK_MAINNET", + "verificationAddEthAddressBody": { + "address": "0x91031dcfdea024b4d51e775486111d2b2a715871", + "ethSignature": "tyxj1...x1cYzhyxw=", + "blockHash": "0xd74860c4bbf574d5ad60f03a478a30f990e05ac723e138a5c860cdb3095f4296" + } + }, + "hash": "0xa505331746ec8c5110a94bdb098cd964e43a8f2b", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "bln1zIZM.../4riB9IVBQ==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9...b6d62558c" + }, + ], + "nextPageToken": "" +} +``` + + +## On Chain API + + +### onChainSignersByFid +Get a list of signers provided by an FID + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID being requested | `fid=2` | +| signer | The optional key of signer | `signer=0x0852c07b5695ff94138b025e3f9b4788e06133f04e254f0ea0eb85a06e999cdd` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/onChainSignersByFid?fid=6833 +``` + + +**Response** +```json +{ + "events": [ + { + "type": "EVENT_TYPE_SIGNER", + "chainId": 10, + "blockNumber": 108875854, + "blockHash": "0xceb1cdc21ee319b06f0455f1cedc0cd4669b471d283a5b2550b65aba0e0c1af0", + "blockTimestamp": 1693350485, + "transactionHash": "0x76e20cf2f7c3db4b78f00f6bb9a7b78b0acfb1eca4348c1f4b5819da66eb2bee", + "logIndex": 2, + "fid": 6833, + "signerEventBody": { + "key": "0x0852c07b5695ff94138b025e3f9b4788e06133f04e254f0ea0eb85a06e999cdd", + "keyType": 1, + "eventType": "SIGNER_EVENT_TYPE_ADD", + "metadata": "AAAAAAAAAAAA...AAAAAAAA", + "metadataType": 1 + }, + "txIndex": 0 + } + ] +} +``` + + + +### onChainEventsByFid +Get a list of signers provided by an FID + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| fid | The FID being requested | `fid=2` | +| event_type | The numeric of string value of the event type being requested. This parameter is required | `event_type=1` OR `event_type=EVENT_TYPE_STORAGE_RENT` | + + +The onChainEventsByFid API will accept the following values for the `event_type` field. + +| String | Numerical value | +| ------ | --------------- | +| EVENT_TYPE_SIGNER | 1 | +| EVENT_TYPE_SIGNER_MIGRATED | 2 | +| EVENT_TYPE_ID_REGISTER | 3 | +| EVENT_TYPE_STORAGE_RENT | 4 | + +**Example** +```bash +curl http://127.0.0.1:2281/v1/onChainEventsByFid?fid=3&event_type=1 +``` + + +**Response** +```json +{ + "events": [ + { + "type": "EVENT_TYPE_SIGNER", + "chainId": 10, + "blockNumber": 108875456, + "blockHash": "0x75fbbb8b2a4ede67ac350e1b0503c6a152c0091bd8e3ef4a6927d58e088eae28", + "blockTimestamp": 1693349689, + "transactionHash": "0x36ef79e6c460e6ae251908be13116ff0065960adb1ae032b4cc65a8352f28952", + "logIndex": 2, + "fid": 3, + "signerEventBody": { + "key": "0xc887f5bf385a4718eaee166481f1832198938cf33e98a82dc81a0b4b81ffe33d", + "keyType": 1, + "eventType": "SIGNER_EVENT_TYPE_ADD", + "metadata": "AAAAAAAAA...AAAAA", + "metadataType": 1 + }, + "txIndex": 0 + }, + ] +} +``` + + +### onChainIdRegistryEventByAddress +Get a list of on chain events for a given Address + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| address | The ETH address being requested | `address=0x74232bf61e994655592747e20bdf6fa9b9476f79` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/onChainIdRegistryEventByAddress?address=0x74232bf61e994655592747e20bdf6fa9b9476f79 +``` + + +**Response** +```json +{ + "type": "EVENT_TYPE_ID_REGISTER", + "chainId": 10, + "blockNumber": 108874508, + "blockHash": "0x20d83804a26247ad8c26d672f2212b28268d145b8c1cefaa4126f7768f46682e", + "blockTimestamp": 1693347793, + "transactionHash": "0xf3481fc32227fbd982b5f30a87be32a2de1fc5736293cae7c3f169da48c3e764", + "logIndex": 7, + "fid": 3, + "idRegisterEventBody": { + "to": "0x74232bf61e994655592747e20bdf6fa9b9476f79", + "eventType": "ID_REGISTER_EVENT_TYPE_REGISTER", + "from": "0x", + "recoveryAddress": "0x00000000fcd5a8e45785c8a4b9a718c9348e4f18" + }, + "txIndex": 0 +} +``` + + + +## SubmitMessage API +The SubmitMessage API lets you submit signed Farcaster protocol messages to the Hub. Note that the message has to be sent as the encoded bytestream of the protobuf (`Message.enocde(msg).finish()` in typescript), as POST data to the endpoint. + +The encoding of the POST data has to be set to `application/octet-stream`. The endpoint returns the Message object as JSON if it was successfully submitted + +### submitMessage +Submit a signed protobuf-serialized message to the Hub + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| | This endpoint accepts no parameters | | + + +**Example** +```bash +curl -X POST "http://127.0.0.1:2281/v1/submitMessage" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@message.encoded.protobuf" + +``` + + +**Response** +```json +{ + "data": { + "type": "MESSAGE_TYPE_CAST_ADD", + "fid": 2, + "timestamp": 48994466, + "network": "FARCASTER_NETWORK_MAINNET", + "castAddBody": { + "embedsDeprecated": [], + "mentions": [], + "parentCastId": { + "fid": 226, + "hash": "0xa48dd46161d8e57725f5e26e34ec19c13ff7f3b9" + }, + "text": "Cast Text", + "mentionsPositions": [], + "embeds": [] + } + }, + "hash": "0xd2b1ddc6c88e865a33cb1a565e0058d757042974", + "hashScheme": "HASH_SCHEME_BLAKE3", + "signature": "3msLXzxB4eEYe...dHrY1vkxcPAA==", + "signatureScheme": "SIGNATURE_SCHEME_ED25519", + "signer": "0x78ff9a...58c" +} +``` + +## Events API +The events API returns events as they are merged into the Hub, which can be used to listen to Hub activity. + +### eventById +Get an event by its Id + + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| event_id | The Hub Id of the event | `event_id=350909155450880` | + + +**Example** +```bash +curl http://127.0.0.1:2281/v1/eventById?id=350909155450880 + +``` + + +**Response** +```json +{ + "type": "HUB_EVENT_TYPE_MERGE_USERNAME_PROOF", + "id": 350909155450880, + "mergeUsernameProofBody": { + "usernameProof": { + "timestamp": 1695049760, + "name": "nftonyp", + "owner": "0x23b3c29900762a70def5dc8890e09dc9019eb553", + "signature": "xp41PgeO...hJpNshw=", + "fid": 20114, + "type": "USERNAME_TYPE_FNAME" + } + } +} +``` + +### events +Get a page of Hub events + +**Query Parameters** +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| from_event_id | An optional Hub Id to start getting events from. This is also returned from the API as `nextPageEventId`, which can be used to page through all the Hub events. Set it to `0` to start from the first event | `from_event_id=350909155450880` | + +**Note** +Hubs prune events older than 3 days, so not all historical events can be fetched via this API + +**Example** +```bash +curl http://127.0.0.1:2281/v1/events?from_event_id=350909155450880 + +``` + + +**Response** +```json +{ + "nextPageEventId": 350909170294785, + "events": [ + { + "type": "HUB_EVENT_TYPE_MERGE_USERNAME_PROOF", + "id": 350909155450880, + "mergeUsernameProofBody": { + "usernameProof": { + "timestamp": 1695049760, + "name": "nftonyp", + "owner": "0x23b3c29900762a70def5dc8890e09dc9019eb553", + "signature": "xp41PgeOz...9Jw5vT/eLnGphJpNshw=", + "fid": 20114, + "type": "USERNAME_TYPE_FNAME" + } + } + }, + ... + ] +} +``` \ No newline at end of file diff --git a/packages/hub-nodejs/docs/README.md b/packages/hub-nodejs/docs/README.md index 264c9209fe..1abd002c60 100644 --- a/packages/hub-nodejs/docs/README.md +++ b/packages/hub-nodejs/docs/README.md @@ -2,7 +2,8 @@ @farcaster/hub-nodejs has five major components: -- A [Client](./Client.md), which can send and receive messages from a Farcaster Hub. +- A grpc [Client](./Client.md), which can send and receive messages from a Farcaster Hub. + - Messages from the Hub can also be read and written using the [HTTP API](https://www.thehubble.xyz/docs/httpapi.html) - [Messages](./Messages.md), which are the atomic units of change on the Farcaster network. - [Builders](./Builders.md), which can be used to construct new messages. - [Signers](./signers/), which are required by Builders to sign messages. diff --git a/yarn.lock b/yarn.lock index 771d553c39..6b1c64f54e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3680,6 +3680,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.0.0": + version "4.1.8" + resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" + integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ== + dependencies: + "@types/ms" "*" + "@types/ffi-napi@^4.0.7": version "4.0.7" resolved "https://registry.npmjs.org/@types/ffi-napi/-/ffi-napi-4.0.7.tgz#b3a9beeae160c74adca801ca1c9defb1ec0a1a32" @@ -3752,11 +3759,23 @@ resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/mdast@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.0.tgz#9f9462d4584a8b3e3711ea8bb4a94c485559ab90" + integrity sha512-YLeG8CujC9adtj/kuDzq1N4tCDYKoZ5l/bnjq8d74+t/3q/tHquJOJKUQXJrLCflOHpKjXgcI/a929gpmLOEng== + dependencies: + "@types/unist" "*" + "@types/minimist@^1.2.0": version "1.2.2" resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/ms@*": + version "0.7.31" + resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/node-cron@^3.0.7": version "3.0.7" resolved "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.7.tgz#978bf75f7247385c61d23b6a060ba9eedb03e2f4" @@ -3848,6 +3867,11 @@ "@types/node" "*" minipass "^4.0.0" +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz#988ae8af1e5239e89f9fbb1ade4c935f4eeedf9a" + integrity sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -4329,6 +4353,11 @@ babel-preset-jest@^29.5.0: babel-plugin-jest-hoist "^29.5.0" babel-preset-current-node-syntax "^1.0.0" +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -4594,6 +4623,11 @@ catering@^2.0.0, catering@^2.1.0: resolved "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chalk@5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" @@ -4626,6 +4660,11 @@ char-regex@^1.0.2: resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -4947,6 +4986,13 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -5016,6 +5062,11 @@ depd@^2.0.0: resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-indent@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" @@ -5031,6 +5082,13 @@ detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + diff-sequences@^29.4.3: version "29.4.3" resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" @@ -5699,6 +5757,11 @@ ext-name@^5.0.0: ext-list "^2.0.0" sort-keys-length "^1.0.0" +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + extendable-error@^0.1.5: version "0.1.7" resolved "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz#60b9adf206264ac920058a7395685ae4670c2b96" @@ -6712,6 +6775,11 @@ is-plain-obj@^2.1.0: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -7824,6 +7892,11 @@ longbits@^1.1.0: byte-access "^1.0.1" uint8arraylist "^2.0.0" +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" @@ -7916,6 +7989,133 @@ map-obj@^4.0.0: resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +markdown-table@^3.0.0: + version "3.0.3" + resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" + integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== + +mdast-util-find-and-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.0.tgz#ca0a9232afdc53f4260582ac2d0227cf94b4a220" + integrity sha512-8wLPIKAvGdA5jgkI8AYKfSorV3og3vE6HA+gKeKEZydbi1EtUu2g4XCxIBj3R+AsFqY/uRtoYbH30tiWsFKkBQ== + dependencies: + "@types/mdast" "^4.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +mdast-util-from-markdown@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88" + integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-gfm-autolink-literal@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a" + integrity sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg== + dependencies: + "@types/mdast" "^4.0.0" + ccount "^2.0.0" + devlop "^1.0.0" + mdast-util-find-and-replace "^3.0.0" + micromark-util-character "^2.0.0" + +mdast-util-gfm-footnote@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9" + integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + +mdast-util-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" + integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" + integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-task-list-item@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" + integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095" + integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-gfm-autolink-literal "^2.0.0" + mdast-util-gfm-footnote "^2.0.0" + mdast-util-gfm-strikethrough "^2.0.0" + mdast-util-gfm-table "^2.0.0" + mdast-util-gfm-task-list-item "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.0.0.tgz#468cbbb277375523de807248b8ad969feb02a5c7" + integrity sha512-xadSsJayQIucJ9n053dfQwVu1kuXg7jCTdYsMK8rqzKZh52nLfSH/k0sAxE0u+pj/zKZX+o5wB+ML5mRayOxFA== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz#9813f1d6e0cdaac7c244ec8c6dabfdb2102ea2b4" + integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + meow@^6.0.0: version "6.1.1" resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -7950,6 +8150,279 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +micromark-core-commonmark@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3" + integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-autolink-literal@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz#f1e50b42e67d441528f39a67133eddde2bbabfd9" + integrity sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-footnote@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz#91afad310065a94b636ab1e9dab2c60d1aab953c" + integrity sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg== + dependencies: + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz#6917db8e320da70e39ffbf97abdbff83e6783e61" + integrity sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz#2cf3fe352d9e089b7ef5fff003bdfe0da29649b7" + integrity sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-tagfilter@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" + integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-gfm-task-list-item@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz#ee8b208f1ced1eb9fb11c19a23666e59d86d4838" + integrity sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" + integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== + dependencies: + micromark-extension-gfm-autolink-literal "^2.0.0" + micromark-extension-gfm-footnote "^2.0.0" + micromark-extension-gfm-strikethrough "^2.0.0" + micromark-extension-gfm-table "^2.0.0" + micromark-extension-gfm-tagfilter "^2.0.0" + micromark-extension-gfm-task-list-item "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07" + integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a" + integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030" + integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95" + integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763" + integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.0.1.tgz#52b824c2e2633b6fb33399d2ec78ee2a90d6b298" + integrity sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89" + integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34" + integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5" + integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.0.tgz#a798808d02cc74113e2c939fc95363096ade7f1d" + integrity sha512-pIgcsGxpHEtTG/rPJRz/HOLSqp5VTuIIjXlPI+6JSDlK2oljApusG6KzpS8AF0ENUMCHlC/IBb5B9xdFiVlm5Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a" + integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4" + integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b" + integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364" + integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz#9f412442d77e0c5789ffdf42377fa8a2bcbdf581" + integrity sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + +micromark-util-types@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + +micromark@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249" + integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -9035,6 +9508,47 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" +remark-gfm@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de" + integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-stringify@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" + integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-to-markdown "^2.0.0" + unified "^11.0.0" + +remark@^15.0.1: + version "15.0.1" + resolved "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz#ac7e7563260513b66426bc47f850e7aa5862c37c" + integrity sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A== + dependencies: + "@types/mdast" "^4.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -9829,6 +10343,11 @@ trim-repeated@^2.0.0: dependencies: escape-string-regexp "^5.0.0" +trough@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" + integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== + truncate-utf8-bytes@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" @@ -10117,6 +10636,19 @@ undici@^5.12.0: dependencies: busboy "^1.6.0" +unified@^11.0.0, unified@^11.0.3: + version "11.0.3" + resolved "https://registry.npmjs.org/unified/-/unified-11.0.3.tgz#e141be0fe466a2d28b2160f62712bc9cbc08fdd4" + integrity sha512-jlCV402P+YDcFcB2VcN/n8JasOddqIiaxv118wNBoZXEhOn+lYG7BR4Bfg2BwxvlK58dwbuH2w7GX2esAjL6Mg== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + unique-filename@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" @@ -10131,6 +10663,37 @@ unique-slug@^4.0.0: dependencies: imurmurhash "^0.1.4" +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -10201,6 +10764,23 @@ varint@^6.0.0: resolved "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" + integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + viem@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/viem/-/viem-1.1.4.tgz#21be6d235fb387a391d4abdbbf95cd93bd066ffb" @@ -10504,3 +11084,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==