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.
- package/README.md +49 -1
- package/index.d.ts +65 -1
- package/index.js +187 -2
- 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,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
4
|
-
"description": "Official SDK for Oceum — governed agent infrastructure. Progressive autonomy,
|
|
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": {
|