create-mercato-app 0.6.2-develop.3467.1.2a1818709d → 0.6.3-develop.3484.1.1427e58147

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.3467.1.2a1818709d",
3
+ "version": "0.6.3-develop.3484.1.1427e58147",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -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"
@@ -42,5 +42,5 @@
42
42
  "cli"
43
43
  ],
44
44
  "license": "MIT",
45
- "stableVersion": "0.6.1"
45
+ "stableVersion": "0.6.2"
46
46
  }
@@ -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
  },
@@ -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', () => ({
@@ -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'
@@ -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
+ ]