@open-mercato/shared 0.6.6-develop.6460.1.f94c74174c → 0.6.6-develop.6471.1.9ab5bdd79a
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/AGENTS.md +1 -0
- package/dist/lib/commands/command-bus.js +4 -2
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +4 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/commands/flush.js +3 -4
- package/dist/lib/commands/flush.js.map +2 -2
- package/dist/lib/commands/helpers.js +3 -1
- package/dist/lib/commands/helpers.js.map +2 -2
- package/dist/lib/commands/registry.js +5 -3
- package/dist/lib/commands/registry.js.map +2 -2
- package/dist/lib/commands/scope.js +5 -3
- package/dist/lib/commands/scope.js.map +2 -2
- package/dist/lib/crud/advanced-filter-integration.js +3 -1
- package/dist/lib/crud/advanced-filter-integration.js.map +2 -2
- package/dist/lib/crud/cache.js +3 -1
- package/dist/lib/crud/cache.js.map +2 -2
- package/dist/lib/crud/custom-fields.js +4 -2
- package/dist/lib/crud/custom-fields.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +6 -10
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +10 -8
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-registry.js +3 -3
- package/dist/lib/crud/interceptor-registry.js.map +2 -2
- package/dist/lib/crud/mutation-guard.js +4 -2
- package/dist/lib/crud/mutation-guard.js.map +2 -2
- package/dist/lib/crud/optimistic-lock.js +5 -6
- package/dist/lib/crud/optimistic-lock.js.map +2 -2
- package/dist/lib/crud/route-mutation-guard.js +3 -1
- package/dist/lib/crud/route-mutation-guard.js.map +2 -2
- package/dist/lib/crud/sync-event-runner.js +3 -1
- package/dist/lib/crud/sync-event-runner.js.map +2 -2
- package/dist/lib/data/engine.js +5 -5
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +5 -3
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/di/container.js +3 -1
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/encryption/aes.js +3 -1
- package/dist/lib/encryption/aes.js.map +2 -2
- package/dist/lib/encryption/entityFields.js +3 -1
- package/dist/lib/encryption/entityFields.js.map +2 -2
- package/dist/lib/encryption/entityIds.js +3 -1
- package/dist/lib/encryption/entityIds.js.map +2 -2
- package/dist/lib/encryption/kms.js +25 -32
- package/dist/lib/encryption/kms.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +3 -1
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +3 -1
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/indexers/error-log.js +4 -2
- package/dist/lib/indexers/error-log.js.map +2 -2
- package/dist/lib/indexers/status-log.js +5 -3
- package/dist/lib/indexers/status-log.js.map +2 -2
- package/dist/lib/logger/index.js +28 -0
- package/dist/lib/logger/index.js.map +7 -0
- package/dist/lib/logger/level.js +43 -0
- package/dist/lib/logger/level.js.map +7 -0
- package/dist/lib/logger/transport.console.js +39 -0
- package/dist/lib/logger/transport.console.js.map +7 -0
- package/dist/lib/logger/transport.js +17 -0
- package/dist/lib/logger/transport.js.map +7 -0
- package/dist/lib/logger/transport.pretty.js +85 -0
- package/dist/lib/logger/transport.pretty.js.map +7 -0
- package/dist/lib/logger/transport.server.js +85 -0
- package/dist/lib/logger/transport.server.js.map +7 -0
- package/dist/lib/middleware/page-executor.js +3 -1
- package/dist/lib/middleware/page-executor.js.map +2 -2
- package/dist/lib/modules/registry.js +3 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/profiler/index.js +3 -2
- package/dist/lib/profiler/index.js.map +2 -2
- package/dist/lib/query/engine.js +5 -6
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/query-extension-runner.js +5 -11
- package/dist/lib/query/query-extension-runner.js.map +2 -2
- package/dist/lib/redis/connection.js +3 -1
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/url.js +7 -5
- package/dist/lib/url.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/analytics.js +3 -1
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/dashboard/widgets.js +3 -1
- package/dist/modules/dashboard/widgets.js.map +2 -2
- package/dist/modules/events/factory.js +5 -3
- package/dist/modules/events/factory.js.map +2 -2
- package/dist/modules/overrides.js +16 -34
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/registry.js +3 -1
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/search.js +3 -1
- package/dist/modules/search.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +6 -6
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/dist/modules/widgets/injection-position.js +3 -1
- package/dist/modules/widgets/injection-position.js.map +2 -2
- package/package.json +7 -2
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +19 -6
- package/src/lib/commands/__tests__/flush.test.ts +24 -15
- package/src/lib/commands/__tests__/normalizeAuthorUserId.test.ts +14 -2
- package/src/lib/commands/__tests__/registry.test.ts +17 -2
- package/src/lib/commands/__tests__/scope.test.ts +21 -9
- package/src/lib/commands/command-bus.ts +5 -2
- package/src/lib/commands/command-interceptor-runner.ts +5 -2
- package/src/lib/commands/flush.ts +4 -5
- package/src/lib/commands/helpers.ts +9 -2
- package/src/lib/commands/registry.ts +6 -3
- package/src/lib/commands/scope.ts +6 -3
- package/src/lib/crud/__tests__/optimistic-lock.test.ts +21 -6
- package/src/lib/crud/__tests__/route-mutation-guard.test.ts +17 -2
- package/src/lib/crud/__tests__/sync-event-runner.test.ts +17 -3
- package/src/lib/crud/advanced-filter-integration.ts +4 -1
- package/src/lib/crud/cache.ts +4 -1
- package/src/lib/crud/custom-fields.ts +5 -2
- package/src/lib/crud/enricher-runner.ts +7 -10
- package/src/lib/crud/factory.ts +11 -10
- package/src/lib/crud/interceptor-registry.ts +4 -3
- package/src/lib/crud/mutation-guard.ts +5 -2
- package/src/lib/crud/optimistic-lock.ts +6 -10
- package/src/lib/crud/route-mutation-guard.ts +4 -1
- package/src/lib/crud/sync-event-runner.ts +4 -1
- package/src/lib/data/__tests__/engine.event-validation.test.ts +26 -15
- package/src/lib/data/engine.ts +6 -6
- package/src/lib/db/mikro.ts +6 -3
- package/src/lib/di/container.ts +4 -1
- package/src/lib/encryption/aes.ts +4 -2
- package/src/lib/encryption/entityFields.ts +5 -1
- package/src/lib/encryption/entityIds.ts +4 -1
- package/src/lib/encryption/kms.ts +27 -33
- package/src/lib/encryption/subscriber.ts +4 -2
- package/src/lib/encryption/tenantDataEncryptionService.ts +4 -2
- package/src/lib/indexers/__tests__/status-log.test.ts +22 -10
- package/src/lib/indexers/error-log.ts +5 -2
- package/src/lib/indexers/status-log.ts +6 -3
- package/src/lib/logger/__tests__/logger.test.ts +539 -0
- package/src/lib/logger/index.ts +33 -0
- package/src/lib/logger/level.ts +50 -0
- package/src/lib/logger/transport.console.ts +41 -0
- package/src/lib/logger/transport.pretty.ts +94 -0
- package/src/lib/logger/transport.server.ts +125 -0
- package/src/lib/logger/transport.ts +17 -0
- package/src/lib/middleware/page-executor.ts +4 -1
- package/src/lib/modules/__tests__/registry.test.ts +28 -21
- package/src/lib/modules/registry.ts +4 -1
- package/src/lib/profiler/index.ts +4 -2
- package/src/lib/query/__tests__/query-extension-runner.test.ts +21 -7
- package/src/lib/query/engine.ts +7 -10
- package/src/lib/query/query-extension-runner.ts +6 -11
- package/src/lib/redis/connection.ts +5 -1
- package/src/lib/url.ts +10 -5
- package/src/modules/__tests__/overrides.test.ts +17 -3
- package/src/modules/__tests__/route-overrides.test.ts +29 -20
- package/src/modules/analytics.ts +5 -1
- package/src/modules/dashboard/widgets.ts +4 -1
- package/src/modules/events/factory.ts +6 -3
- package/src/modules/overrides.ts +18 -34
- package/src/modules/registry.ts +4 -1
- package/src/modules/search.ts +4 -1
- package/src/modules/widgets/injection-loader.ts +7 -6
- package/src/modules/widgets/injection-position.ts +5 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLogger } from "../logger/index.js";
|
|
2
|
+
const logger = createLogger("shared").child({ component: "redis" });
|
|
1
3
|
function getRedisUrl(prefix) {
|
|
2
4
|
if (prefix) {
|
|
3
5
|
const prefixed = process.env[`${prefix}_REDIS_URL`];
|
|
@@ -28,7 +30,7 @@ function parseRedisUrl(url) {
|
|
|
28
30
|
};
|
|
29
31
|
} catch {
|
|
30
32
|
const safeUrl = url.replace(/\/\/[^:]*:[^@]*@/, "//<redacted>@");
|
|
31
|
-
|
|
33
|
+
logger.warn("Failed to parse Redis URL, falling back to localhost:6379", { url: safeUrl });
|
|
32
34
|
return { host: "localhost", port: 6379 };
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/redis/connection.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Shared Redis connection utilities.\n *\n * Every package that needs a Redis URL or parsed connection options\n * should import from here instead of reading env vars directly.\n *\n * The `prefix` parameter lets each subsystem define its own override:\n * getRedisUrl('QUEUE') \u2192 QUEUE_REDIS_URL > REDIS_URL > null\n * getRedisUrl('CACHE') \u2192 CACHE_REDIS_URL > REDIS_URL > null\n * getRedisUrl() \u2192 REDIS_URL > null\n *\n * Returns null when Redis is not configured so callers can explicitly\n * decide whether to fall back to a non-Redis strategy or fail loudly.\n * Use getRedisUrlOrThrow() when Redis is mandatory for the caller.\n */\n\nexport type ParsedRedisConnection = {\n host: string\n port: number\n password?: string\n db?: number\n tls?: Record<string, unknown>\n}\n\n/**\n * Resolve a Redis URL from environment variables.\n *\n * Priority: <PREFIX>_REDIS_URL \u2192 REDIS_URL \u2192 null\n *\n * Returns null when no env var is set. Callers MUST NOT assume a\n * localhost default \u2014 silently connecting to localhost masks missing\n * configuration and stalls on platforms where nothing listens there\n * (e.g., WSL2 without a local Redis).\n */\nexport function getRedisUrl(prefix?: string): string | null {\n if (prefix) {\n const prefixed = process.env[`${prefix}_REDIS_URL`]\n if (prefixed) return prefixed\n }\n return process.env.REDIS_URL || null\n}\n\n/**\n * Like getRedisUrl, but throws a descriptive error when Redis is not\n * configured. Use from code paths that require Redis (e.g. BullMQ\n * async queue, Redis cache strategy, scheduler service).\n */\nexport function getRedisUrlOrThrow(prefix?: string): string {\n const url = getRedisUrl(prefix)\n if (url) return url\n const which = prefix ? `${prefix}_REDIS_URL or REDIS_URL` : 'REDIS_URL'\n throw new Error(\n `Redis URL is not configured. Set ${which} in your environment to use a Redis-backed strategy.`\n )\n}\n\n/**\n * Parse a redis:// URL into a {host, port, password, db} object\n * suitable for BullMQ / ioredis structured connection options.\n *\n * @deprecated Prefer passing the full URL via `{ url: getRedisUrl(...) }` to\n * BullMQ/ioredis \u2014 this preserves rediss://, username, database, and query\n * params that structured parsing may lose. Kept for backward compatibility.\n */\nexport function parseRedisUrl(url: string): ParsedRedisConnection {\n try {\n const parsed = new URL(url)\n const dbStr = parsed.pathname ? parsed.pathname.slice(1) : ''\n const dbParsed = dbStr !== '' ? parseInt(dbStr, 10) : NaN\n const db = Number.isNaN(dbParsed) ? undefined : dbParsed\n return {\n host: parsed.hostname || 'localhost',\n port: parseInt(parsed.port, 10) || 6379,\n password: parsed.password || undefined,\n db,\n tls: parsed.protocol === 'rediss:' ? {} : undefined,\n }\n } catch {\n const safeUrl = url.replace(/\\/\\/[^:]*:[^@]*@/, '//<redacted>@')\n
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'redis' })\n\n/**\n * Shared Redis connection utilities.\n *\n * Every package that needs a Redis URL or parsed connection options\n * should import from here instead of reading env vars directly.\n *\n * The `prefix` parameter lets each subsystem define its own override:\n * getRedisUrl('QUEUE') \u2192 QUEUE_REDIS_URL > REDIS_URL > null\n * getRedisUrl('CACHE') \u2192 CACHE_REDIS_URL > REDIS_URL > null\n * getRedisUrl() \u2192 REDIS_URL > null\n *\n * Returns null when Redis is not configured so callers can explicitly\n * decide whether to fall back to a non-Redis strategy or fail loudly.\n * Use getRedisUrlOrThrow() when Redis is mandatory for the caller.\n */\n\nexport type ParsedRedisConnection = {\n host: string\n port: number\n password?: string\n db?: number\n tls?: Record<string, unknown>\n}\n\n/**\n * Resolve a Redis URL from environment variables.\n *\n * Priority: <PREFIX>_REDIS_URL \u2192 REDIS_URL \u2192 null\n *\n * Returns null when no env var is set. Callers MUST NOT assume a\n * localhost default \u2014 silently connecting to localhost masks missing\n * configuration and stalls on platforms where nothing listens there\n * (e.g., WSL2 without a local Redis).\n */\nexport function getRedisUrl(prefix?: string): string | null {\n if (prefix) {\n const prefixed = process.env[`${prefix}_REDIS_URL`]\n if (prefixed) return prefixed\n }\n return process.env.REDIS_URL || null\n}\n\n/**\n * Like getRedisUrl, but throws a descriptive error when Redis is not\n * configured. Use from code paths that require Redis (e.g. BullMQ\n * async queue, Redis cache strategy, scheduler service).\n */\nexport function getRedisUrlOrThrow(prefix?: string): string {\n const url = getRedisUrl(prefix)\n if (url) return url\n const which = prefix ? `${prefix}_REDIS_URL or REDIS_URL` : 'REDIS_URL'\n throw new Error(\n `Redis URL is not configured. Set ${which} in your environment to use a Redis-backed strategy.`\n )\n}\n\n/**\n * Parse a redis:// URL into a {host, port, password, db} object\n * suitable for BullMQ / ioredis structured connection options.\n *\n * @deprecated Prefer passing the full URL via `{ url: getRedisUrl(...) }` to\n * BullMQ/ioredis \u2014 this preserves rediss://, username, database, and query\n * params that structured parsing may lose. Kept for backward compatibility.\n */\nexport function parseRedisUrl(url: string): ParsedRedisConnection {\n try {\n const parsed = new URL(url)\n const dbStr = parsed.pathname ? parsed.pathname.slice(1) : ''\n const dbParsed = dbStr !== '' ? parseInt(dbStr, 10) : NaN\n const db = Number.isNaN(dbParsed) ? undefined : dbParsed\n return {\n host: parsed.hostname || 'localhost',\n port: parseInt(parsed.port, 10) || 6379,\n password: parsed.password || undefined,\n db,\n tls: parsed.protocol === 'rediss:' ? {} : undefined,\n }\n } catch {\n const safeUrl = url.replace(/\\/\\/[^:]*:[^@]*@/, '//<redacted>@')\n logger.warn('Failed to parse Redis URL, falling back to localhost:6379', { url: safeUrl })\n return { host: 'localhost', port: 6379 }\n }\n}\n\n/**\n * Convenience: resolve the URL from env and parse it in one step.\n * Returns null when Redis is not configured.\n */\nexport function resolveRedisConnection(\n prefix?: string,\n): (ParsedRedisConnection & { url: string }) | null {\n const url = getRedisUrl(prefix)\n if (!url) return null\n return { url, ...parseRedisUrl(url) }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAoC3D,SAAS,YAAY,QAAgC;AAC1D,MAAI,QAAQ;AACV,UAAM,WAAW,QAAQ,IAAI,GAAG,MAAM,YAAY;AAClD,QAAI,SAAU,QAAO;AAAA,EACvB;AACA,SAAO,QAAQ,IAAI,aAAa;AAClC;AAOO,SAAS,mBAAmB,QAAyB;AAC1D,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,IAAK,QAAO;AAChB,QAAM,QAAQ,SAAS,GAAG,MAAM,4BAA4B;AAC5D,QAAM,IAAI;AAAA,IACR,oCAAoC,KAAK;AAAA,EAC3C;AACF;AAUO,SAAS,cAAc,KAAoC;AAChE,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAM,QAAQ,OAAO,WAAW,OAAO,SAAS,MAAM,CAAC,IAAI;AAC3D,UAAM,WAAW,UAAU,KAAK,SAAS,OAAO,EAAE,IAAI;AACtD,UAAM,KAAK,OAAO,MAAM,QAAQ,IAAI,SAAY;AAChD,WAAO;AAAA,MACL,MAAM,OAAO,YAAY;AAAA,MACzB,MAAM,SAAS,OAAO,MAAM,EAAE,KAAK;AAAA,MACnC,UAAU,OAAO,YAAY;AAAA,MAC7B;AAAA,MACA,KAAK,OAAO,aAAa,YAAY,CAAC,IAAI;AAAA,IAC5C;AAAA,EACF,QAAQ;AACN,UAAM,UAAU,IAAI,QAAQ,oBAAoB,eAAe;AAC/D,WAAO,KAAK,6DAA6D,EAAE,KAAK,QAAQ,CAAC;AACzF,WAAO,EAAE,MAAM,aAAa,MAAM,KAAK;AAAA,EACzC;AACF;AAMO,SAAS,uBACd,QACkD;AAClD,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,EAAE,KAAK,GAAG,cAAc,GAAG,EAAE;AACtC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/url.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLogger } from "./logger/index.js";
|
|
2
|
+
const logger = createLogger("shared").child({ component: "origin-check" });
|
|
1
3
|
const DEFAULT_DEV_APP_URL = "http://localhost:3000";
|
|
2
4
|
class AppOriginConfigurationError extends Error {
|
|
3
5
|
constructor(message) {
|
|
@@ -83,9 +85,9 @@ function requestOrigins(input) {
|
|
|
83
85
|
}
|
|
84
86
|
function logOriginDebugContext(input, allowedOrigins, rejectedOrigin, level = "error", nodeEnv) {
|
|
85
87
|
if (level === "warn" && nodeEnv === "test") return;
|
|
86
|
-
const log = level === "warn" ?
|
|
88
|
+
const log = (msg, fields) => level === "warn" ? logger.warn(msg, fields) : logger.error(msg, fields);
|
|
87
89
|
if (typeof input === "string") {
|
|
88
|
-
log("
|
|
90
|
+
log("Origin check rejected string input", {
|
|
89
91
|
requestUrl: input,
|
|
90
92
|
rejectedOrigin: rejectedOrigin ?? null,
|
|
91
93
|
allowedOrigins: Array.from(allowedOrigins)
|
|
@@ -93,13 +95,13 @@ function logOriginDebugContext(input, allowedOrigins, rejectedOrigin, level = "e
|
|
|
93
95
|
return;
|
|
94
96
|
}
|
|
95
97
|
if (!input) {
|
|
96
|
-
log("
|
|
98
|
+
log("Origin check rejected empty input", {
|
|
97
99
|
rejectedOrigin: rejectedOrigin ?? null,
|
|
98
100
|
allowedOrigins: Array.from(allowedOrigins)
|
|
99
101
|
});
|
|
100
102
|
return;
|
|
101
103
|
}
|
|
102
|
-
log("
|
|
104
|
+
log("Origin check rejected request", {
|
|
103
105
|
requestUrl: input.url,
|
|
104
106
|
host: input.headers.get("host"),
|
|
105
107
|
forwardedHost: input.headers.get("x-forwarded-host"),
|
|
@@ -202,7 +204,7 @@ function mapSecurityEmailUrlError(error, mapping) {
|
|
|
202
204
|
return { status: 400, body: { error: "Invalid request origin" } };
|
|
203
205
|
}
|
|
204
206
|
if (error instanceof AppOriginConfigurationError) {
|
|
205
|
-
|
|
207
|
+
logger.error("APP_URL is required in production", { scope: mapping.scope });
|
|
206
208
|
return { status: 500, body: { error: mapping.configMessage } };
|
|
207
209
|
}
|
|
208
210
|
return null;
|
package/dist/lib/url.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/url.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { createLogger } from './logger'\n\nconst logger = createLogger('shared').child({ component: 'origin-check' })\n\nconst DEFAULT_DEV_APP_URL = 'http://localhost:3000'\n\ntype EnvLike = Record<string, string | undefined> & {\n APP_URL?: string\n NEXT_PUBLIC_APP_URL?: string\n APP_ALLOWED_ORIGINS?: string\n NODE_ENV?: string\n}\ntype RequestInput = Request | string | undefined\n\nexport type SecurityEmailUrlErrorMapping = {\n scope: string\n configMessage: string\n}\n\nexport class AppOriginConfigurationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginConfigurationError'\n }\n}\n\nexport class AppOriginRejectedError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginRejectedError'\n }\n}\n\nfunction parseBaseUrl(raw: string | undefined): URL | null {\n const value = raw?.trim()\n if (!value) return null\n try {\n const url = new URL(value)\n url.hash = ''\n url.search = ''\n return url\n } catch {\n return null\n }\n}\n\nfunction normalizeBaseUrl(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.toString().replace(/\\/$/, '') : null\n}\n\nfunction normalizeOrigin(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.origin : null\n}\n\nfunction readCsv(value: string | undefined): string[] {\n return (value ?? '')\n .split(',')\n .map((item) => item.trim())\n .filter((item) => item.length > 0)\n}\n\nfunction readAllowedOrigins(env: EnvLike): Set<string> {\n const origins = new Set<string>()\n for (const raw of [env.APP_URL, env.NEXT_PUBLIC_APP_URL, ...readCsv(env.APP_ALLOWED_ORIGINS)]) {\n const origin = normalizeOrigin(raw)\n if (origin) origins.add(origin)\n }\n return origins\n}\n\nfunction readFirstHeaderValue(value: string | null): string | null {\n const first = value?.split(',')[0]?.trim()\n return first && first.length > 0 ? first : null\n}\n\nfunction originFromHost(protocol: string, rawHost: string | null): string | null {\n const host = readFirstHeaderValue(rawHost)\n if (!host) return null\n return normalizeOrigin(`${protocol}//${host}`)\n}\n\ntype RequestOriginCandidates = {\n urlOrigin: string | null\n headerOrigins: Set<string>\n}\n\nfunction readRequestOriginCandidates(input: RequestInput): RequestOriginCandidates {\n const candidates: RequestOriginCandidates = {\n urlOrigin: null,\n headerOrigins: new Set<string>(),\n }\n if (!input) return candidates\n\n const requestUrl = typeof input === 'string' ? input : input.url\n let parsedUrl: URL\n try {\n parsedUrl = new URL(requestUrl)\n } catch {\n return candidates\n }\n\n candidates.urlOrigin = normalizeOrigin(parsedUrl.origin)\n if (typeof input === 'string') return candidates\n\n const forwardedProto = readFirstHeaderValue(input.headers.get('x-forwarded-proto')) ?? parsedUrl.protocol.replace(/:$/, '')\n const protocol = forwardedProto.endsWith(':') ? forwardedProto : `${forwardedProto}:`\n const hostOrigin = originFromHost(protocol, input.headers.get('host'))\n const forwardedHostOrigin = originFromHost(protocol, input.headers.get('x-forwarded-host'))\n if (hostOrigin) candidates.headerOrigins.add(hostOrigin)\n if (forwardedHostOrigin) candidates.headerOrigins.add(forwardedHostOrigin)\n return candidates\n}\n\nfunction requestOrigins(input: RequestInput): Set<string> {\n const origins = new Set<string>()\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n if (urlOrigin) origins.add(urlOrigin)\n for (const origin of headerOrigins) origins.add(origin)\n return origins\n}\n\nfunction logOriginDebugContext(\n input: RequestInput,\n allowedOrigins: Set<string>,\n rejectedOrigin?: string,\n level: 'error' | 'warn' = 'error',\n nodeEnv?: string,\n): void {\n if (level === 'warn' && nodeEnv === 'test') return\n const log = (msg: string, fields: Record<string, unknown>) =>\n level === 'warn' ? logger.warn(msg, fields) : logger.error(msg, fields)\n\n if (typeof input === 'string') {\n log('Origin check rejected string input', {\n requestUrl: input,\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n if (!input) {\n log('Origin check rejected empty input', {\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n log('Origin check rejected request', {\n requestUrl: input.url,\n host: input.headers.get('host'),\n forwardedHost: input.headers.get('x-forwarded-host'),\n forwardedProto: input.headers.get('x-forwarded-proto'),\n derivedOrigins: Array.from(requestOrigins(input)),\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'\n}\n\nfunction isLoopbackOrigin(origin: string): boolean {\n try {\n return isLoopbackHostname(new URL(origin).hostname)\n } catch {\n return false\n }\n}\n\nfunction normalizeOriginPort(url: URL): string {\n if (url.port) return url.port\n if (url.protocol === 'https:') return '443'\n if (url.protocol === 'http:') return '80'\n return ''\n}\n\nfunction isEquivalentLoopbackOrigin(origin: string, allowedOrigin: string): boolean {\n try {\n const candidateUrl = new URL(origin)\n const allowedUrl = new URL(allowedOrigin)\n if (!isLoopbackHostname(candidateUrl.hostname) || !isLoopbackHostname(allowedUrl.hostname)) {\n return false\n }\n return normalizeOriginPort(candidateUrl) === normalizeOriginPort(allowedUrl)\n } catch {\n return false\n }\n}\n\nfunction shouldAllowLoopbackOrigin(origin: string, allowedOrigins: Set<string>, env: EnvLike): boolean {\n if (!isLoopbackOrigin(origin)) return false\n for (const allowedOrigin of allowedOrigins) {\n if (!isLoopbackOrigin(allowedOrigin)) continue\n if (env.NODE_ENV !== 'production') return true\n if (isEquivalentLoopbackOrigin(origin, allowedOrigin)) return true\n }\n return false\n}\n\nexport function assertAllowedAppOrigin(input: RequestInput, env: EnvLike = process.env): void {\n const allowedOrigins = readAllowedOrigins(env)\n if (allowedOrigins.size === 0) {\n if (env.NODE_ENV === 'production') {\n logOriginDebugContext(input, allowedOrigins)\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return\n }\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n const hasAllowedHeaderOrigin = Array.from(headerOrigins).some((origin) => allowedOrigins.has(origin))\n\n for (const origin of headerOrigins) {\n if (allowedOrigins.has(origin)) continue\n if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) continue\n logOriginDebugContext(input, allowedOrigins, origin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n }\n\n if (!urlOrigin) return\n if (allowedOrigins.has(urlOrigin)) return\n if (shouldAllowLoopbackOrigin(urlOrigin, allowedOrigins, env)) return\n if (isLoopbackOrigin(urlOrigin) && hasAllowedHeaderOrigin) return\n\n logOriginDebugContext(input, allowedOrigins, urlOrigin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n}\n\nexport function resolveRequestOrigin(req: Request): string {\n const url = new URL(req.url)\n const proto = req.headers.get('x-forwarded-proto') || url.protocol.replace(':', '')\n const host = req.headers.get('x-forwarded-host') || req.headers.get('host') || url.host\n return `${proto}://${host}`\n}\n\nexport function getAppBaseUrl(req: Request): string {\n return (\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n resolveRequestOrigin(req)\n )\n}\n\nexport function toAbsoluteUrl(req: Request, path: string): string {\n return new URL(path, getAppBaseUrl(req)).toString()\n}\n\nexport function getSecurityEmailBaseUrl(input?: RequestInput, env: EnvLike = process.env): string {\n const configuredAppUrl = normalizeBaseUrl(env.APP_URL)\n if (!configuredAppUrl) {\n if (env.NODE_ENV === 'production') {\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return DEFAULT_DEV_APP_URL\n }\n\n assertAllowedAppOrigin(input, env)\n return configuredAppUrl\n}\n\nexport function toSecurityEmailUrl(input: RequestInput, path: string, env: EnvLike = process.env): string {\n const base = getSecurityEmailBaseUrl(input, env)\n return `${base}/${path.replace(/^\\/+/, '')}`\n}\n\nexport function mapSecurityEmailUrlError(\n error: unknown,\n mapping: SecurityEmailUrlErrorMapping,\n): { status: number; body: { error: string } } | null {\n if (error instanceof AppOriginRejectedError) {\n return { status: 400, body: { error: 'Invalid request origin' } }\n }\n if (error instanceof AppOriginConfigurationError) {\n logger.error('APP_URL is required in production', { scope: mapping.scope })\n return { status: 500, body: { error: mapping.configMessage } }\n }\n return null\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
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.6-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.6471.1.9ab5bdd79a'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { createLogger } from "../lib/logger/index.js";
|
|
2
|
+
const logger = createLogger("shared").child({ component: "analytics-registry" });
|
|
1
3
|
let _analyticsModuleConfigs = null;
|
|
2
4
|
function registerAnalyticsModuleConfigs(configs) {
|
|
3
5
|
if (_analyticsModuleConfigs !== null && process.env.NODE_ENV === "development") {
|
|
4
|
-
|
|
6
|
+
logger.debug("Analytics module configs re-registered (this may occur during HMR)");
|
|
5
7
|
}
|
|
6
8
|
_analyticsModuleConfigs = configs;
|
|
7
9
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/modules/analytics.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '../lib/logger'\n\nconst logger = createLogger('shared').child({ component: 'analytics-registry' })\n\nexport type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\n\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nexport type AnalyticsFieldType = 'numeric' | 'text' | 'uuid' | 'timestamp' | 'jsonb'\n\nexport type AnalyticsEntityTypeConfig = {\n tableName: string\n schema?: string\n dateField: string\n defaultScopeFields: string[]\n}\n\nexport type AnalyticsFieldMapping = {\n dbColumn: string\n type: AnalyticsFieldType\n}\n\nexport type AnalyticsLabelResolverConfig = {\n table: string\n idColumn: string\n labelColumn: string\n}\n\nexport type AnalyticsEntityConfig = {\n entityId: string\n requiredFeatures: string[]\n entityConfig: AnalyticsEntityTypeConfig\n fieldMappings: Record<string, AnalyticsFieldMapping>\n labelResolvers?: Record<string, AnalyticsLabelResolverConfig>\n}\n\nexport type AnalyticsModuleConfig = {\n entities: AnalyticsEntityConfig[]\n}\n\nlet _analyticsModuleConfigs: AnalyticsModuleConfig[] | null = null\n\nexport function registerAnalyticsModuleConfigs(configs: AnalyticsModuleConfig[]): void {\n if (_analyticsModuleConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Analytics module configs re-registered (this may occur during HMR)')\n }\n _analyticsModuleConfigs = configs\n}\n\nexport function getAnalyticsModuleConfigs(): AnalyticsModuleConfig[] {\n return _analyticsModuleConfigs ?? []\n}\n\nexport function getAllAnalyticsEntityConfigs(): AnalyticsEntityConfig[] {\n const configs = getAnalyticsModuleConfigs()\n return configs.flatMap((moduleConfig) => moduleConfig.entities)\n}\n\nexport function getAnalyticsEntityConfig(entityId: string): AnalyticsEntityConfig | null {\n const entities = getAllAnalyticsEntityConfigs()\n return entities.find((e) => e.entityId === entityId) ?? null\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AAsC/E,IAAI,0BAA0D;AAEvD,SAAS,+BAA+B,SAAwC;AACrF,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,WAAO,MAAM,oEAAoE;AAAA,EACnF;AACA,4BAA0B;AAC5B;AAEO,SAAS,4BAAqD;AACnE,SAAO,2BAA2B,CAAC;AACrC;AAEO,SAAS,+BAAwD;AACtE,QAAM,UAAU,0BAA0B;AAC1C,SAAO,QAAQ,QAAQ,CAAC,iBAAiB,aAAa,QAAQ;AAChE;AAEO,SAAS,yBAAyB,UAAgD;AACvF,QAAM,WAAW,6BAA6B;AAC9C,SAAO,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,KAAK;AAC1D;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { createLogger } from "../../lib/logger/index.js";
|
|
3
|
+
const logger = createLogger("dashboard").child({ component: "widgets" });
|
|
2
4
|
function lazyDashboardWidget(loader) {
|
|
3
5
|
let cached = null;
|
|
4
6
|
let pending = null;
|
|
@@ -23,7 +25,7 @@ function lazyDashboardWidget(loader) {
|
|
|
23
25
|
}).catch((err) => {
|
|
24
26
|
if (!cancelled) {
|
|
25
27
|
try {
|
|
26
|
-
|
|
28
|
+
logger.error("Failed to load dashboard widget component", { err });
|
|
27
29
|
} catch {
|
|
28
30
|
}
|
|
29
31
|
setTick((value) => value + 1);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/dashboard/widgets.ts"],
|
|
4
|
-
"sourcesContent": ["import React, { type ComponentType } from 'react'\n\nexport type DashboardWidgetSize = 'sm' | 'md' | 'lg'\n\nexport type DashboardWidgetMetadata = {\n id: string\n title: string\n description?: string\n subtitle?: string\n features?: string[]\n defaultSize?: DashboardWidgetSize\n defaultSettings?: unknown\n defaultEnabled?: boolean\n tags?: string[]\n category?: string\n icon?: string\n supportsRefresh?: boolean\n}\n\nexport type DashboardLayoutItem = {\n id: string\n widgetId: string\n order: number\n priority?: number\n size?: DashboardWidgetSize\n settings?: unknown\n}\n\nexport type DashboardWidgetRenderMode = 'view' | 'settings'\n\nexport type DashboardWidgetRenderContext = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n userName?: string | null\n userEmail?: string | null\n userLabel?: string | null\n}\n\nexport type DashboardWidgetComponentProps<TSettings = unknown> = {\n mode: DashboardWidgetRenderMode\n layout: DashboardLayoutItem\n settings: TSettings\n context: DashboardWidgetRenderContext\n onSettingsChange: (next: TSettings) => void\n refreshToken: number\n onRefreshStateChange?: (refreshing: boolean) => void\n}\n\nexport type DashboardWidgetModule<TSettings = unknown> = {\n metadata: DashboardWidgetMetadata\n Widget: ComponentType<DashboardWidgetComponentProps<TSettings>>\n hydrateSettings?: (raw: unknown) => TSettings\n dehydrateSettings?: (settings: TSettings) => unknown\n}\n\nexport type DashboardWidgetRenderProps<TSettings = unknown> = DashboardWidgetComponentProps<TSettings>\n\ntype DashboardWidgetLoader<TSettings> = () => Promise<\n | { default: ComponentType<DashboardWidgetComponentProps<TSettings>> }\n | ComponentType<DashboardWidgetComponentProps<TSettings>>\n>\n\nexport function lazyDashboardWidget<TSettings>(\n loader: DashboardWidgetLoader<TSettings>,\n): ComponentType<DashboardWidgetComponentProps<TSettings>> {\n let cached: ComponentType<DashboardWidgetComponentProps<TSettings>> | null = null\n let pending: Promise<void> | null = null\n\n const load = () => {\n if (cached) return Promise.resolve()\n if (!pending) {\n pending = loader()\n .then((mod) => {\n cached = (mod as { default?: ComponentType<DashboardWidgetComponentProps<TSettings>> }).default ??\n (mod as ComponentType<DashboardWidgetComponentProps<TSettings>>)\n })\n .catch((err) => {\n pending = null\n throw err\n })\n }\n return pending\n }\n\n const LazyWidget: ComponentType<DashboardWidgetComponentProps<TSettings>> = (props) => {\n const [, setTick] = React.useState(0)\n React.useEffect(() => {\n let cancelled = false\n void load()\n .then(() => {\n if (!cancelled) setTick((value) => value + 1)\n })\n .catch((err) => {\n if (!cancelled) {\n try {\n
|
|
5
|
-
"mappings": "AAAA,OAAO,WAAmC;AA+
|
|
4
|
+
"sourcesContent": ["import React, { type ComponentType } from 'react'\nimport { createLogger } from '../../lib/logger'\n\nconst logger = createLogger('dashboard').child({ component: 'widgets' })\n\nexport type DashboardWidgetSize = 'sm' | 'md' | 'lg'\n\nexport type DashboardWidgetMetadata = {\n id: string\n title: string\n description?: string\n subtitle?: string\n features?: string[]\n defaultSize?: DashboardWidgetSize\n defaultSettings?: unknown\n defaultEnabled?: boolean\n tags?: string[]\n category?: string\n icon?: string\n supportsRefresh?: boolean\n}\n\nexport type DashboardLayoutItem = {\n id: string\n widgetId: string\n order: number\n priority?: number\n size?: DashboardWidgetSize\n settings?: unknown\n}\n\nexport type DashboardWidgetRenderMode = 'view' | 'settings'\n\nexport type DashboardWidgetRenderContext = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n userName?: string | null\n userEmail?: string | null\n userLabel?: string | null\n}\n\nexport type DashboardWidgetComponentProps<TSettings = unknown> = {\n mode: DashboardWidgetRenderMode\n layout: DashboardLayoutItem\n settings: TSettings\n context: DashboardWidgetRenderContext\n onSettingsChange: (next: TSettings) => void\n refreshToken: number\n onRefreshStateChange?: (refreshing: boolean) => void\n}\n\nexport type DashboardWidgetModule<TSettings = unknown> = {\n metadata: DashboardWidgetMetadata\n Widget: ComponentType<DashboardWidgetComponentProps<TSettings>>\n hydrateSettings?: (raw: unknown) => TSettings\n dehydrateSettings?: (settings: TSettings) => unknown\n}\n\nexport type DashboardWidgetRenderProps<TSettings = unknown> = DashboardWidgetComponentProps<TSettings>\n\ntype DashboardWidgetLoader<TSettings> = () => Promise<\n | { default: ComponentType<DashboardWidgetComponentProps<TSettings>> }\n | ComponentType<DashboardWidgetComponentProps<TSettings>>\n>\n\nexport function lazyDashboardWidget<TSettings>(\n loader: DashboardWidgetLoader<TSettings>,\n): ComponentType<DashboardWidgetComponentProps<TSettings>> {\n let cached: ComponentType<DashboardWidgetComponentProps<TSettings>> | null = null\n let pending: Promise<void> | null = null\n\n const load = () => {\n if (cached) return Promise.resolve()\n if (!pending) {\n pending = loader()\n .then((mod) => {\n cached = (mod as { default?: ComponentType<DashboardWidgetComponentProps<TSettings>> }).default ??\n (mod as ComponentType<DashboardWidgetComponentProps<TSettings>>)\n })\n .catch((err) => {\n pending = null\n throw err\n })\n }\n return pending\n }\n\n const LazyWidget: ComponentType<DashboardWidgetComponentProps<TSettings>> = (props) => {\n const [, setTick] = React.useState(0)\n React.useEffect(() => {\n let cancelled = false\n void load()\n .then(() => {\n if (!cancelled) setTick((value) => value + 1)\n })\n .catch((err) => {\n if (!cancelled) {\n try {\n logger.error('Failed to load dashboard widget component', { err })\n } catch {}\n setTick((value) => value + 1)\n }\n })\n return () => {\n cancelled = true\n }\n }, [])\n if (!cached) return null\n return React.createElement(cached, props)\n }\n\n return LazyWidget\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,WAAmC;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,UAAU,CAAC;AA+DhE,SAAS,oBACd,QACyD;AACzD,MAAI,SAAyE;AAC7E,MAAI,UAAgC;AAEpC,QAAM,OAAO,MAAM;AACjB,QAAI,OAAQ,QAAO,QAAQ,QAAQ;AACnC,QAAI,CAAC,SAAS;AACZ,gBAAU,OAAO,EACd,KAAK,CAAC,QAAQ;AACb,iBAAU,IAA8E,WACrF;AAAA,MACL,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,kBAAU;AACV,cAAM;AAAA,MACR,CAAC;AAAA,IACL;AACA,WAAO;AAAA,EACT;AAEA,QAAM,aAAsE,CAAC,UAAU;AACrF,UAAM,CAAC,EAAE,OAAO,IAAI,MAAM,SAAS,CAAC;AACpC,UAAM,UAAU,MAAM;AACpB,UAAI,YAAY;AAChB,WAAK,KAAK,EACP,KAAK,MAAM;AACV,YAAI,CAAC,UAAW,SAAQ,CAAC,UAAU,QAAQ,CAAC;AAAA,MAC9C,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,YAAI,CAAC,WAAW;AACd,cAAI;AACF,mBAAO,MAAM,6CAA6C,EAAE,IAAI,CAAC;AAAA,UACnE,QAAQ;AAAA,UAAC;AACT,kBAAQ,CAAC,UAAU,QAAQ,CAAC;AAAA,QAC9B;AAAA,MACF,CAAC;AACH,aAAO,MAAM;AACX,oBAAY;AAAA,MACd;AAAA,IACF,GAAG,CAAC,CAAC;AACL,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,MAAM,cAAc,QAAQ,KAAK;AAAA,EAC1C;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLogger } from "../../lib/logger/index.js";
|
|
2
|
+
const logger = createLogger("events").child({ component: "factory" });
|
|
1
3
|
const GLOBAL_EVENT_BUS_KEY = "__openMercatoGlobalEventBus__";
|
|
2
4
|
let globalEventBus = null;
|
|
3
5
|
function setGlobalEventBus(bus) {
|
|
@@ -46,7 +48,7 @@ function isPortalBroadcastEvent(eventId) {
|
|
|
46
48
|
let _registeredEventConfigs = null;
|
|
47
49
|
function registerEventModuleConfigs(configs) {
|
|
48
50
|
if (_registeredEventConfigs !== null && process.env.NODE_ENV === "development") {
|
|
49
|
-
|
|
51
|
+
logger.debug("Event module configs re-registered (this may occur during HMR)");
|
|
50
52
|
}
|
|
51
53
|
_registeredEventConfigs = configs;
|
|
52
54
|
for (const config of configs) {
|
|
@@ -74,12 +76,12 @@ function createModuleEvents(options) {
|
|
|
74
76
|
if (strict) {
|
|
75
77
|
throw new Error(message);
|
|
76
78
|
} else {
|
|
77
|
-
|
|
79
|
+
logger.error("Module tried to emit undeclared event \u2014 add it to the module events.ts first", { moduleId, eventId });
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
const eventBus = getGlobalEventBus();
|
|
81
83
|
if (!eventBus) {
|
|
82
|
-
|
|
84
|
+
logger.warn("Event bus not available, cannot emit event", { eventId });
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
85
87
|
await eventBus.emit(eventId, payload, emitOptions);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/events/factory.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\n// Global set of all declared event IDs for runtime validation\nconst allDeclaredEventIds = new Set<string>()\n\n// Global registry of all declared events with their full definitions\nconst allDeclaredEvents: EventDefinition[] = []\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n allDeclaredEventIds.add(event.id)\n // Avoid duplicates if createModuleEvents/registerEventModuleConfigs is called multiple times (e.g., HMR)\n if (!allDeclaredEvents.find(e => e.id === event.id)) {\n allDeclaredEvents.push(event)\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return allDeclaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(allDeclaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...allDeclaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = allDeclaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = allDeclaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\nlet _registeredEventConfigs: EventModuleConfig[] | null = null\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n if (_registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport { createLogger } from '../../lib/logger'\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\nconst logger = createLogger('events').child({ component: 'factory' })\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\n// Global set of all declared event IDs for runtime validation\nconst allDeclaredEventIds = new Set<string>()\n\n// Global registry of all declared events with their full definitions\nconst allDeclaredEvents: EventDefinition[] = []\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n allDeclaredEventIds.add(event.id)\n // Avoid duplicates if createModuleEvents/registerEventModuleConfigs is called multiple times (e.g., HMR)\n if (!allDeclaredEvents.find(e => e.id === event.id)) {\n allDeclaredEvents.push(event)\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return allDeclaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(allDeclaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...allDeclaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = allDeclaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = allDeclaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\nlet _registeredEventConfigs: EventModuleConfig[] | null = null\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n if (_registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n _registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return _registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added\n const fullEvents: EventDefinition[] = events.map(e => ({\n ...e,\n module: moduleId,\n }))\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n logger.error('Module tried to emit undeclared event \u2014 add it to the module events.ts first', { moduleId, eventId })\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n logger.warn('Event bus not available, cannot emit event', { eventId })\n return\n }\n\n await eventBus.emit(eventId, payload, emitOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,oBAAoB;AAU7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,UAAU,CAAC;AAapE,MAAM,uBAAuB;AAG7B,IAAI,iBAAwC;AAMrC,SAAS,kBAAkB,KAA2B;AAC3D,mBAAiB;AACjB,MAAI;AACF;AAAC,IAAC,WAAuC,oBAAoB,IAAI;AAAA,EACnE,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,oBAA2C;AACzD,MAAI;AACF,UAAM,YAAa,WAAuC,oBAAoB;AAC9E,QAAI,aAAa,OAAO,cAAc,YAAY,OAAQ,UAA6B,SAAS,YAAY;AAC1G,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAOA,MAAM,sBAAsB,oBAAI,IAAY;AAG5C,MAAM,oBAAuC,CAAC;AAE9C,SAAS,iBAAiB,OAA8B;AACtD,sBAAoB,IAAI,MAAM,EAAE;AAEhC,MAAI,CAAC,kBAAkB,KAAK,OAAK,EAAE,OAAO,MAAM,EAAE,GAAG;AACnD,sBAAkB,KAAK,KAAK;AAAA,EAC9B;AACF;AAMO,SAAS,gBAAgB,SAA0B;AACxD,SAAO,oBAAoB,IAAI,OAAO;AACxC;AAMO,SAAS,yBAAmC;AACjD,SAAO,MAAM,KAAK,mBAAmB;AACvC;AAMO,SAAS,oBAAuC;AACrD,SAAO,CAAC,GAAG,iBAAiB;AAC9B;AAMO,SAAS,iBAAiB,SAA0B;AACzD,QAAM,QAAQ,kBAAkB,KAAK,OAAK,EAAE,OAAO,OAAO;AAC1D,SAAO,OAAO,oBAAoB;AACpC;AAMO,SAAS,uBAAuB,SAA0B;AAC/D,QAAM,QAAQ,kBAAkB,KAAK,OAAK,EAAE,OAAO,OAAO;AAC1D,SAAO,OAAO,oBAAoB;AACpC;AAMA,IAAI,0BAAsD;AAMnD,SAAS,2BAA2B,SAAoC;AAC7E,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,WAAO,MAAM,gEAAgE;AAAA,EAC/E;AACA,4BAA0B;AAC1B,aAAW,UAAU,SAAS;AAC5B,eAAW,SAAS,OAAO,QAAQ;AACjC,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF;AACF;AAMO,SAAS,wBAA6C;AAC3D,SAAO,2BAA2B,CAAC;AACrC;AAyCO,SAAS,mBAGd,SAA6E;AAC7E,QAAM,EAAE,UAAU,QAAQ,SAAS,MAAM,IAAI;AAG7C,QAAM,gBAAgB,IAAI,IAAI,OAAO,IAAI,OAAK,EAAE,EAAE,CAAC;AAGnD,QAAM,aAAgC,OAAO,IAAI,QAAM;AAAA,IACrD,GAAG;AAAA,IACH,QAAQ;AAAA,EACV,EAAE;AAGF,aAAW,SAAS,YAAY;AAC9B,qBAAiB,KAAK;AAAA,EACxB;AAKA,QAAM,OAAO,OACX,SACA,SACA,gBACkB;AAElB,QAAI,CAAC,cAAc,IAAI,OAAO,GAAG;AAC/B,YAAM,UACJ,oBAAoB,QAAQ,qCAAqC,OAAO;AAG1E,UAAI,QAAQ;AACV,cAAM,IAAI,MAAM,OAAO;AAAA,MACzB,OAAO;AACL,eAAO,MAAM,qFAAgF,EAAE,UAAU,QAAQ,CAAC;AAAA,MAEpH;AAAA,IACF;AAGA,UAAM,WAAW,kBAAkB;AACnC,QAAI,CAAC,UAAU;AACb,aAAO,KAAK,8CAA8C,EAAE,QAAQ,CAAC;AACrE;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,SAAS,SAAS,WAAW;AAAA,EACnD;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -48,15 +48,15 @@ function applyModuleOverridesFromEnabledModules(modules) {
|
|
|
48
48
|
if (!warnedUnwiredDomains.has(domain)) {
|
|
49
49
|
warnedUnwiredDomains.add(domain);
|
|
50
50
|
const moduleIds = Array.from(new Set(entries.map((e) => e.moduleId))).join(", ");
|
|
51
|
-
|
|
52
|
-
`[Module Overrides] Domain "${domain}" not yet wired \u2014 entry.overrides.${domain} for module(s) [${moduleIds}] was ignored. ${TRACKING_ISSUE_HINT}`
|
|
53
|
-
);
|
|
51
|
+
logger.warn("Override domain not yet wired \u2014 entry ignored", { domain, moduleIds, hint: TRACKING_ISSUE_HINT });
|
|
54
52
|
}
|
|
55
53
|
continue;
|
|
56
54
|
}
|
|
57
55
|
applier(entries);
|
|
58
56
|
}
|
|
59
57
|
}
|
|
58
|
+
import { createLogger } from "../lib/logger/index.js";
|
|
59
|
+
const logger = createLogger("shared").child({ component: "module-overrides" });
|
|
60
60
|
const pageRouteOverrideStore = { modules: {}, programmatic: {} };
|
|
61
61
|
const subscriberOverrideStore = { modules: {}, programmatic: {} };
|
|
62
62
|
const workerOverrideStore = { modules: {}, programmatic: {} };
|
|
@@ -78,7 +78,7 @@ function normalizeIdOverrideKey(key, label) {
|
|
|
78
78
|
if (typeof key !== "string") return null;
|
|
79
79
|
const trimmed = key.trim();
|
|
80
80
|
if (trimmed) return trimmed;
|
|
81
|
-
|
|
81
|
+
logger.warn("Skipping malformed override key \u2014 expected a non-empty string", { label, key });
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
84
|
function clearStore(store) {
|
|
@@ -121,17 +121,13 @@ function applyArrayOverrides(items, overrides, options) {
|
|
|
121
121
|
const replacement = overrides[id];
|
|
122
122
|
if (replacement === null) continue;
|
|
123
123
|
if (options.isReplacement && !options.isReplacement(replacement)) {
|
|
124
|
-
|
|
125
|
-
`[Module Overrides] Skipping malformed ${options.label} override for "${id}" \u2014 replacement has the wrong shape.`
|
|
126
|
-
);
|
|
124
|
+
logger.warn("Skipping malformed override \u2014 replacement has the wrong shape", { label: options.label, id });
|
|
127
125
|
result.push(item);
|
|
128
126
|
continue;
|
|
129
127
|
}
|
|
130
128
|
const replacementId = options.getId(replacement);
|
|
131
129
|
if (replacementId !== id) {
|
|
132
|
-
|
|
133
|
-
`[Module Overrides] Skipping malformed ${options.label} override for "${id}" \u2014 replacement id must match the override key.`
|
|
134
|
-
);
|
|
130
|
+
logger.warn("Skipping malformed override \u2014 replacement id must match the override key", { label: options.label, id });
|
|
135
131
|
result.push(item);
|
|
136
132
|
continue;
|
|
137
133
|
}
|
|
@@ -142,7 +138,7 @@ function applyArrayOverrides(items, overrides, options) {
|
|
|
142
138
|
function warnStaleOverrides(label, overrides, consumed) {
|
|
143
139
|
for (const key of Object.keys(overrides)) {
|
|
144
140
|
if (!consumed.has(key)) {
|
|
145
|
-
|
|
141
|
+
logger.warn("Override did not match any registered entry \u2014 override skipped", { label, key });
|
|
146
142
|
}
|
|
147
143
|
}
|
|
148
144
|
}
|
|
@@ -232,9 +228,7 @@ function applyApiRouteOverrides(overrides) {
|
|
|
232
228
|
for (const [rawKey, value] of Object.entries(overrides)) {
|
|
233
229
|
const key = normalizeApiRouteOverrideKey(rawKey);
|
|
234
230
|
if (!key) {
|
|
235
|
-
|
|
236
|
-
`[Module Overrides] Skipping malformed routes.api key "${rawKey}" \u2014 expected "METHOD /api/path".`
|
|
237
|
-
);
|
|
231
|
+
logger.warn('Skipping malformed routes.api key \u2014 expected "METHOD /api/path"', { key: rawKey });
|
|
238
232
|
continue;
|
|
239
233
|
}
|
|
240
234
|
programmaticApiRouteOverrides[key] = value;
|
|
@@ -403,9 +397,7 @@ function applyApiOverridesToManifests(routes, overrides) {
|
|
|
403
397
|
methodOverrides.set(method, value);
|
|
404
398
|
remainingMethods.push(method);
|
|
405
399
|
} else {
|
|
406
|
-
|
|
407
|
-
`[Module Overrides] Skipping malformed routes.api override for "${key}" \u2014 expected { handler, metadata? } or null.`
|
|
408
|
-
);
|
|
400
|
+
logger.warn("Skipping malformed routes.api override \u2014 expected { handler, metadata? } or null", { key });
|
|
409
401
|
remainingMethods.push(method);
|
|
410
402
|
}
|
|
411
403
|
}
|
|
@@ -433,9 +425,7 @@ function applyApiOverridesToManifests(routes, overrides) {
|
|
|
433
425
|
}
|
|
434
426
|
for (const key of overrideKeys) {
|
|
435
427
|
if (!consumedKeys.has(key)) {
|
|
436
|
-
|
|
437
|
-
`[Module Overrides] routes.api override "${key}" did not match any registered API route \u2014 override skipped.`
|
|
438
|
-
);
|
|
428
|
+
logger.warn("routes.api override did not match any registered API route \u2014 override skipped", { key });
|
|
439
429
|
}
|
|
440
430
|
}
|
|
441
431
|
return result;
|
|
@@ -446,9 +436,7 @@ function applyPageOverridesToManifests(routes, overrides, kind) {
|
|
|
446
436
|
for (const [rawKey, value] of Object.entries(overrides)) {
|
|
447
437
|
const key = normalizePageRouteOverrideKey(rawKey);
|
|
448
438
|
if (!key) {
|
|
449
|
-
|
|
450
|
-
`[Module Overrides] Skipping malformed routes.pages key "${rawKey}" \u2014 expected "/backend/path" or "/frontend/path".`
|
|
451
|
-
);
|
|
439
|
+
logger.warn('Skipping malformed routes.pages key \u2014 expected "/backend/path" or "/frontend/path"', { key: rawKey });
|
|
452
440
|
continue;
|
|
453
441
|
}
|
|
454
442
|
normalizedOverrides[key] = value;
|
|
@@ -468,18 +456,14 @@ function applyPageOverridesToManifests(routes, overrides, kind) {
|
|
|
468
456
|
const override = normalizedOverrides[key];
|
|
469
457
|
if (override === null) continue;
|
|
470
458
|
if (!override || typeof override !== "object") {
|
|
471
|
-
|
|
472
|
-
`[Module Overrides] Skipping malformed routes.pages override for "${key}" \u2014 expected { load?, Component?, metadata? } or null.`
|
|
473
|
-
);
|
|
459
|
+
logger.warn("Skipping malformed routes.pages override \u2014 expected { load?, Component?, metadata? } or null", { key });
|
|
474
460
|
result.push(entry);
|
|
475
461
|
continue;
|
|
476
462
|
}
|
|
477
463
|
const hasLoad = typeof override.load === "function";
|
|
478
464
|
const hasComponent = typeof override.Component === "function";
|
|
479
465
|
if (!hasLoad && !hasComponent && override.metadata === void 0) {
|
|
480
|
-
|
|
481
|
-
`[Module Overrides] Skipping malformed routes.pages override for "${key}" \u2014 expected a loader, component, metadata, or null.`
|
|
482
|
-
);
|
|
466
|
+
logger.warn("Skipping malformed routes.pages override \u2014 expected a loader, component, metadata, or null", { key });
|
|
483
467
|
result.push(entry);
|
|
484
468
|
continue;
|
|
485
469
|
}
|
|
@@ -581,13 +565,13 @@ function applyComponentOverridesToEntries(entries, overrides = composeComponentO
|
|
|
581
565
|
const value = overrides[key];
|
|
582
566
|
if (value === null) continue;
|
|
583
567
|
if (!isComponentOverrideValue(value)) {
|
|
584
|
-
|
|
568
|
+
logger.warn("Skipping malformed widgets.components override \u2014 expected ComponentOverride, ComponentOverride[], or null", { key });
|
|
585
569
|
continue;
|
|
586
570
|
}
|
|
587
571
|
const values = Array.isArray(value) ? value : [value];
|
|
588
572
|
for (const override of values) {
|
|
589
573
|
if (override.target.componentId !== key) {
|
|
590
|
-
|
|
574
|
+
logger.warn("Skipping malformed widgets.components override \u2014 target.componentId must match the override key", { key });
|
|
591
575
|
continue;
|
|
592
576
|
}
|
|
593
577
|
additions.push(override);
|
|
@@ -758,9 +742,7 @@ function routesOverridesApplier(entries) {
|
|
|
758
742
|
for (const [rawKey, value] of Object.entries(api)) {
|
|
759
743
|
const key = normalizeApiRouteOverrideKey(rawKey);
|
|
760
744
|
if (!key) {
|
|
761
|
-
|
|
762
|
-
`[Module Overrides] Skipping malformed routes.api key "${rawKey}" \u2014 expected "METHOD /api/path".`
|
|
763
|
-
);
|
|
745
|
+
logger.warn('Skipping malformed routes.api key \u2014 expected "METHOD /api/path"', { key: rawKey });
|
|
764
746
|
continue;
|
|
765
747
|
}
|
|
766
748
|
modulesConfigApiRouteOverrides[key] = value;
|