mcp-event-intelligence 0.1.0
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/CHANGELOG.md +80 -0
- package/LICENSE +201 -0
- package/README.md +298 -0
- package/SECURITY.md +42 -0
- package/bin/mcp-event-intelligence.mjs +52 -0
- package/dist/src/composite/engine.d.ts +87 -0
- package/dist/src/composite/engine.js +667 -0
- package/dist/src/composite/temporal.d.ts +32 -0
- package/dist/src/composite/temporal.js +317 -0
- package/dist/src/intelligenceProtocol/audit.d.ts +19 -0
- package/dist/src/intelligenceProtocol/audit.js +56 -0
- package/dist/src/intelligenceProtocol/canonical.d.ts +2 -0
- package/dist/src/intelligenceProtocol/canonical.js +21 -0
- package/dist/src/intelligenceProtocol/index.d.ts +12 -0
- package/dist/src/intelligenceProtocol/index.js +8 -0
- package/dist/src/intelligenceProtocol/invariants.d.ts +3 -0
- package/dist/src/intelligenceProtocol/invariants.js +36 -0
- package/dist/src/intelligenceProtocol/lifecycle.d.ts +4 -0
- package/dist/src/intelligenceProtocol/lifecycle.js +24 -0
- package/dist/src/intelligenceProtocol/mapper.d.ts +14 -0
- package/dist/src/intelligenceProtocol/mapper.js +28 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +325 -0
- package/dist/src/intelligenceProtocol/schemas.js +182 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +488 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.js +379 -0
- package/dist/src/intelligenceProtocol/version.d.ts +7 -0
- package/dist/src/intelligenceProtocol/version.js +7 -0
- package/dist/src/mcpEvents/consumer.d.ts +7 -0
- package/dist/src/mcpEvents/consumer.js +14 -0
- package/dist/src/mcpEvents/server.d.ts +41 -0
- package/dist/src/mcpEvents/server.js +122 -0
- package/dist/src/protocol/types.d.ts +37 -0
- package/dist/src/protocol/types.js +1 -0
- package/dist/src/semantic/conditionEngine.d.ts +7 -0
- package/dist/src/semantic/conditionEngine.js +65 -0
- package/dist/src/semantic/types.d.ts +31 -0
- package/dist/src/semantic/types.js +1 -0
- package/dist/src/semantic/typesafeJevEvaluator.d.ts +23 -0
- package/dist/src/semantic/typesafeJevEvaluator.js +95 -0
- package/package.json +85 -0
- package/scripts/host-integration.d.mts +86 -0
- package/scripts/host-integration.mjs +216 -0
- package/scripts/lib/composite-event-consumer.mjs +172 -0
- package/scripts/lib/composite-wake-coordinator.mjs +185 -0
- package/scripts/lib/derived-contract-registry.mjs +210 -0
- package/scripts/lib/derived-event-coordinator.mjs +242 -0
- package/scripts/lib/event-processor.mjs +409 -0
- package/scripts/lib/generic-runtime-wake.mjs +76 -0
- package/scripts/lib/github-mcp-events-adapter.mjs +66 -0
- package/scripts/lib/github-webhook.mjs +88 -0
- package/scripts/lib/http-wake-deliverer.mjs +31 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +193 -0
- package/scripts/lib/mcp-events-client.mjs +519 -0
- package/scripts/lib/persistent-event-store.mjs +769 -0
- package/scripts/lib/temporal-deadline-scheduler.mjs +111 -0
- package/scripts/lib/trigger-control-plane.mjs +939 -0
- package/scripts/lib/trigger-inspector.mjs +631 -0
- package/scripts/lib/wake-signature.mjs +10 -0
- package/scripts/mcp-stdio-server.mjs +380 -0
- package/scripts/mcp-stdio.mjs +3 -0
- package/scripts/run-tests.mjs +51 -0
- package/scripts/service.mjs +756 -0
- package/server.json +68 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mcpOccurrenceToCorrelatableEvent,
|
|
3
|
+
} from '../../dist/src/mcpEvents/consumer.js';
|
|
4
|
+
|
|
5
|
+
export class CompositeEventConsumer {
|
|
6
|
+
constructor({
|
|
7
|
+
store,
|
|
8
|
+
triggerEngine,
|
|
9
|
+
wakeCoordinator = null,
|
|
10
|
+
wakeCoordinators = null,
|
|
11
|
+
derivedEventCoordinator = null,
|
|
12
|
+
processor = null,
|
|
13
|
+
maxDerivedDepth = 16,
|
|
14
|
+
}) {
|
|
15
|
+
this.store = store;
|
|
16
|
+
this.triggerEngine = triggerEngine;
|
|
17
|
+
this.wakeCoordinator = wakeCoordinator;
|
|
18
|
+
this.wakeCoordinators = wakeCoordinators instanceof Map
|
|
19
|
+
? wakeCoordinators
|
|
20
|
+
: new Map();
|
|
21
|
+
this.derivedEventCoordinator = derivedEventCoordinator;
|
|
22
|
+
this.processor = processor;
|
|
23
|
+
this.maxDerivedDepth = Math.max(1, Number(maxDerivedDepth) || 16);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
coordinatorFor(runtime) {
|
|
27
|
+
const configured = this.wakeCoordinators.get(runtime);
|
|
28
|
+
if (configured) return configured;
|
|
29
|
+
return this.wakeCoordinator;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async ingestMcpOccurrence({
|
|
33
|
+
event,
|
|
34
|
+
serverId,
|
|
35
|
+
provider,
|
|
36
|
+
traceId,
|
|
37
|
+
}) {
|
|
38
|
+
const correlatable = await mcpOccurrenceToCorrelatableEvent(
|
|
39
|
+
event,
|
|
40
|
+
{
|
|
41
|
+
traceId,
|
|
42
|
+
...(provider ? { provider } : {}),
|
|
43
|
+
...(serverId ? { serverId } : {}),
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return this.ingestCorrelatable(correlatable);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async ingestCorrelatable(event, context = {}) {
|
|
51
|
+
const derivedDepth = Number(context.derivedDepth ?? 0);
|
|
52
|
+
const results = await this.triggerEngine.ingest(event);
|
|
53
|
+
const deliveries = [];
|
|
54
|
+
const derivedEvents = [];
|
|
55
|
+
|
|
56
|
+
for (const result of results) {
|
|
57
|
+
if (!result.matched || !result.match) continue;
|
|
58
|
+
|
|
59
|
+
const definition = this.store.listTriggers().find(
|
|
60
|
+
(candidate) =>
|
|
61
|
+
candidate.triggerId === result.match.triggerId &&
|
|
62
|
+
candidate.version === result.match.triggerVersion,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!definition) {
|
|
66
|
+
deliveries.push({
|
|
67
|
+
triggerId: result.triggerId,
|
|
68
|
+
matchId: result.match.matchId,
|
|
69
|
+
wakeId: null,
|
|
70
|
+
status: 'trigger_definition_missing',
|
|
71
|
+
runtimeReceiptId: null,
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (definition.derivedEvent && this.derivedEventCoordinator) {
|
|
77
|
+
if (derivedDepth >= this.maxDerivedDepth) {
|
|
78
|
+
const error = new Error(
|
|
79
|
+
`Derived event chain exceeded max depth ${this.maxDerivedDepth}`,
|
|
80
|
+
);
|
|
81
|
+
error.code = 'DERIVED_EVENT_CHAIN_DEPTH_EXCEEDED';
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const emission = await this.derivedEventCoordinator.emitMatched(
|
|
86
|
+
result.match,
|
|
87
|
+
definition,
|
|
88
|
+
);
|
|
89
|
+
const derivedRecord = emission.record;
|
|
90
|
+
const summary = {
|
|
91
|
+
triggerId: result.triggerId,
|
|
92
|
+
matchId: result.match.matchId,
|
|
93
|
+
status: emission.status,
|
|
94
|
+
accepted: emission.accepted,
|
|
95
|
+
eventId: derivedRecord?.event.sourceEventId ?? null,
|
|
96
|
+
eventName: derivedRecord?.event.name ?? definition.derivedEvent.name,
|
|
97
|
+
nested: null,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (derivedRecord) {
|
|
101
|
+
// Re-enter even on replay. Downstream match/event idempotency makes this
|
|
102
|
+
// restart-safe if the process crashed after persistence but before fan-out.
|
|
103
|
+
summary.nested = await this.ingestCorrelatable(
|
|
104
|
+
derivedRecord.event,
|
|
105
|
+
{ derivedDepth: derivedDepth + 1 },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!definition.target) {
|
|
109
|
+
await this.triggerEngine.markDerivedEmitted(
|
|
110
|
+
result.match.matchId,
|
|
111
|
+
derivedRecord.event.sourceEventId,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
derivedEvents.push(summary);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!definition.target) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const coordinator = this.coordinatorFor(definition.target.runtime);
|
|
124
|
+
if (!coordinator) {
|
|
125
|
+
deliveries.push({
|
|
126
|
+
triggerId: result.triggerId,
|
|
127
|
+
matchId: result.match.matchId,
|
|
128
|
+
wakeId: null,
|
|
129
|
+
status: 'runtime_unconfigured',
|
|
130
|
+
runtimeReceiptId: null,
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const delivery = await coordinator.deliverMatched(result.match);
|
|
136
|
+
const persistedWake = delivery.wake;
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
persistedWake?.status === 'delivered' &&
|
|
140
|
+
persistedWake.runtimeReceiptId &&
|
|
141
|
+
this.processor
|
|
142
|
+
) {
|
|
143
|
+
const handled = await this.processor.acknowledgeWake(
|
|
144
|
+
persistedWake.wakeId,
|
|
145
|
+
persistedWake.runtimeReceiptId,
|
|
146
|
+
);
|
|
147
|
+
deliveries.push({
|
|
148
|
+
triggerId: result.triggerId,
|
|
149
|
+
matchId: result.match.matchId,
|
|
150
|
+
wakeId: handled.wakeId,
|
|
151
|
+
status: handled.status,
|
|
152
|
+
runtimeReceiptId: handled.runtimeReceiptId ?? null,
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
deliveries.push({
|
|
156
|
+
triggerId: result.triggerId,
|
|
157
|
+
matchId: result.match.matchId,
|
|
158
|
+
wakeId: persistedWake?.wakeId ?? null,
|
|
159
|
+
status: delivery.status,
|
|
160
|
+
runtimeReceiptId: persistedWake?.runtimeReceiptId ?? null,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
event,
|
|
167
|
+
results,
|
|
168
|
+
deliveries,
|
|
169
|
+
derivedEvents,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WakeRecordSchema,
|
|
3
|
+
sha256Hex,
|
|
4
|
+
} from '../../dist/src/intelligenceProtocol/index.js';
|
|
5
|
+
import {
|
|
6
|
+
buildGenericRuntimeWakePacket,
|
|
7
|
+
} from './generic-runtime-wake.mjs';
|
|
8
|
+
|
|
9
|
+
async function stableId(prefix, value) {
|
|
10
|
+
const hash = await sha256Hex(value);
|
|
11
|
+
return `${prefix}_${hash.slice(0, 24)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class CompositeWakeCoordinator {
|
|
15
|
+
constructor({
|
|
16
|
+
store,
|
|
17
|
+
triggerEngine,
|
|
18
|
+
deliverer = null,
|
|
19
|
+
packetBuilder = buildGenericRuntimeWakePacket,
|
|
20
|
+
now = () => new Date(),
|
|
21
|
+
}) {
|
|
22
|
+
this.store = store;
|
|
23
|
+
this.triggerEngine = triggerEngine;
|
|
24
|
+
this.deliverer = deliverer;
|
|
25
|
+
this.packetBuilder = packetBuilder;
|
|
26
|
+
this.now = now;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async deliverMatched(match) {
|
|
30
|
+
if (!match || (match.status !== 'matched' && match.status !== 'fired')) {
|
|
31
|
+
return { status: 'not_matched', wake: null };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (match.status === 'fired') {
|
|
35
|
+
const existing = match.firedWakeId
|
|
36
|
+
? this.store.latestWake(match.firedWakeId)
|
|
37
|
+
: null;
|
|
38
|
+
return {
|
|
39
|
+
status: 'already_fired',
|
|
40
|
+
wake: existing,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const definition = this.store.listTriggers().find(
|
|
45
|
+
(candidate) =>
|
|
46
|
+
candidate.triggerId === match.triggerId &&
|
|
47
|
+
candidate.version === match.triggerVersion,
|
|
48
|
+
);
|
|
49
|
+
if (!definition) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Trigger definition not found for ${match.triggerId}@${match.triggerVersion}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!definition.target) {
|
|
56
|
+
return { status: 'no_runtime_target', wake: null };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const wakeId = await stableId(
|
|
60
|
+
'wake',
|
|
61
|
+
`composite:${match.matchId}:${definition.target.runtime}:${definition.target.id}`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const existing = this.store.latestWake(wakeId);
|
|
65
|
+
if (existing?.status === 'handled' || existing?.status === 'delivered') {
|
|
66
|
+
if (match.status !== 'fired') {
|
|
67
|
+
await this.triggerEngine.markFired(match.matchId, wakeId);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
status: existing.status === 'handled' ? 'handled' : 'wake_delivered',
|
|
71
|
+
wake: existing,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const queued = WakeRecordSchema.parse({
|
|
76
|
+
wakeId,
|
|
77
|
+
traceId: match.sourceEvents[0]?.traceId ?? match.matchId,
|
|
78
|
+
decisionId: null,
|
|
79
|
+
subscriptionId: `trigger:${match.triggerId}`,
|
|
80
|
+
sourceEventId: match.matchId,
|
|
81
|
+
createdAt: this.now().toISOString(),
|
|
82
|
+
target: definition.target,
|
|
83
|
+
status: 'queued',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!existing || existing.status === 'dead_letter') {
|
|
87
|
+
await this.store.appendWake(queued);
|
|
88
|
+
await this.store.appendAudit({
|
|
89
|
+
auditId: await stableId(
|
|
90
|
+
'audit',
|
|
91
|
+
`${wakeId}:queued:${this.store.auditLength()}`,
|
|
92
|
+
),
|
|
93
|
+
traceId: queued.traceId,
|
|
94
|
+
timestamp: this.now().toISOString(),
|
|
95
|
+
kind: 'wake.queued',
|
|
96
|
+
entityType: 'wake',
|
|
97
|
+
entityId: wakeId,
|
|
98
|
+
toState: 'wake_queued',
|
|
99
|
+
details: {
|
|
100
|
+
triggerMatchId: match.matchId,
|
|
101
|
+
triggerId: match.triggerId,
|
|
102
|
+
composite: true,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!this.deliverer) {
|
|
108
|
+
return { status: 'wake_queued', wake: queued };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const packet = this.packetBuilder({
|
|
113
|
+
wakeId,
|
|
114
|
+
match,
|
|
115
|
+
definition,
|
|
116
|
+
});
|
|
117
|
+
const receipt = await this.deliverer(packet);
|
|
118
|
+
|
|
119
|
+
const delivered = WakeRecordSchema.parse({
|
|
120
|
+
...queued,
|
|
121
|
+
createdAt: this.now().toISOString(),
|
|
122
|
+
status: 'delivered',
|
|
123
|
+
runtimeReceiptId: receipt.runtimeReceiptId,
|
|
124
|
+
});
|
|
125
|
+
await this.store.appendWake(delivered);
|
|
126
|
+
await this.store.appendAudit({
|
|
127
|
+
auditId: await stableId(
|
|
128
|
+
'audit',
|
|
129
|
+
`${wakeId}:delivered:${this.store.auditLength()}`,
|
|
130
|
+
),
|
|
131
|
+
traceId: delivered.traceId,
|
|
132
|
+
timestamp: this.now().toISOString(),
|
|
133
|
+
kind: 'wake.delivered',
|
|
134
|
+
entityType: 'wake',
|
|
135
|
+
entityId: wakeId,
|
|
136
|
+
fromState: 'wake_queued',
|
|
137
|
+
toState: 'wake_delivered',
|
|
138
|
+
details: {
|
|
139
|
+
triggerMatchId: match.matchId,
|
|
140
|
+
runtimeReceiptId: delivered.runtimeReceiptId,
|
|
141
|
+
duplicate: receipt.duplicate === true,
|
|
142
|
+
composite: true,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await this.triggerEngine.markFired(match.matchId, wakeId);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
status: 'wake_delivered',
|
|
150
|
+
wake: delivered,
|
|
151
|
+
packet,
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const deadLetter = WakeRecordSchema.parse({
|
|
155
|
+
...queued,
|
|
156
|
+
createdAt: this.now().toISOString(),
|
|
157
|
+
status: 'dead_letter',
|
|
158
|
+
});
|
|
159
|
+
await this.store.appendWake(deadLetter);
|
|
160
|
+
await this.store.appendAudit({
|
|
161
|
+
auditId: await stableId(
|
|
162
|
+
'audit',
|
|
163
|
+
`${wakeId}:dead-letter:${this.store.auditLength()}`,
|
|
164
|
+
),
|
|
165
|
+
traceId: deadLetter.traceId,
|
|
166
|
+
timestamp: this.now().toISOString(),
|
|
167
|
+
kind: 'wake.dead_letter',
|
|
168
|
+
entityType: 'wake',
|
|
169
|
+
entityId: wakeId,
|
|
170
|
+
fromState: 'wake_queued',
|
|
171
|
+
toState: 'dead_letter',
|
|
172
|
+
details: {
|
|
173
|
+
triggerMatchId: match.matchId,
|
|
174
|
+
message: error instanceof Error ? error.message : 'wake delivery failed',
|
|
175
|
+
composite: true,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
status: 'dead_letter',
|
|
180
|
+
wake: deadLetter,
|
|
181
|
+
error,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sha256Hex,
|
|
3
|
+
} from '../../dist/src/intelligenceProtocol/index.js';
|
|
4
|
+
|
|
5
|
+
function canonicalize(value) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value
|
|
8
|
+
.map(canonicalize)
|
|
9
|
+
.sort((a, b) =>
|
|
10
|
+
JSON.stringify(a).localeCompare(JSON.stringify(b))
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
if (value && typeof value === 'object') {
|
|
14
|
+
return Object.fromEntries(
|
|
15
|
+
Object.keys(value)
|
|
16
|
+
.sort()
|
|
17
|
+
.map((key) => [key, canonicalize(value[key])]),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sameProducer(left, right) {
|
|
24
|
+
return (
|
|
25
|
+
left.triggerId === right.triggerId &&
|
|
26
|
+
left.triggerVersion === right.triggerVersion
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class DerivedContractRegistry {
|
|
31
|
+
constructor({
|
|
32
|
+
store,
|
|
33
|
+
audit = null,
|
|
34
|
+
now = () => new Date(),
|
|
35
|
+
}) {
|
|
36
|
+
this.store = store;
|
|
37
|
+
this.audit = audit;
|
|
38
|
+
this.now = now;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async fingerprint(payloadSchema) {
|
|
42
|
+
return sha256Hex(canonicalize(payloadSchema ?? {}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
list(eventName) {
|
|
46
|
+
return this.store.listDerivedContracts(eventName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get(eventName, contractVersion) {
|
|
50
|
+
return this.store.getDerivedContract(eventName, contractVersion);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async assertCompatible({
|
|
54
|
+
eventName,
|
|
55
|
+
contractVersion,
|
|
56
|
+
payloadSchema,
|
|
57
|
+
}) {
|
|
58
|
+
const canonicalSchema = canonicalize(payloadSchema ?? {});
|
|
59
|
+
const schemaFingerprint = await this.fingerprint(canonicalSchema);
|
|
60
|
+
const existing = this.get(eventName, contractVersion);
|
|
61
|
+
if (
|
|
62
|
+
existing &&
|
|
63
|
+
existing.schemaFingerprint !== schemaFingerprint
|
|
64
|
+
) {
|
|
65
|
+
const error = new Error(
|
|
66
|
+
`Derived event contract conflict for ${eventName}@${contractVersion}`,
|
|
67
|
+
);
|
|
68
|
+
error.code = 'DERIVED_CONTRACT_SCHEMA_CONFLICT';
|
|
69
|
+
error.details = {
|
|
70
|
+
eventName,
|
|
71
|
+
contractVersion,
|
|
72
|
+
existingFingerprint: existing.schemaFingerprint,
|
|
73
|
+
rejectedFingerprint: schemaFingerprint,
|
|
74
|
+
};
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
payloadSchema: canonicalSchema,
|
|
79
|
+
schemaFingerprint,
|
|
80
|
+
existing,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async registerProducer({
|
|
85
|
+
eventName,
|
|
86
|
+
contractVersion,
|
|
87
|
+
payloadSchema,
|
|
88
|
+
producer,
|
|
89
|
+
}) {
|
|
90
|
+
const canonicalSchema = canonicalize(payloadSchema ?? {});
|
|
91
|
+
const schemaFingerprint = await this.fingerprint(canonicalSchema);
|
|
92
|
+
const existing = this.get(eventName, contractVersion);
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
existing &&
|
|
96
|
+
existing.schemaFingerprint !== schemaFingerprint
|
|
97
|
+
) {
|
|
98
|
+
await this.audit?.({
|
|
99
|
+
kind: 'derived_contract.conflict',
|
|
100
|
+
entityType: 'derived_contract',
|
|
101
|
+
entityId: `${eventName}@${contractVersion}`,
|
|
102
|
+
traceId: `derived-contract:${eventName}@${contractVersion}`,
|
|
103
|
+
details: {
|
|
104
|
+
eventName,
|
|
105
|
+
contractVersion,
|
|
106
|
+
existingFingerprint: existing.schemaFingerprint,
|
|
107
|
+
rejectedFingerprint: schemaFingerprint,
|
|
108
|
+
producer,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const error = new Error(
|
|
113
|
+
`Derived event contract conflict for ${eventName}@${contractVersion}`,
|
|
114
|
+
);
|
|
115
|
+
error.code = 'DERIVED_CONTRACT_SCHEMA_CONFLICT';
|
|
116
|
+
error.details = {
|
|
117
|
+
eventName,
|
|
118
|
+
contractVersion,
|
|
119
|
+
existingFingerprint: existing.schemaFingerprint,
|
|
120
|
+
rejectedFingerprint: schemaFingerprint,
|
|
121
|
+
};
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const producers = existing
|
|
126
|
+
? [...existing.producers]
|
|
127
|
+
: [];
|
|
128
|
+
if (!producers.some((candidate) => sameProducer(candidate, producer))) {
|
|
129
|
+
producers.push({
|
|
130
|
+
triggerId: String(producer.triggerId),
|
|
131
|
+
triggerVersion: String(producer.triggerVersion),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const record = await this.store.putDerivedContract({
|
|
136
|
+
eventName,
|
|
137
|
+
contractVersion,
|
|
138
|
+
payloadSchema: canonicalSchema,
|
|
139
|
+
schemaFingerprint,
|
|
140
|
+
producers,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await this.audit?.({
|
|
144
|
+
kind: existing
|
|
145
|
+
? 'derived_contract.producer_registered'
|
|
146
|
+
: 'derived_contract.created',
|
|
147
|
+
entityType: 'derived_contract',
|
|
148
|
+
entityId: `${eventName}@${contractVersion}`,
|
|
149
|
+
traceId: `derived-contract:${eventName}@${contractVersion}`,
|
|
150
|
+
details: {
|
|
151
|
+
eventName,
|
|
152
|
+
contractVersion,
|
|
153
|
+
schemaFingerprint,
|
|
154
|
+
producer,
|
|
155
|
+
producerCount: record.producers.length,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
status: existing ? 'producer_registered' : 'created',
|
|
161
|
+
record,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
resolveConsumer({
|
|
166
|
+
eventName,
|
|
167
|
+
contractVersion,
|
|
168
|
+
}) {
|
|
169
|
+
const contracts = this.list(eventName);
|
|
170
|
+
|
|
171
|
+
if (contractVersion) {
|
|
172
|
+
const contract = contracts.find(
|
|
173
|
+
(candidate) =>
|
|
174
|
+
candidate.contractVersion === String(contractVersion),
|
|
175
|
+
);
|
|
176
|
+
if (!contract) {
|
|
177
|
+
const error = new Error(
|
|
178
|
+
`Unknown derived event contract ${eventName}@${contractVersion}`,
|
|
179
|
+
);
|
|
180
|
+
error.code = 'DERIVED_CONTRACT_NOT_FOUND';
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
return contract;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (contracts.length === 1) {
|
|
187
|
+
return contracts[0];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (contracts.length === 0) {
|
|
191
|
+
const error = new Error(
|
|
192
|
+
`No derived event contract registered for ${eventName}`,
|
|
193
|
+
);
|
|
194
|
+
error.code = 'DERIVED_CONTRACT_NOT_FOUND';
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const error = new Error(
|
|
199
|
+
`Derived event consumer is ambiguous for ${eventName}; explicit contractVersion required`,
|
|
200
|
+
);
|
|
201
|
+
error.code = 'DERIVED_CONTRACT_AMBIGUOUS';
|
|
202
|
+
error.details = {
|
|
203
|
+
eventName,
|
|
204
|
+
availableVersions: contracts.map(
|
|
205
|
+
(contract) => contract.contractVersion,
|
|
206
|
+
),
|
|
207
|
+
};
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|