@fulcrum-governance/sdk 0.1.3 → 0.1.5

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 (37) hide show
  1. package/CHANGELOG.md +18 -4
  2. package/README.md +40 -51
  3. package/dist/__tests__/clients.test.js +127 -12
  4. package/dist/clients/agent.js +18 -12
  5. package/dist/clients/approval.js +15 -2
  6. package/dist/clients/budget.js +15 -2
  7. package/dist/clients/checkpoint.js +15 -2
  8. package/dist/clients/envelope.d.ts +5 -4
  9. package/dist/clients/envelope.js +39 -10
  10. package/dist/clients/eventstore.js +15 -2
  11. package/dist/clients/metrics.js +15 -2
  12. package/dist/clients/policy.d.ts +7 -2
  13. package/dist/clients/policy.js +55 -10
  14. package/dist/clients/tenant.js +15 -2
  15. package/dist/index.js +6 -2
  16. package/dist/instrumentation/__tests__/autoGovern.test.js +3 -2
  17. package/dist/instrumentation/__tests__/costReporter.test.d.ts +5 -0
  18. package/dist/instrumentation/__tests__/costReporter.test.js +137 -0
  19. package/dist/instrumentation/autoGovern.js +63 -1
  20. package/dist/instrumentation/costReporter.d.ts +27 -0
  21. package/dist/instrumentation/costReporter.js +69 -0
  22. package/dist/instrumentation/evaluator.d.ts +8 -0
  23. package/dist/instrumentation/evaluator.js +124 -11
  24. package/dist/instrumentation/index.d.ts +3 -1
  25. package/dist/instrumentation/index.js +3 -1
  26. package/dist/instrumentation/types.d.ts +13 -0
  27. package/package.json +11 -4
  28. package/proto/events.proto +1 -1
  29. package/proto/fulcrum/agent/v1/agent_service.proto +1 -1
  30. package/proto/fulcrum/bridge/v1/bridge.proto +1 -1
  31. package/proto/fulcrum/checkpoint/v1/checkpoint_service.proto +1 -1
  32. package/proto/fulcrum/cost/v1/cost_service.proto +1 -1
  33. package/proto/fulcrum/envelope/v1/envelope.proto +1 -1
  34. package/proto/fulcrum/envelope/v1/envelope_service.proto +1 -1
  35. package/proto/fulcrum/eventstore/v1/eventstore.proto +1 -1
  36. package/proto/fulcrum/policy/v1/policy_service.proto +1 -1
  37. package/proto/fulcrum/tenant/v1/tenant_service.proto +1 -1
package/CHANGELOG.md CHANGED
@@ -7,8 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ### Added
11
- - Comprehensive test suite with >90% coverage target
10
+ ## [0.1.5] - 2026-06-03
11
+
12
+ ### Fixed
13
+ - Update `PolicyClient` to use the hosted gateway's `/api/v1/policies`
14
+ path and unwrap `{ policies, total_count }` list responses.
15
+ - Update `EnvelopeClient` to use the `/api/v1/envelopes/*` gateway paths and
16
+ handle unwrapped REST gateway responses.
17
+ - Allow REST envelope creation without a tenant ID because the hosted gateway
18
+ derives tenant context from the API key.
19
+
20
+ ## [0.1.4] - 2026-02-06
21
+
22
+ ### Changed
23
+ - Version bump to stay in sync with Python SDK
12
24
 
13
25
  ## [0.1.3] - 2026-01-22
14
26
 
@@ -84,8 +96,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
84
96
  - Basic gRPC-web connectivity
85
97
  - Policy evaluation stub
86
98
 
