- Set Spanish as default language with ephemeral/encrypted privacy focus - Translate all user-facing strings and legal pages to Spanish - Replace Norwegian flag with Spanish flag in footer - Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship - Rewrite PrivacyPage: zero data collection, ephemeral design emphasis - Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references - Update PWA manifest, HTML meta tags, package.json branding - Rename webhook headers to X-Paste-Event / X-Paste-Signature - Update API docs title and contact to paste.es / cloudhost.es Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
import isCidr from 'is-cidr';
|
|
import { isIP } from 'is-ip';
|
|
import { z } from 'zod';
|
|
import { EXPIRATION_TIMES_SECONDS } from '../lib/constants';
|
|
|
|
// Hard ceiling for encrypted payloads at parse time (prevents memory exhaustion).
|
|
// Configurable via env var in KB, defaults to 1024 KB (1MB).
|
|
const MAX_ENCRYPTED_PAYLOAD_KB = parseInt(
|
|
process.env.HEMMELIG_MAX_ENCRYPTED_PAYLOAD_SIZE || '1024',
|
|
10
|
|
);
|
|
export const MAX_ENCRYPTED_SIZE = MAX_ENCRYPTED_PAYLOAD_KB * 1024;
|
|
|
|
// Schema for URL parameters (expecting string from URL)
|
|
export const secretsIdParamSchema = z.object({
|
|
id: z.string(),
|
|
});
|
|
|
|
// Schema for query parameters (expecting strings from URL)
|
|
export const secretsQuerySchema = z.object({
|
|
page: z
|
|
.string()
|
|
.optional()
|
|
.refine((val) => val === undefined || /^\d+$/.test(val), {
|
|
message: 'Page must be a positive integer string',
|
|
}),
|
|
limit: z
|
|
.string()
|
|
.optional()
|
|
.refine((val) => val === undefined || /^\d+$/.test(val), {
|
|
message: 'Limit must be a positive integer string',
|
|
}),
|
|
});
|
|
|
|
const jsonToUint8ArraySchema = z.preprocess(
|
|
(arg) => {
|
|
if (arg && typeof arg === 'object' && !Array.isArray(arg)) {
|
|
const values = Object.values(arg);
|
|
if (values.length > MAX_ENCRYPTED_SIZE) {
|
|
return arg; // Let the refine below catch the size error
|
|
}
|
|
|
|
return new Uint8Array(values);
|
|
}
|
|
|
|
return arg;
|
|
},
|
|
z.instanceof(Uint8Array).refine((arr) => arr.length <= MAX_ENCRYPTED_SIZE, {
|
|
message: `Encrypted payload exceeds maximum size of ${MAX_ENCRYPTED_PAYLOAD_KB} KB`,
|
|
})
|
|
);
|
|
|
|
const secretSchema = {
|
|
salt: z.string(),
|
|
secret: jsonToUint8ArraySchema,
|
|
title: jsonToUint8ArraySchema.optional().nullable(),
|
|
password: z.string().optional(),
|
|
expiresAt: z
|
|
.number()
|
|
.refine(
|
|
(val) =>
|
|
EXPIRATION_TIMES_SECONDS.includes(val as (typeof EXPIRATION_TIMES_SECONDS)[number]),
|
|
{
|
|
message: 'Invalid expiration time',
|
|
}
|
|
),
|
|
views: z.number().int().min(1).max(9999).optional(),
|
|
isBurnable: z.boolean().default(true).optional(),
|
|
ipRange: z
|
|
.string()
|
|
.refine((val) => isCidr(val) || isIP(val), {
|
|
message: 'Must be a valid IPv4, IPv6, or CIDR',
|
|
})
|
|
.nullable()
|
|
.optional(),
|
|
fileIds: z.array(z.string()).optional(),
|
|
};
|
|
|
|
export const createSecretsSchema = z.object(secretSchema);
|
|
|
|
export const getSecretSchema = z.object({
|
|
password: z.string().optional(),
|
|
});
|
|
|
|
const internalQueryParamsSchema = z.object({
|
|
skip: z.number().int().min(0).optional(),
|
|
take: z.number().int().min(1).max(100).optional(),
|
|
page: z.number().int().min(1).optional(),
|
|
limit: z.number().int().min(1).max(100).optional(),
|
|
});
|
|
|
|
interface ProcessedSecretsQueryParams {
|
|
skip: number;
|
|
take: number;
|
|
}
|
|
|
|
export const processSecretsQueryParams = (
|
|
query: z.infer<typeof secretsQuerySchema>
|
|
): ProcessedSecretsQueryParams => {
|
|
const page = query.page ? parseInt(query.page, 10) : undefined;
|
|
const limit = query.limit ? parseInt(query.limit, 10) : undefined;
|
|
const take = limit && limit > 0 && limit <= 100 ? limit : 10; // Guaranteed number
|
|
const skip = page && page > 0 ? (page - 1) * take : 0; // Guaranteed number
|
|
|
|
// Optional: Validate other params if needed, but we already have skip/take
|
|
const parseResult = internalQueryParamsSchema.safeParse({
|
|
skip,
|
|
take,
|
|
page,
|
|
limit,
|
|
});
|
|
|
|
if (!parseResult.success) {
|
|
// Log error but return defaults for pagination
|
|
console.error('secrets query parameter processing error:', parseResult.error);
|
|
return { skip: 0, take: 10 };
|
|
}
|
|
|
|
return { skip, take };
|
|
};
|