auto generated openapi schema

This commit is contained in:
Dhravya Shah 2025-03-03 10:59:34 -06:00
parent 8f016b3bfa
commit ff14d77c11
9 changed files with 404 additions and 353 deletions

View file

@ -24,6 +24,7 @@
"openai": "^4.68.4",
"postgres": "^3.4.4",
"uuid": "^11.0.1",
"wrangler": "^3.111.0",
"zod": "^3.23.8"
},
"devDependencies": {

View file

@ -17,6 +17,7 @@ import spacesRoute from "./routes/spaces";
import actions from "./routes/actions";
import memories from "./routes/memories";
import integrations from "./routes/integrations";
import { fromHono, OpenAPIRoute } from "chanfana";
import {
cloudflareRateLimiter,
DurableObjectRateLimiter,
@ -24,190 +25,203 @@ import {
} from "@hono-rate-limiter/cloudflare";
import { ConfigType, GeneralConfigType, rateLimiter } from "hono-rate-limiter";
export const app = new Hono<{ Variables: Variables; Bindings: Env }>()
.use("*", timing())
.use("*", logger())
.use(
"*",
cors({
origin: [
"http://localhost:3000",
"https://supermemory.ai",
"https://*.supermemory.ai",
"https://*.supermemory.com",
"https://supermemory.com",
"chrome-extension://*",
],
allowHeaders: ["*"],
allowMethods: ["*"],
credentials: true,
exposeHeaders: ["*"],
})
)
.use("/v1/*", auth)
.use("/v1/*", (c, next) => {
const user = c.get("user");
if (c.env.NODE_ENV === "development") {
return next();
}
// Create base Hono app first
const honoApp = new Hono<{ Variables: Variables; Bindings: Env }>();
// RATELIMITS
const rateLimitConfig = {
// Endpoints that bypass rate limiting
excludedPaths: [
"/v1/add",
"/v1/chat",
"/v1/suggested-learnings",
"/v1/recommended-questions",
] as (string | RegExp)[],
const app = fromHono(honoApp, {
base: "",
});
// Custom rate limits for specific endpoints
customLimits: {
notionImport: {
paths: ["/v1/integrations/notion/import", "/v1/integrations/notion"],
windowMs: 10 * 60 * 1000, // 10 minutes
limit: 5, // 5 requests per 10 minutes
},
inviteSpace: {
paths: [/^\v1\/spaces\/[^/]+\/invite$/],
windowMs: 60 * 1000, // 1 minute
limit: 5, // 5 requests per minute
},
} as Record<
string,
{ paths: (string | RegExp)[]; windowMs: number; limit: number }
>,
// Add all middleware and routes
app.use("*", timing());
app.use("*", logger());
app.use(
"*",
cors({
origin: [
"http://localhost:3000",
"https://supermemory.ai",
"https://*.supermemory.ai",
"https://*.supermemory.com",
"https://supermemory.com",
"chrome-extension://*",
],
allowHeaders: ["*"],
allowMethods: ["*"],
credentials: true,
exposeHeaders: ["*"],
})
);
default: {
windowMs: 60 * 1000, // 1 minute
limit: 100, // 100 requests per minute
app.use("/v1/*", auth);
app.use("/v1/*", (c, next) => {
const user = c.get("user");
if (c.env.NODE_ENV === "development") {
return next();
}
// RATELIMITS
const rateLimitConfig = {
// Endpoints that bypass rate limiting
excludedPaths: [
"/v1/add",
"/v1/chat",
"/v1/suggested-learnings",
"/v1/recommended-questions",
] as (string | RegExp)[],
// Custom rate limits for specific endpoints
customLimits: {
notionImport: {
paths: ["/v1/integrations/notion/import", "/v1/integrations/notion"],
windowMs: 10 * 60 * 1000, // 10 minutes
limit: 5, // 5 requests per 10 minutes
},
inviteSpace: {
paths: [/^\v1\/spaces\/[^/]+\/invite$/],
windowMs: 60 * 1000, // 1 minute
limit: 5, // 5 requests per minute
},
} as Record<
string,
{ paths: (string | RegExp)[]; windowMs: number; limit: number }
>,
common: {
standardHeaders: "draft-6",
keyGenerator: (c: Context) =>
(user?.uuid ?? c.req.header("cf-connecting-ip")) +
"-" +
new Date().getDate(), // day so that limit gets reset every day
store: new DurableObjectStore({ namespace: c.env.RATE_LIMITER }),
} as GeneralConfigType<ConfigType>,
};
default: {
windowMs: 60 * 1000, // 1 minute
limit: 100, // 100 requests per minute
},
common: {
standardHeaders: "draft-6",
keyGenerator: (c: Context) =>
(user?.uuid ?? c.req.header("cf-connecting-ip")) +
"-" +
new Date().getDate(), // day so that limit gets reset every day
store: new DurableObjectStore({ namespace: c.env.RATE_LIMITER }),
} as GeneralConfigType<ConfigType>,
};
if (
c.req.path &&
rateLimitConfig.excludedPaths.some((path) =>
typeof path === "string" ? c.req.path === path : path.test(c.req.path)
)
) {
return next();
}
// Check for custom rate limits
for (const [_, config] of Object.entries(rateLimitConfig.customLimits)) {
if (
c.req.path &&
rateLimitConfig.excludedPaths.some((path) =>
config.paths.some((path) =>
typeof path === "string" ? c.req.path === path : path.test(c.req.path)
)
) {
return next();
return rateLimiter({
windowMs: config.windowMs,
limit: config.limit,
...rateLimitConfig.common,
})(c as any, next);
}
}
// Check for custom rate limits
for (const [_, config] of Object.entries(rateLimitConfig.customLimits)) {
if (
config.paths.some((path) =>
typeof path === "string" ? c.req.path === path : path.test(c.req.path)
)
) {
return rateLimiter({
windowMs: config.windowMs,
limit: config.limit,
...rateLimitConfig.common,
})(c as any, next);
}
}
// Apply default rate limit
return rateLimiter({
windowMs: rateLimitConfig.default.windowMs,
limit: rateLimitConfig.default.limit,
...rateLimitConfig.common,
})(c as any, next);
});
// Apply default rate limit
return rateLimiter({
windowMs: rateLimitConfig.default.windowMs,
limit: rateLimitConfig.default.limit,
...rateLimitConfig.common,
})(c as any, next);
})
.get("/", (c) => {
return c.html(<LandingPage />);
})
// TEMPORARY REDIRECT
.all("/api/*", async (c) => {
// Get the full URL and path
const url = new URL(c.req.url);
const path = url.pathname;
const newPath = path.replace("/api", "/v1");
app.get("/", (c) => {
return c.html(<LandingPage />);
});
// Preserve query parameters and build target URL
const redirectUrl = "https://api.supermemory.ai" + newPath + url.search;
// TEMPORARY REDIRECT
app.all("/api/*", async (c) => {
// Get the full URL and path
const url = new URL(c.req.url);
const path = url.pathname;
const newPath = path.replace("/api", "/v1");
// Use c.redirect() for a proper redirect
return c.redirect(redirectUrl);
})
.route("/v1/user", user)
.route("/v1/spaces", spacesRoute)
.route("/v1", actions)
.route("/v1/integrations", integrations)
.route("/v1/memories", memories)
.get("/v1/session", (c) => {
const user = c.get("user");
// Preserve query parameters and build target URL
const redirectUrl = "https://api.supermemory.ai" + newPath + url.search;
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
// Use c.redirect() for a proper redirect
return c.redirect(redirectUrl);
});
return c.json({
user,
app.route("/v1/user", user);
app.route("/v1/spaces", spacesRoute);
app.route("/v1", actions);
app.route("/v1/integrations", integrations);
app.route("/v1/memories", memories);
app.get("/v1/session", (c) => {
const user = c.get("user");
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
return c.json({
user,
});
});
app.post(
"/waitlist",
zValidator(
"json",
z.object({ email: z.string().email(), token: z.string() })
),
async (c) => {
const { email, token } = c.req.valid("json");
const address = c.req.raw.headers.get("CF-Connecting-IP");
const idempotencyKey = crypto.randomUUID();
const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const firstResult = await fetch(url, {
body: JSON.stringify({
secret: c.env.TURNSTILE_SECRET_KEY,
response: token,
remoteip: address,
idempotency_key: idempotencyKey,
}),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
})
.post(
"/waitlist",
zValidator(
"json",
z.object({ email: z.string().email(), token: z.string() })
),
async (c) => {
const { email, token } = c.req.valid("json");
const address = c.req.raw.headers.get("CF-Connecting-IP");
const firstOutcome = (await firstResult.json()) as { success: boolean };
const idempotencyKey = crypto.randomUUID();
const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const firstResult = await fetch(url, {
body: JSON.stringify({
secret: c.env.TURNSTILE_SECRET_KEY,
response: token,
remoteip: address,
idempotency_key: idempotencyKey,
}),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!firstOutcome.success) {
console.info("Turnstile verification failed", firstOutcome);
return c.json(
{ error: "Turnstile verification failed" },
439 as StatusCode
);
}
const firstOutcome = (await firstResult.json()) as { success: boolean };
const resend = new Resend(c.env.RESEND_API_KEY);
if (!firstOutcome.success) {
console.info("Turnstile verification failed", firstOutcome);
return c.json(
{ error: "Turnstile verification failed" },
439 as StatusCode
);
}
const db = database(c.env.HYPERDRIVE.connectionString);
const resend = new Resend(c.env.RESEND_API_KEY);
const ip =
c.req.header("cf-connecting-ip") ||
`${c.req.raw.cf?.asn}-${c.req.raw.cf?.country}-${c.req.raw.cf?.city}-${c.req.raw.cf?.region}-${c.req.raw.cf?.postalCode}`;
const db = database(c.env.HYPERDRIVE.connectionString);
const { success } = await c.env.EMAIL_LIMITER.limit({ key: ip });
const ip =
c.req.header("cf-connecting-ip") ||
`${c.req.raw.cf?.asn}-${c.req.raw.cf?.country}-${c.req.raw.cf?.city}-${c.req.raw.cf?.region}-${c.req.raw.cf?.postalCode}`;
if (!success) {
return c.json({ error: "Rate limit exceeded" }, 429);
}
const { success } = await c.env.EMAIL_LIMITER.limit({ key: ip });
if (!success) {
return c.json({ error: "Rate limit exceeded" }, 429);
}
const message = `Supermemory started as a side project a few months ago when I built it as a hackathon project.
const message = `Supermemory started as a side project a few months ago when I built it as a hackathon project.
<br></br>
you guys loved it too much. like wayy too much. it was embarrassing, because this was not it - it was nothing but a hackathon project.
<br></br>
@ -216,26 +230,27 @@ export const app = new Hono<{ Variables: Variables; Bindings: Env }>()
So, it's time to make this good. My vision is to make supermemory the best memory tool on the internet.
`;
try {
await db.insert(waitlist).values({ email });
await resend.emails.send({
from: "Dhravya From Supermemory <waitlist@m.supermemory.com>",
to: email,
subject: "You're in the waitlist - A personal note from Dhravya",
html: `<p>Hi. I'm Dhravya. I'm building Supermemory to help people remember everything.<br></br> ${message} <br></br><br></br>I'll be in touch when we launch! Till then, just reply to this email if you wanna talk :)<br></br>If you want to follow me on X, here's my handle: <a href='https://x.com/dhravyashah'>@dhravyashah</a><br></br><br></br>- Dhravya</p>`,
});
} catch (e) {
console.error(e);
return c.json({ error: "Failed to add to waitlist" }, 400);
}
return c.json({ success: true });
try {
await db.insert(waitlist).values({ email });
await resend.emails.send({
from: "Dhravya From Supermemory <waitlist@m.supermemory.com>",
to: email,
subject: "You're in the waitlist - A personal note from Dhravya",
html: `<p>Hi. I'm Dhravya. I'm building Supermemory to help people remember everything.<br></br> ${message} <br></br><br></br>I'll be in touch when we launch! Till then, just reply to this email if you wanna talk :)<br></br>If you want to follow me on X, here's my handle: <a href='https://x.com/dhravyashah'>@dhravyashah</a><br></br><br></br>- Dhravya</p>`,
});
} catch (e) {
console.error(e);
return c.json({ error: "Failed to add to waitlist" }, 400);
}
)
.onError((err, c) => {
console.error(err);
return c.json({ error: "Internal server error" }, 500);
});
return c.json({ success: true });
}
);
app.onError((err, c) => {
console.error(err);
return c.json({ error: "Internal server error" }, 500);
});
export default {
fetch: app.fetch,

View file

@ -0,0 +1,18 @@
import { fromHono } from "chanfana";
import { Hono } from "hono";
import { serve as serveHono } from "@hono/node-server";
const app = fromHono(new Hono());
app.get("/", (c) => {
return c.json({ message: "Hello, world!" });
});
app.get("/entry/:id", (c) => {
const id = c.req.param("id");
return c.json({ message: `Hello, ${id}!` });
});
serveHono(app, (info) => {
console.log(info);
});

View file

@ -38,8 +38,11 @@ import {
} from "@supermemory/db";
import { typeDecider } from "../utils/typeDecider";
import { isErr, Ok } from "../errors/results";
import { fromHono } from "chanfana";
const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
const actions = fromHono(new Hono<{ Variables: Variables; Bindings: Env }>(), {
base: "",
})
.post(
"/chat",
zValidator(
@ -1195,3 +1198,4 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
);
export default actions;
import { Context } from 'hono/jsx'

View file

@ -5,173 +5,180 @@ import { getAllNotionPageContents } from "../utils/notion";
import { and, eq, or } from "@supermemory/db";
import { documents } from "@supermemory/db/schema";
import { database } from "@supermemory/db";
import { fromHono } from "chanfana";
const integrations = new Hono<{ Variables: Variables; Bindings: Env }>().get("/notion/import", async (c) => {
const user = c.get("user");
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
const integrations = fromHono(
new Hono<{ Variables: Variables; Bindings: Env }>(),
{
base: "",
}
).get("/notion/import", async (c) => {
const user = c.get("user");
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
// Create SSE stream
const stream = new TransformStream();
const writer = stream.writable.getWriter();
const encoder = new TextEncoder();
// Create SSE stream
const stream = new TransformStream();
const writer = stream.writable.getWriter();
const encoder = new TextEncoder();
// Create response first so client gets headers immediately
const response = new Response(stream.readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
// Required CORS headers for SSE
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
},
});
// Create response first so client gets headers immediately
const response = new Response(stream.readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
// Required CORS headers for SSE
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
},
});
const sendMessage = async (data: Record<string, any>) => {
// Proper SSE format requires "data: " prefix and double newline
const formattedData = `data: ${JSON.stringify(data)}\n\n`;
await writer.write(encoder.encode(formattedData));
};
const sendMessage = async (data: Record<string, any>) => {
// Proper SSE format requires "data: " prefix and double newline
const formattedData = `data: ${JSON.stringify(data)}\n\n`;
await writer.write(encoder.encode(formattedData));
};
// Start processing in background
c.executionCtx.waitUntil(
(async () => {
try {
// Send initial heartbeat
await sendMessage({ type: "connected" });
// Start processing in background
c.executionCtx.waitUntil(
(async () => {
try {
// Send initial heartbeat
await sendMessage({ type: "connected" });
const token = await getDecryptedKV(
c.env.ENCRYPTED_TOKENS,
`${user.uuid}-notion`,
`${c.env.WORKOS_COOKIE_PASSWORD}-${user.uuid}`
);
const token = await getDecryptedKV(
c.env.ENCRYPTED_TOKENS,
`${user.uuid}-notion`,
`${c.env.WORKOS_COOKIE_PASSWORD}-${user.uuid}`
);
const stringToken = new TextDecoder().decode(token);
if (!stringToken) {
await sendMessage({ type: "error", error: "No token found" });
await writer.close();
return;
const stringToken = new TextDecoder().decode(token);
if (!stringToken) {
await sendMessage({ type: "error", error: "No token found" });
await writer.close();
return;
}
await sendMessage({ type: "progress", progress: 5 });
// Fetch pages with progress updates
const pages = await getAllNotionPageContents(
stringToken,
async (progress) => {
// Map progress from 0-100 to 5-40 range
const scaledProgress = Math.floor(5 + (progress * 35) / 100);
await sendMessage({ type: "progress", progress: scaledProgress });
}
);
await sendMessage({ type: "progress", progress: 5 });
await sendMessage({ type: "progress", progress: 40 });
// Fetch pages with progress updates
const pages = await getAllNotionPageContents(
stringToken,
async (progress) => {
// Map progress from 0-100 to 5-40 range
const scaledProgress = Math.floor(5 + (progress * 35) / 100);
await sendMessage({ type: "progress", progress: scaledProgress });
}
);
let processed = 0;
const totalPages = pages.length;
await sendMessage({ type: "progress", progress: 40 });
const db = database(c.env.HYPERDRIVE.connectionString);
let processed = 0;
const totalPages = pages.length;
for (const page of pages) {
// Calculate document hash for duplicate detection
const encoder = new TextEncoder();
const data = encoder.encode(page.content);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const documentHash = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
const db = database(c.env.HYPERDRIVE.connectionString);
for (const page of pages) {
// Calculate document hash for duplicate detection
const encoder = new TextEncoder();
const data = encoder.encode(page.content);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const documentHash = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
// Check for duplicates using hash
const existingDocs = await db
.select()
.from(documents)
.where(
and(
eq(documents.userId, user.id),
or(
eq(documents.contentHash, documentHash),
and(
eq(documents.type, "notion"),
or(
eq(documents.url, page.url),
eq(documents.raw, page.content)
)
// Check for duplicates using hash
const existingDocs = await db
.select()
.from(documents)
.where(
and(
eq(documents.userId, user.id),
or(
eq(documents.contentHash, documentHash),
and(
eq(documents.type, "notion"),
or(
eq(documents.url, page.url),
eq(documents.raw, page.content)
)
)
)
);
)
);
if (existingDocs.length > 0) {
await sendMessage({
type: "warning",
message: `Skipping duplicate page: ${page.title}`
});
processed++;
continue;
}
// Insert into documents table first
try {
await db.insert(documents).values({
uuid: page.id,
userId: user.id,
type: "notion",
url: page.url,
title: page.title,
contentHash: documentHash,
raw: page.content,
});
await c.env.CONTENT_WORKFLOW.create({
params: {
userId: user.id,
content: page.url,
spaces: [],
type: "notion",
uuid: page.id,
url: page.url,
prefetched: {
contentToVectorize: page.content,
contentToSave: page.content,
title: page.title,
type: "notion",
},
createdAt: page.createdAt,
},
id: `${user.id}-${page.id}-${new Date().getTime()}`,
});
processed++;
const progress = 50 + Math.floor((processed / totalPages) * 50);
await sendMessage({ type: "progress", progress, page: page.title });
} catch (error) {
console.error(`Failed to process page ${page.title}:`, error);
await sendMessage({
type: "warning",
message: `Failed to process page: ${page.title}`,
error: error instanceof Error ? error.message : "Unknown error"
});
processed++;
continue;
}
if (existingDocs.length > 0) {
await sendMessage({
type: "warning",
message: `Skipping duplicate page: ${page.title}`,
});
processed++;
continue;
}
await sendMessage({ type: "complete", progress: 100 });
await writer.close();
} catch (error) {
console.error("Import error:", error);
await sendMessage({
type: "error",
error: error instanceof Error ? error.message : "Import failed",
});
await writer.close();
// Insert into documents table first
try {
await db.insert(documents).values({
uuid: page.id,
userId: user.id,
type: "notion",
url: page.url,
title: page.title,
contentHash: documentHash,
raw: page.content,
});
await c.env.CONTENT_WORKFLOW.create({
params: {
userId: user.id,
content: page.url,
spaces: [],
type: "notion",
uuid: page.id,
url: page.url,
prefetched: {
contentToVectorize: page.content,
contentToSave: page.content,
title: page.title,
type: "notion",
},
createdAt: page.createdAt,
},
id: `${user.id}-${page.id}-${new Date().getTime()}`,
});
processed++;
const progress = 50 + Math.floor((processed / totalPages) * 50);
await sendMessage({ type: "progress", progress, page: page.title });
} catch (error) {
console.error(`Failed to process page ${page.title}:`, error);
await sendMessage({
type: "warning",
message: `Failed to process page: ${page.title}`,
error: error instanceof Error ? error.message : "Unknown error",
});
processed++;
continue;
}
}
})()
);
return response;
});
await sendMessage({ type: "complete", progress: 100 });
await writer.close();
} catch (error) {
console.error("Import error:", error);
await sendMessage({
type: "error",
error: error instanceof Error ? error.message : "Import failed",
});
await writer.close();
}
})()
);
export default integrations;
return response;
});
export default integrations;

View file

@ -9,8 +9,11 @@ import {
contentToSpace,
} from "@supermemory/db/schema";
import { and, database, desc, eq, or, sql, isNull } from "@supermemory/db";
import { fromHono } from "chanfana";
const memories = new Hono<{ Variables: Variables; Bindings: Env }>()
const memories = fromHono(new Hono<{ Variables: Variables; Bindings: Env }>(), {
base: "",
})
.get(
"/",
zValidator(

View file

@ -12,8 +12,14 @@ import {
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { randomId } from "@supermemory/shared";
import { fromHono } from "chanfana";
const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
const spacesRoute = fromHono(
new Hono<{ Variables: Variables; Bindings: Env }>(),
{
base: "",
}
)
.get("/", async (c) => {
const user = c.get("user");
if (!user) {

View file

@ -11,8 +11,11 @@ import {
import { decryptApiKey, getApiKey } from "../auth";
import { DurableObjectStore } from "@hono-rate-limiter/cloudflare";
import { rateLimiter } from "hono-rate-limiter";
import { fromHono } from "chanfana";
const user = new Hono<{ Variables: Variables; Bindings: Env }>()
const user = fromHono(new Hono<{ Variables: Variables; Bindings: Env }>(), {
base: "",
})
.get("/", (c) => {
return c.json(c.get("user"));
})
@ -132,30 +135,23 @@ const user = new Hono<{ Variables: Variables; Bindings: Env }>()
return c.json({ invitations });
})
.get(
"/key",
async (c) => {
const user = c.get("user");
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
// we need user.id and user.lastApiKeyGeneratedAt
const lastApiKeyGeneratedAt = user.lastApiKeyGeneratedAt?.getTime();
if (!lastApiKeyGeneratedAt) {
return c.json({ error: "No API key generated" }, 400);
}
const key = await getApiKey(
user.uuid,
lastApiKeyGeneratedAt.toString(),
c
);
const decrypted = await decryptApiKey(key, c);
return c.json({ key, decrypted });
.get("/key", async (c) => {
const user = c.get("user");
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
)
// we need user.id and user.lastApiKeyGeneratedAt
const lastApiKeyGeneratedAt = user.lastApiKeyGeneratedAt?.getTime();
if (!lastApiKeyGeneratedAt) {
return c.json({ error: "No API key generated" }, 400);
}
const key = await getApiKey(user.uuid, lastApiKeyGeneratedAt.toString(), c);
const decrypted = await decryptApiKey(key, c);
return c.json({ key, decrypted });
})
.post("/update", async (c) => {
const user = c.get("user");
if (!user) {
@ -194,6 +190,6 @@ const user = new Hono<{ Variables: Variables; Bindings: Env }>()
.where(eq(users.id, user.id));
return c.json({ success: true });
})
});
export default user;

View file

@ -56,6 +56,7 @@
"autoevals": "^0.0.106",
"aws4fetch": "^1.0.20",
"braintrust": "^0.0.171",
"chanfana": "^2.6.3",
"cheerio": "^1.0.0",
"cloudflare": "^3.5.0",
"concurrently": "^9.1.0",