@axonflow/sdk 2.2.0 → 3.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 (126) hide show
  1. package/README.md +185 -8
  2. package/dist/cjs/client.d.ts +530 -8
  3. package/dist/cjs/client.d.ts.map +1 -1
  4. package/dist/cjs/client.js +1760 -45
  5. package/dist/cjs/client.js.map +1 -1
  6. package/dist/cjs/errors.d.ts +22 -0
  7. package/dist/cjs/errors.d.ts.map +1 -1
  8. package/dist/cjs/errors.js +32 -1
  9. package/dist/cjs/errors.js.map +1 -1
  10. package/dist/cjs/index.d.ts +10 -14
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +11 -20
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/types/connector.d.ts +82 -0
  15. package/dist/cjs/types/connector.d.ts.map +1 -1
  16. package/dist/cjs/types/connector.js +7 -0
  17. package/dist/cjs/types/connector.js.map +1 -1
  18. package/dist/cjs/types/execution.d.ts +227 -0
  19. package/dist/cjs/types/execution.d.ts.map +1 -0
  20. package/dist/cjs/types/execution.js +73 -0
  21. package/dist/cjs/types/execution.js.map +1 -0
  22. package/dist/cjs/types/index.d.ts +3 -0
  23. package/dist/cjs/types/index.d.ts.map +1 -1
  24. package/dist/cjs/types/index.js +3 -0
  25. package/dist/cjs/types/index.js.map +1 -1
  26. package/dist/cjs/types/masfeat.d.ts +238 -0
  27. package/dist/cjs/types/masfeat.d.ts.map +1 -0
  28. package/dist/cjs/types/masfeat.js +11 -0
  29. package/dist/cjs/types/masfeat.js.map +1 -0
  30. package/dist/cjs/types/planning.d.ts +126 -1
  31. package/dist/cjs/types/planning.d.ts.map +1 -1
  32. package/dist/cjs/types/policies.d.ts +19 -1
  33. package/dist/cjs/types/policies.d.ts.map +1 -1
  34. package/dist/cjs/types/proxy.d.ts +29 -3
  35. package/dist/cjs/types/proxy.d.ts.map +1 -1
  36. package/dist/cjs/types/workflows.d.ts +318 -0
  37. package/dist/cjs/types/workflows.d.ts.map +1 -0
  38. package/dist/cjs/types/workflows.js +61 -0
  39. package/dist/cjs/types/workflows.js.map +1 -0
  40. package/dist/esm/client.d.ts +530 -8
  41. package/dist/esm/client.d.ts.map +1 -1
  42. package/dist/esm/client.js +1761 -46
  43. package/dist/esm/client.js.map +1 -1
  44. package/dist/esm/errors.d.ts +22 -0
  45. package/dist/esm/errors.d.ts.map +1 -1
  46. package/dist/esm/errors.js +30 -0
  47. package/dist/esm/errors.js.map +1 -1
  48. package/dist/esm/index.d.ts +10 -14
  49. package/dist/esm/index.d.ts.map +1 -1
  50. package/dist/esm/index.js +7 -15
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/types/connector.d.ts +82 -0
  53. package/dist/esm/types/connector.d.ts.map +1 -1
  54. package/dist/esm/types/connector.js +6 -1
  55. package/dist/esm/types/connector.js.map +1 -1
  56. package/dist/esm/types/execution.d.ts +227 -0
  57. package/dist/esm/types/execution.d.ts.map +1 -0
  58. package/dist/esm/types/execution.js +70 -0
  59. package/dist/esm/types/execution.js.map +1 -0
  60. package/dist/esm/types/index.d.ts +3 -0
  61. package/dist/esm/types/index.d.ts.map +1 -1
  62. package/dist/esm/types/index.js +3 -0
  63. package/dist/esm/types/index.js.map +1 -1
  64. package/dist/esm/types/masfeat.d.ts +238 -0
  65. package/dist/esm/types/masfeat.d.ts.map +1 -0
  66. package/dist/esm/types/masfeat.js +10 -0
  67. package/dist/esm/types/masfeat.js.map +1 -0
  68. package/dist/esm/types/planning.d.ts +126 -1
  69. package/dist/esm/types/planning.d.ts.map +1 -1
  70. package/dist/esm/types/policies.d.ts +19 -1
  71. package/dist/esm/types/policies.d.ts.map +1 -1
  72. package/dist/esm/types/proxy.d.ts +29 -3
  73. package/dist/esm/types/proxy.d.ts.map +1 -1
  74. package/dist/esm/types/workflows.d.ts +318 -0
  75. package/dist/esm/types/workflows.d.ts.map +1 -0
  76. package/dist/esm/types/workflows.js +58 -0
  77. package/dist/esm/types/workflows.js.map +1 -0
  78. package/package.json +7 -2
  79. package/dist/cjs/interceptors/anthropic.d.ts +0 -40
  80. package/dist/cjs/interceptors/anthropic.d.ts.map +0 -1
  81. package/dist/cjs/interceptors/anthropic.js +0 -101
  82. package/dist/cjs/interceptors/anthropic.js.map +0 -1
  83. package/dist/cjs/interceptors/base.d.ts +0 -23
  84. package/dist/cjs/interceptors/base.d.ts.map +0 -1
  85. package/dist/cjs/interceptors/base.js +0 -10
  86. package/dist/cjs/interceptors/base.js.map +0 -1
  87. package/dist/cjs/interceptors/bedrock.d.ts +0 -142
  88. package/dist/cjs/interceptors/bedrock.d.ts.map +0 -1
  89. package/dist/cjs/interceptors/bedrock.js +0 -263
  90. package/dist/cjs/interceptors/bedrock.js.map +0 -1
  91. package/dist/cjs/interceptors/gemini.d.ts +0 -89
  92. package/dist/cjs/interceptors/gemini.d.ts.map +0 -1
  93. package/dist/cjs/interceptors/gemini.js +0 -121
  94. package/dist/cjs/interceptors/gemini.js.map +0 -1
  95. package/dist/cjs/interceptors/ollama.d.ts +0 -143
  96. package/dist/cjs/interceptors/ollama.d.ts.map +0 -1
  97. package/dist/cjs/interceptors/ollama.js +0 -153
  98. package/dist/cjs/interceptors/ollama.js.map +0 -1
  99. package/dist/cjs/interceptors/openai.d.ts +0 -40
  100. package/dist/cjs/interceptors/openai.d.ts.map +0 -1
  101. package/dist/cjs/interceptors/openai.js +0 -100
  102. package/dist/cjs/interceptors/openai.js.map +0 -1
  103. package/dist/esm/interceptors/anthropic.d.ts +0 -40
  104. package/dist/esm/interceptors/anthropic.d.ts.map +0 -1
  105. package/dist/esm/interceptors/anthropic.js +0 -96
  106. package/dist/esm/interceptors/anthropic.js.map +0 -1
  107. package/dist/esm/interceptors/base.d.ts +0 -23
  108. package/dist/esm/interceptors/base.d.ts.map +0 -1
  109. package/dist/esm/interceptors/base.js +0 -6
  110. package/dist/esm/interceptors/base.js.map +0 -1
  111. package/dist/esm/interceptors/bedrock.d.ts +0 -142
  112. package/dist/esm/interceptors/bedrock.d.ts.map +0 -1
  113. package/dist/esm/interceptors/bedrock.js +0 -224
  114. package/dist/esm/interceptors/bedrock.js.map +0 -1
  115. package/dist/esm/interceptors/gemini.d.ts +0 -89
  116. package/dist/esm/interceptors/gemini.d.ts.map +0 -1
  117. package/dist/esm/interceptors/gemini.js +0 -116
  118. package/dist/esm/interceptors/gemini.js.map +0 -1
  119. package/dist/esm/interceptors/ollama.d.ts +0 -143
  120. package/dist/esm/interceptors/ollama.d.ts.map +0 -1
  121. package/dist/esm/interceptors/ollama.js +0 -147
  122. package/dist/esm/interceptors/ollama.js.map +0 -1
  123. package/dist/esm/interceptors/openai.d.ts +0 -40
  124. package/dist/esm/interceptors/openai.d.ts.map +0 -1
  125. package/dist/esm/interceptors/openai.js +0 -95
  126. package/dist/esm/interceptors/openai.js.map +0 -1
@@ -1,6 +1,4 @@
1
- import { AuthenticationError, APIError, PolicyViolationError, ConfigurationError, ConnectorError, PlanExecutionError, } from './errors.js';
2
- import { OpenAIInterceptor } from './interceptors/openai.js';
3
- import { AnthropicInterceptor } from './interceptors/anthropic.js';
1
+ import { AuthenticationError, APIError, PolicyViolationError, ConfigurationError, ConnectorError, PlanExecutionError, VersionConflictError, } from './errors.js';
4
2
  import { generateRequestId, debugLog } from './utils/helpers.js';
5
3
  /**
6
4
  * Main AxonFlow client for invisible AI governance
@@ -24,7 +22,7 @@ export class AxonFlow {
24
22
  clientSecret: config.clientSecret,
25
23
  endpoint,
26
24
  mode: config.mode || (hasCredentials ? 'production' : 'sandbox'),
27
- tenant: config.tenant || 'default',
25
+ tenant: config.tenant || '',
28
26
  debug: config.debug || false,
29
27
  timeout: config.timeout || 30000,
30
28
  mapTimeout: config.mapTimeout || 120000, // 2 minutes for MAP operations
@@ -38,8 +36,8 @@ export class AxonFlow {
38
36
  ttl: config.cache?.ttl || 60000,
39
37
  },
40
38
  };
41
- // Initialize interceptors
42
- this.interceptors = [new OpenAIInterceptor(), new AnthropicInterceptor()];
39
+ // Interceptors removed in v3.0.0 (deprecated wrapOpenAIClient/wrapAnthropicClient)
40
+ this.interceptors = [];
43
41
  if (this.config.debug) {
44
42
  // Determine auth method for logging
45
43
  const authMethod = hasCredentials ? 'client-credentials' : 'community (no auth)';
@@ -64,10 +62,25 @@ export class AxonFlow {
64
62
  if (this.config.clientId && this.config.clientSecret) {
65
63
  const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
66
64
  headers['Authorization'] = `Basic ${credentials}`;
65
+ }
66
+ // Always add X-Tenant-ID when clientId is set (required for multi-tenant APIs)
67
+ if (this.config.clientId) {
67
68
  headers['X-Tenant-ID'] = this.config.clientId;
68
69
  }
69
70
  return headers;
70
71
  }
72
+ /**
73
+ * Get the effective clientId, using smart default for community mode.
74
+ *
75
+ * Returns the configured clientId if set, otherwise returns "community"
76
+ * as a smart default. This enables zero-config usage for community/self-hosted
77
+ * deployments while still supporting enterprise deployments with explicit credentials.
78
+ *
79
+ * @returns The clientId to use in requests
80
+ */
81
+ getEffectiveClientId() {
82
+ return this.config.clientId || this.config.tenant || 'community';
83
+ }
71
84
  /**
72
85
  * Main method to protect AI calls with governance
73
86
  * @param aiCall The AI call to protect
@@ -105,7 +118,7 @@ export class AxonFlow {
105
118
  *
106
119
  * **Proxy Mode:**
107
120
  * ```typescript
108
- * const response = await axonflow.executeQuery({
121
+ * const response = await axonflow.proxyLLMCall({
109
122
  * userToken: 'user-123',
110
123
  * query: 'Your prompt here',
111
124
  * requestType: 'chat'
@@ -115,8 +128,8 @@ export class AxonFlow {
115
128
  * See: https://docs.getaxonflow.com/sdk/gateway-mode
116
129
  */
