@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/users/route.js +1 -1
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/customer_accounts/api/magic-link/request.js +15 -1
- package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
- package/dist/modules/customer_accounts/api/password/reset-request.js +15 -1
- package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
- package/dist/modules/customer_accounts/emails/CustomerAuthLinkEmail.js +26 -0
- package/dist/modules/customer_accounts/emails/CustomerAuthLinkEmail.js.map +7 -0
- package/dist/modules/customer_accounts/lib/authLinkEmails.js +67 -0
- package/dist/modules/customer_accounts/lib/authLinkEmails.js.map +7 -0
- package/dist/modules/messages/commands/actions.js +8 -1
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +4 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/session-idempotency.js +29 -5
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.js +19 -0
- package/dist/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.js.map +7 -0
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js +3 -3
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.js +59 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +60 -13
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/users/route.ts +1 -1
- package/src/modules/customer_accounts/api/magic-link/request.ts +16 -3
- package/src/modules/customer_accounts/api/password/reset-request.ts +16 -3
- package/src/modules/customer_accounts/emails/CustomerAuthLinkEmail.tsx +45 -0
- package/src/modules/customer_accounts/i18n/de.json +12 -0
- package/src/modules/customer_accounts/i18n/en.json +12 -0
- package/src/modules/customer_accounts/i18n/es.json +12 -0
- package/src/modules/customer_accounts/i18n/ko.json +12 -0
- package/src/modules/customer_accounts/i18n/pl.json +12 -0
- package/src/modules/customer_accounts/lib/authLinkEmails.ts +77 -0
- package/src/modules/messages/commands/actions.ts +8 -1
- package/src/modules/payment_gateways/data/entities.ts +4 -0
- package/src/modules/payment_gateways/lib/session-idempotency.ts +41 -5
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +14 -1
- package/src/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.ts +18 -0
- package/src/modules/payment_gateways/workers/session-initialization-prune.ts +3 -3
- package/src/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/magic-link/page.tsx +71 -0
- package/src/modules/portal/i18n/de.json +7 -0
- package/src/modules/portal/i18n/en.json +7 -0
- package/src/modules/portal/i18n/es.json +7 -0
- package/src/modules/portal/i18n/ko.json +7 -0
- package/src/modules/portal/i18n/pl.json +7 -0
- 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 {
|
|
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
|
|
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
|
|
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:
|
|
1332
|
-
organizationId:
|
|
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[]
|