oceum 0.1.1 → 0.2.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 (4) hide show
  1. package/README.md +82 -1
  2. package/index.d.ts +49 -0
  3. package/index.js +175 -52
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # oceum
2
2
 
3
- Official SDK for [Oceum](https://oceum.ai) — the management platform for AI agents. Monitor, log, and orchestrate your agents from one dashboard.
3
+ Official SDK for [Oceum](https://oceum.ai) — the command layer for AI agents. Monitor, log, track costs, and orchestrate your agents from one dashboard.
4
4
 
5
5
  Zero dependencies. Works with Node.js 18+, Deno, and Bun.
6
6
 
@@ -30,11 +30,56 @@ const result = await client.wrap('Process leads', async () => {
30
30
  return leads.length;
31
31
  });
32
32
 
33
+ // Report LLM usage for cost tracking
34
+ await client.reportUsage({
35
+ model: 'claude-sonnet',
36
+ tokensInput: 1200,
37
+ tokensOutput: 450,
38
+ });
39
+
33
40
  // Clean up
34
41
  client.stopHeartbeat();
35
42
  await client.setStatus('idle');
36
43
  ```
37
44
 
45
+ ## No SDK? No Problem
46
+
47
+ Oceum is just HTTP. Any language, any framework — POST JSON to the webhook endpoint:
48
+
49
+ ```bash
50
+ # Heartbeat
51
+ curl -X POST https://oceum.ai/api/webhook \
52
+ -H "Authorization: Bearer $OCEUM_API_KEY" \
53
+ -H "Content-Type: application/json" \
54
+ -d '{"event":"heartbeat","agentId":"agt_xxx"}'
55
+
56
+ # Report a completed task
57
+ curl -X POST https://oceum.ai/api/webhook \
58
+ -H "Authorization: Bearer $OCEUM_API_KEY" \
59
+ -H "Content-Type: application/json" \
60
+ -d '{"event":"task_complete","agentId":"agt_xxx","data":{"taskName":"sync-orders"}}'
61
+
62
+ # Report LLM usage
63
+ curl -X POST https://oceum.ai/api/webhook \
64
+ -H "Authorization: Bearer $OCEUM_API_KEY" \
65
+ -H "Content-Type: application/json" \
66
+ -d '{"event":"usage","agentId":"agt_xxx","data":{"model":"gpt-4o","tokensInput":500,"tokensOutput":200}}'
67
+ ```
68
+
69
+ **Python:**
70
+
71
+ ```python
72
+ import requests
73
+
74
+ API_KEY = "oc_xxx"
75
+ AGENT_ID = "agt_xxx"
76
+
77
+ requests.post("https://oceum.ai/api/webhook",
78
+ headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
79
+ json={"event": "heartbeat", "agentId": AGENT_ID}
80
+ )
81
+ ```
82
+
38
83
  ## API
39
84
 
40
85
  | Method | Description |
@@ -48,6 +93,34 @@ await client.setStatus('idle');
48
93
  | `startHeartbeat(ms?)` | Auto-heartbeat every N ms (default: 60s) |
49
94
  | `stopHeartbeat()` | Stop auto-heartbeat |
50
95
  | `wrap(name, fn, meta?)` | Auto start/complete/error around async fn |
96
+ | `reportUsage(opts?)` | Report LLM token usage for cost tracking |
97
+ | `memory(content, opts?)` | Write shared memory visible to peer agents |
98
+ | `readMemory(opts?)` | Read shared memory entries |
99
+ | `vaultStore(data, opts?)` | Store sensitive data, returns vault token |
100
+ | `vaultRetrieve(token)` | Retrieve data using vault token |
101
+ | `vaultProxy(token, req)` | Zero-knowledge API call with vault credential |
102
+ | `vaultRevoke(token)` | Permanently revoke a vault token |
103
+ | `vaultList(opts?)` | List vault tokens (metadata only) |
104
+
105
+ ## Cost Tracking
106
+
107
+ Report LLM token usage per call. Oceum calculates cost using configurable model pricing and enforces budget caps automatically.
108
+
109
+ ```javascript
110
+ // After an LLM call
111
+ await client.reportUsage({
112
+ model: 'claude-haiku',
113
+ tokensInput: 800,
114
+ tokensOutput: 200,
115
+ });
116
+
117
+ // Or include usage in task_complete meta for automatic tracking
118
+ await client.taskComplete('generate-report', {
119
+ usage: { model: 'gpt-4o', tokensInput: 2000, tokensOutput: 1500 }
120
+ });
121
+ ```
122
+
123
+ Budget caps, alert thresholds, and per-model pricing are configured in the Oceum dashboard under Settings > Cost Controls.
51
124
 
52
125
  ## Error Handling
53
126
 
@@ -64,6 +137,14 @@ try {
64
137
  }
65
138
  ```
66
139
 
140
+ ## Webhook Verification
141
+
142
+ Verify incoming webhook signatures with HMAC-SHA256:
143
+
144
+ ```javascript
145
+ const valid = Oceum.verifyWebhookSignature(rawBody, signature, secret);
146
+ ```
147
+
67
148
  ## Docs
68
149
 
69
150
  Full documentation: [oceum.ai/docs](https://oceum.ai/docs.html)
package/index.d.ts CHANGED
@@ -70,6 +70,9 @@ export class Oceum {
70
70
  /** Wrap an async function with automatic task_start, task_complete, and error reporting. */
71
71
  wrap<T>(taskName: string, fn: () => T | Promise<T>, meta?: Record<string, unknown>): Promise<T>;
72
72
 
73
+ /** Report LLM token usage for cost tracking and budget enforcement. */
74
+ reportUsage(options?: UsageReportOptions): Promise<UsageReportResponse>;
75
+
73
76
  /** Write a shared memory entry visible to peer agents. */
74
77
  memory(content: string, options?: MemoryWriteOptions): Promise<WebhookResponse>;
75
78
 
@@ -82,11 +85,23 @@ export class Oceum {
82
85
  /** Retrieve sensitive data from the Vault using a vault token. */
83
86
  vaultRetrieve(token: string): Promise<VaultRetrieveResponse>;
84
87
 
88
+ /**
89
+ * Zero-Knowledge Vault Proxy — make an API call with a vault-stored credential
90
+ * without ever seeing the raw secret.
91
+ */
92
+ vaultProxy(token: string, request: VaultProxyRequest): Promise<VaultProxyResponse>;
93
+
85
94
  /** Revoke a vault token permanently. */
86
95
  vaultRevoke(token: string): Promise<{ ok: boolean; token: string; revoked: boolean }>;
87
96
 
88
97
  /** List vault tokens (metadata only). */
89
98
  vaultList(options?: VaultListOptions): Promise<VaultListResponse>;
99
+
100
+ /**
101
+ * Verify a webhook signature using HMAC-SHA256 with timing-safe comparison.
102
+ * @returns true if the signature is valid
103
+ */
104
+ static verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
90
105
  }
91
106
 
92
107
  export interface MemoryWriteOptions {
@@ -130,6 +145,22 @@ export interface VaultStoreOptions {
130
145
  allowedAgents?: string;
131
146
  ttl?: '1h' | '6h' | '24h' | '7d' | '30d' | 'permanent';
132
147
  label?: string;
148
+ targetDomains?: string[];
149
+ injectionTemplate?: string;
150
+ }
151
+
152
+ export interface VaultProxyRequest {
153
+ method: string;
154
+ url: string;
155
+ headers?: Record<string, string>;
156
+ body?: unknown;
157
+ }
158
+
159
+ export interface VaultProxyResponse {
160
+ ok: boolean;
161
+ status: number;
162
+ headers: Record<string, string>;
163
+ data: unknown;
133
164
  }
134
165
 
135
166
  export interface VaultStoreResponse {
@@ -171,3 +202,21 @@ export interface VaultListResponse {
171
202
  entries: VaultEntry[];
172
203
  count: number;
173
204
  }
205
+
206
+ export interface UsageReportOptions {
207
+ /** Model name (e.g. 'claude-haiku', 'gpt-4o') */
208
+ model?: string;
209
+ /** Input/prompt tokens */
210
+ tokensInput?: number;
211
+ /** Output/completion tokens */
212
+ tokensOutput?: number;
213
+ /** Additional metadata */
214
+ meta?: Record<string, unknown>;
215
+ }
216
+
217
+ export interface UsageReportResponse {
218
+ ok: boolean;
219
+ event: string;
220
+ agentId: string;
221
+ costUsd: number;
222
+ }
package/index.js CHANGED
@@ -1,5 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
4
+
5
+ const MAX_RETRIES = 3;
6
+ const BACKOFF_BASE_MS = 1000;
7
+
3
8
  class OceumError extends Error {
4
9
  constructor(message, statusCode, body) {
5
10
  super(message);
@@ -31,33 +36,146 @@ class Oceum {
31
36
  this.#heartbeatTimer = null;
32
37
  }
33
38
 
39
+ // ── Helpers ─────────────────────────────────────────
40
+
41
+ #sleep(ms) {
42
+ return new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
+
34
45
  // ── Core transport ─────────────────────────────────
35
46
 
36
47
  async #send(event, data = {}) {
37
- const res = await fetch(`${this.#baseUrl}/api/webhook`, {
38
- method: 'POST',
39
- headers: {
40
- 'Content-Type': 'application/json',
41
- 'Authorization': `Bearer ${this.#apiKey}`,
42
- },
43
- body: JSON.stringify({
44
- event,
45
- agentId: this.#agentId,
46
- data,
47
- }),
48
- });
48
+ let lastError;
49
+
50
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
51
+ try {
52
+ const res = await fetch(`${this.#baseUrl}/api/webhook`, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'Authorization': `Bearer ${this.#apiKey}`,
57
+ },
58
+ body: JSON.stringify({
59
+ event,
60
+ agentId: this.#agentId,
61
+ data,
62
+ }),
63
+ });
64
+
65
+ // 429 — respect Retry-After header
66
+ if (res.status === 429 && attempt < MAX_RETRIES) {
67
+ const retryAfter = parseInt(res.headers.get('Retry-After'), 10);
68
+ const waitMs = (retryAfter > 0 ? retryAfter : 1) * 1000;
69
+ await this.#sleep(waitMs);
70
+ continue;
71
+ }
72
+
73
+ // 5xx — exponential backoff
74
+ if (res.status >= 500 && attempt < MAX_RETRIES) {
75
+ await this.#sleep(BACKOFF_BASE_MS * Math.pow(2, attempt));
76
+ continue;
77
+ }
78
+
79
+ const body = await res.json().catch(() => ({}));
80
+
81
+ if (!res.ok) {
82
+ throw new OceumError(
83
+ body.error || `HTTP ${res.status}`,
84
+ res.status,
85
+ body
86
+ );
87
+ }
88
+
89
+ return body;
90
+ } catch (err) {
91
+ lastError = err;
92
+ // If it's already an OceumError (non-retryable status), rethrow immediately
93
+ if (err instanceof OceumError) throw err;
94
+ // Network error — retry with backoff
95
+ if (attempt < MAX_RETRIES) {
96
+ await this.#sleep(BACKOFF_BASE_MS * Math.pow(2, attempt));
97
+ continue;
98
+ }
99
+ }
100
+ }
49
101
 
50
- const body = await res.json().catch(() => ({}));
102
+ throw lastError instanceof OceumError
103
+ ? lastError
104
+ : new OceumError(lastError?.message || 'Request failed after retries', 0, {});
105
+ }
51
106
 
52
- if (!res.ok) {
53
- throw new OceumError(
54
- body.error || `HTTP ${res.status}`,
55
- res.status,
56
- body
57
- );
107
+ async #get(url) {
108
+ let lastError;
109
+
110
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
111
+ try {
112
+ const res = await fetch(url, {
113
+ headers: { 'Authorization': `Bearer ${this.#apiKey}` },
114
+ });
115
+
116
+ // 429 — respect Retry-After header
117
+ if (res.status === 429 && attempt < MAX_RETRIES) {
118
+ const retryAfter = parseInt(res.headers.get('Retry-After'), 10);
119
+ const waitMs = (retryAfter > 0 ? retryAfter : 1) * 1000;
120
+ await this.#sleep(waitMs);
121
+ continue;
122
+ }
123
+
124
+ // 5xx — exponential backoff
125
+ if (res.status >= 500 && attempt < MAX_RETRIES) {
126
+ await this.#sleep(BACKOFF_BASE_MS * Math.pow(2, attempt));
127
+ continue;
128
+ }
129
+
130
+ const body = await res.json().catch(() => ({}));
131
+
132
+ if (!res.ok) {
133
+ throw new OceumError(
134
+ body.error || `HTTP ${res.status}`,
135
+ res.status,
136
+ body
137
+ );
138
+ }
139
+
140
+ return body;
141
+ } catch (err) {
142
+ lastError = err;
143
+ if (err instanceof OceumError) throw err;
144
+ if (attempt < MAX_RETRIES) {
145
+ await this.#sleep(BACKOFF_BASE_MS * Math.pow(2, attempt));
146
+ continue;
147
+ }
148
+ }
58
149
  }
59
150
 
60
- return body;
151
+ throw lastError instanceof OceumError
152
+ ? lastError
153
+ : new OceumError(lastError?.message || 'Request failed after retries', 0, {});
154
+ }
155
+
156
+ // ── Webhook signature verification ──────────────────
157
+
158
+ /**
159
+ * Verify a webhook signature using HMAC-SHA256 with timing-safe comparison.
160
+ * @param {string} payload - Raw request body string
161
+ * @param {string} signature - Signature from the X-Oceum-Signature header
162
+ * @param {string} secret - Your webhook signing secret
163
+ * @returns {boolean}
164
+ */
165
+ static verifyWebhookSignature(payload, signature, secret) {
166
+ const expected = crypto
167
+ .createHmac('sha256', secret)
168
+ .update(payload)
169
+ .digest('hex');
170
+ try {
171
+ return crypto.timingSafeEqual(
172
+ Buffer.from(expected, 'utf8'),
173
+ Buffer.from(signature, 'utf8')
174
+ );
175
+ } catch {
176
+ // Lengths differ — not equal
177
+ return false;
178
+ }
61
179
  }
62
180
 
63
181
  // ── Events ─────────────────────────────────────────
@@ -187,6 +305,20 @@ class Oceum {
187
305
  }
188
306
  }
189
307
 
308
+ // ── Usage / Cost Tracking ──────────────────────────────
309
+
310
+ /**
311
+ * Report LLM token usage for cost tracking and budget enforcement.
312
+ * @param {Object} options
313
+ * @param {string} [options.model] - Model name (e.g. 'claude-haiku', 'gpt-4o')
314
+ * @param {number} [options.tokensInput=0] - Input/prompt tokens
315
+ * @param {number} [options.tokensOutput=0] - Output/completion tokens
316
+ * @param {Object} [options.meta] - Additional metadata
317
+ */
318
+ async reportUsage({ model, tokensInput = 0, tokensOutput = 0, meta } = {}) {
319
+ return this.#send('usage', { model, tokensInput, tokensOutput, meta });
320
+ }
321
+
190
322
  // ── Shared Memory ────────────────────────────────────
191
323
 
192
324
  /**
@@ -221,21 +353,7 @@ class Oceum {
221
353
  if (tag) params.set('tag', tag);
222
354
  params.set('limit', String(limit));
223
355
 
224
- const res = await fetch(`${this.#baseUrl}/api/memory?${params}`, {
225
- headers: { 'Authorization': `Bearer ${this.#apiKey}` },
226
- });
227
-
228
- const body = await res.json().catch(() => ({}));
229
-
230
- if (!res.ok) {
231
- throw new OceumError(
232
- body.error || `HTTP ${res.status}`,
233
- res.status,
234
- body
235
- );
236
- }
237
-
238
- return body;
356
+ return this.#get(`${this.#baseUrl}/api/memory?${params}`);
239
357
  }
240
358
 
241
359
  // ── Vault (Secure Data) ──────────────────────────
@@ -250,9 +368,11 @@ class Oceum {
250
368
  * @param {string} [options.allowedAgents] - Comma-separated agent IDs (for specified_agents)
251
369
  * @param {'1h'|'6h'|'24h'|'7d'|'30d'|'permanent'} [options.ttl='30d']
252
370
  * @param {string} [options.label] - Human-readable label
371
+ * @param {string} [options.targetDomains] - Comma-separated domain allowlist for vault proxy (e.g. 'api.stripe.com,api.openai.com')
372
+ * @param {string} [options.injectionTemplate] - How to inject the secret into proxied requests (default: 'header:Authorization:Bearer {{secret}}')
253
373
  */
254
- async vaultStore(data, { category = 'custom', accessPolicy = 'source_agent', allowedAgents, ttl = '30d', label } = {}) {
255
- return this.#send('vault_store', { data, category, accessPolicy, allowedAgents, ttl, label });
374
+ async vaultStore(data, { category = 'custom', accessPolicy = 'source_agent', allowedAgents, ttl = '30d', label, targetDomains, injectionTemplate } = {}) {
375
+ return this.#send('vault_store', { data, category, accessPolicy, allowedAgents, ttl, label, targetDomains, injectionTemplate });
256
376
  }
257
377
 
258
378
  /**
@@ -263,6 +383,23 @@ class Oceum {
263
383
  return this.#send('vault_retrieve', { token });
264
384
  }
265
385
 
386
+ /**
387
+ * Zero-Knowledge Vault Proxy — make an API call with a vault-stored credential
388
+ * without ever seeing the raw secret. The platform decrypts and injects the
389
+ * credential into the outgoing request, then returns only the API response.
390
+ *
391
+ * @param {string} token - Vault token (vtk_xxx) containing the credential
392
+ * @param {Object} request - The outgoing HTTP request specification
393
+ * @param {string} request.method - HTTP method (GET, POST, PUT, DELETE, PATCH)
394
+ * @param {string} request.url - Target API URL (must match token's target_domains allowlist)
395
+ * @param {Object} [request.headers] - Additional headers (credential injected separately via template)
396
+ * @param {*} [request.body] - Request body (object for JSON, string for raw)
397
+ * @returns {{ ok: boolean, status: number, headers: Object, data: * }}
398
+ */
399
+ async vaultProxy(token, { method, url, headers, body } = {}) {
400
+ return this.#send('vault_proxy', { token, method, url, headers, body });
401
+ }
402
+
266
403
  /**
267
404
  * Revoke a vault token, permanently preventing future retrievals.
268
405
  * @param {string} token - Vault token (vtk_xxx)
@@ -303,21 +440,7 @@ class Oceum {
303
440
  params.set('status', status);
304
441
  params.set('limit', String(limit));
305
442
 
306
- const res = await fetch(`${this.#baseUrl}/api/vault?${params}`, {
307
- headers: { 'Authorization': `Bearer ${this.#apiKey}` },
308
- });
309
-
310
- const body = await res.json().catch(() => ({}));
311
-
312
- if (!res.ok) {
313
- throw new OceumError(
314
- body.error || `HTTP ${res.status}`,
315
- res.status,
316
- body
317
- );
318
- }
319
-
320
- return body;
443
+ return this.#get(`${this.#baseUrl}/api/vault?${params}`);
321
444
  }
322
445
 
323
446
  // ── Getters ────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oceum",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Official SDK for Oceum — the management platform for AI agents. Monitor, log, and orchestrate your agents from one dashboard.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",