@octaviaflow/flow-rules 0.6.0 → 0.8.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/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,
@@ -53,6 +54,8 @@ __export(exports_src, {
53
54
  ForEachConfig: () => ForEachConfig,
54
55
  JoinType: () => JoinType,
55
56
  LogicalOperator: () => LogicalOperator,
57
+ MAPPING_FUNCTIONS: () => MAPPING_FUNCTIONS,
58
+ MAPPING_FUNCTION_SPECS: () => MAPPING_FUNCTION_SPECS,
56
59
  MAX_STEP_CONFIG_BYTES: () => MAX_STEP_CONFIG_BYTES,
57
60
  ManualTriggerConfig: () => ManualTriggerConfig,
58
61
  NodeStatus: () => NodeStatus,
@@ -62,12 +65,18 @@ __export(exports_src, {
62
65
  SortDirection: () => SortDirection,
63
66
  TRANSFORM_OPERATIONS: () => TRANSFORM_OPERATIONS,
64
67
  TransformConfig: () => TransformConfig,
68
+ USER_FUNCTION_PREFIX: () => USER_FUNCTION_PREFIX,
65
69
  VariablesConfig: () => VariablesConfig,
66
70
  actionsRegistry: () => actionsRegistry,
67
71
  buildPathOptions: () => buildPathOptions,
68
72
  collectArrayPaths: () => collectArrayPaths,
69
73
  collectArrayPathsFromSample: () => collectArrayPathsFromSample,
70
74
  configErrors: () => configErrors,
75
+ contractDrift: () => contractDrift,
76
+ contractFieldsFor: () => contractFieldsFor,
77
+ contractFor: () => contractFor,
78
+ contractVocabularyDrift: () => contractVocabularyDrift,
79
+ declaredKeys: () => declaredKeys,
71
80
  emailDefinition: () => emailDefinition,
72
81
  flattenNestedWorkflow: () => flattenNestedWorkflow,
73
82
  generateConnectionsFromSteps: () => generateConnectionsFromSteps,
@@ -83,7 +92,10 @@ __export(exports_src, {
83
92
  getNodeValidationStatus: () => getNodeValidationStatus,
84
93
  getStorageProvider: () => getStorageProvider,
85
94
  inferSchemaFromSample: () => inferSchemaFromSample,
95
+ isEditorPrivateKey: () => isEditorPrivateKey,
96
+ isMappingFunction: () => isMappingFunction,
86
97
  isQueryableConnector: () => isQueryableConnector,
98
+ mappingTransformFunctionName: () => mappingTransformFunctionName,
87
99
  migrateDelayConfig: () => migrateDelayConfig,
88
100
  queryDefinition: () => queryDefinition,
89
101
  storageConfigKeys: () => storageConfigKeys,
@@ -1187,6 +1199,149 @@ var BuildQueryConfigDefaults = {
1187
1199
  parameters: {}
1188
1200
  }
1189
1201
  };
1202
+ var validateBuildQueryConfig = (config) => {
1203
+ const errors = [];
1204
+ const warnings = [];
1205
+ if (!config.connectorId) {
1206
+ errors.push({
1207
+ field: "connectorId",
1208
+ message: "Please select a connector",
1209
+ severity: "error"
1210
+ });
1211
+ }
1212
+ if (!config.mode) {
1213
+ errors.push({
1214
+ field: "mode",
1215
+ message: "Query mode is required",
1216
+ severity: "error"
1217
+ });
1218
+ }
1219
+ if (!config.queryType) {
1220
+ errors.push({
1221
+ field: "queryType",
1222
+ message: "Query type is required",
1223
+ severity: "error"
1224
+ });
1225
+ }
1226
+ if (config.mode === "visual" /* Visual */) {
1227
+ const visualErrors = validateVisualQueryConfig(config.visualConfig);
1228
+ errors.push(...visualErrors);
1229
+ } else if (config.mode === "sql" /* SQL */) {
1230
+ const sqlErrors = validateSQLQueryConfig(config.sqlConfig);
1231
+ errors.push(...sqlErrors);
1232
+ }
1233
+ if (config.timeout !== undefined) {
1234
+ if (config.timeout < 1000) {
1235
+ errors.push({
1236
+ field: "timeout",
1237
+ message: "Timeout must be at least 1000ms",
1238
+ severity: "error"
1239
+ });
1240
+ } else if (config.timeout > 300000) {
1241
+ warnings.push({
1242
+ field: "timeout",
1243
+ message: "Timeout is very high (>5 minutes)",
1244
+ severity: "warning"
1245
+ });
1246
+ }
1247
+ }
1248
+ if (config.maxRows !== undefined) {
1249
+ if (config.maxRows < 1) {
1250
+ errors.push({
1251
+ field: "maxRows",
1252
+ message: "Max rows must be at least 1",
1253
+ severity: "error"
1254
+ });
1255
+ } else if (config.maxRows > 1e4) {
1256
+ warnings.push({
1257
+ field: "maxRows",
1258
+ message: "Max rows is very high (>10,000). This may impact performance.",
1259
+ severity: "warning"
1260
+ });
1261
+ }
1262
+ }
1263
+ return {
1264
+ valid: errors.length === 0,
1265
+ errors,
1266
+ warnings
1267
+ };
1268
+ };
1269
+ var validateVisualQueryConfig = (config) => {
1270
+ const errors = [];
1271
+ if (!config) {
1272
+ errors.push({
1273
+ field: "visualConfig",
1274
+ message: "Visual query configuration is required",
1275
+ severity: "error"
1276
+ });
1277
+ return errors;
1278
+ }
1279
+ if (!config.tables || config.tables.length === 0) {
1280
+ errors.push({
1281
+ field: "visualConfig.tables",
1282
+ message: "At least one table must be selected",
1283
+ severity: "error"
1284
+ });
1285
+ }
1286
+ if (!config.fields || config.fields.length === 0) {
1287
+ errors.push({
1288
+ field: "visualConfig.fields",
1289
+ message: "At least one field must be selected",
1290
+ severity: "error"
1291
+ });
1292
+ }
1293
+ if (config.tables && config.tables.length > 1) {
1294
+ if (!config.joins || config.joins.length === 0) {
1295
+ errors.push({
1296
+ field: "visualConfig.joins",
1297
+ message: "Joins are required when selecting from multiple tables",
1298
+ severity: "error"
1299
+ });
1300
+ }
1301
+ }
1302
+ const hasAggregates = config.fields?.some((field) => field.aggregate);
1303
+ const hasNonAggregates = config.fields?.some((field) => !field.aggregate);
1304
+ if (hasAggregates && hasNonAggregates) {
1305
+ if (!config.groupBy || config.groupBy.length === 0) {
1306
+ errors.push({
1307
+ field: "visualConfig.groupBy",
1308
+ message: "Group by is required when mixing aggregate and non-aggregate fields",
1309
+ severity: "error"
1310
+ });
1311
+ }
1312
+ }
1313
+ return errors;
1314
+ };
1315
+ var validateSQLQueryConfig = (config) => {
1316
+ const errors = [];
1317
+ if (!config) {
1318
+ errors.push({
1319
+ field: "sqlConfig",
1320
+ message: "SQL query configuration is required",
1321
+ severity: "error"
1322
+ });
1323
+ return errors;
1324
+ }
1325
+ if (!config.sql || config.sql.trim().length === 0) {
1326
+ errors.push({
1327
+ field: "sqlConfig.sql",
1328
+ message: "SQL query is required",
1329
+ severity: "error"
1330
+ });
1331
+ } else {
1332
+ const sql = config.sql.trim().toLowerCase();
1333
+ const dangerousKeywords = ["drop", "truncate", "alter", "create", "grant", "revoke"];
1334
+ const hasDangerousKeyword = dangerousKeywords.some((keyword) => sql.includes(keyword + " "));
1335
+ if (hasDangerousKeyword) {
1336
+ errors.push({
1337
+ field: "sqlConfig.sql",
1338
+ message: "SQL query contains potentially dangerous operations (DROP, TRUNCATE, ALTER, etc.)",
1339
+ severity: "error"
1340
+ });
1341
+ }
1342
+ }
1343
+ return errors;
1344
+ };
1190
1345
  function isQueryableConnector(connector) {
1191
1346
  if (connector.isQueryable !== undefined) {
1192
1347
  return connector.isQueryable;
@@ -1212,6 +1367,15 @@ function isQueryableConnector(connector) {
1212
1367
  ];
1213
1368
  return !storageTypes.includes(connector.type.toLowerCase());
1214
1369
  }
1370
+ var BuildQueryContractSchema = {
1371
+ connectorId: { type: "connection", required: true },
1372
+ mode: { type: "select", required: true },
1373
+ queryType: { type: "select", required: true },
1374
+ visualConfig: { type: "object", required: false },
1375
+ sqlConfig: { type: "object", required: false },
1376
+ timeout: { type: "number", required: false },
1377
+ maxRows: { type: "number", required: false }
1378
+ };
1215
1379
 
1216
1380
  // src/catalog/general/query/index.ts
1217
1381
  var queryDefinition = {
@@ -2101,7 +2265,7 @@ var slackMessageDefinition = {
2101
2265
  fields: [
2102
2266
  { key: "connection", label: "Slack Workspace", type: "connection" /* Connection */, validation: { required: true } },
2103
2267
  { key: "channel", label: "Channel/User", type: "string" /* String */, validation: { required: true } },
2104
- { key: "message", label: "Message", type: "string" /* String */, validation: { required: true } },
2268
+ { key: "text", label: "Message", type: "string" /* String */, validation: { required: true } },
2105
2269
  { key: "blocks", label: "Blocks (JSON)", type: "json" /* JSON */, validation: { required: false } }
2106
2270
  ]
2107
2271
  },
@@ -3325,6 +3489,9 @@ function buildPathOptions(upstreamNodes) {
3325
3489
  var RESERVED_KEYS = new Set(["_lineage", "_meta", "_redacted", "_error"]);
3326
3490
  var KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
3327
3491
  var VariablesConfig = {
3492
+ schema: {
3493
+ fields: { type: "array", required: true }
3494
+ },
3328
3495
  defaults: {
3329
3496
  fields: []
3330
3497
  },
@@ -3375,6 +3542,18 @@ function fieldToJsonSchema(f) {
3375
3542
  }
3376
3543
  // src/catalog/utility/debug/config.ts
3377
3544
  var DebugConfig = {
3545
+ schema: {
3546
+ mode: { type: "select", required: true },
3547
+ label: { type: "string", required: false },
3548
+ pinned: { type: "boolean", required: false },
3549
+ condition: { type: "expression", required: false },
3550
+ sampling: { type: "object", required: false },
3551
+ log: { type: "object", required: false },
3552
+ snapshot: { type: "object", required: false },
3553
+ watch: { type: "object", required: false },
3554
+ assert: { type: "object", required: false },
3555
+ profile: { type: "object", required: false }
3556
+ },
3378
3557
  defaults: {
3379
3558
  mode: "log",
3380
3559
  label: "",
@@ -3560,6 +3739,7 @@ var TransformConfig = {
3560
3739
  sheetColumns: ""
3561
3740
  },
3562
3741
  schema: {
3742
+ name: { type: "string", required: false },
3563
3743
  operation: { type: "select", required: true },
3564
3744
  connectorId: { type: "connection", required: true },
3565
3745
  database: { type: "text", required: false },
@@ -4097,6 +4277,661 @@ function validateFlowData(flow) {
4097
4277
  const f = flow;
4098
4278
  return typeof f.id === "string" && typeof f.name === "string" && f.workflow !== undefined && typeof f.workflow === "object" && f.workflow !== null && Array.isArray(f.workflow.steps);
4099
4279
  }
4280
+ // src/catalog/actions/console-log/config.ts
4281
+ var ConsoleLogConfig = {
4282
+ defaults: {
4283
+ message: "",
4284
+ level: "info",
4285
+ data: undefined,
4286
+ includeInputData: true
4287
+ },
4288
+ schema: {
4289
+ includeInputData: { type: "boolean", required: false },
4290
+ message: {
4291
+ type: "string",
4292
+ required: true,
4293
+ description: "Message to log"
4294
+ },
4295
+ level: {
4296
+ type: "select",
4297
+ required: false,
4298
+ description: "Log level for the message"
4299
+ },
4300
+ data: {
4301
+ type: "json",
4302
+ required: false,
4303
+ description: "Additional data to log"
4304
+ }
4305
+ },
4306
+ validate: (config) => {
4307
+ const errors = [];
4308
+ if (!config.message) {
4309
+ errors.push("Message is required");
4310
+ } else if (typeof config.message !== "string") {
4311
+ errors.push("Message must be a string");
4312
+ }
4313
+ if (config.level && !["info", "warning", "error", "debug"].includes(config.level)) {
4314
+ errors.push("Invalid log level");
4315
+ }
4316
+ return {
4317
+ valid: errors.length === 0,
4318
+ errors
4319
+ };
4320
+ },
4321
+ merge: (userConfig) => {
4322
+ return {
4323
+ ...ConsoleLogConfig.defaults,
4324
+ ...userConfig
4325
+ };
4326
+ },
4327
+ getLabel: (config) => {
4328
+ return `Log [${config.level}]: ${config.message}`;
4329
+ }
4330
+ };
4331
+
4332
+ // src/catalog/actions/delay/config.ts
4333
+ var DelayConfig = {
4334
+ defaults: {
4335
+ duration: 1000,
4336
+ unit: "milliseconds"
4337
+ },
4338
+ schema: {
4339
+ duration: {
4340
+ type: "number",
4341
+ required: true,
4342
+ description: "Duration to delay",
4343
+ min: 0
4344
+ },
4345
+ unit: {
4346
+ type: "select",
4347
+ required: false,
4348
+ description: "Time unit for the duration"
4349
+ }
4350
+ },
4351
+ validate: (config) => {
4352
+ const errors = [];
4353
+ if (config.duration === undefined || config.duration === null) {
4354
+ errors.push("Duration is required");
4355
+ } else if (typeof config.duration !== "number") {
4356
+ errors.push("Duration must be a number");
4357
+ } else if (config.duration < 0) {
4358
+ errors.push("Duration must be non-negative");
4359
+ }
4360
+ if (config.unit && !["milliseconds", "seconds", "minutes", "hours"].includes(config.unit)) {
4361
+ errors.push("Invalid time unit");
4362
+ }
4363
+ return {
4364
+ valid: errors.length === 0,
4365
+ errors
4366
+ };
4367
+ },
4368
+ toMilliseconds: (duration, unit = "milliseconds") => {
4369
+ const multipliers = {
4370
+ milliseconds: 1,
4371
+ seconds: 1000,
4372
+ minutes: 60000,
4373
+ hours: 3600000
4374
+ };
4375
+ return duration * (multipliers[unit] || 1);
4376
+ },
4377
+ merge: (userConfig) => {
4378
+ return {
4379
+ ...DelayConfig.defaults,
4380
+ ...userConfig
4381
+ };
4382
+ },
4383
+ getLabel: (config) => {
4384
+ return `Delay ${config.duration}${config.unit ? " " + config.unit : "ms"}`;
4385
+ }
4386
+ };
4387
+
4388
+ // src/catalog/actions/http-request/config.ts
4389
+ var HTTPRequestConfig = {
4390
+ defaults: {
4391
+ apiType: "rest",
4392
+ method: "GET",
4393
+ url: "",
4394
+ queryParams: [],
4395
+ headers: [],
4396
+ bodyType: "json",
4397
+ body: undefined,
4398
+ formData: [],
4399
+ graphqlQuery: "",
4400
+ graphqlVariables: {},
4401
+ auth: { type: "none" },
4402
+ timeout: 30000,
4403
+ failOnError: true,
4404
+ followRedirects: true,
4405
+ retry: { maxAttempts: 1, retryOn: [429, 500, 502, 503, 504], initialDelayMs: 1000, backoffFactor: 2, maxDelayMs: 30000 },
4406
+ pagination: { type: "none" },
4407
+ extract: {},
4408
+ responseType: "json",
4409
+ expectedContentTypes: []
4410
+ },
4411
+ schema: {
4412
+ apiType: { type: "select", required: false },
4413
+ baseUrl: { type: "string", required: false },
4414
+ queryParams: { type: "array", required: false },
4415
+ bodyType: { type: "select", required: false },
4416
+ formData: { type: "array", required: false },
4417
+ graphqlQuery: { type: "code", required: false },
4418
+ graphqlVariables: { type: "json", required: false },
4419
+ auth: { type: "object", required: false },
4420
+ failOnError: { type: "boolean", required: false },
4421
+ followRedirects: { type: "boolean", required: false },
4422
+ retry: { type: "object", required: false },
4423
+ pagination: { type: "object", required: false },
4424
+ extract: { type: "object", required: false },
4425
+ responseType: { type: "select", required: false },
4426
+ expectedContentTypes: { type: "array", required: false },
4427
+ method: {
4428
+ type: "select",
4429
+ required: true,
4430
+ description: "HTTP method to use"
4431
+ },
4432
+ url: {
4433
+ type: "url",
4434
+ required: true,
4435
+ description: "The URL to send the request to"
4436
+ },
4437
+ headers: {
4438
+ type: "json",
4439
+ required: false,
4440
+ description: "HTTP headers to send with the request"
4441
+ },
4442
+ body: {
4443
+ type: "json",
4444
+ required: false,
4445
+ description: "Request body (for POST, PUT, PATCH)"
4446
+ },
4447
+ timeout: {
4448
+ type: "number",
4449
+ required: false,
4450
+ description: "Request timeout in milliseconds",
4451
+ min: 1000
4452
+ }
4453
+ },
4454
+ validate: (config) => {
4455
+ const errors = [];
4456
+ const apiType = config.apiType ?? "rest";
4457
+ if (apiType === "rest" && !config.method) {
4458
+ errors.push("Method is required");
4459
+ }
4460
+ if (apiType === "graphql" && !config.graphqlQuery?.trim()) {
4461
+ errors.push("GraphQL query is required");
4462
+ }
4463
+ if (!config.url) {
4464
+ errors.push("URL is required");
4465
+ } else if (typeof config.url === "string") {
4466
+ try {
4467
+ new URL(config.url);
4468
+ } catch {
4469
+ errors.push("Invalid URL format");
4470
+ }
4471
+ }
4472
+ if (config.timeout !== undefined && typeof config.timeout === "number") {
4473
+ if (config.timeout < 1000) {
4474
+ errors.push("Timeout must be at least 1000ms");
4475
+ }
4476
+ }
4477
+ return {
4478
+ valid: errors.length === 0,
4479
+ errors
4480
+ };
4481
+ },
4482
+ merge: (userConfig) => {
4483
+ return {
4484
+ ...HTTPRequestConfig.defaults,
4485
+ ...userConfig
4486
+ };
4487
+ },
4488
+ getLabel: (config) => {
4489
+ const type = config.apiType === "graphql" ? "GraphQL" : config.method ?? "GET";
4490
+ return `${type} ${config.url}`;
4491
+ }
4492
+ };
4493
+
4494
+ // src/catalog/api/graphql-query/config.ts
4495
+ var GraphQLQueryConfig = {
4496
+ defaults: {
4497
+ endpoint: "",
4498
+ query: "",
4499
+ variables: {},
4500
+ headers: {}
4501
+ },
4502
+ schema: {
4503
+ endpoint: { type: "url", required: true },
4504
+ query: { type: "code", required: true },
4505
+ variables: { type: "json", required: false },
4506
+ headers: { type: "json", required: false }
4507
+ },
4508
+ validate: (config) => {
4509
+ const errors = [];
4510
+ if (!config.endpoint)
4511
+ errors.push("Endpoint is required");
4512
+ if (!config.query)
4513
+ errors.push("Query is required");
4514
+ return { valid: errors.length === 0, errors };
4515
+ },
4516
+ merge: (userConfig) => {
4517
+ return { ...GraphQLQueryConfig.defaults, ...userConfig };
4518
+ },
4519
+ getLabel: (config) => {
4520
+ return `GraphQL: ${config.endpoint}`;
4521
+ }
4522
+ };
4523
+
4524
+ // src/catalog/communication/google-chat/config.ts
4525
+ var GoogleChatConfig = {
4526
+ defaults: { connection: "", space: "", spaceName: "", text: "", threadKey: "" },
4527
+ schema: {
4528
+ connection: { type: "connection", required: true },
4529
+ space: { type: "string", required: true },
4530
+ text: { type: "string", required: false },
4531
+ threadKey: { type: "string", required: false }
4532
+ },
4533
+ validate: (config) => {
4534
+ const errors = [];
4535
+ if (!config.connection)
4536
+ errors.push("Connect a Google Chat account");
4537
+ if (!config.space)
4538
+ errors.push("Pick a space");
4539
+ if (!config.text && !config.message)
4540
+ errors.push("Add a message");
4541
+ return { valid: errors.length === 0, errors };
4542
+ },
4543
+ merge: (userConfig) => ({
4544
+ ...GoogleChatConfig.defaults,
4545
+ ...userConfig
4546
+ }),
4547
+ getLabel: () => "Google Chat"
4548
+ };
4549
+
4550
+ // src/catalog/communication/slack-message/config.ts
4551
+ var SlackMessageConfig = {
4552
+ defaults: { connection: "", messageType: "channel", channel: "", channelName: "", text: "", blocks: undefined },
4553
+ schema: {
4554
+ connection: { type: "connection", required: true },
4555
+ messageType: { type: "string", required: false },
4556
+ channel: { type: "string", required: true },
4557
+ text: { type: "string", required: false },
4558
+ blocks: { type: "json", required: false }
4559
+ },
4560
+ validate: (config) => {
4561
+ const errors = [];
4562
+ if (!config.connection)
4563
+ errors.push("Slack workspace connection is required");
4564
+ if (!config.channel)
4565
+ errors.push(config.messageType === "user" ? "A user is required" : "A channel is required");
4566
+ const hasText = Boolean(config.text || config.message);
4567
+ const hasBlocks = Array.isArray(config.blocks) && config.blocks.length > 0;
4568
+ if (!hasText && !hasBlocks)
4569
+ errors.push("Add a message or at least one block");
4570
+ return { valid: errors.length === 0, errors };
4571
+ },
4572
+ merge: (userConfig) => {
4573
+ return { ...SlackMessageConfig.defaults, ...userConfig };
4574
+ },
4575
+ getLabel: (config) => {
4576
+ const prefix = config.messageType === "user" ? "@" : "#";
4577
+ return config.channelName ? `Slack: ${prefix}${config.channelName}` : config.channel ? `Slack: ${config.channel}` : "Slack";
4578
+ }
4579
+ };
4580
+
4581
+ // src/catalog/communication/teams-message/config.ts
4582
+ var TeamsMessageConfig = {
4583
+ defaults: { connection: "", teamId: "", teamName: "", channelId: "", channelName: "", title: "", message: "" },
4584
+ schema: {
4585
+ connection: { type: "connection", required: true },
4586
+ teamId: { type: "string", required: true },
4587
+ channelId: { type: "string", required: true },
4588
+ title: { type: "string", required: false },
4589
+ message: { type: "string", required: true }
4590
+ },
4591
+ validate: (config) => {
4592
+ const errors = [];
4593
+ if (!config.connection)
4594
+ errors.push("Connect a Microsoft Teams account");
4595
+ if (!config.teamId)
4596
+ errors.push("Pick a team");
4597
+ if (!config.channelId)
4598
+ errors.push("Pick a channel");
4599
+ if (!config.message)
4600
+ errors.push("Add a message");
4601
+ return { valid: errors.length === 0, errors };
4602
+ },
4603
+ merge: (userConfig) => {
4604
+ return { ...TeamsMessageConfig.defaults, ...userConfig };
4605
+ },
4606
+ getLabel: (config) => config.channelName ? `Teams: #${config.channelName}` : "Teams"
4607
+ };
4608
+
4609
+ // src/catalog/communication/webhook-call/config.ts
4610
+ var WebhookCallConfig = {
4611
+ defaults: {
4612
+ url: "",
4613
+ method: "POST",
4614
+ payload: {},
4615
+ headers: {}
4616
+ },
4617
+ schema: {
4618
+ url: { type: "url", required: true },
4619
+ method: { type: "select", required: true },
4620
+ payload: { type: "json", required: false },
4621
+ headers: { type: "json", required: false }
4622
+ },
4623
+ validate: (config) => {
4624
+ const errors = [];
4625
+ if (!config.url)
4626
+ errors.push("Webhook URL is required");
4627
+ if (!config.method)
4628
+ errors.push("HTTP method is required");
4629
+ return { valid: errors.length === 0, errors };
4630
+ },
4631
+ merge: (userConfig) => {
4632
+ return { ...WebhookCallConfig.defaults, ...userConfig };
4633
+ },
4634
+ getLabel: (config) => {
4635
+ return `${config.method}: ${config.url.substring(0, 40)}...`;
4636
+ }
4637
+ };
4638
+
4639
+ // src/catalog/communication/whatsapp-send/config.ts
4640
+ var WhatsappSendConfig = {
4641
+ schema: {
4642
+ connection: { type: "connection", required: true },
4643
+ to: { type: "string", required: true },
4644
+ mode: { type: "select", required: true },
4645
+ templateName: { type: "string", required: false },
4646
+ templateLanguage: { type: "string", required: false },
4647
+ templateVariables: { type: "array", required: false },
4648
+ text: { type: "string", required: false },
4649
+ previewUrl: { type: "boolean", required: false }
4650
+ },
4651
+ defaults: {
4652
+ connection: "",
4653
+ to: "",
4654
+ mode: "template",
4655
+ templateLanguage: "en_US",
4656
+ templateVariables: []
4657
+ },
4658
+ validate: (config) => {
4659
+ const errors = [];
4660
+ if (!config.connection)
4661
+ errors.push("Select a WhatsApp connection");
4662
+ if (!config.to?.trim())
4663
+ errors.push("Recipient phone number is required");
4664
+ if (config.mode === "text") {
4665
+ if (!config.text?.trim())
4666
+ errors.push("Message text is required");
4667
+ } else {
4668
+ if (!config.templateName?.trim())
4669
+ errors.push("Choose an approved template");
4670
+ }
4671
+ return { valid: errors.length === 0, errors };
4672
+ },
4673
+ merge: (userConfig) => {
4674
+ return { ...WhatsappSendConfig.defaults, ...userConfig };
4675
+ },
4676
+ getLabel: (config) => config.mode === "text" ? `WhatsApp to ${config.to || "—"}` : `WhatsApp template "${config.templateName || "—"}" to ${config.to || "—"}`
4677
+ };
4678
+
4679
+ // src/catalog/storage/file-operation/config.ts
4680
+ var FileOperationConfig = {
4681
+ defaults: {
4682
+ connection: "",
4683
+ operation: "upload",
4684
+ sourcePath: "",
4685
+ destinationPath: "",
4686
+ fileContent: "",
4687
+ options: {}
4688
+ },
4689
+ schema: {
4690
+ connection: { type: "connection", required: true },
4691
+ operation: { type: "select", required: true },
4692
+ sourcePath: { type: "string", required: false },
4693
+ destinationPath: { type: "string", required: true },
4694
+ fileContent: { type: "string", required: false },
4695
+ options: { type: "json", required: false }
4696
+ },
4697
+ validate: (config) => {
4698
+ const errors = [];
4699
+ if (!config.connection)
4700
+ errors.push("Storage connection is required");
4701
+ if (!config.operation)
4702
+ errors.push("Operation is required");
4703
+ if (!config.destinationPath)
4704
+ errors.push("Destination path is required");
4705
+ if (config.operation === "upload" || config.operation === "copy" || config.operation === "move") {
4706
+ if (!config.sourcePath) {
4707
+ errors.push("Source path is required for upload/copy/move operations");
4708
+ }
4709
+ }
4710
+ return { valid: errors.length === 0, errors };
4711
+ },
4712
+ merge: (userConfig) => {
4713
+ return { ...FileOperationConfig.defaults, ...userConfig };
4714
+ },
4715
+ getLabel: (config) => {
4716
+ return `${config.operation.toUpperCase()}: ${config.destinationPath}`;
4717
+ }
4718
+ };
4719
+
4720
+ // src/catalog/transforms/format/config.ts
4721
+ var FormatConfig = {
4722
+ defaults: { input: "", format: "json" },
4723
+ schema: {
4724
+ input: { type: "expression", required: true },
4725
+ format: { type: "select", required: true }
4726
+ },
4727
+ validate: (config) => {
4728
+ const errors = [];
4729
+ if (!config.input)
4730
+ errors.push("Input required");
4731
+ if (!config.format)
4732
+ errors.push("Format required");
4733
+ return { valid: errors.length === 0, errors };
4734
+ },
4735
+ merge: (userConfig) => {
4736
+ return { ...FormatConfig.defaults, ...userConfig };
4737
+ },
4738
+ getLabel: (config) => `Format as ${config.format.toUpperCase()}`
4739
+ };
4740
+
4741
+ // src/catalog/triggers/schedule-trigger/config.ts
4742
+ var ScheduleTriggerConfig = {
4743
+ defaults: {
4744
+ scheduleType: "interval",
4745
+ interval: 60,
4746
+ intervalUnit: "minutes",
4747
+ timezone: "UTC"
4748
+ },
4749
+ schema: {
4750
+ scheduleType: { type: "select", required: true },
4751
+ interval: { type: "number", required: false, min: 1 },
4752
+ intervalUnit: { type: "select", required: false },
4753
+ cronExpression: { type: "string", required: false },
4754
+ timezone: { type: "string", required: false }
4755
+ },
4756
+ validate: (config) => {
4757
+ const errors = [];
4758
+ if (!config.scheduleType) {
4759
+ errors.push("Schedule type is required");
4760
+ }
4761
+ if (config.scheduleType === "interval") {
4762
+ if (!config.interval || config.interval < 1) {
4763
+ errors.push("Interval must be at least 1");
4764
+ }
4765
+ }
4766
+ if (config.scheduleType === "cron" && !config.cronExpression) {
4767
+ errors.push("Cron expression is required");
4768
+ }
4769
+ return { valid: errors.length === 0, errors };
4770
+ },
4771
+ merge: (userConfig) => {
4772
+ return { ...ScheduleTriggerConfig.defaults, ...userConfig };
4773
+ },
4774
+ getLabel: (config) => {
4775
+ if (config.scheduleType === "interval") {
4776
+ return `Every ${config.interval} ${config.intervalUnit}`;
4777
+ }
4778
+ return `Schedule: ${config.scheduleType}`;
4779
+ }
4780
+ };
4781
+
4782
+ // src/catalog/triggers/webhook-trigger/config.ts
4783
+ var WebhookTriggerConfig = {
4784
+ defaults: {
4785
+ method: "POST",
4786
+ requireAuth: true
4787
+ },
4788
+ schema: {
4789
+ webhookPath: { type: "string", required: false },
4790
+ method: { type: "select", required: true },
4791
+ requireAuth: { type: "boolean", required: false }
4792
+ },
4793
+ validate: (config) => {
4794
+ const errors = [];
4795
+ if (!config.method) {
4796
+ errors.push("HTTP method is required");
4797
+ }
4798
+ return { valid: errors.length === 0, errors };
4799
+ },
4800
+ merge: (userConfig) => {
4801
+ return { ...WebhookTriggerConfig.defaults, ...userConfig };
4802
+ },
4803
+ getLabel: (config) => {
4804
+ return `Webhook ${config.method}${config.webhookPath ? ` ${config.webhookPath}` : ""}`;
4805
+ }
4806
+ };
4807
+
4808
+ // src/catalog/utility/error/config.ts
4809
+ var ErrorConfig = {
4810
+ defaults: {
4811
+ message: "",
4812
+ code: undefined
4813
+ },
4814
+ schema: {
4815
+ message: { type: "string", required: true },
4816
+ code: { type: "string", required: false }
4817
+ },
4818
+ validate: (config) => {
4819
+ const errors = [];
4820
+ if (!config.message)
4821
+ errors.push("Error message is required");
4822
+ return { valid: errors.length === 0, errors };
4823
+ },
4824
+ merge: (userConfig) => {
4825
+ return { ...ErrorConfig.defaults, ...userConfig };
4826
+ },
4827
+ getLabel: (config) => {
4828
+ return `Error: ${config.message}`;
4829
+ }
4830
+ };
4831
+
4832
+ // src/rules/configContracts.ts
4833
+ var asValidator = (fn) => (config) => fn(config);
4834
+ var CONFIG_CONTRACTS = {
4835
+ console_log: { schema: ConsoleLogConfig.schema, validate: asValidator(ConsoleLogConfig.validate) },
4836
+ delay: { schema: DelayConfig.schema, validate: asValidator(DelayConfig.validate) },
4837
+ http_request: { schema: HTTPRequestConfig.schema, validate: asValidator(HTTPRequestConfig.validate) },
4838
+ graphql_query: { schema: GraphQLQueryConfig.schema, validate: asValidator(GraphQLQueryConfig.validate) },
4839
+ email: { schema: EmailConfig.schema, validate: asValidator(EmailConfig.validate) },
4840
+ google_chat: { schema: GoogleChatConfig.schema, validate: asValidator(GoogleChatConfig.validate) },
4841
+ slack_message: { schema: SlackMessageConfig.schema, validate: asValidator(SlackMessageConfig.validate) },
4842
+ teams_message: { schema: TeamsMessageConfig.schema, validate: asValidator(TeamsMessageConfig.validate) },
4843
+ webhook_call: { schema: WebhookCallConfig.schema, validate: asValidator(WebhookCallConfig.validate) },
4844
+ whatsapp_send: { schema: WhatsappSendConfig.schema, validate: asValidator(WhatsappSendConfig.validate) },
4845
+ query: {
4846
+ schema: BuildQueryContractSchema,
4847
+ validate: (config) => {
4848
+ const r = validateBuildQueryConfig(config);
4849
+ const errors = (r.errors ?? []).filter((e) => e.severity !== "warning").map((e) => e.message);
4850
+ return { valid: errors.length === 0, errors };
4851
+ }
4852
+ },
4853
+ transform: { schema: TransformConfig.schema, validate: asValidator(TransformConfig.validate) },
4854
+ condition: { schema: ConditionConfig.schema, validate: asValidator(ConditionConfig.validate) },
4855
+ for_each: { schema: ForEachConfig.schema, validate: asValidator(ForEachConfig.validate) },
4856
+ file_operation: { schema: FileOperationConfig.schema, validate: asValidator(FileOperationConfig.validate) },
4857
+ format: { schema: FormatConfig.schema, validate: asValidator(FormatConfig.validate) },
4858
+ manual_trigger: { schema: ManualTriggerConfig.schema, validate: asValidator(ManualTriggerConfig.validate) },
4859
+ schedule_trigger: { schema: ScheduleTriggerConfig.schema, validate: asValidator(ScheduleTriggerConfig.validate) },
4860
+ webhook_trigger: { schema: WebhookTriggerConfig.schema, validate: asValidator(WebhookTriggerConfig.validate) },
4861
+ debug: { schema: DebugConfig.schema, validate: asValidator(DebugConfig.validate) },
4862
+ error: { schema: ErrorConfig.schema, validate: asValidator(ErrorConfig.validate) },
4863
+ variables: { schema: VariablesConfig.schema, validate: asValidator(VariablesConfig.validate) }
4864
+ };
4865
+ function contractFor(actionId) {
4866
+ return actionId ? CONFIG_CONTRACTS[actionId] : undefined;
4867
+ }
4868
+ function declaredKeys(actionId) {
4869
+ const c = contractFor(actionId);
4870
+ if (!c)
4871
+ return [];
4872
+ return Object.entries(c.schema).filter(([, f]) => f.type !== "none").map(([k]) => k);
4873
+ }
4874
+ function isEditorPrivateKey(key) {
4875
+ return key.startsWith("_");
4876
+ }
4877
+ var CONTRACT_TYPE_TO_FIELD_TYPE = {
4878
+ string: "string" /* String */,
4879
+ text: "string" /* String */,
4880
+ select: "select" /* Select */,
4881
+ json: "json" /* JSON */,
4882
+ connection: "connection" /* Connection */,
4883
+ number: "number" /* Number */,
4884
+ boolean: "boolean" /* Boolean */,
4885
+ array: "array" /* Array */,
4886
+ object: "object" /* Object */,
4887
+ url: "url" /* URL */,
4888
+ expression: "expression" /* Expression */,
4889
+ code: "code" /* Code */
4890
+ };
4891
+ function humanise(key) {
4892
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/^\w/, (c) => c.toUpperCase());
4893
+ }
4894
+ function contractFieldsFor(definition) {
4895
+ const declared = definition.inputSchema?.fields ?? [];
4896
+ const contract = contractFor(definition.id);
4897
+ if (!contract)
4898
+ return declared;
4899
+ const seen = new Set(declared.map((f) => f.key));
4900
+ const derived = [];
4901
+ for (const [key, field] of Object.entries(contract.schema)) {
4902
+ if (seen.has(key) || field.type === "none")
4903
+ continue;
4904
+ derived.push({
4905
+ key,
4906
+ label: humanise(key),
4907
+ type: CONTRACT_TYPE_TO_FIELD_TYPE[field.type] ?? "string" /* String */,
4908
+ validation: { required: field.required === true },
4909
+ ...field.options ? { options: field.options.map((v) => ({ label: String(v), value: v })) } : {}
4910
+ });
4911
+ }
4912
+ return [...declared, ...derived];
4913
+ }
4914
+ function contractDrift(definition) {
4915
+ const contract = contractFor(definition.id);
4916
+ if (!contract)
4917
+ return [];
4918
+ const covers = (key) => {
4919
+ if (key in contract.schema)
4920
+ return true;
4921
+ const root = key.split(".")[0];
4922
+ const rootField = contract.schema[root];
4923
+ return !!rootField && (rootField.type === "object" || rootField.type === "json");
4924
+ };
4925
+ return (definition.inputSchema?.fields ?? []).map((f) => f.key).filter((k) => !covers(k));
4926
+ }
4927
+ function contractVocabularyDrift(actionId) {
4928
+ const c = contractFor(actionId);
4929
+ if (!c)
4930
+ return [];
4931
+ const known = new Set([...Object.keys(CONTRACT_TYPE_TO_FIELD_TYPE), "none"]);
4932
+ return Object.entries(c.schema).filter(([, f]) => !known.has(f.type)).map(([k, f]) => `${k}:${f.type}`);
4933
+ }
4934
+
4100
4935
  // src/rules/configSafety.ts
4101
4936
  var MAX_STEP_CONFIG_BYTES = 256 * 1024;
4102
4937
  var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
@@ -4205,7 +5040,7 @@ function validateStepConfig(step) {
4205
5040
  const definition = typeof step.actionDefinitionId === "string" ? getActionById(step.actionDefinitionId) : undefined;
4206
5041
  if (!definition)
4207
5042
  return issues;
4208
- const declared = new Map(definition.inputSchema.fields.map((f) => [f.key, f]));
5043
+ const declared = new Map(contractFieldsFor(definition).map((f) => [f.key, f]));
4209
5044
  const entries = Object.entries(config);
4210
5045
  for (const [key, value] of entries) {
4211
5046
  const field = declared.get(key);
@@ -4252,3 +5087,85 @@ function validateStepConfigs(steps) {
4252
5087
  function configErrors(steps) {
4253
5088
  return validateStepConfigs(steps).filter((i) => i.severity === "error");
4254
5089
  }
5090
+ // src/rules/mappingFunctions.ts
5091
+ var str = (v) => String(v ?? "");
5092
+ var HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
5093
+ var MAPPING_FUNCTIONS = {
5094
+ concat: (...args) => args.map((a) => a == null ? "" : String(a)).join(""),
5095
+ uppercase: (val) => str(val).toUpperCase(),
5096
+ lowercase: (val) => str(val).toLowerCase(),
5097
+ trim: (val) => str(val).trim(),
5098
+ parseDate: (val) => {
5099
+ const d = new Date(String(val));
5100
+ return isNaN(d.getTime()) ? null : d.toISOString();
5101
+ },
5102
+ parseNumber: (val) => {
5103
+ const n = Number(val);
5104
+ return isNaN(n) ? null : n;
5105
+ },
5106
+ toString: (val) => str(val),
5107
+ substring: (val, start, end) => {
5108
+ const s = str(val);
5109
+ return s.substring(Number(start) || 0, end !== undefined ? Number(end) : undefined);
5110
+ },
5111
+ replace: (val, search, replacement) => str(val).replace(String(search), String(replacement)),
5112
+ default: (val, fallback) => val == null || val === "" ? fallback : val,
5113
+ normalizeUrl: (val, scheme = "https") => {
5114
+ const s = str(val).trim();
5115
+ if (!s)
5116
+ return "";
5117
+ if (HAS_SCHEME.test(s))
5118
+ return s;
5119
+ return `${String(scheme || "https")}://${s}`;
5120
+ },
5121
+ normalizeEmail: (val) => str(val).trim().toLowerCase(),
5122
+ normalizePhone: (val, countryCode) => {
5123
+ const raw = str(val).trim();
5124
+ if (!raw)
5125
+ return "";
5126
+ const plus = raw.startsWith("+");
5127
+ const digits = raw.replace(/\D/g, "");
5128
+ if (!digits)
5129
+ return "";
5130
+ if (plus)
5131
+ return `+${digits}`;
5132
+ const cc = str(countryCode).replace(/\D/g, "");
5133
+ return cc ? `+${cc}${digits}` : digits;
5134
+ },
5135
+ toDateOnly: (val) => {
5136
+ const d = new Date(String(val));
5137
+ return isNaN(d.getTime()) ? null : d.toISOString().slice(0, 10);
5138
+ }
5139
+ };
5140
+ var MAPPING_FUNCTION_SPECS = [
5141
+ { name: "concat", signature: 'concat(value, "text", …)', description: "Joins the value and any literals into one string.", example: 'concat(value, " Ltd")' },
5142
+ { name: "uppercase", signature: "uppercase(value)", description: "Upper-cases the value.", example: "uppercase(value)" },
5143
+ { name: "lowercase", signature: "lowercase(value)", description: "Lower-cases the value.", example: "lowercase(value)" },
5144
+ { name: "trim", signature: "trim(value)", description: "Removes leading and trailing whitespace.", example: "trim(value)" },
5145
+ { name: "parseDate", signature: "parseDate(value)", description: "Parses to a full ISO timestamp, or null if unparseable.", example: "parseDate(value)" },
5146
+ { name: "parseNumber", signature: "parseNumber(value)", description: "Parses to a number, or null if not numeric.", example: "parseNumber(value)" },
5147
+ { name: "toString", signature: "toString(value)", description: 'Renders the value as a string ("" when empty).', example: "toString(value)" },
5148
+ { name: "substring", signature: "substring(value, start, end?)", description: "Takes part of the value by index.", example: "substring(value, 0, 10)" },
5149
+ { name: "replace", signature: 'replace(value, "find", "with")', description: "Replaces the first match.", example: 'replace(value, "Inc", "Inc.")' },
5150
+ { name: "default", signature: 'default(value, "fallback")', description: "Uses the fallback when the value is empty or missing.", example: 'default(value, "Unknown")' },
5151
+ { name: "normalizeUrl", signature: "normalizeUrl(value, scheme?)", description: "Adds https:// when the value names no scheme. Leaves http://, mailto: and others alone.", example: "normalizeUrl(value)" },
5152
+ { name: "normalizeEmail", signature: "normalizeEmail(value)", description: "Trims and lower-cases an address.", example: "normalizeEmail(value)" },
5153
+ { name: "normalizePhone", signature: "normalizePhone(value, countryCode?)", description: "Strips punctuation to digits, keeping a leading +. With a country code, prefixes numbers that lack one.", example: 'normalizePhone(value, "+1")' },
5154
+ { name: "toDateOnly", signature: "toDateOnly(value)", description: "Reduces a timestamp to YYYY-MM-DD in UTC.", example: "toDateOnly(value)" }
5155
+ ];
5156
+ var USER_FUNCTION_PREFIX = "user.";
5157
+ function isMappingFunction(name) {
5158
+ if (!name)
5159
+ return false;
5160
+ if (name.startsWith(USER_FUNCTION_PREFIX))
5161
+ return name.length > USER_FUNCTION_PREFIX.length;
5162
+ return Object.hasOwn(MAPPING_FUNCTIONS, name);
5163
+ }
5164
+ function mappingTransformFunctionName(transform) {
5165
+ const trimmed = (transform ?? "").trim();
5166
+ if (!trimmed)
5167
+ return null;
5168
+ const normalised = /^[A-Za-z_][\w.]*$/.test(trimmed) ? `${trimmed}(value)` : trimmed;
5169
+ const match = normalised.match(/^([A-Za-z_][\w.]*)\((.*)\)$/s);
5170
+ return match ? match[1] : null;
5171
+ }