@open-mercato/core 0.10.1-develop.7679.1.cb2c13f49e → 0.10.1-develop.7687.1.e1a6f9de51

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.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/users/route.js +1 -1
  3. package/dist/modules/auth/api/users/route.js.map +2 -2
  4. package/dist/modules/customer_accounts/api/magic-link/request.js +15 -1
  5. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  6. package/dist/modules/customer_accounts/api/password/reset-request.js +15 -1
  7. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  8. package/dist/modules/customer_accounts/emails/CustomerAuthLinkEmail.js +26 -0
  9. package/dist/modules/customer_accounts/emails/CustomerAuthLinkEmail.js.map +7 -0
  10. package/dist/modules/customer_accounts/lib/authLinkEmails.js +67 -0
  11. package/dist/modules/customer_accounts/lib/authLinkEmails.js.map +7 -0
  12. package/dist/modules/messages/commands/actions.js +8 -1
  13. package/dist/modules/messages/commands/actions.js.map +2 -2
  14. package/dist/modules/payment_gateways/data/entities.js +4 -0
  15. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  16. package/dist/modules/payment_gateways/lib/session-idempotency.js +29 -5
  17. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  18. package/dist/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.js +19 -0
  19. package/dist/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.js.map +7 -0
  20. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +3 -3
  21. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +2 -2
  22. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.js +59 -0
  23. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.js.map +7 -0
  24. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.js +11 -0
  25. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.js.map +7 -0
  26. package/dist/modules/query_index/lib/engine.js +60 -13
  27. package/dist/modules/query_index/lib/engine.js.map +2 -2
  28. package/package.json +7 -7
  29. package/src/modules/auth/api/users/route.ts +1 -1
  30. package/src/modules/customer_accounts/api/magic-link/request.ts +16 -3
  31. package/src/modules/customer_accounts/api/password/reset-request.ts +16 -3
  32. package/src/modules/customer_accounts/emails/CustomerAuthLinkEmail.tsx +45 -0
  33. package/src/modules/customer_accounts/i18n/de.json +12 -0
  34. package/src/modules/customer_accounts/i18n/en.json +12 -0
  35. package/src/modules/customer_accounts/i18n/es.json +12 -0
  36. package/src/modules/customer_accounts/i18n/ko.json +12 -0
  37. package/src/modules/customer_accounts/i18n/pl.json +12 -0
  38. package/src/modules/customer_accounts/lib/authLinkEmails.ts +77 -0
  39. package/src/modules/messages/commands/actions.ts +8 -1
  40. package/src/modules/payment_gateways/data/entities.ts +4 -0
  41. package/src/modules/payment_gateways/lib/session-idempotency.ts +41 -5
  42. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +14 -1
  43. package/src/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.ts +18 -0
  44. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +3 -3
  45. package/src/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.ts +9 -0
  46. package/src/modules/portal/frontend/[orgSlug]/portal/magic-link/page.tsx +71 -0
  47. package/src/modules/portal/i18n/de.json +7 -0
  48. package/src/modules/portal/i18n/en.json +7 -0
  49. package/src/modules/portal/i18n/es.json +7 -0
  50. package/src/modules/portal/i18n/ko.json +7 -0
  51. package/src/modules/portal/i18n/pl.json +7 -0
  52. package/src/modules/query_index/lib/engine.ts +70 -12
@@ -64,6 +64,13 @@
64
64
  "portal.login.submit": "Anmelden",
65
65
  "portal.login.submitting": "Anmeldung…",
66
66
  "portal.login.title": "Anmelden",
67
+ "portal.magicLink.backToLogin": "Zurück zur Anmeldung",
68
+ "portal.magicLink.description": "Melden Sie sich mit dem Link aus Ihrer E-Mail im Portal an.",
69
+ "portal.magicLink.error.generic": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.",
70
+ "portal.magicLink.error.invalidToken": "Dieser Anmeldelink ist ungültig oder abgelaufen. Fordern Sie einen neuen an.",
71
+ "portal.magicLink.error.missingToken": "Der Anmeldelink ist ungültig oder unvollständig.",
72
+ "portal.magicLink.submit": "Anmelden",
73
+ "portal.magicLink.title": "Mit einem Link anmelden",
67
74
  "portal.nav.account": "Konto",
68
75
  "portal.nav.dashboard": "Dashboard",
69
76
  "portal.nav.home": "Portal",
@@ -64,6 +64,13 @@
64
64
  "portal.login.submit": "Log In",
