@open-mercato/core 0.6.4-develop.4282.1.4d95e85930 → 0.6.4-develop.4305.1.efaf0ebab1

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/AGENTS.md CHANGED
@@ -606,6 +606,7 @@ const myEnricher: ResponseEnricher = {
606
606
  timeout: 2000, // ms, default 2000
607
607
  fallback: { _mymodule: { count: 0 } },// returned on failure
608
608
  critical: false, // true = error propagates to client
609
+ cacheableOnListHit: false, // see "List cache behavior" below (default false)
609
610
  async enrichOne(record, context) {
610
611
  // Add fields to a single record
611
612
  return { ...record, _mymodule: { count: 42 } }
@@ -629,11 +630,20 @@ const crud = makeCrudRoute({
629
630
  })
630
631
  ```
631
632
 
633
+ ### List cache behavior (`cacheableOnListHit`)
634
+
635
+ When the opt-in CRUD list cache (`ENABLE_CRUD_API_CACHE`) is enabled, the factory stores the **enriched** list payload and partitions cache entries by the active-enricher signature (the ACL/tenant-filtered enricher ids for the caller). On a cache hit it must decide whether to re-run enrichers or serve the stored enriched fields directly:
636
+
637
+ - The cache-hit path **skips re-running enrichers only when every active enricher opted in with `cacheableOnListHit: true`** (record-pure cohort). Otherwise it re-runs all active enrichers on a hit and the cache stores the base (pre-enrichment) payload.
638
+ - Set `cacheableOnListHit: true` **only** when the enricher's output for a record is a pure function of that record's own cached state and is invalidated together with it (e.g. fields derived from the same module's own per-record data). The shipped `example.customer-todo-count` enricher keeps the default `false`: it reads other modules' tables (todos and per-customer priority) the list cache does not invalidate on, so it must re-run on every hit.
639
+ - Leave it `false` (the fail-closed default) for any enricher whose output depends on data the list cache does not invalidate on: cross-module / cross-entity reads (e.g. a product image fetched for a sales line), wall-clock-relative values (e.g. "days in stage"), or aggregates over other tables. These MUST re-run on every request so the response reflects current data.
640
+
632
641
  ### Response Enricher Rules
633
642
 
634
643
  - MUST implement `enrichMany()` for batch endpoints (prevents N+1 queries)
635
644
  - MUST namespace enriched fields with `_moduleName` prefix (e.g. `_example.todoCount`)
636
645
  - MUST use `features` array for ACL gating — enricher runs only if user has all listed features
646
+ - MUST keep `cacheableOnListHit` at `false` (default) unless the enriched output is record-pure and invalidated with the host record — opting in on a cross-module/time-relative enricher serves stale data from the shared list cache
637
647
  - Export fields are stripped: `_meta` and `_`-prefixed fields are removed from CSV/Excel exports
638
648
  - Enrichers run after `CrudHooks.afterList`, before HTTP response serialization
639
649
  - `critical: true` propagates errors to the HTTP response; `false` (default) uses fallback silently
@@ -87,7 +87,7 @@ async function preparePdfPagesForOcr(filePath) {
87
87
  pages
88
88
  };
89
89
  } finally {
90
- await pdfDocument.destroy();
90
+ await loadingTask.destroy();
91
91
  }
92
92
  }
93
93
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/pdfProcessing.ts"],
4
- "sourcesContent": ["import fs from 'fs/promises'\nimport path from 'path'\nimport { createRequire } from 'module'\n\nconst moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))\nconst pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))\nconst cMapUrl = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`\nconst standardFontDataUrl = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`\n\nconst MIN_RENDER_SCALE = 2\nconst MAX_RENDER_SCALE = 4\nconst MAX_RENDER_DIMENSION = 2200\nconst MAX_RENDER_PIXELS = 8_000_000\n\ntype PdfPageProxyLike = {\n cleanup: () => void\n getTextContent: () => Promise<{ items?: Array<{ str?: string }> }>\n getViewport: (args: { scale: number }) => { width: number; height: number }\n render: (args: { canvasContext: unknown; viewport: { width: number; height: number } }) => { promise: Promise<void> }\n}\n\ntype PdfCanvasFactoryLike = {\n create: (width: number, height: number) => {\n canvas: { toBuffer: (mimeType: string) => Buffer | Uint8Array }\n context: unknown\n }\n destroy: (canvasAndContext: {\n canvas: { toBuffer: (mimeType: string) => Buffer | Uint8Array } | null\n context: unknown\n }) => void\n}\n\ntype PdfDocumentProxyLike = {\n numPages: number\n canvasFactory: PdfCanvasFactoryLike\n getPage: (pageNumber: number) => Promise<PdfPageProxyLike>\n destroy: () => Promise<void>\n}\n\nexport type PdfPageOcrInput = {\n pageNumber: number\n extractedText: string | null\n imageBuffer: Buffer | null\n}\n\nexport type PdfOcrPreparationResult = {\n pageCount: number\n pages: PdfPageOcrInput[]\n}\n\nfunction normalizePdfText(rawText: string): string | null {\n const normalized = rawText.replace(/\\s+/g, ' ').trim()\n return normalized.length > 0 ? normalized : null\n}\n\nfunction extractPdfTextContent(items: Array<{ str?: string }> | undefined): string | null {\n if (!Array.isArray(items) || items.length === 0) return null\n const collected = items\n .map((item) => (typeof item?.str === 'string' ? item.str : ''))\n .filter((value) => value.length > 0)\n .join(' ')\n return normalizePdfText(collected)\n}\n\nfunction resolvePdfRenderScale(page: PdfPageProxyLike): number {\n const baseViewport = page.getViewport({ scale: 1 })\n const baseLargestDimension = Math.max(baseViewport.width, baseViewport.height)\n let scale = Math.min(\n MAX_RENDER_SCALE,\n Math.max(MIN_RENDER_SCALE, MAX_RENDER_DIMENSION / Math.max(baseLargestDimension, 1)),\n )\n\n let viewport = page.getViewport({ scale })\n const pixelCount = viewport.width * viewport.height\n if (pixelCount > MAX_RENDER_PIXELS) {\n scale *= Math.sqrt(MAX_RENDER_PIXELS / pixelCount)\n viewport = page.getViewport({ scale })\n }\n\n return Math.max(1, scale)\n}\n\nasync function renderPdfPageToImageBuffer(\n page: PdfPageProxyLike,\n canvasFactory: PdfCanvasFactoryLike,\n): Promise<Buffer> {\n const viewport = page.getViewport({ scale: resolvePdfRenderScale(page) })\n const canvasAndContext = canvasFactory.create(viewport.width, viewport.height)\n\n try {\n const renderTask = page.render({\n canvasContext: canvasAndContext.context,\n viewport,\n })\n await renderTask.promise\n const imageBuffer = canvasAndContext.canvas.toBuffer('image/png')\n return Buffer.isBuffer(imageBuffer) ? imageBuffer : Buffer.from(imageBuffer)\n } finally {\n canvasFactory.destroy(canvasAndContext)\n }\n}\n\nexport async function preparePdfPagesForOcr(filePath: string): Promise<PdfOcrPreparationResult> {\n const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')\n const data = new Uint8Array(await fs.readFile(filePath))\n const loadingTask = getDocument({\n data,\n cMapUrl,\n cMapPacked: true,\n standardFontDataUrl,\n })\n\n const pdfDocument = (await loadingTask.promise) as unknown as PdfDocumentProxyLike\n\n try {\n const pages: PdfPageOcrInput[] = []\n\n for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber += 1) {\n const page = await pdfDocument.getPage(pageNumber)\n\n try {\n const textContent = await page.getTextContent()\n const extractedText = extractPdfTextContent(textContent.items)\n\n if (extractedText) {\n pages.push({\n pageNumber,\n extractedText,\n imageBuffer: null,\n })\n continue\n }\n\n pages.push({\n pageNumber,\n extractedText: null,\n imageBuffer: await renderPdfPageToImageBuffer(page, pdfDocument.canvasFactory),\n })\n } finally {\n page.cleanup()\n }\n }\n\n return {\n pageCount: pdfDocument.numPages,\n pages,\n }\n } finally {\n await pdfDocument.destroy()\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import fs from 'fs/promises'\nimport path from 'path'\nimport { createRequire } from 'module'\n\nconst moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))\nconst pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))\nconst cMapUrl = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`\nconst standardFontDataUrl = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`\n\nconst MIN_RENDER_SCALE = 2\nconst MAX_RENDER_SCALE = 4\nconst MAX_RENDER_DIMENSION = 2200\nconst MAX_RENDER_PIXELS = 8_000_000\n\ntype PdfPageProxyLike = {\n cleanup: () => void\n getTextContent: () => Promise<{ items?: Array<{ str?: string }> }>\n getViewport: (args: { scale: number }) => { width: number; height: number }\n render: (args: { canvasContext: unknown; viewport: { width: number; height: number } }) => { promise: Promise<void> }\n}\n\ntype PdfCanvasFactoryLike = {\n create: (width: number, height: number) => {\n canvas: { toBuffer: (mimeType: string) => Buffer | Uint8Array }\n context: unknown\n }\n destroy: (canvasAndContext: {\n canvas: { toBuffer: (mimeType: string) => Buffer | Uint8Array } | null\n context: unknown\n }) => void\n}\n\ntype PdfDocumentProxyLike = {\n numPages: number\n canvasFactory: PdfCanvasFactoryLike\n getPage: (pageNumber: number) => Promise<PdfPageProxyLike>\n}\n\nexport type PdfPageOcrInput = {\n pageNumber: number\n extractedText: string | null\n imageBuffer: Buffer | null\n}\n\nexport type PdfOcrPreparationResult = {\n pageCount: number\n pages: PdfPageOcrInput[]\n}\n\nfunction normalizePdfText(rawText: string): string | null {\n const normalized = rawText.replace(/\\s+/g, ' ').trim()\n return normalized.length > 0 ? normalized : null\n}\n\nfunction extractPdfTextContent(items: Array<{ str?: string }> | undefined): string | null {\n if (!Array.isArray(items) || items.length === 0) return null\n const collected = items\n .map((item) => (typeof item?.str === 'string' ? item.str : ''))\n .filter((value) => value.length > 0)\n .join(' ')\n return normalizePdfText(collected)\n}\n\nfunction resolvePdfRenderScale(page: PdfPageProxyLike): number {\n const baseViewport = page.getViewport({ scale: 1 })\n const baseLargestDimension = Math.max(baseViewport.width, baseViewport.height)\n let scale = Math.min(\n MAX_RENDER_SCALE,\n Math.max(MIN_RENDER_SCALE, MAX_RENDER_DIMENSION / Math.max(baseLargestDimension, 1)),\n )\n\n let viewport = page.getViewport({ scale })\n const pixelCount = viewport.width * viewport.height\n if (pixelCount > MAX_RENDER_PIXELS) {\n scale *= Math.sqrt(MAX_RENDER_PIXELS / pixelCount)\n viewport = page.getViewport({ scale })\n }\n\n return Math.max(1, scale)\n}\n\nasync function renderPdfPageToImageBuffer(\n page: PdfPageProxyLike,\n canvasFactory: PdfCanvasFactoryLike,\n): Promise<Buffer> {\n const viewport = page.getViewport({ scale: resolvePdfRenderScale(page) })\n const canvasAndContext = canvasFactory.create(viewport.width, viewport.height)\n\n try {\n const renderTask = page.render({\n canvasContext: canvasAndContext.context,\n viewport,\n })\n await renderTask.promise\n const imageBuffer = canvasAndContext.canvas.toBuffer('image/png')\n return Buffer.isBuffer(imageBuffer) ? imageBuffer : Buffer.from(imageBuffer)\n } finally {\n canvasFactory.destroy(canvasAndContext)\n }\n}\n\nexport async function preparePdfPagesForOcr(filePath: string): Promise<PdfOcrPreparationResult> {\n const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')\n const data = new Uint8Array(await fs.readFile(filePath))\n const loadingTask = getDocument({\n data,\n cMapUrl,\n cMapPacked: true,\n standardFontDataUrl,\n })\n\n const pdfDocument = (await loadingTask.promise) as unknown as PdfDocumentProxyLike\n\n try {\n const pages: PdfPageOcrInput[] = []\n\n for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber += 1) {\n const page = await pdfDocument.getPage(pageNumber)\n\n try {\n const textContent = await page.getTextContent()\n const extractedText = extractPdfTextContent(textContent.items)\n\n if (extractedText) {\n pages.push({\n pageNumber,\n extractedText,\n imageBuffer: null,\n })\n continue\n }\n\n pages.push({\n pageNumber,\n extractedText: null,\n imageBuffer: await renderPdfPageToImageBuffer(page, pdfDocument.canvasFactory),\n })\n } finally {\n page.cleanup()\n }\n }\n\n return {\n pageCount: pdfDocument.numPages,\n pages,\n }\n } finally {\n await loadingTask.destroy()\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -59,7 +59,7 @@ async function extractPdfText(filePath) {
59
59
  }
60
60
  }
61
61
  } finally {
62
- await pdfDocument.destroy();
62
+ await loadingTask.destroy();
63
63
  }
64
64
  const result = textParts.join("\n").trim();
65
65
  return result.length > 0 ? result : null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/textExtraction.ts"],
4
- "sourcesContent": ["import fs from 'fs/promises'\nimport path from 'path'\nimport { createRequire } from 'module'\n\n// NOTE: child_process is intentionally NOT imported here.\n// This module MUST NOT shell out to any external binary for content extraction.\n// All extraction must use pure-JS libraries. See HUNT-PARSER-01.\n\nconst moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))\nconst pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))\nconst PDF_CMAP_URL = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`\nconst PDF_STANDARD_FONT_DATA_URL = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`\n\ntype ExtractParams = {\n filePath: string\n mimeType?: string | null\n}\n\nfunction isImage(mimeType?: string | null, filePath?: string | null): 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 ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff'].includes(ext)\n }\n return false\n}\n\nfunction isPlainText(mimeType: string, ext: string): boolean {\n return mimeType.startsWith('text/')\n || ext === '.txt'\n || ext === '.md'\n || ext === '.csv'\n || ext === '.log'\n}\n\nfunction isPdf(mimeType: string, ext: string): boolean {\n return mimeType === 'application/pdf' || ext === '.pdf'\n}\n\nfunction isDocx(mimeType: string, ext: string): boolean {\n // mammoth supports only Open XML .docx, not legacy binary .doc files.\n return (\n mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n || ext === '.docx'\n )\n}\n\nasync function extractPlainText(filePath: string): Promise<string | null> {\n try {\n const text = await fs.readFile(filePath, 'utf8')\n const trimmed = text.trim()\n return trimmed.length > 0 ? trimmed : null\n } catch {\n return null\n }\n}\n\nasync function extractPdfText(filePath: string): Promise<string | null> {\n try {\n const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')\n const data = new Uint8Array(await fs.readFile(filePath))\n const loadingTask = getDocument({\n data,\n cMapUrl: PDF_CMAP_URL,\n cMapPacked: true,\n standardFontDataUrl: PDF_STANDARD_FONT_DATA_URL,\n })\n const pdfDocument = await loadingTask.promise\n const textParts: string[] = []\n try {\n for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber += 1) {\n const page = await pdfDocument.getPage(pageNumber)\n try {\n const textContent = await page.getTextContent()\n const items = (textContent as any).items as Array<{ str?: string }> | undefined\n if (Array.isArray(items)) {\n const pageText = items\n .map((item) => (typeof item?.str === 'string' ? item.str : ''))\n .filter((s) => s.length > 0)\n .join(' ')\n if (pageText.trim()) textParts.push(pageText.trim())\n }\n } finally {\n page.cleanup()\n }\n }\n } finally {\n await pdfDocument.destroy()\n }\n const result = textParts.join('\\n').trim()\n return result.length > 0 ? result : null\n } catch {\n return null\n }\n}\n\nasync function extractDocxText(filePath: string): Promise<string | null> {\n try {\n const mammoth = await import('mammoth')\n const result = await mammoth.extractRawText({ path: filePath })\n const trimmed = result.value.trim()\n return trimmed.length > 0 ? trimmed : null\n } catch {\n return null\n }\n}\n\nexport async function extractAttachmentContent(params: ExtractParams): Promise<string | null> {\n const { filePath, mimeType } = params\n if (!filePath) return null\n if (isImage(mimeType, filePath)) return null\n\n const normalized = (mimeType || '').toLowerCase()\n const ext = path.extname(filePath).toLowerCase()\n\n if (isPlainText(normalized, ext)) {\n return extractPlainText(filePath)\n }\n\n if (isPdf(normalized, ext)) {\n return extractPdfText(filePath)\n }\n\n if (isDocx(normalized, ext)) {\n return extractDocxText(filePath)\n }\n\n // XLSX, PPTX, MSG and other Office formats: no safe pure-JS extractor available yet.\n // Return null rather than shelling out to an external binary.\n return null\n}\n"],
4
+ "sourcesContent": ["import fs from 'fs/promises'\nimport path from 'path'\nimport { createRequire } from 'module'\n\n// NOTE: child_process is intentionally NOT imported here.\n// This module MUST NOT shell out to any external binary for content extraction.\n// All extraction must use pure-JS libraries. See HUNT-PARSER-01.\n\nconst moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))\nconst pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))\nconst PDF_CMAP_URL = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`\nconst PDF_STANDARD_FONT_DATA_URL = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`\n\ntype ExtractParams = {\n filePath: string\n mimeType?: string | null\n}\n\nfunction isImage(mimeType?: string | null, filePath?: string | null): 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 ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff'].includes(ext)\n }\n return false\n}\n\nfunction isPlainText(mimeType: string, ext: string): boolean {\n return mimeType.startsWith('text/')\n || ext === '.txt'\n || ext === '.md'\n || ext === '.csv'\n || ext === '.log'\n}\n\nfunction isPdf(mimeType: string, ext: string): boolean {\n return mimeType === 'application/pdf' || ext === '.pdf'\n}\n\nfunction isDocx(mimeType: string, ext: string): boolean {\n // mammoth supports only Open XML .docx, not legacy binary .doc files.\n return (\n mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n || ext === '.docx'\n )\n}\n\nasync function extractPlainText(filePath: string): Promise<string | null> {\n try {\n const text = await fs.readFile(filePath, 'utf8')\n const trimmed = text.trim()\n return trimmed.length > 0 ? trimmed : null\n } catch {\n return null\n }\n}\n\nasync function extractPdfText(filePath: string): Promise<string | null> {\n try {\n const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')\n const data = new Uint8Array(await fs.readFile(filePath))\n const loadingTask = getDocument({\n data,\n cMapUrl: PDF_CMAP_URL,\n cMapPacked: true,\n standardFontDataUrl: PDF_STANDARD_FONT_DATA_URL,\n })\n const pdfDocument = await loadingTask.promise\n const textParts: string[] = []\n try {\n for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber += 1) {\n const page = await pdfDocument.getPage(pageNumber)\n try {\n const textContent = await page.getTextContent()\n const items = (textContent as any).items as Array<{ str?: string }> | undefined\n if (Array.isArray(items)) {\n const pageText = items\n .map((item) => (typeof item?.str === 'string' ? item.str : ''))\n .filter((s) => s.length > 0)\n .join(' ')\n if (pageText.trim()) textParts.push(pageText.trim())\n }\n } finally {\n page.cleanup()\n }\n }\n } finally {\n await loadingTask.destroy()\n }\n const result = textParts.join('\\n').trim()\n return result.length > 0 ? result : null\n } catch {\n return null\n }\n}\n\nasync function extractDocxText(filePath: string): Promise<string | null> {\n try {\n const mammoth = await import('mammoth')\n const result = await mammoth.extractRawText({ path: filePath })\n const trimmed = result.value.trim()\n return trimmed.length > 0 ? trimmed : null\n } catch {\n return null\n }\n}\n\nexport async function extractAttachmentContent(params: ExtractParams): Promise<string | null> {\n const { filePath, mimeType } = params\n if (!filePath) return null\n if (isImage(mimeType, filePath)) return null\n\n const normalized = (mimeType || '').toLowerCase()\n const ext = path.extname(filePath).toLowerCase()\n\n if (isPlainText(normalized, ext)) {\n return extractPlainText(filePath)\n }\n\n if (isPdf(normalized, ext)) {\n return extractPdfText(filePath)\n }\n\n if (isDocx(normalized, ext)) {\n return extractDocxText(filePath)\n }\n\n // XLSX, PPTX, MSG and other Office formats: no safe pure-JS extractor available yet.\n // Return null rather than shelling out to an external binary.\n return null\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  const features = [
2
2
  { id: "feature_toggles.view", title: "View feature toggles", module: "feature_toggles" },
3
- { id: "feature_toggles.manage", title: "Manage feature toggles", module: "feature_toggles" }
3
+ { id: "feature_toggles.manage", title: "Manage per-tenant feature toggle overrides", module: "feature_toggles" },
4
+ { id: "feature_toggles.global.manage", title: "Manage system-wide global feature toggles", module: "feature_toggles" }
4
5
  ];
5
6
  var acl_default = features;
6
7
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/feature_toggles/acl.ts"],
4
- "sourcesContent": ["export const features = [\n { id: 'feature_toggles.view', title: 'View feature toggles', module: 'feature_toggles' },\n { id: 'feature_toggles.manage', title: 'Manage feature toggles', module: 'feature_toggles' },\n]\n\nexport default features\n"],
5
- "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,wBAAwB,OAAO,wBAAwB,QAAQ,kBAAkB;AAAA,EACvF,EAAE,IAAI,0BAA0B,OAAO,0BAA0B,QAAQ,kBAAkB;AAC7F;AAEA,IAAO,cAAQ;",
4
+ "sourcesContent": ["export const features = [\n { id: 'feature_toggles.view', title: 'View feature toggles', module: 'feature_toggles' },\n { id: 'feature_toggles.manage', title: 'Manage per-tenant feature toggle overrides', module: 'feature_toggles' },\n { id: 'feature_toggles.global.manage', title: 'Manage system-wide global feature toggles', module: 'feature_toggles' },\n]\n\nexport default features\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,wBAAwB,OAAO,wBAAwB,QAAQ,kBAAkB;AAAA,EACvF,EAAE,IAAI,0BAA0B,OAAO,8CAA8C,QAAQ,kBAAkB;AAAA,EAC/G,EAAE,IAAI,iCAAiC,OAAO,6CAA6C,QAAQ,kBAAkB;AACvH;AAEA,IAAO,cAAQ;",
6
6
  "names": []
7
7
  }
@@ -23,9 +23,11 @@ const listQuerySchema = z.object({
23
23
  }).passthrough();
24
24
  const routeMetadata = {
25
25
  GET: { requireAuth: true, requireFeatures: ["feature_toggles.view"] },
26
- POST: { requireAuth: true, requireFeatures: ["feature_toggles.manage"] },
27
- PUT: { requireAuth: true, requireFeatures: ["feature_toggles.manage"] },
28
- DELETE: { requireAuth: true, requireFeatures: ["feature_toggles.manage"] }
26
+ // Global feature toggles are platform-wide (no tenant_id); writing them is
27
+ // restricted to super administrators via the dedicated global feature.
28
+ POST: { requireAuth: true, requireFeatures: ["feature_toggles.global.manage"] },
29
+ PUT: { requireAuth: true, requireFeatures: ["feature_toggles.global.manage"] },
30
+ DELETE: { requireAuth: true, requireFeatures: ["feature_toggles.global.manage"] }
29
31
  };
30
32
  const listFields = [
31
33
  "id",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/feature_toggles/api/global/route.ts"],
4
- "sourcesContent": ["import { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { FeatureToggle } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { toggleTypeSchema, toggleCreateSchema, toggleUpdateSchema } from '../../data/validators'\nimport {\n featureTogglesTag,\n featureToggleListResponseSchema,\n featureToggleErrorSchema\n} from '../openapi'\n\nconst rawBodySchema = z.object({}).passthrough()\nconst listQuerySchema = z\n .object({\n page: z.coerce.number().min(1).default(1).describe('Page number for pagination'),\n pageSize: z.coerce.number().min(1).max(200).default(50).describe('Number of items per page (max 200)'),\n search: z.string().optional().describe('Case-insensitive search across identifier, name, description, and category'),\n type: toggleTypeSchema.optional().describe('Filter by toggle type (boolean, string, number, json)'),\n category: z.string().optional().describe('Filter by category (case-insensitive partial match)'),\n name: z.string().optional().describe('Filter by name (case-insensitive partial match)'),\n identifier: z.string().optional().describe('Filter by identifier (case-insensitive partial match)'),\n sortField: z.enum(['id', 'category', 'identifier', 'name', 'createdAt', 'updatedAt', 'type']).optional().describe('Field to sort by'),\n sortDir: z.enum(['asc', 'desc']).optional().describe('Sort direction (ascending or descending)'),\n })\n .passthrough()\n\ntype FeatureToggleListQuery = z.infer<typeof listQuerySchema>\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['feature_toggles.view'] },\n POST: { requireAuth: true, requireFeatures: ['feature_toggles.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['feature_toggles.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['feature_toggles.manage'] },\n}\n\nconst listFields = [\n 'id',\n 'identifier',\n 'name',\n 'description',\n 'category',\n 'type',\n 'default_value',\n 'created_at',\n 'updated_at',\n]\n\nconst buildFilters = (query: FeatureToggleListQuery): Record<string, unknown> => {\n const filters: Record<string, unknown> = {}\n const search = query.search?.trim()\n if (search && search.length > 0) {\n const escaped = escapeLikePattern(search)\n const pattern = `%${escaped}%`\n filters.$or = [\n { identifier: { $ilike: pattern } },\n { name: { $ilike: pattern } },\n { description: { $ilike: pattern } },\n { category: { $ilike: pattern } },\n ]\n }\n const category = query.category?.trim()\n if (category && category.length > 0) {\n filters.category = { $ilike: `%${escapeLikePattern(category)}%` }\n }\n const name = query.name?.trim()\n if (name && name.length > 0) {\n filters.name = { $ilike: `%${escapeLikePattern(name)}%` }\n }\n const identifier = query.identifier?.trim()\n if (identifier && identifier.length > 0) {\n filters.identifier = { $ilike: `%${escapeLikePattern(identifier)}%` }\n }\n const type = query.type?.trim()\n if (type && type.length > 0) {\n filters.type = { $eq: query.type }\n }\n return filters\n}\n\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: FeatureToggle,\n idField: 'id',\n orgField: null,\n tenantField: \"tenantId\",\n softDeleteField: 'deletedAt'\n },\n indexer: { entityType: E.feature_toggles.feature_toggle },\n list: {\n schema: listQuerySchema,\n entityId: E.feature_toggles.feature_toggle,\n fields: listFields,\n sortFieldMap: {\n id: 'id',\n category: 'category',\n identifier: 'identifier',\n name: 'name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n type: 'type',\n },\n transformItem: (item: Record<string, unknown>) => {\n if (!item) return item\n return {\n id: item.id,\n identifier: item.identifier,\n name: item.name,\n description: item.description ?? null,\n category: item.category ?? null,\n type: item.type,\n defaultValue: item.default_value,\n created_at: item.created_at,\n updated_at: item.updated_at,\n }\n },\n buildFilters: async (query) => buildFilters(query),\n },\n actions: {\n create: {\n commandId: 'feature_toggles.global.create',\n schema: rawBodySchema,\n response: ({ result }) => ({ id: result?.toggleId ?? result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'feature_toggles.global.update',\n schema: rawBodySchema,\n response: ({ result }) => ({ id: result?.toggleId ?? result?.id ?? null }),\n status: 200,\n },\n delete: {\n commandId: 'feature_toggles.global.delete',\n schema: rawBodySchema,\n response: ({ result }) => ({ id: result?.toggleId ?? result?.id ?? null }),\n status: 200,\n },\n }\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst createResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst updateResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst deleteResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: featureTogglesTag,\n summary: 'Global feature toggle management',\n methods: {\n GET: {\n summary: 'List global feature toggles',\n description: 'Returns all global feature toggles with filtering and pagination. Requires superadmin role.',\n query: listQuerySchema,\n responses: [\n { status: 200, description: 'Feature toggles collection', schema: featureToggleListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: featureToggleErrorSchema },\n { status: 401, description: 'Unauthorized', schema: featureToggleErrorSchema },\n { status: 403, description: 'Forbidden - superadmin role required', schema: featureToggleErrorSchema },\n ],\n },\n POST: {\n summary: 'Create global feature toggle',\n description: 'Creates a new global feature toggle. Requires superadmin role.',\n requestBody: {\n contentType: 'application/json',\n schema: toggleCreateSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Feature toggle created',\n schema: createResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: featureToggleErrorSchema },\n { status: 401, description: 'Unauthorized', schema: featureToggleErrorSchema },\n { status: 403, description: 'Forbidden - superadmin role required', schema: featureToggleErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update global feature toggle',\n description: 'Updates an existing global feature toggle. Requires superadmin role.',\n requestBody: {\n contentType: 'application/json',\n schema: toggleUpdateSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Feature toggle updated',\n schema: updateResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: featureToggleErrorSchema },\n { status: 401, description: 'Unauthorized', schema: featureToggleErrorSchema },\n { status: 403, description: 'Forbidden - superadmin role required', schema: featureToggleErrorSchema },\n { status: 404, description: 'Feature toggle not found', schema: featureToggleErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete global feature toggle',\n description: 'Soft deletes a global feature toggle by ID. Requires superadmin role.',\n query: z.object({ id: z.string().uuid().describe('Feature toggle identifier') }),\n responses: [\n { status: 200, description: 'Feature toggle deleted', schema: deleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifier', schema: featureToggleErrorSchema },\n { status: 401, description: 'Unauthorized', schema: featureToggleErrorSchema },\n { status: 403, description: 'Forbidden - superadmin role required', schema: featureToggleErrorSchema },\n { status: 404, description: 'Feature toggle not found', schema: featureToggleErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { FeatureToggle } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { toggleTypeSchema, toggleCreateSchema, toggleUpdateSchema } from '../../data/validators'\nimport {\n featureTogglesTag,\n featureToggleListResponseSchema,\n featureToggleErrorSchema\n} from '../openapi'\n\nconst rawBodySchema = z.object({}).passthrough()\nconst listQuerySchema = z\n .object({\n page: z.coerce.number().min(1).default(1).describe('Page number for pagination'),\n pageSize: z.coerce.number().min(1).max(200).default(50).describe('Number of items per page (max 200)'),\n search: z.string().optional().describe('Case-insensitive search across identifier, name, description, and category'),\n type: toggleTypeSchema.optional().describe('Filter by toggle type (boolean, string, number, json)'),\n category: z.string().optional().describe('Filter by category (case-insensitive partial match)'),\n name: z.string().optional().describe('Filter by name (case-insensitive partial match)'),\n identifier: z.string().optional().describe('Filter by identifier (case-insensitive partial match)'),\n sortField: z.enum(['id', 'category', 'identifier', 'name', 'createdAt', 'updatedAt', 'type']).optional().describe('Field to sort by'),\n sortDir: z.enum(['asc', 'desc']).optional().describe('Sort direction (ascending or descending)'),\n })\n .passthrough()\n\ntype FeatureToggleListQuery = z.infer<typeof listQuerySchema>\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['feature_toggles.view'] },\n // Global feature toggles are platform-wide (no tenant_id); writing them is\n // restricted to super administrators via the dedicated global feature.\n POST: { requireAuth: true, requireFeatures: ['feature_toggles.global.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['feature_toggles.global.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['feature_toggles.global.manage'] },\n}\n\nconst listFields = [\n 'id',\n 'identifier',\n 'name',\n 'description',\n 'category',\n 'type',\n 'default_value',\n 'created_at',\n 'updated_at',\n]\n\nconst buildFilters = (query: FeatureToggleListQuery): Record<string, unknown> => {\n const filters: Record<string, unknown> = {}\n const search = query.search?.trim()\n if (search && search.length > 0) {\n const escaped = escapeLikePattern(search)\n const pattern = `%${escaped}%`\n filters.$or = [\n { identifier: { $ilike: pattern } },\n { name: { $ilike: pattern } },\n { description: { $ilike: pattern } },\n { category: { $ilike: pattern } },\n ]\n }\n const category = query.category?.trim()\n if (category && category.length > 0) {\n filters.category = { $ilike: `%${escapeLikePattern(category)}%` }\n }\n const name = query.name?.trim()\n if (name && name.length > 0) {\n filters.name = { $ilike: `%${escapeLikePattern(name)}%` }\n }\n const identifier = query.identifier?.trim()\n if (identifier && identifier.length > 0) {\n filters.identifier = { $ilike: `%${escapeLikePattern(identifier)}%` }\n }\n const type = query.type?.trim()\n if (type && type.length > 0) {\n filters.type = { $eq: query.type }\n }\n return filters\n}\n\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: FeatureToggle,\n idField: 'id',\n orgField: null,\n tenantField: \"tenantId\",\n softDeleteField: 'deletedAt'\n },\n indexer: { entityType: E.feature_toggles.feature_toggle },\n list: {\n schema: listQuerySchema,\n entityId: E.feature_toggles.feature_toggle,\n fields: listFields,\n sortFieldMap: {\n id: 'id',\n category: 'category',\n identifier: 'identifier',\n name: 'name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n type: 'type',\n },\n transformItem: (item: Record<string, unknown>) => {\n if (!item) return item\n return {\n id: item.id,\n identifier: item.identifier,\n name: item.name,\n description: item.description ?? null,\n category: item.category ?? null,\n type: item.type,\n defaultValue: item.default_value,\n created_at: item.created_at,\n updated_at: item.updated_at,\n }\n },\n buildFilters: async (query) => buildFilters(query),\n },\n actions: {\n create: {\n commandId: 'feature_toggles.global.create',\n schema: rawBodySchema,\n response: ({ result }) => ({ id: result?.toggleId ?? result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'feature_toggles.global.update',\n schema: rawBodySchema,\n response: ({ result }) => ({ id: result?.toggleId ?? result?.id ?? null }),\n status: 200,\n },\n delete: {\n commandId: 'feature_toggles.global.delete',\n schema: rawBodySchema,\n response: ({ result }) => ({ id: result?.toggleId ?? result?.id ?? null }),\n status: 200,\n },\n }\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst createResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst updateResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst deleteResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: featureTogglesTag,\n summary: 'Global feature toggle management',\n methods: {\n GET: {\n summary: 'List global feature toggles',\n description: 'Returns all global feature toggles with filtering and pagination. Requires superadmin role.',\n query: listQuerySchema,\n responses: [\n { status: 200, description: 'Feature toggles collection', schema: featureToggleListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: featureToggleErrorSchema },\n { status: 401, description: 'Unauthorized', schema: featureToggleErrorSchema },\n { status: 403, description: 'Forbidden - superadmin role required', schema: featureToggleErrorSchema },\n ],\n },\n POST: {\n summary: 'Create global feature toggle',\n description: 'Creates a new global feature toggle. Requires superadmin role.',\n requestBody: {\n contentType: 'application/json',\n schema: toggleCreateSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Feature toggle created',\n schema: createResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: featureToggleErrorSchema },\n { status: 401, description: 'Unauthorized', schema: featureToggleErrorSchema },\n { status: 403, description: 'Forbidden - superadmin role required', schema: featureToggleErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update global feature toggle',\n description: 'Updates an existing global feature toggle. Requires superadmin role.',\n requestBody: {\n contentType: 'application/json',\n schema: toggleUpdateSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Feature toggle updated',\n schema: updateResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: featureToggleErrorSchema },\n { status: 401, description: 'Unauthorized', schema: featureToggleErrorSchema },\n { status: 403, description: 'Forbidden - superadmin role required', schema: featureToggleErrorSchema },\n { status: 404, description: 'Feature toggle not found', schema: featureToggleErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete global feature toggle',\n description: 'Soft deletes a global feature toggle by ID. Requires superadmin role.',\n query: z.object({ id: z.string().uuid().describe('Feature toggle identifier') }),\n responses: [\n { status: 200, description: 'Feature toggle deleted', schema: deleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifier', schema: featureToggleErrorSchema },\n { status: 401, description: 'Unauthorized', schema: featureToggleErrorSchema },\n { status: 403, description: 'Forbidden - superadmin role required', schema: featureToggleErrorSchema },\n { status: 404, description: 'Feature toggle not found', schema: featureToggleErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -81,7 +81,13 @@ function buildCommandContext(container) {
81
81
  organizationScope: null,
82
82
  selectedOrganizationId: null,
83
83
  organizationIds: null,
84
- request: void 0
84
+ request: void 0,
85
+ // CLI invocations (seed-defaults, toggle-create/update/delete) are trusted
86
+ // server-side calls with no end-user actor. Global feature toggles are a
87
+ // platform-wide table whose writes are restricted to super admins (#2266);
88
+ // this flag lets the command-level guard recognize the system caller so
89
+ // `yarn initialize` can seed defaults without an authenticated super admin.
90
+ systemActor: true
85
91
  };
86
92
  }
87
93
  const createToggle = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/feature_toggles/cli.ts"],
4
- "sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { FeatureToggle } from './data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { toggleCreateSchemaList } from './data/validators'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype ParsedArgs = Record<string, string | boolean>\n\n// ESM equivalent of __dirname\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\nconst defaultFilePath = path.resolve(__dirname, 'defaults.json')\n\nfunction parseArgs(rest: string[]): ParsedArgs {\n const args: ParsedArgs = {}\n for (let index = 0; index < rest.length; index += 1) {\n const part = rest[index]\n if (!part?.startsWith('--')) continue\n const [rawKey, rawValue] = part.slice(2).split('=')\n if (!rawKey) continue\n if (rawValue !== undefined) {\n args[rawKey] = rawValue\n continue\n }\n const next = rest[index + 1]\n if (next && !next.startsWith('--')) {\n args[rawKey] = next\n index += 1\n continue\n }\n args[rawKey] = true\n }\n return args\n}\n\nfunction stringOption(args: ParsedArgs, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw !== 'string') continue\n const trimmed = raw.trim()\n if (trimmed.length > 0) return trimmed\n }\n return undefined\n}\n\nfunction booleanOption(args: ParsedArgs, ...keys: string[]): boolean | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (raw === undefined) continue\n if (raw === true) return true\n if (raw === false) return false\n if (typeof raw === 'string') {\n const trimmed = raw.trim()\n if (!trimmed) return true\n const parsed = parseBooleanToken(trimmed)\n if (parsed !== null) return parsed\n }\n }\n return undefined\n}\n\nfunction parseValue(type: string, value: string | undefined): any {\n if (value === undefined) return undefined\n\n switch (type) {\n case 'boolean':\n if (value === 'true' || value === '1') return true\n if (value === 'false' || value === '0') return false\n return Boolean(value)\n case 'number':\n const num = Number(value)\n if (isNaN(num)) throw new Error(`Invalid number value: ${value}`)\n return num\n case 'json':\n try {\n return JSON.parse(value)\n } catch (e) {\n throw new Error(`Invalid JSON value: ${value}`)\n }\n case 'string':\n default:\n return value\n }\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: null,\n organizationIds: null,\n request: undefined as any,\n }\n}\n\nconst createToggle: ModuleCli = {\n command: 'toggle-create',\n async run(rest) {\n const args = parseArgs(rest)\n const identifier = stringOption(args, 'identifier', 'id')\n const name = stringOption(args, 'name')\n\n if (!identifier || !name) {\n console.error('Usage: mercato feature_toggles toggle-create --identifier <id> --name <name> [--type boolean|string|number|json] [--defaultValue <value>] [--category <value>] [--description <value>]')\n return\n }\n\n const type = stringOption(args, 'type') || 'boolean'\n const defaultValueRaw = stringOption(args, 'defaultValue')\n const category = stringOption(args, 'category')\n const description = stringOption(args, 'description')\n\n\n let defaultValue: any = undefined\n try {\n defaultValue = parseValue(type, defaultValueRaw)\n } catch (e: any) {\n console.error(e.message)\n return\n }\n\n const container = await createRequestContainer()\n try {\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container)\n const { result } = await commandBus.execute('feature_toggles.global.create', {\n input: {\n identifier,\n name,\n type,\n defaultValue: defaultValue,\n category: category ?? null,\n description: description ?? null,\n\n },\n ctx,\n })\n console.log('\u2705 Feature toggle created:', \"Identifier: \" + identifier)\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst updateToggle: ModuleCli = {\n command: 'toggle-update',\n async run(rest) {\n const args = parseArgs(rest)\n const identifier = stringOption(args, 'identifier')\n\n if (!identifier) {\n console.error('Usage: mercato feature_toggles toggle-update --identifier <id> [--name <name>] [--defaultValue <value>] [--category <value>] [--description <value>]')\n return\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const toggle = await em.findOne(FeatureToggle, { identifier })\n if (!toggle) {\n console.error('Feature toggle not found:', identifier)\n return\n }\n\n const name = stringOption(args, 'name')\n const defaultValueRaw = stringOption(args, 'defaultValue')\n const category = stringOption(args, 'category')\n const description = stringOption(args, 'description')\n\n\n let defaultValue: any = undefined\n if (defaultValueRaw !== undefined) {\n try {\n defaultValue = parseValue(toggle.type, defaultValueRaw)\n } catch (e: any) {\n console.error(e.message)\n return\n }\n }\n\n const toggleId = toggle.id\n try {\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container)\n await commandBus.execute('feature_toggles.global.update', {\n input: {\n ...(toggleId ? { id: toggleId } : {}),\n ...(identifier ? { identifier } : {}),\n ...(name ? { name } : {}),\n ...(defaultValue !== undefined ? { defaultValue } : {}),\n ...(category !== undefined ? { category } : {}),\n ...(description !== undefined ? { description } : {}),\n\n },\n ctx,\n })\n console.log('\u2705 Feature toggle updated:', \"Identifier: \" + identifier)\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst deleteToggle: ModuleCli = {\n command: 'toggle-delete',\n async run(rest) {\n const args = parseArgs(rest)\n const identifier = stringOption(args, 'identifier')\n if (!identifier) {\n console.error('Usage: mercato feature_toggles toggle-delete --identifier <id>')\n return\n }\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const toggle = await em.findOne(FeatureToggle, { identifier })\n if (!toggle) {\n console.error('Feature toggle not found:', identifier)\n return\n }\n const toggleId = toggle.id\n try {\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container)\n await commandBus.execute('feature_toggles.global.delete', {\n input: {\n ...(toggleId ? { id: toggleId } : {}),\n ...(identifier ? { identifier } : {}),\n },\n ctx,\n })\n console.log('\u2705 Feature toggle deleted:', identifier ?? toggleId)\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst setOverrideValue: ModuleCli = {\n command: 'override-set-value',\n async run(rest) {\n const args = parseArgs(rest)\n const identifier = stringOption(args, 'identifier')\n const tenantId = stringOption(args, 'tenantId', 'tenant', 'tenantId')\n const valueRaw = stringOption(args, 'value')\n\n if (!identifier || !tenantId) {\n console.error('Usage: mercato feature_toggles override-set-value --identifier <id> --tenantId <uuid> --value <value>')\n return\n }\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const toggle = await em.findOne(FeatureToggle, { identifier })\n if (!toggle) {\n console.error('Feature toggle not found:', identifier)\n return\n }\n\n let value: any = undefined\n if (valueRaw !== undefined) {\n try {\n value = parseValue(toggle.type, valueRaw)\n } catch (e: any) {\n console.error(e.message)\n return\n }\n }\n\n const toggleId = toggle.id\n\n try {\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container)\n await commandBus.execute('feature_toggles.overrides.changeState', {\n input: {\n toggleId,\n tenantId,\n isOverride: true,\n overrideValue: value,\n },\n ctx,\n })\n console.log('\u2705 Feature toggle override updated:', identifier, \"Tenant ID: \" + tenantId, \"Value: \" + (valueRaw || 'unchanged'))\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst seedDefaults: ModuleCli = {\n command: 'seed-defaults',\n async run(rest) {\n const args = parseArgs(rest)\n const filePathFromArgs = stringOption(args, 'filePath')\n const filePath = filePathFromArgs ?? defaultFilePath\n const raw = fs.readFileSync(filePath, 'utf8')\n const parsedJson = JSON.parse(raw)\n const data = Array.isArray(parsedJson) ? parsedJson : parsedJson.toggles\n\n // We can't strictly use toggleCreateSchemaList because it might validate types strictly, \n // but the input JSON from file might have raw values that need refinement if we were going via CLI args.\n // However, since we are reading from JSON, we can assume the types match what zod expects for the literal types.\n // Let's rely on the schema to validate the structure.\n const toggles = toggleCreateSchemaList.parse(data)\n\n const container = await createRequestContainer()\n const commandBus = container.resolve('commandBus') as CommandBus\n const em = container.resolve('em') as EntityManager\n const ctx = buildCommandContext(container)\n let created = 0\n let skipped = 0\n\n for (const toggle of toggles) {\n const existing = await em.findOne(FeatureToggle, { identifier: toggle.identifier, deletedAt: null })\n if (existing) {\n skipped += 1\n continue\n }\n await commandBus.execute('feature_toggles.global.create', {\n input: {\n identifier: toggle.identifier,\n name: toggle.name,\n description: toggle.description ?? null,\n category: toggle.category ?? null,\n\n type: toggle.type,\n defaultValue: toggle.defaultValue,\n },\n ctx,\n })\n created += 1\n console.log(`\u2705 Created feature toggle ${toggle.identifier}`)\n }\n console.log(`\u2705 Feature toggle defaults seeded (created: ${created}, skipped: ${skipped})`)\n },\n}\n\nexport default [createToggle, updateToggle, deleteToggle, setOverrideValue, seedDefaults]\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { FeatureToggle } from './data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { toggleCreateSchemaList } from './data/validators'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype ParsedArgs = Record<string, string | boolean>\n\n// ESM equivalent of __dirname\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\nconst defaultFilePath = path.resolve(__dirname, 'defaults.json')\n\nfunction parseArgs(rest: string[]): ParsedArgs {\n const args: ParsedArgs = {}\n for (let index = 0; index < rest.length; index += 1) {\n const part = rest[index]\n if (!part?.startsWith('--')) continue\n const [rawKey, rawValue] = part.slice(2).split('=')\n if (!rawKey) continue\n if (rawValue !== undefined) {\n args[rawKey] = rawValue\n continue\n }\n const next = rest[index + 1]\n if (next && !next.startsWith('--')) {\n args[rawKey] = next\n index += 1\n continue\n }\n args[rawKey] = true\n }\n return args\n}\n\nfunction stringOption(args: ParsedArgs, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw !== 'string') continue\n const trimmed = raw.trim()\n if (trimmed.length > 0) return trimmed\n }\n return undefined\n}\n\nfunction booleanOption(args: ParsedArgs, ...keys: string[]): boolean | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (raw === undefined) continue\n if (raw === true) return true\n if (raw === false) return false\n if (typeof raw === 'string') {\n const trimmed = raw.trim()\n if (!trimmed) return true\n const parsed = parseBooleanToken(trimmed)\n if (parsed !== null) return parsed\n }\n }\n return undefined\n}\n\nfunction parseValue(type: string, value: string | undefined): any {\n if (value === undefined) return undefined\n\n switch (type) {\n case 'boolean':\n if (value === 'true' || value === '1') return true\n if (value === 'false' || value === '0') return false\n return Boolean(value)\n case 'number':\n const num = Number(value)\n if (isNaN(num)) throw new Error(`Invalid number value: ${value}`)\n return num\n case 'json':\n try {\n return JSON.parse(value)\n } catch (e) {\n throw new Error(`Invalid JSON value: ${value}`)\n }\n case 'string':\n default:\n return value\n }\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: null,\n organizationIds: null,\n request: undefined as any,\n // CLI invocations (seed-defaults, toggle-create/update/delete) are trusted\n // server-side calls with no end-user actor. Global feature toggles are a\n // platform-wide table whose writes are restricted to super admins (#2266);\n // this flag lets the command-level guard recognize the system caller so\n // `yarn initialize` can seed defaults without an authenticated super admin.\n systemActor: true,\n }\n}\n\nconst createToggle: ModuleCli = {\n command: 'toggle-create',\n async run(rest) {\n const args = parseArgs(rest)\n const identifier = stringOption(args, 'identifier', 'id')\n const name = stringOption(args, 'name')\n\n if (!identifier || !name) {\n console.error('Usage: mercato feature_toggles toggle-create --identifier <id> --name <name> [--type boolean|string|number|json] [--defaultValue <value>] [--category <value>] [--description <value>]')\n return\n }\n\n const type = stringOption(args, 'type') || 'boolean'\n const defaultValueRaw = stringOption(args, 'defaultValue')\n const category = stringOption(args, 'category')\n const description = stringOption(args, 'description')\n\n\n let defaultValue: any = undefined\n try {\n defaultValue = parseValue(type, defaultValueRaw)\n } catch (e: any) {\n console.error(e.message)\n return\n }\n\n const container = await createRequestContainer()\n try {\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container)\n const { result } = await commandBus.execute('feature_toggles.global.create', {\n input: {\n identifier,\n name,\n type,\n defaultValue: defaultValue,\n category: category ?? null,\n description: description ?? null,\n\n },\n ctx,\n })\n console.log('\u2705 Feature toggle created:', \"Identifier: \" + identifier)\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst updateToggle: ModuleCli = {\n command: 'toggle-update',\n async run(rest) {\n const args = parseArgs(rest)\n const identifier = stringOption(args, 'identifier')\n\n if (!identifier) {\n console.error('Usage: mercato feature_toggles toggle-update --identifier <id> [--name <name>] [--defaultValue <value>] [--category <value>] [--description <value>]')\n return\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const toggle = await em.findOne(FeatureToggle, { identifier })\n if (!toggle) {\n console.error('Feature toggle not found:', identifier)\n return\n }\n\n const name = stringOption(args, 'name')\n const defaultValueRaw = stringOption(args, 'defaultValue')\n const category = stringOption(args, 'category')\n const description = stringOption(args, 'description')\n\n\n let defaultValue: any = undefined\n if (defaultValueRaw !== undefined) {\n try {\n defaultValue = parseValue(toggle.type, defaultValueRaw)\n } catch (e: any) {\n console.error(e.message)\n return\n }\n }\n\n const toggleId = toggle.id\n try {\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container)\n await commandBus.execute('feature_toggles.global.update', {\n input: {\n ...(toggleId ? { id: toggleId } : {}),\n ...(identifier ? { identifier } : {}),\n ...(name ? { name } : {}),\n ...(defaultValue !== undefined ? { defaultValue } : {}),\n ...(category !== undefined ? { category } : {}),\n ...(description !== undefined ? { description } : {}),\n\n },\n ctx,\n })\n console.log('\u2705 Feature toggle updated:', \"Identifier: \" + identifier)\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst deleteToggle: ModuleCli = {\n command: 'toggle-delete',\n async run(rest) {\n const args = parseArgs(rest)\n const identifier = stringOption(args, 'identifier')\n if (!identifier) {\n console.error('Usage: mercato feature_toggles toggle-delete --identifier <id>')\n return\n }\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const toggle = await em.findOne(FeatureToggle, { identifier })\n if (!toggle) {\n console.error('Feature toggle not found:', identifier)\n return\n }\n const toggleId = toggle.id\n try {\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container)\n await commandBus.execute('feature_toggles.global.delete', {\n input: {\n ...(toggleId ? { id: toggleId } : {}),\n ...(identifier ? { identifier } : {}),\n },\n ctx,\n })\n console.log('\u2705 Feature toggle deleted:', identifier ?? toggleId)\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst setOverrideValue: ModuleCli = {\n command: 'override-set-value',\n async run(rest) {\n const args = parseArgs(rest)\n const identifier = stringOption(args, 'identifier')\n const tenantId = stringOption(args, 'tenantId', 'tenant', 'tenantId')\n const valueRaw = stringOption(args, 'value')\n\n if (!identifier || !tenantId) {\n console.error('Usage: mercato feature_toggles override-set-value --identifier <id> --tenantId <uuid> --value <value>')\n return\n }\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const toggle = await em.findOne(FeatureToggle, { identifier })\n if (!toggle) {\n console.error('Feature toggle not found:', identifier)\n return\n }\n\n let value: any = undefined\n if (valueRaw !== undefined) {\n try {\n value = parseValue(toggle.type, valueRaw)\n } catch (e: any) {\n console.error(e.message)\n return\n }\n }\n\n const toggleId = toggle.id\n\n try {\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container)\n await commandBus.execute('feature_toggles.overrides.changeState', {\n input: {\n toggleId,\n tenantId,\n isOverride: true,\n overrideValue: value,\n },\n ctx,\n })\n console.log('\u2705 Feature toggle override updated:', identifier, \"Tenant ID: \" + tenantId, \"Value: \" + (valueRaw || 'unchanged'))\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst seedDefaults: ModuleCli = {\n command: 'seed-defaults',\n async run(rest) {\n const args = parseArgs(rest)\n const filePathFromArgs = stringOption(args, 'filePath')\n const filePath = filePathFromArgs ?? defaultFilePath\n const raw = fs.readFileSync(filePath, 'utf8')\n const parsedJson = JSON.parse(raw)\n const data = Array.isArray(parsedJson) ? parsedJson : parsedJson.toggles\n\n // We can't strictly use toggleCreateSchemaList because it might validate types strictly, \n // but the input JSON from file might have raw values that need refinement if we were going via CLI args.\n // However, since we are reading from JSON, we can assume the types match what zod expects for the literal types.\n // Let's rely on the schema to validate the structure.\n const toggles = toggleCreateSchemaList.parse(data)\n\n const container = await createRequestContainer()\n const commandBus = container.resolve('commandBus') as CommandBus\n const em = container.resolve('em') as EntityManager\n const ctx = buildCommandContext(container)\n let created = 0\n let skipped = 0\n\n for (const toggle of toggles) {\n const existing = await em.findOne(FeatureToggle, { identifier: toggle.identifier, deletedAt: null })\n if (existing) {\n skipped += 1\n continue\n }\n await commandBus.execute('feature_toggles.global.create', {\n input: {\n identifier: toggle.identifier,\n name: toggle.name,\n description: toggle.description ?? null,\n category: toggle.category ?? null,\n\n type: toggle.type,\n defaultValue: toggle.defaultValue,\n },\n ctx,\n })\n created += 1\n console.log(`\u2705 Created feature toggle ${toggle.identifier}`)\n }\n console.log(`\u2705 Feature toggle defaults seeded (created: ${created}, skipped: ${skipped})`)\n },\n}\n\nexport default [createToggle, updateToggle, deleteToggle, setOverrideValue, seedDefaults]\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,12 @@ import { registerCommand } from "@open-mercato/shared/lib/commands";
5
5
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
6
  import { buildChanges, requireId } from "@open-mercato/shared/lib/commands/helpers";
7
7
  import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
8
+ function assertGlobalToggleSuperAdmin(ctx) {
9
+ if (ctx.systemActor === true) return;
10
+ if (ctx.auth?.isSuperAdmin !== true) {
11
+ throw new CrudHttpError(403, { error: "Global feature toggles can only be managed by a super administrator." });
12
+ }
13
+ }
8
14
  async function loadToggleSnapshot(em, id) {
9
15
  const toggle = await em.findOne(FeatureToggle, { id });
10
16
  if (!toggle) return null;
@@ -30,6 +36,7 @@ async function loadOverrideSnapshots(em, toggleId) {
30
36
  const createToggleCommand = {
31
37
  id: "feature_toggles.global.create",
32
38
  async execute(rawInput, ctx) {
39
+ assertGlobalToggleSuperAdmin(ctx);
33
40
  const parsed = toggleCreateSchema.parse(rawInput);
34
41
  const em = ctx.container.resolve("em").fork();
35
42
  const toggle = em.create(FeatureToggle, {
@@ -90,6 +97,7 @@ const updateToggleCommand = {
90
97
  return snapshot ? { before: snapshot } : {};
91
98
  },
92
99
  async execute(rawInput, ctx) {
100
+ assertGlobalToggleSuperAdmin(ctx);
93
101
  const parsed = toggleUpdateSchema.parse(rawInput);
94
102
  const em = ctx.container.resolve("em").fork();
95
103
  const toggle = await em.findOne(FeatureToggle, { id: parsed.id });
@@ -179,6 +187,7 @@ const deleteToggleCommand = {
179
187
  return snapshot ? { before: snapshot, overrides } : {};
180
188
  },
181
189
  async execute(input, ctx) {
190
+ assertGlobalToggleSuperAdmin(ctx);
182
191
  const id = requireId(input, "Feature toggle id required");
183
192
  const em = ctx.container.resolve("em").fork();
184
193
  const toggle = await em.findOne(FeatureToggle, { id });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/feature_toggles/commands/global.ts"],
4
- "sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { FeatureToggle, FeatureToggleOverride } from '../data/entities'\nimport { ToggleCreateInput, toggleCreateSchema, ToggleUpdateInput, toggleUpdateSchema } from '../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { FeatureTogglesService } from '../lib/feature-flag-check'\n\ntype ToggleSnapshot = {\n id: string\n identifier: string\n name: string\n description: string | null\n category: string | null\n type: 'boolean' | 'string' | 'number' | 'json'\n defaultValue: any\n}\n\ntype OverrideSnapshot = {\n id: string\n toggleId: string\n tenantId: string\n value?: any\n}\n\ntype ToggleUndoPayload = {\n after?: ToggleSnapshot | null\n before?: ToggleSnapshot | null\n overrides?: OverrideSnapshot[]\n}\n\nasync function loadToggleSnapshot(em: EntityManager, id: string): Promise<ToggleSnapshot | null> {\n const toggle = await em.findOne(FeatureToggle, { id })\n if (!toggle) return null\n return {\n id: toggle.id,\n identifier: toggle.identifier,\n name: toggle.name,\n description: toggle.description ?? null,\n category: toggle.category ?? null,\n type: toggle.type ?? 'boolean',\n defaultValue: toggle.defaultValue ?? null,\n }\n}\n\nasync function loadOverrideSnapshots(em: EntityManager, toggleId: string): Promise<OverrideSnapshot[]> {\n const overrides = await em.find(FeatureToggleOverride, { toggle: toggleId })\n return overrides.map(o => ({\n id: o.id,\n toggleId: o.toggle.id,\n tenantId: o.tenantId,\n value: o.value,\n }))\n}\n\nconst createToggleCommand: CommandHandler<ToggleCreateInput, { toggleId: string }> = {\n id: 'feature_toggles.global.create',\n async execute(rawInput, ctx) {\n const parsed = toggleCreateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const toggle = em.create(FeatureToggle, {\n identifier: parsed.identifier,\n name: parsed.name,\n description: parsed.description,\n category: parsed.category,\n type: parsed.type,\n defaultValue: parsed.defaultValue,\n })\n em.persist(toggle)\n await em.flush()\n\n return { toggleId: toggle.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return await loadToggleSnapshot(em, result.toggleId)\n },\n buildLog: async ({ result, ctx }) => {\n const { translate } = await resolveTranslations()\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadToggleSnapshot(em, result.toggleId)\n return {\n actionLabel: translate('feature_toggles.audit.toggles.create', 'Create toggle'),\n resourceKind: 'feature_toggles.global',\n resourceId: result.toggleId,\n snapshotAfter: snapshot ?? null,\n payload: {\n undo: {\n after: snapshot ?? null,\n } satisfies ToggleUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const toggleId = logEntry?.resourceId ?? null\n if (!toggleId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const overrides = await em.find(FeatureToggleOverride, { toggle: toggleId })\n if (overrides.length > 0) {\n em.remove(overrides)\n }\n const toggle = await em.findOne(FeatureToggle, { id: toggleId })\n if (toggle) {\n em.remove(toggle)\n await em.flush()\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n }\n }\n}\n\nconst updateToggleCommand: CommandHandler<ToggleUpdateInput, { toggleId: string }> = {\n id: 'feature_toggles.global.update',\n async prepare(rawInput, ctx) {\n const parsed = toggleUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadToggleSnapshot(em, parsed.id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = toggleUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const toggle = await em.findOne(FeatureToggle, { id: parsed.id })\n if (!toggle) throw new CrudHttpError(404, { error: 'Toggle not found' })\n toggle.identifier = parsed.identifier ?? toggle.identifier\n toggle.name = parsed.name ?? toggle.name\n toggle.description = parsed.description ?? toggle.description\n toggle.category = parsed.category ?? toggle.category\n toggle.type = parsed.type ?? toggle.type\n toggle.defaultValue = parsed.defaultValue ?? toggle.defaultValue\n await em.flush()\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n return { toggleId: toggle.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as ToggleSnapshot | undefined\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const afterSnapshot = await loadToggleSnapshot(em, before.id)\n const changes =\n afterSnapshot && before\n ? buildChanges(\n before as unknown as Record<string, unknown>,\n afterSnapshot as unknown as Record<string, unknown>,\n [\n 'identifier',\n 'name',\n 'description',\n 'category',\n 'failMode',\n 'type',\n 'defaultValue',\n ]\n )\n : {}\n\n return {\n actionLabel: translate('feature_toggles.audit.toggles.update', 'Update toggle'),\n resourceKind: 'feature_toggles.global',\n resourceId: before.id,\n snapshotBefore: before,\n snapshotAfter: afterSnapshot ?? null,\n changes,\n payload: {\n undo: {\n before,\n after: afterSnapshot ?? null,\n } satisfies ToggleUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ToggleUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let toggle = await em.findOne(FeatureToggle, { id: before.id })\n if (!toggle) {\n toggle = em.create(FeatureToggle, {\n id: before.id,\n identifier: before.identifier,\n name: before.name,\n description: before.description,\n category: before.category,\n type: before.type,\n defaultValue: before.defaultValue,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(toggle)\n } else {\n toggle.identifier = before.identifier\n toggle.name = before.name\n toggle.description = before.description\n toggle.category = before.category\n }\n await em.flush()\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n }\n}\n\nconst deleteToggleCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, { toggleId: string }> =\n{\n id: 'feature_toggles.global.delete',\n async prepare(input, ctx) {\n const id = requireId(input, 'Feature toggle id required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadToggleSnapshot(em, id)\n const overrides = await loadOverrideSnapshots(em, id)\n return snapshot ? { before: snapshot, overrides } : {}\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Feature toggle id required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const toggle = await em.findOne(FeatureToggle, { id })\n if (!toggle) throw new CrudHttpError(404, { error: 'Feature toggle not found' })\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n\n const overrides = await em.find(FeatureToggleOverride, { toggle: toggle.id })\n if (overrides.length > 0) {\n em.remove(overrides)\n }\n\n em.remove(toggle)\n await em.flush()\n\n return { toggleId: toggle.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as ToggleSnapshot | undefined\n const overrides = (snapshots as any).overrides as OverrideSnapshot[] | undefined\n\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('feature_toggles.audit.toggles.delete', 'Delete toggle'),\n resourceKind: 'feature_toggles.global',\n resourceId: before.id,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n overrides,\n } satisfies ToggleUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ToggleUndoPayload>(logEntry)\n const before = payload?.before\n const overrides = payload?.overrides || []\n\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let toggle = await em.findOne(FeatureToggle, { id: before.id })\n if (!toggle) {\n toggle = em.create(FeatureToggle, {\n id: before.id,\n identifier: before.identifier,\n name: before.name,\n description: before.description,\n category: before.category,\n type: before.type,\n defaultValue: before.defaultValue,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(toggle)\n\n for (const ov of overrides) {\n const override = em.create(FeatureToggleOverride, {\n id: ov.id,\n toggle: toggle,\n tenantId: ov.tenantId,\n value: ov.value ?? null,\n })\n em.persist(override)\n }\n } else {\n toggle.identifier = before.identifier\n toggle.name = before.name\n toggle.description = before.description\n toggle.category = before.category\n }\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n await em.flush()\n },\n}\n\nregisterCommand(createToggleCommand)\nregisterCommand(updateToggleCommand)\nregisterCommand(deleteToggleCommand)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { FeatureToggle, FeatureToggleOverride } from '../data/entities'\nimport { ToggleCreateInput, toggleCreateSchema, ToggleUpdateInput, toggleUpdateSchema } from '../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { FeatureTogglesService } from '../lib/feature-flag-check'\n\nfunction assertGlobalToggleSuperAdmin(ctx: { auth?: { [key: string]: unknown } | null; systemActor?: boolean }): void {\n // Trusted server-side callers (CLI seed-defaults/toggle-*, tenant setup) run\n // without an authenticated actor and opt in via `systemActor`. HTTP request\n // paths never set it and always carry a real `auth` actor, so an authenticated\n // but non-super-admin caller \u2014 the cross-tenant escalation vector (#2266) \u2014\n // stays denied.\n if (ctx.systemActor === true) return\n if (ctx.auth?.isSuperAdmin !== true) {\n throw new CrudHttpError(403, { error: 'Global feature toggles can only be managed by a super administrator.' })\n }\n}\n\ntype ToggleSnapshot = {\n id: string\n identifier: string\n name: string\n description: string | null\n category: string | null\n type: 'boolean' | 'string' | 'number' | 'json'\n defaultValue: any\n}\n\ntype OverrideSnapshot = {\n id: string\n toggleId: string\n tenantId: string\n value?: any\n}\n\ntype ToggleUndoPayload = {\n after?: ToggleSnapshot | null\n before?: ToggleSnapshot | null\n overrides?: OverrideSnapshot[]\n}\n\nasync function loadToggleSnapshot(em: EntityManager, id: string): Promise<ToggleSnapshot | null> {\n const toggle = await em.findOne(FeatureToggle, { id })\n if (!toggle) return null\n return {\n id: toggle.id,\n identifier: toggle.identifier,\n name: toggle.name,\n description: toggle.description ?? null,\n category: toggle.category ?? null,\n type: toggle.type ?? 'boolean',\n defaultValue: toggle.defaultValue ?? null,\n }\n}\n\nasync function loadOverrideSnapshots(em: EntityManager, toggleId: string): Promise<OverrideSnapshot[]> {\n const overrides = await em.find(FeatureToggleOverride, { toggle: toggleId })\n return overrides.map(o => ({\n id: o.id,\n toggleId: o.toggle.id,\n tenantId: o.tenantId,\n value: o.value,\n }))\n}\n\nconst createToggleCommand: CommandHandler<ToggleCreateInput, { toggleId: string }> = {\n id: 'feature_toggles.global.create',\n async execute(rawInput, ctx) {\n assertGlobalToggleSuperAdmin(ctx)\n const parsed = toggleCreateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const toggle = em.create(FeatureToggle, {\n identifier: parsed.identifier,\n name: parsed.name,\n description: parsed.description,\n category: parsed.category,\n type: parsed.type,\n defaultValue: parsed.defaultValue,\n })\n em.persist(toggle)\n await em.flush()\n\n return { toggleId: toggle.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return await loadToggleSnapshot(em, result.toggleId)\n },\n buildLog: async ({ result, ctx }) => {\n const { translate } = await resolveTranslations()\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadToggleSnapshot(em, result.toggleId)\n return {\n actionLabel: translate('feature_toggles.audit.toggles.create', 'Create toggle'),\n resourceKind: 'feature_toggles.global',\n resourceId: result.toggleId,\n snapshotAfter: snapshot ?? null,\n payload: {\n undo: {\n after: snapshot ?? null,\n } satisfies ToggleUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const toggleId = logEntry?.resourceId ?? null\n if (!toggleId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const overrides = await em.find(FeatureToggleOverride, { toggle: toggleId })\n if (overrides.length > 0) {\n em.remove(overrides)\n }\n const toggle = await em.findOne(FeatureToggle, { id: toggleId })\n if (toggle) {\n em.remove(toggle)\n await em.flush()\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n }\n }\n}\n\nconst updateToggleCommand: CommandHandler<ToggleUpdateInput, { toggleId: string }> = {\n id: 'feature_toggles.global.update',\n async prepare(rawInput, ctx) {\n const parsed = toggleUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadToggleSnapshot(em, parsed.id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n assertGlobalToggleSuperAdmin(ctx)\n const parsed = toggleUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const toggle = await em.findOne(FeatureToggle, { id: parsed.id })\n if (!toggle) throw new CrudHttpError(404, { error: 'Toggle not found' })\n toggle.identifier = parsed.identifier ?? toggle.identifier\n toggle.name = parsed.name ?? toggle.name\n toggle.description = parsed.description ?? toggle.description\n toggle.category = parsed.category ?? toggle.category\n toggle.type = parsed.type ?? toggle.type\n toggle.defaultValue = parsed.defaultValue ?? toggle.defaultValue\n await em.flush()\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n return { toggleId: toggle.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as ToggleSnapshot | undefined\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const afterSnapshot = await loadToggleSnapshot(em, before.id)\n const changes =\n afterSnapshot && before\n ? buildChanges(\n before as unknown as Record<string, unknown>,\n afterSnapshot as unknown as Record<string, unknown>,\n [\n 'identifier',\n 'name',\n 'description',\n 'category',\n 'failMode',\n 'type',\n 'defaultValue',\n ]\n )\n : {}\n\n return {\n actionLabel: translate('feature_toggles.audit.toggles.update', 'Update toggle'),\n resourceKind: 'feature_toggles.global',\n resourceId: before.id,\n snapshotBefore: before,\n snapshotAfter: afterSnapshot ?? null,\n changes,\n payload: {\n undo: {\n before,\n after: afterSnapshot ?? null,\n } satisfies ToggleUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ToggleUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let toggle = await em.findOne(FeatureToggle, { id: before.id })\n if (!toggle) {\n toggle = em.create(FeatureToggle, {\n id: before.id,\n identifier: before.identifier,\n name: before.name,\n description: before.description,\n category: before.category,\n type: before.type,\n defaultValue: before.defaultValue,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(toggle)\n } else {\n toggle.identifier = before.identifier\n toggle.name = before.name\n toggle.description = before.description\n toggle.category = before.category\n }\n await em.flush()\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n }\n}\n\nconst deleteToggleCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, { toggleId: string }> =\n{\n id: 'feature_toggles.global.delete',\n async prepare(input, ctx) {\n const id = requireId(input, 'Feature toggle id required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadToggleSnapshot(em, id)\n const overrides = await loadOverrideSnapshots(em, id)\n return snapshot ? { before: snapshot, overrides } : {}\n },\n async execute(input, ctx) {\n assertGlobalToggleSuperAdmin(ctx)\n const id = requireId(input, 'Feature toggle id required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const toggle = await em.findOne(FeatureToggle, { id })\n if (!toggle) throw new CrudHttpError(404, { error: 'Feature toggle not found' })\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n\n const overrides = await em.find(FeatureToggleOverride, { toggle: toggle.id })\n if (overrides.length > 0) {\n em.remove(overrides)\n }\n\n em.remove(toggle)\n await em.flush()\n\n return { toggleId: toggle.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as ToggleSnapshot | undefined\n const overrides = (snapshots as any).overrides as OverrideSnapshot[] | undefined\n\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('feature_toggles.audit.toggles.delete', 'Delete toggle'),\n resourceKind: 'feature_toggles.global',\n resourceId: before.id,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n overrides,\n } satisfies ToggleUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ToggleUndoPayload>(logEntry)\n const before = payload?.before\n const overrides = payload?.overrides || []\n\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let toggle = await em.findOne(FeatureToggle, { id: before.id })\n if (!toggle) {\n toggle = em.create(FeatureToggle, {\n id: before.id,\n identifier: before.identifier,\n name: before.name,\n description: before.description,\n category: before.category,\n type: before.type,\n defaultValue: before.defaultValue,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(toggle)\n\n for (const ov of overrides) {\n const override = em.create(FeatureToggleOverride, {\n id: ov.id,\n toggle: toggle,\n tenantId: ov.tenantId,\n value: ov.value ?? null,\n })\n em.persist(override)\n }\n } else {\n toggle.identifier = before.identifier\n toggle.name = before.name\n toggle.description = before.description\n toggle.category = before.category\n }\n const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService\n await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)\n await em.flush()\n },\n}\n\nregisterCommand(createToggleCommand)\nregisterCommand(updateToggleCommand)\nregisterCommand(deleteToggleCommand)\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,13 @@
1
1
  const setup = {
2
2
  defaultRoleFeatures: {
3
- admin: ["feature_toggles.*"]
3
+ // Global feature toggles are a platform-wide (non-tenant-scoped) table, so
4
+ // creating/updating/deleting them is restricted to super administrators.
5
+ superadmin: ["feature_toggles.global.manage"],
6
+ // Tenant admins may view global toggles and manage their own per-tenant
7
+ // overrides, but MUST NOT mutate the shared global toggle definitions.
8
+ // Granted explicitly (not via `feature_toggles.*`) so the wildcard does not
9
+ // implicitly cover `feature_toggles.global.manage`.
10
+ admin: ["feature_toggles.view", "feature_toggles.manage"]
4
11
  }
5
12
  };
6
13
  var setup_default = setup;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/feature_toggles/setup.ts"],
4
- "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: ['feature_toggles.*'],\n },\n}\n\nexport default setup\n"],
5
- "mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO,CAAC,mBAAmB;AAAA,EAC7B;AACF;AAEA,IAAO,gBAAQ;",
4
+ "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n // Global feature toggles are a platform-wide (non-tenant-scoped) table, so\n // creating/updating/deleting them is restricted to super administrators.\n superadmin: ['feature_toggles.global.manage'],\n // Tenant admins may view global toggles and manage their own per-tenant\n // overrides, but MUST NOT mutate the shared global toggle definitions.\n // Granted explicitly (not via `feature_toggles.*`) so the wildcard does not\n // implicitly cover `feature_toggles.global.manage`.\n admin: ['feature_toggles.view', 'feature_toggles.manage'],\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA;AAAA;AAAA,IAGnB,YAAY,CAAC,+BAA+B;AAAA;AAAA;AAAA;AAAA;AAAA,IAK5C,OAAO,CAAC,wBAAwB,wBAAwB;AAAA,EAC1D;AACF;AAEA,IAAO,gBAAQ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.4-develop.4282.1.4d95e85930",
3
+ "version": "0.6.4-develop.4305.1.efaf0ebab1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -227,46 +227,46 @@
227
227
  }
228
228
  },
229
229
  "dependencies": {
230
- "@mikro-orm/core": "^7.1.1",
231
- "@mikro-orm/decorators": "^7.1.1",
232
- "@mikro-orm/postgresql": "^7.1.1",
233
- "@xyflow/react": "^12.10.2",
234
- "ai": "^6.0.191",
235
- "date-fns": "4.3.0",
230
+ "@mikro-orm/core": "^7.1.3",
231
+ "@mikro-orm/decorators": "^7.1.3",
232
+ "@mikro-orm/postgresql": "^7.1.3",
233
+ "@xyflow/react": "^12.11.0",
234
+ "ai": "^6.0.194",
235
+ "date-fns": "4.4.0",
236
236
  "date-fns-tz": "^3.2.0",
237
237
  "html-to-text": "^10.0.0",
238
238
  "mammoth": "^1.9.0",
239
- "pdfjs-dist": "^5.7.284",
239
+ "pdfjs-dist": "^6.0.227",
240
240
  "semver": "^7.8.1",
241
- "svix": "^1.92.2",
241
+ "svix": "^1.95.1",
242
242
  "ts-pattern": "^5.0.0",
243
243
  "zod": "^4.4.3"
244
244
  },
245
245
  "peerDependencies": {
246
- "@open-mercato/ai-assistant": "0.6.4-develop.4282.1.4d95e85930",
247
- "@open-mercato/shared": "0.6.4-develop.4282.1.4d95e85930",
248
- "@open-mercato/ui": "0.6.4-develop.4282.1.4d95e85930",
246
+ "@open-mercato/ai-assistant": "0.6.4-develop.4305.1.efaf0ebab1",
247
+ "@open-mercato/shared": "0.6.4-develop.4305.1.efaf0ebab1",
248
+ "@open-mercato/ui": "0.6.4-develop.4305.1.efaf0ebab1",
249
249
  "react": "^19.0.0",
250
250
  "react-dom": "^19.0.0"
251
251
  },
252
252
  "devDependencies": {
253
- "@open-mercato/ai-assistant": "0.6.4-develop.4282.1.4d95e85930",
254
- "@open-mercato/shared": "0.6.4-develop.4282.1.4d95e85930",
255
- "@open-mercato/ui": "0.6.4-develop.4282.1.4d95e85930",
253
+ "@open-mercato/ai-assistant": "0.6.4-develop.4305.1.efaf0ebab1",
254
+ "@open-mercato/shared": "0.6.4-develop.4305.1.efaf0ebab1",
255
+ "@open-mercato/ui": "0.6.4-develop.4305.1.efaf0ebab1",
256
256
  "@testing-library/dom": "^10.4.1",
257
257
  "@testing-library/jest-dom": "^6.9.1",
258
258
  "@testing-library/react": "^16.3.1",
259
259
  "@types/chance": "^1.1.8",
260
260
  "@types/html-to-text": "^9.0.4",
261
261
  "@types/jest": "^30.0.0",
262
- "@types/react": "^19.2.15",
262
+ "@types/react": "^19.2.16",
263
263
  "@types/react-dom": "^19.2.3",
264
264
  "@types/semver": "^7.5.8",
265
265
  "chance": "^1.1.13",
266
266
  "jest": "^30.4.2",
267
267
  "jest-environment-jsdom": "^30.4.1",
268
- "react": "19.2.6",
269
- "react-dom": "19.2.6",
268
+ "react": "19.2.7",
269
+ "react-dom": "19.2.7",
270
270
  "ts-jest": "^29.4.11"
271
271
  },
272
272
  "publishConfig": {
@@ -34,7 +34,6 @@ type PdfDocumentProxyLike = {
34
34
  numPages: number
35
35
  canvasFactory: PdfCanvasFactoryLike
36
36
  getPage: (pageNumber: number) => Promise<PdfPageProxyLike>
37
- destroy: () => Promise<void>
38
37
  }
39
38
 
40
39
  export type PdfPageOcrInput = {
@@ -146,6 +145,6 @@ export async function preparePdfPagesForOcr(filePath: string): Promise<PdfOcrPre
146
145
  pages,
147
146
  }
148
147
  } finally {
149
- await pdfDocument.destroy()
148
+ await loadingTask.destroy()
150
149
  }
151
150
  }
@@ -86,7 +86,7 @@ async function extractPdfText(filePath: string): Promise<string | null> {
86
86
  }
87
87
  }
88
88
  } finally {
89
- await pdfDocument.destroy()
89
+ await loadingTask.destroy()
90
90
  }
91
91
  const result = textParts.join('\n').trim()
92
92
  return result.length > 0 ? result : null
@@ -1,6 +1,7 @@
1
1
  export const features = [
2
2
  { id: 'feature_toggles.view', title: 'View feature toggles', module: 'feature_toggles' },
3
- { id: 'feature_toggles.manage', title: 'Manage feature toggles', module: 'feature_toggles' },
3
+ { id: 'feature_toggles.manage', title: 'Manage per-tenant feature toggle overrides', module: 'feature_toggles' },
4
+ { id: 'feature_toggles.global.manage', title: 'Manage system-wide global feature toggles', module: 'feature_toggles' },
4
5
  ]
5
6
 
6
7
  export default features
@@ -30,9 +30,11 @@ type FeatureToggleListQuery = z.infer<typeof listQuerySchema>
30
30
 
31
31
  const routeMetadata = {
32
32
  GET: { requireAuth: true, requireFeatures: ['feature_toggles.view'] },
33
- POST: { requireAuth: true, requireFeatures: ['feature_toggles.manage'] },
34
- PUT: { requireAuth: true, requireFeatures: ['feature_toggles.manage'] },
35
- DELETE: { requireAuth: true, requireFeatures: ['feature_toggles.manage'] },
33
+ // Global feature toggles are platform-wide (no tenant_id); writing them is
34
+ // restricted to super administrators via the dedicated global feature.
35
+ POST: { requireAuth: true, requireFeatures: ['feature_toggles.global.manage'] },
36
+ PUT: { requireAuth: true, requireFeatures: ['feature_toggles.global.manage'] },
37
+ DELETE: { requireAuth: true, requireFeatures: ['feature_toggles.global.manage'] },
36
38
  }
37
39
 
38
40
  const listFields = [
@@ -95,6 +95,12 @@ function buildCommandContext(container: Awaited<ReturnType<typeof createRequestC
95
95
  selectedOrganizationId: null,
96
96
  organizationIds: null,
97
97
  request: undefined as any,
98
+ // CLI invocations (seed-defaults, toggle-create/update/delete) are trusted
99
+ // server-side calls with no end-user actor. Global feature toggles are a
100
+ // platform-wide table whose writes are restricted to super admins (#2266);
101
+ // this flag lets the command-level guard recognize the system caller so
102
+ // `yarn initialize` can seed defaults without an authenticated super admin.
103
+ systemActor: true,
98
104
  }
99
105
  }
100
106
 
@@ -9,6 +9,18 @@ import { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpe
9
9
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
10
10
  import { FeatureTogglesService } from '../lib/feature-flag-check'
11
11
 
12
+ function assertGlobalToggleSuperAdmin(ctx: { auth?: { [key: string]: unknown } | null; systemActor?: boolean }): void {
13
+ // Trusted server-side callers (CLI seed-defaults/toggle-*, tenant setup) run
14
+ // without an authenticated actor and opt in via `systemActor`. HTTP request
15
+ // paths never set it and always carry a real `auth` actor, so an authenticated
16
+ // but non-super-admin caller — the cross-tenant escalation vector (#2266) —
17
+ // stays denied.
18
+ if (ctx.systemActor === true) return
19
+ if (ctx.auth?.isSuperAdmin !== true) {
20
+ throw new CrudHttpError(403, { error: 'Global feature toggles can only be managed by a super administrator.' })
21
+ }
22
+ }
23
+
12
24
  type ToggleSnapshot = {
13
25
  id: string
14
26
  identifier: string
@@ -59,6 +71,7 @@ async function loadOverrideSnapshots(em: EntityManager, toggleId: string): Promi
59
71
  const createToggleCommand: CommandHandler<ToggleCreateInput, { toggleId: string }> = {
60
72
  id: 'feature_toggles.global.create',
61
73
  async execute(rawInput, ctx) {
74
+ assertGlobalToggleSuperAdmin(ctx)
62
75
  const parsed = toggleCreateSchema.parse(rawInput)
63
76
  const em = (ctx.container.resolve('em') as EntityManager).fork()
64
77
  const toggle = em.create(FeatureToggle, {
@@ -121,6 +134,7 @@ const updateToggleCommand: CommandHandler<ToggleUpdateInput, { toggleId: string
121
134
  return snapshot ? { before: snapshot } : {}
122
135
  },
123
136
  async execute(rawInput, ctx) {
137
+ assertGlobalToggleSuperAdmin(ctx)
124
138
  const parsed = toggleUpdateSchema.parse(rawInput)
125
139
  const em = (ctx.container.resolve('em') as EntityManager).fork()
126
140
  const toggle = await em.findOne(FeatureToggle, { id: parsed.id })
@@ -216,6 +230,7 @@ const deleteToggleCommand: CommandHandler<{ body?: Record<string, unknown>; quer
216
230
  return snapshot ? { before: snapshot, overrides } : {}
217
231
  },
218
232
  async execute(input, ctx) {
233
+ assertGlobalToggleSuperAdmin(ctx)
219
234
  const id = requireId(input, 'Feature toggle id required')
220
235
  const em = (ctx.container.resolve('em') as EntityManager).fork()
221
236
  const toggle = await em.findOne(FeatureToggle, { id })
@@ -2,7 +2,14 @@ import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
2
2
 
3
3
  export const setup: ModuleSetupConfig = {
4
4
  defaultRoleFeatures: {
5
- admin: ['feature_toggles.*'],
5
+ // Global feature toggles are a platform-wide (non-tenant-scoped) table, so
6
+ // creating/updating/deleting them is restricted to super administrators.
7
+ superadmin: ['feature_toggles.global.manage'],
8
+ // Tenant admins may view global toggles and manage their own per-tenant
9
+ // overrides, but MUST NOT mutate the shared global toggle definitions.
10
+ // Granted explicitly (not via `feature_toggles.*`) so the wildcard does not
11
+ // implicitly cover `feature_toggles.global.manage`.
12
+ admin: ['feature_toggles.view', 'feature_toggles.manage'],
6
13
  },
7
14
  }
8
15