Compare commits

..

7 Commits

Author SHA1 Message Date
Daniel Luiz Alves
f63105c5eb [Release] v3.2.5-beta (#320) 2025-10-21 13:47:12 -03:00
Daniel Luiz Alves
cb4ed3f581 version: update package versions from 3.2.4-beta to 3.2.5-beta across all packages 2025-10-21 11:24:11 -03:00
Daniel Luiz Alves
5fe6434027 [Release] v3.2.4-beta (#310) 2025-10-20 14:54:53 -03:00
Daniel Luiz Alves
94e021d8c6 [RELEASE] v3.2.3-beta #278 2025-10-02 10:22:11 -03:00
Daniel Luiz Alves
f3aeaf66df [RELEASE] v3.2.2-beta (#270)
Co-authored-by: Tommy Johnston <tommy@timmygstudios.com>
2025-09-25 15:08:35 -03:00
Daniel Luiz Alves
331624e2f2 UPDATE LICENCE (#252) 2025-09-09 18:28:33 -03:00
Daniel Luiz Alves
ba512ebe95 [RELEASE] v3.2.1-beta (#250) 2025-09-09 16:30:20 -03:00
13 changed files with 28 additions and 528 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "palmr-docs",
"version": "3.2.4-beta",
"version": "3.2.5-beta",
"description": "Docs for Palmr",
"private": true,
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",

View File

@@ -1,6 +1,6 @@
{
"name": "palmr-api",
"version": "3.2.4-beta",
"version": "3.2.5-beta",
"description": "API for Palmr",
"private": true,
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",

View File

@@ -1,9 +1,7 @@
import { MultipartFile } from "@fastify/multipart";
import { FastifyReply, FastifyRequest } from "fastify";
import {
CreateShareSchema,
CreateShareWithFilesSchema,
UpdateShareItemsSchema,
UpdateSharePasswordSchema,
UpdateShareRecipientsSchema,
@@ -34,67 +32,6 @@ export class ShareController {
}
}
async createShareWithFiles(request: FastifyRequest, reply: FastifyReply) {
try {
await request.jwtVerify();
const userId = (request as any).user?.userId;
if (!userId) {
return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." });
}
const parts = request.parts();
const uploadedFiles: MultipartFile[] = [];
let formData: any = {};
for await (const part of parts) {
if (part.type === "file") {
uploadedFiles.push(part as MultipartFile);
} else {
// Handle form fields
const fieldName = part.fieldname;
const value = (part as any).value;
// Parse JSON fields
if (fieldName === "existingFiles" || fieldName === "existingFolders" || fieldName === "recipients") {
try {
formData[fieldName] = JSON.parse(value);
} catch (e) {
formData[fieldName] = value;
}
} else if (fieldName === "maxViews") {
formData[fieldName] = value ? parseInt(value) : null;
} else {
formData[fieldName] = value;
}
}
}
// Validate at least one file or folder is provided
const hasExistingFiles = formData.existingFiles && formData.existingFiles.length > 0;
const hasExistingFolders = formData.existingFolders && formData.existingFolders.length > 0;
const hasNewFiles = uploadedFiles.length > 0;
if (!hasExistingFiles && !hasExistingFolders && !hasNewFiles) {
return reply.status(400).send({
error: "At least one file or folder must be selected or uploaded to create a share",
});
}
// Validate the form data against the schema (excluding file validation)
const input = CreateShareWithFilesSchema.parse(formData);
// Create the share with uploaded files
const share = await this.shareService.createShareWithFiles(input, uploadedFiles, userId);
return reply.status(201).send({ share });
} catch (error: any) {
console.error("Create Share With Files Error:", error);
if (error.errors) {
return reply.status(400).send({ error: error.errors });
}
return reply.status(400).send({ error: error.message || "Unknown error occurred" });
}
}
async listUserShares(request: FastifyRequest, reply: FastifyReply) {
try {
await request.jwtVerify();

View File

@@ -136,24 +136,6 @@ export const CreateShareAliasSchema = z.object({
.describe("The custom alias for the share"),
});
export const CreateShareWithFilesSchema = z.object({
name: z.string().optional().describe("The share name"),
description: z.string().optional().describe("The share description"),
expiration: z
.string()
.datetime({
message: "Data de expiração deve estar no formato ISO 8601 (ex: 2025-02-06T13:20:49Z)",
})
.optional(),
existingFiles: z.array(z.string()).optional().describe("Existing file IDs to include"),
existingFolders: z.array(z.string()).optional().describe("Existing folder IDs to include"),
password: z.string().optional().describe("The share password"),
maxViews: z.number().optional().nullable().describe("The maximum number of views"),
recipients: z.array(z.string().email()).optional().describe("The recipient emails"),
folderId: z.string().optional().nullable().describe("Folder ID to upload new files to"),
});
export type CreateShareInput = z.infer<typeof CreateShareSchema>;
export type CreateShareWithFilesInput = z.infer<typeof CreateShareWithFilesSchema>;
export type UpdateShareInput = z.infer<typeof UpdateShareSchema>;
export type ShareResponse = z.infer<typeof ShareResponseSchema>;

View File

@@ -46,29 +46,6 @@ export async function shareRoutes(app: FastifyInstance) {
shareController.createShare.bind(shareController)
);
app.post(
"/shares/create-with-files",
{
preValidation,
schema: {
tags: ["Share"],
operationId: "createShareWithFiles",
summary: "Create a new share with file uploads",
description:
"Create a new share and upload new files directly in a single action. Supports multipart/form-data.",
consumes: ["multipart/form-data"],
response: {
201: z.object({
share: ShareResponseSchema,
}),
400: z.object({ error: z.string().describe("Error message") }),
401: z.object({ error: z.string().describe("Error message") }),
},
},
},
shareController.createShareWithFiles.bind(shareController)
);
app.get(
"/shares/me",
{

View File

@@ -1,18 +1,10 @@
import * as fs from "fs/promises";
import * as path from "path";
import { MultipartFile } from "@fastify/multipart";
import bcrypt from "bcryptjs";
import { FilesystemStorageProvider } from "../../providers/filesystem-storage.provider";
import { S3StorageProvider } from "../../providers/s3-storage.provider";
import { prisma } from "../../shared/prisma";
import { generateUniqueFileName, parseFileName } from "../../utils/file-name-generator";
import { ConfigService } from "../config/service";
import { EmailService } from "../email/service";
import { FileService } from "../file/service";
import { FolderService } from "../folder/service";
import { UserService } from "../user/service";
import { CreateShareInput, CreateShareWithFilesInput, ShareResponseSchema, UpdateShareInput } from "./dto";
import { CreateShareInput, ShareResponseSchema, UpdateShareInput } from "./dto";
import { IShareRepository, PrismaShareRepository } from "./repository";
export class ShareService {
@@ -121,141 +113,6 @@ export class ShareService {
return ShareResponseSchema.parse(await this.formatShareResponse(shareWithRelations));
}
async createShareWithFiles(data: CreateShareWithFilesInput, uploadedFiles: MultipartFile[], userId: string) {
const configService = new ConfigService();
const fileService = new FileService();
const { password, maxViews, existingFiles, existingFolders, folderId, ...shareData } = data;
// Validate existing files
if (existingFiles && existingFiles.length > 0) {
const files = await prisma.file.findMany({
where: {
id: { in: existingFiles },
userId: userId,
},
});
const notFoundFiles = existingFiles.filter((id) => !files.some((file) => file.id === id));
if (notFoundFiles.length > 0) {
throw new Error(`Files not found or access denied: ${notFoundFiles.join(", ")}`);
}
}
// Validate existing folders
if (existingFolders && existingFolders.length > 0) {
const folders = await prisma.folder.findMany({
where: {
id: { in: existingFolders },
userId: userId,
},
});
const notFoundFolders = existingFolders.filter((id) => !folders.some((folder) => folder.id === id));
if (notFoundFolders.length > 0) {
throw new Error(`Folders not found or access denied: ${notFoundFolders.join(", ")}`);
}
}
// Validate folder if specified
if (folderId) {
const folder = await prisma.folder.findFirst({
where: { id: folderId, userId },
});
if (!folder) {
throw new Error("Folder not found or access denied.");
}
}
const maxFileSize = BigInt(await configService.getValue("maxFileSize"));
const maxTotalStorage = BigInt(await configService.getValue("maxTotalStoragePerUser"));
// Check user storage
const userFiles = await prisma.file.findMany({
where: { userId },
select: { size: true },
});
const currentStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
// Upload new files and create file records
const newFileIds: string[] = [];
for (const uploadedFile of uploadedFiles) {
const buffer = await uploadedFile.toBuffer();
const fileSize = BigInt(buffer.length);
// Validate file size
if (fileSize > maxFileSize) {
const maxSizeMB = Number(maxFileSize) / (1024 * 1024);
throw new Error(`File ${uploadedFile.filename} exceeds the maximum allowed size of ${maxSizeMB}MB`);
}
// Check storage space
if (currentStorage + fileSize > maxTotalStorage) {
const availableSpace = Number(maxTotalStorage - currentStorage) / (1024 * 1024);
throw new Error(`Insufficient storage space. You have ${availableSpace.toFixed(2)}MB available`);
}
// Parse filename
const { baseName, extension } = parseFileName(uploadedFile.filename);
const uniqueName = await generateUniqueFileName(baseName, extension, userId, folderId || null);
// Generate object name
const objectName = `${userId}/${Date.now()}-${baseName}.${extension}`;
// Upload file to storage
if (fileService.isFilesystemMode()) {
// For filesystem mode, we need to use the filesystem provider
const provider = FilesystemStorageProvider.getInstance();
await provider.uploadFile(objectName, buffer);
} else {
// For S3 mode
const provider = new S3StorageProvider();
await provider.uploadFile(objectName, buffer);
}
// Create file record in database
const fileRecord = await prisma.file.create({
data: {
name: uniqueName,
extension: extension,
size: fileSize,
objectName: objectName,
userId,
folderId: folderId || null,
},
});
newFileIds.push(fileRecord.id);
}
// Combine existing and new file IDs
const allFileIds = [...(existingFiles || []), ...newFileIds];
// Validate at least one file or folder
if (allFileIds.length === 0 && (!existingFolders || existingFolders.length === 0)) {
throw new Error("At least one file or folder must be selected or uploaded to create a share");
}
// Create share security
const security = await prisma.shareSecurity.create({
data: {
password: password ? await bcrypt.hash(password, 10) : null,
maxViews: maxViews,
},
});
// Create share
const share = await this.shareRepository.createShare({
...shareData,
files: allFileIds,
folders: existingFolders,
securityId: security.id,
creatorId: userId,
});
const shareWithRelations = await this.shareRepository.findShareById(share.id);
return ShareResponseSchema.parse(await this.formatShareResponse(shareWithRelations));
}
async getShare(shareId: string, password?: string, userId?: string) {
const share = await this.shareRepository.findShareById(shareId);

View File

@@ -143,18 +143,4 @@ export class S3StorageProvider implements StorageProvider {
throw error;
}
}
async uploadFile(objectName: string, buffer: Buffer): Promise<void> {
if (!s3Client) {
throw new Error("S3 client is not available");
}
const command = new PutObjectCommand({
Bucket: bucketName,
Key: objectName,
Body: buffer,
});
await s3Client.send(command);
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "palmr-web",
"version": "3.2.4-beta",
"version": "3.2.5-beta",
"description": "Frontend for Palmr",
"private": true,
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",

View File

@@ -1,38 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3333";
export async function POST(req: NextRequest) {
const cookieHeader = req.headers.get("cookie");
// Get the multipart form data directly
const formData = await req.formData();
const url = `${API_BASE_URL}/shares/create-with-files`;
const apiRes = await fetch(url, {
method: "POST",
headers: {
cookie: cookieHeader || "",
// Don't set Content-Type, let fetch set it with boundary
},
body: formData,
redirect: "manual",
});
const resBody = await apiRes.text();
const res = new NextResponse(resBody, {
status: apiRes.status,
headers: {
"Content-Type": "application/json",
},
});
const setCookie = apiRes.headers.getSetCookie?.() || [];
if (setCookie.length > 0) {
res.headers.set("Set-Cookie", setCookie.join(","));
}
return res;
}

View File

@@ -1,9 +1,8 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { IconCalendar, IconEye, IconLock, IconShare, IconUpload, IconX } from "@tabler/icons-react";
import { IconCalendar, IconEye, IconLock, IconShare } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { useDropzone } from "react-dropzone";
import { toast } from "sonner";
import { FileTree, TreeFile, TreeFolder } from "@/components/tables/files-tree";
@@ -14,8 +13,7 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { createShare, createShareWithFiles } from "@/http/endpoints";
import { formatFileSize } from "@/utils/format-file-size";
import { createShare } from "@/http/endpoints";
interface CreateShareModalProps {
isOpen: boolean;
@@ -41,7 +39,6 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol
const [files, setFiles] = useState<TreeFile[]>([]);
const [folders, setFolders] = useState<TreeFolder[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [newFiles, setNewFiles] = useState<File[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(false);
@@ -88,35 +85,18 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol
maxViews: "",
});
setSelectedItems([]);
setNewFiles([]);
setCurrentTab("details");
}
}, [isOpen, loadData]);
const onDrop = useCallback((acceptedFiles: File[]) => {
setNewFiles((prev) => [...prev, ...acceptedFiles]);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: true,
});
const removeNewFile = (index: number) => {
setNewFiles((prev) => prev.filter((_, i) => i !== index));
};
const handleSubmit = async () => {
if (!formData.name.trim()) {
toast.error("Share name is required");
return;
}
const hasExistingItems = selectedItems.length > 0;
const hasNewFiles = newFiles.length > 0;
if (!hasExistingItems && !hasNewFiles) {
toast.error("Please select at least one file/folder or upload new files");
if (selectedItems.length === 0) {
toast.error("Please select at least one file or folder");
return;
}
@@ -126,40 +106,23 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol
const selectedFiles = selectedItems.filter((id) => files.some((file) => file.id === id));
const selectedFolders = selectedItems.filter((id) => folders.some((folder) => folder.id === id));
const expiration = formData.expiresAt
? (() => {
const dateValue = formData.expiresAt;
if (dateValue.length === 10) {
return new Date(dateValue + "T23:59:59").toISOString();
}
return new Date(dateValue).toISOString();
})()
: undefined;
// Use the new endpoint if there are new files to upload
if (hasNewFiles) {
await createShareWithFiles({
name: formData.name,
description: formData.description || undefined,
password: formData.isPasswordProtected ? formData.password : undefined,
expiration: expiration,
maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined,
existingFiles: selectedFiles.length > 0 ? selectedFiles : undefined,
existingFolders: selectedFolders.length > 0 ? selectedFolders : undefined,
newFiles: newFiles,
});
} else {
// Use the traditional endpoint if only selecting existing files
await createShare({
name: formData.name,
description: formData.description || undefined,
password: formData.isPasswordProtected ? formData.password : undefined,
expiration: expiration,
maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined,
files: selectedFiles,
folders: selectedFolders,
});
}
await createShare({
name: formData.name,
description: formData.description || undefined,
password: formData.isPasswordProtected ? formData.password : undefined,
expiration: formData.expiresAt
? (() => {
const dateValue = formData.expiresAt;
if (dateValue.length === 10) {
return new Date(dateValue + "T23:59:59").toISOString();
}
return new Date(dateValue).toISOString();
})()
: undefined,
maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined,
files: selectedFiles,
folders: selectedFolders,
});
toast.success(t("createShare.success"));
onSuccess();
@@ -183,10 +146,8 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol
};
const selectedCount = selectedItems.length;
const newFilesCount = newFiles.length;
const totalCount = selectedCount + newFilesCount;
const canProceedToFiles = formData.name.trim().length > 0;
const canSubmit = formData.name.trim().length > 0 && totalCount > 0;
const canSubmit = formData.name.trim().length > 0 && selectedCount > 0;
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
@@ -200,7 +161,7 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol
<div className="flex flex-col gap-6 flex-1 min-h-0 w-full overflow-hidden">
<Tabs value={currentTab} onValueChange={setCurrentTab} className="flex-1">
<TabsList className="grid w-full grid-cols-3">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="details">{t("createShare.tabs.shareDetails")}</TabsTrigger>
<TabsTrigger value="files" disabled={!canProceedToFiles}>
{t("createShare.tabs.selectFiles")}
@@ -210,14 +171,6 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol
</span>
)}
</TabsTrigger>
<TabsTrigger value="upload" disabled={!canProceedToFiles}>
{t("createShare.tabs.uploadFiles") || "Upload Files"}
{newFilesCount > 0 && (
<span className="ml-1 text-xs bg-primary text-primary-foreground rounded-full px-2 py-0.5">
{newFilesCount}
</span>
)}
</TabsTrigger>
</TabsList>
<TabsContent value="details" className="space-y-4 mt-4">
@@ -367,87 +320,6 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol
<Button variant="outline" onClick={() => setCurrentTab("details")}>
{t("common.back")}
</Button>
<div className="space-x-2">
<Button variant="outline" onClick={() => setCurrentTab("upload")}>
{t("createShare.nextUploadFiles") || "Upload New Files"}
</Button>
<Button onClick={handleSubmit} disabled={!canSubmit || isLoading}>
{isLoading ? t("common.creating") : t("createShare.create")}
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="upload" className="space-y-4 mt-4 flex-1 min-h-0">
<div className="space-y-4">
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
isDragActive ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-primary/50"
}`}
>
<input {...getInputProps()} />
<IconUpload className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
{isDragActive ? (
<p className="text-sm text-muted-foreground">Drop files here...</p>
) : (
<div>
<p className="text-sm font-medium mb-1">
{t("createShare.upload.dragDrop") || "Drag & drop files here"}
</p>
<p className="text-xs text-muted-foreground">
{t("createShare.upload.orClick") || "or click to browse"}
</p>
</div>
)}
</div>
{newFiles.length > 0 && (
<div className="space-y-2">
<Label>{t("createShare.upload.selectedFiles") || "Selected Files"}</Label>
<div className="max-h-[200px] overflow-y-auto space-y-2">
{newFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-2 bg-muted/50 rounded-md text-sm"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<IconUpload className="h-4 w-4 flex-shrink-0" />
<span className="truncate">{file.name}</span>
<span className="text-xs text-muted-foreground flex-shrink-0">
{formatFileSize(file.size)}
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeNewFile(index)}
className="ml-2 h-6 w-6 p-0"
>
<IconX className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
)}
<div className="text-sm text-muted-foreground">
{totalCount > 0 ? (
<span>
{totalCount} {totalCount === 1 ? "item" : "items"} selected ({selectedCount} existing,{" "}
{newFilesCount} new)
</span>
) : (
<span>No items selected</span>
)}
</div>
</div>
<div className="flex justify-between pt-4">
<Button variant="outline" onClick={() => setCurrentTab("files")}>
{t("common.back")}
</Button>
<div className="space-x-2">
<Button variant="outline" onClick={handleClose}>
{t("common.cancel")}

View File

@@ -10,8 +10,6 @@ import type {
CreateShareAliasResult,
CreateShareBody,
CreateShareResult,
CreateShareWithFilesBody,
CreateShareWithFilesResult,
DeleteShareResult,
GetShareByAliasParams,
GetShareByAliasResult,
@@ -41,63 +39,6 @@ export const createShare = <TData = CreateShareResult>(
return apiInstance.post(`/api/shares/create`, createShareBody, options);
};
/**
* Create a new share with file uploads
* @summary Create a new share and upload files in a single action
*/
export const createShareWithFiles = <TData = CreateShareWithFilesResult>(
createShareWithFilesBody: CreateShareWithFilesBody,
options?: AxiosRequestConfig
): Promise<TData> => {
const formData = new FormData();
// Add text fields
if (createShareWithFilesBody.name) {
formData.append("name", createShareWithFilesBody.name);
}
if (createShareWithFilesBody.description) {
formData.append("description", createShareWithFilesBody.description);
}
if (createShareWithFilesBody.expiration) {
formData.append("expiration", createShareWithFilesBody.expiration);
}
if (createShareWithFilesBody.password) {
formData.append("password", createShareWithFilesBody.password);
}
if (createShareWithFilesBody.maxViews !== undefined && createShareWithFilesBody.maxViews !== null) {
formData.append("maxViews", createShareWithFilesBody.maxViews.toString());
}
if (createShareWithFilesBody.folderId !== undefined) {
formData.append("folderId", createShareWithFilesBody.folderId || "");
}
// Add array fields as JSON strings
if (createShareWithFilesBody.existingFiles && createShareWithFilesBody.existingFiles.length > 0) {
formData.append("existingFiles", JSON.stringify(createShareWithFilesBody.existingFiles));
}
if (createShareWithFilesBody.existingFolders && createShareWithFilesBody.existingFolders.length > 0) {
formData.append("existingFolders", JSON.stringify(createShareWithFilesBody.existingFolders));
}
if (createShareWithFilesBody.recipients && createShareWithFilesBody.recipients.length > 0) {
formData.append("recipients", JSON.stringify(createShareWithFilesBody.recipients));
}
// Add files
if (createShareWithFilesBody.newFiles && createShareWithFilesBody.newFiles.length > 0) {
createShareWithFilesBody.newFiles.forEach((file) => {
formData.append("files", file);
});
}
return apiInstance.post(`/api/shares/create-with-files`, formData, {
...options,
headers: {
...options?.headers,
"Content-Type": "multipart/form-data",
},
});
};
/**
* Update a share
* @summary Update a share

View File

@@ -139,19 +139,6 @@ export interface CreateShareBody {
recipients?: string[];
}
export interface CreateShareWithFilesBody {
name?: string;
description?: string;
expiration?: string;
existingFiles?: string[];
existingFolders?: string[];
password?: string;
maxViews?: number | null;
recipients?: string[];
folderId?: string | null;
newFiles?: File[];
}
export interface UpdateShareBody {
id: string;
name?: string;
@@ -199,7 +186,6 @@ export interface GetShareByAliasParams {
}
export type CreateShareResult = AxiosResponse<CreateShare201>;
export type CreateShareWithFilesResult = AxiosResponse<CreateShare201>;
export type UpdateShareResult = AxiosResponse<UpdateShare200>;
export type ListUserSharesResult = AxiosResponse<ListUserShares200>;
export type GetShareResult = AxiosResponse<GetShare200>;

View File

@@ -1,6 +1,6 @@
{
"name": "palmr-monorepo",
"version": "3.2.4-beta",
"version": "3.2.5-beta",
"description": "Palmr monorepo with Husky configuration",
"private": true,
"packageManager": "pnpm@10.6.0",