65
65
  "portal.login.submitting": "Logging in…",
66
66
  "portal.login.title": "Log In",
67
+ "portal.magicLink.backToLogin": "Back to sign in",
68
+ "portal.magicLink.description": "Continue to sign in to the portal with the link from your email.",
69
+ "portal.magicLink.error.generic": "Sign-in failed. Please try again.",
70
+ "portal.magicLink.error.invalidToken": "This sign-in link is invalid or has expired. Request a new one.",
71
+ "portal.magicLink.error.missingToken": "Sign-in link is invalid or incomplete.",
72
+ "portal.magicLink.submit": "Sign in",
73
+ "portal.magicLink.title": "Sign in with a link",
67
74
  "portal.nav.account": "Account",
68
75
  "portal.nav.dashboard": "Dashboard",
69
76
  "portal.nav.home": "Portal",
@@ -64,6 +64,13 @@
64
64
  "portal.login.submit": "Iniciar sesión",
65
65
  "portal.login.submitting": "Iniciando sesión…",
66
66
  "portal.login.title": "Iniciar sesión",
67
+ "portal.magicLink.backToLogin": "Volver al inicio de sesión",
68
+ "portal.magicLink.description": "Continúa para iniciar sesión en el portal con el enlace de tu correo.",
69
+ "portal.magicLink.error.generic": "Error al iniciar sesión. Inténtalo de nuevo.",
70
+ "portal.magicLink.error.invalidToken": "Este enlace de inicio de sesión no es válido o ha caducado. Solicita uno nuevo.",
71
+ "portal.magicLink.error.missingToken": "El enlace de inicio de sesión no es válido o está incompleto.",
72
+ "portal.magicLink.submit": "Iniciar sesión",
73
+ "portal.magicLink.title": "Inicia sesión con un enlace",
67
74
  "portal.nav.account": "Cuenta",
68
75
  "portal.nav.dashboard": "Panel",
69
76
  "portal.nav.home": "Portal",
@@ -64,6 +64,13 @@
64
64
  "portal.login.submit": "로그인",
65
65
  "portal.login.submitting": "로그인 중…",
66
66
  "portal.login.title": "로그인",
67
+ "portal.magicLink.backToLogin": "로그인으로 돌아가기",
68
+ "portal.magicLink.description": "이메일의 링크로 포털에 로그인하려면 계속하세요.",
69
+ "portal.magicLink.error.generic": "로그인에 실패했습니다. 다시 시도하세요.",
70
+ "portal.magicLink.error.invalidToken": "이 로그인 링크가 유효하지 않거나 만료되었습니다. 새 링크를 요청하세요.",
71
+ "portal.magicLink.error.missingToken": "로그인 링크가 유효하지 않거나 불완전합니다.",
72
+ "portal.magicLink.submit": "로그인",
73
+ "portal.magicLink.title": "링크로 로그인",
67
74
  "portal.nav.account": "계정",
68
75
  "portal.nav.dashboard": "대시보드",
69
76
  "portal.nav.home": "포털",
@@ -64,6 +64,13 @@
64
64
  "portal.login.submit": "Zaloguj się",
65
65
  "portal.login.submitting": "Logowanie…",
66
66
  "portal.login.title": "Logowanie",
67
+ "portal.magicLink.backToLogin": "Wróć do logowania",
68
+ "portal.magicLink.description": "Kontynuuj, aby zalogować się do portalu linkiem z wiadomości e-mail.",
69
+ "portal.magicLink.error.generic": "Logowanie nie powiodło się. Spróbuj ponownie.",
70
+ "portal.magicLink.error.invalidToken": "Ten link do logowania jest nieprawidłowy lub wygasł. Poproś o nowy.",
71
+ "portal.magicLink.error.missingToken": "Link do logowania jest nieprawidłowy lub niekompletny.",
72
+ "portal.magicLink.submit": "Zaloguj się",
73
+ "portal.magicLink.title": "Zaloguj się linkiem",
67
74
  "portal.nav.account": "Konto",
68
75
  "portal.nav.dashboard": "Panel",
69
76
  "portal.nav.home": "Portal",
@@ -17,6 +17,13 @@ import { readCoverageSnapshot, refreshCoverageSnapshot } from './coverage'
17
17
  import { createProfiler, shouldEnableProfiler, type Profiler } from '@open-mercato/shared/lib/profiler'
18
18
  import type { VectorIndexService } from '@open-mercato/search/vector'
