mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add api to handle license api keys
This commit is contained in:
2
apps/api/.env.example
Normal file
2
apps/api/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
LEMON_SQUEEZY_API_KEY=""
|
||||
LEMON_SQUEEZY_STORE_ID=""
|
||||
28
apps/api/.gitignore
vendored
Normal file
28
apps/api/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# dev
|
||||
.yarn/
|
||||
!.yarn/releases
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea/workspace.xml
|
||||
.idea/usage.statistics.xml
|
||||
.idea/shelf
|
||||
|
||||
# deps
|
||||
node_modules/
|
||||
|
||||
# env
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
8
apps/api/README.md
Normal file
8
apps/api/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```
|
||||
open http://localhost:3000
|
||||
```
|
||||
15
apps/api/package.json
Normal file
15
apps/api/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"hono": "^4.5.8",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1"
|
||||
}
|
||||
}
|
||||
66
apps/api/src/index.ts
Normal file
66
apps/api/src/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import { config } from "dotenv";
|
||||
import { validateLemonSqueezyLicense } from "./utils";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
config();
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.use(
|
||||
"/*",
|
||||
cors({
|
||||
origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js
|
||||
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowHeaders: ["Content-Type", "Authorization"],
|
||||
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
|
||||
maxAge: 600,
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
|
||||
export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
||||
export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
||||
|
||||
app.get("/v1/health", (c) => {
|
||||
return c.text("Hello Hono!");
|
||||
});
|
||||
|
||||
app.post("/v1/validate-license", async (c) => {
|
||||
const { licenseKey } = await c.req.json();
|
||||
|
||||
if (!licenseKey) {
|
||||
return c.json({ error: "License key is required" }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const licenseValidation = await validateLemonSqueezyLicense(licenseKey);
|
||||
|
||||
if (licenseValidation.valid) {
|
||||
return c.json({
|
||||
valid: true,
|
||||
message: "License is valid",
|
||||
metadata: licenseValidation.meta,
|
||||
});
|
||||
}
|
||||
return c.json(
|
||||
{
|
||||
valid: false,
|
||||
message: licenseValidation.error || "Invalid license",
|
||||
},
|
||||
400,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error during license validation:", error);
|
||||
return c.json({ error: "Internal server error" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
const port = 4000;
|
||||
console.log(`Server is running on port ${port}`);
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
});
|
||||
16
apps/api/src/types.ts
Normal file
16
apps/api/src/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface LemonSqueezyLicenseResponse {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
meta?: {
|
||||
store_id: string;
|
||||
order_id: number;
|
||||
order_item_id: number;
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
variant_id: number;
|
||||
variant_name: string;
|
||||
customer_id: number;
|
||||
customer_name: string;
|
||||
customer_email: string;
|
||||
};
|
||||
}
|
||||
28
apps/api/src/utils.ts
Normal file
28
apps/api/src/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_STORE_ID } from ".";
|
||||
import type { LemonSqueezyLicenseResponse } from "./types";
|
||||
|
||||
export const validateLemonSqueezyLicense = async (
|
||||
licenseKey: string,
|
||||
): Promise<LemonSqueezyLicenseResponse> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://api.lemonsqueezy.com/v1/licenses/validate",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": LEMON_SQUEEZY_API_KEY as string,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
license_key: licenseKey,
|
||||
store_id: LEMON_SQUEEZY_STORE_ID as string,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
console.error("Error validating license:", error);
|
||||
return { valid: false, error: "Error validating license" };
|
||||
}
|
||||
};
|
||||
14
apps/api/tsconfig.json
Normal file
14
apps/api/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx",
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: Bell,
|
||||
href: "/dashboard/settings/notifications",
|
||||
},
|
||||
{
|
||||
title: "License",
|
||||
label: "",
|
||||
icon: KeyIcon,
|
||||
href: "/dashboard/settings/license",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(user?.canAccessToSSHKeys
|
||||
@@ -102,6 +108,7 @@ import {
|
||||
Activity,
|
||||
Bell,
|
||||
Database,
|
||||
KeyIcon,
|
||||
KeyRound,
|
||||
type LucideIcon,
|
||||
Route,
|
||||
|
||||
1
apps/dokploy/drizzle/0034_silent_slayback.sql
Normal file
1
apps/dokploy/drizzle/0034_silent_slayback.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "admin" ADD COLUMN "licenseKey" text;
|
||||
3084
apps/dokploy/drizzle/meta/0034_snapshot.json
Normal file
3084
apps/dokploy/drizzle/meta/0034_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -239,6 +239,13 @@
|
||||
"when": 1724555040199,
|
||||
"tag": "0033_sweet_black_bird",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 34,
|
||||
"version": "6",
|
||||
"when": 1724631497207,
|
||||
"tag": "0034_silent_slayback",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
145
apps/dokploy/pages/dashboard/settings/license.tsx
Normal file
145
apps/dokploy/pages/dashboard/settings/license.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormMessage,
|
||||
Form,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useEffect, type ReactElement } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const schema = z.object({
|
||||
licenseKey: z.string().min(1, {
|
||||
message: "License key is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof schema>;
|
||||
|
||||
export default function License() {
|
||||
const { data } = api.admin.one.useQuery();
|
||||
const { mutateAsync, isLoading } = api.license.setLicense.useMutation();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
licenseKey: data?.licenseKey || "",
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.licenseKey) {
|
||||
form.reset({
|
||||
licenseKey: data.licenseKey,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync(data.licenseKey)
|
||||
.then(async () => {
|
||||
toast.success("License Key Saved");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the license key");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="w-full">
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">License</CardTitle>
|
||||
<CardDescription className="flex flex-row gap-2">
|
||||
Set your license key to unlock the features
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://dokploy.com/pricing"
|
||||
className="text-primary text-md"
|
||||
>
|
||||
See pricing
|
||||
</Link>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="licenseKey"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>License Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="w-full"
|
||||
placeholder={"Enter your license key"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
License.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<DashboardLayout tab={"settings"}>
|
||||
<SettingsLayout>{page}</SettingsLayout>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { securityRouter } from "./routers/security";
|
||||
import { settingsRouter } from "./routers/settings";
|
||||
import { sshRouter } from "./routers/ssh-key";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { licenseRouter } from "./routers/license";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -58,6 +59,7 @@ export const appRouter = createTRPCRouter({
|
||||
cluster: clusterRouter,
|
||||
notification: notificationRouter,
|
||||
sshKey: sshRouter,
|
||||
license: licenseRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
41
apps/dokploy/server/api/routers/license.ts
Normal file
41
apps/dokploy/server/api/routers/license.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
import { findAdmin, updateAdmin } from "../services/admin";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const licenseRouter = createTRPCRouter({
|
||||
setLicense: adminProcedure.input(z.string()).mutation(async ({ input }) => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
try {
|
||||
const result = await fetch("http://127.0.0.1:4000/v1/validate-license", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
licenseKey: input,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await result.json();
|
||||
|
||||
if (!data.valid) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "License is invalid",
|
||||
});
|
||||
}
|
||||
|
||||
if (data.valid) {
|
||||
return await updateAdmin(admin.authId, {
|
||||
licenseKey: input,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
return await updateAdmin(admin.authId, {
|
||||
licenseKey: "",
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -53,11 +53,6 @@ import { canAccessToTraefikFiles } from "../services/user";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils";
|
||||
import { logRotationManager } from "@/server/utils/access-log/handler";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import type { LogEntry } from "@/server/utils/access-log/types";
|
||||
import EventEmitter from "node:events";
|
||||
|
||||
const ee = new EventEmitter();
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
|
||||
@@ -28,6 +28,7 @@ export const admins = pgTable("admin", {
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
enableLogRotation: boolean("enableLogRotation").notNull().default(false),
|
||||
licenseKey: text("licenseKey"),
|
||||
authId: text("authId")
|
||||
.notNull()
|
||||
.references(() => auth.id, { onDelete: "cascade" }),
|
||||
|
||||
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@@ -36,6 +36,25 @@ importers:
|
||||
specifier: 4.16.2
|
||||
version: 4.16.2
|
||||
|
||||
apps/api:
|
||||
dependencies:
|
||||
'@hono/node-server':
|
||||
specifier: ^1.12.1
|
||||
version: 1.12.1
|
||||
dotenv:
|
||||
specifier: ^16.3.1
|
||||
version: 16.4.5
|
||||
hono:
|
||||
specifier: ^4.5.8
|
||||
version: 4.5.8
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.11.17
|
||||
version: 20.14.10
|
||||
tsx:
|
||||
specifier: ^4.7.1
|
||||
version: 4.16.2
|
||||
|
||||
apps/docs:
|
||||
dependencies:
|
||||
fumadocs-core:
|
||||
@@ -1662,6 +1681,10 @@ packages:
|
||||
peerDependencies:
|
||||
tailwindcss: ^3.0
|
||||
|
||||
'@hono/node-server@1.12.1':
|
||||
resolution: {integrity: sha512-C9l+08O8xtXB7Ppmy8DjBFH1hYji7JKzsU32Yt1poIIbdPp6S7aOI8IldDHD9YFJ55lv2c21ovNrmxatlHfhAg==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
|
||||
'@hookform/resolvers@3.9.0':
|
||||
resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==}
|
||||
peerDependencies:
|
||||
@@ -5664,6 +5687,10 @@ packages:
|
||||
highlight.js@10.7.3:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
|
||||
hono@4.5.8:
|
||||
resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
html-to-text@9.0.5:
|
||||
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -9670,6 +9697,8 @@ snapshots:
|
||||
dependencies:
|
||||
tailwindcss: 3.4.7
|
||||
|
||||
'@hono/node-server@1.12.1': {}
|
||||
|
||||
'@hookform/resolvers@3.9.0(react-hook-form@7.52.1(react@18.2.0))':
|
||||
dependencies:
|
||||
react-hook-form: 7.52.1(react@18.2.0)
|
||||
@@ -14053,7 +14082,7 @@ snapshots:
|
||||
eslint: 8.45.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
|
||||
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.45.0)
|
||||
eslint-plugin-react: 7.35.0(eslint@8.45.0)
|
||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.45.0)
|
||||
@@ -14077,7 +14106,7 @@ snapshots:
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.45.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.5
|
||||
is-core-module: 2.15.0
|
||||
@@ -14099,7 +14128,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0):
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.8
|
||||
array.prototype.findlastindex: 1.2.5
|
||||
@@ -14806,6 +14835,8 @@ snapshots:
|
||||
|
||||
highlight.js@10.7.3: {}
|
||||
|
||||
hono@4.5.8: {}
|
||||
|
||||
html-to-text@9.0.5:
|
||||
dependencies:
|
||||
'@selderee/plugin-htmlparser2': 0.11.0
|
||||
|
||||
@@ -2,3 +2,4 @@ packages:
|
||||
- "apps/dokploy"
|
||||
- "apps/docs"
|
||||
- "apps/website"
|
||||
- "apps/api"
|
||||
|
||||
Reference in New Issue
Block a user