create-mercato-app 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0

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.
Files changed (172) hide show
  1. package/README.md +1 -1
  2. package/agentic/shared/AGENTS.md.template +3 -3
  3. package/agentic/shared/ai/harness/README.md +2 -2
  4. package/agentic/shared/ai/harness/RELEASE.md +6 -8
  5. package/agentic/shared/ai/harness/cases.json +0 -83
  6. package/agentic/shared/ai/harness/cases.schema.json +4 -4
  7. package/agentic/shared/ai/harness/validators.json +1 -1
  8. package/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +12 -3
  9. package/agentic/shared/scripts/run-agent-harness-release.mjs +11 -37
  10. package/dist/agentic/guides/module-facts.json +598 -5508
  11. package/dist/agentic/guides/module-facts.v2.json +598 -5594
  12. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  13. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  14. package/dist/agentic/guides/modules/api_keys/exact-override-targets.md +0 -1
  15. package/dist/agentic/guides/modules/api_keys/index.md +2 -4
  16. package/dist/agentic/guides/modules/attachments/di-registrations-rich.md +5 -6
  17. package/dist/agentic/guides/modules/attachments/di-service-tokens.md +4 -5
  18. package/dist/agentic/guides/modules/attachments/exact-override-targets.md +5 -6
  19. package/dist/agentic/guides/modules/attachments/index.md +2 -2
  20. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  21. package/dist/agentic/guides/modules/auth/di-registrations-rich.md +2 -3
  22. package/dist/agentic/guides/modules/auth/di-service-tokens.md +2 -3
  23. package/dist/agentic/guides/modules/auth/exact-override-targets.md +2 -3
  24. package/dist/agentic/guides/modules/auth/index.md +1 -1
  25. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  26. package/dist/agentic/guides/modules/catalog/incoming-installed-contributions.md +0 -3
  27. package/dist/agentic/guides/modules/catalog/index.md +2 -2
  28. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  29. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  30. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  31. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  32. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  33. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  34. package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +8 -9
  35. package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +8 -9
  36. package/dist/agentic/guides/modules/communication_channels/incoming-installed-contributions.md +0 -2
  37. package/dist/agentic/guides/modules/communication_channels/index.md +3 -3
  38. package/dist/agentic/guides/modules/configs/index.md +1 -1
  39. package/dist/agentic/guides/modules/content/index.md +1 -1
  40. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  41. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  42. package/dist/agentic/guides/modules/customers/active-extension-bindings.md +3 -6
  43. package/dist/agentic/guides/modules/customers/contribution-resolutions.md +0 -1
  44. package/dist/agentic/guides/modules/customers/events.md +1 -2
  45. package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +0 -7
  46. package/dist/agentic/guides/modules/customers/index.md +4 -4
  47. package/dist/agentic/guides/modules/customers/umes-contributions.md +0 -1
  48. package/dist/agentic/guides/modules/customers/umes-hosts.md +0 -1
  49. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  50. package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +2 -2
  51. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  52. package/dist/agentic/guides/modules/data_sync/workers.md +2 -2
  53. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  54. package/dist/agentic/guides/modules/devices/index.md +1 -1
  55. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  56. package/dist/agentic/guides/modules/directory/exact-override-targets.md +0 -2
  57. package/dist/agentic/guides/modules/directory/index.md +3 -5
  58. package/dist/agentic/guides/modules/entities/index.md +1 -1
  59. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  60. package/dist/agentic/guides/modules/events/index.md +1 -1
  61. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  62. package/dist/agentic/guides/modules/gateway_stripe/exact-override-targets.md +1 -1
  63. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  64. package/dist/agentic/guides/modules/gateway_stripe/workers.md +1 -1
  65. package/dist/agentic/guides/modules/generators/index.md +1 -1
  66. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  67. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  68. package/dist/agentic/guides/modules/messages/index.md +1 -1
  69. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  70. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  71. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  72. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  73. package/dist/agentic/guides/modules/planner/index.md +1 -1
  74. package/dist/agentic/guides/modules/portal/index.md +1 -1
  75. package/dist/agentic/guides/modules/progress/index.md +1 -1
  76. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  77. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  78. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  79. package/dist/agentic/guides/modules/resources/index.md +1 -1
  80. package/dist/agentic/guides/modules/sales/active-extension-bindings.md +0 -1
  81. package/dist/agentic/guides/modules/sales/contribution-resolutions.md +0 -1
  82. package/dist/agentic/guides/modules/sales/exact-override-targets.md +4 -5
  83. package/dist/agentic/guides/modules/sales/host-extension-points.md +1 -1
  84. package/dist/agentic/guides/modules/sales/incoming-installed-contributions.md +0 -2
  85. package/dist/agentic/guides/modules/sales/index.md +3 -3
  86. package/dist/agentic/guides/modules/sales/setup.md +1 -1
  87. package/dist/agentic/guides/modules/sales/umes-contributions.md +0 -1
  88. package/dist/agentic/guides/modules/sales/umes-diagnostics.md +0 -1
  89. package/dist/agentic/guides/modules/sales/umes-hosts.md +0 -9
  90. package/dist/agentic/guides/modules/scheduler/cli-commands.md +0 -1
  91. package/dist/agentic/guides/modules/scheduler/exact-override-targets.md +1 -3
  92. package/dist/agentic/guides/modules/scheduler/index.md +2 -2
  93. package/dist/agentic/guides/modules/scheduler/workers.md +1 -2
  94. package/dist/agentic/guides/modules/search/index.md +1 -1
  95. package/dist/agentic/guides/modules/security/index.md +1 -1
  96. package/dist/agentic/guides/modules/security/owned-contract-module-metadata.md +1 -1
  97. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  98. package/dist/agentic/guides/modules/sso/index.md +1 -1
  99. package/dist/agentic/guides/modules/staff/index.md +1 -1
  100. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  101. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  102. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  103. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  104. package/dist/agentic/guides/modules/translations/index.md +1 -1
  105. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  106. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  107. package/dist/agentic/guides/modules/wms/index.md +1 -1
  108. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  109. package/dist/agentic/guides/reference-module-facts.json +2 -2
  110. package/dist/agentic/guides/reference-modules/example/index.md +1 -1
  111. package/dist/agentic/guides/upstream/manifest.json +1 -1
  112. package/dist/agentic/shared/AGENTS.md.template +3 -3
  113. package/dist/agentic/shared/ai/harness/README.md +2 -2
  114. package/dist/agentic/shared/ai/harness/RELEASE.md +6 -8
  115. package/dist/agentic/shared/ai/harness/cases.json +0 -83
  116. package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
  117. package/dist/agentic/shared/ai/harness/validators.json +1 -1
  118. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +12 -3
  119. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +11 -37
  120. package/package.json +4 -5
  121. package/template/.env.example +5 -65
  122. package/template/.yarnrc.yml.template +5 -9
  123. package/template/AGENTS.md +3 -3
  124. package/template/Dockerfile +4 -15
  125. package/template/docker/scripts/init-or-migrate.sh +27 -3
  126. package/template/docker-compose.fullapp.dev.yml +0 -56
  127. package/template/docker-compose.fullapp.yml +0 -78
  128. package/template/next.config.ts +0 -3
  129. package/template/package.json.template +0 -3
  130. package/template/src/components/DemoFeedbackWidget.tsx +12 -4
  131. package/template/src/components/__tests__/DemoFeedbackWidget.test.tsx +53 -0
  132. package/template/src/components/__tests__/demoFeedbackFlag.test.ts +38 -0
  133. package/template/src/components/demoFeedbackFlag.ts +15 -0
  134. package/template/src/i18n/de.json +11 -13
  135. package/template/src/i18n/en.json +2 -4
  136. package/template/src/i18n/es.json +11 -13
  137. package/template/src/i18n/ko.json +2 -4
  138. package/template/src/i18n/pl.json +2 -4
  139. package/template/src/modules/example/__integration__/TC-EXAMPLE-017-bound-extension-ui.spec.ts +19 -31
  140. package/template/src/modules/example/components/TodosTable.tsx +0 -2
  141. package/template/src/modules.ts +0 -12
  142. package/dist/agentic/guides/modules/api_keys/di-registrations-rich.md +0 -11
  143. package/dist/agentic/guides/modules/api_keys/di-service-tokens.md +0 -11
  144. package/dist/agentic/guides/modules/channel_discord/acl-features.md +0 -13
  145. package/dist/agentic/guides/modules/channel_discord/ai-agents.md +0 -11
  146. package/dist/agentic/guides/modules/channel_discord/backend-pages.md +0 -11
  147. package/dist/agentic/guides/modules/channel_discord/cli-commands.md +0 -13
  148. package/dist/agentic/guides/modules/channel_discord/contribution-resolutions.md +0 -14
  149. package/dist/agentic/guides/modules/channel_discord/di-registrations-rich.md +0 -12
  150. package/dist/agentic/guides/modules/channel_discord/exact-override-targets.md +0 -26
  151. package/dist/agentic/guides/modules/channel_discord/index.md +0 -22
  152. package/dist/agentic/guides/modules/channel_discord/owned-contract-module-metadata.md +0 -11
  153. package/dist/agentic/guides/modules/channel_discord/setup.md +0 -11
  154. package/dist/agentic/guides/modules/channel_discord/umes-contributions.md +0 -14
  155. package/dist/agentic/guides/modules/channel_discord/workers.md +0 -12
  156. package/dist/agentic/guides/modules/directory/di-registrations-rich.md +0 -12
  157. package/dist/agentic/guides/modules/directory/di-service-tokens.md +0 -12
  158. package/dist/agentic/guides/modules/documents/acl-features.md +0 -17
  159. package/dist/agentic/guides/modules/documents/backend-pages.md +0 -13
  160. package/dist/agentic/guides/modules/documents/contribution-resolutions.md +0 -27
  161. package/dist/agentic/guides/modules/documents/di-registrations-rich.md +0 -21
  162. package/dist/agentic/guides/modules/documents/domain-commands.md +0 -37
  163. package/dist/agentic/guides/modules/documents/encryption.md +0 -11
  164. package/dist/agentic/guides/modules/documents/entities.md +0 -21
  165. package/dist/agentic/guides/modules/documents/events.md +0 -25
  166. package/dist/agentic/guides/modules/documents/exact-override-targets.md +0 -39
  167. package/dist/agentic/guides/modules/documents/index.md +0 -25
  168. package/dist/agentic/guides/modules/documents/notifications.md +0 -13
  169. package/dist/agentic/guides/modules/documents/owned-contract-module-metadata.md +0 -11
  170. package/dist/agentic/guides/modules/documents/setup.md +0 -11
  171. package/dist/agentic/guides/modules/documents/umes-contributions.md +0 -27
  172. package/dist/agentic/guides/modules/documents/umes-hosts.md +0 -36
