@open-mercato/shared 0.6.8-develop.7015.1.af90a2ddc7 → 0.6.8-develop.7016.1.4ed7b1e49d
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/dist/lib/auth/principal-service.js +1 -0
- package/dist/lib/auth/principal-service.js.map +7 -0
- package/dist/lib/auth/server.js +23 -6
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/data/engine.js +8 -2
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +69 -15
- package/dist/modules/events/factory.js.map +2 -2
- package/package.json +6 -2
- package/src/lib/auth/__tests__/principalServiceExport.test.ts +67 -0
- package/src/lib/auth/__tests__/server.apiKeyCache.test.ts +324 -0
- package/src/lib/auth/principal-service.ts +110 -0
- package/src/lib/auth/server.ts +45 -7
- package/src/lib/data/__tests__/engine.event-validation.test.ts +9 -1
- package/src/lib/data/engine.ts +7 -1
- package/src/modules/events/__tests__/factory.test.ts +88 -0
- package/src/modules/events/factory.ts +111 -19
- package/src/modules/events/types.ts +17 -0
package/src/lib/auth/server.ts
CHANGED
|
@@ -195,13 +195,33 @@ async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
|
|
|
195
195
|
: []
|
|
196
196
|
const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)
|
|
197
197
|
|
|
198
|
+
// A role-level super-admin grant is authorization-grade only when it is genuinely
|
|
199
|
+
// unrestricted: the grant itself must not be organization-scoped, it must belong to the
|
|
200
|
+
// key's tenant, and the key must not be bound to a single organization. RbacService applies
|
|
201
|
+
// the same intersection when projecting the scoped ACL, but generic guards
|
|
202
|
+
// (tenantAccess.resolveIsSuperAdmin, the scoped-API helpers) trust this raw bit *before*
|
|
203
|
+
// live RBAC runs — so an organization-restricted key must never carry it.
|
|
198
204
|
let keyIsSuperAdmin = false
|
|
199
|
-
|
|
200
|
-
|
|
205
|
+
const keyOrganizationId = typeof record.organizationId === 'string' && record.organizationId.trim().length > 0
|
|
206
|
+
? record.organizationId.trim()
|
|
207
|
+
: null
|
|
208
|
+
if (roleIds.length && !keyOrganizationId) {
|
|
209
|
+
const superAcls = await em.find(
|
|
201
210
|
RoleAcl,
|
|
202
|
-
{
|
|
211
|
+
{
|
|
212
|
+
role: { $in: roleIds } as any,
|
|
213
|
+
tenantId: record.tenantId ?? null,
|
|
214
|
+
isSuperAdmin: true,
|
|
215
|
+
deletedAt: null,
|
|
216
|
+
} as any,
|
|
203
217
|
)
|
|
204
|
-
keyIsSuperAdmin =
|
|
218
|
+
keyIsSuperAdmin = superAcls.some((acl) => {
|
|
219
|
+
const organizations = Array.isArray((acl as { organizationsJson?: string[] | null }).organizationsJson)
|
|
220
|
+
? (acl as { organizationsJson?: string[] | null }).organizationsJson as string[]
|
|
221
|
+
: null
|
|
222
|
+
// An empty list means "inherit the key's own binding"; with no binding that is tenant-wide.
|
|
223
|
+
return !organizations || organizations.length === 0 || organizations.includes('__all__')
|
|
224
|
+
})
|
|
205
225
|
}
|
|
206
226
|
|
|
207
227
|
if (cache.shouldWriteLastUsed(record.id)) {
|
|
@@ -213,8 +233,25 @@ async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
|
|
|
213
233
|
}
|
|
214
234
|
}
|
|
215
235
|
|
|
216
|
-
//
|
|
217
|
-
|
|
236
|
+
// Ephemeral session keys are always user-bound. Regular keys retain their
|
|
237
|
+
// legacy creator identity, while tenant-scoped regular keys ignore only the
|
|
238
|
+
// creator's concrete organization when validating the wider key scope.
|
|
239
|
+
// Keep every session marker fail-closed so a malformed session key cannot
|
|
240
|
+
// fall back to the regular key path and escape its user/scope binding.
|
|
241
|
+
const isSessionBoundKey = Boolean(
|
|
242
|
+
record.sessionToken
|
|
243
|
+
|| record.sessionUserId
|
|
244
|
+
|| record.sessionSecretEncrypted
|
|
245
|
+
|| record.opencodeSessionId
|
|
246
|
+
)
|
|
247
|
+
const actualUserId = isSessionBoundKey
|
|
248
|
+
? record.sessionUserId ?? null
|
|
249
|
+
: record.createdBy ?? null
|
|
250
|
+
|
|
251
|
+
if (isSessionBoundKey && !actualUserId) {
|
|
252
|
+
cache.setMiss(secret)
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
218
255
|
|
|
219
256
|
if (actualUserId) {
|
|
220
257
|
const user = await em.findOne(User, { id: actualUserId, deletedAt: null })
|
|
@@ -226,7 +263,8 @@ async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
|
|
|
226
263
|
cache.setMiss(secret)
|
|
227
264
|
return null
|
|
228
265
|
}
|
|
229
|
-
|
|
266
|
+
const requiresExactOrganization = isSessionBoundKey || Boolean(record.organizationId)
|
|
267
|
+
if (requiresExactOrganization && (user.organizationId ?? null) !== (record.organizationId ?? null)) {
|
|
230
268
|
cache.setMiss(secret)
|
|
231
269
|
return null
|
|
232
270
|
}
|
|
@@ -68,7 +68,15 @@ describe('DataEngine event contract validation (issue #1421)', () => {
|
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
expect(emitted).toEqual([
|
|
71
|
-
expect.objectContaining({
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
name: 'issue1421_test.widget.created',
|
|
73
|
+
options: {
|
|
74
|
+
persistent: false,
|
|
75
|
+
tenantId: 'tenant-1',
|
|
76
|
+
organizationId: 'org-1',
|
|
77
|
+
emitterModuleId: 'issue1421_test',
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
72
80
|
])
|
|
73
81
|
expect(loggerWarn).not.toHaveBeenCalled()
|
|
74
82
|
} finally {
|
package/src/lib/data/engine.ts
CHANGED
|
@@ -226,7 +226,12 @@ export class DefaultDataEngine implements DataEngine {
|
|
|
226
226
|
const eventName = `${mod}.${ent}.updated`
|
|
227
227
|
warnIfUndeclaredEvent(eventName, 'setCustomFields')
|
|
228
228
|
try {
|
|
229
|
-
await bus.emitEvent(eventName, { id: recordId, organizationId, tenantId }, {
|
|
229
|
+
await bus.emitEvent(eventName, { id: recordId, organizationId, tenantId }, {
|
|
230
|
+
persistent: true,
|
|
231
|
+
tenantId,
|
|
232
|
+
organizationId,
|
|
233
|
+
emitterModuleId: mod,
|
|
234
|
+
})
|
|
230
235
|
} catch {
|
|
231
236
|
// non-blocking
|
|
232
237
|
}
|
|
@@ -627,6 +632,7 @@ export class DefaultDataEngine implements DataEngine {
|
|
|
627
632
|
persistent: !!events.persistent,
|
|
628
633
|
tenantId: ctx.identifiers.tenantId ?? null,
|
|
629
634
|
organizationId: ctx.identifiers.organizationId ?? null,
|
|
635
|
+
emitterModuleId: events.module,
|
|
630
636
|
})
|
|
631
637
|
} catch {
|
|
632
638
|
// non-blocking
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'
|
|
2
|
+
|
|
3
|
+
type EventFactoryModule = typeof import('../factory')
|
|
4
|
+
|
|
5
|
+
describe('event definition registry', () => {
|
|
6
|
+
const globalScope = globalThis as Record<string, unknown>
|
|
7
|
+
const originalRegistry = globalScope[GLOBAL_EVENT_REGISTRY_KEY]
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
delete globalScope[GLOBAL_EVENT_REGISTRY_KEY]
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
if (originalRegistry === undefined) {
|
|
15
|
+
delete globalScope[GLOBAL_EVENT_REGISTRY_KEY]
|
|
16
|
+
} else {
|
|
17
|
+
globalScope[GLOBAL_EVENT_REGISTRY_KEY] = originalRegistry
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('shares declarations and registered configs across isolated module instances', () => {
|
|
22
|
+
let firstInstance: EventFactoryModule | undefined
|
|
23
|
+
let secondInstance: EventFactoryModule | undefined
|
|
24
|
+
|
|
25
|
+
jest.isolateModules(() => {
|
|
26
|
+
firstInstance = require('../factory') as EventFactoryModule
|
|
27
|
+
const config = firstInstance.createModuleEvents({
|
|
28
|
+
moduleId: 'isolated_events_test',
|
|
29
|
+
events: [{
|
|
30
|
+
id: 'isolated_events_test.invalidated',
|
|
31
|
+
label: 'Invalidated',
|
|
32
|
+
crossProcessBroadcast: true,
|
|
33
|
+
}] as const,
|
|
34
|
+
})
|
|
35
|
+
firstInstance.registerEventModuleConfigs([config])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
jest.isolateModules(() => {
|
|
39
|
+
secondInstance = require('../factory') as EventFactoryModule
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(secondInstance).not.toBe(firstInstance)
|
|
43
|
+
expect(secondInstance?.isEventDeclared('isolated_events_test.invalidated')).toBe(true)
|
|
44
|
+
expect(secondInstance?.isCrossProcessBroadcastEvent('isolated_events_test.invalidated')).toBe(true)
|
|
45
|
+
expect(secondInstance?.getDeclaredEvents()).toEqual(expect.arrayContaining([
|
|
46
|
+
expect.objectContaining({
|
|
47
|
+
id: 'isolated_events_test.invalidated',
|
|
48
|
+
module: 'isolated_events_test',
|
|
49
|
+
}),
|
|
50
|
+
]))
|
|
51
|
+
expect(secondInstance?.getEventModuleConfigs()).toHaveLength(1)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('refreshes a module definition during HMR without duplicating its event id', () => {
|
|
55
|
+
let firstInstance: EventFactoryModule | undefined
|
|
56
|
+
let secondInstance: EventFactoryModule | undefined
|
|
57
|
+
|
|
58
|
+
jest.isolateModules(() => {
|
|
59
|
+
firstInstance = require('../factory') as EventFactoryModule
|
|
60
|
+
firstInstance.createModuleEvents({
|
|
61
|
+
moduleId: 'hmr_events_test',
|
|
62
|
+
events: [{ id: 'hmr_events_test.changed', label: 'Before' }] as const,
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
jest.isolateModules(() => {
|
|
67
|
+
secondInstance = require('../factory') as EventFactoryModule
|
|
68
|
+
secondInstance.createModuleEvents({
|
|
69
|
+
moduleId: 'hmr_events_test',
|
|
70
|
+
events: [{
|
|
71
|
+
id: 'hmr_events_test.changed',
|
|
72
|
+
label: 'After',
|
|
73
|
+
clientBroadcast: true,
|
|
74
|
+
}] as const,
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(firstInstance?.isBroadcastEvent('hmr_events_test.changed')).toBe(true)
|
|
79
|
+
expect(firstInstance?.getAllDeclaredEventIds()).toEqual(['hmr_events_test.changed'])
|
|
80
|
+
expect(firstInstance?.getDeclaredEvents()).toEqual([
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
id: 'hmr_events_test.changed',
|
|
83
|
+
label: 'After',
|
|
84
|
+
clientBroadcast: true,
|
|
85
|
+
}),
|
|
86
|
+
])
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -65,17 +65,54 @@ export function getGlobalEventBus(): GlobalEventBus | null {
|
|
|
65
65
|
// Event Registry for Validation
|
|
66
66
|
// =============================================================================
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
type EventRegistryState = {
|
|
69
|
+
declaredEventIds: Set<string>
|
|
70
|
+
declaredEvents: EventDefinition[]
|
|
71
|
+
registeredEventConfigs: EventModuleConfig[] | null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'
|
|
75
|
+
|
|
76
|
+
const fallbackEventRegistryState: EventRegistryState = {
|
|
77
|
+
declaredEventIds: new Set<string>(),
|
|
78
|
+
declaredEvents: [],
|
|
79
|
+
registeredEventConfigs: null,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isEventRegistryState(value: unknown): value is EventRegistryState {
|
|
83
|
+
if (!value || typeof value !== 'object') return false
|
|
84
|
+
const candidate = value as Partial<EventRegistryState>
|
|
85
|
+
return candidate.declaredEventIds instanceof Set
|
|
86
|
+
&& Array.isArray(candidate.declaredEvents)
|
|
87
|
+
&& (candidate.registeredEventConfigs === null || Array.isArray(candidate.registeredEventConfigs))
|
|
88
|
+
}
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
function getEventRegistryState(): EventRegistryState {
|
|
91
|
+
try {
|
|
92
|
+
const globalScope = globalThis as Record<string, unknown>
|
|
93
|
+
const existing = globalScope[GLOBAL_EVENT_REGISTRY_KEY]
|
|
94
|
+
if (isEventRegistryState(existing)) return existing
|
|
95
|
+
globalScope[GLOBAL_EVENT_REGISTRY_KEY] = fallbackEventRegistryState
|
|
96
|
+
return fallbackEventRegistryState
|
|
97
|
+
} catch {
|
|
98
|
+
// Restricted runtimes may deny global access. Keep the previous
|
|
99
|
+
// module-local behavior as a safe fallback.
|
|
100
|
+
return fallbackEventRegistryState
|
|
101
|
+
}
|
|
102
|
+
}
|
|
73
103
|
|
|
74
104
|
function addDeclaredEvent(event: EventDefinition): void {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
const state = getEventRegistryState()
|
|
106
|
+
state.declaredEventIds.add(event.id)
|
|
107
|
+
const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === event.id)
|
|
108
|
+
if (existingIndex < 0) {
|
|
109
|
+
state.declaredEvents.push(event)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
// Refresh a module's own definition in place during HMR without allowing a
|
|
113
|
+
// duplicate declaration from another module to take over the event id.
|
|
114
|
+
if (state.declaredEvents[existingIndex]?.module === event.module) {
|
|
115
|
+
state.declaredEvents[existingIndex] = event
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -84,7 +121,7 @@ function addDeclaredEvent(event: EventDefinition): void {
|
|
|
84
121
|
* Used for runtime validation to ensure only declared events are emitted.
|
|
85
122
|
*/
|
|
86
123
|
export function isEventDeclared(eventId: string): boolean {
|
|
87
|
-
return
|
|
124
|
+
return getEventRegistryState().declaredEventIds.has(eventId)
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
/**
|
|
@@ -92,7 +129,7 @@ export function isEventDeclared(eventId: string): boolean {
|
|
|
92
129
|
* Useful for debugging and introspection.
|
|
93
130
|
*/
|
|
94
131
|
export function getAllDeclaredEventIds(): string[] {
|
|
95
|
-
return Array.from(
|
|
132
|
+
return Array.from(getEventRegistryState().declaredEventIds)
|
|
96
133
|
}
|
|
97
134
|
|
|
98
135
|
/**
|
|
@@ -100,7 +137,7 @@ export function getAllDeclaredEventIds(): string[] {
|
|
|
100
137
|
* Used by the API to return available events for workflow triggers.
|
|
101
138
|
*/
|
|
102
139
|
export function getDeclaredEvents(): EventDefinition[] {
|
|
103
|
-
return [...
|
|
140
|
+
return [...getEventRegistryState().declaredEvents]
|
|
104
141
|
}
|
|
105
142
|
|
|
106
143
|
/**
|
|
@@ -108,16 +145,52 @@ export function getDeclaredEvents(): EventDefinition[] {
|
|
|
108
145
|
* Used by the SSE endpoint to filter events for the DOM Event Bridge.
|
|
109
146
|
*/
|
|
110
147
|
export function isBroadcastEvent(eventId: string): boolean {
|
|
111
|
-
const event =
|
|
148
|
+
const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)
|
|
112
149
|
return event?.clientBroadcast === true
|
|
113
150
|
}
|
|
114
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Check if an event should be published over the server-to-server event bridge.
|
|
154
|
+
* Browser-broadcast events remain eligible for backward compatibility, while
|
|
155
|
+
* crossProcessBroadcast supports private process coordination without SSE.
|
|
156
|
+
*/
|
|
157
|
+
export function isCrossProcessBroadcastEvent(eventId: string): boolean {
|
|
158
|
+
const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)
|
|
159
|
+
return event?.clientBroadcast === true || event?.crossProcessBroadcast === true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check whether an event is reserved for private server-to-server
|
|
164
|
+
* coordination. Workflow-authored EMIT_EVENT activities must not emit these
|
|
165
|
+
* events because their payload and event id are tenant-managed input.
|
|
166
|
+
*/
|
|
167
|
+
export function isPrivateCrossProcessBroadcastEvent(eventId: string): boolean {
|
|
168
|
+
const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)
|
|
169
|
+
return event?.crossProcessBroadcast === true && event?.clientBroadcast !== true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Verify provenance for a private cross-process event. The module id is
|
|
174
|
+
* stamped by a declared module emitter or another trusted server-side seam;
|
|
175
|
+
* tenant-managed event payloads never participate in this decision.
|
|
176
|
+
*/
|
|
177
|
+
export function isPrivateCrossProcessEventEmitter(
|
|
178
|
+
eventId: string,
|
|
179
|
+
emitterModuleId: string | undefined,
|
|
180
|
+
): boolean {
|
|
181
|
+
const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)
|
|
182
|
+
if (event?.crossProcessBroadcast !== true) return true
|
|
183
|
+
return typeof event.module === 'string'
|
|
184
|
+
&& event.module.length > 0
|
|
185
|
+
&& event.module === emitterModuleId
|
|
186
|
+
}
|
|
187
|
+
|
|
115
188
|
/**
|
|
116
189
|
* Check if an event has portalBroadcast enabled.
|
|
117
190
|
* Used by the portal SSE endpoint to filter events for the Portal Event Bridge.
|
|
118
191
|
*/
|
|
119
192
|
export function isPortalBroadcastEvent(eventId: string): boolean {
|
|
120
|
-
const event =
|
|
193
|
+
const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)
|
|
121
194
|
return event?.portalBroadcast === true
|
|
122
195
|
}
|
|
123
196
|
|
|
@@ -125,17 +198,16 @@ export function isPortalBroadcastEvent(eventId: string): boolean {
|
|
|
125
198
|
// Bootstrap Registration (similar to searchModuleConfigs pattern)
|
|
126
199
|
// =============================================================================
|
|
127
200
|
|
|
128
|
-
let _registeredEventConfigs: EventModuleConfig[] | null = null
|
|
129
|
-
|
|
130
201
|
/**
|
|
131
202
|
* Register event module configurations globally.
|
|
132
203
|
* Called during app bootstrap with configs from events.generated.ts.
|
|
133
204
|
*/
|
|
134
205
|
export function registerEventModuleConfigs(configs: EventModuleConfig[]): void {
|
|
135
|
-
|
|
206
|
+
const state = getEventRegistryState()
|
|
207
|
+
if (state.registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {
|
|
136
208
|
logger.debug('Event module configs re-registered (this may occur during HMR)')
|
|
137
209
|
}
|
|
138
|
-
|
|
210
|
+
state.registeredEventConfigs = configs
|
|
139
211
|
for (const config of configs) {
|
|
140
212
|
for (const event of config.events) {
|
|
141
213
|
addDeclaredEvent(event)
|
|
@@ -148,7 +220,7 @@ export function registerEventModuleConfigs(configs: EventModuleConfig[]): void {
|
|
|
148
220
|
* Returns empty array if not registered.
|
|
149
221
|
*/
|
|
150
222
|
export function getEventModuleConfigs(): EventModuleConfig[] {
|
|
151
|
-
return
|
|
223
|
+
return getEventRegistryState().registeredEventConfigs ?? []
|
|
152
224
|
}
|
|
153
225
|
|
|
154
226
|
// =============================================================================
|
|
@@ -239,7 +311,27 @@ export function createModuleEvents<
|
|
|
239
311
|
return
|
|
240
312
|
}
|
|
241
313
|
|
|
242
|
-
|
|
314
|
+
const eventDefinition = fullEvents.find((event) => event.id === eventId)
|
|
315
|
+
const isClientBroadcast = eventDefinition?.clientBroadcast === true
|
|
316
|
+
const trustedOptions = eventDefinition?.crossProcessBroadcast === true || isClientBroadcast
|
|
317
|
+
? {
|
|
318
|
+
...emitOptions,
|
|
319
|
+
// Browser-broadcast module emitters historically accepted scope in
|
|
320
|
+
// their typed payload. Preserve that contract at the trusted module
|
|
321
|
+
// boundary while the event bus itself relies only on options.
|
|
322
|
+
...(isClientBroadcast && emitOptions?.tenantId === undefined
|
|
323
|
+
? { tenantId: payload.tenantId ?? null }
|
|
324
|
+
: {}),
|
|
325
|
+
...(isClientBroadcast && emitOptions?.organizationId === undefined
|
|
326
|
+
? { organizationId: payload.organizationId ?? null }
|
|
327
|
+
: {}),
|
|
328
|
+
...(isClientBroadcast && emitOptions?.organizationIds === undefined && Array.isArray(payload.organizationIds)
|
|
329
|
+
? { organizationIds: payload.organizationIds.filter((value): value is string => typeof value === 'string') }
|
|
330
|
+
: {}),
|
|
331
|
+
emitterModuleId: moduleId,
|
|
332
|
+
}
|
|
333
|
+
: emitOptions
|
|
334
|
+
await eventBus.emit(eventId, payload, trustedOptions)
|
|
243
335
|
}
|
|
244
336
|
|
|
245
337
|
return {
|
|
@@ -34,6 +34,8 @@ export interface EventDefinition {
|
|
|
34
34
|
excludeFromTriggers?: boolean
|
|
35
35
|
/** When true, this event is bridged to the browser via SSE (DOM Event Bridge). Default: false */
|
|
36
36
|
clientBroadcast?: boolean
|
|
37
|
+
/** When true, this event is bridged between server processes without enabling browser delivery. Default: false */
|
|
38
|
+
crossProcessBroadcast?: boolean
|
|
37
39
|
/** When true, this event is bridged to the customer portal via SSE (Portal Event Bridge). Default: false */
|
|
38
40
|
portalBroadcast?: boolean
|
|
39
41
|
}
|
|
@@ -66,6 +68,21 @@ export interface EmitOptions {
|
|
|
66
68
|
tenantId?: string | null
|
|
67
69
|
/** Trusted organization scope forwarded to subscribers separately from the payload */
|
|
68
70
|
organizationId?: string | null
|
|
71
|
+
/**
|
|
72
|
+
* Trusted multi-organization audience forwarded to subscribers separately
|
|
73
|
+
* from the payload. Mirrors the documented SSE audience contract, where a
|
|
74
|
+
* clientBroadcast event may target several organizations at once; carried in
|
|
75
|
+
* trusted options so the receiver never falls back to payload scope.
|
|
76
|
+
*/
|
|
77
|
+
organizationIds?: string[] | null
|
|
78
|
+
/**
|
|
79
|
+
* Module that emitted a private cross-process coordination event.
|
|
80
|
+
*
|
|
81
|
+
* This is stamped by `createModuleEvents`; application inputs must not set or
|
|
82
|
+
* derive it from event payloads.
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
emitterModuleId?: string
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
// =============================================================================
|