@open-mercato/core 0.9.1-develop.7459.1.99e3accd30 → 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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/route.js +29 -3
  3. package/dist/modules/attachments/api/route.js.map +2 -2
  4. package/dist/modules/attachments/lib/ocrLimits.js +49 -0
  5. package/dist/modules/attachments/lib/ocrLimits.js.map +7 -0
  6. package/dist/modules/attachments/lib/ocrQueue.js +76 -2
  7. package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
  8. package/dist/modules/attachments/lib/ocrService.js +4 -0
  9. package/dist/modules/attachments/lib/ocrService.js.map +2 -2
  10. package/dist/modules/attachments/lib/pdfProcessing.js +14 -2
  11. package/dist/modules/attachments/lib/pdfProcessing.js.map +2 -2
  12. package/dist/modules/attachments/lib/scoped-upload-service.js +4 -2
  13. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  14. package/dist/modules/attachments/lib/textExtraction.js +13 -1
  15. package/dist/modules/attachments/lib/textExtraction.js.map +2 -2
  16. package/dist/modules/catalog/search.js +3 -1
  17. package/dist/modules/catalog/search.js.map +2 -2
  18. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +2 -1
  19. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +2 -2
  20. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +10 -7
  21. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
  22. package/dist/modules/communication_channels/lib/oauth-result.js +12 -0
  23. package/dist/modules/communication_channels/lib/oauth-result.js.map +7 -0
  24. package/package.json +7 -7
  25. package/src/modules/attachments/api/route.ts +36 -3
  26. package/src/modules/attachments/lib/ocrLimits.ts +62 -0
  27. package/src/modules/attachments/lib/ocrQueue.ts +95 -2
  28. package/src/modules/attachments/lib/ocrService.ts +4 -0
  29. package/src/modules/attachments/lib/pdfProcessing.ts +14 -2
  30. package/src/modules/attachments/lib/scoped-upload-service.ts +4 -2
  31. package/src/modules/attachments/lib/textExtraction.ts +14 -1
  32. package/src/modules/catalog/search.ts +5 -1
  33. package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +2 -1
  34. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +12 -9
  35. package/src/modules/communication_channels/i18n/de.json +0 -1
  36. package/src/modules/communication_channels/i18n/en.json +0 -1
  37. package/src/modules/communication_channels/i18n/es.json +0 -1
  38. package/src/modules/communication_channels/i18n/ko.json +0 -1
  39. package/src/modules/communication_channels/i18n/pl.json +0 -1
  40. package/src/modules/communication_channels/lib/oauth-result.ts +13 -0
@@ -0,0 +1,62 @@
1
+ import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
2
+ import type { RateLimitConfig } from '@open-mercato/shared/lib/ratelimit/types'
3
+
4
+ const DEFAULT_MAX_OCR_PAGES = 50
5
+ const DEFAULT_OCR_PAGE_TIMEOUT_MS = 60_000
6
+ const DEFAULT_OCR_MAX_OUTPUT_TOKENS = 4_096
7
+ const DEFAULT_OCR_MAX_CONCURRENCY = 2
8
+ const DEFAULT_OCR_MAX_WAIT_QUEUE = 50
9
+
10
+ function parsePositiveInt(raw: string | undefined, fallback: number): number {
11
+ if (raw === undefined || raw.trim() === '') return fallback
12
+ const parsed = Number(raw)
13
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback
14
+ return Math.floor(parsed)
15
+ }
16
+
17
+ /** Cap PDF pages processed for OCR / text extraction (env: OM_ATTACHMENT_OCR_MAX_PAGES). */
18
+ export function resolveMaxOcrPages(): number {
19
+ return parsePositiveInt(process.env.OM_ATTACHMENT_OCR_MAX_PAGES, DEFAULT_MAX_OCR_PAGES)
20
+ }
21
+
22
+ /** Per-page LLM OCR call timeout in ms (env: OM_ATTACHMENT_OCR_PAGE_TIMEOUT_MS). */
23
+ export function resolveOcrPageTimeoutMs(): number {
24
+ return parsePositiveInt(process.env.OM_ATTACHMENT_OCR_PAGE_TIMEOUT_MS, DEFAULT_OCR_PAGE_TIMEOUT_MS)
25
+ }
26
+
27
+ /** Max output tokens for a single OCR generateText call (env: OM_ATTACHMENT_OCR_MAX_OUTPUT_TOKENS). */
28
+ export function resolveOcrMaxOutputTokens(): number {
29
+ return parsePositiveInt(process.env.OM_ATTACHMENT_OCR_MAX_OUTPUT_TOKENS, DEFAULT_OCR_MAX_OUTPUT_TOKENS)
30
+ }
31
+
32
+ /** Max concurrent in-process OCR jobs (env: OM_ATTACHMENT_OCR_MAX_CONCURRENCY). */
33
+ export function resolveOcrMaxConcurrency(): number {
34
+ return parsePositiveInt(process.env.OM_ATTACHMENT_OCR_MAX_CONCURRENCY, DEFAULT_OCR_MAX_CONCURRENCY)
35
+ }
36
+
37
+ /** Max waiters blocked on the in-process OCR concurrency slot (env: OM_ATTACHMENT_OCR_MAX_WAIT_QUEUE). */
38
+ export function resolveOcrMaxWaitQueue(): number {
39
+ return parsePositiveInt(process.env.OM_ATTACHMENT_OCR_MAX_WAIT_QUEUE, DEFAULT_OCR_MAX_WAIT_QUEUE)
40
+ }
41
+
42
+ /** How many PDF pages to iterate given document length and the configured cap. */
43
+ export function resolvePdfPageIterationLimit(numPages: number, maxPages: number = resolveMaxOcrPages()): number {
44
+ if (!Number.isFinite(numPages) || numPages <= 0) return 0
45
+ if (!Number.isFinite(maxPages) || maxPages <= 0) return 0
46
+ return Math.min(Math.floor(numPages), Math.floor(maxPages))
47
+ }
48
+
49
+ /**
50
+ * Per-tenant/user upload throttle for POST /api/attachments.
51
+ * Env: RATE_LIMIT_ATTACHMENTS_UPLOAD_{POINTS,DURATION,BLOCK_DURATION}.
52
+ */
53
+ export function resolveAttachmentsUploadRateLimitConfig(): RateLimitConfig {
54
+ // Default sits above typical multi-file library batches (dialog has no hard
55
+ // file cap). Client retry/partial-progress UX for rejected mid-batch files is
56
+ // tracked in https://github.com/open-mercato/open-mercato/issues/6397.
57
+ return readEndpointRateLimitConfig('ATTACHMENTS_UPLOAD', {
58
+ points: 100,
59
+ duration: 60,
60
+ keyPrefix: 'attachments_upload',
61
+ })
62
+ }
@@ -3,6 +3,8 @@ import { Attachment, AttachmentPartition } from '../data/entities'
3
3
  import { OcrService } from './ocrService'
4
4
  import type { StorageDriver } from './drivers/types'
5
5
  import { createLogger } from '@open-mercato/shared/lib/logger'
6
+ import { resolveOcrMaxConcurrency, resolveOcrMaxWaitQueue } from './ocrLimits'
7
+ import { extractAttachmentContent } from './textExtraction'
6
8
 
7
9
  const logger = createLogger('attachments').child({ component: 'ocr' })
8
10
 
@@ -15,6 +17,45 @@ export type OcrRequestedEvent = {
15
17
  tenantId: string | null
16
18
  }
17
19
 
