@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.
@@ -4,10 +4,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
5
  import { runWithCacheTenant } from "@open-mercato/cache";
6
6
  import { collectCrudCacheStats, purgeCrudCacheSegment } from "@open-mercato/shared/lib/crud/cache-stats";
7
- import {
8
- bridgeLegacyGuard,
9
- runMutationGuards
10
- } from "@open-mercato/shared/lib/crud/mutation-guard-registry";
7
+ import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
11
8
  import {
12
9
  configsTag,
13
10
  cacheStatsResponseSchema,
@@ -22,10 +19,6 @@ const metadata = {
22
19
  GET: { requireAuth: true, requireFeatures: ["configs.cache.view"] },
23
20
  POST: { requireAuth: true, requireFeatures: ["configs.cache.manage"] }
24
21
  };
25
- function resolveUserFeatures(auth) {
26
- const features = auth?.features;
27
- return Array.isArray(features) ? features.filter((value) => typeof value === "string") : [];
28
- }
29
22
  async function GET(req) {
30
23
  const auth = await getAuthFromRequest(req);
31
24
  if (!auth?.sub) {
@@ -85,30 +78,23 @@ async function POST(req) {
85
78
  { status: 400 }
86
79
  );
87
80
  }
88
- const guardInput = {
89
- tenantId: tenantScope ?? "",
90
- organizationId: auth.orgId ?? null,
91
- userId: auth.sub,
92
- resourceKind: "configs.cache",
93
- resourceId: segment ?? tenantScope ?? "all",
94
- operation: "delete",
95
- requestMethod: req.method,
96
- requestHeaders: req.headers,
97
- mutationPayload: { action, ...segment ? { segment } : {} }
98
- };
99
- const guard = bridgeLegacyGuard(container);
100
- let afterSuccessCallbacks = [];
101
- if (guard) {
102
- const guardResult = await runMutationGuards([guard], guardInput, {
103
- userFeatures: resolveUserFeatures(auth)
104
- });
105
- if (!guardResult.ok) {
106
- return NextResponse.json(
107
- guardResult.errorBody ?? { error: translate("configs.cache.purgeError", "Failed to purge cache segment.") },
108
- { status: guardResult.errorStatus ?? 422 }
109
- );
81
+ const guardResult = await runRouteMutationGuards({
82
+ container,
83
+ req,
84
+ auth: {
85
+ tenantId: tenantScope ?? "",
86
+ organizationId: auth.orgId ?? null,
87
+ userId: auth.sub
88
+ },
89
+ input: {
90
+ resourceKind: "configs.cache",
91
+ resourceId: segment ?? tenantScope ?? "all",
92
+ operation: "delete",
93
+ mutationPayload: { action, ...segment ? { segment } : {} }
110
94
  }
111
- afterSuccessCallbacks = guardResult.afterSuccessCallbacks;
95
+ });
96
+ if (!guardResult.ok) {
97
+ return guardResult.response;
112
98
  }
113
99
  let responseBody;
114
100
  if (action === "purgeSegment") {
@@ -120,20 +106,7 @@ async function POST(req) {
120
106
  const stats = await runWithCacheTenant(tenantScope, () => collectCrudCacheStats(cache));
121
107
  responseBody = { action: "purgeAll", stats };
122
108
  }
123
- for (const callback of afterSuccessCallbacks) {
124
- if (!callback.guard.afterSuccess) continue;
125
- await callback.guard.afterSuccess({
126
- tenantId: tenantScope ?? "",
127
- organizationId: auth.orgId ?? null,
128
- userId: auth.sub,
129
- resourceKind: "configs.cache",
130
- resourceId: segment ?? tenantScope ?? "all",
131
- operation: "delete",
132
- requestMethod: req.method,
133
- requestHeaders: req.headers,
134
- metadata: callback.metadata ?? null
135
- });
136
- }
109
+ await guardResult.runAfterSuccess();
137
110
  return NextResponse.json(responseBody);
138
111
  } catch (error) {
139
112
  logger.error("failed to purge cache", { component: "cache", err: error });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/configs/api/cache/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { collectCrudCacheStats, purgeCrudCacheSegment } from '@open-mercato/shared/lib/crud/cache-stats'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport {\n configsTag,\n cacheStatsResponseSchema,\n cachePurgeRequestSchema,\n cachePurgeAllResponseSchema,\n cachePurgeSegmentResponseSchema,\n configErrorSchema,\n} from '../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('configs')\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['configs.cache.view'] },\n POST: { requireAuth: true, requireFeatures: ['configs.cache.manage'] },\n} as const\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown } | null)?.features\n return Array.isArray(features) ? features.filter((value): value is string => typeof value === 'string') : []\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const { translate } = await resolveTranslations()\n\n try {\n const container = await createRequestContainer()\n let cache: CacheStrategy\n try {\n cache = (container.resolve('cache') as CacheStrategy)\n } catch {\n return NextResponse.json(\n { error: translate('configs.cache.unavailable', 'Cache service is unavailable.') },\n { status: 503 },\n )\n }\n const stats = await runWithCacheTenant(auth.tenantId ?? null, () => collectCrudCacheStats(cache))\n return NextResponse.json(stats)\n } catch (error) {\n logger.error('failed to resolve cache stats', { component: 'cache', err: error })\n return NextResponse.json(\n { error: translate('configs.cache.unavailable', 'Cache service is unavailable.') },\n { status: 500 },\n )\n }\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const { translate } = await resolveTranslations()\n\n let body: any = null\n try {\n body = await req.json()\n } catch {\n body = null\n }\n\n const action = typeof body?.action === 'string' ? body.action : 'purgeAll'\n\n try {\n const container = await createRequestContainer()\n let cache: CacheStrategy\n try {\n cache = (container.resolve('cache') as CacheStrategy)\n } catch {\n return NextResponse.json(\n { error: translate('configs.cache.unavailable', 'Cache service is unavailable.') },\n { status: 503 },\n )\n }\n const tenantScope = auth.tenantId ?? null\n\n const segment =\n action === 'purgeSegment'\n ? (typeof body?.segment === 'string' ? body.segment.trim() : '')\n : null\n if (action === 'purgeSegment' && !segment) {\n return NextResponse.json(\n { error: translate('configs.cache.purgeError', 'Failed to purge cache segment.') },\n { status: 400 },\n )\n }\n\n // Mutation-guard contract for this custom write route (packages/core/AGENTS.md\n // \u2192 API Routes). A cache purge carries no per-record optimistic-lock version, so\n // the default OSS guard short-circuits; wiring it keeps the route on the shared\n // write-guard interception path for any tenant-registered guard.\n const guardInput: MutationGuardInput = {\n tenantId: tenantScope ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'configs.cache',\n resourceId: segment ?? tenantScope ?? 'all',\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { action, ...(segment ? { segment } : {}) },\n }\n const guard = bridgeLegacyGuard(container)\n let afterSuccessCallbacks: Awaited<ReturnType<typeof runMutationGuards>>['afterSuccessCallbacks'] = []\n if (guard) {\n const guardResult = await runMutationGuards([guard], guardInput, {\n userFeatures: resolveUserFeatures(auth),\n })\n if (!guardResult.ok) {\n return NextResponse.json(\n guardResult.errorBody ?? { error: translate('configs.cache.purgeError', 'Failed to purge cache segment.') },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n afterSuccessCallbacks = guardResult.afterSuccessCallbacks\n }\n\n let responseBody: Record<string, unknown>\n if (action === 'purgeSegment') {\n const result = await runWithCacheTenant(tenantScope, () => purgeCrudCacheSegment(cache, segment!))\n const stats = await runWithCacheTenant(tenantScope, () => collectCrudCacheStats(cache))\n responseBody = { action: 'purgeSegment', segment, deleted: result.deleted, stats }\n } else {\n await runWithCacheTenant(tenantScope, () => cache.clear())\n const stats = await runWithCacheTenant(tenantScope, () => collectCrudCacheStats(cache))\n responseBody = { action: 'purgeAll', stats }\n }\n\n for (const callback of afterSuccessCallbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n tenantId: tenantScope ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'configs.cache',\n resourceId: segment ?? tenantScope ?? 'all',\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: callback.metadata ?? null,\n })\n }\n\n return NextResponse.json(responseBody)\n } catch (error) {\n logger.error('failed to purge cache', { component: 'cache', err: error })\n return NextResponse.json(\n { error: translate('configs.cache.purgeError', 'Failed to purge cache segment.') },\n { status: 500 },\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: configsTag,\n summary: 'Cache management',\n methods: {\n GET: {\n summary: 'Get cache statistics',\n description: 'Returns detailed cache statistics including total entries and breakdown by cache segments. Requires cache service to be available.',\n responses: [\n { status: 200, description: 'Cache statistics', schema: cacheStatsResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: configErrorSchema },\n { status: 500, description: 'Failed to resolve cache stats', schema: configErrorSchema },\n { status: 503, description: 'Cache service unavailable', schema: configErrorSchema },\n ],\n },\n POST: {\n summary: 'Purge cache',\n description: 'Purges cache entries. Supports two actions: purgeAll (clears entire cache) or purgeSegment (clears specific segment). Returns updated cache statistics after purge.',\n requestBody: {\n contentType: 'application/json',\n schema: cachePurgeRequestSchema,\n },\n responses: [\n { status: 200, description: 'All cache cleared successfully', schema: cachePurgeAllResponseSchema },\n { status: 200, description: 'Cache segment cleared successfully', schema: cachePurgeSegmentResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request - missing segment identifier for purgeSegment action', schema: configErrorSchema },\n { status: 401, description: 'Unauthorized', schema: configErrorSchema },\n { status: 500, description: 'Failed to purge cache', schema: configErrorSchema },\n { status: 503, description: 'Cache service unavailable', schema: configErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { collectCrudCacheStats, purgeCrudCacheSegment } from '@open-mercato/shared/lib/crud/cache-stats'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport {\n configsTag,\n cacheStatsResponseSchema,\n cachePurgeRequestSchema,\n cachePurgeAllResponseSchema,\n cachePurgeSegmentResponseSchema,\n configErrorSchema,\n} from '../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('configs')\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['configs.cache.view'] },\n POST: { requireAuth: true, requireFeatures: ['configs.cache.manage'] },\n} as const\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const { translate } = await resolveTranslations()\n\n try {\n const container = await createRequestContainer()\n let cache: CacheStrategy\n try {\n cache = (container.resolve('cache') as CacheStrategy)\n } catch {\n return NextResponse.json(\n { error: translate('configs.cache.unavailable', 'Cache service is unavailable.') },\n { status: 503 },\n )\n }\n const stats = await runWithCacheTenant(auth.tenantId ?? null, () => collectCrudCacheStats(cache))\n return NextResponse.json(stats)\n } catch (error) {\n logger.error('failed to resolve cache stats', { component: 'cache', err: error })\n return NextResponse.json(\n { error: translate('configs.cache.unavailable', 'Cache service is unavailable.') },\n { status: 500 },\n )\n }\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const { translate } = await resolveTranslations()\n\n let body: any = null\n try {\n body = await req.json()\n } catch {\n body = null\n }\n\n const action = typeof body?.action === 'string' ? body.action : 'purgeAll'\n\n try {\n const container = await createRequestContainer()\n let cache: CacheStrategy\n try {\n cache = (container.resolve('cache') as CacheStrategy)\n } catch {\n return NextResponse.json(\n { error: translate('configs.cache.unavailable', 'Cache service is unavailable.') },\n { status: 503 },\n )\n }\n const tenantScope = auth.tenantId ?? null\n\n const segment =\n action === 'purgeSegment'\n ? (typeof body?.segment === 'string' ? body.segment.trim() : '')\n : null\n if (action === 'purgeSegment' && !segment) {\n return NextResponse.json(\n { error: translate('configs.cache.purgeError', 'Failed to purge cache segment.') },\n { status: 400 },\n )\n }\n\n // Mutation-guard contract for this custom write route (packages/core/AGENTS.md\n // \u2192 API Routes). A cache purge carries no per-record optimistic-lock version, so\n // the default OSS guard short-circuits; wiring it keeps the route on the shared\n // write-guard interception path for any tenant-registered guard.\n const guardResult = await runRouteMutationGuards({\n container,\n req,\n auth: {\n tenantId: tenantScope ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n },\n input: {\n resourceKind: 'configs.cache',\n resourceId: segment ?? tenantScope ?? 'all',\n operation: 'delete',\n mutationPayload: { action, ...(segment ? { segment } : {}) },\n },\n })\n if (!guardResult.ok) {\n return guardResult.response\n }\n\n let responseBody: Record<string, unknown>\n if (action === 'purgeSegment') {\n const result = await runWithCacheTenant(tenantScope, () => purgeCrudCacheSegment(cache, segment!))\n const stats = await runWithCacheTenant(tenantScope, () => collectCrudCacheStats(cache))\n responseBody = { action: 'purgeSegment', segment, deleted: result.deleted, stats }\n } else {\n await runWithCacheTenant(tenantScope, () => cache.clear())\n const stats = await runWithCacheTenant(tenantScope, () => collectCrudCacheStats(cache))\n responseBody = { action: 'purgeAll', stats }\n }\n\n await guardResult.runAfterSuccess()\n\n return NextResponse.json(responseBody)\n } catch (error) {\n logger.error('failed to purge cache', { component: 'cache', err: error })\n return NextResponse.json(\n { error: translate('configs.cache.purgeError', 'Failed to purge cache segment.') },\n { status: 500 },\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: configsTag,\n summary: 'Cache management',\n methods: {\n GET: {\n summary: 'Get cache statistics',\n description: 'Returns detailed cache statistics including total entries and breakdown by cache segments. Requires cache service to be available.',\n responses: [\n { status: 200, description: 'Cache statistics', schema: cacheStatsResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: configErrorSchema },\n { status: 500, description: 'Failed to resolve cache stats', schema: configErrorSchema },\n { status: 503, description: 'Cache service unavailable', schema: configErrorSchema },\n ],\n },\n POST: {\n summary: 'Purge cache',\n description: 'Purges cache entries. Supports two actions: purgeAll (clears entire cache) or purgeSegment (clears specific segment). Returns updated cache statistics after purge.',\n requestBody: {\n contentType: 'application/json',\n schema: cachePurgeRequestSchema,\n },\n responses: [\n { status: 200, description: 'All cache cleared successfully', schema: cachePurgeAllResponseSchema },\n { status: 200, description: 'Cache segment cleared successfully', schema: cachePurgeSegmentResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request - missing segment identifier for purgeSegment action', schema: configErrorSchema },\n { status: 401, description: 'Unauthorized', schema: configErrorSchema },\n { status: 500, description: 'Failed to purge cache', schema: configErrorSchema },\n { status: 503, description: 'Cache service unavailable', schema: configErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,25 +2,55 @@ import {
2
2
  bridgeLegacyGuard,
3
3
  runMutationGuards
4
4
  } from "@open-mercato/shared/lib/crud/mutation-guard-registry";
5
+ import { getAllMutationGuardInstances } from "@open-mercato/shared/lib/crud/mutation-guard-store";
6
+ import { createLogger } from "@open-mercato/shared/lib/logger";
7
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
8
+ const logger = createLogger("integrations");
5
9
  function resolveUserFeatures(auth) {
6
10
  const features = auth?.features;
7
11
  if (!Array.isArray(features)) return [];
8
12
  return features.filter((value) => typeof value === "string");
9
13
  }
10
- async function runIntegrationMutationGuards(container, input, userFeatures) {
14
+ async function runIntegrationMutationGuards(container, input, extraFeatures = []) {
15
+ const allGuards = [...getAllMutationGuardInstances()];
11
16
  const legacyGuard = bridgeLegacyGuard(container);
12
- if (!legacyGuard) {
17
+ if (legacyGuard) allGuards.push(legacyGuard);
18
+ if (!allGuards.length) {
13
19
  return { ok: true, afterSuccessCallbacks: [] };
14
20
  }
15
- return runMutationGuards([legacyGuard], input, { userFeatures });
21
+ let grantedFeatures = [];
22
+ try {
23
+ const rbacService = container.resolve("rbacService");
24
+ grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {
25
+ tenantId: input.tenantId,
26
+ organizationId: input.organizationId
27
+ }) ?? [];
28
+ } catch (err) {
29
+ logger.error("Failed to resolve mutation guard feature grants", { err });
30
+ getTelemetryRuntime()?.reportError(err, {
31
+ module: "integrations",
32
+ code: "integrations.mutation_guard_feature_resolution_failed"
33
+ });
34
+ }
35
+ const userFeatures = Array.from(/* @__PURE__ */ new Set([...grantedFeatures, ...extraFeatures]));
36
+ return runMutationGuards(allGuards, input, { userFeatures });
16
37
  }
17
38
  async function runIntegrationMutationGuardAfterSuccess(callbacks, input) {
18
39
  for (const callback of callbacks) {
19
40
  if (!callback.guard.afterSuccess) continue;
20
- await callback.guard.afterSuccess({
21
- ...input,
22
- metadata: callback.metadata ?? null
23
- });
41
+ try {
42
+ await callback.guard.afterSuccess({
43
+ ...input,
44
+ metadata: callback.metadata ?? null
45
+ });
46
+ } catch (err) {
47
+ logger.error("Mutation guard afterSuccess failed", { guardId: callback.guard.id, err });
48
+ getTelemetryRuntime()?.reportError(err, {
49
+ module: "integrations",
50
+ code: "integrations.mutation_guard_after_success_failed",
51
+ attributes: { guardId: callback.guard.id }
52
+ });
53
+ }
24
54
  }
25
55
  }
26
56
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/integrations/api/guards.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\n\ntype GuardAfterCallback = {\n guard: MutationGuard\n metadata: Record<string, unknown> | null\n}\n\nexport function resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nexport async function runIntegrationMutationGuards(\n container: AwilixContainer,\n input: MutationGuardInput,\n userFeatures: string[],\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: GuardAfterCallback[]\n}> {\n const legacyGuard = bridgeLegacyGuard(container)\n if (!legacyGuard) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards([legacyGuard], input, { userFeatures })\n}\n\nexport async function runIntegrationMutationGuardAfterSuccess(\n callbacks: GuardAfterCallback[],\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n }\n}\n"],
5
- "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAOA,SAAS,oBAAoB,MAAyB;AAC3D,QAAM,WAAY,MAAiC;AACnD,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO,CAAC;AACtC,SAAO,SAAS,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC9E;AAEA,eAAsB,6BACpB,WACA,OACA,cAOC;AACD,QAAM,cAAc,kBAAkB,SAAS;AAC/C,MAAI,CAAC,aAAa;AAChB,WAAO,EAAE,IAAI,MAAM,uBAAuB,CAAC,EAAE;AAAA,EAC/C;AAEA,SAAO,kBAAkB,CAAC,WAAW,GAAG,OAAO,EAAE,aAAa,CAAC;AACjE;AAEA,eAAsB,wCACpB,WACA,OAUe;AACf,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,MAAM,aAAc;AAClC,UAAM,SAAS,MAAM,aAAa;AAAA,MAChC,GAAG;AAAA,MACH,UAAU,SAAS,YAAY;AAAA,IACjC,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nconst logger = createLogger('integrations')\n\ntype GuardAfterCallback = {\n guard: MutationGuard\n metadata: Record<string, unknown> | null\n}\n\n/** @deprecated Mutation guard grants are resolved from RBAC by `runIntegrationMutationGuards`. */\nexport function resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nexport async function runIntegrationMutationGuards(\n container: AwilixContainer,\n input: MutationGuardInput,\n extraFeatures: string[] = [],\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: GuardAfterCallback[]\n}> {\n const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]\n const legacyGuard = bridgeLegacyGuard(container)\n if (legacyGuard) allGuards.push(legacyGuard)\n if (!allGuards.length) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n let grantedFeatures: string[] = []\n try {\n const rbacService = container.resolve('rbacService') as {\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n } | undefined\n grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }) ?? []\n } catch (err) {\n logger.error('Failed to resolve mutation guard feature grants', { err })\n getTelemetryRuntime()?.reportError(err, {\n module: 'integrations',\n code: 'integrations.mutation_guard_feature_resolution_failed',\n })\n }\n\n const userFeatures = Array.from(new Set([...grantedFeatures, ...extraFeatures]))\n return runMutationGuards(allGuards, input, { userFeatures })\n}\n\nexport async function runIntegrationMutationGuardAfterSuccess(\n callbacks: GuardAfterCallback[],\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n try {\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n } catch (err) {\n logger.error('Mutation guard afterSuccess failed', { guardId: callback.guard.id, err })\n getTelemetryRuntime()?.reportError(err, {\n module: 'integrations',\n code: 'integrations.mutation_guard_after_success_failed',\n attributes: { guardId: callback.guard.id },\n })\n }\n }\n}\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,oCAAoC;AAC7C,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAEpC,MAAM,SAAS,aAAa,cAAc;AAQnC,SAAS,oBAAoB,MAAyB;AAC3D,QAAM,WAAY,MAAiC;AACnD,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO,CAAC;AACtC,SAAO,SAAS,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC9E;AAEA,eAAsB,6BACpB,WACA,OACA,gBAA0B,CAAC,GAO1B;AACD,QAAM,YAA6B,CAAC,GAAG,6BAA6B,CAAC;AACrE,QAAM,cAAc,kBAAkB,SAAS;AAC/C,MAAI,YAAa,WAAU,KAAK,WAAW;AAC3C,MAAI,CAAC,UAAU,QAAQ;AACrB,WAAO,EAAE,IAAI,MAAM,uBAAuB,CAAC,EAAE;AAAA,EAC/C;AAEA,MAAI,kBAA4B,CAAC;AACjC,MAAI;AACF,UAAM,cAAc,UAAU,QAAQ,aAAa;AAMnD,sBAAkB,MAAM,aAAa,qBAAqB,MAAM,QAAQ;AAAA,MACtE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,IACxB,CAAC,KAAK,CAAC;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,MAAM,mDAAmD,EAAE,IAAI,CAAC;AACvE,wBAAoB,GAAG,YAAY,KAAK;AAAA,MACtC,QAAQ;AAAA,MACR,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,aAAa,CAAC,CAAC;AAC/E,SAAO,kBAAkB,WAAW,OAAO,EAAE,aAAa,CAAC;AAC7D;AAEA,eAAsB,wCACpB,WACA,OAUe;AACf,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,MAAM,aAAc;AAClC,QAAI;AACF,YAAM,SAAS,MAAM,aAAa;AAAA,QAChC,GAAG;AAAA,QACH,UAAU,SAAS,YAAY;AAAA,MACjC,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,aAAO,MAAM,sCAAsC,EAAE,SAAS,SAAS,MAAM,IAAI,IAAI,CAAC;AACtF,0BAAoB,GAAG,YAAY,KAAK;AAAA,QACtC,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,YAAY,EAAE,SAAS,SAAS,MAAM,GAAG;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -2,25 +2,55 @@ import {
2
2
  bridgeLegacyGuard,
3
3
  runMutationGuards
4
4
  } from "@open-mercato/shared/lib/crud/mutation-guard-registry";
5
+ import { getAllMutationGuardInstances } from "@open-mercato/shared/lib/crud/mutation-guard-store";
6
+ import { createLogger } from "@open-mercato/shared/lib/logger";
7
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
8
+ const logger = createLogger("messages");
5
9
  function resolveUserFeatures(auth) {
6
10
  const features = auth?.features;
7
11
  if (!Array.isArray(features)) return [];
8
12
  return features.filter((value) => typeof value === "string");
9
13
  }
10
- async function runMessageMutationGuards(container, input, userFeatures) {
14
+ async function runMessageMutationGuards(container, input, extraFeatures = []) {
15
+ const allGuards = [...getAllMutationGuardInstances()];
11
16
  const legacyGuard = bridgeLegacyGuard(container);
12
- if (!legacyGuard) {
17
+ if (legacyGuard) allGuards.push(legacyGuard);
18
+ if (!allGuards.length) {
13
19
  return { ok: true, afterSuccessCallbacks: [] };
14
20
  }
15
- return runMutationGuards([legacyGuard], input, { userFeatures });
21
+ let grantedFeatures = [];
22
+ try {
23
+ const rbacService = container.resolve("rbacService");
24
+ grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {
25
+ tenantId: input.tenantId,
26
+ organizationId: input.organizationId
27
+ }) ?? [];
28
+ } catch (err) {
29
+ logger.error("Failed to resolve mutation guard feature grants", { err });
30
+ getTelemetryRuntime()?.reportError(err, {
31
+ module: "messages",
32
+ code: "messages.mutation_guard_feature_resolution_failed"
33
+ });
34
+ }
35
+ const userFeatures = Array.from(/* @__PURE__ */ new Set([...grantedFeatures, ...extraFeatures]));
36
+ return runMutationGuards(allGuards, input, { userFeatures });
16
37
  }
17
38
  async function runMessageMutationGuardAfterSuccess(callbacks, input) {
18
39
  for (const callback of callbacks) {
19
40
  if (!callback.guard.afterSuccess) continue;
20
- await callback.guard.afterSuccess({
21
- ...input,
22
- metadata: callback.metadata ?? null
23
- });
41
+ try {
42
+ await callback.guard.afterSuccess({
43
+ ...input,
44
+ metadata: callback.metadata ?? null
45
+ });
46
+ } catch (err) {
47
+ logger.error("Mutation guard afterSuccess failed", { guardId: callback.guard.id, err });
48
+ getTelemetryRuntime()?.reportError(err, {
49
+ module: "messages",
50
+ code: "messages.mutation_guard_after_success_failed",
51
+ attributes: { guardId: callback.guard.id }
52
+ });
53
+ }
24
54
  }
25
55
  }
26
56
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/api/guards.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\n\ntype GuardAfterCallback = {\n guard: MutationGuard\n metadata: Record<string, unknown> | null\n}\n\nexport function resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nexport async function runMessageMutationGuards(\n container: AwilixContainer,\n input: MutationGuardInput,\n userFeatures: string[],\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: GuardAfterCallback[]\n}> {\n const legacyGuard = bridgeLegacyGuard(container)\n if (!legacyGuard) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards([legacyGuard], input, { userFeatures })\n}\n\nexport async function runMessageMutationGuardAfterSuccess(\n callbacks: GuardAfterCallback[],\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n }\n}\n"],
5
- "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAOA,SAAS,oBAAoB,MAAyB;AAC3D,QAAM,WAAY,MAAiC;AACnD,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO,CAAC;AACtC,SAAO,SAAS,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC9E;AAEA,eAAsB,yBACpB,WACA,OACA,cAOC;AACD,QAAM,cAAc,kBAAkB,SAAS;AAC/C,MAAI,CAAC,aAAa;AAChB,WAAO,EAAE,IAAI,MAAM,uBAAuB,CAAC,EAAE;AAAA,EAC/C;AAEA,SAAO,kBAAkB,CAAC,WAAW,GAAG,OAAO,EAAE,aAAa,CAAC;AACjE;AAEA,eAAsB,oCACpB,WACA,OAUe;AACf,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,MAAM,aAAc;AAClC,UAAM,SAAS,MAAM,aAAa;AAAA,MAChC,GAAG;AAAA,MACH,UAAU,SAAS,YAAY;AAAA,IACjC,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nconst logger = createLogger('messages')\n\ntype GuardAfterCallback = {\n guard: MutationGuard\n metadata: Record<string, unknown> | null\n}\n\n/** @deprecated Mutation guard grants are resolved from RBAC by `runMessageMutationGuards`. */\nexport function resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nexport async function runMessageMutationGuards(\n container: AwilixContainer,\n input: MutationGuardInput,\n extraFeatures: string[] = [],\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: GuardAfterCallback[]\n}> {\n const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]\n const legacyGuard = bridgeLegacyGuard(container)\n if (legacyGuard) allGuards.push(legacyGuard)\n if (!allGuards.length) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n let grantedFeatures: string[] = []\n try {\n const rbacService = container.resolve('rbacService') as {\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n } | undefined\n grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }) ?? []\n } catch (err) {\n logger.error('Failed to resolve mutation guard feature grants', { err })\n getTelemetryRuntime()?.reportError(err, {\n module: 'messages',\n code: 'messages.mutation_guard_feature_resolution_failed',\n })\n }\n\n const userFeatures = Array.from(new Set([...grantedFeatures, ...extraFeatures]))\n return runMutationGuards(allGuards, input, { userFeatures })\n}\n\nexport async function runMessageMutationGuardAfterSuccess(\n callbacks: GuardAfterCallback[],\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n try {\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n } catch (err) {\n logger.error('Mutation guard afterSuccess failed', { guardId: callback.guard.id, err })\n getTelemetryRuntime()?.reportError(err, {\n module: 'messages',\n code: 'messages.mutation_guard_after_success_failed',\n attributes: { guardId: callback.guard.id },\n })\n }\n }\n}\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,oCAAoC;AAC7C,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAEpC,MAAM,SAAS,aAAa,UAAU;AAQ/B,SAAS,oBAAoB,MAAyB;AAC3D,QAAM,WAAY,MAAiC;AACnD,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO,CAAC;AACtC,SAAO,SAAS,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC9E;AAEA,eAAsB,yBACpB,WACA,OACA,gBAA0B,CAAC,GAO1B;AACD,QAAM,YAA6B,CAAC,GAAG,6BAA6B,CAAC;AACrE,QAAM,cAAc,kBAAkB,SAAS;AAC/C,MAAI,YAAa,WAAU,KAAK,WAAW;AAC3C,MAAI,CAAC,UAAU,QAAQ;AACrB,WAAO,EAAE,IAAI,MAAM,uBAAuB,CAAC,EAAE;AAAA,EAC/C;AAEA,MAAI,kBAA4B,CAAC;AACjC,MAAI;AACF,UAAM,cAAc,UAAU,QAAQ,aAAa;AAMnD,sBAAkB,MAAM,aAAa,qBAAqB,MAAM,QAAQ;AAAA,MACtE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,IACxB,CAAC,KAAK,CAAC;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,MAAM,mDAAmD,EAAE,IAAI,CAAC;AACvE,wBAAoB,GAAG,YAAY,KAAK;AAAA,MACtC,QAAQ;AAAA,MACR,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,aAAa,CAAC,CAAC;AAC/E,SAAO,kBAAkB,WAAW,OAAO,EAAE,aAAa,CAAC;AAC7D;AAEA,eAAsB,oCACpB,WACA,OAUe;AACf,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,MAAM,aAAc;AAClC,QAAI;AACF,YAAM,SAAS,MAAM,aAAa;AAAA,QAChC,GAAG;AAAA,QACH,UAAU,SAAS,YAAY;AAAA,MACjC,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,aAAO,MAAM,sCAAsC,EAAE,SAAS,SAAS,MAAM,IAAI,IAAI,CAAC;AACtF,0BAAoB,GAAG,YAAY,KAAK;AAAA,QACtC,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,YAAY,EAAE,SAAS,SAAS,MAAM,GAAG;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -2,25 +2,55 @@ import {
2
2
  bridgeLegacyGuard,
3
3
  runMutationGuards
4
4
  } from "@open-mercato/shared/lib/crud/mutation-guard-registry";
5
+ import { getAllMutationGuardInstances } from "@open-mercato/shared/lib/crud/mutation-guard-store";
6
+ import { createLogger } from "@open-mercato/shared/lib/logger";
7
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
8
+ const logger = createLogger("payment_gateways");
5
9
  function resolveUserFeatures(auth) {
6
10
  const features = auth?.features;
7
11
  if (!Array.isArray(features)) return [];
8
12
  return features.filter((value) => typeof value === "string");
9
13
  }
10
- async function runPaymentGatewayMutationGuards(container, input, userFeatures) {
14
+ async function runPaymentGatewayMutationGuards(container, input, extraFeatures = []) {
15
+ const allGuards = [...getAllMutationGuardInstances()];
11
16
  const legacyGuard = bridgeLegacyGuard(container);
12
- if (!legacyGuard) {
17
+ if (legacyGuard) allGuards.push(legacyGuard);
18
+ if (!allGuards.length) {
13
19
  return { ok: true, afterSuccessCallbacks: [] };
14
20
  }
15
- return runMutationGuards([legacyGuard], input, { userFeatures });
21
+ let grantedFeatures = [];
22
+ try {
23
+ const rbacService = container.resolve("rbacService");
24
+ grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {
25
+ tenantId: input.tenantId,
26
+ organizationId: input.organizationId
27
+ }) ?? [];
28
+ } catch (err) {
29
+ logger.error("Failed to resolve mutation guard feature grants", { err });
30
+ getTelemetryRuntime()?.reportError(err, {
31
+ module: "payment_gateways",
32
+ code: "payment_gateways.mutation_guard_feature_resolution_failed"
33
+ });
34
+ }
35
+ const userFeatures = Array.from(/* @__PURE__ */ new Set([...grantedFeatures, ...extraFeatures]));
36
+ return runMutationGuards(allGuards, input, { userFeatures });
16
37
  }
17
38
  async function runPaymentGatewayMutationGuardAfterSuccess(callbacks, input) {
18
39
  for (const callback of callbacks) {
19
40
  if (!callback.guard.afterSuccess) continue;
20
- await callback.guard.afterSuccess({
21
- ...input,
22
- metadata: callback.metadata ?? null
23
- });
41
+ try {
42
+ await callback.guard.afterSuccess({
43
+ ...input,
44
+ metadata: callback.metadata ?? null
45
+ });
46
+ } catch (err) {
47
+ logger.error("Mutation guard afterSuccess failed", { guardId: callback.guard.id, err });
48
+ getTelemetryRuntime()?.reportError(err, {
49
+ module: "payment_gateways",
50
+ code: "payment_gateways.mutation_guard_after_success_failed",
51
+ attributes: { guardId: callback.guard.id }
52
+ });
53
+ }
24
54
  }
25
55
  }
26
56
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/payment_gateways/api/guards.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\n\ntype GuardAfterCallback = {\n guard: MutationGuard\n metadata: Record<string, unknown> | null\n}\n\nexport function resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nexport async function runPaymentGatewayMutationGuards(\n container: AwilixContainer,\n input: MutationGuardInput,\n userFeatures: string[],\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: GuardAfterCallback[]\n}> {\n const legacyGuard = bridgeLegacyGuard(container)\n if (!legacyGuard) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards([legacyGuard], input, { userFeatures })\n}\n\nexport async function runPaymentGatewayMutationGuardAfterSuccess(\n callbacks: GuardAfterCallback[],\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n }\n}\n"],
5
- "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAOA,SAAS,oBAAoB,MAAyB;AAC3D,QAAM,WAAY,MAAiC;AACnD,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO,CAAC;AACtC,SAAO,SAAS,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC9E;AAEA,eAAsB,gCACpB,WACA,OACA,cAOC;AACD,QAAM,cAAc,kBAAkB,SAAS;AAC/C,MAAI,CAAC,aAAa;AAChB,WAAO,EAAE,IAAI,MAAM,uBAAuB,CAAC,EAAE;AAAA,EAC/C;AAEA,SAAO,kBAAkB,CAAC,WAAW,GAAG,OAAO,EAAE,aAAa,CAAC;AACjE;AAEA,eAAsB,2CACpB,WACA,OAUe;AACf,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,MAAM,aAAc;AAClC,UAAM,SAAS,MAAM,aAAa;AAAA,MAChC,GAAG;AAAA,MACH,UAAU,SAAS,YAAY;AAAA,IACjC,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nconst logger = createLogger('payment_gateways')\n\ntype GuardAfterCallback = {\n guard: MutationGuard\n metadata: Record<string, unknown> | null\n}\n\n/** @deprecated Mutation guard grants are resolved from RBAC by `runPaymentGatewayMutationGuards`. */\nexport function resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nexport async function runPaymentGatewayMutationGuards(\n container: AwilixContainer,\n input: MutationGuardInput,\n extraFeatures: string[] = [],\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: GuardAfterCallback[]\n}> {\n const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]\n const legacyGuard = bridgeLegacyGuard(container)\n if (legacyGuard) allGuards.push(legacyGuard)\n if (!allGuards.length) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n let grantedFeatures: string[] = []\n try {\n const rbacService = container.resolve('rbacService') as {\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n } | undefined\n grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }) ?? []\n } catch (err) {\n logger.error('Failed to resolve mutation guard feature grants', { err })\n getTelemetryRuntime()?.reportError(err, {\n module: 'payment_gateways',\n code: 'payment_gateways.mutation_guard_feature_resolution_failed',\n })\n }\n\n const userFeatures = Array.from(new Set([...grantedFeatures, ...extraFeatures]))\n return runMutationGuards(allGuards, input, { userFeatures })\n}\n\nexport async function runPaymentGatewayMutationGuardAfterSuccess(\n callbacks: GuardAfterCallback[],\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n try {\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n } catch (err) {\n logger.error('Mutation guard afterSuccess failed', { guardId: callback.guard.id, err })\n getTelemetryRuntime()?.reportError(err, {\n module: 'payment_gateways',\n code: 'payment_gateways.mutation_guard_after_success_failed',\n attributes: { guardId: callback.guard.id },\n })\n }\n }\n}\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,oCAAoC;AAC7C,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAEpC,MAAM,SAAS,aAAa,kBAAkB;AAQvC,SAAS,oBAAoB,MAAyB;AAC3D,QAAM,WAAY,MAAiC;AACnD,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO,CAAC;AACtC,SAAO,SAAS,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC9E;AAEA,eAAsB,gCACpB,WACA,OACA,gBAA0B,CAAC,GAO1B;AACD,QAAM,YAA6B,CAAC,GAAG,6BAA6B,CAAC;AACrE,QAAM,cAAc,kBAAkB,SAAS;AAC/C,MAAI,YAAa,WAAU,KAAK,WAAW;AAC3C,MAAI,CAAC,UAAU,QAAQ;AACrB,WAAO,EAAE,IAAI,MAAM,uBAAuB,CAAC,EAAE;AAAA,EAC/C;AAEA,MAAI,kBAA4B,CAAC;AACjC,MAAI;AACF,UAAM,cAAc,UAAU,QAAQ,aAAa;AAMnD,sBAAkB,MAAM,aAAa,qBAAqB,MAAM,QAAQ;AAAA,MACtE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,IACxB,CAAC,KAAK,CAAC;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,MAAM,mDAAmD,EAAE,IAAI,CAAC;AACvE,wBAAoB,GAAG,YAAY,KAAK;AAAA,MACtC,QAAQ;AAAA,MACR,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,aAAa,CAAC,CAAC;AAC/E,SAAO,kBAAkB,WAAW,OAAO,EAAE,aAAa,CAAC;AAC7D;AAEA,eAAsB,2CACpB,WACA,OAUe;AACf,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,MAAM,aAAc;AAClC,QAAI;AACF,YAAM,SAAS,MAAM,aAAa;AAAA,QAChC,GAAG;AAAA,QACH,UAAU,SAAS,YAAY;AAAA,MACjC,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,aAAO,MAAM,sCAAsC,EAAE,SAAS,SAAS,MAAM,IAAI,IAAI,CAAC;AACtF,0BAAoB,GAAG,YAAY,KAAK;AAAA,QACtC,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,YAAY,EAAE,SAAS,SAAS,MAAM,GAAG;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -7,10 +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
- } from "@open-mercato/shared/lib/crud/mutation-guard-registry";
10
+ import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
14
11
  import {
15
12
  customSendSchema,
16
13
  customSendResponseSchema,
@@ -23,16 +20,6 @@ const errorResponseSchema = z.object({ error: z.string() });
23
20
  const metadata = {
24
21
  POST: { requireAuth: true, requireFeatures: ["push_notifications.send_custom"] }
25
22
  };
26
- function resolveUserFeatures(auth) {
27
- const features = auth?.features;
28
- if (!Array.isArray(features)) return [];
29
- return features.filter((value) => typeof value === "string");
30
- }
31
- async function runGuards(container, userFeatures, input) {
32
- const legacyGuard = bridgeLegacyGuard(container);
33
- if (!legacyGuard) return { ok: true, afterSuccessCallbacks: [] };
34
- return runMutationGuards([legacyGuard], input, { userFeatures });
35
- }
36
23
  async function POST(req) {
37
24
  const { translate } = await resolveTranslations();
38
25
  try {
@@ -47,23 +34,23 @@ async function POST(req) {
47
34
  const body = customSendSchema.parse(await readJsonSafe(req, {}));
48
35
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
49
36
  const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth) ?? null;
50
- const guardInput = {
51
- tenantId: auth.tenantId,
52
- organizationId,
53
- userId: auth.sub,
54
- resourceKind: RESOURCE_KIND,
55
- resourceId: body.recipientUserId,
56
- operation: "create",
57
- requestMethod: req.method,
58
- requestHeaders: req.headers,
59
- mutationPayload: body
60
- };
61
- const guardResult = await runGuards(container, resolveUserFeatures(auth), guardInput);
37
+ const guardResult = await runRouteMutationGuards({
38
+ container,
39
+ req,
40
+ auth: {
41
+ tenantId: auth.tenantId,
42
+ organizationId,
43
+ userId: auth.sub
44
+ },
45
+ input: {
46
+ resourceKind: RESOURCE_KIND,
47
+ resourceId: body.recipientUserId,
48
+ operation: "create",
49
+ mutationPayload: body
50
+ }
51
+ });
62
52
  if (!guardResult.ok) {
63
- return NextResponse.json(
64
- guardResult.errorBody ?? { error: translate("push_notifications.errors.send_failed", "Operation blocked") },
65
- { status: guardResult.errorStatus ?? 422 }
66
- );
53
+ return guardResult.response;
67
54
  }
68
55
  const service = container.resolve("pushNotificationService");
69
56
  const result = await service.sendCustomPush({
@@ -78,14 +65,7 @@ async function POST(req) {
78
65
  pushOptions: body.pushOptions,
79
66
  silent: body.silent ?? false
80
67
  });
81
- for (const callback of guardResult.afterSuccessCallbacks) {
82
- if (!callback.guard.afterSuccess) continue;
83
- await callback.guard.afterSuccess({
84
- ...guardInput,
85
- resourceId: body.recipientUserId,
86
- metadata: callback.metadata ?? null
87
- });
88
- }
68
+ await guardResult.runAfterSuccess();
89
69
  const responseBody = result.enqueued === 0 ? {
90
70
  enqueued: 0,
91
71
  warning: CUSTOM_SEND_NO_DEVICES_WARNING,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/push_notifications/api/custom-send/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport type { AwilixContainer } from 'awilix'\nimport {\n customSendSchema,\n customSendResponseSchema,\n CUSTOM_SEND_NO_DEVICES_WARNING,\n} from '../../data/validators'\nimport type { PushNotificationService } from '../../lib/send-custom-push'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\n\nconst logger = createLogger('push_notifications')\n\nconst RESOURCE_KIND = 'push_notifications.push_notification_delivery'\n\nconst errorResponseSchema = z.object({ error: z.string() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['push_notifications.send_custom'] },\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n container: AwilixContainer,\n userFeatures: string[],\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(container)\n if (!legacyGuard) return { ok: true, afterSuccessCallbacks: [] }\n return runMutationGuards([legacyGuard], input, { userFeatures })\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.sub) {\n return NextResponse.json(\n { error: translate('push_notifications.errors.unauthorized', 'Unauthorized') },\n { status: 401 },\n )\n }\n\n const body = customSendSchema.parse(await readJsonSafe(req, {}))\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth) ?? null\n\n // Custom write route \u2192 wire the mutation-guard registry (AGENTS \u2192 API Routes). The send creates\n // append-only delivery rows; map it to a `create` on the delivery resource keyed by recipient.\n const guardInput: MutationGuardInput = {\n tenantId: auth.tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: RESOURCE_KIND,\n resourceId: body.recipientUserId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: body,\n }\n const guardResult = await runGuards(container, resolveUserFeatures(auth), guardInput)\n if (!guardResult.ok) {\n return NextResponse.json(\n guardResult.errorBody ?? { error: translate('push_notifications.errors.send_failed', 'Operation blocked') },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const service = container.resolve('pushNotificationService') as PushNotificationService\n const result = await service.sendCustomPush({\n resolve: (<T = unknown,>(name: string): T => container.resolve(name) as T),\n tenantId: auth.tenantId,\n userId: body.recipientUserId,\n organizationId,\n deviceId: body.deviceId,\n title: body.title,\n body: body.body ?? null,\n data: body.data,\n pushOptions: body.pushOptions,\n silent: body.silent ?? false,\n })\n\n for (const callback of guardResult.afterSuccessCallbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...guardInput,\n resourceId: body.recipientUserId,\n metadata: callback.metadata ?? null,\n })\n }\n\n // A well-formed request that enqueued nothing (no push channel, no in-scope device, or no device\n // whose provider matches an active channel) previously returned a bare 201 \u2014 a silent\n // success-with-no-send that hid, for example, a tenant-level admin targeting org-scoped devices.\n // Surface an explicit machine-readable warning + human message so the caller can react.\n const responseBody: z.infer<typeof customSendResponseSchema> =\n result.enqueued === 0\n ? {\n enqueued: 0,\n warning: CUSTOM_SEND_NO_DEVICES_WARNING,\n message: translate(\n 'push_notifications.warnings.no_matching_devices_in_scope',\n 'No push-capable devices matched this recipient in the selected scope, so nothing was sent.',\n ),\n }\n : { enqueued: result.enqueued }\n\n // 201 Created only when jobs were actually enqueued; the no-op branch returns 200 OK so callers\n // that key off the status code aren't told something was created when nothing was.\n return NextResponse.json(responseBody, { status: result.enqueued === 0 ? 200 : 201 })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('push_notifications.errors.invalid_payload', 'Invalid request'), details: err.flatten() },\n { status: 400 },\n )\n }\n logger.error('push_notifications.custom-send.POST failed', { err })\n return NextResponse.json(\n { error: translate('push_notifications.errors.send_failed', 'Failed to send push notification') },\n { status: 500 },\n )\n }\n}\n\nexport const openApi = {\n POST: {\n summary: 'Send a custom push notification',\n description:\n \"Admin-only: deliver a one-off, free-text visible push to all of a single user's push-capable devices. No in-app notification or email is created.\",\n tags: ['PushNotifications'],\n requestBody: { schema: customSendSchema },\n responses: {\n 200: {\n description:\n 'Nothing was deliverable in scope: `enqueued` is 0 and a `warning` code plus human `message` explain why (no silent no-op). Returned instead of 201 because nothing was created.',\n content: { 'application/json': { schema: customSendResponseSchema } },\n },\n 201: {\n description: 'Per-device push jobs enqueued.',\n content: { 'application/json': { schema: customSendResponseSchema } },\n },\n 400: {\n description: 'Invalid request',\n content: { 'application/json': { schema: errorResponseSchema } },\n },\n 401: {\n description: 'Unauthorized',\n content: { 'application/json': { schema: errorResponseSchema } },\n },\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport {\n customSendSchema,\n customSendResponseSchema,\n CUSTOM_SEND_NO_DEVICES_WARNING,\n} from '../../data/validators'\nimport type { PushNotificationService } from '../../lib/send-custom-push'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\n\nconst logger = createLogger('push_notifications')\n\nconst RESOURCE_KIND = 'push_notifications.push_notification_delivery'\n\nconst errorResponseSchema = z.object({ error: z.string() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['push_notifications.send_custom'] },\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.sub) {\n return NextResponse.json(\n { error: translate('push_notifications.errors.unauthorized', 'Unauthorized') },\n { status: 401 },\n )\n }\n\n const body = customSendSchema.parse(await readJsonSafe(req, {}))\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth) ?? null\n\n // Custom write route \u2192 wire the mutation-guard registry (AGENTS \u2192 API Routes). The send creates\n // append-only delivery rows; map it to a `create` on the delivery resource keyed by recipient.\n const guardResult = await runRouteMutationGuards({\n container,\n req,\n auth: {\n tenantId: auth.tenantId,\n organizationId,\n userId: auth.sub,\n },\n input: {\n resourceKind: RESOURCE_KIND,\n resourceId: body.recipientUserId,\n operation: 'create',\n mutationPayload: body,\n },\n })\n if (!guardResult.ok) {\n return guardResult.response\n }\n\n const service = container.resolve('pushNotificationService') as PushNotificationService\n const result = await service.sendCustomPush({\n resolve: (<T = unknown,>(name: string): T => container.resolve(name) as T),\n tenantId: auth.tenantId,\n userId: body.recipientUserId,\n organizationId,\n deviceId: body.deviceId,\n title: body.title,\n body: body.body ?? null,\n data: body.data,\n pushOptions: body.pushOptions,\n silent: body.silent ?? false,\n })\n\n await guardResult.runAfterSuccess()\n\n // A well-formed request that enqueued nothing (no push channel, no in-scope device, or no device\n // whose provider matches an active channel) previously returned a bare 201 \u2014 a silent\n // success-with-no-send that hid, for example, a tenant-level admin targeting org-scoped devices.\n // Surface an explicit machine-readable warning + human message so the caller can react.\n const responseBody: z.infer<typeof customSendResponseSchema> =\n result.enqueued === 0\n ? {\n enqueued: 0,\n warning: CUSTOM_SEND_NO_DEVICES_WARNING,\n message: translate(\n 'push_notifications.warnings.no_matching_devices_in_scope',\n 'No push-capable devices matched this recipient in the selected scope, so nothing was sent.',\n ),\n }\n : { enqueued: result.enqueued }\n\n // 201 Created only when jobs were actually enqueued; the no-op branch returns 200 OK so callers\n // that key off the status code aren't told something was created when nothing was.\n return NextResponse.json(responseBody, { status: result.enqueued === 0 ? 200 : 201 })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('push_notifications.errors.invalid_payload', 'Invalid request'), details: err.flatten() },\n { status: 400 },\n )\n }\n logger.error('push_notifications.custom-send.POST failed', { err })\n return NextResponse.json(\n { error: translate('push_notifications.errors.send_failed', 'Failed to send push notification') },\n { status: 500 },\n )\n }\n}\n\nexport const openApi = {\n POST: {\n summary: 'Send a custom push notification',\n description:\n \"Admin-only: deliver a one-off, free-text visible push to all of a single user's push-capable devices. No in-app notification or email is created.\",\n tags: ['PushNotifications'],\n requestBody: { schema: customSendSchema },\n responses: {\n 200: {\n description:\n 'Nothing was deliverable in scope: `enqueued` is 0 and a `warning` code plus human `message` explain why (no silent no-op). Returned instead of 201 because nothing was created.',\n content: { 'application/json': { schema: customSendResponseSchema } },\n },\n 201: {\n description: 'Per-device push jobs enqueued.',\n content: { 'application/json': { schema: customSendResponseSchema } },\n },\n 400: {\n description: 'Invalid request',\n content: { 'application/json': { schema: errorResponseSchema } },\n },\n 401: {\n description: 'Unauthorized',\n content: { 'application/json': { schema: errorResponseSchema } },\n },\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,yCAAyC;AAClD,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,uBAAuB;AAEhC,MAAM,SAAS,aAAa,oBAAoB;AAEhD,MAAM,gBAAgB;AAEtB,MAAM,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAEnD,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,gCAAgC,EAAE;AACjF;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC,KAAK,KAAK;AACxC,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,UAAU,0CAA0C,cAAc,EAAE;AAAA,QAC7E,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,OAAO,iBAAiB,MAAM,MAAM,aAAa,KAAK,CAAC,CAAC,CAAC;AAC/D,UAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,UAAM,iBAAiB,kCAAkC,OAAO,IAAI,KAAK;AAIzE,UAAM,cAAc,MAAM,uBAAuB;AAAA,MAC/C;AAAA,MACA;AAAA,MACA,MAAM;AAAA,QACJ,UAAU,KAAK;AAAA,QACf;AAAA,QACA,QAAQ,KAAK;AAAA,MACf;AAAA,MACA,OAAO;AAAA,QACL,cAAc;AAAA,QACd,YAAY,KAAK;AAAA,QACjB,WAAW;AAAA,QACX,iBAAiB;AAAA,MACnB;AAAA,IACF,CAAC;AACD,QAAI,CAAC,YAAY,IAAI;AACnB,aAAO,YAAY;AAAA,IACrB;AAEA,UAAM,UAAU,UAAU,QAAQ,yBAAyB;AAC3D,UAAM,SAAS,MAAM,QAAQ,eAAe;AAAA,MAC1C,UAAU,CAAe,SAAoB,UAAU,QAAQ,IAAI;AAAA,MACnE,UAAU,KAAK;AAAA,MACf,QAAQ,KAAK;AAAA,MACb;AAAA,MACA,UAAU,KAAK;AAAA,MACf,OAAO,KAAK;AAAA,MACZ,MAAM,KAAK,QAAQ;AAAA,MACnB,MAAM,KAAK;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,QAAQ,KAAK,UAAU;AAAA,IACzB,CAAC;AAED,UAAM,YAAY,gBAAgB;AAMlC,UAAM,eACJ,OAAO,aAAa,IAChB;AAAA,MACE,UAAU;AAAA,MACV,SAAS;AAAA,MACT,SAAS;AAAA,QACP;AAAA,QACA;AAAA,MACF;AAAA,IACF,IACA,EAAE,UAAU,OAAO,SAAS;AAIlC,WAAO,aAAa,KAAK,cAAc,EAAE,QAAQ,OAAO,aAAa,IAAI,MAAM,IAAI,CAAC;AAAA,EACtF,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,EAAG,QAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AACnF,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,UAAU,6CAA6C,iBAAiB,GAAG,SAAS,IAAI,QAAQ,EAAE;AAAA,QAC3G,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AACA,WAAO,MAAM,8CAA8C,EAAE,IAAI,CAAC;AAClE,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,yCAAyC,kCAAkC,EAAE;AAAA,MAChG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,aACE;AAAA,IACF,MAAM,CAAC,mBAAmB;AAAA,IAC1B,aAAa,EAAE,QAAQ,iBAAiB;AAAA,IACxC,WAAW;AAAA,MACT,KAAK;AAAA,QACH,aACE;AAAA,QACF,SAAS,EAAE,oBAAoB,EAAE,QAAQ,yBAAyB,EAAE;AAAA,MACtE;AAAA,MACA,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS,EAAE,oBAAoB,EAAE,QAAQ,yBAAyB,EAAE;AAAA,MACtE;AAAA,MACA,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,EAAE;AAAA,MACjE;AAAA,MACA,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,EAAE;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }