@open-mercato/core 0.6.8-develop.7015.1.af90a2ddc7 → 0.6.8-develop.7019.1.f4c01c4b5c

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 (82) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/api_keys/api/keys/route.js +1 -1
  3. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  4. package/dist/modules/api_keys/di.js +9 -0
  5. package/dist/modules/api_keys/di.js.map +7 -0
  6. package/dist/modules/api_keys/services/principalService.js +31 -0
  7. package/dist/modules/api_keys/services/principalService.js.map +7 -0
  8. package/dist/modules/attachments/di.js +11 -1
  9. package/dist/modules/attachments/di.js.map +2 -2
  10. package/dist/modules/attachments/index.js.map +1 -1
  11. package/dist/modules/attachments/lib/attachment-service.js +313 -0
  12. package/dist/modules/attachments/lib/attachment-service.js.map +7 -0
  13. package/dist/modules/attachments/lib/scoped-upload-service.js +15 -2
  14. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  15. package/dist/modules/attachments/lib/upload-limits.js +6 -2
  16. package/dist/modules/attachments/lib/upload-limits.js.map +2 -2
  17. package/dist/modules/auth/data/entities.js.map +2 -2
  18. package/dist/modules/auth/di.js +35 -9
  19. package/dist/modules/auth/di.js.map +2 -2
  20. package/dist/modules/auth/services/principalService.js +200 -0
  21. package/dist/modules/auth/services/principalService.js.map +7 -0
  22. package/dist/modules/auth/services/rbacService.js +67 -24
  23. package/dist/modules/auth/services/rbacService.js.map +3 -3
  24. package/dist/modules/auth/services/roleOrganizationScope.js +41 -0
  25. package/dist/modules/auth/services/roleOrganizationScope.js.map +7 -0
  26. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +1 -1
  27. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  28. package/dist/modules/customers/components/calendar/EventPeekPopover.js +3 -2
  29. package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
  30. package/dist/modules/data_sync/lib/abandoned-run.js +19 -0
  31. package/dist/modules/data_sync/lib/abandoned-run.js.map +7 -0
  32. package/dist/modules/data_sync/workers/sync-export.js +5 -1
  33. package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
  34. package/dist/modules/data_sync/workers/sync-import.js +5 -1
  35. package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
  36. package/dist/modules/directory/di.js +9 -0
  37. package/dist/modules/directory/di.js.map +2 -2
  38. package/dist/modules/directory/services/organizationHierarchyService.js +29 -0
  39. package/dist/modules/directory/services/organizationHierarchyService.js.map +7 -0
  40. package/dist/modules/directory/services/organizationScopeService.js +56 -0
  41. package/dist/modules/directory/services/organizationScopeService.js.map +7 -0
  42. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  43. package/dist/modules/warranty_claims/api/portal/attachments/route.js +2 -2
  44. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
  45. package/dist/modules/workflows/lib/activity-executor.js +4 -0
  46. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  47. package/package.json +7 -7
  48. package/src/modules/api_keys/api/keys/route.ts +3 -1
  49. package/src/modules/api_keys/di.ts +7 -0
  50. package/src/modules/api_keys/services/principalService.ts +33 -0
  51. package/src/modules/attachments/AGENTS.md +15 -0
  52. package/src/modules/attachments/di.ts +16 -0
  53. package/src/modules/attachments/i18n/de.json +5 -0
  54. package/src/modules/attachments/i18n/en.json +5 -0
  55. package/src/modules/attachments/i18n/es.json +5 -0
  56. package/src/modules/attachments/i18n/ko.json +5 -0
  57. package/src/modules/attachments/i18n/pl.json +5 -0
  58. package/src/modules/attachments/index.ts +10 -0
  59. package/src/modules/attachments/lib/attachment-service.ts +468 -0
  60. package/src/modules/attachments/lib/scoped-upload-service.ts +46 -0
  61. package/src/modules/attachments/lib/upload-limits.ts +6 -2
  62. package/src/modules/auth/data/entities.ts +4 -1
  63. package/src/modules/auth/di.ts +46 -16
  64. package/src/modules/auth/services/principalService.ts +266 -0
  65. package/src/modules/auth/services/rbacService.ts +110 -24
  66. package/src/modules/auth/services/roleOrganizationScope.ts +70 -0
  67. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +1 -1
  68. package/src/modules/customers/components/calendar/EventPeekPopover.tsx +4 -2
  69. package/src/modules/customers/i18n/de.json +1 -1
  70. package/src/modules/customers/i18n/en.json +1 -1
  71. package/src/modules/customers/i18n/es.json +1 -1
  72. package/src/modules/customers/i18n/ko.json +1 -1
  73. package/src/modules/customers/i18n/pl.json +1 -1
  74. package/src/modules/data_sync/lib/abandoned-run.ts +56 -0
  75. package/src/modules/data_sync/workers/sync-export.ts +4 -0
  76. package/src/modules/data_sync/workers/sync-import.ts +4 -0
  77. package/src/modules/directory/di.ts +14 -2
  78. package/src/modules/directory/services/organizationHierarchyService.ts +34 -0
  79. package/src/modules/directory/services/organizationScopeService.ts +85 -0
  80. package/src/modules/directory/utils/organizationScope.ts +4 -15
  81. package/src/modules/warranty_claims/api/portal/attachments/route.ts +2 -2
  82. package/src/modules/workflows/lib/activity-executor.ts +9 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/calendar/CalendarSettingsModal.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Info, X } from 'lucide-react'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { TagInput } from '@open-mercato/ui/primitives/tag-input'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport {\n CalendarPreferences,\n ConflictScope,\n MAX_ACTIVITY_TYPES,\n MAX_EVENT_CATEGORIES,\n} from '../../lib/calendar/preferences'\nimport { SegmentGroup } from './editor/SegmentGroup'\n\nexport type CalendarSettingsModalProps = {\n open: boolean\n preferences: CalendarPreferences\n seedActivityTypes: string[]\n onOpenChange(open: boolean): void\n onSave(next: CalendarPreferences): void\n}\n\ntype ToggleKey = 'showCrmActivities' | 'aiSummaries' | 'conflictWarnings' | 'showWeekends'\n\n// An empty Activity Types list is an intentional floor meaning \"surface all\n// dictionary types\" rather than \"surface none\". When the stored list is empty\n// the modal seeds the dictionary types for display. NOTE: since the editor's\n// Category quick-pick was removed (owner feedback, #3552) these lists no longer\n// affect the event editor \u2014 the type switcher always shows the full dictionary.\nfunction buildDraft(preferences: CalendarPreferences, seedActivityTypes: string[]): CalendarPreferences {\n return {\n ...preferences,\n eventCategories: [...preferences.eventCategories],\n activityTypes:\n preferences.activityTypes.length > 0\n ? [...preferences.activityTypes]\n : seedActivityTypes.slice(0, MAX_ACTIVITY_TYPES),\n }\n}\n\nexport function CalendarSettingsModal({\n open,\n preferences,\n seedActivityTypes,\n onOpenChange,\n onSave,\n}: CalendarSettingsModalProps) {\n const t = useT()\n const [draft, setDraft] = React.useState<CalendarPreferences>(() => buildDraft(preferences, seedActivityTypes))\n const openRef = React.useRef(false)\n\n React.useEffect(() => {\n if (open && !openRef.current) setDraft(buildDraft(preferences, seedActivityTypes))\n openRef.current = open\n }, [open, preferences, seedActivityTypes])\n\n const handleSave = React.useCallback(() => {\n onSave(draft)\n onOpenChange(false)\n }, [draft, onOpenChange, onSave])\n\n const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave })\n\n const toggle = (key: ToggleKey) => (checked: boolean) => setDraft((current) => ({ ...current, [key]: checked }))\n\n const toggleRows: Array<{ key: ToggleKey; label: string }> = [\n { key: 'showCrmActivities', label: t('customers.calendar.settings.showCrmActivities', 'Show CRM activities on calendar') },\n { key: 'aiSummaries', label: t('customers.calendar.settings.aiSummaries', 'AI summaries & quick actions') },\n { key: 'conflictWarnings', label: t('customers.calendar.settings.conflictWarnings', 'Conflict warnings') },\n { key: 'showWeekends', label: t('customers.calendar.settings.showWeekends', 'Show weekends') },\n ]\n\n const title = t('customers.calendar.settings.title', 'Customization')\n const removeTagLabel = React.useCallback(\n (tag: string) => t('customers.calendar.settings.removeTag', 'Remove {tag}', { tag }),\n [t],\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n onKeyDown={handleKeyDown}\n aria-describedby={undefined}\n dismissible={false}\n className=\"flex w-full max-w-[400px] flex-col gap-0 overflow-hidden rounded-2xl border-0 bg-card p-0 shadow-xl\"\n >\n <VisuallyHidden>\n <DialogTitle>{title}</DialogTitle>\n </VisuallyHidden>\n <div className=\"flex shrink-0 items-start gap-3.5 border-b border-border py-4 pl-5 pr-4\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <p className=\"text-sm font-medium leading-5 text-foreground\">{title}</p>\n <p className=\"text-xs leading-4 text-muted-foreground\">\n {t('customers.calendar.settings.subtitle', 'Customise your calendar module.')}\n </p>\n </div>\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => onOpenChange(false)}\n aria-label={t('customers.calendar.settings.close', 'Close')}\n className=\"shrink-0 text-muted-foreground\"\n >\n <X aria-hidden className=\"size-5\" />\n </IconButton>\n </div>\n\n <div className=\"flex flex-col gap-4 p-5\">\n <SettingsTagInput\n label={t('customers.calendar.settings.eventCategories', 'Event Categories')}\n maxLabel={t('customers.calendar.settings.max', '(max. {count})', { count: MAX_EVENT_CATEGORIES })}\n hint={t(\n 'customers.calendar.settings.eventCategoriesHint',\n 'Your own grouping labels (e.g. Team Meeting, Sales Call). Offered when creating an event.',\n )}\n placeholder={t('customers.calendar.settings.addCategory', 'Add a category\u2026')}\n value={draft.eventCategories}\n maxTags={MAX_EVENT_CATEGORIES}\n removeTagLabel={removeTagLabel}\n onChange={(eventCategories) => setDraft((current) => ({ ...current, eventCategories }))}\n />\n <SettingsTagInput\n label={t('customers.calendar.settings.activityTypes', 'Activity Types')}\n maxLabel={t('customers.calendar.settings.max', '(max. {count})', { count: MAX_ACTIVITY_TYPES })}\n hint={t(\n 'customers.calendar.settings.activityTypesHint',\n 'The activity types your calendar surfaces when creating an event. Seeded from your workspace dictionary.',\n )}\n placeholder={t('customers.calendar.settings.addType', 'Add a type\u2026')}\n value={draft.activityTypes}\n maxTags={MAX_ACTIVITY_TYPES}\n removeTagLabel={removeTagLabel}\n onChange={(activityTypes) => setDraft((current) => ({ ...current, activityTypes }))}\n />\n {toggleRows.map((row) => (\n <React.Fragment key={row.key}>\n <div className=\"flex items-center gap-2\">\n <Switch\n checked={draft[row.key]}\n onCheckedChange={toggle(row.key)}\n aria-label={row.label}\n />\n <span className=\"text-sm leading-5 text-foreground\">{row.label}</span>\n </div>\n {row.key === 'conflictWarnings' && draft.conflictWarnings ? (\n <div className=\"flex flex-col gap-1.5 pl-11\">\n <span className=\"text-xs leading-4 text-muted-foreground\">\n {t(\n 'customers.calendar.settings.conflictScopeHint',\n 'Choose whose overlaps the calendar flags as conflicts.',\n )}\n </span>\n <SegmentGroup<ConflictScope>\n ariaLabel={t('customers.calendar.settings.conflictScope', 'Conflict scope')}\n value={draft.conflictScope}\n onChange={(conflictScope) => setDraft((current) => ({ ...current, conflictScope }))}\n options={[\n { value: 'mine', label: t('customers.calendar.settings.conflictScopeMine', 'My meetings') },\n { value: 'all', label: t('customers.calendar.settings.conflictScopeAll', 'All meetings') },\n ]}\n />\n </div>\n ) : null}\n </React.Fragment>\n ))}\n </div>\n\n <div className=\"flex shrink-0 items-center gap-3 border-t border-border px-5 py-4\">\n <Button type=\"button\" variant=\"outline\" className=\"flex-1\" onClick={() => onOpenChange(false)}>\n {t('customers.calendar.settings.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" className=\"flex-1\" onClick={handleSave}>\n {t('customers.calendar.settings.save', 'Save Changes')}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\ntype SettingsTagInputProps = {\n label: string\n maxLabel: string\n hint: string\n placeholder: string\n value: string[]\n maxTags: number\n removeTagLabel: (tag: string) => string\n onChange(value: string[]): void\n}\n\nfunction SettingsTagInput({ label, maxLabel, hint, placeholder, value, maxTags, removeTagLabel, onChange }: SettingsTagInputProps) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"text-sm font-medium leading-5 text-foreground\">{label}</span>\n <span className=\"text-sm leading-5 text-muted-foreground\">{maxLabel}</span>\n <SimpleTooltip content={hint}>\n <span className=\"inline-flex text-muted-foreground\" tabIndex={0} role=\"img\" aria-label={hint}>\n <Info aria-hidden className=\"size-4\" />\n </span>\n </SimpleTooltip>\n </div>\n <TagInput\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n maxTags={maxTags}\n aria-label={label}\n removeTagLabel={removeTagLabel}\n />\n </div>\n )\n}\n"],