117
130
  async protect(aiCall) {
118
- console.warn('[AxonFlow] protect() is deprecated and will be removed in v2.0.0. ' +
119
- 'Use Gateway Mode (getPolicyApprovedContext + auditLLMCall) or Proxy Mode (executeQuery) instead. ' +
131
+ console.warn('[AxonFlow] protect() is deprecated and will be removed in a future version. ' +
132
+ 'Use Gateway Mode (getPolicyApprovedContext + auditLLMCall) or Proxy Mode (proxyLLMCall) instead. ' +
120
133
  'See: https://docs.getaxonflow.com/sdk/gateway-mode');
121
134
  try {
122
135
  // Extract request details from the AI call
@@ -377,10 +390,19 @@ export class AxonFlow {
377
390
  }
378
391
  }
379
392
  /**
380
- * Execute a query through AxonFlow with policy enforcement (Proxy Mode).
393
+ * Send a query through AxonFlow with full policy enforcement (Proxy Mode).
394
+ *
395
+ * This is Proxy Mode - AxonFlow acts as an intermediary, making the LLM call on your behalf.
381
396
  *
382
- * This is the primary method for Proxy Mode, where AxonFlow handles policy
383
- * checking and optionally routes requests to LLM providers.
397
+ * Use this when you want AxonFlow to:
398
+ * - Evaluate policies before the LLM call
399
+ * - Make the LLM call to the configured provider
400
+ * - Filter/redact sensitive data from responses
401
+ * - Automatically track costs and audit the interaction
402
+ *
403
+ * For Gateway Mode (lower latency, you make the LLM call), use:
404
+ * - getPolicyApprovedContext() before your LLM call
405
+ * - auditLLMCall() after your LLM call
384
406
  *
385
407
  * @param options - Query execution options
386
408
  * @returns ExecuteQueryResponse with results or error information
@@ -390,7 +412,7 @@ export class AxonFlow {
390
412
  *
391
413
  * @example
392
414
  * ```typescript
393
- * const response = await axonflow.executeQuery({
415
+ * const response = await axonflow.proxyLLMCall({
394
416
  * userToken: 'user-123',
395
417
  * query: 'Explain quantum computing',
396
418
  * requestType: 'chat',
@@ -402,13 +424,13 @@ export class AxonFlow {
402
424
  * }
403
425
  * ```
404
426
  */
405
- async executeQuery(options) {
427
+ async proxyLLMCall(options) {
406
428
  // Default to "anonymous" if userToken is empty/undefined (community mode)
407
429
  const effectiveUserToken = options.userToken || 'anonymous';
408
430
  const agentRequest = {
409
431
  query: options.query,
410
432
  user_token: effectiveUserToken,
411
- client_id: this.config.tenant,
433
+ client_id: this.config.clientId || this.config.tenant,
412
434
  request_type: options.requestType,
413
435
  context: options.context || {},
414
436
  };
@@ -418,7 +440,7 @@ export class AxonFlow {
418
440
  ...this.getAuthHeaders(),
419
441
  };
420
442
  if (this.config.debug) {
421
- debugLog('Proxy Mode: executeQuery', {
443
+ debugLog('Proxy Mode: proxyLLMCall', {
422
444
  requestType: options.requestType,
423
445
  query: options.query.substring(0, 50),
424
446
  });
@@ -429,9 +451,28 @@ export class AxonFlow {
429
451
  body: JSON.stringify(agentRequest),
430
452
  signal: AbortSignal.timeout(this.config.timeout),
431
453
  });
454
+ let data;
432
455
  if (!response.ok) {
433
456
  const errorText = await response.text();
434
- if (response.status === 401 || response.status === 403) {
457
+ // Handle HTTP 402 (Payment Required) for budget exceeded - parse as blocked response with budgetInfo
458
+ if (response.status === 402) {
459
+ try {
460
+ data = JSON.parse(errorText);
461
+ // If it has budget_info, treat as valid blocked response (fall through to normal processing)
462
+ if (data.budget_info) {
463
+ // Fall through to normal response processing below
464
+ }
465
+ else {
466
+ throw new APIError(response.status, 'Payment Required', errorText);
467
+ }
468
+ }
469
+ catch (e) {
470
+ if (e instanceof APIError)
471
+ throw e;
472
+ throw new APIError(response.status, 'Payment Required', errorText);
473
+ }
474
+ }
475
+ else if (response.status === 401 || response.status === 403) {
435
476
  // Try to parse as JSON for policy violation info
436
477
  try {
437
478
  const errorJson = JSON.parse(errorText);
@@ -445,11 +486,17 @@ export class AxonFlow {
445
486
  }
446
487
  throw new AuthenticationError(`Request failed: ${errorText}`);
447
488
  }
448
- throw new APIError(response.status, response.statusText, errorText);
489
+ else {
490
+ throw new APIError(response.status, response.statusText, errorText);
491
+ }
492
+ }
493
+ // Parse response if not already parsed (from 402 handling)
494
+ if (!data) {
495
+ data = await response.json();
449
496
  }
450
- const data = await response.json();
451
497
  // Check for policy violation in successful response (some blocked responses return 200)
452
- if (data.blocked) {
498
+ // Note: Don't throw for budget blocks (402 responses) - return with budgetInfo instead
499
+ if (data.blocked && !data.budget_info) {
453
500
  throw new PolicyViolationError(data.block_reason || 'Request blocked by policy', data.policy_info?.policies_evaluated);
454
501
  }
455
502
  // Transform snake_case response to camelCase
@@ -474,8 +521,20 @@ export class AxonFlow {
474
521
  codeArtifact: data.policy_info.code_artifact,
475
522
  };
476
523
  }
524
+ // Parse budget info if present (Issue #1082)
525
+ if (data.budget_info) {
526
+ result.budgetInfo = {
527
+ budgetId: data.budget_info.budget_id,
528
+ budgetName: data.budget_info.budget_name,
529
+ usedUsd: data.budget_info.used_usd || 0,
530
+ limitUsd: data.budget_info.limit_usd || 0,
531
+ percentage: data.budget_info.percentage || 0,
532
+ exceeded: data.budget_info.exceeded || false,
533
+ action: data.budget_info.action,
534
+ };
535
+ }
477
536
  if (this.config.debug) {
478
- debugLog('Proxy Mode: executeQuery result', {
537
+ debugLog('Proxy Mode: proxyLLMCall result', {
479
538
  success: result.success,
480
539
  blocked: result.blocked,
481
540
  hasData: !!result.data,
@@ -575,19 +634,115 @@ export class AxonFlow {
575
634
  meta: agentResponse.metadata,
576
635
  };
577
636
  }
637
+ /**
638
+ * Execute a query directly against the MCP connector endpoint.
639
+ *
640
+ * This method calls the agent's /mcp/resources/query endpoint which provides:
641
+ * - Request-phase policy evaluation (SQLi blocking, PII blocking)
642
+ * - Response-phase policy evaluation (PII redaction)
643
+ * - PolicyInfo metadata in responses
644
+ *
645
+ * @example
646
+ * ```typescript
647
+ * const response = await axonflow.mcpQuery({
648
+ * connector: 'postgres',
649
+ * statement: 'SELECT * FROM customers LIMIT 10',
650
+ * });
651
+ *
652
+ * if (response.redacted) {
653
+ * console.log('Fields redacted:', response.redacted_fields);
654
+ * }
655
+ * console.log('Policies evaluated:', response.policy_info?.policies_evaluated);
656
+ * ```
657
+ *
658
+ * @param options - Query options including connector name and SQL statement
659
+ * @returns ConnectorResponse with data, redaction info, and policy_info
660
+ * @throws ConnectorError if the request is blocked by policy or fails
661
+ */
662
+ async mcpQuery(options) {
663
+ if (!options.connector) {
664
+ throw new ConnectorError('connector name is required', undefined, 'mcpQuery');
665
+ }
666
+ if (!options.statement) {
667
+ throw new ConnectorError('statement is required', undefined, 'mcpQuery');
668
+ }
669
+ const url = `${this.config.endpoint}/mcp/resources/query`;
670
+ const headers = {
671
+ 'Content-Type': 'application/json',
672
+ ...this.getAuthHeaders(),
673
+ };
674
+ const body = {
675
+ connector: options.connector,
676
+ statement: options.statement,
677
+ options: options.options || {},
678
+ };
679
+ if (this.config.debug) {
680
+ debugLog('MCP Query', {
681
+ connector: options.connector,
682
+ statement: options.statement.substring(0, 50),
683
+ });
684
+ }
685
+ const response = await fetch(url, {
686
+ method: 'POST',
687
+ headers,
688
+ body: JSON.stringify(body),
689
+ signal: AbortSignal.timeout(this.config.timeout),
690
+ });
691
+ const responseData = await response.json();
692
+ // Handle policy blocks (403 responses)
693
+ if (!response.ok) {
694
+ throw new ConnectorError(responseData.error || `MCP query failed: ${response.status} ${response.statusText}`, options.connector, 'mcpQuery');
695
+ }
696
+ if (this.config.debug) {
697
+ debugLog('MCP Query result', {
698
+ connector: options.connector,
699
+ success: responseData.success,
700
+ redacted: responseData.redacted,
701
+ policiesEvaluated: responseData.policy_info?.policies_evaluated,
702
+ });
703
+ }
704
+ return {
705
+ success: responseData.success,
706
+ data: responseData.data,
707
+ error: responseData.error,
708
+ meta: responseData.meta,
709
+ redacted: responseData.redacted,
710
+ redacted_fields: responseData.redacted_fields,
711
+ policy_info: responseData.policy_info,
712
+ };
713
+ }
714
+ /**
715
+ * Execute a statement against an MCP connector (alias for mcpQuery).
716
+ *
717
+ * Same as mcpQuery but follows the naming convention of other execute* methods.
718
+ *
719
+ * @param options - Query options including connector name and SQL statement
720
+ * @returns ConnectorResponse with data, redaction info, and policy_info
721
+ */
722
+ async mcpExecute(options) {
723
+ return this.mcpQuery(options);
724
+ }
578
725
  /**
579
726
  * Generate a multi-agent execution plan from a natural language query
580
727
  * @param query - Natural language query describing the task
581
728
  * @param domain - Optional domain hint (travel, healthcare, etc.)
582
729
  * @param userToken - Optional user token for authentication (defaults to tenant/client_id)
730
+ * @param options - Optional plan generation options (execution mode, etc.)
583
731
  */
584
- async generatePlan(query, domain, userToken) {
732
+ async generatePlan(query, domain, userToken, options) {
733
+ const context = {};
734
+ if (domain) {
735
+ context.domain = domain;
736
+ }
737
+ if (options?.executionMode) {
738
+ context.execution_mode = options.executionMode;
739
+ }
585
740
  const agentRequest = {
586
741
  query,
587
- user_token: userToken || this.config.tenant,
588
- client_id: this.config.tenant,
742
+ user_token: userToken || this.config.clientId || this.config.tenant,
743
+ client_id: this.config.clientId || this.config.tenant,
589
744
  request_type: 'multi-agent-plan',
590
- context: domain ? { domain } : {},
745
+ context,
591
746
  };
592
747
  const url = `${this.config.endpoint}/api/request`;
593
748
  const headers = {
@@ -631,8 +786,8 @@ export class AxonFlow {
631
786
  async executePlan(planId, userToken) {
632
787
  const agentRequest = {
633
788
  query: '',
634
- user_token: userToken || this.config.tenant,
635
- client_id: this.config.tenant,
789
+ user_token: userToken || this.config.clientId || this.config.tenant,
790
+ client_id: this.config.clientId || this.config.tenant,
636
791
  request_type: 'execute-plan',
637
792
  context: { plan_id: planId },
638
793
  };
@@ -653,16 +808,43 @@ export class AxonFlow {
653
808
  throw new PlanExecutionError(`Plan execution failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'execution');
654
809
  }
655
810
  const agentResponse = await response.json();
811
+ // Detect nested data.success=false (agent wraps orchestrator errors)
812
+ let success = agentResponse.success;
813
+ let error = agentResponse.error;
814
+ let result = agentResponse.result;
815
+ const data = agentResponse.data;
816
+ if (data && typeof data === 'object' && data.success === false) {
817
+ success = false;
818
+ if (data.error && !error)
819
+ error = data.error;
820
+ // Throw on nested failure (e.g., cancelled plan execution)
821
+ throw new PlanExecutionError(error || 'Plan execution failed', planId, 'execution');
822
+ }
823
+ if (!result && data?.result)
824
+ result = data.result;
656
825
  if (this.config.debug) {
657
- debugLog('Plan executed', { planId, success: agentResponse.success });
826
+ debugLog('Plan executed', { planId, success });
827
+ }
828
+ // Read status from response data if available (e.g., "awaiting_approval" for confirm mode)
829
+ let status = success ? 'completed' : 'failed';
830
+ if (data &&
831
+ typeof data === 'object' &&
832
+ 'status' in data &&
833
+ typeof data.status === 'string' &&
834
+ data.status) {
835
+ status = data.status;
836
+ }
837
+ else if (agentResponse.metadata?.status) {
838
+ status = agentResponse.metadata.status;
658
839
  }
659
840
  return {
660
841
  planId,
661
- status: agentResponse.success ? 'completed' : 'failed',
662
- result: agentResponse.result,
663
- stepResults: agentResponse.metadata?.step_results,
664
- error: agentResponse.error,
665
- duration: agentResponse.metadata?.duration,
842
+ status,
843
+ result,
844
+ workflowId: data?.workflow_id,
845
+ stepResults: agentResponse.metadata?.step_results ?? data?.metadata?.step_results,
846
+ error,
847
+ duration: agentResponse.metadata?.duration ?? data?.metadata?.duration,
666
848
  };
667
849
  }
668
850
  /**
@@ -688,6 +870,150 @@ export class AxonFlow {
688
870
  duration: status.duration,
689
871
  };
690
872
  }
873
+ /**
874
+ * Cancel a running or pending plan
875
+ * @param planId - ID of the plan to cancel
876
+ * @param reason - Optional reason for cancellation
877
+ */
878
+ async cancelPlan(planId, reason) {
879
+ const url = `${this.config.endpoint}/api/v1/plan/${planId}/cancel`;
880
+ const headers = {
881
+ 'Content-Type': 'application/json',
882
+ ...this.getAuthHeaders(),
883
+ };
884
+ const body = {};
885
+ if (reason) {
886
+ body.reason = reason;
887
+ }
888
+ const response = await fetch(url, {
889
+ method: 'POST',
890
+ headers,
891
+ body: JSON.stringify(body),
892
+ signal: AbortSignal.timeout(this.config.mapTimeout),
893
+ });
894
+ if (!response.ok) {
895
+ const errorText = await response.text();
896
+ throw new PlanExecutionError(`Plan cancellation failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'cancel');
897
+ }
898
+ const data = await response.json();
899
+ if (this.config.debug) {
900
+ debugLog('Plan cancelled', { planId, status: data.status });
901
+ }
902
+ return {
903
+ planId: data.plan_id || planId,
904
+ status: data.status,
905
+ message: data.message,
906
+ };
907
+ }
908
+ /**
909
+ * Update a plan with optimistic concurrency control.
910
+ * Throws VersionConflictError on 409 (version mismatch).
911
+ * @param planId - ID of the plan to update
912
+ * @param request - Update request with version and fields to change
913
+ */
914
+ async updatePlan(planId, request) {
915
+ const url = `${this.config.endpoint}/api/v1/plan/${planId}`;
916
+ const headers = {
917
+ 'Content-Type': 'application/json',
918
+ ...this.getAuthHeaders(),
919
+ };
920
+ const body = {
921
+ version: request.version,
922
+ };
923
+ if (request.executionMode) {
924
+ body.execution_mode = request.executionMode;
925
+ }
926
+ if (request.domain) {
927
+ body.domain = request.domain;
928
+ }
929
+ const response = await fetch(url, {
930
+ method: 'PUT',
931
+ headers,
932
+ body: JSON.stringify(body),
933
+ signal: AbortSignal.timeout(this.config.mapTimeout),
934
+ });
935
+ if (response.status === 409) {
936
+ const errorData = await response.json().catch(() => ({}));
937
+ throw new VersionConflictError(planId, request.version, errorData.current_version);
938
+ }
939
+ if (!response.ok) {
940
+ const errorText = await response.text();
941
+ throw new PlanExecutionError(`Plan update failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'update');
942
+ }
943
+ const data = await response.json();
944
+ if (this.config.debug) {
945
+ debugLog('Plan updated', { planId, version: data.version });
946
+ }
947
+ return {
948
+ planId: data.plan_id || planId,
949
+ version: data.version,
950
+ status: data.status,
951
+ success: data.success ?? true,
952
+ };
953
+ }
954
+ /**
955
+ * Get version history for a plan
956
+ * @param planId - ID of the plan
957
+ */
958
+ async getPlanVersions(planId) {
959
+ const url = `${this.config.endpoint}/api/v1/plan/${planId}/versions`;
960
+ const headers = {
961
+ ...this.getAuthHeaders(),
962
+ };
963
+ const response = await fetch(url, {
964
+ method: 'GET',
965
+ headers,
966
+ signal: AbortSignal.timeout(this.config.timeout),
967
+ });
968
+ if (!response.ok) {
969
+ const errorText = await response.text();
970
+ throw new PlanExecutionError(`Get plan versions failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'versions');
971
+ }
972
+ const data = await response.json();
973
+ const versions = (data.versions || []).map((v) => ({
974
+ version: v.version,
975
+ changedAt: v.changed_at,
976
+ changedBy: v.changed_by,
977
+ changeType: v.change_type,
978
+ changeSummary: v.change_summary,
979
+ }));
980
+ return {
981
+ planId: data.plan_id || planId,
982
+ versions,
983
+ };
984
+ }
985
+ /**
986
+ * Resume a paused plan (e.g., after approval gate or confirm mode)
987
+ * @param planId - ID of the plan to resume
988
+ * @param approved - Whether the plan is approved to proceed (defaults to true)
989
+ */
990
+ async resumePlan(planId, approved) {
991
+ const url = `${this.config.endpoint}/api/v1/plan/${planId}/resume`;
992
+ const headers = {
993
+ 'Content-Type': 'application/json',
994
+ ...this.getAuthHeaders(),
995
+ };
996
+ const response = await fetch(url, {
997
+ method: 'POST',
998
+ headers,
999
+ body: JSON.stringify({ approved: approved ?? true }),
1000
+ signal: AbortSignal.timeout(this.config.mapTimeout),
1001
+ });
1002
+ if (!response.ok) {
1003
+ const errorText = await response.text();
1004
+ throw new PlanExecutionError(`Plan resume failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'resume');
1005
+ }
1006
+ const data = await response.json();
1007
+ if (this.config.debug) {
1008
+ debugLog('Plan resumed', { planId, approved: data.approved });
1009
+ }
1010
+ return {
1011
+ planId: data.plan_id || planId,
1012
+ status: data.status,
1013
+ approved: data.approved,
1014
+ message: data.message,
1015
+ };
1016
+ }
691
1017
  // ============================================================================
692
1018
  // Gateway Mode Methods
693
1019
  // ============================================================================
@@ -740,12 +1066,12 @@ export class AxonFlow {
740
1066
  * ```
741
1067
  */
742
1068
  async getPolicyApprovedContext(options) {
743
- // Gateway Mode - credentials optional for community/self-hosted mode
744
- // Server decides whether to require authentication based on DEPLOYMENT_MODE
1069
+ // Use smart default for clientId - enables zero-config community mode
1070
+ const clientId = this.getEffectiveClientId();
745
1071
  const url = `${this.config.endpoint}/api/policy/pre-check`;
746
1072
  const requestBody = {
747
1073
  user_token: options.userToken,
748
- client_id: this.config.tenant,
1074
+ client_id: clientId,
749
1075
  query: options.query,
750
1076
  data_sources: options.dataSources || [],
751
1077
  context: options.context || {},
@@ -825,12 +1151,12 @@ export class AxonFlow {
825
1151
  * ```
826
1152
  */
827
1153
  async auditLLMCall(options) {
828
- // Gateway Mode - credentials optional for community/self-hosted mode
829
- // Server decides whether to require authentication based on DEPLOYMENT_MODE
1154
+ // Use smart default for clientId - enables zero-config community mode
1155
+ const clientId = this.getEffectiveClientId();
830
1156
  const url = `${this.config.endpoint}/api/audit/llm-call`;
831
1157
  const requestBody = {
832
1158
  context_id: options.contextId,
833
- client_id: this.config.tenant,
1159
+ client_id: clientId,
834
1160
  response_summary: options.responseSummary,
835
1161
  provider: options.provider,
836
1162
  model: options.model,
@@ -1036,11 +1362,9 @@ export class AxonFlow {
1036
1362
  'Content-Type': 'application/json',
1037
1363
  ...this.getAuthHeaders(),
1038
1364
  };
1039
- // Always include tenant ID for policy APIs (X-Org-ID header for server compatibility)
1040
- // Note: getAuthHeaders() already adds X-Tenant-ID when tenant is non-default
1041
- if (this.config.tenant) {
1042
- headers['X-Org-ID'] = this.config.tenant;
1043
- }
1365
+ // Note: X-Tenant-ID is set by getAuthHeaders() from clientId
1366
+ // Do NOT set X-Org-ID here - the server derives org from tenant context
1367
+ // Setting X-Org-ID to 'default' breaks budget queries which expect org_id to match client.OrgID
1044
1368
  return headers;
1045
1369
  }
1046
1370
  /**
@@ -1400,6 +1724,10 @@ export class AxonFlow {
1400
1724
  const params = new URLSearchParams();
1401
1725
  if (options?.type)
1402
1726
  params.set('type', options.type);
1727
+ if (options?.tier)
1728
+ params.set('tier', options.tier);
1729
+ if (options?.organizationId)
1730
+ params.set('organization_id', options.organizationId);
1403
1731
  if (options?.enabled !== undefined)
1404
1732
  params.set('enabled', String(options.enabled));
1405
1733
  if (options?.limit)
@@ -1460,8 +1788,27 @@ export class AxonFlow {
1460
1788
  if (this.config.debug) {
1461
1789
  debugLog('Creating dynamic policy', { name: policy.name });
1462
1790
  }
1791
+ // Convert camelCase to snake_case for API compatibility
1792
+ const requestBody = {
1793
+ name: policy.name,
1794
+ type: policy.type,
1795
+ conditions: policy.conditions,
1796
+ actions: policy.actions,
1797
+ };
1798
+ if (policy.description)
1799
+ requestBody.description = policy.description;
1800
+ if (policy.category)
1801
+ requestBody.category = policy.category;
1802
+ if (policy.priority !== undefined)
1803
+ requestBody.priority = policy.priority;
1804
+ if (policy.enabled !== undefined)
1805
+ requestBody.enabled = policy.enabled;
1806
+ requestBody.tier = policy.tier || 'tenant';
1807
+ if (policy.organizationId) {
1808
+ requestBody.organization_id = policy.organizationId;
1809
+ }
1463
1810
  // API returns {"policy": {...}} wrapper via Agent proxy
1464
- const response = await this.orchestratorRequest('POST', '/api/v1/dynamic-policies', policy);
1811
+ const response = await this.orchestratorRequest('POST', '/api/v1/dynamic-policies', requestBody);
1465
1812
  // Handle both wrapped and unwrapped responses for compatibility
1466
1813
  return 'policy' in response ? response.policy : response;
1467
1814
  }
@@ -1476,8 +1823,30 @@ export class AxonFlow {
1476
1823
  if (this.config.debug) {
1477
1824
  debugLog('Updating dynamic policy', { id, updates: Object.keys(policy) });
1478
1825
  }
1826
+ // Convert camelCase to snake_case for API compatibility
1827
+ const requestBody = {};
1828
+ if (policy.name !== undefined)
1829
+ requestBody.name = policy.name;
1830
+ if (policy.description !== undefined)
1831
+ requestBody.description = policy.description;
1832
+ if (policy.type !== undefined)
1833
+ requestBody.type = policy.type;
1834
+ if (policy.category !== undefined)
1835
+ requestBody.category = policy.category;
1836
+ if (policy.tier !== undefined)
1837
+ requestBody.tier = policy.tier;
1838
+ if (policy.organizationId !== undefined)
1839
+ requestBody.organization_id = policy.organizationId;
1840
+ if (policy.conditions !== undefined)
1841
+ requestBody.conditions = policy.conditions;
1842
+ if (policy.actions !== undefined)
1843
+ requestBody.actions = policy.actions;
1844
+ if (policy.priority !== undefined)
1845
+ requestBody.priority = policy.priority;
1846
+ if (policy.enabled !== undefined)
1847
+ requestBody.enabled = policy.enabled;
1479
1848
  // API returns {"policy": {...}} wrapper via Agent proxy
1480
- const response = await this.orchestratorRequest('PUT', `/api/v1/dynamic-policies/${id}`, policy);
1849
+ const response = await this.orchestratorRequest('PUT', `/api/v1/dynamic-policies/${id}`, requestBody);
1481
1850
  // Handle both wrapped and unwrapped responses for compatibility
1482
1851
  return 'policy' in response ? response.policy : response;
1483
1852
  }
@@ -2786,5 +3155,1351 @@ export class AxonFlow {
2786
3155
  }
2787
3156
  return response.text();
2788
3157
  }
3158
+ // =============================================================================
3159
+ // Workflow Control Plane (Issue #834)
3160
+ // =============================================================================
3161
+ /**
3162
+ * Create a new workflow for governance tracking.
3163
+ *
3164
+ * Call this at the start of your external orchestrator workflow (LangChain, LangGraph, CrewAI, etc.)
3165
+ * to register it with AxonFlow for governance tracking.
3166
+ *
3167
+ * @example
3168
+ * ```typescript
3169
+ * const workflow = await client.createWorkflow({
3170
+ * workflow_name: 'customer-support-agent',
3171
+ * source: 'langgraph',
3172
+ * total_steps: 5,
3173
+ * metadata: { customer_id: 'cust-123' }
3174
+ * });
3175
+ * console.log(`Workflow created: ${workflow.workflow_id}`);
3176
+ * ```
3177
+ */
3178
+ async createWorkflow(request) {
3179
+ const response = await this.orchestratorRequest('POST', '/api/v1/workflows', request);
3180
+ return response;
3181
+ }
3182
+ /**
3183
+ * Get the status of a workflow.
3184
+ *
3185
+ * @example
3186
+ * ```typescript
3187
+ * const status = await client.getWorkflow('wf_123');
3188
+ * console.log(`Status: ${status.status}, Step: ${status.current_step_index}`);
3189
+ * ```
3190
+ */
3191
+ async getWorkflow(workflowId) {
3192
+ if (!workflowId) {
3193
+ throw new ConfigurationError('Workflow ID is required');
3194
+ }
3195
+ const response = await this.orchestratorRequest('GET', `/api/v1/workflows/${workflowId}`);
3196
+ return response;
3197
+ }
3198
+ /**
3199
+ * Check if a workflow step is allowed to proceed (step gate).
3200
+ *
3201
+ * This is the core governance method. Call this before executing each step
3202
+ * in your workflow to check if the step is allowed based on policies.
3203
+ *
3204
+ * @example
3205
+ * ```typescript
3206
+ * const gate = await client.stepGate('wf_123', 'step-generate-code', {
3207
+ * step_name: 'Generate Code',
3208
+ * step_type: 'llm_call',
3209
+ * model: 'gpt-4',
3210
+ * provider: 'openai',
3211
+ * step_input: { prompt: 'Generate a hello world function' }
3212
+ * });
3213
+ *
3214
+ * if (gate.decision === 'block') {
3215
+ * throw new Error(`Step blocked: ${gate.reason}`);
3216
+ * }
3217
+ * if (gate.decision === 'require_approval') {
3218
+ * console.log(`Approval required: ${gate.approval_url}`);
3219
+ * return;
3220
+ * }
3221
+ * // Step is allowed, proceed with execution
3222
+ * ```
3223
+ */
3224
+ async stepGate(workflowId, stepId, request) {
3225
+ if (!workflowId) {
3226
+ throw new ConfigurationError('Workflow ID is required');
3227
+ }
3228
+ if (!stepId) {
3229
+ throw new ConfigurationError('Step ID is required');
3230
+ }
3231
+ const response = await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/steps/${stepId}/gate`, request);
3232
+ return response;
3233
+ }
3234
+ /**
3235
+ * Complete a workflow successfully.
3236
+ *
3237
+ * Call this when your workflow has completed all steps successfully.
3238
+ *
3239
+ * @example
3240
+ * ```typescript
3241
+ * await client.completeWorkflow('wf_123');
3242
+ * ```
3243
+ */
3244
+ async completeWorkflow(workflowId) {
3245
+ if (!workflowId) {
3246
+ throw new ConfigurationError('Workflow ID is required');
3247
+ }
3248
+ await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/complete`, {});
3249
+ }
3250
+ /**
3251
+ * Abort a workflow.
3252
+ *
3253
+ * Call this when you need to stop a workflow due to an error or user request.
3254
+ *
3255
+ * @example
3256
+ * ```typescript
3257
+ * await client.abortWorkflow('wf_123', 'User cancelled the operation');
3258
+ * ```
3259
+ */
3260
+ async abortWorkflow(workflowId, reason) {
3261
+ if (!workflowId) {
3262
+ throw new ConfigurationError('Workflow ID is required');
3263
+ }
3264
+ const request = reason ? { reason } : {};
3265
+ await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/abort`, request);
3266
+ }
3267
+ /**
3268
+ * Mark a workflow step as completed.
3269
+ *
3270
+ * Call this after a step has been executed successfully.
3271
+ *
3272
+ * @example
3273
+ * ```typescript
3274
+ * await client.markStepCompleted('wf_123', 'step-1', {
3275
+ * output: { result: 'success' }
3276
+ * });
3277
+ * ```
3278
+ */
3279
+ async markStepCompleted(workflowId, stepId, request) {
3280
+ if (!workflowId) {
3281
+ throw new ConfigurationError('Workflow ID is required');
3282
+ }
3283
+ if (!stepId) {
3284
+ throw new ConfigurationError('Step ID is required');
3285
+ }
3286
+ await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/steps/${stepId}/complete`, request || {});
3287
+ }
3288
+ /**
3289
+ * Resume a workflow after approval.
3290
+ *
3291
+ * Call this after a step has been approved to continue the workflow.
3292
+ *
3293
+ * @example
3294
+ * ```typescript
3295
+ * // After approval received via webhook or polling
3296
+ * await client.resumeWorkflow('wf_123');
3297
+ * ```
3298
+ */
3299
+ async resumeWorkflow(workflowId) {
3300
+ if (!workflowId) {
3301
+ throw new ConfigurationError('Workflow ID is required');
3302
+ }
3303
+ await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/resume`, {});
3304
+ }
3305
+ /**
3306
+ * List workflows with optional filters.
3307
+ *
3308
+ * @example
3309
+ * ```typescript
3310
+ * const result = await client.listWorkflows({
3311
+ * status: 'in_progress',
3312
+ * source: 'langgraph',
3313
+ * limit: 10
3314
+ * });
3315
+ * console.log(`Found ${result.total} workflows`);
3316
+ * ```
3317
+ */
3318
+ async listWorkflows(options) {
3319
+ const params = new URLSearchParams();
3320
+ if (options?.status) {
3321
+ params.set('status', options.status);
3322
+ }
3323
+ if (options?.source) {
3324
+ params.set('source', options.source);
3325
+ }
3326
+ if (options?.limit !== undefined) {
3327
+ params.set('limit', options.limit.toString());
3328
+ }
3329
+ if (options?.offset !== undefined) {
3330
+ params.set('offset', options.offset.toString());
3331
+ }
3332
+ const queryString = params.toString();
3333
+ const path = queryString ? `/api/v1/workflows?${queryString}` : '/api/v1/workflows';
3334
+ const response = await this.orchestratorRequest('GET', path);
3335
+ return response;
3336
+ }
3337
+ // =============================================================================
3338
+ // WCP Approval Methods (Feature 5)
3339
+ // =============================================================================
3340
+ /**
3341
+ * Approve a workflow step that requires human approval.
3342
+ *
3343
+ * Call this to approve a step that was gated with a 'require_approval' decision.
3344
+ *
3345
+ * @param workflowId - ID of the workflow
3346
+ * @param stepId - ID of the step to approve
3347
+ * @returns Approval response with status
3348
+ *
3349
+ * @example
3350
+ * ```typescript
3351
+ * const result = await client.approveStep('wf_123', 'step_456');
3352
+ * console.log(`Step ${result.step_id} status: ${result.status}`);
3353
+ * ```
3354
+ */
3355
+ async approveStep(workflowId, stepId) {
3356
+ if (!workflowId) {
3357
+ throw new ConfigurationError('Workflow ID is required');
3358
+ }
3359
+ if (!stepId) {
3360
+ throw new ConfigurationError('Step ID is required');
3361
+ }
3362
+ return this.orchestratorRequest('POST', `/api/v1/workflow-control/${workflowId}/steps/${stepId}/approve`, {});
3363
+ }
3364
+ /**
3365
+ * Reject a workflow step that requires human approval.
3366
+ *
3367
+ * Call this to reject a step that was gated with a 'require_approval' decision.
3368
+ *
3369
+ * @param workflowId - ID of the workflow
3370
+ * @param stepId - ID of the step to reject
3371
+ * @param reason - Optional reason for rejection
3372
+ * @returns Rejection response with status
3373
+ *
3374
+ * @example
3375
+ * ```typescript
3376
+ * const result = await client.rejectStep('wf_123', 'step_456', 'Policy violation detected');
3377
+ * console.log(`Step ${result.step_id} status: ${result.status}`);
3378
+ * ```
3379
+ */
3380
+ async rejectStep(workflowId, stepId, reason) {
3381
+ if (!workflowId) {
3382
+ throw new ConfigurationError('Workflow ID is required');
3383
+ }
3384
+ if (!stepId) {
3385
+ throw new ConfigurationError('Step ID is required');
3386
+ }
3387
+ const body = {};
3388
+ if (reason) {
3389
+ body.reason = reason;
3390
+ }
3391
+ return this.orchestratorRequest('POST', `/api/v1/workflow-control/${workflowId}/steps/${stepId}/reject`, body);
3392
+ }
3393
+ /**
3394
+ * Get pending approvals for workflow steps.
3395
+ *
3396
+ * Lists all steps that are waiting for human approval across all workflows.
3397
+ *
3398
+ * @param options - Optional filtering options
3399
+ * @returns List of pending approvals with total count
3400
+ *
3401
+ * @example
3402
+ * ```typescript
3403
+ * const pending = await client.getPendingApprovals({ limit: 10 });
3404
+ * console.log(`${pending.total} approvals pending`);
3405
+ * for (const approval of pending.approvals) {
3406
+ * console.log(`${approval.workflow_name} / ${approval.step_name}`);
3407
+ * }
3408
+ * ```
3409
+ */
3410
+ async getPendingApprovals(options) {
3411
+ const params = new URLSearchParams();
3412
+ if (options?.limit !== undefined) {
3413
+ params.set('limit', options.limit.toString());
3414
+ }
3415
+ const queryString = params.toString();
3416
+ const path = queryString
3417
+ ? `/api/v1/workflow-control/pending-approvals?${queryString}`
3418
+ : '/api/v1/workflow-control/pending-approvals';
3419
+ return this.orchestratorRequest('GET', path);
3420
+ }
3421
+ // =============================================================================
3422
+ // Plan Rollback (Feature 7)
3423
+ // =============================================================================
3424
+ /**
3425
+ * Rollback a plan to a previous version.
3426
+ *
3427
+ * @param planId - ID of the plan to rollback
3428
+ * @param targetVersion - Version number to rollback to
3429
+ * @returns Rollback response with version information
3430
+ *
3431
+ * @example
3432
+ * ```typescript
3433
+ * const result = await client.rollbackPlan('plan_123', 2);
3434
+ * console.log(`Rolled back to v${result.version} from v${result.previousVersion}`);
3435
+ * ```
3436
+ */
3437
+ async rollbackPlan(planId, targetVersion) {
3438
+ const url = `${this.config.endpoint}/api/v1/plan/${planId}/rollback/${targetVersion}`;
3439
+ const headers = {
3440
+ 'Content-Type': 'application/json',
3441
+ ...this.getAuthHeaders(),
3442
+ };
3443
+ const response = await fetch(url, {
3444
+ method: 'POST',
3445
+ headers,
3446
+ body: JSON.stringify({}),
3447
+ signal: AbortSignal.timeout(this.config.mapTimeout),
3448
+ });
3449
+ if (!response.ok) {
3450
+ const errorText = await response.text();
3451
+ throw new PlanExecutionError(`Plan rollback failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'rollback');
3452
+ }
3453
+ const data = await response.json();
3454
+ if (this.config.debug) {
3455
+ debugLog('Plan rolled back', { planId, version: data.version });
3456
+ }
3457
+ return {
3458
+ planId: data.plan_id || planId,
3459
+ version: data.version,
3460
+ previousVersion: data.previous_version,
3461
+ status: data.status,
3462
+ };
3463
+ }
3464
+ // =============================================================================
3465
+ // Webhook CRUD Methods (Feature 7)
3466
+ // =============================================================================
3467
+ /**
3468
+ * Create a webhook subscription.
3469
+ *
3470
+ * @param request - Webhook configuration
3471
+ * @returns Created webhook subscription
3472
+ *
3473
+ * @example
3474
+ * ```typescript
3475
+ * const webhook = await client.createWebhook({
3476
+ * url: 'https://example.com/webhook',
3477
+ * events: ['workflow.completed', 'step.approval_required'],
3478
+ * active: true
3479
+ * });
3480
+ * console.log(`Webhook created: ${webhook.id}`);
3481
+ * ```
3482
+ */
3483
+ async createWebhook(request) {
3484
+ return this.orchestratorRequest('POST', '/api/v1/webhooks', request);
3485
+ }
3486
+ /**
3487
+ * Get a webhook subscription by ID.
3488
+ *
3489
+ * @param webhookId - ID of the webhook to retrieve
3490
+ * @returns Webhook subscription details
3491
+ *
3492
+ * @example
3493
+ * ```typescript
3494
+ * const webhook = await client.getWebhook('wh_123');
3495
+ * console.log(`Webhook URL: ${webhook.url}, Active: ${webhook.active}`);
3496
+ * ```
3497
+ */
3498
+ async getWebhook(webhookId) {
3499
+ if (!webhookId) {
3500
+ throw new ConfigurationError('Webhook ID is required');
3501
+ }
3502
+ return this.orchestratorRequest('GET', `/api/v1/webhooks/${webhookId}`);
3503
+ }
3504
+ /**
3505
+ * Update a webhook subscription.
3506
+ *
3507
+ * @param webhookId - ID of the webhook to update
3508
+ * @param request - Fields to update
3509
+ * @returns Updated webhook subscription
3510
+ *
3511
+ * @example
3512
+ * ```typescript
3513
+ * const webhook = await client.updateWebhook('wh_123', {
3514
+ * events: ['workflow.completed'],
3515
+ * active: false
3516
+ * });
3517
+ * ```
3518
+ */
3519
+ async updateWebhook(webhookId, request) {
3520
+ if (!webhookId) {
3521
+ throw new ConfigurationError('Webhook ID is required');
3522
+ }
3523
+ return this.orchestratorRequest('PUT', `/api/v1/webhooks/${webhookId}`, request);
3524
+ }
3525
+ /**
3526
+ * Delete a webhook subscription.
3527
+ *
3528
+ * @param webhookId - ID of the webhook to delete
3529
+ *
3530
+ * @example
3531
+ * ```typescript
3532
+ * await client.deleteWebhook('wh_123');
3533
+ * ```
3534
+ */
3535
+ async deleteWebhook(webhookId) {
3536
+ if (!webhookId) {
3537
+ throw new ConfigurationError('Webhook ID is required');
3538
+ }
3539
+ await this.orchestratorRequest('DELETE', `/api/v1/webhooks/${webhookId}`);
3540
+ }
3541
+ /**
3542
+ * List all webhook subscriptions.
3543
+ *
3544
+ * @returns List of webhook subscriptions with total count
3545
+ *
3546
+ * @example
3547
+ * ```typescript
3548
+ * const result = await client.listWebhooks();
3549
+ * console.log(`${result.total} webhooks configured`);
3550
+ * for (const wh of result.webhooks) {
3551
+ * console.log(`${wh.id}: ${wh.url} (${wh.active ? 'active' : 'inactive'})`);
3552
+ * }
3553
+ * ```
3554
+ */
3555
+ async listWebhooks() {
3556
+ return this.orchestratorRequest('GET', '/api/v1/webhooks');
3557
+ }
3558
+ // ===========================================================================
3559
+ // MAS FEAT Compliance Methods (Enterprise)
3560
+ // ===========================================================================
3561
+ /**
3562
+ * MAS FEAT compliance module for Singapore regulatory compliance.
3563
+ *
3564
+ * Enterprise Feature: Requires AxonFlow Enterprise license.
3565
+ *
3566
+ * @example
3567
+ * ```typescript
3568
+ * // Register an AI system
3569
+ * const system = await axonflow.masfeat.registerSystem({
3570
+ * systemId: 'credit-scoring-v1',
3571
+ * systemName: 'Credit Scoring AI',
3572
+ * useCase: 'credit_scoring',
3573
+ * ownerTeam: 'Risk Management',
3574
+ * customerImpact: 4,
3575
+ * modelComplexity: 3,
3576
+ * humanReliance: 5
3577
+ * });
3578
+ *
3579
+ * // Configure kill switch
3580
+ * const ks = await axonflow.masfeat.configureKillSwitch('credit-scoring-v1', {
3581
+ * accuracyThreshold: 0.85,
3582
+ * biasThreshold: 0.15,
3583
+ * autoTriggerEnabled: true
3584
+ * });
3585
+ * ```
3586
+ */
3587
+ get masfeat() {
3588
+ return {
3589
+ // Registry methods
3590
+ registerSystem: this.masfeatRegisterSystem.bind(this),
3591
+ getSystem: this.masfeatGetSystem.bind(this),
3592
+ updateSystem: this.masfeatUpdateSystem.bind(this),
3593
+ listSystems: this.masfeatListSystems.bind(this),
3594
+ activateSystem: this.masfeatActivateSystem.bind(this),
3595
+ retireSystem: this.masfeatRetireSystem.bind(this),
3596
+ getRegistrySummary: this.masfeatGetRegistrySummary.bind(this),
3597
+ // Assessment methods
3598
+ createAssessment: this.masfeatCreateAssessment.bind(this),
3599
+ getAssessment: this.masfeatGetAssessment.bind(this),
3600
+ updateAssessment: this.masfeatUpdateAssessment.bind(this),
3601
+ listAssessments: this.masfeatListAssessments.bind(this),
3602
+ submitAssessment: this.masfeatSubmitAssessment.bind(this),
3603
+ approveAssessment: this.masfeatApproveAssessment.bind(this),
3604
+ rejectAssessment: this.masfeatRejectAssessment.bind(this),
3605
+ // Kill switch methods
3606
+ getKillSwitch: this.masfeatGetKillSwitch.bind(this),
3607
+ configureKillSwitch: this.masfeatConfigureKillSwitch.bind(this),
3608
+ checkKillSwitch: this.masfeatCheckKillSwitch.bind(this),
3609
+ triggerKillSwitch: this.masfeatTriggerKillSwitch.bind(this),
3610
+ restoreKillSwitch: this.masfeatRestoreKillSwitch.bind(this),
3611
+ enableKillSwitch: this.masfeatEnableKillSwitch.bind(this),
3612
+ disableKillSwitch: this.masfeatDisableKillSwitch.bind(this),
3613
+ getKillSwitchHistory: this.masfeatGetKillSwitchHistory.bind(this),
3614
+ };
3615
+ }
3616
+ // Registry Methods
3617
+ async masfeatRegisterSystem(request) {
3618
+ const url = `${this.config.endpoint}/api/v1/masfeat/registry`;
3619
+ const body = {
3620
+ system_id: request.systemId,
3621
+ system_name: request.systemName,
3622
+ description: request.description,
3623
+ use_case: request.useCase,
3624
+ owner_team: request.ownerTeam,
3625
+ technical_owner: request.technicalOwner,
3626
+ owner_email: request.businessOwner,
3627
+ risk_rating_impact: request.customerImpact,
3628
+ risk_rating_complexity: request.modelComplexity,
3629
+ risk_rating_reliance: request.humanReliance,
3630
+ metadata: request.metadata,
3631
+ };
3632
+ const response = await fetch(url, {
3633
+ method: 'POST',
3634
+ headers: {
3635
+ 'Content-Type': 'application/json',
3636
+ ...this.getAuthHeaders(),
3637
+ },
3638
+ body: JSON.stringify(body),
3639
+ signal: AbortSignal.timeout(this.config.timeout),
3640
+ });
3641
+ if (!response.ok) {
3642
+ const errorText = await response.text();
3643
+ throw new APIError(response.status, response.statusText, errorText);
3644
+ }
3645
+ return this.mapSystemResponse(await response.json());
3646
+ }
3647
+ async masfeatGetSystem(systemId) {
3648
+ const url = `${this.config.endpoint}/api/v1/masfeat/registry/${systemId}`;
3649
+ const response = await fetch(url, {
3650
+ method: 'GET',
3651
+ headers: {
3652
+ ...this.getAuthHeaders(),
3653
+ },
3654
+ signal: AbortSignal.timeout(this.config.timeout),
3655
+ });
3656
+ if (!response.ok) {
3657
+ const errorText = await response.text();
3658
+ throw new APIError(response.status, response.statusText, errorText);
3659
+ }
3660
+ return this.mapSystemResponse(await response.json());
3661
+ }
3662
+ async masfeatUpdateSystem(systemId, request) {
3663
+ const url = `${this.config.endpoint}/api/v1/masfeat/registry/${systemId}`;
3664
+ const body = {};
3665
+ if (request.systemName !== undefined)
3666
+ body.system_name = request.systemName;
3667
+ if (request.description !== undefined)
3668
+ body.description = request.description;
3669
+ if (request.ownerTeam !== undefined)
3670
+ body.owner_team = request.ownerTeam;
3671
+ if (request.technicalOwner !== undefined)
3672
+ body.technical_owner = request.technicalOwner;
3673
+ if (request.businessOwner !== undefined)
3674
+ body.business_owner = request.businessOwner;
3675
+ if (request.customerImpact !== undefined)
3676
+ body.customer_impact = request.customerImpact;
3677
+ if (request.modelComplexity !== undefined)
3678
+ body.model_complexity = request.modelComplexity;
3679
+ if (request.humanReliance !== undefined)
3680
+ body.human_reliance = request.humanReliance;
3681
+ if (request.metadata !== undefined)
3682
+ body.metadata = request.metadata;
3683
+ const response = await fetch(url, {
3684
+ method: 'PUT',
3685
+ headers: {
3686
+ 'Content-Type': 'application/json',
3687
+ ...this.getAuthHeaders(),
3688
+ },
3689
+ body: JSON.stringify(body),
3690
+ signal: AbortSignal.timeout(this.config.timeout),
3691
+ });
3692
+ if (!response.ok) {
3693
+ const errorText = await response.text();
3694
+ throw new APIError(response.status, response.statusText, errorText);
3695
+ }
3696
+ return this.mapSystemResponse(await response.json());
3697
+ }
3698
+ async masfeatListSystems(options) {
3699
+ const params = new URLSearchParams();
3700
+ if (options?.status)
3701
+ params.append('status', options.status);
3702
+ if (options?.useCase)
3703
+ params.append('use_case', options.useCase);
3704
+ if (options?.materiality)
3705
+ params.append('materiality', options.materiality);
3706
+ if (options?.limit)
3707
+ params.append('limit', options.limit.toString());
3708
+ if (options?.offset)
3709
+ params.append('offset', options.offset.toString());
3710
+ const queryString = params.toString();
3711
+ const url = `${this.config.endpoint}/api/v1/masfeat/registry${queryString ? `?${queryString}` : ''}`;
3712
+ const response = await fetch(url, {
3713
+ method: 'GET',
3714
+ headers: {
3715
+ ...this.getAuthHeaders(),
3716
+ },
3717
+ signal: AbortSignal.timeout(this.config.timeout),
3718
+ });
3719
+ if (!response.ok) {
3720
+ const errorText = await response.text();
3721
+ throw new APIError(response.status, response.statusText, errorText);
3722
+ }
3723
+ const data = await response.json();
3724
+ return (data || []).map((s) => this.mapSystemResponse(s));
3725
+ }
3726
+ async masfeatActivateSystem(systemId) {
3727
+ // Use PUT to update status - the /activate endpoint doesn't exist
3728
+ const url = `${this.config.endpoint}/api/v1/masfeat/registry/${systemId}`;
3729
+ const response = await fetch(url, {
3730
+ method: 'PUT',
3731
+ headers: {
3732
+ 'Content-Type': 'application/json',
3733
+ ...this.getAuthHeaders(),
3734
+ },
3735
+ body: JSON.stringify({ status: 'active' }),
3736
+ signal: AbortSignal.timeout(this.config.timeout),
3737
+ });
3738
+ if (!response.ok) {
3739
+ const errorText = await response.text();
3740
+ throw new APIError(response.status, response.statusText, errorText);
3741
+ }
3742
+ return this.mapSystemResponse(await response.json());
3743
+ }
3744
+ async masfeatRetireSystem(systemId) {
3745
+ const url = `${this.config.endpoint}/api/v1/masfeat/registry/${systemId}`;
3746
+ const response = await fetch(url, {
3747
+ method: 'DELETE',
3748
+ headers: {
3749
+ ...this.getAuthHeaders(),
3750
+ },
3751
+ signal: AbortSignal.timeout(this.config.timeout),
3752
+ });
3753
+ if (!response.ok) {
3754
+ const errorText = await response.text();
3755
+ throw new APIError(response.status, response.statusText, errorText);
3756
+ }
3757
+ return this.mapSystemResponse(await response.json());
3758
+ }
3759
+ async masfeatGetRegistrySummary() {
3760
+ const url = `${this.config.endpoint}/api/v1/masfeat/registry/summary`;
3761
+ const response = await fetch(url, {
3762
+ method: 'GET',
3763
+ headers: {
3764
+ ...this.getAuthHeaders(),
3765
+ },
3766
+ signal: AbortSignal.timeout(this.config.timeout),
3767
+ });
3768
+ if (!response.ok) {
3769
+ const errorText = await response.text();
3770
+ throw new APIError(response.status, response.statusText, errorText);
3771
+ }
3772
+ const data = await response.json();
3773
+ return {
3774
+ totalSystems: data.total_systems,
3775
+ activeSystems: data.active_systems,
3776
+ highMaterialityCount: data.high_materiality_count ?? data.high_materiality ?? 0,
3777
+ mediumMaterialityCount: data.medium_materiality_count ?? data.medium_materiality ?? 0,
3778
+ lowMaterialityCount: data.low_materiality_count ?? data.low_materiality ?? 0,
3779
+ byUseCase: data.by_use_case || {},
3780
+ byStatus: data.by_status || {},
3781
+ };
3782
+ }
3783
+ // Assessment Methods
3784
+ async masfeatCreateAssessment(request) {
3785
+ const url = `${this.config.endpoint}/api/v1/masfeat/assessments`;
3786
+ const body = {
3787
+ system_id: request.systemId,
3788
+ assessment_type: request.assessmentType || 'periodic',
3789
+ assessors: request.assessors,
3790
+ };
3791
+ if (request.assessmentDate)
3792
+ body.assessment_date = request.assessmentDate.toISOString();
3793
+ if (request.fairnessScore !== undefined)
3794
+ body.fairness_score = request.fairnessScore;
3795
+ if (request.ethicsScore !== undefined)
3796
+ body.ethics_score = request.ethicsScore;
3797
+ if (request.accountabilityScore !== undefined)
3798
+ body.accountability_score = request.accountabilityScore;
3799
+ if (request.transparencyScore !== undefined)
3800
+ body.transparency_score = request.transparencyScore;
3801
+ if (request.fairnessDetails)
3802
+ body.fairness_details = request.fairnessDetails;
3803
+ if (request.ethicsDetails)
3804
+ body.ethics_details = request.ethicsDetails;
3805
+ if (request.accountabilityDetails)
3806
+ body.accountability_details = request.accountabilityDetails;
3807
+ if (request.transparencyDetails)
3808
+ body.transparency_details = request.transparencyDetails;
3809
+ if (request.recommendations)
3810
+ body.recommendations = request.recommendations;
3811
+ if (request.findings) {
3812
+ body.findings = request.findings.map(f => ({
3813
+ id: f.id,
3814
+ pillar: f.pillar,
3815
+ severity: f.severity,
3816
+ category: f.category,
3817
+ description: f.description,
3818
+ status: f.status,
3819
+ remediation: f.remediation,
3820
+ due_date: f.dueDate?.toISOString(),
3821
+ }));
3822
+ }
3823
+ const response = await fetch(url, {
3824
+ method: 'POST',
3825
+ headers: {
3826
+ 'Content-Type': 'application/json',
3827
+ ...this.getAuthHeaders(),
3828
+ },
3829
+ body: JSON.stringify(body),
3830
+ signal: AbortSignal.timeout(this.config.timeout),
3831
+ });
3832
+ if (!response.ok) {
3833
+ const errorText = await response.text();
3834
+ throw new APIError(response.status, response.statusText, errorText);
3835
+ }
3836
+ return this.mapAssessmentResponse(await response.json());
3837
+ }
3838
+ async masfeatGetAssessment(assessmentId) {
3839
+ const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}`;
3840
+ const response = await fetch(url, {
3841
+ method: 'GET',
3842
+ headers: {
3843
+ ...this.getAuthHeaders(),
3844
+ },
3845
+ signal: AbortSignal.timeout(this.config.timeout),
3846
+ });
3847
+ if (!response.ok) {
3848
+ const errorText = await response.text();
3849
+ throw new APIError(response.status, response.statusText, errorText);
3850
+ }
3851
+ return this.mapAssessmentResponse(await response.json());
3852
+ }
3853
+ async masfeatUpdateAssessment(assessmentId, request) {
3854
+ const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}`;
3855
+ const body = {};
3856
+ if (request.fairnessScore !== undefined)
3857
+ body.fairness_score = request.fairnessScore;
3858
+ if (request.ethicsScore !== undefined)
3859
+ body.ethics_score = request.ethicsScore;
3860
+ if (request.accountabilityScore !== undefined)
3861
+ body.accountability_score = request.accountabilityScore;
3862
+ if (request.transparencyScore !== undefined)
3863
+ body.transparency_score = request.transparencyScore;
3864
+ if (request.fairnessDetails !== undefined)
3865
+ body.fairness_details = request.fairnessDetails;
3866
+ if (request.ethicsDetails !== undefined)
3867
+ body.ethics_details = request.ethicsDetails;
3868
+ if (request.accountabilityDetails !== undefined)
3869
+ body.accountability_details = request.accountabilityDetails;
3870
+ if (request.transparencyDetails !== undefined)
3871
+ body.transparency_details = request.transparencyDetails;
3872
+ if (request.findings !== undefined) {
3873
+ body.findings = request.findings.map(f => ({
3874
+ id: f.id,
3875
+ pillar: f.pillar,
3876
+ severity: f.severity,
3877
+ category: f.category,
3878
+ description: f.description,
3879
+ status: f.status,
3880
+ remediation: f.remediation,
3881
+ due_date: f.dueDate?.toISOString(),
3882
+ }));
3883
+ }
3884
+ if (request.recommendations !== undefined)
3885
+ body.recommendations = request.recommendations;
3886
+ if (request.assessors !== undefined)
3887
+ body.assessors = request.assessors;
3888
+ const response = await fetch(url, {
3889
+ method: 'PUT',
3890
+ headers: {
3891
+ 'Content-Type': 'application/json',
3892
+ ...this.getAuthHeaders(),
3893
+ },
3894
+ body: JSON.stringify(body),
3895
+ signal: AbortSignal.timeout(this.config.timeout),
3896
+ });
3897
+ if (!response.ok) {
3898
+ const errorText = await response.text();
3899
+ throw new APIError(response.status, response.statusText, errorText);
3900
+ }
3901
+ return this.mapAssessmentResponse(await response.json());
3902
+ }
3903
+ async masfeatListAssessments(options) {
3904
+ const params = new URLSearchParams();
3905
+ if (options?.systemId)
3906
+ params.append('system_id', options.systemId);
3907
+ if (options?.status)
3908
+ params.append('status', options.status);
3909
+ if (options?.limit)
3910
+ params.append('limit', options.limit.toString());
3911
+ if (options?.offset)
3912
+ params.append('offset', options.offset.toString());
3913
+ const queryString = params.toString();
3914
+ const url = `${this.config.endpoint}/api/v1/masfeat/assessments${queryString ? `?${queryString}` : ''}`;
3915
+ const response = await fetch(url, {
3916
+ method: 'GET',
3917
+ headers: {
3918
+ ...this.getAuthHeaders(),
3919
+ },
3920
+ signal: AbortSignal.timeout(this.config.timeout),
3921
+ });
3922
+ if (!response.ok) {
3923
+ const errorText = await response.text();
3924
+ throw new APIError(response.status, response.statusText, errorText);
3925
+ }
3926
+ const data = await response.json();
3927
+ return (data || []).map((a) => this.mapAssessmentResponse(a));
3928
+ }
3929
+ async masfeatSubmitAssessment(assessmentId) {
3930
+ const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}/submit`;
3931
+ const response = await fetch(url, {
3932
+ method: 'POST',
3933
+ headers: {
3934
+ 'Content-Type': 'application/json',
3935
+ ...this.getAuthHeaders(),
3936
+ },
3937
+ signal: AbortSignal.timeout(this.config.timeout),
3938
+ });
3939
+ if (!response.ok) {
3940
+ const errorText = await response.text();
3941
+ throw new APIError(response.status, response.statusText, errorText);
3942
+ }
3943
+ return this.mapAssessmentResponse(await response.json());
3944
+ }
3945
+ async masfeatApproveAssessment(assessmentId, request) {
3946
+ const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}/approve`;
3947
+ const response = await fetch(url, {
3948
+ method: 'POST',
3949
+ headers: {
3950
+ 'Content-Type': 'application/json',
3951
+ ...this.getAuthHeaders(),
3952
+ },
3953
+ body: JSON.stringify({
3954
+ approved_by: request.approvedBy,
3955
+ comments: request.comments,
3956
+ }),
3957
+ signal: AbortSignal.timeout(this.config.timeout),
3958
+ });
3959
+ if (!response.ok) {
3960
+ const errorText = await response.text();
3961
+ throw new APIError(response.status, response.statusText, errorText);
3962
+ }
3963
+ return this.mapAssessmentResponse(await response.json());
3964
+ }
3965
+ async masfeatRejectAssessment(assessmentId, request) {
3966
+ const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}/reject`;
3967
+ const response = await fetch(url, {
3968
+ method: 'POST',
3969
+ headers: {
3970
+ 'Content-Type': 'application/json',
3971
+ ...this.getAuthHeaders(),
3972
+ },
3973
+ body: JSON.stringify({
3974
+ rejected_by: request.rejectedBy,
3975
+ reason: request.reason,
3976
+ }),
3977
+ signal: AbortSignal.timeout(this.config.timeout),
3978
+ });
3979
+ if (!response.ok) {
3980
+ const errorText = await response.text();
3981
+ throw new APIError(response.status, response.statusText, errorText);
3982
+ }
3983
+ return this.mapAssessmentResponse(await response.json());
3984
+ }
3985
+ // Kill Switch Methods
3986
+ async masfeatGetKillSwitch(systemId) {
3987
+ const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}`;
3988
+ const response = await fetch(url, {
3989
+ method: 'GET',
3990
+ headers: {
3991
+ ...this.getAuthHeaders(),
3992
+ },
3993
+ signal: AbortSignal.timeout(this.config.timeout),
3994
+ });
3995
+ if (!response.ok) {
3996
+ const errorText = await response.text();
3997
+ throw new APIError(response.status, response.statusText, errorText);
3998
+ }
3999
+ return this.mapKillSwitchResponse(await response.json());
4000
+ }
4001
+ async masfeatConfigureKillSwitch(systemId, request) {
4002
+ const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/configure`;
4003
+ const body = {};
4004
+ if (request.accuracyThreshold !== undefined)
4005
+ body.accuracy_threshold = request.accuracyThreshold;
4006
+ if (request.biasThreshold !== undefined)
4007
+ body.bias_threshold = request.biasThreshold;
4008
+ if (request.errorRateThreshold !== undefined)
4009
+ body.error_rate_threshold = request.errorRateThreshold;
4010
+ if (request.autoTriggerEnabled !== undefined)
4011
+ body.auto_trigger_enabled = request.autoTriggerEnabled;
4012
+ const response = await fetch(url, {
4013
+ method: 'POST',
4014
+ headers: {
4015
+ 'Content-Type': 'application/json',
4016
+ ...this.getAuthHeaders(),
4017
+ },
4018
+ body: JSON.stringify(body),
4019
+ signal: AbortSignal.timeout(this.config.timeout),
4020
+ });
4021
+ if (!response.ok) {
4022
+ const errorText = await response.text();
4023
+ throw new APIError(response.status, response.statusText, errorText);
4024
+ }
4025
+ return this.mapKillSwitchResponse(await response.json());
4026
+ }
4027
+ async masfeatCheckKillSwitch(systemId, request) {
4028
+ const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/check`;
4029
+ const response = await fetch(url, {
4030
+ method: 'POST',
4031
+ headers: {
4032
+ 'Content-Type': 'application/json',
4033
+ ...this.getAuthHeaders(),
4034
+ },
4035
+ body: JSON.stringify({
4036
+ accuracy: request.accuracy,
4037
+ bias_score: request.biasScore,
4038
+ error_rate: request.errorRate,
4039
+ }),
4040
+ signal: AbortSignal.timeout(this.config.timeout),
4041
+ });
4042
+ if (!response.ok) {
4043
+ const errorText = await response.text();
4044
+ throw new APIError(response.status, response.statusText, errorText);
4045
+ }
4046
+ return this.mapKillSwitchResponse(await response.json());
4047
+ }
4048
+ async masfeatTriggerKillSwitch(systemId, request) {
4049
+ const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/trigger`;
4050
+ const response = await fetch(url, {
4051
+ method: 'POST',
4052
+ headers: {
4053
+ 'Content-Type': 'application/json',
4054
+ ...this.getAuthHeaders(),
4055
+ },
4056
+ body: JSON.stringify({
4057
+ reason: request.reason,
4058
+ triggered_by: request.triggeredBy,
4059
+ }),
4060
+ signal: AbortSignal.timeout(this.config.timeout),
4061
+ });
4062
+ if (!response.ok) {
4063
+ const errorText = await response.text();
4064
+ throw new APIError(response.status, response.statusText, errorText);
4065
+ }
4066
+ return this.mapKillSwitchResponse(await response.json());
4067
+ }
4068
+ async masfeatRestoreKillSwitch(systemId, request) {
4069
+ const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/restore`;
4070
+ const response = await fetch(url, {
4071
+ method: 'POST',
4072
+ headers: {
4073
+ 'Content-Type': 'application/json',
4074
+ ...this.getAuthHeaders(),
4075
+ },
4076
+ body: JSON.stringify({
4077
+ reason: request.reason,
4078
+ restored_by: request.restoredBy,
4079
+ }),
4080
+ signal: AbortSignal.timeout(this.config.timeout),
4081
+ });
4082
+ if (!response.ok) {
4083
+ const errorText = await response.text();
4084
+ throw new APIError(response.status, response.statusText, errorText);
4085
+ }
4086
+ return this.mapKillSwitchResponse(await response.json());
4087
+ }
4088
+ async masfeatEnableKillSwitch(systemId) {
4089
+ const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/enable`;
4090
+ const response = await fetch(url, {
4091
+ method: 'POST',
4092
+ headers: {
4093
+ 'Content-Type': 'application/json',
4094
+ ...this.getAuthHeaders(),
4095
+ },
4096
+ signal: AbortSignal.timeout(this.config.timeout),
4097
+ });
4098
+ if (!response.ok) {
4099
+ const errorText = await response.text();
4100
+ throw new APIError(response.status, response.statusText, errorText);
4101
+ }
4102
+ return this.mapKillSwitchResponse(await response.json());
4103
+ }
4104
+ async masfeatDisableKillSwitch(systemId, request) {
4105
+ const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/disable`;
4106
+ const response = await fetch(url, {
4107
+ method: 'POST',
4108
+ headers: {
4109
+ 'Content-Type': 'application/json',
4110
+ ...this.getAuthHeaders(),
4111
+ },
4112
+ body: JSON.stringify({ reason: request?.reason }),
4113
+ signal: AbortSignal.timeout(this.config.timeout),
4114
+ });
4115
+ if (!response.ok) {
4116
+ const errorText = await response.text();
4117
+ throw new APIError(response.status, response.statusText, errorText);
4118
+ }
4119
+ return this.mapKillSwitchResponse(await response.json());
4120
+ }
4121
+ async masfeatGetKillSwitchHistory(systemId, limit) {
4122
+ const params = new URLSearchParams();
4123
+ if (limit)
4124
+ params.append('limit', limit.toString());
4125
+ const queryString = params.toString();
4126
+ const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/history${queryString ? `?${queryString}` : ''}`;
4127
+ const response = await fetch(url, {
4128
+ method: 'GET',
4129
+ headers: {
4130
+ ...this.getAuthHeaders(),
4131
+ },
4132
+ signal: AbortSignal.timeout(this.config.timeout),
4133
+ });
4134
+ if (!response.ok) {
4135
+ const errorText = await response.text();
4136
+ throw new APIError(response.status, response.statusText, errorText);
4137
+ }
4138
+ let data = await response.json();
4139
+ // Handle nested response format {history: [...], count: N}
4140
+ if (data && typeof data === 'object' && 'history' in data) {
4141
+ data = data.history;
4142
+ }
4143
+ return (data || []).map((e) => ({
4144
+ id: e.id,
4145
+ killSwitchId: e.kill_switch_id,
4146
+ // Handle both API formats: event_type (SDK expected) vs action (API actual)
4147
+ eventType: e.event_type || e.action,
4148
+ // Build eventData from additional fields if not present
4149
+ eventData: e.event_data ||
4150
+ (e.previous_status || e.new_status || e.reason
4151
+ ? { previousStatus: e.previous_status, newStatus: e.new_status, reason: e.reason }
4152
+ : undefined),
4153
+ // Handle both API formats: created_by vs performed_by
4154
+ createdBy: e.created_by || e.performed_by,
4155
+ // Handle both API formats: created_at vs performed_at
4156
+ createdAt: new Date(e.created_at || e.performed_at),
4157
+ }));
4158
+ }
4159
+ // Helper methods for MAS FEAT
4160
+ mapSystemResponse(data) {
4161
+ return {
4162
+ id: data.id,
4163
+ orgId: data.org_id,
4164
+ systemId: data.system_id,
4165
+ systemName: data.system_name,
4166
+ description: data.description,
4167
+ useCase: data.use_case,
4168
+ ownerTeam: data.owner_team,
4169
+ technicalOwner: data.technical_owner,
4170
+ businessOwner: data.business_owner || data.owner_email,
4171
+ customerImpact: data.customer_impact ?? data.risk_rating_impact,
4172
+ modelComplexity: data.model_complexity ?? data.risk_rating_complexity,
4173
+ humanReliance: data.human_reliance ?? data.risk_rating_reliance,
4174
+ materiality: data.materiality || data.materiality_classification,
4175
+ status: data.status,
4176
+ metadata: data.metadata,
4177
+ createdAt: new Date(data.created_at),
4178
+ updatedAt: new Date(data.updated_at),
4179
+ createdBy: data.created_by,
4180
+ };
4181
+ }
4182
+ mapFindingResponse(data) {
4183
+ return {
4184
+ id: data.id,
4185
+ pillar: data.pillar,
4186
+ severity: data.severity,
4187
+ category: data.category,
4188
+ description: data.description,
4189
+ status: data.status,
4190
+ remediation: data.remediation,
4191
+ dueDate: data.due_date ? new Date(data.due_date) : undefined,
4192
+ };
4193
+ }
4194
+ mapAssessmentResponse(data) {
4195
+ return {
4196
+ id: data.id,
4197
+ orgId: data.org_id,
4198
+ systemId: data.system_id,
4199
+ assessmentType: data.assessment_type,
4200
+ status: data.status,
4201
+ assessmentDate: new Date(data.assessment_date),
4202
+ validUntil: data.valid_until ? new Date(data.valid_until) : undefined,
4203
+ fairnessScore: data.fairness_score,
4204
+ ethicsScore: data.ethics_score,
4205
+ accountabilityScore: data.accountability_score,
4206
+ transparencyScore: data.transparency_score,
4207
+ overallScore: data.overall_score,
4208
+ fairnessDetails: data.fairness_details,
4209
+ ethicsDetails: data.ethics_details,
4210
+ accountabilityDetails: data.accountability_details,
4211
+ transparencyDetails: data.transparency_details,
4212
+ findings: data.findings?.map((f) => this.mapFindingResponse(f)),
4213
+ recommendations: data.recommendations,
4214
+ assessors: data.assessors,
4215
+ approvedBy: data.approved_by,
4216
+ approvedAt: data.approved_at ? new Date(data.approved_at) : undefined,
4217
+ createdAt: new Date(data.created_at),
4218
+ updatedAt: new Date(data.updated_at),
4219
+ createdBy: data.created_by,
4220
+ };
4221
+ }
4222
+ mapKillSwitchResponse(data) {
4223
+ // Handle nested response format (trigger/restore return {kill_switch: {...}, message: ...})
4224
+ if (data.kill_switch) {
4225
+ data = data.kill_switch;
4226
+ }
4227
+ return {
4228
+ id: data.id,
4229
+ orgId: data.org_id,
4230
+ systemId: data.system_id,
4231
+ status: data.status,
4232
+ accuracyThreshold: data.accuracy_threshold,
4233
+ biasThreshold: data.bias_threshold,
4234
+ errorRateThreshold: data.error_rate_threshold,
4235
+ autoTriggerEnabled: data.auto_trigger_enabled,
4236
+ triggeredAt: data.triggered_at ? new Date(data.triggered_at) : undefined,
4237
+ triggeredBy: data.triggered_by,
4238
+ triggeredReason: data.triggered_reason || data.trigger_reason,
4239
+ restoredAt: data.restored_at ? new Date(data.restored_at) : undefined,
4240
+ restoredBy: data.restored_by,
4241
+ createdAt: new Date(data.created_at),
4242
+ updatedAt: new Date(data.updated_at),
4243
+ };
4244
+ }
4245
+ // ============================================================================
4246
+ // Unified Execution Tracking Methods (Issue #1075)
4247
+ // ============================================================================
4248
+ /**
4249
+ * Get unified execution status for a MAP plan or WCP workflow.
4250
+ *
4251
+ * This method provides a consistent interface for tracking execution progress
4252
+ * regardless of whether the underlying execution is a MAP plan or WCP workflow.
4253
+ *
4254
+ * @param executionId - The execution ID (plan ID or workflow ID)
4255
+ * @returns Unified execution status
4256
+ *
4257
+ * @example
4258
+ * ```typescript
4259
+ * // Get status for any execution (MAP or WCP)
4260
+ * const status = await client.getExecutionStatus('exec_123');
4261
+ * console.log(`Type: ${status.execution_type}`);
4262
+ * console.log(`Status: ${status.status}`);
4263
+ * console.log(`Progress: ${status.progress_percent}%`);
4264
+ *
4265
+ * // Check steps
4266
+ * for (const step of status.steps) {
4267
+ * console.log(` Step ${step.step_index}: ${step.step_name} - ${step.status}`);
4268
+ * }
4269
+ * ```
4270
+ */
4271
+ async getExecutionStatus(executionId) {
4272
+ if (!executionId) {
4273
+ throw new ConfigurationError('Execution ID is required');
4274
+ }
4275
+ if (this.config.debug) {
4276
+ debugLog('Getting execution status', { executionId });
4277
+ }
4278
+ return this.orchestratorRequest('GET', `/api/v1/unified/executions/${executionId}`);
4279
+ }
4280
+ /**
4281
+ * List unified executions with optional filters.
4282
+ *
4283
+ * Returns a paginated list of executions (both MAP plans and WCP workflows)
4284
+ * with optional filtering by type, status, tenant, or organization.
4285
+ * This method provides a unified view across all execution types.
4286
+ *
4287
+ * @param options - Filter and pagination options
4288
+ * @returns Paginated list of unified executions
4289
+ *
4290
+ * @example
4291
+ * ```typescript
4292
+ * // List all running executions
4293
+ * const result = await client.listUnifiedExecutions({
4294
+ * status: 'running',
4295
+ * limit: 20
4296
+ * });
4297
+ * console.log(`Found ${result.total} running executions`);
4298
+ *
4299
+ * // List only MAP plans
4300
+ * const mapPlans = await client.listUnifiedExecutions({
4301
+ * execution_type: 'map_plan',
4302
+ * limit: 50
4303
+ * });
4304
+ *
4305
+ * // List WCP workflows for a specific tenant
4306
+ * const workflows = await client.listUnifiedExecutions({
4307
+ * execution_type: 'wcp_workflow',
4308
+ * tenant_id: 'tenant_123'
4309
+ * });
4310
+ * ```
4311
+ */
4312
+ async listUnifiedExecutions(options) {
4313
+ const params = new URLSearchParams();
4314
+ if (options?.execution_type) {
4315
+ params.set('execution_type', options.execution_type);
4316
+ }
4317
+ if (options?.status) {
4318
+ params.set('status', options.status);
4319
+ }
4320
+ if (options?.tenant_id) {
4321
+ params.set('tenant_id', options.tenant_id);
4322
+ }
4323
+ if (options?.org_id) {
4324
+ params.set('org_id', options.org_id);
4325
+ }
4326
+ if (options?.limit !== undefined) {
4327
+ params.set('limit', options.limit.toString());
4328
+ }
4329
+ if (options?.offset !== undefined) {
4330
+ params.set('offset', options.offset.toString());
4331
+ }
4332
+ const queryString = params.toString();
4333
+ const path = queryString
4334
+ ? `/api/v1/unified/executions?${queryString}`
4335
+ : '/api/v1/unified/executions';
4336
+ if (this.config.debug) {
4337
+ debugLog('Listing unified executions', { options });
4338
+ }
4339
+ return this.orchestratorRequest('GET', path);
4340
+ }
4341
+ /**
4342
+ * Cancel a unified execution (MAP plan or WCP workflow).
4343
+ *
4344
+ * This method cancels an execution via the unified execution API,
4345
+ * automatically propagating to the correct subsystem (MAP or WCP).
4346
+ *
4347
+ * @param executionId - The execution ID (plan ID or workflow ID)
4348
+ * @param reason - Optional reason for cancellation
4349
+ *
4350
+ * @example
4351
+ * ```typescript
4352
+ * await client.cancelExecution('wf_abc123', 'User requested cancellation');
4353
+ * ```
4354
+ */
4355
+ async cancelExecution(executionId, reason) {
4356
+ if (!executionId) {
4357
+ throw new ConfigurationError('Execution ID is required');
4358
+ }
4359
+ const body = reason ? { reason } : {};
4360
+ await this.orchestratorRequest('POST', `/api/v1/unified/executions/${executionId}/cancel`, body);
4361
+ }
4362
+ /**
4363
+ * Stream real-time execution status updates via Server-Sent Events (SSE).
4364
+ *
4365
+ * Connects to the SSE streaming endpoint and invokes the callback with each
4366
+ * ExecutionStatus update as it arrives. The stream automatically closes when
4367
+ * the execution reaches a terminal state (completed, failed, cancelled, aborted,
4368
+ * or expired).
4369
+ *
4370
+ * @param executionId - The execution ID (plan ID or workflow ID)
4371
+ * @param callback - Function called with each ExecutionStatus update
4372
+ * @param options - Optional configuration including an AbortSignal for cancellation
4373
+ *
4374
+ * @example
4375
+ * ```typescript
4376
+ * // Stream with a callback
4377
+ * await client.streamExecutionStatus('exec_123', (status) => {
4378
+ * console.log(`Progress: ${status.progress_percent}%`);
4379
+ * console.log(`Status: ${status.status}`);
4380
+ * for (const step of status.steps) {
4381
+ * console.log(` Step ${step.step_index}: ${step.step_name} - ${step.status}`);
4382
+ * }
4383
+ * });
4384
+ *
4385
+ * // Stream with abort support
4386
+ * const controller = new AbortController();
4387
+ * setTimeout(() => controller.abort(), 60000); // timeout after 1 minute
4388
+ * await client.streamExecutionStatus('exec_123', (status) => {
4389
+ * console.log(`${status.status}: ${status.progress_percent}%`);
4390
+ * }, { signal: controller.signal });
4391
+ * ```
4392
+ */
4393
+ async streamExecutionStatus(executionId, callback, options) {
4394
+ if (!executionId) {
4395
+ throw new ConfigurationError('Execution ID is required');
4396
+ }
4397
+ const url = `${this.config.endpoint}/api/v1/executions/${executionId}/stream`;
4398
+ const headers = this.buildAuthHeaders();
4399
+ // Override Content-Type for SSE — Accept is what matters
4400
+ headers['Accept'] = 'text/event-stream';
4401
+ delete headers['Content-Type'];
4402
+ if (this.config.debug) {
4403
+ debugLog('Streaming execution status', { executionId, url });
4404
+ }
4405
+ const fetchOptions = {
4406
+ method: 'GET',
4407
+ headers,
4408
+ };
4409
+ if (options?.signal) {
4410
+ fetchOptions.signal = options.signal;
4411
+ }
4412
+ let response;
4413
+ try {
4414
+ response = await fetch(url, fetchOptions);
4415
+ }
4416
+ catch (error) {
4417
+ if (error instanceof Error && error.name === 'AbortError') {
4418
+ return; // Clean exit on abort
4419
+ }
4420
+ throw error;
4421
+ }
4422
+ if (!response.ok) {
4423
+ const errorText = await response.text();
4424
+ if (response.status === 401 || response.status === 403) {
4425
+ throw new AuthenticationError(`Stream request failed: ${errorText}`);
4426
+ }
4427
+ if (response.status === 404) {
4428
+ throw new APIError(404, 'Not Found', errorText);
4429
+ }
4430
+ throw new APIError(response.status, response.statusText, errorText);
4431
+ }
4432
+ if (!response.body) {
4433
+ throw new APIError(0, 'No Body', 'SSE response has no body');
4434
+ }
4435
+ const reader = response.body.getReader();
4436
+ const decoder = new TextDecoder();
4437
+ let buffer = '';
4438
+ try {
4439
+ while (true) {
4440
+ const { done, value } = await reader.read();
4441
+ if (done) {
4442
+ break;
4443
+ }
4444
+ buffer += decoder.decode(value, { stream: true });
4445
+ // Process complete SSE events (separated by double newline)
4446
+ const events = buffer.split('\n\n');
4447
+ // Keep the last (potentially incomplete) chunk in the buffer
4448
+ buffer = events.pop() || '';
4449
+ for (const event of events) {
4450
+ const trimmed = event.trim();
4451
+ if (!trimmed) {
4452
+ continue;
4453
+ }
4454
+ // Parse SSE data lines (handle both "data: " and "data:" formats per SSE spec)
4455
+ for (const line of trimmed.split('\n')) {
4456
+ let jsonStr;
4457
+ if (line.startsWith('data: ')) {
4458
+ jsonStr = line.slice(6);
4459
+ }
4460
+ else if (line.startsWith('data:')) {
4461
+ jsonStr = line.slice(5);
4462
+ }
4463
+ if (jsonStr !== undefined) {
4464
+ if (!jsonStr || jsonStr === '[DONE]') {
4465
+ continue;
4466
+ }
4467
+ try {
4468
+ const status = JSON.parse(jsonStr);
4469
+ callback(status);
4470
+ // Check for terminal status — stream is done
4471
+ if (status.status === 'completed' ||
4472
+ status.status === 'failed' ||
4473
+ status.status === 'cancelled' ||
4474
+ status.status === 'aborted' ||
4475
+ status.status === 'expired') {
4476
+ return;
4477
+ }
4478
+ }
4479
+ catch (parseError) {
4480
+ if (this.config.debug) {
4481
+ debugLog('Failed to parse SSE data', { jsonStr, error: parseError });
4482
+ }
4483
+ }
4484
+ }
4485
+ }
4486
+ }
4487
+ }
4488
+ }
4489
+ catch (error) {
4490
+ if (error instanceof Error && error.name === 'AbortError') {
4491
+ return; // Clean exit on abort
4492
+ }
4493
+ throw error;
4494
+ }
4495
+ finally {
4496
+ try {
4497
+ reader.releaseLock();
4498
+ }
4499
+ catch {
4500
+ // Reader may already be released
4501
+ }
4502
+ }
4503
+ }
2789
4504
  }
2790
4505
  //# sourceMappingURL=client.js.map