@open-mercato/core 0.6.8-develop.6930.1.1e5976efc3 → 0.6.8-develop.6940.1.177ea30c6e

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 (55) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/dbFixtures.js.map +2 -2
  3. package/dist/helpers/integration/ui.js +3 -0
  4. package/dist/helpers/integration/ui.js.map +2 -2
  5. package/dist/modules/auth/lib/setup-app.js +3 -2
  6. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  7. package/dist/modules/catalog/widgets/injection-table.js +5 -4
  8. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  9. package/dist/modules/currencies/di.js +2 -0
  10. package/dist/modules/currencies/di.js.map +2 -2
  11. package/dist/modules/currencies/services/providers/registry.js +31 -0
  12. package/dist/modules/currencies/services/providers/registry.js.map +7 -0
  13. package/dist/modules/directory/api/organization-switcher/route.js +7 -4
  14. package/dist/modules/directory/api/organization-switcher/route.js.map +2 -2
  15. package/dist/modules/entities/api/encryption.js +22 -11
  16. package/dist/modules/entities/api/encryption.js.map +2 -2
  17. package/dist/modules/notifications/api/openapi.js +5 -0
  18. package/dist/modules/notifications/api/openapi.js.map +2 -2
  19. package/dist/modules/notifications/api/route.js +27 -4
  20. package/dist/modules/notifications/api/route.js.map +2 -2
  21. package/dist/modules/notifications/api/settings/route.js +2 -1
  22. package/dist/modules/notifications/api/settings/route.js.map +2 -2
  23. package/dist/modules/notifications/api/unread-count/route.js +4 -2
  24. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  25. package/dist/modules/notifications/lib/routeHelpers.js +25 -0
  26. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  27. package/dist/modules/payment_gateways/lib/descriptor-service.js +12 -1
  28. package/dist/modules/payment_gateways/lib/descriptor-service.js.map +2 -2
  29. package/dist/modules/progress/data/validators.js +2 -1
  30. package/dist/modules/progress/data/validators.js.map +2 -2
  31. package/dist/modules/progress/lib/progressServiceImpl.js +4 -0
  32. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  33. package/dist/modules/sales/commands/payments.js +6 -4
  34. package/dist/modules/sales/commands/payments.js.map +2 -2
  35. package/dist/modules/sales/widgets/injection-table.js +7 -4
  36. package/dist/modules/sales/widgets/injection-table.js.map +2 -2
  37. package/package.json +7 -7
  38. package/src/helpers/integration/dbFixtures.ts +20 -2
  39. package/src/helpers/integration/ui.ts +12 -0
  40. package/src/modules/auth/lib/setup-app.ts +8 -2
  41. package/src/modules/catalog/widgets/injection-table.ts +5 -4
  42. package/src/modules/currencies/di.ts +2 -0
  43. package/src/modules/currencies/services/providers/registry.ts +35 -0
  44. package/src/modules/directory/api/organization-switcher/route.ts +16 -4
  45. package/src/modules/entities/api/encryption.ts +22 -12
  46. package/src/modules/notifications/api/openapi.ts +5 -0
  47. package/src/modules/notifications/api/route.ts +31 -2
  48. package/src/modules/notifications/api/settings/route.ts +8 -1
  49. package/src/modules/notifications/api/unread-count/route.ts +4 -2
  50. package/src/modules/notifications/lib/routeHelpers.ts +60 -0
  51. package/src/modules/payment_gateways/lib/descriptor-service.ts +12 -0
  52. package/src/modules/progress/data/validators.ts +1 -0
  53. package/src/modules/progress/lib/progressServiceImpl.ts +4 -0
  54. package/src/modules/sales/commands/payments.ts +8 -4
  55. package/src/modules/sales/widgets/injection-table.ts +7 -4
@@ -10,17 +10,12 @@ import {
10
10
  runCrudMutationGuardAfterSuccess,
11
11
  validateCrudMutationGuard
12
12
  } from "@open-mercato/shared/lib/crud/mutation-guard";
13
+ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
13
14
  const ENCRYPTION_MAP_RESOURCE_KIND = "entities.encryption_map";
14
15
  const metadata = {
15
16
  GET: { requireAuth: true, requireFeatures: ["entities.definitions.manage"] },
16
17
  POST: { requireAuth: true, requireFeatures: ["entities.definitions.manage"] }
17
18
  };
