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.
- package/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +40 -17
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +40 -17
- package/package.json +4 -5
- package/template/.env.example +24 -1
- package/template/.railwayignore +2 -2
- package/template/AGENTS.md +1 -1
- package/template/docker-compose.fullapp.dev.yml +2 -0
- package/template/docker-compose.fullapp.yml +2 -0
- package/template/src/app/api/[...slug]/route.ts +57 -26
- package/template/src/modules/example/api/todos/route.ts +2 -1
- package/template/src/modules/example/backend/payments/page.tsx +0 -4
- package/template/src/modules.ts +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
Proposed fix: Add the metadata export.
|
|
94
116
|
|
|
95
117
|
5. **Is the dev server running with latest changes?**
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
Proposed fix: Add the metadata export.
|
|
94
116
|
|
|
95
117
|
5. **Is the dev server running with latest changes?**
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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**
|
|
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
|
|
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
|
|
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.
|
|
25
|
-
"esbuild": "^0.28.
|
|
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
|
}
|
package/template/.env.example
CHANGED
|
@@ -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
|
package/template/.railwayignore
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -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
|
-
|
|
171
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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>
|
package/template/src/modules.ts
CHANGED
|
@@ -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'
|