@open-mercato/shared 0.6.4-develop.4331.1.64a8535120 → 0.6.4-develop.4358.1.233d5675c7

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.
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.4-develop.4331.1.64a8535120";
1
+ const APP_VERSION = "0.6.4-develop.4358.1.233d5675c7";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -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.4-develop.4331.1.64a8535120'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.4-develop.4358.1.233d5675c7'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,9 @@ import { z } from "zod";
2
2
  import { testLinearRegex } from "../../lib/regex/linear.js";
3
3
  const MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH = 500;
4
4
  const MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH = 1e4;
5
+ const MAX_CUSTOM_FIELD_KEYS_PER_RECORD = 128;
6
+ const UNKNOWN_CUSTOM_FIELD_ERROR = "[internal] Unknown custom field";
7
+ const TOO_MANY_CUSTOM_FIELDS_ERROR = "[internal] Too many custom fields";
5
8
  const VALIDATION_RULES = [
6
9
  "required",
7
10
  "date",
@@ -82,8 +85,27 @@ function evalRule(rule, value, kind) {
82
85
  return null;
83
86
  }
84
87
  }
85
- function validateValuesAgainstDefs(values, defs) {
88
+ function countPresentValueKeys(values) {
89
+ let count = 0;
90
+ for (const key of Object.keys(values)) {
91
+ if (values[key] !== void 0) count++;
92
+ }
93
+ return count;
94
+ }
95
+ function validateValuesAgainstDefs(values, defs, options = {}) {
86
96
  const errors = {};
97
+ if (options.rejectUndeclaredKeys) {
98
+ const allowedKeys = new Set(defs.map((def) => def.key));
99
+ for (const key of Object.keys(values)) {
100
+ if (values[key] === void 0) continue;
101
+ if (!allowedKeys.has(key)) {
102
+ errors[`cf_${key}`] = UNKNOWN_CUSTOM_FIELD_ERROR;
103
+ }
104
+ }
105
+ }
106
+ if (countPresentValueKeys(values) > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {
107
+ errors._customFields = TOO_MANY_CUSTOM_FIELDS_ERROR;
108
+ }
87
109
  for (const def of defs) {
88
110
  const cfg = def?.configJson || {};
89
111
  const rules = Array.isArray(cfg.validation) ? cfg.validation : [];
@@ -100,8 +122,11 @@ function validateValuesAgainstDefs(values, defs) {
100
122
  return { ok: Object.keys(errors).length === 0, fieldErrors: errors };
101
123
  }
102
124
  export {
125
+ MAX_CUSTOM_FIELD_KEYS_PER_RECORD,
103
126
  MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH,
104
127
  MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH,
128
+ TOO_MANY_CUSTOM_FIELDS_ERROR,
129
+ UNKNOWN_CUSTOM_FIELD_ERROR,
105
130
  VALIDATION_RULES,
106
131
  validateValuesAgainstDefs,
107
132
  validationRuleSchema,
@@ -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\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 const isEmpty = (v: any) => v == null || (typeof v === 'string' && v.trim() === '') || (Array.isArray(v) && v.length === 0)\n\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\nexport function validateValuesAgainstDefs(\n values: Record<string, any>,\n defs: CustomFieldDefLike[],\n): { ok: boolean; fieldErrors: Record<string, string> } {\n const errors: Record<string, string> = {}\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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",
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 const isEmpty = (v: any) => v == null || (typeof v === 'string' && v.trim() === '') || (Array.isArray(v) && v.length === 0)\n\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.4-develop.4331.1.64a8535120",
3
+ "version": "0.6.4-develop.4358.1.233d5675c7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -92,7 +92,7 @@
92
92
  "@mikro-orm/core": "^7.1.3",
93
93
  "@mikro-orm/decorators": "^7.1.3",
94
94
  "@mikro-orm/postgresql": "^7.1.3",
95
- "@open-mercato/cache": "0.6.4-develop.4331.1.64a8535120",
95
+ "@open-mercato/cache": "0.6.4-develop.4358.1.233d5675c7",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.1.0",
98
98
  "re2js": "2.8.3",
@@ -1,7 +1,10 @@
1
1
  /** @jest-environment node */
2
2
  import {
3
+ MAX_CUSTOM_FIELD_KEYS_PER_RECORD,
3
4
  MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH,
4
5
  MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH,
6
+ TOO_MANY_CUSTOM_FIELDS_ERROR,
7
+ UNKNOWN_CUSTOM_FIELD_ERROR,
5
8
  validateValuesAgainstDefs,
6
9
  } from '../validation'
7
10
 
@@ -122,6 +125,45 @@ describe('validateValuesAgainstDefs', () => {
122
125
  expect(result.fieldErrors['cf_body']).toBe('body too large')
123
126
  })
124
127
 
128
+ it('rejects values for undeclared keys only when rejectUndeclaredKeys is set', () => {
129
+ const defs = [{ key: 'priority', kind: 'integer', configJson: {} }]
130
+ const strict = validateValuesAgainstDefs({ priority: 1, undeclared: 'x' }, defs as any, {
131
+ rejectUndeclaredKeys: true,
132
+ })
133
+
134
+ expect(strict.ok).toBe(false)
135
+ expect(strict.fieldErrors.cf_undeclared).toBe(UNKNOWN_CUSTOM_FIELD_ERROR)
136
+ })
137
+
138
+ it('persists undeclared keys by default (trusted command writes)', () => {
139
+ const defs = [{ key: 'priority', kind: 'integer', configJson: {} }]
140
+ const result = validateValuesAgainstDefs({ priority: 1, undeclared: 'x' }, defs as any)
141
+
142
+ expect(result.ok).toBe(true)
143
+ })
144
+
145
+ it('ignores undefined keys even in strict mode', () => {
146
+ const defs = [{ key: 'priority', kind: 'integer', configJson: {} }]
147
+ const result = validateValuesAgainstDefs({ priority: 1, undeclared: undefined }, defs as any, {
148
+ rejectUndeclaredKeys: true,
149
+ })
150
+
151
+ expect(result.ok).toBe(true)
152
+ })
153
+
154
+ it('rejects payloads with too many custom field keys', () => {
155
+ const values: Record<string, number> = {}
156
+ for (let index = 0; index < MAX_CUSTOM_FIELD_KEYS_PER_RECORD + 1; index++) {
157
+ values[`field_${index}`] = index
158
+ }
159
+
160
+ const defs = Object.keys(values).map((key) => ({ key, kind: 'integer', configJson: {} }))
161
+ const result = validateValuesAgainstDefs(values, defs as any)
162
+
163
+ expect(result.ok).toBe(false)
164
+ expect(result.fieldErrors._customFields).toBe(TOO_MANY_CUSTOM_FIELDS_ERROR)
165
+ })
166
+
125
167
  it('fails closed before testing oversized regex patterns', () => {
126
168
  const defs = [
127
169
  {
@@ -3,6 +3,9 @@ import { testLinearRegex } from '../../lib/regex/linear'
3
3
 
4
4
  export const MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH = 500
5
5
  export const MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH = 10_000
6
+ export const MAX_CUSTOM_FIELD_KEYS_PER_RECORD = 128
7
+ export const UNKNOWN_CUSTOM_FIELD_ERROR = '[internal] Unknown custom field'
8
+ export const TOO_MANY_CUSTOM_FIELDS_ERROR = '[internal] Too many custom fields'
6
9
 
7
10
  // Supported rule types for custom fields validation
8
11
  export const VALIDATION_RULES = [
@@ -101,11 +104,44 @@ function evalRule(rule: ValidationRule, value: any, kind: string): string | null
101
104
  }
102
105
  }
103
106
 
107
+ function countPresentValueKeys(values: Record<string, unknown>): number {
108
+ let count = 0
109
+ for (const key of Object.keys(values)) {
110
+ if (values[key] !== undefined) count++
111
+ }
112
+ return count
113
+ }
114
+
115
+ export type ValidateValuesOptions = {
116
+ // When true, value keys that have no matching CustomFieldDef are rejected
117
+ // (OWASP A03/A04 EAV mass-assignment guard for untrusted entry points such as
118
+ // the generic `/api/entities/records` endpoint). Trusted first-party command
119
+ // writes persist dynamic/internal keys, so they leave this off and rely on the
120
+ // always-on per-record key cap below as the unbounded-injection backstop.
121
+ rejectUndeclaredKeys?: boolean
122
+ }
123
+
104
124
  export function validateValuesAgainstDefs(
105
125
  values: Record<string, any>,
106
126
  defs: CustomFieldDefLike[],
127
+ options: ValidateValuesOptions = {},
107
128
  ): { ok: boolean; fieldErrors: Record<string, string> } {
108
129
  const errors: Record<string, string> = {}
130
+
131
+ if (options.rejectUndeclaredKeys) {
132
+ const allowedKeys = new Set(defs.map((def) => def.key))
133
+ for (const key of Object.keys(values)) {
134
+ if (values[key] === undefined) continue
135
+ if (!allowedKeys.has(key)) {
136
+ errors[`cf_${key}`] = UNKNOWN_CUSTOM_FIELD_ERROR
137
+ }
138
+ }
139
+ }
140
+
141
+ if (countPresentValueKeys(values) > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {
142
+ errors._customFields = TOO_MANY_CUSTOM_FIELDS_ERROR
143
+ }
144
+
109
145
  for (const def of defs) {
110
146
  const cfg = def?.configJson || {}
111
147
  const rules: ValidationRule[] = Array.isArray(cfg.validation) ? cfg.validation : []