diff --git a/apps/web/package.json b/apps/web/package.json index eb8811dac..7c1a7a42b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,6 +20,8 @@ "@auth/prisma-adapter": "^2.7.4", "@aws-sdk/client-s3": "^3.645.0", "@aws-sdk/s3-request-presigner": "^3.645.0", + "@hono-rate-limiter/redis": "^0.1.4", + "@hono/zod-validator": "^0.5.0", "@hookform/resolvers": "^3.3.1", "@next/bundle-analyzer": "^15.3.1", "@next/env": "^15.3.1", @@ -50,7 +52,7 @@ "@upstash/qstash": "^2.7.17", "@upstash/ratelimit": "^1.2.1", "@vercel/functions": "^2.0.0", - "@vercel/kv": "^2.0.0", + "@vercel/kv": "^3.0.0", "ai": "^4.1.50", "autoprefixer": "^10.4.13", "calendar-link": "^2.6.0", @@ -59,6 +61,8 @@ "cookie": "^0.7.0", "crypto": "^1.0.1", "dayjs": "^1.11.13", + "hono": "^4.7.10", + "hono-rate-limiter": "^0.2.1", "i18next": "^24.2.2", "i18next-http-backend": "^3.0.2", "i18next-icu": "^2.3.0", @@ -92,7 +96,7 @@ "superjson": "^2.0.0", "tailwindcss": "^3.4.17", "timezone-soft": "^1.5.1", - "zod": "^3.23.8" + "zod": "^3.25.6" }, "devDependencies": { "@babel/core": "^7.26.10", diff --git a/apps/web/src/app/api/licensing/v1/[...route]/route.ts b/apps/web/src/app/api/licensing/v1/[...route]/route.ts new file mode 100644 index 000000000..d08bdf43d --- /dev/null +++ b/apps/web/src/app/api/licensing/v1/[...route]/route.ts @@ -0,0 +1,153 @@ +import { env } from "@/env"; +import { generateLicenseKey } from "@/features/licensing/helpers/generate-license-key"; +import { + type CreateLicenseResponse, + type ValidateLicenseKeyResponse, + createLicenseInputSchema, + validateLicenseKeyInputSchema, +} from "@/features/licensing/schema"; +import { isSelfHosted } from "@/utils/constants"; +import { RedisStore } from "@hono-rate-limiter/redis"; +import { zValidator } from "@hono/zod-validator"; +import { prisma } from "@rallly/database"; +import { kv } from "@vercel/kv"; +import { Hono } from "hono"; +import { rateLimiter } from "hono-rate-limiter"; +import { bearerAuth } from "hono/bearer-auth"; +import { some } from "hono/combine"; +import { handle } from "hono/vercel"; + +const isKvAvailable = + process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN; + +const app = new Hono().basePath("/api/licensing/v1"); + +app.use("*", async (c, next) => { + if (isSelfHosted) { + return c.json({ error: "Not available in self-hosted instances" }, 404); + } + return next(); +}); + +if (env.LICENSE_API_AUTH_TOKEN) { + app.post( + "/licenses", + zValidator("json", createLicenseInputSchema), + some( + bearerAuth({ token: env.LICENSE_API_AUTH_TOKEN }), + rateLimiter({ + windowMs: 60 * 60 * 1000, + limit: 10, + store: isKvAvailable + ? new RedisStore({ + client: kv, + }) + : undefined, + }), + ), + async (c) => { + const { + type, + seats, + expiresAt, + licenseeEmail, + licenseeName, + version, + stripeCustomerId, + } = c.req.valid("json"); + + try { + const license = await prisma.license.create({ + data: { + licenseKey: generateLicenseKey({ version }), + version, + type, + seats, + issuedAt: new Date(), + expiresAt, + licenseeEmail, + licenseeName, + stripeCustomerId, + }, + }); + return c.json({ + data: { key: license.licenseKey }, + } satisfies CreateLicenseResponse); + } catch (error) { + console.error("Failed to create license:", error); + return c.json({ error: "Failed to create license" }, 500); + } + }, + ); +} + +app.post( + "/licenses/actions/validate-key", + zValidator("json", validateLicenseKeyInputSchema), + rateLimiter({ + keyGenerator: async (c) => { + const { key, fingerprint } = await c.req.json(); + return `validate-key:${key}:${fingerprint}`; + }, + windowMs: 60 * 60 * 1000, + limit: 10, + store: isKvAvailable + ? new RedisStore({ + client: kv, + }) + : undefined, + }), + async (c) => { + const { key, fingerprint } = c.req.valid("json"); + + const license = await prisma.license.findUnique({ + where: { + licenseKey: key, + }, + select: { + id: true, + licenseKey: true, + status: true, + issuedAt: true, + expiresAt: true, + licenseeEmail: true, + licenseeName: true, + seats: true, + type: true, + version: true, + }, + }); + + if (!license) { + return c.json({ error: "License not found" }, 404); + } + + await prisma.licenseValidation.create({ + data: { + licenseId: license.id, + ipAddress: c.req.header("x-forwarded-for"), + fingerprint, + validatedAt: new Date(), + userAgent: c.req.header("user-agent"), + }, + }); + + return c.json({ + data: { + key: license.licenseKey, + valid: license.status === "ACTIVE", + status: license.status, + issuedAt: license.issuedAt, + expiresAt: license.expiresAt, + licenseeEmail: license.licenseeEmail, + licenseeName: license.licenseeName, + seats: license.seats, + type: license.type, + version: license.version, + }, + } satisfies ValidateLicenseKeyResponse); + }, +); + +export const GET = handle(app); +export const POST = handle(app); diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 20bf88433..549ad90c7 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -74,6 +74,11 @@ export const env = createEnv({ * @default "false" */ MODERATION_ENABLED: z.enum(["true", "false"]).default("false"), + /** + * Licensing API Configuration + */ + LICENSE_API_URL: z.string().optional(), + LICENSE_API_AUTH_TOKEN: z.string().optional(), }, /* * Environment variables available on the client (and server). @@ -125,6 +130,8 @@ export const env = createEnv({ NOREPLY_EMAIL_NAME: process.env.NOREPLY_EMAIL_NAME, OPENAI_API_KEY: process.env.OPENAI_API_KEY, MODERATION_ENABLED: process.env.MODERATION_ENABLED, + LICENSE_API_URL: process.env.LICENSE_API_URL, + LICENSE_API_AUTH_TOKEN: process.env.LICENSE_API_AUTH_TOKEN, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, }); diff --git a/apps/web/src/features/licensing/actions/validate-license.ts b/apps/web/src/features/licensing/actions/validate-license.ts new file mode 100644 index 000000000..70d4634aa --- /dev/null +++ b/apps/web/src/features/licensing/actions/validate-license.ts @@ -0,0 +1,43 @@ +"use server"; + +import { rateLimit } from "@/features/rate-limit"; +import { prisma } from "@rallly/database"; +import { licensingClient } from "../client"; + +export async function validateLicenseKey(key: string) { + const { success } = await rateLimit("validate_license_key", 10, "1 m"); + + if (!success) { + throw new Error("Rate limit exceeded"); + } + + const { data, error } = await licensingClient.validateLicenseKey({ + key, + }); + + if (error) { + throw new Error(`License validation failed: ${error}`); + } + + if (!data) { + return { + valid: false, + }; + } + + await prisma.instanceLicense.create({ + data: { + licenseKey: data.key, + licenseeName: data.licenseeName, + licenseeEmail: data.licenseeEmail, + issuedAt: data.issuedAt, + expiresAt: data.expiresAt, + seats: data.seats, + type: data.type, + }, + }); + + return { + valid: true, + }; +} diff --git a/apps/web/src/features/licensing/client.ts b/apps/web/src/features/licensing/client.ts new file mode 100644 index 000000000..cc999e185 --- /dev/null +++ b/apps/web/src/features/licensing/client.ts @@ -0,0 +1,7 @@ +import { env } from "@/env"; +import { LicensingClient } from "./lib/licensing-client"; + +export const licensingClient = new LicensingClient({ + apiUrl: env.LICENSE_API_URL, + authToken: env.LICENSE_API_AUTH_TOKEN, +}); diff --git a/apps/web/src/features/licensing/helpers/calculate-checksum.ts b/apps/web/src/features/licensing/helpers/calculate-checksum.ts new file mode 100644 index 000000000..db6c57935 --- /dev/null +++ b/apps/web/src/features/licensing/helpers/calculate-checksum.ts @@ -0,0 +1,13 @@ +/** + * Calculate a checksum for a string + * @param str The string to calculate the checksum for + * @returns The checksum + */ +export function calculateChecksum(str: string): string { + // Simple checksum: sum char codes, mod 100000, base36 + let sum = 0; + for (let i = 0; i < str.length; i++) { + sum += str.charCodeAt(i); + } + return (sum % 100000).toString(36).toUpperCase().padStart(5, "0"); +} diff --git a/apps/web/src/features/licensing/helpers/check-license-key.ts b/apps/web/src/features/licensing/helpers/check-license-key.ts new file mode 100644 index 000000000..f2f1e41e3 --- /dev/null +++ b/apps/web/src/features/licensing/helpers/check-license-key.ts @@ -0,0 +1,9 @@ +import { calculateChecksum } from "./calculate-checksum"; + +export function checkLicenseKey(key: string): boolean { + const parts = key.split("-"); + if (parts.length !== 6) return false; + const checksum = parts[5]; + const licenseBody = parts.slice(0, 5).join("-"); + return calculateChecksum(licenseBody) === checksum; +} diff --git a/apps/web/src/features/licensing/helpers/generate-license-key.ts b/apps/web/src/features/licensing/helpers/generate-license-key.ts new file mode 100644 index 000000000..e4ae149ba --- /dev/null +++ b/apps/web/src/features/licensing/helpers/generate-license-key.ts @@ -0,0 +1,21 @@ +import { customAlphabet } from "nanoid"; +import { calculateChecksum } from "./calculate-checksum"; + +const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +const generate = customAlphabet(alphabet, 4); + +/** + * Generate user friendly licenses + * eg. RLYV4-ABCD-1234-ABCD-1234-XXXX + */ +export const generateLicenseKey = ({ version }: { version?: number }) => { + let license = `RLYV${version ?? "X"}-`; + for (let i = 0; i < 4; i++) { + license += generate(); + if (i < 3) { + license += "-"; + } + } + const checksum = calculateChecksum(license); + return `${license}-${checksum}`; +}; diff --git a/apps/web/src/features/licensing/index.ts b/apps/web/src/features/licensing/index.ts new file mode 100644 index 000000000..d40bfc15c --- /dev/null +++ b/apps/web/src/features/licensing/index.ts @@ -0,0 +1 @@ +export { LicensingClient } from "./lib/licensing-client"; diff --git a/apps/web/src/features/licensing/lib/licensing-client.ts b/apps/web/src/features/licensing/lib/licensing-client.ts new file mode 100644 index 000000000..f74cc6658 --- /dev/null +++ b/apps/web/src/features/licensing/lib/licensing-client.ts @@ -0,0 +1,59 @@ +import { + type CreateLicenseInput, + type ValidateLicenseInputKeySchema, + createLicenseResponseSchema, + validateLicenseKeyResponseSchema, +} from "../schema"; + +export class LicensingClient { + apiUrl: string; + authToken?: string; + + constructor({ + apiUrl = "https://licensing.rallly.co", + authToken, + }: { + apiUrl?: string; + authToken?: string; + }) { + this.apiUrl = apiUrl; + this.authToken = authToken; + } + async createLicense(input: CreateLicenseInput) { + if (!this.authToken) { + throw new Error("Licensing API auth token is not configured."); + } + + const res = await fetch(`${this.apiUrl}/api/v1/licenses`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.authToken}`, + }, + body: JSON.stringify(input), + }); + + if (!res.ok) { + throw new Error("Failed to create license."); + } + return createLicenseResponseSchema.parse(await res.json()); + } + async validateLicenseKey(input: ValidateLicenseInputKeySchema) { + const res = await fetch( + `${this.apiUrl}/api/v1/licenses/actions/validate-key`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }, + ); + + if (!res.ok) { + throw new Error("Failed to validate license key."); + } + + return validateLicenseKeyResponseSchema.parse(await res.json()); + } +} diff --git a/apps/web/src/features/licensing/schema.ts b/apps/web/src/features/licensing/schema.ts new file mode 100644 index 000000000..6cdebfe93 --- /dev/null +++ b/apps/web/src/features/licensing/schema.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; + +// ========================= +// Enums & Basic Types +// ========================= + +export const licenseTypeSchema = z.enum(["PLUS", "ORGANIZATION", "ENTERPRISE"]); +export type LicenseType = z.infer; + +export const licenseStatusSchema = z.enum(["ACTIVE", "REVOKED"]); +export type LicenseStatus = z.infer; + +// ========================= +// Generic API Response +// ========================= + +export const apiResponseSchema = (dataSchema: T) => + z.object({ + data: dataSchema.optional(), + error: z.string().optional(), + }); + +export type ApiResponse = { + data?: T; + error?: string; +}; + +// ========================= +// Create License +// ========================= + +export const createLicenseInputSchema = z.object({ + type: licenseTypeSchema, + seats: z.number().optional(), + expiresAt: z.date().optional(), + licenseeEmail: z.string().optional(), + licenseeName: z.string().optional(), + version: z.number().optional(), + stripeCustomerId: z.string().optional(), +}); +export type CreateLicenseInput = z.infer; + +export const createLicenseResponseSchema = apiResponseSchema( + z.object({ + key: z.string(), + }), +); +export type CreateLicenseResponse = z.infer; + +// ========================= +// Validate License Key +// ========================= + +export const validateLicenseKeyInputSchema = z.object({ + key: z.string(), + fingerprint: z.string().optional(), +}); +export type ValidateLicenseInputKeySchema = z.infer< + typeof validateLicenseKeyInputSchema +>; + +export const validateLicenseKeyResponseSchema = apiResponseSchema( + z.object({ + key: z.string(), + valid: z.boolean(), + status: licenseStatusSchema, + issuedAt: z.date(), + expiresAt: z.date().nullable(), + licenseeEmail: z.string().nullable(), + licenseeName: z.string().nullable(), + seats: z.number().nullable(), + type: licenseTypeSchema, + version: z.number().nullable(), + }), +); + +export type ValidateLicenseKeyResponse = z.infer< + typeof validateLicenseKeyResponseSchema +>; + +export const licenseCheckoutMetadataSchema = z.object({ + licenseType: licenseTypeSchema, + seats: z.number(), +}); + +export type LicenseCheckoutMetada = z.infer< + typeof licenseCheckoutMetadataSchema +>; diff --git a/packages/database/prisma/migrations/20250522165415_licensing_schema/migration.sql b/packages/database/prisma/migrations/20250522165415_licensing_schema/migration.sql new file mode 100644 index 000000000..41555663b --- /dev/null +++ b/packages/database/prisma/migrations/20250522165415_licensing_schema/migration.sql @@ -0,0 +1,59 @@ +-- CreateEnum +CREATE TYPE "LicenseType" AS ENUM ('PLUS', 'ORGANIZATION', 'ENTERPRISE'); + +-- CreateEnum +CREATE TYPE "LicenseStatus" AS ENUM ('ACTIVE', 'REVOKED'); + +-- CreateTable +CREATE TABLE "licenses" ( + "id" TEXT NOT NULL, + "license_key" TEXT NOT NULL, + "version" INTEGER, + "type" "LicenseType" NOT NULL, + "seats" INTEGER, + "issued_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" TIMESTAMP(3), + "licensee_email" TEXT, + "licensee_name" TEXT, + "status" "LicenseStatus" NOT NULL DEFAULT 'ACTIVE', + "stripe_customer_id" TEXT, + + CONSTRAINT "licenses_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "license_validations" ( + "id" TEXT NOT NULL, + "license_id" TEXT NOT NULL, + "ip_address" TEXT, + "fingerprint" TEXT, + "validated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "user_agent" TEXT, + + CONSTRAINT "license_validations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "instance_licenses" ( + "id" TEXT NOT NULL, + "license_key" TEXT NOT NULL, + "version" INTEGER, + "type" "LicenseType" NOT NULL, + "seats" INTEGER, + "issued_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" TIMESTAMP(3), + "licensee_email" TEXT, + "licensee_name" TEXT, + "status" "LicenseStatus" NOT NULL DEFAULT 'ACTIVE', + + CONSTRAINT "instance_licenses_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "licenses_license_key_key" ON "licenses"("license_key"); + +-- CreateIndex +CREATE UNIQUE INDEX "instance_licenses_license_key_key" ON "instance_licenses"("license_key"); + +-- AddForeignKey +ALTER TABLE "license_validations" ADD CONSTRAINT "license_validations_license_id_fkey" FOREIGN KEY ("license_id") REFERENCES "licenses"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/prisma/models/licensing.prisma b/packages/database/prisma/models/licensing.prisma new file mode 100644 index 000000000..1bae90e40 --- /dev/null +++ b/packages/database/prisma/models/licensing.prisma @@ -0,0 +1,56 @@ +enum LicenseType { + PLUS + ORGANIZATION + ENTERPRISE +} + +enum LicenseStatus { + ACTIVE + REVOKED +} + +model License { + id String @id @default(cuid()) + licenseKey String @unique @map("license_key") + version Int? @map("version") + type LicenseType + seats Int? @map("seats") + issuedAt DateTime @default(now()) @map("issued_at") + expiresAt DateTime? @map("expires_at") + licenseeEmail String? @map("licensee_email") + licenseeName String? @map("licensee_name") + status LicenseStatus @default(ACTIVE) @map("status") + stripeCustomerId String? @map("stripe_customer_id") + + validations LicenseValidation[] + + @@map("licenses") +} + +model LicenseValidation { + id String @id @default(cuid()) + licenseId String @map("license_id") + license License @relation(fields: [licenseId], references: [id], onDelete: Cascade) + ipAddress String? @map("ip_address") + fingerprint String? @map("fingerprint") + validatedAt DateTime @default(now()) @map("validated_at") + userAgent String? @map("user_agent") + + @@map("license_validations") +} + + +model InstanceLicense { + id String @id @default(cuid()) + licenseKey String @unique @map("license_key") + version Int? @map("version") + type LicenseType + seats Int? @map("seats") + issuedAt DateTime @default(now()) @map("issued_at") + expiresAt DateTime? @map("expires_at") + licenseeEmail String? @map("licensee_email") + licenseeName String? @map("licensee_name") + status LicenseStatus @default(ACTIVE) @map("status") + + @@map("instance_licenses") +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43f0e599b..0f7b0b1a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,7 +152,7 @@ importers: dependencies: '@ai-sdk/openai': specifier: ^1.2.0 - version: 1.3.20(zod@3.24.3) + version: 1.3.20(zod@3.25.20) '@auth/prisma-adapter': specifier: ^2.7.4 version: 2.9.0(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3))(nodemailer@6.10.1) @@ -162,6 +162,12 @@ importers: '@aws-sdk/s3-request-presigner': specifier: ^3.645.0 version: 3.797.0 + '@hono-rate-limiter/redis': + specifier: ^0.1.4 + version: 0.1.4(hono-rate-limiter@0.2.3(hono@4.7.10)) + '@hono/zod-validator': + specifier: ^0.5.0 + version: 0.5.0(hono@4.7.10)(zod@3.25.20) '@hookform/resolvers': specifier: ^3.3.1 version: 3.10.0(react-hook-form@7.56.1(react@19.1.0)) @@ -221,7 +227,7 @@ importers: version: 8.1.0(typescript@5.8.3) '@t3-oss/env-nextjs': specifier: ^0.11.0 - version: 0.11.1(typescript@5.8.3)(zod@3.24.3) + version: 0.11.1(typescript@5.8.3)(zod@3.25.20) '@tanstack/react-query': specifier: ^5.74.11 version: 5.74.11(react@19.1.0) @@ -253,11 +259,11 @@ importers: specifier: ^2.0.0 version: 2.0.0(@aws-sdk/credential-provider-web-identity@3.797.0) '@vercel/kv': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^3.0.0 + version: 3.0.0 ai: specifier: ^4.1.50 - version: 4.3.10(react@19.1.0)(zod@3.24.3) + version: 4.3.10(react@19.1.0)(zod@3.25.20) autoprefixer: specifier: ^10.4.13 version: 10.4.21(postcss@8.5.3) @@ -279,6 +285,12 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + hono: + specifier: ^4.7.10 + version: 4.7.10 + hono-rate-limiter: + specifier: ^0.2.1 + version: 0.2.3(hono@4.7.10) i18next: specifier: ^24.2.2 version: 24.2.3(typescript@5.8.3) @@ -379,8 +391,8 @@ importers: specifier: ^1.5.1 version: 1.5.2 zod: - specifier: ^3.23.8 - version: 3.24.3 + specifier: ^3.25.6 + version: 3.25.20 devDependencies: '@babel/core': specifier: ^7.26.10 @@ -2026,6 +2038,17 @@ packages: resolution: {integrity: sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==} engines: {node: '>=10.13.0'} + '@hono-rate-limiter/redis@0.1.4': + resolution: {integrity: sha512-RSrVX5N2Oo/xXApskegu667cBVHyr8RXGWnbRDGjU2py8pN4BttEKSHA0iKi3BAwh1xSkENgDRng4tpFD9DbKg==} + peerDependencies: + hono-rate-limiter: ^0.2.1 + + '@hono/zod-validator@0.5.0': + resolution: {integrity: sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg==} + peerDependencies: + hono: '>=3.9.0' + zod: ^3.19.1 + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -4221,8 +4244,8 @@ packages: '@aws-sdk/credential-provider-web-identity': optional: true - '@vercel/kv@2.0.0': - resolution: {integrity: sha512-zdVrhbzZBYo5d1Hfn4bKtqCeKf0FuzW8rSHauzQVMUgv1+1JOwof2mWcBuI+YMJy8s0G0oqAUfQ7HgUDzb8EbA==} + '@vercel/kv@3.0.0': + resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==} engines: {node: '>=14.6'} '@vitest/expect@2.1.9': @@ -5296,6 +5319,15 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono-rate-limiter@0.2.3: + resolution: {integrity: sha512-/pQIJWMd8saoySMoM3rhHNiC3XtZuZDkxBvtnPmRKrpNBZIG38k0cs+nVmBW5pc8HAmkS+I/14rbHfRObekWmw==} + peerDependencies: + hono: ^4.1.1 + + hono@4.7.10: + resolution: {integrity: sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==} + engines: {node: '>=16.9.0'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -7341,47 +7373,47 @@ packages: peerDependencies: zod: ^3.24.1 - zod@3.24.3: - resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} + zod@3.25.20: + resolution: {integrity: sha512-z03fqpTMDF1G02VLKUMt6vyACE7rNWkh3gpXVHgPTw28NPtDFRGvcpTtPwn2kMKtQ0idtYJUTxchytmnqYswcw==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: - '@ai-sdk/openai@1.3.20(zod@3.24.3)': + '@ai-sdk/openai@1.3.20(zod@3.25.20)': dependencies: '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.7(zod@3.24.3) - zod: 3.24.3 + '@ai-sdk/provider-utils': 2.2.7(zod@3.25.20) + zod: 3.25.20 - '@ai-sdk/provider-utils@2.2.7(zod@3.24.3)': + '@ai-sdk/provider-utils@2.2.7(zod@3.25.20)': dependencies: '@ai-sdk/provider': 1.1.3 nanoid: 3.3.11 secure-json-parse: 2.7.0 - zod: 3.24.3 + zod: 3.25.20 '@ai-sdk/provider@1.1.3': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@1.2.9(react@19.1.0)(zod@3.24.3)': + '@ai-sdk/react@1.2.9(react@19.1.0)(zod@3.25.20)': dependencies: - '@ai-sdk/provider-utils': 2.2.7(zod@3.24.3) - '@ai-sdk/ui-utils': 1.2.8(zod@3.24.3) + '@ai-sdk/provider-utils': 2.2.7(zod@3.25.20) + '@ai-sdk/ui-utils': 1.2.8(zod@3.25.20) react: 19.1.0 swr: 2.3.3(react@19.1.0) throttleit: 2.1.0 optionalDependencies: - zod: 3.24.3 + zod: 3.25.20 - '@ai-sdk/ui-utils@1.2.8(zod@3.24.3)': + '@ai-sdk/ui-utils@1.2.8(zod@3.25.20)': dependencies: '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.7(zod@3.24.3) - zod: 3.24.3 - zod-to-json-schema: 3.24.5(zod@3.24.3) + '@ai-sdk/provider-utils': 2.2.7(zod@3.25.20) + zod: 3.25.20 + zod-to-json-schema: 3.24.5(zod@3.25.20) '@alloc/quick-lru@5.2.0': {} @@ -8973,6 +9005,15 @@ snapshots: dependencies: is-negated-glob: 1.0.0 + '@hono-rate-limiter/redis@0.1.4(hono-rate-limiter@0.2.3(hono@4.7.10))': + dependencies: + hono-rate-limiter: 0.2.3(hono@4.7.10) + + '@hono/zod-validator@0.5.0(hono@4.7.10)(zod@3.25.20)': + dependencies: + hono: 4.7.10 + zod: 3.25.20 + '@hookform/resolvers@3.10.0(react-hook-form@7.56.1(react@19.1.0))': dependencies: react-hook-form: 7.56.1(react@19.1.0) @@ -10977,16 +11018,16 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.11.1(typescript@5.8.3)(zod@3.24.3)': + '@t3-oss/env-core@0.11.1(typescript@5.8.3)(zod@3.25.20)': dependencies: - zod: 3.24.3 + zod: 3.25.20 optionalDependencies: typescript: 5.8.3 - '@t3-oss/env-nextjs@0.11.1(typescript@5.8.3)(zod@3.24.3)': + '@t3-oss/env-nextjs@0.11.1(typescript@5.8.3)(zod@3.25.20)': dependencies: - '@t3-oss/env-core': 0.11.1(typescript@5.8.3)(zod@3.24.3) - zod: 3.24.3 + '@t3-oss/env-core': 0.11.1(typescript@5.8.3)(zod@3.25.20) + zod: 3.25.20 optionalDependencies: typescript: 5.8.3 @@ -11270,7 +11311,7 @@ snapshots: optionalDependencies: '@aws-sdk/credential-provider-web-identity': 3.797.0 - '@vercel/kv@2.0.0': + '@vercel/kv@3.0.0': dependencies: '@upstash/redis': 1.34.8 @@ -11447,15 +11488,15 @@ snapshots: transitivePeerDependencies: - supports-color - ai@4.3.10(react@19.1.0)(zod@3.24.3): + ai@4.3.10(react@19.1.0)(zod@3.25.20): dependencies: '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.7(zod@3.24.3) - '@ai-sdk/react': 1.2.9(react@19.1.0)(zod@3.24.3) - '@ai-sdk/ui-utils': 1.2.8(zod@3.24.3) + '@ai-sdk/provider-utils': 2.2.7(zod@3.25.20) + '@ai-sdk/react': 1.2.9(react@19.1.0)(zod@3.25.20) + '@ai-sdk/ui-utils': 1.2.8(zod@3.25.20) '@opentelemetry/api': 1.9.0 jsondiffpatch: 0.6.0 - zod: 3.24.3 + zod: 3.25.20 optionalDependencies: react: 19.1.0 @@ -12469,6 +12510,12 @@ snapshots: dependencies: react-is: 16.13.1 + hono-rate-limiter@0.2.3(hono@4.7.10): + dependencies: + hono: 4.7.10 + + hono@4.7.10: {} + html-escaper@2.0.2: {} html-parse-stringify@3.0.1: @@ -14858,10 +14905,10 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 - zod-to-json-schema@3.24.5(zod@3.24.3): + zod-to-json-schema@3.24.5(zod@3.25.20): dependencies: - zod: 3.24.3 + zod: 3.25.20 - zod@3.24.3: {} + zod@3.25.20: {} zwitch@2.0.4: {}