Add licensing api (#1723)

This commit is contained in:
Luke Vella 2025-05-23 15:35:40 +01:00 committed by GitHub
parent 982fc39ac7
commit 679d6fb034
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 607 additions and 40 deletions

View file

@ -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",

View file

@ -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);

View file

@ -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,
});

View file

@ -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,
};
}

View file

@ -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,
});

View file

@ -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");
}

View file

@ -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;
}

View file

@ -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}`;
};

View file

@ -0,0 +1 @@
export { LicensingClient } from "./lib/licensing-client";

View file

@ -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());
}
}

View file

@ -0,0 +1,88 @@
import { z } from "zod";
// =========================
// Enums & Basic Types
// =========================
export const licenseTypeSchema = z.enum(["PLUS", "ORGANIZATION", "ENTERPRISE"]);
export type LicenseType = z.infer<typeof licenseTypeSchema>;
export const licenseStatusSchema = z.enum(["ACTIVE", "REVOKED"]);
export type LicenseStatus = z.infer<typeof licenseStatusSchema>;
// =========================
// Generic API Response
// =========================
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
data: dataSchema.optional(),
error: z.string().optional(),
});
export type ApiResponse<T> = {
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<typeof createLicenseInputSchema>;
export const createLicenseResponseSchema = apiResponseSchema(
z.object({
key: z.string(),
}),
);
export type CreateLicenseResponse = z.infer<typeof createLicenseResponseSchema>;
// =========================
// 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
>;

View file

@ -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;

View file

@ -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")
}

123
pnpm-lock.yaml generated
View file

@ -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: {}