forlogic-core 2.3.5 → 2.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.note/memory/security/lib-hardening-roadmap.md +45 -0
- package/dist/assets/index.d.ts +2 -2
- package/dist/components/modules/ModulesContent.d.ts +3 -1
- package/dist/components/modules/ModulesDialog.d.ts +1 -1
- package/dist/components/modules/types.d.ts +2 -0
- package/dist/edge/index.d.ts +98 -0
- package/dist/edge/index.esm.js +124 -0
- package/dist/edge/index.js +134 -0
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1 -1
- package/dist/providers/CoreProviders.d.ts +16 -1
- package/dist/storage/index.d.ts +57 -0
- package/dist/storage/index.esm.js +107 -0
- package/dist/storage/index.js +115 -0
- package/dist/utils/generateId.d.ts +15 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/sanitizeError.d.ts +9 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.esm.js +13 -0
- package/dist/validation/index.js +20 -0
- package/dist/validation/schemas.d.ts +26 -0
- package/dist/vite/index.esm.js +10 -1
- package/dist/vite/index.js +10 -1
- package/docs/SECURITY.md +245 -0
- package/docs/WORKSPACE_KNOWLEDGE.md +1 -0
- package/package.json +17 -1
|
@@ -82,7 +82,22 @@ export interface CoreProvidersProps {
|
|
|
82
82
|
* @default 'supabase'
|
|
83
83
|
*/
|
|
84
84
|
backend?: BackendMode;
|
|
85
|
+
/**
|
|
86
|
+
* Modo de segurança da lib.
|
|
87
|
+
*
|
|
88
|
+
* - `'legacy'` (default): comportamento histórico — JWT em localStorage, CORS permissivo,
|
|
89
|
+
* mensagens de erro verbose. Mantém compatibilidade com todos os consumidores atuais.
|
|
90
|
+
* - `'strict'`: ativa antecipadamente os comportamentos do hardening v2:
|
|
91
|
+
* mensagens de erro sanitizadas SEMPRE (mesmo em DEV), refresh de token agressivo,
|
|
92
|
+
* warnings extras no console para padrões inseguros (apikey em URL, etc).
|
|
93
|
+
*
|
|
94
|
+
* Use `'strict'` em produção para validar antes da release v2.0.0 da lib.
|
|
95
|
+
*
|
|
96
|
+
* @default 'legacy'
|
|
97
|
+
*/
|
|
98
|
+
securityMode?: 'legacy' | 'strict';
|
|
85
99
|
}
|
|
100
|
+
export declare function getSecurityMode(): 'legacy' | 'strict';
|
|
86
101
|
/**
|
|
87
102
|
* CoreProviders - Encapsulates all essential providers for forlogic-core applications
|
|
88
103
|
*
|
|
@@ -104,4 +119,4 @@ export interface CoreProvidersProps {
|
|
|
104
119
|
* }
|
|
105
120
|
* ```
|
|
106
121
|
*/
|
|
107
|
-
export declare function CoreProviders({ children, queryClient, moduleAlias, moduleAccessGuardProps, appTranslations, clarityProjectId, clarityMode, backend, }: CoreProvidersProps): import("react/jsx-runtime").JSX.Element;
|
|
122
|
+
export declare function CoreProviders({ children, queryClient, moduleAlias, moduleAccessGuardProps, appTranslations, clarityProjectId, clarityMode, backend, securityMode, }: CoreProvidersProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forlogic-core/storage — helpers de segurança para upload de arquivos.
|
|
3
|
+
*
|
|
4
|
+
* Cobre os pontos críticos do hardening:
|
|
5
|
+
* - Bucket privado por default + URL assinada de TTL curto.
|
|
6
|
+
* - Validação por magic bytes (não confiar em extensão/MIME do cliente).
|
|
7
|
+
* - Nome aleatório (evita path traversal e enumeration).
|
|
8
|
+
* - Allowlist de MIME por categoria.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SAFE_IMAGE_MIME: readonly ["image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"];
|
|
11
|
+
export declare const SAFE_DOCUMENT_MIME: readonly ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "text/plain", "text/csv"];
|
|
12
|
+
/**
|
|
13
|
+
* Buckets que SEMPRE devem ser privados (acesso via signed URL).
|
|
14
|
+
* Lista canônica para auditoria cross-projeto.
|
|
15
|
+
*/
|
|
16
|
+
export declare const RECOMMENDED_PRIVATE_BUCKETS: readonly ["imports", "contracts", "user-uploads", "resumes", "pdi-uploads", "performance", "knowledge-files", "trainings"];
|
|
17
|
+
/**
|
|
18
|
+
* Lê os primeiros bytes do arquivo e retorna o MIME real (ou `null` se desconhecido).
|
|
19
|
+
* Lança erro se detectar formato perigoso (executável).
|
|
20
|
+
*/
|
|
21
|
+
export declare function detectMimeFromMagicBytes(file: Blob | File): Promise<string | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Valida que o arquivo casa com pelo menos um dos MIMEs permitidos (via magic bytes).
|
|
24
|
+
* Não confia no `file.type` (que vem do cliente e é falsificável).
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateMagicBytes(file: Blob | File, allowed: readonly string[]): Promise<{
|
|
27
|
+
ok: true;
|
|
28
|
+
mime: string;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
reason: string;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Gera um nome de arquivo aleatório e seguro, preservando a extensão original.
|
|
35
|
+
* Remove path traversal, caracteres de controle e nomes reservados.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sanitizeAndRandomizeFilename(originalName: string): string;
|
|
38
|
+
interface SupabaseLikeStorage {
|
|
39
|
+
from(bucket: string): {
|
|
40
|
+
createSignedUrl(path: string, expiresIn: number): Promise<{
|
|
41
|
+
data: {
|
|
42
|
+
signedUrl: string;
|
|
43
|
+
} | null;
|
|
44
|
+
error: {
|
|
45
|
+
message: string;
|
|
46
|
+
} | null;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wrapper sobre `supabase.storage.from(bucket).createSignedUrl`.
|
|
52
|
+
* Default TTL de 5 minutos — força chamadas curtas e auditáveis.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getSignedUrl(client: {
|
|
55
|
+
storage: SupabaseLikeStorage;
|
|
56
|
+
}, bucket: string, path: string, ttlSeconds?: number): Promise<string>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
function generateId(kind = "uuid") {
|
|
2
|
+
if (kind === "short") {
|
|
3
|
+
const bytes2 = new Uint8Array(9);
|
|
4
|
+
crypto.getRandomValues(bytes2);
|
|
5
|
+
return Array.from(bytes2, (b) => b.toString(36).padStart(2, "0")).join("").slice(0, 12);
|
|
6
|
+
}
|
|
7
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
8
|
+
return crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
const bytes = new Uint8Array(16);
|
|
11
|
+
crypto.getRandomValues(bytes);
|
|
12
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
13
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
14
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SAFE_IMAGE_MIME = [
|
|
19
|
+
"image/jpeg",
|
|
20
|
+
"image/png",
|
|
21
|
+
"image/webp",
|
|
22
|
+
"image/gif",
|
|
23
|
+
"image/svg+xml"
|
|
24
|
+
];
|
|
25
|
+
const SAFE_DOCUMENT_MIME = [
|
|
26
|
+
"application/pdf",
|
|
27
|
+
"application/msword",
|
|
28
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
29
|
+
"application/vnd.ms-excel",
|
|
30
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
31
|
+
"application/vnd.ms-powerpoint",
|
|
32
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
33
|
+
"text/plain",
|
|
34
|
+
"text/csv"
|
|
35
|
+
];
|
|
36
|
+
const RECOMMENDED_PRIVATE_BUCKETS = [
|
|
37
|
+
"imports",
|
|
38
|
+
"contracts",
|
|
39
|
+
"user-uploads",
|
|
40
|
+
"resumes",
|
|
41
|
+
"pdi-uploads",
|
|
42
|
+
"performance",
|
|
43
|
+
"knowledge-files",
|
|
44
|
+
"trainings"
|
|
45
|
+
];
|
|
46
|
+
const SIGNATURES = [
|
|
47
|
+
{ mime: "image/jpeg", offset: 0, bytes: [255, 216, 255] },
|
|
48
|
+
{ mime: "image/png", offset: 0, bytes: [137, 80, 78, 71] },
|
|
49
|
+
{ mime: "image/gif", offset: 0, bytes: [71, 73, 70, 56] },
|
|
50
|
+
{ mime: "image/webp", offset: 8, bytes: [87, 69, 66, 80] },
|
|
51
|
+
{ mime: "application/pdf", offset: 0, bytes: [37, 80, 68, 70] },
|
|
52
|
+
{ mime: "application/zip", offset: 0, bytes: [80, 75, 3, 4] }
|
|
53
|
+
// .docx/.xlsx/.pptx
|
|
54
|
+
];
|
|
55
|
+
const DANGEROUS_SIGNATURES = [
|
|
56
|
+
{ mime: "application/x-msdownload", offset: 0, bytes: [77, 90] },
|
|
57
|
+
// PE/EXE
|
|
58
|
+
{ mime: "application/x-elf", offset: 0, bytes: [127, 69, 76, 70] }
|
|
59
|
+
// ELF
|
|
60
|
+
];
|
|
61
|
+
function matches(buf, sig) {
|
|
62
|
+
for (let i = 0; i < sig.bytes.length; i++) {
|
|
63
|
+
if (buf[sig.offset + i] !== sig.bytes[i]) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
async function detectMimeFromMagicBytes(file) {
|
|
68
|
+
const buf = new Uint8Array(await file.slice(0, 16).arrayBuffer());
|
|
69
|
+
for (const sig of DANGEROUS_SIGNATURES) {
|
|
70
|
+
if (matches(buf, sig)) {
|
|
71
|
+
throw new Error(`Arquivo execut\xE1vel detectado (${sig.mime}). Upload bloqueado.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const sig of SIGNATURES) {
|
|
75
|
+
if (matches(buf, sig)) return sig.mime;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
async function validateMagicBytes(file, allowed) {
|
|
80
|
+
try {
|
|
81
|
+
const detected = await detectMimeFromMagicBytes(file);
|
|
82
|
+
if (!detected) return { ok: false, reason: "Tipo de arquivo n\xE3o reconhecido" };
|
|
83
|
+
if (!allowed.includes(detected)) {
|
|
84
|
+
return { ok: false, reason: `Tipo ${detected} n\xE3o permitido` };
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, mime: detected };
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return { ok: false, reason: e instanceof Error ? e.message : "Valida\xE7\xE3o falhou" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function sanitizeAndRandomizeFilename(originalName) {
|
|
92
|
+
const lastDot = originalName.lastIndexOf(".");
|
|
93
|
+
const ext = lastDot > 0 ? originalName.slice(lastDot + 1).toLowerCase() : "";
|
|
94
|
+
const safeExt = ext.replace(/[^a-z0-9]/g, "").slice(0, 8);
|
|
95
|
+
const random = generateId("short");
|
|
96
|
+
return safeExt ? `${random}.${safeExt}` : random;
|
|
97
|
+
}
|
|
98
|
+
async function getSignedUrl(client, bucket, path, ttlSeconds = 300) {
|
|
99
|
+
if (ttlSeconds > 3600) {
|
|
100
|
+
throw new Error("TTL > 1h n\xE3o \xE9 permitido por padr\xE3o. Justifique no call-site.");
|
|
101
|
+
}
|
|
102
|
+
const { data, error } = await client.storage.from(bucket).createSignedUrl(path, ttlSeconds);
|
|
103
|
+
if (error || !data) throw new Error(error?.message ?? "Falha ao gerar signed URL");
|
|
104
|
+
return data.signedUrl;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { RECOMMENDED_PRIVATE_BUCKETS, SAFE_DOCUMENT_MIME, SAFE_IMAGE_MIME, detectMimeFromMagicBytes, getSignedUrl, sanitizeAndRandomizeFilename, validateMagicBytes };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function generateId(kind = "uuid") {
|
|
4
|
+
if (kind === "short") {
|
|
5
|
+
const bytes2 = new Uint8Array(9);
|
|
6
|
+
crypto.getRandomValues(bytes2);
|
|
7
|
+
return Array.from(bytes2, (b) => b.toString(36).padStart(2, "0")).join("").slice(0, 12);
|
|
8
|
+
}
|
|
9
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
10
|
+
return crypto.randomUUID();
|
|
11
|
+
}
|
|
12
|
+
const bytes = new Uint8Array(16);
|
|
13
|
+
crypto.getRandomValues(bytes);
|
|
14
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
15
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
16
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
17
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SAFE_IMAGE_MIME = [
|
|
21
|
+
"image/jpeg",
|
|
22
|
+
"image/png",
|
|
23
|
+
"image/webp",
|
|
24
|
+
"image/gif",
|
|
25
|
+
"image/svg+xml"
|
|
26
|
+
];
|
|
27
|
+
const SAFE_DOCUMENT_MIME = [
|
|
28
|
+
"application/pdf",
|
|
29
|
+
"application/msword",
|
|
30
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
31
|
+
"application/vnd.ms-excel",
|
|
32
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
33
|
+
"application/vnd.ms-powerpoint",
|
|
34
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
35
|
+
"text/plain",
|
|
36
|
+
"text/csv"
|
|
37
|
+
];
|
|
38
|
+
const RECOMMENDED_PRIVATE_BUCKETS = [
|
|
39
|
+
"imports",
|
|
40
|
+
"contracts",
|
|
41
|
+
"user-uploads",
|
|
42
|
+
"resumes",
|
|
43
|
+
"pdi-uploads",
|
|
44
|
+
"performance",
|
|
45
|
+
"knowledge-files",
|
|
46
|
+
"trainings"
|
|
47
|
+
];
|
|
48
|
+
const SIGNATURES = [
|
|
49
|
+
{ mime: "image/jpeg", offset: 0, bytes: [255, 216, 255] },
|
|
50
|
+
{ mime: "image/png", offset: 0, bytes: [137, 80, 78, 71] },
|
|
51
|
+
{ mime: "image/gif", offset: 0, bytes: [71, 73, 70, 56] },
|
|
52
|
+
{ mime: "image/webp", offset: 8, bytes: [87, 69, 66, 80] },
|
|
53
|
+
{ mime: "application/pdf", offset: 0, bytes: [37, 80, 68, 70] },
|
|
54
|
+
{ mime: "application/zip", offset: 0, bytes: [80, 75, 3, 4] }
|
|
55
|
+
// .docx/.xlsx/.pptx
|
|
56
|
+
];
|
|
57
|
+
const DANGEROUS_SIGNATURES = [
|
|
58
|
+
{ mime: "application/x-msdownload", offset: 0, bytes: [77, 90] },
|
|
59
|
+
// PE/EXE
|
|
60
|
+
{ mime: "application/x-elf", offset: 0, bytes: [127, 69, 76, 70] }
|
|
61
|
+
// ELF
|
|
62
|
+
];
|
|
63
|
+
function matches(buf, sig) {
|
|
64
|
+
for (let i = 0; i < sig.bytes.length; i++) {
|
|
65
|
+
if (buf[sig.offset + i] !== sig.bytes[i]) return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
async function detectMimeFromMagicBytes(file) {
|
|
70
|
+
const buf = new Uint8Array(await file.slice(0, 16).arrayBuffer());
|
|
71
|
+
for (const sig of DANGEROUS_SIGNATURES) {
|
|
72
|
+
if (matches(buf, sig)) {
|
|
73
|
+
throw new Error(`Arquivo execut\xE1vel detectado (${sig.mime}). Upload bloqueado.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const sig of SIGNATURES) {
|
|
77
|
+
if (matches(buf, sig)) return sig.mime;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
async function validateMagicBytes(file, allowed) {
|
|
82
|
+
try {
|
|
83
|
+
const detected = await detectMimeFromMagicBytes(file);
|
|
84
|
+
if (!detected) return { ok: false, reason: "Tipo de arquivo n\xE3o reconhecido" };
|
|
85
|
+
if (!allowed.includes(detected)) {
|
|
86
|
+
return { ok: false, reason: `Tipo ${detected} n\xE3o permitido` };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, mime: detected };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return { ok: false, reason: e instanceof Error ? e.message : "Valida\xE7\xE3o falhou" };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function sanitizeAndRandomizeFilename(originalName) {
|
|
94
|
+
const lastDot = originalName.lastIndexOf(".");
|
|
95
|
+
const ext = lastDot > 0 ? originalName.slice(lastDot + 1).toLowerCase() : "";
|
|
96
|
+
const safeExt = ext.replace(/[^a-z0-9]/g, "").slice(0, 8);
|
|
97
|
+
const random = generateId("short");
|
|
98
|
+
return safeExt ? `${random}.${safeExt}` : random;
|
|
99
|
+
}
|
|
100
|
+
async function getSignedUrl(client, bucket, path, ttlSeconds = 300) {
|
|
101
|
+
if (ttlSeconds > 3600) {
|
|
102
|
+
throw new Error("TTL > 1h n\xE3o \xE9 permitido por padr\xE3o. Justifique no call-site.");
|
|
103
|
+
}
|
|
104
|
+
const { data, error } = await client.storage.from(bucket).createSignedUrl(path, ttlSeconds);
|
|
105
|
+
if (error || !data) throw new Error(error?.message ?? "Falha ao gerar signed URL");
|
|
106
|
+
return data.signedUrl;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
exports.RECOMMENDED_PRIVATE_BUCKETS = RECOMMENDED_PRIVATE_BUCKETS;
|
|
110
|
+
exports.SAFE_DOCUMENT_MIME = SAFE_DOCUMENT_MIME;
|
|
111
|
+
exports.SAFE_IMAGE_MIME = SAFE_IMAGE_MIME;
|
|
112
|
+
exports.detectMimeFromMagicBytes = detectMimeFromMagicBytes;
|
|
113
|
+
exports.getSignedUrl = getSignedUrl;
|
|
114
|
+
exports.sanitizeAndRandomizeFilename = sanitizeAndRandomizeFilename;
|
|
115
|
+
exports.validateMagicBytes = validateMagicBytes;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gerador de IDs criptograficamente seguros.
|
|
3
|
+
*
|
|
4
|
+
* Substitui o padrão antigo `Date.now().toString()`, que é previsível,
|
|
5
|
+
* colide sob alta concorrência e revela timestamps internos.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const id = generateId(); // UUID v4 padrão
|
|
9
|
+
* const short = generateId('short'); // 12 chars base36 (não-criptográfico)
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateId(kind?: 'uuid' | 'short'): string;
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Use `generateId()`. `Date.now()` como ID é previsível e colide.
|
|
14
|
+
*/
|
|
15
|
+
export declare const timestampId: () => string;
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export declare const debounce: <T extends (...args: any[]) => any>(func: T, wait
|
|
|
7
7
|
export declare const slugify: (text: string) => string;
|
|
8
8
|
export { generatePastelBg, getLuminance, getContrastRatio } from './color';
|
|
9
9
|
export { handleExternalLink, buildModuleUrl } from './linkHelpers';
|
|
10
|
+
export { generateId, timestampId } from './generateId';
|
|
11
|
+
export { sanitizeErrorMessage, stripTechHeaders } from './sanitizeError';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retorna uma mensagem segura para exibir ao usuário final.
|
|
3
|
+
* Em DEV, retorna a mensagem original. Em PROD, mascara detalhes técnicos.
|
|
4
|
+
*/
|
|
5
|
+
export declare function sanitizeErrorMessage(error: unknown): string;
|
|
6
|
+
/**
|
|
7
|
+
* Remove campos técnicos de um objeto de erro antes de enviá-lo para logs externos.
|
|
8
|
+
*/
|
|
9
|
+
export declare function stripTechHeaders<T extends Record<string, unknown>>(payload: T): Partial<T>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './schemas';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const emailSchema = z.string().trim().toLowerCase().min(3, "E-mail muito curto").max(254, "E-mail muito longo").email("E-mail inv\xE1lido");
|
|
4
|
+
const aliasSchema = z.string().trim().toLowerCase().min(2, "Alias muito curto").max(64, "Alias muito longo").regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, "Alias cont\xE9m caracteres inv\xE1lidos");
|
|
5
|
+
const uuidSchema = z.string().uuid("UUID inv\xE1lido");
|
|
6
|
+
const userIdSchema = z.string().trim().min(1, "userId obrigat\xF3rio").max(64, "userId muito longo").regex(/^[a-zA-Z0-9._-]+$/, "userId cont\xE9m caracteres inv\xE1lidos");
|
|
7
|
+
const safeStringSchema = (max = 1e3) => z.string().trim().max(max, `Texto excede ${max} caracteres`).refine((v) => !/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(v), "Cont\xE9m caracteres inv\xE1lidos");
|
|
8
|
+
const paginationSchema = z.object({
|
|
9
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
10
|
+
pageSize: z.coerce.number().int().min(1).max(500).default(50)
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export { aliasSchema, emailSchema, paginationSchema, safeStringSchema, userIdSchema, uuidSchema };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
|
|
5
|
+
const emailSchema = zod.z.string().trim().toLowerCase().min(3, "E-mail muito curto").max(254, "E-mail muito longo").email("E-mail inv\xE1lido");
|
|
6
|
+
const aliasSchema = zod.z.string().trim().toLowerCase().min(2, "Alias muito curto").max(64, "Alias muito longo").regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, "Alias cont\xE9m caracteres inv\xE1lidos");
|
|
7
|
+
const uuidSchema = zod.z.string().uuid("UUID inv\xE1lido");
|
|
8
|
+
const userIdSchema = zod.z.string().trim().min(1, "userId obrigat\xF3rio").max(64, "userId muito longo").regex(/^[a-zA-Z0-9._-]+$/, "userId cont\xE9m caracteres inv\xE1lidos");
|
|
9
|
+
const safeStringSchema = (max = 1e3) => zod.z.string().trim().max(max, `Texto excede ${max} caracteres`).refine((v) => !/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(v), "Cont\xE9m caracteres inv\xE1lidos");
|
|
10
|
+
const paginationSchema = zod.z.object({
|
|
11
|
+
page: zod.z.coerce.number().int().min(1).default(1),
|
|
12
|
+
pageSize: zod.z.coerce.number().int().min(1).max(500).default(50)
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
exports.aliasSchema = aliasSchema;
|
|
16
|
+
exports.emailSchema = emailSchema;
|
|
17
|
+
exports.paginationSchema = paginationSchema;
|
|
18
|
+
exports.safeStringSchema = safeStringSchema;
|
|
19
|
+
exports.userIdSchema = userIdSchema;
|
|
20
|
+
exports.uuidSchema = uuidSchema;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schemas zod compartilhados para validações comuns em toda a stack
|
|
4
|
+
* (forms, edge functions, payloads de API).
|
|
5
|
+
*
|
|
6
|
+
* Centralizar reduz superfície de bugs e garante regras consistentes
|
|
7
|
+
* de hardening entre projetos que consomem `forlogic-core`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const emailSchema: z.ZodString;
|
|
10
|
+
export declare const aliasSchema: z.ZodString;
|
|
11
|
+
export declare const uuidSchema: z.ZodString;
|
|
12
|
+
/** IDs de usuário Qualiex são short alphanumeric (NUNCA UUIDs). */
|
|
13
|
+
export declare const userIdSchema: z.ZodString;
|
|
14
|
+
/** String sanitizada: sem caracteres de controle, tamanho limitado. */
|
|
15
|
+
export declare const safeStringSchema: (max?: number) => z.ZodEffects<z.ZodString, string, string>;
|
|
16
|
+
export declare const paginationSchema: z.ZodObject<{
|
|
17
|
+
page: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
pageSize: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
page?: number;
|
|
21
|
+
pageSize?: number;
|
|
22
|
+
}, {
|
|
23
|
+
page?: number;
|
|
24
|
+
pageSize?: number;
|
|
25
|
+
}>;
|
|
26
|
+
export type PaginationParams = z.infer<typeof paginationSchema>;
|
package/dist/vite/index.esm.js
CHANGED
|
@@ -137,6 +137,13 @@ function createSecurityHeadersPlugin(isDev, options = {}) {
|
|
|
137
137
|
configureServer(server) {
|
|
138
138
|
server.middlewares.use((req, res, next) => {
|
|
139
139
|
const origin = req.headers.origin;
|
|
140
|
+
const method = (req.method ?? "").toUpperCase();
|
|
141
|
+
if (method === "TRACE" || method === "CONNECT") {
|
|
142
|
+
res.statusCode = 405;
|
|
143
|
+
res.setHeader("Allow", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD");
|
|
144
|
+
res.end();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
140
147
|
if (origin && isOriginTrusted(origin, allTrustedOrigins)) {
|
|
141
148
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
142
149
|
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
@@ -149,12 +156,12 @@ function createSecurityHeadersPlugin(isDev, options = {}) {
|
|
|
149
156
|
"authorization, x-client-info, apikey, content-type, x-csrf-token, x-nonce"
|
|
150
157
|
);
|
|
151
158
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
152
|
-
res.setHeader("Vary", "Origin");
|
|
153
159
|
} else if (origin) {
|
|
154
160
|
console.warn(
|
|
155
161
|
`\u{1F6A8} CORS BLOCKED: Untrusted origin attempted access: ${origin}`
|
|
156
162
|
);
|
|
157
163
|
}
|
|
164
|
+
res.setHeader("Vary", "Origin");
|
|
158
165
|
res.setHeader(
|
|
159
166
|
"Strict-Transport-Security",
|
|
160
167
|
"max-age=31536000; includeSubDomains; preload"
|
|
@@ -167,6 +174,8 @@ function createSecurityHeadersPlugin(isDev, options = {}) {
|
|
|
167
174
|
"Permissions-Policy",
|
|
168
175
|
"camera=(), microphone=(), geolocation=(), fullscreen=(self), payment=(), display-capture=()"
|
|
169
176
|
);
|
|
177
|
+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
178
|
+
res.setHeader("Cross-Origin-Resource-Policy", "same-site");
|
|
170
179
|
res.setHeader("Content-Security-Policy", cspPolicy);
|
|
171
180
|
next();
|
|
172
181
|
});
|
package/dist/vite/index.js
CHANGED
|
@@ -139,6 +139,13 @@ function createSecurityHeadersPlugin(isDev, options = {}) {
|
|
|
139
139
|
configureServer(server) {
|
|
140
140
|
server.middlewares.use((req, res, next) => {
|
|
141
141
|
const origin = req.headers.origin;
|
|
142
|
+
const method = (req.method ?? "").toUpperCase();
|
|
143
|
+
if (method === "TRACE" || method === "CONNECT") {
|
|
144
|
+
res.statusCode = 405;
|
|
145
|
+
res.setHeader("Allow", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD");
|
|
146
|
+
res.end();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
142
149
|
if (origin && isOriginTrusted(origin, allTrustedOrigins)) {
|
|
143
150
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
144
151
|
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
@@ -151,12 +158,12 @@ function createSecurityHeadersPlugin(isDev, options = {}) {
|
|
|
151
158
|
"authorization, x-client-info, apikey, content-type, x-csrf-token, x-nonce"
|
|
152
159
|
);
|
|
153
160
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
154
|
-
res.setHeader("Vary", "Origin");
|
|
155
161
|
} else if (origin) {
|
|
156
162
|
console.warn(
|
|
157
163
|
`\u{1F6A8} CORS BLOCKED: Untrusted origin attempted access: ${origin}`
|
|
158
164
|
);
|
|
159
165
|
}
|
|
166
|
+
res.setHeader("Vary", "Origin");
|
|
160
167
|
res.setHeader(
|
|
161
168
|
"Strict-Transport-Security",
|
|
162
169
|
"max-age=31536000; includeSubDomains; preload"
|
|
@@ -169,6 +176,8 @@ function createSecurityHeadersPlugin(isDev, options = {}) {
|
|
|
169
176
|
"Permissions-Policy",
|
|
170
177
|
"camera=(), microphone=(), geolocation=(), fullscreen=(self), payment=(), display-capture=()"
|
|
171
178
|
);
|
|
179
|
+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
180
|
+
res.setHeader("Cross-Origin-Resource-Policy", "same-site");
|
|
172
181
|
res.setHeader("Content-Security-Policy", cspPolicy);
|
|
173
182
|
next();
|
|
174
183
|
});
|