@open-mercato/shared 0.6.7-develop.6860.1.f796b22e80 → 0.6.7
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 +6 -57
- package/build.mjs +4 -2
- package/dist/lib/auth/server.js +2 -15
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +43 -117
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +1 -5
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +2 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +11 -2
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +4 -63
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-runner.js +2 -2
- package/dist/lib/crud/interceptor-runner.js.map +2 -2
- package/dist/lib/crud/mutation-guard-registry.js +2 -2
- package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
- package/dist/lib/crud/types.js +0 -4
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/engine.js +11 -37
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +12 -3
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/db/pg-errors.js +0 -42
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +6 -48
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +0 -56
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +10 -2
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +17 -52
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/index.js +3 -47
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +1 -5
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/query/engine.js +40 -54
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +2 -7
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/config.js +2 -61
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenize.js +11 -23
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/index.js +1 -13
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +7 -12
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/overrides.js +1 -50
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +1 -8
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js +0 -1
- package/dist/modules/registry.js.map +2 -2
- package/dist/security/enabledModulesRegistry.js +11 -56
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/jest.config.cjs +1 -2
- package/package.json +9 -19
- package/src/lib/ai/llm-provider.ts +0 -33
- package/src/lib/auth/__tests__/server.test.ts +0 -23
- package/src/lib/auth/server.ts +2 -23
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +8 -63
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +1 -21
- package/src/lib/bootstrap/dynamicLoader.ts +65 -213
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +0 -28
- package/src/lib/commands/command-bus.ts +1 -5
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +0 -298
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +0 -26
- package/src/lib/crud/enricher-runner.ts +11 -2
- package/src/lib/crud/factory.ts +1 -69
- package/src/lib/crud/interceptor-runner.ts +2 -2
- package/src/lib/crud/mutation-guard-registry.ts +2 -2
- package/src/lib/crud/types.ts +0 -3
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +3 -18
- package/src/lib/data/engine.ts +16 -50
- package/src/lib/db/__tests__/mikro.test.ts +25 -0
- package/src/lib/db/mikro.ts +38 -13
- package/src/lib/db/pg-errors.ts +0 -58
- package/src/lib/di/container.ts +8 -72
- package/src/lib/email/__tests__/send.test.ts +1 -37
- package/src/lib/email/send.ts +0 -79
- package/src/lib/encryption/__tests__/subscriber.test.ts +2 -54
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -58
- package/src/lib/encryption/subscriber.ts +14 -2
- package/src/lib/encryption/tenantDataEncryptionService.ts +18 -64
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +2 -6
- package/src/lib/logger/__tests__/logger.test.ts +0 -77
- package/src/lib/logger/index.ts +1 -59
- package/src/lib/modules/registry.ts +0 -10
- package/src/lib/query/__tests__/engine.test.ts +13 -65
- package/src/lib/query/engine.ts +58 -75
- package/src/lib/query/types.ts +5 -4
- package/src/lib/redis/__tests__/connection.test.ts +0 -16
- package/src/lib/redis/connection.ts +7 -10
- package/src/lib/search/__tests__/config.test.ts +0 -177
- package/src/lib/search/config.ts +8 -106
- package/src/lib/search/tokenize.ts +11 -33
- package/src/lib/webhooks/__tests__/verify.test.ts +1 -20
- package/src/lib/webhooks/index.ts +1 -19
- package/src/lib/webhooks/verify.ts +9 -17
- package/src/modules/__tests__/registry.test.ts +2 -10
- package/src/modules/analytics.ts +0 -7
- package/src/modules/customer-auth.ts +0 -1
- package/src/modules/encryption.ts +0 -3
- package/src/modules/navigation/backendChrome.ts +0 -15
- package/src/modules/overrides.ts +0 -103
- package/src/modules/payment_gateways/types.ts +0 -41
- package/src/modules/registry.ts +0 -3
- package/src/security/enabledModulesRegistry.ts +12 -64
- package/dist/lib/ai/safety-identifier.js +0 -32
- package/dist/lib/ai/safety-identifier.js.map +0 -7
- package/dist/lib/auth/organizationScope.js +0 -34
- package/dist/lib/auth/organizationScope.js.map +0 -7
- package/dist/lib/bootstrap/clientOnlyModules.js +0 -55
- package/dist/lib/bootstrap/clientOnlyModules.js.map +0 -7
- package/dist/lib/data/consistency.js +0 -19
- package/dist/lib/data/consistency.js.map +0 -7
- package/dist/lib/db/entityMetadata.js +0 -26
- package/dist/lib/db/entityMetadata.js.map +0 -7
- package/dist/lib/logger/extension.js +0 -29
- package/dist/lib/logger/extension.js.map +0 -7
- package/dist/lib/modules/surfaceFingerprint.js +0 -47
- package/dist/lib/modules/surfaceFingerprint.js.map +0 -7
- package/dist/lib/query/ciphertext-search-warning.js +0 -45
- package/dist/lib/query/ciphertext-search-warning.js.map +0 -7
- package/dist/lib/search/auto-indexing.js +0 -14
- package/dist/lib/search/auto-indexing.js.map +0 -7
- package/dist/lib/search/availability.js +0 -107
- package/dist/lib/search/availability.js.map +0 -7
- package/dist/lib/search/tokenLookup.js +0 -46
- package/dist/lib/search/tokenLookup.js.map +0 -7
- package/dist/lib/telemetry/runtime.js +0 -35
- package/dist/lib/telemetry/runtime.js.map +0 -7
- package/dist/lib/webhooks/body.js +0 -57
- package/dist/lib/webhooks/body.js.map +0 -7
- package/dist/lib/webhooks/inbound-types.js +0 -1
- package/dist/lib/webhooks/inbound-types.js.map +0 -7
- package/dist/modules/widgets/extension-points.js +0 -96
- package/dist/modules/widgets/extension-points.js.map +0 -7
- package/dist/security/featurePolicy.js +0 -62
- package/dist/security/featurePolicy.js.map +0 -7
- package/scripts/versionSource.cjs +0 -65
- package/src/lib/__tests__/versionSource.test.ts +0 -46
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +0 -59
- package/src/lib/ai/__tests__/safety-identifier.test.ts +0 -71
- package/src/lib/ai/safety-identifier.ts +0 -73
- package/src/lib/auth/__tests__/organizationScope.test.ts +0 -107
- package/src/lib/auth/organizationScope.ts +0 -65
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +0 -189
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +0 -98
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +0 -211
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +0 -192
- package/src/lib/bootstrap/clientOnlyModules.ts +0 -85
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +0 -171
- package/src/lib/data/__tests__/consistency.test.ts +0 -38
- package/src/lib/data/consistency.ts +0 -17
- package/src/lib/db/__tests__/entityMetadata.test.ts +0 -38
- package/src/lib/db/__tests__/pg-errors.test.ts +0 -48
- package/src/lib/db/entityMetadata.ts +0 -43
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +0 -155
- package/src/lib/di/__tests__/container-app-di.test.ts +0 -173
- package/src/lib/di/__tests__/registrar-error-log.test.ts +0 -154
- package/src/lib/i18n/__tests__/config.test.ts +0 -8
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +0 -30
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +0 -29
- package/src/lib/logger/extension.ts +0 -59
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +0 -122
- package/src/lib/modules/surfaceFingerprint.ts +0 -87
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +0 -178
- package/src/lib/query/ciphertext-search-warning.ts +0 -95
- package/src/lib/search/__tests__/availability.test.ts +0 -211
- package/src/lib/search/__tests__/tokenLookup.test.ts +0 -206
- package/src/lib/search/__tests__/tokenize.test.ts +0 -49
- package/src/lib/search/auto-indexing.ts +0 -22
- package/src/lib/search/availability.ts +0 -232
- package/src/lib/search/tokenLookup.ts +0 -133
- package/src/lib/telemetry/runtime.ts +0 -72
- package/src/lib/webhooks/__tests__/body.test.ts +0 -95
- package/src/lib/webhooks/body.ts +0 -70
- package/src/lib/webhooks/inbound-types.ts +0 -125
- package/src/modules/__tests__/nav-group-order-override.test.ts +0 -183
- package/src/modules/payment_gateways/__tests__/types.test.ts +0 -37
- package/src/modules/widgets/__tests__/extension-points.test.ts +0 -101
- package/src/modules/widgets/extension-points.ts +0 -524
- package/src/security/__tests__/featurePolicy.test.ts +0 -166
- package/src/security/featurePolicy.ts +0 -89
|
@@ -80,9 +80,6 @@ class TenantDataEncryptionService {
|
|
|
80
80
|
this.missCache = TenantDataEncryptionService.globalMissCache;
|
|
81
81
|
this.cache = opts?.cache;
|
|
82
82
|
this.kms = opts?.kms ?? createKmsService();
|
|
83
|
-
this.systemDefaultMaps = new Map(
|
|
84
|
-
(opts?.defaultEncryptionMaps ?? []).filter((map) => map.keyScope === "system").map((map) => [map.entityId, map])
|
|
85
|
-
);
|
|
86
83
|
}
|
|
87
84
|
static {
|
|
88
85
|
this.globalMemoryCache = /* @__PURE__ */ new Map();
|
|
@@ -163,23 +160,6 @@ class TenantDataEncryptionService {
|
|
|
163
160
|
fields: readEncryptedFieldsJson(row)
|
|
164
161
|
};
|
|
165
162
|
}
|
|
166
|
-
applySystemDefault(record, entityId) {
|
|
167
|
-
const declared = this.systemDefaultMaps.get(entityId);
|
|
168
|
-
if (!declared) return record;
|
|
169
|
-
const fields = declared.fields.map((field) => ({
|
|
170
|
-
field: field.field,
|
|
171
|
-
hashField: field.hashField ?? null
|
|
172
|
-
}));
|
|
173
|
-
const declaredFields = new Set(fields.map((field) => field.field));
|
|
174
|
-
for (const field of record?.fields ?? []) {
|
|
175
|
-
if (!declaredFields.has(field.field)) fields.push(field);
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
entityId,
|
|
179
|
-
keyScope: "system",
|
|
180
|
-
fields
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
163
|
async getMap(key) {
|
|
184
164
|
const shouldSkipLookup = (tag) => {
|
|
185
165
|
const expiresAt = this.missCache.get(tag);
|
|
@@ -202,13 +182,13 @@ class TenantDataEncryptionService {
|
|
|
202
182
|
if (this.inflightMaps.has(tag)) {
|
|
203
183
|
const pending2 = this.inflightMaps.get(tag);
|
|
204
184
|
const resolved = await pending2;
|
|
205
|
-
if (resolved) return
|
|
185
|
+
if (resolved) return resolved;
|
|
206
186
|
}
|
|
207
187
|
const mem = this.memoryCache.get(tag);
|
|
208
|
-
if (mem) return
|
|
188
|
+
if (mem) return mem;
|
|
209
189
|
if (this.cache && typeof this.cache.get === "function") {
|
|
210
190
|
const cached = await this.cache.get(tag);
|
|
211
|
-
if (cached) return
|
|
191
|
+
if (cached) return cached;
|
|
212
192
|
}
|
|
213
193
|
const pending = this.fetchMap(candidate);
|
|
214
194
|
this.inflightMaps.set(tag, pending);
|
|
@@ -228,9 +208,9 @@ class TenantDataEncryptionService {
|
|
|
228
208
|
if (this.cache && typeof this.cache.set === "function") {
|
|
229
209
|
await this.cache.set(tag, loaded, { ttl: 300 });
|
|
230
210
|
}
|
|
231
|
-
return
|
|
211
|
+
return loaded;
|
|
232
212
|
}
|
|
233
|
-
return
|
|
213
|
+
return null;
|
|
234
214
|
}
|
|
235
215
|
async fetchAllOrganizationFieldNames(entityId, tenantId) {
|
|
236
216
|
const conn = getSqlConnection(this.em);
|
|
@@ -272,21 +252,8 @@ class TenantDataEncryptionService {
|
|
|
272
252
|
this.inflightDeks.delete(tenantId);
|
|
273
253
|
this.kms.invalidateDek?.(tenantId);
|
|
274
254
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
*
|
|
278
|
-
* `isEnabled()` folds the environment toggle together with KMS health, so by default an
|
|
279
|
-
* unhealthy KMS reports "nothing is encrypted" — which is safe for write paths but wrong for
|
|
280
|
-
* readers that must decide whether a stored column holds ciphertext. `ignoreRuntimeHealth`
|
|
281
|
-
* answers the on-disk question instead: it consults the map even when the KMS cannot currently
|
|
282
|
-
* resolve a DEK, so callers can fail closed rather than treat ciphertext as plaintext (#4622).
|
|
283
|
-
*/
|
|
284
|
-
async getEncryptedFieldNames(entityId, tenantId, organizationId, options) {
|
|
285
|
-
if (options?.ignoreRuntimeHealth) {
|
|
286
|
-
if (!isTenantDataEncryptionEnabled()) return [];
|
|
287
|
-
} else if (!this.isEnabled()) {
|
|
288
|
-
return [];
|
|
289
|
-
}
|
|
255
|
+
async getEncryptedFieldNames(entityId, tenantId, organizationId) {
|
|
256
|
+
if (!this.isEnabled()) return [];
|
|
290
257
|
const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null });
|
|
291
258
|
const fields = new Set(normalizeEncryptedFieldNames(map?.fields));
|
|
292
259
|
if (organizationId == null) {
|
|
@@ -342,17 +309,16 @@ class TenantDataEncryptionService {
|
|
|
342
309
|
debug("\u26AA\uFE0F encrypt.skip.disabled", { entityId, tenantId });
|
|
343
310
|
return payload;
|
|
344
311
|
}
|
|
312
|
+
const dek = await this.resolveDekForEncrypt(tenantId ?? null);
|
|
313
|
+
if (!dek) {
|
|
314
|
+
debug("\u26A0\uFE0F encrypt.skip.no-dek", { entityId, tenantId });
|
|
315
|
+
return payload;
|
|
316
|
+
}
|
|
345
317
|
const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null });
|
|
346
318
|
if (!map || !map.fields?.length) {
|
|
347
319
|
debug("\u26AA\uFE0F encrypt.skip.no-map", { entityId, tenantId });
|
|
348
320
|
return payload;
|
|
349
321
|
}
|
|
350
|
-
const keyId = map.keyScope === "system" ? `system:${entityId}` : tenantId ?? null;
|
|
351
|
-
const dek = await this.resolveDekForEncrypt(keyId);
|
|
352
|
-
if (!dek) {
|
|
353
|
-
debug("\u26A0\uFE0F encrypt.skip.no-dek", { entityId, tenantId, keyScope: map.keyScope ?? "tenant" });
|
|
354
|
-
return payload;
|
|
355
|
-
}
|
|
356
322
|
debug("\u{1F512} encrypt_entity", { entityId, tenantId, organizationId, fields: map.fields.length });
|
|
357
323
|
return this.encryptFields(payload, map.fields, dek);
|
|
358
324
|
}
|
|
@@ -361,17 +327,16 @@ class TenantDataEncryptionService {
|
|
|
361
327
|
debug("\u26AA\uFE0F decrypt.skip.disabled", { entityId, tenantId });
|
|
362
328
|
return payload;
|
|
363
329
|
}
|
|
330
|
+
const dek = await this.getDek(tenantId ?? null);
|
|
331
|
+
if (!dek) {
|
|
332
|
+
debug("\u26A0\uFE0F decrypt.skip.no-dek", { entityId, tenantId });
|
|
333
|
+
return payload;
|
|
334
|
+
}
|
|
364
335
|
const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null });
|
|
365
336
|
if (!map || !map.fields?.length) {
|
|
366
337
|
debug("\u26AA\uFE0F decrypt.skip.no-map", { entityId, tenantId });
|
|
367
338
|
return payload;
|
|
368
339
|
}
|
|
369
|
-
const keyId = map.keyScope === "system" ? `system:${entityId}` : tenantId ?? null;
|
|
370
|
-
const dek = await this.getDek(keyId);
|
|
371
|
-
if (!dek) {
|
|
372
|
-
debug("\u26A0\uFE0F decrypt.skip.no-dek", { entityId, tenantId, keyScope: map.keyScope ?? "tenant" });
|
|
373
|
-
return payload;
|
|
374
|
-
}
|
|
375
340
|
debug("\u{1F513} decrypt_entity", { entityId, tenantId, organizationId, fields: map.fields.length });
|
|
376
341
|
return this.decryptFields(payload, map.fields, dek);
|
|
377
342
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/encryption/tenantDataEncryptionService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { decryptWithAesGcm, encryptWithAesGcm, hashForLookup } from './aes'\nimport { createKmsService, type KmsService, type TenantDek } from './kms'\nimport { isTenantDataEncryptionEnabled, isEncryptionDebugEnabled } from './toggles'\nimport { createLogger } from '../logger'\nimport type { EncryptionKeyScope, ModuleEncryptionMap } from '../../modules/encryption'\n\nconst logger = createLogger('shared').child({ component: 'tenant-encryption' })\n\nexport type EncryptedFieldRule = {\n field: string\n hashField?: string | null\n}\n\nexport type EncryptionMapRecord = {\n entityId: string\n keyScope?: EncryptionKeyScope\n fields: EncryptedFieldRule[]\n}\n\ntype MapCacheKey = {\n entityId: string\n tenantId: string | null\n organizationId: string | null\n}\n\ntype SqlConnection = {\n execute(sql: string, params?: readonly unknown[]): Promise<unknown>\n}\n\nconst MAP_MISS_TTL_MS = 5 * 60 * 1000\n// Mirror the Vault KMS default DEK TTL so a rotated/revoked tenant key is picked\n// up by long-lived processes without a restart (#2746). The service-level cache\n// previously had no TTL and shadowed the KMS's own 15-minute expiry.\nconst DEK_CACHE_TTL_MS = 15 * 60 * 1000\n\nfunction cacheKey(key: MapCacheKey): string {\n return [\n 'encmap',\n key.entityId.toLowerCase(),\n key.tenantId ?? 'null',\n key.organizationId ?? 'null',\n ].join(':')\n}\n\nfunction debug(event: string, payload: Record<string, unknown>) {\n if (!isEncryptionDebugEnabled()) return\n try {\n logger.debug(event, payload)\n } catch {\n // ignore\n }\n}\n\nconst toSnakeCase = (value: string): string =>\n value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()\n\nconst toCamelCase = (value: string): string =>\n value.replace(/_([a-z])/g, (_, c) => c.toUpperCase())\n\nfunction findKey(obj: Record<string, unknown>, key: string): string | null {\n const candidates = [key, toSnakeCase(key), toCamelCase(key)]\n for (const candidate of candidates) {\n if (Object.prototype.hasOwnProperty.call(obj, candidate)) return candidate\n }\n return null\n}\n\n/**\n * Decode a decrypted entity-field payload back into its original value.\n *\n * The encrypt path stores raw strings unwrapped and JSON-stringifies non-string\n * values. Blindly running `JSON.parse` on every decrypted value would coerce\n * text columns whose contents happen to be valid JSON primitives \u2014 e.g. the\n * string `\"123\"` \u2014 back into numbers/booleans, which then breaks string-typed\n * consumers (see issue #1734). Only restructure the value when the decrypted\n * payload is unambiguously a JSON object or array; otherwise return the raw\n * decrypted string. Numeric/boolean entity columns are not in any current\n * encryption map, so this is backward-compatible.\n *\n * NOTE (issue #1810 follow-up): `decryptFields` no longer calls this helper for\n * entity-field decryption \u2014 typed string columns whose contents happen to look\n * like JSON (e.g. a display name `{\"a\":1}`) must remain raw strings to avoid\n * downstream React-render crashes. Callers that legitimately need the parse\n * (audit-log jsonb columns, custom-field rotation, encryption CLI) MUST invoke\n * `parseDecryptedFieldValue` themselves on the decrypted payload.\n */\nexport function parseDecryptedFieldValue(decrypted: string): unknown {\n if (decrypted.length === 0) return decrypted\n const first = decrypted[0]\n if (first !== '{' && first !== '[') return decrypted\n try {\n return JSON.parse(decrypted)\n } catch {\n return decrypted\n }\n}\n\n/**\n * A value is only treated as \"already encrypted\" when it actually decrypts\n * under the tenant DEK \u2014 i.e. the AES-GCM authentication tag verifies. A purely\n * structural `<iv>:<ct>:<tag>:v1` shape check is forgeable: attacker-controlled\n * field values (e.g. their own profile email/phone) could impersonate ciphertext\n * to skip encryption-at-rest and the lookup hash entirely (issue #2720). Binding\n * the check to a successful authenticated decrypt makes forgery infeasible, so a\n * fake payload simply gets encrypted like any other plaintext.\n */\nfunction isEncryptedWithDek(value: unknown, dek: TenantDek): boolean {\n if (typeof value !== 'string') return false\n const parts = value.split(':')\n if (parts.length !== 4 || parts[3] !== 'v1') return false\n return decryptWithAesGcm(value, dek.key) !== null\n}\n\nfunction normalizeEncryptedFieldNames(fields: readonly { field?: unknown }[] | null | undefined): string[] {\n if (!Array.isArray(fields)) return []\n return fields\n .map((rule) => rule.field)\n .filter((field): field is string => typeof field === 'string' && field.trim().length > 0)\n}\n\nfunction readEncryptedFieldsJson(row: Record<string, unknown>): EncryptedFieldRule[] {\n const raw = row.fields_json ?? row.fieldsJson\n if (Array.isArray(raw)) return raw as EncryptedFieldRule[]\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw)\n return Array.isArray(parsed) ? parsed as EncryptedFieldRule[] : []\n } catch {\n return []\n }\n }\n return []\n}\n\nfunction getSqlConnection(em: EntityManager): SqlConnection | null {\n const source = em as { getConnection?: () => unknown }\n const conn = source.getConnection?.()\n if (!conn || typeof conn !== 'object') return null\n const candidate = conn as { execute?: unknown }\n if (typeof candidate.execute !== 'function') return null\n return candidate as SqlConnection\n}\n\nexport class TenantDataEncryptionService {\n private static globalMemoryCache = new Map<string, EncryptionMapRecord>()\n private static globalInflightMaps = new Map<string, Promise<EncryptionMapRecord | null>>()\n private static globalDekCache = new Map<string, TenantDek>()\n private static globalInflightDeks = new Map<string, Promise<TenantDek | null>>()\n private static globalMissCache = new Map<string, number>()\n private readonly kms: KmsService\n private readonly cache?: CacheStrategy\n private readonly memoryCache = TenantDataEncryptionService.globalMemoryCache\n private readonly dekCache = TenantDataEncryptionService.globalDekCache\n private readonly inflightDeks = TenantDataEncryptionService.globalInflightDeks\n private readonly inflightMaps = TenantDataEncryptionService.globalInflightMaps\n private readonly missCache = TenantDataEncryptionService.globalMissCache\n private readonly systemDefaultMaps: Map<string, ModuleEncryptionMap>\n\n constructor(\n private em: EntityManager,\n opts?: {\n cache?: CacheStrategy\n kms?: KmsService\n defaultEncryptionMaps?: readonly ModuleEncryptionMap[]\n }\n ) {\n this.cache = opts?.cache\n this.kms = opts?.kms ?? createKmsService()\n this.systemDefaultMaps = new Map(\n (opts?.defaultEncryptionMaps ?? [])\n .filter((map) => map.keyScope === 'system')\n .map((map) => [map.entityId, map]),\n )\n }\n\n isEnabled(): boolean {\n return isTenantDataEncryptionEnabled() && this.kms.isHealthy()\n }\n\n private isDekExpired(dek: TenantDek): boolean {\n return Date.now() - dek.fetchedAt > DEK_CACHE_TTL_MS\n }\n\n async getDek(tenantId: string | null | undefined): Promise<TenantDek | null> {\n if (!tenantId) return null\n const cached = this.dekCache.get(tenantId)\n if (cached && !this.isDekExpired(cached)) return cached\n if (cached) this.dekCache.delete(tenantId)\n const dek = await this.kms.getTenantDek(tenantId)\n if (!dek) {\n debug('\uD83D\uDD0E dek.miss', { tenantId })\n } else {\n debug('\u2705 dek.hit', { tenantId })\n }\n if (dek) this.dekCache.set(tenantId, dek)\n return dek\n }\n\n private async resolveDekForEncrypt(tenantId: string | null): Promise<TenantDek | null> {\n const existing = await this.getDek(tenantId)\n if (existing || !tenantId) return existing ?? null\n if (typeof this.kms.createTenantDek !== 'function') return existing ?? null\n // Dedupe concurrent first-time creation within this process so two callers\n // can't each generate a distinct DEK and overwrite one another (#2746).\n // Mirrors the encryption-map inflight dedupe (`globalInflightMaps`).\n const pending = this.inflightDeks.get(tenantId)\n if (pending) return pending\n const creation = (async () => {\n const created = await this.kms.createTenantDek(tenantId)\n if (created) this.dekCache.set(tenantId, created)\n return created ?? null\n })()\n this.inflightDeks.set(tenantId, creation)\n try {\n return await creation\n } finally {\n this.inflightDeks.delete(tenantId)\n }\n }\n\n async createDek(tenantId: string): Promise<TenantDek | null> {\n const dek = await this.kms.createTenantDek(tenantId)\n if (dek) this.dekCache.set(tenantId, dek)\n return dek\n }\n\n private async fetchMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {\n // Bypass ORM lifecycle hooks to avoid recursive decrypt loops by querying directly.\n const conn = getSqlConnection(this.em)\n if (!conn) return null\n const sql = `\n select entity_id, fields_json\n from encryption_maps\n where entity_id = ?\n and tenant_id is not distinct from ?\n and organization_id is not distinct from ?\n and is_active = true\n and deleted_at is null\n limit 1\n `\n const rows = await conn.execute(sql, [key.entityId, key.tenantId ?? null, key.organizationId ?? null])\n const row = Array.isArray(rows) && rows.length && rows[0] && typeof rows[0] === 'object'\n ? rows[0] as Record<string, unknown>\n : null\n if (!row) return null\n return {\n entityId: String(row.entity_id ?? row.entityId ?? key.entityId),\n fields: readEncryptedFieldsJson(row),\n }\n }\n\n private applySystemDefault(record: EncryptionMapRecord | null, entityId: string): EncryptionMapRecord | null {\n const declared = this.systemDefaultMaps.get(entityId)\n if (!declared) return record\n const fields: EncryptedFieldRule[] = declared.fields.map((field) => ({\n field: field.field,\n hashField: field.hashField ?? null,\n }))\n const declaredFields = new Set(fields.map((field) => field.field))\n for (const field of record?.fields ?? []) {\n if (!declaredFields.has(field.field)) fields.push(field)\n }\n return {\n entityId,\n keyScope: 'system',\n fields,\n }\n }\n\n private async getMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {\n const shouldSkipLookup = (tag: string) => {\n const expiresAt = this.missCache.get(tag)\n if (!expiresAt) return false\n if (expiresAt > Date.now()) return true\n this.missCache.delete(tag)\n return false\n }\n const recordMiss = (tag: string) => {\n this.missCache.set(tag, Date.now() + MAP_MISS_TTL_MS)\n }\n\n const candidates: MapCacheKey[] = [\n key,\n { entityId: key.entityId, tenantId: key.tenantId ?? null, organizationId: null },\n { entityId: key.entityId, tenantId: null, organizationId: null },\n ]\n for (const candidate of candidates) {\n const tag = cacheKey(candidate)\n if (shouldSkipLookup(tag)) continue\n if (this.inflightMaps.has(tag)) {\n const pending = this.inflightMaps.get(tag)!\n const resolved = await pending\n if (resolved) return this.applySystemDefault(resolved, key.entityId)\n }\n const mem = this.memoryCache.get(tag)\n if (mem) return this.applySystemDefault(mem, key.entityId)\n if (this.cache && typeof this.cache.get === 'function') {\n const cached = await this.cache.get(tag)\n if (cached) return this.applySystemDefault(cached as EncryptionMapRecord, key.entityId)\n }\n const pending = this.fetchMap(candidate)\n this.inflightMaps.set(tag, pending)\n const loaded = await pending\n this.inflightMaps.delete(tag)\n if (!loaded) {\n recordMiss(tag)\n debug('\uD83D\uDD0D encmap.miss', {\n entityId: candidate.entityId,\n tenantId: candidate.tenantId,\n organizationId: candidate.organizationId,\n })\n continue\n }\n this.missCache.delete(tag)\n this.memoryCache.set(tag, loaded)\n if (this.cache && typeof this.cache.set === 'function') {\n await this.cache.set(tag, loaded, { ttl: 300 })\n }\n return this.applySystemDefault(loaded, key.entityId)\n }\n return this.applySystemDefault(null, key.entityId)\n }\n\n private async fetchAllOrganizationFieldNames(entityId: string, tenantId: string | null): Promise<string[]> {\n const conn = getSqlConnection(this.em)\n if (!conn) return []\n const sql = `\n select fields_json\n from encryption_maps\n where entity_id = ?\n and tenant_id is not distinct from ?\n and organization_id is not null\n and is_active = true\n and deleted_at is null\n `\n const rows = await conn.execute(sql, [entityId, tenantId])\n if (!Array.isArray(rows) || rows.length === 0) return []\n const names = new Set<string>()\n for (const row of rows) {\n if (!row || typeof row !== 'object') continue\n for (const field of normalizeEncryptedFieldNames(readEncryptedFieldsJson(row as Record<string, unknown>))) {\n names.add(field)\n }\n }\n return Array.from(names)\n }\n\n async invalidateMap(entityId: string, tenantId: string | null, organizationId: string | null): Promise<void> {\n const tag = cacheKey({ entityId, tenantId, organizationId })\n this.memoryCache.delete(tag)\n this.inflightMaps.delete(tag)\n this.missCache.delete(tag)\n if (this.cache && typeof (this.cache as any).delete === 'function') {\n await (this.cache as any).delete(tag)\n }\n }\n\n // Force a flush of a tenant's cached DEK across the service-level cache and the\n // underlying KMS cache so an operator can pick up a rotated/revoked key without\n // a process restart (#2746).\n invalidateDek(tenantId: string): void {\n this.dekCache.delete(tenantId)\n this.inflightDeks.delete(tenantId)\n this.kms.invalidateDek?.(tenantId)\n }\n\n /**\n * Lists the fields an encryption map marks as encrypted at rest.\n *\n * `isEnabled()` folds the environment toggle together with KMS health, so by default an\n * unhealthy KMS reports \"nothing is encrypted\" \u2014 which is safe for write paths but wrong for\n * readers that must decide whether a stored column holds ciphertext. `ignoreRuntimeHealth`\n * answers the on-disk question instead: it consults the map even when the KMS cannot currently\n * resolve a DEK, so callers can fail closed rather than treat ciphertext as plaintext (#4622).\n */\n async getEncryptedFieldNames(\n entityId: string,\n tenantId: string | null | undefined,\n organizationId?: string | null,\n options?: { ignoreRuntimeHealth?: boolean }\n ): Promise<string[]> {\n if (options?.ignoreRuntimeHealth) {\n if (!isTenantDataEncryptionEnabled()) return []\n } else if (!this.isEnabled()) {\n return []\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n const fields = new Set(normalizeEncryptedFieldNames(map?.fields))\n if (organizationId == null) {\n for (const field of await this.fetchAllOrganizationFieldNames(entityId, tenantId ?? null)) {\n fields.add(field)\n }\n }\n return Array.from(fields)\n }\n\n private encryptFields(\n obj: Record<string, unknown>,\n fields: EncryptedFieldRule[],\n dek: TenantDek\n ): Record<string, unknown> {\n const clone: Record<string, unknown> = { ...obj }\n for (const rule of fields) {\n const key = findKey(clone, rule.field)\n if (!key) continue\n const value = clone[key]\n if (value === null || value === undefined) continue\n // Avoid double-encrypting payloads that genuinely decrypt under this DEK.\n // A forged ciphertext-shaped string fails this check and is encrypted as\n // plaintext, closing the encryption-at-rest bypass (issue #2720).\n if (isEncryptedWithDek(value, dek)) continue\n const serialized = typeof value === 'string' ? value : JSON.stringify(value)\n const payload = encryptWithAesGcm(serialized, dek.key)\n clone[key] = payload.value\n if (rule.hashField) {\n const hashKey = findKey(clone, rule.hashField) ?? rule.hashField\n clone[hashKey] = hashForLookup(serialized)\n }\n }\n return clone\n }\n\n private decryptFields(\n obj: Record<string, unknown>,\n fields: EncryptedFieldRule[],\n dek: TenantDek\n ): Record<string, unknown> {\n const clone: Record<string, unknown> = { ...obj }\n const maybeDecrypt = (payload: string): string | null => {\n const first = decryptWithAesGcm(payload, dek.key)\n if (first === null) return null\n // Handle accidental double-encryption: if the first pass still looks like a v1 payload, try once more.\n const parts = first.split(':')\n if (parts.length === 4 && parts[3] === 'v1') {\n const second = decryptWithAesGcm(first, dek.key)\n return second ?? first\n }\n return first\n }\n for (const rule of fields) {\n const key = findKey(clone, rule.field)\n if (!key) continue\n const value = clone[key]\n if (typeof value !== 'string') continue\n const decrypted = maybeDecrypt(value)\n if (decrypted === null) continue\n // Entity fields are typed columns (string/text). Never auto-parse to an object \u2014\n // it triggers React-render crashes when a string value happens to be valid JSON\n // (issue #1810 follow-up). Custom field values use a separate helper that\n // preserves their typed-JSON contract.\n clone[key] = decrypted\n }\n return clone\n }\n\n async encryptEntityPayload(\n entityId: string,\n payload: Record<string, unknown>,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<Record<string, unknown>> {\n if (!this.isEnabled()) {\n debug('\u26AA\uFE0F encrypt.skip.disabled', { entityId, tenantId })\n return payload\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n if (!map || !map.fields?.length) {\n debug('\u26AA\uFE0F encrypt.skip.no-map', { entityId, tenantId })\n return payload\n }\n const keyId = map.keyScope === 'system' ? `system:${entityId}` : tenantId ?? null\n const dek = await this.resolveDekForEncrypt(keyId)\n if (!dek) {\n debug('\u26A0\uFE0F encrypt.skip.no-dek', { entityId, tenantId, keyScope: map.keyScope ?? 'tenant' })\n return payload\n }\n debug('\uD83D\uDD12 encrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })\n return this.encryptFields(payload, map.fields, dek)\n }\n\n async decryptEntityPayload(\n entityId: string,\n payload: Record<string, unknown>,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<Record<string, unknown>> {\n if (!isTenantDataEncryptionEnabled()) {\n debug('\u26AA\uFE0F decrypt.skip.disabled', { entityId, tenantId })\n return payload\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n if (!map || !map.fields?.length) {\n debug('\u26AA\uFE0F decrypt.skip.no-map', { entityId, tenantId })\n return payload\n }\n const keyId = map.keyScope === 'system' ? `system:${entityId}` : tenantId ?? null\n const dek = await this.getDek(keyId)\n if (!dek) {\n debug('\u26A0\uFE0F decrypt.skip.no-dek', { entityId, tenantId, keyScope: map.keyScope ?? 'tenant' })\n return payload\n }\n debug('\uD83D\uDD13 decrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })\n return this.decryptFields(payload, map.fields, dek)\n }\n}\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,mBAAmB,mBAAmB,qBAAqB;AACpE,SAAS,wBAAyD;AAClE,SAAS,+BAA+B,gCAAgC;AACxE,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { decryptWithAesGcm, encryptWithAesGcm, hashForLookup } from './aes'\nimport { createKmsService, type KmsService, type TenantDek } from './kms'\nimport { isTenantDataEncryptionEnabled, isEncryptionDebugEnabled } from './toggles'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'tenant-encryption' })\n\nexport type EncryptedFieldRule = {\n field: string\n hashField?: string | null\n}\n\nexport type EncryptionMapRecord = {\n entityId: string\n fields: EncryptedFieldRule[]\n}\n\ntype MapCacheKey = {\n entityId: string\n tenantId: string | null\n organizationId: string | null\n}\n\ntype SqlConnection = {\n execute(sql: string, params?: readonly unknown[]): Promise<unknown>\n}\n\nconst MAP_MISS_TTL_MS = 5 * 60 * 1000\n// Mirror the Vault KMS default DEK TTL so a rotated/revoked tenant key is picked\n// up by long-lived processes without a restart (#2746). The service-level cache\n// previously had no TTL and shadowed the KMS's own 15-minute expiry.\nconst DEK_CACHE_TTL_MS = 15 * 60 * 1000\n\nfunction cacheKey(key: MapCacheKey): string {\n return [\n 'encmap',\n key.entityId.toLowerCase(),\n key.tenantId ?? 'null',\n key.organizationId ?? 'null',\n ].join(':')\n}\n\nfunction debug(event: string, payload: Record<string, unknown>) {\n if (!isEncryptionDebugEnabled()) return\n try {\n logger.debug(event, payload)\n } catch {\n // ignore\n }\n}\n\nconst toSnakeCase = (value: string): string =>\n value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()\n\nconst toCamelCase = (value: string): string =>\n value.replace(/_([a-z])/g, (_, c) => c.toUpperCase())\n\nfunction findKey(obj: Record<string, unknown>, key: string): string | null {\n const candidates = [key, toSnakeCase(key), toCamelCase(key)]\n for (const candidate of candidates) {\n if (Object.prototype.hasOwnProperty.call(obj, candidate)) return candidate\n }\n return null\n}\n\n/**\n * Decode a decrypted entity-field payload back into its original value.\n *\n * The encrypt path stores raw strings unwrapped and JSON-stringifies non-string\n * values. Blindly running `JSON.parse` on every decrypted value would coerce\n * text columns whose contents happen to be valid JSON primitives \u2014 e.g. the\n * string `\"123\"` \u2014 back into numbers/booleans, which then breaks string-typed\n * consumers (see issue #1734). Only restructure the value when the decrypted\n * payload is unambiguously a JSON object or array; otherwise return the raw\n * decrypted string. Numeric/boolean entity columns are not in any current\n * encryption map, so this is backward-compatible.\n *\n * NOTE (issue #1810 follow-up): `decryptFields` no longer calls this helper for\n * entity-field decryption \u2014 typed string columns whose contents happen to look\n * like JSON (e.g. a display name `{\"a\":1}`) must remain raw strings to avoid\n * downstream React-render crashes. Callers that legitimately need the parse\n * (audit-log jsonb columns, custom-field rotation, encryption CLI) MUST invoke\n * `parseDecryptedFieldValue` themselves on the decrypted payload.\n */\nexport function parseDecryptedFieldValue(decrypted: string): unknown {\n if (decrypted.length === 0) return decrypted\n const first = decrypted[0]\n if (first !== '{' && first !== '[') return decrypted\n try {\n return JSON.parse(decrypted)\n } catch {\n return decrypted\n }\n}\n\n/**\n * A value is only treated as \"already encrypted\" when it actually decrypts\n * under the tenant DEK \u2014 i.e. the AES-GCM authentication tag verifies. A purely\n * structural `<iv>:<ct>:<tag>:v1` shape check is forgeable: attacker-controlled\n * field values (e.g. their own profile email/phone) could impersonate ciphertext\n * to skip encryption-at-rest and the lookup hash entirely (issue #2720). Binding\n * the check to a successful authenticated decrypt makes forgery infeasible, so a\n * fake payload simply gets encrypted like any other plaintext.\n */\nfunction isEncryptedWithDek(value: unknown, dek: TenantDek): boolean {\n if (typeof value !== 'string') return false\n const parts = value.split(':')\n if (parts.length !== 4 || parts[3] !== 'v1') return false\n return decryptWithAesGcm(value, dek.key) !== null\n}\n\nfunction normalizeEncryptedFieldNames(fields: readonly { field?: unknown }[] | null | undefined): string[] {\n if (!Array.isArray(fields)) return []\n return fields\n .map((rule) => rule.field)\n .filter((field): field is string => typeof field === 'string' && field.trim().length > 0)\n}\n\nfunction readEncryptedFieldsJson(row: Record<string, unknown>): EncryptedFieldRule[] {\n const raw = row.fields_json ?? row.fieldsJson\n if (Array.isArray(raw)) return raw as EncryptedFieldRule[]\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw)\n return Array.isArray(parsed) ? parsed as EncryptedFieldRule[] : []\n } catch {\n return []\n }\n }\n return []\n}\n\nfunction getSqlConnection(em: EntityManager): SqlConnection | null {\n const source = em as { getConnection?: () => unknown }\n const conn = source.getConnection?.()\n if (!conn || typeof conn !== 'object') return null\n const candidate = conn as { execute?: unknown }\n if (typeof candidate.execute !== 'function') return null\n return candidate as SqlConnection\n}\n\nexport class TenantDataEncryptionService {\n private static globalMemoryCache = new Map<string, EncryptionMapRecord>()\n private static globalInflightMaps = new Map<string, Promise<EncryptionMapRecord | null>>()\n private static globalDekCache = new Map<string, TenantDek>()\n private static globalInflightDeks = new Map<string, Promise<TenantDek | null>>()\n private static globalMissCache = new Map<string, number>()\n private readonly kms: KmsService\n private readonly cache?: CacheStrategy\n private readonly memoryCache = TenantDataEncryptionService.globalMemoryCache\n private readonly dekCache = TenantDataEncryptionService.globalDekCache\n private readonly inflightDeks = TenantDataEncryptionService.globalInflightDeks\n private readonly inflightMaps = TenantDataEncryptionService.globalInflightMaps\n private readonly missCache = TenantDataEncryptionService.globalMissCache\n\n constructor(\n private em: EntityManager,\n opts?: { cache?: CacheStrategy; kms?: KmsService }\n ) {\n this.cache = opts?.cache\n this.kms = opts?.kms ?? createKmsService()\n }\n\n isEnabled(): boolean {\n return isTenantDataEncryptionEnabled() && this.kms.isHealthy()\n }\n\n private isDekExpired(dek: TenantDek): boolean {\n return Date.now() - dek.fetchedAt > DEK_CACHE_TTL_MS\n }\n\n async getDek(tenantId: string | null | undefined): Promise<TenantDek | null> {\n if (!tenantId) return null\n const cached = this.dekCache.get(tenantId)\n if (cached && !this.isDekExpired(cached)) return cached\n if (cached) this.dekCache.delete(tenantId)\n const dek = await this.kms.getTenantDek(tenantId)\n if (!dek) {\n debug('\uD83D\uDD0E dek.miss', { tenantId })\n } else {\n debug('\u2705 dek.hit', { tenantId })\n }\n if (dek) this.dekCache.set(tenantId, dek)\n return dek\n }\n\n private async resolveDekForEncrypt(tenantId: string | null): Promise<TenantDek | null> {\n const existing = await this.getDek(tenantId)\n if (existing || !tenantId) return existing ?? null\n if (typeof this.kms.createTenantDek !== 'function') return existing ?? null\n // Dedupe concurrent first-time creation within this process so two callers\n // can't each generate a distinct DEK and overwrite one another (#2746).\n // Mirrors the encryption-map inflight dedupe (`globalInflightMaps`).\n const pending = this.inflightDeks.get(tenantId)\n if (pending) return pending\n const creation = (async () => {\n const created = await this.kms.createTenantDek(tenantId)\n if (created) this.dekCache.set(tenantId, created)\n return created ?? null\n })()\n this.inflightDeks.set(tenantId, creation)\n try {\n return await creation\n } finally {\n this.inflightDeks.delete(tenantId)\n }\n }\n\n async createDek(tenantId: string): Promise<TenantDek | null> {\n const dek = await this.kms.createTenantDek(tenantId)\n if (dek) this.dekCache.set(tenantId, dek)\n return dek\n }\n\n private async fetchMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {\n // Bypass ORM lifecycle hooks to avoid recursive decrypt loops by querying directly.\n const conn = getSqlConnection(this.em)\n if (!conn) return null\n const sql = `\n select entity_id, fields_json\n from encryption_maps\n where entity_id = ?\n and tenant_id is not distinct from ?\n and organization_id is not distinct from ?\n and is_active = true\n and deleted_at is null\n limit 1\n `\n const rows = await conn.execute(sql, [key.entityId, key.tenantId ?? null, key.organizationId ?? null])\n const row = Array.isArray(rows) && rows.length && rows[0] && typeof rows[0] === 'object'\n ? rows[0] as Record<string, unknown>\n : null\n if (!row) return null\n return {\n entityId: String(row.entity_id ?? row.entityId ?? key.entityId),\n fields: readEncryptedFieldsJson(row),\n }\n }\n\n private async getMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {\n const shouldSkipLookup = (tag: string) => {\n const expiresAt = this.missCache.get(tag)\n if (!expiresAt) return false\n if (expiresAt > Date.now()) return true\n this.missCache.delete(tag)\n return false\n }\n const recordMiss = (tag: string) => {\n this.missCache.set(tag, Date.now() + MAP_MISS_TTL_MS)\n }\n\n const candidates: MapCacheKey[] = [\n key,\n { entityId: key.entityId, tenantId: key.tenantId ?? null, organizationId: null },\n { entityId: key.entityId, tenantId: null, organizationId: null },\n ]\n for (const candidate of candidates) {\n const tag = cacheKey(candidate)\n if (shouldSkipLookup(tag)) continue\n if (this.inflightMaps.has(tag)) {\n const pending = this.inflightMaps.get(tag)!\n const resolved = await pending\n if (resolved) return resolved\n }\n const mem = this.memoryCache.get(tag)\n if (mem) return mem\n if (this.cache && typeof this.cache.get === 'function') {\n const cached = await this.cache.get(tag)\n if (cached) return cached as EncryptionMapRecord\n }\n const pending = this.fetchMap(candidate)\n this.inflightMaps.set(tag, pending)\n const loaded = await pending\n this.inflightMaps.delete(tag)\n if (!loaded) {\n recordMiss(tag)\n debug('\uD83D\uDD0D encmap.miss', {\n entityId: candidate.entityId,\n tenantId: candidate.tenantId,\n organizationId: candidate.organizationId,\n })\n continue\n }\n this.missCache.delete(tag)\n this.memoryCache.set(tag, loaded)\n if (this.cache && typeof this.cache.set === 'function') {\n await this.cache.set(tag, loaded, { ttl: 300 })\n }\n return loaded\n }\n return null\n }\n\n private async fetchAllOrganizationFieldNames(entityId: string, tenantId: string | null): Promise<string[]> {\n const conn = getSqlConnection(this.em)\n if (!conn) return []\n const sql = `\n select fields_json\n from encryption_maps\n where entity_id = ?\n and tenant_id is not distinct from ?\n and organization_id is not null\n and is_active = true\n and deleted_at is null\n `\n const rows = await conn.execute(sql, [entityId, tenantId])\n if (!Array.isArray(rows) || rows.length === 0) return []\n const names = new Set<string>()\n for (const row of rows) {\n if (!row || typeof row !== 'object') continue\n for (const field of normalizeEncryptedFieldNames(readEncryptedFieldsJson(row as Record<string, unknown>))) {\n names.add(field)\n }\n }\n return Array.from(names)\n }\n\n async invalidateMap(entityId: string, tenantId: string | null, organizationId: string | null): Promise<void> {\n const tag = cacheKey({ entityId, tenantId, organizationId })\n this.memoryCache.delete(tag)\n this.inflightMaps.delete(tag)\n this.missCache.delete(tag)\n if (this.cache && typeof (this.cache as any).delete === 'function') {\n await (this.cache as any).delete(tag)\n }\n }\n\n // Force a flush of a tenant's cached DEK across the service-level cache and the\n // underlying KMS cache so an operator can pick up a rotated/revoked key without\n // a process restart (#2746).\n invalidateDek(tenantId: string): void {\n this.dekCache.delete(tenantId)\n this.inflightDeks.delete(tenantId)\n this.kms.invalidateDek?.(tenantId)\n }\n\n async getEncryptedFieldNames(\n entityId: string,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<string[]> {\n if (!this.isEnabled()) return []\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n const fields = new Set(normalizeEncryptedFieldNames(map?.fields))\n if (organizationId == null) {\n for (const field of await this.fetchAllOrganizationFieldNames(entityId, tenantId ?? null)) {\n fields.add(field)\n }\n }\n return Array.from(fields)\n }\n\n private encryptFields(\n obj: Record<string, unknown>,\n fields: EncryptedFieldRule[],\n dek: TenantDek\n ): Record<string, unknown> {\n const clone: Record<string, unknown> = { ...obj }\n for (const rule of fields) {\n const key = findKey(clone, rule.field)\n if (!key) continue\n const value = clone[key]\n if (value === null || value === undefined) continue\n // Avoid double-encrypting payloads that genuinely decrypt under this DEK.\n // A forged ciphertext-shaped string fails this check and is encrypted as\n // plaintext, closing the encryption-at-rest bypass (issue #2720).\n if (isEncryptedWithDek(value, dek)) continue\n const serialized = typeof value === 'string' ? value : JSON.stringify(value)\n const payload = encryptWithAesGcm(serialized, dek.key)\n clone[key] = payload.value\n if (rule.hashField) {\n const hashKey = findKey(clone, rule.hashField) ?? rule.hashField\n clone[hashKey] = hashForLookup(serialized)\n }\n }\n return clone\n }\n\n private decryptFields(\n obj: Record<string, unknown>,\n fields: EncryptedFieldRule[],\n dek: TenantDek\n ): Record<string, unknown> {\n const clone: Record<string, unknown> = { ...obj }\n const maybeDecrypt = (payload: string): string | null => {\n const first = decryptWithAesGcm(payload, dek.key)\n if (first === null) return null\n // Handle accidental double-encryption: if the first pass still looks like a v1 payload, try once more.\n const parts = first.split(':')\n if (parts.length === 4 && parts[3] === 'v1') {\n const second = decryptWithAesGcm(first, dek.key)\n return second ?? first\n }\n return first\n }\n for (const rule of fields) {\n const key = findKey(clone, rule.field)\n if (!key) continue\n const value = clone[key]\n if (typeof value !== 'string') continue\n const decrypted = maybeDecrypt(value)\n if (decrypted === null) continue\n // Entity fields are typed columns (string/text). Never auto-parse to an object \u2014\n // it triggers React-render crashes when a string value happens to be valid JSON\n // (issue #1810 follow-up). Custom field values use a separate helper that\n // preserves their typed-JSON contract.\n clone[key] = decrypted\n }\n return clone\n }\n\n async encryptEntityPayload(\n entityId: string,\n payload: Record<string, unknown>,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<Record<string, unknown>> {\n if (!this.isEnabled()) {\n debug('\u26AA\uFE0F encrypt.skip.disabled', { entityId, tenantId })\n return payload\n }\n const dek = await this.resolveDekForEncrypt(tenantId ?? null)\n if (!dek) {\n debug('\u26A0\uFE0F encrypt.skip.no-dek', { entityId, tenantId })\n return payload\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n if (!map || !map.fields?.length) {\n debug('\u26AA\uFE0F encrypt.skip.no-map', { entityId, tenantId })\n return payload\n }\n debug('\uD83D\uDD12 encrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })\n return this.encryptFields(payload, map.fields, dek)\n }\n\n async decryptEntityPayload(\n entityId: string,\n payload: Record<string, unknown>,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<Record<string, unknown>> {\n if (!isTenantDataEncryptionEnabled()) {\n debug('\u26AA\uFE0F decrypt.skip.disabled', { entityId, tenantId })\n return payload\n }\n const dek = await this.getDek(tenantId ?? null)\n if (!dek) {\n debug('\u26A0\uFE0F decrypt.skip.no-dek', { entityId, tenantId })\n return payload\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n if (!map || !map.fields?.length) {\n debug('\u26AA\uFE0F decrypt.skip.no-map', { entityId, tenantId })\n return payload\n }\n debug('\uD83D\uDD13 decrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })\n return this.decryptFields(payload, map.fields, dek)\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["pending"]
|
|
7
7
|
}
|
package/dist/lib/i18n/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/i18n/config.ts"],
|
|
4
|
-
"sourcesContent": ["export type Locale = 'en' | 'pl' | 'es' | 'de'
|
|
5
|
-
"mappings": "AAEO,MAAM,UAAoB,CAAC,MAAM,MAAM,MAAM,
|
|
4
|
+
"sourcesContent": ["export type Locale = 'en' | 'pl' | 'es' | 'de'\n\nexport const locales: Locale[] = ['en', 'pl', 'es', 'de']\nexport const defaultLocale: Locale = 'en'\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,UAAoB,CAAC,MAAM,MAAM,MAAM,IAAI;AACjD,MAAM,gBAAwB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/i18n/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defaultLocale, locales } from "./config.js";
|
|
2
2
|
import { resolveForcedLocale, resolveLocaleFromAcceptLanguage } from "./locale.js";
|
|
3
3
|
import { createFallbackTranslator, createTranslator } from "./translate.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getModules } from "../modules/registry.js";
|
|
5
5
|
import { loadAppDictionary } from "./app-dictionaries.js";
|
|
6
6
|
import { getCachedDictionary, setCachedDictionary } from "./dictionary-cache.js";
|
|
7
|
-
import { registerModules, getModules } from "../modules/registry.js";
|
|
7
|
+
import { registerModules, getModules as getModules2 } from "../modules/registry.js";
|
|
8
8
|
import { registerAppDictionaryLoader } from "./app-dictionaries.js";
|
|
9
9
|
import { invalidateDictionaryCache } from "./dictionary-cache.js";
|
|
10
10
|
function flattenDictionary(source, prefix = "") {
|
|
@@ -46,7 +46,7 @@ async function loadDictionary(locale) {
|
|
|
46
46
|
if (cached) return cached;
|
|
47
47
|
const baseRaw = await loadAppDictionary(locale);
|
|
48
48
|
const merged = { ...flattenDictionary(baseRaw) };
|
|
49
|
-
const modules =
|
|
49
|
+
const modules = getModules();
|
|
50
50
|
for (const m of modules) {
|
|
51
51
|
const dict = m.translations?.[locale];
|
|
52
52
|
if (dict) Object.assign(merged, flattenDictionary(dict));
|
|
@@ -67,7 +67,7 @@ try {
|
|
|
67
67
|
}
|
|
68
68
|
export {
|
|
69
69
|
detectLocale,
|
|
70
|
-
getModules,
|
|
70
|
+
getModules2 as getModules,
|
|
71
71
|
invalidateDictionaryCache,
|
|
72
72
|
loadDictionary,
|
|
73
73
|
registerAppDictionaryLoader,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/i18n/server.ts"],
|
|
4
|
-
"sourcesContent": ["import { defaultLocale, locales, type Locale } from './config'\nimport type { Dict } from './context'\nimport { resolveForcedLocale, resolveLocaleFromAcceptLanguage } from './locale'\nimport { createFallbackTranslator, createTranslator } from './translate'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe,eAA4B;AAEpD,SAAS,qBAAqB,uCAAuC;AACrE,SAAS,0BAA0B,wBAAwB;AAC3D,SAAS,
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { defaultLocale, locales, type Locale } from './config'\nimport type { Dict } from './context'\nimport { resolveForcedLocale, resolveLocaleFromAcceptLanguage } from './locale'\nimport { createFallbackTranslator, createTranslator } from './translate'\nimport { getModules } from '../modules/registry'\nimport { loadAppDictionary } from './app-dictionaries'\nimport { getCachedDictionary, setCachedDictionary } from './dictionary-cache'\n\n// Re-export for backwards compatibility\nexport { registerModules, getModules } from '../modules/registry'\nexport { registerAppDictionaryLoader } from './app-dictionaries'\nexport { invalidateDictionaryCache } from './dictionary-cache'\n\nfunction flattenDictionary(source: unknown, prefix = ''): Dict {\n if (!source || typeof source !== 'object' || Array.isArray(source)) return {}\n const result: Dict = {}\n for (const [key, value] of Object.entries(source as Record<string, unknown>)) {\n if (!key) continue\n const nextKey = prefix ? `${prefix}.${key}` : key\n if (typeof value === 'string') {\n result[nextKey] = value\n } else if (value && typeof value === 'object' && !Array.isArray(value)) {\n Object.assign(result, flattenDictionary(value, nextKey))\n }\n }\n return result\n}\n\nexport async function detectLocale(): Promise<Locale> {\n // Ops-level override: pin the whole app to one locale (default: unset).\n const forced = resolveForcedLocale(process.env)\n if (forced) return forced\n // Dynamic import to avoid requiring Next.js in non-Next.js contexts (CLI, tests)\n try {\n const { cookies, headers } = await import('next/headers')\n try {\n const c = (await cookies()).get('locale')?.value\n if (c && locales.includes(c as Locale)) return c as Locale\n } catch {\n // cookies() may not be available outside request context (e.g., in tests)\n }\n try {\n const accept = (await headers()).get('accept-language') || ''\n const match = resolveLocaleFromAcceptLanguage(accept)\n if (match) return match\n } catch {\n // headers() may not be available outside request context (e.g., in tests)\n }\n } catch {\n // next/headers not available (CLI context)\n }\n return defaultLocale\n}\n\nexport async function loadDictionary(locale: Locale): Promise<Dict> {\n // Locale dictionaries are immutable at runtime, so the flatten+merge below\n // only needs to run once per locale. The cache is invalidated whenever\n // modules or the app dictionary loader are (re)registered.\n const cached = getCachedDictionary(locale)\n if (cached) return cached\n // Load from registry instead of @/ import (works in standalone packages)\n const baseRaw = await loadAppDictionary(locale)\n const merged: Dict = { ...flattenDictionary(baseRaw) }\n const modules = getModules()\n for (const m of modules) {\n const dict = m.translations?.[locale]\n if (dict) Object.assign(merged, flattenDictionary(dict))\n }\n setCachedDictionary(locale, merged)\n return merged\n}\n\nexport async function resolveTranslations() {\n const locale = await detectLocale()\n const dict = await loadDictionary(locale)\n const t = createTranslator(dict)\n const translate = createFallbackTranslator(dict)\n return { locale, dict, t, translate }\n}\n// Hint Next.js to keep this server-only; ignore if unavailable when running scripts outside Next.\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n require('server-only')\n} catch {\n // noop: allows running generator scripts without Next's server-only package\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe,eAA4B;AAEpD,SAAS,qBAAqB,uCAAuC;AACrE,SAAS,0BAA0B,wBAAwB;AAC3D,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB,2BAA2B;AAGzD,SAAS,iBAAiB,cAAAA,mBAAkB;AAC5C,SAAS,mCAAmC;AAC5C,SAAS,iCAAiC;AAE1C,SAAS,kBAAkB,QAAiB,SAAS,IAAU;AAC7D,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAC5E,QAAM,SAAe,CAAC;AACtB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAiC,GAAG;AAC5E,QAAI,CAAC,IAAK;AACV,UAAM,UAAU,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AAC9C,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,OAAO,IAAI;AAAA,IACpB,WAAW,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACtE,aAAO,OAAO,QAAQ,kBAAkB,OAAO,OAAO,CAAC;AAAA,IACzD;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,eAAgC;AAEpD,QAAM,SAAS,oBAAoB,QAAQ,GAAG;AAC9C,MAAI,OAAQ,QAAO;AAEnB,MAAI;AACF,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,OAAO,cAAc;AACxD,QAAI;AACF,YAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,QAAQ,GAAG;AAC3C,UAAI,KAAK,QAAQ,SAAS,CAAW,EAAG,QAAO;AAAA,IACjD,QAAQ;AAAA,IAER;AACA,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,GAAG,IAAI,iBAAiB,KAAK;AAC3D,YAAM,QAAQ,gCAAgC,MAAM;AACpD,UAAI,MAAO,QAAO;AAAA,IACpB,QAAQ;AAAA,IAER;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,eAAsB,eAAe,QAA+B;AAIlE,QAAM,SAAS,oBAAoB,MAAM;AACzC,MAAI,OAAQ,QAAO;AAEnB,QAAM,UAAU,MAAM,kBAAkB,MAAM;AAC9C,QAAM,SAAe,EAAE,GAAG,kBAAkB,OAAO,EAAE;AACrD,QAAM,UAAU,WAAW;AAC3B,aAAW,KAAK,SAAS;AACvB,UAAM,OAAO,EAAE,eAAe,MAAM;AACpC,QAAI,KAAM,QAAO,OAAO,QAAQ,kBAAkB,IAAI,CAAC;AAAA,EACzD;AACA,sBAAoB,QAAQ,MAAM;AAClC,SAAO;AACT;AAEA,eAAsB,sBAAsB;AAC1C,QAAM,SAAS,MAAM,aAAa;AAClC,QAAM,OAAO,MAAM,eAAe,MAAM;AACxC,QAAM,IAAI,iBAAiB,IAAI;AAC/B,QAAM,YAAY,yBAAyB,IAAI;AAC/C,SAAO,EAAE,QAAQ,MAAM,GAAG,UAAU;AACtC;AAEA,IAAI;AAEF,UAAQ,aAAa;AACvB,QAAQ;AAER;",
|
|
6
|
+
"names": ["getModules"]
|
|
7
7
|
}
|
package/dist/lib/logger/index.js
CHANGED
|
@@ -1,53 +1,12 @@
|
|
|
1
1
|
import { selectTransport } from "./transport.js";
|
|
2
|
-
import {
|
|
3
|
-
import { isLevelEnabled } from "./level.js";
|
|
4
|
-
import { getLogLevel, isLevelEnabled as isLevelEnabled2, resetLogLevelCache, OM_LOG_LEVEL_ENV } from "./level.js";
|
|
2
|
+
import { getLogLevel, isLevelEnabled, resetLogLevelCache, OM_LOG_LEVEL_ENV } from "./level.js";
|
|
5
3
|
import { resetServerLoggerCache, OM_LOG_DESTINATION_ENV } from "./transport.server.js";
|
|
6
4
|
import { resetLogPrettyCache, OM_LOG_PRETTY_ENV } from "./transport.pretty.js";
|
|
7
|
-
import {
|
|
8
|
-
getLoggerExtension as getLoggerExtension2,
|
|
9
|
-
registerLoggerExtension,
|
|
10
|
-
resetLoggerExtension
|
|
11
|
-
} from "./extension.js";
|
|
12
5
|
const loggerRegistry = /* @__PURE__ */ new Map();
|
|
13
|
-
function createExtendedLogger(namespace, transport, bindings = {}) {
|
|
14
|
-
const emit = (level, msg, fields) => {
|
|
15
|
-
if (!isLevelEnabled(level)) return;
|
|
16
|
-
const extension = getLoggerExtension();
|
|
17
|
-
let context = {};
|
|
18
|
-
try {
|
|
19
|
-
context = extension?.enrich?.() ?? {};
|
|
20
|
-
} catch {
|
|
21
|
-
}
|
|
22
|
-
const mergedFields = { ...fields, ...context };
|
|
23
|
-
transport[level](msg, mergedFields);
|
|
24
|
-
try {
|
|
25
|
-
extension?.emit?.({
|
|
26
|
-
level,
|
|
27
|
-
namespace,
|
|
28
|
-
message: msg,
|
|
29
|
-
fields: { ...bindings, ...mergedFields },
|
|
30
|
-
time: Date.now()
|
|
31
|
-
});
|
|
32
|
-
} catch {
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
return {
|
|
36
|
-
debug: (msg, fields) => emit("debug", msg, fields),
|
|
37
|
-
info: (msg, fields) => emit("info", msg, fields),
|
|
38
|
-
warn: (msg, fields) => emit("warn", msg, fields),
|
|
39
|
-
error: (msg, fields) => emit("error", msg, fields),
|
|
40
|
-
child: (childBindings) => createExtendedLogger(
|
|
41
|
-
namespace,
|
|
42
|
-
transport.child(childBindings),
|
|
43
|
-
{ ...bindings, ...childBindings }
|
|
44
|
-
)
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
6
|
function createLogger(namespace) {
|
|
48
7
|
const existing = loggerRegistry.get(namespace);
|
|
49
8
|
if (existing) return existing;
|
|
50
|
-
const created =
|
|
9
|
+
const created = selectTransport(namespace);
|
|
51
10
|
loggerRegistry.set(namespace, created);
|
|
52
11
|
return created;
|
|
53
12
|
}
|
|
@@ -60,12 +19,9 @@ export {
|
|
|
60
19
|
OM_LOG_PRETTY_ENV,
|
|
61
20
|
createLogger,
|
|
62
21
|
getLogLevel,
|
|
63
|
-
|
|
64
|
-
isLevelEnabled2 as isLevelEnabled,
|
|
65
|
-
registerLoggerExtension,
|
|
22
|
+
isLevelEnabled,
|
|
66
23
|
resetLogLevelCache,
|
|
67
24
|
resetLogPrettyCache,
|
|
68
|
-
resetLoggerExtension,
|
|
69
25
|
resetLoggerRegistry,
|
|
70
26
|
resetServerLoggerCache
|
|
71
27
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/logger/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { selectTransport } from './transport'\
|
|
5
|
-
"mappings": "AAAA,SAAS,uBAAuB;
|
|
6
|
-
"names": [
|
|
4
|
+
"sourcesContent": ["import { selectTransport } from './transport'\n\nexport type { LogLevel } from './level'\nexport { getLogLevel, isLevelEnabled, resetLogLevelCache, OM_LOG_LEVEL_ENV } from './level'\nexport { resetServerLoggerCache, OM_LOG_DESTINATION_ENV } from './transport.server'\nexport { resetLogPrettyCache, OM_LOG_PRETTY_ENV } from './transport.pretty'\n\nexport type LogBindings = Record<string, unknown>\n\nexport interface Logger {\n debug(msg: string, fields?: LogBindings): void\n info(msg: string, fields?: LogBindings): void\n warn(msg: string, fields?: LogBindings): void\n error(msg: string, fields?: LogBindings): void\n /** Returns a logger with `bindings` merged into every subsequent line. */\n child(bindings: LogBindings): Logger\n}\n\nconst loggerRegistry = new Map<string, Logger>()\n\n/** Create (or reuse) a namespaced logger. `namespace` is attached as `name`. */\nexport function createLogger(namespace: string): Logger {\n const existing = loggerRegistry.get(namespace)\n if (existing) return existing\n const created = selectTransport(namespace)\n loggerRegistry.set(namespace, created)\n return created\n}\n\n/** Internal: clear cached loggers so tests can re-run transport selection. */\nexport function resetLoggerRegistry(): void {\n loggerRegistry.clear()\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uBAAuB;AAGhC,SAAS,aAAa,gBAAgB,oBAAoB,wBAAwB;AAClF,SAAS,wBAAwB,8BAA8B;AAC/D,SAAS,qBAAqB,yBAAyB;AAavD,MAAM,iBAAiB,oBAAI,IAAoB;AAGxC,SAAS,aAAa,WAA2B;AACtD,QAAM,WAAW,eAAe,IAAI,SAAS;AAC7C,MAAI,SAAU,QAAO;AACrB,QAAM,UAAU,gBAAgB,SAAS;AACzC,iBAAe,IAAI,WAAW,OAAO;AACrC,SAAO;AACT;AAGO,SAAS,sBAA4B;AAC1C,iBAAe,MAAM;AACvB;",
|
|
6
|
+
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/modules/registry.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Module } from '@open-mercato/shared/modules/registry'\nimport { applyModuleOverridesToModules } from '@open-mercato/shared/modules/overrides'\nimport { invalidateDictionaryCache } from '../i18n/dictionary-cache'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'modules-registry' })\n\n// Registration pattern for publishable packages.\n// Use globalThis to survive tsx/esbuild module duplication where the same\n// registry.ts file can be loaded as multiple module instances when mixing\n// dynamic and static imports \u2014 for example a standalone integration test\n// bootstraps via the source path while a worker handler resolves it through\n// node_modules/@open-mercato/shared/dist/. Mirrors the same workaround used\n// by `getDiRegistrars()` in `../di/container.ts`.\nconst GLOBAL_KEY = '__openMercatoModulesRegistry__'\n\nfunction getGlobalModules(): Module[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalModules(modules: Module[]): void {\n ;(globalThis as any)[GLOBAL_KEY] = modules\n}\n\nfunction hasRuntimeContracts(entry: Module): boolean {\n return Object.keys(entry).some((key) => key !== 'id' && key !== 'translations')\n}\n\nfunction isI18nOnlyRegistration(modules: Module[]): boolean {\n return modules.length > 0 && modules.every((entry) => !hasRuntimeContracts(entry))\n}\n\nfunction mergeI18nModules(existing: Module[], incoming: Module[]): Module[] {\n const incomingById = new Map(incoming.map((entry) => [entry.id, entry]))\n const existingIds = new Set(existing.map((entry) => entry.id))\n const merged = existing.map((entry) => {\n const i18nModule = incomingById.get(entry.id)\n if (!i18nModule?.translations) return entry\n return {\n ...entry,\n translations: i18nModule.translations,\n }\n })\n\n for (const entry of incoming) {\n if (!existingIds.has(entry.id)) merged.push(entry)\n }\n\n return merged\n}\n\nexport function registerModules(modules: Module[]) {\n const existing = getGlobalModules()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Modules re-registered (this may occur during HMR)')\n }\n const nextModules = applyModuleOverridesToModules(modules)\n const shouldMergeI18nOnly = existing !== null\n && existing.some(hasRuntimeContracts)\n && isI18nOnlyRegistration(nextModules)\n setGlobalModules(shouldMergeI18nOnly ? mergeI18nModules(existing, nextModules) : nextModules)\n invalidateDictionaryCache()\n}\n\nexport function getModules(): Module[] {\n const modules = getGlobalModules()\n if (!modules) {\n throw new Error('[Bootstrap] Modules not registered. Call registerModules() at bootstrap.')\n }\n return modules\n}\n
|
|
5
|
-
"mappings": "AACA,SAAS,qCAAqC;AAC9C,SAAS,iCAAiC;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAS7E,MAAM,aAAa;AAEnB,SAAS,mBAAoC;AAC3C,SAAQ,WAAmB,UAAU,KAAK;AAC5C;AAEA,SAAS,iBAAiB,SAAyB;AACjD;AAAC,EAAC,WAAmB,UAAU,IAAI;AACrC;AAEA,SAAS,oBAAoB,OAAwB;AACnD,SAAO,OAAO,KAAK,KAAK,EAAE,KAAK,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,cAAc;AAChF;AAEA,SAAS,uBAAuB,SAA4B;AAC1D,SAAO,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAC,UAAU,CAAC,oBAAoB,KAAK,CAAC;AACnF;AAEA,SAAS,iBAAiB,UAAoB,UAA8B;AAC1E,QAAM,eAAe,IAAI,IAAI,SAAS,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;AACvE,QAAM,cAAc,IAAI,IAAI,SAAS,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAC7D,QAAM,SAAS,SAAS,IAAI,CAAC,UAAU;AACrC,UAAM,aAAa,aAAa,IAAI,MAAM,EAAE;AAC5C,QAAI,CAAC,YAAY,aAAc,QAAO;AACtC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,cAAc,WAAW;AAAA,IAC3B;AAAA,EACF,CAAC;AAED,aAAW,SAAS,UAAU;AAC5B,QAAI,CAAC,YAAY,IAAI,MAAM,EAAE,EAAG,QAAO,KAAK,KAAK;AAAA,EACnD;AAEA,SAAO;AACT;AAEO,SAAS,gBAAgB,SAAmB;AACjD,QAAM,WAAW,iBAAiB;AAClC,MAAI,aAAa,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC/D,WAAO,MAAM,mDAAmD;AAAA,EAClE;AACA,QAAM,cAAc,8BAA8B,OAAO;AACzD,QAAM,sBAAsB,aAAa,QACpC,SAAS,KAAK,mBAAmB,KACjC,uBAAuB,WAAW;AACvC,mBAAiB,sBAAsB,iBAAiB,UAAU,WAAW,IAAI,WAAW;AAC5F,4BAA0B;AAC5B;AAEO,SAAS,aAAuB;AACrC,QAAM,UAAU,iBAAiB;AACjC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AACA,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["import type { Module } from '@open-mercato/shared/modules/registry'\nimport { applyModuleOverridesToModules } from '@open-mercato/shared/modules/overrides'\nimport { invalidateDictionaryCache } from '../i18n/dictionary-cache'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'modules-registry' })\n\n// Registration pattern for publishable packages.\n// Use globalThis to survive tsx/esbuild module duplication where the same\n// registry.ts file can be loaded as multiple module instances when mixing\n// dynamic and static imports \u2014 for example a standalone integration test\n// bootstraps via the source path while a worker handler resolves it through\n// node_modules/@open-mercato/shared/dist/. Mirrors the same workaround used\n// by `getDiRegistrars()` in `../di/container.ts`.\nconst GLOBAL_KEY = '__openMercatoModulesRegistry__'\n\nfunction getGlobalModules(): Module[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalModules(modules: Module[]): void {\n ;(globalThis as any)[GLOBAL_KEY] = modules\n}\n\nfunction hasRuntimeContracts(entry: Module): boolean {\n return Object.keys(entry).some((key) => key !== 'id' && key !== 'translations')\n}\n\nfunction isI18nOnlyRegistration(modules: Module[]): boolean {\n return modules.length > 0 && modules.every((entry) => !hasRuntimeContracts(entry))\n}\n\nfunction mergeI18nModules(existing: Module[], incoming: Module[]): Module[] {\n const incomingById = new Map(incoming.map((entry) => [entry.id, entry]))\n const existingIds = new Set(existing.map((entry) => entry.id))\n const merged = existing.map((entry) => {\n const i18nModule = incomingById.get(entry.id)\n if (!i18nModule?.translations) return entry\n return {\n ...entry,\n translations: i18nModule.translations,\n }\n })\n\n for (const entry of incoming) {\n if (!existingIds.has(entry.id)) merged.push(entry)\n }\n\n return merged\n}\n\nexport function registerModules(modules: Module[]) {\n const existing = getGlobalModules()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Modules re-registered (this may occur during HMR)')\n }\n const nextModules = applyModuleOverridesToModules(modules)\n const shouldMergeI18nOnly = existing !== null\n && existing.some(hasRuntimeContracts)\n && isI18nOnlyRegistration(nextModules)\n setGlobalModules(shouldMergeI18nOnly ? mergeI18nModules(existing, nextModules) : nextModules)\n invalidateDictionaryCache()\n}\n\nexport function getModules(): Module[] {\n const modules = getGlobalModules()\n if (!modules) {\n throw new Error('[Bootstrap] Modules not registered. Call registerModules() at bootstrap.')\n }\n return modules\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,qCAAqC;AAC9C,SAAS,iCAAiC;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAS7E,MAAM,aAAa;AAEnB,SAAS,mBAAoC;AAC3C,SAAQ,WAAmB,UAAU,KAAK;AAC5C;AAEA,SAAS,iBAAiB,SAAyB;AACjD;AAAC,EAAC,WAAmB,UAAU,IAAI;AACrC;AAEA,SAAS,oBAAoB,OAAwB;AACnD,SAAO,OAAO,KAAK,KAAK,EAAE,KAAK,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,cAAc;AAChF;AAEA,SAAS,uBAAuB,SAA4B;AAC1D,SAAO,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAC,UAAU,CAAC,oBAAoB,KAAK,CAAC;AACnF;AAEA,SAAS,iBAAiB,UAAoB,UAA8B;AAC1E,QAAM,eAAe,IAAI,IAAI,SAAS,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;AACvE,QAAM,cAAc,IAAI,IAAI,SAAS,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAC7D,QAAM,SAAS,SAAS,IAAI,CAAC,UAAU;AACrC,UAAM,aAAa,aAAa,IAAI,MAAM,EAAE;AAC5C,QAAI,CAAC,YAAY,aAAc,QAAO;AACtC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,cAAc,WAAW;AAAA,IAC3B;AAAA,EACF,CAAC;AAED,aAAW,SAAS,UAAU;AAC5B,QAAI,CAAC,YAAY,IAAI,MAAM,EAAE,EAAG,QAAO,KAAK,KAAK;AAAA,EACnD;AAEA,SAAO;AACT;AAEO,SAAS,gBAAgB,SAAmB;AACjD,QAAM,WAAW,iBAAiB;AAClC,MAAI,aAAa,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC/D,WAAO,MAAM,mDAAmD;AAAA,EAClE;AACA,QAAM,cAAc,8BAA8B,OAAO;AACzD,QAAM,sBAAsB,aAAa,QACpC,SAAS,KAAK,mBAAmB,KACjC,uBAAuB,WAAW;AACvC,mBAAiB,sBAAsB,iBAAiB,UAAU,WAAW,IAAI,WAAW;AAC5F,4BAA0B;AAC5B;AAEO,SAAS,aAAuB;AACrC,QAAM,UAAU,iBAAiB;AACjC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|