create-mercato-app 0.6.5-develop.5337.1.534b781eac → 0.6.5

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.
@@ -39,7 +39,7 @@ When the developer reports a problem, follow this order:
39
39
 
40
40
  ### Step 2: Check Generated Files
41
41
 
42
- Run these commands first they fix 60%+ of issues:
42
+ These commands fix 60%+ of issues, so they are usually the first fix to propose (they are mutating — propose, then run after confirmation per [Step 4](#step-4-propose-before-fixing)):
43
43
 
44
44
  ```bash
45
45
  yarn generate # Regenerate module discovery files
@@ -61,6 +61,28 @@ ls .mercato/generated/
61
61
  yarn typecheck
62
62
  ```
63
63
 
64
+ ### Step 4: Propose Before Fixing
65
+
66
+ Once you have diagnosed the root cause, **do not apply the fix immediately**. First present:
67
+
68
+ 1. The **root cause** — what is actually broken and why.
69
+ 2. The **proposed fix** — the exact commands and/or code changes you intend to apply.
70
+
71
+ Then **wait for explicit user confirmation** before applying any **mutating** change. This keeps the developer in control and avoids surprise edits, migrations, or restarts.
72
+
73
+ **Read-only diagnostics may run without asking** — they only gather information and change nothing:
74
+
75
+ | Allowed without confirmation (read-only) | Requires confirmation (mutating) |
76
+ |------------------------------------------|----------------------------------|
77
+ | `yarn typecheck` | `yarn generate` |
78
+ | `grep` / file reads / `ls` | `yarn db:generate` |
79
+ | log / browser-console inspection | `yarn db:migrate` |
80
+ | `docker compose ps` | editing files |
81
+ | `curl` against a running endpoint (GET) | restarting the dev server (`yarn dev`) |
82
+ | | `docker compose up` |
83
+
84
+ When in doubt about whether an action mutates state, treat it as mutating and ask first. Once the user confirms, apply the fix and verify it.
85
+
64
86
  ---
65
87
 
66
88
  ## 2. Module Issues
@@ -76,24 +98,24 @@ yarn typecheck
76
98
  // Must have this entry:
77
99
  { id: '<module_id>', from: '@app' }
78
100
  ```
79
- Fix: Add the entry and run `yarn generate`.
101
+ Proposed fix: Add the entry and run `yarn generate`.
80
102
 
81
103
  2. **Did you run `yarn generate`?**
82
104
  Check if `.mercato/generated/` contains your module's entries.
83
- Fix: Run `yarn generate`.
105
+ Proposed fix: Run `yarn generate`.
84
106
 
85
107
  3. **Is the module folder named correctly?**
86
108
  Must be plural, snake_case: `src/modules/<module_id>/`
87
- Fix: Rename folder to match module ID.
109
+ Proposed fix: Rename folder to match module ID.
88
110
 
89
111
  4. **Does `index.ts` export `metadata`?**
90
112
  ```typescript
91
113
  export const metadata: ModuleInfo = { name: '<module_id>', ... }
92
114
  ```
93
- Fix: Add the metadata export.
115
+ Proposed fix: Add the metadata export.
94
116
 
95
117
  5. **Is the dev server running with latest changes?**
96
- Fix: Restart with `yarn dev`.
118
+ Proposed fix: Restart with `yarn dev`.
97
119
 
98
120
  ### Module loads but pages 404
99
121
 
@@ -104,17 +126,17 @@ yarn typecheck
104
126
  1. **Are backend page files in the right location?**
105
127
  - List page: `backend/page.tsx` (not `backend/index.tsx`)
106
128
  - Detail page: `backend/<entities>/[id].tsx` (bracket notation)
107
- Fix: Rename to match auto-discovery convention.
129
+ Proposed fix: Rename to match auto-discovery convention.
108
130
 
109
131
  2. **Do pages export `metadata` with `requireAuth`?**
110
132
  ```typescript
111
133
  export const metadata = { requireAuth: true, features: ['<module_id>.view'] }
112
134
  ```
113
- Fix: Add metadata export.
135
+ Proposed fix: Add metadata export.
114
136
 
115
137
  3. **Does the user have the required ACL features?**
116
138
  Check `setup.ts` has `defaultRoleFeatures` for the user's role.
117
- Fix: Add features to role defaults, re-run setup.
139
+ Proposed fix: Add features to role defaults, re-run setup.
118
140
 
119
141
  ---
120
142
 
@@ -130,22 +152,22 @@ yarn typecheck
130
152
  ```bash
131
153
  yarn db:generate # Probes/creates migration file
132
154
  ```
133
- Fix: Run `yarn db:generate` to inspect the required migration, then keep only the scoped SQL for your module and update `src/modules/<module_id>/migrations/.snapshot-open-mercato.json`.
155
+ Proposed fix: Run `yarn db:generate` to inspect the required migration, then keep only the scoped SQL for your module and update `src/modules/<module_id>/migrations/.snapshot-open-mercato.json`.
134
156
 
135
157
  2. **Is the entity declared in the right file with the right imports?**
136
158
  Entity classes belong in `src/modules/<module_id>/data/entities.ts` and decorators must come from `@mikro-orm/decorators/legacy`.
137
- Fix: move stale `entities/<Entity>.ts` patterns into `data/entities.ts` and fix the imports before regenerating the migration.
159
+ Proposed fix: move stale `entities/<Entity>.ts` patterns into `data/entities.ts` and fix the imports before regenerating the migration.
138
160
 
139
161
  3. **Did you apply the migration?**
140
162
  ```bash
141
163
  yarn db:migrate # Applies pending migrations
142
164
  ```
143
- Fix: Run `yarn db:migrate`.
165
+ Proposed fix: Run `yarn db:migrate`.
144
166
 
145
167
  4. **Is the migration file correct?**
146
168
  Check `src/modules/<module_id>/migrations/` for the latest migration.
147
169
  Verify it has the expected columns and types.
148
- Fix: If wrong, delete the migration file, fix the entity, and regenerate.
170
+ Proposed fix: If wrong, delete the migration file, fix the entity, and regenerate.
149
171
 
150
172
  ### Migration generation creates unexpected changes
151
173
 
@@ -160,11 +182,11 @@ yarn typecheck
160
182
 
161
183
  2. **Did you modify a core module entity without ejecting?**
162
184
  Never edit `node_modules/@open-mercato/*`.
163
- Fix: Revert changes to node_modules. Use UMES extensions instead, or eject the module.
185
+ Proposed fix: Revert changes to node_modules. Use UMES extensions instead, or eject the module.
164
186
 
165
187
  3. **Is a module snapshot stale?**
166
188
  Check whether the generated SQL recreates a table or column that already has a committed migration.
167
- Fix: update that module's `migrations/.snapshot-open-mercato.json` to include the already-migrated schema, then re-run `yarn db:generate` and expect `no changes`.
189
+ Proposed fix: update that module's `migrations/.snapshot-open-mercato.json` to include the already-migrated schema, then re-run `yarn db:generate` and expect `no changes`.
168
190
 
169
191
  ### Entity changes not reflected
170
192
 
@@ -443,9 +465,10 @@ yarn dev # 5. Restart dev server
443
465
 
444
466
  ## Rules
445
467
 
446
- - **ALWAYS** run `yarn generate` as first diagnostic step
468
+ - **ALWAYS** present the diagnosed root cause and the proposed fix (commands/code), then **wait for explicit user confirmation before applying any mutating change** (see [Step 4](#step-4-propose-before-fixing)). Only read-only diagnostics may run without asking.
447
469
  - **ALWAYS** check server logs / browser console for actual error messages
448
470
  - **NEVER** edit files in `.mercato/generated/` or `node_modules/`
449
471
  - **NEVER** assume the issue — verify with actual error output
472
+ - Treat `yarn generate` as the most likely first fix, but propose it before running — it regenerates files and is a mutating action
450
473
  - Fix the root cause, not the symptom — temporary workarounds become permanent bugs
451
- - When suggesting a fix, include the exact command or code change needed
474
+ - When proposing a fix, include the exact command or code change needed
@@ -39,7 +39,7 @@ When the developer reports a problem, follow this order:
39
39
 
40
40
  ### Step 2: Check Generated Files
41
41
 
42
- Run these commands first they fix 60%+ of issues:
42
+ These commands fix 60%+ of issues, so they are usually the first fix to propose (they are mutating — propose, then run after confirmation per [Step 4](#step-4-propose-before-fixing)):
43
43
 
44
44
  ```bash
45
45
  yarn generate # Regenerate module discovery files
@@ -61,6 +61,28 @@ ls .mercato/generated/
61
61
  yarn typecheck
62
62
  ```
63
63
 
64
+ ### Step 4: Propose Before Fixing
65
+
66
+ Once you have diagnosed the root cause, **do not apply the fix immediately**. First present:
67
+
68
+ 1. The **root cause** — what is actually broken and why.
69
+ 2. The **proposed fix** — the exact commands and/or code changes you intend to apply.
70
+
71
+ Then **wait for explicit user confirmation** before applying any **mutating** change. This keeps the developer in control and avoids surprise edits, migrations, or restarts.
72
+
73
+ **Read-only diagnostics may run without asking** — they only gather information and change nothing:
74
+
75
+ | Allowed without confirmation (read-only) | Requires confirmation (mutating) |
76
+ |------------------------------------------|----------------------------------|
77
+ | `yarn typecheck` | `yarn generate` |
78
+ | `grep` / file reads / `ls` | `yarn db:generate` |
79
+ | log / browser-console inspection | `yarn db:migrate` |
80
+ | `docker compose ps` | editing files |
81
+ | `curl` against a running endpoint (GET) | restarting the dev server (`yarn dev`) |
82
+ | | `docker compose up` |
83
+
84
+ When in doubt about whether an action mutates state, treat it as mutating and ask first. Once the user confirms, apply the fix and verify it.
85
+
64
86
  ---
65
87
 
66
88
  ## 2. Module Issues
@@ -76,24 +98,24 @@ yarn typecheck
76
98
  // Must have this entry:
77
99
  { id: '<module_id>', from: '@app' }
78
100
  ```
79
- Fix: Add the entry and run `yarn generate`.
101
+ Proposed fix: Add the entry and run `yarn generate`.
80
102
 
81
103
  2. **Did you run `yarn generate`?**
82
104
  Check if `.mercato/generated/` contains your module's entries.
83
- Fix: Run `yarn generate`.
105
+ Proposed fix: Run `yarn generate`.
84
106
 
85
107
  3. **Is the module folder named correctly?**
86
108
  Must be plural, snake_case: `src/modules/<module_id>/`
87
- Fix: Rename folder to match module ID.
109
+ Proposed fix: Rename folder to match module ID.
88
110
 
89
111
  4. **Does `index.ts` export `metadata`?**
90
112
  ```typescript
91
113
  export const metadata: ModuleInfo = { name: '<module_id>', ... }
92
114
  ```
93
- Fix: Add the metadata export.
115
+ Proposed fix: Add the metadata export.
94
116
 
95
117
  5. **Is the dev server running with latest changes?**
96
- Fix: Restart with `yarn dev`.
118
+ Proposed fix: Restart with `yarn dev`.
97
119
 
98
120
  ### Module loads but pages 404
99
121
 
@@ -104,17 +126,17 @@ yarn typecheck
104
126
  1. **Are backend page files in the right location?**
105
127
  - List page: `backend/page.tsx` (not `backend/index.tsx`)
106
128
  - Detail page: `backend/<entities>/[id].tsx` (bracket notation)
107
- Fix: Rename to match auto-discovery convention.
129
+ Proposed fix: Rename to match auto-discovery convention.
108
130
 
109
131
  2. **Do pages export `metadata` with `requireAuth`?**
110
132
  ```typescript
111
133
  export const metadata = { requireAuth: true, features: ['<module_id>.view'] }
112
134
  ```
113
- Fix: Add metadata export.
135
+ Proposed fix: Add metadata export.
114
136
 
115
137
  3. **Does the user have the required ACL features?**
116
138
  Check `setup.ts` has `defaultRoleFeatures` for the user's role.
117
- Fix: Add features to role defaults, re-run setup.
139
+ Proposed fix: Add features to role defaults, re-run setup.
118
140
 
119
141
  ---
120
142
 
@@ -130,22 +152,22 @@ yarn typecheck
130
152
  ```bash
131
153
  yarn db:generate # Probes/creates migration file
132
154
  ```
133
- Fix: Run `yarn db:generate` to inspect the required migration, then keep only the scoped SQL for your module and update `src/modules/<module_id>/migrations/.snapshot-open-mercato.json`.
155
+ Proposed fix: Run `yarn db:generate` to inspect the required migration, then keep only the scoped SQL for your module and update `src/modules/<module_id>/migrations/.snapshot-open-mercato.json`.
134
156
 
135
157
  2. **Is the entity declared in the right file with the right imports?**
136
158
  Entity classes belong in `src/modules/<module_id>/data/entities.ts` and decorators must come from `@mikro-orm/decorators/legacy`.
137
- Fix: move stale `entities/<Entity>.ts` patterns into `data/entities.ts` and fix the imports before regenerating the migration.
159
+ Proposed fix: move stale `entities/<Entity>.ts` patterns into `data/entities.ts` and fix the imports before regenerating the migration.
138
160
 
139
161
  3. **Did you apply the migration?**
140
162
  ```bash
141
163
  yarn db:migrate # Applies pending migrations
142
164
  ```
143
- Fix: Run `yarn db:migrate`.
165
+ Proposed fix: Run `yarn db:migrate`.
144
166
 
145
167
  4. **Is the migration file correct?**
146
168
  Check `src/modules/<module_id>/migrations/` for the latest migration.
147
169
  Verify it has the expected columns and types.
148
- Fix: If wrong, delete the migration file, fix the entity, and regenerate.
170
+ Proposed fix: If wrong, delete the migration file, fix the entity, and regenerate.
149
171
 
150
172
  ### Migration generation creates unexpected changes
151
173
 
@@ -160,11 +182,11 @@ yarn typecheck
160
182
 
161
183
  2. **Did you modify a core module entity without ejecting?**
162
184
  Never edit `node_modules/@open-mercato/*`.
163
- Fix: Revert changes to node_modules. Use UMES extensions instead, or eject the module.
185
+ Proposed fix: Revert changes to node_modules. Use UMES extensions instead, or eject the module.
164
186
 
165
187
  3. **Is a module snapshot stale?**
166
188
  Check whether the generated SQL recreates a table or column that already has a committed migration.
167
- Fix: update that module's `migrations/.snapshot-open-mercato.json` to include the already-migrated schema, then re-run `yarn db:generate` and expect `no changes`.
189
+ Proposed fix: update that module's `migrations/.snapshot-open-mercato.json` to include the already-migrated schema, then re-run `yarn db:generate` and expect `no changes`.
168
190
 
169
191
  ### Entity changes not reflected
170
192
 
@@ -443,9 +465,10 @@ yarn dev # 5. Restart dev server
443
465
 
444
466
  ## Rules
445
467
 
446
- - **ALWAYS** run `yarn generate` as first diagnostic step
468
+ - **ALWAYS** present the diagnosed root cause and the proposed fix (commands/code), then **wait for explicit user confirmation before applying any mutating change** (see [Step 4](#step-4-propose-before-fixing)). Only read-only diagnostics may run without asking.
447
469
  - **ALWAYS** check server logs / browser console for actual error messages
448
470
  - **NEVER** edit files in `.mercato/generated/` or `node_modules/`
449
471
  - **NEVER** assume the issue — verify with actual error output
472
+ - Treat `yarn generate` as the most likely first fix, but propose it before running — it regenerates files and is a mutating action
450
473
  - Fix the root cause, not the symptom — temporary workarounds become permanent bugs
451
- - When suggesting a fix, include the exact command or code change needed
474
+ - When proposing a fix, include the exact command or code change needed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mercato-app",
3
- "version": "0.6.5-develop.5337.1.534b781eac",
3
+ "version": "0.6.5",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -21,8 +21,8 @@
21
21
  "tar": "^7.5.16"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/node": "^25.9.2",
25
- "esbuild": "^0.28.0",
24
+ "@types/node": "^25.9.3",
25
+ "esbuild": "^0.28.1",
26
26
  "tsx": "^4.22.4",
27
27
  "typescript": "^6.0.3"
28
28
  },
@@ -41,6 +41,5 @@
41
41
  "scaffolding",
42
42
  "cli"
43
43
  ],
44
- "license": "MIT",
45
- "stableVersion": "0.6.4"
44
+ "license": "MIT"
46
45
  }
@@ -30,6 +30,10 @@ NEXTAUTH_SECRET=
30
30
  APP_URL=http://localhost:3000
31
31
  # Additional trusted origins for app requests that must pass host validation.
32
32
  # APP_URL is required in production for security-sensitive email links.
33
+ # In dev mode these values also govern the HMR WebSocket (/_next/webpack-hmr):
34
+ # add your public/reverse-proxy/Tailscale host here so remote dev access works,
35
+ # then restart the dev server. Non-allowlisted origins are still rejected.
36
+ # In Docker dev, the value must also be forwarded into the app container.
33
37
  # Example: APP_ALLOWED_ORIGINS=https://admin.example.com,https://ops.example.com
34
38
  # APP_ALLOWED_ORIGINS=
35
39
 
@@ -190,6 +194,16 @@ CACHE_STRATEGY=sqlite
190
194
  # Default TTL in milliseconds (optional)
191
195
  CACHE_TTL=300000
192
196
 
197
+ # Max entries retained by the in-memory cache strategy before LRU eviction
198
+ # (default: 50000). Bounds memory for the process-wide cache singleton. A
199
+ # non-positive value disables the cap (unbounded — not recommended).
200
+ #CACHE_MEMORY_MAX_ENTRIES=50000
201
+
202
+ # The cache service is shared process-wide (a singleton) so cross-request
203
+ # caches actually hit. Set to off/false to fall back to a per-request cache
204
+ # instance (legacy behavior). Default: on.
205
+ #OM_CACHE_SINGLETON=on
206
+
193
207
  # Redis Configuration
194
208
  #REDIS_PORT=6379
195
209
 
@@ -208,12 +222,21 @@ DB_POOL_MIN=5
208
222
  DB_POOL_MAX=20
209
223
  DB_POOL_IDLE_TIMEOUT=30000
210
224
  DB_POOL_ACQUIRE_TIMEOUT=60000
225
+ # Connection-pinning guards (milliseconds). idle_in_transaction defaults to 120000 in every
226
+ # environment so a leaked open transaction cannot pin a pool connection forever; set to 0 to disable.
227
+ #DB_IDLE_IN_TRANSACTION_TIMEOUT_MS=120000
228
+ # Opt-in server-side timeouts. Unset = no timeout (legacy behavior). Set to cap runaway
229
+ # queries / lock waits so they cannot exhaust the pool. Recommended in production.
230
+ #DB_STATEMENT_TIMEOUT_MS=30000
231
+ #DB_LOCK_TIMEOUT_MS=10000
211
232
 
212
233
  # Audit log retention windows
213
234
  # Core resources (users, roles) keep access history longer to support investigations.
214
235
  AUDIT_LOGS_CORE_RETENTION_DAYS=7
215
236
  # Non-core reads rotate aggressively to limit storage.
216
237
  AUDIT_LOGS_NON_CORE_RETENTION_HOURS=8
238
+ # Minimum gap between retention sweeps per process. 0 rotates on every write.
239
+ AUDIT_LOGS_ROTATE_INTERVAL_MS=60000
217
240
 
218
241
  # Tenant data encryption (enabled by default)
219
242
  TENANT_DATA_ENCRYPTION=yes
@@ -549,7 +572,7 @@ OCR_MODEL=gpt-4o
549
572
  # ============================================================================
550
573
  # Customer Portal Custom Domain (Phase 1–3)
551
574
  # ============================================================================
552
- # See .ai/specs/2026-04-08-portal-custom-domain-routing.md and
575
+ # See .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md and
553
576
  # docker/traefik/README.md. Required when custom-domain mappings are in use.
554
577
 
555
578
  # Primary platform host — excluded from the Traefik domain-check middleware
@@ -9,8 +9,8 @@ node_modules/
9
9
  coverage/
10
10
  test-results/
11
11
  playwright-report/
12
- storage/
13
- data/
12
+ /storage/
13
+ /data/
14
14
  *.db
15
15
  *.db-shm
16
16
  *.db-wal
@@ -316,7 +316,7 @@ export const aiAgentOverrides: AiAgentOverridesMap = {
316
316
  }
317
317
  ```
318
318
 
319
- 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):
319
+ 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/implemented/2026-05-04-modules-ts-unified-overrides.md):
320
320
 
321
321
  ```ts
322
322
  // src/modules.ts
@@ -38,6 +38,8 @@ services:
38
38
  DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-open-mercato}
39
39
  JWT_SECRET: ${JWT_SECRET:-JWT}
40
40
  APP_URL: ${APP_URL:-http://localhost:3000}
41
+ # Additional trusted app/dev origins for the Open Mercato origin allowlist (resolveAllowedDevOrigins)
42
+ APP_ALLOWED_ORIGINS: ${APP_ALLOWED_ORIGINS:-}
41
43
  CACHE_REDIS_URL: redis://redis:6379
42
44
  CACHE_STRATEGY: ${CACHE_STRATEGY:-redis}
43
45
  ENABLE_CRUD_API_CACHE: ${ENABLE_CRUD_API_CACHE:-true}
@@ -40,6 +40,8 @@ services:
40
40
  DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-open-mercato}
41
41
  JWT_SECRET: ${JWT_SECRET:-JWT}
42
42
  APP_URL: ${APP_URL:-http://localhost:3000}
43
+ # Additional trusted app/dev origins for the Open Mercato origin allowlist (resolveAllowedDevOrigins)
44
+ APP_ALLOWED_ORIGINS: ${APP_ALLOWED_ORIGINS:-}
43
45
  CACHE_REDIS_URL: redis://redis:6379
44
46
  CACHE_STRATEGY: ${CACHE_STRATEGY:-redis}
45
47
  ENABLE_CRUD_API_CACHE: ${ENABLE_CRUD_API_CACHE:-true}
@@ -139,7 +139,7 @@ function normalizeLoadedMetadata(
139
139
  return { [method]: metadata }
140
140
  }
141
141
 
142
- async function checkAuthorization(
142
+ export async function checkAuthorization(
143
143
  methodMetadata: MethodMetadata | null,
144
144
  auth: AuthContext,
145
145
  req: NextRequest
@@ -167,24 +167,31 @@ async function checkAuthorization(
167
167
  }
168
168
 
169
169
  if (auth && requiresAuthentication) {
170
- const rawTenantCandidate = await extractTenantCandidate(req)
171
- if (rawTenantCandidate !== undefined) {
170
+ // HTTP parameter pollution hardening (issue #2665): a request can carry `tenantId`
171
+ // more than once — repeated `?tenantId=` query params, or in both the query string
172
+ // and the body. The dispatcher and downstream handlers may disagree on which
173
+ // occurrence wins (here historically `getAll().last`, handlers use `get()` first),
174
+ // so the authorization gate must validate EVERY distinct candidate. If any one
175
+ // targets a tenant the actor may not select, the request is rejected — making this
176
+ // decision binding regardless of how a handler later parses the same request.
177
+ const rawTenantCandidates = await extractTenantCandidates(req)
178
+ const actorTenant = normalizeTenantId(auth.tenantId ?? null) ?? null
179
+ const enforcedCandidates = new Set<string | null>()
180
+ for (const rawTenantCandidate of rawTenantCandidates) {
172
181
  const tenantCandidate = sanitizeTenantCandidate(rawTenantCandidate)
173
- if (tenantCandidate !== undefined) {
174
- const normalizedCandidate = normalizeTenantId(tenantCandidate) ?? null
175
- const actorTenant = normalizeTenantId(auth.tenantId ?? null) ?? null
176
- const tenantDiffers = normalizedCandidate !== actorTenant
177
- if (tenantDiffers) {
178
- try {
179
- const guardContainer = await ensureContainer()
180
- await enforceTenantSelection({ auth, container: guardContainer }, tenantCandidate)
181
- } catch (error) {
182
- if (isCrudHttpError(error)) {
183
- return NextResponse.json(error.body ?? { error: t('api.errors.forbidden', 'Forbidden') }, { status: error.status })
184
- }
185
- throw error
186
- }
182
+ if (tenantCandidate === undefined) continue
183
+ const normalizedCandidate = normalizeTenantId(tenantCandidate) ?? null
184
+ if (normalizedCandidate === actorTenant) continue
185
+ if (enforcedCandidates.has(normalizedCandidate)) continue
186
+ enforcedCandidates.add(normalizedCandidate)
187
+ try {
188
+ const guardContainer = await ensureContainer()
189
+ await enforceTenantSelection({ auth, container: guardContainer }, tenantCandidate)
190
+ } catch (error) {
191
+ if (isCrudHttpError(error)) {
192
+ return NextResponse.json(error.body ?? { error: t('api.errors.forbidden', 'Forbidden') }, { status: error.status })
187
193
  }
194
+ throw error
188
195
  }
189
196
  }
190
197
  }
@@ -248,17 +255,12 @@ function sanitizeTenantCandidate(candidate: unknown): unknown {
248
255
  return candidate
249
256
  }
250
257
 
251
- export async function extractTenantCandidate(req: NextRequest): Promise<unknown> {
252
- const tenantParams = req.nextUrl?.searchParams?.getAll?.('tenantId') ?? []
253
- if (tenantParams.length > 0) {
254
- return tenantParams[tenantParams.length - 1]
255
- }
256
-
258
+ function bodyCarriesTenantId(req: NextRequest): boolean {
257
259
  const method = (req.method || 'GET').toUpperCase()
258
- if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
259
- return undefined
260
- }
260
+ return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS'
261
+ }
261
262
 
263
+ async function extractBodyTenantCandidate(req: NextRequest): Promise<unknown> {
262
264
  const rawContentType = req.headers.get('content-type')
263
265
  if (!rawContentType) return undefined
264
266
  const contentType = rawContentType.split(';')[0].trim().toLowerCase()
@@ -284,6 +286,35 @@ export async function extractTenantCandidate(req: NextRequest): Promise<unknown>
284
286
  return undefined
285
287
  }
286
288
 
289
+ export async function extractTenantCandidate(req: NextRequest): Promise<unknown> {
290
+ const tenantParams = req.nextUrl?.searchParams?.getAll?.('tenantId') ?? []
291
+ if (tenantParams.length > 0) {
292
+ return tenantParams[tenantParams.length - 1]
293
+ }
294
+
295
+ if (!bodyCarriesTenantId(req)) {
296
+ return undefined
297
+ }
298
+
299
+ return extractBodyTenantCandidate(req)
300
+ }
301
+
302
+ // Returns every `tenantId` candidate the request carries — each repeated `?tenantId=`
303
+ // query param plus any body-level value. The dispatcher enforces tenant selection
304
+ // against all of them so a downstream handler that reads a different occurrence
305
+ // (e.g. `searchParams.get()` first vs. `getAll()` last) cannot be tricked into using a
306
+ // candidate that was never authorized (issue #2665).
307
+ export async function extractTenantCandidates(req: NextRequest): Promise<unknown[]> {
308
+ const candidates: unknown[] = [...(req.nextUrl?.searchParams?.getAll?.('tenantId') ?? [])]
309
+
310
+ if (bodyCarriesTenantId(req)) {
311
+ const bodyCandidate = await extractBodyTenantCandidate(req)
312
+ if (bodyCandidate !== undefined) candidates.push(bodyCandidate)
313
+ }
314
+
315
+ return candidates
316
+ }
317
+
287
318
  async function handleRequest(
288
319
  method: HttpMethod,
289
320
  req: NextRequest,
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { z } from 'zod'
3
3
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
4
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
4
5
  import { Todo } from '../../data/entities'
5
6
 
6
7
  const ENTITY_ID = 'example:todo' as const
@@ -105,7 +106,7 @@ export const { metadata, GET, POST, PUT, DELETE } = makeCrudRoute({
105
106
  if (ids.length > 0) F.id = { $in: ids }
106
107
  }
107
108
  if (q.id) F.id = q.id
108
- if (q.title) F.title = { $ilike: `%${q.title}%` }
109
+ if (q.title) F.title = { $ilike: `%${escapeLikePattern(q.title)}%` }
109
110
  if (q.isDone !== undefined) F.is_done = q.isDone as any
110
111
  if (q.organizationId) F.organization_id = q.organizationId
111
112
  if (q.createdFrom || q.createdTo) {
@@ -20,8 +20,6 @@ import {
20
20
  Ban,
21
21
  ArrowDownToLine,
22
22
  Undo2,
23
- CheckCircle2,
24
- AlertCircle,
25
23
  Info,
26
24
  Zap,
27
25
  } from 'lucide-react'
@@ -433,7 +431,6 @@ export default function PaymentGatewayDemoPage() {
433
431
  {/* Error Display */}
434
432
  {error && (
435
433
  <Alert variant="destructive">
436
- <AlertCircle className="size-4" />
437
434
  <AlertTitle>{t('example.payments.error.title', 'Error')}</AlertTitle>
438
435
  <AlertDescription>{error}</AlertDescription>
439
436
  </Alert>
@@ -442,7 +439,6 @@ export default function PaymentGatewayDemoPage() {
442
439
  {/* Action Result */}
443
440
  {actionResult && (
444
441
  <Alert variant="success">
445
- <CheckCircle2 className="size-4" />
446
442
  <AlertTitle>{t('example.payments.success.title', 'Success')}</AlertTitle>
447
443
  <AlertDescription>{actionResult}</AlertDescription>
448
444
  </Alert>
@@ -4,7 +4,7 @@
4
4
  // - overrides: optional unified per-app override surface — replace or
5
5
  // disable any contract a module presents: AI, routes, events, workers,
6
6
  // widgets, notifications, interceptors, setup, ACL, DI, encryption, etc.
7
- // See `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` and
7
+ // See `.ai/specs/implemented/2026-05-04-modules-ts-unified-overrides.md` and
8
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'