@open-mercato/core 0.10.1-develop.7728.1.6b52eb2849 → 0.10.1-develop.7736.1.88e07906bf

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 6147 entry points
1
+ [build:core] found 6151 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -110,6 +110,14 @@ class OcrService {
110
110
  guidance = "Rate limit exceeded. Please try again later.";
111
111
  break;
112
112
  default:
113
+ if (err?.name === "TimeoutError") {
114
+ guidance = `OCR request timed out: ${rawMessage}`;
115
+ break;
116
+ }
117
+ if (err?.name === "AbortError") {
118
+ guidance = `OCR request was aborted: ${rawMessage}`;
119
+ break;
120
+ }
113
121
  guidance = rawMessage.includes("https://") ? rawMessage : `${rawMessage}. Check OPENAI_API_KEY.`;
114
122
  }
115
123
  const wrapped = new Error(`[attachments.ocr] ${guidance}`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/ocrService.ts"],
4
- "sourcesContent": ["import { generateText } from 'ai'\nimport { createOpenAI } from '@ai-sdk/openai'\nimport fs from 'fs/promises'\nimport path from 'path'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveOcrMaxOutputTokens, resolveOcrPageTimeoutMs } from './ocrLimits'\n\nconst logger = createLogger('attachments').child({ component: 'ocr-service' })\n\nexport type OcrServiceOptions = {\n apiKey?: string\n model?: string\n}\n\nexport type OcrInput = {\n filePath: string\n mimeType: string | null\n model?: string\n}\n\nexport type OcrResult = {\n content: string\n pageCount?: number\n processingTimeMs: number\n}\n\nconst DEFAULT_MODEL = 'gpt-4o'\nconst OCR_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff']\n\nconst DEFAULT_OCR_PROMPT = `Extract all text content from this image. Preserve the structure and formatting where possible. Output the text in markdown format. If there are tables, preserve them as markdown tables. If there is no text visible, respond with an empty string.`\n\nfunction isImageMimeType(mimeType: string | null, filePath?: string): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized.startsWith('image/')) return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return OCR_IMAGE_EXTENSIONS.includes(ext)\n }\n return false\n}\n\nfunction isPdfMimeType(mimeType: string | null, filePath?: string): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized === 'application/pdf') return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return ext === '.pdf'\n }\n return false\n}\n\nfunction getImageMediaType(mimeType: string | null, filePath: string): string {\n if (mimeType && mimeType.startsWith('image/')) {\n return mimeType\n }\n const ext = path.extname(filePath).toLowerCase()\n const mimeMap: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.bmp': 'image/bmp',\n '.webp': 'image/webp',\n '.tiff': 'image/tiff',\n }\n return mimeMap[ext] || 'image/png'\n}\n\nexport class OcrService {\n private readonly apiKey: string | null\n private readonly defaultModel: string\n private client: ReturnType<typeof createOpenAI> | null = null\n\n constructor(opts: OcrServiceOptions = {}) {\n this.apiKey = opts.apiKey ?? process.env.OPENAI_API_KEY ?? null\n this.defaultModel = opts.model ?? process.env.OCR_MODEL ?? DEFAULT_MODEL\n }\n\n get available(): boolean {\n return Boolean(this.apiKey)\n }\n\n private ensureClient() {\n if (!this.apiKey) {\n throw new Error('[attachments.ocr] Missing OPENAI_API_KEY environment variable')\n }\n if (!this.client) {\n this.client = createOpenAI({ apiKey: this.apiKey })\n }\n return this.client\n }\n\n private async generateTextFromImageBuffer(params: {\n imageBuffer: Buffer\n mediaType: string\n model: string\n }): Promise<string> {\n const { imageBuffer, mediaType, model } = params\n const client = this.ensureClient()\n const base64 = imageBuffer.toString('base64')\n const dataUrl = `data:${mediaType};base64,${base64}`\n\n try {\n const result = await generateText({\n model: client(model),\n abortSignal: AbortSignal.timeout(resolveOcrPageTimeoutMs()),\n maxRetries: 0,\n maxOutputTokens: resolveOcrMaxOutputTokens(),\n messages: [\n {\n role: 'user',\n content: [\n {\n type: 'image',\n image: dataUrl,\n },\n {\n type: 'text',\n text: process.env.OCR_DEFAULT_PROMPT ?? DEFAULT_OCR_PROMPT,\n },\n ],\n },\n ],\n })\n\n return result.text.trim()\n } catch (err: any) {\n const statusCandidate =\n err?.statusCode ?? err?.status ?? err?.response?.status ?? err?.response?.statusCode\n const status =\n typeof statusCandidate === 'number'\n ? Number.isFinite(statusCandidate) ? statusCandidate : undefined\n : typeof statusCandidate === 'string'\n ? Number.parseInt(statusCandidate, 10)\n : undefined\n const apiError = err?.data?.error ?? err?.body?.error ?? err?.response?.data?.error\n const apiMessage = apiError?.message ?? err?.response?.data?.message\n const apiCode = typeof apiError?.code === 'string' ? apiError.code : undefined\n const rawMessage = typeof apiMessage === 'string'\n ? apiMessage\n : (typeof err?.message === 'string' ? err.message : 'OCR request failed')\n\n let guidance: string\n switch (apiCode) {\n case 'insufficient_quota':\n guidance = 'OpenAI usage quota exceeded. Please review your plan and billing.'\n break\n case 'invalid_api_key':\n guidance = 'Invalid OpenAI API key. Update the key and retry.'\n break\n case 'account_deactivated':\n guidance = 'OpenAI account is disabled. Contact OpenAI support or provide a different key.'\n break\n case 'rate_limit_exceeded':\n guidance = 'Rate limit exceeded. Please try again later.'\n break\n default:\n guidance = rawMessage.includes('https://')\n ? rawMessage\n : `${rawMessage}. Check OPENAI_API_KEY.`\n }\n\n const wrapped = new Error(`[attachments.ocr] ${guidance}`)\n if (typeof status === 'number' && Number.isFinite(status)) {\n const normalizedStatus = status === 401 || status === 403 ? 502 : status\n if (normalizedStatus >= 400 && normalizedStatus < 600) {\n (wrapped as any).status = normalizedStatus\n }\n }\n if (apiCode) {\n (wrapped as any).code = apiCode\n }\n (wrapped as any).cause = err\n throw wrapped\n }\n }\n\n async processImage(input: OcrInput): Promise<OcrResult> {\n const startTime = Date.now()\n const { filePath, mimeType, model } = input\n const resolvedModel = model ?? this.defaultModel\n const imageBuffer = await fs.readFile(filePath)\n\n return {\n content: await this.generateTextFromImageBuffer({\n imageBuffer,\n mediaType: getImageMediaType(mimeType, filePath),\n model: resolvedModel,\n }),\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n async processPdf(input: OcrInput): Promise<OcrResult> {\n const startTime = Date.now()\n const { filePath, model } = input\n const resolvedModel = model ?? this.defaultModel\n const { preparePdfPagesForOcr } = await import('./pdfProcessing')\n const preparedPdf = await preparePdfPagesForOcr(filePath)\n\n if (preparedPdf.pageCount === 0) {\n return {\n content: '',\n pageCount: 0,\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n const pageContents: string[] = []\n\n for (const page of preparedPdf.pages) {\n try {\n let pageContent = page.extractedText\n\n if (!pageContent && page.imageBuffer) {\n pageContent = await this.generateTextFromImageBuffer({\n imageBuffer: page.imageBuffer,\n mediaType: 'image/png',\n model: resolvedModel,\n })\n }\n\n if (pageContent) {\n if (preparedPdf.pageCount > 1) {\n pageContents.push(`--- Page ${page.pageNumber} ---\\n\\n${pageContent}`)\n } else {\n pageContents.push(pageContent)\n }\n }\n } catch (error) {\n logger.error('Failed to process PDF page', { pageNumber: page.pageNumber, err: error })\n }\n }\n\n return {\n content: pageContents.join('\\n\\n'),\n pageCount: preparedPdf.pageCount,\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n async processFile(input: OcrInput): Promise<OcrResult | null> {\n const { filePath, mimeType } = input\n\n if (isPdfMimeType(mimeType, filePath)) {\n logger.info('Processing PDF attachment via pdfjs-dist')\n return this.processPdf(input)\n }\n\n if (isImageMimeType(mimeType, filePath)) {\n logger.info('Processing image attachment via LLM OCR')\n return this.processImage(input)\n }\n\n logger.info('Unsupported file type for OCR', { mimeType: mimeType || 'unknown' })\n return null\n }\n}\n\nexport function shouldUseLlmOcr(mimeType: string | null, fileName: string): boolean {\n if (isPdfMimeType(mimeType, fileName)) return true\n return isImageMimeType(mimeType, fileName)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { generateText } from 'ai'\nimport { createOpenAI } from '@ai-sdk/openai'\nimport fs from 'fs/promises'\nimport path from 'path'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveOcrMaxOutputTokens, resolveOcrPageTimeoutMs } from './ocrLimits'\n\nconst logger = createLogger('attachments').child({ component: 'ocr-service' })\n\nexport type OcrServiceOptions = {\n apiKey?: string\n model?: string\n}\n\nexport type OcrInput = {\n filePath: string\n mimeType: string | null\n model?: string\n}\n\nexport type OcrResult = {\n content: string\n pageCount?: number\n processingTimeMs: number\n}\n\nconst DEFAULT_MODEL = 'gpt-4o'\nconst OCR_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff']\n\nconst DEFAULT_OCR_PROMPT = `Extract all text content from this image. Preserve the structure and formatting where possible. Output the text in markdown format. If there are tables, preserve them as markdown tables. If there is no text visible, respond with an empty string.`\n\nfunction isImageMimeType(mimeType: string | null, filePath?: string): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized.startsWith('image/')) return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return OCR_IMAGE_EXTENSIONS.includes(ext)\n }\n return false\n}\n\nfunction isPdfMimeType(mimeType: string | null, filePath?: string): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized === 'application/pdf') return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return ext === '.pdf'\n }\n return false\n}\n\nfunction getImageMediaType(mimeType: string | null, filePath: string): string {\n if (mimeType && mimeType.startsWith('image/')) {\n return mimeType\n }\n const ext = path.extname(filePath).toLowerCase()\n const mimeMap: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.bmp': 'image/bmp',\n '.webp': 'image/webp',\n '.tiff': 'image/tiff',\n }\n return mimeMap[ext] || 'image/png'\n}\n\nexport class OcrService {\n private readonly apiKey: string | null\n private readonly defaultModel: string\n private client: ReturnType<typeof createOpenAI> | null = null\n\n constructor(opts: OcrServiceOptions = {}) {\n this.apiKey = opts.apiKey ?? process.env.OPENAI_API_KEY ?? null\n this.defaultModel = opts.model ?? process.env.OCR_MODEL ?? DEFAULT_MODEL\n }\n\n get available(): boolean {\n return Boolean(this.apiKey)\n }\n\n private ensureClient() {\n if (!this.apiKey) {\n throw new Error('[attachments.ocr] Missing OPENAI_API_KEY environment variable')\n }\n if (!this.client) {\n this.client = createOpenAI({ apiKey: this.apiKey })\n }\n return this.client\n }\n\n private async generateTextFromImageBuffer(params: {\n imageBuffer: Buffer\n mediaType: string\n model: string\n }): Promise<string> {\n const { imageBuffer, mediaType, model } = params\n const client = this.ensureClient()\n const base64 = imageBuffer.toString('base64')\n const dataUrl = `data:${mediaType};base64,${base64}`\n\n try {\n const result = await generateText({\n model: client(model),\n abortSignal: AbortSignal.timeout(resolveOcrPageTimeoutMs()),\n maxRetries: 0,\n maxOutputTokens: resolveOcrMaxOutputTokens(),\n messages: [\n {\n role: 'user',\n content: [\n {\n type: 'image',\n image: dataUrl,\n },\n {\n type: 'text',\n text: process.env.OCR_DEFAULT_PROMPT ?? DEFAULT_OCR_PROMPT,\n },\n ],\n },\n ],\n })\n\n return result.text.trim()\n } catch (err: any) {\n const statusCandidate =\n err?.statusCode ?? err?.status ?? err?.response?.status ?? err?.response?.statusCode\n const status =\n typeof statusCandidate === 'number'\n ? Number.isFinite(statusCandidate) ? statusCandidate : undefined\n : typeof statusCandidate === 'string'\n ? Number.parseInt(statusCandidate, 10)\n : undefined\n const apiError = err?.data?.error ?? err?.body?.error ?? err?.response?.data?.error\n const apiMessage = apiError?.message ?? err?.response?.data?.message\n const apiCode = typeof apiError?.code === 'string' ? apiError.code : undefined\n const rawMessage = typeof apiMessage === 'string'\n ? apiMessage\n : (typeof err?.message === 'string' ? err.message : 'OCR request failed')\n\n let guidance: string\n switch (apiCode) {\n case 'insufficient_quota':\n guidance = 'OpenAI usage quota exceeded. Please review your plan and billing.'\n break\n case 'invalid_api_key':\n guidance = 'Invalid OpenAI API key. Update the key and retry.'\n break\n case 'account_deactivated':\n guidance = 'OpenAI account is disabled. Contact OpenAI support or provide a different key.'\n break\n case 'rate_limit_exceeded':\n guidance = 'Rate limit exceeded. Please try again later.'\n break\n default:\n if (err?.name === 'TimeoutError') {\n guidance = `OCR request timed out: ${rawMessage}`\n break\n }\n if (err?.name === 'AbortError') {\n guidance = `OCR request was aborted: ${rawMessage}`\n break\n }\n guidance = rawMessage.includes('https://')\n ? rawMessage\n : `${rawMessage}. Check OPENAI_API_KEY.`\n }\n\n const wrapped = new Error(`[attachments.ocr] ${guidance}`)\n if (typeof status === 'number' && Number.isFinite(status)) {\n const normalizedStatus = status === 401 || status === 403 ? 502 : status\n if (normalizedStatus >= 400 && normalizedStatus < 600) {\n (wrapped as any).status = normalizedStatus\n }\n }\n if (apiCode) {\n (wrapped as any).code = apiCode\n }\n (wrapped as any).cause = err\n throw wrapped\n }\n }\n\n async processImage(input: OcrInput): Promise<OcrResult> {\n const startTime = Date.now()\n const { filePath, mimeType, model } = input\n const resolvedModel = model ?? this.defaultModel\n const imageBuffer = await fs.readFile(filePath)\n\n return {\n content: await this.generateTextFromImageBuffer({\n imageBuffer,\n mediaType: getImageMediaType(mimeType, filePath),\n model: resolvedModel,\n }),\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n async processPdf(input: OcrInput): Promise<OcrResult> {\n const startTime = Date.now()\n const { filePath, model } = input\n const resolvedModel = model ?? this.defaultModel\n const { preparePdfPagesForOcr } = await import('./pdfProcessing')\n const preparedPdf = await preparePdfPagesForOcr(filePath)\n\n if (preparedPdf.pageCount === 0) {\n return {\n content: '',\n pageCount: 0,\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n const pageContents: string[] = []\n\n for (const page of preparedPdf.pages) {\n try {\n let pageContent = page.extractedText\n\n if (!pageContent && page.imageBuffer) {\n pageContent = await this.generateTextFromImageBuffer({\n imageBuffer: page.imageBuffer,\n mediaType: 'image/png',\n model: resolvedModel,\n })\n }\n\n if (pageContent) {\n if (preparedPdf.pageCount > 1) {\n pageContents.push(`--- Page ${page.pageNumber} ---\\n\\n${pageContent}`)\n } else {\n pageContents.push(pageContent)\n }\n }\n } catch (error) {\n logger.error('Failed to process PDF page', { pageNumber: page.pageNumber, err: error })\n }\n }\n\n return {\n content: pageContents.join('\\n\\n'),\n pageCount: preparedPdf.pageCount,\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n async processFile(input: OcrInput): Promise<OcrResult | null> {\n const { filePath, mimeType } = input\n\n if (isPdfMimeType(mimeType, filePath)) {\n logger.info('Processing PDF attachment via pdfjs-dist')\n return this.processPdf(input)\n }\n\n if (isImageMimeType(mimeType, filePath)) {\n logger.info('Processing image attachment via LLM OCR')\n return this.processImage(input)\n }\n\n logger.info('Unsupported file type for OCR', { mimeType: mimeType || 'unknown' })\n return null\n }\n}\n\nexport function shouldUseLlmOcr(mimeType: string | null, fileName: string): boolean {\n if (isPdfMimeType(mimeType, fileName)) return true\n return isImageMimeType(mimeType, fileName)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -72,6 +72,8 @@ async function POST(req) {
72
72
  const res = buildSafeRedirectResponse(req, "/login");
73
73
  res.cookies.set("auth_token", "", { path: "/", maxAge: 0 });
74
74
  res.cookies.set("session_token", "", { path: "/", maxAge: 0 });
75
+ res.cookies.set("om_selected_org", "", { path: "/", maxAge: 0 });
76
+ res.cookies.set("om_selected_tenant", "", { path: "/", maxAge: 0 });
75
77
  return res;
76
78
  }
77
79
  const metadata = {
@@ -83,7 +85,7 @@ const openApi = {
83
85
  methods: {
84
86
  POST: {
85
87
  summary: "Invalidate session and redirect",
86
- description: "Revokes the session identified by the `auth_token` sid claim and the `session_token` cookie, clears both cookies, and redirects the browser to the login page. A valid access token is not required, so a logout submitted after the access token expired still revokes the remember-me refresh session. A request that carries neither cookie is only redirected.",
88
+ description: "Revokes the session identified by the `auth_token` sid claim and the `session_token` cookie, clears both cookies together with the organization-scope cookies (`om_selected_org`, `om_selected_tenant`), and redirects the browser to the login page. A valid access token is not required, so a logout submitted after the access token expired still revokes the remember-me refresh session. A request that carries neither session cookie is only redirected.",
87
89
  responses: [
88
90
  { status: 307, description: "Redirect to login after successful logout", mediaType: "text/html" }
89
91
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/api/logout.ts"],
4
- "sourcesContent": ["import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { buildSafeRedirectResponse } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { emitAuthEvent } from '@open-mercato/core/modules/auth/events'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\ntype AuthTokenClaims = {\n userId: string | null\n sessionId: string | null\n tenantId: string | null\n organizationId: string | null\n}\n\nconst emptyAuthTokenClaims: AuthTokenClaims = Object.freeze({\n userId: null,\n sessionId: null,\n tenantId: null,\n organizationId: null,\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n if (!m) return null\n try {\n return decodeURIComponent(m[1])\n } catch {\n return null\n }\n}\n\nfunction readStringClaim(payload: Record<string, unknown>, key: string): string | null {\n const value = payload[key]\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction extractAuthTokenClaims(token: string | null): AuthTokenClaims {\n if (!token) return emptyAuthTokenClaims\n try {\n const payload = verifyJwt(token) as Record<string, unknown> | null\n if (!payload || payload.type === 'customer') return emptyAuthTokenClaims\n return {\n userId: readStringClaim(payload, 'sub'),\n sessionId: readStringClaim(payload, 'sid'),\n tenantId: readStringClaim(payload, 'tenantId'),\n organizationId: readStringClaim(payload, 'orgId'),\n }\n } catch {\n return emptyAuthTokenClaims\n }\n}\n\nexport async function POST(req: Request) {\n const sessToken = parseCookie(req, 'session_token')\n const authToken = parseCookie(req, 'auth_token')\n if (!sessToken && !authToken) return buildSafeRedirectResponse(req, '/login')\n const claims = extractAuthTokenClaims(authToken)\n let sessionRevoked = false\n if (sessToken || claims.sessionId) {\n try {\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n if (claims.sessionId) {\n await auth.deleteSessionById(claims.sessionId)\n }\n if (sessToken) {\n await auth.deleteSessionByToken(sessToken)\n }\n sessionRevoked = true\n } catch (error) {\n getTelemetryRuntime()?.reportError(error, { module: 'auth', code: 'auth.logout_revoke_failed' })\n }\n }\n if (claims.userId) {\n void emitAuthEvent('auth.logout', {\n id: claims.userId,\n tenantId: claims.tenantId,\n organizationId: claims.organizationId,\n sessionId: claims.sessionId,\n sessionRevoked,\n at: new Date().toISOString(),\n }, { persistent: true }).catch(() => undefined)\n }\n const res = buildSafeRedirectResponse(req, '/login')\n res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n res.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n return res\n}\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Log out current session',\n methods: {\n POST: {\n summary: 'Invalidate session and redirect',\n description: 'Revokes the session identified by the `auth_token` sid claim and the `session_token` cookie, clears both cookies, and redirects the browser to the login page. A valid access token is not required, so a logout submitted after the access token expired still revokes the remember-me refresh session. A request that carries neither cookie is only redirected.',\n responses: [\n { status: 307, description: 'Redirect to login after successful logout', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { buildSafeRedirectResponse } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { emitAuthEvent } from '@open-mercato/core/modules/auth/events'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\ntype AuthTokenClaims = {\n userId: string | null\n sessionId: string | null\n tenantId: string | null\n organizationId: string | null\n}\n\nconst emptyAuthTokenClaims: AuthTokenClaims = Object.freeze({\n userId: null,\n sessionId: null,\n tenantId: null,\n organizationId: null,\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n if (!m) return null\n try {\n return decodeURIComponent(m[1])\n } catch {\n return null\n }\n}\n\nfunction readStringClaim(payload: Record<string, unknown>, key: string): string | null {\n const value = payload[key]\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction extractAuthTokenClaims(token: string | null): AuthTokenClaims {\n if (!token) return emptyAuthTokenClaims\n try {\n const payload = verifyJwt(token) as Record<string, unknown> | null\n if (!payload || payload.type === 'customer') return emptyAuthTokenClaims\n return {\n userId: readStringClaim(payload, 'sub'),\n sessionId: readStringClaim(payload, 'sid'),\n tenantId: readStringClaim(payload, 'tenantId'),\n organizationId: readStringClaim(payload, 'orgId'),\n }\n } catch {\n return emptyAuthTokenClaims\n }\n}\n\nexport async function POST(req: Request) {\n const sessToken = parseCookie(req, 'session_token')\n const authToken = parseCookie(req, 'auth_token')\n if (!sessToken && !authToken) return buildSafeRedirectResponse(req, '/login')\n const claims = extractAuthTokenClaims(authToken)\n let sessionRevoked = false\n if (sessToken || claims.sessionId) {\n try {\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n if (claims.sessionId) {\n await auth.deleteSessionById(claims.sessionId)\n }\n if (sessToken) {\n await auth.deleteSessionByToken(sessToken)\n }\n sessionRevoked = true\n } catch (error) {\n getTelemetryRuntime()?.reportError(error, { module: 'auth', code: 'auth.logout_revoke_failed' })\n }\n }\n if (claims.userId) {\n void emitAuthEvent('auth.logout', {\n id: claims.userId,\n tenantId: claims.tenantId,\n organizationId: claims.organizationId,\n sessionId: claims.sessionId,\n sessionRevoked,\n at: new Date().toISOString(),\n }, { persistent: true }).catch(() => undefined)\n }\n const res = buildSafeRedirectResponse(req, '/login')\n res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n res.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n res.cookies.set('om_selected_org', '', { path: '/', maxAge: 0 })\n res.cookies.set('om_selected_tenant', '', { path: '/', maxAge: 0 })\n return res\n}\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Log out current session',\n methods: {\n POST: {\n summary: 'Invalidate session and redirect',\n description: 'Revokes the session identified by the `auth_token` sid claim and the `session_token` cookie, clears both cookies together with the organization-scope cookies (`om_selected_org`, `om_selected_tenant`), and redirects the browser to the login page. A valid access token is not required, so a logout submitted after the access token expired still revokes the remember-me refresh session. A request that carries neither session cookie is only redirected.',\n responses: [\n { status: 307, description: 'Redirect to login after successful logout', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }