@open-mercato/core 0.9.1-develop.7518.1.343d9198f1 → 0.9.1-develop.7532.1.8eac7e198f

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 (88) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/logout.js +6 -1
  3. package/dist/modules/auth/api/logout.js.map +2 -2
  4. package/dist/modules/auth/api/session/refresh.js +6 -1
  5. package/dist/modules/auth/api/session/refresh.js.map +2 -2
  6. package/dist/modules/catalog/ai-tools/mutation-pack.js +10 -5
  7. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +2 -2
  8. package/dist/modules/catalog/ai-tools/types.js.map +2 -2
  9. package/dist/modules/customer_accounts/api/login.js +14 -7
  10. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  11. package/dist/modules/customer_accounts/services/customerUserService.js +10 -10
  12. package/dist/modules/customer_accounts/services/customerUserService.js.map +2 -2
  13. package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js +1 -1
  14. package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js.map +1 -1
  15. package/dist/modules/forms/api/form-submissions/[id]/submit/route.js +12 -2
  16. package/dist/modules/forms/api/form-submissions/[id]/submit/route.js.map +2 -2
  17. package/dist/modules/forms/api/form-submissions/route.js +16 -1
  18. package/dist/modules/forms/api/form-submissions/route.js.map +2 -2
  19. package/dist/modules/forms/api/public/submissions/[id]/submit/route.js +6 -1
  20. package/dist/modules/forms/api/public/submissions/[id]/submit/route.js.map +2 -2
  21. package/dist/modules/forms/api/runtime-helpers.js +16 -0
  22. package/dist/modules/forms/api/runtime-helpers.js.map +2 -2
  23. package/dist/modules/forms/data/validators.js.map +2 -2
  24. package/dist/modules/forms/lib/portal-subject-policy.js +17 -0
  25. package/dist/modules/forms/lib/portal-subject-policy.js.map +7 -0
  26. package/dist/modules/forms/lib/runtime-principal.js +27 -10
  27. package/dist/modules/forms/lib/runtime-principal.js.map +2 -2
  28. package/dist/modules/forms/services/distribution-service.js +35 -13
  29. package/dist/modules/forms/services/distribution-service.js.map +2 -2
  30. package/dist/modules/forms/services/submission-service.js +4 -0
  31. package/dist/modules/forms/services/submission-service.js.map +2 -2
  32. package/dist/modules/staff/commands/leave-requests.js +54 -24
  33. package/dist/modules/staff/commands/leave-requests.js.map +2 -2
  34. package/dist/modules/staff/commands/timesheets-entries.js +4 -0
  35. package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
  36. package/dist/modules/staff/commands/timesheets-tags.js +27 -8
  37. package/dist/modules/staff/commands/timesheets-tags.js.map +2 -2
  38. package/dist/modules/warranty_claims/api/portal/attachments/route.js +19 -7
  39. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
  40. package/dist/modules/warranty_claims/commands/claims.js +1 -1
  41. package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
  42. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +6 -6
  43. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +2 -2
  44. package/dist/modules/wms/commands/inventory-actions.js +19 -3
  45. package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
  46. package/dist/modules/workflows/lib/async-activity-wait.js +43 -0
  47. package/dist/modules/workflows/lib/async-activity-wait.js.map +7 -0
  48. package/dist/modules/workflows/lib/parallel-handler.js +20 -14
  49. package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
  50. package/dist/modules/workflows/lib/workflow-executor.js +21 -23
  51. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  52. package/package.json +7 -7
  53. package/src/modules/auth/api/logout.ts +6 -1
  54. package/src/modules/auth/api/session/refresh.ts +6 -1
  55. package/src/modules/catalog/ai-tools/mutation-pack.ts +14 -5
  56. package/src/modules/catalog/ai-tools/types.ts +1 -0
  57. package/src/modules/customer_accounts/api/login.ts +15 -7
  58. package/src/modules/customer_accounts/services/customerUserService.ts +34 -10
  59. package/src/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.ts +1 -1
  60. package/src/modules/forms/api/form-submissions/[id]/submit/route.ts +16 -3
  61. package/src/modules/forms/api/form-submissions/route.ts +21 -2
  62. package/src/modules/forms/api/public/submissions/[id]/submit/route.ts +7 -0
  63. package/src/modules/forms/api/runtime-helpers.ts +16 -0
  64. package/src/modules/forms/data/validators.ts +2 -1
  65. package/src/modules/forms/i18n/de.json +1 -0
  66. package/src/modules/forms/i18n/en.json +1 -0
  67. package/src/modules/forms/i18n/es.json +1 -0
  68. package/src/modules/forms/i18n/ko.json +1 -0
  69. package/src/modules/forms/i18n/pl.json +1 -0
  70. package/src/modules/forms/lib/portal-subject-policy.ts +42 -0
  71. package/src/modules/forms/lib/runtime-principal.ts +35 -11
  72. package/src/modules/forms/services/distribution-service.ts +35 -13
  73. package/src/modules/forms/services/submission-service.ts +12 -0
  74. package/src/modules/staff/commands/leave-requests.ts +54 -23
  75. package/src/modules/staff/commands/timesheets-entries.ts +4 -0
  76. package/src/modules/staff/commands/timesheets-tags.ts +37 -8
  77. package/src/modules/staff/i18n/de.json +2 -0
  78. package/src/modules/staff/i18n/en.json +2 -0
  79. package/src/modules/staff/i18n/es.json +2 -0
  80. package/src/modules/staff/i18n/ko.json +2 -0
  81. package/src/modules/staff/i18n/pl.json +2 -0
  82. package/src/modules/warranty_claims/api/portal/attachments/route.ts +20 -8
  83. package/src/modules/warranty_claims/commands/claims.ts +1 -1
  84. package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +5 -5
  85. package/src/modules/wms/commands/inventory-actions.ts +30 -10
  86. package/src/modules/workflows/lib/async-activity-wait.ts +71 -0
  87. package/src/modules/workflows/lib/parallel-handler.ts +24 -15
  88. package/src/modules/workflows/lib/workflow-executor.ts +24 -27
@@ -1,4 +1,4 @@
1
- [build:core] found 5734 entry points
1
+ [build:core] found 5745 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 252 entry points
4
4
  [build:core:generated] built successfully
@@ -11,7 +11,12 @@ const emptyAuthTokenClaims = Object.freeze({
11
11
  function parseCookie(req, name) {
12
12
  const cookie = req.headers.get("cookie") || "";
13
13
  const m = cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]+)"));
14
- return m ? decodeURIComponent(m[1]) : null;
14
+ if (!m) return null;
15
+ try {
16
+ return decodeURIComponent(m[1]);
17
+ } catch {
18
+ return null;
19
+ }
15
20
  }
