@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
@@ -1,4 +1,4 @@
1
- import { AuthenticationError, APIError, PolicyViolationError } from './errors.js';
1
+ import { AuthenticationError, APIError, PolicyViolationError, ConfigurationError, ConnectorError, PlanExecutionError, } from './errors.js';
2
2
  import { OpenAIInterceptor } from './interceptors/openai.js';
3
3
  import { AnthropicInterceptor } from './interceptors/anthropic.js';
4
4
  import { generateRequestId, debugLog } from './utils/helpers.js';
@@ -9,15 +9,19 @@ export class AxonFlow {
9
9
  constructor(config) {
10
10
  this.interceptors = [];
11
11
  this.sessionCookie = null;
12
+ // Configuration validation
13
+ if (config.clientSecret && !config.clientId) {
14
+ throw new ConfigurationError('clientSecret requires clientId to be set. ' +
15
+ 'Provide both clientId and clientSecret for OAuth2-style authentication.');
16
+ }
12
17
  // Set defaults first to determine endpoint
13
18
  const endpoint = config.endpoint || 'https://staging-eu.getaxonflow.com';
14
- // Credentials are optional for community/self-hosted deployments
15
- // Enterprise features (Gateway Mode, Policy CRUD) require credentials
16
- const hasCredentials = !!(config.licenseKey || config.apiKey);
19
+ // Credentials check: OAuth2-style (clientId/clientSecret)
20
+ const hasCredentials = !!(config.clientId && config.clientSecret);
17
21
  // Set configuration
18
22
  this.config = {
19
- apiKey: config.apiKey,
20
- licenseKey: config.licenseKey,
23
+ clientId: config.clientId,
24
+ clientSecret: config.clientSecret,
21
25
  endpoint,
22
26
  mode: config.mode || (hasCredentials ? 'production' : 'sandbox'),
23
27
  tenant: config.tenant || 'default',
@@ -37,17 +41,33 @@ export class AxonFlow {
37
41
  // Initialize interceptors
38
42
  this.interceptors = [new OpenAIInterceptor(), new AnthropicInterceptor()];
39
43
  if (this.config.debug) {
44
+ // Determine auth method for logging
45
+ const authMethod = hasCredentials ? 'client-credentials' : 'community (no auth)';
40
46
  debugLog('AxonFlow initialized', {
41
47
  mode: this.config.mode,
42
48
  endpoint: this.config.endpoint,
43
- authMethod: hasCredentials
44
- ? this.config.licenseKey
45
- ? 'license-key'
46
- : 'api-key'
47
- : 'community (no auth)',
49
+ authMethod,
48
50
  });
49
51
  }
50
52
  }
53
+ /**
54
+ * Get authentication headers based on configured credentials.
55
+ *
56
+ * Uses OAuth2-style Basic auth: Authorization: Basic base64(clientId:clientSecret)
57
+ * Also adds X-Tenant-ID header from clientId for tenant context.
58
+ *
59
+ * @returns Headers object with authentication headers
60
+ */
61
+ getAuthHeaders() {
62
+ const headers = {};
63
+ // OAuth2-style client credentials
64
+ if (this.config.clientId && this.config.clientSecret) {
65
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
66
+ headers['Authorization'] = `Basic ${credentials}`;
67
+ headers['X-Tenant-ID'] = this.config.clientId;
68
+ }
69
+ return headers;
70
+ }
51
71
  /**
52
72
  * Main method to protect AI calls with governance
53
73
  * @param aiCall The AI call to protect
@@ -166,8 +186,8 @@ export class AxonFlow {
166
186
  // Transform SDK request to Agent API format
167
187
  const agentRequest = {
168
188
  query: request.aiRequest.prompt,
169
- user_token: this.config.apiKey || '',
170
- client_id: this.config.tenant,
189
+ user_token: '',
190
+ client_id: this.config.clientId || this.config.tenant,
171
191
  request_type: 'llm_chat',
172
192
  context: {
173
193
  provider: request.aiRequest.provider,
@@ -179,12 +199,8 @@ export class AxonFlow {
179
199
  };
180
200
  const headers = {
181
201
  'Content-Type': 'application/json',
202
+ ...this.getAuthHeaders(),
182
203
  };
183
- // Add auth headers only when credentials are provided
184
- // Community/self-hosted mode works without credentials
185
- if (this.config.licenseKey) {
186
- headers['X-License-Key'] = this.config.licenseKey;
187
- }
188
204
  const response = await fetch(url, {
189
205
  method: 'POST',
190
206
  headers,
@@ -247,9 +263,10 @@ export class AxonFlow {
247
263
  /**
248
264
  * Create a sandbox client for testing
249
265
  */
250
- static sandbox(apiKey = 'demo-key') {
266
+ static sandbox(clientId = 'demo-client', clientSecret = 'demo-secret') {
251
267
  return new AxonFlow({
252
- apiKey,
268
+ clientId,
269
+ clientSecret,
253
270
  mode: 'sandbox',
254
271
  endpoint: 'https://staging-eu.getaxonflow.com',
255
272
  debug: true,
@@ -386,9 +403,11 @@ export class AxonFlow {
386
403
  * ```
387
404
  */
388
405
  async executeQuery(options) {
406
+ // Default to "anonymous" if userToken is empty/undefined (community mode)
407
+ const effectiveUserToken = options.userToken || 'anonymous';
389
408
  const agentRequest = {
390
409
  query: options.query,
391
- user_token: options.userToken,
410
+ user_token: effectiveUserToken,
392
411
  client_id: this.config.tenant,
393
412
  request_type: options.requestType,
394
413
  context: options.context || {},
@@ -396,15 +415,8 @@ export class AxonFlow {
396
415
  const url = `${this.config.endpoint}/api/request`;
397
416
  const headers = {
398
417
  'Content-Type': 'application/json',
418
+ ...this.getAuthHeaders(),
399
419
  };
400
- // Add auth headers only when credentials are provided
401
- // Community/self-hosted mode works without credentials
402
- if (this.config.licenseKey) {
403
- headers['X-License-Key'] = this.config.licenseKey;
404
- }
405
- else if (this.config.apiKey) {
406
- headers['X-Client-Secret'] = this.config.apiKey;
407
- }
408
420
  if (this.config.debug) {
409
421
  debugLog('Proxy Mode: executeQuery', {
410
422
  requestType: options.requestType,
@@ -529,8 +541,8 @@ export class AxonFlow {
529
541
  async queryConnector(connectorName, query, params) {
530
542
  const agentRequest = {
531
543
  query,
532
- user_token: this.config.apiKey || '',
533
- client_id: this.config.tenant,
544
+ user_token: '',
545
+ client_id: this.config.clientId || this.config.tenant,
534
546
  request_type: 'mcp-query',
535
547
  context: {
536
548
  connector: connectorName,
@@ -540,10 +552,8 @@ export class AxonFlow {
540
552
  const url = `${this.config.endpoint}/api/request`;
541
553
  const headers = {
542
554
  'Content-Type': 'application/json',
555
+ ...this.getAuthHeaders(),
543
556
  };
544
- if (this.config.licenseKey) {
545
- headers['X-License-Key'] = this.config.licenseKey;
546
- }
547
557
  const response = await fetch(url, {
548
558
  method: 'POST',
549
559
  headers,
@@ -552,7 +562,7 @@ export class AxonFlow {
552
562
  });
553
563
  if (!response.ok) {
554
564
  const errorText = await response.text();
555
- throw new Error(`Connector query failed: ${response.status} ${response.statusText} - ${errorText}`);
565
+ throw new ConnectorError(`Connector query failed: ${response.status} ${response.statusText} - ${errorText}`, connectorName, 'query');
556
566
  }
557
567
  const agentResponse = await response.json();
558
568
  if (this.config.debug) {
@@ -565,6 +575,94 @@ export class AxonFlow {
565
575
  meta: agentResponse.metadata,
566
576
  };
567
577
  }
578
+ /**
579
+ * Execute a query directly against the MCP connector endpoint.
580
+ *
581
+ * This method calls the agent's /mcp/resources/query endpoint which provides:
582
+ * - Request-phase policy evaluation (SQLi blocking, PII blocking)
583
+ * - Response-phase policy evaluation (PII redaction)
584
+ * - PolicyInfo metadata in responses
585
+ *
586
+ * @example
587
+ * ```typescript
588
+ * const response = await axonflow.mcpQuery({
589
+ * connector: 'postgres',
590
+ * statement: 'SELECT * FROM customers LIMIT 10',
591
+ * });
592
+ *
593
+ * if (response.redacted) {
594
+ * console.log('Fields redacted:', response.redacted_fields);
595
+ * }
596
+ * console.log('Policies evaluated:', response.policy_info?.policies_evaluated);
597
+ * ```
598
+ *
599
+ * @param options - Query options including connector name and SQL statement
600
+ * @returns ConnectorResponse with data, redaction info, and policy_info
601
+ * @throws ConnectorError if the request is blocked by policy or fails
602
+ */
603
+ async mcpQuery(options) {
604
+ if (!options.connector) {
605
+ throw new ConnectorError('connector name is required', undefined, 'mcpQuery');
606
+ }
607
+ if (!options.statement) {
608
+ throw new ConnectorError('statement is required', undefined, 'mcpQuery');
609
+ }
610
+ const url = `${this.config.endpoint}/mcp/resources/query`;
611
+ const headers = {
612
+ 'Content-Type': 'application/json',
613
+ ...this.getAuthHeaders(),
614
+ };
615
+ const body = {
616
+ connector: options.connector,
617
+ statement: options.statement,
618
+ options: options.options || {},
619
+ };
620
+ if (this.config.debug) {
621
+ debugLog('MCP Query', {
622
+ connector: options.connector,
623
+ statement: options.statement.substring(0, 50),
624
+ });
625
+ }
626
+ const response = await fetch(url, {
627
+ method: 'POST',
628
+ headers,
629
+ body: JSON.stringify(body),
630
+ signal: AbortSignal.timeout(this.config.timeout),
631
+ });
632
+ const responseData = await response.json();
633
+ // Handle policy blocks (403 responses)
634
+ if (!response.ok) {
635
+ throw new ConnectorError(responseData.error || `MCP query failed: ${response.status} ${response.statusText}`, options.connector, 'mcpQuery');
636
+ }
637
+ if (this.config.debug) {
638
+ debugLog('MCP Query result', {
639
+ connector: options.connector,
640
+ success: responseData.success,
641
+ redacted: responseData.redacted,
642
+ policiesEvaluated: responseData.policy_info?.policies_evaluated,
643
+ });
644
+ }
645
+ return {
646
+ success: responseData.success,
647
+ data: responseData.data,
648
+ error: responseData.error,
649
+ meta: responseData.meta,
650
+ redacted: responseData.redacted,
651
+ redacted_fields: responseData.redacted_fields,
652
+ policy_info: responseData.policy_info,
653
+ };
654
+ }
655
+ /**
656
+ * Execute a statement against an MCP connector (alias for mcpQuery).
657
+ *
658
+ * Same as mcpQuery but follows the naming convention of other execute* methods.
659
+ *
660
+ * @param options - Query options including connector name and SQL statement
661
+ * @returns ConnectorResponse with data, redaction info, and policy_info
662
+ */
663
+ async mcpExecute(options) {
664
+ return this.mcpQuery(options);
665
+ }
568
666
  /**
569
667
  * Generate a multi-agent execution plan from a natural language query
570
668
  * @param query - Natural language query describing the task
@@ -582,10 +680,8 @@ export class AxonFlow {
582
680
  const url = `${this.config.endpoint}/api/request`;
583
681
  const headers = {
584
682
  'Content-Type': 'application/json',
683
+ ...this.getAuthHeaders(),
585
684
  };
586
- if (this.config.licenseKey) {
587
- headers['X-License-Key'] = this.config.licenseKey;
588
- }
589
685
  // Use mapTimeout for MAP operations (default 2 minutes)
590
686
  const response = await fetch(url, {
591
687
  method: 'POST',
@@ -595,11 +691,11 @@ export class AxonFlow {
595
691
  });
596
692
  if (!response.ok) {
597
693
  const errorText = await response.text();
598
- throw new Error(`Plan generation failed: ${response.status} ${response.statusText} - ${errorText}`);
694
+ throw new PlanExecutionError(`Plan generation failed: ${response.status} ${response.statusText} - ${errorText}`, undefined, 'generation');
599
695
  }
600
696
  const agentResponse = await response.json();
601
697
  if (!agentResponse.success) {
602
- throw new Error(`Plan generation failed: ${agentResponse.error}`);
698
+ throw new PlanExecutionError(`Plan generation failed: ${agentResponse.error}`, undefined, 'generation');
603
699
  }
604
700
  // plan_id can be at top level or inside data
605
701
  const planId = agentResponse.plan_id || agentResponse.data?.plan_id;
@@ -631,10 +727,8 @@ export class AxonFlow {
631
727
  const url = `${this.config.endpoint}/api/request`;
632
728
  const headers = {
633
729
  'Content-Type': 'application/json',
730
+ ...this.getAuthHeaders(),
634
731
  };
635
- if (this.config.licenseKey) {
636
- headers['X-License-Key'] = this.config.licenseKey;
637
- }
638
732
  // Use mapTimeout for MAP operations (default 2 minutes)
639
733
  const response = await fetch(url, {
640
734
  method: 'POST',
@@ -644,7 +738,7 @@ export class AxonFlow {
644
738
  });
645
739
  if (!response.ok) {
646
740
  const errorText = await response.text();
647
- throw new Error(`Plan execution failed: ${response.status} ${response.statusText} - ${errorText}`);
741
+ throw new PlanExecutionError(`Plan execution failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'execution');
648
742
  }
649
743
  const agentResponse = await response.json();
650
744
  if (this.config.debug) {
@@ -663,7 +757,7 @@ export class AxonFlow {
663
757
  * Get the status of a running or completed plan
664
758
  */
665
759
  async getPlanStatus(planId) {
666
- const url = `${this.config.endpoint}/api/plans/${planId}`;
760
+ const url = `${this.config.endpoint}/api/v1/plan/${planId}`;
667
761
  const response = await fetch(url, {
668
762
  method: 'GET',
669
763
  signal: AbortSignal.timeout(this.config.timeout),
@@ -746,15 +840,8 @@ export class AxonFlow {
746
840
  };
747
841
  const headers = {
748
842
  'Content-Type': 'application/json',
843
+ ...this.getAuthHeaders(),
749
844
  };
750
- // Add auth headers only when credentials are provided
751
- // Community/self-hosted mode works without credentials
752
- if (this.config.licenseKey) {
753
- headers['X-License-Key'] = this.config.licenseKey;
754
- }
755
- else if (this.config.apiKey) {
756
- headers['X-Client-Secret'] = this.config.apiKey;
757
- }
758
845
  if (this.config.debug) {
759
846
  debugLog('Gateway Mode: Pre-check', { query: options.query.substring(0, 50) });
760
847
  }
@@ -845,15 +932,8 @@ export class AxonFlow {
845
932
  };
846
933
  const headers = {
847
934
  'Content-Type': 'application/json',
935
+ ...this.getAuthHeaders(),
848
936
  };
849
- // Add auth headers only when credentials are provided
850
- // Community/self-hosted mode works without credentials
851
- if (this.config.licenseKey) {
852
- headers['X-License-Key'] = this.config.licenseKey;
853
- }
854
- else if (this.config.apiKey) {
855
- headers['X-Client-Secret'] = this.config.apiKey;
856
- }
857
937
  if (this.config.debug) {
858
938
  debugLog('Gateway Mode: Audit', {
859
939
  contextId: options.contextId,
@@ -1035,24 +1115,20 @@ export class AxonFlow {
1035
1115
  // Policy CRUD Methods - Static Policies
1036
1116
  // ============================================================================
1037
1117
  /**
1038
- * Build authentication headers for API requests
1118
+ * Build authentication headers for API requests.
1119
+ * Includes Content-Type and X-Org-ID for policy APIs.
1120
+ * Uses getAuthHeaders() for authentication credentials.
1039
1121
  */
1040
1122
  buildAuthHeaders() {
1041
1123
  const headers = {
1042
1124
  'Content-Type': 'application/json',
1125
+ ...this.getAuthHeaders(),
1043
1126
  };
1044
1127
  // Always include tenant ID for policy APIs (X-Org-ID header for server compatibility)
1128
+ // Note: getAuthHeaders() already adds X-Tenant-ID when tenant is non-default
1045
1129
  if (this.config.tenant) {
1046
1130
  headers['X-Org-ID'] = this.config.tenant;
1047
1131
  }
1048
- // Add auth headers only when credentials are provided
1049
- // Community/self-hosted mode works without credentials
1050
- if (this.config.licenseKey) {
1051
- headers['X-License-Key'] = this.config.licenseKey;
1052
- }
1053
- else if (this.config.apiKey) {
1054
- headers['X-Client-Secret'] = this.config.apiKey;
1055
- }
1056
1132
  return headers;
1057
1133
  }
1058
1134
  /**
@@ -1077,8 +1153,8 @@ export class AxonFlow {
1077
1153
  }
1078
1154
  throw new APIError(response.status, response.statusText, errorText);
1079
1155
  }
1080
- // Handle DELETE responses with no body
1081
- if (response.status === 204 || method === 'DELETE') {
1156
+ // Handle 204 No Content responses
1157
+ if (response.status === 204) {
1082
1158
  return undefined;
1083
1159
  }
1084
1160
  return response.json();
@@ -1432,7 +1508,7 @@ export class AxonFlow {
1432
1508
  // API returns {"policies": [...]} wrapper via Agent proxy
1433
1509
  const response = await this.orchestratorRequest('GET', path);
1434
1510
  // Handle both wrapped and unwrapped responses for compatibility
1435
- return Array.isArray(response) ? response : response.policies;
1511
+ return Array.isArray(response) ? response : response.policies || [];
1436
1512
  }
1437
1513
  /**
1438
1514
  * Get a specific dynamic policy by ID.
@@ -1540,7 +1616,7 @@ export class AxonFlow {
1540
1616
  // API returns {"policies": [...]} wrapper via Agent proxy
1541
1617
  const response = await this.orchestratorRequest('GET', path);
1542
1618
  // Handle both wrapped and unwrapped responses for compatibility
1543
- return Array.isArray(response) ? response : response.policies;
1619
+ return Array.isArray(response) ? response : response.policies || [];
1544
1620
  }
1545
1621
  // ============================================================================
1546
1622
  // Portal Authentication Methods (Enterprise)
@@ -1925,6 +2001,49 @@ export class AxonFlow {
1925
2001
  providerType: response.provider_type,
1926
2002
  };
1927
2003
  }
2004
+ /**
2005
+ * Close a PR without merging and optionally delete the branch.
2006
+ * Useful for cleaning up test PRs created by examples.
2007
+ *
2008
+ * @param prId - PR record ID
2009
+ * @param deleteBranch - Whether to delete the associated branch (default: true)
2010
+ * @returns Closed PR record
2011
+ *
2012
+ * @example
2013
+ * ```typescript
2014
+ * // Close PR and delete branch
2015
+ * const pr = await axonflow.closePR('pr_123');
2016
+ * console.log(`PR #${pr.prNumber} closed`);
2017
+ *
2018
+ * // Close PR but keep branch
2019
+ * const pr = await axonflow.closePR('pr_123', false);
2020
+ * ```
2021
+ */
2022
+ async closePR(prId, deleteBranch = true) {
2023
+ if (this.config.debug) {
2024
+ debugLog('Closing PR', { prId, deleteBranch });
2025
+ }
2026
+ const query = deleteBranch ? '?delete_branch=true' : '';
2027
+ const response = await this.portalRequest('DELETE', `/api/v1/code-governance/prs/${prId}${query}`);
2028
+ return {
2029
+ id: response.id,
2030
+ prNumber: response.pr_number,
2031
+ prUrl: response.pr_url,
2032
+ title: response.title,
2033
+ state: response.state,
2034
+ owner: response.owner,
2035
+ repo: response.repo,
2036
+ headBranch: response.head_branch,
2037
+ baseBranch: response.base_branch,
2038
+ filesCount: response.files_count,
2039
+ secretsDetected: response.secrets_detected,
2040
+ unsafePatterns: response.unsafe_patterns,
2041
+ createdAt: response.created_at,
2042
+ closedAt: response.closed_at,
2043
+ createdBy: response.created_by,
2044
+ providerType: response.provider_type,
2045
+ };
2046
+ }
1928
2047
  /**
1929
2048
  * Sync PR status with the Git provider.
1930
2049
  * This updates the local record with the current state from GitHub/GitLab/Bitbucket.
@@ -2118,8 +2237,8 @@ export class AxonFlow {
2118
2237
  }
2119
2238
  throw new APIError(response.status, response.statusText, errorText);
2120
2239
  }
2121
- // Handle DELETE responses with no body
2122
- if (response.status === 204 || method === 'DELETE') {
2240
+ // Handle 204 No Content responses
2241
+ if (response.status === 204) {
2123
2242
  return undefined;
2124
2243
  }
2125
2244
  return response.json();
@@ -2162,8 +2281,8 @@ export class AxonFlow {
2162
2281
  }
2163
2282
  throw new APIError(response.status, response.statusText, errorText);
2164
2283
  }
2165
- // Handle DELETE responses with no body
2166
- if (response.status === 204 || method === 'DELETE') {
2284
+ // Handle 204 No Content responses
2285
+ if (response.status === 204) {
2167
2286
  return undefined;
2168
2287
  }
2169
2288
  return response.json();