@axonflow/sdk 2.1.0 → 2.3.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.
Files changed (42) hide show
  1. package/README.md +39 -36
  2. package/dist/cjs/client.d.ts +75 -2
  3. package/dist/cjs/client.d.ts.map +1 -1
  4. package/dist/cjs/client.js +196 -77
  5. package/dist/cjs/client.js.map +1 -1
  6. package/dist/cjs/errors.d.ts +93 -6
  7. package/dist/cjs/errors.d.ts.map +1 -1
  8. package/dist/cjs/errors.js +126 -12
  9. package/dist/cjs/errors.js.map +1 -1
  10. package/dist/cjs/index.d.ts +3 -3
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +7 -3
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/types/code-governance.d.ts +2 -0
  15. package/dist/cjs/types/code-governance.d.ts.map +1 -1
  16. package/dist/cjs/types/config.d.ts +15 -7
  17. package/dist/cjs/types/config.d.ts.map +1 -1
  18. package/dist/cjs/types/connector.d.ts +28 -0
  19. package/dist/cjs/types/connector.d.ts.map +1 -1
  20. package/dist/cjs/types/proxy.d.ts +2 -2
  21. package/dist/cjs/types/proxy.d.ts.map +1 -1
  22. package/dist/esm/client.d.ts +75 -2
  23. package/dist/esm/client.d.ts.map +1 -1
  24. package/dist/esm/client.js +197 -78
  25. package/dist/esm/client.js.map +1 -1
  26. package/dist/esm/errors.d.ts +93 -6
  27. package/dist/esm/errors.d.ts.map +1 -1
  28. package/dist/esm/errors.js +121 -11
  29. package/dist/esm/errors.js.map +1 -1
  30. package/dist/esm/index.d.ts +3 -3
  31. package/dist/esm/index.d.ts.map +1 -1
  32. package/dist/esm/index.js +3 -3
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/types/code-governance.d.ts +2 -0
  35. package/dist/esm/types/code-governance.d.ts.map +1 -1
  36. package/dist/esm/types/config.d.ts +15 -7
  37. package/dist/esm/types/config.d.ts.map +1 -1
  38. package/dist/esm/types/connector.d.ts +28 -0
  39. package/dist/esm/types/connector.d.ts.map +1 -1
  40. package/dist/esm/types/proxy.d.ts +2 -2
  41. package/dist/esm/types/proxy.d.ts.map +1 -1
  42. package/package.json +1 -1