20
+ /** Outcome of `requestOcrProcessing` — background LLM OCR vs inline text fallback. */
21
+ export type OcrProcessingDispatch = 'queued' | 'inline_fallback'
22
+
23
+ let activeOcrJobs = 0
24
+ const ocrWaitQueue: Array<() => void> = []
25
+ // Jobs scheduled with setImmediate that have not yet entered a slot or the wait queue.
26
+ // Counted toward the wait-queue cap so a burst in one event-loop turn cannot pass the check together.
27
+ let pendingOcrJobs = 0
28
+
29
+ /** Test-only: reset in-process OCR concurrency bookkeeping. */
30
+ export function resetOcrConcurrencyStateForTests(): void {
31
+ activeOcrJobs = 0
32
+ ocrWaitQueue.length = 0
33
+ pendingOcrJobs = 0
34
+ }
35
+
36
+ /** Test-only: inspect in-process OCR concurrency counters. */
37
+ export function getOcrConcurrencyStateForTests(): { active: number; waiting: number } {
38
+ return { active: activeOcrJobs, waiting: ocrWaitQueue.length }
39
+ }
40
+
41
+ export async function withOcrConcurrencySlot<T>(run: () => Promise<T>): Promise<T> {
42
+ const maxConcurrency = resolveOcrMaxConcurrency()
43
+ // Re-check after wake: multiple waiters can resume when one slot frees (barging).
44
+ while (activeOcrJobs >= maxConcurrency) {
45
+ await new Promise<void>((resolve) => {
46
+ ocrWaitQueue.push(resolve)
47
+ })
48
+ }
49
+ activeOcrJobs += 1
50
+ try {
51
+ return await run()
52
+ } finally {
53
+ activeOcrJobs -= 1
54
+ const next = ocrWaitQueue.shift()
55
+ if (next) next()
56
+ }
57
+ }
58
+
18
59
  export async function processAttachmentOcr(
19
60
  em: EntityManager,
20
61
  payload: OcrRequestedEvent,
@@ -73,12 +114,49 @@ export async function processAttachmentOcr(
73
114
  }
74
115
  }
75
116
 