@@ -46,33 +46,6 @@ NEXTAUTH_SECRET=
46
46
 
47
47
  # Public URL used in emails and onboarding flows
48
48
  APP_URL=http://localhost:3000
49
-
50
- # Documents realtime collaboration (optional — run `yarn documents:collab`)
51
- # All collab vars are optional: leave NEXT_PUBLIC_DOCUMENTS_COLLAB_URL unset to
52
- # disable realtime collaboration; the document editor keeps working in
53
- # single-user mode. The URL is embedded at app build time; use a
54
- # browser-reachable wss:// URL in production. App and sidecar must share the
55
- # v2 secret. When DOCUMENTS_COLLAB_ALLOWED_ORIGINS is unset, the sidecar trusts
56
- # APP_URL / NEXT_PUBLIC_APP_URL.
57
- DOCUMENTS_COLLAB_PORT=4101
58
- DOCUMENTS_COLLAB_REDIS_URL=redis://localhost:6379
59
- # Required with collaboration Redis in production. Every sidecar replica in one
60
- # deployment must share this value, and deployments sharing Redis must use
61
- # different values. Allowed: letters, numbers, dot, underscore, colon, dash.
62
- DOCUMENTS_COLLAB_REDIS_PREFIX=open-mercato:documents:collab:local
63
- # Local development example: ws://localhost:4101. Production runtime ignores a
64
- # loopback value and falls back to single-user editing without failing the build.
65
- NEXT_PUBLIC_DOCUMENTS_COLLAB_URL=
66
- # Optional exact comma-separated browser origins; defaults to APP_URL.
67
- DOCUMENTS_COLLAB_ALLOWED_ORIGINS=
68
- # Required only when collaboration is enabled; generate a unique high-entropy
69
- # value containing at least 32 UTF-8 bytes. The sidecar fails closed when the
70
- # secret is missing or too short.
71
- DOCUMENTS_COLLAB_JWT_SECRET_V2=
72
- # Optional v1 rollout secret; when set it must differ from the v2 secret.
73
- # DOCUMENTS_COLLAB_JWT_SECRET=
74
- # Standalone sidecars set this to the app root (for example /app in Docker).
75
- DOCUMENTS_COLLAB_APP_ROOT=.
76
49
  # Additional trusted origins for app requests that must pass host validation.
77
50
  # APP_URL is required in production for security-sensitive email links.
78
51
  # In dev mode these values also govern the HMR WebSocket (/_next/webpack-hmr):
@@ -91,7 +64,11 @@ DOCUMENTS_COLLAB_APP_ROOT=.
91
64
 
92
65
  # Enable self-service onboarding (default: false)
93
66
  SELF_SERVICE_ONBOARDING_ENABLED=false
94
- # Enable Open Mercato demo widgets (default: false)
67
+ # Enable Open Mercato demo widgets. The code default when this is unset is
68
+ # `true` — scaffolds ship it off explicitly, so keep the line rather than
69
+ # deleting it. To hide just the contact/feedback widget while keeping demo mode
70
+ # on, set the `ff_om_hide_contact` key in the browser's localStorage:
71
+ # https://docs.open-mercato.dev/architecture/configuration-decision-guide#ff-om-hide-contact
95
72
  DEMO_MODE=false
96
73
 
97
74
  # Hide the backend footer status bar (app version + terms/privacy links) (default: false)
@@ -135,10 +112,6 @@ OM_SECURITY_WEBAUTHN_RP_NAME=Open Mercato
135
112
  # Defaults to the hostname from APP_URL / NEXT_PUBLIC_APP_URL when left blank.
136
113
  OM_SECURITY_WEBAUTHN_RP_ID=
137
114
  OM_SECURITY_RECOVERY_CODE_COUNT=10
138
- # ⚠️ EMERGENCY BREAK-GLASS ONLY — when true, MFA enforcement is DISABLED platform-wide:
139
- # all MFA challenges and enrollment redirects are skipped and sudo step-up always falls back
140
- # to password. Keep false in normal operation; when enabled the app emits a warn-level
141
- # structured log at startup and on every bypassed challenge so the event is auditable.
142
115
  OM_SECURITY_MFA_EMERGENCY_BYPASS=false
143
116
 
144
117
  # Outgoing email sender priority:
@@ -276,11 +249,6 @@ OM_QUERY_INDEX_AUTO_REINDEX_DEBOUNCE_MS=30000
276
249
  # Skip immediate coverage recalculation on read (default: false)
277
250
  OPTIMIZE_INDEX_COVERAGE_STATS=false
278
251
 
279
- # Maximum reported list total (default: 10000). Counting stops after this many
280
- # matching rows; capped totals render as "10 000+" and responses carry
281
- # totalIsCapped: true. 0 disables capping and restores exact counts everywhere.
282
- OM_LIST_COUNT_CAP=10000
283
-
284
252
  # Continue serving results from entity indexes when coverage gaps are detected (default: true)
285
253
  FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES=true
286
254
 
@@ -290,11 +258,6 @@ OM_SEARCH_MIN_LEN=3
290
258
  OM_SEARCH_ENABLE_PARTIAL=true
291
259
  OM_SEARCH_HASH_ALGO=sha256
292
260
  OM_SEARCH_STORE_RAW_TOKENS=false
293
- # Run like/ilike on PLAINTEXT base columns as exact SQL ILIKE instead of the approximate
294
- # search-token match (encrypted columns always keep the token path). Off by default: token
295
- # matching avoids the full scan an unanchored ILIKE may need, but it is approximate --
296
- # fragments shorter than OM_SEARCH_MIN_LEN vanish, so short document numbers over-match.
297
- OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS=false
298
261
  # Bound token fan-out per indexed record (0 disables an individual limit)
299
262
  OM_SEARCH_MAX_FIELD_CHARS=20000
300
263
  OM_SEARCH_MAX_TOKENS_PER_FIELD=5000
@@ -306,14 +269,6 @@ OM_SEARCH_MAX_TOKENS_PER_RECORD=20000
306
269
  # OM_SEARCH_FIELD_BLOCKLIST=body,customers:customer_interaction@notes
307
270
  # TTL for the process-level "does this entity have search tokens?" probe cache (ms, 0 disables).
308
271
  OM_SEARCH_TOKEN_PRESENCE_CACHE_MS=30000
309
- # Offer base `customers:customer_entity` rows as search results alongside the person/company
310
- # profiles. Off by default: a customer is found through their profile, which is what carries the
311
- # detail-page link, so the base row only adds a second, navigation-less copy of the same customer
312
- # to global search. Turn it on to restore the previous behavior -- both rows are returned and the
313
- # presenter enricher merges the pair back into one canonical result. Read-side only: the rows stay
314
- # in search_tokens either way, because the People/Companies list search and the query engines'
315
- # encrypted like/ilike rewrite resolve ids through them.
316
- OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY=false
317
272
 
318
273
  # Search module debug logging (Meilisearch, vector search, reindex operations)
319
274
  OM_SEARCH_DEBUG=false
@@ -795,21 +750,6 @@ ACME_EMAIL=
795
750
  # OM_CHANNEL_IMAP_ALLOW_INTERNAL_HOSTS=false
796
751
  # OM_CHANNEL_IMAP_HARD_CAP_PER_POLL=200
797
752
  # OM_CHANNEL_IMAP_SOCKET_TIMEOUT_MS=60000
798
- # Discord bot channel. Normally connected interactively in /backend/integrations;
799
- # set the three credential vars only for deployment-managed bootstrap. They are
800
- # applied on tenant creation and can be re-applied with
801
- # `yarn mercato channel_discord configure-from-env --tenant <id> --org <id>`.
802
- # OM_CHANNEL_DISCORD_BOT_TOKEN=
803
- # OM_CHANNEL_DISCORD_APPLICATION_ID=
804
- # OM_CHANNEL_DISCORD_PUBLIC_KEY=
805
- # OM_CHANNEL_DISCORD_GUILD_ID=
806
- # OM_CHANNEL_DISCORD_DEFAULT_CHANNEL_ID=
807
- # Overwrite credentials that already exist for the tenant (default: keep them).
808
- # OM_CHANNEL_DISCORD_FORCE_PRECONFIGURE=false
809
- # Skip opening gateway WebSockets (CI / send-only deployments).
810
- # OM_CHANNEL_DISCORD_GATEWAY_DISABLED=false
811
- # Discord REST request timeout in milliseconds.
812
- # OM_CHANNEL_DISCORD_REQUEST_TIMEOUT_MS=15000
813
753
 
814
754
  # --- Logging (structured logging facade, @open-mercato/shared/lib/logger) ---
815
755
  # Docs: https://docs.openmercato.com/framework/runtime/logging
@@ -1,15 +1,11 @@
1
1
  nodeLinker: node-modules
2
2
 
3
3
  # Yarn refuses versions younger than `npmMinimalAgeGate` (default 1 day) with
4
- # "All versions satisfying ... are quarantined". Raised to 5 days to close the
5
- # supply-chain window where a compromised maintainer account pushes a malicious
6
- # patch before any advisory exists. For urgent patches use --no-time-gate.
7
- # This app pins the exact @open-mercato release it was scaffolded from, which
8
- # is usually minutes old, so the first-party scope opts out while every
9
- # third-party package keeps the cooldown. A per-scope `npmRegistryServer` entry
10
- # shadows a top-level `npmMinimalAgeGate: 0`, so the opt-out has to be
11
- # expressed as a preapproval.
12
- npmMinimalAgeGate: 5d
4
+ # "All versions satisfying ... are quarantined". This app pins the exact
5
+ # @open-mercato release it was scaffolded from, which is usually minutes old, so
6
+ # the first-party scope opts out while every third-party package keeps the
7
+ # cooldown. A per-scope `npmRegistryServer` entry shadows a top-level
8
+ # `npmMinimalAgeGate: 0`, so the opt-out has to be expressed as a preapproval.
13
9
  npmPreapprovedPackages:
14
10
  - "@open-mercato/*"
15
11
  {{REGISTRY_CONFIG}}
@@ -12,7 +12,7 @@ Route first; never probe unmatched context.
12
12
  - Put entities in `src/modules/<id>/data/entities.ts`; API routes need per-method `metadata` + `openApi`.
13
13
  - Editable records expose `updated_at`/`updatedAt`; custom update/delete clients send the version and surface 409s.
14
14
  - Run `yarn db:generate`, review scoped SQL/snapshot, and ask before applying it.
15
- - Run `yarn generate` after discovery files/`src/modules.ts`/routes/pages/events/widgets/agents/tools/workflows change.
15
+ - Run `yarn generate` after discovery files, `src/modules.ts`, routes, pages, events, widgets, agents, tools, or workflows change.
16
16
  - Contract-surface changes (route/schema/ID/export/seam/signature/event payload/CLI) MUST read `.ai/guides/upstream/BACKWARD_COMPATIBILITY.md`; tenant/org scope alone is not a contract.
17
17
  - Localize strings; use shared UI/tokens and cover loading/empty/error/conflict/keyboard/a11y.
18
18
 
@@ -23,8 +23,8 @@ Route first; never probe unmatched context.
23
23
  ## Never
24
24
 
25
25
  - Never leak tenants, trust payload scope, or treat missing scope as unrestricted.
26
- - Never edit `node_modules`/`.mercato/generated/**`/generated facts/shipped migrations.
27
- - Never use cross-module ORM relations; use IDs/snapshots/events/enrichers/extensions/optional DI.
26
+ - Never edit `node_modules`, `.mercato/generated/**`, generated facts, or shipped migrations.
27
+ - Never use cross-module ORM relations; use IDs/snapshots, events, enrichers, extensions, or optional DI.
28
28
  - Never use raw admin `fetch`/`<form>`, ad hoc crypto/cache/queues, role-name guards, or direct mutations when helpers exist.
29
29
  - Never hard-code user strings/status colors; expose secrets/transcripts; or guess answerable contracts.
30
30
 
@@ -3,8 +3,6 @@ FROM node:24-alpine AS builder
3
3
  ENV NEXT_TELEMETRY_DISABLED=1 \
4
4
  PATH=/app/node_modules/.bin:$PATH
5
5
  ARG OPEN_MERCATO_DOCKER_REGISTRY_HOST=host.docker.internal
6
- ARG NEXT_PUBLIC_DOCUMENTS_COLLAB_URL
7
- ENV NEXT_PUBLIC_DOCUMENTS_COLLAB_URL=${NEXT_PUBLIC_DOCUMENTS_COLLAB_URL}
8
6
 
9
7
  WORKDIR /app
10
8
 
@@ -60,31 +58,22 @@ COPY docker/scripts/init-or-migrate.sh /app/docker/scripts/init-or-migrate.sh
60
58
  RUN chmod +x /app/docker/scripts/dev-entrypoint.sh
61
59
  RUN chmod +x /app/docker/scripts/init-or-migrate.sh
62
60
 
63
- EXPOSE 3000 4101
61
+ EXPOSE 3000
64
62
  CMD ["/bin/sh", "/app/docker/scripts/dev-entrypoint.sh"]
65
63
 
66
64
  FROM node:24-alpine AS runner
67
65
 
68
66
  ARG CONTAINER_PORT=3000
69
- ARG DOCUMENTS_COLLAB_PORT=4101
70
67
  ARG OPEN_MERCATO_DOCKER_REGISTRY_HOST=host.docker.internal
71
- # Chromium backs the Documents PDF export (puppeteer-core). Build with
72
- # --build-arg INSTALL_CHROMIUM=1 to include it; PDF export otherwise returns 503.
73
- ARG INSTALL_CHROMIUM=0
74
68
 
75
69
  ENV NODE_ENV=production \
76
70
  NEXT_TELEMETRY_DISABLED=1 \
77
71
  PATH=/app/node_modules/.bin:$PATH \
78
- PORT=${CONTAINER_PORT} \
79
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
72
+ PORT=${CONTAINER_PORT}
80
73
 
81
74
  WORKDIR /app
82
75
 
83
- RUN if [ "$INSTALL_CHROMIUM" = "1" ]; then \
84
- apk add --no-cache ca-certificates chromium openssl; \
85
- else \
86
- apk add --no-cache ca-certificates openssl; \
87
- fi
76
+ RUN apk add --no-cache ca-certificates openssl
88
77
  RUN corepack enable
89
78
 
90
79
  COPY package.json yarn.lock .yarnrc.yml ./
@@ -118,5 +107,5 @@ RUN adduser -D -u 1001 omuser \
118
107
 
119
108
  USER omuser
120
109
 
121
- EXPOSE ${CONTAINER_PORT} ${DOCUMENTS_COLLAB_PORT}
110
+ EXPOSE ${CONTAINER_PORT}
122
111
  CMD ["yarn", "start"]
@@ -4,6 +4,7 @@ set -e
4
4
  MARKER_FILE="${INIT_MARKER_FILE:-/tmp/init-marker/.seeded}"
5
5
  INIT_COMMAND="${INIT_COMMAND:-yarn initialize}"
6
6
  MIGRATE_COMMAND="${MIGRATE_COMMAND:-yarn db:migrate}"
7
+ SYNC_ROLE_ACLS_COMMAND="${SYNC_ROLE_ACLS_COMMAND:-yarn mercato auth sync-role-acls}"
7
8
  ALREADY_INITIALIZED_PATTERN='Initialization aborted: found [0-9][0-9]* existing user\(s\) in the database\.'
8
9
  CLI_NOT_FOUND_PATTERN='command not found: mercato'
9
10
 
@@ -11,12 +12,16 @@ run_command_with_cli_recovery() {
11
12
  COMMAND="$1"
12
13
  LOG_FILE="$2"
13
14
 
14
- if sh -lc "${COMMAND}" >"${LOG_FILE}" 2>&1; then
15
+ # `STATUS=$?` after a failed `if ...; then ...; fi` reads the compound statement's own
16
+ # status, which POSIX defines as 0 — so every failure used to be reported as success and
17
+ # a broken migration silently fell through to `yarn start`. Capture the status directly.
18
+ STATUS=0
19
+ sh -lc "${COMMAND}" >"${LOG_FILE}" 2>&1 || STATUS=$?
20
+
21
+ if [ "${STATUS}" -eq 0 ]; then
15
22
  return 0
16
23
  fi
17
24
 
18
- STATUS=$?
19
-
20
25
  if ! grep -Fq "${CLI_NOT_FOUND_PATTERN}" "${LOG_FILE}"; then
21
26
  return "${STATUS}"
22
27
  fi
@@ -26,6 +31,23 @@ run_command_with_cli_recovery() {
26
31
  sh -lc "${COMMAND}" >"${LOG_FILE}" 2>&1
27
32
  }
28
33
 
34
+ # Modules enabled after the database was first seeded declare new `defaultRoleFeatures`
35
+ # that existing tenants never received — `mercato init` applies them only at bootstrap.
36
+ # The sync command is idempotent, so run it after every migration and keep it non-fatal:
37
+ # a stale grant set must not block the container from starting.
38
+ sync_role_acls() {
39
+ SYNC_LOG_FILE="$(mktemp)"
40
+
41
+ if run_command_with_cli_recovery "${SYNC_ROLE_ACLS_COMMAND}" "${SYNC_LOG_FILE}"; then
42
+ cat "${SYNC_LOG_FILE}"
43
+ else
44
+ echo "WARNING: role ACL sync failed; newly enabled modules may be inaccessible until it is run manually."
45
+ cat "${SYNC_LOG_FILE}"
46
+ fi
47
+
48
+ rm -f "${SYNC_LOG_FILE}"
49
+ }
50
+
29
51
  if [ ! -f "${MARKER_FILE}" ]; then
30
52
  echo "First run: full initialization..."
31
53
  LOG_FILE="$(mktemp)"
@@ -48,6 +70,7 @@ if [ ! -f "${MARKER_FILE}" ]; then
48
70
  if run_command_with_cli_recovery "${MIGRATE_COMMAND}" "${RECOVERY_LOG_FILE}"; then
49
71
  cat "${RECOVERY_LOG_FILE}"
50
72
  rm -f "${RECOVERY_LOG_FILE}"
73
+ sync_role_acls
51
74
  else
52
75
  RECOVERY_STATUS=$?
53
76
  cat "${RECOVERY_LOG_FILE}"
@@ -70,6 +93,7 @@ LOG_FILE="$(mktemp)"
70
93
  if run_command_with_cli_recovery "${MIGRATE_COMMAND}" "${LOG_FILE}"; then
71
94
  cat "${LOG_FILE}"
72
95
  rm -f "${LOG_FILE}"
96
+ sync_role_acls
73
97
  exit 0
74
98
  fi
75
99
 
@@ -40,8 +40,6 @@ services:
40
40
  APP_URL: ${APP_URL:-http://localhost:3000}
41
41
  # Additional trusted app/dev origins for the Open Mercato origin allowlist (resolveAllowedDevOrigins)
42
42
  APP_ALLOWED_ORIGINS: ${APP_ALLOWED_ORIGINS:-}
43
- NEXT_PUBLIC_DOCUMENTS_COLLAB_URL: ${NEXT_PUBLIC_DOCUMENTS_COLLAB_URL:-}
44
- DOCUMENTS_COLLAB_JWT_SECRET_V2: ${DOCUMENTS_COLLAB_JWT_SECRET_V2:-dev-only-documents-collab-v2-secret}
45
43
  CACHE_REDIS_URL: redis://redis:6379
46
44
  CACHE_STRATEGY: ${CACHE_STRATEGY:-redis}
47
45
  ENABLE_CRUD_API_CACHE: ${ENABLE_CRUD_API_CACHE:-true}
@@ -78,60 +76,6 @@ services:
78
76
  networks:
79
77
  - mercato-network-fullapp
80
78
 
81
- documents-collab:
82
- profiles:
83
- - documents-collab
84
- build:
85
- context: .
86
- target: dev
87
- args:
88
- - OPEN_MERCATO_DOCKER_REGISTRY_HOST=host.docker.internal
89
- extra_hosts:
90
- - "host.docker.internal:host-gateway"
91
- command: ["yarn", "documents:collab"]
92
- working_dir: /app
93
- # Restart so a first boot that races the shared app build comes back up
94
- # once its dependencies are present, instead of exiting and staying dead.
95
- restart: unless-stopped
96
- volumes:
97
- - .:/app
98
- - app_node_modules:/app/node_modules
99
- ports:
100
- - "${DOCUMENTS_COLLAB_PORT:-4101}:${DOCUMENTS_COLLAB_PORT:-4101}"
101
- environment:
102
- NODE_ENV: development
103
- DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-open-mercato}
104
- JWT_SECRET: ${JWT_SECRET:-JWT}
105
- APP_URL: ${APP_URL:-http://localhost:3000}
106
- NEXT_PUBLIC_APP_URL: ${APP_URL:-http://localhost:3000}
107
- DOCUMENTS_COLLAB_PORT: ${DOCUMENTS_COLLAB_PORT:-4101}
108
- DOCUMENTS_COLLAB_REDIS_URL: redis://redis:6379
109
- DOCUMENTS_COLLAB_REDIS_PREFIX: ${DOCUMENTS_COLLAB_REDIS_PREFIX:-open-mercato:documents:collab:${DEPLOY_ENV:-local}}
110
- # Same Redis as the app so multi-instance collaboration sync works out of the box
111
- # (DOCUMENTS_COLLAB_REDIS_URL wins when both are set).
112
- REDIS_URL: redis://redis:6379
113
- DOCUMENTS_COLLAB_ALLOWED_ORIGINS: ${DOCUMENTS_COLLAB_ALLOWED_ORIGINS:-http://localhost:3000}
114
- DOCUMENTS_COLLAB_JWT_SECRET_V2: ${DOCUMENTS_COLLAB_JWT_SECRET_V2:-dev-only-documents-collab-v2-secret}
115
- DOCUMENTS_COLLAB_JWT_SECRET: ${DOCUMENTS_COLLAB_JWT_SECRET:-}
116
- DOCUMENTS_COLLAB_APP_ROOT: /app
117
- # Mirror the app service's tenant-encryption configuration so the sidecar
118
- # can decrypt the same data as the app.
119
- TENANT_DATA_ENCRYPTION: ${TENANT_DATA_ENCRYPTION:-true}
120
- TENANT_DATA_ENCRYPTION_DEBUG: ${TENANT_DATA_ENCRYPTION_DEBUG:-false}
121
- TENANT_DATA_ENCRYPTION_FALLBACK_KEY: ${TENANT_DATA_ENCRYPTION_FALLBACK_KEY:-dev-tenant-encryption-fallback-key-32chars}
122
- TENANT_DATA_ENCRYPTION_KEY: ${TENANT_DATA_ENCRYPTION_KEY:-}
123
- CACHE_REDIS_URL: redis://redis:6379
124
- CACHE_STRATEGY: ${CACHE_STRATEGY:-redis}
125
- depends_on:
126
- app:
127
- condition: service_started
128
- postgres:
129
- condition: service_healthy
130
- redis:
131
- condition: service_healthy
132
- networks:
133
- - mercato-network-fullapp
134
-
135
79
  postgres:
136
80
  image: pgvector/pgvector:pg17-trixie
137
81
  restart: unless-stopped
@@ -5,12 +5,6 @@ services:
5
5
  context: .
6
6
  args:
7
7
  - CONTAINER_PORT=${CONTAINER_PORT:-3000}
8
- - INSTALL_CHROMIUM=${INSTALL_CHROMIUM:-0}
9
- # Optional: browser-reachable ws(s):// endpoint baked in at build time.
10
- # Leave unset to disable realtime document collaboration (the editor
11
- # falls back to single-user editing). Invalid production values are
12
- # ignored at runtime instead of failing the image build.
13
- - NEXT_PUBLIC_DOCUMENTS_COLLAB_URL=${NEXT_PUBLIC_DOCUMENTS_COLLAB_URL:-}
14
8
  - OPEN_MERCATO_DOCKER_REGISTRY_HOST=host.docker.internal
15
9
  extra_hosts:
16
10
  - "host.docker.internal:host-gateway"
@@ -59,13 +53,7 @@ services:
59
53
  JWT_LEGACY_CUTOVER_AT: "${JWT_LEGACY_CUTOVER_AT:-}"
60
54
  NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-}
61
55
  CONSENT_INTEGRITY_SECRET: ${CONSENT_INTEGRITY_SECRET:-}
62
- # Set APP_URL to the public application origin in production deployments.
63
56
  APP_URL: ${APP_URL:-http://localhost:3000}
64
- # Optional: unset disables realtime document collaboration (single-user editing keeps working).
65
- NEXT_PUBLIC_DOCUMENTS_COLLAB_URL: ${NEXT_PUBLIC_DOCUMENTS_COLLAB_URL:-}
66
- # Required only when collaboration is enabled: a unique high-entropy shared secret
67
- # (>= 32 UTF-8 bytes). The sidecar fails closed at runtime when it is missing or too short.
68
- DOCUMENTS_COLLAB_JWT_SECRET_V2: ${DOCUMENTS_COLLAB_JWT_SECRET_V2:-}
69
57
  # Additional trusted app/dev origins for the Open Mercato origin allowlist (resolveAllowedDevOrigins)
70
58
  APP_ALLOWED_ORIGINS: ${APP_ALLOWED_ORIGINS:-}
71
59
  REDIS_URL: ${REDIS_URL:-}
@@ -112,72 +100,6 @@ services:
112
100
  networks:
113
101
  - mercato-network-fullapp
114
102
 
115
- documents-collab:
116
- profiles:
117
- - documents-collab
118
- build:
119
- context: .
120
- args:
121
- - CONTAINER_PORT=${CONTAINER_PORT:-3000}
122
- - DOCUMENTS_COLLAB_PORT=${DOCUMENTS_COLLAB_PORT:-4101}
123
- - INSTALL_CHROMIUM=${INSTALL_CHROMIUM:-0}
124
- - NEXT_PUBLIC_DOCUMENTS_COLLAB_URL=${NEXT_PUBLIC_DOCUMENTS_COLLAB_URL:-}
125
- - OPEN_MERCATO_DOCKER_REGISTRY_HOST=host.docker.internal
126
- extra_hosts:
127
- - "host.docker.internal:host-gateway"
128
- command: ["yarn", "documents:collab"]
129
- working_dir: /app
130
- restart: unless-stopped
131
- ports:
132
- - "${DOCUMENTS_COLLAB_PORT:-4101}:${DOCUMENTS_COLLAB_PORT:-4101}"
133
- # Liveness only: any HTTP reply (including 503 when collaboration is not
134
- # configured) means the process is serving; a connection error means it is
135
- # down. Readiness would false-negative on the optional collab feature.
136
- healthcheck:
137
- test: ["CMD-SHELL", "node -e \"require('http').get('http://127.0.0.1:'+(process.env.DOCUMENTS_COLLAB_PORT||4101)+'/healthz',r=>process.exit(0)).on('error',()=>process.exit(1))\""]
138
- interval: 30s
139
- timeout: 5s
140
- retries: 3
141
- environment:
142
- NODE_ENV: production
143
- DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-open-mercato}
144
- # Same required secret and bounded legacy-token window as the app service:
145
- # the sidecar boots the app container and must never fall back to a
146
- # published signing secret either.
147
- JWT_SECRET: "${JWT_SECRET:?set JWT_SECRET in the .env file next to this compose file - generate one with: openssl rand -hex 32}"
148
- JWT_LEGACY_GRACE_MINUTES: "${JWT_LEGACY_GRACE_MINUTES:-480}"
149
- JWT_LEGACY_CUTOVER_AT: "${JWT_LEGACY_CUTOVER_AT:-}"
150
- APP_URL: ${APP_URL:-http://localhost:3000}
151
- NEXT_PUBLIC_APP_URL: ${APP_URL:-http://localhost:3000}
152
- DOCUMENTS_COLLAB_PORT: ${DOCUMENTS_COLLAB_PORT:-4101}
153
- DOCUMENTS_COLLAB_REDIS_URL: redis://redis:6379
154
- DOCUMENTS_COLLAB_REDIS_PREFIX: ${DOCUMENTS_COLLAB_REDIS_PREFIX:-open-mercato:documents:collab:${DEPLOY_ENV:-local}}
155
- # Same Redis as the app so multi-instance collaboration sync works out of the box
156
- # (DOCUMENTS_COLLAB_REDIS_URL wins when both are set).
157
- REDIS_URL: redis://redis:6379
158
- DOCUMENTS_COLLAB_ALLOWED_ORIGINS: ${DOCUMENTS_COLLAB_ALLOWED_ORIGINS:-${APP_URL}}
159
- # Required only when collaboration is enabled; the sidecar fails closed when missing/too short.
160
- DOCUMENTS_COLLAB_JWT_SECRET_V2: ${DOCUMENTS_COLLAB_JWT_SECRET_V2:-}
161
- DOCUMENTS_COLLAB_JWT_SECRET: ${DOCUMENTS_COLLAB_JWT_SECRET:-}
162
- DOCUMENTS_COLLAB_APP_ROOT: /app
163
- # Mirror the app service's tenant-encryption configuration so the sidecar
164
- # can decrypt the same data as the app.
165
- TENANT_DATA_ENCRYPTION: ${TENANT_DATA_ENCRYPTION:-true}
166
- TENANT_DATA_ENCRYPTION_DEBUG: ${TENANT_DATA_ENCRYPTION_DEBUG:-false}
167
- TENANT_DATA_ENCRYPTION_FALLBACK_KEY: ${TENANT_DATA_ENCRYPTION_FALLBACK_KEY:-dev-tenant-encryption-fallback-key-32chars}
168
- TENANT_DATA_ENCRYPTION_KEY: ${TENANT_DATA_ENCRYPTION_KEY:-}
169
- CACHE_REDIS_URL: redis://redis:6379
170
- CACHE_STRATEGY: ${CACHE_STRATEGY:-redis}
171
- depends_on:
172
- app:
173
- condition: service_started
174
- postgres:
175
- condition: service_healthy
176
- redis:
177
- condition: service_healthy
178
- networks:
179
- - mercato-network-fullapp
180
-
181
103
  postgres:
182
104
  image: pgvector/pgvector:pg17-trixie
183
105
  restart: unless-stopped
@@ -59,9 +59,6 @@ const nextConfig: NextConfig & { agentRules?: boolean } = {
59
59
  'esbuild',
60
60
  '@esbuild/darwin-arm64',
61
61
  '@open-mercato/cli',
62
- 'puppeteer-core',
63
- 'jszip',
64
- 'yjs',
65
62
  // Telemetry: the OTEL SDK + instrumentations must run as real Node modules,
66
63
  // not be bundled — the auto-instrumentations (pg/undici) monkey-patch the
67
64
  // underlying drivers at runtime. The full list is owned by
@@ -16,7 +16,6 @@
16
16
  "dev:reset": "node ./scripts/dev-reset.mjs",
17
17
  "build": "yarn generate && cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 next build",
18
18
  "start": "mercato server start",
19
- "documents:collab": "node ./node_modules/@open-mercato/documents/dist/server/documents-collab-server.js",
20
19
  "lint": "eslint .",
21
20
  "ds:check": "node scripts/ds-check.mjs",
22
21
  "i18n:check-hardcoded": "node scripts/i18n-check-hardcoded.mjs",
@@ -53,7 +52,6 @@
53
52
  "@modelcontextprotocol/sdk": "^1.29.0",
54
53
  "@open-mercato/ai-assistant": "{{PACKAGE_VERSION}}",
55
54
  "@open-mercato/cache": "{{PACKAGE_VERSION}}",
56
- "@open-mercato/channel-discord": "{{PACKAGE_VERSION}}",
57
55
  "@open-mercato/channel-gmail": "{{PACKAGE_VERSION}}",
58
56
  "@open-mercato/channel-imap": "{{PACKAGE_VERSION}}",
59
57
  "@open-mercato/channel-apns": "{{PACKAGE_VERSION}}",
@@ -63,7 +61,6 @@
63
61
  "@open-mercato/cli": "{{PACKAGE_VERSION}}",
64
62
  "@open-mercato/content": "{{PACKAGE_VERSION}}",
65
63
  "@open-mercato/core": "{{PACKAGE_VERSION}}",
66
- "@open-mercato/documents": "{{PACKAGE_VERSION}}",
67
64
  "@open-mercato/events": "{{PACKAGE_VERSION}}",
68
65
  "@open-mercato/onboarding": "{{PACKAGE_VERSION}}",
69
66
  "@open-mercato/queue": "{{PACKAGE_VERSION}}",
@@ -14,6 +14,7 @@ import { Spinner } from '@open-mercato/ui/primitives/spinner'
14
14
  import { useAiDock } from '@open-mercato/ui/ai/AiDock'
15
15
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
16
16
  import { useT } from '@open-mercato/shared/lib/i18n/context'
17
+ import { isContactWidgetHidden } from './demoFeedbackFlag'
17
18
 
18
19
  const SUPPRESS_COOKIE = 'om_feedback_suppress'
19
20
  const SHOWN_TODAY_COOKIE = 'om_feedback_shown'
@@ -48,6 +49,7 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
48
49
  const [open, setOpen] = useState(false)
49
50
  const [captionIndex, setCaptionIndex] = useState(0)
50
51
  const [mounted, setMounted] = useState(false)
52
+ const [hiddenByFlag, setHiddenByFlag] = useState(false)
51
53
 
52
54
  // form state
53
55
  const [email, setEmail] = useState('')
@@ -63,7 +65,13 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
63
65
  const autoShownRef = useRef(false)
64
66
  const [otherModalOpen, setOtherModalOpen] = useState(false)
65
67
 
66
- useEffect(() => { setMounted(true) }, [])
68
+ // The `ff_om_hide_contact` localStorage flag lets a demo tenant hide the
69
+ // contact/feedback surface entirely (floating CTA and the inactivity popup).
70
+ // It is read after mount because localStorage is unavailable during SSR.
71
+ useEffect(() => {
72
+ setMounted(true)
73
+ setHiddenByFlag(isContactWidgetHidden())
74
+ }, [])
67
75
 
68
76
  // Track whether another Radix Dialog or native <dialog> modal is currently open.
69
77
  // The floating button + auto-popup must defer while the user is mid-task in another modal —
@@ -99,7 +107,7 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
99
107
  // operator is mid-conversation with an assistant and a popup would be
100
108
  // disruptive on top of (or competing with) the dock surface.
101
109
  useEffect(() => {
102
- if (!demoModeEnabled || !mounted) return
110
+ if (!demoModeEnabled || !mounted || hiddenByFlag) return
103
111
  if (aiDockActive) return
104
112
  if (getCookie(SUPPRESS_COOKIE) === '1') return
105
113
  if (getCookie(SHOWN_TODAY_COOKIE) === todayKey()) return
@@ -133,7 +141,7 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
133
141
  events.forEach((ev) => window.removeEventListener(ev, resetTimer))
134
142
  if (inactivityTimer.current) clearTimeout(inactivityTimer.current)
135
143
  }
136
- }, [demoModeEnabled, mounted, aiDockActive])
144
+ }, [demoModeEnabled, mounted, aiDockActive, hiddenByFlag])
137
145
 
138
146
  const handleSubmit = useCallback(async () => {
139
147
  setFieldErrors({})
@@ -205,7 +213,7 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
205
213
  }
206
214
  }, [submitState, resetForm])
207
215
 
208
- if (!mounted) return null
216
+ if (!mounted || hiddenByFlag) return null
209
217
 
210
218
  const caption = CAPTIONS[captionIndex]
211
219
  const currentCaption = t(caption.key, caption.fallback)
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import '@testing-library/jest-dom'
6
+ import { render, screen } from '@testing-library/react'
7
+ import { DemoFeedbackWidget } from '../DemoFeedbackWidget'
8
+ import { HIDE_CONTACT_FLAG_KEY } from '../demoFeedbackFlag'
9
+
10
+ jest.mock('next/image', () => ({
11
+ __esModule: true,
12
+ default: ({ alt }: { alt: string }) => <span data-testid="logo">{alt}</span>,
13
+ }))
14
+
15
+ jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
16
+ useT: () => (_key: string, fallback?: string) => fallback ?? '',
17
+ }))
18
+
19
+ jest.mock('@open-mercato/ui/ai/AiDock', () => ({
20
+ useAiDock: () => ({ state: {} }),
21
+ }))
22
+
23
+ jest.mock('@open-mercato/ui/backend/utils/apiCall', () => ({
24
+ apiCall: jest.fn(),
25
+ }))
26
+
27
+ describe('DemoFeedbackWidget', () => {
28
+ afterEach(() => {
29
+ window.localStorage.clear()
30
+ })
31
+
32
+ it('renders the floating contact button by default', () => {
33
+ render(<DemoFeedbackWidget demoModeEnabled={true} />)
34
+
35
+ expect(screen.getByRole('button', { name: 'Open feedback form' })).toBeInTheDocument()
36
+ })
37
+
38
+ it('hides the floating contact button when ff_om_hide_contact is set', () => {
39
+ window.localStorage.setItem(HIDE_CONTACT_FLAG_KEY, '1')
40
+
41
+ render(<DemoFeedbackWidget demoModeEnabled={true} />)
42
+
43
+ expect(screen.queryByRole('button', { name: 'Open feedback form' })).not.toBeInTheDocument()
44
+ })
45
+
46
+ it('keeps the button when the flag is explicitly disabled', () => {
47
+ window.localStorage.setItem(HIDE_CONTACT_FLAG_KEY, 'false')
48
+
49
+ render(<DemoFeedbackWidget demoModeEnabled={true} />)
50
+
51
+ expect(screen.getByRole('button', { name: 'Open feedback form' })).toBeInTheDocument()
52
+ })
53
+ })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { HIDE_CONTACT_FLAG_KEY, isContactWidgetHidden } from '../demoFeedbackFlag'
6
+
7
+ describe('isContactWidgetHidden', () => {
8
+ afterEach(() => {
9
+ window.localStorage.clear()
10
+ jest.restoreAllMocks()
11
+ })
12
+
13
+ it('is false when the flag is absent', () => {
14
+ expect(isContactWidgetHidden()).toBe(false)
15
+ })
16
+
17
+ it('is true when the flag is present with an empty value', () => {
18
+ window.localStorage.setItem(HIDE_CONTACT_FLAG_KEY, '')
19
+ expect(isContactWidgetHidden()).toBe(true)
20
+ })
21
+
22
+ it.each(['1', 'true', 'yes', 'on', 'whatever'])('is true for value %s', (value) => {
23
+ window.localStorage.setItem(HIDE_CONTACT_FLAG_KEY, value)
24
+ expect(isContactWidgetHidden()).toBe(true)
25
+ })
26
+
27
+ it.each(['0', 'false', 'off', 'no'])('is false for explicit opt-out value %s', (value) => {
28
+ window.localStorage.setItem(HIDE_CONTACT_FLAG_KEY, value)
29
+ expect(isContactWidgetHidden()).toBe(false)
30
+ })
31
+
32
+ it('is false when localStorage access throws', () => {
33
+ jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
34
+ throw new Error('denied')
35
+ })
36
+ expect(isContactWidgetHidden()).toBe(false)
37
+ })
38
+ })
@@ -0,0 +1,15 @@
1
+ import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
2
+
3
+ export const HIDE_CONTACT_FLAG_KEY = 'ff_om_hide_contact'
4
+
5
+ export function isContactWidgetHidden(): boolean {
6
+ if (typeof window === 'undefined') return false
7
+ let raw: string | null = null
8
+ try {
9
+ raw = window.localStorage.getItem(HIDE_CONTACT_FLAG_KEY)
10
+ } catch {
11
+ return false
12
+ }
13
+ if (raw === null) return false
14
+ return parseBooleanWithDefault(raw, true)
15
+ }