@open-mercato/core 0.6.7-develop.6785.1.1dd7cfac55 → 0.6.7-develop.6795.1.8a3f27921c

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 (196) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities.ids.generated.js +1 -0
  3. package/dist/generated/entities.ids.generated.js.map +2 -2
  4. package/dist/modules/auth/backend/settings/page.js +14 -9
  5. package/dist/modules/auth/backend/settings/page.js.map +2 -2
  6. package/dist/modules/customers/cli.js +17 -7
  7. package/dist/modules/customers/cli.js.map +2 -2
  8. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +229 -216
  9. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  10. package/dist/modules/customers/search.js +50 -29
  11. package/dist/modules/customers/search.js.map +2 -2
  12. package/dist/modules/data_sync/api/mappings/[id]/route.js +22 -9
  13. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  14. package/dist/modules/data_sync/api/mappings/route.js +13 -4
  15. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  16. package/dist/modules/data_sync/api/options.js +7 -2
  17. package/dist/modules/data_sync/api/options.js.map +2 -2
  18. package/dist/modules/data_sync/api/run.js +7 -2
  19. package/dist/modules/data_sync/api/run.js.map +2 -2
  20. package/dist/modules/data_sync/api/runs/[id]/cancel.js +8 -3
  21. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  22. package/dist/modules/data_sync/api/runs/[id]/retry.js +7 -2
  23. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  24. package/dist/modules/data_sync/api/runs/[id]/route.js +8 -3
  25. package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
  26. package/dist/modules/data_sync/api/runs.js +7 -2
  27. package/dist/modules/data_sync/api/runs.js.map +2 -2
  28. package/dist/modules/data_sync/api/schedules/[id]/route.js +19 -6
  29. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  30. package/dist/modules/data_sync/api/schedules/route.js +13 -4
  31. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  32. package/dist/modules/data_sync/api/validate.js +9 -4
  33. package/dist/modules/data_sync/api/validate.js.map +2 -2
  34. package/dist/modules/design_system/acl.js +9 -0
  35. package/dist/modules/design_system/acl.js.map +7 -0
  36. package/dist/modules/design_system/backend/design-system/page.js +11 -0
  37. package/dist/modules/design_system/backend/design-system/page.js.map +7 -0
  38. package/dist/modules/design_system/backend/design-system/page.meta.js +15 -0
  39. package/dist/modules/design_system/backend/design-system/page.meta.js.map +7 -0
  40. package/dist/modules/design_system/gallery/components/CodeSnippet.js +52 -0
  41. package/dist/modules/design_system/gallery/components/CodeSnippet.js.map +7 -0
  42. package/dist/modules/design_system/gallery/components/EntryCard.js +63 -0
  43. package/dist/modules/design_system/gallery/components/EntryCard.js.map +7 -0
  44. package/dist/modules/design_system/gallery/components/GalleryShell.js +163 -0
  45. package/dist/modules/design_system/gallery/components/GalleryShell.js.map +7 -0
  46. package/dist/modules/design_system/gallery/components/VariantPreview.js +9 -0
  47. package/dist/modules/design_system/gallery/components/VariantPreview.js.map +7 -0
  48. package/dist/modules/design_system/gallery/entries/banners.js +186 -0
  49. package/dist/modules/design_system/gallery/entries/banners.js.map +7 -0
  50. package/dist/modules/design_system/gallery/entries/buttons.js +361 -0
  51. package/dist/modules/design_system/gallery/entries/buttons.js.map +7 -0
  52. package/dist/modules/design_system/gallery/entries/charts.js +457 -0
  53. package/dist/modules/design_system/gallery/entries/charts.js.map +7 -0
  54. package/dist/modules/design_system/gallery/entries/dates.js +306 -0
  55. package/dist/modules/design_system/gallery/entries/dates.js.map +7 -0
  56. package/dist/modules/design_system/gallery/entries/detail.js +358 -0
  57. package/dist/modules/design_system/gallery/entries/detail.js.map +7 -0
  58. package/dist/modules/design_system/gallery/entries/display.js +785 -0
  59. package/dist/modules/design_system/gallery/entries/display.js.map +7 -0
  60. package/dist/modules/design_system/gallery/entries/feedback.js +658 -0
  61. package/dist/modules/design_system/gallery/entries/feedback.js.map +7 -0
  62. package/dist/modules/design_system/gallery/entries/filters.js +505 -0
  63. package/dist/modules/design_system/gallery/entries/filters.js.map +7 -0
  64. package/dist/modules/design_system/gallery/entries/foundations.js +453 -0
  65. package/dist/modules/design_system/gallery/entries/foundations.js.map +7 -0
  66. package/dist/modules/design_system/gallery/entries/icons.js +140 -0
  67. package/dist/modules/design_system/gallery/entries/icons.js.map +7 -0
  68. package/dist/modules/design_system/gallery/entries/inputs.js +1217 -0
  69. package/dist/modules/design_system/gallery/entries/inputs.js.map +7 -0
  70. package/dist/modules/design_system/gallery/entries/messages.js +334 -0
  71. package/dist/modules/design_system/gallery/entries/messages.js.map +7 -0
  72. package/dist/modules/design_system/gallery/entries/navigation.js +475 -0
  73. package/dist/modules/design_system/gallery/entries/navigation.js.map +7 -0
  74. package/dist/modules/design_system/gallery/entries/notifications.js +257 -0
  75. package/dist/modules/design_system/gallery/entries/notifications.js.map +7 -0
  76. package/dist/modules/design_system/gallery/entries/overlays.js +576 -0
  77. package/dist/modules/design_system/gallery/entries/overlays.js.map +7 -0
  78. package/dist/modules/design_system/gallery/entries/scaffolding.js +378 -0
  79. package/dist/modules/design_system/gallery/entries/scaffolding.js.map +7 -0
  80. package/dist/modules/design_system/gallery/entries/schedule.js +297 -0
  81. package/dist/modules/design_system/gallery/entries/schedule.js.map +7 -0
  82. package/dist/modules/design_system/gallery/registry.js +139 -0
  83. package/dist/modules/design_system/gallery/registry.js.map +7 -0
  84. package/dist/modules/design_system/gallery/types.js +1 -0
  85. package/dist/modules/design_system/gallery/types.js.map +7 -0
  86. package/dist/modules/design_system/index.js +9 -0
  87. package/dist/modules/design_system/index.js.map +7 -0
  88. package/dist/modules/design_system/setup.js +14 -0
  89. package/dist/modules/design_system/setup.js.map +7 -0
  90. package/dist/modules/inbox_ops/search.js +15 -4
  91. package/dist/modules/inbox_ops/search.js.map +2 -2
  92. package/dist/modules/integrations/api/[id]/credentials/route.js +11 -5
  93. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  94. package/dist/modules/integrations/api/[id]/health/route.js +6 -3
  95. package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
  96. package/dist/modules/integrations/api/[id]/route.js +6 -3
  97. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  98. package/dist/modules/integrations/api/[id]/state/route.js +6 -3
  99. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  100. package/dist/modules/integrations/api/[id]/version/route.js +6 -3
  101. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  102. package/dist/modules/integrations/api/logs/route.js +6 -3
  103. package/dist/modules/integrations/api/logs/route.js.map +2 -2
  104. package/dist/modules/integrations/api/route.js +6 -3
  105. package/dist/modules/integrations/api/route.js.map +2 -2
  106. package/dist/modules/integrations/api/umes-read.js +2 -2
  107. package/dist/modules/integrations/api/umes-read.js.map +2 -2
  108. package/dist/modules/integrations/lib/organization-scope.js +2 -9
  109. package/dist/modules/integrations/lib/organization-scope.js.map +2 -2
  110. package/dist/modules/messages/search.js +10 -5
  111. package/dist/modules/messages/search.js.map +2 -2
  112. package/dist/modules/query_index/data/entities.js +2 -1
  113. package/dist/modules/query_index/data/entities.js.map +2 -2
  114. package/dist/modules/query_index/lib/engine.js +32 -50
  115. package/dist/modules/query_index/lib/engine.js.map +2 -2
  116. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js +17 -0
  117. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js.map +7 -0
  118. package/generated/entities.ids.generated.ts +1 -0
  119. package/package.json +16 -16
  120. package/src/modules/auth/backend/settings/page.tsx +17 -8
  121. package/src/modules/customers/cli.ts +17 -7
  122. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +24 -13
  123. package/src/modules/customers/i18n/de.json +17 -0
  124. package/src/modules/customers/i18n/en.json +17 -0
  125. package/src/modules/customers/i18n/es.json +17 -0
  126. package/src/modules/customers/i18n/pl.json +17 -0
  127. package/src/modules/customers/search.ts +59 -27
  128. package/src/modules/data_sync/AGENTS.md +1 -0
  129. package/src/modules/data_sync/api/mappings/[id]/route.ts +22 -9
  130. package/src/modules/data_sync/api/mappings/route.ts +13 -4
  131. package/src/modules/data_sync/api/options.ts +7 -2
  132. package/src/modules/data_sync/api/run.ts +7 -2
  133. package/src/modules/data_sync/api/runs/[id]/cancel.ts +8 -3
  134. package/src/modules/data_sync/api/runs/[id]/retry.ts +7 -2
  135. package/src/modules/data_sync/api/runs/[id]/route.ts +8 -3
  136. package/src/modules/data_sync/api/runs.ts +7 -2
  137. package/src/modules/data_sync/api/schedules/[id]/route.ts +19 -6
  138. package/src/modules/data_sync/api/schedules/route.ts +13 -4
  139. package/src/modules/data_sync/api/validate.ts +9 -4
  140. package/src/modules/design_system/AGENTS.md +34 -0
  141. package/src/modules/design_system/acl.ts +5 -0
  142. package/src/modules/design_system/backend/design-system/page.meta.ts +11 -0
  143. package/src/modules/design_system/backend/design-system/page.tsx +14 -0
  144. package/src/modules/design_system/gallery/components/CodeSnippet.tsx +55 -0
  145. package/src/modules/design_system/gallery/components/EntryCard.tsx +95 -0
  146. package/src/modules/design_system/gallery/components/GalleryShell.tsx +206 -0
  147. package/src/modules/design_system/gallery/components/VariantPreview.tsx +15 -0
  148. package/src/modules/design_system/gallery/entries/banners.tsx +226 -0
  149. package/src/modules/design_system/gallery/entries/buttons.tsx +422 -0
  150. package/src/modules/design_system/gallery/entries/charts.tsx +518 -0
  151. package/src/modules/design_system/gallery/entries/dates.tsx +325 -0
  152. package/src/modules/design_system/gallery/entries/detail.tsx +403 -0
  153. package/src/modules/design_system/gallery/entries/display.tsx +893 -0
  154. package/src/modules/design_system/gallery/entries/feedback.tsx +746 -0
  155. package/src/modules/design_system/gallery/entries/filters.tsx +570 -0
  156. package/src/modules/design_system/gallery/entries/foundations.tsx +529 -0
  157. package/src/modules/design_system/gallery/entries/icons.tsx +168 -0
  158. package/src/modules/design_system/gallery/entries/inputs.tsx +1516 -0
  159. package/src/modules/design_system/gallery/entries/messages.tsx +339 -0
  160. package/src/modules/design_system/gallery/entries/navigation.tsx +573 -0
  161. package/src/modules/design_system/gallery/entries/notifications.tsx +285 -0
  162. package/src/modules/design_system/gallery/entries/overlays.tsx +677 -0
  163. package/src/modules/design_system/gallery/entries/scaffolding.tsx +430 -0
  164. package/src/modules/design_system/gallery/entries/schedule.tsx +307 -0
  165. package/src/modules/design_system/gallery/registry.ts +152 -0
  166. package/src/modules/design_system/gallery/types.ts +32 -0
  167. package/src/modules/design_system/i18n/.hardcoded-allowlist.json +71 -0
  168. package/src/modules/design_system/i18n/de.json +46 -0
  169. package/src/modules/design_system/i18n/en.json +46 -0
  170. package/src/modules/design_system/i18n/es.json +46 -0
  171. package/src/modules/design_system/i18n/pl.json +46 -0
  172. package/src/modules/design_system/index.ts +7 -0
  173. package/src/modules/design_system/setup.ts +12 -0
  174. package/src/modules/inbox_ops/i18n/de.json +3 -0
  175. package/src/modules/inbox_ops/i18n/en.json +3 -0
  176. package/src/modules/inbox_ops/i18n/es.json +3 -0
  177. package/src/modules/inbox_ops/i18n/pl.json +3 -0
  178. package/src/modules/inbox_ops/search.ts +20 -4
  179. package/src/modules/integrations/api/[id]/credentials/route.ts +11 -5
  180. package/src/modules/integrations/api/[id]/health/route.ts +6 -3
  181. package/src/modules/integrations/api/[id]/route.ts +6 -3
  182. package/src/modules/integrations/api/[id]/state/route.ts +6 -3
  183. package/src/modules/integrations/api/[id]/version/route.ts +6 -3
  184. package/src/modules/integrations/api/logs/route.ts +6 -3
  185. package/src/modules/integrations/api/route.ts +6 -3
  186. package/src/modules/integrations/api/umes-read.ts +2 -2
  187. package/src/modules/integrations/lib/organization-scope.ts +5 -25
  188. package/src/modules/messages/i18n/de.json +2 -0
  189. package/src/modules/messages/i18n/en.json +2 -0
  190. package/src/modules/messages/i18n/es.json +2 -0
  191. package/src/modules/messages/i18n/pl.json +2 -0
  192. package/src/modules/messages/search.ts +11 -5
  193. package/src/modules/query_index/data/entities.ts +7 -0
  194. package/src/modules/query_index/lib/engine.ts +40 -63
  195. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +12 -0
  196. package/src/modules/query_index/migrations/Migration20260731105052_query_index.ts +35 -0
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
3
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
5
  import { listSyncRunsQuerySchema } from '../data/validators'
5
6
  import type { SyncRunService } from '../lib/sync-run-service'
@@ -15,9 +16,13 @@ export const openApi = {
15
16
 
16
17
  export async function GET(req: Request) {
17
18
  const auth = await getAuthFromRequest(req)
18
- if (!auth?.tenantId || !auth.orgId) {
19
+ if (!auth?.tenantId) {
19
20
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
20
21
  }
22
+ const organizationId = resolveActiveOrganizationId(auth)
23
+ if (!organizationId) {
24
+ return organizationScopeRequiredResponse()
25
+ }
21
26
 
22
27
  const url = new URL(req.url)
23
28
  const parsed = listSyncRunsQuerySchema.safeParse({
@@ -38,7 +43,7 @@ export async function GET(req: Request) {
38
43
  const syncRunService = container.resolve('dataSyncRunService') as SyncRunService
39
44
 
40
45
  const { items, total } = await syncRunService.listRuns(parsed.data, {
41
- organizationId: auth.orgId as string,
46
+ organizationId,
42
47
  tenantId: auth.tenantId,
43
48
  })
44
49
 
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
4
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
6
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
6
7
  import { readOptimisticLockExpected } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
@@ -30,9 +31,13 @@ export const openApi = {
30
31
 
31
32
  export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
32
33
  const auth = await getAuthFromRequest(req)
33
- if (!auth?.tenantId || !auth.orgId) {
34
+ if (!auth?.tenantId) {
34
35
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
35
36
  }
37
+ const organizationId = resolveActiveOrganizationId(auth)
38
+ if (!organizationId) {
39
+ return organizationScopeRequiredResponse()
40
+ }
36
41
 
37
42
  const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
38
43
  ? await (ctx.params as Promise<{ id?: string }>)
@@ -46,7 +51,7 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
46
51
  const container = await createRequestContainer()
47
52
  const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
48
53
  const schedule = await scheduleService.getById(parsedParams.data.id, {
49
- organizationId: auth.orgId as string,
54
+ organizationId,
50
55
  tenantId: auth.tenantId,
51
56
  })
52
57
 
@@ -59,9 +64,13 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
59
64
 
60
65
  export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
61
66
  const auth = await getAuthFromRequest(req)
62
- if (!auth?.tenantId || !auth.orgId) {
67
+ if (!auth?.tenantId) {
63
68
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
64
69
  }
70
+ const organizationId = resolveActiveOrganizationId(auth)
71
+ if (!organizationId) {
72
+ return organizationScopeRequiredResponse()
73
+ }
65
74
 
66
75
  const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
67
76
  ? await (ctx.params as Promise<{ id?: string }>)
@@ -80,7 +89,7 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
80
89
 
81
90
  const container = await createRequestContainer()
82
91
  const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
83
- const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
92
+ const scope = { organizationId, tenantId: auth.tenantId }
84
93
  const current = await scheduleService.getById(parsedParams.data.id, scope)
85
94
 
86
95
  if (!current) {
@@ -141,9 +150,13 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
141
150
 
142
151
  export async function DELETE(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
143
152
  const auth = await getAuthFromRequest(req)
144
- if (!auth?.tenantId || !auth.orgId) {
153
+ if (!auth?.tenantId) {
145
154
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
146
155
  }
156
+ const organizationId = resolveActiveOrganizationId(auth)
157
+ if (!organizationId) {
158
+ return organizationScopeRequiredResponse()
159
+ }
147
160
 
148
161
  const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
149
162
  ? await (ctx.params as Promise<{ id?: string }>)
@@ -156,7 +169,7 @@ export async function DELETE(req: Request, ctx: { params?: Promise<{ id?: string
156
169
 
157
170
  const container = await createRequestContainer()
158
171
  const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
159
- const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
172
+ const scope = { organizationId, tenantId: auth.tenantId }
160
173
 
161
174
  const guardResult = await validateCrudMutationGuard(container, {
162
175
  tenantId: auth.tenantId,
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
3
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
5
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
6
  import { createSyncScheduleSchema, listSyncSchedulesQuerySchema } from '../../data/validators'
@@ -24,9 +25,13 @@ export const openApi = {
24
25
 
25
26
  export async function GET(req: Request) {
26
27
  const auth = await getAuthFromRequest(req)
27
- if (!auth?.tenantId || !auth.orgId) {
28
+ if (!auth?.tenantId) {
28
29
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
29
30
  }
31
+ const organizationId = resolveActiveOrganizationId(auth)
32
+ if (!organizationId) {
33
+ return organizationScopeRequiredResponse()
34
+ }
30
35
 
31
36
  const url = new URL(req.url)
32
37
  const parsed = listSyncSchedulesQuerySchema.safeParse({
@@ -43,7 +48,7 @@ export async function GET(req: Request) {
43
48
 
44
49
  const container = await createRequestContainer()
45
50
  const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
46
- const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
51
+ const scope = { organizationId, tenantId: auth.tenantId }
47
52
  const { items, total } = await scheduleService.listSchedules(parsed.data, scope)
48
53
 
49
54
  return NextResponse.json({
@@ -57,9 +62,13 @@ export async function GET(req: Request) {
57
62
 
58
63
  export async function POST(req: Request) {
59
64
  const auth = await getAuthFromRequest(req)
60
- if (!auth?.tenantId || !auth.orgId) {
65
+ if (!auth?.tenantId) {
61
66
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
62
67
  }
68
+ const organizationId = resolveActiveOrganizationId(auth)
69
+ if (!organizationId) {
70
+ return organizationScopeRequiredResponse()
71
+ }
63
72
 
64
73
  const payload = await readJsonSafe(req)
65
74
  const parsed = createSyncScheduleSchema.safeParse(payload)
@@ -69,7 +78,7 @@ export async function POST(req: Request) {
69
78
 
70
79
  const container = await createRequestContainer()
71
80
  const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
72
- const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
81
+ const scope = { organizationId, tenantId: auth.tenantId }
73
82
 
74
83
  const guardResult = await validateCrudMutationGuard(container, {
75
84
  tenantId: auth.tenantId,
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
3
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
5
  import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
5
6
  import type { CredentialsService } from '../../integrations/lib/credentials-service'
@@ -17,9 +18,13 @@ export const openApi = {
17
18
 
18
19
  export async function POST(req: Request) {
19
20
  const auth = await getAuthFromRequest(req)
20
- if (!auth?.tenantId || !auth.orgId) {
21
+ if (!auth?.tenantId) {
21
22
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
22
23
  }
24
+ const organizationId = resolveActiveOrganizationId(auth)
25
+ if (!organizationId) {
26
+ return organizationScopeRequiredResponse()
27
+ }
23
28
 
24
29
  const parsed = validateConnectionSchema.safeParse(await req.json())
25
30
  if (!parsed.success) {
@@ -39,7 +44,7 @@ export async function POST(req: Request) {
39
44
  const container = await createRequestContainer()
40
45
  const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
41
46
  const credentials = await credentialsService.resolve(integration.id, {
42
- organizationId: auth.orgId as string,
47
+ organizationId,
43
48
  tenantId: auth.tenantId,
44
49
  })
45
50
 
@@ -50,7 +55,7 @@ export async function POST(req: Request) {
50
55
  const mapping = await adapter.getMapping({
51
56
  entityType: parsed.data.entityType,
52
57
  scope: {
53
- organizationId: auth.orgId as string,
58
+ organizationId,
54
59
  tenantId: auth.tenantId,
55
60
  },
56
61
  })
@@ -64,7 +69,7 @@ export async function POST(req: Request) {
64
69
  credentials,
65
70
  mapping,
66
71
  scope: {
67
- organizationId: auth.orgId as string,
72
+ organizationId,
68
73
  tenantId: auth.tenantId,
69
74
  },
70
75
  })
@@ -0,0 +1,34 @@
1
+ # Design System Module — Agent Guidelines
2
+
3
+ Live component gallery at `/backend/design-system`, gated by `design_system.view`. Entries are plain data (`gallery/entries/<family>.tsx`) listed in the family manifest (`gallery/registry.ts`) and lazy-loaded per family. The gallery is a DS consumer: it must showcase the DS without ever violating it.
4
+
5
+ ## Registry Contract
6
+
7
+ - `gallery/types.ts` defines `GalleryVariant` / `GalleryEntry` / `GalleryFamily` — do not extend them casually; the shapes are the spec contract (`.ai/specs/2026-07-05-ds-live-component-gallery.md`).
8
+ - A family = one manifest row in `galleryFamilies` + one `entries/<id>.tsx` exporting `entries: GalleryEntry[]`.
9
+ - Every variant keeps `render` and `code` side by side in one object literal; review them together — colocation is the drift defense.
10
+
11
+ ## Always
12
+
13
+ 1. **MUST keep every variant `code` snippet containing the entry's `importPath`** — enforced by `gallery/__tests__/registry-integrity.test.ts`.
14
+ 2. **MUST update the coverage allowlist honestly** — when adding a primitive entry, delete its row from `PENDING_FAMILIES` in `gallery/__tests__/gallery-coverage.test.ts`; when a new primitive lands without an entry, add it there with a one-line reason. `PENDING_FAMILIES` must only shrink as families land.
15
+ 3. **MUST use semantic tokens only** in gallery chrome (`border-border`, `bg-background`, `bg-muted`, `text-muted-foreground`) and lucide icons only.
16
+ 4. **MUST route all gallery chrome strings through `i18n/` in all four locales** (en, pl, es, de) via `useT()`. Component titles and variant names (`Button`, `destructive-soft`) are proper nouns and stay untranslated.
17
+ 5. **MUST render entries with inline mock props only** — the gallery never calls tenant APIs and never renders tenant data.
18
+ 6. **MUST keep `figmaNodeId` in `<page>:<node>` format** pointing into `DS_FIGMA_FILE`; omit it when unknown — never guess node ids.
19
+
20
+ ## Never
21
+
22
+ - Never add gallery-specific props or code to `packages/ui` — the gallery consumes primitives exactly as any other module does.
23
+ - Never hardcode colors, arbitrary sizes, or full-pill chips in gallery chrome.
24
+ - Never add API routes, entities, DI registrations, or tenant-scoped anything to this module.
25
+ - Never remove a file from the primitives coverage enumeration by loosening the test — only entries or allowlist rows with reasons.
26
+
27
+ ## Validation Commands
28
+
29
+ ```bash
30
+ yarn generate
31
+ yarn workspace @open-mercato/core test --testPathPattern design_system
32
+ yarn workspace @open-mercato/core typecheck
33
+ yarn i18n:check-hardcoded
34
+ ```
@@ -0,0 +1,5 @@
1
+ export const features = [
2
+ { id: 'design_system.view', title: 'View the design system gallery', module: 'design_system' },
3
+ ]
4
+
5
+ export default features
@@ -0,0 +1,11 @@
1
+ export const metadata = {
2
+ requireAuth: true,
3
+ requireFeatures: ['design_system.view'],
4
+ pageTitle: 'Design system',
5
+ pageTitleKey: 'design_system.nav.title',
6
+ pageGroup: 'Developer',
7
+ pageGroupKey: 'design_system.nav.group',
8
+ pageOrder: 900,
9
+ icon: 'shapes',
10
+ breadcrumb: [{ label: 'Design system', labelKey: 'design_system.nav.title' }],
11
+ }
@@ -0,0 +1,14 @@
1
+ "use client"
2
+
3
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
4
+ import { GalleryShell } from '../../gallery/components/GalleryShell'
5
+
6
+ export default function DesignSystemGalleryPage() {
7
+ return (
8
+ <Page>
9
+ <PageBody>
10
+ <GalleryShell />
11
+ </PageBody>
12
+ </Page>
13
+ )
14
+ }
@@ -0,0 +1,55 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Copy } from 'lucide-react'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
7
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
8
+
9
+ export function CodeSnippet({ code }: { code: string }) {
10
+ const t = useT()
11
+ const copyLabel = t('design_system.gallery.copy', 'Copy code')
12
+
13
+ const onCopy = React.useCallback(async () => {
14
+ try {
15
+ if (navigator.clipboard) {
16
+ await navigator.clipboard.writeText(code)
17
+ } else {
18
+ // Non-secure contexts (plain-HTTP self-hosted installs) have no
19
+ // navigator.clipboard — fall back to the legacy selection API.
20
+ const holder = document.createElement('textarea')
21
+ holder.value = code
22
+ holder.setAttribute('readonly', '')
23
+ holder.style.position = 'fixed'
24
+ holder.style.opacity = '0'
25
+ document.body.appendChild(holder)
26
+ holder.select()
27
+ const copied = document.execCommand('copy')
28
+ holder.remove()
29
+ if (!copied) throw new Error('[internal] execCommand copy failed')
30
+ }
31
+ flash(t('design_system.gallery.copied', 'Snippet copied to clipboard'), 'success')
32
+ } catch {
33
+ flash(t('design_system.gallery.copyFailed', 'Could not copy the snippet'), 'error')
34
+ }
35
+ }, [code, t])
36
+
37
+ return (
38
+ <div className="relative rounded-md border border-border bg-muted/50">
39
+ <pre className="overflow-x-auto p-3 pr-12 text-xs leading-relaxed">
40
+ <code>{code}</code>
41
+ </pre>
42
+ <IconButton
43
+ type="button"
44
+ variant="ghost"
45
+ size="sm"
46
+ className="absolute right-1.5 top-1.5"
47
+ aria-label={copyLabel}
48
+ title={copyLabel}
49
+ onClick={onCopy}
50
+ >
51
+ <Copy />
52
+ </IconButton>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Check, ExternalLink, X } from 'lucide-react'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { LinkButton } from '@open-mercato/ui/primitives/link-button'
7
+ import type { GalleryEntry } from '../types'
8
+ import { DS_DOCS_URL, figmaNodeUrl } from '../registry'
9
+ import { VariantPreview } from './VariantPreview'
10
+ import { CodeSnippet } from './CodeSnippet'
11
+
12
+ export function EntryCard({ entry }: { entry: GalleryEntry }) {
13
+ const t = useT()
14
+
15
+ return (
16
+ <section
17
+ id={`gallery-entry-${entry.id}`}
18
+ aria-labelledby={`gallery-entry-${entry.id}-title`}
19
+ className="scroll-mt-4 space-y-4 rounded-lg border border-border bg-card p-4"
20
+ >
21
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
22
+ <div className="min-w-0 space-y-1">
23
+ <h3 id={`gallery-entry-${entry.id}-title`} className="text-base font-semibold leading-tight">
24
+ {entry.title}
25
+ </h3>
26
+ {entry.descriptionKey ? (
27
+ <p className="text-sm text-muted-foreground">{t(entry.descriptionKey, entry.title)}</p>
28
+ ) : null}
29
+ <code className="block truncate text-xs text-muted-foreground">{entry.importPath}</code>
30
+ </div>
31
+ <div className="flex shrink-0 items-center gap-3">
32
+ {entry.figmaNodeId ? (
33
+ <LinkButton asChild size="sm">
34
+ <a href={figmaNodeUrl(entry.figmaNodeId)} target="_blank" rel="noreferrer">
35
+ {t('design_system.gallery.openInFigma', 'Open in Figma')}
36
+ <ExternalLink />
37
+ </a>
38
+ </LinkButton>
39
+ ) : null}
40
+ {entry.docsAnchor ? (
41
+ <LinkButton asChild size="sm" variant="gray">
42
+ <a href={`${DS_DOCS_URL}${entry.docsAnchor}`} target="_blank" rel="noreferrer">
43
+ {t('design_system.gallery.viewDocs', 'View docs')}
44
+ <ExternalLink />
45
+ </a>
46
+ </LinkButton>
47
+ ) : null}
48
+ </div>
49
+ </div>
50
+ {entry.usage && (entry.usage.do?.length || entry.usage.dont?.length) ? (
51
+ <div className="grid gap-4 rounded-md border border-border bg-muted/30 p-4 sm:grid-cols-2">
52
+ {entry.usage.do?.length ? (
53
+ <div className="space-y-1.5">
54
+ <div className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
55
+ {t('design_system.gallery.usageDo', 'Do')}
56
+ </div>
57
+ <ul className="space-y-1.5">
58
+ {entry.usage.do.map((rule) => (
59
+ <li key={rule} className="flex items-start gap-2 text-sm text-foreground">
60
+ <Check aria-hidden className="mt-0.5 size-3.5 shrink-0 text-status-success-icon" />
61
+ <span>{rule}</span>
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ </div>
66
+ ) : null}
67
+ {entry.usage.dont?.length ? (
68
+ <div className="space-y-1.5">
69
+ <div className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
70
+ {t('design_system.gallery.usageDont', "Don't")}
71
+ </div>
72
+ <ul className="space-y-1.5">
73
+ {entry.usage.dont.map((rule) => (
74
+ <li key={rule} className="flex items-start gap-2 text-sm text-foreground">
75
+ <X aria-hidden className="mt-0.5 size-3.5 shrink-0 text-status-error-icon" />
76
+ <span>{rule}</span>
77
+ </li>
78
+ ))}
79
+ </ul>
80
+ </div>
81
+ ) : null}
82
+ </div>
83
+ ) : null}
84
+ <div className="space-y-4">
85
+ {entry.variants.map((variant) => (
86
+ <div key={variant.id} className="space-y-2">
87
+ <div className="text-sm font-medium text-foreground">{variant.title}</div>
88
+ <VariantPreview>{variant.render()}</VariantPreview>
89
+ <CodeSnippet code={variant.code} />
90
+ </div>
91
+ ))}
92
+ </div>
93
+ </section>
94
+ )
95
+ }
@@ -0,0 +1,206 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { SectionPage } from '@open-mercato/ui/backend/section-page'
7
+ import type { SectionNavGroup } from '@open-mercato/ui/backend/section-page'
8
+ import { SearchInput } from '@open-mercato/ui/primitives/search-input'
9
+ import { Skeleton } from '@open-mercato/ui/primitives/skeleton'
10
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
11
+ import { Button } from '@open-mercato/ui/primitives/button'
12
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
13
+ import type { GalleryEntry, GalleryFamily } from '../types'
14
+ import { GALLERY_BASE_PATH, galleryFamilies } from '../registry'
15
+ import { EntryCard } from './EntryCard'
16
+
17
+ /** 'buttons' → 'Buttons' — untranslated fallback when a family labelKey has no message. */
18
+ function familyLabelFallback(id: string): string {
19
+ return id.charAt(0).toUpperCase() + id.slice(1).replace(/[-_]/g, ' ')
20
+ }
21
+
22
+ function matchesQuery(entry: GalleryEntry, query: string): boolean {
23
+ const needle = query.trim().toLowerCase()
24
+ if (!needle) return true
25
+ if (entry.id.toLowerCase().includes(needle) || entry.title.toLowerCase().includes(needle)) return true
26
+ if (entry.keywords?.some((keyword) => keyword.toLowerCase().includes(needle))) return true
27
+ // 'radiobutton' should still find 'radio' — match when the query CONTAINS the id/title too.
28
+ return needle.includes(entry.id.toLowerCase()) || needle.includes(entry.title.toLowerCase())
29
+ }
30
+
31
+ type LoadedEntries = Record<string, GalleryEntry[]>
32
+
33
+ export function GalleryShell() {
34
+ const t = useT()
35
+ const searchParams = useSearchParams()
36
+ const [loaded, setLoaded] = React.useState<LoadedEntries>({})
37
+ const [failed, setFailed] = React.useState<Record<string, boolean>>({})
38
+ const [search, setSearch] = React.useState('')
39
+ const pendingRef = React.useRef(new Set<string>())
40
+
41
+ const familyParam = searchParams?.get('family') ?? null
42
+ const entryParam = searchParams?.get('entry') ?? null
43
+
44
+ // Clicking a family in the section nav exits search mode — otherwise the
45
+ // query keeps precedence and family navigation appears dead.
46
+ React.useEffect(() => {
47
+ setSearch('')
48
+ }, [familyParam])
49
+ const activeFamily: GalleryFamily | undefined =
50
+ galleryFamilies.find((family) => family.id === familyParam) ?? galleryFamilies[0]
51
+ const activeFamilyId = activeFamily?.id ?? null
52
+
53
+ const loadFamily = React.useCallback((family: GalleryFamily) => {
54
+ if (pendingRef.current.has(family.id)) return
55
+ pendingRef.current.add(family.id)
56
+ setFailed((prev) => (prev[family.id] ? { ...prev, [family.id]: false } : prev))
57
+ family
58
+ .load()
59
+ .then((mod) => {
60
+ setLoaded((prev) => (prev[family.id] ? prev : { ...prev, [family.id]: mod.entries }))
61
+ })
62
+ .catch(() => {
63
+ pendingRef.current.delete(family.id)
64
+ setFailed((prev) => ({ ...prev, [family.id]: true }))
65
+ })
66
+ }, [])
67
+
68
+ // Lazy-load the active family; when searching, load every family so results
69
+ // span the whole gallery.
70
+ const searching = search.trim().length > 0
71
+ React.useEffect(() => {
72
+ if (searching) {
73
+ for (const family of galleryFamilies) loadFamily(family)
74
+ } else if (activeFamily) {
75
+ loadFamily(activeFamily)
76
+ }
77
+ }, [searching, activeFamily, loadFamily])
78
+
79
+ // Deep link: ?entry=<id> scrolls the entry into view once its family loaded.
80
+ const activeEntries = activeFamilyId ? loaded[activeFamilyId] : undefined
81
+ React.useEffect(() => {
82
+ if (!entryParam || searching || !activeEntries) return
83
+ const node = document.getElementById(`gallery-entry-${entryParam}`)
84
+ node?.scrollIntoView({ block: 'start' })
85
+ }, [entryParam, searching, activeEntries])
86
+
87
+ const sections: SectionNavGroup[] = React.useMemo(
88
+ () => [
89
+ {
90
+ id: 'families',
91
+ label: 'Families',
92
+ labelKey: 'design_system.gallery.familiesGroup',
93
+ items: galleryFamilies.map((family) => ({
94
+ id: family.id,
95
+ label: familyLabelFallback(family.id),
96
+ labelKey: family.labelKey,
97
+ icon: family.icon,
98
+ href: `${GALLERY_BASE_PATH}?family=${family.id}`,
99
+ })),
100
+ },
101
+ ],
102
+ [],
103
+ )
104
+
105
+ const renderFamilySkeleton = () => (
106
+ <div className="space-y-4" data-testid="gallery-family-skeleton">
107
+ <Skeleton className="h-40 w-full" />
108
+ <Skeleton className="h-40 w-full" />
109
+ </div>
110
+ )
111
+
112
+ const renderSearchResults = () => {
113
+ const groups = galleryFamilies
114
+ .map((family) => ({
115
+ family,
116
+ entries: (loaded[family.id] ?? []).filter((entry) => matchesQuery(entry, search)),
117
+ }))
118
+ .filter((group) => group.entries.length > 0)
119
+ const stillLoading = galleryFamilies.some((family) => !loaded[family.id] && !failed[family.id])
120
+ if (groups.length === 0 && stillLoading) return renderFamilySkeleton()
121
+ if (groups.length === 0) {
122
+ return (
123
+ <EmptyState
124
+ title={t('design_system.gallery.noResults', 'No components match your search')}
125
+ />
126
+ )
127
+ }
128
+ return (
129
+ <div className="space-y-8">
130
+ {groups.map(({ family, entries }) => (
131
+ <div key={family.id} className="space-y-4">
132
+ <h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
133
+ {t(family.labelKey, familyLabelFallback(family.id))}
134
+ </h2>
135
+ {entries.map((entry) => (
136
+ <EntryCard key={entry.id} entry={entry} />
137
+ ))}
138
+ </div>
139
+ ))}
140
+ </div>
141
+ )
142
+ }
143
+
144
+ const renderActiveFamily = () => {
145
+ if (!activeFamily) return null
146
+ const entries = loaded[activeFamily.id]
147
+ if (!entries && failed[activeFamily.id]) {
148
+ return (
149
+ <ErrorMessage
150
+ label={t('design_system.gallery.loadFailed', 'Could not load this family')}
151
+ action={
152
+ <Button type="button" variant="outline" size="sm" onClick={() => loadFamily(activeFamily)}>
153
+ {t('design_system.gallery.retry', 'Retry')}
154
+ </Button>
155
+ }
156
+ />
157
+ )
158
+ }
159
+ if (!entries) return renderFamilySkeleton()
160
+ return (
161
+ <div className="space-y-4">
162
+ <h2 className="text-lg font-semibold">
163
+ {t(activeFamily.labelKey, familyLabelFallback(activeFamily.id))}
164
+ </h2>
165
+ {entries.length > 1 ? (
166
+ <nav
167
+ aria-label={t('design_system.gallery.familyContents', 'Components in this family')}
168
+ className="flex flex-wrap items-center gap-1.5 rounded-md border border-border bg-muted/30 p-2"
169
+ >
170
+ {entries.map((entry) => (
171
+ <a
172
+ key={entry.id}
173
+ href={`#gallery-entry-${entry.id}`}
174
+ className="rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:shadow-focus"
175
+ >
176
+ {entry.title}
177
+ </a>
178
+ ))}
179
+ </nav>
180
+ ) : null}
181
+ {entries.map((entry) => (
182
+ <EntryCard key={entry.id} entry={entry} />
183
+ ))}
184
+ </div>
185
+ )
186
+ }
187
+
188
+ return (
189
+ <SectionPage
190
+ title="Design system"
191
+ titleKey="design_system.nav.title"
192
+ sections={sections}
193
+ activePath={activeFamilyId ? `${GALLERY_BASE_PATH}?family=${activeFamilyId}` : GALLERY_BASE_PATH}
194
+ >
195
+ <div className="mx-auto w-full max-w-5xl space-y-6">
196
+ <SearchInput
197
+ value={search}
198
+ onChange={setSearch}
199
+ placeholder={t('design_system.gallery.searchPlaceholder', 'Search components…')}
200
+ aria-label={t('design_system.gallery.searchPlaceholder', 'Search components…')}
201
+ />
202
+ {searching ? renderSearchResults() : renderActiveFamily()}
203
+ </div>
204
+ </SectionPage>
205
+ )
206
+ }
@@ -0,0 +1,15 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ /**
6
+ * Bordered stage a variant renders inside. Chrome uses semantic tokens only —
7
+ * the stage must never impose colors of its own on the previewed component.
8
+ */
9
+ export function VariantPreview({ children }: { children: React.ReactNode }) {
10
+ return (
11
+ <div className="flex flex-wrap items-center gap-3 rounded-md border border-border bg-background p-4">
12
+ {children}
13
+ </div>
14
+ )
15
+ }