@open-mercato/shared 0.6.5-develop.5200.1.871eca3402 → 0.6.5-develop.5212.1.b47932beef
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/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/entities/validation.js +14 -1
- package/dist/modules/entities/validation.js.map +2 -2
- package/package.json +3 -3
- package/src/modules/entities/__tests__/validation.test.ts +37 -0
- package/src/modules/entities/validation.ts +20 -2
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.5-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.5-develop.5212.1.b47932beef'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -36,8 +36,21 @@ const validationRuleSchema = z.discriminatedUnion("rule", [
|
|
|
36
36
|
})
|
|
37
37
|
]);
|
|
38
38
|
const validationRulesArraySchema = z.array(validationRuleSchema).max(32);
|
|
39
|
+
const isEmpty = (v) => v == null || typeof v === "string" && v.trim() === "" || Array.isArray(v) && v.length === 0;
|
|
39
40
|
function evalRule(rule, value, kind) {
|
|
40
|
-
|
|
41
|
+
if (rule.rule === "required") {
|
|
42
|
+
return isEmpty(value) ? rule.message : null;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
for (const item of value) {
|
|
46
|
+
const msg = evalScalarRule(rule, item, kind);
|
|
47
|
+
if (msg) return msg;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return evalScalarRule(rule, value, kind);
|
|
52
|
+
}
|
|
53
|
+
function evalScalarRule(rule, value, kind) {
|
|
41
54
|
switch (rule.rule) {
|
|
42
55
|
case "required":
|
|
43
56
|
return isEmpty(value) ? rule.message : null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/entities/validation.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport { testLinearRegex } from '../../lib/regex/linear'\n\nexport const MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH = 500\nexport const MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH = 10_000\nexport const MAX_CUSTOM_FIELD_KEYS_PER_RECORD = 128\nexport const UNKNOWN_CUSTOM_FIELD_ERROR = '[internal] Unknown custom field'\nexport const TOO_MANY_CUSTOM_FIELDS_ERROR = '[internal] Too many custom fields'\n\n// Supported rule types for custom fields validation\nexport const VALIDATION_RULES = [\n 'required',\n 'date',\n 'integer',\n 'float',\n 'lt',\n 'lte',\n 'gt',\n 'gte',\n 'eq',\n 'ne',\n 'regex',\n] as const\n\nexport type ValidationRuleKind = typeof VALIDATION_RULES[number]\n\nexport const validationRuleSchema = z.discriminatedUnion('rule', [\n z.object({ rule: z.literal('required'), message: z.string().min(1) }),\n z.object({ rule: z.literal('date'), message: z.string().min(1) }),\n z.object({ rule: z.literal('integer'), message: z.string().min(1) }),\n z.object({ rule: z.literal('float'), message: z.string().min(1) }),\n z.object({ rule: z.literal('lt'), param: z.number(), message: z.string().min(1) }),\n z.object({ rule: z.literal('lte'), param: z.number(), message: z.string().min(1) }),\n z.object({ rule: z.literal('gt'), param: z.number(), message: z.string().min(1) }),\n z.object({ rule: z.literal('gte'), param: z.number(), message: z.string().min(1) }),\n z.object({ rule: z.literal('eq'), param: z.any(), message: z.string().min(1) }),\n z.object({ rule: z.literal('ne'), param: z.any(), message: z.string().min(1) }),\n z.object({\n rule: z.literal('regex'),\n param: z.string().min(1),\n message: z.string().min(1),\n }),\n])\n\nexport type ValidationRule = z.infer<typeof validationRuleSchema>\n\nexport const validationRulesArraySchema = z.array(validationRuleSchema).max(32)\n\nexport type CustomFieldDefLike = {\n key: string\n kind: string\n configJson?: any\n}\n\n// Evaluate a single rule against a value\nfunction evalRule(rule: ValidationRule, value: any, kind: string): string | null {\n
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAEzB,MAAM,wCAAwC;AAC9C,MAAM,sCAAsC;AAC5C,MAAM,mCAAmC;AACzC,MAAM,6BAA6B;AACnC,MAAM,+BAA+B;AAGrC,MAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,MAAM,uBAAuB,EAAE,mBAAmB,QAAQ;AAAA,EAC/D,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,UAAU,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EACpE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EAChE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAS,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EACnE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EACjE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,IAAI,GAAG,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EACjF,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EAClF,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,IAAI,GAAG,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EACjF,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EAClF,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,IAAI,GAAG,OAAO,EAAE,IAAI,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EAC9E,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,IAAI,GAAG,OAAO,EAAE,IAAI,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,EAC9E,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,OAAO;AAAA,IACvB,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,CAAC;AACH,CAAC;AAIM,MAAM,6BAA6B,EAAE,MAAM,oBAAoB,EAAE,IAAI,EAAE;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { testLinearRegex } from '../../lib/regex/linear'\n\nexport const MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH = 500\nexport const MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH = 10_000\nexport const MAX_CUSTOM_FIELD_KEYS_PER_RECORD = 128\nexport const UNKNOWN_CUSTOM_FIELD_ERROR = '[internal] Unknown custom field'\nexport const TOO_MANY_CUSTOM_FIELDS_ERROR = '[internal] Too many custom fields'\n\n// Supported rule types for custom fields validation\nexport const VALIDATION_RULES = [\n 'required',\n 'date',\n 'integer',\n 'float',\n 'lt',\n 'lte',\n 'gt',\n 'gte',\n 'eq',\n 'ne',\n 'regex',\n] as const\n\nexport type ValidationRuleKind = typeof VALIDATION_RULES[number]\n\nexport const validationRuleSchema = z.discriminatedUnion('rule', [\n z.object({ rule: z.literal('required'), message: z.string().min(1) }),\n z.object({ rule: z.literal('date'), message: z.string().min(1) }),\n z.object({ rule: z.literal('integer'), message: z.string().min(1) }),\n z.object({ rule: z.literal('float'), message: z.string().min(1) }),\n z.object({ rule: z.literal('lt'), param: z.number(), message: z.string().min(1) }),\n z.object({ rule: z.literal('lte'), param: z.number(), message: z.string().min(1) }),\n z.object({ rule: z.literal('gt'), param: z.number(), message: z.string().min(1) }),\n z.object({ rule: z.literal('gte'), param: z.number(), message: z.string().min(1) }),\n z.object({ rule: z.literal('eq'), param: z.any(), message: z.string().min(1) }),\n z.object({ rule: z.literal('ne'), param: z.any(), message: z.string().min(1) }),\n z.object({\n rule: z.literal('regex'),\n param: z.string().min(1),\n message: z.string().min(1),\n }),\n])\n\nexport type ValidationRule = z.infer<typeof validationRuleSchema>\n\nexport const validationRulesArraySchema = z.array(validationRuleSchema).max(32)\n\nexport type CustomFieldDefLike = {\n key: string\n kind: string\n configJson?: any\n}\n\nconst isEmpty = (v: any) => v == null || (typeof v === 'string' && v.trim() === '') || (Array.isArray(v) && v.length === 0)\n\n// Evaluate a single rule against a value. Multi-value fields (e.g. `text` with\n// `multi: true`) carry array values; every rule except `required` is applied to\n// each element so a regex like `^[a-z0-9_-]+$` is checked per tag instead of the\n// comma-joined string representation.\nfunction evalRule(rule: ValidationRule, value: any, kind: string): string | null {\n if (rule.rule === 'required') {\n return isEmpty(value) ? rule.message : null\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n const msg = evalScalarRule(rule, item, kind)\n if (msg) return msg\n }\n return null\n }\n return evalScalarRule(rule, value, kind)\n}\n\n// Evaluate a single rule against a scalar (non-array) value.\nfunction evalScalarRule(rule: ValidationRule, value: any, kind: string): string | null {\n switch (rule.rule) {\n case 'required':\n return isEmpty(value) ? rule.message : null\n case 'date': {\n if (isEmpty(value)) return null\n const d = new Date(String(value))\n return isNaN(d.getTime()) ? rule.message : null\n }\n case 'integer': {\n if (isEmpty(value)) return null\n const n = Number(value)\n return Number.isInteger(n) ? null : rule.message\n }\n case 'float': {\n if (isEmpty(value)) return null\n const n = Number(value)\n return Number.isFinite(n) ? null : rule.message\n }\n case 'lt':\n if (isEmpty(value)) return null\n return Number(value) < (rule as any).param ? null : rule.message\n case 'lte':\n if (isEmpty(value)) return null\n return Number(value) <= (rule as any).param ? null : rule.message\n case 'gt':\n if (isEmpty(value)) return null\n return Number(value) > (rule as any).param ? null : rule.message\n case 'gte':\n if (isEmpty(value)) return null\n return Number(value) >= (rule as any).param ? null : rule.message\n case 'eq':\n if (isEmpty(value)) return null\n return value === (rule as any).param ? null : rule.message\n case 'ne':\n if (isEmpty(value)) return null\n return value !== (rule as any).param ? null : rule.message\n case 'regex':\n if (isEmpty(value)) return null\n const regexResult = testLinearRegex(String((rule as any).param), String(value), {\n maxPatternLength: MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH,\n maxInputLength: MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH,\n })\n return regexResult.ok && regexResult.matched ? null : rule.message\n default:\n return null\n }\n}\n\nfunction countPresentValueKeys(values: Record<string, unknown>): number {\n let count = 0\n for (const key of Object.keys(values)) {\n if (values[key] !== undefined) count++\n }\n return count\n}\n\nexport type ValidateValuesOptions = {\n // When true, value keys that have no matching CustomFieldDef are rejected\n // (OWASP A03/A04 EAV mass-assignment guard for untrusted entry points such as\n // the generic `/api/entities/records` endpoint). Trusted first-party command\n // writes persist dynamic/internal keys, so they leave this off and rely on the\n // always-on per-record key cap below as the unbounded-injection backstop.\n rejectUndeclaredKeys?: boolean\n}\n\nexport function validateValuesAgainstDefs(\n values: Record<string, any>,\n defs: CustomFieldDefLike[],\n options: ValidateValuesOptions = {},\n): { ok: boolean; fieldErrors: Record<string, string> } {\n const errors: Record<string, string> = {}\n\n if (options.rejectUndeclaredKeys) {\n const allowedKeys = new Set(defs.map((def) => def.key))\n for (const key of Object.keys(values)) {\n if (values[key] === undefined) continue\n if (!allowedKeys.has(key)) {\n errors[`cf_${key}`] = UNKNOWN_CUSTOM_FIELD_ERROR\n }\n }\n }\n\n if (countPresentValueKeys(values) > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n errors._customFields = TOO_MANY_CUSTOM_FIELDS_ERROR\n }\n\n for (const def of defs) {\n const cfg = def?.configJson || {}\n const rules: ValidationRule[] = Array.isArray(cfg.validation) ? cfg.validation : []\n if (rules.length === 0) continue\n const value = values[def.key]\n for (const r of rules) {\n const msg = evalRule(r as any, value, def.kind)\n if (msg) {\n errors[`cf_${def.key}`] = msg\n break\n }\n }\n }\n return { ok: Object.keys(errors).length === 0, fieldErrors: errors }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.5212.1.b47932beef",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -92,9 +92,9 @@
|
|
|
92
92
|
"@mikro-orm/core": "^7.1.4",
|
|
93
93
|
"@mikro-orm/decorators": "^7.1.4",
|
|
94
94
|
"@mikro-orm/postgresql": "^7.1.4",
|
|
95
|
-
"@open-mercato/cache": "0.6.5-develop.
|
|
95
|
+
"@open-mercato/cache": "0.6.5-develop.5212.1.b47932beef",
|
|
96
96
|
"dotenv": "^17.4.2",
|
|
97
|
-
"rate-limiter-flexible": "^11.
|
|
97
|
+
"rate-limiter-flexible": "^11.2.0",
|
|
98
98
|
"re2js": "2.8.3",
|
|
99
99
|
"reflect-metadata": "^0.2.2",
|
|
100
100
|
"sanitize-html": "^2.17.4",
|
|
@@ -56,6 +56,43 @@ describe('validateValuesAgainstDefs', () => {
|
|
|
56
56
|
expect(r.ok).toBe(true)
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
+
it('applies value rules per-element for multi-value (array) fields', () => {
|
|
60
|
+
const defs = [
|
|
61
|
+
{ key: 'labels', kind: 'text', configJson: { validation: [ { rule: 'regex', param: '^[a-z0-9_-]+$', message: 'Labels must be slug-like' } ] } },
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
// Multiple slug-like labels must pass — the regex is checked per element,
|
|
65
|
+
// not against the comma-joined string representation (issue #2650).
|
|
66
|
+
let r = validateValuesAgainstDefs({ labels: ['frontend', 'backend', 'ops'] }, defs as any)
|
|
67
|
+
expect(r.ok).toBe(true)
|
|
68
|
+
expect(Object.keys(r.fieldErrors).length).toBe(0)
|
|
69
|
+
|
|
70
|
+
// A single slug-like label still passes.
|
|
71
|
+
r = validateValuesAgainstDefs({ labels: ['frontend'] }, defs as any)
|
|
72
|
+
expect(r.ok).toBe(true)
|
|
73
|
+
|
|
74
|
+
// Any non-conforming element fails the whole field.
|
|
75
|
+
r = validateValuesAgainstDefs({ labels: ['frontend', 'Not Valid!'] }, defs as any)
|
|
76
|
+
expect(r.ok).toBe(false)
|
|
77
|
+
expect(r.fieldErrors['cf_labels']).toBe('Labels must be slug-like')
|
|
78
|
+
|
|
79
|
+
// An empty array is treated as empty (no value rules apply).
|
|
80
|
+
r = validateValuesAgainstDefs({ labels: [] }, defs as any)
|
|
81
|
+
expect(r.ok).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('enforces required on multi-value fields by array emptiness, not per-element', () => {
|
|
85
|
+
const defs = [
|
|
86
|
+
{ key: 'labels', kind: 'text', configJson: { validation: [ { rule: 'required', message: 'labels required' } ] } },
|
|
87
|
+
]
|
|
88
|
+
let r = validateValuesAgainstDefs({ labels: [] }, defs as any)
|
|
89
|
+
expect(r.ok).toBe(false)
|
|
90
|
+
expect(r.fieldErrors['cf_labels']).toBe('labels required')
|
|
91
|
+
|
|
92
|
+
r = validateValuesAgainstDefs({ labels: ['frontend', 'backend'] }, defs as any)
|
|
93
|
+
expect(r.ok).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
|
|
59
96
|
it('evaluates dangerous backtracking regex rules in bounded time', () => {
|
|
60
97
|
const defs = [
|
|
61
98
|
{
|
|
@@ -52,10 +52,28 @@ export type CustomFieldDefLike = {
|
|
|
52
52
|
configJson?: any
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const isEmpty = (v: any) => v == null || (typeof v === 'string' && v.trim() === '') || (Array.isArray(v) && v.length === 0)
|
|
56
|
+
|
|
57
|
+
// Evaluate a single rule against a value. Multi-value fields (e.g. `text` with
|
|
58
|
+
// `multi: true`) carry array values; every rule except `required` is applied to
|
|
59
|
+
// each element so a regex like `^[a-z0-9_-]+$` is checked per tag instead of the
|
|
60
|
+
// comma-joined string representation.
|
|
56
61
|
function evalRule(rule: ValidationRule, value: any, kind: string): string | null {
|
|
57
|
-
|
|
62
|
+
if (rule.rule === 'required') {
|
|
63
|
+
return isEmpty(value) ? rule.message : null
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
for (const item of value) {
|
|
67
|
+
const msg = evalScalarRule(rule, item, kind)
|
|
68
|
+
if (msg) return msg
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
return evalScalarRule(rule, value, kind)
|
|
73
|
+
}
|
|
58
74
|
|
|
75
|
+
// Evaluate a single rule against a scalar (non-array) value.
|
|
76
|
+
function evalScalarRule(rule: ValidationRule, value: any, kind: string): string | null {
|
|
59
77
|
switch (rule.rule) {
|
|
60
78
|
case 'required':
|
|
61
79
|
return isEmpty(value) ? rule.message : null
|