87
- [Unreleased]: https://github.com/fulcrum-dev/fulcrum/compare/sdk/typescript/v0.1.3...HEAD
88
- [0.1.3]: https://github.com/fulcrum-dev/fulcrum/compare/sdk/typescript/v0.1.2...sdk/typescript/v0.1.3
99
+ [Unreleased]: https://github.com/Fulcrum-Governance/fulcrum-io/compare/sdk/typescript/v0.1.5...HEAD
100
+ [0.1.5]: https://github.com/Fulcrum-Governance/fulcrum-io/compare/sdk/typescript/v0.1.4...sdk/typescript/v0.1.5
101
+ [0.1.4]: https://github.com/Fulcrum-Governance/fulcrum-io/compare/sdk/typescript/v0.1.3...sdk/typescript/v0.1.4
102
+ [0.1.3]: https://github.com/Fulcrum-Governance/fulcrum-io/compare/sdk/typescript/v0.1.2...sdk/typescript/v0.1.3
89
103
  [0.1.2]: https://github.com/fulcrum-dev/fulcrum/compare/sdk/typescript/v0.1.1...sdk/typescript/v0.1.2
90
104
  [0.1.1]: https://github.com/fulcrum-dev/fulcrum/compare/sdk/typescript/v0.1.0...sdk/typescript/v0.1.1
91
105
  [0.1.0]: https://github.com/fulcrum-dev/fulcrum/compare/sdk/typescript/v0.0.1...sdk/typescript/v0.1.0
package/README.md CHANGED
@@ -14,33 +14,45 @@ npm install @fulcrum-governance/sdk
14
14
 
15
15
  ## Quick Start
16
16
 
17
+ Hosted Fulcrum uses the REST API and `X-API-Key` authentication:
18
+
17
19
  ```typescript
18
- import { FulcrumClient } from '@fulcrum-governance/sdk';
20
+ import {
21
+ PolicyClient,
22
+ EnvelopeClient,
23
+ } from '@fulcrum-governance/sdk';
19
24
 
20
- // Initialize client
21
- const client = new FulcrumClient({
22
- host: 'your-fulcrum-server:50051',
23
- apiKey: 'your-api-key',
24
- onFailure: 'FAIL_OPEN' // or 'FAIL_CLOSED'
25
+ const apiBase = process.env.FULCRUM_API_BASE || 'https://api.fulcrumlayer.io';
26
+ const apiKey = process.env.FULCRUM_API_KEY!;
27
+
28
+ const policyClient = new PolicyClient({
29
+ baseUrl: apiBase,
30
+ apiKey,
25
31
  });
26
32
 
27
- // Wrap agent executions in governance envelopes
28
- const envelope = client.envelope({
29
- workflowId: 'customer-support-bot',
30
- tenantId: 'your-tenant-id'
33
+ const policies = await policyClient.list();
34
+ console.log(`Visible policies: ${policies.length}`);
35
+
36
+ const envelopes = new EnvelopeClient({
37
+ baseUrl: apiBase,
38
+ apiKey,
31
39
  });
32
40
 
33
- // Check if action is allowed before executing
34
- const allowed = await envelope.guard('send_email', userMessage);
41
+ const envelope = await envelopes.create({
42
+ workflow_id: 'customer-support-bot',
43
+ metadata: { example: 'typescript-sdk' },
44
+ });
35
45
 
36
- if (allowed) {
37
- // Action approved - proceed
38
- const result = await sendEmail(userMessage);
39
- envelope.log('email_sent', { recipient: email, status: 'success' });
40
- } else {
41
- // Action blocked by policy
42
- envelope.log('action_blocked', { reason: 'policy_violation' });
43
- }
46
+ await envelopes.updateStatus({
47
+ envelope_id: envelope.envelope_id,
48
+ status: 'RUNNING',
49
+ });
50
+
51
+ // Run your agent work here, then close the envelope.
52
+ await envelopes.updateStatus({
53
+ envelope_id: envelope.envelope_id,
54
+ status: 'COMPLETED',
55
+ });
44
56
  ```
45
57
 
46
58
  ## Features
@@ -72,16 +84,14 @@ const client = new FulcrumClient({
72
84
  ### Environment Variables
73
85
 
74
86
  ```bash
75
- export FULCRUM_HOST="localhost:50051"
76
87
  export FULCRUM_API_KEY="your-api-key"
77
- export FULCRUM_TENANT_ID="your-tenant-id"
88
+ export FULCRUM_API_BASE="https://api.fulcrumlayer.io"
78
89
  export FULCRUM_TIMEOUT_MS="500"
79
90
  ```
80
91
 
81
- ```typescript
82
- // Client auto-discovers from environment
83
- const client = FulcrumClient.fromEnv();
84
- ```
92
+ The lower-level `FulcrumClient` gRPC API is still available for compatible
93
+ self-hosted or direct gRPC deployments. The hosted first-run path uses the REST
94
+ clients above.
85
95
 
86
96
  ## API Reference
87
97
 
@@ -363,13 +373,14 @@ The SDK includes REST-based clients for managing Fulcrum resources through the D
363
373
 
364
374
  ### PolicyClient
365
375
 
366
- Manage governance policies programmatically:
376
+ List governance policies visible to your API key. Create and edit hosted
377
+ policies in the Fulcrum dashboard before using the API-key gateway.
367
378
 
368
379
  ```typescript
369
380
  import { PolicyClient } from '@fulcrum-governance/sdk';
370
381
 
371
382
  const policyClient = new PolicyClient({
372
- baseUrl: 'https://your-fulcrum-dashboard.com',
383
+ baseUrl: 'https://api.fulcrumlayer.io',
373
384
  apiKey: 'your-api-key',
374
385
  timeoutMs: 5000 // optional, default: 5000
375
386
  });
@@ -379,33 +390,11 @@ const policies = await policyClient.list();
379
390
 
380
391
  // List with filters
381
392
  const activePolicies = await policyClient.list({
382
- status: 'active',
383
- priority: 'high'
393
+ status: 'ACTIVE'
384
394
  });
385
395
 
386
396
  // Get a specific policy
387
397
  const policy = await policyClient.get('policy-id');
388
-
389
- // Create a new policy
390
- const newPolicy = await policyClient.create({
391
- name: 'No PII in Responses',
392
- description: 'Prevents sensitive data exposure',
393
- rules: [{ type: 'semantic', pattern: 'pii_detection' }],
394
- priority: 1,
395
- enabled: true
396
- });
397
-
398
- // Update a policy
399
- const updated = await policyClient.update('policy-id', {
400
- name: 'Updated Policy Name',
401
- priority: 2
402
- });
403
-
404
- // Enable/disable policies
405
- await policyClient.setEnabled('policy-id', false);
406
-
407
- // Delete a policy
408
- await policyClient.delete('policy-id');
409
398
  ```
410
399
 
411
400
  ### ApprovalClient
@@ -36,15 +36,35 @@ describe('PolicyClient', () => {
36
36
  describe('list', () => {
37
37
  it('should fetch policies successfully', async () => {
38
38
  const mockPolicies = [
39
- { id: '1', name: 'Policy 1', policy_type: 'cost', enabled: true },
40
- { id: '2', name: 'Policy 2', policy_type: 'rate', enabled: false },
39
+ {
40
+ id: '1',
41
+ name: 'Policy 1',
42
+ policy_type: 'cost',
43
+ rules: {},
44
+ enabled: true,
45
+ priority: 0,
46
+ status: 'ACTIVE',
47
+ created_at: '',
48
+ updated_at: '',
49
+ },
50
+ {
51
+ id: '2',
52
+ name: 'Policy 2',
53
+ policy_type: 'rate',
54
+ rules: {},
55
+ enabled: false,
56
+ priority: 0,
57
+ status: 'ACTIVE',
58
+ created_at: '',
59
+ updated_at: '',
60
+ },
41
61
  ];
42
62
  mockFetch.mockResolvedValueOnce({
43
63
  ok: true,
44
- json: async () => mockPolicies,
64
+ json: async () => ({ policies: mockPolicies, total_count: 2 }),
45
65
  });
46
66
  const policies = await client.list();
47
- expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies`, expect.objectContaining({
67
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/policies`, expect.objectContaining({
48
68
  method: 'GET',
49
69
  headers: { 'Content-Type': 'application/json' },
50
70
  }));
@@ -59,7 +79,7 @@ describe('PolicyClient', () => {
59
79
  expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('status=ACTIVE'), expect.any(Object));
60
80
  expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('policy_type=cost'), expect.any(Object));
61
81
  });
62
- it('should include Authorization header when apiKey is set', async () => {
82
+ it('should include X-API-Key header when apiKey is set', async () => {
63
83
  const authedClient = new clients_1.PolicyClient({ baseUrl, apiKey: 'my-key' });
64
84
  mockFetch.mockResolvedValueOnce({
65
85
  ok: true,
@@ -68,10 +88,38 @@ describe('PolicyClient', () => {
68
88
  await authedClient.list();
69
89
  expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
70
90
  headers: expect.objectContaining({
71
- Authorization: 'Bearer my-key',
91
+ 'X-API-Key': 'my-key',
72
92
  }),
73
93
  }));
74
94
  });
95
+ it('should normalize hosted gateway policy records', async () => {
96
+ mockFetch.mockResolvedValueOnce({
97
+ ok: true,
98
+ json: async () => ({
99
+ policies: [
100
+ {
101
+ policy_id: 'policy-gateway-1',
102
+ name: 'Gateway Policy',
103
+ status: 'POLICY_STATUS_ACTIVE',
104
+ priority: 7,
105
+ },
106
+ ],
107
+ total_count: 1,
108
+ }),
109
+ });
110
+ const policies = await client.list();
111
+ expect(policies).toEqual([
112
+ expect.objectContaining({
113
+ id: 'policy-gateway-1',
114
+ name: 'Gateway Policy',
115
+ policy_type: 'governance',
116
+ rules: {},
117
+ enabled: true,
118
+ status: 'ACTIVE',
119
+ priority: 7,
120
+ }),
121
+ ]);
122
+ });
75
123
  it('should throw PolicyClientError on failure', async () => {
76
124
  mockFetch.mockResolvedValueOnce({
77
125
  ok: false,
@@ -95,13 +143,19 @@ describe('PolicyClient', () => {
95
143
  id: '123',
96
144
  name: 'Test Policy',
97
145
  policy_type: 'cost',
146
+ rules: {},
147
+ enabled: true,
148
+ priority: 0,
149
+ status: 'ACTIVE',
150
+ created_at: '',
151
+ updated_at: '',
98
152
  };
99
153
  mockFetch.mockResolvedValueOnce({
100
154
  ok: true,
101
155
  json: async () => mockPolicy,
102
156
  });
103
157
  const policy = await client.get('123');
104
- expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies/123`, expect.objectContaining({ method: 'GET' }));
158
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/policies/123`, expect.objectContaining({ method: 'GET' }));
105
159
  expect(policy).toEqual(mockPolicy);
106
160
  });
107
161
  it('should throw on 404', async () => {
@@ -120,13 +174,21 @@ describe('PolicyClient', () => {
120
174
  policy_type: 'cost',
121
175
  rules: { max_cost: 100 },
122
176
  };
123
- const createdPolicy = { id: '456', ...newPolicy, enabled: true };
177
+ const createdPolicy = {
178
+ id: '456',
179
+ ...newPolicy,
180
+ enabled: true,
181
+ priority: 0,
182
+ status: 'ACTIVE',
183
+ created_at: '',
184
+ updated_at: '',
185
+ };
124
186
  mockFetch.mockResolvedValueOnce({
125
187
  ok: true,
126
188
  json: async () => createdPolicy,
127
189
  });
128
190
  const result = await client.create(newPolicy);
129
- expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies`, expect.objectContaining({
191
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/policies`, expect.objectContaining({
130
192
  method: 'POST',
131
193
  body: JSON.stringify(newPolicy),
132
194
  }));
@@ -159,7 +221,7 @@ describe('PolicyClient', () => {
159
221
  json: async () => ({ id: '123', ...updates }),
160
222
  });
161
223
  const result = await client.update('123', updates);
162
- expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies/123`, expect.objectContaining({
224
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/policies/123`, expect.objectContaining({
163
225
  method: 'PATCH',
164
226
  body: JSON.stringify(updates),
165
227
  }));
@@ -173,7 +235,7 @@ describe('PolicyClient', () => {
173
235
  json: async () => ({}),
174
236
  });
175
237
  await expect(client.delete('123')).resolves.toBeUndefined();
176
- expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies/123`, expect.objectContaining({ method: 'DELETE' }));
238
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/policies/123`, expect.objectContaining({ method: 'DELETE' }));
177
239
  });
178
240
  });
179
241
  describe('setEnabled', () => {
@@ -183,7 +245,7 @@ describe('PolicyClient', () => {
183
245
  json: async () => ({ id: '123', enabled: true }),
184
246
  });
185
247
  const result = await client.setEnabled('123', true);
186
- expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies/123`, expect.objectContaining({
248
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/policies/123`, expect.objectContaining({
187
249
  method: 'PATCH',
188
250
  body: JSON.stringify({ enabled: true }),
189
251
  }));
@@ -586,6 +648,59 @@ describe('BudgetClient', () => {
586
648
  });
587
649
  });
588
650
  });
651
+ describe('EnvelopeClient', () => {
652
+ const baseUrl = 'http://localhost:3000';
653
+ let client;
654
+ beforeEach(() => {
655
+ jest.clearAllMocks();
656
+ client = new clients_1.EnvelopeClient({ baseUrl, apiKey: 'test-key' });
657
+ });
658
+ it('should create envelope through the REST gateway path', async () => {
659
+ mockFetch.mockResolvedValueOnce({
660
+ ok: true,
661
+ json: async () => ({
662
+ envelope_id: 'env-1',
663
+ status: 'ENVELOPE_STATUS_AUTHORIZED',
664
+ }),
665
+ });
666
+ const envelope = await client.create({
667
+ adapter_type: 'typescript-sdk',
668
+ workflow_id: 'quickstart-agent',
669
+ metadata: { source: 'test' },
670
+ });
671
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/envelopes/create`, expect.objectContaining({ method: 'POST' }));
672
+ expect(envelope.envelope_id).toBe('env-1');
673
+ });
674
+ it('should update envelope status through the REST gateway path', async () => {
675
+ mockFetch.mockResolvedValueOnce({
676
+ ok: true,
677
+ json: async () => ({
678
+ envelope_id: 'env-1',
679
+ status: 'ENVELOPE_STATUS_COMPLETED',
680
+ }),
681
+ });
682
+ const envelope = await client.updateStatus({
683
+ envelope_id: 'env-1',
684
+ status: 'COMPLETED',
685
+ error_message: 'quickstart complete',
686
+ });
687
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/envelopes/status`, expect.objectContaining({ method: 'POST' }));
688
+ expect(envelope.status).toBe('ENVELOPE_STATUS_COMPLETED');
689
+ });
690
+ it('should fetch one envelope through the REST gateway query path', async () => {
691
+ mockFetch.mockResolvedValueOnce({
692
+ ok: true,
693
+ json: async () => ({
694
+ envelope_id: 'env-1',
695
+ status: 'ENVELOPE_STATUS_RUNNING',
696
+ metadata: {},
697
+ }),
698
+ });
699
+ const envelope = await client.get('env-1');
700
+ expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/v1/envelopes/get?envelope_id=env-1`, expect.objectContaining({ method: 'GET' }));
701
+ expect(envelope.envelope_id).toBe('env-1');
702
+ });
703
+ });
589
704
  describe('Error classes', () => {
590
705
  describe('PolicyClientError', () => {
591
706
  it('should have correct name and properties', () => {
@@ -42,21 +42,27 @@ const protoLoader = __importStar(require("@grpc/proto-loader"));
42
42
  const util_1 = require("util");
43
43
  const path_1 = __importDefault(require("path"));
44
44
  const PROTO_PATH = path_1.default.join(__dirname, '../../proto/fulcrum/agent/v1/agent_service.proto');
45
- const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
46
- keepCase: true,
47
- longs: String,
48
- enums: String,
49
- defaults: true,
50
- oneofs: true,
51
- });
52
- const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
53
- const fulcrumPackage = protoDescriptor.fulcrum;
54
- const agentPackage = fulcrumPackage.agent;
55
- const agentV1 = agentPackage.v1;
45
+ let agentV1;
46
+ function getAgentV1() {
47
+ if (!agentV1) {
48
+ const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
49
+ keepCase: true,
50
+ longs: String,
51
+ enums: String,
52
+ defaults: true,
53
+ oneofs: true,
54
+ });
55
+ const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
56
+ const fulcrumPackage = protoDescriptor.fulcrum;
57
+ const agentPackage = fulcrumPackage.agent;
58
+ agentV1 = agentPackage.v1;
59
+ }
60
+ return agentV1;
61
+ }
56
62
  class AgentClient {
57
63
  constructor(address, credentials) {
58
64
  const creds = credentials || grpc.credentials.createInsecure();
59
- this.client = new agentV1.AgentService(address, creds);
65
+ this.client = new (getAgentV1()).AgentService(address, creds);
60
66
  }
61
67
  /**
62
68
  * Creates a new agent for the authenticated tenant.
@@ -20,7 +20,7 @@ class ApprovalClient {
20
20
  'Content-Type': 'application/json',
21
21
  };
22
22
  if (this.apiKey) {
23
- headers['Authorization'] = `Bearer ${this.apiKey}`;
23
+ headers['X-API-Key'] = this.apiKey;
24
24
  }
25
25
  const response = await fetch(`${this.baseUrl}${path}`, {
26
26
  method,
@@ -30,7 +30,20 @@ class ApprovalClient {
30
30
  });
31
31
  if (!response.ok) {
32
32
  const errorData = (await response.json().catch(() => ({})));
33
- throw new ApprovalClientError(errorData.error || `Request failed with status ${response.status}`, response.status);
33
+ const serverMessage = errorData.error || errorData.message || errorData.detail || '';
34
+ let errorMessage;
35
+ if (response.status === 401) {
36
+ errorMessage = this.apiKey
37
+ ? `Authentication failed: ${serverMessage || 'invalid API key'}. Verify your API key is correct and active. Check your keys in the Fulcrum dashboard under Settings > API Keys.`
38
+ : 'Authentication required: no API key configured. Pass apiKey when creating the client. Generate a key in the Fulcrum dashboard under Settings > API Keys.';
39
+ }
40
+ else if (response.status === 403) {
41
+ errorMessage = `Insufficient permissions: ${serverMessage || 'access denied'}. Your API key does not have the required scopes. Update key permissions in the Fulcrum dashboard under Settings > API Keys.`;
42
+ }
43
+ else {
44
+ errorMessage = serverMessage || `Request failed with status ${response.status}`;
45
+ }
46
+ throw new ApprovalClientError(errorMessage, response.status);
34
47
  }
35
48
  return (await response.json());
36
49
  }
@@ -20,7 +20,7 @@ class BudgetClient {
20
20
  'Content-Type': 'application/json',
21
21
  };
22
22
  if (this.apiKey) {
23
- headers['Authorization'] = `Bearer ${this.apiKey}`;
23
+ headers['X-API-Key'] = this.apiKey;
24
24
  }
25
25
  const response = await fetch(`${this.baseUrl}${path}`, {
26
26
  method,
@@ -30,7 +30,20 @@ class BudgetClient {
30
30
  });
31
31
  if (!response.ok) {
32
32
  const errorData = (await response.json().catch(() => ({})));
33
- throw new BudgetClientError(errorData.error || `Request failed with status ${response.status}`, response.status);
33
+ const serverMessage = errorData.error || errorData.message || errorData.detail || '';
34
+ let errorMessage;
35
+ if (response.status === 401) {
36
+ errorMessage = this.apiKey
37
+ ? `Authentication failed: ${serverMessage || 'invalid API key'}. Verify your API key is correct and active. Check your keys in the Fulcrum dashboard under Settings > API Keys.`
38
+ : 'Authentication required: no API key configured. Pass apiKey when creating the client. Generate a key in the Fulcrum dashboard under Settings > API Keys.';
39
+ }
40
+ else if (response.status === 403) {
41
+ errorMessage = `Insufficient permissions: ${serverMessage || 'access denied'}. Your API key does not have the required scopes. Update key permissions in the Fulcrum dashboard under Settings > API Keys.`;
42
+ }
43
+ else {
44
+ errorMessage = serverMessage || `Request failed with status ${response.status}`;
45
+ }
46
+ throw new BudgetClientError(errorMessage, response.status);
34
47
  }
35
48
  return (await response.json());
36
49
  }
@@ -20,7 +20,7 @@ class CheckpointClient {
20
20
  'Content-Type': 'application/json',
21
21
  };
22
22
  if (this.apiKey) {
23
- headers['Authorization'] = `Bearer ${this.apiKey}`;
23
+ headers['X-API-Key'] = this.apiKey;
24
24
  }
25
25
  const response = await fetch(`${this.baseUrl}${path}`, {
26
26
  method,
@@ -30,7 +30,20 @@ class CheckpointClient {
30
30
  });
31
31
  if (!response.ok) {
32
32
  const errorData = (await response.json().catch(() => ({})));
33
- throw new CheckpointClientError(errorData.error || `Request failed with status ${response.status}`, response.status);
33
+ const serverMessage = errorData.error || errorData.message || errorData.detail || '';
34
+ let errorMessage;
35
+ if (response.status === 401) {
36
+ errorMessage = this.apiKey
37
+ ? `Authentication failed: ${serverMessage || 'invalid API key'}. Verify your API key is correct and active. Check your keys in the Fulcrum dashboard under Settings > API Keys.`
38
+ : 'Authentication required: no API key configured. Pass apiKey when creating the client. Generate a key in the Fulcrum dashboard under Settings > API Keys.';
39
+ }
40
+ else if (response.status === 403) {
41
+ errorMessage = `Insufficient permissions: ${serverMessage || 'access denied'}. Your API key does not have the required scopes. Update key permissions in the Fulcrum dashboard under Settings > API Keys.`;
42
+ }
43
+ else {
44
+ errorMessage = serverMessage || `Request failed with status ${response.status}`;
45
+ }
46
+ throw new CheckpointClientError(errorMessage, response.status);
34
47
  }
35
48
  const text = await response.text();
36
49
  return text ? JSON.parse(text) : {};
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Works with the Fulcrum API for managing execution envelopes that wrap AI agent operations.
5
5
  */
6
- export type EnvelopeStatus = 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'TIMEOUT';
6
+ export type EnvelopeStatus = 'PENDING' | 'AUTHORIZED' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'TERMINATED' | 'TIMEOUT' | 'ENVELOPE_STATUS_AUTHORIZED' | 'ENVELOPE_STATUS_RUNNING' | 'ENVELOPE_STATUS_COMPLETED' | 'ENVELOPE_STATUS_FAILED' | 'ENVELOPE_STATUS_TERMINATED';
7
7
  export interface EnvelopeData {
8
8
  envelope_id: string;
9
9
  tenant_id: string;
@@ -20,9 +20,9 @@ export interface EnvelopeData {
20
20
  error_message?: string;
21
21
  }
22
22
  export interface CreateEnvelopeRequest {
23
- tenant_id: string;
24
- adapter_type: string;
25
- workflow_id: string;
23
+ tenant_id?: string;
24
+ adapter_type?: string;
25
+ workflow_id?: string;
26
26
  budget_id?: string;
27
27
  metadata?: Record<string, string>;
28
28
  }
@@ -54,6 +54,7 @@ export declare class EnvelopeClient {
54
54
  private timeout;
55
55
  constructor(options: EnvelopeClientOptions);
56
56
  private request;
57
+ private unwrapEnvelope;
57
58
  /**
58
59
  * Create a new execution envelope.
59
60
  */
@@ -43,8 +43,21 @@ class EnvelopeClient {
43
43
  signal: controller.signal,
44
44
  });
45
45
  if (!response.ok) {
46
- const error = await response.json().catch(() => ({}));
47
- throw new EnvelopeClientError(error.message || `Request failed with status ${response.status}`, response.status, error);
46
+ const errorData = await response.json().catch(() => ({}));
47
+ const serverMessage = errorData.error || errorData.message || errorData.detail || '';
48
+ let errorMessage;
49
+ if (response.status === 401) {
50
+ errorMessage = this.apiKey
51
+ ? `Authentication failed: ${serverMessage || 'invalid API key'}. Verify your API key is correct and active. Check your keys in the Fulcrum dashboard under Settings > API Keys.`
52
+ : 'Authentication required: no API key configured. Pass apiKey when creating the client. Generate a key in the Fulcrum dashboard under Settings > API Keys.';
53
+ }
54
+ else if (response.status === 403) {
55
+ errorMessage = `Insufficient permissions: ${serverMessage || 'access denied'}. Your API key does not have the required scopes. Update key permissions in the Fulcrum dashboard under Settings > API Keys.`;
56
+ }
57
+ else {
58
+ errorMessage = serverMessage || `Request failed with status ${response.status}`;
59
+ }
60
+ throw new EnvelopeClientError(errorMessage, response.status, errorData);
48
61
  }
49
62
  return await response.json();
50
63
  }
@@ -52,27 +65,43 @@ class EnvelopeClient {
52
65
  clearTimeout(timeoutId);
53
66
  }
54
67
  }
68
+ unwrapEnvelope(response) {
69
+ return 'envelope' in response ? response.envelope : response;
70
+ }
55
71
  /**
56
72
  * Create a new execution envelope.
57
73
  */
58
74
  async create(request) {
59
- const response = await this.request('POST', '/v1/envelopes', request);
60
- return response.envelope;
75
+ const metadata = { ...(request.metadata || {}) };
76
+ if (request.workflow_id) {
77
+ metadata.workflow_id = metadata.workflow_id || request.workflow_id;
78
+ }
79
+ const body = {
80
+ adapter_type: request.adapter_type || 'typescript-sdk',
81
+ ...(request.budget_id ? { budget_id: request.budget_id } : {}),
82
+ metadata,
83
+ };
84
+ const response = await this.request('POST', '/api/v1/envelopes/create', body);
85
+ return this.unwrapEnvelope(response);
61
86
  }
62
87
  /**
63
88
  * Get an envelope by ID.
64
89
  */
65
90
  async get(envelopeId) {
66
- const response = await this.request('GET', `/v1/envelopes/${envelopeId}`);
67
- return response.envelope;
91
+ const response = await this.request('GET', '/api/v1/envelopes/get', undefined, { envelope_id: envelopeId });
92
+ return this.unwrapEnvelope(response);
68
93
  }
69
94
  /**
70
95
  * Update the status of an envelope.
71
96
  */
72
97
  async updateStatus(request) {
73
- const { envelope_id, ...body } = request;
74
- const response = await this.request('PATCH', `/v1/envelopes/${envelope_id}/status`, body);
75
- return response.envelope;
98
+ const body = {
99
+ envelope_id: request.envelope_id,
100
+ status: request.status,
101
+ ...(request.error_message ? { reason: request.error_message } : {}),
102
+ };
103
+ const response = await this.request('POST', '/api/v1/envelopes/status', body);
104
+ return this.unwrapEnvelope(response);
76
105
  }
77
106
  /**
78
107
  * List envelopes with optional filters.
@@ -88,7 +117,7 @@ class EnvelopeClient {
88
117
  params.workflow_id = request.workflow_id;
89
118
  if (request.status)
90
119
  params.status = request.status;
91
- const response = await this.request('GET', '/v1/envelopes', undefined, params);
120
+ const response = await this.request('GET', '/api/v1/envelopes', undefined, params);
92
121
  return response.envelopes;
93
122
  }
94
123
  }