@octaviaflow/flow-rules 0.7.0 → 0.8.1

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/dist/index.cjs CHANGED
@@ -44,6 +44,7 @@ __export(exports_src, {
44
44
  ActionType: () => ActionType,
45
45
  AggregateFunction: () => AggregateFunction,
46
46
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
47
+ CONFIG_CONTRACTS: () => CONFIG_CONTRACTS,
47
48
  ConditionConfig: () => ConditionConfig,
48
49
  DATABASE_CONNECTOR_IDS: () => DATABASE_CONNECTOR_IDS,
49
50
  DebugConfig: () => DebugConfig,
@@ -71,6 +72,12 @@ __export(exports_src, {
71
72
  collectArrayPaths: () => collectArrayPaths,
72
73
  collectArrayPathsFromSample: () => collectArrayPathsFromSample,
73
74
  configErrors: () => configErrors,
75
+ contractDerivedKeys: () => contractDerivedKeys,
76
+ contractDrift: () => contractDrift,
77
+ contractFieldsFor: () => contractFieldsFor,
78
+ contractFor: () => contractFor,
79
+ contractVocabularyDrift: () => contractVocabularyDrift,
80
+ declaredKeys: () => declaredKeys,
74
81
  emailDefinition: () => emailDefinition,
75
82
  flattenNestedWorkflow: () => flattenNestedWorkflow,
76
83
  generateConnectionsFromSteps: () => generateConnectionsFromSteps,
@@ -86,6 +93,7 @@ __export(exports_src, {
86
93
  getNodeValidationStatus: () => getNodeValidationStatus,
87
94
  getStorageProvider: () => getStorageProvider,
88
95
  inferSchemaFromSample: () => inferSchemaFromSample,
96
+ isEditorPrivateKey: () => isEditorPrivateKey,
89
97
  isMappingFunction: () => isMappingFunction,
90
98
  isQueryableConnector: () => isQueryableConnector,
91
99
  mappingTransformFunctionName: () => mappingTransformFunctionName,
@@ -1192,6 +1200,149 @@ var BuildQueryConfigDefaults = {
1192
1200
  parameters: {}
1193
1201
  }
1194
1202
  };
1203
+ var validateBuildQueryConfig = (config) => {
1204
+ const errors = [];
1205
+ const warnings = [];
1206
+ if (!config.connectorId) {
1207
+ errors.push({
1208
+ field: "connectorId",
1209
+ message: "Please select a connector",
1210
+ severity: "error"
1211
+ });
1212
+ }
1213
+ if (!config.mode) {
1214
+ errors.push({
1215
+ field: "mode",
1216
+ message: "Query mode is required",
1217
+ severity: "error"
1218
+ });
1219
+ }
1220
+ if (!config.queryType) {
1221
+ errors.push({
1222
+ field: "queryType",
1223
+ message: "Query type is required",
1224
+ severity: "error"
1225
+ });
1226
+ }
1227
+ if (config.mode === "visual" /* Visual */) {
1228
+ const visualErrors = validateVisualQueryConfig(config.visualConfig);
1229
+ errors.push(...visualErrors);
1230
+ } else if (config.mode === "sql" /* SQL */) {
1231
+ const sqlErrors = validateSQLQueryConfig(config.sqlConfig);
1232
+ errors.push(...sqlErrors);
1233
+ }
1234
+ if (config.timeout !== undefined) {
1235
+ if (config.timeout < 1000) {
1236
+ errors.push({
1237
+ field: "timeout",
1238
+ message: "Timeout must be at least 1000ms",
1239
+ severity: "error"
1240
+ });
1241
+ } else if (config.timeout > 300000) {
1242
+ warnings.push({
1243
+ field: "timeout",
1244
+ message: "Timeout is very high (>5 minutes)",
1245
+ severity: "warning"
1246
+ });
1247
+ }
1248
+ }
1249
+ if (config.maxRows !== undefined) {
1250
+ if (config.maxRows < 1) {
1251
+ errors.push({
1252
+ field: "maxRows",
1253
+ message: "Max rows must be at least 1",
1254
+ severity: "error"
1255
+ });
1256
+ } else if (config.maxRows > 1e4) {
1257
+ warnings.push({
1258
+ field: "maxRows",
1259
+ message: "Max rows is very high (>10,000). This may impact performance.",
1260
+ severity: "warning"
1261
+ });
1262
+ }
1263
+ }
1264
+ return {
1265
+ valid: errors.length === 0,
1266
+ errors,
1267
+ warnings
1268
+ };
1269
+ };
1270
+ var validateVisualQueryConfig = (config) => {
1271
+ const errors = [];
1272
+ if (!config) {
1273
+ errors.push({
1274
+ field: "visualConfig",
1275
+ message: "Visual query configuration is required",
1276
+ severity: "error"
1277
+ });
1278
+ return errors;
1279
+ }
1280
+ if (!config.tables || config.tables.length === 0) {
1281
+ errors.push({
1282
+ field: "visualConfig.tables",
1283
+ message: "At least one table must be selected",
1284
+ severity: "error"
1285
+ });
1286
+ }
1287
+ if (!config.fields || config.fields.length === 0) {
1288
+ errors.push({
1289
+ field: "visualConfig.fields",
1290
+ message: "At least one field must be selected",
1291
+ severity: "error"
1292
+ });
1293
+ }
1294
+ if (config.tables && config.tables.length > 1) {
1295
+ if (!config.joins || config.joins.length === 0) {
1296
+ errors.push({
1297
+ field: "visualConfig.joins",
1298
+ message: "Joins are required when selecting from multiple tables",
1299
+ severity: "error"
1300
+ });
1301
+ }
1302
+ }
1303
+ const hasAggregates = config.fields?.some((field) => field.aggregate);
1304
+ const hasNonAggregates = config.fields?.some((field) => !field.aggregate);
1305
+ if (hasAggregates && hasNonAggregates) {
1306
+ if (!config.groupBy || config.groupBy.length === 0) {
1307
+ errors.push({
1308
+ field: "visualConfig.groupBy",
1309
+ message: "Group by is required when mixing aggregate and non-aggregate fields",
1310
+ severity: "error"
1311
+ });
1312
+ }
1313
+ }
1314
+ return errors;
1315
+ };
1316
+ var validateSQLQueryConfig = (config) => {
1317
+ const errors = [];
1318
+ if (!config) {
1319
+ errors.push({
1320
+ field: "sqlConfig",
1321
+ message: "SQL query configuration is required",
1322
+ severity: "error"
1323
+ });
1324
+ return errors;
1325
+ }
1326
+ if (!config.sql || config.sql.trim().length === 0) {
1327
+ errors.push({
1328
+ field: "sqlConfig.sql",
1329
+ message: "SQL query is required",
1330
+ severity: "error"
1331
+ });
1332
+ } else {
1333
+ const sql = config.sql.trim().toLowerCase();
1334
+ const dangerousKeywords = ["drop", "truncate", "alter", "create", "grant", "revoke"];
1335
+ const hasDangerousKeyword = dangerousKeywords.some((keyword) => sql.includes(keyword + " "));
1336
+ if (hasDangerousKeyword) {
1337
+ errors.push({
1338
+ field: "sqlConfig.sql",
1339
+ message: "SQL query contains potentially dangerous operations (DROP, TRUNCATE, ALTER, etc.)",
1340
+ severity: "error"
1341
+ });
1342
+ }
1343
+ }
1344
+ return errors;
1345
+ };
1195
1346
  function isQueryableConnector(connector) {
1196
1347
  if (connector.isQueryable !== undefined) {
1197
1348
  return connector.isQueryable;
@@ -1217,6 +1368,15 @@ function isQueryableConnector(connector) {
1217
1368
  ];
1218
1369
  return !storageTypes.includes(connector.type.toLowerCase());
1219
1370
  }
1371
+ var BuildQueryContractSchema = {
1372
+ connectorId: { type: "connection", required: true },
1373
+ mode: { type: "select", required: true },
1374
+ queryType: { type: "select", required: true },
1375
+ visualConfig: { type: "object", required: false },
1376
+ sqlConfig: { type: "object", required: false },
1377
+ timeout: { type: "number", required: false },
1378
+ maxRows: { type: "number", required: false }
1379
+ };
1220
1380
 
1221
1381
  // src/catalog/general/query/index.ts
1222
1382
  var queryDefinition = {
@@ -2106,7 +2266,7 @@ var slackMessageDefinition = {
2106
2266
  fields: [
2107
2267
  { key: "connection", label: "Slack Workspace", type: "connection" /* Connection */, validation: { required: true } },
2108
2268
  { key: "channel", label: "Channel/User", type: "string" /* String */, validation: { required: true } },
2109
- { key: "message", label: "Message", type: "string" /* String */, validation: { required: true } },
2269
+ { key: "text", label: "Message", type: "string" /* String */, validation: { required: true } },
2110
2270
  { key: "blocks", label: "Blocks (JSON)", type: "json" /* JSON */, validation: { required: false } }
2111
2271
  ]
2112
2272
  },
@@ -3330,6 +3490,9 @@ function buildPathOptions(upstreamNodes) {
3330
3490
  var RESERVED_KEYS = new Set(["_lineage", "_meta", "_redacted", "_error"]);
3331
3491
  var KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
3332
3492
  var VariablesConfig = {
3493
+ schema: {
3494
+ fields: { type: "array", required: true }
3495
+ },
3333
3496
  defaults: {
3334
3497
  fields: []
3335
3498
  },
@@ -3380,6 +3543,18 @@ function fieldToJsonSchema(f) {
3380
3543
  }
3381
3544
  // src/catalog/utility/debug/config.ts
3382
3545
  var DebugConfig = {
3546
+ schema: {
3547
+ mode: { type: "select", required: true },
3548
+ label: { type: "string", required: false },
3549
+ pinned: { type: "boolean", required: false },
3550
+ condition: { type: "expression", required: false },
3551
+ sampling: { type: "object", required: false },
3552
+ log: { type: "object", required: false },
3553
+ snapshot: { type: "object", required: false },
3554
+ watch: { type: "object", required: false },
3555
+ assert: { type: "object", required: false },
3556
+ profile: { type: "object", required: false }
3557
+ },
3383
3558
  defaults: {
3384
3559
  mode: "log",
3385
3560
  label: "",
@@ -3565,6 +3740,7 @@ var TransformConfig = {
3565
3740
  sheetColumns: ""
3566
3741
  },
3567
3742
  schema: {
3743
+ name: { type: "string", required: false },
3568
3744
  operation: { type: "select", required: true },
3569
3745
  connectorId: { type: "connection", required: true },
3570
3746
  database: { type: "text", required: false },
@@ -4102,6 +4278,665 @@ function validateFlowData(flow) {
4102
4278
  const f = flow;
4103
4279
  return typeof f.id === "string" && typeof f.name === "string" && f.workflow !== undefined && typeof f.workflow === "object" && f.workflow !== null && Array.isArray(f.workflow.steps);
4104
4280
  }
4281
+ // src/catalog/actions/console-log/config.ts
4282
+ var ConsoleLogConfig = {
4283
+ defaults: {
4284
+ message: "",
4285
+ level: "info",
4286
+ data: undefined,
4287
+ includeInputData: true
4288
+ },
4289
+ schema: {
4290
+ includeInputData: { type: "boolean", required: false },
4291
+ message: {
4292
+ type: "string",
4293
+ required: true,
4294
+ description: "Message to log"
4295
+ },
4296
+ level: {
4297
+ type: "select",
4298
+ required: false,
4299
+ description: "Log level for the message"
4300
+ },
4301
+ data: {
4302
+ type: "json",
4303
+ required: false,
4304
+ description: "Additional data to log"
4305
+ }
4306
+ },
4307
+ validate: (config) => {
4308
+ const errors = [];
4309
+ if (!config.message) {
4310
+ errors.push("Message is required");
4311
+ } else if (typeof config.message !== "string") {
4312
+ errors.push("Message must be a string");
4313
+ }
4314
+ if (config.level && !["info", "warning", "error", "debug"].includes(config.level)) {
4315
+ errors.push("Invalid log level");
4316
+ }
4317
+ return {
4318
+ valid: errors.length === 0,
4319
+ errors
4320
+ };
4321
+ },
4322
+ merge: (userConfig) => {
4323
+ return {
4324
+ ...ConsoleLogConfig.defaults,
4325
+ ...userConfig
4326
+ };
4327
+ },
4328
+ getLabel: (config) => {
4329
+ return `Log [${config.level}]: ${config.message}`;
4330
+ }
4331
+ };
4332
+
4333
+ // src/catalog/actions/delay/config.ts
4334
+ var DelayConfig = {
4335
+ defaults: {
4336
+ duration: 1000,
4337
+ unit: "milliseconds"
4338
+ },
4339
+ schema: {
4340
+ duration: {
4341
+ type: "number",
4342
+ required: true,
4343
+ description: "Duration to delay",
4344
+ min: 0
4345
+ },
4346
+ unit: {
4347
+ type: "select",
4348
+ required: false,
4349
+ description: "Time unit for the duration"
4350
+ }
4351
+ },
4352
+ validate: (config) => {
4353
+ const errors = [];
4354
+ if (config.duration === undefined || config.duration === null) {
4355
+ errors.push("Duration is required");
4356
+ } else if (typeof config.duration !== "number") {
4357
+ errors.push("Duration must be a number");
4358
+ } else if (config.duration < 0) {
4359
+ errors.push("Duration must be non-negative");
4360
+ }
4361
+ if (config.unit && !["milliseconds", "seconds", "minutes", "hours"].includes(config.unit)) {
4362
+ errors.push("Invalid time unit");
4363
+ }
4364
+ return {
4365
+ valid: errors.length === 0,
4366
+ errors
4367
+ };
4368
+ },
4369
+ toMilliseconds: (duration, unit = "milliseconds") => {
4370
+ const multipliers = {
4371
+ milliseconds: 1,
4372
+ seconds: 1000,
4373
+ minutes: 60000,
4374
+ hours: 3600000
4375
+ };
4376
+ return duration * (multipliers[unit] || 1);
4377
+ },
4378
+ merge: (userConfig) => {
4379
+ return {
4380
+ ...DelayConfig.defaults,
4381
+ ...userConfig
4382
+ };
4383
+ },
4384
+ getLabel: (config) => {
4385
+ return `Delay ${config.duration}${config.unit ? " " + config.unit : "ms"}`;
4386
+ }
4387
+ };
4388
+
4389
+ // src/catalog/actions/http-request/config.ts
4390
+ var HTTPRequestConfig = {
4391
+ defaults: {
4392
+ apiType: "rest",
4393
+ method: "GET",
4394
+ url: "",
4395
+ queryParams: [],
4396
+ headers: [],
4397
+ bodyType: "json",
4398
+ body: undefined,
4399
+ formData: [],
4400
+ graphqlQuery: "",
4401
+ graphqlVariables: {},
4402
+ auth: { type: "none" },
4403
+ timeout: 30000,
4404
+ failOnError: true,
4405
+ followRedirects: true,
4406
+ retry: { maxAttempts: 1, retryOn: [429, 500, 502, 503, 504], initialDelayMs: 1000, backoffFactor: 2, maxDelayMs: 30000 },
4407
+ pagination: { type: "none" },
4408
+ extract: {},
4409
+ responseType: "json",
4410
+ expectedContentTypes: []
4411
+ },
4412
+ schema: {
4413
+ apiType: { type: "select", required: false },
4414
+ baseUrl: { type: "string", required: false },
4415
+ queryParams: { type: "array", required: false },
4416
+ bodyType: { type: "select", required: false },
4417
+ formData: { type: "array", required: false },
4418
+ graphqlQuery: { type: "code", required: false },
4419
+ graphqlVariables: { type: "json", required: false },
4420
+ auth: { type: "object", required: false },
4421
+ failOnError: { type: "boolean", required: false },
4422
+ followRedirects: { type: "boolean", required: false },
4423
+ retry: { type: "object", required: false },
4424
+ pagination: { type: "object", required: false },
4425
+ extract: { type: "object", required: false },
4426
+ responseType: { type: "select", required: false },
4427
+ expectedContentTypes: { type: "array", required: false },
4428
+ method: {
4429
+ type: "select",
4430
+ required: true,
4431
+ description: "HTTP method to use"
4432
+ },
4433
+ url: {
4434
+ type: "url",
4435
+ required: true,
4436
+ description: "The URL to send the request to"
4437
+ },
4438
+ headers: {
4439
+ type: "json",
4440
+ required: false,
4441
+ description: "HTTP headers to send with the request"
4442
+ },
4443
+ body: {
4444
+ type: "json",
4445
+ required: false,
4446
+ description: "Request body (for POST, PUT, PATCH)"
4447
+ },
4448
+ timeout: {
4449
+ type: "number",
4450
+ required: false,
4451
+ description: "Request timeout in milliseconds",
4452
+ min: 1000
4453
+ }
4454
+ },
4455
+ validate: (config) => {
4456
+ const errors = [];
4457
+ const apiType = config.apiType ?? "rest";
4458
+ if (apiType === "rest" && !config.method) {
4459
+ errors.push("Method is required");
4460
+ }
4461
+ if (apiType === "graphql" && !config.graphqlQuery?.trim()) {
4462
+ errors.push("GraphQL query is required");
4463
+ }
4464
+ if (!config.url) {
4465
+ errors.push("URL is required");
4466
+ } else if (typeof config.url === "string") {
4467
+ try {
4468
+ new URL(config.url);
4469
+ } catch {
4470
+ errors.push("Invalid URL format");
4471
+ }
4472
+ }
4473
+ if (config.timeout !== undefined && typeof config.timeout === "number") {
4474
+ if (config.timeout < 1000) {
4475
+ errors.push("Timeout must be at least 1000ms");
4476
+ }
4477
+ }
4478
+ return {
4479
+ valid: errors.length === 0,
4480
+ errors
4481
+ };
4482
+ },
4483
+ merge: (userConfig) => {
4484
+ return {
4485
+ ...HTTPRequestConfig.defaults,
4486
+ ...userConfig
4487
+ };
4488
+ },
4489
+ getLabel: (config) => {
4490
+ const type = config.apiType === "graphql" ? "GraphQL" : config.method ?? "GET";
4491
+ return `${type} ${config.url}`;
4492
+ }
4493
+ };
4494
+
4495
+ // src/catalog/api/graphql-query/config.ts
4496
+ var GraphQLQueryConfig = {
4497
+ defaults: {
4498
+ endpoint: "",
4499
+ query: "",
4500
+ variables: {},
4501
+ headers: {}
4502
+ },
4503
+ schema: {
4504
+ endpoint: { type: "url", required: true },
4505
+ query: { type: "code", required: true },
4506
+ variables: { type: "json", required: false },
4507
+ headers: { type: "json", required: false }
4508
+ },
4509
+ validate: (config) => {
4510
+ const errors = [];
4511
+ if (!config.endpoint)
4512
+ errors.push("Endpoint is required");
4513
+ if (!config.query)
4514
+ errors.push("Query is required");
4515
+ return { valid: errors.length === 0, errors };
4516
+ },
4517
+ merge: (userConfig) => {
4518
+ return { ...GraphQLQueryConfig.defaults, ...userConfig };
4519
+ },
4520
+ getLabel: (config) => {
4521
+ return `GraphQL: ${config.endpoint}`;
4522
+ }
4523
+ };
4524
+
4525
+ // src/catalog/communication/google-chat/config.ts
4526
+ var GoogleChatConfig = {
4527
+ defaults: { connection: "", space: "", spaceName: "", text: "", threadKey: "" },
4528
+ schema: {
4529
+ connection: { type: "connection", required: true },
4530
+ space: { type: "string", required: true },
4531
+ text: { type: "string", required: false },
4532
+ threadKey: { type: "string", required: false }
4533
+ },
4534
+ validate: (config) => {
4535
+ const errors = [];
4536
+ if (!config.connection)
4537
+ errors.push("Connect a Google Chat account");
4538
+ if (!config.space)
4539
+ errors.push("Pick a space");
4540
+ if (!config.text && !config.message)
4541
+ errors.push("Add a message");
4542
+ return { valid: errors.length === 0, errors };
4543
+ },
4544
+ merge: (userConfig) => ({
4545
+ ...GoogleChatConfig.defaults,
4546
+ ...userConfig
4547
+ }),
4548
+ getLabel: () => "Google Chat"
4549
+ };
4550
+
4551
+ // src/catalog/communication/slack-message/config.ts
4552
+ var SlackMessageConfig = {
4553
+ defaults: { connection: "", messageType: "channel", channel: "", channelName: "", text: "", blocks: undefined },
4554
+ schema: {
4555
+ connection: { type: "connection", required: true },
4556
+ messageType: { type: "string", required: false },
4557
+ channel: { type: "string", required: true },
4558
+ text: { type: "string", required: false },
4559
+ blocks: { type: "json", required: false }
4560
+ },
4561
+ validate: (config) => {
4562
+ const errors = [];
4563
+ if (!config.connection)
4564
+ errors.push("Slack workspace connection is required");
4565
+ if (!config.channel)
4566
+ errors.push(config.messageType === "user" ? "A user is required" : "A channel is required");
4567
+ const hasText = Boolean(config.text || config.message);
4568
+ const hasBlocks = Array.isArray(config.blocks) && config.blocks.length > 0;
4569
+ if (!hasText && !hasBlocks)
4570
+ errors.push("Add a message or at least one block");
4571
+ return { valid: errors.length === 0, errors };
4572
+ },
4573
+ merge: (userConfig) => {
4574
+ return { ...SlackMessageConfig.defaults, ...userConfig };
4575
+ },
4576
+ getLabel: (config) => {
4577
+ const prefix = config.messageType === "user" ? "@" : "#";
4578
+ return config.channelName ? `Slack: ${prefix}${config.channelName}` : config.channel ? `Slack: ${config.channel}` : "Slack";
4579
+ }
4580
+ };
4581
+
4582
+ // src/catalog/communication/teams-message/config.ts
4583
+ var TeamsMessageConfig = {
4584
+ defaults: { connection: "", teamId: "", teamName: "", channelId: "", channelName: "", title: "", message: "" },
4585
+ schema: {
4586
+ connection: { type: "connection", required: true },
4587
+ teamId: { type: "string", required: true },
4588
+ channelId: { type: "string", required: true },
4589
+ title: { type: "string", required: false },
4590
+ message: { type: "string", required: true }
4591
+ },
4592
+ validate: (config) => {
4593
+ const errors = [];
4594
+ if (!config.connection)
4595
+ errors.push("Connect a Microsoft Teams account");
4596
+ if (!config.teamId)
4597
+ errors.push("Pick a team");
4598
+ if (!config.channelId)
4599
+ errors.push("Pick a channel");
4600
+ if (!config.message)
4601
+ errors.push("Add a message");
4602
+ return { valid: errors.length === 0, errors };
4603
+ },
4604
+ merge: (userConfig) => {
4605
+ return { ...TeamsMessageConfig.defaults, ...userConfig };
4606
+ },
4607
+ getLabel: (config) => config.channelName ? `Teams: #${config.channelName}` : "Teams"
4608
+ };
4609
+
4610
+ // src/catalog/communication/webhook-call/config.ts
4611
+ var WebhookCallConfig = {
4612
+ defaults: {
4613
+ url: "",
4614
+ method: "POST",
4615
+ payload: {},
4616
+ headers: {}
4617
+ },
4618
+ schema: {
4619
+ url: { type: "url", required: true },
4620
+ method: { type: "select", required: true },
4621
+ payload: { type: "json", required: false },
4622
+ headers: { type: "json", required: false }
4623
+ },
4624
+ validate: (config) => {
4625
+ const errors = [];
4626
+ if (!config.url)
4627
+ errors.push("Webhook URL is required");
4628
+ if (!config.method)
4629
+ errors.push("HTTP method is required");
4630
+ return { valid: errors.length === 0, errors };
4631
+ },
4632
+ merge: (userConfig) => {
4633
+ return { ...WebhookCallConfig.defaults, ...userConfig };
4634
+ },
4635
+ getLabel: (config) => {
4636
+ return `${config.method}: ${config.url.substring(0, 40)}...`;
4637
+ }
4638
+ };
4639
+
4640
+ // src/catalog/communication/whatsapp-send/config.ts
4641
+ var WhatsappSendConfig = {
4642
+ schema: {
4643
+ connection: { type: "connection", required: true },
4644
+ to: { type: "string", required: true },
4645
+ mode: { type: "select", required: true },
4646
+ templateName: { type: "string", required: false },
4647
+ templateLanguage: { type: "string", required: false },
4648
+ templateVariables: { type: "array", required: false },
4649
+ text: { type: "string", required: false },
4650
+ previewUrl: { type: "boolean", required: false }
4651
+ },
4652
+ defaults: {
4653
+ connection: "",
4654
+ to: "",
4655
+ mode: "template",
4656
+ templateLanguage: "en_US",
4657
+ templateVariables: []
4658
+ },
4659
+ validate: (config) => {
4660
+ const errors = [];
4661
+ if (!config.connection)
4662
+ errors.push("Select a WhatsApp connection");
4663
+ if (!config.to?.trim())
4664
+ errors.push("Recipient phone number is required");
4665
+ if (config.mode === "text") {
4666
+ if (!config.text?.trim())
4667
+ errors.push("Message text is required");
4668
+ } else {
4669
+ if (!config.templateName?.trim())
4670
+ errors.push("Choose an approved template");
4671
+ }
4672
+ return { valid: errors.length === 0, errors };
4673
+ },
4674
+ merge: (userConfig) => {
4675
+ return { ...WhatsappSendConfig.defaults, ...userConfig };
4676
+ },
4677
+ getLabel: (config) => config.mode === "text" ? `WhatsApp to ${config.to || "—"}` : `WhatsApp template "${config.templateName || "—"}" to ${config.to || "—"}`
4678
+ };
4679
+
4680
+ // src/catalog/storage/file-operation/config.ts
4681
+ var FileOperationConfig = {
4682
+ defaults: {
4683
+ connection: "",
4684
+ operation: "upload",
4685
+ sourcePath: "",
4686
+ destinationPath: "",
4687
+ fileContent: "",
4688
+ options: {}
4689
+ },
4690
+ schema: {
4691
+ connection: { type: "connection", required: true },
4692
+ operation: { type: "select", required: true },
4693
+ sourcePath: { type: "string", required: false },
4694
+ destinationPath: { type: "string", required: true },
4695
+ fileContent: { type: "string", required: false },
4696
+ options: { type: "json", required: false }
4697
+ },
4698
+ validate: (config) => {
4699
+ const errors = [];
4700
+ if (!config.connection)
4701
+ errors.push("Storage connection is required");
4702
+ if (!config.operation)
4703
+ errors.push("Operation is required");
4704
+ if (!config.destinationPath)
4705
+ errors.push("Destination path is required");
4706
+ if (config.operation === "upload" || config.operation === "copy" || config.operation === "move") {
4707
+ if (!config.sourcePath) {
4708
+ errors.push("Source path is required for upload/copy/move operations");
4709
+ }
4710
+ }
4711
+ return { valid: errors.length === 0, errors };
4712
+ },
4713
+ merge: (userConfig) => {
4714
+ return { ...FileOperationConfig.defaults, ...userConfig };
4715
+ },
4716
+ getLabel: (config) => {
4717
+ return `${config.operation.toUpperCase()}: ${config.destinationPath}`;
4718
+ }
4719
+ };
4720
+
4721
+ // src/catalog/transforms/format/config.ts
4722
+ var FormatConfig = {
4723
+ defaults: { input: "", format: "json" },
4724
+ schema: {
4725
+ input: { type: "expression", required: true },
4726
+ format: { type: "select", required: true }
4727
+ },
4728
+ validate: (config) => {
4729
+ const errors = [];
4730
+ if (!config.input)
4731
+ errors.push("Input required");
4732
+ if (!config.format)
4733
+ errors.push("Format required");
4734
+ return { valid: errors.length === 0, errors };
4735
+ },
4736
+ merge: (userConfig) => {
4737
+ return { ...FormatConfig.defaults, ...userConfig };
4738
+ },
4739
+ getLabel: (config) => `Format as ${config.format.toUpperCase()}`
4740
+ };
4741
+
4742
+ // src/catalog/triggers/schedule-trigger/config.ts
4743
+ var ScheduleTriggerConfig = {
4744
+ defaults: {
4745
+ scheduleType: "interval",
4746
+ interval: 60,
4747
+ intervalUnit: "minutes",
4748
+ timezone: "UTC"
4749
+ },
4750
+ schema: {
4751
+ scheduleType: { type: "select", required: true },
4752
+ interval: { type: "number", required: false, min: 1 },
4753
+ intervalUnit: { type: "select", required: false },
4754
+ cronExpression: { type: "string", required: false },
4755
+ timezone: { type: "string", required: false }
4756
+ },
4757
+ validate: (config) => {
4758
+ const errors = [];
4759
+ if (!config.scheduleType) {
4760
+ errors.push("Schedule type is required");
4761
+ }
4762
+ if (config.scheduleType === "interval") {
4763
+ if (!config.interval || config.interval < 1) {
4764
+ errors.push("Interval must be at least 1");
4765
+ }
4766
+ }
4767
+ if (config.scheduleType === "cron" && !config.cronExpression) {
4768
+ errors.push("Cron expression is required");
4769
+ }
4770
+ return { valid: errors.length === 0, errors };
4771
+ },
4772
+ merge: (userConfig) => {
4773
+ return { ...ScheduleTriggerConfig.defaults, ...userConfig };
4774
+ },
4775
+ getLabel: (config) => {
4776
+ if (config.scheduleType === "interval") {
4777
+ return `Every ${config.interval} ${config.intervalUnit}`;
4778
+ }
4779
+ return `Schedule: ${config.scheduleType}`;
4780
+ }
4781
+ };
4782
+
4783
+ // src/catalog/triggers/webhook-trigger/config.ts
4784
+ var WebhookTriggerConfig = {
4785
+ defaults: {
4786
+ method: "POST",
4787
+ requireAuth: true
4788
+ },
4789
+ schema: {
4790
+ webhookPath: { type: "string", required: false },
4791
+ method: { type: "select", required: true },
4792
+ requireAuth: { type: "boolean", required: false }
4793
+ },
4794
+ validate: (config) => {
4795
+ const errors = [];
4796
+ if (!config.method) {
4797
+ errors.push("HTTP method is required");
4798
+ }
4799
+ return { valid: errors.length === 0, errors };
4800
+ },
4801
+ merge: (userConfig) => {
4802
+ return { ...WebhookTriggerConfig.defaults, ...userConfig };
4803
+ },
4804
+ getLabel: (config) => {
4805
+ return `Webhook ${config.method}${config.webhookPath ? ` ${config.webhookPath}` : ""}`;
4806
+ }
4807
+ };
4808
+
4809
+ // src/catalog/utility/error/config.ts
4810
+ var ErrorConfig = {
4811
+ defaults: {
4812
+ message: "",
4813
+ code: undefined
4814
+ },
4815
+ schema: {
4816
+ message: { type: "string", required: true },
4817
+ code: { type: "string", required: false }
4818
+ },
4819
+ validate: (config) => {
4820
+ const errors = [];
4821
+ if (!config.message)
4822
+ errors.push("Error message is required");
4823
+ return { valid: errors.length === 0, errors };
4824
+ },
4825
+ merge: (userConfig) => {
4826
+ return { ...ErrorConfig.defaults, ...userConfig };
4827
+ },
4828
+ getLabel: (config) => {
4829
+ return `Error: ${config.message}`;
4830
+ }
4831
+ };
4832
+
4833
+ // src/rules/configContracts.ts
4834
+ var asValidator = (fn) => (config) => fn(config);
4835
+ var CONFIG_CONTRACTS = {
4836
+ console_log: { schema: ConsoleLogConfig.schema, validate: asValidator(ConsoleLogConfig.validate) },
4837
+ delay: { schema: DelayConfig.schema, validate: asValidator(DelayConfig.validate) },
4838
+ http_request: { schema: HTTPRequestConfig.schema, validate: asValidator(HTTPRequestConfig.validate) },
4839
+ graphql_query: { schema: GraphQLQueryConfig.schema, validate: asValidator(GraphQLQueryConfig.validate) },
4840
+ email: { schema: EmailConfig.schema, validate: asValidator(EmailConfig.validate) },
4841
+ google_chat: { schema: GoogleChatConfig.schema, validate: asValidator(GoogleChatConfig.validate) },
4842
+ slack_message: { schema: SlackMessageConfig.schema, validate: asValidator(SlackMessageConfig.validate) },
4843
+ teams_message: { schema: TeamsMessageConfig.schema, validate: asValidator(TeamsMessageConfig.validate) },
4844
+ webhook_call: { schema: WebhookCallConfig.schema, validate: asValidator(WebhookCallConfig.validate) },
4845
+ whatsapp_send: { schema: WhatsappSendConfig.schema, validate: asValidator(WhatsappSendConfig.validate) },
4846
+ query: {
4847
+ schema: BuildQueryContractSchema,
4848
+ validate: (config) => {
4849
+ const r = validateBuildQueryConfig(config);
4850
+ const errors = (r.errors ?? []).filter((e) => e.severity !== "warning").map((e) => e.message);
4851
+ return { valid: errors.length === 0, errors };
4852
+ }
4853
+ },
4854
+ transform: { schema: TransformConfig.schema, validate: asValidator(TransformConfig.validate) },
4855
+ condition: { schema: ConditionConfig.schema, validate: asValidator(ConditionConfig.validate) },
4856
+ for_each: { schema: ForEachConfig.schema, validate: asValidator(ForEachConfig.validate) },
4857
+ file_operation: { schema: FileOperationConfig.schema, validate: asValidator(FileOperationConfig.validate) },
4858
+ format: { schema: FormatConfig.schema, validate: asValidator(FormatConfig.validate) },
4859
+ manual_trigger: { schema: ManualTriggerConfig.schema, validate: asValidator(ManualTriggerConfig.validate) },
4860
+ schedule_trigger: { schema: ScheduleTriggerConfig.schema, validate: asValidator(ScheduleTriggerConfig.validate) },
4861
+ webhook_trigger: { schema: WebhookTriggerConfig.schema, validate: asValidator(WebhookTriggerConfig.validate) },
4862
+ debug: { schema: DebugConfig.schema, validate: asValidator(DebugConfig.validate) },
4863
+ error: { schema: ErrorConfig.schema, validate: asValidator(ErrorConfig.validate) },
4864
+ variables: { schema: VariablesConfig.schema, validate: asValidator(VariablesConfig.validate) }
4865
+ };
4866
+ function contractFor(actionId) {
4867
+ return actionId ? CONFIG_CONTRACTS[actionId] : undefined;
4868
+ }
4869
+ function declaredKeys(actionId) {
4870
+ const c = contractFor(actionId);
4871
+ if (!c)
4872
+ return [];
4873
+ return Object.entries(c.schema).filter(([, f]) => f.type !== "none").map(([k]) => k);
4874
+ }
4875
+ function isEditorPrivateKey(key) {
4876
+ return key.startsWith("_");
4877
+ }
4878
+ var CONTRACT_TYPE_TO_FIELD_TYPE = {
4879
+ string: "string" /* String */,
4880
+ text: "string" /* String */,
4881
+ select: "select" /* Select */,
4882
+ json: "json" /* JSON */,
4883
+ connection: "connection" /* Connection */,
4884
+ number: "number" /* Number */,
4885
+ boolean: "boolean" /* Boolean */,
4886
+ array: "array" /* Array */,
4887
+ object: "object" /* Object */,
4888
+ url: "url" /* URL */,
4889
+ expression: "expression" /* Expression */,
4890
+ code: "code" /* Code */
4891
+ };
4892
+ function humanise(key) {
4893
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/^\w/, (c) => c.toUpperCase());
4894
+ }
4895
+ function contractFieldsFor(definition) {
4896
+ const declared = definition.inputSchema?.fields ?? [];
4897
+ const contract = contractFor(definition.id);
4898
+ if (!contract)
4899
+ return declared;
4900
+ const seen = new Set(declared.map((f) => f.key));
4901
+ const derived = [];
4902
+ for (const [key, field] of Object.entries(contract.schema)) {
4903
+ if (seen.has(key) || field.type === "none")
4904
+ continue;
4905
+ derived.push({
4906
+ key,
4907
+ label: humanise(key),
4908
+ type: CONTRACT_TYPE_TO_FIELD_TYPE[field.type] ?? "string" /* String */,
4909
+ validation: { required: field.required === true },
4910
+ ...field.options ? { options: field.options.map((v) => ({ label: String(v), value: v })) } : {}
4911
+ });
4912
+ }
4913
+ return [...declared, ...derived];
4914
+ }
4915
+ function contractDrift(definition) {
4916
+ const contract = contractFor(definition.id);
4917
+ if (!contract)
4918
+ return [];
4919
+ const covers = (key) => {
4920
+ if (key in contract.schema)
4921
+ return true;
4922
+ const root = key.split(".")[0];
4923
+ const rootField = contract.schema[root];
4924
+ return !!rootField && (rootField.type === "object" || rootField.type === "json");
4925
+ };
4926
+ return (definition.inputSchema?.fields ?? []).map((f) => f.key).filter((k) => !covers(k));
4927
+ }
4928
+ function contractVocabularyDrift(actionId) {
4929
+ const c = contractFor(actionId);
4930
+ if (!c)
4931
+ return [];
4932
+ const known = new Set([...Object.keys(CONTRACT_TYPE_TO_FIELD_TYPE), "none"]);
4933
+ return Object.entries(c.schema).filter(([, f]) => !known.has(f.type)).map(([k, f]) => `${k}:${f.type}`);
4934
+ }
4935
+ function contractDerivedKeys(definition) {
4936
+ const own = new Set((definition.inputSchema?.fields ?? []).map((f) => f.key));
4937
+ return new Set(contractFieldsFor(definition).map((f) => f.key).filter((k) => !own.has(k)));
4938
+ }
4939
+
4105
4940
  // src/rules/configSafety.ts
4106
4941
  var MAX_STEP_CONFIG_BYTES = 256 * 1024;
4107
4942
  var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
@@ -4210,7 +5045,8 @@ function validateStepConfig(step) {
4210
5045
  const definition = typeof step.actionDefinitionId === "string" ? getActionById(step.actionDefinitionId) : undefined;
4211
5046
  if (!definition)
4212
5047
  return issues;
4213
- const declared = new Map(definition.inputSchema.fields.map((f) => [f.key, f]));
5048
+ const declared = new Map(contractFieldsFor(definition).map((f) => [f.key, f]));
5049
+ const derived = contractDerivedKeys(definition);
4214
5050
  const entries = Object.entries(config);
4215
5051
  for (const [key, value] of entries) {
4216
5052
  const field = declared.get(key);
@@ -4226,7 +5062,7 @@ function validateStepConfig(step) {
4226
5062
  }
4227
5063
  if (!typeMatches(field, value)) {
4228
5064
  issues.push({
4229
- severity: "error",
5065
+ severity: derived.has(key) ? "warning" : "error",
4230
5066
  code: "FIELD_WRONG_TYPE",
4231
5067
  stepId,
4232
5068
  field: key,