1
mirror of https://github.com/jakejarvis/domainstack.io.git synced 2025-12-02 19:33:48 -05:00
Files
domainstack.io/trpc/init.ts

118 lines
3.4 KiB
TypeScript

import { initTRPC } from "@trpc/server";
import { headers } from "next/headers";
import { after } from "next/server";
import superjson from "superjson";
import { updateLastAccessed } from "@/lib/db/repos/domains";
import { toRegistrableDomain } from "@/lib/domain-server";
const IP_HEADERS = ["x-real-ip", "x-forwarded-for", "cf-connecting-ip"];
const resolveRequestIp = async () => {
try {
const headerList = await headers();
for (const name of IP_HEADERS) {
const value = headerList.get(name);
if (value) {
const first = value.split(",")[0]?.trim();
if (first) {
return first;
}
}
}
} catch {
// headers() is only available inside Next.js request lifecycle hooks.
// Ignore errors when invoked in tests or scripts.
}
return null;
};
export const createContext = async (opts?: { req?: Request }) => {
const req = opts?.req;
const ip = await resolveRequestIp();
return { req, ip } as const;
};
export type Context = Awaited<ReturnType<typeof createContext>>;
export const t = initTRPC
.context<Context>()
.meta<Record<string, unknown>>()
.create({
transformer: superjson,
});
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
/**
* Middleware to log the start, end, and duration of a procedure.
* @param path - The path of the procedure
* @param type - The type of the procedure
* @param next - The next middleware
* @returns The result of the next middleware
*/
const withLogging = t.middleware(async ({ path, type, next }) => {
const start = performance.now();
console.debug(`[trpc] start ${path} (${type})`);
try {
const result = await next();
const durationMs = Math.round(performance.now() - start);
console.info(`[trpc] ok ${path} (${type}) ${durationMs}ms`);
return result;
} catch (err) {
const durationMs = Math.round(performance.now() - start);
const error = err instanceof Error ? err : new Error(String(err));
console.error(
JSON.stringify({
level: "error",
message: `[trpc] error ${path} (${type})`,
path,
type,
durationMs,
error: {
message: error.message,
stack: error.stack,
cause: error.cause,
},
}),
);
throw err;
}
});
/**
* Middleware to record that a domain was accessed by a user (for decay calculation).
* Expects input to have a `domain` field.
* Schedules the write to happen after the response is sent using Next.js after().
*/
const withDomainAccessUpdate = t.middleware(async ({ input, next }) => {
// Check if input is a valid object with a domain property
if (
input &&
typeof input === "object" &&
"domain" in input &&
typeof input.domain === "string"
) {
const registrable = toRegistrableDomain(input.domain);
if (registrable) {
console.debug(`[trpc] recording access for domain: ${registrable}`);
after(() => updateLastAccessed(registrable));
}
}
return next();
});
/**
* Public procedure with logging.
* Use this for all public endpoints (e.g. health check, etc).
*/
export const publicProcedure = t.procedure.use(withLogging);
/**
* Domain-specific procedure with "last accessed at" tracking.
* Use this for all domain data endpoints (dns, hosting, seo, etc).
*/
export const domainProcedure = publicProcedure.use(withDomainAccessUpdate);