5
- "mappings": ";AA8FU,cAGA,YAHA;AA5FV,YAAY,WAAW;AACvB,SAAS,MAAM,SAAS;AACxB,SAAS,sBAAsB;AAC/B,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,mBAAmB;AACnD,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC;AAAA,EAGE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAiB7B,SAAS,WAAW,aAAkC,mBAAkD;AACtG,SAAO;AAAA,IACL,GAAG;AAAA,IACH,iBAAiB,CAAC,GAAG,YAAY,eAAe;AAAA,IAChD,eACE,YAAY,cAAc,SAAS,IAC/B,CAAC,GAAG,YAAY,aAAa,IAC7B,kBAAkB,MAAM,GAAG,kBAAkB;AAAA,EACrD;AACF;AAEO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA+B;AAC7B,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAA8B,MAAM,WAAW,aAAa,iBAAiB,CAAC;AAC9G,QAAM,UAAU,MAAM,OAAO,KAAK;AAElC,QAAM,UAAU,MAAM;AACpB,QAAI,QAAQ,CAAC,QAAQ,QAAS,UAAS,WAAW,aAAa,iBAAiB,CAAC;AACjF,YAAQ,UAAU;AAAA,EACpB,GAAG,CAAC,MAAM,aAAa,iBAAiB,CAAC;AAEzC,QAAM,aAAa,MAAM,YAAY,MAAM;AACzC,WAAO,KAAK;AACZ,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,OAAO,cAAc,MAAM,CAAC;AAEhC,QAAM,gBAAgB,oBAAoB,EAAE,WAAW,WAAW,CAAC;AAEnE,QAAM,SAAS,CAAC,QAAmB,CAAC,YAAqB,SAAS,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,QAAQ,EAAE;AAE/G,QAAM,aAAuD;AAAA,IAC3D,EAAE,KAAK,qBAAqB,OAAO,EAAE,iDAAiD,iCAAiC,EAAE;AAAA,IACzH,EAAE,KAAK,eAAe,OAAO,EAAE,2CAA2C,8BAA8B,EAAE;AAAA,IAC1G,EAAE,KAAK,oBAAoB,OAAO,EAAE,gDAAgD,mBAAmB,EAAE;AAAA,IACzG,EAAE,KAAK,gBAAgB,OAAO,EAAE,4CAA4C,eAAe,EAAE;AAAA,EAC/F;AAEA,QAAM,QAAQ,EAAE,qCAAqC,eAAe;AACpE,QAAM,iBAAiB,MAAM;AAAA,IAC3B,CAAC,QAAgB,EAAE,yCAAyC,gBAAgB,EAAE,IAAI,CAAC;AAAA,IACnF,CAAC,CAAC;AAAA,EACJ;AAEA,SACE,oBAAC,UAAO,MAAY,cAClB;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,MACX,oBAAkB;AAAA,MAClB,aAAa;AAAA,MACb,WAAU;AAAA,MAEV;AAAA,4BAAC,kBACC,8BAAC,eAAa,iBAAM,GACtB;AAAA,QACA,qBAAC,SAAI,WAAU,2EACb;AAAA,+BAAC,SAAI,WAAU,sCACb;AAAA,gCAAC,OAAE,WAAU,iDAAiD,iBAAM;AAAA,YACpE,oBAAC,OAAE,WAAU,2CACV,YAAE,wCAAwC,iCAAiC,GAC9E;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAS,MAAM,aAAa,KAAK;AAAA,cACjC,cAAY,EAAE,qCAAqC,OAAO;AAAA,cAC1D,WAAU;AAAA,cAEV,8BAAC,KAAE,eAAW,MAAC,WAAU,UAAS;AAAA;AAAA,UACpC;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,EAAE,+CAA+C,kBAAkB;AAAA,cAC1E,UAAU,EAAE,mCAAmC,kBAAkB,EAAE,OAAO,qBAAqB,CAAC;AAAA,cAChG,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,aAAa,EAAE,2CAA2C,sBAAiB;AAAA,cAC3E,OAAO,MAAM;AAAA,cACb,SAAS;AAAA,cACT;AAAA,cACA,UAAU,CAAC,oBAAoB,SAAS,CAAC,aAAa,EAAE,GAAG,SAAS,gBAAgB,EAAE;AAAA;AAAA,UACxF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,EAAE,6CAA6C,gBAAgB;AAAA,cACtE,UAAU,EAAE,mCAAmC,kBAAkB,EAAE,OAAO,mBAAmB,CAAC;AAAA,cAC9F,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,aAAa,EAAE,uCAAuC,kBAAa;AAAA,cACnE,OAAO,MAAM;AAAA,cACb,SAAS;AAAA,cACT;AAAA,cACA,UAAU,CAAC,kBAAkB,SAAS,CAAC,aAAa,EAAE,GAAG,SAAS,cAAc,EAAE;AAAA;AAAA,UACpF;AAAA,UACC,WAAW,IAAI,CAAC,QACf,qBAAC,MAAM,UAAN,EACC;AAAA,iCAAC,SAAI,WAAU,2BACb;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAS,MAAM,IAAI,GAAG;AAAA,kBACtB,iBAAiB,OAAO,IAAI,GAAG;AAAA,kBAC/B,cAAY,IAAI;AAAA;AAAA,cAClB;AAAA,cACA,oBAAC,UAAK,WAAU,qCAAqC,cAAI,OAAM;AAAA,eACjE;AAAA,YACC,IAAI,QAAQ,sBAAsB,MAAM,mBACvC,qBAAC,SAAI,WAAU,+BACb;AAAA,kCAAC,UAAK,WAAU,2CACb;AAAA,gBACC;AAAA,gBACA;AAAA,cACF,GACF;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAW,EAAE,6CAA6C,gBAAgB;AAAA,kBAC1E,OAAO,MAAM;AAAA,kBACb,UAAU,CAAC,kBAAkB,SAAS,CAAC,aAAa,EAAE,GAAG,SAAS,cAAc,EAAE;AAAA,kBAClF,SAAS;AAAA,oBACP,EAAE,OAAO,QAAQ,OAAO,EAAE,iDAAiD,aAAa,EAAE;AAAA,oBAC1F,EAAE,OAAO,OAAO,OAAO,EAAE,gDAAgD,cAAc,EAAE;AAAA,kBAC3F;AAAA;AAAA,cACF;AAAA,eACF,IACE;AAAA,eA3Be,IAAI,GA4BzB,CACD;AAAA,WACH;AAAA,QAEA,qBAAC,SAAI,WAAU,qEACb;AAAA,8BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,WAAU,UAAS,SAAS,MAAM,aAAa,KAAK,GACzF,YAAE,sCAAsC,QAAQ,GACnD;AAAA,UACA,oBAAC,UAAO,MAAK,UAAS,WAAU,UAAS,SAAS,YAC/C,YAAE,oCAAoC,cAAc,GACvD;AAAA,WACF;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;AAaA,SAAS,iBAAiB,EAAE,OAAO,UAAU,MAAM,aAAa,OAAO,SAAS,gBAAgB,SAAS,GAA0B;AACjI,SACE,qBAAC,SAAI,WAAU,uBACb;AAAA,yBAAC,SAAI,WAAU,2BACb;AAAA,0BAAC,UAAK,WAAU,iDAAiD,iBAAM;AAAA,MACvE,oBAAC,UAAK,WAAU,2CAA2C,oBAAS;AAAA,MACpE,oBAAC,iBAAc,SAAS,MACtB,8BAAC,UAAK,WAAU,qCAAoC,UAAU,GAAG,MAAK,OAAM,cAAY,MACtF,8BAAC,QAAK,eAAW,MAAC,WAAU,UAAS,GACvC,GACF;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,cAAY;AAAA,QACZ;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Info, X } from 'lucide-react'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { TagInput } from '@open-mercato/ui/primitives/tag-input'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport {\n CalendarPreferences,\n ConflictScope,\n MAX_ACTIVITY_TYPES,\n MAX_EVENT_CATEGORIES,\n} from '../../lib/calendar/preferences'\nimport { SegmentGroup } from './editor/SegmentGroup'\n\nexport type CalendarSettingsModalProps = {\n open: boolean\n preferences: CalendarPreferences\n seedActivityTypes: string[]\n onOpenChange(open: boolean): void\n onSave(next: CalendarPreferences): void\n}\n\ntype ToggleKey = 'showCrmActivities' | 'aiSummaries' | 'conflictWarnings' | 'showWeekends'\n\n// An empty Activity Types list is an intentional floor meaning \"surface all\n// dictionary types\" rather than \"surface none\". When the stored list is empty\n// the modal seeds the dictionary types for display. NOTE: since the editor's\n// Category quick-pick was removed (owner feedback, #3552) these lists no longer\n// affect the event editor \u2014 the type switcher always shows the full dictionary.\nfunction buildDraft(preferences: CalendarPreferences, seedActivityTypes: string[]): CalendarPreferences {\n return {\n ...preferences,\n eventCategories: [...preferences.eventCategories],\n activityTypes:\n preferences.activityTypes.length > 0\n ? [...preferences.activityTypes]\n : seedActivityTypes.slice(0, MAX_ACTIVITY_TYPES),\n }\n}\n\nexport function CalendarSettingsModal({\n open,\n preferences,\n seedActivityTypes,\n onOpenChange,\n onSave,\n}: CalendarSettingsModalProps) {\n const t = useT()\n const [draft, setDraft] = React.useState<CalendarPreferences>(() => buildDraft(preferences, seedActivityTypes))\n const openRef = React.useRef(false)\n\n React.useEffect(() => {\n if (open && !openRef.current) setDraft(buildDraft(preferences, seedActivityTypes))\n openRef.current = open\n }, [open, preferences, seedActivityTypes])\n\n const handleSave = React.useCallback(() => {\n onSave(draft)\n onOpenChange(false)\n }, [draft, onOpenChange, onSave])\n\n const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave })\n\n const toggle = (key: ToggleKey) => (checked: boolean) => setDraft((current) => ({ ...current, [key]: checked }))\n\n const toggleRows: Array<{ key: ToggleKey; label: string }> = [\n { key: 'showCrmActivities', label: t('customers.calendar.settings.showCrmActivities', 'Show CRM activities on calendar') },\n { key: 'aiSummaries', label: t('customers.calendar.settings.aiSummaries', 'AI summaries') },\n { key: 'conflictWarnings', label: t('customers.calendar.settings.conflictWarnings', 'Conflict warnings') },\n { key: 'showWeekends', label: t('customers.calendar.settings.showWeekends', 'Show weekends') },\n ]\n\n const title = t('customers.calendar.settings.title', 'Customization')\n const removeTagLabel = React.useCallback(\n (tag: string) => t('customers.calendar.settings.removeTag', 'Remove {tag}', { tag }),\n [t],\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n onKeyDown={handleKeyDown}\n aria-describedby={undefined}\n dismissible={false}\n className=\"flex w-full max-w-[400px] flex-col gap-0 overflow-hidden rounded-2xl border-0 bg-card p-0 shadow-xl\"\n >\n <VisuallyHidden>\n <DialogTitle>{title}</DialogTitle>\n </VisuallyHidden>\n <div className=\"flex shrink-0 items-start gap-3.5 border-b border-border py-4 pl-5 pr-4\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <p className=\"text-sm font-medium leading-5 text-foreground\">{title}</p>\n <p className=\"text-xs leading-4 text-muted-foreground\">\n {t('customers.calendar.settings.subtitle', 'Customise your calendar module.')}\n </p>\n </div>\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => onOpenChange(false)}\n aria-label={t('customers.calendar.settings.close', 'Close')}\n className=\"shrink-0 text-muted-foreground\"\n >\n <X aria-hidden className=\"size-5\" />\n </IconButton>\n </div>\n\n <div className=\"flex flex-col gap-4 p-5\">\n <SettingsTagInput\n label={t('customers.calendar.settings.eventCategories', 'Event Categories')}\n maxLabel={t('customers.calendar.settings.max', '(max. {count})', { count: MAX_EVENT_CATEGORIES })}\n hint={t(\n 'customers.calendar.settings.eventCategoriesHint',\n 'Your own grouping labels (e.g. Team Meeting, Sales Call). Offered when creating an event.',\n )}\n placeholder={t('customers.calendar.settings.addCategory', 'Add a category\u2026')}\n value={draft.eventCategories}\n maxTags={MAX_EVENT_CATEGORIES}\n removeTagLabel={removeTagLabel}\n onChange={(eventCategories) => setDraft((current) => ({ ...current, eventCategories }))}\n />\n <SettingsTagInput\n label={t('customers.calendar.settings.activityTypes', 'Activity Types')}\n maxLabel={t('customers.calendar.settings.max', '(max. {count})', { count: MAX_ACTIVITY_TYPES })}\n hint={t(\n 'customers.calendar.settings.activityTypesHint',\n 'The activity types your calendar surfaces when creating an event. Seeded from your workspace dictionary.',\n )}\n placeholder={t('customers.calendar.settings.addType', 'Add a type\u2026')}\n value={draft.activityTypes}\n maxTags={MAX_ACTIVITY_TYPES}\n removeTagLabel={removeTagLabel}\n onChange={(activityTypes) => setDraft((current) => ({ ...current, activityTypes }))}\n />\n {toggleRows.map((row) => (\n <React.Fragment key={row.key}>\n <div className=\"flex items-center gap-2\">\n <Switch\n checked={draft[row.key]}\n onCheckedChange={toggle(row.key)}\n aria-label={row.label}\n />\n <span className=\"text-sm leading-5 text-foreground\">{row.label}</span>\n </div>\n {row.key === 'conflictWarnings' && draft.conflictWarnings ? (\n <div className=\"flex flex-col gap-1.5 pl-11\">\n <span className=\"text-xs leading-4 text-muted-foreground\">\n {t(\n 'customers.calendar.settings.conflictScopeHint',\n 'Choose whose overlaps the calendar flags as conflicts.',\n )}\n </span>\n <SegmentGroup<ConflictScope>\n ariaLabel={t('customers.calendar.settings.conflictScope', 'Conflict scope')}\n value={draft.conflictScope}\n onChange={(conflictScope) => setDraft((current) => ({ ...current, conflictScope }))}\n options={[\n { value: 'mine', label: t('customers.calendar.settings.conflictScopeMine', 'My meetings') },\n { value: 'all', label: t('customers.calendar.settings.conflictScopeAll', 'All meetings') },\n ]}\n />\n </div>\n ) : null}\n </React.Fragment>\n ))}\n </div>\n\n <div className=\"flex shrink-0 items-center gap-3 border-t border-border px-5 py-4\">\n <Button type=\"button\" variant=\"outline\" className=\"flex-1\" onClick={() => onOpenChange(false)}>\n {t('customers.calendar.settings.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" className=\"flex-1\" onClick={handleSave}>\n {t('customers.calendar.settings.save', 'Save Changes')}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\ntype SettingsTagInputProps = {\n label: string\n maxLabel: string\n hint: string\n placeholder: string\n value: string[]\n maxTags: number\n removeTagLabel: (tag: string) => string\n onChange(value: string[]): void\n}\n\nfunction SettingsTagInput({ label, maxLabel, hint, placeholder, value, maxTags, removeTagLabel, onChange }: SettingsTagInputProps) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"text-sm font-medium leading-5 text-foreground\">{label}</span>\n <span className=\"text-sm leading-5 text-muted-foreground\">{maxLabel}</span>\n <SimpleTooltip content={hint}>\n <span className=\"inline-flex text-muted-foreground\" tabIndex={0} role=\"img\" aria-label={hint}>\n <Info aria-hidden className=\"size-4\" />\n </span>\n </SimpleTooltip>\n </div>\n <TagInput\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n maxTags={maxTags}\n aria-label={label}\n removeTagLabel={removeTagLabel}\n />\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -19,6 +19,7 @@ function EventPeekPopover({
19
19
  joinUrl,
20
20
  aiSummaries,
21
21
  canManage = true,
22
+ side = "right",
22
23
  onOpenChange,
23
24
  onJoin,
24
25
  onEdit,
@@ -39,10 +40,10 @@ function EventPeekPopover({
39
40
  (part) => Boolean(part)
40
41
  );
41
42
  const showSummary = aiSummaries && metaParts.length > 0;
42
- const showJoin = aiSummaries && Boolean(joinUrl);
43
+ const showJoin = Boolean(joinUrl);
43
44
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange, children: [
44
45
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children }),
45
- /* @__PURE__ */ jsx(PopoverContent, { align: "start", side: "right", sideOffset: 8, className: "w-56 p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
46
+ /* @__PURE__ */ jsx(PopoverContent, { align: "start", side, sideOffset: 8, className: "w-56 p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
46
47
  /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold leading-5 text-foreground", children: title }),
47
48
  /* @__PURE__ */ jsx("p", { className: "text-xs leading-4 text-muted-foreground", children: `${dateLabel} \xB7 ${timeRange}` }),
48
49
  showSummary ? /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1 text-xs leading-4 text-muted-foreground", children: [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/calendar/EventPeekPopover.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { formatTimeRange } from './EventBlock'\nimport type { CalendarItem, CalendarPlatform } from './types'\n\nconst PLATFORM_FULL_LABELS: Record<CalendarPlatform, { key: string; fallback: string }> = {\n zoom: { key: 'customers.calendar.platformFull.zoom', fallback: 'Zoom' },\n meet: { key: 'customers.calendar.platformFull.meet', fallback: 'Google Meet' },\n slack: { key: 'customers.calendar.platformFull.slack', fallback: 'Slack' },\n teams: { key: 'customers.calendar.platformFull.teams', fallback: 'Microsoft Teams' },\n}\n\nexport type EventPeekPopoverProps = {\n item: CalendarItem\n open: boolean\n joinUrl: string | null\n aiSummaries: boolean\n canManage?: boolean\n onOpenChange(open: boolean): void\n onJoin(item: CalendarItem): void\n onEdit(item: CalendarItem): void\n children: React.ReactNode\n}\n\nexport function EventPeekPopover({\n item,\n open,\n joinUrl,\n aiSummaries,\n canManage = true,\n onOpenChange,\n onJoin,\n onEdit,\n children,\n}: EventPeekPopoverProps) {\n const t = useT()\n const locale = useLocale()\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n const dateLabel = React.useMemo(\n () => new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(item.start),\n [locale, item.start],\n )\n const timeRange = item.allDay\n ? t('customers.calendar.grid.allDay', 'All day')\n : formatTimeRange(locale, item.start, item.end)\n\n const attendeeCount = item.participants.length\n const attendeesLabel =\n attendeeCount === 1\n ? t('customers.calendar.peek.attendee', '1 attendee')\n : t('customers.calendar.peek.attendees', '{count} attendees', { count: attendeeCount })\n const platformLabel =\n item.locationKind === 'platform' && item.platform\n ? t(PLATFORM_FULL_LABELS[item.platform].key, PLATFORM_FULL_LABELS[item.platform].fallback)\n : null\n const metaParts = [platformLabel, attendeeCount > 0 ? attendeesLabel : null].filter(\n (part): part is string => Boolean(part),\n )\n const showSummary = aiSummaries && metaParts.length > 0\n const showJoin = aiSummaries && Boolean(joinUrl)\n\n return (\n <Popover open={open} onOpenChange={onOpenChange}>\n <PopoverTrigger asChild>{children}</PopoverTrigger>\n <PopoverContent align=\"start\" side=\"right\" sideOffset={8} className=\"w-56 p-3\">\n <div className=\"flex flex-col gap-2\">\n <p className=\"text-sm font-semibold leading-5 text-foreground\">{title}</p>\n <p className=\"text-xs leading-4 text-muted-foreground\">{`${dateLabel} \u00B7 ${timeRange}`}</p>\n {showSummary ? (\n <p className=\"flex items-center gap-1 text-xs leading-4 text-muted-foreground\">\n <Sparkles aria-hidden className=\"size-3.5 shrink-0\" />\n <span className=\"truncate\">{metaParts.join(' \u00B7 ')}</span>\n </p>\n ) : null}\n <div className=\"flex items-center gap-2 pt-1\">\n {showJoin ? (\n <Button type=\"button\" size=\"sm\" onClick={() => onJoin(item)}>\n {t('customers.calendar.peek.join', 'Join')}\n </Button>\n ) : null}\n {canManage ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => {\n onOpenChange(false)\n onEdit(item)\n }}\n >\n {t('customers.calendar.peek.edit', 'Edit')}\n </Button>\n ) : (\n <SimpleTooltip\n content={t(\n 'customers.calendar.peek.editForbidden',\n \"You don't have permission to edit events\",\n )}\n >\n <span className=\"inline-flex\">\n <Button type=\"button\" size=\"sm\" variant=\"secondary\" disabled className=\"pointer-events-none\">\n {t('customers.calendar.peek.edit', 'Edit')}\n </Button>\n </span>\n </SimpleTooltip>\n )}\n </div>\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { formatTimeRange } from './EventBlock'\nimport type { CalendarItem, CalendarPlatform } from './types'\n\nconst PLATFORM_FULL_LABELS: Record<CalendarPlatform, { key: string; fallback: string }> = {\n zoom: { key: 'customers.calendar.platformFull.zoom', fallback: 'Zoom' },\n meet: { key: 'customers.calendar.platformFull.meet', fallback: 'Google Meet' },\n slack: { key: 'customers.calendar.platformFull.slack', fallback: 'Slack' },\n teams: { key: 'customers.calendar.platformFull.teams', fallback: 'Microsoft Teams' },\n}\n\nexport type EventPeekPopoverProps = {\n item: CalendarItem\n open: boolean\n joinUrl: string | null\n aiSummaries: boolean\n canManage?: boolean\n side?: React.ComponentProps<typeof PopoverContent>['side']\n onOpenChange(open: boolean): void\n onJoin(item: CalendarItem): void\n onEdit(item: CalendarItem): void\n children: React.ReactNode\n}\n\nexport function EventPeekPopover({\n item,\n open,\n joinUrl,\n aiSummaries,\n canManage = true,\n side = 'right',\n onOpenChange,\n onJoin,\n onEdit,\n children,\n}: EventPeekPopoverProps) {\n const t = useT()\n const locale = useLocale()\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n const dateLabel = React.useMemo(\n () => new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(item.start),\n [locale, item.start],\n )\n const timeRange = item.allDay\n ? t('customers.calendar.grid.allDay', 'All day')\n : formatTimeRange(locale, item.start, item.end)\n\n const attendeeCount = item.participants.length\n const attendeesLabel =\n attendeeCount === 1\n ? t('customers.calendar.peek.attendee', '1 attendee')\n : t('customers.calendar.peek.attendees', '{count} attendees', { count: attendeeCount })\n const platformLabel =\n item.locationKind === 'platform' && item.platform\n ? t(PLATFORM_FULL_LABELS[item.platform].key, PLATFORM_FULL_LABELS[item.platform].fallback)\n : null\n const metaParts = [platformLabel, attendeeCount > 0 ? attendeesLabel : null].filter(\n (part): part is string => Boolean(part),\n )\n const showSummary = aiSummaries && metaParts.length > 0\n const showJoin = Boolean(joinUrl)\n\n return (\n <Popover open={open} onOpenChange={onOpenChange}>\n <PopoverTrigger asChild>{children}</PopoverTrigger>\n <PopoverContent align=\"start\" side={side} sideOffset={8} className=\"w-56 p-3\">\n <div className=\"flex flex-col gap-2\">\n <p className=\"text-sm font-semibold leading-5 text-foreground\">{title}</p>\n <p className=\"text-xs leading-4 text-muted-foreground\">{`${dateLabel} \u00B7 ${timeRange}`}</p>\n {showSummary ? (\n <p className=\"flex items-center gap-1 text-xs leading-4 text-muted-foreground\">\n <Sparkles aria-hidden className=\"size-3.5 shrink-0\" />\n <span className=\"truncate\">{metaParts.join(' \u00B7 ')}</span>\n </p>\n ) : null}\n <div className=\"flex items-center gap-2 pt-1\">\n {showJoin ? (\n <Button type=\"button\" size=\"sm\" onClick={() => onJoin(item)}>\n {t('customers.calendar.peek.join', 'Join')}\n </Button>\n ) : null}\n {canManage ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => {\n onOpenChange(false)\n onEdit(item)\n }}\n >\n {t('customers.calendar.peek.edit', 'Edit')}\n </Button>\n ) : (\n <SimpleTooltip\n content={t(\n 'customers.calendar.peek.editForbidden',\n \"You don't have permission to edit events\",\n )}\n >\n <span className=\"inline-flex\">\n <Button type=\"button\" size=\"sm\" variant=\"secondary\" disabled className=\"pointer-events-none\">\n {t('customers.calendar.peek.edit', 'Edit')}\n </Button>\n </span>\n </SimpleTooltip>\n )}\n </div>\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,19 @@
1
+ async function failAbandonedRun(payload, info) {
2
+ const data = payload;
3
+ const runId = data?.payload?.runId;
4
+ const scope = data?.payload?.scope;
5
+ if (typeof runId !== "string" || typeof scope?.organizationId !== "string" || typeof scope?.tenantId !== "string") return;
6
+ const runScope = { organizationId: scope.organizationId, tenantId: scope.tenantId };
7
+ const errorMessage = `the queue abandoned this run's job without running it: ${info.reason}`;
8
+ const { createRequestContainer } = await import("@open-mercato/shared/lib/di/container");
9
+ const container = await createRequestContainer();
10
+ const runService = container.resolve("dataSyncRunService");
11
+ const run = await runService.markStatus(runId, "failed", runScope, errorMessage);
12
+ if (!run?.progressJobId) return;
13
+ const progressService = container.resolve("progressService");
14
+ await progressService.failJob(run.progressJobId, { errorMessage }, runScope);
15
+ }
16
+ export {
17
+ failAbandonedRun
18
+ };
19
+ //# sourceMappingURL=abandoned-run.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/data_sync/lib/abandoned-run.ts"],
4
+ "sourcesContent": ["import type { AbandonedJobInfo } from '@open-mercato/queue'\n\n/**\n * Fail the run behind a job the queue gave up on.\n *\n * A resumable data_sync job is ONE long-lived job for a whole run \u2014 `runImport` is entered once and\n * stays there for the duration, which for a full backfill is days. Every worker death (a deploy, an\n * OOM) therefore stalls that job, and BullMQ's stalled counter is cumulative for the job's life:\n * past `DATA_SYNC_MAX_STALLED_COUNT` it writes a deferred failure and the next worker fails the job\n * BEFORE calling the processor.\n *\n * So `runImport` never runs, never throws, and never finalizes \u2014 and `sync_runs` is left saying\n * `running` for a run nothing is running, forever. That is the residual state `queue-policy.ts`\n * describes as what remains after a job exhausts its stall budget; this repairs it.\n *\n * It only ever moves a non-terminal run: `markStatus` refuses to overwrite `completed` / `failed` /\n * `cancelled`, so a job abandoned after its run has ended is a no-op. `onJobAbandoned` may deliver\n * the same job more than once, which for the same reason is also a no-op.\n *\n * The run's progress job is failed alongside it, the way the worker's own catch path does in\n * `workers/sync-import.ts`. Without that the two halves disagree \u2014 the run reads `failed` while the\n * operation still shows as in flight in the top bar \u2014 until the progress staleness sweep eventually\n * fails it with a generic no-heartbeat message instead of the reason the queue recorded.\n */\nexport async function failAbandonedRun(payload: unknown, info: AbandonedJobInfo): Promise<void> {\n const data = payload as { payload?: { runId?: unknown; scope?: { organizationId?: unknown; tenantId?: unknown } } } | undefined\n const runId = data?.payload?.runId\n const scope = data?.payload?.scope\n // Nothing to repair without both: the run row is keyed by id AND tenant scope.\n if (typeof runId !== 'string' || typeof scope?.organizationId !== 'string' || typeof scope?.tenantId !== 'string') return\n\n const runScope = { organizationId: scope.organizationId, tenantId: scope.tenantId }\n const errorMessage = `the queue abandoned this run's job without running it: ${info.reason}`\n\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const runService = container.resolve('dataSyncRunService') as {\n markStatus(\n runId: string,\n status: string,\n scope: { organizationId: string; tenantId: string },\n error?: string,\n ): Promise<{ progressJobId?: string | null } | null>\n }\n const run = await runService.markStatus(runId, 'failed', runScope, errorMessage)\n if (!run?.progressJobId) return\n\n const progressService = container.resolve('progressService') as {\n failJob(\n jobId: string,\n input: { errorMessage: string },\n ctx: { organizationId: string; tenantId: string },\n ): Promise<unknown>\n }\n await progressService.failJob(run.progressJobId, { errorMessage }, runScope)\n}\n"],
5
+ "mappings": "AAwBA,eAAsB,iBAAiB,SAAkB,MAAuC;AAC9F,QAAM,OAAO;AACb,QAAM,QAAQ,MAAM,SAAS;AAC7B,QAAM,QAAQ,MAAM,SAAS;AAE7B,MAAI,OAAO,UAAU,YAAY,OAAO,OAAO,mBAAmB,YAAY,OAAO,OAAO,aAAa,SAAU;AAEnH,QAAM,WAAW,EAAE,gBAAgB,MAAM,gBAAgB,UAAU,MAAM,SAAS;AAClF,QAAM,eAAe,0DAA0D,KAAK,MAAM;AAE1F,QAAM,EAAE,uBAAuB,IAAI,MAAM,OAAO,uCAAuC;AACvF,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,aAAa,UAAU,QAAQ,oBAAoB;AAQzD,QAAM,MAAM,MAAM,WAAW,WAAW,OAAO,UAAU,UAAU,YAAY;AAC/E,MAAI,CAAC,KAAK,cAAe;AAEzB,QAAM,kBAAkB,UAAU,QAAQ,iBAAiB;AAO3D,QAAM,gBAAgB,QAAQ,IAAI,eAAe,EAAE,aAAa,GAAG,QAAQ;AAC7E;",
6
+ "names": []
7
+ }
@@ -3,6 +3,7 @@ import {
3
3
  DATA_SYNC_LOCK_DURATION_MS,
4
4
  DATA_SYNC_MAX_STALLED_COUNT
5
5
  } from "../lib/queue-policy.js";
6
+ import { failAbandonedRun } from "../lib/abandoned-run.js";
6
7
  import { createLogger } from "@open-mercato/shared/lib/logger";
7
8
  const logger = createLogger("data_sync").child({ component: "sync-export" });
8
9
  const metadata = {
@@ -10,7 +11,10 @@ const metadata = {
10
11
  id: "data-sync:export",
11
12
  concurrency: 5,
12
13
  lockDuration: DATA_SYNC_LOCK_DURATION_MS,
13
- maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT
14
+ maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,
15
+ // Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker
16
+ // restart, and the process that restarts never constructs the producer's queue instance.
17
+ onJobAbandoned: failAbandonedRun
14
18
  };
15
19
  async function handle(job, ctx) {
16
20
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/workers/sync-export.ts"],
4
- "sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncEngine } from '../lib/sync-engine'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport {\n DATA_SYNC_EXPORT_QUEUE,\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n} from '../lib/queue-policy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-export' })\n\ntype SyncJobPayload = {\n runId: string\n batchSize: number\n scope: {\n organizationId: string\n tenantId: string\n userId?: string | null\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: DATA_SYNC_EXPORT_QUEUE,\n id: 'data-sync:export',\n concurrency: 5,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {\n try {\n const engine = ctx.resolve<SyncEngine>('dataSyncEngine')\n await engine.runExport(job.payload.runId, job.payload.batchSize, job.payload.scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Data sync export worker failed'\n const errorStack = error instanceof Error ? error.stack : undefined\n\n try {\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const run = await syncRunService.getRun(job.payload.runId, job.payload.scope)\n\n if (run && run.status !== 'completed' && run.status !== 'failed' && run.status !== 'cancelled') {\n await syncRunService.markStatus(run.id, 'failed', job.payload.scope, message)\n if (run.progressJobId) {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: message,\n errorStack,\n },\n job.payload.scope,\n )\n }\n }\n } catch (finalizeError) {\n logger.error('Failed to finalize crashed export worker job', { err: finalizeError })\n }\n\n throw error\n }\n}\n"],
5
- "mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAYpE,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AACnB;AAMA,eAAO,OAA8B,KAAgC,KAAoC;AACvG,MAAI;AACF,UAAM,SAAS,IAAI,QAAoB,gBAAgB;AACvD,UAAM,OAAO,UAAU,IAAI,QAAQ,OAAO,IAAI,QAAQ,WAAW,IAAI,QAAQ,KAAK;AAAA,EACpF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,aAAa,iBAAiB,QAAQ,MAAM,QAAQ;AAE1D,QAAI;AACF,YAAM,iBAAiB,IAAI,QAAwB,oBAAoB;AACvE,YAAM,kBAAkB,IAAI,QAAyB,iBAAiB;AACtE,YAAM,MAAM,MAAM,eAAe,OAAO,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK;AAE5E,UAAI,OAAO,IAAI,WAAW,eAAe,IAAI,WAAW,YAAY,IAAI,WAAW,aAAa;AAC9F,cAAM,eAAe,WAAW,IAAI,IAAI,UAAU,IAAI,QAAQ,OAAO,OAAO;AAC5E,YAAI,IAAI,eAAe;AACrB,gBAAM,gBAAgB;AAAA,YACpB,IAAI;AAAA,YACJ;AAAA,cACE,cAAc;AAAA,cACd;AAAA,YACF;AAAA,YACA,IAAI,QAAQ;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,eAAe;AACtB,aAAO,MAAM,gDAAgD,EAAE,KAAK,cAAc,CAAC;AAAA,IACrF;AAEA,UAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncEngine } from '../lib/sync-engine'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport {\n DATA_SYNC_EXPORT_QUEUE,\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n} from '../lib/queue-policy'\nimport { failAbandonedRun } from '../lib/abandoned-run'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-export' })\n\ntype SyncJobPayload = {\n runId: string\n batchSize: number\n scope: {\n organizationId: string\n tenantId: string\n userId?: string | null\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: DATA_SYNC_EXPORT_QUEUE,\n id: 'data-sync:export',\n concurrency: 5,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n // Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker\n // restart, and the process that restarts never constructs the producer's queue instance.\n onJobAbandoned: failAbandonedRun,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {\n try {\n const engine = ctx.resolve<SyncEngine>('dataSyncEngine')\n await engine.runExport(job.payload.runId, job.payload.batchSize, job.payload.scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Data sync export worker failed'\n const errorStack = error instanceof Error ? error.stack : undefined\n\n try {\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const run = await syncRunService.getRun(job.payload.runId, job.payload.scope)\n\n if (run && run.status !== 'completed' && run.status !== 'failed' && run.status !== 'cancelled') {\n await syncRunService.markStatus(run.id, 'failed', job.payload.scope, message)\n if (run.progressJobId) {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: message,\n errorStack,\n },\n job.payload.scope,\n )\n }\n }\n } catch (finalizeError) {\n logger.error('Failed to finalize crashed export worker job', { err: finalizeError })\n }\n\n throw error\n }\n}\n"],
5
+ "mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAYpE,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA;AAAA;AAAA,EAGjB,gBAAgB;AAClB;AAMA,eAAO,OAA8B,KAAgC,KAAoC;AACvG,MAAI;AACF,UAAM,SAAS,IAAI,QAAoB,gBAAgB;AACvD,UAAM,OAAO,UAAU,IAAI,QAAQ,OAAO,IAAI,QAAQ,WAAW,IAAI,QAAQ,KAAK;AAAA,EACpF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,aAAa,iBAAiB,QAAQ,MAAM,QAAQ;AAE1D,QAAI;AACF,YAAM,iBAAiB,IAAI,QAAwB,oBAAoB;AACvE,YAAM,kBAAkB,IAAI,QAAyB,iBAAiB;AACtE,YAAM,MAAM,MAAM,eAAe,OAAO,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK;AAE5E,UAAI,OAAO,IAAI,WAAW,eAAe,IAAI,WAAW,YAAY,IAAI,WAAW,aAAa;AAC9F,cAAM,eAAe,WAAW,IAAI,IAAI,UAAU,IAAI,QAAQ,OAAO,OAAO;AAC5E,YAAI,IAAI,eAAe;AACrB,gBAAM,gBAAgB;AAAA,YACpB,IAAI;AAAA,YACJ;AAAA,cACE,cAAc;AAAA,cACd;AAAA,YACF;AAAA,YACA,IAAI,QAAQ;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,eAAe;AACtB,aAAO,MAAM,gDAAgD,EAAE,KAAK,cAAc,CAAC;AAAA,IACrF;AAEA,UAAM;AAAA,EACR;AACF;",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ import {
3
3
  DATA_SYNC_LOCK_DURATION_MS,
4
4
  DATA_SYNC_MAX_STALLED_COUNT
5
5
  } from "../lib/queue-policy.js";
6
+ import { failAbandonedRun } from "../lib/abandoned-run.js";
6
7
  import { createLogger } from "@open-mercato/shared/lib/logger";
7
8
  const logger = createLogger("data_sync").child({ component: "sync-import" });
8
9
  const metadata = {
@@ -10,7 +11,10 @@ const metadata = {
10
11
  id: "data-sync:import",
11
12
  concurrency: 5,
12
13
  lockDuration: DATA_SYNC_LOCK_DURATION_MS,
13
- maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT
14
+ maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,
15
+ // Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker
16
+ // restart, and the process that restarts never constructs the producer's queue instance.
17
+ onJobAbandoned: failAbandonedRun
14
18
  };
15
19
  async function handle(job, ctx) {
16
20
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/workers/sync-import.ts"],
4
- "sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncEngine } from '../lib/sync-engine'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport {\n DATA_SYNC_IMPORT_QUEUE,\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n} from '../lib/queue-policy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-import' })\n\ntype SyncJobPayload = {\n runId: string\n batchSize: number\n scope: {\n organizationId: string\n tenantId: string\n userId?: string | null\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: DATA_SYNC_IMPORT_QUEUE,\n id: 'data-sync:import',\n concurrency: 5,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {\n try {\n const engine = ctx.resolve<SyncEngine>('dataSyncEngine')\n await engine.runImport(job.payload.runId, job.payload.batchSize, job.payload.scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Data sync import worker failed'\n const errorStack = error instanceof Error ? error.stack : undefined\n\n try {\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const run = await syncRunService.getRun(job.payload.runId, job.payload.scope)\n\n if (run && run.status !== 'completed' && run.status !== 'failed' && run.status !== 'cancelled') {\n await syncRunService.markStatus(run.id, 'failed', job.payload.scope, message)\n if (run.progressJobId) {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: message,\n errorStack,\n },\n job.payload.scope,\n )\n }\n }\n } catch (finalizeError) {\n logger.error('Failed to finalize crashed import worker job', { err: finalizeError })\n }\n\n throw error\n }\n}\n"],
5
- "mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAYpE,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AACnB;AAMA,eAAO,OAA8B,KAAgC,KAAoC;AACvG,MAAI;AACF,UAAM,SAAS,IAAI,QAAoB,gBAAgB;AACvD,UAAM,OAAO,UAAU,IAAI,QAAQ,OAAO,IAAI,QAAQ,WAAW,IAAI,QAAQ,KAAK;AAAA,EACpF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,aAAa,iBAAiB,QAAQ,MAAM,QAAQ;AAE1D,QAAI;AACF,YAAM,iBAAiB,IAAI,QAAwB,oBAAoB;AACvE,YAAM,kBAAkB,IAAI,QAAyB,iBAAiB;AACtE,YAAM,MAAM,MAAM,eAAe,OAAO,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK;AAE5E,UAAI,OAAO,IAAI,WAAW,eAAe,IAAI,WAAW,YAAY,IAAI,WAAW,aAAa;AAC9F,cAAM,eAAe,WAAW,IAAI,IAAI,UAAU,IAAI,QAAQ,OAAO,OAAO;AAC5E,YAAI,IAAI,eAAe;AACrB,gBAAM,gBAAgB;AAAA,YACpB,IAAI;AAAA,YACJ;AAAA,cACE,cAAc;AAAA,cACd;AAAA,YACF;AAAA,YACA,IAAI,QAAQ;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,eAAe;AACtB,aAAO,MAAM,gDAAgD,EAAE,KAAK,cAAc,CAAC;AAAA,IACrF;AAEA,UAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncEngine } from '../lib/sync-engine'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport {\n DATA_SYNC_IMPORT_QUEUE,\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n} from '../lib/queue-policy'\nimport { failAbandonedRun } from '../lib/abandoned-run'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-import' })\n\ntype SyncJobPayload = {\n runId: string\n batchSize: number\n scope: {\n organizationId: string\n tenantId: string\n userId?: string | null\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: DATA_SYNC_IMPORT_QUEUE,\n id: 'data-sync:import',\n concurrency: 5,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n // Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker\n // restart, and the process that restarts never constructs the producer's queue instance.\n onJobAbandoned: failAbandonedRun,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {\n try {\n const engine = ctx.resolve<SyncEngine>('dataSyncEngine')\n await engine.runImport(job.payload.runId, job.payload.batchSize, job.payload.scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Data sync import worker failed'\n const errorStack = error instanceof Error ? error.stack : undefined\n\n try {\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const run = await syncRunService.getRun(job.payload.runId, job.payload.scope)\n\n if (run && run.status !== 'completed' && run.status !== 'failed' && run.status !== 'cancelled') {\n await syncRunService.markStatus(run.id, 'failed', job.payload.scope, message)\n if (run.progressJobId) {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: message,\n errorStack,\n },\n job.payload.scope,\n )\n }\n }\n } catch (finalizeError) {\n logger.error('Failed to finalize crashed import worker job', { err: finalizeError })\n }\n\n throw error\n }\n}\n"],
5
+ "mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAYpE,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA;AAAA;AAAA,EAGjB,gBAAgB;AAClB;AAMA,eAAO,OAA8B,KAAgC,KAAoC;AACvG,MAAI;AACF,UAAM,SAAS,IAAI,QAAoB,gBAAgB;AACvD,UAAM,OAAO,UAAU,IAAI,QAAQ,OAAO,IAAI,QAAQ,WAAW,IAAI,QAAQ,KAAK;AAAA,EACpF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,aAAa,iBAAiB,QAAQ,MAAM,QAAQ;AAE1D,QAAI;AACF,YAAM,iBAAiB,IAAI,QAAwB,oBAAoB;AACvE,YAAM,kBAAkB,IAAI,QAAyB,iBAAiB;AACtE,YAAM,MAAM,MAAM,eAAe,OAAO,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK;AAE5E,UAAI,OAAO,IAAI,WAAW,eAAe,IAAI,WAAW,YAAY,IAAI,WAAW,aAAa;AAC9F,cAAM,eAAe,WAAW,IAAI,IAAI,UAAU,IAAI,QAAQ,OAAO,OAAO;AAC5E,YAAI,IAAI,eAAe;AACrB,gBAAM,gBAAgB;AAAA,YACpB,IAAI;AAAA,YACJ;AAAA,cACE,cAAc;AAAA,cACd;AAAA,YACF;AAAA,YACA,IAAI,QAAQ;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,eAAe;AACtB,aAAO,MAAM,gDAAgD,EAAE,KAAK,cAAc,CAAC;AAAA,IACrF;AAEA,UAAM;AAAA,EACR;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,13 @@
1
+ import { asClass, asFunction } from "awilix";
2
+ import { DefaultOrganizationScopeService } from "./services/organizationScopeService.js";
3
+ import { DefaultOrganizationHierarchyService } from "./services/organizationHierarchyService.js";
1
4
  function register(container) {
5
+ container.register({
6
+ organizationHierarchyService: asClass(DefaultOrganizationHierarchyService).scoped(),
7
+ organizationScopeService: asFunction(
8
+ (cradle) => new DefaultOrganizationScopeService(cradle.em, cradle.rbacService, container)
9
+ ).scoped().proxy()
10
+ });
2
11
  }
3
12
  export {
4
13
  register
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/directory/di.ts"],
4
- "sourcesContent": ["import type { AppContainer } from '@open-mercato/shared/lib/di/container'\n\nexport function register(container: AppContainer) {\n // Directory module has no services yet; placeholder for future overrides\n}\n\n"],
5
- "mappings": "AAEO,SAAS,SAAS,WAAyB;AAElD;",
4
+ "sourcesContent": ["import { asClass, asFunction } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { DefaultOrganizationScopeService } from './services/organizationScopeService'\nimport { DefaultOrganizationHierarchyService } from './services/organizationHierarchyService'\n\ntype OrganizationScopeRbac = ConstructorParameters<typeof DefaultOrganizationScopeService>[1]\n\nexport function register(container: AppContainer) {\n container.register({\n organizationHierarchyService: asClass(DefaultOrganizationHierarchyService).scoped(),\n organizationScopeService: asFunction((cradle: { em: EntityManager; rbacService: OrganizationScopeRbac }) =>\n new DefaultOrganizationScopeService(cradle.em, cradle.rbacService, container),\n )\n .scoped()\n .proxy(),\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,SAAS,kBAAkB;AAGpC,SAAS,uCAAuC;AAChD,SAAS,2CAA2C;AAI7C,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,8BAA8B,QAAQ,mCAAmC,EAAE,OAAO;AAAA,IAClF,0BAA0B;AAAA,MAAW,CAAC,WACpC,IAAI,gCAAgC,OAAO,IAAI,OAAO,aAAa,SAAS;AAAA,IAC9E,EACG,OAAO,EACP,MAAM;AAAA,EACX,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,29 @@
1
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
+ import { Organization } from "../data/entities.js";
3
+ function normalizeIds(values) {
4
+ if (!Array.isArray(values)) return [];
5
+ return Array.from(new Set(values.map((value) => typeof value === "string" ? value.trim() : "").filter(Boolean)));
6
+ }
7
+ class DefaultOrganizationHierarchyService {
8
+ constructor(em) {
9
+ this.em = em;
10
+ }
11
+ async resolveAncestorIds(input) {
12
+ const organization = await findOneWithDecryption(
13
+ this.em,
14
+ Organization,
15
+ {
16
+ id: input.organizationId,
17
+ tenant: input.tenantId,
18
+ deletedAt: null
19
+ },
20
+ { fields: ["id", "ancestorIds"] },
21
+ { tenantId: input.tenantId, organizationId: input.organizationId }
22
+ );
23
+ return organization ? normalizeIds(organization.ancestorIds) : null;
24
+ }
25
+ }
26
+ export {
27
+ DefaultOrganizationHierarchyService
28
+ };
29
+ //# sourceMappingURL=organizationHierarchyService.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/directory/services/organizationHierarchyService.ts"],
4
+ "sourcesContent": ["import type { FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Organization } from '../data/entities'\n\nfunction normalizeIds(values: unknown): string[] {\n if (!Array.isArray(values)) return []\n return Array.from(new Set(values\n .map((value) => typeof value === 'string' ? value.trim() : '')\n .filter(Boolean)))\n}\n\nexport class DefaultOrganizationHierarchyService implements OrganizationHierarchyService {\n constructor(private readonly em: EntityManager) {}\n\n async resolveAncestorIds(input: {\n tenantId: string\n organizationId: string\n }): Promise<string[] | null> {\n const organization = await findOneWithDecryption(\n this.em,\n Organization,\n {\n id: input.organizationId,\n tenant: input.tenantId,\n deletedAt: null,\n } as FilterQuery<Organization>,\n { fields: ['id', 'ancestorIds'] },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n return organization ? normalizeIds(organization.ancestorIds) : null\n }\n}\n"],
5
+ "mappings": "AAGA,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAE7B,SAAS,aAAa,QAA2B;AAC/C,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACpC,SAAO,MAAM,KAAK,IAAI,IAAI,OACvB,IAAI,CAAC,UAAU,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI,EAAE,EAC5D,OAAO,OAAO,CAAC,CAAC;AACrB;AAEO,MAAM,oCAA4E;AAAA,EACvF,YAA6B,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAEjD,MAAM,mBAAmB,OAGI;AAC3B,UAAM,eAAe,MAAM;AAAA,MACzB,KAAK;AAAA,MACL;AAAA,MACA;AAAA,QACE,IAAI,MAAM;AAAA,QACV,QAAQ,MAAM;AAAA,QACd,WAAW;AAAA,MACb;AAAA,MACA,EAAE,QAAQ,CAAC,MAAM,aAAa,EAAE;AAAA,MAChC,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,IACnE;AACA,WAAO,eAAe,aAAa,aAAa,WAAW,IAAI;AAAA,EACjE;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,56 @@
1
+ import { resolveOrganizationScope, resolveOrganizationScopeForRequest } from "../utils/organizationScope.js";
2
+ class DefaultOrganizationScopeService {
3
+ constructor(em, rbac, container) {
4
+ this.em = em;
5
+ this.rbac = rbac;
6
+ this.container = container;
7
+ }
8
+ resolve(input) {
9
+ const auth = input.auth;
10
+ if (input.freshAcl && auth?.sub) {
11
+ return this.resolveFresh({
12
+ auth,
13
+ selectedId: input.selectedId,
14
+ tenantId: input.tenantId
15
+ }).then((result) => result.scope);
16
+ }
17
+ return resolveOrganizationScope({
18
+ em: this.em,
19
+ rbac: this.rbac,
20
+ auth: input.auth,
21
+ selectedId: input.selectedId,
22
+ tenantId: input.tenantId
23
+ });
24
+ }
25
+ async resolveFresh(input) {
26
+ await this.rbac.invalidateUserCache(input.auth.sub);
27
+ const acl = await this.rbac.loadAcl(input.auth.sub, {
28
+ tenantId: input.tenantId ?? input.auth.tenantId ?? null,
29
+ organizationId: input.selectedId ?? input.auth.orgId ?? null
30
+ });
31
+ const freshRbac = {
32
+ loadAcl: async () => acl
33
+ };
34
+ const scope = await resolveOrganizationScope({
35
+ em: this.em,
36
+ rbac: freshRbac,
37
+ auth: input.auth,
38
+ selectedId: input.selectedId,
39
+ tenantId: input.tenantId
40
+ });
41
+ return { scope, acl };
42
+ }
43
+ resolveForRequest(input) {
44
+ return resolveOrganizationScopeForRequest({
45
+ container: this.container,
46
+ auth: input.auth,
47
+ request: input.request,
48
+ selectedId: input.selectedId,
49
+ tenantId: input.tenantId
50
+ });
51
+ }
52
+ }
53
+ export {
54
+ DefaultOrganizationScopeService
55
+ };
56
+ //# sourceMappingURL=organizationScopeService.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/directory/services/organizationScopeService.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type {\n OrganizationScopeService,\n OrganizationScopeRequest,\n} from '@open-mercato/shared/lib/auth/principal-service'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScope, resolveOrganizationScopeForRequest } from '../utils/organizationScope'\n\ntype OrganizationScopeRbac = {\n invalidateUserCache(userId: string): Promise<void>\n loadAcl(\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ): Promise<{ isSuperAdmin: boolean; features: string[]; organizations: string[] | null }>\n}\n\nexport class DefaultOrganizationScopeService implements OrganizationScopeService {\n constructor(\n private readonly em: EntityManager,\n private readonly rbac: OrganizationScopeRbac,\n private readonly container: AwilixContainer,\n ) {}\n\n resolve(input: {\n auth: AuthContext | null | undefined\n selectedId?: string | null\n tenantId?: string | null\n freshAcl?: boolean\n }) {\n const auth = input.auth\n if (input.freshAcl && auth?.sub) {\n return this.resolveFresh({\n auth,\n selectedId: input.selectedId,\n tenantId: input.tenantId,\n }).then((result) => result.scope)\n }\n return resolveOrganizationScope({\n em: this.em,\n rbac: this.rbac as Parameters<typeof resolveOrganizationScope>[0]['rbac'],\n auth: input.auth,\n selectedId: input.selectedId,\n tenantId: input.tenantId,\n })\n }\n\n async resolveFresh(input: {\n auth: NonNullable<AuthContext>\n selectedId?: string | null\n tenantId?: string | null\n }) {\n await this.rbac.invalidateUserCache(input.auth.sub)\n const acl = await this.rbac.loadAcl(input.auth.sub, {\n tenantId: input.tenantId ?? input.auth.tenantId ?? null,\n organizationId: input.selectedId ?? input.auth.orgId ?? null,\n })\n const freshRbac: Parameters<typeof resolveOrganizationScope>[0]['rbac'] = {\n loadAcl: async () => acl,\n }\n const scope = await resolveOrganizationScope({\n em: this.em,\n rbac: freshRbac,\n auth: input.auth,\n selectedId: input.selectedId,\n tenantId: input.tenantId,\n })\n return { scope, acl }\n }\n\n resolveForRequest(input: {\n auth: AuthContext | null | undefined\n request?: OrganizationScopeRequest\n selectedId?: string | null\n tenantId?: string | null\n }) {\n return resolveOrganizationScopeForRequest({\n container: this.container,\n auth: input.auth,\n request: input.request,\n selectedId: input.selectedId,\n tenantId: input.tenantId,\n })\n }\n}\n"],
5
+ "mappings": "AAOA,SAAS,0BAA0B,0CAA0C;AAUtE,MAAM,gCAAoE;AAAA,EAC/E,YACmB,IACA,MACA,WACjB;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAEH,QAAQ,OAKL;AACD,UAAM,OAAO,MAAM;AACnB,QAAI,MAAM,YAAY,MAAM,KAAK;AAC/B,aAAO,KAAK,aAAa;AAAA,QACvB;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,UAAU,MAAM;AAAA,MAClB,CAAC,EAAE,KAAK,CAAC,WAAW,OAAO,KAAK;AAAA,IAClC;AACA,WAAO,yBAAyB;AAAA,MAC9B,IAAI,KAAK;AAAA,MACT,MAAM,KAAK;AAAA,MACX,MAAM,MAAM;AAAA,MACZ,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,IAClB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,aAAa,OAIhB;AACD,UAAM,KAAK,KAAK,oBAAoB,MAAM,KAAK,GAAG;AAClD,UAAM,MAAM,MAAM,KAAK,KAAK,QAAQ,MAAM,KAAK,KAAK;AAAA,MAClD,UAAU,MAAM,YAAY,MAAM,KAAK,YAAY;AAAA,MACnD,gBAAgB,MAAM,cAAc,MAAM,KAAK,SAAS;AAAA,IAC1D,CAAC;AACD,UAAM,YAAoE;AAAA,MACxE,SAAS,YAAY;AAAA,IACvB;AACA,UAAM,QAAQ,MAAM,yBAAyB;AAAA,MAC3C,IAAI,KAAK;AAAA,MACT,MAAM;AAAA,MACN,MAAM,MAAM;AAAA,MACZ,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,IAClB,CAAC;AACD,WAAO,EAAE,OAAO,IAAI;AAAA,EACtB;AAAA,EAEA,kBAAkB,OAKf;AACD,WAAO,mCAAmC;AAAA,MACxC,WAAW,KAAK;AAAA,MAChB,MAAM,MAAM;AAAA,MACZ,SAAS,MAAM;AAAA,MACf,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,IAClB,CAAC;AAAA,EACH;AACF;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/directory/utils/organizationScope.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { getCurrentCacheTenant, runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('directory').child({ component: 'org-scope-cache' })\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type OrganizationScope = {\n selectedId: string | null\n filterIds: string[] | null\n allowedIds: string[] | null\n tenantId: string | null\n // True when the caller explicitly selected a concrete organization (cookie /\n // selectedId param) that could not be honored \u2014 it does not exist for the\n // tenant or is not accessible. Reads degrade gracefully (filterIds/selectedId\n // fall back to the caller's accessible orgs), but writes MUST fail loudly on\n // this so a record never lands under an org the caller did not intend. Absent\n // (undefined) means the selection \u2014 if any \u2014 was honored.\n selectionRejected?: boolean\n}\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded as a backstop. Tag-based invalidation also fires\n// eagerly: per-user entries are dropped by RbacService.invalidateUserCache\n// (every ACL/role grant change goes through it \u2014 see buildOrgScopeUserCacheTag)\n// and per-tenant entries by the directory.organization.* subscriber plus\n// RbacService.invalidateTenantCache (role-ACL changes).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\n// Tag builders are exported so the modules that own the \"this user's scope\n// changed\" / \"this tenant's org tree changed\" signals (auth RBAC invalidation,\n// the directory.organization.* subscriber) can drop the matching cross-request\n// cache entries without re-deriving the tag format. Keeping the format in one\n// place is what lets the TTL be enabled safely (issue #2259).\nexport function buildOrgScopeUserCacheTag(userId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`\n}\n\nexport function buildOrgScopeTenantCacheTag(tenantId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n buildOrgScopeUserCacheTag(parts.userId),\n buildOrgScopeTenantCacheTag(parts.effectiveTenantId),\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n const flagOk = record.selectionRejected === undefined || typeof record.selectionRejected === 'boolean'\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds) && flagOk\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n tenantId?: string | null,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n const cacheTenantId = tenantId === undefined ? getCurrentCacheTenant() : tenantId\n await runWithCacheTenant(cacheTenantId, () =>\n cache.deleteByTags([buildOrgScopeUserCacheTag(userId)]),\n )\n } catch (err) {\n logger.warn('Cache invalidate user failed', { err })\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await runWithCacheTenant(tenantId, () =>\n cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)]),\n )\n } catch (err) {\n logger.warn('Cache invalidate tenant failed', { err })\n }\n}\n\n// Issue #2259 \u2014 per-request memoization. resolveOrganizationScopeForRequest\n// runs at least twice per CRUD request: once for the route-level feature check\n// (resolveFeatureCheckContext) and once inside the shared factory's withCtx.\n// Those two call sites use different request-scoped DI containers but are handed\n// the SAME Request instance, so memoizing the resolved scope on a WeakMap keyed\n// by that request collapses the duplicate work \u2014 and the duplicate\n// `organizations` SELECT \u2014 into a single resolution. The inner map is keyed by\n// the same identity tuple as the cross-request cache key, so distinct explicit\n// selectedId/tenant overrides on one request stay independent. There is no\n// staleness risk: the memo lives only for the lifetime of one request and is\n// dropped with the request object by the GC.\nconst orgScopeRequestMemo = new WeakMap<object, Map<string, Promise<OrganizationScope>>>()\n\nfunction getRequestScopeMemo(request: unknown): Map<string, Promise<OrganizationScope>> | null {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null\n const key = request as object\n let memo = orgScopeRequestMemo.get(key)\n if (!memo) {\n memo = new Map<string, Promise<OrganizationScope>>()\n orgScopeRequestMemo.set(key, memo)\n }\n return memo\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nfunction normalizeOrganizationIds(ids: string[]): string[] {\n return Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n}\n\n// Map each organization id to itself plus its persisted descendant ids. Only\n// orgs that exist for the tenant and are not soft-deleted are included, so an\n// unknown/inaccessible id simply has no entry (matching the per-id query that\n// returned an empty set for it).\ntype OrgDescendantMap = Map<string, string[]>\n\n// Issue #2228 \u2014 single round-trip for org-scope resolution. Instead of issuing\n// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4\n// sequential queries per request: accessible set, fallback set, selected set),\n// gather every candidate id up front and fetch their descendant expansions in\n// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.\nasync function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {\n const unique = normalizeOrganizationIds(ids)\n if (!unique.length) return new Map()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const map: OrgDescendantMap = new Map()\n for (const org of orgs) {\n const id = String(org.id)\n const expansion = [id]\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) expansion.push(String(desc))\n }\n map.set(id, expansion)\n }\n return map\n}\n\nfunction expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {\n const set = new Set<string>()\n for (const value of ids) {\n const id = normalizeOrganizationId(value)\n if (!id || isAllOrganizationsSelection(id)) continue\n const expansion = map.get(id)\n if (!expansion) continue\n for (const entry of expansion) set.add(entry)\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: RbacService\n auth: AuthContext\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n\n // Every id that could be expanded below \u2014 accessible set, fallback (account)\n // org, and the requested selection \u2014 is known up front, so fetch them all in\n // a single `organizations` query and expand from the in-memory map.\n const candidateIds = [\n ...(accessibleList ?? []),\n ...(fallbackOrgId ? [fallbackOrgId] : []),\n ...(normalizedSelectedId ? [normalizedSelectedId] : []),\n ]\n const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)\n const loadFallbackSet = (): Set<string> | null =>\n fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = expandWithDescendants(orgDescendants, accessibleList)\n }\n\n if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n allowedSet = computed\n }\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n // A selection is only honored when it resolves to a real, non-deleted org for\n // this tenant. `orgDescendants` holds exactly the existing orgs among the\n // candidate ids, so a stale/dead id (e.g. a selected-org cookie or JWT org\n // that no longer resolves after a DB reset) has no entry and is dropped here.\n // Without the existence guard an unrestricted (all-orgs) principal \u2014 whose\n // `allowedSet` is null \u2014 would accept the dead id as `effectiveSelected`, and\n // writes derived from `selectedId` would land under an org the read scope\n // (`filterIds`) never filters to, silently orphaning the record.\n const selectionResolvesToOrg = (id: string): boolean => orgDescendants.has(id)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if ((allowedSet === null || allowedSet.has(initialSelected)) && selectionResolvesToOrg(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = loadFallbackSet()\n // Keep the write target (`selectedId`) aligned with the read scope\n // (`filterIds`): when an unrestricted principal's requested selection was\n // dropped above, fall the selection back to the account org too so a record\n // created here is always readable back by the same caller.\n if (!effectiveSelected && fallbackOrgId && filterSet && filterSet.size > 0) {\n effectiveSelected = fallbackOrgId\n }\n }\n\n if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n // A concrete organization was explicitly requested (`normalizedSelectedId` is\n // null for both \"no selection\" and the \"all organizations\" token) but the\n // resolver could not honor it \u2014 it was dropped as non-existent/inaccessible\n // and the effective selection fell back to something else. Surface this so the\n // write layer can reject the request instead of silently creating the record\n // under the fallback org. Only set the field when true to keep the scope shape\n // unchanged for the common (honored) case.\n const selectionRejected = normalizedSelectedId !== null && effectiveSelected !== normalizedSelectedId\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n ...(selectionRejected ? { selectionRejected: true } : {}),\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n const requestMemo = getRequestScopeMemo(request)\n if (requestMemo && cacheKey) {\n const memoized = requestMemo.get(cacheKey)\n if (memoized) return memoized\n }\n\n const resolveScope = async (): Promise<OrganizationScope> => {\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n logger.warn('Cache read failed', { err })\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n logger.warn('Cache write failed', { err })\n }\n }\n\n return baseScope\n }\n\n if (requestMemo && cacheKey) {\n const pending = resolveScope()\n requestMemo.set(cacheKey, pending)\n return pending\n }\n\n return resolveScope()\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<FeatureCheckContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n const organizationId =\n scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n\n return { organizationId, scope, allowedOrganizationIds }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { OrganizationScope } from '@open-mercato/shared/lib/auth/principal-service'\nimport { getCurrentCacheTenant, runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('directory').child({ component: 'org-scope-cache' })\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type { OrganizationScope }\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded as a backstop. Tag-based invalidation also fires\n// eagerly: per-user entries are dropped by RbacService.invalidateUserCache\n// (every ACL/role grant change goes through it \u2014 see buildOrgScopeUserCacheTag)\n// and per-tenant entries by the directory.organization.* subscriber plus\n// RbacService.invalidateTenantCache (role-ACL changes).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\n// Tag builders are exported so the modules that own the \"this user's scope\n// changed\" / \"this tenant's org tree changed\" signals (auth RBAC invalidation,\n// the directory.organization.* subscriber) can drop the matching cross-request\n// cache entries without re-deriving the tag format. Keeping the format in one\n// place is what lets the TTL be enabled safely (issue #2259).\nexport function buildOrgScopeUserCacheTag(userId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`\n}\n\nexport function buildOrgScopeTenantCacheTag(tenantId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n buildOrgScopeUserCacheTag(parts.userId),\n buildOrgScopeTenantCacheTag(parts.effectiveTenantId),\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n const flagOk = record.selectionRejected === undefined || typeof record.selectionRejected === 'boolean'\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds) && flagOk\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n tenantId?: string | null,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n const cacheTenantId = tenantId === undefined ? getCurrentCacheTenant() : tenantId\n await runWithCacheTenant(cacheTenantId, () =>\n cache.deleteByTags([buildOrgScopeUserCacheTag(userId)]),\n )\n } catch (err) {\n logger.warn('Cache invalidate user failed', { err })\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await runWithCacheTenant(tenantId, () =>\n cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)]),\n )\n } catch (err) {\n logger.warn('Cache invalidate tenant failed', { err })\n }\n}\n\n// Issue #2259 \u2014 per-request memoization. resolveOrganizationScopeForRequest\n// runs at least twice per CRUD request: once for the route-level feature check\n// (resolveFeatureCheckContext) and once inside the shared factory's withCtx.\n// Those two call sites use different request-scoped DI containers but are handed\n// the SAME Request instance, so memoizing the resolved scope on a WeakMap keyed\n// by that request collapses the duplicate work \u2014 and the duplicate\n// `organizations` SELECT \u2014 into a single resolution. The inner map is keyed by\n// the same identity tuple as the cross-request cache key, so distinct explicit\n// selectedId/tenant overrides on one request stay independent. There is no\n// staleness risk: the memo lives only for the lifetime of one request and is\n// dropped with the request object by the GC.\nconst orgScopeRequestMemo = new WeakMap<object, Map<string, Promise<OrganizationScope>>>()\n\nfunction getRequestScopeMemo(request: unknown): Map<string, Promise<OrganizationScope>> | null {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null\n const key = request as object\n let memo = orgScopeRequestMemo.get(key)\n if (!memo) {\n memo = new Map<string, Promise<OrganizationScope>>()\n orgScopeRequestMemo.set(key, memo)\n }\n return memo\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nfunction normalizeOrganizationIds(ids: string[]): string[] {\n return Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n}\n\n// Map each organization id to itself plus its persisted descendant ids. Only\n// orgs that exist for the tenant and are not soft-deleted are included, so an\n// unknown/inaccessible id simply has no entry (matching the per-id query that\n// returned an empty set for it).\ntype OrgDescendantMap = Map<string, string[]>\n\n// Issue #2228 \u2014 single round-trip for org-scope resolution. Instead of issuing\n// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4\n// sequential queries per request: accessible set, fallback set, selected set),\n// gather every candidate id up front and fetch their descendant expansions in\n// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.\nasync function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {\n const unique = normalizeOrganizationIds(ids)\n if (!unique.length) return new Map()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const map: OrgDescendantMap = new Map()\n for (const org of orgs) {\n const id = String(org.id)\n const expansion = [id]\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) expansion.push(String(desc))\n }\n map.set(id, expansion)\n }\n return map\n}\n\nfunction expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {\n const set = new Set<string>()\n for (const value of ids) {\n const id = normalizeOrganizationId(value)\n if (!id || isAllOrganizationsSelection(id)) continue\n const expansion = map.get(id)\n if (!expansion) continue\n for (const entry of expansion) set.add(entry)\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: Pick<RbacService, 'loadAcl'>\n auth: AuthContext | null | undefined\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n\n // Every id that could be expanded below \u2014 accessible set, fallback (account)\n // org, and the requested selection \u2014 is known up front, so fetch them all in\n // a single `organizations` query and expand from the in-memory map.\n const candidateIds = [\n ...(accessibleList ?? []),\n ...(fallbackOrgId ? [fallbackOrgId] : []),\n ...(normalizedSelectedId ? [normalizedSelectedId] : []),\n ]\n const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)\n const loadFallbackSet = (): Set<string> | null =>\n fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = expandWithDescendants(orgDescendants, accessibleList)\n }\n\n if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n allowedSet = computed\n }\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n // A selection is only honored when it resolves to a real, non-deleted org for\n // this tenant. `orgDescendants` holds exactly the existing orgs among the\n // candidate ids, so a stale/dead id (e.g. a selected-org cookie or JWT org\n // that no longer resolves after a DB reset) has no entry and is dropped here.\n // Without the existence guard an unrestricted (all-orgs) principal \u2014 whose\n // `allowedSet` is null \u2014 would accept the dead id as `effectiveSelected`, and\n // writes derived from `selectedId` would land under an org the read scope\n // (`filterIds`) never filters to, silently orphaning the record.\n const selectionResolvesToOrg = (id: string): boolean => orgDescendants.has(id)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if ((allowedSet === null || allowedSet.has(initialSelected)) && selectionResolvesToOrg(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = loadFallbackSet()\n // Keep the write target (`selectedId`) aligned with the read scope\n // (`filterIds`): when an unrestricted principal's requested selection was\n // dropped above, fall the selection back to the account org too so a record\n // created here is always readable back by the same caller.\n if (!effectiveSelected && fallbackOrgId && filterSet && filterSet.size > 0) {\n effectiveSelected = fallbackOrgId\n }\n }\n\n if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n // A concrete organization was explicitly requested (`normalizedSelectedId` is\n // null for both \"no selection\" and the \"all organizations\" token) but the\n // resolver could not honor it \u2014 it was dropped as non-existent/inaccessible\n // and the effective selection fell back to something else. Surface this so the\n // write layer can reject the request instead of silently creating the record\n // under the fallback org. Only set the field when true to keep the scope shape\n // unchanged for the common (honored) case.\n const selectionRejected = normalizedSelectedId !== null && effectiveSelected !== normalizedSelectedId\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n ...(selectionRejected ? { selectionRejected: true } : {}),\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n const requestMemo = getRequestScopeMemo(request)\n if (requestMemo && cacheKey) {\n const memoized = requestMemo.get(cacheKey)\n if (memoized) return memoized\n }\n\n const resolveScope = async (): Promise<OrganizationScope> => {\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n logger.warn('Cache read failed', { err })\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n logger.warn('Cache write failed', { err })\n }\n }\n\n return baseScope\n }\n\n if (requestMemo && cacheKey) {\n const pending = resolveScope()\n requestMemo.set(cacheKey, pending)\n return pending\n }\n\n return resolveScope()\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<FeatureCheckContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n const organizationId =\n scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n\n return { organizationId, scope, allowedOrganizationIds }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }