create-mercato-app 0.6.1-develop.3090.06ab462170 → 0.6.1-develop.3102.d6e7e6d57a
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/AGENTS.md.template +38 -5
- package/agentic/shared/ai/qa/tests/playwright.config.ts +5 -0
- package/agentic/shared/ai/skills/data-model-design/SKILL.md +91 -3
- package/agentic/shared/ai/skills/implement-spec/SKILL.md +9 -5
- package/agentic/shared/ai/skills/module-scaffold/SKILL.md +57 -7
- package/agentic/shared/ai/skills/spec-writing/SKILL.md +11 -2
- package/agentic/shared/ai/skills/spec-writing/references/spec-checklist.md +24 -7
- package/dist/agentic/shared/AGENTS.md.template +38 -5
- package/dist/agentic/shared/ai/qa/tests/playwright.config.ts +5 -0
- package/dist/agentic/shared/ai/skills/data-model-design/SKILL.md +91 -3
- package/dist/agentic/shared/ai/skills/implement-spec/SKILL.md +9 -5
- package/dist/agentic/shared/ai/skills/module-scaffold/SKILL.md +57 -7
- package/dist/agentic/shared/ai/skills/spec-writing/SKILL.md +11 -2
- package/dist/agentic/shared/ai/skills/spec-writing/references/spec-checklist.md +24 -7
- package/package.json +1 -1
- package/template/AGENTS.md +81 -2
- package/template/package.json.template +1 -0
- package/template/scripts/dev-reset.mjs +27 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Context Routing — {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
**MANDATORY CONTEXT LOADING** — see Critical Rule
|
|
3
|
+
**MANDATORY CONTEXT LOADING** — see the "BEFORE writing ANY code" Critical Rule below.
|
|
4
4
|
Before writing code, find your task below and `Read` the listed files.
|
|
5
5
|
Do NOT load the entire src/ tree — Open Mercato apps can have many modules.
|
|
6
6
|
|
|
@@ -51,7 +51,8 @@ step, you WILL produce incorrect imports and miss required patterns.
|
|
|
51
51
|
| Add caching | `.ai/guides/cache.md` |
|
|
52
52
|
| Add background workers | `.ai/guides/queue.md` |
|
|
53
53
|
| Use i18n (translations) | `.ai/guides/shared.md` → i18n |
|
|
54
|
-
| Use encrypted queries | `.ai/guides/shared.md` → Encryption |
|
|
54
|
+
| Use encrypted queries (read sensitive columns that already have an encryption map; for authoring a NEW sensitive column see the row below first) | `.ai/guides/shared.md` → Encryption |
|
|
55
|
+
| **Encrypt sensitive / GDPR-relevant fields** ("we need this column encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", addresses, contact info, free-text notes about people, integration credentials, secrets) — declare them in the framework's encryption-maps mechanism, never hand-rolled AES/KMS | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps, then `.ai/skills/module-scaffold/SKILL.md` → Encryption maps. Reference: <https://docs.open-mercato.dev/user-guide/encryption> |
|
|
55
56
|
| Use apiCall / UI components | `.ai/guides/ui.md` |
|
|
56
57
|
| Add permissions (RBAC) | `.ai/guides/core.md` → Access Control |
|
|
57
58
|
| Add notifications | `.ai/guides/core.md` → Notifications |
|
|
@@ -130,11 +131,35 @@ src/modules/<id>/
|
|
|
130
131
|
├── ce.ts # Custom entities / custom field sets
|
|
131
132
|
├── translations.ts # Translatable fields per entity
|
|
132
133
|
├── notifications.ts # Notification type definitions
|
|
133
|
-
|
|
134
|
+
├── notifications.client.ts # Client-side notification renderers
|
|
135
|
+
└── encryption.ts # Tenant data encryption maps (defaultEncryptionMaps) for sensitive / GDPR fields
|
|
134
136
|
```
|
|
135
137
|
|
|
136
138
|
Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
|
|
137
139
|
|
|
140
|
+
## Mandatory Module Mechanisms (every module MUST use these — no DIY substitutes)
|
|
141
|
+
|
|
142
|
+
When the user asks to **create a new application** or a **new module**, do not invent your own routing, auth, persistence, forms, or caching. The framework provides one canonical primitive for each concern. If a feature is not on this list and not in the Task → Context Map, ask before adding it — do not roll your own.
|
|
143
|
+
|
|
144
|
+
| Concern | Canonical mechanism | Where to learn it |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| Module structure & auto-discovery | `src/modules/<id>/{api,backend,frontend,data,subscribers,workers,widgets}` + `index.ts` + `src/modules.ts` (`from: '@app'`) — discovered by `yarn generate` | `.ai/skills/module-scaffold/SKILL.md`, `.ai/guides/core.md` → Module Files; <https://docs.open-mercato.dev/framework/modules/overview> |
|
|
147
|
+
| **Backend admin pages** | Auto-discovered files under `src/modules/<id>/backend/**`, paired `page.meta.ts` with `requireAuth` + `requireFeatures` + `pageGroup`/`pageGroupKey`/`pageOrder` | `.ai/skills/backend-ui-design/SKILL.md`, `.ai/skills/module-scaffold/references/navigation-patterns.md`; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
|
|
148
|
+
| **Frontend public pages** | Auto-discovered files under `src/modules/<id>/frontend/**`. Customer portal pages live under `frontend/[orgSlug]/portal/<path>/page.tsx` with `requireCustomerAuth` / `requireCustomerFeatures` in `page.meta.ts` | `.ai/guides/ui.md` → Portal Extension; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
|
|
149
|
+
| **API routes** | Files under `src/modules/<id>/api/**/route.ts` exporting handlers + `metadata` (per-method `requireAuth` / `requireFeatures`) + `openApi`. NEVER write a top-level `export const requireAuth` — the registry no longer recognises it | `.ai/guides/core.md` → API Routes; <https://docs.open-mercato.dev/framework/api/api-development-guide> |
|
|
150
|
+
| **CRUD APIs (factory)** | `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Always set `indexer` so query-index coverage stays correct. Custom (non-`makeCrudRoute`) write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success | `.ai/skills/module-scaffold/SKILL.md` → Create API Routes; <https://docs.open-mercato.dev/framework/api/crud-factory> |
|
|
151
|
+
| **CRUD forms in admin** | `<CrudForm entityId apiPath mode fields />` from `@open-mercato/ui/backend/CrudForm`; helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`; `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors`. Never raw `<form>`, never raw `fetch` | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/crud-form> |
|
|
152
|
+
| **DataTables in admin** | `<DataTable entityId apiPath title columns />` from `@open-mercato/ui/backend/DataTable`. Keep `entityId` and `extensionTableId` stable so widget injection (columns, row actions, bulk actions, filters, toolbar) keeps working | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/data-grids> |
|
|
153
|
+
| **Authorization (RBAC)** | Declare features in `<module>/acl.ts`, grant them in `<module>/setup.ts` `defaultRoleFeatures`, gate pages and routes with `requireFeatures` in `metadata` / `page.meta.ts`. NEVER use `requireRoles` (role names mutate). Run `yarn mercato auth sync-role-acls` after adding new features | `.ai/guides/core.md` → Access Control; <https://docs.open-mercato.dev/framework/rbac/overview> |
|
|
154
|
+
| **Multi-tenant scoping (default for every entity)** | Every tenant-scoped entity MUST include indexed `organization_id` and `tenant_id` columns and every read/write MUST filter by them. The CRUD factory injects scope automatically — do NOT bypass it. For ad-hoc queries use `withScopedPayload` from `@open-mercato/shared/lib/api/scoped` | `.ai/skills/data-model-design/SKILL.md`; <https://docs.open-mercato.dev/architecture/system-overview> |
|
|
155
|
+
| **Encryption maps for sensitive data** | Declare a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` from `@open-mercato/shared/modules/encryption`. Read encrypted columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find`. NEVER hand-roll AES/KMS, NEVER use `em.find` on encrypted columns | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps; <https://docs.open-mercato.dev/user-guide/encryption> |
|
|
156
|
+
| **Cache** | Resolve the cache from DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tag with `tenant:<id>` / `org:<id>` and the entity-type tag so invalidation stays tenant-scoped | `.ai/guides/cache.md`; <https://docs.open-mercato.dev/user-guide/cache-management> |
|
|
157
|
+
| **Background workers** | `src/modules/<id>/workers/*.ts` exporting `metadata: { queue, id?, concurrency? }` + default handler. Never spin up custom queues | `.ai/guides/queue.md`; <https://docs.open-mercato.dev/framework/events/queue-workers> |
|
|
158
|
+
| **Events between modules** | `<module>/events.ts` with `createModuleEvents({ moduleId, events } as const)`. Subscribers in `subscribers/*.ts`. Never call other modules' services directly across module boundaries | `.ai/guides/events.md`; <https://docs.open-mercato.dev/framework/events/overview> |
|
|
159
|
+
| **i18n (every user-facing string)** | `useT()` client-side from `@open-mercato/shared/lib/i18n/context`, `resolveTranslations()` server-side from `@open-mercato/shared/lib/i18n/server`; keys in `src/i18n/<locale>.json`. Never hard-code labels in components | `.ai/guides/shared.md` → i18n |
|
|
160
|
+
|
|
161
|
+
> Rule of thumb: if you find yourself reaching for raw `fetch`, raw `<form>`, ad-hoc `crypto`, ad-hoc `Redis`, or a manual `m2m` join across modules, stop and check the row above — there is a canonical helper.
|
|
162
|
+
|
|
138
163
|
## CRITICAL rules — always follow without exception
|
|
139
164
|
|
|
140
165
|
1. **Entity classes live in `src/modules/<module>/data/entities.ts` and MUST import decorators from `@mikro-orm/decorators/legacy`.** Start there for every schema change.
|
|
@@ -156,9 +181,16 @@ Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
|
|
|
156
181
|
**Dashboards fallback rule.** When the user (or the `trim-unused-modules` skill) disables the `dashboards` module, you MUST update `src/app/(backend)/backend/page.tsx` so it no longer renders `<DashboardScreen />`. Replace the dashboard render with a `redirect(...)` to the first enabled backend page for the current user — preferring pages already registered in the main sidebar group and respecting the admin/superadmin role of the caller. Otherwise `/backend` will crash at build or request time because the removed module no longer ships `DashboardScreen`. Always fall back to `/backend/profile` only if no other backend page is available.
|
|
157
182
|
9. **New features MUST be visible to default roles immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin role and any other appropriate default roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls` so existing tenants pick up the new feature without a reinstall. Use `--tenant <tenantId>` only when the user asks to target one tenant. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
|
|
158
183
|
10. **Strict Design System alignment for every UI change.** Any UI you add or edit MUST use the Open Mercato design system components and tokens. No hardcoded Tailwind status colors (`text-red-500`, `bg-green-100`, etc.) — use semantic tokens (`text-status-error-text`, `bg-status-success-bg`, …). No arbitrary text sizes (`text-[11px]`, `text-[13px]`) — use the Tailwind scale (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`) or the `text-overline` token for 11px uppercase labels. In PAGE BODY UI, use `lucide-react` icons (never inline `<svg>`). Use `StatusBadge` for entity status, `Alert` for inline feedback, `FormField` for standalone form inputs, `SectionHeader` for detail-page section headings, `CollapsibleSection` for collapsible regions, `LoadingMessage`/`Spinner`/`DataLoader` for async states, and `EmptyState` (or DataTable's `emptyState` prop) for empty lists. For list pages, follow `.ai/skills/backend-ui-design/SKILL.md` and prefer the `DataTable` host pattern shown there (`entityId`, `apiPath`, stable `extensionTableId`, and explicit pagination props when you own the data source). Every dialog MUST support `Cmd/Ctrl+Enter` to submit and `Escape` to cancel. Every icon-only button MUST have an `aria-label`. These rules apply to `src/modules/<module>/backend/**` and `src/modules/<module>/frontend/**` alike.
|
|
159
|
-
11. **
|
|
184
|
+
11. **Sensitive / GDPR fields MUST go through the encryption-maps mechanism — never hand-rolled.** The framework provides per-tenant DEKs, KMS-backed key resolution, and a declarative field-level map. Whenever the user asks for "this field encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", or you are designing a column that will hold names, addresses, contacts, free-text notes about people, integration secrets/credentials, or any data subject to a data-processing agreement, you MUST:
|
|
185
|
+
- Declare the entity + field list in `src/modules/<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type imported from `@open-mercato/shared/modules/encryption`).
|
|
186
|
+
- Read those columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` (passing `tenantId` and `organizationId`). Never use raw `em.find` on encrypted columns.
|
|
187
|
+
- For deterministic-lookup fields (e.g., login email), declare a sibling `hashField` in the map so equality lookups still work.
|
|
188
|
+
- Run `yarn mercato entities seed-encryption --tenant <tenantId>` after adding maps so existing tenants pick them up; new tenants get them automatically during `auth:setup`.
|
|
189
|
+
- Treat hand-rolled AES, raw `crypto.subtle`, custom KMS calls, or storing plaintext "for now" as broken — rewrite via the maps. See `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps and <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
190
|
+
12. **BEFORE writing ANY code**, you MUST:
|
|
160
191
|
- Match your task against the **Task → Context Map** above
|
|
161
192
|
- `Read` every file listed in the "Load" column for your task type
|
|
193
|
+
- Read the **Mandatory Module Mechanisms** section above to confirm which canonical primitives apply (CRUD factory, CrudForm, DataTable, RBAC, multi-tenant scoping, encryption maps, cache, events) — do not invent your own substitutes
|
|
162
194
|
- Only then proceed to implementation
|
|
163
195
|
- If your task matches multiple rows, load ALL listed files
|
|
164
196
|
- **Do NOT skip this step.** The guides contain canonical import paths, required patterns, and conventions that CANNOT be reliably inferred from existing code alone. Skipping leads to wrong imports, missing conventions, and rework.
|
|
@@ -197,7 +229,8 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
|
197
229
|
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
198
230
|
|
|
199
231
|
// CRUD forms
|
|
200
|
-
import { CrudForm,
|
|
232
|
+
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
233
|
+
import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
201
234
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
202
235
|
|
|
203
236
|
// UI components (MUST use — never raw <button>)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { defineConfig } from '@playwright/test'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
3
4
|
import { discoverIntegrationSpecFiles } from '@open-mercato/cli/lib/testing/integration-discovery'
|
|
4
5
|
|
|
5
6
|
const captureScreenshots = process.env.PW_CAPTURE_SCREENSHOTS === '1'
|
|
6
7
|
const isGitHubActions = process.env.GITHUB_ACTIONS === 'true'
|
|
8
|
+
// Standalone apps generated from this template declare `"type": "module"`,
|
|
9
|
+
// so the CommonJS `__dirname` is undefined when Playwright loads this config
|
|
10
|
+
// under the Node ESM loader. Reconstruct it from `import.meta.url`.
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
12
|
const projectRoot = path.resolve(__dirname, '..', '..', '..')
|
|
8
13
|
const qaTestResultsRoot = path.join(projectRoot, '.ai', 'qa', 'test-results')
|
|
9
14
|
const normalizePath = (value: string) => value.split(path.sep).join('/')
|
|
@@ -16,7 +16,8 @@ Design entities, relationships, and manage the migration lifecycle following Ope
|
|
|
16
16
|
5. [Cross-Module References](#5-cross-module-references)
|
|
17
17
|
6. [Migration Lifecycle](#6-migration-lifecycle)
|
|
18
18
|
7. [Advanced Patterns](#7-advanced-patterns)
|
|
19
|
-
8. [
|
|
19
|
+
8. [Sensitive Data and Encryption Maps](#8-sensitive-data-and-encryption-maps)
|
|
20
|
+
9. [Anti-Patterns](#9-anti-patterns)
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
@@ -483,7 +484,89 @@ export class TicketHistory {
|
|
|
483
484
|
|
|
484
485
|
---
|
|
485
486
|
|
|
486
|
-
## 8.
|
|
487
|
+
## 8. Sensitive Data and Encryption Maps
|
|
488
|
+
|
|
489
|
+
When the developer asks for "we need this column encrypted", "store this securely", "this is PII", "GDPR", or "encryption at rest" — and whenever you are designing a column that will hold names, addresses, contact information, free-text notes about people, integration credentials, secrets, or any data subject to a data-processing agreement — use the framework's **encryption-maps mechanism**. Do NOT hand-roll AES, raw `crypto.subtle`, custom KMS calls, or "TODO encrypt later" stubs.
|
|
490
|
+
|
|
491
|
+
The mechanism gives you:
|
|
492
|
+
|
|
493
|
+
- Per-tenant Data Encryption Keys (DEKs) resolved through the configured KMS (Vault by default, env-fallback in dev).
|
|
494
|
+
- Declarative, per-entity, per-field encryption with optional deterministic-hash sibling columns for equality lookups (for example login by email).
|
|
495
|
+
- Boot-time auto-application: every enabled module's `defaultEncryptionMaps` is collected during `auth:setup` and applied when `TENANT_DATA_ENCRYPTION=yes`.
|
|
496
|
+
- A `findWithDecryption` / `findOneWithDecryption` read API that transparently decrypts on read.
|
|
497
|
+
|
|
498
|
+
### When encryption is mandatory
|
|
499
|
+
|
|
500
|
+
| Field example | Encrypt? |
|
|
501
|
+
|---|---|
|
|
502
|
+
| First name, last name, preferred name | Yes |
|
|
503
|
+
| Email, phone | Yes — usually with a `hashField` for lookups |
|
|
504
|
+
| Postal address (line 1/2, city, region, postal code, country) | Yes |
|
|
505
|
+
| Free-text comments / notes / activity bodies that mention people | Yes |
|
|
506
|
+
| Integration secrets, API keys, OAuth tokens, webhook signing keys | Yes |
|
|
507
|
+
| Document numbers (tax IDs, national IDs) | Yes |
|
|
508
|
+
| Status enums, counters, timestamps, FKs, currency codes | No |
|
|
509
|
+
| Public catalog metadata (product titles for a public storefront) | Usually no |
|
|
510
|
+
|
|
511
|
+
If you are unsure, default to encrypting and confirm with the user — re-introducing encryption later requires a backfill, but turning it off later is a single map edit.
|
|
512
|
+
|
|
513
|
+
### Declare the map in `<module>/encryption.ts`
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
517
|
+
|
|
518
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
519
|
+
{
|
|
520
|
+
entityId: '<module_id>:<entity>', // matches the entity's table id (colon-separated)
|
|
521
|
+
fields: [
|
|
522
|
+
{ field: 'first_name' },
|
|
523
|
+
{ field: 'last_name' },
|
|
524
|
+
{ field: 'phone' },
|
|
525
|
+
// Sibling deterministic hash for equality lookups (e.g. login by email).
|
|
526
|
+
// Add a matching `<field>_hash varchar` column to the entity.
|
|
527
|
+
{ field: 'email', hashField: 'email_hash' },
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
export default defaultEncryptionMaps
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Read with decryption — never raw `em.find`
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
539
|
+
|
|
540
|
+
// Signature: (em, entityName, where, options?, scope?). MikroORM FindOptions go in slot 4
|
|
541
|
+
// (pass `undefined` if you have none), the decryption scope `{ tenantId, organizationId }` in slot 5.
|
|
542
|
+
const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
|
|
543
|
+
const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Calling `em.find` on an encrypted column returns ciphertext, breaks search, and silently leaks bug surface. The `findWithDecryption` family is the one entry point.
|
|
547
|
+
|
|
548
|
+
### Apply maps to existing tenants
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
New tenants pick up the maps automatically during `auth:setup`. Toggling the **Encrypted** flag on a custom field via the admin UI also only applies to data written **after** the change — backfill historical plaintext rows by running `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` it skips already-encrypted fields and just encrypts plaintext). Use `yarn mercato entities decrypt-database` to roll back. For full UI flows and CLI options see <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
555
|
+
|
|
556
|
+
### Vector search caveat
|
|
557
|
+
|
|
558
|
+
The `vector` module stores raw embeddings unencrypted in the vector store (e.g. pgvector). Even though the source text is decrypted only transiently to compute embeddings, treat the embeddings as sensitive: avoid embedding raw high-sensitivity text and rely on disk-level / managed-database encryption-at-rest for the vector column.
|
|
559
|
+
|
|
560
|
+
### Environment switches
|
|
561
|
+
|
|
562
|
+
- `TENANT_DATA_ENCRYPTION=yes|no` (default `yes`) — set to `no` to run the hooks as no-op (validation still applies).
|
|
563
|
+
- `TENANT_DATA_ENCRYPTION_DEBUG=yes` — log map evaluation, KMS calls, cache hits.
|
|
564
|
+
- `VAULT_ADDR` / `VAULT_TOKEN` / `VAULT_KV_PATH` — HashiCorp Vault KMS configuration.
|
|
565
|
+
- `TENANT_DATA_ENCRYPTION_FALLBACK_KEY` — local/dev fallback key when Vault is unavailable. In dev, `AUTH_SECRET` / `NEXTAUTH_SECRET` is used as a last resort; production falls back to noop KMS.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## 9. Anti-Patterns
|
|
487
570
|
|
|
488
571
|
| Anti-Pattern | Problem | Correct Pattern |
|
|
489
572
|
|-------------|---------|-----------------|
|
|
@@ -498,6 +581,10 @@ export class TicketHistory {
|
|
|
498
581
|
| Storing arrays as comma-separated strings | Can't query, no integrity | Use `jsonb` arrays or junction tables |
|
|
499
582
|
| UUID FK without index | Slow joins | Always `@Index()` on FK columns |
|
|
500
583
|
| Nullable required fields | Data integrity issues | Use `!` assertion for required, `null` for optional |
|
|
584
|
+
| Hand-rolled AES / `crypto.subtle` / custom KMS for sensitive columns | Per-tenant key isolation, hash lookups, key rotation, and admin UI all break | Declare `<module>/encryption.ts` with `defaultEncryptionMaps`; let the framework manage DEKs and Vault |
|
|
585
|
+
| Reading encrypted columns with raw `em.find` / `em.findOne` | Returns ciphertext, breaks search, silent data corruption | Use `findWithDecryption` / `findOneWithDecryption` with `{ tenantId, organizationId }` |
|
|
586
|
+
| Storing PII as plaintext "for now" / TODO comments | GDPR violation, leaks at rest, expensive backfill later | Encrypt from day one; toggling later only protects new writes |
|
|
587
|
+
| Encrypting an `email` column without a `hashField` | Login / equality lookups stop working | Declare a sibling `hashField` (e.g. `email_hash`) in the encryption map and add the matching `varchar` column |
|
|
501
588
|
|
|
502
589
|
---
|
|
503
590
|
|
|
@@ -515,7 +602,8 @@ export class TicketHistory {
|
|
|
515
602
|
- **MUST** specify `length` on all `varchar` columns
|
|
516
603
|
- **MUST NOT** use ORM relationship decorators across module boundaries
|
|
517
604
|
- **MUST NOT** rename or drop columns in a single release
|
|
518
|
-
- **MUST
|
|
605
|
+
- **MUST** declare encrypted columns in `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]`, and read them via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` — see section 8
|
|
606
|
+
- **MUST NOT** hand-roll AES / KMS calls or store sensitive columns as plaintext "for now" — use the encryption-maps mechanism in section 8
|
|
519
607
|
- Use `jsonb` for flexible/nested data, proper columns for queryable/sortable data
|
|
520
608
|
- Use junction tables for many-to-many relationships
|
|
521
609
|
- Derive TypeScript types from Zod schemas, never duplicate type definitions
|
|
@@ -40,14 +40,18 @@ For every piece of code, enforce these code-review rules inline:
|
|
|
40
40
|
| Area | Rule |
|
|
41
41
|
|------|------|
|
|
42
42
|
| Types | No `any` — use zod + `z.infer` |
|
|
43
|
-
| API routes | Export `openApi` and `metadata` with
|
|
44
|
-
|
|
|
43
|
+
| API routes | Export `openApi` and per-method `metadata` with `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`) |
|
|
44
|
+
| **CRUD APIs** | **Use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success. See `AGENTS.md` → Mandatory Module Mechanisms.** |
|
|
45
|
+
| Entities | Standard columns, snake_case, UUID PKs, indexed `organization_id` + `tenant_id` |
|
|
45
46
|
| Security | `findWithDecryption`, tenant scoping, zod validation |
|
|
46
|
-
|
|
|
47
|
-
|
|
|
47
|
+
| **Encryption maps** | **For every PII / GDPR-relevant column the phase touches, declare in `<module>/encryption.ts` exporting `defaultEncryptionMaps` (type from `@open-mercato/shared/modules/encryption`). Reads via `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`). Equality-lookup columns declare a sibling `hashField`. NEVER hand-rolled AES/KMS, `crypto.subtle`, or "encrypt later" stubs. See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps; `.ai/skills/module-scaffold/SKILL.md` § Encryption maps.** |
|
|
48
|
+
| UI | `<CrudForm>`/`<DataTable>` (with stable `entityId` + `extensionTableId`), `apiCall` (never raw `fetch`), `flash()`, `<LoadingMessage>`/`<ErrorMessage>` |
|
|
49
|
+
| **Design System** | **Semantic status tokens (no `text-red-*` / `bg-green-*`); Tailwind text scale (no `text-[13px]` / `text-[11px]`); shared primitives `StatusBadge` / `Alert` / `FormField` / `SectionHeader` / `CollapsibleSection` / `LoadingMessage` / `Spinner` / `DataLoader` / `EmptyState`; lucide-react icons in PAGE BODY (never inline `<svg>`); `aria-label` on every icon-only button; Boy Scout rule on touched lines. See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) + `.ai/skills/backend-ui-design/SKILL.md`.** |
|
|
50
|
+
| **Cache** | **Resolve via DI (`container.resolve('cache')`); tag with `tenant:<id>` / `org:<id>`; declare invalidation per write path. NEVER `new Redis(...)` or raw SQLite.** |
|
|
51
|
+
| Events | `createModuleEvents()` with `as const`, subscribers export `metadata`; cross-module side effects via subscribers, never direct imports |
|
|
48
52
|
| i18n | `useT()` client, `resolveTranslations()` server, no hardcoded strings |
|
|
49
53
|
| Imports | Package-level `@open-mercato/<pkg>/...` for framework imports |
|
|
50
|
-
| Mutations | `useGuardedMutation` when not using CrudForm |
|
|
54
|
+
| Mutations | `useGuardedMutation` when not using CrudForm; pass `retryLastMutation` in injection context |
|
|
51
55
|
| Keyboard | `Cmd/Ctrl+Enter` submit, `Escape` cancel on dialogs |
|
|
52
56
|
| Naming | Modules plural snake_case, events `module.entity.past_tense`, features `module.action` |
|
|
53
57
|
|
|
@@ -41,8 +41,9 @@ Before writing any code, ask the developer:
|
|
|
41
41
|
- [ ] Background workers
|
|
42
42
|
- [ ] CLI commands
|
|
43
43
|
- [ ] Custom fields support
|
|
44
|
+
- [ ] **Sensitive / GDPR-relevant fields** (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — if yes, an `encryption.ts` declaring `defaultEncryptionMaps` is mandatory; see section 11 → Encryption maps
|
|
44
45
|
|
|
45
|
-
If the developer provides a brief description, infer reasonable defaults and confirm.
|
|
46
|
+
If the developer provides a brief description, infer reasonable defaults and confirm. When key fields include names, emails, phones, addresses, free-text comments, or external API keys, treat the encryption checkbox as `yes` by default and confirm with the user rather than skipping it silently.
|
|
46
47
|
|
|
47
48
|
---
|
|
48
49
|
|
|
@@ -57,6 +58,7 @@ src/modules/<module_id>/
|
|
|
57
58
|
├── setup.ts # Tenant init, role features
|
|
58
59
|
├── di.ts # Awilix DI registrations
|
|
59
60
|
├── events.ts # Typed event declarations (if needed)
|
|
61
|
+
├── encryption.ts # Tenant data encryption maps (only if entity has sensitive/GDPR fields)
|
|
60
62
|
├── data/
|
|
61
63
|
│ ├── entities.ts # MikroORM entity classes
|
|
62
64
|
│ └── validators.ts # Zod validation schemas
|
|
@@ -178,7 +180,7 @@ Use `makeCrudRoute` for standard CRUD. Each HTTP method lives in its own file.
|
|
|
178
180
|
**File**: `src/modules/<module_id>/api/get/<entities>.ts`
|
|
179
181
|
|
|
180
182
|
```typescript
|
|
181
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
183
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
182
184
|
import { <Entity> } from '../../data/entities'
|
|
183
185
|
|
|
184
186
|
const handler = makeCrudRoute({
|
|
@@ -201,7 +203,7 @@ export const openApi = {
|
|
|
201
203
|
**File**: `src/modules/<module_id>/api/post/<entities>.ts`
|
|
202
204
|
|
|
203
205
|
```typescript
|
|
204
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
206
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
205
207
|
import { <Entity> } from '../../data/entities'
|
|
206
208
|
import { create<Entity>Schema } from '../../data/validators'
|
|
207
209
|
|
|
@@ -225,7 +227,7 @@ export const openApi = {
|
|
|
225
227
|
**File**: `src/modules/<module_id>/api/put/<entities>.ts`
|
|
226
228
|
|
|
227
229
|
```typescript
|
|
228
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
230
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
229
231
|
import { <Entity> } from '../../data/entities'
|
|
230
232
|
import { update<Entity>Schema } from '../../data/validators'
|
|
231
233
|
|
|
@@ -249,7 +251,7 @@ export const openApi = {
|
|
|
249
251
|
**File**: `src/modules/<module_id>/api/delete/<entities>.ts`
|
|
250
252
|
|
|
251
253
|
```typescript
|
|
252
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
254
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
253
255
|
import { <Entity> } from '../../data/entities'
|
|
254
256
|
|
|
255
257
|
const handler = makeCrudRoute({
|
|
@@ -346,7 +348,7 @@ export const metadata = {
|
|
|
346
348
|
|
|
347
349
|
```tsx
|
|
348
350
|
'use client'
|
|
349
|
-
import { CrudForm } from '@open-mercato/ui/backend/
|
|
351
|
+
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
350
352
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
351
353
|
|
|
352
354
|
export default function Create<Entity>Page() {
|
|
@@ -384,7 +386,7 @@ export const metadata = {
|
|
|
384
386
|
|
|
385
387
|
```tsx
|
|
386
388
|
'use client'
|
|
387
|
-
import { CrudForm } from '@open-mercato/ui/backend/
|
|
389
|
+
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
388
390
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
389
391
|
|
|
390
392
|
export default function Edit<Entity>Page({ params }: { params: { id: string } }) {
|
|
@@ -573,6 +575,52 @@ export default function registerCli(program: any) {
|
|
|
573
575
|
}
|
|
574
576
|
```
|
|
575
577
|
|
|
578
|
+
### Encryption maps (sensitive / GDPR-relevant fields)
|
|
579
|
+
|
|
580
|
+
**Mandatory** when the entity stores PII, contact info, addresses, free-text notes about people, integration credentials, secrets, or anything subject to a data-processing agreement. Do NOT hand-roll AES, KMS calls, or "TODO encrypt later" stubs — the framework provides per-tenant DEKs and a declarative field-level map.
|
|
581
|
+
|
|
582
|
+
**File**: `src/modules/<module_id>/encryption.ts`
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
586
|
+
|
|
587
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
588
|
+
{
|
|
589
|
+
entityId: '<module_id>:<entity>', // matches data/entities.ts table id, colon-separated
|
|
590
|
+
fields: [
|
|
591
|
+
{ field: 'first_name' },
|
|
592
|
+
{ field: 'last_name' },
|
|
593
|
+
{ field: 'phone' },
|
|
594
|
+
// Add a hashField for deterministic equality lookups (e.g. login by email):
|
|
595
|
+
{ field: 'email', hashField: 'email_hash' },
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
]
|
|
599
|
+
|
|
600
|
+
export default defaultEncryptionMaps
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Read paths** — never `em.find` an encrypted column directly:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
607
|
+
|
|
608
|
+
// Signature: (em, entityName, where, options?, scope?) — MikroORM FindOptions in slot 4
|
|
609
|
+
// (pass `undefined` when none), decryption scope in slot 5.
|
|
610
|
+
const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
|
|
611
|
+
const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Apply to existing tenants** after declaring or updating maps:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
New tenants pick up `defaultEncryptionMaps` automatically during `auth:setup`. Toggling the **Encrypted** flag for a field only applies to data written **after** the change — historical plaintext rows stay as they were until backfilled via `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` the command only encrypts plaintext and skips already-encrypted fields). Use `yarn mercato entities decrypt-database` to roll back. For end-to-end usage and admin UI flows see <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
621
|
+
|
|
622
|
+
> Tip: when `email` (or any other column) needs deterministic lookups while encrypted, declare a sibling `hashField` in the map and add a matching `varchar` column to the entity. The framework keeps the hash in sync on writes; queries can target the hash instead of the cleartext column.
|
|
623
|
+
|
|
576
624
|
---
|
|
577
625
|
|
|
578
626
|
## 12. Wire & Verify
|
|
@@ -658,3 +706,5 @@ yarn dev # Start dev server
|
|
|
658
706
|
- **MUST NOT** run `yarn db:migrate` without explicit user confirmation
|
|
659
707
|
- **MUST NOT** create ORM relationships (`@ManyToOne`, `@OneToMany`) to entities in other modules
|
|
660
708
|
- **MUST NOT** edit `.mercato/generated/*` files manually
|
|
709
|
+
- **MUST** declare `<module>/encryption.ts` exporting `defaultEncryptionMaps` whenever the entity stores sensitive / GDPR-relevant fields (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — and read those columns via `findWithDecryption` / `findOneWithDecryption`
|
|
710
|
+
- **MUST NOT** hand-roll AES/KMS calls or store "we'll encrypt this later" plaintext for sensitive columns — use the encryption-maps mechanism described in section 11 → Encryption maps
|
|
@@ -60,6 +60,9 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
|
|
|
60
60
|
3. **Singularity Law**: Singular naming for entities, commands, events, feature IDs.
|
|
61
61
|
4. **Undo Contract**: Is the "Undo" logic as detailed as the "Execute"?
|
|
62
62
|
5. **Module Isolation**: Using Event Bus for side effects or cheating with direct imports?
|
|
63
|
+
6. **Canonical Mechanisms**: Does the spec reach for the framework primitives (`makeCrudRoute`, `<CrudForm>`, `<DataTable>`, `apiCall` / `useGuardedMutation`, DI-resolved cache, `createModuleEvents`) or invent its own substitute? See `AGENTS.md` → **Mandatory Module Mechanisms** for the full canon and links. No raw `fetch`, no raw `<form>`, no `new Redis(...)`, no manual cross-module ORM joins.
|
|
64
|
+
7. **Sensitive Data**: For every PII / GDPR / address / contact / free-text-about-people / integration-credential column the spec proposes, does it declare an `encryption.ts` `defaultEncryptionMaps` entry and route reads through `findWithDecryption`? See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table and `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps. No hand-rolled AES, no `crypto.subtle`, no "TODO encrypt later".
|
|
65
|
+
8. **Design System**: Does every UI mock / className snippet in the spec match the DS canon — semantic status tokens (no `text-red-*` / `bg-green-*`), Tailwind text scale (no `text-[11px]` / `text-[13px]`), shared primitives (`StatusBadge`, `Alert`, `FormField`, `SectionHeader`, `CollapsibleSection`, `LoadingMessage` / `Spinner` / `DataLoader`, `EmptyState`), lucide-react icons in page body (never inline `<svg>`), dialog `Cmd/Ctrl+Enter` submit + `Escape` cancel, `aria-label` on every icon-only button? See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment for every UI change) and `.ai/skills/backend-ui-design/SKILL.md`. Specs that touch existing pages MUST honour the Boy Scout rule.
|
|
63
66
|
|
|
64
67
|
## Quick Rule Reference
|
|
65
68
|
|
|
@@ -68,9 +71,15 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
|
|
|
68
71
|
- **`organization_id`** is mandatory for all tenant-scoped entities.
|
|
69
72
|
- **Undoability** is the default for state changes.
|
|
70
73
|
- **Zod validation** for all API inputs.
|
|
74
|
+
- **Encryption maps** for every sensitive / GDPR-relevant column (declare in `<module>/encryption.ts`, read via `findWithDecryption`) — see `AGENTS.md` → Data Encryption.
|
|
75
|
+
- **Canonical primitives** for CRUD APIs (`makeCrudRoute`), backend forms (`CrudForm`), tables (`DataTable`), HTTP (`apiCall` — never raw `fetch`), non-`CrudForm` writes (`useGuardedMutation`), cache (DI-resolved `@open-mercato/cache`), events (`createModuleEvents`) — see `AGENTS.md` → Mandatory Module Mechanisms.
|
|
76
|
+
- **Design System** tokens and shared UI primitives — no hardcoded status colors, no arbitrary text sizes, no inline `<svg>` in page-body UI. See `AGENTS.md` → Design System.
|
|
71
77
|
|
|
72
78
|
## Reference Materials
|
|
73
79
|
|
|
74
80
|
- [Spec Template](references/spec-template.md)
|
|
75
|
-
- [Spec Checklist](references/spec-checklist.md)
|
|
76
|
-
- [AGENTS.md](../../../AGENTS.md)
|
|
81
|
+
- [Spec Checklist](references/spec-checklist.md) — § 3 covers encryption maps; § 5 covers canonical mechanisms + DS
|
|
82
|
+
- [AGENTS.md](../../../AGENTS.md) — Mandatory Module Mechanisms table; CRITICAL Rule #10 (Design System); CRITICAL Rule #11 (Encryption maps)
|
|
83
|
+
- [`.ai/skills/data-model-design/SKILL.md`](../data-model-design/SKILL.md) → Sensitive Data and Encryption Maps
|
|
84
|
+
- [`.ai/skills/module-scaffold/SKILL.md`](../module-scaffold/SKILL.md) → Encryption maps
|
|
85
|
+
- [`.ai/skills/backend-ui-design/SKILL.md`](../backend-ui-design/SKILL.md) — DS-compliant pages
|
|
@@ -19,26 +19,42 @@ Every item must be answered in the spec or marked N/A with justification.
|
|
|
19
19
|
|
|
20
20
|
## 3. Data Integrity & Security
|
|
21
21
|
|
|
22
|
-
- [ ] Entities include `id`, `organization_id`, `created_at`, `updated_at`
|
|
22
|
+
- [ ] Entities include `id`, `organization_id`, `tenant_id`, `created_at`, `updated_at`, `deleted_at`, `is_active`
|
|
23
23
|
- [ ] Write operations define transaction boundaries
|
|
24
24
|
- [ ] Input validation uses zod schemas
|
|
25
25
|
- [ ] All user input validated before business logic/persistence
|
|
26
|
-
- [ ] Auth guards
|
|
27
|
-
- [ ] Tenant isolation: every scoped query filters by `organization_id`
|
|
26
|
+
- [ ] Auth guards declared per-method in `metadata` (`requireAuth`, `requireFeatures`) — never legacy top-level `export const requireAuth`
|
|
27
|
+
- [ ] Tenant isolation: every scoped query filters by `organization_id` (and `tenant_id` where applicable)
|
|
28
|
+
- [ ] **Encryption maps mechanism is used (no hand-rolled crypto).** For every PII / GDPR-relevant column the spec proposes — names, addresses, contacts, free-text notes about people, integration credentials, secrets, document numbers — the spec MUST declare them in a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type from `@open-mercato/shared/modules/encryption`). Reads MUST go through `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`) from `@open-mercato/shared/lib/encryption/find`. Equality-lookup columns (e.g. login email) declare a sibling `hashField`. No `crypto.subtle`, no custom KMS calls, no "TODO encrypt later". See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps.
|
|
28
29
|
|
|
29
30
|
## 4. Commands, Events & Naming
|
|
30
31
|
|
|
31
32
|
- [ ] Naming is singular and consistent
|
|
32
33
|
- [ ] All mutations are commands with undo logic
|
|
33
|
-
- [ ] Events declared in
|
|
34
|
+
- [ ] Events declared in `<module>/events.ts` via `createModuleEvents({ moduleId, events } as const)` before emitting; cross-module side effects use `subscribers/*.ts`, never direct cross-module imports
|
|
34
35
|
- [ ] Side-effect reversibility is documented
|
|
35
36
|
|
|
36
|
-
## 5. API & UI
|
|
37
|
+
## 5. API & UI — Canonical Mechanisms
|
|
37
38
|
|
|
38
39
|
- [ ] API contracts are complete (request/response/errors)
|
|
39
40
|
- [ ] Routes include `openApi` expectations
|
|
40
|
-
- [ ]
|
|
41
|
-
- [ ]
|
|
41
|
+
- [ ] **Canonical mechanisms — no DIY substitutes.** The spec MUST reach for the framework primitives, not invent its own. See `AGENTS.md` → **Mandatory Module Mechanisms**.
|
|
42
|
+
- [ ] **CRUD APIs** use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes call `validateCrudMutationGuard` before mutation and `runCrudMutationGuardAfterSuccess` after.
|
|
43
|
+
- [ ] **API route files export `metadata`** with per-method `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`).
|
|
44
|
+
- [ ] **Backend forms** use `<CrudForm>` from `@open-mercato/ui/backend/CrudForm` with helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`, throwing `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors` for field-level errors. No raw `<form>`, no raw `fetch`.
|
|
45
|
+
- [ ] **Lists** use `<DataTable entityId apiPath columns />` from `@open-mercato/ui/backend/DataTable` with stable `entityId` / `extensionTableId` so widget injection (columns / row actions / bulk actions / filters / toolbar) keeps working.
|
|
46
|
+
- [ ] **HTTP clients** use `apiCall` / `apiCallOrThrow` / `readApiResultOrThrow` from `@open-mercato/ui/backend/utils/apiCall` — never raw `fetch`.
|
|
47
|
+
- [ ] **Non-`CrudForm` writes** are wrapped in `useGuardedMutation(...).runMutation(...)` and pass `retryLastMutation` in the injection context.
|
|
48
|
+
- [ ] **Cache** is resolved via DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tags include `tenant:<id>` / `org:<id>`.
|
|
49
|
+
- [ ] **Design System compliance for every UI mock and className snippet in the spec.** See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) and `.ai/skills/backend-ui-design/SKILL.md`.
|
|
50
|
+
- [ ] Use semantic status tokens (`text-status-error-text`, `bg-status-success-bg`, `border-status-warning-border`, `text-status-info-icon`, `text-destructive`, `bg-destructive`) — NEVER hardcoded shades like `text-red-500`, `bg-green-100`, `text-amber-*`, `text-emerald-*`, `bg-blue-*`. Status tokens already cover dark mode; no `dark:` overrides.
|
|
51
|
+
- [ ] Use the Tailwind text scale (`text-xs` 12, `text-sm` 14, `text-base` 16, `text-lg` 18, `text-xl` 20, `text-2xl` 24) or `text-overline` for 11px uppercase labels — NEVER arbitrary sizes (`text-[11px]`, `text-[13px]`, `text-[15px]`, `p-[13px]`, `rounded-[24px]`, `z-[9999]`).
|
|
52
|
+
- [ ] Use shared primitives instead of raw HTML: `<Alert variant=...>` for inline status, `flash('msg', 'success|error|warning|info')` for toasts, `useConfirmDialog()` for destructive confirmations, `<StatusBadge>` for entity status, `<FormField label error>` to wrap form inputs, `<SectionHeader title count action>` for section headers, `<CollapsibleSection>` for collapsible regions, `<LoadingMessage>` / `<Spinner>` / `<DataLoader>` for async states, `<EmptyState>` (or DataTable `emptyState` prop) for empty lists.
|
|
53
|
+
- [ ] Use lucide-react icons in PAGE BODY UI (`Page`, `DataTable`, `CrudForm`, cards, buttons) — never inline `<svg>`. Sizes from the `size-{3|4|5|6}` scale; `strokeWidth` is not overridden per-instance. `page.meta.ts` icons follow the `React.createElement('svg', …)` pattern.
|
|
54
|
+
- [ ] Every dialog supports `Cmd/Ctrl+Enter` to submit and `Escape` to cancel.
|
|
55
|
+
- [ ] Every icon-only button has an `aria-label`.
|
|
56
|
+
- [ ] Boy Scout rule: when the spec edits an existing page, any line touched gets migrated to semantic tokens / DS scale.
|
|
57
|
+
- [ ] i18n keys are planned for user-facing strings (`useT()` client-side, `resolveTranslations()` server-side; never hard-coded labels)
|
|
42
58
|
- [ ] Pagination limits defined (`pageSize <= 100`)
|
|
43
59
|
|
|
44
60
|
## 6. Risks & Anti-Patterns
|
|
@@ -48,3 +64,4 @@ Every item must be answered in the spec or marked N/A with justification.
|
|
|
48
64
|
- [ ] Does not introduce cross-module ORM links
|
|
49
65
|
- [ ] Does not skip undoability for state changes
|
|
50
66
|
- [ ] Does not mix MVP with speculative future phases
|
|
67
|
+
- [ ] Does not introduce hand-rolled AES, raw `fetch`, raw `<form>`, `new Redis(...)`, or arbitrary Tailwind sizes / status colors
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Context Routing — {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
**MANDATORY CONTEXT LOADING** — see Critical Rule
|
|
3
|
+
**MANDATORY CONTEXT LOADING** — see the "BEFORE writing ANY code" Critical Rule below.
|
|
4
4
|
Before writing code, find your task below and `Read` the listed files.
|
|
5
5
|
Do NOT load the entire src/ tree — Open Mercato apps can have many modules.
|
|
6
6
|
|
|
@@ -51,7 +51,8 @@ step, you WILL produce incorrect imports and miss required patterns.
|
|
|
51
51
|
| Add caching | `.ai/guides/cache.md` |
|
|
52
52
|
| Add background workers | `.ai/guides/queue.md` |
|
|
53
53
|
| Use i18n (translations) | `.ai/guides/shared.md` → i18n |
|
|
54
|
-
| Use encrypted queries | `.ai/guides/shared.md` → Encryption |
|
|
54
|
+
| Use encrypted queries (read sensitive columns that already have an encryption map; for authoring a NEW sensitive column see the row below first) | `.ai/guides/shared.md` → Encryption |
|
|
55
|
+
| **Encrypt sensitive / GDPR-relevant fields** ("we need this column encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", addresses, contact info, free-text notes about people, integration credentials, secrets) — declare them in the framework's encryption-maps mechanism, never hand-rolled AES/KMS | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps, then `.ai/skills/module-scaffold/SKILL.md` → Encryption maps. Reference: <https://docs.open-mercato.dev/user-guide/encryption> |
|
|
55
56
|
| Use apiCall / UI components | `.ai/guides/ui.md` |
|
|
56
57
|
| Add permissions (RBAC) | `.ai/guides/core.md` → Access Control |
|
|
57
58
|
| Add notifications | `.ai/guides/core.md` → Notifications |
|
|
@@ -130,11 +131,35 @@ src/modules/<id>/
|
|
|
130
131
|
├── ce.ts # Custom entities / custom field sets
|
|
131
132
|
├── translations.ts # Translatable fields per entity
|
|
132
133
|
├── notifications.ts # Notification type definitions
|
|
133
|
-
|
|
134
|
+
├── notifications.client.ts # Client-side notification renderers
|
|
135
|
+
└── encryption.ts # Tenant data encryption maps (defaultEncryptionMaps) for sensitive / GDPR fields
|
|
134
136
|
```
|
|
135
137
|
|
|
136
138
|
Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
|
|
137
139
|
|
|
140
|
+
## Mandatory Module Mechanisms (every module MUST use these — no DIY substitutes)
|
|
141
|
+
|
|
142
|
+
When the user asks to **create a new application** or a **new module**, do not invent your own routing, auth, persistence, forms, or caching. The framework provides one canonical primitive for each concern. If a feature is not on this list and not in the Task → Context Map, ask before adding it — do not roll your own.
|
|
143
|
+
|
|
144
|
+
| Concern | Canonical mechanism | Where to learn it |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| Module structure & auto-discovery | `src/modules/<id>/{api,backend,frontend,data,subscribers,workers,widgets}` + `index.ts` + `src/modules.ts` (`from: '@app'`) — discovered by `yarn generate` | `.ai/skills/module-scaffold/SKILL.md`, `.ai/guides/core.md` → Module Files; <https://docs.open-mercato.dev/framework/modules/overview> |
|
|
147
|
+
| **Backend admin pages** | Auto-discovered files under `src/modules/<id>/backend/**`, paired `page.meta.ts` with `requireAuth` + `requireFeatures` + `pageGroup`/`pageGroupKey`/`pageOrder` | `.ai/skills/backend-ui-design/SKILL.md`, `.ai/skills/module-scaffold/references/navigation-patterns.md`; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
|
|
148
|
+
| **Frontend public pages** | Auto-discovered files under `src/modules/<id>/frontend/**`. Customer portal pages live under `frontend/[orgSlug]/portal/<path>/page.tsx` with `requireCustomerAuth` / `requireCustomerFeatures` in `page.meta.ts` | `.ai/guides/ui.md` → Portal Extension; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
|
|
149
|
+
| **API routes** | Files under `src/modules/<id>/api/**/route.ts` exporting handlers + `metadata` (per-method `requireAuth` / `requireFeatures`) + `openApi`. NEVER write a top-level `export const requireAuth` — the registry no longer recognises it | `.ai/guides/core.md` → API Routes; <https://docs.open-mercato.dev/framework/api/api-development-guide> |
|
|
150
|
+
| **CRUD APIs (factory)** | `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Always set `indexer` so query-index coverage stays correct. Custom (non-`makeCrudRoute`) write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success | `.ai/skills/module-scaffold/SKILL.md` → Create API Routes; <https://docs.open-mercato.dev/framework/api/crud-factory> |
|
|
151
|
+
| **CRUD forms in admin** | `<CrudForm entityId apiPath mode fields />` from `@open-mercato/ui/backend/CrudForm`; helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`; `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors`. Never raw `<form>`, never raw `fetch` | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/crud-form> |
|
|
152
|
+
| **DataTables in admin** | `<DataTable entityId apiPath title columns />` from `@open-mercato/ui/backend/DataTable`. Keep `entityId` and `extensionTableId` stable so widget injection (columns, row actions, bulk actions, filters, toolbar) keeps working | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/data-grids> |
|
|
153
|
+
| **Authorization (RBAC)** | Declare features in `<module>/acl.ts`, grant them in `<module>/setup.ts` `defaultRoleFeatures`, gate pages and routes with `requireFeatures` in `metadata` / `page.meta.ts`. NEVER use `requireRoles` (role names mutate). Run `yarn mercato auth sync-role-acls` after adding new features | `.ai/guides/core.md` → Access Control; <https://docs.open-mercato.dev/framework/rbac/overview> |
|
|
154
|
+
| **Multi-tenant scoping (default for every entity)** | Every tenant-scoped entity MUST include indexed `organization_id` and `tenant_id` columns and every read/write MUST filter by them. The CRUD factory injects scope automatically — do NOT bypass it. For ad-hoc queries use `withScopedPayload` from `@open-mercato/shared/lib/api/scoped` | `.ai/skills/data-model-design/SKILL.md`; <https://docs.open-mercato.dev/architecture/system-overview> |
|
|
155
|
+
| **Encryption maps for sensitive data** | Declare a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` from `@open-mercato/shared/modules/encryption`. Read encrypted columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find`. NEVER hand-roll AES/KMS, NEVER use `em.find` on encrypted columns | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps; <https://docs.open-mercato.dev/user-guide/encryption> |
|
|
156
|
+
| **Cache** | Resolve the cache from DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tag with `tenant:<id>` / `org:<id>` and the entity-type tag so invalidation stays tenant-scoped | `.ai/guides/cache.md`; <https://docs.open-mercato.dev/user-guide/cache-management> |
|
|
157
|
+
| **Background workers** | `src/modules/<id>/workers/*.ts` exporting `metadata: { queue, id?, concurrency? }` + default handler. Never spin up custom queues | `.ai/guides/queue.md`; <https://docs.open-mercato.dev/framework/events/queue-workers> |
|
|
158
|
+
| **Events between modules** | `<module>/events.ts` with `createModuleEvents({ moduleId, events } as const)`. Subscribers in `subscribers/*.ts`. Never call other modules' services directly across module boundaries | `.ai/guides/events.md`; <https://docs.open-mercato.dev/framework/events/overview> |
|
|
159
|
+
| **i18n (every user-facing string)** | `useT()` client-side from `@open-mercato/shared/lib/i18n/context`, `resolveTranslations()` server-side from `@open-mercato/shared/lib/i18n/server`; keys in `src/i18n/<locale>.json`. Never hard-code labels in components | `.ai/guides/shared.md` → i18n |
|
|
160
|
+
|
|
161
|
+
> Rule of thumb: if you find yourself reaching for raw `fetch`, raw `<form>`, ad-hoc `crypto`, ad-hoc `Redis`, or a manual `m2m` join across modules, stop and check the row above — there is a canonical helper.
|
|
162
|
+
|
|
138
163
|
## CRITICAL rules — always follow without exception
|
|
139
164
|
|
|
140
165
|
1. **Entity classes live in `src/modules/<module>/data/entities.ts` and MUST import decorators from `@mikro-orm/decorators/legacy`.** Start there for every schema change.
|
|
@@ -156,9 +181,16 @@ Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
|
|
|
156
181
|
**Dashboards fallback rule.** When the user (or the `trim-unused-modules` skill) disables the `dashboards` module, you MUST update `src/app/(backend)/backend/page.tsx` so it no longer renders `<DashboardScreen />`. Replace the dashboard render with a `redirect(...)` to the first enabled backend page for the current user — preferring pages already registered in the main sidebar group and respecting the admin/superadmin role of the caller. Otherwise `/backend` will crash at build or request time because the removed module no longer ships `DashboardScreen`. Always fall back to `/backend/profile` only if no other backend page is available.
|
|
157
182
|
9. **New features MUST be visible to default roles immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin role and any other appropriate default roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls` so existing tenants pick up the new feature without a reinstall. Use `--tenant <tenantId>` only when the user asks to target one tenant. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
|
|
158
183
|
10. **Strict Design System alignment for every UI change.** Any UI you add or edit MUST use the Open Mercato design system components and tokens. No hardcoded Tailwind status colors (`text-red-500`, `bg-green-100`, etc.) — use semantic tokens (`text-status-error-text`, `bg-status-success-bg`, …). No arbitrary text sizes (`text-[11px]`, `text-[13px]`) — use the Tailwind scale (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`) or the `text-overline` token for 11px uppercase labels. In PAGE BODY UI, use `lucide-react` icons (never inline `<svg>`). Use `StatusBadge` for entity status, `Alert` for inline feedback, `FormField` for standalone form inputs, `SectionHeader` for detail-page section headings, `CollapsibleSection` for collapsible regions, `LoadingMessage`/`Spinner`/`DataLoader` for async states, and `EmptyState` (or DataTable's `emptyState` prop) for empty lists. For list pages, follow `.ai/skills/backend-ui-design/SKILL.md` and prefer the `DataTable` host pattern shown there (`entityId`, `apiPath`, stable `extensionTableId`, and explicit pagination props when you own the data source). Every dialog MUST support `Cmd/Ctrl+Enter` to submit and `Escape` to cancel. Every icon-only button MUST have an `aria-label`. These rules apply to `src/modules/<module>/backend/**` and `src/modules/<module>/frontend/**` alike.
|
|
159
|
-
11. **
|
|
184
|
+
11. **Sensitive / GDPR fields MUST go through the encryption-maps mechanism — never hand-rolled.** The framework provides per-tenant DEKs, KMS-backed key resolution, and a declarative field-level map. Whenever the user asks for "this field encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", or you are designing a column that will hold names, addresses, contacts, free-text notes about people, integration secrets/credentials, or any data subject to a data-processing agreement, you MUST:
|
|
185
|
+
- Declare the entity + field list in `src/modules/<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type imported from `@open-mercato/shared/modules/encryption`).
|
|
186
|
+
- Read those columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` (passing `tenantId` and `organizationId`). Never use raw `em.find` on encrypted columns.
|
|
187
|
+
- For deterministic-lookup fields (e.g., login email), declare a sibling `hashField` in the map so equality lookups still work.
|
|
188
|
+
- Run `yarn mercato entities seed-encryption --tenant <tenantId>` after adding maps so existing tenants pick them up; new tenants get them automatically during `auth:setup`.
|
|
189
|
+
- Treat hand-rolled AES, raw `crypto.subtle`, custom KMS calls, or storing plaintext "for now" as broken — rewrite via the maps. See `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps and <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
190
|
+
12. **BEFORE writing ANY code**, you MUST:
|
|
160
191
|
- Match your task against the **Task → Context Map** above
|
|
161
192
|
- `Read` every file listed in the "Load" column for your task type
|
|
193
|
+
- Read the **Mandatory Module Mechanisms** section above to confirm which canonical primitives apply (CRUD factory, CrudForm, DataTable, RBAC, multi-tenant scoping, encryption maps, cache, events) — do not invent your own substitutes
|
|
162
194
|
- Only then proceed to implementation
|
|
163
195
|
- If your task matches multiple rows, load ALL listed files
|
|
164
196
|
- **Do NOT skip this step.** The guides contain canonical import paths, required patterns, and conventions that CANNOT be reliably inferred from existing code alone. Skipping leads to wrong imports, missing conventions, and rework.
|
|
@@ -197,7 +229,8 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
|
197
229
|
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
198
230
|
|
|
199
231
|
// CRUD forms
|
|
200
|
-
import { CrudForm,
|
|
232
|
+
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
233
|
+
import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
201
234
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
202
235
|
|
|
203
236
|
// UI components (MUST use — never raw <button>)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { defineConfig } from '@playwright/test'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
3
4
|
import { discoverIntegrationSpecFiles } from '@open-mercato/cli/lib/testing/integration-discovery'
|
|
4
5
|
|
|
5
6
|
const captureScreenshots = process.env.PW_CAPTURE_SCREENSHOTS === '1'
|
|
6
7
|
const isGitHubActions = process.env.GITHUB_ACTIONS === 'true'
|
|
8
|
+
// Standalone apps generated from this template declare `"type": "module"`,
|
|
9
|
+
// so the CommonJS `__dirname` is undefined when Playwright loads this config
|
|
10
|
+
// under the Node ESM loader. Reconstruct it from `import.meta.url`.
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
12
|
const projectRoot = path.resolve(__dirname, '..', '..', '..')
|
|
8
13
|
const qaTestResultsRoot = path.join(projectRoot, '.ai', 'qa', 'test-results')
|
|
9
14
|
const normalizePath = (value: string) => value.split(path.sep).join('/')
|
|
@@ -16,7 +16,8 @@ Design entities, relationships, and manage the migration lifecycle following Ope
|
|
|
16
16
|
5. [Cross-Module References](#5-cross-module-references)
|
|
17
17
|
6. [Migration Lifecycle](#6-migration-lifecycle)
|
|
18
18
|
7. [Advanced Patterns](#7-advanced-patterns)
|
|
19
|
-
8. [
|
|
19
|
+
8. [Sensitive Data and Encryption Maps](#8-sensitive-data-and-encryption-maps)
|
|
20
|
+
9. [Anti-Patterns](#9-anti-patterns)
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
@@ -483,7 +484,89 @@ export class TicketHistory {
|
|
|
483
484
|
|
|
484
485
|
---
|
|
485
486
|
|
|
486
|
-
## 8.
|
|
487
|
+
## 8. Sensitive Data and Encryption Maps
|
|
488
|
+
|
|
489
|
+
When the developer asks for "we need this column encrypted", "store this securely", "this is PII", "GDPR", or "encryption at rest" — and whenever you are designing a column that will hold names, addresses, contact information, free-text notes about people, integration credentials, secrets, or any data subject to a data-processing agreement — use the framework's **encryption-maps mechanism**. Do NOT hand-roll AES, raw `crypto.subtle`, custom KMS calls, or "TODO encrypt later" stubs.
|
|
490
|
+
|
|
491
|
+
The mechanism gives you:
|
|
492
|
+
|
|
493
|
+
- Per-tenant Data Encryption Keys (DEKs) resolved through the configured KMS (Vault by default, env-fallback in dev).
|
|
494
|
+
- Declarative, per-entity, per-field encryption with optional deterministic-hash sibling columns for equality lookups (for example login by email).
|
|
495
|
+
- Boot-time auto-application: every enabled module's `defaultEncryptionMaps` is collected during `auth:setup` and applied when `TENANT_DATA_ENCRYPTION=yes`.
|
|
496
|
+
- A `findWithDecryption` / `findOneWithDecryption` read API that transparently decrypts on read.
|
|
497
|
+
|
|
498
|
+
### When encryption is mandatory
|
|
499
|
+
|
|
500
|
+
| Field example | Encrypt? |
|
|
501
|
+
|---|---|
|
|
502
|
+
| First name, last name, preferred name | Yes |
|
|
503
|
+
| Email, phone | Yes — usually with a `hashField` for lookups |
|
|
504
|
+
| Postal address (line 1/2, city, region, postal code, country) | Yes |
|
|
505
|
+
| Free-text comments / notes / activity bodies that mention people | Yes |
|
|
506
|
+
| Integration secrets, API keys, OAuth tokens, webhook signing keys | Yes |
|
|
507
|
+
| Document numbers (tax IDs, national IDs) | Yes |
|
|
508
|
+
| Status enums, counters, timestamps, FKs, currency codes | No |
|
|
509
|
+
| Public catalog metadata (product titles for a public storefront) | Usually no |
|
|
510
|
+
|
|
511
|
+
If you are unsure, default to encrypting and confirm with the user — re-introducing encryption later requires a backfill, but turning it off later is a single map edit.
|
|
512
|
+
|
|
513
|
+
### Declare the map in `<module>/encryption.ts`
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
517
|
+
|
|
518
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
519
|
+
{
|
|
520
|
+
entityId: '<module_id>:<entity>', // matches the entity's table id (colon-separated)
|
|
521
|
+
fields: [
|
|
522
|
+
{ field: 'first_name' },
|
|
523
|
+
{ field: 'last_name' },
|
|
524
|
+
{ field: 'phone' },
|
|
525
|
+
// Sibling deterministic hash for equality lookups (e.g. login by email).
|
|
526
|
+
// Add a matching `<field>_hash varchar` column to the entity.
|
|
527
|
+
{ field: 'email', hashField: 'email_hash' },
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
export default defaultEncryptionMaps
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Read with decryption — never raw `em.find`
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
539
|
+
|
|
540
|
+
// Signature: (em, entityName, where, options?, scope?). MikroORM FindOptions go in slot 4
|
|
541
|
+
// (pass `undefined` if you have none), the decryption scope `{ tenantId, organizationId }` in slot 5.
|
|
542
|
+
const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
|
|
543
|
+
const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Calling `em.find` on an encrypted column returns ciphertext, breaks search, and silently leaks bug surface. The `findWithDecryption` family is the one entry point.
|
|
547
|
+
|
|
548
|
+
### Apply maps to existing tenants
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
New tenants pick up the maps automatically during `auth:setup`. Toggling the **Encrypted** flag on a custom field via the admin UI also only applies to data written **after** the change — backfill historical plaintext rows by running `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` it skips already-encrypted fields and just encrypts plaintext). Use `yarn mercato entities decrypt-database` to roll back. For full UI flows and CLI options see <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
555
|
+
|
|
556
|
+
### Vector search caveat
|
|
557
|
+
|
|
558
|
+
The `vector` module stores raw embeddings unencrypted in the vector store (e.g. pgvector). Even though the source text is decrypted only transiently to compute embeddings, treat the embeddings as sensitive: avoid embedding raw high-sensitivity text and rely on disk-level / managed-database encryption-at-rest for the vector column.
|
|
559
|
+
|
|
560
|
+
### Environment switches
|
|
561
|
+
|
|
562
|
+
- `TENANT_DATA_ENCRYPTION=yes|no` (default `yes`) — set to `no` to run the hooks as no-op (validation still applies).
|
|
563
|
+
- `TENANT_DATA_ENCRYPTION_DEBUG=yes` — log map evaluation, KMS calls, cache hits.
|
|
564
|
+
- `VAULT_ADDR` / `VAULT_TOKEN` / `VAULT_KV_PATH` — HashiCorp Vault KMS configuration.
|
|
565
|
+
- `TENANT_DATA_ENCRYPTION_FALLBACK_KEY` — local/dev fallback key when Vault is unavailable. In dev, `AUTH_SECRET` / `NEXTAUTH_SECRET` is used as a last resort; production falls back to noop KMS.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## 9. Anti-Patterns
|
|
487
570
|
|
|
488
571
|
| Anti-Pattern | Problem | Correct Pattern |
|
|
489
572
|
|-------------|---------|-----------------|
|
|
@@ -498,6 +581,10 @@ export class TicketHistory {
|
|
|
498
581
|
| Storing arrays as comma-separated strings | Can't query, no integrity | Use `jsonb` arrays or junction tables |
|
|
499
582
|
| UUID FK without index | Slow joins | Always `@Index()` on FK columns |
|
|
500
583
|
| Nullable required fields | Data integrity issues | Use `!` assertion for required, `null` for optional |
|
|
584
|
+
| Hand-rolled AES / `crypto.subtle` / custom KMS for sensitive columns | Per-tenant key isolation, hash lookups, key rotation, and admin UI all break | Declare `<module>/encryption.ts` with `defaultEncryptionMaps`; let the framework manage DEKs and Vault |
|
|
585
|
+
| Reading encrypted columns with raw `em.find` / `em.findOne` | Returns ciphertext, breaks search, silent data corruption | Use `findWithDecryption` / `findOneWithDecryption` with `{ tenantId, organizationId }` |
|
|
586
|
+
| Storing PII as plaintext "for now" / TODO comments | GDPR violation, leaks at rest, expensive backfill later | Encrypt from day one; toggling later only protects new writes |
|
|
587
|
+
| Encrypting an `email` column without a `hashField` | Login / equality lookups stop working | Declare a sibling `hashField` (e.g. `email_hash`) in the encryption map and add the matching `varchar` column |
|
|
501
588
|
|
|
502
589
|
---
|
|
503
590
|
|
|
@@ -515,7 +602,8 @@ export class TicketHistory {
|
|
|
515
602
|
- **MUST** specify `length` on all `varchar` columns
|
|
516
603
|
- **MUST NOT** use ORM relationship decorators across module boundaries
|
|
517
604
|
- **MUST NOT** rename or drop columns in a single release
|
|
518
|
-
- **MUST
|
|
605
|
+
- **MUST** declare encrypted columns in `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]`, and read them via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` — see section 8
|
|
606
|
+
- **MUST NOT** hand-roll AES / KMS calls or store sensitive columns as plaintext "for now" — use the encryption-maps mechanism in section 8
|
|
519
607
|
- Use `jsonb` for flexible/nested data, proper columns for queryable/sortable data
|
|
520
608
|
- Use junction tables for many-to-many relationships
|
|
521
609
|
- Derive TypeScript types from Zod schemas, never duplicate type definitions
|
|
@@ -40,14 +40,18 @@ For every piece of code, enforce these code-review rules inline:
|
|
|
40
40
|
| Area | Rule |
|
|
41
41
|
|------|------|
|
|
42
42
|
| Types | No `any` — use zod + `z.infer` |
|
|
43
|
-
| API routes | Export `openApi` and `metadata` with
|
|
44
|
-
|
|
|
43
|
+
| API routes | Export `openApi` and per-method `metadata` with `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`) |
|
|
44
|
+
| **CRUD APIs** | **Use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success. See `AGENTS.md` → Mandatory Module Mechanisms.** |
|
|
45
|
+
| Entities | Standard columns, snake_case, UUID PKs, indexed `organization_id` + `tenant_id` |
|
|
45
46
|
| Security | `findWithDecryption`, tenant scoping, zod validation |
|
|
46
|
-
|
|
|
47
|
-
|
|
|
47
|
+
| **Encryption maps** | **For every PII / GDPR-relevant column the phase touches, declare in `<module>/encryption.ts` exporting `defaultEncryptionMaps` (type from `@open-mercato/shared/modules/encryption`). Reads via `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`). Equality-lookup columns declare a sibling `hashField`. NEVER hand-rolled AES/KMS, `crypto.subtle`, or "encrypt later" stubs. See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps; `.ai/skills/module-scaffold/SKILL.md` § Encryption maps.** |
|
|
48
|
+
| UI | `<CrudForm>`/`<DataTable>` (with stable `entityId` + `extensionTableId`), `apiCall` (never raw `fetch`), `flash()`, `<LoadingMessage>`/`<ErrorMessage>` |
|
|
49
|
+
| **Design System** | **Semantic status tokens (no `text-red-*` / `bg-green-*`); Tailwind text scale (no `text-[13px]` / `text-[11px]`); shared primitives `StatusBadge` / `Alert` / `FormField` / `SectionHeader` / `CollapsibleSection` / `LoadingMessage` / `Spinner` / `DataLoader` / `EmptyState`; lucide-react icons in PAGE BODY (never inline `<svg>`); `aria-label` on every icon-only button; Boy Scout rule on touched lines. See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) + `.ai/skills/backend-ui-design/SKILL.md`.** |
|
|
50
|
+
| **Cache** | **Resolve via DI (`container.resolve('cache')`); tag with `tenant:<id>` / `org:<id>`; declare invalidation per write path. NEVER `new Redis(...)` or raw SQLite.** |
|
|
51
|
+
| Events | `createModuleEvents()` with `as const`, subscribers export `metadata`; cross-module side effects via subscribers, never direct imports |
|
|
48
52
|
| i18n | `useT()` client, `resolveTranslations()` server, no hardcoded strings |
|
|
49
53
|
| Imports | Package-level `@open-mercato/<pkg>/...` for framework imports |
|
|
50
|
-
| Mutations | `useGuardedMutation` when not using CrudForm |
|
|
54
|
+
| Mutations | `useGuardedMutation` when not using CrudForm; pass `retryLastMutation` in injection context |
|
|
51
55
|
| Keyboard | `Cmd/Ctrl+Enter` submit, `Escape` cancel on dialogs |
|
|
52
56
|
| Naming | Modules plural snake_case, events `module.entity.past_tense`, features `module.action` |
|
|
53
57
|
|
|
@@ -41,8 +41,9 @@ Before writing any code, ask the developer:
|
|
|
41
41
|
- [ ] Background workers
|
|
42
42
|
- [ ] CLI commands
|
|
43
43
|
- [ ] Custom fields support
|
|
44
|
+
- [ ] **Sensitive / GDPR-relevant fields** (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — if yes, an `encryption.ts` declaring `defaultEncryptionMaps` is mandatory; see section 11 → Encryption maps
|
|
44
45
|
|
|
45
|
-
If the developer provides a brief description, infer reasonable defaults and confirm.
|
|
46
|
+
If the developer provides a brief description, infer reasonable defaults and confirm. When key fields include names, emails, phones, addresses, free-text comments, or external API keys, treat the encryption checkbox as `yes` by default and confirm with the user rather than skipping it silently.
|
|
46
47
|
|
|
47
48
|
---
|
|
48
49
|
|
|
@@ -57,6 +58,7 @@ src/modules/<module_id>/
|
|
|
57
58
|
├── setup.ts # Tenant init, role features
|
|
58
59
|
├── di.ts # Awilix DI registrations
|
|
59
60
|
├── events.ts # Typed event declarations (if needed)
|
|
61
|
+
├── encryption.ts # Tenant data encryption maps (only if entity has sensitive/GDPR fields)
|
|
60
62
|
├── data/
|
|
61
63
|
│ ├── entities.ts # MikroORM entity classes
|
|
62
64
|
│ └── validators.ts # Zod validation schemas
|
|
@@ -178,7 +180,7 @@ Use `makeCrudRoute` for standard CRUD. Each HTTP method lives in its own file.
|
|
|
178
180
|
**File**: `src/modules/<module_id>/api/get/<entities>.ts`
|
|
179
181
|
|
|
180
182
|
```typescript
|
|
181
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
183
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
182
184
|
import { <Entity> } from '../../data/entities'
|
|
183
185
|
|
|
184
186
|
const handler = makeCrudRoute({
|
|
@@ -201,7 +203,7 @@ export const openApi = {
|
|
|
201
203
|
**File**: `src/modules/<module_id>/api/post/<entities>.ts`
|
|
202
204
|
|
|
203
205
|
```typescript
|
|
204
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
206
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
205
207
|
import { <Entity> } from '../../data/entities'
|
|
206
208
|
import { create<Entity>Schema } from '../../data/validators'
|
|
207
209
|
|
|
@@ -225,7 +227,7 @@ export const openApi = {
|
|
|
225
227
|
**File**: `src/modules/<module_id>/api/put/<entities>.ts`
|
|
226
228
|
|
|
227
229
|
```typescript
|
|
228
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
230
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
229
231
|
import { <Entity> } from '../../data/entities'
|
|
230
232
|
import { update<Entity>Schema } from '../../data/validators'
|
|
231
233
|
|
|
@@ -249,7 +251,7 @@ export const openApi = {
|
|
|
249
251
|
**File**: `src/modules/<module_id>/api/delete/<entities>.ts`
|
|
250
252
|
|
|
251
253
|
```typescript
|
|
252
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/
|
|
254
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
253
255
|
import { <Entity> } from '../../data/entities'
|
|
254
256
|
|
|
255
257
|
const handler = makeCrudRoute({
|
|
@@ -346,7 +348,7 @@ export const metadata = {
|
|
|
346
348
|
|
|
347
349
|
```tsx
|
|
348
350
|
'use client'
|
|
349
|
-
import { CrudForm } from '@open-mercato/ui/backend/
|
|
351
|
+
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
350
352
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
351
353
|
|
|
352
354
|
export default function Create<Entity>Page() {
|
|
@@ -384,7 +386,7 @@ export const metadata = {
|
|
|
384
386
|
|
|
385
387
|
```tsx
|
|
386
388
|
'use client'
|
|
387
|
-
import { CrudForm } from '@open-mercato/ui/backend/
|
|
389
|
+
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
388
390
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
389
391
|
|
|
390
392
|
export default function Edit<Entity>Page({ params }: { params: { id: string } }) {
|
|
@@ -573,6 +575,52 @@ export default function registerCli(program: any) {
|
|
|
573
575
|
}
|
|
574
576
|
```
|
|
575
577
|
|
|
578
|
+
### Encryption maps (sensitive / GDPR-relevant fields)
|
|
579
|
+
|
|
580
|
+
**Mandatory** when the entity stores PII, contact info, addresses, free-text notes about people, integration credentials, secrets, or anything subject to a data-processing agreement. Do NOT hand-roll AES, KMS calls, or "TODO encrypt later" stubs — the framework provides per-tenant DEKs and a declarative field-level map.
|
|
581
|
+
|
|
582
|
+
**File**: `src/modules/<module_id>/encryption.ts`
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
586
|
+
|
|
587
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
588
|
+
{
|
|
589
|
+
entityId: '<module_id>:<entity>', // matches data/entities.ts table id, colon-separated
|
|
590
|
+
fields: [
|
|
591
|
+
{ field: 'first_name' },
|
|
592
|
+
{ field: 'last_name' },
|
|
593
|
+
{ field: 'phone' },
|
|
594
|
+
// Add a hashField for deterministic equality lookups (e.g. login by email):
|
|
595
|
+
{ field: 'email', hashField: 'email_hash' },
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
]
|
|
599
|
+
|
|
600
|
+
export default defaultEncryptionMaps
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Read paths** — never `em.find` an encrypted column directly:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
607
|
+
|
|
608
|
+
// Signature: (em, entityName, where, options?, scope?) — MikroORM FindOptions in slot 4
|
|
609
|
+
// (pass `undefined` when none), decryption scope in slot 5.
|
|
610
|
+
const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
|
|
611
|
+
const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Apply to existing tenants** after declaring or updating maps:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
New tenants pick up `defaultEncryptionMaps` automatically during `auth:setup`. Toggling the **Encrypted** flag for a field only applies to data written **after** the change — historical plaintext rows stay as they were until backfilled via `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` the command only encrypts plaintext and skips already-encrypted fields). Use `yarn mercato entities decrypt-database` to roll back. For end-to-end usage and admin UI flows see <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
621
|
+
|
|
622
|
+
> Tip: when `email` (or any other column) needs deterministic lookups while encrypted, declare a sibling `hashField` in the map and add a matching `varchar` column to the entity. The framework keeps the hash in sync on writes; queries can target the hash instead of the cleartext column.
|
|
623
|
+
|
|
576
624
|
---
|
|
577
625
|
|
|
578
626
|
## 12. Wire & Verify
|
|
@@ -658,3 +706,5 @@ yarn dev # Start dev server
|
|
|
658
706
|
- **MUST NOT** run `yarn db:migrate` without explicit user confirmation
|
|
659
707
|
- **MUST NOT** create ORM relationships (`@ManyToOne`, `@OneToMany`) to entities in other modules
|
|
660
708
|
- **MUST NOT** edit `.mercato/generated/*` files manually
|
|
709
|
+
- **MUST** declare `<module>/encryption.ts` exporting `defaultEncryptionMaps` whenever the entity stores sensitive / GDPR-relevant fields (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — and read those columns via `findWithDecryption` / `findOneWithDecryption`
|
|
710
|
+
- **MUST NOT** hand-roll AES/KMS calls or store "we'll encrypt this later" plaintext for sensitive columns — use the encryption-maps mechanism described in section 11 → Encryption maps
|
|
@@ -60,6 +60,9 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
|
|
|
60
60
|
3. **Singularity Law**: Singular naming for entities, commands, events, feature IDs.
|
|
61
61
|
4. **Undo Contract**: Is the "Undo" logic as detailed as the "Execute"?
|
|
62
62
|
5. **Module Isolation**: Using Event Bus for side effects or cheating with direct imports?
|
|
63
|
+
6. **Canonical Mechanisms**: Does the spec reach for the framework primitives (`makeCrudRoute`, `<CrudForm>`, `<DataTable>`, `apiCall` / `useGuardedMutation`, DI-resolved cache, `createModuleEvents`) or invent its own substitute? See `AGENTS.md` → **Mandatory Module Mechanisms** for the full canon and links. No raw `fetch`, no raw `<form>`, no `new Redis(...)`, no manual cross-module ORM joins.
|
|
64
|
+
7. **Sensitive Data**: For every PII / GDPR / address / contact / free-text-about-people / integration-credential column the spec proposes, does it declare an `encryption.ts` `defaultEncryptionMaps` entry and route reads through `findWithDecryption`? See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table and `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps. No hand-rolled AES, no `crypto.subtle`, no "TODO encrypt later".
|
|
65
|
+
8. **Design System**: Does every UI mock / className snippet in the spec match the DS canon — semantic status tokens (no `text-red-*` / `bg-green-*`), Tailwind text scale (no `text-[11px]` / `text-[13px]`), shared primitives (`StatusBadge`, `Alert`, `FormField`, `SectionHeader`, `CollapsibleSection`, `LoadingMessage` / `Spinner` / `DataLoader`, `EmptyState`), lucide-react icons in page body (never inline `<svg>`), dialog `Cmd/Ctrl+Enter` submit + `Escape` cancel, `aria-label` on every icon-only button? See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment for every UI change) and `.ai/skills/backend-ui-design/SKILL.md`. Specs that touch existing pages MUST honour the Boy Scout rule.
|
|
63
66
|
|
|
64
67
|
## Quick Rule Reference
|
|
65
68
|
|
|
@@ -68,9 +71,15 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
|
|
|
68
71
|
- **`organization_id`** is mandatory for all tenant-scoped entities.
|
|
69
72
|
- **Undoability** is the default for state changes.
|
|
70
73
|
- **Zod validation** for all API inputs.
|
|
74
|
+
- **Encryption maps** for every sensitive / GDPR-relevant column (declare in `<module>/encryption.ts`, read via `findWithDecryption`) — see `AGENTS.md` → Data Encryption.
|
|
75
|
+
- **Canonical primitives** for CRUD APIs (`makeCrudRoute`), backend forms (`CrudForm`), tables (`DataTable`), HTTP (`apiCall` — never raw `fetch`), non-`CrudForm` writes (`useGuardedMutation`), cache (DI-resolved `@open-mercato/cache`), events (`createModuleEvents`) — see `AGENTS.md` → Mandatory Module Mechanisms.
|
|
76
|
+
- **Design System** tokens and shared UI primitives — no hardcoded status colors, no arbitrary text sizes, no inline `<svg>` in page-body UI. See `AGENTS.md` → Design System.
|
|
71
77
|
|
|
72
78
|
## Reference Materials
|
|
73
79
|
|
|
74
80
|
- [Spec Template](references/spec-template.md)
|
|
75
|
-
- [Spec Checklist](references/spec-checklist.md)
|
|
76
|
-
- [AGENTS.md](../../../AGENTS.md)
|
|
81
|
+
- [Spec Checklist](references/spec-checklist.md) — § 3 covers encryption maps; § 5 covers canonical mechanisms + DS
|
|
82
|
+
- [AGENTS.md](../../../AGENTS.md) — Mandatory Module Mechanisms table; CRITICAL Rule #10 (Design System); CRITICAL Rule #11 (Encryption maps)
|
|
83
|
+
- [`.ai/skills/data-model-design/SKILL.md`](../data-model-design/SKILL.md) → Sensitive Data and Encryption Maps
|
|
84
|
+
- [`.ai/skills/module-scaffold/SKILL.md`](../module-scaffold/SKILL.md) → Encryption maps
|
|
85
|
+
- [`.ai/skills/backend-ui-design/SKILL.md`](../backend-ui-design/SKILL.md) — DS-compliant pages
|
|
@@ -19,26 +19,42 @@ Every item must be answered in the spec or marked N/A with justification.
|
|
|
19
19
|
|
|
20
20
|
## 3. Data Integrity & Security
|
|
21
21
|
|
|
22
|
-
- [ ] Entities include `id`, `organization_id`, `created_at`, `updated_at`
|
|
22
|
+
- [ ] Entities include `id`, `organization_id`, `tenant_id`, `created_at`, `updated_at`, `deleted_at`, `is_active`
|
|
23
23
|
- [ ] Write operations define transaction boundaries
|
|
24
24
|
- [ ] Input validation uses zod schemas
|
|
25
25
|
- [ ] All user input validated before business logic/persistence
|
|
26
|
-
- [ ] Auth guards
|
|
27
|
-
- [ ] Tenant isolation: every scoped query filters by `organization_id`
|
|
26
|
+
- [ ] Auth guards declared per-method in `metadata` (`requireAuth`, `requireFeatures`) — never legacy top-level `export const requireAuth`
|
|
27
|
+
- [ ] Tenant isolation: every scoped query filters by `organization_id` (and `tenant_id` where applicable)
|
|
28
|
+
- [ ] **Encryption maps mechanism is used (no hand-rolled crypto).** For every PII / GDPR-relevant column the spec proposes — names, addresses, contacts, free-text notes about people, integration credentials, secrets, document numbers — the spec MUST declare them in a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type from `@open-mercato/shared/modules/encryption`). Reads MUST go through `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`) from `@open-mercato/shared/lib/encryption/find`. Equality-lookup columns (e.g. login email) declare a sibling `hashField`. No `crypto.subtle`, no custom KMS calls, no "TODO encrypt later". See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps.
|
|
28
29
|
|
|
29
30
|
## 4. Commands, Events & Naming
|
|
30
31
|
|
|
31
32
|
- [ ] Naming is singular and consistent
|
|
32
33
|
- [ ] All mutations are commands with undo logic
|
|
33
|
-
- [ ] Events declared in
|
|
34
|
+
- [ ] Events declared in `<module>/events.ts` via `createModuleEvents({ moduleId, events } as const)` before emitting; cross-module side effects use `subscribers/*.ts`, never direct cross-module imports
|
|
34
35
|
- [ ] Side-effect reversibility is documented
|
|
35
36
|
|
|
36
|
-
## 5. API & UI
|
|
37
|
+
## 5. API & UI — Canonical Mechanisms
|
|
37
38
|
|
|
38
39
|
- [ ] API contracts are complete (request/response/errors)
|
|
39
40
|
- [ ] Routes include `openApi` expectations
|
|
40
|
-
- [ ]
|
|
41
|
-
- [ ]
|
|
41
|
+
- [ ] **Canonical mechanisms — no DIY substitutes.** The spec MUST reach for the framework primitives, not invent its own. See `AGENTS.md` → **Mandatory Module Mechanisms**.
|
|
42
|
+
- [ ] **CRUD APIs** use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes call `validateCrudMutationGuard` before mutation and `runCrudMutationGuardAfterSuccess` after.
|
|
43
|
+
- [ ] **API route files export `metadata`** with per-method `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`).
|
|
44
|
+
- [ ] **Backend forms** use `<CrudForm>` from `@open-mercato/ui/backend/CrudForm` with helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`, throwing `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors` for field-level errors. No raw `<form>`, no raw `fetch`.
|
|
45
|
+
- [ ] **Lists** use `<DataTable entityId apiPath columns />` from `@open-mercato/ui/backend/DataTable` with stable `entityId` / `extensionTableId` so widget injection (columns / row actions / bulk actions / filters / toolbar) keeps working.
|
|
46
|
+
- [ ] **HTTP clients** use `apiCall` / `apiCallOrThrow` / `readApiResultOrThrow` from `@open-mercato/ui/backend/utils/apiCall` — never raw `fetch`.
|
|
47
|
+
- [ ] **Non-`CrudForm` writes** are wrapped in `useGuardedMutation(...).runMutation(...)` and pass `retryLastMutation` in the injection context.
|
|
48
|
+
- [ ] **Cache** is resolved via DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tags include `tenant:<id>` / `org:<id>`.
|
|
49
|
+
- [ ] **Design System compliance for every UI mock and className snippet in the spec.** See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) and `.ai/skills/backend-ui-design/SKILL.md`.
|
|
50
|
+
- [ ] Use semantic status tokens (`text-status-error-text`, `bg-status-success-bg`, `border-status-warning-border`, `text-status-info-icon`, `text-destructive`, `bg-destructive`) — NEVER hardcoded shades like `text-red-500`, `bg-green-100`, `text-amber-*`, `text-emerald-*`, `bg-blue-*`. Status tokens already cover dark mode; no `dark:` overrides.
|
|
51
|
+
- [ ] Use the Tailwind text scale (`text-xs` 12, `text-sm` 14, `text-base` 16, `text-lg` 18, `text-xl` 20, `text-2xl` 24) or `text-overline` for 11px uppercase labels — NEVER arbitrary sizes (`text-[11px]`, `text-[13px]`, `text-[15px]`, `p-[13px]`, `rounded-[24px]`, `z-[9999]`).
|
|
52
|
+
- [ ] Use shared primitives instead of raw HTML: `<Alert variant=...>` for inline status, `flash('msg', 'success|error|warning|info')` for toasts, `useConfirmDialog()` for destructive confirmations, `<StatusBadge>` for entity status, `<FormField label error>` to wrap form inputs, `<SectionHeader title count action>` for section headers, `<CollapsibleSection>` for collapsible regions, `<LoadingMessage>` / `<Spinner>` / `<DataLoader>` for async states, `<EmptyState>` (or DataTable `emptyState` prop) for empty lists.
|
|
53
|
+
- [ ] Use lucide-react icons in PAGE BODY UI (`Page`, `DataTable`, `CrudForm`, cards, buttons) — never inline `<svg>`. Sizes from the `size-{3|4|5|6}` scale; `strokeWidth` is not overridden per-instance. `page.meta.ts` icons follow the `React.createElement('svg', …)` pattern.
|
|
54
|
+
- [ ] Every dialog supports `Cmd/Ctrl+Enter` to submit and `Escape` to cancel.
|
|
55
|
+
- [ ] Every icon-only button has an `aria-label`.
|
|
56
|
+
- [ ] Boy Scout rule: when the spec edits an existing page, any line touched gets migrated to semantic tokens / DS scale.
|
|
57
|
+
- [ ] i18n keys are planned for user-facing strings (`useT()` client-side, `resolveTranslations()` server-side; never hard-coded labels)
|
|
42
58
|
- [ ] Pagination limits defined (`pageSize <= 100`)
|
|
43
59
|
|
|
44
60
|
## 6. Risks & Anti-Patterns
|
|
@@ -48,3 +64,4 @@ Every item must be answered in the spec or marked N/A with justification.
|
|
|
48
64
|
- [ ] Does not introduce cross-module ORM links
|
|
49
65
|
- [ ] Does not skip undoability for state changes
|
|
50
66
|
- [ ] Does not mix MVP with speculative future phases
|
|
67
|
+
- [ ] Does not introduce hand-rolled AES, raw `fetch`, raw `<form>`, `new Redis(...)`, or arbitrary Tailwind sizes / status colors
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -62,9 +62,12 @@ mercato test coverage
|
|
|
62
62
|
# Generate code from modules
|
|
63
63
|
yarn generate
|
|
64
64
|
|
|
65
|
-
# Manually purge structural
|
|
65
|
+
# Manually purge structural caches when needed (Redis nav:* + Turbopack barrel mtimes)
|
|
66
66
|
yarn mercato configs cache structural --all-tenants
|
|
67
67
|
|
|
68
|
+
# Escape hatch: clear .next/cache/turbopack when Turbopack still serves a stale chunk
|
|
69
|
+
yarn dev:reset
|
|
70
|
+
|
|
68
71
|
# Database operations
|
|
69
72
|
yarn db:generate # Generate/probe migrations; keep or write only scoped SQL and update the touched snapshot
|
|
70
73
|
yarn db:migrate # Run migrations
|
|
@@ -210,7 +213,7 @@ Practical consequences:
|
|
|
210
213
|
Standalone apps consume the AI framework from `@open-mercato/ai-assistant` (in `node_modules/`). The same conventions used in the monorepo apply here:
|
|
211
214
|
|
|
212
215
|
- Add a typed agent for a new module by creating `<module>/ai-agents.ts` + `<module>/ai-tools.ts` at the **module root**. Run `yarn generate` after.
|
|
213
|
-
- Add inline UI widgets (record cards, custom server-emitted parts) per the [UI Parts guide](https://docs.
|
|
216
|
+
- Add inline UI widgets (record cards, custom server-emitted parts) per the [UI Parts guide](https://docs.open-mercato.dev/framework/ai-assistant/ui-parts).
|
|
214
217
|
- Replace or disable an agent / tool that another module shipped through three paths: extra `aiAgentOverrides` / `aiToolOverrides` exports on the existing `<module>/ai-agents.ts` / `<module>/ai-tools.ts` (per-module), inline on a `ModuleEntry` in `src/modules.ts` (per-app), or programmatically via `applyAiAgentOverrides({...})` / `applyAiToolOverrides({...})` from `@open-mercato/ai-assistant`. `null` disables; a definition replaces. Resolution order is **programmatic → modules.ts → file-based → base**.
|
|
215
218
|
|
|
216
219
|
Example per-module override (preferred when the override should ship with a module):
|
|
@@ -320,6 +323,80 @@ Do this automatically unless the user has explicitly said otherwise. If the curr
|
|
|
320
323
|
|
|
321
324
|
Feature IDs are FROZEN once shipped (they are stored in the DB as `role_features.feature_id`). If a rename is required, add the new ID, grant it, and keep the old one alongside as a deprecated alias until downstream data can be migrated.
|
|
322
325
|
|
|
326
|
+
## Mandatory Module Mechanisms (no DIY substitutes)
|
|
327
|
+
|
|
328
|
+
When building a new application or a new module under `src/modules/<id>/`, do not invent custom routing, auth, persistence, forms, or caching. The framework provides one canonical primitive for each concern. If a feature is not on this list, ask before adding it.
|
|
329
|
+
|
|
330
|
+
| Concern | Canonical mechanism | Reference |
|
|
331
|
+
|---|---|---|
|
|
332
|
+
| Module structure & auto-discovery | `src/modules/<id>/{api,backend,frontend,data,subscribers,workers,widgets}` + `index.ts` + `src/modules.ts` (`from: '@app'`); discovered by `yarn generate` | <https://docs.open-mercato.dev/framework/modules/overview> |
|
|
333
|
+
| Backend admin pages | Auto-discovered files under `backend/**` with paired `page.meta.ts` (`requireAuth`, `requireFeatures`, `pageGroup`, `pageGroupKey`, `pageOrder`) | <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
|
|
334
|
+
| Frontend public pages and customer portal | Auto-discovered files under `frontend/**`. Portal pages live at `frontend/[orgSlug]/portal/<path>/page.tsx` with `requireCustomerAuth` / `requireCustomerFeatures` | <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
|
|
335
|
+
| API routes (auth + OpenAPI) | `src/modules/<id>/api/**/route.ts` exporting handlers + `metadata` (per-method `requireAuth` / `requireFeatures`) + `openApi` | <https://docs.open-mercato.dev/framework/api/api-development-guide> |
|
|
336
|
+
| CRUD APIs (factory) | `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory` | <https://docs.open-mercato.dev/framework/api/crud-factory> |
|
|
337
|
+
| CRUD forms in admin | `<CrudForm entityId apiPath mode fields />` from `@open-mercato/ui/backend/CrudForm`; helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`; `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors`. Never raw `<form>` or raw `fetch` | <https://docs.open-mercato.dev/framework/admin-ui/crud-form> |
|
|
338
|
+
| DataTables in admin | `<DataTable entityId apiPath columns />` from `@open-mercato/ui/backend/DataTable`; keep `entityId` and `extensionTableId` stable so widget injection (columns, row actions, filters, toolbar) keeps working | <https://docs.open-mercato.dev/framework/admin-ui/data-grids> |
|
|
339
|
+
| Authorization (RBAC) | Declare features in `<module>/acl.ts`, grant in `<module>/setup.ts` `defaultRoleFeatures`, gate routes/pages with `requireFeatures` in `metadata`. NEVER use `requireRoles`. Run `yarn mercato auth sync-role-acls` after adding features | <https://docs.open-mercato.dev/framework/rbac/overview> |
|
|
340
|
+
| Multi-tenant scoping (default) | Every tenant-scoped entity MUST include indexed `organization_id` and `tenant_id`; every read/write filters by them. The CRUD factory injects the scope automatically — do not bypass it | <https://docs.open-mercato.dev/architecture/system-overview> |
|
|
341
|
+
| **Encryption maps for sensitive data** | Declare `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]`; read via `findWithDecryption` / `findOneWithDecryption`. NEVER hand-roll AES/KMS — see the next section | <https://docs.open-mercato.dev/user-guide/encryption> |
|
|
342
|
+
| Cache | Resolve from DI (`container.resolve('cache')`); never `new Redis(...)` or raw SQLite. Tag with `tenant:<id>` / `org:<id>` for tenant-scoped invalidation | <https://docs.open-mercato.dev/user-guide/cache-management> |
|
|
343
|
+
| Background workers | `src/modules/<id>/workers/*.ts` exporting `metadata: { queue, id?, concurrency? }` + default handler. Never spin up custom queues | <https://docs.open-mercato.dev/framework/events/queue-workers> |
|
|
344
|
+
| Events between modules | `<module>/events.ts` with `createModuleEvents({ moduleId, events } as const)`; subscribers in `subscribers/*.ts` | <https://docs.open-mercato.dev/framework/events/overview> |
|
|
345
|
+
| i18n (every user-facing string) | `useT()` client-side from `@open-mercato/shared/lib/i18n/context`, `resolveTranslations()` server-side from `@open-mercato/shared/lib/i18n/server`; keys in `src/i18n/<locale>.json` | The `@open-mercato/shared` package (`node_modules/@open-mercato/shared/lib/i18n/`) |
|
|
346
|
+
|
|
347
|
+
> Rule of thumb: if you reach for raw `fetch`, raw `<form>`, ad-hoc `crypto`, ad-hoc `Redis`, or a manual cross-module ORM join, stop and check the row above first.
|
|
348
|
+
|
|
349
|
+
## Data Encryption (sensitive / GDPR-relevant fields)
|
|
350
|
+
|
|
351
|
+
The framework ships a tenant-data-encryption mechanism with per-tenant DEKs, KMS-backed key resolution (Vault by default), declarative field-level maps per module, and deterministic-hash sibling columns for equality lookups. **Use it. Never hand-roll AES, `crypto.subtle`, or custom KMS calls. Never store sensitive columns as plaintext "for now".**
|
|
352
|
+
|
|
353
|
+
When the user asks for "we need this column encrypted", "store this securely", "this is PII", "GDPR", or "encryption at rest" — and whenever you are designing a column that holds names, addresses, contact info, free-text notes about people, integration credentials, secrets, or anything subject to a data-processing agreement — declare an `encryption.ts` at the module root.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// src/modules/<module>/encryption.ts
|
|
357
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
358
|
+
|
|
359
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
360
|
+
{
|
|
361
|
+
entityId: '<module>:<entity>',
|
|
362
|
+
fields: [
|
|
363
|
+
{ field: 'first_name' },
|
|
364
|
+
{ field: 'last_name' },
|
|
365
|
+
{ field: 'phone' },
|
|
366
|
+
// For deterministic equality lookups (e.g. login by email), add a sibling hash column.
|
|
367
|
+
{ field: 'email', hashField: 'email_hash' },
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
export default defaultEncryptionMaps
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Read with decryption — never raw `em.find` / `em.findOne` on encrypted columns:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
379
|
+
|
|
380
|
+
// Signature: (em, entityName, where, options?, scope?). Pass MikroORM FindOptions in slot 4
|
|
381
|
+
// (or `undefined`), and the decryption scope in slot 5.
|
|
382
|
+
const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
|
|
383
|
+
const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Apply maps to existing tenants after declaring them (new tenants pick them up automatically during `auth:setup`):
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Notes:
|
|
393
|
+
|
|
394
|
+
- Toggling the **Encrypted** flag on a custom field via the admin UI only applies to data written *after* the change. Backfill historical plaintext rows with `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` it only encrypts plaintext and skips already-encrypted fields). Use `yarn mercato entities decrypt-database` to roll back.
|
|
395
|
+
- The `vector` module stores raw embeddings unencrypted in the vector store — treat embeddings as sensitive even when the source text is encrypted.
|
|
396
|
+
- Env switches: `TENANT_DATA_ENCRYPTION` (default `yes`), `TENANT_DATA_ENCRYPTION_DEBUG`, Vault (`VAULT_ADDR` / `VAULT_TOKEN` / `VAULT_KV_PATH`), and dev fallback (`TENANT_DATA_ENCRYPTION_FALLBACK_KEY`).
|
|
397
|
+
|
|
398
|
+
Full guide: <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
399
|
+
|
|
323
400
|
## Design System (Strict — applies to every UI change)
|
|
324
401
|
|
|
325
402
|
All UI added or edited in `src/modules/<module>/backend/**` or `src/modules/<module>/frontend/**` MUST follow the Open Mercato design system. Non-compliant code will be blocked in `auto-review-pr`.
|
|
@@ -401,6 +478,8 @@ Notes:
|
|
|
401
478
|
|
|
402
479
|
The standalone template enables the `configs` module from `@open-mercato/core`, so `yarn mercato configs cache ...` is available here after installation. After structural changes such as enabling or disabling modules, adding or removing backend/frontend pages, or changing sidebar/navigation injections, run `yarn generate`. The generator now performs a best-effort structural cache purge automatically after successful generation; if the cache command is unavailable, generation still succeeds.
|
|
403
480
|
|
|
481
|
+
The structural cache purge invalidates two layers: Redis `nav:*` cache keys and Turbopack's module-graph fingerprints (it bumps mtimes on every file in `.mercato/generated/` without changing content). When Turbopack still serves a stale compiled chunk after a structural change — typically because its own internal cache pinned a previous compile error — run `yarn dev:reset` to clear `.next/cache/turbopack` and restart `yarn dev`.
|
|
482
|
+
|
|
404
483
|
Detail/read-model APIs that expose `customFields` must return bare field keys via `normalizeCustomFieldResponse()` (for example `{ priority: 3 }`). Keep `cf_` / `cf:` prefixes for request payloads, filters, and form field IDs only.
|
|
405
484
|
|
|
406
485
|
### Path Aliases
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dev": "node ./scripts/dev.mjs",
|
|
13
13
|
"dev:classic": "node ./scripts/dev.mjs --classic",
|
|
14
14
|
"dev:verbose": "node ./scripts/dev.mjs --verbose",
|
|
15
|
+
"dev:reset": "node ./scripts/dev-reset.mjs",
|
|
15
16
|
"build": "yarn generate && next build",
|
|
16
17
|
"start": "mercato server start",
|
|
17
18
|
"lint": "next lint",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
|
|
6
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const appDir = path.resolve(here, '..')
|
|
8
|
+
const targets = [
|
|
9
|
+
path.join(appDir, '.next', 'cache', 'turbopack'),
|
|
10
|
+
path.join(appDir, '.next', 'cache', 'webpack'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
let removed = 0
|
|
14
|
+
for (const target of targets) {
|
|
15
|
+
if (!fs.existsSync(target)) continue
|
|
16
|
+
fs.rmSync(target, { recursive: true, force: true })
|
|
17
|
+
console.log(`🧹 [dev:reset] removed ${path.relative(appDir, target)}`)
|
|
18
|
+
removed += 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (removed === 0) {
|
|
22
|
+
console.log('🧹 [dev:reset] nothing to clean — .next/cache subdirectories already absent')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log('')
|
|
26
|
+
console.log('✅ Turbopack/webpack cache cleared.')
|
|
27
|
+
console.log(' Stop any running `yarn dev` and start it again to pick up fresh module output.')
|