18
- function resolveScope(auth) {
19
- return {
20
- tenantId: auth.tenantId ?? null,
21
- organizationId: auth.orgId ?? null
22
- };
23
- }
24
19
  function toIsoOrNull(value) {
25
20
  if (value == null) return null;
26
21
  if (value instanceof Date) {
@@ -36,8 +31,10 @@ async function GET(req) {
36
31
  if (!entityId) return NextResponse.json({ error: "entityId is required" }, { status: 400 });
37
32
  const auth = await getAuthFromRequest(req);
38
33
  if (!auth?.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
39
- const { tenantId, organizationId } = resolveScope(auth);
40
34
  const container = await createRequestContainer();
35
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
36
+ const tenantId = scope.tenantId ?? auth.tenantId;
37
+ const organizationId = scope.selectedId;
41
38
  const em = container.resolve("em");
42
39
  const repo = em.getRepository(EncryptionMap);
43
40
  const candidates = [
@@ -71,11 +68,20 @@ async function POST(req) {
71
68
  }
72
69
  const auth = await getAuthFromRequest(req);
73
70
  if (!auth?.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
74
- const scope = resolveScope(auth);
75
71
  const payload = parsed.data;
76
- const tenantId = auth.tenantId;
77
- const organizationId = scope.organizationId;
78
72
  const container = await createRequestContainer();
73
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
74
+ if (scope.selectionRejected) {
75
+ return NextResponse.json(
76
+ {
77
+ error: "Your selected organization is no longer available. Please re-select an organization and try again.",
78
+ code: "organization_selection_invalid"
79
+ },
80
+ { status: 422 }
81
+ );
82
+ }
83
+ const tenantId = scope.tenantId ?? auth.tenantId;
84
+ const organizationId = scope.selectedId;
79
85
  const em = container.resolve("em");
80
86
  const repo = em.getRepository(EncryptionMap);
81
87
  const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null });
@@ -151,6 +157,10 @@ const conflictResponseSchema = z.object({
151
157
  currentUpdatedAt: z.string(),
152
158
  expectedUpdatedAt: z.string()
153
159
  });
160
+ const organizationSelectionInvalidResponseSchema = z.object({
161
+ error: z.string(),
162
+ code: z.literal("organization_selection_invalid")
163
+ });
154
164
  const openApi = {
155
165
  tag: "Entities",
156
166
  summary: "Manage encryption maps",
@@ -167,7 +177,8 @@ const openApi = {
167
177
  requestBody: { contentType: "application/json", schema: upsertEncryptionMapSchema },
168
178
  responses: [
169
179
  { status: 200, description: "Saved", schema: z.object({ ok: z.boolean(), updatedAt: z.string().nullable().optional() }) },
170
- { status: 409, description: "Optimistic-lock conflict (stale write)", schema: conflictResponseSchema }
180
+ { status: 409, description: "Optimistic-lock conflict (stale write)", schema: conflictResponseSchema },
181
+ { status: 422, description: "Selected organization is unavailable", schema: organizationSelectionInvalidResponseSchema }
171
182
  ]
172
183
  }
173
184
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/api/encryption.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 { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\nimport { upsertEncryptionMapSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst ENCRYPTION_MAP_RESOURCE_KIND = 'entities.encryption_map'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nfunction resolveScope(auth: { tenantId?: string | null; orgId?: string | null }) {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n\nfunction toIsoOrNull(value: Date | string | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n return trimmed.length ? trimmed : null\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const { tenantId, organizationId } = resolveScope(auth)\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n // Prefer tenant+org, then tenant-global, then global\n const candidates = [\n { entityId, tenantId, organizationId },\n { entityId, tenantId, organizationId: null },\n { entityId, tenantId: null, organizationId: null },\n ]\n let record: any = null\n for (const where of candidates) {\n // eslint-disable-next-line no-await-in-loop\n const found = await repo.findOne({ ...where, deletedAt: null })\n if (found) {\n record = found\n break\n }\n }\n\n return NextResponse.json({\n entityId,\n tenantId,\n organizationId,\n fields: record?.fieldsJson ?? [],\n isActive: record?.isActive ?? true,\n updatedAt: toIsoOrNull(record?.updatedAt),\n })\n}\n\nexport async function POST(req: Request) {\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = upsertEncryptionMapSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const scope = resolveScope(auth)\n const payload = parsed.data\n const tenantId: string = auth.tenantId\n const organizationId = scope.organizationId\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null })\n\n // Reject stale writes: a save started from an older tab must not silently\n // overwrite a newer encryption configuration. No-op when the client did not\n // send the expected-version header (strictly additive).\n if (existing) {\n enforceCommandOptimisticLock({\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: existing.id,\n current: existing.updatedAt,\n request: req,\n })\n }\n\n // Mutation-guard contract for custom write routes. The resource is the\n // encryption map for this entity scoped to the tenant/organization.\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: existing?.id ?? payload.entityId,\n operation: existing ? 'update' : 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let saved: any\n if (existing) {\n existing.fieldsJson = payload.fields\n existing.isActive = payload.isActive ?? true\n existing.updatedAt = new Date()\n await em.persist(existing).flush()\n saved = existing\n } else {\n const map = repo.create({\n entityId: payload.entityId,\n tenantId,\n organizationId,\n fieldsJson: payload.fields,\n isActive: payload.isActive ?? true,\n })\n await em.persist(map).flush()\n saved = map\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: saved?.id ?? payload.entityId,\n operation: existing ? 'update' : 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n try {\n const svc = container.resolve('tenantEncryptionService') as { invalidateMap?: (e: string, t: string | null, o: string | null) => Promise<void> }\n await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId)\n } catch {\n // best-effort cache bust\n }\n\n return NextResponse.json({ ok: true, updatedAt: toIsoOrNull(saved?.updatedAt) })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n}\n\nconst conflictResponseSchema = z.object({\n error: z.string(),\n code: z.string(),\n currentUpdatedAt: z.string(),\n expectedUpdatedAt: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage encryption maps',\n methods: {\n GET: {\n summary: 'Fetch encryption map',\n description: 'Returns the encrypted field map for the current tenant/organization scope.',\n query: z.object({ entityId: z.string() }),\n responses: [{ status: 200, description: 'Map', schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional(), updatedAt: z.string().nullable().optional() }) }],\n },\n POST: {\n summary: 'Upsert encryption map',\n description: 'Creates or updates the encryption map for the current tenant/organization scope. Enforces optimistic locking when the caller sends the expected version header.',\n requestBody: { contentType: 'application/json', schema: upsertEncryptionMapSchema },\n responses: [\n { status: 200, description: 'Saved', schema: z.object({ ok: z.boolean(), updatedAt: z.string().nullable().optional() }) },\n { status: 409, description: 'Optimistic-lock conflict (stale write)', schema: conflictResponseSchema },\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 { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\nimport { upsertEncryptionMapSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nconst ENCRYPTION_MAP_RESOURCE_KIND = 'entities.encryption_map'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nfunction toIsoOrNull(value: Date | string | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n return trimmed.length ? trimmed : null\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const tenantId = scope.tenantId ?? auth.tenantId\n const organizationId = scope.selectedId\n const em = container.resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n // Prefer tenant+org, then tenant-global, then global\n const candidates = [\n { entityId, tenantId, organizationId },\n { entityId, tenantId, organizationId: null },\n { entityId, tenantId: null, organizationId: null },\n ]\n let record: any = null\n for (const where of candidates) {\n const found = await repo.findOne({ ...where, deletedAt: null })\n if (found) {\n record = found\n break\n }\n }\n\n return NextResponse.json({\n entityId,\n tenantId,\n organizationId,\n fields: record?.fieldsJson ?? [],\n isActive: record?.isActive ?? true,\n updatedAt: toIsoOrNull(record?.updatedAt),\n })\n}\n\nexport async function POST(req: Request) {\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = upsertEncryptionMapSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const payload = parsed.data\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (scope.selectionRejected) {\n return NextResponse.json(\n {\n error: 'Your selected organization is no longer available. Please re-select an organization and try again.',\n code: 'organization_selection_invalid',\n },\n { status: 422 },\n )\n }\n const tenantId = scope.tenantId ?? auth.tenantId\n const organizationId = scope.selectedId\n const em = container.resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null })\n\n // Reject stale writes: a save started from an older tab must not silently\n // overwrite a newer encryption configuration. No-op when the client did not\n // send the expected-version header (strictly additive).\n if (existing) {\n enforceCommandOptimisticLock({\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: existing.id,\n current: existing.updatedAt,\n request: req,\n })\n }\n\n // Mutation-guard contract for custom write routes. The resource is the\n // encryption map for this entity scoped to the tenant/organization.\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: existing?.id ?? payload.entityId,\n operation: existing ? 'update' : 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let saved: any\n if (existing) {\n existing.fieldsJson = payload.fields\n existing.isActive = payload.isActive ?? true\n existing.updatedAt = new Date()\n await em.persist(existing).flush()\n saved = existing\n } else {\n const map = repo.create({\n entityId: payload.entityId,\n tenantId,\n organizationId,\n fieldsJson: payload.fields,\n isActive: payload.isActive ?? true,\n })\n await em.persist(map).flush()\n saved = map\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: saved?.id ?? payload.entityId,\n operation: existing ? 'update' : 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n try {\n const svc = container.resolve('tenantEncryptionService') as { invalidateMap?: (e: string, t: string | null, o: string | null) => Promise<void> }\n await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId)\n } catch {\n // best-effort cache bust\n }\n\n return NextResponse.json({ ok: true, updatedAt: toIsoOrNull(saved?.updatedAt) })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n}\n\nconst conflictResponseSchema = z.object({\n error: z.string(),\n code: z.string(),\n currentUpdatedAt: z.string(),\n expectedUpdatedAt: z.string(),\n})\n\nconst organizationSelectionInvalidResponseSchema = z.object({\n error: z.string(),\n code: z.literal('organization_selection_invalid'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage encryption maps',\n methods: {\n GET: {\n summary: 'Fetch encryption map',\n description: 'Returns the encrypted field map for the current tenant/organization scope.',\n query: z.object({ entityId: z.string() }),\n responses: [{ status: 200, description: 'Map', schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional(), updatedAt: z.string().nullable().optional() }) }],\n },\n POST: {\n summary: 'Upsert encryption map',\n description: 'Creates or updates the encryption map for the current tenant/organization scope. Enforces optimistic locking when the caller sends the expected version header.',\n requestBody: { contentType: 'application/json', schema: upsertEncryptionMapSchema },\n responses: [\n { status: 200, description: 'Saved', schema: z.object({ ok: z.boolean(), updatedAt: z.string().nullable().optional() }) },\n { status: 409, description: 'Optimistic-lock conflict (stale write)', schema: conflictResponseSchema },\n { status: 422, description: 'Selected organization is unavailable', schema: organizationSelectionInvalidResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -43,6 +43,10 @@ const okResponseSchema = z.object({
43
43
  const errorResponseSchema = z.object({
44
44
  error: z.string()
45
45
  });
46
+ const scopeErrorResponseSchema = z.object({
47
+ error: z.string(),
48
+ code: z.string()
49
+ });
46
50
  const unreadCountResponseSchema = z.object({
47
51
  unreadCount: z.number()
48
52
  });
@@ -71,6 +75,7 @@ export {
71
75
  notificationSettingsResponseSchema,
72
76
  notificationSettingsUpdateResponseSchema,
73
77
  okResponseSchema,
78
+ scopeErrorResponseSchema,
74
79
  unreadCountResponseSchema
75
80
  };
76
81
  //# sourceMappingURL=openapi.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/api/openapi.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { createCrudOpenApiFactory, createPagedListResponseSchema } from '@open-mercato/shared/lib/openapi/crud'\nimport {\n listNotificationsSchema,\n createNotificationSchema,\n executeActionSchema,\n notificationDeliveryConfigSchema,\n} from '../data/validators'\n\nexport const buildNotificationsCrudOpenApi = createCrudOpenApiFactory({\n defaultTag: 'Notifications',\n})\n\nexport const notificationItemSchema = z.object({\n id: z.string().uuid(),\n type: z.string(),\n title: z.string(),\n body: z.string().nullable().optional(),\n titleKey: z.string().nullable().optional(),\n bodyKey: z.string().nullable().optional(),\n titleVariables: z.record(z.string(), z.string()).nullable().optional(),\n bodyVariables: z.record(z.string(), z.string()).nullable().optional(),\n icon: z.string().nullable().optional(),\n severity: z.string(),\n status: z.string(),\n actions: z.array(z.object({\n id: z.string(),\n label: z.string(),\n labelKey: z.string().optional(),\n variant: z.string().optional(),\n icon: z.string().optional(),\n })),\n primaryActionId: z.string().optional(),\n sourceModule: z.string().nullable().optional(),\n sourceEntityType: z.string().nullable().optional(),\n sourceEntityId: z.string().uuid().nullable().optional(),\n linkHref: z.string().nullable().optional(),\n createdAt: z.string(),\n readAt: z.string().nullable().optional(),\n actionTaken: z.string().nullable().optional(),\n})\n\nexport const okResponseSchema = z.object({\n ok: z.boolean(),\n})\n\nexport const errorResponseSchema = z.object({\n error: z.string(),\n})\n\nexport const unreadCountResponseSchema = z.object({\n unreadCount: z.number(),\n})\n\nexport const actionResultResponseSchema = z.object({\n ok: z.boolean(),\n result: z.unknown().optional(),\n href: z.string().optional(),\n})\n\nexport const notificationSettingsResponseSchema = z.object({\n settings: notificationDeliveryConfigSchema,\n})\n\nexport const notificationSettingsUpdateResponseSchema = z.object({\n ok: z.boolean(),\n settings: notificationDeliveryConfigSchema,\n})\n\nexport {\n createPagedListResponseSchema,\n listNotificationsSchema,\n createNotificationSchema,\n executeActionSchema,\n notificationDeliveryConfigSchema,\n}\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,0BAA0B,qCAAqC;AACxE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,gCAAgC,yBAAyB;AAAA,EACpE,YAAY;AACd,CAAC;AAEM,MAAM,yBAAyB,EAAE,OAAO;AAAA,EAC7C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO;AAAA,EACf,OAAO,EAAE,OAAO;AAAA,EAChB,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACrC,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACxC,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACrE,eAAe,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACpE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACrC,UAAU,EAAE,OAAO;AAAA,EACnB,QAAQ,EAAE,OAAO;AAAA,EACjB,SAAS,EAAE,MAAM,EAAE,OAAO;AAAA,IACxB,IAAI,EAAE,OAAO;AAAA,IACb,OAAO,EAAE,OAAO;AAAA,IAChB,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,IAC9B,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,IAC7B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,CAAC,CAAC;AAAA,EACF,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC7C,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzC,WAAW,EAAE,OAAO;AAAA,EACpB,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACvC,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC9C,CAAC;AAEM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,IAAI,EAAE,QAAQ;AAChB,CAAC;AAEM,MAAM,sBAAsB,EAAE,OAAO;AAAA,EAC1C,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,aAAa,EAAE,OAAO;AACxB,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,IAAI,EAAE,QAAQ;AAAA,EACd,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC7B,MAAM,EAAE,OAAO,EAAE,SAAS;AAC5B,CAAC;AAEM,MAAM,qCAAqC,EAAE,OAAO;AAAA,EACzD,UAAU;AACZ,CAAC;AAEM,MAAM,2CAA2C,EAAE,OAAO;AAAA,EAC/D,IAAI,EAAE,QAAQ;AAAA,EACd,UAAU;AACZ,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { createCrudOpenApiFactory, createPagedListResponseSchema } from '@open-mercato/shared/lib/openapi/crud'\nimport {\n listNotificationsSchema,\n createNotificationSchema,\n executeActionSchema,\n notificationDeliveryConfigSchema,\n} from '../data/validators'\n\nexport const buildNotificationsCrudOpenApi = createCrudOpenApiFactory({\n defaultTag: 'Notifications',\n})\n\nexport const notificationItemSchema = z.object({\n id: z.string().uuid(),\n type: z.string(),\n title: z.string(),\n body: z.string().nullable().optional(),\n titleKey: z.string().nullable().optional(),\n bodyKey: z.string().nullable().optional(),\n titleVariables: z.record(z.string(), z.string()).nullable().optional(),\n bodyVariables: z.record(z.string(), z.string()).nullable().optional(),\n icon: z.string().nullable().optional(),\n severity: z.string(),\n status: z.string(),\n actions: z.array(z.object({\n id: z.string(),\n label: z.string(),\n labelKey: z.string().optional(),\n variant: z.string().optional(),\n icon: z.string().optional(),\n })),\n primaryActionId: z.string().optional(),\n sourceModule: z.string().nullable().optional(),\n sourceEntityType: z.string().nullable().optional(),\n sourceEntityId: z.string().uuid().nullable().optional(),\n linkHref: z.string().nullable().optional(),\n createdAt: z.string(),\n readAt: z.string().nullable().optional(),\n actionTaken: z.string().nullable().optional(),\n})\n\nexport const okResponseSchema = z.object({\n ok: z.boolean(),\n})\n\nexport const errorResponseSchema = z.object({\n error: z.string(),\n})\n\nexport const scopeErrorResponseSchema = z.object({\n error: z.string(),\n code: z.string(),\n})\n\nexport const unreadCountResponseSchema = z.object({\n unreadCount: z.number(),\n})\n\nexport const actionResultResponseSchema = z.object({\n ok: z.boolean(),\n result: z.unknown().optional(),\n href: z.string().optional(),\n})\n\nexport const notificationSettingsResponseSchema = z.object({\n settings: notificationDeliveryConfigSchema,\n})\n\nexport const notificationSettingsUpdateResponseSchema = z.object({\n ok: z.boolean(),\n settings: notificationDeliveryConfigSchema,\n})\n\nexport {\n createPagedListResponseSchema,\n listNotificationsSchema,\n createNotificationSchema,\n executeActionSchema,\n notificationDeliveryConfigSchema,\n}\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,0BAA0B,qCAAqC;AACxE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,gCAAgC,yBAAyB;AAAA,EACpE,YAAY;AACd,CAAC;AAEM,MAAM,yBAAyB,EAAE,OAAO;AAAA,EAC7C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO;AAAA,EACf,OAAO,EAAE,OAAO;AAAA,EAChB,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACrC,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACxC,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACrE,eAAe,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACpE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACrC,UAAU,EAAE,OAAO;AAAA,EACnB,QAAQ,EAAE,OAAO;AAAA,EACjB,SAAS,EAAE,MAAM,EAAE,OAAO;AAAA,IACxB,IAAI,EAAE,OAAO;AAAA,IACb,OAAO,EAAE,OAAO;AAAA,IAChB,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,IAC9B,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,IAC7B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,CAAC,CAAC;AAAA,EACF,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC7C,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzC,WAAW,EAAE,OAAO;AAAA,EACpB,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACvC,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC9C,CAAC;AAEM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,IAAI,EAAE,QAAQ;AAChB,CAAC;AAEM,MAAM,sBAAsB,EAAE,OAAO;AAAA,EAC1C,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,2BAA2B,EAAE,OAAO;AAAA,EAC/C,OAAO,EAAE,OAAO;AAAA,EAChB,MAAM,EAAE,OAAO;AACjB,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,aAAa,EAAE,OAAO;AACxB,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,IAAI,EAAE,QAAQ;AAAA,EACd,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC7B,MAAM,EAAE,OAAO,EAAE,SAAS;AAC5B,CAAC;AAEM,MAAM,qCAAqC,EAAE,OAAO;AAAA,EACzD,UAAU;AACZ,CAAC;AAEM,MAAM,2CAA2C,EAAE,OAAO;AAAA,EAC/D,IAAI,EAAE,QAAQ;AAAA,EACd,UAAU;AACZ,CAAC;",
6
6
  "names": []
7
7
  }
@@ -18,13 +18,16 @@ import {
18
18
  NOTIFICATION_RESOURCE_KIND,
19
19
  notificationCrudErrorResponse,
20
20
  notificationValidationErrorResponse,
21
+ resolveGuardedNotificationContext,
21
22
  resolveNotificationContext,
22
- runGuardedNotificationWrite
23
+ runGuardedNotificationWrite,
24
+ TENANT_SCOPE_REQUIRED_ERROR_CODE
23
25
  } from "../lib/routeHelpers.js";
24
26
  import {
25
27
  buildNotificationsCrudOpenApi,
26
28
  createPagedListResponseSchema,
27
- notificationItemSchema
29
+ notificationItemSchema,
30
+ scopeErrorResponseSchema
28
31
  } from "./openapi.js";
29
32
  const metadata = {
30
33
  GET: { requireAuth: true },
@@ -56,7 +59,9 @@ function isNotificationsListPayload(value) {
56
59
  return Array.isArray(payload.items) && typeof payload.total === "number" && typeof payload.page === "number" && typeof payload.pageSize === "number" && typeof payload.totalPages === "number";
57
60
  }
58
61
  async function GET(req) {
59
- const { ctx, scope } = await resolveNotificationContext(req);
62
+ const resolved = await resolveGuardedNotificationContext(req);
63
+ if (!resolved.ok) return resolved.response;
64
+ const { ctx, scope } = resolved;
60
65
  const em = ctx.container.resolve("em");
61
66
  const url = new URL(req.url);
62
67
  const queryParams = Object.fromEntries(url.searchParams.entries());
@@ -170,7 +175,7 @@ async function POST(req) {
170
175
  throw error;
171
176
  }
172
177
  }
173
- const openApi = buildNotificationsCrudOpenApi({
178
+ const notificationsCrudOpenApi = buildNotificationsCrudOpenApi({
174
179
  resourceName: "Notification",
175
180
  querySchema: listNotificationsSchema,
176
181
  listResponseSchema: createPagedListResponseSchema(notificationItemSchema),
@@ -180,6 +185,24 @@ const openApi = buildNotificationsCrudOpenApi({
180
185
  description: "Creates a notification for a user."
181
186
  }
182
187
  });
188
+ const notificationsCrudGet = notificationsCrudOpenApi.methods?.GET ?? {};
189
+ const openApi = {
190
+ ...notificationsCrudOpenApi,
191
+ methods: {
192
+ ...notificationsCrudOpenApi.methods,
193
+ GET: {
194
+ ...notificationsCrudGet,
195
+ errors: [
196
+ ...notificationsCrudGet.errors ?? [],
197
+ {
198
+ status: 403,
199
+ description: `Request could not be resolved to a tenant scope (code: ${TENANT_SCOPE_REQUIRED_ERROR_CODE})`,
200
+ schema: scopeErrorResponseSchema
201
+ }
202
+ ]
203
+ }
204
+ }
205
+ };
183
206
  export {
184
207
  GET,
185
208
  POST,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/api/route.ts"],
4
- "sourcesContent": ["import { createHash } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n debugCrudCache,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { Notification } from '../data/entities'\nimport { listNotificationsSchema, createNotificationSchema } from '../data/validators'\nimport { toNotificationDto } from '../lib/notificationMapper'\nimport {\n buildNotificationReadScopeWhere,\n getNotificationReadScopeTagOrganizationIds,\n} from '../lib/notificationScope'\nimport {\n NOTIFICATION_RESOURCE_KIND,\n notificationCrudErrorResponse,\n notificationValidationErrorResponse,\n resolveNotificationContext,\n runGuardedNotificationWrite,\n} from '../lib/routeHelpers'\nimport {\n buildNotificationsCrudOpenApi,\n createPagedListResponseSchema,\n notificationItemSchema,\n} from './openapi'\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['notifications.create'] },\n}\n\nconst NOTIFICATIONS_LIST_TTL_MS = 10_000\n// v2: the key gained the selected-organization scope (#4840). Bumping the version\n// retires v1 entries, which were shared across organizations for the same user.\nconst NOTIFICATIONS_LIST_CACHE_VERSION = 2\nconst NOTIFICATIONS_LIST_RESOURCE = 'notifications.notification'\n\ntype NotificationsListPayload = {\n items: ReturnType<typeof toNotificationDto>[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nexport function buildNotificationsListCacheKey(\n userId: string,\n organizationScope: { organizationId: string | null; organizationIds: string[] },\n input: z.infer<typeof listNotificationsSchema>,\n): string {\n const normalizedIds = Array.from(new Set(\n organizationScope.organizationIds.filter((value) => value.trim().length > 0),\n )).sort((left, right) => left.localeCompare(right))\n const scopeKey = normalizedIds.length === 0\n ? 'no-access'\n : `${organizationScope.organizationId ?? 'none'}:scope=${createHash('sha256')\n .update(normalizedIds.join('\\0'))\n .digest('hex')\n .slice(0, 16)}`\n const filterSignature = JSON.stringify({\n status: Array.isArray(input.status)\n ? [...input.status].sort((left, right) => left.localeCompare(right))\n : input.status ?? null,\n type: input.type ?? null,\n severity: input.severity ?? null,\n sourceEntityType: input.sourceEntityType ?? null,\n sourceEntityId: input.sourceEntityId ?? null,\n since: input.since ?? null,\n page: input.page,\n pageSize: input.pageSize,\n })\n return `notifications:list:v${NOTIFICATIONS_LIST_CACHE_VERSION}:u=${userId}:org=${scopeKey}:filters=${filterSignature}`\n}\n\nfunction isNotificationsListPayload(value: unknown): value is NotificationsListPayload {\n if (!value || typeof value !== 'object') return false\n const payload = value as Partial<NotificationsListPayload>\n return (\n Array.isArray(payload.items)\n && typeof payload.total === 'number'\n && typeof payload.page === 'number'\n && typeof payload.pageSize === 'number'\n && typeof payload.totalPages === 'number'\n )\n}\n\nexport async function GET(req: Request) {\n const { ctx, scope } = await resolveNotificationContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n\n const url = new URL(req.url)\n const queryParams = Object.fromEntries(url.searchParams.entries())\n const input = listNotificationsSchema.parse(queryParams)\n const userId = scope.userId\n // Mirrors api/unread-count/route.ts: unrestricted and omitted legacy organization\n // scopes cannot be safely tagged for invalidation, because organization-specific\n // writes only invalidate their own collection tag. Leave those scopes uncached\n // rather than serving a stale tenant-wide list until the TTL expires.\n const cacheableOrganizationIds = Array.isArray(scope.organizationIds)\n ? scope.organizationIds\n : null\n const cache = userId && cacheableOrganizationIds && isCrudCacheEnabled()\n ? resolveCrudCache(ctx.container)\n : null\n const cacheKey = cache && userId && cacheableOrganizationIds\n ? buildNotificationsListCacheKey(\n userId,\n { organizationId: scope.organizationId, organizationIds: cacheableOrganizationIds },\n input,\n )\n : null\n\n if (cache && cacheKey) {\n try {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n if (isNotificationsListPayload(cached)) {\n return Response.json(cached)\n }\n } catch (error) {\n debugCrudCache('notifications-list-cache-read-failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n const filters: Record<string, unknown> = {\n recipientUserId: userId,\n tenantId: scope.tenantId,\n ...buildNotificationReadScopeWhere(scope),\n }\n\n if (input.status) {\n filters.status = Array.isArray(input.status) ? { $in: input.status } : input.status\n } else {\n filters.status = { $ne: 'dismissed' }\n }\n if (input.type) {\n filters.type = input.type\n }\n if (input.severity) {\n filters.severity = input.severity\n }\n if (input.sourceEntityType) {\n filters.sourceEntityType = input.sourceEntityType\n }\n if (input.sourceEntityId) {\n filters.sourceEntityId = input.sourceEntityId\n }\n if (input.since) {\n filters.createdAt = { $gt: new Date(input.since) }\n }\n\n const [notifications, total] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: input.pageSize,\n offset: (input.page - 1) * input.pageSize,\n }),\n em.count(Notification, filters),\n ])\n\n const items = notifications.map(toNotificationDto)\n\n const payload: NotificationsListPayload = {\n items,\n total,\n page: input.page,\n pageSize: input.pageSize,\n totalPages: Math.ceil(total / input.pageSize),\n }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(scope.tenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: NOTIFICATIONS_LIST_TTL_MS,\n tags: buildCollectionTags(\n NOTIFICATIONS_LIST_RESOURCE,\n scope.tenantId,\n getNotificationReadScopeTagOrganizationIds(scope),\n ),\n }),\n )\n } catch (error) {\n debugCrudCache('notifications-list-cache-write-failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n return Response.json(payload)\n}\n\nexport async function POST(req: Request) {\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n const body = await req.json().catch(() => ({}))\n const parsed = createNotificationSchema.safeParse(body)\n if (!parsed.success) {\n return notificationValidationErrorResponse(parsed.error)\n }\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n operation: 'create',\n payload: parsed.data as Record<string, unknown>,\n },\n () => service.create(parsed.data, scope),\n )\n if (!guarded.ok) return guarded.response\n\n return Response.json({ id: guarded.result.id }, { status: 201 })\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n}\n\nexport const openApi = buildNotificationsCrudOpenApi({\n resourceName: 'Notification',\n querySchema: listNotificationsSchema,\n listResponseSchema: createPagedListResponseSchema(notificationItemSchema),\n create: {\n schema: createNotificationSchema,\n responseSchema: z.object({ id: z.string().uuid() }),\n description: 'Creates a notification for a user.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n debugCrudCache,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { Notification } from '../data/entities'\nimport { listNotificationsSchema, createNotificationSchema } from '../data/validators'\nimport { toNotificationDto } from '../lib/notificationMapper'\nimport {\n buildNotificationReadScopeWhere,\n getNotificationReadScopeTagOrganizationIds,\n} from '../lib/notificationScope'\nimport {\n NOTIFICATION_RESOURCE_KIND,\n notificationCrudErrorResponse,\n notificationValidationErrorResponse,\n resolveGuardedNotificationContext,\n resolveNotificationContext,\n runGuardedNotificationWrite,\n TENANT_SCOPE_REQUIRED_ERROR_CODE,\n} from '../lib/routeHelpers'\nimport {\n buildNotificationsCrudOpenApi,\n createPagedListResponseSchema,\n notificationItemSchema,\n scopeErrorResponseSchema,\n} from './openapi'\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['notifications.create'] },\n}\n\nconst NOTIFICATIONS_LIST_TTL_MS = 10_000\n// v2: the key gained the selected-organization scope (#4840). Bumping the version\n// retires v1 entries, which were shared across organizations for the same user.\nconst NOTIFICATIONS_LIST_CACHE_VERSION = 2\nconst NOTIFICATIONS_LIST_RESOURCE = 'notifications.notification'\n\ntype NotificationsListPayload = {\n items: ReturnType<typeof toNotificationDto>[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nexport function buildNotificationsListCacheKey(\n userId: string,\n organizationScope: { organizationId: string | null; organizationIds: string[] },\n input: z.infer<typeof listNotificationsSchema>,\n): string {\n const normalizedIds = Array.from(new Set(\n organizationScope.organizationIds.filter((value) => value.trim().length > 0),\n )).sort((left, right) => left.localeCompare(right))\n const scopeKey = normalizedIds.length === 0\n ? 'no-access'\n : `${organizationScope.organizationId ?? 'none'}:scope=${createHash('sha256')\n .update(normalizedIds.join('\\0'))\n .digest('hex')\n .slice(0, 16)}`\n const filterSignature = JSON.stringify({\n status: Array.isArray(input.status)\n ? [...input.status].sort((left, right) => left.localeCompare(right))\n : input.status ?? null,\n type: input.type ?? null,\n severity: input.severity ?? null,\n sourceEntityType: input.sourceEntityType ?? null,\n sourceEntityId: input.sourceEntityId ?? null,\n since: input.since ?? null,\n page: input.page,\n pageSize: input.pageSize,\n })\n return `notifications:list:v${NOTIFICATIONS_LIST_CACHE_VERSION}:u=${userId}:org=${scopeKey}:filters=${filterSignature}`\n}\n\nfunction isNotificationsListPayload(value: unknown): value is NotificationsListPayload {\n if (!value || typeof value !== 'object') return false\n const payload = value as Partial<NotificationsListPayload>\n return (\n Array.isArray(payload.items)\n && typeof payload.total === 'number'\n && typeof payload.page === 'number'\n && typeof payload.pageSize === 'number'\n && typeof payload.totalPages === 'number'\n )\n}\n\nexport async function GET(req: Request) {\n const resolved = await resolveGuardedNotificationContext(req)\n if (!resolved.ok) return resolved.response\n const { ctx, scope } = resolved\n const em = ctx.container.resolve('em') as EntityManager\n\n const url = new URL(req.url)\n const queryParams = Object.fromEntries(url.searchParams.entries())\n const input = listNotificationsSchema.parse(queryParams)\n const userId = scope.userId\n // Mirrors api/unread-count/route.ts: unrestricted and omitted legacy organization\n // scopes cannot be safely tagged for invalidation, because organization-specific\n // writes only invalidate their own collection tag. Leave those scopes uncached\n // rather than serving a stale tenant-wide list until the TTL expires.\n const cacheableOrganizationIds = Array.isArray(scope.organizationIds)\n ? scope.organizationIds\n : null\n const cache = userId && cacheableOrganizationIds && isCrudCacheEnabled()\n ? resolveCrudCache(ctx.container)\n : null\n const cacheKey = cache && userId && cacheableOrganizationIds\n ? buildNotificationsListCacheKey(\n userId,\n { organizationId: scope.organizationId, organizationIds: cacheableOrganizationIds },\n input,\n )\n : null\n\n if (cache && cacheKey) {\n try {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n if (isNotificationsListPayload(cached)) {\n return Response.json(cached)\n }\n } catch (error) {\n debugCrudCache('notifications-list-cache-read-failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n const filters: Record<string, unknown> = {\n recipientUserId: userId,\n tenantId: scope.tenantId,\n ...buildNotificationReadScopeWhere(scope),\n }\n\n if (input.status) {\n filters.status = Array.isArray(input.status) ? { $in: input.status } : input.status\n } else {\n filters.status = { $ne: 'dismissed' }\n }\n if (input.type) {\n filters.type = input.type\n }\n if (input.severity) {\n filters.severity = input.severity\n }\n if (input.sourceEntityType) {\n filters.sourceEntityType = input.sourceEntityType\n }\n if (input.sourceEntityId) {\n filters.sourceEntityId = input.sourceEntityId\n }\n if (input.since) {\n filters.createdAt = { $gt: new Date(input.since) }\n }\n\n const [notifications, total] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: input.pageSize,\n offset: (input.page - 1) * input.pageSize,\n }),\n em.count(Notification, filters),\n ])\n\n const items = notifications.map(toNotificationDto)\n\n const payload: NotificationsListPayload = {\n items,\n total,\n page: input.page,\n pageSize: input.pageSize,\n totalPages: Math.ceil(total / input.pageSize),\n }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(scope.tenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: NOTIFICATIONS_LIST_TTL_MS,\n tags: buildCollectionTags(\n NOTIFICATIONS_LIST_RESOURCE,\n scope.tenantId,\n getNotificationReadScopeTagOrganizationIds(scope),\n ),\n }),\n )\n } catch (error) {\n debugCrudCache('notifications-list-cache-write-failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n return Response.json(payload)\n}\n\nexport async function POST(req: Request) {\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n const body = await req.json().catch(() => ({}))\n const parsed = createNotificationSchema.safeParse(body)\n if (!parsed.success) {\n return notificationValidationErrorResponse(parsed.error)\n }\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n operation: 'create',\n payload: parsed.data as Record<string, unknown>,\n },\n () => service.create(parsed.data, scope),\n )\n if (!guarded.ok) return guarded.response\n\n return Response.json({ id: guarded.result.id }, { status: 201 })\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n}\n\nconst notificationsCrudOpenApi = buildNotificationsCrudOpenApi({\n resourceName: 'Notification',\n querySchema: listNotificationsSchema,\n listResponseSchema: createPagedListResponseSchema(notificationItemSchema),\n create: {\n schema: createNotificationSchema,\n responseSchema: z.object({ id: z.string().uuid() }),\n description: 'Creates a notification for a user.',\n },\n})\n\nconst notificationsCrudGet = notificationsCrudOpenApi.methods?.GET ?? {}\n\n// The CRUD factory documents errors only for DELETE, and POST already gets an auto-generated 403\n// from its `requireFeatures` metadata. GET is authenticated-only, so its tenant-scope rejection has\n// to be declared here to reach the generated spec.\nexport const openApi: OpenApiRouteDoc = {\n ...notificationsCrudOpenApi,\n methods: {\n ...notificationsCrudOpenApi.methods,\n GET: {\n ...notificationsCrudGet,\n errors: [\n ...(notificationsCrudGet.errors ?? []),\n {\n status: 403,\n description: `Request could not be resolved to a tenant scope (code: ${TENANT_SCOPE_REQUIRED_ERROR_CODE})`,\n schema: scopeErrorResponseSchema,\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -61,12 +61,13 @@ async function POST(req) {
61
61
  { status: 400 }
62
62
  );
63
63
  }
64
+ const actorTenantId = auth.actorTenantId ?? null;
64
65
  const container = await createRequestContainer();
65
66
  try {
66
67
  const guarded = await runGuardedNotificationWrite(
67
68
  container,
68
69
  {
69
- tenantId: auth.tenantId ?? "",
70
+ tenantId: auth.tenantId ?? actorTenantId ?? "",
70
71
  organizationId: auth.orgId ?? null,
71
72
  userId: auth.sub ?? null
72
73
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/notifications/api/settings/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { notificationDeliveryConfigSchema } from '../../data/validators'\nimport {\n errorResponseSchema,\n notificationSettingsResponseSchema,\n notificationSettingsUpdateResponseSchema,\n} from '../openapi'\nimport {\n DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n resolveNotificationDeliveryConfig,\n saveNotificationDeliveryConfig,\n} from '../../lib/deliveryConfig'\nimport {\n NOTIFICATION_SETTINGS_RESOURCE_KIND,\n runGuardedNotificationWrite,\n} from '../../lib/routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['notifications.manage'] },\n POST: { requireAuth: true, requireFeatures: ['notifications.manage'] },\n}\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const settings = await resolveNotificationDeliveryConfig(container, {\n defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n })\n return NextResponse.json({ settings })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json(\n { error: t('api.errors.invalidPayload', 'Invalid request body') },\n { status: 400 }\n )\n }\n\n const parsed = notificationDeliveryConfigSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: t('notifications.delivery.settings.invalid', 'Invalid delivery settings') },\n { status: 400 }\n )\n }\n\n const container = await createRequestContainer()\n try {\n const guarded = await runGuardedNotificationWrite(\n container,\n {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub ?? null,\n },\n req,\n {\n resourceKind: NOTIFICATION_SETTINGS_RESOURCE_KIND,\n operation: 'update',\n payload: parsed.data as Record<string, unknown>,\n },\n async () => {\n await saveNotificationDeliveryConfig(container, parsed.data)\n return resolveNotificationDeliveryConfig(container, {\n defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n })\n },\n )\n if (!guarded.ok) return guarded.response\n return NextResponse.json({ ok: true, settings: guarded.result })\n } catch (error) {\n return NextResponse.json(\n { error: error instanceof Error ? error.message : t('api.errors.internal', 'Internal error') },\n { status: 500 }\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get notification delivery settings',\n tags: ['Notifications'],\n responses: {\n 200: {\n description: 'Current delivery settings',\n content: {\n 'application/json': {\n schema: notificationSettingsResponseSchema,\n },\n },\n },\n 401: {\n description: 'Unauthorized',\n content: {\n 'application/json': {\n schema: errorResponseSchema,\n },\n },\n },\n },\n },\n POST: {\n summary: 'Update notification delivery settings',\n tags: ['Notifications'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: notificationDeliveryConfigSchema,\n },\n },\n },\n responses: {\n 200: {\n description: 'Delivery settings updated',\n content: {\n 'application/json': {\n schema: notificationSettingsUpdateResponseSchema,\n },\n },\n },\n 400: {\n description: 'Invalid request body',\n content: {\n 'application/json': {\n schema: errorResponseSchema,\n },\n },\n },\n 401: {\n description: 'Unauthorized',\n content: {\n 'application/json': {\n schema: errorResponseSchema,\n },\n },\n },\n 500: {\n description: 'Internal error',\n content: {\n 'application/json': {\n schema: errorResponseSchema,\n },\n },\n },\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { notificationDeliveryConfigSchema } from '../../data/validators'\nimport {\n errorResponseSchema,\n notificationSettingsResponseSchema,\n notificationSettingsUpdateResponseSchema,\n} from '../openapi'\nimport {\n DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n resolveNotificationDeliveryConfig,\n saveNotificationDeliveryConfig,\n} from '../../lib/deliveryConfig'\nimport {\n NOTIFICATION_SETTINGS_RESOURCE_KIND,\n runGuardedNotificationWrite,\n} from '../../lib/routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['notifications.manage'] },\n POST: { requireAuth: true, requireFeatures: ['notifications.manage'] },\n}\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const settings = await resolveNotificationDeliveryConfig(container, {\n defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n })\n return NextResponse.json({ settings })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json(\n { error: t('api.errors.invalidPayload', 'Invalid request body') },\n { status: 400 }\n )\n }\n\n const parsed = notificationDeliveryConfigSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: t('notifications.delivery.settings.invalid', 'Invalid delivery settings') },\n { status: 400 }\n )\n }\n\n // Unlike the other write routes this one does not go through `resolveNotificationContext`, so it\n // never sees the organization scope's `actorTenantId` fallback. A super-admin scoped away from\n // their own tenant has `auth.tenantId === null` with the real tenant preserved in `actorTenantId`,\n // and delivery settings are instance-global anyway \u2014 reading it here keeps that caller working\n // while a genuinely tenant-less principal still fails the guard below.\n const actorTenantId = (auth as { actorTenantId?: string | null }).actorTenantId ?? null\n\n const container = await createRequestContainer()\n try {\n const guarded = await runGuardedNotificationWrite(\n container,\n {\n tenantId: auth.tenantId ?? actorTenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub ?? null,\n },\n req,\n {\n resourceKind: NOTIFICATION_SETTINGS_RESOURCE_KIND,\n operation: 'update',\n payload: parsed.data as Record<string, unknown>,\n },\n async () => {\n await saveNotificationDeliveryConfig(container, parsed.data)\n return resolveNotificationDeliveryConfig(container, {\n defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n })\n },\n )\n if (!guarded.ok) return guarded.response\n return NextResponse.json({ ok: true, settings: guarded.result })\n } catch (error) {\n return NextResponse.json(\n { error: error instanceof Error ? error.message : t('api.errors.internal', 'Internal error') },\n { status: 500 }\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get notification delivery settings',\n tags: ['Notifications'],\n responses: {\n 200: {\n description: 'Current delivery settings',\n content: {\n 'application/json': {\n schema: notificationSettingsResponseSchema,\n },\n },\n },\n 401: {\n description: 'Unauthorized',\n content: {\n 'application/json': {\n schema: errorResponseSchema,\n },\n },\n },\n },\n },\n POST: {\n summary: 'Update notification delivery settings',\n tags: ['Notifications'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: notificationDeliveryConfigSchema,\n },\n },\n },\n responses: {\n 200: {\n description: 'Delivery settings updated',\n content: {\n 'application/json': {\n schema: notificationSettingsUpdateResponseSchema,\n },\n },\n },\n 400: {\n description: 'Invalid request body',\n content: {\n 'application/json': {\n schema: errorResponseSchema,\n },\n },\n },\n 401: {\n description: 'Unauthorized',\n content: {\n 'application/json': {\n schema: errorResponseSchema,\n },\n },\n },\n 500: {\n description: 'Internal error',\n content: {\n 'application/json': {\n schema: errorResponseSchema,\n },\n },\n },\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "@open-mercato/shared/lib/crud/cache";
8
8
  import { Notification } from "../../data/entities.js";
9
9
  import { unreadCountResponseSchema } from "../openapi.js";
10
- import { resolveNotificationContext } from "../../lib/routeHelpers.js";
10
+ import { resolveGuardedNotificationContext } from "../../lib/routeHelpers.js";
11
11
  import {
12
12
  buildNotificationReadScopeWhere,
13
13
  getNotificationReadScopeTagOrganizationIds
@@ -25,7 +25,9 @@ function buildUnreadCountCacheKey(params) {
25
25
  return `notifications:unread-count:u=${params.userId}:org=${scopeKey}`;
26
26
  }
27
27
  async function GET(req) {
28
- const { scope, ctx } = await resolveNotificationContext(req);
28
+ const resolved = await resolveGuardedNotificationContext(req);
29
+ if (!resolved.ok) return resolved.response;
30
+ const { scope, ctx } = resolved;
29
31
  const em = ctx.container.resolve("em");
30
32
  const userId = scope.userId;
31
33
  const cacheableOrganizationIds = Array.isArray(scope.organizationIds) ? scope.organizationIds : null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/notifications/api/unread-count/route.ts"],
4
- "sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { Notification } from '../../data/entities'\nimport { unreadCountResponseSchema } from '../openapi'\nimport { resolveNotificationContext } from '../../lib/routeHelpers'\nimport {\n buildNotificationReadScopeWhere,\n getNotificationReadScopeTagOrganizationIds,\n} from '../../lib/notificationScope'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst UNREAD_COUNT_RESOURCE = 'notifications.notification'\nconst UNREAD_COUNT_TTL_MS = 10_000\n\nfunction buildUnreadCountCacheKey(params: {\n userId: string\n organizationId: string | null\n organizationIds: string[]\n}): string {\n const normalizedIds = Array.from(new Set(\n params.organizationIds.filter((value) => value.trim().length > 0),\n )).sort((left, right) => left.localeCompare(right))\n const scopeKey = normalizedIds.length === 0\n ? 'no-access'\n : `${params.organizationId ?? 'none'}:scope=${createHash('sha256')\n .update(normalizedIds.join('\\0'))\n .digest('hex')\n .slice(0, 16)}`\n return `notifications:unread-count:u=${params.userId}:org=${scopeKey}`\n}\n\nexport async function GET(req: Request) {\n const { scope, ctx } = await resolveNotificationContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n\n const userId = scope.userId\n const cacheableOrganizationIds = Array.isArray(scope.organizationIds)\n ? scope.organizationIds\n : null\n // Unrestricted and omitted legacy scopes cannot be safely tagged for invalidation:\n // org-specific writes only invalidate their own collection tag. Keep those scopes\n // uncached rather than serving a stale tenant-wide total until the TTL expires.\n const cache = userId && cacheableOrganizationIds && isCrudCacheEnabled()\n ? resolveCrudCache(ctx.container)\n : null\n const cacheKey = cache && userId && cacheableOrganizationIds\n ? buildUnreadCountCacheKey({\n userId,\n organizationId: scope.organizationId,\n organizationIds: cacheableOrganizationIds,\n })\n : null\n\n if (cache && cacheKey) {\n try {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n if (typeof cached === 'number') {\n return Response.json({ unreadCount: cached })\n }\n } catch {}\n }\n\n const count = await em.count(Notification, {\n recipientUserId: userId,\n tenantId: scope.tenantId,\n status: 'unread',\n ...buildNotificationReadScopeWhere(scope),\n })\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(scope.tenantId, () =>\n cache.set(cacheKey, count, {\n ttl: UNREAD_COUNT_TTL_MS,\n tags: buildCollectionTags(\n UNREAD_COUNT_RESOURCE,\n scope.tenantId,\n getNotificationReadScopeTagOrganizationIds(scope),\n ),\n }),\n )\n } catch {}\n }\n\n return Response.json({ unreadCount: count })\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get unread notification count',\n tags: ['Notifications'],\n responses: {\n 200: {\n description: 'Unread count',\n content: {\n 'application/json': {\n schema: unreadCountResponseSchema,\n },\n },\n },\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAC3C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAC3B;AAEA,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAE5B,SAAS,yBAAyB,QAIvB;AACT,QAAM,gBAAgB,MAAM,KAAK,IAAI;AAAA,IACnC,OAAO,gBAAgB,OAAO,CAAC,UAAU,MAAM,KAAK,EAAE,SAAS,CAAC;AAAA,EAClE,CAAC,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC;AAClD,QAAM,WAAW,cAAc,WAAW,IACtC,cACA,GAAG,OAAO,kBAAkB,MAAM,UAAU,WAAW,QAAQ,EAC5D,OAAO,cAAc,KAAK,IAAI,CAAC,EAC/B,OAAO,KAAK,EACZ,MAAM,GAAG,EAAE,CAAC;AACnB,SAAO,gCAAgC,OAAO,MAAM,QAAQ,QAAQ;AACtE;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAC3D,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AAErC,QAAM,SAAS,MAAM;AACrB,QAAM,2BAA2B,MAAM,QAAQ,MAAM,eAAe,IAChE,MAAM,kBACN;AAIJ,QAAM,QAAQ,UAAU,4BAA4B,mBAAmB,IACnE,iBAAiB,IAAI,SAAS,IAC9B;AACJ,QAAM,WAAW,SAAS,UAAU,2BAChC,yBAAyB;AAAA,IACvB;AAAA,IACA,gBAAgB,MAAM;AAAA,IACtB,iBAAiB;AAAA,EACnB,CAAC,IACD;AAEJ,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM,SAAS,MAAM,mBAAmB,MAAM,UAAU,MAAM,MAAM,IAAI,QAAQ,CAAC;AACjF,UAAI,OAAO,WAAW,UAAU;AAC9B,eAAO,SAAS,KAAK,EAAE,aAAa,OAAO,CAAC;AAAA,MAC9C;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,QAAM,QAAQ,MAAM,GAAG,MAAM,cAAc;AAAA,IACzC,iBAAiB;AAAA,IACjB,UAAU,MAAM;AAAA,IAChB,QAAQ;AAAA,IACR,GAAG,gCAAgC,KAAK;AAAA,EAC1C,CAAC;AAED,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM;AAAA,QAAmB,MAAM;AAAA,QAAU,MACvC,MAAM,IAAI,UAAU,OAAO;AAAA,UACzB,KAAK;AAAA,UACL,MAAM;AAAA,YACJ;AAAA,YACA,MAAM;AAAA,YACN,2CAA2C,KAAK;AAAA,UAClD;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,SAAO,SAAS,KAAK,EAAE,aAAa,MAAM,CAAC;AAC7C;AAEO,MAAM,UAAU;AAAA,EACrB,KAAK;AAAA,IACH,SAAS;AAAA,IACT,MAAM,CAAC,eAAe;AAAA,IACtB,WAAW;AAAA,MACT,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { Notification } from '../../data/entities'\nimport { unreadCountResponseSchema } from '../openapi'\nimport { resolveGuardedNotificationContext } from '../../lib/routeHelpers'\nimport {\n buildNotificationReadScopeWhere,\n getNotificationReadScopeTagOrganizationIds,\n} from '../../lib/notificationScope'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst UNREAD_COUNT_RESOURCE = 'notifications.notification'\nconst UNREAD_COUNT_TTL_MS = 10_000\n\nfunction buildUnreadCountCacheKey(params: {\n userId: string\n organizationId: string | null\n organizationIds: string[]\n}): string {\n const normalizedIds = Array.from(new Set(\n params.organizationIds.filter((value) => value.trim().length > 0),\n )).sort((left, right) => left.localeCompare(right))\n const scopeKey = normalizedIds.length === 0\n ? 'no-access'\n : `${params.organizationId ?? 'none'}:scope=${createHash('sha256')\n .update(normalizedIds.join('\\0'))\n .digest('hex')\n .slice(0, 16)}`\n return `notifications:unread-count:u=${params.userId}:org=${scopeKey}`\n}\n\nexport async function GET(req: Request) {\n const resolved = await resolveGuardedNotificationContext(req)\n if (!resolved.ok) return resolved.response\n const { scope, ctx } = resolved\n const em = ctx.container.resolve('em') as EntityManager\n\n const userId = scope.userId\n const cacheableOrganizationIds = Array.isArray(scope.organizationIds)\n ? scope.organizationIds\n : null\n // Unrestricted and omitted legacy scopes cannot be safely tagged for invalidation:\n // org-specific writes only invalidate their own collection tag. Keep those scopes\n // uncached rather than serving a stale tenant-wide total until the TTL expires.\n const cache = userId && cacheableOrganizationIds && isCrudCacheEnabled()\n ? resolveCrudCache(ctx.container)\n : null\n const cacheKey = cache && userId && cacheableOrganizationIds\n ? buildUnreadCountCacheKey({\n userId,\n organizationId: scope.organizationId,\n organizationIds: cacheableOrganizationIds,\n })\n : null\n\n if (cache && cacheKey) {\n try {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n if (typeof cached === 'number') {\n return Response.json({ unreadCount: cached })\n }\n } catch {}\n }\n\n const count = await em.count(Notification, {\n recipientUserId: userId,\n tenantId: scope.tenantId,\n status: 'unread',\n ...buildNotificationReadScopeWhere(scope),\n })\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(scope.tenantId, () =>\n cache.set(cacheKey, count, {\n ttl: UNREAD_COUNT_TTL_MS,\n tags: buildCollectionTags(\n UNREAD_COUNT_RESOURCE,\n scope.tenantId,\n getNotificationReadScopeTagOrganizationIds(scope),\n ),\n }),\n )\n } catch {}\n }\n\n return Response.json({ unreadCount: count })\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get unread notification count',\n tags: ['Notifications'],\n responses: {\n 200: {\n description: 'Unread count',\n content: {\n 'application/json': {\n schema: unreadCountResponseSchema,\n },\n },\n },\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,yCAAyC;AAClD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAC3B;AAEA,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAE5B,SAAS,yBAAyB,QAIvB;AACT,QAAM,gBAAgB,MAAM,KAAK,IAAI;AAAA,IACnC,OAAO,gBAAgB,OAAO,CAAC,UAAU,MAAM,KAAK,EAAE,SAAS,CAAC;AAAA,EAClE,CAAC,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC;AAClD,QAAM,WAAW,cAAc,WAAW,IACtC,cACA,GAAG,OAAO,kBAAkB,MAAM,UAAU,WAAW,QAAQ,EAC5D,OAAO,cAAc,KAAK,IAAI,CAAC,EAC/B,OAAO,KAAK,EACZ,MAAM,GAAG,EAAE,CAAC;AACnB,SAAO,gCAAgC,OAAO,MAAM,QAAQ,QAAQ;AACtE;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,WAAW,MAAM,kCAAkC,GAAG;AAC5D,MAAI,CAAC,SAAS,GAAI,QAAO,SAAS;AAClC,QAAM,EAAE,OAAO,IAAI,IAAI;AACvB,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AAErC,QAAM,SAAS,MAAM;AACrB,QAAM,2BAA2B,MAAM,QAAQ,MAAM,eAAe,IAChE,MAAM,kBACN;AAIJ,QAAM,QAAQ,UAAU,4BAA4B,mBAAmB,IACnE,iBAAiB,IAAI,SAAS,IAC9B;AACJ,QAAM,WAAW,SAAS,UAAU,2BAChC,yBAAyB;AAAA,IACvB;AAAA,IACA,gBAAgB,MAAM;AAAA,IACtB,iBAAiB;AAAA,EACnB,CAAC,IACD;AAEJ,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM,SAAS,MAAM,mBAAmB,MAAM,UAAU,MAAM,MAAM,IAAI,QAAQ,CAAC;AACjF,UAAI,OAAO,WAAW,UAAU;AAC9B,eAAO,SAAS,KAAK,EAAE,aAAa,OAAO,CAAC;AAAA,MAC9C;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,QAAM,QAAQ,MAAM,GAAG,MAAM,cAAc;AAAA,IACzC,iBAAiB;AAAA,IACjB,UAAU,MAAM;AAAA,IAChB,QAAQ;AAAA,IACR,GAAG,gCAAgC,KAAK;AAAA,EAC1C,CAAC;AAED,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM;AAAA,QAAmB,MAAM;AAAA,QAAU,MACvC,MAAM,IAAI,UAAU,OAAO;AAAA,UACzB,KAAK;AAAA,UACL,MAAM;AAAA,YACJ;AAAA,YACA,MAAM;AAAA,YACN,2CAA2C,KAAK;AAAA,UAClD;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,SAAO,SAAS,KAAK,EAAE,aAAa,MAAM,CAAC;AAC7C;AAEO,MAAM,UAAU;AAAA,EACrB,KAAK;AAAA,IACH,SAAS;AAAA,IACT,MAAM,CAAC,eAAe;AAAA,IACtB,WAAW;AAAA,MACT,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -26,6 +26,18 @@ function notificationCrudErrorResponse(error) {
26
26
  if (!isCrudHttpError(error)) return null;
27
27
  return Response.json(error.body ?? { error: "Notification request failed" }, { status: error.status });
28
28
  }
29
+ const TENANT_SCOPE_REQUIRED_ERROR_CODE = "tenant_scope_required";
30
+ async function requireResolvedNotificationTenantScope(scope) {
31
+ if (scope.tenantId) return null;
32
+ const { t } = await resolveTranslations();
33
+ return Response.json(
34
+ {
35
+ error: t("api.errors.forbidden", "Forbidden"),
36
+ code: TENANT_SCOPE_REQUIRED_ERROR_CODE
37
+ },
38
+ { status: 403 }
39
+ );
40
+ }
29
41
  async function resolveNotificationContext(req) {
30
42
  const { ctx } = await resolveRequestContext(req);
31
43
  const organizationScope = await resolveOrganizationScopeForRequest({
@@ -51,7 +63,17 @@ async function resolveNotificationContext(req) {
51
63
  ctx
52
64
  };
53
65
  }
66
+ async function resolveGuardedNotificationContext(req) {
67
+ const resolved = await resolveNotificationContext(req);
68
+ const tenantScopeGuard = await requireResolvedNotificationTenantScope(resolved.scope);
69
+ if (tenantScopeGuard) return { ok: false, response: tenantScopeGuard };
70
+ return { ok: true, ...resolved };
71
+ }
54
72
  async function runGuardedNotificationWrite(container, scope, req, options, write) {
73
+ const tenantScopeGuard = await requireResolvedNotificationTenantScope(scope);
74
+ if (tenantScopeGuard) {
75
+ return { ok: false, response: tenantScopeGuard };
76
+ }
55
77
  const guarded = await runRouteMutationGuards({
56
78
  container,
57
79
  req,
@@ -193,12 +215,15 @@ function createSingleNotificationActionOpenApi(summary, description) {
193
215
  export {
194
216
  NOTIFICATION_RESOURCE_KIND,
195
217
  NOTIFICATION_SETTINGS_RESOURCE_KIND,
218
+ TENANT_SCOPE_REQUIRED_ERROR_CODE,
196
219
  createBulkNotificationOpenApi,
197
220
  createBulkNotificationRoute,
198
221
  createSingleNotificationActionOpenApi,
199
222
  createSingleNotificationActionRoute,
200
223
  notificationCrudErrorResponse,
201
224
  notificationValidationErrorResponse,
225
+ requireResolvedNotificationTenantScope,
226
+ resolveGuardedNotificationContext,
202
227
  resolveNotificationContext,
203
228
  runGuardedNotificationWrite
204
229
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/lib/routeHelpers.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport { resolveRequestContext } from '@open-mercato/shared/lib/api/context'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveNotificationService, type NotificationService } from './notificationService'\n\n/**\n * Mutation-guard resource kind for notification rows.\n */\nexport const NOTIFICATION_RESOURCE_KIND = 'notifications.notification'\n\n/**\n * Mutation-guard resource kind for notification delivery settings.\n */\nexport const NOTIFICATION_SETTINGS_RESOURCE_KIND = 'notifications.settings'\n\n/**\n * Notification scope context for service calls\n */\nexport interface NotificationScope {\n tenantId: string\n organizationId: string | null\n organizationIds?: string[] | null\n userId: string | null\n}\n\n/**\n * Resolved notification context from a request\n */\nexport interface NotificationRequestContext {\n service: NotificationService\n scope: NotificationScope\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx']\n}\n\nfunction formatZodIssues(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.length ? `${issue.path.join('.')}: ` : ''\n return `${path}${issue.message}`\n })\n .join('; ')\n}\n\nexport async function notificationValidationErrorResponse(error: z.ZodError): Promise<Response> {\n const { t } = await resolveTranslations()\n const prefix = t('api.errors.invalidPayload', 'Invalid request body')\n const details = formatZodIssues(error)\n return Response.json(\n { error: details ? `${prefix}: ${details}` : prefix },\n { status: 400 },\n )\n}\n\nexport function notificationCrudErrorResponse(error: unknown): Response | null {\n if (!isCrudHttpError(error)) return null\n return Response.json(error.body ?? { error: 'Notification request failed' }, { status: error.status })\n}\n\n/**\n * Resolve notification service and scope from a request.\n * Centralizes the common pattern used across all notification API routes.\n */\nexport async function resolveNotificationContext(req: Request): Promise<NotificationRequestContext> {\n const { ctx } = await resolveRequestContext(req)\n const organizationScope = await resolveOrganizationScopeForRequest({\n container: ctx.container,\n auth: ctx.auth,\n request: req,\n ...(ctx.selectedOrganizationId === undefined\n ? {}\n : { selectedId: ctx.selectedOrganizationId }),\n })\n const tenantId = organizationScope.tenantId ?? ctx.auth?.tenantId ?? ''\n const organizationId = organizationScope.selectedId\n const organizationIds = organizationScope.filterIds\n ctx.organizationScope = organizationScope\n ctx.selectedOrganizationId = organizationId\n ctx.organizationIds = organizationIds\n return {\n service: resolveNotificationService(ctx.container),\n scope: {\n tenantId,\n organizationId,\n organizationIds,\n userId: ctx.auth?.sub ?? null,\n },\n ctx,\n }\n}\n\n/**\n * Mutation-guard options for a notification write.\n */\nexport interface NotificationMutationGuardOptions {\n resourceKind: string\n resourceId?: string | null\n operation: 'create' | 'update' | 'delete' | 'custom'\n payload?: Record<string, unknown> | null\n}\n\nexport type GuardedNotificationWriteResult<T> =\n | { ok: true; result: T }\n | { ok: false; response: Response }\n\n/**\n * Run a notification write through the mutation guard lifecycle.\n * Validates before the mutation, performs the write, then runs after-success\n * hooks only when the write succeeded and the guard requested them. Returns the\n * guard's own block response when validation fails so authorization behavior and\n * conflict shapes are preserved.\n */\nexport async function runGuardedNotificationWrite<T>(\n container: AwilixContainer,\n scope: NotificationScope,\n req: Request,\n options: NotificationMutationGuardOptions,\n write: () => Promise<T>,\n): Promise<GuardedNotificationWriteResult<T>> {\n const guarded = await runRouteMutationGuards({\n container,\n req,\n auth: {\n userId: scope.userId ?? '',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n input: {\n resourceKind: options.resourceKind,\n resourceId: options.resourceId ?? null,\n operation: options.operation,\n mutationPayload: options.payload ?? null,\n },\n })\n if (!guarded.ok) {\n return { ok: false, response: guarded.response }\n }\n\n const result = await write()\n\n await guarded.runAfterSuccess()\n\n return { ok: true, result }\n}\n\n/**\n * Create a POST handler for bulk notification creation routes.\n * Used by batch, role, and feature notification endpoints.\n */\nexport function createBulkNotificationRoute<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n serviceMethod: 'createBatch' | 'createForRole' | 'createForFeature'\n) {\n return async function POST(req: Request) {\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n const body = await req.json().catch(() => ({}))\n const parsed = schema.safeParse(body)\n if (!parsed.success) {\n return notificationValidationErrorResponse(parsed.error)\n }\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n operation: 'create',\n payload: parsed.data as Record<string, unknown>,\n },\n () => service[serviceMethod](parsed.data as never, scope),\n )\n if (!guarded.ok) return guarded.response\n const notifications = guarded.result\n\n return Response.json({\n ok: true,\n count: notifications.length,\n ids: notifications.map((n) => n.id),\n }, { status: 201 })\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n }\n}\n\n/**\n * Create OpenAPI spec for bulk notification creation routes.\n */\nexport function createBulkNotificationOpenApi<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n summary: string,\n description?: string\n) {\n return {\n POST: {\n summary,\n description,\n tags: ['Notifications'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema,\n },\n },\n },\n responses: {\n 201: {\n description: 'Notifications created',\n content: {\n 'application/json': {\n schema: z.object({\n ok: z.boolean(),\n count: z.number(),\n ids: z.array(z.string().uuid()),\n }),\n },\n },\n },\n },\n },\n }\n}\n\n/**\n * Create a PUT handler for single notification action routes.\n * Used by read and dismiss endpoints.\n */\nexport function createSingleNotificationActionRoute(\n serviceMethod: 'markAsRead' | 'dismiss'\n) {\n return async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {\n const { id } = await params\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n resourceId: id,\n operation: 'update',\n },\n () => service[serviceMethod](id, scope),\n )\n if (!guarded.ok) return guarded.response\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n\n return Response.json({ ok: true })\n }\n}\n\n/**\n * Create OpenAPI spec for single notification action routes.\n */\nexport function createSingleNotificationActionOpenApi(\n summary: string,\n description: string\n) {\n return {\n PUT: {\n summary,\n tags: ['Notifications'],\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n ],\n responses: {\n 200: {\n description,\n content: {\n 'application/json': {\n schema: z.object({ ok: z.boolean() }),\n },\n },\n },\n },\n },\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport { resolveRequestContext } from '@open-mercato/shared/lib/api/context'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveNotificationService, type NotificationService } from './notificationService'\n\n/**\n * Mutation-guard resource kind for notification rows.\n */\nexport const NOTIFICATION_RESOURCE_KIND = 'notifications.notification'\n\n/**\n * Mutation-guard resource kind for notification delivery settings.\n */\nexport const NOTIFICATION_SETTINGS_RESOURCE_KIND = 'notifications.settings'\n\n/**\n * Notification scope context for service calls\n */\nexport interface NotificationScope {\n tenantId: string\n organizationId: string | null\n organizationIds?: string[] | null\n userId: string | null\n}\n\n/**\n * Resolved notification context from a request\n */\nexport interface NotificationRequestContext {\n service: NotificationService\n scope: NotificationScope\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx']\n}\n\nfunction formatZodIssues(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.length ? `${issue.path.join('.')}: ` : ''\n return `${path}${issue.message}`\n })\n .join('; ')\n}\n\nexport async function notificationValidationErrorResponse(error: z.ZodError): Promise<Response> {\n const { t } = await resolveTranslations()\n const prefix = t('api.errors.invalidPayload', 'Invalid request body')\n const details = formatZodIssues(error)\n return Response.json(\n { error: details ? `${prefix}: ${details}` : prefix },\n { status: 400 },\n )\n}\n\nexport function notificationCrudErrorResponse(error: unknown): Response | null {\n if (!isCrudHttpError(error)) return null\n return Response.json(error.body ?? { error: 'Notification request failed' }, { status: error.status })\n}\n\n/**\n * Machine-readable discriminator for the tenant-scope rejection, mirroring\n * `ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE`. Without it a client cannot tell an unresolved scope\n * apart from an ordinary permission denial, since both are a 403 carrying only a message.\n */\nexport const TENANT_SCOPE_REQUIRED_ERROR_CODE = 'tenant_scope_required'\n\n/**\n * Fail closed when a request cannot be resolved to a tenant.\n *\n * `resolveNotificationContext` falls back to `''` when neither the organization scope nor the auth\n * context yields a tenant \u2014 reachable for a genuinely tenant-less principal such as an unscoped\n * super-admin API key. `notifications.tenant_id` is a NOT NULL uuid, so that `''` makes the driver\n * reject every read and write built from the scope; dropping the tenant predicate instead would\n * leave `recipientUserId` as the only thing keeping a read inside one tenant.\n *\n * Unlike the audit-log read guard there is no `isSuperAdmin` escape hatch: notification rows are\n * per-recipient and per-tenant, so there is no cross-tenant read mode to preserve.\n *\n * Plain truthiness is deliberate \u2014 it matches the `?? ''` sentinel and also rejects a null or\n * omitted tenant reaching this helper from a caller that builds its own scope.\n */\nexport async function requireResolvedNotificationTenantScope(\n scope: { tenantId?: string | null },\n): Promise<Response | null> {\n if (scope.tenantId) return null\n const { t } = await resolveTranslations()\n return Response.json(\n {\n error: t('api.errors.forbidden', 'Forbidden'),\n code: TENANT_SCOPE_REQUIRED_ERROR_CODE,\n },\n { status: 403 },\n )\n}\n\n/**\n * Resolve notification service and scope from a request.\n * Centralizes the common pattern used across all notification API routes.\n */\nexport async function resolveNotificationContext(req: Request): Promise<NotificationRequestContext> {\n const { ctx } = await resolveRequestContext(req)\n const organizationScope = await resolveOrganizationScopeForRequest({\n container: ctx.container,\n auth: ctx.auth,\n request: req,\n ...(ctx.selectedOrganizationId === undefined\n ? {}\n : { selectedId: ctx.selectedOrganizationId }),\n })\n const tenantId = organizationScope.tenantId ?? ctx.auth?.tenantId ?? ''\n const organizationId = organizationScope.selectedId\n const organizationIds = organizationScope.filterIds\n ctx.organizationScope = organizationScope\n ctx.selectedOrganizationId = organizationId\n ctx.organizationIds = organizationIds\n return {\n service: resolveNotificationService(ctx.container),\n scope: {\n tenantId,\n organizationId,\n organizationIds,\n userId: ctx.auth?.sub ?? null,\n },\n ctx,\n }\n}\n\nexport type GuardedNotificationContext =\n | ({ ok: true } & NotificationRequestContext)\n | { ok: false; response: Response }\n\n/**\n * Resolve the notification context and reject the request when it has no tenant.\n *\n * Writes are structurally safe because every one of them funnels through\n * `runGuardedNotificationWrite`. Reads have no such choke point, so they resolve their context\n * through this wrapper instead: a route that forgets to check cannot compile past the discriminated\n * result, which makes the guard impossible to skip by omission rather than by convention.\n */\nexport async function resolveGuardedNotificationContext(req: Request): Promise<GuardedNotificationContext> {\n const resolved = await resolveNotificationContext(req)\n const tenantScopeGuard = await requireResolvedNotificationTenantScope(resolved.scope)\n if (tenantScopeGuard) return { ok: false, response: tenantScopeGuard }\n return { ok: true, ...resolved }\n}\n\n/**\n * Mutation-guard options for a notification write.\n */\nexport interface NotificationMutationGuardOptions {\n resourceKind: string\n resourceId?: string | null\n operation: 'create' | 'update' | 'delete' | 'custom'\n payload?: Record<string, unknown> | null\n}\n\nexport type GuardedNotificationWriteResult<T> =\n | { ok: true; result: T }\n | { ok: false; response: Response }\n\n/**\n * Run a notification write through the mutation guard lifecycle.\n * Validates before the mutation, performs the write, then runs after-success\n * hooks only when the write succeeded and the guard requested them. Returns the\n * guard's own block response when validation fails so authorization behavior and\n * conflict shapes are preserved.\n */\nexport async function runGuardedNotificationWrite<T>(\n container: AwilixContainer,\n scope: NotificationScope,\n req: Request,\n options: NotificationMutationGuardOptions,\n write: () => Promise<T>,\n): Promise<GuardedNotificationWriteResult<T>> {\n const tenantScopeGuard = await requireResolvedNotificationTenantScope(scope)\n if (tenantScopeGuard) {\n return { ok: false, response: tenantScopeGuard }\n }\n\n const guarded = await runRouteMutationGuards({\n container,\n req,\n auth: {\n userId: scope.userId ?? '',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n input: {\n resourceKind: options.resourceKind,\n resourceId: options.resourceId ?? null,\n operation: options.operation,\n mutationPayload: options.payload ?? null,\n },\n })\n if (!guarded.ok) {\n return { ok: false, response: guarded.response }\n }\n\n const result = await write()\n\n await guarded.runAfterSuccess()\n\n return { ok: true, result }\n}\n\n/**\n * Create a POST handler for bulk notification creation routes.\n * Used by batch, role, and feature notification endpoints.\n */\nexport function createBulkNotificationRoute<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n serviceMethod: 'createBatch' | 'createForRole' | 'createForFeature'\n) {\n return async function POST(req: Request) {\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n const body = await req.json().catch(() => ({}))\n const parsed = schema.safeParse(body)\n if (!parsed.success) {\n return notificationValidationErrorResponse(parsed.error)\n }\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n operation: 'create',\n payload: parsed.data as Record<string, unknown>,\n },\n () => service[serviceMethod](parsed.data as never, scope),\n )\n if (!guarded.ok) return guarded.response\n const notifications = guarded.result\n\n return Response.json({\n ok: true,\n count: notifications.length,\n ids: notifications.map((n) => n.id),\n }, { status: 201 })\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n }\n}\n\n/**\n * Create OpenAPI spec for bulk notification creation routes.\n */\nexport function createBulkNotificationOpenApi<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n summary: string,\n description?: string\n) {\n return {\n POST: {\n summary,\n description,\n tags: ['Notifications'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema,\n },\n },\n },\n responses: {\n 201: {\n description: 'Notifications created',\n content: {\n 'application/json': {\n schema: z.object({\n ok: z.boolean(),\n count: z.number(),\n ids: z.array(z.string().uuid()),\n }),\n },\n },\n },\n },\n },\n }\n}\n\n/**\n * Create a PUT handler for single notification action routes.\n * Used by read and dismiss endpoints.\n */\nexport function createSingleNotificationActionRoute(\n serviceMethod: 'markAsRead' | 'dismiss'\n) {\n return async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {\n const { id } = await params\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n resourceId: id,\n operation: 'update',\n },\n () => service[serviceMethod](id, scope),\n )\n if (!guarded.ok) return guarded.response\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n\n return Response.json({ ok: true })\n }\n}\n\n/**\n * Create OpenAPI spec for single notification action routes.\n */\nexport function createSingleNotificationActionOpenApi(\n summary: string,\n description: string\n) {\n return {\n PUT: {\n summary,\n tags: ['Notifications'],\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n ],\n responses: {\n 200: {\n description,\n content: {\n 'application/json': {\n schema: z.object({ ok: z.boolean() }),\n },\n },\n },\n },\n },\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
- getAllIntegrations
2
+ getAllIntegrations,
3
+ resolveIntegrationCredentialsSchema
3
4
  } from "@open-mercato/shared/modules/integrations/types";
4
5
  import {
5
6
  getPaymentGatewayDescriptor,
@@ -21,6 +22,16 @@ async function resolveDescriptor(descriptor, scope, deps) {
21
22
  configurationStatus: "unmanaged"
22
23
  };
23
24
  }
25
+ const credentialsSchema = resolveIntegrationCredentialsSchema(integration.id);
26
+ if (credentialsSchema?.fields.length === 0) {
27
+ return {
28
+ ...descriptor,
29
+ integrationId: integration.id,
30
+ requiresConfiguration: false,
31
+ isConfigured: true,
32
+ configurationStatus: "unmanaged"
33
+ };
34
+ }
24
35
  const [credentials, state] = await Promise.all([
25
36
  deps.integrationCredentialsService.resolve(integration.id, scope),
26
37
  deps.integrationStateService.resolveState(integration.id, scope)