@open-mercato/core 0.9.0 → 0.9.1-develop.7462.1.94c09f158f
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/generated/entities/customer_dictionary_entry/index.js +2 -0
- package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
- package/dist/generated/entities/customer_interaction/index.js +2 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/attachments/api/route.js +29 -3
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/ocrLimits.js +49 -0
- package/dist/modules/attachments/lib/ocrLimits.js.map +7 -0
- package/dist/modules/attachments/lib/ocrQueue.js +76 -2
- package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
- package/dist/modules/attachments/lib/ocrService.js +4 -0
- package/dist/modules/attachments/lib/ocrService.js.map +2 -2
- package/dist/modules/attachments/lib/pdfProcessing.js +14 -2
- package/dist/modules/attachments/lib/pdfProcessing.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +4 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/textExtraction.js +13 -1
- package/dist/modules/attachments/lib/textExtraction.js.map +2 -2
- package/dist/modules/catalog/commands/categories.js +1 -1
- package/dist/modules/catalog/commands/categories.js.map +2 -2
- package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
- package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +52 -17
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/search.js +3 -1
- package/dist/modules/catalog/search.js.map +2 -2
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +2 -1
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +10 -7
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
- package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-result.js +12 -0
- package/dist/modules/communication_channels/lib/oauth-result.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
- package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
- package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
- package/dist/modules/customers/api/activity-types/route.js +151 -0
- package/dist/modules/customers/api/activity-types/route.js.map +7 -0
- package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -0
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/calendar-event-types.js +548 -0
- package/dist/modules/customers/calendar-event-types.js.map +7 -0
- package/dist/modules/customers/commands/dictionaries.js +70 -10
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +187 -22
- package/dist/modules/customers/commands/interactions.js.map +3 -3
- package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
- package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +3 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
- package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
- package/dist/modules/customers/components/calendar/types.js +1 -0
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +7 -3
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
- package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonCard.js +5 -2
- package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/data/entities.js +6 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +39 -10
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/di.js +20 -1
- package/dist/modules/customers/di.js.map +2 -2
- package/dist/modules/customers/extension-points.js +13 -0
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
- package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
- package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
- package/dist/modules/customers/lib/calendar/timezone.js +111 -0
- package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -0
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +1 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
- package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
- package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +13 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/planner/lib/availabilityMerge.js +101 -38
- package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
- package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/generated/entities/customer_dictionary_entry/index.ts +1 -0
- package/generated/entities/customer_interaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +9 -8
- package/src/modules/attachments/api/route.ts +36 -3
- package/src/modules/attachments/lib/ocrLimits.ts +62 -0
- package/src/modules/attachments/lib/ocrQueue.ts +95 -2
- package/src/modules/attachments/lib/ocrService.ts +4 -0
- package/src/modules/attachments/lib/pdfProcessing.ts +14 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +4 -2
- package/src/modules/attachments/lib/textExtraction.ts +14 -1
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/catalog/commands/categories.ts +1 -1
- package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
- package/src/modules/catalog/commands/products.ts +58 -17
- package/src/modules/catalog/search.ts +5 -1
- package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +2 -1
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +12 -9
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
- package/src/modules/communication_channels/i18n/de.json +4 -1
- package/src/modules/communication_channels/i18n/en.json +4 -1
- package/src/modules/communication_channels/i18n/es.json +4 -1
- package/src/modules/communication_channels/i18n/ko.json +4 -1
- package/src/modules/communication_channels/i18n/pl.json +4 -1
- package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
- package/src/modules/communication_channels/lib/oauth-result.ts +13 -0
- package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
- package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
- package/src/modules/customers/calendar-event-types.ts +674 -0
- package/src/modules/customers/commands/dictionaries.ts +84 -8
- package/src/modules/customers/commands/interactions.ts +244 -21
- package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
- package/src/modules/customers/components/DictionarySettings.tsx +5 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
- package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
- package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
- package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
- package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
- package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
- package/src/modules/customers/components/detail/DealForm.tsx +7 -3
- package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
- package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
- package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
- package/src/modules/customers/components/detail/types.ts +1 -0
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
- package/src/modules/customers/data/entities.ts +8 -1
- package/src/modules/customers/data/validators.ts +42 -7
- package/src/modules/customers/di.ts +21 -0
- package/src/modules/customers/extension-points.ts +13 -0
- package/src/modules/customers/i18n/de.json +104 -8
- package/src/modules/customers/i18n/en.json +104 -8
- package/src/modules/customers/i18n/es.json +104 -8
- package/src/modules/customers/i18n/ko.json +104 -8
- package/src/modules/customers/i18n/pl.json +104 -8
- package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
- package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
- package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
- package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
- package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
- package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
- package/src/modules/customers/lib/calendar/timezone.ts +117 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -0
- package/src/modules/customers/lib/todoCompatibility.ts +2 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
- package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
- package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
- package/src/modules/messages/subscribers/message-notification.ts +13 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
- package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
- package/src/modules/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
- package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
- package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
|
@@ -94,10 +94,12 @@ function buildProductPresenter(translate, record) {
|
|
|
94
94
|
) ?? label;
|
|
95
95
|
const isActive = record.is_active ?? record.isActive;
|
|
96
96
|
const statusText = isActive === false ? translate("catalog.search.status.inactive", "Inactive") : null;
|
|
97
|
+
const productType = readRecordText(record, "product_type", "productType");
|
|
98
|
+
const productTypeLabel = productType ? translate(`catalog.products.types.${productType}`, productType) : null;
|
|
97
99
|
const subtitle = formatSubtitle(
|
|
98
100
|
readRecordText(record, "subtitle"),
|
|
99
101
|
readRecordText(record, "sku"),
|
|
100
|
-
|
|
102
|
+
productTypeLabel,
|
|
101
103
|
statusText
|
|
102
104
|
) ?? snippet(readRecordText(record, "description"));
|
|
103
105
|
return { title, subtitle, icon: "package", badge: label };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/catalog/search.ts"],
|
|
4
|
-
"sourcesContent": ["import type { SearchBuildContext, SearchIndexSource, SearchModuleConfig, SearchResultPresenter } from '@open-mercato/shared/modules/search'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nconst CATALOG_PRODUCTS_URL = '/backend/catalog/products'\nconst CATALOG_CATEGORIES_URL = '/backend/catalog/categories'\nconst CATALOG_CONFIG_URL = '/backend/config/catalog'\n\nfunction normalizeText(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n }\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n return null\n}\n\nfunction pickText(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n const text = normalizeText(candidate)\n if (text) return text\n }\n return null\n}\n\nfunction readRecordText(record: Record<string, unknown>, ...keys: string[]): string | null {\n for (const key of keys) {\n const text = normalizeText(record[key])\n if (text) return text\n }\n return null\n}\n\nfunction formatSubtitle(...parts: Array<unknown>): string | undefined {\n const text = parts\n .map((part) => normalizeText(part))\n .filter((value): value is string => Boolean(value))\n if (!text.length) return undefined\n return text.join(' \u00B7 ')\n}\n\nfunction snippet(value: unknown, maxLength = 140): string | undefined {\n const text = normalizeText(value)\n if (!text) return undefined\n if (text.length <= maxLength) return text\n return `${text.slice(0, maxLength - 3)}...`\n}\n\nfunction appendLine(lines: string[], label: string, value: unknown) {\n if (value === null || value === undefined) return\n const text = Array.isArray(value)\n ? value.map((item) => normalizeText(item)).filter(Boolean).join(', ')\n : typeof value === 'object'\n ? JSON.stringify(value)\n : String(value)\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction friendlyLabel(input: string): string {\n return input\n .replace(/^cf:/, '')\n .replace(/_/g, ' ')\n .replace(/([a-z])([A-Z])/g, (_match, firstChar, secondChar) => `${firstChar} ${secondChar}`)\n .replace(/\\b\\w/g, (character) => character.toUpperCase())\n}\n\nfunction appendCustomFieldLines(lines: string[], customFields: Record<string, unknown>) {\n for (const [key, value] of Object.entries(customFields)) {\n if (value === null || value === undefined) continue\n appendLine(lines, friendlyLabel(key), value)\n }\n}\n\nfunction buildIndexSource(\n ctx: SearchBuildContext,\n presenter: SearchResultPresenter,\n lines: string[],\n): SearchIndexSource | null {\n appendCustomFieldLines(lines, ctx.customFields)\n if (!lines.length) return null\n return {\n text: lines,\n presenter,\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }\n}\n\nfunction buildProductUrl(productId: string | null): string | null {\n if (!productId) return null\n return `${CATALOG_PRODUCTS_URL}/${encodeURIComponent(productId)}`\n}\n\nfunction buildVariantUrl(productId: string | null, variantId: string | null): string | null {\n if (!productId || !variantId) return null\n return `${CATALOG_PRODUCTS_URL}/${encodeURIComponent(productId)}/variants/${encodeURIComponent(variantId)}`\n}\n\nfunction buildCategoryUrl(categoryId: string | null): string | null {\n if (!categoryId) return null\n return `${CATALOG_CATEGORIES_URL}/${encodeURIComponent(categoryId)}/edit`\n}\n\nfunction resolveProductId(record: Record<string, unknown>): string | null {\n // Check direct FK fields first\n const directId = readRecordText(record, 'product_id', 'productId')\n if (directId) return directId\n // Check if product is a string (FK value)\n const product = record.product\n if (typeof product === 'string') return product\n // Check if product is an object with id\n if (product && typeof product === 'object') {\n const productObj = product as Record<string, unknown>\n return readRecordText(productObj, 'id')\n }\n return null\n}\n\nfunction buildProductPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.product', 'Product')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'sku'),\n readRecordText(record, 'handle'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'subtitle'),\n readRecordText(record, 'sku'),\n readRecordText(record, 'product_type', 'productType'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'package', badge: label }\n}\n\nfunction buildVariantPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.variant', 'Variant')\n const isDefault = record.is_default ?? record.isDefault\n const defaultLabel = isDefault ? translate('catalog.search.variant.default', 'Default') : null\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'sku'),\n defaultLabel,\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'sku'),\n readRecordText(record, 'barcode'),\n defaultLabel,\n statusText,\n )\n return { title, subtitle, icon: 'box', badge: label }\n}\n\nfunction buildCategoryPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.category', 'Category')\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'slug'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'tree_path', 'treePath'),\n readRecordText(record, 'slug'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'folder-tree', badge: label }\n}\n\nfunction buildOfferPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n channelName?: string | null,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.offer', 'Channel Offer')\n const channelLabel = channelName ?? readRecordText(record, 'channel_name', 'channelName')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'id'),\n ) ?? label\n const titleWithChannel = channelLabel ? `${title} \u00B7 ${channelLabel}` : title\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(statusText) ?? snippet(readRecordText(record, 'description'))\n return { title: titleWithChannel, subtitle, icon: 'store', badge: label }\n}\n\nfunction buildTagPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.tag', 'Tag')\n const title = pickText(\n readRecordText(record, 'label'),\n readRecordText(record, 'slug'),\n readRecordText(record, 'id'),\n ) ?? label\n const subtitle = formatSubtitle(readRecordText(record, 'slug'))\n return { title, subtitle, icon: 'tag', badge: label }\n}\n\nfunction buildPriceKindPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.priceKind', 'Price Kind')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'code'),\n readRecordText(record, 'id'),\n ) ?? label\n const displayMode = readRecordText(record, 'display_mode', 'displayMode')\n const isPromotion = record.is_promotion ?? record.isPromotion\n const promotionLabel = isPromotion ? translate('catalog.search.priceKind.promotion', 'Promotion') : null\n const displayModeLabel = displayMode === 'including-tax'\n ? translate('catalog.search.priceKind.includingTax', 'Incl. tax')\n : displayMode === 'excluding-tax'\n ? translate('catalog.search.priceKind.excludingTax', 'Excl. tax')\n : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'code'),\n displayModeLabel,\n promotionLabel,\n )\n return { title, subtitle, icon: 'dollar-sign', badge: label }\n}\n\nfunction buildUnitConversionPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.unitConversion', 'Unit Conversion')\n const unitCode = readRecordText(record, 'unit_code', 'unitCode')\n const factor = readRecordText(record, 'to_base_factor', 'toBaseFactor')\n const title = unitCode ? `${unitCode} (\u00D7${factor ?? '?'})` : label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(unitCode, statusText)\n return { title, subtitle, icon: 'ruler', badge: label }\n}\n\nfunction buildOptionSchemaPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.optionSchema', 'Option Schema')\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'code'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'code'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'settings', badge: label }\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: 'catalog:catalog_product',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 10,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Subtitle', record.subtitle)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'SKU', record.sku)\n appendLine(lines, 'Handle', record.handle)\n appendLine(lines, 'Product type', record.product_type ?? record.productType)\n appendLine(lines, 'Base unit', record.default_unit ?? record.defaultUnit)\n appendLine(lines, 'Default sales unit', record.default_sales_unit ?? record.defaultSalesUnit)\n appendLine(\n lines,\n 'Default sales quantity',\n record.default_sales_unit_quantity ?? record.defaultSalesUnitQuantity,\n )\n appendLine(lines, 'UoM rounding mode', record.uom_rounding_mode ?? record.uomRoundingMode)\n appendLine(lines, 'UoM rounding scale', record.uom_rounding_scale ?? record.uomRoundingScale)\n appendLine(lines, 'Unit price enabled', record.unit_price_enabled ?? record.unitPriceEnabled)\n appendLine(\n lines,\n 'Unit price reference unit',\n record.unit_price_reference_unit ?? record.unitPriceReferenceUnit,\n )\n appendLine(\n lines,\n 'Unit price base quantity',\n record.unit_price_base_quantity ?? record.unitPriceBaseQuantity,\n )\n return buildIndexSource(ctx, buildProductPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildProductPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => buildProductUrl(readRecordText(ctx.record, 'id')),\n fieldPolicy: {\n searchable: [\n 'title',\n 'subtitle',\n 'description',\n 'sku',\n 'handle',\n 'product_type',\n 'default_unit',\n 'default_sales_unit',\n 'unit_price_reference_unit',\n ],\n excluded: ['metadata', 'dimensions', 'tax_rate_id'],\n },\n },\n {\n entityId: 'catalog:catalog_product_variant',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 8,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'SKU', record.sku)\n appendLine(lines, 'Barcode', record.barcode)\n appendLine(lines, 'Option values', record.option_values ?? record.optionValues)\n return buildIndexSource(ctx, buildVariantPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildVariantPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n const variantId = readRecordText(ctx.record, 'id')\n return buildVariantUrl(productId, variantId)\n },\n fieldPolicy: {\n searchable: ['name', 'sku', 'barcode'],\n excluded: ['metadata', 'dimensions', 'option_values', 'tax_rate_id'],\n },\n },\n {\n entityId: 'catalog:catalog_product_category',\n aclFeatures: ['catalog.categories.view'],\n enabled: true,\n priority: 7,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Slug', record.slug)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'Path', record.tree_path ?? record.treePath)\n return buildIndexSource(ctx, buildCategoryPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildCategoryPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => buildCategoryUrl(readRecordText(ctx.record, 'id')),\n fieldPolicy: {\n searchable: ['name', 'slug', 'description', 'tree_path'],\n excluded: ['metadata', 'ancestor_ids', 'child_ids', 'descendant_ids'],\n },\n },\n {\n entityId: 'catalog:catalog_offer',\n aclFeatures: ['sales.channels.manage'],\n enabled: true,\n priority: 7,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'Channel', record.channel_id ?? record.channelId)\n return buildIndexSource(ctx, buildOfferPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildOfferPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n return buildProductUrl(productId)\n },\n fieldPolicy: {\n searchable: ['title', 'description'],\n excluded: ['metadata'],\n },\n },\n {\n entityId: 'catalog:catalog_product_tag',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 5,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Label', record.label)\n appendLine(lines, 'Slug', record.slug)\n return buildIndexSource(ctx, buildTagPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildTagPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_PRODUCTS_URL,\n fieldPolicy: {\n searchable: ['label', 'slug'],\n },\n },\n {\n entityId: 'catalog:catalog_price_kind',\n aclFeatures: ['catalog.settings.manage'],\n enabled: true,\n priority: 4,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Code', record.code)\n appendLine(lines, 'Display mode', record.display_mode ?? record.displayMode)\n appendLine(lines, 'Currency', record.currency_code ?? record.currencyCode)\n return buildIndexSource(ctx, buildPriceKindPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildPriceKindPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_CONFIG_URL,\n fieldPolicy: {\n searchable: ['title', 'code'],\n },\n },\n {\n entityId: 'catalog:catalog_product_unit_conversion',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 3,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Unit code', record.unit_code ?? record.unitCode)\n appendLine(lines, 'To base factor', record.to_base_factor ?? record.toBaseFactor)\n appendLine(lines, 'Sort order', record.sort_order ?? record.sortOrder)\n const isActive = record.is_active ?? record.isActive\n appendLine(lines, 'Active', isActive)\n return buildIndexSource(ctx, buildUnitConversionPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildUnitConversionPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n return buildProductUrl(productId)\n },\n fieldPolicy: {\n searchable: ['unit_code'],\n excluded: ['metadata'],\n },\n },\n {\n entityId: 'catalog:catalog_option_schema_template',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 4,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Code', record.code)\n appendLine(lines, 'Description', record.description)\n return buildIndexSource(ctx, buildOptionSchemaPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildOptionSchemaPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_CONFIG_URL,\n fieldPolicy: {\n searchable: ['name', 'code', 'description'],\n excluded: ['schema', 'metadata'],\n },\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { SearchBuildContext, SearchIndexSource, SearchModuleConfig, SearchResultPresenter } from '@open-mercato/shared/modules/search'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nconst CATALOG_PRODUCTS_URL = '/backend/catalog/products'\nconst CATALOG_CATEGORIES_URL = '/backend/catalog/categories'\nconst CATALOG_CONFIG_URL = '/backend/config/catalog'\n\nfunction normalizeText(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n }\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n return null\n}\n\nfunction pickText(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n const text = normalizeText(candidate)\n if (text) return text\n }\n return null\n}\n\nfunction readRecordText(record: Record<string, unknown>, ...keys: string[]): string | null {\n for (const key of keys) {\n const text = normalizeText(record[key])\n if (text) return text\n }\n return null\n}\n\nfunction formatSubtitle(...parts: Array<unknown>): string | undefined {\n const text = parts\n .map((part) => normalizeText(part))\n .filter((value): value is string => Boolean(value))\n if (!text.length) return undefined\n return text.join(' \u00B7 ')\n}\n\nfunction snippet(value: unknown, maxLength = 140): string | undefined {\n const text = normalizeText(value)\n if (!text) return undefined\n if (text.length <= maxLength) return text\n return `${text.slice(0, maxLength - 3)}...`\n}\n\nfunction appendLine(lines: string[], label: string, value: unknown) {\n if (value === null || value === undefined) return\n const text = Array.isArray(value)\n ? value.map((item) => normalizeText(item)).filter(Boolean).join(', ')\n : typeof value === 'object'\n ? JSON.stringify(value)\n : String(value)\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction friendlyLabel(input: string): string {\n return input\n .replace(/^cf:/, '')\n .replace(/_/g, ' ')\n .replace(/([a-z])([A-Z])/g, (_match, firstChar, secondChar) => `${firstChar} ${secondChar}`)\n .replace(/\\b\\w/g, (character) => character.toUpperCase())\n}\n\nfunction appendCustomFieldLines(lines: string[], customFields: Record<string, unknown>) {\n for (const [key, value] of Object.entries(customFields)) {\n if (value === null || value === undefined) continue\n appendLine(lines, friendlyLabel(key), value)\n }\n}\n\nfunction buildIndexSource(\n ctx: SearchBuildContext,\n presenter: SearchResultPresenter,\n lines: string[],\n): SearchIndexSource | null {\n appendCustomFieldLines(lines, ctx.customFields)\n if (!lines.length) return null\n return {\n text: lines,\n presenter,\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }\n}\n\nfunction buildProductUrl(productId: string | null): string | null {\n if (!productId) return null\n return `${CATALOG_PRODUCTS_URL}/${encodeURIComponent(productId)}`\n}\n\nfunction buildVariantUrl(productId: string | null, variantId: string | null): string | null {\n if (!productId || !variantId) return null\n return `${CATALOG_PRODUCTS_URL}/${encodeURIComponent(productId)}/variants/${encodeURIComponent(variantId)}`\n}\n\nfunction buildCategoryUrl(categoryId: string | null): string | null {\n if (!categoryId) return null\n return `${CATALOG_CATEGORIES_URL}/${encodeURIComponent(categoryId)}/edit`\n}\n\nfunction resolveProductId(record: Record<string, unknown>): string | null {\n // Check direct FK fields first\n const directId = readRecordText(record, 'product_id', 'productId')\n if (directId) return directId\n // Check if product is a string (FK value)\n const product = record.product\n if (typeof product === 'string') return product\n // Check if product is an object with id\n if (product && typeof product === 'object') {\n const productObj = product as Record<string, unknown>\n return readRecordText(productObj, 'id')\n }\n return null\n}\n\nfunction buildProductPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.product', 'Product')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'sku'),\n readRecordText(record, 'handle'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const productType = readRecordText(record, 'product_type', 'productType')\n const productTypeLabel = productType\n ? translate(`catalog.products.types.${productType}`, productType)\n : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'subtitle'),\n readRecordText(record, 'sku'),\n productTypeLabel,\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'package', badge: label }\n}\n\nfunction buildVariantPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.variant', 'Variant')\n const isDefault = record.is_default ?? record.isDefault\n const defaultLabel = isDefault ? translate('catalog.search.variant.default', 'Default') : null\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'sku'),\n defaultLabel,\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'sku'),\n readRecordText(record, 'barcode'),\n defaultLabel,\n statusText,\n )\n return { title, subtitle, icon: 'box', badge: label }\n}\n\nfunction buildCategoryPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.category', 'Category')\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'slug'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'tree_path', 'treePath'),\n readRecordText(record, 'slug'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'folder-tree', badge: label }\n}\n\nfunction buildOfferPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n channelName?: string | null,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.offer', 'Channel Offer')\n const channelLabel = channelName ?? readRecordText(record, 'channel_name', 'channelName')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'id'),\n ) ?? label\n const titleWithChannel = channelLabel ? `${title} \u00B7 ${channelLabel}` : title\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(statusText) ?? snippet(readRecordText(record, 'description'))\n return { title: titleWithChannel, subtitle, icon: 'store', badge: label }\n}\n\nfunction buildTagPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.tag', 'Tag')\n const title = pickText(\n readRecordText(record, 'label'),\n readRecordText(record, 'slug'),\n readRecordText(record, 'id'),\n ) ?? label\n const subtitle = formatSubtitle(readRecordText(record, 'slug'))\n return { title, subtitle, icon: 'tag', badge: label }\n}\n\nfunction buildPriceKindPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.priceKind', 'Price Kind')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'code'),\n readRecordText(record, 'id'),\n ) ?? label\n const displayMode = readRecordText(record, 'display_mode', 'displayMode')\n const isPromotion = record.is_promotion ?? record.isPromotion\n const promotionLabel = isPromotion ? translate('catalog.search.priceKind.promotion', 'Promotion') : null\n const displayModeLabel = displayMode === 'including-tax'\n ? translate('catalog.search.priceKind.includingTax', 'Incl. tax')\n : displayMode === 'excluding-tax'\n ? translate('catalog.search.priceKind.excludingTax', 'Excl. tax')\n : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'code'),\n displayModeLabel,\n promotionLabel,\n )\n return { title, subtitle, icon: 'dollar-sign', badge: label }\n}\n\nfunction buildUnitConversionPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.unitConversion', 'Unit Conversion')\n const unitCode = readRecordText(record, 'unit_code', 'unitCode')\n const factor = readRecordText(record, 'to_base_factor', 'toBaseFactor')\n const title = unitCode ? `${unitCode} (\u00D7${factor ?? '?'})` : label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(unitCode, statusText)\n return { title, subtitle, icon: 'ruler', badge: label }\n}\n\nfunction buildOptionSchemaPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.optionSchema', 'Option Schema')\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'code'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'code'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'settings', badge: label }\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: 'catalog:catalog_product',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 10,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Subtitle', record.subtitle)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'SKU', record.sku)\n appendLine(lines, 'Handle', record.handle)\n appendLine(lines, 'Product type', record.product_type ?? record.productType)\n appendLine(lines, 'Base unit', record.default_unit ?? record.defaultUnit)\n appendLine(lines, 'Default sales unit', record.default_sales_unit ?? record.defaultSalesUnit)\n appendLine(\n lines,\n 'Default sales quantity',\n record.default_sales_unit_quantity ?? record.defaultSalesUnitQuantity,\n )\n appendLine(lines, 'UoM rounding mode', record.uom_rounding_mode ?? record.uomRoundingMode)\n appendLine(lines, 'UoM rounding scale', record.uom_rounding_scale ?? record.uomRoundingScale)\n appendLine(lines, 'Unit price enabled', record.unit_price_enabled ?? record.unitPriceEnabled)\n appendLine(\n lines,\n 'Unit price reference unit',\n record.unit_price_reference_unit ?? record.unitPriceReferenceUnit,\n )\n appendLine(\n lines,\n 'Unit price base quantity',\n record.unit_price_base_quantity ?? record.unitPriceBaseQuantity,\n )\n return buildIndexSource(ctx, buildProductPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildProductPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => buildProductUrl(readRecordText(ctx.record, 'id')),\n fieldPolicy: {\n searchable: [\n 'title',\n 'subtitle',\n 'description',\n 'sku',\n 'handle',\n 'product_type',\n 'default_unit',\n 'default_sales_unit',\n 'unit_price_reference_unit',\n ],\n excluded: ['metadata', 'dimensions', 'tax_rate_id'],\n },\n },\n {\n entityId: 'catalog:catalog_product_variant',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 8,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'SKU', record.sku)\n appendLine(lines, 'Barcode', record.barcode)\n appendLine(lines, 'Option values', record.option_values ?? record.optionValues)\n return buildIndexSource(ctx, buildVariantPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildVariantPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n const variantId = readRecordText(ctx.record, 'id')\n return buildVariantUrl(productId, variantId)\n },\n fieldPolicy: {\n searchable: ['name', 'sku', 'barcode'],\n excluded: ['metadata', 'dimensions', 'option_values', 'tax_rate_id'],\n },\n },\n {\n entityId: 'catalog:catalog_product_category',\n aclFeatures: ['catalog.categories.view'],\n enabled: true,\n priority: 7,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Slug', record.slug)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'Path', record.tree_path ?? record.treePath)\n return buildIndexSource(ctx, buildCategoryPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildCategoryPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => buildCategoryUrl(readRecordText(ctx.record, 'id')),\n fieldPolicy: {\n searchable: ['name', 'slug', 'description', 'tree_path'],\n excluded: ['metadata', 'ancestor_ids', 'child_ids', 'descendant_ids'],\n },\n },\n {\n entityId: 'catalog:catalog_offer',\n aclFeatures: ['sales.channels.manage'],\n enabled: true,\n priority: 7,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'Channel', record.channel_id ?? record.channelId)\n return buildIndexSource(ctx, buildOfferPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildOfferPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n return buildProductUrl(productId)\n },\n fieldPolicy: {\n searchable: ['title', 'description'],\n excluded: ['metadata'],\n },\n },\n {\n entityId: 'catalog:catalog_product_tag',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 5,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Label', record.label)\n appendLine(lines, 'Slug', record.slug)\n return buildIndexSource(ctx, buildTagPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildTagPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_PRODUCTS_URL,\n fieldPolicy: {\n searchable: ['label', 'slug'],\n },\n },\n {\n entityId: 'catalog:catalog_price_kind',\n aclFeatures: ['catalog.settings.manage'],\n enabled: true,\n priority: 4,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Code', record.code)\n appendLine(lines, 'Display mode', record.display_mode ?? record.displayMode)\n appendLine(lines, 'Currency', record.currency_code ?? record.currencyCode)\n return buildIndexSource(ctx, buildPriceKindPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildPriceKindPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_CONFIG_URL,\n fieldPolicy: {\n searchable: ['title', 'code'],\n },\n },\n {\n entityId: 'catalog:catalog_product_unit_conversion',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 3,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Unit code', record.unit_code ?? record.unitCode)\n appendLine(lines, 'To base factor', record.to_base_factor ?? record.toBaseFactor)\n appendLine(lines, 'Sort order', record.sort_order ?? record.sortOrder)\n const isActive = record.is_active ?? record.isActive\n appendLine(lines, 'Active', isActive)\n return buildIndexSource(ctx, buildUnitConversionPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildUnitConversionPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n return buildProductUrl(productId)\n },\n fieldPolicy: {\n searchable: ['unit_code'],\n excluded: ['metadata'],\n },\n },\n {\n entityId: 'catalog:catalog_option_schema_template',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 4,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Code', record.code)\n appendLine(lines, 'Description', record.description)\n return buildIndexSource(ctx, buildOptionSchemaPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildOptionSchemaPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_CONFIG_URL,\n fieldPolicy: {\n searchable: ['name', 'code', 'description'],\n excluded: ['schema', 'metadata'],\n },\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
OAuthStateError,
|
|
14
14
|
verifyOAuthState
|
|
15
15
|
} from "../../../../../lib/oauth-state.js";
|
|
16
|
+
import { OAUTH_RESULT_QUERY_PARAM } from "../../../../../lib/oauth-result.js";
|
|
16
17
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
17
18
|
const logger = createLogger("communication_channels").child({ component: "oauth-callback" });
|
|
18
19
|
const metadata = {
|
|
@@ -26,7 +27,7 @@ function redirectWithFlash(req, returnUrl, flash) {
|
|
|
26
27
|
normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),
|
|
27
28
|
getAppBaseUrl(req)
|
|
28
29
|
);
|
|
29
|
-
base.searchParams.set(
|
|
30
|
+
base.searchParams.set(OAUTH_RESULT_QUERY_PARAM, flash.type);
|
|
30
31
|
if (flash.code) base.searchParams.set("code", flash.code);
|
|
31
32
|
if (flash.provider) base.searchParams.set("provider", flash.provider);
|
|
32
33
|
if (flash.channelId) base.searchParams.set("channelId", flash.channelId);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/communication_channels/api/get/oauth/%5Bprovider%5D/callback/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getAppBaseUrl, toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createConnectedChannelRow, MailboxAlreadyConnectedError } from '../../../../../lib/connect-channel'\nimport { getChannelAdapter } from '../../../../../lib/adapter-registry-singleton'\nimport { resolveOAuthClientCredentials } from '../../../../../lib/oauth-client-config'\nimport {\n COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,\n consumeOAuthStateOnce,\n DEFAULT_OAUTH_RETURN_URL,\n normalizeOAuthReturnUrl,\n OAuthStateError,\n verifyOAuthState,\n type OAuthStateConsumeStore,\n} from '../../../../../lib/oauth-state'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'oauth-callback' })\n\nexport const metadata = {\n path: '/communication_channels/oauth/[provider]/callback',\n // No auth feature gate \u2014 the state cookie carries identity. The route still\n // verifies the session below to bind the callback to its initiator.\n GET: { requireAuth: true },\n}\n\ntype RouteContext = {\n params: Promise<{ provider: string }> | { provider: string }\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\nfunction redirectWithFlash(\n req: Request,\n returnUrl: string,\n flash: { type: 'connected' | 'error'; code?: string; provider?: string; channelId?: string },\n): Response {\n const base = new URL(\n normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),\n getAppBaseUrl(req),\n )\n base.searchParams.set('flash', flash.type)\n if (flash.code) base.searchParams.set('code', flash.code)\n if (flash.provider) base.searchParams.set('provider', flash.provider)\n if (flash.channelId) base.searchParams.set('channelId', flash.channelId)\n const response = NextResponse.redirect(base.toString(), 302)\n response.cookies.set({\n name: COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,\n value: '',\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { provider } = await context.params\n const url = new URL(req.url)\n const code = url.searchParams.get('code') ?? ''\n const stateParam = url.searchParams.get('state') ?? ''\n const error = url.searchParams.get('error')\n\n if (error) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, { type: 'error', code: error, provider })\n }\n if (!code || !stateParam) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'missing_code_or_state',\n provider,\n })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'unauthorized',\n provider,\n })\n }\n\n const adapter = getChannelAdapter(provider)\n if (!adapter || typeof adapter.exchangeOAuthCode !== 'function') {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'unknown_provider',\n provider,\n })\n }\n\n const cookieValue = req.headers.get('cookie') ?? ''\n const stateCookie = parseCookie(cookieValue, COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME)\n\n let statePayload\n try {\n statePayload = verifyOAuthState({\n cookie: stateCookie,\n expectedUserId: auth.sub as string,\n expectedProviderKey: provider,\n expectedState: stateParam,\n })\n } catch (err) {\n const errCode = err instanceof OAuthStateError ? err.code : 'invalid_state'\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: errCode,\n provider,\n })\n }\n\n // Defense-in-depth: the signed state is minted for a specific tenant at\n // initiate time and `verifyOAuthState` already binds it to `auth.sub`. Assert\n // the session tenant matches too, so a token/session anomaly can never create\n // a channel under a different tenant than the one that started the flow.\n if (statePayload.tenantId !== auth.tenantId) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'tenant_mismatch',\n provider,\n })\n }\n\n const returnUrl = normalizeOAuthReturnUrl(statePayload.returnUrl, DEFAULT_OAUTH_RETURN_URL)\n\n // Exchange the code via the adapter.\n const container = await createRequestContainer()\n\n // Single-use consume: reject a captured valid cookie replayed within the TTL.\n const resolved = (() => { try { return container.resolve('cache') } catch { return null } })()\n if (\n !resolved ||\n typeof (resolved as OAuthStateConsumeStore).has !== 'function' ||\n typeof (resolved as OAuthStateConsumeStore).set !== 'function'\n ) {\n return redirectWithFlash(req, returnUrl, { type: 'error', code: 'state_store_unavailable', provider })\n }\n const oauthStateStore = resolved as OAuthStateConsumeStore\n try {\n await consumeOAuthStateOnce(oauthStateStore, statePayload)\n } catch (err) {\n const errCode = err instanceof OAuthStateError ? err.code : 'invalid_state'\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: errCode,\n provider,\n })\n }\n const credentialsService = (() => {\n try {\n return container.resolve('integrationCredentialsService') as CredentialsServiceLike\n } catch {\n return null\n }\n })()\n // Resolve the tenant's OAuth client app config from the `channel_<provider>`\n // integration (userId = null). Same source the `initiate` route uses; a\n // missing row means the provider was never configured, so bounce back with an\n // actionable code rather than attempting an exchange with empty credentials.\n const oauthClientCredentials = await resolveOAuthClientCredentials(credentialsService, provider, {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? null,\n })\n if (!oauthClientCredentials) {\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'oauth_client_not_configured',\n provider,\n })\n }\n\n // Must byte-for-byte match the redirect_uri sent at authorize time (the\n // `initiate` route), including behind a reverse proxy \u2014 derive it from the\n // configured app origin rather than the raw request URL.\n const redirectUri = toAbsoluteUrl(req, `/api/communication_channels/oauth/${provider}/callback`)\n\n let exchange\n try {\n exchange = await adapter.exchangeOAuthCode({\n code,\n redirectUri,\n credentials: oauthClientCredentials,\n scope: {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? statePayload.tenantId,\n },\n stateExtra: statePayload.extra,\n })\n } catch (err) {\n logger.warn(\n 'code exchange failed for provider',\n { provider, err },\n )\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'exchange_failed',\n provider,\n })\n }\n\n // Persist the encrypted credentials under a per-user `integration_credentials`\n // row, then create / update the per-user `CommunicationChannel`.\n const em = (container.resolve('em') as EntityManager).fork()\n // Per-user scope: pass `userId` so the credentials service writes to a\n // user-scoped row instead of overwriting the tenant-wide row. Without this,\n // two users on the same tenant share one credentials row (see review R2-C1\n // / N1, 2026-05-26).\n const credentialsScope = {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? statePayload.tenantId,\n userId: auth.sub as string,\n }\n let credentialsRefId: string | null = null\n let credentialsPersisted = false\n if (credentialsService?.save) {\n try {\n // Save under a per-provider integration id namespace; per-user scoping is\n // recorded on `IntegrationCredentials.user_id` via the `scope.userId`.\n // Arg order MUST be (integrationId, credentials, scope) \u2014 matches the real\n // CredentialsService signature; the legacy reversed order corrupted the\n // saved row by writing the scope object into the credentials field.\n await credentialsService.save(\n `channel_${provider}`,\n {\n ...exchange.credentials,\n userId: auth.sub,\n expiresAt: exchange.expiresAt ? exchange.expiresAt.toISOString() : undefined,\n },\n credentialsScope,\n )\n credentialsPersisted = true\n } catch (err) {\n logger.warn(\n 'persisting credentials failed for provider',\n { provider, err },\n )\n }\n }\n // Resolve the saved row's id so we can link `channel.credentialsRef` to it.\n // `credentialsService.save` is `void`-returning by contract, so we re-find the\n // row immediately afterwards. Best-effort \u2014 null is acceptable; the channel\n // creation below will downgrade to `requires_reauth` so workers don't poll\n // a credential-less channel.\n if (credentialsPersisted) {\n try {\n const { IntegrationCredentials } = await import('@open-mercato/core/modules/integrations/data/entities')\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId: `channel_${provider}`,\n tenantId: credentialsScope.tenantId,\n organizationId: credentialsScope.organizationId,\n userId: credentialsScope.userId,\n deletedAt: null,\n },\n undefined,\n credentialsScope,\n )\n credentialsRefId = (row as { id?: string } | null)?.id ?? null\n } catch {\n credentialsRefId = null\n }\n }\n\n const displayName =\n exchange.displayName ?? exchange.externalIdentifier ?? `${provider} channel`\n // Fail-safe: if credentials persistence failed (no `credentialsRef` available),\n // create the channel in `requires_reauth` so workers don't poll a channel that\n // has no usable credentials. The user can re-run the OAuth flow to recover\n // (see review R2-H4 / F6, 2026-05-26).\n const credentialsAvailable = credentialsRefId !== null\n let channel\n try {\n channel = await createConnectedChannelRow({\n em,\n adapter,\n providerKey: provider,\n displayName,\n externalIdentifier: exchange.externalIdentifier ?? null,\n credentialsRefId,\n userId: auth.sub as string,\n scope: { tenantId: statePayload.tenantId, organizationId: statePayload.organizationId ?? null },\n })\n } catch (err) {\n // Same mailbox already connected via another provider \u2014 don't create a second\n // channel that double-ingests every message; send the user back with a flash.\n if (err instanceof MailboxAlreadyConnectedError) {\n logger.warn('mailbox already connected via another provider', { existingProviderKey: err.existingProviderKey })\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'mailbox_already_connected',\n provider,\n })\n }\n throw err\n }\n\n // Spec C \u00A7 Phase C5 \u2014 best-effort push registration for OAuth providers\n // that support it (Gmail). Same shape as the credential-connect\n // path: failures persist as `pushStatus='failed'` on channelState and DO\n // NOT fail the connect \u2014 polling fallback covers until the operator clicks\n // \"Re-register push\" on the channels page.\n const adapterSupportsPush =\n typeof adapter.registerPush === 'function' && typeof adapter.unregisterPush === 'function'\n if (\n credentialsAvailable &&\n adapterSupportsPush &&\n statePayload.organizationId &&\n provider === 'gmail'\n ) {\n try {\n const { pushRegister } = await import('../../../../../commands/push-register')\n await pushRegister({\n container,\n scope: {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId,\n userId: auth.sub as string,\n },\n input: { channelId: channel.id },\n })\n } catch (err) {\n logger.warn(\n 'best-effort pushRegister failed for channel',\n { channelId: channel.id, err },\n )\n }\n }\n\n return redirectWithFlash(req, returnUrl, {\n type: 'connected',\n provider,\n channelId: channel.id,\n })\n}\n\nfunction parseCookie(header: string, name: string): string | null {\n if (!header) return null\n const segments = header.split(';')\n for (const segment of segments) {\n const trimmed = segment.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return decodeURIComponent(trimmed.slice(name.length + 1))\n }\n }\n return null\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n GET: {\n summary: 'OAuth callback \u2014 exchange code, persist credentials, create per-user channel',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 302, description: 'Redirect back to returnUrl with flash query params' },\n ],\n },\n },\n}\nexport default GET\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,eAAe,qBAAqB;AAC7C,SAAS,8BAA8B;AAEvC,SAAS,2BAA2B,oCAAoC;AACxE,SAAS,yBAAyB;AAClC,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,iBAAiB,CAAC;AAEpF,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA;AAAA;AAAA,EAGN,KAAK,EAAE,aAAa,KAAK;AAC3B;AAkBA,SAAS,kBACP,KACA,WACA,OACU;AACV,QAAM,OAAO,IAAI;AAAA,IACf,wBAAwB,WAAW,wBAAwB;AAAA,IAC3D,cAAc,GAAG;AAAA,EACnB;AACA,OAAK,aAAa,IAAI,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getAppBaseUrl, toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createConnectedChannelRow, MailboxAlreadyConnectedError } from '../../../../../lib/connect-channel'\nimport { getChannelAdapter } from '../../../../../lib/adapter-registry-singleton'\nimport { resolveOAuthClientCredentials } from '../../../../../lib/oauth-client-config'\nimport {\n COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,\n consumeOAuthStateOnce,\n DEFAULT_OAUTH_RETURN_URL,\n normalizeOAuthReturnUrl,\n OAuthStateError,\n verifyOAuthState,\n type OAuthStateConsumeStore,\n} from '../../../../../lib/oauth-state'\nimport { OAUTH_RESULT_QUERY_PARAM } from '../../../../../lib/oauth-result'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'oauth-callback' })\n\nexport const metadata = {\n path: '/communication_channels/oauth/[provider]/callback',\n // No auth feature gate \u2014 the state cookie carries identity. The route still\n // verifies the session below to bind the callback to its initiator.\n GET: { requireAuth: true },\n}\n\ntype RouteContext = {\n params: Promise<{ provider: string }> | { provider: string }\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\nfunction redirectWithFlash(\n req: Request,\n returnUrl: string,\n flash: { type: 'connected' | 'error'; code?: string; provider?: string; channelId?: string },\n): Response {\n const base = new URL(\n normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),\n getAppBaseUrl(req),\n )\n base.searchParams.set(OAUTH_RESULT_QUERY_PARAM, flash.type)\n if (flash.code) base.searchParams.set('code', flash.code)\n if (flash.provider) base.searchParams.set('provider', flash.provider)\n if (flash.channelId) base.searchParams.set('channelId', flash.channelId)\n const response = NextResponse.redirect(base.toString(), 302)\n response.cookies.set({\n name: COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,\n value: '',\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { provider } = await context.params\n const url = new URL(req.url)\n const code = url.searchParams.get('code') ?? ''\n const stateParam = url.searchParams.get('state') ?? ''\n const error = url.searchParams.get('error')\n\n if (error) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, { type: 'error', code: error, provider })\n }\n if (!code || !stateParam) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'missing_code_or_state',\n provider,\n })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'unauthorized',\n provider,\n })\n }\n\n const adapter = getChannelAdapter(provider)\n if (!adapter || typeof adapter.exchangeOAuthCode !== 'function') {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'unknown_provider',\n provider,\n })\n }\n\n const cookieValue = req.headers.get('cookie') ?? ''\n const stateCookie = parseCookie(cookieValue, COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME)\n\n let statePayload\n try {\n statePayload = verifyOAuthState({\n cookie: stateCookie,\n expectedUserId: auth.sub as string,\n expectedProviderKey: provider,\n expectedState: stateParam,\n })\n } catch (err) {\n const errCode = err instanceof OAuthStateError ? err.code : 'invalid_state'\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: errCode,\n provider,\n })\n }\n\n // Defense-in-depth: the signed state is minted for a specific tenant at\n // initiate time and `verifyOAuthState` already binds it to `auth.sub`. Assert\n // the session tenant matches too, so a token/session anomaly can never create\n // a channel under a different tenant than the one that started the flow.\n if (statePayload.tenantId !== auth.tenantId) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'tenant_mismatch',\n provider,\n })\n }\n\n const returnUrl = normalizeOAuthReturnUrl(statePayload.returnUrl, DEFAULT_OAUTH_RETURN_URL)\n\n // Exchange the code via the adapter.\n const container = await createRequestContainer()\n\n // Single-use consume: reject a captured valid cookie replayed within the TTL.\n const resolved = (() => { try { return container.resolve('cache') } catch { return null } })()\n if (\n !resolved ||\n typeof (resolved as OAuthStateConsumeStore).has !== 'function' ||\n typeof (resolved as OAuthStateConsumeStore).set !== 'function'\n ) {\n return redirectWithFlash(req, returnUrl, { type: 'error', code: 'state_store_unavailable', provider })\n }\n const oauthStateStore = resolved as OAuthStateConsumeStore\n try {\n await consumeOAuthStateOnce(oauthStateStore, statePayload)\n } catch (err) {\n const errCode = err instanceof OAuthStateError ? err.code : 'invalid_state'\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: errCode,\n provider,\n })\n }\n const credentialsService = (() => {\n try {\n return container.resolve('integrationCredentialsService') as CredentialsServiceLike\n } catch {\n return null\n }\n })()\n // Resolve the tenant's OAuth client app config from the `channel_<provider>`\n // integration (userId = null). Same source the `initiate` route uses; a\n // missing row means the provider was never configured, so bounce back with an\n // actionable code rather than attempting an exchange with empty credentials.\n const oauthClientCredentials = await resolveOAuthClientCredentials(credentialsService, provider, {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? null,\n })\n if (!oauthClientCredentials) {\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'oauth_client_not_configured',\n provider,\n })\n }\n\n // Must byte-for-byte match the redirect_uri sent at authorize time (the\n // `initiate` route), including behind a reverse proxy \u2014 derive it from the\n // configured app origin rather than the raw request URL.\n const redirectUri = toAbsoluteUrl(req, `/api/communication_channels/oauth/${provider}/callback`)\n\n let exchange\n try {\n exchange = await adapter.exchangeOAuthCode({\n code,\n redirectUri,\n credentials: oauthClientCredentials,\n scope: {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? statePayload.tenantId,\n },\n stateExtra: statePayload.extra,\n })\n } catch (err) {\n logger.warn(\n 'code exchange failed for provider',\n { provider, err },\n )\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'exchange_failed',\n provider,\n })\n }\n\n // Persist the encrypted credentials under a per-user `integration_credentials`\n // row, then create / update the per-user `CommunicationChannel`.\n const em = (container.resolve('em') as EntityManager).fork()\n // Per-user scope: pass `userId` so the credentials service writes to a\n // user-scoped row instead of overwriting the tenant-wide row. Without this,\n // two users on the same tenant share one credentials row (see review R2-C1\n // / N1, 2026-05-26).\n const credentialsScope = {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? statePayload.tenantId,\n userId: auth.sub as string,\n }\n let credentialsRefId: string | null = null\n let credentialsPersisted = false\n if (credentialsService?.save) {\n try {\n // Save under a per-provider integration id namespace; per-user scoping is\n // recorded on `IntegrationCredentials.user_id` via the `scope.userId`.\n // Arg order MUST be (integrationId, credentials, scope) \u2014 matches the real\n // CredentialsService signature; the legacy reversed order corrupted the\n // saved row by writing the scope object into the credentials field.\n await credentialsService.save(\n `channel_${provider}`,\n {\n ...exchange.credentials,\n userId: auth.sub,\n expiresAt: exchange.expiresAt ? exchange.expiresAt.toISOString() : undefined,\n },\n credentialsScope,\n )\n credentialsPersisted = true\n } catch (err) {\n logger.warn(\n 'persisting credentials failed for provider',\n { provider, err },\n )\n }\n }\n // Resolve the saved row's id so we can link `channel.credentialsRef` to it.\n // `credentialsService.save` is `void`-returning by contract, so we re-find the\n // row immediately afterwards. Best-effort \u2014 null is acceptable; the channel\n // creation below will downgrade to `requires_reauth` so workers don't poll\n // a credential-less channel.\n if (credentialsPersisted) {\n try {\n const { IntegrationCredentials } = await import('@open-mercato/core/modules/integrations/data/entities')\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId: `channel_${provider}`,\n tenantId: credentialsScope.tenantId,\n organizationId: credentialsScope.organizationId,\n userId: credentialsScope.userId,\n deletedAt: null,\n },\n undefined,\n credentialsScope,\n )\n credentialsRefId = (row as { id?: string } | null)?.id ?? null\n } catch {\n credentialsRefId = null\n }\n }\n\n const displayName =\n exchange.displayName ?? exchange.externalIdentifier ?? `${provider} channel`\n // Fail-safe: if credentials persistence failed (no `credentialsRef` available),\n // create the channel in `requires_reauth` so workers don't poll a channel that\n // has no usable credentials. The user can re-run the OAuth flow to recover\n // (see review R2-H4 / F6, 2026-05-26).\n const credentialsAvailable = credentialsRefId !== null\n let channel\n try {\n channel = await createConnectedChannelRow({\n em,\n adapter,\n providerKey: provider,\n displayName,\n externalIdentifier: exchange.externalIdentifier ?? null,\n credentialsRefId,\n userId: auth.sub as string,\n scope: { tenantId: statePayload.tenantId, organizationId: statePayload.organizationId ?? null },\n })\n } catch (err) {\n // Same mailbox already connected via another provider \u2014 don't create a second\n // channel that double-ingests every message; send the user back with a flash.\n if (err instanceof MailboxAlreadyConnectedError) {\n logger.warn('mailbox already connected via another provider', { existingProviderKey: err.existingProviderKey })\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'mailbox_already_connected',\n provider,\n })\n }\n throw err\n }\n\n // Spec C \u00A7 Phase C5 \u2014 best-effort push registration for OAuth providers\n // that support it (Gmail). Same shape as the credential-connect\n // path: failures persist as `pushStatus='failed'` on channelState and DO\n // NOT fail the connect \u2014 polling fallback covers until the operator clicks\n // \"Re-register push\" on the channels page.\n const adapterSupportsPush =\n typeof adapter.registerPush === 'function' && typeof adapter.unregisterPush === 'function'\n if (\n credentialsAvailable &&\n adapterSupportsPush &&\n statePayload.organizationId &&\n provider === 'gmail'\n ) {\n try {\n const { pushRegister } = await import('../../../../../commands/push-register')\n await pushRegister({\n container,\n scope: {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId,\n userId: auth.sub as string,\n },\n input: { channelId: channel.id },\n })\n } catch (err) {\n logger.warn(\n 'best-effort pushRegister failed for channel',\n { channelId: channel.id, err },\n )\n }\n }\n\n return redirectWithFlash(req, returnUrl, {\n type: 'connected',\n provider,\n channelId: channel.id,\n })\n}\n\nfunction parseCookie(header: string, name: string): string | null {\n if (!header) return null\n const segments = header.split(';')\n for (const segment of segments) {\n const trimmed = segment.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return decodeURIComponent(trimmed.slice(name.length + 1))\n }\n }\n return null\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n GET: {\n summary: 'OAuth callback \u2014 exchange code, persist credentials, create per-user channel',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 302, description: 'Redirect back to returnUrl with flash query params' },\n ],\n },\n },\n}\nexport default GET\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,eAAe,qBAAqB;AAC7C,SAAS,8BAA8B;AAEvC,SAAS,2BAA2B,oCAAoC;AACxE,SAAS,yBAAyB;AAClC,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,gCAAgC;AACzC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,iBAAiB,CAAC;AAEpF,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA;AAAA;AAAA,EAGN,KAAK,EAAE,aAAa,KAAK;AAC3B;AAkBA,SAAS,kBACP,KACA,WACA,OACU;AACV,QAAM,OAAO,IAAI;AAAA,IACf,wBAAwB,WAAW,wBAAwB;AAAA,IAC3D,cAAc,GAAG;AAAA,EACnB;AACA,OAAK,aAAa,IAAI,0BAA0B,MAAM,IAAI;AAC1D,MAAI,MAAM,KAAM,MAAK,aAAa,IAAI,QAAQ,MAAM,IAAI;AACxD,MAAI,MAAM,SAAU,MAAK,aAAa,IAAI,YAAY,MAAM,QAAQ;AACpE,MAAI,MAAM,UAAW,MAAK,aAAa,IAAI,aAAa,MAAM,SAAS;AACvE,QAAM,WAAW,aAAa,SAAS,KAAK,SAAS,GAAG,GAAG;AAC3D,WAAS,QAAQ,IAAI;AAAA,IACnB,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,IACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,EACV,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,IAAI,KAAc,SAA0C;AAChF,QAAM,EAAE,SAAS,IAAI,MAAM,QAAQ;AACnC,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,OAAO,IAAI,aAAa,IAAI,MAAM,KAAK;AAC7C,QAAM,aAAa,IAAI,aAAa,IAAI,OAAO,KAAK;AACpD,QAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAE1C,MAAI,OAAO;AACT,WAAO,kBAAkB,KAAK,0BAA0B,EAAE,MAAM,SAAS,MAAM,OAAO,SAAS,CAAC;AAAA,EAClG;AACA,MAAI,CAAC,QAAQ,CAAC,YAAY;AACxB,WAAO,kBAAkB,KAAK,0BAA0B;AAAA,MACtD,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,kBAAkB,KAAK,0BAA0B;AAAA,MACtD,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,UAAU,kBAAkB,QAAQ;AAC1C,MAAI,CAAC,WAAW,OAAO,QAAQ,sBAAsB,YAAY;AAC/D,WAAO,kBAAkB,KAAK,0BAA0B;AAAA,MACtD,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,cAAc,IAAI,QAAQ,IAAI,QAAQ,KAAK;AACjD,QAAM,cAAc,YAAY,aAAa,8CAA8C;AAE3F,MAAI;AACJ,MAAI;AACF,mBAAe,iBAAiB;AAAA,MAC9B,QAAQ;AAAA,MACR,gBAAgB,KAAK;AAAA,MACrB,qBAAqB;AAAA,MACrB,eAAe;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,kBAAkB,IAAI,OAAO;AAC5D,WAAO,kBAAkB,KAAK,0BAA0B;AAAA,MACtD,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AAMA,MAAI,aAAa,aAAa,KAAK,UAAU;AAC3C,WAAO,kBAAkB,KAAK,0BAA0B;AAAA,MACtD,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,YAAY,wBAAwB,aAAa,WAAW,wBAAwB;AAG1F,QAAM,YAAY,MAAM,uBAAuB;AAG/C,QAAM,YAAY,MAAM;AAAE,QAAI;AAAE,aAAO,UAAU,QAAQ,OAAO;AAAA,IAAE,QAAQ;AAAE,aAAO;AAAA,IAAK;AAAA,EAAE,GAAG;AAC7F,MACE,CAAC,YACD,OAAQ,SAAoC,QAAQ,cACpD,OAAQ,SAAoC,QAAQ,YACpD;AACA,WAAO,kBAAkB,KAAK,WAAW,EAAE,MAAM,SAAS,MAAM,2BAA2B,SAAS,CAAC;AAAA,EACvG;AACA,QAAM,kBAAkB;AACxB,MAAI;AACF,UAAM,sBAAsB,iBAAiB,YAAY;AAAA,EAC3D,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,kBAAkB,IAAI,OAAO;AAC5D,WAAO,kBAAkB,KAAK,WAAW;AAAA,MACvC,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AACA,QAAM,sBAAsB,MAAM;AAChC,QAAI;AACF,aAAO,UAAU,QAAQ,+BAA+B;AAAA,IAC1D,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAKH,QAAM,yBAAyB,MAAM,8BAA8B,oBAAoB,UAAU;AAAA,IAC/F,UAAU,aAAa;AAAA,IACvB,gBAAgB,aAAa,kBAAkB;AAAA,EACjD,CAAC;AACD,MAAI,CAAC,wBAAwB;AAC3B,WAAO,kBAAkB,KAAK,WAAW;AAAA,MACvC,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AAKA,QAAM,cAAc,cAAc,KAAK,qCAAqC,QAAQ,WAAW;AAE/F,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,QAAQ,kBAAkB;AAAA,MACzC;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb,OAAO;AAAA,QACL,UAAU,aAAa;AAAA,QACvB,gBAAgB,aAAa,kBAAkB,aAAa;AAAA,MAC9D;AAAA,MACA,YAAY,aAAa;AAAA,IAC3B,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO;AAAA,MACL;AAAA,MACA,EAAE,UAAU,IAAI;AAAA,IAClB;AACA,WAAO,kBAAkB,KAAK,WAAW;AAAA,MACvC,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH;AAIA,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAK3D,QAAM,mBAAmB;AAAA,IACvB,UAAU,aAAa;AAAA,IACvB,gBAAgB,aAAa,kBAAkB,aAAa;AAAA,IAC5D,QAAQ,KAAK;AAAA,EACf;AACA,MAAI,mBAAkC;AACtC,MAAI,uBAAuB;AAC3B,MAAI,oBAAoB,MAAM;AAC5B,QAAI;AAMF,YAAM,mBAAmB;AAAA,QACvB,WAAW,QAAQ;AAAA,QACnB;AAAA,UACE,GAAG,SAAS;AAAA,UACZ,QAAQ,KAAK;AAAA,UACb,WAAW,SAAS,YAAY,SAAS,UAAU,YAAY,IAAI;AAAA,QACrE;AAAA,QACA;AAAA,MACF;AACA,6BAAuB;AAAA,IACzB,SAAS,KAAK;AACZ,aAAO;AAAA,QACL;AAAA,QACA,EAAE,UAAU,IAAI;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAMA,MAAI,sBAAsB;AACxB,QAAI;AACF,YAAM,EAAE,uBAAuB,IAAI,MAAM,OAAO,uDAAuD;AACvG,YAAM,EAAE,sBAAsB,IAAI,MAAM,OAAO,0CAA0C;AACzF,YAAM,MAAM,MAAM;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,UACE,eAAe,WAAW,QAAQ;AAAA,UAClC,UAAU,iBAAiB;AAAA,UAC3B,gBAAgB,iBAAiB;AAAA,UACjC,QAAQ,iBAAiB;AAAA,UACzB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,yBAAoB,KAAgC,MAAM;AAAA,IAC5D,QAAQ;AACN,yBAAmB;AAAA,IACrB;AAAA,EACF;AAEA,QAAM,cACJ,SAAS,eAAe,SAAS,sBAAsB,GAAG,QAAQ;AAKpE,QAAM,uBAAuB,qBAAqB;AAClD,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,0BAA0B;AAAA,MACxC;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb;AAAA,MACA,oBAAoB,SAAS,sBAAsB;AAAA,MACnD;AAAA,MACA,QAAQ,KAAK;AAAA,MACb,OAAO,EAAE,UAAU,aAAa,UAAU,gBAAgB,aAAa,kBAAkB,KAAK;AAAA,IAChG,CAAC;AAAA,EACH,SAAS,KAAK;AAGZ,QAAI,eAAe,8BAA8B;AAC/C,aAAO,KAAK,kDAAkD,EAAE,qBAAqB,IAAI,oBAAoB,CAAC;AAC9G,aAAO,kBAAkB,KAAK,WAAW;AAAA,QACvC,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,MACF,CAAC;AAAA,IACH;AACA,UAAM;AAAA,EACR;AAOA,QAAM,sBACJ,OAAO,QAAQ,iBAAiB,cAAc,OAAO,QAAQ,mBAAmB;AAClF,MACE,wBACA,uBACA,aAAa,kBACb,aAAa,SACb;AACA,QAAI;AACF,YAAM,EAAE,aAAa,IAAI,MAAM,OAAO,uCAAuC;AAC7E,YAAM,aAAa;AAAA,QACjB;AAAA,QACA,OAAO;AAAA,UACL,UAAU,aAAa;AAAA,UACvB,gBAAgB,aAAa;AAAA,UAC7B,QAAQ,KAAK;AAAA,QACf;AAAA,QACA,OAAO,EAAE,WAAW,QAAQ,GAAG;AAAA,MACjC,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,aAAO;AAAA,QACL;AAAA,QACA,EAAE,WAAW,QAAQ,IAAI,IAAI;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAEA,SAAO,kBAAkB,KAAK,WAAW;AAAA,IACvC,MAAM;AAAA,IACN;AAAA,IACA,WAAW,QAAQ;AAAA,EACrB,CAAC;AACH;AAEA,SAAS,YAAY,QAAgB,MAA6B;AAChE,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,WAAW,OAAO,MAAM,GAAG;AACjC,aAAW,WAAW,UAAU;AAC9B,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,GAAG,IAAI,GAAG,GAAG;AAClC,aAAO,mBAAmB,QAAQ,MAAM,KAAK,SAAS,CAAC,CAAC;AAAA,IAC1D;AAAA,EACF;AACA,SAAO;AACT;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,uBAAuB;AAAA,EAC9B,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,MAAM,CAAC,uBAAuB;AAAA,MAC9B,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,qDAAqD;AAAA,MACnF;AAAA,IACF;AAAA,EACF;AACF;AACA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -25,16 +25,14 @@ async function POST(req) {
|
|
|
25
25
|
if (!auth?.sub || !auth?.tenantId) {
|
|
26
26
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const json = await readJsonSafe(req, null);
|
|
31
|
-
body = bodySchema.parse(json);
|
|
32
|
-
} catch (err) {
|
|
28
|
+
const parsed = bodySchema.safeParse(await readJsonSafe(req, null));
|
|
29
|
+
if (!parsed.success) {
|
|
33
30
|
return NextResponse.json(
|
|
34
|
-
{ error:
|
|
31
|
+
{ error: "Invalid request body", details: parsed.error.issues },
|
|
35
32
|
{ status: 422 }
|
|
36
33
|
);
|
|
37
34
|
}
|
|
35
|
+
const body = parsed.data;
|
|
38
36
|
const container = await createRequestContainer();
|
|
39
37
|
const adapterRegistry = container.resolve("channelAdapterRegistry");
|
|
40
38
|
const earlyAdapter = adapterRegistry.get(body.providerKey);
|
package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/communication_channels/api/post/channels/connect/credentials/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../../../commands/connect-credential-channel'\nimport type { ChannelAdapterRegistry } from '../../../../../lib/registry'\nimport { validateRouteMutationGuard } from '../../../../../lib/route-mutation-guard'\n\nexport const metadata = {\n path: '/communication_channels/channels/connect/credentials',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_user_channel'],\n },\n}\n\nconst bodySchema = z.object({\n providerKey: z.string().min(1).max(64),\n displayName: z.string().min(1).max(255),\n credentials: z.record(z.string(), z.unknown()),\n pollIntervalSeconds: z.number().int().positive().max(86_400).optional(),\n})\n\nexport async function POST(req: Request): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,OAGK;AAEP,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,iBAAiB,CAAC,6CAA6C;AAAA,EACjE;AACF;AAEA,MAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EACrC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACtC,aAAa,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EAC7C,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,KAAM,EAAE,SAAS;AACxE,CAAC;AAED,eAAsB,KAAK,KAAiC;AAC1D,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../../../commands/connect-credential-channel'\nimport type { ChannelAdapterRegistry } from '../../../../../lib/registry'\nimport { validateRouteMutationGuard } from '../../../../../lib/route-mutation-guard'\n\nexport const metadata = {\n path: '/communication_channels/channels/connect/credentials',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_user_channel'],\n },\n}\n\nconst bodySchema = z.object({\n providerKey: z.string().min(1).max(64),\n displayName: z.string().min(1).max(255),\n credentials: z.record(z.string(), z.unknown()),\n pollIntervalSeconds: z.number().int().positive().max(86_400).optional(),\n})\n\nexport async function POST(req: Request): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const parsed = bodySchema.safeParse(await readJsonSafe(req, null))\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request body', details: parsed.error.issues },\n { status: 422 },\n )\n }\n const body = parsed.data\n\n const container = await createRequestContainer()\n\n // Tenant-scoped providers (push: FCM/APNs/Expo) must go through the admin-gated\n // tenant route, not this per-user one (granted to manager/employee via\n // `connect_user_channel`). Reject them here so a non-admin can't mint a\n // privileged tenant-wide channel. The command enforces the same invariant\n // (`wrong_scope_for_route`) as defense-in-depth.\n const adapterRegistry = container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const earlyAdapter = adapterRegistry.get(body.providerKey)\n if (earlyAdapter && earlyAdapter.channelScope === 'tenant') {\n return NextResponse.json(\n {\n error: 'This provider is connected tenant-wide by an administrator, not per-user.',\n code: 'provider_is_tenant_scoped',\n },\n { status: 403 },\n )\n }\n\n const guard = await validateRouteMutationGuard({\n container,\n req,\n auth,\n input: {\n resourceKind: 'communication_channels.channel',\n resourceId: `new:${body.providerKey}`,\n operation: 'create',\n mutationPayload: {\n providerKey: body.providerKey,\n displayName: body.displayName,\n pollIntervalSeconds: body.pollIntervalSeconds,\n },\n },\n })\n if ('response' in guard) return guard.response\n\n const commandBus = container.resolve('commandBus') as CommandBus\n\n const input: ConnectCredentialChannelInput = {\n providerKey: body.providerKey,\n displayName: body.displayName,\n credentials: body.credentials,\n pollIntervalSeconds: body.pollIntervalSeconds,\n userId: auth.sub as string,\n scope: {\n tenantId: auth.tenantId as string,\n organizationId: (auth as { orgId?: string | null }).orgId ?? null,\n },\n }\n const { result } = await commandBus.execute<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n >(COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID, {\n input,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: (auth as { orgId?: string | null }).orgId ?? null,\n organizationIds: (auth as { orgId?: string | null }).orgId\n ? [(auth as { orgId?: string | null }).orgId!]\n : null,\n },\n })\n\n if (result.status === 'no_adapter') {\n return NextResponse.json({ error: result.reason }, { status: 404 })\n }\n if (result.status === 'wrong_scope_for_route') {\n return NextResponse.json(\n {\n error: 'This provider is connected tenant-wide by an administrator, not per-user.',\n code: 'provider_is_tenant_scoped',\n },\n { status: 403 },\n )\n }\n if (result.status === 'validation_failed') {\n return NextResponse.json(\n {\n error: 'Credential validation failed',\n fieldErrors: result.errors,\n ...(result.errorCodes ? { fieldErrorCodes: result.errorCodes } : {}),\n },\n { status: 422 },\n )\n }\n if (result.status === 'duplicate_mailbox') {\n return NextResponse.json(\n {\n error: `This mailbox is already connected via \"${result.existingProviderKey}\". Disconnect it there before connecting it again with a different provider.`,\n code: 'mailbox_already_connected',\n },\n { status: 409 },\n )\n }\n await guard.afterSuccess()\n return NextResponse.json(\n {\n channelId: result.channelId,\n externalIdentifier: result.externalIdentifier,\n },\n { status: 201 },\n )\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary: 'Connect a credential-based per-user channel (IMAP/SMTP)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 201, description: 'Channel connected' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Provider is tenant-scoped \u2014 use the tenant connect route' },\n { status: 404, description: 'Unknown provider' },\n { status: 409, description: 'Mailbox already connected via another provider' },\n { status: 422, description: 'Invalid body or credential validation failed' },\n ],\n },\n },\n}\nexport default POST\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js
CHANGED
|
@@ -25,16 +25,14 @@ async function POST(req) {
|
|
|
25
25
|
if (!auth?.sub || !auth?.tenantId) {
|
|
26
26
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const json = await readJsonSafe(req, null);
|
|
31
|
-
body = bodySchema.parse(json);
|
|
32
|
-
} catch (err) {
|
|
28
|
+
const parsed = bodySchema.safeParse(await readJsonSafe(req, null));
|
|
29
|
+
if (!parsed.success) {
|
|
33
30
|
return NextResponse.json(
|
|
34
|
-
{ error:
|
|
31
|
+
{ error: "Invalid request body", details: parsed.error.issues },
|
|
35
32
|
{ status: 422 }
|
|
36
33
|
);
|
|
37
34
|
}
|
|
35
|
+
const body = parsed.data;
|
|
38
36
|
const container = await createRequestContainer();
|
|
39
37
|
const adapterRegistry = container.resolve("channelAdapterRegistry");
|
|
40
38
|
const adapter = adapterRegistry.get(body.providerKey);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../../../commands/connect-credential-channel'\nimport type { ChannelAdapterRegistry } from '../../../../../lib/registry'\nimport { validateRouteMutationGuard } from '../../../../../lib/route-mutation-guard'\n\nexport const metadata = {\n path: '/communication_channels/channels/connect/tenant-credentials',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_tenant_channel'],\n },\n}\n\nconst bodySchema = z.object({\n providerKey: z.string().min(1).max(64),\n displayName: z.string().min(1).max(255),\n credentials: z.record(z.string(), z.unknown()),\n pollIntervalSeconds: z.number().int().positive().max(86_400).optional(),\n})\n\nexport async function POST(req: Request): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,OAGK;AAEP,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,iBAAiB,CAAC,+CAA+C;AAAA,EACnE;AACF;AAEA,MAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EACrC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACtC,aAAa,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EAC7C,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,KAAM,EAAE,SAAS;AACxE,CAAC;AAED,eAAsB,KAAK,KAAiC;AAC1D,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../../../commands/connect-credential-channel'\nimport type { ChannelAdapterRegistry } from '../../../../../lib/registry'\nimport { validateRouteMutationGuard } from '../../../../../lib/route-mutation-guard'\n\nexport const metadata = {\n path: '/communication_channels/channels/connect/tenant-credentials',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_tenant_channel'],\n },\n}\n\nconst bodySchema = z.object({\n providerKey: z.string().min(1).max(64),\n displayName: z.string().min(1).max(255),\n credentials: z.record(z.string(), z.unknown()),\n pollIntervalSeconds: z.number().int().positive().max(86_400).optional(),\n})\n\nexport async function POST(req: Request): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const parsed = bodySchema.safeParse(await readJsonSafe(req, null))\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request body', details: parsed.error.issues },\n { status: 422 },\n )\n }\n const body = parsed.data\n\n const container = await createRequestContainer()\n\n // Reject providers that are NOT tenant-scoped so an admin can't force a\n // per-user provider (Gmail/IMAP) into a shared tenant-wide channel. Push\n // providers (FCM/APNs/Expo) declare `channelScope: 'tenant'` on their adapter.\n const adapterRegistry = container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const adapter = adapterRegistry.get(body.providerKey)\n if (!adapter) {\n return NextResponse.json(\n { error: `No adapter registered for provider '${body.providerKey}'` },\n { status: 404 },\n )\n }\n if (adapter.channelScope !== 'tenant') {\n return NextResponse.json(\n {\n error: 'This provider connects per-user channels; use the personal channel connect flow instead.',\n code: 'provider_not_tenant_scoped',\n },\n { status: 400 },\n )\n }\n\n const guard = await validateRouteMutationGuard({\n container,\n req,\n auth,\n input: {\n resourceKind: 'communication_channels.channel',\n resourceId: `new:${body.providerKey}`,\n operation: 'create',\n mutationPayload: {\n providerKey: body.providerKey,\n displayName: body.displayName,\n pollIntervalSeconds: body.pollIntervalSeconds,\n },\n },\n })\n if ('response' in guard) return guard.response\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const orgId = (auth as { orgId?: string | null }).orgId ?? null\n\n const input: ConnectCredentialChannelInput = {\n providerKey: body.providerKey,\n displayName: body.displayName,\n credentials: body.credentials,\n pollIntervalSeconds: body.pollIntervalSeconds,\n // Tenant-wide: no owning user. The command re-derives the effective scope\n // from the adapter, so the channel + credentials are stored with user_id NULL.\n userId: null,\n scope: {\n tenantId: auth.tenantId as string,\n organizationId: orgId,\n },\n }\n const { result } = await commandBus.execute<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n >(COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID, {\n input,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: orgId,\n organizationIds: orgId ? [orgId] : null,\n },\n })\n\n if (result.status === 'no_adapter') {\n return NextResponse.json({ error: result.reason }, { status: 404 })\n }\n if (result.status === 'wrong_scope_for_route') {\n // Unreachable in practice \u2014 this route always dispatches with userId: null \u2014\n // but handle it so the result union narrows and the invariant is explicit.\n return NextResponse.json(\n { error: 'Unexpected per-user scope on the tenant connect route.', code: 'wrong_scope_for_route' },\n { status: 500 },\n )\n }\n if (result.status === 'validation_failed') {\n // `fieldErrors` stays English for API consumers and logs; `fieldErrorCodes`\n // lets the connect widget render a localized message per field.\n return NextResponse.json(\n {\n error: 'Credential validation failed',\n fieldErrors: result.errors,\n ...(result.errorCodes ? { fieldErrorCodes: result.errorCodes } : {}),\n },\n { status: 422 },\n )\n }\n if (result.status === 'duplicate_mailbox') {\n return NextResponse.json(\n {\n error: `This mailbox is already connected via \"${result.existingProviderKey}\". Disconnect it there before connecting it again with a different provider.`,\n code: 'mailbox_already_connected',\n },\n { status: 409 },\n )\n }\n await guard.afterSuccess()\n return NextResponse.json(\n {\n channelId: result.channelId,\n externalIdentifier: result.externalIdentifier,\n },\n { status: 201 },\n )\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary: 'Connect a tenant-wide credential-based channel (push: FCM/APNs/Expo)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 201, description: 'Channel connected' },\n { status: 400, description: 'Provider is not tenant-scoped' },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Unknown provider' },\n { status: 422, description: 'Invalid body or credential validation failed' },\n ],\n },\n },\n}\nexport default POST\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,OAGK;AAEP,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,iBAAiB,CAAC,+CAA+C;AAAA,EACnE;AACF;AAEA,MAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EACrC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACtC,aAAa,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EAC7C,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,KAAM,EAAE,SAAS;AACxE,CAAC;AAED,eAAsB,KAAK,KAAiC;AAC1D,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,SAAS,WAAW,UAAU,MAAM,aAAa,KAAK,IAAI,CAAC;AACjE,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,wBAAwB,SAAS,OAAO,MAAM,OAAO;AAAA,MAC9D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,QAAM,OAAO,OAAO;AAEpB,QAAM,YAAY,MAAM,uBAAuB;AAK/C,QAAM,kBAAkB,UAAU,QAAQ,wBAAwB;AAClE,QAAM,UAAU,gBAAgB,IAAI,KAAK,WAAW;AACpD,MAAI,CAAC,SAAS;AACZ,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,uCAAuC,KAAK,WAAW,IAAI;AAAA,MACpE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,MAAI,QAAQ,iBAAiB,UAAU;AACrC,WAAO,aAAa;AAAA,MAClB;AAAA,QACE,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,2BAA2B;AAAA,IAC7C;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,MACL,cAAc;AAAA,MACd,YAAY,OAAO,KAAK,WAAW;AAAA,MACnC,WAAW;AAAA,MACX,iBAAiB;AAAA,QACf,aAAa,KAAK;AAAA,QAClB,aAAa,KAAK;AAAA,QAClB,qBAAqB,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA,EACF,CAAC;AACD,MAAI,cAAc,MAAO,QAAO,MAAM;AAEtC,QAAM,aAAa,UAAU,QAAQ,YAAY;AACjD,QAAM,QAAS,KAAmC,SAAS;AAE3D,QAAM,QAAuC;AAAA,IAC3C,aAAa,KAAK;AAAA,IAClB,aAAa,KAAK;AAAA,IAClB,aAAa,KAAK;AAAA,IAClB,qBAAqB,KAAK;AAAA;AAAA;AAAA,IAG1B,QAAQ;AAAA,IACR,OAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,gBAAgB;AAAA,IAClB;AAAA,EACF;AACA,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW,QAGlC,8DAA8D;AAAA,IAC9D;AAAA,IACA,KAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB,wBAAwB;AAAA,MACxB,iBAAiB,QAAQ,CAAC,KAAK,IAAI;AAAA,IACrC;AAAA,EACF,CAAC;AAED,MAAI,OAAO,WAAW,cAAc;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,OAAO,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpE;AACA,MAAI,OAAO,WAAW,yBAAyB;AAG7C,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,0DAA0D,MAAM,wBAAwB;AAAA,MACjG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,MAAI,OAAO,WAAW,qBAAqB;AAGzC,WAAO,aAAa;AAAA,MAClB;AAAA,QACE,OAAO;AAAA,QACP,aAAa,OAAO;AAAA,QACpB,GAAI,OAAO,aAAa,EAAE,iBAAiB,OAAO,WAAW,IAAI,CAAC;AAAA,MACpE;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,MAAI,OAAO,WAAW,qBAAqB;AACzC,WAAO,aAAa;AAAA,MAClB;AAAA,QACE,OAAO,0CAA0C,OAAO,mBAAmB;AAAA,QAC3E,MAAM;AAAA,MACR;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,QAAM,MAAM,aAAa;AACzB,SAAO,aAAa;AAAA,IAClB;AAAA,MACE,WAAW,OAAO;AAAA,MAClB,oBAAoB,OAAO;AAAA,IAC7B;AAAA,IACA,EAAE,QAAQ,IAAI;AAAA,EAChB;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,uBAAuB;AAAA,EAC9B,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,MAAM,CAAC,uBAAuB;AAAA,MAC9B,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,QAChD,EAAE,QAAQ,KAAK,aAAa,gCAAgC;AAAA,QAC5D,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,+CAA+C;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;AACA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,8 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
6
6
|
import { toAbsoluteUrl } from "@open-mercato/shared/lib/url";
|
|
7
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
8
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
7
9
|
import { getChannelAdapter } from "../../../../../lib/adapter-registry-singleton.js";
|
|
8
10
|
import { resolveOAuthClientCredentials } from "../../../../../lib/oauth-client-config.js";
|
|
9
11
|
import {
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
normalizeOAuthReturnUrl,
|
|
16
18
|
OAuthStateError
|
|
17
19
|
} from "../../../../../lib/oauth-state.js";
|
|
20
|
+
const logger = createLogger("communication_channels").child({ component: "oauth-initiate" });
|
|
18
21
|
const metadata = {
|
|
19
22
|
path: "/communication_channels/oauth/[provider]/initiate",
|
|
20
23
|
POST: {
|
|
@@ -34,6 +37,20 @@ const initiateBodySchema = z.object({
|
|
|
34
37
|
function defaultRedirectUri(req, providerKey) {
|
|
35
38
|
return toAbsoluteUrl(req, `/api/communication_channels/oauth/${providerKey}/callback`);
|
|
36
39
|
}
|
|
40
|
+
function oauthStateErrorResponse(err) {
|
|
41
|
+
if (err instanceof OAuthStateError) {
|
|
42
|
+
if (err.code === "missing_secret") {
|
|
43
|
+
logger.error("OAuth state secret is not configured", { err });
|
|
44
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
45
|
+
module: "communication_channels",
|
|
46
|
+
code: "communication_channels.missing_secret"
|
|
47
|
+
});
|
|
48
|
+
return NextResponse.json({ error: "OAuth is not available", code: err.code }, { status: 500 });
|
|
49
|
+
}
|
|
50
|
+
return NextResponse.json({ error: err.message, code: err.code }, { status: 500 });
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
37
54
|
async function POST(req, context) {
|
|
38
55
|
const { provider } = await context.params;
|
|
39
56
|
if (!/^[a-z0-9_-]+$/i.test(provider)) {
|
|
@@ -88,13 +105,20 @@ async function POST(req, context) {
|
|
|
88
105
|
);
|
|
89
106
|
}
|
|
90
107
|
const redirectUri = defaultRedirectUri(req, provider);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
let stateEnvelope;
|
|
109
|
+
try {
|
|
110
|
+
stateEnvelope = createOAuthState({
|
|
111
|
+
userId: auth.sub,
|
|
112
|
+
tenantId: auth.tenantId,
|
|
113
|
+
organizationId: auth.orgId ?? null,
|
|
114
|
+
providerKey: provider,
|
|
115
|
+
returnUrl: normalizeOAuthReturnUrl(body.returnUrl, DEFAULT_OAUTH_RETURN_URL)
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const response2 = oauthStateErrorResponse(err);
|
|
119
|
+
if (response2) return response2;
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
98
122
|
let result;
|
|
99
123
|
try {
|
|
100
124
|
result = await adapter.buildOAuthAuthorizeUrl({
|
|
@@ -115,10 +139,21 @@ async function POST(req, context) {
|
|
|
115
139
|
const message = err instanceof Error ? err.message : "Failed to build OAuth authorize URL";
|
|
116
140
|
return NextResponse.json({ error: message }, { status: 502 });
|
|
117
141
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
let finalCookie;
|
|
143
|
+
if (result.extra) {
|
|
144
|
+
try {
|
|
145
|
+
finalCookie = (await import("../../../../../lib/oauth-state.js")).encryptOAuthState({
|
|
146
|
+
...stateEnvelope.payload,
|
|
147
|
+
extra: { ...stateEnvelope.payload.extra ?? {}, ...result.extra }
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const response2 = oauthStateErrorResponse(err);
|
|
151
|
+
if (response2) return response2;
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
finalCookie = stateEnvelope.cookie;
|
|
156
|
+
}
|
|
122
157
|
const response = NextResponse.json({ authorizeUrl: result.authorizeUrl });
|
|
123
158
|
response.cookies.set({
|
|
124
159
|
name: COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,
|
|
@@ -142,6 +177,7 @@ const openApi = {
|
|
|
142
177
|
{ status: 400, description: "Invalid provider or unsupported (no OAuth)" },
|
|
143
178
|
{ status: 401, description: "Unauthorized" },
|
|
144
179
|
{ status: 422, description: "Invalid request body" },
|
|
180
|
+
{ status: 500, description: "OAuth state secret missing or state encryption failed" },
|
|
145
181
|
{ status: 502, description: "Adapter failed to build authorize URL" }
|
|
146
182
|
]
|
|
147
183
|
}
|