@@ -12,15 +12,19 @@ class AxonFlow {
12
12
  constructor(config) {
13
13
  this.interceptors = [];
14
14
  this.sessionCookie = null;
15
+ // Configuration validation
16
+ if (config.clientSecret && !config.clientId) {
17
+ throw new errors_1.ConfigurationError('clientSecret requires clientId to be set. ' +
18
+ 'Provide both clientId and clientSecret for OAuth2-style authentication.');
19
+ }
15
20
  // Set defaults first to determine endpoint
16
21
  const endpoint = config.endpoint || 'https://staging-eu.getaxonflow.com';
17
- // Credentials are optional for community/self-hosted deployments
18
- // Enterprise features (Gateway Mode, Policy CRUD) require credentials
19
- const hasCredentials = !!(config.licenseKey || config.apiKey);
22
+ // Credentials check: OAuth2-style (clientId/clientSecret)
23
+ const hasCredentials = !!(config.clientId && config.clientSecret);
20
24
  // Set configuration
21
25
  this.config = {
22
- apiKey: config.apiKey,
23
- licenseKey: config.licenseKey,
26
+ clientId: config.clientId,
27
+ clientSecret: config.clientSecret,
24
28
  endpoint,
25
29
  mode: config.mode || (hasCredentials ? 'production' : 'sandbox'),
26
30
  tenant: config.tenant || 'default',
@@ -40,17 +44,33 @@ class AxonFlow {
40
44
  // Initialize interceptors
41
45
  this.interceptors = [new openai_1.OpenAIInterceptor(), new anthropic_1.AnthropicInterceptor()];
42
46
  if (this.config.debug) {
47
+ // Determine auth method for logging
48
+ const authMethod = hasCredentials ? 'client-credentials' : 'community (no auth)';
43
49
  (0, helpers_1.debugLog)('AxonFlow initialized', {
44
50
  mode: this.config.mode,
45
51
  endpoint: this.config.endpoint,
46
- authMethod: hasCredentials
47
- ? this.config.licenseKey
48
- ? 'license-key'
49
- : 'api-key'
50
- : 'community (no auth)',
52
+ authMethod,
51
53
  });
52
54
  }
53
55
  }
56
+ /**
57
+ * Get authentication headers based on configured credentials.
58
+ *
59
+ * Uses OAuth2-style Basic auth: Authorization: Basic base64(clientId:clientSecret)
60
+ * Also adds X-Tenant-ID header from clientId for tenant context.
61
+ *
62
+ * @returns Headers object with authentication headers
63
+ */
64
+ getAuthHeaders() {
65
+ const headers = {};
66
+ // OAuth2-style client credentials
67
+ if (this.config.clientId && this.config.clientSecret) {
68
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
69
+ headers['Authorization'] = `Basic ${credentials}`;
70
+ headers['X-Tenant-ID'] = this.config.clientId;
71
+ }
72
+ return headers;
73
+ }
54
74
  /**
55
75
  * Main method to protect AI calls with governance
56
76
  * @param aiCall The AI call to protect
@@ -169,8 +189,8 @@ class AxonFlow {
169
189
  // Transform SDK request to Agent API format
170
190
  const agentRequest = {
171
191
  query: request.aiRequest.prompt,
172
- user_token: this.config.apiKey || '',
173
- client_id: this.config.tenant,
192
+ user_token: '',
193
+ client_id: this.config.clientId || this.config.tenant,
174
194
  request_type: 'llm_chat',
175
195
  context: {
176
196
  provider: request.aiRequest.provider,
@@ -182,12 +202,8 @@ class AxonFlow {
182
202
  };
183
203
  const headers = {
184
204
  'Content-Type': 'application/json',
205
+ ...this.getAuthHeaders(),
185
206
  };
186
- // Add auth headers only when credentials are provided
187
- // Community/self-hosted mode works without credentials
188
- if (this.config.licenseKey) {
189
- headers['X-License-Key'] = this.config.licenseKey;
190
- }
191
207
  const response = await fetch(url, {
192
208
  method: 'POST',
193
209
  headers,
@@ -250,9 +266,10 @@ class AxonFlow {
250
266
  /**
251
267
  * Create a sandbox client for testing
252
268
  */
253
- static sandbox(apiKey = 'demo-key') {
269
+ static sandbox(clientId = 'demo-client', clientSecret = 'demo-secret') {
254
270
  return new AxonFlow({
255
- apiKey,
271
+ clientId,
272
+ clientSecret,
256
273
  mode: 'sandbox',
257
274
  endpoint: 'https://staging-eu.getaxonflow.com',
258
275
  debug: true,
@@ -389,9 +406,11 @@ class AxonFlow {
389
406
  * ```
390
407
  */
391
408
  async executeQuery(options) {
409
+ // Default to "anonymous" if userToken is empty/undefined (community mode)
410
+ const effectiveUserToken = options.userToken || 'anonymous';
392
411
  const agentRequest = {
393
412
  query: options.query,
394
- user_token: options.userToken,
413
+ user_token: effectiveUserToken,
395
414
  client_id: this.config.tenant,
396
415
  request_type: options.requestType,
397
416
  context: options.context || {},
@@ -399,15 +418,8 @@ class AxonFlow {
399
418
  const url = `${this.config.endpoint}/api/request`;
400
419
  const headers = {
401
420
  'Content-Type': 'application/json',
421
+ ...this.getAuthHeaders(),
402
422
  };
403
- // Add auth headers only when credentials are provided
404
- // Community/self-hosted mode works without credentials
405
- if (this.config.licenseKey) {
406
- headers['X-License-Key'] = this.config.licenseKey;
407
- }
408
- else if (this.config.apiKey) {
409
- headers['X-Client-Secret'] = this.config.apiKey;
410
- }
411
423
  if (this.config.debug) {
412
424
  (0, helpers_1.debugLog)('Proxy Mode: executeQuery', {
413
425
  requestType: options.requestType,
@@ -532,8 +544,8 @@ class AxonFlow {
532
544
  async queryConnector(connectorName, query, params) {
533
545
  const agentRequest = {
534
546
  query,
535
- user_token: this.config.apiKey || '',
536
- client_id: this.config.tenant,
547
+ user_token: '',
548
+ client_id: this.config.clientId || this.config.tenant,
537
549
  request_type: 'mcp-query',
538
550
  context: {
539
551
  connector: connectorName,
@@ -543,10 +555,8 @@ class AxonFlow {
543
555
  const url = `${this.config.endpoint}/api/request`;
544
556
  const headers = {
545
557
  'Content-Type': 'application/json',
558
+ ...this.getAuthHeaders(),
546
559
  };
547
- if (this.config.licenseKey) {
548
- headers['X-License-Key'] = this.config.licenseKey;
549
- }
550
560
  const response = await fetch(url, {
551
561
  method: 'POST',
552
562
  headers,
@@ -555,7 +565,7 @@ class AxonFlow {
555
565
  });
556
566
  if (!response.ok) {
557
567
  const errorText = await response.text();
558
- throw new Error(`Connector query failed: ${response.status} ${response.statusText} - ${errorText}`);
568
+ throw new errors_1.ConnectorError(`Connector query failed: ${response.status} ${response.statusText} - ${errorText}`, connectorName, 'query');
559
569
  }
560
570
  const agentResponse = await response.json();
561
571
  if (this.config.debug) {
@@ -568,6 +578,94 @@ class AxonFlow {
568
578
  meta: agentResponse.metadata,
569
579
  };
570
580
  }
581
+ /**
582
+ * Execute a query directly against the MCP connector endpoint.
583
+ *
584
+ * This method calls the agent's /mcp/resources/query endpoint which provides:
585
+ * - Request-phase policy evaluation (SQLi blocking, PII blocking)
586
+ * - Response-phase policy evaluation (PII redaction)
587
+ * - PolicyInfo metadata in responses
588
+ *
589
+ * @example
590
+ * ```typescript
591
+ * const response = await axonflow.mcpQuery({
592
+ * connector: 'postgres',
593
+ * statement: 'SELECT * FROM customers LIMIT 10',
594
+ * });
595
+ *
596
+ * if (response.redacted) {
597
+ * console.log('Fields redacted:', response.redacted_fields);
598
+ * }
599
+ * console.log('Policies evaluated:', response.policy_info?.policies_evaluated);
600
+ * ```
601
+ *
602
+ * @param options - Query options including connector name and SQL statement
603
+ * @returns ConnectorResponse with data, redaction info, and policy_info
604
+ * @throws ConnectorError if the request is blocked by policy or fails
605
+ */
606
+ async mcpQuery(options) {
607
+ if (!options.connector) {
608
+ throw new errors_1.ConnectorError('connector name is required', undefined, 'mcpQuery');
609
+ }
610
+ if (!options.statement) {
611
+ throw new errors_1.ConnectorError('statement is required', undefined, 'mcpQuery');
612
+ }
613
+ const url = `${this.config.endpoint}/mcp/resources/query`;
614
+ const headers = {
615
+ 'Content-Type': 'application/json',
616
+ ...this.getAuthHeaders(),
617
+ };
618
+ const body = {
619
+ connector: options.connector,
620
+ statement: options.statement,
621
+ options: options.options || {},
622
+ };
623
+ if (this.config.debug) {
624
+ (0, helpers_1.debugLog)('MCP Query', {
625
+ connector: options.connector,
626
+ statement: options.statement.substring(0, 50),
627
+ });
628
+ }
629
+ const response = await fetch(url, {
630
+ method: 'POST',
631
+ headers,
632
+ body: JSON.stringify(body),
633
+ signal: AbortSignal.timeout(this.config.timeout),
634
+ });
635
+ const responseData = await response.json();
636
+ // Handle policy blocks (403 responses)
637
+ if (!response.ok) {
638
+ throw new errors_1.ConnectorError(responseData.error || `MCP query failed: ${response.status} ${response.statusText}`, options.connector, 'mcpQuery');
639
+ }
640
+ if (this.config.debug) {
641
+ (0, helpers_1.debugLog)('MCP Query result', {
642
+ connector: options.connector,
643
+ success: responseData.success,
644
+ redacted: responseData.redacted,
645
+ policiesEvaluated: responseData.policy_info?.policies_evaluated,
646
+ });
647
+ }
648
+ return {
649
+ success: responseData.success,
650
+ data: responseData.data,
651
+ error: responseData.error,
652
+ meta: responseData.meta,
653
+ redacted: responseData.redacted,
654
+ redacted_fields: responseData.redacted_fields,
655
+ policy_info: responseData.policy_info,
656
+ };
657
+ }
658
+ /**
659
+ * Execute a statement against an MCP connector (alias for mcpQuery).
660
+ *
661
+ * Same as mcpQuery but follows the naming convention of other execute* methods.
662
+ *
663
+ * @param options - Query options including connector name and SQL statement
664
+ * @returns ConnectorResponse with data, redaction info, and policy_info
665
+ */
666
+ async mcpExecute(options) {
667
+ return this.mcpQuery(options);
668
+ }
571
669
  /**
572
670
  * Generate a multi-agent execution plan from a natural language query
573
671
  * @param query - Natural language query describing the task
@@ -585,10 +683,8 @@ class AxonFlow {
585
683
  const url = `${this.config.endpoint}/api/request`;
586
684
  const headers = {
587
685
  'Content-Type': 'application/json',
686
+ ...this.getAuthHeaders(),
588
687
  };
589
- if (this.config.licenseKey) {
590
- headers['X-License-Key'] = this.config.licenseKey;
591
- }
592
688
  // Use mapTimeout for MAP operations (default 2 minutes)
593
689
  const response = await fetch(url, {
594
690
  method: 'POST',
@@ -598,11 +694,11 @@ class AxonFlow {
598
694
  });
599
695
  if (!response.ok) {
600
696
  const errorText = await response.text();
601
- throw new Error(`Plan generation failed: ${response.status} ${response.statusText} - ${errorText}`);
697
+ throw new errors_1.PlanExecutionError(`Plan generation failed: ${response.status} ${response.statusText} - ${errorText}`, undefined, 'generation');
602
698
  }
603
699
  const agentResponse = await response.json();
604
700
  if (!agentResponse.success) {
605
- throw new Error(`Plan generation failed: ${agentResponse.error}`);
701
+ throw new errors_1.PlanExecutionError(`Plan generation failed: ${agentResponse.error}`, undefined, 'generation');
606
702
  }
607
703
  // plan_id can be at top level or inside data
608
704
  const planId = agentResponse.plan_id || agentResponse.data?.plan_id;
@@ -634,10 +730,8 @@ class AxonFlow {
634
730
  const url = `${this.config.endpoint}/api/request`;
635
731
  const headers = {
636
732
  'Content-Type': 'application/json',
733
+ ...this.getAuthHeaders(),
637
734
  };
638
- if (this.config.licenseKey) {
639
- headers['X-License-Key'] = this.config.licenseKey;
640
- }
641
735
  // Use mapTimeout for MAP operations (default 2 minutes)
642
736
  const response = await fetch(url, {
643
737
  method: 'POST',
@@ -647,7 +741,7 @@ class AxonFlow {
647
741
  });
648
742
  if (!response.ok) {
649
743
  const errorText = await response.text();
650
- throw new Error(`Plan execution failed: ${response.status} ${response.statusText} - ${errorText}`);
744
+ throw new errors_1.PlanExecutionError(`Plan execution failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'execution');
651
745
  }
652
746
  const agentResponse = await response.json();
653
747
  if (this.config.debug) {
@@ -666,7 +760,7 @@ class AxonFlow {
666
760
  * Get the status of a running or completed plan
667
761
  */
668
762
  async getPlanStatus(planId) {
669
- const url = `${this.config.endpoint}/api/plans/${planId}`;
763
+ const url = `${this.config.endpoint}/api/v1/plan/${planId}`;
670
764
  const response = await fetch(url, {
671
765
  method: 'GET',
672
766
  signal: AbortSignal.timeout(this.config.timeout),
@@ -749,15 +843,8 @@ class AxonFlow {
749
843
  };
750
844
  const headers = {
751
845
  'Content-Type': 'application/json',
846
+ ...this.getAuthHeaders(),
752
847
  };
753
- // Add auth headers only when credentials are provided
754
- // Community/self-hosted mode works without credentials
755
- if (this.config.licenseKey) {
756
- headers['X-License-Key'] = this.config.licenseKey;
757
- }
758
- else if (this.config.apiKey) {
759
- headers['X-Client-Secret'] = this.config.apiKey;
760
- }
761
848
  if (this.config.debug) {
762
849
  (0, helpers_1.debugLog)('Gateway Mode: Pre-check', { query: options.query.substring(0, 50) });
763
850
  }
@@ -848,15 +935,8 @@ class AxonFlow {
848
935
  };
849
936
  const headers = {
850
937
  'Content-Type': 'application/json',
938
+ ...this.getAuthHeaders(),
851
939
  };
852
- // Add auth headers only when credentials are provided
853
- // Community/self-hosted mode works without credentials
854
- if (this.config.licenseKey) {
855
- headers['X-License-Key'] = this.config.licenseKey;
856
- }
857
- else if (this.config.apiKey) {
858
- headers['X-Client-Secret'] = this.config.apiKey;
859
- }
860
940
  if (this.config.debug) {
861
941
  (0, helpers_1.debugLog)('Gateway Mode: Audit', {
862
942
  contextId: options.contextId,
@@ -1038,24 +1118,20 @@ class AxonFlow {
1038
1118
  // Policy CRUD Methods - Static Policies
1039
1119
  // ============================================================================
1040
1120
  /**
1041
- * Build authentication headers for API requests
1121
+ * Build authentication headers for API requests.
1122
+ * Includes Content-Type and X-Org-ID for policy APIs.
1123
+ * Uses getAuthHeaders() for authentication credentials.
1042
1124
  */
1043
1125
  buildAuthHeaders() {
1044
1126
  const headers = {
1045
1127
  'Content-Type': 'application/json',
1128
+ ...this.getAuthHeaders(),
1046
1129
  };
1047
1130
  // Always include tenant ID for policy APIs (X-Org-ID header for server compatibility)
1131
+ // Note: getAuthHeaders() already adds X-Tenant-ID when tenant is non-default
1048
1132
  if (this.config.tenant) {
1049
1133
  headers['X-Org-ID'] = this.config.tenant;
1050
1134
  }
1051
- // Add auth headers only when credentials are provided
1052
- // Community/self-hosted mode works without credentials
1053
- if (this.config.licenseKey) {
1054
- headers['X-License-Key'] = this.config.licenseKey;
1055
- }
1056
- else if (this.config.apiKey) {
1057
- headers['X-Client-Secret'] = this.config.apiKey;
1058
- }
1059
1135
  return headers;
1060
1136
  }
1061
1137
  /**
@@ -1080,8 +1156,8 @@ class AxonFlow {
1080
1156
  }
1081
1157
  throw new errors_1.APIError(response.status, response.statusText, errorText);
1082
1158
  }
1083
- // Handle DELETE responses with no body
1084
- if (response.status === 204 || method === 'DELETE') {
1159
+ // Handle 204 No Content responses
1160
+ if (response.status === 204) {
1085
1161
  return undefined;
1086
1162
  }
1087
1163
  return response.json();
@@ -1435,7 +1511,7 @@ class AxonFlow {
1435
1511
  // API returns {"policies": [...]} wrapper via Agent proxy
1436
1512
  const response = await this.orchestratorRequest('GET', path);
1437
1513
  // Handle both wrapped and unwrapped responses for compatibility
1438
- return Array.isArray(response) ? response : response.policies;
1514
+ return Array.isArray(response) ? response : response.policies || [];
1439
1515
  }
1440
1516
  /**
1441
1517
  * Get a specific dynamic policy by ID.
@@ -1543,7 +1619,7 @@ class AxonFlow {
1543
1619
  // API returns {"policies": [...]} wrapper via Agent proxy
1544
1620
  const response = await this.orchestratorRequest('GET', path);
1545
1621
  // Handle both wrapped and unwrapped responses for compatibility
1546
- return Array.isArray(response) ? response : response.policies;
1622
+ return Array.isArray(response) ? response : response.policies || [];
1547
1623
  }
1548
1624
  // ============================================================================
1549
1625
  // Portal Authentication Methods (Enterprise)
@@ -1928,6 +2004,49 @@ class AxonFlow {
1928
2004
  providerType: response.provider_type,
1929
2005
  };
1930
2006
  }
2007
+ /**
2008
+ * Close a PR without merging and optionally delete the branch.
2009
+ * Useful for cleaning up test PRs created by examples.
2010
+ *
2011
+ * @param prId - PR record ID
2012
+ * @param deleteBranch - Whether to delete the associated branch (default: true)
2013
+ * @returns Closed PR record
2014
+ *
2015
+ * @example
2016
+ * ```typescript
2017
+ * // Close PR and delete branch
2018
+ * const pr = await axonflow.closePR('pr_123');
2019
+ * console.log(`PR #${pr.prNumber} closed`);
2020
+ *
2021
+ * // Close PR but keep branch
2022
+ * const pr = await axonflow.closePR('pr_123', false);
2023
+ * ```
2024
+ */
2025
+ async closePR(prId, deleteBranch = true) {
2026
+ if (this.config.debug) {
2027
+ (0, helpers_1.debugLog)('Closing PR', { prId, deleteBranch });
2028
+ }
2029
+ const query = deleteBranch ? '?delete_branch=true' : '';
2030
+ const response = await this.portalRequest('DELETE', `/api/v1/code-governance/prs/${prId}${query}`);
2031
+ return {
2032
+ id: response.id,
2033
+ prNumber: response.pr_number,
2034
+ prUrl: response.pr_url,
2035
+ title: response.title,
2036
+ state: response.state,
2037
+ owner: response.owner,
2038
+ repo: response.repo,
2039
+ headBranch: response.head_branch,
2040
+ baseBranch: response.base_branch,
2041
+ filesCount: response.files_count,
2042
+ secretsDetected: response.secrets_detected,
2043
+ unsafePatterns: response.unsafe_patterns,
2044
+ createdAt: response.created_at,
2045
+ closedAt: response.closed_at,
2046
+ createdBy: response.created_by,
2047
+ providerType: response.provider_type,
2048
+ };
2049
+ }
1931
2050
  /**
1932
2051
  * Sync PR status with the Git provider.
1933
2052
  * This updates the local record with the current state from GitHub/GitLab/Bitbucket.
@@ -2121,8 +2240,8 @@ class AxonFlow {
2121
2240
  }
2122
2241
  throw new errors_1.APIError(response.status, response.statusText, errorText);
2123
2242
  }
2124
- // Handle DELETE responses with no body
2125
- if (response.status === 204 || method === 'DELETE') {
2243
+ // Handle 204 No Content responses
2244
+ if (response.status === 204) {
2126
2245
  return undefined;
2127
2246
  }
2128
2247
  return response.json();
@@ -2165,8 +2284,8 @@ class AxonFlow {
2165
2284
  }
2166
2285
  throw new errors_1.APIError(response.status, response.statusText, errorText);
2167
2286
  }
2168
- // Handle DELETE responses with no body
2169
- if (response.status === 204 || method === 'DELETE') {
2287
+ // Handle 204 No Content responses
2288
+ if (response.status === 204) {
2170
2289
  return undefined;
2171
2290
  }
2172
2291
  return response.json();