@open-mercato/core 0.10.1-develop.7785.1.ec0898da36 → 0.10.1-develop.7787.1.96c7cfc66f

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.
@@ -5,12 +5,18 @@ import {
5
5
  type MutationGuard,
6
6
  type MutationGuardInput,
7
7
  } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
8
+ import { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'
9
+ import { createLogger } from '@open-mercato/shared/lib/logger'
10
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
11
+
12
+ const logger = createLogger('messages')
8
13
 
9
14
  type GuardAfterCallback = {
10
15
  guard: MutationGuard
11
16
  metadata: Record<string, unknown> | null
12
17
  }
13
18
 
19
+ /** @deprecated Mutation guard grants are resolved from RBAC by `runMessageMutationGuards`. */
14
20
  export function resolveUserFeatures(auth: unknown): string[] {
15
21
  const features = (auth as { features?: unknown })?.features
16
22
  if (!Array.isArray(features)) return []
@@ -20,7 +26,7 @@ export function resolveUserFeatures(auth: unknown): string[] {
20
26
  export async function runMessageMutationGuards(
21
27
  container: AwilixContainer,
22
28
  input: MutationGuardInput,
23
- userFeatures: string[],
29
+ extraFeatures: string[] = [],
24
30
  ): Promise<{
25
31
  ok: boolean
26
32
  errorBody?: Record<string, unknown>
@@ -28,12 +34,35 @@ export async function runMessageMutationGuards(
28
34
  modifiedPayload?: Record<string, unknown>
29
35
  afterSuccessCallbacks: GuardAfterCallback[]
30
36
  }> {
37
+ const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]
31
38
  const legacyGuard = bridgeLegacyGuard(container)
32
- if (!legacyGuard) {
39
+ if (legacyGuard) allGuards.push(legacyGuard)
40
+ if (!allGuards.length) {
33
41
  return { ok: true, afterSuccessCallbacks: [] }
34
42
  }
35
43
 
36
- return runMutationGuards([legacyGuard], input, { userFeatures })
44
+ let grantedFeatures: string[] = []
45
+ try {
46
+ const rbacService = container.resolve('rbacService') as {
47
+ getGrantedFeatures?: (
48
+ userId: string,
49
+ scope: { tenantId: string | null; organizationId: string | null },
50
+ ) => Promise<string[]>
51
+ } | undefined
52
+ grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {
53
+ tenantId: input.tenantId,
54
+ organizationId: input.organizationId,
55
+ }) ?? []
56
+ } catch (err) {
57
+ logger.error('Failed to resolve mutation guard feature grants', { err })
58
+ getTelemetryRuntime()?.reportError(err, {
59
+ module: 'messages',
60
+ code: 'messages.mutation_guard_feature_resolution_failed',
61
+ })
62
+ }
63
+
64
+ const userFeatures = Array.from(new Set([...grantedFeatures, ...extraFeatures]))
65
+ return runMutationGuards(allGuards, input, { userFeatures })
37
66
  }
38
67
 
39
68
  export async function runMessageMutationGuardAfterSuccess(
@@ -51,9 +80,18 @@ export async function runMessageMutationGuardAfterSuccess(
51
80
  ): Promise<void> {
52
81
  for (const callback of callbacks) {
53
82
  if (!callback.guard.afterSuccess) continue
54
- await callback.guard.afterSuccess({
55
- ...input,
56
- metadata: callback.metadata ?? null,
57
- })
83
+ try {
84
+ await callback.guard.afterSuccess({
85
+ ...input,
86
+ metadata: callback.metadata ?? null,
87
+ })
88
+ } catch (err) {
89
+ logger.error('Mutation guard afterSuccess failed', { guardId: callback.guard.id, err })
90
+ getTelemetryRuntime()?.reportError(err, {
91
+ module: 'messages',
92
+ code: 'messages.mutation_guard_after_success_failed',
93
+ attributes: { guardId: callback.guard.id },
94
+ })
95
+ }
58
96
  }
59
97
  }
@@ -5,12 +5,18 @@ import {
5
5
  type MutationGuard,
6
6
  type MutationGuardInput,
7
7
  } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
8
+ import { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'
9
+ import { createLogger } from '@open-mercato/shared/lib/logger'
10
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
11
+
12
+ const logger = createLogger('payment_gateways')
8
13
 
9
14
  type GuardAfterCallback = {
10
15
  guard: MutationGuard
11
16
  metadata: Record<string, unknown> | null
12
17
  }
13
18
 
19
+ /** @deprecated Mutation guard grants are resolved from RBAC by `runPaymentGatewayMutationGuards`. */
14
20
  export function resolveUserFeatures(auth: unknown): string[] {
15
21
  const features = (auth as { features?: unknown })?.features
16
22
  if (!Array.isArray(features)) return []
@@ -20,7 +26,7 @@ export function resolveUserFeatures(auth: unknown): string[] {
20
26
  export async function runPaymentGatewayMutationGuards(
21
27
  container: AwilixContainer,
22
28
  input: MutationGuardInput,
23
- userFeatures: string[],
29
+ extraFeatures: string[] = [],
24
30
  ): Promise<{
25
31
  ok: boolean
26
32
  errorBody?: Record<string, unknown>
@@ -28,12 +34,35 @@ export async function runPaymentGatewayMutationGuards(
28
34
  modifiedPayload?: Record<string, unknown>
29
35
  afterSuccessCallbacks: GuardAfterCallback[]
30
36
  }> {
37
+ const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]
31
38
  const legacyGuard = bridgeLegacyGuard(container)
32
- if (!legacyGuard) {
39
+ if (legacyGuard) allGuards.push(legacyGuard)
40
+ if (!allGuards.length) {
33
41
  return { ok: true, afterSuccessCallbacks: [] }
34
42
  }
35
43
 
36
- return runMutationGuards([legacyGuard], input, { userFeatures })
44
+ let grantedFeatures: string[] = []
45
+ try {
46
+ const rbacService = container.resolve('rbacService') as {
47
+ getGrantedFeatures?: (
48
+ userId: string,
49
+ scope: { tenantId: string | null; organizationId: string | null },
50
+ ) => Promise<string[]>
51
+ } | undefined
52
+ grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {
53
+ tenantId: input.tenantId,
54
+ organizationId: input.organizationId,
55
+ }) ?? []
56
+ } catch (err) {
57
+ logger.error('Failed to resolve mutation guard feature grants', { err })
58
+ getTelemetryRuntime()?.reportError(err, {
59
+ module: 'payment_gateways',
60
+ code: 'payment_gateways.mutation_guard_feature_resolution_failed',
61
+ })
62
+ }
63
+
64
+ const userFeatures = Array.from(new Set([...grantedFeatures, ...extraFeatures]))
65
+ return runMutationGuards(allGuards, input, { userFeatures })
37
66
  }
38
67
 
39
68
  export async function runPaymentGatewayMutationGuardAfterSuccess(
@@ -51,9 +80,18 @@ export async function runPaymentGatewayMutationGuardAfterSuccess(
51
80
  ): Promise<void> {
52
81
  for (const callback of callbacks) {
53
82
  if (!callback.guard.afterSuccess) continue
54
- await callback.guard.afterSuccess({
55
- ...input,
56
- metadata: callback.metadata ?? null,
57
- })
83
+ try {
84
+ await callback.guard.afterSuccess({
85
+ ...input,
86
+ metadata: callback.metadata ?? null,
87
+ })
88
+ } catch (err) {
89
+ logger.error('Mutation guard afterSuccess failed', { guardId: callback.guard.id, err })
90
+ getTelemetryRuntime()?.reportError(err, {
91
+ module: 'payment_gateways',
92
+ code: 'payment_gateways.mutation_guard_after_success_failed',
93
+ attributes: { guardId: callback.guard.id },
94
+ })
95
+ }
58
96
  }
59
97
  }
@@ -7,13 +7,7 @@ import { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/di
7
7
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
8
8
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
9
  import { createLogger } from '@open-mercato/shared/lib/logger'
10
- import {
11
- bridgeLegacyGuard,
12
- runMutationGuards,
13
- type MutationGuard,
14
- type MutationGuardInput,
15
- } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
16
- import type { AwilixContainer } from 'awilix'
10
+ import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
17
11
  import {
18
12
  customSendSchema,
19
13
  customSendResponseSchema,
@@ -32,27 +26,6 @@ export const metadata = {
32
26
  POST: { requireAuth: true, requireFeatures: ['push_notifications.send_custom'] },
33
27
  }
34
28
 
35
- function resolveUserFeatures(auth: unknown): string[] {
36
- const features = (auth as { features?: unknown })?.features
37
- if (!Array.isArray(features)) return []
38
- return features.filter((value): value is string => typeof value === 'string')
39
- }
40
-
41
- async function runGuards(
42
- container: AwilixContainer,
43
- userFeatures: string[],
44
- input: MutationGuardInput,
45
- ): Promise<{
46
- ok: boolean
47
- errorBody?: Record<string, unknown>
48
- errorStatus?: number
49
- afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>
50
- }> {
51
- const legacyGuard = bridgeLegacyGuard(container)
52
- if (!legacyGuard) return { ok: true, afterSuccessCallbacks: [] }
53
- return runMutationGuards([legacyGuard], input, { userFeatures })
54
- }
55
-
56
29
  export async function POST(req: Request) {
57
30
  const { translate } = await resolveTranslations()
58
31
  try {
@@ -71,23 +44,23 @@ export async function POST(req: Request) {
71
44
 
72
45
  // Custom write route → wire the mutation-guard registry (AGENTS → API Routes). The send creates
73
46
  // append-only delivery rows; map it to a `create` on the delivery resource keyed by recipient.
74
- const guardInput: MutationGuardInput = {
75
- tenantId: auth.tenantId,
76
- organizationId,
77
- userId: auth.sub,
78
- resourceKind: RESOURCE_KIND,
79
- resourceId: body.recipientUserId,
80
- operation: 'create',
81
- requestMethod: req.method,
82
- requestHeaders: req.headers,
83
- mutationPayload: body,
84
- }
85
- const guardResult = await runGuards(container, resolveUserFeatures(auth), guardInput)
47
+ const guardResult = await runRouteMutationGuards({
48
+ container,
49
+ req,
50
+ auth: {
51
+ tenantId: auth.tenantId,
52
+ organizationId,
53
+ userId: auth.sub,
54
+ },
55
+ input: {
56
+ resourceKind: RESOURCE_KIND,
57
+ resourceId: body.recipientUserId,
58
+ operation: 'create',
59
+ mutationPayload: body,
60
+ },
61
+ })
86
62
  if (!guardResult.ok) {
87
- return NextResponse.json(
88
- guardResult.errorBody ?? { error: translate('push_notifications.errors.send_failed', 'Operation blocked') },
89
- { status: guardResult.errorStatus ?? 422 },
90
- )
63
+ return guardResult.response
91
64
  }
92
65
 
93
66
  const service = container.resolve('pushNotificationService') as PushNotificationService
@@ -104,14 +77,7 @@ export async function POST(req: Request) {
104
77
  silent: body.silent ?? false,
105
78
  })
106
79
 
107
- for (const callback of guardResult.afterSuccessCallbacks) {
108
- if (!callback.guard.afterSuccess) continue
109
- await callback.guard.afterSuccess({
110
- ...guardInput,
111
- resourceId: body.recipientUserId,
112
- metadata: callback.metadata ?? null,
113
- })
114
- }
80
+ await guardResult.runAfterSuccess()
115
81
 
116
82
  // A well-formed request that enqueued nothing (no push channel, no in-scope device, or no device
117
83
  // whose provider matches an active channel) previously returned a bare 201 — a silent
@@ -9,12 +9,7 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
9
9
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
10
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
11
11
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
12
- import {
13
- bridgeLegacyGuard,
14
- runMutationGuards,
15
- type MutationGuard,
16
- type MutationGuardInput,
17
- } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
12
+ import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
18
13
  import { withScopedPayload } from '../../utils'
19
14
  import { createLogger } from '@open-mercato/shared/lib/logger'
20
15
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
@@ -35,53 +30,6 @@ type RequestContext = {
35
30
  ctx: CommandRuntimeContext
36
31
  }
37
32
 
38
- function resolveUserFeatures(auth: unknown): string[] {
39
- const features = (auth as { features?: unknown })?.features
40
- if (!Array.isArray(features)) return []
41
- return features.filter((value): value is string => typeof value === 'string')
42
- }
43
-
44
- async function runGuards(
45
- ctx: CommandRuntimeContext,
46
- input: MutationGuardInput,
47
- ): Promise<{
48
- ok: boolean
49
- errorBody?: Record<string, unknown>
50
- errorStatus?: number
51
- afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>
52
- }> {
53
- const legacyGuard = bridgeLegacyGuard(ctx.container)
54
- if (!legacyGuard) {
55
- return { ok: true, afterSuccessCallbacks: [] }
56
- }
57
-
58
- return runMutationGuards([legacyGuard], input, {
59
- userFeatures: resolveUserFeatures(ctx.auth),
60
- })
61
- }
62
-
63
- async function runGuardAfterSuccessCallbacks(
64
- callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,
65
- input: {
66
- tenantId: string
67
- organizationId: string | null
68
- userId: string
69
- resourceKind: string
70
- resourceId: string
71
- operation: 'create' | 'update' | 'delete'
72
- requestMethod: string
73
- requestHeaders: Headers
74
- },
75
- ): Promise<void> {
76
- for (const callback of callbacks) {
77
- if (!callback.guard.afterSuccess) continue
78
- await callback.guard.afterSuccess({
79
- ...input,
80
- metadata: callback.metadata ?? null,
81
- })
82
- }
83
- }
84
-
85
33
  async function resolveRequestContext(req: Request): Promise<RequestContext> {
86
34
  const container = await createRequestContainer()
87
35
  const auth = await getAuthFromRequest(req)
@@ -118,18 +66,23 @@ export async function POST(req: Request) {
118
66
  const payload = await req.json().catch(() => ({}))
119
67
  const scoped = withScopedPayload(payload ?? {}, ctx, translate)
120
68
  const input = convertSchema.parse(scoped)
121
- const guardResult = await runGuards(ctx, {
122
- tenantId: ctx.auth?.tenantId ?? '',
123
- organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
124
- userId: ctx.auth?.sub ?? '',
125
- resourceKind: 'sales.quote',
126
- resourceId: input.quoteId,
127
- operation: 'update',
128
- requestMethod: req.method,
129
- requestHeaders: req.headers,
69
+ const guardResult = await runRouteMutationGuards({
70
+ container: ctx.container,
71
+ req,
72
+ auth: {
73
+ tenantId: ctx.auth?.tenantId ?? '',
74
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
75
+ userId: ctx.auth?.sub ?? '',
76
+ },
77
+ input: {
78
+ resourceKind: 'sales.quote',
79
+ resourceId: input.quoteId,
80
+ operation: 'update',
81
+ mutationPayload: input,
82
+ },
130
83
  })
131
84
  if (!guardResult.ok) {
132
- return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
85
+ return guardResult.response
133
86
  }
134
87
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
135
88
  const { result, logEntry } = await commandBus.execute<
@@ -159,18 +112,7 @@ export async function POST(req: Request) {
159
112
  )
160
113
  }
161
114
 
162
- if (guardResult.afterSuccessCallbacks.length) {
163
- await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
164
- tenantId: ctx.auth?.tenantId ?? '',
165
- organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
166
- userId: ctx.auth?.sub ?? '',
167
- resourceKind: 'sales.quote',
168
- resourceId: input.quoteId,
169
- operation: 'update',
170
- requestMethod: req.method,
171
- requestHeaders: req.headers,
172
- })
173
- }
115
+ await guardResult.runAfterSuccess()
174
116
 
175
117
  return jsonResponse
176
118
  } catch (err) {
@@ -8,12 +8,7 @@ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
8
8
  import { resolveTranslations, detectLocale } from '@open-mercato/shared/lib/i18n/server'
9
9
  import { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'
10
10
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
11
- import {
12
- bridgeLegacyGuard,
13
- runMutationGuards,
14
- type MutationGuard,
15
- type MutationGuardInput,
16
- } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
11
+ import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
17
12
  import type { EntityManager } from '@mikro-orm/postgresql'
18
13
  import crypto from 'node:crypto'
19
14
  import { withScopedPayload } from '../../utils'
@@ -37,53 +32,6 @@ type RequestContext = {
37
32
  ctx: CommandRuntimeContext
38
33
  }
39
34
 
40
- function resolveUserFeatures(auth: unknown): string[] {
41
- const features = (auth as { features?: unknown })?.features
42
- if (!Array.isArray(features)) return []
43
- return features.filter((value): value is string => typeof value === 'string')
44
- }
45
-
46
- async function runGuards(
47
- ctx: CommandRuntimeContext,
48
- input: MutationGuardInput,
49
- ): Promise<{
50
- ok: boolean
51
- errorBody?: Record<string, unknown>
52
- errorStatus?: number
53
- afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>
54
- }> {
55
- const legacyGuard = bridgeLegacyGuard(ctx.container)
56
- if (!legacyGuard) {
57
- return { ok: true, afterSuccessCallbacks: [] }
58
- }
59
-
60
- return runMutationGuards([legacyGuard], input, {
61
- userFeatures: resolveUserFeatures(ctx.auth),
62
- })
63
- }
64
-
65
- async function runGuardAfterSuccessCallbacks(
66
- callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,
67
- input: {
68
- tenantId: string
69
- organizationId: string | null
70
- userId: string
71
- resourceKind: string
72
- resourceId: string
73
- operation: 'create' | 'update' | 'delete'
74
- requestMethod: string
75
- requestHeaders: Headers
76
- },
77
- ): Promise<void> {
78
- for (const callback of callbacks) {
79
- if (!callback.guard.afterSuccess) continue
80
- await callback.guard.afterSuccess({
81
- ...input,
82
- metadata: callback.metadata ?? null,
83
- })
84
- }
85
- }
86
-
87
35
  async function resolveRequestContext(req: Request): Promise<RequestContext> {
88
36
  const container = await createRequestContainer()
89
37
  const auth = await getAuthFromRequest(req)
@@ -143,18 +91,23 @@ export async function POST(req: Request) {
143
91
  const payload = await req.json().catch(() => ({}))
144
92
  const scoped = withScopedPayload(payload ?? {}, ctx, translate)
145
93
  const input = quoteSendSchema.parse(scoped)
146
- const guardResult = await runGuards(ctx, {
147
- tenantId: ctx.auth?.tenantId ?? '',
148
- organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
149
- userId: ctx.auth?.sub ?? '',
150
- resourceKind: 'sales.quote',
151
- resourceId: input.quoteId,
152
- operation: 'update',
153
- requestMethod: req.method,
154
- requestHeaders: req.headers,
94
+ const guardResult = await runRouteMutationGuards({
95
+ container: ctx.container,
96
+ req,
97
+ auth: {
98
+ tenantId: ctx.auth?.tenantId ?? '',
99
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
100
+ userId: ctx.auth?.sub ?? '',
101
+ },
102
+ input: {
103
+ resourceKind: 'sales.quote',
104
+ resourceId: input.quoteId,
105
+ operation: 'update',
106
+ mutationPayload: input,
107
+ },
155
108
  })
156
109
  if (!guardResult.ok) {
157
- return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
110
+ return guardResult.response
158
111
  }
159
112
 
160
113
  const em = (ctx.container.resolve('em') as EntityManager).fork()
@@ -232,18 +185,7 @@ export async function POST(req: Request) {
232
185
  organizationId: quote.organizationId,
233
186
  })
234
187
 
235
- if (guardResult.afterSuccessCallbacks.length) {
236
- await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
237
- tenantId: ctx.auth?.tenantId ?? '',
238
- organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
239
- userId: ctx.auth?.sub ?? '',
240
- resourceKind: 'sales.quote',
241
- resourceId: input.quoteId,
242
- operation: 'update',
243
- requestMethod: req.method,
244
- requestHeaders: req.headers,
245
- })
246
- }
188
+ await guardResult.runAfterSuccess()
247
189
 
248
190
  return NextResponse.json({ ok: true })
249
191
  } catch (err) {