16
21
  function readStringClaim(payload, key) {
17
22
  const value = payload[key];
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/api/logout.ts"],
4
- "sourcesContent": ["import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { buildSafeRedirectResponse } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { emitAuthEvent } from '@open-mercato/core/modules/auth/events'\n\ntype AuthTokenClaims = {\n userId: string | null\n sessionId: string | null\n tenantId: string | null\n organizationId: string | null\n}\n\nconst emptyAuthTokenClaims: AuthTokenClaims = Object.freeze({\n userId: null,\n sessionId: null,\n tenantId: null,\n organizationId: null,\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nfunction readStringClaim(payload: Record<string, unknown>, key: string): string | null {\n const value = payload[key]\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction extractAuthTokenClaims(token: string | null): AuthTokenClaims {\n if (!token) return emptyAuthTokenClaims\n try {\n const payload = verifyJwt(token) as Record<string, unknown> | null\n if (!payload) return emptyAuthTokenClaims\n return {\n userId: readStringClaim(payload, 'sub'),\n sessionId: readStringClaim(payload, 'sid'),\n tenantId: readStringClaim(payload, 'tenantId'),\n organizationId: readStringClaim(payload, 'orgId'),\n }\n } catch {\n return emptyAuthTokenClaims\n }\n}\n\nexport async function POST(req: Request) {\n const sessToken = parseCookie(req, 'session_token')\n const authToken = parseCookie(req, 'auth_token')\n const claims = extractAuthTokenClaims(authToken)\n let sessionRevoked = false\n if (sessToken || claims.sessionId) {\n try {\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n if (claims.sessionId) {\n await auth.deleteSessionById(claims.sessionId)\n }\n if (sessToken) {\n await auth.deleteSessionByToken(sessToken)\n }\n sessionRevoked = true\n } catch {}\n }\n if (claims.userId) {\n void emitAuthEvent('auth.logout', {\n id: claims.userId,\n tenantId: claims.tenantId,\n organizationId: claims.organizationId,\n sessionId: claims.sessionId,\n sessionRevoked,\n at: new Date().toISOString(),\n }, { persistent: true }).catch(() => undefined)\n }\n const res = buildSafeRedirectResponse(req, '/login')\n res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n res.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n return res\n}\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Log out current session',\n methods: {\n POST: {\n summary: 'Invalidate session and redirect',\n description: 'Clears authentication cookies and redirects the browser to the login page.',\n responses: [\n { status: 302, description: 'Redirect to login after successful logout', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AACA,SAAS,8BAA8B;AAEvC,SAAS,iBAAiB;AAC1B,SAAS,iCAAiC;AAC1C,SAAS,qBAAqB;AAS9B,MAAM,uBAAwC,OAAO,OAAO;AAAA,EAC1D,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,UAAU;AAAA,EACV,gBAAgB;AAClB,CAAC;AAED,SAAS,YAAY,KAAc,MAA6B;AAC9D,QAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC5C,QAAM,IAAI,OAAO,MAAM,IAAI,OAAO,gBAAgB,OAAO,UAAU,CAAC;AACpE,SAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,IAAI;AACxC;AAEA,SAAS,gBAAgB,SAAkC,KAA4B;AACrF,QAAM,QAAQ,QAAQ,GAAG;AACzB,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEA,SAAS,uBAAuB,OAAuC;AACrE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO;AAAA,MACL,QAAQ,gBAAgB,SAAS,KAAK;AAAA,MACtC,WAAW,gBAAgB,SAAS,KAAK;AAAA,MACzC,UAAU,gBAAgB,SAAS,UAAU;AAAA,MAC7C,gBAAgB,gBAAgB,SAAS,OAAO;AAAA,IAClD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,YAAY,YAAY,KAAK,eAAe;AAClD,QAAM,YAAY,YAAY,KAAK,YAAY;AAC/C,QAAM,SAAS,uBAAuB,SAAS;AAC/C,MAAI,iBAAiB;AACrB,MAAI,aAAa,OAAO,WAAW;AACjC,QAAI;AACF,YAAM,IAAI,MAAM,uBAAuB;AACvC,YAAM,OAAO,EAAE,QAAqB,aAAa;AACjD,UAAI,OAAO,WAAW;AACpB,cAAM,KAAK,kBAAkB,OAAO,SAAS;AAAA,MAC/C;AACA,UAAI,WAAW;AACb,cAAM,KAAK,qBAAqB,SAAS;AAAA,MAC3C;AACA,uBAAiB;AAAA,IACnB,QAAQ;AAAA,IAAC;AAAA,EACX;AACA,MAAI,OAAO,QAAQ;AACjB,SAAK,cAAc,eAAe;AAAA,MAChC,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,WAAW,OAAO;AAAA,MAClB;AAAA,MACA,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC7B,GAAG,EAAE,YAAY,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EAChD;AACA,QAAM,MAAM,0BAA0B,KAAK,QAAQ;AACnD,MAAI,QAAQ,IAAI,cAAc,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC1D,MAAI,QAAQ,IAAI,iBAAiB,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC7D,SAAO;AACT;AAEO,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,6CAA6C,WAAW,YAAY;AAAA,MAClG;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { buildSafeRedirectResponse } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { emitAuthEvent } from '@open-mercato/core/modules/auth/events'\n\ntype AuthTokenClaims = {\n userId: string | null\n sessionId: string | null\n tenantId: string | null\n organizationId: string | null\n}\n\nconst emptyAuthTokenClaims: AuthTokenClaims = Object.freeze({\n userId: null,\n sessionId: null,\n tenantId: null,\n organizationId: null,\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n if (!m) return null\n try {\n return decodeURIComponent(m[1])\n } catch {\n return null\n }\n}\n\nfunction readStringClaim(payload: Record<string, unknown>, key: string): string | null {\n const value = payload[key]\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction extractAuthTokenClaims(token: string | null): AuthTokenClaims {\n if (!token) return emptyAuthTokenClaims\n try {\n const payload = verifyJwt(token) as Record<string, unknown> | null\n if (!payload) return emptyAuthTokenClaims\n return {\n userId: readStringClaim(payload, 'sub'),\n sessionId: readStringClaim(payload, 'sid'),\n tenantId: readStringClaim(payload, 'tenantId'),\n organizationId: readStringClaim(payload, 'orgId'),\n }\n } catch {\n return emptyAuthTokenClaims\n }\n}\n\nexport async function POST(req: Request) {\n const sessToken = parseCookie(req, 'session_token')\n const authToken = parseCookie(req, 'auth_token')\n const claims = extractAuthTokenClaims(authToken)\n let sessionRevoked = false\n if (sessToken || claims.sessionId) {\n try {\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n if (claims.sessionId) {\n await auth.deleteSessionById(claims.sessionId)\n }\n if (sessToken) {\n await auth.deleteSessionByToken(sessToken)\n }\n sessionRevoked = true\n } catch {}\n }\n if (claims.userId) {\n void emitAuthEvent('auth.logout', {\n id: claims.userId,\n tenantId: claims.tenantId,\n organizationId: claims.organizationId,\n sessionId: claims.sessionId,\n sessionRevoked,\n at: new Date().toISOString(),\n }, { persistent: true }).catch(() => undefined)\n }\n const res = buildSafeRedirectResponse(req, '/login')\n res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n res.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n return res\n}\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Log out current session',\n methods: {\n POST: {\n summary: 'Invalidate session and redirect',\n description: 'Clears authentication cookies and redirects the browser to the login page.',\n responses: [\n { status: 302, description: 'Redirect to login after successful logout', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AACA,SAAS,8BAA8B;AAEvC,SAAS,iBAAiB;AAC1B,SAAS,iCAAiC;AAC1C,SAAS,qBAAqB;AAS9B,MAAM,uBAAwC,OAAO,OAAO;AAAA,EAC1D,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,UAAU;AAAA,EACV,gBAAgB;AAClB,CAAC;AAED,SAAS,YAAY,KAAc,MAA6B;AAC9D,QAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC5C,QAAM,IAAI,OAAO,MAAM,IAAI,OAAO,gBAAgB,OAAO,UAAU,CAAC;AACpE,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,WAAO,mBAAmB,EAAE,CAAC,CAAC;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,gBAAgB,SAAkC,KAA4B;AACrF,QAAM,QAAQ,QAAQ,GAAG;AACzB,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEA,SAAS,uBAAuB,OAAuC;AACrE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO;AAAA,MACL,QAAQ,gBAAgB,SAAS,KAAK;AAAA,MACtC,WAAW,gBAAgB,SAAS,KAAK;AAAA,MACzC,UAAU,gBAAgB,SAAS,UAAU;AAAA,MAC7C,gBAAgB,gBAAgB,SAAS,OAAO;AAAA,IAClD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,YAAY,YAAY,KAAK,eAAe;AAClD,QAAM,YAAY,YAAY,KAAK,YAAY;AAC/C,QAAM,SAAS,uBAAuB,SAAS;AAC/C,MAAI,iBAAiB;AACrB,MAAI,aAAa,OAAO,WAAW;AACjC,QAAI;AACF,YAAM,IAAI,MAAM,uBAAuB;AACvC,YAAM,OAAO,EAAE,QAAqB,aAAa;AACjD,UAAI,OAAO,WAAW;AACpB,cAAM,KAAK,kBAAkB,OAAO,SAAS;AAAA,MAC/C;AACA,UAAI,WAAW;AACb,cAAM,KAAK,qBAAqB,SAAS;AAAA,MAC3C;AACA,uBAAiB;AAAA,IACnB,QAAQ;AAAA,IAAC;AAAA,EACX;AACA,MAAI,OAAO,QAAQ;AACjB,SAAK,cAAc,eAAe;AAAA,MAChC,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,WAAW,OAAO;AAAA,MAClB;AAAA,MACA,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC7B,GAAG,EAAE,YAAY,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EAChD;AACA,QAAM,MAAM,0BAA0B,KAAK,QAAQ;AACnD,MAAI,QAAQ,IAAI,cAAc,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC1D,MAAI,QAAQ,IAAI,iBAAiB,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC7D,SAAO;AACT;AAEO,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,6CAA6C,WAAW,YAAY;AAAA,MAClG;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -24,7 +24,12 @@ const refreshIpRateLimitConfig = readEndpointRateLimitConfig("REFRESH_IP", {
24
24
  function parseCookie(req, name) {
25
25
  const cookie = req.headers.get("cookie") || "";
26
26
  const m = cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]+)"));
27
- return m ? decodeURIComponent(m[1]) : null;
27
+ if (!m) return null;
28
+ try {
29
+ return decodeURIComponent(m[1]);
30
+ } catch {
31
+ return null;
32
+ }
28
33
  }
