@open-mercato/core 0.4.6-develop-6d72ec5960 → 0.4.6-develop-cd1e2a9a0e

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 (226) hide show
  1. package/AGENTS.md +10 -0
  2. package/dist/generated/entities/integration_credentials/index.js +19 -0
  3. package/dist/generated/entities/integration_credentials/index.js.map +7 -0
  4. package/dist/generated/entities/integration_log/index.js +27 -0
  5. package/dist/generated/entities/integration_log/index.js.map +7 -0
  6. package/dist/generated/entities/integration_state/index.js +27 -0
  7. package/dist/generated/entities/integration_state/index.js.map +7 -0
  8. package/dist/generated/entities/sync_cursor/index.js +19 -0
  9. package/dist/generated/entities/sync_cursor/index.js.map +7 -0
  10. package/dist/generated/entities/sync_external_id_mapping/index.js +27 -0
  11. package/dist/generated/entities/sync_external_id_mapping/index.js.map +7 -0
  12. package/dist/generated/entities/sync_mapping/index.js +19 -0
  13. package/dist/generated/entities/sync_mapping/index.js.map +7 -0
  14. package/dist/generated/entities/sync_run/index.js +45 -0
  15. package/dist/generated/entities/sync_run/index.js.map +7 -0
  16. package/dist/generated/entities/sync_schedule/index.js +35 -0
  17. package/dist/generated/entities/sync_schedule/index.js.map +7 -0
  18. package/dist/generated/entities.ids.generated.js +14 -0
  19. package/dist/generated/entities.ids.generated.js.map +2 -2
  20. package/dist/generated/entity-fields-registry.js +16 -0
  21. package/dist/generated/entity-fields-registry.js.map +2 -2
  22. package/dist/modules/data_sync/acl.js +11 -0
  23. package/dist/modules/data_sync/acl.js.map +7 -0
  24. package/dist/modules/data_sync/api/mappings/[id]/route.js +137 -0
  25. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +7 -0
  26. package/dist/modules/data_sync/api/mappings/route.js +132 -0
  27. package/dist/modules/data_sync/api/mappings/route.js.map +7 -0
  28. package/dist/modules/data_sync/api/run.js +87 -0
  29. package/dist/modules/data_sync/api/run.js.map +7 -0
  30. package/dist/modules/data_sync/api/runs/[id]/cancel.js +49 -0
  31. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +7 -0
  32. package/dist/modules/data_sync/api/runs/[id]/retry.js +93 -0
  33. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +7 -0
  34. package/dist/modules/data_sync/api/runs/[id]/route.js +69 -0
  35. package/dist/modules/data_sync/api/runs/[id]/route.js.map +7 -0
  36. package/dist/modules/data_sync/api/runs.js +66 -0
  37. package/dist/modules/data_sync/api/runs.js.map +7 -0
  38. package/dist/modules/data_sync/api/validate.js +66 -0
  39. package/dist/modules/data_sync/api/validate.js.map +7 -0
  40. package/dist/modules/data_sync/backend/data-sync/page.js +216 -0
  41. package/dist/modules/data_sync/backend/data-sync/page.js.map +7 -0
  42. package/dist/modules/data_sync/backend/data-sync/page.meta.js +25 -0
  43. package/dist/modules/data_sync/backend/data-sync/page.meta.js.map +7 -0
  44. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +178 -0
  45. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +7 -0
  46. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js +14 -0
  47. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js.map +7 -0
  48. package/dist/modules/data_sync/data/entities.js +228 -0
  49. package/dist/modules/data_sync/data/entities.js.map +7 -0
  50. package/dist/modules/data_sync/data/validators.js +32 -0
  51. package/dist/modules/data_sync/data/validators.js.map +7 -0
  52. package/dist/modules/data_sync/di.js +26 -0
  53. package/dist/modules/data_sync/di.js.map +7 -0
  54. package/dist/modules/data_sync/events.js +16 -0
  55. package/dist/modules/data_sync/events.js.map +7 -0
  56. package/dist/modules/data_sync/index.js +9 -0
  57. package/dist/modules/data_sync/index.js.map +7 -0
  58. package/dist/modules/data_sync/lib/adapter-registry.js +16 -0
  59. package/dist/modules/data_sync/lib/adapter-registry.js.map +7 -0
  60. package/dist/modules/data_sync/lib/adapter.js +1 -0
  61. package/dist/modules/data_sync/lib/adapter.js.map +7 -0
  62. package/dist/modules/data_sync/lib/id-mapping.js +79 -0
  63. package/dist/modules/data_sync/lib/id-mapping.js.map +7 -0
  64. package/dist/modules/data_sync/lib/queue.js +17 -0
  65. package/dist/modules/data_sync/lib/queue.js.map +7 -0
  66. package/dist/modules/data_sync/lib/sync-engine.js +309 -0
  67. package/dist/modules/data_sync/lib/sync-engine.js.map +7 -0
  68. package/dist/modules/data_sync/lib/sync-run-service.js +148 -0
  69. package/dist/modules/data_sync/lib/sync-run-service.js.map +7 -0
  70. package/dist/modules/data_sync/migrations/Migration20260304113737.js +17 -0
  71. package/dist/modules/data_sync/migrations/Migration20260304113737.js.map +7 -0
  72. package/dist/modules/data_sync/setup.js +13 -0
  73. package/dist/modules/data_sync/setup.js.map +7 -0
  74. package/dist/modules/data_sync/workers/sync-export.js +14 -0
  75. package/dist/modules/data_sync/workers/sync-export.js.map +7 -0
  76. package/dist/modules/data_sync/workers/sync-import.js +14 -0
  77. package/dist/modules/data_sync/workers/sync-import.js.map +7 -0
  78. package/dist/modules/data_sync/workers/sync-scheduled.js +63 -0
  79. package/dist/modules/data_sync/workers/sync-scheduled.js.map +7 -0
  80. package/dist/modules/entities/lib/encryptionDefaults.js +4 -0
  81. package/dist/modules/entities/lib/encryptionDefaults.js.map +2 -2
  82. package/dist/modules/integrations/acl.js +4 -1
  83. package/dist/modules/integrations/acl.js.map +2 -2
  84. package/dist/modules/integrations/api/[id]/credentials/route.js +127 -0
  85. package/dist/modules/integrations/api/[id]/credentials/route.js.map +7 -0
  86. package/dist/modules/integrations/api/[id]/health/route.js +46 -0
  87. package/dist/modules/integrations/api/[id]/health/route.js.map +7 -0
  88. package/dist/modules/integrations/api/[id]/route.js +65 -0
  89. package/dist/modules/integrations/api/[id]/route.js.map +7 -0
  90. package/dist/modules/integrations/api/[id]/state/route.js +109 -0
  91. package/dist/modules/integrations/api/[id]/state/route.js.map +7 -0
  92. package/dist/modules/integrations/api/[id]/version/route.js +117 -0
  93. package/dist/modules/integrations/api/[id]/version/route.js.map +7 -0
  94. package/dist/modules/integrations/api/guards.js +31 -0
  95. package/dist/modules/integrations/api/guards.js.map +7 -0
  96. package/dist/modules/integrations/api/logs/route.js +60 -0
  97. package/dist/modules/integrations/api/logs/route.js.map +7 -0
  98. package/dist/modules/integrations/api/openapi.js +25 -0
  99. package/dist/modules/integrations/api/openapi.js.map +7 -0
  100. package/dist/modules/integrations/api/route.js +68 -0
  101. package/dist/modules/integrations/api/route.js.map +7 -0
  102. package/dist/modules/integrations/backend/integrations/[id]/page.js +313 -0
  103. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +7 -0
  104. package/dist/modules/integrations/backend/integrations/[id]/page.meta.js +15 -0
  105. package/dist/modules/integrations/backend/integrations/[id]/page.meta.js.map +7 -0
  106. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +189 -0
  107. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +7 -0
  108. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.meta.js +15 -0
  109. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.meta.js.map +7 -0
  110. package/dist/modules/integrations/backend/integrations/page.js +212 -0
  111. package/dist/modules/integrations/backend/integrations/page.js.map +7 -0
  112. package/dist/modules/integrations/backend/integrations/page.meta.js +22 -0
  113. package/dist/modules/integrations/backend/integrations/page.meta.js.map +7 -0
  114. package/dist/modules/integrations/data/enrichers.js +27 -12
  115. package/dist/modules/integrations/data/enrichers.js.map +2 -2
  116. package/dist/modules/integrations/data/entities.js +136 -1
  117. package/dist/modules/integrations/data/entities.js.map +2 -2
  118. package/dist/modules/integrations/data/validators.js +36 -0
  119. package/dist/modules/integrations/data/validators.js.map +7 -0
  120. package/dist/modules/integrations/di.js +24 -0
  121. package/dist/modules/integrations/di.js.map +7 -0
  122. package/dist/modules/integrations/events.js +19 -0
  123. package/dist/modules/integrations/events.js.map +7 -0
  124. package/dist/modules/integrations/lib/credentials-service.js +159 -0
  125. package/dist/modules/integrations/lib/credentials-service.js.map +7 -0
  126. package/dist/modules/integrations/lib/health-service.js +37 -0
  127. package/dist/modules/integrations/lib/health-service.js.map +7 -0
  128. package/dist/modules/integrations/lib/log-service.js +66 -0
  129. package/dist/modules/integrations/lib/log-service.js.map +7 -0
  130. package/dist/modules/integrations/lib/registry-service.js +33 -0
  131. package/dist/modules/integrations/lib/registry-service.js.map +7 -0
  132. package/dist/modules/integrations/lib/state-service.js +55 -0
  133. package/dist/modules/integrations/lib/state-service.js.map +7 -0
  134. package/dist/modules/integrations/lib/types.js +1 -0
  135. package/dist/modules/integrations/lib/types.js.map +7 -0
  136. package/dist/modules/integrations/migrations/Migration20260304113737.js +19 -0
  137. package/dist/modules/integrations/migrations/Migration20260304113737.js.map +7 -0
  138. package/dist/modules/integrations/setup.js +2 -2
  139. package/dist/modules/integrations/setup.js.map +2 -2
  140. package/dist/modules/integrations/widgets/injection-table.js.map +1 -1
  141. package/dist/modules/integrations/workers/log-pruner.js +18 -0
  142. package/dist/modules/integrations/workers/log-pruner.js.map +7 -0
  143. package/generated/entities/integration_credentials/index.ts +8 -0
  144. package/generated/entities/integration_log/index.ts +12 -0
  145. package/generated/entities/integration_state/index.ts +12 -0
  146. package/generated/entities/sync_cursor/index.ts +8 -0
  147. package/generated/entities/sync_external_id_mapping/index.ts +12 -0
  148. package/generated/entities/sync_mapping/index.ts +8 -0
  149. package/generated/entities/sync_run/index.ts +21 -0
  150. package/generated/entities/sync_schedule/index.ts +16 -0
  151. package/generated/entities.ids.generated.ts +14 -0
  152. package/generated/entity-fields-registry.ts +16 -0
  153. package/package.json +2 -2
  154. package/src/modules/data_sync/AGENTS.md +157 -0
  155. package/src/modules/data_sync/acl.ts +7 -0
  156. package/src/modules/data_sync/api/mappings/[id]/route.ts +158 -0
  157. package/src/modules/data_sync/api/mappings/route.ts +144 -0
  158. package/src/modules/data_sync/api/run.ts +97 -0
  159. package/src/modules/data_sync/api/runs/[id]/cancel.ts +57 -0
  160. package/src/modules/data_sync/api/runs/[id]/retry.ts +108 -0
  161. package/src/modules/data_sync/api/runs/[id]/route.ts +81 -0
  162. package/src/modules/data_sync/api/runs.ts +69 -0
  163. package/src/modules/data_sync/api/validate.ts +73 -0
  164. package/src/modules/data_sync/backend/data-sync/page.meta.ts +21 -0
  165. package/src/modules/data_sync/backend/data-sync/page.tsx +244 -0
  166. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.meta.ts +10 -0
  167. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +278 -0
  168. package/src/modules/data_sync/data/entities.ts +180 -0
  169. package/src/modules/data_sync/data/validators.ts +35 -0
  170. package/src/modules/data_sync/di.ts +38 -0
  171. package/src/modules/data_sync/events.ts +12 -0
  172. package/src/modules/data_sync/i18n/de.json +48 -0
  173. package/src/modules/data_sync/i18n/en.json +48 -0
  174. package/src/modules/data_sync/i18n/es.json +48 -0
  175. package/src/modules/data_sync/i18n/pl.json +48 -0
  176. package/src/modules/data_sync/index.ts +5 -0
  177. package/src/modules/data_sync/lib/adapter-registry.ts +15 -0
  178. package/src/modules/data_sync/lib/adapter.ts +90 -0
  179. package/src/modules/data_sync/lib/id-mapping.ts +95 -0
  180. package/src/modules/data_sync/lib/queue.ts +19 -0
  181. package/src/modules/data_sync/lib/sync-engine.ts +375 -0
  182. package/src/modules/data_sync/lib/sync-run-service.ts +187 -0
  183. package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +653 -0
  184. package/src/modules/data_sync/migrations/Migration20260304113737.ts +19 -0
  185. package/src/modules/data_sync/setup.ts +11 -0
  186. package/src/modules/data_sync/workers/sync-export.ts +27 -0
  187. package/src/modules/data_sync/workers/sync-import.ts +27 -0
  188. package/src/modules/data_sync/workers/sync-scheduled.ts +84 -0
  189. package/src/modules/entities/lib/encryptionDefaults.ts +4 -0
  190. package/src/modules/integrations/AGENTS.md +160 -0
  191. package/src/modules/integrations/acl.ts +3 -0
  192. package/src/modules/integrations/api/[id]/credentials/route.ts +142 -0
  193. package/src/modules/integrations/api/[id]/health/route.ts +53 -0
  194. package/src/modules/integrations/api/[id]/route.ts +76 -0
  195. package/src/modules/integrations/api/[id]/state/route.ts +121 -0
  196. package/src/modules/integrations/api/[id]/version/route.ts +132 -0
  197. package/src/modules/integrations/api/guards.ts +59 -0
  198. package/src/modules/integrations/api/logs/route.ts +63 -0
  199. package/src/modules/integrations/api/openapi.ts +22 -0
  200. package/src/modules/integrations/api/route.ts +73 -0
  201. package/src/modules/integrations/backend/integrations/[id]/page.meta.ts +11 -0
  202. package/src/modules/integrations/backend/integrations/[id]/page.tsx +424 -0
  203. package/src/modules/integrations/backend/integrations/bundle/[id]/page.meta.ts +11 -0
  204. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +249 -0
  205. package/src/modules/integrations/backend/integrations/page.meta.ts +18 -0
  206. package/src/modules/integrations/backend/integrations/page.tsx +296 -0
  207. package/src/modules/integrations/data/enrichers.ts +35 -18
  208. package/src/modules/integrations/data/entities.ts +114 -5
  209. package/src/modules/integrations/data/validators.ts +41 -0
  210. package/src/modules/integrations/di.ts +31 -0
  211. package/src/modules/integrations/events.ts +17 -0
  212. package/src/modules/integrations/i18n/de.json +70 -0
  213. package/src/modules/integrations/i18n/en.json +70 -0
  214. package/src/modules/integrations/i18n/es.json +70 -0
  215. package/src/modules/integrations/i18n/pl.json +70 -0
  216. package/src/modules/integrations/lib/credentials-service.ts +204 -0
  217. package/src/modules/integrations/lib/health-service.ts +59 -0
  218. package/src/modules/integrations/lib/log-service.ts +84 -0
  219. package/src/modules/integrations/lib/registry-service.ts +42 -0
  220. package/src/modules/integrations/lib/state-service.ts +64 -0
  221. package/src/modules/integrations/lib/types.ts +4 -0
  222. package/src/modules/integrations/migrations/.snapshot-open-mercato.json +582 -0
  223. package/src/modules/integrations/migrations/Migration20260304113737.ts +21 -0
  224. package/src/modules/integrations/setup.ts +2 -2
  225. package/src/modules/integrations/widgets/injection-table.ts +1 -1
  226. package/src/modules/integrations/workers/log-pruner.ts +30 -0