19
19
  import { decryptIndexDocCustomFields } from '@open-mercato/shared/lib/encryption/indexDoc'
20
+ import {
21
+ DECLINED_ENCRYPTED_SORT_LOG_MESSAGE,
22
+ DECRYPT_REFUSAL_LOG_MESSAGE,
23
+ DecryptRefusalTally,
24
+ resolveDecryptEnabled,
25
+ resolveDecryptScope,
26
+ } from '@open-mercato/shared/lib/encryption/decryptScope'
20
27
  import {
21
28
  buildCustomFieldKindIndex,
22
29
  createCustomFieldKindMapResolver,
@@ -49,7 +56,13 @@ import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
49
56
  import { buildContainmentPatterns } from '@open-mercato/shared/lib/search/containment'
50
57
  import { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from '@open-mercato/shared/lib/query/query-extension-runner'
51
58
  import { warnOnCiphertextLikeFallback } from '@open-mercato/shared/lib/query/ciphertext-search-warning'
52
- import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from '@open-mercato/shared/lib/query/encrypted-sort'
59
+ import {
60
+ matchEncryptedSortFields,
61
+ resolveEncryptedFieldNames,
62
+ resolveEncryptedSortFields,
63
+ resolveEncryptedSortMaxRows,
64
+ sortRowsInMemory,
65
+ } from '@open-mercato/shared/lib/query/encrypted-sort'
53
66
  import { resolveListCountCap } from '@open-mercato/shared/lib/query/count-cap'
54
67
  import { resolveCfDefIndexOrgCandidates } from '@open-mercato/shared/lib/crud/custom-field-definition-index'
55
68
  import { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'
@@ -776,7 +789,8 @@ export class HybridQueryEngine implements QueryEngine {
776
789
  const fallbackOrgId =
777
790
  opts.organizationId
778
791
  ?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null)
779
- const encSvc = this.getEncryptionService()
792
+ const decryptEnabled = resolveDecryptEnabled(opts)
793
+ const encSvc = decryptEnabled ? this.getEncryptionService() : null
780
794
  const resolvedSorts: Sort[] = []
781
795
  for (const sort of opts.sort || []) {
782
796
  const field = String(sort.field)
@@ -787,14 +801,40 @@ export class HybridQueryEngine implements QueryEngine {
787
801
  if (baseField) resolvedSorts.push({ ...sort, field: baseField })
788
802
  }
789
803
  }
790
- const encryptedSortFields = await resolveEncryptedSortFields(
804
+ const plainSortFields = resolvedSorts
805
+ .filter((sort) => !sort.field.startsWith('cf:'))
806
+ .map((sort) => sort.field)
807
+ const encryptedFieldNames = await resolveEncryptedFieldNames(
791
808
  encSvc,
792
809
  entity,
793
- resolvedSorts.filter((sort) => !sort.field.startsWith('cf:')).map((sort) => sort.field),
794
810
  opts.tenantId ?? null,
795
811
  fallbackOrgId,
796
812
  )
813
+ const encryptedSortFields = matchEncryptedSortFields(encryptedFieldNames, plainSortFields)
797
814
  const requiresPlaintextSort = encryptedSortFields.size > 0
815
+ if (!decryptEnabled && plainSortFields.length > 0) {
816
+ const declinedSortFields = await resolveEncryptedSortFields(
817
+ this.getEncryptionService(),
818
+ entity,
819
+ plainSortFields,
820
+ opts.tenantId ?? null,
821
+ fallbackOrgId,
822
+ )
823
+ if (declinedSortFields.size > 0) {
824
+ logger.warn(DECLINED_ENCRYPTED_SORT_LOG_MESSAGE, {
825
+ entity: String(entity),
826
+ sortFields: Array.from(declinedSortFields),
827
+ })
828
+ }
829
+ }
830
+ const requestedFields = opts.fields && opts.fields.length ? opts.fields.map(String) : null
831
+ const injectsTenantIdForDecrypt =
832
+ encSvc != null
833
+ && hasTenantColumn
834
+ && requestedFields != null
835
+ && !requestedFields.includes('tenant_id')
836
+ && !requestedFields.includes('tenantId')
837
+ && (encryptedFieldNames === null || encryptedFieldNames.length > 0)
798
838
 
799
839
  const cfSortKeys = Array.from(new Set(
800
840
  resolvedSorts.filter((sort) => sort.field.startsWith('cf:')).map((sort) => sort.field.slice(3))
@@ -1177,6 +1217,7 @@ export class HybridQueryEngine implements QueryEngine {
1177
1217
  if (requiresPlaintextSort) {
1178
1218
  for (const field of encryptedSortFields) selectFieldSet.add(field)
1179
1219
  }
1220
+ if (injectsTenantIdForDecrypt) selectFieldSet.add('tenant_id')
1180
1221
  if (opts.includeCustomFields === true) {
1181
1222
  const entityIds = Array.from(new Set(indexSources.map((src) => String(src.entityId))))
1182
1223
  try {
@@ -1304,20 +1345,28 @@ export class HybridQueryEngine implements QueryEngine {
1304
1345
  return customFieldKinds
1305
1346
  }
1306
1347
 
1348
+ const refusalTally = new DecryptRefusalTally()
1349
+
1307
1350
  const decryptRow = async (item: Record<string, unknown>): Promise<Record<string, unknown>> => {
1308
1351
  let next = item
1352
+ if (!encSvc) return next
1353
+ const decision = resolveDecryptScope({
1354
+ rowTenantId: (next?.tenant_id ?? next?.tenantId ?? null) as string | null,
1355
+ rowOrganizationId: (next?.organization_id ?? next?.organizationId ?? null) as string | null,
1356
+ callerTenantId: opts.tenantId ?? null,
1357
+ callerOrganizationId: fallbackOrgId ?? null,
1358
+ })
1359
+ if (!decision.decrypt) {
1360
+ refusalTally.record(decision, next?.id == null ? null : String(next.id))
1361
+ return next
1362
+ }
1309
1363
  if (encSvc?.decryptEntityPayload) {
1310
1364
  const decrypt = encSvc.decryptEntityPayload.bind(encSvc) as (
1311
1365
  entityId: EntityId, payload: Record<string, unknown>, tenantId: string | null, organizationId: string | null,
1312
1366
  options?: { em?: EntityManager },
1313
1367
  ) => Promise<Record<string, unknown>>
1314
1368
  try {
1315
- const decrypted = await decrypt(
1316
- entity, next,
1317
- (next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null) as string | null,
1318
- (next?.organization_id ?? next?.organizationId ?? fallbackOrgId ?? null) as string | null,
1319
- { em: this.em },
1320
- )
1369
+ const decrypted = await decrypt(entity, next, decision.tenantId, decision.organizationId, { em: this.em })
1321
1370
  next = { ...next, ...decrypted }
1322
1371
  } catch (err) {
1323
1372
  logger.error('Error decrypting entity payload', { err })
@@ -1328,8 +1377,8 @@ export class HybridQueryEngine implements QueryEngine {
1328
1377
  // The kind is resolved against the very scope the value is decrypted with, so an
1329
1378
  // organization-specific definition can never type another organization's value.
1330
1379
  const cfScope = {
1331
- tenantId: (next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null) as string | null,
1332
- organizationId: (next?.organization_id ?? next?.organizationId ?? fallbackOrgId ?? null) as string | null,
1380
+ tenantId: decision.tenantId,
1381
+ organizationId: decision.organizationId,
1333
1382
  }
1334
1383
  const resolveKinds = await resolveCustomFieldKinds()
1335
1384
  next = await decryptIndexDocCustomFields(
@@ -1341,6 +1390,11 @@ export class HybridQueryEngine implements QueryEngine {
1341
1390
  return next
1342
1391
  }
1343
1392
 
1393
+ const reportDecryptRefusals = () => {
1394
+ if (refusalTally.refused === 0) return
1395
+ logger.warn(DECRYPT_REFUSAL_LOG_MESSAGE, refusalTally.toLogContext(String(entity)))
1396
+ }
1397
+
1344
1398
  let items: Record<string, unknown>[]
1345
1399
  let encryptedSortRowCapWarning: EncryptedSortRowCapWarning | undefined
1346
1400
 
@@ -1431,6 +1485,10 @@ export class HybridQueryEngine implements QueryEngine {
1431
1485
  ) as Record<string, unknown>[]
1432
1486
  items = await mapWithConcurrency(itemsRaw, DECRYPT_CONCURRENCY, decryptRow)
1433
1487
  }
1488
+ reportDecryptRefusals()
1489
+ if (injectsTenantIdForDecrypt) {
1490
+ for (const row of items) delete row.tenant_id
1491
+ }
1434
1492
  if (debugEnabled) this.debug('query:complete', { entity, total, items: items.length })
1435
1493
 
1436
1494
  const typedItems = items as unknown as T[]