@open-mercato/shared 0.6.3-develop.3562.1.71049275ff → 0.6.3-develop.3576.1.51c9149577
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/crud/sync-event-runner.js +2 -8
- package/dist/lib/crud/sync-event-runner.js.map +2 -2
- package/dist/lib/events/patterns.js +2 -10
- package/dist/lib/events/patterns.js.map +2 -2
- package/dist/lib/patterns/wildcard.js +42 -0
- package/dist/lib/patterns/wildcard.js.map +7 -0
- package/dist/lib/regex/linear.js +31 -0
- package/dist/lib/regex/linear.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/entities/validation.js +15 -7
- package/dist/modules/entities/validation.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +2 -2
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/package.json +3 -2
- package/src/lib/crud/__tests__/sync-event-runner.test.ts +5 -0
- package/src/lib/crud/sync-event-runner.ts +2 -11
- package/src/lib/events/__tests__/patterns.test.ts +5 -0
- package/src/lib/events/patterns.ts +3 -16
- package/src/lib/patterns/__tests__/wildcard.test.ts +27 -0
- package/src/lib/patterns/wildcard.ts +59 -0
- package/src/lib/regex/linear.ts +48 -0
- package/src/modules/entities/__tests__/validation.test.ts +97 -2
- package/src/modules/entities/validation.ts +14 -9
- package/src/modules/widgets/__tests__/injection-loader.required-modules.test.ts +21 -0
- package/src/modules/widgets/injection-loader.ts +2 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 211 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
-
}
|
|
1
|
+
import { matchWildcardPattern } from "@open-mercato/shared/lib/patterns/wildcard";
|
|
4
2
|
function matchesEventPattern(pattern, eventId) {
|
|
5
|
-
|
|
6
|
-
if (pattern === "*") return true;
|
|
7
|
-
const escaped = escapeRegex(pattern).replace(/\\\*/g, ".*");
|
|
8
|
-
const regex = new RegExp("^" + escaped + "$");
|
|
9
|
-
return regex.test(eventId);
|
|
3
|
+
return matchWildcardPattern(eventId, pattern);
|
|
10
4
|
}
|
|
11
5
|
function collectSyncSubscribers(allSyncSubscribers, eventId) {
|
|
12
6
|
return allSyncSubscribers.filter((s) => matchesEventPattern(s.metadata.event, eventId)).sort((a, b) => (a.metadata.priority ?? 50) - (b.metadata.priority ?? 50));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/crud/sync-event-runner.ts"],
|
|
4
|
-
"sourcesContent": ["import type { SyncCrudEventPayload } from './sync-event-types'\nimport type { SyncSubscriberEntry } from './sync-subscriber-store'\n\n// ---------------------------------------------------------------------------\n// Event pattern matching\n// ---------------------------------------------------------------------------\n\
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { SyncCrudEventPayload } from './sync-event-types'\nimport type { SyncSubscriberEntry } from './sync-subscriber-store'\nimport { matchWildcardPattern } from '@open-mercato/shared/lib/patterns/wildcard'\n\n// ---------------------------------------------------------------------------\n// Event pattern matching\n// ---------------------------------------------------------------------------\n\nexport function matchesEventPattern(pattern: string, eventId: string): boolean {\n return matchWildcardPattern(eventId, pattern)\n}\n\n// ---------------------------------------------------------------------------\n// Collect matching subscribers\n// ---------------------------------------------------------------------------\n\nexport function collectSyncSubscribers(\n allSyncSubscribers: SyncSubscriberEntry[],\n eventId: string,\n): SyncSubscriberEntry[] {\n return allSyncSubscribers\n .filter((s) => matchesEventPattern(s.metadata.event, eventId))\n .sort((a, b) => (a.metadata.priority ?? 50) - (b.metadata.priority ?? 50))\n}\n\n// ---------------------------------------------------------------------------\n// Run sync before-event subscribers\n// ---------------------------------------------------------------------------\n\nexport async function runSyncBeforeEvent(\n subscribers: SyncSubscriberEntry[],\n payload: SyncCrudEventPayload,\n ctx: { resolve: <T = unknown>(name: string) => T },\n): Promise<{ ok: boolean; errorBody?: Record<string, unknown>; errorStatus?: number; modifiedPayload?: Record<string, unknown> }> {\n let currentPayload = payload.payload\n\n for (const subscriber of subscribers) {\n const result = await subscriber.handler({ ...payload, payload: currentPayload }, ctx)\n\n if (result?.ok === false) {\n const body = result.body ?? { error: result.message ?? 'Operation blocked', subscriberId: subscriber.metadata.id }\n return { ok: false, errorBody: body, errorStatus: result.status ?? 422 }\n }\n\n if (result?.modifiedPayload) {\n currentPayload = { ...currentPayload, ...result.modifiedPayload }\n }\n }\n\n return { ok: true, modifiedPayload: currentPayload !== payload.payload ? currentPayload : undefined }\n}\n\n// ---------------------------------------------------------------------------\n// Run sync after-event subscribers\n// ---------------------------------------------------------------------------\n\nexport async function runSyncAfterEvent(\n subscribers: SyncSubscriberEntry[],\n payload: SyncCrudEventPayload,\n ctx: { resolve: <T = unknown>(name: string) => T },\n): Promise<void> {\n for (const subscriber of subscribers) {\n try {\n await subscriber.handler(payload, ctx)\n } catch (error) {\n console.error(`[sync-event] after-subscriber failed: ${subscriber.metadata.id}`, error)\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,4BAA4B;AAM9B,SAAS,oBAAoB,SAAiB,SAA0B;AAC7E,SAAO,qBAAqB,SAAS,OAAO;AAC9C;AAMO,SAAS,uBACd,oBACA,SACuB;AACvB,SAAO,mBACJ,OAAO,CAAC,MAAM,oBAAoB,EAAE,SAAS,OAAO,OAAO,CAAC,EAC5D,KAAK,CAAC,GAAG,OAAO,EAAE,SAAS,YAAY,OAAO,EAAE,SAAS,YAAY,GAAG;AAC7E;AAMA,eAAsB,mBACpB,aACA,SACA,KACgI;AAChI,MAAI,iBAAiB,QAAQ;AAE7B,aAAW,cAAc,aAAa;AACpC,UAAM,SAAS,MAAM,WAAW,QAAQ,EAAE,GAAG,SAAS,SAAS,eAAe,GAAG,GAAG;AAEpF,QAAI,QAAQ,OAAO,OAAO;AACxB,YAAM,OAAO,OAAO,QAAQ,EAAE,OAAO,OAAO,WAAW,qBAAqB,cAAc,WAAW,SAAS,GAAG;AACjH,aAAO,EAAE,IAAI,OAAO,WAAW,MAAM,aAAa,OAAO,UAAU,IAAI;AAAA,IACzE;AAEA,QAAI,QAAQ,iBAAiB;AAC3B,uBAAiB,EAAE,GAAG,gBAAgB,GAAG,OAAO,gBAAgB;AAAA,IAClE;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,MAAM,iBAAiB,mBAAmB,QAAQ,UAAU,iBAAiB,OAAU;AACtG;AAMA,eAAsB,kBACpB,aACA,SACA,KACe;AACf,aAAW,cAAc,aAAa;AACpC,QAAI;AACF,YAAM,WAAW,QAAQ,SAAS,GAAG;AAAA,IACvC,SAAS,OAAO;AACd,cAAQ,MAAM,yCAAyC,WAAW,SAAS,EAAE,IAAI,KAAK;AAAA,IACxF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
function getSingleSegmentPatternRegex(pattern) {
|
|
3
|
-
const existing = singleSegmentPatternCache.get(pattern);
|
|
4
|
-
if (existing) return existing;
|
|
5
|
-
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^.]+");
|
|
6
|
-
const compiled = new RegExp(`^${regexPattern}$`);
|
|
7
|
-
singleSegmentPatternCache.set(pattern, compiled);
|
|
8
|
-
return compiled;
|
|
9
|
-
}
|
|
1
|
+
import { matchWildcardPattern } from "@open-mercato/shared/lib/patterns/wildcard";
|
|
10
2
|
function matchEventPattern(eventName, pattern, options) {
|
|
11
3
|
const mode = options?.mode ?? "single-segment";
|
|
12
4
|
if (pattern === "*") return true;
|
|
@@ -23,7 +15,7 @@ function matchEventPattern(eventName, pattern, options) {
|
|
|
23
15
|
}
|
|
24
16
|
return false;
|
|
25
17
|
}
|
|
26
|
-
return
|
|
18
|
+
return matchWildcardPattern(eventName, pattern, { singleSegmentWildcard: true });
|
|
27
19
|
}
|
|
28
20
|
function matchAnyEventPattern(eventName, patterns, options) {
|
|
29
21
|
for (const pattern of patterns) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/events/patterns.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { matchWildcardPattern } from '@open-mercato/shared/lib/patterns/wildcard'\n\nexport type EventPatternMode = 'single-segment' | 'prefix'\n\ntype MatchEventPatternOptions = {\n mode?: EventPatternMode\n}\n\nexport function matchEventPattern(\n eventName: string,\n pattern: string,\n options?: MatchEventPatternOptions,\n): boolean {\n const mode = options?.mode ?? 'single-segment'\n\n if (pattern === '*') return true\n if (pattern === eventName) return true\n if (!pattern.includes('*')) return false\n\n if (mode === 'prefix') {\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2)\n return eventName.startsWith(prefix + '.')\n }\n\n if (pattern.endsWith('*')) {\n const prefix = pattern.slice(0, -1)\n return eventName.startsWith(prefix)\n }\n\n return false\n }\n\n return matchWildcardPattern(eventName, pattern, { singleSegmentWildcard: true })\n}\n\nexport function matchAnyEventPattern(\n eventName: string,\n patterns: Iterable<string>,\n options?: MatchEventPatternOptions,\n): boolean {\n for (const pattern of patterns) {\n if (matchEventPattern(eventName, pattern, options)) {\n return true\n }\n }\n\n return false\n}\n\nexport function matchWebhookEventPattern(eventName: string, pattern: string): boolean {\n return matchEventPattern(eventName, pattern, { mode: 'prefix' })\n}\n\nexport function matchAnyWebhookEventPattern(eventName: string, patterns: Iterable<string>): boolean {\n return matchAnyEventPattern(eventName, patterns, { mode: 'prefix' })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,4BAA4B;AAQ9B,SAAS,kBACd,WACA,SACA,SACS;AACT,QAAM,OAAO,SAAS,QAAQ;AAE9B,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,YAAY,UAAW,QAAO;AAClC,MAAI,CAAC,QAAQ,SAAS,GAAG,EAAG,QAAO;AAEnC,MAAI,SAAS,UAAU;AACrB,QAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,YAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;AAClC,aAAO,UAAU,WAAW,SAAS,GAAG;AAAA,IAC1C;AAEA,QAAI,QAAQ,SAAS,GAAG,GAAG;AACzB,YAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;AAClC,aAAO,UAAU,WAAW,MAAM;AAAA,IACpC;AAEA,WAAO;AAAA,EACT;AAEA,SAAO,qBAAqB,WAAW,SAAS,EAAE,uBAAuB,KAAK,CAAC;AACjF;AAEO,SAAS,qBACd,WACA,UACA,SACS;AACT,aAAW,WAAW,UAAU;AAC9B,QAAI,kBAAkB,WAAW,SAAS,OAAO,GAAG;AAClD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,yBAAyB,WAAmB,SAA0B;AACpF,SAAO,kBAAkB,WAAW,SAAS,EAAE,MAAM,SAAS,CAAC;AACjE;AAEO,SAAS,4BAA4B,WAAmB,UAAqC;AAClG,SAAO,qBAAqB,WAAW,UAAU,EAAE,MAAM,SAAS,CAAC;AACrE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function matchWildcardSegment(pattern, value, options) {
|
|
2
|
+
let patternIndex = 0;
|
|
3
|
+
let valueIndex = 0;
|
|
4
|
+
let starIndex = -1;
|
|
5
|
+
let valueRetryIndex = 0;
|
|
6
|
+
while (valueIndex < value.length) {
|
|
7
|
+
const patternChar = pattern[patternIndex];
|
|
8
|
+
const valueChar = value[valueIndex];
|
|
9
|
+
if (patternChar === "*") {
|
|
10
|
+
starIndex = patternIndex;
|
|
11
|
+
valueRetryIndex = valueIndex;
|
|
12
|
+
patternIndex += 1;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (patternChar === valueChar) {
|
|
16
|
+
patternIndex += 1;
|
|
17
|
+
valueIndex += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (starIndex !== -1 && (!options.singleSegmentWildcard || value[valueRetryIndex] !== ".")) {
|
|
21
|
+
patternIndex = starIndex + 1;
|
|
22
|
+
valueRetryIndex += 1;
|
|
23
|
+
valueIndex = valueRetryIndex;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
while (pattern[patternIndex] === "*") {
|
|
29
|
+
patternIndex += 1;
|
|
30
|
+
}
|
|
31
|
+
return patternIndex === pattern.length;
|
|
32
|
+
}
|
|
33
|
+
function matchWildcardPattern(value, pattern, options = {}) {
|
|
34
|
+
if (pattern === value) return true;
|
|
35
|
+
if (pattern === "*") return true;
|
|
36
|
+
if (!pattern.includes("*")) return false;
|
|
37
|
+
return matchWildcardSegment(pattern, value, options);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
matchWildcardPattern
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=wildcard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/patterns/wildcard.ts"],
|
|
4
|
+
"sourcesContent": ["export type WildcardMatchOptions = {\n singleSegmentWildcard?: boolean\n}\n\nfunction matchWildcardSegment(pattern: string, value: string, options: WildcardMatchOptions): boolean {\n let patternIndex = 0\n let valueIndex = 0\n let starIndex = -1\n let valueRetryIndex = 0\n\n while (valueIndex < value.length) {\n const patternChar = pattern[patternIndex]\n const valueChar = value[valueIndex]\n\n if (patternChar === '*') {\n starIndex = patternIndex\n valueRetryIndex = valueIndex\n patternIndex += 1\n continue\n }\n\n if (patternChar === valueChar) {\n patternIndex += 1\n valueIndex += 1\n continue\n }\n\n if (\n starIndex !== -1\n && (!options.singleSegmentWildcard || value[valueRetryIndex] !== '.')\n ) {\n patternIndex = starIndex + 1\n valueRetryIndex += 1\n valueIndex = valueRetryIndex\n continue\n }\n\n return false\n }\n\n while (pattern[patternIndex] === '*') {\n patternIndex += 1\n }\n\n return patternIndex === pattern.length\n}\n\nexport function matchWildcardPattern(\n value: string,\n pattern: string,\n options: WildcardMatchOptions = {},\n): boolean {\n if (pattern === value) return true\n if (pattern === '*') return true\n if (!pattern.includes('*')) return false\n\n return matchWildcardSegment(pattern, value, options)\n}\n\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,qBAAqB,SAAiB,OAAe,SAAwC;AACpG,MAAI,eAAe;AACnB,MAAI,aAAa;AACjB,MAAI,YAAY;AAChB,MAAI,kBAAkB;AAEtB,SAAO,aAAa,MAAM,QAAQ;AAChC,UAAM,cAAc,QAAQ,YAAY;AACxC,UAAM,YAAY,MAAM,UAAU;AAElC,QAAI,gBAAgB,KAAK;AACvB,kBAAY;AACZ,wBAAkB;AAClB,sBAAgB;AAChB;AAAA,IACF;AAEA,QAAI,gBAAgB,WAAW;AAC7B,sBAAgB;AAChB,oBAAc;AACd;AAAA,IACF;AAEA,QACE,cAAc,OACV,CAAC,QAAQ,yBAAyB,MAAM,eAAe,MAAM,MACjE;AACA,qBAAe,YAAY;AAC3B,yBAAmB;AACnB,mBAAa;AACb;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,SAAO,QAAQ,YAAY,MAAM,KAAK;AACpC,oBAAgB;AAAA,EAClB;AAEA,SAAO,iBAAiB,QAAQ;AAClC;AAEO,SAAS,qBACd,OACA,SACA,UAAgC,CAAC,GACxB;AACT,MAAI,YAAY,MAAO,QAAO;AAC9B,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,CAAC,QAAQ,SAAS,GAAG,EAAG,QAAO;AAEnC,SAAO,qBAAqB,SAAS,OAAO,OAAO;AACrD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RE2JS } from "re2js";
|
|
2
|
+
const DEFAULT_MAX_PATTERN_LENGTH = 500;
|
|
3
|
+
const DEFAULT_MAX_INPUT_LENGTH = 1e4;
|
|
4
|
+
const MAX_REGEX_CACHE_SIZE = 256;
|
|
5
|
+
const compiledRegexCache = /* @__PURE__ */ new Map();
|
|
6
|
+
function getCachedRegex(pattern) {
|
|
7
|
+
const cached = compiledRegexCache.get(pattern);
|
|
8
|
+
if (cached) return cached;
|
|
9
|
+
const compiled = RE2JS.compile(pattern);
|
|
10
|
+
if (compiledRegexCache.size >= MAX_REGEX_CACHE_SIZE) {
|
|
11
|
+
const oldestKey = compiledRegexCache.keys().next().value;
|
|
12
|
+
if (oldestKey) compiledRegexCache.delete(oldestKey);
|
|
13
|
+
}
|
|
14
|
+
compiledRegexCache.set(pattern, compiled);
|
|
15
|
+
return compiled;
|
|
16
|
+
}
|
|
17
|
+
function testLinearRegex(pattern, input, options = {}) {
|
|
18
|
+
const maxPatternLength = options.maxPatternLength ?? DEFAULT_MAX_PATTERN_LENGTH;
|
|
19
|
+
const maxInputLength = options.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH;
|
|
20
|
+
if (pattern.length > maxPatternLength) return { ok: false, reason: "pattern_too_long" };
|
|
21
|
+
if (input.length > maxInputLength) return { ok: false, reason: "input_too_long" };
|
|
22
|
+
try {
|
|
23
|
+
return { ok: true, matched: getCachedRegex(pattern).test(input) };
|
|
24
|
+
} catch {
|
|
25
|
+
return { ok: false, reason: "invalid_pattern" };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
testLinearRegex
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=linear.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/regex/linear.ts"],
|
|
4
|
+
"sourcesContent": ["import { RE2JS } from 're2js'\n\nconst DEFAULT_MAX_PATTERN_LENGTH = 500\nconst DEFAULT_MAX_INPUT_LENGTH = 10_000\nconst MAX_REGEX_CACHE_SIZE = 256\n\nconst compiledRegexCache = new Map<string, RE2JS>()\n\nexport type LinearRegexTestOptions = {\n maxPatternLength?: number\n maxInputLength?: number\n}\n\nexport type LinearRegexTestResult =\n | { ok: true; matched: boolean }\n | { ok: false; reason: 'pattern_too_long' | 'input_too_long' | 'invalid_pattern' }\n\nfunction getCachedRegex(pattern: string): RE2JS {\n const cached = compiledRegexCache.get(pattern)\n if (cached) return cached\n\n const compiled = RE2JS.compile(pattern)\n if (compiledRegexCache.size >= MAX_REGEX_CACHE_SIZE) {\n const oldestKey = compiledRegexCache.keys().next().value\n if (oldestKey) compiledRegexCache.delete(oldestKey)\n }\n compiledRegexCache.set(pattern, compiled)\n return compiled\n}\n\nexport function testLinearRegex(\n pattern: string,\n input: string,\n options: LinearRegexTestOptions = {},\n): LinearRegexTestResult {\n const maxPatternLength = options.maxPatternLength ?? DEFAULT_MAX_PATTERN_LENGTH\n const maxInputLength = options.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH\n\n if (pattern.length > maxPatternLength) return { ok: false, reason: 'pattern_too_long' }\n if (input.length > maxInputLength) return { ok: false, reason: 'input_too_long' }\n\n try {\n return { ok: true, matched: getCachedRegex(pattern).test(input) }\n } catch {\n return { ok: false, reason: 'invalid_pattern' }\n }\n}\n\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,aAAa;AAEtB,MAAM,6BAA6B;AACnC,MAAM,2BAA2B;AACjC,MAAM,uBAAuB;AAE7B,MAAM,qBAAqB,oBAAI,IAAmB;AAWlD,SAAS,eAAe,SAAwB;AAC9C,QAAM,SAAS,mBAAmB,IAAI,OAAO;AAC7C,MAAI,OAAQ,QAAO;AAEnB,QAAM,WAAW,MAAM,QAAQ,OAAO;AACtC,MAAI,mBAAmB,QAAQ,sBAAsB;AACnD,UAAM,YAAY,mBAAmB,KAAK,EAAE,KAAK,EAAE;AACnD,QAAI,UAAW,oBAAmB,OAAO,SAAS;AAAA,EACpD;AACA,qBAAmB,IAAI,SAAS,QAAQ;AACxC,SAAO;AACT;AAEO,SAAS,gBACd,SACA,OACA,UAAkC,CAAC,GACZ;AACvB,QAAM,mBAAmB,QAAQ,oBAAoB;AACrD,QAAM,iBAAiB,QAAQ,kBAAkB;AAEjD,MAAI,QAAQ,SAAS,iBAAkB,QAAO,EAAE,IAAI,OAAO,QAAQ,mBAAmB;AACtF,MAAI,MAAM,SAAS,eAAgB,QAAO,EAAE,IAAI,OAAO,QAAQ,iBAAiB;AAEhF,MAAI;AACF,WAAO,EAAE,IAAI,MAAM,SAAS,eAAe,OAAO,EAAE,KAAK,KAAK,EAAE;AAAA,EAClE,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,QAAQ,kBAAkB;AAAA,EAChD;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
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.3-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.3-develop.3576.1.51c9149577'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { testLinearRegex } from "../../lib/regex/linear.js";
|
|
3
|
+
const MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH = 500;
|
|
4
|
+
const MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH = 1e4;
|
|
2
5
|
const VALIDATION_RULES = [
|
|
3
6
|
"required",
|
|
4
7
|
"date",
|
|
@@ -23,7 +26,11 @@ const validationRuleSchema = z.discriminatedUnion("rule", [
|
|
|
23
26
|
z.object({ rule: z.literal("gte"), param: z.number(), message: z.string().min(1) }),
|
|
24
27
|
z.object({ rule: z.literal("eq"), param: z.any(), message: z.string().min(1) }),
|
|
25
28
|
z.object({ rule: z.literal("ne"), param: z.any(), message: z.string().min(1) }),
|
|
26
|
-
z.object({
|
|
29
|
+
z.object({
|
|
30
|
+
rule: z.literal("regex"),
|
|
31
|
+
param: z.string().min(1),
|
|
32
|
+
message: z.string().min(1)
|
|
33
|
+
})
|
|
27
34
|
]);
|
|
28
35
|
const validationRulesArraySchema = z.array(validationRuleSchema).max(32);
|
|
29
36
|
function evalRule(rule, value, kind) {
|
|
@@ -66,12 +73,11 @@ function evalRule(rule, value, kind) {
|
|
|
66
73
|
return value !== rule.param ? null : rule.message;
|
|
67
74
|
case "regex":
|
|
68
75
|
if (isEmpty(value)) return null;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
}
|
|
76
|
+
const regexResult = testLinearRegex(String(rule.param), String(value), {
|
|
77
|
+
maxPatternLength: MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH,
|
|
78
|
+
maxInputLength: MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH
|
|
79
|
+
});
|
|
80
|
+
return regexResult.ok && regexResult.matched ? null : rule.message;
|
|
75
81
|
default:
|
|
76
82
|
return null;
|
|
77
83
|
}
|
|
@@ -94,6 +100,8 @@ function validateValuesAgainstDefs(values, defs) {
|
|
|
94
100
|
return { ok: Object.keys(errors).length === 0, fieldErrors: errors };
|
|
95
101
|
}
|
|
96
102
|
export {
|
|
103
|
+
MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH,
|
|
104
|
+
MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH,
|
|
97
105
|
VALIDATION_RULES,
|
|
98
106
|
validateValuesAgainstDefs,
|
|
99
107
|
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'\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({
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;
|
|
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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { matchWildcardPattern } from "@open-mercato/shared/lib/patterns/wildcard";
|
|
1
2
|
import {
|
|
2
3
|
applyInjectionWidgetOverridesToEntries,
|
|
3
4
|
applyInjectionWidgetOverridesToTables
|
|
@@ -324,8 +325,7 @@ async function getResolvedEntriesForSpot(spotId) {
|
|
|
324
325
|
for (const [candidateSpotId, candidateEntries] of table.entries()) {
|
|
325
326
|
if (candidateSpotId === spotId) continue;
|
|
326
327
|
if (!candidateSpotId.includes("*")) continue;
|
|
327
|
-
|
|
328
|
-
if (!pattern.test(spotId)) continue;
|
|
328
|
+
if (!matchWildcardPattern(spotId, candidateSpotId)) continue;
|
|
329
329
|
wildcardEntries.push(...candidateEntries);
|
|
330
330
|
}
|
|
331
331
|
const dedupedEntries = /* @__PURE__ */ new Map();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/widgets/injection-loader.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleInjectionWidgetEntry } from '../registry'\nimport type {\n InjectionAnyWidgetModule,\n InjectionDataWidgetModule,\n InjectionWidgetMetadata,\n InjectionWidgetModule,\n InjectionSpotId,\n ModuleInjectionSlot,\n ModuleInjectionTable,\n InjectionWidgetPlacement,\n} from './injection'\nimport {\n applyInjectionWidgetOverridesToEntries,\n applyInjectionWidgetOverridesToTables,\n} from '../overrides'\n\ntype LoadedWidgetModule = InjectionWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }\ntype LoadedDataWidgetModule = InjectionDataWidgetModule & { metadata: InjectionWidgetMetadata }\n\nexport type LoadedInjectionWidget = LoadedWidgetModule & {\n moduleId: string\n key: string\n placement?: {\n groupId?: string\n groupLabel?: string\n groupDescription?: string\n column?: 1 | 2\n kind?: 'tab' | 'group' | 'stack'\n [k: string]: unknown\n }\n}\n\nexport type LoadedInjectionDataWidget = LoadedDataWidgetModule & {\n moduleId: string\n key: string\n placement?: {\n groupId?: string\n groupLabel?: string\n groupDescription?: string\n column?: 1 | 2\n kind?: 'tab' | 'group' | 'stack'\n [k: string]: unknown\n }\n}\n\ntype WidgetEntry = ModuleInjectionWidgetEntry & { moduleId: string }\n\n// Registration pattern for publishable packages\nlet _coreInjectionWidgetEntries: ModuleInjectionWidgetEntry[] | null = null\nlet _coreInjectionTables: Array<{ moduleId: string; table: ModuleInjectionTable }> | null = null\nlet _enabledModuleIds: ReadonlySet<string> | null = null\nlet _injectionRegistryVersion = 0\nconst GLOBAL_INJECTION_WIDGETS_KEY = '__openMercatoCoreInjectionWidgetEntries__'\nconst GLOBAL_INJECTION_TABLES_KEY = '__openMercatoCoreInjectionTables__'\nconst GLOBAL_ENABLED_MODULE_IDS_KEY = '__openMercatoEnabledModuleIds__'\nconst GLOBAL_INJECTION_REGISTRY_VERSION_KEY = '__openMercatoCoreInjectionRegistryVersion__'\nconst INJECTION_REGISTRY_CHANGED_EVENT = '__openMercatoInjectionRegistryChanged__'\n\nfunction readGlobalInjectionWidgets(): ModuleInjectionWidgetEntry[] | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_WIDGETS_KEY]\n return Array.isArray(value) ? (value as ModuleInjectionWidgetEntry[]) : null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalInjectionWidgets(entries: ModuleInjectionWidgetEntry[]) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_INJECTION_WIDGETS_KEY] = entries\n } catch {\n // ignore global assignment failures\n }\n}\n\nfunction readGlobalEnabledModuleIds(): ReadonlySet<string> | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_ENABLED_MODULE_IDS_KEY]\n if (value instanceof Set) return value as ReadonlySet<string>\n return null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalEnabledModuleIds(ids: ReadonlySet<string>) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_ENABLED_MODULE_IDS_KEY] = ids\n } catch {\n // ignore global assignment failures\n }\n}\n\nfunction readGlobalInjectionTables(): Array<{ moduleId: string; table: ModuleInjectionTable }> | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_TABLES_KEY]\n return Array.isArray(value) ? (value as Array<{ moduleId: string; table: ModuleInjectionTable }>) : null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalInjectionTables(tables: Array<{ moduleId: string; table: ModuleInjectionTable }>) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_INJECTION_TABLES_KEY] = tables\n } catch {\n // ignore global assignment failures\n }\n}\n\nfunction readGlobalInjectionRegistryVersion(): number | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_REGISTRY_VERSION_KEY]\n return typeof value === 'number' ? value : null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalInjectionRegistryVersion(version: number) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_INJECTION_REGISTRY_VERSION_KEY] = version\n } catch {\n // ignore global assignment failures\n }\n}\n\nfunction notifyInjectionRegistryChanged() {\n _injectionRegistryVersion += 1\n writeGlobalInjectionRegistryVersion(_injectionRegistryVersion)\n invalidateInjectionWidgetCache()\n\n if (typeof window === 'undefined') return\n\n window.dispatchEvent(new CustomEvent(INJECTION_REGISTRY_CHANGED_EVENT, {\n detail: { version: _injectionRegistryVersion },\n }))\n}\n\nexport function registerCoreInjectionWidgets(entries: ModuleInjectionWidgetEntry[]) {\n if (_coreInjectionWidgetEntries !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Core injection widgets re-registered (this may occur during HMR)')\n }\n const finalEntries = applyInjectionWidgetOverridesToEntries(entries)\n _coreInjectionWidgetEntries = finalEntries\n writeGlobalInjectionWidgets(finalEntries)\n notifyInjectionRegistryChanged()\n}\n\nexport function getCoreInjectionWidgets(): ModuleInjectionWidgetEntry[] {\n const globalEntries = readGlobalInjectionWidgets()\n if (globalEntries) return globalEntries\n if (!_coreInjectionWidgetEntries) {\n // On client-side, bootstrap doesn't run - return empty array gracefully\n if (typeof window !== 'undefined') {\n return []\n }\n throw new Error('[Bootstrap] Core injection widgets not registered. Call registerCoreInjectionWidgets() at bootstrap.')\n }\n return _coreInjectionWidgetEntries\n}\n\nexport function registerCoreInjectionTables(tables: Array<{ moduleId: string; table: ModuleInjectionTable }>) {\n if (_coreInjectionTables !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Core injection tables re-registered (this may occur during HMR)')\n }\n const finalTables = applyInjectionWidgetOverridesToTables(tables)\n _coreInjectionTables = finalTables\n writeGlobalInjectionTables(finalTables)\n notifyInjectionRegistryChanged()\n}\n\n/**\n * Register the canonical set of enabled module IDs for the running app.\n *\n * This is the authoritative signal used by `requiredModules` widget gating \u2014\n * deriving \"enabled\" from injection tables or widget entries is unreliable\n * because modules without injection widgets (for example `ai_assistant`) do\n * not contribute entries to either source. Bootstrap callers should pass\n * every module ID present in the app's module registry.\n */\nexport function registerEnabledModuleIds(moduleIds: Iterable<string>) {\n const next = new Set<string>()\n for (const moduleId of moduleIds) {\n if (typeof moduleId === 'string' && moduleId.length > 0) next.add(moduleId)\n }\n if (_enabledModuleIds !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Enabled module IDs re-registered (this may occur during HMR)')\n }\n _enabledModuleIds = next\n writeGlobalEnabledModuleIds(next)\n notifyInjectionRegistryChanged()\n}\n\nexport function getEnabledModuleIds(): ReadonlySet<string> | null {\n return readGlobalEnabledModuleIds() ?? _enabledModuleIds\n}\n\nexport function getInjectionRegistryVersion(): number {\n const globalVersion = readGlobalInjectionRegistryVersion()\n if (globalVersion !== null) return globalVersion\n return _injectionRegistryVersion\n}\n\nexport function subscribeToInjectionRegistryChanges(listener: () => void): () => void {\n if (typeof window === 'undefined') {\n return () => {}\n }\n\n const handler = () => listener()\n window.addEventListener(INJECTION_REGISTRY_CHANGED_EVENT, handler)\n return () => {\n window.removeEventListener(INJECTION_REGISTRY_CHANGED_EVENT, handler)\n }\n}\n\nexport function getCoreInjectionTables(): Array<{ moduleId: string; table: ModuleInjectionTable }> {\n const globalTables = readGlobalInjectionTables()\n if (globalTables) return globalTables\n if (!_coreInjectionTables) {\n // On client-side, bootstrap doesn't run - return empty array gracefully\n if (typeof window !== 'undefined') {\n return []\n }\n throw new Error('[Bootstrap] Core injection tables not registered. Call registerCoreInjectionTables() at bootstrap.')\n }\n return _coreInjectionTables\n}\n\nlet widgetEntriesPromise: Promise<WidgetEntry[]> | null = null\ntype TableEntry = {\n widgetId: string\n moduleId: string\n priority: number\n placement?: ModuleInjectionSlot extends infer S\n ? S extends { widgetId: string }\n ? Omit<S, 'widgetId' | 'priority'>\n : never\n : never\n}\nlet injectionTablePromise: Promise<Map<InjectionSpotId, TableEntry[]>> | null = null\n\nfunction isInjectionSlotObject(value: ModuleInjectionSlot): value is InjectionWidgetPlacement & { widgetId: string; priority?: number } {\n return typeof value === 'object' && value !== null && 'widgetId' in value\n}\n\n/**\n * Invalidate the widget entries and widget module cache.\n * Call this when the generated registry is updated or modules are reloaded.\n */\nexport function invalidateInjectionWidgetCache() {\n widgetEntriesPromise = null\n injectionTablePromise = null\n widgetCache.clear()\n warnedRequiredModuleSkips.clear()\n}\n\nasync function loadWidgetEntries(): Promise<WidgetEntry[]> {\n if (!widgetEntriesPromise) {\n const promise = Promise.resolve().then(() =>\n getCoreInjectionWidgets().map((entry) => ({\n ...entry,\n moduleId: entry.moduleId || 'unknown',\n }))\n )\n widgetEntriesPromise = promise.catch((err) => {\n if (widgetEntriesPromise === promise) {\n widgetEntriesPromise = null\n }\n throw err\n })\n }\n return widgetEntriesPromise\n}\n\nasync function loadInjectionTable(): Promise<Map<InjectionSpotId, TableEntry[]>> {\n if (!injectionTablePromise) {\n const promise = Promise.resolve().then(() => {\n const list = getCoreInjectionTables()\n const table = new Map<InjectionSpotId, TableEntry[]>()\n\n for (const entry of list) {\n const injectionTable = entry.table ?? {}\n for (const [spotId, widgetIds] of Object.entries(injectionTable)) {\n const widgets = Array.isArray(widgetIds) ? widgetIds : [widgetIds]\n const existing = table.get(spotId) ?? []\n for (const widgetEntry of widgets) {\n if (typeof widgetEntry === 'string') {\n existing.push({ widgetId: widgetEntry, moduleId: entry.moduleId, priority: 0 })\n continue\n }\n if (isInjectionSlotObject(widgetEntry)) {\n const { widgetId, priority = 0, ...placement } = widgetEntry\n existing.push({\n widgetId,\n moduleId: entry.moduleId,\n priority: typeof priority === 'number' ? priority : 0,\n placement,\n })\n continue\n }\n }\n table.set(spotId, existing)\n }\n }\n\n for (const [spotId, widgets] of table.entries()) {\n table.set(spotId, widgets.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)))\n }\n\n return table\n })\n injectionTablePromise = promise.catch((err) => {\n if (injectionTablePromise === promise) {\n injectionTablePromise = null\n }\n throw err\n })\n }\n return injectionTablePromise\n}\n\nconst widgetCache = new Map<string, Promise<InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }>>()\n\nfunction isDataWidgetModule(widget: Record<string, unknown>): widget is LoadedDataWidgetModule {\n const keys = [\n 'columns',\n 'rowActions',\n 'bulkActions',\n 'filters',\n 'fields',\n 'steps',\n 'badge',\n 'menuItems',\n ]\n return keys.some((key) => key in widget)\n}\n\nfunction ensureValidInjectionModule(mod: unknown, key: string, moduleId: string): (InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }) {\n if (!mod || typeof mod !== 'object') {\n throw new Error(`Invalid injection widget module \"${key}\" from \"${moduleId}\" (expected object export)`)\n }\n const widget = (mod as { default?: InjectionAnyWidgetModule<any, any> }).default ?? (mod as InjectionAnyWidgetModule<any, any>)\n if (!widget || typeof widget !== 'object') {\n throw new Error(`Invalid injection widget export \"${key}\" from \"${moduleId}\" (missing default export)`) \n }\n if (!('metadata' in widget) || !widget.metadata || typeof widget.metadata !== 'object') {\n throw new Error(`Injection widget \"${key}\" from \"${moduleId}\" is missing metadata`)\n }\n const metadata = widget.metadata\n if (typeof metadata.id !== 'string' || metadata.id.length === 0) {\n throw new Error(`Injection widget \"${key}\" from \"${moduleId}\" metadata.id must be a non-empty string`)\n }\n const normalized = {\n ...widget,\n metadata,\n }\n\n if ('Widget' in normalized && typeof normalized.Widget === 'function') {\n if (typeof metadata.title !== 'string' || metadata.title.length === 0) {\n throw new Error(`Injection widget \"${metadata.id}\" from \"${moduleId}\" must have a title`)\n }\n return normalized\n }\n\n if (!isDataWidgetModule(normalized as Record<string, unknown>)) {\n throw new Error(\n `Injection widget \"${metadata.id}\" from \"${moduleId}\" must export either Widget component or a declarative data payload`\n )\n }\n\n return normalized\n}\n\nfunction isLoadedInjectionWidget(\n module: InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }\n): module is LoadedWidgetModule {\n return 'Widget' in module && typeof module.Widget === 'function'\n}\n\nfunction isLoadedInjectionDataWidget(\n module: InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }\n): module is LoadedDataWidgetModule {\n return !isLoadedInjectionWidget(module)\n}\n\nasync function loadEntry(entry: WidgetEntry): Promise<InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }> {\n if (!widgetCache.has(entry.key)) {\n const promise = entry.loader().then((mod) => ensureValidInjectionModule(mod, entry.key, entry.moduleId))\n widgetCache.set(entry.key, promise)\n }\n return widgetCache.get(entry.key)!\n}\n\nfunction getEnabledModuleIdsForInjection(): ReadonlySet<string> {\n // Prefer the explicit enabled-modules registry populated by bootstrap.\n // This is the only signal that includes modules without injection widgets\n // (for example `ai_assistant`), so it is required for `requiredModules`\n // gating to be sound.\n const explicit = readGlobalEnabledModuleIds() ?? _enabledModuleIds\n if (explicit) return explicit\n\n // Fallback: derive from injection tables and widget entries. This keeps\n // older bootstrap paths (and callers that have not yet wired\n // `registerEnabledModuleIds`) working \u2014 at the cost of mis-classifying\n // dependency modules that ship no widgets. New apps MUST call\n // `registerEnabledModuleIds` to get accurate gating.\n const enabled = new Set<string>()\n const tables = readGlobalInjectionTables() ?? _coreInjectionTables ?? []\n for (const entry of tables) {\n if (entry?.moduleId) enabled.add(entry.moduleId)\n }\n const entries = readGlobalInjectionWidgets() ?? _coreInjectionWidgetEntries ?? []\n for (const entry of entries) {\n if (entry?.moduleId) enabled.add(entry.moduleId)\n }\n return enabled\n}\n\nfunction widgetMissingRequiredModules(\n metadata: InjectionWidgetMetadata,\n enabledModuleIds: ReadonlySet<string>,\n): string[] {\n const required = metadata.requiredModules\n if (!Array.isArray(required) || required.length === 0) return []\n const missing: string[] = []\n for (const moduleId of required) {\n if (typeof moduleId !== 'string' || moduleId.length === 0) continue\n if (!enabledModuleIds.has(moduleId)) missing.push(moduleId)\n }\n return missing\n}\n\nconst warnedRequiredModuleSkips = new Set<string>()\n\nfunction warnSkippedWidget(metadataId: string, missingModules: string[]) {\n const key = `${metadataId}:${missingModules.join(',')}`\n if (warnedRequiredModuleSkips.has(key)) return\n warnedRequiredModuleSkips.add(key)\n if (process.env.NODE_ENV === 'development') {\n console.debug(\n `[InjectionLoader] Skipping widget \"${metadataId}\" \u2014 required module(s) not enabled: ${missingModules.join(', ')}`,\n )\n }\n}\n\nasync function getResolvedEntriesForSpot(spotId: InjectionSpotId): Promise<TableEntry[]> {\n const table = await loadInjectionTable()\n const exactEntries = table.get(spotId) ?? []\n const wildcardEntries: TableEntry[] = []\n\n for (const [candidateSpotId, candidateEntries] of table.entries()) {\n if (candidateSpotId === spotId) continue\n if (!candidateSpotId.includes('*')) continue\n const pattern = new RegExp(`^${candidateSpotId.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&').replace(/\\*/g, '.*')}$`)\n if (!pattern.test(spotId)) continue\n wildcardEntries.push(...candidateEntries)\n }\n\n const dedupedEntries = new Map<string, TableEntry>()\n for (const entry of [...exactEntries, ...wildcardEntries]) {\n const cacheKey = `${entry.moduleId}:${entry.widgetId}`\n const previous = dedupedEntries.get(cacheKey)\n if (!previous || (entry.priority ?? 0) > (previous.priority ?? 0)) {\n dedupedEntries.set(cacheKey, entry)\n }\n }\n\n return Array.from(dedupedEntries.values()).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))\n}\n\nexport async function loadAllInjectionWidgets(): Promise<LoadedInjectionWidget[]> {\n const widgetEntries = await loadWidgetEntries()\n const enabledModuleIds = getEnabledModuleIdsForInjection()\n const loaded = await Promise.all(\n widgetEntries.map(async (entry) => {\n const module = await loadEntry(entry)\n if (!isLoadedInjectionWidget(module)) return null\n const missing = widgetMissingRequiredModules(module.metadata, enabledModuleIds)\n if (missing.length > 0) {\n warnSkippedWidget(module.metadata.id, missing)\n return null\n }\n return { ...module, moduleId: entry.moduleId, key: entry.key }\n })\n )\n const byId = new Map<string, LoadedInjectionWidget>()\n for (const widget of loaded) {\n if (!widget) continue\n if (!byId.has(widget.metadata.id)) {\n byId.set(widget.metadata.id, widget)\n }\n }\n return Array.from(byId.values())\n}\n\nexport async function loadInjectionWidgetById(widgetId: string): Promise<LoadedInjectionWidget | null> {\n const widgetEntries = await loadWidgetEntries()\n const enabledModuleIds = getEnabledModuleIdsForInjection()\n for (const entry of widgetEntries) {\n const module = await loadEntry(entry)\n if (!isLoadedInjectionWidget(module)) continue\n if (module.metadata.id === widgetId) {\n const missing = widgetMissingRequiredModules(module.metadata, enabledModuleIds)\n if (missing.length > 0) {\n warnSkippedWidget(module.metadata.id, missing)\n return null\n }\n return { ...module, moduleId: entry.moduleId, key: entry.key }\n }\n }\n return null\n}\n\nexport async function loadInjectionDataWidgetById(widgetId: string): Promise<LoadedInjectionDataWidget | null> {\n const widgetEntries = await loadWidgetEntries()\n const enabledModuleIds = getEnabledModuleIdsForInjection()\n for (const entry of widgetEntries) {\n const module = await loadEntry(entry)\n if (!isLoadedInjectionDataWidget(module)) continue\n if (module.metadata.id === widgetId) {\n const missing = widgetMissingRequiredModules(module.metadata, enabledModuleIds)\n if (missing.length > 0) {\n warnSkippedWidget(module.metadata.id, missing)\n return null\n }\n return { ...module, moduleId: entry.moduleId, key: entry.key }\n }\n }\n return null\n}\n\nexport async function loadInjectionWidgetsForSpot(spotId: InjectionSpotId): Promise<LoadedInjectionWidget[]> {\n const entries = await getResolvedEntriesForSpot(spotId)\n const widgets: LoadedInjectionWidget[] = []\n for (const { widgetId, placement, priority } of entries) {\n const widget = await loadInjectionWidgetById(widgetId)\n if (!widget) continue\n const combinedPlacement = placement\n ? { ...placement, priority: typeof priority === 'number' ? priority : 0 }\n : { priority: typeof priority === 'number' ? priority : 0 }\n widgets.push({ ...widget, placement: combinedPlacement })\n }\n return widgets\n}\n\nexport async function loadInjectionDataWidgetsForSpot(spotId: InjectionSpotId): Promise<LoadedInjectionDataWidget[]> {\n const entries = await getResolvedEntriesForSpot(spotId)\n const widgets: LoadedInjectionDataWidget[] = []\n for (const { widgetId, placement, priority } of entries) {\n const widget = await loadInjectionDataWidgetById(widgetId)\n if (!widget) continue\n const combinedPlacement = placement\n ? { ...placement, priority: typeof priority === 'number' ? priority : 0 }\n : { priority: typeof priority === 'number' ? priority : 0 }\n widgets.push({ ...widget, placement: combinedPlacement })\n }\n return widgets\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { ModuleInjectionWidgetEntry } from '../registry'\nimport { matchWildcardPattern } from '@open-mercato/shared/lib/patterns/wildcard'\nimport type {\n InjectionAnyWidgetModule,\n InjectionDataWidgetModule,\n InjectionWidgetMetadata,\n InjectionWidgetModule,\n InjectionSpotId,\n ModuleInjectionSlot,\n ModuleInjectionTable,\n InjectionWidgetPlacement,\n} from './injection'\nimport {\n applyInjectionWidgetOverridesToEntries,\n applyInjectionWidgetOverridesToTables,\n} from '../overrides'\n\ntype LoadedWidgetModule = InjectionWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }\ntype LoadedDataWidgetModule = InjectionDataWidgetModule & { metadata: InjectionWidgetMetadata }\n\nexport type LoadedInjectionWidget = LoadedWidgetModule & {\n moduleId: string\n key: string\n placement?: {\n groupId?: string\n groupLabel?: string\n groupDescription?: string\n column?: 1 | 2\n kind?: 'tab' | 'group' | 'stack'\n [k: string]: unknown\n }\n}\n\nexport type LoadedInjectionDataWidget = LoadedDataWidgetModule & {\n moduleId: string\n key: string\n placement?: {\n groupId?: string\n groupLabel?: string\n groupDescription?: string\n column?: 1 | 2\n kind?: 'tab' | 'group' | 'stack'\n [k: string]: unknown\n }\n}\n\ntype WidgetEntry = ModuleInjectionWidgetEntry & { moduleId: string }\n\n// Registration pattern for publishable packages\nlet _coreInjectionWidgetEntries: ModuleInjectionWidgetEntry[] | null = null\nlet _coreInjectionTables: Array<{ moduleId: string; table: ModuleInjectionTable }> | null = null\nlet _enabledModuleIds: ReadonlySet<string> | null = null\nlet _injectionRegistryVersion = 0\nconst GLOBAL_INJECTION_WIDGETS_KEY = '__openMercatoCoreInjectionWidgetEntries__'\nconst GLOBAL_INJECTION_TABLES_KEY = '__openMercatoCoreInjectionTables__'\nconst GLOBAL_ENABLED_MODULE_IDS_KEY = '__openMercatoEnabledModuleIds__'\nconst GLOBAL_INJECTION_REGISTRY_VERSION_KEY = '__openMercatoCoreInjectionRegistryVersion__'\nconst INJECTION_REGISTRY_CHANGED_EVENT = '__openMercatoInjectionRegistryChanged__'\n\nfunction readGlobalInjectionWidgets(): ModuleInjectionWidgetEntry[] | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_WIDGETS_KEY]\n return Array.isArray(value) ? (value as ModuleInjectionWidgetEntry[]) : null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalInjectionWidgets(entries: ModuleInjectionWidgetEntry[]) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_INJECTION_WIDGETS_KEY] = entries\n } catch {\n // ignore global assignment failures\n }\n}\n\nfunction readGlobalEnabledModuleIds(): ReadonlySet<string> | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_ENABLED_MODULE_IDS_KEY]\n if (value instanceof Set) return value as ReadonlySet<string>\n return null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalEnabledModuleIds(ids: ReadonlySet<string>) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_ENABLED_MODULE_IDS_KEY] = ids\n } catch {\n // ignore global assignment failures\n }\n}\n\nfunction readGlobalInjectionTables(): Array<{ moduleId: string; table: ModuleInjectionTable }> | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_TABLES_KEY]\n return Array.isArray(value) ? (value as Array<{ moduleId: string; table: ModuleInjectionTable }>) : null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalInjectionTables(tables: Array<{ moduleId: string; table: ModuleInjectionTable }>) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_INJECTION_TABLES_KEY] = tables\n } catch {\n // ignore global assignment failures\n }\n}\n\nfunction readGlobalInjectionRegistryVersion(): number | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_REGISTRY_VERSION_KEY]\n return typeof value === 'number' ? value : null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalInjectionRegistryVersion(version: number) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_INJECTION_REGISTRY_VERSION_KEY] = version\n } catch {\n // ignore global assignment failures\n }\n}\n\nfunction notifyInjectionRegistryChanged() {\n _injectionRegistryVersion += 1\n writeGlobalInjectionRegistryVersion(_injectionRegistryVersion)\n invalidateInjectionWidgetCache()\n\n if (typeof window === 'undefined') return\n\n window.dispatchEvent(new CustomEvent(INJECTION_REGISTRY_CHANGED_EVENT, {\n detail: { version: _injectionRegistryVersion },\n }))\n}\n\nexport function registerCoreInjectionWidgets(entries: ModuleInjectionWidgetEntry[]) {\n if (_coreInjectionWidgetEntries !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Core injection widgets re-registered (this may occur during HMR)')\n }\n const finalEntries = applyInjectionWidgetOverridesToEntries(entries)\n _coreInjectionWidgetEntries = finalEntries\n writeGlobalInjectionWidgets(finalEntries)\n notifyInjectionRegistryChanged()\n}\n\nexport function getCoreInjectionWidgets(): ModuleInjectionWidgetEntry[] {\n const globalEntries = readGlobalInjectionWidgets()\n if (globalEntries) return globalEntries\n if (!_coreInjectionWidgetEntries) {\n // On client-side, bootstrap doesn't run - return empty array gracefully\n if (typeof window !== 'undefined') {\n return []\n }\n throw new Error('[Bootstrap] Core injection widgets not registered. Call registerCoreInjectionWidgets() at bootstrap.')\n }\n return _coreInjectionWidgetEntries\n}\n\nexport function registerCoreInjectionTables(tables: Array<{ moduleId: string; table: ModuleInjectionTable }>) {\n if (_coreInjectionTables !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Core injection tables re-registered (this may occur during HMR)')\n }\n const finalTables = applyInjectionWidgetOverridesToTables(tables)\n _coreInjectionTables = finalTables\n writeGlobalInjectionTables(finalTables)\n notifyInjectionRegistryChanged()\n}\n\n/**\n * Register the canonical set of enabled module IDs for the running app.\n *\n * This is the authoritative signal used by `requiredModules` widget gating \u2014\n * deriving \"enabled\" from injection tables or widget entries is unreliable\n * because modules without injection widgets (for example `ai_assistant`) do\n * not contribute entries to either source. Bootstrap callers should pass\n * every module ID present in the app's module registry.\n */\nexport function registerEnabledModuleIds(moduleIds: Iterable<string>) {\n const next = new Set<string>()\n for (const moduleId of moduleIds) {\n if (typeof moduleId === 'string' && moduleId.length > 0) next.add(moduleId)\n }\n if (_enabledModuleIds !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Enabled module IDs re-registered (this may occur during HMR)')\n }\n _enabledModuleIds = next\n writeGlobalEnabledModuleIds(next)\n notifyInjectionRegistryChanged()\n}\n\nexport function getEnabledModuleIds(): ReadonlySet<string> | null {\n return readGlobalEnabledModuleIds() ?? _enabledModuleIds\n}\n\nexport function getInjectionRegistryVersion(): number {\n const globalVersion = readGlobalInjectionRegistryVersion()\n if (globalVersion !== null) return globalVersion\n return _injectionRegistryVersion\n}\n\nexport function subscribeToInjectionRegistryChanges(listener: () => void): () => void {\n if (typeof window === 'undefined') {\n return () => {}\n }\n\n const handler = () => listener()\n window.addEventListener(INJECTION_REGISTRY_CHANGED_EVENT, handler)\n return () => {\n window.removeEventListener(INJECTION_REGISTRY_CHANGED_EVENT, handler)\n }\n}\n\nexport function getCoreInjectionTables(): Array<{ moduleId: string; table: ModuleInjectionTable }> {\n const globalTables = readGlobalInjectionTables()\n if (globalTables) return globalTables\n if (!_coreInjectionTables) {\n // On client-side, bootstrap doesn't run - return empty array gracefully\n if (typeof window !== 'undefined') {\n return []\n }\n throw new Error('[Bootstrap] Core injection tables not registered. Call registerCoreInjectionTables() at bootstrap.')\n }\n return _coreInjectionTables\n}\n\nlet widgetEntriesPromise: Promise<WidgetEntry[]> | null = null\ntype TableEntry = {\n widgetId: string\n moduleId: string\n priority: number\n placement?: ModuleInjectionSlot extends infer S\n ? S extends { widgetId: string }\n ? Omit<S, 'widgetId' | 'priority'>\n : never\n : never\n}\nlet injectionTablePromise: Promise<Map<InjectionSpotId, TableEntry[]>> | null = null\n\nfunction isInjectionSlotObject(value: ModuleInjectionSlot): value is InjectionWidgetPlacement & { widgetId: string; priority?: number } {\n return typeof value === 'object' && value !== null && 'widgetId' in value\n}\n\n/**\n * Invalidate the widget entries and widget module cache.\n * Call this when the generated registry is updated or modules are reloaded.\n */\nexport function invalidateInjectionWidgetCache() {\n widgetEntriesPromise = null\n injectionTablePromise = null\n widgetCache.clear()\n warnedRequiredModuleSkips.clear()\n}\n\nasync function loadWidgetEntries(): Promise<WidgetEntry[]> {\n if (!widgetEntriesPromise) {\n const promise = Promise.resolve().then(() =>\n getCoreInjectionWidgets().map((entry) => ({\n ...entry,\n moduleId: entry.moduleId || 'unknown',\n }))\n )\n widgetEntriesPromise = promise.catch((err) => {\n if (widgetEntriesPromise === promise) {\n widgetEntriesPromise = null\n }\n throw err\n })\n }\n return widgetEntriesPromise\n}\n\nasync function loadInjectionTable(): Promise<Map<InjectionSpotId, TableEntry[]>> {\n if (!injectionTablePromise) {\n const promise = Promise.resolve().then(() => {\n const list = getCoreInjectionTables()\n const table = new Map<InjectionSpotId, TableEntry[]>()\n\n for (const entry of list) {\n const injectionTable = entry.table ?? {}\n for (const [spotId, widgetIds] of Object.entries(injectionTable)) {\n const widgets = Array.isArray(widgetIds) ? widgetIds : [widgetIds]\n const existing = table.get(spotId) ?? []\n for (const widgetEntry of widgets) {\n if (typeof widgetEntry === 'string') {\n existing.push({ widgetId: widgetEntry, moduleId: entry.moduleId, priority: 0 })\n continue\n }\n if (isInjectionSlotObject(widgetEntry)) {\n const { widgetId, priority = 0, ...placement } = widgetEntry\n existing.push({\n widgetId,\n moduleId: entry.moduleId,\n priority: typeof priority === 'number' ? priority : 0,\n placement,\n })\n continue\n }\n }\n table.set(spotId, existing)\n }\n }\n\n for (const [spotId, widgets] of table.entries()) {\n table.set(spotId, widgets.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)))\n }\n\n return table\n })\n injectionTablePromise = promise.catch((err) => {\n if (injectionTablePromise === promise) {\n injectionTablePromise = null\n }\n throw err\n })\n }\n return injectionTablePromise\n}\n\nconst widgetCache = new Map<string, Promise<InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }>>()\n\nfunction isDataWidgetModule(widget: Record<string, unknown>): widget is LoadedDataWidgetModule {\n const keys = [\n 'columns',\n 'rowActions',\n 'bulkActions',\n 'filters',\n 'fields',\n 'steps',\n 'badge',\n 'menuItems',\n ]\n return keys.some((key) => key in widget)\n}\n\nfunction ensureValidInjectionModule(mod: unknown, key: string, moduleId: string): (InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }) {\n if (!mod || typeof mod !== 'object') {\n throw new Error(`Invalid injection widget module \"${key}\" from \"${moduleId}\" (expected object export)`)\n }\n const widget = (mod as { default?: InjectionAnyWidgetModule<any, any> }).default ?? (mod as InjectionAnyWidgetModule<any, any>)\n if (!widget || typeof widget !== 'object') {\n throw new Error(`Invalid injection widget export \"${key}\" from \"${moduleId}\" (missing default export)`) \n }\n if (!('metadata' in widget) || !widget.metadata || typeof widget.metadata !== 'object') {\n throw new Error(`Injection widget \"${key}\" from \"${moduleId}\" is missing metadata`)\n }\n const metadata = widget.metadata\n if (typeof metadata.id !== 'string' || metadata.id.length === 0) {\n throw new Error(`Injection widget \"${key}\" from \"${moduleId}\" metadata.id must be a non-empty string`)\n }\n const normalized = {\n ...widget,\n metadata,\n }\n\n if ('Widget' in normalized && typeof normalized.Widget === 'function') {\n if (typeof metadata.title !== 'string' || metadata.title.length === 0) {\n throw new Error(`Injection widget \"${metadata.id}\" from \"${moduleId}\" must have a title`)\n }\n return normalized\n }\n\n if (!isDataWidgetModule(normalized as Record<string, unknown>)) {\n throw new Error(\n `Injection widget \"${metadata.id}\" from \"${moduleId}\" must export either Widget component or a declarative data payload`\n )\n }\n\n return normalized\n}\n\nfunction isLoadedInjectionWidget(\n module: InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }\n): module is LoadedWidgetModule {\n return 'Widget' in module && typeof module.Widget === 'function'\n}\n\nfunction isLoadedInjectionDataWidget(\n module: InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }\n): module is LoadedDataWidgetModule {\n return !isLoadedInjectionWidget(module)\n}\n\nasync function loadEntry(entry: WidgetEntry): Promise<InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }> {\n if (!widgetCache.has(entry.key)) {\n const promise = entry.loader().then((mod) => ensureValidInjectionModule(mod, entry.key, entry.moduleId))\n widgetCache.set(entry.key, promise)\n }\n return widgetCache.get(entry.key)!\n}\n\nfunction getEnabledModuleIdsForInjection(): ReadonlySet<string> {\n // Prefer the explicit enabled-modules registry populated by bootstrap.\n // This is the only signal that includes modules without injection widgets\n // (for example `ai_assistant`), so it is required for `requiredModules`\n // gating to be sound.\n const explicit = readGlobalEnabledModuleIds() ?? _enabledModuleIds\n if (explicit) return explicit\n\n // Fallback: derive from injection tables and widget entries. This keeps\n // older bootstrap paths (and callers that have not yet wired\n // `registerEnabledModuleIds`) working \u2014 at the cost of mis-classifying\n // dependency modules that ship no widgets. New apps MUST call\n // `registerEnabledModuleIds` to get accurate gating.\n const enabled = new Set<string>()\n const tables = readGlobalInjectionTables() ?? _coreInjectionTables ?? []\n for (const entry of tables) {\n if (entry?.moduleId) enabled.add(entry.moduleId)\n }\n const entries = readGlobalInjectionWidgets() ?? _coreInjectionWidgetEntries ?? []\n for (const entry of entries) {\n if (entry?.moduleId) enabled.add(entry.moduleId)\n }\n return enabled\n}\n\nfunction widgetMissingRequiredModules(\n metadata: InjectionWidgetMetadata,\n enabledModuleIds: ReadonlySet<string>,\n): string[] {\n const required = metadata.requiredModules\n if (!Array.isArray(required) || required.length === 0) return []\n const missing: string[] = []\n for (const moduleId of required) {\n if (typeof moduleId !== 'string' || moduleId.length === 0) continue\n if (!enabledModuleIds.has(moduleId)) missing.push(moduleId)\n }\n return missing\n}\n\nconst warnedRequiredModuleSkips = new Set<string>()\n\nfunction warnSkippedWidget(metadataId: string, missingModules: string[]) {\n const key = `${metadataId}:${missingModules.join(',')}`\n if (warnedRequiredModuleSkips.has(key)) return\n warnedRequiredModuleSkips.add(key)\n if (process.env.NODE_ENV === 'development') {\n console.debug(\n `[InjectionLoader] Skipping widget \"${metadataId}\" \u2014 required module(s) not enabled: ${missingModules.join(', ')}`,\n )\n }\n}\n\nasync function getResolvedEntriesForSpot(spotId: InjectionSpotId): Promise<TableEntry[]> {\n const table = await loadInjectionTable()\n const exactEntries = table.get(spotId) ?? []\n const wildcardEntries: TableEntry[] = []\n\n for (const [candidateSpotId, candidateEntries] of table.entries()) {\n if (candidateSpotId === spotId) continue\n if (!candidateSpotId.includes('*')) continue\n if (!matchWildcardPattern(spotId, candidateSpotId)) continue\n wildcardEntries.push(...candidateEntries)\n }\n\n const dedupedEntries = new Map<string, TableEntry>()\n for (const entry of [...exactEntries, ...wildcardEntries]) {\n const cacheKey = `${entry.moduleId}:${entry.widgetId}`\n const previous = dedupedEntries.get(cacheKey)\n if (!previous || (entry.priority ?? 0) > (previous.priority ?? 0)) {\n dedupedEntries.set(cacheKey, entry)\n }\n }\n\n return Array.from(dedupedEntries.values()).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))\n}\n\nexport async function loadAllInjectionWidgets(): Promise<LoadedInjectionWidget[]> {\n const widgetEntries = await loadWidgetEntries()\n const enabledModuleIds = getEnabledModuleIdsForInjection()\n const loaded = await Promise.all(\n widgetEntries.map(async (entry) => {\n const module = await loadEntry(entry)\n if (!isLoadedInjectionWidget(module)) return null\n const missing = widgetMissingRequiredModules(module.metadata, enabledModuleIds)\n if (missing.length > 0) {\n warnSkippedWidget(module.metadata.id, missing)\n return null\n }\n return { ...module, moduleId: entry.moduleId, key: entry.key }\n })\n )\n const byId = new Map<string, LoadedInjectionWidget>()\n for (const widget of loaded) {\n if (!widget) continue\n if (!byId.has(widget.metadata.id)) {\n byId.set(widget.metadata.id, widget)\n }\n }\n return Array.from(byId.values())\n}\n\nexport async function loadInjectionWidgetById(widgetId: string): Promise<LoadedInjectionWidget | null> {\n const widgetEntries = await loadWidgetEntries()\n const enabledModuleIds = getEnabledModuleIdsForInjection()\n for (const entry of widgetEntries) {\n const module = await loadEntry(entry)\n if (!isLoadedInjectionWidget(module)) continue\n if (module.metadata.id === widgetId) {\n const missing = widgetMissingRequiredModules(module.metadata, enabledModuleIds)\n if (missing.length > 0) {\n warnSkippedWidget(module.metadata.id, missing)\n return null\n }\n return { ...module, moduleId: entry.moduleId, key: entry.key }\n }\n }\n return null\n}\n\nexport async function loadInjectionDataWidgetById(widgetId: string): Promise<LoadedInjectionDataWidget | null> {\n const widgetEntries = await loadWidgetEntries()\n const enabledModuleIds = getEnabledModuleIdsForInjection()\n for (const entry of widgetEntries) {\n const module = await loadEntry(entry)\n if (!isLoadedInjectionDataWidget(module)) continue\n if (module.metadata.id === widgetId) {\n const missing = widgetMissingRequiredModules(module.metadata, enabledModuleIds)\n if (missing.length > 0) {\n warnSkippedWidget(module.metadata.id, missing)\n return null\n }\n return { ...module, moduleId: entry.moduleId, key: entry.key }\n }\n }\n return null\n}\n\nexport async function loadInjectionWidgetsForSpot(spotId: InjectionSpotId): Promise<LoadedInjectionWidget[]> {\n const entries = await getResolvedEntriesForSpot(spotId)\n const widgets: LoadedInjectionWidget[] = []\n for (const { widgetId, placement, priority } of entries) {\n const widget = await loadInjectionWidgetById(widgetId)\n if (!widget) continue\n const combinedPlacement = placement\n ? { ...placement, priority: typeof priority === 'number' ? priority : 0 }\n : { priority: typeof priority === 'number' ? priority : 0 }\n widgets.push({ ...widget, placement: combinedPlacement })\n }\n return widgets\n}\n\nexport async function loadInjectionDataWidgetsForSpot(spotId: InjectionSpotId): Promise<LoadedInjectionDataWidget[]> {\n const entries = await getResolvedEntriesForSpot(spotId)\n const widgets: LoadedInjectionDataWidget[] = []\n for (const { widgetId, placement, priority } of entries) {\n const widget = await loadInjectionDataWidgetById(widgetId)\n if (!widget) continue\n const combinedPlacement = placement\n ? { ...placement, priority: typeof priority === 'number' ? priority : 0 }\n : { priority: typeof priority === 'number' ? priority : 0 }\n widgets.push({ ...widget, placement: combinedPlacement })\n }\n return widgets\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.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3576.1.51c9149577",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -92,9 +92,10 @@
|
|
|
92
92
|
"@mikro-orm/core": "^7.0.17",
|
|
93
93
|
"@mikro-orm/decorators": "^7.0.17",
|
|
94
94
|
"@mikro-orm/postgresql": "^7.0.17",
|
|
95
|
-
"@open-mercato/cache": "0.6.3-develop.
|
|
95
|
+
"@open-mercato/cache": "0.6.3-develop.3576.1.51c9149577",
|
|
96
96
|
"dotenv": "^17.4.2",
|
|
97
97
|
"rate-limiter-flexible": "^11.1.0",
|
|
98
|
+
"re2js": "2.8.2",
|
|
98
99
|
"reflect-metadata": "^0.2.2",
|
|
99
100
|
"sanitize-html": "^2.17.4",
|
|
100
101
|
"undici": "^8.3.0"
|
|
@@ -22,6 +22,11 @@ describe('matchesEventPattern', () => {
|
|
|
22
22
|
expect(matchesEventPattern('example.*', 'example.todo.created')).toBe(true)
|
|
23
23
|
expect(matchesEventPattern('example.*', 'example.item.updated')).toBe(true)
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
it('treats regex metacharacters as literals', () => {
|
|
27
|
+
expect(matchesEventPattern('example.(todo).*', 'example.(todo).created')).toBe(true)
|
|
28
|
+
expect(matchesEventPattern('example.+.*', 'example.todo.created')).toBe(false)
|
|
29
|
+
})
|
|
25
30
|
})
|
|
26
31
|
|
|
27
32
|
describe('collectSyncSubscribers', () => {
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import type { SyncCrudEventPayload } from './sync-event-types'
|
|
2
2
|
import type { SyncSubscriberEntry } from './sync-subscriber-store'
|
|
3
|
+
import { matchWildcardPattern } from '@open-mercato/shared/lib/patterns/wildcard'
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Event pattern matching
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
|
|
8
|
-
function escapeRegex(input: string): string {
|
|
9
|
-
// Escape all characters with special meaning in regular expressions
|
|
10
|
-
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
11
|
-
}
|
|
12
|
-
|
|
13
9
|
export function matchesEventPattern(pattern: string, eventId: string): boolean {
|
|
14
|
-
|
|
15
|
-
if (pattern === '*') return true
|
|
16
|
-
// Escape the pattern for literal use in a RegExp, then turn "\*" into ".*" for wildcard support
|
|
17
|
-
const escaped = escapeRegex(pattern).replace(/\\\*/g, '.*')
|
|
18
|
-
const regex = new RegExp('^' + escaped + '$')
|
|
19
|
-
return regex.test(eventId)
|
|
10
|
+
return matchWildcardPattern(eventId, pattern)
|
|
20
11
|
}
|
|
21
12
|
|
|
22
13
|
// ---------------------------------------------------------------------------
|
|
@@ -19,6 +19,11 @@ describe('matchEventPattern', () => {
|
|
|
19
19
|
expect(matchEventPattern('customers.person', 'customers.*')).toBe(true)
|
|
20
20
|
expect(matchEventPattern('customers.person.deleted', 'customers.*')).toBe(false)
|
|
21
21
|
})
|
|
22
|
+
|
|
23
|
+
it('treats regex metacharacters as literal pattern characters', () => {
|
|
24
|
+
expect(matchEventPattern('customers.(person).created', 'customers.(person).*')).toBe(true)
|
|
25
|
+
expect(matchEventPattern('customers.person.created', 'customers.+.*')).toBe(false)
|
|
26
|
+
})
|
|
22
27
|
})
|
|
23
28
|
|
|
24
29
|
describe('prefix mode', () => {
|
|
@@ -1,24 +1,11 @@
|
|
|
1
|
+
import { matchWildcardPattern } from '@open-mercato/shared/lib/patterns/wildcard'
|
|
2
|
+
|
|
1
3
|
export type EventPatternMode = 'single-segment' | 'prefix'
|
|
2
4
|
|
|
3
5
|
type MatchEventPatternOptions = {
|
|
4
6
|
mode?: EventPatternMode
|
|
5
7
|
}
|
|
6
8
|
|
|
7
|
-
const singleSegmentPatternCache = new Map<string, RegExp>()
|
|
8
|
-
|
|
9
|
-
function getSingleSegmentPatternRegex(pattern: string): RegExp {
|
|
10
|
-
const existing = singleSegmentPatternCache.get(pattern)
|
|
11
|
-
if (existing) return existing
|
|
12
|
-
|
|
13
|
-
const regexPattern = pattern
|
|
14
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
15
|
-
.replace(/\*/g, '[^.]+')
|
|
16
|
-
|
|
17
|
-
const compiled = new RegExp(`^${regexPattern}$`)
|
|
18
|
-
singleSegmentPatternCache.set(pattern, compiled)
|
|
19
|
-
return compiled
|
|
20
|
-
}
|
|
21
|
-
|
|
22
9
|
export function matchEventPattern(
|
|
23
10
|
eventName: string,
|
|
24
11
|
pattern: string,
|
|
@@ -44,7 +31,7 @@ export function matchEventPattern(
|
|
|
44
31
|
return false
|
|
45
32
|
}
|
|
46
33
|
|
|
47
|
-
return
|
|
34
|
+
return matchWildcardPattern(eventName, pattern, { singleSegmentWildcard: true })
|
|
48
35
|
}
|
|
49
36
|
|
|
50
37
|
export function matchAnyEventPattern(
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { matchWildcardPattern } from '../wildcard'
|
|
2
|
+
|
|
3
|
+
describe('matchWildcardPattern', () => {
|
|
4
|
+
it('matches exact values and the global wildcard', () => {
|
|
5
|
+
expect(matchWildcardPattern('customers.person.created', 'customers.person.created')).toBe(true)
|
|
6
|
+
expect(matchWildcardPattern('customers.person.created', '*')).toBe(true)
|
|
7
|
+
expect(matchWildcardPattern('customers.person.created', 'sales.order.created')).toBe(false)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('matches multi-segment wildcard patterns without regex execution', () => {
|
|
11
|
+
expect(matchWildcardPattern('example.todo.created', 'example.*')).toBe(true)
|
|
12
|
+
expect(matchWildcardPattern('example.todo.created', 'example.*.created')).toBe(true)
|
|
13
|
+
expect(matchWildcardPattern('example.todo.created', '*.todo.*')).toBe(true)
|
|
14
|
+
expect(matchWildcardPattern('example.todo.created', 'customers.*')).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('can restrict wildcards to a single dotted segment', () => {
|
|
18
|
+
expect(matchWildcardPattern('customers.person', 'customers.*', { singleSegmentWildcard: true })).toBe(true)
|
|
19
|
+
expect(matchWildcardPattern('customers.person.deleted', 'customers.*', { singleSegmentWildcard: true })).toBe(false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('treats regex metacharacters as literals', () => {
|
|
23
|
+
expect(matchWildcardPattern('cache:(tenant)[1]', 'cache:(tenant)[*]')).toBe(true)
|
|
24
|
+
expect(matchWildcardPattern('cache:(tenant)[1]', 'cache:.+')).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type WildcardMatchOptions = {
|
|
2
|
+
singleSegmentWildcard?: boolean
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function matchWildcardSegment(pattern: string, value: string, options: WildcardMatchOptions): boolean {
|
|
6
|
+
let patternIndex = 0
|
|
7
|
+
let valueIndex = 0
|
|
8
|
+
let starIndex = -1
|
|
9
|
+
let valueRetryIndex = 0
|
|
10
|
+
|
|
11
|
+
while (valueIndex < value.length) {
|
|
12
|
+
const patternChar = pattern[patternIndex]
|
|
13
|
+
const valueChar = value[valueIndex]
|
|
14
|
+
|
|
15
|
+
if (patternChar === '*') {
|
|
16
|
+
starIndex = patternIndex
|
|
17
|
+
valueRetryIndex = valueIndex
|
|
18
|
+
patternIndex += 1
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (patternChar === valueChar) {
|
|
23
|
+
patternIndex += 1
|
|
24
|
+
valueIndex += 1
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
starIndex !== -1
|
|
30
|
+
&& (!options.singleSegmentWildcard || value[valueRetryIndex] !== '.')
|
|
31
|
+
) {
|
|
32
|
+
patternIndex = starIndex + 1
|
|
33
|
+
valueRetryIndex += 1
|
|
34
|
+
valueIndex = valueRetryIndex
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
while (pattern[patternIndex] === '*') {
|
|
42
|
+
patternIndex += 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return patternIndex === pattern.length
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function matchWildcardPattern(
|
|
49
|
+
value: string,
|
|
50
|
+
pattern: string,
|
|
51
|
+
options: WildcardMatchOptions = {},
|
|
52
|
+
): boolean {
|
|
53
|
+
if (pattern === value) return true
|
|
54
|
+
if (pattern === '*') return true
|
|
55
|
+
if (!pattern.includes('*')) return false
|
|
56
|
+
|
|
57
|
+
return matchWildcardSegment(pattern, value, options)
|
|
58
|
+
}
|
|
59
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { RE2JS } from 're2js'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_PATTERN_LENGTH = 500
|
|
4
|
+
const DEFAULT_MAX_INPUT_LENGTH = 10_000
|
|
5
|
+
const MAX_REGEX_CACHE_SIZE = 256
|
|
6
|
+
|
|
7
|
+
const compiledRegexCache = new Map<string, RE2JS>()
|
|
8
|
+
|
|
9
|
+
export type LinearRegexTestOptions = {
|
|
10
|
+
maxPatternLength?: number
|
|
11
|
+
maxInputLength?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LinearRegexTestResult =
|
|
15
|
+
| { ok: true; matched: boolean }
|
|
16
|
+
| { ok: false; reason: 'pattern_too_long' | 'input_too_long' | 'invalid_pattern' }
|
|
17
|
+
|
|
18
|
+
function getCachedRegex(pattern: string): RE2JS {
|
|
19
|
+
const cached = compiledRegexCache.get(pattern)
|
|
20
|
+
if (cached) return cached
|
|
21
|
+
|
|
22
|
+
const compiled = RE2JS.compile(pattern)
|
|
23
|
+
if (compiledRegexCache.size >= MAX_REGEX_CACHE_SIZE) {
|
|
24
|
+
const oldestKey = compiledRegexCache.keys().next().value
|
|
25
|
+
if (oldestKey) compiledRegexCache.delete(oldestKey)
|
|
26
|
+
}
|
|
27
|
+
compiledRegexCache.set(pattern, compiled)
|
|
28
|
+
return compiled
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function testLinearRegex(
|
|
32
|
+
pattern: string,
|
|
33
|
+
input: string,
|
|
34
|
+
options: LinearRegexTestOptions = {},
|
|
35
|
+
): LinearRegexTestResult {
|
|
36
|
+
const maxPatternLength = options.maxPatternLength ?? DEFAULT_MAX_PATTERN_LENGTH
|
|
37
|
+
const maxInputLength = options.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH
|
|
38
|
+
|
|
39
|
+
if (pattern.length > maxPatternLength) return { ok: false, reason: 'pattern_too_long' }
|
|
40
|
+
if (input.length > maxInputLength) return { ok: false, reason: 'input_too_long' }
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
return { ok: true, matched: getCachedRegex(pattern).test(input) }
|
|
44
|
+
} catch {
|
|
45
|
+
return { ok: false, reason: 'invalid_pattern' }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** @jest-environment node */
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH,
|
|
4
|
+
MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH,
|
|
5
|
+
validateValuesAgainstDefs,
|
|
6
|
+
} from '../validation'
|
|
3
7
|
|
|
4
8
|
describe('validateValuesAgainstDefs', () => {
|
|
5
9
|
it('validates required and integer/float comparisons', () => {
|
|
@@ -48,5 +52,96 @@ describe('validateValuesAgainstDefs', () => {
|
|
|
48
52
|
r = validateValuesAgainstDefs({ code: 'ok', status: 'open' }, defs as any)
|
|
49
53
|
expect(r.ok).toBe(true)
|
|
50
54
|
})
|
|
51
|
-
})
|
|
52
55
|
|
|
56
|
+
it('evaluates dangerous backtracking regex rules in bounded time', () => {
|
|
57
|
+
const defs = [
|
|
58
|
+
{
|
|
59
|
+
key: 'eu_vat_number',
|
|
60
|
+
kind: 'text',
|
|
61
|
+
configJson: {
|
|
62
|
+
validation: [
|
|
63
|
+
{
|
|
64
|
+
rule: 'regex',
|
|
65
|
+
param: '^(GB|FR|DE|IT|ES|PL|NL|BE|SE|AT|DK|FI|PT|IE|GR|CZ|RO|HU|SK|BG|HR|SI|LT|LV|EE|LU|MT|CY)?([0-9A-Za-z]+)*$',
|
|
66
|
+
message: 'bad vat',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const startedAt = Date.now()
|
|
74
|
+
const result = validateValuesAgainstDefs(
|
|
75
|
+
{ eu_vat_number: `${'A'.repeat(64)}!` },
|
|
76
|
+
defs as any,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect(Date.now() - startedAt).toBeLessThan(2_000)
|
|
80
|
+
expect(result.ok).toBe(false)
|
|
81
|
+
expect(result.fieldErrors['cf_eu_vat_number']).toBe('bad vat')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('fails closed for unsupported regex syntax', () => {
|
|
85
|
+
const defs = [
|
|
86
|
+
{
|
|
87
|
+
key: 'code',
|
|
88
|
+
kind: 'text',
|
|
89
|
+
configJson: {
|
|
90
|
+
validation: [
|
|
91
|
+
{ rule: 'regex', param: '(?=safe)safe', message: 'unsupported regex' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
const result = validateValuesAgainstDefs({ code: 'safe' }, defs as any)
|
|
98
|
+
|
|
99
|
+
expect(result.ok).toBe(false)
|
|
100
|
+
expect(result.fieldErrors['cf_code']).toBe('unsupported regex')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('fails closed before testing oversized regex input values', () => {
|
|
104
|
+
const defs = [
|
|
105
|
+
{
|
|
106
|
+
key: 'body',
|
|
107
|
+
kind: 'multiline',
|
|
108
|
+
configJson: {
|
|
109
|
+
validation: [
|
|
110
|
+
{ rule: 'regex', param: '^a+$', message: 'body too large' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
const result = validateValuesAgainstDefs(
|
|
117
|
+
{ body: 'a'.repeat(MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH + 1) },
|
|
118
|
+
defs as any,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
expect(result.ok).toBe(false)
|
|
122
|
+
expect(result.fieldErrors['cf_body']).toBe('body too large')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('fails closed before testing oversized regex patterns', () => {
|
|
126
|
+
const defs = [
|
|
127
|
+
{
|
|
128
|
+
key: 'code',
|
|
129
|
+
kind: 'text',
|
|
130
|
+
configJson: {
|
|
131
|
+
validation: [
|
|
132
|
+
{
|
|
133
|
+
rule: 'regex',
|
|
134
|
+
param: `^${'a'.repeat(MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH)}$`,
|
|
135
|
+
message: 'pattern too large',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
const result = validateValuesAgainstDefs({ code: 'a' }, defs as any)
|
|
143
|
+
|
|
144
|
+
expect(result.ok).toBe(false)
|
|
145
|
+
expect(result.fieldErrors['cf_code']).toBe('pattern too large')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { testLinearRegex } from '../../lib/regex/linear'
|
|
3
|
+
|
|
4
|
+
export const MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH = 500
|
|
5
|
+
export const MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH = 10_000
|
|
2
6
|
|
|
3
7
|
// Supported rule types for custom fields validation
|
|
4
8
|
export const VALIDATION_RULES = [
|
|
@@ -28,7 +32,11 @@ export const validationRuleSchema = z.discriminatedUnion('rule', [
|
|
|
28
32
|
z.object({ rule: z.literal('gte'), param: z.number(), message: z.string().min(1) }),
|
|
29
33
|
z.object({ rule: z.literal('eq'), param: z.any(), message: z.string().min(1) }),
|
|
30
34
|
z.object({ rule: z.literal('ne'), param: z.any(), message: z.string().min(1) }),
|
|
31
|
-
z.object({
|
|
35
|
+
z.object({
|
|
36
|
+
rule: z.literal('regex'),
|
|
37
|
+
param: z.string().min(1),
|
|
38
|
+
message: z.string().min(1),
|
|
39
|
+
}),
|
|
32
40
|
])
|
|
33
41
|
|
|
34
42
|
export type ValidationRule = z.infer<typeof validationRuleSchema>
|
|
@@ -83,13 +91,11 @@ function evalRule(rule: ValidationRule, value: any, kind: string): string | null
|
|
|
83
91
|
return value !== (rule as any).param ? null : rule.message
|
|
84
92
|
case 'regex':
|
|
85
93
|
if (isEmpty(value)) return null
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return rule.message
|
|
92
|
-
}
|
|
94
|
+
const regexResult = testLinearRegex(String((rule as any).param), String(value), {
|
|
95
|
+
maxPatternLength: MAX_CUSTOM_FIELD_REGEX_PATTERN_LENGTH,
|
|
96
|
+
maxInputLength: MAX_CUSTOM_FIELD_REGEX_INPUT_LENGTH,
|
|
97
|
+
})
|
|
98
|
+
return regexResult.ok && regexResult.matched ? null : rule.message
|
|
93
99
|
default:
|
|
94
100
|
return null
|
|
95
101
|
}
|
|
@@ -115,4 +121,3 @@ export function validateValuesAgainstDefs(
|
|
|
115
121
|
}
|
|
116
122
|
return { ok: Object.keys(errors).length === 0, fieldErrors: errors }
|
|
117
123
|
}
|
|
118
|
-
|
|
@@ -173,4 +173,25 @@ describe('Injection loader — requiredModules gating (#1849)', () => {
|
|
|
173
173
|
expect(ids).toContain(ALWAYS_AVAILABLE_WIDGET_ID)
|
|
174
174
|
expect(ids).not.toContain(REQUIRES_AI_WIDGET_ID)
|
|
175
175
|
})
|
|
176
|
+
|
|
177
|
+
it('loads wildcard spot entries without regex pattern execution', async () => {
|
|
178
|
+
invalidateInjectionWidgetCache()
|
|
179
|
+
registerCoreInjectionWidgets([
|
|
180
|
+
makeWidgetEntry('host', 'host/widgets/injection/always-available/widget.ts', async () => alwaysAvailableWidget),
|
|
181
|
+
])
|
|
182
|
+
registerCoreInjectionTables([
|
|
183
|
+
{
|
|
184
|
+
moduleId: 'host',
|
|
185
|
+
table: {
|
|
186
|
+
'data-table:host.*:search-trailing': [
|
|
187
|
+
{ widgetId: ALWAYS_AVAILABLE_WIDGET_ID, priority: 50 },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
])
|
|
192
|
+
registerEnabledModuleIds(['host'])
|
|
193
|
+
|
|
194
|
+
const loaded = await loadInjectionWidgetsForSpot(HOST_SPOT_ID)
|
|
195
|
+
expect(loaded.map((widget) => widget.metadata.id)).toEqual([ALWAYS_AVAILABLE_WIDGET_ID])
|
|
196
|
+
})
|
|
176
197
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModuleInjectionWidgetEntry } from '../registry'
|
|
2
|
+
import { matchWildcardPattern } from '@open-mercato/shared/lib/patterns/wildcard'
|
|
2
3
|
import type {
|
|
3
4
|
InjectionAnyWidgetModule,
|
|
4
5
|
InjectionDataWidgetModule,
|
|
@@ -452,8 +453,7 @@ async function getResolvedEntriesForSpot(spotId: InjectionSpotId): Promise<Table
|
|
|
452
453
|
for (const [candidateSpotId, candidateEntries] of table.entries()) {
|
|
453
454
|
if (candidateSpotId === spotId) continue
|
|
454
455
|
if (!candidateSpotId.includes('*')) continue
|
|
455
|
-
|
|
456
|
-
if (!pattern.test(spotId)) continue
|
|
456
|
+
if (!matchWildcardPattern(spotId, candidateSpotId)) continue
|
|
457
457
|
wildcardEntries.push(...candidateEntries)
|
|
458
458
|
}
|
|
459
459
|
|