@open-mercato/core 0.5.1-develop.2652.0276e72e45 → 0.5.1-develop.2663.2c29774b5b
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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +26 -0
- package/dist/modules/auth/lib/backendChrome.js +3 -1
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +8 -2
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/customer_accounts/api/password/reset-confirm.js +7 -0
- package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/nav.js +77 -0
- package/dist/modules/customer_accounts/api/portal/nav.js.map +7 -0
- package/dist/modules/customer_accounts/api/signup.js +20 -8
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerSessionService.js +32 -0
- package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
- package/dist/modules/directory/api/organizations/route.js +10 -0
- package/dist/modules/directory/api/organizations/route.js.map +3 -3
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +13 -2
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/create/page.js +12 -2
- package/dist/modules/directory/backend/directory/organizations/create/page.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +4 -3
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.meta.js +17 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.meta.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.meta.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.meta.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.meta.js +17 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.meta.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.meta.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/verify/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/verify/page.meta.js.map +7 -0
- package/dist/modules/workflows/lib/activity-executor.js +35 -17
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/package.json +3 -3
- package/src/modules/auth/lib/backendChrome.tsx +3 -1
- package/src/modules/auth/services/rbacService.ts +8 -2
- package/src/modules/customer_accounts/api/password/reset-confirm.ts +9 -0
- package/src/modules/customer_accounts/api/portal/nav.ts +87 -0
- package/src/modules/customer_accounts/api/signup.ts +23 -7
- package/src/modules/customer_accounts/services/customerSessionService.ts +39 -0
- package/src/modules/directory/api/organizations/route.ts +11 -0
- package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -3
- package/src/modules/directory/backend/directory/organizations/create/page.tsx +15 -3
- package/src/modules/directory/i18n/de.json +2 -0
- package/src/modules/directory/i18n/en.json +2 -0
- package/src/modules/directory/i18n/es.json +2 -0
- package/src/modules/directory/i18n/pl.json +2 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +4 -3
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.meta.ts +15 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.meta.ts +15 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/verify/page.meta.ts +9 -0
- package/src/modules/workflows/lib/activity-executor.ts +64 -25
|
@@ -24,6 +24,7 @@ type OrganizationResponse = {
|
|
|
24
24
|
items: Array<{
|
|
25
25
|
id: string
|
|
26
26
|
name: string
|
|
27
|
+
slug?: string | null
|
|
27
28
|
tenantId: string
|
|
28
29
|
tenantName?: string | null
|
|
29
30
|
parentId: string | null
|
|
@@ -131,6 +132,7 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
|
|
|
131
132
|
setInitialValues({
|
|
132
133
|
id: record.id,
|
|
133
134
|
name: record.name,
|
|
135
|
+
slug: record.slug ?? '',
|
|
134
136
|
parentId: record.parentId || '',
|
|
135
137
|
isActive: record.isActive,
|
|
136
138
|
tenantId: resolvedTenantId,
|
|
@@ -194,6 +196,12 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
|
|
|
194
196
|
} as CrudField,
|
|
195
197
|
] : []),
|
|
196
198
|
{ id: 'name', label: t('directory.organizations.form.field.name', 'Name'), type: 'text', required: true },
|
|
199
|
+
{
|
|
200
|
+
id: 'slug',
|
|
201
|
+
label: t('directory.organizations.form.field.slug', 'Slug'),
|
|
202
|
+
type: 'text',
|
|
203
|
+
description: t('directory.organizations.form.field.slug.description', 'URL-safe identifier used for the customer portal (lowercase letters, numbers, hyphens, underscores).'),
|
|
204
|
+
},
|
|
197
205
|
{
|
|
198
206
|
id: 'parentId',
|
|
199
207
|
label: t('directory.organizations.form.field.parent', 'Parent'),
|
|
@@ -237,8 +245,8 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
|
|
|
237
245
|
|
|
238
246
|
const detailFields = React.useMemo(() => (
|
|
239
247
|
actorIsSuperAdmin
|
|
240
|
-
? ['tenantId', 'name', 'parentId', 'childrenInfo', 'isActive']
|
|
241
|
-
: ['name', 'parentId', 'childrenInfo', 'isActive']
|
|
248
|
+
? ['tenantId', 'name', 'slug', 'parentId', 'childrenInfo', 'isActive']
|
|
249
|
+
: ['name', 'slug', 'parentId', 'childrenInfo', 'isActive']
|
|
242
250
|
), [actorIsSuperAdmin])
|
|
243
251
|
|
|
244
252
|
const groups: CrudFormGroup[] = React.useMemo(() => ([
|
|
@@ -278,7 +286,7 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
|
|
|
278
286
|
fields={fields}
|
|
279
287
|
groups={groups}
|
|
280
288
|
entityId={E.directory.organization}
|
|
281
|
-
initialValues={initialValues ?? { id: orgId, tenantId: tenantId ?? null, name: '', parentId: '', isActive: true, childIds: [] }}
|
|
289
|
+
initialValues={initialValues ?? { id: orgId, tenantId: tenantId ?? null, name: '', slug: '', parentId: '', isActive: true, childIds: [] }}
|
|
282
290
|
isLoading={loading}
|
|
283
291
|
loadingMessage={t('directory.organizations.form.loading', 'Loading organization...')}
|
|
284
292
|
submitLabel={t('directory.organizations.form.action.save', 'Save')}
|
|
@@ -315,6 +323,7 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
|
|
|
315
323
|
type UpdateOrganizationPayload = {
|
|
316
324
|
id: string
|
|
317
325
|
name: string
|
|
326
|
+
slug?: string | null
|
|
318
327
|
isActive: boolean
|
|
319
328
|
parentId: string | null
|
|
320
329
|
childIds: string[]
|
|
@@ -371,6 +380,11 @@ export async function submitUpdateOrganization(options: {
|
|
|
371
380
|
childIds: originalChildIds,
|
|
372
381
|
}
|
|
373
382
|
|
|
383
|
+
if (typeof values.slug === 'string') {
|
|
384
|
+
const trimmedSlug = values.slug.trim()
|
|
385
|
+
payload.slug = trimmedSlug.length ? trimmedSlug : null
|
|
386
|
+
}
|
|
387
|
+
|
|
374
388
|
if (submittedTenantId !== undefined && submittedTenantId !== null) {
|
|
375
389
|
payload.tenantId = submittedTenantId
|
|
376
390
|
}
|
|
@@ -132,6 +132,12 @@ export default function CreateOrganizationPage() {
|
|
|
132
132
|
} as CrudField,
|
|
133
133
|
] : []),
|
|
134
134
|
{ id: 'name', label: t('directory.organizations.form.field.name', 'Name'), type: 'text', required: true },
|
|
135
|
+
{
|
|
136
|
+
id: 'slug',
|
|
137
|
+
label: t('directory.organizations.form.field.slug', 'Slug'),
|
|
138
|
+
type: 'text',
|
|
139
|
+
description: t('directory.organizations.form.field.slug.description', 'URL-safe identifier used for the customer portal (lowercase letters, numbers, hyphens, underscores). Generated from the name when left blank.'),
|
|
140
|
+
},
|
|
135
141
|
{
|
|
136
142
|
id: 'parentId',
|
|
137
143
|
label: t('directory.organizations.form.field.parent', 'Parent'),
|
|
@@ -166,8 +172,8 @@ export default function CreateOrganizationPage() {
|
|
|
166
172
|
|
|
167
173
|
const detailFields = React.useMemo(() => (
|
|
168
174
|
actorIsSuperAdmin
|
|
169
|
-
? ['tenantId', 'name', 'parentId', 'childIds', 'isActive']
|
|
170
|
-
: ['name', 'parentId', 'childIds', 'isActive']
|
|
175
|
+
? ['tenantId', 'name', 'slug', 'parentId', 'childIds', 'isActive']
|
|
176
|
+
: ['name', 'slug', 'parentId', 'childIds', 'isActive']
|
|
171
177
|
), [actorIsSuperAdmin])
|
|
172
178
|
|
|
173
179
|
const groups: CrudFormGroup[] = React.useMemo(() => ([
|
|
@@ -186,7 +192,7 @@ export default function CreateOrganizationPage() {
|
|
|
186
192
|
fields={fields}
|
|
187
193
|
groups={groups}
|
|
188
194
|
entityId={E.directory.organization}
|
|
189
|
-
initialValues={{ tenantId: selectedTenantId ?? null, name: '', parentId: '', childIds: [], isActive: true }}
|
|
195
|
+
initialValues={{ tenantId: selectedTenantId ?? null, name: '', slug: '', parentId: '', childIds: [], isActive: true }}
|
|
190
196
|
submitLabel={t('directory.organizations.form.action.create', 'Create')}
|
|
191
197
|
cancelHref="/backend/directory/organizations"
|
|
192
198
|
successRedirect={`/backend/directory/organizations?flash=${successMessage}&type=success`}
|
|
@@ -208,6 +214,7 @@ export default function CreateOrganizationPage() {
|
|
|
208
214
|
|
|
209
215
|
type CreateOrganizationPayload = {
|
|
210
216
|
name: string
|
|
217
|
+
slug?: string | null
|
|
211
218
|
isActive: boolean
|
|
212
219
|
parentId: string | null
|
|
213
220
|
childIds: string[]
|
|
@@ -261,6 +268,11 @@ export async function submitCreateOrganization(options: {
|
|
|
261
268
|
childIds: Array.isArray(values.childIds) ? values.childIds.filter((id): id is string => typeof id === 'string') : [],
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
if (typeof values.slug === 'string') {
|
|
272
|
+
const trimmedSlug = values.slug.trim()
|
|
273
|
+
if (trimmedSlug.length) payload.slug = trimmedSlug
|
|
274
|
+
}
|
|
275
|
+
|
|
264
276
|
if (tenantValue) payload.tenantId = tenantValue
|
|
265
277
|
if (Object.keys(customFields).length > 0) payload.customFields = customFields
|
|
266
278
|
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"directory.organizations.form.field.isActive": "Aktiv",
|
|
28
28
|
"directory.organizations.form.field.name": "Name",
|
|
29
29
|
"directory.organizations.form.field.parent": "Übergeordnete Organisation",
|
|
30
|
+
"directory.organizations.form.field.slug": "Slug",
|
|
31
|
+
"directory.organizations.form.field.slug.description": "URL-sicherer Bezeichner für das Kundenportal (Kleinbuchstaben, Zahlen, Bindestriche, Unterstriche).",
|
|
30
32
|
"directory.organizations.form.field.tenant": "Mandant",
|
|
31
33
|
"directory.organizations.form.group.customFields": "Benutzerdefinierte Daten",
|
|
32
34
|
"directory.organizations.form.group.details": "Details",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"directory.organizations.form.field.isActive": "Active",
|
|
28
28
|
"directory.organizations.form.field.name": "Name",
|
|
29
29
|
"directory.organizations.form.field.parent": "Parent",
|
|
30
|
+
"directory.organizations.form.field.slug": "Slug",
|
|
31
|
+
"directory.organizations.form.field.slug.description": "URL-safe identifier used for the customer portal (lowercase letters, numbers, hyphens, underscores).",
|
|
30
32
|
"directory.organizations.form.field.tenant": "Tenant",
|
|
31
33
|
"directory.organizations.form.group.customFields": "Custom Data",
|
|
32
34
|
"directory.organizations.form.group.details": "Details",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"directory.organizations.form.field.isActive": "Activo",
|
|
28
28
|
"directory.organizations.form.field.name": "Nombre",
|
|
29
29
|
"directory.organizations.form.field.parent": "Padre",
|
|
30
|
+
"directory.organizations.form.field.slug": "Slug",
|
|
31
|
+
"directory.organizations.form.field.slug.description": "Identificador seguro para URL usado en el portal de clientes (minúsculas, números, guiones, guiones bajos).",
|
|
30
32
|
"directory.organizations.form.field.tenant": "Inquilino",
|
|
31
33
|
"directory.organizations.form.group.customFields": "Datos personalizados",
|
|
32
34
|
"directory.organizations.form.group.details": "Detalles",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"directory.organizations.form.field.isActive": "Aktywna",
|
|
28
28
|
"directory.organizations.form.field.name": "Nazwa",
|
|
29
29
|
"directory.organizations.form.field.parent": "Organizacja nadrzędna",
|
|
30
|
+
"directory.organizations.form.field.slug": "Identyfikator URL",
|
|
31
|
+
"directory.organizations.form.field.slug.description": "Identyfikator używany w adresie portalu klienta (małe litery, cyfry, myślniki, podkreślenia).",
|
|
30
32
|
"directory.organizations.form.field.tenant": "Najemca",
|
|
31
33
|
"directory.organizations.form.group.customFields": "Dane niestandardowe",
|
|
32
34
|
"directory.organizations.form.group.details": "Szczegóły",
|
|
@@ -23,8 +23,9 @@ export function useMessageDetails(id: string) {
|
|
|
23
23
|
queryClient,
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const invalidateMessageQueries = React.useCallback(
|
|
27
27
|
(payload: Record<string, unknown>) => {
|
|
28
|
+
void queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })
|
|
28
29
|
void queryClient.invalidateQueries({ queryKey: ['messages', 'detail', id] })
|
|
29
30
|
const messageId = typeof payload.messageId === 'string' ? payload.messageId : null
|
|
30
31
|
if (messageId && messageId !== id) {
|
|
@@ -37,9 +38,9 @@ export function useMessageDetails(id: string) {
|
|
|
37
38
|
useAppEvent(
|
|
38
39
|
'messages.message.*',
|
|
39
40
|
(evt) => {
|
|
40
|
-
|
|
41
|
+
invalidateMessageQueries((evt.payload ?? {}) as Record<string, unknown>)
|
|
41
42
|
},
|
|
42
|
-
[
|
|
43
|
+
[invalidateMessageQueries],
|
|
43
44
|
)
|
|
44
45
|
|
|
45
46
|
useAppEvent(
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PageMetadata } from '@open-mercato/shared/modules/registry'
|
|
2
|
+
|
|
3
|
+
export const metadata: PageMetadata = {
|
|
4
|
+
requireCustomerAuth: true,
|
|
5
|
+
titleKey: 'portal.dashboard.title',
|
|
6
|
+
title: 'Dashboard',
|
|
7
|
+
nav: {
|
|
8
|
+
label: 'Dashboard',
|
|
9
|
+
labelKey: 'portal.nav.dashboard',
|
|
10
|
+
group: 'main',
|
|
11
|
+
order: 10,
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default metadata
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PageMetadata } from '@open-mercato/shared/modules/registry'
|
|
2
|
+
|
|
3
|
+
export const metadata: PageMetadata = {
|
|
4
|
+
requireCustomerAuth: true,
|
|
5
|
+
titleKey: 'portal.nav.profile',
|
|
6
|
+
title: 'Profile',
|
|
7
|
+
nav: {
|
|
8
|
+
label: 'Profile',
|
|
9
|
+
labelKey: 'portal.nav.profile',
|
|
10
|
+
group: 'account',
|
|
11
|
+
order: 10,
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default metadata
|
|
@@ -28,7 +28,18 @@ import { logWorkflowEvent } from './event-logger'
|
|
|
28
28
|
export { isPrivateUrl } from '@open-mercato/shared/lib/network'
|
|
29
29
|
|
|
30
30
|
function isAllowPrivateWorkflowWebhookUrlsEnabled(): boolean {
|
|
31
|
-
|
|
31
|
+
if (parseBooleanWithDefault(process.env.OM_WORKFLOWS_ALLOW_PRIVATE_URLS, false)) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (parseBooleanWithDefault(process.env.WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS, false)) {
|
|
36
|
+
console.warn(
|
|
37
|
+
'[CALL_WEBHOOK] WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS is deprecated. Use OM_WORKFLOWS_ALLOW_PRIVATE_URLS instead. SSRF protection is bypassed.'
|
|
38
|
+
)
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
// ============================================================================
|
|
@@ -788,10 +799,13 @@ export async function executeCallApi(
|
|
|
788
799
|
// SECURITY.md changelog entry for this fix.
|
|
789
800
|
//
|
|
790
801
|
// The resolution strategy is:
|
|
791
|
-
// 1. Use the workflow instance's `
|
|
792
|
-
// started the instance), when available.
|
|
793
|
-
//
|
|
794
|
-
// the
|
|
802
|
+
// 1. Use the workflow instance's `metadata.initiatedBy` user (whoever
|
|
803
|
+
// manually started the instance), when available. Only this user's
|
|
804
|
+
// current active roles are used — we never fall back to the author
|
|
805
|
+
// when the initiator is known, because that would escalate the
|
|
806
|
+
// initiator's privileges.
|
|
807
|
+
// 2. Fall back to the workflow definition's `createdBy` (author) only
|
|
808
|
+
// when the instance was started by an event trigger with no user.
|
|
795
809
|
// 3. If no traceable principal exists, the activity refuses to run —
|
|
796
810
|
// there is no "system" fallback that bypasses RBAC.
|
|
797
811
|
const resolvedRoleIds = await resolveCallApiRoleIds(apiKeyEm, context.workflowInstance)
|
|
@@ -885,38 +899,28 @@ export type CallApiInstanceLike = {
|
|
|
885
899
|
tenantId: string
|
|
886
900
|
organizationId: string
|
|
887
901
|
definitionId: string
|
|
902
|
+
metadata?: { initiatedBy?: string | null } | null
|
|
888
903
|
}
|
|
889
904
|
|
|
890
|
-
|
|
905
|
+
async function resolveActiveRoleIdsForUser(
|
|
891
906
|
em: any,
|
|
892
|
-
|
|
907
|
+
userId: string,
|
|
908
|
+
scope: { tenantId: string; organizationId: string },
|
|
893
909
|
): Promise<string[]> {
|
|
894
|
-
if (!instance.definitionId) return []
|
|
895
|
-
|
|
896
910
|
const { findOneWithDecryption, findWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')
|
|
897
911
|
const { User, UserRole, Role } = await import('../../auth/data/entities')
|
|
898
|
-
const { WorkflowDefinition } = await import('../data/entities')
|
|
899
|
-
|
|
900
|
-
const scope = { tenantId: instance.tenantId, organizationId: instance.organizationId }
|
|
901
912
|
|
|
902
|
-
const
|
|
903
|
-
id:
|
|
904
|
-
tenantId:
|
|
905
|
-
}, {}, scope)
|
|
906
|
-
const authorUserId = definition?.createdBy
|
|
907
|
-
if (!authorUserId) return []
|
|
908
|
-
|
|
909
|
-
const author = await findOneWithDecryption(em, User, {
|
|
910
|
-
id: authorUserId,
|
|
911
|
-
tenantId: instance.tenantId,
|
|
913
|
+
const user = await findOneWithDecryption(em, User, {
|
|
914
|
+
id: userId,
|
|
915
|
+
tenantId: scope.tenantId,
|
|
912
916
|
deletedAt: null,
|
|
913
917
|
}, {}, scope)
|
|
914
|
-
if (!
|
|
918
|
+
if (!user) return []
|
|
915
919
|
|
|
916
920
|
const userRoles = await findWithDecryption(
|
|
917
921
|
em,
|
|
918
922
|
UserRole,
|
|
919
|
-
{ user:
|
|
923
|
+
{ user: user.id, deletedAt: null },
|
|
920
924
|
{ populate: ['role'] },
|
|
921
925
|
scope,
|
|
922
926
|
)
|
|
@@ -928,12 +932,47 @@ export async function resolveCallApiRoleIds(
|
|
|
928
932
|
|
|
929
933
|
const scopedRoles = await findWithDecryption(em, Role, {
|
|
930
934
|
id: { $in: roleIds },
|
|
931
|
-
tenantId:
|
|
935
|
+
tenantId: scope.tenantId,
|
|
932
936
|
deletedAt: null,
|
|
933
937
|
}, {}, scope)
|
|
934
938
|
return scopedRoles.map((r: any) => r.id as string)
|
|
935
939
|
}
|
|
936
940
|
|
|
941
|
+
export async function resolveCallApiRoleIds(
|
|
942
|
+
em: any,
|
|
943
|
+
instance: CallApiInstanceLike
|
|
944
|
+
): Promise<string[]> {
|
|
945
|
+
if (!instance.definitionId) return []
|
|
946
|
+
|
|
947
|
+
const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')
|
|
948
|
+
const { WorkflowDefinition } = await import('../data/entities')
|
|
949
|
+
|
|
950
|
+
const scope = { tenantId: instance.tenantId, organizationId: instance.organizationId }
|
|
951
|
+
|
|
952
|
+
// 1. Prefer the triggering user (whoever manually started this instance).
|
|
953
|
+
// WorkflowInstance.metadata.initiatedBy is the canonical record of that
|
|
954
|
+
// principal for user-started instances; use their current role set so
|
|
955
|
+
// CALL_API never exceeds the initiator's permissions. Refuse if the
|
|
956
|
+
// initiator has no active scoped roles — do not fall back to the
|
|
957
|
+
// definition author, which would escalate the initiator's privileges.
|
|
958
|
+
const initiatorUserId = instance.metadata?.initiatedBy ?? null
|
|
959
|
+
if (initiatorUserId) {
|
|
960
|
+
return resolveActiveRoleIdsForUser(em, initiatorUserId, scope)
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// 2. Event-triggered instance with no human initiator: fall back to the
|
|
964
|
+
// definition author. Soft-deleted definitions must not mint keys.
|
|
965
|
+
const definition = await findOneWithDecryption(em, WorkflowDefinition, {
|
|
966
|
+
id: instance.definitionId,
|
|
967
|
+
tenantId: instance.tenantId,
|
|
968
|
+
deletedAt: null,
|
|
969
|
+
}, {}, scope)
|
|
970
|
+
const authorUserId = definition?.createdBy
|
|
971
|
+
if (!authorUserId) return []
|
|
972
|
+
|
|
973
|
+
return resolveActiveRoleIdsForUser(em, authorUserId, scope)
|
|
974
|
+
}
|
|
975
|
+
|
|
937
976
|
/**
|
|
938
977
|
* Build full API URL from endpoint
|
|
939
978
|
* - Relative paths (/api/...) → prepend APP_URL
|