@@ -0,0 +1,27 @@
1
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import type { SyncEngine } from '../lib/sync-engine'
3
+
4
+ type SyncJobPayload = {
5
+ runId: string
6
+ batchSize: number
7
+ scope: {
8
+ organizationId: string
9
+ tenantId: string
10
+ userId?: string | null
11
+ }
12
+ }
13
+
14
+ export const metadata: WorkerMeta = {
15
+ queue: 'data-sync-export',
16
+ id: 'data-sync:export',
17
+ concurrency: 5,
18
+ }
19
+
20
+ type HandlerContext = JobContext & {
21
+ resolve: <T = unknown>(name: string) => T
22
+ }
23
+
24
+ export default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {
25
+ const engine = ctx.resolve<SyncEngine>('dataSyncEngine')
26
+ await engine.runExport(job.payload.runId, job.payload.batchSize, job.payload.scope)
27
+ }
@@ -0,0 +1,27 @@
1
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import type { SyncEngine } from '../lib/sync-engine'
3
+
4
+ type SyncJobPayload = {
5
+ runId: string
6
+ batchSize: number
7
+ scope: {
8
+ organizationId: string
9
+ tenantId: string
10
+ userId?: string | null
11
+ }
12
+ }
13
+
14
+ export const metadata: WorkerMeta = {
15
+ queue: 'data-sync-import',
16
+ id: 'data-sync:import',
17
+ concurrency: 5,
18
+ }
19
+
20
+ type HandlerContext = JobContext & {
21
+ resolve: <T = unknown>(name: string) => T
22
+ }
23
+
24
+ export default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {
25
+ const engine = ctx.resolve<SyncEngine>('dataSyncEngine')
26
+ await engine.runImport(job.payload.runId, job.payload.batchSize, job.payload.scope)
27
+ }
@@ -0,0 +1,84 @@
1
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import type { SyncRunService } from '../lib/sync-run-service'
5
+ import { SyncSchedule } from '../data/entities'
6
+ import { getSyncQueue } from '../lib/queue'
7
+
8
+ type ScheduledSyncPayload = {
9
+ scheduleId: string
10
+ scope: {
11
+ organizationId: string
12
+ tenantId: string
13
+ }
14
+ }
15
+
16
+ export const metadata: WorkerMeta = {
17
+ queue: 'data-sync-scheduled',
18
+ id: 'data-sync:scheduled',
19
+ concurrency: 3,
20
+ }
21
+
22
+ type HandlerContext = JobContext & {
23
+ resolve: <T = unknown>(name: string) => T
24
+ }
25
+
26
+ export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx: HandlerContext): Promise<void> {
27
+ const em = ctx.resolve<EntityManager>('em')
28
+ const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')
29
+
30
+ const schedule = await findOneWithDecryption(
31
+ em,
32
+ SyncSchedule,
33
+ {
34
+ id: job.payload.scheduleId,
35
+ organizationId: job.payload.scope.organizationId,
36
+ tenantId: job.payload.scope.tenantId,
37
+ deletedAt: null,
38
+ },
39
+ undefined,
40
+ job.payload.scope,
41
+ )
42
+
43
+ if (!schedule || !schedule.isEnabled) {
44
+ return
45
+ }
46
+
47
+ const overlap = await syncRunService.findRunningOverlap(
48
+ schedule.integrationId,
49
+ schedule.entityType,
50
+ schedule.direction,
51
+ job.payload.scope,
52
+ )
53
+ if (overlap) {
54
+ return
55
+ }
56
+
57
+ const cursor = schedule.fullSync
58
+ ? null
59
+ : await syncRunService.resolveCursor(
60
+ schedule.integrationId,
61
+ schedule.entityType,
62
+ schedule.direction,
63
+ job.payload.scope,
64
+ )
65
+
66
+ const run = await syncRunService.createRun({
67
+ integrationId: schedule.integrationId,
68
+ entityType: schedule.entityType,
69
+ direction: schedule.direction,
70
+ cursor,
71
+ triggeredBy: 'scheduler',
72
+ }, job.payload.scope)
73
+
74
+ schedule.lastRunAt = new Date()
75
+ await em.flush()
76
+
77
+ const queueName = schedule.direction === 'import' ? 'data-sync-import' : 'data-sync-export'
78
+ const queue = getSyncQueue(queueName)
79
+ await queue.enqueue({
80
+ runId: run.id,
81
+ batchSize: 100,
82
+ scope: job.payload.scope,
83
+ })
84
+ }
@@ -144,6 +144,10 @@ export const DEFAULT_ENCRYPTION_MAPS: Array<{ entityId: string; fields: Array<{
144
144
  { field: 'context_json' },
145
145
  ],
146
146
  },
147
+ {
148
+ entityId: 'integrations:integration_credentials',
149
+ fields: [{ field: 'credentials' }],
150
+ },
147
151
  {
148
152
  entityId: 'staff:staff_leave_request',
149
153
  fields: [
@@ -0,0 +1,160 @@
1
+ # Integrations Module — Agent Guide
2
+
3
+ The `integrations` module is the foundation layer for all external connectors (payment gateways, shipping carriers, communication channels, data sync providers, etc.). It provides three shared mechanisms: **Integration Registry**, **Credentials API**, and **Operation Logs**.
4
+
5
+ **Spec**: `.ai/specs/SPEC-045-2026-02-24-integration-marketplace.md` + `.ai/specs/SPEC-045a-foundation.md`
6
+
7
+ ---
8
+
9
+ ## Module Structure
10
+
11
+ ```
12
+ packages/core/src/modules/integrations/
13
+ ├── index.ts # Module metadata
14
+ ├── di.ts # DI registrations (4 services)
15
+ ├── acl.ts # Features: view, manage, credentials.manage
16
+ ├── setup.ts # Default role features
17
+ ├── events.ts # 4 typed events
18
+ ├── data/
19
+ │ ├── entities.ts # IntegrationCredentials, IntegrationState, IntegrationLog, SyncExternalIdMapping
20
+ │ ├── validators.ts # Zod schemas for all API inputs
21
+ │ └── enrichers.ts # External ID response enricher
22
+ ├── lib/
23
+ │ ├── registry-service.ts # Read-only access to in-memory integration registry
24
+ │ ├── credentials-service.ts # Encrypted CRUD + bundle credential fallthrough
25
+ │ ├── state-service.ts # Enable/disable + API version + reauth + health state
26
+ │ ├── log-service.ts # Structured logging with scoped loggers + pruning
27
+ │ └── health-service.ts # Resolves and runs provider health checks via DI
28
+ ├── api/
29
+ │ ├── route.ts # GET /api/integrations — list all
30
+ │ ├── logs/route.ts # GET /api/integrations/logs — query logs
31
+ │ └── [id]/
32
+ │ ├── route.ts # GET /api/integrations/:id — detail
33
+ │ ├── state/route.ts # PUT — enable/disable
34
+ │ ├── credentials/route.ts # GET/PUT — read/save credentials
35
+ │ ├── version/route.ts # PUT — change API version
36
+ │ └── health/route.ts # POST — trigger health check
37
+ ├── workers/
38
+ │ └── log-pruner.ts # Scheduled log retention cleanup
39
+ ├── backend/
40
+ │ └── integrations/
41
+ │ ├── page.tsx # Marketplace listing page
42
+ │ ├── page.meta.ts
43
+ │ ├── [id]/
44
+ │ │ ├── page.tsx # Integration detail (tabs: credentials/version/health/logs)
45
+ │ │ └── page.meta.ts
46
+ │ └── bundle/[id]/
47
+ │ ├── page.tsx # Bundle config (shared credentials + per-integration toggles)
48
+ │ └── page.meta.ts
49
+ ├── widgets/
50
+ │ ├── injection-table.ts
51
+ │ └── injection/external-ids/
52
+ │ └── widget.client.tsx # External ID display for any entity detail page
53
+ └── i18n/
54
+ ├── en.json
55
+ └── pl.json
56
+ ```
57
+
58
+ ## Key Services (DI)
59
+
60
+ | Service Name | Factory | Purpose |
61
+ |---|---|---|
62
+ | `integrationCredentialsService` | `createCredentialsService(em)` | Encrypted credential CRUD with bundle fallthrough |
63
+ | `integrationStateService` | `createIntegrationStateService(em)` | Upsert integration state (enabled, version, health, reauth) |
64
+ | `integrationLogService` | `createIntegrationLogService(em)` | Structured logging: write, query, prune, scoped logger |
65
+ | `integrationHealthService` | `createHealthService(container, stateService, logService)` | Resolves named health check service from DI, runs check, updates state |
66
+
67
+ ## Adding a New Integration Provider
68
+
69
+ 1. Create a new module (e.g., `packages/core/src/modules/gateway_stripe/`)
70
+ 2. Add `integration.ts` at the module root exporting `IntegrationDefinition`
71
+ 3. Declare `credentials.fields` for the admin UI to render a dynamic form
72
+ 4. Optionally declare `healthCheck.service` (register the service in your `di.ts`)
73
+ 5. Optionally declare `apiVersions` for versioned external APIs
74
+ 6. Run `yarn generate` to auto-discover the integration
75
+
76
+ ### Bundle Integrations
77
+
78
+ For platform connectors with multiple integrations (e.g., MedusaJS):
79
+ - Export `bundle: IntegrationBundle` and `integrations: IntegrationDefinition[]`
80
+ - Set `bundleId` on each child integration
81
+ - Bundle credentials are shared via fallthrough: child reads own credentials first, then bundle's
82
+
83
+ ## Credential Resolution Order
84
+
85
+ 1. Direct credentials for the integration ID
86
+ 2. If `bundleId` is set, fallback to bundle's credentials
87
+ 3. Return `null` if neither exists
88
+
89
+ ## Events
90
+
91
+ | Event ID | Emitted When |
92
+ |---|---|
93
+ | `integrations.credentials.updated` | Credentials saved |
94
+ | `integrations.state.updated` | Integration enabled/disabled or reauth flag changed |
95
+ | `integrations.version.changed` | API version changed |
96
+ | `integrations.log.created` | Log entry written (excluded from triggers) |
97
+
98
+ ## ACL Features
99
+
100
+ - `integrations.view` — view marketplace, detail, logs
101
+ - `integrations.manage` — enable/disable, change version, run health checks
102
+ - `integrations.credentials.manage` — read/save credentials
103
+
104
+ ## UMES Extensibility
105
+
106
+ Integration provider modules can leverage the full **Unified Module Extension System (UMES)** — see `.ai/specs/SPEC-041-2026-02-24-universal-module-extension-system.md` for details.
107
+
108
+ ### Available Extension Points for Providers
109
+
110
+ | Extension Mechanism | Use Case | Files |
111
+ |---|---|---|
112
+ | **Widget Injection** | Inject UI tabs, cards, or status badges into other modules' pages | `widgets/injection/`, `widgets/injection-table.ts` |
113
+ | **Event Subscribers** | React to integration events (`integrations.state.updated`, etc.) for side-effects | `subscribers/*.ts` |
114
+ | **Entity Extensions** | Link provider data to core entities (e.g., external IDs on orders) | `data/extensions.ts` |
115
+ | **Response Enrichers** | Attach provider-specific data to other modules' API responses | `data/enrichers.ts` |
116
+ | **API Interceptors** | Intercept other modules' API routes (before/after hooks) | `api/interceptors.ts` |
117
+ | **Component Replacement** | Override or wrap UI components from other modules | `widgets/components.ts` |
118
+ | **Menu Injection** | Add sidebar/settings menu items | via `useInjectedMenuItems` |
119
+ | **Notifications** | Emit in-app notifications on integration events | `notifications.ts`, `subscribers/` |
120
+ | **DOM Event Bridge** | Push real-time events to browser (SSE) | Set `clientBroadcast: true` in event definitions |
121
+
122
+ ### Key UMES Imports for Providers
123
+
124
+ ```typescript
125
+ import { InjectionPosition } from '@open-mercato/shared/modules/widgets/injection-position'
126
+ import { useInjectionDataWidgets } from '@open-mercato/ui/backend/injection/useInjectionDataWidgets'
127
+ import { useInjectedMenuItems } from '@open-mercato/ui/backend/injection/useInjectedMenuItems'
128
+ import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
129
+ import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
130
+ import type { ResponseEnricher } from '@open-mercato/shared/lib/crud/response-enricher'
131
+ import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
132
+ ```
133
+
134
+ ### Example: External ID Widget
135
+
136
+ The integrations module itself uses UMES to inject external ID displays on any entity detail page via `widgets/injection/external-ids/widget.client.tsx`, mapped through `widgets/injection-table.ts`. Follow this pattern for provider-specific widgets.
137
+
138
+ ## Progress Delivery Contract
139
+
140
+ - `ProgressTopBar` polls `/api/progress/active` every 5s (`useProgressPoll`).
141
+ - SSE DOM bridge forwards only events with `clientBroadcast: true`.
142
+ - `progress.job.*` events are not yet marked `clientBroadcast: true` — polling is the active mechanism.
143
+
144
+ ## Integration Test Expectations
145
+
146
+ - Module-local integration tests go under `__integration__/`
147
+ - Use helpers from `@open-mercato/core/modules/core/__integration__/helpers/*`
148
+ - Tests must create prerequisites via API and clean up in `finally`
149
+
150
+ ## MUST Rules
151
+
152
+ - **Never import from provider modules** — integrations module is generic; providers import from integrations, not vice versa
153
+ - **Always scope by organizationId + tenantId** — every entity query and service call
154
+ - **Use `findWithDecryption`/`findOneWithDecryption`** for credential reads
155
+ - **Never log credential values** — log service strips secret fields from payload
156
+ - **Health check services** must be registered in DI by the provider module, not by integrations
157
+ - **API routes must export `openApi`** for documentation generation
158
+ - **All user-facing strings** via i18n keys in `i18n/en.json`
159
+ - **Keep ACL default export shape** consistent: `export const features = [...]; export default features`
160
+ - **Registry/type contracts** live in `@open-mercato/shared/modules/integrations/types`
@@ -1,4 +1,7 @@
1
1
  export const features = [
2
2
  { id: 'integrations.view', title: 'View integrations and external ID mappings', module: 'integrations' },
3
3
  { id: 'integrations.manage', title: 'Manage integration configurations', module: 'integrations' },
4
+ { id: 'integrations.credentials.manage', title: 'Manage integration credentials', module: 'integrations' },
4
5
  ]
6
+
7
+ export default features
@@ -0,0 +1,142 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
6
+ import { emitIntegrationsEvent } from '../../../events'
7
+ import { saveCredentialsSchema } from '../../../data/validators'
8
+ import type { CredentialsService } from '../../../lib/credentials-service'
9
+ import {
10
+ resolveUserFeatures,
11
+ runIntegrationMutationGuardAfterSuccess,
12
+ runIntegrationMutationGuards,
13
+ } from '../../guards'
14
+
15
+ const idParamsSchema = z.object({ id: z.string().min(1) })
16
+
17
+ export const metadata = {
18
+ GET: { requireAuth: true, requireFeatures: ['integrations.credentials.manage'] },
19
+ PUT: { requireAuth: true, requireFeatures: ['integrations.credentials.manage'] },
20
+ }
21
+
22
+ export const openApi = {
23
+ tags: ['Integrations'],
24
+ summary: 'Get or save integration credentials',
25
+ }
26
+
27
+ function resolveParams(ctx: { params?: Promise<{ id?: string }> | { id?: string } }): Promise<{ id?: string } | undefined> | { id?: string } | undefined {
28
+ if (!ctx.params) return undefined
29
+ if (typeof (ctx.params as Promise<unknown>).then === 'function') {
30
+ return ctx.params as Promise<{ id?: string }>
31
+ }
32
+ return ctx.params as { id?: string }
33
+ }
34
+
35
+ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
36
+ const auth = await getAuthFromRequest(req)
37
+ if (!auth?.tenantId || !auth.orgId) {
38
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
39
+ }
40
+
41
+ const rawParams = await resolveParams(ctx)
42
+ const parsedParams = idParamsSchema.safeParse(rawParams)
43
+ if (!parsedParams.success) {
44
+ return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })
45
+ }
46
+
47
+ const integration = getIntegration(parsedParams.data.id)
48
+ if (!integration) {
49
+ return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
50
+ }
51
+
52
+ const container = await createRequestContainer()
53
+ const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
54
+ const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
55
+
56
+ const values = await credentialsService.resolve(integration.id, scope)
57
+
58
+ return NextResponse.json({
59
+ integrationId: integration.id,
60
+ schema: credentialsService.getSchema(integration.id),
61
+ credentials: values ?? {},
62
+ })
63
+ }
64
+
65
+ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
66
+ const auth = await getAuthFromRequest(req)
67
+ if (!auth?.tenantId || !auth.orgId) {
68
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
69
+ }
70
+
71
+ const rawParams = await resolveParams(ctx)
72
+ const parsedParams = idParamsSchema.safeParse(rawParams)
73
+ if (!parsedParams.success) {
74
+ return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })
75
+ }
76
+
77
+ const integration = getIntegration(parsedParams.data.id)
78
+ if (!integration) {
79
+ return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
80
+ }
81
+
82
+ const payload = await req.json().catch(() => null)
83
+ const parsedBody = saveCredentialsSchema.safeParse(payload)
84
+ if (!parsedBody.success) {
85
+ return NextResponse.json({ error: 'Invalid credentials payload', details: parsedBody.error.flatten() }, { status: 422 })
86
+ }
87
+
88
+ const container = await createRequestContainer()
89
+ const guardResult = await runIntegrationMutationGuards(
90
+ container,
91
+ {
92
+ tenantId: auth.tenantId,
93
+ organizationId: auth.orgId,
94
+ userId: auth.sub ?? '',
95
+ resourceKind: 'integrations.integration',
96
+ resourceId: integration.id,
97
+ operation: 'update',
98
+ requestMethod: req.method,
99
+ requestHeaders: req.headers,
100
+ mutationPayload: parsedBody.data as Record<string, unknown>,
101
+ },
102
+ resolveUserFeatures(auth),
103
+ )
104
+ if (!guardResult.ok) {
105
+ return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
106
+ }
107
+
108
+ let payloadData = parsedBody.data
109
+ if (guardResult.modifiedPayload) {
110
+ const mergedPayload = { ...parsedBody.data, ...guardResult.modifiedPayload }
111
+ const reparsed = saveCredentialsSchema.safeParse(mergedPayload)
112
+ if (!reparsed.success) {
113
+ return NextResponse.json({ error: 'Invalid credentials payload after guard transform', details: reparsed.error.flatten() }, { status: 422 })
114
+ }
115
+ payloadData = reparsed.data
116
+ }
117
+
118
+ const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
119
+ const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
120
+
121
+ await credentialsService.save(integration.id, payloadData.credentials, scope)
122
+
123
+ await emitIntegrationsEvent('integrations.credentials.updated', {
124
+ integrationId: integration.id,
125
+ tenantId: auth.tenantId,
126
+ organizationId: auth.orgId,
127
+ userId: auth.sub,
128
+ })
129
+
130
+ await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
131
+ tenantId: auth.tenantId,
132
+ organizationId: auth.orgId,
133
+ userId: auth.sub ?? '',
134
+ resourceKind: 'integrations.integration',
135
+ resourceId: integration.id,
136
+ operation: 'update',
137
+ requestMethod: req.method,
138
+ requestHeaders: req.headers,
139
+ })
140
+
141
+ return NextResponse.json({ ok: true })
142
+ }
@@ -0,0 +1,53 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
6
+ import type { IntegrationHealthService } from '../../../lib/health-service'
7
+
8
+ const idParamsSchema = z.object({ id: z.string().min(1) })
9
+
10
+ export const metadata = {
11
+ POST: { requireAuth: true, requireFeatures: ['integrations.manage'] },
12
+ }
13
+
14
+ export const openApi = {
15
+ tags: ['Integrations'],
16
+ summary: 'Run health check for an integration',
17
+ }
18
+
19
+ export async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
20
+ const auth = await getAuthFromRequest(req)
21
+ if (!auth?.tenantId || !auth.orgId) {
22
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
23
+ }
24
+
25
+ const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
26
+ ? await (ctx.params as Promise<{ id?: string }>)
27
+ : (ctx.params as { id?: string } | undefined)
28
+
29
+ const parsedParams = idParamsSchema.safeParse(rawParams)
30
+ if (!parsedParams.success) {
31
+ return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })
32
+ }
33
+
34
+ const integration = getIntegration(parsedParams.data.id)
35
+ if (!integration) {
36
+ return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
37
+ }
38
+
39
+ const container = await createRequestContainer()
40
+ const healthService = container.resolve('integrationHealthService') as IntegrationHealthService
41
+
42
+ const result = await healthService.runHealthCheck(
43
+ integration.id,
44
+ { organizationId: auth.orgId as string, tenantId: auth.tenantId },
45
+ )
46
+
47
+ return NextResponse.json({
48
+ status: result.status,
49
+ message: result.message ?? null,
50
+ details: result.details ?? null,
51
+ checkedAt: new Date().toISOString(),
52
+ })
53
+ }
@@ -0,0 +1,76 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getBundle, getBundleIntegrations, getIntegration } from '@open-mercato/shared/modules/integrations/types'
6
+ import type { CredentialsService } from '../../lib/credentials-service'
7
+ import type { IntegrationStateService } from '../../lib/state-service'
8
+
9
+ const idParamsSchema = z.object({ id: z.string().min(1) })
10
+
11
+ export const metadata = {
12
+ GET: { requireAuth: true, requireFeatures: ['integrations.view'] },
13
+ }
14
+
15
+ export const openApi = {
16
+ tags: ['Integrations'],
17
+ summary: 'Get integration detail',
18
+ }
19
+
20
+ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
21
+ const auth = await getAuthFromRequest(req)
22
+ if (!auth?.tenantId || !auth.orgId) {
23
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
24
+ }
25
+
26
+ const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
27
+ ? await (ctx.params as Promise<{ id?: string }>)
28
+ : (ctx.params as { id?: string } | undefined)
29
+
30
+ const parsedParams = idParamsSchema.safeParse(rawParams)
31
+ if (!parsedParams.success) {
32
+ return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })
33
+ }
34
+
35
+ const integration = getIntegration(parsedParams.data.id)
36
+ if (!integration) {
37
+ return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
38
+ }
39
+
40
+ const container = await createRequestContainer()
41
+ const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
42
+ const stateService = container.resolve('integrationStateService') as IntegrationStateService
43
+ const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
44
+
45
+ const [credentials, state] = await Promise.all([
46
+ credentialsService.resolve(integration.id, scope),
47
+ stateService.get(integration.id, scope),
48
+ ])
49
+
50
+ const bundle = integration.bundleId ? getBundle(integration.bundleId) : undefined
51
+ const bundleIntegrations = integration.bundleId
52
+ ? await Promise.all(
53
+ getBundleIntegrations(integration.bundleId).map(async (item) => {
54
+ const itemState = await stateService.get(item.id, scope)
55
+ return {
56
+ ...item,
57
+ isEnabled: itemState?.isEnabled ?? true,
58
+ }
59
+ }),
60
+ )
61
+ : []
62
+
63
+ return NextResponse.json({
64
+ integration,
65
+ bundle,
66
+ bundleIntegrations,
67
+ state: {
68
+ isEnabled: state?.isEnabled ?? true,
69
+ apiVersion: state?.apiVersion ?? null,
70
+ reauthRequired: state?.reauthRequired ?? false,
71
+ lastHealthStatus: state?.lastHealthStatus ?? null,
72
+ lastHealthCheckedAt: state?.lastHealthCheckedAt?.toISOString() ?? null,
73
+ },
74
+ hasCredentials: Boolean(credentials),
75
+ })
76
+ }