@octaviaflow/flow-rules 0.8.1 → 0.10.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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * What a Connector Trigger can subscribe to, per connector.
3
+ *
4
+ * ONE vocabulary for the editor's panel and for an agent authoring over MCP.
5
+ * The trigger's `events` — `[{ entity, operations }]` — is what registers the
6
+ * flow's connector subscription, the record every inbound provider event is
7
+ * matched against: an entity name outside this list (or in the wrong case —
8
+ * HubSpot is lowercase, Salesforce is capitalised) registers a subscription
9
+ * that never fires, with no error anywhere. An agent wrote `lead` for a
10
+ * HubSpot contact trigger on 2026-09-26 and spent an hour on it.
11
+ *
12
+ * Copied as-is from octaviaflow-ui's ConnectorTriggerPanel (the panel's tables
13
+ * were the only copy); the panel is to import from here in its next flow-rules
14
+ * upgrade.
15
+ */
16
+ export interface ConnectorEventEntity {
17
+ entity: string;
18
+ supportedOperations: string[];
19
+ }
20
+ /** One subscribed pair, as stored in a trigger step's `events`. */
21
+ export interface ConnectorTriggerEvent {
22
+ entity: string;
23
+ operations: string[];
24
+ }
25
+ /**
26
+ * Connector ids that emit events a flow trigger can subscribe to. Database /
27
+ * http / openapi / ftp / etc. connectors are intentionally excluded: they
28
+ * cannot push events.
29
+ */
30
+ export declare const EVENT_CAPABLE_CONNECTOR_IDS: ReadonlySet<string>;
31
+ /** Every subscribable entity, by connector id. */
32
+ export declare const CONNECTOR_EVENT_ENTITIES: Readonly<Record<string, ConnectorEventEntity[]>>;
33
+ export declare function getEntitiesForConnector(connectorId: string): ConnectorEventEntity[];
34
+ /** True when this connector type can drive an event trigger. */
35
+ export declare function isEventCapableConnector(connectorId: string): boolean;
36
+ /**
37
+ * The one-line summary the canvas shows under a Connector Trigger
38
+ * (`Hubspot · 4 events`) — the same string the panel writes.
39
+ */
40
+ export declare function summarizeConnectorTriggerEvents(connectorName: string, events: ConnectorTriggerEvent[]): string;
41
+ /**
42
+ * Why `events` cannot register a trigger on this connector — or `null` when
43
+ * it can. Names are matched EXACTLY, because the inbound matcher matches them
44
+ * exactly; a case-only miss says what was meant.
45
+ */
46
+ export declare function validateConnectorTriggerEvents(connectorId: string, events: unknown): string | null;
package/dist/index.cjs CHANGED
@@ -45,9 +45,11 @@ __export(exports_src, {
45
45
  AggregateFunction: () => AggregateFunction,
46
46
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
47
47
  CONFIG_CONTRACTS: () => CONFIG_CONTRACTS,
48
+ CONNECTOR_EVENT_ENTITIES: () => CONNECTOR_EVENT_ENTITIES,
48
49
  ConditionConfig: () => ConditionConfig,
49
50
  DATABASE_CONNECTOR_IDS: () => DATABASE_CONNECTOR_IDS,
50
51
  DebugConfig: () => DebugConfig,
52
+ EVENT_CAPABLE_CONNECTOR_IDS: () => EVENT_CAPABLE_CONNECTOR_IDS,
51
53
  EdgeType: () => EdgeType,
52
54
  EmailConfig: () => EmailConfig,
53
55
  FieldType: () => FieldType,
@@ -89,20 +91,24 @@ __export(exports_src, {
89
91
  getActionsByCategory: () => getActionsByCategory,
90
92
  getAllActions: () => getAllActions,
91
93
  getEdgeValidationStatus: () => getEdgeValidationStatus,
94
+ getEntitiesForConnector: () => getEntitiesForConnector,
92
95
  getNodeConfigStatus: () => getNodeConfigStatus,
93
96
  getNodeValidationStatus: () => getNodeValidationStatus,
94
97
  getStorageProvider: () => getStorageProvider,
95
98
  inferSchemaFromSample: () => inferSchemaFromSample,
96
99
  isEditorPrivateKey: () => isEditorPrivateKey,
100
+ isEventCapableConnector: () => isEventCapableConnector,
97
101
  isMappingFunction: () => isMappingFunction,
98
102
  isQueryableConnector: () => isQueryableConnector,
99
103
  mappingTransformFunctionName: () => mappingTransformFunctionName,
100
104
  migrateDelayConfig: () => migrateDelayConfig,
101
105
  queryDefinition: () => queryDefinition,
102
106
  storageConfigKeys: () => storageConfigKeys,
107
+ summarizeConnectorTriggerEvents: () => summarizeConnectorTriggerEvents,
103
108
  transformConnectionToEdge: () => transformConnectionToEdge,
104
109
  transformFlowToWorkflow: () => transformFlowToWorkflow,
105
110
  transformStepToNode: () => transformStepToNode,
111
+ validateConnectorTriggerEvents: () => validateConnectorTriggerEvents,
106
112
  validateEdge: () => validateEdge,
107
113
  validateFlowData: () => validateFlowData,
108
114
  validateNode: () => validateNode,
@@ -555,7 +561,7 @@ var connectorTriggerDefinition = {
555
561
  key: "events",
556
562
  label: "Events",
557
563
  type: "json" /* JSON */,
558
- description: "Entity/operation pairs to subscribe to",
564
+ description: 'Entity/operation pairs to subscribe to, as [{ "entity": "contact", "operations": ["Create", "Update"] }]. ' + "This is what registers the trigger — the platform derives the flow's connector subscription from it. " + "Entity and operation names are per connector (describe_action_type lists them); a name outside that list never fires.",
559
565
  validation: { required: true }
560
566
  }
561
567
  ]
@@ -3974,6 +3980,124 @@ function storageConfigKeys() {
3974
3980
  }
3975
3981
  return [...keys];
3976
3982
  }
3983
+ // src/catalog/triggers/connector-trigger/events.ts
3984
+ var EVENT_CAPABLE_CONNECTOR_IDS = new Set([
3985
+ "quickbooks_online",
3986
+ "shopify",
3987
+ "hubspot",
3988
+ "salesforce",
3989
+ "google_sheets",
3990
+ "whatsapp"
3991
+ ]);
3992
+ var QBO_ENTITIES = [
3993
+ { entity: "Invoice", supportedOperations: ["Create", "Update", "Delete", "Void", "Emailed"] },
3994
+ { entity: "Customer", supportedOperations: ["Create", "Update", "Delete", "Merge"] },
3995
+ { entity: "Payment", supportedOperations: ["Create", "Update", "Delete", "Void"] },
3996
+ { entity: "Bill", supportedOperations: ["Create", "Update", "Delete", "Void"] },
3997
+ { entity: "Estimate", supportedOperations: ["Create", "Update", "Delete"] },
3998
+ { entity: "SalesReceipt", supportedOperations: ["Create", "Update", "Delete", "Void", "Emailed"] },
3999
+ { entity: "CreditMemo", supportedOperations: ["Create", "Update", "Delete", "Void", "Emailed"] },
4000
+ { entity: "Vendor", supportedOperations: ["Create", "Update", "Delete", "Merge"] },
4001
+ { entity: "Item", supportedOperations: ["Create", "Update", "Delete"] },
4002
+ { entity: "Account", supportedOperations: ["Create", "Update", "Delete", "Merge"] },
4003
+ { entity: "JournalEntry", supportedOperations: ["Create", "Update", "Delete", "Void"] },
4004
+ { entity: "PurchaseOrder", supportedOperations: ["Create", "Update", "Delete"] }
4005
+ ];
4006
+ var SALESFORCE_ENTITIES = [
4007
+ { entity: "Lead", supportedOperations: ["Create", "Update", "Delete", "Undelete"] },
4008
+ { entity: "Account", supportedOperations: ["Create", "Update", "Delete", "Undelete"] },
4009
+ { entity: "Contact", supportedOperations: ["Create", "Update", "Delete", "Undelete"] },
4010
+ { entity: "Opportunity", supportedOperations: ["Create", "Update", "Delete", "Undelete"] },
4011
+ { entity: "Case", supportedOperations: ["Create", "Update", "Delete", "Undelete"] }
4012
+ ];
4013
+ var HUBSPOT_ENTITIES = [
4014
+ { entity: "contact", supportedOperations: ["Create", "Update", "Delete", "Merge", "Restore", "AssociationChange"] },
4015
+ { entity: "company", supportedOperations: ["Create", "Update", "Delete", "Merge", "Restore", "AssociationChange"] },
4016
+ { entity: "deal", supportedOperations: ["Create", "Update", "Delete", "Merge", "Restore", "AssociationChange"] },
4017
+ { entity: "ticket", supportedOperations: ["Create", "Update", "Delete", "Merge", "Restore", "AssociationChange"] },
4018
+ { entity: "product", supportedOperations: ["Create", "Update", "Delete", "Restore"] },
4019
+ { entity: "line_item", supportedOperations: ["Create", "Update", "Delete", "Restore"] },
4020
+ { entity: "lead", supportedOperations: ["Create", "Update", "Delete", "Restore"] },
4021
+ { entity: "quote", supportedOperations: ["Create", "Update", "Delete", "Restore"] },
4022
+ { entity: "call", supportedOperations: ["Create", "Update", "Delete"] },
4023
+ { entity: "email", supportedOperations: ["Create", "Update", "Delete"] },
4024
+ { entity: "meeting_event", supportedOperations: ["Create", "Update", "Delete"] },
4025
+ { entity: "task", supportedOperations: ["Create", "Update", "Delete"] },
4026
+ { entity: "note", supportedOperations: ["Create", "Update", "Delete"] },
4027
+ { entity: "order", supportedOperations: ["Create", "Update", "Delete"] },
4028
+ { entity: "cart", supportedOperations: ["Create", "Update", "Delete"] },
4029
+ { entity: "invoice", supportedOperations: ["Create", "Update", "Delete"] }
4030
+ ];
4031
+ var SHOPIFY_ENTITIES = [
4032
+ { entity: "Order", supportedOperations: ["Create", "Update", "Paid", "Fulfilled", "Cancelled"] },
4033
+ { entity: "Product", supportedOperations: ["Create", "Update"] },
4034
+ { entity: "Customer", supportedOperations: ["Create", "Update"] }
4035
+ ];
4036
+ var GOOGLE_SHEETS_ENTITIES = [
4037
+ { entity: "Row", supportedOperations: ["Create", "Update", "Delete"] },
4038
+ { entity: "Sheet", supportedOperations: ["Create", "Update"] }
4039
+ ];
4040
+ var WHATSAPP_ENTITIES = [
4041
+ { entity: "Message", supportedOperations: ["Received"] }
4042
+ ];
4043
+ var CONNECTOR_EVENT_ENTITIES = {
4044
+ quickbooks_online: QBO_ENTITIES,
4045
+ salesforce: SALESFORCE_ENTITIES,
4046
+ hubspot: HUBSPOT_ENTITIES,
4047
+ shopify: SHOPIFY_ENTITIES,
4048
+ google_sheets: GOOGLE_SHEETS_ENTITIES,
4049
+ whatsapp: WHATSAPP_ENTITIES
4050
+ };
4051
+ function getEntitiesForConnector(connectorId) {
4052
+ return CONNECTOR_EVENT_ENTITIES[connectorId] ?? [];
4053
+ }
4054
+ function isEventCapableConnector(connectorId) {
4055
+ return EVENT_CAPABLE_CONNECTOR_IDS.has(connectorId) && getEntitiesForConnector(connectorId).length > 0;
4056
+ }
4057
+ function summarizeConnectorTriggerEvents(connectorName, events) {
4058
+ const eventCount = events.reduce((acc, ev) => acc + ev.operations.length, 0);
4059
+ if (eventCount === 0)
4060
+ return connectorName;
4061
+ const noun = eventCount === 1 ? "event" : "events";
4062
+ return `${connectorName} · ${eventCount} ${noun}`;
4063
+ }
4064
+ function validateConnectorTriggerEvents(connectorId, events) {
4065
+ const entities = getEntitiesForConnector(connectorId);
4066
+ if (!isEventCapableConnector(connectorId)) {
4067
+ return `"${connectorId}" does not emit events a trigger can subscribe to. ` + `Event-capable connectors: ${[...EVENT_CAPABLE_CONNECTOR_IDS].join(", ")}.`;
4068
+ }
4069
+ if (!Array.isArray(events) || events.length === 0) {
4070
+ return 'events must be a non-empty array of { "entity": string, "operations": string[] }.';
4071
+ }
4072
+ const seen = new Set;
4073
+ for (const ev of events) {
4074
+ const entity = ev?.entity;
4075
+ const operations = ev?.operations;
4076
+ if (typeof entity !== "string" || !entity) {
4077
+ return 'Each event needs an "entity" (string).';
4078
+ }
4079
+ const known = entities.find((e) => e.entity === entity);
4080
+ if (!known) {
4081
+ const caseHit = entities.find((e) => e.entity.toLowerCase() === entity.toLowerCase());
4082
+ const hint = caseHit ? ` Did you mean "${caseHit.entity}"? Names are case-sensitive.` : "";
4083
+ return `Unknown entity "${entity}" for ${connectorId}.${hint} Entities: ${entities.map((e) => e.entity).join(", ")}.`;
4084
+ }
4085
+ if (seen.has(entity))
4086
+ return `Entity "${entity}" is listed twice — give it one entry with all its operations.`;
4087
+ seen.add(entity);
4088
+ if (!Array.isArray(operations) || operations.length === 0) {
4089
+ return `Event "${entity}" needs a non-empty "operations" array. Operations: ${known.supportedOperations.join(", ")}.`;
4090
+ }
4091
+ for (const op of operations) {
4092
+ if (typeof op !== "string" || !known.supportedOperations.includes(op)) {
4093
+ const caseHit = typeof op === "string" ? known.supportedOperations.find((o) => o.toLowerCase() === op.toLowerCase()) : undefined;
4094
+ const hint = caseHit ? ` Did you mean "${caseHit}"?` : "";
4095
+ return `Unknown operation "${String(op)}" for ${connectorId} ${entity}.${hint} Operations: ${known.supportedOperations.join(", ")}.`;
4096
+ }
4097
+ }
4098
+ }
4099
+ return null;
4100
+ }
3977
4101
  // src/catalog/workflow.ts
3978
4102
  var NodeStatus;
3979
4103
  ((NodeStatus) => {
@@ -5046,7 +5170,6 @@ function validateStepConfig(step) {
5046
5170
  if (!definition)
5047
5171
  return issues;
5048
5172
  const declared = new Map(contractFieldsFor(definition).map((f) => [f.key, f]));
5049
- const derived = contractDerivedKeys(definition);
5050
5173
  const entries = Object.entries(config);
5051
5174
  for (const [key, value] of entries) {
5052
5175
  const field = declared.get(key);
@@ -5062,7 +5185,7 @@ function validateStepConfig(step) {
5062
5185
  }
5063
5186
  if (!typeMatches(field, value)) {
5064
5187
  issues.push({
5065
- severity: derived.has(key) ? "warning" : "error",
5188
+ severity: "error",
5066
5189
  code: "FIELD_WRONG_TYPE",
5067
5190
  stepId,
5068
5191
  field: key,
package/dist/index.d.ts CHANGED
@@ -40,6 +40,7 @@ export { DATABASE_CONNECTOR_IDS, type FieldMapping, TRANSFORM_OPERATIONS, Transf
40
40
  export { STORAGE_CONNECTOR_IDS, type StorageExtraField, type StoragePathField, getStorageProvider, storageConfigKeys, } from "./catalog/storage/storageCapabilities";
41
41
  export { ManualTriggerConfig } from "./catalog/triggers/manual-trigger/index";
42
42
  export { type ManualTriggerConfigType } from "./catalog/triggers/manual-trigger/config";
43
+ export { CONNECTOR_EVENT_ENTITIES, EVENT_CAPABLE_CONNECTOR_IDS, getEntitiesForConnector, isEventCapableConnector, summarizeConnectorTriggerEvents, validateConnectorTriggerEvents, type ConnectorEventEntity, type ConnectorTriggerEvent, } from "./catalog/triggers/connector-trigger/events";
43
44
  export * from "./catalog/workflow";
44
45
  export { type FlowConnection, type FlowData, type FlowStep, generateConnectionsFromSteps, transformConnectionToEdge, transformFlowToWorkflow, transformStepToNode, validateFlowData, } from "./rules/flowShape";
45
46
  export { flattenNestedWorkflow } from "./rules/flattenNestedWorkflow";
package/dist/index.js CHANGED
@@ -441,7 +441,7 @@ var connectorTriggerDefinition = {
441
441
  key: "events",
442
442
  label: "Events",
443
443
  type: "json" /* JSON */,
444
- description: "Entity/operation pairs to subscribe to",
444
+ description: 'Entity/operation pairs to subscribe to, as [{ "entity": "contact", "operations": ["Create", "Update"] }]. ' + "This is what registers the trigger — the platform derives the flow's connector subscription from it. " + "Entity and operation names are per connector (describe_action_type lists them); a name outside that list never fires.",
445
445
  validation: { required: true }
446
446
  }
447
447
  ]
@@ -3860,6 +3860,124 @@ function storageConfigKeys() {
3860
3860
  }
3861
3861
  return [...keys];
3862
3862
  }
3863
+ // src/catalog/triggers/connector-trigger/events.ts
3864
+ var EVENT_CAPABLE_CONNECTOR_IDS = new Set([
3865
+ "quickbooks_online",
3866
+ "shopify",
3867
+ "hubspot",
3868
+ "salesforce",
3869
+ "google_sheets",
3870
+ "whatsapp"
3871
+ ]);
3872
+ var QBO_ENTITIES = [
3873
+ { entity: "Invoice", supportedOperations: ["Create", "Update", "Delete", "Void", "Emailed"] },
3874
+ { entity: "Customer", supportedOperations: ["Create", "Update", "Delete", "Merge"] },
3875
+ { entity: "Payment", supportedOperations: ["Create", "Update", "Delete", "Void"] },
3876
+ { entity: "Bill", supportedOperations: ["Create", "Update", "Delete", "Void"] },
3877
+ { entity: "Estimate", supportedOperations: ["Create", "Update", "Delete"] },
3878
+ { entity: "SalesReceipt", supportedOperations: ["Create", "Update", "Delete", "Void", "Emailed"] },
3879
+ { entity: "CreditMemo", supportedOperations: ["Create", "Update", "Delete", "Void", "Emailed"] },
3880
+ { entity: "Vendor", supportedOperations: ["Create", "Update", "Delete", "Merge"] },
3881
+ { entity: "Item", supportedOperations: ["Create", "Update", "Delete"] },
3882
+ { entity: "Account", supportedOperations: ["Create", "Update", "Delete", "Merge"] },
3883
+ { entity: "JournalEntry", supportedOperations: ["Create", "Update", "Delete", "Void"] },
3884
+ { entity: "PurchaseOrder", supportedOperations: ["Create", "Update", "Delete"] }
3885
+ ];
3886
+ var SALESFORCE_ENTITIES = [
3887
+ { entity: "Lead", supportedOperations: ["Create", "Update", "Delete", "Undelete"] },
3888
+ { entity: "Account", supportedOperations: ["Create", "Update", "Delete", "Undelete"] },
3889
+ { entity: "Contact", supportedOperations: ["Create", "Update", "Delete", "Undelete"] },
3890
+ { entity: "Opportunity", supportedOperations: ["Create", "Update", "Delete", "Undelete"] },
3891
+ { entity: "Case", supportedOperations: ["Create", "Update", "Delete", "Undelete"] }
3892
+ ];
3893
+ var HUBSPOT_ENTITIES = [
3894
+ { entity: "contact", supportedOperations: ["Create", "Update", "Delete", "Merge", "Restore", "AssociationChange"] },
3895
+ { entity: "company", supportedOperations: ["Create", "Update", "Delete", "Merge", "Restore", "AssociationChange"] },
3896
+ { entity: "deal", supportedOperations: ["Create", "Update", "Delete", "Merge", "Restore", "AssociationChange"] },
3897
+ { entity: "ticket", supportedOperations: ["Create", "Update", "Delete", "Merge", "Restore", "AssociationChange"] },
3898
+ { entity: "product", supportedOperations: ["Create", "Update", "Delete", "Restore"] },
3899
+ { entity: "line_item", supportedOperations: ["Create", "Update", "Delete", "Restore"] },
3900
+ { entity: "lead", supportedOperations: ["Create", "Update", "Delete", "Restore"] },
3901
+ { entity: "quote", supportedOperations: ["Create", "Update", "Delete", "Restore"] },
3902
+ { entity: "call", supportedOperations: ["Create", "Update", "Delete"] },
3903
+ { entity: "email", supportedOperations: ["Create", "Update", "Delete"] },
3904
+ { entity: "meeting_event", supportedOperations: ["Create", "Update", "Delete"] },
3905
+ { entity: "task", supportedOperations: ["Create", "Update", "Delete"] },
3906
+ { entity: "note", supportedOperations: ["Create", "Update", "Delete"] },
3907
+ { entity: "order", supportedOperations: ["Create", "Update", "Delete"] },
3908
+ { entity: "cart", supportedOperations: ["Create", "Update", "Delete"] },
3909
+ { entity: "invoice", supportedOperations: ["Create", "Update", "Delete"] }
3910
+ ];
3911
+ var SHOPIFY_ENTITIES = [
3912
+ { entity: "Order", supportedOperations: ["Create", "Update", "Paid", "Fulfilled", "Cancelled"] },
3913
+ { entity: "Product", supportedOperations: ["Create", "Update"] },
3914
+ { entity: "Customer", supportedOperations: ["Create", "Update"] }
3915
+ ];
3916
+ var GOOGLE_SHEETS_ENTITIES = [
3917
+ { entity: "Row", supportedOperations: ["Create", "Update", "Delete"] },
3918
+ { entity: "Sheet", supportedOperations: ["Create", "Update"] }
3919
+ ];
3920
+ var WHATSAPP_ENTITIES = [
3921
+ { entity: "Message", supportedOperations: ["Received"] }
3922
+ ];
3923
+ var CONNECTOR_EVENT_ENTITIES = {
3924
+ quickbooks_online: QBO_ENTITIES,
3925
+ salesforce: SALESFORCE_ENTITIES,
3926
+ hubspot: HUBSPOT_ENTITIES,
3927
+ shopify: SHOPIFY_ENTITIES,
3928
+ google_sheets: GOOGLE_SHEETS_ENTITIES,
3929
+ whatsapp: WHATSAPP_ENTITIES
3930
+ };
3931
+ function getEntitiesForConnector(connectorId) {
3932
+ return CONNECTOR_EVENT_ENTITIES[connectorId] ?? [];
3933
+ }
3934
+ function isEventCapableConnector(connectorId) {
3935
+ return EVENT_CAPABLE_CONNECTOR_IDS.has(connectorId) && getEntitiesForConnector(connectorId).length > 0;
3936
+ }
3937
+ function summarizeConnectorTriggerEvents(connectorName, events) {
3938
+ const eventCount = events.reduce((acc, ev) => acc + ev.operations.length, 0);
3939
+ if (eventCount === 0)
3940
+ return connectorName;
3941
+ const noun = eventCount === 1 ? "event" : "events";
3942
+ return `${connectorName} · ${eventCount} ${noun}`;
3943
+ }
3944
+ function validateConnectorTriggerEvents(connectorId, events) {
3945
+ const entities = getEntitiesForConnector(connectorId);
3946
+ if (!isEventCapableConnector(connectorId)) {
3947
+ return `"${connectorId}" does not emit events a trigger can subscribe to. ` + `Event-capable connectors: ${[...EVENT_CAPABLE_CONNECTOR_IDS].join(", ")}.`;
3948
+ }
3949
+ if (!Array.isArray(events) || events.length === 0) {
3950
+ return 'events must be a non-empty array of { "entity": string, "operations": string[] }.';
3951
+ }
3952
+ const seen = new Set;
3953
+ for (const ev of events) {
3954
+ const entity = ev?.entity;
3955
+ const operations = ev?.operations;
3956
+ if (typeof entity !== "string" || !entity) {
3957
+ return 'Each event needs an "entity" (string).';
3958
+ }
3959
+ const known = entities.find((e) => e.entity === entity);
3960
+ if (!known) {
3961
+ const caseHit = entities.find((e) => e.entity.toLowerCase() === entity.toLowerCase());
3962
+ const hint = caseHit ? ` Did you mean "${caseHit.entity}"? Names are case-sensitive.` : "";
3963
+ return `Unknown entity "${entity}" for ${connectorId}.${hint} Entities: ${entities.map((e) => e.entity).join(", ")}.`;
3964
+ }
3965
+ if (seen.has(entity))
3966
+ return `Entity "${entity}" is listed twice — give it one entry with all its operations.`;
3967
+ seen.add(entity);
3968
+ if (!Array.isArray(operations) || operations.length === 0) {
3969
+ return `Event "${entity}" needs a non-empty "operations" array. Operations: ${known.supportedOperations.join(", ")}.`;
3970
+ }
3971
+ for (const op of operations) {
3972
+ if (typeof op !== "string" || !known.supportedOperations.includes(op)) {
3973
+ const caseHit = typeof op === "string" ? known.supportedOperations.find((o) => o.toLowerCase() === op.toLowerCase()) : undefined;
3974
+ const hint = caseHit ? ` Did you mean "${caseHit}"?` : "";
3975
+ return `Unknown operation "${String(op)}" for ${connectorId} ${entity}.${hint} Operations: ${known.supportedOperations.join(", ")}.`;
3976
+ }
3977
+ }
3978
+ }
3979
+ return null;
3980
+ }
3863
3981
  // src/catalog/workflow.ts
3864
3982
  var NodeStatus;
3865
3983
  ((NodeStatus) => {
@@ -4932,7 +5050,6 @@ function validateStepConfig(step) {
4932
5050
  if (!definition)
4933
5051
  return issues;
4934
5052
  const declared = new Map(contractFieldsFor(definition).map((f) => [f.key, f]));
4935
- const derived = contractDerivedKeys(definition);
4936
5053
  const entries = Object.entries(config);
4937
5054
  for (const [key, value] of entries) {
4938
5055
  const field = declared.get(key);
@@ -4948,7 +5065,7 @@ function validateStepConfig(step) {
4948
5065
  }
4949
5066
  if (!typeMatches(field, value)) {
4950
5067
  issues.push({
4951
- severity: derived.has(key) ? "warning" : "error",
5068
+ severity: "error",
4952
5069
  code: "FIELD_WRONG_TYPE",
4953
5070
  stepId,
4954
5071
  field: key,
@@ -5068,9 +5185,11 @@ export {
5068
5185
  AggregateFunction,
5069
5186
  CONDITION_OPERATORS,
5070
5187
  CONFIG_CONTRACTS,
5188
+ CONNECTOR_EVENT_ENTITIES,
5071
5189
  ConditionConfig,
5072
5190
  DATABASE_CONNECTOR_IDS,
5073
5191
  DebugConfig,
5192
+ EVENT_CAPABLE_CONNECTOR_IDS,
5074
5193
  EdgeType,
5075
5194
  EmailConfig,
5076
5195
  FieldType,
@@ -5112,20 +5231,24 @@ export {
5112
5231
  getActionsByCategory,
5113
5232
  getAllActions,
5114
5233
  getEdgeValidationStatus,
5234
+ getEntitiesForConnector,
5115
5235
  getNodeConfigStatus,
5116
5236
  getNodeValidationStatus,
5117
5237
  getStorageProvider,
5118
5238
  inferSchemaFromSample,
5119
5239
  isEditorPrivateKey,
5240
+ isEventCapableConnector,
5120
5241
  isMappingFunction,
5121
5242
  isQueryableConnector,
5122
5243
  mappingTransformFunctionName,
5123
5244
  migrateDelayConfig,
5124
5245
  queryDefinition,
5125
5246
  storageConfigKeys,
5247
+ summarizeConnectorTriggerEvents,
5126
5248
  transformConnectionToEdge,
5127
5249
  transformFlowToWorkflow,
5128
5250
  transformStepToNode,
5251
+ validateConnectorTriggerEvents,
5129
5252
  validateEdge,
5130
5253
  validateFlowData,
5131
5254
  validateNode,
@@ -93,12 +93,13 @@ export declare function contractVocabularyDrift(actionId: string): string[];
93
93
  /**
94
94
  * Keys `contractFieldsFor` ADDED — declared by config.ts but not by inputSchema.
95
95
  *
96
- * The fence treats a type mismatch on one of these as a WARNING, not an error,
97
- * until it has been measured against real flows. The reason is concrete: the
98
- * moment http_request's contract declared `queryParams: array`, a fixture that
99
- * stored it as an object stopped saving. On beta 71 of 131 real config keys
100
- * were undeclared, so any of them may be stored in a shape the contract does
101
- * not expect — and refusing the save would leave whoever opens that flow
102
- * unable to save it. inputSchema-declared keys keep their existing error.
96
+ * In 0.8.1 the fence treated a type mismatch on one of these as a WARNING,
97
+ * pending a measurement: the moment http_request's contract declared
98
+ * `queryParams: array`, a fixture storing it as an object stopped saving, and
99
+ * with 71 of 131 real keys previously undeclared any might have been stored in
100
+ * an unexpected shape. Measured 2026-09-26 against every flow on beta — 79
101
+ * flows, 302 steps — zero mismatches. 0.9.0 promotes them to errors, the same
102
+ * as any other declared key. This helper remains for tooling that wants to
103
+ * know which keys the contract added.
103
104
  */
104
105
  export declare function contractDerivedKeys(definition: ActionDefinition): Set<string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@octaviaflow/flow-rules",
3
- "version": "0.8.1",
4
- "description": "The flow action catalog and the rules engine behind Flow Doctor \u2014 one definition of what a step accepts and what makes a flow valid, shared by the editor and the server",
3
+ "version": "0.10.0",
4
+ "description": "The flow action catalog and the rules engine behind Flow Doctor — one definition of what a step accepts and what makes a flow valid, shared by the editor and the server",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",