29
34
  function carriesMfaPendingToken(req) {
30
35
  const authHeader = (req.headers.get("authorization") || "").trim();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/auth/api/session/refresh.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { isMfaPendingJwtPayload, signJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { z } from 'zod'\n\nconst refreshRateLimitConfig = readEndpointRateLimitConfig('REFRESH', {\n points: 15, duration: 60, blockDuration: 60, keyPrefix: 'refresh',\n})\nconst refreshIpRateLimitConfig = readEndpointRateLimitConfig('REFRESH_IP', {\n points: 60, duration: 60, blockDuration: 60, keyPrefix: 'refresh-ip',\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\n// Both handlers are `requireAuth: false`, so the dispatcher's MFA-pending gate never inspects\n// this route's caller. Minting a full staff JWT for a browser that is still holding a provisional\n// `mfa_pending` token would hand it the access the outstanding second factor is meant to withhold,\n// so the pending credential is checked here directly. `refreshFromSessionToken` itself has no MFA\n// awareness \u2014 it validates only the token hash and expiry.\nfunction carriesMfaPendingToken(req: Request): boolean {\n const authHeader = (req.headers.get('authorization') || '').trim()\n const bearer = authHeader.toLowerCase().startsWith('bearer ') ? authHeader.slice(7).trim() : null\n const token = bearer || parseCookie(req, 'auth_token')\n if (!token) return false\n try {\n return isMfaPendingJwtPayload(verifyJwt(token))\n } catch {\n return false\n }\n}\n\ntype RefreshedSession = NonNullable<Awaited<ReturnType<AuthService['refreshFromSessionToken']>>>\n\n// Scope claims must stay absent rather than stringified when the user has no tenant/org:\n// `String(null)` yields the literal \"null\", which is not a UUID, so session-integrity\n// resolution rejects the token it just minted and the caller is stuck in a refresh loop.\n// Mirrors how `api/login.ts` builds the same claims.\nfunction buildStaffJwtClaims({ user, roles, session }: RefreshedSession) {\n return {\n sub: String(user.id),\n sid: session ? String(session.id) : undefined,\n tenantId: user.tenantId ? String(user.tenantId) : null,\n orgId: user.organizationId ? String(user.organizationId) : null,\n email: user.email,\n roles,\n }\n}\n\nfunction clearStaffAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n response.cookies.set('session_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin\n const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/')\n if (carriesMfaPendingToken(req)) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const token = parseCookie(req, 'session_token')\n if (!token) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const jwt = signJwt(buildStaffJwtClaims(ctx))\n const res = buildSafeRedirectResponse(req, redirectTo)\n res.cookies.set('auth_token', jwt, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })\n return res\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n let token: string | null = null\n\n try {\n const body = await req.json()\n const parsed = refreshSessionRequestSchema.safeParse(body)\n if (parsed.success) {\n token = parsed.data.refreshToken\n }\n } catch {\n // Invalid JSON\n }\n\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: refreshIpRateLimitConfig,\n compoundConfig: refreshRateLimitConfig,\n compoundIdentifier: token ?? undefined,\n })\n if (rateLimitError) return rateLimitError\n\n if (carriesMfaPendingToken(req)) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n )\n }\n\n if (!token) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidPayload', 'Missing or invalid refresh token'),\n }, { status: 400 })\n )\n }\n\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n\n if (!ctx) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n )\n }\n\n const jwt = signJwt(buildStaffJwtClaims(ctx))\n\n const res = NextResponse.json({\n ok: true,\n accessToken: jwt,\n expiresIn: 60 * 60 * 8,\n })\n\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n\n return res\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nconst refreshQuerySchema = z.object({\n redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),\n})\n\nconst refreshSuccessSchema = z.object({\n ok: z.literal(true),\n accessToken: z.string().describe('New JWT access token'),\n expiresIn: z.number().describe('Token expiration time in seconds'),\n})\n\nconst refreshErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Refresh session token',\n methods: {\n GET: {\n summary: 'Refresh auth cookie from session token (browser)',\n description: 'Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.',\n query: refreshQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to target location when session is valid', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Refresh access token (API/mobile)',\n description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',\n requestBody: { schema: refreshSessionRequestSchema, contentType: 'application/json' },\n responses: [\n { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },\n { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },\n { status: 429, description: 'Too many refresh attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { isMfaPendingJwtPayload, signJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { z } from 'zod'\n\nconst refreshRateLimitConfig = readEndpointRateLimitConfig('REFRESH', {\n points: 15, duration: 60, blockDuration: 60, keyPrefix: 'refresh',\n})\nconst refreshIpRateLimitConfig = readEndpointRateLimitConfig('REFRESH_IP', {\n points: 60, duration: 60, blockDuration: 60, keyPrefix: 'refresh-ip',\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n if (!m) return null\n try {\n return decodeURIComponent(m[1])\n } catch {\n return null\n }\n}\n\n// Both handlers are `requireAuth: false`, so the dispatcher's MFA-pending gate never inspects\n// this route's caller. Minting a full staff JWT for a browser that is still holding a provisional\n// `mfa_pending` token would hand it the access the outstanding second factor is meant to withhold,\n// so the pending credential is checked here directly. `refreshFromSessionToken` itself has no MFA\n// awareness \u2014 it validates only the token hash and expiry.\nfunction carriesMfaPendingToken(req: Request): boolean {\n const authHeader = (req.headers.get('authorization') || '').trim()\n const bearer = authHeader.toLowerCase().startsWith('bearer ') ? authHeader.slice(7).trim() : null\n const token = bearer || parseCookie(req, 'auth_token')\n if (!token) return false\n try {\n return isMfaPendingJwtPayload(verifyJwt(token))\n } catch {\n return false\n }\n}\n\ntype RefreshedSession = NonNullable<Awaited<ReturnType<AuthService['refreshFromSessionToken']>>>\n\n// Scope claims must stay absent rather than stringified when the user has no tenant/org:\n// `String(null)` yields the literal \"null\", which is not a UUID, so session-integrity\n// resolution rejects the token it just minted and the caller is stuck in a refresh loop.\n// Mirrors how `api/login.ts` builds the same claims.\nfunction buildStaffJwtClaims({ user, roles, session }: RefreshedSession) {\n return {\n sub: String(user.id),\n sid: session ? String(session.id) : undefined,\n tenantId: user.tenantId ? String(user.tenantId) : null,\n orgId: user.organizationId ? String(user.organizationId) : null,\n email: user.email,\n roles,\n }\n}\n\nfunction clearStaffAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n response.cookies.set('session_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin\n const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/')\n if (carriesMfaPendingToken(req)) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const token = parseCookie(req, 'session_token')\n if (!token) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const jwt = signJwt(buildStaffJwtClaims(ctx))\n const res = buildSafeRedirectResponse(req, redirectTo)\n res.cookies.set('auth_token', jwt, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })\n return res\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n let token: string | null = null\n\n try {\n const body = await req.json()\n const parsed = refreshSessionRequestSchema.safeParse(body)\n if (parsed.success) {\n token = parsed.data.refreshToken\n }\n } catch {\n // Invalid JSON\n }\n\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: refreshIpRateLimitConfig,\n compoundConfig: refreshRateLimitConfig,\n compoundIdentifier: token ?? undefined,\n })\n if (rateLimitError) return rateLimitError\n\n if (carriesMfaPendingToken(req)) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n )\n }\n\n if (!token) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidPayload', 'Missing or invalid refresh token'),\n }, { status: 400 })\n )\n }\n\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n\n if (!ctx) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n )\n }\n\n const jwt = signJwt(buildStaffJwtClaims(ctx))\n\n const res = NextResponse.json({\n ok: true,\n accessToken: jwt,\n expiresIn: 60 * 60 * 8,\n })\n\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n\n return res\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nconst refreshQuerySchema = z.object({\n redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),\n})\n\nconst refreshSuccessSchema = z.object({\n ok: z.literal(true),\n accessToken: z.string().describe('New JWT access token'),\n expiresIn: z.number().describe('Token expiration time in seconds'),\n})\n\nconst refreshErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Refresh session token',\n methods: {\n GET: {\n summary: 'Refresh auth cookie from session token (browser)',\n description: 'Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.',\n query: refreshQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to target location when session is valid', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Refresh access token (API/mobile)',\n description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',\n requestBody: { schema: refreshSessionRequestSchema, contentType: 'application/json' },\n responses: [\n { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },\n { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },\n { status: 429, description: 'Too many refresh attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -476,6 +476,12 @@ function mediaLabel(row) {
476
476
  if (row.fileName && row.fileName.trim().length) return row.fileName;
477
477
  return row.id;
478
478
  }
479
+ function mediaUpdatePatch(entry) {
480
+ return {
481
+ ...entry.altText !== void 0 ? { altText: entry.altText } : {},
482
+ ...entry.caption !== void 0 ? { caption: entry.caption } : {}
483
+ };
484
+ }
479
485
  const updateProductMediaDescriptionsTool = {
480
486
  name: "catalog.update_product_media_descriptions",
481
487
  displayName: "Update product media descriptions",
@@ -493,12 +499,14 @@ const updateProductMediaDescriptionsTool = {
493
499
  for (const entry of input.mediaUpdates) {
494
500
  const media = await loadProductMediaForScope(em, ctx, tenantId, entry.mediaId);
495
501
  if (!media) continue;
502
+ const before = mediaSnapshot(media);
496
503
  rows.push({
497
504
  recordId: media.id,
498
505
  entityType: "catalog.product_media",
499
506
  label: mediaLabel(media),
500
507
  recordVersion: recordVersionFromUpdatedAt(media.createdAt ?? null),
501
- before: mediaSnapshot(media)
508
+ before,
509
+ after: { ...before, ...mediaUpdatePatch(entry) }
502
510
  });
503
511
  }
504
512
  return rows;
@@ -526,10 +534,7 @@ const updateProductMediaDescriptionsTool = {
526
534
  const before = mediaSnapshot(media);
527
535
  touched.push({
528
536
  row: media,
529
- patch: {
530
- ...entry.altText !== void 0 ? { altText: entry.altText } : {},
531
- ...entry.caption !== void 0 ? { caption: entry.caption } : {}
532
- },
537
+ patch: mediaUpdatePatch(entry),
533
538
  before
534
539
  });
535
540
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/catalog/ai-tools/mutation-pack.ts"],
4
- "sourcesContent": ["/**\n * Catalog D18 mutation tool pack (Phase 3 WS-C, Step 5.14).\n *\n * Ships the four mutation tools the `catalog.merchandising_assistant`\n * (Step 4.9) whitelists under the pending-action approval contract:\n *\n * 1. `catalog.update_product` \u2014 single-record write (isBulk=false).\n * 2. `catalog.bulk_update_products` \u2014 batch write (isBulk=true). One\n * pending action per batch; per-record versions carried in\n * `records[]` so Step 5.8's confirm route can reject stale rows\n * individually without aborting the batch.\n * 3. `catalog.apply_attribute_extraction` \u2014 batch write that persists\n * the structured output of `catalog.extract_attributes_from_description`\n * (Step 3.12) against the CURRENT attribute schema. Schema drift is\n * caught at two points: (a) the tool's pre-submit validator rejects\n * attribute keys that do not resolve to a custom-field definition,\n * and (b) the Step 5.8 re-check loop calls `loadBeforeRecords` again\n * before confirm.\n * 4. `catalog.update_product_media_descriptions` \u2014 batch write that\n * updates altText / caption on product media attachments via the\n * attachment's `storageMetadata` envelope. No attachments command\n * exists yet, so the handler performs a guarded direct EM flush.\n * The writes are scalar only (no intermediate queries on the same EM)\n * so `em.flush()` in a single phase is safe and matches the spec's\n * \"single pending-action per batch\" contract.\n *\n * All four tools:\n * - Are tenant + organization scoped through `findOneWithDecryption` /\n * `findWithDecryption` before any write.\n * - Use `z.object(...).strict()` on their input schemas (spec \u00A77 rule:\n * reject hallucinated attribute names / unknown fields).\n * - Preserve per-record result shape when only some records fail:\n * handler returns `{ records: [{ recordId, status, before, after, error? }], failedRecordIds }`\n * so Step 5.8's executor can populate `failedRecords[]` on the\n * `AiPendingAction` row without the caller treating a partial failure\n * as a batch failure.\n * - Delegate the authoritative write to an existing command\n * (`catalog.products.update`) where possible, so all downstream side\n * effects (audit log, `catalog.product.updated` event, query index\n * refresh, notifications) stay identical to a direct API write. The\n * media-description tool is the one exception: it updates\n * `attachments.storage_metadata` directly because no attachments\n * command exists. Tenant + organization scope are re-checked inside\n * the direct write.\n *\n * BC: additive only. `CatalogAiToolDefinition` grows optional\n * `loadBeforeRecord` / `loadBeforeRecords` / `isBulk` fields that default\n * to undefined on every pre-5.14 tool. No new feature ids, no DB\n * migrations, no event-id changes. Mutation-policy override at the\n * tenant level is still the only lever that unlocks writes \u2014 agent\n * `readOnly: true` stays unchanged (Step 5.13 discipline).\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport { loadCustomFieldDefinitionIndex } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n createAiApiOperationRunner,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { CatalogProduct, CatalogProductPrice } from '../data/entities'\nimport { Attachment } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n assertTenantScope,\n type CatalogAiToolDefinition,\n type CatalogToolContext,\n type CatalogToolLoadBeforeRecord,\n type CatalogToolLoadBeforeSingleRecord,\n} from './types'\n\nfunction resolveEm(ctx: CatalogToolContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CatalogToolContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nfunction recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string | null {\n if (!updatedAt) return null\n const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt)\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n}\n\nasync function loadProductForScope(\n em: EntityManager,\n ctx: CatalogToolContext,\n tenantId: string,\n productId: string,\n): Promise<CatalogProduct | null> {\n const where: Record<string, unknown> = { id: productId, tenantId, deletedAt: null }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const row = await findOneWithDecryption<CatalogProduct>(\n em,\n CatalogProduct,\n where as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!row || row.tenantId !== tenantId) return null\n if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null\n return row\n}\n\nfunction productSnapshot(row: CatalogProduct): Record<string, unknown> {\n return {\n title: row.title ?? null,\n subtitle: row.subtitle ?? null,\n description: row.description ?? null,\n sku: row.sku ?? null,\n handle: row.handle ?? null,\n isActive: row.isActive ?? null,\n primaryCurrencyCode: row.primaryCurrencyCode ?? null,\n }\n}\n\nfunction productLabel(row: CatalogProduct): string {\n if (row.title && row.title.trim().length) return row.title\n if (row.sku && row.sku.trim().length) return row.sku\n return row.id\n}\n\n/* -------------------------------------------------------------------------- */\n/* Price pre-submit validation */\n/* -------------------------------------------------------------------------- */\n\nconst pricePatchSchema = z\n .object({\n priceKindId: z.string().uuid().optional(),\n currencyCode: z.string().trim().min(3).max(8).optional(),\n amount: z.number().nonnegative(),\n })\n .strict()\n\nasync function validateProductPriceScope(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n productId: string,\n priceInput: z.infer<typeof pricePatchSchema>,\n): Promise<{ ok: true } | { ok: false; code: string; message: string }> {\n if (priceInput.currencyCode) {\n const candidates = await findWithDecryption<CatalogProductPrice>(\n em,\n CatalogProductPrice,\n { tenantId, product: productId } as any,\n undefined,\n { tenantId, organizationId },\n )\n const seen = new Set<string>(candidates.map((entry) => (entry.currencyCode ?? '').toUpperCase()))\n if (seen.size > 0 && !seen.has(priceInput.currencyCode.toUpperCase())) {\n return {\n ok: false,\n code: 'currency_out_of_scope',\n message: `Currency \"${priceInput.currencyCode}\" is not configured for product ${productId}.`,\n }\n }\n }\n return { ok: true }\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.update_product (single-record) */\n/* -------------------------------------------------------------------------- */\n\nconst updateProductInput = z\n .object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n title: z.string().trim().min(1).max(255).optional(),\n subtitle: z.string().trim().max(255).nullable().optional(),\n description: z.string().trim().max(4000).nullable().optional(),\n isActive: z.boolean().optional(),\n price: pricePatchSchema.optional().describe('Optional price adjustment; must match an existing currency for the product.'),\n })\n .strict()\n\ntype UpdateProductInput = z.infer<typeof updateProductInput>\n\nconst updateProductTool: CatalogAiToolDefinition = {\n name: 'catalog.update_product',\n displayName: 'Update product',\n description:\n 'Update a single catalog product (title, subtitle, description, isActive, optional price). Routes through the AI pending-action approval gate.',\n inputSchema: updateProductInput as z.ZodType<unknown>,\n requiredFeatures: ['catalog.products.manage'],\n tags: ['write', 'catalog', 'merchandising'],\n isMutation: true,\n isBulk: false,\n loadBeforeRecord: async (\n rawInput,\n ctx,\n ): Promise<CatalogToolLoadBeforeSingleRecord | null> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateProductInput = updateProductInput.parse(rawInput)\n const em = resolveEm(ctx)\n const row = await loadProductForScope(em, ctx, tenantId, input.productId)\n if (!row) return null\n return {\n recordId: row.id,\n entityType: 'catalog.product',\n recordVersion: recordVersionFromUpdatedAt(row.updatedAt),\n before: productSnapshot(row),\n }\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateProductInput = updateProductInput.parse(rawInput)\n const em = resolveEm(ctx)\n const row = await loadProductForScope(em, ctx, tenantId, input.productId)\n if (!row) {\n throw new Error(`Product \"${input.productId}\" is not accessible to the caller.`)\n }\n const organizationId = row.organizationId\n const before = productSnapshot(row)\n\n if (input.price) {\n const check = await validateProductPriceScope(\n em,\n tenantId,\n organizationId,\n row.id,\n input.price,\n )\n if (!check.ok) {\n const error = new Error(check.message) as Error & { code?: string }\n error.code = check.code\n throw error\n }\n }\n\n const body: Record<string, unknown> = {\n id: row.id,\n tenantId,\n organizationId,\n }\n if (input.title !== undefined) body.title = input.title\n if (input.subtitle !== undefined) body.subtitle = input.subtitle\n if (input.description !== undefined) body.description = input.description\n if (input.isActive !== undefined) body.isActive = input.isActive\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run({\n method: 'PUT',\n path: '/catalog/products',\n body,\n })\n if (!response.success) {\n throw new Error(response.error ?? `Failed to update product \"${row.id}\"`)\n }\n\n const after = await loadProductForScope(em, ctx, tenantId, row.id)\n return {\n recordId: row.id,\n commandName: 'catalog.products.update',\n before,\n after: after ? productSnapshot(after) : null,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.bulk_update_products (batch) */\n/* -------------------------------------------------------------------------- */\n\nconst bulkUpdateProductRecordSchema = z\n .object({\n recordId: z.string().uuid(),\n title: z.string().trim().min(1).max(255).optional(),\n subtitle: z.string().trim().max(255).nullable().optional(),\n description: z.string().trim().max(4000).nullable().optional(),\n isActive: z.boolean().optional(),\n price: pricePatchSchema.optional(),\n })\n .strict()\n\nconst bulkUpdateProductsInput = z\n .object({\n records: z\n .array(bulkUpdateProductRecordSchema)\n .min(1)\n .max(50)\n .describe('One entry per product to update. Max 50 rows per batch.'),\n })\n .strict()\n\ntype BulkUpdateProductsInput = z.infer<typeof bulkUpdateProductsInput>\ntype BulkUpdateProductPatch = z.infer<typeof bulkUpdateProductRecordSchema>\n\ntype BulkRecordResult = {\n recordId: string\n status: 'updated' | 'skipped' | 'failed'\n before: Record<string, unknown> | null\n after: Record<string, unknown> | null\n error?: { code: string; message: string }\n}\n\nconst bulkUpdateProductsTool: CatalogAiToolDefinition = {\n name: 'catalog.bulk_update_products',\n displayName: 'Bulk update products',\n description:\n 'Update several catalog products in a single approval. Emits ONE pending action whose records[] contains one diff per product. Stale-version or cross-tenant rows are collected in failedRecords[] without aborting the remaining writes.',\n inputSchema: bulkUpdateProductsInput as z.ZodType<unknown>,\n requiredFeatures: ['catalog.products.manage'],\n tags: ['write', 'catalog', 'merchandising', 'bulk'],\n isMutation: true,\n isBulk: true,\n loadBeforeRecords: async (rawInput, ctx): Promise<CatalogToolLoadBeforeRecord[]> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: BulkUpdateProductsInput = bulkUpdateProductsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const rows: CatalogToolLoadBeforeRecord[] = []\n for (const entry of input.records) {\n const product = await loadProductForScope(em, ctx, tenantId, entry.recordId)\n if (!product) continue\n rows.push({\n recordId: product.id,\n entityType: 'catalog.product',\n label: productLabel(product),\n recordVersion: recordVersionFromUpdatedAt(product.updatedAt),\n before: productSnapshot(product),\n })\n }\n return rows\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: BulkUpdateProductsInput = bulkUpdateProductsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const results: BulkRecordResult[] = []\n const failedRecordIds: string[] = []\n for (const entry of input.records) {\n const product = await loadProductForScope(em, ctx, tenantId, entry.recordId)\n if (!product) {\n failedRecordIds.push(entry.recordId)\n results.push({\n recordId: entry.recordId,\n status: 'skipped',\n before: null,\n after: null,\n error: { code: 'record_not_found', message: 'Product is not accessible to the caller.' },\n })\n continue\n }\n const organizationId = product.organizationId\n const before = productSnapshot(product)\n if (entry.price) {\n const check = await validateProductPriceScope(em, tenantId, organizationId, product.id, entry.price)\n if (!check.ok) {\n failedRecordIds.push(product.id)\n results.push({\n recordId: product.id,\n status: 'failed',\n before,\n after: null,\n error: { code: check.code, message: check.message },\n })\n continue\n }\n }\n const body: Record<string, unknown> = {\n id: product.id,\n tenantId,\n organizationId,\n }\n if (entry.title !== undefined) body.title = entry.title\n if (entry.subtitle !== undefined) body.subtitle = entry.subtitle\n if (entry.description !== undefined) body.description = entry.description\n if (entry.isActive !== undefined) body.isActive = entry.isActive\n try {\n const response = await runner.run({\n method: 'PUT',\n path: '/catalog/products',\n body,\n })\n if (!response.success) {\n const code =\n typeof (response.details as { code?: unknown } | undefined)?.code === 'string'\n ? ((response.details as { code: string }).code)\n : 'command_failed'\n failedRecordIds.push(product.id)\n results.push({\n recordId: product.id,\n status: 'failed',\n before,\n after: null,\n error: { code, message: response.error ?? 'API operation failed' },\n })\n continue\n }\n const after = await loadProductForScope(em, ctx, tenantId, product.id)\n results.push({\n recordId: product.id,\n status: 'updated',\n before,\n after: after ? productSnapshot(after) : null,\n })\n } catch (error) {\n failedRecordIds.push(product.id)\n const message = error instanceof Error ? error.message : String(error)\n const code = ((error as { code?: unknown })?.code as string | undefined) ?? 'command_failed'\n results.push({\n recordId: product.id,\n status: 'failed',\n before,\n after: null,\n error: { code, message },\n })\n }\n }\n const everyFailed = results.length > 0 && results.every((entry) => entry.status !== 'updated')\n return {\n commandName: 'catalog.products.update',\n records: results,\n failedRecordIds,\n error: everyFailed ? { code: 'all_records_failed', message: 'No records were updated.' } : undefined,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.apply_attribute_extraction (batch) */\n/* -------------------------------------------------------------------------- */\n\nconst attributeExtractionRecordSchema = z\n .object({\n recordId: z.string().uuid(),\n attributes: z.record(z.string(), z.unknown()).describe('Attribute key \u2192 value map produced by catalog.extract_attributes_from_description.'),\n })\n .strict()\n\nconst applyAttributeExtractionInput = z\n .object({\n records: z\n .array(attributeExtractionRecordSchema)\n .min(1)\n .max(50)\n .describe('One extraction payload per product. Max 50 rows per batch.'),\n })\n .strict()\n\ntype ApplyAttributeExtractionInput = z.infer<typeof applyAttributeExtractionInput>\n\nasync function resolveAttributeKeyIndex(\n ctx: CatalogToolContext,\n tenantId: string,\n): Promise<Set<string>> {\n try {\n const em = resolveEm(ctx)\n const index = await loadCustomFieldDefinitionIndex({\n em,\n entityIds: E.catalog.catalog_product as string,\n tenantId,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n })\n const keys = new Set<string>()\n if (index && typeof (index as any).keys === 'function') {\n for (const key of (index as any).keys() as Iterable<string>) {\n keys.add(String(key))\n }\n }\n return keys\n } catch {\n return new Set<string>()\n }\n}\n\nconst applyAttributeExtractionTool: CatalogAiToolDefinition = {\n name: 'catalog.apply_attribute_extraction',\n displayName: 'Apply attribute extraction',\n description:\n 'Persist the structured attribute output of catalog.extract_attributes_from_description. Re-validates every attribute key against the CURRENT catalog.product custom-field schema before the pending action is created \u2014 schema drift trips attribute_not_in_schema.',\n inputSchema: applyAttributeExtractionInput as z.ZodType<unknown>,\n requiredFeatures: ['catalog.products.manage'],\n tags: ['write', 'catalog', 'merchandising', 'attributes'],\n isMutation: true,\n isBulk: true,\n loadBeforeRecords: async (rawInput, ctx): Promise<CatalogToolLoadBeforeRecord[]> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: ApplyAttributeExtractionInput = applyAttributeExtractionInput.parse(rawInput)\n const em = resolveEm(ctx)\n const rows: CatalogToolLoadBeforeRecord[] = []\n for (const entry of input.records) {\n const product = await loadProductForScope(em, ctx, tenantId, entry.recordId)\n if (!product) continue\n rows.push({\n recordId: product.id,\n entityType: 'catalog.product',\n label: productLabel(product),\n recordVersion: recordVersionFromUpdatedAt(product.updatedAt),\n before: { attributes: {} },\n })\n }\n return rows\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: ApplyAttributeExtractionInput = applyAttributeExtractionInput.parse(rawInput)\n const em = resolveEm(ctx)\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const knownKeys = await resolveAttributeKeyIndex(ctx, tenantId)\n const results: BulkRecordResult[] = []\n const failedRecordIds: string[] = []\n for (const entry of input.records) {\n const product = await loadProductForScope(em, ctx, tenantId, entry.recordId)\n if (!product) {\n failedRecordIds.push(entry.recordId)\n results.push({\n recordId: entry.recordId,\n status: 'skipped',\n before: null,\n after: null,\n error: { code: 'record_not_found', message: 'Product is not accessible to the caller.' },\n })\n continue\n }\n const attributeKeys = Object.keys(entry.attributes ?? {})\n const unknownKey = knownKeys.size > 0\n ? attributeKeys.find((key) => !knownKeys.has(key))\n : undefined\n if (unknownKey) {\n failedRecordIds.push(product.id)\n results.push({\n recordId: product.id,\n status: 'failed',\n before: { attributes: {} },\n after: null,\n error: {\n code: 'attribute_not_in_schema',\n message: `Attribute \"${unknownKey}\" is not part of the current catalog.product schema.`,\n },\n })\n continue\n }\n const organizationId = product.organizationId\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(entry.attributes ?? {})) {\n customFields[key] = value\n }\n const body: Record<string, unknown> = {\n id: product.id,\n tenantId,\n organizationId,\n customFields,\n }\n try {\n const response = await runner.run({\n method: 'PUT',\n path: '/catalog/products',\n body,\n })\n if (!response.success) {\n const code =\n typeof (response.details as { code?: unknown } | undefined)?.code === 'string'\n ? ((response.details as { code: string }).code)\n : 'command_failed'\n failedRecordIds.push(product.id)\n results.push({\n recordId: product.id,\n status: 'failed',\n before: { attributes: {} },\n after: null,\n error: { code, message: response.error ?? 'API operation failed' },\n })\n continue\n }\n results.push({\n recordId: product.id,\n status: 'updated',\n before: { attributes: {} },\n after: { attributes: entry.attributes },\n })\n } catch (error) {\n failedRecordIds.push(product.id)\n const message = error instanceof Error ? error.message : String(error)\n const code = ((error as { code?: unknown })?.code as string | undefined) ?? 'command_failed'\n results.push({\n recordId: product.id,\n status: 'failed',\n before: { attributes: {} },\n after: null,\n error: { code, message },\n })\n }\n }\n const everyFailed = results.length > 0 && results.every((entry) => entry.status !== 'updated')\n return {\n commandName: 'catalog.products.update',\n records: results,\n failedRecordIds,\n error: everyFailed ? { code: 'all_records_failed', message: 'No records were updated.' } : undefined,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.update_product_media_descriptions (one-or-many media updates) */\n/* -------------------------------------------------------------------------- */\n\nconst mediaUpdateRecordSchema = z\n .object({\n mediaId: z.string().uuid(),\n altText: z.string().trim().max(500).optional(),\n caption: z.string().trim().max(1000).optional(),\n })\n .strict()\n\nconst updateProductMediaDescriptionsInput = z\n .object({\n mediaUpdates: z\n .array(mediaUpdateRecordSchema)\n .min(1)\n .max(100)\n .describe('One media record descriptor per attachment id. Exactly one entry is allowed for single-record writes; many entries for bulk writes.'),\n })\n .strict()\n\ntype UpdateProductMediaDescriptionsInput = z.infer<typeof updateProductMediaDescriptionsInput>\n\nasync function loadProductMediaForScope(\n em: EntityManager,\n ctx: CatalogToolContext,\n tenantId: string,\n mediaId: string,\n): Promise<Attachment | null> {\n const where: Record<string, unknown> = {\n id: mediaId,\n tenantId,\n entityId: E.catalog.catalog_product,\n }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const row = await findOneWithDecryption<Attachment>(\n em,\n Attachment,\n where as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!row || row.tenantId !== tenantId) return null\n if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null\n return row\n}\n\nfunction mediaSnapshot(row: Attachment): Record<string, unknown> {\n const metadata = row.storageMetadata ?? {}\n return {\n altText: typeof metadata?.altText === 'string' ? metadata.altText : null,\n caption: typeof metadata?.caption === 'string' ? metadata.caption : null,\n }\n}\n\nfunction mediaLabel(row: Attachment): string {\n if (row.fileName && row.fileName.trim().length) return row.fileName\n return row.id\n}\n\nconst updateProductMediaDescriptionsTool: CatalogAiToolDefinition = {\n name: 'catalog.update_product_media_descriptions',\n displayName: 'Update product media descriptions',\n description:\n 'Update altText / caption on one or many product media attachments in a single approval. Emits one pending action with records[] carrying per-media diffs. Accepts one entry for single-record writes or many for bulk writes \u2014 always routes through loadBeforeRecords to guarantee schema-consistent preview.',\n inputSchema: updateProductMediaDescriptionsInput as z.ZodType<unknown>,\n requiredFeatures: ['catalog.products.manage'],\n tags: ['write', 'catalog', 'merchandising', 'media'],\n isMutation: true,\n isBulk: true,\n loadBeforeRecords: async (rawInput, ctx): Promise<CatalogToolLoadBeforeRecord[]> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateProductMediaDescriptionsInput =\n updateProductMediaDescriptionsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const rows: CatalogToolLoadBeforeRecord[] = []\n for (const entry of input.mediaUpdates) {\n const media = await loadProductMediaForScope(em, ctx, tenantId, entry.mediaId)\n if (!media) continue\n rows.push({\n recordId: media.id,\n entityType: 'catalog.product_media',\n label: mediaLabel(media),\n recordVersion: recordVersionFromUpdatedAt(media.createdAt ?? null),\n before: mediaSnapshot(media),\n })\n }\n return rows\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateProductMediaDescriptionsInput =\n updateProductMediaDescriptionsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const results: BulkRecordResult[] = []\n const failedRecordIds: string[] = []\n const touched: Array<{ row: Attachment; patch: { altText?: string; caption?: string }; before: Record<string, unknown> }> = []\n for (const entry of input.mediaUpdates) {\n const media = await loadProductMediaForScope(em, ctx, tenantId, entry.mediaId)\n if (!media) {\n failedRecordIds.push(entry.mediaId)\n results.push({\n recordId: entry.mediaId,\n status: 'skipped',\n before: null,\n after: null,\n error: { code: 'record_not_found', message: 'Media is not accessible to the caller.' },\n })\n continue\n }\n const before = mediaSnapshot(media)\n touched.push({\n row: media,\n patch: {\n ...(entry.altText !== undefined ? { altText: entry.altText } : {}),\n ...(entry.caption !== undefined ? { caption: entry.caption } : {}),\n },\n before,\n })\n }\n if (touched.length > 0) {\n for (const { row, patch } of touched) {\n const next = { ...(row.storageMetadata ?? {}) } as Record<string, unknown>\n if (patch.altText !== undefined) next.altText = patch.altText\n if (patch.caption !== undefined) next.caption = patch.caption\n row.storageMetadata = next\n }\n await em.flush()\n for (const { row, patch, before } of touched) {\n results.push({\n recordId: row.id,\n status: 'updated',\n before,\n after: {\n altText: patch.altText ?? (before as any).altText ?? null,\n caption: patch.caption ?? (before as any).caption ?? null,\n },\n })\n }\n }\n const everyFailed = results.length > 0 && results.every((entry) => entry.status !== 'updated')\n return {\n commandName: 'catalog.products.media.update_descriptions',\n records: results,\n failedRecordIds,\n error: everyFailed ? { code: 'all_records_failed', message: 'No media records were updated.' } : undefined,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n\nexport const mutationAiTools: CatalogAiToolDefinition[] = [\n updateProductTool,\n bulkUpdateProductsTool,\n applyAttributeExtractionTool,\n updateProductMediaDescriptionsTool,\n]\n\nexport default mutationAiTools\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Catalog D18 mutation tool pack (Phase 3 WS-C, Step 5.14).\n *\n * Ships the four mutation tools the `catalog.merchandising_assistant`\n * (Step 4.9) whitelists under the pending-action approval contract:\n *\n * 1. `catalog.update_product` \u2014 single-record write (isBulk=false).\n * 2. `catalog.bulk_update_products` \u2014 batch write (isBulk=true). One\n * pending action per batch; per-record versions carried in\n * `records[]` so Step 5.8's confirm route can reject stale rows\n * individually without aborting the batch.\n * 3. `catalog.apply_attribute_extraction` \u2014 batch write that persists\n * the structured output of `catalog.extract_attributes_from_description`\n * (Step 3.12) against the CURRENT attribute schema. Schema drift is\n * caught at two points: (a) the tool's pre-submit validator rejects\n * attribute keys that do not resolve to a custom-field definition,\n * and (b) the Step 5.8 re-check loop calls `loadBeforeRecords` again\n * before confirm.\n * 4. `catalog.update_product_media_descriptions` \u2014 batch write that\n * updates altText / caption on product media attachments via the\n * attachment's `storageMetadata` envelope. No attachments command\n * exists yet, so the handler performs a guarded direct EM flush.\n * The writes are scalar only (no intermediate queries on the same EM)\n * so `em.flush()` in a single phase is safe and matches the spec's\n * \"single pending-action per batch\" contract.\n *\n * All four tools:\n * - Are tenant + organization scoped through `findOneWithDecryption` /\n * `findWithDecryption` before any write.\n * - Use `z.object(...).strict()` on their input schemas (spec \u00A77 rule:\n * reject hallucinated attribute names / unknown fields).\n * - Preserve per-record result shape when only some records fail:\n * handler returns `{ records: [{ recordId, status, before, after, error? }], failedRecordIds }`\n * so Step 5.8's executor can populate `failedRecords[]` on the\n * `AiPendingAction` row without the caller treating a partial failure\n * as a batch failure.\n * - Delegate the authoritative write to an existing command\n * (`catalog.products.update`) where possible, so all downstream side\n * effects (audit log, `catalog.product.updated` event, query index\n * refresh, notifications) stay identical to a direct API write. The\n * media-description tool is the one exception: it updates\n * `attachments.storage_metadata` directly because no attachments\n * command exists. Tenant + organization scope are re-checked inside\n * the direct write.\n *\n * BC: additive only. `CatalogAiToolDefinition` grows optional\n * `loadBeforeRecord` / `loadBeforeRecords` / `isBulk` fields that default\n * to undefined on every pre-5.14 tool. No new feature ids, no DB\n * migrations, no event-id changes. Mutation-policy override at the\n * tenant level is still the only lever that unlocks writes \u2014 agent\n * `readOnly: true` stays unchanged (Step 5.13 discipline).\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport { loadCustomFieldDefinitionIndex } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n createAiApiOperationRunner,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { CatalogProduct, CatalogProductPrice } from '../data/entities'\nimport { Attachment } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n assertTenantScope,\n type CatalogAiToolDefinition,\n type CatalogToolContext,\n type CatalogToolLoadBeforeRecord,\n type CatalogToolLoadBeforeSingleRecord,\n} from './types'\n\nfunction resolveEm(ctx: CatalogToolContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CatalogToolContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nfunction recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string | null {\n if (!updatedAt) return null\n const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt)\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n}\n\nasync function loadProductForScope(\n em: EntityManager,\n ctx: CatalogToolContext,\n tenantId: string,\n productId: string,\n): Promise<CatalogProduct | null> {\n const where: Record<string, unknown> = { id: productId, tenantId, deletedAt: null }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const row = await findOneWithDecryption<CatalogProduct>(\n em,\n CatalogProduct,\n where as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!row || row.tenantId !== tenantId) return null\n if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null\n return row\n}\n\nfunction productSnapshot(row: CatalogProduct): Record<string, unknown> {\n return {\n title: row.title ?? null,\n subtitle: row.subtitle ?? null,\n description: row.description ?? null,\n sku: row.sku ?? null,\n handle: row.handle ?? null,\n isActive: row.isActive ?? null,\n primaryCurrencyCode: row.primaryCurrencyCode ?? null,\n }\n}\n\nfunction productLabel(row: CatalogProduct): string {\n if (row.title && row.title.trim().length) return row.title\n if (row.sku && row.sku.trim().length) return row.sku\n return row.id\n}\n\n/* -------------------------------------------------------------------------- */\n/* Price pre-submit validation */\n/* -------------------------------------------------------------------------- */\n\nconst pricePatchSchema = z\n .object({\n priceKindId: z.string().uuid().optional(),\n currencyCode: z.string().trim().min(3).max(8).optional(),\n amount: z.number().nonnegative(),\n })\n .strict()\n\nasync function validateProductPriceScope(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n productId: string,\n priceInput: z.infer<typeof pricePatchSchema>,\n): Promise<{ ok: true } | { ok: false; code: string; message: string }> {\n if (priceInput.currencyCode) {\n const candidates = await findWithDecryption<CatalogProductPrice>(\n em,\n CatalogProductPrice,\n { tenantId, product: productId } as any,\n undefined,\n { tenantId, organizationId },\n )\n const seen = new Set<string>(candidates.map((entry) => (entry.currencyCode ?? '').toUpperCase()))\n if (seen.size > 0 && !seen.has(priceInput.currencyCode.toUpperCase())) {\n return {\n ok: false,\n code: 'currency_out_of_scope',\n message: `Currency \"${priceInput.currencyCode}\" is not configured for product ${productId}.`,\n }\n }\n }\n return { ok: true }\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.update_product (single-record) */\n/* -------------------------------------------------------------------------- */\n\nconst updateProductInput = z\n .object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n title: z.string().trim().min(1).max(255).optional(),\n subtitle: z.string().trim().max(255).nullable().optional(),\n description: z.string().trim().max(4000).nullable().optional(),\n isActive: z.boolean().optional(),\n price: pricePatchSchema.optional().describe('Optional price adjustment; must match an existing currency for the product.'),\n })\n .strict()\n\ntype UpdateProductInput = z.infer<typeof updateProductInput>\n\nconst updateProductTool: CatalogAiToolDefinition = {\n name: 'catalog.update_product',\n displayName: 'Update product',\n description:\n 'Update a single catalog product (title, subtitle, description, isActive, optional price). Routes through the AI pending-action approval gate.',\n inputSchema: updateProductInput as z.ZodType<unknown>,\n requiredFeatures: ['catalog.products.manage'],\n tags: ['write', 'catalog', 'merchandising'],\n isMutation: true,\n isBulk: false,\n loadBeforeRecord: async (\n rawInput,\n ctx,\n ): Promise<CatalogToolLoadBeforeSingleRecord | null> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateProductInput = updateProductInput.parse(rawInput)\n const em = resolveEm(ctx)\n const row = await loadProductForScope(em, ctx, tenantId, input.productId)\n if (!row) return null\n return {\n recordId: row.id,\n entityType: 'catalog.product',\n recordVersion: recordVersionFromUpdatedAt(row.updatedAt),\n before: productSnapshot(row),\n }\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateProductInput = updateProductInput.parse(rawInput)\n const em = resolveEm(ctx)\n const row = await loadProductForScope(em, ctx, tenantId, input.productId)\n if (!row) {\n throw new Error(`Product \"${input.productId}\" is not accessible to the caller.`)\n }\n const organizationId = row.organizationId\n const before = productSnapshot(row)\n\n if (input.price) {\n const check = await validateProductPriceScope(\n em,\n tenantId,\n organizationId,\n row.id,\n input.price,\n )\n if (!check.ok) {\n const error = new Error(check.message) as Error & { code?: string }\n error.code = check.code\n throw error\n }\n }\n\n const body: Record<string, unknown> = {\n id: row.id,\n tenantId,\n organizationId,\n }\n if (input.title !== undefined) body.title = input.title\n if (input.subtitle !== undefined) body.subtitle = input.subtitle\n if (input.description !== undefined) body.description = input.description\n if (input.isActive !== undefined) body.isActive = input.isActive\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run({\n method: 'PUT',\n path: '/catalog/products',\n body,\n })\n if (!response.success) {\n throw new Error(response.error ?? `Failed to update product \"${row.id}\"`)\n }\n\n const after = await loadProductForScope(em, ctx, tenantId, row.id)\n return {\n recordId: row.id,\n commandName: 'catalog.products.update',\n before,\n after: after ? productSnapshot(after) : null,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.bulk_update_products (batch) */\n/* -------------------------------------------------------------------------- */\n\nconst bulkUpdateProductRecordSchema = z\n .object({\n recordId: z.string().uuid(),\n title: z.string().trim().min(1).max(255).optional(),\n subtitle: z.string().trim().max(255).nullable().optional(),\n description: z.string().trim().max(4000).nullable().optional(),\n isActive: z.boolean().optional(),\n price: pricePatchSchema.optional(),\n })\n .strict()\n\nconst bulkUpdateProductsInput = z\n .object({\n records: z\n .array(bulkUpdateProductRecordSchema)\n .min(1)\n .max(50)\n .describe('One entry per product to update. Max 50 rows per batch.'),\n })\n .strict()\n\ntype BulkUpdateProductsInput = z.infer<typeof bulkUpdateProductsInput>\ntype BulkUpdateProductPatch = z.infer<typeof bulkUpdateProductRecordSchema>\n\ntype BulkRecordResult = {\n recordId: string\n status: 'updated' | 'skipped' | 'failed'\n before: Record<string, unknown> | null\n after: Record<string, unknown> | null\n error?: { code: string; message: string }\n}\n\nconst bulkUpdateProductsTool: CatalogAiToolDefinition = {\n name: 'catalog.bulk_update_products',\n displayName: 'Bulk update products',\n description:\n 'Update several catalog products in a single approval. Emits ONE pending action whose records[] contains one diff per product. Stale-version or cross-tenant rows are collected in failedRecords[] without aborting the remaining writes.',\n inputSchema: bulkUpdateProductsInput as z.ZodType<unknown>,\n requiredFeatures: ['catalog.products.manage'],\n tags: ['write', 'catalog', 'merchandising', 'bulk'],\n isMutation: true,\n isBulk: true,\n loadBeforeRecords: async (rawInput, ctx): Promise<CatalogToolLoadBeforeRecord[]> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: BulkUpdateProductsInput = bulkUpdateProductsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const rows: CatalogToolLoadBeforeRecord[] = []\n for (const entry of input.records) {\n const product = await loadProductForScope(em, ctx, tenantId, entry.recordId)\n if (!product) continue\n rows.push({\n recordId: product.id,\n entityType: 'catalog.product',\n label: productLabel(product),\n recordVersion: recordVersionFromUpdatedAt(product.updatedAt),\n before: productSnapshot(product),\n })\n }\n return rows\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: BulkUpdateProductsInput = bulkUpdateProductsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const results: BulkRecordResult[] = []\n const failedRecordIds: string[] = []\n for (const entry of input.records) {\n const product = await loadProductForScope(em, ctx, tenantId, entry.recordId)\n if (!product) {\n failedRecordIds.push(entry.recordId)\n results.push({\n recordId: entry.recordId,\n status: 'skipped',\n before: null,\n after: null,\n error: { code: 'record_not_found', message: 'Product is not accessible to the caller.' },\n })\n continue\n }\n const organizationId = product.organizationId\n const before = productSnapshot(product)\n if (entry.price) {\n const check = await validateProductPriceScope(em, tenantId, organizationId, product.id, entry.price)\n if (!check.ok) {\n failedRecordIds.push(product.id)\n results.push({\n recordId: product.id,\n status: 'failed',\n before,\n after: null,\n error: { code: check.code, message: check.message },\n })\n continue\n }\n }\n const body: Record<string, unknown> = {\n id: product.id,\n tenantId,\n organizationId,\n }\n if (entry.title !== undefined) body.title = entry.title\n if (entry.subtitle !== undefined) body.subtitle = entry.subtitle\n if (entry.description !== undefined) body.description = entry.description\n if (entry.isActive !== undefined) body.isActive = entry.isActive\n try {\n const response = await runner.run({\n method: 'PUT',\n path: '/catalog/products',\n body,\n })\n if (!response.success) {\n const code =\n typeof (response.details as { code?: unknown } | undefined)?.code === 'string'\n ? ((response.details as { code: string }).code)\n : 'command_failed'\n failedRecordIds.push(product.id)\n results.push({\n recordId: product.id,\n status: 'failed',\n before,\n after: null,\n error: { code, message: response.error ?? 'API operation failed' },\n })\n continue\n }\n const after = await loadProductForScope(em, ctx, tenantId, product.id)\n results.push({\n recordId: product.id,\n status: 'updated',\n before,\n after: after ? productSnapshot(after) : null,\n })\n } catch (error) {\n failedRecordIds.push(product.id)\n const message = error instanceof Error ? error.message : String(error)\n const code = ((error as { code?: unknown })?.code as string | undefined) ?? 'command_failed'\n results.push({\n recordId: product.id,\n status: 'failed',\n before,\n after: null,\n error: { code, message },\n })\n }\n }\n const everyFailed = results.length > 0 && results.every((entry) => entry.status !== 'updated')\n return {\n commandName: 'catalog.products.update',\n records: results,\n failedRecordIds,\n error: everyFailed ? { code: 'all_records_failed', message: 'No records were updated.' } : undefined,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.apply_attribute_extraction (batch) */\n/* -------------------------------------------------------------------------- */\n\nconst attributeExtractionRecordSchema = z\n .object({\n recordId: z.string().uuid(),\n attributes: z.record(z.string(), z.unknown()).describe('Attribute key \u2192 value map produced by catalog.extract_attributes_from_description.'),\n })\n .strict()\n\nconst applyAttributeExtractionInput = z\n .object({\n records: z\n .array(attributeExtractionRecordSchema)\n .min(1)\n .max(50)\n .describe('One extraction payload per product. Max 50 rows per batch.'),\n })\n .strict()\n\ntype ApplyAttributeExtractionInput = z.infer<typeof applyAttributeExtractionInput>\n\nasync function resolveAttributeKeyIndex(\n ctx: CatalogToolContext,\n tenantId: string,\n): Promise<Set<string>> {\n try {\n const em = resolveEm(ctx)\n const index = await loadCustomFieldDefinitionIndex({\n em,\n entityIds: E.catalog.catalog_product as string,\n tenantId,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n })\n const keys = new Set<string>()\n if (index && typeof (index as any).keys === 'function') {\n for (const key of (index as any).keys() as Iterable<string>) {\n keys.add(String(key))\n }\n }\n return keys\n } catch {\n return new Set<string>()\n }\n}\n\nconst applyAttributeExtractionTool: CatalogAiToolDefinition = {\n name: 'catalog.apply_attribute_extraction',\n displayName: 'Apply attribute extraction',\n description:\n 'Persist the structured attribute output of catalog.extract_attributes_from_description. Re-validates every attribute key against the CURRENT catalog.product custom-field schema before the pending action is created \u2014 schema drift trips attribute_not_in_schema.',\n inputSchema: applyAttributeExtractionInput as z.ZodType<unknown>,\n requiredFeatures: ['catalog.products.manage'],\n tags: ['write', 'catalog', 'merchandising', 'attributes'],\n isMutation: true,\n isBulk: true,\n loadBeforeRecords: async (rawInput, ctx): Promise<CatalogToolLoadBeforeRecord[]> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: ApplyAttributeExtractionInput = applyAttributeExtractionInput.parse(rawInput)\n const em = resolveEm(ctx)\n const rows: CatalogToolLoadBeforeRecord[] = []\n for (const entry of input.records) {\n const product = await loadProductForScope(em, ctx, tenantId, entry.recordId)\n if (!product) continue\n rows.push({\n recordId: product.id,\n entityType: 'catalog.product',\n label: productLabel(product),\n recordVersion: recordVersionFromUpdatedAt(product.updatedAt),\n before: { attributes: {} },\n })\n }\n return rows\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: ApplyAttributeExtractionInput = applyAttributeExtractionInput.parse(rawInput)\n const em = resolveEm(ctx)\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const knownKeys = await resolveAttributeKeyIndex(ctx, tenantId)\n const results: BulkRecordResult[] = []\n const failedRecordIds: string[] = []\n for (const entry of input.records) {\n const product = await loadProductForScope(em, ctx, tenantId, entry.recordId)\n if (!product) {\n failedRecordIds.push(entry.recordId)\n results.push({\n recordId: entry.recordId,\n status: 'skipped',\n before: null,\n after: null,\n error: { code: 'record_not_found', message: 'Product is not accessible to the caller.' },\n })\n continue\n }\n const attributeKeys = Object.keys(entry.attributes ?? {})\n const unknownKey = knownKeys.size > 0\n ? attributeKeys.find((key) => !knownKeys.has(key))\n : undefined\n if (unknownKey) {\n failedRecordIds.push(product.id)\n results.push({\n recordId: product.id,\n status: 'failed',\n before: { attributes: {} },\n after: null,\n error: {\n code: 'attribute_not_in_schema',\n message: `Attribute \"${unknownKey}\" is not part of the current catalog.product schema.`,\n },\n })\n continue\n }\n const organizationId = product.organizationId\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(entry.attributes ?? {})) {\n customFields[key] = value\n }\n const body: Record<string, unknown> = {\n id: product.id,\n tenantId,\n organizationId,\n customFields,\n }\n try {\n const response = await runner.run({\n method: 'PUT',\n path: '/catalog/products',\n body,\n })\n if (!response.success) {\n const code =\n typeof (response.details as { code?: unknown } | undefined)?.code === 'string'\n ? ((response.details as { code: string }).code)\n : 'command_failed'\n failedRecordIds.push(product.id)\n results.push({\n recordId: product.id,\n status: 'failed',\n before: { attributes: {} },\n after: null,\n error: { code, message: response.error ?? 'API operation failed' },\n })\n continue\n }\n results.push({\n recordId: product.id,\n status: 'updated',\n before: { attributes: {} },\n after: { attributes: entry.attributes },\n })\n } catch (error) {\n failedRecordIds.push(product.id)\n const message = error instanceof Error ? error.message : String(error)\n const code = ((error as { code?: unknown })?.code as string | undefined) ?? 'command_failed'\n results.push({\n recordId: product.id,\n status: 'failed',\n before: { attributes: {} },\n after: null,\n error: { code, message },\n })\n }\n }\n const everyFailed = results.length > 0 && results.every((entry) => entry.status !== 'updated')\n return {\n commandName: 'catalog.products.update',\n records: results,\n failedRecordIds,\n error: everyFailed ? { code: 'all_records_failed', message: 'No records were updated.' } : undefined,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.update_product_media_descriptions (one-or-many media updates) */\n/* -------------------------------------------------------------------------- */\n\nconst mediaUpdateRecordSchema = z\n .object({\n mediaId: z.string().uuid(),\n altText: z.string().trim().max(500).optional(),\n caption: z.string().trim().max(1000).optional(),\n })\n .strict()\n\nconst updateProductMediaDescriptionsInput = z\n .object({\n mediaUpdates: z\n .array(mediaUpdateRecordSchema)\n .min(1)\n .max(100)\n .describe('One media record descriptor per attachment id. Exactly one entry is allowed for single-record writes; many entries for bulk writes.'),\n })\n .strict()\n\ntype UpdateProductMediaDescriptionsInput = z.infer<typeof updateProductMediaDescriptionsInput>\n\nasync function loadProductMediaForScope(\n em: EntityManager,\n ctx: CatalogToolContext,\n tenantId: string,\n mediaId: string,\n): Promise<Attachment | null> {\n const where: Record<string, unknown> = {\n id: mediaId,\n tenantId,\n entityId: E.catalog.catalog_product,\n }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const row = await findOneWithDecryption<Attachment>(\n em,\n Attachment,\n where as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!row || row.tenantId !== tenantId) return null\n if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null\n return row\n}\n\nfunction mediaSnapshot(row: Attachment): Record<string, unknown> {\n const metadata = row.storageMetadata ?? {}\n return {\n altText: typeof metadata?.altText === 'string' ? metadata.altText : null,\n caption: typeof metadata?.caption === 'string' ? metadata.caption : null,\n }\n}\n\nfunction mediaLabel(row: Attachment): string {\n if (row.fileName && row.fileName.trim().length) return row.fileName\n return row.id\n}\n\nfunction mediaUpdatePatch(entry: {\n altText?: string\n caption?: string\n}): { altText?: string; caption?: string } {\n return {\n ...(entry.altText !== undefined ? { altText: entry.altText } : {}),\n ...(entry.caption !== undefined ? { caption: entry.caption } : {}),\n }\n}\n\nconst updateProductMediaDescriptionsTool: CatalogAiToolDefinition = {\n name: 'catalog.update_product_media_descriptions',\n displayName: 'Update product media descriptions',\n description:\n 'Update altText / caption on one or many product media attachments in a single approval. Emits one pending action with records[] carrying per-media diffs. Accepts one entry for single-record writes or many for bulk writes \u2014 always routes through loadBeforeRecords to guarantee schema-consistent preview.',\n inputSchema: updateProductMediaDescriptionsInput as z.ZodType<unknown>,\n requiredFeatures: ['catalog.products.manage'],\n tags: ['write', 'catalog', 'merchandising', 'media'],\n isMutation: true,\n isBulk: true,\n loadBeforeRecords: async (rawInput, ctx): Promise<CatalogToolLoadBeforeRecord[]> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateProductMediaDescriptionsInput =\n updateProductMediaDescriptionsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const rows: CatalogToolLoadBeforeRecord[] = []\n for (const entry of input.mediaUpdates) {\n const media = await loadProductMediaForScope(em, ctx, tenantId, entry.mediaId)\n if (!media) continue\n const before = mediaSnapshot(media)\n rows.push({\n recordId: media.id,\n entityType: 'catalog.product_media',\n label: mediaLabel(media),\n recordVersion: recordVersionFromUpdatedAt(media.createdAt ?? null),\n before,\n after: { ...before, ...mediaUpdatePatch(entry) },\n })\n }\n return rows\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateProductMediaDescriptionsInput =\n updateProductMediaDescriptionsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const results: BulkRecordResult[] = []\n const failedRecordIds: string[] = []\n const touched: Array<{ row: Attachment; patch: { altText?: string; caption?: string }; before: Record<string, unknown> }> = []\n for (const entry of input.mediaUpdates) {\n const media = await loadProductMediaForScope(em, ctx, tenantId, entry.mediaId)\n if (!media) {\n failedRecordIds.push(entry.mediaId)\n results.push({\n recordId: entry.mediaId,\n status: 'skipped',\n before: null,\n after: null,\n error: { code: 'record_not_found', message: 'Media is not accessible to the caller.' },\n })\n continue\n }\n const before = mediaSnapshot(media)\n touched.push({\n row: media,\n patch: mediaUpdatePatch(entry),\n before,\n })\n }\n if (touched.length > 0) {\n for (const { row, patch } of touched) {\n const next = { ...(row.storageMetadata ?? {}) } as Record<string, unknown>\n if (patch.altText !== undefined) next.altText = patch.altText\n if (patch.caption !== undefined) next.caption = patch.caption\n row.storageMetadata = next\n }\n await em.flush()\n for (const { row, patch, before } of touched) {\n results.push({\n recordId: row.id,\n status: 'updated',\n before,\n after: {\n altText: patch.altText ?? (before as any).altText ?? null,\n caption: patch.caption ?? (before as any).caption ?? null,\n },\n })\n }\n }\n const everyFailed = results.length > 0 && results.every((entry) => entry.status !== 'updated')\n return {\n commandName: 'catalog.products.media.update_descriptions',\n records: results,\n failedRecordIds,\n error: everyFailed ? { code: 'all_records_failed', message: 'No media records were updated.' } : undefined,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n\nexport const mutationAiTools: CatalogAiToolDefinition[] = [\n updateProductTool,\n bulkUpdateProductsTool,\n applyAttributeExtractionTool,\n updateProductMediaDescriptionsTool,\n]\n\nexport default mutationAiTools\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/catalog/ai-tools/types.ts"],
4
- "sourcesContent": ["/**\n * Local AI tool shape for the catalog module (Phase 1 WS-C, Step 3.10).\n *\n * Mirrors the pattern established by the customers pack (Step 3.9): the\n * catalog module declares its read-only tools as plain objects whose shape\n * is a strict subset of `AiToolDefinition` from `@open-mercato/ai-assistant`.\n * Keeping the shape local avoids pulling the ai-assistant package into the\n * core module graph for jest and sidesteps a cross-package `moduleNameMapper`.\n * The generator walks every module root for a default / `aiTools` export with\n * this shape.\n */\nimport type { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\n\nexport interface CatalogToolContext {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n apiKeySecret?: string\n sessionId?: string\n}\n\n/**\n * Shape returned by `loadBeforeRecord` on a single-record mutation tool.\n * Mirrors `AiToolLoadBeforeSingleRecord` from `@open-mercato/ai-assistant/lib/types`.\n */\nexport interface CatalogToolLoadBeforeSingleRecord {\n recordId: string\n entityType: string\n recordVersion: string | null\n before: Record<string, unknown>\n}\n\n/**\n * Shape returned by `loadBeforeRecords` on a bulk mutation tool. Mirrors\n * `AiToolLoadBeforeRecord` from `@open-mercato/ai-assistant/lib/types` \u2014 the\n * Step 5.6 `prepareMutation` runtime wraps this into the `records[]` array on\n * the emitted `mutation-preview-card`.\n */\nexport interface CatalogToolLoadBeforeRecord {\n recordId: string\n entityType: string\n label: string\n recordVersion: string | null\n before: Record<string, unknown>\n}\n\nexport interface CatalogAiToolDefinition<TInput = unknown, TOutput = unknown> {\n name: string\n displayName?: string\n description: string\n inputSchema: z.ZodType<TInput>\n requiredFeatures?: string[]\n tags?: string[]\n isMutation?: boolean\n isBulk?: boolean\n maxCallsPerTurn?: number\n supportsAttachments?: boolean\n handler: (input: TInput, context: CatalogToolContext) => Promise<TOutput>\n loadBeforeRecord?: (\n input: TInput,\n context: CatalogToolContext,\n ) => Promise<CatalogToolLoadBeforeSingleRecord | null>\n loadBeforeRecords?: (\n input: TInput,\n context: CatalogToolContext,\n ) => Promise<CatalogToolLoadBeforeRecord[]>\n}\n\nexport function assertTenantScope(ctx: CatalogToolContext): {\n tenantId: string\n organizationId: string | null\n} {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for catalog.* tools')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n"],
5
- "mappings": "AAwEO,SAAS,kBAAkB,KAGhC;AACA,MAAI,CAAC,IAAI,UAAU;AACjB,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AACA,SAAO,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AACtE;",
4
+ "sourcesContent": ["/**\n * Local AI tool shape for the catalog module (Phase 1 WS-C, Step 3.10).\n *\n * Mirrors the pattern established by the customers pack (Step 3.9): the\n * catalog module declares its read-only tools as plain objects whose shape\n * is a strict subset of `AiToolDefinition` from `@open-mercato/ai-assistant`.\n * Keeping the shape local avoids pulling the ai-assistant package into the\n * core module graph for jest and sidesteps a cross-package `moduleNameMapper`.\n * The generator walks every module root for a default / `aiTools` export with\n * this shape.\n */\nimport type { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\n\nexport interface CatalogToolContext {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n apiKeySecret?: string\n sessionId?: string\n}\n\n/**\n * Shape returned by `loadBeforeRecord` on a single-record mutation tool.\n * Mirrors `AiToolLoadBeforeSingleRecord` from `@open-mercato/ai-assistant/lib/types`.\n */\nexport interface CatalogToolLoadBeforeSingleRecord {\n recordId: string\n entityType: string\n recordVersion: string | null\n before: Record<string, unknown>\n}\n\n/**\n * Shape returned by `loadBeforeRecords` on a bulk mutation tool. Mirrors\n * `AiToolLoadBeforeRecord` from `@open-mercato/ai-assistant/lib/types` \u2014 the\n * Step 5.6 `prepareMutation` runtime wraps this into the `records[]` array on\n * the emitted `mutation-preview-card`.\n */\nexport interface CatalogToolLoadBeforeRecord {\n recordId: string\n entityType: string\n label: string\n recordVersion: string | null\n before: Record<string, unknown>\n after?: Record<string, unknown>\n}\n\nexport interface CatalogAiToolDefinition<TInput = unknown, TOutput = unknown> {\n name: string\n displayName?: string\n description: string\n inputSchema: z.ZodType<TInput>\n requiredFeatures?: string[]\n tags?: string[]\n isMutation?: boolean\n isBulk?: boolean\n maxCallsPerTurn?: number\n supportsAttachments?: boolean\n handler: (input: TInput, context: CatalogToolContext) => Promise<TOutput>\n loadBeforeRecord?: (\n input: TInput,\n context: CatalogToolContext,\n ) => Promise<CatalogToolLoadBeforeSingleRecord | null>\n loadBeforeRecords?: (\n input: TInput,\n context: CatalogToolContext,\n ) => Promise<CatalogToolLoadBeforeRecord[]>\n}\n\nexport function assertTenantScope(ctx: CatalogToolContext): {\n tenantId: string\n organizationId: string | null\n} {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for catalog.* tools')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n"],
5
+ "mappings": "AAyEO,SAAS,kBAAkB,KAGhC;AACA,MAAI,CAAC,IAAI,UAAU;AACjB,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AACA,SAAO,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AACtE;",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  import { loginSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
5
5
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
6
  import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
7
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
7
8
  import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
8
9
  import { getClientIp } from "@open-mercato/shared/lib/ratelimit/helpers";
9
10
  import {
@@ -18,6 +19,9 @@ import {
18
19
  } from "@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext";
19
20
  const metadata = { requireAuth: false };
20
21
  const TIMING_EQUALIZATION_HASH = "$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i";
22
+ function reportLoginEventError(error) {
23
+ getTelemetryRuntime()?.reportError(error, { module: "customer_accounts", code: "customer_accounts.login_event_failed" });
24
+ }
21
25
  async function POST(req) {
22
26
  let body;
23
27
  try {
@@ -56,23 +60,23 @@ async function POST(req) {
56
60
  const user = await customerUserService.findByEmail(email, tenantId);
57
61
  if (!user || !user.passwordHash) {
58
62
  await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
59
- void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_credentials", tenantId }).catch(() => void 0);
63
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_credentials", tenantId }).catch(reportLoginEventError);
60
64
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
61
65
  }
62
66
  if (!user.isActive) {
63
67
  await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
64
- void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "inactive", tenantId }).catch(() => void 0);
68
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "inactive", tenantId }).catch(reportLoginEventError);
65
69
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
66
70
  }
67
71
  if (customerUserService.checkLockout(user)) {
68
72
  await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
69
- void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(() => void 0);
73
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(reportLoginEventError);
70
74
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
71
75
  }
72
76
  const passwordValid = await customerUserService.verifyPassword(user, password);
73
77
  if (!passwordValid) {
74
78
  await customerUserService.incrementFailedAttempts(user);
75
- void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_password", tenantId }).catch(() => void 0);
79
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_password", tenantId }).catch(reportLoginEventError);
76
80
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
77
81
  }
78
82
  if (!user.emailVerifiedAt) {
@@ -80,10 +84,13 @@ async function POST(req) {
80
84
  email,
81
85
  reason: "email_not_verified",
82
86
  tenantId
83
- }).catch(() => void 0);
87
+ }).catch(reportLoginEventError);
88
+ return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
89
+ }
90
+ if (!await customerUserService.resetFailedAttemptsUnlessLocked(user)) {
91
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(reportLoginEventError);
84
92
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
85
93
  }
86
- await customerUserService.resetFailedAttempts(user);
87
94
  await customerUserService.updateLastLoginAt(user);
88
95
  if (compoundKey) {
89
96
  await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig);
@@ -100,7 +107,7 @@ async function POST(req) {
100
107
  email: user.email,
101
108
  tenantId,
102
109
  organizationId: user.organizationId
103
- }).catch(() => void 0);
110
+ }).catch(reportLoginEventError);
104
111
  const res = NextResponse.json({
105
112
  ok: true,
106
113
  user: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/api/login.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the real-account and unknown/inactive/locked/no-hash login branches\n// so account existence cannot be inferred from a timing side channel. Mirrors signup.ts.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n\n // Equalize response timing and error responses across every failed-login branch so an\n // attacker cannot enumerate which emails have an account in this tenant. Unknown,\n // password-less, inactive, locked, wrong-password, and unverified accounts all run a\n // bcrypt comparison and return the same generic 401 \u2014 lockout/deactivation guidance is\n // conveyed out-of-band (e.g. email), never in the synchronous response.\n if (!user || !user.passwordHash) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'inactive', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n await customerUserService.resetFailedAttempts(user)\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId,\n organizationId: user.organizationId,\n })\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId: user.organizationId,\n }).catch(() => undefined)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate customer credentials',\n description: 'Validates customer credentials and issues JWT + session cookies.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: loginSchema,\n description: 'Login payload with email and password.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Invalid credentials', schema: errorSchema },\n { status: 429, description: 'Too many login attempts', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer login',\n description: 'Handles customer authentication and session issuance.',\n methods: { POST: methodDoc },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the real-account and unknown/inactive/locked/no-hash login branches\n// so account existence cannot be inferred from a timing side channel. Mirrors signup.ts.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\n\nfunction reportLoginEventError(error: unknown): void {\n getTelemetryRuntime()?.reportError(error, { module: 'customer_accounts', code: 'customer_accounts.login_event_failed' })\n}\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n\n // Equalize response timing and error responses across every failed-login branch so an\n // attacker cannot enumerate which emails have an account in this tenant. Unknown,\n // password-less, inactive, locked, wrong-password, and unverified accounts all run a\n // bcrypt comparison and return the same generic 401 \u2014 lockout/deactivation guidance is\n // conveyed out-of-band (e.g. email), never in the synchronous response.\n if (!user || !user.passwordHash) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'inactive', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!(await customerUserService.resetFailedAttemptsUnlessLocked(user))) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId,\n organizationId: user.organizationId,\n })\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId: user.organizationId,\n }).catch(reportLoginEventError)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate customer credentials',\n description: 'Validates customer credentials and issues JWT + session cookies.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: loginSchema,\n description: 'Login payload with email and password.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Invalid credentials', schema: errorSchema },\n { status: 429, description: 'Too many login attempts', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer login',\n description: 'Handles customer authentication and session issuance.',\n methods: { POST: methodDoc },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }