create-mercato-app 0.6.2-develop.3461.1.605f31c2c9 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mercato-app",
3
- "version": "0.6.2-develop.3461.1.605f31c2c9",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "node build.mjs",
16
- "test": "node --import tsx --test src/**/*.test.ts",
16
+ "test": "node --import tsx --test --test-timeout=120000 src/**/*.test.ts",
17
17
  "typecheck": "tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
@@ -21,10 +21,10 @@
21
21
  "tar": "^7.5.15"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/node": "^25.6.2",
24
+ "@types/node": "^25.9.0",
25
25
  "esbuild": "^0.28.0",
26
- "tsx": "^4.21.0",
27
- "typescript": "^5.9.3"
26
+ "tsx": "^4.22.2",
27
+ "typescript": "^6.0.3"
28
28
  },
29
29
  "publishConfig": {
30
30
  "access": "public"
@@ -41,6 +41,5 @@
41
41
  "scaffolding",
42
42
  "cli"
43
43
  ],
44
- "license": "MIT",
45
- "stableVersion": "0.6.1"
44
+ "license": "MIT"
46
45
  }
@@ -55,6 +55,13 @@ OM_ENABLE_ENTERPRISE_MODULES_SSO=false
55
55
  # Requires OM_ENABLE_ENTERPRISE_MODULES=true
56
56
  OM_ENABLE_ENTERPRISE_MODULES_SECURITY=false
57
57
 
58
+ # Enable the S3-compatible storage provider module (default: false).
59
+ # When true, `storage_s3` is added to enabledModules in modules.ts and the
60
+ # Integration Marketplace exposes the "S3 Object Storage" card.
61
+ # The provider can also be preconfigured from env vars — see the
62
+ # "S3 Storage Preconfiguration" block at the bottom of this file.
63
+ OM_ENABLE_STORAGE_S3=false
64
+
58
65
  # OM_SECURITY_MFA_SETUP_SECRET=change-me-mfa-setup-secret
59
66
  OM_SECURITY_TOTP_ISSUER=Open Mercato
60
67
  OM_SECURITY_TOTP_WINDOW=1
@@ -451,6 +458,52 @@ OCR_MODEL=gpt-4o
451
458
  # OM_INTEGRATION_AKENEO_CATEGORIES_SETTINGS_JSON=
452
459
  # OM_INTEGRATION_AKENEO_ATTRIBUTES_SETTINGS_JSON=
453
460
 
461
+ # ============================================================================
462
+ # S3 Storage Preconfiguration
463
+ # ============================================================================
464
+ # If these credentials are set, the storage_s3 provider preconfigures itself
465
+ # during tenant setup and can be rerun later (for existing tenants) with:
466
+ # yarn mercato storage_s3 configure-from-env --tenant <tenantId> --org <organizationId>
467
+ #
468
+ # For unattended deploys (Dokploy, Coolify, Kamal, etc.), use --all-tenants
469
+ # as a post-deploy hook to apply the preset to every active organization
470
+ # without tracking UUIDs in your deploy config:
471
+ # yarn mercato storage_s3 configure-from-env --all-tenants
472
+ #
473
+ # The CLI exits non-zero on an incomplete preset so CI/automation can detect
474
+ # misconfiguration. Pass --force (or set OM_INTEGRATION_STORAGE_S3_FORCE_PRECONFIGURE=true)
475
+ # to overwrite credentials that have already been stored for the tenant.
476
+ #
477
+ # The module itself is only loaded when OM_ENABLE_STORAGE_S3=true (see above)
478
+ # — without that flag the preconfiguration block below is ignored.
479
+ #
480
+ # Required for auto-preconfiguration:
481
+ # OM_INTEGRATION_STORAGE_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
482
+ # OM_INTEGRATION_STORAGE_S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
483
+ # OM_INTEGRATION_STORAGE_S3_REGION=eu-central-1
484
+ # OM_INTEGRATION_STORAGE_S3_BUCKET=my-mercato-attachments
485
+ #
486
+ # Optional behavior:
487
+ # OM_INTEGRATION_STORAGE_S3_SESSION_TOKEN=
488
+ # OM_INTEGRATION_STORAGE_S3_ENDPOINT=https://fra1.digitaloceanspaces.com
489
+ # OM_INTEGRATION_STORAGE_S3_FORCE_PATH_STYLE=false
490
+ # OM_INTEGRATION_STORAGE_S3_FORCE_PRECONFIGURE=false
491
+ #
492
+ # Notes:
493
+ # - SESSION_TOKEN is only needed for short-lived STS credentials.
494
+ # - Leave ENDPOINT blank for AWS S3. Set it for MinIO, DigitalOcean Spaces,
495
+ # Cloudflare R2, Backblaze B2, etc.
496
+ # - FORCE_PATH_STYLE must be true for MinIO and most self-hosted S3 services.
497
+ # - Leave the block commented out when S3 should not be preconfigured.
498
+ # OM_INTEGRATION_STORAGE_S3_ACCESS_KEY_ID=
499
+ # OM_INTEGRATION_STORAGE_S3_SECRET_ACCESS_KEY=
500
+ # OM_INTEGRATION_STORAGE_S3_REGION=
501
+ # OM_INTEGRATION_STORAGE_S3_BUCKET=
502
+ # OM_INTEGRATION_STORAGE_S3_SESSION_TOKEN=
503
+ # OM_INTEGRATION_STORAGE_S3_ENDPOINT=
504
+ # OM_INTEGRATION_STORAGE_S3_FORCE_PATH_STYLE=false
505
+ # OM_INTEGRATION_STORAGE_S3_FORCE_PRECONFIGURE=false
506
+
454
507
  # ============================================================================
455
508
  # InboxOps Configuration (email-to-ERP agent)
456
509
  # ============================================================================
@@ -147,6 +147,17 @@ This is a Next.js 16 application built on the **Open Mercato** modular ERP frame
147
147
  - `src/bootstrap.ts` - Application initialization (imports generated files, registers i18n)
148
148
  - `.mercato/generated/` - Auto-generated files from `yarn generate` (do not edit manually)
149
149
 
150
+ ### Module Overrides
151
+
152
+ Use the unified `entry.overrides` field in `src/modules.ts` when this app needs to replace or disable a contract from a package-backed module without forking it. The template ships a non-applied `moduleOverrideExamples` object covering AI, routes, events, workers, widgets, notifications, interceptors, setup, ACL, DI, and encryption. Copy only the specific domains you need into the target module entry's `overrides` field.
153
+
154
+ Rules:
155
+
156
+ - `null` disables a matching contract; an object/function definition replaces it.
157
+ - API route keys are `'METHOD /api/path'`; page route keys are `'/backend/path'` or `'/frontend/path'`.
158
+ - `setup` overrides apply to the module entry carrying them, not to a separate setup id map.
159
+ - The standard `src/bootstrap.ts` already calls `applyModuleOverridesFromEnabledModules(enabledModules)` before registries load.
160
+
150
161
  ### Routing Structure
151
162
 
152
163
  - `/backend/*` - Admin panel routes (AppShell with sidebar navigation)
@@ -248,7 +259,7 @@ export const aiAgentOverrides: AiAgentOverridesMap = {
248
259
  }
249
260
  ```
250
261
 
251
- Example `modules.ts` inline override (preferred for app-level decisions that don't deserve a fake module). AI lives at `overrides.ai.*`; other domains (routes, events, workers, widgets, …) reuse the same `entry.overrides` umbrella per the [unified spec](https://github.com/open-mercato/open-mercato/blob/main/.ai/specs/2026-05-04-modules-ts-unified-overrides.md) — AI is Phase 1, other domains roll out as separate PRs:
262
+ Example `modules.ts` inline override (preferred for app-level decisions that do not deserve a fake module). All module contract domains live under the same `entry.overrides` umbrella per the [unified spec](https://github.com/open-mercato/open-mercato/blob/main/.ai/specs/2026-05-04-modules-ts-unified-overrides.md):
252
263
 
253
264
  ```ts
254
265
  // src/modules.ts
@@ -260,6 +271,17 @@ Example `modules.ts` inline override (preferred for app-level decisions that don
260
271
  agents: { 'catalog.catalog_assistant': null },
261
272
  tools: { 'inbox_ops_accept_action': null },
262
273
  },
274
+ routes: {
275
+ api: {
276
+ 'GET /api/example/override-probe': {
277
+ handler: async () => Response.json({ ok: true, source: 'override' }),
278
+ metadata: { requireAuth: false },
279
+ },
280
+ },
281
+ pages: {
282
+ '/backend/example/reports': null,
283
+ },
284
+ },
263
285
  },
264
286
  },
265
287
  ```
@@ -290,6 +312,108 @@ yarn mercato configs cache structural --all-tenants
290
312
 
291
313
  Refer to the `create-ai-agent` skill (`.ai/skills/create-ai-agent/SKILL.md`) and the public docs at `framework/ai-assistant/overrides` for the full contract, MUST rules, and the resolution order.
292
314
 
315
+ ### Unified module contract overrides
316
+
317
+ The same `entry.overrides` surface that disables/replaces AI agents also wires routes, subscribers, workers, widgets, notifications, interceptors, enrichers, guards, CLI commands, setup hooks, ACL features, DI bindings, and encryption maps. Use it when you want to replace a contract shipped by an upstream module without forking the source.
318
+
319
+ ```ts
320
+ // src/modules.ts — representative examples across override phases
321
+ {
322
+ id: 'example',
323
+ from: '@app',
324
+ overrides: {
325
+ routes: {
326
+ api: {
327
+ // disable
328
+ 'DELETE /api/example/items': null,
329
+ // replace
330
+ 'POST /api/example/items': {
331
+ handler: async (req) => new Response(JSON.stringify({ ok: true }), { status: 200 }),
332
+ metadata: { requireAuth: true, requireFeatures: ['example.manage'] },
333
+ },
334
+ },
335
+ pages: {
336
+ '/backend/example/items': null,
337
+ },
338
+ },
339
+ events: {
340
+ subscribers: {
341
+ 'example.todo.created.notify': null,
342
+ },
343
+ },
344
+ workers: {
345
+ 'example:sync': null,
346
+ },
347
+ widgets: {
348
+ injection: { 'example.toolbar': null },
349
+ dashboard: { 'example.kpi': null },
350
+ components: {
351
+ 'page:/backend/example': {
352
+ target: { componentId: 'page:/backend/example' },
353
+ priority: 10,
354
+ propsTransform: (props) => props,
355
+ },
356
+ },
357
+ },
358
+ notifications: {
359
+ types: { 'example.notice': null },
360
+ handlers: { 'example.notice.toast': null },
361
+ },
362
+ interceptors: { 'example.items.audit': null },
363
+ commandInterceptors: { 'example.command.audit': null },
364
+ enrichers: { 'example.items.enricher': null },
365
+ guards: { 'example.backend.guard': null },
366
+ cli: { 'example seed': null },
367
+ setup: {
368
+ defaultRoleFeatures: { admin: ['example.view'] },
369
+ seedExamples: false,
370
+ },
371
+ acl: {
372
+ features: { 'example.manage': null },
373
+ },
374
+ di: {
375
+ exampleService: {
376
+ register: (container, key) => container.register({ [key]: { mode: 'replacement' } }),
377
+ },
378
+ },
379
+ encryption: {
380
+ maps: { 'example:item': null },
381
+ },
382
+ },
383
+ },
384
+ ```
385
+
386
+ Programmatic equivalent (boot-time, env-driven, or test scaffold):
387
+
388
+ ```ts
389
+ // src/bootstrap.ts (extra)
390
+ import {
391
+ applyApiRouteOverrides,
392
+ applyPageRouteOverrides,
393
+ applyWorkerOverrides,
394
+ } from '@open-mercato/shared/modules/overrides'
395
+
396
+ applyApiRouteOverrides({
397
+ 'GET /api/example/items': null,
398
+ })
399
+ applyPageRouteOverrides({
400
+ '/backend/example/items': null,
401
+ })
402
+ applyWorkerOverrides({
403
+ 'example:sync': null,
404
+ })
405
+ ```
406
+
407
+ Key rules:
408
+
409
+ - Keys are `'METHOD /api/path'`; method is normalized, path leading slash optional, trailing slashes stripped.
410
+ - Page route keys are `'/backend/path'` or `'/frontend/path'`.
411
+ - `null` disables the matching contract; a definition replaces it.
412
+ - Programmatic > `modules.ts` > file-based where supported > base. The dispatcher MUST run before registries first-load — the template's `bootstrap.ts` already does this.
413
+ - Stale override keys log a warning so operators notice renamed or removed upstream contracts.
414
+
415
+ Full reference: `framework/modules/overrides` and `framework/modules/routes-and-pages`.
416
+
293
417
  ## Disabling the Dashboards Module: Update /backend
294
418
 
295
419
  The default `/backend` page (`src/app/(backend)/backend/page.tsx`) renders `<DashboardScreen />` from `@open-mercato/ui/backend/dashboard`. That component's data flow depends on the `dashboards` module being enabled — widgets, layouts, and the dashboard API routes all live there.
@@ -78,7 +78,7 @@
78
78
  "@uiw/react-markdown-preview": "^5.2.0",
79
79
  "@uiw/react-md-editor": "^4.1.0",
80
80
  "@xyflow/react": "^12.6.0",
81
- "ai": "^6.0.177",
81
+ "ai": "^6.0.185",
82
82
  "awilix": "^12.0.5",
83
83
  "bcryptjs": "^3.0.3",
84
84
  "class-variance-authority": "^0.7.1",
@@ -90,7 +90,7 @@
90
90
  "mammoth": "^1.9.0",
91
91
  "newrelic": "^13.19.1",
92
92
  "next": "16.2.6",
93
- "pg": "8.20.0",
93
+ "pg": "8.21.0",
94
94
  "pdfjs-dist": "^5.4.149",
95
95
  "react": "19.2.5",
96
96
  "react-big-calendar": "^1.19.4",
@@ -103,8 +103,8 @@
103
103
  "semver": "^7.7.4",
104
104
  "tailwind-merge": "^3.5.0",
105
105
  "zod": "4.3.6",
106
- "@stripe/react-stripe-js": "^6.3.0",
107
- "@stripe/stripe-js": "^9.3.1",
106
+ "@stripe/react-stripe-js": "^6.4.0",
107
+ "@stripe/stripe-js": "^9.6.0",
108
108
  "@open-mercato/gateway-stripe": "{{PACKAGE_VERSION}}",
109
109
  "@open-mercato/sync-akeneo": "{{PACKAGE_VERSION}}"
110
110
  },
@@ -0,0 +1,33 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ // Greenfield must not inherit the prior run's compiler state. Wiping the
5
+ // configured Next.js distDir (`.mercato/next`) plus the legacy `.next` location guarantees Turbopack rebuilds the route table
6
+ // and middleware manifest from scratch on the next launch.
7
+ export const GREENFIELD_PURGE_TARGETS = Object.freeze([
8
+ Object.freeze(['.mercato', 'next']),
9
+ Object.freeze(['.next']),
10
+ ])
11
+
12
+ export function purgeAppBuildCaches({
13
+ rootDir = process.cwd(),
14
+ fsImpl = fs,
15
+ logger = console,
16
+ targets = GREENFIELD_PURGE_TARGETS,
17
+ } = {}) {
18
+ const removed = []
19
+ for (const segments of targets) {
20
+ const target = path.join(rootDir, ...segments)
21
+ if (!fsImpl.existsSync(target)) continue
22
+ fsImpl.rmSync(target, { recursive: true, force: true })
23
+ removed.push(segments.join('/'))
24
+ }
25
+ if (removed.length === 0) {
26
+ logger.log('🧹 [dev:greenfield] no stale Next/Turbopack build directories to purge')
27
+ } else {
28
+ for (const relPath of removed) {
29
+ logger.log(`🧹 [dev:greenfield] removed ${relPath}`)
30
+ }
31
+ }
32
+ return { removed }
33
+ }
@@ -22,6 +22,7 @@ import {
22
22
  resolveProgressPercent,
23
23
  stripAnsi,
24
24
  } from './dev-splash-helpers.mjs'
25
+ import { purgeAppBuildCaches } from './dev-cache-purge.mjs'
25
26
  import { resolveSpawnCommand } from './dev-spawn-utils.mjs'
26
27
  import { createDevSplashCodingFlow } from './dev-splash-coding-flow.mjs'
27
28
  import { createDevSplashGitRepoFlow } from './dev-splash-git-repo-flow.mjs'
@@ -1647,6 +1648,7 @@ async function runClassicStandardDev() {
1647
1648
  }
1648
1649
 
1649
1650
  async function runGreenfieldDev() {
1651
+ purgeAppBuildCaches()
1650
1652
  await runStage('🧱 Greenfield build packages', ['build:packages'], { stageCurrent: 1, stageTotal: 5 })
1651
1653
  await runStage('🧬 Greenfield generate artifacts', ['generate'], { stageCurrent: 2, stageTotal: 5 })
1652
1654
  await runStage('🧱 Greenfield rebuild packages', ['build:packages'], { stageCurrent: 3, stageTotal: 5 })
@@ -1657,6 +1659,7 @@ async function runGreenfieldDev() {
1657
1659
  }
1658
1660
 
1659
1661
  async function runClassicGreenfieldDev() {
1662
+ purgeAppBuildCaches()
1660
1663
  await runRawYarnCommand(['build:packages'])
1661
1664
  await runRawYarnCommand(['generate'])
1662
1665
  await runRawYarnCommand(['build:packages'])
@@ -1,8 +1,9 @@
1
1
  import { notFound, redirect } from 'next/navigation'
2
2
  import { cookies } from 'next/headers'
3
3
  import Link from 'next/link'
4
- import { findRouteManifestMatch } from '@open-mercato/shared/modules/registry'
4
+ import { findRouteManifestMatch, getBackendRouteManifests, registerBackendRouteManifests } from '@open-mercato/shared/modules/registry'
5
5
  import { backendRoutes } from '@/.mercato/generated/backend-routes.generated'
6
+ import { bootstrap } from '@/bootstrap'
6
7
  import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
7
8
  import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
8
9
  import { ApplyBreadcrumb } from '@open-mercato/ui/backend/AppShell'
@@ -17,6 +18,9 @@ import { resolveLocalizedTitleMetadata } from '@/lib/metadata'
17
18
  import { resolvePageMiddlewareRedirect } from '@open-mercato/shared/lib/middleware/page-executor'
18
19
  import { backendMiddlewareEntries } from '@/.mercato/generated/backend-middleware.generated'
19
20
 
21
+ bootstrap()
22
+ registerBackendRouteManifests(backendRoutes)
23
+
20
24
  type Awaitable<T> = T | Promise<T>
21
25
 
22
26
  type BackendParams = { params: Awaitable<{ slug?: string[] }> }
@@ -39,7 +43,7 @@ async function renderAccessDenied() {
39
43
  export async function generateMetadata(props: BackendParams): Promise<Metadata> {
40
44
  const params = await props.params
41
45
  const pathname = '/backend/' + (params.slug?.join('/') ?? '')
42
- const match = findRouteManifestMatch(backendRoutes, pathname)
46
+ const match = findRouteManifestMatch(getBackendRouteManifests(), pathname)
43
47
  if (!match) {
44
48
  return {}
45
49
  }
@@ -53,7 +57,7 @@ export async function generateMetadata(props: BackendParams): Promise<Metadata>
53
57
  export default async function BackendCatchAll(props: BackendParams) {
54
58
  const params = await props.params
55
59
  const pathname = '/backend/' + (params.slug?.join('/') ?? '')
56
- const match = findRouteManifestMatch(backendRoutes, pathname)
60
+ const match = findRouteManifestMatch(getBackendRouteManifests(), pathname)
57
61
  if (!match) return notFound()
58
62
  let auth: AuthContext = null
59
63
  let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null
@@ -8,6 +8,11 @@ jest.mock('@/.mercato/generated/backend-routes.generated', () => ({
8
8
  backendRoutes: [],
9
9
  }))
10
10
 
11
+ jest.mock('@/bootstrap', () => ({
12
+ bootstrap: jest.fn(),
13
+ isBootstrapped: jest.fn(() => true),
14
+ }))
15
+
11
16
  import BackendCatchAll from '@/app/(backend)/backend/[...slug]/page'
12
17
 
13
18
  // Mock UI breadcrumb component to avoid UI package dependency
@@ -39,6 +44,8 @@ jest.mock('@open-mercato/shared/modules/registry', () => ({
39
44
  },
40
45
  params: {},
41
46
  })),
47
+ getBackendRouteManifests: jest.fn(() => []),
48
+ registerBackendRouteManifests: jest.fn(),
42
49
  }))
43
50
 
44
51
  jest.mock('@/.mercato/generated/backend-middleware.generated', () => ({
@@ -106,6 +106,7 @@ export default async function BackendLayout({
106
106
  rightHeaderSlot={(
107
107
  <BackendHeaderChrome
108
108
  email={auth?.email}
109
+ userId={auth?.sub ?? null}
109
110
  embeddingConfigured={embeddingConfigured}
110
111
  missingConfigMessage={missingConfigMessage}
111
112
  tenantId={auth?.tenantId ?? null}
@@ -1,9 +1,8 @@
1
1
  import { notFound, redirect } from 'next/navigation'
2
2
  import Link from 'next/link'
3
- import { findRouteManifestMatch, registerFrontendRouteManifests } from '@open-mercato/shared/modules/registry'
3
+ import { findRouteManifestMatch, getFrontendRouteManifests, registerFrontendRouteManifests } from '@open-mercato/shared/modules/registry'
4
+ import { bootstrap } from '@/bootstrap'
4
5
  import { frontendRoutes } from '@/.mercato/generated/frontend-routes.generated'
5
-
6
- registerFrontendRouteManifests(frontendRoutes)
7
6
  import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
8
7
  import { AccessDeniedMessage } from '@open-mercato/ui/backend/detail'
9
8
  import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
@@ -16,6 +15,9 @@ import { resolveLocalizedTitleMetadata } from '@/lib/metadata'
16
15
  import { resolvePageMiddlewareRedirect } from '@open-mercato/shared/lib/middleware/page-executor'
17
16
  import { frontendMiddlewareEntries } from '@/.mercato/generated/frontend-middleware.generated'
18
17
 
18
+ bootstrap()
19
+ registerFrontendRouteManifests(frontendRoutes)
20
+
19
21
  type FrontendParams = { params: Promise<{ slug: string[] }> }
20
22
 
21
23
  async function renderAccessDenied() {
@@ -36,7 +38,7 @@ async function renderAccessDenied() {
36
38
  export async function generateMetadata({ params }: FrontendParams): Promise<Metadata> {
37
39
  const p = await params
38
40
  const pathname = '/' + (p.slug?.join('/') ?? '')
39
- const match = findRouteManifestMatch(frontendRoutes, pathname)
41
+ const match = findRouteManifestMatch(getFrontendRouteManifests(), pathname)
40
42
  if (!match) {
41
43
  return {}
42
44
  }
@@ -50,7 +52,7 @@ export async function generateMetadata({ params }: FrontendParams): Promise<Meta
50
52
  export default async function SiteCatchAll({ params }: FrontendParams) {
51
53
  const p = await params
52
54
  const pathname = '/' + (p.slug?.join('/') ?? '')
53
- const match = findRouteManifestMatch(frontendRoutes, pathname)
55
+ const match = findRouteManifestMatch(getFrontendRouteManifests(), pathname)
54
56
  if (!match) return notFound()
55
57
 
56
58
  // Customer portal auth gate — separate from staff auth
@@ -1,17 +1,9 @@
1
1
  import { NextResponse, type NextRequest } from 'next/server'
2
- import { findApiRouteManifestMatch, registerApiRouteManifests, registerBackendRouteManifests, registerFrontendRouteManifests, type HttpMethod } from '@open-mercato/shared/modules/registry'
2
+ import { findApiRouteManifestMatch, getApiRouteManifests, registerApiRouteManifests, type HttpMethod } from '@open-mercato/shared/modules/registry'
3
3
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
4
  import { apiRoutes } from '@/.mercato/generated/api-routes.generated'
5
- import { backendRoutes } from '@/.mercato/generated/backend-routes.generated'
6
- import { frontendRoutes } from '@/.mercato/generated/frontend-routes.generated'
7
5
  import { resolveAuthFromRequestDetailed } from '@open-mercato/shared/lib/auth/server'
8
6
  import { bootstrap } from '@/bootstrap'
9
-
10
- // Ensure all package registrations are initialized for API routes
11
- bootstrap()
12
- registerBackendRouteManifests(backendRoutes)
13
- registerFrontendRouteManifests(frontendRoutes)
14
- registerApiRouteManifests(apiRoutes)
15
7
  import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
16
8
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
17
9
  import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
@@ -25,6 +17,10 @@ import { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FAL
25
17
  import { getGlobalEventBus } from '@open-mercato/shared/modules/events'
26
18
  import { applicationLifecycleEvents, type ApplicationLifecycleEventId } from '@open-mercato/shared/lib/runtime/events'
27
19
 
20
+ // Ensure all package registrations are initialized for API routes.
21
+ bootstrap()
22
+ registerApiRouteManifests(apiRoutes)
23
+
28
24
  type MethodMetadata = {
29
25
  requireAuth?: boolean
30
26
  /** @deprecated Use `requireFeatures` instead — role names are mutable and can be spoofed */
@@ -293,7 +289,7 @@ async function handleRequest(
293
289
  receivedAt: new Date().toISOString(),
294
290
  }
295
291
  await emitLifecycleEvent(applicationLifecycleEvents.requestReceived, receivedPayload)
296
- const match = findApiRouteManifestMatch(apiRoutes, method, pathname)
292
+ const match = findApiRouteManifestMatch(getApiRouteManifests(), method, pathname)
297
293
  if (!match) {
298
294
  const response = NextResponse.json({ error: t('api.errors.notFound', 'Not Found') }, { status: 404 })
299
295
  await emitLifecycleEvent(applicationLifecycleEvents.requestNotFound, {
@@ -29,7 +29,7 @@ registerAppDictionaryLoader(async (locale: Locale): Promise<Record<string, unkno
29
29
  })
30
30
 
31
31
  // modules.ts inline overrides (replace/disable any contract a module
32
- // presents AI today, other domains rolling out per the unified spec).
32
+ // presents through the unified modules.ts override surface).
33
33
  // Importing @open-mercato/ai-assistant here also runs the side-effect
34
34
  // that registers the AI domain applier with the umbrella dispatcher.
35
35
  import { enabledModules } from '@/modules'
@@ -5,6 +5,7 @@ import Link from 'next/link'
5
5
  import * as React from 'react'
6
6
  import { MoreHorizontal, PlugZap, Settings, Mail } from 'lucide-react'
7
7
  import { hasFeature } from '@open-mercato/shared/security/features'
8
+ import { AuthSessionGuard } from '@open-mercato/ui/backend/AuthSessionGuard'
8
9
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
9
10
  import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
10
11
  import { IntegrationsButton } from '@open-mercato/ui/backend/IntegrationsButton'
@@ -37,6 +38,7 @@ const LazyMessagesIcon = dynamic(
37
38
 
38
39
  type BackendHeaderChromeProps = {
39
40
  email?: string
41
+ userId: string | null
40
42
  embeddingConfigured: boolean
41
43
  missingConfigMessage: string
42
44
  tenantId: string | null
@@ -102,6 +104,7 @@ function MobileMoreMenu({ items }: { items: MobileMoreItem[] }) {
102
104
 
103
105
  export function BackendHeaderChrome({
104
106
  email,
107
+ userId,
105
108
  embeddingConfigured,
106
109
  missingConfigMessage,
107
110
  tenantId,
@@ -160,6 +163,7 @@ export function BackendHeaderChrome({
160
163
 
161
164
  return (
162
165
  <>
166
+ <AuthSessionGuard serverUserId={userId} />
163
167
  {isReady && showAiAssistant ? (
164
168
  <AiAssistantShellIntegration tenantId={tenantId} organizationId={organizationId}>
165
169
  <LazyAiChatHeaderButton />
@@ -39,6 +39,10 @@ jest.mock('@open-mercato/ui/backend/SettingsButton', () => ({
39
39
  SettingsButton: () => <div data-testid="settings-button" />,
40
40
  }))
41
41
 
42
+ jest.mock('@open-mercato/ui/backend/AuthSessionGuard', () => ({
43
+ AuthSessionGuard: () => <div data-testid="auth-session-guard" />,
44
+ }))
45
+
42
46
  jest.mock('@/components/AiAssistantShellIntegration', () => ({
43
47
  AiAssistantShellIntegration: ({ children }: { children: React.ReactNode }) => <>{children}</>,
44
48
  }))
@@ -48,6 +52,7 @@ describe('BackendHeaderChrome', () => {
48
52
  const { container } = render(
49
53
  <BackendHeaderChrome
50
54
  email="demo@example.com"
55
+ userId="user-1"
51
56
  embeddingConfigured={false}
52
57
  missingConfigMessage=""
53
58
  tenantId={null}
@@ -0,0 +1,21 @@
1
+ import { test, expect } from '@playwright/test'
2
+
3
+ function resolveUrl(path: string): string {
4
+ const baseUrl = process.env.BASE_URL?.trim()
5
+ return baseUrl ? `${baseUrl}${path}` : path
6
+ }
7
+
8
+ test.describe('TC-UMES-022: modules.ts overrides', () => {
9
+ test('routes.api override replaces the example override probe handler', async ({ request }) => {
10
+ const response = await request.get(resolveUrl('/api/example/override-probe'))
11
+
12
+ expect(response.ok(), `GET /api/example/override-probe returned ${response.status()}`).toBeTruthy()
13
+ const body = await response.json()
14
+ expect(body).toMatchObject({
15
+ ok: true,
16
+ source: 'modules.ts override',
17
+ route: 'example.override-probe',
18
+ })
19
+ })
20
+ })
21
+
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import { exampleTag } from '../openapi'
4
+
5
+ export const metadata = {
6
+ GET: { requireAuth: false },
7
+ }
8
+
9
+ export async function GET() {
10
+ return Response.json({
11
+ ok: true,
12
+ source: 'base',
13
+ route: 'example.override-probe',
14
+ })
15
+ }
16
+
17
+ export const openApi: OpenApiRouteDoc = {
18
+ tag: exampleTag,
19
+ methods: {
20
+ GET: {
21
+ summary: 'Example override probe',
22
+ description: 'Returns a small payload used by integration tests to verify modules.ts API route overrides.',
23
+ tags: [exampleTag],
24
+ responses: [
25
+ {
26
+ status: 200,
27
+ description: 'Probe payload',
28
+ schema: z.object({
29
+ ok: z.boolean(),
30
+ source: z.string(),
31
+ route: z.string(),
32
+ }),
33
+ },
34
+ ],
35
+ },
36
+ },
37
+ }
38
+
@@ -2,12 +2,13 @@
2
2
  // - id: module id (plural snake_case; special cases: 'auth')
3
3
  // - from: '@open-mercato/core' | '@app' | custom alias/path in future
4
4
  // - overrides: optional unified per-app override surface — replace or
5
- // disable any contract a module presents. AI is wired today (Phase 1);
6
- // other domains are stubbed and emit a one-shot warning if used.
5
+ // disable any contract a module presents: AI, routes, events, workers,
6
+ // widgets, notifications, interceptors, setup, ACL, DI, encryption, etc.
7
7
  // See `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` and
8
- // `apps/docs/docs/framework/ai-assistant/overrides.mdx`.
8
+ // `apps/docs/docs/framework/modules/overrides.mdx`.
9
9
  import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
10
10
  import type { ModuleOverrides } from '@open-mercato/shared/modules/overrides'
11
+ import { officialModuleEntries } from './official-modules.generated'
11
12
 
12
13
  export type ModuleEntry = {
13
14
  id: string
@@ -15,6 +16,52 @@ export type ModuleEntry = {
15
16
  overrides?: ModuleOverrides
16
17
  }
17
18
 
19
+ /**
20
+ * Copyable examples for every wired `entry.overrides` domain.
21
+ *
22
+ * This object is intentionally not assigned to any enabled module. Use it as
23
+ * a reference when a downstream app needs to disable or replace contracts
24
+ * from a package-backed module without editing that module's source.
25
+ */
26
+ export const moduleOverrideExamples: ModuleOverrides = {
27
+ ai: {
28
+ agents: { 'catalog.catalog_assistant': null },
29
+ tools: { inbox_ops_accept_action: null },
30
+ },
31
+ routes: {
32
+ api: { 'DELETE /api/example/items': null },
33
+ pages: { '/backend/example/reports': null },
34
+ },
35
+ events: {
36
+ subscribers: { 'example.todo.audit': null },
37
+ },
38
+ workers: { 'example:sync': null },
39
+ widgets: {
40
+ injection: { 'example.sidebar': null },
41
+ components: { 'page:/backend/example': null },
42
+ dashboard: { 'example.kpi': null },
43
+ },
44
+ notifications: {
45
+ types: { 'example.notice': null },
46
+ handlers: { 'example.notice.toast': null },
47
+ },
48
+ interceptors: { 'example.items.interceptor': null },
49
+ commandInterceptors: { 'example.command.interceptor': null },
50
+ enrichers: { 'example.items.enricher': null },
51
+ guards: { 'example.backend.guard': null },
52
+ cli: { 'example seed': null },
53
+ setup: {
54
+ seedExamples: false,
55
+ },
56
+ acl: {
57
+ features: { 'example.manage': null },
58
+ },
59
+ di: { exampleService: null },
60
+ encryption: {
61
+ maps: { 'example:item': null },
62
+ },
63
+ }
64
+
18
65
  export const enabledModules: ModuleEntry[] = [
19
66
  { id: 'dashboards', from: '@open-mercato/core' },
20
67
  { id: 'auth', from: '@open-mercato/core' },
@@ -60,10 +107,33 @@ export const enabledModules: ModuleEntry[] = [
60
107
  { id: 'webhooks', from: '@open-mercato/webhooks' },
61
108
  { id: 'customer_accounts', from: '@open-mercato/core' },
62
109
  { id: 'portal', from: '@open-mercato/core' },
63
- { id: 'example', from: '@app' },
110
+ {
111
+ id: 'example',
112
+ from: '@app',
113
+ overrides: {
114
+ routes: {
115
+ api: {
116
+ 'GET /api/example/override-probe': {
117
+ handler: async () => Response.json({
118
+ ok: true,
119
+ source: 'modules.ts override',
120
+ route: 'example.override-probe',
121
+ }),
122
+ metadata: { requireAuth: false },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ },
64
128
  { id: 'ratelimit_probe', from: '@app' },
65
129
  ]
66
130
 
131
+ // Official modules activated via official-modules.json / official-modules.local.json
132
+ // (managed by `yarn official-modules`; backed by the external/official-modules submodule).
133
+ for (const entry of officialModuleEntries) {
134
+ if (!enabledModules.some((existing) => existing.id === entry.id)) enabledModules.push(entry)
135
+ }
136
+
67
137
  if (enabledModules.some((entry) => entry.id === 'example')) {
68
138
  enabledModules.push({ id: 'example_customers_sync', from: '@app' })
69
139
  }
@@ -0,0 +1,14 @@
1
+ // AUTO-GENERATED — do not edit by hand.
2
+ // Source: official-modules.json (+ official-modules.local.json override).
3
+ // Regenerate with: yarn official-modules
4
+ //
5
+ // Why is this file in src/ and not under .mercato/generated/?
6
+ // It is a *versioned* generated registry — the activation set must travel
7
+ // with the repo and survive `yarn clean-generated`. Moving it under any
8
+ // `generated/` folder would gitignore-and-wipe it. See
9
+ // .ai/specs/2026-05-19-official-modules-generated-location-decision.md
10
+ // and the "Generated Files: versioned vs ephemeral" section in AGENTS.md.
11
+ import type { ModuleEntry } from './modules'
12
+
13
+ export const officialModuleEntries: ModuleEntry[] = [
14
+ ]