Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add procedure for upserting weaveVM blob storage references #683

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hot-bobcats-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/api": minor
---

Added a protected procedure to upsert weaveVM blob storage references
2 changes: 1 addition & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ SWARM_BATCH_ID=f89e63edf757f06e89933761d6d46592d03026efb9871f9d244f34da86b6c242

FILE_SYSTEM_STORAGE_PATH=test-blobscan-blobs


WEAVEVM_API_KEY=weavevm-api-key
# Blob Propagator

BLOB_PROPAGATOR_TMP_BLOB_STORAGE=FILE_SYSTEM
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/app/docs/environment/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ nextjs:
| `FILE_SYSTEM_STORAGE_PATH` | Store blobs in this path | No | `/tmp/blobscan-blobs` |
| `WEAVEVM_STORAGE_ENABLED` | Weavevm storage usage | No | `false` |
| `WEAVEVM_STORAGE_API_BASE_URL` | Weavevm API base url | No | (empty) |
| `WEAVEVM_API_KEY` | API key required to authenticate requests to the WeaveVM blob storage reference updater endpoint | No | (empty) |
| `STATS_SYNCER_DAILY_CRON_PATTERN` | Cron pattern for the daily stats job | No | `30 0 * * * *` |
| `STATS_SYNCER_OVERALL_CRON_PATTERN` | Cron pattern for the overall stats job | No | `*/15 * * * *` |
| `SWARM_STAMP_CRON_PATTERN` | Cron pattern for swarm job | No | `*/15 * * * *` |
Expand Down
35 changes: 6 additions & 29 deletions packages/api/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
NodeHTTPResponse,
} from "@trpc/server/adapters/node-http";
import cookie from "cookie";
import jwt from "jsonwebtoken";

import type { BlobPropagator } from "@blobscan/blob-propagator";
import { getBlobPropagator } from "@blobscan/blob-propagator";
Expand All @@ -17,6 +16,8 @@ import { prisma } from "@blobscan/db";
import { env } from "@blobscan/env";

import { PostHogClient, shouldIgnoreURL } from "./posthog";
import type { APIClient } from "./utils";
import { retrieveAPIClient } from "./utils";

type NextHTTPRequest = CreateNextContextOptions["req"];

Expand All @@ -25,38 +26,14 @@ export type CreateContextOptions =
| CreateNextContextOptions;

type CreateInnerContextOptions = Partial<CreateContextOptions> & {
apiClient: string | null;
apiClient?: APIClient;
};

export function getJWTFromRequest(req: NodeHTTPRequest | NextHTTPRequest) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return null;
}

try {
const [type, token] = authHeader.split(" ");
if (type !== "Bearer" || !token) {
return null;
}

const decoded = jwt.verify(token, env.SECRET_KEY) as string;

return decoded;
} catch (err) {
if (err instanceof jwt.JsonWebTokenError) {
return null;
}

throw new TRPCError({ code: "BAD_REQUEST" });
}
}

export type TRPCInnerContext = {
prisma: typeof prisma;
blobStorageManager: BlobStorageManager;
blobPropagator: BlobPropagator | undefined;
apiClient: string | null | undefined;
blobPropagator?: BlobPropagator;
apiClient?: APIClient;
};

export async function createTRPCInnerContext(
Expand Down Expand Up @@ -92,7 +69,7 @@ export function createTRPCContext(
) {
return async (opts: CreateContextOptions) => {
try {
const apiClient = getJWTFromRequest(opts.req);
const apiClient = retrieveAPIClient(opts.req);

const innerContext = await createTRPCInnerContext({
apiClient,
Expand Down
13 changes: 0 additions & 13 deletions packages/api/src/middlewares/isJWTAuthed.ts

This file was deleted.

15 changes: 15 additions & 0 deletions packages/api/src/middlewares/withAuthed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { TRPCError } from "@trpc/server";

import { t } from "../trpc-client";
import type { APIClientType } from "../utils";

export const withAuthed = (expectedApiClientType: APIClientType) =>
t.middleware(({ ctx, next }) => {
if (ctx.apiClient?.type !== expectedApiClientType) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

return next({
ctx,
});
});
6 changes: 6 additions & 0 deletions packages/api/src/procedures/authed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { withAuthed } from "../middlewares/withAuthed";
import type { APIClientType } from "../utils";
import { publicProcedure } from "./public";

export const createAuthedProcedure = (apiClientType: APIClientType) =>
publicProcedure.use(withAuthed(apiClientType));
2 changes: 1 addition & 1 deletion packages/api/src/procedures/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./jwt-authed";
export * from "./authed";
export * from "./public";
4 changes: 0 additions & 4 deletions packages/api/src/procedures/jwt-authed.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/api/src/routers/blob/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { getAll } from "./getAll";
import { getBlobDataByBlobId } from "./getBlobDataByBlobId";
import { getByBlobId } from "./getByBlobId";
import { getCount } from "./getCount";
import { upsertWeaveVMReferences } from "./upsertWeaveVMReferences";

export const blobRouter = t.router({
getAll,
getByBlobId,
getBlobDataByBlobId,
getCount,
upsertWeaveVMReferences,
});
64 changes: 64 additions & 0 deletions packages/api/src/routers/blob/upsertWeaveVMReferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { TRPCError } from "@trpc/server";

import type { BlobDataStorageReference } from "@blobscan/db";
import { BlobStorage } from "@blobscan/db/prisma/enums";
import { z } from "@blobscan/zod";

import { createAuthedProcedure } from "../../procedures";

const inputSchema = z.object({
blobHashes: z.array(z.string()),
});

export const upsertWeaveVMReferences = createAuthedProcedure("weavevm")
.meta({
openapi: {
method: "PUT",
path: "/blobs/weavevm-references",
tags: ["blobs"],
summary: "upserts weaveVM blob references.",
},
})
.input(inputSchema)
.output(z.void())
.mutation(async ({ ctx: { prisma }, input: { blobHashes } }) => {
if (!blobHashes.length) {
return;
}

const blobDataStorageReferences = blobHashes.map<BlobDataStorageReference>(
(hash) => ({
blobHash: hash,
dataReference: hash,
blobStorage: BlobStorage.WEAVEVM,
})
);

const dbVersionedHashes = await prisma.blob
.findMany({
select: {
versionedHash: true,
},
where: {
versionedHash: {
in: blobHashes,
},
},
})
.then((blobs) => blobs.map((b) => b.versionedHash));

const missingHashes = blobHashes
.filter((hash) => !dbVersionedHashes.includes(hash))
.map((hash) => `"${hash}"`);

if (missingHashes.length > 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Couldn't found the following blobs: ${missingHashes.join(
", "
)}`,
});
}

await prisma.blobDataStorageReference.upsertMany(blobDataStorageReferences);
});
4 changes: 2 additions & 2 deletions packages/api/src/routers/blockchain-sync-state/updateState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";

import { z } from "@blobscan/zod";

import { jwtAuthedProcedure } from "../../procedures";
import { createAuthedProcedure } from "../../procedures";
import { BASE_PATH } from "./common";

export const inputSchema = z.object({
Expand All @@ -13,7 +13,7 @@ export const inputSchema = z.object({

export const outputSchema = z.void();

export const updateState = jwtAuthedProcedure
export const updateState = createAuthedProcedure("indexer")
.meta({
openapi: {
method: "PUT",
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/routers/indexer/handleReorgedSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Prisma } from "@blobscan/db";
import { z } from "@blobscan/zod";

import type { TRPCInnerContext } from "../../context";
import { jwtAuthedProcedure } from "../../procedures";
import { createAuthedProcedure } from "../../procedures";
import { INDEXER_PATH } from "./common";

const inputSchema = z.object({
Expand Down Expand Up @@ -121,7 +121,7 @@ async function generateBlockCleanupOperations(
return referenceRemovalOps;
}

export const handleReorgedSlots = jwtAuthedProcedure
export const handleReorgedSlots = createAuthedProcedure("indexer")
.meta({
openapi: {
method: "PUT",
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/routers/indexer/indexData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TRPCError } from "@trpc/server";
import type { BlobDataStorageReference } from "@blobscan/db";
import { toBigIntSchema, z } from "@blobscan/zod";

import { jwtAuthedProcedure } from "../../procedures";
import { createAuthedProcedure } from "../../procedures";
import { INDEXER_PATH } from "./common";
import {
createDBAddresses,
Expand Down Expand Up @@ -58,7 +58,7 @@ export type IndexDataInput = z.input<typeof inputSchema>;

export type IndexDataFormattedInput = z.output<typeof inputSchema>;

export const indexData = jwtAuthedProcedure
export const indexData = createAuthedProcedure("indexer")
.meta({
openapi: {
method: "PUT",
Expand Down
56 changes: 56 additions & 0 deletions packages/api/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { TRPCError } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { NodeHTTPRequest } from "@trpc/server/adapters/node-http";
import jwt from "jsonwebtoken";

import { env } from "@blobscan/env";

type NextHTTPRequest = CreateNextContextOptions["req"];

type HTTPRequest = NodeHTTPRequest | NextHTTPRequest;

export type APIClientType = "indexer" | "weavevm";

export type APIClient = {
type: APIClientType;
};

function verifyIndexerClient(token: string) {
const decoded = jwt.verify(token, env.SECRET_KEY) as string;

return decoded;
}

function verifyWeaveVMClient(token: string) {
return token === env.WEAVEVM_API_KEY;
}

export function retrieveAPIClient(req: HTTPRequest): APIClient | undefined {
const authHeader = req.headers.authorization;

if (!authHeader) {
return;
}

const [type, token] = authHeader.split(" ");

if (type !== "Bearer" || !token) {
return;

Check warning on line 38 in packages/api/src/utils/auth.ts

View check run for this annotation

Codecov / codecov/patch

packages/api/src/utils/auth.ts#L38

Added line #L38 was not covered by tests
}

try {
if (verifyWeaveVMClient(token)) {
return { type: "weavevm" };
}

if (verifyIndexerClient(token)) {
return { type: "indexer" };
}
} catch (err) {
if (err instanceof jwt.JsonWebTokenError) {
return;
}

throw new TRPCError({ code: "BAD_REQUEST" });
}

Check warning on line 55 in packages/api/src/utils/auth.ts

View check run for this annotation

Codecov / codecov/patch

packages/api/src/utils/auth.ts#L50-L55

Added lines #L50 - L55 were not covered by tests
}
1 change: 1 addition & 0 deletions packages/api/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./auth";
export * from "./blob";
export * from "./identifiers";
export * from "./schemas";
Expand Down
4 changes: 4 additions & 0 deletions packages/api/test/__snapshots__/blob.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2623,3 +2623,7 @@ exports[`Blob router > getByBlobId > should get a blob by versioned hash 1`] = `
"versionedHash": "blobHash004",
}
`;

exports[`Blob router > upsertWeaveVMReferences > when authorized > should fail when one or more provided blobs do not exist 1`] = `"Couldn't found the following blobs: \\"nonExistingBlobHash\\""`;

exports[`Blob router > upsertWeaveVMReferences > when authorized > should fail when one or more provided blobs do not exist 2`] = `undefined`;
Loading
Loading