oceum 0.3.0 → 0.5.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 +49 -1
  2. package/index.d.ts +65 -1
  3. package/index.js +187 -2
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # oceum
2
2
 
3
- Official SDK for [Oceum](https://oceum.ai) — governed agent infrastructure. Progressive autonomy, zero-knowledge vault, governed execution, and enterprise-grade observability.
3
+ Official SDK for [Oceum](https://oceum.ai) — governed agent infrastructure. Progressive autonomy, blind-relay vault (the agent never sees the secret), governed execution, and enterprise-grade observability.
4
4
 
5
5
  Zero dependencies. Works with Node.js 18+, Deno, and Bun.
6
6
 
@@ -148,6 +148,54 @@ try {
148
148
  }
149
149
  ```
150
150
 
151
+ ## Optimistic Concurrency (v0.5.0)
152
+
153
+ Avoid silently overwriting fresher state when two writers race for the same row.
154
+
155
+ ### When to use
156
+
157
+ - You read a vault entry, present it to the user for editing, and want to make sure no other process modified it before the user submits.
158
+ - Your agent reads a memory entry, derives a decision, and writes back — and another agent might be doing the same thing concurrently.
159
+ - You're updating an integration's config and another admin might be doing the same in another tab.
160
+
161
+ ### Pattern
162
+
163
+ ```javascript
164
+ const { Oceum, OceumConflictError } = require('oceum');
165
+ const oceum = new Oceum({ apiKey: 'oc_xxx', agentId: 'agt_xxx' });
166
+
167
+ // 1. Read with hash
168
+ const { data, hash } = await oceum.vaultReadWithHash('vtk_xxx');
169
+
170
+ // 2. Modify
171
+ const newPayload = { label: 'Updated label' };
172
+
173
+ // 3. Write expecting the original hash
174
+ try {
175
+ const { hash: newHash } = await oceum.vaultWriteExpecting('vtk_xxx', hash, newPayload);
176
+ console.log('Saved. New hash:', newHash);
177
+ } catch (err) {
178
+ if (err instanceof OceumConflictError) {
179
+ // Another writer beat you to it. err.current is the latest server state.
180
+ console.warn('Conflict! Server has:', err.current);
181
+ // Re-read, merge, retry — or surface to the user.
182
+ } else {
183
+ throw err;
184
+ }
185
+ }
186
+ ```
187
+
188
+ Same pattern for memory and integrations:
189
+ - `oceum.memoryReadWithHash(memoryId)` / `oceum.memoryWriteExpecting(memoryId, hash, payload)`
190
+ - `oceum.integrationReadWithHash(integrationId)` / `oceum.integrationWriteExpecting(integrationId, hash, payload)`
191
+
192
+ ### Important notes
193
+
194
+ - **Do NOT use on memory rows whose name starts with `pulse-`** — those are system-internal upserts and may cause spurious 409s.
195
+ - The SDK does NOT auto-retry on 409 — re-attempting with the same stale hash would just 409 again. Caller must re-read first.
196
+ - 5xx errors and network errors retry as before (exponential backoff, MAX_RETRIES=3).
197
+ - The SDK sends `If-Match: <hash>` HTTP header — RFC 7232 standard. Server returns the new hash in the response body and as an `ETag` header.
198
+
151
199
  ## Webhook Verification
152
200
 
153
201
  Verify incoming webhook signatures with HMAC-SHA256:
package/index.d.ts CHANGED
@@ -35,6 +35,33 @@ export class OceumError extends Error {
35
35
  constructor(message: string, statusCode: number, body: Record<string, unknown>);
36
36
  }
37
37
 
38
+ /** Phase 36 (v0.5.0): conflict body shape returned by writeExpecting on 409. */
39
+ export interface ConflictBody {
40
+ current: Record<string, unknown>;
41
+ currentHash: string;
42
+ attemptedHash: string;
43
+ }
44
+
45
+ /**
46
+ * Phase 36 (v0.5.0): thrown when a writeExpecting call hits a hash mismatch (409).
47
+ * Carries the current row + currentHash so the consumer can immediately
48
+ * decide whether to merge, retry, or surface to the user.
49
+ */
50
+ export class OceumConflictError extends OceumError {
51
+ /** The current row state (server-authoritative) */
52
+ readonly current: Record<string, unknown>;
53
+ /** The version_hash currently stored */
54
+ readonly currentHash: string;
55
+ /** The hash the SDK caller sent in If-Match */
56
+ readonly attemptedHash: string;
57
+ constructor(
58
+ message: string,
59
+ current: Record<string, unknown>,
60
+ currentHash: string,
61
+ attemptedHash: string,
62
+ );
63
+ }
64
+
38
65
  export class Oceum {
39
66
  constructor(config: OceumConfig);
40
67
 
@@ -86,7 +113,7 @@ export class Oceum {
86
113
  vaultRetrieve(token: string): Promise<VaultRetrieveResponse>;
87
114
 
88
115
  /**
89
- * Zero-Knowledge Vault Proxy — make an API call with a vault-stored credential
116
+ * Blind-Relay Vault Proxy — make an API call with a vault-stored credential
90
117
  * without ever seeing the raw secret.
91
118
  */
92
119
  vaultProxy(token: string, request: VaultProxyRequest): Promise<VaultProxyResponse>;
@@ -97,6 +124,43 @@ export class Oceum {
97
124
  /** List vault tokens (metadata only). */
98
125
  vaultList(options?: VaultListOptions): Promise<VaultListResponse>;
99
126
 
127
+ // ── Phase 36: Optimistic Concurrency (v0.5.0) ───────────────────
128
+
129
+ /**
130
+ * Read a vault token along with its version_hash for optimistic concurrency.
131
+ * Use the returned hash with vaultWriteExpecting to safely update the token.
132
+ */
133
+ vaultReadWithHash(token: string): Promise<{ data: string; hash: string }>;
134
+
135
+ /**
136
+ * Write a vault token expecting the row to currently match `hash`.
137
+ * Throws OceumConflictError on hash mismatch (409). Does NOT auto-retry.
138
+ */
139
+ vaultWriteExpecting(token: string, hash: string, payload: Record<string, unknown>): Promise<{ ok: boolean; hash: string }>;
140
+
141
+ /**
142
+ * Read a memory entry along with its version_hash.
143
+ * Do NOT use on memory rows whose name starts with `pulse-` (system-internal upserts).
144
+ */
145
+ memoryReadWithHash(memoryId: string): Promise<{ data: Record<string, unknown>; hash: string }>;
146
+
147
+ /**
148
+ * Write a memory entry expecting the row to currently match `hash`.
149
+ * Throws OceumConflictError on hash mismatch (409). Does NOT auto-retry.
150
+ */
151
+ memoryWriteExpecting(memoryId: string, hash: string, payload: Record<string, unknown>): Promise<{ ok: boolean; hash: string }>;
152
+
153
+ /**
154
+ * Read an integration entry along with its version_hash.
155
+ */
156
+ integrationReadWithHash(integrationId: string): Promise<{ data: Record<string, unknown>; hash: string }>;
157
+
158
+ /**
159
+ * Write an integration entry expecting the row to currently match `hash`.
160
+ * Throws OceumConflictError on hash mismatch (409). Does NOT auto-retry.
161
+ */
162
+ integrationWriteExpecting(integrationId: string, hash: string, payload: Record<string, unknown>): Promise<{ ok: boolean; hash: string }>;
163
+
100
164
  /**
101
165
  * Verify a webhook signature using HMAC-SHA256 with timing-safe comparison.
102
166
  * @returns true if the signature is valid
package/index.js CHANGED
@@ -14,6 +14,33 @@ class OceumError extends Error {
14
14
  }
15
15
  }
16
16
 
17
+ /**
18
+ * Phase 36 (v0.5.0): thrown when a writeExpecting call hits a hash mismatch (409).
19
+ * Carries the current row + currentHash so the consumer can immediately
20
+ * decide whether to merge, retry, or surface to the user.
21
+ *
22
+ * Example:
23
+ * try {
24
+ * await oceum.memoryWriteExpecting('mem-1', oldHash, { content: 'X' });
25
+ * } catch (err) {
26
+ * if (err instanceof OceumConflictError) {
27
+ * console.warn('Conflict! Server has:', err.current);
28
+ * // Re-read, merge, retry — or surface to the user.
29
+ * } else {
30
+ * throw err;
31
+ * }
32
+ * }
33
+ */
34
+ class OceumConflictError extends OceumError {
35
+ constructor(message, current, currentHash, attemptedHash) {
36
+ super(message, 409, { current, currentHash, attemptedHash });
37
+ this.name = 'OceumConflictError';
38
+ this.current = current;
39
+ this.currentHash = currentHash;
40
+ this.attemptedHash = attemptedHash;
41
+ }
42
+ }
43
+
17
44
  class Oceum {
18
45
  #apiKey;
19
46
  #agentId;
@@ -104,6 +131,83 @@ class Oceum {
104
131
  : new OceumError(lastError?.message || 'Request failed after retries', 0, {});
105
132
  }
106
133
 
134
+ /**
135
+ * Phase 36: variant of #send that adds extra HTTP headers (e.g. If-Match).
136
+ * On 409 response, throws OceumConflictError instead of OceumError.
137
+ * Does NOT auto-retry on 409 (caller must re-read first — re-attempting
138
+ * with the same stale hash would just 409 again).
139
+ */
140
+ async #sendWithHeaders(event, data = {}, headers = {}) {
141
+ let lastError;
142
+
143
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
144
+ try {
145
+ const res = await fetch(`${this.#baseUrl}/api/webhook`, {
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ 'Authorization': `Bearer ${this.#apiKey}`,
150
+ ...headers,
151
+ },
152
+ body: JSON.stringify({
153
+ event,
154
+ agentId: this.#agentId,
155
+ data,
156
+ }),
157
+ });
158
+
159
+ // 409 — never retry; throw OceumConflictError immediately.
160
+ if (res.status === 409) {
161
+ const body = await res.json().catch(() => ({}));
162
+ throw new OceumConflictError(
163
+ body.error || 'version_hash mismatch',
164
+ body.current,
165
+ body.currentHash,
166
+ body.attemptedHash,
167
+ );
168
+ }
169
+
170
+ // 429 — respect Retry-After header
171
+ if (res.status === 429 && attempt < MAX_RETRIES) {
172
+ const retryAfter = parseInt(res.headers.get('Retry-After'), 10);
173
+ const waitMs = (retryAfter > 0 ? retryAfter : 1) * 1000;
174
+ await this.#sleep(waitMs);
175
+ continue;
176
+ }
177
+
178
+ // 5xx — exponential backoff (matches #send behavior)
179
+ if (res.status >= 500 && attempt < MAX_RETRIES) {
180
+ await this.#sleep(BACKOFF_BASE_MS * Math.pow(2, attempt));
181
+ continue;
182
+ }
183
+
184
+ const body = await res.json().catch(() => ({}));
185
+
186
+ if (!res.ok) {
187
+ throw new OceumError(
188
+ body.error || `HTTP ${res.status}`,
189
+ res.status,
190
+ body,
191
+ );
192
+ }
193
+
194
+ return body;
195
+ } catch (err) {
196
+ lastError = err;
197
+ if (err instanceof OceumConflictError) throw err; // never retry conflicts
198
+ if (err instanceof OceumError) throw err;
199
+ if (attempt < MAX_RETRIES) {
200
+ await this.#sleep(BACKOFF_BASE_MS * Math.pow(2, attempt));
201
+ continue;
202
+ }
203
+ }
204
+ }
205
+
206
+ throw lastError instanceof OceumError
207
+ ? lastError
208
+ : new OceumError(lastError?.message || 'Request failed after retries', 0, {});
209
+ }
210
+
107
211
  async #get(url) {
108
212
  let lastError;
109
213
 
@@ -384,7 +488,7 @@ class Oceum {
384
488
  }
385
489
 
386
490
  /**
387
- * Zero-Knowledge Vault Proxy — make an API call with a vault-stored credential
491
+ * Blind-Relay Vault Proxy — make an API call with a vault-stored credential
388
492
  * without ever seeing the raw secret. The platform decrypts and injects the
389
493
  * credential into the outgoing request, then returns only the API response.
390
494
  *
@@ -424,10 +528,91 @@ class Oceum {
424
528
  return this.#get(`${this.#baseUrl}/api/vault?${params}`);
425
529
  }
426
530
 
531
+ // ── Phase 36: Optimistic Concurrency (v0.5.0) ──────
532
+
533
+ /**
534
+ * Read a vault token along with its version_hash for optimistic concurrency.
535
+ * Use the returned hash with vaultWriteExpecting to safely update the token
536
+ * without overwriting changes from a concurrent writer.
537
+ * @param {string} token - Vault token (vtk_xxx)
538
+ * @returns {Promise<{data: string, hash: string}>}
539
+ */
540
+ async vaultReadWithHash(token) {
541
+ const res = await this.#send('vault_retrieve', { token });
542
+ return { data: res.data, hash: res.version_hash };
543
+ }
544
+
545
+ /**
546
+ * Write a vault token expecting the row to currently match `hash`.
547
+ * Throws OceumConflictError on hash mismatch (409). Does NOT retry on 409.
548
+ * @param {string} token - Vault token (vtk_xxx)
549
+ * @param {string} hash - The hash returned from the previous vaultReadWithHash
550
+ * @param {Object} payload - Update fields (label, accessPolicy, etc.)
551
+ * @returns {Promise<{ok: boolean, hash: string}>}
552
+ */
553
+ async vaultWriteExpecting(token, hash, payload) {
554
+ if (!hash) throw new Error('oceum: vaultWriteExpecting requires a hash from vaultReadWithHash');
555
+ const res = await this.#sendWithHeaders('vault_update', { token, ...payload }, { 'If-Match': hash });
556
+ return { ok: true, hash: res.version_hash };
557
+ }
558
+
559
+ /**
560
+ * Read a memory entry along with its version_hash.
561
+ *
562
+ * NOTE: do NOT use on memory rows whose name starts with `pulse-` —
563
+ * those are system-internal upserts (Pitfall 5). Use is unsupported and
564
+ * may cause spurious 409s.
565
+ *
566
+ * @param {string} memoryId
567
+ * @returns {Promise<{data: Object, hash: string}>}
568
+ */
569
+ async memoryReadWithHash(memoryId) {
570
+ const res = await this.#send('memory_read_one', { memoryId });
571
+ return { data: res.memory, hash: res.memory && res.memory.version_hash };
572
+ }
573
+
574
+ /**
575
+ * Write a memory entry expecting the row to currently match `hash`.
576
+ * Throws OceumConflictError on hash mismatch (409). Does NOT retry on 409.
577
+ * @param {string} memoryId
578
+ * @param {string} hash
579
+ * @param {Object} payload - Update fields (content, category, etc.)
580
+ * @returns {Promise<{ok: boolean, hash: string}>}
581
+ */
582
+ async memoryWriteExpecting(memoryId, hash, payload) {
583
+ if (!hash) throw new Error('oceum: memoryWriteExpecting requires a hash from memoryReadWithHash');
584
+ const res = await this.#sendWithHeaders('memory_update', { memoryId, ...payload }, { 'If-Match': hash });
585
+ return { ok: true, hash: res.version_hash };
586
+ }
587
+
588
+ /**
589
+ * Read an integration entry along with its version_hash.
590
+ * @param {string} integrationId
591
+ * @returns {Promise<{data: Object, hash: string}>}
592
+ */
593
+ async integrationReadWithHash(integrationId) {
594
+ const res = await this.#send('integration_read', { integrationId });
595
+ return { data: res.integration, hash: res.integration && res.integration.version_hash };
596
+ }
597
+
598
+ /**
599
+ * Write an integration entry expecting the row to currently match `hash`.
600
+ * Throws OceumConflictError on hash mismatch (409). Does NOT retry on 409.
601
+ * @param {string} integrationId
602
+ * @param {string} hash
603
+ * @param {Object} payload - Update fields (config, status, agentTypes, etc.)
604
+ * @returns {Promise<{ok: boolean, hash: string}>}
605
+ */
606
+ async integrationWriteExpecting(integrationId, hash, payload) {
607
+ if (!hash) throw new Error('oceum: integrationWriteExpecting requires a hash from integrationReadWithHash');
608
+ const res = await this.#sendWithHeaders('integration_update', { integrationId, ...payload }, { 'If-Match': hash });
609
+ return { ok: true, hash: res.version_hash };
610
+ }
611
+
427
612
  // ── Getters ────────────────────────────────────────
428
613
 
429
614
  get agentId() { return this.#agentId; }
430
615
  get baseUrl() { return this.#baseUrl; }
431
616
  }
432
617
 
433
- module.exports = { Oceum, OceumError };
618
+ module.exports = { Oceum, OceumError, OceumConflictError };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "oceum",
3
- "version": "0.3.0",
4
- "description": "Official SDK for Oceum — governed agent infrastructure. Progressive autonomy, zero-knowledge vault, governed execution, and enterprise-grade observability.",
3
+ "version": "0.5.0",
4
+ "description": "Official SDK for Oceum — governed agent infrastructure. Progressive autonomy, blind-relay vault (the agent never sees the secret), governed execution, and enterprise-grade observability.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "exports": {