117
+ /**
118
+ * When the in-process OCR wait queue is full, extract plain text/PDF/DOCX content
119
+ * inline (no LLM) so `attachment.content` is not left NULL forever.
120
+ */
121
+ async function persistInlineTextExtractionFallback(
122
+ em: EntityManager,
123
+ payload: OcrRequestedEvent,
124
+ driver: StorageDriver,
125
+ ): Promise<void> {
126
+ const { attachmentId, storagePath, mimeType, partitionCode } = payload
127
+ const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath)
128
+ try {
129
+ const content = await extractAttachmentContent({ filePath, mimeType })
130
+ if (!content) {
131
+ logger.info('OCR wait queue full; inline extraction produced no content', { attachmentId })
132
+ return
133
+ }
134
+ const row = await em.findOne(Attachment, { id: attachmentId })
135
+ if (!row) {
136
+ logger.error('Attachment not found during OCR overflow fallback', { attachmentId })
137
+ return
138
+ }
139
+ row.content = content
140
+ await em.persist(row).flush()
141
+ logger.info('OCR wait queue full; stored inline text extraction', {
142
+ attachmentId,
143
+ contentLength: content.length,
144
+ })
145
+ } catch (error) {
146
+ logger.error('OCR wait queue overflow inline extraction failed', { attachmentId, err: error })
147
+ } finally {
148
+ await cleanup().catch((cleanupError) => {
149
+ logger.warn('Temp file cleanup failed after OCR overflow fallback', { err: cleanupError })
150
+ })
151
+ }
152
+ }
153
+
76
154
  export async function requestOcrProcessing(
77
155
  em: EntityManager,
78
156
  attachment: Attachment,
79
157
  driver: StorageDriver,
80
158
  storagePath: string,
81
- ): Promise<void> {
159
+ ): Promise<OcrProcessingDispatch> {
82
160
  const payload: OcrRequestedEvent = {
83
161
  attachmentId: attachment.id,
84
162
  storagePath,
@@ -97,9 +175,24 @@ export async function requestOcrProcessing(
97
175
 
98
176
  const workerEm = em.fork()
99
177
 
178
+ const maxWaitQueue = resolveOcrMaxWaitQueue()
179
+ if (ocrWaitQueue.length + pendingOcrJobs >= maxWaitQueue) {
180
+ logger.warn('OCR wait queue full; falling back to inline text extraction', {
181
+ attachmentId: attachment.id,
182
+ waiting: ocrWaitQueue.length,
183
+ pending: pendingOcrJobs,
184
+ maxWaitQueue,
185
+ })
186
+ await persistInlineTextExtractionFallback(workerEm, payload, driver)
187
+ return 'inline_fallback'
188
+ }
189
+
190
+ pendingOcrJobs += 1
100
191
  setImmediate(() => {
101
- processAttachmentOcr(workerEm, payload, driver).catch((error) => {
192
+ pendingOcrJobs -= 1
193
+ withOcrConcurrencySlot(() => processAttachmentOcr(workerEm, payload, driver)).catch((error) => {
102
194
  logger.error('Background processing error', { err: error })
103
195
  })
104
196
  })
197
+ return 'queued'
105
198
  }
@@ -3,6 +3,7 @@ import { createOpenAI } from '@ai-sdk/openai'
3
3
  import fs from 'fs/promises'
4
4
  import path from 'path'
5
5
  import { createLogger } from '@open-mercato/shared/lib/logger'
6
+ import { resolveOcrMaxOutputTokens, resolveOcrPageTimeoutMs } from './ocrLimits'
6
7
 
7
8
  const logger = createLogger('attachments').child({ component: 'ocr-service' })
8
9
 
@@ -102,6 +103,9 @@ export class OcrService {
102
103
  try {
103
104
  const result = await generateText({
104
105
  model: client(model),
106
+ abortSignal: AbortSignal.timeout(resolveOcrPageTimeoutMs()),
107
+ maxRetries: 0,
108
+ maxOutputTokens: resolveOcrMaxOutputTokens(),
105
109
  messages: [
106
110
  {
107
111
  role: 'user',
@@ -1,11 +1,14 @@
1
1
  import fs from 'fs/promises'
2
2
  import path from 'path'
3
3
  import { createRequire } from 'module'
4
+ import { createLogger } from '@open-mercato/shared/lib/logger'
5
+ import { resolveMaxOcrPages, resolvePdfPageIterationLimit } from './ocrLimits'
4
6
 
5
7
  const moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))
6
8
  const pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))
7
9
  const cMapUrl = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`
8
10
  const standardFontDataUrl = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`
11
+ const logger = createLogger('attachments').child({ component: 'pdf-processing' })
9
12
 
10
13
  const MIN_RENDER_SCALE = 2
11
14
  const MAX_RENDER_SCALE = 4
@@ -113,8 +116,17 @@ export async function preparePdfPagesForOcr(filePath: string): Promise<PdfOcrPre
113
116
 
114
117
  try {
115
118
  const pages: PdfPageOcrInput[] = []
119
+ const maxPages = resolveMaxOcrPages()
120
+ const pageLimit = resolvePdfPageIterationLimit(pdfDocument.numPages, maxPages)
121
+ if (pdfDocument.numPages > pageLimit) {
122
+ logger.warn('PDF page count exceeds OCR cap; truncating', {
123
+ numPages: pdfDocument.numPages,
124
+ maxPages: pageLimit,
125
+ filePath,
126
+ })
127
+ }
116
128
 
117
- for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber += 1) {
129
+ for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
118
130
  const page = await pdfDocument.getPage(pageNumber)
119
131
 
120
132
  try {
@@ -141,7 +153,7 @@ export async function preparePdfPagesForOcr(filePath: string): Promise<PdfOcrPre
141
153
  }
142
154
 
143
155
  return {
144
- pageCount: pdfDocument.numPages,
156
+ pageCount: pages.length,
145
157
  pages,
146
158
  }
147
159
  } finally {
@@ -265,9 +265,11 @@ export class ScopedAttachmentUploadService {
265
265
  }
266
266
 
267
267
  if (useLlmOcr) {
268
- requestOcrProcessing(em, attachment, driver, storedPath).catch((error) => {
268
+ try {
269
+ await requestOcrProcessing(em, attachment, driver, storedPath)
270
+ } catch (error) {
269
271
  logger.error('Scoped attachment OCR scheduling failed', { err: error })
270
- })
272
+ }
271
273
  }
272
274
  if (this.deps.dataEngine) {
273
275
  await emitCrudSideEffects({
@@ -1,6 +1,10 @@
1
1
  import fs from 'fs/promises'
2
2
  import path from 'path'
3
3
  import { createRequire } from 'module'
4
+ import { createLogger } from '@open-mercato/shared/lib/logger'
5
+ import { resolveMaxOcrPages, resolvePdfPageIterationLimit } from './ocrLimits'
6
+
7
+ const logger = createLogger('attachments').child({ component: 'text-extraction' })
4
8
 
5
9
  // NOTE: child_process is intentionally NOT imported here.
6
10
  // This module MUST NOT shell out to any external binary for content extraction.
@@ -68,8 +72,17 @@ async function extractPdfText(filePath: string): Promise<string | null> {
68
72
  })
69
73
  const pdfDocument = await loadingTask.promise
70
74
  const textParts: string[] = []
75
+ const maxPages = resolveMaxOcrPages()
76
+ const pageLimit = resolvePdfPageIterationLimit(pdfDocument.numPages, maxPages)
77
+ if (pdfDocument.numPages > pageLimit) {
78
+ logger.warn('PDF page count exceeds text-extraction cap; truncating', {
79
+ numPages: pdfDocument.numPages,
80
+ maxPages: pageLimit,
81
+ filePath,
82
+ })
83
+ }
71
84
  try {
72
- for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber += 1) {
85
+ for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
73
86
  const page = await pdfDocument.getPage(pageNumber)
74
87
  try {
75
88
  const textContent = await page.getTextContent()
@@ -131,10 +131,14 @@ function buildProductPresenter(
131
131
  ) ?? label
132
132
  const isActive = record.is_active ?? record.isActive
133
133
  const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null
134
+ const productType = readRecordText(record, 'product_type', 'productType')
135
+ const productTypeLabel = productType
136
+ ? translate(`catalog.products.types.${productType}`, productType)
137
+ : null
134
138
  const subtitle = formatSubtitle(
135
139
  readRecordText(record, 'subtitle'),
136
140
  readRecordText(record, 'sku'),
137
- readRecordText(record, 'product_type', 'productType'),
141
+ productTypeLabel,
138
142
  statusText,
139
143
  ) ?? snippet(readRecordText(record, 'description'))
140
144
  return { title, subtitle, icon: 'package', badge: label }
@@ -15,6 +15,7 @@ import {
15
15
  verifyOAuthState,
16
16
  type OAuthStateConsumeStore,
17
17
  } from '../../../../../lib/oauth-state'
18
+ import { OAUTH_RESULT_QUERY_PARAM } from '../../../../../lib/oauth-result'
18
19
  import { createLogger } from '@open-mercato/shared/lib/logger'
19
20
 
20
21
  const logger = createLogger('communication_channels').child({ component: 'oauth-callback' })
@@ -51,7 +52,7 @@ function redirectWithFlash(
51
52
  normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),
52
53
  getAppBaseUrl(req),
53
54
  )
54
- base.searchParams.set('flash', flash.type)
55
+ base.searchParams.set(OAUTH_RESULT_QUERY_PARAM, flash.type)
55
56
  if (flash.code) base.searchParams.set('code', flash.code)
56
57
  if (flash.provider) base.searchParams.set('provider', flash.provider)
57
58
  if (flash.channelId) base.searchParams.set('channelId', flash.channelId)
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { extensionPoints } from '@open-mercato/core/modules/communication_channels/extension-points'
5
5
  import { getImportHistoryLimits } from '@open-mercato/core/modules/communication_channels/lib/import-history-limits'
6
+ import { OAUTH_RESULT_QUERY_PARAM, stripOAuthResultParams } from '@open-mercato/core/modules/communication_channels/lib/oauth-result'
6
7
  import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
7
8
  import { useRouter, useSearchParams } from 'next/navigation'
8
9
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -76,7 +77,7 @@ export default function ProfileCommunicationChannelsPage() {
76
77
  const t = useT()
77
78
  const router = useRouter()
78
79
  const searchParams = useSearchParams()
79
- const flashType = searchParams?.get('flash')
80
+ const flashType = searchParams?.get(OAUTH_RESULT_QUERY_PARAM)
80
81
  const flashCode = searchParams?.get('code')
81
82
  const flashProvider = searchParams?.get('provider')
82
83
 
@@ -96,10 +97,13 @@ export default function ProfileCommunicationChannelsPage() {
96
97
 
97
98
  React.useEffect(() => {
98
99
  if (flashType === 'connected') {
100
+ const providerLabel = flashProvider
101
+ ? t(`communication_channels.channel.providers.${flashProvider}`, '')
102
+ : ''
99
103
  flash(
100
- flashProvider
104
+ providerLabel
101
105
  ? t('communication_channels.profile.flash.connectedWithProvider', 'Channel connected ({provider}).', {
102
- provider: flashProvider,
106
+ provider: providerLabel,
103
107
  })
104
108
  : t('communication_channels.profile.flash.connected', 'Channel connected.'),
105
109
  'success',
@@ -126,15 +130,14 @@ export default function ProfileCommunicationChannelsPage() {
126
130
  'communication_channels.profile.flash.stateStoreUnavailable',
127
131
  'A server error prevented completing the connection. Please try again in a moment.',
128
132
  )
129
- : flashCode
130
- ? t('communication_channels.profile.flash.errorWithCode', 'Failed to connect channel — {code}.', {
131
- code: flashCode,
132
- })
133
- : t('communication_channels.profile.flash.error', 'Failed to connect channel.'),
133
+ : t('communication_channels.profile.flash.error', 'Failed to connect channel.'),
134
134
  'error',
135
135
  )
136
+ } else {
137
+ return
136
138
  }
137
- }, [flashType, flashCode, flashProvider, t])
139
+ router.replace(stripOAuthResultParams(window.location.href), { scroll: false })
140
+ }, [flashType, flashCode, flashProvider, router, t])
138
141
 
139
142
  React.useEffect(() => {
140
143
  let cancelled = false
@@ -107,7 +107,6 @@
107
107
  "communication_channels.profile.flash.connected": "Kanal verbunden.",
108
108
  "communication_channels.profile.flash.connectedWithProvider": "Kanal verbunden ({provider}).",
109
109
  "communication_channels.profile.flash.error": "Kanal konnte nicht verbunden werden.",
110
- "communication_channels.profile.flash.errorWithCode": "Kanal konnte nicht verbunden werden — {code}.",
111
110
  "communication_channels.profile.flash.replayedState": "Dieser Verbindungslink wurde bereits verwendet — bitte starten Sie die Verbindung erneut.",
112
111
  "communication_channels.profile.flash.stateStoreUnavailable": "Ein Serverfehler hat die Verbindung verhindert. Bitte versuchen Sie es in Kürze erneut.",
113
112
  "communication_channels.profile.group": "Profil",
@@ -107,7 +107,6 @@
107
107
  "communication_channels.profile.flash.connected": "Channel connected.",
108
108
  "communication_channels.profile.flash.connectedWithProvider": "Channel connected ({provider}).",
109
109
  "communication_channels.profile.flash.error": "Failed to connect channel.",
110
- "communication_channels.profile.flash.errorWithCode": "Failed to connect channel — {code}.",
111
110
  "communication_channels.profile.flash.replayedState": "This connection link was already used — please start the connection again.",
112
111
  "communication_channels.profile.flash.stateStoreUnavailable": "A server error prevented completing the connection. Please try again in a moment.",
113
112
  "communication_channels.profile.group": "Profile",
@@ -107,7 +107,6 @@
107
107
  "communication_channels.profile.flash.connected": "Canal conectado.",
108
108
  "communication_channels.profile.flash.connectedWithProvider": "Canal conectado ({provider}).",
109
109
  "communication_channels.profile.flash.error": "No se pudo conectar el canal.",
110
- "communication_channels.profile.flash.errorWithCode": "No se pudo conectar el canal — {code}.",
111
110
  "communication_channels.profile.flash.replayedState": "Este enlace de conexión ya se utilizó — inicia la conexión de nuevo.",
112
111
  "communication_channels.profile.flash.stateStoreUnavailable": "Un error del servidor impidió completar la conexión. Inténtalo de nuevo en un momento.",
113
112
  "communication_channels.profile.group": "Perfil",
@@ -107,7 +107,6 @@
107
107
  "communication_channels.profile.flash.connected": "채널이 연결되었습니다.",
108
108
  "communication_channels.profile.flash.connectedWithProvider": "채널이 연결되었습니다 ({provider}).",
109
109
  "communication_channels.profile.flash.error": "채널 연결에 실패했습니다.",
110
- "communication_channels.profile.flash.errorWithCode": "채널 연결에 실패했습니다 — {code}.",
111
110
  "communication_channels.profile.flash.replayedState": "이 연결 링크는 이미 사용되었습니다. 연결을 다시 시작해 주세요.",
112
111
  "communication_channels.profile.flash.stateStoreUnavailable": "서버 오류로 연결을 완료할 수 없습니다. 잠시 후 다시 시도해 주세요.",
113
112
  "communication_channels.profile.group": "프로필",
@@ -107,7 +107,6 @@
107
107
  "communication_channels.profile.flash.connected": "Kanał połączony.",
108
108
  "communication_channels.profile.flash.connectedWithProvider": "Kanał połączony ({provider}).",
109
109
  "communication_channels.profile.flash.error": "Nie udało się połączyć kanału.",
110
- "communication_channels.profile.flash.errorWithCode": "Nie udało się połączyć kanału — {code}.",
111
110
  "communication_channels.profile.flash.replayedState": "Ten link połączenia został już użyty — rozpocznij połączenie ponownie.",
112
111
  "communication_channels.profile.flash.stateStoreUnavailable": "Błąd serwera uniemożliwił dokończenie połączenia. Spróbuj ponownie za chwilę.",
113
112
  "communication_channels.profile.group": "Profil",
@@ -0,0 +1,13 @@
1
+ // The OAuth callback reports its outcome to the return page through this query
2
+ // param. It must not be `flash`: the global <FlashMessages> host reads `?flash=`
3
+ // as the message text and would render the raw outcome ("error"/"connected") as
4
+ // a success toast (#6402).
5
+ export const OAUTH_RESULT_QUERY_PARAM = 'oauth'
6
+
7
+ const OAUTH_RESULT_QUERY_PARAMS = [OAUTH_RESULT_QUERY_PARAM, 'code', 'provider', 'channelId']
8
+
9
+ export function stripOAuthResultParams(href: string): string {
10
+ const url = new URL(href)
11
+ for (const param of OAUTH_RESULT_QUERY_PARAMS) url.searchParams.delete(param)
12
+ return `${url.pathname}${url.search}${url.hash}`
13
+ }